Added RecordManager implementation to remove d2datadict singletons (#736)
* Added RecordManager implementation to remove d2datadict singletons * fix object lookup test
This commit is contained in:
parent
ef0fbc0581
commit
271673851a
|
@ -199,6 +199,7 @@ const (
|
||||||
SetItems = "/data/global/excel/SetItems.txt"
|
SetItems = "/data/global/excel/SetItems.txt"
|
||||||
AutoMagic = "/data/global/excel/automagic.txt"
|
AutoMagic = "/data/global/excel/automagic.txt"
|
||||||
BodyLocations = "/data/global/excel/bodylocs.txt"
|
BodyLocations = "/data/global/excel/bodylocs.txt"
|
||||||
|
Events = "/data/global/excel/events.txt"
|
||||||
Properties = "/data/global/excel/Properties.txt"
|
Properties = "/data/global/excel/Properties.txt"
|
||||||
Hireling = "/data/global/excel/hireling.txt"
|
Hireling = "/data/global/excel/hireling.txt"
|
||||||
DifficultyLevels = "/data/global/excel/difficultylevels.txt"
|
DifficultyLevels = "/data/global/excel/difficultylevels.txt"
|
||||||
|
|
|
@ -5,6 +5,12 @@ import (
|
||||||
"image/color"
|
"image/color"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dat"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dat"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dc6"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dc6"
|
||||||
|
@ -37,6 +43,106 @@ type AssetManager struct {
|
||||||
fonts d2interface.Cache
|
fonts d2interface.Cache
|
||||||
palettes d2interface.Cache
|
palettes d2interface.Cache
|
||||||
transforms d2interface.Cache
|
transforms d2interface.Cache
|
||||||
|
Records *d2records.RecordManager
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *AssetManager) init() error {
|
||||||
|
rm, err := d2records.NewRecordManager()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
am.Records = rm
|
||||||
|
|
||||||
|
err = am.initDataDictionaries()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *AssetManager) initDataDictionaries() error {
|
||||||
|
dictPaths := []string{
|
||||||
|
d2resource.LevelType,
|
||||||
|
d2resource.LevelPreset,
|
||||||
|
d2resource.LevelWarp,
|
||||||
|
d2resource.ObjectType,
|
||||||
|
d2resource.ObjectDetails,
|
||||||
|
d2resource.Weapons,
|
||||||
|
d2resource.Armor,
|
||||||
|
d2resource.Misc,
|
||||||
|
d2resource.Books,
|
||||||
|
d2resource.ItemTypes,
|
||||||
|
d2resource.UniqueItems,
|
||||||
|
d2resource.Missiles,
|
||||||
|
d2resource.SoundSettings,
|
||||||
|
d2resource.MonStats,
|
||||||
|
d2resource.MonStats2,
|
||||||
|
d2resource.MonPreset,
|
||||||
|
d2resource.MonProp,
|
||||||
|
d2resource.MonType,
|
||||||
|
d2resource.MonMode,
|
||||||
|
d2resource.MagicPrefix,
|
||||||
|
d2resource.MagicSuffix,
|
||||||
|
d2resource.ItemStatCost,
|
||||||
|
d2resource.ItemRatio,
|
||||||
|
d2resource.Overlays,
|
||||||
|
d2resource.CharStats,
|
||||||
|
d2resource.Hireling,
|
||||||
|
d2resource.Experience,
|
||||||
|
d2resource.Gems,
|
||||||
|
d2resource.QualityItems,
|
||||||
|
d2resource.Runes,
|
||||||
|
d2resource.DifficultyLevels,
|
||||||
|
d2resource.AutoMap,
|
||||||
|
d2resource.LevelDetails,
|
||||||
|
d2resource.LevelMaze,
|
||||||
|
d2resource.LevelSubstitutions,
|
||||||
|
d2resource.CubeRecipes,
|
||||||
|
d2resource.SuperUniques,
|
||||||
|
d2resource.Inventory,
|
||||||
|
d2resource.Skills,
|
||||||
|
d2resource.SkillCalc,
|
||||||
|
d2resource.MissileCalc,
|
||||||
|
d2resource.Properties,
|
||||||
|
d2resource.SkillDesc,
|
||||||
|
d2resource.BodyLocations,
|
||||||
|
d2resource.Sets,
|
||||||
|
d2resource.SetItems,
|
||||||
|
d2resource.AutoMagic,
|
||||||
|
d2resource.TreasureClass,
|
||||||
|
d2resource.States,
|
||||||
|
d2resource.SoundEnvirons,
|
||||||
|
d2resource.Shrines,
|
||||||
|
d2resource.ElemType,
|
||||||
|
d2resource.PlrMode,
|
||||||
|
d2resource.PetType,
|
||||||
|
d2resource.NPC,
|
||||||
|
d2resource.MonsterUniqueModifier,
|
||||||
|
d2resource.MonsterEquipment,
|
||||||
|
d2resource.UniqueAppellation,
|
||||||
|
d2resource.MonsterLevel,
|
||||||
|
d2resource.MonsterSound,
|
||||||
|
d2resource.MonsterSequence,
|
||||||
|
d2resource.PlayerClass,
|
||||||
|
d2resource.MonsterPlacement,
|
||||||
|
d2resource.ObjectGroup,
|
||||||
|
d2resource.CompCode,
|
||||||
|
d2resource.MonsterAI,
|
||||||
|
d2resource.RarePrefix,
|
||||||
|
d2resource.RareSuffix,
|
||||||
|
d2resource.Events,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, path := range dictPaths {
|
||||||
|
err := am.LoadRecords(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadAsset loads an asset
|
// LoadAsset loads an asset
|
||||||
|
@ -244,6 +350,37 @@ func (am *AssetManager) LoadPaletteTransform(path string) (*d2pl2.PL2, error) {
|
||||||
return pl2, nil
|
return pl2, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadDataDictionary loads a txt data file
|
||||||
|
func (am *AssetManager) LoadDataDictionary(path string) (*d2txt.DataDictionary, error) {
|
||||||
|
// we purposefully do not cache data dictionaries because we are already
|
||||||
|
// caching the file data. The underlying csv.Reader does not implement io.Seeker,
|
||||||
|
// so after it has been iterated through, we cannot iterate through it again.
|
||||||
|
//
|
||||||
|
// The easy way around this is to not cache d2txt.DataDictionary objects, and just create
|
||||||
|
// a new instance from cached file data if/when we ever need to reload the data dict
|
||||||
|
if data, err := am.LoadFile(path); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else {
|
||||||
|
return d2txt.LoadDataDictionary(data), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadRecords will load the records for the given path into the record manager.
|
||||||
|
// This is dependant on the record manager having bound a loader for the given path.
|
||||||
|
func (am *AssetManager) LoadRecords(path string) error {
|
||||||
|
dict, err := am.LoadDataDictionary(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = am.Records.Load(path, dict)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// loadDC6 creates an Animation from d2dc6.DC6 and d2dat.DATPalette
|
// loadDC6 creates an Animation from d2dc6.DC6 and d2dat.DATPalette
|
||||||
func (am *AssetManager) loadDC6(path string,
|
func (am *AssetManager) loadDC6(path string,
|
||||||
palette d2interface.Palette, effect d2enum.DrawEffect) (d2interface.Animation, error) {
|
palette d2interface.Palette, effect d2enum.DrawEffect) (d2interface.Animation, error) {
|
||||||
|
|
|
@ -4,17 +4,24 @@ import (
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2cache"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2cache"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader"
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2loader"
|
||||||
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2config"
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2config"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewAssetManager creates and assigns all necessary dependencies for the AssetManager top-level functions to work correctly
|
// NewAssetManager creates and assigns all necessary dependencies for the AssetManager top-level functions to work correctly
|
||||||
func NewAssetManager(config *d2config.Configuration) (*AssetManager, error) {
|
func NewAssetManager(config *d2config.Configuration) (*AssetManager, error) {
|
||||||
manager := &AssetManager{
|
manager := &AssetManager{
|
||||||
d2loader.NewLoader(config),
|
d2loader.NewLoader(config),
|
||||||
d2cache.CreateCache(animationBudget),
|
|
||||||
d2cache.CreateCache(tableBudget),
|
d2cache.CreateCache(tableBudget),
|
||||||
|
d2cache.CreateCache(animationBudget),
|
||||||
d2cache.CreateCache(fontBudget),
|
d2cache.CreateCache(fontBudget),
|
||||||
d2cache.CreateCache(paletteBudget),
|
d2cache.CreateCache(paletteBudget),
|
||||||
d2cache.CreateCache(paletteTransformBudget),
|
d2cache.CreateCache(paletteTransformBudget),
|
||||||
|
&d2records.RecordManager{},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := manager.init()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return manager, nil
|
return manager, nil
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func autoMagicLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(AutoMagic, 0)
|
||||||
|
|
||||||
|
charCodeMap := map[string]d2enum.Hero{
|
||||||
|
"ama": d2enum.HeroAmazon,
|
||||||
|
"ass": d2enum.HeroAssassin,
|
||||||
|
"bar": d2enum.HeroBarbarian,
|
||||||
|
"dru": d2enum.HeroDruid,
|
||||||
|
"nec": d2enum.HeroNecromancer,
|
||||||
|
"pal": d2enum.HeroPaladin,
|
||||||
|
"sor": d2enum.HeroSorceress,
|
||||||
|
}
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &AutoMagicRecord{
|
||||||
|
Name: d.String("Name"),
|
||||||
|
Version: d.Number("version"),
|
||||||
|
Spawnable: d.Number("spawnable") > 0,
|
||||||
|
SpawnOnRare: d.Number("rare") > 0,
|
||||||
|
MinSpawnLevel: d.Number("level"),
|
||||||
|
MaxSpawnLevel: d.Number("maxlevel"),
|
||||||
|
LevelRequirement: d.Number("levelreq"),
|
||||||
|
Class: charCodeMap[d.String("class")],
|
||||||
|
ClassLevelRequirement: d.Number("classlevelreq"),
|
||||||
|
Frequency: d.Number("frequency"),
|
||||||
|
Group: d.Number("group"),
|
||||||
|
ModCode: [3]string{
|
||||||
|
d.String("mod1code"),
|
||||||
|
d.String("mod2code"),
|
||||||
|
d.String("mod3code"),
|
||||||
|
},
|
||||||
|
ModParam: [3]int{
|
||||||
|
d.Number("mod1param"),
|
||||||
|
d.Number("mod2param"),
|
||||||
|
d.Number("mod3param"),
|
||||||
|
},
|
||||||
|
ModMin: [3]int{
|
||||||
|
d.Number("mod1min"),
|
||||||
|
d.Number("mod2min"),
|
||||||
|
d.Number("mod3min"),
|
||||||
|
},
|
||||||
|
ModMax: [3]int{
|
||||||
|
d.Number("mod1max"),
|
||||||
|
d.Number("mod2max"),
|
||||||
|
d.Number("mod3max"),
|
||||||
|
},
|
||||||
|
Transform: d.Number("transform") > 0,
|
||||||
|
PaletteTransform: d.Number("transformcolor"),
|
||||||
|
IncludeItemCodes: [7]string{
|
||||||
|
d.String("itype1"),
|
||||||
|
d.String("itype2"),
|
||||||
|
d.String("itype3"),
|
||||||
|
d.String("itype4"),
|
||||||
|
d.String("itype5"),
|
||||||
|
d.String("itype6"),
|
||||||
|
d.String("itype7"),
|
||||||
|
},
|
||||||
|
ExcludeItemCodes: [3]string{
|
||||||
|
d.String("etype1"),
|
||||||
|
d.String("etype2"),
|
||||||
|
d.String("etype3"),
|
||||||
|
},
|
||||||
|
CostDivide: d.Number("divide"),
|
||||||
|
CostMultiply: d.Number("multiply"),
|
||||||
|
CostAdd: d.Number("add"),
|
||||||
|
}
|
||||||
|
|
||||||
|
records = append(records, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d AutoMagic records", len(records))
|
||||||
|
|
||||||
|
r.Item.AutoMagic = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
|
||||||
|
// AutoMagic has all of the AutoMagicRecords, used for generating magic properties for spawned items
|
||||||
|
type AutoMagic []*AutoMagicRecord
|
||||||
|
|
||||||
|
// AutoMagicRecord describes rules for automatically generating magic properties when spawning
|
||||||
|
// items
|
||||||
|
type AutoMagicRecord struct {
|
||||||
|
// IncludeItemCodes
|
||||||
|
// itype 1 to itype7
|
||||||
|
// "Include Type" fields. You need to place item codes in any of these columns to allow that item
|
||||||
|
// to receive mods from this row. See the note below.
|
||||||
|
IncludeItemCodes [7]string
|
||||||
|
|
||||||
|
// ModCode
|
||||||
|
// They're the Property codes from Properties.txt.
|
||||||
|
// These determine the actual properties which make up this autoprefix.
|
||||||
|
// Each autoprefix can include up to three modifiers.
|
||||||
|
ModCode [3]string
|
||||||
|
|
||||||
|
// ExcludeItemCodes
|
||||||
|
// etype 1 to etype3
|
||||||
|
// 'Exclude type' . This field prevents certain mods from spawning on specific item codes.
|
||||||
|
ExcludeItemCodes [3]string
|
||||||
|
|
||||||
|
// ModParam, min, max
|
||||||
|
// Parameter, min, and max values for the property
|
||||||
|
ModParam [3]int
|
||||||
|
ModMin [3]int
|
||||||
|
ModMax [3]int
|
||||||
|
|
||||||
|
// Name
|
||||||
|
// String Comment Blizzard lists the equivalent prefix/affix here.
|
||||||
|
// You can use what ever you wish here though. Handy for keeping track of groups.
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// Version
|
||||||
|
// it needs to be set to 0 if the prefix\affix you want to create or edit is going to be a
|
||||||
|
// classic-only item ( with "classic" we mean "non-expansion" mode,
|
||||||
|
// which you can toggle on and off when creating a character) or set to 100 if it's going to be
|
||||||
|
// available in Expansion. This field is important,
|
||||||
|
// as Items with " version" set to 100 will NOT be generated in Classic Diablo II.
|
||||||
|
Version int
|
||||||
|
|
||||||
|
// MinSpawnLevel
|
||||||
|
// this field accepts numeric values and specifies the minimum level from which this autoprefix
|
||||||
|
// can spawn. The column in question can be combined with the following maxlevel: to effectively
|
||||||
|
// control groups of automods,
|
||||||
|
// because you can use this field to combine multiple rows so that the autoprefixes are assigned
|
||||||
|
// based on the level of the treasure drop [see below].
|
||||||
|
MinSpawnLevel int
|
||||||
|
|
||||||
|
// MaxSpawnLevel
|
||||||
|
// this field accepts numeric values and specifies the maximum level beyond which the automod
|
||||||
|
// stop spawning.
|
||||||
|
MaxSpawnLevel int
|
||||||
|
|
||||||
|
// LevelRequirement
|
||||||
|
// It is the level requirement for this autoprefix.
|
||||||
|
// This value is added to the Level Requirement of the item generated with this mod.
|
||||||
|
LevelRequirement int
|
||||||
|
|
||||||
|
// Class
|
||||||
|
// the class type
|
||||||
|
Class d2enum.Hero
|
||||||
|
|
||||||
|
// ClassLevelRequirement
|
||||||
|
// If class is set, this should allow a separate level requirement for this class.
|
||||||
|
// This is a polite thing to do,
|
||||||
|
// as other classes gain no advantage from class specific modifiers.
|
||||||
|
// I am uncertain that this actually works.
|
||||||
|
ClassLevelRequirement int
|
||||||
|
|
||||||
|
// Frequency
|
||||||
|
// For autoprefix groups, it states the chance to spawn this specific group member vs others.
|
||||||
|
// Higher numbers means the automod will be more common. The 1.
|
||||||
|
// 09 version file guide has some formuae relateing to this.
|
||||||
|
Frequency int
|
||||||
|
|
||||||
|
// Group
|
||||||
|
// This field accepts numeric values and groups all the lines with the same values,
|
||||||
|
// which are treated as a group. Only one autoprefix per group can be chosen,
|
||||||
|
// and groups are influenced by levelreq, classlevelreq and frequency The 1.
|
||||||
|
// 09 version file guide has a very nice tutorial about how to set up groups.
|
||||||
|
// NOTE: The group number must also be entered in the 'auto prefix' column of each entry in
|
||||||
|
// Weapons.txt or Armor.txt in order for the property to appear.
|
||||||
|
Group int
|
||||||
|
|
||||||
|
// PaletteTransform
|
||||||
|
// If transform is set to 1 then the item will be colored with the chosen color code,
|
||||||
|
// taken from Colors.txt
|
||||||
|
PaletteTransform int
|
||||||
|
|
||||||
|
// CostDivide
|
||||||
|
// Numeric value that acts as divisor for the item price.
|
||||||
|
CostDivide int
|
||||||
|
|
||||||
|
// CostMultiply
|
||||||
|
// Numeric value that acts as multiplier for the item price.
|
||||||
|
CostMultiply int
|
||||||
|
|
||||||
|
// CostAdd
|
||||||
|
// Numeric value that acts as a flat sum added to the item price.
|
||||||
|
CostAdd int
|
||||||
|
|
||||||
|
// Spawnable
|
||||||
|
// It is a boolean type field, and states if this autoprefix can actually spawn in the game.
|
||||||
|
// You can disable this row by setting it to 0 , or enable it by setting it to 1
|
||||||
|
Spawnable bool
|
||||||
|
|
||||||
|
// SpawnOnRare
|
||||||
|
// It decides whether this autoprefix spawns on rare quality items or not.
|
||||||
|
// You can prevent that from happening by setting it to 0 , or you can allow it by setting it to 1
|
||||||
|
SpawnOnRare bool
|
||||||
|
|
||||||
|
// transform
|
||||||
|
// It is a boolean value whichallows the colorization of the items.
|
||||||
|
Transform bool
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func autoMapLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(AutoMaps, 0)
|
||||||
|
|
||||||
|
var frameFields = []string{"Cel1", "Cel2", "Cel3", "Cel4"}
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &AutoMapRecord{
|
||||||
|
LevelName: d.String("LevelName"),
|
||||||
|
TileName: d.String("TileName"),
|
||||||
|
|
||||||
|
Style: d.Number("Style"),
|
||||||
|
StartSequence: d.Number("StartSequence"),
|
||||||
|
EndSequence: d.Number("EndSequence"),
|
||||||
|
|
||||||
|
// Note: aren't useful see the AutoMapRecord struct.
|
||||||
|
//Type1: d.String("Type1"),
|
||||||
|
//Type2: d.String("Type2"),
|
||||||
|
//Type3: d.String("Type3"),
|
||||||
|
//Type4: d.String("Type4"),
|
||||||
|
}
|
||||||
|
record.Frames = make([]int, len(frameFields))
|
||||||
|
|
||||||
|
for i := range frameFields {
|
||||||
|
record.Frames[i] = d.Number(frameFields[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
records = append(records, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d AutoMapRecord records", len(records))
|
||||||
|
|
||||||
|
r.Level.AutoMaps = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// AutoMapRecord represents one row from d2data.mpq/AutoMap.txt.
|
||||||
|
// Based on the information here https://d2mods.info/forum/kb/viewarticle?a=419
|
||||||
|
type AutoMapRecord struct {
|
||||||
|
// LevelName is a string with an act number followed
|
||||||
|
// by a level type, separated by a space. For example:
|
||||||
|
// '1 Barracks' is the barracks level in act 1.
|
||||||
|
LevelName string
|
||||||
|
|
||||||
|
// TileName refers to a certain tile orientation.
|
||||||
|
// See https://d2mods.info/forum/kb/viewarticle?a=468
|
||||||
|
TileName string
|
||||||
|
|
||||||
|
// Style is the top index in a 2D tile array.
|
||||||
|
Style int // tiles[autoMapRecord.Style][]
|
||||||
|
|
||||||
|
// StartSequence and EndSequence are sub indices the
|
||||||
|
// same 2D array as Style. They describe a range of
|
||||||
|
// tiles for which covered by this AutoMapRecord.
|
||||||
|
// In some rows you can find a value of -1. This means
|
||||||
|
// the game will only look at Style and TileName to
|
||||||
|
// determine which tiles are addressed.
|
||||||
|
StartSequence int // tiles[][autoMapRecord.StartSequence]
|
||||||
|
EndSequence int // tiles[][autoMapRecord.EndSequence]
|
||||||
|
|
||||||
|
// Type values are described as:
|
||||||
|
// "...just comment fields, as far as I know. Put in
|
||||||
|
// whatever you like..."
|
||||||
|
// The values seem functional but naming conventions
|
||||||
|
// vary between LevelNames.
|
||||||
|
// Type1 string
|
||||||
|
// Type2 string
|
||||||
|
// Type3 string
|
||||||
|
// Type4 string // Note: I commented these out for now because they supposedly aren't useful see the LoadAutoMaps function.
|
||||||
|
|
||||||
|
// Frames determine the frame of the MaxiMap(s).dc6 that
|
||||||
|
// will be applied to the specified tiles. The frames
|
||||||
|
// are in rows, if each row holds 20 images (when you
|
||||||
|
// re-extract the chart with Dc6Table, you can specify
|
||||||
|
// how many graphics a line can hold), line 1 includes
|
||||||
|
// icons 0-19, line 2 from 20 to 39 etc.
|
||||||
|
// Multiple values exist for Cel (and Type) to enable
|
||||||
|
// variation. Presumably game chooses randomly between
|
||||||
|
// any of the 4 values which are not set to -1.
|
||||||
|
Frames []int
|
||||||
|
}
|
||||||
|
|
||||||
|
// AutoMaps contains all data in AutoMap.txt.
|
||||||
|
type AutoMaps []*AutoMapRecord
|
|
@ -0,0 +1,29 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func bodyLocationsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(BodyLocations)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
location := &BodyLocationRecord{
|
||||||
|
Name: d.String("Name"),
|
||||||
|
Code: d.String("Code"),
|
||||||
|
}
|
||||||
|
records[location.Code] = location
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
panic(d.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d Body Location records", len(records))
|
||||||
|
|
||||||
|
r.BodyLocations = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// BodyLocations contains the body location records
|
||||||
|
type BodyLocations map[string]*BodyLocationRecord
|
||||||
|
|
||||||
|
// BodyLocationRecord describes a body location that items can be equipped to
|
||||||
|
type BodyLocationRecord struct {
|
||||||
|
Name string
|
||||||
|
Code string
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func booksLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(Books)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &BooksRecord{
|
||||||
|
Name: d.String("Name"),
|
||||||
|
Namco: d.String("Namco"),
|
||||||
|
Completed: d.String("Completed"),
|
||||||
|
ScrollSpellCode: d.String("ScrollSpellCode"),
|
||||||
|
BookSpellCode: d.String("BooksSpellCode"),
|
||||||
|
Pspell: d.Number("pSpell"),
|
||||||
|
SpellIcon: d.Number("SpellIcon"),
|
||||||
|
ScrollSkill: d.String("ScrollSkill"),
|
||||||
|
BookSkill: d.String("BookSkill"),
|
||||||
|
BaseCost: d.Number("BaseCost"),
|
||||||
|
CostPerCharge: d.Number("CostPerCharge"),
|
||||||
|
}
|
||||||
|
records[record.Namco] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
panic(d.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d book items", len(records))
|
||||||
|
|
||||||
|
r.Item.Books = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// Books stores all of the BooksRecords
|
||||||
|
type Books map[string]*BooksRecord
|
||||||
|
|
||||||
|
// BooksRecord is a representation of a row from books.txt
|
||||||
|
type BooksRecord struct {
|
||||||
|
Name string
|
||||||
|
Namco string // The displayed name, where the string prefix is "Tome"
|
||||||
|
Completed string
|
||||||
|
ScrollSpellCode string
|
||||||
|
BookSpellCode string
|
||||||
|
Pspell int
|
||||||
|
SpellIcon int
|
||||||
|
ScrollSkill string
|
||||||
|
BookSkill string
|
||||||
|
BaseCost int
|
||||||
|
CostPerCharge int
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func skillCalcLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records, err := loadCalculations(d)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d Skill Calculation records", len(records))
|
||||||
|
|
||||||
|
r.Calculation.Skills = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func missileCalcLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records, err := loadCalculations(d)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d Missile Calculation records", len(records))
|
||||||
|
|
||||||
|
r.Calculation.Missiles = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadCalculations(d *d2txt.DataDictionary) (Calculations, error) {
|
||||||
|
records := make(Calculations)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &CalculationRecord{
|
||||||
|
Code: d.String("code"),
|
||||||
|
Description: d.String("*desc"),
|
||||||
|
}
|
||||||
|
records[record.Code] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return nil, d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d Skill Calculation records", len(records))
|
||||||
|
|
||||||
|
return records, nil
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// Calculations is where calculation records are stored
|
||||||
|
type Calculations map[string]*CalculationRecord
|
||||||
|
|
||||||
|
// CalculationRecord The skillcalc.txt and misscalc.txt files are essentially lookup tables
|
||||||
|
// for the Skills.txt and Missiles.txt Calc functions To avoid duplication (since they have
|
||||||
|
// identical fields) they are both represented by the CalculationRecord type
|
||||||
|
type CalculationRecord struct {
|
||||||
|
Code string
|
||||||
|
Description string
|
||||||
|
}
|
|
@ -0,0 +1,145 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func charStatsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(CharStats)
|
||||||
|
|
||||||
|
stringMap := map[string]d2enum.Hero{
|
||||||
|
"Amazon": d2enum.HeroAmazon,
|
||||||
|
"Barbarian": d2enum.HeroBarbarian,
|
||||||
|
"Druid": d2enum.HeroDruid,
|
||||||
|
"Assassin": d2enum.HeroAssassin,
|
||||||
|
"Necromancer": d2enum.HeroNecromancer,
|
||||||
|
"Paladin": d2enum.HeroPaladin,
|
||||||
|
"Sorceress": d2enum.HeroSorceress,
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenMap := map[string]d2enum.WeaponClass{
|
||||||
|
"": d2enum.WeaponClassNone,
|
||||||
|
"hth": d2enum.WeaponClassHandToHand,
|
||||||
|
"bow": d2enum.WeaponClassBow,
|
||||||
|
"1hs": d2enum.WeaponClassOneHandSwing,
|
||||||
|
"1ht": d2enum.WeaponClassOneHandThrust,
|
||||||
|
"stf": d2enum.WeaponClassStaff,
|
||||||
|
"2hs": d2enum.WeaponClassTwoHandSwing,
|
||||||
|
"2ht": d2enum.WeaponClassTwoHandThrust,
|
||||||
|
"xbw": d2enum.WeaponClassCrossbow,
|
||||||
|
"1js": d2enum.WeaponClassLeftJabRightSwing,
|
||||||
|
"1jt": d2enum.WeaponClassLeftJabRightThrust,
|
||||||
|
"1ss": d2enum.WeaponClassLeftSwingRightSwing,
|
||||||
|
"1st": d2enum.WeaponClassLeftSwingRightThrust,
|
||||||
|
"ht1": d2enum.WeaponClassOneHandToHand,
|
||||||
|
"ht2": d2enum.WeaponClassTwoHandToHand,
|
||||||
|
}
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &CharStatsRecord{
|
||||||
|
Class: stringMap[d.String("class")],
|
||||||
|
|
||||||
|
InitStr: d.Number("str"),
|
||||||
|
InitDex: d.Number("dex"),
|
||||||
|
InitVit: d.Number("vit"),
|
||||||
|
InitEne: d.Number("int"),
|
||||||
|
InitStamina: d.Number("stamina"),
|
||||||
|
|
||||||
|
ManaRegen: d.Number("ManaRegen"),
|
||||||
|
ToHitFactor: d.Number("ToHitFactor"),
|
||||||
|
|
||||||
|
VelocityWalk: d.Number("WalkVelocity"),
|
||||||
|
VelocityRun: d.Number("RunVelocity"),
|
||||||
|
StaminaRunDrain: d.Number("RunDrain"),
|
||||||
|
|
||||||
|
LifePerLevel: d.Number("LifePerLevel"),
|
||||||
|
ManaPerLevel: d.Number("ManaPerLevel"),
|
||||||
|
StaminaPerLevel: d.Number("StaminaPerLevel"),
|
||||||
|
|
||||||
|
LifePerVit: d.Number("LifePerVitality"),
|
||||||
|
ManaPerEne: d.Number("ManaPerMagic"),
|
||||||
|
StaminaPerVit: d.Number("StaminaPerVitality"),
|
||||||
|
|
||||||
|
StatPerLevel: d.Number("StatPerLevel"),
|
||||||
|
BlockFactor: d.Number("BlockFactor"),
|
||||||
|
|
||||||
|
StartSkillBonus: d.String("StartSkill"),
|
||||||
|
SkillStrAll: d.String("StrAllSkills"),
|
||||||
|
SkillStrClassOnly: d.String("StrClassOnly"),
|
||||||
|
|
||||||
|
BaseSkill: [10]string{
|
||||||
|
d.String("Skill 1"),
|
||||||
|
d.String("Skill 2"),
|
||||||
|
d.String("Skill 3"),
|
||||||
|
d.String("Skill 4"),
|
||||||
|
d.String("Skill 5"),
|
||||||
|
d.String("Skill 6"),
|
||||||
|
d.String("Skill 7"),
|
||||||
|
d.String("Skill 8"),
|
||||||
|
d.String("Skill 9"),
|
||||||
|
d.String("Skill 10"),
|
||||||
|
},
|
||||||
|
|
||||||
|
SkillStrTab: [3]string{
|
||||||
|
d.String("StrSkillTab1"),
|
||||||
|
d.String("StrSkillTab2"),
|
||||||
|
d.String("StrSkillTab3"),
|
||||||
|
},
|
||||||
|
|
||||||
|
BaseWeaponClass: tokenMap[d.String("baseWClass")],
|
||||||
|
|
||||||
|
StartItem: [10]string{
|
||||||
|
d.String("item1"),
|
||||||
|
d.String("item2"),
|
||||||
|
d.String("item3"),
|
||||||
|
d.String("item4"),
|
||||||
|
d.String("item5"),
|
||||||
|
d.String("item6"),
|
||||||
|
d.String("item7"),
|
||||||
|
d.String("item8"),
|
||||||
|
d.String("item9"),
|
||||||
|
d.String("item10"),
|
||||||
|
},
|
||||||
|
|
||||||
|
StartItemLocation: [10]string{
|
||||||
|
d.String("item1loc"),
|
||||||
|
d.String("item2loc"),
|
||||||
|
d.String("item3loc"),
|
||||||
|
d.String("item4loc"),
|
||||||
|
d.String("item5loc"),
|
||||||
|
d.String("item6loc"),
|
||||||
|
d.String("item7loc"),
|
||||||
|
d.String("item8loc"),
|
||||||
|
d.String("item9loc"),
|
||||||
|
d.String("item10loc"),
|
||||||
|
},
|
||||||
|
|
||||||
|
StartItemCount: [10]int{
|
||||||
|
d.Number("item1count"),
|
||||||
|
d.Number("item2count"),
|
||||||
|
d.Number("item3count"),
|
||||||
|
d.Number("item4count"),
|
||||||
|
d.Number("item5count"),
|
||||||
|
d.Number("item6count"),
|
||||||
|
d.Number("item7count"),
|
||||||
|
d.Number("item8count"),
|
||||||
|
d.Number("item9count"),
|
||||||
|
d.Number("item10count"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
records[record.Class] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d CharStats records", len(records))
|
||||||
|
|
||||||
|
r.Character.Stats = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
|
||||||
|
// CharStats holds all of the CharStatsRecords
|
||||||
|
type CharStats map[d2enum.Hero]*CharStatsRecord
|
||||||
|
|
||||||
|
// CharStatsRecord is a struct that represents a single row from charstats.txt
|
||||||
|
type CharStatsRecord struct {
|
||||||
|
Class d2enum.Hero
|
||||||
|
|
||||||
|
// the initial stats at character level 1
|
||||||
|
InitStr int // initial strength
|
||||||
|
InitDex int // initial dexterity
|
||||||
|
InitVit int // initial vitality
|
||||||
|
InitEne int // initial energy
|
||||||
|
InitStamina int // initial stamina
|
||||||
|
|
||||||
|
ManaRegen int // number of seconds to regen mana completely
|
||||||
|
ToHitFactor int // added to basic AR of character class
|
||||||
|
|
||||||
|
VelocityWalk int // velocity of the character while walking
|
||||||
|
VelocityRun int // velocity of the character while running
|
||||||
|
StaminaRunDrain int // rate of stamina loss, lower is longer drain time
|
||||||
|
|
||||||
|
// NOTE: Each point of Life/Mana/Stamina is divided by 256 for precision.
|
||||||
|
// value is in fourths, lowest possible is 64/256
|
||||||
|
LifePerLevel int // amount of life per character level
|
||||||
|
ManaPerLevel int // amount of mana per character level
|
||||||
|
StaminaPerLevel int // amount of stamina per character level
|
||||||
|
|
||||||
|
LifePerVit int // life per point of vitality
|
||||||
|
ManaPerEne int // mana per point of energy
|
||||||
|
StaminaPerVit int // stamina per point of vitality
|
||||||
|
|
||||||
|
StatPerLevel int // amount of stat points per level
|
||||||
|
|
||||||
|
BlockFactor int // added to base shield block% in armor.txt (display & calc)
|
||||||
|
|
||||||
|
// appears on starting weapon
|
||||||
|
StartSkillBonus string // a key that points to a property
|
||||||
|
|
||||||
|
// The skills the character class starts with (always available)
|
||||||
|
BaseSkill [10]string // the base skill keys of the character, always available
|
||||||
|
|
||||||
|
// string for bonus to class skills (ex: +1 to all Amazon skills).
|
||||||
|
SkillStrAll string // string for bonus to all skills
|
||||||
|
SkillStrTab [3]string // string for bonus per skill tabs
|
||||||
|
SkillStrClassOnly string // string for class-exclusive skills
|
||||||
|
|
||||||
|
BaseWeaponClass d2enum.WeaponClass // controls animation when unarmed
|
||||||
|
|
||||||
|
StartItem [10]string // tokens for the starting items
|
||||||
|
StartItemLocation [10]string // locations of the starting items
|
||||||
|
StartItemCount [10]int // amount of the starting items
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func componentCodesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(ComponentCodes)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &ComponentCodeRecord{
|
||||||
|
Component: d.String("component"),
|
||||||
|
Code: d.String("code"),
|
||||||
|
}
|
||||||
|
records[record.Component] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d ComponentCode records", len(records))
|
||||||
|
|
||||||
|
r.ComponentCodes = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// ComponentCodes is a lookup table for DCC Animation Component Subtype,
|
||||||
|
// it links hardcoded data with the txt files
|
||||||
|
type ComponentCodes map[string]*ComponentCodeRecord
|
||||||
|
|
||||||
|
// ComponentCodeRecord represents a single row from compcode.txt
|
||||||
|
type ComponentCodeRecord struct {
|
||||||
|
Component string
|
||||||
|
Code string
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// these show up in a lot of txt files where blizzard added LoD expansion stuff
|
||||||
|
const (
|
||||||
|
expansionString = "Expansion" // used in the txt files to denote where LoD expansion data starts
|
||||||
|
expansionCode = 100 // a version code for LoD expansion content
|
||||||
|
)
|
|
@ -0,0 +1,184 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func cubeRecipeLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make([]*CubeRecipeRecord, 0)
|
||||||
|
|
||||||
|
// There are repeated fields and sections in this file, some
|
||||||
|
// of which have inconsistent naming conventions. These slices
|
||||||
|
// are a simple way to handle them.
|
||||||
|
var outputFields = []string{"output", "output b", "output c"}
|
||||||
|
|
||||||
|
var outputLabels = []string{"", "b ", "c "}
|
||||||
|
|
||||||
|
var propLabels = []string{"mod 1", "mod 2", "mod 3", "mod 4", "mod 5"}
|
||||||
|
|
||||||
|
var inputFields = []string{"input 1", "input 2", "input 3", "input 4", "input 5", "input 6", "input 7"}
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &CubeRecipeRecord{
|
||||||
|
Description: d.String("description"),
|
||||||
|
|
||||||
|
Enabled: d.Bool("enabled"),
|
||||||
|
Ladder: d.Bool("ladder"),
|
||||||
|
|
||||||
|
MinDiff: d.Number("min diff"),
|
||||||
|
Version: d.Number("version"),
|
||||||
|
|
||||||
|
ReqStatID: d.Number("param"),
|
||||||
|
ReqOperation: d.Number("op"),
|
||||||
|
ReqValue: d.Number("value"),
|
||||||
|
|
||||||
|
Class: classFieldToEnum(d.String("class")),
|
||||||
|
|
||||||
|
NumInputs: d.Number("numinputs"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create inputs - input 1-7
|
||||||
|
record.Inputs = make([]CubeRecipeItem, len(inputFields))
|
||||||
|
for i := range inputFields {
|
||||||
|
record.Inputs[i] = newCubeRecipeItem(
|
||||||
|
d.String(inputFields[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create outputs - output "", b, c
|
||||||
|
record.Outputs = make([]CubeRecipeResult, len(outputLabels))
|
||||||
|
for o, outLabel := range outputLabels {
|
||||||
|
record.Outputs[o] = CubeRecipeResult{
|
||||||
|
Item: newCubeRecipeItem(
|
||||||
|
d.String(outputFields[o])),
|
||||||
|
|
||||||
|
Level: d.Number(outLabel + "lvl"),
|
||||||
|
ILevel: d.Number(outLabel + "plvl"),
|
||||||
|
PLevel: d.Number(outLabel + "ilvl"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create properties - mod 1-5
|
||||||
|
properties := make([]CubeRecipeItemProperty, len(propLabels))
|
||||||
|
for p, prop := range propLabels {
|
||||||
|
properties[p] = CubeRecipeItemProperty{
|
||||||
|
Code: d.String(outLabel + prop),
|
||||||
|
Chance: d.Number(outLabel + prop + " chance"),
|
||||||
|
Param: d.Number(outLabel + prop + " param"),
|
||||||
|
Min: d.Number(outLabel + prop + " min"),
|
||||||
|
Max: d.Number(outLabel + prop + " max"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record.Outputs[o].Properties = properties
|
||||||
|
}
|
||||||
|
|
||||||
|
records = append(records, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d CubeMainRecord records", len(records))
|
||||||
|
|
||||||
|
r.Item.Recipes = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newCubeRecipeItem constructs a CubeRecipeItem from a string of
|
||||||
|
// arguments. arguments include at least an item and sometimes
|
||||||
|
// parameters and/or a count (qty parameter). For example:
|
||||||
|
// "weap,sock,mag,qty=10"
|
||||||
|
func newCubeRecipeItem(f string) CubeRecipeItem {
|
||||||
|
args := splitFieldValue(f)
|
||||||
|
|
||||||
|
item := CubeRecipeItem{
|
||||||
|
Code: args[0], // the first argument is always the item count
|
||||||
|
Count: 1, // default to a count of 1 (no qty parameter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore the first argument
|
||||||
|
args = args[1:]
|
||||||
|
|
||||||
|
// Find the qty parameter if it was provided,
|
||||||
|
// convert to int and assign to item.Count
|
||||||
|
for idx, arg := range args {
|
||||||
|
if !strings.HasPrefix(arg, "qty") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := strconv.Atoi(strings.Split(arg, "=")[1])
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error parsing item count:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
item.Count = count
|
||||||
|
|
||||||
|
// Remove the qty parameter
|
||||||
|
if idx != len(args)-1 {
|
||||||
|
args[idx] = args[len(args)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
args = args[:len(args)-1]
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// No other arguments were provided
|
||||||
|
if len(args) == 0 {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record the argument strings
|
||||||
|
item.Params = make([]string, len(args))
|
||||||
|
for idx, arg := range args {
|
||||||
|
item.Params[idx] = arg
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
// classFieldToEnum converts class tokens to s2enum.Hero.
|
||||||
|
func classFieldToEnum(f string) []d2enum.Hero {
|
||||||
|
split := splitFieldValue(f)
|
||||||
|
enums := make([]d2enum.Hero, len(split))
|
||||||
|
|
||||||
|
for idx, class := range split {
|
||||||
|
if class == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch class {
|
||||||
|
case "bar":
|
||||||
|
enums[idx] = d2enum.HeroBarbarian
|
||||||
|
case "nec":
|
||||||
|
enums[idx] = d2enum.HeroNecromancer
|
||||||
|
case "pal":
|
||||||
|
enums[idx] = d2enum.HeroPaladin
|
||||||
|
case "ass":
|
||||||
|
enums[idx] = d2enum.HeroAssassin
|
||||||
|
case "sor":
|
||||||
|
enums[idx] = d2enum.HeroSorceress
|
||||||
|
case "ama":
|
||||||
|
enums[idx] = d2enum.HeroAmazon
|
||||||
|
case "dru":
|
||||||
|
enums[idx] = d2enum.HeroDruid
|
||||||
|
default:
|
||||||
|
log.Fatalf("Unknown hero token: '%s'", class)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return enums
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitFieldValue splits a string array from the following format:
|
||||||
|
// "one,two,three"
|
||||||
|
func splitFieldValue(s string) []string {
|
||||||
|
return strings.Split(strings.Trim(s, "\""), ",")
|
||||||
|
}
|
|
@ -0,0 +1,136 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
|
||||||
|
// CubeRecipes contains all rows in CubeMain.txt.
|
||||||
|
type CubeRecipes []*CubeRecipeRecord
|
||||||
|
|
||||||
|
// CubeRecipeRecord represents one row from CubeMain.txt.
|
||||||
|
// It is one possible recipe for the Horadric Cube, with
|
||||||
|
// requirements and output items.
|
||||||
|
// See: https://d2mods.info/forum/kb/viewarticle?a=284
|
||||||
|
type CubeRecipeRecord struct {
|
||||||
|
// Description has no function, it just describes the
|
||||||
|
// recipe.
|
||||||
|
Description string
|
||||||
|
|
||||||
|
// Enabled is true if the recipe is active in game.
|
||||||
|
Enabled bool
|
||||||
|
|
||||||
|
// Ladder is true if the recipe is only allowed in
|
||||||
|
// ladder on realms. Also works for single player
|
||||||
|
// TCP/IP.
|
||||||
|
Ladder bool
|
||||||
|
|
||||||
|
// MinDiff sets the minimum difficulty level required
|
||||||
|
// to use this recipe.
|
||||||
|
MinDiff int // 0, 1, 2 = normal, nightmare, hell
|
||||||
|
|
||||||
|
// Version specifies whether the recipe is old
|
||||||
|
// classic, new classic or expansion.
|
||||||
|
Version int // 0, 1, 100 = old cl, new cl, expansion
|
||||||
|
|
||||||
|
// The following three 'Req' values form a comparison:
|
||||||
|
// if <ReqStatID> <ReqOperation> <ReqValue> then recipe
|
||||||
|
// is allowed.
|
||||||
|
//
|
||||||
|
// ReqStatID is an ID value from the ItemStatsCost
|
||||||
|
// data set specifying the stat to compare. Whether
|
||||||
|
// this references a player or item stat depends on
|
||||||
|
// the Operator.
|
||||||
|
ReqStatID int
|
||||||
|
// ReqOperation is a number describing the
|
||||||
|
// comparison operator and the action to take if
|
||||||
|
// it evaluates to true. See Appendix A in the
|
||||||
|
// linked article and note that 1, 2, 27 and 28
|
||||||
|
// are unusual.
|
||||||
|
ReqOperation int // 1 - 28
|
||||||
|
// ReqValue is the number the stat is compared against.
|
||||||
|
ReqValue int
|
||||||
|
|
||||||
|
// Class Can be used to make recipes class
|
||||||
|
// specific. Example class codes given are:
|
||||||
|
// ama bar pal nec sor dru ass
|
||||||
|
//
|
||||||
|
// Since this field isn't used in the game data,
|
||||||
|
// classFieldToEnum has been implemented based on that
|
||||||
|
// example. It understands the following syntax,
|
||||||
|
// which may be incorrect:
|
||||||
|
// "ama,bar,dru"
|
||||||
|
Class []d2enum.Hero
|
||||||
|
|
||||||
|
// NumInputs is the total count of input items
|
||||||
|
// required, including counts in item stacks.
|
||||||
|
NumInputs int
|
||||||
|
|
||||||
|
// Inputs is the actual recipe, a collection of
|
||||||
|
// items/stacks with parameters required to
|
||||||
|
// obtain the items defined in Outputs.
|
||||||
|
Inputs []CubeRecipeItem
|
||||||
|
|
||||||
|
// Outputs are the items created when the recipe
|
||||||
|
// is used.
|
||||||
|
Outputs []CubeRecipeResult
|
||||||
|
}
|
||||||
|
|
||||||
|
// CubeRecipeResult is an item generated on use of a
|
||||||
|
// cube recipe.
|
||||||
|
type CubeRecipeResult struct {
|
||||||
|
// Item is the item, with a count and parameters.
|
||||||
|
Item CubeRecipeItem
|
||||||
|
|
||||||
|
// Level causes the item to be a specific level.
|
||||||
|
//
|
||||||
|
// Note that this value force spawns the item at
|
||||||
|
// this specific level. Its also used in the
|
||||||
|
// formula for the next two fields.
|
||||||
|
Level int // the item level of Item
|
||||||
|
|
||||||
|
// PLevel uses a portion of the players level for
|
||||||
|
// the output level.
|
||||||
|
PLevel int
|
||||||
|
|
||||||
|
// ILevel uses a portion of the first input's
|
||||||
|
// level for the output level.
|
||||||
|
ILevel int
|
||||||
|
|
||||||
|
// Properties is a list of properties which may
|
||||||
|
// be attached to Item.
|
||||||
|
Properties []CubeRecipeItemProperty
|
||||||
|
}
|
||||||
|
|
||||||
|
// CubeRecipeItem represents an item, with a stack count
|
||||||
|
// and parameters. Here it is used to describe the
|
||||||
|
// required ingredients of the recipe and the output
|
||||||
|
// result. See:
|
||||||
|
// https://d2mods.info/forum/kb/viewarticle?a=284
|
||||||
|
type CubeRecipeItem struct {
|
||||||
|
Code string // item code e.g. 'weap'
|
||||||
|
Params []string // list of parameters e.g. 'sock'
|
||||||
|
Count int // required stack count
|
||||||
|
}
|
||||||
|
|
||||||
|
// CubeRecipeItemProperty represents the mod #,
|
||||||
|
// mod # chance, mod # param, mod # min, mod # max
|
||||||
|
// fields in cubemain.txt
|
||||||
|
type CubeRecipeItemProperty struct {
|
||||||
|
Code string // the code field from properties.txt
|
||||||
|
|
||||||
|
// Note: I can't find any example value for this
|
||||||
|
// so I've made it an int for now
|
||||||
|
Chance int // the chance to apply the property
|
||||||
|
|
||||||
|
// Note: The few examples in cubemain.txt are integers,
|
||||||
|
// however d2records.UniqueItemProperty is a similar
|
||||||
|
// struct which handles a similar field that may be a
|
||||||
|
// string or an integer.
|
||||||
|
//
|
||||||
|
// See: https://d2mods.info/forum/kb/viewarticle?a=345
|
||||||
|
// "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."
|
||||||
|
Param int // for properties that use parameters
|
||||||
|
|
||||||
|
Min int // the minimum value of the property stat
|
||||||
|
Max int // the maximum value of the property stat
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func difficultyLevelsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(DifficultyLevels)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &DifficultyLevelRecord{
|
||||||
|
Name: d.String("Name"),
|
||||||
|
ResistancePenalty: d.Number("ResistPenalty"),
|
||||||
|
DeathExperiencePenalty: d.Number("DeathExpPenalty"),
|
||||||
|
DropChanceLow: d.Number("UberCodeOddsNormal"),
|
||||||
|
DropChanceNormal: d.Number("UberCodeOddsNormal"),
|
||||||
|
DropChanceSuperior: d.Number("UberCodeOddsNormal"),
|
||||||
|
DropChanceExceptional: d.Number("UberCodeOddsNormal"),
|
||||||
|
DropChanceMagic: d.Number("UberCodeOddsGood"),
|
||||||
|
DropChanceRare: d.Number("UberCodeOddsGood"),
|
||||||
|
DropChanceSet: d.Number("UberCodeOddsGood"),
|
||||||
|
DropChanceUnique: d.Number("UberCodeOddsGood"),
|
||||||
|
MonsterSkillBonus: d.Number("MonsterSkillBonus"),
|
||||||
|
MonsterColdDivisor: d.Number("MonsterColdDivisor"),
|
||||||
|
MonsterFreezeDivisor: d.Number("MonsterFreezeDivisor"),
|
||||||
|
AiCurseDivisor: d.Number("AiCurseDivisor"),
|
||||||
|
LifeStealDivisor: d.Number("LifeStealDivisor"),
|
||||||
|
ManaStealDivisor: d.Number("ManaStealDivisor"),
|
||||||
|
}
|
||||||
|
records[record.Name] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d DifficultyLevel records", len(records))
|
||||||
|
|
||||||
|
r.DifficultyLevels = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// DifficultyLevels contain the difficulty records for each difficulty
|
||||||
|
type DifficultyLevels map[string]*DifficultyLevelRecord
|
||||||
|
|
||||||
|
// DifficultyLevelRecord contain the parameters that change for different difficulties
|
||||||
|
type DifficultyLevelRecord struct {
|
||||||
|
// Difficulty name. it is hardcoded and you cannot add new ones unless you do
|
||||||
|
// some Code Edits
|
||||||
|
Name string // Name
|
||||||
|
|
||||||
|
// Resistance penalty in the current difficulty.
|
||||||
|
ResistancePenalty int // ResistPenalty
|
||||||
|
|
||||||
|
// The percentage of experience you lose when you die on this difficulty.
|
||||||
|
DeathExperiencePenalty int // DeathExpPenalty
|
||||||
|
|
||||||
|
// Not Used. Pre 1.07 it was the percentage of low quality, normal, superior and
|
||||||
|
// exceptional items dropped on this difficulty.
|
||||||
|
DropChanceLow int // UberCodeOddsNormal
|
||||||
|
DropChanceNormal int // UberCodeOddsNormal
|
||||||
|
DropChanceSuperior int // UberCodeOddsNormal
|
||||||
|
DropChanceExceptional int // UberCodeOddsNormal
|
||||||
|
// Gravestench - I'm splitting this field because I feel it's incoherent
|
||||||
|
// to keep all of those drop chances together, even if it is that way in the
|
||||||
|
// txt file...
|
||||||
|
|
||||||
|
// Not used. Pre 1.07 it was the percentage of magic, rare, set and unique
|
||||||
|
// exceptional items dropped on this difficulty.
|
||||||
|
DropChanceMagic int // UberCodeOddsGood
|
||||||
|
DropChanceRare int // UberCodeOddsGood
|
||||||
|
DropChanceSet int // UberCodeOddsGood
|
||||||
|
DropChanceUnique int // UberCodeOddsGood
|
||||||
|
// Gravestench - same as my above comment
|
||||||
|
|
||||||
|
// Not used and didn't exist pre 1.07.
|
||||||
|
// UltraCodeOddsNormal
|
||||||
|
|
||||||
|
// Additional skill points added to monster skills specified in MonStats.txt
|
||||||
|
// for this difficulty. It has nothing to do with the missile damage bonus.
|
||||||
|
MonsterSkillBonus int // MonsterSkillBonus
|
||||||
|
|
||||||
|
// This value is a divisor, and so never set it to 0. It applies to the monster
|
||||||
|
// freezing length and cold length duration.
|
||||||
|
MonsterColdDivisor int // MonsterColdDivisor
|
||||||
|
MonsterFreezeDivisor int // MonsterFreezeDivisor
|
||||||
|
|
||||||
|
// These values are divisor and they're used respectively for AI altering states
|
||||||
|
AiCurseDivisor int // AiCurseDivisor
|
||||||
|
LifeStealDivisor int // LifeStealDivisor
|
||||||
|
ManaStealDivisor int // ManaStealDivisor
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
// The rest of these are listed on PK page, but not present in
|
||||||
|
// my copy of the txt file (patch_d2/data/global/excel/difficultylevels.txt)
|
||||||
|
// so I am going to leave these comments
|
||||||
|
|
||||||
|
// Effective percentage of damage and attack rating added to Extra Strong
|
||||||
|
// Unique/Minion and Champion monsters. This field is actually a coefficient,
|
||||||
|
// as the total bonus output is BonusFromMonUMod/100*ThisField
|
||||||
|
// UniqueDamageBonus
|
||||||
|
// ChampionDamageBonus
|
||||||
|
|
||||||
|
// This is a percentage of how much damage your mercenaries do to an Act boss.
|
||||||
|
// HireableBossDamagePercent
|
||||||
|
|
||||||
|
// Monster Corpse Explosion damage percent limit. Since the monsters HP grows
|
||||||
|
// proportionally to the number of players in the game, you can set a cap via
|
||||||
|
// this field.
|
||||||
|
// MonsterCEDamagePercent
|
||||||
|
|
||||||
|
// Maximum cap of the monster hit points percentage that can be damaged through
|
||||||
|
// Static Field. Setting these columns to 0 will make Static Field work the same
|
||||||
|
// way it did in Classic Diablo II.
|
||||||
|
// StaticFieldMin
|
||||||
|
|
||||||
|
// Parameters for gambling. They states the odds to find Rares, Sets, Uniques,
|
||||||
|
// Exceptionals and Elite items when gambling. See Appendix A
|
||||||
|
// GambleRare
|
||||||
|
// GambleSet
|
||||||
|
// GambleUnique
|
||||||
|
// GambleUber
|
||||||
|
// GambleUltra
|
||||||
|
// -----------------------------------------------------------------------
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadElemTypes loads ElemTypeRecords into ElemTypes
|
||||||
|
func elemTypesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(ElemTypes)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &ElemTypeRecord{
|
||||||
|
ElemType: d.String("Elemental Type"),
|
||||||
|
Code: d.String("Code"),
|
||||||
|
}
|
||||||
|
records[record.ElemType] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d ElemType records", len(records))
|
||||||
|
|
||||||
|
r.ElemTypes = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// ElemTypes stores the ElemTypeRecords
|
||||||
|
type ElemTypes map[string]*ElemTypeRecord
|
||||||
|
|
||||||
|
// ElemTypeRecord represents a single line in ElemType.txt
|
||||||
|
type ElemTypeRecord struct {
|
||||||
|
// ElemType Elemental damage type name
|
||||||
|
ElemType string
|
||||||
|
|
||||||
|
// Code Elemental damage type code
|
||||||
|
Code string
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadEvents loads all of the event records from events.txt
|
||||||
|
func eventsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(Events, 0)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &EventRecord{
|
||||||
|
Event: d.String("event"),
|
||||||
|
}
|
||||||
|
|
||||||
|
records = append(records, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d Event records", len(records))
|
||||||
|
|
||||||
|
r.Character.Events = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// Events holds all of the event records from events.txt
|
||||||
|
type Events []*EventRecord
|
||||||
|
|
||||||
|
// EventRecord is a representation of a single row from events.txt
|
||||||
|
type EventRecord struct {
|
||||||
|
Event string
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
/* first column of experience.txt
|
||||||
|
Level
|
||||||
|
Amazon
|
||||||
|
Sorceress
|
||||||
|
Necromancer
|
||||||
|
Paladin
|
||||||
|
Barbarian
|
||||||
|
Druid
|
||||||
|
Assassin
|
||||||
|
ExpRatio
|
||||||
|
|
||||||
|
second row is special case, shows max levels
|
||||||
|
|
||||||
|
MaxLvl
|
||||||
|
99
|
||||||
|
99
|
||||||
|
99
|
||||||
|
99
|
||||||
|
99
|
||||||
|
99
|
||||||
|
99
|
||||||
|
10
|
||||||
|
|
||||||
|
the rest are the breakpoints records
|
||||||
|
*/
|
||||||
|
|
||||||
|
func experienceLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
breakpoints := make(ExperienceBreakpoints)
|
||||||
|
|
||||||
|
d.Next() // move to the first row, the max level data
|
||||||
|
|
||||||
|
// parse the max level data
|
||||||
|
maxLevels := ExperienceMaxLevels{
|
||||||
|
d2enum.HeroAmazon: d.Number("Amazon"),
|
||||||
|
d2enum.HeroBarbarian: d.Number("Barbarian"),
|
||||||
|
d2enum.HeroDruid: d.Number("Druid"),
|
||||||
|
d2enum.HeroAssassin: d.Number("Assassin"),
|
||||||
|
d2enum.HeroNecromancer: d.Number("Necromancer"),
|
||||||
|
d2enum.HeroPaladin: d.Number("Paladin"),
|
||||||
|
d2enum.HeroSorceress: d.Number("Sorceress"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &ExperienceBreakpointsRecord{
|
||||||
|
Level: d.Number("Level"),
|
||||||
|
HeroBreakpoints: map[d2enum.Hero]int{
|
||||||
|
d2enum.HeroAmazon: d.Number("Amazon"),
|
||||||
|
d2enum.HeroBarbarian: d.Number("Barbarian"),
|
||||||
|
d2enum.HeroDruid: d.Number("Druid"),
|
||||||
|
d2enum.HeroAssassin: d.Number("Assassin"),
|
||||||
|
d2enum.HeroNecromancer: d.Number("Necromancer"),
|
||||||
|
d2enum.HeroPaladin: d.Number("Paladin"),
|
||||||
|
d2enum.HeroSorceress: d.Number("Sorceress"),
|
||||||
|
},
|
||||||
|
Ratio: d.Number("ExpRatio"),
|
||||||
|
}
|
||||||
|
breakpoints[record.Level] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d Experience Breakpoint records", len(breakpoints))
|
||||||
|
|
||||||
|
r.Character.MaxLevel = maxLevels
|
||||||
|
r.Character.Experience = breakpoints
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
|
||||||
|
// ExperienceBreakpoints describes the required experience
|
||||||
|
// for each level for each character class
|
||||||
|
type ExperienceBreakpoints map[int]*ExperienceBreakpointsRecord
|
||||||
|
|
||||||
|
// Type ExperienceMaxLevels defines the max character levels
|
||||||
|
type ExperienceMaxLevels map[d2enum.Hero]int
|
||||||
|
|
||||||
|
// ExperienceBreakpointsRecord describes the experience points required to
|
||||||
|
// gain a level for all character classes
|
||||||
|
type ExperienceBreakpointsRecord struct {
|
||||||
|
Level int
|
||||||
|
HeroBreakpoints map[d2enum.Hero]int
|
||||||
|
Ratio int
|
||||||
|
}
|
|
@ -0,0 +1,70 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadGems loads gem records into a map[string]*GemsRecord
|
||||||
|
func gemsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(Gems)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
gem := &GemsRecord{
|
||||||
|
Name: d.String("name"),
|
||||||
|
Letter: d.String("letter"),
|
||||||
|
Transform: d.Number("transform"),
|
||||||
|
Code: d.String("code"),
|
||||||
|
Nummods: d.Number("nummods"),
|
||||||
|
WeaponMod1Code: d.String("weaponMod1Code"),
|
||||||
|
WeaponMod1Param: d.Number("weaponMod1Param"),
|
||||||
|
WeaponMod1Min: d.Number("weaponMod1Min"),
|
||||||
|
WeaponMod1Max: d.Number("weaponMod1Max"),
|
||||||
|
WeaponMod2Code: d.String("weaponMod2Code"),
|
||||||
|
WeaponMod2Param: d.Number("weaponMod2Param"),
|
||||||
|
WeaponMod2Min: d.Number("weaponMod2Min"),
|
||||||
|
WeaponMod2Max: d.Number("weaponMod2Max"),
|
||||||
|
WeaponMod3Code: d.String("weaponMod3Code"),
|
||||||
|
WeaponMod3Param: d.Number("weaponMod3Param"),
|
||||||
|
WeaponMod3Min: d.Number("weaponMod3Min"),
|
||||||
|
WeaponMod3Max: d.Number("weaponMod3Max"),
|
||||||
|
HelmMod1Code: d.String("helmMod1Code"),
|
||||||
|
HelmMod1Param: d.Number("helmMod1Param"),
|
||||||
|
HelmMod1Min: d.Number("helmMod1Min"),
|
||||||
|
HelmMod1Max: d.Number("helmMod1Max"),
|
||||||
|
HelmMod2Code: d.String("helmMod2Code"),
|
||||||
|
HelmMod2Param: d.Number("helmMod2Param"),
|
||||||
|
HelmMod2Min: d.Number("helmMod2Min"),
|
||||||
|
HelmMod2Max: d.Number("helmMod2Max"),
|
||||||
|
HelmMod3Code: d.String("helmMod3Code"),
|
||||||
|
HelmMod3Param: d.Number("helmMod3Param"),
|
||||||
|
HelmMod3Min: d.Number("helmMod3Min"),
|
||||||
|
HelmMod3Max: d.Number("helmMod3Max"),
|
||||||
|
ShieldMod1Code: d.String("shieldMod1Code"),
|
||||||
|
ShieldMod1Param: d.Number("shieldMod1Param"),
|
||||||
|
ShieldMod1Min: d.Number("shieldMod1Min"),
|
||||||
|
ShieldMod1Max: d.Number("shieldMod1Max"),
|
||||||
|
ShieldMod2Code: d.String("shieldMod2Code"),
|
||||||
|
ShieldMod2Param: d.Number("shieldMod2Param"),
|
||||||
|
ShieldMod2Min: d.Number("shieldMod2Min"),
|
||||||
|
ShieldMod2Max: d.Number("shieldMod2Max"),
|
||||||
|
ShieldMod3Code: d.String("shieldMod3Code"),
|
||||||
|
ShieldMod3Param: d.Number("shieldMod3Param"),
|
||||||
|
ShieldMod3Min: d.Number("shieldMod3Min"),
|
||||||
|
ShieldMod3Max: d.Number("shieldMod3Max"),
|
||||||
|
}
|
||||||
|
|
||||||
|
records[gem.Name] = gem
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d Gems records", len(records))
|
||||||
|
|
||||||
|
r.Item.Gems = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// Gems stores all of the GemsRecords
|
||||||
|
type Gems map[string]*GemsRecord
|
||||||
|
|
||||||
|
// GemsRecord is a representation of a single row of gems.txt
|
||||||
|
// it describes the properties of socketable items
|
||||||
|
type GemsRecord struct {
|
||||||
|
Name string
|
||||||
|
Letter string
|
||||||
|
Transform int
|
||||||
|
Code string
|
||||||
|
Nummods int
|
||||||
|
WeaponMod1Code string
|
||||||
|
WeaponMod1Param int
|
||||||
|
WeaponMod1Min int
|
||||||
|
WeaponMod1Max int
|
||||||
|
WeaponMod2Code string
|
||||||
|
WeaponMod2Param int
|
||||||
|
WeaponMod2Min int
|
||||||
|
WeaponMod2Max int
|
||||||
|
WeaponMod3Code string
|
||||||
|
WeaponMod3Param int
|
||||||
|
WeaponMod3Min int
|
||||||
|
WeaponMod3Max int
|
||||||
|
HelmMod1Code string
|
||||||
|
HelmMod1Param int
|
||||||
|
HelmMod1Min int
|
||||||
|
HelmMod1Max int
|
||||||
|
HelmMod2Code string
|
||||||
|
HelmMod2Param int
|
||||||
|
HelmMod2Min int
|
||||||
|
HelmMod2Max int
|
||||||
|
HelmMod3Code string
|
||||||
|
HelmMod3Param int
|
||||||
|
HelmMod3Min int
|
||||||
|
HelmMod3Max int
|
||||||
|
ShieldMod1Code string
|
||||||
|
ShieldMod1Param int
|
||||||
|
ShieldMod1Min int
|
||||||
|
ShieldMod1Max int
|
||||||
|
ShieldMod2Code string
|
||||||
|
ShieldMod2Param int
|
||||||
|
ShieldMod2Min int
|
||||||
|
ShieldMod2Max int
|
||||||
|
ShieldMod3Code string
|
||||||
|
ShieldMod3Param int
|
||||||
|
ShieldMod3Min int
|
||||||
|
ShieldMod3Max int
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadHireling loads hireling data into []*HirelingRecord
|
||||||
|
func hirelingLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make([]*HirelingRecord, 0)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
hireling := &HirelingRecord{
|
||||||
|
Hireling: d.String("Hireling"),
|
||||||
|
SubType: d.String("SubType"),
|
||||||
|
ID: d.Number("Id"),
|
||||||
|
Class: d.Number("Class"),
|
||||||
|
Act: d.Number("Act"),
|
||||||
|
Difficulty: d.Number("Difficulty"),
|
||||||
|
Level: d.Number("Level"),
|
||||||
|
Seller: d.Number("Seller"),
|
||||||
|
NameFirst: d.String("NameFirst"),
|
||||||
|
NameLast: d.String("NameLast"),
|
||||||
|
Gold: d.Number("Gold"),
|
||||||
|
ExpPerLvl: d.Number("Exp/Lvl"),
|
||||||
|
HP: d.Number("HP"),
|
||||||
|
HPPerLvl: d.Number("HP/Lvl"),
|
||||||
|
Defense: d.Number("Defense"),
|
||||||
|
DefPerLvl: d.Number("Id"),
|
||||||
|
Str: d.Number("Str"),
|
||||||
|
StrPerLvl: d.Number("Str/Lvl"),
|
||||||
|
Dex: d.Number("Dex"),
|
||||||
|
DexPerLvl: d.Number("Dex/Lvl"),
|
||||||
|
AR: d.Number("AR"),
|
||||||
|
ARPerLvl: d.Number("AR/Lvl"),
|
||||||
|
Share: d.Number("Share"),
|
||||||
|
DmgMin: d.Number("Dmg-Min"),
|
||||||
|
DmgMax: d.Number("Dmg-Max"),
|
||||||
|
DmgPerLvl: d.Number("Dmg/Lvl"),
|
||||||
|
Resist: d.Number("Resist"),
|
||||||
|
ResistPerLvl: d.Number("Resist/Lvl"),
|
||||||
|
WType1: d.String("WType1"),
|
||||||
|
WType2: d.String("WType2"),
|
||||||
|
HireDesc: d.String("HireDesc"),
|
||||||
|
DefaultChance: d.Number("DefaultChance"),
|
||||||
|
Skill1: d.String("Skill1"),
|
||||||
|
Mode1: d.Number("Mode1"),
|
||||||
|
Chance1: d.Number("Chance1"),
|
||||||
|
ChancePerLevel1: d.Number("ChancePerLvl1"),
|
||||||
|
Level1: d.Number("Level1"),
|
||||||
|
LvlPerLvl1: d.Number("LvlPerLvl1"),
|
||||||
|
Skill2: d.String("Skill2"),
|
||||||
|
Mode2: d.Number("Mode2"),
|
||||||
|
Chance2: d.Number("Chance2"),
|
||||||
|
ChancePerLevel2: d.Number("ChancePerLvl2"),
|
||||||
|
Level2: d.Number("Level2"),
|
||||||
|
LvlPerLvl2: d.Number("LvlPerLvl2"),
|
||||||
|
Skill3: d.String("Skill3"),
|
||||||
|
Mode3: d.Number("Mode3"),
|
||||||
|
Chance3: d.Number("Chance3"),
|
||||||
|
ChancePerLevel3: d.Number("ChancePerLvl3"),
|
||||||
|
Level3: d.Number("Level3"),
|
||||||
|
LvlPerLvl3: d.Number("LvlPerLvl3"),
|
||||||
|
Skill4: d.String("Skill4"),
|
||||||
|
Mode4: d.Number("Mode4"),
|
||||||
|
Chance4: d.Number("Chance4"),
|
||||||
|
ChancePerLevel4: d.Number("ChancePerLvl4"),
|
||||||
|
Level4: d.Number("Level4"),
|
||||||
|
LvlPerLvl4: d.Number("LvlPerLvl4"),
|
||||||
|
Skill5: d.String("Skill5"),
|
||||||
|
Mode5: d.Number("Mode5"),
|
||||||
|
Chance5: d.Number("Chance5"),
|
||||||
|
ChancePerLevel5: d.Number("ChancePerLvl5"),
|
||||||
|
Level5: d.Number("Level5"),
|
||||||
|
LvlPerLvl5: d.Number("LvlPerLvl5"),
|
||||||
|
Skill6: d.String("Skill6"),
|
||||||
|
Mode6: d.Number("Mode6"),
|
||||||
|
Chance6: d.Number("Chance6"),
|
||||||
|
ChancePerLevel6: d.Number("ChancePerLvl6"),
|
||||||
|
Level6: d.Number("Level6"),
|
||||||
|
LvlPerLvl6: d.Number("LvlPerLvl6"),
|
||||||
|
Head: d.Number("Head"),
|
||||||
|
Torso: d.Number("Torso"),
|
||||||
|
Weapon: d.Number("Weapon"),
|
||||||
|
Shield: d.Number("Shield"),
|
||||||
|
}
|
||||||
|
records = append(records, hireling)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d Hireling records", len(records))
|
||||||
|
|
||||||
|
r.Hirelings = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// Hirelings stores hireling (mercenary) records
|
||||||
|
type Hirelings []*HirelingRecord
|
||||||
|
|
||||||
|
// HirelingRecord is a representation of rows in hireling.txt
|
||||||
|
// these records describe mercenaries
|
||||||
|
type HirelingRecord struct {
|
||||||
|
Hireling string
|
||||||
|
SubType string
|
||||||
|
ID int
|
||||||
|
Class int
|
||||||
|
Act int
|
||||||
|
Difficulty int
|
||||||
|
Level int
|
||||||
|
Seller int
|
||||||
|
NameFirst string
|
||||||
|
NameLast string
|
||||||
|
Gold int
|
||||||
|
ExpPerLvl int
|
||||||
|
HP int
|
||||||
|
HPPerLvl int
|
||||||
|
Defense int
|
||||||
|
DefPerLvl int
|
||||||
|
Str int
|
||||||
|
StrPerLvl int
|
||||||
|
Dex int
|
||||||
|
DexPerLvl int
|
||||||
|
AR int
|
||||||
|
ARPerLvl int
|
||||||
|
Share int
|
||||||
|
DmgMin int
|
||||||
|
DmgMax int
|
||||||
|
DmgPerLvl int
|
||||||
|
Resist int
|
||||||
|
ResistPerLvl int
|
||||||
|
WType1 string
|
||||||
|
WType2 string
|
||||||
|
HireDesc string
|
||||||
|
DefaultChance int
|
||||||
|
Skill1 string
|
||||||
|
Mode1 int
|
||||||
|
Chance1 int
|
||||||
|
ChancePerLevel1 int
|
||||||
|
Level1 int
|
||||||
|
LvlPerLvl1 int
|
||||||
|
Skill2 string
|
||||||
|
Mode2 int
|
||||||
|
Chance2 int
|
||||||
|
ChancePerLevel2 int
|
||||||
|
Level2 int
|
||||||
|
LvlPerLvl2 int
|
||||||
|
Skill3 string
|
||||||
|
Mode3 int
|
||||||
|
Chance3 int
|
||||||
|
ChancePerLevel3 int
|
||||||
|
Level3 int
|
||||||
|
LvlPerLvl3 int
|
||||||
|
Skill4 string
|
||||||
|
Mode4 int
|
||||||
|
Chance4 int
|
||||||
|
ChancePerLevel4 int
|
||||||
|
Level4 int
|
||||||
|
LvlPerLvl4 int
|
||||||
|
Skill5 string
|
||||||
|
Mode5 int
|
||||||
|
Chance5 int
|
||||||
|
ChancePerLevel5 int
|
||||||
|
Level5 int
|
||||||
|
LvlPerLvl5 int
|
||||||
|
Skill6 string
|
||||||
|
Mode6 int
|
||||||
|
Chance6 int
|
||||||
|
ChancePerLevel6 int
|
||||||
|
Level6 int
|
||||||
|
LvlPerLvl6 int
|
||||||
|
Head int
|
||||||
|
Torso int
|
||||||
|
Weapon int
|
||||||
|
Shield int
|
||||||
|
}
|
|
@ -0,0 +1,140 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadInventory loads all of the inventory records from inventory.txt
|
||||||
|
func inventoryLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(Inventory)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
// we need to calc the width/height for the box as it isn't
|
||||||
|
// specified in the txt file
|
||||||
|
pBox := &box{}
|
||||||
|
pBox.Left = d.Number("invLeft")
|
||||||
|
pBox.Right = d.Number("invRight")
|
||||||
|
pBox.Top = d.Number("invTop")
|
||||||
|
pBox.Bottom = d.Number("invBottom")
|
||||||
|
pBox.Width = pBox.Right - pBox.Left
|
||||||
|
pBox.Height = pBox.Bottom - pBox.Top
|
||||||
|
|
||||||
|
gBox := &box{
|
||||||
|
Left: d.Number("gridLeft"),
|
||||||
|
Right: d.Number("gridRight"),
|
||||||
|
Top: d.Number("gridTop"),
|
||||||
|
Bottom: d.Number("gridBottom"),
|
||||||
|
}
|
||||||
|
gBox.Width = gBox.Right - gBox.Left
|
||||||
|
gBox.Height = gBox.Bottom - gBox.Top
|
||||||
|
|
||||||
|
record := &InventoryRecord{
|
||||||
|
Name: d.String("class"),
|
||||||
|
Panel: pBox,
|
||||||
|
Grid: &grid{
|
||||||
|
Box: gBox,
|
||||||
|
Rows: d.Number("gridY"),
|
||||||
|
Columns: d.Number("gridX"),
|
||||||
|
CellWidth: d.Number("gridBoxWidth"),
|
||||||
|
CellHeight: d.Number("gridBoxHeight"),
|
||||||
|
},
|
||||||
|
Slots: map[d2enum.EquippedSlot]*box{
|
||||||
|
d2enum.EquippedSlotHead: {
|
||||||
|
d.Number("headLeft"),
|
||||||
|
d.Number("headRight"),
|
||||||
|
d.Number("headTop"),
|
||||||
|
d.Number("headBottom"),
|
||||||
|
d.Number("headWidth"),
|
||||||
|
d.Number("headHeight"),
|
||||||
|
},
|
||||||
|
d2enum.EquippedSlotNeck: {
|
||||||
|
d.Number("neckLeft"),
|
||||||
|
d.Number("neckRight"),
|
||||||
|
d.Number("neckTop"),
|
||||||
|
d.Number("neckBottom"),
|
||||||
|
d.Number("neckWidth"),
|
||||||
|
d.Number("neckHeight"),
|
||||||
|
},
|
||||||
|
d2enum.EquippedSlotTorso: {
|
||||||
|
d.Number("torsoLeft"),
|
||||||
|
d.Number("torsoRight"),
|
||||||
|
d.Number("torsoTop"),
|
||||||
|
d.Number("torsoBottom"),
|
||||||
|
d.Number("torsoWidth"),
|
||||||
|
d.Number("torsoHeight"),
|
||||||
|
},
|
||||||
|
d2enum.EquippedSlotLeftArm: {
|
||||||
|
d.Number("lArmLeft"),
|
||||||
|
d.Number("lArmRight"),
|
||||||
|
d.Number("lArmTop"),
|
||||||
|
d.Number("lArmBottom"),
|
||||||
|
d.Number("lArmWidth"),
|
||||||
|
d.Number("lArmHeight"),
|
||||||
|
},
|
||||||
|
d2enum.EquippedSlotRightArm: {
|
||||||
|
d.Number("rArmLeft"),
|
||||||
|
d.Number("rArmRight"),
|
||||||
|
d.Number("rArmTop"),
|
||||||
|
d.Number("rArmBottom"),
|
||||||
|
d.Number("rArmWidth"),
|
||||||
|
d.Number("rArmHeight"),
|
||||||
|
},
|
||||||
|
d2enum.EquippedSlotLeftHand: {
|
||||||
|
d.Number("lHandLeft"),
|
||||||
|
d.Number("lHandRight"),
|
||||||
|
d.Number("lHandTop"),
|
||||||
|
d.Number("lHandBottom"),
|
||||||
|
d.Number("lHandWidth"),
|
||||||
|
d.Number("lHandHeight"),
|
||||||
|
},
|
||||||
|
d2enum.EquippedSlotRightHand: {
|
||||||
|
d.Number("rHandLeft"),
|
||||||
|
d.Number("rHandRight"),
|
||||||
|
d.Number("rHandTop"),
|
||||||
|
d.Number("rHandBottom"),
|
||||||
|
d.Number("rHandWidth"),
|
||||||
|
d.Number("rHandHeight"),
|
||||||
|
},
|
||||||
|
d2enum.EquippedSlotGloves: {
|
||||||
|
d.Number("glovesLeft"),
|
||||||
|
d.Number("glovesRight"),
|
||||||
|
d.Number("glovesTop"),
|
||||||
|
d.Number("glovesBottom"),
|
||||||
|
d.Number("glovesWidth"),
|
||||||
|
d.Number("glovesHeight"),
|
||||||
|
},
|
||||||
|
d2enum.EquippedSlotBelt: {
|
||||||
|
d.Number("beltLeft"),
|
||||||
|
d.Number("beltRight"),
|
||||||
|
d.Number("beltTop"),
|
||||||
|
d.Number("beltBottom"),
|
||||||
|
d.Number("beltWidth"),
|
||||||
|
d.Number("beltHeight"),
|
||||||
|
},
|
||||||
|
d2enum.EquippedSlotLegs: {
|
||||||
|
d.Number("feetLeft"),
|
||||||
|
d.Number("feetRight"),
|
||||||
|
d.Number("feetTop"),
|
||||||
|
d.Number("feetBottom"),
|
||||||
|
d.Number("feetWidth"),
|
||||||
|
d.Number("feetHeight"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
records[record.Name] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d Inventory Panel records", len(records))
|
||||||
|
|
||||||
|
r.Layout.Inventory = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
|
||||||
|
// Inventory holds all of the inventory records from inventory.txt
|
||||||
|
type Inventory map[string]*InventoryRecord //nolint:gochecknoglobals // Currently global by design
|
||||||
|
|
||||||
|
// InventoryRecord represents a single row from inventory.txt, it describes the grid
|
||||||
|
// layout and positioning of various inventory-related ui panels.
|
||||||
|
type InventoryRecord struct {
|
||||||
|
Name string
|
||||||
|
Panel *box
|
||||||
|
Grid *grid
|
||||||
|
Slots map[d2enum.EquippedSlot]*box
|
||||||
|
}
|
||||||
|
|
||||||
|
type box struct {
|
||||||
|
Left int
|
||||||
|
Right int
|
||||||
|
Top int
|
||||||
|
Bottom int
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
}
|
||||||
|
|
||||||
|
type grid struct {
|
||||||
|
Box *box
|
||||||
|
Rows int
|
||||||
|
Columns int
|
||||||
|
CellWidth int
|
||||||
|
CellHeight int
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadMagicPrefix loads MagicPrefix.txt
|
||||||
|
func magicPrefixLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
superType := d2enum.ItemAffixPrefix
|
||||||
|
|
||||||
|
subType := d2enum.ItemAffixMagic
|
||||||
|
|
||||||
|
affixes, groups, err := loadAffixDictionary(d, superType, subType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Item.Magic.Prefix = affixes
|
||||||
|
r.Item.MagicPrefixGroups = groups
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadMagicSuffix loads MagicSuffix.txt
|
||||||
|
func magicSuffixLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
superType := d2enum.ItemAffixSuffix
|
||||||
|
|
||||||
|
subType := d2enum.ItemAffixMagic
|
||||||
|
|
||||||
|
affixes, groups, err := loadAffixDictionary(d, superType, subType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Item.Magic.Suffix = affixes
|
||||||
|
r.Item.MagicSuffixGroups = groups
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAffixString(t1 d2enum.ItemAffixSuperType, t2 d2enum.ItemAffixSubType) string {
|
||||||
|
var name = ""
|
||||||
|
|
||||||
|
if t2 == d2enum.ItemAffixMagic {
|
||||||
|
name = "Magic"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t1 {
|
||||||
|
case d2enum.ItemAffixPrefix:
|
||||||
|
name += "Prefix"
|
||||||
|
case d2enum.ItemAffixSuffix:
|
||||||
|
name += "Suffix"
|
||||||
|
}
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadAffixDictionary(
|
||||||
|
d *d2txt.DataDictionary,
|
||||||
|
superType d2enum.ItemAffixSuperType,
|
||||||
|
subType d2enum.ItemAffixSubType,
|
||||||
|
) (map[string]*ItemAffixCommonRecord, ItemAffixGroups, error) {
|
||||||
|
records, groups, err := createItemAffixRecords(d, superType, subType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
name := getAffixString(superType, subType)
|
||||||
|
log.Printf("Loaded %d %s records", len(records), name)
|
||||||
|
|
||||||
|
return records, groups, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createItemAffixRecords(
|
||||||
|
d *d2txt.DataDictionary,
|
||||||
|
superType d2enum.ItemAffixSuperType,
|
||||||
|
subType d2enum.ItemAffixSubType,
|
||||||
|
) (map[string]*ItemAffixCommonRecord, ItemAffixGroups, error) {
|
||||||
|
records := make(map[string]*ItemAffixCommonRecord)
|
||||||
|
groups := make(ItemAffixGroups)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
affix := &ItemAffixCommonRecord{
|
||||||
|
Name: d.String("Name"),
|
||||||
|
Version: d.Number("version"),
|
||||||
|
Type: subType,
|
||||||
|
IsPrefix: superType == d2enum.ItemAffixPrefix,
|
||||||
|
IsSuffix: superType == d2enum.ItemAffixSuffix,
|
||||||
|
Spawnable: d.Bool("spawnable"),
|
||||||
|
Rare: d.Bool("rare"),
|
||||||
|
Level: d.Number("level"),
|
||||||
|
MaxLevel: d.Number("maxlevel"),
|
||||||
|
LevelReq: d.Number("levelreq"),
|
||||||
|
Class: d.String("classspecific"),
|
||||||
|
ClassLevelReq: d.Number("classlevelreq"),
|
||||||
|
Frequency: d.Number("frequency"),
|
||||||
|
GroupID: d.Number("group"),
|
||||||
|
Transform: d.Bool("transform"),
|
||||||
|
TransformColor: d.String("transformcolor"),
|
||||||
|
PriceAdd: d.Number("add"),
|
||||||
|
PriceScale: d.Number("multiply"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
minKey := fmt.Sprintf("mod%dmin", i)
|
||||||
|
maxKey := fmt.Sprintf("mod%dmax", i)
|
||||||
|
modifier := &ItemAffixCommonModifier{
|
||||||
|
Code: d.String(codeKey),
|
||||||
|
Parameter: d.Number(paramKey),
|
||||||
|
Min: d.Number(minKey),
|
||||||
|
Max: d.Number(maxKey),
|
||||||
|
}
|
||||||
|
affix.Modifiers = append(affix.Modifiers, modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
// items to include for spawning
|
||||||
|
for i := 1; i <= 7; i++ {
|
||||||
|
itemKey := fmt.Sprintf("itype%d", i)
|
||||||
|
itemToken := d.String(itemKey)
|
||||||
|
affix.ItemInclude = append(affix.ItemInclude, itemToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
// items to exclude for spawning
|
||||||
|
for i := 1; i <= 7; i++ {
|
||||||
|
itemKey := fmt.Sprintf("etype%d", i)
|
||||||
|
itemToken := d.String(itemKey)
|
||||||
|
affix.ItemExclude = append(affix.ItemExclude, itemToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found := groups[affix.GroupID]; !found {
|
||||||
|
ItemAffixGroup := &ItemAffixCommonGroup{}
|
||||||
|
ItemAffixGroup.ID = affix.GroupID
|
||||||
|
groups[affix.GroupID] = ItemAffixGroup
|
||||||
|
}
|
||||||
|
|
||||||
|
group := groups[affix.GroupID]
|
||||||
|
group.AddMember(affix)
|
||||||
|
|
||||||
|
records[affix.Name] = affix
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return nil, nil, d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, groups, nil
|
||||||
|
}
|
|
@ -0,0 +1,93 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
|
||||||
|
// MagicPrefix stores all of the magic prefix records
|
||||||
|
type MagicPrefix map[string]*ItemAffixCommonRecord
|
||||||
|
|
||||||
|
// MagicSuffix stores all of the magic suffix records
|
||||||
|
type MagicSuffix map[string]*ItemAffixCommonRecord
|
||||||
|
|
||||||
|
// ItemAffixGroups are groups of MagicPrefix/Suffixes
|
||||||
|
type ItemAffixGroups map[int]*ItemAffixCommonGroup
|
||||||
|
|
||||||
|
// ItemAffixCommonModifier is the generic modifier form that prefix/suffix shares
|
||||||
|
// modifiers are like dynamic properties, they have a key that points to a property
|
||||||
|
// a parameter for the property, and a min/max value
|
||||||
|
type ItemAffixCommonModifier struct {
|
||||||
|
Code string
|
||||||
|
Parameter int
|
||||||
|
Min int
|
||||||
|
Max int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemAffixCommonGroup is a grouping that is common between prefix/suffix
|
||||||
|
type ItemAffixCommonGroup struct {
|
||||||
|
ID int
|
||||||
|
Members map[string]*ItemAffixCommonRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemAffixCommonRecord is a common definition that both prefix and suffix use
|
||||||
|
type ItemAffixCommonRecord struct {
|
||||||
|
Group *ItemAffixCommonGroup
|
||||||
|
Modifiers []*ItemAffixCommonModifier
|
||||||
|
|
||||||
|
ItemInclude []string
|
||||||
|
ItemExclude []string
|
||||||
|
|
||||||
|
Name string
|
||||||
|
Class string
|
||||||
|
TransformColor string
|
||||||
|
|
||||||
|
Version int
|
||||||
|
Type d2enum.ItemAffixSubType
|
||||||
|
|
||||||
|
Level int
|
||||||
|
MaxLevel int
|
||||||
|
|
||||||
|
LevelReq int
|
||||||
|
ClassLevelReq int
|
||||||
|
|
||||||
|
Frequency int
|
||||||
|
GroupID int
|
||||||
|
|
||||||
|
PriceAdd int
|
||||||
|
PriceScale int
|
||||||
|
|
||||||
|
IsPrefix bool
|
||||||
|
IsSuffix bool
|
||||||
|
|
||||||
|
Spawnable bool
|
||||||
|
Rare bool
|
||||||
|
Transform bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *ItemAffixCommonGroup) AddMember(a *ItemAffixCommonRecord) {
|
||||||
|
if g.Members == nil {
|
||||||
|
g.Members = make(map[string]*ItemAffixCommonRecord)
|
||||||
|
}
|
||||||
|
|
||||||
|
g.Members[a.Name] = a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *ItemAffixCommonGroup) GetTotalFrequency() int {
|
||||||
|
total := 0
|
||||||
|
|
||||||
|
for _, affix := range g.Members {
|
||||||
|
total += affix.Frequency
|
||||||
|
}
|
||||||
|
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProbabilityToSpawn returns the chance of the affix spawning on an
|
||||||
|
// item with a given quality level
|
||||||
|
func (a *ItemAffixCommonRecord) ProbabilityToSpawn(qlvl int) float64 {
|
||||||
|
if (qlvl > a.MaxLevel) || (qlvl < a.Level) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
p := float64(a.Frequency) / float64(a.Group.GetTotalFrequency())
|
||||||
|
|
||||||
|
return p
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
)
|
||||||
|
|
||||||
|
func armorLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
if r.Item.Armors != nil {
|
||||||
|
return nil // already loaded
|
||||||
|
}
|
||||||
|
|
||||||
|
records, err := loadCommonItems(d, d2enum.InventoryItemTypeArmor)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d armors", len(records))
|
||||||
|
|
||||||
|
r.Item.Armors = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,229 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2calculation"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadCommonItems(d *d2txt.DataDictionary, source d2enum.InventoryItemType) (CommonItems, error) {
|
||||||
|
records := make(CommonItems)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &ItemCommonRecord{
|
||||||
|
Source: source,
|
||||||
|
|
||||||
|
Name: d.String("name"),
|
||||||
|
|
||||||
|
Version: d.Number("version"),
|
||||||
|
CompactSave: d.Number("compactsave") > 0,
|
||||||
|
Rarity: d.Number("rarity"),
|
||||||
|
Spawnable: d.Number("spawnable") > 0,
|
||||||
|
|
||||||
|
MinAC: d.Number("minac"),
|
||||||
|
MaxAC: d.Number("maxac"),
|
||||||
|
Absorbs: d.Number("absorbs"),
|
||||||
|
Speed: d.Number("speed"),
|
||||||
|
RequiredStrength: d.Number("reqstr"),
|
||||||
|
Block: d.Number("block"),
|
||||||
|
Durability: d.Number("durability"),
|
||||||
|
NoDurability: d.Number("nodurability") > 0,
|
||||||
|
|
||||||
|
Level: d.Number("level"),
|
||||||
|
RequiredLevel: d.Number("levelreq"),
|
||||||
|
Cost: d.Number("cost"),
|
||||||
|
GambleCost: d.Number("gamble cost"),
|
||||||
|
Code: d.String("code"),
|
||||||
|
NameString: d.String("namestr"),
|
||||||
|
MagicLevel: d.Number("magic lvl"),
|
||||||
|
AutoPrefix: d.Number("auto prefix"),
|
||||||
|
|
||||||
|
AlternateGfx: d.String("alternategfx"),
|
||||||
|
OpenBetaGfx: d.String("OpenBetaGfx"),
|
||||||
|
NormalCode: d.String("normcode"),
|
||||||
|
UberCode: d.String("ubercode"),
|
||||||
|
UltraCode: d.String("ultracode"),
|
||||||
|
|
||||||
|
SpellOffset: d.Number("spelloffset"),
|
||||||
|
|
||||||
|
Component: d.Number("component"),
|
||||||
|
InventoryWidth: d.Number("invwidth"),
|
||||||
|
InventoryHeight: d.Number("invheight"),
|
||||||
|
HasInventory: d.Number("hasinv") > 0,
|
||||||
|
GemSockets: d.Number("gemsockets"),
|
||||||
|
GemApplyType: d.Number("gemapplytype"),
|
||||||
|
|
||||||
|
FlippyFile: d.String("flippyfile"),
|
||||||
|
InventoryFile: d.String("invfile"),
|
||||||
|
UniqueInventoryFile: d.String("uniqueinvfile"),
|
||||||
|
SetInventoryFile: d.String("setinvfile"),
|
||||||
|
|
||||||
|
AnimRightArm: d.Number("rArm"),
|
||||||
|
AnimLeftArm: d.Number("lArm"),
|
||||||
|
AnimTorso: d.Number("Torso"),
|
||||||
|
AnimLegs: d.Number("Legs"),
|
||||||
|
AnimRightShoulderPad: d.Number("rSPad"),
|
||||||
|
AnimLeftShoulderPad: d.Number("lSPad"),
|
||||||
|
|
||||||
|
Useable: d.Number("useable") > 0,
|
||||||
|
|
||||||
|
Throwable: d.Number("throwable") > 0,
|
||||||
|
Stackable: d.Number("stackable") > 0,
|
||||||
|
MinStack: d.Number("minstack"),
|
||||||
|
MaxStack: d.Number("maxstack"),
|
||||||
|
|
||||||
|
Type: d.String("type"),
|
||||||
|
Type2: d.String("type2"),
|
||||||
|
|
||||||
|
DropSound: d.String("dropsound"),
|
||||||
|
DropSfxFrame: d.Number("dropsfxframe"),
|
||||||
|
UseSound: d.String("usesound"),
|
||||||
|
|
||||||
|
Unique: d.Number("unique") > 0,
|
||||||
|
Transparent: d.Number("transparent") > 0,
|
||||||
|
TransTable: d.Number("transtbl"),
|
||||||
|
Quivered: d.Number("quivered") > 0,
|
||||||
|
LightRadius: d.Number("lightradius"),
|
||||||
|
Belt: d.Number("belt") > 0,
|
||||||
|
|
||||||
|
Quest: d.Number("quest"),
|
||||||
|
|
||||||
|
MissileType: d.Number("missiletype"),
|
||||||
|
DurabilityWarning: d.Number("durwarning"),
|
||||||
|
QuantityWarning: d.Number("qntwarning"),
|
||||||
|
|
||||||
|
MinDamage: d.Number("mindam"),
|
||||||
|
MaxDamage: d.Number("maxdam"),
|
||||||
|
StrengthBonus: d.Number("StrBonus"),
|
||||||
|
DexterityBonus: d.Number("DexBonus"),
|
||||||
|
|
||||||
|
GemOffset: d.Number("gemoffset"),
|
||||||
|
BitField1: d.Number("bitfield1"),
|
||||||
|
|
||||||
|
Vendors: createItemVendorParams(d),
|
||||||
|
|
||||||
|
SourceArt: d.String("Source Art"),
|
||||||
|
GameArt: d.String("Game Art"),
|
||||||
|
ColorTransform: d.Number("Transform"),
|
||||||
|
InventoryColorTransform: d.Number("InvTrans"),
|
||||||
|
|
||||||
|
SkipName: d.Number("SkipName") > 0,
|
||||||
|
NightmareUpgrade: d.String("NightmareUpgrade"),
|
||||||
|
HellUpgrade: d.String("HellUpgrade"),
|
||||||
|
|
||||||
|
Nameable: d.Number("Nameable") > 0,
|
||||||
|
|
||||||
|
// weapon params
|
||||||
|
BarbOneOrTwoHanded: d.Number("1or2handed") > 0,
|
||||||
|
UsesTwoHands: d.Number("2handed") > 0,
|
||||||
|
Min2HandDamage: d.Number("2handmindam"),
|
||||||
|
Max2HandDamage: d.Number("2handmaxdam"),
|
||||||
|
MinMissileDamage: d.Number("minmisdam"),
|
||||||
|
MaxMissileDamage: d.Number("maxmisdam"),
|
||||||
|
MissileSpeed: d.Number("misspeed"),
|
||||||
|
ExtraRange: d.Number("rangeadder"),
|
||||||
|
|
||||||
|
RequiredDexterity: d.Number("reqdex"),
|
||||||
|
|
||||||
|
WeaponClass: d.String("wclass"),
|
||||||
|
WeaponClass2Hand: d.String("2handedwclass"),
|
||||||
|
|
||||||
|
HitClass: d.String("hit class"),
|
||||||
|
SpawnStack: d.Number("spawnstack"),
|
||||||
|
|
||||||
|
SpecialFeature: d.String("special"),
|
||||||
|
|
||||||
|
QuestDifficultyCheck: d.Number("questdiffcheck") > 0,
|
||||||
|
|
||||||
|
PermStoreItem: d.Number("PermStoreItem") > 0,
|
||||||
|
|
||||||
|
// misc params
|
||||||
|
FlavorText: d.String("szFlavorText"),
|
||||||
|
|
||||||
|
Transmogrify: d.Number("Transmogrify") > 0,
|
||||||
|
TransmogCode: d.String("TMogType"),
|
||||||
|
TransmogMin: d.Number("TMogMin"),
|
||||||
|
TransmogMax: d.Number("TMogMax"),
|
||||||
|
|
||||||
|
AutoBelt: d.Number("autobelt") > 0,
|
||||||
|
|
||||||
|
SpellIcon: d.Number("spellicon"),
|
||||||
|
SpellType: d.Number("pSpell"),
|
||||||
|
OverlayState: d.String("state"),
|
||||||
|
CureOverlayStates: [2]string{
|
||||||
|
d.String("cstate1"),
|
||||||
|
d.String("cstate2"),
|
||||||
|
},
|
||||||
|
EffectLength: d.Number("len"),
|
||||||
|
UsageStats: createItemUsageStats(d),
|
||||||
|
|
||||||
|
SpellDescriptionType: d.Number("spelldesc"),
|
||||||
|
// 0 = none, 1 = use desc string, 2 = use desc string + calc value
|
||||||
|
SpellDescriptionString: d.String("spelldescstr"),
|
||||||
|
SpellDescriptionCalc: d2calculation.CalcString(d.String("spelldesccalc")),
|
||||||
|
|
||||||
|
BetterGem: d.String("BetterGem"),
|
||||||
|
|
||||||
|
Multibuy: d.Number("multibuy") > 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
records[record.Code] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return nil, d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createItemVendorParams(d *d2txt.DataDictionary) map[string]*ItemVendorParams {
|
||||||
|
vs := []string{
|
||||||
|
"Charsi",
|
||||||
|
"Gheed",
|
||||||
|
"Akara",
|
||||||
|
"Fara",
|
||||||
|
"Lysander",
|
||||||
|
"Drognan",
|
||||||
|
"Hralti",
|
||||||
|
"Alkor",
|
||||||
|
"Ormus",
|
||||||
|
"Elzix",
|
||||||
|
"Asheara",
|
||||||
|
"Cain",
|
||||||
|
"Halbu",
|
||||||
|
"Jamella",
|
||||||
|
"Larzuk",
|
||||||
|
"Malah",
|
||||||
|
"Drehya",
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string]*ItemVendorParams)
|
||||||
|
|
||||||
|
for _, name := range vs {
|
||||||
|
wvp := ItemVendorParams{
|
||||||
|
Min: d.Number(fmt.Sprintf("%s%s", name, "Min")),
|
||||||
|
Max: d.Number(fmt.Sprintf("%s%s", name, "Max")),
|
||||||
|
MagicMin: d.Number(fmt.Sprintf("%s%s", name, "MagicMin")),
|
||||||
|
MagicMax: d.Number(fmt.Sprintf("%s%s", name, "MagicMax")),
|
||||||
|
MagicLevel: d.Number(fmt.Sprintf("%s%s", name, "MagicLvl")),
|
||||||
|
}
|
||||||
|
result[name] = &wvp
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func createItemUsageStats(d *d2txt.DataDictionary) [3]ItemUsageStat {
|
||||||
|
result := [3]ItemUsageStat{}
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
result[i].Stat = d.String("stat" + strconv.Itoa(i))
|
||||||
|
result[i].Calc = d2calculation.CalcString(d.String("calc" + strconv.Itoa(i)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2calculation"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommonItems stores all ItemCommonRecords
|
||||||
|
type CommonItems map[string]*ItemCommonRecord
|
||||||
|
|
||||||
|
// ItemCommonRecord is a representation of entries from armor.txt, weapons.txt, and misc.txt
|
||||||
|
type ItemCommonRecord struct {
|
||||||
|
UsageStats [3]ItemUsageStat // stat boosts applied upon usage
|
||||||
|
CureOverlayStates [2]string // name of the overlay states that are removed upon use of this item
|
||||||
|
OverlayState string // name of the overlay state to be applied upon use of this item
|
||||||
|
SpellDescriptionString string // points to a string containing the description
|
||||||
|
BetterGem string // 3 char code pointing to the gem this upgrades to (non if not applicable)
|
||||||
|
SpellDescriptionCalc d2calculation.CalcString // a calc string what value to display
|
||||||
|
WeaponClass string // what kind of attack does this weapon have (i.e. determines attack animations)
|
||||||
|
WeaponClass2Hand string // what kind of attack when wielded with two hands
|
||||||
|
HitClass string // determines sounds/graphic effects when attacking
|
||||||
|
SpecialFeature string // Just a comment
|
||||||
|
FlavorText string // unknown, probably just for reference
|
||||||
|
TransmogCode string // the 3 char code representing the item this becomes via transmog
|
||||||
|
NightmareUpgrade string // upgraded in higher difficulties
|
||||||
|
HellUpgrade string
|
||||||
|
SourceArt string // unused?
|
||||||
|
GameArt string // unused?
|
||||||
|
Vendors map[string]*ItemVendorParams // controls vendor settings
|
||||||
|
Type string // base type in ItemTypes.txt
|
||||||
|
Type2 string
|
||||||
|
DropSound string // sfx for dropping
|
||||||
|
UseSound string // sfx for using
|
||||||
|
FlippyFile string // DC6 file animation to play when item drops on the ground
|
||||||
|
InventoryFile string // DC6 file used in your inventory
|
||||||
|
UniqueInventoryFile string // DC6 file used by the unique version of this item
|
||||||
|
SetInventoryFile string // DC6 file used by the set version of this item
|
||||||
|
Code string // identifies the item
|
||||||
|
NameString string // seems to be identical to code?
|
||||||
|
AlternateGfx string // code of the DCC used when equipped
|
||||||
|
OpenBetaGfx string // unknown
|
||||||
|
NormalCode string
|
||||||
|
UberCode string
|
||||||
|
UltraCode string
|
||||||
|
Name string
|
||||||
|
Source d2enum.InventoryItemType
|
||||||
|
|
||||||
|
Version int // 0 = classic, 100 = expansion
|
||||||
|
Rarity int // higher, the rarer
|
||||||
|
MinAC int
|
||||||
|
MaxAC int
|
||||||
|
Absorbs int // unused?
|
||||||
|
Speed int // affects movement speed of wielder, >0 = you move slower, <0 = you move faster
|
||||||
|
RequiredStrength int
|
||||||
|
Block int // chance to block, capped at 75%
|
||||||
|
Durability int // base durability 0-255
|
||||||
|
Level int // base item level (controls monster drops, for instance a lv20 monster cannot drop a lv30 item)
|
||||||
|
RequiredLevel int // required level to wield
|
||||||
|
Cost int // base cost
|
||||||
|
GambleCost int // for reference only, not used
|
||||||
|
MagicLevel int // additional magic level (for gambling?)
|
||||||
|
AutoPrefix int // prefix automatically assigned to this item on spawn, maps to group column of Automagic.txt
|
||||||
|
SpellOffset int // unknown
|
||||||
|
Component int // corresponds to Composit.txt, player animation layer used by this
|
||||||
|
InventoryWidth int
|
||||||
|
InventoryHeight int
|
||||||
|
GemSockets int // number of gems to store
|
||||||
|
GemApplyType int // what kind of gem effect is applied
|
||||||
|
// 0 = weapon, 1= armor or helmet, 2 = shield
|
||||||
|
|
||||||
|
// these represent how player animations and graphics change upon wearing this
|
||||||
|
// these come from ArmType.txt
|
||||||
|
AnimRightArm int
|
||||||
|
AnimLeftArm int
|
||||||
|
AnimTorso int
|
||||||
|
AnimLegs int
|
||||||
|
AnimRightShoulderPad int
|
||||||
|
AnimLeftShoulderPad int
|
||||||
|
|
||||||
|
MinStack int // min size of stack when item is spawned, used if stackable
|
||||||
|
MaxStack int // max size of stack when item is spawned
|
||||||
|
DropSfxFrame int // what frame of drop animation the sfx triggers on
|
||||||
|
TransTable int // unknown, related to blending mode?
|
||||||
|
LightRadius int // apparently unused
|
||||||
|
Quest int // indicates that this item belongs to a given quest?
|
||||||
|
MissileType int // missile gfx for throwing
|
||||||
|
DurabilityWarning int // controls what warning icon appears when durability is low
|
||||||
|
QuantityWarning int // controls at what quantity the low quantity warning appears
|
||||||
|
MinDamage int
|
||||||
|
MaxDamage int
|
||||||
|
StrengthBonus int
|
||||||
|
DexterityBonus int
|
||||||
|
// final mindam = min * str / strbonus + min * dex / dexbonus
|
||||||
|
// same for maxdam
|
||||||
|
|
||||||
|
GemOffset int // unknown
|
||||||
|
BitField1 int // 1 = leather item, 3 = metal
|
||||||
|
ColorTransform int // colormap to use for player's gfx
|
||||||
|
InventoryColorTransform int // colormap to use for inventory's gfx
|
||||||
|
Min2HandDamage int
|
||||||
|
Max2HandDamage int
|
||||||
|
MinMissileDamage int // ranged damage stats
|
||||||
|
MaxMissileDamage int
|
||||||
|
MissileSpeed int // unknown, affects movement speed of wielder during ranged attacks?
|
||||||
|
ExtraRange int // base range = 1, if this is non-zero add this to the range
|
||||||
|
// final mindam = min * str / strbonus + min * dex / dexbonus
|
||||||
|
// same for maxdam
|
||||||
|
RequiredDexterity int
|
||||||
|
SpawnStack int // unknown, something to do with stack size when spawned (sold maybe?)
|
||||||
|
TransmogMin int // min amount of the transmog item to create
|
||||||
|
TransmogMax int // max ''
|
||||||
|
SpellIcon int // which icon to display when used? Is this always -1?
|
||||||
|
SpellType int // determines what kind of function is used when you use this item
|
||||||
|
EffectLength int // timer for timed usage effects
|
||||||
|
SpellDescriptionType int // specifies how to format the usage description
|
||||||
|
// 0 = none, 1 = use desc string, 2 = use desc string + calc value
|
||||||
|
|
||||||
|
AutoBelt bool // if true, item is put into your belt when picked up
|
||||||
|
HasInventory bool // if true, item can store gems or runes
|
||||||
|
CompactSave bool // if true, doesn't store any stats upon saving
|
||||||
|
Spawnable bool // if 0, cannot spawn in shops
|
||||||
|
NoDurability bool // if true, item has no durability
|
||||||
|
Useable bool // can be used via right click if true
|
||||||
|
// game knows what to do if used by item code
|
||||||
|
Throwable bool
|
||||||
|
Stackable bool // can be stacked in inventory
|
||||||
|
Unique bool // if true, only spawns as unique
|
||||||
|
Transparent bool // unused
|
||||||
|
Quivered bool // if true, requires ammo to use
|
||||||
|
Belt bool // tells what kind of belt this item is
|
||||||
|
SkipName bool // if true, don't include the base name in the item description
|
||||||
|
Nameable bool // if true, item can be personalized
|
||||||
|
BarbOneOrTwoHanded bool // if true, barb can wield this in one or two hands
|
||||||
|
UsesTwoHands bool // if true, it's a 2handed weapon
|
||||||
|
QuestDifficultyCheck bool // if true, item only works in the difficulty it was found in
|
||||||
|
PermStoreItem bool // if true, vendor will always sell this
|
||||||
|
Transmogrify bool // if true, can be turned into another item via right click
|
||||||
|
Multibuy bool // if true, when you buy via right click + shift it will fill your belt automatically
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemUsageStat the stat that gets applied when the item is used
|
||||||
|
type ItemUsageStat struct {
|
||||||
|
Stat string // name of the stat to add to
|
||||||
|
Calc d2calculation.CalcString // calc string representing the amount to add
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemVendorParams are parameters that vendors use
|
||||||
|
type ItemVendorParams struct {
|
||||||
|
Min int // minimum of this item they can stock
|
||||||
|
Max int // max they can stock
|
||||||
|
MagicMin int
|
||||||
|
MagicMax int
|
||||||
|
MagicLevel int
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadMiscItems loads ItemCommonRecords from misc.txt
|
||||||
|
func miscItemsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records, err := loadCommonItems(d, d2enum.InventoryItemTypeItem)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d misc items", len(records))
|
||||||
|
|
||||||
|
r.Item.Misc = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func itemQualityLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(ItemQualities)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
qual := &ItemQualityRecord{
|
||||||
|
NumMods: d.Number("nummods"),
|
||||||
|
Mod1Code: d.String("mod1code"),
|
||||||
|
Mod1Param: d.Number("mod1param"),
|
||||||
|
Mod1Min: d.Number("mod1min"),
|
||||||
|
Mod1Max: d.Number("mod1max"),
|
||||||
|
Mod2Code: d.String("mod2code"),
|
||||||
|
Mod2Param: d.Number("mod2param"),
|
||||||
|
Mod2Min: d.Number("mod2min"),
|
||||||
|
Mod2Max: d.Number("mod2max"),
|
||||||
|
Armor: d.Bool("armor"),
|
||||||
|
Weapon: d.Bool("weapon"),
|
||||||
|
Shield: d.Bool("shield"),
|
||||||
|
Thrown: d.Bool("thrown"),
|
||||||
|
Scepter: d.Bool("scepter"),
|
||||||
|
Wand: d.Bool("wand"),
|
||||||
|
Staff: d.Bool("staff"),
|
||||||
|
Bow: d.Bool("bow"),
|
||||||
|
Boots: d.Bool("boots"),
|
||||||
|
Gloves: d.Bool("gloves"),
|
||||||
|
Belt: d.Bool("belt"),
|
||||||
|
Level: d.Number("level"),
|
||||||
|
Multiply: d.Number("multiply"),
|
||||||
|
Add: d.Number("add"),
|
||||||
|
}
|
||||||
|
|
||||||
|
records[strconv.Itoa(len(records))] = qual
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Item.Quality = records
|
||||||
|
|
||||||
|
log.Printf("Loaded %d ItemQualities records", len(records))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// ItemQualities stores all of the QualityRecords
|
||||||
|
type ItemQualities map[string]*ItemQualityRecord
|
||||||
|
|
||||||
|
// ItemQualityRecord represents a single row of ItemQualities.txt, which controls
|
||||||
|
// properties for superior quality items
|
||||||
|
type ItemQualityRecord struct {
|
||||||
|
NumMods int
|
||||||
|
Mod1Code string
|
||||||
|
Mod1Param int
|
||||||
|
Mod1Min int
|
||||||
|
Mod1Max int
|
||||||
|
Mod2Code string
|
||||||
|
Mod2Param int
|
||||||
|
Mod2Min int
|
||||||
|
Mod2Max int
|
||||||
|
|
||||||
|
// The following fields determine this row's applicability to
|
||||||
|
// categories of item.
|
||||||
|
Armor bool
|
||||||
|
Weapon bool
|
||||||
|
Shield bool
|
||||||
|
Thrown bool
|
||||||
|
Scepter bool
|
||||||
|
Wand bool
|
||||||
|
Staff bool
|
||||||
|
Bow bool
|
||||||
|
Boots bool
|
||||||
|
Gloves bool
|
||||||
|
Belt bool
|
||||||
|
|
||||||
|
Level int
|
||||||
|
Multiply int
|
||||||
|
Add int
|
||||||
|
}
|
|
@ -0,0 +1,64 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadItemRatios loads all of the ItemRatioRecords from ItemRatio.txt
|
||||||
|
func itemRatioLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(map[string]*ItemRatioRecord)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &ItemRatioRecord{
|
||||||
|
Function: d.String("Function"),
|
||||||
|
Version: d.Bool("Version"),
|
||||||
|
Uber: d.Bool("Uber"),
|
||||||
|
ClassSpecific: d.Bool("Class Specific"),
|
||||||
|
UniqueDropInfo: DropRatioInfo{
|
||||||
|
Frequency: d.Number("Unique"),
|
||||||
|
Divisor: d.Number("UniqueDivisor"),
|
||||||
|
DivisorMin: d.Number("UniqueMin"),
|
||||||
|
},
|
||||||
|
RareDropInfo: DropRatioInfo{
|
||||||
|
Frequency: d.Number("Rare"),
|
||||||
|
Divisor: d.Number("RareDivisor"),
|
||||||
|
DivisorMin: d.Number("RareMin"),
|
||||||
|
},
|
||||||
|
SetDropInfo: DropRatioInfo{
|
||||||
|
Frequency: d.Number("Set"),
|
||||||
|
Divisor: d.Number("SetDivisor"),
|
||||||
|
DivisorMin: d.Number("SetMin"),
|
||||||
|
},
|
||||||
|
MagicDropInfo: DropRatioInfo{
|
||||||
|
Frequency: d.Number("Magic"),
|
||||||
|
Divisor: d.Number("MagicDivisor"),
|
||||||
|
DivisorMin: d.Number("MagicMin"),
|
||||||
|
},
|
||||||
|
HiQualityDropInfo: DropRatioInfo{
|
||||||
|
Frequency: d.Number("HiQuality"),
|
||||||
|
Divisor: d.Number("HiQualityDivisor"),
|
||||||
|
DivisorMin: 0,
|
||||||
|
},
|
||||||
|
NormalDropInfo: DropRatioInfo{
|
||||||
|
Frequency: d.Number("Normal"),
|
||||||
|
Divisor: d.Number("NormalDivisor"),
|
||||||
|
DivisorMin: 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
records[record.Function+strconv.FormatBool(record.Version)] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d ItemRatio records", len(records))
|
||||||
|
|
||||||
|
r.Item.Ratios = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// ItemRatios holds all of the ItemRatioRecords from ItemRatio.txt
|
||||||
|
type ItemRatios map[string]*ItemRatioRecord
|
||||||
|
|
||||||
|
// A helper type for item drop calculation
|
||||||
|
type DropRatioInfo struct {
|
||||||
|
Frequency int
|
||||||
|
Divisor int
|
||||||
|
DivisorMin int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemRatioRecord encapsulates information found in ItemRatio.txt, it specifies drop ratios
|
||||||
|
// for various types of items
|
||||||
|
// The information has been gathered from [https://d2mods.info/forum/kb/viewarticle?a=387]
|
||||||
|
type ItemRatioRecord struct {
|
||||||
|
Function string
|
||||||
|
// 0 for classic, 1 for LoD
|
||||||
|
Version bool
|
||||||
|
|
||||||
|
// 0 for normal, 1 for exceptional
|
||||||
|
Uber bool
|
||||||
|
ClassSpecific bool
|
||||||
|
|
||||||
|
// All following fields are used in item drop calculation
|
||||||
|
UniqueDropInfo DropRatioInfo
|
||||||
|
RareDropInfo DropRatioInfo
|
||||||
|
SetDropInfo DropRatioInfo
|
||||||
|
MagicDropInfo DropRatioInfo
|
||||||
|
HiQualityDropInfo DropRatioInfo
|
||||||
|
NormalDropInfo DropRatioInfo
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadItemTypes loads ItemType records
|
||||||
|
func itemTypesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(ItemTypes)
|
||||||
|
|
||||||
|
charCodeMap := map[string]d2enum.Hero{
|
||||||
|
"ama": d2enum.HeroAmazon,
|
||||||
|
"ass": d2enum.HeroAssassin,
|
||||||
|
"bar": d2enum.HeroBarbarian,
|
||||||
|
"dru": d2enum.HeroDruid,
|
||||||
|
"nec": d2enum.HeroNecromancer,
|
||||||
|
"pal": d2enum.HeroPaladin,
|
||||||
|
"sor": d2enum.HeroSorceress,
|
||||||
|
}
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
if d.String("*eol") == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
itemType := &ItemTypeRecord{
|
||||||
|
Name: d.String("ItemType"),
|
||||||
|
Code: d.String("Code"),
|
||||||
|
Equiv1: d.String("Equiv1"),
|
||||||
|
Equiv2: d.String("Equiv2"),
|
||||||
|
Repair: d.Number("Repair") > 0,
|
||||||
|
Body: d.Number("Body") > 0,
|
||||||
|
BodyLoc1: d.Number("BodyLoc1"),
|
||||||
|
BodyLoc2: d.Number("BodyLoc2"),
|
||||||
|
Shoots: d.String("Shoots"),
|
||||||
|
Quiver: d.String("Quiver"),
|
||||||
|
Throwable: d.Number("Throwable") > 0,
|
||||||
|
Reload: d.Number("Reload") > 0,
|
||||||
|
ReEquip: d.Number("ReEquip") > 0,
|
||||||
|
AutoStack: d.Number("AutoStack") > 0,
|
||||||
|
Magic: d.Number("Magic") > 0,
|
||||||
|
Rare: d.Number("Rare") > 0,
|
||||||
|
Normal: d.Number("Normal") > 0,
|
||||||
|
Charm: d.Number("Charm") > 0,
|
||||||
|
Gem: d.Number("Gem") > 0,
|
||||||
|
Beltable: d.Number("Beltable") > 0,
|
||||||
|
MaxSock1: d.Number("MaxSock1"),
|
||||||
|
MaxSock25: d.Number("MaxSock25"),
|
||||||
|
MaxSock40: d.Number("MaxSock40"),
|
||||||
|
TreasureClass: d.Number("TreasureClass"),
|
||||||
|
Rarity: d.Number("Rarity"),
|
||||||
|
StaffMods: charCodeMap[d.String("StaffMods")],
|
||||||
|
CostFormula: d.Number("CostFormula"),
|
||||||
|
Class: charCodeMap[d.String("Class")],
|
||||||
|
VarInvGfx: d.Number("VarInvGfx"),
|
||||||
|
InvGfx1: d.String("InvGfx1"),
|
||||||
|
InvGfx2: d.String("InvGfx2"),
|
||||||
|
InvGfx3: d.String("InvGfx3"),
|
||||||
|
InvGfx4: d.String("InvGfx4"),
|
||||||
|
InvGfx5: d.String("InvGfx5"),
|
||||||
|
InvGfx6: d.String("InvGfx6"),
|
||||||
|
StorePage: d.String("StorePage"),
|
||||||
|
}
|
||||||
|
|
||||||
|
records[itemType.Code] = itemType
|
||||||
|
}
|
||||||
|
|
||||||
|
equivMap := LoadItemEquivalencies(r.Item.All, records)
|
||||||
|
|
||||||
|
for idx := range records {
|
||||||
|
records[idx].EquivalentItems = equivMap[records[idx].Code]
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d ItemType records", len(records))
|
||||||
|
|
||||||
|
r.Item.Types = records
|
||||||
|
r.Item.Equivalency = equivMap
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadItemEquivalencies loads a map of ItemType string codes to slices of ItemCommonRecord pointers
|
||||||
|
func LoadItemEquivalencies(allItems CommonItems, allTypes ItemTypes) ItemEquivalenceMap {
|
||||||
|
equivMap := make(ItemEquivalenceMap)
|
||||||
|
|
||||||
|
for typeCode := range allTypes {
|
||||||
|
code := []string{
|
||||||
|
typeCode,
|
||||||
|
allTypes[typeCode].Equiv1,
|
||||||
|
allTypes[typeCode].Equiv2,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, str := range code {
|
||||||
|
if str == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if equivMap[str] == nil {
|
||||||
|
equivMap[str] = make([]*ItemCommonRecord, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for icrCode := range allItems {
|
||||||
|
commonItem := allItems[icrCode]
|
||||||
|
updateEquivalencies(allTypes, equivMap, commonItem, allTypes[commonItem.Type], nil)
|
||||||
|
|
||||||
|
if commonItem.Type2 != "" { // some items (like gems) have a secondary type
|
||||||
|
updateEquivalencies(allTypes, equivMap, commonItem, allTypes[commonItem.Type2], nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return equivMap
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateEquivalencies(
|
||||||
|
allTypes ItemTypes,
|
||||||
|
equivMap ItemEquivalenceMap,
|
||||||
|
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, equivMap[itemType.Code]) {
|
||||||
|
equivMap[itemType.Code] = append(equivMap[itemType.Code], icr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if itemType.Equiv1 != "" {
|
||||||
|
updateEquivalencies(allTypes, equivMap, icr, allTypes[itemType.Equiv1], checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
if itemType.Equiv2 != "" {
|
||||||
|
updateEquivalencies(allTypes, equivMap, icr, allTypes[itemType.Equiv2], checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func itemEquivPresent(icr *ItemCommonRecord, list []*ItemCommonRecord) bool {
|
||||||
|
for idx := range list {
|
||||||
|
if list[idx] == icr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,196 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ItemTypes stores all of the ItemTypeRecords
|
||||||
|
type ItemTypes map[string]*ItemTypeRecord
|
||||||
|
|
||||||
|
// ItemEquivalenceMap describes item equivalencies for ItemTypes
|
||||||
|
type ItemEquivalenceMap map[string]ItemEquivalenceList
|
||||||
|
|
||||||
|
// ItemEquivalenceList is an equivalence map that each ItemTypeRecord will have
|
||||||
|
type ItemEquivalenceList []*ItemCommonRecord
|
||||||
|
|
||||||
|
// ItemEquivalenceByRecord is used for getting equivalent item codes using an ItemCommonRecord
|
||||||
|
type ItemEquivalenceByRecord map[*ItemCommonRecord][]string
|
||||||
|
|
||||||
|
// ItemTypeRecord describes the types for items
|
||||||
|
type ItemTypeRecord struct {
|
||||||
|
// Name (ItemType)
|
||||||
|
// A comment field that contains the “internal name” of this iType,
|
||||||
|
// you can basically enter anything you wish here,
|
||||||
|
// but since you can add as many comment columns as you wish,
|
||||||
|
// there is no reason to use it for another purpose .
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// Code
|
||||||
|
// The ID pointer of this ItemType, this pointer is used in many txt files (armor.txt,
|
||||||
|
// cubemain.txt, misc.txt, skills.txt, treasureclassex.txt, weapons.txt),
|
||||||
|
// never use the same ID pointer twice,
|
||||||
|
// the game will only use the first instance and ignore all other occurrences.
|
||||||
|
// ID pointers are case sensitive, 3-4 chars long and can contain numbers, letters and symbols.
|
||||||
|
Code string
|
||||||
|
|
||||||
|
// Equiv1-2
|
||||||
|
// This is used to define the parent iType, note that an iType can have multiple parents (
|
||||||
|
// as will be shown in the cladogram – link below),
|
||||||
|
// the only thing you must avoid at all cost is creating infinite loops.
|
||||||
|
// I haven't ever tested what happens when you create an iType loop,
|
||||||
|
// but infinite loops are something you should always avoid.
|
||||||
|
Equiv1 string
|
||||||
|
Equiv2 string
|
||||||
|
|
||||||
|
// Shoots
|
||||||
|
// This column specifies which type of quiver (“ammo”) this iType (
|
||||||
|
// in case it is a weapon) requires in order to shoot (
|
||||||
|
// you use the ID pointer of the quiver iType here).
|
||||||
|
// Caution: The place it checks which missile to pick (either arrow, bolt,
|
||||||
|
// explosive arrow or magic arrow) is buried deep within D2Common.dll,
|
||||||
|
// the section can be modified, there is an extensive post discussing this in Code Editing.
|
||||||
|
// - Thanks go to Kingpin for spotting a silly little mistake in here.
|
||||||
|
Shoots string
|
||||||
|
|
||||||
|
// Quiver
|
||||||
|
// The equivalent to the previous column,
|
||||||
|
// in here you specify which weapon this quiver is linked to. Make sure the two columns match. (
|
||||||
|
// this also uses the ID pointer of course).
|
||||||
|
Quiver string
|
||||||
|
|
||||||
|
// InvGfx1-6
|
||||||
|
// This column contains the file names of the inventory graphics that are randomly picked for
|
||||||
|
// this iType, so if you use columns 1-3, you will set VarInvGfx to 3 (duh).
|
||||||
|
InvGfx1 string
|
||||||
|
InvGfx2 string
|
||||||
|
InvGfx3 string
|
||||||
|
InvGfx4 string
|
||||||
|
InvGfx5 string
|
||||||
|
InvGfx6 string
|
||||||
|
|
||||||
|
// StorePage
|
||||||
|
// The page code for the page a vendor should place this iType in when sold,
|
||||||
|
// if you enable the magic tab in D2Client.dll,
|
||||||
|
// you need to use the proper code here to put items in that tab.
|
||||||
|
// Right now the ones used are weap = weapons1 and 2, armo = armor and misc = miscellaneous.
|
||||||
|
StorePage string
|
||||||
|
|
||||||
|
// BodyLoc1-2
|
||||||
|
// If you have set the previous column to 1,
|
||||||
|
// you need to specify the inventory slots in which the item has to be equipped. (
|
||||||
|
// the codes used by this field are read from BodyLocs.txt)
|
||||||
|
BodyLoc1 int
|
||||||
|
BodyLoc2 int
|
||||||
|
|
||||||
|
// MaxSock1, MaxSock25, MaxSock40
|
||||||
|
// Maximum sockets for iLvl 1-25,
|
||||||
|
// 26-40 and 40+. The range is hardcoded but the location is known,
|
||||||
|
// so you can alter around the range to your liking. On normal,
|
||||||
|
// items dropped from monsters are limited to 3, on nightmare to 4 and on hell to 6 sockets,
|
||||||
|
// irregardless of this columns content.
|
||||||
|
MaxSock1 int
|
||||||
|
MaxSock25 int
|
||||||
|
MaxSock40 int
|
||||||
|
|
||||||
|
// TreasureClass
|
||||||
|
// Can this iType ID Pointer be used as an auto TC in TreasureClassEx.txt. 1=Yes,
|
||||||
|
// 0=No. *Such as armo3-99 and weap3-99 etc.
|
||||||
|
TreasureClass int
|
||||||
|
|
||||||
|
// Rarity
|
||||||
|
// Dunno what it does, may have to do with the chance that an armor or weapon rack will pick
|
||||||
|
// items of this iType. If it works like other rarity fields,
|
||||||
|
// the chance is rarity / total_rarity * 100.
|
||||||
|
Rarity int
|
||||||
|
|
||||||
|
// StaffMods
|
||||||
|
// Contains the class code for the character class that should get +skills from this iType (
|
||||||
|
// such as wands that can spawn with +Necromancer skills). Note,
|
||||||
|
// this only works if the item is not low quality, set or unique. Note,
|
||||||
|
// that this uses the vanilla min/max skill IDs for each class as the range for the skill pool,
|
||||||
|
// so if you add new class skills to the end of the file, you should use automagic.txt instead
|
||||||
|
StaffMods d2enum.Hero
|
||||||
|
|
||||||
|
// CostFormula
|
||||||
|
// Does the game generate the sell/repair/buy prices of this iType based on its modifiers or does
|
||||||
|
// it use only the cost specific in the respective item txt files. 2=Organ (
|
||||||
|
// probably higher price based on unit that dropped the organ), 1=Yes, 0=No.
|
||||||
|
// Note: Only applies to items that are not unique or set, for those the price is solely controlled
|
||||||
|
// by the base item file and by the bonus to price given in SetItems and UniqueItems txt files.
|
||||||
|
// The exact functionality remains unknown, as for example charms, have this disabled.
|
||||||
|
CostFormula int
|
||||||
|
|
||||||
|
// Class
|
||||||
|
// Contains the class code for the class that should be able to use this iType (
|
||||||
|
// for class specific items).
|
||||||
|
Class d2enum.Hero
|
||||||
|
|
||||||
|
// VarInvGfx
|
||||||
|
// This column contains the sum of randomly picked inventory graphics this iType can have.
|
||||||
|
VarInvGfx int
|
||||||
|
|
||||||
|
// Repair
|
||||||
|
// Boolean, 1=Merchants can repair this item type, 0=Merchants cannot repair this iType (note,
|
||||||
|
// this also refers to charges being rechargeable).
|
||||||
|
Repair bool
|
||||||
|
|
||||||
|
// Body
|
||||||
|
// Boolean, 1=The character can wear this iType,
|
||||||
|
// 0=This iType can only be carried in the inventory,
|
||||||
|
// cube or stash (and belt if it is set as “beltable” in the other item related txt files)
|
||||||
|
Body bool
|
||||||
|
|
||||||
|
// Throwable
|
||||||
|
// Can this iType be thrown (determines whenever it uses the quantity and throwing damage columns
|
||||||
|
// in Weapons.txt for example).
|
||||||
|
Throwable bool
|
||||||
|
|
||||||
|
// Reload
|
||||||
|
// Can the this item be re-stacked via drag and drop. 1=Yes, 0=No.
|
||||||
|
Reload bool
|
||||||
|
|
||||||
|
// ReEquip
|
||||||
|
// If the ammo runs out the game will automatically pick the next item of the same iType to
|
||||||
|
// be equipped in it's place.
|
||||||
|
// 1=Yes, 0=No. (more clearly, when you use up all the arrows in a quiver, the next quiver,
|
||||||
|
// if available, will be equipped in its place).
|
||||||
|
ReEquip bool
|
||||||
|
|
||||||
|
// AutoStack
|
||||||
|
// Are identical stacks automatically combined when you pick the up? 1=Yes, 0=No. (for example,
|
||||||
|
// which you pick up throwing potions or normal javelins,
|
||||||
|
// they are automatically combined with those you already have)
|
||||||
|
AutoStack bool
|
||||||
|
|
||||||
|
// Magic
|
||||||
|
// Is this iType always Magic? 1=Yes, 0=No.
|
||||||
|
Magic bool
|
||||||
|
|
||||||
|
// Rare
|
||||||
|
// Can this iType spawn as a rare item?
|
||||||
|
// 1=Yes, 0=No.
|
||||||
|
// Note: If you want an item that spawns only as magic or rare,
|
||||||
|
// you need to set the previous column to 1 as well.
|
||||||
|
Rare bool
|
||||||
|
|
||||||
|
// Normal
|
||||||
|
// Is this iType always Normal? 1=Yes, 0=No.
|
||||||
|
Normal bool
|
||||||
|
|
||||||
|
// Charm
|
||||||
|
// Does this iType function as a charm? 1=Yes, 0=No. Note: This effect is hardcoded,
|
||||||
|
// if you need a new charm type, you must use the char iType in one of the equivs.
|
||||||
|
Charm bool
|
||||||
|
|
||||||
|
// Gem
|
||||||
|
// Can this iType be inserted into sockets? 1=Yes,
|
||||||
|
// 0=No (Link your item to the sock iType instead to achieve this).
|
||||||
|
Gem bool
|
||||||
|
|
||||||
|
// Beltable
|
||||||
|
// Can this iType be placed in your characters belt slots? 1=Yes,
|
||||||
|
// 0=No. (This requires further tweaking in other txt files).
|
||||||
|
Beltable bool
|
||||||
|
|
||||||
|
EquivalentItems ItemEquivalenceList
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadWeapons loads weapon records
|
||||||
|
func weaponsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records, err := loadCommonItems(d, d2enum.InventoryItemTypeWeapon)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d weapons", len(records))
|
||||||
|
|
||||||
|
r.Item.Weapons = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadItemStatCosts loads ItemStatCostRecord's from text
|
||||||
|
func itemStatCostLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(ItemStatCosts)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &ItemStatCostRecord{
|
||||||
|
Name: d.String("Stat"),
|
||||||
|
Index: d.Number("ID"),
|
||||||
|
|
||||||
|
Signed: d.Number("Signed") > 0,
|
||||||
|
KeepZero: d.Number("keepzero") > 0,
|
||||||
|
|
||||||
|
// Ranged: d.Number("Ranged") > 0,
|
||||||
|
MinAccr: d.Number("MinAccr"),
|
||||||
|
|
||||||
|
UpdateAnimRate: d.Number("UpdateAnimRate") > 0,
|
||||||
|
|
||||||
|
SendOther: d.Number("Send Other") > 0,
|
||||||
|
SendBits: d.Number("Send Bits"),
|
||||||
|
SendParam: d.Number("Send Param Bits"),
|
||||||
|
|
||||||
|
Saved: d.Number("CSvBits") > 0,
|
||||||
|
SavedSigned: d.Number("CSvSigned") > 0,
|
||||||
|
SavedBits: d.Number("CSvBits"),
|
||||||
|
SaveBits: d.Number("Save Bits"),
|
||||||
|
SaveAdd: d.Number("Save Add"),
|
||||||
|
SaveParamBits: d.Number("Save Param Bits"),
|
||||||
|
|
||||||
|
Encode: d2enum.EncodingType(d.Number("Encode")),
|
||||||
|
|
||||||
|
CallbackEnabled: d.Number("fCallback") > 0,
|
||||||
|
|
||||||
|
CostAdd: d.Number("Add"),
|
||||||
|
CostMultiply: d.Number("Multiply"),
|
||||||
|
ValShift: d.Number("ValShift"),
|
||||||
|
|
||||||
|
OperatorType: d2enum.OperatorType(d.Number("op")),
|
||||||
|
OpParam: d.Number("op param"),
|
||||||
|
OpBase: d.String("op base"),
|
||||||
|
OpStat1: d.String("op stat1"),
|
||||||
|
OpStat2: d.String("op stat2"),
|
||||||
|
OpStat3: d.String("op stat3"),
|
||||||
|
|
||||||
|
Direct: d.Number("direct") > 0,
|
||||||
|
MaxStat: d.String("maxstat"),
|
||||||
|
|
||||||
|
ItemSpecific: d.Number("itemspecific") > 0,
|
||||||
|
DamageRelated: d.Number("damagerelated") > 0,
|
||||||
|
|
||||||
|
EventID1: d2enum.GetItemEventType(d.String("itemevent1")),
|
||||||
|
EventID2: d2enum.GetItemEventType(d.String("itemevent2")),
|
||||||
|
EventFuncID1: d2enum.ItemEventFuncID(d.Number("itemeventfunc1")),
|
||||||
|
EventFuncID2: d2enum.ItemEventFuncID(d.Number("itemeventfunc2")),
|
||||||
|
|
||||||
|
DescPriority: d.Number("descpriority"),
|
||||||
|
DescFnID: d.Number("descfunc"),
|
||||||
|
// DescVal: d.Number("descval"), // needs special handling
|
||||||
|
DescStrPos: d.String("descstrpos"),
|
||||||
|
DescStrNeg: d.String("descstrneg"),
|
||||||
|
DescStr2: d.String("descstr2"),
|
||||||
|
|
||||||
|
DescGroup: d.Number("dgrp"),
|
||||||
|
DescGroupFuncID: d.Number("dgrpfunc"),
|
||||||
|
|
||||||
|
DescGroupVal: d.Number("dgrpval"),
|
||||||
|
DescGroupStrPos: d.String("dgrpstrpos"),
|
||||||
|
DescGroupStrNeg: d.String("dgrpstrneg"),
|
||||||
|
DescGroupStr2: d.String("dgrpstr2"),
|
||||||
|
|
||||||
|
Stuff: d.String("stuff"),
|
||||||
|
}
|
||||||
|
|
||||||
|
descValStr := d.String("descval")
|
||||||
|
switch descValStr {
|
||||||
|
case "2":
|
||||||
|
record.DescVal = 2
|
||||||
|
case "0":
|
||||||
|
record.DescVal = 0
|
||||||
|
default:
|
||||||
|
// handle empty fields, seems like they should have been 1
|
||||||
|
record.DescVal = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
records[record.Name] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d ItemStatCost records", len(records))
|
||||||
|
|
||||||
|
r.Item.Stats = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
|
||||||
|
// ItemStatCostRecord represents a row from itemstatcost.txt
|
||||||
|
// these records describe the stat values and costs (in shops) of items
|
||||||
|
// refer to https://d2mods.info/forum/kb/viewarticle?a=448
|
||||||
|
type ItemStatCostRecord struct {
|
||||||
|
Name string
|
||||||
|
OpBase string
|
||||||
|
OpStat1 string
|
||||||
|
OpStat2 string
|
||||||
|
OpStat3 string
|
||||||
|
|
||||||
|
MaxStat string // if Direct true, will not exceed val of MaxStat
|
||||||
|
DescStrPos string // string used when val is positive
|
||||||
|
DescStrNeg string
|
||||||
|
DescStr2 string // additional string used by some string funcs
|
||||||
|
DescGroupStrPos string // string used when val is positive
|
||||||
|
DescGroupStrNeg string
|
||||||
|
DescGroupStr2 string // additional string used by some string funcs
|
||||||
|
|
||||||
|
// Stuff
|
||||||
|
// Stay far away from this column unless you really know what you're
|
||||||
|
// doing and / or work for Blizzard, this column is used during bin-file
|
||||||
|
// creation to generate a cache regulating the op-stat stuff and other
|
||||||
|
// things, changing it can be futile, it works like the constants column
|
||||||
|
// in MonUMod.txt and has no other relation to ItemStatCost.txt, the first
|
||||||
|
// stat in the file simply must have this set or else you may break the
|
||||||
|
// entire op stuff.
|
||||||
|
Stuff string
|
||||||
|
|
||||||
|
Index int
|
||||||
|
|
||||||
|
// path_d2.mpq version doesnt have Ranged columne, excluding for now
|
||||||
|
// Ranged bool // game attempts to keep stat in a range, like strength >-1
|
||||||
|
MinAccr int // minimum ranged value
|
||||||
|
|
||||||
|
SendBits int // #bits to send in stat update
|
||||||
|
SendParam int // #bits to send in stat update
|
||||||
|
|
||||||
|
SavedBits int // #bits allocated to the value in .d2s file
|
||||||
|
|
||||||
|
SaveBits int // #bits saved to .d2s files, max == 2^SaveBits-1
|
||||||
|
SaveAdd int // how large the negative range is (lowers max, as well)
|
||||||
|
SaveParamBits int // #param bits are saved (safe value is 17)
|
||||||
|
|
||||||
|
Encode d2enum.EncodingType // how the stat is encoded in .d2s files
|
||||||
|
|
||||||
|
// these two fields control additional cost on items
|
||||||
|
// cost * (1 + value * multiply / 1024)) + add (...)
|
||||||
|
CostAdd int
|
||||||
|
CostMultiply int
|
||||||
|
// CostDivide // exists in txt, but division hardcoded to 1024
|
||||||
|
// if divide is used, could we do (?):
|
||||||
|
// cost * (1 + value * multiply / divide)) + add (...)
|
||||||
|
|
||||||
|
ValShift int // controls how stat is stored in .d2s
|
||||||
|
// so that you can save `+1` instead of `+256`
|
||||||
|
|
||||||
|
OperatorType d2enum.OperatorType
|
||||||
|
OpParam int
|
||||||
|
|
||||||
|
EventID1 d2enum.ItemEventType
|
||||||
|
EventID2 d2enum.ItemEventType
|
||||||
|
EventFuncID1 d2enum.ItemEventFuncID
|
||||||
|
EventFuncID2 d2enum.ItemEventFuncID
|
||||||
|
|
||||||
|
DescPriority int // determines order when displayed
|
||||||
|
DescFnID int
|
||||||
|
|
||||||
|
// Controls whenever and if so in what way the stat value is shown
|
||||||
|
// 0 = doesn't show the value of the stat
|
||||||
|
// 1 = shows the value of the stat infront of the description
|
||||||
|
// 2 = shows the value of the stat after the description.
|
||||||
|
DescVal int
|
||||||
|
|
||||||
|
// when stats in the same group have the same value they use the
|
||||||
|
// group func for desc (they need to be in the same affix)
|
||||||
|
DescGroup int
|
||||||
|
DescGroupVal int
|
||||||
|
DescGroupFuncID int
|
||||||
|
|
||||||
|
CallbackEnabled bool // whether callback fn is called if value changes
|
||||||
|
Signed bool // whether the stat is signed
|
||||||
|
KeepZero bool // prevent from going negative (assume only client side)
|
||||||
|
UpdateAnimRate bool // when altered, forces speed handler to adjust speed
|
||||||
|
SendOther bool // whether to send to other clients
|
||||||
|
Saved bool // whether this stat is saved in .d2s files
|
||||||
|
SavedSigned bool // whether the stat is saved as signed/unsigned
|
||||||
|
Direct bool // whether is temporary or permanent
|
||||||
|
ItemSpecific bool // prevents stacking with an existing stat on item
|
||||||
|
// like when socketing a jewel
|
||||||
|
|
||||||
|
DamageRelated bool // prevents stacking of stats while dual wielding
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemStatCosts stores all of the ItemStatCostRecords
|
||||||
|
type ItemStatCosts map[string]*ItemStatCostRecord
|
|
@ -0,0 +1,174 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
)
|
||||||
|
|
||||||
|
func levelDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(LevelDetails)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &LevelDetailsRecord{
|
||||||
|
Name: d.String("Name "),
|
||||||
|
ID: d.Number("Id"),
|
||||||
|
Palette: d.Number("Pal"),
|
||||||
|
Act: d.Number("Act"),
|
||||||
|
QuestFlag: d.Number("QuestFlag"),
|
||||||
|
QuestFlagExpansion: d.Number("QuestFlagEx"),
|
||||||
|
AutomapIndex: d.Number("Layer"),
|
||||||
|
SizeXNormal: d.Number("SizeX"),
|
||||||
|
SizeYNormal: d.Number("SizeY"),
|
||||||
|
SizeXNightmare: d.Number("SizeX(N)"),
|
||||||
|
SizeYNightmare: d.Number("SizeY(N)"),
|
||||||
|
SizeXHell: d.Number("SizeX(H)"),
|
||||||
|
SizeYHell: d.Number("SizeY(H)"),
|
||||||
|
WorldOffsetX: d.Number("OffsetX"),
|
||||||
|
WorldOffsetY: d.Number("OffsetY"),
|
||||||
|
DependantLevelID: d.Number("Depend"),
|
||||||
|
TeleportFlag: d2enum.TeleportFlag(d.Number("Teleport")),
|
||||||
|
EnableRain: d.Number("Rain") > 0,
|
||||||
|
EnableMud: d.Number("Mud") > 0,
|
||||||
|
EnablePerspective: d.Number("NoPer") > 0,
|
||||||
|
EnableLineOfSightDraw: d.Number("LOSDraw") > 0,
|
||||||
|
EnableFloorFliter: d.Number("FloorFilter") > 0,
|
||||||
|
EnableBlankScreen: d.Number("BlankScreen") > 0,
|
||||||
|
EnableDrawEdges: d.Number("DrawEdges") > 0,
|
||||||
|
IsInside: d.Number("IsInside") > 0,
|
||||||
|
LevelGenerationType: d2enum.LevelGenerationType(d.Number("DrlgType")),
|
||||||
|
LevelType: d.Number("LevelType"),
|
||||||
|
SubType: d.Number("SubType"),
|
||||||
|
SubTheme: d.Number("SubTheme"),
|
||||||
|
SubWaypoint: d.Number("SubWaypoint"),
|
||||||
|
SubShrine: d.Number("SubShrine"),
|
||||||
|
LevelLinkID0: d.Number("Vis0"),
|
||||||
|
LevelLinkID1: d.Number("Vis1"),
|
||||||
|
LevelLinkID2: d.Number("Vis2"),
|
||||||
|
LevelLinkID3: d.Number("Vis3"),
|
||||||
|
LevelLinkID4: d.Number("Vis4"),
|
||||||
|
LevelLinkID5: d.Number("Vis5"),
|
||||||
|
LevelLinkID6: d.Number("Vis6"),
|
||||||
|
LevelLinkID7: d.Number("Vis7"),
|
||||||
|
WarpGraphicsID0: d.Number("Warp0"),
|
||||||
|
WarpGraphicsID1: d.Number("Warp1"),
|
||||||
|
WarpGraphicsID2: d.Number("Warp2"),
|
||||||
|
WarpGraphicsID3: d.Number("Warp3"),
|
||||||
|
WarpGraphicsID4: d.Number("Warp4"),
|
||||||
|
WarpGraphicsID5: d.Number("Warp5"),
|
||||||
|
WarpGraphicsID6: d.Number("Warp6"),
|
||||||
|
WarpGraphicsID7: d.Number("Warp7"),
|
||||||
|
LightIntensity: d.Number("Intensity"),
|
||||||
|
Red: d.Number("Red"),
|
||||||
|
Green: d.Number("Green"),
|
||||||
|
Blue: d.Number("Blue"),
|
||||||
|
PortalEnable: d.Number("Portal") > 0,
|
||||||
|
PortalRepositionEnable: d.Number("Position") > 0,
|
||||||
|
SaveMonsterStates: d.Number("SaveMonsters") > 0,
|
||||||
|
SaveMerchantStates: d.Number("SaveMonsters") > 0,
|
||||||
|
QuestID: d.Number("Quest"),
|
||||||
|
WarpClearanceDistance: d.Number("WarpDist"),
|
||||||
|
MonsterLevelNormal: d.Number("MonLvl1"),
|
||||||
|
MonsterLevelNightmare: d.Number("MonLvl2"),
|
||||||
|
MonsterLevelHell: d.Number("MonLvl3"),
|
||||||
|
MonsterLevelNormalEx: d.Number("MonLvl1Ex"),
|
||||||
|
MonsterLevelNightmareEx: d.Number("MonLvl2Ex"),
|
||||||
|
MonsterLevelHellEx: d.Number("MonLvl3Ex"),
|
||||||
|
MonsterDensityNormal: d.Number("MonDen"),
|
||||||
|
MonsterDensityNightmare: d.Number("MonDen(N)"),
|
||||||
|
MonsterDensityHell: d.Number("MonDen(H)"),
|
||||||
|
MonsterUniqueMinNormal: d.Number("MonUMin"),
|
||||||
|
MonsterUniqueMinNightmare: d.Number("MonUMin(N)"),
|
||||||
|
MonsterUniqueMinHell: d.Number("MonUMin(H)"),
|
||||||
|
MonsterUniqueMaxNormal: d.Number("MonUMax"),
|
||||||
|
MonsterUniqueMaxNightmare: d.Number("MonUMax(N)"),
|
||||||
|
MonsterUniqueMaxHell: d.Number("MonUMax(H)"),
|
||||||
|
MonsterWanderEnable: d.Number("MonWndr") > 0,
|
||||||
|
MonsterSpecialWalk: d.Number("MonSpcWalk") > 0,
|
||||||
|
NumMonsterTypes: d.Number("NumMon"),
|
||||||
|
MonsterID1Normal: d.String("mon1"),
|
||||||
|
MonsterID2Normal: d.String("mon2"),
|
||||||
|
MonsterID3Normal: d.String("mon3"),
|
||||||
|
MonsterID4Normal: d.String("mon4"),
|
||||||
|
MonsterID5Normal: d.String("mon5"),
|
||||||
|
MonsterID6Normal: d.String("mon6"),
|
||||||
|
MonsterID7Normal: d.String("mon7"),
|
||||||
|
MonsterID8Normal: d.String("mon8"),
|
||||||
|
MonsterID9Normal: d.String("mon9"),
|
||||||
|
MonsterID10Normal: d.String("mon10"),
|
||||||
|
MonsterID1Nightmare: d.String("nmon1"),
|
||||||
|
MonsterID2Nightmare: d.String("nmon2"),
|
||||||
|
MonsterID3Nightmare: d.String("nmon3"),
|
||||||
|
MonsterID4Nightmare: d.String("nmon4"),
|
||||||
|
MonsterID5Nightmare: d.String("nmon5"),
|
||||||
|
MonsterID6Nightmare: d.String("nmon6"),
|
||||||
|
MonsterID7Nightmare: d.String("nmon7"),
|
||||||
|
MonsterID8Nightmare: d.String("nmon8"),
|
||||||
|
MonsterID9Nightmare: d.String("nmon9"),
|
||||||
|
MonsterID10Nightmare: d.String("nmon10"),
|
||||||
|
MonsterID1Hell: d.String("nmon1"),
|
||||||
|
MonsterID2Hell: d.String("nmon2"),
|
||||||
|
MonsterID3Hell: d.String("nmon3"),
|
||||||
|
MonsterID4Hell: d.String("nmon4"),
|
||||||
|
MonsterID5Hell: d.String("nmon5"),
|
||||||
|
MonsterID6Hell: d.String("nmon6"),
|
||||||
|
MonsterID7Hell: d.String("nmon7"),
|
||||||
|
MonsterID8Hell: d.String("nmon8"),
|
||||||
|
MonsterID9Hell: d.String("nmon9"),
|
||||||
|
MonsterID10Hell: d.String("nmon10"),
|
||||||
|
MonsterPreferRanged: d.Number("rangedspawn") > 0,
|
||||||
|
MonsterUniqueID1: d.String("umon1"),
|
||||||
|
MonsterUniqueID2: d.String("umon2"),
|
||||||
|
MonsterUniqueID3: d.String("umon3"),
|
||||||
|
MonsterUniqueID4: d.String("umon4"),
|
||||||
|
MonsterUniqueID5: d.String("umon5"),
|
||||||
|
MonsterUniqueID6: d.String("umon6"),
|
||||||
|
MonsterUniqueID7: d.String("umon7"),
|
||||||
|
MonsterUniqueID8: d.String("umon8"),
|
||||||
|
MonsterUniqueID9: d.String("umon9"),
|
||||||
|
MonsterUniqueID10: d.String("umon10"),
|
||||||
|
MonsterCritterID1: d.String("cmon1"),
|
||||||
|
MonsterCritterID2: d.String("cmon2"),
|
||||||
|
MonsterCritterID3: d.String("cmon3"),
|
||||||
|
MonsterCritterID4: d.String("cmon4"),
|
||||||
|
MonsterCritter1SpawnChance: d.Number("cpct1"),
|
||||||
|
MonsterCritter2SpawnChance: d.Number("cpct2"),
|
||||||
|
MonsterCritter3SpawnChance: d.Number("cpct3"),
|
||||||
|
MonsterCritter4SpawnChance: d.Number("cpct4"),
|
||||||
|
SoundEnvironmentID: d.Number("SoundEnv"),
|
||||||
|
WaypointID: d.Number("Waypoint"),
|
||||||
|
LevelDisplayName: d.String("LevelName"),
|
||||||
|
LevelWarpName: d.String("LevelWarp"),
|
||||||
|
TitleImageName: d.String("EntryFile"),
|
||||||
|
ObjectGroupID0: d.Number("ObjGrp0"),
|
||||||
|
ObjectGroupID1: d.Number("ObjGrp1"),
|
||||||
|
ObjectGroupID2: d.Number("ObjGrp2"),
|
||||||
|
ObjectGroupID3: d.Number("ObjGrp3"),
|
||||||
|
ObjectGroupID4: d.Number("ObjGrp4"),
|
||||||
|
ObjectGroupID5: d.Number("ObjGrp5"),
|
||||||
|
ObjectGroupID6: d.Number("ObjGrp6"),
|
||||||
|
ObjectGroupID7: d.Number("ObjGrp7"),
|
||||||
|
ObjectGroupSpawnChance0: d.Number("ObjPrb0"),
|
||||||
|
ObjectGroupSpawnChance1: d.Number("ObjPrb1"),
|
||||||
|
ObjectGroupSpawnChance2: d.Number("ObjPrb2"),
|
||||||
|
ObjectGroupSpawnChance3: d.Number("ObjPrb3"),
|
||||||
|
ObjectGroupSpawnChance4: d.Number("ObjPrb4"),
|
||||||
|
ObjectGroupSpawnChance5: d.Number("ObjPrb5"),
|
||||||
|
ObjectGroupSpawnChance6: d.Number("ObjPrb6"),
|
||||||
|
ObjectGroupSpawnChance7: d.Number("ObjPrb7"),
|
||||||
|
}
|
||||||
|
records[record.ID] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d LevelDetails records", len(records))
|
||||||
|
|
||||||
|
r.Level.Details = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,362 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
|
||||||
|
// LevelDetails has all of the LevelDetailsRecords
|
||||||
|
type LevelDetails map[int]*LevelDetailsRecord
|
||||||
|
|
||||||
|
// LevelDetailsRecord is a representation of a row from levels.txt
|
||||||
|
// it describes lots of things about the levels, like where they are connected,
|
||||||
|
// what kinds of monsters spawn, the level generator type, and lots of other stuff.
|
||||||
|
type LevelDetailsRecord struct {
|
||||||
|
|
||||||
|
// Name
|
||||||
|
// This column has no function, it only serves as a comment field to make it
|
||||||
|
// easier to identify the Level name
|
||||||
|
Name string // Name <-- the corresponding column name in the txt
|
||||||
|
|
||||||
|
// mon1-mon25 work in Normal difficulty, while nmon1-nmon25 in Nightmare and
|
||||||
|
// Hell. They tell the game which monster ID taken from MonStats.txt.
|
||||||
|
// NOTE: you need to manually add from mon11 to mon25 and from nmon11 to
|
||||||
|
// nmon25 !
|
||||||
|
MonsterID1Normal string // mon1
|
||||||
|
MonsterID2Normal string // mon2
|
||||||
|
MonsterID3Normal string // mon3
|
||||||
|
MonsterID4Normal string // mon4
|
||||||
|
MonsterID5Normal string // mon5
|
||||||
|
MonsterID6Normal string // mon6
|
||||||
|
MonsterID7Normal string // mon7
|
||||||
|
MonsterID8Normal string // mon8
|
||||||
|
MonsterID9Normal string // mon9
|
||||||
|
MonsterID10Normal string // mon10
|
||||||
|
|
||||||
|
MonsterID1Nightmare string // nmon1
|
||||||
|
MonsterID2Nightmare string // nmon2
|
||||||
|
MonsterID3Nightmare string // nmon3
|
||||||
|
MonsterID4Nightmare string // nmon4
|
||||||
|
MonsterID5Nightmare string // nmon5
|
||||||
|
MonsterID6Nightmare string // nmon6
|
||||||
|
MonsterID7Nightmare string // nmon7
|
||||||
|
MonsterID8Nightmare string // nmon8
|
||||||
|
MonsterID9Nightmare string // nmon9
|
||||||
|
MonsterID10Nightmare string // nmon10
|
||||||
|
|
||||||
|
// Gravestench - adding additional fields for Hell, original txt combined
|
||||||
|
// the nighmare and hell ID's stringo the same field
|
||||||
|
MonsterID1Hell string // nmon1
|
||||||
|
MonsterID2Hell string // nmon2
|
||||||
|
MonsterID3Hell string // nmon3
|
||||||
|
MonsterID4Hell string // nmon4
|
||||||
|
MonsterID5Hell string // nmon5
|
||||||
|
MonsterID6Hell string // nmon6
|
||||||
|
MonsterID7Hell string // nmon7
|
||||||
|
MonsterID8Hell string // nmon8
|
||||||
|
MonsterID9Hell string // nmon9
|
||||||
|
MonsterID10Hell string // nmon10
|
||||||
|
|
||||||
|
// Works only in normal and it tells which ID will be used for Champion and
|
||||||
|
// Random Uniques. The ID is taken from MonStats.txtOnly the first ten
|
||||||
|
// columns appear in the unmodded file. In 1.10 final, beta 1.10s and
|
||||||
|
// v1.11+ you can add the missing umon11-umon25 columns.
|
||||||
|
// NOTE: you can allow umon1-25 to also work in Nightmare and Hell by
|
||||||
|
// following this simple ASM edit
|
||||||
|
// (https://d2mods.info/forum/viewtopic.php?f=8&t=53969&p=425179&hilit=umon#p425179)
|
||||||
|
MonsterUniqueID1 string // umon1
|
||||||
|
MonsterUniqueID2 string // umon2
|
||||||
|
MonsterUniqueID3 string // umon3
|
||||||
|
MonsterUniqueID4 string // umon4
|
||||||
|
MonsterUniqueID5 string // umon5
|
||||||
|
MonsterUniqueID6 string // umon6
|
||||||
|
MonsterUniqueID7 string // umon7
|
||||||
|
MonsterUniqueID8 string // umon8
|
||||||
|
MonsterUniqueID9 string // umon9
|
||||||
|
MonsterUniqueID10 string // umon10
|
||||||
|
|
||||||
|
// Critter Species 1-4. Uses the ID from monstats2.txt and only monsters
|
||||||
|
// with critter column set to 1 can spawn here. critter column is also found
|
||||||
|
// in monstats2.txt. Critters are in reality only present clientside.
|
||||||
|
MonsterCritterID1 string // cmon1
|
||||||
|
MonsterCritterID2 string // cmon2
|
||||||
|
MonsterCritterID3 string // cmon3
|
||||||
|
MonsterCritterID4 string // cmon4
|
||||||
|
|
||||||
|
// String Code for the Display name of the Level
|
||||||
|
LevelDisplayName string // LevelName
|
||||||
|
|
||||||
|
LevelWarpName string // LevelWarp
|
||||||
|
|
||||||
|
// Which *.DC6 Title Image is loaded when you enter this area. this file
|
||||||
|
// MUST exist, otherwise you will crash with an exception when you enter the
|
||||||
|
// level (for all levels below the expansion row, the files must be
|
||||||
|
// present in the expension folders)
|
||||||
|
TitleImageName string // EntryFile
|
||||||
|
|
||||||
|
// ID
|
||||||
|
// Level ID (used in columns like VIS0-7)
|
||||||
|
ID int
|
||||||
|
|
||||||
|
// Palette is the Act Palette . Reference only
|
||||||
|
Palette int // Pal
|
||||||
|
|
||||||
|
// Act that the Level is located in (internal enumeration ranges from 0 to 4)
|
||||||
|
Act int // Act
|
||||||
|
|
||||||
|
// QuestFlag, QuestExpansionFlag
|
||||||
|
// Used the first one in Classic games and the latter in Expansion games ,
|
||||||
|
// they set a questflag. If this flag is set, a character must have
|
||||||
|
// completed the quest associated with the flag to take a town portal to
|
||||||
|
// the area in question. A character can always use a portal to get back to
|
||||||
|
// town.
|
||||||
|
QuestFlag int // QuestFlag
|
||||||
|
QuestFlagExpansion int // QuestFlagEx
|
||||||
|
|
||||||
|
// Each layer is an unique ID. This number is used to store each automap on
|
||||||
|
// a character. This is used by the game to remember what level the automap
|
||||||
|
// are for.
|
||||||
|
// NOTE: you need to use the extended levels plugin to be able to add
|
||||||
|
// additional layers.
|
||||||
|
AutomapIndex int // Layer
|
||||||
|
|
||||||
|
// SizeXNormal -- SizeYHell If this is a preset area this sets the
|
||||||
|
// X size for the area. Othervise use the same value here that are used in
|
||||||
|
// lvlprest.txt to set the size for the .ds1 file.
|
||||||
|
SizeXNormal int // SizeX
|
||||||
|
SizeYNormal int // SizeY
|
||||||
|
SizeXNightmare int // SizeX(N)
|
||||||
|
SizeYNightmare int // SizeY(N)
|
||||||
|
SizeXHell int // SizeX(H)
|
||||||
|
SizeYHell int // SizeY(H)
|
||||||
|
|
||||||
|
// They set the X\Y position in the world space
|
||||||
|
WorldOffsetX int // OffsetX
|
||||||
|
WorldOffsetY int // OffsetY
|
||||||
|
|
||||||
|
// This set what level id's are the Depended level.
|
||||||
|
// Example: Monastery uses this field to place its entrance always at same
|
||||||
|
// location.
|
||||||
|
DependantLevelID int // Depend
|
||||||
|
|
||||||
|
// The type of the Level (Id from lvltypes.txt)
|
||||||
|
LevelType int // LevelType
|
||||||
|
|
||||||
|
// Controls if teleport is allowed in that level.
|
||||||
|
// 0 = Teleport not allowed
|
||||||
|
// 1 = Teleport allowed
|
||||||
|
// 2 = Teleport allowed, but not able to use teleport throu walls/objects
|
||||||
|
// (maybe for objects this is controlled by IsDoor column in objects.txt)
|
||||||
|
TeleportFlag d2enum.TeleportFlag // Teleport
|
||||||
|
|
||||||
|
// Setting for Level Generation: You have 3 possibilities here:
|
||||||
|
// 1 Random Maze
|
||||||
|
// 2 Preset Area
|
||||||
|
// 3 Wilderness level
|
||||||
|
LevelGenerationType d2enum.LevelGenerationType // DrlgType
|
||||||
|
|
||||||
|
// NOTE
|
||||||
|
// IDs from LvlSub.txt, which is used to randomize outdoor areas, such as
|
||||||
|
// spawning ponds in the blood moor and more stones in the Stoney Field.
|
||||||
|
// This is all changeable, the other subcolumns are explained in this post.
|
||||||
|
|
||||||
|
// Setting Regarding the level sub-type.
|
||||||
|
// Example: 6=wilderness, 9=desert etc, -1=no subtype.
|
||||||
|
SubType int // SubType
|
||||||
|
|
||||||
|
// Tells which subtheme a wilderness area should use.
|
||||||
|
// Themes ranges from -1 (no subtheme) to 4.
|
||||||
|
SubTheme int // SubTheme
|
||||||
|
|
||||||
|
// Setting Regarding Waypoints
|
||||||
|
// NOTE: it does NOT control waypoint placement.
|
||||||
|
SubWaypoint int // SubWaypoint
|
||||||
|
|
||||||
|
// Setting Regarding Shrines.
|
||||||
|
// NOTE: it does NOT control which Shrine will spawn.
|
||||||
|
SubShrine int // SubShrine
|
||||||
|
|
||||||
|
// These fields allow linking level serverside, allowing you to travel
|
||||||
|
// through areas. The Vis must be filled in with the LevelID your level is
|
||||||
|
// linked with, but the actuall number of Vis ( 0 - 7 ) is determined by
|
||||||
|
// your actual map (the .ds1 fle).
|
||||||
|
// Example: Normally Cave levels are only using vis 0-3 and wilderness areas 4-7 .
|
||||||
|
LevelLinkID0 int // Vis0
|
||||||
|
LevelLinkID1 int // Vis1
|
||||||
|
LevelLinkID2 int // Vis2
|
||||||
|
LevelLinkID3 int // Vis3
|
||||||
|
LevelLinkID4 int // Vis4
|
||||||
|
LevelLinkID5 int // Vis5
|
||||||
|
LevelLinkID6 int // Vis6
|
||||||
|
LevelLinkID7 int // Vis7
|
||||||
|
|
||||||
|
// This controls the visual graphics then you move the mouse pointer over
|
||||||
|
// an entrance. To show the graphics you use an ID from lvlwarp.txt and the
|
||||||
|
// behavior on the graphics is controlled by lvlwarp.txt. Your Warps must
|
||||||
|
// match your Vis.
|
||||||
|
// Example: If your level uses Vis 3,5,7 then you must also use Warp 3,5,7 .
|
||||||
|
WarpGraphicsID0 int // Warp0
|
||||||
|
WarpGraphicsID1 int // Warp1
|
||||||
|
WarpGraphicsID2 int // Warp2
|
||||||
|
WarpGraphicsID3 int // Warp3
|
||||||
|
WarpGraphicsID4 int // Warp4
|
||||||
|
WarpGraphicsID5 int // Warp5
|
||||||
|
WarpGraphicsID6 int // Warp6
|
||||||
|
WarpGraphicsID7 int // Warp7
|
||||||
|
|
||||||
|
// These settings handle the light intensity as well as its RGB components
|
||||||
|
LightIntensity int // Intensity
|
||||||
|
Red int // Red
|
||||||
|
Green int // Green
|
||||||
|
Blue int // Blue
|
||||||
|
|
||||||
|
// What quest is this level related to. This is the quest id (as example the
|
||||||
|
// first quest Den of Evil are set to 1, since its the first quest).
|
||||||
|
QuestID int // Quest
|
||||||
|
|
||||||
|
// This sets the minimum distance from a VisX or WarpX location that a
|
||||||
|
// monster, object or tile can be spawned at. (also applies to waypoints and
|
||||||
|
// some preset portals).
|
||||||
|
WarpClearanceDistance int // WarpDist
|
||||||
|
|
||||||
|
// Area Level on Normal-Nightmare-Hell in Classic and Expansion.
|
||||||
|
// It controls the item level of items that drop from chests etc.
|
||||||
|
MonsterLevelNormal int // MonLvl1
|
||||||
|
MonsterLevelNightmare int // MonLvl2
|
||||||
|
MonsterLevelHell int // MonLvl3
|
||||||
|
MonsterLevelNormalEx int // MonLvl1Ex
|
||||||
|
MonsterLevelNightmareEx int // MonLvl2Ex
|
||||||
|
MonsterLevelHellEx int // MonLvl3Ex
|
||||||
|
|
||||||
|
// This is a chance in 100000ths that a monster pack will spawn on a tile.
|
||||||
|
// The maximum chance the game allows is 10% (aka 10000) in v1.10+,
|
||||||
|
MonsterDensityNormal int // MonDen
|
||||||
|
MonsterDensityNightmare int // MonDen(N)
|
||||||
|
MonsterDensityHell int // MonDen(H)
|
||||||
|
|
||||||
|
// Minimum - Maximum Unique and Champion Monsters Spawned in this Level.
|
||||||
|
// Whenever any spawn at all however is bound to MonDen.
|
||||||
|
MonsterUniqueMinNormal int // MonUMin
|
||||||
|
MonsterUniqueMinNightmare int // MonUMin(N)
|
||||||
|
MonsterUniqueMinHell int // MonUMin(H)
|
||||||
|
|
||||||
|
MonsterUniqueMaxNormal int // MonUMax
|
||||||
|
MonsterUniqueMaxNightmare int // MonUMax(N)
|
||||||
|
MonsterUniqueMaxHell int // MonUMax(H)
|
||||||
|
|
||||||
|
// Number of different Monster Types that will be present in this area, the
|
||||||
|
// maximum is 13. You can have up to 13 different monster types at a time in
|
||||||
|
// Nightmare and Hell difficulties, selected randomly from nmon1-nmon25. In
|
||||||
|
// Normal difficulty you can have up to 13 normal monster types selected
|
||||||
|
// randomly from mon1-mon25, and the same number of champion and unique
|
||||||
|
// types selected randomly from umon1-umon25.
|
||||||
|
NumMonsterTypes int // NumMon
|
||||||
|
|
||||||
|
// Controls the chance for a critter to spawn.
|
||||||
|
MonsterCritter1SpawnChance int // cpct1
|
||||||
|
MonsterCritter2SpawnChance int // cpct2
|
||||||
|
MonsterCritter3SpawnChance int // cpct3
|
||||||
|
MonsterCritter4SpawnChance int // cpct4
|
||||||
|
|
||||||
|
// Referes to a entry in SoundEnviron.txt (for the Levels Music)
|
||||||
|
SoundEnvironmentID int // SoundEnv
|
||||||
|
|
||||||
|
// 255 means no Waipoint for this level, while others state the Waypoint' ID
|
||||||
|
// for the level
|
||||||
|
// NOTE: you can switch waypoint destinations between areas this way, not
|
||||||
|
// between acts however so don't even bother to try.
|
||||||
|
WaypointID int // Waypoint
|
||||||
|
|
||||||
|
// this field uses the ID of the ObjectGroup you want to Spawn in this Area,
|
||||||
|
// taken from Objgroup.txt.
|
||||||
|
ObjectGroupID0 int // ObjGrp0
|
||||||
|
ObjectGroupID1 int // ObjGrp1
|
||||||
|
ObjectGroupID2 int // ObjGrp2
|
||||||
|
ObjectGroupID3 int // ObjGrp3
|
||||||
|
ObjectGroupID4 int // ObjGrp4
|
||||||
|
ObjectGroupID5 int // ObjGrp5
|
||||||
|
ObjectGroupID6 int // ObjGrp6
|
||||||
|
ObjectGroupID7 int // ObjGrp7
|
||||||
|
|
||||||
|
// These fields indicates the chance for each object group to spawn (if you
|
||||||
|
// use ObjGrp0 then set ObjPrb0 to a value below 100)
|
||||||
|
ObjectGroupSpawnChance0 int // ObjPrb0
|
||||||
|
ObjectGroupSpawnChance1 int // ObjPrb1
|
||||||
|
ObjectGroupSpawnChance2 int // ObjPrb2
|
||||||
|
ObjectGroupSpawnChance3 int // ObjPrb3
|
||||||
|
ObjectGroupSpawnChance4 int // ObjPrb4
|
||||||
|
ObjectGroupSpawnChance5 int // ObjPrb5
|
||||||
|
ObjectGroupSpawnChance6 int // ObjPrb6
|
||||||
|
ObjectGroupSpawnChance7 int // ObjPrb7
|
||||||
|
|
||||||
|
// It sets whether rain or snow (in act 5 only) can fall . Set it to 1 in
|
||||||
|
// order to enable it, 0 to disable it.
|
||||||
|
EnableRain bool // Rain
|
||||||
|
|
||||||
|
// Unused setting (In pre beta D2 Blizzard planned Rain to generate Mud
|
||||||
|
// which would have slowed your character's speed down, but this never made
|
||||||
|
// it into the final game). the field is read by the code but the return
|
||||||
|
// value is never utilized.
|
||||||
|
EnableMud bool // Mud
|
||||||
|
|
||||||
|
// Setting for 3D Enhanced D2 that disables Perspective Mode for a specific
|
||||||
|
// level. A value of 1 enables the users to choose between normal and
|
||||||
|
// Perspective view, while 0 disables that choice.
|
||||||
|
EnablePerspective bool // NoPer
|
||||||
|
|
||||||
|
// Allows you to look through objects and walls even if they are not in a
|
||||||
|
// wilderness level. 1 enables it, 0 disables it.
|
||||||
|
EnableLineOfSightDraw bool // LOSDraw
|
||||||
|
|
||||||
|
// Unknown. Probably has to do with Tiles and their Placement.
|
||||||
|
// 1 enables it, 0 disables it.
|
||||||
|
EnableFloorFliter bool // FloorFilter
|
||||||
|
|
||||||
|
// Unknown. Probably has to do with tiles and their placement.
|
||||||
|
// 1 enables it, 0 disables it.
|
||||||
|
EnableBlankScreen bool // BlankScreen
|
||||||
|
|
||||||
|
// for levels bordered with mountains or walls, like the act 1 wildernesses.
|
||||||
|
// 1 enables it, 0 disables it.
|
||||||
|
EnableDrawEdges bool // DrawEdges
|
||||||
|
|
||||||
|
// Setting it to 1 makes the level to be treated as an indoor area, while
|
||||||
|
// 0 makes this level an outdoor. Indoor areas are not affected by day-night
|
||||||
|
// cycles, because they always use the light values specified in Intensity,
|
||||||
|
// Red, Green, Blue. this field also controls whenever sounds will echo if
|
||||||
|
// you're running the game with a sound card capable of it and have
|
||||||
|
// environment sound effects set to true.
|
||||||
|
IsInside bool // IsInside
|
||||||
|
|
||||||
|
// This field is required for some levels, entering those levels when portal
|
||||||
|
// field isn't set will often crash the game. This also applies to
|
||||||
|
// duplicates of those levels created with both of the extended level
|
||||||
|
// plugins.
|
||||||
|
PortalEnable bool // Portal
|
||||||
|
|
||||||
|
// This controls if you can re-position a portal in a level or not. If it's
|
||||||
|
// set to 1 you will be able to reposition the portal by using either map
|
||||||
|
// entry#76 Tp Location #79. If both tiles are in the level it will use Tp
|
||||||
|
// Location #79. If set to 0 the map won't allow repositioning.
|
||||||
|
PortalRepositionEnable bool // Position
|
||||||
|
|
||||||
|
// Setting this field to 1 will make the monsters status saved in the map.
|
||||||
|
// Setting it to 0 will allow some useful things like NPC refreshing their
|
||||||
|
// stores.
|
||||||
|
// WARNING: Do not set this to 1 for non-town areas, or the monsters you'll
|
||||||
|
// flee from will simply vanish and never reappear. They won't even be
|
||||||
|
// replaced by new ones
|
||||||
|
// Gravestench - this funcionality should not be in one field
|
||||||
|
SaveMonsterStates bool // SaveMonsters
|
||||||
|
SaveMerchantStates bool // SaveMonsters
|
||||||
|
|
||||||
|
// No info on the PK page, but I'm guessing it's for monster wandering
|
||||||
|
MonsterWanderEnable bool // MonWndr
|
||||||
|
|
||||||
|
// This setting is hardcoded to certain level Ids, like the River Of Flame,
|
||||||
|
// enabling it in other places can glitch up the game, so leave it alone.
|
||||||
|
// It is not known what exactly it does however.
|
||||||
|
MonsterSpecialWalk bool // MonSpcWalk
|
||||||
|
|
||||||
|
// Give preference to monsters set to ranged=1 in MonStats.txt on Nightmare
|
||||||
|
// and Hell difficulties when picking something to spawn.
|
||||||
|
MonsterPreferRanged bool // rangedspawn
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func levelMazeDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(LevelMazeDetails)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &LevelMazeDetailsRecord{
|
||||||
|
Name: d.String("Name"),
|
||||||
|
LevelID: d.Number("Level"),
|
||||||
|
NumRoomsNormal: d.Number("Rooms"),
|
||||||
|
NumRoomsNightmare: d.Number("Rooms(N)"),
|
||||||
|
NumRoomsHell: d.Number("Rooms(H)"),
|
||||||
|
SizeX: d.Number("SizeX"),
|
||||||
|
SizeY: d.Number("SizeY"),
|
||||||
|
}
|
||||||
|
records[record.LevelID] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d LevelMazeDetails records", len(records))
|
||||||
|
|
||||||
|
r.Level.Maze = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// LevelMazeDetails stores all of the LevelMazeDetailsRecords
|
||||||
|
type LevelMazeDetails map[int]*LevelMazeDetailsRecord
|
||||||
|
|
||||||
|
// LevelMazeDetailsRecord is a representation of a row from lvlmaze.txt
|
||||||
|
// these records define the parameters passed to the maze level generator
|
||||||
|
type LevelMazeDetailsRecord struct {
|
||||||
|
// descriptive, not loaded in game. Corresponds with Name field in
|
||||||
|
// Levels.txt
|
||||||
|
Name string // Name
|
||||||
|
|
||||||
|
// ID from Levels.txt
|
||||||
|
// NOTE: Cave 1 is the Den of Evil, its associated treasure level is quest
|
||||||
|
// only.
|
||||||
|
LevelID int // Level
|
||||||
|
|
||||||
|
// the minimum number of .ds1 map sections that will make up the maze in
|
||||||
|
// Normal, Nightmare and Hell difficulties.
|
||||||
|
NumRoomsNormal int // Rooms
|
||||||
|
NumRoomsNightmare int // Rooms(N)
|
||||||
|
NumRoomsHell int // Rooms(H)
|
||||||
|
|
||||||
|
// the size in the X\Y direction of any component ds1 map section.
|
||||||
|
SizeX int // SizeX
|
||||||
|
SizeY int // SizeY
|
||||||
|
|
||||||
|
// Possibly related to how adjacent .ds1s are connected with each other,
|
||||||
|
// but what the different values are for is unknown.
|
||||||
|
// Merge int // Merge
|
||||||
|
|
||||||
|
// Included in the original Diablo II beta tests and in the demo version.
|
||||||
|
// Beta
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadLevelPresets loads level presets from text file
|
||||||
|
func levelPresetLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(LevelPresets)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := LevelPresetRecord{
|
||||||
|
Name: d.String("Name"),
|
||||||
|
DefinitionID: d.Number("Def"),
|
||||||
|
LevelID: d.Number("LevelId"),
|
||||||
|
Populate: d.Number("Populate") == 1,
|
||||||
|
Logicals: d.Number("Logicals") == 1,
|
||||||
|
Outdoors: d.Number("Outdoors") == 1,
|
||||||
|
Animate: d.Number("Animate") == 1,
|
||||||
|
KillEdge: d.Number("KillEdge") == 1,
|
||||||
|
FillBlanks: d.Number("FillBlanks") == 1,
|
||||||
|
SizeX: d.Number("SizeX"),
|
||||||
|
SizeY: d.Number("SizeY"),
|
||||||
|
AutoMap: d.Number("AutoMap") == 1,
|
||||||
|
Scan: d.Number("Scan") == 1,
|
||||||
|
Pops: d.Number("Pops"),
|
||||||
|
PopPad: d.Number("PopPad"),
|
||||||
|
FileCount: d.Number("Files"),
|
||||||
|
Files: [6]string{
|
||||||
|
d.String("File1"),
|
||||||
|
d.String("File2"),
|
||||||
|
d.String("File3"),
|
||||||
|
d.String("File4"),
|
||||||
|
d.String("File5"),
|
||||||
|
d.String("File6"),
|
||||||
|
},
|
||||||
|
Dt1Mask: uint(d.Number("Dt1Mask")),
|
||||||
|
Beta: d.Number("Beta") == 1,
|
||||||
|
Expansion: d.Number("Expansion") == 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
records[record.DefinitionID] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d level presets", len(records))
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Level.Presets = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// LevelPresets stores all of the LevelPresetRecords
|
||||||
|
type LevelPresets map[int]LevelPresetRecord
|
||||||
|
|
||||||
|
// LevelPresetRecord is a representation of a row from lvlprest.txt
|
||||||
|
// these records define parameters for the preset level map generator
|
||||||
|
type LevelPresetRecord struct {
|
||||||
|
Files [6]string
|
||||||
|
Name string
|
||||||
|
DefinitionID int
|
||||||
|
LevelID int
|
||||||
|
SizeX int
|
||||||
|
SizeY int
|
||||||
|
Pops int
|
||||||
|
PopPad int
|
||||||
|
FileCount int
|
||||||
|
Dt1Mask uint
|
||||||
|
Populate bool
|
||||||
|
Logicals bool
|
||||||
|
Outdoors bool
|
||||||
|
Animate bool
|
||||||
|
KillEdge bool
|
||||||
|
FillBlanks bool
|
||||||
|
AutoMap bool
|
||||||
|
Scan bool
|
||||||
|
Beta bool
|
||||||
|
Expansion bool
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func levelSubstitutionsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(LevelSubstitutions)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &LevelSubstitutionRecord{
|
||||||
|
Name: d.String("Name"),
|
||||||
|
ID: d.Number("Type"),
|
||||||
|
File: d.String("File"),
|
||||||
|
IsExpansion: d.Number("Expansion") > 0,
|
||||||
|
BorderType: d.Number("BordType"),
|
||||||
|
GridSize: d.Number("GridSize"),
|
||||||
|
Mask: d.Number("Dt1Mask"),
|
||||||
|
ChanceSpawn0: d.Number("Prob0"),
|
||||||
|
ChanceSpawn1: d.Number("Prob1"),
|
||||||
|
ChanceSpawn2: d.Number("Prob2"),
|
||||||
|
ChanceSpawn3: d.Number("Prob3"),
|
||||||
|
ChanceSpawn4: d.Number("Prob4"),
|
||||||
|
ChanceFloor0: d.Number("Trials0"),
|
||||||
|
ChanceFloor1: d.Number("Trials1"),
|
||||||
|
ChanceFloor2: d.Number("Trials2"),
|
||||||
|
ChanceFloor3: d.Number("Trials3"),
|
||||||
|
ChanceFloor4: d.Number("Trials4"),
|
||||||
|
GridMax0: d.Number("Max0"),
|
||||||
|
GridMax1: d.Number("Max1"),
|
||||||
|
GridMax2: d.Number("Max2"),
|
||||||
|
GridMax3: d.Number("Max3"),
|
||||||
|
GridMax4: d.Number("Max4"),
|
||||||
|
}
|
||||||
|
|
||||||
|
records[record.ID] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d LevelSubstitution records", len(records))
|
||||||
|
|
||||||
|
r.Level.Sub = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// LevelSubstitutions stores all of the LevelSubstitutionRecords
|
||||||
|
type LevelSubstitutions map[int]*LevelSubstitutionRecord
|
||||||
|
|
||||||
|
// LevelSubstitutionRecord is a representation of a row from lvlsub.txt
|
||||||
|
// these records are parameters for levels and describe substitution rules
|
||||||
|
type LevelSubstitutionRecord struct {
|
||||||
|
// Description, reference only.
|
||||||
|
Name string // Name
|
||||||
|
|
||||||
|
// This value is used in Levels.txt, in the column 'SubType'. You'll notice
|
||||||
|
// that in LvlSub.txt some rows use the same value, we can say they forms
|
||||||
|
// groups. If you count each row of a group starting from 0, then you'll
|
||||||
|
// obtain what is written in Levels.txt, columns 'SubTheme', 'SubWaypoint'
|
||||||
|
// and 'SubShrine'. (added by Paul Siramy)
|
||||||
|
ID int // Type
|
||||||
|
|
||||||
|
// What .ds1 is being used.
|
||||||
|
File string // File
|
||||||
|
|
||||||
|
// 0 for classic, 1 for Expansion.
|
||||||
|
IsExpansion bool // Expansion
|
||||||
|
|
||||||
|
// Unknown as all have 0.
|
||||||
|
// CheckAll
|
||||||
|
|
||||||
|
// this field can contain values ranging from -1 to 2
|
||||||
|
// NOTE: wall types have 0, 1 or 2, while Non-wall types have -1.
|
||||||
|
BorderType int // BordType
|
||||||
|
|
||||||
|
// Set it to 1 or 2 I'm assuming this means a block of tiles ie: 4x4.
|
||||||
|
GridSize int // GridSize
|
||||||
|
|
||||||
|
// For some rows, this is their place in LvlTypes.txt. The Dt1 mask also
|
||||||
|
// includes the mask for the Floor.Dt1 of that level. (see Trials0 below)
|
||||||
|
Mask int // Dt1Mask
|
||||||
|
|
||||||
|
// The probability of the Dt1 being spawned.
|
||||||
|
ChanceSpawn0 int // Prob0
|
||||||
|
ChanceSpawn1 int // Prob1
|
||||||
|
ChanceSpawn2 int // Prob2
|
||||||
|
ChanceSpawn3 int // Prob3
|
||||||
|
ChanceSpawn4 int // Prob4
|
||||||
|
|
||||||
|
// This appears to be a chance of either a floor tile being spawned or the
|
||||||
|
// actual Dt1..
|
||||||
|
ChanceFloor0 int // Trials0
|
||||||
|
ChanceFloor1 int // Trials1
|
||||||
|
ChanceFloor2 int // Trials2
|
||||||
|
ChanceFloor3 int // Trials3
|
||||||
|
ChanceFloor4 int // Trials4
|
||||||
|
|
||||||
|
// This appears to be how much will spawn in the Grid.
|
||||||
|
GridMax0 int // Max0
|
||||||
|
GridMax1 int // Max1
|
||||||
|
GridMax2 int // Max2
|
||||||
|
GridMax3 int // Max3
|
||||||
|
GridMax4 int // Max4
|
||||||
|
|
||||||
|
// Beta
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadLevelTypes loads the LevelTypeRecords
|
||||||
|
func levelTypesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(LevelTypes, 0)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &LevelTypeRecord{
|
||||||
|
[32]string{
|
||||||
|
d.String("File 1"),
|
||||||
|
d.String("File 2"),
|
||||||
|
d.String("File 3"),
|
||||||
|
d.String("File 4"),
|
||||||
|
d.String("File 5"),
|
||||||
|
d.String("File 6"),
|
||||||
|
d.String("File 7"),
|
||||||
|
d.String("File 8"),
|
||||||
|
d.String("File 9"),
|
||||||
|
d.String("File 10"),
|
||||||
|
d.String("File 11"),
|
||||||
|
d.String("File 12"),
|
||||||
|
d.String("File 13"),
|
||||||
|
d.String("File 14"),
|
||||||
|
d.String("File 15"),
|
||||||
|
d.String("File 16"),
|
||||||
|
d.String("File 17"),
|
||||||
|
d.String("File 18"),
|
||||||
|
d.String("File 19"),
|
||||||
|
d.String("File 20"),
|
||||||
|
d.String("File 21"),
|
||||||
|
d.String("File 22"),
|
||||||
|
d.String("File 23"),
|
||||||
|
d.String("File 24"),
|
||||||
|
d.String("File 25"),
|
||||||
|
d.String("File 26"),
|
||||||
|
d.String("File 27"),
|
||||||
|
d.String("File 28"),
|
||||||
|
d.String("File 29"),
|
||||||
|
d.String("File 30"),
|
||||||
|
d.String("File 31"),
|
||||||
|
d.String("File 32"),
|
||||||
|
},
|
||||||
|
d.String("Name"),
|
||||||
|
d.Number("Id"),
|
||||||
|
d.Number("Act"),
|
||||||
|
d.Number("Beta") > 0,
|
||||||
|
d.Number("Expansion") > 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
records = append(records, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d LevelType records", len(records))
|
||||||
|
|
||||||
|
r.Level.Types = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// LevelTypes stores all of the LevelTypeRecords
|
||||||
|
type LevelTypes []*LevelTypeRecord
|
||||||
|
|
||||||
|
// LevelTypeRecord is a representation of a row from lvltype.txt
|
||||||
|
// the fields describe what ds1 files a level uses
|
||||||
|
type LevelTypeRecord struct {
|
||||||
|
Files [32]string
|
||||||
|
Name string
|
||||||
|
ID int
|
||||||
|
Act int
|
||||||
|
Beta bool
|
||||||
|
Expansion bool
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func levelWarpsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(LevelWarps)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &LevelWarpRecord{
|
||||||
|
Name: d.String("Name"),
|
||||||
|
ID: d.Number("Id"),
|
||||||
|
SelectX: d.Number("SelectX"),
|
||||||
|
SelectY: d.Number("SelectY"),
|
||||||
|
SelectDX: d.Number("SelectDX"),
|
||||||
|
SelectDY: d.Number("SelectDY"),
|
||||||
|
ExitWalkX: d.Number("ExitWalkX"),
|
||||||
|
ExitWalkY: d.Number("ExitWalkY"),
|
||||||
|
OffsetX: d.Number("OffsetX"),
|
||||||
|
OffsetY: d.Number("OffsetY"),
|
||||||
|
LitVersion: d.Bool("LitVersion"),
|
||||||
|
Tiles: d.Number("Tiles"),
|
||||||
|
Direction: d.String("Direction"),
|
||||||
|
}
|
||||||
|
records[record.ID] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d level warps", len(records))
|
||||||
|
|
||||||
|
r.Level.Warp = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// LevelWarps loaded from txt records
|
||||||
|
type LevelWarps map[int]*LevelWarpRecord
|
||||||
|
|
||||||
|
// LevelWarpRecord is a representation of a row from lvlwarp.txt
|
||||||
|
// it describes the warp graphics offsets and dimensions for levels
|
||||||
|
type LevelWarpRecord struct {
|
||||||
|
Name string
|
||||||
|
ID int
|
||||||
|
SelectX int
|
||||||
|
SelectY int
|
||||||
|
SelectDX int
|
||||||
|
SelectDY int
|
||||||
|
ExitWalkX int
|
||||||
|
ExitWalkY int
|
||||||
|
OffsetX int
|
||||||
|
OffsetY int
|
||||||
|
LitVersion bool
|
||||||
|
Tiles int
|
||||||
|
Direction string
|
||||||
|
}
|
|
@ -0,0 +1,309 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2calculation"
|
||||||
|
)
|
||||||
|
|
||||||
|
func missilesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(Missiles)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &MissileRecord{
|
||||||
|
Name: d.String("Missile"),
|
||||||
|
Id: d.Number("Id"),
|
||||||
|
|
||||||
|
ClientMovementFunc: d.Number("pCltDoFunc"),
|
||||||
|
ClientCollisionFunc: d.Number("pCltHitFunc"),
|
||||||
|
ServerMovementFunc: d.Number("pSrvDoFunc"),
|
||||||
|
ServerCollisionFunc: d.Number("pSrvHitFunc"),
|
||||||
|
ServerDamageFunc: d.Number("pSrvDmgFunc"),
|
||||||
|
|
||||||
|
ServerMovementCalc: MissileCalc{
|
||||||
|
Calc: "SrvCalc1",
|
||||||
|
Desc: "*srv calc 1 desc",
|
||||||
|
Params: []MissileCalcParam{
|
||||||
|
{
|
||||||
|
d.Number("Param1"),
|
||||||
|
d.String("*param1 desc"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d.Number("Param2"),
|
||||||
|
d.String("*param2 desc"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d.Number("Param3"),
|
||||||
|
d.String("*param3 desc"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d.Number("Param4"),
|
||||||
|
d.String("*param4 desc"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d.Number("Param5"),
|
||||||
|
d.String("*param5 desc"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ClientMovementCalc: MissileCalc{
|
||||||
|
Calc: "CltCalc1",
|
||||||
|
Desc: "*client calc 1 desc",
|
||||||
|
Params: []MissileCalcParam{
|
||||||
|
{
|
||||||
|
d.Number("CltParam1"),
|
||||||
|
d.String("*client param1 desc"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d.Number("CltParam2"),
|
||||||
|
d.String("*client param2 desc"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d.Number("CltParam3"),
|
||||||
|
d.String("*client param3 desc"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d.Number("CltParam4"),
|
||||||
|
d.String("*client param4 desc"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d.Number("CltParam5"),
|
||||||
|
d.String("*client param5 desc"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ServerCollisionCalc: MissileCalc{
|
||||||
|
Calc: "SHitCalc1",
|
||||||
|
Desc: "*server hit calc 1 desc",
|
||||||
|
Params: []MissileCalcParam{
|
||||||
|
{
|
||||||
|
d.Number("sHitPar1"),
|
||||||
|
d.String("*server hit param1 desc"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d.Number("sHitPar2"),
|
||||||
|
d.String("*server hit param2 desc"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d.Number("sHitPar3"),
|
||||||
|
d.String("*server hit param3 desc"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ClientCollisionCalc: MissileCalc{
|
||||||
|
Calc: "CHitCalc1",
|
||||||
|
Desc: "*client hit calc 1 desc",
|
||||||
|
Params: []MissileCalcParam{
|
||||||
|
{
|
||||||
|
d.Number("cHitPar1"),
|
||||||
|
d.String("*client hit param1 desc"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d.Number("cHitPar2"),
|
||||||
|
d.String("*client hit param2 desc"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d.Number("cHitPar3"),
|
||||||
|
d.String("*client hit param3 desc"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ServerDamageCalc: MissileCalc{
|
||||||
|
Calc: "DmgCalc1",
|
||||||
|
Desc: "*damage calc 1",
|
||||||
|
Params: []MissileCalcParam{
|
||||||
|
{
|
||||||
|
d.Number("dParam1"),
|
||||||
|
d.String("*damage param1 desc"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d.Number("dParam2"),
|
||||||
|
d.String("*damage param2 desc"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Velocity: d.Number("Vel"),
|
||||||
|
MaxVelocity: d.Number("MaxVel"),
|
||||||
|
LevelVelocityBonus: d.Number("VelLev"),
|
||||||
|
Accel: d.Number("Accel"),
|
||||||
|
Range: d.Number("Range"),
|
||||||
|
LevelRangeBonus: d.Number("LevRange"),
|
||||||
|
|
||||||
|
Light: MissileLight{
|
||||||
|
Diameter: d.Number("Light"),
|
||||||
|
Flicker: d.Number("Flicker"),
|
||||||
|
Red: uint8(d.Number("Red")),
|
||||||
|
Green: uint8(d.Number("Green")),
|
||||||
|
Blue: uint8(d.Number("Blue")),
|
||||||
|
},
|
||||||
|
|
||||||
|
Animation: MissileAnimation{
|
||||||
|
StepsBeforeVisible: d.Number("InitSteps"),
|
||||||
|
StepsBeforeActive: d.Number("Activate"),
|
||||||
|
LoopAnimation: d.Number("LoopAnim") > 0,
|
||||||
|
CelFileName: d.String("CelFile"),
|
||||||
|
AnimationRate: d.Number("animrate"),
|
||||||
|
AnimationLength: d.Number("AnimLen"),
|
||||||
|
AnimationSpeed: d.Number("AnimSpeed"),
|
||||||
|
StartingFrame: d.Number("RandStart"),
|
||||||
|
HasSubLoop: d.Number("SubLoop") > 0,
|
||||||
|
SubStartingFrame: d.Number("SubStart"),
|
||||||
|
SubEndingFrame: d.Number("SubStop"),
|
||||||
|
},
|
||||||
|
|
||||||
|
Collision: MissileCollision{
|
||||||
|
CollisionType: d.Number("CollideType"),
|
||||||
|
DestroyedUponCollision: d.Number("CollideKill") > 0,
|
||||||
|
FriendlyFire: d.Number("CollideFriend") > 0,
|
||||||
|
LastCollide: d.Number("LastCollide") > 0,
|
||||||
|
Collision: d.Number("Collision") > 0,
|
||||||
|
ClientCollision: d.Number("ClientCol") > 0,
|
||||||
|
ClientSend: d.Number("ClientSend") > 0,
|
||||||
|
UseCollisionTimer: d.Number("NextHit") > 0,
|
||||||
|
TimerFrames: d.Number("NextDelay"),
|
||||||
|
},
|
||||||
|
|
||||||
|
XOffset: d.Number("xoffset"),
|
||||||
|
YOffset: d.Number("yoffset"),
|
||||||
|
ZOffset: d.Number("zoffset"),
|
||||||
|
Size: d.Number("Size"),
|
||||||
|
|
||||||
|
DestroyedByTP: d.Number("SrcTown") > 0,
|
||||||
|
DestroyedByTPFrame: d.Number("CltSrcTown"),
|
||||||
|
CanDestroy: d.Number("CanDestroy") > 0,
|
||||||
|
|
||||||
|
UseAttackRating: d.Number("ToHit") > 0,
|
||||||
|
AlwaysExplode: d.Number("AlwaysExplode") > 0,
|
||||||
|
|
||||||
|
ClientExplosion: d.Number("Explosion") > 0,
|
||||||
|
TownSafe: d.Number("Town") > 0,
|
||||||
|
IgnoreBossModifiers: d.Number("NoUniqueMod") > 0,
|
||||||
|
IgnoreMultishot: d.Number("NoMultiShot") > 0,
|
||||||
|
HolyFilterType: d.Number("Holy"),
|
||||||
|
CanBeSlowed: d.Number("CanSlow") > 0,
|
||||||
|
TriggersHitEvents: d.Number("ReturnFire") > 0,
|
||||||
|
TriggersGetHit: d.Number("GetHit") > 0,
|
||||||
|
SoftHit: d.Number("SoftHit") > 0,
|
||||||
|
KnockbackPercent: d.Number("KnockBack"),
|
||||||
|
|
||||||
|
TransparencyMode: d.Number("Trans"),
|
||||||
|
|
||||||
|
UseQuantity: d.Number("Qty") > 0,
|
||||||
|
AffectedByPierce: d.Number("Pierce") > 0,
|
||||||
|
SpecialSetup: d.Number("SpecialSetup") > 0,
|
||||||
|
|
||||||
|
MissileSkill: d.Number("MissileSkill") > 0,
|
||||||
|
SkillName: d.String("Skill"),
|
||||||
|
|
||||||
|
ResultFlags: d.Number("ResultFlags"),
|
||||||
|
HitFlags: d.Number("HitFlags"),
|
||||||
|
|
||||||
|
HitShift: d.Number("HitShift"),
|
||||||
|
ApplyMastery: d.Number("ApplyMastery") > 0,
|
||||||
|
SourceDamage: d.Number("SrcDamage"),
|
||||||
|
HalfDamageForTwoHander: d.Number("Half2HSrc") > 0,
|
||||||
|
SourceMissDamage: d.Number("SrcMissDmg"),
|
||||||
|
|
||||||
|
Damage: MissileDamage{
|
||||||
|
MinDamage: d.Number("MinDamage"),
|
||||||
|
MinLevelDamage: [5]int{
|
||||||
|
d.Number("MinLevDam1"),
|
||||||
|
d.Number("MinLevDam2"),
|
||||||
|
d.Number("MinLevDam3"),
|
||||||
|
d.Number("MinLevDam4"),
|
||||||
|
d.Number("MinLevDam5"),
|
||||||
|
},
|
||||||
|
MaxDamage: d.Number("MaxDamage"),
|
||||||
|
MaxLevelDamage: [5]int{
|
||||||
|
d.Number("MaxLevDam1"),
|
||||||
|
d.Number("MaxLevDam2"),
|
||||||
|
d.Number("MaxLevDam3"),
|
||||||
|
d.Number("MaxLevDam4"),
|
||||||
|
d.Number("MaxLevDam5"),
|
||||||
|
},
|
||||||
|
DamageSynergyPerCalc: d2calculation.CalcString(d.String("DmgSymPerCalc")),
|
||||||
|
},
|
||||||
|
ElementalDamage: MissileElementalDamage{
|
||||||
|
ElementType: d.String("EType"),
|
||||||
|
Damage: MissileDamage{
|
||||||
|
MinDamage: d.Number("MinEDamage"),
|
||||||
|
MinLevelDamage: [5]int{
|
||||||
|
d.Number("MinELevDam1"),
|
||||||
|
d.Number("MinELevDam2"),
|
||||||
|
d.Number("MinELevDam3"),
|
||||||
|
d.Number("MinELevDam4"),
|
||||||
|
d.Number("MinELevDam5"),
|
||||||
|
},
|
||||||
|
MaxDamage: d.Number("MaxEDamage"),
|
||||||
|
MaxLevelDamage: [5]int{
|
||||||
|
d.Number("MaxELevDam1"),
|
||||||
|
d.Number("MaxELevDam2"),
|
||||||
|
d.Number("MaxELevDam3"),
|
||||||
|
d.Number("MaxELevDam4"),
|
||||||
|
d.Number("MaxELevDam5"),
|
||||||
|
},
|
||||||
|
DamageSynergyPerCalc: d2calculation.CalcString(d.String("EDmgSymPerCalc")),
|
||||||
|
},
|
||||||
|
Duration: d.Number("ELen"),
|
||||||
|
LevelDuration: [3]int{
|
||||||
|
d.Number("ELevLen1"),
|
||||||
|
d.Number("ELevLen2"),
|
||||||
|
d.Number("ELevLen3"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
HitClass: d.Number("HitClass"),
|
||||||
|
NumDirections: d.Number("NumDirections"),
|
||||||
|
LocalBlood: d.Number("LocalBlood"),
|
||||||
|
DamageReductionRate: d.Number("DamageRate"),
|
||||||
|
|
||||||
|
TravelSound: d.String("TravelSound"),
|
||||||
|
HitSound: d.String("HitSound"),
|
||||||
|
ProgSound: d.String("ProgSound"),
|
||||||
|
ProgOverlay: d.String("ProgOverlay"),
|
||||||
|
ExplosionMissile: d.String("ExplosionMissile"),
|
||||||
|
|
||||||
|
SubMissile: [3]string{
|
||||||
|
d.String("SubMissile1"),
|
||||||
|
d.String("SubMissile2"),
|
||||||
|
d.String("SubMissile3"),
|
||||||
|
},
|
||||||
|
HitSubMissile: [4]string{
|
||||||
|
d.String("HitSubMissile1"),
|
||||||
|
d.String("HitSubMissile2"),
|
||||||
|
d.String("HitSubMissile3"),
|
||||||
|
d.String("HitSubMissile4"),
|
||||||
|
},
|
||||||
|
ClientSubMissile: [3]string{
|
||||||
|
d.String("CltSubMissile1"),
|
||||||
|
d.String("CltSubMissile2"),
|
||||||
|
d.String("CltSubMissile3"),
|
||||||
|
},
|
||||||
|
ClientHitSubMissile: [4]string{
|
||||||
|
d.String("CltHitSubMissile1"),
|
||||||
|
d.String("CltHitSubMissile2"),
|
||||||
|
d.String("CltHitSubMissile3"),
|
||||||
|
d.String("CltHitSubMissile4"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
records[record.Id] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d Missile Records", len(records))
|
||||||
|
|
||||||
|
r.Missiles = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,199 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2calculation"
|
||||||
|
|
||||||
|
// Missiles stores all of the MissileRecords
|
||||||
|
type Missiles map[int]*MissileRecord
|
||||||
|
|
||||||
|
// MissileCalcParam is a calculation parameter for a missile
|
||||||
|
type MissileCalcParam struct {
|
||||||
|
Param int
|
||||||
|
Desc string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MissileCalc is a calculation for a missile
|
||||||
|
type MissileCalc struct {
|
||||||
|
Calc d2calculation.CalcString
|
||||||
|
Desc string
|
||||||
|
Params []MissileCalcParam
|
||||||
|
}
|
||||||
|
|
||||||
|
// MissileLight has the parameters for missile lighting
|
||||||
|
type MissileLight struct {
|
||||||
|
Diameter int
|
||||||
|
Flicker int
|
||||||
|
Red uint8
|
||||||
|
Green uint8
|
||||||
|
Blue uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// MissileAnimation stores parameters for a missile animation
|
||||||
|
type MissileAnimation struct {
|
||||||
|
CelFileName string
|
||||||
|
StepsBeforeVisible int
|
||||||
|
StepsBeforeActive int
|
||||||
|
AnimationRate int // seems to do nothing
|
||||||
|
AnimationLength int
|
||||||
|
AnimationSpeed int
|
||||||
|
StartingFrame int // called "RandFrame"
|
||||||
|
SubStartingFrame int
|
||||||
|
SubEndingFrame int
|
||||||
|
LoopAnimation bool
|
||||||
|
HasSubLoop bool // runs after first animation ends
|
||||||
|
}
|
||||||
|
|
||||||
|
// MissileCollision parameters for missile collision
|
||||||
|
type MissileCollision struct {
|
||||||
|
CollisionType int // controls the kind of collision
|
||||||
|
// 0 = none, 1 = units only, 3 = normal (units, walls),
|
||||||
|
// 6 = walls only, 8 = walls, units, and floors
|
||||||
|
TimerFrames int // how many frames to persist
|
||||||
|
DestroyedUponCollision bool
|
||||||
|
FriendlyFire bool
|
||||||
|
LastCollide bool // unknown
|
||||||
|
Collision bool // unknown
|
||||||
|
ClientCollision bool // unknown
|
||||||
|
ClientSend bool // unclear
|
||||||
|
UseCollisionTimer bool // after hit, use timer before dying
|
||||||
|
}
|
||||||
|
|
||||||
|
// MissileDamage parameters for calculating missile physical damage
|
||||||
|
type MissileDamage struct {
|
||||||
|
MinDamage int
|
||||||
|
MaxDamage int
|
||||||
|
MinLevelDamage [5]int // additional damage per missile level
|
||||||
|
// [0]: lvs 2-8, [1]: lvs 9-16, [2]: lvs 17-22, [3]: lvs 23-28, [4]: lv 29+
|
||||||
|
MaxLevelDamage [5]int // see above
|
||||||
|
DamageSynergyPerCalc d2calculation.CalcString // works like synergy in skills.txt, not clear
|
||||||
|
}
|
||||||
|
|
||||||
|
// MissileElementalDamage parameters for calculating missile elemental damage
|
||||||
|
type MissileElementalDamage struct {
|
||||||
|
Damage MissileDamage
|
||||||
|
ElementType string
|
||||||
|
Duration int // frames, 25 = 1 second
|
||||||
|
LevelDuration [3]int // 0,1,2, unknown level intervals, bonus duration per level
|
||||||
|
}
|
||||||
|
|
||||||
|
// MissileRecord is a representation of a row from missiles.txt
|
||||||
|
type MissileRecord struct {
|
||||||
|
ServerMovementCalc MissileCalc
|
||||||
|
ClientMovementCalc MissileCalc
|
||||||
|
ServerCollisionCalc MissileCalc
|
||||||
|
ClientCollisionCalc MissileCalc
|
||||||
|
ServerDamageCalc MissileCalc
|
||||||
|
Light MissileLight
|
||||||
|
Animation MissileAnimation
|
||||||
|
Collision MissileCollision
|
||||||
|
Damage MissileDamage
|
||||||
|
ElementalDamage MissileElementalDamage
|
||||||
|
SubMissile [3]string // 0,1,2 name of missiles spawned by movement function
|
||||||
|
HitSubMissile [4]string // 0,1,2 name of missiles spawned by collision function
|
||||||
|
ClientSubMissile [3]string // see above, but for client only
|
||||||
|
ClientHitSubMissile [4]string // see above, but for client only
|
||||||
|
Name string
|
||||||
|
|
||||||
|
SkillName string // if not empty, the missile will refer to this skill instead of its own data for the following:
|
||||||
|
// "ResultFlags, HitFlags, HitShift, HitClass, SrcDamage (SrcDam in skills.txt!),
|
||||||
|
// MinDam, MinLevDam1-5, MaxDam, MaxLevDam1-5, DmgSymPerCalc, EType, EMin, EMinLev1-5,
|
||||||
|
// EMax, EMaxLev1-5, EDmgSymPerCalc, ELen, ELenLev1-3, ELenSymPerCalc"
|
||||||
|
|
||||||
|
TravelSound string // name of sound to play during lifetime
|
||||||
|
// whether or not it loops depends on the specific sound's settings?
|
||||||
|
// if it doesn't loop, it's just a on-spawn sound effect
|
||||||
|
HitSound string // sound plays upon collision
|
||||||
|
ProgSound string // plays at "special events", like a mariachi band
|
||||||
|
|
||||||
|
ProgOverlay string // name of an overlay from overlays.txt to use at special events
|
||||||
|
ExplosionMissile string // name of a missile from missiles.txt that is created upon collision
|
||||||
|
// or anytime it is destroyed if AlwaysExplode is true
|
||||||
|
|
||||||
|
Id int //nolint:golint,stylecheck // ID is the correct key
|
||||||
|
|
||||||
|
ClientMovementFunc int
|
||||||
|
ClientCollisionFunc int
|
||||||
|
ServerMovementFunc int
|
||||||
|
ServerCollisionFunc int
|
||||||
|
ServerDamageFunc int
|
||||||
|
|
||||||
|
Velocity int
|
||||||
|
MaxVelocity int
|
||||||
|
LevelVelocityBonus int
|
||||||
|
Accel int
|
||||||
|
Range int
|
||||||
|
LevelRangeBonus int
|
||||||
|
|
||||||
|
XOffset int
|
||||||
|
YOffset int
|
||||||
|
ZOffset int
|
||||||
|
Size int // diameter
|
||||||
|
|
||||||
|
DestroyedByTPFrame int // see above, for client side, (this is a guess) which frame it vanishes on
|
||||||
|
|
||||||
|
HolyFilterType int // specifies what this missile can hit
|
||||||
|
KnockbackPercent int // chance of knocking the target back, 0-100
|
||||||
|
|
||||||
|
TransparencyMode int // controls rendering
|
||||||
|
// 0 = normal, 1 = alpha blending (darker = more transparent)
|
||||||
|
// 2 = special (black and white?)
|
||||||
|
|
||||||
|
ResultFlags int // unknown
|
||||||
|
// 4 = normal missiles, 5 = explosions, 8 = non-damaging missiles
|
||||||
|
HitFlags int // unknown
|
||||||
|
// 2 = explosions, 5 = freezing arrow
|
||||||
|
|
||||||
|
HitShift int // damage is measured in 256s
|
||||||
|
// the actual damage is [damage] * 2 ^ [hitshift]
|
||||||
|
// e.g. 100 damage, 8 hitshift = 100 * 2 ^ 8 = 100 * 256 = 25600
|
||||||
|
// (visually, the damage is this result / 256)
|
||||||
|
|
||||||
|
SourceDamage int // 0-128, 128 is 100%
|
||||||
|
SourceMissDamage int // 0-128, 128 is 100%
|
||||||
|
// unknown, only used for poison clouds.
|
||||||
|
|
||||||
|
HitClass int // controls clientside aesthetic effects for collisions
|
||||||
|
// particularly sound effects that are played on a hit
|
||||||
|
NumDirections int // count of dirs in the DCC loaded by CelFile
|
||||||
|
// apparently this value is no longer needed in D2
|
||||||
|
LocalBlood int // blood effects?
|
||||||
|
// 0 = no blood, 1 = blood, 2 = blood and affected by open wounds
|
||||||
|
DamageReductionRate int // how many frames between applications of the
|
||||||
|
// magic_damage_reduced stat, so for instance on a 0 this stat applies every frame
|
||||||
|
// on a 3, only every 4th frame has damage reduced
|
||||||
|
|
||||||
|
DestroyedByTP bool // if true, destroyed when source player teleports to town
|
||||||
|
CanDestroy bool // unknown
|
||||||
|
|
||||||
|
UseAttackRating bool // if true, uses 'attack rating' to determine if it hits or misses
|
||||||
|
// if false, has a 95% chance to hit.
|
||||||
|
AlwaysExplode bool // if true, always calls its collision function when it is destroyed,
|
||||||
|
// even if it doesn't hit anything
|
||||||
|
// note that some collision functions (lightning fury)
|
||||||
|
// seem to ignore this and always explode regardless of setting (requires investigation)
|
||||||
|
|
||||||
|
ClientExplosion bool // if true, does not really exist
|
||||||
|
// is only aesthetic / client side
|
||||||
|
TownSafe bool // if true, doesn't vanish when spawned in town
|
||||||
|
// if false, vanishes when spawned in town
|
||||||
|
IgnoreBossModifiers bool // if true, doesn't get bonuses from boss mods
|
||||||
|
IgnoreMultishot bool // if true, can't gain the mulitshot modifier
|
||||||
|
// 0 = all units, 1 = undead only, 2 = demons only, 3 = all units (again?)
|
||||||
|
CanBeSlowed bool // if true, is affected by skill_handofathena
|
||||||
|
TriggersHitEvents bool // if true, triggers events that happen "upon getting hit" on targets
|
||||||
|
TriggersGetHit bool // if true, can cause target to enter hit recovery mode
|
||||||
|
SoftHit bool // unknown
|
||||||
|
|
||||||
|
UseQuantity bool // if true, uses quantity
|
||||||
|
// not clear what this means. Also apparently requires a special starting function in skills.txt
|
||||||
|
AffectedByPierce bool // if true, affected by the pierce modifier and the Pierce skill
|
||||||
|
SpecialSetup bool // unknown, only true for potions
|
||||||
|
|
||||||
|
MissileSkill bool // if true, applies elemental damage from items to the splash radius instead of normal damage modifiers
|
||||||
|
|
||||||
|
ApplyMastery bool // unknown
|
||||||
|
// percentage of source units attack properties to apply to the missile?
|
||||||
|
// not only affects damage but also other modifiers like lifesteal and manasteal (need a complete list)
|
||||||
|
// setting this to -1 "gets rid of SrcDmg from skills.txt", not clear what that means
|
||||||
|
HalfDamageForTwoHander bool // if true, damage is halved when a two-handed weapon is used
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadMonsterAI loads MonsterAIRecords from monai.txt
|
||||||
|
func monsterAiLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(MonsterAI)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &MonsterAIRecord{
|
||||||
|
AI: d.String("AI"),
|
||||||
|
}
|
||||||
|
records[record.AI] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d MonsterAI records", len(records))
|
||||||
|
|
||||||
|
r.Monster.AI = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// MonsterAI holds the MonsterAIRecords, The monai.txt file is a lookup table for unit AI codes
|
||||||
|
type MonsterAI map[string]*MonsterAIRecord
|
||||||
|
|
||||||
|
// MonsterAIRecord represents a single row from monai.txt
|
||||||
|
type MonsterAIRecord struct {
|
||||||
|
AI string
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadMonsterEquipment loads MonsterEquipmentRecords into MonsterEquipment
|
||||||
|
func monsterEquipmentLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(map[string][]*MonsterEquipmentRecord)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &MonsterEquipmentRecord{
|
||||||
|
Name: d.String("monster"),
|
||||||
|
OnInit: d.Bool("oninit"),
|
||||||
|
Level: d.Number("level"),
|
||||||
|
Equipment: make([]*monEquip, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := 0; idx < numMonEquippedItems; idx++ {
|
||||||
|
num := idx + 1
|
||||||
|
code := d.String(fmt.Sprintf(fmtCode, num))
|
||||||
|
location := d.String(fmt.Sprintf(fmtLocation, num))
|
||||||
|
quality := d.Number(fmt.Sprintf(fmtQuality, num))
|
||||||
|
|
||||||
|
if code == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
equip := &monEquip{code, location, quality}
|
||||||
|
|
||||||
|
record.Equipment = append(record.Equipment, equip)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := records[record.Name]; !ok {
|
||||||
|
records[record.Name] = make([]*MonsterEquipmentRecord, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
records[record.Name] = append(records[record.Name], record)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
length := 0
|
||||||
|
for k := range records {
|
||||||
|
length += len(records[k])
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d MonsterEquipment records", length)
|
||||||
|
|
||||||
|
r.Monster.Equipment = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
const (
|
||||||
|
numMonEquippedItems = 3
|
||||||
|
fmtLocation = "loc%d"
|
||||||
|
fmtQuality = "mod%d"
|
||||||
|
fmtCode = "item%d"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MonsterEquipment stores the MonsterEquipmentRecords
|
||||||
|
type MonsterEquipment map[string][]*MonsterEquipmentRecord
|
||||||
|
|
||||||
|
// MonsterEquipmentRecord represents a single line in monequip.txt
|
||||||
|
// Information gathered from [https://d2mods.info/forum/kb/viewarticle?a=365]
|
||||||
|
type MonsterEquipmentRecord struct {
|
||||||
|
// Name of monster, pointer to MonStats.txt
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// If true, monster is created by level, otherwise created by skill
|
||||||
|
OnInit bool
|
||||||
|
|
||||||
|
// Not written in description, only appear on monsters with OnInit false,
|
||||||
|
// Level of skill for which this equipment row can be used?
|
||||||
|
Level int
|
||||||
|
|
||||||
|
Equipment []*monEquip
|
||||||
|
}
|
||||||
|
|
||||||
|
type monEquip struct {
|
||||||
|
// Code of item, probably from ItemCommonRecords
|
||||||
|
Code string
|
||||||
|
|
||||||
|
// Location the body location of the item
|
||||||
|
Location string
|
||||||
|
|
||||||
|
// Quality of the item
|
||||||
|
Quality int
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// MonsterLevels stores the MonsterLevelRecords
|
||||||
|
type MonsterLevels map[int]*MonsterLevelRecord
|
||||||
|
|
||||||
|
// MonsterLevelRecord represents a single row in monlvl.txt
|
||||||
|
type MonsterLevelRecord struct {
|
||||||
|
|
||||||
|
// The level
|
||||||
|
Level int
|
||||||
|
|
||||||
|
// Values for Battle.net
|
||||||
|
BattleNet monsterDifficultyLevels
|
||||||
|
|
||||||
|
// Values for ladder/single player/lan
|
||||||
|
Ladder monsterDifficultyLevels
|
||||||
|
}
|
||||||
|
|
||||||
|
type monsterDifficultyLevels struct {
|
||||||
|
Normal monsterLevelValues
|
||||||
|
Nightmare monsterLevelValues
|
||||||
|
Hell monsterLevelValues
|
||||||
|
}
|
||||||
|
|
||||||
|
type monsterLevelValues struct {
|
||||||
|
// DefenseRating AC is calcuated as (MonLvl.txt Ac * Monstats.txt AC) / 100)
|
||||||
|
DefenseRating int // also known as armor class
|
||||||
|
|
||||||
|
// ToHit influences ToHit values for both attacks
|
||||||
|
// (MonLvl.txt TH * Monstats.txt A1TH
|
||||||
|
// and MonLvl.txt TH * Monstats.txt A2TH) / 100
|
||||||
|
AttackRating int
|
||||||
|
|
||||||
|
// Hitpoints, influences both minimum and maximum HP
|
||||||
|
// (MonLvl.txt HP * Monstats.txt minHP) / 100
|
||||||
|
// and MonLvl.txt HP * Monstats.txt maxHP) / 100
|
||||||
|
Hitpoints int
|
||||||
|
|
||||||
|
// Damage, influences minimum and maximum damage for both attacks
|
||||||
|
// MonLvl.txt DM * Monstats.txt A1MinD) / 100
|
||||||
|
// and MonLvl.txt DM * Monstats.txt A1MaxD) / 100
|
||||||
|
// and MonLvl.txt DM * Monstats.txt A2MinD) / 100
|
||||||
|
// and MonLvl.txt DM * Monstats.txt A2MaxD) / 100
|
||||||
|
Damage int
|
||||||
|
|
||||||
|
// Experience points,
|
||||||
|
// the formula is (MonLvl.txt XP * Monstats.txt Exp) / 100
|
||||||
|
Experience int
|
||||||
|
}
|
|
@ -0,0 +1,62 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func monsterLevelsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(map[int]*MonsterLevelRecord)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &MonsterLevelRecord{
|
||||||
|
Level: d.Number("Level"),
|
||||||
|
BattleNet: monsterDifficultyLevels{
|
||||||
|
Normal: monsterLevelValues{
|
||||||
|
Hitpoints: d.Number("HP"),
|
||||||
|
Damage: d.Number("DM"),
|
||||||
|
Experience: d.Number("XP"),
|
||||||
|
},
|
||||||
|
Nightmare: monsterLevelValues{
|
||||||
|
Hitpoints: d.Number("HP(N)"),
|
||||||
|
Damage: d.Number("DM(N)"),
|
||||||
|
Experience: d.Number("XP(N)"),
|
||||||
|
},
|
||||||
|
Hell: monsterLevelValues{
|
||||||
|
Hitpoints: d.Number("HP(H)"),
|
||||||
|
Damage: d.Number("DM(H)"),
|
||||||
|
Experience: d.Number("XP(H)"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Ladder: monsterDifficultyLevels{
|
||||||
|
Normal: monsterLevelValues{
|
||||||
|
Hitpoints: d.Number("L-HP"),
|
||||||
|
Damage: d.Number("L-DM"),
|
||||||
|
Experience: d.Number("L-XP"),
|
||||||
|
},
|
||||||
|
Nightmare: monsterLevelValues{
|
||||||
|
Hitpoints: d.Number("L-HP(N)"),
|
||||||
|
Damage: d.Number("L-DM(N)"),
|
||||||
|
Experience: d.Number("L-XP(N)"),
|
||||||
|
},
|
||||||
|
Hell: monsterLevelValues{
|
||||||
|
Hitpoints: d.Number("L-HP(H)"),
|
||||||
|
Damage: d.Number("L-DM(H)"),
|
||||||
|
Experience: d.Number("L-XP(H)"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
records[record.Level] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d MonsterLevel records", len(records))
|
||||||
|
|
||||||
|
r.Monster.Levels = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadMonModes loads monster records
|
||||||
|
func monsterModeLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(MonModes)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &MonModeRecord{
|
||||||
|
Name: d.String("name"),
|
||||||
|
Token: d.String("token"),
|
||||||
|
Code: d.String("code"),
|
||||||
|
}
|
||||||
|
records[record.Name] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d MonMode records", len(records))
|
||||||
|
|
||||||
|
r.Monster.Modes = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// MonModes stores all of the GemsRecords
|
||||||
|
type MonModes map[string]*MonModeRecord
|
||||||
|
|
||||||
|
// MonModeRecord is a representation of a single row of Monmode.txt
|
||||||
|
type MonModeRecord struct {
|
||||||
|
Name string
|
||||||
|
Token string
|
||||||
|
Code string
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadMonsterPlacements loads the MonsterPlacementRecords into MonsterPlacements.
|
||||||
|
func monsterPlacementsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(MonsterPlacements, 0)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
records = append(records, MonsterPlacementRecord(d.String("code")))
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Monster.Placements = records
|
||||||
|
|
||||||
|
log.Printf("Loaded %d MonsterPlacement records", len(records))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// MonsterPlacements stores the MonsterPlacementRecords.
|
||||||
|
type MonsterPlacements []MonsterPlacementRecord
|
||||||
|
|
||||||
|
// MonsterPlacementRecord represents a line from MonPlace.txt.
|
||||||
|
type MonsterPlacementRecord string
|
|
@ -0,0 +1,31 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadMonPresets loads monster presets from monpresets.txt
|
||||||
|
func monsterPresetLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(MonPresets)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
act := int32(d.Number("Act"))
|
||||||
|
if _, ok := records[act]; !ok {
|
||||||
|
records[act] = make([]string, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
records[act] = append(records[act], d.String("Place"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d MonPreset records", len(records))
|
||||||
|
|
||||||
|
r.Monster.Presets = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// MonPresets stores monster presets
|
||||||
|
type MonPresets map[int32][]string
|
|
@ -0,0 +1,66 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func monsterPropertiesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(MonsterProperties)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &MonPropRecord{
|
||||||
|
ID: d.String("Id"),
|
||||||
|
|
||||||
|
Properties: struct {
|
||||||
|
Normal [NumMonProps]*MonProp
|
||||||
|
Nightmare [NumMonProps]*MonProp
|
||||||
|
Hell [NumMonProps]*MonProp
|
||||||
|
}{
|
||||||
|
[NumMonProps]*MonProp{},
|
||||||
|
[NumMonProps]*MonProp{},
|
||||||
|
[NumMonProps]*MonProp{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx := 1; idx <= NumMonProps; idx++ {
|
||||||
|
record.Properties.Normal[idx-1] = &MonProp{
|
||||||
|
Code: d.String(fmt.Sprintf(FmtProp, idx, FmtNormal)),
|
||||||
|
Param: d.String(fmt.Sprintf(FmtPar, idx, FmtNormal)),
|
||||||
|
Chance: d.Number(fmt.Sprintf(FmtChance, idx, FmtNormal)),
|
||||||
|
Min: d.Number(fmt.Sprintf(FmtMin, idx, FmtNormal)),
|
||||||
|
Max: d.Number(fmt.Sprintf(FmtMax, idx, FmtNormal)),
|
||||||
|
}
|
||||||
|
|
||||||
|
record.Properties.Nightmare[idx-1] = &MonProp{
|
||||||
|
Code: d.String(fmt.Sprintf(FmtProp, idx, FmtNightmare)),
|
||||||
|
Param: d.String(fmt.Sprintf(FmtPar, idx, FmtNightmare)),
|
||||||
|
Chance: d.Number(fmt.Sprintf(FmtChance, idx, FmtNightmare)),
|
||||||
|
Min: d.Number(fmt.Sprintf(FmtMin, idx, FmtNightmare)),
|
||||||
|
Max: d.Number(fmt.Sprintf(FmtMax, idx, FmtNightmare)),
|
||||||
|
}
|
||||||
|
|
||||||
|
record.Properties.Hell[idx-1] = &MonProp{
|
||||||
|
Code: d.String(fmt.Sprintf(FmtProp, idx, FmtHell)),
|
||||||
|
Param: d.String(fmt.Sprintf(FmtPar, idx, FmtHell)),
|
||||||
|
Chance: d.Number(fmt.Sprintf(FmtChance, idx, FmtHell)),
|
||||||
|
Min: d.Number(fmt.Sprintf(FmtMin, idx, FmtHell)),
|
||||||
|
Max: d.Number(fmt.Sprintf(FmtMax, idx, FmtHell)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
records[record.ID] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d MonProp records", len(records))
|
||||||
|
|
||||||
|
r.Monster.Props = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
const (
|
||||||
|
NumMonProps = 6
|
||||||
|
FmtProp = "prop%d%s"
|
||||||
|
FmtChance = "chance%d%s"
|
||||||
|
FmtPar = "par%d%s"
|
||||||
|
FmtMin = "min%d%s"
|
||||||
|
FmtMax = "max%d%s"
|
||||||
|
FmtNormal = ""
|
||||||
|
FmtNightmare = " (N)"
|
||||||
|
FmtHell = " (H)"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MonsterProperties stores all of the MonPropRecords
|
||||||
|
type MonsterProperties map[string]*MonPropRecord
|
||||||
|
|
||||||
|
// MonPropRecord is a representation of a single row of monprop.txt
|
||||||
|
type MonPropRecord struct {
|
||||||
|
ID string
|
||||||
|
|
||||||
|
Properties struct {
|
||||||
|
Normal [NumMonProps]*MonProp
|
||||||
|
Nightmare [NumMonProps]*MonProp
|
||||||
|
Hell [NumMonProps]*MonProp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MonProp is a monster property
|
||||||
|
type MonProp struct {
|
||||||
|
Code string
|
||||||
|
Param string
|
||||||
|
Chance int
|
||||||
|
Min int
|
||||||
|
Max int
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadMonsterSequences loads the MonsterSequenceRecords into MonsterSequences
|
||||||
|
func monsterSequencesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(MonsterSequences)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
name := d.String("sequence")
|
||||||
|
|
||||||
|
if _, ok := records[name]; !ok {
|
||||||
|
record := &MonsterSequenceRecord{
|
||||||
|
Name: name,
|
||||||
|
Frames: make([]*MonsterSequenceFrame, 0),
|
||||||
|
}
|
||||||
|
records[name] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
records[name].Frames = append(records[name].Frames, &MonsterSequenceFrame{
|
||||||
|
Mode: d.String("mode"),
|
||||||
|
Frame: d.Number("frame"),
|
||||||
|
Direction: d.Number("dir"),
|
||||||
|
Event: d.Number("event"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d MonsterSequence records", len(records))
|
||||||
|
|
||||||
|
r.Monster.Sequences = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// MonsterSequences contains the MonsterSequenceRecords
|
||||||
|
type MonsterSequences map[string]*MonsterSequenceRecord
|
||||||
|
|
||||||
|
// MonsterSequenceRecord contains a record for a monster sequence
|
||||||
|
// Composed of multiple lines from monseq.txt with the same name in the first column.
|
||||||
|
// Information gathered from [https://d2mods.info/forum/kb/viewarticle?a=395]
|
||||||
|
type MonsterSequenceRecord struct {
|
||||||
|
|
||||||
|
// Name of the sequence, referred to by monstats.txt
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// Frames of this sequence
|
||||||
|
Frames []*MonsterSequenceFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
// MonsterSequenceFrame represents a single frame of a monster sequence
|
||||||
|
type MonsterSequenceFrame struct {
|
||||||
|
// The animation mode for this frame (refers to MonMode.txt)
|
||||||
|
Mode string
|
||||||
|
|
||||||
|
// The frame of the animation mode used for this frame of the sequence
|
||||||
|
Frame int
|
||||||
|
|
||||||
|
// Direction of the frame, enumerated by d2enum.AnimationFrameDirection
|
||||||
|
Direction int
|
||||||
|
|
||||||
|
// Event triggered by this frame
|
||||||
|
Event int
|
||||||
|
}
|
|
@ -0,0 +1,67 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Information gathered from [https://d2mods.info/forum/kb/viewarticle?a=418]
|
||||||
|
|
||||||
|
// LoadMonsterSounds loads MonsterSoundRecords into MonsterSounds
|
||||||
|
func monsterSoundsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(MonsterSounds)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &MonsterSoundRecord{
|
||||||
|
ID: d.String("Id"),
|
||||||
|
Attack1: d.String("Attack1"),
|
||||||
|
Weapon1: d.String("Weapon1"),
|
||||||
|
Attack1Delay: d.Number("Att1Del"),
|
||||||
|
Weapon1Delay: d.Number("Wea1Del"),
|
||||||
|
Attack1Probability: d.Number("Att1Prb"),
|
||||||
|
Weapon1Volume: d.Number("Wea1Vol"),
|
||||||
|
Attack2: d.String("Attack2"),
|
||||||
|
Weapon2: d.String("Weapon2"),
|
||||||
|
Attack2Delay: d.Number("Att2Del"),
|
||||||
|
Weapon2Delay: d.Number("Wea2Del"),
|
||||||
|
Attack2Probability: d.Number("Att2Prb"),
|
||||||
|
Weapon2Volume: d.Number("Wea2Vol"),
|
||||||
|
Skill1: d.String("Skill1"),
|
||||||
|
Skill2: d.String("Skill2"),
|
||||||
|
Skill3: d.String("Skill3"),
|
||||||
|
Skill4: d.String("Skill4"),
|
||||||
|
Footstep: d.String("Footstep"),
|
||||||
|
FootstepLayer: d.String("FootstepLayer"),
|
||||||
|
FootstepCount: d.Number("FsCnt"),
|
||||||
|
FootstepOffset: d.Number("FsOff"),
|
||||||
|
FootstepProbability: d.Number("FsPrb"),
|
||||||
|
Neutral: d.String("Neutral"),
|
||||||
|
NeutralTime: d.Number("NeuTime"),
|
||||||
|
Init: d.String("Init"),
|
||||||
|
Taunt: d.String("Taunt"),
|
||||||
|
Flee: d.String("Flee"),
|
||||||
|
CvtMo1: d.String("CvtMo1"),
|
||||||
|
CvtMo2: d.String("CvtMo2"),
|
||||||
|
CvtMo3: d.String("CvtMo3"),
|
||||||
|
CvtSk1: d.String("CvtSk1"),
|
||||||
|
CvtSk2: d.String("CvtSk2"),
|
||||||
|
CvtSk3: d.String("CvtSk3"),
|
||||||
|
CvtTgt1: d.String("CvtTgt1"),
|
||||||
|
CvtTgt2: d.String("CvtTgt2"),
|
||||||
|
CvtTgt3: d.String("CvtTgt3"),
|
||||||
|
}
|
||||||
|
|
||||||
|
records[record.ID] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d Monster Sound records", len(records))
|
||||||
|
|
||||||
|
r.Monster.Sounds = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// MonsterSounds stores the MonsterSoundRecords
|
||||||
|
type MonsterSounds map[string]*MonsterSoundRecord
|
||||||
|
|
||||||
|
// MonsterSoundRecord represents a single line in MonSounds.txt
|
||||||
|
type MonsterSoundRecord struct {
|
||||||
|
// ID is the identifier, used in MonStats.txt to refer to a particular sound record
|
||||||
|
ID string
|
||||||
|
|
||||||
|
// Melee attack sound ID, refers to a sound from Sounds.txt
|
||||||
|
Attack1 string
|
||||||
|
|
||||||
|
// Weapon attack sound ID, refers to a sound from Sounds.txt
|
||||||
|
Weapon1 string
|
||||||
|
|
||||||
|
// Delay in frames of Attack1 sound
|
||||||
|
Attack1Delay int
|
||||||
|
|
||||||
|
// Delay in frames of Weapon1 sound
|
||||||
|
Weapon1Delay int
|
||||||
|
|
||||||
|
// Probability of playing Attack1 sound instead of Weapon1
|
||||||
|
Attack1Probability int
|
||||||
|
|
||||||
|
// Overrides weapon volume from Sounds.txt
|
||||||
|
Weapon1Volume int
|
||||||
|
|
||||||
|
// Ditto, 2 sets of sounds are possible
|
||||||
|
Attack2 string
|
||||||
|
Weapon2 string
|
||||||
|
Attack2Delay int
|
||||||
|
Weapon2Delay int
|
||||||
|
Attack2Probability int
|
||||||
|
Weapon2Volume int
|
||||||
|
|
||||||
|
// Sound when monster takes a hit, refers to a sound from Sounds.txt
|
||||||
|
HitSound string
|
||||||
|
|
||||||
|
// Sound when monster dies, refers to a sound from Sounds.txt
|
||||||
|
DeathSound string
|
||||||
|
|
||||||
|
// Delay in frames of HitSound
|
||||||
|
HitDelay int
|
||||||
|
|
||||||
|
// Delay in frames of DeathSound
|
||||||
|
DeaDelay int
|
||||||
|
|
||||||
|
// Sound when monster enters skill mode
|
||||||
|
Skill1 string
|
||||||
|
Skill2 string
|
||||||
|
Skill3 string
|
||||||
|
Skill4 string
|
||||||
|
|
||||||
|
// Sound played each loop of the WL animation
|
||||||
|
Footstep string
|
||||||
|
|
||||||
|
// Additional WL animation sound
|
||||||
|
FootstepLayer string
|
||||||
|
|
||||||
|
// Number of footstep sounds played (e.g. 2 for two-legged monsters)
|
||||||
|
FootstepCount int
|
||||||
|
|
||||||
|
// FsOff, possibly delay between footstep sounds
|
||||||
|
FootstepOffset int
|
||||||
|
|
||||||
|
// Probability of playing footstep sound, percentage
|
||||||
|
FootstepProbability int
|
||||||
|
|
||||||
|
// Sound when monster is neutral (also played when walking)
|
||||||
|
Neutral string
|
||||||
|
|
||||||
|
// Delay in frames between neutral sounds
|
||||||
|
NeutralTime int
|
||||||
|
|
||||||
|
// Sound when monster is initialized
|
||||||
|
Init string
|
||||||
|
|
||||||
|
// Sound when monster is encountered
|
||||||
|
Taunt string
|
||||||
|
|
||||||
|
// Sound when monster retreats
|
||||||
|
Flee string
|
||||||
|
|
||||||
|
// The following are related to skills in some way
|
||||||
|
// Initial monster animation code (MonMode.txt)
|
||||||
|
CvtMo1 string
|
||||||
|
// ID of skill
|
||||||
|
CvtSk1 string
|
||||||
|
// End monster animation code (MonMode.txt)
|
||||||
|
CvtTgt1 string
|
||||||
|
|
||||||
|
CvtMo2 string
|
||||||
|
CvtSk2 string
|
||||||
|
CvtTgt2 string
|
||||||
|
|
||||||
|
CvtMo3 string
|
||||||
|
CvtSk3 string
|
||||||
|
CvtTgt3 string
|
||||||
|
}
|
|
@ -0,0 +1,181 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadMonStats2 loads MonStats2Records from monstats2.txt
|
||||||
|
//nolint:funlen //just a big data loader
|
||||||
|
func monsterStats2Loader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(MonStats2)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &MonStats2Record{
|
||||||
|
Key: d.String("Id"),
|
||||||
|
Height: d.Number("Height"),
|
||||||
|
OverlayHeight: d.Number("OverlayHeight"),
|
||||||
|
PixelHeight: d.Number("pixHeight"),
|
||||||
|
SizeX: d.Number("SizeX"),
|
||||||
|
SizeY: d.Number("SizeY"),
|
||||||
|
SpawnMethod: d.Number("spawnCol"),
|
||||||
|
MeleeRng: d.Number("MeleeRng"),
|
||||||
|
BaseWeaponClass: d.String("BaseW"),
|
||||||
|
HitClass: d.Number("HitClass"),
|
||||||
|
EquipmentOptions: [16][]string{
|
||||||
|
d.List("HDv"),
|
||||||
|
d.List("TRv"),
|
||||||
|
d.List("LGv"),
|
||||||
|
d.List("Rav"),
|
||||||
|
d.List("Lav"),
|
||||||
|
d.List("RHv"),
|
||||||
|
d.List("LHv"),
|
||||||
|
d.List("SHv"),
|
||||||
|
d.List("S1v"),
|
||||||
|
d.List("S2v"),
|
||||||
|
d.List("S3v"),
|
||||||
|
d.List("S4v"),
|
||||||
|
d.List("S5v"),
|
||||||
|
d.List("S6v"),
|
||||||
|
d.List("S7v"),
|
||||||
|
d.List("S8v"),
|
||||||
|
},
|
||||||
|
HasComponent: [16]bool{
|
||||||
|
d.Bool("HD"),
|
||||||
|
d.Bool("TR"),
|
||||||
|
d.Bool("LG"),
|
||||||
|
d.Bool("RA"),
|
||||||
|
d.Bool("LA"),
|
||||||
|
d.Bool("RH"),
|
||||||
|
d.Bool("LH"),
|
||||||
|
d.Bool("SH"),
|
||||||
|
d.Bool("S1"),
|
||||||
|
d.Bool("S2"),
|
||||||
|
d.Bool("S3"),
|
||||||
|
d.Bool("S4"),
|
||||||
|
d.Bool("S5"),
|
||||||
|
d.Bool("S6"),
|
||||||
|
d.Bool("S7"),
|
||||||
|
d.Bool("S8"),
|
||||||
|
},
|
||||||
|
TotalPieces: d.Number("TotalPieces"),
|
||||||
|
HasAnimationMode: [16]bool{
|
||||||
|
d.Bool("mDT"),
|
||||||
|
d.Bool("mNU"),
|
||||||
|
d.Bool("mWL"),
|
||||||
|
d.Bool("mGH"),
|
||||||
|
d.Bool("mA1"),
|
||||||
|
d.Bool("mA2"),
|
||||||
|
d.Bool("mBL"),
|
||||||
|
d.Bool("mSC"),
|
||||||
|
d.Bool("mS1"),
|
||||||
|
d.Bool("mS2"),
|
||||||
|
d.Bool("mS3"),
|
||||||
|
d.Bool("mS4"),
|
||||||
|
d.Bool("mDD"),
|
||||||
|
d.Bool("mKB"),
|
||||||
|
d.Bool("mSQ"),
|
||||||
|
d.Bool("mRN"),
|
||||||
|
},
|
||||||
|
DirectionsPerMode: [16]int{
|
||||||
|
d.Number("dDT"),
|
||||||
|
d.Number("dNU"),
|
||||||
|
d.Number("dWL"),
|
||||||
|
d.Number("dGH"),
|
||||||
|
d.Number("dA1"),
|
||||||
|
d.Number("dA2"),
|
||||||
|
d.Number("dBL"),
|
||||||
|
d.Number("dSC"),
|
||||||
|
d.Number("dS1"),
|
||||||
|
d.Number("dS2"),
|
||||||
|
d.Number("dS3"),
|
||||||
|
d.Number("dS4"),
|
||||||
|
d.Number("dDD"),
|
||||||
|
d.Number("dKB"),
|
||||||
|
d.Number("dSQ"),
|
||||||
|
d.Number("dRN"),
|
||||||
|
},
|
||||||
|
A1mv: d.Bool("A1mv"),
|
||||||
|
A2mv: d.Bool("A2mv"),
|
||||||
|
SCmv: d.Bool("SCmv"),
|
||||||
|
S1mv: d.Bool("S1mv"),
|
||||||
|
S2mv: d.Bool("S2mv"),
|
||||||
|
S3mv: d.Bool("S3mv"),
|
||||||
|
S4mv: d.Bool("S4mv"),
|
||||||
|
NoGfxHitTest: d.Bool("noGfxHitTest"),
|
||||||
|
BoxTop: d.Number("htTop"),
|
||||||
|
BoxLeft: d.Number("htLeft"),
|
||||||
|
BoxWidth: d.Number("htWidth"),
|
||||||
|
BoxHeight: d.Number("htHeight"),
|
||||||
|
Restore: d.Number("restore"),
|
||||||
|
AutomapCel: d.Number("automapCel"),
|
||||||
|
NoMap: d.Bool("noMap"),
|
||||||
|
NoOvly: d.Bool("noOvly"),
|
||||||
|
IsSelectable: d.Bool("isSel"),
|
||||||
|
AllySelectable: d.Bool("alSel"),
|
||||||
|
shiftSel: d.Bool("shiftSel"),
|
||||||
|
NotSelectable: d.Bool("noSel"),
|
||||||
|
IsCorpseSelectable: d.Bool("corpseSel"),
|
||||||
|
IsAttackable: d.Bool("isAtt"),
|
||||||
|
IsRevivable: d.Bool("revive"),
|
||||||
|
IsCritter: d.Bool("critter"),
|
||||||
|
IsSmall: d.Bool("small"),
|
||||||
|
IsLarge: d.Bool("large"),
|
||||||
|
IsSoft: d.Bool("soft"),
|
||||||
|
IsInert: d.Bool("inert"),
|
||||||
|
objCol: d.Bool("objCol"),
|
||||||
|
IsCorpseCollidable: d.Bool("deadCol"),
|
||||||
|
IsCorpseWalkable: d.Bool("unflatDead"),
|
||||||
|
HasShadow: d.Bool("Shadow"),
|
||||||
|
NoUniqueShift: d.Bool("noUniqueShift"),
|
||||||
|
CompositeDeath: d.Bool("compositeDeath"),
|
||||||
|
LocalBlood: d.Number("localBlood"),
|
||||||
|
Bleed: d.Number("Bleed"),
|
||||||
|
Light: d.Number("Light"),
|
||||||
|
LightR: d.Number("light-r"),
|
||||||
|
LightG: d.Number("light-g"),
|
||||||
|
lightB: d.Number("light-b"),
|
||||||
|
NormalPalette: d.Number("Utrans"),
|
||||||
|
NightmarePalette: d.Number("Utrans(N)"),
|
||||||
|
HellPalatte: d.Number("Utrans(H)"),
|
||||||
|
Heart: d.String("Heart"),
|
||||||
|
BodyPart: d.String("BodyPart"),
|
||||||
|
InfernoLen: d.Number("InfernoLen"),
|
||||||
|
InfernoAnim: d.Number("InfernoAnim"),
|
||||||
|
InfernoRollback: d.Number("InfernoRollback"),
|
||||||
|
ResurrectMode: monsterAnimationModeFromString(d.String("ResurrectMode")),
|
||||||
|
ResurrectSkill: d.String("ResurrectSkill"),
|
||||||
|
}
|
||||||
|
|
||||||
|
records[record.Key] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
panic(d.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d MonStats2 records", len(records))
|
||||||
|
|
||||||
|
r.Monster.Stats2 = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//nolint:gochecknoglobals // better for lookup
|
||||||
|
var monsterAnimationModeLookup = map[string]d2enum.MonsterAnimationMode{
|
||||||
|
d2enum.MonsterAnimationModeNeutral.String(): d2enum.MonsterAnimationModeNeutral,
|
||||||
|
d2enum.MonsterAnimationModeSkill1.String(): d2enum.MonsterAnimationModeSkill1,
|
||||||
|
d2enum.MonsterAnimationModeSequence.String(): d2enum.MonsterAnimationModeSequence,
|
||||||
|
}
|
||||||
|
|
||||||
|
func monsterAnimationModeFromString(s string) d2enum.MonsterAnimationMode {
|
||||||
|
v, ok := monsterAnimationModeLookup[s]
|
||||||
|
if !ok {
|
||||||
|
log.Fatalf("unhandled MonsterAnimationMode %q", s)
|
||||||
|
return d2enum.MonsterAnimationModeNeutral
|
||||||
|
}
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
|
@ -0,0 +1,164 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
|
||||||
|
// MonStats2 stores all of the MonStats2Records
|
||||||
|
type MonStats2 map[string]*MonStats2Record
|
||||||
|
|
||||||
|
// MonStats2Record is a representation of a row from monstats2.txt
|
||||||
|
type MonStats2Record struct {
|
||||||
|
// Available options for equipment
|
||||||
|
// randomly selected from
|
||||||
|
EquipmentOptions [16][]string
|
||||||
|
|
||||||
|
Key string // Key, the object ID MonStatEx feild from MonStat
|
||||||
|
BaseWeaponClass string
|
||||||
|
ResurrectSkill string
|
||||||
|
Heart string
|
||||||
|
BodyPart string
|
||||||
|
|
||||||
|
// These follow three are apparently unused
|
||||||
|
Height int
|
||||||
|
OverlayHeight int
|
||||||
|
PixelHeight int
|
||||||
|
|
||||||
|
// Diameter in subtiles
|
||||||
|
SizeX int
|
||||||
|
SizeY int
|
||||||
|
|
||||||
|
// Bounding box
|
||||||
|
BoxTop int
|
||||||
|
BoxLeft int
|
||||||
|
BoxWidth int
|
||||||
|
BoxHeight int
|
||||||
|
|
||||||
|
// Spawn method used
|
||||||
|
SpawnMethod int
|
||||||
|
|
||||||
|
// Melee radius
|
||||||
|
MeleeRng int
|
||||||
|
|
||||||
|
// base weaponclass?
|
||||||
|
HitClass int
|
||||||
|
|
||||||
|
// Sum of available components
|
||||||
|
TotalPieces int
|
||||||
|
|
||||||
|
// Number of directions for each mode
|
||||||
|
DirectionsPerMode [16]int
|
||||||
|
|
||||||
|
// If the units is restored on map reload
|
||||||
|
Restore int
|
||||||
|
|
||||||
|
// What maximap index is used for the automap
|
||||||
|
AutomapCel int
|
||||||
|
|
||||||
|
// Blood offset?
|
||||||
|
LocalBlood int
|
||||||
|
|
||||||
|
// 0 = don't bleed, 1 = small blood missile, 2 = small and large, > 3 other missiles?
|
||||||
|
Bleed int
|
||||||
|
|
||||||
|
// If the unit is lights up the area
|
||||||
|
Light int
|
||||||
|
|
||||||
|
// Light color
|
||||||
|
LightR int
|
||||||
|
LightG int
|
||||||
|
lightB int
|
||||||
|
|
||||||
|
// Palettes per difficulty
|
||||||
|
NormalPalette int
|
||||||
|
NightmarePalette int
|
||||||
|
HellPalatte int
|
||||||
|
|
||||||
|
// These two are useless as of 1.07
|
||||||
|
|
||||||
|
// Inferno animation stuff
|
||||||
|
InfernoLen int
|
||||||
|
InfernoAnim int
|
||||||
|
InfernoRollback int
|
||||||
|
// Which mode is used after resurrection
|
||||||
|
ResurrectMode d2enum.MonsterAnimationMode
|
||||||
|
|
||||||
|
// This specifies if the size values get used for collision detection
|
||||||
|
NoGfxHitTest bool
|
||||||
|
|
||||||
|
// Does the unit have this component
|
||||||
|
HasComponent [16]bool
|
||||||
|
|
||||||
|
// Available animation modes
|
||||||
|
HasAnimationMode [16]bool
|
||||||
|
|
||||||
|
// Available modes while moving aside from WL and RN
|
||||||
|
A1mv bool
|
||||||
|
A2mv bool
|
||||||
|
SCmv bool
|
||||||
|
S1mv bool
|
||||||
|
S2mv bool
|
||||||
|
S3mv bool
|
||||||
|
S4mv bool
|
||||||
|
|
||||||
|
// true of unit uses an automap entry
|
||||||
|
NoMap bool
|
||||||
|
|
||||||
|
// If the units can use overlays
|
||||||
|
NoOvly bool
|
||||||
|
|
||||||
|
// If unit is selectable
|
||||||
|
IsSelectable bool
|
||||||
|
|
||||||
|
// If unit is selectable by allies
|
||||||
|
AllySelectable bool
|
||||||
|
|
||||||
|
// If unit is not selectable
|
||||||
|
NotSelectable bool
|
||||||
|
|
||||||
|
// Kinda unk, used for bonewalls etc that are not properly selectable
|
||||||
|
shiftSel bool
|
||||||
|
|
||||||
|
// if the units corpse is selectable
|
||||||
|
IsCorpseSelectable bool
|
||||||
|
|
||||||
|
// If the unit is attackable
|
||||||
|
IsAttackable bool
|
||||||
|
|
||||||
|
// If the unit is revivable
|
||||||
|
IsRevivable bool
|
||||||
|
|
||||||
|
// If the unit is a critter
|
||||||
|
IsCritter bool
|
||||||
|
|
||||||
|
// If the unit is Small, Small units can be knocked back with 100% efficiency
|
||||||
|
IsSmall bool
|
||||||
|
|
||||||
|
// Large units can be knocked back at 25% efficincy
|
||||||
|
IsLarge bool
|
||||||
|
|
||||||
|
// Possibly to do with sound, usually set for creatures without flesh
|
||||||
|
IsSoft bool
|
||||||
|
|
||||||
|
// Aggressive or harmless, usually NPC's
|
||||||
|
IsInert bool
|
||||||
|
|
||||||
|
// Unknown
|
||||||
|
objCol bool
|
||||||
|
|
||||||
|
// Enables collision on corpse for units
|
||||||
|
IsCorpseCollidable bool
|
||||||
|
|
||||||
|
// Can the corpse be walked through
|
||||||
|
IsCorpseWalkable bool
|
||||||
|
|
||||||
|
// If the unit casts a shadow
|
||||||
|
HasShadow bool
|
||||||
|
|
||||||
|
// If unique palettes should not be used
|
||||||
|
NoUniqueShift bool
|
||||||
|
|
||||||
|
// If multiple layers should be used on death (otherwise only TR)
|
||||||
|
CompositeDeath bool
|
||||||
|
|
||||||
|
// Which skill is used for resurrection
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,282 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadMonStats loads monstats
|
||||||
|
func monsterStatsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(MonStats)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &MonStatsRecord{
|
||||||
|
Key: d.String("Id"),
|
||||||
|
ID: d.Number("hcIdx"),
|
||||||
|
BaseKey: d.String("BaseId"),
|
||||||
|
NextKey: d.String("NextInClass"),
|
||||||
|
PaletteId: d.Number("TransLvl"),
|
||||||
|
NameString: d.String("NameStr"),
|
||||||
|
ExtraDataKey: d.String("MonStatsEx"),
|
||||||
|
PropertiesKey: d.String("MonProp"),
|
||||||
|
MonsterGroup: d.String("MonType"),
|
||||||
|
AiKey: d.String("AI"),
|
||||||
|
DescriptionStringTableKey: d.String("DescStr"),
|
||||||
|
AnimationDirectoryToken: d.String("Code"),
|
||||||
|
Enabled: d.Number("enabled") > 0,
|
||||||
|
IsRanged: d.Number("rangedtype") > 0,
|
||||||
|
SpawnsMinions: d.Number("placespawn") > 0,
|
||||||
|
SpawnKey: d.String("spawn"),
|
||||||
|
SpawnOffsetX: d.Number("spawnx"),
|
||||||
|
SpawnOffsetY: d.Number("spawny"),
|
||||||
|
SpawnAnimationKey: d.String("spawnmode"),
|
||||||
|
MinionId1: d.String("minion1"),
|
||||||
|
MinionId2: d.String("minion2"),
|
||||||
|
IsLeader: d.Number("SetBoss") > 0,
|
||||||
|
TransferLeadership: d.Number("BossXfer") > 0,
|
||||||
|
MinionPartyMin: d.Number("PartyMin"),
|
||||||
|
MinionPartyMax: d.Number("PartyMax"),
|
||||||
|
MinionGroupMin: d.Number("MinGrp"),
|
||||||
|
MinionGroupMax: d.Number("MaxGrp"),
|
||||||
|
PopulationReductionPercent: d.Number("sparsePopulate"),
|
||||||
|
SpeedBase: d.Number("Velocity"),
|
||||||
|
SpeedRun: d.Number("Run"),
|
||||||
|
Rarity: d.Number("Rarity"),
|
||||||
|
LevelNormal: d.Number("Level"),
|
||||||
|
LevelNightmare: d.Number("Level(N)"),
|
||||||
|
LevelHell: d.Number("Level(H)"),
|
||||||
|
SoundKeyNormal: d.String("MonSound"),
|
||||||
|
SoundKeySpecial: d.String("UMonSound"),
|
||||||
|
ThreatLevel: d.Number("threat"),
|
||||||
|
AiDelayNormal: d.Number("aidel"),
|
||||||
|
AiDelayNightmare: d.Number("aidel(N)"),
|
||||||
|
AiDelayHell: d.Number("aidel(H)"),
|
||||||
|
AiDistanceNormal: d.Number("aidist"),
|
||||||
|
AiDistanceNightmare: d.Number("aidist(N)"),
|
||||||
|
AiDistanceHell: d.Number("aidist(H)"),
|
||||||
|
AiParameterNormal1: d.Number("aip1"),
|
||||||
|
AiParameterNormal2: d.Number("aip2"),
|
||||||
|
AiParameterNormal3: d.Number("aip3"),
|
||||||
|
AiParameterNormal4: d.Number("aip4"),
|
||||||
|
AiParameterNormal5: d.Number("aip5"),
|
||||||
|
AiParameterNormal6: d.Number("aip6"),
|
||||||
|
AiParameterNormal7: d.Number("aip7"),
|
||||||
|
AiParameterNormal8: d.Number("aip8"),
|
||||||
|
AiParameterNightmare1: d.Number("aip1(N)"),
|
||||||
|
AiParameterNightmare2: d.Number("aip2(N)"),
|
||||||
|
AiParameterNightmare3: d.Number("aip3(N)"),
|
||||||
|
AiParameterNightmare4: d.Number("aip4(N)"),
|
||||||
|
AiParameterNightmare5: d.Number("aip5(N)"),
|
||||||
|
AiParameterNightmare6: d.Number("aip6(N)"),
|
||||||
|
AiParameterNightmare7: d.Number("aip7(N)"),
|
||||||
|
AiParameterNightmare8: d.Number("aip8(N)"),
|
||||||
|
AiParameterHell1: d.Number("aip1(H)"),
|
||||||
|
AiParameterHell2: d.Number("aip2(H)"),
|
||||||
|
AiParameterHell3: d.Number("aip3(H)"),
|
||||||
|
AiParameterHell4: d.Number("aip4(H)"),
|
||||||
|
AiParameterHell5: d.Number("aip5(H)"),
|
||||||
|
AiParameterHell6: d.Number("aip6(H)"),
|
||||||
|
AiParameterHell7: d.Number("aip7(H)"),
|
||||||
|
AiParameterHell8: d.Number("aip8(H)"),
|
||||||
|
MissileA1: d.String("MissA1"),
|
||||||
|
MissileA2: d.String("MissA2"),
|
||||||
|
MissileS1: d.String("MissS1"),
|
||||||
|
MissileS2: d.String("MissS2"),
|
||||||
|
MissileS3: d.String("MissS3"),
|
||||||
|
MissileS4: d.String("MissS4"),
|
||||||
|
MissileC: d.String("MissC"),
|
||||||
|
MissileSQ: d.String("MissSQ"),
|
||||||
|
Alignment: d2enum.MonsterAlignmentType(d.Number("Align")),
|
||||||
|
IsLevelSpawnable: d.Number("isSpawn") > 0,
|
||||||
|
IsMelee: d.Number("isMelee") > 0,
|
||||||
|
IsNpc: d.Number("npc") > 0,
|
||||||
|
IsInteractable: d.Number("interact") > 0,
|
||||||
|
HasInventory: d.Number("inventory") > 0,
|
||||||
|
CanEnterTown: d.Number("inTown") > 0,
|
||||||
|
IsUndeadLow: d.Number("lUndead") > 0,
|
||||||
|
IsUndeadHigh: d.Number("hUndead") > 0,
|
||||||
|
IsDemon: d.Number("demon") > 0,
|
||||||
|
IsFlying: d.Number("flying") > 0,
|
||||||
|
CanOpenDoors: d.Number("opendoors") > 0,
|
||||||
|
IsSpecialBoss: d.Number("boss") > 0,
|
||||||
|
IsActBoss: d.Number("primeevil") > 0,
|
||||||
|
IsKillable: d.Number("killable") > 0,
|
||||||
|
IsAiSwitchable: d.Number("switchai") > 0,
|
||||||
|
DisableAura: d.Number("noAura") > 0,
|
||||||
|
DisableMultiShot: d.Number("nomultishot") > 0,
|
||||||
|
DisableCounting: d.Number("neverCount") > 0,
|
||||||
|
IgnorePets: d.Number("petIgnore") > 0,
|
||||||
|
DealsDamageOnDeath: d.Number("deathDmg") > 0,
|
||||||
|
GenericSpawn: d.Number("genericSpawn") > 0,
|
||||||
|
SkillId1: d.String("Skill1"),
|
||||||
|
SkillId2: d.String("Skill2"),
|
||||||
|
SkillId3: d.String("Skill3"),
|
||||||
|
SkillId4: d.String("Skill4"),
|
||||||
|
SkillId5: d.String("Skill5"),
|
||||||
|
SkillId6: d.String("Skill6"),
|
||||||
|
SkillId7: d.String("Skill7"),
|
||||||
|
SkillId8: d.String("Skill8"),
|
||||||
|
SkillAnimation1: d.String("Sk1mode"),
|
||||||
|
SkillAnimation2: d.String("Sk2mode"),
|
||||||
|
SkillAnimation3: d.String("Sk3mode"),
|
||||||
|
SkillAnimation4: d.String("Sk4mode"),
|
||||||
|
SkillAnimation5: d.String("Sk5mode"),
|
||||||
|
SkillAnimation6: d.String("Sk6mode"),
|
||||||
|
SkillAnimation7: d.String("Sk7mode"),
|
||||||
|
SkillAnimation8: d.String("Sk8mode"),
|
||||||
|
SkillLevel1: d.Number("Sk1lvl"),
|
||||||
|
SkillLevel2: d.Number("Sk2lvl"),
|
||||||
|
SkillLevel3: d.Number("Sk3lvl"),
|
||||||
|
SkillLevel4: d.Number("Sk4lvl"),
|
||||||
|
SkillLevel5: d.Number("Sk5lvl"),
|
||||||
|
SkillLevel6: d.Number("Sk6lvl"),
|
||||||
|
SkillLevel7: d.Number("Sk7lvl"),
|
||||||
|
SkillLevel8: d.Number("Sk8lvl"),
|
||||||
|
LeechSensitivityNormal: d.Number("Drain"),
|
||||||
|
LeechSensitivityNightmare: d.Number("Drain(N)"),
|
||||||
|
LeechSensitivityHell: d.Number("Drain(H)"),
|
||||||
|
ColdSensitivityNormal: d.Number("coldeffect"),
|
||||||
|
ColdSensitivityNightmare: d.Number("coldeffect(N)"),
|
||||||
|
ColdSensitivityHell: d.Number("coldeffect(H)"),
|
||||||
|
ResistancePhysicalNormal: d.Number("ResDm"),
|
||||||
|
ResistancePhysicalNightmare: d.Number("ResDm(N)"),
|
||||||
|
ResistancePhysicalHell: d.Number("ResDm(H)"),
|
||||||
|
ResistanceMagicNormal: d.Number("ResMa"),
|
||||||
|
ResistanceMagicNightmare: d.Number("ResMa(N)"),
|
||||||
|
ResistanceMagicHell: d.Number("ResMa(H)"),
|
||||||
|
ResistanceFireNormal: d.Number("ResFi"),
|
||||||
|
ResistanceFireNightmare: d.Number("ResFi(N)"),
|
||||||
|
ResistanceFireHell: d.Number("ResFi(H)"),
|
||||||
|
ResistanceLightningNormal: d.Number("ResLi"),
|
||||||
|
ResistanceLightningNightmare: d.Number("ResLi(N)"),
|
||||||
|
ResistanceLightningHell: d.Number("ResLi(H)"),
|
||||||
|
ResistanceColdNormal: d.Number("ResCo"),
|
||||||
|
ResistanceColdNightmare: d.Number("ResCo(N)"),
|
||||||
|
ResistanceColdHell: d.Number("ResCo(H)"),
|
||||||
|
ResistancePoisonNormal: d.Number("ResPo"),
|
||||||
|
ResistancePoisonNightmare: d.Number("ResPo(N)"),
|
||||||
|
ResistancePoisonHell: d.Number("ResPo(H)"),
|
||||||
|
HealthRegenPerFrame: d.Number("DamageRegen"),
|
||||||
|
DamageSkillId: d.String("SkillDamage"),
|
||||||
|
IgnoreMonLevelTxt: d.Number("noRatio") > 0,
|
||||||
|
CanBlockWithoutShield: d.Number("NoShldBlock") > 0,
|
||||||
|
ChanceToBlockNormal: d.Number("ToBlock"),
|
||||||
|
ChanceToBlockNightmare: d.Number("ToBlock(N)"),
|
||||||
|
ChanceToBlockHell: d.Number("ToBlock(H)"),
|
||||||
|
ChanceDeadlyStrike: d.Number("Crit"),
|
||||||
|
MinHPNormal: d.Number("minHP"),
|
||||||
|
MinHPNightmare: d.Number("MinHP(N)"),
|
||||||
|
MinHPHell: d.Number("MinHP(H)"),
|
||||||
|
MaxHPNormal: d.Number("maxHP"),
|
||||||
|
MaxHPNightmare: d.Number("MaxHP(N)"),
|
||||||
|
MaxHPHell: d.Number("MaxHP(H)"),
|
||||||
|
ArmorClassNormal: d.Number("AC"),
|
||||||
|
ArmorClassNightmare: d.Number("AC(N)"),
|
||||||
|
ArmorClassHell: d.Number("AC(H)"),
|
||||||
|
ExperienceNormal: d.Number("Exp"),
|
||||||
|
ExperienceNightmare: d.Number("Exp(N)"),
|
||||||
|
ExperienceHell: d.Number("Exp(H)"),
|
||||||
|
DamageMinA1Normal: d.Number("A1MinD"),
|
||||||
|
DamageMinA1Nightmare: d.Number("A1MinD(N)"),
|
||||||
|
DamageMinA1Hell: d.Number("A1MinD(H)"),
|
||||||
|
DamageMaxA1Normal: d.Number("A1MaxD"),
|
||||||
|
DamageMaxA1Nightmare: d.Number("A1MaxD(N)"),
|
||||||
|
DamageMaxA1Hell: d.Number("A1MaxD(H)"),
|
||||||
|
DamageMinA2Normal: d.Number("A2MinD"),
|
||||||
|
DamageMinA2Nightmare: d.Number("A2MinD(N)"),
|
||||||
|
DamageMinA2Hell: d.Number("A2MinD(H)"),
|
||||||
|
DamageMaxA2Normal: d.Number("A2MaxD"),
|
||||||
|
DamageMaxA2Nightmare: d.Number("A2MaxD(N)"),
|
||||||
|
DamageMaxA2Hell: d.Number("A2MaxD(H)"),
|
||||||
|
DamageMinS1Normal: d.Number("S1MinD"),
|
||||||
|
DamageMinS1Nightmare: d.Number("S1MinD(N)"),
|
||||||
|
DamageMinS1Hell: d.Number("S1MinD(H)"),
|
||||||
|
DamageMaxS1Normal: d.Number("S1MaxD"),
|
||||||
|
DamageMaxS1Nightmare: d.Number("S1MaxD(N)"),
|
||||||
|
DamageMaxS1Hell: d.Number("S1MaxD(H)"),
|
||||||
|
AttackRatingA1Normal: d.Number("A1TH"),
|
||||||
|
AttackRatingA1Nightmare: d.Number("A1TH(N)"),
|
||||||
|
AttackRatingA1Hell: d.Number("A1TH(H)"),
|
||||||
|
AttackRatingA2Normal: d.Number("A2TH"),
|
||||||
|
AttackRatingA2Nightmare: d.Number("A2TH(N)"),
|
||||||
|
AttackRatingA2Hell: d.Number("A2TH(H)"),
|
||||||
|
AttackRatingS1Normal: d.Number("S1TH"),
|
||||||
|
AttackRatingS1Nightmare: d.Number("S1TH(N)"),
|
||||||
|
AttackRatingS1Hell: d.Number("S1TH(H)"),
|
||||||
|
ElementAttackMode1: d.String("El1Mode"),
|
||||||
|
ElementAttackMode2: d.String("El2Mode"),
|
||||||
|
ElementAttackMode3: d.String("El3Mode"),
|
||||||
|
ElementType1: d.String("El1Type"),
|
||||||
|
ElementType2: d.String("El2Type"),
|
||||||
|
ElementType3: d.String("El3Type"),
|
||||||
|
ElementChance1Normal: d.Number("El1Pct"),
|
||||||
|
ElementChance1Nightmare: d.Number("El1Pct(N)"),
|
||||||
|
ElementChance1Hell: d.Number("El1Pct(H)"),
|
||||||
|
ElementChance2Normal: d.Number("El2Pct"),
|
||||||
|
ElementChance2Nightmare: d.Number("El2Pct(N)"),
|
||||||
|
ElementChance2Hell: d.Number("El2Pct(H)"),
|
||||||
|
ElementChance3Normal: d.Number("El3Pct"),
|
||||||
|
ElementChance3Nightmare: d.Number("El3Pct(N)"),
|
||||||
|
ElementChance3Hell: d.Number("El3Pct(H)"),
|
||||||
|
ElementDamageMin1Normal: d.Number("El1MinD"),
|
||||||
|
ElementDamageMin1Nightmare: d.Number("El1MinD(N)"),
|
||||||
|
ElementDamageMin1Hell: d.Number("El1MinD(H)"),
|
||||||
|
ElementDamageMin2Normal: d.Number("El2MinD"),
|
||||||
|
ElementDamageMin2Nightmare: d.Number("El2MinD(N)"),
|
||||||
|
ElementDamageMin2Hell: d.Number("El2MinD(H)"),
|
||||||
|
ElementDamageMin3Normal: d.Number("El3MinD"),
|
||||||
|
ElementDamageMin3Nightmare: d.Number("El3MinD(N)"),
|
||||||
|
ElementDamageMin3Hell: d.Number("El3MinD(H)"),
|
||||||
|
ElementDamageMax1Normal: d.Number("El1MaxD"),
|
||||||
|
ElementDamageMax1Nightmare: d.Number("El1MaxD(N)"),
|
||||||
|
ElementDamageMax1Hell: d.Number("El1MaxD(H)"),
|
||||||
|
ElementDamageMax2Normal: d.Number("El2MaxD"),
|
||||||
|
ElementDamageMax2Nightmare: d.Number("El2MaxD(N)"),
|
||||||
|
ElementDamageMax2Hell: d.Number("El2MaxD(H)"),
|
||||||
|
ElementDamageMax3Normal: d.Number("El3MaxD"),
|
||||||
|
ElementDamageMax3Nightmare: d.Number("El3MaxD(N)"),
|
||||||
|
ElementDamageMax3Hell: d.Number("El3MaxD(H)"),
|
||||||
|
ElementDuration1Normal: d.Number("El1Dur"),
|
||||||
|
ElementDuration1Nightmare: d.Number("El1Dur(N)"),
|
||||||
|
ElementDuration1Hell: d.Number("El1Dur(H)"),
|
||||||
|
ElementDuration2Normal: d.Number("El2Dur"),
|
||||||
|
ElementDuration2Nightmare: d.Number("El2Dur(N)"),
|
||||||
|
ElementDuration2Hell: d.Number("El2Dur(H)"),
|
||||||
|
ElementDuration3Normal: d.Number("El3Dur"),
|
||||||
|
ElementDuration3Nightmare: d.Number("El3Dur(N)"),
|
||||||
|
ElementDuration3Hell: d.Number("El3Dur(H)"),
|
||||||
|
TreasureClassNormal: d.String("TreasureClass1"),
|
||||||
|
TreasureClassNightmare: d.String("TreasureClass1(N)"),
|
||||||
|
TreasureClassHell: d.String("TreasureClass1(H)"),
|
||||||
|
TreasureClassChampionNormal: d.String("TreasureClass2"),
|
||||||
|
TreasureClassChampionNightmare: d.String("TreasureClass2(N)"),
|
||||||
|
TreasureClassChampionHell: d.String("TreasureClass2(H)"),
|
||||||
|
TreasureClass3UniqueNormal: d.String("TreasureClass3"),
|
||||||
|
TreasureClass3UniqueNightmare: d.String("TreasureClass3(N)"),
|
||||||
|
TreasureClass3UniqueHell: d.String("TreasureClass3(H)"),
|
||||||
|
TreasureClassQuestNormal: d.String("TreasureClass4"),
|
||||||
|
TreasureClassQuestNightmare: d.String("TreasureClass4(N)"),
|
||||||
|
TreasureClassQuestHell: d.String("TreasureClass4(H)"),
|
||||||
|
TreasureClassQuestTriggerId: d.String("TCQuestId"),
|
||||||
|
TreasureClassQuestCompleteId: d.String("TCQuestCP"),
|
||||||
|
SpecialEndDeath: d.Number("SplEndDeath"),
|
||||||
|
SpecialGetModeChart: d.Number("SplGetModeChart") > 0,
|
||||||
|
SpecialEndGeneric: d.Number("SplEndGeneric") > 0,
|
||||||
|
SpecialClientEnd: d.Number("SplClientEnd") > 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
records[record.Key] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d MonStats records", len(records))
|
||||||
|
|
||||||
|
r.Monster.Stats = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,678 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
|
||||||
|
// https://d2mods.info/forum/kb/viewarticle?a=360
|
||||||
|
|
||||||
|
// MonStats stores all of the MonStat Records
|
||||||
|
type MonStats map[string]*MonStatsRecord
|
||||||
|
|
||||||
|
type (
|
||||||
|
// MonStatsRecord represents a single row from `data/global/excel/monstats.txt` in the MPQ files.
|
||||||
|
// These records are used for creating monsters.
|
||||||
|
MonStatsRecord struct {
|
||||||
|
|
||||||
|
// Key contains the pointer that will be used in other txt files
|
||||||
|
// such as levels.txt and superuniques.txt.
|
||||||
|
Key string // called `Id` in monstats.txt
|
||||||
|
|
||||||
|
// Id is the actual internal ID of the unit (this is what the ID pointer
|
||||||
|
// actually points at) remember that no two units can have the same ID,
|
||||||
|
// this will result in lots of unpredictable behavior and crashes so please
|
||||||
|
// don’t do it. This 'HarcCodedInDeX' is used for several things, such as
|
||||||
|
// determining whenever the unit uses DCC or DC6 graphics (like mephisto
|
||||||
|
// and the death animations of Diablo, the Maggoc Queen etc.), the hcIdx
|
||||||
|
// column also links other hardcoded effects to the units, such as the
|
||||||
|
// transparency on necro summons and the name-color change on unique boss
|
||||||
|
// units (thanks to Kingpin for the info)
|
||||||
|
ID int // called `hcIdx` in monstats.txt
|
||||||
|
|
||||||
|
// BaseKey is an ID pointer of the “base” unit for this specific
|
||||||
|
// monster type (ex. There are five types of “Fallen”; all of them have
|
||||||
|
// fallen1 as their “base” unit).
|
||||||
|
BaseKey string // called `BaseId` in monstats.txt
|
||||||
|
|
||||||
|
// NextKey is the ID of the next unit in the chain. (fallen1 has the ID pointer of fallen2 in here).
|
||||||
|
// The game uses this for “map generated” monsters such as the fallen in the fallen camps,
|
||||||
|
// which get picked based on area level.
|
||||||
|
NextKey string // called `NextInClass` in monstats.txt
|
||||||
|
|
||||||
|
// NameStringTableKey the string-key used in the TBL (string.tbl,
|
||||||
|
// expansionstring.tbl and patchstring.tbl) files to make this monsters
|
||||||
|
// name appear when you highlight it.
|
||||||
|
NameString string // called `NameStr` in monstats.txt
|
||||||
|
|
||||||
|
// ExtraDataKey the ID pointer to an entry in MonStats2.txt.
|
||||||
|
ExtraDataKey string // called `MonStatsEx` in monstats.txt
|
||||||
|
|
||||||
|
// PropertiesKey contains the ID pointer to an entry in MonProp.txt which
|
||||||
|
// controls what special modifiers are appended to the unit
|
||||||
|
PropertiesKey string // called `MonProp` in monstats.txt
|
||||||
|
|
||||||
|
// MonsterGroup contains the group ID of the “super group” this monster
|
||||||
|
// belongs to, IE all skeletons belong to the "super group" skeleton. The
|
||||||
|
MonsterGroup string // called `MonType` in monstats.txt
|
||||||
|
|
||||||
|
// AiKey tells the game which AI to use for this monster. Every AI
|
||||||
|
// needs a specific set of animation modes (GH, A1, A2, S1, WL, RN etc).
|
||||||
|
AiKey string // called `AI` in monstats.txt
|
||||||
|
|
||||||
|
// DescriptionStringTableKey contains the string-key used in the TBL (string.tbl,
|
||||||
|
// expansionstring.tbl and patchstring.tbl) files for the monsters
|
||||||
|
// description (leave it blank for no description).
|
||||||
|
// NOTE: ever wondered how to make it say something below the monster
|
||||||
|
// name (such as “Drains Mana and Stamina etc), well this is how you do it.
|
||||||
|
// Just put the string-key of the string you want to display below the
|
||||||
|
// monsters name in here.
|
||||||
|
DescriptionStringTableKey string // called `DescStr` in monstats.txt
|
||||||
|
|
||||||
|
// AnimationDirectoryToken controls which token (IE name of a folder that
|
||||||
|
// contains animations) the game uses for this monster.
|
||||||
|
AnimationDirectoryToken string // called `Code` in monstats.txt
|
||||||
|
|
||||||
|
// SpawnKey contains the key of the unit to spawn.
|
||||||
|
SpawnKey string // called `spawn` in monstats.txt
|
||||||
|
|
||||||
|
// SpawnAnimationKey
|
||||||
|
// which animation mode will the spawned monster be spawned in.
|
||||||
|
SpawnAnimationKey string // called `spawnmode` in monstats.txt
|
||||||
|
|
||||||
|
// MinionId1 is an Id of a minion that spawns when this monster is created
|
||||||
|
MinionId1 string //nolint:golint,stylecheck // called `minion1` in monstats.txt
|
||||||
|
|
||||||
|
// MinionId2 is an Id of a minion that spawns when this monster is created
|
||||||
|
MinionId2 string //nolint:golint,stylecheck // called `minion2` in monstats.txt
|
||||||
|
|
||||||
|
// SoundKeyNormal, SoundKeySpecial
|
||||||
|
// specifies the ID pointer to this monsters “Sound Bank” in MonSound.txt
|
||||||
|
// when this monster is normal.
|
||||||
|
SoundKeyNormal string // called `MonSound` in monstats.txt
|
||||||
|
SoundKeySpecial string // called `UMonSound` in monstats.txt
|
||||||
|
|
||||||
|
// MissileA1 -- MissileSQ
|
||||||
|
// these columns control “non-skill-related” missiles used by the monster.
|
||||||
|
// For example if you enter a missile ID pointer (from Missiles.txt) in
|
||||||
|
// MissA1 then, whenever the monster uses its A1 mode, it will shoot a
|
||||||
|
// missile, this however will successfully prevent it from dealing any damage
|
||||||
|
// with the swing of A1.
|
||||||
|
// NOTE: for the beginners, A1=Attack1, A2=Attack2, S1=Skill1, S2=Skill2,
|
||||||
|
// S3=Skill3, S4=Skill4, C=Cast, SQ=Sequence.
|
||||||
|
MissileA1 string // called `MissA1` in monstats.txt
|
||||||
|
MissileA2 string // called `MissA2` in monstats.txt
|
||||||
|
MissileS1 string // called `MissS1` in monstats.txt
|
||||||
|
MissileS2 string // called `MissS2` in monstats.txt
|
||||||
|
MissileS3 string // called `MissS3` in monstats.txt
|
||||||
|
MissileS4 string // called `MissS4` in monstats.txt
|
||||||
|
MissileC string // called `MissC` in monstats.txt
|
||||||
|
MissileSQ string // called `MissSQ` in monstats.txt
|
||||||
|
|
||||||
|
// SkillId1 -- SkillId8
|
||||||
|
// the ID Pointer to the skill (from Skills.txt) the monster will cast when
|
||||||
|
// this specific slot is accessed by the AI. Which slots are used is
|
||||||
|
// determined by the units AI.
|
||||||
|
SkillId1 string //nolint:golint,stylecheck // called `Skill1` in monstats.txt
|
||||||
|
SkillId2 string //nolint:golint,stylecheck // called `Skill2` in monstats.txt
|
||||||
|
SkillId3 string //nolint:golint,stylecheck // called `Skill3` in monstats.txt
|
||||||
|
SkillId4 string //nolint:golint,stylecheck // called `Skill4` in monstats.txt
|
||||||
|
SkillId5 string //nolint:golint,stylecheck // called `Skill5` in monstats.txt
|
||||||
|
SkillId6 string //nolint:golint,stylecheck // called `Skill6` in monstats.txt
|
||||||
|
SkillId7 string //nolint:golint,stylecheck // called `Skill7` in monstats.txt
|
||||||
|
SkillId8 string //nolint:golint,stylecheck // called `Skill8` in monstats.txt
|
||||||
|
|
||||||
|
// SkillAnimation1 -- SkillAnimation8
|
||||||
|
// the graphical MODE (or SEQUENCE) this unit uses when it uses this skill.
|
||||||
|
SkillAnimation1 string // called `Sk1mode` in monstats.txt
|
||||||
|
SkillAnimation2 string // called `Sk2mode` in monstats.txt
|
||||||
|
SkillAnimation3 string // called `Sk3mode` in monstats.txt
|
||||||
|
SkillAnimation4 string // called `Sk4mode` in monstats.txt
|
||||||
|
SkillAnimation5 string // called `Sk5mode` in monstats.txt
|
||||||
|
SkillAnimation6 string // called `Sk6mode` in monstats.txt
|
||||||
|
SkillAnimation7 string // called `Sk7mode` in monstats.txt
|
||||||
|
SkillAnimation8 string // called `Sk8mode` in monstats.txt
|
||||||
|
|
||||||
|
// DamageSkillId
|
||||||
|
// ID Pointer to the skill that controls this units damage. This is used for
|
||||||
|
// the druids summons. IE their damage is specified solely by Skills.txt and
|
||||||
|
// not by MonStats.txt.
|
||||||
|
DamageSkillId string //nolint:golint,stylecheck // called `SkillDamage` in monstats.txt
|
||||||
|
|
||||||
|
// ElementAttackMode1 -- ElementAttackMode3
|
||||||
|
// the mode to which the elemental damage is appended. The modes to which
|
||||||
|
// you would usually attack elemental damage are A1, A2, S1, S2, S3, S4, SQ
|
||||||
|
// or C as these are the only ones that naturally contain trigger bytes.
|
||||||
|
ElementAttackMode1 string // called `El1Mode` in monstats.txt
|
||||||
|
ElementAttackMode2 string // called `El2Mode` in monstats.txt
|
||||||
|
ElementAttackMode3 string // called `El3Mode` in monstats.txt
|
||||||
|
|
||||||
|
// ElementType1 -- ElementType3
|
||||||
|
// the type of the elemental damage appended to an attack. There are several
|
||||||
|
// elements: fire=Fire Damage, ltng=Lightning Damage, cold=Cold Damage
|
||||||
|
// (uses duration), pois = Poison Damage (uses duration), mag=Magic Damage,
|
||||||
|
// life=Life Drain (the monster heals the specified amount when it hits
|
||||||
|
// you), mana=Mana Drain (the monster steals the specified amount of mana
|
||||||
|
// when it hits you), stam=Stamina Drain (the monster steals the specified
|
||||||
|
// amount of stamina when it hits you), stun=Stun Damage (uses duration,
|
||||||
|
// damage is not used, this only effects pets and mercs, players will not
|
||||||
|
// get immobilized but they will get thrown into hit recovery whenever they
|
||||||
|
// get hit by an attack, no matter what type of attack it is, thanks to
|
||||||
|
// Brother Laz clearing this one up), rand=Random Damage (uses duration,
|
||||||
|
// either does Poison, Cold, Fire or Lightning damage, randomly picked for
|
||||||
|
// every attack), burn=Burning Damage (uses duration, this damage type
|
||||||
|
// cannot be resisted or reduced in any way), frze=Freezing Damage (uses
|
||||||
|
// duration, this will effect players like normal cold damage but will
|
||||||
|
// freeze and shatter pets). If you want to give your monster knockback use
|
||||||
|
// MonProp.txt.
|
||||||
|
ElementType1 string // called `El1Type` in monstats.txt
|
||||||
|
ElementType2 string // called `El2Type` in monstats.txt
|
||||||
|
ElementType3 string // called `El3Type` in monstats.txt
|
||||||
|
|
||||||
|
// TreasureClassNormal
|
||||||
|
// Treasure class for normal monsters, champions, uniques, and quests
|
||||||
|
// on the respective difficulties.
|
||||||
|
TreasureClassNormal string // called `TreasureClass1` in monstats.txt
|
||||||
|
TreasureClassNightmare string // called `TreasureClass1(N)` in monstats.txt
|
||||||
|
TreasureClassHell string // called `TreasureClass1(H)` in monstats.txt
|
||||||
|
TreasureClassChampionNormal string // called `TreasureClass2` in monstats.txt
|
||||||
|
TreasureClassChampionNightmare string // called `TreasureClass2(N)` in monstats.txt
|
||||||
|
TreasureClassChampionHell string // called `TreasureClass2(H)` in monstats.txt
|
||||||
|
TreasureClass3UniqueNormal string // called `TreasureClass3` in monstats.txt
|
||||||
|
TreasureClass3UniqueNightmare string // called `TreasureClass3(N)` in monstats.txt
|
||||||
|
TreasureClass3UniqueHell string // called `TreasureClass3(H)` in monstats.txt
|
||||||
|
TreasureClassQuestNormal string // called `TreasureClass4` in monstats.txt
|
||||||
|
TreasureClassQuestNightmare string // called `TreasureClass4(N)` in monstats.txt
|
||||||
|
TreasureClassQuestHell string // called `TreasureClass4(H)` in monstats.txt
|
||||||
|
|
||||||
|
// TreasureClassQuestTriggerId
|
||||||
|
// the ID of the Quest that triggers the Quest Treasureclass drop.
|
||||||
|
TreasureClassQuestTriggerId string //nolint:golint,stylecheck // called `TCQuestId` in monstats.txt
|
||||||
|
|
||||||
|
// TreasureClassQuestCompleteId
|
||||||
|
// the ID of the Quest State that you need to complete to trigger the Quest
|
||||||
|
// Treasureclass trop.
|
||||||
|
TreasureClassQuestCompleteId string //nolint:golint,stylecheck // called `TCQuestCP` in monstats.txt
|
||||||
|
|
||||||
|
// PaletteId indicates which palette (color) entry the unit will use, most
|
||||||
|
// monsters have a palshift.dat file in their COF folder, this file
|
||||||
|
// contains 8 palettes, starting from index 0. These palettes are used by
|
||||||
|
// the game to make the various monster sub-types appear with color
|
||||||
|
// variations. The game with use the palette from the palettes file
|
||||||
|
// corresponding to the value in this column plus 2; eg: translvl = 0 will
|
||||||
|
// use the third palette in the file.
|
||||||
|
// NOTE: some tokens (token = IE name of a folder that contains animations)
|
||||||
|
// such as FC do not accept their palettes.
|
||||||
|
// NOTE no 2: some monsters got unused palettes, ZM (zombie) for example
|
||||||
|
// will turn light-rotten-green with palette nr 5 and pink-creamy with 6.
|
||||||
|
PaletteId int //nolint:golint,stylecheck // called `TransLvl` in monstats.txt
|
||||||
|
|
||||||
|
// SpawnOffsetX, SpawnOffsetY
|
||||||
|
// are the x/y offsets at which spawned monsters are placed. IE this prevents
|
||||||
|
// the spawned monsters from being created at the same x/y coordinates as
|
||||||
|
// the spawner itself.
|
||||||
|
SpawnOffsetX int // called `spawnx` in monstats.txt
|
||||||
|
SpawnOffsetY int // called `spawny` in monstats.txt
|
||||||
|
|
||||||
|
// MinionPartyMin, MinionPartyMax controls how many minions are spawned together with this unit.
|
||||||
|
MinionPartyMin int // called `PartyMin` in monstats.txt
|
||||||
|
MinionPartyMax int // called `PartyMax` in monstats.txt
|
||||||
|
|
||||||
|
// MinionGroupMin, MinionGroupMax
|
||||||
|
// controls how many units of the base unit to spawn.
|
||||||
|
MinionGroupMin int // called `MinGrp` in monstats.txt
|
||||||
|
MinionGroupMax int // called `MaxGrp` in monstats.txt
|
||||||
|
|
||||||
|
// PopulationReductionPercent controls the overall chance something will spawn in
|
||||||
|
// percentages. Blank entries are the same as 100%.
|
||||||
|
PopulationReductionPercent int // called `sparsePopulate` in monstats.txt
|
||||||
|
|
||||||
|
// SpeedBase, SpeedRun
|
||||||
|
// controls the walking and running speed of this monster respectively.
|
||||||
|
// NOTE: RUN is only used if the monster has a RN mode and its AI uses that
|
||||||
|
// mode.
|
||||||
|
SpeedBase int // called `Velocity` in monstats.txt
|
||||||
|
SpeedRun int // called `Run` in monstats.txt
|
||||||
|
|
||||||
|
// Rarity controls the overall odds that this monster will be spawned.
|
||||||
|
// IE Lets say in Levels.txt you have two monsters set to spawn - Monster A
|
||||||
|
// has rarity of 10 whereas Monster B has rarity of 1 and the level in
|
||||||
|
// question is limited to 1 monster type. First the game sums up the
|
||||||
|
// chances (11) and then calculates the odds of the monster spawning. Which
|
||||||
|
// would be 1/11 (9% chance) for Monster B and 10/11 (91% chance) for
|
||||||
|
// Monster A, thus Monster A is a lot more common than monster B. If you set
|
||||||
|
// this column to 0 then the monster will never be selected by Levels.txt
|
||||||
|
// for obvious reasons.
|
||||||
|
Rarity int // called `Rarity` in monstats.txt
|
||||||
|
|
||||||
|
// LevelNormal, LevelNightmare, LevelHell
|
||||||
|
// controls the monsters level on the specified difficulty. This setting is
|
||||||
|
// only used on normal. On nightmare and hell the monsters level is
|
||||||
|
// identical with the area level from Levels.txt, unless your monster has
|
||||||
|
// BOSS column set to 1, in this case its level will be always taken from
|
||||||
|
// these 3 columns.
|
||||||
|
LevelNormal int // called `Level` in monstats.txt
|
||||||
|
LevelNightmare int // called `Level(N)` in monstats.txt
|
||||||
|
LevelHell int // called `Level(H)` in monstats.txt
|
||||||
|
|
||||||
|
// used by the game to tell AIs which unit to target first. The higher this
|
||||||
|
// is the higher the threat level. Setting this to 25 or so on Maggot Eggs
|
||||||
|
// would make your Mercenary NPC try to destroy those first.
|
||||||
|
ThreatLevel int // called `threat` in monstats.txt
|
||||||
|
|
||||||
|
// AiDelayNormal, AiDelayNightmare, AiDelayHell
|
||||||
|
// this controls delays between AI ticks (on normal, nightmare and hell).
|
||||||
|
// The lower the number, the faster the AI's will attack thanks to reduced
|
||||||
|
// delay between swings, casting spells, throwing missiles etc. Please
|
||||||
|
// remember that some AI's got individual delays between attacks, this will
|
||||||
|
// still make them faster and seemingly more deadly though.
|
||||||
|
AiDelayNormal int // called `aidel` in monstats.txt
|
||||||
|
AiDelayNightmare int // called `aidel(N)` in monstats.txt
|
||||||
|
AiDelayHell int // called `aidel(H)` in monstats.txt
|
||||||
|
|
||||||
|
// AiDistanceNormal, AiDistanceNightmare, AiDistanceHell
|
||||||
|
// the distance in cells from which AI is activated. Most AI"s have base
|
||||||
|
// hardcoded activation radius of 35 which stands for a distamnce of about
|
||||||
|
// 1 screen, thus leaving these fields blank sets this to 35 automatically.
|
||||||
|
AiDistanceNormal int // called `aidist` in monstats.txt
|
||||||
|
AiDistanceNightmare int // called `aidist(N)` in monstats.txt
|
||||||
|
AiDistanceHell int // called `aidist(H)` in monstats.txt
|
||||||
|
|
||||||
|
// AiParameterNormal1, AiParameterNightmare1, AiParameterHell1
|
||||||
|
// these cells are very important, they pass on parameters (in percentage)
|
||||||
|
// to the AI code. For descriptions about what all these AI's do, check
|
||||||
|
// The AI Compendium. https://d2mods.info/forum/viewtopic.php?t=36230
|
||||||
|
// Warning: many people have trouble with the AI of the Imps, this AI is
|
||||||
|
// special and uses multiple rows.
|
||||||
|
AiParameterNormal1 int // called `aip1` in monstats.txt
|
||||||
|
AiParameterNormal2 int // called `aip2` in monstats.txt
|
||||||
|
AiParameterNormal3 int // called `aip3` in monstats.txt
|
||||||
|
AiParameterNormal4 int // called `aip4` in monstats.txt
|
||||||
|
AiParameterNormal5 int // called `aip5` in monstats.txt
|
||||||
|
AiParameterNormal6 int // called `aip6` in monstats.txt
|
||||||
|
AiParameterNormal7 int // called `aip7` in monstats.txt
|
||||||
|
AiParameterNormal8 int // called `aip8` in monstats.txt
|
||||||
|
AiParameterNightmare1 int // called `aip1(N)` in monstats.txt
|
||||||
|
AiParameterNightmare2 int // called `aip2(N)` in monstats.txt
|
||||||
|
AiParameterNightmare3 int // called `aip3(N)` in monstats.txt
|
||||||
|
AiParameterNightmare4 int // called `aip4(N)` in monstats.txt
|
||||||
|
AiParameterNightmare5 int // called `aip5(N)` in monstats.txt
|
||||||
|
AiParameterNightmare6 int // called `aip6(N)` in monstats.txt
|
||||||
|
AiParameterNightmare7 int // called `aip7(N)` in monstats.txt
|
||||||
|
AiParameterNightmare8 int // called `aip8(N)` in monstats.txt
|
||||||
|
AiParameterHell1 int // called `aip1(H)` in monstats.txt
|
||||||
|
AiParameterHell2 int // called `aip2(H)` in monstats.txt
|
||||||
|
AiParameterHell3 int // called `aip3(H)` in monstats.txt
|
||||||
|
AiParameterHell4 int // called `aip4(H)` in monstats.txt
|
||||||
|
AiParameterHell5 int // called `aip5(H)` in monstats.txt
|
||||||
|
AiParameterHell6 int // called `aip6(H)` in monstats.txt
|
||||||
|
AiParameterHell7 int // called `aip7(H)` in monstats.txt
|
||||||
|
AiParameterHell8 int // called `aip8(H)` in monstats.txt
|
||||||
|
|
||||||
|
// Alignment controls whenever the monster fights on your side or
|
||||||
|
// fights against you (or if it just walks around, IE a critter).
|
||||||
|
// If you want to turn some obsolete NPCs into enemies, this is
|
||||||
|
// one of the settings you will need to modify. Setting it to 2
|
||||||
|
// without adjusting other settings (related to AI and also some
|
||||||
|
// in MonStats2) it will simply attack everything.
|
||||||
|
Alignment d2enum.MonsterAlignmentType // called `Align` in monstats.txt
|
||||||
|
|
||||||
|
// SkillLevel1 -- SkillLevel8
|
||||||
|
// the skill level of the skill in question. This gets a bonus on nightmare
|
||||||
|
// and hell which you can modify in DifficultyLevels.txt.
|
||||||
|
SkillLevel1 int // called `Sk1lvl` in monstats.txt
|
||||||
|
SkillLevel2 int // called `Sk2lvl` in monstats.txt
|
||||||
|
SkillLevel3 int // called `Sk3lvl` in monstats.txt
|
||||||
|
SkillLevel4 int // called `Sk4lvl` in monstats.txt
|
||||||
|
SkillLevel5 int // called `Sk5lvl` in monstats.txt
|
||||||
|
SkillLevel6 int // called `Sk6lvl` in monstats.txt
|
||||||
|
SkillLevel7 int // called `Sk7lvl` in monstats.txt
|
||||||
|
SkillLevel8 int // called `Sk8lvl` in monstats.txt
|
||||||
|
|
||||||
|
// LeechSensitivityNormal / Nightmare / Hell
|
||||||
|
// controls the effectiveness of Life and Mana steal from equipment on this
|
||||||
|
// unit on the respective difficulties. 0=Can’t leech at all. Remember that
|
||||||
|
// besides this, Life and Mana Steal is further limited by DifficultyLevels.txt.
|
||||||
|
LeechSensitivityNormal int // called `Drain` in monstats.txt
|
||||||
|
LeechSensitivityNightmare int // called `Drain(N)` in monstats.txt
|
||||||
|
LeechSensitivityHell int // called `Drain(H)` in monstats.txt
|
||||||
|
|
||||||
|
// ColdSensitivityNormal / Nightmare / Hell
|
||||||
|
// controls the effectiveness of cold effect and its duration and freeze
|
||||||
|
// duration on this unit. The lower this value is, the more speed this unit
|
||||||
|
// looses when its under the effect of cold, also freezing/cold effect will
|
||||||
|
// stay for longer. Positive values will make the unit faster (thanks to
|
||||||
|
// Brother Laz for confirming my assumption), and 0 will make it
|
||||||
|
// unfreezeable. Besides this, cold length and freeze length settings are
|
||||||
|
// also set in DifficultyLevels.txt.
|
||||||
|
ColdSensitivityNormal int // called `coldeffect` in monstats.txt
|
||||||
|
ColdSensitivityNightmare int // called `coldeffect(N)` in monstats.txt
|
||||||
|
ColdSensitivityHell int // called `coldeffect(H)` in monstats.txt
|
||||||
|
|
||||||
|
// ResistancePhysicalNormal
|
||||||
|
// Damage resistance on the respective difficulties. Negative values mean
|
||||||
|
// that the unit takes more damage from this element, values at or above 100
|
||||||
|
// will result in immunity.
|
||||||
|
ResistancePhysicalNormal int // called `ResDm` in monstats.txt
|
||||||
|
ResistancePhysicalNightmare int // called `ResDm(N)` in monstats.txt
|
||||||
|
ResistancePhysicalHell int // called `ResDm(H)` in monstats.txt
|
||||||
|
ResistanceMagicNormal int // called `ResMa` in monstats.txt
|
||||||
|
ResistanceMagicNightmare int // called `ResMa(N)` in monstats.txt
|
||||||
|
ResistanceMagicHell int // called `ResMa(H)` in monstats.txt
|
||||||
|
ResistanceFireNormal int // called `ResFi` in monstats.txt
|
||||||
|
ResistanceFireNightmare int // called `ResFi(N)` in monstats.txt
|
||||||
|
ResistanceFireHell int // called `ResFi(H)` in monstats.txt
|
||||||
|
ResistanceLightningNormal int // called `ResLi` in monstats.txt
|
||||||
|
ResistanceLightningNightmare int // called `ResLi(N)` in monstats.txt
|
||||||
|
ResistanceLightningHell int // called `ResLi(H)` in monstats.txt
|
||||||
|
ResistanceColdNormal int // called `ResCo` in monstats.txt
|
||||||
|
ResistanceColdNightmare int // called `ResCo(N)` in monstats.txt
|
||||||
|
ResistanceColdHell int // called `ResCo(H)` in monstats.txt
|
||||||
|
ResistancePoisonNormal int // called `ResPo` in monstats.txt
|
||||||
|
ResistancePoisonNightmare int // called `ResPo(N)` in monstats.txt
|
||||||
|
ResistancePoisonHell int // called `ResPo(H)` in monstats.txt
|
||||||
|
|
||||||
|
// HealthRegenPerFrame
|
||||||
|
// this controls how much health this unit regenerates per frame. Sometimes
|
||||||
|
// this is altered by the units AI. The formula is (REGEN * HP) / 4096. So
|
||||||
|
// a monster with 200 hp and a regen rate of 10 would regenerate ~0,5 HP
|
||||||
|
// (~12 per second) every frame (1 second = 25 frames).
|
||||||
|
HealthRegenPerFrame int // called `DamageRegen` in monstats.txt
|
||||||
|
|
||||||
|
// ChanceToBlockNormal / Nightmare / Hell
|
||||||
|
// this units chance to block. See the above column for details when this
|
||||||
|
// applies or not. Monsters are capped at 75% block as players are AFAIK.
|
||||||
|
ChanceToBlockNormal int // called `ToBlock` in monstats.txt
|
||||||
|
ChanceToBlockNightmare int // called `ToBlock(N)` in monstats.txt
|
||||||
|
ChanceToBlockHell int // called `ToBlock(H)` in monstats.txt
|
||||||
|
|
||||||
|
// ChanceDeadlyStrike
|
||||||
|
// this units chance of scoring a critical hit (dealing double the damage).
|
||||||
|
ChanceDeadlyStrike int // called `Crit` in monstats.txt
|
||||||
|
|
||||||
|
// MinHPNormal -- MaxHPHell
|
||||||
|
// minHp, maxHp, minHp(N), maxHp(N), minHp(H), maxHp(H): this units minimum
|
||||||
|
// and maximum HP on the respective difficulties.
|
||||||
|
// NOTE: Monster HitPoints are calculated as the following: (minHp * Hp from
|
||||||
|
// MonLvl.txt)/100 for minimal hp and (maxHp * Hp from MonLvl.txt)/100 for
|
||||||
|
// maximum hp.
|
||||||
|
// To make this guide idiot-proof, we will calculate the hit points of a
|
||||||
|
// Hungry Dead from vanilla on Normal difficulty and Single Player mode.
|
||||||
|
// It has minHp = 101 and maxHp = 186 and level 2. Hp for level 2 in
|
||||||
|
// MonLvl.txt = 9
|
||||||
|
// It means Hungry Dead has (101*9)/100 ~ 9 of minimum hp and
|
||||||
|
// (186*9)/100 ~ 17 maximum hit points. You have to remember monsters on
|
||||||
|
// nightmare and hell take their level (unless Boss = 1) from area level of
|
||||||
|
// Levels.txt instead of Level column of MonStats.txt. I hope this is clear.
|
||||||
|
MinHPNormal int // called `minHP` in monstats.txt
|
||||||
|
MinHPNightmare int // called `MinHP(N)` in monstats.txt
|
||||||
|
MinHPHell int // called `MinHP(H)` in monstats.txt
|
||||||
|
MaxHPNormal int // called `maxHP` in monstats.txt
|
||||||
|
MaxHPNightmare int // called `MaxHP(N)` in monstats.txt
|
||||||
|
MaxHPHell int // called `MaxHP(H)` in monstats.txt
|
||||||
|
|
||||||
|
// ArmorClassNormal -- Hell
|
||||||
|
// this units Armor Class on the respective difficulties. The calculation is
|
||||||
|
// the same (analogical) as for hit points.
|
||||||
|
ArmorClassNormal int // called `AC` in monstats.txt
|
||||||
|
ArmorClassNightmare int // called `AC(N)` in monstats.txt
|
||||||
|
ArmorClassHell int // called `AC(H)` in monstats.txt
|
||||||
|
|
||||||
|
// ExperienceNormal -- Hell
|
||||||
|
// the experience you get when killing this unit on the respective
|
||||||
|
// difficulty. The calculation is the same (analogical) as for hit points.
|
||||||
|
ExperienceNormal int // called `Exp` in monstats.txt
|
||||||
|
ExperienceNightmare int // called `Exp(N)` in monstats.txt
|
||||||
|
ExperienceHell int // called `Exp(H)` in monstats.txt
|
||||||
|
|
||||||
|
// DamageMinA1Normal / Nightmare / Hell
|
||||||
|
// DamageMaxA1Normal / Nightmare /Hell
|
||||||
|
// this units minimum and maximum damage when it uses A1/A2/S1 mode.
|
||||||
|
// The calculation is the same (analogical) as for hit points.
|
||||||
|
DamageMinA1Normal int // called `A1MinD` in monstats.txt
|
||||||
|
DamageMinA1Nightmare int // called `A1MinD(N)` in monstats.txt
|
||||||
|
DamageMinA1Hell int // called `A1MinD(H)` in monstats.txt
|
||||||
|
DamageMaxA1Normal int // called `A1MaxD` in monstats.txt
|
||||||
|
DamageMaxA1Nightmare int // called `A1MaxD(N)` in monstats.txt
|
||||||
|
DamageMaxA1Hell int // called `A1MaxD(H)` in monstats.txt
|
||||||
|
DamageMinA2Normal int // called `A2MinD` in monstats.txt
|
||||||
|
DamageMinA2Nightmare int // called `A2MinD(N)` in monstats.txt
|
||||||
|
DamageMinA2Hell int // called `A2MinD(H)` in monstats.txt
|
||||||
|
DamageMaxA2Normal int // called `A2MaxD` in monstats.txt
|
||||||
|
DamageMaxA2Nightmare int // called `A2MaxD(N)` in monstats.txt
|
||||||
|
DamageMaxA2Hell int // called `A2MaxD(H)` in monstats.txt
|
||||||
|
DamageMinS1Normal int // called `S1MinD` in monstats.txt
|
||||||
|
DamageMinS1Nightmare int // called `S1MinD(N)` in monstats.txt
|
||||||
|
DamageMinS1Hell int // called `S1MinD(H)` in monstats.txt
|
||||||
|
DamageMaxS1Normal int // called `S1MaxD` in monstats.txt
|
||||||
|
DamageMaxS1Nightmare int // called `S1MaxD(N)` in monstats.txt
|
||||||
|
DamageMaxS1Hell int // called `S1MaxD(H)` in monstats.txt
|
||||||
|
|
||||||
|
// AttackRatingA1Normal AttackRatingS1Hell
|
||||||
|
// this units attack rating for A1/A2/S1 mode on the respective difficulties
|
||||||
|
// The calculation is the same (analogical) as for hit points.
|
||||||
|
AttackRatingA1Normal int // called `A1TH` in monstats.txt
|
||||||
|
AttackRatingA1Nightmare int // called `A1TH(N)` in monstats.txt
|
||||||
|
AttackRatingA1Hell int // called `A1TH(H)` in monstats.txt
|
||||||
|
AttackRatingA2Normal int // called `A2TH` in monstats.txt
|
||||||
|
AttackRatingA2Nightmare int // called `A2TH(N)` in monstats.txt
|
||||||
|
AttackRatingA2Hell int // called `A2TH(H)` in monstats.txt
|
||||||
|
AttackRatingS1Normal int // called `S1TH` in monstats.txt
|
||||||
|
AttackRatingS1Nightmare int // called `S1TH(N)` in monstats.txt
|
||||||
|
AttackRatingS1Hell int // called `S1TH(H)` in monstats.txt
|
||||||
|
|
||||||
|
// ElementChance1Normal -- ElementChance3Hell
|
||||||
|
// chance to append elemental damage to an attack on the respective
|
||||||
|
// difficulties. 0=Never append, 100=Always append.
|
||||||
|
ElementChance1Normal int // called `El1Pct` in monstats.txt
|
||||||
|
ElementChance1Nightmare int // called `El1Pct(N)` in monstats.txt
|
||||||
|
ElementChance1Hell int // called `El1Pct(H)` in monstats.txt
|
||||||
|
ElementChance2Normal int // called `El2Pct` in monstats.txt
|
||||||
|
ElementChance2Nightmare int // called `El2Pct(N)` in monstats.txt
|
||||||
|
ElementChance2Hell int // called `El2Pct(H)` in monstats.txt
|
||||||
|
ElementChance3Normal int // called `El3Pct` in monstats.txt
|
||||||
|
ElementChance3Nightmare int // called `El3Pct(N)` in monstats.txt
|
||||||
|
ElementChance3Hell int // called `El3Pct(H)` in monstats.txt
|
||||||
|
|
||||||
|
// ElementDamageMin1Normal -- ElementDamageMax3Hell
|
||||||
|
// minimum and Maximum elemental damage to append to the attack on the
|
||||||
|
// respective difficulties. Note that you should only append elemental
|
||||||
|
// damage to those missiles that don’t have any set in Missiles.txt. The
|
||||||
|
// calculation is the same (analogical) as for hit points.
|
||||||
|
ElementDamageMin1Normal int // called `El1MinD` in monstats.txt
|
||||||
|
ElementDamageMin1Nightmare int // called `El1MinD(N)` in monstats.txt
|
||||||
|
ElementDamageMin1Hell int // called `El1MinD(H)` in monstats.txt
|
||||||
|
ElementDamageMin2Normal int // called `El2MinD` in monstats.txt
|
||||||
|
ElementDamageMin2Nightmare int // called `El2MinD(N)` in monstats.txt
|
||||||
|
ElementDamageMin2Hell int // called `El2MinD(H)` in monstats.txt
|
||||||
|
ElementDamageMin3Normal int // called `El3MinD` in monstats.txt
|
||||||
|
ElementDamageMin3Nightmare int // called `El3MinD(N)` in monstats.txt
|
||||||
|
ElementDamageMin3Hell int // called `El3MinD(H)` in monstats.txt
|
||||||
|
ElementDamageMax1Normal int // called `El1MaxD` in monstats.txt
|
||||||
|
ElementDamageMax1Nightmare int // called `El1MaxD(N)` in monstats.txt
|
||||||
|
ElementDamageMax1Hell int // called `El1MaxD(H)` in monstats.txt
|
||||||
|
ElementDamageMax2Normal int // called `El2MaxD` in monstats.txt
|
||||||
|
ElementDamageMax2Nightmare int // called `El2MaxD(N)` in monstats.txt
|
||||||
|
ElementDamageMax2Hell int // called `El2MaxD(H)` in monstats.txt
|
||||||
|
ElementDamageMax3Normal int // called `El3MaxD` in monstats.txt
|
||||||
|
ElementDamageMax3Nightmare int // called `El3MaxD(N)` in monstats.txt
|
||||||
|
ElementDamageMax3Hell int // called `El3MaxD(H)` in monstats.txt
|
||||||
|
|
||||||
|
// ElementDuration1Normal -- ElementDuration3Hell
|
||||||
|
// duration of the elemental effect (for freeze, burn, cold, poison and
|
||||||
|
// stun) on the respective difficulties.
|
||||||
|
ElementDuration1Normal int // called `El1Dur` in monstats.txt
|
||||||
|
ElementDuration1Nightmare int // called `El1Dur(N)` in monstats.txt
|
||||||
|
ElementDuration1Hell int // called `El1Dur(H)` in monstats.txt
|
||||||
|
ElementDuration2Normal int // called `El2Dur` in monstats.txt
|
||||||
|
ElementDuration2Nightmare int // called `El2Dur(N)` in monstats.txt
|
||||||
|
ElementDuration2Hell int // called `El2Dur(H)` in monstats.txt
|
||||||
|
ElementDuration3Normal int // called `El3Dur` in monstats.txt
|
||||||
|
ElementDuration3Nightmare int // called `El3Dur(N)` in monstats.txt
|
||||||
|
ElementDuration3Hell int // called `El3Dur(H)` in monstats.txt
|
||||||
|
|
||||||
|
// SpecialEndDeath
|
||||||
|
// 0 == no special death
|
||||||
|
// 1 == spawn minion1 on death
|
||||||
|
// 2 == kill mounted minion on death (ie the guard tower)
|
||||||
|
SpecialEndDeath int // called `SplEndDeath` in monstats.txt
|
||||||
|
|
||||||
|
// Enabled controls whenever the unit can be
|
||||||
|
// used at all for any purpose whatsoever. This is not the only setting
|
||||||
|
// that controls this; there are some other things that can also disable
|
||||||
|
// the unit (Rarity and isSpawn columns see those for description).
|
||||||
|
Enabled bool // called `enabled` in monstats.txt
|
||||||
|
|
||||||
|
// SpawnsMinions tells the game whenever this
|
||||||
|
// unit is a “nest”. IE, monsters that spawn new monsters have this set to
|
||||||
|
// 1. Note that you can make any monster spawn new monsters, irregardless of
|
||||||
|
// its AI, all you need to do is adjust spawn related columns and make sure
|
||||||
|
// one of its skills is either “Nest” or “Minion Spawner”.
|
||||||
|
SpawnsMinions bool // called `placespawn` in monstats.txt
|
||||||
|
|
||||||
|
// IsLeader controls if a monster is the leader of minions it spawns
|
||||||
|
// a leadercan order "raid on target" it causes group members to use
|
||||||
|
// SK1 instead of A1 and A2 modes while raiding.
|
||||||
|
IsLeader bool // called `SetBoss` in monstats.txt
|
||||||
|
|
||||||
|
// TransferLeadership is connected with the previous one,
|
||||||
|
// when "boss of the group" is killed, the "leadership" is passed to one of
|
||||||
|
// his minions.
|
||||||
|
TransferLeadership bool // called `BossXfer` in monstats.txt
|
||||||
|
|
||||||
|
// Boolean, 1=spawnable, 0=not spawnable. This controls whenever this unit
|
||||||
|
// can be spawned via Levels.txt.
|
||||||
|
IsLevelSpawnable bool // called `isSpawn` in monstats.txt
|
||||||
|
|
||||||
|
// IsMelee controls whenever
|
||||||
|
// this unit can spawn with boss modifiers such as multiple shot or not.
|
||||||
|
IsMelee bool // called `isMelee` in monstats.txt
|
||||||
|
|
||||||
|
// IsNPC controls whenever the unit is a NPC or not.
|
||||||
|
IsNpc bool // called `npc` in monstats.txt
|
||||||
|
|
||||||
|
// IsInteractable
|
||||||
|
// controls whenever you can interact with this unit. IE this controls
|
||||||
|
// whenever it opens a speech-box or menu when you click on the unit. To
|
||||||
|
// turn units like Kaeleen or Flavie into enemies you will need to set this
|
||||||
|
// to 0 (you will also need to set NPC to 0 for that).
|
||||||
|
IsInteractable bool // called `interact` in monstats.txt
|
||||||
|
|
||||||
|
// IsRanged tells the game whenever this is a ranged attacker. It will make it possible for
|
||||||
|
// monsters to spawn with multiple shot modifier.
|
||||||
|
IsRanged bool // called `rangedtype` in monstats.txt
|
||||||
|
|
||||||
|
// HasInventory Controls whenever this
|
||||||
|
// NPC or UNIT can carry items with it. For NPCs this means that you can
|
||||||
|
// access their Inventory and buy items (if you disable this and then try to
|
||||||
|
// access this feature it will cause a crash so don’t do it unless you know
|
||||||
|
// what you’re doing). For Monsters this means that they can access their
|
||||||
|
// equipment data in MonEquip.txt.
|
||||||
|
HasInventory bool // called `inventory` in monstats.txt
|
||||||
|
|
||||||
|
// CanEnterTown
|
||||||
|
// controls whenever enemies can follow you into a town or not. This should be set to
|
||||||
|
// 1 for everything that spawns in a town for obvious reasons. According to
|
||||||
|
// informations from Ogodei, it also disables/enables collision in
|
||||||
|
// singleplayer and allows pets to walk/not walk in city in multiplayer.
|
||||||
|
// In multiplayer collision is always set to 0 for pets.
|
||||||
|
CanEnterTown bool // called `inTown` in monstats.txt
|
||||||
|
|
||||||
|
// IsUndeadLow, IsUndeadHigh
|
||||||
|
// Blizzard used this to differentiate High and Low Undead (IE low
|
||||||
|
// undead like Zombies, Skeletons etc are set to 1 here), both this and
|
||||||
|
// HUNDEAD will make the unit be considered undead. Low undeads can be
|
||||||
|
// resurrected by high undeads. High undeads can't resurrect eachother.
|
||||||
|
IsUndeadLow bool // called `lUndead` in monstats.txt
|
||||||
|
IsUndeadHigh bool // called `hUndead` in monstats.txt
|
||||||
|
|
||||||
|
// IsDemon makes the game consider this unit a demon.
|
||||||
|
IsDemon bool // called `demon` in monstats.txt
|
||||||
|
|
||||||
|
// IsFlying If you set this to 1 the monster will be able to move fly over
|
||||||
|
// obstacles such as puddles and rivers.
|
||||||
|
IsFlying bool // called `flying` in monstats.txt
|
||||||
|
|
||||||
|
// CanOpenDoors controls whether monsters can open doors or not
|
||||||
|
CanOpenDoors bool // called `opendoors` in monstats.txt
|
||||||
|
|
||||||
|
// IsSpecialBoss controls whenever this unit
|
||||||
|
// is a special boss, as mentioned already, monsters set as boss IGNORE the
|
||||||
|
// level settings, IE they will always spawn with the levels specified in
|
||||||
|
// MonStats.txt. Boss will gain some special resistances, such as immunity
|
||||||
|
// to being stunned (!!!), also it will not be affected by things like
|
||||||
|
// deadly strike the way normal monsters are.
|
||||||
|
IsSpecialBoss bool // called `boss` in monstats.txt
|
||||||
|
|
||||||
|
// IsActBoss
|
||||||
|
// Setting this to 1 will give your monsters huge (300% IIRC) damage bonus
|
||||||
|
// against hirelings and summons. Ever wondered why Diablo destroys your
|
||||||
|
// skeletons with 1 fire nova while barely doing anything to you? Here is
|
||||||
|
// your answer.
|
||||||
|
IsActBoss bool // called `primeevil` in monstats.txt
|
||||||
|
|
||||||
|
// IsKillable will make the monster absolutely unkillable.
|
||||||
|
IsKillable bool // called `killable` in monstats.txt
|
||||||
|
|
||||||
|
// IsAiSwitchable Gives controls if this units mind may
|
||||||
|
// be altered by “mind altering skills” like Attract, Conversion, Revive
|
||||||
|
IsAiSwitchable bool // called `switchai` in monstats.txt
|
||||||
|
|
||||||
|
// DisableAura Monsters set to 0 here
|
||||||
|
// will not be effected by friendly auras
|
||||||
|
DisableAura bool // called `noAura` in monstats.txt
|
||||||
|
|
||||||
|
// DisableMultiShot
|
||||||
|
// This is another layer of security to prevent this modifier from spawning,
|
||||||
|
// besides the ISMELEE layer.
|
||||||
|
DisableMultiShot bool // called `nomultishot` in monstats.txt
|
||||||
|
|
||||||
|
// DisableCounting
|
||||||
|
// prevents your pets from being counted as population in said area, for
|
||||||
|
// example thanks to this you can finish The Den Of Evil quest while having
|
||||||
|
// pets summoned.
|
||||||
|
DisableCounting bool // called `neverCount` in monstats.txt
|
||||||
|
|
||||||
|
// IgnorePets
|
||||||
|
// Summons and hirelings are ignored by this unit, 0=Summons and
|
||||||
|
// hirelings are noticed by this unit. If you set this to 1 you will the
|
||||||
|
// monsters going directly for the player.
|
||||||
|
IgnorePets bool // called `petIgnore` in monstats.txt
|
||||||
|
|
||||||
|
// DealsDamageOnDeath This works similar to corpse explosion (its based on
|
||||||
|
// hitpoints) and damages the surrounding players when the unit dies. (Ever
|
||||||
|
// wanted to prevent those undead stygian dolls from doing damage when they
|
||||||
|
// die, this is all there is to it)
|
||||||
|
DealsDamageOnDeath bool // called `deathDmg` in monstats.txt
|
||||||
|
|
||||||
|
// GenericSpawn Has to do
|
||||||
|
// something is with minions being transformed into suicide minions, the
|
||||||
|
// exact purpose of this is a mystery.
|
||||||
|
GenericSpawn bool // called `genericSpawn` in monstats.txt
|
||||||
|
|
||||||
|
// IgnoreMonLevelTxt Does this unit use
|
||||||
|
// MonLevel.txt or does it use the stats listed in MonStats.txt as is.
|
||||||
|
// Setting this to 1 will result in an array of problems, such as the
|
||||||
|
// appended elemental damage being completely ignored, irregardless of the
|
||||||
|
// values in it.
|
||||||
|
IgnoreMonLevelTxt bool // called `noRatio` in monstats.txt
|
||||||
|
|
||||||
|
// CanBlockWithoutShield in order for a unit to
|
||||||
|
// block it needs the BL mode, if this is set to 1 then it will block
|
||||||
|
// irregardless of what modes it has.
|
||||||
|
CanBlockWithoutShield bool // called `NoShldBlock` in monstats.txt
|
||||||
|
|
||||||
|
// SpecialGetModeChart
|
||||||
|
// Unknown but could be telling the game to look at some internal table.
|
||||||
|
// This is used for some Act Bosses and monsters like Putrid Defilers.
|
||||||
|
SpecialGetModeChart bool // called `SplGetModeChar` in monstats.txt
|
||||||
|
|
||||||
|
// SpecialEndGeneric Works in conjunction with SPLCLIENTEND, this
|
||||||
|
// makes the unit untargetable when it is first spawned (used for those monsters that are under water, under ground or fly above you)
|
||||||
|
SpecialEndGeneric bool // called `SplEndGeneric` in monstats.txt
|
||||||
|
|
||||||
|
// SpecialClientEnd Works in conjunction with SPLENDGENERIC, this
|
||||||
|
// makes the unit invisible when it is first spawned (used for those
|
||||||
|
// monsters that are under water, under ground or fly above you), this is
|
||||||
|
// also used for units that have other special drawing setups.
|
||||||
|
SpecialClientEnd bool // called `SplClientEnd` in monstats.txt
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
|
@ -0,0 +1,48 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func monsterSuperUniqeLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(SuperUniques)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &SuperUniqueRecord{
|
||||||
|
Key: d.String("Superunique"),
|
||||||
|
Name: d.String("Name"),
|
||||||
|
Class: d.String("Class"),
|
||||||
|
HcIdx: d.String("hcIdx"),
|
||||||
|
MonSound: d.String("MonSound"),
|
||||||
|
Mod: [3]int{
|
||||||
|
d.Number("Mod1"),
|
||||||
|
d.Number("Mod2"),
|
||||||
|
d.Number("Mod3"),
|
||||||
|
},
|
||||||
|
MinGrp: d.Number("MinGrp"),
|
||||||
|
MaxGrp: d.Number("MaxGrp"),
|
||||||
|
IsExpansion: d.Bool("EClass"),
|
||||||
|
AutoPosition: d.Bool("AutoPos"),
|
||||||
|
Stacks: d.Bool("Stacks"),
|
||||||
|
TreasureClassNormal: d.String("TC"),
|
||||||
|
TreasureClassNightmare: d.String("TC(N)"),
|
||||||
|
TreasureClassHell: d.String("TC(H)"),
|
||||||
|
UTransNormal: d.String("Utrans"),
|
||||||
|
UTransNightmare: d.String("Utrans(N)"),
|
||||||
|
UTransHell: d.String("Utrans(H)"),
|
||||||
|
}
|
||||||
|
records[record.Key] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Monster.Unique.Super = records
|
||||||
|
|
||||||
|
log.Printf("Loaded %d SuperUnique records", len(records))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// https://d2mods.info/forum/kb/viewarticle?a=162
|
||||||
|
|
||||||
|
// SuperUniques stores all of the SuperUniqueRecords
|
||||||
|
type SuperUniques map[string]*SuperUniqueRecord
|
||||||
|
|
||||||
|
// SuperUniqueRecord Defines the unique monsters and their properties.
|
||||||
|
// SuperUnique monsters are boss monsters which always appear at the same places
|
||||||
|
// and always have the same base special abilities
|
||||||
|
// with the addition of one or two extra ones per difficulty (Nightmare provides one extra ability, Hell provides two).
|
||||||
|
// Notable examples are enemies such as Corpsefire, Pindleskin or Nihlathak.
|
||||||
|
type SuperUniqueRecord struct {
|
||||||
|
|
||||||
|
// id of the SuperUnique Monster. Each SuperUnique Monster must use a different id.
|
||||||
|
// It also serves as the string to use in the 'Place' field of MonPreset.txt
|
||||||
|
Key string // Superunique
|
||||||
|
|
||||||
|
// Name for this SuperUnique which must be retrieved from a .TBL file
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// the base monster type of the SuperUnique, refers to the "Key" field in monstats.go ("ID" column in the MonStats.txt)
|
||||||
|
Class string
|
||||||
|
|
||||||
|
// This is the "hardcoded index".
|
||||||
|
// Vanilla SuperUniques in the game ranges from 0 to 65. Some of them have some hardcoded stuffs attached.
|
||||||
|
// NOTE: It is also possible to create new SuperUniques with hardcoded stuff attached. To do this, you can use a hcIx from 0 to 65.
|
||||||
|
// Example A: If you create a new SuperUnique with a hcIdx of 42 (Shenk the Overseer) then whatever its Class,
|
||||||
|
// this SuperUnique will have 20 Enslaved as minions (exactly like the vanilla Shenk, and in spite of NOT being Shenk).
|
||||||
|
// Example B: If you want a simple new SuperUnique, you must use a hcIdx greater than 65,
|
||||||
|
// because greater indexes don't exist in the code and therefore your new boss won't have anything special attached
|
||||||
|
HcIdx string
|
||||||
|
|
||||||
|
// This field forces the SuperUnique to use a special set of sounds for attacks, taunts, death etc.
|
||||||
|
// The Countess is a clear and noticeable example of this. The MonSound set is taken from MonSounds.txt.
|
||||||
|
MonSound string
|
||||||
|
|
||||||
|
// These three fields assign special abilities so SuperUnique monsters such as "Fire Enchanted" or "Stone Skin".
|
||||||
|
// These fields refers to the ID's corresponding to the properties in MonUMod.txt.
|
||||||
|
// Here is the list of available properties.
|
||||||
|
// 0. None
|
||||||
|
// 1. Inits the random name seed, automatically added to monster, you don't need to add this UMod.
|
||||||
|
// 2. Hit Point bonus which is automatically added to the monster. You don't really need to manually add this UMod
|
||||||
|
// 3. Increases the light radius and picks a random color for it (bugged in v1.10+).
|
||||||
|
// 4. Increases the monster level, resulting in higher damage.
|
||||||
|
// 5. Extra Strong: increases physical damage done by boss.
|
||||||
|
// 6. Extra Fast: faster walk / run and attack speed (Although the increased attack speed isn't added in newer versions . . .)
|
||||||
|
// 7. Cursed: randomly cast Amplify Damage when hitting
|
||||||
|
// 8. Magic Resist: +50% resistance against Elemental attacks (Fire, Cold, Lightning and Poison)
|
||||||
|
// 9. Fire Enchanted: additional fire damage and +50% fire resistance.
|
||||||
|
// 10. When killed, release a poisonous cloud, like the Mummies in Act 2.
|
||||||
|
// 11. Corpse will spawn little white maggots (like Duriel).
|
||||||
|
// 12. Works for Bloodraven only, and seems to have something to do with her Death sequence.
|
||||||
|
// 13. Ignore your Armor Class and nearly always hit you.
|
||||||
|
// 14. It should add damage to its minions
|
||||||
|
// 15. When killed, all his minions die immediately as well.
|
||||||
|
// 16. Adds base champion modifiers [color=#0040FF][b](champions only)[/b][/color]
|
||||||
|
// 17. Lightning Enchanted: additional lightning damage, +50% lightning resistance and release Charged Bolts when hit.
|
||||||
|
// 18. Cold Enchanted: additional cold damage, +50% cold resistance, and releases a Frost Nova upon death
|
||||||
|
// 19. Assigns extra damage to hireling attacks, relic from pre-lod, causes bugged damage.
|
||||||
|
// 20. Releases Charged Bolts when hit, like the Scarabs in act 2.
|
||||||
|
// 21. Present in the code, but it seems to have no effect.
|
||||||
|
// 22. Has to do with quests, but is non-functional for Superuniques which aren´t in relation to a quest.
|
||||||
|
// 23. Has a poison aura that poisons you when you're approaching him, adds poison damage to attack.
|
||||||
|
// 24. Code present, but untested in v1.10+, does something else now.
|
||||||
|
// 25. Mana Burn: steals mana from you and heals itself when hitting. Adds magic resistance.
|
||||||
|
// 26. TeleHeal: randomly warps around when attacked and heals itself.
|
||||||
|
// 27. Spectral Hit: deals random elemental damage when hitting
|
||||||
|
// 28. Stone Skin: +80% physical damage resistance, increases defense
|
||||||
|
// 29. Multiple Shots: Ranged attackers shoots several missiles at once.
|
||||||
|
// 30. Aura Enchanted: Assigns a random offensive aura (aside from Thorns, Sanctuary and Concentration) to the SuperUnique
|
||||||
|
// 31. Explodes in a Corpse Explosion when killed.
|
||||||
|
// 32. Explodeswith a fiery flash when killed (Visual effect only).
|
||||||
|
// 33. Explode and chills you when killed (like suicide minions). It heavily reduces the Boss' Hit Points
|
||||||
|
// 34. Self-resurrect effect for Reanimate Horde, bugged on other units.
|
||||||
|
// 35. Shatter into Ice pieces when killed, no corpse remains.
|
||||||
|
// 36. Adds physical resistance and reduces movement speed(used for Champions only)
|
||||||
|
// 37. Alters champion stats (used for Champions only)
|
||||||
|
// 38. Champion cannot be cursed (used for Champions only)
|
||||||
|
// 39. Alters champion stats (used for Champions only)
|
||||||
|
// 40. Releases a painworm when killed, but display is very buggy.
|
||||||
|
// 41. Code present, but has no effect in-game, probably due to bugs
|
||||||
|
// 42. Releases a Nova when killed, but display is bugged.
|
||||||
|
Mod [3]int
|
||||||
|
|
||||||
|
// These two fields control the Minimum and Maximum amount of minions which will be spawned along with the SuperUnique.
|
||||||
|
// If those values differ, the game will roll a random number within the MinGrp and the MaxGrp.
|
||||||
|
MinGrp int
|
||||||
|
MaxGrp int
|
||||||
|
|
||||||
|
// Boolean indicates if the game is expansion or classic
|
||||||
|
IsExpansion bool // named as "EClass" in the SuperUniques.txt
|
||||||
|
|
||||||
|
// This field states whether the SuperUnique will be placed within a radius from his original
|
||||||
|
// position(defined by the .ds1 map file), or not.
|
||||||
|
// false means that the boss will spawn in a random position within a large radius from its actual
|
||||||
|
// position in the .ds1 file,
|
||||||
|
// true means it will spawn exactly where expected.
|
||||||
|
AutoPosition bool
|
||||||
|
|
||||||
|
// Specifies if this SuperUnique can spawn more than once in the same game.
|
||||||
|
// true means it can spawn more than once in the same game, false means it can not.
|
||||||
|
Stacks bool
|
||||||
|
|
||||||
|
// Treasure Classes for the 3 Difficulties.
|
||||||
|
// These columns list the treasureclass that is valid if this boss is killed and drops something.
|
||||||
|
// These fields must contain the values taken from the "TreasureClass" column in TreasureClassEx.txt (Expansion)
|
||||||
|
// or TreasureClass (Classic).
|
||||||
|
TreasureClassNormal string
|
||||||
|
TreasureClassNightmare string
|
||||||
|
TreasureClassHell string
|
||||||
|
|
||||||
|
// These fields dictate which RandTransform.dat color index the SuperUnique will use respectively in Normal, Nightmare and Hell mode.
|
||||||
|
UTransNormal string
|
||||||
|
UTransNightmare string
|
||||||
|
UTransHell string
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func monsterTypesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(MonsterTypes)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &MonTypeRecord{
|
||||||
|
Type: d.String("type"),
|
||||||
|
Equiv1: d.String("equiv1"),
|
||||||
|
Equiv2: d.String("equiv2"),
|
||||||
|
Equiv3: d.String("equiv3"),
|
||||||
|
StrSing: d.String("strsing"),
|
||||||
|
StrPlural: d.String("strplur"),
|
||||||
|
}
|
||||||
|
records[record.Type] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
panic(d.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d MonType records", len(records))
|
||||||
|
|
||||||
|
r.Monster.Types = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// MonTypes stores all of the MonTypeRecords
|
||||||
|
type MonsterTypes map[string]*MonTypeRecord
|
||||||
|
|
||||||
|
// MonTypeRecord is a representation of a single row of MonType.txt.
|
||||||
|
type MonTypeRecord struct {
|
||||||
|
Type string
|
||||||
|
Equiv1 string
|
||||||
|
Equiv2 string
|
||||||
|
Equiv3 string
|
||||||
|
// StrSing is the string displayed for the singular form (Skeleton), note
|
||||||
|
// that this is unused in the original engine, since the only modifier
|
||||||
|
// display code that accesses MonType uses StrPlur.
|
||||||
|
StrSing string
|
||||||
|
StrPlural string
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func monsterUniqModifiersLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(MonsterUniqueModifiers)
|
||||||
|
constants := make([]int, 0)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &MonUModRecord{
|
||||||
|
Name: d.String("uniquemod"),
|
||||||
|
ID: d.Number("id"),
|
||||||
|
Enabled: d.Bool("enabled"),
|
||||||
|
ExpansionOnly: d.Number("version") == expansionCode,
|
||||||
|
Xfer: d.Bool("xfer"),
|
||||||
|
Champion: d.Bool("champion"),
|
||||||
|
FPick: d.Number("fpick"),
|
||||||
|
Exclude1: d.String("exclude1"),
|
||||||
|
Exclude2: d.String("exclude2"),
|
||||||
|
PickFrequencies: struct {
|
||||||
|
Normal *PickFreq
|
||||||
|
Nightmare *PickFreq
|
||||||
|
Hell *PickFreq
|
||||||
|
}{
|
||||||
|
Normal: &PickFreq{
|
||||||
|
Champion: d.Number("cpick"),
|
||||||
|
Unique: d.Number("upick"),
|
||||||
|
},
|
||||||
|
Nightmare: &PickFreq{
|
||||||
|
Champion: d.Number("cpick (N)"),
|
||||||
|
Unique: d.Number("upick (N)"),
|
||||||
|
},
|
||||||
|
Hell: &PickFreq{
|
||||||
|
Champion: d.Number("cpick (H)"),
|
||||||
|
Unique: d.Number("upick (H)"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
records[record.Name] = record
|
||||||
|
|
||||||
|
constants = append(constants, d.Number("constants"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d MonsterUniqueModifier records", len(records))
|
||||||
|
|
||||||
|
r.Monster.Unique.Mods = records
|
||||||
|
r.Monster.Unique.Constants = constants
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// See [https://d2mods.info/forum/kb/viewarticle?a=161] for more info
|
||||||
|
|
||||||
|
// MonsterUniqueModifiers stores the MonsterUniqueModifierRecords
|
||||||
|
type MonsterUniqueModifiers map[string]*MonUModRecord
|
||||||
|
|
||||||
|
// MonsterUniqueModifierConstants contains constants from monumod.txt,
|
||||||
|
// can be accessed with indices from d2enum.MonUModConstIndex
|
||||||
|
type MonsterUniqueModifierConstants []int
|
||||||
|
|
||||||
|
// MonUModRecord represents a single line in monumod.txt
|
||||||
|
// Information gathered from [https://d2mods.info/forum/kb/viewarticle?a=161]
|
||||||
|
type MonUModRecord struct {
|
||||||
|
// Name of modifer, not used by other files
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// ID of the modifier,
|
||||||
|
// the Mod fields of SuperUniqueRecord refer to these ID's
|
||||||
|
ID int
|
||||||
|
|
||||||
|
// Enabled boolean for whether this modifier can be applied
|
||||||
|
Enabled bool
|
||||||
|
|
||||||
|
// ExpansionOnly boolean for whether this modifier can only be applied in an expansion game.
|
||||||
|
// In the file, the value 100 represents expansion only
|
||||||
|
ExpansionOnly bool
|
||||||
|
|
||||||
|
// If true, "Minion" will be displayed below the life bar of minions of
|
||||||
|
// the monster with this modifier
|
||||||
|
Xfer bool
|
||||||
|
|
||||||
|
// Champion boolean, only usable by champion monsters
|
||||||
|
Champion bool
|
||||||
|
|
||||||
|
// FPick Unknown
|
||||||
|
FPick int
|
||||||
|
|
||||||
|
// Exclude1 monster type code that cannot have this modifier
|
||||||
|
Exclude1 string
|
||||||
|
|
||||||
|
// Exclude2 monster type code that cannot have this modifier
|
||||||
|
Exclude2 string
|
||||||
|
|
||||||
|
PickFrequencies struct {
|
||||||
|
Normal *PickFreq
|
||||||
|
Nightmare *PickFreq
|
||||||
|
Hell *PickFreq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PickFreq struct {
|
||||||
|
// Champion pick frequency
|
||||||
|
Champion int
|
||||||
|
|
||||||
|
// Unique pick frequency
|
||||||
|
Unique int
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func npcLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(NPCs)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &NPCRecord{
|
||||||
|
Name: d.String("npc"),
|
||||||
|
Multipliers: &costMultiplier{
|
||||||
|
Buy: float64(d.Number("buy mult")) / costDivisor,
|
||||||
|
Sell: float64(d.Number("sell mult")) / costDivisor,
|
||||||
|
Repair: float64(d.Number("rep mult")) / costDivisor,
|
||||||
|
},
|
||||||
|
MaxBuy: struct {
|
||||||
|
Normal int
|
||||||
|
Nightmare int
|
||||||
|
Hell int
|
||||||
|
}{
|
||||||
|
Normal: d.Number("max buy"),
|
||||||
|
Nightmare: d.Number("max buy (N)"),
|
||||||
|
Hell: d.Number("max buy (H)"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
record.QuestMultipliers = make(map[int]*costMultiplier)
|
||||||
|
|
||||||
|
if flagStr := d.String("questflag A"); flagStr != "" {
|
||||||
|
flag := d.Number("questflag A")
|
||||||
|
record.QuestMultipliers[flag] = &costMultiplier{
|
||||||
|
float64(d.Number("questbuymult A")) / costDivisor,
|
||||||
|
float64(d.Number("questsellmult A")) / costDivisor,
|
||||||
|
float64(d.Number("questrepmult A")) / costDivisor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if flagStr := d.String("questflag B"); flagStr != "" {
|
||||||
|
flag := d.Number("questflag B")
|
||||||
|
record.QuestMultipliers[flag] = &costMultiplier{
|
||||||
|
float64(d.Number("questbuymult B")) / costDivisor,
|
||||||
|
float64(d.Number("questsellmult B")) / costDivisor,
|
||||||
|
float64(d.Number("questrepmult B")) / costDivisor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if flagStr := d.String("questflag C"); flagStr != "" {
|
||||||
|
flag := d.Number("questflag C")
|
||||||
|
record.QuestMultipliers[flag] = &costMultiplier{
|
||||||
|
float64(d.Number("questbuymult C")) / costDivisor,
|
||||||
|
float64(d.Number("questsellmult C")) / costDivisor,
|
||||||
|
float64(d.Number("questrepmult C")) / costDivisor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
records[record.Name] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.NPCs = records
|
||||||
|
|
||||||
|
log.Printf("Loaded %d NPC records", len(records))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
const (
|
||||||
|
costDivisor = 1024.
|
||||||
|
)
|
||||||
|
|
||||||
|
// NPCs stores the NPCRecords
|
||||||
|
type NPCs map[string]*NPCRecord
|
||||||
|
|
||||||
|
// NPCRecord represents a single line in NPC.txt
|
||||||
|
// The information has been gathered from [https:// d2mods.info/forum/kb/viewarticle?a=387]
|
||||||
|
type NPCRecord struct {
|
||||||
|
// Name is an ID pointer to row of this npc in monstats.txt
|
||||||
|
Name string
|
||||||
|
|
||||||
|
Multipliers *costMultiplier
|
||||||
|
|
||||||
|
QuestMultipliers map[int]*costMultiplier
|
||||||
|
|
||||||
|
// MaxBuy is the maximum amount of gold an NPC will pay for an item for the corresponding
|
||||||
|
// difficulty
|
||||||
|
MaxBuy struct {
|
||||||
|
Normal int
|
||||||
|
Nightmare int
|
||||||
|
Hell int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type costMultiplier struct {
|
||||||
|
// Buy is a percentage of base item price used when an item is bought by NPC
|
||||||
|
Buy float64
|
||||||
|
|
||||||
|
// Sell is a percentage of base item price used when an item is sold by NPC
|
||||||
|
Sell float64
|
||||||
|
|
||||||
|
// Repair is a percentage of base item price used to calculate the base repair price
|
||||||
|
Repair float64
|
||||||
|
}
|
|
@ -0,0 +1,239 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
//nolint:funlen // Makes no sense to split
|
||||||
|
func objectDetailsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(ObjectDetails)
|
||||||
|
|
||||||
|
i := -1
|
||||||
|
inc := func() int {
|
||||||
|
i++
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
record := &ObjectDetailsRecord{
|
||||||
|
Name: d.String("Name"),
|
||||||
|
Description: d.String("description - not loaded"),
|
||||||
|
id: d.Number("Id"),
|
||||||
|
token: d.String("Token"),
|
||||||
|
|
||||||
|
SpawnMax: d.Number("SpawnMax"),
|
||||||
|
Selectable: [8]bool{
|
||||||
|
d.Number("Selectable0") == 1,
|
||||||
|
d.Number("Selectable1") == 1,
|
||||||
|
d.Number("Selectable2") == 1,
|
||||||
|
d.Number("Selectable3") == 1,
|
||||||
|
d.Number("Selectable4") == 1,
|
||||||
|
d.Number("Selectable5") == 1,
|
||||||
|
d.Number("Selectable6") == 1,
|
||||||
|
d.Number("Selectable7") == 1,
|
||||||
|
},
|
||||||
|
TrapProbability: d.Number("TrapProb"),
|
||||||
|
|
||||||
|
SizeX: d.Number("SizeX"),
|
||||||
|
SizeY: d.Number("SizeY"),
|
||||||
|
|
||||||
|
NTgtFX: d.Number("nTgtFX"),
|
||||||
|
NTgtFY: d.Number("nTgtFY"),
|
||||||
|
NTgtBX: d.Number("nTgtBX"),
|
||||||
|
NTgtBY: d.Number("nTgtBY"),
|
||||||
|
|
||||||
|
FrameCount: [8]int{
|
||||||
|
d.Number("FrameCnt0"),
|
||||||
|
d.Number("FrameCnt1"),
|
||||||
|
d.Number("FrameCnt2"),
|
||||||
|
d.Number("FrameCnt3"),
|
||||||
|
d.Number("FrameCnt4"),
|
||||||
|
d.Number("FrameCnt5"),
|
||||||
|
d.Number("FrameCnt6"),
|
||||||
|
d.Number("FrameCnt7"),
|
||||||
|
},
|
||||||
|
FrameDelta: [8]int{
|
||||||
|
d.Number("FrameDelta0"),
|
||||||
|
d.Number("FrameDelta1"),
|
||||||
|
d.Number("FrameDelta2"),
|
||||||
|
d.Number("FrameDelta3"),
|
||||||
|
d.Number("FrameDelta4"),
|
||||||
|
d.Number("FrameDelta5"),
|
||||||
|
d.Number("FrameDelta6"),
|
||||||
|
d.Number("FrameDelta7"),
|
||||||
|
},
|
||||||
|
CycleAnimation: [8]bool{
|
||||||
|
d.Number("CycleAnim0") == 1,
|
||||||
|
d.Number("CycleAnim1") == 1,
|
||||||
|
d.Number("CycleAnim2") == 1,
|
||||||
|
d.Number("CycleAnim3") == 1,
|
||||||
|
d.Number("CycleAnim4") == 1,
|
||||||
|
d.Number("CycleAnim5") == 1,
|
||||||
|
d.Number("CycleAnim6") == 1,
|
||||||
|
d.Number("CycleAnim7") == 1,
|
||||||
|
},
|
||||||
|
LightDiameter: [8]int{
|
||||||
|
d.Number("Lit0"),
|
||||||
|
d.Number("Lit1"),
|
||||||
|
d.Number("Lit2"),
|
||||||
|
d.Number("Lit3"),
|
||||||
|
d.Number("Lit4"),
|
||||||
|
d.Number("Lit5"),
|
||||||
|
d.Number("Lit6"),
|
||||||
|
d.Number("Lit7"),
|
||||||
|
},
|
||||||
|
BlocksLight: [8]bool{
|
||||||
|
d.Number("BlocksLight0") == 1,
|
||||||
|
d.Number("BlocksLight1") == 1,
|
||||||
|
d.Number("BlocksLight2") == 1,
|
||||||
|
d.Number("BlocksLight3") == 1,
|
||||||
|
d.Number("BlocksLight4") == 1,
|
||||||
|
d.Number("BlocksLight5") == 1,
|
||||||
|
d.Number("BlocksLight6") == 1,
|
||||||
|
d.Number("BlocksLight7") == 1,
|
||||||
|
},
|
||||||
|
HasCollision: [8]bool{
|
||||||
|
d.Number("HasCollision0") == 1,
|
||||||
|
d.Number("HasCollision1") == 1,
|
||||||
|
d.Number("HasCollision2") == 1,
|
||||||
|
d.Number("HasCollision3") == 1,
|
||||||
|
d.Number("HasCollision4") == 1,
|
||||||
|
d.Number("HasCollision5") == 1,
|
||||||
|
d.Number("HasCollision6") == 1,
|
||||||
|
d.Number("HasCollision7") == 1,
|
||||||
|
},
|
||||||
|
IsAttackable: d.Number("IsAttackable0") == 1,
|
||||||
|
StartFrame: [8]int{
|
||||||
|
d.Number("Start0"),
|
||||||
|
d.Number("Start1"),
|
||||||
|
d.Number("Start2"),
|
||||||
|
d.Number("Start3"),
|
||||||
|
d.Number("Start4"),
|
||||||
|
d.Number("Start5"),
|
||||||
|
d.Number("Start6"),
|
||||||
|
d.Number("Start7"),
|
||||||
|
},
|
||||||
|
|
||||||
|
EnvEffect: d.Number("EnvEffect") == 1,
|
||||||
|
IsDoor: d.Number("IsDoor") == 1,
|
||||||
|
BlockVisibility: d.Number("BlocksVis") == 1,
|
||||||
|
Orientation: d.Number("Orientation"),
|
||||||
|
Trans: d.Number("Trans"),
|
||||||
|
|
||||||
|
OrderFlag: [8]int{
|
||||||
|
d.Number("OrderFlag0"),
|
||||||
|
d.Number("OrderFlag1"),
|
||||||
|
d.Number("OrderFlag2"),
|
||||||
|
d.Number("OrderFlag3"),
|
||||||
|
d.Number("OrderFlag4"),
|
||||||
|
d.Number("OrderFlag5"),
|
||||||
|
d.Number("OrderFlag6"),
|
||||||
|
d.Number("OrderFlag7"),
|
||||||
|
},
|
||||||
|
PreOperate: d.Number("PreOperate") == 1,
|
||||||
|
HasAnimationMode: [8]bool{
|
||||||
|
d.Number("Mode0") == 1,
|
||||||
|
d.Number("Mode1") == 1,
|
||||||
|
d.Number("Mode2") == 1,
|
||||||
|
d.Number("Mode3") == 1,
|
||||||
|
d.Number("Mode4") == 1,
|
||||||
|
d.Number("Mode5") == 1,
|
||||||
|
d.Number("Mode6") == 1,
|
||||||
|
d.Number("Mode7") == 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
XOffset: d.Number("Yoffset"),
|
||||||
|
YOffset: d.Number("Xoffset"),
|
||||||
|
Draw: d.Number("Draw") == 1,
|
||||||
|
|
||||||
|
LightRed: uint8(d.Number("Red")),
|
||||||
|
LightGreen: uint8(d.Number("Green")),
|
||||||
|
LightBlue: uint8(d.Number("Blue")),
|
||||||
|
|
||||||
|
SelHD: d.Number("HD") == 1,
|
||||||
|
SelTR: d.Number("TR") == 1,
|
||||||
|
SelLG: d.Number("LG") == 1,
|
||||||
|
SelRA: d.Number("RA") == 1,
|
||||||
|
SelLA: d.Number("LA") == 1,
|
||||||
|
SelRH: d.Number("RH") == 1,
|
||||||
|
SelLH: d.Number("LH") == 1,
|
||||||
|
SelSH: d.Number("SH") == 1,
|
||||||
|
SelS: [8]bool{
|
||||||
|
d.Number("S1") == 1,
|
||||||
|
d.Number("S2") == 1,
|
||||||
|
d.Number("S3") == 1,
|
||||||
|
d.Number("S4") == 1,
|
||||||
|
d.Number("S5") == 1,
|
||||||
|
d.Number("S6") == 1,
|
||||||
|
d.Number("S7") == 1,
|
||||||
|
d.Number("S8") == 1,
|
||||||
|
},
|
||||||
|
|
||||||
|
TotalPieces: d.Number("TotalPieces"),
|
||||||
|
SubClass: d.Number("SubClass"),
|
||||||
|
|
||||||
|
XSpace: d.Number("Xspace"),
|
||||||
|
YSpace: d.Number("Yspace"),
|
||||||
|
|
||||||
|
NameOffset: d.Number("NameOffset"),
|
||||||
|
|
||||||
|
MonsterOk: uint8(d.Number("MonsterOK")) == 1,
|
||||||
|
OperateRange: d.Number("OperateRange"),
|
||||||
|
ShrineFunction: d.Number("ShrineFunction"),
|
||||||
|
Restore: uint8(d.Number("Restore")) == 1,
|
||||||
|
|
||||||
|
Parm: [8]int{
|
||||||
|
d.Number("Parm0"),
|
||||||
|
d.Number("Parm1"),
|
||||||
|
d.Number("Parm2"),
|
||||||
|
d.Number("Parm3"),
|
||||||
|
d.Number("Parm4"),
|
||||||
|
d.Number("Parm5"),
|
||||||
|
d.Number("Parm6"),
|
||||||
|
d.Number("Parm7"),
|
||||||
|
},
|
||||||
|
Act: d.Number("Act"),
|
||||||
|
Lockable: uint8(d.Number("Lockable")) == 1,
|
||||||
|
Gore: uint8(d.Number("Gore")) == 1,
|
||||||
|
Sync: uint8(d.Number("Sync")) == 1,
|
||||||
|
Flicker: uint8(d.Number("Flicker")) == 1,
|
||||||
|
Damage: d.Number("Damage"),
|
||||||
|
Beta: uint8(d.Number("Beta")) == 1,
|
||||||
|
Overlay: uint8(d.Number("Overlay")) == 1,
|
||||||
|
CollisionSubst: uint8(d.Number("CollisionSubst")) == 1,
|
||||||
|
|
||||||
|
Left: d.Number("Left"),
|
||||||
|
Top: d.Number("Top"),
|
||||||
|
Width: d.Number("Width"),
|
||||||
|
Height: d.Number("Height"),
|
||||||
|
|
||||||
|
OperateFn: d.Number("OperateFn"),
|
||||||
|
PopulateFn: d.Number("PopulateFn"),
|
||||||
|
InitFn: d.Number("InitFn"),
|
||||||
|
ClientFn: d.Number("ClientFn"),
|
||||||
|
|
||||||
|
RestoreVirgins: uint8(d.Number("RestoreVirgins")) == 1,
|
||||||
|
BlockMissile: uint8(d.Number("BlockMissile")) == 1,
|
||||||
|
DrawUnder: uint8(d.Number("DrawUnder")) == 1,
|
||||||
|
OpenWarp: uint8(d.Number("OpenWarp")) == 1,
|
||||||
|
|
||||||
|
AutoMap: d.Number("AutoMap"),
|
||||||
|
}
|
||||||
|
|
||||||
|
inc()
|
||||||
|
|
||||||
|
records[i] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d objects", len(records))
|
||||||
|
|
||||||
|
r.Object.Details = records
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
// ObjectDetails stores all of the ObjectDetailRecords
|
||||||
|
type ObjectDetails map[int]*ObjectDetailsRecord
|
||||||
|
|
||||||
|
// An ObjectRecord represents the settings for one type of object from objects.txt
|
||||||
|
type ObjectDetailsRecord struct {
|
||||||
|
Index int // Line number in file, this is the actual index used for objects
|
||||||
|
FrameCount [8]int // how many frames does this mode have, 0 = skip
|
||||||
|
FrameDelta [8]int // what rate is the animation played at (256 = 100% speed)
|
||||||
|
LightDiameter [8]int
|
||||||
|
|
||||||
|
StartFrame [8]int
|
||||||
|
|
||||||
|
OrderFlag [8]int // 0 = object, 1 = floor, 2 = wall
|
||||||
|
Parm [8]int // unknown
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
|
||||||
|
// Don't use, get token from objtypes
|
||||||
|
token string // refers to what graphics this object uses
|
||||||
|
|
||||||
|
// Don't use, index by line number
|
||||||
|
id int //nolint:golint,stylecheck // unused, indexed by line number instead
|
||||||
|
SpawnMax int // unused?
|
||||||
|
TrapProbability int // unused
|
||||||
|
|
||||||
|
SizeX int
|
||||||
|
SizeY int
|
||||||
|
|
||||||
|
NTgtFX int // unknown
|
||||||
|
NTgtFY int // unknown
|
||||||
|
NTgtBX int // unknown
|
||||||
|
NTgtBY int // unknown
|
||||||
|
|
||||||
|
Orientation int // unknown (1=sw, 2=nw, 3=se, 4=ne)
|
||||||
|
Trans int // controls palette mapping
|
||||||
|
|
||||||
|
XOffset int // in pixels offset
|
||||||
|
YOffset int
|
||||||
|
|
||||||
|
TotalPieces int // selectable DCC components count
|
||||||
|
SubClass int // subclass of object:
|
||||||
|
// 1 = shrine
|
||||||
|
// 2 = obelisk
|
||||||
|
// 4 = portal
|
||||||
|
// 8 = container
|
||||||
|
// 16 = arcane sanctuary gateway
|
||||||
|
// 32 = well
|
||||||
|
// 64 = waypoint
|
||||||
|
// 128 = secret jails door
|
||||||
|
|
||||||
|
XSpace int // unknown
|
||||||
|
YSpace int
|
||||||
|
|
||||||
|
NameOffset int // pixels to offset the name from the animation pivot
|
||||||
|
|
||||||
|
OperateRange int // distance object can be used from, might be unused
|
||||||
|
ShrineFunction int // unused
|
||||||
|
|
||||||
|
Act int // what acts this object can appear in (15 = all three)
|
||||||
|
|
||||||
|
Damage int // amount of damage done by this (used depending on operatefn)
|
||||||
|
|
||||||
|
Left int // unknown, clickable bounding box?
|
||||||
|
Top int
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
|
||||||
|
OperateFn int // what function is called when the player clicks on the object
|
||||||
|
PopulateFn int // what function is used to spawn this object?
|
||||||
|
InitFn int // what function is run when the object is initialized?
|
||||||
|
ClientFn int // controls special audio-visual functions
|
||||||
|
|
||||||
|
// 'To ...' or 'trap door' when highlighting, not sure which is T/F
|
||||||
|
AutoMap int // controls how this object appears on the map
|
||||||
|
// 0 = it doesn't, rest of modes need to be analyzed
|
||||||
|
|
||||||
|
CycleAnimation [8]bool // probably whether animation loops
|
||||||
|
Selectable [8]bool // is this mode selectable
|
||||||
|
BlocksLight [8]bool
|
||||||
|
HasCollision [8]bool
|
||||||
|
HasAnimationMode [8]bool // 'Mode' in source, true if this mode is used
|
||||||
|
SelS [8]bool
|
||||||
|
IsAttackable bool // do we kick it when interacting
|
||||||
|
EnvEffect bool // unknown
|
||||||
|
IsDoor bool
|
||||||
|
BlockVisibility bool // only works with IsDoor
|
||||||
|
PreOperate bool // unknown
|
||||||
|
Draw bool // if false, object isn't drawn (shadow is still drawn and player can still select though)
|
||||||
|
SelHD bool // whether these DCC components are selectable
|
||||||
|
SelTR bool
|
||||||
|
SelLG bool
|
||||||
|
SelRA bool
|
||||||
|
SelLA bool
|
||||||
|
SelRH bool
|
||||||
|
SelLH bool
|
||||||
|
SelSH bool
|
||||||
|
MonsterOk bool // unknown
|
||||||
|
Restore bool // if true, object is stored in memory and will be retained if you leave and re-enter the area
|
||||||
|
Lockable bool
|
||||||
|
Gore bool // unknown, something with corpses
|
||||||
|
Sync bool // unknown
|
||||||
|
Flicker bool // light flickers if true
|
||||||
|
Beta bool // if true, appeared in the beta?
|
||||||
|
Overlay bool // unknown
|
||||||
|
CollisionSubst bool // unknown, controls some kind of special collision checking?
|
||||||
|
RestoreVirgins bool // if true, only restores unused objects (see Restore)
|
||||||
|
BlockMissile bool // if true, missiles collide with this
|
||||||
|
DrawUnder bool // if true, drawn as a floor tile is
|
||||||
|
OpenWarp bool // needs clarification, controls whether highlighting shows
|
||||||
|
|
||||||
|
LightRed byte // if lightdiameter is set, rgb of the light
|
||||||
|
LightGreen byte
|
||||||
|
LightBlue byte
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadObjectGroups loads the ObjectGroupRecords into ObjectGroups.
|
||||||
|
func objectGroupsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
|
||||||
|
records := make(ObjectGroups)
|
||||||
|
|
||||||
|
for d.Next() {
|
||||||
|
groupName := d.String("GroupName")
|
||||||
|
if groupName == expansionDataMarker {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
shrines, wells := d.Bool("Shrines"), d.Bool("Wells")
|
||||||
|
record := &ObjectGroupRecord{
|
||||||
|
GroupName: groupName,
|
||||||
|
Offset: d.Number("Offset"),
|
||||||
|
Members: createMembers(d, shrines || wells),
|
||||||
|
Shrines: shrines,
|
||||||
|
Wells: wells,
|
||||||
|
}
|
||||||
|
records[record.Offset] = record
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.Err != nil {
|
||||||
|
return d.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Loaded %d ObjectGroup records", len(records))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createMembers(d *d2txt.DataDictionary, shrinesOrWells bool) *[objectsGroupSize]ObjectGroupMember {
|
||||||
|
var members [objectsGroupSize]ObjectGroupMember
|
||||||
|
|
||||||
|
for i := 0; i < objectsGroupSize; i++ {
|
||||||
|
suffix := strconv.Itoa(i)
|
||||||
|
members[i].ID = d.Number("ID" + suffix)
|
||||||
|
|
||||||
|
members[i].Density = d.Number("DENSITY" + suffix)
|
||||||
|
if members[i].Density < memberDensityMin || members[i].Density > memberDensityMax {
|
||||||
|
panic(fmt.Sprintf("Invalid object group member density: %v, in group: %v",
|
||||||
|
members[i].Density, d.String("GroupName"))) // Vanilla crashes when density is over 125.
|
||||||
|
}
|
||||||
|
|
||||||
|
if shrinesOrWells && members[i].Density != 0 {
|
||||||
|
panic(fmt.Sprintf("Shrine and well object groups must have densities set to 0, in group: %v", d.String("GroupName")))
|
||||||
|
}
|
||||||
|
|
||||||
|
members[i].Probability = d.Number("PROB" + suffix)
|
||||||
|
if members[i].Probability < memberProbabilityMin || members[i].Probability > memberProbabilityMax {
|
||||||
|
panic(fmt.Sprintf("Invalid object group member probability: %v, in group: %v",
|
||||||
|
members[i].Probability, d.String("GroupName")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &members
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
const (
|
||||||
|
objectsGroupSize = 7
|
||||||
|
memberDensityMin = 0
|
||||||
|
memberDensityMax = 125
|
||||||
|
memberProbabilityMin = 0
|
||||||
|
memberProbabilityMax = 100
|
||||||
|
expansionDataMarker = "EXPANSION"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ObjectGroups stores the ObjectGroupRecords.
|
||||||
|
type ObjectGroups map[int]*ObjectGroupRecord
|
||||||
|
|
||||||
|
// ObjectGroupRecord represents a single line in objgroup.txt.
|
||||||
|
// Information has been gathered from [https://d2mods.info/forum/kb/viewarticle?a=394].
|
||||||
|
type ObjectGroupRecord struct {
|
||||||
|
// GroupName is the name of the group.
|
||||||
|
GroupName string
|
||||||
|
|
||||||
|
// Offset is the ID of the group, referred to by Levels.txt.
|
||||||
|
Offset int
|
||||||
|
|
||||||
|
// Members are the objects in the group.
|
||||||
|
Members *[objectsGroupSize]ObjectGroupMember
|
||||||
|
|
||||||
|
// Shrines determines whether this is a group of shrines.
|
||||||
|
// Note: for shrine groups, densities must be set to 0.
|
||||||
|
Shrines bool
|
||||||
|
|
||||||
|
// Wells determines whether this is a group of wells.
|
||||||
|
// Note: for wells groups, densities must be set to 0.
|
||||||
|
Wells bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ObjectGroupMember represents a member of an object group.
|
||||||
|
type ObjectGroupMember struct {
|
||||||
|
// ID is the ID of the object.
|
||||||
|
ID int
|
||||||
|
|
||||||
|
// Density is how densely the level is filled with the object.
|
||||||
|
// Must be below 125.
|
||||||
|
Density int
|
||||||
|
|
||||||
|
// Probability is the probability of this particular object being spawned.
|
||||||
|
// The value is a percentage.
|
||||||
|
Probability int
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IndexedObjects is a slice of object records for quick lookups.
|
||||||
|
// nil checks should be done for uninitialized values at each level.
|
||||||
|
// [Act 1-5][Type 1-2][ID 0-855]
|
||||||
|
//nolint:gochecknoglobals // Currently global by design
|
||||||
|
type IndexedObjects [][][]*ObjectLookupRecord
|
||||||
|
|
||||||
|
// ObjectLookupRecord is a representation of a row from objectLookups.txt
|
||||||
|
type ObjectLookupRecord struct {
|
||||||
|
Act int
|
||||||
|
Type d2enum.ObjectType
|
||||||
|
Id int //nolint:golint,stylecheck // ID is the right key
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
ObjectsTxtId int //nolint:golint,stylecheck // ID is the right key
|
||||||
|
MonstatsTxtId int //nolint:golint,stylecheck // ID is the right key
|
||||||
|
Direction int
|
||||||
|
Base string
|
||||||
|
Token string
|
||||||
|
Mode string
|
||||||
|
Class string
|
||||||
|
HD string
|
||||||
|
TR string
|
||||||
|
LG string
|
||||||
|
RA string
|
||||||
|
LA string
|
||||||
|
RH string
|
||||||
|
LH string
|
||||||
|
SH string
|
||||||
|
S1 string
|
||||||
|
S2 string
|
||||||
|
S3 string
|
||||||
|
S4 string
|
||||||
|
S5 string
|
||||||
|
S6 string
|
||||||
|
S7 string
|
||||||
|
S8 string
|
||||||
|
ColorMap string
|
||||||
|
Index int
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,45 @@
|
||||||
|
package d2records
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
|
||||||
|
|
||||||
|
testify "github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Verify the lookup returns the right object after indexing.
|
||||||
|
func TestIndexObjects(t *testing.T) {
|
||||||
|
assert := testify.New(t)
|
||||||
|
|
||||||
|
r, _ := NewRecordManager()
|
||||||
|
|
||||||
|
testObjects := []ObjectLookupRecord{
|
||||||
|
{Act: 1, Type: d2enum.ObjectTypeCharacter, Id: 0, Description: "Act1CharId0"},
|
||||||
|
{Act: 1, Type: d2enum.ObjectTypeCharacter, Id: 1, Description: "Act1CharId1"},
|
||||||
|
{Act: 1, Type: d2enum.ObjectTypeCharacter, Id: 2, Description: "Act1CharId2"},
|
||||||
|
{Act: 1, Type: d2enum.ObjectTypeCharacter, Id: 3, Description: "Act1CharId3"},
|
||||||
|
{Act: 1, Type: d2enum.ObjectTypeItem, Id: 0, Description: "Act1ItemId0"},
|
||||||
|
{Act: 1, Type: d2enum.ObjectTypeItem, Id: 1, Description: "Act1ItemId1"},
|
||||||
|
{Act: 1, Type: d2enum.ObjectTypeItem, Id: 2, Description: "Act1ItemId2"},
|
||||||
|
{Act: 1, Type: d2enum.ObjectTypeItem, Id: 3, Description: "Act1ItemId3"},
|
||||||
|
{Act: 2, Type: d2enum.ObjectTypeCharacter, Id: 0, Description: "Act2CharId0"},
|
||||||
|
{Act: 2, Type: d2enum.ObjectTypeCharacter, Id: 1, Description: "Act2CharId1"},
|
||||||
|
{Act: 2, Type: d2enum.ObjectTypeCharacter, Id: 2, Description: "Act2CharId2"},
|
||||||
|
{Act: 2, Type: d2enum.ObjectTypeCharacter, Id: 3, Description: "Act2CharId3"},
|
||||||
|
{Act: 2, Type: d2enum.ObjectTypeItem, Id: 0, Description: "Act2ItemId0"},
|
||||||
|
{Act: 2, Type: d2enum.ObjectTypeItem, Id: 1, Description: "Act2ItemId1"},
|
||||||
|
{Act: 2, Type: d2enum.ObjectTypeItem, Id: 2, Description: "Act2ItemId2"},
|
||||||
|
{Act: 2, Type: d2enum.ObjectTypeItem, Id: 3, Description: "Act2ItemId3"},
|
||||||
|
}
|
||||||
|
|
||||||
|
r.initObjectRecords(testObjects)
|
||||||
|
|
||||||
|
typeCharacter := int(d2enum.ObjectTypeCharacter)
|
||||||
|
typeItem := int(d2enum.ObjectTypeItem)
|
||||||
|
|
||||||
|
assert.Equal("Act1CharId2", r.lookupObject(1, typeCharacter, 2).Description)
|
||||||
|
assert.Equal("Act1ItemId0", r.lookupObject(1, typeItem, 0).Description)
|
||||||
|
assert.Equal("Act2CharId3", r.lookupObject(2, typeCharacter, 3).Description)
|
||||||
|
assert.Equal("Act2ItemId1", r.lookupObject(2, typeItem, 1).Description)
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue