Removing d2datadict singletons (#738)

* Remove weapons, armor, misc, itemCommon, itemTyps datadict singletons

- removed loader calls from d2app
- removed the HeroObjects singleton from `d2core/d2inventory`
- added an InventoryItemFactory in d2inventory
- package-level functions that use data records are now methods of the InventoryItemFactory
- renamed ItemGenerator in d2item to ItemFactory
- package-level functions that use records are now methods of ItemFactory
- d2map.MapEntityFactory now has an item factory instance for creating items
- fixed a bug in unique item record loader where it loaded an empty record
- added a PlayerStateFactory for creating a player state (uses the asset manager)
- updated the test inventory/equipment code in d2player to handle errors from the ItemFactory
- character select and character creation screens have a player state and inventory item factory
- updated item tests to use the item factory

* minor edit

* Removed d2datadict.Experience singleton

added a HeroStatsFactory, much like the other factories. The factory  gets an
asset manager reference in order to use data records.

* removed d2datadict.AutoMagic singleton

* removed d2datadict.AutoMap singleton

* removed d2datadict.BodyLocations singleton

* removed d2datadict.Books singleton

* Removed singletons for level records

- removed loader calls in d2app
- changed type references from d2datadict to d2records
- added a `MapGenerator` in d2mapgen which uses thew asset manager and map engine
- package-level map generation functions are now MapGenerator methods
- `d2datadict.GetLevelDetails(id int)` is now a method of the RecordManager

* remove SkillCalc and MissileCalc singletons

* Removed CharStats and ItemStatCost singletons

- added an ItemStatFactory which uses the asset manager to create stats
- package-level functions for stats in d2item are now StatFactory methods
- changed type references from d2datadict to d2records
- `d2player.GetAllPlayerStates` is now a method of the `PlayerStateFactory`

* Removed DkillDesc and Skills singletons from d2datadict

- removed loader calls from d2app
- diablo2stats.Stat instances are given a reference to the factory for doing record lookups

* update the stats test to use mock a asset manager and stat factory

* fixed diablo2stats tests and diablo2item tests

* removed CompCodes singleton from d2datadict

* remove cubemain singleton from d2datadict

* removed DifficultyLevels singleton from d2datadict

* removed ElemTypes singleton from d2datadict

* removed events.go loader from d2datadict (was unused)

* removed Gems singleton from d2datadict

* removed Hireling and Inventory singletons from d2datadict

* removed MagicPrefix and MagicSuffix singletons from d2datadict

* removed ItemRatios singleton from d2datadict

* removed Missiles singleton from d2datadict

* removed MonModes singleton

* Removed all monster and npc singletons from d2datadict

- MapStamp instances now get a reference to their factory for doing record lookups

* removed SoundEntry and SoundEnviron singletons from d2datadict
This commit is contained in:
gravestench 2020-09-20 14:52:01 -07:00 committed by GitHub
parent a4e9797431
commit fc87b2be7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
110 changed files with 2372 additions and 9693 deletions

View File

@ -31,7 +31,6 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2config"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2inventory"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
"github.com/OpenDiablo2/OpenDiablo2/d2game/d2gamescreen"
@ -195,8 +194,6 @@ func (a *App) initialize() error {
return err
}
d2inventory.LoadHeroObjects()
a.ui.Initialize()
return nil
@ -226,73 +223,25 @@ func (a *App) loadDataDict() error {
path string
loader func(data []byte)
}{
{d2resource.LevelType, d2datadict.LoadLevelTypes},
{d2resource.LevelPreset, d2datadict.LoadLevelPresets},
{d2resource.LevelWarp, d2datadict.LoadLevelWarps},
{d2resource.ObjectType, d2datadict.LoadObjectTypes},
{d2resource.ObjectDetails, d2datadict.LoadObjects},
{d2resource.Weapons, d2datadict.LoadWeapons},
{d2resource.Armor, d2datadict.LoadArmors},
{d2resource.Books, d2datadict.LoadBooks},
{d2resource.Misc, d2datadict.LoadMiscItems},
{d2resource.UniqueItems, d2datadict.LoadUniqueItems},
{d2resource.Missiles, d2datadict.LoadMissiles},
{d2resource.SoundSettings, d2datadict.LoadSounds},
{d2resource.AnimationData, d2data.LoadAnimationData},
{d2resource.MonStats, d2datadict.LoadMonStats},
{d2resource.MonStats2, d2datadict.LoadMonStats2},
{d2resource.MonPreset, d2datadict.LoadMonPresets},
{d2resource.MonProp, d2datadict.LoadMonProps},
{d2resource.MonType, d2datadict.LoadMonTypes},
{d2resource.MonMode, d2datadict.LoadMonModes},
{d2resource.MagicPrefix, d2datadict.LoadMagicPrefix},
{d2resource.MagicSuffix, d2datadict.LoadMagicSuffix},
{d2resource.ItemStatCost, d2datadict.LoadItemStatCosts},
{d2resource.ItemRatio, d2datadict.LoadItemRatios},
{d2resource.Overlays, d2datadict.LoadOverlays},
{d2resource.CharStats, d2datadict.LoadCharStats},
{d2resource.Hireling, d2datadict.LoadHireling},
{d2resource.Experience, d2datadict.LoadExperienceBreakpoints},
{d2resource.Gems, d2datadict.LoadGems},
{d2resource.QualityItems, d2datadict.LoadQualityItems},
{d2resource.Runes, d2datadict.LoadRunewords},
{d2resource.DifficultyLevels, d2datadict.LoadDifficultyLevels},
{d2resource.AutoMap, d2datadict.LoadAutoMaps},
{d2resource.LevelDetails, d2datadict.LoadLevelDetails},
{d2resource.LevelMaze, d2datadict.LoadLevelMazeDetails},
{d2resource.LevelSubstitutions, d2datadict.LoadLevelSubstitutions},
{d2resource.CubeRecipes, d2datadict.LoadCubeRecipes},
{d2resource.SuperUniques, d2datadict.LoadSuperUniques},
{d2resource.Inventory, d2datadict.LoadInventory},
{d2resource.Skills, d2datadict.LoadSkills},
{d2resource.SkillCalc, d2datadict.LoadSkillCalculations},
{d2resource.MissileCalc, d2datadict.LoadMissileCalculations},
{d2resource.Properties, d2datadict.LoadProperties},
{d2resource.SkillDesc, d2datadict.LoadSkillDescriptions},
{d2resource.ItemTypes, d2datadict.LoadItemTypes},
{d2resource.BodyLocations, d2datadict.LoadBodyLocations},
{d2resource.Sets, d2datadict.LoadSetRecords},
{d2resource.SetItems, d2datadict.LoadSetItems},
{d2resource.AutoMagic, d2datadict.LoadAutoMagicRecords},
{d2resource.TreasureClass, d2datadict.LoadTreasureClassRecords},
{d2resource.States, d2datadict.LoadStates},
{d2resource.SoundEnvirons, d2datadict.LoadSoundEnvirons},
{d2resource.Shrines, d2datadict.LoadShrines},
{d2resource.ElemType, d2datadict.LoadElemTypes},
{d2resource.PlrMode, d2datadict.LoadPlrModes},
{d2resource.PetType, d2datadict.LoadPetTypes},
{d2resource.NPC, d2datadict.LoadNPCs},
{d2resource.MonsterUniqueModifier, d2datadict.LoadMonsterUniqueModifiers},
{d2resource.MonsterEquipment, d2datadict.LoadMonsterEquipment},
{d2resource.UniqueAppellation, d2datadict.LoadUniqueAppellations},
{d2resource.MonsterLevel, d2datadict.LoadMonsterLevels},
{d2resource.MonsterSound, d2datadict.LoadMonsterSounds},
{d2resource.MonsterSequence, d2datadict.LoadMonsterSequences},
{d2resource.PlayerClass, d2datadict.LoadPlayerClasses},
{d2resource.MonsterPlacement, d2datadict.LoadMonsterPlacements},
{d2resource.ObjectGroup, d2datadict.LoadObjectGroups},
{d2resource.CompCode, d2datadict.LoadComponentCodes},
{d2resource.MonsterAI, d2datadict.LoadMonsterAI},
{d2resource.RarePrefix, d2datadict.LoadRareItemPrefixRecords},
{d2resource.RareSuffix, d2datadict.LoadRareItemSuffixRecords},
}
@ -308,8 +257,6 @@ func (a *App) loadDataDict() error {
entry.loader(data)
}
d2datadict.LoadItemEquivalencies() // depends on ItemCommon and ItemTypes
return nil
}
@ -683,16 +630,23 @@ func updateInitError(target d2interface.Surface) error {
func (a *App) ToMainMenu() {
buildInfo := d2gamescreen.BuildInfo{Branch: a.gitBranch, Commit: a.gitCommit}
mainMenu := d2gamescreen.CreateMainMenu(a, a.asset, a.renderer, a.inputManager, a.audio, a.ui,
buildInfo)
mainMenu, err := d2gamescreen.CreateMainMenu(a, a.asset, a.renderer, a.inputManager, a.audio, a.ui, buildInfo)
if err != nil {
log.Print(err)
return
}
a.screen.SetNextScreen(mainMenu)
}
// ToSelectHero forces the game to transition to the Select Hero (create character) screen
func (a *App) ToSelectHero(connType d2clientconnectiontype.ClientConnectionType, host string) {
selectHero := d2gamescreen.CreateSelectHeroClass(a, a.asset, a.renderer, a.audio, a.ui,
connType, host)
selectHero, err := d2gamescreen.CreateSelectHeroClass(a, a.asset, a.renderer, a.audio, a.ui, connType, host)
if err != nil {
log.Print(err)
return
}
a.screen.SetNextScreen(selectHero)
}
@ -719,9 +673,12 @@ func (a *App) ToCharacterSelect(connType d2clientconnectiontype.ClientConnection
// ToMapEngineTest forces the game to transition to the map engine test screen
func (a *App) ToMapEngineTest(region, level int) {
met := d2gamescreen.CreateMapEngineTest(region, level, a.asset, a.terminal, a.renderer,
a.inputManager,
a.audio, a.screen)
met, err := d2gamescreen.CreateMapEngineTest(region, level, a.asset, a.terminal, a.renderer, a.inputManager, a.audio, a.screen)
if err != nil {
return
log.Print(err)
}
a.screen.SetNextScreen(met)
}

View File

@ -1,17 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
)
// Armors stores all of the ArmorRecords
//nolint:gochecknoglobals // Currently global by design, only written once
var Armors map[string]*ItemCommonRecord
// LoadArmors loads entries from armor.txt as ItemCommonRecords
func LoadArmors(file []byte) {
Armors = LoadCommonItems(file, d2enum.InventoryItemTypeArmor)
log.Printf("Loaded %d armors", len(Armors))
}

View File

@ -1,206 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// 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
}
// AutoMagic has all of the AutoMagicRecords, used for generating magic properties for spawned items
var AutoMagic []*AutoMagicRecord //nolint:gochecknoglobals // Currently global by design, only written once
// LoadAutoMagicRecords loads AutoMagic records from automagic.txt
func LoadAutoMagicRecords(file []byte) {
AutoMagic = make([]*AutoMagicRecord, 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,
}
d := d2txt.LoadDataDictionary(file)
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"),
}
AutoMagic = append(AutoMagic, record)
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d AutoMagic records", len(AutoMagic))
}

View File

@ -1,98 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// 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.
//nolint:gochecknoglobals // Current design is to have these global
var AutoMaps []*AutoMapRecord
// LoadAutoMaps populates AutoMaps with the data from AutoMap.txt.
// It also amends a duplicate field (column) name in that data.
func LoadAutoMaps(file []byte) {
AutoMaps = make([]*AutoMapRecord, 0)
var frameFields = []string{"Cel1", "Cel2", "Cel3", "Cel4"}
// Split file by newlines and tabs
d := d2txt.LoadDataDictionary(file)
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"),
//Type1: d.String("Type1"),
//Type2: d.String("Type2"),
//Type3: d.String("Type3"),
//Type4: d.String("Type4"),
// Note: I commented these out for now because they supposedly
// aren't useful see the AutoMapRecord struct.
}
record.Frames = make([]int, len(frameFields))
for i := range frameFields {
record.Frames[i] = d.Number(frameFields[i])
}
AutoMaps = append(AutoMaps, record)
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d AutoMapRecord records", len(AutoMaps))
}

View File

@ -1,37 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// BodyLocationRecord describes a body location that items can be equipped to
type BodyLocationRecord struct {
Name string
Code string
}
// BodyLocations contains the body location records
//nolint:gochecknoglobals // Currently global by design, only written once
var BodyLocations map[string]*BodyLocationRecord
// LoadBodyLocations loads body locations from
func LoadBodyLocations(file []byte) {
BodyLocations = make(map[string]*BodyLocationRecord)
d := d2txt.LoadDataDictionary(file)
for d.Next() {
location := &BodyLocationRecord{
Name: d.String("Name"),
Code: d.String("Code"),
}
BodyLocations[location.Code] = location
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d Body Location records", len(BodyLocations))
}

View File

@ -1,54 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// 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
}
// Books stores all of the BooksRecords
var Books map[string]*BooksRecord //nolint:gochecknoglobals // Currently global by design, only written once
// LoadBooks loads Books records into a map[string]*BooksRecord
func LoadBooks(file []byte) {
Books = make(map[string]*BooksRecord)
d := d2txt.LoadDataDictionary(file)
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"),
}
Books[record.Namco] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d book items", len(Books))
}

View File

@ -1,61 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// 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
}
// SkillCalculations is where calculation records are stored
var SkillCalculations map[string]*CalculationRecord //nolint:gochecknoglobals // Currently global by design
// MissileCalculations is where missile calculations are stored
var MissileCalculations map[string]*CalculationRecord //nolint:gochecknoglobals // Currently global by design
// LoadSkillCalculations loads skill calculation records from skillcalc.txt
func LoadSkillCalculations(file []byte) {
SkillCalculations = make(map[string]*CalculationRecord)
d := d2txt.LoadDataDictionary(file)
for d.Next() {
record := &CalculationRecord{
Code: d.String("code"),
Description: d.String("*desc"),
}
SkillCalculations[record.Code] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d Skill Calculation records", len(SkillCalculations))
}
// LoadMissileCalculations loads missile calculation records from misscalc.txt
func LoadMissileCalculations(file []byte) {
MissileCalculations = make(map[string]*CalculationRecord)
d := d2txt.LoadDataDictionary(file)
for d.Next() {
record := &CalculationRecord{
Code: d.String("code"),
Description: d.String("*desc"),
}
MissileCalculations[record.Code] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d Missile Calculation records", len(MissileCalculations))
}

View File

@ -1,201 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// 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
}
// CharStats holds all of the CharStatsRecords
//nolint:gochecknoglobals // Currently global by design, only written once
var CharStats map[d2enum.Hero]*CharStatsRecord
var charStringMap map[string]d2enum.Hero //nolint:gochecknoglobals // Currently global by design
var weaponTokenMap map[string]d2enum.WeaponClass //nolint:gochecknoglobals // Currently global by design
// LoadCharStats loads charstats.txt file contents into map[d2enum.Hero]*CharStatsRecord
//nolint:funlen // Makes no sense to split
// LoadCharStats loads charstats.txt file contents into map[d2enum.Hero]*CharStatsRecord
func LoadCharStats(file []byte) {
CharStats = make(map[d2enum.Hero]*CharStatsRecord)
charStringMap = 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,
}
weaponTokenMap = 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,
}
d := d2txt.LoadDataDictionary(file)
for d.Next() {
record := &CharStatsRecord{
Class: charStringMap[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: weaponTokenMap[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"),
},
}
CharStats[record.Class] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d CharStats records", len(CharStats))
}

View File

@ -1,37 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// ComponentCodeRecord represents a single row from compcode.txt
type ComponentCodeRecord struct {
Component string
Code string
}
// ComponentCodes is a lookup table for DCC Animation Component Subtype,
// it links hardcoded data with the txt files
var ComponentCodes map[string]*ComponentCodeRecord //nolint:gochecknoglobals // Currently global by design
// LoadComponentCodes loads components code records from compcode.txt
func LoadComponentCodes(file []byte) {
ComponentCodes = make(map[string]*ComponentCodeRecord)
d := d2txt.LoadDataDictionary(file)
for d.Next() {
record := &ComponentCodeRecord{
Component: d.String("component"),
Code: d.String("code"),
}
ComponentCodes[record.Component] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d ComponentCode records", len(ComponentCodes))
}

View File

@ -1,317 +0,0 @@
package d2datadict
import (
"log"
"strconv"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// 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.go
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.go are integers,
// however d2datadict.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
}
// CubeRecipes contains all rows in CubeMain.txt.
//nolint:gochecknoglobals // Currently global by design, only written once
var CubeRecipes []*CubeRecipeRecord
// LoadCubeRecipes populates CubeRecipes with
// the data from CubeMain.txt.
func LoadCubeRecipes(file []byte) {
CubeRecipes = 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"}
d := d2txt.LoadDataDictionary(file)
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
}
CubeRecipes = append(CubeRecipes, record)
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d CubeMainRecord records", len(CubeRecipes))
}
// 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

@ -1,128 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// DifficultyLevels contain the difficulty records for each difficulty
//nolint:gochecknoglobals // Current design is to have these global
var DifficultyLevels map[string]*DifficultyLevelRecord
// DifficultyLevelRecord contain the parameters that change for different difficultios
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
// -----------------------------------------------------------------------
}
// LoadDifficultyLevels is a loader for difficultylevels.txt
func LoadDifficultyLevels(file []byte) {
DifficultyLevels = make(map[string]*DifficultyLevelRecord)
d := d2txt.LoadDataDictionary(file)
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"),
}
DifficultyLevels[record.Name] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d DifficultyLevel records", len(DifficultyLevels))
}

View File

@ -1,39 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// 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
}
// ElemTypes stores the ElemTypeRecords
var ElemTypes map[string]*ElemTypeRecord //nolint:gochecknoglobals // Currently global by design
// LoadElemTypes loads ElemTypeRecords into ElemTypes
func LoadElemTypes(file []byte) {
ElemTypes = make(map[string]*ElemTypeRecord)
d := d2txt.LoadDataDictionary(file)
for d.Next() {
record := &ElemTypeRecord{
ElemType: d.String("Elemental Type"),
Code: d.String("Code"),
}
ElemTypes[record.ElemType] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d ElemType records", len(ElemTypes))
}

View File

@ -1,34 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// EventRecord is a representation of a single row from events.txt
type EventRecord struct {
Event string
}
// Events holds all of the event records from events.txt
var Events map[string]*EventRecord //nolint:gochecknoglobals // Currently global by design
// LoadEvents loads all of the event records from events.txt
func LoadEvents(file []byte) {
Events = make(map[string]*EventRecord)
d := d2txt.LoadDataDictionary(file)
for d.Next() {
record := &EventRecord{
Event: d.String("event"),
}
Events[record.Event] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d Event records", len(Events))
}

View File

@ -1,101 +0,0 @@
package d2datadict
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
*/
// 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
}
// ExperienceBreakpoints describes the required experience
// for each level for each character class
//nolint:gochecknoglobals // Currently global by design, only written once
var ExperienceBreakpoints map[int]*ExperienceBreakpointsRecord
//nolint:gochecknoglobals // Currently global by design
var maxLevels map[d2enum.Hero]int
// GetMaxLevelByHero returns the highest level attainable for a hero type
func GetMaxLevelByHero(heroType d2enum.Hero) int {
return maxLevels[heroType]
}
// GetExperienceBreakpoint given a hero type and a level, returns the experience required for the level
func GetExperienceBreakpoint(heroType d2enum.Hero, level int) int {
return ExperienceBreakpoints[level].HeroBreakpoints[heroType]
}
// LoadExperienceBreakpoints loads experience.txt into a map
// ExperienceBreakpoints []*ExperienceBreakpointsRecord
func LoadExperienceBreakpoints(file []byte) {
ExperienceBreakpoints = make(map[int]*ExperienceBreakpointsRecord)
d := d2txt.LoadDataDictionary(file)
d.Next()
// the first row describes the max level of char classes
maxLevels = 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"),
}
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"),
}
ExperienceBreakpoints[record.Level] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d ExperienceBreakpoint records", len(ExperienceBreakpoints))
}

View File

@ -1,115 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// 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
}
// Gems stores all of the GemsRecords
var Gems map[string]*GemsRecord //nolint:gochecknoglobals // Currently global by design, only written once
// LoadGems loads gem records into a map[string]*GemsRecord
func LoadGems(file []byte) {
Gems = make(map[string]*GemsRecord)
d := d2txt.LoadDataDictionary(file)
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"),
}
Gems[gem.Name] = gem
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d Gems records", len(Gems))
}

View File

@ -1,178 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// 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
}
// Hirelings stores hireling (mercenary) records
//nolint:gochecknoglobals // Currently global by design, only written once
var Hirelings []*HirelingRecord
// LoadHireling loads hireling data into []*HirelingRecord
func LoadHireling(file []byte) {
Hirelings = make([]*HirelingRecord, 0)
d := d2txt.LoadDataDictionary(file)
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"),
}
Hirelings = append(Hirelings, hireling)
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d Hireling records", len(Hirelings))
}

View File

@ -1,166 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
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
}
// 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
}
// Inventory holds all of the inventory records from inventory.txt
var Inventory map[string]*InventoryRecord //nolint:gochecknoglobals // Currently global by design
// LoadInventory loads all of the inventory records from inventory.txt
func LoadInventory(file []byte) { //nolint:funlen // doesn't make sense to split
d := d2txt.LoadDataDictionary(file)
Inventory = make(map[string]*InventoryRecord)
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"),
},
},
}
Inventory[record.Name] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d Inventory Panel records", len(Inventory))
}

View File

@ -1,231 +0,0 @@
package d2datadict
import (
"fmt"
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// MagicPrefix stores all of the magic prefix records
// nolint:gochecknoglobals // Currently global by design
var MagicPrefix map[string]*ItemAffixCommonRecord
// MagicSuffix stores all of the magic suffix records
// nolint:gochecknoglobals // Currently global by design
var MagicSuffix map[string]*ItemAffixCommonRecord
// LoadMagicPrefix loads MagicPrefix.txt
func LoadMagicPrefix(file []byte) {
superType := d2enum.ItemAffixPrefix
subType := d2enum.ItemAffixMagic
MagicPrefix = loadDictionary(file, superType, subType)
}
// LoadMagicSuffix loads MagicSuffix.txt
func LoadMagicSuffix(file []byte) {
superType := d2enum.ItemAffixSuffix
subType := d2enum.ItemAffixMagic
MagicSuffix = loadDictionary(file, superType, subType)
}
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 loadDictionary(
file []byte,
superType d2enum.ItemAffixSuperType,
subType d2enum.ItemAffixSubType,
) map[string]*ItemAffixCommonRecord {
d := d2txt.LoadDataDictionary(file)
records := createItemAffixRecords(d, superType, subType)
name := getAffixString(superType, subType)
log.Printf("Loaded %d %s records", len(records), name)
return records
}
func createItemAffixRecords(
d *d2txt.DataDictionary,
superType d2enum.ItemAffixSuperType,
subType d2enum.ItemAffixSubType,
) map[string]*ItemAffixCommonRecord {
records := make(map[string]*ItemAffixCommonRecord)
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)
}
// affix groupis
if ItemAffixGroups == nil {
ItemAffixGroups = make(map[int]*ItemAffixCommonGroup)
}
if _, found := ItemAffixGroups[affix.GroupID]; !found {
ItemAffixGroup := &ItemAffixCommonGroup{}
ItemAffixGroup.ID = affix.GroupID
ItemAffixGroups[affix.GroupID] = ItemAffixGroup
}
group := ItemAffixGroups[affix.GroupID]
group.addMember(affix)
records[affix.Name] = affix
}
if d.Err != nil {
panic(d.Err)
}
return records
}
// ItemAffixGroups are groups of MagicPrefix/Suffixes
var ItemAffixGroups map[int]*ItemAffixCommonGroup //nolint:gochecknoglobals // Currently global by design
// ItemAffixCommonGroup is a grouping that is common between prefix/suffix
type ItemAffixCommonGroup struct {
ID int
Members map[string]*ItemAffixCommonRecord
}
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
}
// 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
}
// 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
}
// 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

@ -1,398 +0,0 @@
package d2datadict
import (
"strconv"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2calculation"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
)
// 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 uint8
}
// CommonItems stores all ItemCommonRecords
var CommonItems map[string]*ItemCommonRecord //nolint:gochecknoglobals // Currently global by design
// LoadCommonItems loads armor/weapons/misc.txt ItemCommonRecords
func LoadCommonItems(file []byte, source d2enum.InventoryItemType) map[string]*ItemCommonRecord {
if CommonItems == nil {
CommonItems = make(map[string]*ItemCommonRecord)
}
items := make(map[string]*ItemCommonRecord)
data := strings.Split(string(file), "\r\n")
mapping := mapHeaders(data[0])
for lineno, line := range data {
if lineno == 0 {
continue
}
if line == "" {
continue
}
rec := createCommonItemRecord(line, mapping, source)
if rec.Name == expansion {
continue
}
items[rec.Code] = &rec
CommonItems[rec.Code] = &rec
}
return items
}
//nolint:funlen // Makes no sens to split
func createCommonItemRecord(line string, mapping map[string]int, source d2enum.InventoryItemType) ItemCommonRecord {
r := strings.Split(line, "\t")
result := ItemCommonRecord{
Source: source,
Name: mapLoadString(&r, mapping, "name"),
Version: mapLoadInt(&r, mapping, "version"),
CompactSave: mapLoadBool(&r, mapping, "compactsave"),
Rarity: mapLoadInt(&r, mapping, "rarity"),
Spawnable: mapLoadBool(&r, mapping, "spawnable"),
MinAC: mapLoadInt(&r, mapping, "minac"),
MaxAC: mapLoadInt(&r, mapping, "maxac"),
Absorbs: mapLoadInt(&r, mapping, "absorbs"),
Speed: mapLoadInt(&r, mapping, "speed"),
RequiredStrength: mapLoadInt(&r, mapping, "reqstr"),
Block: mapLoadInt(&r, mapping, "block"),
Durability: mapLoadInt(&r, mapping, "durability"),
NoDurability: mapLoadBool(&r, mapping, "nodurability"),
Level: mapLoadInt(&r, mapping, "level"),
RequiredLevel: mapLoadInt(&r, mapping, "levelreq"),
Cost: mapLoadInt(&r, mapping, "cost"),
GambleCost: mapLoadInt(&r, mapping, "gamble cost"),
Code: mapLoadString(&r, mapping, "code"),
NameString: mapLoadString(&r, mapping, "namestr"),
MagicLevel: mapLoadInt(&r, mapping, "magic lvl"),
AutoPrefix: mapLoadInt(&r, mapping, "auto prefix"),
AlternateGfx: mapLoadString(&r, mapping, "alternategfx"),
OpenBetaGfx: mapLoadString(&r, mapping, "OpenBetaGfx"),
NormalCode: mapLoadString(&r, mapping, "normcode"),
UberCode: mapLoadString(&r, mapping, "ubercode"),
UltraCode: mapLoadString(&r, mapping, "ultracode"),
SpellOffset: mapLoadInt(&r, mapping, "spelloffset"),
Component: mapLoadInt(&r, mapping, "component"),
InventoryWidth: mapLoadInt(&r, mapping, "invwidth"),
InventoryHeight: mapLoadInt(&r, mapping, "invheight"),
HasInventory: mapLoadBool(&r, mapping, "hasinv"),
GemSockets: mapLoadInt(&r, mapping, "gemsockets"),
GemApplyType: mapLoadInt(&r, mapping, "gemapplytype"),
FlippyFile: mapLoadString(&r, mapping, "flippyfile"),
InventoryFile: mapLoadString(&r, mapping, "invfile"),
UniqueInventoryFile: mapLoadString(&r, mapping, "uniqueinvfile"),
SetInventoryFile: mapLoadString(&r, mapping, "setinvfile"),
AnimRightArm: mapLoadInt(&r, mapping, "rArm"),
AnimLeftArm: mapLoadInt(&r, mapping, "lArm"),
AnimTorso: mapLoadInt(&r, mapping, "Torso"),
AnimLegs: mapLoadInt(&r, mapping, "Legs"),
AnimRightShoulderPad: mapLoadInt(&r, mapping, "rSPad"),
AnimLeftShoulderPad: mapLoadInt(&r, mapping, "lSPad"),
Useable: mapLoadBool(&r, mapping, "useable"),
Throwable: mapLoadBool(&r, mapping, "throwable"),
Stackable: mapLoadBool(&r, mapping, "stackable"),
MinStack: mapLoadInt(&r, mapping, "minstack"),
MaxStack: mapLoadInt(&r, mapping, "maxstack"),
Type: mapLoadString(&r, mapping, "type"),
Type2: mapLoadString(&r, mapping, "type2"),
DropSound: mapLoadString(&r, mapping, "dropsound"),
DropSfxFrame: mapLoadInt(&r, mapping, "dropsfxframe"),
UseSound: mapLoadString(&r, mapping, "usesound"),
Unique: mapLoadBool(&r, mapping, "unique"),
Transparent: mapLoadBool(&r, mapping, "transparent"),
TransTable: mapLoadInt(&r, mapping, "transtbl"),
Quivered: mapLoadBool(&r, mapping, "quivered"),
LightRadius: mapLoadInt(&r, mapping, "lightradius"),
Belt: mapLoadBool(&r, mapping, "belt"),
Quest: mapLoadInt(&r, mapping, "quest"),
MissileType: mapLoadInt(&r, mapping, "missiletype"),
DurabilityWarning: mapLoadInt(&r, mapping, "durwarning"),
QuantityWarning: mapLoadInt(&r, mapping, "qntwarning"),
MinDamage: mapLoadInt(&r, mapping, "mindam"),
MaxDamage: mapLoadInt(&r, mapping, "maxdam"),
StrengthBonus: mapLoadInt(&r, mapping, "StrBonus"),
DexterityBonus: mapLoadInt(&r, mapping, "DexBonus"),
GemOffset: mapLoadInt(&r, mapping, "gemoffset"),
BitField1: mapLoadInt(&r, mapping, "bitfield1"),
Vendors: createItemVendorParams(&r, mapping),
SourceArt: mapLoadString(&r, mapping, "Source Art"),
GameArt: mapLoadString(&r, mapping, "Game Art"),
ColorTransform: mapLoadInt(&r, mapping, "Transform"),
InventoryColorTransform: mapLoadInt(&r, mapping, "InvTrans"),
SkipName: mapLoadBool(&r, mapping, "SkipName"),
NightmareUpgrade: mapLoadString(&r, mapping, "NightmareUpgrade"),
HellUpgrade: mapLoadString(&r, mapping, "HellUpgrade"),
Nameable: mapLoadBool(&r, mapping, "Nameable"),
// weapon params
BarbOneOrTwoHanded: mapLoadBool(&r, mapping, "1or2handed"),
UsesTwoHands: mapLoadBool(&r, mapping, "2handed"),
Min2HandDamage: mapLoadInt(&r, mapping, "2handmindam"),
Max2HandDamage: mapLoadInt(&r, mapping, "2handmaxdam"),
MinMissileDamage: mapLoadInt(&r, mapping, "minmisdam"),
MaxMissileDamage: mapLoadInt(&r, mapping, "maxmisdam"),
MissileSpeed: mapLoadInt(&r, mapping, "misspeed"),
ExtraRange: mapLoadInt(&r, mapping, "rangeadder"),
RequiredDexterity: mapLoadInt(&r, mapping, "reqdex"),
WeaponClass: mapLoadString(&r, mapping, "wclass"),
WeaponClass2Hand: mapLoadString(&r, mapping, "2handedwclass"),
HitClass: mapLoadString(&r, mapping, "hit class"),
SpawnStack: mapLoadInt(&r, mapping, "spawnstack"),
SpecialFeature: mapLoadString(&r, mapping, "special"),
QuestDifficultyCheck: mapLoadBool(&r, mapping, "questdiffcheck"),
PermStoreItem: mapLoadBool(&r, mapping, "PermStoreItem"),
// misc params
FlavorText: mapLoadString(&r, mapping, "szFlavorText"),
Transmogrify: mapLoadBool(&r, mapping, "Transmogrify"),
TransmogCode: mapLoadString(&r, mapping, "TMogType"),
TransmogMin: mapLoadInt(&r, mapping, "TMogMin"),
TransmogMax: mapLoadInt(&r, mapping, "TMogMax"),
AutoBelt: mapLoadBool(&r, mapping, "autobelt"),
SpellIcon: mapLoadInt(&r, mapping, "spellicon"),
SpellType: mapLoadInt(&r, mapping, "pSpell"),
OverlayState: mapLoadString(&r, mapping, "state"),
CureOverlayStates: [2]string{
mapLoadString(&r, mapping, "cstate1"),
mapLoadString(&r, mapping, "cstate2"),
},
EffectLength: mapLoadInt(&r, mapping, "len"),
UsageStats: createItemUsageStats(&r, mapping),
SpellDescriptionType: mapLoadInt(&r, mapping, "spelldesc"),
// 0 = none, 1 = use desc string, 2 = use desc string + calc value
SpellDescriptionString: mapLoadString(&r, mapping, "spelldescstr"),
SpellDescriptionCalc: d2calculation.CalcString(mapLoadString(&r, mapping, "spelldesccalc")),
BetterGem: mapLoadString(&r, mapping, "BetterGem"),
Multibuy: mapLoadBool(&r, mapping, "multibuy"),
}
return result
}
func createItemVendorParams(r *[]string, mapping map[string]int) map[string]*ItemVendorParams {
vs := make([]string, 17)
vs[0] = "Charsi"
vs[1] = "Gheed"
vs[2] = "Akara"
vs[3] = "Fara"
vs[4] = "Lysander"
vs[5] = "Drognan"
vs[6] = "Hralti"
vs[7] = "Alkor"
vs[8] = "Ormus"
vs[9] = "Elzix"
vs[10] = "Asheara"
vs[11] = "Cain"
vs[12] = "Halbu"
vs[13] = "Jamella"
vs[14] = "Larzuk"
vs[15] = "Malah"
vs[16] = "Drehya"
result := make(map[string]*ItemVendorParams)
for _, name := range vs {
wvp := ItemVendorParams{
Min: mapLoadInt(r, mapping, name+"Min"),
Max: mapLoadInt(r, mapping, name+"Max"),
MagicMin: mapLoadInt(r, mapping, name+"MagicMin"),
MagicMax: mapLoadInt(r, mapping, name+"MagicMax"),
MagicLevel: mapLoadUint8(r, mapping, name+"MagicLvl"),
}
result[name] = &wvp
}
return result
}
func createItemUsageStats(r *[]string, mapping map[string]int) [3]ItemUsageStat {
result := [3]ItemUsageStat{}
for i := 0; i < 3; i++ {
result[i].Stat = mapLoadString(r, mapping, "stat"+strconv.Itoa(i))
result[i].Calc = d2calculation.CalcString(mapLoadString(r, mapping, "calc"+strconv.Itoa(i)))
}
return result
}

View File

@ -1,91 +0,0 @@
package d2datadict
import (
"log"
"strconv"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// 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
}
// ItemRatios holds all of the ItemRatioRecords from ItemRatio.txt
var ItemRatios map[string]*ItemRatioRecord //nolint:gochecknoglobals // Currently global by design
// LoadItemRatios loads all of the ItemRatioRecords from ItemRatio.txt
func LoadItemRatios(file []byte) {
ItemRatios = make(map[string]*ItemRatioRecord)
d := d2txt.LoadDataDictionary(file)
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,
},
}
ItemRatios[record.Function+strconv.FormatBool(record.Version)] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d ItemRatio records", len(ItemRatios))
}

View File

@ -1,357 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// 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
}
// ItemTypes stores all of the ItemTypeRecords
var ItemTypes map[string]*ItemTypeRecord //nolint:gochecknoglobals // Currently global by design, only written once
// LoadItemTypes loads ItemType records
func LoadItemTypes(file []byte) {
ItemTypes = make(map[string]*ItemTypeRecord)
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,
}
d := d2txt.LoadDataDictionary(file)
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"),
}
ItemTypes[itemType.Code] = itemType
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d ItemType records", len(ItemTypes))
}
// ItemEquivalenciesByTypeCode describes item equivalencies for ItemTypes
var ItemEquivalenciesByTypeCode map[string][]*ItemCommonRecord //nolint:gochecknoglobals // Currently global by design
// LoadItemEquivalencies loads a map of ItemType string codes to slices of ItemCommonRecord pointers
func LoadItemEquivalencies() {
ItemEquivalenciesByTypeCode = make(map[string][]*ItemCommonRecord)
makeEmptyEquivalencyMaps()
for icrCode := range CommonItems {
commonItem := CommonItems[icrCode]
updateEquivalencies(commonItem, ItemTypes[commonItem.Type], nil)
if commonItem.Type2 != "" { // some items (like gems) have a secondary type
updateEquivalencies(commonItem, ItemTypes[commonItem.Type2], nil)
}
}
}
func makeEmptyEquivalencyMaps() {
for typeCode := range ItemTypes {
code := []string{
typeCode,
ItemTypes[typeCode].Equiv1,
ItemTypes[typeCode].Equiv2,
}
for _, str := range code {
if str == "" {
continue
}
if ItemEquivalenciesByTypeCode[str] == nil {
ItemEquivalenciesByTypeCode[str] = make([]*ItemCommonRecord, 0)
}
}
}
}
func updateEquivalencies(icr *ItemCommonRecord, itemType *ItemTypeRecord, checked []string) {
if itemType.Code == "" {
return
}
if checked == nil {
checked = make([]string, 0)
}
checked = append(checked, itemType.Code)
if !itemEquivPresent(icr, ItemEquivalenciesByTypeCode[itemType.Code]) {
ItemEquivalenciesByTypeCode[itemType.Code] = append(ItemEquivalenciesByTypeCode[itemType.Code], icr)
}
if itemType.Equiv1 != "" {
updateEquivalencies(icr, ItemTypes[itemType.Equiv1], checked)
}
if itemType.Equiv2 != "" {
updateEquivalencies(icr, ItemTypes[itemType.Equiv2], checked)
}
}
func itemEquivPresent(icr *ItemCommonRecord, list []*ItemCommonRecord) bool {
for idx := range list {
if list[idx] == icr {
return true
}
}
return false
}
var itemCommonTypeLookup map[*ItemCommonRecord][]string //nolint:gochecknoglobals // Currently global by design
// FindEquivalentTypesByItemCommonRecord returns itemtype codes that are equivalent
// to the given item common record
func FindEquivalentTypesByItemCommonRecord(icr *ItemCommonRecord) []string {
if itemCommonTypeLookup == nil {
itemCommonTypeLookup = make(map[*ItemCommonRecord][]string)
}
// the first lookup generates the lookup table entry, next time will just use the table
if itemCommonTypeLookup[icr] == nil {
itemCommonTypeLookup[icr] = make([]string, 0)
for code := range ItemEquivalenciesByTypeCode {
icrList := ItemEquivalenciesByTypeCode[code]
for idx := range icrList {
if icr == icrList[idx] {
itemCommonTypeLookup[icr] = append(itemCommonTypeLookup[icr], code)
break
}
}
}
}
return itemCommonTypeLookup[icr]
}

View File

@ -1,199 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// 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
//nolint:gochecknoglobals // Currently global by design
var ItemStatCosts map[string]*ItemStatCostRecord
// LoadItemStatCosts loads ItemStatCostRecord's from text
func LoadItemStatCosts(file []byte) {
ItemStatCosts = make(map[string]*ItemStatCostRecord)
d := d2txt.LoadDataDictionary(file)
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
}
ItemStatCosts[record.Name] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d ItemStatCost records", len(ItemStatCosts))
}

View File

@ -1,65 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// 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
}
// LevelMazeDetails stores all of the LevelMazeDetailsRecords
var LevelMazeDetails map[int]*LevelMazeDetailsRecord //nolint:gochecknoglobals // Currently global by design
// LoadLevelMazeDetails loads LevelMazeDetailsRecords from text file
func LoadLevelMazeDetails(file []byte) {
LevelMazeDetails = make(map[int]*LevelMazeDetailsRecord)
d := d2txt.LoadDataDictionary(file)
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"),
}
LevelMazeDetails[record.LevelID] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d LevelMazeDetails records", len(LevelMazeDetails))
}

View File

@ -1,109 +0,0 @@
package d2datadict
import (
"log"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
)
// 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
}
// CreateLevelPresetRecord parses a row from lvlprest.txt into a LevelPresetRecord
func createLevelPresetRecord(props []string) LevelPresetRecord {
i := -1
inc := func() int {
i++
return i
}
result := LevelPresetRecord{
Name: props[inc()],
DefinitionID: d2util.StringToInt(props[inc()]),
LevelID: d2util.StringToInt(props[inc()]),
Populate: d2util.StringToUint8(props[inc()]) == 1,
Logicals: d2util.StringToUint8(props[inc()]) == 1,
Outdoors: d2util.StringToUint8(props[inc()]) == 1,
Animate: d2util.StringToUint8(props[inc()]) == 1,
KillEdge: d2util.StringToUint8(props[inc()]) == 1,
FillBlanks: d2util.StringToUint8(props[inc()]) == 1,
SizeX: d2util.StringToInt(props[inc()]),
SizeY: d2util.StringToInt(props[inc()]),
AutoMap: d2util.StringToUint8(props[inc()]) == 1,
Scan: d2util.StringToUint8(props[inc()]) == 1,
Pops: d2util.StringToInt(props[inc()]),
PopPad: d2util.StringToInt(props[inc()]),
FileCount: d2util.StringToInt(props[inc()]),
Files: [6]string{
props[inc()],
props[inc()],
props[inc()],
props[inc()],
props[inc()],
props[inc()],
},
Dt1Mask: d2util.StringToUint(props[inc()]),
Beta: d2util.StringToUint8(props[inc()]) == 1,
Expansion: d2util.StringToUint8(props[inc()]) == 1,
}
return result
}
// LevelPresets stores all of the LevelPresetRecords
var LevelPresets map[int]LevelPresetRecord //nolint:gochecknoglobals // Currently global by design
// LoadLevelPresets loads level presets from text file
func LoadLevelPresets(file []byte) {
LevelPresets = make(map[int]LevelPresetRecord)
data := strings.Split(string(file), "\r\n")[1:]
for _, line := range data {
if line == "" {
continue
}
props := strings.Split(line, "\t")
if props[1] == "" {
continue // any line without a definition id is skipped (e.g. the "Expansion" line)
}
rec := createLevelPresetRecord(props)
LevelPresets[rec.DefinitionID] = rec
}
log.Printf("Loaded %d level presets", len(LevelPresets))
}
// LevelPreset looks up a LevelPresetRecord by ID
func LevelPreset(id int) LevelPresetRecord {
for i := 0; i < len(LevelPresets); i++ {
if LevelPresets[i].DefinitionID == id {
return LevelPresets[i]
}
}
panic("Unknown level preset")
}

View File

@ -1,109 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// 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
}
// LevelSubstitutions stores all of the LevelSubstitutionRecords
//nolint:gochecknoglobals // Currently global by design
var LevelSubstitutions map[int]*LevelSubstitutionRecord
// LoadLevelSubstitutions loads lvlsub.txt and parses into records
func LoadLevelSubstitutions(file []byte) {
LevelSubstitutions = make(map[int]*LevelSubstitutionRecord)
d := d2txt.LoadDataDictionary(file)
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"),
}
LevelSubstitutions[record.ID] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d LevelSubstitution records", len(LevelSubstitutions))
}

View File

@ -1,62 +0,0 @@
package d2datadict
import (
"log"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
)
// 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
}
// LevelTypes stores all of the LevelTypeRecords
var LevelTypes []LevelTypeRecord //nolint:gochecknoglobals // Currently global by design,
// LoadLevelTypes loads the LevelTypeRecords
func LoadLevelTypes(file []byte) {
data := strings.Split(string(file), "\r\n")[1:]
LevelTypes = make([]LevelTypeRecord, len(data))
for i, j := 0, 0; i < len(data); i, j = i+1, j+1 {
idx := -1
inc := func() int {
idx++
return idx
}
if data[i] == "" {
continue
}
parts := strings.Split(data[i], "\t")
if parts[0] == "Expansion" {
j--
continue
}
LevelTypes[j].Name = parts[inc()]
LevelTypes[j].ID = d2util.StringToInt(parts[inc()])
for fileIdx := range LevelTypes[i].Files {
LevelTypes[j].Files[fileIdx] = parts[inc()]
if LevelTypes[j].Files[fileIdx] == "0" {
LevelTypes[j].Files[fileIdx] = ""
}
}
LevelTypes[j].Beta = parts[inc()] != "1"
LevelTypes[j].Act = d2util.StringToInt(parts[inc()])
LevelTypes[j].Expansion = parts[inc()] != "1"
}
log.Printf("Loaded %d LevelType records", len(LevelTypes))
}

View File

@ -1,56 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// 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
}
// LevelWarps loaded from txt records
//nolint:gochecknoglobals // Currently global by design, only written once
var LevelWarps map[int]*LevelWarpRecord
// LoadLevelWarps loads LevelWarpRecord's from text file data
func LoadLevelWarps(file []byte) {
LevelWarps = make(map[int]*LevelWarpRecord)
d := d2txt.LoadDataDictionary(file)
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"),
}
LevelWarps[record.ID] = record
}
log.Printf("Loaded %d level warps", len(LevelWarps))
}

View File

@ -1,543 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// 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
}
// LevelDetails has all of the LevelDetailsRecords
//nolint:gochecknoglobals // Currently global by design, only written once
var LevelDetails map[int]*LevelDetailsRecord
// GetLevelDetails gets a LevelDetailsRecord by the record Id
func GetLevelDetails(id int) *LevelDetailsRecord {
for i := 0; i < len(LevelDetails); i++ {
if LevelDetails[i].ID == id {
return LevelDetails[i]
}
}
return nil
}
// LoadLevelDetails loads level details records from levels.txt
//nolint:funlen // Txt loader, makes no sense to split
func LoadLevelDetails(file []byte) {
LevelDetails = make(map[int]*LevelDetailsRecord)
d := d2txt.LoadDataDictionary(file)
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"),
}
LevelDetails[record.ID] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d LevelDetails records", len(LevelDetails))
}

View File

@ -1,16 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
)
// MiscItems stores all of the ItemCommonRecords for misc.txt
var MiscItems map[string]*ItemCommonRecord //nolint:gochecknoglobals // Currently global by design
// LoadMiscItems loads ItemCommonRecords from misc.txt
func LoadMiscItems(file []byte) {
MiscItems = LoadCommonItems(file, d2enum.InventoryItemTypeItem)
log.Printf("Loaded %d misc items", len(MiscItems))
}

View File

@ -1,445 +0,0 @@
package d2datadict
import (
"log"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2calculation"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
)
// 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
}
func createMissileRecord(line string) MissileRecord {
r := strings.Split(line, "\t")
i := -1
inc := func() int {
i++
return i
}
// note: in this file, empties are equivalent to zero, so all numerical conversions should
// be wrapped in an d2common.EmptyToZero transform
result := MissileRecord{
Name: r[inc()],
Id: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
ClientMovementFunc: d2util.StringToInt(d2util.EmptyToZero(d2util.AsterToEmpty(r[inc()]))),
ClientCollisionFunc: d2util.StringToInt(d2util.EmptyToZero(d2util.AsterToEmpty(r[inc()]))),
ServerMovementFunc: d2util.StringToInt(d2util.EmptyToZero(d2util.AsterToEmpty(r[inc()]))),
ServerCollisionFunc: d2util.StringToInt(d2util.EmptyToZero(d2util.AsterToEmpty(r[inc()]))),
ServerDamageFunc: d2util.StringToInt(d2util.EmptyToZero(d2util.AsterToEmpty(r[inc()]))),
ServerMovementCalc: loadMissileCalc(&r, inc, 5),
ClientMovementCalc: loadMissileCalc(&r, inc, 5),
ServerCollisionCalc: loadMissileCalc(&r, inc, 3),
ClientCollisionCalc: loadMissileCalc(&r, inc, 3),
ServerDamageCalc: loadMissileCalc(&r, inc, 2),
Velocity: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
MaxVelocity: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
LevelVelocityBonus: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
Accel: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
Range: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
LevelRangeBonus: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
Light: loadMissileLight(&r, inc),
Animation: loadMissileAnimation(&r, inc),
Collision: loadMissileCollision(&r, inc),
XOffset: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
YOffset: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
ZOffset: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
Size: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
DestroyedByTP: d2util.StringToInt(d2util.EmptyToZero(r[inc()])) == 1,
DestroyedByTPFrame: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
CanDestroy: d2util.StringToInt(d2util.EmptyToZero(r[inc()])) == 1,
UseAttackRating: d2util.StringToInt(d2util.EmptyToZero(r[inc()])) == 1,
AlwaysExplode: d2util.StringToInt(d2util.EmptyToZero(r[inc()])) == 1,
ClientExplosion: d2util.StringToInt(d2util.EmptyToZero(r[inc()])) == 1,
TownSafe: d2util.StringToInt(d2util.EmptyToZero(r[inc()])) == 1,
IgnoreBossModifiers: d2util.StringToInt(d2util.EmptyToZero(r[inc()])) == 1,
IgnoreMultishot: d2util.StringToInt(d2util.EmptyToZero(r[inc()])) == 1,
HolyFilterType: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
CanBeSlowed: d2util.StringToInt(d2util.EmptyToZero(r[inc()])) == 1,
TriggersHitEvents: d2util.StringToInt(d2util.EmptyToZero(r[inc()])) == 1,
TriggersGetHit: d2util.StringToInt(d2util.EmptyToZero(r[inc()])) == 1,
SoftHit: d2util.StringToInt(d2util.EmptyToZero(r[inc()])) == 1,
KnockbackPercent: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
TransparencyMode: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
UseQuantity: d2util.StringToInt(d2util.EmptyToZero(r[inc()])) == 1,
AffectedByPierce: d2util.StringToInt(d2util.EmptyToZero(r[inc()])) == 1,
SpecialSetup: d2util.StringToInt(d2util.EmptyToZero(r[inc()])) == 1,
MissileSkill: d2util.StringToInt(d2util.EmptyToZero(r[inc()])) == 1,
SkillName: r[inc()],
ResultFlags: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
HitFlags: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
HitShift: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
ApplyMastery: d2util.StringToInt(d2util.EmptyToZero(r[inc()])) == 1,
SourceDamage: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
HalfDamageForTwoHander: d2util.StringToInt(d2util.EmptyToZero(r[inc()])) == 1,
SourceMissDamage: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
Damage: loadMissileDamage(&r, inc),
ElementalDamage: loadMissileElementalDamage(&r, inc),
HitClass: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
NumDirections: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
LocalBlood: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
DamageReductionRate: d2util.StringToInt(d2util.EmptyToZero(r[inc()])),
TravelSound: r[inc()],
HitSound: r[inc()],
ProgSound: r[inc()],
ProgOverlay: r[inc()],
ExplosionMissile: r[inc()],
SubMissile: [3]string{r[inc()], r[inc()], r[inc()]},
HitSubMissile: [4]string{r[inc()], r[inc()], r[inc()], r[inc()]},
ClientSubMissile: [3]string{r[inc()], r[inc()], r[inc()]},
ClientHitSubMissile: [4]string{r[inc()], r[inc()], r[inc()], r[inc()]},
}
return result
}
// Missiles stores all of the MissileRecords
//nolint:gochecknoglobals // Currently global by design, only written once
var Missiles map[int]*MissileRecord
var missilesByName map[string]*MissileRecord
// GetMissileByName allows lookup of a MissileRecord by a given name. The name will be lowercased and stripped of whitespaces.
func GetMissileByName(missileName string) *MissileRecord {
return missilesByName[sanitize(missileName)]
}
// LoadMissiles loads MissileRecords from missiles.txt
func LoadMissiles(file []byte) {
Missiles = make(map[int]*MissileRecord)
missilesByName = make(map[string]*MissileRecord)
data := strings.Split(string(file), "\r\n")[1:]
for _, line := range data {
if line == "" {
continue
}
rec := createMissileRecord(line)
Missiles[rec.Id] = &rec
missilesByName[sanitize(rec.Name)] = &rec
}
log.Printf("Loaded %d missiles", len(Missiles))
}
func sanitize(missileName string) string {
return strings.ToLower(strings.ReplaceAll(missileName, " ", ""))
}
func loadMissileCalcParam(r *[]string, inc func() int) MissileCalcParam {
result := MissileCalcParam{
Param: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
Desc: (*r)[inc()],
}
return result
}
func loadMissileCalc(r *[]string, inc func() int, params int) MissileCalc {
result := MissileCalc{
Calc: d2calculation.CalcString((*r)[inc()]),
Desc: (*r)[inc()],
}
result.Params = make([]MissileCalcParam, params)
for p := 0; p < params; p++ {
result.Params[p] = loadMissileCalcParam(r, inc)
}
return result
}
func loadMissileLight(r *[]string, inc func() int) MissileLight {
result := MissileLight{
Diameter: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
Flicker: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
Red: d2util.StringToUint8(d2util.EmptyToZero((*r)[inc()])),
Green: d2util.StringToUint8(d2util.EmptyToZero((*r)[inc()])),
Blue: d2util.StringToUint8(d2util.EmptyToZero((*r)[inc()])),
}
return result
}
func loadMissileAnimation(r *[]string, inc func() int) MissileAnimation {
result := MissileAnimation{
StepsBeforeVisible: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
StepsBeforeActive: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
LoopAnimation: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])) == 1,
CelFileName: (*r)[inc()],
AnimationRate: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
AnimationLength: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
AnimationSpeed: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
StartingFrame: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
HasSubLoop: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])) == 1,
SubStartingFrame: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
SubEndingFrame: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
}
return result
}
func loadMissileCollision(r *[]string, inc func() int) MissileCollision {
result := MissileCollision{
CollisionType: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
DestroyedUponCollision: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])) == 1,
FriendlyFire: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])) == 1,
LastCollide: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])) == 1,
Collision: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])) == 1,
ClientCollision: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])) == 1,
ClientSend: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])) == 1,
UseCollisionTimer: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])) == 1,
TimerFrames: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
}
return result
}
func loadMissileDamage(r *[]string, inc func() int) MissileDamage {
result := MissileDamage{
MinDamage: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
MinLevelDamage: [5]int{
d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
},
MaxDamage: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
MaxLevelDamage: [5]int{
d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
},
DamageSynergyPerCalc: d2calculation.CalcString((*r)[inc()]),
}
return result
}
func loadMissileElementalDamage(r *[]string, inc func() int) MissileElementalDamage {
result := MissileElementalDamage{
ElementType: (*r)[inc()],
Damage: loadMissileDamage(r, inc),
Duration: d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
LevelDuration: [3]int{
d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
d2util.StringToInt(d2util.EmptyToZero((*r)[inc()])),
},
}
return result
}

View File

@ -1,38 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// MonModeRecord is a representation of a single row of Monmode.txt
type MonModeRecord struct {
Name string
Token string
Code string
}
// MonModes stores all of the GemsRecords
var MonModes map[string]*MonModeRecord //nolint:gochecknoglobals // Currently global by design, only written once
// LoadMonModes loads gem records into a map[string]*MonModeRecord
func LoadMonModes(file []byte) {
MonModes = make(map[string]*MonModeRecord)
d := d2txt.LoadDataDictionary(file)
for d.Next() {
record := &MonModeRecord{
Name: d.String("name"),
Token: d.String("token"),
Code: d.String("code"),
}
MonModes[record.Name] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d MonMode records", len(MonModes))
}

View File

@ -1,32 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// MonPresets stores monster presets
//nolint:gochecknoglobals // Currently global by design, only written once
var MonPresets map[int32][]string
// LoadMonPresets loads monster presets from monpresets.txt
func LoadMonPresets(file []byte) {
MonPresets = make(map[int32][]string)
d := d2txt.LoadDataDictionary(file)
for d.Next() {
act := int32(d.Number("Act"))
if _, ok := MonPresets[act]; !ok {
MonPresets[act] = make([]string, 0)
}
MonPresets[act] = append(MonPresets[act], d.String("Place"))
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d MonPreset records", len(MonPresets))
}

View File

@ -1,98 +0,0 @@
package d2datadict
import (
"fmt"
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
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)"
)
// 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
}
}
type monProp struct {
Code string
Param string
Chance int
Min int
Max int
}
// MonProps stores all of the MonPropRecords
var MonProps map[string]*MonPropRecord //nolint:gochecknoglobals // Currently global by design, only written once
// LoadMonProps loads monster property records into a map[string]*MonPropRecord
func LoadMonProps(file []byte) {
MonProps = make(map[string]*MonPropRecord)
d := d2txt.LoadDataDictionary(file)
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)),
}
}
MonProps[record.ID] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d MonProp records", len(MonProps))
}

View File

@ -1,954 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// https://d2mods.info/forum/kb/viewarticle?a=360
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
}
)
// MonStats stores all of the MonStat Records
var MonStats map[string]*MonStatsRecord //nolint:gochecknoglobals // Currently global by design, only written once
// LoadMonStats loads monstats
func LoadMonStats(file []byte) { // nolint:funlen // Makes no sense to split
MonStats = make(map[string]*MonStatsRecord)
d := d2txt.LoadDataDictionary(file)
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,
}
MonStats[record.Key] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d MonStats records", len(MonStats))
}

View File

@ -1,339 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// 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
}
// MonStats2 stores all of the MonStats2Records
//nolint:gochecknoglobals // Current design issue
var MonStats2 map[string]*MonStats2Record
// LoadMonStats2 loads MonStats2Records from monstats2.txt
//nolint:funlen //just a big data loader
func LoadMonStats2(file []byte) {
MonStats2 = make(map[string]*MonStats2Record)
d := d2txt.LoadDataDictionary(file)
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"),
}
MonStats2[record.Key] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d MonStats2 records", len(MonStats2))
}
//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

@ -1,34 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// MonsterAIRecord represents a single row from monai.txt
type MonsterAIRecord struct {
AI string
}
// MonsterAI holds the MonsterAIRecords, The monai.txt file is a lookup table for unit AI codes
var MonsterAI map[string]*MonsterAIRecord //nolint:gochecknoglobals // Currently global by design
// LoadMonsterAI loads MonsterAIRecords from monai.txt
func LoadMonsterAI(file []byte) {
MonsterAI = make(map[string]*MonsterAIRecord)
d := d2txt.LoadDataDictionary(file)
for d.Next() {
record := &MonsterAIRecord{
AI: d.String("AI"),
}
MonsterAI[record.AI] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d MonsterAI records", len(MonsterAI))
}

View File

@ -1,92 +0,0 @@
package d2datadict
import (
"fmt"
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
const (
numMonEquippedItems = 3
fmtLocation = "loc%d"
fmtQuality = "mod%d"
fmtCode = "item%d"
)
// 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
}
// MonsterEquipment stores the MonsterEquipmentRecords
var MonsterEquipment map[string][]*MonsterEquipmentRecord //nolint:gochecknoglobals // Currently global by design
// LoadMonsterEquipment loads MonsterEquipmentRecords into MonsterEquipment
func LoadMonsterEquipment(file []byte) {
MonsterEquipment = make(map[string][]*MonsterEquipmentRecord)
d := d2txt.LoadDataDictionary(file)
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 := MonsterEquipment[record.Name]; !ok {
MonsterEquipment[record.Name] = make([]*MonsterEquipmentRecord, 0)
}
MonsterEquipment[record.Name] = append(MonsterEquipment[record.Name], record)
}
if d.Err != nil {
panic(d.Err)
}
length := 0
for k := range MonsterEquipment {
length += len(MonsterEquipment[k])
}
log.Printf("Loaded %d MonsterEquipment records", length)
}

View File

@ -1,108 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// 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
}
// MonsterLevels stores the MonsterLevelRecords
var MonsterLevels map[int]*MonsterLevelRecord //nolint:gochecknoglobals // Currently global by design
// LoadMonsterLevels loads LoadMonsterLevelRecords into MonsterLevels
func LoadMonsterLevels(file []byte) {
MonsterLevels = make(map[int]*MonsterLevelRecord)
d := d2txt.LoadDataDictionary(file)
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)"),
},
},
}
MonsterLevels[record.Level] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d MonsterLevel records", len(MonsterLevels))
}

View File

@ -1,27 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// MonsterPlacementRecord represents a line from MonPlace.txt.
type MonsterPlacementRecord string
// MonsterPlacements stores the MonsterPlacementRecords.
var MonsterPlacements []MonsterPlacementRecord //nolint:gochecknoglobals // Currently global by design
// LoadMonsterPlacements loads the MonsterPlacementRecords into MonsterPlacements.
func LoadMonsterPlacements(file []byte) {
d := d2txt.LoadDataDictionary(file)
for d.Next() {
MonsterPlacements = append(MonsterPlacements, MonsterPlacementRecord(d.String("code")))
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d MonsterPlacement records", len(MonsterPlacements))
}

View File

@ -1,70 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// 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
}
// MonsterSequences contains the MonsterSequenceRecords
// nolint:gochecknoglobals // Currently global by design
var MonsterSequences map[string]*MonsterSequenceRecord
// LoadMonsterSequences loads the MonsterSequenceRecords into MonsterSequences
func LoadMonsterSequences(file []byte) {
MonsterSequences = make(map[string]*MonsterSequenceRecord)
d := d2txt.LoadDataDictionary(file)
for d.Next() {
name := d.String("sequence")
if _, ok := MonsterSequences[name]; !ok {
record := &MonsterSequenceRecord{
Name: name,
Frames: make([]*MonsterSequenceFrame, 0),
}
MonsterSequences[name] = record
}
MonsterSequences[name].Frames = append(MonsterSequences[name].Frames, &MonsterSequenceFrame{
Mode: d.String("mode"),
Frame: d.Number("frame"),
Direction: d.Number("dir"),
Event: d.Number("event"),
})
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d MonsterSequence records", len(MonsterSequences))
}

View File

@ -1,163 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// Information gathered from [https://d2mods.info/forum/kb/viewarticle?a=418]
// 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
}
// MonsterSounds stores the MonsterSoundRecords
//nolint:gochecknoglobals // Currently global by design
var MonsterSounds map[string]*MonsterSoundRecord
// LoadMonsterSounds loads MonsterSoundRecords into MonsterSounds
func LoadMonsterSounds(file []byte) {
MonsterSounds = make(map[string]*MonsterSoundRecord)
d := d2txt.LoadDataDictionary(file)
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"),
}
MonsterSounds[record.ID] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d MonsterUniqueModifier records", len(MonsterUniqueModifiers))
}

View File

@ -1,119 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
const (
numModifierConstants = 34
)
// MonsterUniqueModifierRecord represents a single line in monumod.txt
// Information gathered from [https://d2mods.info/forum/kb/viewarticle?a=161]
type MonsterUniqueModifierRecord 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
}
// MonsterUniqueModifiers stores the MonsterUniqueModifierRecords
var MonsterUniqueModifiers map[string]*MonsterUniqueModifierRecord //nolint:gochecknoglobals // Currently global by design
// MonsterUniqueModifierConstants contains constants from monumod.txt,
// can be accessed with indices from d2enum.MonUModConstIndex
var MonsterUniqueModifierConstants []int //nolint:gochecknoglobals // currently global by design
// See [https://d2mods.info/forum/kb/viewarticle?a=161] for more info
// LoadMonsterUniqueModifiers loads MonsterUniqueModifierRecords into MonsterUniqueModifiers
func LoadMonsterUniqueModifiers(file []byte) {
MonsterUniqueModifiers = make(map[string]*MonsterUniqueModifierRecord)
MonsterUniqueModifierConstants = make([]int, 0, numModifierConstants)
d := d2txt.LoadDataDictionary(file)
for d.Next() {
record := &MonsterUniqueModifierRecord{
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)"),
},
},
}
MonsterUniqueModifiers[record.Name] = record
if len(MonsterUniqueModifierConstants) < numModifierConstants {
MonsterUniqueModifierConstants = append(MonsterUniqueModifierConstants, d.Number("constants"))
}
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d MonsterUniqueModifier records", len(MonsterUniqueModifiers))
}

View File

@ -1,47 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// 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
}
// MonTypes stores all of the MonTypeRecords
var MonTypes map[string]*MonTypeRecord //nolint:gochecknoglobals // Currently global by design, only written once
// LoadMonTypes loads MonType records into a map[string]*MonTypeRecord
func LoadMonTypes(file []byte) {
MonTypes = make(map[string]*MonTypeRecord)
d := d2txt.LoadDataDictionary(file)
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"),
}
MonTypes[record.Type] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d MonType records", len(MonTypes))
}

View File

@ -1,107 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
const (
costDivisor = 1024.
)
// 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
}
// NPCs stores the NPCRecords
var NPCs map[string]*NPCRecord // nolint:gochecknoglobals // Currently global by design
// LoadNPCs loads NPCRecords into NPCs
func LoadNPCs(file []byte) {
NPCs = make(map[string]*NPCRecord)
d := d2txt.LoadDataDictionary(file)
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,
}
}
NPCs[record.Name] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d NPC records", len(NPCs))
}

View File

@ -1,266 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2calculation"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2calculation/d2parser"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// SkillDescriptionRecord is a single row from skilldesc.txt and is used for
// generating text strings for skills.
type SkillDescriptionRecord struct {
Name string // skilldesc
SkillPage string // SkillPage
SkillRow string // SkillRow
SkillColumn string // SkillColumn
ListRow string // ListRow
ListPool string // ListPool
IconCel int // IconCel
NameKey string // str name
ShortKey string // str short
LongKey string // str long
AltKey string // str alt
ManaKey string // str mana
Descdam string // descdam
DdamCalc1 d2calculation.Calculation // ddam calc1
DdamCalc2 d2calculation.Calculation // ddam calc2
P1dmelem string // p1dmelem
P1dmmin d2calculation.Calculation // p1dmmin
P1dmmax d2calculation.Calculation // p1dmmax
P2dmelem string // p2dmelem
P2dmmin d2calculation.Calculation // p2dmmin
P2dmmax d2calculation.Calculation // p2dmmax
P3dmelem string // p3dmelem
P3dmmin d2calculation.Calculation // p3dmmin
P3dmmax d2calculation.Calculation // p3dmmax
Descatt string // descatt
Descmissile1 string // descmissile1
Descmissile2 string // descmissile2
Descmissile3 string // descmissile3
Descline1 string // descline1
Desctexta1 string // desctexta1
Desctextb1 string // desctextb1
Desccalca1 d2calculation.Calculation // desccalca1
Desccalcb1 d2calculation.Calculation // desccalcb1
Descline2 string // descline2
Desctexta2 string // desctexta2
Desctextb2 string // desctextb2
Desccalca2 d2calculation.Calculation // desccalca2
Desccalcb2 d2calculation.Calculation // desccalcb2
Descline3 string // descline3
Desctexta3 string // desctexta3
Desctextb3 string // desctextb3
Desccalca3 d2calculation.Calculation // desccalca3
Desccalcb3 d2calculation.Calculation // desccalcb3
Descline4 string // descline4
Desctexta4 string // desctexta4
Desctextb4 string // desctextb4
Desccalca4 d2calculation.Calculation // desccalca4
Desccalcb4 d2calculation.Calculation // desccalcb4
Descline5 string // descline5
Desctexta5 string // desctexta5
Desctextb5 string // desctextb5
Desccalca5 d2calculation.Calculation // desccalca5
Desccalcb5 d2calculation.Calculation // desccalcb5
Descline6 string // descline6
Desctexta6 string // desctexta6
Desctextb6 string // desctextb6
Desccalca6 d2calculation.Calculation // desccalca6
Desccalcb6 d2calculation.Calculation // desccalcb6
Dsc2line1 string // dsc2line1
Dsc2texta1 string // dsc2texta1
Dsc2textb1 string // dsc2textb1
Dsc2calca1 d2calculation.Calculation // dsc2calca1
Dsc2calcb1 d2calculation.Calculation // dsc2calcb1
Dsc2line2 string // dsc2line2
Dsc2texta2 string // dsc2texta2
Dsc2textb2 string // dsc2textb2
Dsc2calca2 d2calculation.Calculation // dsc2calca2
Dsc2calcb2 d2calculation.Calculation // dsc2calcb2
Dsc2line3 string // dsc2line3
Dsc2texta3 string // dsc2texta3
Dsc2textb3 string // dsc2textb3
Dsc2calca3 d2calculation.Calculation // dsc2calca3
Dsc2calcb3 d2calculation.Calculation // dsc2calcb3
Dsc2line4 string // dsc2line4
Dsc2texta4 string // dsc2texta4
Dsc2textb4 string // dsc2textb4
Dsc2calca4 d2calculation.Calculation // dsc2calca4
Dsc2calcb4 d2calculation.Calculation // dsc2calcb4
Dsc3line1 string // dsc3line1
Dsc3texta1 string // dsc3texta1
Dsc3textb1 string // dsc3textb1
Dsc3calca1 d2calculation.Calculation // dsc3calca1
Dsc3calcb1 d2calculation.Calculation // dsc3calcb1
Dsc3line2 string // dsc3line2
Dsc3texta2 string // dsc3texta2
Dsc3textb2 string // dsc3textb2
Dsc3calca2 d2calculation.Calculation // dsc3calca2
Dsc3calcb2 d2calculation.Calculation // dsc3calcb2
Dsc3line3 string // dsc3line3
Dsc3texta3 string // dsc3texta3
Dsc3textb3 string // dsc3textb3
Dsc3calca3 d2calculation.Calculation // dsc3calca3
Dsc3calcb3 d2calculation.Calculation // dsc3calcb3
Dsc3line4 string // dsc3line4
Dsc3texta4 string // dsc3texta4
Dsc3textb4 string // dsc3textb4
Dsc3calca4 d2calculation.Calculation // dsc3calca4
Dsc3calcb4 d2calculation.Calculation // dsc3calcb4
Dsc3line5 string // dsc3line5
Dsc3texta5 string // dsc3texta5
Dsc3textb5 string // dsc3textb5
Dsc3calca5 d2calculation.Calculation // dsc3calca5
Dsc3calcb5 d2calculation.Calculation // dsc3calcb5
Dsc3line6 string // dsc3line6
Dsc3texta6 string // dsc3texta6
Dsc3textb6 string // dsc3textb6
Dsc3calca6 d2calculation.Calculation // dsc3calca6
Dsc3calcb6 d2calculation.Calculation // dsc3calcb6
Dsc3line7 string // dsc3line7
Dsc3texta7 string // dsc3texta7
Dsc3textb7 string // dsc3textb7
Dsc3calca7 d2calculation.Calculation // dsc3calca7
Dsc3calcb7 d2calculation.Calculation // dsc3calcb7
}
// SkillDescriptions stores all of the SkillDescriptionRecords
//nolint:gochecknoglobals // Currently global by design
var SkillDescriptions map[string]*SkillDescriptionRecord
// LoadSkillDescriptions loads skill description records from skilldesc.txt
func LoadSkillDescriptions(file []byte) { //nolint:funlen // doesn't make sense to split
SkillDescriptions = make(map[string]*SkillDescriptionRecord)
parser := d2parser.New()
parser.SetCurrentReference("skill", "TODO: connect skill with description!") //nolint:godox // TODO: Connect skill with description.
d := d2txt.LoadDataDictionary(file)
for d.Next() {
record := &SkillDescriptionRecord{
d.String("skilldesc"),
d.String("SkillPage"),
d.String("SkillRow"),
d.String("SkillColumn"),
d.String("ListRow"),
d.String("ListPool"),
d.Number("IconCel"),
d.String("str name"),
d.String("str short"),
d.String("str long"),
d.String("str alt"),
d.String("str mana"),
d.String("descdam"),
parser.Parse(d.String("ddam calc1")),
parser.Parse(d.String("ddam calc2")),
d.String("p1dmelem"),
parser.Parse(d.String("p1dmmin")),
parser.Parse(d.String("p1dmmax")),
d.String("p2dmelem"),
parser.Parse(d.String("p2dmmin")),
parser.Parse(d.String("p2dmmax")),
d.String("p3dmelem"),
parser.Parse(d.String("p3dmmin")),
parser.Parse(d.String("p3dmmax")),
d.String("descatt"),
d.String("descmissile1"),
d.String("descmissile2"),
d.String("descmissile3"),
d.String("descline1"),
d.String("desctexta1"),
d.String("desctextb1"),
parser.Parse(d.String("desccalca1")),
parser.Parse(d.String("desccalcb1")),
d.String("descline2"),
d.String("desctexta2"),
d.String("desctextb2"),
parser.Parse(d.String("desccalca2")),
parser.Parse(d.String("desccalcb2")),
d.String("descline3"),
d.String("desctexta3"),
d.String("desctextb3"),
parser.Parse(d.String("desccalca3")),
parser.Parse(d.String("desccalcb3")),
d.String("descline4"),
d.String("desctexta4"),
d.String("desctextb4"),
parser.Parse(d.String("desccalca4")),
parser.Parse(d.String("desccalcb4")),
d.String("descline5"),
d.String("desctexta5"),
d.String("desctextb5"),
parser.Parse(d.String("desccalca5")),
parser.Parse(d.String("desccalcb5")),
d.String("descline6"),
d.String("desctexta6"),
d.String("desctextb6"),
parser.Parse(d.String("desccalca6")),
parser.Parse(d.String("desccalcb6")),
d.String("dsc2line1"),
d.String("dsc2texta1"),
d.String("dsc2textb1"),
parser.Parse(d.String("dsc2calca1")),
parser.Parse(d.String("dsc2calcb1")),
d.String("dsc2line2"),
d.String("dsc2texta2"),
d.String("dsc2textb2"),
parser.Parse(d.String("dsc2calca2")),
parser.Parse(d.String("dsc2calcb2")),
d.String("dsc2line3"),
d.String("dsc2texta3"),
d.String("dsc2textb3"),
parser.Parse(d.String("dsc2calca3")),
parser.Parse(d.String("dsc2calcb3")),
d.String("dsc2line4"),
d.String("dsc2texta4"),
d.String("dsc2textb4"),
parser.Parse(d.String("dsc2calca4")),
parser.Parse(d.String("dsc2calcb4")),
d.String("dsc3line1"),
d.String("dsc3texta1"),
d.String("dsc3textb1"),
parser.Parse(d.String("dsc3calca1")),
parser.Parse(d.String("dsc3calcb1")),
d.String("dsc3line2"),
d.String("dsc3texta2"),
d.String("dsc3textb2"),
parser.Parse(d.String("dsc3calca2")),
parser.Parse(d.String("dsc3calcb2")),
d.String("dsc3line3"),
d.String("dsc3texta3"),
d.String("dsc3textb3"),
parser.Parse(d.String("dsc3calca3")),
parser.Parse(d.String("dsc3calcb3")),
d.String("dsc3line4"),
d.String("dsc3texta4"),
d.String("dsc3textb4"),
parser.Parse(d.String("dsc3calca4")),
parser.Parse(d.String("dsc3calcb4")),
d.String("dsc3line5"),
d.String("dsc3texta5"),
d.String("dsc3textb5"),
parser.Parse(d.String("dsc3calca5")),
parser.Parse(d.String("dsc3calcb5")),
d.String("dsc3line6"),
d.String("dsc3texta6"),
d.String("dsc3textb6"),
parser.Parse(d.String("dsc3calca6")),
parser.Parse(d.String("dsc3calcb6")),
d.String("dsc3line7"),
d.String("dsc3texta7"),
d.String("dsc3textb7"),
parser.Parse(d.String("dsc3calca7")),
parser.Parse(d.String("dsc3calcb7")),
}
SkillDescriptions[record.Name] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d Skill Description records", len(SkillDescriptions))
}

View File

@ -1,534 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2calculation"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2calculation/d2parser"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// SkillDetails has all of the SkillRecords
//nolint:gochecknoglobals // Currently global by design, only written once
var SkillDetails map[int]*SkillRecord
var skillDetailsByName map[string]*SkillRecord
// SkillRecord is a row from the skills.txt file. Here are two resources for more info on each field
// [https://d2mods.info/forum/viewtopic.php?t=41556, https://d2mods.info/forum/kb/viewarticle?a=246]
type SkillRecord struct {
Skill string
Charclass string
Skilldesc string
Prgcalc1 d2calculation.Calculation
Prgcalc2 d2calculation.Calculation
Prgcalc3 d2calculation.Calculation
Srvmissile string
Srvmissilea string
Srvmissileb string
Srvmissilec string
Srvoverlay string
Aurastate string
Auratargetstate string
Auralencalc d2calculation.Calculation
Aurarangecalc d2calculation.Calculation
Aurastat1 string
Aurastatcalc1 d2calculation.Calculation
Aurastat2 string
Aurastatcalc2 d2calculation.Calculation
Aurastat3 string
Aurastatcalc3 d2calculation.Calculation
Aurastat4 string
Aurastatcalc4 d2calculation.Calculation
Aurastat5 string
Aurastatcalc5 d2calculation.Calculation
Aurastat6 string
Aurastatcalc6 d2calculation.Calculation
Auraevent1 string
Auraevent2 string
Auraevent3 string
Auratgtevent string
Auratgteventfunc string
Passivestate string
Passiveitype string
Passivestat1 string
Passivecalc1 d2calculation.Calculation
Passivestat2 string
Passivecalc2 d2calculation.Calculation
Passivestat3 string
Passivecalc3 d2calculation.Calculation
Passivestat4 string
Passivecalc4 d2calculation.Calculation
Passivestat5 string
Passivecalc5 d2calculation.Calculation
Passiveevent string
Passiveeventfunc string
Summon string
Pettype string
Petmax d2calculation.Calculation
Summode string
Sumskill1 string
Sumsk1calc d2calculation.Calculation
Sumskill2 string
Sumsk2calc d2calculation.Calculation
Sumskill3 string
Sumsk3calc d2calculation.Calculation
Sumskill4 string
Sumsk4calc d2calculation.Calculation
Sumskill5 string
Sumsk5calc d2calculation.Calculation
Sumoverlay string
Stsound string
Stsoundclass string
Dosound string
DosoundA string
DosoundB string
Tgtoverlay string
Tgtsound string
Prgoverlay string
Prgsound string
Castoverlay string
Cltoverlaya string
Cltoverlayb string
Cltmissile string
Cltmissilea string
Cltmissileb string
Cltmissilec string
Cltmissiled string
Cltcalc1 d2calculation.Calculation
Cltcalc2 d2calculation.Calculation
Cltcalc3 d2calculation.Calculation
Range string
Itypea1 string
Itypea2 string
Itypea3 string
Etypea1 string
Etypea2 string
Itypeb1 string
Itypeb2 string
Itypeb3 string
Etypeb1 string
Etypeb2 string
Anim string
Seqtrans string
Monanim string
ItemCastSound string
ItemCastOverlay string
Skpoints d2calculation.Calculation
Reqskill1 string
Reqskill2 string
Reqskill3 string
State1 string
State2 string
State3 string
Perdelay d2calculation.Calculation
Calc1 d2calculation.Calculation
Calc2 d2calculation.Calculation
Calc3 d2calculation.Calculation
Calc4 d2calculation.Calculation
ToHitCalc d2calculation.Calculation
DmgSymPerCalc d2calculation.Calculation
EType string
EDmgSymPerCalc d2calculation.Calculation
ELenSymPerCalc d2calculation.Calculation
ID int
Srvstfunc int
Srvdofunc int
Srvprgfunc1 int
Srvprgfunc2 int
Srvprgfunc3 int
Prgdam int
Aurafilter int
Auraeventfunc1 int
Auraeventfunc2 int
Auraeventfunc3 int
Sumumod int
Cltstfunc int
Cltdofunc int
Cltprgfunc1 int
Cltprgfunc2 int
Cltprgfunc3 int
Attackrank int
Weapsel int
Seqnum int
Seqinput int
LineOfSight int
SelectProc int
ItemEffect int
ItemCltEffect int
ItemTgtDo int
ItemTarget int
Reqlevel int
Maxlvl int
Reqstr int
Reqdex int
Reqint int
Reqvit int
Restrict int
Delay int
Checkfunc int
Startmana int
Minmana int
Manashift int
Mana int
Lvlmana int
Param1 int
Param2 int
Param3 int
Param4 int
Param5 int
Param6 int
Param7 int
Param8 int
ToHit int
LevToHit int
ResultFlags int
HitFlags int
HitClass int
HitShift int
SrcDam int
MinDam int
MinLevDam1 int
MinLevDam2 int
MinLevDam3 int
MinLevDam4 int
MinLevDam5 int
MaxDam int
MaxLevDam1 int
MaxLevDam2 int
MaxLevDam3 int
MaxLevDam4 int
MaxLevDam5 int
EMin int
EMinLev1 int
EMinLev2 int
EMinLev3 int
EMinLev4 int
EMinLev5 int
EMax int
EMaxLev1 int
EMaxLev2 int
EMaxLev3 int
EMaxLev4 int
EMaxLev5 int
ELen int
ELevLen1 int
ELevLen2 int
ELevLen3 int
Aitype int
Aibonus int
CostMult int
CostAdd int
Prgstack bool
Decquant bool
Lob bool
Stsuccessonly bool
Stsounddelay bool
Weaponsnd bool
Warp bool
Immediate bool
Enhanceable bool
Noammo bool
Durability bool
UseAttackRate bool
TargetableOnly bool
SearchEnemyXY bool
SearchEnemyNear bool
SearchOpenXY bool
TargetCorpse bool
TargetPet bool
TargetAlly bool
TargetItem bool
AttackNoMana bool
TgtPlaceCheck bool
ItemCheckStart bool
ItemCltCheckStart bool
Leftskill bool
Repeat bool
Nocostinstate bool
Usemanaondo bool
Interrupt bool
InTown bool
Aura bool
Periodic bool
Finishing bool
Passive bool
Progressive bool
General bool
Scroll bool
InGame bool
Kick bool
}
// LoadSkills loads skills.txt file contents into a skill record map
//nolint:funlen // Makes no sense to split
// LoadCharStats loads charstats.txt file contents into map[d2enum.Hero]*CharStatsRecord
func LoadSkills(file []byte) {
SkillDetails = make(map[int]*SkillRecord)
skillDetailsByName = make(map[string]*SkillRecord)
parser := d2parser.New()
d := d2txt.LoadDataDictionary(file)
for d.Next() {
name := d.String("skill")
parser.SetCurrentReference("skill", name)
record := &SkillRecord{
Skill: d.String("skill"),
ID: d.Number("Id"),
Charclass: d.String("charclass"),
Skilldesc: d.String("skilldesc"),
Srvstfunc: d.Number("srvstfunc"),
Srvdofunc: d.Number("srvdofunc"),
Prgstack: d.Bool("prgstack"),
Srvprgfunc1: d.Number("srvprgfunc1"),
Srvprgfunc2: d.Number("srvprgfunc2"),
Srvprgfunc3: d.Number("srvprgfunc3"),
Prgcalc1: parser.Parse(d.String("prgcalc1")),
Prgcalc2: parser.Parse(d.String("prgcalc2")),
Prgcalc3: parser.Parse(d.String("prgcalc3")),
Prgdam: d.Number("prgdam"),
Srvmissile: d.String("srvmissile"),
Decquant: d.Bool("decquant"),
Lob: d.Bool("lob"),
Srvmissilea: d.String("srvmissilea"),
Srvmissileb: d.String("srvmissileb"),
Srvmissilec: d.String("srvmissilec"),
Srvoverlay: d.String("srvoverlay"),
Aurafilter: d.Number("aurafilter"),
Aurastate: d.String("aurastate"),
Auratargetstate: d.String("auratargetstate"),
Auralencalc: parser.Parse(d.String("auralencalc")),
Aurarangecalc: parser.Parse(d.String("aurarangecalc")),
Aurastat1: d.String("aurastat1"),
Aurastatcalc1: parser.Parse(d.String("aurastatcalc1")),
Aurastat2: d.String("aurastat2"),
Aurastatcalc2: parser.Parse(d.String("aurastatcalc2")),
Aurastat3: d.String("aurastat3"),
Aurastatcalc3: parser.Parse(d.String("aurastatcalc3")),
Aurastat4: d.String("aurastat4"),
Aurastatcalc4: parser.Parse(d.String("aurastatcalc4")),
Aurastat5: d.String("aurastat5"),
Aurastatcalc5: parser.Parse(d.String("aurastatcalc5")),
Aurastat6: d.String("aurastat6"),
Aurastatcalc6: parser.Parse(d.String("aurastatcalc6")),
Auraevent1: d.String("auraevent1"),
Auraeventfunc1: d.Number("auraeventfunc1"),
Auraevent2: d.String("auraevent2"),
Auraeventfunc2: d.Number("auraeventfunc2"),
Auraevent3: d.String("auraevent3"),
Auraeventfunc3: d.Number("auraeventfunc3"),
Auratgtevent: d.String("auratgtevent"),
Auratgteventfunc: d.String("auratgteventfunc"),
Passivestate: d.String("passivestate"),
Passiveitype: d.String("passiveitype"),
Passivestat1: d.String("passivestat1"),
Passivecalc1: parser.Parse(d.String("passivecalc1")),
Passivestat2: d.String("passivestat2"),
Passivecalc2: parser.Parse(d.String("passivecalc2")),
Passivestat3: d.String("passivestat3"),
Passivecalc3: parser.Parse(d.String("passivecalc3")),
Passivestat4: d.String("passivestat4"),
Passivecalc4: parser.Parse(d.String("passivecalc4")),
Passivestat5: d.String("passivestat5"),
Passivecalc5: parser.Parse(d.String("passivecalc5")),
Passiveevent: d.String("passiveevent"),
Passiveeventfunc: d.String("passiveeventfunc"),
Summon: d.String("summon"),
Pettype: d.String("pettype"),
Petmax: parser.Parse(d.String("petmax")),
Summode: d.String("summode"),
Sumskill1: d.String("sumskill1"),
Sumsk1calc: parser.Parse(d.String("sumsk1calc")),
Sumskill2: d.String("sumskill2"),
Sumsk2calc: parser.Parse(d.String("sumsk2calc")),
Sumskill3: d.String("sumskill3"),
Sumsk3calc: parser.Parse(d.String("sumsk3calc")),
Sumskill4: d.String("sumskill4"),
Sumsk4calc: parser.Parse(d.String("sumsk4calc")),
Sumskill5: d.String("sumskill5"),
Sumsk5calc: parser.Parse(d.String("sumsk5calc")),
Sumumod: d.Number("sumumod"),
Sumoverlay: d.String("sumoverlay"),
Stsuccessonly: d.Bool("stsuccessonly"),
Stsound: d.String("stsound"),
Stsoundclass: d.String("stsoundclass"),
Stsounddelay: d.Bool("stsounddelay"),
Weaponsnd: d.Bool("weaponsnd"),
Dosound: d.String("dosound"),
DosoundA: d.String("dosound a"),
DosoundB: d.String("dosound b"),
Tgtoverlay: d.String("tgtoverlay"),
Tgtsound: d.String("tgtsound"),
Prgoverlay: d.String("prgoverlay"),
Prgsound: d.String("prgsound"),
Castoverlay: d.String("castoverlay"),
Cltoverlaya: d.String("cltoverlaya"),
Cltoverlayb: d.String("cltoverlayb"),
Cltstfunc: d.Number("cltstfunc"),
Cltdofunc: d.Number("cltdofunc"),
Cltprgfunc1: d.Number("cltprgfunc1"),
Cltprgfunc2: d.Number("cltprgfunc2"),
Cltprgfunc3: d.Number("cltprgfunc3"),
Cltmissile: d.String("cltmissile"),
Cltmissilea: d.String("cltmissilea"),
Cltmissileb: d.String("cltmissileb"),
Cltmissilec: d.String("cltmissilec"),
Cltmissiled: d.String("cltmissiled"),
Cltcalc1: parser.Parse(d.String("cltcalc1")),
Cltcalc2: parser.Parse(d.String("cltcalc2")),
Cltcalc3: parser.Parse(d.String("cltcalc3")),
Warp: d.Bool("warp"),
Immediate: d.Bool("immediate"),
Enhanceable: d.Bool("enhanceable"),
Attackrank: d.Number("attackrank"),
Noammo: d.Bool("noammo"),
Range: d.String("range"),
Weapsel: d.Number("weapsel"),
Itypea1: d.String("itypea1"),
Itypea2: d.String("itypea2"),
Itypea3: d.String("itypea3"),
Etypea1: d.String("etypea1"),
Etypea2: d.String("etypea2"),
Itypeb1: d.String("itypeb1"),
Itypeb2: d.String("itypeb2"),
Itypeb3: d.String("itypeb3"),
Etypeb1: d.String("etypeb1"),
Etypeb2: d.String("etypeb2"),
Anim: d.String("anim"),
Seqtrans: d.String("seqtrans"),
Monanim: d.String("monanim"),
Seqnum: d.Number("seqnum"),
Seqinput: d.Number("seqinput"),
Durability: d.Bool("durability"),
UseAttackRate: d.Bool("UseAttackRate"),
LineOfSight: d.Number("LineOfSight"),
TargetableOnly: d.Bool("TargetableOnly"),
SearchEnemyXY: d.Bool("SearchEnemyXY"),
SearchEnemyNear: d.Bool("SearchEnemyNear"),
SearchOpenXY: d.Bool("SearchOpenXY"),
SelectProc: d.Number("SelectProc"),
TargetCorpse: d.Bool("TargetCorpse"),
TargetPet: d.Bool("TargetPet"),
TargetAlly: d.Bool("TargetAlly"),
TargetItem: d.Bool("TargetItem"),
AttackNoMana: d.Bool("AttackNoMana"),
TgtPlaceCheck: d.Bool("TgtPlaceCheck"),
ItemEffect: d.Number("ItemEffect"),
ItemCltEffect: d.Number("ItemCltEffect"),
ItemTgtDo: d.Number("ItemTgtDo"),
ItemTarget: d.Number("ItemTarget"),
ItemCheckStart: d.Bool("ItemCheckStart"),
ItemCltCheckStart: d.Bool("ItemCltCheckStart"),
ItemCastSound: d.String("ItemCastSound"),
ItemCastOverlay: d.String("ItemCastOverlay"),
Skpoints: parser.Parse(d.String("skpoints")),
Reqlevel: d.Number("reqlevel"),
Maxlvl: d.Number("maxlvl"),
Reqstr: d.Number("reqstr"),
Reqdex: d.Number("reqdex"),
Reqint: d.Number("reqint"),
Reqvit: d.Number("reqvit"),
Reqskill1: d.String("reqskill1"),
Reqskill2: d.String("reqskill2"),
Reqskill3: d.String("reqskill3"),
Restrict: d.Number("restrict"),
State1: d.String("State1"),
State2: d.String("State2"),
State3: d.String("State3"),
Delay: d.Number("delay"),
Leftskill: d.Bool("leftskill"),
Repeat: d.Bool("repeat"),
Checkfunc: d.Number("checkfunc"),
Nocostinstate: d.Bool("nocostinstate"),
Usemanaondo: d.Bool("usemanaondo"),
Startmana: d.Number("startmana"),
Minmana: d.Number("minmana"),
Manashift: d.Number("manashift"),
Mana: d.Number("mana"),
Lvlmana: d.Number("lvlmana"),
Interrupt: d.Bool("interrupt"),
InTown: d.Bool("InTown"),
Aura: d.Bool("aura"),
Periodic: d.Bool("periodic"),
Perdelay: parser.Parse(d.String("perdelay")),
Finishing: d.Bool("finishing"),
Passive: d.Bool("passive"),
Progressive: d.Bool("progressive"),
General: d.Bool("general"),
Scroll: d.Bool("scroll"),
Calc1: parser.Parse(d.String("calc1")),
Calc2: parser.Parse(d.String("calc2")),
Calc3: parser.Parse(d.String("calc3")),
Calc4: parser.Parse(d.String("calc4")),
Param1: d.Number("Param1"),
Param2: d.Number("Param2"),
Param3: d.Number("Param3"),
Param4: d.Number("Param4"),
Param5: d.Number("Param5"),
Param6: d.Number("Param6"),
Param7: d.Number("Param7"),
Param8: d.Number("Param8"),
InGame: d.Bool("InGame"),
ToHit: d.Number("ToHit"),
LevToHit: d.Number("LevToHit"),
ToHitCalc: parser.Parse(d.String("ToHitCalc")),
ResultFlags: d.Number("ResultFlags"),
HitFlags: d.Number("HitFlags"),
HitClass: d.Number("HitClass"),
Kick: d.Bool("Kick"),
HitShift: d.Number("HitShift"),
SrcDam: d.Number("SrcDam"),
MinDam: d.Number("MinDam"),
MinLevDam1: d.Number("MinLevDam1"),
MinLevDam2: d.Number("MinLevDam2"),
MinLevDam3: d.Number("MinLevDam3"),
MinLevDam4: d.Number("MinLevDam4"),
MinLevDam5: d.Number("MinLevDam5"),
MaxDam: d.Number("MaxDam"),
MaxLevDam1: d.Number("MaxLevDam1"),
MaxLevDam2: d.Number("MaxLevDam2"),
MaxLevDam3: d.Number("MaxLevDam3"),
MaxLevDam4: d.Number("MaxLevDam4"),
MaxLevDam5: d.Number("MaxLevDam5"),
DmgSymPerCalc: parser.Parse(d.String("DmgSymPerCalc")),
EType: d.String("EType"),
EMin: d.Number("EMin"),
EMinLev1: d.Number("EMinLev1"),
EMinLev2: d.Number("EMinLev2"),
EMinLev3: d.Number("EMinLev3"),
EMinLev4: d.Number("EMinLev4"),
EMinLev5: d.Number("EMinLev5"),
EMax: d.Number("EMax"),
EMaxLev1: d.Number("EMaxLev1"),
EMaxLev2: d.Number("EMaxLev2"),
EMaxLev3: d.Number("EMaxLev3"),
EMaxLev4: d.Number("EMaxLev4"),
EMaxLev5: d.Number("EMaxLev5"),
EDmgSymPerCalc: parser.Parse(d.String("EDmgSymPerCalc")),
ELen: d.Number("ELen"),
ELevLen1: d.Number("ELevLen1"),
ELevLen2: d.Number("ELevLen2"),
ELevLen3: d.Number("ELevLen3"),
ELenSymPerCalc: parser.Parse(d.String("ELenSymPerCalc")),
Aitype: d.Number("aitype"),
Aibonus: d.Number("aibonus"),
CostMult: d.Number("cost mult"),
CostAdd: d.Number("cost add"),
}
SkillDetails[record.ID] = record
skillDetailsByName[record.Skill] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d Skill records", len(SkillDetails))
}
// GetSkillByName returns the skill record for the given Skill name.
func GetSkillByName(skillName string) *SkillRecord {
return skillDetailsByName[skillName]
}

View File

@ -1,81 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// SoundEnvironRecord describes the different sound environments. Not listed on Phrozen Keep.
type SoundEnvironRecord struct {
Handle string
Index int
Song int
DayAmbience int
NightAmbience int
DayEvent int
NightEvent int
EventDelay int
Indoors int
Material1 int
Material2 int
EAXEnviron int
EAXEnvSize int
EAXEnvDiff int
EAXRoomVol int
EAXRoomHF int
EAXDecayTime int
EAXDecayHF int
EAXReflect int
EAXReflectDelay int
EAXReverb int
EAXRevDelay int
EAXRoomRoll int
EAXAirAbsorb int
}
// SoundEnvirons contains the SoundEnviron records
//nolint:gochecknoglobals // Currently global by design, only written once
var SoundEnvirons map[int]*SoundEnvironRecord
// LoadSoundEnvirons loads SoundEnvirons from the supplied file
func LoadSoundEnvirons(file []byte) {
SoundEnvirons = make(map[int]*SoundEnvironRecord)
d := d2txt.LoadDataDictionary(file)
for d.Next() {
record := &SoundEnvironRecord{
Handle: d.String("Handle"),
Index: d.Number("Index"),
Song: d.Number("Song"),
DayAmbience: d.Number("Day Ambience"),
NightAmbience: d.Number("Night Ambience"),
DayEvent: d.Number("Day Event"),
NightEvent: d.Number("Night Event"),
EventDelay: d.Number("Event Delay"),
Indoors: d.Number("Indoors"),
Material1: d.Number("Material 1"),
Material2: d.Number("Material 2"),
EAXEnviron: d.Number("EAX Environ"),
EAXEnvSize: d.Number("EAX Env Size"),
EAXEnvDiff: d.Number("EAX Env Diff"),
EAXRoomVol: d.Number("EAX Room Vol"),
EAXRoomHF: d.Number("EAX Room HF"),
EAXDecayTime: d.Number("EAX Decay Time"),
EAXDecayHF: d.Number("EAX Decay HF"),
EAXReflect: d.Number("EAX Reflect"),
EAXReflectDelay: d.Number("EAX Reflect Delay"),
EAXReverb: d.Number("EAX Reverb"),
EAXRevDelay: d.Number("EAX Rev Delay"),
EAXRoomRoll: d.Number("EAX Room Roll"),
EAXAirAbsorb: d.Number("EAX Air Absorb"),
}
SoundEnvirons[record.Index] = record
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d SoundEnviron records", len(SoundEnvirons))
}

View File

@ -1,94 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
)
// SoundEntry represents a sound entry
type SoundEntry struct {
Handle string
FileName string
Index int
Volume int
GroupSize int
FadeIn int
FadeOut int
Duration int
Compound int
Reverb int
Falloff int
Priority int
Block1 int
Block2 int
Block3 int
Loop bool
DeferInst bool
StopInst bool
Cache bool
AsyncOnly bool
Stream bool
Stereo bool
Tracking bool
Solo bool
MusicVol bool
}
// Sounds stores all of the SoundEntries
//nolint:gochecknoglobals // Currently global by design, only written once
var Sounds map[string]*SoundEntry
// LoadSounds loads SoundEntries from sounds.txt
func LoadSounds(file []byte) {
Sounds = make(map[string]*SoundEntry)
d := d2txt.LoadDataDictionary(file)
for d.Next() {
entry := &SoundEntry{
Handle: d.String("Sound"),
Index: d.Number("Index"),
FileName: d.String("FileName"),
Volume: d.Number("Volume"),
GroupSize: d.Number("Group Size"),
Loop: d.Bool("Loop"),
FadeIn: d.Number("Fade In"),
FadeOut: d.Number("Fade Out"),
DeferInst: d.Bool("Defer Inst"),
StopInst: d.Bool("Stop Inst"),
Duration: d.Number("Duration"),
Compound: d.Number("Compound"),
Reverb: d.Number("Reverb"),
Falloff: d.Number("Falloff"),
Cache: d.Bool("Cache"),
AsyncOnly: d.Bool("Async Only"),
Priority: d.Number("Priority"),
Stream: d.Bool("Stream"),
Stereo: d.Bool("Stereo"),
Tracking: d.Bool("Tracking"),
Solo: d.Bool("Solo"),
MusicVol: d.Bool("Music Vol"),
Block1: d.Number("Block 1"),
Block2: d.Number("Block 2"),
Block3: d.Number("Block 3"),
}
Sounds[entry.Handle] = entry
}
if d.Err != nil {
panic(d.Err)
}
log.Printf("Loaded %d sound definitions", len(Sounds))
}
// SelectSoundByIndex selects a sound by its ID
func SelectSoundByIndex(index int) *SoundEntry {
for idx := range Sounds {
if Sounds[idx].Index == index {
return Sounds[idx]
}
}
return nil
}

View File

@ -1,16 +0,0 @@
package d2datadict
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
)
// Weapons stores all of the WeaponRecords
var Weapons map[string]*ItemCommonRecord //nolint:gochecknoglobals // Currently global by design, only written once
// LoadWeapons loads weapon records
func LoadWeapons(file []byte) {
Weapons = LoadCommonItems(file, d2enum.InventoryItemTypeWeapon)
log.Printf("Loaded %d weapons", len(Weapons))
}

View File

@ -4,7 +4,6 @@ package ebiten
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
@ -19,7 +18,7 @@ var _ d2interface.AudioProvider = &AudioProvider{} // Static check to confirm st
// CreateAudio creates an instance of ebiten's audio provider
func CreateAudio(am *d2asset.AssetManager) (*AudioProvider, error) {
result := &AudioProvider{
assetManager: am,
asset: am,
}
var err error
@ -35,7 +34,7 @@ func CreateAudio(am *d2asset.AssetManager) (*AudioProvider, error) {
// AudioProvider represents a provider capable of playing audio
type AudioProvider struct {
assetManager *d2asset.AssetManager
asset *d2asset.AssetManager
audioContext *audio.Context // The Audio context
bgmAudio *audio.Player // The audio player
lastBgm string
@ -65,7 +64,7 @@ func (eap *AudioProvider) PlayBGM(song string) {
}
}
audioStream, err := eap.assetManager.LoadFileStream(song)
audioStream, err := eap.asset.LoadFileStream(song)
if err != nil {
panic(err)
@ -128,17 +127,17 @@ func (eap *AudioProvider) createSoundEffect(sfx string, context *audio.Context,
soundFile := "/data/global/sfx/"
if _, exists := d2datadict.Sounds[sfx]; exists {
soundEntry := d2datadict.Sounds[sfx]
if _, exists := eap.asset.Records.Sound.Details[sfx]; exists {
soundEntry := eap.asset.Records.Sound.Details[sfx]
soundFile += soundEntry.FileName
} else {
soundFile += sfx
}
audioData, err := eap.assetManager.LoadFileStream(soundFile)
audioData, err := eap.asset.LoadFileStream(soundFile)
if err != nil {
audioData, err = eap.assetManager.LoadFileStream("/data/global/music/" + sfx)
audioData, err = eap.asset.LoadFileStream("/data/global/music/" + sfx)
}
if err != nil {

View File

@ -4,7 +4,10 @@ import (
"log"
"math/rand"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
)
@ -23,7 +26,7 @@ const originalFPS float64 = 25
// A Sound that can be started and stopped
type Sound struct {
effect d2interface.SoundEffect
entry *d2datadict.SoundEntry
entry *d2records.SoundDetailsRecord
volume float64
vTarget float64
vRate float64
@ -95,6 +98,7 @@ func (s *Sound) Stop() {
// SoundEngine provides functions for playing sounds
type SoundEngine struct {
asset *d2asset.AssetManager
provider d2interface.AudioProvider
timer float64
accTime float64
@ -102,8 +106,10 @@ type SoundEngine struct {
}
// NewSoundEngine creates a new sound engine
func NewSoundEngine(provider d2interface.AudioProvider, term d2interface.Terminal) *SoundEngine {
func NewSoundEngine(provider d2interface.AudioProvider,
asset *d2asset.AssetManager, term d2interface.Terminal) *SoundEngine {
r := SoundEngine{
asset: asset,
provider: provider,
sounds: map[*Sound]struct{}{},
timer: 1,
@ -173,10 +179,10 @@ func (s *SoundEngine) PlaySoundID(id int) *Sound {
return nil
}
entry := d2datadict.SelectSoundByIndex(id)
entry := s.asset.Records.SelectSoundByIndex(id)
if entry.GroupSize > 0 {
entry = d2datadict.SelectSoundByIndex(entry.Index + rand.Intn(entry.GroupSize))
entry = s.asset.Records.SelectSoundByIndex(entry.Index + rand.Intn(entry.GroupSize))
}
effect, _ := s.provider.LoadSound(entry.FileName, entry.Loop, entry.MusicVol)
@ -195,6 +201,6 @@ func (s *SoundEngine) PlaySoundID(id int) *Sound {
// PlaySoundHandle plays a sound by sounds.txt handle
func (s *SoundEngine) PlaySoundHandle(handle string) *Sound {
sound := d2datadict.Sounds[handle].Index
sound := s.asset.Records.Sound.Details[handle].Index
return s.PlaySoundID(sound)
}

View File

@ -3,14 +3,14 @@ package d2audio
import (
"math/rand"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
)
const assumedFPS = 25
// SoundEnvironment represents the audio environment for map areas
type SoundEnvironment struct {
environment *d2datadict.SoundEnvironRecord
environment *d2records.SoundEnvironRecord
engine *SoundEngine
bgm *Sound
ambiance *Sound
@ -21,7 +21,7 @@ type SoundEnvironment struct {
func NewSoundEnvironment(soundEngine *SoundEngine) SoundEnvironment {
r := SoundEnvironment{
// Start with env NONE
environment: d2datadict.SoundEnvirons[0],
environment: soundEngine.asset.Records.Sound.Environment[0],
engine: soundEngine,
}
@ -31,7 +31,7 @@ func NewSoundEnvironment(soundEngine *SoundEngine) SoundEnvironment {
// SetEnv sets the sound environment using the given record index
func (s *SoundEnvironment) SetEnv(environmentIdx int) {
if s.environment.Index != environmentIdx {
newEnv := d2datadict.SoundEnvirons[environmentIdx]
newEnv := s.engine.asset.Records.Sound.Environment[environmentIdx]
if s.environment.Song != newEnv.Song {
if s.bgm != nil {

View File

@ -4,14 +4,15 @@ import (
"encoding/json"
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
)
// HeroSkill stores additional payload for a skill of a hero.
type HeroSkill struct {
*d2datadict.SkillRecord
*d2datadict.SkillDescriptionRecord
*d2records.SkillRecord
*d2records.SkillDescriptionRecord
SkillPoints int
shallow *shallowHeroSkill
}
// An auxilary struct which only stores the ID of the SkillRecord, instead of the whole SkillRecord and SkillDescrptionRecord.
@ -38,14 +39,12 @@ func (hs *HeroSkill) MarshalJSON() ([]byte, error) {
// UnmarshalJSON overrides the default logic used when the HeroSkill is deserialized from a byte array.
func (hs *HeroSkill) UnmarshalJSON(data []byte) error {
shallow := shallowHeroSkill{}
if err := json.Unmarshal(data, &shallow); err != nil {
shallow := &shallowHeroSkill{}
if err := json.Unmarshal(data, shallow); err != nil {
return err
}
hs.SkillRecord = d2datadict.SkillDetails[shallow.SkillID]
hs.SkillDescriptionRecord = d2datadict.SkillDescriptions[hs.SkillRecord.Skilldesc]
hs.SkillPoints = shallow.SkillPoints
hs.shallow = shallow
return nil
}

View File

@ -1,26 +0,0 @@
package d2hero
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
// HeroSkillsState hold all spells that a hero has.
type HeroSkillsState map[int] *HeroSkill
// CreateHeroSkillsState will assemble the hero skills from the class stats record.
func CreateHeroSkillsState(classStats *d2datadict.CharStatsRecord) *HeroSkillsState {
baseSkills := HeroSkillsState{}
for idx := range classStats.BaseSkill {
skillName := &classStats.BaseSkill[idx]
if len(*skillName) == 0 {
continue
}
skillRecord := d2datadict.GetSkillByName(*skillName)
baseSkills[skillRecord.ID] = &HeroSkill{SkillPoints: 1, SkillRecord: skillRecord}
}
skillRecord := d2datadict.GetSkillByName("Attack")
baseSkills[skillRecord.ID] = &HeroSkill{SkillPoints: 1, SkillRecord: skillRecord}
return &baseSkills
}

View File

@ -0,0 +1,20 @@
package d2hero
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2inventory"
)
// HeroState stores the state of the player
type HeroState struct {
HeroName string `json:"heroName"`
HeroType d2enum.Hero `json:"heroType"`
HeroLevel int `json:"heroLevel"`
Act int `json:"act"`
FilePath string `json:"-"`
Equipment d2inventory.CharacterEquipment `json:"equipment"`
Stats *HeroStatsState `json:"stats"`
Skills map[int]*HeroSkill `json:"skills"`
X float64 `json:"x"`
Y float64 `json:"y"`
}

View File

@ -0,0 +1,235 @@
package d2hero
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"strconv"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2inventory"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
)
// NewHeroStateFactory creates a new HeroStateFactory and initializes it.
func NewHeroStateFactory(asset *d2asset.AssetManager) (*HeroStateFactory, error) {
inventoryItemFactory, err := d2inventory.NewInventoryItemFactory(asset)
if err != nil {
return nil, err
}
factory := &HeroStateFactory{
asset: asset,
InventoryItemFactory: inventoryItemFactory,
}
return factory, nil
}
// HeroStateFactory is responsible for creating player state objects
type HeroStateFactory struct {
asset *d2asset.AssetManager
*d2inventory.InventoryItemFactory
}
// CreateHeroState creates a HeroState instance and returns a pointer to it
func (f *HeroStateFactory) CreateHeroState(
heroName string,
hero d2enum.Hero,
statsState *HeroStatsState,
) (*HeroState, error) {
result := &HeroState{
HeroName: heroName,
HeroType: hero,
Act: 1,
Stats: statsState,
Equipment: f.DefaultHeroItems[hero],
FilePath: "",
}
defaultStats := f.asset.Records.Character.Stats[hero]
skillState, err := f.CreateHeroSkillsState(defaultStats)
if err != nil {
return nil, err
}
result.Skills = skillState
return result, nil
}
// GetAllHeroStates returns all player saves
func (f *HeroStateFactory) GetAllHeroStates() ([]*HeroState, error) {
basePath, _ := f.getGameBaseSavePath()
files, _ := ioutil.ReadDir(basePath)
result := make([]*HeroState, 0)
for _, file := range files {
fileName := file.Name()
if file.IsDir() || len(fileName) < 5 || !strings.EqualFold(fileName[len(fileName)-4:], ".od2") {
continue
}
gameState := f.LoadHeroState(path.Join(basePath, file.Name()))
if gameState == nil || gameState.HeroType == d2enum.HeroNone {
} else if gameState.Stats == nil || gameState.Skills == nil {
// temporarily loading default class stats if the character was created before saving stats/skills was introduced
// to be removed in the future
classStats := f.asset.Records.Character.Stats[gameState.HeroType]
gameState.Stats = f.CreateHeroStatsState(gameState.HeroType, classStats)
skillState, err := f.CreateHeroSkillsState(classStats)
if err != nil {
return nil, err
}
gameState.Skills = skillState
if err := f.Save(gameState); err != nil {
fmt.Printf("failed to save game state!, err: %v\n", err)
}
}
result = append(result, gameState)
}
return result, nil
}
// CreateHeroSkillsState will assemble the hero skills from the class stats record.
func (f *HeroStateFactory) CreateHeroSkillsState(classStats *d2records.CharStatsRecord) (map[int]*HeroSkill, error) {
baseSkills := map[int]*HeroSkill{}
for idx := range classStats.BaseSkill {
skillName := &classStats.BaseSkill[idx]
if len(*skillName) == 0 {
continue
}
skill, err := f.CreateHeroSkill(1, *skillName)
if err != nil {
continue
}
baseSkills[skill.ID] = skill
}
skillRecord, err := f.CreateHeroSkill(1, "Attack")
if err != nil {
return nil, err
}
baseSkills[skillRecord.ID] = skillRecord
return baseSkills, nil
}
// CreateHeroSkill creates an instance of a skill
func (f *HeroStateFactory) CreateHeroSkill(points int, name string) (*HeroSkill, error) {
skillRecord := f.asset.Records.GetSkillByName(name)
if skillRecord == nil {
return nil, fmt.Errorf("Skill not found: %s", name)
}
skillDescRecord, found := f.asset.Records.Skill.Descriptions[skillRecord.Skilldesc]
if !found {
return nil, fmt.Errorf("Skill Description not found: %s", name)
}
result := &HeroSkill{
SkillPoints: points,
SkillRecord: skillRecord,
SkillDescriptionRecord: skillDescRecord,
}
return result, nil
}
// HasGameStates returns true if the player has any previously saved game
func (f *HeroStateFactory) HasGameStates() bool {
basePath, _ := f.getGameBaseSavePath()
files, _ := ioutil.ReadDir(basePath)
return len(files) > 0
}
// CreateTestGameState is used for the map engine previewer
func (f *HeroStateFactory) CreateTestGameState() *HeroState {
result := &HeroState{}
return result
}
// LoadHeroState loads the player state from the file
func (f *HeroStateFactory) LoadHeroState(filePath string) *HeroState {
strData, err := ioutil.ReadFile(filePath)
if err != nil {
return nil
}
result := &HeroState{
FilePath: filePath,
}
err = json.Unmarshal(strData, result)
if err != nil {
return nil
}
// Here, we turn the shallow skill data back into records from the asset manager.
// This is because this factory has a reference to the asset manager with loaded records.
// We cant do this while unmarshalling because there is no reference to the asset manager.
for idx := range result.Skills {
hs := result.Skills[idx]
hs.SkillRecord = f.asset.Records.Skill.Details[hs.shallow.SkillID]
hs.SkillDescriptionRecord = f.asset.Records.Skill.Descriptions[hs.SkillRecord.Skilldesc]
hs.SkillPoints = hs.shallow.SkillPoints
}
return result
}
func (f *HeroStateFactory) getGameBaseSavePath() (string, error) {
configDir, err := os.UserConfigDir()
if err != nil {
return "", err
}
return path.Join(configDir, "OpenDiablo2/Saves"), nil
}
func (f *HeroStateFactory) getFirstFreeFileName() string {
i := 0
basePath, _ := f.getGameBaseSavePath()
for {
filePath := path.Join(basePath, strconv.Itoa(i)+".od2")
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return filePath
}
i++
}
}
// Save saves the player state to a file
func (f *HeroStateFactory) Save(state *HeroState) error {
if state.FilePath == "" {
state.FilePath = f.getFirstFreeFileName()
}
if err := os.MkdirAll(path.Dir(state.FilePath), 0755); err != nil {
return err
}
fileJSON, _ := json.MarshalIndent(state, "", " ")
if err := ioutil.WriteFile(state.FilePath, fileJSON, 0644); err != nil {
return err
}
return nil
}

View File

@ -1,8 +1,8 @@
package d2hero
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
)
// HeroStatsState is a serializable state of hero stats.
@ -35,11 +35,11 @@ type HeroStatsState struct {
}
// CreateHeroStatsState generates a running state from a hero stats.
func CreateHeroStatsState(heroClass d2enum.Hero, classStats *d2datadict.CharStatsRecord) *HeroStatsState {
func (f *HeroStateFactory) CreateHeroStatsState(heroClass d2enum.Hero, classStats *d2records.CharStatsRecord) *HeroStatsState {
result := HeroStatsState{
Level: 1,
Experience: 0,
NextLevelExp: d2datadict.GetExperienceBreakpoint(heroClass, 1),
NextLevelExp: f.asset.Records.GetExperienceBreakpoint(heroClass, 1),
Strength: classStats.InitStr,
Dexterity: classStats.InitDex,
Vitality: classStats.InitVit,

View File

@ -5,39 +5,4 @@ import (
)
// HeroObjects map contains the hero type to CharacterEquipments
var HeroObjects map[d2enum.Hero]CharacterEquipment
// LoadHeroObjects loads the equipment objects of the hero
func LoadHeroObjects() {
//Mode: d2enum.AnimationModePlayerNeutral.String(),
//Base: "/data/global/chars",
HeroObjects = map[d2enum.Hero]CharacterEquipment{
d2enum.HeroBarbarian: {
RightHand: GetWeaponItemByCode("hax"),
Shield: GetArmorItemByCode("buc"),
},
d2enum.HeroNecromancer: {
RightHand: GetWeaponItemByCode("wnd"),
},
d2enum.HeroPaladin: {
RightHand: GetWeaponItemByCode("ssd"),
Shield: GetArmorItemByCode("buc"),
},
d2enum.HeroAssassin: {
RightHand: GetWeaponItemByCode("ktr"),
Shield: GetArmorItemByCode("buc"),
},
d2enum.HeroSorceress: {
RightHand: GetWeaponItemByCode("sst"),
LeftHand: GetWeaponItemByCode("sst"),
},
d2enum.HeroAmazon: {
RightHand: GetWeaponItemByCode("jav"),
Shield: GetArmorItemByCode("buc"),
},
d2enum.HeroDruid: {
RightHand: GetWeaponItemByCode("clb"),
Shield: GetArmorItemByCode("buc"),
},
}
}
type HeroObjects map[d2enum.Hero]CharacterEquipment

View File

@ -1,9 +1,6 @@
package d2inventory
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
)
@ -18,22 +15,6 @@ type InventoryItemArmor struct {
ArmorClass string `json:"armorClass"`
}
// GetArmorItemByCode returns the armor item for the given code
func GetArmorItemByCode(code string) *InventoryItemArmor {
result := d2datadict.Armors[code]
if result == nil {
log.Fatalf("Could not find armor entry for code '%s'", code)
}
return &InventoryItemArmor{
InventorySizeX: result.InventoryWidth,
InventorySizeY: result.InventoryHeight,
ItemName: result.Name,
ItemCode: result.Code,
ArmorClass: "lit", // TODO: Where does this come from?
}
}
// GetArmorClass returns the class of the armor
func (v *InventoryItemArmor) GetArmorClass() string {
if v == nil || v.ItemCode == "" {

View File

@ -0,0 +1,107 @@
package d2inventory
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
)
// NewInventoryItemFactory creates a new InventoryItemFactory and initializes it
func NewInventoryItemFactory(asset *d2asset.AssetManager) (*InventoryItemFactory, error) {
factory := &InventoryItemFactory{asset: asset}
factory.loadHeroObjects()
return factory, nil
}
// InventoryItemFactory is responsible for creating inventory items
type InventoryItemFactory struct {
asset *d2asset.AssetManager
DefaultHeroItems HeroObjects
}
// LoadHeroObjects loads the equipment objects of the hero
func (f *InventoryItemFactory) loadHeroObjects() {
//Mode: d2enum.AnimationModePlayerNeutral.String(),
//Base: "/data/global/chars",
f.DefaultHeroItems = map[d2enum.Hero]CharacterEquipment{
d2enum.HeroBarbarian: {
RightHand: f.GetWeaponItemByCode("hax"),
Shield: f.GetArmorItemByCode("buc"),
},
d2enum.HeroNecromancer: {
RightHand: f.GetWeaponItemByCode("wnd"),
},
d2enum.HeroPaladin: {
RightHand: f.GetWeaponItemByCode("ssd"),
Shield: f.GetArmorItemByCode("buc"),
},
d2enum.HeroAssassin: {
RightHand: f.GetWeaponItemByCode("ktr"),
Shield: f.GetArmorItemByCode("buc"),
},
d2enum.HeroSorceress: {
RightHand: f.GetWeaponItemByCode("sst"),
LeftHand: f.GetWeaponItemByCode("sst"),
},
d2enum.HeroAmazon: {
RightHand: f.GetWeaponItemByCode("jav"),
Shield: f.GetArmorItemByCode("buc"),
},
d2enum.HeroDruid: {
RightHand: f.GetWeaponItemByCode("clb"),
Shield: f.GetArmorItemByCode("buc"),
},
}
}
// GetArmorItemByCode returns the armor item for the given code
func (f *InventoryItemFactory) GetArmorItemByCode(code string) *InventoryItemArmor {
result := f.asset.Records.Item.Armors[code]
if result == nil {
log.Fatalf("Could not find armor entry for code '%s'", code)
}
return &InventoryItemArmor{
InventorySizeX: result.InventoryWidth,
InventorySizeY: result.InventoryHeight,
ItemName: result.Name,
ItemCode: result.Code,
ArmorClass: "lit", // TODO: Where does this come from?
}
}
// GetMiscItemByCode returns the miscellaneous item for the given code
func (f *InventoryItemFactory) GetMiscItemByCode(code string) *InventoryItemMisc {
result := f.asset.Records.Item.Misc[code]
if result == nil {
log.Fatalf("Could not find misc item entry for code '%s'", code)
}
return &InventoryItemMisc{
InventorySizeX: result.InventoryWidth,
InventorySizeY: result.InventoryHeight,
ItemName: result.Name,
ItemCode: result.Code,
}
}
// GetWeaponItemByCode returns the weapon item for the given code
func (f *InventoryItemFactory) GetWeaponItemByCode(code string) *InventoryItemWeapon {
// TODO: Non-normal codes will fail here...
result := f.asset.Records.Item.Weapons[code]
if result == nil {
log.Fatalf("Could not find weapon entry for code '%s'", code)
}
return &InventoryItemWeapon{
InventorySizeX: result.InventoryWidth,
InventorySizeY: result.InventoryHeight,
ItemName: result.Name,
ItemCode: result.Code,
WeaponClass: result.WeaponClass,
WeaponClassOffHand: result.WeaponClass2Hand,
}
}

View File

@ -1,9 +1,6 @@
package d2inventory
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
)
@ -17,21 +14,6 @@ type InventoryItemMisc struct {
ItemCode string `json:"itemCode"`
}
// GetMiscItemByCode returns the miscellaneous item for the given code
func GetMiscItemByCode(code string) *InventoryItemMisc {
result := d2datadict.MiscItems[code]
if result == nil {
log.Fatalf("Could not find misc item entry for code '%s'", code)
}
return &InventoryItemMisc{
InventorySizeX: result.InventoryWidth,
InventorySizeY: result.InventoryHeight,
ItemName: result.Name,
ItemCode: result.Code,
}
}
// InventoryItemName returns the name of the miscellaneous item
func (v *InventoryItemMisc) InventoryItemName() string {
if v == nil {

View File

@ -1,9 +1,6 @@
package d2inventory
import (
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
)
@ -19,24 +16,6 @@ type InventoryItemWeapon struct {
WeaponClassOffHand string `json:"weaponClassOffHand"`
}
// GetWeaponItemByCode returns the weapon item for the given code
func GetWeaponItemByCode(code string) *InventoryItemWeapon {
// TODO: Non-normal codes will fail here...
result := d2datadict.Weapons[code]
if result == nil {
log.Fatalf("Could not find weapon entry for code '%s'", code)
}
return &InventoryItemWeapon{
InventorySizeX: result.InventoryWidth,
InventorySizeY: result.InventoryHeight,
ItemName: result.Name,
ItemCode: result.Code,
WeaponClass: result.WeaponClass,
WeaponClassOffHand: result.WeaponClass2Hand,
}
}
// GetWeaponClass returns the class of the weapon
func (v *InventoryItemWeapon) GetWeaponClass() string {
if v == nil || v.ItemCode == "" {

View File

@ -1,94 +0,0 @@
package diablo2item
import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
func NewItem(codes ...string) *Item {
var item *Item
var common, set, unique string
var prefixes, suffixes []string
for _, code := range codes {
if found := d2datadict.CommonItems[code]; found != nil {
common = code
continue
}
if found := d2datadict.SetItems[code]; found != nil {
set = code
continue
}
if found := d2datadict.UniqueItems[code]; found != nil {
unique = code
continue
}
if found := d2datadict.MagicPrefix[code]; found != nil {
if prefixes == nil {
prefixes = make([]string, 0)
}
prefixes = append(prefixes, code)
continue
}
if found := d2datadict.MagicSuffix[code]; found != nil {
if suffixes == nil {
suffixes = make([]string, 0)
}
suffixes = append(suffixes, code)
continue
}
}
if common != "" { // we will at least have a regular item
item = &Item{CommonCode: common}
if set != "" { // it's a set item
item.SetItemCode = set
return item.init()
}
if unique != "" { // it's a unique item
item.UniqueCode = unique
return item.init()
}
if prefixes != nil {
if len(prefixes) > 0 { // it's a magic or rare item
item.PrefixCodes = prefixes
}
}
if suffixes != nil {
if len(suffixes) > 0 { // it's a magic or rare item
item.SuffixCodes = suffixes
}
}
return item.init()
}
return nil
}
// NewProperty creates a property
func NewProperty(code string, values ...int) *Property {
record := d2datadict.Properties[code]
if record == nil {
return nil
}
result := &Property{
record: record,
inputParams: values,
}
return result.init()
}

View File

@ -6,12 +6,13 @@ import (
"sort"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2tbl"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2item"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats/diablo2stats"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
)
@ -48,9 +49,10 @@ const (
var _ d2item.Item = &Item{}
type Item struct {
name string
Seed int64
rand *rand.Rand // non-global rand instance for re-generating the item
factory *ItemFactory
name string
Seed int64
rand *rand.Rand // non-global rand instance for re-generating the item
slotType d2enum.EquippedSlot
@ -177,18 +179,18 @@ func (i *Item) ItemLevel() int {
}
// TypeRecord returns the ItemTypeRecord of the item
func (i *Item) TypeRecord() *d2datadict.ItemTypeRecord {
return d2datadict.ItemTypes[i.TypeCode]
func (i *Item) TypeRecord() *d2records.ItemTypeRecord {
return i.factory.asset.Records.Item.Types[i.TypeCode]
}
// CommonRecord returns the ItemCommonRecord of the item
func (i *Item) CommonRecord() *d2datadict.ItemCommonRecord {
return d2datadict.CommonItems[i.CommonCode]
func (i *Item) CommonRecord() *d2records.ItemCommonRecord {
return i.factory.asset.Records.Item.All[i.CommonCode]
}
// UniqueRecord returns the UniqueItemRecord of the item
func (i *Item) UniqueRecord() *d2datadict.UniqueItemRecord {
return d2datadict.UniqueItems[i.UniqueCode]
func (i *Item) UniqueRecord() *d2records.UniqueItemRecord {
return i.factory.asset.Records.Item.Unique[i.UniqueCode]
}
// SetRecord returns the SetRecord of the item
@ -202,24 +204,24 @@ func (i *Item) SetItemRecord() *d2datadict.SetItemRecord {
}
// PrefixRecords returns the ItemAffixCommonRecords of the prefixes of the item
func (i *Item) PrefixRecords() []*d2datadict.ItemAffixCommonRecord {
return affixRecords(i.PrefixCodes, d2datadict.MagicPrefix)
func (i *Item) PrefixRecords() []*d2records.ItemAffixCommonRecord {
return affixRecords(i.PrefixCodes, i.factory.asset.Records.Item.Magic.Prefix)
}
// SuffixRecords returns the ItemAffixCommonRecords of the prefixes of the item
func (i *Item) SuffixRecords() []*d2datadict.ItemAffixCommonRecord {
return affixRecords(i.SuffixCodes, d2datadict.MagicSuffix)
func (i *Item) SuffixRecords() []*d2records.ItemAffixCommonRecord {
return affixRecords(i.SuffixCodes, i.factory.asset.Records.Item.Magic.Suffix)
}
func affixRecords(
fromCodes []string,
affixes map[string]*d2datadict.ItemAffixCommonRecord,
) []*d2datadict.ItemAffixCommonRecord {
affixes map[string]*d2records.ItemAffixCommonRecord,
) []*d2records.ItemAffixCommonRecord {
if len(fromCodes) < 1 {
return nil
}
result := make([]*d2datadict.ItemAffixCommonRecord, len(fromCodes))
result := make([]*d2records.ItemAffixCommonRecord, len(fromCodes))
for idx, code := range fromCodes {
rec := affixes[code]
@ -344,15 +346,19 @@ func (i *Item) pickMagicAffixes(mod DropModifier) {
totalAffixes = numPrefixes + numSuffixes
}
i.PrefixCodes = i.pickRandomAffixes(numPrefixes, totalAffixes, d2datadict.MagicPrefix)
i.SuffixCodes = i.pickRandomAffixes(numSuffixes, totalAffixes, d2datadict.MagicSuffix)
prefixes := i.factory.asset.Records.Item.Magic.Prefix
suffixes := i.factory.asset.Records.Item.Magic.Prefix
i.PrefixCodes = i.pickRandomAffixes(numPrefixes, totalAffixes, prefixes)
i.SuffixCodes = i.pickRandomAffixes(numSuffixes, totalAffixes, suffixes)
}
func (i *Item) pickRandomAffixes(max, totalMax int, affixMap map[string]*d2datadict.ItemAffixCommonRecord) []string {
func (i *Item) pickRandomAffixes(max, totalMax int,
affixMap map[string]*d2records.ItemAffixCommonRecord) []string {
pickedCodes := make([]string, 0)
for numPicks := 0; numPicks < max; numPicks++ {
matches := findMatchingAffixes(i.CommonRecord(), affixMap)
matches := i.factory.FindMatchingAffixes(i.CommonRecord(), affixMap)
if rollPrefix := i.rand.Intn(2); rollPrefix > 0 {
affixCount := len(i.PrefixRecords()) + len(i.SuffixRecords())
@ -506,7 +512,7 @@ func (i *Item) updateItemAttributes() {
}
func (i *Item) generateAffixProperties(pool PropertyPool) []*Property {
var affixRecords []*d2datadict.ItemAffixCommonRecord
var affixRecords []*d2records.ItemAffixCommonRecord
switch pool {
case PropertyPoolPrefix:
@ -530,7 +536,7 @@ func (i *Item) generateAffixProperties(pool PropertyPool) []*Property {
for modIdx := range affix.Modifiers {
mod := affix.Modifiers[modIdx]
prop := NewProperty(mod.Code, mod.Parameter, mod.Min, mod.Max)
prop := i.factory.NewProperty(mod.Code, mod.Parameter, mod.Min, mod.Max)
if prop == nil {
continue
}
@ -558,14 +564,14 @@ func (i *Item) generateUniqueProperties() []*Property {
paramInt := getNumericComponent(propInfo.Parameter)
if paramStr != "" {
for skillID := range d2datadict.SkillDetails {
if d2datadict.SkillDetails[skillID].Skill == paramStr {
for skillID := range i.factory.asset.Records.Skill.Details {
if i.factory.asset.Records.Skill.Details[skillID].Skill == paramStr {
paramInt = skillID
}
}
}
prop := NewProperty(propInfo.Code, paramInt, propInfo.Min, propInfo.Max)
prop := i.factory.NewProperty(propInfo.Code, paramInt, propInfo.Min, propInfo.Max)
if prop == nil {
continue
}
@ -592,14 +598,14 @@ func (i *Item) generateSetItemProperties() []*Property {
paramInt := getNumericComponent(setProp.Parameter)
if paramStr != "" {
for skillID := range d2datadict.SkillDetails {
if d2datadict.SkillDetails[skillID].Skill == paramStr {
for skillID := range i.factory.asset.Records.Skill.Details {
if i.factory.asset.Records.Skill.Details[skillID].Skill == paramStr {
paramInt = skillID
}
}
}
prop := NewProperty(setProp.Code, paramInt, setProp.Min, setProp.Max)
prop := i.factory.NewProperty(setProp.Code, paramInt, setProp.Min, setProp.Max)
if prop == nil {
continue
}
@ -687,7 +693,7 @@ func (i *Item) GetStatStrings() []string {
}
if len(stats) > 0 {
stats = diablo2stats.NewStatList(stats...).ReduceStats().Stats()
stats = i.factory.stat.NewStatList(stats...).ReduceStats().Stats()
}
sort.Slice(stats, func(i, j int) bool { return stats[i].Priority() > stats[j].Priority() })
@ -702,7 +708,7 @@ func (i *Item) GetStatStrings() []string {
return result
}
func findMatchingUniqueRecords(icr *d2datadict.ItemCommonRecord) []*d2datadict.UniqueItemRecord {
func findMatchingUniqueRecords(icr *d2records.ItemCommonRecord) []*d2datadict.UniqueItemRecord {
result := make([]*d2datadict.UniqueItemRecord, 0)
c1, c2, c3, c4 := icr.Code, icr.NormalCode, icr.UberCode, icr.UltraCode
@ -720,7 +726,7 @@ func findMatchingUniqueRecords(icr *d2datadict.ItemCommonRecord) []*d2datadict.U
}
// find possible SetItemRecords that the given ItemCommonRecord can have
func findMatchingSetItemRecords(icr *d2datadict.ItemCommonRecord) []*d2datadict.SetItemRecord {
func findMatchingSetItemRecords(icr *d2records.ItemCommonRecord) []*d2datadict.SetItemRecord {
result := make([]*d2datadict.SetItemRecord, 0)
c1, c2, c3, c4 := icr.Code, icr.NormalCode, icr.UberCode, icr.UltraCode
@ -735,55 +741,6 @@ func findMatchingSetItemRecords(icr *d2datadict.ItemCommonRecord) []*d2datadict.
return result
}
// for a given ItemCommonRecord, find all possible affixes that can spawn
func findMatchingAffixes(
icr *d2datadict.ItemCommonRecord,
fromAffixes map[string]*d2datadict.ItemAffixCommonRecord,
) []*d2datadict.ItemAffixCommonRecord {
result := make([]*d2datadict.ItemAffixCommonRecord, 0)
equivItemTypes := d2datadict.FindEquivalentTypesByItemCommonRecord(icr)
for prefixIdx := range fromAffixes {
include, exclude := false, false
affix := fromAffixes[prefixIdx]
for itemTypeIdx := range equivItemTypes {
itemType := equivItemTypes[itemTypeIdx]
for _, excludedType := range affix.ItemExclude {
if itemType == excludedType {
exclude = true
break
}
}
if exclude {
break
}
for _, includedType := range affix.ItemInclude {
if itemType == includedType {
include = true
break
}
}
if !include {
continue
}
if icr.Level < affix.Level {
continue
}
result = append(result, affix)
}
}
return result
}
// these functions are to satisfy the inventory grid item interface
// GetInventoryItemName returns the item name
@ -795,8 +752,8 @@ func (i *Item) GetInventoryItemName() string {
func (i *Item) GetInventoryItemType() d2enum.InventoryItemType {
typeCode := i.TypeRecord().Code
armorEquiv := d2datadict.ItemEquivalenciesByTypeCode["armo"]
weaponEquiv := d2datadict.ItemEquivalenciesByTypeCode["weap"]
armorEquiv := i.factory.asset.Records.Item.Equivalency["armo"]
weaponEquiv := i.factory.asset.Records.Item.Equivalency["weap"]
for idx := range armorEquiv {
if armorEquiv[idx].Code == typeCode {

View File

@ -0,0 +1,426 @@
package diablo2item
import (
"errors"
"math/rand"
"regexp"
"strconv"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats/diablo2stats"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
)
const (
defaultSeed = 0
)
const (
DropModifierBaseProbability = 1024 // base DropModifier probability total
)
type DropModifier int
const (
DropModifierNone DropModifier = iota
DropModifierUnique
DropModifierSet
DropModifierRare
DropModifierMagic
)
const (
// DynamicItemLevelRange for treasure codes like `armo33`, this code is used to
// select all equivalent items (matching `armo` in this case) with item levels 33,34,35
DynamicItemLevelRange = 3
)
const (
goldItemCodeWithMult = "gld,mul="
goldItemCode = "gld"
)
func NewItemFactory(asset *d2asset.AssetManager) (*ItemFactory, error) {
itemFactory := &ItemFactory{
asset: asset,
Seed: 0,
}
itemFactory.SetSeed(defaultSeed)
statFactory, err := diablo2stats.NewStatFactory(asset)
if err != nil {
return nil, err
}
itemFactory.stat = statFactory
return itemFactory, nil
}
// ItemFactory is a diablo 2 implementation of an item generator
type ItemFactory struct {
asset *d2asset.AssetManager
stat *diablo2stats.StatFactory
rand *rand.Rand
source rand.Source
Seed int64
}
// SetSeed sets the item generator seed
func (f *ItemFactory) SetSeed(seed int64) {
if f.rand == nil || f.source == nil {
f.source = rand.NewSource(seed)
f.rand = rand.New(f.source)
}
f.Seed = seed
}
func (f *ItemFactory) NewItem(codes ...string) (*Item, error) {
var item *Item
var common, set, unique string
var prefixes, suffixes []string
for _, code := range codes {
if found := f.asset.Records.Item.All[code]; found != nil {
common = code
continue
}
if found := d2datadict.SetItems[code]; found != nil {
set = code
continue
}
if found := d2datadict.UniqueItems[code]; found != nil {
unique = code
continue
}
if found := f.asset.Records.Item.Magic.Prefix[code]; found != nil {
if prefixes == nil {
prefixes = make([]string, 0)
}
prefixes = append(prefixes, code)
continue
}
if found := f.asset.Records.Item.Magic.Suffix[code]; found != nil {
if suffixes == nil {
suffixes = make([]string, 0)
}
suffixes = append(suffixes, code)
continue
}
}
if common != "" { // we will at least have a regular item
item = &Item{CommonCode: common}
if set != "" { // it's a set item
item.SetItemCode = set
return item.init(), nil
}
if unique != "" { // it's a unique item
item.UniqueCode = unique
return item.init(), nil
}
if prefixes != nil {
if len(prefixes) > 0 { // it's a magic or rare item
item.PrefixCodes = prefixes
}
}
if suffixes != nil {
if len(suffixes) > 0 { // it's a magic or rare item
item.SuffixCodes = suffixes
}
}
item.factory = f
return item.init(), nil
}
return nil, errors.New("cannot create item")
}
// NewProperty creates a property
func (f *ItemFactory) NewProperty(code string, values ...int) *Property {
record := f.asset.Records.Properties[code]
if record == nil {
return nil
}
result := &Property{
factory: f,
record: record,
inputParams: values,
}
return result.init()
}
func (f *ItemFactory) rollDropModifier(tcr *d2datadict.TreasureClassRecord) DropModifier {
modMap := map[int]DropModifier{
0: DropModifierNone,
1: DropModifierUnique,
2: DropModifierSet,
3: DropModifierRare,
4: DropModifierMagic,
}
dropModifiers := []int{
DropModifierBaseProbability,
tcr.FreqUnique,
tcr.FreqSet,
tcr.FreqRare,
tcr.FreqMagic,
}
for idx := range dropModifiers {
if idx == 0 {
continue
}
dropModifiers[idx] += dropModifiers[idx-1]
}
roll := f.rand.Intn(dropModifiers[len(dropModifiers)-1])
for idx := range dropModifiers {
if roll < dropModifiers[idx] {
return modMap[idx]
}
}
return DropModifierNone
}
func (f *ItemFactory) rollTreasurePick(tcr *d2datadict.TreasureClassRecord) *d2datadict.Treasure {
// treasure probabilities
tprob := make([]int, len(tcr.Treasures)+1)
total := tcr.FreqNoDrop
tprob[0] = total
for idx := range tcr.Treasures {
total += tcr.Treasures[idx].Probability
tprob[idx+1] = total
}
roll := f.rand.Intn(total)
for idx := range tprob {
if roll < tprob[idx] {
if idx == 0 {
break
}
return tcr.Treasures[idx-1]
}
}
return nil
}
// ItemsFromTreasureClass rolls for and creates items using a treasure class record
func (f *ItemFactory) ItemsFromTreasureClass(tcr *d2datadict.TreasureClassRecord) []*Item {
result := make([]*Item, 0)
treasurePicks := make([]*d2datadict.Treasure, 0)
// if tcr.NumPicks is negative, each item probability is instead a count for how many
// of that treasure to drop
if tcr.NumPicks < 0 {
picksLeft := tcr.NumPicks
// for each of the treasures, we pick it N times, where N is the count for the item
// we do this until we run out of picks
for idx := range tcr.Treasures {
howMany := tcr.Treasures[idx].Probability
for count := 0; count < howMany && picksLeft < 0; count++ {
treasurePicks = append(treasurePicks, tcr.Treasures[idx])
picksLeft++
}
}
} else {
// for N picks, we roll for a treasure and append to our treasures if it isn't a NoDrop
for picksLeft := tcr.NumPicks; picksLeft > 0; picksLeft-- {
rolledTreasure := f.rollTreasurePick(tcr)
if rolledTreasure == nil {
continue
}
treasurePicks = append(treasurePicks, rolledTreasure)
}
}
// for each of our picked/rolled treasures, we will attempt to generate an item.
// The treasure may actually be a reference to another treasure class, in which
// case we will roll that treasure class, eventually getting a slice of items
for idx := range treasurePicks {
picked := treasurePicks[idx]
if record, found := d2datadict.TreasureClass[picked.Code]; found {
// the code is for a treasure class, we roll again using that TC
itemSlice := f.ItemsFromTreasureClass(record)
for itemIdx := range itemSlice {
itemSlice[itemIdx].applyDropModifier(f.rollDropModifier(tcr))
itemSlice[itemIdx].init()
result = append(result, itemSlice[itemIdx])
}
} else {
// the code is not for a treasure class, but for an item
item := f.ItemFromTreasure(picked)
if item != nil {
item.applyDropModifier(f.rollDropModifier(tcr))
item.init()
result = append(result, item)
}
}
}
return result
}
// ItemFromTreasure rolls for a f.rand.m item using the Treasure struct (from d2datadict)
func (f *ItemFactory) ItemFromTreasure(treasure *d2datadict.Treasure) *Item {
result := &Item{
rand: rand.New(rand.NewSource(f.Seed)),
}
// in this case, the treasure code is a code used by an ItemCommonRecord
commonRecord := f.asset.Records.Item.All[treasure.Code]
if commonRecord != nil {
result.CommonCode = commonRecord.Code
return result
}
// next, we check if the treasure code is a generic type like `armo`
equivList := f.asset.Records.Item.Equivalency[treasure.Code]
if equivList != nil {
result.CommonCode = equivList[f.rand.Intn(len(equivList))].Code
return result
}
// in this case, the treasure code is something like `armo23` and needs to
// be resolved to ItemCommonRecords for armors with levels 23,24,25
matches := f.resolveDynamicTreasureCode(treasure.Code)
if matches != nil {
numItems := len(matches)
if numItems < 1 {
return nil
}
result.CommonCode = matches[f.rand.Intn(numItems)].Code
return result
}
return nil
}
// FindMatchingAffixes for a given ItemCommonRecord, find all possible affixes that can spawn
func (f *ItemFactory) FindMatchingAffixes(
icr *d2records.ItemCommonRecord,
fromAffixes map[string]*d2records.ItemAffixCommonRecord,
) []*d2records.ItemAffixCommonRecord {
result := make([]*d2records.ItemAffixCommonRecord, 0)
equivItemTypes := f.asset.Records.FindEquivalentTypesByItemCommonRecord(icr)
for prefixIdx := range fromAffixes {
include, exclude := false, false
affix := fromAffixes[prefixIdx]
for itemTypeIdx := range equivItemTypes {
itemType := equivItemTypes[itemTypeIdx]
for _, excludedType := range affix.ItemExclude {
if itemType == excludedType {
exclude = true
break
}
}
if exclude {
break
}
for _, includedType := range affix.ItemInclude {
if itemType == includedType {
include = true
break
}
}
if !include {
continue
}
if icr.Level < affix.Level {
continue
}
result = append(result, affix)
}
}
return result
}
func (f *ItemFactory) resolveDynamicTreasureCode(code string) []*d2records.ItemCommonRecord {
numericComponent := getNumericComponent(code)
stringComponent := getStringComponent(code)
if stringComponent == goldItemCodeWithMult {
// todo need to do something with the numeric component (the gold multiplier)
stringComponent = goldItemCode
}
result := make([]*d2records.ItemCommonRecord, 0)
equivList := f.asset.Records.Item.Equivalency[stringComponent]
for idx := range equivList {
record := equivList[idx]
minLevel := numericComponent
maxLevel := minLevel + DynamicItemLevelRange
if record.Level >= minLevel && record.Level < maxLevel {
result = append(result, record)
}
}
return result
}
func getStringComponent(code string) string {
re := regexp.MustCompile(`\d+`)
return string(re.ReplaceAll([]byte(code), []byte("")))
}
func getNumericComponent(code string) int {
result := 0
re := regexp.MustCompile(`\D`)
numStr := string(re.ReplaceAll([]byte(code), []byte("")))
if number, err := strconv.ParseInt(numStr, 10, 32); err == nil {
result = int(number)
}
return result
}

View File

@ -1,253 +0,0 @@
package diablo2item
import (
"math/rand"
"regexp"
"strconv"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
)
const (
DropModifierBaseProbability = 1024 // base DropModifier probability total
)
type DropModifier int
const (
DropModifierNone DropModifier = iota
DropModifierUnique
DropModifierSet
DropModifierRare
DropModifierMagic
)
const (
// DynamicItemLevelRange for treasure codes like `armo33`, this code is used to
// select all equivalent items (matching `armo` in this case) with item levels 33,34,35
DynamicItemLevelRange = 3
)
const (
goldItemCodeWithMult = "gld,mul="
goldItemCode = "gld"
)
// ItemGenerator is a diablo 2 implementation of an item generator
type ItemGenerator struct {
rand *rand.Rand
source rand.Source
Seed int64
}
// SetSeed sets the item generator seed
func (ig *ItemGenerator) SetSeed(seed int64) {
if ig.rand == nil || ig.source == nil {
ig.source = rand.NewSource(seed)
ig.rand = rand.New(ig.source)
}
ig.Seed = seed
}
func (ig *ItemGenerator) rollDropModifier(tcr *d2datadict.TreasureClassRecord) DropModifier {
modMap := map[int]DropModifier{
0: DropModifierNone,
1: DropModifierUnique,
2: DropModifierSet,
3: DropModifierRare,
4: DropModifierMagic,
}
dropModifiers := []int{
DropModifierBaseProbability,
tcr.FreqUnique,
tcr.FreqSet,
tcr.FreqRare,
tcr.FreqMagic,
}
for idx := range dropModifiers {
if idx == 0 {
continue
}
dropModifiers[idx] += dropModifiers[idx-1]
}
roll := ig.rand.Intn(dropModifiers[len(dropModifiers)-1])
for idx := range dropModifiers {
if roll < dropModifiers[idx] {
return modMap[idx]
}
}
return DropModifierNone
}
func (ig *ItemGenerator) rollTreasurePick(tcr *d2datadict.TreasureClassRecord) *d2datadict.Treasure {
// treasure probabilities
tprob := make([]int, len(tcr.Treasures)+1)
total := tcr.FreqNoDrop
tprob[0] = total
for idx := range tcr.Treasures {
total += tcr.Treasures[idx].Probability
tprob[idx+1] = total
}
roll := ig.rand.Intn(total)
for idx := range tprob {
if roll < tprob[idx] {
if idx == 0 {
break
}
return tcr.Treasures[idx-1]
}
}
return nil
}
// ItemsFromTreasureClass rolls for and creates items using a treasure class record
func (ig *ItemGenerator) ItemsFromTreasureClass(tcr *d2datadict.TreasureClassRecord) []*Item {
result := make([]*Item, 0)
treasurePicks := make([]*d2datadict.Treasure, 0)
// if tcr.NumPicks is negative, each item probability is instead a count for how many
// of that treasure to drop
if tcr.NumPicks < 0 {
picksLeft := tcr.NumPicks
// for each of the treasures, we pick it N times, where N is the count for the item
// we do this until we run out of picks
for idx := range tcr.Treasures {
howMany := tcr.Treasures[idx].Probability
for count := 0; count < howMany && picksLeft < 0; count++ {
treasurePicks = append(treasurePicks, tcr.Treasures[idx])
picksLeft++
}
}
} else {
// for N picks, we roll for a treasure and append to our treasures if it isn't a NoDrop
for picksLeft := tcr.NumPicks; picksLeft > 0; picksLeft-- {
rolledTreasure := ig.rollTreasurePick(tcr)
if rolledTreasure == nil {
continue
}
treasurePicks = append(treasurePicks, rolledTreasure)
}
}
// for each of our picked/rolled treasures, we will attempt to generate an item.
// The treasure may actually be a reference to another treasure class, in which
// case we will roll that treasure class, eventually getting a slice of items
for idx := range treasurePicks {
picked := treasurePicks[idx]
if record, found := d2datadict.TreasureClass[picked.Code]; found {
// the code is for a treasure class, we roll again using that TC
itemSlice := ig.ItemsFromTreasureClass(record)
for itemIdx := range itemSlice {
itemSlice[itemIdx].applyDropModifier(ig.rollDropModifier(tcr))
itemSlice[itemIdx].init()
result = append(result, itemSlice[itemIdx])
}
} else {
// the code is not for a treasure class, but for an item
item := ig.ItemFromTreasure(picked)
if item != nil {
item.applyDropModifier(ig.rollDropModifier(tcr))
item.init()
result = append(result, item)
}
}
}
return result
}
// ItemFromTreasure rolls for a ig.rand.m item using the Treasure struct (from d2datadict)
func (ig *ItemGenerator) ItemFromTreasure(treasure *d2datadict.Treasure) *Item {
result := &Item{
rand: rand.New(rand.NewSource(ig.Seed)),
}
// in this case, the treasure code is a code used by an ItemCommonRecord
commonRecord := d2datadict.CommonItems[treasure.Code]
if commonRecord != nil {
result.CommonCode = commonRecord.Code
return result
}
// next, we check if the treasure code is a generic type like `armo`
equivList := d2datadict.ItemEquivalenciesByTypeCode[treasure.Code]
if equivList != nil {
result.CommonCode = equivList[ig.rand.Intn(len(equivList))].Code
return result
}
// in this case, the treasure code is something like `armo23` and needs to
// be resolved to ItemCommonRecords for armors with levels 23,24,25
matches := resolveDynamicTreasureCode(treasure.Code)
if matches != nil {
numItems := len(matches)
if numItems < 1 {
return nil
}
result.CommonCode = matches[ig.rand.Intn(numItems)].Code
return result
}
return nil
}
func resolveDynamicTreasureCode(code string) []*d2datadict.ItemCommonRecord {
numericComponent := getNumericComponent(code)
stringComponent := getStringComponent(code)
if stringComponent == goldItemCodeWithMult {
// todo need to do something with the numeric component (the gold multiplier)
stringComponent = goldItemCode
}
result := make([]*d2datadict.ItemCommonRecord, 0)
equivList := d2datadict.ItemEquivalenciesByTypeCode[stringComponent]
for idx := range equivList {
record := equivList[idx]
minLevel := numericComponent
maxLevel := minLevel + DynamicItemLevelRange
if record.Level >= minLevel && record.Level < maxLevel {
result = append(result, record)
}
}
return result
}
func getStringComponent(code string) string {
re := regexp.MustCompile(`\d+`)
return string(re.ReplaceAll([]byte(code), []byte("")))
}
func getNumericComponent(code string) int {
result := 0
re := regexp.MustCompile(`\D`)
numStr := string(re.ReplaceAll([]byte(code), []byte("")))
if number, err := strconv.ParseInt(numStr, 10, 32); err == nil {
result = int(number)
}
return result
}

View File

@ -3,9 +3,8 @@ package diablo2item
import (
"math/rand"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats/diablo2stats"
)
const (
@ -67,15 +66,10 @@ const (
fnRandClassSkill = 36
)
// Property is an item property. Properties act as stat initializers, as well as
// item attribute initializers. A good example of this is for the `Ethereal` property,
// which DOES have a stat, but the stat is actually non-printable as far as the record
// in itemstatcosts.txt is concerned. The behavior of displaying `Ethereal` on an item
// in diablo 2 is hardcoded into whatever handled displaying item descriptions, not
// what was generating stat descriptions (this is a guess, though).
// Another example in min/max damage properties, which do NOT have stats!
// Property is an item property.
type Property struct {
record *d2datadict.PropertyRecord
factory *ItemFactory
record *d2records.PropertyRecord
stats []d2stats.Stat
PropertyType PropertyType
@ -119,7 +113,7 @@ func (p *Property) init() *Property {
// repeat the previous fn with the same parameters, but for a different stat.
func (p *Property) eval(propStatIdx, previousFnID int) (stat d2stats.Stat, funcID int) {
pStatRecord := p.record.Stats[propStatIdx]
iscRecord := d2datadict.ItemStatCosts[pStatRecord.StatCode]
iscRecord := p.factory.asset.Records.Item.Stats[pStatRecord.StatCode]
funcID = pStatRecord.FunctionID
@ -169,7 +163,7 @@ func (p *Property) eval(propStatIdx, previousFnID int) (stat d2stats.Stat, funcI
}
// fnValuesToStat Applies a value to a stat, can use SetX parameter.
func (p *Property) fnValuesToStat(iscRecord *d2datadict.ItemStatCostRecord) d2stats.Stat {
func (p *Property) fnValuesToStat(iscRecord *d2records.ItemStatCostRecord) d2stats.Stat {
// the only special case to handle for this function is for
// property "color", which corresponds to ISC record "item_lightcolor"
// I'm not yet sure how to handle this special case... it is likely
@ -196,7 +190,7 @@ func (p *Property) fnValuesToStat(iscRecord *d2datadict.ItemStatCostRecord) d2st
statValue = float64(rand.Intn(max-min+1) + min)
return diablo2stats.NewStat(iscRecord.Name, statValue, propParam)
return p.factory.stat.NewStat(iscRecord.Name, statValue, propParam)
}
// fnComputeInteger Dmg-min related ???
@ -216,7 +210,7 @@ func (p *Property) fnComputeInteger() int {
}
// fnClassSkillTab skilltab skill group ???
func (p *Property) fnClassSkillTab(iscRecord *d2datadict.ItemStatCostRecord) d2stats.Stat {
func (p *Property) fnClassSkillTab(iscRecord *d2records.ItemStatCostRecord) d2stats.Stat {
// from here: https://d2mods.info/forum/kb/viewarticle?a=45
// Amazon
// 0 - Bow & Crossbow
@ -251,11 +245,11 @@ func (p *Property) fnClassSkillTab(iscRecord *d2datadict.ItemStatCostRecord) d2s
classIdx := float64(param / skillTabsPerClass)
level := float64(rand.Intn(max-min+1) + min)
return diablo2stats.NewStat(iscRecord.Name, level, classIdx, skillTabIdx)
return p.factory.stat.NewStat(iscRecord.Name, level, classIdx, skillTabIdx)
}
// fnProcs event-based skills ???
func (p *Property) fnProcs(iscRecord *d2datadict.ItemStatCostRecord) d2stats.Stat {
func (p *Property) fnProcs(iscRecord *d2records.ItemStatCostRecord) d2stats.Stat {
var skillID, chance, skillLevel float64
switch len(p.inputParams) {
@ -267,11 +261,11 @@ func (p *Property) fnProcs(iscRecord *d2datadict.ItemStatCostRecord) d2stats.Sta
skillLevel = float64(p.inputParams[2])
}
return diablo2stats.NewStat(iscRecord.Name, chance, skillLevel, skillID)
return p.factory.stat.NewStat(iscRecord.Name, chance, skillLevel, skillID)
}
// fnRandomSkill random selection of parameters for parameter-based stat ???
func (p *Property) fnRandomSkill(iscRecord *d2datadict.ItemStatCostRecord) d2stats.Stat {
func (p *Property) fnRandomSkill(iscRecord *d2records.ItemStatCostRecord) d2stats.Stat {
var skillLevel, skillID float64
invalidHeroIndex := -1.0
@ -285,22 +279,22 @@ func (p *Property) fnRandomSkill(iscRecord *d2datadict.ItemStatCostRecord) d2sta
skillID = float64(rand.Intn(max-min+1) + min)
}
return diablo2stats.NewStat(iscRecord.Name, skillLevel, skillID, invalidHeroIndex)
return p.factory.stat.NewStat(iscRecord.Name, skillLevel, skillID, invalidHeroIndex)
}
// fnStatParam use param field only
func (p *Property) fnStatParam(iscRecord *d2datadict.ItemStatCostRecord) d2stats.Stat {
func (p *Property) fnStatParam(iscRecord *d2records.ItemStatCostRecord) d2stats.Stat {
switch len(p.inputParams) {
case noValue:
return nil
default:
val := float64(p.inputParams[0])
return diablo2stats.NewStat(iscRecord.Name, val)
return p.factory.stat.NewStat(iscRecord.Name, val)
}
}
// fnChargeRelated Related to charged item.
func (p *Property) fnChargeRelated(iscRecord *d2datadict.ItemStatCostRecord) d2stats.Stat {
func (p *Property) fnChargeRelated(iscRecord *d2records.ItemStatCostRecord) d2stats.Stat {
var lvl, skill, charges float64
switch len(p.inputParams) {
@ -311,7 +305,7 @@ func (p *Property) fnChargeRelated(iscRecord *d2datadict.ItemStatCostRecord) d2s
skill = float64(p.inputParams[0])
charges = float64(p.inputParams[1])
return diablo2stats.NewStat(iscRecord.Name, lvl, skill, charges, charges)
return p.factory.stat.NewStat(iscRecord.Name, lvl, skill, charges, charges)
}
}
@ -333,8 +327,8 @@ func (p *Property) fnBoolean() bool {
// fnClassSkills Add to group of skills, group determined by stat ID, uses ValX parameter.
func (p *Property) fnClassSkills(
propStatRecord *d2datadict.PropertyStatRecord,
iscRecord *d2datadict.ItemStatCostRecord,
propStatRecord *d2records.PropertyStatRecord,
iscRecord *d2records.ItemStatCostRecord,
) d2stats.Stat {
// in order 0..6
// Amazon
@ -355,16 +349,16 @@ func (p *Property) fnClassSkills(
statValue := rand.Intn(max-min+1) + min
classIdx = propStatRecord.Value
return diablo2stats.NewStat(iscRecord.Name, float64(statValue), float64(classIdx))
return p.factory.stat.NewStat(iscRecord.Name, float64(statValue), float64(classIdx))
}
// fnStateApplyToTarget property applied to character or target monster ???
func (p *Property) fnStateApplyToTarget(iscRecord *d2datadict.ItemStatCostRecord) d2stats.Stat {
func (p *Property) fnStateApplyToTarget(iscRecord *d2records.ItemStatCostRecord) d2stats.Stat {
// todo need to implement states
return nil
}
// fnRandClassSkill property applied to character or target monster ???
func (p *Property) fnRandClassSkill(iscRecord *d2datadict.ItemStatCostRecord) d2stats.Stat {
func (p *Property) fnRandClassSkill(iscRecord *d2records.ItemStatCostRecord) d2stats.Stat {
return nil
}

View File

@ -7,413 +7,419 @@ import (
"testing"
"time"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
)
//nolint:funlen // this just gets mock data ready for the tests
func TestStat_InitMockData(t *testing.T) {
var itemStatCosts = map[string]*d2datadict.ItemStatCostRecord{
"strength": {
Name: "strength",
DescFnID: 1,
DescVal: 1,
DescStrPos: "to Strength",
DescStrNeg: "to Strength",
},
"dexterity": {
Name: "dexterity",
DescFnID: 1,
DescVal: 1,
DescStrPos: "to Dexterity",
DescStrNeg: "to Dexterity",
},
"vitality": {
Name: "vitality",
DescFnID: 1,
DescVal: 1,
DescStrPos: "to Vitality",
DescStrNeg: "to Vitality",
},
"energy": {
Name: "energy",
DescFnID: 1,
DescVal: 1,
DescStrPos: "to Energy",
DescStrNeg: "to Energy",
},
"hpregen": {
Name: "hpregen",
DescFnID: 1,
DescVal: 2,
DescStrPos: "Replenish Life",
DescStrNeg: "Drain Life",
},
"toblock": {
Name: "toblock",
DescFnID: 2,
DescVal: 1,
DescStrPos: "Increased Chance of Blocking",
DescStrNeg: "Increased Chance of Blocking",
},
"item_absorblight_percent": {
Name: "item_absorblight_percent",
DescFnID: 2,
DescVal: 2,
DescStrPos: "Lightning Absorb",
DescStrNeg: "Lightning Absorb",
},
"item_maxdurability_percent": {
Name: "item_maxdurability_percent",
DescFnID: 2,
DescVal: 2,
DescStrPos: "Increase Maximum Durability",
DescStrNeg: "Increase Maximum Durability",
},
"item_restinpeace": {
Name: "item_restinpeace",
DescFnID: 3,
DescVal: 0,
DescStrPos: "Slain Monsters Rest in Peace",
DescStrNeg: "Slain Monsters Rest in Peace",
},
"normal_damage_reduction": {
Name: "normal_damage_reduction",
DescFnID: 3,
DescVal: 2,
DescStrPos: "Damage Reduced by",
DescStrNeg: "Damage Reduced by",
},
"poisonresist": {
Name: "poisonresist",
DescFnID: 4,
DescVal: 2,
DescStrPos: "Poison Resist",
DescStrNeg: "Poison Resist",
},
"item_fastermovevelocity": {
Name: "item_fastermovevelocity",
DescFnID: 4,
DescVal: 1,
DescStrPos: "Faster Run/Walk",
DescStrNeg: "Faster Run/Walk",
},
"item_howl": {
Name: "item_howl",
DescFnID: 5,
DescVal: 2,
DescStrPos: "Hit Causes Monster to Flee",
DescStrNeg: "Hit Causes Monster to Flee",
},
"item_hp_perlevel": {
Name: "item_hp_perlevel",
DescFnID: 6,
DescVal: 1,
DescStrPos: "to Life",
DescStrNeg: "to Life",
DescStr2: "(Based on Character Level)",
},
"item_resist_ltng_perlevel": {
Name: "item_resist_ltng_perlevel",
DescFnID: 7,
DescVal: 2,
DescStrPos: "Lightning Resist",
DescStrNeg: "Lightning Resist",
DescStr2: "(Based on Character Level)",
},
"item_find_magic_perlevel": {
Name: "item_find_magic_perlevel",
DescFnID: 7,
DescVal: 1,
DescStrPos: "Better Chance of Getting Magic Items",
DescStrNeg: "Better Chance of Getting Magic Items",
DescStr2: "(Based on Character Level)",
},
"item_armorpercent_perlevel": {
Name: "item_armorpercent_perlevel",
DescFnID: 8,
DescVal: 1,
DescStrPos: "Enhanced Defense",
DescStrNeg: "Enhanced Defense",
DescStr2: "(Based on Character Level)",
},
"item_regenstamina_perlevel": {
Name: "item_regenstamina_perlevel",
DescFnID: 8,
DescVal: 2,
DescStrPos: "Heal Stamina Plus",
DescStrNeg: "Heal Stamina Plus",
DescStr2: "(Based on Character Level)",
},
"item_thorns_perlevel": {
Name: "item_thorns_perlevel",
DescFnID: 9,
DescVal: 2,
DescStrPos: "Attacker Takes Damage of",
DescStrNeg: "Attacker Takes Damage of",
DescStr2: "(Based on Character Level)",
},
"item_replenish_durability": {
Name: "item_replenish_durability",
DescFnID: 11,
DescVal: 1,
DescStrPos: "Repairs %v durability per second",
DescStrNeg: "Repairs %v durability per second",
DescStr2: "",
},
"item_stupidity": {
Name: "item_stupidity",
DescFnID: 12,
DescVal: 2,
DescStrPos: "Hit Blinds Target",
DescStrNeg: "Hit Blinds Target",
},
"item_addclassskills": {
Name: "item_addclassskills",
DescFnID: 13,
DescVal: 1,
},
"item_addskill_tab": {
Name: "item_addskill_tab",
DescFnID: 14,
DescVal: 1,
},
"item_skillonattack": {
Name: "item_skillonattack",
DescFnID: 15,
DescVal: 1,
DescStrPos: "%d%% Chance to cast level %d %s on attack",
DescStrNeg: "%d%% Chance to cast level %d %s on attack",
},
"item_aura": {
Name: "item_aura",
DescFnID: 16,
DescVal: 1,
DescStrPos: "Level %d %s Aura When Equipped",
DescStrNeg: "Level %d %s Aura When Equipped",
},
"item_fractionaltargetac": {
Name: "item_fractionaltargetac",
DescFnID: 20,
DescVal: 1,
DescStrPos: "Target Defense",
DescStrNeg: "Target Defense",
},
"attack_vs_montype": {
Name: "item_fractionaltargetac",
DescFnID: 22,
DescVal: 1,
DescStrPos: "to Attack Rating versus",
DescStrNeg: "to Attack Rating versus",
},
"item_reanimate": {
Name: "item_reanimate",
DescFnID: 23,
DescVal: 2,
DescStrPos: "Reanimate as:",
DescStrNeg: "Reanimate as:",
},
"item_charged_skill": {
Name: "item_charged_skill",
DescFnID: 24,
DescVal: 2,
DescStrPos: "(%d/%d Charges)",
DescStrNeg: "(%d/%d Charges)",
},
"item_singleskill": {
Name: "item_singleskill",
DescFnID: 27,
DescVal: 0,
},
"item_nonclassskill": {
Name: "item_nonclassskill",
DescFnID: 28,
DescVal: 2,
DescStrPos: "(%d/%d Charges)",
DescStrNeg: "(%d/%d Charges)",
},
"item_armor_percent": {
Name: "item_armor_percent",
DescFnID: 4,
DescVal: 1,
DescStrPos: "Enhanced Defense",
DescStrNeg: "Enhanced Defense",
},
"item_fastercastrate": {
Name: "item_fastercastrate",
DescFnID: 4,
DescVal: 1,
DescStrPos: "Faster Cast Rate",
DescStrNeg: "Faster Cast Rate",
},
"item_skillonlevelup": {
Name: "item_skillonlevelup",
DescFnID: 15,
DescVal: 0,
DescStrPos: "%d%% Chance to cast level %d %s when you Level-Up",
DescStrNeg: "%d%% Chance to cast level %d %s when you Level-Up",
},
"item_numsockets": {
Name: "item_numsockets",
},
"poisonmindam": {
Name: "poisonmindam",
DescFnID: 1,
DescVal: 1,
DescStrPos: "to Minimum Poison Damage",
DescStrNeg: "to Minimum Poison Damage",
},
"poisonmaxdam": {
Name: "poisonmaxdam",
DescFnID: 1,
DescVal: 1,
DescStrPos: "to Maximum Poison Damage",
DescStrNeg: "to Maximum Poison Damage",
},
"poisonlength": {
Name: "poisonlength",
},
}
var itemStatCosts = map[string]*d2records.ItemStatCostRecord{
"strength": {
Name: "strength",
DescFnID: 1,
DescVal: 1,
DescStrPos: "to Strength",
DescStrNeg: "to Strength",
},
"dexterity": {
Name: "dexterity",
DescFnID: 1,
DescVal: 1,
DescStrPos: "to Dexterity",
DescStrNeg: "to Dexterity",
},
"vitality": {
Name: "vitality",
DescFnID: 1,
DescVal: 1,
DescStrPos: "to Vitality",
DescStrNeg: "to Vitality",
},
"energy": {
Name: "energy",
DescFnID: 1,
DescVal: 1,
DescStrPos: "to Energy",
DescStrNeg: "to Energy",
},
"hpregen": {
Name: "hpregen",
DescFnID: 1,
DescVal: 2,
DescStrPos: "Replenish Life",
DescStrNeg: "Drain Life",
},
"toblock": {
Name: "toblock",
DescFnID: 2,
DescVal: 1,
DescStrPos: "Increased Chance of Blocking",
DescStrNeg: "Increased Chance of Blocking",
},
"item_absorblight_percent": {
Name: "item_absorblight_percent",
DescFnID: 2,
DescVal: 2,
DescStrPos: "Lightning Absorb",
DescStrNeg: "Lightning Absorb",
},
"item_maxdurability_percent": {
Name: "item_maxdurability_percent",
DescFnID: 2,
DescVal: 2,
DescStrPos: "Increase Maximum Durability",
DescStrNeg: "Increase Maximum Durability",
},
"item_restinpeace": {
Name: "item_restinpeace",
DescFnID: 3,
DescVal: 0,
DescStrPos: "Slain Monsters Rest in Peace",
DescStrNeg: "Slain Monsters Rest in Peace",
},
"normal_damage_reduction": {
Name: "normal_damage_reduction",
DescFnID: 3,
DescVal: 2,
DescStrPos: "Damage Reduced by",
DescStrNeg: "Damage Reduced by",
},
"poisonresist": {
Name: "poisonresist",
DescFnID: 4,
DescVal: 2,
DescStrPos: "Poison Resist",
DescStrNeg: "Poison Resist",
},
"item_fastermovevelocity": {
Name: "item_fastermovevelocity",
DescFnID: 4,
DescVal: 1,
DescStrPos: "Faster Run/Walk",
DescStrNeg: "Faster Run/Walk",
},
"item_howl": {
Name: "item_howl",
DescFnID: 5,
DescVal: 2,
DescStrPos: "Hit Causes Monster to Flee",
DescStrNeg: "Hit Causes Monster to Flee",
},
"item_hp_perlevel": {
Name: "item_hp_perlevel",
DescFnID: 6,
DescVal: 1,
DescStrPos: "to Life",
DescStrNeg: "to Life",
DescStr2: "(Based on Character Level)",
},
"item_resist_ltng_perlevel": {
Name: "item_resist_ltng_perlevel",
DescFnID: 7,
DescVal: 2,
DescStrPos: "Lightning Resist",
DescStrNeg: "Lightning Resist",
DescStr2: "(Based on Character Level)",
},
"item_find_magic_perlevel": {
Name: "item_find_magic_perlevel",
DescFnID: 7,
DescVal: 1,
DescStrPos: "Better Chance of Getting Magic Items",
DescStrNeg: "Better Chance of Getting Magic Items",
DescStr2: "(Based on Character Level)",
},
"item_armorpercent_perlevel": {
Name: "item_armorpercent_perlevel",
DescFnID: 8,
DescVal: 1,
DescStrPos: "Enhanced Defense",
DescStrNeg: "Enhanced Defense",
DescStr2: "(Based on Character Level)",
},
"item_regenstamina_perlevel": {
Name: "item_regenstamina_perlevel",
DescFnID: 8,
DescVal: 2,
DescStrPos: "Heal Stamina Plus",
DescStrNeg: "Heal Stamina Plus",
DescStr2: "(Based on Character Level)",
},
"item_thorns_perlevel": {
Name: "item_thorns_perlevel",
DescFnID: 9,
DescVal: 2,
DescStrPos: "Attacker Takes Damage of",
DescStrNeg: "Attacker Takes Damage of",
DescStr2: "(Based on Character Level)",
},
"item_replenish_durability": {
Name: "item_replenish_durability",
DescFnID: 11,
DescVal: 1,
DescStrPos: "Repairs %v durability per second",
DescStrNeg: "Repairs %v durability per second",
DescStr2: "",
},
"item_stupidity": {
Name: "item_stupidity",
DescFnID: 12,
DescVal: 2,
DescStrPos: "Hit Blinds Target",
DescStrNeg: "Hit Blinds Target",
},
"item_addclassskills": {
Name: "item_addclassskills",
DescFnID: 13,
DescVal: 1,
},
"item_addskill_tab": {
Name: "item_addskill_tab",
DescFnID: 14,
DescVal: 1,
},
"item_skillonattack": {
Name: "item_skillonattack",
DescFnID: 15,
DescVal: 1,
DescStrPos: "%d%% Chance to cast level %d %s on attack",
DescStrNeg: "%d%% Chance to cast level %d %s on attack",
},
"item_aura": {
Name: "item_aura",
DescFnID: 16,
DescVal: 1,
DescStrPos: "Level %d %s Aura When Equipped",
DescStrNeg: "Level %d %s Aura When Equipped",
},
"item_fractionaltargetac": {
Name: "item_fractionaltargetac",
DescFnID: 20,
DescVal: 1,
DescStrPos: "Target Defense",
DescStrNeg: "Target Defense",
},
"attack_vs_montype": {
Name: "item_fractionaltargetac",
DescFnID: 22,
DescVal: 1,
DescStrPos: "to Attack Rating versus",
DescStrNeg: "to Attack Rating versus",
},
"item_reanimate": {
Name: "item_reanimate",
DescFnID: 23,
DescVal: 2,
DescStrPos: "Reanimate as:",
DescStrNeg: "Reanimate as:",
},
"item_charged_skill": {
Name: "item_charged_skill",
DescFnID: 24,
DescVal: 2,
DescStrPos: "(%d/%d Charges)",
DescStrNeg: "(%d/%d Charges)",
},
"item_singleskill": {
Name: "item_singleskill",
DescFnID: 27,
DescVal: 0,
},
"item_nonclassskill": {
Name: "item_nonclassskill",
DescFnID: 28,
DescVal: 2,
DescStrPos: "(%d/%d Charges)",
DescStrNeg: "(%d/%d Charges)",
},
"item_armor_percent": {
Name: "item_armor_percent",
DescFnID: 4,
DescVal: 1,
DescStrPos: "Enhanced Defense",
DescStrNeg: "Enhanced Defense",
},
"item_fastercastrate": {
Name: "item_fastercastrate",
DescFnID: 4,
DescVal: 1,
DescStrPos: "Faster Cast Rate",
DescStrNeg: "Faster Cast Rate",
},
"item_skillonlevelup": {
Name: "item_skillonlevelup",
DescFnID: 15,
DescVal: 0,
DescStrPos: "%d%% Chance to cast level %d %s when you Level-Up",
DescStrNeg: "%d%% Chance to cast level %d %s when you Level-Up",
},
"item_numsockets": {
Name: "item_numsockets",
},
"poisonmindam": {
Name: "poisonmindam",
DescFnID: 1,
DescVal: 1,
DescStrPos: "to Minimum Poison Damage",
DescStrNeg: "to Minimum Poison Damage",
},
"poisonmaxdam": {
Name: "poisonmaxdam",
DescFnID: 1,
DescVal: 1,
DescStrPos: "to Maximum Poison Damage",
DescStrNeg: "to Maximum Poison Damage",
},
"poisonlength": {
Name: "poisonlength",
},
}
var charStats = map[d2enum.Hero]*d2datadict.CharStatsRecord{
d2enum.HeroPaladin: {
Class: d2enum.HeroPaladin,
SkillStrAll: "to Paladin Skill Levels",
SkillStrClassOnly: "(Paladin Only)",
SkillStrTab: [3]string{
"+%d to Combat Skills",
"+%d to Offensive Auras",
"+%d to Defensive Auras",
},
var charStats = map[d2enum.Hero]*d2records.CharStatsRecord{
d2enum.HeroPaladin: {
Class: d2enum.HeroPaladin,
SkillStrAll: "to Paladin Skill Levels",
SkillStrClassOnly: "(Paladin Only)",
SkillStrTab: [3]string{
"+%d to Combat Skills",
"+%d to Offensive Auras",
"+%d to Defensive Auras",
},
}
},
}
var skillDetails = map[int]*d2datadict.SkillRecord{
37: {Skill: "Warmth"},
64: {Skill: "Frozen Orb"},
}
var skillDetails = map[int]*d2records.SkillRecord{
37: {Skill: "Warmth"},
64: {Skill: "Frozen Orb"},
}
var monStats = map[string]*d2datadict.MonStatsRecord{
"Specter": {NameString: "Specter", ID: 40},
}
var monStats = map[string]*d2records.MonStatsRecord{
"Specter": {NameString: "Specter", ID: 40},
}
properties := map[string]*d2datadict.PropertyRecord{
"allstats": {
Code: "allstats",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 1, StatCode: "strength"},
{FunctionID: 3, StatCode: "dexterity"},
{FunctionID: 3, StatCode: "vitality"},
{FunctionID: 3, StatCode: "energy"},
},
var properties = map[string]*d2records.PropertyRecord{
"allstats": {
Code: "allstats",
Stats: [7]*d2records.PropertyStatRecord{
{FunctionID: 1, StatCode: "strength"},
{FunctionID: 3, StatCode: "dexterity"},
{FunctionID: 3, StatCode: "vitality"},
{FunctionID: 3, StatCode: "energy"},
},
"ac%": {
Code: "ac%",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 2, StatCode: "item_armor_percent"},
},
},
"ac%": {
Code: "ac%",
Stats: [7]*d2records.PropertyStatRecord{
{FunctionID: 2, StatCode: "item_armor_percent"},
},
// dmg-min, dmg-max, dmg%, indestruct, and ethereal do not yield stats.
// these properties are used specifically to compute a value.
"dmg-min": {
Code: "dmg-min",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 5},
},
},
"dmg-min": {
Code: "dmg-min",
Stats: [7]*d2records.PropertyStatRecord{
{FunctionID: 5},
},
"dmg-max": {
Code: "dmg-max",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 6},
},
},
"dmg-max": {
Code: "dmg-max",
Stats: [7]*d2records.PropertyStatRecord{
{FunctionID: 6},
},
"dmg%": {
Code: "dmg%",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 7},
},
},
"dmg%": {
Code: "dmg%",
Stats: [7]*d2records.PropertyStatRecord{
{FunctionID: 7},
},
"cast1": {
Code: "cast1",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 8, StatCode: "item_fastercastrate"},
},
},
"cast1": {
Code: "cast1",
Stats: [7]*d2records.PropertyStatRecord{
{FunctionID: 8, StatCode: "item_fastercastrate"},
},
"skilltab": {
Code: "skilltab",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 10, StatCode: "item_addskill_tab"},
},
},
"skilltab": {
Code: "skilltab",
Stats: [7]*d2records.PropertyStatRecord{
{FunctionID: 10, StatCode: "item_addskill_tab"},
},
"levelup-skill": {
Code: "levelup-skill",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 11, StatCode: "item_skillonlevelup"},
},
},
"levelup-skill": {
Code: "levelup-skill",
Stats: [7]*d2records.PropertyStatRecord{
{FunctionID: 11, StatCode: "item_skillonlevelup"},
},
"skill-rand": {
Code: "skill-rand",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 12, StatCode: "item_singleskill"},
},
},
"skill-rand": {
Code: "skill-rand",
Stats: [7]*d2records.PropertyStatRecord{
{FunctionID: 12, StatCode: "item_singleskill"},
},
"dur%": {
Code: "dur%",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 13, StatCode: "item_maxdurability_percent"},
},
},
"dur%": {
Code: "dur%",
Stats: [7]*d2records.PropertyStatRecord{
{FunctionID: 13, StatCode: "item_maxdurability_percent"},
},
"sock": {
Code: "sock",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 14, StatCode: "item_numsockets"},
},
},
"sock": {
Code: "sock",
Stats: [7]*d2records.PropertyStatRecord{
{FunctionID: 14, StatCode: "item_numsockets"},
},
"dmg-pois": {
Code: "dmg-pois",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 15, StatCode: "poisonmindam"},
{FunctionID: 16, StatCode: "poisonmaxdam"},
{FunctionID: 17, StatCode: "poisonlength"},
},
},
"dmg-pois": {
Code: "dmg-pois",
Stats: [7]*d2records.PropertyStatRecord{
{FunctionID: 15, StatCode: "poisonmindam"},
{FunctionID: 16, StatCode: "poisonmaxdam"},
{FunctionID: 17, StatCode: "poisonlength"},
},
"charged": {
Code: "charged",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 19, StatCode: "item_charged_skill"},
},
},
"charged": {
Code: "charged",
Stats: [7]*d2records.PropertyStatRecord{
{FunctionID: 19, StatCode: "item_charged_skill"},
},
"indestruct": {
Code: "indestruct",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 20},
},
},
"indestruct": {
Code: "indestruct",
Stats: [7]*d2records.PropertyStatRecord{
{FunctionID: 20},
},
"pal": {
Code: "pal",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 21, StatCode: "item_addclassskills", Value: 3},
},
},
"pal": {
Code: "pal",
Stats: [7]*d2records.PropertyStatRecord{
{FunctionID: 21, StatCode: "item_addclassskills", Value: 3},
},
"oskill": {
Code: "oskill",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 22, StatCode: "item_nonclassskill"},
},
},
"oskill": {
Code: "oskill",
Stats: [7]*d2records.PropertyStatRecord{
{FunctionID: 22, StatCode: "item_nonclassskill"},
},
"ethereal": {
Code: "ethereal",
Stats: [7]*d2datadict.PropertyStatRecord{
{FunctionID: 23},
},
},
"ethereal": {
Code: "ethereal",
Stats: [7]*d2records.PropertyStatRecord{
{FunctionID: 23},
},
}
},
}
d2datadict.ItemStatCosts = itemStatCosts
d2datadict.CharStats = charStats
d2datadict.SkillDetails = skillDetails
d2datadict.MonStats = monStats
d2datadict.Properties = properties
var testAssetManager *d2asset.AssetManager
var testItemFactory *ItemFactory
func TestSetup(t *testing.T) {
testAssetManager = &d2asset.AssetManager{}
testAssetManager.Records = &d2records.RecordManager{}
testItemFactory, _ = NewItemFactory(testAssetManager)
testAssetManager.Records.Item.Stats = itemStatCosts
testAssetManager.Records.Character.Stats = charStats
testAssetManager.Records.Skill.Details = skillDetails
testAssetManager.Records.Monster.Stats = monStats
testAssetManager.Records.Properties = properties
}
func TestNewProperty(t *testing.T) { //nolint:funlen it's mostly test-case definitions
@ -546,7 +552,7 @@ func TestNewProperty(t *testing.T) { //nolint:funlen it's mostly test-case defin
for testIdx := range tests {
test := &tests[testIdx]
prop := NewProperty(test.propKey, test.inputValues...)
prop := testItemFactory.NewProperty(test.propKey, test.inputValues...)
if prop == nil {
t.Error("property is nil")

View File

@ -4,9 +4,10 @@ import (
"log"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dt1"
@ -24,17 +25,17 @@ type MapEngine struct {
seed int64 // The map seed
entities map[string]d2interface.MapEntity // Entities on the map
tiles []MapTile
size d2geom.Size // Size of the map, in tiles
levelType d2datadict.LevelTypeRecord // Level type of this map
dt1TileData []d2dt1.Tile // DT1 tile data
startSubTileX int // Starting X position
startSubTileY int // Starting Y position
dt1Files []string // List of DS1 strings
size d2geom.Size // Size of the map, in tiles
levelType d2records.LevelTypeRecord // Level type of this map
dt1TileData []d2dt1.Tile // DT1 tile data
startSubTileX int // Starting X position
startSubTileY int // Starting Y position
dt1Files []string // List of DS1 strings
}
// CreateMapEngine creates a new instance of the map engine and returns a pointer to it.
func CreateMapEngine(asset *d2asset.AssetManager) *MapEngine {
entity := d2mapentity.NewMapEntityFactory(asset)
entity, _ := d2mapentity.NewMapEntityFactory(asset)
stamp := d2mapstamp.NewStampFactory(asset, entity)
engine := &MapEngine{
@ -54,7 +55,7 @@ func (m *MapEngine) GetStartingPosition() (x, y int) {
// ResetMap clears all map and entity data and reloads it from the cached files.
func (m *MapEngine) ResetMap(levelType d2enum.RegionIdType, width, height int) {
m.entities = make(map[string]d2interface.MapEntity)
m.levelType = d2datadict.LevelTypes[levelType]
m.levelType = *m.asset.Records.Level.Types[levelType]
m.size = d2geom.Size{Width: width, Height: height}
m.tiles = make([]MapTile, width*height)
m.dt1TileData = make([]d2dt1.Tile, 0)
@ -115,7 +116,7 @@ func (m *MapEngine) AddDS1(fileName string) {
}
// LevelType returns the level type of this map.
func (m *MapEngine) LevelType() d2datadict.LevelTypeRecord {
func (m *MapEngine) LevelType() d2records.LevelTypeRecord {
return m.levelType
}

View File

@ -1,16 +1,17 @@
package d2mapentity
import (
"errors"
"fmt"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
uuid "github.com/satori/go.uuid"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2tbl"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero"
@ -19,13 +20,31 @@ import (
)
// NewMapEntityFactory creates a MapEntityFactory instance with the given asset manager
func NewMapEntityFactory(asset *d2asset.AssetManager) *MapEntityFactory {
return &MapEntityFactory{asset}
func NewMapEntityFactory(asset *d2asset.AssetManager) (*MapEntityFactory, error) {
itemFactory, err := diablo2item.NewItemFactory(asset)
if err != nil {
return nil, err
}
stateFactory, err := d2hero.NewHeroStateFactory(asset)
if err != nil {
return nil, err
}
entityFactory := &MapEntityFactory{
stateFactory,
asset,
itemFactory,
}
return entityFactory, nil
}
// MapEntityFactory creates map entities for the MapEngine
type MapEntityFactory struct {
*d2hero.HeroStateFactory
asset *d2asset.AssetManager
item *diablo2item.ItemFactory
}
// NewAnimatedEntity creates an instance of AnimatedEntity
@ -41,7 +60,7 @@ func NewAnimatedEntity(x, y int, animation d2interface.Animation) *AnimatedEntit
// NewPlayer creates a new player entity and returns a pointer to it.
func (f *MapEntityFactory) NewPlayer(id, name string, x, y, direction int, heroType d2enum.Hero,
stats *d2hero.HeroStatsState, skills *d2hero.HeroSkillsState, equipment *d2inventory.CharacterEquipment) *Player {
stats *d2hero.HeroStatsState, skills map[int]*d2hero.HeroSkill, equipment *d2inventory.CharacterEquipment) *Player {
layerEquipment := &[d2enum.CompositeTypeMax]string{
d2enum.CompositeTypeHead: equipment.Head.GetArmorClass(),
d2enum.CompositeTypeTorso: equipment.Torso.GetArmorClass(),
@ -59,21 +78,25 @@ func (f *MapEntityFactory) NewPlayer(id, name string, x, y, direction int, heroT
panic(err)
}
stats.NextLevelExp = d2datadict.GetExperienceBreakpoint(heroType, stats.Level)
stats.NextLevelExp = f.asset.Records.GetExperienceBreakpoint(heroType, stats.Level)
stats.Stamina = stats.MaxStamina
defaultCharStats := f.asset.Records.Character.Stats[heroType]
statsState := f.HeroStateFactory.CreateHeroStatsState(heroType, defaultCharStats)
heroState, _ := f.CreateHeroState(name, heroType, statsState)
attackSkillID := 0
result := &Player{
mapEntity: newMapEntity(x, y),
composite: composite,
Equipment: equipment,
Stats: stats,
Skills: skills,
Stats: heroState.Stats,
Skills: heroState.Skills,
//TODO: active left & right skill should be loaded from save file instead
LeftSkill: (*skills)[attackSkillID],
RightSkill: (*skills)[attackSkillID],
name: name,
Class: heroType,
LeftSkill: heroState.Skills[attackSkillID],
RightSkill: heroState.Skills[attackSkillID],
name: name,
Class: heroType,
//nameLabel: d2ui.NewLabel(d2resource.FontFormal11, d2resource.PaletteStatic),
isRunToggled: true,
isInTown: true,
@ -99,7 +122,7 @@ func (f *MapEntityFactory) NewPlayer(id, name string, x, y, direction int, heroT
}
// NewMissile creates a new Missile and initializes it's animation.
func (f *MapEntityFactory) NewMissile(x, y int, record *d2datadict.MissileRecord) (*Missile, error) {
func (f *MapEntityFactory) NewMissile(x, y int, record *d2records.MissileRecord) (*Missile, error) {
animation, err := f.asset.LoadAnimation(
fmt.Sprintf("%s/%s.dcc", d2resource.MissileData, record.Animation.CelFileName),
d2resource.PaletteUnits,
@ -128,10 +151,10 @@ func (f *MapEntityFactory) NewMissile(x, y int, record *d2datadict.MissileRecord
// NewItem creates an item map entity
func (f *MapEntityFactory) NewItem(x, y int, codes ...string) (*Item, error) {
item := diablo2item.NewItem(codes...)
item, err := f.item.NewItem(codes...)
if item == nil {
return nil, errors.New(errInvalidItemCodes)
if err != nil {
return nil, err
}
filename := item.CommonRecord().FlippyFile
@ -155,12 +178,12 @@ func (f *MapEntityFactory) NewItem(x, y int, codes ...string) (*Item, error) {
}
// NewNPC creates a new NPC and returns a pointer to it.
func (f *MapEntityFactory) NewNPC(x, y int, monstat *d2datadict.MonStatsRecord, direction int) (*NPC, error) {
func (f *MapEntityFactory) NewNPC(x, y int, monstat *d2records.MonStatsRecord, direction int) (*NPC, error) {
result := &NPC{
mapEntity: newMapEntity(x, y),
HasPaths: false,
monstatRecord: monstat,
monstatEx: d2datadict.MonStats2[monstat.ExtraDataKey],
monstatEx: f.asset.Records.Monster.Stats2[monstat.ExtraDataKey],
}
var equipment [16]string
@ -214,9 +237,9 @@ func (f *MapEntityFactory) NewObject(x, y int, objectRec *d2datadict.ObjectRecor
entity.composite = composite
entity.setMode(d2enum.ObjectAnimationModeNeutral, 0, false)
_ = entity.setMode(d2enum.ObjectAnimationModeNeutral, 0, false)
initObject(entity)
_, _ = initObject(entity)
return entity, nil
}

View File

@ -9,10 +9,6 @@ import (
// static check that item implements map entity interface
var _ d2interface.MapEntity = &Item{}
const (
errInvalidItemCodes = "invalid item codes supplied"
)
// Item is a map entity for an item
type Item struct {
*AnimatedEntity

View File

@ -3,15 +3,15 @@ package d2mapentity
import (
"math"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
)
// Missile is a simple animated entity representing a projectile,
// such as a spell or arrow.
type Missile struct {
*AnimatedEntity
record *d2datadict.MissileRecord
record *d2records.MissileRecord
}
// ID returns the missile uuid

View File

@ -3,7 +3,8 @@ package d2mapentity
import (
"math/rand"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector"
@ -21,8 +22,8 @@ type NPC struct {
action int
path int
repetitions int
monstatRecord *d2datadict.MonStatsRecord
monstatEx *d2datadict.MonStats2Record
monstatRecord *d2records.MonStatsRecord
monstatEx *d2records.MonStats2Record
HasPaths bool
isDone bool
}

View File

@ -19,7 +19,7 @@ type Player struct {
composite *d2asset.Composite
Equipment *d2inventory.CharacterEquipment
Stats *d2hero.HeroStatsState
Skills *d2hero.HeroSkillsState
Skills map[int]*d2hero.HeroSkill
LeftSkill *d2hero.HeroSkill
RightSkill *d2hero.HeroSkill
Class d2enum.Hero

View File

@ -5,34 +5,24 @@ import (
"math/rand"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2geom"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapengine"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapgen/d2wilderness"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapstamp"
)
func loadPreset(mapEngine *d2mapengine.MapEngine, id, index int) *d2mapstamp.Stamp {
for _, file := range d2datadict.LevelPreset(id).Files {
mapEngine.AddDS1(file)
}
return mapEngine.LoadStamp(d2enum.RegionAct1Wilderness, id, index)
}
// GenerateAct1Overworld generates the map and entities for the first town and surrounding area.
func GenerateAct1Overworld(mapEngine *d2mapengine.MapEngine) {
rand.Seed(mapEngine.Seed())
func (g *MapGenerator) GenerateAct1Overworld() {
rand.Seed(g.engine.Seed())
wilderness1Details := d2datadict.GetLevelDetails(2)
wilderness1Details := g.asset.Records.GetLevelDetails(2)
mapEngine.ResetMap(d2enum.RegionAct1Town, 150, 150)
mapWidth := mapEngine.Size().Width
mapHeight := mapEngine.Size().Height
g.engine.ResetMap(d2enum.RegionAct1Town, 150, 150)
mapWidth := g.engine.Size().Width
mapHeight := g.engine.Size().Height
townStamp := mapEngine.LoadStamp(d2enum.RegionAct1Town, 1, -1)
townStamp := g.engine.LoadStamp(d2enum.RegionAct1Town, 1, -1)
townStamp.RegionPath()
townSize := townStamp.Size()
@ -40,57 +30,59 @@ func GenerateAct1Overworld(mapEngine *d2mapengine.MapEngine) {
switch {
case strings.Contains(townStamp.RegionPath(), "E1"):
mapEngine.PlaceStamp(townStamp, 0, 0)
generateWilderness1TownEast(mapEngine, townSize.Width, 0)
g.engine.PlaceStamp(townStamp, 0, 0)
g.generateWilderness1TownEast(townSize.Width, 0)
case strings.Contains(townStamp.RegionPath(), "S1"):
mapEngine.PlaceStamp(townStamp, mapWidth-townSize.Width, 0)
rightWaterBorderStamp := loadPreset(mapEngine, d2wilderness.WaterBorderEast, 0)
rightWaterBorderStamp2 := loadPreset(mapEngine, d2wilderness.WaterBorderWest, 0)
g.engine.PlaceStamp(townStamp, mapWidth-townSize.Width, 0)
rightWaterBorderStamp := g.loadPreset(d2wilderness.WaterBorderEast, 0)
rightWaterBorderStamp2 := g.loadPreset(d2wilderness.WaterBorderWest, 0)
for y := townSize.Height; y < mapHeight-9; y += 9 {
mapEngine.PlaceStamp(rightWaterBorderStamp, mapWidth-17, y)
mapEngine.PlaceStamp(rightWaterBorderStamp2, mapWidth-9, y)
g.engine.PlaceStamp(rightWaterBorderStamp, mapWidth-17, y)
g.engine.PlaceStamp(rightWaterBorderStamp2, mapWidth-9, y)
}
generateWilderness1TownSouth(mapEngine, mapWidth-wilderness1Details.SizeXNormal-14, townSize.Height)
g.generateWilderness1TownSouth(mapWidth-wilderness1Details.SizeXNormal-14, townSize.Height)
case strings.Contains(townStamp.RegionPath(), "W1"):
mapEngine.PlaceStamp(townStamp, mapWidth-townSize.Width, mapHeight-townSize.Height)
generateWilderness1TownWest(mapEngine, mapWidth-townSize.Width-wilderness1Details.SizeXNormal, mapHeight-wilderness1Details.SizeYNormal)
g.engine.PlaceStamp(townStamp, mapWidth-townSize.Width, mapHeight-townSize.Height)
startX := mapWidth - townSize.Width - wilderness1Details.SizeXNormal
startY := mapHeight - wilderness1Details.SizeYNormal
g.generateWilderness1TownWest(startX, startY)
default:
mapEngine.PlaceStamp(townStamp, mapWidth-townSize.Width, mapHeight-townSize.Height)
g.engine.PlaceStamp(townStamp, mapWidth-townSize.Width, mapHeight-townSize.Height)
}
}
func generateWilderness1TownEast(mapEngine *d2mapengine.MapEngine, startX, startY int) {
levelDetails := d2datadict.GetLevelDetails(2)
func (g *MapGenerator) generateWilderness1TownEast(startX, startY int) {
levelDetails := g.asset.Records.GetLevelDetails(2)
fenceNorthStamp := []*d2mapstamp.Stamp{
loadPreset(mapEngine, d2wilderness.TreeBorderNorth, 0),
loadPreset(mapEngine, d2wilderness.TreeBorderNorth, 1),
loadPreset(mapEngine, d2wilderness.TreeBorderNorth, 2),
g.loadPreset(d2wilderness.TreeBorderNorth, 0),
g.loadPreset(d2wilderness.TreeBorderNorth, 1),
g.loadPreset(d2wilderness.TreeBorderNorth, 2),
}
fenceSouthStamp := []*d2mapstamp.Stamp{
loadPreset(mapEngine, d2wilderness.TreeBorderSouth, 0),
loadPreset(mapEngine, d2wilderness.TreeBorderSouth, 1),
loadPreset(mapEngine, d2wilderness.TreeBorderSouth, 2),
g.loadPreset(d2wilderness.TreeBorderSouth, 0),
g.loadPreset(d2wilderness.TreeBorderSouth, 1),
g.loadPreset(d2wilderness.TreeBorderSouth, 2),
}
fenceWestStamp := []*d2mapstamp.Stamp{
loadPreset(mapEngine, d2wilderness.TreeBorderWest, 0),
loadPreset(mapEngine, d2wilderness.TreeBorderWest, 1),
loadPreset(mapEngine, d2wilderness.TreeBorderWest, 2),
g.loadPreset(d2wilderness.TreeBorderWest, 0),
g.loadPreset(d2wilderness.TreeBorderWest, 1),
g.loadPreset(d2wilderness.TreeBorderWest, 2),
}
fenceEastStamp := []*d2mapstamp.Stamp{
loadPreset(mapEngine, d2wilderness.TreeBorderEast, 0),
loadPreset(mapEngine, d2wilderness.TreeBorderEast, 1),
loadPreset(mapEngine, d2wilderness.TreeBorderEast, 2),
g.loadPreset(d2wilderness.TreeBorderEast, 0),
g.loadPreset(d2wilderness.TreeBorderEast, 1),
g.loadPreset(d2wilderness.TreeBorderEast, 2),
}
fenceSouthWestStamp := loadPreset(mapEngine, d2wilderness.TreeBorderSouthWest, 0)
fenceNorthEastStamp := loadPreset(mapEngine, d2wilderness.TreeBorderNorthEast, 0)
fenceSouthEastStamp := loadPreset(mapEngine, d2wilderness.TreeBorderSouthEast, 0)
fenceWestEdge := loadPreset(mapEngine, d2wilderness.TreeBoxNorthEast, 0)
fenceSouthWestStamp := g.loadPreset(d2wilderness.TreeBorderSouthWest, 0)
fenceNorthEastStamp := g.loadPreset(d2wilderness.TreeBorderNorthEast, 0)
fenceSouthEastStamp := g.loadPreset(d2wilderness.TreeBorderSouthEast, 0)
fenceWestEdge := g.loadPreset(d2wilderness.TreeBoxNorthEast, 0)
areaRect := d2geom.Rectangle{
Left: startX,
@ -98,54 +90,57 @@ func generateWilderness1TownEast(mapEngine *d2mapengine.MapEngine, startX, start
Width: levelDetails.SizeXNormal,
Height: levelDetails.SizeYNormal - 3,
}
generateWilderness1Contents(mapEngine, areaRect)
g.generateWilderness1Contents(areaRect)
// Draw the north and south fence
for i := 0; i < 9; i++ {
mapEngine.PlaceStamp(fenceNorthStamp[rand.Intn(3)], startX+(i*9), startY)
mapEngine.PlaceStamp(fenceSouthStamp[rand.Intn(3)], startX+(i*9), startY+(levelDetails.SizeYNormal+6))
g.engine.PlaceStamp(fenceNorthStamp[rand.Intn(3)], startX+(i*9), startY)
g.engine.PlaceStamp(fenceSouthStamp[rand.Intn(3)], startX+(i*9),
startY+(levelDetails.SizeYNormal+6))
}
// West fence
for i := 1; i < 6; i++ {
mapEngine.PlaceStamp(fenceWestStamp[rand.Intn(3)], startX, startY+(levelDetails.SizeYNormal+6)-(i*9))
g.engine.PlaceStamp(fenceWestStamp[rand.Intn(3)], startX,
startY+(levelDetails.SizeYNormal+6)-(i*9))
}
// East Fence
for i := 1; i < 10; i++ {
mapEngine.PlaceStamp(fenceEastStamp[rand.Intn(3)], startX+levelDetails.SizeXNormal, startY+(i*9))
g.engine.PlaceStamp(fenceEastStamp[rand.Intn(3)], startX+levelDetails.SizeXNormal, startY+(i*9))
}
mapEngine.PlaceStamp(fenceSouthWestStamp, startX, startY+levelDetails.SizeYNormal+6)
mapEngine.PlaceStamp(fenceWestEdge, startX, startY+(levelDetails.SizeYNormal-3)-45)
mapEngine.PlaceStamp(fenceNorthEastStamp, startX+levelDetails.SizeXNormal, startY)
mapEngine.PlaceStamp(fenceSouthEastStamp, startX+levelDetails.SizeXNormal, startY+levelDetails.SizeYNormal+6)
g.engine.PlaceStamp(fenceSouthWestStamp, startX, startY+levelDetails.SizeYNormal+6)
g.engine.PlaceStamp(fenceWestEdge, startX, startY+(levelDetails.SizeYNormal-3)-45)
g.engine.PlaceStamp(fenceNorthEastStamp, startX+levelDetails.SizeXNormal, startY)
g.engine.PlaceStamp(fenceSouthEastStamp, startX+levelDetails.SizeXNormal, startY+levelDetails.SizeYNormal+6)
}
func generateWilderness1TownSouth(mapEngine *d2mapengine.MapEngine, startX, startY int) {
levelDetails := d2datadict.GetLevelDetails(2)
func (g *MapGenerator) generateWilderness1TownSouth(startX, startY int) {
levelDetails := g.asset.Records.GetLevelDetails(2)
fenceNorthStamp := []*d2mapstamp.Stamp{
loadPreset(mapEngine, d2wilderness.TreeBorderNorth, 0),
loadPreset(mapEngine, d2wilderness.TreeBorderNorth, 1),
loadPreset(mapEngine, d2wilderness.TreeBorderNorth, 2),
g.loadPreset(d2wilderness.TreeBorderNorth, 0),
g.loadPreset(d2wilderness.TreeBorderNorth, 1),
g.loadPreset(d2wilderness.TreeBorderNorth, 2),
}
fenceWestStamp := []*d2mapstamp.Stamp{
loadPreset(mapEngine, d2wilderness.TreeBorderWest, 0),
loadPreset(mapEngine, d2wilderness.TreeBorderWest, 1),
loadPreset(mapEngine, d2wilderness.TreeBorderWest, 2),
g.loadPreset(d2wilderness.TreeBorderWest, 0),
g.loadPreset(d2wilderness.TreeBorderWest, 1),
g.loadPreset(d2wilderness.TreeBorderWest, 2),
}
fenceSouthStamp := []*d2mapstamp.Stamp{
loadPreset(mapEngine, d2wilderness.TreeBorderSouth, 0),
loadPreset(mapEngine, d2wilderness.TreeBorderSouth, 1),
loadPreset(mapEngine, d2wilderness.TreeBorderSouth, 2),
g.loadPreset(d2wilderness.TreeBorderSouth, 0),
g.loadPreset(d2wilderness.TreeBorderSouth, 1),
g.loadPreset(d2wilderness.TreeBorderSouth, 2),
}
fenceNorthWestStamp := loadPreset(mapEngine, d2wilderness.TreeBorderNorthWest, 0)
fenceSouthWestStamp := loadPreset(mapEngine, d2wilderness.TreeBorderSouthWest, 0)
fenceWaterBorderSouthEast := loadPreset(mapEngine, d2wilderness.WaterBorderEast, 1)
fenceNorthWestStamp := g.loadPreset(d2wilderness.TreeBorderNorthWest, 0)
fenceSouthWestStamp := g.loadPreset(d2wilderness.TreeBorderSouthWest, 0)
fenceWaterBorderSouthEast := g.loadPreset(d2wilderness.WaterBorderEast, 1)
areaRect := d2geom.Rectangle{
Left: startX + 2,
@ -153,84 +148,84 @@ func generateWilderness1TownSouth(mapEngine *d2mapengine.MapEngine, startX, star
Width: levelDetails.SizeXNormal - 2,
Height: levelDetails.SizeYNormal - 3,
}
generateWilderness1Contents(mapEngine, areaRect)
g.generateWilderness1Contents(areaRect)
// Draw the north fence
for i := 0; i < 4; i++ {
mapEngine.PlaceStamp(fenceNorthStamp[rand.Intn(3)], startX+(i*9)+5, startY-6)
g.engine.PlaceStamp(fenceNorthStamp[rand.Intn(3)], startX+(i*9)+5, startY-6)
}
// Draw the west fence
for i := 0; i < 8; i++ {
mapEngine.PlaceStamp(fenceWestStamp[rand.Intn(3)], startX, startY+(i*9)+3)
g.engine.PlaceStamp(fenceWestStamp[rand.Intn(3)], startX, startY+(i*9)+3)
}
// Draw the south fence
for i := 1; i < 9; i++ {
mapEngine.PlaceStamp(fenceSouthStamp[rand.Intn(3)], startX+(i*9), startY+(8*9)+3)
g.engine.PlaceStamp(fenceSouthStamp[rand.Intn(3)], startX+(i*9), startY+(8*9)+3)
}
mapEngine.PlaceStamp(fenceNorthWestStamp, startX, startY-6)
mapEngine.PlaceStamp(fenceSouthWestStamp, startX, startY+(8*9)+3)
mapEngine.PlaceStamp(fenceWaterBorderSouthEast, startX+(9*9)-4, startY+(8*9)+1)
g.engine.PlaceStamp(fenceNorthWestStamp, startX, startY-6)
g.engine.PlaceStamp(fenceSouthWestStamp, startX, startY+(8*9)+3)
g.engine.PlaceStamp(fenceWaterBorderSouthEast, startX+(9*9)-4, startY+(8*9)+1)
}
func generateWilderness1TownWest(mapEngine *d2mapengine.MapEngine, startX, startY int) {
levelDetails := d2datadict.GetLevelDetails(2)
func (g *MapGenerator) generateWilderness1TownWest(startX, startY int) {
levelDetails := g.asset.Records.GetLevelDetails(2)
fenceEastEdge := loadPreset(mapEngine, d2wilderness.TreeBoxSouthWest, 0)
fenceNorthWestStamp := loadPreset(mapEngine, d2wilderness.TreeBorderNorthWest, 0)
fenceNorthEastStamp := loadPreset(mapEngine, d2wilderness.TreeBorderNorthEast, 0)
fenceSouthWestStamp := loadPreset(mapEngine, d2wilderness.TreeBorderSouthWest, 0)
fenceEastEdge := g.loadPreset(d2wilderness.TreeBoxSouthWest, 0)
fenceNorthWestStamp := g.loadPreset(d2wilderness.TreeBorderNorthWest, 0)
fenceNorthEastStamp := g.loadPreset(d2wilderness.TreeBorderNorthEast, 0)
fenceSouthWestStamp := g.loadPreset(d2wilderness.TreeBorderSouthWest, 0)
fenceSouthStamp := []*d2mapstamp.Stamp{
loadPreset(mapEngine, d2wilderness.TreeBorderSouth, 0),
loadPreset(mapEngine, d2wilderness.TreeBorderSouth, 1),
loadPreset(mapEngine, d2wilderness.TreeBorderSouth, 2),
g.loadPreset(d2wilderness.TreeBorderSouth, 0),
g.loadPreset(d2wilderness.TreeBorderSouth, 1),
g.loadPreset(d2wilderness.TreeBorderSouth, 2),
}
fenceNorthStamp := []*d2mapstamp.Stamp{
loadPreset(mapEngine, d2wilderness.TreeBorderNorth, 0),
loadPreset(mapEngine, d2wilderness.TreeBorderNorth, 1),
loadPreset(mapEngine, d2wilderness.TreeBorderNorth, 2),
g.loadPreset(d2wilderness.TreeBorderNorth, 0),
g.loadPreset(d2wilderness.TreeBorderNorth, 1),
g.loadPreset(d2wilderness.TreeBorderNorth, 2),
}
fenceEastStamp := []*d2mapstamp.Stamp{
loadPreset(mapEngine, d2wilderness.TreeBorderEast, 0),
loadPreset(mapEngine, d2wilderness.TreeBorderEast, 1),
loadPreset(mapEngine, d2wilderness.TreeBorderEast, 2),
g.loadPreset(d2wilderness.TreeBorderEast, 0),
g.loadPreset(d2wilderness.TreeBorderEast, 1),
g.loadPreset(d2wilderness.TreeBorderEast, 2),
}
fenceWestStamp := []*d2mapstamp.Stamp{
loadPreset(mapEngine, d2wilderness.TreeBorderWest, 0),
loadPreset(mapEngine, d2wilderness.TreeBorderWest, 1),
loadPreset(mapEngine, d2wilderness.TreeBorderWest, 2),
g.loadPreset(d2wilderness.TreeBorderWest, 0),
g.loadPreset(d2wilderness.TreeBorderWest, 1),
g.loadPreset(d2wilderness.TreeBorderWest, 2),
}
// Draw the north and south fences
for i := 0; i < 9; i++ {
if i > 0 && i < 8 {
mapEngine.PlaceStamp(fenceNorthStamp[rand.Intn(3)], startX+(i*9)-1, startY-15)
g.engine.PlaceStamp(fenceNorthStamp[rand.Intn(3)], startX+(i*9)-1, startY-15)
}
mapEngine.PlaceStamp(fenceSouthStamp[rand.Intn(3)], startX+(i*9)-1, startY+levelDetails.SizeYNormal-12)
g.engine.PlaceStamp(fenceSouthStamp[rand.Intn(3)], startX+(i*9)-1, startY+levelDetails.SizeYNormal-12)
}
// Draw the east fence
for i := 0; i < 6; i++ {
mapEngine.PlaceStamp(fenceEastStamp[rand.Intn(3)], startX+levelDetails.SizeXNormal-9, startY+(i*9)-6)
g.engine.PlaceStamp(fenceEastStamp[rand.Intn(3)], startX+levelDetails.SizeXNormal-9, startY+(i*9)-6)
}
// Draw the west fence
for i := 0; i < 9; i++ {
mapEngine.PlaceStamp(fenceWestStamp[rand.Intn(3)], startX, startY+(i*9)-6)
g.engine.PlaceStamp(fenceWestStamp[rand.Intn(3)], startX, startY+(i*9)-6)
}
// Draw the west fence
mapEngine.PlaceStamp(fenceEastEdge, startX+levelDetails.SizeXNormal-9, startY+39)
mapEngine.PlaceStamp(fenceNorthWestStamp, startX, startY-15)
mapEngine.PlaceStamp(fenceSouthWestStamp, startX, startY+levelDetails.SizeYNormal-12)
mapEngine.PlaceStamp(fenceNorthEastStamp, startX+levelDetails.SizeXNormal-9, startY-15)
g.engine.PlaceStamp(fenceEastEdge, startX+levelDetails.SizeXNormal-9, startY+39)
g.engine.PlaceStamp(fenceNorthWestStamp, startX, startY-15)
g.engine.PlaceStamp(fenceSouthWestStamp, startX, startY+levelDetails.SizeYNormal-12)
g.engine.PlaceStamp(fenceNorthEastStamp, startX+levelDetails.SizeXNormal-9, startY-15)
areaRect := d2geom.Rectangle{
Left: startX + 9,
@ -238,13 +233,13 @@ func generateWilderness1TownWest(mapEngine *d2mapengine.MapEngine, startX, start
Width: levelDetails.SizeXNormal - 9,
Height: levelDetails.SizeYNormal - 2,
}
generateWilderness1Contents(mapEngine, areaRect)
g.generateWilderness1Contents(areaRect)
}
func generateWilderness1Contents(mapEngine *d2mapengine.MapEngine, rect d2geom.Rectangle) {
levelDetails := d2datadict.GetLevelDetails(2)
func (g *MapGenerator) generateWilderness1Contents(rect d2geom.Rectangle) {
levelDetails := g.asset.Records.GetLevelDetails(2)
denOfEvil := loadPreset(mapEngine, d2wilderness.DenOfEvilEntrance, 0)
denOfEvil := g.loadPreset(d2wilderness.DenOfEvilEntrance, 0)
denOfEvilLoc := d2geom.Point{
X: rect.Left + (rect.Width / 2) + rand.Intn(10),
Y: rect.Top + (rect.Height / 2) + rand.Intn(10),
@ -253,36 +248,36 @@ func generateWilderness1Contents(mapEngine *d2mapengine.MapEngine, rect d2geom.R
// Fill in the grass
for y := 0; y < rect.Height; y++ {
for x := 0; x < rect.Width; x++ {
tile := mapEngine.Tile(rect.Left+x, rect.Top+y)
tile := g.engine.Tile(rect.Left+x, rect.Top+y)
tile.RegionType = d2enum.RegionIdType(levelDetails.LevelType)
tile.Components.Floors = []d2ds1.FloorShadowRecord{{Prop1: 1, Style: 0, Sequence: 0}} // wildernessGrass
tile.PrepareTile(x, y, mapEngine)
tile.PrepareTile(x, y, g.engine)
}
}
stuff := []*d2mapstamp.Stamp{
loadPreset(mapEngine, d2wilderness.StoneFill1, 0),
loadPreset(mapEngine, d2wilderness.StoneFill1, 1),
loadPreset(mapEngine, d2wilderness.StoneFill1, 2),
loadPreset(mapEngine, d2wilderness.StoneFill2, 0),
loadPreset(mapEngine, d2wilderness.StoneFill2, 1),
loadPreset(mapEngine, d2wilderness.StoneFill2, 2),
loadPreset(mapEngine, d2wilderness.Cottages1, 0),
loadPreset(mapEngine, d2wilderness.Cottages1, 1),
loadPreset(mapEngine, d2wilderness.Cottages1, 2),
loadPreset(mapEngine, d2wilderness.Cottages1, 3),
loadPreset(mapEngine, d2wilderness.Cottages1, 4),
loadPreset(mapEngine, d2wilderness.Cottages1, 5),
loadPreset(mapEngine, d2wilderness.FallenCamp1, 0),
loadPreset(mapEngine, d2wilderness.FallenCamp1, 1),
loadPreset(mapEngine, d2wilderness.FallenCamp1, 2),
loadPreset(mapEngine, d2wilderness.FallenCamp1, 3),
loadPreset(mapEngine, d2wilderness.Pond, 0),
loadPreset(mapEngine, d2wilderness.SwampFill1, 0),
loadPreset(mapEngine, d2wilderness.SwampFill2, 0),
g.loadPreset(d2wilderness.StoneFill1, 0),
g.loadPreset(d2wilderness.StoneFill1, 1),
g.loadPreset(d2wilderness.StoneFill1, 2),
g.loadPreset(d2wilderness.StoneFill2, 0),
g.loadPreset(d2wilderness.StoneFill2, 1),
g.loadPreset(d2wilderness.StoneFill2, 2),
g.loadPreset(d2wilderness.Cottages1, 0),
g.loadPreset(d2wilderness.Cottages1, 1),
g.loadPreset(d2wilderness.Cottages1, 2),
g.loadPreset(d2wilderness.Cottages1, 3),
g.loadPreset(d2wilderness.Cottages1, 4),
g.loadPreset(d2wilderness.Cottages1, 5),
g.loadPreset(d2wilderness.FallenCamp1, 0),
g.loadPreset(d2wilderness.FallenCamp1, 1),
g.loadPreset(d2wilderness.FallenCamp1, 2),
g.loadPreset(d2wilderness.FallenCamp1, 3),
g.loadPreset(d2wilderness.Pond, 0),
g.loadPreset(d2wilderness.SwampFill1, 0),
g.loadPreset(d2wilderness.SwampFill2, 0),
}
mapEngine.PlaceStamp(denOfEvil, denOfEvilLoc.X, denOfEvilLoc.Y)
g.engine.PlaceStamp(denOfEvil, denOfEvilLoc.X, denOfEvilLoc.Y)
numPlaced := 0
for numPlaced < 25 {
@ -295,34 +290,9 @@ func generateWilderness1Contents(mapEngine *d2mapengine.MapEngine, rect d2geom.R
Height: stamp.Size().Height,
}
if areaEmpty(mapEngine, stampRect) {
mapEngine.PlaceStamp(stamp, stampRect.Left, stampRect.Top)
if areaEmpty(g.engine, stampRect) {
g.engine.PlaceStamp(stamp, stampRect.Left, stampRect.Top)
numPlaced++
}
}
}
func areaEmpty(mapEngine *d2mapengine.MapEngine, rect d2geom.Rectangle) bool {
mapHeight := mapEngine.Size().Height
mapWidth := mapEngine.Size().Width
if rect.Top < 0 || rect.Left < 0 || rect.Bottom() >= mapHeight || rect.Right() >= mapWidth {
return false
}
for y := rect.Top; y <= rect.Bottom(); y++ {
for x := rect.Left; x <= rect.Right(); x++ {
if len(mapEngine.Tile(x, y).Components.Floors) == 0 {
continue
}
floor := mapEngine.Tile(x, y).Components.Floors[0]
if floor.Style != 0 || floor.Sequence != 0 || floor.Prop1 != 1 {
return false
}
}
}
return true
}

View File

@ -0,0 +1,59 @@
package d2mapgen
import (
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2geom"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapengine"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapstamp"
)
// NewMapGenerator creates a map generator instance
func NewMapGenerator(a *d2asset.AssetManager, e *d2mapengine.MapEngine) (*MapGenerator, error) {
generator := &MapGenerator{
asset: a,
engine: e,
}
return generator, nil
}
// MapGenerator generates maps for the map engine
type MapGenerator struct {
asset *d2asset.AssetManager
engine *d2mapengine.MapEngine
}
func (g *MapGenerator) loadPreset(id, index int) *d2mapstamp.Stamp {
for _, file := range g.asset.Records.LevelPreset(id).Files {
g.engine.AddDS1(file)
}
return g.engine.LoadStamp(d2enum.RegionAct1Wilderness, id, index)
}
func areaEmpty(mapEngine *d2mapengine.MapEngine, rect d2geom.Rectangle) bool {
mapHeight := mapEngine.Size().Height
mapWidth := mapEngine.Size().Width
if rect.Top < 0 || rect.Left < 0 || rect.Bottom() >= mapHeight || rect.Right() >= mapWidth {
return false
}
for y := rect.Top; y <= rect.Bottom(); y++ {
for x := rect.Left; x <= rect.Right(); x++ {
if len(mapEngine.Tile(x, y).Components.Floors) == 0 {
continue
}
floor := mapEngine.Tile(x, y).Components.Floors[0]
if floor.Style != 0 || floor.Sequence != 0 || floor.Prop1 != 1 {
return false
}
}
}
return true
}

View File

@ -6,7 +6,6 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2ds1"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2dt1"
@ -25,10 +24,11 @@ type StampFactory struct {
// LoadStamp loads the Stamp data from file.
func (f *StampFactory) LoadStamp(levelType d2enum.RegionIdType, levelPreset, fileIndex int) *Stamp {
stamp := &Stamp{
factory: f,
entity: f.entity,
regionID: levelType,
levelType: d2datadict.LevelTypes[levelType],
levelPreset: d2datadict.LevelPresets[levelPreset],
levelType: *f.asset.Records.Level.Types[levelType],
levelPreset: f.asset.Records.Level.Presets[levelPreset],
}
for _, levelTypeDt1 := range &stamp.levelType.Files {

View File

@ -11,6 +11,7 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2path"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
)
const (
@ -19,13 +20,14 @@ const (
// Stamp represents a pre-fabricated map stamp that can be placed on a map.
type Stamp struct {
factory *StampFactory
entity *d2mapentity.MapEntityFactory
regionPath string // The file path of the region
regionID d2enum.RegionIdType
levelType d2datadict.LevelTypeRecord // The level type id for this stamp
levelPreset d2datadict.LevelPresetRecord // The level preset id for this stamp
tiles []d2dt1.Tile // The tiles contained on this stamp
ds1 *d2ds1.DS1 // The backing DS1 file for this stamp
levelType d2records.LevelTypeRecord // The level type id for this stamp
levelPreset d2records.LevelPresetRecord // The level preset id for this stamp
tiles []d2dt1.Tile // The tiles contained on this stamp
ds1 *d2ds1.DS1 // The backing DS1 file for this stamp
}
// Size returns the size of the stamp in tiles.
@ -34,12 +36,12 @@ func (mr *Stamp) Size() d2geom.Size {
}
// LevelPreset returns the level preset ID.
func (mr *Stamp) LevelPreset() d2datadict.LevelPresetRecord {
func (mr *Stamp) LevelPreset() d2records.LevelPresetRecord {
return mr.levelPreset
}
// LevelType returns the level type ID.
func (mr *Stamp) LevelType() d2datadict.LevelTypeRecord {
func (mr *Stamp) LevelType() d2records.LevelTypeRecord {
return mr.levelType
}
@ -76,7 +78,8 @@ func (mr *Stamp) Entities(tileOffsetX, tileOffsetY int) []d2interface.MapEntity
for _, object := range mr.ds1.Objects {
if object.Type == int(d2enum.ObjectTypeCharacter) {
monstat := d2datadict.MonStats[d2datadict.MonPresets[mr.ds1.Act][object.ID]]
monPreset := mr.factory.asset.Records.Monster.Presets[mr.ds1.Act][object.ID]
monstat := mr.factory.asset.Records.Monster.Stats[monPreset]
// If monstat is nil here it is a place_ type object, idk how to handle those yet.
// (See monpreset and monplace txts for reference)
if monstat != nil {

View File

@ -2,6 +2,7 @@ package d2records
import (
"log"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
@ -10,6 +11,7 @@ import (
func missilesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
records := make(Missiles)
r.missilesByName = make(missilesByName)
for d.Next() {
record := &MissileRecord{
@ -295,6 +297,7 @@ func missilesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
}
records[record.Id] = record
r.missilesByName[sanitizeMissilesKey(record.Name)] = record
}
if d.Err != nil {
@ -307,3 +310,7 @@ func missilesLoader(r *RecordManager, d *d2txt.DataDictionary) error {
return nil
}
func sanitizeMissilesKey(missileName string) string {
return strings.ToLower(strings.ReplaceAll(missileName, " ", ""))
}

View File

@ -5,6 +5,8 @@ import "github.com/OpenDiablo2/OpenDiablo2/d2common/d2calculation"
// Missiles stores all of the MissileRecords
type Missiles map[int]*MissileRecord
type missilesByName map[string]*MissileRecord
// MissileCalcParam is a calculation parameter for a missile
type MissileCalcParam struct {
Param int

View File

@ -91,6 +91,7 @@ type RecordManager struct {
Warp LevelWarps
}
Missiles
missilesByName
Monster struct {
AI MonsterAI
Equipment MonsterEquipment
@ -274,6 +275,17 @@ func (r *RecordManager) GetExperienceBreakpoint(heroType d2enum.Hero, level int)
return r.Character.Experience[level].HeroBreakpoints[heroType]
}
// GetLevelDetails gets a LevelDetailsRecord by the record Id
func (r *RecordManager) GetLevelDetails(id int) *LevelDetailsRecord {
for i := 0; i < len(r.Level.Details); i++ {
if r.Level.Details[i].ID == id {
return r.Level.Details[i]
}
}
return nil
}
// LevelPreset looks up a LevelPresetRecord by ID
func (r *RecordManager) LevelPreset(id int) LevelPresetRecord {
for i := 0; i < len(r.Level.Presets); i++ {
@ -371,3 +383,19 @@ func (r *RecordManager) SelectSoundByIndex(index int) *SoundDetailsRecord {
return nil
}
// GetSkillByName returns the skill record for the given Skill name.
func (r *RecordManager) GetSkillByName(skillName string) *SkillRecord {
for idx := range r.Skill.Details {
if r.Skill.Details[idx].Skill == skillName {
return r.Skill.Details[idx]
}
}
return nil
}
// GetMissileByName allows lookup of a MissileRecord by a given name. The name will be lowercased and stripped of whitespaces.
func (r *RecordManager) GetMissileByName(missileName string) *MissileRecord {
return r.missilesByName[sanitizeMissilesKey(missileName)]
}

View File

@ -23,7 +23,7 @@ func skillDescriptionLoader(r *RecordManager, d *d2txt.DataDictionary) error {
d.String("SkillColumn"),
d.String("ListRow"),
d.String("ListPool"),
d.String("IconCel"),
d.Number("IconCel"),
d.String("str name"),
d.String("str short"),
d.String("str long"),

View File

@ -14,7 +14,7 @@ type SkillDescriptionRecord struct {
SkillColumn string // SkillColumn
ListRow string // ListRow
ListPool string // ListPool
IconCel string // IconCel
IconCel int // IconCel
NameKey string // str name
ShortKey string // str short
LongKey string // str long

View File

@ -116,6 +116,10 @@ func uniqueItemsLoader(r *RecordManager, d *d2txt.DataDictionary) error {
},
}
if record.Name == "" {
continue
}
records[record.Name] = record
}

View File

@ -1,47 +0,0 @@
package diablo2stats
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
)
// NewStat creates a stat instance with the given record and values
func NewStat(key string, values ...float64) d2stats.Stat {
record := d2datadict.ItemStatCosts[key]
if record == nil {
return nil
}
stat := &diablo2Stat{
record: record,
}
stat.init(values...) // init stat values, value types, and value combination rules
return stat
}
// NewStatList creates a stat list
func NewStatList(stats ...d2stats.Stat) d2stats.StatList {
return &Diablo2StatList{stats}
}
// NewValue creates a stat value of the given type
func NewValue(t d2stats.StatNumberType, c d2stats.ValueCombineType) d2stats.StatValue {
sv := &Diablo2StatValue{
numberType: t,
combineType: c,
}
switch t {
case d2stats.StatValueFloat:
sv.stringerFn = stringerUnsignedFloat
case d2stats.StatValueInt:
sv.stringerFn = stringerUnsignedInt
default:
sv.stringerFn = stringerEmpty
}
return sv
}

View File

@ -4,7 +4,8 @@ import (
"fmt"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2tbl"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
)
@ -43,8 +44,9 @@ const (
// diablo2Stat is an implementation of an OpenDiablo2 Stat, with a set of values.
// It is pretty tightly coupled to the data files for d2
type diablo2Stat struct {
record *d2datadict.ItemStatCostRecord
values []d2stats.StatValue
factory *StatFactory
record *d2records.ItemStatCostRecord
values []d2stats.StatValue
}
// depending on the stat record, sets up the proper number of values,
@ -62,113 +64,120 @@ func (s *diablo2Stat) init(numbers ...float64) { //nolint:funlen doesn't make se
// 0-value descfnID field but need to store values
s.values = make([]d2stats.StatValue, len(numbers))
for idx := range s.values {
s.values[idx] = NewValue(intVal, sum).SetStringer(stringerIntSigned)
s.values[idx] = s.factory.NewValue(intVal, sum).SetStringer(s.factory.stringerIntSigned)
}
case 1:
// +31 to Strength
// Replenish Life +20 || Drain Life -8
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntSigned)
s.values[0] = s.factory.NewValue(intVal, sum).SetStringer(s.factory.stringerIntSigned)
case 2:
// +16% Increased Chance of Blocking
// Lightning Absorb +10%
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntPercentageSigned)
s.values[0] = s.factory.NewValue(intVal,
sum).SetStringer(s.factory.stringerIntPercentageSigned)
case 3:
// Damage Reduced by 25
// Slain Monsters Rest in Peace
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum)
s.values[0] = s.factory.NewValue(intVal, sum)
case 4:
// Poison Resist +25%
// +25% Faster Run/Walk
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntPercentageSigned)
s.values[0] = s.factory.NewValue(intVal,
sum).SetStringer(s.factory.stringerIntPercentageSigned)
case 5:
// Hit Causes Monster to Flee 25%
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum)
s.values[0].SetStringer(stringerIntPercentageUnsigned)
s.values[0] = s.factory.NewValue(intVal, sum)
s.values[0].SetStringer(s.factory.stringerIntPercentageUnsigned)
case 6:
// +25 to Life (Based on Character Level)
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntSigned)
s.values[0] = s.factory.NewValue(intVal, sum).SetStringer(s.factory.stringerIntSigned)
case 7:
// Lightning Resist +25% (Based on Character Level)
// +25% Better Chance of Getting Magic Items (Based on Character Level)
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntPercentageSigned)
s.values[0] = s.factory.NewValue(intVal,
sum).SetStringer(s.factory.stringerIntPercentageSigned)
case 8:
// +25% Enhanced Defense (Based on Character Level)
// Heal Stamina Plus +25% (Based on Character Level)
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntPercentageSigned)
s.values[0] = s.factory.NewValue(intVal,
sum).SetStringer(s.factory.stringerIntPercentageSigned)
case 9:
// Attacker Takes Damage of 25 (Based on Character Level)
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum)
s.values[0] = s.factory.NewValue(intVal, sum)
case 11:
// Repairs 2 durability per second
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum)
s.values[0] = s.factory.NewValue(intVal, sum)
case 12:
// Hit Blinds Target +5
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntSigned)
s.values[0] = s.factory.NewValue(intVal, sum).SetStringer(s.factory.stringerIntSigned)
case 13:
// +5 to Paladin Skill Levels
s.values = make([]d2stats.StatValue, twoValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntSigned)
s.values[1] = NewValue(intVal, sum).SetStringer(stringerClassAllSkills)
s.values[0] = s.factory.NewValue(intVal, sum).SetStringer(s.factory.stringerIntSigned)
s.values[1] = s.factory.NewValue(intVal, sum).SetStringer(s.factory.stringerClassAllSkills)
case 14:
// +5 to Combat Skills (Paladin Only)
s.values = make([]d2stats.StatValue, threeValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntSigned)
s.values[1] = NewValue(intVal, sum).SetStringer(stringerClassOnly)
s.values[2] = NewValue(intVal, static)
s.values[0] = s.factory.NewValue(intVal, sum).SetStringer(s.factory.stringerIntSigned)
s.values[1] = s.factory.NewValue(intVal, sum).SetStringer(s.factory.stringerClassOnly)
s.values[2] = s.factory.NewValue(intVal, static)
case 15:
// 5% Chance to cast level 7 Frozen Orb on attack
s.values = make([]d2stats.StatValue, threeValue)
s.values[0] = NewValue(intVal, sum)
s.values[1] = NewValue(intVal, static)
s.values[2] = NewValue(intVal, static).SetStringer(stringerSkillName)
s.values[0] = s.factory.NewValue(intVal, sum)
s.values[1] = s.factory.NewValue(intVal, static)
s.values[2] = s.factory.NewValue(intVal, static).SetStringer(s.factory.stringerSkillName)
case 16:
// Level 3 Warmth Aura When Equipped
s.values = make([]d2stats.StatValue, twoValue)
s.values[0] = NewValue(intVal, sum)
s.values[1] = NewValue(intVal, static).SetStringer(stringerSkillName)
s.values[0] = s.factory.NewValue(intVal, sum)
s.values[1] = s.factory.NewValue(intVal, static).SetStringer(s.factory.stringerSkillName)
case 20:
// -25% Target Defense
s.values = make([]d2stats.StatValue, oneValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntPercentageSigned)
s.values[0] = s.factory.NewValue(intVal,
sum).SetStringer(s.factory.stringerIntPercentageSigned)
case 22:
// 25% to Attack Rating versus Specter
s.values = make([]d2stats.StatValue, twoValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntPercentageUnsigned)
s.values[1] = NewValue(intVal, static).SetStringer(stringerMonsterName)
s.values[0] = s.factory.NewValue(intVal,
sum).SetStringer(s.factory.stringerIntPercentageUnsigned)
s.values[1] = s.factory.NewValue(intVal, static).SetStringer(s.factory.stringerMonsterName)
case 23:
// 25% Reanimate as: Specter
s.values = make([]d2stats.StatValue, twoValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntPercentageUnsigned)
s.values[1] = NewValue(intVal, static).SetStringer(stringerMonsterName)
s.values[0] = s.factory.NewValue(intVal,
sum).SetStringer(s.factory.stringerIntPercentageUnsigned)
s.values[1] = s.factory.NewValue(intVal, static).SetStringer(s.factory.stringerMonsterName)
case 24:
// Level 25 Frozen Orb (19/20 Charges)
s.values = make([]d2stats.StatValue, fourValue)
s.values[0] = NewValue(intVal, static)
s.values[1] = NewValue(intVal, static).SetStringer(stringerSkillName)
s.values[2] = NewValue(intVal, static)
s.values[3] = NewValue(intVal, static)
s.values[0] = s.factory.NewValue(intVal, static)
s.values[1] = s.factory.NewValue(intVal, static).SetStringer(s.factory.stringerSkillName)
s.values[2] = s.factory.NewValue(intVal, static)
s.values[3] = s.factory.NewValue(intVal, static)
case 27:
// +25 to Frozen Orb (Paladin Only)
s.values = make([]d2stats.StatValue, threeValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntSigned)
s.values[1] = NewValue(intVal, static).SetStringer(stringerSkillName)
s.values[2] = NewValue(intVal, static).SetStringer(stringerClassOnly)
s.values[0] = s.factory.NewValue(intVal, sum).SetStringer(s.factory.stringerIntSigned)
s.values[1] = s.factory.NewValue(intVal, static).SetStringer(s.factory.stringerSkillName)
s.values[2] = s.factory.NewValue(intVal, static).SetStringer(s.factory.stringerClassOnly)
case 28:
// +25 to Frozen Orb
s.values = make([]d2stats.StatValue, twoValue)
s.values[0] = NewValue(intVal, sum).SetStringer(stringerIntSigned)
s.values[1] = NewValue(intVal, static).SetStringer(stringerSkillName)
s.values[0] = s.factory.NewValue(intVal, sum).SetStringer(s.factory.stringerIntSigned)
s.values[1] = s.factory.NewValue(intVal, static).SetStringer(s.factory.stringerSkillName)
default:
return
}
@ -341,7 +350,7 @@ func (s *diablo2Stat) String() string { //nolint:gocyclo switch statement is not
for idx := range s.values {
if s.values[idx].Stringer() == nil {
s.values[idx].SetStringer(stringerUnsignedInt)
s.values[idx].SetStringer(s.factory.stringerUnsignedInt)
}
}
@ -507,9 +516,9 @@ func (s *diablo2Stat) descFn13() string {
func (s *diablo2Stat) descFn14() string {
// strings come out like `+5 to Combat Skills (Paladin Only)`
numSkills, hero, skillTab := s.values[0], s.values[1], s.values[2]
heroMap := getHeroMap()
heroMap := s.factory.getHeroMap()
heroIndex := hero.Int()
classRecord := d2datadict.CharStats[heroMap[heroIndex]]
classRecord := s.factory.asset.Records.Character.Stats[heroMap[heroIndex]]
// diablo 2 is hardcoded to have only 3 skill tabs
skillTabIndex := skillTab.Int()

View File

@ -0,0 +1,136 @@
package diablo2stats
import (
"fmt"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2tbl"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
)
func NewStatFactory(asset *d2asset.AssetManager) (*StatFactory, error) {
factory := &StatFactory{asset: asset}
return factory, nil
}
// StatFactory is responsible for creating stats
type StatFactory struct {
asset *d2asset.AssetManager
}
// NewStat creates a stat instance with the given record and values
func (f *StatFactory) NewStat(key string, values ...float64) d2stats.Stat {
record := f.asset.Records.Item.Stats[key]
if record == nil {
return nil
}
stat := &diablo2Stat{
factory: f,
record: record,
}
stat.init(values...) // init stat values, value types, and value combination rules
return stat
}
// NewStatList creates a stat list
func (f *StatFactory) NewStatList(stats ...d2stats.Stat) d2stats.StatList {
return &Diablo2StatList{stats}
}
// NewValue creates a stat value of the given type
func (f *StatFactory) NewValue(t d2stats.StatNumberType, c d2stats.ValueCombineType) d2stats.StatValue {
sv := &Diablo2StatValue{
numberType: t,
combineType: c,
}
switch t {
case d2stats.StatValueFloat:
sv.stringerFn = f.stringerUnsignedFloat
case d2stats.StatValueInt:
sv.stringerFn = f.stringerUnsignedInt
default:
sv.stringerFn = f.stringerEmpty
}
return sv
}
const (
monsterNotFound = "{Monster not found!}"
)
func (f *StatFactory) getHeroMap() []d2enum.Hero {
return []d2enum.Hero{
d2enum.HeroAmazon,
d2enum.HeroSorceress,
d2enum.HeroNecromancer,
d2enum.HeroPaladin,
d2enum.HeroBarbarian,
d2enum.HeroDruid,
d2enum.HeroAssassin,
}
}
func (f *StatFactory) stringerUnsignedInt(sv d2stats.StatValue) string {
return fmt.Sprintf("%d", sv.Int())
}
func (f *StatFactory) stringerUnsignedFloat(sv d2stats.StatValue) string {
return fmt.Sprintf("%.2f", sv.Float())
}
func (f *StatFactory) stringerEmpty(_ d2stats.StatValue) string {
return ""
}
func (f *StatFactory) stringerIntSigned(sv d2stats.StatValue) string {
return fmt.Sprintf("%+d", sv.Int())
}
func (f *StatFactory) stringerIntPercentageSigned(sv d2stats.StatValue) string {
return fmt.Sprintf("%+d%%", sv.Int())
}
func (f *StatFactory) stringerIntPercentageUnsigned(sv d2stats.StatValue) string {
return fmt.Sprintf("%d%%", sv.Int())
}
func (f *StatFactory) stringerClassAllSkills(sv d2stats.StatValue) string {
heroIndex := sv.Int()
heroMap := f.getHeroMap()
classRecord := f.asset.Records.Character.Stats[heroMap[heroIndex]]
return d2tbl.TranslateString(classRecord.SkillStrAll)
}
func (f *StatFactory) stringerClassOnly(sv d2stats.StatValue) string {
heroMap := f.getHeroMap()
heroIndex := sv.Int()
classRecord := f.asset.Records.Character.Stats[heroMap[heroIndex]]
classOnlyKey := classRecord.SkillStrClassOnly
return d2tbl.TranslateString(classOnlyKey)
}
func (f *StatFactory) stringerSkillName(sv d2stats.StatValue) string {
skillRecord := f.asset.Records.Skill.Details[sv.Int()]
return skillRecord.Skill
}
func (f *StatFactory) stringerMonsterName(sv d2stats.StatValue) string {
for key := range f.asset.Records.Monster.Stats {
if f.asset.Records.Monster.Stats[key].ID == sv.Int() {
return f.asset.Records.Monster.Stats[key].NameString
}
}
return monsterNotFound
}

View File

@ -4,7 +4,10 @@ import (
"fmt"
"testing"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
)
@ -13,254 +16,261 @@ const (
errFmt string = "%v:\n\tDescFnID: %v\n\tKey: %v\n\tVal: %+v\n\texpected: %v\n\tgot: %v\n\n"
)
//nolint:funlen // this just gets mock data ready for the tests
func TestStat_InitMockData(t *testing.T) {
var itemStatCosts = map[string]*d2datadict.ItemStatCostRecord{
"strength": {
Name: "strength",
DescFnID: 1,
DescVal: int(descValPrefix),
DescStrPos: "to Strength",
DescStrNeg: "to Strength",
},
"dexterity": {
Name: "dexterity",
DescFnID: 1,
DescVal: int(descValPrefix),
DescStrPos: "to Dexterity",
DescStrNeg: "to Dexterity",
},
"vitality": {
Name: "vitality",
DescFnID: 1,
DescVal: int(descValPrefix),
DescStrPos: "to Vitality",
DescStrNeg: "to Vitality",
},
"energy": {
Name: "energy",
DescFnID: 1,
DescVal: int(descValPrefix),
DescStrPos: "to Energy",
DescStrNeg: "to Energy",
},
"hpregen": {
Name: "hpregen",
DescFnID: 1,
DescVal: int(descValPostfix),
DescStrPos: "Replenish Life",
DescStrNeg: "Drain Life",
},
"toblock": {
Name: "toblock",
DescFnID: 2,
DescVal: int(descValPrefix),
DescStrPos: "Increased Chance of Blocking",
DescStrNeg: "Increased Chance of Blocking",
},
"item_absorblight_percent": {
Name: "item_absorblight_percent",
DescFnID: 2,
DescVal: int(descValPostfix),
DescStrPos: "Lightning Absorb",
DescStrNeg: "Lightning Absorb",
},
"item_restinpeace": {
Name: "item_restinpeace",
DescFnID: 3,
DescVal: int(descValHide),
DescStrPos: "Slain Monsters Rest in Peace",
DescStrNeg: "Slain Monsters Rest in Peace",
},
"normal_damage_reduction": {
Name: "normal_damage_reduction",
DescFnID: 3,
DescVal: int(descValPostfix),
DescStrPos: "Damage Reduced by",
DescStrNeg: "Damage Reduced by",
},
"poisonresist": {
Name: "poisonresist",
DescFnID: 4,
DescVal: int(descValPostfix),
DescStrPos: "Poison Resist",
DescStrNeg: "Poison Resist",
},
"item_fastermovevelocity": {
Name: "item_fastermovevelocity",
DescFnID: 4,
DescVal: int(descValPrefix),
DescStrPos: "Faster Run/Walk",
DescStrNeg: "Faster Run/Walk",
},
"item_howl": {
Name: "item_howl",
DescFnID: 5,
DescVal: int(descValPostfix),
DescStrPos: "Hit Causes Monster to Flee",
DescStrNeg: "Hit Causes Monster to Flee",
},
"item_hp_perlevel": {
Name: "item_hp_perlevel",
DescFnID: 6,
DescVal: int(descValPrefix),
DescStrPos: "to Life",
DescStrNeg: "to Life",
DescStr2: "(Based on Character Level)",
},
"item_resist_ltng_perlevel": {
Name: "item_resist_ltng_perlevel",
DescFnID: 7,
DescVal: int(descValPostfix),
DescStrPos: "Lightning Resist",
DescStrNeg: "Lightning Resist",
DescStr2: "(Based on Character Level)",
},
"item_find_magic_perlevel": {
Name: "item_find_magic_perlevel",
DescFnID: 7,
DescVal: int(descValPrefix),
DescStrPos: "Better Chance of Getting Magic Items",
DescStrNeg: "Better Chance of Getting Magic Items",
DescStr2: "(Based on Character Level)",
},
"item_armorpercent_perlevel": {
Name: "item_armorpercent_perlevel",
DescFnID: 8,
DescVal: int(descValPrefix),
DescStrPos: "Enhanced Defense",
DescStrNeg: "Enhanced Defense",
DescStr2: "(Based on Character Level)",
},
"item_regenstamina_perlevel": {
Name: "item_regenstamina_perlevel",
DescFnID: 8,
DescVal: int(descValPostfix),
DescStrPos: "Heal Stamina Plus",
DescStrNeg: "Heal Stamina Plus",
DescStr2: "(Based on Character Level)",
},
"item_thorns_perlevel": {
Name: "item_thorns_perlevel",
DescFnID: 9,
DescVal: int(descValPostfix),
DescStrPos: "Attacker Takes Damage of",
DescStrNeg: "Attacker Takes Damage of",
DescStr2: "(Based on Character Level)",
},
"item_replenish_durability": {
Name: "item_replenish_durability",
DescFnID: 11,
DescVal: int(descValPrefix),
DescStrPos: "Repairs %v durability per second",
DescStrNeg: "Repairs %v durability per second",
DescStr2: "",
},
"item_stupidity": {
Name: "item_stupidity",
DescFnID: 12,
DescVal: int(descValPostfix),
DescStrPos: "Hit Blinds Target",
DescStrNeg: "Hit Blinds Target",
},
"item_addclassskills": {
Name: "item_addclassskills",
DescFnID: 13,
DescVal: int(descValPrefix),
},
"item_addskill_tab": {
Name: "item_addskill_tab",
DescFnID: 14,
DescVal: int(descValPrefix),
},
"item_skillonattack": {
Name: "item_skillonattack",
DescFnID: 15,
DescVal: int(descValPrefix),
DescStrPos: "%d%% Chance to cast level %d %s on attack",
DescStrNeg: "%d%% Chance to cast level %d %s on attack",
},
"item_aura": {
Name: "item_aura",
DescFnID: 16,
DescVal: int(descValPrefix),
DescStrPos: "Level %d %s Aura When Equipped",
DescStrNeg: "Level %d %s Aura When Equipped",
},
"item_fractionaltargetac": {
Name: "item_fractionaltargetac",
DescFnID: 20,
DescVal: int(descValPrefix),
DescStrPos: "Target Defense",
DescStrNeg: "Target Defense",
},
"attack_vs_montype": {
Name: "item_fractionaltargetac",
DescFnID: 22,
DescVal: int(descValPrefix),
DescStrPos: "to Attack Rating versus",
DescStrNeg: "to Attack Rating versus",
},
"item_reanimate": {
Name: "item_reanimate",
DescFnID: 23,
DescVal: int(descValPostfix),
DescStrPos: "Reanimate as:",
DescStrNeg: "Reanimate as:",
},
"item_charged_skill": {
Name: "item_charged_skill",
DescFnID: 24,
DescVal: int(descValPostfix),
DescStrPos: "(%d/%d Charges)",
DescStrNeg: "(%d/%d Charges)",
},
"item_singleskill": {
Name: "item_singleskill",
DescFnID: 27,
DescVal: int(descValPostfix),
DescStrPos: "(%d/%d Charges)",
DescStrNeg: "(%d/%d Charges)",
},
"item_nonclassskill": {
Name: "item_nonclassskill",
DescFnID: 28,
DescVal: int(descValPostfix),
DescStrPos: "(%d/%d Charges)",
DescStrNeg: "(%d/%d Charges)",
},
}
var itemStatCosts = map[string]*d2records.ItemStatCostRecord{
"strength": {
Name: "strength",
DescFnID: 1,
DescVal: int(descValPrefix),
DescStrPos: "to Strength",
DescStrNeg: "to Strength",
},
"dexterity": {
Name: "dexterity",
DescFnID: 1,
DescVal: int(descValPrefix),
DescStrPos: "to Dexterity",
DescStrNeg: "to Dexterity",
},
"vitality": {
Name: "vitality",
DescFnID: 1,
DescVal: int(descValPrefix),
DescStrPos: "to Vitality",
DescStrNeg: "to Vitality",
},
"energy": {
Name: "energy",
DescFnID: 1,
DescVal: int(descValPrefix),
DescStrPos: "to Energy",
DescStrNeg: "to Energy",
},
"hpregen": {
Name: "hpregen",
DescFnID: 1,
DescVal: int(descValPostfix),
DescStrPos: "Replenish Life",
DescStrNeg: "Drain Life",
},
"toblock": {
Name: "toblock",
DescFnID: 2,
DescVal: int(descValPrefix),
DescStrPos: "Increased Chance of Blocking",
DescStrNeg: "Increased Chance of Blocking",
},
"item_absorblight_percent": {
Name: "item_absorblight_percent",
DescFnID: 2,
DescVal: int(descValPostfix),
DescStrPos: "Lightning Absorb",
DescStrNeg: "Lightning Absorb",
},
"item_restinpeace": {
Name: "item_restinpeace",
DescFnID: 3,
DescVal: int(descValHide),
DescStrPos: "Slain Monsters Rest in Peace",
DescStrNeg: "Slain Monsters Rest in Peace",
},
"normal_damage_reduction": {
Name: "normal_damage_reduction",
DescFnID: 3,
DescVal: int(descValPostfix),
DescStrPos: "Damage Reduced by",
DescStrNeg: "Damage Reduced by",
},
"poisonresist": {
Name: "poisonresist",
DescFnID: 4,
DescVal: int(descValPostfix),
DescStrPos: "Poison Resist",
DescStrNeg: "Poison Resist",
},
"item_fastermovevelocity": {
Name: "item_fastermovevelocity",
DescFnID: 4,
DescVal: int(descValPrefix),
DescStrPos: "Faster Run/Walk",
DescStrNeg: "Faster Run/Walk",
},
"item_howl": {
Name: "item_howl",
DescFnID: 5,
DescVal: int(descValPostfix),
DescStrPos: "Hit Causes Monster to Flee",
DescStrNeg: "Hit Causes Monster to Flee",
},
"item_hp_perlevel": {
Name: "item_hp_perlevel",
DescFnID: 6,
DescVal: int(descValPrefix),
DescStrPos: "to Life",
DescStrNeg: "to Life",
DescStr2: "(Based on Character Level)",
},
"item_resist_ltng_perlevel": {
Name: "item_resist_ltng_perlevel",
DescFnID: 7,
DescVal: int(descValPostfix),
DescStrPos: "Lightning Resist",
DescStrNeg: "Lightning Resist",
DescStr2: "(Based on Character Level)",
},
"item_find_magic_perlevel": {
Name: "item_find_magic_perlevel",
DescFnID: 7,
DescVal: int(descValPrefix),
DescStrPos: "Better Chance of Getting Magic Items",
DescStrNeg: "Better Chance of Getting Magic Items",
DescStr2: "(Based on Character Level)",
},
"item_armorpercent_perlevel": {
Name: "item_armorpercent_perlevel",
DescFnID: 8,
DescVal: int(descValPrefix),
DescStrPos: "Enhanced Defense",
DescStrNeg: "Enhanced Defense",
DescStr2: "(Based on Character Level)",
},
"item_regenstamina_perlevel": {
Name: "item_regenstamina_perlevel",
DescFnID: 8,
DescVal: int(descValPostfix),
DescStrPos: "Heal Stamina Plus",
DescStrNeg: "Heal Stamina Plus",
DescStr2: "(Based on Character Level)",
},
"item_thorns_perlevel": {
Name: "item_thorns_perlevel",
DescFnID: 9,
DescVal: int(descValPostfix),
DescStrPos: "Attacker Takes Damage of",
DescStrNeg: "Attacker Takes Damage of",
DescStr2: "(Based on Character Level)",
},
"item_replenish_durability": {
Name: "item_replenish_durability",
DescFnID: 11,
DescVal: int(descValPrefix),
DescStrPos: "Repairs %v durability per second",
DescStrNeg: "Repairs %v durability per second",
DescStr2: "",
},
"item_stupidity": {
Name: "item_stupidity",
DescFnID: 12,
DescVal: int(descValPostfix),
DescStrPos: "Hit Blinds Target",
DescStrNeg: "Hit Blinds Target",
},
"item_addclassskills": {
Name: "item_addclassskills",
DescFnID: 13,
DescVal: int(descValPrefix),
},
"item_addskill_tab": {
Name: "item_addskill_tab",
DescFnID: 14,
DescVal: int(descValPrefix),
},
"item_skillonattack": {
Name: "item_skillonattack",
DescFnID: 15,
DescVal: int(descValPrefix),
DescStrPos: "%d%% Chance to cast level %d %s on attack",
DescStrNeg: "%d%% Chance to cast level %d %s on attack",
},
"item_aura": {
Name: "item_aura",
DescFnID: 16,
DescVal: int(descValPrefix),
DescStrPos: "Level %d %s Aura When Equipped",
DescStrNeg: "Level %d %s Aura When Equipped",
},
"item_fractionaltargetac": {
Name: "item_fractionaltargetac",
DescFnID: 20,
DescVal: int(descValPrefix),
DescStrPos: "Target Defense",
DescStrNeg: "Target Defense",
},
"attack_vs_montype": {
Name: "item_fractionaltargetac",
DescFnID: 22,
DescVal: int(descValPrefix),
DescStrPos: "to Attack Rating versus",
DescStrNeg: "to Attack Rating versus",
},
"item_reanimate": {
Name: "item_reanimate",
DescFnID: 23,
DescVal: int(descValPostfix),
DescStrPos: "Reanimate as:",
DescStrNeg: "Reanimate as:",
},
"item_charged_skill": {
Name: "item_charged_skill",
DescFnID: 24,
DescVal: int(descValPostfix),
DescStrPos: "(%d/%d Charges)",
DescStrNeg: "(%d/%d Charges)",
},
"item_singleskill": {
Name: "item_singleskill",
DescFnID: 27,
DescVal: int(descValPostfix),
DescStrPos: "(%d/%d Charges)",
DescStrNeg: "(%d/%d Charges)",
},
"item_nonclassskill": {
Name: "item_nonclassskill",
DescFnID: 28,
DescVal: int(descValPostfix),
DescStrPos: "(%d/%d Charges)",
DescStrNeg: "(%d/%d Charges)",
},
}
var charStats = map[d2enum.Hero]*d2datadict.CharStatsRecord{
d2enum.HeroPaladin: {
Class: d2enum.HeroPaladin,
SkillStrAll: "to Paladin Skill Levels",
SkillStrClassOnly: "(Paladin Only)",
SkillStrTab: [3]string{
"+%d to Combat Skills",
"+%d to Offensive Auras",
"+%d to Defensive Auras",
},
var skillDetails = map[int]*d2records.SkillRecord{
37: {Skill: "Warmth"},
64: {Skill: "Frozen Orb"},
}
var monStats = map[string]*d2records.MonStatsRecord{
"Specter": {NameString: "Specter", ID: 40},
}
var charStats = map[d2enum.Hero]*d2records.CharStatsRecord{
d2enum.HeroPaladin: {
Class: d2enum.HeroPaladin,
SkillStrAll: "to Paladin Skill Levels",
SkillStrClassOnly: "(Paladin Only)",
SkillStrTab: [3]string{
"+%d to Combat Skills",
"+%d to Offensive Auras",
"+%d to Defensive Auras",
},
}
},
}
var testAssetManager2 *d2asset.AssetManager
var skillDetails = map[int]*d2datadict.SkillRecord{
37: {Skill: "Warmth"},
64: {Skill: "Frozen Orb"},
}
var testStatFactory2 *StatFactory
var monStats = map[string]*d2datadict.MonStatsRecord{
"Specter": {NameString: "Specter", ID: 40},
}
func TestSetup_Stat(t *testing.T) {
testAssetManager2 = &d2asset.AssetManager{}
testAssetManager2.Records = &d2records.RecordManager{}
d2datadict.ItemStatCosts = itemStatCosts
d2datadict.CharStats = charStats
d2datadict.SkillDetails = skillDetails
d2datadict.MonStats = monStats
testStatFactory2, _ = NewStatFactory(testAssetManager2)
testAssetManager2.Records.Item.Stats = itemStatCosts
testAssetManager2.Records.Character.Stats = charStats
testAssetManager2.Records.Skill.Details = skillDetails
testAssetManager2.Records.Monster.Stats = monStats
}
func TestStat_Clone(t *testing.T) {
s1 := NewStat("strength", 5)
s1 := testStatFactory2.NewStat("strength", 5)
s2 := s1.Clone()
// make sure the stats are distinct
@ -371,9 +381,9 @@ func TestStat_Descriptions(t *testing.T) {
for idx := range tests {
test := tests[idx]
key := test.recordKey
record := d2datadict.ItemStatCosts[key]
record := itemStatCosts[key]
expect := test.expect
stat := NewStat(key, test.vals...)
stat := testStatFactory2.NewStat(key, test.vals...)
if got := stat.String(); got != expect {
t.Errorf(errFmt, errStr, record.DescFnID, test.recordKey, test.vals, expect, got)
@ -386,8 +396,8 @@ func TestStat_Descriptions(t *testing.T) {
}
func TestDiablo2Stat_Combine(t *testing.T) {
a := NewStat("item_nonclassskill", 25, 64) // "+25 to Frozen Orb"
b := NewStat("item_nonclassskill", 5, 64) // "+5 to Frozen Orb"
a := testStatFactory2.NewStat("item_nonclassskill", 25, 64) // "+25 to Frozen Orb"
b := testStatFactory2.NewStat("item_nonclassskill", 5, 64) // "+5 to Frozen Orb"
c, err := a.Combine(b)
@ -395,7 +405,7 @@ func TestDiablo2Stat_Combine(t *testing.T) {
t.Errorf("stats combination failed\r%s", err)
}
d := NewStat("item_nonclassskill", 5, 37) // "+5 to Warmth"
d := testStatFactory2.NewStat("item_nonclassskill", 5, 37) // "+5 to Warmth"
_, err = c.Combine(d)
if err == nil {

View File

@ -1,83 +1 @@
package diablo2stats
import (
"fmt"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2tbl"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
)
const (
monsterNotFound = "{Monster not found!}"
)
func getHeroMap() []d2enum.Hero {
return []d2enum.Hero{
d2enum.HeroAmazon,
d2enum.HeroSorceress,
d2enum.HeroNecromancer,
d2enum.HeroPaladin,
d2enum.HeroBarbarian,
d2enum.HeroDruid,
d2enum.HeroAssassin,
}
}
func stringerUnsignedInt(sv d2stats.StatValue) string {
return fmt.Sprintf("%d", sv.Int())
}
func stringerUnsignedFloat(sv d2stats.StatValue) string {
return fmt.Sprintf("%.2f", sv.Float())
}
func stringerEmpty(_ d2stats.StatValue) string {
return ""
}
func stringerIntSigned(sv d2stats.StatValue) string {
return fmt.Sprintf("%+d", sv.Int())
}
func stringerIntPercentageSigned(sv d2stats.StatValue) string {
return fmt.Sprintf("%+d%%", sv.Int())
}
func stringerIntPercentageUnsigned(sv d2stats.StatValue) string {
return fmt.Sprintf("%d%%", sv.Int())
}
func stringerClassAllSkills(sv d2stats.StatValue) string {
heroIndex := sv.Int()
heroMap := getHeroMap()
classRecord := d2datadict.CharStats[heroMap[heroIndex]]
return d2tbl.TranslateString(classRecord.SkillStrAll)
}
func stringerClassOnly(sv d2stats.StatValue) string {
heroMap := getHeroMap()
heroIndex := sv.Int()
classRecord := d2datadict.CharStats[heroMap[heroIndex]]
classOnlyKey := classRecord.SkillStrClassOnly
return d2tbl.TranslateString(classOnlyKey)
}
func stringerSkillName(sv d2stats.StatValue) string {
skillRecord := d2datadict.SkillDetails[sv.Int()]
return skillRecord.Skill
}
func stringerMonsterName(sv d2stats.StatValue) string {
for key := range d2datadict.MonStats {
if d2datadict.MonStats[key].ID == sv.Int() {
return d2datadict.MonStats[key].NameString
}
}
return monsterNotFound
}

View File

@ -3,11 +3,30 @@ package diablo2stats
import (
"testing"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
)
var testAssetManager *d2asset.AssetManager
var testStatFactory *StatFactory
func TestSetup_StatList(t *testing.T) {
testAssetManager = &d2asset.AssetManager{}
testAssetManager.Records = &d2records.RecordManager{}
testStatFactory, _ = NewStatFactory(testAssetManager)
testAssetManager.Records.Item.Stats = itemStatCosts
testAssetManager.Records.Character.Stats = charStats
testAssetManager.Records.Skill.Details = skillDetails
testAssetManager.Records.Monster.Stats = monStats
}
func TestDiablo2StatList_Index(t *testing.T) {
strength := NewStat("strength", 10)
strength := testStatFactory.NewStat("strength", 10)
list1 := &Diablo2StatList{stats: []d2stats.Stat{strength}}
if list1.Index(0) != strength {
@ -16,7 +35,7 @@ func TestDiablo2StatList_Index(t *testing.T) {
}
func TestStatList_Clone(t *testing.T) {
strength := NewStat("strength", 10)
strength := testStatFactory.NewStat("strength", 10)
list1 := &Diablo2StatList{}
list1.Push(strength)
@ -38,13 +57,13 @@ func TestStatList_Clone(t *testing.T) {
func TestStatList_Reduce(t *testing.T) {
stats := []d2stats.Stat{
NewStat("strength", 1),
NewStat("strength", 1),
NewStat("strength", 1),
NewStat("strength", 1),
testStatFactory.NewStat("strength", 1),
testStatFactory.NewStat("strength", 1),
testStatFactory.NewStat("strength", 1),
testStatFactory.NewStat("strength", 1),
}
list := NewStatList(stats...)
list := testStatFactory.NewStatList(stats...)
reduction := list.ReduceStats()
if len(reduction.Stats()) != 1 || reduction.Index(0).String() != "+4 to Strength" {
@ -52,13 +71,13 @@ func TestStatList_Reduce(t *testing.T) {
}
stats = []d2stats.Stat{
NewStat("strength", 1),
NewStat("energy", 1),
NewStat("dexterity", 1),
NewStat("vitality", 1),
testStatFactory.NewStat("strength", 1),
testStatFactory.NewStat("energy", 1),
testStatFactory.NewStat("dexterity", 1),
testStatFactory.NewStat("vitality", 1),
}
list = NewStatList(stats...)
list = testStatFactory.NewStatList(stats...)
reduction = list.ReduceStats()
if len(reduction.Stats()) != 4 {
@ -69,10 +88,10 @@ func TestStatList_Reduce(t *testing.T) {
func TestStatList_Append(t *testing.T) {
list1 := &Diablo2StatList{
[]d2stats.Stat{
NewStat("strength", 1),
NewStat("energy", 1),
NewStat("dexterity", 1),
NewStat("vitality", 1),
testStatFactory.NewStat("strength", 1),
testStatFactory.NewStat("energy", 1),
testStatFactory.NewStat("dexterity", 1),
testStatFactory.NewStat("vitality", 1),
},
}
list2 := list1.Clone()

View File

@ -6,16 +6,16 @@ import (
"math"
"os"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2inventory"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
"github.com/OpenDiablo2/OpenDiablo2/d2game/d2player"
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype"
)
@ -23,6 +23,7 @@ import (
type CharacterSelect struct {
asset *d2asset.AssetManager
*d2mapentity.MapEntityFactory
*d2hero.HeroStateFactory
background *d2ui.Sprite
newCharButton *d2ui.Button
convertCharButton *d2ui.Button
@ -40,7 +41,7 @@ type CharacterSelect struct {
characterStatsLabel [8]*d2ui.Label
characterExpLabel [8]*d2ui.Label
characterImage [8]*d2mapentity.Player
gameStates []*d2player.PlayerState
gameStates []*d2hero.HeroState
selectedCharacter int
showDeleteConfirmation bool
connectionType d2clientconnectiontype.ClientConnectionType
@ -64,10 +65,13 @@ func CreateCharacterSelect(
connectionType d2clientconnectiontype.ClientConnectionType,
connectionHost string,
) *CharacterSelect {
playerStateFactory, _ := d2hero.NewHeroStateFactory(asset) // TODO: handle errors
entityFactory, _ := d2mapentity.NewMapEntityFactory(asset)
return &CharacterSelect{
selectedCharacter: -1,
asset: asset,
MapEntityFactory: d2mapentity.NewMapEntityFactory(asset),
MapEntityFactory: entityFactory,
renderer: renderer,
connectionType: connectionType,
connectionHost: connectionHost,
@ -75,6 +79,7 @@ func CreateCharacterSelect(
audioProvider: audioProvider,
navigator: navigator,
uiManager: ui,
HeroStateFactory: playerStateFactory,
}
}
@ -282,7 +287,7 @@ func (v *CharacterSelect) updateCharacterBoxes() {
v.characterExpLabel[i].SetText(d2ui.ColorTokenize(expText, d2ui.ColorTokenGreen))
heroType := v.gameStates[idx].HeroType
equipment := d2inventory.HeroObjects[heroType]
equipment := v.DefaultHeroItems[heroType]
// TODO: Generate or load the object from the actual player data...
v.characterImage[i] = v.NewPlayer("", "", 0, 0, 0,
@ -434,7 +439,11 @@ func (v *CharacterSelect) toggleDeleteCharacterDialog(showDialog bool) {
}
func (v *CharacterSelect) refreshGameStates() {
v.gameStates = d2player.GetAllPlayerStates()
gameStates, err := v.HeroStateFactory.GetAllHeroStates()
if err == nil {
v.gameStates = gameStates
}
v.updateCharacterBoxes()
if len(v.gameStates) > 0 {

View File

@ -10,7 +10,6 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2audio"
@ -93,7 +92,7 @@ func CreateGame(
audioProvider: audioProvider,
renderer: renderer,
terminal: term,
soundEngine: d2audio.NewSoundEngine(audioProvider, term),
soundEngine: d2audio.NewSoundEngine(audioProvider, asset, term),
uiManager: ui,
guiManager: guiManager,
}
@ -142,7 +141,7 @@ func (v *Game) OnLoad(_ d2screen.LoadingState) {
func(name string) {
x := int(v.localPlayer.Position.X())
y := int(v.localPlayer.Position.Y())
monstat := d2datadict.MonStats[name]
monstat := v.asset.Records.Monster.Stats[name]
if monstat == nil {
v.terminal.OutputErrorf("no monstat entry for \"%s\"", name)
return
@ -233,12 +232,13 @@ func (v *Game) Advance(elapsed float64) error {
tile := v.gameClient.MapEngine.TileAt(int(tilePosition.X()), int(tilePosition.Y()))
if tile != nil {
v.soundEnv.SetEnv(d2datadict.LevelDetails[int(tile.RegionType)].SoundEnvironmentID)
levelDetails := v.asset.Records.Level.Details[int(tile.RegionType)]
v.soundEnv.SetEnv(levelDetails.SoundEnvironmentID)
// skip showing zone change text the first time we enter the world
if v.lastRegionType != d2enum.RegionNone && v.lastRegionType != tile.RegionType {
//TODO: Should not be using RegionType as an index - this will return incorrect LevelDetails record for most of the zones.
areaName := d2datadict.LevelDetails[int(tile.RegionType)].LevelDisplayName
areaName := levelDetails.LevelDisplayName
areaChgStr := fmt.Sprintf("Entering The %s", areaName)
v.gameControls.SetZoneChangeText(areaChgStr)
v.gameControls.ShowZoneChangeText()

View File

@ -8,6 +8,8 @@ import (
"os/exec"
"runtime"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2tbl"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
@ -16,7 +18,6 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
"github.com/OpenDiablo2/OpenDiablo2/d2game/d2player"
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype"
"github.com/OpenDiablo2/OpenDiablo2/d2script"
)
@ -118,6 +119,7 @@ type MainMenu struct {
scriptEngine *d2script.ScriptEngine
navigator Navigator
uiManager *d2ui.UIManager
heroState *d2hero.HeroStateFactory
buildInfo BuildInfo
}
@ -131,8 +133,13 @@ func CreateMainMenu(
audioProvider d2interface.AudioProvider,
ui *d2ui.UIManager,
buildInfo BuildInfo,
) *MainMenu {
return &MainMenu{
) (*MainMenu, error) {
heroStateFactory, err := d2hero.NewHeroStateFactory(asset)
if err != nil {
return nil, err
}
mainMenu := &MainMenu{
asset: asset,
screenMode: ScreenModeUnknown,
leftButtonHeld: true,
@ -142,7 +149,10 @@ func CreateMainMenu(
navigator: navigator,
buildInfo: buildInfo,
uiManager: ui,
heroState: heroStateFactory,
}
return mainMenu, nil
}
// OnLoad is called to load the resources for the main menu
@ -320,7 +330,7 @@ func (v *MainMenu) onMapTestClicked() {
}
func (v *MainMenu) onSinglePlayerClicked() {
if d2player.HasGameStates() {
if v.heroState.HasGameStates() {
// Go here only if existing characters are available to select
v.navigator.ToCharacterSelect(d2clientconnectiontype.Local, v.tcpJoinGameEntry.GetText())
} else {

View File

@ -7,6 +7,8 @@ import (
"strings"
"time"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector"
@ -16,7 +18,6 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapgen"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2maprenderer"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen"
"github.com/OpenDiablo2/OpenDiablo2/d2game/d2player"
)
type regionSpec struct {
@ -84,15 +85,17 @@ func getRegions() []regionSpec {
// MapEngineTest represents the MapEngineTest screen
type MapEngineTest struct {
asset *d2asset.AssetManager
gameState *d2player.PlayerState
mapEngine *d2mapengine.MapEngine
mapRenderer *d2maprenderer.MapRenderer
terminal d2interface.Terminal
renderer d2interface.Renderer
inputManager d2interface.InputManager
audioProvider d2interface.AudioProvider
screen *d2screen.ScreenManager
asset *d2asset.AssetManager
playerStateFactory *d2hero.HeroStateFactory
playerState *d2hero.HeroState
mapEngine *d2mapengine.MapEngine
mapGen *d2mapgen.MapGenerator
mapRenderer *d2maprenderer.MapRenderer
terminal d2interface.Terminal
renderer d2interface.Renderer
inputManager d2interface.InputManager
audioProvider d2interface.AudioProvider
screen *d2screen.ScreenManager
lastMouseX, lastMouseY int
selX, selY int
@ -116,23 +119,30 @@ func CreateMapEngineTest(currentRegion,
inputManager d2interface.InputManager,
audioProvider d2interface.AudioProvider,
screen *d2screen.ScreenManager,
) *MapEngineTest {
result := &MapEngineTest{
currentRegion: currentRegion,
levelPreset: levelPreset,
fileIndex: 0,
regionSpec: regionSpec{},
filesCount: 0,
asset: asset,
terminal: term,
renderer: renderer,
inputManager: inputManager,
audioProvider: audioProvider,
screen: screen,
) (*MapEngineTest, error) {
heroStateFactory, err := d2hero.NewHeroStateFactory(asset)
if err != nil {
return nil, err
}
result.gameState = d2player.CreateTestGameState()
return result
result := &MapEngineTest{
currentRegion: currentRegion,
levelPreset: levelPreset,
fileIndex: 0,
regionSpec: regionSpec{},
filesCount: 0,
asset: asset,
terminal: term,
renderer: renderer,
inputManager: inputManager,
audioProvider: audioProvider,
screen: screen,
playerStateFactory: heroStateFactory,
}
result.playerState = heroStateFactory.CreateTestGameState()
return result, nil
}
func (met *MapEngineTest) loadRegionByIndex(n, levelPreset, fileIndex int) {
@ -167,9 +177,12 @@ func (met *MapEngineTest) loadRegionByIndex(n, levelPreset, fileIndex int) {
met.levelPreset = levelPreset
}
mapGen, _ := d2mapgen.NewMapGenerator(met.asset, met.mapEngine)
met.mapGen = mapGen
if n == 0 {
met.mapEngine.SetSeed(time.Now().UnixNano())
d2mapgen.GenerateAct1Overworld(met.mapEngine)
met.mapGen.GenerateAct1Overworld()
} else {
met.mapEngine = d2mapengine.CreateMapEngine(met.asset) // necessary for map name update
met.mapEngine.SetSeed(time.Now().UnixNano())

View File

@ -4,7 +4,10 @@ import (
"fmt"
"image"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2inventory"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2tbl"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
@ -14,7 +17,6 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
"github.com/OpenDiablo2/OpenDiablo2/d2game/d2player"
"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype"
)
@ -268,18 +270,20 @@ func (hri *HeroRenderInfo) advance(elapsed float64) {
// SelectHeroClass represents the Select Hero Class screen
type SelectHeroClass struct {
asset *d2asset.AssetManager
uiManager *d2ui.UIManager
bgImage *d2ui.Sprite
campfire *d2ui.Sprite
headingLabel *d2ui.Label
heroClassLabel *d2ui.Label
heroDesc1Label *d2ui.Label
heroDesc2Label *d2ui.Label
heroDesc3Label *d2ui.Label
heroNameTextbox *d2ui.TextBox
heroNameLabel *d2ui.Label
heroRenderInfo map[d2enum.Hero]*HeroRenderInfo
asset *d2asset.AssetManager
uiManager *d2ui.UIManager
bgImage *d2ui.Sprite
campfire *d2ui.Sprite
headingLabel *d2ui.Label
heroClassLabel *d2ui.Label
heroDesc1Label *d2ui.Label
heroDesc2Label *d2ui.Label
heroDesc3Label *d2ui.Label
heroNameTextbox *d2ui.TextBox
heroNameLabel *d2ui.Label
heroRenderInfo map[d2enum.Hero]*HeroRenderInfo
*d2inventory.InventoryItemFactory
*d2hero.HeroStateFactory
selectedHero d2enum.Hero
exitButton *d2ui.Button
okButton *d2ui.Button
@ -304,20 +308,32 @@ func CreateSelectHeroClass(
ui *d2ui.UIManager,
connectionType d2clientconnectiontype.ClientConnectionType,
connectionHost string,
) *SelectHeroClass {
result := &SelectHeroClass{
asset: asset,
heroRenderInfo: make(map[d2enum.Hero]*HeroRenderInfo),
selectedHero: d2enum.HeroNone,
connectionType: connectionType,
connectionHost: connectionHost,
audioProvider: audioProvider,
renderer: renderer,
navigator: navigator,
uiManager: ui,
) (*SelectHeroClass, error) {
playerStateFactory, err := d2hero.NewHeroStateFactory(asset)
if err != nil {
return nil, err
}
return result
inventoryItemFactory, err := d2inventory.NewInventoryItemFactory(asset)
if err != nil {
return nil, err
}
result := &SelectHeroClass{
asset: asset,
heroRenderInfo: make(map[d2enum.Hero]*HeroRenderInfo),
selectedHero: d2enum.HeroNone,
connectionType: connectionType,
connectionHost: connectionHost,
audioProvider: audioProvider,
renderer: renderer,
navigator: navigator,
uiManager: ui,
HeroStateFactory: playerStateFactory,
InventoryItemFactory: inventoryItemFactory,
}
return result, nil
}
// OnLoad loads the resources for the Select Hero Class screen
@ -469,12 +485,23 @@ func (v *SelectHeroClass) onExitButtonClicked() {
}
func (v *SelectHeroClass) onOkButtonClicked() {
gameState := d2player.CreatePlayerState(
v.heroNameTextbox.GetText(),
v.selectedHero,
d2datadict.CharStats[v.selectedHero],
)
v.navigator.ToCreateGame(gameState.FilePath, d2clientconnectiontype.Local, v.connectionHost)
heroName := v.heroNameTextbox.GetText()
defaultStats := v.asset.Records.Character.Stats[v.selectedHero]
statsState := v.CreateHeroStatsState(v.selectedHero, defaultStats)
playerState, err := v.CreateHeroState(heroName, v.selectedHero, statsState)
if err := v.Save(playerState); err != nil {
fmt.Printf("failed to save game state!, err: %v\n", err)
}
if err != nil {
return
}
playerState.Equipment = v.InventoryItemFactory.DefaultHeroItems[v.selectedHero]
v.navigator.ToCreateGame(playerState.FilePath, d2clientconnectiontype.Local, v.connectionHost)
}
// Render renders the Select Hero Class screen

View File

@ -1,8 +1,8 @@
package d2player
import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
)
// EquipmentSlot represents an equipment slot for a player
@ -14,7 +14,7 @@ type EquipmentSlot struct {
height int
}
func genEquipmentSlotsMap(record *d2datadict.InventoryRecord) map[d2enum.EquippedSlot]EquipmentSlot {
func genEquipmentSlotsMap(record *d2records.InventoryRecord) map[d2enum.EquippedSlot]EquipmentSlot {
slotMap := map[d2enum.EquippedSlot]EquipmentSlot{}
slots := []d2enum.EquippedSlot{

View File

@ -12,7 +12,6 @@ import (
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
"github.com/OpenDiablo2/OpenDiablo2/d2game/d2player/help"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui"
@ -51,6 +50,7 @@ type GameControls struct {
renderer d2interface.Renderer // TODO: This shouldn't be a dependency
inputListener InputCallbackListener
hero *d2mapentity.Player
heroState *d2hero.HeroStateFactory
mapEngine *d2mapengine.MapEngine
mapRenderer *d2maprenderer.MapRenderer
uiManager *d2ui.UIManager
@ -155,18 +155,24 @@ func NewGameControls(
return nil, fmt.Errorf("unknown hero class: %d", hero.Class)
}
inventoryRecord := d2datadict.Inventory[inventoryRecordKey]
inventoryRecord := asset.Records.Layout.Inventory[inventoryRecordKey]
hoverLabel := nameLabel
hoverLabel.SetBackgroundColor(color.RGBA{0, 0, 0, uint8(128)})
globeStatsLabel := hpManaStatsLabel
heroState, err := d2hero.NewHeroStateFactory(asset)
if err != nil {
return nil, err
}
gc := &GameControls{
asset: asset,
uiManager: ui,
renderer: renderer,
hero: hero,
heroState: heroState,
mapEngine: mapEngine,
inputListener: inputListener,
mapRenderer: mapRenderer,
@ -196,7 +202,7 @@ func NewGameControls(
isSinglePlayer: isSinglePlayer,
}
err := term.BindAction("freecam", "toggle free camera movement", func() {
err = term.BindAction("freecam", "toggle free camera movement", func() {
gc.FreeCam = !gc.FreeCam
})
@ -205,13 +211,23 @@ func NewGameControls(
}
err = term.BindAction("setleftskill", "set skill to fire on left click", func(id int) {
skillRecord := d2datadict.SkillDetails[id]
gc.hero.LeftSkill = &d2hero.HeroSkill{SkillPoints: 0, SkillRecord: skillRecord, SkillDescriptionRecord: d2datadict.SkillDescriptions[skillRecord.Skilldesc]}
skillRecord := gc.asset.Records.Skill.Details[id]
skill, err := heroState.CreateHeroSkill(0, skillRecord.Skill)
if err != nil {
term.OutputErrorf("cannot create skill with ID of %d", id)
}
gc.hero.LeftSkill = skill
})
err = term.BindAction("setrightskill", "set skill to fire on right click", func(id int) {
skillRecord := d2datadict.SkillDetails[id]
gc.hero.RightSkill = &d2hero.HeroSkill{SkillPoints: 0, SkillRecord: skillRecord, SkillDescriptionRecord: d2datadict.SkillDescriptions[skillRecord.Skilldesc]}
skillRecord := gc.asset.Records.Skill.Details[id]
skill, err := heroState.CreateHeroSkill(0, skillRecord.Skill)
if err != nil {
term.OutputErrorf("cannot create skill with ID of %d", id)
}
gc.hero.RightSkill = skill
})
if err != nil {
@ -416,7 +432,7 @@ func (g *GameControls) Load() {
attackIconID := 2
g.leftSkill = &SkillResource{SkillIcon: genericSkillsSprite, IconNumber: attackIconID, SkillResourcePath: d2resource.GenericSkills}
g.rightSkill = &SkillResource{SkillIcon: genericSkillsSprite, IconNumber: attackIconID, SkillResourcePath: d2resource.GenericSkills}
g.rightSkill = &SkillResource{SkillIcon: genericSkillsSprite, IconNumber: attackIconID, SkillResourcePath: d2resource.GenericSkills}
g.loadUIButtons()

View File

@ -4,7 +4,8 @@ import (
"fmt"
"image/color"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
@ -17,6 +18,7 @@ import (
// Inventory represents the inventory
type Inventory struct {
asset *d2asset.AssetManager
item *diablo2item.ItemFactory
uiManager *d2ui.UIManager
frame *d2ui.Sprite
panel *d2ui.Sprite
@ -34,13 +36,16 @@ type Inventory struct {
// NewInventory creates an inventory instance and returns a pointer to it
func NewInventory(asset *d2asset.AssetManager, ui *d2ui.UIManager,
record *d2datadict.InventoryRecord) *Inventory {
record *d2records.InventoryRecord) *Inventory {
hoverLabel := ui.NewLabel(d2resource.FontFormal11, d2resource.PaletteStatic)
hoverLabel.Alignment = d2gui.HorizontalAlignCenter
itemFactory, _ := diablo2item.NewItemFactory(asset) // TODO handle errors
return &Inventory{
asset: asset,
uiManager: ui,
item: itemFactory,
grid: NewItemGrid(asset, ui, record),
originX: record.Panel.Left,
hoverLabel: hoverLabel,
@ -74,28 +79,52 @@ func (g *Inventory) Load() {
g.frame, _ = g.uiManager.NewSprite(d2resource.Frame, d2resource.PaletteSky)
g.panel, _ = g.uiManager.NewSprite(d2resource.InventoryCharacterPanel, d2resource.PaletteSky)
items := []InventoryItem{
diablo2item.NewItem("kit", "Crimson", "of the Bat", "of Frost").Identify(),
diablo2item.NewItem("rin", "Steel", "of Shock").Identify(),
diablo2item.NewItem("jav").Identify(),
diablo2item.NewItem("buc").Identify(),
// diablo2item.NewItem("Arctic Furs", "qui"),
// TODO: Load the player's actual items
// TODO: remove this item test code
testInventoryCodes := [][]string{
{"kit", "Crimson", "of the Bat", "of Frost"},
{"rin", "Steel", "of Shock"},
{"jav"},
{"buc"},
}
inventoryItems := make([]InventoryItem, 0)
for idx := range testInventoryCodes {
item, err := g.item.NewItem(testInventoryCodes[idx]...)
if err != nil {
continue
}
item.Identify()
inventoryItems = append(inventoryItems, item)
}
testEquippedItemCodes := map[d2enum.EquippedSlot][]string{
d2enum.EquippedSlotLeftArm: {"wnd"},
d2enum.EquippedSlotRightArm: {"buc"},
d2enum.EquippedSlotHead: {"crn"},
d2enum.EquippedSlotTorso: {"plt"},
d2enum.EquippedSlotLegs: {"vbt"},
d2enum.EquippedSlotBelt: {"vbl"},
d2enum.EquippedSlotGloves: {"lgl"},
d2enum.EquippedSlotLeftHand: {"rin"},
d2enum.EquippedSlotRightHand: {"rin"},
d2enum.EquippedSlotNeck: {"amu"},
}
for slot := range testEquippedItemCodes {
item, err := g.item.NewItem(testEquippedItemCodes[slot]...)
if err != nil {
continue
}
g.grid.ChangeEquippedSlot(slot, item)
}
g.grid.ChangeEquippedSlot(d2enum.EquippedSlotLeftArm, diablo2item.NewItem("wnd"))
g.grid.ChangeEquippedSlot(d2enum.EquippedSlotRightArm, diablo2item.NewItem("buc"))
g.grid.ChangeEquippedSlot(d2enum.EquippedSlotHead, diablo2item.NewItem("crn"))
g.grid.ChangeEquippedSlot(d2enum.EquippedSlotTorso, diablo2item.NewItem("plt"))
g.grid.ChangeEquippedSlot(d2enum.EquippedSlotLegs, diablo2item.NewItem("vbt"))
g.grid.ChangeEquippedSlot(d2enum.EquippedSlotBelt, diablo2item.NewItem("vbl"))
g.grid.ChangeEquippedSlot(d2enum.EquippedSlotGloves, diablo2item.NewItem("lgl"))
g.grid.ChangeEquippedSlot(d2enum.EquippedSlotLeftHand, diablo2item.NewItem("rin"))
g.grid.ChangeEquippedSlot(d2enum.EquippedSlotRightHand, diablo2item.NewItem("rin"))
g.grid.ChangeEquippedSlot(d2enum.EquippedSlotNeck, diablo2item.NewItem("amu"))
// TODO: Load the player's actual items
_, err := g.grid.Add(items...)
_, err := g.grid.Add(inventoryItems...)
if err != nil {
fmt.Printf("could not add items to the inventory, err: %v\n", err)
}

View File

@ -5,7 +5,8 @@ import (
"fmt"
"log"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
@ -46,7 +47,7 @@ type ItemGrid struct {
}
func NewItemGrid(asset *d2asset.AssetManager, ui *d2ui.UIManager,
record *d2datadict.InventoryRecord) *ItemGrid {
record *d2records.InventoryRecord) *ItemGrid {
grid := record.Grid
return &ItemGrid{

View File

@ -1,155 +0,0 @@
package d2player
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"strconv"
"strings"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2inventory"
)
// PlayerState stores the state of the player
type PlayerState struct {
HeroName string `json:"heroName"`
HeroType d2enum.Hero `json:"heroType"`
HeroLevel int `json:"heroLevel"`
Act int `json:"act"`
FilePath string `json:"-"`
Equipment d2inventory.CharacterEquipment `json:"equipment"`
Stats *d2hero.HeroStatsState `json:"stats"`
Skills *d2hero.HeroSkillsState `json:"skills"`
X float64 `json:"x"`
Y float64 `json:"y"`
}
// HasGameStates returns true if the player has any previously saved game
func HasGameStates() bool {
basePath, _ := getGameBaseSavePath()
files, _ := ioutil.ReadDir(basePath)
return len(files) > 0
}
// GetAllPlayerStates returns all player saves
func GetAllPlayerStates() []*PlayerState {
basePath, _ := getGameBaseSavePath()
files, _ := ioutil.ReadDir(basePath)
result := make([]*PlayerState, 0)
for _, file := range files {
fileName := file.Name()
if file.IsDir() || len(fileName) < 5 || !strings.EqualFold(fileName[len(fileName)-4:], ".od2") {
continue
}
gameState := LoadPlayerState(path.Join(basePath, file.Name()))
if gameState == nil || gameState.HeroType == d2enum.HeroNone {
continue
} else if gameState.Stats == nil || gameState.Skills == nil {
// temporarily loading default class stats if the character was created before saving stats/skills was introduced
// to be removed in the future
classStats := d2datadict.CharStats[gameState.HeroType]
gameState.Stats = d2hero.CreateHeroStatsState(gameState.HeroType, classStats)
gameState.Skills = d2hero.CreateHeroSkillsState(classStats)
if err := gameState.Save(); err != nil {
fmt.Printf("failed to save game state!, err: %v\n", err)
}
}
result = append(result, gameState)
}
return result
}
// CreateTestGameState is used for the map engine previewer
func CreateTestGameState() *PlayerState {
result := &PlayerState{}
return result
}
// LoadPlayerState loads the player state from the file
func LoadPlayerState(filePath string) *PlayerState {
strData, err := ioutil.ReadFile(filePath)
if err != nil {
return nil
}
result := &PlayerState{
FilePath: filePath,
}
err = json.Unmarshal(strData, result)
if err != nil {
return nil
}
return result
}
// CreatePlayerState creates a PlayerState instance and returns a pointer to it
func CreatePlayerState(heroName string, hero d2enum.Hero, classStats *d2datadict.CharStatsRecord) *PlayerState {
result := &PlayerState{
HeroName: heroName,
HeroType: hero,
Act: 1,
Stats: d2hero.CreateHeroStatsState(hero, classStats),
Skills: d2hero.CreateHeroSkillsState(classStats),
Equipment: d2inventory.HeroObjects[hero],
FilePath: "",
}
if err := result.Save(); err != nil {
fmt.Printf("failed to save game state!, err: %v\n", err)
return nil
}
return result
}
func getGameBaseSavePath() (string, error) {
configDir, err := os.UserConfigDir()
if err != nil {
return "", err
}
return path.Join(configDir, "OpenDiablo2/Saves"), nil
}
func getFirstFreeFileName() string {
i := 0
basePath, _ := getGameBaseSavePath()
for {
filePath := path.Join(basePath, strconv.Itoa(i)+".od2")
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return filePath
}
i++
}
}
// Save saves the player state to a file
func (v *PlayerState) Save() error {
if v.FilePath == "" {
v.FilePath = getFirstFreeFileName()
}
if err := os.MkdirAll(path.Dir(v.FilePath), 0755); err != nil {
return err
}
fileJSON, _ := json.MarshalIndent(v, "", " ")
if err := ioutil.WriteFile(v.FilePath, fileJSON, 0644); err != nil {
return err
}
return nil
}

Some files were not shown because too many files have changed in this diff Show More