diff --git a/.gitignore b/.gitignore index a5cafb8e..af8a78ef 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,6 @@ /OpenDiablo2.exe /OpenDiablo2 **/*.pprof -tags +*.swp +.*.swp +tags \ No newline at end of file diff --git a/d2common/d2data/d2datadict/itemstatcost.go b/d2common/d2data/d2datadict/itemstatcost.go new file mode 100644 index 00000000..5a9b9f3b --- /dev/null +++ b/d2common/d2data/d2datadict/itemstatcost.go @@ -0,0 +1,320 @@ +package d2datadict + +import ( + "github.com/OpenDiablo2/OpenDiablo2/d2common" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" + "log" +) + +// refer to https://d2mods.info/forum/kb/viewarticle?a=448 +type ItemStatCostRecord struct { + Name string + Index int + + Signed bool // whether the stat is signed + KeepZero bool // prevent from going negative (assume only client side) + + // 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 + + UpdateAnimRate bool // when altered, forces speed handler to adjust speed + + SendOther bool // whether to send to other clients + SendBits int // #bits to send in stat update + SendParam int // #bits to send in stat update + + Saved bool // whether this stat is saved in .d2s files + SavedSigned bool // whether the stat is saved as signed/unsigned + 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 EncodingType // how the stat is encoded in .d2s files + + CallbackEnabled bool // whether callback fn is called if value changes + + // 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 OperatorType + OpParam int + OpBase string + OpStat1 string + OpStat2 string + OpStat3 string + + Direct bool // whether is temporary or permanent + MaxStat string // if Direct true, will not exceed val of MaxStat + + ItemSpecific bool // prevents stacking with an existing stat on item + // like when socketing a jewel + + DamageRelated bool // prevents stacking of stats while dual wielding + + EventID1 d2enum.ItemEventType + EventID2 d2enum.ItemEventType + EventFuncID1 d2enum.ItemEventFuncID + EventFuncID2 d2enum.ItemEventFuncID + + DescPriority int // determines order when displayed + DescFnID d2enum.DescFuncID + DescFn interface{} // the sprintf func + + // 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 + DescStrPos string // string used when val is positive + DescStrNeg string + DescStr2 string // additional string used by some string funcs + + // 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 + DescGroupFuncID d2enum.DescFuncID + DescGroupFn interface{} // group sprintf func + DescGroupVal int + DescGroupStrPos string // string used when val is positive + DescGroupStrNeg string + DescGroupStr2 string // additional string used by some string funcs + + // 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 // ? TODO ? +} + +type EncodingType int + +const ( + // TODO: determine other encoding types. + // didn't see anything about how this stuff is encoded, or the types... + EncodeDefault = EncodingType(iota) +) + +type OperatorType int // for dynamic properties + +const ( + // just adds the stat to the unit directly + OpDefault = OperatorType(iota) + + // adds opstat.base * statvalue / 100 to the opstat. + Op1 + + // adds (statvalue * basevalue) / (2 ^ param) to the opstat + // this does not work properly with any stat other then level because of the + // way this is updated, it is only refreshed when you re-equip the item, + // your character is saved or you level up, similar to passive skills, just + // because it looks like it works in the item description + // does not mean it does, the game just recalculates the information in the + // description every frame, while the values remain unchanged serverside. + Op2 + + // this is a percentage based version of op #2 + // look at op #2 for information about the formula behind it, just + // remember the stat is increased by a percentage rather then by adding + // an integer. + Op3 + + // this works the same way op #2 works, however the stat bonus is + // added to the item and not to the player (so that +defense per level + // properly adds the defense to the armor and not to the character + // directly!) + Op4 + + // this works like op #4 but is percentage based, it is used for percentage + // based increase of stats that are found on the item itself, and not stats + // that are found on the character. + Op5 + + // like for op #7, however this adds a plain bonus to the stat, and just + // like #7 it also doesn't work so I won't bother to explain the arithmetic + // behind it either. + Op6 + + // this is used to increase a stat based on the current daytime of the game + // world by a percentage, there is no need to explain the arithmetics + // behind it because frankly enough it just doesn't work serverside, it + // only updates clientside so this op is essentially useless. + Op7 + + // hardcoded to work only with maxmana, this will apply the proper amount + // of mana to your character based on CharStats.txt for the amount of energy + // the stat added (doesn't work for non characters) + Op8 + + // hardcoded to work only with maxhp and maxstamina, this will apply the + // proper amount of maxhp and maxstamina to your character based on + // CharStats.txt for the amount of vitality the stat added (doesn't work + // for non characters) + Op9 + + // doesn't do anything, this has no switch case in the op function. + Op10 + + // adds opstat.base * statvalue / 100 similar to 1 and 13, the code just + // does a few more checks + Op11 + + // doesn't do anything, this has no switch case in the op function. + Op12 + + // adds opstat.base * statvalue / 100 to the value of opstat, this is + // useable only on items it will not apply the bonus to other unit types + // (this is why it is used for +% durability, +% level requirement, + // +% damage, +% defense ). + Op13 +) + +/* column names from path_d2.mpq/data/global/excel/ItemStatCost.txt +Stat +ID +Send Other +Signed +Send Bits +Send Param Bits +UpdateAnimRate +Saved +CSvSigned +CSvBits +CSvParam +fCallback +fMin +MinAccr +Encode +Add +Multiply +Divide +ValShift +1.09-Save Bits +1.09-Save Add +Save Bits +Save Add +Save Param Bits +keepzero +op +op param +op base +op stat1 +op stat2 +op stat3 +direct +maxstat +itemspecific +damagerelated +itemevent1 +itemeventfunc1 +itemevent2 +itemeventfunc2 +descpriority +descfunc +descval +descstrpos +descstrneg +descstr2 +dgrp +dgrpfunc +dgrpval +dgrpstrpos +dgrpstrneg +dgrpstr2 +stuff +*eol +*/ + +var ItemStatCosts map[string]*ItemStatCostRecord + +func LoadItemStatCosts(file []byte) { + ItemStatCosts = make(map[string]*ItemStatCostRecord, 0) + d := d2common.LoadDataDictionary(string(file)) + r := make([]*ItemStatCostRecord, 0) + + for idx, _ := range d.Data { + record := &ItemStatCostRecord{ + Name: d.GetString("Stat", idx), + Index: d.GetNumber("ID", idx), + + Signed: d.GetNumber("Signed", idx) > 0, + KeepZero: d.GetNumber("keepzero", idx) > 0, + + // Ranged: d.GetNumber("Ranged", idx) > 0, + MinAccr: d.GetNumber("MinAccr", idx), + + UpdateAnimRate: d.GetNumber("UpdateAnimRate", idx) > 0, + + SendOther: d.GetNumber("Send Other", idx) > 0, + SendBits: d.GetNumber("Send Bits", idx), + SendParam: d.GetNumber("Send Param Bits", idx), + + Saved: d.GetNumber("CSvBits", idx) > 0, + SavedSigned: d.GetNumber("CSvSigned", idx) > 0, + SavedBits: d.GetNumber("CSvBits", idx), + SaveBits: d.GetNumber("Save Bits", idx), + SaveAdd: d.GetNumber("Save Add", idx), + SaveParamBits: d.GetNumber("Save Param Bits", idx), + + Encode: EncodingType(d.GetNumber("Encode", idx)), + + CallbackEnabled: d.GetNumber("fCallback", idx) > 0, + + CostAdd: d.GetNumber("Add", idx), + CostMultiply: d.GetNumber("Multiply", idx), + ValShift: d.GetNumber("ValShift", idx), + + OperatorType: OperatorType(d.GetNumber("op", idx)), + OpParam: d.GetNumber("op param", idx), + OpBase: d.GetString("op base", idx), + OpStat1: d.GetString("op stat1", idx), + OpStat2: d.GetString("op stat2", idx), + OpStat3: d.GetString("op stat3", idx), + + Direct: d.GetNumber("direct", idx) > 0, + MaxStat: d.GetString("maxstat", idx), + + ItemSpecific: d.GetNumber("itemspecific", idx) > 0, + DamageRelated: d.GetNumber("damagerelated", idx) > 0, + + EventID1: d2enum.GetItemEventType(d.GetString("itemevent1", idx)), + EventID2: d2enum.GetItemEventType(d.GetString("itemevent2", idx)), + EventFuncID1: d2enum.GetItemEventFuncID(d.GetNumber("itemeventfunc1", idx)), + EventFuncID2: d2enum.GetItemEventFuncID(d.GetNumber("itemeventfunc2", idx)), + + DescPriority: d.GetNumber("descpriority", idx), + DescFnID: d2enum.DescFuncID(d.GetNumber("descfunc", idx)), + DescFn: d2enum.GetDescFunction(d2enum.DescFuncID(d.GetNumber("descfunc", idx))), + DescVal: d.GetNumber("descval", idx), + DescStrPos: d.GetString("descstrpos", idx), + DescStrNeg: d.GetString("descstrneg", idx), + DescStr2: d.GetString("descstr2", idx), + + DescGroup: d.GetNumber("dgrp", idx), + DescGroupFuncID: d2enum.DescFuncID(d.GetNumber("dgrpfunc", idx)), + DescGroupFn: d2enum.GetDescFunction(d2enum.DescFuncID(d.GetNumber("dgrpfunc", idx))), + DescGroupVal: d.GetNumber("dgrpval", idx), + DescGroupStrPos: d.GetString("dgrpstrpos", idx), + DescGroupStrNeg: d.GetString("dgrpstrneg", idx), + DescGroupStr2: d.GetString("dgrpstr2", idx), + + Stuff: d.GetString("stuff", idx), + } + + r = append(r, record) + } + log.Printf("Loaded %d ItemStatCost records", len(r)) +} diff --git a/d2common/d2enum/description_functions.go b/d2common/d2enum/description_functions.go new file mode 100644 index 00000000..e14e28c6 --- /dev/null +++ b/d2common/d2enum/description_functions.go @@ -0,0 +1,194 @@ +package d2enum + +import ( + "fmt" +) + +type DescFuncID int + +func Format1(value float64, string1 string) string { + // +[value] [string1] + return fmt.Sprintf("+%f %s", value, string1) +} + +func Format2(value float64, string1 string) string { + // [value]% [string1] + return fmt.Sprintf("%f%% %s", value, string1) +} + +func Format3(value float64, string1 string) string { + // [value] [string1] + return fmt.Sprintf("%f %s", value, string1) +} + +func Format4(value float64, string1 string) string { + // +[value]% [string1] + return fmt.Sprintf("+%f%% %s", value, string1) +} + +func Format5(value float64, string1 string) string { + // [value*100/128]% [string1] + return fmt.Sprintf("%f%% %s", (value*100.0)/128.0, string1) +} + +func Format6(value float64, string1, string2 string) string { + // +[value] [string1] [string2] + return fmt.Sprintf("+%f %s %s", value, string1, string2) +} + +func Format7(value float64, string1, string2 string) string { + // [value]% [string1] [string2] + return fmt.Sprintf("%f%% %s %s", value, string1, string2) +} + +func Format8(value float64, string1, string2 string) string { + // +[value]% [string1] [string2] + return fmt.Sprintf("+%f%% %s %s", value, string1, string2) +} + +func Format9(value float64, string1, string2 string) string { + // [value] [string1] [string2] + return fmt.Sprintf("%f %s %s", value, string1, string2) +} + +func Format10(value float64, string1, string2 string) string { + // [value*100/128]% [string1] [string2] + return fmt.Sprintf("%f%% %s %s", (value*100.0)/128.0, string1, string2) +} + +func Format11(value float64) string { + // Repairs 1 Durability In [100 / value] Seconds + return fmt.Sprintf("Repairs 1 Durability In %.0f Seconds", 100.0/value) +} + +func Format12(value float64, string1 string) string { + // +[value] [string1] + return fmt.Sprintf("+%f %s", value, string1) +} + +func Format13(value float64, class string) string { + // +[value] to [class] Skill Levels + return fmt.Sprintf("+%.0f to %s Skill Levels", value, class) +} + +func Format14(value float64, skilltab, class string) string { + // +[value] to [skilltab] Skill Levels ([class] Only) + fmtStr := "+%.0f to %s Skill Levels (%s Only)" + return fmt.Sprintf(fmtStr, value, skilltab, class) +} + +func Format15(value float64, slvl int, skill, event string) string { + // [value]% chance to cast [slvl] [skill] on [event] + fmtStr := "%.0f%% chance to cast %d %s on %s" + return fmt.Sprintf(fmtStr, value, slvl, skill, event) +} + +func Format16(slvl int, skill string) string { + // Level [sLvl] [skill] Aura When Equipped + return fmt.Sprintf("Level %d %s Aura When Equipped", slvl, skill) +} + +func Format17(value float64, string1 string, time int) string { + // [value] [string1] (Increases near [time]) + return fmt.Sprintf("%f %s (Increases near %d)", value, string1, time) +} + +func Format18(value float64, string1 string, time int) string { + // [value]% [string1] (Increases near [time]) + return fmt.Sprintf("%f%% %s (Increases near %d)", value, string1, time) +} + +func Format19(value float64, string1 string) string { + // this is used by stats that use Blizzard's sprintf implementation + // (if you don't know what that is, it won't be of interest to you + // eitherway I guess), look at how prismatic is setup, the string is + // the format that gets passed to their sprintf spinoff. + return "" // TODO +} + +func Format20(value float64, string1 string) string { + // [value * -1]% [string1] + return fmt.Sprintf("%f%% %s", value*-1.0, string1) +} + +func Format21(value float64, string1 string) string { + // [value * -1] [string1] + return fmt.Sprintf("%f %s", value*-1.0, string1) +} + +func Format22(value float64, string1, montype string) string { + // [value]% [string1] [montype] + return fmt.Sprintf("%f%% %s %s", value, string1, montype) +} + +func Format23(value float64, string1 string) string { + // (warning: this is bugged in vanilla and doesn't work properly + // see CE forum) + return "" // TODO +} + +func Format24(value float64, string1, monster string) string { + // [value]% [string1] [monster] + return fmt.Sprintf("%f%% %s %s", value, string1, monster) +} + +func Format25(slvl float64, skill string, charges, maxCharges int) string { + // Level [slvl] [skill] ([charges]/[maxCharges] Charges) + fmtStr := "Level %.0f %s (%d/%d Charges)" + return fmt.Sprintf(fmtStr, slvl, skill, charges, maxCharges) +} + +func Format26(value float64, string1 string) string { + // not used by vanilla, present in the code but I didn't test it yet + return "" // TODO +} + +func Format27(value float64, string1 string) string { + // not used by vanilla, present in the code but I didn't test it yet + return "" // TODO +} + +func Format28(value float64, skill, class string) string { + // +[value] to [skill] ([class] Only) + return fmt.Sprintf("+%f to %s (%s Only)", value, skill, class) +} + +func Format29(value float64, skill string) string { + // +[value] to [skill] + return fmt.Sprintf("+%.0f to %s", value, skill) +} + +func GetDescFunction(n DescFuncID) interface{} { + m := map[DescFuncID]interface{}{ + DescFuncID(0): Format1, + DescFuncID(1): Format2, + DescFuncID(2): Format3, + DescFuncID(3): Format4, + DescFuncID(4): Format5, + DescFuncID(5): Format6, + DescFuncID(6): Format7, + DescFuncID(7): Format8, + DescFuncID(8): Format9, + DescFuncID(9): Format10, + DescFuncID(10): Format11, + DescFuncID(11): Format12, + DescFuncID(12): Format13, + DescFuncID(13): Format14, + DescFuncID(14): Format15, + DescFuncID(15): Format16, + DescFuncID(16): Format17, + DescFuncID(17): Format18, + DescFuncID(18): Format19, + DescFuncID(19): Format20, + DescFuncID(20): Format21, + DescFuncID(21): Format22, + DescFuncID(22): Format23, + DescFuncID(23): Format24, + DescFuncID(24): Format25, + DescFuncID(25): Format26, + DescFuncID(26): Format27, + DescFuncID(27): Format28, + DescFuncID(28): Format29, + } + return m[n] +} diff --git a/d2common/d2enum/item_event_functions.go b/d2common/d2enum/item_event_functions.go new file mode 100644 index 00000000..35fe9055 --- /dev/null +++ b/d2common/d2enum/item_event_functions.go @@ -0,0 +1,143 @@ +package d2enum + +type ItemEventFuncID int + +const ( + // shoots a missile at the owner of a missile that has just hit you + // (Chilling Armor uses this) + ReflectMissile = ItemEventFuncID(iota) + + // freezes the attacker for a set duration the attacker + // (Frozen Armor uses this) + FreezeAttacker + + // does cold damage to and chills the attacker (Shiver Armor uses this) + FreezeChillAttacker + + // % of damage taken is done to the attacker + // (Iron Maiden, thorns uses a hardcoded stat) + ReflectPercentDamage + + // % of damage done added to life, bypassing the targets resistance + // (used by Life Tap) + DamageDealtToHealth + + // attacker takes physical damage of # + AttackerTakesPhysical + + // knocks the target back + Knockback + + // induces fear in the target making it run away + InduceFear + + // applies Dim Vision to the target (it casts the actual curse on the + // monster) + BlindTarget + + // attacker takes lightning damage of # + AttackerTakesLightning + + // attacker takes fire damage of # + AttackerTakesFire + + // attacker takes cold damage of # + AttackerTakesCold + + // % damage taken is added to mana + DamageTakenToMana + + // freezes the target + FreezeTarget + + // causes the target to bleed and lose life (negative life regen) + OpenWounds + + // crushing blow against the target + CrushingBlow + + // mana after killing a monster + ManaOnKillMonster + + // life after killing a demon + LifeOnKillDemon + + // slows the target + SlowTarget + + // casts a skill against the defender + CastSkillAgainstDefender + + // casts a skill against the attacker + CastSkillAgainstAttacker + + // absorbs physical damage taken (used by Bone Armor) + AbsorbPhysical + + // transfers damage done from the summon to the owner (used by Blood Golem) + TakeSummonDamage + + // used by Energy Shield to absorb damage and shift it from life to mana + ManaAbsorbsDamage + + // absorbs elemental damage taken (used by Cyclone Armor) + AbsorbElementalDamage + + // transfers damage taken from the summon to the owner (used by Blood Golem) + TakeSummonDamage2 + + // used to slow the attacker if he hits a unit that has the slow target stat + // (used by Clay Golem) + TargetSlowsTarget + + // life after killing a monster + LifeOnKillMonster + + // destroys the corpse of a killed monster (rest in peace effect) + RestInPeace + + // cast a skill when the event occurs, without a target + CastSkillWithoutTarget + + // reanimate the target as the specified monster + ReanimateTargetAsMonster +) + +func GetItemEventFuncID(n int) ItemEventFuncID { + m := map[int]ItemEventFuncID{ + 0: ReflectMissile, + 1: FreezeAttacker, + 2: FreezeChillAttacker, + 3: ReflectPercentDamage, + 4: DamageDealtToHealth, + 5: AttackerTakesPhysical, + 6: Knockback, + 7: InduceFear, + 8: BlindTarget, + 9: AttackerTakesLightning, + 10: AttackerTakesFire, + 11: AttackerTakesCold, + 12: DamageTakenToMana, + 13: FreezeTarget, + 14: OpenWounds, + 15: CrushingBlow, + 16: ManaOnKillMonster, + 17: LifeOnKillDemon, + 18: SlowTarget, + 19: CastSkillAgainstDefender, + 20: CastSkillAgainstAttacker, + 21: AbsorbPhysical, + 22: TakeSummonDamage, + 23: ManaAbsorbsDamage, + 24: AbsorbElementalDamage, + 25: TakeSummonDamage2, + 26: TargetSlowsTarget, + 27: LifeOnKillMonster, + 28: RestInPeace, + 29: CastSkillWithoutTarget, + 30: ReanimateTargetAsMonster, + } + return m[n] +} + +//? do i need to do this ? //go:generate stringer -linecomment -type AnimationMode diff --git a/d2common/d2enum/item_events.go b/d2common/d2enum/item_events.go new file mode 100644 index 00000000..56dc1f59 --- /dev/null +++ b/d2common/d2enum/item_events.go @@ -0,0 +1,39 @@ +package d2enum + +// used in ItemStatCost +type ItemEventType int + +const ( + HitByMissile = ItemEventType(iota) // hit By a Missile + DamagedInMelee // Damaged in Melee + DamagedByMissile // Damaged By Missile + AttackedInMelee // melee Attack atttempt + DoActive // do active state skill + DoMeleeDamage // do damage in melee + DoMissileDamage // do missile damage + DoMeleeAttack // do melee attack + DoMissileAttack // do missile attack + Kill // killed something + Killed // killed By something + AbsorbDamage // dealt damage + LevelUp // gain a level +) + +func GetItemEventType(s string) ItemEventType { + strLookupTable := map[string]ItemEventType{ + "HitByMissile": HitByMissile, + "DamagedInMelee": DamagedInMelee, + "DamagedByMissile": DamagedByMissile, + "AttackedInMelee": AttackedInMelee, + "DoActive": DoActive, + "DoMeleeDamage": DoMeleeDamage, + "DoMissileDamage": DoMissileDamage, + "DoMeleeAttack": DoMeleeAttack, + "DoMissileAttack": DoMissileAttack, + "Kill": Kill, + "Killed": Killed, + "AbsorbDamage": AbsorbDamage, + "LevelUp": LevelUp, + } + return strLookupTable[s] +} diff --git a/d2common/d2resource/resource_paths.go b/d2common/d2resource/resource_paths.go index 0066d84d..5092ee7c 100644 --- a/d2common/d2resource/resource_paths.go +++ b/d2common/d2resource/resource_paths.go @@ -174,6 +174,7 @@ const ( LevelDetails = "/data/global/excel/Levels.bin" ObjectDetails = "/data/global/excel/Objects.txt" SoundSettings = "/data/global/excel/Sounds.txt" + ItemStatCost = "/data/global/excel/ItemStatCost.txt" // --- Animations --- diff --git a/main.go b/main.go index b63557bb..e56984e1 100644 --- a/main.go +++ b/main.go @@ -389,6 +389,8 @@ func loadDataDict() error { {d2resource.MonStats, d2datadict.LoadMonStats}, {d2resource.MagicPrefix, d2datadict.LoadMagicPrefix}, {d2resource.MagicSuffix, d2datadict.LoadMagicSuffix}, + {d2resource.ItemStatCost, d2datadict.LoadItemStatCosts}, + } for _, entry := range entries {