mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2025-01-11 11:57:44 -05:00
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"
|
||||
AutoMagic = "/data/global/excel/automagic.txt"
|
||||
BodyLocations = "/data/global/excel/bodylocs.txt"
|
||||
Events = "/data/global/excel/events.txt"
|
||||
Properties = "/data/global/excel/Properties.txt"
|
||||
Hireling = "/data/global/excel/hireling.txt"
|
||||
DifficultyLevels = "/data/global/excel/difficultylevels.txt"
|
||||
|
@ -5,6 +5,12 @@ import (
|
||||
"image/color"
|
||||
"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/d2fileformats/d2dat"
|
||||
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dc6"
|
||||
@ -37,6 +43,106 @@ type AssetManager struct {
|
||||
fonts d2interface.Cache
|
||||
palettes 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
|
||||
@ -244,6 +350,37 @@ func (am *AssetManager) LoadPaletteTransform(path string) (*d2pl2.PL2, error) {
|
||||
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
|
||||
func (am *AssetManager) loadDC6(path string,
|
||||
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/d2loader"
|
||||
"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
|
||||
func NewAssetManager(config *d2config.Configuration) (*AssetManager, error) {
|
||||
manager := &AssetManager{
|
||||
d2loader.NewLoader(config),
|
||||
d2cache.CreateCache(animationBudget),
|
||||
d2cache.CreateCache(tableBudget),
|
||||
d2cache.CreateCache(animationBudget),
|
||||
d2cache.CreateCache(fontBudget),
|
||||
d2cache.CreateCache(paletteBudget),
|
||||
d2cache.CreateCache(paletteTransformBudget),
|
||||
&d2records.RecordManager{},
|
||||
}
|
||||
|
||||
err := manager.init()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return manager, nil
|
||||
|
89
d2core/d2records/automagic_loader.go
Normal file
89
d2core/d2records/automagic_loader.go
Normal file
@ -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
|
||||
}
|
121
d2core/d2records/automagic_record.go
Normal file
121
d2core/d2records/automagic_record.go
Normal file
@ -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
|
||||
}
|
47
d2core/d2records/automap_loader.go
Normal file
47
d2core/d2records/automap_loader.go
Normal file
@ -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
|
||||
}
|
50
d2core/d2records/automap_record.go
Normal file
50
d2core/d2records/automap_record.go
Normal file
@ -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
|
29
d2core/d2records/body_locations_loader.go
Normal file
29
d2core/d2records/body_locations_loader.go
Normal file
@ -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
|
||||
}
|
10
d2core/d2records/body_locations_record.go
Normal file
10
d2core/d2records/body_locations_record.go
Normal file
@ -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
|
||||
}
|
38
d2core/d2records/books_loader.go
Normal file
38
d2core/d2records/books_loader.go
Normal file
@ -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
|
||||
}
|
19
d2core/d2records/books_record.go
Normal file
19
d2core/d2records/books_record.go
Normal file
@ -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
|
||||
}
|
53
d2core/d2records/calculations_loader.go
Normal file
53
d2core/d2records/calculations_loader.go
Normal file
@ -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
|
||||
}
|
12
d2core/d2records/calculations_record.go
Normal file
12
d2core/d2records/calculations_record.go
Normal file
@ -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
|
||||
}
|
145
d2core/d2records/charstats_loader.go
Normal file
145
d2core/d2records/charstats_loader.go
Normal file
@ -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
|
||||
}
|
56
d2core/d2records/charstats_record.go
Normal file
56
d2core/d2records/charstats_record.go
Normal file
@ -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
|
||||
}
|
29
d2core/d2records/component_codes_loader.go
Normal file
29
d2core/d2records/component_codes_loader.go
Normal file
@ -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
|
||||
}
|
11
d2core/d2records/component_codes_record.go
Normal file
11
d2core/d2records/component_codes_record.go
Normal file
@ -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
|
||||
}
|
7
d2core/d2records/constants.go
Normal file
7
d2core/d2records/constants.go
Normal file
@ -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
|
||||
)
|
184
d2core/d2records/cubemain_loader.go
Normal file
184
d2core/d2records/cubemain_loader.go
Normal file
@ -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, "\""), ",")
|
||||
}
|
136
d2core/d2records/cubemain_record.go
Normal file
136
d2core/d2records/cubemain_record.go
Normal file
@ -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
|
||||
}
|
44
d2core/d2records/difficultylevels_loader.go
Normal file
44
d2core/d2records/difficultylevels_loader.go
Normal file
@ -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
|
||||
}
|
86
d2core/d2records/difficultylevels_record.go
Normal file
86
d2core/d2records/difficultylevels_record.go
Normal file
@ -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
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
}
|
30
d2core/d2records/elemtype_loader.go
Normal file
30
d2core/d2records/elemtype_loader.go
Normal file
@ -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
|
||||
}
|
13
d2core/d2records/elemtype_record.go
Normal file
13
d2core/d2records/elemtype_record.go
Normal file
@ -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
|
||||
}
|
30
d2core/d2records/events_loader.go
Normal file
30
d2core/d2records/events_loader.go
Normal file
@ -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
|
||||
}
|
9
d2core/d2records/events_record.go
Normal file
9
d2core/d2records/events_record.go
Normal file
@ -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
|
||||
}
|
79
d2core/d2records/experience_loader.go
Normal file
79
d2core/d2records/experience_loader.go
Normal file
@ -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
|
||||
}
|
18
d2core/d2records/experience_record.go
Normal file
18
d2core/d2records/experience_record.go
Normal file
@ -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
|
||||
}
|
70
d2core/d2records/gems_loader.go
Normal file
70
d2core/d2records/gems_loader.go
Normal file
@ -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
|
||||
}
|
50
d2core/d2records/gems_record.go
Normal file
50
d2core/d2records/gems_record.go
Normal file
@ -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
|
||||
}
|
100
d2core/d2records/hireling_loader.go
Normal file
100
d2core/d2records/hireling_loader.go
Normal file
@ -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
|
||||
}
|
81
d2core/d2records/hireling_record.go
Normal file
81
d2core/d2records/hireling_record.go
Normal file
@ -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
|
||||
}
|
140
d2core/d2records/inventory_loader.go
Normal file
140
d2core/d2records/inventory_loader.go
Normal file
@ -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
|
||||
}
|
32
d2core/d2records/inventory_record.go
Normal file
32
d2core/d2records/inventory_record.go
Normal file
@ -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
|
||||
}
|
154
d2core/d2records/item_affix_loader.go
Normal file
154
d2core/d2records/item_affix_loader.go
Normal file
@ -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
|
||||
}
|
93
d2core/d2records/item_affix_record.go
Normal file
93
d2core/d2records/item_affix_record.go
Normal file
@ -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
|
||||
}
|
26
d2core/d2records/item_armor_loader.go
Normal file
26
d2core/d2records/item_armor_loader.go
Normal file
@ -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
|
||||
}
|
229
d2core/d2records/item_common_loader.go
Normal file
229
d2core/d2records/item_common_loader.go
Normal file
@ -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
|
||||
}
|
154
d2core/d2records/item_common_record.go
Normal file
154
d2core/d2records/item_common_record.go
Normal file
@ -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
|
||||
}
|
23
d2core/d2records/item_misc_loader.go
Normal file
23
d2core/d2records/item_misc_loader.go
Normal file
@ -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
|
||||
}
|
52
d2core/d2records/item_quality_loader.go
Normal file
52
d2core/d2records/item_quality_loader.go
Normal file
@ -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
|
||||
}
|
36
d2core/d2records/item_quality_record.go
Normal file
36
d2core/d2records/item_quality_record.go
Normal file
@ -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
|
||||
}
|
64
d2core/d2records/item_ratio_loader.go
Normal file
64
d2core/d2records/item_ratio_loader.go
Normal file
@ -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
|
||||
}
|
32
d2core/d2records/item_ratio_record.go
Normal file
32
d2core/d2records/item_ratio_record.go
Normal file
@ -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
|
||||
}
|
161
d2core/d2records/item_types_loader.go
Normal file
161
d2core/d2records/item_types_loader.go
Normal file
@ -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
|
||||
}
|
196
d2core/d2records/item_types_record.go
Normal file
196
d2core/d2records/item_types_record.go
Normal file
@ -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
|
||||
}
|
23
d2core/d2records/item_weapons_loader.go
Normal file
23
d2core/d2records/item_weapons_loader.go
Normal file
@ -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
|
||||
}
|
105
d2core/d2records/itemstatcost_loader.go
Normal file
105
d2core/d2records/itemstatcost_loader.go
Normal file
@ -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
|
||||
}
|
99
d2core/d2records/itemstatcost_record.go
Normal file
99
d2core/d2records/itemstatcost_record.go
Normal file
@ -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
|
174
d2core/d2records/level_details_loader.go
Normal file
174
d2core/d2records/level_details_loader.go
Normal file
@ -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
|
||||
}
|
362
d2core/d2records/level_details_record.go
Normal file
362
d2core/d2records/level_details_record.go
Normal file
@ -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
|
||||
|
||||
}
|
34
d2core/d2records/level_maze_loader.go
Normal file
34
d2core/d2records/level_maze_loader.go
Normal file
@ -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
|
||||
}
|
34
d2core/d2records/level_maze_record.go
Normal file
34
d2core/d2records/level_maze_record.go
Normal file
@ -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
|
||||
}
|
56
d2core/d2records/level_presets_loader.go
Normal file
56
d2core/d2records/level_presets_loader.go
Normal file
@ -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
|
||||
}
|
29
d2core/d2records/level_presets_record.go
Normal file
29
d2core/d2records/level_presets_record.go
Normal file
@ -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
|
||||
}
|
50
d2core/d2records/level_substitutions_loader.go
Normal file
50
d2core/d2records/level_substitutions_loader.go
Normal file
@ -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
|
||||
}
|
62
d2core/d2records/level_substitutions_record.go
Normal file
62
d2core/d2records/level_substitutions_record.go
Normal file
@ -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
|
||||
}
|
68
d2core/d2records/level_types_loader.go
Normal file
68
d2core/d2records/level_types_loader.go
Normal file
@ -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
|
||||
}
|
15
d2core/d2records/level_types_record.go
Normal file
15
d2core/d2records/level_types_record.go
Normal file
@ -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
|
||||
}
|
40
d2core/d2records/level_warp_loader.go
Normal file
40
d2core/d2records/level_warp_loader.go
Normal file
@ -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
|
||||
}
|
22
d2core/d2records/level_warp_record.go
Normal file
22
d2core/d2records/level_warp_record.go
Normal file
@ -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
|
||||
}
|
309
d2core/d2records/missiles_loader.go
Normal file
309
d2core/d2records/missiles_loader.go
Normal file
@ -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
|
||||
}
|
199
d2core/d2records/missiles_record.go
Normal file
199
d2core/d2records/missiles_record.go
Normal file
@ -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
|
||||
|
||||
}
|
29
d2core/d2records/monster_ai_loader.go
Normal file
29
d2core/d2records/monster_ai_loader.go
Normal file
@ -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
|
||||
}
|
9
d2core/d2records/monster_ai_record.go
Normal file
9
d2core/d2records/monster_ai_record.go
Normal file
@ -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
|
||||
}
|
58
d2core/d2records/monster_equipment_loader.go
Normal file
58
d2core/d2records/monster_equipment_loader.go
Normal file
@ -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
|
||||
}
|
38
d2core/d2records/monster_equipment_record.go
Normal file
38
d2core/d2records/monster_equipment_record.go
Normal file
@ -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
|
||||
}
|
49
d2core/d2records/monster_levels_loader.go
Normal file
49
d2core/d2records/monster_levels_loader.go
Normal file
@ -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
|
||||
}
|
62
d2core/d2records/monster_levels_record.go
Normal file
62
d2core/d2records/monster_levels_record.go
Normal file
@ -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
|
||||
}
|
31
d2core/d2records/monster_mode_loader.go
Normal file
31
d2core/d2records/monster_mode_loader.go
Normal file
@ -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
|
||||
}
|
11
d2core/d2records/monster_mode_record.go
Normal file
11
d2core/d2records/monster_mode_record.go
Normal file
@ -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
|
||||
}
|
26
d2core/d2records/monster_placement_loader.go
Normal file
26
d2core/d2records/monster_placement_loader.go
Normal file
@ -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
|
||||
}
|
7
d2core/d2records/monster_placement_record.go
Normal file
7
d2core/d2records/monster_placement_record.go
Normal file
@ -0,0 +1,7 @@
|
||||
package d2records
|
||||
|
||||
// MonsterPlacements stores the MonsterPlacementRecords.
|
||||
type MonsterPlacements []MonsterPlacementRecord
|
||||
|
||||
// MonsterPlacementRecord represents a line from MonPlace.txt.
|
||||
type MonsterPlacementRecord string
|
31
d2core/d2records/monster_preset_loader.go
Normal file
31
d2core/d2records/monster_preset_loader.go
Normal file
@ -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
|
||||
}
|
4
d2core/d2records/monster_preset_record.go
Normal file
4
d2core/d2records/monster_preset_record.go
Normal file
@ -0,0 +1,4 @@
|
||||
package d2records
|
||||
|
||||
// MonPresets stores monster presets
|
||||
type MonPresets map[int32][]string
|
66
d2core/d2records/monster_property_loader.go
Normal file
66
d2core/d2records/monster_property_loader.go
Normal file
@ -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
|
||||
}
|
36
d2core/d2records/monster_property_record.go
Normal file
36
d2core/d2records/monster_property_record.go
Normal file
@ -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
|
||||
}
|
41
d2core/d2records/monster_sequence_loader.go
Normal file
41
d2core/d2records/monster_sequence_loader.go
Normal file
@ -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
|
||||
}
|
31
d2core/d2records/monster_sequence_record.go
Normal file
31
d2core/d2records/monster_sequence_record.go
Normal file
@ -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
|
||||
}
|
67
d2core/d2records/monster_sound_loader.go
Normal file
67
d2core/d2records/monster_sound_loader.go
Normal file
@ -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
|
||||
}
|
100
d2core/d2records/monster_sound_record.go
Normal file
100
d2core/d2records/monster_sound_record.go
Normal file
@ -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
|
||||
}
|
181
d2core/d2records/monster_stats2_loader.go
Normal file
181
d2core/d2records/monster_stats2_loader.go
Normal file
@ -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
|
||||
}
|
164
d2core/d2records/monster_stats2_record.go
Normal file
164
d2core/d2records/monster_stats2_record.go
Normal file
@ -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
|
||||
|
||||
}
|
282
d2core/d2records/monster_stats_loader.go
Normal file
282
d2core/d2records/monster_stats_loader.go
Normal file
@ -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
|
||||
}
|
678
d2core/d2records/monster_stats_record.go
Normal file
678
d2core/d2records/monster_stats_record.go
Normal file
@ -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
|
||||
|
||||
}
|
||||
)
|
48
d2core/d2records/monster_super_unique_loader.go
Normal file
48
d2core/d2records/monster_super_unique_loader.go
Normal file
@ -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
|
||||
}
|
117
d2core/d2records/monster_super_unique_record.go
Normal file
117
d2core/d2records/monster_super_unique_record.go
Normal file
@ -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
|
||||
}
|
33
d2core/d2records/monster_type_loader.go
Normal file
33
d2core/d2records/monster_type_loader.go
Normal file
@ -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
|
||||
}
|
17
d2core/d2records/monster_type_record.go
Normal file
17
d2core/d2records/monster_type_record.go
Normal file
@ -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
|
||||
}
|
59
d2core/d2records/monster_unique_modifiers_loader.go
Normal file
59
d2core/d2records/monster_unique_modifiers_loader.go
Normal file
@ -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
|
||||
}
|
58
d2core/d2records/monster_unique_modifiers_record.go
Normal file
58
d2core/d2records/monster_unique_modifiers_record.go
Normal file
@ -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
|
||||
}
|
72
d2core/d2records/npc_loader.go
Normal file
72
d2core/d2records/npc_loader.go
Normal file
@ -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
|
||||
}
|
38
d2core/d2records/npc_record.go
Normal file
38
d2core/d2records/npc_record.go
Normal file
@ -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
|
||||
}
|
239
d2core/d2records/object_details_loader.go
Normal file
239
d2core/d2records/object_details_loader.go
Normal file
@ -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
|
||||
}
|
116
d2core/d2records/object_details_record.go
Normal file
116
d2core/d2records/object_details_record.go
Normal file
@ -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
|
||||
}
|
66
d2core/d2records/object_groups_loader.go
Normal file
66
d2core/d2records/object_groups_loader.go
Normal file
@ -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
|
||||
}
|
48
d2core/d2records/object_groups_record.go
Normal file
48
d2core/d2records/object_groups_record.go
Normal file
@ -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
|
||||
}
|
45
d2core/d2records/object_lookup_record.go
Normal file
45
d2core/d2records/object_lookup_record.go
Normal file
@ -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
|
||||
}
|
7902
d2core/d2records/object_lookup_record_data.go
Normal file
7902
d2core/d2records/object_lookup_record_data.go
Normal file
File diff suppressed because it is too large
Load Diff
45
d2core/d2records/object_lookup_record_test.go
Normal file
45
d2core/d2records/object_lookup_record_test.go
Normal file
@ -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
Block a user