Added RecordManager implementation to remove d2datadict singletons (#736)

* Added RecordManager implementation to remove d2datadict singletons

* fix object lookup test
This commit is contained in:
gravestench 2020-09-19 11:33:40 -07:00 committed by GitHub
parent ef0fbc0581
commit 271673851a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
142 changed files with 18916 additions and 1 deletions

View File

@ -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"

View File

@ -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) {

View File

@ -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

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

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

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

View 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

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

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

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

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

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

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

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

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

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

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

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

View 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, "\""), ",")
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

@ -0,0 +1,7 @@
package d2records
// MonsterPlacements stores the MonsterPlacementRecords.
type MonsterPlacements []MonsterPlacementRecord
// MonsterPlacementRecord represents a line from MonPlace.txt.
type MonsterPlacementRecord string

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

View File

@ -0,0 +1,4 @@
package d2records
// MonPresets stores monster presets
type MonPresets map[int32][]string

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

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

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

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

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

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

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

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

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

View 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
// dont 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=Cant 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 dont 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 dont do it unless you know
// what youre 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
}
)

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

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

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

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

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

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

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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

View 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