diff --git a/src/OpenAC.MosswartMassacre/Inventory/DecalItemBuilder.cs b/src/OpenAC.MosswartMassacre/Inventory/DecalItemBuilder.cs new file mode 100644 index 0000000..a9b7124 --- /dev/null +++ b/src/OpenAC.MosswartMassacre/Inventory/DecalItemBuilder.cs @@ -0,0 +1,262 @@ +using AcDream.Plugin.Abstractions; + +namespace OpenAC.MosswartMassacre.Inventory; + +/// +/// Everything the builder reads for one item, gathered on the tick thread. +/// +/// The item's header fields as the host reports them. +/// The item's raw property tables and appraisal profiles, when the host has them. +/// True once the item has been appraised this session. +/// When it was appraised, in Unix seconds; zero when it has not been. +/// The header words the contract does not expose yet. +internal readonly record struct ItemSource( + PluginInventoryItem Item, + PluginItemProperties? Properties, + bool HasIdData, + int LastIdTime, + ItemHeaderWords Words); + +/// +/// Builds the item record the backend expects from what the host reports: +/// the raw property tables as they are, plus the framework keys the original +/// filled from the object's header and appraisal profiles. +/// +/// +/// +/// Which keys appear, and when, follows the original's own item dumps: a +/// header field's key is present exactly when the server sent that field, +/// the value key is always present (-1 when the server sent none), the +/// underlay key accompanies the second header word, the spell counts come +/// with a non-empty appraised spell book, and the weapon and armor keys with +/// their appraisal profiles. The slot key was never in the tables and is not +/// written. +/// +/// +/// The tables are written in ascending key order. The original wrote them in +/// its framework's hash-table order, which depends on the insertion history +/// and cannot be reproduced; the backend reads them by key. +/// +/// +internal static class DecalItemBuilder +{ + /// The item's own active enchantments carry this bit in the appraised spell book. + public const uint ActiveSpellBit = 0x80000000u; + + private const uint IconBase = 0x06000000u; + + /// The original's MyWorldObjectCreator.Create. + public static MyWorldObject Create(in ItemSource source) + { + PluginInventoryItem item = source.Item; + ItemHeaderWords words = source.Words; + var ints = new Dictionary(); + var doubles = new Dictionary(); + var bools = new Dictionary(); + var strings = new Dictionary(); + + if (source.Properties is { } properties) + { + foreach (KeyValuePair kvp in properties.Ints ?? EmptyInts) + ints[unchecked((int)kvp.Key)] = kvp.Value; + foreach (KeyValuePair kvp in properties.Floats ?? EmptyDoubles) + doubles[unchecked((int)kvp.Key)] = kvp.Value; + foreach (KeyValuePair kvp in properties.Bools ?? EmptyBools) + bools[unchecked((int)kvp.Key)] = kvp.Value; + foreach (KeyValuePair kvp in properties.Strings ?? EmptyStrings) + strings[unchecked((int)kvp.Key)] = kvp.Value; + } + + bool Sent(uint flag, bool whenUnknown) => + words.WeenieHeaderFlags is uint flags ? (flags & flag) != 0u : whenUnknown; + + // Always in the header. + strings[DecalKeys.RawName] = item.Name; + ints[DecalKeys.WeenieClassId] = unchecked((int)item.WeenieClassId); + ints[DecalKeys.Icon] = ShortIcon(item.IconId); + ints[DecalKeys.Category] = unchecked((int)item.ItemType); + ints[DecalKeys.Behavior] = unchecked((int)item.PublicFlags); + int container = ContainerOf(item); + ints[DecalKeys.Container] = container; + + if (Sent(HeaderFlags.PluralName, !string.IsNullOrEmpty(item.PluralName))) + strings[DecalKeys.SecondaryName] = item.PluralName; + if (Sent(HeaderFlags.ItemsCapacity | HeaderFlags.ContainersCapacity, item.ItemsCapacity != 0 || item.ContainersCapacity != 0)) + { + ints[DecalKeys.ItemSlots] = item.ItemsCapacity; + ints[DecalKeys.PackSlots] = item.ContainersCapacity; + } + if (Sent(HeaderFlags.Value, item.Value != 0)) + ints[DecalKeys.RawValue] = item.Value; + else if (!ints.ContainsKey(DecalKeys.RawValue)) + ints[DecalKeys.RawValue] = -1; + if (Sent(HeaderFlags.Usable, item.Useability != 0u)) + ints[DecalKeys.Usable] = unchecked((int)item.Useability); + if (Sent(HeaderFlags.UseRadius, item.UseRadius != 0f)) + doubles[DecalKeys.ApproachDistance] = item.UseRadius; + if (Sent(HeaderFlags.UiEffects, item.Effects != 0u)) + ints[DecalKeys.IconOutline] = unchecked((int)item.Effects); + if (Sent(HeaderFlags.AmmoType, item.AmmoType != 0u)) + ints[DecalKeys.MissileType] = unchecked((int)item.AmmoType); + if (Sent(HeaderFlags.CombatUse, item.CombatUse != 0)) + ints[DecalKeys.EquipType] = item.CombatUse; + if (Sent(HeaderFlags.Structure | HeaderFlags.MaxStructure, item.MaximumStructure != 0 || item.Structure != 0)) + { + ints[DecalKeys.RawMaxStructure] = item.MaximumStructure; + ints[DecalKeys.RawStructure] = item.Structure; + } + if (Sent(HeaderFlags.StackSize | HeaderFlags.MaxStackSize, item.MaximumStackSize > 1)) + { + ints[DecalKeys.StackCount] = item.StackSize; + ints[DecalKeys.StackMax] = item.MaximumStackSize; + } + if (Sent(HeaderFlags.ValidLocations, item.ValidLocations != 0u)) + ints[DecalKeys.EquipableSlots] = unchecked((int)item.ValidLocations); + if (Sent(HeaderFlags.CurrentlyWieldedLocation, item.EquippedLocation != 0u)) + ints[DecalKeys.RawEquippedSlots] = unchecked((int)item.EquippedLocation); + if (Sent(HeaderFlags.Priority, item.CoverageMask != 0u)) + ints[DecalKeys.Coverage] = unchecked((int)item.CoverageMask); + if (Sent(HeaderFlags.TargetType, item.TargetType != 0u)) + ints[DecalKeys.UsageMask] = unchecked((int)item.TargetType); + if (Sent(HeaderFlags.Burden, item.Burden != 0)) + ints[DecalKeys.RawBurden] = item.Burden; + if (Sent(HeaderFlags.Spell, item.SpellId != 0u)) + ints[DecalKeys.SpellDid] = unchecked((int)item.SpellId); + if (Sent(HeaderFlags.Workmanship, item.Workmanship != 0f)) + doubles[DecalKeys.SalvageWorkmanship] = item.Workmanship; + if (words.HookType is int hookType && Sent(HeaderFlags.HookType, true)) + ints[DecalKeys.HookType] = hookType; + if (Sent(HeaderFlags.IconOverlay, item.IconOverlayId != 0u)) + ints[DecalKeys.IconOverlay] = ShortIcon(item.IconOverlayId); + if (Sent(HeaderFlags.MaterialType, item.MaterialType != 0u)) + ints[DecalKeys.RawMaterial] = unchecked((int)item.MaterialType); + + if ((item.PublicFlags & HeaderFlags.IncludesSecondHeader) != 0u) + { + ints[DecalKeys.IconUnderlay] = ShortIcon(item.IconUnderlayId); + if (words.WeenieHeaderFlags2 is uint flags2) + ints[DecalKeys.CreateFlags2] = unchecked((int)flags2); + } + + if (words.WeenieHeaderFlags is uint headerFlags) + ints[DecalKeys.CreateFlags1] = unchecked((int)headerFlags); + if (words.PhysicsDescriptionFlags is uint physicsFlags) + ints[DecalKeys.PhysicsDataFlags] = unchecked((int)physicsFlags); + if (words.PhysicsWord is uint physicsWord) + ints[DecalKeys.PhysicsWord] = unchecked((int)physicsWord); + if (words.SetupId is uint setup) + ints[DecalKeys.Setup] = unchecked((int)setup); + if (words.Scale is double scale) + doubles[DecalKeys.Scale] = scale; + if (words.ParentObjectId is uint parent) + ints[DecalKeys.Wielder] = unchecked((int)parent); + if (words.ParentLocation is int parentLocation) + ints[DecalKeys.WieldingSlot] = parentLocation; + + // From the appraisal. + var spells = new List(); + var activeSpells = new List(); + IReadOnlyList spellBook = item.AppraisedSpellIds ?? Array.Empty(); + foreach (uint entry in spellBook) + { + if ((entry & ActiveSpellBit) != 0u) + activeSpells.Add(unchecked((int)(entry & ~ActiveSpellBit))); + else + spells.Add(unchecked((int)entry)); + } + if (spellBook.Count > 0) + { + ints[DecalKeys.SpellCount] = spells.Count; + ints[DecalKeys.ActiveSpellCount] = activeSpells.Count; + } + + if (source.Properties?.WeaponProfile is { } weapon) + { + ints[DecalKeys.WeapSpeed] = weapon.WeaponTime; + ints[DecalKeys.EquipSkill] = unchecked((int)weapon.WeaponSkill); + ints[DecalKeys.DamageType] = weapon.DamageType; + ints[DecalKeys.MaxDamage] = weapon.Damage; + doubles[DecalKeys.Variance] = weapon.DamageVariance; + doubles[DecalKeys.AttackBonus] = weapon.WeaponOffense; + doubles[DecalKeys.Range] = weapon.MaxVelocity; + doubles[DecalKeys.DamageBonus] = weapon.DamageMod; + } + + if (source.Properties?.ArmorProfile is { } armor) + { + doubles[DecalKeys.SlashProt] = armor.SlashMod; + doubles[DecalKeys.PierceProt] = armor.PierceMod; + doubles[DecalKeys.BludgeonProt] = armor.BludgeonMod; + doubles[DecalKeys.AcidProt] = armor.AcidMod; + doubles[DecalKeys.LightningProt] = armor.ElectricMod; + doubles[DecalKeys.FireProt] = armor.FireMod; + doubles[DecalKeys.ColdProt] = armor.ColdMod; + } + + return new MyWorldObject + { + HasIdData = source.HasIdData, + Id = unchecked((int)item.ObjectId), + LastIdTime = source.LastIdTime, + ObjectClass = (int)item.ObjectClass, + Icon = ShortIcon(item.IconId), + ContainerId = container, + BoolValues = Sorted(bools), + DoubleValues = Sorted(doubles), + IntValues = Sorted(ints), + StringValues = Sorted(strings), + ActiveSpells = activeSpells, + Spells = spells, + }; + } + + /// + /// The original's MyWorldObjectCreator.Combine: an item remembered + /// with appraisal data from an earlier session keeps that data while the + /// live item has none, refreshed by whatever the live item does carry. + /// + public static MyWorldObject Combine(MyWorldObject older, MyWorldObject newer) + { + // Repairs an icon missing from an older saved record. + if (older.Icon == 0 && newer.Icon != 0) + older.Icon = newer.Icon; + + if (!older.HasIdData || newer.HasIdData) + return newer; + + older.AddTo(newer.BoolValues, newer.DoubleValues, newer.IntValues, newer.StringValues); + older.ContainerId = newer.ContainerId; + older.BoolValues = Sorted(older.BoolValues); + older.DoubleValues = Sorted(older.DoubleValues); + older.IntValues = Sorted(older.IntValues); + older.StringValues = Sorted(older.StringValues); + return older; + } + + /// + /// An icon id as the framework reported it: without the icon table's + /// 0x06000000 prefix. + /// + public static int ShortIcon(uint iconId) => + iconId >= IconBase ? unchecked((int)(iconId - IconBase)) : unchecked((int)iconId); + + /// + /// The framework's container of an item: the pack holding it, or the + /// creature wielding it when it is wielded. + /// + public static int ContainerOf(in PluginInventoryItem item) => + unchecked((int)(item.ContainerObjectId != 0u ? item.ContainerObjectId : item.WielderObjectId)); + + private static Dictionary Sorted(Dictionary table) + { + var sorted = new Dictionary(table.Count); + foreach (int key in table.Keys.Order()) + sorted[key] = table[key]; + return sorted; + } + + private static readonly IReadOnlyDictionary EmptyInts = new Dictionary(); + private static readonly IReadOnlyDictionary EmptyDoubles = new Dictionary(); + private static readonly IReadOnlyDictionary EmptyBools = new Dictionary(); + private static readonly IReadOnlyDictionary EmptyStrings = new Dictionary(); +} diff --git a/src/OpenAC.MosswartMassacre/Inventory/DecalKeys.cs b/src/OpenAC.MosswartMassacre/Inventory/DecalKeys.cs new file mode 100644 index 0000000..a4a5377 --- /dev/null +++ b/src/OpenAC.MosswartMassacre/Inventory/DecalKeys.cs @@ -0,0 +1,110 @@ +namespace OpenAC.MosswartMassacre.Inventory; + +/// +/// The pseudo keys the original's client framework mixed into an item's +/// property tables next to the raw game property ids. The backend reads +/// them by these numbers, so they are part of the wire. +/// +internal static class DecalKeys +{ + // Integer table. + public const int WeenieClassId = 218103808; + public const int Icon = 218103809; + public const int Container = 218103810; + public const int ItemSlots = 218103812; + public const int PackSlots = 218103813; + public const int StackCount = 218103814; + public const int StackMax = 218103815; + public const int SpellDid = 218103816; + public const int Wielder = 218103818; + public const int WieldingSlot = 218103819; + public const int Coverage = 218103821; + public const int EquipableSlots = 218103822; + public const int EquipType = 218103823; + public const int IconOutline = 218103824; + public const int MissileType = 218103825; + public const int UsageMask = 218103826; + public const int HookType = 218103828; + public const int Setup = 218103830; + public const int PhysicsWord = 218103831; + public const int CreateFlags1 = 218103832; + public const int CreateFlags2 = 218103833; + public const int Category = 218103834; + public const int Behavior = 218103835; + public const int SpellCount = 218103838; + public const int WeapSpeed = 218103839; + public const int EquipSkill = 218103840; + public const int DamageType = 218103841; + public const int MaxDamage = 218103842; + public const int Usable = 218103843; + public const int PhysicsDataFlags = 218103847; + public const int ActiveSpellCount = 218103848; + public const int IconOverlay = 218103849; + public const int IconUnderlay = 218103850; + + // Floating-point table. + public const int SlashProt = 167772160; + public const int PierceProt = 167772161; + public const int BludgeonProt = 167772162; + public const int AcidProt = 167772163; + public const int LightningProt = 167772164; + public const int FireProt = 167772165; + public const int ColdProt = 167772166; + public const int ApproachDistance = 167772168; + public const int SalvageWorkmanship = 167772169; + public const int Scale = 167772170; + public const int Variance = 167772171; + public const int AttackBonus = 167772172; + public const int Range = 167772173; + public const int DamageBonus = 167772174; + + // Text table. + public const int SecondaryName = 184549376; + + // Raw game property ids the framework filled from the object's header. + public const int RawBurden = 5; + public const int RawEquippedSlots = 10; + public const int RawValue = 19; + public const int RawMaxStructure = 91; + public const int RawStructure = 92; + public const int RawMaterial = 131; + public const int RawName = 1; + public const int RawManaRate = 5; + public const int RawCurrentMana = 107; + public const int RawMaximumMana = 108; +} + +/// +/// The bits of the object header's first flag word, which say which optional +/// header fields the server sent. The framework's key tables held a field +/// exactly when its bit was set. +/// +internal static class HeaderFlags +{ + public const uint PluralName = 0x00000001; + public const uint ItemsCapacity = 0x00000002; + public const uint ContainersCapacity = 0x00000004; + public const uint Value = 0x00000008; + public const uint Usable = 0x00000010; + public const uint UseRadius = 0x00000020; + public const uint UiEffects = 0x00000080; + public const uint AmmoType = 0x00000100; + public const uint CombatUse = 0x00000200; + public const uint Structure = 0x00000400; + public const uint MaxStructure = 0x00000800; + public const uint StackSize = 0x00001000; + public const uint MaxStackSize = 0x00002000; + public const uint ValidLocations = 0x00010000; + public const uint CurrentlyWieldedLocation = 0x00020000; + public const uint Priority = 0x00040000; + public const uint TargetType = 0x00080000; + public const uint Burden = 0x00200000; + public const uint Spell = 0x00400000; + public const uint Workmanship = 0x01000000; + public const uint HookType = 0x10000000; + public const uint IconOverlay = 0x40000000; + public const uint MaterialType = 0x80000000; + + /// The public-flags bit that says a second header word follows. + public const uint IncludesSecondHeader = 0x04000000; +} diff --git a/src/OpenAC.MosswartMassacre/Inventory/ItemDictionaries.cs b/src/OpenAC.MosswartMassacre/Inventory/ItemDictionaries.cs new file mode 100644 index 0000000..b82efe4 --- /dev/null +++ b/src/OpenAC.MosswartMassacre/Inventory/ItemDictionaries.cs @@ -0,0 +1,441 @@ +namespace OpenAC.MosswartMassacre.Inventory; + +/// +/// The lookup tables the item record's getters read (material, skill, mastery +/// and item-set names; the item-spell effects behind the "buffed" and +/// "calced" numbers). Copied verbatim from the original plugin's shared code: +/// the getters' output is part of the wire, so a changed name or number here +/// changes a frame. +/// +internal static class ItemDictionaries +{ + /// + /// Returns a dictionary of skill ids vs names + /// + /// + public static readonly Dictionary SkillInfo = new Dictionary + { + // This list was taken from the Alinco source + { 0x1, "Axe" }, + { 0x2, "Bow" }, + { 0x3, "Crossbow" }, + { 0x4, "Dagger" }, + { 0x5, "Mace" }, + { 0x6, "Melee Defense" }, + { 0x7, "Missile Defense" }, + { 0x8, "Sling" }, + { 0x9, "Spear" }, + { 0xA, "Staff" }, + { 0xB, "Sword" }, + { 0xC, "Thrown Weapons" }, + { 0xD, "Unarmed Combat" }, + { 0xE, "Arcane Lore" }, + { 0xF, "Magic Defense" }, + { 0x10, "Mana Conversion" }, + { 0x12, "Item Tinkering" }, + { 0x13, "Assess Person" }, + { 0x14, "Deception" }, + { 0x15, "Healing" }, + { 0x16, "Jump" }, + { 0x17, "Lockpick" }, + { 0x18, "Run" }, + { 0x1B, "Assess Creature" }, + { 0x1C, "Weapon Tinkering" }, + { 0x1D, "Armor Tinkering" }, + { 0x1E, "Magic Item Tinkering" }, + { 0x1F, "Creature Enchantment" }, + { 0x20, "Item Enchantment" }, + { 0x21, "Life Magic" }, + { 0x22, "War Magic" }, + { 0x23, "Leadership" }, + { 0x24, "Loyalty" }, + { 0x25, "Fletching" }, + { 0x26, "Alchemy" }, + { 0x27, "Cooking" }, + { 0x28, "Salvaging" }, + { 0x29, "Two Handed Combat" }, + { 0x2A, "Gearcraft"}, + { 0x2B, "Void" }, + { 0x2C, "Heavy Weapons" }, + { 0x2D, "Light Weapons" }, + { 0x2E, "Finesse Weapons" }, + { 0x2F, "Missile Weapons" }, + { 0x30, "Shield" }, + { 0x31, "Dual Wield" }, + { 0x32, "Recklessness" }, + { 0x33, "Sneak Attack" }, + { 0x34, "Dirty Fighting" }, + { 0x35, "Challenge" }, + { 0x36, "Summoning" }, + }; + + /// + /// Returns a dictionary of mastery ids vs names + /// + /// + public static readonly Dictionary MasteryInfo = new Dictionary + { + { 1, "Unarmed Weapon" }, + { 2, "Sword" }, + { 3, "Axe" }, + { 4, "Mace" }, + { 5, "Spear" }, + { 6, "Dagger" }, + { 7, "Staff" }, + { 8, "Bow" }, + { 9, "Crossbow" }, + { 10, "Thrown" }, + { 11, "Two Handed Combat" }, + }; + + /// + /// Returns a dictionary of attribute set ids vs names + /// + /// + public static readonly Dictionary AttributeSetInfo = new Dictionary + { + // This list was taken from Virindi Tank Loot Editor + // 01 + { 02, "Test"}, + // 03 + { 04, "Carraida's Benediction"}, + { 05, "Noble Relic Set" }, + { 06, "Ancient Relic Set" }, + { 07, "Relic Alduressa Set" }, + { 08, "Shou-jen Set" }, + { 09, "Empyrean Rings Set" }, + { 10, "Arm, Mind, Heart Set" }, + { 11, "Coat of the Perfect Light Set" }, + { 12, "Leggings of Perfect Light Set" }, + { 13, "Soldier's Set" }, + { 14, "Adept's Set" }, + { 15, "Archer's Set" }, + { 16, "Defender's Set" }, + { 17, "Tinker's Set" }, + { 18, "Crafter's Set" }, + { 19, "Hearty Set" }, + { 20, "Dexterous Set" }, + { 21, "Wise Set" }, + { 22, "Swift Set" }, + { 23, "Hardenend Set" }, + { 24, "Reinforced Set" }, + { 25, "Interlocking Set" }, + { 26, "Flame Proof Set" }, + { 27, "Acid Proof Set" }, + { 28, "Cold Proof Set" }, + { 29, "Lightning Proof Set" }, + { 30, "Dedication Set" }, + { 31, "Gladiatorial Clothing Set" }, + { 32, "Ceremonial Clothing" }, + { 33, "Protective Clothing" }, + { 34, "Noobie Armor" }, + { 35, "Sigil of Defense" }, + { 36, "Sigil of Destruction" }, + { 37, "Sigil of Fury" }, + { 38, "Sigil of Growth" }, + { 39, "Sigil of Vigor" }, + { 40, "Heroic Protector Set" }, + { 41, "Heroic Destroyer Set" }, + { 42, "Olthoi Armor D Red" }, + { 43, "Olthoi Armor C Rat" }, + { 44, "Olthoi Armor C Red" }, + { 45, "Olthoi Armor D Rat" }, + { 46, "Upgraded Relic Alduressa Set" }, + { 47, "Upgraded Ancient Relic Set" }, + { 48, "Upgraded Noble Relic Set" }, + { 49, "Weave of Alchemy" }, + { 50, "Weave of Arcane Lore" }, + { 51, "Weave of Armor Tinkering" }, + { 52, "Weave of Assess Person" }, + { 53, "Weave of Light Weapons" }, + { 54, "Weave of Missile Weapons" }, + { 55, "Weave of Cooking" }, + { 56, "Weave of Creature Enchantment" }, + { 57, "Weave of Missile Weapons" }, + { 58, "Weave of Finesse" }, + { 59, "Weave of Deception" }, + { 60, "Weave of Fletching" }, + { 61, "Weave of Healing" }, + { 62, "Weave of Item Enchantment" }, + { 63, "Weave of Item Tinkering" }, + { 64, "Weave of Leadership" }, + { 65, "Weave of Life Magic" }, + { 66, "Weave of Loyalty" }, + { 67, "Weave of Light Weapons" }, + { 68, "Weave of Magic Defense" }, + { 69, "Weave of Magic Item Tinkering" }, + { 70, "Weave of Mana Conversion" }, + { 71, "Weave of Melee Defense" }, + { 72, "Weave of Missile Defense" }, + { 73, "Weave of Salvaging" }, + { 74, "Weave of Light Weapons" }, + { 75, "Weave of Light Weapons" }, + { 76, "Weave of Heavy Weapons" }, + { 77, "Weave of Missile Weapons" }, + { 78, "Weave of Two Handed Combat" }, + { 79, "Weave of Light Weapons" }, + { 80, "Weave of Void Magic" }, + { 81, "Weave of War Magic" }, + { 82, "Weave of Weapon Tinkering" }, + { 83, "Weave of Assess Creature " }, + { 84, "Weave of Dirty Fighting" }, + { 85, "Weave of Dual Wield" }, + { 86, "Weave of Recklessness" }, + { 87, "Weave of Shield" }, + { 88, "Weave of Sneak Attack" }, + { 89, "Ninja_New" }, + { 90, "Weave of Summoning" }, + + { 91, "Shrouded Soul" }, + { 92, "Darkened Mind" }, + { 93, "Clouded Spirit" }, + { 94, "Minor Stinging Shrouded Soul" }, + { 95, "Minor Sparking Shrouded Soul" }, + { 96, "Minor Smoldering Shrouded Soul" }, + { 97, "Minor Shivering Shrouded Soul" }, + { 98, "Minor Stinging Darkened Mind" }, + { 99, "Minor Sparking Darkened Mind" }, + + { 100, "Minor Smoldering Darkened Mind" }, + { 101, "Minor Shivering Darkened Mind" }, + { 102, "Minor Stinging Clouded Spirit" }, + { 103, "Minor Sparking Clouded Spirit" }, + { 104, "Minor Smoldering Clouded Spirit" }, + { 105, "Minor Shivering Clouded Spirit" }, + { 106, "Major Stinging Shrouded Soul" }, + { 107, "Major Sparking Shrouded Soul" }, + { 108, "Major Smoldering Shrouded Soul" }, + { 109, "Major Shivering Shrouded Soul" }, + + { 110, "Major Stinging Darkened Mind" }, + { 111, "Major Sparking Darkened Mind" }, + { 112, "Major Smoldering Darkened Mind" }, + { 113, "Major Shivering Darkened Mind" }, + { 114, "Major Stinging Clouded Spirit" }, + { 115, "Major Sparking Clouded Spirit" }, + { 116, "Major Smoldering Clouded Spirit" }, + { 117, "Major Shivering Clouded Spirit" }, + { 118, "Blackfire Stinging Shrouded Soul" }, + { 119, "Blackfire Sparking Shrouded Soul" }, + + { 120, "Blackfire Smoldering Shrouded Soul" }, + { 121, "Blackfire Shivering Shrouded Soul" }, + { 122, "Blackfire Stinging Darkened Mind" }, + { 123, "Blackfire Sparking Darkened Mind" }, + { 124, "Blackfire Smoldering Darkened Mind" }, + { 125, "Blackfire Shivering Darkened Mind" }, + { 126, "Blackfire Stinging Clouded Spirit" }, + { 127, "Blackfire Sparking Clouded Spirit" }, + { 128, "Blackfire Smoldering Clouded Spirit" }, + { 129, "Blackfire Shivering Clouded Spirit" }, + + { 130, "Shimmering Shadows" }, + + { 131, "Brown Society Locket" }, + { 132, "Yellow Society Locket" }, + { 133, "Red Society Band" }, + { 134, "Green Society Band" }, + { 135, "Purple Society Band" }, + { 136, "Blue Society Band" }, + + { 137, "Gauntlet Garb" }, + + { 138, "UNKNOWN_138" }, // Possibly Paragon Missile Weapons + { 139, "UNKNOWN_139" }, // Possibly Paragon Casters + { 140, "UNKNOWN_140" }, // Possibly Paragon Melee Weapons + }; + + /// + /// Returns a dictionary of material ids vs names + /// + /// + public static readonly Dictionary MaterialInfo = new Dictionary + { + { 1, "Ceramic" }, + { 2, "Porcelain" }, + // 3 + { 4, "Linen" }, + { 5, "Satin" }, + { 6, "Silk" }, + { 7, "Velvet" }, + { 8, "Wool" }, + // 9 + { 10, "Agate" }, + { 11, "Amber" }, + { 12, "Amethyst" }, + { 13, "Aquamarine" }, + { 14, "Azurite" }, + { 15, "Black Garnet" }, + { 16, "Black Opal" }, + { 17, "Bloodstone" }, + { 18, "Carnelian" }, + { 19, "Citrine" }, + { 20, "Diamond" }, + { 21, "Emerald" }, + { 22, "Fire Opal" }, + { 23, "Green Garnet" }, + { 24, "Green Jade" }, + { 25, "Hematite" }, + { 26, "Imperial Topaz" }, + { 27, "Jet" }, + { 28, "Lapis Lazuli" }, + { 29, "Lavender Jade" }, + { 30, "Malachite" }, + { 31, "Moonstone" }, + { 32, "Onyx" }, + { 33, "Opal" }, + { 34, "Peridot" }, + { 35, "Red Garnet" }, + { 36, "Red Jade" }, + { 37, "Rose Quartz" }, + { 38, "Ruby" }, + { 39, "Sapphire" }, + { 40, "Smokey Quartz" }, + { 41, "Sunstone" }, + { 42, "Tiger Eye" }, + { 43, "Tourmaline" }, + { 44, "Turquoise" }, + { 45, "White Jade" }, + { 46, "White Quartz" }, + { 47, "White Sapphire" }, + { 48, "Yellow Garnet" }, + { 49, "Yellow Topaz" }, + { 50, "Zircon" }, + { 51, "Ivory" }, + { 52, "Leather" }, + { 53, "Armoredillo Hide" }, + { 54, "Gromnie Hide" }, + { 55, "Reed Shark Hide" }, + // 56 + { 57, "Brass" }, + { 58, "Bronze" }, + { 59, "Copper" }, + { 60, "Gold" }, + { 61, "Iron" }, + { 62, "Pyreal" }, + { 63, "Silver" }, + { 64, "Steel" }, + // 65 + { 66, "Alabaster" }, + { 67, "Granite" }, + { 68, "Marble" }, + { 69, "Obsidian" }, + { 70, "Sandstone" }, + { 71, "Serpentine" }, + { 73, "Ebony" }, + { 74, "Mahogany" }, + { 75, "Oak" }, + { 76, "Pine" }, + { 77, "Teak" }, + }; + + public struct SpellInfo + where T : struct + { + public readonly int Key; + public readonly T Change; + public readonly T Bonus; + + public SpellInfo(int key, T change, T bonus = default) + { + Key = key; + Change = change; + Bonus = bonus; + } + } + + // Taken from Decal.Adapter.Wrappers.LongValueKey + const int LongValueKey_MaxDamage = 218103842; + const int LongValueKey_ArmorLevel = 28; + + public static readonly Dictionary> LongValueKeySpellEffects = new Dictionary>() + { + // In 2012 they removed these item spells and converted them to auras that are cast on the player, not on the item. + { 1616, new SpellInfo(LongValueKey_MaxDamage, 20)}, // Blood Drinker VI + { 2096, new SpellInfo(LongValueKey_MaxDamage, 22)}, // Infected Caress + //{ 5183, new SpellInfo(LongValueKey_MaxDamage, 22)}, // Incantation of Blood Drinker Pre Feb-2013 + //{ 4395, new SpellInfo(LongValueKey_MaxDamage, 24, 2)}, // Incantation of Blood Drinker, this spell on the item adds 2 more points of damage over a user casted 8 Pre Feb-2013 + { 5183, new SpellInfo(LongValueKey_MaxDamage, 24)}, // Incantation of Blood Drinker Post Feb-2013 + { 4395, new SpellInfo(LongValueKey_MaxDamage, 24)}, // Incantation of Blood Drinker Post Feb-2013 + + { 2598, new SpellInfo(LongValueKey_MaxDamage, 2, 2)}, // Minor Blood Thirst + { 2586, new SpellInfo(LongValueKey_MaxDamage, 4, 4)}, // Major Blood Thirst + { 4661, new SpellInfo(LongValueKey_MaxDamage, 7, 7)}, // Epic Blood Thirst + { 6089, new SpellInfo(LongValueKey_MaxDamage, 10, 10)}, // Legendary Blood Thirst + + { 3688, new SpellInfo(LongValueKey_MaxDamage, 300)}, // Prodigal Blood Drinker + + + { 1486, new SpellInfo(LongValueKey_ArmorLevel, 200)}, // Impenetrability VI + { 2108, new SpellInfo(LongValueKey_ArmorLevel, 220)}, // Brogard's Defiance + { 4407, new SpellInfo(LongValueKey_ArmorLevel, 240)}, // Incantation of Impenetrability + + { 2604, new SpellInfo(LongValueKey_ArmorLevel, 20, 20)}, // Minor Impenetrability + { 2592, new SpellInfo(LongValueKey_ArmorLevel, 40, 40)}, // Major Impenetrability + { 4667, new SpellInfo(LongValueKey_ArmorLevel, 60, 60)}, // Epic Impenetrability + { 6095, new SpellInfo(LongValueKey_ArmorLevel, 80, 80)}, // Legendary Impenetrability + }; + + // Taken from Decal.Adapter.Wrappers.DoubleValueKey + const int DoubleValueKey_ElementalDamageVersusMonsters = 152; + const int DoubleValueKey_AttackBonus = 167772172; + const int DoubleValueKey_MeleeDefenseBonus = 29; + const int DoubleValueKey_ManaCBonus = 144; + + public static readonly Dictionary> DoubleValueKeySpellEffects = new Dictionary>() + { + // In 2012 they removed these item spells and converted them to auras that are cast on the player, not on the item. + { 3258, new SpellInfo(DoubleValueKey_ElementalDamageVersusMonsters, .06)}, // Spirit Drinker VI + { 3259, new SpellInfo(DoubleValueKey_ElementalDamageVersusMonsters, .07)}, // Infected Spirit Caress + //{ 5182, new SpellInfo(DoubleValueKey_ElementalDamageVersusMonsters, .07)}, // Incantation of Spirit Drinker Pre Feb-2013 + //{ 4414, new SpellInfo(DoubleValueKey_ElementalDamageVersusMonsters, .08, .01)}, // Incantation of Spirit Drinker, this spell on the item adds 1 more % of damage over a user casted 8 Pre Feb-2013 + { 5182, new SpellInfo(DoubleValueKey_ElementalDamageVersusMonsters, .08)}, // Incantation of Spirit Drinker Post Feb-2013 + { 4414, new SpellInfo(DoubleValueKey_ElementalDamageVersusMonsters, .08)}, // Incantation of Spirit Drinker, this spell on the item adds 1 more % of damage over a user casted 8 Post Feb-2013 + + { 3251, new SpellInfo(DoubleValueKey_ElementalDamageVersusMonsters, .01, .01)}, // Minor Spirit Thirst + { 3250, new SpellInfo(DoubleValueKey_ElementalDamageVersusMonsters, .03, .03)}, // Major Spirit Thirst + { 4670, new SpellInfo(DoubleValueKey_ElementalDamageVersusMonsters, .05, .05)}, // Epic Spirit Thirst + { 6098, new SpellInfo(DoubleValueKey_ElementalDamageVersusMonsters, .07, .07)}, // Legendary Spirit Thirst + + { 3735, new SpellInfo(DoubleValueKey_ElementalDamageVersusMonsters, .15)}, // Prodigal Spirit Drinker + + + // In 2012 they removed these item spells and converted them to auras that are cast on the player, not on the item. + { 1592, new SpellInfo(DoubleValueKey_AttackBonus, .15)}, // Heart Seeker VI + { 2106, new SpellInfo(DoubleValueKey_AttackBonus, .17)}, // Elysa's Sight + { 4405, new SpellInfo(DoubleValueKey_AttackBonus, .20)}, // Incantation of Heart Seeker + + { 2603, new SpellInfo(DoubleValueKey_AttackBonus, .03, .03)}, // Minor Heart Thirst + { 2591, new SpellInfo(DoubleValueKey_AttackBonus, .05, .05)}, // Major Heart Thirst + { 4666, new SpellInfo(DoubleValueKey_AttackBonus, .07, .07)}, // Epic Heart Thirst + { 6094, new SpellInfo(DoubleValueKey_AttackBonus, .09, .09)}, // Legendary Heart Thirst + + + // In 2012 they removed these item spells and converted them to auras that are cast on the player, not on the item. + { 1605, new SpellInfo(DoubleValueKey_MeleeDefenseBonus, .15)}, // Defender VI + { 2101, new SpellInfo(DoubleValueKey_MeleeDefenseBonus, .17)}, // Cragstone's Will + //{ 4400, new SpellInfo(DoubleValueKey_MeleeDefenseBonus, .17)}, // Incantation of Defender Pre Feb-2013 + { 4400, new SpellInfo(DoubleValueKey_MeleeDefenseBonus, .20)}, // Incantation of Defender Post Feb-2013 + + { 2600, new SpellInfo(DoubleValueKey_MeleeDefenseBonus, .03, .03)}, // Minor Defender + { 3985, new SpellInfo(DoubleValueKey_MeleeDefenseBonus, .04, .04)}, // Mukkir Sense + { 2588, new SpellInfo(DoubleValueKey_MeleeDefenseBonus, .05, .05)}, // Major Defender + { 4663, new SpellInfo(DoubleValueKey_MeleeDefenseBonus, .07, .07)}, // Epic Defender + { 6091, new SpellInfo(DoubleValueKey_MeleeDefenseBonus, .09, .09)}, // Legendary Defender + + { 3699, new SpellInfo(DoubleValueKey_MeleeDefenseBonus, .25)}, // Prodigal Defender + + + // In 2012 they removed these item spells and converted them to auras that are cast on the player, not on the item. + { 1480, new SpellInfo(DoubleValueKey_ManaCBonus, 1.60)}, // Hermetic Link VI + { 2117, new SpellInfo(DoubleValueKey_ManaCBonus, 1.70)}, // Mystic's Blessing + { 4418, new SpellInfo(DoubleValueKey_ManaCBonus, 1.80)}, // Incantation of Hermetic Link + + { 3201, new SpellInfo(DoubleValueKey_ManaCBonus, 1.05, 1.05)}, // Feeble Hermetic Link + { 3199, new SpellInfo(DoubleValueKey_ManaCBonus, 1.10, 1.10)}, // Minor Hermetic Link + { 3202, new SpellInfo(DoubleValueKey_ManaCBonus, 1.15, 1.15)}, // Moderate Hermetic Link + { 3200, new SpellInfo(DoubleValueKey_ManaCBonus, 1.20, 1.20)}, // Major Hermetic Link + { 6086, new SpellInfo(DoubleValueKey_ManaCBonus, 1.25, 1.25)}, // Epic Hermetic Link + { 6087, new SpellInfo(DoubleValueKey_ManaCBonus, 1.30, 1.30)}, // Legendary Hermetic Link + }; +} diff --git a/src/OpenAC.MosswartMassacre/Inventory/ItemHeaderSource.cs b/src/OpenAC.MosswartMassacre/Inventory/ItemHeaderSource.cs new file mode 100644 index 0000000..329374a --- /dev/null +++ b/src/OpenAC.MosswartMassacre/Inventory/ItemHeaderSource.cs @@ -0,0 +1,76 @@ +using AcDream.Plugin.Abstractions; + +namespace OpenAC.MosswartMassacre.Inventory; + +/// +/// The header words the original's key tables carried for every item that +/// the plugin contract does not hand a plugin yet. Each one that is null is +/// left out of the item record, which is what the original sent for an item +/// whose framework lacked the value. +/// +/// +/// +/// matters beyond its own key: it says which +/// optional header fields the server sent. While it is null the builder +/// decides presence from the field's value instead (present when non-zero), +/// which is wrong for a field the server sent as zero, most often the +/// equipable-slots word. +/// +/// +/// The physics words come from the object's physics description, which +/// the contract does not expose at all. +/// +/// +internal readonly record struct ItemHeaderWords +{ + /// The object header's first flag word (key 218103832). + public uint? WeenieHeaderFlags { get; init; } + + /// The object header's second flag word (key 218103833), when the header has one. + public uint? WeenieHeaderFlags2 { get; init; } + + /// The physics description's flag word (key 218103847). + public uint? PhysicsDescriptionFlags { get; init; } + + /// The physics word the framework filed under key 218103831. + public uint? PhysicsWord { get; init; } + + /// The model setup id (key 218103830). + public uint? SetupId { get; init; } + + /// + /// The value the framework filed under its scale key (167772170). It was + /// 0.0 in every recorded item whose physics description carried a scale. + /// + public double? Scale { get; init; } + + /// The object a wielded item hangs from (key 218103818). + public uint? ParentObjectId { get; init; } + + /// Where on its parent a wielded item hangs (key 218103819). + public int? ParentLocation { get; init; } + + /// The header's hook type (key 218103828). + public int? HookType { get; init; } +} + +/// Reads the header words for one owned item. +internal interface IItemHeaderSource +{ + ItemHeaderWords Read(in PluginInventoryItem item); +} + +/// +/// The contract this plugin builds against exposes none of these words, so +/// every one is absent. Replace this when the contract grows them. +/// +internal sealed class HostItemHeaderSource : IItemHeaderSource +{ + public static HostItemHeaderSource Instance { get; } = new(); + + private HostItemHeaderSource() + { + } + + public ItemHeaderWords Read(in PluginInventoryItem item) => default; +} diff --git a/src/OpenAC.MosswartMassacre/Inventory/MyWorldObject.cs b/src/OpenAC.MosswartMassacre/Inventory/MyWorldObject.cs new file mode 100644 index 0000000..81d4000 --- /dev/null +++ b/src/OpenAC.MosswartMassacre/Inventory/MyWorldObject.cs @@ -0,0 +1,313 @@ +using System.Globalization; + +namespace OpenAC.MosswartMassacre.Inventory; + +/// +/// One item as the backend receives it: the original plugin's item record, +/// carried over member for member because it is serialized as-is into +/// full_inventory and inventory_delta. +/// +/// +/// +/// Newtonsoft writes the public fields first, in declaration order, then every +/// public getter, in declaration order. The getters are part of the wire: the +/// backend reads several of them (Material, EquipSkill, MaxDamage, WieldLevel, +/// ...). Keep every member, its type, its name and its position exactly as +/// they are; a double getter must stay a double so -1 is written +/// -1.0. +/// +/// +/// The four tables are keyed the way the original's client framework keyed +/// them: raw game property ids mixed with that framework's own pseudo keys +/// (see ). fills them. +/// +/// +internal sealed class MyWorldObject +{ + public bool HasIdData; + public int Id; + public int LastIdTime; + public int ObjectClass; + public int Icon; + public int ContainerId; + + public Dictionary BoolValues = []; + public Dictionary DoubleValues = []; + public Dictionary IntValues = []; + public Dictionary StringValues = []; + + public List ActiveSpells = []; + public List Spells = []; + + /// + /// Copies every entry of the given tables over this record's, keeping any + /// key only this record has. The original guarded each copy with a check + /// against the table it was iterating, which is always true, so every + /// incoming entry simply overwrites. + /// + public void AddTo( + IDictionary boolValues, + IDictionary doubleValues, + IDictionary intValues, + IDictionary stringValues) + { + foreach (KeyValuePair kvp in boolValues) + BoolValues[kvp.Key] = kvp.Value; + foreach (KeyValuePair kvp in doubleValues) + DoubleValues[kvp.Key] = kvp.Value; + foreach (KeyValuePair kvp in intValues) + IntValues[kvp.Key] = kvp.Value; + foreach (KeyValuePair kvp in stringValues) + StringValues[kvp.Key] = kvp.Value; + } + + public string? Material { get { if (IntValues.ContainsKey(131)) return ItemDictionaries.MaterialInfo.ContainsKey(IntValues[131]) ? ItemDictionaries.MaterialInfo[IntValues[131]] : IntValues[131].ToString(CultureInfo.InvariantCulture); return null; } } + + public string? Name { get { return StringValues.ContainsKey(1) ? StringValues[1] : null; } } + + public string? EquipSkill { get { if (IntValues.ContainsKey(218103840)) return ItemDictionaries.SkillInfo.ContainsKey(IntValues[218103840]) ? ItemDictionaries.SkillInfo[IntValues[218103840]] : IntValues[218103840].ToString(CultureInfo.InvariantCulture); return null; } } + + public string? Mastery { get { if (IntValues.ContainsKey(353)) return ItemDictionaries.MasteryInfo.ContainsKey(IntValues[353]) ? ItemDictionaries.MasteryInfo[IntValues[353]] : IntValues[353].ToString(CultureInfo.InvariantCulture); return null; } } + + public string? ItemSet { get { if (IntValues.ContainsKey(265)) return ItemDictionaries.AttributeSetInfo.ContainsKey(IntValues[265]) ? ItemDictionaries.AttributeSetInfo[IntValues[265]] : IntValues[265].ToString(CultureInfo.InvariantCulture); return null; } } + + public int ArmorLevel { get { return IntValues.ContainsKey(28) ? IntValues[28] : -1; } } + + public string? Imbue + { + get + { + if (!IntValues.ContainsKey(179) || IntValues[179] == 0) return null; + + string retVal = string.Empty; + if ((IntValues[179] & 1) == 1) retVal += " CS"; + if ((IntValues[179] & 2) == 2) retVal += " CB"; + if ((IntValues[179] & 4) == 4) retVal += " AR"; + if ((IntValues[179] & 8) == 8) retVal += " SlashRend"; + if ((IntValues[179] & 16) == 16) retVal += " PierceRend"; + if ((IntValues[179] & 32) == 32) retVal += " BludgeRend"; + if ((IntValues[179] & 64) == 64) retVal += " AcidRend"; + if ((IntValues[179] & 128) == 128) retVal += " FrostRend"; + if ((IntValues[179] & 256) == 256) retVal += " LightRend"; + if ((IntValues[179] & 512) == 512) retVal += " FireRend"; + if ((IntValues[179] & 1024) == 1024) retVal += " MeleeImbue"; + if ((IntValues[179] & 4096) == 4096) retVal += " MagicImbue"; + if ((IntValues[179] & 8192) == 8192) retVal += " Hematited"; + if ((IntValues[179] & 536870912) == 536870912) retVal += " MagicAbsorb"; + retVal = retVal.Trim(); + return retVal; + } + } + + public int Tinks { get { return IntValues.ContainsKey(171) ? IntValues[171] : -1; } } + + public int MaxDamage { get { return IntValues.ContainsKey(218103842) ? IntValues[218103842] : -1; } } + + public int ElementalDmgBonus { get { return IntValues.ContainsKey(204) ? IntValues[204] : -1; } } + + public double Variance { get { return DoubleValues.ContainsKey(167772171) ? DoubleValues[167772171] : -1; } } + + public double DamageBonus { get { return DoubleValues.ContainsKey(167772174) ? DoubleValues[167772174] : -1; } } + + public double ElementalDamageVersusMonsters { get { return DoubleValues.ContainsKey(152) ? DoubleValues[152] : -1; } } + + public double AttackBonus { get { return DoubleValues.ContainsKey(167772172) ? DoubleValues[167772172] : -1; } } + + public double MeleeDefenseBonus { get { return DoubleValues.ContainsKey(29) ? DoubleValues[29] : -1; } } + + public double MagicDBonus { get { return DoubleValues.ContainsKey(150) ? DoubleValues[150] : -1; } } + + public double MissileDBonus { get { return DoubleValues.ContainsKey(149) ? DoubleValues[149] : -1; } } + + public double ManaCBonus { get { return DoubleValues.ContainsKey(144) ? DoubleValues[144] : -1; } } + + public int WieldLevel { get { if (IntValues.ContainsKey(160) && IntValues[160] > 0 && IntValues.ContainsKey(158) && IntValues[158] == 7 && IntValues.ContainsKey(159) && IntValues[159] == 1) return IntValues[160]; return -1; } } + + public int SkillLevel { get { if (IntValues.ContainsKey(160) && IntValues[160] > 0 && (!IntValues.ContainsKey(158) || IntValues[158] != 7) && IntValues.ContainsKey(159)) return IntValues[160]; return -1; } } + + public int LoreRequirement { get { return IntValues.ContainsKey(109) ? IntValues[109] : -1; } } + + public double SalvageWorkmanship { get { return DoubleValues.ContainsKey(167772169) ? DoubleValues[167772169] : -1; } } + + public int Workmanship { get { return IntValues.ContainsKey(105) ? IntValues[105] : -1; } } + + public int Value { get { return IntValues.ContainsKey(19) ? IntValues[19] : -1; } } + + public int Burden { get { return IntValues.ContainsKey(5) ? IntValues[5] : -1; } } + + public int DamRating { get { return IntValues.ContainsKey(370) ? IntValues[370] : -1; } } + + public int DamResistRating { get { return IntValues.ContainsKey(371) ? IntValues[371] : -1; } } + + public int CritRating { get { return IntValues.ContainsKey(372) ? IntValues[372] : -1; } } + + public int CritResistRating { get { return IntValues.ContainsKey(373) ? IntValues[373] : -1; } } + + public int CritDamRating { get { return IntValues.ContainsKey(374) ? IntValues[374] : -1; } } + + public int CritDamResistRating { get { return IntValues.ContainsKey(375) ? IntValues[375] : -1; } } + + public int HealBoostRating { get { return IntValues.ContainsKey(376) ? IntValues[376] : -1; } } + + public int VitalityRating { get { return IntValues.ContainsKey(379) ? IntValues[379] : -1; } } + + /// The sum of every rating on the item, or -1 when it carries none. + public int TotalRating + { + get + { + if (DamRating == -1 && DamResistRating == -1 && CritRating == -1 && CritResistRating == -1 && CritDamRating == -1 && CritDamResistRating == -1 && HealBoostRating == -1 && VitalityRating == -1) + return -1; + + return Math.Max(DamRating, 0) + Math.Max(DamResistRating, 0) + Math.Max(CritRating, 0) + Math.Max(CritResistRating, 0) + Math.Max(CritDamRating, 0) + Math.Max(CritDamResistRating, 0) + Math.Max(HealBoostRating, 0) + Math.Max(VitalityRating, 0); + } + } + + /// + /// The armor level less buffs and tinks (20 each, the imbue excluded), plus + /// any impenetrability cantrip; a non-loot item (no material) loses 200. + /// + public int CalcedStartingArmorLevel + { + get + { + int armorFromTinks = 0; + int armorFromBuffs = 0; + + if (Tinks > 0 && ArmorLevel > 0) + armorFromTinks = (Imbue != null) ? (Tinks - 1) * 20 : Tinks * 20; + + if ((!IntValues.ContainsKey(131) || IntValues[131] == 0) && ArmorLevel > 0) + armorFromTinks = 200; + + foreach (int spell in ActiveSpells) + { + foreach (KeyValuePair> effect in ItemDictionaries.LongValueKeySpellEffects) + { + if (spell == effect.Key && effect.Value.Key == 28) + armorFromBuffs += effect.Value.Change; + } + } + + foreach (int spell in Spells) + { + foreach (KeyValuePair> effect in ItemDictionaries.LongValueKeySpellEffects) + { + if (spell == effect.Key && effect.Value.Key == 28) + armorFromBuffs -= effect.Value.Bonus; + } + } + + return ArmorLevel - armorFromTinks - armorFromBuffs; + } + } + + /// A melee weapon's best damage over time after ten tinks, from its variance, max damage and tinks. + public double CalcedBuffedTinkedDoT + { + get + { + if (!DoubleValues.ContainsKey(167772171) || !IntValues.ContainsKey(218103842)) + return -1; + + double variance = DoubleValues.ContainsKey(167772171) ? DoubleValues[167772171] : 0; + int maxDamage = GetBuffedIntValueKey(218103842); + + int numberOfTinksLeft = Math.Max(10 - Math.Max(Tinks, 0), 0); + + if (!IntValues.ContainsKey(179) || IntValues[179] == 0) + numberOfTinksLeft--; + + if (!IntValues.ContainsKey(131) || IntValues[131] == 0) + numberOfTinksLeft = 0; + + for (int i = 1; i <= numberOfTinksLeft; i++) + { + double ironTinkDoT = CalculateDamageOverTime(maxDamage + 24 + 1, variance); + double graniteTinkDoT = CalculateDamageOverTime(maxDamage + 24, variance * .8); + + if (ironTinkDoT >= graniteTinkDoT) + maxDamage++; + else + variance *= .8; + } + + return CalculateDamageOverTime(maxDamage + 24, variance); + } + } + + public double CalcedBuffedMissileDamage { get { if (!IntValues.ContainsKey(218103842) || !DoubleValues.ContainsKey(167772174) || !IntValues.ContainsKey(204)) return -1; return GetBuffedIntValueKey(218103842) + (((GetBuffedDoubleValueKey(167772174) - 1) * 100) / 3) + GetBuffedIntValueKey(204); } } + + public double BuffedElementalDamageVersusMonsters { get { return GetBuffedDoubleValueKey(152, -1); } } + + public double BuffedAttackBonus { get { return GetBuffedDoubleValueKey(167772172, -1); } } + + public double BuffedMeleeDefenseBonus { get { return GetBuffedDoubleValueKey(29, -1); } } + + public double BuffedManaCBonus { get { return GetBuffedDoubleValueKey(144, -1); } } + + public int GetBuffedIntValueKey(int key, int defaultValue = 0) + { + if (!IntValues.ContainsKey(key)) + return defaultValue; + + int value = IntValues[key]; + + foreach (int spell in ActiveSpells) + { + if (ItemDictionaries.LongValueKeySpellEffects.ContainsKey(spell) && ItemDictionaries.LongValueKeySpellEffects[spell].Key == key) + value -= ItemDictionaries.LongValueKeySpellEffects[spell].Change; + } + + foreach (int spell in Spells) + { + if (ItemDictionaries.LongValueKeySpellEffects.ContainsKey(spell) && ItemDictionaries.LongValueKeySpellEffects[spell].Key == key) + value += ItemDictionaries.LongValueKeySpellEffects[spell].Bonus; + } + + return value; + } + + public double GetBuffedDoubleValueKey(int key, double defaultValue = 0) + { + if (!DoubleValues.ContainsKey(key)) + return defaultValue; + + double value = DoubleValues[key]; + + foreach (int spell in ActiveSpells) + { + if (ItemDictionaries.DoubleValueKeySpellEffects.ContainsKey(spell) && ItemDictionaries.DoubleValueKeySpellEffects[spell].Key == key) + { + if (Math.Abs(ItemDictionaries.DoubleValueKeySpellEffects[spell].Change - 1) < double.Epsilon) + value /= ItemDictionaries.DoubleValueKeySpellEffects[spell].Change; + else + value -= ItemDictionaries.DoubleValueKeySpellEffects[spell].Change; + } + } + + foreach (int spell in Spells) + { + if (ItemDictionaries.DoubleValueKeySpellEffects.ContainsKey(spell) && ItemDictionaries.DoubleValueKeySpellEffects[spell].Key == key && Math.Abs(ItemDictionaries.DoubleValueKeySpellEffects[spell].Bonus - 0) > double.Epsilon) + { + if (Math.Abs(ItemDictionaries.DoubleValueKeySpellEffects[spell].Change - 1) < double.Epsilon) + value *= ItemDictionaries.DoubleValueKeySpellEffects[spell].Bonus; + else + value += ItemDictionaries.DoubleValueKeySpellEffects[spell].Bonus; + } + } + + return value; + } + + /// maxDamage * ((1 - critChance) * (2 - variance) / 2 + (critChance * critMultiplier)). + public static double CalculateDamageOverTime(int maxDamage, double variance, double critChance = .1, double critMultiplier = 2) + { + return maxDamage * ((1 - critChance) * (2 - variance) / 2 + (critChance * critMultiplier)); + } + + public override string? ToString() + { + return Name; + } +}