diff --git a/CraftMagicItems.sln b/CraftMagicItems.sln index fc80f38..48f8197 100644 --- a/CraftMagicItems.sln +++ b/CraftMagicItems.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.30104.148 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CraftMagicItems", "CraftMagicItems\CraftMagicItems.csproj", "{9379C37F-B226-4F81-893D-372F0CCEAAE5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CraftMagicItemsTests", "CraftMagicItemsTests\CraftMagicItemsTests.csproj", "{FDC2E2FA-48A9-4F1B-9C9B-1B1D43628884}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug (2.1)|Any CPU = Debug (2.1)|Any CPU @@ -22,6 +24,14 @@ Global {9379C37F-B226-4F81-893D-372F0CCEAAE5}.Release|Any CPU.ActiveCfg = Release|Any CPU {9379C37F-B226-4F81-893D-372F0CCEAAE5}.Release|Any CPU.Build.0 = Release|Any CPU {9379C37F-B226-4F81-893D-372F0CCEAAE5}.Release|Any CPU.Deploy.0 = Release|Any CPU + {FDC2E2FA-48A9-4F1B-9C9B-1B1D43628884}.Debug (2.1)|Any CPU.ActiveCfg = Debug (2.1)|Any CPU + {FDC2E2FA-48A9-4F1B-9C9B-1B1D43628884}.Debug (2.1)|Any CPU.Build.0 = Debug (2.1)|Any CPU + {FDC2E2FA-48A9-4F1B-9C9B-1B1D43628884}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FDC2E2FA-48A9-4F1B-9C9B-1B1D43628884}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FDC2E2FA-48A9-4F1B-9C9B-1B1D43628884}.Release (2.1)|Any CPU.ActiveCfg = Release|Any CPU + {FDC2E2FA-48A9-4F1B-9C9B-1B1D43628884}.Release (2.1)|Any CPU.Build.0 = Release|Any CPU + {FDC2E2FA-48A9-4F1B-9C9B-1B1D43628884}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FDC2E2FA-48A9-4F1B-9C9B-1B1D43628884}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/CraftMagicItems/Accessors.cs b/CraftMagicItems/Accessors.cs index a28cfb0..9af1c43 100644 --- a/CraftMagicItems/Accessors.cs +++ b/CraftMagicItems/Accessors.cs @@ -37,74 +37,39 @@ namespace CraftMagicItems { public delegate TResult FastStaticInvoker(T1 arg1, T2 arg2, T3 arg3); public class Accessors { - public static Harmony12.AccessTools.FieldRef CreateFieldRef(string name) { + public static HarmonyLib.AccessTools.FieldRef CreateFieldRef(string name) { var classType = typeof(TClass); var resultType = typeof(TResult); - var fieldInfo = Harmony12.AccessTools.Field(classType, name); + var fieldInfo = HarmonyLib.AccessTools.Field(classType, name); if (fieldInfo == null) { throw new Exception($"{classType} does not contain field {name}"); } - if (!resultType.IsAssignableFrom(fieldInfo.FieldType)) { + if (!resultType.IsAssignableFrom(fieldInfo.FieldType) && (!fieldInfo.FieldType.IsEnum || resultType != typeof(int))) { throw new InvalidCastException($"Cannot cast field type {resultType} as {fieldInfo.FieldType} for class {classType} field {name}"); } - var fieldRef = Harmony12.AccessTools.FieldRefAccess(name); + var fieldRef = HarmonyLib.AccessTools.FieldRefAccess(name); return fieldRef; } - public static FastGetter CreateGetter(Type classType, Type resultType, string name) { - var fieldInfo = Harmony12.AccessTools.Field(classType, name); - var propInfo = Harmony12.AccessTools.Property(classType, name); - if (fieldInfo == null && propInfo == null) { - throw new Exception($"{classType} does not contain field or property {name}"); - } - - bool isProp = propInfo != null; - Type memberType = isProp ? propInfo.PropertyType : fieldInfo.FieldType; - string memberTypeName = isProp ? "property" : "field"; - if (!resultType.IsAssignableFrom(memberType)) { - throw new InvalidCastException($"Cannot cast field type {resultType} as {memberType} for class {classType} {memberTypeName} {name}"); - } - - var handler = isProp ? Harmony12.FastAccess.CreateGetterHandler(propInfo) : Harmony12.FastAccess.CreateGetterHandler(fieldInfo); - return new FastGetter(handler); - } - - public static FastGetter CreateGetter(string name) { + public static FastSetter CreateSetter(string name) { var classType = typeof(TClass); - var resultType = typeof(TResult); - var handler = CreateGetter(classType, resultType, name); - return instance => (TResult) handler.Invoke(instance); - } - - public static FastSetter CreateSetter(Type classType, Type valueType, string name) { - var propertyInfo = Harmony12.AccessTools.Property(classType, name); - var fieldInfo = Harmony12.AccessTools.Field(classType, name); - if (propertyInfo == null && fieldInfo == null) { + var propertySetter = HarmonyLib.AccessTools.PropertySetter(classType, name); + if (propertySetter == null) { throw new Exception($"{classType} does not contain a field or property {name}"); } - - var isProperty = propertyInfo != null; - var memberType = isProperty ? propertyInfo.PropertyType : fieldInfo.FieldType; - var memberTypeName = isProperty ? "property" : "field"; + var propertyInfo = HarmonyLib.AccessTools.Property(classType, name); + var memberType = propertyInfo.PropertyType; + var valueType = typeof(TValue); if (!valueType.IsAssignableFrom(memberType) && (!memberType.IsEnum || valueType != typeof(int))) { - throw new Exception($"Cannot cast property type {valueType} as {memberType} for class {classType} {memberTypeName} {name}"); + throw new Exception($"Cannot cast property type {valueType} as {memberType} for class {classType} property {name}"); } - - var handler = isProperty ? Harmony12.FastAccess.CreateSetterHandler(propertyInfo) : Harmony12.FastAccess.CreateSetterHandler(fieldInfo); - return new FastSetter(handler); - } - - public static FastSetter CreateSetter(string name) { - var classType = typeof(TClass); - var valueType = typeof(TValue); - var handler = CreateSetter(classType, valueType, name); - return (instance, value) => handler.Invoke(instance, value); + return new FastSetter(HarmonyLib.AccessTools.MethodDelegate>(propertySetter)); } private static MethodInfo GetMethodInfoValidated(Type classType, string name, Type resultType, Type[] args, Type[] typeArgs) { - var methodInfo = Harmony12.AccessTools.Method(classType, name, args, typeArgs); + var methodInfo = HarmonyLib.AccessTools.Method(classType, name, args, typeArgs); if (methodInfo == null) { var argString = string.Join(", ", args.Select(t => t.ToString())); throw new Exception($"{classType} does not contain method {name} with arguments {argString}"); @@ -119,7 +84,7 @@ private static MethodInfo GetMethodInfoValidated(Type classType, string name, Ty private static FastInvoker CreateInvoker(Type classType, string name, Type resultType, Type[] args, Type[] typeArgs = null) { var methodInfo = GetMethodInfoValidated(classType, name, resultType, args, typeArgs); - return new FastInvoker(Harmony12.MethodInvoker.GetHandler(methodInfo)); + return new FastInvoker(HarmonyLib.MethodInvoker.GetHandler(methodInfo)); } public static FastInvoker CreateInvoker(string name) { @@ -156,9 +121,9 @@ public static FastInvoker CreateInvoker CreateStaticInvoker(Type classType, string name) { diff --git a/CraftMagicItems/AfterBuild.bat b/CraftMagicItems/AfterBuild.bat index 22c7b99..68323d6 100644 --- a/CraftMagicItems/AfterBuild.bat +++ b/CraftMagicItems/AfterBuild.bat @@ -6,10 +6,10 @@ xcopy CraftMagicItems.dll CraftMagicItems || goto :error xcopy ..\..\..\Info.json CraftMagicItems || goto :error xcopy /E /I ..\..\..\L10n CraftMagicItems\L10n || goto :error xcopy /E /I ..\..\..\Icons CraftMagicItems\Icons || goto :error -xcopy /E /I ..\..\..\Data CraftMagicItems\Data || goto :error +xcopy /E /I Data CraftMagicItems\Data || goto :error "C:\Program Files\7-Zip\7z.exe" a CraftMagicItems.zip CraftMagicItems || goto :error -"C:\Program Files\7-Zip\7z.exe" a CraftMagicItems-Source.zip ..\..\*.cs ..\..\..\L10n ..\..\..\Icons ..\..\..\Data || goto :error +"C:\Program Files\7-Zip\7z.exe" a CraftMagicItems-Source.zip ..\..\*.cs ..\..\..\L10n ..\..\..\Icons Data || goto :error goto :EOF diff --git a/CraftMagicItems/BondedItemComponent.cs b/CraftMagicItems/BondedItemComponent.cs index 1c4f7f0..bebb387 100644 --- a/CraftMagicItems/BondedItemComponent.cs +++ b/CraftMagicItems/BondedItemComponent.cs @@ -1,3 +1,4 @@ +using CraftMagicItems.Localization; using Kingmaker; using Kingmaker.Items; using Kingmaker.Items.Slots; @@ -43,7 +44,7 @@ public void HandleEquipmentSlotUpdated(ItemSlot slot, ItemEntity previousItem) { } // If the owner of a bonded object casts a spell when not wielding it, they need to make a Concentration check or lose the spell. - [Harmony12.HarmonyPatch(typeof(UnitUseAbility), "MakeConcentrationCheckIfCastingIsDifficult")] + [HarmonyLib.HarmonyPatch(typeof(UnitUseAbility), "MakeConcentrationCheckIfCastingIsDifficult")] // ReSharper disable once UnusedMember.Local private static class UnitUseAbilityMakeConcentrationCheckIfCastingIsDifficultPatch { // ReSharper disable once UnusedMember.Local @@ -52,7 +53,7 @@ private static void Postfix(UnitUseAbility __instance) { var bondedComponent = Main.GetBondedItemComponentForCaster(caster.Descriptor); if (bondedComponent != null && bondedComponent.ownerItem != null && bondedComponent.ownerItem.Wielder != caster.Descriptor) { Main.AddBattleLogMessage( - Main.L10NFormat(caster, "craftMagicItems-logMessage-not-wielding-bonded-item", bondedComponent.ownerItem.Name), + LocalizationHelper.FormatLocalizedString(caster, "craftMagicItems-logMessage-not-wielding-bonded-item", bondedComponent.ownerItem.Name), new L10NString("craftMagicItems-bonded-item-glossary")); // Concentration checks have no way of overriding the DC, so contrive some fake damage to give a DC of 20 + spell level. var ruleDamage = new RuleDealDamage(caster, caster, null); diff --git a/CraftMagicItems/Config/DictionaryData.cs b/CraftMagicItems/Config/DictionaryData.cs new file mode 100644 index 0000000..fd6d239 --- /dev/null +++ b/CraftMagicItems/Config/DictionaryData.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using Kingmaker.Blueprints.Items; +using Kingmaker.Blueprints.Items.Equipment; +using Kingmaker.Enums.Damage; + +namespace CraftMagicItems.Config +{ + /// + /// Class containing all of the loaded dictionaries of data that are loaded + /// from config files or game resources + /// + public class DictionaryData + { + /// Collection of items that are related to spells + public readonly Dictionary>> SpellIdToItem; + + /// Crafting data loaded fron JSON files and its hierarchy + public readonly Dictionary> SubCraftingData; + + /// Collection of items matching type of blueprint (shield, armor, weapon) + public readonly Dictionary TypeToItem; + + /// Collection of various item blueprints, keyed on enchantment blueprint ID + public readonly Dictionary> EnchantmentIdToItem; + + /// Collection of various recipies, keyed on enchantment blueprint ID + public readonly Dictionary> EnchantmentIdToRecipe; + + /// Collection of various recipies, keyed on physical material + public readonly Dictionary> MaterialToRecipe; + + /// Collection of various enchantment costs, keyed on enchantment blueprint ID + public readonly Dictionary EnchantmentIdToCost; + + /// Array of item crafting data read from JSON file + public ItemCraftingData[] ItemCraftingData; + + /// Array of custom loot data read from JSON file + public CustomLootItem[] CustomLootItems; + + /// Default constructor + public DictionaryData() + { + SpellIdToItem = new Dictionary>>(); + SubCraftingData = new Dictionary>(); + TypeToItem = new Dictionary(); + EnchantmentIdToItem = new Dictionary>(); + EnchantmentIdToRecipe = new Dictionary>(); + MaterialToRecipe = new Dictionary>(); + EnchantmentIdToCost = new Dictionary(); + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Config/Selections.cs b/CraftMagicItems/Config/Selections.cs new file mode 100644 index 0000000..25d83ea --- /dev/null +++ b/CraftMagicItems/Config/Selections.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; +using CraftMagicItems.UI; +using Kingmaker.Blueprints.Items; +using Kingmaker.Blueprints.Items.Equipment; +using Kingmaker.EntitySystem.Entities; + +namespace CraftMagicItems.Config +{ + /// Class containing the selections from the UI + public class Selections + { + /// Currently selected index for a given setting + /// + /// TODO: This should probably be broken up for ease of maintenance + /// + public readonly Dictionary SelectedIndex = new Dictionary(); + + /// + /// The currently-selected caster level with which to create an item (wand, scroll, potion, item effect, etc.) + /// i.e.: "cast {spell} as an Nth level caster" + /// + public int SelectedCasterLevel; + + /// Flag indicating whether to ONLY display currently prepared/available spells + public bool SelectedShowPreparedSpells; + + /// Is a double weapon's second side selected? + public bool SelectedDoubleWeaponSecondEnd; + + /// Is a shield's weapon (bash, spikes, etc.) seleted? + public bool SelectedShieldWeapon; + + /// Selected times that an item can cast a given spell per day + public int SelectedCastsPerDay; + + /// Currently selected base item blueprint + public BlueprintItemEquipment SelectedBaseBlueprint; + + /// User-entered custom name for the item + public string SelectedCustomName; + + /// Has the user selected to bond with a new item + public bool SelectedBondWithNewObject; + + /// Currently selected crafter/caster + public UnitEntityData CurrentCaster; + + /// Currently-selected crafting section to render + public OpenSection CurrentSection = OpenSection.CraftMagicItemsSection; + + /// Blueprint that is currently being upgraded + public BlueprintItem UpgradingBlueprint; + + + /// Retrieves the select index in matching the key of + /// Label used as a key to search on + /// The selected index value, or 0 if cannot be found + public int GetSelectionIndex(string label) + { + return SelectedIndex.ContainsKey(label) ? SelectedIndex[label] : 0; + } + + /// Sets the matching the key of + /// Label used as a key to search on + /// Value to assign + public void SetSelectionIndex(string label, int value) + { + SelectedIndex[label] = value; + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Constants/Areas.cs b/CraftMagicItems/Constants/Areas.cs new file mode 100644 index 0000000..fde61de --- /dev/null +++ b/CraftMagicItems/Constants/Areas.cs @@ -0,0 +1,24 @@ +namespace CraftMagicItems.Constants +{ + /// Class containing unique identifiers for area blueprints + public static class Areas + { + /// Collection of blueprint unique identifiers for "safe" areas + public static readonly string[] SafeBlueprintAreaGuids = + { + "141f6999dada5a842a46bb3f029c287a", // Dire Narlmarches village + "e0852647faf68a641a0a5ec5436fc0bf", // Dunsward village + "537acbd9039b6a04f915dfc21572affb", // Glenebon village + "3ddf191773e8c2f448d31289b8d654bf", // Kamelands village + "f1b76870cc69e6a479c767cbe3c8a8ca", // North Narlmarches village + "ea3788bcdf33d884baffc34440ff620f", // Outskirts village + "e7292eab463c4924c8f14548545e25b7", // Silverstep village + "7c4a954c65e8d7146a6edc00c498a582", // South Narlmarches village + "7d9616b3807840c47ba3b2ab380c55a0", // Tors of Levenies village + "653811192a3fcd148816384a9492bd08", // Secluded Lodge + "fd1b6fa9f788ca24e86bd922a10da080", // Tenebrous Depths start hub + "c49315fe499f0e5468af6f19242499a2", // Tenebrous Depths start hub (Roguelike) + // TODO: camp outside the capital? + }; + } +} \ No newline at end of file diff --git a/CraftMagicItems/Constants/ClassBlueprints.cs b/CraftMagicItems/Constants/ClassBlueprints.cs new file mode 100644 index 0000000..0aa0f26 --- /dev/null +++ b/CraftMagicItems/Constants/ClassBlueprints.cs @@ -0,0 +1,15 @@ +namespace CraftMagicItems.Constants +{ + /// Contains constants for class/archetype blueprint unique identifiers + public static class ClassBlueprints + { + /// Blueprint for Alchemist class progression + public const string AlchemistProgressionGuid = "efd55ff9be2fda34981f5b9c83afe4f1"; + + /// Blueprint for Alchemist Grenadier archetype + public const string AlchemistGrenadierArchetypeGuid = "6af888a7800b3e949a40f558ff204aae"; + + /// Blueprint for Wizard Scroll Savant archetype + public const string ScrollSavantArchetypeGuid = "f43c78692a4e10d43a38bd6aedf53c1b"; + } +} \ No newline at end of file diff --git a/CraftMagicItems/Constants/DefaultCosts.cs b/CraftMagicItems/Constants/DefaultCosts.cs new file mode 100644 index 0000000..0373cca --- /dev/null +++ b/CraftMagicItems/Constants/DefaultCosts.cs @@ -0,0 +1,27 @@ +namespace CraftMagicItems.Constants +{ + /// Default costs (in Gold/GP) of items and such + public static class DefaultCosts + { + /// Cost of applying masterwork to armor + public const int ArmorMasterworkCost = 150; + + /// Cost of applying masterwork to a weapon + public const int WeaponMasterworkCost = 300; + + /// Cost of applying adamantine + public const int Adamantine = 3000; + + /// Cost of applying mithral per pound + public const int MithralPerPound = 500; + + /// Default cost for adding a +N enchantment to a weapon + public const int WeaponPlusCost = 2000; + + /// Default cost for adding a +N enchantment to an armor + public const int ArmorPlusCost = 1000; + + /// Default cost for adding a +N enchantment to an Amulet of Mighty Fists + public const int UnarmedPlusCost = 4000; + } +} \ No newline at end of file diff --git a/CraftMagicItems/Constants/DifficultyClass.cs b/CraftMagicItems/Constants/DifficultyClass.cs new file mode 100644 index 0000000..30b2c85 --- /dev/null +++ b/CraftMagicItems/Constants/DifficultyClass.cs @@ -0,0 +1,15 @@ +namespace CraftMagicItems.Constants +{ + /// Constants class containing modifiers and penalties to crafting DC + public static class DifficultyClass + { + /// Additional DC modifier for missing prerequisites to crafting + public const int MissingPrerequisiteDCModifier = 5; + + /// Additional DC modifier for a required spell from a Wizard's opposition school of magic + public const int OppositionSchoolDCModifier = 4; + + /// Penalty to progress that can be made while adventuring + public const int AdventuringProgressPenalty = 4; + } +} \ No newline at end of file diff --git a/CraftMagicItems/Constants/EnchantmentBlueprints.cs b/CraftMagicItems/Constants/EnchantmentBlueprints.cs new file mode 100644 index 0000000..20442dc --- /dev/null +++ b/CraftMagicItems/Constants/EnchantmentBlueprints.cs @@ -0,0 +1,50 @@ +namespace CraftMagicItems.Constants +{ + /// Class containing constants for identifying blueprint unique identifiers for item enchantments + public static class EnchantmentBlueprints + { + /// Unique identifier for the Longshank-bane weapon enchantment + public const string LongshankBaneGuid = "92a1f5db1a03c5b468828c25dd375806"; + + /// Collection of unique identifiers for enchantments applicable to both melee weapons and Amulet of Mighty Fists + public static readonly UnarmedStrikeEnchantment[] ItemEnchantmentGuids = + { + new UnarmedStrikeEnchantment + { + WeaponEnchantmentGuid = "d42fc23b92c640846ac137dc26e000d4", + UnarmedEnchantmentGuid = "da7d830b3f75749458c2e51524805560", + Description = "Enchantment +1" + }, + new UnarmedStrikeEnchantment + { + WeaponEnchantmentGuid = "eb2faccc4c9487d43b3575d7e77ff3f5", + UnarmedEnchantmentGuid = "49f9befa0e77cd5428ca3b28fd66a54e", + Description = "Enchantment +2" + }, + new UnarmedStrikeEnchantment + { + WeaponEnchantmentGuid = "80bb8a737579e35498177e1e3c75899b", + UnarmedEnchantmentGuid = "bae627dfb77c2b048900f154719ca07b", + Description = "Enchantment +3" + }, + new UnarmedStrikeEnchantment + { + WeaponEnchantmentGuid = "783d7d496da6ac44f9511011fc5f1979", + UnarmedEnchantmentGuid = "a4016a5d78384a94581497d0d135d98b", + Description = "Enchantment +4" + }, + new UnarmedStrikeEnchantment + { + WeaponEnchantmentGuid = "bdba267e951851449af552aa9f9e3992", + UnarmedEnchantmentGuid = "c3ad7f708c573b24082dde91b081ca5f", + Description = "Enchantment +5" + }, + new UnarmedStrikeEnchantment + { + WeaponEnchantmentGuid = "a36ad92c51789b44fa8a1c5c116a1328", + UnarmedEnchantmentGuid = "90316f5801dbe4748a66816a7c00380c", + Description = "Agile" + }, + }; + } +} \ No newline at end of file diff --git a/CraftMagicItems/Constants/Features.cs b/CraftMagicItems/Constants/Features.cs new file mode 100644 index 0000000..3f2f5a1 --- /dev/null +++ b/CraftMagicItems/Constants/Features.cs @@ -0,0 +1,30 @@ +using Kingmaker.Blueprints.Classes; + +namespace CraftMagicItems.Constants +{ + /// Class containing constants for abilities and Feats + public static class Features + { + /// Collection of that crafting feats can be added to + public static readonly FeatureGroup[] CraftingFeatGroups = { FeatureGroup.Feat, FeatureGroup.WizardFeat }; + + /// Blueprint unique identifier for the items that are "martial" weapons + /// Used to determine which mundane items are martial under special conditions + public const string MartialWeaponProficiencies = "203992ef5b35c864390b4e4a1e200629"; + + /// Blueprint unique identifier for the Channel Energy ability applied to a character + public const string ChannelEnergyFeatureGuid = "a79013ff4bcd4864cb669622a29ddafb"; + + /// Blueprint unique identifier for the Shield Master feat + public const string ShieldMasterGuid = "dbec636d84482944f87435bd31522fcc"; + + /// Blueprint unique identifier for the Prodigious Two-Weapon Fighting feat + public const string ProdigiousTwoWeaponFightingGuid = "ddba046d03074037be18ad33ea462028"; + + /// Blueprint unique identifiers for applied class features that have bonded item features added by the mod + public static readonly string[] BondedItemFeatures = { + "2fb5e65bd57caa943b45ee32d825e9b9", + "aa34ca4f3cd5e5d49b2475fcfdf56b24" + }; + } +} \ No newline at end of file diff --git a/CraftMagicItems/Constants/ItemQualityBlueprints.cs b/CraftMagicItems/Constants/ItemQualityBlueprints.cs new file mode 100644 index 0000000..28858cb --- /dev/null +++ b/CraftMagicItems/Constants/ItemQualityBlueprints.cs @@ -0,0 +1,21 @@ +namespace CraftMagicItems.Constants +{ + /// Class containing constants for identifying blueprint unique identifiers for items + public static class ItemQualityBlueprints + { + /// Blueprint guid for masterwork quality items + public const string MasterworkGuid = "6b38844e2bffbac48b63036b66e735be"; + + /// Blueprint for mithral item special material/enchantment + public const string MithralArmorEnchantmentGuid = "7b95a819181574a4799d93939aa99aff"; + + /// Blueprint guid for oversized items + public const string OversizedGuid = "d8e1ebc1062d8cc42abff78783856b0d"; + + /// Blueprint guid for light shields + public const string WeaponLightShieldGuid = "1fd965e522502fe479fdd423cca07684"; + + /// Blueprint guid for heavy shields + public const string WeaponHeavyShieldGuid = "be9b6408e6101cb4997a8996484baf19"; + } +} \ No newline at end of file diff --git a/CraftMagicItems/Constants/LocalizedStringBlueprints.cs b/CraftMagicItems/Constants/LocalizedStringBlueprints.cs new file mode 100644 index 0000000..ba73161 --- /dev/null +++ b/CraftMagicItems/Constants/LocalizedStringBlueprints.cs @@ -0,0 +1,21 @@ +using CraftMagicItems.Localization; +using Kingmaker.Localization; + +namespace CraftMagicItems.Constants +{ + /// Constants class for references to localized strings + public static class LocalizedStringBlueprints + { + /// Localized "Caster Level" reference + public static readonly LocalizedString CasterLevelLocalized = new L10NString("dfb34498-61df-49b1-af18-0a84ce47fc98"); + + /// Localized "<b></b> uses item: <b></b>." reference + public static readonly LocalizedString CharacterUsedItemLocalized = new L10NString("be7942ed-3af1-4fc7-b20b-41966d2f80b7"); + + /// Localized "Shield Bash" reference + public static readonly LocalizedString ShieldBashLocalized = new L10NString("314ff56d-e93b-4915-8ca4-24a7670ad436"); + + /// Localized "Qualities" reference + public static readonly LocalizedString QualitiesLocalized = new L10NString("0f84fde9-14ca-4e2f-9c82-b2522039dbff"); + } +} \ No newline at end of file diff --git a/CraftMagicItems/Constants/MechanicsBlueprints.cs b/CraftMagicItems/Constants/MechanicsBlueprints.cs new file mode 100644 index 0000000..f9c5f7c --- /dev/null +++ b/CraftMagicItems/Constants/MechanicsBlueprints.cs @@ -0,0 +1,9 @@ +namespace CraftMagicItems.Constants +{ + /// Class containing constants for game mechanics + public static class MechanicsBlueprints + { + /// Constant for Two-weapon fighting game mechanics + public const string TwoWeaponFightingBasicMechanicsGuid = "6948b379c0562714d9f6d58ccbfa8faa"; + } +} \ No newline at end of file diff --git a/CraftMagicItems/Constants/UnarmedStrikeEnchantment.cs b/CraftMagicItems/Constants/UnarmedStrikeEnchantment.cs new file mode 100644 index 0000000..25a6622 --- /dev/null +++ b/CraftMagicItems/Constants/UnarmedStrikeEnchantment.cs @@ -0,0 +1,15 @@ +namespace CraftMagicItems.Constants +{ + /// Structure defining enchantments that exist for both melee and unarmed strikes + public struct UnarmedStrikeEnchantment + { + /// Used for descriptive purposes rather than a comment in code that someone might delete + public string Description; + + /// The unique identifier of the weapon enchantment blueprint to copy data from + public string WeaponEnchantmentGuid; + + /// The unique identifier of the unarmed strike blueprint to copy data into + public string UnarmedEnchantmentGuid; + } +} \ No newline at end of file diff --git a/CraftMagicItems/Constants/VisualAdjustmentPatches.cs b/CraftMagicItems/Constants/VisualAdjustmentPatches.cs new file mode 100644 index 0000000..bec9e54 --- /dev/null +++ b/CraftMagicItems/Constants/VisualAdjustmentPatches.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using CraftMagicItems.Patches; + +namespace CraftMagicItems.Constants +{ + /// Constants class containing definitions + public static class VisualAdjustmentPatches + { + /// Collection of patches for visual adjustments to displayed items + public static IEnumerable LeftHandedWeaponPatchList + { + get + { + return new[] + { + DuelingSword, + Tongi, + Falcata, + Estoc, + Rapier, + HeavyPick, + Trident, + HeavyMace, + HeavyFlail, + }; + } + } + + /// Dueling sword patch + private static readonly IkPatch DuelingSword = new IkPatch("a6f7e3dc443ff114ba68b4648fd33e9f", 0.00f, -0.10f, 0.01f); + + /// Tongi patch + private static readonly IkPatch Tongi = new IkPatch("13fa38737d46c9e4abc7f4d74aaa59c3", 0.00f, -0.36f, 0.00f); + + /// Falcata patch + private static readonly IkPatch Falcata = new IkPatch("1af5621e2ae551e42bd1dd6744d98639", 0.00f, -0.07f, 0.00f); + + /// Estoc patch + private static readonly IkPatch Estoc = new IkPatch("d516765b3c2904e4a939749526a52a9a", 0.00f, -0.15f, 0.00f); + + /// Rapier patch + private static readonly IkPatch Rapier = new IkPatch("2ece38f30500f454b8569136221e55b0", 0.00f, -0.08f, 0.00f); + + /// Heavy pick patch + private static readonly IkPatch HeavyPick = new IkPatch("a492410f3d65f744c892faf09daad84a", 0.00f, -0.20f, 0.00f); + + /// Trident patch + private static readonly IkPatch Trident = new IkPatch("6ff66364e0a2c89469c2e52ebb46365e", 0.00f, -0.10f, 0.00f); + + /// Heavy mace patch + private static readonly IkPatch HeavyMace = new IkPatch("d5a167f0f0208dd439ec7481e8989e21", 0.00f, -0.08f, 0.00f); + + /// Heavy flail patch + private static readonly IkPatch HeavyFlail = new IkPatch("8fefb7e0da38b06408f185e29372c703", -0.14f, 0.00f, 0.00f); + } +} \ No newline at end of file diff --git a/CraftMagicItems/CraftMagicItems.csproj b/CraftMagicItems/CraftMagicItems.csproj index 89ba380..742cf62 100644 --- a/CraftMagicItems/CraftMagicItems.csproj +++ b/CraftMagicItems/CraftMagicItems.csproj @@ -60,9 +60,9 @@ - + False - ..\..\..\..\..\..\Program Files\UnityModManager-0.14.2a\0Harmony12.dll + ..\..\..\..\..\..\Program Files\UnityModManager\0Harmony.dll ..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Pathfinder Kingmaker\Kingmaker_Data\Managed\Assembly-CSharp.dll @@ -75,18 +75,7 @@ ..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Pathfinder Kingmaker\Kingmaker_Data\Managed\Newtonsoft.Json.dll - - - - - - - ..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Pathfinder Kingmaker\Kingmaker_Data\Managed\UnityEngine.dll - - - ..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Pathfinder Kingmaker\Kingmaker_Data\Managed\UnityEngine.AnimationModule.dll - ..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Pathfinder Kingmaker\Kingmaker_Data\Managed\UnityEngine.CoreModule.dll @@ -97,9 +86,6 @@ ..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Pathfinder Kingmaker\Kingmaker_Data\Managed\UnityEngine.IMGUIModule.dll - - ..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Pathfinder Kingmaker\Kingmaker_Data\Managed\UnityEngine.UI.dll - ..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Pathfinder Kingmaker\Kingmaker_Data\Managed\UnityModManager\UnityModManager.dll @@ -107,25 +93,122 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + - + + - + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + diff --git a/CraftMagicItems/CraftMagicItemsAccessors.cs b/CraftMagicItems/CraftMagicItemsAccessors.cs index 0482276..11535e9 100644 --- a/CraftMagicItems/CraftMagicItemsAccessors.cs +++ b/CraftMagicItems/CraftMagicItemsAccessors.cs @@ -29,115 +29,112 @@ namespace CraftMagicItems { * Spacehamster's idea: create reflection-based accessors up front, so the mod fails on startup if the Kingmaker code changes in an incompatible way. */ public class CraftMagicItemsAccessors { - public readonly FastGetter> GetSpellbookSpecialLists = - Accessors.CreateGetter>("m_SpecialLists"); + public readonly HarmonyLib.AccessTools.FieldRef> GetSpellbookSpecialLists = + Accessors.CreateFieldRef>("m_SpecialLists"); - public readonly FastGetter> GetFeatureCollectionFacts = - Accessors.CreateGetter>("m_Facts"); + public readonly HarmonyLib.AccessTools.FieldRef GetActionBarManagerNeedReset = Accessors.CreateFieldRef("m_NeedReset"); - public readonly FastGetter GetActionBarManagerNeedReset = Accessors.CreateGetter("m_NeedReset"); + public readonly HarmonyLib.AccessTools.FieldRef GetActionBarManagerSelected = + Accessors.CreateFieldRef("m_Selected"); - public readonly FastGetter GetActionBarManagerSelected = - Accessors.CreateGetter("m_Selected"); + public readonly HarmonyLib.AccessTools.FieldRef[]> GetSpellbookKnownSpells = + Accessors.CreateFieldRef[]>("m_KnownSpells"); - public readonly FastGetter[]> GetSpellbookKnownSpells = - Accessors.CreateGetter[]>("m_KnownSpells"); + public readonly HarmonyLib.AccessTools.FieldRef> GetSpellbookKnownSpellLevels = + Accessors.CreateFieldRef>("m_KnownSpellLevels"); - public readonly FastGetter> GetSpellbookKnownSpellLevels = - Accessors.CreateGetter>("m_KnownSpellLevels"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintUnitFactDisplayName = + Accessors.CreateFieldRef("m_DisplayName"); - public readonly FastSetter SetBlueprintUnitFactDisplayName = - Accessors.CreateSetter("m_DisplayName"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintUnitFactDescription = + Accessors.CreateFieldRef("m_Description"); - public readonly FastSetter SetBlueprintUnitFactDescription = - Accessors.CreateSetter("m_Description"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintUnitFactIcon = Accessors.CreateFieldRef("m_Icon"); - public readonly FastSetter SetBlueprintUnitFactIcon = Accessors.CreateSetter("m_Icon"); + public readonly HarmonyLib.AccessTools.FieldRef> SetBlueprintItemCachedEnchantments = + Accessors.CreateFieldRef>("m_CachedEnchantments"); - public readonly FastSetter SetBlueprintItemEquipmentUsableCost = - Accessors.CreateSetter("m_Cost"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemShieldArmorComponent = + Accessors.CreateFieldRef("m_ArmorComponent"); - public readonly FastSetter> SetBlueprintItemCachedEnchantments = - Accessors.CreateSetter>("m_CachedEnchantments"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemShieldWeaponComponent = + Accessors.CreateFieldRef("m_WeaponComponent"); - public readonly FastSetter SetBlueprintItemShieldArmorComponent = - Accessors.CreateSetter("m_ArmorComponent"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemWeaponDamageType = + Accessors.CreateFieldRef("m_DamageType"); - public readonly FastSetter SetBlueprintItemShieldWeaponComponent = - Accessors.CreateSetter("m_WeaponComponent"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemWeaponOverrideDamageType = + Accessors.CreateFieldRef("m_OverrideDamageType"); - public readonly FastSetter SetBlueprintItemWeaponDamageType = - Accessors.CreateSetter("m_DamageType"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemBaseDamage = + Accessors.CreateFieldRef("m_BaseDamage"); - public readonly FastSetter SetBlueprintItemWeaponOverrideDamageType = - Accessors.CreateSetter("m_OverrideDamageType"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemIcon = Accessors.CreateFieldRef("m_Icon"); - public readonly FastSetter SetBlueprintItemBaseDamage = - Accessors.CreateSetter("m_BaseDamage"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemEquipmentHandVisualParameters = + Accessors.CreateFieldRef("m_VisualParameters"); - public readonly FastSetter SetBlueprintItemIcon = Accessors.CreateSetter("m_Icon"); + public readonly HarmonyLib.AccessTools.FieldRef SetWeaponVisualParametersModel = + Accessors.CreateFieldRef("m_WeaponModel"); - public readonly FastSetter SetBlueprintItemEquipmentHandVisualParameters = - Accessors.CreateSetter("m_VisualParameters"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemEquipmentWeaponAnimationStyle = + Accessors.CreateFieldRef("m_WeaponAnimationStyle"); - public readonly FastSetter SetWeaponVisualParametersModel = - Accessors.CreateSetter("m_WeaponModel"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemArmorVisualParameters = + Accessors.CreateFieldRef("m_VisualParameters"); - public readonly FastSetter SetBlueprintItemEquipmentWeaponAnimationStyle = - Accessors.CreateSetter("m_WeaponAnimationStyle"); + //public readonly SetterHandler SetBlueprintBuffFlags = Accessors.CreateSetter("m_Flags"); + public void SetBlueprintBuffFlags(BlueprintBuff buff, int flags) { + HarmonyLib.AccessTools.Field(typeof(BlueprintBuff), "m_Flags").SetValue(buff, flags); + } - public readonly FastSetter SetBlueprintItemArmorVisualParameters = - Accessors.CreateSetter("m_VisualParameters"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemDisplayNameText = + Accessors.CreateFieldRef("m_DisplayNameText"); - public readonly FastSetter SetBlueprintBuffFlags = Accessors.CreateSetter("m_Flags"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemDescriptionText = + Accessors.CreateFieldRef("m_DescriptionText"); - public readonly FastSetter SetBlueprintItemDisplayNameText = - Accessors.CreateSetter("m_DisplayNameText"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemFlavorText = + Accessors.CreateFieldRef("m_FlavorText"); - public readonly FastSetter SetBlueprintItemDescriptionText = - Accessors.CreateSetter("m_DescriptionText"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemCost = Accessors.CreateFieldRef("m_Cost"); - public readonly FastSetter SetBlueprintItemFlavorText = - Accessors.CreateSetter("m_FlavorText"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemIsStackable = Accessors.CreateFieldRef("m_IsStackable"); - public readonly FastSetter SetBlueprintItemCost = Accessors.CreateSetter("m_Cost"); + public readonly HarmonyLib.AccessTools.FieldRef GetBlueprintItemEnchantmentEnchantName = + Accessors.CreateFieldRef("m_EnchantName"); - public readonly FastSetter SetBlueprintItemIsStackable = Accessors.CreateSetter("m_IsStackable"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemEnchantmentEnchantName = + Accessors.CreateFieldRef("m_EnchantName"); - public readonly FastGetter GetBlueprintItemEnchantmentEnchantName = - Accessors.CreateGetter("m_EnchantName"); + public readonly HarmonyLib.AccessTools.FieldRef GetBlueprintItemEnchantmentDescription = + Accessors.CreateFieldRef("m_Description"); - public readonly FastSetter SetBlueprintItemEnchantmentEnchantName = - Accessors.CreateSetter("m_EnchantName"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemEnchantmentDescription = + Accessors.CreateFieldRef("m_Description"); - public readonly FastGetter GetBlueprintItemEnchantmentDescription = - Accessors.CreateGetter("m_Description"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemEnchantmentPrefix = + Accessors.CreateFieldRef("m_Prefix"); - public readonly FastSetter SetBlueprintItemEnchantmentDescription = - Accessors.CreateSetter("m_Description"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemEnchantmentSuffix = + Accessors.CreateFieldRef("m_Suffix"); - public readonly FastSetter SetBlueprintItemEnchantmentPrefix = - Accessors.CreateSetter("m_Prefix"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemEnchantmentEnchantmentCost = + Accessors.CreateFieldRef("m_EnchantmentCost"); - public readonly FastSetter SetBlueprintItemEnchantmentSuffix = - Accessors.CreateSetter("m_Suffix"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemEnchantmentEnchantmentIdentifyDC = + Accessors.CreateFieldRef("m_IdentifyDC"); - public readonly FastSetter SetBlueprintItemEnchantmentEnchantmentCost = - Accessors.CreateSetter("m_EnchantmentCost"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintScriptableObjectAssetGuid = + Accessors.CreateFieldRef("m_AssetGuid"); - public readonly FastSetter SetBlueprintItemEnchantmentEnchantmentIdentifyDC = - Accessors.CreateSetter("m_IdentifyDC"); + public readonly HarmonyLib.AccessTools.FieldRef SetLootItemsPackFixedItem = Accessors.CreateFieldRef("m_Item"); - public readonly FastSetter SetBlueprintScriptableObjectAssetGuid = - Accessors.CreateSetter("m_AssetGuid"); - - public readonly FastSetter SetLootItemsPackFixedItem = Accessors.CreateSetter("m_Item"); - - public readonly FastSetter SetLootItemItem = Accessors.CreateSetter("m_Item"); + public readonly HarmonyLib.AccessTools.FieldRef SetLootItemItem = Accessors.CreateFieldRef("m_Item"); public readonly FastSetter SetRuleDealDamageDamage = Accessors.CreateSetter("Damage"); - public readonly FastSetter SetBlueprintItemWeight = Accessors.CreateSetter("m_Weight"); + public readonly HarmonyLib.AccessTools.FieldRef SetBlueprintItemWeight = Accessors.CreateFieldRef("m_Weight"); public readonly FastStaticInvoker CallUIUtilityItemGetQualities = Accessors.CreateStaticInvoker(typeof(UIUtilityItem), "GetQualities"); diff --git a/CraftMagicItems/CraftingLogic.cs b/CraftMagicItems/CraftingLogic.cs new file mode 100644 index 0000000..9c18d59 --- /dev/null +++ b/CraftMagicItems/CraftingLogic.cs @@ -0,0 +1,583 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using CraftMagicItems.Constants; +using CraftMagicItems.Localization; +using Kingmaker; +using Kingmaker.Blueprints; +using Kingmaker.Blueprints.Classes; +using Kingmaker.Blueprints.Classes.Spells; +using Kingmaker.Blueprints.Items.Equipment; +using Kingmaker.Blueprints.Root; +using Kingmaker.EntitySystem.Stats; +using Kingmaker.Enums; +using Kingmaker.Items; +using Kingmaker.UI; +using Kingmaker.UI.Common; +using Kingmaker.UI.Log; +using Kingmaker.UnitLogic; +using Kingmaker.UnitLogic.Abilities; +using Kingmaker.UnitLogic.Abilities.Blueprints; +using Kingmaker.UnitLogic.Alignments; +using Kingmaker.Utility; + +namespace CraftMagicItems +{ + /// Core logic class for crafting items + public static class CraftingLogic + { + public static void WorkOnProjects(UnitDescriptor caster, bool returningToCapital) + { + if (!caster.IsPlayerFaction || caster.State.IsDead || caster.State.IsFinallyDead) + { + return; + } + + Main.Selections.CurrentCaster = caster.Unit; + var withPlayer = Game.Instance.Player.PartyCharacters.Contains(caster.Unit); + var playerInCapital = Main.IsPlayerInCapital(); + // Only update characters in the capital when the player is also there. + if (!withPlayer && !playerInCapital) + { + // Character is back in the capital - skipping them for now. + return; + } + + var isAdventuring = withPlayer && !Main.IsPlayerSomewhereSafe(); + var timer = Main.GetCraftingTimerComponentForCaster(caster); + if (timer == null || timer.CraftingProjects.Count == 0) + { + // Character is not doing any crafting + return; + } + + // Round up the number of days, so caster makes some progress on a new project the first time they rest. + var interval = Game.Instance.Player.GameTime.Subtract(timer.LastUpdated); + var daysAvailableToCraft = (int)Math.Ceiling(interval.TotalDays); + if (daysAvailableToCraft <= 0) + { + if (isAdventuring) + { + Main.AddBattleLogMessage(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-not-full-day")); + } + + return; + } + + // Time passes for this character even if they end up making no progress on their projects. LastUpdated can go up to + // a day into the future, due to the rounding up of daysAvailableToCraft. + timer.LastUpdated += TimeSpan.FromDays(daysAvailableToCraft); + // Work on projects sequentially, skipping any that can't progress due to missing items, missing prerequisites or having too high a DC. + foreach (var project in timer.CraftingProjects.ToList()) + { + if (project.UpgradeItem != null) + { + // Check if the item has been dropped and picked up again, which apparently creates a new object with the same blueprint. + if (project.UpgradeItem.Collection != Game.Instance.Player.Inventory && project.UpgradeItem.Collection != Game.Instance.Player.SharedStash) + { + var itemInStash = Game.Instance.Player.SharedStash.FirstOrDefault(item => item.Blueprint == project.UpgradeItem.Blueprint); + if (itemInStash != null) + { + Main.ItemUpgradeProjects.Remove(project.UpgradeItem); + Main.ItemUpgradeProjects[itemInStash] = project; + project.UpgradeItem = itemInStash; + } + else + { + var itemInInventory = Game.Instance.Player.Inventory.FirstOrDefault(item => item.Blueprint == project.UpgradeItem.Blueprint); + if (itemInInventory != null) + { + Main.ItemUpgradeProjects.Remove(project.UpgradeItem); + Main.ItemUpgradeProjects[itemInInventory] = project; + project.UpgradeItem = itemInInventory; + } + } + } + + // Check that the caster can get at the item they're upgrading... it must be in the party inventory, and either un-wielded, or the crafter + // must be with the wielder (together in the capital or out in the party together). + var wieldedInParty = (project.UpgradeItem.Wielder == null || Game.Instance.Player.PartyCharacters.Contains(project.UpgradeItem.Wielder.Unit)); + if ((!playerInCapital || returningToCapital) + && (project.UpgradeItem.Collection != Game.Instance.Player.SharedStash || withPlayer) + && (project.UpgradeItem.Collection != Game.Instance.Player.Inventory || ((!withPlayer || !wieldedInParty) && (withPlayer || wieldedInParty)))) + { + project.AddMessage(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-missing-upgrade-item", project.UpgradeItem.Blueprint.Name)); + Main.AddBattleLogMessage(project.LastMessage); + continue; + } + } + + var craftingData = Main.LoadedData.ItemCraftingData.FirstOrDefault(data => data.Name == project.ItemType); + StatType craftingSkill; + int dc; + int progressRate; + + if (project.ItemType == Main.BondedItemRitual) + { + craftingSkill = StatType.SkillKnowledgeArcana; + dc = 10 + project.Crafter.Stats.GetStat(craftingSkill).ModifiedValue; + progressRate = Main.ModSettings.MagicCraftingRate; + } + else if (IsMundaneCraftingData(craftingData)) + { + craftingSkill = StatType.SkillKnowledgeWorld; + dc = Main.CalculateMundaneCraftingDC((RecipeBasedItemCraftingData)craftingData, project.ResultItem.Blueprint, caster); + progressRate = Main.ModSettings.MundaneCraftingRate; + } + else + { + craftingSkill = StatType.SkillKnowledgeArcana; + dc = 5 + project.CasterLevel; + progressRate = Main.ModSettings.MagicCraftingRate; + } + + var missing = CheckSpellPrerequisites(project, caster, isAdventuring, out var missingSpells, out var spellsToCast); + if (missing > 0) + { + var missingSpellNames = missingSpells + .Select(ability => ability.Name) + .BuildCommaList(project.AnyPrerequisite); + + if (craftingData.PrerequisitesMandatory || project.PrerequisitesMandatory) + { + project.AddMessage(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-missing-prerequisite", + project.ResultItem.Name, missingSpellNames)); + Main.AddBattleLogMessage(project.LastMessage); + // If the item type has mandatory prerequisites and some are missing, move on to the next project. + continue; + } + + Main.AddBattleLogMessage(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-missing-spell", missingSpellNames, + DifficultyClass.MissingPrerequisiteDCModifier * missing)); + } + var missing2 = CheckFeatPrerequisites(project, caster, out var missingFeats); + if (missing2 > 0) + { + var missingFeatNames = missingFeats.Select(ability => ability.Name).BuildCommaList(project.AnyPrerequisite); + Main.AddBattleLogMessage(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-missing-feat", missingFeatNames, + DifficultyClass.MissingPrerequisiteDCModifier * missing2)); + } + missing += missing2; + missing += CheckCrafterPrerequisites(project, caster); + dc += DifficultyClass.MissingPrerequisiteDCModifier * missing; + var casterLevel = Main.CharacterCasterLevel(caster); + if (casterLevel < project.CasterLevel) + { + // Rob's ruling... if you're below the prerequisite caster level, you're considered to be missing a prerequisite for each + // level you fall short, unless ModSettings.CasterLevelIsSinglePrerequisite is true. + var casterLevelPenalty = Main.ModSettings.CasterLevelIsSinglePrerequisite + ? DifficultyClass.MissingPrerequisiteDCModifier + : DifficultyClass.MissingPrerequisiteDCModifier * (project.CasterLevel - casterLevel); + dc += casterLevelPenalty; + Main.AddBattleLogMessage(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-low-caster-level", project.CasterLevel, casterLevelPenalty)); + } + var oppositionSchool = CheckForOppositionSchool(caster, project.SpellPrerequisites); + if (oppositionSchool != SpellSchool.None) + { + dc += DifficultyClass.OppositionSchoolDCModifier; + Main.AddBattleLogMessage(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-opposition-school", + LocalizedTexts.Instance.SpellSchoolNames.GetText(oppositionSchool), DifficultyClass.OppositionSchoolDCModifier)); + } + + var skillCheck = 10 + caster.Stats.GetStat(craftingSkill).ModifiedValue; + if (skillCheck < dc) + { + // Can't succeed by taking 10... move on to the next project. + project.AddMessage(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-dc-too-high", project.ResultItem.Name, + LocalizedTexts.Instance.Stats.GetText(craftingSkill), skillCheck, dc)); + Main.AddBattleLogMessage(project.LastMessage); + continue; + } + + // Cleared the last hurdle, so caster is going to make progress on this project. + // You only work at 1/4 speed if you're crafting while adventuring. + var adventuringPenalty = !isAdventuring || Main.ModSettings.CraftAtFullSpeedWhileAdventuring ? 1 : DifficultyClass.AdventuringProgressPenalty; + // Each 1 by which the skill check exceeds the DC increases the crafting rate by 20% of the base progressRate + var progressPerDay = (int)(progressRate * (1 + (float)(skillCheck - dc) / 5) / adventuringPenalty); + var daysUntilProjectFinished = (int)Math.Ceiling(1.0 * (project.TargetCost - project.Progress) / progressPerDay); + var daysCrafting = Math.Min(daysUntilProjectFinished, daysAvailableToCraft); + var progressGold = daysCrafting * progressPerDay; + foreach (var spell in spellsToCast) + { + if (spell.SourceItem != null) + { + // Use items whether we're adventuring or not, one charge per day of daysCrafting. We might run out of charges... + if (spell.SourceItem.IsSpendCharges && !((BlueprintItemEquipment)spell.SourceItem.Blueprint).RestoreChargesOnRest) + { + var itemSpell = spell; + for (var day = 0; day < daysCrafting; ++day) + { + if (itemSpell.SourceItem.Charges <= 0) + { + // This item is exhausted and we haven't finished crafting - find another item. + itemSpell = FindCasterSpell(caster, spell.Blueprint, isAdventuring, spellsToCast); + } + + if (itemSpell == null) + { + // We've run out of items that can cast the spell...crafting progress is going to slow, if not stop. + progressGold -= progressPerDay * (daysCrafting - day); + skillCheck -= DifficultyClass.MissingPrerequisiteDCModifier; + if (craftingData.PrerequisitesMandatory || project.PrerequisitesMandatory) + { + Main.AddBattleLogMessage(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-missing-prerequisite", project.ResultItem.Name, spell.Name)); + daysCrafting = day; + break; + } + + Main.AddBattleLogMessage(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-missing-spell", spell.Name, DifficultyClass.MissingPrerequisiteDCModifier)); + if (skillCheck < dc) + { + // Can no longer make progress + Main.AddBattleLogMessage(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-dc-too-high", project.ResultItem.Name, + LocalizedTexts.Instance.Stats.GetText(craftingSkill), skillCheck, dc)); + daysCrafting = day; + } + else + { + // Progress will be slower, but they don't need to cast this spell any longer. + progressPerDay = (int)(progressRate * (1 + (float)(skillCheck - dc) / 5) / adventuringPenalty); + daysUntilProjectFinished = + day + (int)Math.Ceiling(1.0 * (project.TargetCost - project.Progress - progressGold) / progressPerDay); + daysCrafting = Math.Min(daysUntilProjectFinished, daysAvailableToCraft); + progressGold += (daysCrafting - day) * progressPerDay; + } + + break; + } + + GameLogContext.SourceUnit = caster.Unit; + GameLogContext.Text = itemSpell.SourceItem.Name; + Main.AddBattleLogMessage(LocalizedStringBlueprints.CharacterUsedItemLocalized); + GameLogContext.Clear(); + itemSpell.SourceItem.SpendCharges(caster); + } + } + } + else if (isAdventuring) + { + // Actually cast the spells if we're adventuring. + Main.AddBattleLogMessage(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-expend-spell", spell.Name)); + spell.SpendFromSpellbook(); + } + } + + var progressKey = project.ItemType == Main.BondedItemRitual + ? "craftMagicItems-logMessage-made-progress-bondedItem" + : "craftMagicItems-logMessage-made-progress"; + var progress = LocalizationHelper.FormatLocalizedString(progressKey, progressGold, project.TargetCost - project.Progress, project.ResultItem.Name); + var checkResult = LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-made-progress-check", LocalizedTexts.Instance.Stats.GetText(craftingSkill), + skillCheck, dc); + Main.AddBattleLogMessage(progress, checkResult); + daysAvailableToCraft -= daysCrafting; + project.Progress += progressGold; + if (project.Progress >= project.TargetCost) + { + // Completed the project! + if (project.ItemType == Main.BondedItemRitual) + { + Main.AddBattleLogMessage(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-bonding-ritual-complete", project.ResultItem.Name), project.ResultItem); + Main.BondWithObject(project.Crafter, project.ResultItem); + } + else + { + Main.AddBattleLogMessage(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-crafting-complete", project.ResultItem.Name), project.ResultItem); + CraftItem(project.ResultItem, project.UpgradeItem); + } + timer.CraftingProjects.Remove(project); + if (project.UpgradeItem == null) + { + Main.ItemCreationProjects.Remove(project); + } + else + { + Main.ItemUpgradeProjects.Remove(project.UpgradeItem); + } + } + else + { + var completeKey = project.ItemType == Main.BondedItemRitual + ? "craftMagicItems-logMessage-made-progress-bonding-ritual-amount-complete" + : "craftMagicItems-logMessage-made-progress-amount-complete"; + var amountComplete = LocalizationHelper.FormatLocalizedString(completeKey, project.ResultItem.Name, 100 * project.Progress / project.TargetCost); + Main.AddBattleLogMessage(amountComplete, project.ResultItem); + project.AddMessage($"{progress} {checkResult}"); + } + + if (daysAvailableToCraft <= 0) + { + return; + } + } + + if (daysAvailableToCraft > 0) + { + // They didn't use up all available days - reset the time they can start crafting to now. + timer.LastUpdated = Game.Instance.Player.GameTime; + } + } + + private static int CheckCrafterPrerequisites(CraftingProjectData project, UnitDescriptor caster) + { + var missing = GetMissingCrafterPrerequisites(project.CrafterPrerequisites, caster); + foreach (var prerequisite in missing) + { + Main.AddBattleLogMessage(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-missing-crafter-prerequisite", + new L10NString($"craftMagicItems-crafter-prerequisite-{prerequisite}"), DifficultyClass.MissingPrerequisiteDCModifier)); + } + + return missing.Count; + } + + private static int CheckFeatPrerequisites(CraftingProjectData project, UnitDescriptor caster, out List missingFeats) + { + return CheckFeatPrerequisites(project.FeatPrerequisites, project.AnyPrerequisite, caster, out missingFeats); + } + + public static int CheckFeatPrerequisites(BlueprintFeature[] prerequisites, bool anyPrerequisite, UnitDescriptor caster, + out List missingFeats) + { + missingFeats = new List(); + if (prerequisites != null) + { + foreach (var featBlueprint in prerequisites) + { + var feat = caster.GetFeature(featBlueprint); + if (feat != null) + { + if (anyPrerequisite) + { + missingFeats.Clear(); + return 0; + } + } + else + { + missingFeats.Add(featBlueprint); + } + } + } + + return anyPrerequisite ? Math.Min(1, missingFeats.Count) : missingFeats.Count; + } + + private static int CheckSpellPrerequisites(CraftingProjectData project, UnitDescriptor caster, bool mustPrepare, + out List missingSpells, out List spellsToCast) + { + return CheckSpellPrerequisites(project.SpellPrerequisites, project.AnyPrerequisite, caster, mustPrepare, out missingSpells, out spellsToCast); + } + + public static int CheckSpellPrerequisites(BlueprintAbility[] prerequisites, bool anyPrerequisite, UnitDescriptor caster, bool mustPrepare, + out List missingSpells, out List spellsToCast) + { + spellsToCast = new List(); + missingSpells = new List(); + if (prerequisites != null) + { + foreach (var spellBlueprint in prerequisites) + { + var spell = FindCasterSpell(caster, spellBlueprint, mustPrepare, spellsToCast); + if (spell != null) + { + spellsToCast.Add(spell); + if (anyPrerequisite) + { + missingSpells.Clear(); + return 0; + } + } + else + { + missingSpells.Add(spellBlueprint); + } + } + } + + return anyPrerequisite ? Math.Min(1, missingSpells.Count) : missingSpells.Count; + } + + public static SpellSchool CheckForOppositionSchool(UnitDescriptor crafter, BlueprintAbility[] prerequisiteSpells) + { + if (prerequisiteSpells != null) + { + foreach (var spell in prerequisiteSpells) + { + if (crafter.Spellbooks.Any(spellbook => spellbook.Blueprint.SpellList.Contains(spell) + && spellbook.OppositionSchools.Contains(spell.School))) + { + return spell.School; + } + } + } + return SpellSchool.None; + } + + public static List GetMissingCrafterPrerequisites(CrafterPrerequisiteType[] prerequisites, UnitDescriptor caster) + { + var missingCrafterPrerequisites = new List(); + if (prerequisites != null) + { + missingCrafterPrerequisites.AddRange(prerequisites.Where(prerequisite => + prerequisite == CrafterPrerequisiteType.AlignmentLawful && (caster.Alignment.Value.ToMask() & AlignmentMaskType.Lawful) == 0 + || prerequisite == CrafterPrerequisiteType.AlignmentGood && (caster.Alignment.Value.ToMask() & AlignmentMaskType.Good) == 0 + || prerequisite == CrafterPrerequisiteType.AlignmentChaotic && (caster.Alignment.Value.ToMask() & AlignmentMaskType.Chaotic) == 0 + || prerequisite == CrafterPrerequisiteType.AlignmentEvil && (caster.Alignment.Value.ToMask() & AlignmentMaskType.Evil) == 0 + || prerequisite == CrafterPrerequisiteType.FeatureChannelEnergy && + caster.GetFeature(ResourcesLibrary.TryGetBlueprint(Features.ChannelEnergyFeatureGuid)) == null + )); + } + + return missingCrafterPrerequisites; + } + + private static AbilityData FindCasterSpell(UnitDescriptor caster, BlueprintAbility spellBlueprint, bool mustHavePrepared, + IReadOnlyCollection spellsToCast) + { + foreach (var spellbook in caster.Spellbooks) + { + var spellLevel = spellbook.GetSpellLevel(spellBlueprint); + if (spellLevel > spellbook.MaxSpellLevel || spellLevel < 0) + { + continue; + } + + if (mustHavePrepared && spellLevel > 0) + { + if (spellbook.Blueprint.Spontaneous) + { + // Count how many other spells of this class and level they're going to cast, to ensure they don't double-dip on spell slots. + var toCastCount = spellsToCast.Count(ability => ability.Spellbook == spellbook && spellbook.GetSpellLevel(ability) == spellLevel); + // Spontaneous spellcaster must have enough spell slots of the required level. + if (spellbook.GetSpontaneousSlots(spellLevel) <= toCastCount) + { + continue; + } + } + else + { + // Prepared spellcaster must have memorized the spell... + var spellSlot = spellbook.GetMemorizedSpells(spellLevel).FirstOrDefault(slot => + slot.Available && (slot.Spell?.Blueprint == spellBlueprint || + spellBlueprint.Parent && slot.Spell?.Blueprint == spellBlueprint.Parent)); + if (spellSlot == null && (spellbook.GetSpontaneousConversionSpells(spellLevel).Contains(spellBlueprint) || + (spellBlueprint.Parent && + spellbook.GetSpontaneousConversionSpells(spellLevel).Contains(spellBlueprint.Parent)))) + { + // ... or be able to convert, in which case any available spell of the given level will do. + spellSlot = spellbook.GetMemorizedSpells(spellLevel).FirstOrDefault(slot => slot.Available); + } + + if (spellSlot == null) + { + continue; + } + + return spellSlot.Spell; + } + } + + return spellbook.GetKnownSpells(spellLevel).Concat(spellbook.GetSpecialSpells(spellLevel)) + .First(known => known.Blueprint == spellBlueprint || + (spellBlueprint.Parent && known.Blueprint == spellBlueprint.Parent)); + } + + // Try casting the spell from an item + ItemEntity fromItem = null; + var fromItemCharges = 0; + foreach (var item in caster.Inventory) + { + // Check (non-potion) items wielded by the caster to see if they can cast the required spell + if (item.Wielder == caster && (!(item.Blueprint is BlueprintItemEquipmentUsable usable) || usable.Type != UsableItemType.Potion) + && (item.Ability?.Blueprint == spellBlueprint || + (spellBlueprint.Parent && item.Ability?.Blueprint == spellBlueprint.Parent))) + { + // Choose the item with the most available charges, with a multiplier if the item restores charges on rest. + var charges = item.Charges * (((BlueprintItemEquipment)item.Blueprint).RestoreChargesOnRest ? 50 : 1); + if (charges > fromItemCharges) + { + fromItem = item; + fromItemCharges = charges; + } + } + } + + return fromItem?.Ability?.Data; + } + + public static bool IsMundaneCraftingData(ItemCraftingData craftingData) + { + return craftingData.FeatGuid == null; + } + + public static void CraftItem(ItemEntity resultItem, ItemEntity upgradeItem = null) + { + var characters = UIUtility.GetGroup(true).Where(character => character.IsPlayerFaction && !character.Descriptor.IsPet); + foreach (var character in characters) + { + var bondedComponent = Main.GetBondedItemComponentForCaster(character.Descriptor); + if (bondedComponent && bondedComponent.ownerItem == upgradeItem) + { + bondedComponent.ownerItem = resultItem; + } + } + + using (new DisableBattleLog(!Main.ModSettings.CraftingTakesNoTime)) + { + var holdingSlot = upgradeItem?.HoldingSlot; + var slotIndex = upgradeItem?.InventorySlotIndex; + var inventory = true; + if (upgradeItem != null) + { + if (Game.Instance.Player.Inventory.Contains(upgradeItem)) + { + Game.Instance.Player.Inventory.Remove(upgradeItem); + } + else + { + Game.Instance.Player.SharedStash.Remove(upgradeItem); + inventory = false; + } + } + if (holdingSlot == null) + { + if (inventory) + { + Game.Instance.Player.Inventory.Add(resultItem); + } + else + { + Game.Instance.Player.SharedStash.Add(resultItem); + } + if (slotIndex is int value) + { + resultItem.SetSlotIndex(value); + } + } + else + { + holdingSlot.InsertItem(resultItem); + } + } + + if (resultItem is ItemEntityUsable usable) + { + switch (usable.Blueprint.Type) + { + case UsableItemType.Scroll: + Game.Instance.UI.Common.UISound.Play(UISoundType.NewInformation); + break; + case UsableItemType.Potion: + Game.Instance.UI.Common.UISound.PlayItemSound(SlotAction.Take, resultItem, false); + break; + default: + Game.Instance.UI.Common.UISound.Play(UISoundType.SettlementBuildStart); + break; + } + } + else + { + Game.Instance.UI.Common.UISound.Play(UISoundType.SettlementBuildStart); + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/CustomBlueprintBuilder.cs b/CraftMagicItems/CustomBlueprintBuilder.cs index 5da90f5..9a5206d 100644 --- a/CraftMagicItems/CustomBlueprintBuilder.cs +++ b/CraftMagicItems/CustomBlueprintBuilder.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Reflection; using System.Text.RegularExpressions; using Kingmaker.Blueprints; #if !PATCH21_BETA @@ -102,7 +100,7 @@ private static BlueprintScriptableObject PatchBlueprint(string assetId, Blueprin } #endif // Insert patched blueprint into ResourcesLibrary under the new GUID. - Main.Accessors.SetBlueprintScriptableObjectAssetGuid(blueprint, newAssetId); + Main.Accessors.SetBlueprintScriptableObjectAssetGuid(blueprint) = newAssetId; if (ResourcesLibrary.LibraryObject.BlueprintsByAssetId != null) { ResourcesLibrary.LibraryObject.BlueprintsByAssetId[newAssetId] = blueprint; } @@ -127,19 +125,7 @@ public static string AssetGuidWithoutMatch(string assetGuid, Match match = null) } // This patch is generic, and makes custom blueprints fall back to their initial version. - [Harmony12.HarmonyPatch(typeof(ResourcesLibrary), "TryGetBlueprint")] - // ReSharper disable once UnusedMember.Local - private static class ResourcesLibraryTryGetBlueprintFallbackPatch { - // ReSharper disable once UnusedMember.Local - private static MethodBase TargetMethod() { - // ResourcesLibrary.TryGetBlueprint has two definitions which only differ by return type :( - var allMethods = typeof(ResourcesLibrary).GetMethods(); - return allMethods.Single(info => info.Name == "TryGetBlueprint" && info.ReturnType == typeof(BlueprintScriptableObject)); - } - - // ReSharper disable once UnusedMember.Local - [Harmony12.HarmonyPriority(Harmony12.Priority.First)] - // ReSharper disable once InconsistentNaming + public static class ResourcesLibraryTryGetBlueprintFallbackPatch { private static void Postfix(string assetId, ref BlueprintScriptableObject __result) { if (__result == null && assetId.Length > VanillaAssetIdLength) { // Failed to load custom blueprint - return the original. @@ -149,26 +135,13 @@ private static void Postfix(string assetId, ref BlueprintScriptableObject __resu } } - [Harmony12.HarmonyPatch(typeof(ResourcesLibrary), "TryGetBlueprint")] - // ReSharper disable once UnusedMember.Local - private static class ResourcesLibraryTryGetBlueprintModPatch { - // ReSharper disable once UnusedMember.Local - private static MethodBase TargetMethod() { - // ResourcesLibrary.TryGetBlueprint has two definitions which only differ by return type :( - var allMethods = typeof(ResourcesLibrary).GetMethods(); - return allMethods.Single(info => info.Name == "TryGetBlueprint" && info.ReturnType == typeof(BlueprintScriptableObject)); - } - - // ReSharper disable once UnusedMember.Local + public static class ResourcesLibraryTryGetBlueprintModPatch { private static void Prefix(ref string assetId) { // Perform any backward compatibility substitutions for (var index = 0; index < substitutions.Length; index ++) { assetId = assetId.Replace(substitutions[index].oldGuid, substitutions[index].newGuid); } } - - // ReSharper disable once UnusedMember.Local - // ReSharper disable once InconsistentNaming private static void Postfix(string assetId, ref BlueprintScriptableObject __result) { if (__result != null && assetId != __result.AssetGuid) { __result = PatchBlueprint(assetId, __result); diff --git a/Data/ArmsArmorRecipes.json b/CraftMagicItems/Data/ArmsArmorRecipes.json similarity index 100% rename from Data/ArmsArmorRecipes.json rename to CraftMagicItems/Data/ArmsArmorRecipes.json diff --git a/Data/CommonRecipes.json b/CraftMagicItems/Data/CommonRecipes.json similarity index 100% rename from Data/CommonRecipes.json rename to CraftMagicItems/Data/CommonRecipes.json diff --git a/Data/ItemTypes.json b/CraftMagicItems/Data/ItemTypes.json similarity index 100% rename from Data/ItemTypes.json rename to CraftMagicItems/Data/ItemTypes.json diff --git a/Data/LootItems.json b/CraftMagicItems/Data/LootItems.json similarity index 100% rename from Data/LootItems.json rename to CraftMagicItems/Data/LootItems.json diff --git a/Data/MundaneRecipes.json b/CraftMagicItems/Data/MundaneRecipes.json similarity index 100% rename from Data/MundaneRecipes.json rename to CraftMagicItems/Data/MundaneRecipes.json diff --git a/Data/RingRecipes.json b/CraftMagicItems/Data/RingRecipes.json similarity index 100% rename from Data/RingRecipes.json rename to CraftMagicItems/Data/RingRecipes.json diff --git a/Data/RodRecipes.json b/CraftMagicItems/Data/RodRecipes.json similarity index 100% rename from Data/RodRecipes.json rename to CraftMagicItems/Data/RodRecipes.json diff --git a/Data/WondrousRecipes.json b/CraftMagicItems/Data/WondrousRecipes.json similarity index 100% rename from Data/WondrousRecipes.json rename to CraftMagicItems/Data/WondrousRecipes.json diff --git a/CraftMagicItems/L10NString.cs b/CraftMagicItems/Localization/L10NString.cs similarity index 59% rename from CraftMagicItems/L10NString.cs rename to CraftMagicItems/Localization/L10NString.cs index a012823..c22f0e6 100644 --- a/CraftMagicItems/L10NString.cs +++ b/CraftMagicItems/Localization/L10NString.cs @@ -1,41 +1,52 @@ using System.Text.RegularExpressions; using Kingmaker.Localization; -namespace CraftMagicItems { - public class L10NString : LocalizedString { +namespace CraftMagicItems.Localization +{ + public class L10NString : LocalizedString + { private static readonly Regex StringModifier = new Regex(@"(?[-0-9a-f]+)/((?[^/]+)/(?[^/]*)/)+"); - public L10NString(string key) { - if (LocalizationManager.CurrentPack != null && !LocalizationManager.CurrentPack.Strings.ContainsKey(key)) { + public L10NString(string key) + { + if (LocalizationManager.CurrentPack != null && !LocalizationManager.CurrentPack.Strings.ContainsKey(key)) + { var match = StringModifier.Match(key); - if (match.Success) { + if (match.Success) + { // If we're modifying an existing string, we need to insert it into the language bundle up front. var result = new L10NString(match.Groups["key"].Value).ToString(); var count = match.Groups["find"].Captures.Count; - for (var index = 0; index < count; ++index) { + for (var index = 0; index < count; ++index) + { result = result.Replace(match.Groups["find"].Captures[index].Value, match.Groups["replace"].Captures[index].Value); } LocalizationManager.CurrentPack.Strings[key] = result; } } - Harmony12.Traverse.Create(this).Field("m_Key").SetValue(key); + HarmonyLib.Traverse.Create(this).Field("m_Key").SetValue(key); } } - public class FakeL10NString : LocalizedString { + public class FakeL10NString : LocalizedString + { private readonly string fakeValue; - public FakeL10NString(string fakeValue) { + public FakeL10NString(string fakeValue) + { this.fakeValue = fakeValue; - Harmony12.Traverse.Create(this).Field("m_Key").SetValue(fakeValue); + HarmonyLib.Traverse.Create(this).Field("m_Key").SetValue(fakeValue); } - [Harmony12.HarmonyPatch(typeof(LocalizedString), "LoadString")] + [HarmonyLib.HarmonyPatch(typeof(LocalizedString), "LoadString")] // ReSharper disable once UnusedMember.Local - private static class LocalizedStringLoadStringPatch { + private static class LocalizedStringLoadStringPatch + { // ReSharper disable once UnusedMember.Local - private static bool Prefix(LocalizedString __instance, ref string __result) { - if (__instance is FakeL10NString fake) { + private static bool Prefix(LocalizedString __instance, ref string __result) + { + if (__instance is FakeL10NString fake) + { __result = fake.fakeValue; return false; } @@ -44,12 +55,15 @@ private static bool Prefix(LocalizedString __instance, ref string __result) { } } - [Harmony12.HarmonyPatch(typeof(LocalizedString), "IsSet")] + [HarmonyLib.HarmonyPatch(typeof(LocalizedString), "IsSet")] // ReSharper disable once UnusedMember.Local - private static class LocalizedStringIsSetPatch { + private static class LocalizedStringIsSetPatch + { // ReSharper disable once UnusedMember.Local - private static bool Prefix(LocalizedString __instance, ref bool __result) { - if (__instance is FakeL10NString fake) { + private static bool Prefix(LocalizedString __instance, ref bool __result) + { + if (__instance is FakeL10NString fake) + { __result = !string.IsNullOrEmpty(fake.fakeValue); return false; } @@ -58,12 +72,15 @@ private static bool Prefix(LocalizedString __instance, ref bool __result) { } } - [Harmony12.HarmonyPatch(typeof(LocalizedString), "IsEmpty")] + [HarmonyLib.HarmonyPatch(typeof(LocalizedString), "IsEmpty")] // ReSharper disable once UnusedMember.Local - private static class LocalizedStringIsEmptyPatch { + private static class LocalizedStringIsEmptyPatch + { // ReSharper disable once UnusedMember.Local - private static bool Prefix(LocalizedString __instance, ref bool __result) { - if (__instance is FakeL10NString fake) { + private static bool Prefix(LocalizedString __instance, ref bool __result) + { + if (__instance is FakeL10NString fake) + { __result = string.IsNullOrEmpty(fake.fakeValue); return false; } diff --git a/CraftMagicItems/L10n.cs b/CraftMagicItems/Localization/L10n.cs similarity index 63% rename from CraftMagicItems/L10n.cs rename to CraftMagicItems/Localization/L10n.cs index f7789bd..081ee0b 100644 --- a/CraftMagicItems/L10n.cs +++ b/CraftMagicItems/Localization/L10n.cs @@ -9,56 +9,74 @@ using Kingmaker.Localization; using Newtonsoft.Json; -namespace CraftMagicItems { - public class L10NData { +namespace CraftMagicItems.Localization +{ + public class L10NData + { [JsonProperty] public string Key; [JsonProperty] public string Value; } - class L10n { + /// Localization is sometimes written in English as l10n, where 10 is the number of letters in the English word between 'l' and 'n' + class L10N + { private static readonly Dictionary ModifiedL10NStrings = new Dictionary(); private static bool initialLoad; private static bool enabled = true; - private static void LoadL10NStrings() { - if (LocalizationManager.CurrentPack == null) { + private static void LoadL10NStrings() + { + if (LocalizationManager.CurrentPack == null) + { return; } initialLoad = true; var currentLocale = LocalizationManager.CurrentLocale.ToString(); var fileName = $"{Main.ModEntry.Path}/L10n/Strings_{currentLocale}.json"; - if (!File.Exists(fileName)) { + if (!File.Exists(fileName)) + { Main.ModEntry.Logger.Warning($"Localised text for current local \"{currentLocale}\" not found, falling back on enGB."); currentLocale = "enGB"; fileName = $"{Main.ModEntry.Path}/L10n/Strings_{currentLocale}.json"; } - try { + try + { var allStrings = Main.ReadJsonFile(fileName); - foreach (var data in allStrings) { + foreach (var data in allStrings) + { var value = data.Value; - if (LocalizationManager.CurrentPack.Strings.ContainsKey(data.Key)) { + if (LocalizationManager.CurrentPack.Strings.ContainsKey(data.Key)) + { var original = LocalizationManager.CurrentPack.Strings[data.Key]; ModifiedL10NStrings.Add(data.Key, original); - if (value[0] == '+') { + if (value[0] == '+') + { value = original + value.Substring(1); } } LocalizationManager.CurrentPack.Strings[data.Key] = value; } - } catch (Exception e) { + } + catch (Exception e) + { Main.ModEntry.Logger.Warning($"Exception loading L10n data for locale {currentLocale}: {e}"); throw; } } - public static void SetEnabled(bool newEnabled) { - if (LocalizationManager.CurrentPack != null) { - if (!initialLoad) { + public static void SetEnabled(bool newEnabled) + { + if (LocalizationManager.CurrentPack != null) + { + if (!initialLoad) + { LoadL10NStrings(); } - if (enabled != newEnabled) { + + if (enabled != newEnabled) + { enabled = newEnabled; foreach (var key in ModifiedL10NStrings.Keys.ToArray()) { var swap = ModifiedL10NStrings[key]; @@ -69,31 +87,38 @@ public static void SetEnabled(bool newEnabled) { } } - [Harmony12.HarmonyPatch(typeof(LocalizationManager))] - [Harmony12.HarmonyPatch("CurrentLocale", Harmony12.MethodType.Setter)] - private static class LocalizationManagerCurrentLocaleSetterPatch { + [HarmonyLib.HarmonyPatch(typeof(LocalizationManager), "CurrentLocale", HarmonyLib.MethodType.Setter)] + private static class LocalizationManagerCurrentLocaleSetterPatch + { // ReSharper disable once UnusedMember.Local - private static void Postfix() { + private static void Postfix() + { LoadL10NStrings(); } } - [Harmony12.HarmonyPatch(typeof(MainMenu), "Start")] - private static class MainMenuStartPatch { - private static void Prefix() { + [HarmonyLib.HarmonyPatch(typeof(MainMenu), "Start")] + private static class MainMenuStartPatch + { + private static void Prefix() + { // Kingmaker Mod Loader doesn't appear to patch the game before LocalizationManager.CurrentLocale has been set. - if (!initialLoad) { + if (!initialLoad) + { LoadL10NStrings(); } } } #if PATCH21 - [Harmony12.HarmonyPatch(typeof(MainMenuUiContext), "Initialize")] - private static class MainMenuUiContextInitializePatch { - private static void Prefix() { + [HarmonyLib.HarmonyPatch(typeof(MainMenuUiContext), "Initialize")] + private static class MainMenuUiContextInitializePatch + { + private static void Prefix() + { // Kingmaker Mod Loader doesn't appear to patch the game before LocalizationManager.CurrentLocale has been set. - if (!initialLoad) { + if (!initialLoad) + { LoadL10NStrings(); } } diff --git a/CraftMagicItems/Localization/LocalizationHelper.cs b/CraftMagicItems/Localization/LocalizationHelper.cs new file mode 100644 index 0000000..d24942d --- /dev/null +++ b/CraftMagicItems/Localization/LocalizationHelper.cs @@ -0,0 +1,33 @@ +using Kingmaker.EntitySystem.Entities; +using Kingmaker.UI.Log; + +namespace CraftMagicItems.Localization +{ + /// Class containing methods used for formatting localized strings + public static class LocalizationHelper + { + /// Formats the localized string specified in + /// specifying what is taking action (such as a crafter) + /// Key used to look up a localized string + /// Collection of arguments used in + /// A representation of the localized string + public static string FormatLocalizedString(UnitEntityData sourceUnit, string key, params object[] args) + { + // Set GameLogContext so the caster will be used when generating localized strings. + GameLogContext.SourceUnit = sourceUnit; + var template = new L10NString(key); + var result = string.Format(template.ToString(), args); + GameLogContext.Clear(); + return result; + } + + /// Adds the currently selected caster to the localized string specified in + /// Key used to look up a localized string + /// Arguments used in + /// A representation of the localized string + public static string FormatLocalizedString(string key, params object[] args) + { + return FormatLocalizedString(Main.Selections.CurrentCaster ?? Main.GetSelectedCrafter(false), key, args); + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Main.cs b/CraftMagicItems/Main.cs index 878b387..467be3c 100644 --- a/CraftMagicItems/Main.cs +++ b/CraftMagicItems/Main.cs @@ -1,36 +1,33 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Reflection; -using System.Reflection.Emit; -using System.Runtime.Serialization; using System.Text.RegularExpressions; +using CraftMagicItems.Config; +using CraftMagicItems.Constants; +using CraftMagicItems.Localization; +using CraftMagicItems.Patches; +using CraftMagicItems.Patches.Harmony; +using CraftMagicItems.UI; +using CraftMagicItems.UI.Sections; +using CraftMagicItems.UI.UnityModManager; using Kingmaker; #if PATCH21 -using Kingmaker.Assets.UI.Context; #endif using Kingmaker.Blueprints; using Kingmaker.Blueprints.Classes; -using Kingmaker.Blueprints.Classes.Selection; using Kingmaker.Blueprints.Classes.Spells; -using Kingmaker.Blueprints.Facts; using Kingmaker.Blueprints.Items; using Kingmaker.Blueprints.Items.Armors; using Kingmaker.Blueprints.Items.Ecnchantments; using Kingmaker.Blueprints.Items.Equipment; using Kingmaker.Blueprints.Items.Shields; using Kingmaker.Blueprints.Items.Weapons; -using Kingmaker.Blueprints.Loot; using Kingmaker.Blueprints.Root; using Kingmaker.Blueprints.Root.Strings.GameLog; -using Kingmaker.Controllers.Rest; -using Kingmaker.Designers; using Kingmaker.Designers.Mechanics.EquipmentEnchants; using Kingmaker.Designers.Mechanics.Facts; -using Kingmaker.Designers.Mechanics.WeaponEnchants; -using Kingmaker.Designers.TempMapCode.Capital; using Kingmaker.EntitySystem.Entities; using Kingmaker.EntitySystem.Stats; using Kingmaker.Enums; @@ -41,17 +38,12 @@ using Kingmaker.Items.Slots; #endif using Kingmaker.Kingdom; -using Kingmaker.Localization; using Kingmaker.PubSubSystem; using Kingmaker.RuleSystem; -using Kingmaker.RuleSystem.Rules; using Kingmaker.RuleSystem.Rules.Abilities; -using Kingmaker.RuleSystem.Rules.Damage; using Kingmaker.UI; -using Kingmaker.UI.ActionBar; using Kingmaker.UI.Common; using Kingmaker.UI.Log; -using Kingmaker.UI.Tooltip; using Kingmaker.UnitLogic; using Kingmaker.UnitLogic.Abilities; using Kingmaker.UnitLogic.Abilities.Blueprints; @@ -62,99 +54,18 @@ using Kingmaker.UnitLogic.Buffs; using Kingmaker.UnitLogic.Buffs.Blueprints; using Kingmaker.UnitLogic.FactLogic; -using Kingmaker.UnitLogic.Mechanics.Components; -using Kingmaker.UnitLogic.Parts; using Kingmaker.Utility; -using Kingmaker.View.Equipment; using Newtonsoft.Json; using UnityEngine; using UnityModManagerNet; using Random = System.Random; -namespace CraftMagicItems { - public class Settings : UnityModManager.ModSettings { - public const int MagicCraftingProgressPerDay = 500; - public const int MundaneCraftingProgressPerDay = 5; - - public bool CraftingCostsNoGold; - public bool IgnoreCraftingFeats; - public bool CraftingTakesNoTime; - public float CraftingPriceScale = 1; - public bool CraftAtFullSpeedWhileAdventuring; - public bool CasterLevelIsSinglePrerequisite; - public bool IgnoreFeatCasterLevelRestriction; - public bool IgnorePlusTenItemMaximum; - public bool CustomCraftRate; - public int MagicCraftingRate = MagicCraftingProgressPerDay; - public int MundaneCraftingRate = MundaneCraftingProgressPerDay; - - public override void Save(UnityModManager.ModEntry modEntry) { - Save(this, modEntry); - } - } - +namespace CraftMagicItems +{ public static class Main { - private const int MissingPrerequisiteDCModifier = 5; - private const int OppositionSchoolDCModifier = 4; - private const int AdventuringProgressPenalty = 4; - private const int WeaponMasterworkCost = 300; - private const int WeaponPlusCost = 2000; - private const int ArmorMasterworkCost = 150; - private const int ArmorPlusCost = 1000; - private const int UnarmedPlusCost = 4000; - private const string BondedItemRitual = "bondedItemRitual"; - - private static readonly string[] CraftingPriceStrings = { - "100% (Owlcat prices)", - "200% (Tabletop prices)", - "Custom" - }; - - private static readonly FeatureGroup[] CraftingFeatGroups = {FeatureGroup.Feat, FeatureGroup.WizardFeat}; - private const string MasterworkGuid = "6b38844e2bffbac48b63036b66e735be"; - public const string MithralArmorEnchantmentGuid = "7b95a819181574a4799d93939aa99aff"; - private const string OversizedGuid = "d8e1ebc1062d8cc42abff78783856b0d"; - private const string AlchemistProgressionGuid = "efd55ff9be2fda34981f5b9c83afe4f1"; - private const string AlchemistGrenadierArchetypeGuid = "6af888a7800b3e949a40f558ff204aae"; - private const string ScrollSavantArchetypeGuid = "f43c78692a4e10d43a38bd6aedf53c1b"; - private const string MartialWeaponProficiencies = "203992ef5b35c864390b4e4a1e200629"; - private const string ChannelEnergyFeatureGuid = "a79013ff4bcd4864cb669622a29ddafb"; - private const string ShieldMasterGuid = "dbec636d84482944f87435bd31522fcc"; - private const string ProdigiousTwoWeaponFightingGuid = "ddba046d03074037be18ad33ea462028"; - private const string TwoWeaponFightingBasicMechanicsGuid = "6948b379c0562714d9f6d58ccbfa8faa"; - private const string LongshankBaneGuid = "92a1f5db1a03c5b468828c25dd375806"; - private const string WeaponLightShieldGuid = "1fd965e522502fe479fdd423cca07684"; - private const string WeaponHeavyShieldGuid = "be9b6408e6101cb4997a8996484baf19"; - - private static readonly string[] SafeBlueprintAreaGuids = { - "141f6999dada5a842a46bb3f029c287a", // Dire Narlmarches village - "e0852647faf68a641a0a5ec5436fc0bf", // Dunsward village - "537acbd9039b6a04f915dfc21572affb", // Glenebon village - "3ddf191773e8c2f448d31289b8d654bf", // Kamelands village - "f1b76870cc69e6a479c767cbe3c8a8ca", // North Narlmarches village - "ea3788bcdf33d884baffc34440ff620f", // Outskirts village - "e7292eab463c4924c8f14548545e25b7", // Silverstep village - "7c4a954c65e8d7146a6edc00c498a582", // South Narlmarches village - "7d9616b3807840c47ba3b2ab380c55a0", // Tors of Levenies village - "653811192a3fcd148816384a9492bd08", // Secluded Lodge - "fd1b6fa9f788ca24e86bd922a10da080", // Tenebrous Depths start hub - "c49315fe499f0e5468af6f19242499a2", // Tenebrous Depths start hub (Roguelike) - }; - - private static readonly string[] ItemEnchantmentGuids = { - "d42fc23b92c640846ac137dc26e000d4", "da7d830b3f75749458c2e51524805560", // Enchantment +1 - "eb2faccc4c9487d43b3575d7e77ff3f5", "49f9befa0e77cd5428ca3b28fd66a54e", // Enchantment +2 - "80bb8a737579e35498177e1e3c75899b", "bae627dfb77c2b048900f154719ca07b", // Enchantment +3 - "783d7d496da6ac44f9511011fc5f1979", "a4016a5d78384a94581497d0d135d98b", // Enchantment +4 - "bdba267e951851449af552aa9f9e3992", "c3ad7f708c573b24082dde91b081ca5f", // Enchantment +5 - "a36ad92c51789b44fa8a1c5c116a1328", "90316f5801dbe4748a66816a7c00380c", // Agile - }; + public const string BondedItemRitual = "bondedItemRitual"; private const string CustomPriceLabel = "Crafting Cost: "; - private static readonly LocalizedString CasterLevelLocalized = new L10NString("dfb34498-61df-49b1-af18-0a84ce47fc98"); - private static readonly LocalizedString CharacterUsedItemLocalized = new L10NString("be7942ed-3af1-4fc7-b20b-41966d2f80b7"); - private static readonly LocalizedString ShieldBashLocalized = new L10NString("314ff56d-e93b-4915-8ca4-24a7670ad436"); - private static readonly LocalizedString QualitiesLocalized = new L10NString("0f84fde9-14ca-4e2f-9c82-b2522039dbff"); private static readonly WeaponCategory[] AmmunitionWeaponCategories = { WeaponCategory.Longbow, @@ -172,19 +83,6 @@ public static class Main { ItemsFilter.ItemType.Neck }; - private static readonly string[] BondedItemFeatures = { - "2fb5e65bd57caa943b45ee32d825e9b9", - "aa34ca4f3cd5e5d49b2475fcfdf56b24" - }; - - private enum OpenSection { - CraftMagicItemsSection, - CraftMundaneItemsSection, - ProjectsSection, - FeatsSection, - CheatsSection - } - private enum ItemLocationFilter { All, @@ -193,128 +91,74 @@ private enum ItemLocationFilter Stash } - struct IkPatch { - public IkPatch(string uuid, float x, float y, float z) { - m_uuid = uuid; - m_x = x; m_y = y; m_z = z; - } - public string m_uuid; - public float m_x, m_y, m_z; - } - - private static readonly IkPatch[] IkPatchList = { - new IkPatch("a6f7e3dc443ff114ba68b4648fd33e9f", 0.00f, -0.10f, 0.01f), // dueling sword - new IkPatch("13fa38737d46c9e4abc7f4d74aaa59c3", 0.00f, -0.36f, 0.00f), // tongi - new IkPatch("1af5621e2ae551e42bd1dd6744d98639", 0.00f, -0.07f, 0.00f), // falcata - new IkPatch("d516765b3c2904e4a939749526a52a9a", 0.00f, -0.15f, 0.00f), // estoc - new IkPatch("2ece38f30500f454b8569136221e55b0", 0.00f, -0.08f, 0.00f), // rapier - new IkPatch("a492410f3d65f744c892faf09daad84a", 0.00f, -0.20f, 0.00f), // heavy pick - new IkPatch("6ff66364e0a2c89469c2e52ebb46365e", 0.00f, -0.10f, 0.00f), // trident - new IkPatch("d5a167f0f0208dd439ec7481e8989e21", 0.00f, -0.08f, 0.00f), // heavy mace - new IkPatch("8fefb7e0da38b06408f185e29372c703", -0.14f, 0.00f, 0.00f), // heavy flail + public static readonly MethodPatch[] MethodPatchList = + { + new MethodPatch( + typeof(ResourcesLibrary).GetMethods().Single(m => m.Name == "TryGetBlueprint" && m.ReturnType == typeof(BlueprintScriptableObject)), + postfix: new HarmonyLib.HarmonyMethod(typeof(CustomBlueprintBuilder.ResourcesLibraryTryGetBlueprintFallbackPatch).GetMethod("Postfix", BindingFlags.NonPublic | BindingFlags.Static)) { priority = HarmonyLib.Priority.First }), + new MethodPatch(typeof(ResourcesLibrary).GetMethods().Single(m => m.Name == "TryGetBlueprint" && m.ReturnType == typeof(BlueprintScriptableObject)), + new HarmonyLib.HarmonyMethod(typeof(CustomBlueprintBuilder.ResourcesLibraryTryGetBlueprintModPatch).GetMethod("Prefix", BindingFlags.NonPublic | BindingFlags.Static)), + new HarmonyLib.HarmonyMethod(typeof(CustomBlueprintBuilder.ResourcesLibraryTryGetBlueprintModPatch).GetMethod("Postfix", BindingFlags.NonPublic | BindingFlags.Static))), + new MethodPatch( + typeof(Game).GetMethod("OnAreaLoaded", BindingFlags.NonPublic | BindingFlags.Instance), + postfix: new HarmonyLib.HarmonyMethod(typeof(GameOnAreaLoadedPatch).GetMethod("Postfix", BindingFlags.NonPublic | BindingFlags.Static))), + new MethodPatch( + typeof(Player).GetMethod("PostLoad"), + postfix: new HarmonyLib.HarmonyMethod(typeof(PlayerPostLoadPatch).GetMethod("Postfix", BindingFlags.NonPublic | BindingFlags.Static))), }; - public static UnityModManager.ModEntry ModEntry; + /// UI selections made within Unity Mod Manager + public static Selections Selections; + + /// Settings that are saved and managed by Unity Mod Manager public static Settings ModSettings; + + /// Dictionaries that are loaded from configs and such + public static DictionaryData LoadedData; + + public static UnityModManager.ModEntry ModEntry; public static CraftMagicItemsAccessors Accessors; - public static ItemCraftingData[] ItemCraftingData; - public static CustomLootItem[] CustomLootItems; - - private static bool modEnabled = true; - private static Harmony12.HarmonyInstance harmonyInstance; - private static CraftMagicItemsBlueprintPatcher blueprintPatcher; - private static OpenSection currentSection = OpenSection.CraftMagicItemsSection; - private static readonly Dictionary SelectedIndex = new Dictionary(); - private static int selectedCasterLevel; - private static bool selectedShowPreparedSpells; - private static bool selectedDoubleWeaponSecondEnd; - private static bool selectedShieldWeapon; - private static int selectedCastsPerDay; - private static BlueprintItemEquipment selectedBaseBlueprint; - private static string selectedCustomName; - private static BlueprintItem upgradingBlueprint; - private static bool selectedBondWithNewObject; - private static UnitEntityData currentCaster; - - private static readonly Dictionary>> SpellIdToItem = - new Dictionary>>(); - - private static readonly Dictionary> SubCraftingData = new Dictionary>(); - private static readonly Dictionary TypeToItem = new Dictionary(); - private static readonly Dictionary> EnchantmentIdToItem = new Dictionary>(); - private static readonly Dictionary> EnchantmentIdToRecipe = new Dictionary>(); - private static readonly Dictionary> MaterialToRecipe = new Dictionary>(); - private static readonly Dictionary EnchantmentIdToCost = new Dictionary(); #if PATCH21 - private static readonly List PendingLogItems = new List(); + public static readonly List PendingLogItems = new List(); #else - private static readonly List PendingLogItems = new List(); - + public static readonly List PendingLogItems = new List(); #endif - private static readonly Dictionary ItemUpgradeProjects = new Dictionary(); - private static readonly List ItemCreationProjects = new List(); - - private static readonly Random RandomGenerator = new Random(); + public static bool modEnabled = true; + public static CraftMagicItemsBlueprintPatcher blueprintPatcher; - /** - * Patch all HarmonyPatch classes in the assembly, starting in the order of the methods named in methodNameOrder, and the rest after that. - */ - private static void PatchAllOrdered(params string[] methodNameOrder) { - var allAttributes = Assembly.GetExecutingAssembly().GetTypes() - .Select(type => new {type, methods = Harmony12.HarmonyMethodExtensions.GetHarmonyMethods(type)}) - .Where(pair => pair.methods != null && pair.methods.Count > 0) - .Select(pair => new {pair.type, attributes = Harmony12.HarmonyMethod.Merge(pair.methods)}) - .OrderBy(pair => methodNameOrder - .Select((name, index) => new {name, index}) - .FirstOrDefault(nameIndex => nameIndex.name.Equals(pair.attributes.methodName))?.index - ?? methodNameOrder.Length) - ; - foreach (var pair in allAttributes) { - new Harmony12.PatchProcessor(harmonyInstance, pair.type, pair.attributes).Patch(); - } - } + public static readonly Dictionary ItemUpgradeProjects = new Dictionary(); + public static readonly List ItemCreationProjects = new List(); - /** - * Unpatch all HarmonyPatch classes for harmonyInstance, except the ones whose method names match exceptMethodName - */ - private static void UnpatchAllExcept(params string[] exceptMethodName) { - if (harmonyInstance != null) { - try { - foreach (var method in harmonyInstance.GetPatchedMethods().ToArray()) { - if (!exceptMethodName.Contains(method.Name) && harmonyInstance.GetPatchInfo(method).Owners.Contains(harmonyInstance.Id)) { - harmonyInstance.Unpatch(method, Harmony12.HarmonyPatchType.All, harmonyInstance.Id); - } - } - } catch (Exception e) { - ModEntry.Logger.Error($"Exception during Un-patching: {e}"); - } - } - } + private static readonly Random RandomGenerator = new Random(); // ReSharper disable once UnusedMember.Local - private static void Load(UnityModManager.ModEntry modEntry) { - try { + private static void Load(UnityModManager.ModEntry modEntry) + { + HarmonyPatcher patcher = new HarmonyPatcher(modEntry.Logger.Error); + try + { + Selections = new Selections(); ModEntry = modEntry; ModSettings = UnityModManager.ModSettings.Load(modEntry); - SelectedIndex[CustomPriceLabel] = Mathf.Abs(ModSettings.CraftingPriceScale - 1f) < 0.001 ? 0 : + Selections.SelectedIndex[CustomPriceLabel] = Mathf.Abs(ModSettings.CraftingPriceScale - 1f) < 0.001 ? 0 : Mathf.Abs(ModSettings.CraftingPriceScale - 2f) < 0.001 ? 1 : 2; modEnabled = modEntry.Active; modEntry.OnSaveGUI = OnSaveGui; modEntry.OnToggle = OnToggle; modEntry.OnGUI = OnGui; CustomBlueprintBuilder.InitialiseBlueprintRegex(CraftMagicItemsBlueprintPatcher.BlueprintRegex); - harmonyInstance = Harmony12.HarmonyInstance.Create("kingmaker.craftMagicItems"); - // Patch the recovery methods first. - PatchAllOrdered("TryGetBlueprint", "PostLoad", "OnAreaLoaded"); + patcher.PatchAllOrdered(MethodPatchList); // Patch the recovery methods first. Accessors = new CraftMagicItemsAccessors(); blueprintPatcher = new CraftMagicItemsBlueprintPatcher(Accessors, modEnabled); - } catch (Exception e) { + } + catch (Exception e) + { modEntry.Logger.Error($"Exception during Load: {e}"); modEnabled = false; CustomBlueprintBuilder.Enabled = false; // Unpatch everything except methods involved in recovering a save when mod is disabled. - UnpatchAllExcept("TryGetBlueprint", "PostLoad", "OnAreaLoaded"); + patcher.UnpatchAllExcept(MethodPatchList); throw; } } @@ -332,7 +176,7 @@ private static bool OnToggle(UnityModManager.ModEntry modEntry, bool enabled) { private static void OnGui(UnityModManager.ModEntry modEntry) { if (!modEnabled) { - RenderLabel("The mod is disabled. Loading saved games with custom items and feats will cause them to revert to regular versions."); + UmmUiRenderer.RenderLabelRow("The mod is disabled. Loading saved games with custom items and feats will cause them to revert to regular versions."); return; } @@ -344,66 +188,53 @@ private static void OnGui(UnityModManager.ModEntry modEntry) { && Game.Instance.CurrentMode != GameModeType.EscMode && Game.Instance.CurrentMode != GameModeType.Rest && Game.Instance.CurrentMode != GameModeType.Kingdom) { - RenderLabel("Item crafting is not available in this game state."); + UmmUiRenderer.RenderLabelRow("Item crafting is not available in this game state."); return; } GUILayout.BeginVertical(); - RenderLabel($"Number of custom Craft Magic Items blueprints loaded: {CustomBlueprintBuilder.CustomBlueprintIDs.Count}"); + UmmUiRenderer.RenderLabelRow($"Number of custom Craft Magic Items blueprints loaded: {CustomBlueprintBuilder.CustomBlueprintIDs.Count}"); GetSelectedCrafter(true); - if (RenderToggleSection(ref currentSection, OpenSection.CraftMagicItemsSection, "Craft Magic Items")) { + //render toggleable views in the main functionality of the mod + if (UmmUiRenderer.RenderToggleSection("Craft Magic Items", Selections.CurrentSection == OpenSection.CraftMagicItemsSection)) + { + Selections.CurrentSection = OpenSection.CraftMagicItemsSection; RenderCraftMagicItemsSection(); } - if (RenderToggleSection(ref currentSection, OpenSection.CraftMundaneItemsSection, "Craft Mundane Items")) { + if (UmmUiRenderer.RenderToggleSection("Craft Mundane Items", Selections.CurrentSection == OpenSection.CraftMundaneItemsSection)) + { + Selections.CurrentSection = OpenSection.CraftMundaneItemsSection; RenderCraftMundaneItemsSection(); } - if (RenderToggleSection(ref currentSection, OpenSection.ProjectsSection, "Work in Progress")) { + if (UmmUiRenderer.RenderToggleSection("Work in Progress", Selections.CurrentSection == OpenSection.ProjectsSection)) + { + Selections.CurrentSection = OpenSection.ProjectsSection; RenderProjectsSection(); } - if (RenderToggleSection(ref currentSection, OpenSection.FeatsSection, "Feat Reassignment")) { - RenderFeatReassignmentSection(); + if (UmmUiRenderer.RenderToggleSection("Feat Reassignment", Selections.CurrentSection == OpenSection.FeatsSection)) + { + Selections.CurrentSection = OpenSection.FeatsSection; + UserInterfaceEventHandlingLogic.RenderFeatReassignmentSection(FeatReassignmentSectionRendererFactory.GetFeatReassignmentSectionRenderer()); } - if (RenderToggleSection(ref currentSection, OpenSection.CheatsSection, "Cheats")) { - RenderCheatsSection(); + if (UmmUiRenderer.RenderToggleSection("Cheats", Selections.CurrentSection == OpenSection.CheatsSection)) + { + Selections.CurrentSection = OpenSection.CheatsSection; + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(CheatSectionRendererFactory.GetCheatSectionRenderer(), ModSettings, CustomPriceLabel); } GUILayout.EndVertical(); - } catch (Exception e) { - modEntry.Logger.Error($"Error rendering GUI: {e}"); } - } - - public static string L10NFormat(UnitEntityData sourceUnit, string key, params object[] args) { - // Set GameLogContext so the caster will be used when generating localized strings. - GameLogContext.SourceUnit = sourceUnit; - var template = new L10NString(key); - var result = string.Format(template.ToString(), args); - GameLogContext.Clear(); - return result; - } - - private static string L10NFormat(string key, params object[] args) { - return L10NFormat(currentCaster ?? GetSelectedCrafter(false), key, args); - } - - private static bool RenderToggleSection(ref OpenSection current, OpenSection mySection, string label) { - GUILayout.BeginVertical("box"); - GUILayout.BeginHorizontal(); - bool toggledOn = GUILayout.Toggle(current == mySection, " " + label + ""); - if (toggledOn) { - current = mySection; + catch (Exception e) + { + modEntry.Logger.Error($"Error rendering GUI: {e}"); } - - GUILayout.EndHorizontal(); - GUILayout.EndVertical(); - return toggledOn; } public static T ReadJsonFile(string fileName, params JsonConverter[] converters) { @@ -423,7 +254,7 @@ public static T ReadJsonFile(string fileName, params JsonConverter[] converte } } - private static CraftingTimerComponent GetCraftingTimerComponentForCaster(UnitDescriptor caster, bool create = false) { + public static CraftingTimerComponent GetCraftingTimerComponentForCaster(UnitDescriptor caster, bool create = false) { // Manually search caster.Buffs rather than using GetFact, because we don't want to TryGetBlueprint if the mod is disabled. var timerBuff = caster.Buffs.Enumerable.FirstOrDefault(fact => fact.Blueprint.AssetGuid == CraftMagicItemsBlueprintPatcher.TimerBlueprintGuid); if (timerBuff == null) { @@ -478,14 +309,16 @@ public static BondedItemComponent GetBondedItemComponentForCaster(UnitDescriptor return bondedItemBuff.SelectComponents().First(); } + /// Renders the crafting menu (by selected character) private static void RenderCraftMagicItemsSection() { var caster = GetSelectedCrafter(false); if (caster == null) { return; } + //can the character have a bonded item? var hasBondedItemFeature = - caster.Descriptor.Progression.Features.Enumerable.Any(feature => BondedItemFeatures.Contains(feature.Blueprint.AssetGuid)); + caster.Descriptor.Progression.Features.Enumerable.Any(feature => Features.BondedItemFeatures.Contains(feature.Blueprint.AssetGuid)); var bondedItemData = GetBondedItemComponentForCaster(caster.Descriptor); if (!hasBondedItemFeature && bondedItemData && bondedItemData.ownerItem != null) { @@ -501,20 +334,27 @@ private static void RenderCraftMagicItemsSection() { } } - var itemTypes = ItemCraftingData + //what crafting options are available (which feats are available for the selected character) + var itemTypes = LoadedData.ItemCraftingData .Where(data => data.FeatGuid != null && (ModSettings.IgnoreCraftingFeats || CharacterHasFeat(caster, data.FeatGuid))) .ToArray(); if (!Enumerable.Any(itemTypes) && !hasBondedItemFeature) { - RenderLabel($"{caster.CharacterName} does not know any crafting feats."); + UmmUiRenderer.RenderLabelRow($"{caster.CharacterName} does not know any crafting feats."); return; } + //list the selection items var itemTypeNames = itemTypes.Select(data => new L10NString(data.NameId).ToString()) .PrependConditional(hasBondedItemFeature, new L10NString("craftMagicItems-bonded-object-name")).ToArray(); - var selectedItemTypeIndex = RenderSelection("Crafting: ", itemTypeNames, 6, ref selectedCustomName, false); + + //render whatever the user has selected + var selectedItemTypeIndex = DrawSelectionUserInterfaceElements("Crafting: ", itemTypeNames, 6, ref Selections.SelectedCustomName, false); + + //render options for actual selection if (hasBondedItemFeature && selectedItemTypeIndex == 0) { RenderBondedItemCrafting(caster); - } else { + } + else { var craftingData = itemTypes[hasBondedItemFeature ? selectedItemTypeIndex - 1 : selectedItemTypeIndex]; if (craftingData is SpellBasedItemCraftingData spellBased) { RenderSpellBasedCrafting(caster, spellBased); @@ -523,13 +363,14 @@ private static void RenderCraftMagicItemsSection() { } } - RenderLabel($"Current Money: {Game.Instance.Player.Money}"); + //render current cash + UmmUiRenderer.RenderLabelRow($"Current Money: {Game.Instance.Player.Money}"); } private static RecipeBasedItemCraftingData GetBondedItemCraftingData(BondedItemComponent bondedComponent) { // Find crafting data relevant to the bonded item - return ItemCraftingData.OfType() - .First(data => data.Slots.Contains(bondedComponent.ownerItem.Blueprint.ItemType) && !IsMundaneCraftingData(data)); + return LoadedData.ItemCraftingData.OfType() + .First(data => data.Slots.Contains(bondedComponent.ownerItem.Blueprint.ItemType) && !CraftingLogic.IsMundaneCraftingData(data)); } private static void UnBondFromCurrentBondedItem(UnitEntityData caster) { @@ -555,7 +396,7 @@ private static void UnBondFromCurrentBondedItem(UnitEntityData caster) { } } - private static void BondWithObject(UnitEntityData caster, ItemEntity item) { + public static void BondWithObject(UnitEntityData caster, ItemEntity item) { UnBondFromCurrentBondedItem(caster); var bondedComponent = GetBondedItemComponentForCaster(caster.Descriptor, true); bondedComponent.ownerItem = item; @@ -571,28 +412,28 @@ private static void RenderBondedItemCrafting(UnitEntityData caster) { var projects = GetCraftingTimerComponentForCaster(caster.Descriptor); var ritualProject = projects == null ? null : projects.CraftingProjects.FirstOrDefault(project => project.ItemType == BondedItemRitual); if (ritualProject != null) { - RenderLabel($"{caster.CharacterName} is in the process of bonding with {ritualProject.ResultItem.Name}"); + UmmUiRenderer.RenderLabelRow($"{caster.CharacterName} is in the process of bonding with {ritualProject.ResultItem.Name}"); return; } var bondedComponent = GetBondedItemComponentForCaster(caster.Descriptor); var characterCasterLevel = CharacterCasterLevel(caster.Descriptor); - if (bondedComponent == null || bondedComponent.ownerItem == null || selectedBondWithNewObject) { - if (selectedBondWithNewObject) { - RenderLabel("You may bond with a different object by performing a special ritual that costs 200 gp per caster level. This ritual takes 8 " + + if (bondedComponent == null || bondedComponent.ownerItem == null || Selections.SelectedBondWithNewObject) { + if (Selections.SelectedBondWithNewObject) { + UmmUiRenderer.RenderLabelRow("You may bond with a different object by performing a special ritual that costs 200 gp per caster level. This ritual takes 8 " + "hours to complete. Items replaced in this way do not possess any of the additional enchantments of the previous bonded item, " + "and the previous bonded item loses any enchantments you added via your bond."); if (GUILayout.Button("Cancel bonding to a new object")) { - selectedBondWithNewObject = false; + Selections.SelectedBondWithNewObject = false; } } - RenderLabel( + UmmUiRenderer.RenderLabelRow( "You can enchant additional magic abilities to your bonded item as if you had the required Item Creation Feat, as long as you also " + "meet the caster level prerequisite of the feat. Abilities added in this fashion function only for you, and no-one else can add " + "enchantments to your bonded item."); - RenderLabel(new L10NString("craftMagicItems-bonded-item-glossary")); - RenderLabel("Choose your bonded item."); + UmmUiRenderer.RenderLabelRow(new L10NString("craftMagicItems-bonded-item-glossary")); + UmmUiRenderer.RenderLabelRow("Choose your bonded item."); var names = BondedItemSlots.Select(slot => new L10NString(GetSlotStringKey(slot, null)).ToString()).ToArray(); - var selectedItemSlotIndex = RenderSelection("Item type", names, 10); + var selectedItemSlotIndex = DrawSelectionUserInterfaceElements("Item type", names, 10); var selectedSlot = BondedItemSlots[selectedItemSlotIndex]; var items = Game.Instance.Player.Inventory .Where(item => item.Blueprint is BlueprintItemEquipment blueprint @@ -604,31 +445,31 @@ private static void RenderBondedItemCrafting(UnitEntityData caster) { .OrderBy(item => item.Name) .ToArray(); if (items.Length == 0) { - RenderLabel("You do not have any item of that type currently equipped."); + UmmUiRenderer.RenderLabelRow("You do not have any item of that type currently equipped."); return; } var itemNames = items.Select(item => item.Name).ToArray(); - var selectedUpgradeItemIndex = RenderSelection("Item: ", itemNames, 5); + var selectedUpgradeItemIndex = DrawSelectionUserInterfaceElements("Item: ", itemNames, 5); var selectedItem = items[selectedUpgradeItemIndex]; - var goldCost = !selectedBondWithNewObject || ModSettings.CraftingCostsNoGold ? 0 : 200 * characterCasterLevel; + var goldCost = !Selections.SelectedBondWithNewObject || ModSettings.CraftingCostsNoGold ? 0 : 200 * characterCasterLevel; var canAfford = BuildCostString(out var cost, null, goldCost); var label = $"Make {selectedItem.Name} your bonded item{(goldCost == 0 ? "" : " for " + cost)}"; if (!canAfford) { - RenderLabel(label); + UmmUiRenderer.RenderLabelRow(label); } else if (GUILayout.Button(label)) { if (goldCost > 0) { Game.Instance.UI.Common.UISound.Play(UISoundType.LootCollectGold); Game.Instance.Player.SpendMoney(goldCost); } - if (selectedBondWithNewObject) { - selectedBondWithNewObject = false; + if (Selections.SelectedBondWithNewObject) { + Selections.SelectedBondWithNewObject = false; if (!ModSettings.CraftingTakesNoTime) { // Create project - AddBattleLogMessage(L10NFormat("craftMagicItems-logMessage-begin-ritual-bonded-item", cost, selectedItem.Name)); + AddBattleLogMessage(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-begin-ritual-bonded-item", cost, selectedItem.Name)); var project = new CraftingProjectData(caster, ModSettings.MagicCraftingRate, goldCost, 0, selectedItem, BondedItemRitual); AddNewProject(caster.Descriptor, project); CalculateProjectEstimate(project); - currentSection = OpenSection.ProjectsSection; + Selections.CurrentSection = OpenSection.ProjectsSection; return; } } @@ -638,15 +479,15 @@ private static void RenderBondedItemCrafting(UnitEntityData caster) { GUILayout.BeginHorizontal(); GUILayout.Label($"Your bonded item: {bondedComponent.ownerItem.Name}"); if (GUILayout.Button("Bond with a different item", GUILayout.ExpandWidth(false))) { - selectedBondWithNewObject = true; + Selections.SelectedBondWithNewObject = true; } GUILayout.EndHorizontal(); var craftingData = GetBondedItemCraftingData(bondedComponent); if (bondedComponent.ownerItem.Wielder != null && !IsPlayerInCapital() && !Game.Instance.Player.PartyCharacters.Contains(bondedComponent.ownerItem.Wielder.Unit)) { - RenderLabel($"You cannot enchant {bondedComponent.ownerItem.Name} because you cannot currently access it."); + UmmUiRenderer.RenderLabelRow($"You cannot enchant {bondedComponent.ownerItem.Name} because you cannot currently access it."); } else if (!ModSettings.IgnoreFeatCasterLevelRestriction && characterCasterLevel < craftingData.MinimumCasterLevel) { - RenderLabel($"You will not be able to enchant your bonded item until your caster level reaches {craftingData.MinimumCasterLevel} " + + UmmUiRenderer.RenderLabelRow($"You will not be able to enchant your bonded item until your caster level reaches {craftingData.MinimumCasterLevel} " + $"(currently {characterCasterLevel})."); } else { RenderRecipeBasedCrafting(caster, craftingData, bondedComponent.ownerItem); @@ -657,41 +498,41 @@ private static void RenderBondedItemCrafting(UnitEntityData caster) { private static void RenderSpellBasedCrafting(UnitEntityData caster, SpellBasedItemCraftingData craftingData) { var spellbooks = caster.Descriptor.Spellbooks.Where(book => book.CasterLevel > 0).ToList(); if (spellbooks.Count == 0) { - RenderLabel($"{caster.CharacterName} is not yet able to cast spells."); + UmmUiRenderer.RenderLabelRow($"{caster.CharacterName} is not yet able to cast spells."); return; } var selectedSpellbookIndex = 0; if (spellbooks.Count != 1) { var spellBookNames = spellbooks.Select(book => book.Blueprint.Name.ToString()).ToArray(); - selectedSpellbookIndex = RenderSelection("Class: ", spellBookNames, 10); + selectedSpellbookIndex = DrawSelectionUserInterfaceElements("Class: ", spellBookNames, 10); } var spellbook = spellbooks[selectedSpellbookIndex]; var maxLevel = Math.Min(spellbook.MaxSpellLevel, craftingData.MaxSpellLevel); var spellLevelNames = Enumerable.Range(0, maxLevel + 1).Select(index => $"Level {index}").ToArray(); - var spellLevel = RenderSelection("Spell level: ", spellLevelNames, 10); + var spellLevel = DrawSelectionUserInterfaceElements("Spell level: ", spellLevelNames, 10); if (spellLevel > 0 && !spellbook.Blueprint.Spontaneous) { if (ModSettings.CraftingTakesNoTime) { - selectedShowPreparedSpells = true; + Selections.SelectedShowPreparedSpells = true; } else { GUILayout.BeginHorizontal(); - selectedShowPreparedSpells = GUILayout.Toggle(selectedShowPreparedSpells, " Show prepared spells only"); + Selections.SelectedShowPreparedSpells = GUILayout.Toggle(Selections.SelectedShowPreparedSpells, " Show prepared spells only"); GUILayout.EndHorizontal(); } } else { - selectedShowPreparedSpells = false; + Selections.SelectedShowPreparedSpells = false; } List spellOptions; - if (selectedShowPreparedSpells) { + if (Selections.SelectedShowPreparedSpells) { // Prepared spellcaster spellOptions = spellbook.GetMemorizedSpells(spellLevel).Where(slot => slot.Available).Select(slot => slot.Spell).ToList(); } else { // Cantrips/Orisons or Spontaneous spellcaster or showing all known spells if (spellLevel > 0 && spellbook.Blueprint.Spontaneous) { var spontaneousSlots = spellbook.GetSpontaneousSlots(spellLevel); - RenderLabel($"{caster.CharacterName} can cast {spontaneousSlots} more level {spellLevel} spells today."); + UmmUiRenderer.RenderLabelRow($"{caster.CharacterName} can cast {spontaneousSlots} more level {spellLevel} spells today."); if (spontaneousSlots == 0 && ModSettings.CraftingTakesNoTime) { return; } @@ -701,20 +542,20 @@ private static void RenderSpellBasedCrafting(UnitEntityData caster, SpellBasedIt } if (!spellOptions.Any()) { - RenderLabel($"{caster.CharacterName} does not know any level {spellLevel} spells."); + UmmUiRenderer.RenderLabelRow($"{caster.CharacterName} does not know any level {spellLevel} spells."); } else { var minCasterLevel = Math.Max(1, 2 * spellLevel - 1); var maxCasterLevel = CharacterCasterLevel(caster.Descriptor, spellbook); if (minCasterLevel < maxCasterLevel) { - RenderIntSlider(ref selectedCasterLevel, "Caster level: ", minCasterLevel, maxCasterLevel); + Selections.SelectedCasterLevel = UmmUiRenderer.RenderIntSlider("Caster level: ", Selections.SelectedCasterLevel, minCasterLevel, maxCasterLevel); } else { - selectedCasterLevel = minCasterLevel; - RenderLabel($"Caster level: {selectedCasterLevel}"); + Selections.SelectedCasterLevel = minCasterLevel; + UmmUiRenderer.RenderLabelRow($"Caster level: {Selections.SelectedCasterLevel}"); } - RenderCraftingSkillInformation(caster, StatType.SkillKnowledgeArcana, 5 + selectedCasterLevel, selectedCasterLevel); + RenderCraftingSkillInformation(caster, StatType.SkillKnowledgeArcana, 5 + Selections.SelectedCasterLevel, Selections.SelectedCasterLevel); - if (selectedShowPreparedSpells && spellbook.GetSpontaneousConversionSpells(spellLevel).Any()) { + if (Selections.SelectedShowPreparedSpells && spellbook.GetSpontaneousConversionSpells(spellLevel).Any()) { var firstSpell = spellbook.Blueprint.Spontaneous ? spellbook.GetKnownSpells(spellLevel).First(spell => true) : spellbook.GetMemorizedSpells(spellLevel).FirstOrDefault(slot => slot.Available)?.Spell; @@ -728,16 +569,23 @@ private static void RenderSpellBasedCrafting(UnitEntityData caster, SpellBasedIt } } - foreach (var spell in spellOptions.OrderBy(spell => spell.Name).GroupBy(spell => spell.Name).Select(group => group.First())) { - if (spell.MetamagicData != null && spell.MetamagicData.NotEmpty) { + foreach (var spell in spellOptions.OrderBy(spell => spell.Name).GroupBy(spell => spell.Name).Select(group => group.First())) + { + if (spell.MetamagicData != null && spell.MetamagicData.NotEmpty) + { GUILayout.Label($"Cannot craft {new L10NString(craftingData.NameId)} of {spell.Name} with metamagic applied."); - } else if (spell.Blueprint.Variants != null) { + } + else if (spell.Blueprint.Variants != null) + { // Spells with choices (e.g. Protection from Alignment, which can be Protection from Evil, Good, Chaos or Law) - foreach (var variant in spell.Blueprint.Variants) { - RenderSpellBasedCraftItemControl(caster, craftingData, spell, variant, spellLevel, selectedCasterLevel); + foreach (var variant in spell.Blueprint.Variants) + { + AttemptSpellBasedCraftItemAndRender(caster, craftingData, spell, variant, spellLevel, Selections.SelectedCasterLevel); } - } else { - RenderSpellBasedCraftItemControl(caster, craftingData, spell, spell.Blueprint, spellLevel, selectedCasterLevel); + } + else + { + AttemptSpellBasedCraftItemAndRender(caster, craftingData, spell, spell.Blueprint, spellLevel, Selections.SelectedCasterLevel); } } } @@ -775,7 +623,7 @@ private static string GetSlotStringKey(ItemsFilter.ItemType slot, SlotRestrictio } } - private static IEnumerable GetEnchantments(BlueprintItem blueprint, RecipeData sourceRecipe = null) { + public static IEnumerable GetEnchantments(BlueprintItem blueprint, RecipeData sourceRecipe = null) { if (blueprint is BlueprintItemShield shield) { // A shield can be treated as armor or as a weapon... assume armor unless being used by a recipe which applies to weapons. var weaponRecipe = sourceRecipe?.OnlyForSlots?.Contains(ItemsFilter.ItemType.Weapon) ?? false; @@ -807,7 +655,7 @@ public static BlueprintItem GetBaseBlueprint(BlueprintItem blueprint) { } } - private static string GetBlueprintItemType(BlueprintItem blueprint) { + public static string GetBlueprintItemType(BlueprintItem blueprint) { string assetGuid = null; switch (blueprint) { case BlueprintItemArmor armor: assetGuid = armor.Type.AssetGuid; break; @@ -819,17 +667,17 @@ private static string GetBlueprintItemType(BlueprintItem blueprint) { private static BlueprintItem GetStandardItem(BlueprintItem blueprint) { string assetGuid = GetBlueprintItemType(blueprint); - return !string.IsNullOrEmpty(assetGuid) && TypeToItem.ContainsKey(assetGuid) ? TypeToItem[assetGuid] : null; + return !string.IsNullOrEmpty(assetGuid) && LoadedData.TypeToItem.ContainsKey(assetGuid) ? LoadedData.TypeToItem[assetGuid] : null; } public static RecipeData FindSourceRecipe(string selectedEnchantmentId, BlueprintItem blueprint) { List recipes = null; - if (EnchantmentIdToRecipe.ContainsKey(selectedEnchantmentId)) { - recipes = EnchantmentIdToRecipe[selectedEnchantmentId]; + if (LoadedData.EnchantmentIdToRecipe.ContainsKey(selectedEnchantmentId)) { + recipes = LoadedData.EnchantmentIdToRecipe[selectedEnchantmentId]; } else { foreach (var material in blueprintPatcher.PhysicalDamageMaterialEnchantments.Keys) { if (blueprintPatcher.PhysicalDamageMaterialEnchantments[material] == selectedEnchantmentId) { - recipes = MaterialToRecipe[material]; + recipes = LoadedData.MaterialToRecipe[material]; } } } @@ -852,14 +700,14 @@ private static string FindSupersededEnchantmentId(BlueprintItem blueprint, strin // Special case - enchanting a masterwork item supersedes the masterwork quality if (IsMasterwork(blueprint)) { - return MasterworkGuid; + return ItemQualityBlueprints.MasterworkGuid; } } return null; } - private static bool DoesItemMatchAllEnchantments(BlueprintItemEquipment blueprint, string selectedEnchantmentId, + public static bool DoesItemMatchAllEnchantments(BlueprintItemEquipment blueprint, string selectedEnchantmentId, string selectedEnchantmentIdSecond = null, BlueprintItemEquipment upgradeItem = null, bool checkPrice = false) { var isNotable = upgradeItem && upgradeItem.IsNotable; var ability = upgradeItem ? upgradeItem.Ability : null; @@ -927,11 +775,11 @@ private static IEnumerable PrependConditional(this IEnumerable target, return prepend ? items.Concat(target ?? throw new ArgumentException(nameof(target))) : target; } - private static string Join(this IEnumerable enumeration, string delimiter = ", ") { + public static string Join(this IEnumerable enumeration, string delimiter = ", ") { return enumeration.Aggregate("", (prev, curr) => prev + (prev != "" ? delimiter : "") + curr.ToString()); } - private static string BuildCommaList(this IEnumerable list, bool or) { + public static string BuildCommaList(this IEnumerable list, bool or) { var array = list.ToArray(); if (array.Length < 2) { return array.Join(); @@ -947,15 +795,15 @@ private static string BuildCommaList(this IEnumerable list, bool or) { } var key = or ? "craftMagicItems-logMessage-comma-list-or" : "craftMagicItems-logMessage-comma-list-and"; - return L10NFormat(key, commaList, array[array.Length - 1]); + return LocalizationHelper.FormatLocalizedString(key, commaList, array[array.Length - 1]); } private static bool IsMasterwork(BlueprintItem blueprint) { - return GetEnchantments(blueprint).Any(enchantment => enchantment.AssetGuid == MasterworkGuid); + return GetEnchantments(blueprint).Any(enchantment => enchantment.AssetGuid == ItemQualityBlueprints.MasterworkGuid); } - private static bool IsOversized(BlueprintItem blueprint) { - return GetEnchantments(blueprint).Any(enchantment => enchantment.AssetGuid.StartsWith(OversizedGuid) && !enchantment.GetComponent()); + public static bool IsOversized(BlueprintItem blueprint) { + return GetEnchantments(blueprint).Any(enchantment => enchantment.AssetGuid.StartsWith(ItemQualityBlueprints.OversizedGuid) && !enchantment.GetComponent()); } // Use instead of UIUtility.IsMagicItem. @@ -1084,7 +932,7 @@ private static bool RecipeAppliesToBlueprint(RecipeData recipe, BlueprintItem bl ; } - private static ItemEntity BuildItemEntity(BlueprintItem blueprint, ItemCraftingData craftingData, UnitEntityData crafter) { + public static ItemEntity BuildItemEntity(BlueprintItem blueprint, ItemCraftingData craftingData, UnitEntityData crafter) { var item = blueprint.CreateEntity(); item.Identify(); item.SetVendorIfNull(crafter); @@ -1126,7 +974,7 @@ private static bool DoesBlueprintMatchRestrictions(BlueprintItemEquipment bluepr return true; } - private static string GetBonusString(int bonus, RecipeData recipe) { + public static string GetBonusString(int bonus, RecipeData recipe) { bonus *= recipe.BonusMultiplier == 0 ? 1 : recipe.BonusMultiplier; return recipe.BonusDieSize != 0 ? new DiceFormula(bonus, recipe.BonusDieSize).ToString() : bonus.ToString(); } @@ -1172,23 +1020,27 @@ private static bool DoesItemMatchLocationFilter(ItemEntity item, UnitEntityData private static void RenderRecipeBasedCrafting(UnitEntityData caster, RecipeBasedItemCraftingData craftingData, ItemEntity upgradeItem = null) { ItemsFilter.ItemType selectedSlot; + + //specific item if (upgradeItem != null) { selectedSlot = upgradeItem.Blueprint.ItemType; while (ItemUpgradeProjects.ContainsKey(upgradeItem)) { upgradeItem = ItemUpgradeProjects[upgradeItem].ResultItem; } - RenderLabel($"Enchanting {upgradeItem.Name}"); - } else { + UmmUiRenderer.RenderLabelRow($"Enchanting {upgradeItem.Name}"); + } + //slot + else { // Choose slot/weapon type. var selectedItemSlotIndex = 0; if (craftingData.Slots.Length > 1) { var names = craftingData.Slots.Select(slot => new L10NString(GetSlotStringKey(slot, craftingData.SlotRestrictions)).ToString()).ToArray(); - selectedItemSlotIndex = RenderSelection("Item type", names, 10, ref selectedCustomName); + selectedItemSlotIndex = DrawSelectionUserInterfaceElements("Item type", names, 10, ref Selections.SelectedCustomName); } var locationFilter = ItemLocationFilter.All; var locationNames = Enum.GetNames(typeof(ItemLocationFilter)); - locationFilter = (ItemLocationFilter)RenderSelection("Item location", locationNames, locationNames.Length, ref selectedCustomName); + locationFilter = (ItemLocationFilter)DrawSelectionUserInterfaceElements("Item location", locationNames, locationNames.Length, ref Selections.SelectedCustomName); selectedSlot = craftingData.Slots[selectedItemSlotIndex]; var playerInCapital = IsPlayerInCapital(); @@ -1219,11 +1071,11 @@ private static void RenderRecipeBasedCrafting(UnitEntityData caster, RecipeBased var itemNames = items.Select(item => item.Name).PrependConditional(canCreateNew, new L10NString("craftMagicItems-label-craft-new-item")) .ToArray(); if (itemNames.Length == 0) { - RenderLabel($"{caster.CharacterName} can not access any items of that type."); + UmmUiRenderer.RenderLabelRow($"{caster.CharacterName} can not access any items of that type."); return; } - var selectedUpgradeItemIndex = RenderSelection("Item: ", itemNames, 5, ref selectedCustomName); + var selectedUpgradeItemIndex = DrawSelectionUserInterfaceElements("Item: ", itemNames, 5, ref Selections.SelectedCustomName); // See existing item details and enchantments. var index = selectedUpgradeItemIndex - (canCreateNew ? 1 : 0); upgradeItem = index < 0 ? null : items[index]; @@ -1236,11 +1088,11 @@ private static void RenderRecipeBasedCrafting(UnitEntityData caster, RecipeBased if (upgradeItemDoubleWeapon != null && upgradeItemDoubleWeapon.Blueprint.Double) { GUILayout.BeginHorizontal(); GUILayout.Label($"{upgradeItem.Name} is a double weapon; enchanting ", GUILayout.ExpandWidth(false)); - var label = selectedDoubleWeaponSecondEnd ? "Secondary end" : "Primary end"; + var label = Selections.SelectedDoubleWeaponSecondEnd ? "Secondary end" : "Primary end"; if (GUILayout.Button(label, GUILayout.ExpandWidth(false))) { - selectedDoubleWeaponSecondEnd = !selectedDoubleWeaponSecondEnd; + Selections.SelectedDoubleWeaponSecondEnd = !Selections.SelectedDoubleWeaponSecondEnd; } - if (selectedDoubleWeaponSecondEnd) { + if (Selections.SelectedDoubleWeaponSecondEnd) { upgradeItem = upgradeItemDoubleWeapon.Second; } else { upgradeItemDoubleWeapon = null; @@ -1252,23 +1104,23 @@ private static void RenderRecipeBasedCrafting(UnitEntityData caster, RecipeBased if (upgradeItemShieldWeapon != null) { GUILayout.BeginHorizontal(); GUILayout.Label($"{upgradeItem.Name} is a shield; enchanting ", GUILayout.ExpandWidth(false)); - var label = selectedShieldWeapon ? "Shield Bash" : "Shield"; + var label = Selections.SelectedShieldWeapon ? "Shield Bash" : "Shield"; if (GUILayout.Button(label, GUILayout.ExpandWidth(false))) { - selectedShieldWeapon = !selectedShieldWeapon; + Selections.SelectedShieldWeapon = !Selections.SelectedShieldWeapon; } - if (selectedShieldWeapon) { + if (Selections.SelectedShieldWeapon) { upgradeItem = upgradeItemShieldWeapon; } else { upgradeItem = upgradeItemShieldArmor; } GUILayout.EndHorizontal(); } else { - selectedShieldWeapon = false; + Selections.SelectedShieldWeapon = false; } if (upgradeItemShield != null) { - RenderLabel(BuildItemDescription(upgradeItemShield)); + UmmUiRenderer.RenderLabelRow(BuildItemDescription(upgradeItemShield)); } else { - RenderLabel(BuildItemDescription(upgradeItem)); + UmmUiRenderer.RenderLabelRow(BuildItemDescription(upgradeItem)); } } @@ -1285,7 +1137,7 @@ private static void RenderRecipeBasedCrafting(UnitEntityData caster, RecipeBased ? new string[0] : new[] {new L10NString("craftMagicItems-label-cast-spell-n-times").ToString()}) .ToArray(); - var selectedRecipeIndex = RenderSelection("Enchantment: ", recipeNames, 5, ref selectedCustomName); + var selectedRecipeIndex = DrawSelectionUserInterfaceElements("Enchantment: ", recipeNames, 5, ref Selections.SelectedCustomName); if (selectedRecipeIndex == availableRecipes.Length) { // Cast spell N times RenderCastSpellNTimes(caster, craftingData, upgradeItemShield ?? upgradeItem, selectedSlot); @@ -1299,7 +1151,7 @@ private static void RenderRecipeBasedCrafting(UnitEntityData caster, RecipeBased .OrderBy(recipe => recipe.NameId) .ToArray(); recipeNames = availableSubRecipes.Select(recipe => recipe.NameId).ToArray(); - var selectedSubRecipeIndex = RenderSelection(category + ": ", recipeNames, 5, ref selectedCustomName); + var selectedSubRecipeIndex = DrawSelectionUserInterfaceElements(category + ": ", recipeNames, 5, ref Selections.SelectedCustomName); selectedRecipe = availableSubRecipes[selectedSubRecipeIndex]; } @@ -1332,7 +1184,7 @@ private static void RenderRecipeBasedCrafting(UnitEntityData caster, RecipeBased } return false; }))) { - RenderLabel("This item cannot be further upgraded with this enchantment."); + UmmUiRenderer.RenderLabelRow("This item cannot be further upgraded with this enchantment."); return; } else if (availableEnchantments.Length > 0 && selectedRecipe.Enchantments.Length > 1) { var counter = selectedRecipe.Enchantments.Length - availableEnchantments.Length; @@ -1340,7 +1192,7 @@ private static void RenderRecipeBasedCrafting(UnitEntityData caster, RecipeBased counter++; return enchantment.Name.Empty() ? GetBonusString(counter, selectedRecipe) : enchantment.Name; }); - selectedEnchantmentIndex = RenderSelection("", enchantmentNames.ToArray(), 6); + selectedEnchantmentIndex = DrawSelectionUserInterfaceElements("", enchantmentNames.ToArray(), 6); } selectedEnchantment = availableEnchantments[selectedEnchantmentIndex]; @@ -1352,16 +1204,16 @@ private static void RenderRecipeBasedCrafting(UnitEntityData caster, RecipeBased : selectedRecipe.Enchantments.FindIndex(e => e == selectedEnchantment) * selectedRecipe.CasterLevelMultiplier); if (selectedEnchantment != null) { if (!string.IsNullOrEmpty(selectedEnchantment.Description)) { - RenderLabel(selectedEnchantment.Description); + UmmUiRenderer.RenderLabelRow(selectedEnchantment.Description); } if (selectedRecipe.CostType == RecipeCostType.EnhancementLevelSquared) { - RenderLabel($"Plus equivalent: +{GetPlusOfRecipe(selectedRecipe, selectedRecipe.Enchantments.FindIndex(e => e == selectedEnchantment) + 1)}"); + UmmUiRenderer.RenderLabelRow($"Plus equivalent: +{GetPlusOfRecipe(selectedRecipe, selectedRecipe.Enchantments.FindIndex(e => e == selectedEnchantment) + 1)}"); } } GUILayout.BeginHorizontal(); GUILayout.Label("Prerequisites: ", GUILayout.ExpandWidth(false)); - var prerequisites = $"{CasterLevelLocalized} {casterLevel}"; + var prerequisites = $"{LocalizedStringBlueprints.CasterLevelLocalized} {casterLevel}"; if (selectedRecipe.PrerequisiteSpells != null && selectedRecipe.PrerequisiteSpells.Length > 0) { prerequisites += $"; {selectedRecipe.PrerequisiteSpells.Select(ability => ability.Name).BuildCommaList(selectedRecipe.AnyPrerequisite)}"; } @@ -1371,7 +1223,7 @@ private static void RenderRecipeBasedCrafting(UnitEntityData caster, RecipeBased } if (selectedRecipe.CrafterPrerequisites != null) { - prerequisites += "; " + L10NFormat("craftMagicItems-crafter-prerequisite-required", selectedRecipe.CrafterPrerequisites + prerequisites += "; " + LocalizationHelper.FormatLocalizedString("craftMagicItems-crafter-prerequisite-required", selectedRecipe.CrafterPrerequisites .Select(prerequisite => new L10NString($"craftMagicItems-crafter-prerequisite-{prerequisite}").ToString()) .BuildCommaList(false)); } @@ -1397,7 +1249,7 @@ private static void RenderRecipeBasedCrafting(UnitEntityData caster, RecipeBased DoesItemMatchAllEnchantments(blueprint, null, selectedEnchantment.AssetGuid, upgradeItemDoubleWeapon?.Blueprint as BlueprintItemEquipment, false) ); } else if (upgradeItemShield != null) { - if (selectedShieldWeapon) { + if (Selections.SelectedShieldWeapon) { matchingItem = allItemBlueprintsWithEnchantment?.FirstOrDefault(blueprint => DoesItemMatchAllEnchantments(blueprint, null, selectedEnchantment.AssetGuid, upgradeItemShield?.Blueprint as BlueprintItemEquipment, false) ); @@ -1431,8 +1283,8 @@ private static void RenderRecipeBasedCrafting(UnitEntityData caster, RecipeBased } else if (upgradeItem != null) { // Upgrading to a custom blueprint var name = upgradeItemShield?.Blueprint?.Name ?? upgradeItem.Blueprint.Name; - RenderCustomNameField(name); - name = selectedCustomName == name ? null : selectedCustomName; + Selections.SelectedCustomName = UmmUiRenderer.RenderCustomNameField(name, Selections.SelectedCustomName); + name = Selections.SelectedCustomName == name ? null : Selections.SelectedCustomName; IEnumerable enchantments; string supersededEnchantmentId; if (selectedRecipe.EnchantmentsCumulative) { @@ -1446,7 +1298,7 @@ private static void RenderRecipeBasedCrafting(UnitEntityData caster, RecipeBased if (upgradeItemShield != null) { upgradeItem = upgradeItemShield; } - if (selectedShieldWeapon) { + if (Selections.SelectedShieldWeapon) { itemGuid = blueprintPatcher.BuildCustomRecipeItemGuid(upgradeItemShieldWeapon.Blueprint.AssetGuid, enchantments, supersededEnchantmentId == null ? null : new[] {supersededEnchantmentId}); itemGuid = blueprintPatcher.BuildCustomRecipeItemGuid(upgradeItemShield.Blueprint.AssetGuid, Enumerable.Empty(), @@ -1465,8 +1317,8 @@ private static void RenderRecipeBasedCrafting(UnitEntityData caster, RecipeBased } else { // Crafting a new custom blueprint from scratch. SelectRandomApplicableBaseGuid(craftingData, selectedSlot); - var baseBlueprint = selectedBaseBlueprint; - RenderCustomNameField($"{selectedRecipe.NameId} {new L10NString(GetSlotStringKey(selectedSlot, craftingData.SlotRestrictions))}"); + var baseBlueprint = Selections.SelectedBaseBlueprint; + Selections.SelectedCustomName = UmmUiRenderer.RenderCustomNameField($"{selectedRecipe.NameId} {new L10NString(GetSlotStringKey(selectedSlot, craftingData.SlotRestrictions))}", Selections.SelectedCustomName); var enchantmentsToRemove = GetEnchantments(baseBlueprint, selectedRecipe).Select(enchantment => enchantment.AssetGuid).ToArray(); IEnumerable enchantments; if (selectedRecipe.EnchantmentsCumulative) { @@ -1474,19 +1326,19 @@ private static void RenderRecipeBasedCrafting(UnitEntityData caster, RecipeBased } else { enchantments = new List { selectedEnchantment.AssetGuid }; } - itemGuid = blueprintPatcher.BuildCustomRecipeItemGuid(selectedBaseBlueprint.AssetGuid, enchantments, enchantmentsToRemove, - selectedCustomName ?? "[custom item]", "null", "null"); + itemGuid = blueprintPatcher.BuildCustomRecipeItemGuid(Selections.SelectedBaseBlueprint.AssetGuid, enchantments, enchantmentsToRemove, + Selections.SelectedCustomName ?? "[custom item]", "null", "null"); itemToCraft = ResourcesLibrary.TryGetBlueprint(itemGuid); } if (!itemToCraft) { - RenderLabel($"Error: null custom item from looking up blueprint ID {itemGuid}"); + UmmUiRenderer.RenderLabelRow($"Error: null custom item from looking up blueprint ID {itemGuid}"); } else { if (IsItemLegalEnchantmentLevel(itemToCraft)) { RenderRecipeBasedCraftItemControl(caster, craftingData, selectedRecipe, casterLevel, itemToCraft, upgradeItem); } else { var maxEnchantmentLevel = ItemMaxEnchantmentLevel(itemToCraft); - RenderLabel($"This would result in {itemToCraft.Name} having an equivalent enhancement bonus of more than +{maxEnchantmentLevel}"); + UmmUiRenderer.RenderLabelRow($"This would result in {itemToCraft.Name} having an equivalent enhancement bonus of more than +{maxEnchantmentLevel}"); } } } @@ -1550,27 +1402,27 @@ private static string BuildItemDescription(ItemEntity item) { qualities = GetEnchantmentNames(shield.ArmorComponent); var weaponQualities = shield.WeaponComponent == null ? null : GetEnchantmentNames(shield.WeaponComponent); if (!string.IsNullOrEmpty(weaponQualities)) { - qualities = $"{qualities}{(string.IsNullOrEmpty(qualities) ? "" : ", ")}{ShieldBashLocalized}: {weaponQualities}"; + qualities = $"{qualities}{(string.IsNullOrEmpty(qualities) ? "" : ", ")}{LocalizedStringBlueprints.ShieldBashLocalized}: {weaponQualities}"; } } else { qualities = GetEnchantmentNames(item.Blueprint); } if (!string.IsNullOrEmpty(qualities)) { - description += $"{(string.IsNullOrEmpty(description) ? "" : "\n")}{QualitiesLocalized}: {qualities}"; + description += $"{(string.IsNullOrEmpty(description) ? "" : "\n")}{LocalizedStringBlueprints.QualitiesLocalized}: {qualities}"; } } return description; } private static void SelectRandomApplicableBaseGuid(ItemCraftingData craftingData, ItemsFilter.ItemType selectedSlot) { - if (selectedBaseBlueprint != null) { - var baseBlueprint = selectedBaseBlueprint; + if (Selections.SelectedBaseBlueprint != null) { + var baseBlueprint = Selections.SelectedBaseBlueprint; if (!baseBlueprint || !DoesBlueprintMatchSlot(baseBlueprint, selectedSlot)) { - selectedBaseBlueprint = null; + Selections.SelectedBaseBlueprint = null; } } - selectedBaseBlueprint = selectedBaseBlueprint ?? RandomBaseBlueprintId(craftingData, + Selections.SelectedBaseBlueprint = Selections.SelectedBaseBlueprint ?? RandomBaseBlueprintId(craftingData, blueprint => DoesBlueprintMatchSlot(blueprint, selectedSlot)); } @@ -1580,10 +1432,10 @@ private static void RenderCastSpellNTimes(UnitEntityData caster, RecipeBasedItem if (upgradeItem != null) { equipment = upgradeItem.Blueprint as BlueprintItemEquipment; if (equipment == null || equipment.Ability != null && equipment.SpendCharges && !equipment.RestoreChargesOnRest) { - RenderLabel($"{upgradeItem.Name} cannot cast a spell N times a day (this is unexpected - please let the mod author know)"); + UmmUiRenderer.RenderLabelRow($"{upgradeItem.Name} cannot cast a spell N times a day (this is unexpected - please let the mod author know)"); return; } else if (equipment.Ability != null && !equipment.Ability.IsSpell) { - RenderLabel($"{equipment.Ability.Name} is not a spell, so cannot be upgraded."); + UmmUiRenderer.RenderLabelRow($"{equipment.Ability.Name} is not a spell, so cannot be upgraded."); return; } } @@ -1594,12 +1446,12 @@ private static void RenderCastSpellNTimes(UnitEntityData caster, RecipeBasedItem // Choose a spellbook known to the caster var spellbooks = caster.Descriptor.Spellbooks.ToList(); var spellBookNames = spellbooks.Select(book => book.Blueprint.Name.ToString()).Concat(Enumerable.Repeat("From Items", 1)).ToArray(); - var selectedSpellbookIndex = RenderSelection("Source: ", spellBookNames, 10, ref selectedCustomName); + var selectedSpellbookIndex = DrawSelectionUserInterfaceElements("Source: ", spellBookNames, 10, ref Selections.SelectedCustomName); if (selectedSpellbookIndex < spellbooks.Count) { var spellbook = spellbooks[selectedSpellbookIndex]; // Choose a spell level var spellLevelNames = Enumerable.Range(0, spellbook.Blueprint.MaxSpellLevel + 1).Select(index => $"Level {index}").ToArray(); - spellLevel = RenderSelection("Spell level: ", spellLevelNames, 10, ref selectedCustomName); + spellLevel = DrawSelectionUserInterfaceElements("Spell level: ", spellLevelNames, 10, ref Selections.SelectedCustomName); var specialSpellLists = Accessors.GetSpellbookSpecialLists(spellbook); var spellOptions = spellbook.Blueprint.SpellList.GetSpells(spellLevel) .Concat(specialSpellLists.Aggregate(new List(), (allSpecial, spellList) => spellList.GetSpells(spellLevel))) @@ -1607,16 +1459,16 @@ private static void RenderCastSpellNTimes(UnitEntityData caster, RecipeBasedItem .OrderBy(spell => spell.Name) .ToArray(); if (!spellOptions.Any()) { - RenderLabel($"There are no level {spellLevel} {spellbook.Blueprint.Name} spells"); + UmmUiRenderer.RenderLabelRow($"There are no level {spellLevel} {spellbook.Blueprint.Name} spells"); return; } var spellNames = spellOptions.Select(spell => spell.Name).ToArray(); - var selectedSpellIndex = RenderSelection("Spell: ", spellNames, 4, ref selectedCustomName); + var selectedSpellIndex = DrawSelectionUserInterfaceElements("Spell: ", spellNames, 4, ref Selections.SelectedCustomName); ability = spellOptions[selectedSpellIndex]; if (ability.HasVariants && ability.Variants != null) { var selectedVariantIndex = - RenderSelection("Variant: ", ability.Variants.Select(spell => spell.Name).ToArray(), 4, ref selectedCustomName); + DrawSelectionUserInterfaceElements("Variant: ", ability.Variants.Select(spell => spell.Name).ToArray(), 4, ref Selections.SelectedCustomName); ability = ability.Variants[selectedVariantIndex]; } } else { @@ -1630,71 +1482,71 @@ private static void RenderCastSpellNTimes(UnitEntityData caster, RecipeBasedItem .OrderBy(item => item.Name) .ToArray(); if (itemBlueprints.Length == 0) { - RenderLabel("You are not wielding any items that can cast spells."); + UmmUiRenderer.RenderLabelRow("You are not wielding any items that can cast spells."); return; } var itemNames = itemBlueprints.Select(item => item.Name).ToArray(); - var itemIndex = RenderSelection("Cast from item: ", itemNames, 5, ref selectedCustomName); + var itemIndex = DrawSelectionUserInterfaceElements("Cast from item: ", itemNames, 5, ref Selections.SelectedCustomName); var selectedItemBlueprint = itemBlueprints[itemIndex]; ability = selectedItemBlueprint.Ability; spellLevel = selectedItemBlueprint.SpellLevel; - RenderLabel($"Spell: {ability.Name}"); + UmmUiRenderer.RenderLabelRow($"Spell: {ability.Name}"); } } else { ability = equipment.Ability; spellLevel = equipment.SpellLevel; GameLogContext.Count = equipment.Charges; - RenderLabel($"Current: {L10NFormat("craftMagicItems-label-cast-spell-n-times-details", ability.Name, equipment.CasterLevel)}"); + UmmUiRenderer.RenderLabelRow($"Current: {LocalizationHelper.FormatLocalizedString("craftMagicItems-label-cast-spell-n-times-details", ability.Name, equipment.CasterLevel)}"); GameLogContext.Clear(); } // Choose a caster level var minCasterLevel = Math.Max(equipment == null ? 0 : equipment.CasterLevel, Math.Max(1, 2 * spellLevel - 1)); - RenderIntSlider(ref selectedCasterLevel, "Caster level: ", minCasterLevel, 20); + Selections.SelectedCasterLevel = UmmUiRenderer.RenderIntSlider("Caster level: ", Selections.SelectedCasterLevel, minCasterLevel, 20); // Choose number of times per day var maxCastsPerDay = equipment == null ? 10 : ((equipment.Charges + 10) / 10) * 10; - RenderIntSlider(ref selectedCastsPerDay, "Casts per day: ", equipment == null ? 1 : equipment.Charges, maxCastsPerDay); - if (equipment != null && ability == equipment.Ability && selectedCasterLevel == equipment.CasterLevel && selectedCastsPerDay == equipment.Charges) { - RenderLabel($"No changes made to {equipment.Name}"); + Selections.SelectedCastsPerDay = UmmUiRenderer.RenderIntSlider("Casts per day: ", Selections.SelectedCastsPerDay, equipment == null ? 1 : equipment.Charges, maxCastsPerDay); + if (equipment != null && ability == equipment.Ability && Selections.SelectedCasterLevel == equipment.CasterLevel && Selections.SelectedCastsPerDay == equipment.Charges) { + UmmUiRenderer.RenderLabelRow($"No changes made to {equipment.Name}"); return; } // Show skill info - RenderCraftingSkillInformation(caster, StatType.SkillKnowledgeArcana, 5 + selectedCasterLevel, selectedCasterLevel, new[] {ability}); + RenderCraftingSkillInformation(caster, StatType.SkillKnowledgeArcana, 5 + Selections.SelectedCasterLevel, Selections.SelectedCasterLevel, new[] {ability}); string itemGuid; if (upgradeItem == null) { // Option to rename item - RenderCustomNameField($"{ability.Name} {new L10NString(GetSlotStringKey(selectedSlot, craftingData.SlotRestrictions))}"); + Selections.SelectedCustomName = UmmUiRenderer.RenderCustomNameField($"{ability.Name} {new L10NString(GetSlotStringKey(selectedSlot, craftingData.SlotRestrictions))}", Selections.SelectedCustomName); // Pick random base item SelectRandomApplicableBaseGuid(craftingData, selectedSlot); // Create customised item GUID - var baseBlueprint = selectedBaseBlueprint; + var baseBlueprint = Selections.SelectedBaseBlueprint; var enchantmentsToRemove = GetEnchantments(baseBlueprint).Select(enchantment => enchantment.AssetGuid).ToArray(); - itemGuid = blueprintPatcher.BuildCustomRecipeItemGuid(selectedBaseBlueprint.AssetGuid, new List(), enchantmentsToRemove, selectedCustomName, - ability.AssetGuid, "null", casterLevel: selectedCasterLevel, spellLevel: spellLevel, perDay: selectedCastsPerDay); + itemGuid = blueprintPatcher.BuildCustomRecipeItemGuid(Selections.SelectedBaseBlueprint.AssetGuid, new List(), enchantmentsToRemove, Selections.SelectedCustomName, + ability.AssetGuid, "null", casterLevel: Selections.SelectedCasterLevel, spellLevel: spellLevel, perDay: Selections.SelectedCastsPerDay); } else { // Option to rename item - RenderCustomNameField(upgradeItem.Blueprint.Name); + Selections.SelectedCustomName = UmmUiRenderer.RenderCustomNameField(upgradeItem.Blueprint.Name, Selections.SelectedCustomName); // Create customised item GUID itemGuid = blueprintPatcher.BuildCustomRecipeItemGuid(upgradeItem.Blueprint.AssetGuid, new List(), null, - selectedCustomName == upgradeItem.Blueprint.Name ? null : selectedCustomName, ability.AssetGuid, - casterLevel: selectedCasterLevel == equipment.CasterLevel ? -1 : selectedCasterLevel, + Selections.SelectedCustomName == upgradeItem.Blueprint.Name ? null : Selections.SelectedCustomName, ability.AssetGuid, + casterLevel: Selections.SelectedCasterLevel == equipment.CasterLevel ? -1 : Selections.SelectedCasterLevel, spellLevel: spellLevel == equipment.SpellLevel ? -1 : spellLevel, - perDay: selectedCastsPerDay == equipment.Charges ? -1 : selectedCastsPerDay); + perDay: Selections.SelectedCastsPerDay == equipment.Charges ? -1 : Selections.SelectedCastsPerDay); } var itemToCraft = ResourcesLibrary.TryGetBlueprint(itemGuid); // Render craft button - GameLogContext.Count = selectedCastsPerDay; - RenderLabel(L10NFormat("craftMagicItems-label-cast-spell-n-times-details", ability.Name, selectedCasterLevel)); + GameLogContext.Count = Selections.SelectedCastsPerDay; + UmmUiRenderer.RenderLabelRow(LocalizationHelper.FormatLocalizedString("craftMagicItems-label-cast-spell-n-times-details", ability.Name, Selections.SelectedCasterLevel)); GameLogContext.Clear(); var recipe = new RecipeData { PrerequisiteSpells = new[] {ability}, PrerequisitesMandatory = true }; - RenderRecipeBasedCraftItemControl(caster, craftingData, recipe, selectedCasterLevel, itemToCraft, upgradeItem); + RenderRecipeBasedCraftItemControl(caster, craftingData, recipe, Selections.SelectedCasterLevel, itemToCraft, upgradeItem); } public static int CharacterCasterLevel(UnitDescriptor character, Spellbook forSpellbook = null) { @@ -1718,31 +1570,19 @@ public static int CharacterCasterLevel(UnitDescriptor character, Spellbook forSp return casterLevel; } - private static SpellSchool CheckForOppositionSchool(UnitDescriptor crafter, BlueprintAbility[] prerequisiteSpells) { - if (prerequisiteSpells != null) { - foreach (var spell in prerequisiteSpells) { - if (crafter.Spellbooks.Any(spellbook => spellbook.Blueprint.SpellList.Contains(spell) - && spellbook.OppositionSchools.Contains(spell.School))) { - return spell.School; - } - } - } - return SpellSchool.None; - } - private static int RenderCraftingSkillInformation(UnitEntityData crafter, StatType skill, int dc, int casterLevel = 0, BlueprintAbility[] prerequisiteSpells = null, BlueprintFeature[] prerequisiteFeats = null, bool anyPrerequisite = false, CrafterPrerequisiteType[] crafterPrerequisites = null, bool render = true) { if (render) { - RenderLabel($"Base Crafting DC: {dc}"); + UmmUiRenderer.RenderLabelRow($"Base Crafting DC: {dc}"); } // ReSharper disable once UnusedVariable - var missing = CheckSpellPrerequisites(prerequisiteSpells, anyPrerequisite, crafter.Descriptor, false, out var missingSpells, + var missing = CraftingLogic.CheckSpellPrerequisites(prerequisiteSpells, anyPrerequisite, crafter.Descriptor, false, out var missingSpells, // ReSharper disable once UnusedVariable out var spellsToCast); - missing += CheckFeatPrerequisites(prerequisiteFeats, anyPrerequisite, crafter.Descriptor, out var missingFeats); - missing += GetMissingCrafterPrerequisites(crafterPrerequisites, crafter.Descriptor).Count; + missing += CraftingLogic.CheckFeatPrerequisites(prerequisiteFeats, anyPrerequisite, crafter.Descriptor, out var missingFeats); + missing += CraftingLogic.GetMissingCrafterPrerequisites(crafterPrerequisites, crafter.Descriptor).Count; var crafterCasterLevel = CharacterCasterLevel(crafter.Descriptor); var casterLevelShortfall = Math.Max(0, casterLevel - crafterCasterLevel); if (casterLevelShortfall > 0 && ModSettings.CasterLevelIsSinglePrerequisite) { @@ -1750,31 +1590,31 @@ private static int RenderCraftingSkillInformation(UnitEntityData crafter, StatTy casterLevelShortfall = 0; } if (missing > 0 && render) { - RenderLabel( - $"{crafter.CharacterName} is unable to meet {missing} of the prerequisites, raising the DC by {MissingPrerequisiteDCModifier * missing}"); + UmmUiRenderer.RenderLabelRow( + $"{crafter.CharacterName} is unable to meet {missing} of the prerequisites, raising the DC by {DifficultyClass.MissingPrerequisiteDCModifier * missing}"); } if (casterLevelShortfall > 0 && render) { - RenderLabel(L10NFormat("craftMagicItems-logMessage-low-caster-level", casterLevel, MissingPrerequisiteDCModifier * casterLevelShortfall)); + UmmUiRenderer.RenderLabelRow(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-low-caster-level", casterLevel, DifficultyClass.MissingPrerequisiteDCModifier * casterLevelShortfall)); } // Rob's ruling... if you're below the prerequisite caster level, you're considered to be missing a prerequisite for each // level you fall short. - dc += MissingPrerequisiteDCModifier * (missing + casterLevelShortfall); - var oppositionSchool = CheckForOppositionSchool(crafter.Descriptor, prerequisiteSpells); + dc += DifficultyClass.MissingPrerequisiteDCModifier * (missing + casterLevelShortfall); + var oppositionSchool = CraftingLogic.CheckForOppositionSchool(crafter.Descriptor, prerequisiteSpells); if (oppositionSchool != SpellSchool.None) { - dc += OppositionSchoolDCModifier; + dc += DifficultyClass.OppositionSchoolDCModifier; if (render) { - RenderLabel(L10NFormat("craftMagicItems-logMessage-opposition-school", LocalizedTexts.Instance.SpellSchoolNames.GetText(oppositionSchool), - OppositionSchoolDCModifier)); + UmmUiRenderer.RenderLabelRow(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-opposition-school", LocalizedTexts.Instance.SpellSchoolNames.GetText(oppositionSchool), + DifficultyClass.OppositionSchoolDCModifier)); } } var skillCheck = 10 + crafter.Stats.GetStat(skill).ModifiedValue; if (render) { - RenderLabel(L10NFormat("craftMagicItems-logMessage-made-progress-check", LocalizedTexts.Instance.Stats.GetText(skill), skillCheck, dc)); + UmmUiRenderer.RenderLabelRow(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-made-progress-check", LocalizedTexts.Instance.Stats.GetText(skill), skillCheck, dc)); } var skillMargin = skillCheck - dc; if (skillMargin < 0 && render) { - RenderLabel(ModSettings.CraftingTakesNoTime + UmmUiRenderer.RenderLabelRow(ModSettings.CraftingTakesNoTime ? $"This project would be too hard for {crafter.CharacterName} if \"Crafting Takes No Time\" cheat was disabled." : $"Warning: This project will be too hard for {crafter.CharacterName}"); } @@ -1805,7 +1645,7 @@ private static void CancelCraftingProject(CraftingProjectData project) { Game.Instance.UI.Common.UISound.Play(UISoundType.LootCollectGold); var goldRefund = project.GoldSpent >= 0 ? project.GoldSpent : project.TargetCost; Game.Instance.Player.GainMoney(goldRefund); - var craftingData = ItemCraftingData.FirstOrDefault(data => data.Name == project.ItemType); + var craftingData = LoadedData.ItemCraftingData.FirstOrDefault(data => data.Name == project.ItemType); BuildCostString(out var cost, craftingData, goldRefund, project.SpellPrerequisites, project.ResultItem.Blueprint, project.UpgradeItem?.Blueprint); var factor = GetMaterialComponentMultiplier(craftingData, project.ResultItem.Blueprint, project.UpgradeItem?.Blueprint); if (factor > 0) { @@ -1817,7 +1657,7 @@ private static void CancelCraftingProject(CraftingProjectData project) { } } - AddBattleLogMessage(L10NFormat("craftMagicItems-logMessage-crafting-cancelled", project.ResultItem.Name, cost)); + AddBattleLogMessage(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-crafting-cancelled", project.ResultItem.Name, cost)); } var timer = GetCraftingTimerComponentForCaster(project.Crafter.Descriptor); @@ -1841,7 +1681,7 @@ private static int CalculateBaseMundaneCraftingDC(RecipeBasedItemCraftingData cr return dc + shield.ArmorComponent.ArmorBonus; case BlueprintItemWeapon weapon: if (weapon.Category.HasSubCategory(WeaponSubCategory.Exotic)) { - var martialWeaponProficiencies = ResourcesLibrary.TryGetBlueprint(MartialWeaponProficiencies); + var martialWeaponProficiencies = ResourcesLibrary.TryGetBlueprint(Features.MartialWeaponProficiencies); if (martialWeaponProficiencies != null && martialWeaponProficiencies.GetComponents() .Any(addProficiencies => addProficiencies.RaceRestriction != null && addProficiencies.RaceRestriction == crafter.Progression.Race @@ -1860,7 +1700,7 @@ private static int CalculateBaseMundaneCraftingDC(RecipeBasedItemCraftingData cr return dc; } - private static int CalculateMundaneCraftingDC(RecipeBasedItemCraftingData craftingData, BlueprintItem blueprint, UnitDescriptor crafter, + public static int CalculateMundaneCraftingDC(RecipeBasedItemCraftingData craftingData, BlueprintItem blueprint, UnitDescriptor crafter, RecipeData recipe = null) { var dc = CalculateBaseMundaneCraftingDC(craftingData, blueprint, crafter); return dc + (recipe?.MundaneDC ?? blueprint.Enchantments @@ -1874,36 +1714,36 @@ private static void RenderCraftMundaneItemsSection() { var crafter = GetSelectedCrafter(false); // Choose crafting data - var itemTypes = ItemCraftingData + var itemTypes = LoadedData.ItemCraftingData .Where(data => data.NameId != null && data.FeatGuid == null - && (data.ParentNameId == null || SubCraftingData[data.ParentNameId][0] == data)) + && (data.ParentNameId == null || LoadedData.SubCraftingData[data.ParentNameId][0] == data)) .ToArray(); var itemTypeNames = itemTypes.Select(data => new L10NString(data.ParentNameId ?? data.NameId).ToString()).ToArray(); - var selectedItemTypeIndex = upgradingBlueprint == null - ? RenderSelection("Mundane Crafting: ", itemTypeNames, 6, ref selectedCustomName) - : GetSelectionIndex("Mundane Crafting: "); + var selectedItemTypeIndex = Selections.UpgradingBlueprint == null + ? DrawSelectionUserInterfaceElements("Mundane Crafting: ", itemTypeNames, 6, ref Selections.SelectedCustomName) + : Selections.GetSelectionIndex("Mundane Crafting: "); var selectedCraftingData = itemTypes[selectedItemTypeIndex]; if (selectedCraftingData.ParentNameId != null) { - itemTypeNames = SubCraftingData[selectedCraftingData.ParentNameId].Select(data => new L10NString(data.NameId).ToString()).ToArray(); + itemTypeNames = LoadedData.SubCraftingData[selectedCraftingData.ParentNameId].Select(data => new L10NString(data.NameId).ToString()).ToArray(); var label = new L10NString(selectedCraftingData.ParentNameId) + ": "; - var selectedItemSubTypeIndex = upgradingBlueprint == null - ? RenderSelection(label, itemTypeNames, 6) - : GetSelectionIndex(label); + var selectedItemSubTypeIndex = Selections.UpgradingBlueprint == null + ? DrawSelectionUserInterfaceElements(label, itemTypeNames, 6) + : Selections.GetSelectionIndex(label); - selectedCraftingData = SubCraftingData[selectedCraftingData.ParentNameId][selectedItemSubTypeIndex]; + selectedCraftingData = LoadedData.SubCraftingData[selectedCraftingData.ParentNameId][selectedItemSubTypeIndex]; } if (!(selectedCraftingData is RecipeBasedItemCraftingData craftingData)) { - RenderLabel("Unable to find mundane crafting recipe."); + UmmUiRenderer.RenderLabelRow("Unable to find mundane crafting recipe."); return; } BlueprintItem baseBlueprint; - if (upgradingBlueprint != null) { - baseBlueprint = upgradingBlueprint; - RenderLabel($"Applying upgrades to {baseBlueprint.Name}"); + if (Selections.UpgradingBlueprint != null) { + baseBlueprint = Selections.UpgradingBlueprint; + UmmUiRenderer.RenderLabelRow($"Applying upgrades to {baseBlueprint.Name}"); } else { // Choose mundane item of selected type to create var blueprints = craftingData.NewItemBaseIDs @@ -1913,14 +1753,14 @@ private static void RenderCraftMundaneItemsSection() { .ToArray(); var blueprintNames = blueprints.Select(item => item.Name).ToArray(); if (blueprintNames.Length == 0) { - RenderLabel("No known items of that type."); + UmmUiRenderer.RenderLabelRow("No known items of that type."); return; } - var selectedUpgradeItemIndex = RenderSelection("Item: ", blueprintNames, 5, ref selectedCustomName); + var selectedUpgradeItemIndex = DrawSelectionUserInterfaceElements("Item: ", blueprintNames, 5, ref Selections.SelectedCustomName); baseBlueprint = blueprints[selectedUpgradeItemIndex]; // See existing item details and enchantments. - RenderLabel(baseBlueprint.Description); + UmmUiRenderer.RenderLabelRow(baseBlueprint.Description); } // Assume only one slot type per crafting data @@ -1934,13 +1774,13 @@ private static void RenderCraftMundaneItemsSection() { .OrderBy(recipe => recipe.NameId) .ToArray(); var recipeNames = availableRecipes.Select(recipe => recipe.NameId).ToArray(); - var selectedRecipeIndex = RenderSelection("Craft: ", recipeNames, 6, ref selectedCustomName); + var selectedRecipeIndex = DrawSelectionUserInterfaceElements("Craft: ", recipeNames, 6, ref Selections.SelectedCustomName); var selectedRecipe = availableRecipes.Any() ? availableRecipes[selectedRecipeIndex] : null; var selectedEnchantment = selectedRecipe?.Enchantments.Length == 1 ? selectedRecipe.Enchantments[0] : null; if (selectedRecipe != null && selectedRecipe.Material != 0) { - RenderLabel(GetWeaponMaterialDescription(selectedRecipe.Material)); + UmmUiRenderer.RenderLabelRow(GetWeaponMaterialDescription(selectedRecipe.Material)); } else if (selectedEnchantment != null && !string.IsNullOrEmpty(selectedEnchantment.Description)) { - RenderLabel(selectedEnchantment.Description); + UmmUiRenderer.RenderLabelRow(selectedEnchantment.Description); } var dc = craftingData.MundaneEnhancementsStackable @@ -1982,7 +1822,7 @@ private static void RenderCraftMundaneItemsSection() { if (shield.WeaponComponent != null) { PhysicalDamageMaterial material = selectedRecipe.Material; if ((shield.WeaponComponent.DamageType.Physical.Form & PhysicalDamageForm.Bludgeoning) != 0 - && selectedEnchantment != null && selectedEnchantment.AssetGuid == MithralArmorEnchantmentGuid) { + && selectedEnchantment != null && selectedEnchantment.AssetGuid == ItemQualityBlueprints.MithralArmorEnchantmentGuid) { material = PhysicalDamageMaterial.Silver; } var weaponEnchantments = selectedEnchantment != null && selectedRecipe.Restrictions.Contains(ItemRestrictions.ShieldWeapon) ? @@ -2003,15 +1843,15 @@ private static void RenderCraftMundaneItemsSection() { } if (!itemToCraft) { - RenderLabel($"Error: null custom item from looking up blueprint ID {itemGuid}"); + UmmUiRenderer.RenderLabelRow($"Error: null custom item from looking up blueprint ID {itemGuid}"); } else { - if (upgradingBlueprint != null && GUILayout.Button($"Cancel {baseBlueprint.Name}", GUILayout.ExpandWidth(false))) { - upgradingBlueprint = null; + if (Selections.UpgradingBlueprint != null && GUILayout.Button($"Cancel {baseBlueprint.Name}", GUILayout.ExpandWidth(false))) { + Selections.UpgradingBlueprint = null; } if (craftingData.MundaneEnhancementsStackable) { if (upgradeName != null && GUILayout.Button($"Add {upgradeName} to {baseBlueprint.Name}", GUILayout.ExpandWidth(false))) { - upgradingBlueprint = itemToCraft; + Selections.UpgradingBlueprint = itemToCraft; } RenderRecipeBasedCraftItemControl(crafter, craftingData, null, 0, baseBlueprint); @@ -2020,7 +1860,7 @@ private static void RenderCraftMundaneItemsSection() { } } - RenderLabel($"Current Money: {Game.Instance.Player.Money}"); + UmmUiRenderer.RenderLabelRow($"Current Money: {Game.Instance.Player.Money}"); } private static string GetWeaponMaterialDescription(PhysicalDamageMaterial material) { @@ -2074,11 +1914,11 @@ private static void RenderProjectsSection() { var timer = GetCraftingTimerComponentForCaster(caster.Descriptor); if (timer == null || timer.CraftingProjects.Count == 0) { - RenderLabel($"{caster.CharacterName} is not currently working on any crafting projects."); + UmmUiRenderer.RenderLabelRow($"{caster.CharacterName} is not currently working on any crafting projects."); return; } - RenderLabel($"{caster.CharacterName} currently has {timer.CraftingProjects.Count} crafting projects in progress."); + UmmUiRenderer.RenderLabelRow($"{caster.CharacterName} currently has {timer.CraftingProjects.Count} crafting projects in progress."); var firstItem = true; foreach (var project in timer.CraftingProjects.ToArray()) { GUILayout.BeginHorizontal(); @@ -2095,135 +1935,27 @@ private static void RenderProjectsSection() { timer.CraftingProjects.Insert(0, project); } GUILayout.EndHorizontal(); - RenderLabel($" {BuildItemDescription(project.ResultItem).Replace("\n", "\n ")}"); - RenderLabel($" {project.LastMessage}"); - } - } - - private static void RenderFeatReassignmentSection() { - var caster = GetSelectedCrafter(false); - if (caster == null) { - return; - } - - var casterLevel = CharacterCasterLevel(caster.Descriptor); - var missingFeats = ItemCraftingData - .Where(data => data.FeatGuid != null && !CharacterHasFeat(caster, data.FeatGuid) && data.MinimumCasterLevel <= casterLevel) - .ToArray(); - if (missingFeats.Length == 0) { - RenderLabel($"{caster.CharacterName} does not currently qualify for any crafting feats."); - return; - } - - RenderLabel( - "Use this section to reassign previous feat choices for this character to crafting feats. Warning: This is a one-way assignment!"); - var selectedFeatToLearn = RenderSelection("Feat to learn", missingFeats.Select(data => new L10NString(data.NameId).ToString()).ToArray(), 6); - var learnFeatData = missingFeats[selectedFeatToLearn]; - var learnFeat = ResourcesLibrary.TryGetBlueprint(learnFeatData.FeatGuid); - if (learnFeat == null) { - throw new Exception($"Unable to find feat with guid {learnFeatData.FeatGuid}"); - } - - var removedFeatIndex = 0; - foreach (var feature in caster.Descriptor.Progression.Features) { - if (!feature.Blueprint.HideInUI && feature.Blueprint.HasGroup(CraftingFeatGroups) - && (feature.SourceProgression != null || feature.SourceRace != null)) { - GUILayout.BeginHorizontal(); - GUILayout.Label($"Feat: {feature.Name}", GUILayout.ExpandWidth(false)); - if (GUILayout.Button($"<- {learnFeat.Name}", GUILayout.ExpandWidth(false))) { - var currentRank = feature.Rank; - caster.Descriptor.Progression.ReplaceFeature(feature.Blueprint, learnFeat); - if (currentRank == 1) { - foreach (var addFact in feature.SelectComponents((AddFacts addFacts) => true)) { - addFact.OnFactDeactivate(); - } - - caster.Descriptor.Progression.Features.RemoveFact(feature); - } - - var addedFeature = caster.Descriptor.Progression.Features.AddFeature(learnFeat); - addedFeature.Source = feature.Source; - var mFacts = Accessors.GetFeatureCollectionFacts(caster.Descriptor.Progression.Features); - if (removedFeatIndex < mFacts.Count) { - // Move the new feat to the place in the list originally occupied by the removed one. - mFacts.Remove(addedFeature); - mFacts.Insert(removedFeatIndex, addedFeature); - } - - ActionBarManager.Instance.HandleAbilityRemoved(null); - } - - GUILayout.EndHorizontal(); - } - - removedFeatIndex++; - } - } - - private static void RenderCheatsSection() { - RenderCheckbox(ref ModSettings.CraftingCostsNoGold, "Crafting costs no gold and no material components."); - if (!ModSettings.CraftingCostsNoGold) { - var selectedCustomPriceScaleIndex = RenderSelection(CustomPriceLabel, CraftingPriceStrings, 4); - if (selectedCustomPriceScaleIndex == 2) { - GUILayout.BeginHorizontal(); - GUILayout.Label("Custom Cost Factor: ", GUILayout.ExpandWidth(false)); - ModSettings.CraftingPriceScale = GUILayout.HorizontalSlider(ModSettings.CraftingPriceScale * 100, 0, 500, GUILayout.Width(300)) / 100; - GUILayout.Label(Mathf.Round(ModSettings.CraftingPriceScale * 100).ToString(CultureInfo.InvariantCulture)); - GUILayout.EndHorizontal(); - } else { - ModSettings.CraftingPriceScale = 1 + selectedCustomPriceScaleIndex; - } - - if (selectedCustomPriceScaleIndex != 0) { - RenderLabel( - "Note: The sale price of custom crafted items will also be scaled by this factor, but vanilla items crafted by this mod" + - " will continue to use Owlcat's sale price, creating a price difference between the cost of crafting and sale price."); - } - } - - RenderCheckbox(ref ModSettings.IgnoreCraftingFeats, "Crafting does not require characters to take crafting feats."); - RenderCheckbox(ref ModSettings.CraftingTakesNoTime, "Crafting takes no time to complete."); - if (!ModSettings.CraftingTakesNoTime) { - RenderCheckbox(ref ModSettings.CustomCraftRate, "Craft at a non-standard rate."); - if (ModSettings.CustomCraftRate) { - var maxMagicRate = ((ModSettings.MagicCraftingRate + 1000) / 1000) * 1000; - RenderIntSlider(ref ModSettings.MagicCraftingRate, "Magic Item Crafting Rate", 1, maxMagicRate); - var maxMundaneRate = ((ModSettings.MundaneCraftingRate + 10) / 10) * 10; - RenderIntSlider(ref ModSettings.MundaneCraftingRate, "Mundane Item Crafting Rate", 1, maxMundaneRate); - } else { - ModSettings.MagicCraftingRate = Settings.MagicCraftingProgressPerDay; - ModSettings.MundaneCraftingRate = Settings.MundaneCraftingProgressPerDay; - } + UmmUiRenderer.RenderLabelRow($" {BuildItemDescription(project.ResultItem).Replace("\n", "\n ")}"); + UmmUiRenderer.RenderLabelRow($" {project.LastMessage}"); } - RenderCheckbox(ref ModSettings.CasterLevelIsSinglePrerequisite, - "When crafting, a Caster Level less than the prerequisite counts as a single missing prerequisite."); - RenderCheckbox(ref ModSettings.CraftAtFullSpeedWhileAdventuring, "Characters craft at full speed while adventuring (instead of 25% speed)."); - RenderCheckbox(ref ModSettings.IgnorePlusTenItemMaximum, "Ignore the rule that limits arms and armor to a maximum of +10 equivalent."); - RenderCheckbox(ref ModSettings.IgnoreFeatCasterLevelRestriction, "Ignore the crafting feat Caster Level prerequisites when learning feats."); - } - - private static void RenderLabel(string label) { - GUILayout.BeginHorizontal(); - GUILayout.Label(label); - GUILayout.EndHorizontal(); } - private static bool IsPlayerSomewhereSafe() { - if (Game.Instance.CurrentlyLoadedArea != null && SafeBlueprintAreaGuids.Contains(Game.Instance.CurrentlyLoadedArea.AssetGuid)) { + public static bool IsPlayerSomewhereSafe() { + if (Game.Instance.CurrentlyLoadedArea != null && Areas.SafeBlueprintAreaGuids.Contains(Game.Instance.CurrentlyLoadedArea.AssetGuid)) { return true; } // Otherwise, check if they're in the capital. return IsPlayerInCapital(); } - private static bool IsPlayerInCapital() { + public static bool IsPlayerInCapital() { // Detect if the player is in the capital, or in kingdom management from the throne room. return (Game.Instance.CurrentlyLoadedArea != null && Game.Instance.CurrentlyLoadedArea.IsCapital) || (Game.Instance.CurrentMode == GameModeType.Kingdom && KingdomTimelineManager.CanAdvanceTime()); } - private static UnitEntityData GetSelectedCrafter(bool render) { - currentCaster = null; + public static UnitEntityData GetSelectedCrafter(bool render) { + Selections.CurrentCaster = null; // Only allow remote companions if the player is in the capital. var remote = IsPlayerInCapital(); var characters = UIUtility.GetGroup(remote).Where(character => character != null @@ -2234,19 +1966,19 @@ private static UnitEntityData GetSelectedCrafter(bool render) { .ToArray(); if (characters.Length == 0) { if (render) { - RenderLabel("No living characters available."); + UmmUiRenderer.RenderLabelRow("No living characters available."); } return null; } const string label = "Crafter: "; - var selectedSpellcasterIndex = GetSelectionIndex(label); + var selectedSpellcasterIndex = Selections.GetSelectionIndex(label); if (render) { var partyNames = characters.Select(entity => $"{entity.CharacterName}" + $"{((GetCraftingTimerComponentForCaster(entity.Descriptor)?.CraftingProjects.Any() ?? false) ? "*" : "")}") .ToArray(); - selectedSpellcasterIndex = RenderSelection(label, partyNames, 8, ref upgradingBlueprint); + selectedSpellcasterIndex = DrawSelectionUserInterfaceElements(label, partyNames, 8, ref Selections.UpgradingBlueprint); } if (selectedSpellcasterIndex >= characters.Length) { selectedSpellcasterIndex = 0; @@ -2254,83 +1986,51 @@ private static UnitEntityData GetSelectedCrafter(bool render) { return characters[selectedSpellcasterIndex]; } - private static int RenderSelection(string label, string[] options, int xCount) { + /// Renders a selection of to Unity Mod Manager + /// Type of item being rendered + /// Label for the selection + /// Options for the selection + /// How many elements to fit in the horizontal direction + public static int DrawSelectionUserInterfaceElements(string label, string[] options, int horizontalCount) + { var dummy = ""; - return RenderSelection(label, options, xCount, ref dummy); - } - - private static int GetSelectionIndex(string label) { - return SelectedIndex.ContainsKey(label) ? SelectedIndex[label] : 0; + return DrawSelectionUserInterfaceElements(label, options, horizontalCount, ref dummy); } - private static int RenderSelection(string label, string[] options, int xCount, ref T emptyOnChange, bool addSpace = true) { - var index = GetSelectionIndex(label); - if (index >= options.Length) { + public static int DrawSelectionUserInterfaceElements(string label, string[] options, int horizontalCount, ref T emptyOnChange, bool addSpace = true) + { + var index = Selections.GetSelectionIndex(label); + if (index >= options.Length) + { index = 0; } - if (addSpace) { - GUILayout.Space(20); - } - GUILayout.BeginHorizontal(); - GUILayout.Label(label, GUILayout.ExpandWidth(false)); - var newIndex = GUILayout.SelectionGrid(index, options, xCount); - if (index != newIndex) { - emptyOnChange = default(T); - } - - GUILayout.EndHorizontal(); - SelectedIndex[label] = newIndex; - return newIndex; - } - - private static void RenderIntSlider(ref int value, string label, int min, int max) { - value = Mathf.Clamp(value, min, max); - GUILayout.BeginHorizontal(); - GUILayout.Label(label, GUILayout.ExpandWidth(false)); - value = Mathf.RoundToInt(GUILayout.HorizontalSlider(value, min, max, GUILayout.Width(300))); - GUILayout.Label($"{value}", GUILayout.ExpandWidth(false)); - GUILayout.EndHorizontal(); - } - - private static void RenderCustomNameField(string defaultValue) { - GUILayout.BeginHorizontal(); - GUILayout.Label("Name: ", GUILayout.ExpandWidth(false)); - if (string.IsNullOrEmpty(selectedCustomName)) { - selectedCustomName = defaultValue; - } + var newIndex = UmmUiRenderer.RenderSelection(label, options, index, horizontalCount, addSpace); - selectedCustomName = GUILayout.TextField(selectedCustomName, GUILayout.Width(300)); - if (selectedCustomName.Trim().Length == 0) { - selectedCustomName = null; + if (index != newIndex) + { + emptyOnChange = default(T); } - GUILayout.EndHorizontal(); - } - - private static void RenderCheckbox(ref bool value, string label) { - GUILayout.BeginHorizontal(); - if (GUILayout.Button($"{(value ? "" : "")} {label}", GUILayout.ExpandWidth(false))) { - value = !value; - } + Selections.SetSelectionIndex(label, newIndex); - GUILayout.EndHorizontal(); + return newIndex; } public static void AddItemBlueprintForSpell(UsableItemType itemType, BlueprintItemEquipment itemBlueprint) { - if (!SpellIdToItem.ContainsKey(itemType)) { - SpellIdToItem.Add(itemType, new Dictionary>()); + if (!LoadedData.SpellIdToItem.ContainsKey(itemType)) { + LoadedData.SpellIdToItem.Add(itemType, new Dictionary>()); } - if (!SpellIdToItem[itemType].ContainsKey(itemBlueprint.Ability.AssetGuid)) { - SpellIdToItem[itemType][itemBlueprint.Ability.AssetGuid] = new List(); + if (!LoadedData.SpellIdToItem[itemType].ContainsKey(itemBlueprint.Ability.AssetGuid)) { + LoadedData.SpellIdToItem[itemType][itemBlueprint.Ability.AssetGuid] = new List(); } - SpellIdToItem[itemType][itemBlueprint.Ability.AssetGuid].Add(itemBlueprint); + LoadedData.SpellIdToItem[itemType][itemBlueprint.Ability.AssetGuid].Add(itemBlueprint); } public static List FindItemBlueprintsForSpell(BlueprintScriptableObject spell, UsableItemType itemType) { - if (!SpellIdToItem.ContainsKey(itemType)) { + if (!LoadedData.SpellIdToItem.ContainsKey(itemType)) { #if PATCH21_BETA var allUsableItems = ResourcesLibrary.GetBlueprints(); #else @@ -2343,52 +2043,14 @@ public static List FindItemBlueprintsForSpell(BlueprintS } } - return SpellIdToItem[itemType].ContainsKey(spell.AssetGuid) ? SpellIdToItem[itemType][spell.AssetGuid] : null; - } - - private static void AddItemForType(BlueprintItem blueprint) { - string assetGuid = GetBlueprintItemType(blueprint); - if (!string.IsNullOrEmpty(assetGuid)) { - TypeToItem.Add(assetGuid, blueprint); - } - } - - private static void AddItemIdForEnchantment(BlueprintItemEquipment itemBlueprint) { - if (itemBlueprint != null) { - foreach (var enchantment in GetEnchantments(itemBlueprint)) { - if (!EnchantmentIdToItem.ContainsKey(enchantment.AssetGuid)) { - EnchantmentIdToItem[enchantment.AssetGuid] = new List(); - } - - EnchantmentIdToItem[enchantment.AssetGuid].Add(itemBlueprint); - } - } - } - - private static void AddRecipeForEnchantment(string enchantmentId, RecipeData recipe) { - if (!EnchantmentIdToRecipe.ContainsKey(enchantmentId)) { - EnchantmentIdToRecipe.Add(enchantmentId, new List()); - } - - if (!EnchantmentIdToRecipe[enchantmentId].Contains(recipe)) { - EnchantmentIdToRecipe[enchantmentId].Add(recipe); - } - } - - private static void AddRecipeForMaterial(PhysicalDamageMaterial material, RecipeData recipe) { - if (!MaterialToRecipe.ContainsKey(material)) { - MaterialToRecipe.Add(material, new List()); - } - if (!MaterialToRecipe[material].Contains(recipe)) { - MaterialToRecipe[material].Add(recipe); - } + return LoadedData.SpellIdToItem[itemType].ContainsKey(spell.AssetGuid) ? LoadedData.SpellIdToItem[itemType][spell.AssetGuid] : null; } private static IEnumerable FindItemBlueprintForEnchantmentId(string assetGuid) { - return EnchantmentIdToItem.ContainsKey(assetGuid) ? EnchantmentIdToItem[assetGuid] : null; + return LoadedData.EnchantmentIdToItem.ContainsKey(assetGuid) ? LoadedData.EnchantmentIdToItem[assetGuid] : null; } - private static bool CharacterHasFeat(UnitEntityData caster, string featGuid) { + public static bool CharacterHasFeat(UnitEntityData caster, string featGuid) { return caster.Descriptor.Progression.Features.Enumerable.Any(feat => feat.Blueprint.AssetGuid == featGuid); } @@ -2397,58 +2059,6 @@ private static BlueprintItemEquipment RandomBaseBlueprintId(ItemCraftingData ite return blueprintIds[RandomGenerator.Next(blueprintIds.Length)]; } - private static void CraftItem(ItemEntity resultItem, ItemEntity upgradeItem = null) { - var characters = UIUtility.GetGroup(true).Where(character => character.IsPlayerFaction && !character.Descriptor.IsPet); - foreach (var character in characters) { - var bondedComponent = GetBondedItemComponentForCaster(character.Descriptor); - if (bondedComponent && bondedComponent.ownerItem == upgradeItem) { - bondedComponent.ownerItem = resultItem; - } - } - - using (new DisableBattleLog(!ModSettings.CraftingTakesNoTime)) { - var holdingSlot = upgradeItem?.HoldingSlot; - var slotIndex = upgradeItem?.InventorySlotIndex; - var inventory = true; - if (upgradeItem != null) { - if (Game.Instance.Player.Inventory.Contains(upgradeItem)) { - Game.Instance.Player.Inventory.Remove(upgradeItem); - } else { - Game.Instance.Player.SharedStash.Remove(upgradeItem); - inventory = false; - } - } - if (holdingSlot == null) { - if (inventory) { - Game.Instance.Player.Inventory.Add(resultItem); - } else { - Game.Instance.Player.SharedStash.Add(resultItem); - } - if (slotIndex is int value) { - resultItem.SetSlotIndex(value); - } - } else { - holdingSlot.InsertItem(resultItem); - } - } - - if (resultItem is ItemEntityUsable usable) { - switch (usable.Blueprint.Type) { - case UsableItemType.Scroll: - Game.Instance.UI.Common.UISound.Play(UISoundType.NewInformation); - break; - case UsableItemType.Potion: - Game.Instance.UI.Common.UISound.PlayItemSound(SlotAction.Take, resultItem, false); - break; - default: - Game.Instance.UI.Common.UISound.Play(UISoundType.SettlementBuildStart); - break; - } - } else { - Game.Instance.UI.Common.UISound.Play(UISoundType.SettlementBuildStart); - } - } - private static int CalculateSpellBasedGoldCost(SpellBasedItemCraftingData craftingData, int spellLevel, int casterLevel) { return spellLevel == 0 ? craftingData.BaseItemGoldCost * casterLevel / 8 : craftingData.BaseItemGoldCost * spellLevel * casterLevel / 4; } @@ -2462,7 +2072,7 @@ private static bool BuildCostString(out string cost, ItemCraftingData craftingDa } else { canAfford = (Game.Instance.Player.Money >= goldCost); var notAffordGold = canAfford ? "" : new L10NString("craftMagicItems-label-cost-gold-too-much"); - cost = L10NFormat("craftMagicItems-label-cost-gold", goldCost, notAffordGold); + cost = LocalizationHelper.FormatLocalizedString("craftMagicItems-label-cost-gold", goldCost, notAffordGold); var itemTotals = new Dictionary(); if (spellBlueprintArray != null) { foreach (var spellBlueprint in spellBlueprintArray) { @@ -2487,7 +2097,7 @@ private static bool BuildCostString(out string cost, ItemCraftingData craftingDa notAffordItems = new L10NString("craftMagicItems-label-cost-items-too-much"); } - cost += L10NFormat("craftMagicItems-label-cost-gold-and-items", pair.Value, pair.Key.Name, notAffordItems); + cost += LocalizationHelper.FormatLocalizedString("craftMagicItems-label-cost-gold-and-items", pair.Value, pair.Key.Name, notAffordItems); } } @@ -2505,7 +2115,7 @@ private static void AddNewProject(UnitDescriptor casterDescriptor, CraftingProje } private static void CalculateProjectEstimate(CraftingProjectData project) { - var craftingData = ItemCraftingData.FirstOrDefault(data => data.Name == project.ItemType); + var craftingData = LoadedData.ItemCraftingData.FirstOrDefault(data => data.Name == project.ItemType); StatType craftingSkill; int dc; int progressRate; @@ -2513,7 +2123,7 @@ private static void CalculateProjectEstimate(CraftingProjectData project) { craftingSkill = StatType.SkillKnowledgeArcana; dc = 10 + project.Crafter.Stats.GetStat(craftingSkill).ModifiedValue; progressRate = ModSettings.MagicCraftingRate; - } else if (IsMundaneCraftingData(craftingData)) { + } else if (CraftingLogic.IsMundaneCraftingData(craftingData)) { craftingSkill = StatType.SkillKnowledgeWorld; var recipeBasedItemCraftingData = (RecipeBasedItemCraftingData) craftingData; dc = CalculateMundaneCraftingDC(recipeBasedItemCraftingData, project.ResultItem.Blueprint, project.Crafter.Descriptor); @@ -2531,22 +2141,25 @@ private static void CalculateProjectEstimate(CraftingProjectData project) { if (ModSettings.CraftAtFullSpeedWhileAdventuring) { project.AddMessage(new L10NString("craftMagicItems-time-estimate-single-rate")); } else { - var progressPerDayAdventuring = (int) (progressRate * (1 + (float) skillMargin / 5) / AdventuringProgressPenalty); + var progressPerDayAdventuring = (int) (progressRate * (1 + (float) skillMargin / 5) / DifficultyClass.AdventuringProgressPenalty); var adventuringDayCount = (project.TargetCost + progressPerDayAdventuring - 1) / progressPerDayAdventuring; project.AddMessage(adventuringDayCount == 1 ? new L10NString("craftMagicItems-time-estimate-one-day") - : L10NFormat("craftMagicItems-time-estimate-adventuring-capital", adventuringDayCount)); + : LocalizationHelper.FormatLocalizedString("craftMagicItems-time-estimate-adventuring-capital", adventuringDayCount)); } GameLogContext.Clear(); AddBattleLogMessage(project.LastMessage); } - private static void RenderSpellBasedCraftItemControl(UnitEntityData caster, SpellBasedItemCraftingData craftingData, AbilityData spell, - BlueprintAbility spellBlueprint, int spellLevel, int casterLevel) { + private static void AttemptSpellBasedCraftItemAndRender(UnitEntityData caster, SpellBasedItemCraftingData craftingData, + AbilityData spell, BlueprintAbility spellBlueprint, int spellLevel, int casterLevel) + { var itemBlueprintList = FindItemBlueprintsForSpell(spellBlueprint, craftingData.UsableItemType); - if (itemBlueprintList == null && craftingData.NewItemBaseIDs == null) { - GUILayout.Label(L10NFormat("craftMagicItems-label-no-item-exists", new L10NString(craftingData.NamePrefixId), spellBlueprint.Name)); + if (itemBlueprintList == null && craftingData.NewItemBaseIDs == null) + { + var message = LocalizationHelper.FormatLocalizedString("craftMagicItems-label-no-item-exists", new L10NString(craftingData.NamePrefixId), spellBlueprint.Name); + UmmUiRenderer.RenderLabel(message); return; } @@ -2557,61 +2170,84 @@ private static void RenderSpellBasedCraftItemControl(UnitEntityData caster, Spel var custom = existingItemBlueprint == null || existingItemBlueprint.AssetGuid.Contains(CraftMagicItemsBlueprintPatcher.BlueprintPrefix) ? new L10NString("craftMagicItems-label-custom").ToString() : ""; - var label = L10NFormat("craftMagicItems-label-craft-spell-item", custom, new L10NString(craftingData.NamePrefixId), spellBlueprint.Name, cost); - if (!canAfford) { - GUILayout.Label(label); - } else if (GUILayout.Button(label, GUILayout.ExpandWidth(false))) { - BlueprintItem itemBlueprint; - if (itemBlueprintList == null) { - // No items for that spell exist at all - create a custom blueprint with casterLevel, spellLevel and spellId - var blueprintId = - blueprintPatcher.BuildCustomSpellItemGuid(RandomBaseBlueprintId(craftingData).AssetGuid, casterLevel, spellLevel, spellBlueprint.AssetGuid); - itemBlueprint = ResourcesLibrary.TryGetBlueprint(blueprintId); - } else if (existingItemBlueprint == null) { - // No item for this spell & caster level - create a custom blueprint with casterLevel and optionally SpellLevel - var blueprintId = blueprintPatcher.BuildCustomSpellItemGuid(itemBlueprintList[0].AssetGuid, casterLevel, - itemBlueprintList[0].SpellLevel == spellLevel ? -1 : spellLevel); - itemBlueprint = ResourcesLibrary.TryGetBlueprint(blueprintId); - } else { - // Item with matching spell, level and caster level exists. Use that. - itemBlueprint = existingItemBlueprint; - } - - if (itemBlueprint == null) { - throw new Exception( - $"Unable to build blueprint for spellId {spellBlueprint.AssetGuid}, spell level {spellLevel}, caster level {casterLevel}"); - } + var label = LocalizationHelper.FormatLocalizedString("craftMagicItems-label-craft-spell-item", custom, new L10NString(craftingData.NamePrefixId), spellBlueprint.Name, cost); - // Pay gold and material components up front. - if (ModSettings.CraftingCostsNoGold) { - goldCost = 0; - } else { - Game.Instance.UI.Common.UISound.Play(UISoundType.LootCollectGold); - Game.Instance.Player.SpendMoney(goldCost); - if (spellBlueprint.MaterialComponent.Item != null) { - Game.Instance.Player.Inventory.Remove(spellBlueprint.MaterialComponent.Item, - spellBlueprint.MaterialComponent.Count * craftingData.Charges); - } - } + //if the player cannot afford the time (not enough gold), alert them + if (!canAfford) + { + UmmUiRenderer.RenderLabel(label); + } + // ... otherwise let them spend their money + else if (GUILayout.Button(label, GUILayout.ExpandWidth(false))) + { + BeginCraftingSpellBasedItem(caster, craftingData, spell, spellBlueprint, spellLevel, casterLevel, itemBlueprintList, existingItemBlueprint, requiredProgress, goldCost, cost); + } + } - var resultItem = BuildItemEntity(itemBlueprint, craftingData, caster); - AddBattleLogMessage(L10NFormat("craftMagicItems-logMessage-begin-crafting", cost, itemBlueprint.Name), resultItem); - if (ModSettings.CraftingTakesNoTime) { - AddBattleLogMessage(L10NFormat("craftMagicItems-logMessage-expend-spell", spell.Name)); - spell.SpendFromSpellbook(); - CraftItem(resultItem); - } else { - var project = new CraftingProjectData(caster, requiredProgress, goldCost, casterLevel, resultItem, craftingData.Name, null, - new[] {spellBlueprint}); - AddNewProject(caster.Descriptor, project); - CalculateProjectEstimate(project); - currentSection = OpenSection.ProjectsSection; + private static void BeginCraftingSpellBasedItem(UnitEntityData caster, SpellBasedItemCraftingData craftingData, + AbilityData spell, BlueprintAbility spellBlueprint, Int32 spellLevel, Int32 casterLevel, + List itemBlueprintList, BlueprintItemEquipment existingItemBlueprint, + Int32 requiredProgress, Int32 goldCost, String cost) + { + BlueprintItem itemBlueprint; + if (itemBlueprintList == null) + { + // No items for that spell exist at all - create a custom blueprint with casterLevel, spellLevel and spellId + var blueprintId = blueprintPatcher.BuildCustomSpellItemGuid(RandomBaseBlueprintId(craftingData).AssetGuid, + casterLevel, spellLevel, spellBlueprint.AssetGuid); + itemBlueprint = ResourcesLibrary.TryGetBlueprint(blueprintId); + } + else if (existingItemBlueprint == null) + { + // No item for this spell & caster level - create a custom blueprint with casterLevel and optionally SpellLevel + var blueprintId = blueprintPatcher.BuildCustomSpellItemGuid(itemBlueprintList[0].AssetGuid, casterLevel, + itemBlueprintList[0].SpellLevel == spellLevel ? -1 : spellLevel); + itemBlueprint = ResourcesLibrary.TryGetBlueprint(blueprintId); + } + else + { + // Item with matching spell, level and caster level exists. Use that. + itemBlueprint = existingItemBlueprint; + } + + if (itemBlueprint == null) + { + throw new Exception( + $"Unable to build blueprint for spellId {spellBlueprint.AssetGuid}, spell level {spellLevel}, caster level {casterLevel}"); + } + + // Pay gold and material components up front. + if (ModSettings.CraftingCostsNoGold) + { + goldCost = 0; + } + else + { + Game.Instance.UI.Common.UISound.Play(UISoundType.LootCollectGold); + Game.Instance.Player.SpendMoney(goldCost); + if (spellBlueprint.MaterialComponent.Item != null) + { + Game.Instance.Player.Inventory.Remove(spellBlueprint.MaterialComponent.Item, + spellBlueprint.MaterialComponent.Count * craftingData.Charges); } } - } - private static bool IsMundaneCraftingData(ItemCraftingData craftingData) { - return craftingData.FeatGuid == null; + var resultItem = BuildItemEntity(itemBlueprint, craftingData, caster); + AddBattleLogMessage(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-begin-crafting", cost, itemBlueprint.Name), resultItem); + if (ModSettings.CraftingTakesNoTime) + { + AddBattleLogMessage(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-expend-spell", spell.Name)); + spell.SpendFromSpellbook(); + CraftingLogic.CraftItem(resultItem); + } + else + { + var project = new CraftingProjectData(caster, requiredProgress, goldCost, casterLevel, resultItem, craftingData.Name, null, + new[] { spellBlueprint }); + AddNewProject(caster.Descriptor, project); + CalculateProjectEstimate(project); + Selections.CurrentSection = OpenSection.ProjectsSection; + } } private static void RenderRecipeBasedCraftItemControl(UnitEntityData caster, ItemCraftingData craftingData, RecipeData recipe, int casterLevel, @@ -2621,7 +2257,7 @@ private static void RenderRecipeBasedCraftItemControl(UnitEntityData caster, Ite int materialComponentCost = recipe?.MaterialComponentCost ?? 0; var requiredProgress = (baseCost - upgradeCost - materialComponentCost) / 4; var goldCost = materialComponentCost + (int)Mathf.Round(requiredProgress * ModSettings.CraftingPriceScale); - if (IsMundaneCraftingData(craftingData)) { + if (CraftingLogic.IsMundaneCraftingData(craftingData)) { // For mundane crafting, the gold cost is less, and the cost of the recipes don't increase the required progress. goldCost = Math.Max(1, (goldCost * 2 + 2) / 3); var recipeCost = 0; @@ -2644,7 +2280,7 @@ private static void RenderRecipeBasedCraftItemControl(UnitEntityData caster, Ite var blueprintCost = standardBlueprint.Cost; var blueprintWeight = standardBlueprint.Weight; foreach (var enchantment in itemBlueprint.Enchantments) { - if (enchantment.AssetGuid.StartsWith(OversizedGuid)) { + if (enchantment.AssetGuid.StartsWith(ItemQualityBlueprints.OversizedGuid)) { var weaponBaseSizeChange = enchantment.GetComponent(); if (weaponBaseSizeChange != null) { var sizeCategoryChange = weaponBaseSizeChange.SizeCategoryChange; @@ -2681,10 +2317,10 @@ private static void RenderRecipeBasedCraftItemControl(UnitEntityData caster, Ite ? new L10NString("craftMagicItems-label-custom").ToString() : ""; var label = upgradeItem == null - ? L10NFormat("craftMagicItems-label-craft-item", custom, itemBlueprint.Name, cost) + ? LocalizationHelper.FormatLocalizedString("craftMagicItems-label-craft-item", custom, itemBlueprint.Name, cost) : itemBlueprint is BlueprintItemWeapon otherWeapon && otherWeapon.Double - ? L10NFormat("craftMagicItems-label-upgrade-weapon-double", upgradeItem.Blueprint.Name, custom, itemBlueprint.Name, otherWeapon.SecondWeapon.Name, cost) - : L10NFormat("craftMagicItems-label-upgrade-item", upgradeItem.Blueprint.Name, custom, itemBlueprint.Name, cost); + ? LocalizationHelper.FormatLocalizedString("craftMagicItems-label-upgrade-weapon-double", upgradeItem.Blueprint.Name, custom, itemBlueprint.Name, otherWeapon.SecondWeapon.Name, cost) + : LocalizationHelper.FormatLocalizedString("craftMagicItems-label-upgrade-item", upgradeItem.Blueprint.Name, custom, itemBlueprint.Name, cost); if (!canAfford) { GUILayout.Label(label); } else if (GUILayout.Button(label, GUILayout.ExpandWidth(false))) { @@ -2705,96 +2341,23 @@ private static void RenderRecipeBasedCraftItemControl(UnitEntityData caster, Ite } var resultItem = BuildItemEntity(itemBlueprint, craftingData, caster); - AddBattleLogMessage(L10NFormat("craftMagicItems-logMessage-begin-crafting", cost, itemBlueprint.Name), resultItem); + AddBattleLogMessage(LocalizationHelper.FormatLocalizedString("craftMagicItems-logMessage-begin-crafting", cost, itemBlueprint.Name), resultItem); if (ModSettings.CraftingTakesNoTime) { - CraftItem(resultItem, upgradeItem); + CraftingLogic.CraftItem(resultItem, upgradeItem); } else { var project = new CraftingProjectData(caster, requiredProgress, goldCost, casterLevel, resultItem, craftingData.Name, recipe?.Name, recipe?.PrerequisiteSpells ?? new BlueprintAbility[0], recipe?.PrerequisiteFeats ?? Array.Empty(), recipe?.PrerequisitesMandatory ?? false, recipe?.AnyPrerequisite ?? false, upgradeItem, recipe?.CrafterPrerequisites ?? new CrafterPrerequisiteType[0]); AddNewProject(caster.Descriptor, project); CalculateProjectEstimate(project); - currentSection = OpenSection.ProjectsSection; + Selections.CurrentSection = OpenSection.ProjectsSection; } // Reset base blueprint for next item - selectedBaseBlueprint = null; + Selections.SelectedBaseBlueprint = null; // And stop upgrading the item, if relevant. - upgradingBlueprint = null; - } - } - - public static LocalizedString BuildCustomRecipeItemDescription(BlueprintItem blueprint, IList enchantments, - IList skipped, IList removed, bool replaceAbility, string ability, int casterLevel, int perDay) { - var extraDescription = enchantments - .Select(enchantment => { - var recipe = FindSourceRecipe(enchantment.AssetGuid, blueprint); - if (recipe == null) { - if (skipped.Contains(enchantment)) { - return ""; - } else if (!string.IsNullOrEmpty(enchantment.Name)) { - return enchantment.Name; - } else { - return "Unknown"; - } - } else if (recipe.Enchantments.Length <= 1) { - if (skipped.Contains(enchantment)) { - return ""; - } else { - if (!string.IsNullOrEmpty(enchantment.Name)) { - return enchantment.Name; - } else { - return recipe.NameId; - } - } - } - var newBonus = recipe.Enchantments.FindIndex(e => e == enchantment) + 1; - var bonusString = GetBonusString(newBonus, recipe); - var bonusDescription = recipe.BonusTypeId != null - ? L10NFormat("craftMagicItems-custom-description-bonus-to", new L10NString(recipe.BonusTypeId), recipe.NameId) - : recipe.BonusToId != null - ? L10NFormat("craftMagicItems-custom-description-bonus-to", recipe.NameId, new L10NString(recipe.BonusToId)) - : L10NFormat("craftMagicItems-custom-description-bonus", recipe.NameId); - var upgradeFrom = removed.FirstOrDefault(remove => FindSourceRecipe(remove.AssetGuid, blueprint) == recipe); - var oldBonus = int.MaxValue; - if (upgradeFrom != null) { - oldBonus = recipe.Enchantments.FindIndex(e => e == upgradeFrom) + 1; - } - if (oldBonus > newBonus) { - if (skipped.Contains(enchantment)) { - return new L10NString(""); - } else { - return L10NFormat("craftMagicItems-custom-description-enchantment-template", bonusString, bonusDescription); - } - } else { - removed.Remove(upgradeFrom); - } - return L10NFormat("craftMagicItems-custom-description-enchantment-upgrade-template", bonusDescription, - GetBonusString(oldBonus, recipe), bonusString); - }) - .OrderBy(enchantmentDescription => enchantmentDescription) - .Select(enchantmentDescription => string.IsNullOrEmpty(enchantmentDescription) ? "" : "\n* " + enchantmentDescription) - .Join(""); - if (blueprint is BlueprintItemEquipment equipment && (ability != null && ability != "null" || casterLevel > -1 || perDay > -1)) { - GameLogContext.Count = equipment.Charges; - extraDescription += "\n* " + (equipment.Charges == 1 ? L10NFormat("craftMagicItems-label-cast-spell-n-times-details-single", equipment.Ability.Name, equipment.CasterLevel) : - L10NFormat("craftMagicItems-label-cast-spell-n-times-details-multiple", equipment.Ability.Name, equipment.CasterLevel, equipment.Charges)); - GameLogContext.Clear(); - } - - string description; - if (removed.Count == 0 && !replaceAbility) { - description = blueprint.Description; - if (extraDescription.Length > 0) { - description += new L10NString("craftMagicItems-custom-description-additional") + extraDescription; - } - } else if (extraDescription.Length > 0) { - description = new L10NString("craftMagicItems-custom-description-start") + extraDescription; - } else { - description = ""; + Selections.UpgradingBlueprint = null; } - - return new FakeL10NString(description); } private static int ItemPlus(BlueprintItem blueprint) { @@ -2823,6 +2386,7 @@ private static int ItemPlus(BlueprintItem blueprint) { return 0; } + private static int ItemMaxEnchantmentLevel(BlueprintItem blueprint) { if (blueprint is BlueprintItemWeapon || blueprint is BlueprintItemArmor || blueprint is BlueprintItemShield) { return 10; @@ -2839,7 +2403,7 @@ public static int ItemPlusEquivalent(BlueprintItem blueprint) { var enhancementLevel = 0; var cumulative = new Dictionary(); foreach (var enchantment in blueprint.Enchantments) { - if (EnchantmentIdToRecipe.ContainsKey(enchantment.AssetGuid)) { + if (LoadedData.EnchantmentIdToRecipe.ContainsKey(enchantment.AssetGuid)) { var recipe = FindSourceRecipe(enchantment.AssetGuid, blueprint); if (recipe != null && recipe.CostType == RecipeCostType.EnhancementLevelSquared) { var level = recipe.Enchantments.FindIndex(e => e == enchantment) + 1; @@ -2871,9 +2435,11 @@ private static bool IsItemLegalEnchantmentLevel(BlueprintItem blueprint) { return ItemPlusEquivalent(blueprint) <= ItemMaxEnchantmentLevel(blueprint); } - private static int GetEnchantmentCost(string enchantmentId, BlueprintItem blueprint) { + public static int GetEnchantmentCost(string enchantmentId, BlueprintItem blueprint) + { var recipe = FindSourceRecipe(enchantmentId, blueprint); - if (recipe != null) { + if (recipe != null) + { var index = recipe.Enchantments.FindIndex(enchantment => enchantment.AssetGuid == enchantmentId); var casterLevel = recipe.CasterLevelStart + index * recipe.CasterLevelMultiplier; var epicFactor = casterLevel > 20 ? 2 : 1; @@ -2892,23 +2458,33 @@ private static int GetEnchantmentCost(string enchantmentId, BlueprintItem bluepr } } - return EnchantmentIdToCost.ContainsKey(enchantmentId) ? EnchantmentIdToCost[enchantmentId] : 0; + return LoadedData.EnchantmentIdToCost.ContainsKey(enchantmentId) ? LoadedData.EnchantmentIdToCost[enchantmentId] : 0; } private static int GetSpecialMaterialCost(PhysicalDamageMaterial material, BlueprintItemWeapon weapon, int baseCost, float weight) { switch (material) { case PhysicalDamageMaterial.Adamantite: - return 3000 - WeaponMasterworkCost; // Cost of masterwork is subsumed by the cost of adamantite + return DefaultCosts.Adamantine - DefaultCosts.WeaponMasterworkCost; // Cost of masterwork is subsumed by the cost of adamantite case PhysicalDamageMaterial.ColdIron: var enhancementLevel = ItemPlusEquivalent(weapon); // Cold Iron weapons cost double, excluding the masterwork component and 2000 extra for enchanting the first +1 // double weapon - return baseCost + (enhancementLevel > 0 ? WeaponPlusCost : 0); + return baseCost + (enhancementLevel > 0 ? DefaultCosts.WeaponPlusCost : 0); case PhysicalDamageMaterial.Silver: - // PhysicalDamageMaterial.Silver is really Mithral. Non-armor Mithral items cost 500 gp per pound of the original, non-Mithral item, which + // PhysicalDamageMaterial.Silver is really Mithral. + // Non-armor Mithral items cost 500 gp per pound of the original, non-Mithral item, which // translates to 1000 gp per pound of Mithral. See https://paizo.com/paizo/faq/v5748nruor1fm#v5748eaic9r9u // Only charge for weight on the primary half - return (int)(500 * weight) - WeaponMasterworkCost; // Cost of masterwork is subsumed by the cost of mithral + + // TODO: this is wrong and only applies to weapons and wondrous items. + // see: https://www.d20pfsrd.com/equipment/special-materials/#Mithral + // Type of Item Item Cost Modifier + // Light armor +1,000 gp + // Medium armor +4,000 gp + // Heavy armor +9,000 gp + // Shield +1,000 gp + // Other items +500 gp/lb. + return (int)(DefaultCosts.MithralPerPound * weight) - DefaultCosts.WeaponMasterworkCost; // Cost of masterwork is subsumed by the cost of mithral default: return 0; } @@ -2943,9 +2519,9 @@ public static int RulesRecipeItemCost(BlueprintItem blueprint, int baseCost = -1 var mithralArmorEnchantmentGuid = false; var cost = 0; foreach (var enchantment in blueprint.Enchantments) { - if (enchantment.AssetGuid == MithralArmorEnchantmentGuid) { + if (enchantment.AssetGuid == ItemQualityBlueprints.MithralArmorEnchantmentGuid) { mithralArmorEnchantmentGuid = true; - } else if (enchantment.AssetGuid.StartsWith(OversizedGuid)) { + } else if (enchantment.AssetGuid.StartsWith(ItemQualityBlueprints.OversizedGuid)) { var weaponBaseSizeChange = enchantment.GetComponent(); if (weaponBaseSizeChange != null) { var sizeCategoryChange = weaponBaseSizeChange.SizeCategoryChange; @@ -2979,18 +2555,18 @@ public static int RulesRecipeItemCost(BlueprintItem blueprint, int baseCost = -1 var enhancementLevel = ItemPlusEquivalent(blueprint); if (blueprint is BlueprintItemWeapon weapon) { if (enhancementLevel > 0) { - cost += WeaponMasterworkCost; + cost += DefaultCosts.WeaponMasterworkCost; } if (weapon.DamageType.Physical.Material != 0) { cost += GetSpecialMaterialCost(weapon.DamageType.Physical.Material, weapon, baseCost, weight); } } else if (blueprint is BlueprintItemArmor) { if (enhancementLevel > 0 && !mithralArmorEnchantmentGuid) { - cost += ArmorMasterworkCost; + cost += DefaultCosts.ArmorMasterworkCost; } } - var factor = blueprint is BlueprintItemWeapon ? WeaponPlusCost : ArmorPlusCost; + var factor = blueprint is BlueprintItemWeapon ? DefaultCosts.WeaponPlusCost : DefaultCosts.ArmorPlusCost; cost += enhancementLevel * enhancementLevel * factor; if (blueprint is BlueprintItemWeapon doubleWeapon && doubleWeapon.Double) { return baseCost + cost + RulesRecipeItemCost(doubleWeapon.SecondWeapon, 0, 0.0f); @@ -3000,7 +2576,7 @@ public static int RulesRecipeItemCost(BlueprintItem blueprint, int baseCost = -1 if (ItemPlusEquivalent(blueprint) > 0) { var enhancementLevel = ItemPlusEquivalent(blueprint); - var enhancementCost = enhancementLevel * enhancementLevel * UnarmedPlusCost; + var enhancementCost = enhancementLevel * enhancementLevel * DefaultCosts.UnarmedPlusCost; cost += enhancementCost; if (mostExpensiveEnchantmentCost < enhancementCost) { mostExpensiveEnchantmentCost = enhancementCost; @@ -3011,1712 +2587,30 @@ public static int RulesRecipeItemCost(BlueprintItem blueprint, int baseCost = -1 return (3 * (baseCost + cost) - mostExpensiveEnchantmentCost) / (blueprint is BlueprintItemEquipmentUsable ? 1 : 2); } - // Attempt to work out the cost of enchantments which aren't in recipes by checking if blueprint, which contains the enchantment, contains only other - // enchantments whose cost is know. - private static bool ReverseEngineerEnchantmentCost(BlueprintItemEquipment blueprint, string enchantmentId) { - if (blueprint == null || blueprint.IsNotable || blueprint.Ability != null || blueprint.ActivatableAbility != null) { - return false; + public static void AddBattleLogMessage(string message, object tooltip = null, Color? color = null) { +#if PATCH21 + var data = new LogItemData(message, color ?? GameLogStrings.Instance.DefaultColor, tooltip, PrefixIcon.None, new List { LogChannel.Combat }); +#else + var data = new LogDataManager.LogItemData(message, color ?? GameLogStrings.Instance.DefaultColor, tooltip, PrefixIcon.None); +#endif + if (Game.Instance.UI.BattleLogManager) { + Game.Instance.UI.BattleLogManager.LogView.AddLogEntry(data); + } else { + PendingLogItems.Add(data); } + } - if (blueprint is BlueprintItemShield || blueprint is BlueprintItemWeapon || blueprint is BlueprintItemArmor) { - // Cost of enchantments on arms and armor is different, and can be treated as a straight delta. + public static bool EquipmentEnchantmentValid(ItemEntityWeapon weapon, ItemEntity owner) + { + if ((weapon == owner) || + (weapon != null && (weapon.Blueprint.IsNatural || weapon.Blueprint.IsUnarmed))) + { return true; } - - var mostExpensiveEnchantmentCost = 0; - var costSum = 0; - foreach (var enchantment in blueprint.Enchantments) { - if (enchantment.AssetGuid == enchantmentId) { - continue; - } - - if (!EnchantmentIdToRecipe.ContainsKey(enchantment.AssetGuid) && !EnchantmentIdToCost.ContainsKey(enchantment.AssetGuid)) { - return false; - } - - var enchantmentCost = GetEnchantmentCost(enchantment.AssetGuid, blueprint); - costSum += enchantmentCost; - if (mostExpensiveEnchantmentCost < enchantmentCost) { - mostExpensiveEnchantmentCost = enchantmentCost; - } - } - - var remainder = blueprint.Cost - 3 * costSum / 2; - if (remainder >= mostExpensiveEnchantmentCost) { - // enchantmentId is the most expensive enchantment - EnchantmentIdToCost[enchantmentId] = remainder; - } else { - // mostExpensiveEnchantmentCost is the most expensive enchantment - EnchantmentIdToCost[enchantmentId] = (2 * remainder + mostExpensiveEnchantmentCost) / 3; - } - - return true; - } - - [Harmony12.HarmonyPatch(typeof(MainMenu), "Start")] - private static class MainMenuStartPatch { - private static bool mainMenuStarted; - - private static void InitialiseCraftingData() { - // Read the crafting data now that ResourcesLibrary is loaded. - ItemCraftingData = ReadJsonFile($"{ModEntry.Path}/Data/ItemTypes.json", new CraftingTypeConverter()); - // Initialise lookup tables. - foreach (var itemData in ItemCraftingData) { - if (itemData is RecipeBasedItemCraftingData recipeBased) { - recipeBased.Recipes = recipeBased.RecipeFileNames.Aggregate(Enumerable.Empty(), - (all, fileName) => all.Concat(ReadJsonFile($"{ModEntry.Path}/Data/{fileName}")) - ).Where(recipe => { - return (recipe.ResultItem != null) - || (recipe.Enchantments.Length > 0) - || (recipe.NoResultItem && recipe.NoEnchantments); - }).ToArray(); - - foreach (var recipe in recipeBased.Recipes) { - if (recipe.ResultItem != null) { - if (recipe.NameId == null) { - recipe.NameId = recipe.ResultItem.Name; - } else { - recipe.NameId = new L10NString(recipe.NameId).ToString(); - } - } else if (recipe.NameId != null) { - recipe.NameId = new L10NString(recipe.NameId).ToString(); - } - if (recipe.ParentNameId != null) { - recipe.ParentNameId = new L10NString(recipe.ParentNameId).ToString(); - } - recipe.Enchantments.ForEach(enchantment => AddRecipeForEnchantment(enchantment.AssetGuid, recipe)); - if (recipe.Material != 0) { - AddRecipeForMaterial(recipe.Material, recipe); - } - - if (recipe.ParentNameId != null) { - recipeBased.SubRecipes = recipeBased.SubRecipes ?? new Dictionary>(); - if (!recipeBased.SubRecipes.ContainsKey(recipe.ParentNameId)) { - recipeBased.SubRecipes[recipe.ParentNameId] = new List(); - } - - recipeBased.SubRecipes[recipe.ParentNameId].Add(recipe); - } - } - - if (recipeBased.Name.StartsWith("CraftMundane")) { - foreach (var blueprint in recipeBased.NewItemBaseIDs) { - if (!blueprint.AssetGuid.Contains("#CraftMagicItems")) { - AddItemForType(blueprint); - } - } - } - } - - if (itemData.ParentNameId != null) { - if (!SubCraftingData.ContainsKey(itemData.ParentNameId)) { - SubCraftingData[itemData.ParentNameId] = new List(); - } - - SubCraftingData[itemData.ParentNameId].Add(itemData); - } - } - - var allUsableItems = ResourcesLibrary.GetBlueprints(); - foreach (var item in allUsableItems) { - AddItemIdForEnchantment(item); - } - - var allNonRecipeEnchantmentsInItems = ResourcesLibrary.GetBlueprints() - .Where(enchantment => !EnchantmentIdToRecipe.ContainsKey(enchantment.AssetGuid) && EnchantmentIdToItem.ContainsKey(enchantment.AssetGuid)) - .ToArray(); - // BlueprintEnchantment.EnchantmentCost seems to be full of nonsense values - attempt to set cost of each enchantment by using the prices of - // items with enchantments. - foreach (var enchantment in allNonRecipeEnchantmentsInItems) { - var itemsWithEnchantment = EnchantmentIdToItem[enchantment.AssetGuid]; - foreach (var item in itemsWithEnchantment) { - if (DoesItemMatchAllEnchantments(item, enchantment.AssetGuid)) { - EnchantmentIdToCost[enchantment.AssetGuid] = item.Cost; - break; - } - } - } - - foreach (var enchantment in allNonRecipeEnchantmentsInItems) { - if (!EnchantmentIdToCost.ContainsKey(enchantment.AssetGuid)) { - var itemsWithEnchantment = EnchantmentIdToItem[enchantment.AssetGuid]; - foreach (var item in itemsWithEnchantment) { - if (ReverseEngineerEnchantmentCost(item, enchantment.AssetGuid)) { - break; - } - } - } - } - CustomLootItems = ReadJsonFile($"{ModEntry.Path}/Data/LootItems.json"); - } - - private static void AddCraftingFeats(ObjectIDGenerator idGenerator, BlueprintProgression progression) { - foreach (var levelEntry in progression.LevelEntries) { - foreach (var featureBase in levelEntry.Features) { - var selection = featureBase as BlueprintFeatureSelection; - if (selection != null && (CraftingFeatGroups.Contains(selection.Group) || CraftingFeatGroups.Contains(selection.Group2))) { - // Use ObjectIDGenerator to detect which shared lists we've added the feats to. - idGenerator.GetId(selection.AllFeatures, out var firstTime); - if (firstTime) { - foreach (var data in ItemCraftingData) { - if (data.FeatGuid != null) { - var featBlueprint = ResourcesLibrary.TryGetBlueprint(data.FeatGuid) as BlueprintFeature; - var list = selection.AllFeatures.ToList(); - list.Add(featBlueprint); - selection.AllFeatures = list.ToArray(); - idGenerator.GetId(selection.AllFeatures, out firstTime); - } - } - } - } - } - } - } - - private static void AddAllCraftingFeats() { - var idGenerator = new ObjectIDGenerator(); - // Add crafting feats to general feat selection - AddCraftingFeats(idGenerator, Game.Instance.BlueprintRoot.Progression.FeatsProgression); - // ... and to relevant class feat selections. - foreach (var characterClass in Game.Instance.BlueprintRoot.Progression.CharacterClasses) { - AddCraftingFeats(idGenerator, characterClass.Progression); - } - - // Alchemists get Brew Potion as a bonus 1st level feat, except for Grenadier archetype alchemists. - var brewPotionData = ItemCraftingData.First(data => data.Name == "Potion"); - var brewPotion = ResourcesLibrary.TryGetBlueprint(brewPotionData.FeatGuid); - var alchemistProgression = ResourcesLibrary.TryGetBlueprint(AlchemistProgressionGuid); - var grenadierArchetype = ResourcesLibrary.TryGetBlueprint(AlchemistGrenadierArchetypeGuid); - if (brewPotion != null && alchemistProgression != null && grenadierArchetype != null) { - var firstLevelIndex = alchemistProgression.LevelEntries.FindIndex((levelEntry) => (levelEntry.Level == 1)); - alchemistProgression.LevelEntries[firstLevelIndex].Features.Add(brewPotion); - alchemistProgression.UIDeterminatorsGroup = alchemistProgression.UIDeterminatorsGroup.Concat(new[] {brewPotion}).ToArray(); - // Vanilla Grenadier has no level 1 RemoveFeatures, but a mod may have changed that, so search for it as well. - var firstLevelGrenadierRemoveIndex = grenadierArchetype.RemoveFeatures.FindIndex((levelEntry) => (levelEntry.Level == 1)); - if (firstLevelGrenadierRemoveIndex < 0) { - var removeFeatures = new[] {new LevelEntry {Level = 1}}; - grenadierArchetype.RemoveFeatures = removeFeatures.Concat(grenadierArchetype.RemoveFeatures).ToArray(); - firstLevelGrenadierRemoveIndex = 0; - } - grenadierArchetype.RemoveFeatures[firstLevelGrenadierRemoveIndex].Features.Add(brewPotion); - } else { - ModEntry.Logger.Warning("Failed to locate Alchemist progression, Grenadier archetype or Brew Potion feat!"); - } - - // Scroll Savant should get Scribe Scroll as a bonus 1st level feat. - var scribeScrollData = ItemCraftingData.First(data => data.Name == "Scroll"); - var scribeScroll = ResourcesLibrary.TryGetBlueprint(scribeScrollData.FeatGuid); - var scrollSavantArchetype = ResourcesLibrary.TryGetBlueprint(ScrollSavantArchetypeGuid); - if (scribeScroll != null && scrollSavantArchetype != null) { - var firstLevelAdd = scrollSavantArchetype.AddFeatures.First((levelEntry) => (levelEntry.Level == 1)); - firstLevelAdd.Features.Add(scribeScroll); - } else { - ModEntry.Logger.Warning("Failed to locate Scroll Savant archetype or Scribe Scroll feat!"); - } - } - - [Harmony12.HarmonyPatch(typeof(TwoWeaponFightingAttackPenalty), "OnEventAboutToTrigger", new Type[] { typeof(RuleCalculateAttackBonusWithoutTarget) })] - private static class TwoWeaponFightingAttackPenaltyOnEventAboutToTriggerPatch { - static public BlueprintFeature ShieldMaster; - static MethodInfo methodToFind; - private static bool Prepare() { - try { - methodToFind = Harmony12.AccessTools.Property(typeof(ItemEntityWeapon), nameof(ItemEntityWeapon.IsShield)).GetGetMethod(); - } catch (Exception ex) { - Main.ModEntry.Logger.Log($"Error Preparing: {ex.Message}"); - return false; - } - return true; - } - private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator il) { - Label start = il.DefineLabel(); - yield return new Harmony12.CodeInstruction(OpCodes.Ldarg_0); - yield return new Harmony12.CodeInstruction(OpCodes.Ldarg_1); - yield return new Harmony12.CodeInstruction(OpCodes.Call, new Func(CheckShieldMaster).Method); - yield return new Harmony12.CodeInstruction(OpCodes.Brfalse_S, start); - yield return new Harmony12.CodeInstruction(OpCodes.Ret); - var skip = 0; - Harmony12.CodeInstruction prev = instructions.First(); - prev.labels.Add(start); - foreach (var inst in instructions.Skip(1)) { - if (prev.opcode == OpCodes.Ldloc_1 && inst.opcode == OpCodes.Callvirt && inst.operand as MethodInfo == methodToFind) { - // ldloc.1 - // callvirt instance bool Kingmaker.Items.ItemEntityWeapon::get_IsShield() - // brtrue.s IL_0152 - skip = 3; - } - if (skip > 0) { - skip--; - } else { - yield return prev; - } - prev = inst; - } - if (skip == 0) { - yield return prev; - } - } - - private static bool CheckShieldMaster(TwoWeaponFightingAttackPenalty component, RuleCalculateAttackBonusWithoutTarget evt) { - ItemEntityWeapon maybeWeapon2 = evt.Initiator.Body.SecondaryHand.MaybeWeapon; -#if !PATCH21 - RuleAttackWithWeapon ruleAttackWithWeapon = evt.Reason.Rule as RuleAttackWithWeapon; - if (ruleAttackWithWeapon != null && !ruleAttackWithWeapon.IsFullAttack) - return true; -#endif - return maybeWeapon2 != null && evt.Weapon == maybeWeapon2 && maybeWeapon2.IsShield && component.Owner.Progression.Features.HasFact(ShieldMaster); - } - } - - [AllowMultipleComponents] - public class ShieldMasterPatch : GameLogicComponent, IInitiatorRulebookHandler, IInitiatorRulebookHandler, IInitiatorRulebookHandler { - public void OnEventAboutToTrigger(RuleCalculateDamage evt) { - if (!evt.Initiator.Body.SecondaryHand.HasShield || evt.DamageBundle.Weapon == null || !evt.DamageBundle.Weapon.IsShield) { - return; - } - var armorEnhancementBonus = GameHelper.GetItemEnhancementBonus(evt.Initiator.Body.SecondaryHand.Shield.ArmorComponent); - var weaponEnhancementBonus = GameHelper.GetItemEnhancementBonus(evt.Initiator.Body.SecondaryHand.Shield.WeaponComponent); - if (weaponEnhancementBonus == 0 && evt.Initiator.Body.SecondaryHand.Shield.WeaponComponent.Blueprint.IsMasterwork) { - weaponEnhancementBonus = 1; - } - var itemEnhancementBonus = armorEnhancementBonus - weaponEnhancementBonus; - PhysicalDamage physicalDamage = evt.DamageBundle.WeaponDamage as PhysicalDamage; - if (physicalDamage != null && itemEnhancementBonus > 0) { - physicalDamage.Enchantment += itemEnhancementBonus; - physicalDamage.EnchantmentTotal += itemEnhancementBonus; - } - } - public void OnEventDidTrigger(RuleCalculateWeaponStats evt) { } - - public void OnEventAboutToTrigger(RuleCalculateWeaponStats evt) { - if (!evt.Initiator.Body.SecondaryHand.HasShield || evt.Weapon == null || !evt.Weapon.IsShield) { - return; - } - var armorEnhancementBonus = GameHelper.GetItemEnhancementBonus(evt.Initiator.Body.SecondaryHand.Shield.ArmorComponent); - var weaponEnhancementBonus = GameHelper.GetItemEnhancementBonus(evt.Initiator.Body.SecondaryHand.Shield.WeaponComponent); - var itemEnhancementBonus = armorEnhancementBonus - weaponEnhancementBonus; - if (itemEnhancementBonus > 0) { - evt.AddBonusDamage(itemEnhancementBonus); - } - } - public void OnEventDidTrigger(RuleCalculateDamage evt) { } - - public void OnEventAboutToTrigger(RuleCalculateAttackBonusWithoutTarget evt) { - if (!evt.Initiator.Body.SecondaryHand.HasShield || evt.Weapon == null || !evt.Weapon.IsShield) { - return; - } - var armorEnhancementBonus = GameHelper.GetItemEnhancementBonus(evt.Initiator.Body.SecondaryHand.Shield.ArmorComponent); - var weaponEnhancementBonus = GameHelper.GetItemEnhancementBonus(evt.Initiator.Body.SecondaryHand.Shield.WeaponComponent); - var num = armorEnhancementBonus - weaponEnhancementBonus; - if (num > 0) { - evt.AddBonus(num, base.Fact); - } - } - public void OnEventDidTrigger(RuleCalculateAttackBonusWithoutTarget evt) { } - } - - private static void PatchBlueprints() { - var shieldMaster = ResourcesLibrary.TryGetBlueprint(ShieldMasterGuid); - var twoWeaponFighting = ResourcesLibrary.TryGetBlueprint(TwoWeaponFightingBasicMechanicsGuid); - TwoWeaponFightingAttackPenaltyOnEventAboutToTriggerPatch.ShieldMaster = shieldMaster; - Accessors.SetBlueprintUnitFactDisplayName(twoWeaponFighting, new L10NString("e32ce256-78dc-4fd0-bf15-21f9ebdf9921")); - - for (int i = 0; i < shieldMaster.ComponentsArray.Length; i++) { - if (shieldMaster.ComponentsArray[i] is ShieldMaster component) { - shieldMaster.ComponentsArray[i] = CraftMagicItems.Accessors.Create(a => { - a.name = component.name.Replace("ShieldMaster", "ShieldMasterPatch"); - }); - } - } - - var lightShield = ResourcesLibrary.TryGetBlueprint(WeaponLightShieldGuid); - Accessors.SetBlueprintItemBaseDamage(lightShield, new DiceFormula(1, DiceType.D3)); - var heavyShield = ResourcesLibrary.TryGetBlueprint(WeaponHeavyShieldGuid); - Accessors.SetBlueprintItemBaseDamage(heavyShield, new DiceFormula(1, DiceType.D4)); - - for (int i = 0; i < ItemEnchantmentGuids.Length; i += 2) { - var source = ResourcesLibrary.TryGetBlueprint(ItemEnchantmentGuids[i]); - var dest = ResourcesLibrary.TryGetBlueprint(ItemEnchantmentGuids[i + 1]); - Accessors.SetBlueprintItemEnchantmentEnchantName(dest, Accessors.GetBlueprintItemEnchantmentEnchantName(source)); - Accessors.SetBlueprintItemEnchantmentDescription(dest, Accessors.GetBlueprintItemEnchantmentDescription(source)); - } - - var longshankBane = ResourcesLibrary.TryGetBlueprint(LongshankBaneGuid); - if (longshankBane.ComponentsArray.Length >= 2 && longshankBane.ComponentsArray[1] is WeaponConditionalDamageDice conditional) { - for (int i = 0; i < conditional.Conditions.Conditions.Length; i++) { - if (conditional.Conditions.Conditions[i] is Kingmaker.Designers.EventConditionActionSystem.Conditions.HasFact condition) { -#if PATCH21_BETA - var replace = SerializedScriptableObject.CreateInstance(); -#else - var replace = ScriptableObject.CreateInstance(); -#endif - replace.Fact = condition.Fact; - replace.name = condition.name.Replace("HasFact", "ContextConditionHasFact"); - conditional.Conditions.Conditions[i] = replace; - } - } - } - } - - private static void PatchIk() { - foreach (var patch in IkPatchList) { - var weapon = ResourcesLibrary.TryGetBlueprint(patch.m_uuid); - if (weapon != null) { - var model = weapon.VisualParameters.Model; var equipmentOffsets = model.GetComponent(); - var locator = new GameObject(); - locator.transform.SetParent(model.transform); - locator.transform.localPosition = new Vector3(patch.m_x, patch.m_y, patch.m_z); - locator.transform.localEulerAngles = new Vector3(0.0f, 0.0f, 0.0f); - locator.transform.localScale = new Vector3(1.0f, 1.0f, 1.0f); - equipmentOffsets.IkTargetLeftHand = locator.transform; - } - } - } - - private static void InitialiseMod() { - if (modEnabled) { - PatchBlueprints(); - PatchIk(); - InitialiseCraftingData(); - AddAllCraftingFeats(); - } - } - - [Harmony12.HarmonyPriority(Harmony12.Priority.Last)] - public static void Postfix() { - if (!mainMenuStarted) { - mainMenuStarted = true; - InitialiseMod(); - } - } - - public static void ModEnabledChanged() { - if (!mainMenuStarted && ResourcesLibrary.LibraryObject != null) { - mainMenuStarted = true; - L10n.SetEnabled(true); - SustenanceEnchantment.MainMenuStartPatch.Postfix(); - WildEnchantment.MainMenuStartPatch.Postfix(); - CreateQuiverAbility.MainMenuStartPatch.Postfix(); - } - - if (!modEnabled) { - // Reset everything InitialiseMod initialises - ItemCraftingData = null; - SubCraftingData.Clear(); - SpellIdToItem.Clear(); - TypeToItem.Clear(); - EnchantmentIdToItem.Clear(); - EnchantmentIdToCost.Clear(); - EnchantmentIdToRecipe.Clear(); - } else if (mainMenuStarted) { - // If the mod is enabled and we're past the Start of main menu, (re-)initialise. - InitialiseMod(); - } - L10n.SetEnabled(modEnabled); - } - } - -#if PATCH21 - [Harmony12.HarmonyPatch(typeof(MainMenuUiContext), "Initialize")] - private static class MainMenuUiContextInitializePatch { - [Harmony12.HarmonyPriority(Harmony12.Priority.Last)] - private static void Postfix() { - MainMenuStartPatch.Postfix(); - } - } -#endif - -#if !PATCH21 - // Fix issue in Owlcat's UI - ActionBarManager.Update does not refresh the Groups (spells/Actions/Belt) - [Harmony12.HarmonyPatch(typeof(ActionBarManager), "Update")] - private static class ActionBarManagerUpdatePatch { - private static void Prefix(ActionBarManager __instance) { - var mNeedReset = Accessors.GetActionBarManagerNeedReset(__instance); - if (mNeedReset) { - var mSelected = Accessors.GetActionBarManagerSelected(__instance); - __instance.Group.Set(mSelected); - } - } - } -#endif - - [Harmony12.HarmonyPatch(typeof(BlueprintItemEquipmentUsable), "Cost", Harmony12.MethodType.Getter)] - // ReSharper disable once UnusedMember.Local - private static class BlueprintItemEquipmentUsableCostPatch { - // ReSharper disable once UnusedMember.Local - private static void Postfix(BlueprintItemEquipmentUsable __instance, ref int __result) { - if (__result == 0 && __instance.SpellLevel == 0) { - // Owlcat's cost calculation doesn't handle level 0 spells properly. - int chargeCost; - switch (__instance.Type) { - case UsableItemType.Wand: - chargeCost = 15; - break; - case UsableItemType.Scroll: - chargeCost = 25; - break; - case UsableItemType.Potion: - chargeCost = 50; - break; - default: - return; - } - __result = __instance.CasterLevel * chargeCost * __instance.Charges / 2; - } - } - } - - // Load Variant spells into m_KnownSpellLevels - [Harmony12.HarmonyPatch(typeof(Spellbook), "PostLoad")] - // ReSharper disable once UnusedMember.Local - private static class SpellbookPostLoadPatch { - // ReSharper disable once UnusedMember.Local - private static void Postfix(Spellbook __instance) { - if (!modEnabled) { - return; - } - - var mKnownSpells = Accessors.GetSpellbookKnownSpells(__instance); - var mKnownSpellLevels = Accessors.GetSpellbookKnownSpellLevels(__instance); - for (var level = 0; level < mKnownSpells.Length; ++level) { - foreach (var spell in mKnownSpells[level]) { - if (spell.Blueprint.Variants != null) { - foreach (var variant in spell.Blueprint.Variants) { - mKnownSpellLevels[variant] = level; - } - } - } - } - } - } - - // Owlcat's code doesn't correctly detect that a variant spell is in a spellList when its parent spell is. - [Harmony12.HarmonyPatch(typeof(BlueprintAbility), "IsInSpellList")] - // ReSharper disable once UnusedMember.Global - public static class BlueprintAbilityIsInSpellListPatch { - // ReSharper disable once UnusedMember.Local - private static void Postfix(BlueprintAbility __instance, BlueprintSpellList spellList, ref bool __result) { - if (!__result && __instance.Parent != null && __instance.Parent != __instance) { - __result = __instance.Parent.IsInSpellList(spellList); - } - } - } - - public static void AddBattleLogMessage(string message, object tooltip = null, Color? color = null) { -#if PATCH21 - var data = new LogItemData(message, color ?? GameLogStrings.Instance.DefaultColor, tooltip, PrefixIcon.None, new List { LogChannel.Combat }); -#else - var data = new LogDataManager.LogItemData(message, color ?? GameLogStrings.Instance.DefaultColor, tooltip, PrefixIcon.None); -#endif - if (Game.Instance.UI.BattleLogManager) { - Game.Instance.UI.BattleLogManager.LogView.AddLogEntry(data); - } else { - PendingLogItems.Add(data); - } - } - -#if !PATCH21 - [Harmony12.HarmonyPatch(typeof(LogDataManager.LogItemData), "UpdateSize")] - private static class LogItemDataUpdateSizePatch { - // ReSharper disable once UnusedMember.Local - private static bool Prefix() { - // Avoid null pointer exception when BattleLogManager not set. - return Game.Instance.UI.BattleLogManager != null; - } - } -#endif - - // Add "pending" log items when the battle log becomes available again, so crafting messages sent when e.g. camping - // in the overland map are still shown eventually. - [Harmony12.HarmonyPatch(typeof(BattleLogManager), "Initialize")] - // ReSharper disable once UnusedMember.Local - private static class BattleLogManagerInitializePatch { - // ReSharper disable once UnusedMember.Local - private static void Postfix() { - if (Enumerable.Any(PendingLogItems)) { - foreach (var item in PendingLogItems) { - item.UpdateSize(); - Game.Instance.UI.BattleLogManager.LogView.AddLogEntry(item); - } - - PendingLogItems.Clear(); - } - } - } - - private static AbilityData FindCasterSpell(UnitDescriptor caster, BlueprintAbility spellBlueprint, bool mustHavePrepared, - IReadOnlyCollection spellsToCast) { - foreach (var spellbook in caster.Spellbooks) { - var spellLevel = spellbook.GetSpellLevel(spellBlueprint); - if (spellLevel > spellbook.MaxSpellLevel || spellLevel < 0) { - continue; - } - - if (mustHavePrepared && spellLevel > 0) { - if (spellbook.Blueprint.Spontaneous) { - // Count how many other spells of this class and level they're going to cast, to ensure they don't double-dip on spell slots. - var toCastCount = spellsToCast.Count(ability => ability.Spellbook == spellbook && spellbook.GetSpellLevel(ability) == spellLevel); - // Spontaneous spellcaster must have enough spell slots of the required level. - if (spellbook.GetSpontaneousSlots(spellLevel) <= toCastCount) { - continue; - } - } else { - // Prepared spellcaster must have memorized the spell... - var spellSlot = spellbook.GetMemorizedSpells(spellLevel).FirstOrDefault(slot => - slot.Available && (slot.Spell?.Blueprint == spellBlueprint || - spellBlueprint.Parent && slot.Spell?.Blueprint == spellBlueprint.Parent)); - if (spellSlot == null && (spellbook.GetSpontaneousConversionSpells(spellLevel).Contains(spellBlueprint) || - (spellBlueprint.Parent && - spellbook.GetSpontaneousConversionSpells(spellLevel).Contains(spellBlueprint.Parent)))) { - // ... or be able to convert, in which case any available spell of the given level will do. - spellSlot = spellbook.GetMemorizedSpells(spellLevel).FirstOrDefault(slot => slot.Available); - } - - if (spellSlot == null) { - continue; - } - - return spellSlot.Spell; - } - } - - return spellbook.GetKnownSpells(spellLevel).Concat(spellbook.GetSpecialSpells(spellLevel)) - .First(known => known.Blueprint == spellBlueprint || - (spellBlueprint.Parent && known.Blueprint == spellBlueprint.Parent)); - } - - // Try casting the spell from an item - ItemEntity fromItem = null; - var fromItemCharges = 0; - foreach (var item in caster.Inventory) { - // Check (non-potion) items wielded by the caster to see if they can cast the required spell - if (item.Wielder == caster && (!(item.Blueprint is BlueprintItemEquipmentUsable usable) || usable.Type != UsableItemType.Potion) - && (item.Ability?.Blueprint == spellBlueprint || - (spellBlueprint.Parent && item.Ability?.Blueprint == spellBlueprint.Parent))) { - // Choose the item with the most available charges, with a multiplier if the item restores charges on rest. - var charges = item.Charges * (((BlueprintItemEquipment) item.Blueprint).RestoreChargesOnRest ? 50 : 1); - if (charges > fromItemCharges) { - fromItem = item; - fromItemCharges = charges; - } - } - } - - return fromItem?.Ability?.Data; - } - - private static int CheckSpellPrerequisites(CraftingProjectData project, UnitDescriptor caster, bool mustPrepare, - out List missingSpells, out List spellsToCast) { - return CheckSpellPrerequisites(project.SpellPrerequisites, project.AnyPrerequisite, caster, mustPrepare, out missingSpells, out spellsToCast); - } - - private static int CheckSpellPrerequisites(BlueprintAbility[] prerequisites, bool anyPrerequisite, UnitDescriptor caster, bool mustPrepare, - out List missingSpells, out List spellsToCast) { - spellsToCast = new List(); - missingSpells = new List(); - if (prerequisites != null) { - foreach (var spellBlueprint in prerequisites) { - var spell = FindCasterSpell(caster, spellBlueprint, mustPrepare, spellsToCast); - if (spell != null) { - spellsToCast.Add(spell); - if (anyPrerequisite) { - missingSpells.Clear(); - return 0; - } - } else { - missingSpells.Add(spellBlueprint); - } - } - } - - return anyPrerequisite ? Math.Min(1, missingSpells.Count) : missingSpells.Count; - } - - private static int CheckFeatPrerequisites(CraftingProjectData project, UnitDescriptor caster, out List missingFeats) { - return CheckFeatPrerequisites(project.FeatPrerequisites, project.AnyPrerequisite, caster, out missingFeats); - } - - private static int CheckFeatPrerequisites(BlueprintFeature[] prerequisites, bool anyPrerequisite, UnitDescriptor caster, - out List missingFeats) { - missingFeats = new List(); - if (prerequisites != null) { - foreach (var featBlueprint in prerequisites) { - var feat = caster.GetFeature(featBlueprint); - if (feat != null) { - if (anyPrerequisite) { - missingFeats.Clear(); - return 0; - } - } else { - missingFeats.Add(featBlueprint); - } - } - } - - return anyPrerequisite ? Math.Min(1, missingFeats.Count) : missingFeats.Count; - } - - private static int CheckCrafterPrerequisites(CraftingProjectData project, UnitDescriptor caster) { - var missing = GetMissingCrafterPrerequisites(project.CrafterPrerequisites, caster); - foreach (var prerequisite in missing) { - AddBattleLogMessage(L10NFormat("craftMagicItems-logMessage-missing-crafter-prerequisite", - new L10NString($"craftMagicItems-crafter-prerequisite-{prerequisite}"), MissingPrerequisiteDCModifier)); - } - - return missing.Count; - } - - private static List GetMissingCrafterPrerequisites(CrafterPrerequisiteType[] prerequisites, UnitDescriptor caster) { - var missingCrafterPrerequisites = new List(); - if (prerequisites != null) { - missingCrafterPrerequisites.AddRange(prerequisites.Where(prerequisite => - prerequisite == CrafterPrerequisiteType.AlignmentLawful && (caster.Alignment.Value.ToMask() & AlignmentMaskType.Lawful) == 0 - || prerequisite == CrafterPrerequisiteType.AlignmentGood && (caster.Alignment.Value.ToMask() & AlignmentMaskType.Good) == 0 - || prerequisite == CrafterPrerequisiteType.AlignmentChaotic && (caster.Alignment.Value.ToMask() & AlignmentMaskType.Chaotic) == 0 - || prerequisite == CrafterPrerequisiteType.AlignmentEvil && (caster.Alignment.Value.ToMask() & AlignmentMaskType.Evil) == 0 - || prerequisite == CrafterPrerequisiteType.FeatureChannelEnergy && - caster.GetFeature(ResourcesLibrary.TryGetBlueprint(ChannelEnergyFeatureGuid)) == null - )); - } - - return missingCrafterPrerequisites; - } - - private static void WorkOnProjects(UnitDescriptor caster, bool returningToCapital) { - if (!caster.IsPlayerFaction || caster.State.IsDead || caster.State.IsFinallyDead) { - return; - } - - currentCaster = caster.Unit; - var withPlayer = Game.Instance.Player.PartyCharacters.Contains(caster.Unit); - var playerInCapital = IsPlayerInCapital(); - // Only update characters in the capital when the player is also there. - if (!withPlayer && !playerInCapital) { - // Character is back in the capital - skipping them for now. - return; - } - - var isAdventuring = withPlayer && !IsPlayerSomewhereSafe(); - var timer = GetCraftingTimerComponentForCaster(caster); - if (timer == null || timer.CraftingProjects.Count == 0) { - // Character is not doing any crafting - return; - } - - // Round up the number of days, so caster makes some progress on a new project the first time they rest. - var interval = Game.Instance.Player.GameTime.Subtract(timer.LastUpdated); - var daysAvailableToCraft = (int) Math.Ceiling(interval.TotalDays); - if (daysAvailableToCraft <= 0) { - if (isAdventuring) { - AddBattleLogMessage(L10NFormat("craftMagicItems-logMessage-not-full-day")); - } - - return; - } - - // Time passes for this character even if they end up making no progress on their projects. LastUpdated can go up to - // a day into the future, due to the rounding up of daysAvailableToCraft. - timer.LastUpdated += TimeSpan.FromDays(daysAvailableToCraft); - // Work on projects sequentially, skipping any that can't progress due to missing items, missing prerequisites or having too high a DC. - foreach (var project in timer.CraftingProjects.ToList()) { - if (project.UpgradeItem != null) { - // Check if the item has been dropped and picked up again, which apparently creates a new object with the same blueprint. - if (project.UpgradeItem.Collection != Game.Instance.Player.Inventory && project.UpgradeItem.Collection != Game.Instance.Player.SharedStash) { - var itemInStash = Game.Instance.Player.SharedStash.FirstOrDefault(item => item.Blueprint == project.UpgradeItem.Blueprint); - if (itemInStash != null) { - ItemUpgradeProjects.Remove(project.UpgradeItem); - ItemUpgradeProjects[itemInStash] = project; - project.UpgradeItem = itemInStash; - } else { - var itemInInventory = Game.Instance.Player.Inventory.FirstOrDefault(item => item.Blueprint == project.UpgradeItem.Blueprint); - if (itemInInventory != null) { - ItemUpgradeProjects.Remove(project.UpgradeItem); - ItemUpgradeProjects[itemInInventory] = project; - project.UpgradeItem = itemInInventory; - } - } - } - - // Check that the caster can get at the item they're upgrading... it must be in the party inventory, and either un-wielded, or the crafter - // must be with the wielder (together in the capital or out in the party together). - var wieldedInParty = (project.UpgradeItem.Wielder == null || Game.Instance.Player.PartyCharacters.Contains(project.UpgradeItem.Wielder.Unit)); - if ((!playerInCapital || returningToCapital) - && (project.UpgradeItem.Collection != Game.Instance.Player.SharedStash || withPlayer) - && (project.UpgradeItem.Collection != Game.Instance.Player.Inventory || ((!withPlayer || !wieldedInParty) && (withPlayer || wieldedInParty)))) { - project.AddMessage(L10NFormat("craftMagicItems-logMessage-missing-upgrade-item", project.UpgradeItem.Blueprint.Name)); - AddBattleLogMessage(project.LastMessage); - continue; - } - } - - var craftingData = ItemCraftingData.FirstOrDefault(data => data.Name == project.ItemType); - StatType craftingSkill; - int dc; - int progressRate; - - if (project.ItemType == BondedItemRitual) { - craftingSkill = StatType.SkillKnowledgeArcana; - dc = 10 + project.Crafter.Stats.GetStat(craftingSkill).ModifiedValue; - progressRate = ModSettings.MagicCraftingRate; - } else if (IsMundaneCraftingData(craftingData)) { - craftingSkill = StatType.SkillKnowledgeWorld; - dc = CalculateMundaneCraftingDC((RecipeBasedItemCraftingData) craftingData, project.ResultItem.Blueprint, caster); - progressRate = ModSettings.MundaneCraftingRate; - } else { - craftingSkill = StatType.SkillKnowledgeArcana; - dc = 5 + project.CasterLevel; - progressRate = ModSettings.MagicCraftingRate; - } - - var missing = CheckSpellPrerequisites(project, caster, isAdventuring, out var missingSpells, out var spellsToCast); - if (missing > 0) { - var missingSpellNames = missingSpells.Select(ability => ability.Name).BuildCommaList(project.AnyPrerequisite); - if (craftingData.PrerequisitesMandatory || project.PrerequisitesMandatory) { - project.AddMessage(L10NFormat("craftMagicItems-logMessage-missing-prerequisite", - project.ResultItem.Name, missingSpellNames)); - AddBattleLogMessage(project.LastMessage); - // If the item type has mandatory prerequisites and some are missing, move on to the next project. - continue; - } - - AddBattleLogMessage(L10NFormat("craftMagicItems-logMessage-missing-spell", missingSpellNames, - MissingPrerequisiteDCModifier * missing)); - } - var missing2 = CheckFeatPrerequisites(project, caster, out var missingFeats); - if (missing2 > 0) { - var missingFeatNames = missingFeats.Select(ability => ability.Name).BuildCommaList(project.AnyPrerequisite); - AddBattleLogMessage(L10NFormat("craftMagicItems-logMessage-missing-feat", missingFeatNames, - MissingPrerequisiteDCModifier * missing2)); - } - missing += missing2; - missing += CheckCrafterPrerequisites(project, caster); - dc += MissingPrerequisiteDCModifier * missing; - var casterLevel = CharacterCasterLevel(caster); - if (casterLevel < project.CasterLevel) { - // Rob's ruling... if you're below the prerequisite caster level, you're considered to be missing a prerequisite for each - // level you fall short, unless ModSettings.CasterLevelIsSinglePrerequisite is true. - var casterLevelPenalty = ModSettings.CasterLevelIsSinglePrerequisite - ? MissingPrerequisiteDCModifier - : MissingPrerequisiteDCModifier * (project.CasterLevel - casterLevel); - dc += casterLevelPenalty; - AddBattleLogMessage(L10NFormat("craftMagicItems-logMessage-low-caster-level", project.CasterLevel, casterLevelPenalty)); - } - var oppositionSchool = CheckForOppositionSchool(caster, project.SpellPrerequisites); - if (oppositionSchool != SpellSchool.None) { - dc += OppositionSchoolDCModifier; - AddBattleLogMessage(L10NFormat("craftMagicItems-logMessage-opposition-school", - LocalizedTexts.Instance.SpellSchoolNames.GetText(oppositionSchool), OppositionSchoolDCModifier)); - } - - var skillCheck = 10 + caster.Stats.GetStat(craftingSkill).ModifiedValue; - if (skillCheck < dc) { - // Can't succeed by taking 10... move on to the next project. - project.AddMessage(L10NFormat("craftMagicItems-logMessage-dc-too-high", project.ResultItem.Name, - LocalizedTexts.Instance.Stats.GetText(craftingSkill), skillCheck, dc)); - AddBattleLogMessage(project.LastMessage); - continue; - } - - // Cleared the last hurdle, so caster is going to make progress on this project. - // You only work at 1/4 speed if you're crafting while adventuring. - var adventuringPenalty = !isAdventuring || ModSettings.CraftAtFullSpeedWhileAdventuring ? 1 : AdventuringProgressPenalty; - // Each 1 by which the skill check exceeds the DC increases the crafting rate by 20% of the base progressRate - var progressPerDay = (int) (progressRate * (1 + (float) (skillCheck - dc) / 5) / adventuringPenalty); - var daysUntilProjectFinished = (int) Math.Ceiling(1.0 * (project.TargetCost - project.Progress) / progressPerDay); - var daysCrafting = Math.Min(daysUntilProjectFinished, daysAvailableToCraft); - var progressGold = daysCrafting * progressPerDay; - foreach (var spell in spellsToCast) { - if (spell.SourceItem != null) { - // Use items whether we're adventuring or not, one charge per day of daysCrafting. We might run out of charges... - if (spell.SourceItem.IsSpendCharges && !((BlueprintItemEquipment) spell.SourceItem.Blueprint).RestoreChargesOnRest) { - var itemSpell = spell; - for (var day = 0; day < daysCrafting; ++day) { - if (itemSpell.SourceItem.Charges <= 0) { - // This item is exhausted and we haven't finished crafting - find another item. - itemSpell = FindCasterSpell(caster, spell.Blueprint, isAdventuring, spellsToCast); - } - - if (itemSpell == null) { - // We've run out of items that can cast the spell...crafting progress is going to slow, if not stop. - progressGold -= progressPerDay * (daysCrafting - day); - skillCheck -= MissingPrerequisiteDCModifier; - if (craftingData.PrerequisitesMandatory || project.PrerequisitesMandatory) { - AddBattleLogMessage(L10NFormat("craftMagicItems-logMessage-missing-prerequisite", project.ResultItem.Name, spell.Name)); - daysCrafting = day; - break; - } - - AddBattleLogMessage(L10NFormat("craftMagicItems-logMessage-missing-spell", spell.Name, MissingPrerequisiteDCModifier)); - if (skillCheck < dc) { - // Can no longer make progress - AddBattleLogMessage(L10NFormat("craftMagicItems-logMessage-dc-too-high", project.ResultItem.Name, - LocalizedTexts.Instance.Stats.GetText(craftingSkill), skillCheck, dc)); - daysCrafting = day; - } else { - // Progress will be slower, but they don't need to cast this spell any longer. - progressPerDay = (int) (progressRate * (1 + (float) (skillCheck - dc) / 5) / adventuringPenalty); - daysUntilProjectFinished = - day + (int) Math.Ceiling(1.0 * (project.TargetCost - project.Progress - progressGold) / progressPerDay); - daysCrafting = Math.Min(daysUntilProjectFinished, daysAvailableToCraft); - progressGold += (daysCrafting - day) * progressPerDay; - } - - break; - } - - GameLogContext.SourceUnit = caster.Unit; - GameLogContext.Text = itemSpell.SourceItem.Name; - AddBattleLogMessage(CharacterUsedItemLocalized); - GameLogContext.Clear(); - itemSpell.SourceItem.SpendCharges(caster); - } - } - } else if (isAdventuring) { - // Actually cast the spells if we're adventuring. - AddBattleLogMessage(L10NFormat("craftMagicItems-logMessage-expend-spell", spell.Name)); - spell.SpendFromSpellbook(); - } - } - - var progressKey = project.ItemType == BondedItemRitual - ? "craftMagicItems-logMessage-made-progress-bondedItem" - : "craftMagicItems-logMessage-made-progress"; - var progress = L10NFormat(progressKey, progressGold, project.TargetCost - project.Progress, project.ResultItem.Name); - var checkResult = L10NFormat("craftMagicItems-logMessage-made-progress-check", LocalizedTexts.Instance.Stats.GetText(craftingSkill), - skillCheck, dc); - AddBattleLogMessage(progress, checkResult); - daysAvailableToCraft -= daysCrafting; - project.Progress += progressGold; - if (project.Progress >= project.TargetCost) { - // Completed the project! - if (project.ItemType == BondedItemRitual) { - AddBattleLogMessage(L10NFormat("craftMagicItems-logMessage-bonding-ritual-complete", project.ResultItem.Name), project.ResultItem); - BondWithObject(project.Crafter, project.ResultItem); - } else { - AddBattleLogMessage(L10NFormat("craftMagicItems-logMessage-crafting-complete", project.ResultItem.Name), project.ResultItem); - CraftItem(project.ResultItem, project.UpgradeItem); - } - timer.CraftingProjects.Remove(project); - if (project.UpgradeItem == null) { - ItemCreationProjects.Remove(project); - } else { - ItemUpgradeProjects.Remove(project.UpgradeItem); - } - } else { - var completeKey = project.ItemType == BondedItemRitual - ? "craftMagicItems-logMessage-made-progress-bonding-ritual-amount-complete" - : "craftMagicItems-logMessage-made-progress-amount-complete"; - var amountComplete = L10NFormat(completeKey, project.ResultItem.Name, 100 * project.Progress / project.TargetCost); - AddBattleLogMessage(amountComplete, project.ResultItem); - project.AddMessage($"{progress} {checkResult}"); - } - - if (daysAvailableToCraft <= 0) { - return; - } - } - - if (daysAvailableToCraft > 0) { - // They didn't use up all available days - reset the time they can start crafting to now. - timer.LastUpdated = Game.Instance.Player.GameTime; - } - } - - [Harmony12.HarmonyPatch(typeof(CapitalCompanionLogic), "OnFactActivate")] - // ReSharper disable once UnusedMember.Local - private static class CapitalCompanionLogicOnFactActivatePatch { - // ReSharper disable once UnusedMember.Local - private static void Prefix() { - // Trigger project work on companions left behind in the capital, with a flag saying the party wasn't around while they were working. - foreach (var companion in Game.Instance.Player.RemoteCompanions) { - if (companion.Value != null) { - WorkOnProjects(companion.Value.Descriptor, true); - } - } - } - } - - // Make characters in the party work on their crafting projects when they rest. - [Harmony12.HarmonyPatch(typeof(RestController), "ApplyRest")] - // ReSharper disable once UnusedMember.Local - private static class RestControllerApplyRestPatch { - // ReSharper disable once UnusedMember.Local - private static void Prefix(UnitDescriptor unit) { - WorkOnProjects(unit, false); - } - } - - private static void AddToLootTables(BlueprintItem blueprint, string[] tableNames, bool firstTime) { - var tableCount = tableNames.Length; - foreach (var loot in ResourcesLibrary.GetBlueprints()) { - if (tableNames.Contains(loot.name)) { - tableCount--; - if (!loot.Items.Any(entry => entry.Item == blueprint)) { - var lootItems = loot.Items.ToList(); - lootItems.Add(new LootEntry {Count = 1, Item = blueprint}); - loot.Items = lootItems.ToArray(); - } - } - } - foreach (var unitLoot in ResourcesLibrary.GetBlueprints()) { - if (tableNames.Contains(unitLoot.name)) { - tableCount--; - if (unitLoot is BlueprintSharedVendorTable vendor) { - if (firstTime) { - var vendorTable = Game.Instance.Player.SharedVendorTables.GetTable(vendor); - vendorTable.Add(blueprint.CreateEntity()); - } - } else if (!unitLoot.ComponentsArray.Any(component => component is LootItemsPackFixed pack && pack.Item.Item == blueprint)) { - var lootItem = new LootItem(); - Accessors.SetLootItemItem(lootItem, blueprint); -#if PATCH21_BETA - var lootComponent = SerializedScriptableObject.CreateInstance(); -#else - var lootComponent = ScriptableObject.CreateInstance(); -#endif - Accessors.SetLootItemsPackFixedItem(lootComponent, lootItem); - blueprintPatcher.EnsureComponentNameUnique(lootComponent, unitLoot.ComponentsArray); - var components = unitLoot.ComponentsArray.ToList(); - components.Add(lootComponent); - unitLoot.ComponentsArray = components.ToArray(); - } - } - } - if (tableCount > 0) { - Harmony12.FileLog.Log($"!!! Failed to match all loot table names for {blueprint.Name}. {tableCount} table names not found."); - } - } - - private static void UpgradeSave(Version version) { - foreach (var lootItem in CustomLootItems) { - var firstTime = (version == null || version.CompareTo(lootItem.AddInVersion) < 0); - var item = ResourcesLibrary.TryGetBlueprint(lootItem.AssetGuid); - if (item == null) { - Harmony12.FileLog.Log($"!!! Loot item not created: {lootItem.AssetGuid}"); - } else { - AddToLootTables(item, lootItem.LootTables, firstTime); - } - } - } - - // After loading a save, perform various backward compatibility and initialisation operations. - [Harmony12.HarmonyPatch(typeof(Player), "PostLoad")] - // ReSharper disable once UnusedMember.Local - private static class PlayerPostLoadPatch { - // ReSharper disable once UnusedMember.Local - private static void Postfix() { - ItemUpgradeProjects.Clear(); - ItemCreationProjects.Clear(); - - var characterList = UIUtility.GetGroup(true); - foreach (var character in characterList) { - // If the mod is disabled, this will clean up crafting timer "buff" from all casters. - var timer = GetCraftingTimerComponentForCaster(character.Descriptor, character.IsMainCharacter); - var bondedItemComponent = GetBondedItemComponentForCaster(character.Descriptor); - - if (!modEnabled) { - continue; - } - - if (timer != null) { - foreach (var project in timer.CraftingProjects) { - if (project.ItemBlueprint != null) { - // Migrate all projects using ItemBlueprint to use ResultItem - var craftingData = ItemCraftingData.First(data => data.Name == project.ItemType); - project.ResultItem = BuildItemEntity(project.ItemBlueprint, craftingData, character); - project.ItemBlueprint = null; - } - - project.Crafter = character; - if (!project.ResultItem.HasUniqueVendor) { - // Set "vendor" of item if it's already in progress - project.ResultItem.SetVendorIfNull(character); - } - project.ResultItem.PostLoad(); - if (project.UpgradeItem == null) { - ItemCreationProjects.Add(project); - } else { - ItemUpgradeProjects[project.UpgradeItem] = project; - project.UpgradeItem.PostLoad(); - } - } - - if (character.IsMainCharacter) { - UpgradeSave(string.IsNullOrEmpty(timer.Version) ? null : Version.Parse(timer.Version)); - timer.Version = ModEntry.Version.ToString(); - } - } - - if (bondedItemComponent != null) { - bondedItemComponent.ownerItem?.PostLoad(); - bondedItemComponent.everyoneElseItem?.PostLoad(); - } - - // Retroactively give character any crafting feats in their past progression data which they don't actually have - // (e.g. Alchemists getting Brew Potion) - foreach (var characterClass in character.Descriptor.Progression.Classes) { - foreach (var levelData in characterClass.CharacterClass.Progression.LevelEntries) { - if (levelData.Level <= characterClass.Level) { - foreach (var feature in levelData.Features.OfType()) { - if (feature.AssetGuid.Contains("#CraftMagicItems(feat=") && !CharacterHasFeat(character, feature.AssetGuid)) { - character.Descriptor.Progression.Features.AddFeature(feature); - } - } - } - } - } - } - } - } - - [Harmony12.HarmonyPatch(typeof(Game), "OnAreaLoaded")] - // ReSharper disable once UnusedMember.Local - private static class GameOnAreaLoadedPatch { - // ReSharper disable once UnusedMember.Local - private static void Postfix() { - if (CustomBlueprintBuilder.DidDowngrade) { - UIUtility.ShowMessageBox("Craft Magic Items is disabled. All your custom enchanted items and crafting feats have been replaced with " + -#if PATCH21 - "vanilla versions.", DialogMessageBoxBase.BoxType.Message, null); -#else - "vanilla versions.", DialogMessageBox.BoxType.Message, null); -#endif - - CustomBlueprintBuilder.Reset(); - } - } - } - - [Harmony12.HarmonyPatch(typeof(WeaponParametersAttackBonus), "OnEventAboutToTrigger")] - // ReSharper disable once UnusedMember.Local - private static class WeaponParametersAttackBonusOnEventAboutToTriggerPatch { - private static bool Prefix(WeaponParametersAttackBonus __instance, RuleCalculateAttackBonusWithoutTarget evt) { - if (evt.Weapon != null && __instance.OnlyFinessable && evt.Weapon.Blueprint.Type.Category.HasSubCategory(WeaponSubCategory.Finessable) && - IsOversized(evt.Weapon.Blueprint)) { - return false; - } else { - return true; - } - } - } - - [Harmony12.HarmonyPatch(typeof(WeaponParametersDamageBonus), "OnEventAboutToTrigger", new Type[] { typeof(RuleCalculateWeaponStats) })] - // ReSharper disable once UnusedMember.Local - private static class WeaponParametersDamageBonusOnEventAboutToTriggerPatch { - private static bool Prefix(WeaponParametersDamageBonus __instance, RuleCalculateWeaponStats evt) { - if (evt.Weapon != null && __instance.OnlyFinessable && evt.Weapon.Blueprint.Type.Category.HasSubCategory(WeaponSubCategory.Finessable) && - IsOversized(evt.Weapon.Blueprint)) { - return false; - } else { - return true; - } - } - } - - [Harmony12.HarmonyPatch(typeof(AttackStatReplacement), "OnEventAboutToTrigger")] - // ReSharper disable once UnusedMember.Local - private static class AttackStatReplacementOnEventAboutToTriggerPatch { - private static bool Prefix(AttackStatReplacement __instance, RuleCalculateAttackBonusWithoutTarget evt) { - if (evt.Weapon != null && __instance.SubCategory == WeaponSubCategory.Finessable && - evt.Weapon.Blueprint.Type.Category.HasSubCategory(WeaponSubCategory.Finessable) && IsOversized(evt.Weapon.Blueprint)) { - return false; - } else { - return true; - } + else + { + return false; } } - - [Harmony12.HarmonyPatch(typeof(DamageGrace), "OnEventAboutToTrigger")] - // ReSharper disable once UnusedMember.Local - private static class DamageGraceOnEventAboutToTriggerPatch { - private static bool Prefix(DamageGrace __instance, RuleCalculateWeaponStats evt) { - if (evt.Weapon != null && evt.Weapon.Blueprint.Type.Category.HasSubCategory(WeaponSubCategory.Finessable) && - IsOversized(evt.Weapon.Blueprint)) { - return false; - } else { - return true; - } - } - } - - [Harmony12.HarmonyPatch(typeof(UIUtilityItem), "GetQualities")] - // ReSharper disable once UnusedMember.Local - private static class UIUtilityItemGetQualitiesPatch { - // ReSharper disable once UnusedMember.Local - private static void Postfix(ItemEntity item, ref string __result) { - if (!item.IsIdentified) { - return; - } - ItemEntityWeapon itemEntityWeapon = item as ItemEntityWeapon; - if (itemEntityWeapon == null) { - return; - } - WeaponCategory category = itemEntityWeapon.Blueprint.Category; - if (category.HasSubCategory(WeaponSubCategory.Finessable) && IsOversized(itemEntityWeapon.Blueprint)) { - __result = __result.Replace(LocalizedTexts.Instance.WeaponSubCategories.GetText(WeaponSubCategory.Finessable), ""); - __result = __result.Replace(", ,", ","); - char[] charsToTrim = { ',', ' ' }; - __result = __result.Trim(charsToTrim); - } - } - } - - [Harmony12.HarmonyPatch(typeof(UIUtilityItem), "FillArmorEnchantments")] - // ReSharper disable once UnusedMember.Local - private static class UIUtilityItemFillArmorEnchantmentsPatch { - // ReSharper disable once UnusedMember.Local - private static void Postfix(TooltipData data, ItemEntityShield armor) { - if (armor.IsIdentified) { - foreach (var itemEnchantment in armor.Enchantments) { - itemEnchantment.Blueprint.CallComponents(c => { - if (c.Descriptor != ModifierDescriptor.ArmorEnhancement && c.Descriptor != ModifierDescriptor.ShieldEnhancement && !data.StatBonus.ContainsKey(c.Stat)) { - data.StatBonus.Add(c.Stat, UIUtility.AddSign(c.Value)); - } - }); - var component = itemEnchantment.Blueprint.GetComponent(); - if (component != null) { - StatType[] saves = { StatType.SaveReflex, StatType.SaveWill, StatType.SaveFortitude }; - foreach (var save in saves) { - if (!data.StatBonus.ContainsKey(save)) { - data.StatBonus.Add(save, UIUtility.AddSign(component.Value)); - } - } - } - } - } - } - } - - [Harmony12.HarmonyPatch(typeof(UIUtilityItem), "FillEnchantmentDescription")] - // ReSharper disable once UnusedMember.Local - private static class UIUtilityItemFillEnchantmentDescriptionPatch { - // ReSharper disable once UnusedMember.Local - private static bool Prefix(ItemEntity item, TooltipData data, ref string __result) { - string text = string.Empty; - if (item is ItemEntityShield shield && shield.IsIdentified) { - // It appears that shields are not properly identified when found. - shield.ArmorComponent.Identify(); - shield.WeaponComponent?.Identify(); - return true; - } else if (item.Blueprint.ItemType == ItemsFilter.ItemType.Neck && ItemPlusEquivalent(item.Blueprint) > 0) { - if (item.IsIdentified) { - foreach (ItemEnchantment itemEnchantment in item.Enchantments) { - itemEnchantment.Blueprint.CallComponents(c => { - if (!data.StatBonus.ContainsKey(c.Stat)) { - data.StatBonus.Add(c.Stat, UIUtility.AddSign(c.Value)); - } - }); - if (!data.Texts.ContainsKey(TooltipElement.Qualities)) { - data.Texts[TooltipElement.Qualities] = Accessors.CallUIUtilityItemGetQualities(item); - } - if (!string.IsNullOrEmpty(itemEnchantment.Blueprint.Description)) { - text += string.Format("{0}\n", itemEnchantment.Blueprint.Name); - text = text + itemEnchantment.Blueprint.Description + "\n\n"; - } - } - if (item.Enchantments.Any() && !data.Texts.ContainsKey(TooltipElement.Qualities)) { - data.Texts[TooltipElement.Qualities] = GetEnhancementBonus(item); - } - if (GetItemEnhancementBonus(item) > 0) { - data.Texts[TooltipElement.Enhancement] = GetEnhancementBonus(item); - } - } - __result = text; - return false; - } else { - return true; - } - } - private static string GetEnhancementBonus(ItemEntity item) { - if (!item.IsIdentified) { - return string.Empty; - } - int itemEnhancementBonus = GetItemEnhancementBonus(item); - return (itemEnhancementBonus == 0) ? string.Empty : UIUtility.AddSign(itemEnhancementBonus); - } - public static int GetItemEnhancementBonus(ItemEntity item) { - return item.Enchantments.SelectMany((ItemEnchantment f) => f.SelectComponents()).Aggregate(0, (int s, EquipmentWeaponTypeEnhancement e) => s + e.Enhancement); - } - - private static void Postfix(ItemEntity item, TooltipData data, ref string __result) { - if (item is ItemEntityShield shield) { - if (shield.WeaponComponent != null) { - TooltipData tmp = new TooltipData(); - string result = Accessors.CallUIUtilityItemFillEnchantmentDescription(shield.WeaponComponent, tmp); - if (!string.IsNullOrEmpty(result)) { - __result += $"{ShieldBashLocalized}\n"; - __result += result; - } - data.Texts[TooltipElement.AttackType] = tmp.Texts[TooltipElement.AttackType]; - data.Texts[TooltipElement.ProficiencyGroup] = tmp.Texts[TooltipElement.ProficiencyGroup]; - if (tmp.Texts.ContainsKey(TooltipElement.Qualities) && !string.IsNullOrEmpty(tmp.Texts[TooltipElement.Qualities])) { - if (data.Texts.ContainsKey(TooltipElement.Qualities)) { - data.Texts[TooltipElement.Qualities] += $", {ShieldBashLocalized}: {tmp.Texts[TooltipElement.Qualities]}"; - } else { - data.Texts[TooltipElement.Qualities] = $"{ShieldBashLocalized}: {tmp.Texts[TooltipElement.Qualities]}"; - } - } - data.Texts[TooltipElement.Damage] = tmp.Texts[TooltipElement.Damage]; - if (tmp.Texts.ContainsKey(TooltipElement.EquipDamage)) { - data.Texts[TooltipElement.EquipDamage] = tmp.Texts[TooltipElement.EquipDamage]; - } - if (tmp.Texts.ContainsKey(TooltipElement.PhysicalDamage)) { - data.Texts[TooltipElement.PhysicalDamage] = tmp.Texts[TooltipElement.PhysicalDamage]; - data.PhysicalDamage = tmp.PhysicalDamage; - } - data.Energy = tmp.Energy; - data.OtherDamage = tmp.OtherDamage; - data.Texts[TooltipElement.Range] = tmp.Texts[TooltipElement.Range]; - data.Texts[TooltipElement.CriticalHit] = tmp.Texts[TooltipElement.CriticalHit]; - if (tmp.Texts.ContainsKey(TooltipElement.Enhancement)) { - data.Texts[TooltipElement.Enhancement] = tmp.Texts[TooltipElement.Enhancement]; - } - } - if (GameHelper.GetItemEnhancementBonus(shield.ArmorComponent) > 0) { - if (data.Texts.ContainsKey(TooltipElement.Damage)) { - data.Texts[Enum.GetValues(typeof(TooltipElement)).Cast().Max() + 1] = UIUtility.AddSign(GameHelper.GetItemEnhancementBonus(shield.ArmorComponent)); - } else { - data.Texts[TooltipElement.Enhancement] = UIUtility.AddSign(GameHelper.GetItemEnhancementBonus(shield.ArmorComponent)); - } - } - } - if (data.Texts.ContainsKey(TooltipElement.Qualities)) { - data.Texts[TooltipElement.Qualities] = data.Texts[TooltipElement.Qualities].Replace(" ,", ","); - } - } - } - - [Harmony12.HarmonyPatch(typeof(UIUtility), "IsMagicItem")] - // ReSharper disable once UnusedMember.Local - private static class UIUtilityIsMagicItem { - // ReSharper disable once UnusedMember.Local - private static void Postfix(ItemEntity item, ref bool __result) { - if (__result == false && item != null && item.IsIdentified && item is ItemEntityShield shield && shield.WeaponComponent != null) { - __result = ItemPlusEquivalent(shield.WeaponComponent.Blueprint) > 0; - } - } - } - - [Harmony12.HarmonyPatch(typeof(ItemEntity), "VendorDescription", Harmony12.MethodType.Getter)] - // ReSharper disable once UnusedMember.Local - private static class ItemEntityVendorDescriptionPatch { - // ReSharper disable once UnusedMember.Local - private static bool Prefix(ItemEntity __instance, ref string __result) { - // If the "vendor" is a party member, return that the item was crafted rather than from a merchant -#if PATCH21 - if (__instance.VendorBlueprint != null && __instance.VendorBlueprint.IsCompanion) { - foreach (var companion in UIUtility.GetGroup(true)) { - if (companion.Blueprint == __instance.VendorBlueprint) { - __result = L10NFormat("craftMagicItems-crafted-source-description", companion.CharacterName); - break; - } - } - return false; - } -#else - if (__instance.Vendor != null && __instance.Vendor.IsPlayerFaction) { - __result = L10NFormat("craftMagicItems-crafted-source-description", __instance.Vendor.CharacterName); - return false; - } -#endif - return true; - } - } - - // Owlcat's code doesn't filter out undamaged characters, so it will always return someone. This meant that with the "auto-cast healing" camping - // option enabled on, healers would burn all their spell slots healing undamaged characters when they started resting, leaving them no spells to cast - // when crafting. Change it so it returns null if the most damaged character is undamaged. - [Harmony12.HarmonyPatch(typeof(UnitUseSpellsOnRest), "GetUnitWithMaxDamage")] - // ReSharper disable once UnusedMember.Local - private static class UnitUseSpellsOnRestGetUnitWithMaxDamagePatch { - // ReSharper disable once UnusedMember.Local - private static void Postfix(ref UnitEntityData __result) { - if (__result.Damage == 0 && (UnitPartDualCompanion.GetPair(__result)?.Damage ?? 0) == 0) { - __result = null; - } - } - } - - private static bool EquipmentEnchantmentValid(ItemEntityWeapon weapon, ItemEntity owner) { - if ((weapon == owner) || - (weapon != null && (weapon.Blueprint.IsNatural || weapon.Blueprint.IsUnarmed))) { - return true; - } else { - return false; - } - } - - [Harmony12.HarmonyPatch(typeof(WeaponEnergyDamageDice), "OnEventAboutToTrigger")] - // ReSharper disable once UnusedMember.Local - private static class WeaponEnergyDamageDiceOnEventAboutToTriggerPatch { - // ReSharper disable once UnusedMember.Local - private static bool Prefix(WeaponEnergyDamageDice __instance, RuleCalculateWeaponStats evt) { - if (__instance is ItemEnchantmentLogic logic) { - if (EquipmentEnchantmentValid(evt.Weapon, logic.Owner)) { - DamageDescription item = new DamageDescription - { - TypeDescription = new DamageTypeDescription - { - Type = DamageType.Energy, - Energy = __instance.Element - }, - Dice = __instance.EnergyDamageDice - }; - evt.DamageDescription.Add(item); - } - return false; - } else { - return true; - } - } - } - - [Harmony12.HarmonyPatch(typeof(WeaponEnergyBurst), "OnEventAboutToTrigger")] - // ReSharper disable once UnusedMember.Local - private static class WeaponEnergyBurstOnEventAboutToTriggerPatch { - // ReSharper disable once UnusedMember.Local - private static bool Prefix(WeaponEnergyBurst __instance, RuleDealDamage evt) { - if (__instance is ItemEnchantmentLogic logic) { - if (logic.Owner == null || evt.AttackRoll == null || !evt.AttackRoll.IsCriticalConfirmed || evt.AttackRoll.FortificationNegatesCriticalHit || evt.DamageBundle.WeaponDamage == null) { - return false; - } - if (EquipmentEnchantmentValid(evt.DamageBundle.Weapon, logic.Owner)) { - RuleCalculateWeaponStats ruleCalculateWeaponStats = Rulebook.Trigger(new RuleCalculateWeaponStats(Game.Instance.DefaultUnit, evt.DamageBundle.Weapon, null)); - DiceFormula dice = new DiceFormula(Math.Max(ruleCalculateWeaponStats.CriticalMultiplier - 1, 1), __instance.Dice); - evt.DamageBundle.Add(new EnergyDamage(dice, __instance.Element)); - } - return false; - } else { - return true; - } - } - } - - [Harmony12.HarmonyPatch(typeof(WeaponExtraAttack), "OnEventAboutToTrigger")] - // ReSharper disable once UnusedMember.Local - private static class WeaponExtraAttackOnEventAboutToTriggerPatch { - // ReSharper disable once UnusedMember.Local - private static bool Prefix(WeaponExtraAttack __instance, RuleCalculateAttacksCount evt) { - if (__instance is ItemEnchantmentLogic logic) { - if (logic.Owner is ItemEntityWeapon) { - evt.AddExtraAttacks(__instance.Number, __instance.Haste, __instance.Owner); - } else if (evt.Initiator.GetFirstWeapon() != null && - (evt.Initiator.GetFirstWeapon().Blueprint.IsNatural || evt.Initiator.GetFirstWeapon().Blueprint.IsUnarmed)) { - evt.AddExtraAttacks(__instance.Number, __instance.Haste); - } - return false; - } else { - return true; - } - } - } - - [Harmony12.HarmonyPatch(typeof(WeaponDamageAgainstAlignment), "OnEventAboutToTrigger")] - // ReSharper disable once UnusedMember.Local - private static class WeaponDamageAgainstAlignmentOnEventAboutToTriggerPatch { - // ReSharper disable once UnusedMember.Local - private static bool Prefix(WeaponDamageAgainstAlignment __instance, RulePrepareDamage evt) { - if (__instance is ItemEnchantmentLogic logic) { - if (evt.DamageBundle.WeaponDamage == null) { - return false; - } - evt.DamageBundle.WeaponDamage.AddAlignment(__instance.WeaponAlignment); - - if (evt.Target.Descriptor.Alignment.Value.HasComponent(__instance.EnemyAlignment) && EquipmentEnchantmentValid(evt.DamageBundle.Weapon, logic.Owner)) { - int rollsCount = __instance.Value.DiceCountValue.Calculate(logic.Context); - int bonusDamage = __instance.Value.BonusValue.Calculate(logic.Context); - EnergyDamage energyDamage = new EnergyDamage(new DiceFormula(rollsCount, __instance.Value.DiceType), __instance.DamageType); - energyDamage.AddBonusTargetRelated(bonusDamage); - evt.DamageBundle.Add(energyDamage); - } - return false; - } else { - return true; - } - } - } - - [Harmony12.HarmonyPatch(typeof(WeaponConditionalEnhancementBonus), "OnEventAboutToTrigger", new Type[] { typeof(RuleCalculateWeaponStats) })] - // ReSharper disable once UnusedMember.Local - private static class WeaponConditionalEnhancementBonusOnEventAboutToTriggerRuleCalculateWeaponStatsPatch { - // ReSharper disable once UnusedMember.Local - private static bool Prefix(WeaponConditionalEnhancementBonus __instance, RuleCalculateWeaponStats evt) { - if (__instance is ItemEnchantmentLogic logic) { - if (__instance.IsBane) { - if (logic.Owner.Enchantments.Any((ItemEnchantment e) => e.Get())) { - return false; - } - } - if (__instance.CheckWielder) { - using (logic.Enchantment.Context.GetDataScope(evt.Initiator)) { - if (EquipmentEnchantmentValid(evt.Weapon, logic.Owner) && __instance.Conditions.Check(null)) { - evt.AddBonusDamage(__instance.EnhancementBonus); - evt.Enhancement += __instance.EnhancementBonus; - evt.EnhancementTotal += __instance.EnhancementBonus; - } - } - } else if (evt.AttackWithWeapon != null) { - using (logic.Enchantment.Context.GetDataScope(evt.AttackWithWeapon.Target)) { - if (EquipmentEnchantmentValid(evt.Weapon, logic.Owner) && __instance.Conditions.Check(null)) { - evt.AddBonusDamage(__instance.EnhancementBonus); - evt.Enhancement += __instance.EnhancementBonus; - evt.EnhancementTotal += __instance.EnhancementBonus; - } - } - } - return false; - } else { - return true; - } - } - } - - [Harmony12.HarmonyPatch(typeof(WeaponConditionalEnhancementBonus), "OnEventAboutToTrigger", new Type[] { typeof(RuleCalculateAttackBonus) })] - // ReSharper disable once UnusedMember.Local - private static class WeaponConditionalEnhancementBonusOnEventAboutToTriggerRuleCalculateAttackBonusPatch { - // ReSharper disable once UnusedMember.Local - private static bool Prefix(WeaponConditionalEnhancementBonus __instance, RuleCalculateAttackBonus evt) { - if (__instance is ItemEnchantmentLogic logic) { - if (__instance.IsBane) { - if (logic.Owner.Enchantments.Any((ItemEnchantment e) => e.Get())) { - return false; - } - } - if (__instance.CheckWielder) { - using (logic.Enchantment.Context.GetDataScope(evt.Initiator)) { - if (EquipmentEnchantmentValid(evt.Weapon, logic.Owner) && __instance.Conditions.Check(null)) { - evt.AddBonus(__instance.EnhancementBonus, logic.Fact); - } - } - } else { - using (logic.Enchantment.Context.GetDataScope(evt.Target)) { - if (EquipmentEnchantmentValid(evt.Weapon, logic.Owner) && __instance.Conditions.Check(null)) { - evt.AddBonus(__instance.EnhancementBonus, logic.Fact); - } - } - } - return false; - } else { - return true; - } - } - } - - [Harmony12.HarmonyPatch(typeof(WeaponConditionalDamageDice), "OnEventAboutToTrigger")] - // ReSharper disable once UnusedMember.Local - private static class WeaponConditionalDamageDiceOnEventAboutToTriggerPatch { - // ReSharper disable once UnusedMember.Local - private static bool Prefix(WeaponConditionalDamageDice __instance, RulePrepareDamage evt) { - if (__instance is ItemEnchantmentLogic logic) { - if (evt.DamageBundle.WeaponDamage == null) { - return false; - } - if (__instance.IsBane) { - if (logic.Owner.Enchantments.Any((ItemEnchantment e) => e.Get())) { - return false; - } - } - if (__instance.CheckWielder) { - using (logic.Enchantment.Context.GetDataScope(logic.Owner.Wielder.Unit)) { - if (EquipmentEnchantmentValid(evt.DamageBundle.Weapon, logic.Owner) && __instance.Conditions.Check(null)) { - BaseDamage damage = __instance.Damage.CreateDamage(); - evt.DamageBundle.Add(damage); - } - } - } else { - using (logic.Enchantment.Context.GetDataScope(evt.Target)) { - if (EquipmentEnchantmentValid(evt.DamageBundle.Weapon, logic.Owner) && __instance.Conditions.Check(null)) { - BaseDamage damage2 = __instance.Damage.CreateDamage(); - evt.DamageBundle.Add(damage2); - } - } - } - return false; - } else { - return true; - } - } - } - - [Harmony12.HarmonyPatch(typeof(BrilliantEnergy), "OnEventAboutToTrigger")] - // ReSharper disable once UnusedMember.Local - private static class BrilliantEnergyOnEventAboutToTriggerPatch { - // ReSharper disable once UnusedMember.Local - private static bool Prefix(BrilliantEnergy __instance, RuleCalculateAC evt) { - if (__instance is ItemEnchantmentLogic logic) { - if (evt.Reason.Item is ItemEntityWeapon weapon && EquipmentEnchantmentValid(weapon, logic.Owner)) { - evt.BrilliantEnergy = logic.Fact; - } - return false; - } else { - return true; - } - } - } - - [Harmony12.HarmonyPatch(typeof(MissAgainstFactOwner), "OnEventAboutToTrigger")] - // ReSharper disable once UnusedMember.Local - private static class MissAgainstFactOwnerOnEventAboutToTriggerPatch { - // ReSharper disable once UnusedMember.Local - private static bool Prefix(MissAgainstFactOwner __instance, RuleAttackRoll evt) { - if (__instance is ItemEnchantmentLogic logic) { - if (EquipmentEnchantmentValid(evt.Weapon, logic.Owner)) { - foreach (BlueprintUnitFact blueprint in __instance.Facts) { - if (evt.Target.Descriptor.HasFact(blueprint)) { - evt.AutoMiss = true; - return false; - } - } - } - return false; - } else { - return true; - } - } - } - - [Harmony12.HarmonyPatch(typeof(WeaponReality), "OnEventAboutToTrigger")] - // ReSharper disable once UnusedMember.Local - private static class WeaponRealityOnEventAboutToTriggerPatch { - // ReSharper disable once UnusedMember.Local - private static bool Prefix(WeaponReality __instance, RulePrepareDamage evt) { - if (__instance is ItemEnchantmentLogic logic) { - if (evt.DamageBundle.WeaponDamage == null) { - return false; - } - if (EquipmentEnchantmentValid(evt.DamageBundle.Weapon, logic.Owner)) { - evt.DamageBundle.WeaponDamage.Reality |= __instance.Reality; - } - return false; - } else { - return true; - } - } - } - - [Harmony12.HarmonyPatch(typeof(AddInitiatorAttackRollTrigger), "CheckConditions")] - // ReSharper disable once UnusedMember.Local - private static class AddInitiatorAttackRollTriggerCheckConditionsPatch { - // ReSharper disable once UnusedMember.Local - private static bool Prefix(AddInitiatorAttackRollTrigger __instance, RuleAttackRoll evt, ref bool __result) { - if (__instance is GameLogicComponent logic) { - ItemEnchantment itemEnchantment = logic.Fact as ItemEnchantment; - ItemEntity itemEntity = (itemEnchantment != null) ? itemEnchantment.Owner : null; - RuleAttackWithWeapon ruleAttackWithWeapon = evt.Reason.Rule as RuleAttackWithWeapon; - ItemEntityWeapon itemEntityWeapon = (ruleAttackWithWeapon != null) ? ruleAttackWithWeapon.Weapon : null; - __result = (itemEntity == null || itemEntity == itemEntityWeapon || evt.Weapon.Blueprint.IsNatural || evt.Weapon.Blueprint.IsUnarmed) && - (!__instance.CheckWeapon || (itemEntityWeapon != null && __instance.WeaponCategory == itemEntityWeapon.Blueprint.Category)) && - (!__instance.OnlyHit || evt.IsHit) && (!__instance.CriticalHit || (evt.IsCriticalConfirmed && !evt.FortificationNegatesCriticalHit)) && - (!__instance.SneakAttack || (evt.IsSneakAttack && !evt.FortificationNegatesSneakAttack)) && - (__instance.AffectFriendlyTouchSpells || evt.Initiator.IsEnemy(evt.Target) || evt.Weapon.Blueprint.Type.AttackType != AttackType.Touch); - return false; - } else { - return true; - } - } - } - -#if !PATCH21 - [Harmony12.HarmonyPatch(typeof(RuleCalculateAttacksCount), "OnTrigger")] - private static class RuleCalculateAttacksCountOnTriggerPatch { - private static void Postfix(RuleCalculateAttacksCount __instance) { - int num = __instance.Initiator.Stats.BaseAttackBonus; - int val = Math.Min(Math.Max(0, num / 5 - ((num % 5 != 0) ? 0 : 1)), 3); - HandSlot primaryHand = __instance.Initiator.Body.PrimaryHand; - HandSlot secondaryHand = __instance.Initiator.Body.SecondaryHand; - ItemEntityWeapon maybeWeapon = primaryHand.MaybeWeapon; - BlueprintItemWeapon blueprintItemWeapon = (maybeWeapon != null) ? maybeWeapon.Blueprint : null; - BlueprintItemWeapon blueprintItemWeapon2; - if (secondaryHand.MaybeShield != null) { - if (__instance.Initiator.Descriptor.State.Features.ShieldBash) { - ItemEntityWeapon weaponComponent = secondaryHand.MaybeShield.WeaponComponent; - blueprintItemWeapon2 = ((weaponComponent != null) ? weaponComponent.Blueprint : null); - } else { - blueprintItemWeapon2 = null; - } - } else { - ItemEntityWeapon maybeWeapon2 = secondaryHand.MaybeWeapon; - blueprintItemWeapon2 = ((maybeWeapon2 != null) ? maybeWeapon2.Blueprint : null); - } - if ((primaryHand.MaybeWeapon == null || !primaryHand.MaybeWeapon.HoldInTwoHands) && (blueprintItemWeapon == null || blueprintItemWeapon.IsUnarmed) && blueprintItemWeapon2 && !blueprintItemWeapon2.IsUnarmed) { - __instance.SecondaryHand.PenalizedAttacks += Math.Max(0, val); - } - } - } - - [Harmony12.HarmonyPatch(typeof(ItemEntityWeapon), "HoldInTwoHands", Harmony12.MethodType.Getter)] - private static class ItemEntityWeaponHoldInTwoHandsPatch { - private static void Postfix(ItemEntityWeapon __instance, ref bool __result) { - if (!__result) { - if (__instance.IsShield && __instance.Blueprint.IsOneHandedWhichCanBeUsedWithTwoHands && __instance.Wielder != null) { - HandSlot handSlot = __instance.Wielder.Body.PrimaryHand; - __result = handSlot != null && !handSlot.HasItem; - } - } - } - } -#endif - - [Harmony12.HarmonyPatch(typeof(DescriptionTemplatesItem), "ItemEnergy")] - private static class DescriptionTemplatesItemItemEnergyPatch { - private static void Postfix(TooltipData data, bool __result) { - if (__result) { - if (data.Energy.Count > 0) { - data.Energy.Clear(); - } - } - } - } - - [Harmony12.HarmonyPatch(typeof(DescriptionTemplatesItem), "ItemEnhancement")] - private static class DescriptionTemplatesItemItemEnhancementPatch { - private static void Postfix(TooltipData data) { - if (data.Texts.ContainsKey(Enum.GetValues(typeof(TooltipElement)).Cast().Max() + 1)) { - data.Texts[TooltipElement.Enhancement] = data.Texts[Enum.GetValues(typeof(TooltipElement)).Cast().Max() + 1]; - } else if (data.Texts.ContainsKey(TooltipElement.Enhancement)) { - data.Texts.Remove(TooltipElement.Enhancement); - } - } - } - - [Harmony12.HarmonyPatch(typeof(DescriptionTemplatesItem), "ItemEnergyResisit")] - private static class DescriptionTemplatesItemItemEnergyResisitPatch { - private static bool Prefix(ref bool __result) { - __result = false; - return false; - } - } - - [Harmony12.HarmonyPatch(typeof(UnitViewHandSlotData), "OwnerWeaponScale", Harmony12.MethodType.Getter)] - private static class UnitViewHandSlotDataWeaponScalePatch { - private static void Postfix(UnitViewHandSlotData __instance, ref float __result) { - if (__instance.VisibleItem is ItemEntityWeapon weapon && !weapon.Blueprint.AssetGuid.Contains(",visual=")) { - var enchantment = GetEnchantments(weapon.Blueprint).FirstOrDefault(e => e.AssetGuid.StartsWith(OversizedGuid)); - if (enchantment != null) { - var component = enchantment.GetComponent(); - if (component != null) { - if (component.SizeCategoryChange > 0) { - __result *= 4.0f / 3.0f; - } else if (component.SizeCategoryChange < 0) { - __result *= 0.75f; - } - } - } - } - } - } - -#if !PATCH21 - [Harmony12.HarmonyPatch(typeof(ActivatableAbility), "OnEventDidTrigger", new Type[] { typeof(RuleAttackWithWeaponResolve) })] - private static class ActivatableAbilityOnEventDidTriggerRuleAttackWithWeaponResolvePatch { - private static bool Prefix(ActivatableAbility __instance, RuleAttackWithWeaponResolve evt) { - if (evt.Damage != null && evt.AttackRoll.IsHit) { - return false; - } else { - return true; - } - } - } -#endif } } \ No newline at end of file diff --git a/CraftMagicItems/CraftMagicItemsBlueprintPatcher.cs b/CraftMagicItems/Patches/CraftMagicItemsBlueprintPatcher.cs similarity index 72% rename from CraftMagicItems/CraftMagicItemsBlueprintPatcher.cs rename to CraftMagicItems/Patches/CraftMagicItemsBlueprintPatcher.cs index e0c1843..29bc6df 100644 --- a/CraftMagicItems/CraftMagicItemsBlueprintPatcher.cs +++ b/CraftMagicItems/Patches/CraftMagicItemsBlueprintPatcher.cs @@ -3,6 +3,9 @@ using System.Linq; using System.Reflection; using System.Text.RegularExpressions; +using CraftMagicItems.Constants; +using CraftMagicItems.Localization; +using CraftMagicItems.Patches.Harmony; using Kingmaker.Blueprints; using Kingmaker.Blueprints.Classes; using Kingmaker.Blueprints.Items; @@ -18,12 +21,13 @@ using Kingmaker.Localization; using Kingmaker.ResourceLinks; using Kingmaker.UI.Common; +using Kingmaker.UI.Log; using Kingmaker.UnitLogic.Abilities.Blueprints; using Kingmaker.UnitLogic.ActivatableAbilities; using Kingmaker.UnitLogic.Buffs.Blueprints; +using Kingmaker.Utility; using Kingmaker.View.Animation; using Kingmaker.Utility; -using Kingmaker.UnitLogic.Abilities.Components; #if PATCH21_BETA using Kingmaker.Blueprints.DirectSerialization; #else @@ -31,8 +35,10 @@ using Object = UnityEngine.Object; #endif -namespace CraftMagicItems { - public class CraftMagicItemsBlueprintPatcher { +namespace CraftMagicItems.Patches +{ + public class CraftMagicItemsBlueprintPatcher + { public const string TimerBlueprintGuid = "52e4be2ba79c8c94d907bdbaf23ec15f#CraftMagicItems(timer)"; public const string BondedItemBuffBlueprintGuid = "1efa689e594ca82428b8fff1a739c9be#CraftMagicItems(bondedItem)"; @@ -75,7 +81,8 @@ public class CraftMagicItemsBlueprintPatcher { private readonly CraftMagicItemsAccessors accessors; - public CraftMagicItemsBlueprintPatcher(CraftMagicItemsAccessors accessors, bool modEnabled) { + public CraftMagicItemsBlueprintPatcher(CraftMagicItemsAccessors accessors, bool modEnabled) + { this.accessors = accessors; CustomBlueprintBuilder.Initialise(ApplyBlueprintPatch, modEnabled, new CustomBlueprintBuilder.Substitution { @@ -152,18 +159,22 @@ public CraftMagicItemsBlueprintPatcher(CraftMagicItemsAccessors accessors, bool }); } - public string BuildCustomSpellItemGuid(string originalGuid, int casterLevel, int spellLevel = -1, string spellId = null) { + public string BuildCustomSpellItemGuid(string originalGuid, int casterLevel, int spellLevel = -1, string spellId = null) + { // Check if GUID is already customised by this mod var match = BlueprintRegex.Match(originalGuid); - if (match.Success && match.Groups["casterLevel"].Success) { + if (match.Success && match.Groups["casterLevel"].Success) + { // Remove the existing customisation originalGuid = CustomBlueprintBuilder.AssetGuidWithoutMatch(originalGuid, match); // Use any values which aren't explicitly overridden - if (spellLevel == -1 && match.Groups["spellLevelMatch"].Success) { + if (spellLevel == -1 && match.Groups["spellLevelMatch"].Success) + { spellLevel = int.Parse(match.Groups["spellLevel"].Value); } - if (spellId == null && match.Groups["spellIdMatch"].Success) { + if (spellId == null && match.Groups["spellIdMatch"].Success) + { spellId = match.Groups["spellId"].Value; } } @@ -177,18 +188,23 @@ public string BuildCustomSpellItemGuid(string originalGuid, int casterLevel, int public string BuildCustomRecipeItemGuid(string originalGuid, IEnumerable enchantments, string[] remove = null, string name = null, string ability = null, string activatableAbility = null, int charges = -1, int weight = -1, PhysicalDamageMaterial material = 0, string visual = null, string animation = null, int casterLevel = -1, int spellLevel = -1, int perDay = -1, string nameId = null, string descriptionId = null, string secondEndGuid = null, - int priceAdjust = 0) { + int priceAdjust = 0) + { // Check if GUID is already customised by this mod var match = BlueprintRegex.Match(originalGuid); - if (match.Success && match.Groups["enchantments"].Success) { + if (match.Success && match.Groups["enchantments"].Success) + { var enchantmentsList = enchantments.Concat(match.Groups["enchantments"].Value.Split(';')) .Where(guid => guid.Length > 0).Distinct().ToList(); var removeList = match.Groups["remove"].Success ? (remove ?? Enumerable.Empty()).Concat(match.Groups["remove"].Value.Split(';')).Distinct().ToList() : remove?.ToList(); - if (removeList != null) { - foreach (var guid in removeList.ToArray()) { - if (enchantmentsList.Contains(guid)) { + if (removeList != null) + { + foreach (var guid in removeList.ToArray()) + { + if (enchantmentsList.Contains(guid)) + { enchantmentsList.Remove(guid); removeList.Remove(guid); } @@ -197,64 +213,79 @@ public string BuildCustomRecipeItemGuid(string originalGuid, IEnumerable enchantments = enchantmentsList; remove = removeList?.Count > 0 ? removeList.ToArray() : null; - if (name == null && match.Groups["name"].Success) { + if (name == null && match.Groups["name"].Success) + { name = match.Groups["name"].Value; nameId = null; } - if (ability == null && match.Groups["ability"].Success) { + if (ability == null && match.Groups["ability"].Success) + { ability = match.Groups["ability"].Value; } - if (activatableAbility == null && match.Groups["activatableAbility"].Success) { + if (activatableAbility == null && match.Groups["activatableAbility"].Success) + { activatableAbility = match.Groups["activatableAbility"].Value; } - if (charges == -1 && match.Groups["charges"].Success) { + if (charges == -1 && match.Groups["charges"].Success) + { perDay = int.Parse(match.Groups["charges"].Value); } - if (weight == -1 && match.Groups["weight"].Success) { + if (weight == -1 && match.Groups["weight"].Success) + { weight = int.Parse(match.Groups["weight"].Value); } - if (material == 0 && match.Groups["material"].Success) { + if (material == 0 && match.Groups["material"].Success) + { Enum.TryParse(match.Groups["material"].Value, out material); } - if (visual == null && match.Groups["visual"].Success) { + if (visual == null && match.Groups["visual"].Success) + { visual = match.Groups["visual"].Value; } - if (animation == null && match.Groups["animation"].Success) { + if (animation == null && match.Groups["animation"].Success) + { animation = match.Groups["animation"].Value; } - if (priceAdjust == 0 && match.Groups["priceAdjust"].Success) { + if (priceAdjust == 0 && match.Groups["priceAdjust"].Success) + { priceAdjust = int.Parse(match.Groups["priceAdjust"].Value); } - if (match.Groups["casterLevel"].Success) { + if (match.Groups["casterLevel"].Success) + { casterLevel = Math.Max(casterLevel, int.Parse(match.Groups["casterLevel"].Value)); } - if (match.Groups["spellLevel"].Success) { + if (match.Groups["spellLevel"].Success) + { spellLevel = Math.Max(spellLevel, int.Parse(match.Groups["spellLevel"].Value)); } - if (perDay == -1 && match.Groups["perDay"].Success) { + if (perDay == -1 && match.Groups["perDay"].Success) + { perDay = int.Parse(match.Groups["perDay"].Value); } - if (name == null && nameId == null && match.Groups["nameId"].Success) { + if (name == null && nameId == null && match.Groups["nameId"].Success) + { nameId = match.Groups["nameId"].Value; } - if (descriptionId == null && match.Groups["descriptionId"].Success) { + if (descriptionId == null && match.Groups["descriptionId"].Success) + { descriptionId = match.Groups["descriptionId"].Value; } - if (secondEndGuid == null && match.Groups["secondEnd"].Success) { + if (secondEndGuid == null && match.Groups["secondEnd"].Success) + { secondEndGuid = match.Groups["secondEnd"].Value; } @@ -282,9 +313,11 @@ public string BuildCustomRecipeItemGuid(string originalGuid, IEnumerable ")"; } - private string BuildCustomComponentsItemGuid(string originalGuid, string[] values, string nameId, string descriptionId) { + private string BuildCustomComponentsItemGuid(string originalGuid, string[] values, string nameId, string descriptionId) + { var components = ""; - for (var index = 0; index < values.Length; index += 3) { + for (var index = 0; index < values.Length; index += 3) + { components += $"{(index > 0 ? "," : "")}Component[{values[index]}]{values[index + 1]}={values[index + 2]}"; } @@ -292,20 +325,23 @@ private string BuildCustomComponentsItemGuid(string originalGuid, string[] value $"{originalGuid}{BlueprintPrefix}({components}{(nameId == null ? "" : $",nameId={nameId}")}{(descriptionId == null ? "" : $",descriptionId={descriptionId}")})"; } - private string BuildCustomFeatGuid(string originalGuid, string feat) { + private string BuildCustomFeatGuid(string originalGuid, string feat) + { return $"{originalGuid}{BlueprintPrefix}(feat={feat})"; } - private void ApplyBuffBlueprintPatch(BlueprintBuff blueprint, BlueprintComponent component, string nameId) { - blueprint.ComponentsArray = new[] {component}; + private void ApplyBuffBlueprintPatch(BlueprintBuff blueprint, BlueprintComponent component, string nameId) + { + blueprint.ComponentsArray = new[] { component }; accessors.SetBlueprintBuffFlags(blueprint, 2 + 8); // BlueprintBluff.Flags enum is private. Values are HiddenInUi = 2 + StayOnDeath = 8 blueprint.FxOnStart = new PrefabLink(); blueprint.FxOnRemove = new PrefabLink(); // Set the display name - it's hidden in the UI, but someone might find it in Bag of Tricks. - accessors.SetBlueprintUnitFactDisplayName(blueprint, new L10NString(nameId)); + accessors.SetBlueprintUnitFactDisplayName(blueprint) = new L10NString(nameId); } - private string ApplyTimerBlueprintPatch(BlueprintBuff blueprint) { + private string ApplyTimerBlueprintPatch(BlueprintBuff blueprint) + { #if PATCH21_BETA ApplyBuffBlueprintPatch(blueprint, SerializedScriptableObject.CreateInstance(), "craftMagicItems-timer-buff-name"); #else @@ -315,7 +351,8 @@ private string ApplyTimerBlueprintPatch(BlueprintBuff blueprint) { return TimerBlueprintGuid; } - private string ApplyBondedItemBlueprintPatch(BlueprintBuff blueprint) { + private string ApplyBondedItemBlueprintPatch(BlueprintBuff blueprint) + { #if PATCH21_BETA ApplyBuffBlueprintPatch(blueprint, SerializedScriptableObject.CreateInstance(), "craftMagicItems-bondedItem-buff-name"); #else @@ -324,74 +361,85 @@ private string ApplyBondedItemBlueprintPatch(BlueprintBuff blueprint) { return BondedItemBuffBlueprintGuid; } - private string ApplyFeatBlueprintPatch(BlueprintFeature blueprint, Match match) { + private string ApplyFeatBlueprintPatch(BlueprintFeature blueprint, Match match) + { var feat = match.Groups["feat"].Value; - accessors.SetBlueprintUnitFactDisplayName(blueprint, new L10NString($"craftMagicItems-feat-{feat}-displayName")); - accessors.SetBlueprintUnitFactDescription(blueprint, new L10NString($"craftMagicItems-feat-{feat}-description")); + accessors.SetBlueprintUnitFactDisplayName(blueprint) = new L10NString($"craftMagicItems-feat-{feat}-displayName"); + accessors.SetBlueprintUnitFactDescription(blueprint) = new L10NString($"craftMagicItems-feat-{feat}-description"); var icon = Image2Sprite.Create($"{Main.ModEntry.Path}/Icons/craft-{feat}.png"); - accessors.SetBlueprintUnitFactIcon(blueprint, icon); + accessors.SetBlueprintUnitFactIcon(blueprint) = icon; #if PATCH21_BETA var prerequisite = SerializedScriptableObject.CreateInstance(); #else var prerequisite = ScriptableObject.CreateInstance(); #endif var featGuid = BuildCustomFeatGuid(blueprint.AssetGuid, feat); - var itemData = Main.ItemCraftingData.First(data => data.FeatGuid == featGuid); + var itemData = Main.LoadedData.ItemCraftingData.First(data => data.FeatGuid == featGuid); prerequisite.SetPrerequisiteCasterLevel(itemData.MinimumCasterLevel); - blueprint.ComponentsArray = new BlueprintComponent[] {prerequisite}; + blueprint.ComponentsArray = new BlueprintComponent[] { prerequisite }; return featGuid; } - private string ApplySpellItemBlueprintPatch(BlueprintItemEquipmentUsable blueprint, Match match) { + private string ApplySpellItemBlueprintPatch(BlueprintItemEquipmentUsable blueprint, Match match) + { var casterLevel = int.Parse(match.Groups["casterLevel"].Value); blueprint.CasterLevel = casterLevel; var spellLevel = -1; - if (match.Groups["spellLevelMatch"].Success) { + if (match.Groups["spellLevelMatch"].Success) + { spellLevel = int.Parse(match.Groups["spellLevel"].Value); blueprint.SpellLevel = spellLevel; } string spellId = null; - if (match.Groups["spellIdMatch"].Success) { + if (match.Groups["spellIdMatch"].Success) + { spellId = match.Groups["spellId"].Value; - blueprint.Ability = (BlueprintAbility) ResourcesLibrary.TryGetBlueprint(spellId); + blueprint.Ability = (BlueprintAbility)ResourcesLibrary.TryGetBlueprint(spellId); blueprint.DC = 0; } - if (blueprint.Ability != null && blueprint.Ability.LocalizedSavingThrow != null && blueprint.Ability.LocalizedSavingThrow.IsSet()) { + if (blueprint.Ability != null && blueprint.Ability.LocalizedSavingThrow != null && blueprint.Ability.LocalizedSavingThrow.IsSet()) + { blueprint.DC = 10 + blueprint.SpellLevel * 3 / 2; } - accessors.SetBlueprintItemEquipmentUsableCost(blueprint, 0); // Allow the game to auto-calculate the cost + accessors.SetBlueprintItemCost(blueprint) = 0; // Allow the game to auto-calculate the cost // Also store the new item blueprint in our spell-to-item lookup dictionary. var itemBlueprintsForSpell = Main.FindItemBlueprintsForSpell(blueprint.Ability, blueprint.Type); - if (itemBlueprintsForSpell == null || !itemBlueprintsForSpell.Contains(blueprint)) { + if (itemBlueprintsForSpell == null || !itemBlueprintsForSpell.Contains(blueprint)) + { Main.AddItemBlueprintForSpell(blueprint.Type, blueprint); } return BuildCustomSpellItemGuid(blueprint.AssetGuid, casterLevel, spellLevel, spellId); } - public static bool DoesBlueprintShowEnchantments(BlueprintItem blueprint) { - if (blueprint.ItemType == ItemsFilter.ItemType.Neck && Main.ItemPlusEquivalent(blueprint) > 0) { + public static bool DoesBlueprintShowEnchantments(BlueprintItem blueprint) + { + if (blueprint.ItemType == ItemsFilter.ItemType.Neck && Main.ItemPlusEquivalent(blueprint) > 0) + { return true; } return SlotsWhichShowEnchantments.Contains(blueprint.ItemType); } - private string ApplyRecipeItemBlueprintPatch(BlueprintItemEquipment blueprint, Match match) { + private string ApplyRecipeItemBlueprintPatch(BlueprintItemEquipment blueprint, Match match) + { var priceDelta = blueprint.Cost - Main.RulesRecipeItemCost(blueprint); string secondEndGuid = null; var removedIds = new List(); - if (match.Groups["remove"].Success) { + if (match.Groups["remove"].Success) + { removedIds = match.Groups["remove"].Value.Split(';').ToList(); } var enchantmentsValue = match.Groups["enchantments"].Value; var enchantmentIds = enchantmentsValue.Split(';').ToList(); - if (blueprint is BlueprintItemShield shield) { + if (blueprint is BlueprintItemShield shield) + { #if PATCH21_BETA var armorComponentClone = (BlueprintItemArmor)SerializedScriptableObject.Instantiate(shield.ArmorComponent); armorComponentClone.AssetGuid = shield.ArmorComponent.AssetGuid; @@ -400,15 +448,21 @@ private string ApplyRecipeItemBlueprintPatch(BlueprintItemEquipment blueprint, M var armorComponentClone = Object.Instantiate(shield.ArmorComponent); #endif ApplyRecipeItemBlueprintPatch(armorComponentClone, match); - if (match.Groups["secondEnd"].Success) { + if (match.Groups["secondEnd"].Success) + { secondEndGuid = match.Groups["secondEnd"].Value; - } else if (shield.WeaponComponent != null) { + } + else if (shield.WeaponComponent != null) + { var weaponEnchantmentIds = enchantmentIds; - if (weaponEnchantmentIds.Count > 0) { + if (weaponEnchantmentIds.Count > 0) + { enchantmentIds = new List(); var armorEnchantments = armorComponentClone.Enchantments; - foreach (var enchantment in armorEnchantments) { - if (weaponEnchantmentIds.Contains(enchantment.AssetGuid)) { + foreach (var enchantment in armorEnchantments) + { + if (weaponEnchantmentIds.Contains(enchantment.AssetGuid)) + { weaponEnchantmentIds.Remove(enchantment.AssetGuid); enchantmentIds.Add(enchantment.AssetGuid); } @@ -416,37 +470,47 @@ private string ApplyRecipeItemBlueprintPatch(BlueprintItemEquipment blueprint, M } var weaponRemovedIds = removedIds; - if (weaponRemovedIds.Count > 0) { + if (weaponRemovedIds.Count > 0) + { removedIds = new List(); var armorEnchantments = shield.ArmorComponent.Enchantments; - foreach (var enchantment in armorEnchantments) { - if (weaponRemovedIds.Contains(enchantment.AssetGuid)) { + foreach (var enchantment in armorEnchantments) + { + if (weaponRemovedIds.Contains(enchantment.AssetGuid)) + { weaponRemovedIds.Remove(enchantment.AssetGuid); removedIds.Add(enchantment.AssetGuid); } } } - if (weaponEnchantmentIds.Count > 0 || weaponRemovedIds.Count > 0) { + if (weaponEnchantmentIds.Count > 0 || weaponRemovedIds.Count > 0) + { PhysicalDamageMaterial weaponMaterial = 0; - if (match.Groups["material"].Success) { + if (match.Groups["material"].Success) + { Enum.TryParse(match.Groups["material"].Value, out weaponMaterial); } secondEndGuid = BuildCustomRecipeItemGuid(shield.WeaponComponent.AssetGuid, weaponEnchantmentIds, weaponRemovedIds.Count > 0 ? weaponRemovedIds.ToArray() : null, material: weaponMaterial); } } - if (secondEndGuid != null) { + + if (secondEndGuid != null) + { var weaponComponent = ResourcesLibrary.TryGetBlueprint(secondEndGuid); - if ((weaponComponent.DamageType.Physical.Form & PhysicalDamageForm.Piercing) != 0) { - accessors.SetBlueprintItemWeight(weaponComponent, 5.0f); - } else { - accessors.SetBlueprintItemWeight(weaponComponent, 0.0f); + if ((weaponComponent.DamageType.Physical.Form & PhysicalDamageForm.Piercing) != 0) + { + accessors.SetBlueprintItemWeight(weaponComponent) = 5.0f; + } + else + { + accessors.SetBlueprintItemWeight(weaponComponent) = 0.0f; } - accessors.SetBlueprintItemShieldWeaponComponent(shield, weaponComponent); - accessors.SetBlueprintItemWeight(armorComponentClone, armorComponentClone.Weight + weaponComponent.Weight); + accessors.SetBlueprintItemShieldWeaponComponent(shield) = weaponComponent; + accessors.SetBlueprintItemWeight(armorComponentClone) = armorComponentClone.Weight + weaponComponent.Weight; } - accessors.SetBlueprintItemShieldArmorComponent(shield, armorComponentClone); + accessors.SetBlueprintItemShieldArmorComponent(shield) = armorComponentClone; } var initiallyMundane = blueprint.Enchantments.Count == 0 && blueprint.Ability == null && blueprint.ActivatableAbility == null; @@ -454,15 +518,20 @@ private string ApplyRecipeItemBlueprintPatch(BlueprintItemEquipment blueprint, M // Copy Enchantments so we leave base blueprint alone var enchantmentsCopy = blueprint.Enchantments.ToList(); - if (!(blueprint is BlueprintItemShield)) { - accessors.SetBlueprintItemCachedEnchantments(blueprint, enchantmentsCopy); + if (!(blueprint is BlueprintItemShield)) + { + accessors.SetBlueprintItemCachedEnchantments(blueprint) = enchantmentsCopy; } + // Remove enchantments first, to see if we end up with an item with no abilities. var removed = new List(); - if (match.Groups["remove"].Success) { - foreach (var guid in removedIds) { + if (match.Groups["remove"].Success) + { + foreach (var guid in removedIds) + { var enchantment = ResourcesLibrary.TryGetBlueprint(guid); - if (!enchantment) { + if (!enchantment) + { throw new Exception($"Failed to load enchantment {guid}"); } @@ -472,9 +541,11 @@ private string ApplyRecipeItemBlueprintPatch(BlueprintItemEquipment blueprint, M } string ability = null; - if (match.Groups["ability"].Success) { + if (match.Groups["ability"].Success) + { ability = match.Groups["ability"].Value; - if (blueprint.Ability != null) { + if (blueprint.Ability != null) + { replaceAbility = true; } blueprint.Ability = ability == "null" ? null : ResourcesLibrary.TryGetBlueprint(ability); @@ -483,9 +554,11 @@ private string ApplyRecipeItemBlueprintPatch(BlueprintItemEquipment blueprint, M } string activatableAbility = null; - if (match.Groups["activatableAbility"].Success) { + if (match.Groups["activatableAbility"].Success) + { activatableAbility = match.Groups["activatableAbility"].Value; - if (blueprint.ActivatableAbility != null) { + if (blueprint.ActivatableAbility != null) + { replaceAbility = true; } blueprint.ActivatableAbility = activatableAbility == "null" @@ -494,26 +567,31 @@ private string ApplyRecipeItemBlueprintPatch(BlueprintItemEquipment blueprint, M } int charges = -1; - if (match.Groups["charges"].Success) { + if (match.Groups["charges"].Success) + { charges = int.Parse(match.Groups["charges"].Value); blueprint.Charges = charges; blueprint.SpendCharges = true; blueprint.RestoreChargesOnRest = false; - accessors.SetBlueprintItemIsStackable(blueprint, true); + accessors.SetBlueprintItemIsStackable(blueprint) = true; } int weight = -1; - if (match.Groups["weight"].Success) { + if (match.Groups["weight"].Success) + { weight = int.Parse(match.Groups["weight"].Value); - accessors.SetBlueprintItemWeight(blueprint, weight * .01f); + accessors.SetBlueprintItemWeight(blueprint) = weight * .01f; } var priceAdjust = 0; - if (match.Groups["priceAdjust"].Success) { + if (match.Groups["priceAdjust"].Success) + { priceAdjust = int.Parse(match.Groups["priceAdjust"].Value); priceDelta += priceAdjust; - } else if (!initiallyMundane && enchantmentsCopy.Count == 0 - && (blueprint.Ability == null || ability != null) && (blueprint.ActivatableAbility == null || activatableAbility != null)) { + } + else if (!initiallyMundane && enchantmentsCopy.Count == 0 + && (blueprint.Ability == null || ability != null) && (blueprint.ActivatableAbility == null || activatableAbility != null)) + { // We're down to a base item with no abilities - reset priceDelta. priceDelta = 0; } @@ -521,181 +599,224 @@ private string ApplyRecipeItemBlueprintPatch(BlueprintItemEquipment blueprint, M var skipped = new List(); var enchantmentsForDescription = new List(); int sizeCategoryChange = 0; - if (!string.IsNullOrEmpty(enchantmentsValue)) { - foreach (var guid in enchantmentIds) { + if (!string.IsNullOrEmpty(enchantmentsValue)) + { + foreach (var guid in enchantmentIds) + { var enchantment = ResourcesLibrary.TryGetBlueprint(guid); - if (!enchantment) { + if (!enchantment) + { throw new Exception($"Failed to load enchantment {guid}"); } var component = enchantment.GetComponent(); if (!string.IsNullOrEmpty(enchantment.Name) || - (component && component.Descriptor != ModifierDescriptor.ArmorEnhancement && component.Descriptor != ModifierDescriptor.ShieldEnhancement)) { + (component && component.Descriptor != ModifierDescriptor.ArmorEnhancement && component.Descriptor != ModifierDescriptor.ShieldEnhancement)) + { skipped.Add(enchantment); } enchantmentsForDescription.Add(enchantment); - if (blueprint is BlueprintItemArmor && guid == Main.MithralArmorEnchantmentGuid) { + if (blueprint is BlueprintItemArmor && guid == ItemQualityBlueprints.MithralArmorEnchantmentGuid) + { // Mithral equipment has half weight - accessors.SetBlueprintItemWeight(blueprint, blueprint.Weight / 2); + accessors.SetBlueprintItemWeight(blueprint) = blueprint.Weight / 2; } - if (blueprint is BlueprintItemEquipmentHand) { + if (blueprint is BlueprintItemEquipmentHand) + { var weaponBaseSizeChange = enchantment.GetComponent(); - if (weaponBaseSizeChange != null) { + if (weaponBaseSizeChange != null) + { sizeCategoryChange = weaponBaseSizeChange.SizeCategoryChange; - if (sizeCategoryChange > 0) { - accessors.SetBlueprintItemWeight(blueprint, blueprint.Weight * 2); - } else if (sizeCategoryChange < 0) { - accessors.SetBlueprintItemWeight(blueprint, blueprint.Weight / 2); + if (sizeCategoryChange > 0) + { + accessors.SetBlueprintItemWeight(blueprint) = blueprint.Weight * 2; + } + else if (sizeCategoryChange < 0) + { + accessors.SetBlueprintItemWeight(blueprint) = blueprint.Weight / 2; } } } if (!(blueprint is BlueprintItemShield) && (Main.GetItemType(blueprint) != ItemsFilter.ItemType.Shield - || Main.FindSourceRecipe(guid, blueprint) != null)) { + || Main.FindSourceRecipe(guid, blueprint) != null)) + { enchantmentsCopy.Add(enchantment); } } } PhysicalDamageMaterial material = 0; - if (match.Groups["material"].Success) { + if (match.Groups["material"].Success) + { Enum.TryParse(match.Groups["material"].Value, out material); - if (blueprint is BlueprintItemWeapon weapon) { - accessors.SetBlueprintItemWeaponDamageType(weapon, TraverseCloneAndSetField(weapon.DamageType, "Physical.Material", material.ToString())); - accessors.SetBlueprintItemWeaponOverrideDamageType(weapon, true); + if (blueprint is BlueprintItemWeapon weapon) + { + accessors.SetBlueprintItemWeaponDamageType(weapon) = TraverseCloneAndSetField(weapon.DamageType, "Physical.Material", material.ToString()); + accessors.SetBlueprintItemWeaponOverrideDamageType(weapon) = true; var materialGuid = PhysicalDamageMaterialEnchantments[material]; var enchantment = ResourcesLibrary.TryGetBlueprint(materialGuid); enchantmentsCopy.Add(enchantment); skipped.Add(enchantment); enchantmentsForDescription.Add(enchantment); - if (material == PhysicalDamageMaterial.Silver) { + if (material == PhysicalDamageMaterial.Silver) + { // PhysicalDamageMaterial.Silver is really Mithral, and Mithral equipment has half weight - accessors.SetBlueprintItemWeight(blueprint, blueprint.Weight / 2); + accessors.SetBlueprintItemWeight(blueprint) = blueprint.Weight / 2; } } } var equipmentHand = blueprint as BlueprintItemEquipmentHand; string visual = null; - if (match.Groups["visual"].Success) { + if (match.Groups["visual"].Success) + { visual = match.Groups["visual"].Value; // Copy icon from a different item var copyFromBlueprint = visual == "null" ? null : ResourcesLibrary.TryGetBlueprint(visual); var iconSprite = copyFromBlueprint == null ? null : copyFromBlueprint.Icon; - accessors.SetBlueprintItemIcon(blueprint, iconSprite); - if (equipmentHand != null && copyFromBlueprint is BlueprintItemEquipmentHand srcEquipmentHand) { - accessors.SetBlueprintItemEquipmentHandVisualParameters(equipmentHand, srcEquipmentHand.VisualParameters); - } else if (blueprint is BlueprintItemArmor armor && copyFromBlueprint is BlueprintItemArmor srcArmor) { - accessors.SetBlueprintItemArmorVisualParameters(armor, srcArmor.VisualParameters); + accessors.SetBlueprintItemIcon(blueprint) = iconSprite; + if (equipmentHand != null && copyFromBlueprint is BlueprintItemEquipmentHand srcEquipmentHand) + { + accessors.SetBlueprintItemEquipmentHandVisualParameters(equipmentHand) = srcEquipmentHand.VisualParameters; + } + else if (blueprint is BlueprintItemArmor armor && copyFromBlueprint is BlueprintItemArmor srcArmor) + { + accessors.SetBlueprintItemArmorVisualParameters(armor) = srcArmor.VisualParameters; } } string animation = null; - if (match.Groups["animation"].Success) { + if (match.Groups["animation"].Success) + { animation = match.Groups["animation"].Value; WeaponAnimationStyle weaponAnimation; - if (Enum.TryParse(animation, out weaponAnimation)) { - if (equipmentHand != null) { - accessors.SetBlueprintItemEquipmentWeaponAnimationStyle(equipmentHand.VisualParameters, weaponAnimation); + if (Enum.TryParse(animation, out weaponAnimation)) + { + if (equipmentHand != null) + { + accessors.SetBlueprintItemEquipmentWeaponAnimationStyle(equipmentHand.VisualParameters) = weaponAnimation; } } } var casterLevel = -1; - if (match.Groups["casterLevel"].Success) { + if (match.Groups["casterLevel"].Success) + { casterLevel = int.Parse(match.Groups["casterLevel"].Value); blueprint.CasterLevel = casterLevel; } var spellLevel = -1; - if (match.Groups["spellLevel"].Success) { + if (match.Groups["spellLevel"].Success) + { spellLevel = int.Parse(match.Groups["spellLevel"].Value); blueprint.SpellLevel = spellLevel; } var perDay = -1; - if (match.Groups["perDay"].Success) { + if (match.Groups["perDay"].Success) + { perDay = int.Parse(match.Groups["perDay"].Value); blueprint.Charges = perDay; blueprint.SpendCharges = true; blueprint.RestoreChargesOnRest = true; - if (blueprint.Ability.LocalizedSavingThrow != null && blueprint.Ability.LocalizedSavingThrow.IsSet()) { + if (blueprint.Ability.LocalizedSavingThrow != null && blueprint.Ability.LocalizedSavingThrow.IsSet()) + { blueprint.DC = 10 + blueprint.SpellLevel * 3 / 2; - } else { + } + else + { blueprint.DC = 0; } } string name = null; - if (match.Groups["name"].Success) { + if (match.Groups["name"].Success) + { name = match.Groups["name"].Value; - accessors.SetBlueprintItemDisplayNameText(blueprint, new FakeL10NString(name)); + accessors.SetBlueprintItemDisplayNameText(blueprint) = new FakeL10NString(name); } string nameId = null; - if (name == null && match.Groups["nameId"].Success) { + if (name == null && match.Groups["nameId"].Success) + { nameId = match.Groups["nameId"].Value; - accessors.SetBlueprintItemDisplayNameText(blueprint, new L10NString(nameId)); + accessors.SetBlueprintItemDisplayNameText(blueprint) = new L10NString(nameId); } string descriptionId = null; - if (match.Groups["descriptionId"].Success) { + if (match.Groups["descriptionId"].Success) + { descriptionId = match.Groups["descriptionId"].Value; - if (descriptionId == "craftMagicItems-material-silver-weapon-description") { + if (descriptionId == "craftMagicItems-material-silver-weapon-description") + { // Backwards compatibility - remove custom silver weapon description descriptionId = null; } } - if (match.Groups["secondEnd"].Success && blueprint is BlueprintItemWeapon doubleWeapon) { + if (match.Groups["secondEnd"].Success && blueprint is BlueprintItemWeapon doubleWeapon) + { secondEndGuid = match.Groups["secondEnd"].Value; doubleWeapon.SecondWeapon = ResourcesLibrary.TryGetBlueprint(secondEndGuid); } - if (!DoesBlueprintShowEnchantments(blueprint)) { + if (!DoesBlueprintShowEnchantments(blueprint)) + { skipped.Clear(); } - if (descriptionId != null) { - accessors.SetBlueprintItemDescriptionText(blueprint, new L10NString(descriptionId)); - accessors.SetBlueprintItemFlavorText(blueprint, new L10NString("")); - } else if ((blueprint is BlueprintItemShield || Main.GetItemType(blueprint) != ItemsFilter.ItemType.Shield) - && (!DoesBlueprintShowEnchantments(blueprint) || enchantmentsForDescription.Count != skipped.Count || removed.Count > 0)) { - accessors.SetBlueprintItemDescriptionText(blueprint, - Main.BuildCustomRecipeItemDescription(blueprint, enchantmentsForDescription, skipped, removed, replaceAbility, ability, casterLevel, perDay)); - accessors.SetBlueprintItemFlavorText(blueprint, new L10NString("")); + if (descriptionId != null) + { + accessors.SetBlueprintItemDescriptionText(blueprint) = new L10NString(descriptionId); + accessors.SetBlueprintItemFlavorText(blueprint) = new L10NString(""); + } + else if ((blueprint is BlueprintItemShield || Main.GetItemType(blueprint) != ItemsFilter.ItemType.Shield) + && (!DoesBlueprintShowEnchantments(blueprint) || enchantmentsForDescription.Count != skipped.Count || removed.Count > 0)) + { + accessors.SetBlueprintItemDescriptionText(blueprint) = + BuildCustomRecipeItemDescription(blueprint, enchantmentsForDescription, skipped, removed, replaceAbility, ability, casterLevel, perDay); + accessors.SetBlueprintItemFlavorText(blueprint) = new L10NString(""); } - accessors.SetBlueprintItemCost(blueprint, Main.RulesRecipeItemCost(blueprint) + priceDelta); + accessors.SetBlueprintItemCost(blueprint) = Main.RulesRecipeItemCost(blueprint) + priceDelta; return BuildCustomRecipeItemGuid(blueprint.AssetGuid, enchantmentIds, removedIds.Count > 0 ? removedIds.ToArray() : null, name, ability, activatableAbility, charges, weight, material, visual, animation, casterLevel, spellLevel, perDay, nameId, descriptionId, secondEndGuid, priceAdjust); } - private T CloneObject(T originalObject) { + private T CloneObject(T originalObject) + { var type = originalObject.GetType(); #if PATCH21_BETA - if (typeof(BlueprintScriptableObject).IsAssignableFrom(type)) { + if (typeof(BlueprintScriptableObject).IsAssignableFrom(type)) + { var srcBlueprint = originalObject as BlueprintScriptableObject; var dstBlueprint = (BlueprintScriptableObject)SerializedScriptableObject.Instantiate(originalObject as BlueprintScriptableObject); dstBlueprint.AssetGuid = srcBlueprint.AssetGuid; dstBlueprint.name = srcBlueprint.name + "(Clone)"; return (T)(object)dstBlueprint; - } else if (typeof(SerializedScriptableObject).IsAssignableFrom(type)) { + } + else if (typeof(SerializedScriptableObject).IsAssignableFrom(type)) + { return (T)(object)SerializedScriptableObject.Instantiate(originalObject as SerializedScriptableObject); } #else - if (typeof(ScriptableObject).IsAssignableFrom(type)) { + if (typeof(ScriptableObject).IsAssignableFrom(type)) + { return (T) (object) Object.Instantiate(originalObject as Object); } #endif var clone = (T) Activator.CreateInstance(type); #if PATCH21_BETA - for (; type != null && type != typeof(IDirectlySerializable); type = type.BaseType) { + for (; type != null && type != typeof(IDirectlySerializable); type = type.BaseType) #else - for (; type != null && type != typeof(Object); type = type.BaseType) { + for (; type != null && type != typeof(Object); type = type.BaseType) #endif + { var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); - foreach (var field in fields) { + foreach (var field in fields) + { field.SetValue(clone, field.GetValue(originalObject)); } } @@ -703,11 +824,14 @@ private T CloneObject(T originalObject) { return clone; } - private T TraverseCloneAndSetField(T original, string field, string value) where T : class { - if (string.IsNullOrEmpty(field)) { + private T TraverseCloneAndSetField(T original, string field, string value) where T : class + { + if (string.IsNullOrEmpty(field)) + { value = value.Replace("#", ", "); var componentType = Type.GetType(value); - if (componentType == null) { + if (componentType == null) + { throw new Exception($"Failed to create object with type {value}"); } @@ -720,84 +844,117 @@ private T TraverseCloneAndSetField(T original, string field, string value) wh #endif : Activator.CreateInstance(componentType); - if (!(componentObject is T component)) { + if (!(componentObject is T component)) + { throw new Exception($"Failed to create expected instance with type {value}, " + $"result is {componentType.FullName}"); } return component; - } else { + } + else + { // Strip leading . off field - if (field.StartsWith(".")) { + if (field.StartsWith(".")) + { field = field.Substring(1); } } var clone = CloneObject(original); var fieldNameEnd = field.IndexOf('.'); - if (fieldNameEnd < 0) { - var fieldAccess = Harmony12.Traverse.Create(clone).Field(field); - if (!fieldAccess.FieldExists()) { + if (fieldNameEnd < 0) + { + var fieldAccess = HarmonyLib.Traverse.Create(clone).Field(field); + if (!fieldAccess.FieldExists()) + { throw new Exception( - $"Field {field} does not exist on original of type {clone.GetType().FullName}, available fields: {string.Join(", ", Harmony12.Traverse.Create(clone).Fields())}"); + $"Field {field} does not exist on original of type {clone.GetType().FullName}, available fields: {string.Join(", ", HarmonyLib.Traverse.Create(clone).Fields())}"); } - if (value == "null") { + if (value == "null") + { fieldAccess.SetValue(null); - } else if (typeof(BlueprintScriptableObject).IsAssignableFrom(fieldAccess.GetValueType())) { + } + else if (typeof(BlueprintScriptableObject).IsAssignableFrom(fieldAccess.GetValueType())) + { fieldAccess.SetValue(ResourcesLibrary.TryGetBlueprint(value)); - } else if (fieldAccess.GetValueType() == typeof(LocalizedString)) { + } + else if (fieldAccess.GetValueType() == typeof(LocalizedString)) + { fieldAccess.SetValue(new L10NString(value)); - } else if (fieldAccess.GetValueType() == typeof(bool)) { + } + else if (fieldAccess.GetValueType() == typeof(bool)) + { fieldAccess.SetValue(value == "true"); - } else if (fieldAccess.GetValueType() == typeof(int)) { + } + else if (fieldAccess.GetValueType() == typeof(int)) + { fieldAccess.SetValue(int.Parse(value)); - } else if (fieldAccess.GetValueType().IsEnum) { + } + else if (fieldAccess.GetValueType().IsEnum) + { fieldAccess.SetValue(Enum.Parse(fieldAccess.GetValueType(), value)); - } else { + } + else + { fieldAccess.SetValue(value); } - } else { + } + else + { var thisField = field.Substring(0, fieldNameEnd); var remainingFields = field.Substring(fieldNameEnd + 1); var arrayPos = thisField.IndexOf('['); - if (arrayPos < 0) { - var fieldAccess = Harmony12.Traverse.Create(clone).Field(thisField); - if (!fieldAccess.FieldExists()) { + if (arrayPos < 0) + { + var fieldAccess = HarmonyLib.Traverse.Create(clone).Field(thisField); + if (!fieldAccess.FieldExists()) + { throw new Exception( - $"Field {thisField} does not exist on original of type {clone.GetType().FullName}, available fields: {string.Join(", ", Harmony12.Traverse.Create(clone).Fields())}"); + $"Field {thisField} does not exist on original of type {clone.GetType().FullName}, available fields: {string.Join(", ", HarmonyLib.Traverse.Create(clone).Fields())}"); } - if (fieldAccess.GetValueType().IsArray) { + if (fieldAccess.GetValueType().IsArray) + { throw new Exception($"Field {thisField} is an array but overall access {field} did not index the array"); } fieldAccess.SetValue(TraverseCloneAndSetField(fieldAccess.GetValue(), remainingFields, value)); - } else { + } + else + { var index = int.Parse(new string(thisField.Skip(arrayPos + 1).TakeWhile(char.IsDigit).ToArray())); thisField = field.Substring(0, arrayPos); - var fieldAccess = Harmony12.Traverse.Create(clone).Field(thisField); - if (!fieldAccess.FieldExists()) { + var fieldAccess = HarmonyLib.Traverse.Create(clone).Field(thisField); + if (!fieldAccess.FieldExists()) + { throw new Exception( - $"Field {thisField} does not exist on original of type {clone.GetType().FullName}, available fields: {string.Join(", ", Harmony12.Traverse.Create(clone).Fields())}"); + $"Field {thisField} does not exist on original of type {clone.GetType().FullName}, available fields: {string.Join(", ", HarmonyLib.Traverse.Create(clone).Fields())}"); } - if (!fieldAccess.GetValueType().IsArray) { + if (!fieldAccess.GetValueType().IsArray) + { throw new Exception( $"Field {thisField} is of type {fieldAccess.GetValueType().FullName} but overall access {field} used an array index"); } // TODO if I use fieldAccess.GetValue().ToArray() to make this universally applicable, the SetValue fails saying it can't // convert object[] to e.g. Condition[]. Hard-code to only support Condition for array for now. - if (fieldAccess.GetValueType() == typeof(Condition[])) { + if (fieldAccess.GetValueType() == typeof(Condition[])) + { var arrayClone = fieldAccess.GetValue().ToArray(); arrayClone[index] = TraverseCloneAndSetField(arrayClone[index], remainingFields, value); fieldAccess.SetValue(arrayClone); - } else if (fieldAccess.GetValueType() == typeof(GameAction[])) { + } + else if (fieldAccess.GetValueType() == typeof(GameAction[])) + { var arrayClone = fieldAccess.GetValue().ToArray(); arrayClone[index] = TraverseCloneAndSetField(arrayClone[index], remainingFields, value); fieldAccess.SetValue(arrayClone); - } else { + } + else + { throw new Exception( $"Field {thisField} is of unsupported array type {fieldAccess.GetValueType().FullName} ({field})"); } @@ -807,36 +964,43 @@ private T TraverseCloneAndSetField(T original, string field, string value) wh return clone; } - public void EnsureComponentNameUnique(BlueprintComponent component, BlueprintComponent[] existing) { + public void EnsureComponentNameUnique(BlueprintComponent component, BlueprintComponent[] existing) + { // According to Elmindra, components which are serialized need to have unique names in their array var name = component.name; var suffix = 0; - while (existing.Any(blueprint => blueprint.name == name)) { + while (existing.Any(blueprint => blueprint.name == name)) + { suffix++; name = $"{component.name}_{suffix}"; } component.name = name; } - private string ApplyItemEnchantmentBlueprintPatch(BlueprintScriptableObject blueprint, Match match) { + private string ApplyItemEnchantmentBlueprintPatch(BlueprintScriptableObject blueprint, Match match) + { var values = new List(); // Ensure Components array is not shared with base blueprint var componentsCopy = blueprint.ComponentsArray.ToArray(); var indexCaptures = match.Groups["index"].Captures; var fieldCaptures = match.Groups["field"].Captures; var valueCaptures = match.Groups["value"].Captures; - for (var index = 0; index < indexCaptures.Count; ++index) { + for (var index = 0; index < indexCaptures.Count; ++index) + { var componentIndex = int.Parse(indexCaptures[index].Value); var field = fieldCaptures[index].Value; var value = valueCaptures[index].Value; values.Add(indexCaptures[index].Value); values.Add(field); values.Add(value); - if (componentIndex >= componentsCopy.Length) { + if (componentIndex >= componentsCopy.Length) + { var component = TraverseCloneAndSetField(null, field, value); EnsureComponentNameUnique(component, componentsCopy); - componentsCopy = componentsCopy.Concat(new[] {component}).ToArray(); - } else { + componentsCopy = componentsCopy.Concat(new[] { component }).ToArray(); + } + else + { componentsCopy[componentIndex] = TraverseCloneAndSetField(componentsCopy[componentIndex], field, value); } } @@ -847,28 +1011,42 @@ private string ApplyItemEnchantmentBlueprintPatch(BlueprintScriptableObject blue var buff = blueprint as BlueprintBuff; var ability = blueprint as BlueprintAbility; string nameId = null; - if (match.Groups["nameId"].Success) { + if (match.Groups["nameId"].Success) + { nameId = match.Groups["nameId"].Value; - if (enchantment != null) { - accessors.SetBlueprintItemEnchantmentEnchantName(enchantment, new L10NString(nameId)); - } else if (feature != null) { - accessors.SetBlueprintUnitFactDisplayName(feature, new L10NString(nameId)); - } else if (buff != null) { - accessors.SetBlueprintUnitFactDisplayName(buff, new L10NString(nameId)); + if (enchantment != null) + { + accessors.SetBlueprintItemEnchantmentEnchantName(enchantment) = new L10NString(nameId); + } + else if (feature != null) + { + accessors.SetBlueprintUnitFactDisplayName(feature) = new L10NString(nameId); + } + else if (buff != null) + { + accessors.SetBlueprintUnitFactDisplayName(buff) = new L10NString(nameId); } } string descriptionId = null; - if (match.Groups["descriptionId"].Success) { + if (match.Groups["descriptionId"].Success) + { descriptionId = match.Groups["descriptionId"].Value; - if (enchantment != null) { - accessors.SetBlueprintItemEnchantmentDescription(enchantment, new L10NString(descriptionId)); - } else if (feature != null) { - accessors.SetBlueprintUnitFactDescription(feature, new L10NString(descriptionId)); - } else if (buff != null) { - accessors.SetBlueprintUnitFactDescription(buff, new L10NString(descriptionId)); - } else if (ability != null) { - accessors.SetBlueprintUnitFactDescription(ability, new L10NString(descriptionId)); + if (enchantment != null) + { + accessors.SetBlueprintItemEnchantmentDescription(enchantment) = new L10NString(descriptionId); + } + else if (feature != null) + { + accessors.SetBlueprintUnitFactDescription(feature) = new L10NString(descriptionId); + } + else if (buff != null) + { + accessors.SetBlueprintUnitFactDescription(buff) = new L10NString(descriptionId); + } + else if (ability != null) + { + accessors.SetBlueprintUnitFactDescription(ability) = new L10NString(descriptionId); } } @@ -878,8 +1056,10 @@ private string ApplyItemEnchantmentBlueprintPatch(BlueprintScriptableObject blue // Make our mod-specific updates to the blueprint based on the data stored in assetId. Return a string which // is the AssetGuid of the supplied blueprint plus our customization again, or null if we couldn't change the // blueprint. - private string ApplyBlueprintPatch(BlueprintScriptableObject blueprint, Match match) { - switch (blueprint) { + private string ApplyBlueprintPatch(BlueprintScriptableObject blueprint, Match match) + { + switch (blueprint) + { case BlueprintBuff buff when match.Groups["timer"].Success: return ApplyTimerBlueprintPatch(buff); case BlueprintBuff buff when match.Groups["bondedItem"].Success: @@ -898,10 +1078,113 @@ private string ApplyBlueprintPatch(BlueprintScriptableObject blueprint, Match ma return ApplyItemEnchantmentBlueprintPatch(buff, match); case BlueprintAbility ability when match.Groups["components"].Success: return ApplyItemEnchantmentBlueprintPatch(ability, match); - default: { - throw new Exception($"Match of assetId {match.Value} didn't match blueprint type {blueprint.GetType()}"); + default: + { + throw new Exception($"Match of assetId {match.Value} didn't match blueprint type {blueprint.GetType()}"); + } + } + } + + public static LocalizedString BuildCustomRecipeItemDescription(BlueprintItem blueprint, IList enchantments, + IList skipped, IList removed, bool replaceAbility, string ability, int casterLevel, int perDay) + { + var extraDescription = enchantments + .Select(enchantment => { + var recipe = Main.FindSourceRecipe(enchantment.AssetGuid, blueprint); + if (recipe == null) + { + if (skipped.Contains(enchantment)) + { + return ""; + } + else if (!string.IsNullOrEmpty(enchantment.Name)) + { + return enchantment.Name; + } + else + { + return "Unknown"; + } + } + else if (recipe.Enchantments.Length <= 1) + { + if (skipped.Contains(enchantment)) + { + return ""; + } + else + { + if (!string.IsNullOrEmpty(enchantment.Name)) + { + return enchantment.Name; + } + else + { + return recipe.NameId; + } + } + } + var newBonus = recipe.Enchantments.FindIndex(e => e == enchantment) + 1; + var bonusString = Main.GetBonusString(newBonus, recipe); + var bonusDescription = recipe.BonusTypeId != null + ? LocalizationHelper.FormatLocalizedString("craftMagicItems-custom-description-bonus-to", new L10NString(recipe.BonusTypeId), recipe.NameId) + : recipe.BonusToId != null + ? LocalizationHelper.FormatLocalizedString("craftMagicItems-custom-description-bonus-to", recipe.NameId, new L10NString(recipe.BonusToId)) + : LocalizationHelper.FormatLocalizedString("craftMagicItems-custom-description-bonus", recipe.NameId); + var upgradeFrom = removed.FirstOrDefault(remove => Main.FindSourceRecipe(remove.AssetGuid, blueprint) == recipe); + var oldBonus = int.MaxValue; + if (upgradeFrom != null) + { + oldBonus = recipe.Enchantments.FindIndex(e => e == upgradeFrom) + 1; + } + if (oldBonus > newBonus) + { + if (skipped.Contains(enchantment)) + { + return new L10NString(""); + } + else + { + return LocalizationHelper.FormatLocalizedString("craftMagicItems-custom-description-enchantment-template", bonusString, bonusDescription); + } + } + else + { + removed.Remove(upgradeFrom); + } + return LocalizationHelper.FormatLocalizedString("craftMagicItems-custom-description-enchantment-upgrade-template", bonusDescription, + Main.GetBonusString(oldBonus, recipe), bonusString); + }) + .OrderBy(enchantmentDescription => enchantmentDescription) + .Select(enchantmentDescription => string.IsNullOrEmpty(enchantmentDescription) ? "" : "\n* " + enchantmentDescription) + .Join(""); + if (blueprint is BlueprintItemEquipment equipment && (ability != null && ability != "null" || casterLevel > -1 || perDay > -1)) + { + GameLogContext.Count = equipment.Charges; + extraDescription += "\n* " + (equipment.Charges == 1 ? LocalizationHelper.FormatLocalizedString("craftMagicItems-label-cast-spell-n-times-details-single", equipment.Ability.Name, equipment.CasterLevel) : + LocalizationHelper.FormatLocalizedString("craftMagicItems-label-cast-spell-n-times-details-multiple", equipment.Ability.Name, equipment.CasterLevel, equipment.Charges)); + GameLogContext.Clear(); + } + + string description; + if (removed.Count == 0 && !replaceAbility) + { + description = blueprint.Description; + if (extraDescription.Length > 0) + { + description += new L10NString("craftMagicItems-custom-description-additional") + extraDescription; } } + else if (extraDescription.Length > 0) + { + description = new L10NString("craftMagicItems-custom-description-start") + extraDescription; + } + else + { + description = ""; + } + + return new FakeL10NString(description); } } } \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/ActionBarManagerUpdatePatch.cs b/CraftMagicItems/Patches/Harmony/ActionBarManagerUpdatePatch.cs new file mode 100644 index 0000000..1b848ec --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/ActionBarManagerUpdatePatch.cs @@ -0,0 +1,20 @@ +#if !PATCH21 +using Kingmaker.UI.ActionBar; +#endif + +namespace CraftMagicItems.Patches.Harmony +{ +#if !PATCH21 + // Fix issue in Owlcat's UI - ActionBarManager.Update does not refresh the Groups (spells/Actions/Belt) + [HarmonyLib.HarmonyPatch(typeof(ActionBarManager), "Update")] + public static class ActionBarManagerUpdatePatch { + private static void Prefix(ActionBarManager __instance) { + var mNeedReset = Main.Accessors.GetActionBarManagerNeedReset(__instance); + if (mNeedReset) { + var mSelected = Main.Accessors.GetActionBarManagerSelected(__instance); + __instance.Group.Set(mSelected); + } + } + } +#endif +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/ActivatableAbilityOnEventDidTriggerRuleAttackWithWeaponResolvePatch.cs b/CraftMagicItems/Patches/Harmony/ActivatableAbilityOnEventDidTriggerRuleAttackWithWeaponResolvePatch.cs new file mode 100644 index 0000000..92ec38a --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/ActivatableAbilityOnEventDidTriggerRuleAttackWithWeaponResolvePatch.cs @@ -0,0 +1,21 @@ +#if !PATCH21 +using System; +using Kingmaker.RuleSystem.Rules; +using Kingmaker.UnitLogic.ActivatableAbilities; +#endif + +namespace CraftMagicItems.Patches.Harmony +{ +#if !PATCH21 + [HarmonyLib.HarmonyPatch(typeof(ActivatableAbility), "OnEventDidTrigger", new Type[] { typeof(RuleAttackWithWeaponResolve) })] + public static class ActivatableAbilityOnEventDidTriggerRuleAttackWithWeaponResolvePatch { + private static bool Prefix(ActivatableAbility __instance, RuleAttackWithWeaponResolve evt) { + if (evt.Damage != null && evt.AttackRoll.IsHit) { + return false; + } else { + return true; + } + } + } +#endif +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/AddInitiatorAttackRollTriggerCheckConditionsPatch.cs b/CraftMagicItems/Patches/Harmony/AddInitiatorAttackRollTriggerCheckConditionsPatch.cs new file mode 100644 index 0000000..ed25651 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/AddInitiatorAttackRollTriggerCheckConditionsPatch.cs @@ -0,0 +1,36 @@ +using Kingmaker.Blueprints; +using Kingmaker.Blueprints.Items.Ecnchantments; +using Kingmaker.Items; +using Kingmaker.RuleSystem; +using Kingmaker.RuleSystem.Rules; +using Kingmaker.UnitLogic.Mechanics.Components; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(AddInitiatorAttackRollTrigger), "CheckConditions")] + // ReSharper disable once UnusedMember.Local + public static class AddInitiatorAttackRollTriggerCheckConditionsPatch + { + // ReSharper disable once UnusedMember.Local + private static bool Prefix(AddInitiatorAttackRollTrigger __instance, RuleAttackRoll evt, ref bool __result) + { + if (__instance is GameLogicComponent logic) + { + ItemEnchantment itemEnchantment = logic.Fact as ItemEnchantment; + ItemEntity itemEntity = (itemEnchantment != null) ? itemEnchantment.Owner : null; + RuleAttackWithWeapon ruleAttackWithWeapon = evt.Reason.Rule as RuleAttackWithWeapon; + ItemEntityWeapon itemEntityWeapon = (ruleAttackWithWeapon != null) ? ruleAttackWithWeapon.Weapon : null; + __result = (itemEntity == null || itemEntity == itemEntityWeapon || evt.Weapon.Blueprint.IsNatural || evt.Weapon.Blueprint.IsUnarmed) && + (!__instance.CheckWeapon || (itemEntityWeapon != null && __instance.WeaponCategory == itemEntityWeapon.Blueprint.Category)) && + (!__instance.OnlyHit || evt.IsHit) && (!__instance.CriticalHit || (evt.IsCriticalConfirmed && !evt.FortificationNegatesCriticalHit)) && + (!__instance.SneakAttack || (evt.IsSneakAttack && !evt.FortificationNegatesSneakAttack)) && + (__instance.AffectFriendlyTouchSpells || evt.Initiator.IsEnemy(evt.Target) || evt.Weapon.Blueprint.Type.AttackType != AttackType.Touch); + return false; + } + else + { + return true; + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/AttackStatReplacementOnEventAboutToTriggerPatch.cs b/CraftMagicItems/Patches/Harmony/AttackStatReplacementOnEventAboutToTriggerPatch.cs new file mode 100644 index 0000000..ecb0f87 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/AttackStatReplacementOnEventAboutToTriggerPatch.cs @@ -0,0 +1,26 @@ +using Kingmaker.Enums; +using Kingmaker.RuleSystem.Rules; +using Kingmaker.UnitLogic.FactLogic; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(AttackStatReplacement), "OnEventAboutToTrigger")] + // ReSharper disable once UnusedMember.Local + public static class AttackStatReplacementOnEventAboutToTriggerPatch + { + private static bool Prefix(AttackStatReplacement __instance, RuleCalculateAttackBonusWithoutTarget evt) + { + if (evt.Weapon != null + && __instance.SubCategory == WeaponSubCategory.Finessable + && evt.Weapon.Blueprint.Type.Category.HasSubCategory(WeaponSubCategory.Finessable) && + Main.IsOversized(evt.Weapon.Blueprint)) + { + return false; + } + else + { + return true; + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/BattleLogManagerInitializePatch.cs b/CraftMagicItems/Patches/Harmony/BattleLogManagerInitializePatch.cs new file mode 100644 index 0000000..cc59bfc --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/BattleLogManagerInitializePatch.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Runtime.Serialization; +using System.Text.RegularExpressions; +using CraftMagicItems.Config; +using CraftMagicItems.Constants; +using CraftMagicItems.Localization; +using CraftMagicItems.Patches; +using CraftMagicItems.UI; +using CraftMagicItems.UI.Sections; +using CraftMagicItems.UI.UnityModManager; +using Kingmaker; +#if PATCH21 +using Kingmaker.Assets.UI.Context; +#endif +using Kingmaker.Blueprints; +using Kingmaker.Blueprints.Classes; +using Kingmaker.Blueprints.Classes.Selection; +using Kingmaker.Blueprints.Classes.Spells; +using Kingmaker.Blueprints.Facts; +using Kingmaker.Blueprints.Items; +using Kingmaker.Blueprints.Items.Armors; +using Kingmaker.Blueprints.Items.Ecnchantments; +using Kingmaker.Blueprints.Items.Equipment; +using Kingmaker.Blueprints.Items.Shields; +using Kingmaker.Blueprints.Items.Weapons; +using Kingmaker.Blueprints.Loot; +using Kingmaker.Blueprints.Root; +using Kingmaker.Blueprints.Root.Strings.GameLog; +using Kingmaker.Controllers.Rest; +using Kingmaker.Designers; +using Kingmaker.Designers.Mechanics.EquipmentEnchants; +using Kingmaker.Designers.Mechanics.Facts; +using Kingmaker.Designers.Mechanics.WeaponEnchants; +using Kingmaker.Designers.TempMapCode.Capital; +using Kingmaker.EntitySystem.Entities; +using Kingmaker.EntitySystem.Stats; +using Kingmaker.Enums; +using Kingmaker.Enums.Damage; +using Kingmaker.GameModes; +using Kingmaker.Items; +#if !PATCH21 +using Kingmaker.Items.Slots; +#endif +using Kingmaker.Kingdom; +using Kingmaker.Localization; +using Kingmaker.PubSubSystem; +using Kingmaker.RuleSystem; +using Kingmaker.RuleSystem.Rules; +using Kingmaker.RuleSystem.Rules.Abilities; +using Kingmaker.RuleSystem.Rules.Damage; +using Kingmaker.UI; +using Kingmaker.UI.ActionBar; +using Kingmaker.UI.Common; +using Kingmaker.UI.Log; +using Kingmaker.UI.Tooltip; +using Kingmaker.UnitLogic; +using Kingmaker.UnitLogic.Abilities; +using Kingmaker.UnitLogic.Abilities.Blueprints; +#if !PATCH21 +using Kingmaker.UnitLogic.ActivatableAbilities; +#endif +using Kingmaker.UnitLogic.Alignments; +using Kingmaker.UnitLogic.Buffs; +using Kingmaker.UnitLogic.Buffs.Blueprints; +using Kingmaker.UnitLogic.FactLogic; +using Kingmaker.UnitLogic.Mechanics.Components; +using Kingmaker.UnitLogic.Parts; +using Kingmaker.Utility; +using Kingmaker.View.Equipment; +using Newtonsoft.Json; +using UnityEngine; +using UnityModManagerNet; +using Random = System.Random; + +namespace CraftMagicItems.Patches.Harmony +{ + // Add "pending" log items when the battle log becomes available again, so crafting messages sent when e.g. camping + // in the overland map are still shown eventually. + [HarmonyLib.HarmonyPatch(typeof(BattleLogManager), "Initialize")] + // ReSharper disable once UnusedMember.Local + public static class BattleLogManagerInitializePatch + { + // ReSharper disable once UnusedMember.Local + private static void Postfix() + { + if (Enumerable.Any(Main.PendingLogItems)) + { + foreach (var item in Main.PendingLogItems) + { + item.UpdateSize(); + Game.Instance.UI.BattleLogManager.LogView.AddLogEntry(item); + } + + Main.PendingLogItems.Clear(); + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/BlueprintAbilityIsInSpellListPatch.cs b/CraftMagicItems/Patches/Harmony/BlueprintAbilityIsInSpellListPatch.cs new file mode 100644 index 0000000..cba43f2 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/BlueprintAbilityIsInSpellListPatch.cs @@ -0,0 +1,20 @@ +using Kingmaker.Blueprints.Classes.Spells; +using Kingmaker.UnitLogic.Abilities.Blueprints; + +namespace CraftMagicItems.Patches.Harmony +{ + // Owlcat's code doesn't correctly detect that a variant spell is in a spellList when its parent spell is. + [HarmonyLib.HarmonyPatch(typeof(BlueprintAbility), "IsInSpellList")] + // ReSharper disable once UnusedMember.Global + public static class BlueprintAbilityIsInSpellListPatch + { + // ReSharper disable once UnusedMember.Local + private static void Postfix(BlueprintAbility __instance, BlueprintSpellList spellList, ref bool __result) + { + if (!__result && __instance.Parent != null && __instance.Parent != __instance) + { + __result = __instance.Parent.IsInSpellList(spellList); + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/BlueprintItemEquipmentUsableCostPatch.cs b/CraftMagicItems/Patches/Harmony/BlueprintItemEquipmentUsableCostPatch.cs new file mode 100644 index 0000000..73f72be --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/BlueprintItemEquipmentUsableCostPatch.cs @@ -0,0 +1,35 @@ +using Kingmaker.Blueprints.Items.Equipment; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(BlueprintItemEquipmentUsable), "Cost", HarmonyLib.MethodType.Getter)] + // ReSharper disable once UnusedMember.Local + public static class BlueprintItemEquipmentUsableCostPatch + { + // ReSharper disable once UnusedMember.Local + private static void Postfix(BlueprintItemEquipmentUsable __instance, ref int __result) + { + if (__result == 0 && __instance.SpellLevel == 0) + { + // Owlcat's cost calculation doesn't handle level 0 spells properly. + int chargeCost; + switch (__instance.Type) + { + case UsableItemType.Wand: + chargeCost = 15; + break; + case UsableItemType.Scroll: + chargeCost = 25; + break; + case UsableItemType.Potion: + chargeCost = 50; + break; + default: + return; + } + + __result = __instance.CasterLevel * chargeCost * __instance.Charges / 2; + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/BrilliantEnergyOnEventAboutToTriggerPatch.cs b/CraftMagicItems/Patches/Harmony/BrilliantEnergyOnEventAboutToTriggerPatch.cs new file mode 100644 index 0000000..f79bd99 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/BrilliantEnergyOnEventAboutToTriggerPatch.cs @@ -0,0 +1,29 @@ +using Kingmaker.Blueprints.Items.Ecnchantments; +using Kingmaker.Designers.Mechanics.Facts; +using Kingmaker.Items; +using Kingmaker.RuleSystem.Rules; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(BrilliantEnergy), "OnEventAboutToTrigger")] + // ReSharper disable once UnusedMember.Local + public static class BrilliantEnergyOnEventAboutToTriggerPatch + { + // ReSharper disable once UnusedMember.Local + private static bool Prefix(BrilliantEnergy __instance, RuleCalculateAC evt) + { + if (__instance is ItemEnchantmentLogic logic) + { + if (evt.Reason.Item is ItemEntityWeapon weapon && Main.EquipmentEnchantmentValid(weapon, logic.Owner)) + { + evt.BrilliantEnergy = logic.Fact; + } + return false; + } + else + { + return true; + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/CapitalCompanionLogicOnFactActivatePatch.cs b/CraftMagicItems/Patches/Harmony/CapitalCompanionLogicOnFactActivatePatch.cs new file mode 100644 index 0000000..7d2c455 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/CapitalCompanionLogicOnFactActivatePatch.cs @@ -0,0 +1,23 @@ +using Kingmaker; +using Kingmaker.Designers.TempMapCode.Capital; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(CapitalCompanionLogic), "OnFactActivate")] + // ReSharper disable once UnusedMember.Local + public static class CapitalCompanionLogicOnFactActivatePatch + { + // ReSharper disable once UnusedMember.Local + private static void Prefix() + { + // Trigger project work on companions left behind in the capital, with a flag saying the party wasn't around while they were working. + foreach (var companion in Game.Instance.Player.RemoteCompanions) + { + if (companion.Value != null) + { + CraftingLogic.WorkOnProjects(companion.Value.Descriptor, true); + } + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/DamageGraceOnEventAboutToTriggerPatch.cs b/CraftMagicItems/Patches/Harmony/DamageGraceOnEventAboutToTriggerPatch.cs new file mode 100644 index 0000000..5fcc67c --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/DamageGraceOnEventAboutToTriggerPatch.cs @@ -0,0 +1,25 @@ +using Kingmaker.Designers.Mechanics.Facts; +using Kingmaker.Enums; +using Kingmaker.RuleSystem.Rules; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(DamageGrace), "OnEventAboutToTrigger")] + // ReSharper disable once UnusedMember.Local + public static class DamageGraceOnEventAboutToTriggerPatch + { + private static bool Prefix(DamageGrace __instance, RuleCalculateWeaponStats evt) + { + if (evt.Weapon != null + && evt.Weapon.Blueprint.Type.Category.HasSubCategory(WeaponSubCategory.Finessable) + && Main.IsOversized(evt.Weapon.Blueprint)) + { + return false; + } + else + { + return true; + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/DescriptionTemplatesItemItemEnergyPatch.cs b/CraftMagicItems/Patches/Harmony/DescriptionTemplatesItemItemEnergyPatch.cs new file mode 100644 index 0000000..6f493b5 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/DescriptionTemplatesItemItemEnergyPatch.cs @@ -0,0 +1,19 @@ +using Kingmaker.UI.Tooltip; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(DescriptionTemplatesItem), "ItemEnergy")] + public static class DescriptionTemplatesItemItemEnergyPatch + { + private static void Postfix(TooltipData data, bool __result) + { + if (__result) + { + if (data.Energy.Count > 0) + { + data.Energy.Clear(); + } + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/DescriptionTemplatesItemItemEnhancementPatch.cs b/CraftMagicItems/Patches/Harmony/DescriptionTemplatesItemItemEnhancementPatch.cs new file mode 100644 index 0000000..2d93bd0 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/DescriptionTemplatesItemItemEnhancementPatch.cs @@ -0,0 +1,22 @@ +using System; +using System.Linq; +using Kingmaker.UI.Tooltip; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(DescriptionTemplatesItem), "ItemEnhancement")] + public static class DescriptionTemplatesItemItemEnhancementPatch + { + private static void Postfix(TooltipData data) + { + if (data.Texts.ContainsKey(Enum.GetValues(typeof(TooltipElement)).Cast().Max() + 1)) + { + data.Texts[TooltipElement.Enhancement] = data.Texts[Enum.GetValues(typeof(TooltipElement)).Cast().Max() + 1]; + } + else if (data.Texts.ContainsKey(TooltipElement.Enhancement)) + { + data.Texts.Remove(TooltipElement.Enhancement); + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/GameOnAreaLoadedPatch.cs b/CraftMagicItems/Patches/Harmony/GameOnAreaLoadedPatch.cs new file mode 100644 index 0000000..0c4d360 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/GameOnAreaLoadedPatch.cs @@ -0,0 +1,24 @@ +using Kingmaker.UI; +using Kingmaker.UI.Common; + +namespace CraftMagicItems.Patches.Harmony +{ + public static class GameOnAreaLoadedPatch + { + private static void Postfix() + { + if (CustomBlueprintBuilder.DidDowngrade) + { + UIUtility.ShowMessageBox("Craft Magic Items is disabled. All your custom enchanted items and crafting feats have been replaced with vanilla versions.", +#if PATCH21 + DialogMessageBoxBase.BoxType.Message, +#else + DialogMessageBox.BoxType.Message, +#endif + null); + + CustomBlueprintBuilder.Reset(); + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/ItemEntityVendorDescriptionPatch.cs b/CraftMagicItems/Patches/Harmony/ItemEntityVendorDescriptionPatch.cs new file mode 100644 index 0000000..af20d89 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/ItemEntityVendorDescriptionPatch.cs @@ -0,0 +1,37 @@ +using CraftMagicItems.Localization; +using Kingmaker.Items; +using Kingmaker.UI.Common; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(ItemEntity), "VendorDescription", HarmonyLib.MethodType.Getter)] + // ReSharper disable once UnusedMember.Local + public static class ItemEntityVendorDescriptionPatch + { + // ReSharper disable once UnusedMember.Local + private static bool Prefix(ItemEntity __instance, ref string __result) + { + // If the "vendor" is a party member, return that the item was crafted rather than from a merchant +#if PATCH21 + if (__instance.VendorBlueprint != null && __instance.VendorBlueprint.IsCompanion) + { + foreach (var companion in UIUtility.GetGroup(true)) + { + if (companion.Blueprint == __instance.VendorBlueprint) + { + __result = LocalizationHelper.FormatLocalizedString("craftMagicItems-crafted-source-description", companion.CharacterName); + break; + } + } + return false; + } +#else + if (__instance.Vendor != null && __instance.Vendor.IsPlayerFaction) { + __result = LocalizationHelper.FormatLocalizedString("craftMagicItems-crafted-source-description", __instance.Vendor.CharacterName); + return false; + } +#endif + return true; + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/ItemEntityWeaponHoldInTwoHandsPatch.cs b/CraftMagicItems/Patches/Harmony/ItemEntityWeaponHoldInTwoHandsPatch.cs new file mode 100644 index 0000000..96905c6 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/ItemEntityWeaponHoldInTwoHandsPatch.cs @@ -0,0 +1,22 @@ +#if !PATCH21 +using HarmonyLib; +using Kingmaker.Items; +using Kingmaker.Items.Slots; +#endif + +namespace CraftMagicItems.Patches.Harmony +{ +#if !PATCH21 + [HarmonyLib.HarmonyPatch(typeof(ItemEntityWeapon), "HoldInTwoHands", MethodType.Getter)] + public static class ItemEntityWeaponHoldInTwoHandsPatch { + private static void Postfix(ItemEntityWeapon __instance, ref bool __result) { + if (!__result) { + if (__instance.IsShield && __instance.Blueprint.IsOneHandedWhichCanBeUsedWithTwoHands && __instance.Wielder != null) { + HandSlot handSlot = __instance.Wielder.Body.PrimaryHand; + __result = handSlot != null && !handSlot.HasItem; + } + } + } + } +#endif +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/LogItemDataUpdateSizePatch.cs b/CraftMagicItems/Patches/Harmony/LogItemDataUpdateSizePatch.cs new file mode 100644 index 0000000..1dca45e --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/LogItemDataUpdateSizePatch.cs @@ -0,0 +1,22 @@ +#if !PATCH21 +using Kingmaker; +using Kingmaker.Items.Slots; +using Kingmaker.UI.Log; +using Kingmaker.UnitLogic.ActivatableAbilities; +#endif + +namespace CraftMagicItems.Patches.Harmony +{ +#if !PATCH21 + [HarmonyLib.HarmonyPatch(typeof(LogDataManager.LogItemData), "UpdateSize")] + public static class LogItemDataUpdateSizePatch + { + // ReSharper disable once UnusedMember.Local + private static bool Prefix() + { + // Avoid null pointer exception when BattleLogManager not set. + return Game.Instance.UI.BattleLogManager != null; + } + } +#endif +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/MainMenuStartPatch.cs b/CraftMagicItems/Patches/Harmony/MainMenuStartPatch.cs new file mode 100644 index 0000000..3e784f6 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/MainMenuStartPatch.cs @@ -0,0 +1,444 @@ +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using CraftMagicItems.Constants; +using CraftMagicItems.Enchantments; +using CraftMagicItems.Localization; +using Kingmaker; +using Kingmaker.Blueprints; +using Kingmaker.Blueprints.Classes; +using Kingmaker.Blueprints.Classes.Selection; +using Kingmaker.Blueprints.Items; +using Kingmaker.Blueprints.Items.Armors; +using Kingmaker.Blueprints.Items.Ecnchantments; +using Kingmaker.Blueprints.Items.Equipment; +using Kingmaker.Blueprints.Items.Shields; +using Kingmaker.Blueprints.Items.Weapons; +using Kingmaker.Designers.Mechanics.Facts; +using Kingmaker.Enums.Damage; +using Kingmaker.RuleSystem; +using Kingmaker.UnitLogic.Mechanics.Components; +using Kingmaker.Utility; +using UnityEngine; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(MainMenu), "Start")] + public static class MainMenuStartPatch + { + private static bool mainMenuStarted; + + private static void InitialiseCraftingData() + { + // Read the crafting data now that ResourcesLibrary is loaded. + Main.LoadedData.ItemCraftingData = Main.ReadJsonFile($"{Main.ModEntry.Path}/Data/ItemTypes.json", new CraftingTypeConverter()); + // Initialise lookup tables. + foreach (var itemData in Main.LoadedData.ItemCraftingData) + { + if (itemData is RecipeBasedItemCraftingData recipeBased) + { + recipeBased.Recipes = recipeBased.RecipeFileNames.Aggregate(Enumerable.Empty(), + (all, fileName) => all.Concat(Main.ReadJsonFile($"{Main.ModEntry.Path}/Data/{fileName}")) + ).Where(recipe => { + return (recipe.ResultItem != null) + || (recipe.Enchantments.Length > 0) + || (recipe.NoResultItem && recipe.NoEnchantments); + }).ToArray(); + + foreach (var recipe in recipeBased.Recipes) + { + if (recipe.ResultItem != null) + { + if (recipe.NameId == null) + { + recipe.NameId = recipe.ResultItem.Name; + } + else + { + recipe.NameId = new L10NString(recipe.NameId).ToString(); + } + } + else if (recipe.NameId != null) + { + recipe.NameId = new L10NString(recipe.NameId).ToString(); + } + if (recipe.ParentNameId != null) + { + recipe.ParentNameId = new L10NString(recipe.ParentNameId).ToString(); + } + recipe.Enchantments.ForEach(enchantment => AddRecipeForEnchantment(enchantment.AssetGuid, recipe)); + if (recipe.Material != 0) + { + AddRecipeForMaterial(recipe.Material, recipe); + } + + if (recipe.ParentNameId != null) + { + recipeBased.SubRecipes = recipeBased.SubRecipes ?? new Dictionary>(); + if (!recipeBased.SubRecipes.ContainsKey(recipe.ParentNameId)) + { + recipeBased.SubRecipes[recipe.ParentNameId] = new List(); + } + + recipeBased.SubRecipes[recipe.ParentNameId].Add(recipe); + } + } + + if (recipeBased.Name.StartsWith("CraftMundane")) + { + foreach (var blueprint in recipeBased.NewItemBaseIDs) + { + if (!blueprint.AssetGuid.Contains("#CraftMagicItems")) + { + AddItemForType(blueprint); + } + } + } + } + + if (itemData.ParentNameId != null) + { + if (!Main.LoadedData.SubCraftingData.ContainsKey(itemData.ParentNameId)) + { + Main.LoadedData.SubCraftingData[itemData.ParentNameId] = new List(); + } + + Main.LoadedData.SubCraftingData[itemData.ParentNameId].Add(itemData); + } + } + + var allUsableItems = ResourcesLibrary.GetBlueprints(); + foreach (var item in allUsableItems) + { + AddItemIdForEnchantment(item); + } + + var allNonRecipeEnchantmentsInItems = ResourcesLibrary.GetBlueprints() + .Where(enchantment => !Main.LoadedData.EnchantmentIdToRecipe.ContainsKey(enchantment.AssetGuid) && Main.LoadedData.EnchantmentIdToItem.ContainsKey(enchantment.AssetGuid)) + .ToArray(); + // BlueprintEnchantment.EnchantmentCost seems to be full of nonsense values - attempt to set cost of each enchantment by using the prices of + // items with enchantments. + foreach (var enchantment in allNonRecipeEnchantmentsInItems) + { + var itemsWithEnchantment = Main.LoadedData.EnchantmentIdToItem[enchantment.AssetGuid]; + foreach (var item in itemsWithEnchantment) + { + if (Main.DoesItemMatchAllEnchantments(item, enchantment.AssetGuid)) + { + Main.LoadedData.EnchantmentIdToCost[enchantment.AssetGuid] = item.Cost; + break; + } + } + } + + foreach (var enchantment in allNonRecipeEnchantmentsInItems) + { + if (!Main.LoadedData.EnchantmentIdToCost.ContainsKey(enchantment.AssetGuid)) + { + var itemsWithEnchantment = Main.LoadedData.EnchantmentIdToItem[enchantment.AssetGuid]; + foreach (var item in itemsWithEnchantment) + { + if (ReverseEngineerEnchantmentCost(item, enchantment.AssetGuid)) + { + break; + } + } + } + } + + Main.LoadedData.CustomLootItems = Main.ReadJsonFile($"{Main.ModEntry.Path}/Data/LootItems.json"); + } + + private static void AddCraftingFeats(ObjectIDGenerator idGenerator, BlueprintProgression progression) + { + foreach (var levelEntry in progression.LevelEntries) + { + foreach (var featureBase in levelEntry.Features) + { + var selection = featureBase as BlueprintFeatureSelection; + if (selection != null && (Features.CraftingFeatGroups.Contains(selection.Group) || Features.CraftingFeatGroups.Contains(selection.Group2))) + { + // Use ObjectIDGenerator to detect which shared lists we've added the feats to. + idGenerator.GetId(selection.AllFeatures, out var firstTime); + if (firstTime) + { + foreach (var data in Main.LoadedData.ItemCraftingData) + { + if (data.FeatGuid != null) + { + var featBlueprint = ResourcesLibrary.TryGetBlueprint(data.FeatGuid) as BlueprintFeature; + var list = selection.AllFeatures.ToList(); + list.Add(featBlueprint); + selection.AllFeatures = list.ToArray(); + idGenerator.GetId(selection.AllFeatures, out firstTime); + } + } + } + } + } + } + } + + private static void AddAllCraftingFeats() + { + var idGenerator = new ObjectIDGenerator(); + // Add crafting feats to general feat selection + AddCraftingFeats(idGenerator, Game.Instance.BlueprintRoot.Progression.FeatsProgression); + // ... and to relevant class feat selections. + foreach (var characterClass in Game.Instance.BlueprintRoot.Progression.CharacterClasses) + { + AddCraftingFeats(idGenerator, characterClass.Progression); + } + + // Alchemists get Brew Potion as a bonus 1st level feat, except for Grenadier archetype alchemists. + var brewPotionData = Main.LoadedData.ItemCraftingData.First(data => data.Name == "Potion"); + var brewPotion = ResourcesLibrary.TryGetBlueprint(brewPotionData.FeatGuid); + var alchemistProgression = ResourcesLibrary.TryGetBlueprint(ClassBlueprints.AlchemistProgressionGuid); + var grenadierArchetype = ResourcesLibrary.TryGetBlueprint(ClassBlueprints.AlchemistGrenadierArchetypeGuid); + if (brewPotion != null && alchemistProgression != null && grenadierArchetype != null) + { + var firstLevelIndex = alchemistProgression.LevelEntries.FindIndex((levelEntry) => (levelEntry.Level == 1)); + alchemistProgression.LevelEntries[firstLevelIndex].Features.Add(brewPotion); + alchemistProgression.UIDeterminatorsGroup = alchemistProgression.UIDeterminatorsGroup.Concat(new[] { brewPotion }).ToArray(); + // Vanilla Grenadier has no level 1 RemoveFeatures, but a mod may have changed that, so search for it as well. + var firstLevelGrenadierRemoveIndex = grenadierArchetype.RemoveFeatures.FindIndex((levelEntry) => (levelEntry.Level == 1)); + if (firstLevelGrenadierRemoveIndex < 0) + { + var removeFeatures = new[] { new LevelEntry { Level = 1 } }; + grenadierArchetype.RemoveFeatures = removeFeatures.Concat(grenadierArchetype.RemoveFeatures).ToArray(); + firstLevelGrenadierRemoveIndex = 0; + } + grenadierArchetype.RemoveFeatures[firstLevelGrenadierRemoveIndex].Features.Add(brewPotion); + } + else + { + Main.ModEntry.Logger.Warning("Failed to locate Alchemist progression, Grenadier archetype or Brew Potion feat!"); + } + + // Scroll Savant should get Scribe Scroll as a bonus 1st level feat. + var scribeScrollData = Main.LoadedData.ItemCraftingData.First(data => data.Name == "Scroll"); + var scribeScroll = ResourcesLibrary.TryGetBlueprint(scribeScrollData.FeatGuid); + var scrollSavantArchetype = ResourcesLibrary.TryGetBlueprint(ClassBlueprints.ScrollSavantArchetypeGuid); + if (scribeScroll != null && scrollSavantArchetype != null) + { + var firstLevelAdd = scrollSavantArchetype.AddFeatures.First((levelEntry) => (levelEntry.Level == 1)); + firstLevelAdd.Features.Add(scribeScroll); + } + else + { + Main.ModEntry.Logger.Warning("Failed to locate Scroll Savant archetype or Scribe Scroll feat!"); + } + } + + private static void PatchBlueprints() + { + var shieldMaster = ResourcesLibrary.TryGetBlueprint(Features.ShieldMasterGuid); + var twoWeaponFighting = ResourcesLibrary.TryGetBlueprint(MechanicsBlueprints.TwoWeaponFightingBasicMechanicsGuid); + TwoWeaponFightingAttackPenaltyOnEventAboutToTriggerPatch.ShieldMaster = shieldMaster; + Main.Accessors.SetBlueprintUnitFactDisplayName(twoWeaponFighting) = new L10NString("e32ce256-78dc-4fd0-bf15-21f9ebdf9921"); + + for (int i = 0; i < shieldMaster.ComponentsArray.Length; i++) + { + if (shieldMaster.ComponentsArray[i] is ShieldMaster component) + { + shieldMaster.ComponentsArray[i] = Accessors.Create(a => { + a.name = component.name.Replace("ShieldMaster", "ShieldMasterPatch"); + }); + } + } + + var lightShield = ResourcesLibrary.TryGetBlueprint(ItemQualityBlueprints.WeaponLightShieldGuid); + Main.Accessors.SetBlueprintItemBaseDamage(lightShield) = new DiceFormula(1, DiceType.D3); + var heavyShield = ResourcesLibrary.TryGetBlueprint(ItemQualityBlueprints.WeaponHeavyShieldGuid); + Main.Accessors.SetBlueprintItemBaseDamage(heavyShield) = new DiceFormula(1, DiceType.D4); + + for (int i = 0; i < EnchantmentBlueprints.ItemEnchantmentGuids.Length; i++) + { + var source = ResourcesLibrary.TryGetBlueprint(EnchantmentBlueprints.ItemEnchantmentGuids[i].WeaponEnchantmentGuid); + var dest = ResourcesLibrary.TryGetBlueprint(EnchantmentBlueprints.ItemEnchantmentGuids[i].UnarmedEnchantmentGuid); + Main.Accessors.SetBlueprintItemEnchantmentEnchantName(dest) = Main.Accessors.GetBlueprintItemEnchantmentEnchantName(source); + Main.Accessors.SetBlueprintItemEnchantmentDescription(dest) = Main.Accessors.GetBlueprintItemEnchantmentDescription(source); + } + + var longshankBane = ResourcesLibrary.TryGetBlueprint(EnchantmentBlueprints.LongshankBaneGuid); + if (longshankBane.ComponentsArray.Length >= 2 && longshankBane.ComponentsArray[1] is WeaponConditionalDamageDice conditional) + { + for (int i = 0; i < conditional.Conditions.Conditions.Length; i++) + { + if (conditional.Conditions.Conditions[i] is Kingmaker.Designers.EventConditionActionSystem.Conditions.HasFact condition) + { +#if PATCH21_BETA + var replace = SerializedScriptableObject.CreateInstance(); +#else + var replace = ScriptableObject.CreateInstance(); +#endif + replace.Fact = condition.Fact; + replace.name = condition.name.Replace("HasFact", "ContextConditionHasFact"); + conditional.Conditions.Conditions[i] = replace; + } + } + } + } + + private static void InitialiseMod() + { + if (Main.modEnabled) + { + PatchBlueprints(); + LeftHandVisualDisplayPatcher.PatchLeftHandedWeaponModels(); + InitialiseCraftingData(); + AddAllCraftingFeats(); + } + } + + [HarmonyLib.HarmonyPriority(HarmonyLib.Priority.Last)] + public static void Postfix() + { + if (!mainMenuStarted) + { + mainMenuStarted = true; + InitialiseMod(); + } + } + + public static void ModEnabledChanged() + { + if (!mainMenuStarted && ResourcesLibrary.LibraryObject != null) + { + mainMenuStarted = true; + L10N.SetEnabled(true); + SustenanceEnchantment.MainMenuStartPatch.Postfix(); + WildEnchantment.MainMenuStartPatch.Postfix(); + CreateQuiverAbility.MainMenuStartPatch.Postfix(); + InitialiseMod(); + return; + } + + HarmonyPatcher patcher = new HarmonyPatcher(Main.ModEntry.Logger.Error); + + if (!Main.modEnabled) + { + // Reset everything InitialiseMod initialises + Main.LoadedData.ItemCraftingData = null; + Main.LoadedData.SubCraftingData.Clear(); + Main.LoadedData.SpellIdToItem.Clear(); + Main.LoadedData.TypeToItem.Clear(); + Main.LoadedData.EnchantmentIdToItem.Clear(); + Main.LoadedData.EnchantmentIdToCost.Clear(); + Main.LoadedData.EnchantmentIdToRecipe.Clear(); + patcher.UnpatchAllExcept(Main.MethodPatchList); + } + else if (mainMenuStarted) + { + // If the mod is enabled and we're past the Start of main menu, (re-)initialise. + patcher.PatchAllOrdered(); + InitialiseMod(); + } + L10N.SetEnabled(Main.modEnabled); + } + + /// + /// Attempt to work out the cost of enchantments which aren't in recipes by checking if blueprint, which contains the enchantment, contains only other + /// enchantments whose cost is known. + /// + public static bool ReverseEngineerEnchantmentCost(BlueprintItemEquipment blueprint, string enchantmentId) + { + if (blueprint == null || blueprint.IsNotable || blueprint.Ability != null || blueprint.ActivatableAbility != null) + { + return false; + } + + if (blueprint is BlueprintItemShield || blueprint is BlueprintItemWeapon || blueprint is BlueprintItemArmor) + { + // Cost of enchantments on arms and armor is different, and can be treated as a straight delta. + return true; + } + + var mostExpensiveEnchantmentCost = 0; + var costSum = 0; + foreach (var enchantment in blueprint.Enchantments) + { + if (enchantment.AssetGuid == enchantmentId) + { + continue; + } + + if (!Main.LoadedData.EnchantmentIdToRecipe.ContainsKey(enchantment.AssetGuid) && !Main.LoadedData.EnchantmentIdToCost.ContainsKey(enchantment.AssetGuid)) + { + return false; + } + + var enchantmentCost = Main.GetEnchantmentCost(enchantment.AssetGuid, blueprint); + costSum += enchantmentCost; + if (mostExpensiveEnchantmentCost < enchantmentCost) + { + mostExpensiveEnchantmentCost = enchantmentCost; + } + } + + var remainder = blueprint.Cost - 3 * costSum / 2; + if (remainder >= mostExpensiveEnchantmentCost) + { + // enchantmentId is the most expensive enchantment + Main.LoadedData.EnchantmentIdToCost[enchantmentId] = remainder; + } + else + { + // mostExpensiveEnchantmentCost is the most expensive enchantment + Main.LoadedData.EnchantmentIdToCost[enchantmentId] = (2 * remainder + mostExpensiveEnchantmentCost) / 3; + } + + return true; + } + + public static void AddRecipeForMaterial(PhysicalDamageMaterial material, RecipeData recipe) + { + if (!Main.LoadedData.MaterialToRecipe.ContainsKey(material)) + { + Main.LoadedData.MaterialToRecipe.Add(material, new List()); + } + if (!Main.LoadedData.MaterialToRecipe[material].Contains(recipe)) + { + Main.LoadedData.MaterialToRecipe[material].Add(recipe); + } + } + + public static void AddRecipeForEnchantment(string enchantmentId, RecipeData recipe) + { + if (!Main.LoadedData.EnchantmentIdToRecipe.ContainsKey(enchantmentId)) + { + Main.LoadedData.EnchantmentIdToRecipe.Add(enchantmentId, new List()); + } + + if (!Main.LoadedData.EnchantmentIdToRecipe[enchantmentId].Contains(recipe)) + { + Main.LoadedData.EnchantmentIdToRecipe[enchantmentId].Add(recipe); + } + } + + public static void AddItemIdForEnchantment(BlueprintItemEquipment itemBlueprint) + { + if (itemBlueprint != null) + { + foreach (var enchantment in Main.GetEnchantments(itemBlueprint)) + { + if (!Main.LoadedData.EnchantmentIdToItem.ContainsKey(enchantment.AssetGuid)) + { + Main.LoadedData.EnchantmentIdToItem[enchantment.AssetGuid] = new List(); + } + + Main.LoadedData.EnchantmentIdToItem[enchantment.AssetGuid].Add(itemBlueprint); + } + } + } + + public static void AddItemForType(BlueprintItem blueprint) + { + string assetGuid = Main.GetBlueprintItemType(blueprint); + if (!string.IsNullOrEmpty(assetGuid)) + { + Main.LoadedData.TypeToItem.Add(assetGuid, blueprint); + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/MainMenuUiContextInitializePatch.cs b/CraftMagicItems/Patches/Harmony/MainMenuUiContextInitializePatch.cs new file mode 100644 index 0000000..fe59a0e --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/MainMenuUiContextInitializePatch.cs @@ -0,0 +1,18 @@ +#if PATCH21 +using Kingmaker.Assets.UI.Context; +#endif + +namespace CraftMagicItems.Patches.Harmony +{ +#if PATCH21 + [HarmonyLib.HarmonyPatch(typeof(MainMenuUiContext), "Initialize")] + public static class MainMenuUiContextInitializePatch + { + [HarmonyLib.HarmonyPriority(HarmonyLib.Priority.Last)] + private static void Postfix() + { + MainMenuStartPatch.Postfix(); + } + } +#endif +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/MissAgainstFactOwnerOnEventAboutToTriggerPatch.cs b/CraftMagicItems/Patches/Harmony/MissAgainstFactOwnerOnEventAboutToTriggerPatch.cs new file mode 100644 index 0000000..f17e487 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/MissAgainstFactOwnerOnEventAboutToTriggerPatch.cs @@ -0,0 +1,38 @@ +using Kingmaker.Blueprints.Facts; +using Kingmaker.Blueprints.Items.Ecnchantments; +using Kingmaker.Designers.Mechanics.Facts; +using Kingmaker.RuleSystem.Rules; +using Kingmaker.UnitLogic; +using Kingmaker.Utility; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(MissAgainstFactOwner), "OnEventAboutToTrigger")] + // ReSharper disable once UnusedMember.Local + public static class MissAgainstFactOwnerOnEventAboutToTriggerPatch + { + // ReSharper disable once UnusedMember.Local + private static bool Prefix(MissAgainstFactOwner __instance, RuleAttackRoll evt) + { + if (__instance is ItemEnchantmentLogic logic) + { + if (Main.EquipmentEnchantmentValid(evt.Weapon, logic.Owner)) + { + foreach (BlueprintUnitFact blueprint in __instance.Facts) + { + if (evt.Target.Descriptor.HasFact(blueprint)) + { + evt.AutoMiss = true; + return false; + } + } + } + return false; + } + else + { + return true; + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/PlayerPostLoadPatch.cs b/CraftMagicItems/Patches/Harmony/PlayerPostLoadPatch.cs new file mode 100644 index 0000000..b2f83f4 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/PlayerPostLoadPatch.cs @@ -0,0 +1,167 @@ +using System; +using System.Linq; +using Kingmaker; +using Kingmaker.Blueprints; +using Kingmaker.Blueprints.Classes; +using Kingmaker.Blueprints.Items; +using Kingmaker.Blueprints.Loot; +using Kingmaker.Items; +using Kingmaker.UI.Common; +using UnityEngine; + +namespace CraftMagicItems.Patches.Harmony +{ + public static class PlayerPostLoadPatch + { + private static void Postfix() + { + Main.ItemUpgradeProjects.Clear(); + Main.ItemCreationProjects.Clear(); + + var characterList = UIUtility.GetGroup(true); + foreach (var character in characterList) + { + // If the mod is disabled, this will clean up crafting timer "buff" from all casters. + var timer = Main.GetCraftingTimerComponentForCaster(character.Descriptor, character.IsMainCharacter); + var bondedItemComponent = Main.GetBondedItemComponentForCaster(character.Descriptor); + + if (!Main.modEnabled) + { + continue; + } + + if (timer != null) + { + foreach (var project in timer.CraftingProjects) + { + if (project.ItemBlueprint != null) + { + // Migrate all projects using ItemBlueprint to use ResultItem + var craftingData = Main.LoadedData.ItemCraftingData.First(data => data.Name == project.ItemType); + project.ResultItem = Main.BuildItemEntity(project.ItemBlueprint, craftingData, character); + project.ItemBlueprint = null; + } + + project.Crafter = character; + if (!project.ResultItem.HasUniqueVendor) + { + // Set "vendor" of item if it's already in progress + project.ResultItem.SetVendorIfNull(character); + } + project.ResultItem.PostLoad(); + + if (project.UpgradeItem == null) + { + Main.ItemCreationProjects.Add(project); + } + else + { + Main.ItemUpgradeProjects[project.UpgradeItem] = project; + project.UpgradeItem.PostLoad(); + } + } + + if (character.IsMainCharacter) + { + UpgradeSave(string.IsNullOrEmpty(timer.Version) ? null : Version.Parse(timer.Version)); + timer.Version = Main.ModEntry.Version.ToString(); + } + } + + if (bondedItemComponent != null) + { + bondedItemComponent.ownerItem?.PostLoad(); + bondedItemComponent.everyoneElseItem?.PostLoad(); + } + + // Retroactively give character any crafting feats in their past progression data which they don't actually have + // (e.g. Alchemists getting Brew Potion) + foreach (var characterClass in character.Descriptor.Progression.Classes) + { + foreach (var levelData in characterClass.CharacterClass.Progression.LevelEntries) + { + if (levelData.Level <= characterClass.Level) + { + foreach (var feature in levelData.Features.OfType()) + { + if (feature.AssetGuid.Contains("#CraftMagicItems(feat=") && !Main.CharacterHasFeat(character, feature.AssetGuid)) + { + character.Descriptor.Progression.Features.AddFeature(feature); + } + } + } + } + } + } + } + + private static void AddToLootTables(BlueprintItem blueprint, string[] tableNames, bool firstTime) + { + var tableCount = tableNames.Length; + foreach (var loot in ResourcesLibrary.GetBlueprints()) + { + if (tableNames.Contains(loot.name)) + { + tableCount--; + if (!loot.Items.Any(entry => entry.Item == blueprint)) + { + var lootItems = loot.Items.ToList(); + lootItems.Add(new LootEntry { Count = 1, Item = blueprint }); + loot.Items = lootItems.ToArray(); + } + } + } + foreach (var unitLoot in ResourcesLibrary.GetBlueprints()) + { + if (tableNames.Contains(unitLoot.name)) + { + tableCount--; + if (unitLoot is BlueprintSharedVendorTable vendor) + { + if (firstTime) + { + var vendorTable = Game.Instance.Player.SharedVendorTables.GetTable(vendor); + vendorTable.Add(blueprint.CreateEntity()); + } + } + else if (!unitLoot.ComponentsArray.Any(component => component is LootItemsPackFixed pack && pack.Item.Item == blueprint)) + { + var lootItem = new LootItem(); + Main.Accessors.SetLootItemItem(lootItem) = blueprint; +#if PATCH21_BETA + var lootComponent = SerializedScriptableObject.CreateInstance(); +#else + var lootComponent = ScriptableObject.CreateInstance(); +#endif + Main.Accessors.SetLootItemsPackFixedItem(lootComponent) = lootItem; + Main.blueprintPatcher.EnsureComponentNameUnique(lootComponent, unitLoot.ComponentsArray); + var components = unitLoot.ComponentsArray.ToList(); + components.Add(lootComponent); + unitLoot.ComponentsArray = components.ToArray(); + } + } + } + if (tableCount > 0) + { + HarmonyLib.FileLog.Log($"!!! Failed to match all loot table names for {blueprint.Name}. {tableCount} table names not found."); + } + } + + public static void UpgradeSave(Version version) + { + foreach (var lootItem in Main.LoadedData.CustomLootItems) + { + var firstTime = (version == null || version.CompareTo(lootItem.AddInVersion) < 0); + var item = ResourcesLibrary.TryGetBlueprint(lootItem.AssetGuid); + if (item == null) + { + HarmonyLib.FileLog.Log($"!!! Loot item not created: {lootItem.AssetGuid}"); + } + else + { + AddToLootTables(item, lootItem.LootTables, firstTime); + } + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/QuiverAbility.cs b/CraftMagicItems/Patches/Harmony/QuiverAbility.cs similarity index 79% rename from CraftMagicItems/QuiverAbility.cs rename to CraftMagicItems/Patches/Harmony/QuiverAbility.cs index 7e8ba2a..6e260b6 100644 --- a/CraftMagicItems/QuiverAbility.cs +++ b/CraftMagicItems/Patches/Harmony/QuiverAbility.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Reflection; using System.Reflection.Emit; -using Harmony12; +using CraftMagicItems.Localization; using Kingmaker; #if PATCH21 using Kingmaker.Assets.UI.Context; @@ -19,11 +19,11 @@ using Object = UnityEngine.Object; #endif -namespace CraftMagicItems { +namespace CraftMagicItems.Patches.Harmony { public class CreateQuiverAbility : ScriptableObject { private static bool initialised; - [Harmony12.HarmonyPatch(typeof(MainMenu), "Start")] + [HarmonyLib.HarmonyPatch(typeof(MainMenu), "Start")] // ReSharper disable once UnusedMember.Local public static class MainMenuStartPatch { private static void AddQuiver(BlueprintActivatableAbility ability, BlueprintBuff buff, string guid, PhysicalDamageMaterial material) { @@ -43,8 +43,8 @@ private static void AddQuiver(BlueprintActivatableAbility ability, BlueprintBuff #endif quiverBuff.ComponentsArray = new BlueprintComponent[] { component }; - Main.Accessors.SetBlueprintUnitFactDisplayName(quiverBuff, new L10NString($"craftMagicItems-mundane-{material.ToString().ToLower()}-quiver-name")); - Main.Accessors.SetBlueprintUnitFactDescription(quiverBuff, new L10NString($"craftMagicItems-mundane-{material.ToString().ToLower()}-quiver-description")); + Main.Accessors.SetBlueprintUnitFactDisplayName(quiverBuff) = new L10NString($"craftMagicItems-mundane-{material.ToString().ToLower()}-quiver-name"); + Main.Accessors.SetBlueprintUnitFactDescription(quiverBuff) = new L10NString($"craftMagicItems-mundane-{material.ToString().ToLower()}-quiver-description"); #if PATCH21_BETA quiverBuff.OnEnable(); foreach (var c in quiverBuff.ComponentsArray) { @@ -54,7 +54,7 @@ private static void AddQuiver(BlueprintActivatableAbility ability, BlueprintBuff var buffGuid = $"{guid}#CraftMagicItems({material.ToString()}QuiverBuff)"; - Main.Accessors.SetBlueprintScriptableObjectAssetGuid(quiverBuff, buffGuid); + Main.Accessors.SetBlueprintScriptableObjectAssetGuid(quiverBuff) = buffGuid; ResourcesLibrary.LibraryObject.BlueprintsByAssetId?.Add(buffGuid, quiverBuff); ResourcesLibrary.LibraryObject.GetAllBlueprints()?.Add(quiverBuff); @@ -65,8 +65,8 @@ private static void AddQuiver(BlueprintActivatableAbility ability, BlueprintBuff #endif quiverAbility.Buff = quiverBuff; - Main.Accessors.SetBlueprintUnitFactDisplayName(quiverAbility, new L10NString($"craftMagicItems-mundane-{material.ToString().ToLower()}-quiver-name")); - Main.Accessors.SetBlueprintUnitFactDescription(quiverAbility, new L10NString($"craftMagicItems-mundane-{material.ToString().ToLower()}-quiver-description")); + Main.Accessors.SetBlueprintUnitFactDisplayName(quiverAbility) = new L10NString($"craftMagicItems-mundane-{material.ToString().ToLower()}-quiver-name"); + Main.Accessors.SetBlueprintUnitFactDescription(quiverAbility) = new L10NString($"craftMagicItems-mundane-{material.ToString().ToLower()}-quiver-description"); #if PATCH21_BETA quiverBuff.OnEnable(); foreach (var c in quiverAbility.ComponentsArray) { @@ -76,7 +76,7 @@ private static void AddQuiver(BlueprintActivatableAbility ability, BlueprintBuff var abilityGuid = $"{guid}#CraftMagicItems({material.ToString()}QuiverAbility)"; - Main.Accessors.SetBlueprintScriptableObjectAssetGuid(quiverAbility, abilityGuid); + Main.Accessors.SetBlueprintScriptableObjectAssetGuid(quiverAbility) = abilityGuid; ResourcesLibrary.LibraryObject.BlueprintsByAssetId?.Add(abilityGuid, quiverAbility); ResourcesLibrary.LibraryObject.GetAllBlueprints()?.Add(quiverAbility); } @@ -97,7 +97,7 @@ public static void Postfix() { } #if PATCH21 - [Harmony12.HarmonyPatch(typeof(MainMenuUiContext), "Initialize")] + [HarmonyLib.HarmonyPatch(typeof(MainMenuUiContext), "Initialize")] private static class MainMenuUiContextInitializePatch { private static void Postfix() { MainMenuStartPatch.Postfix(); @@ -105,17 +105,17 @@ private static void Postfix() { } #endif - [Harmony12.HarmonyPatch(typeof(ItemSlot), "InsertItem")] + [HarmonyLib.HarmonyPatch(typeof(ItemSlot), "InsertItem")] // ReSharper disable once UnusedMember.Local private static class ItemSlotInsertItemPatch { - static readonly MethodInfo methodToReplace = AccessTools.Property(typeof(ItemEntity), "IsStackable").GetGetMethod(); + static readonly MethodInfo methodToReplace = HarmonyLib.AccessTools.Property(typeof(ItemEntity), "IsStackable").GetGetMethod(); static readonly string quiverGuid = "25f9b5ef564cbef49a1e54c48e67dfc1#CraftMagicItems"; // ReSharper disable once UnusedMember.Local - private static IEnumerable Transpiler(IEnumerable instructions) { + private static IEnumerable Transpiler(IEnumerable instructions) { foreach (var inst in instructions) { if (inst.opcode == OpCodes.Callvirt && inst.operand as MethodInfo == methodToReplace) { - yield return new Harmony12.CodeInstruction(OpCodes.Call, new Func(IsStackable).Method); + yield return new HarmonyLib.CodeInstruction(OpCodes.Call, new Func(IsStackable).Method); } else { yield return inst; } diff --git a/CraftMagicItems/Patches/Harmony/RestControllerApplyRestPatch.cs b/CraftMagicItems/Patches/Harmony/RestControllerApplyRestPatch.cs new file mode 100644 index 0000000..34804d2 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/RestControllerApplyRestPatch.cs @@ -0,0 +1,17 @@ +using Kingmaker.Controllers.Rest; +using Kingmaker.UnitLogic; + +namespace CraftMagicItems.Patches.Harmony +{ + // Make characters in the party work on their crafting projects when they rest. + [HarmonyLib.HarmonyPatch(typeof(RestController), "ApplyRest")] + // ReSharper disable once UnusedMember.Local + public static class RestControllerApplyRestPatch + { + // ReSharper disable once UnusedMember.Local + private static void Prefix(UnitDescriptor unit) + { + CraftingLogic.WorkOnProjects(unit, false); + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/RuleCalculateAttacksCountOnTriggerPatch.cs b/CraftMagicItems/Patches/Harmony/RuleCalculateAttacksCountOnTriggerPatch.cs new file mode 100644 index 0000000..2953e77 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/RuleCalculateAttacksCountOnTriggerPatch.cs @@ -0,0 +1,49 @@ +#if !PATCH21 +using System; +using Kingmaker.Blueprints.Items.Weapons; +using Kingmaker.Items; +using Kingmaker.Items.Slots; +using Kingmaker.RuleSystem.Rules; +#endif + +namespace CraftMagicItems.Patches.Harmony +{ +#if !PATCH21 + [HarmonyLib.HarmonyPatch(typeof(RuleCalculateAttacksCount), "OnTrigger")] + public static class RuleCalculateAttacksCountOnTriggerPatch + { + private static void Postfix(RuleCalculateAttacksCount __instance) + { + int num = __instance.Initiator.Stats.BaseAttackBonus; + int val = Math.Min(Math.Max(0, num / 5 - ((num % 5 != 0) ? 0 : 1)), 3); + HandSlot primaryHand = __instance.Initiator.Body.PrimaryHand; + HandSlot secondaryHand = __instance.Initiator.Body.SecondaryHand; + ItemEntityWeapon maybeWeapon = primaryHand.MaybeWeapon; + BlueprintItemWeapon blueprintItemWeapon = (maybeWeapon != null) ? maybeWeapon.Blueprint : null; + BlueprintItemWeapon blueprintItemWeapon2; + if (secondaryHand.MaybeShield != null) + { + if (__instance.Initiator.Descriptor.State.Features.ShieldBash) + { + ItemEntityWeapon weaponComponent = secondaryHand.MaybeShield.WeaponComponent; + blueprintItemWeapon2 = ((weaponComponent != null) ? weaponComponent.Blueprint : null); + } + else + { + blueprintItemWeapon2 = null; + } + } + else + { + ItemEntityWeapon maybeWeapon2 = secondaryHand.MaybeWeapon; + blueprintItemWeapon2 = ((maybeWeapon2 != null) ? maybeWeapon2.Blueprint : null); + } + + if ((primaryHand.MaybeWeapon == null || !primaryHand.MaybeWeapon.HoldInTwoHands) && (blueprintItemWeapon == null || blueprintItemWeapon.IsUnarmed) && blueprintItemWeapon2 && !blueprintItemWeapon2.IsUnarmed) + { + __instance.SecondaryHand.PenalizedAttacks += Math.Max(0, val); + } + } + } +#endif +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/ShieldMasterPatch.cs b/CraftMagicItems/Patches/Harmony/ShieldMasterPatch.cs new file mode 100644 index 0000000..d33f0b7 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/ShieldMasterPatch.cs @@ -0,0 +1,81 @@ +#if PATCH21 +#endif +using Kingmaker.Blueprints; +using Kingmaker.Designers; +#if !PATCH21 +using Kingmaker.Items.Slots; +#endif +using Kingmaker.PubSubSystem; +using Kingmaker.RuleSystem.Rules; +using Kingmaker.RuleSystem.Rules.Damage; +#if !PATCH21 +using Kingmaker.UnitLogic.ActivatableAbilities; +#endif + +namespace CraftMagicItems.Patches.Harmony +{ + [AllowMultipleComponents] + public class ShieldMasterPatch : GameLogicComponent, IInitiatorRulebookHandler, IInitiatorRulebookHandler, IInitiatorRulebookHandler + { + public void OnEventAboutToTrigger(RuleCalculateDamage evt) + { + if (!evt.Initiator.Body.SecondaryHand.HasShield || evt.DamageBundle.Weapon == null || !evt.DamageBundle.Weapon.IsShield) + { + return; + } + + var armorEnhancementBonus = GameHelper.GetItemEnhancementBonus(evt.Initiator.Body.SecondaryHand.Shield.ArmorComponent); + var weaponEnhancementBonus = GameHelper.GetItemEnhancementBonus(evt.Initiator.Body.SecondaryHand.Shield.WeaponComponent); + if (weaponEnhancementBonus == 0 && evt.Initiator.Body.SecondaryHand.Shield.WeaponComponent.Blueprint.IsMasterwork) + { + weaponEnhancementBonus = 1; + } + + var itemEnhancementBonus = armorEnhancementBonus - weaponEnhancementBonus; + PhysicalDamage physicalDamage = evt.DamageBundle.WeaponDamage as PhysicalDamage; + if (physicalDamage != null && itemEnhancementBonus > 0) + { + physicalDamage.Enchantment += itemEnhancementBonus; + physicalDamage.EnchantmentTotal += itemEnhancementBonus; + } + } + + public void OnEventDidTrigger(RuleCalculateWeaponStats evt) { } + + public void OnEventAboutToTrigger(RuleCalculateWeaponStats evt) + { + if (!evt.Initiator.Body.SecondaryHand.HasShield || evt.Weapon == null || !evt.Weapon.IsShield) + { + return; + } + var armorEnhancementBonus = GameHelper.GetItemEnhancementBonus(evt.Initiator.Body.SecondaryHand.Shield.ArmorComponent); + var weaponEnhancementBonus = GameHelper.GetItemEnhancementBonus(evt.Initiator.Body.SecondaryHand.Shield.WeaponComponent); + var itemEnhancementBonus = armorEnhancementBonus - weaponEnhancementBonus; + if (itemEnhancementBonus > 0) + { + evt.AddBonusDamage(itemEnhancementBonus); + } + } + + public void OnEventDidTrigger(RuleCalculateDamage evt) { } + + public void OnEventAboutToTrigger(RuleCalculateAttackBonusWithoutTarget evt) + { + if (!evt.Initiator.Body.SecondaryHand.HasShield || evt.Weapon == null || !evt.Weapon.IsShield) + { + return; + } + + var armorEnhancementBonus = GameHelper.GetItemEnhancementBonus(evt.Initiator.Body.SecondaryHand.Shield.ArmorComponent); + var weaponEnhancementBonus = GameHelper.GetItemEnhancementBonus(evt.Initiator.Body.SecondaryHand.Shield.WeaponComponent); + var num = armorEnhancementBonus - weaponEnhancementBonus; + + if (num > 0) + { + evt.AddBonus(num, base.Fact); + } + } + + public void OnEventDidTrigger(RuleCalculateAttackBonusWithoutTarget evt) { } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/SpellbookPostLoadPatch.cs b/CraftMagicItems/Patches/Harmony/SpellbookPostLoadPatch.cs new file mode 100644 index 0000000..a2c63e7 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/SpellbookPostLoadPatch.cs @@ -0,0 +1,35 @@ +using Kingmaker.UnitLogic; + +namespace CraftMagicItems.Patches.Harmony +{ + // Load Variant spells into m_KnownSpellLevels + [HarmonyLib.HarmonyPatch(typeof(Spellbook), "PostLoad")] + // ReSharper disable once UnusedMember.Local + public static class SpellbookPostLoadPatch + { + // ReSharper disable once UnusedMember.Local + private static void Postfix(Spellbook __instance) + { + if (!Main.modEnabled) + { + return; + } + + var mKnownSpells = Main.Accessors.GetSpellbookKnownSpells(__instance); + var mKnownSpellLevels = Main.Accessors.GetSpellbookKnownSpellLevels(__instance); + for (var level = 0; level < mKnownSpells.Length; ++level) + { + foreach (var spell in mKnownSpells[level]) + { + if (spell.Blueprint.Variants != null) + { + foreach (var variant in spell.Blueprint.Variants) + { + mKnownSpellLevels[variant] = level; + } + } + } + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/TwoWeaponFightingAttackPenaltyOnEventAboutToTriggerPatch.cs b/CraftMagicItems/Patches/Harmony/TwoWeaponFightingAttackPenaltyOnEventAboutToTriggerPatch.cs new file mode 100644 index 0000000..46f5856 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/TwoWeaponFightingAttackPenaltyOnEventAboutToTriggerPatch.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +#if PATCH21 +#endif +using Kingmaker.Blueprints.Classes; +using Kingmaker.Designers.Mechanics.Facts; +using Kingmaker.Items; +#if !PATCH21 +using Kingmaker.Items.Slots; +#endif +using Kingmaker.RuleSystem.Rules; +#if !PATCH21 +using Kingmaker.UnitLogic.ActivatableAbilities; +#endif + +namespace CraftMagicItems.Patches.Harmony +{ + + [HarmonyLib.HarmonyPatch(typeof(TwoWeaponFightingAttackPenalty), "OnEventAboutToTrigger", new Type[] { typeof(RuleCalculateAttackBonusWithoutTarget) })] + public static class TwoWeaponFightingAttackPenaltyOnEventAboutToTriggerPatch + { + static public BlueprintFeature ShieldMaster; + static MethodInfo methodToFind; + private static bool Prepare() + { + try + { + methodToFind = HarmonyLib.AccessTools.Property(typeof(ItemEntityWeapon), nameof(ItemEntityWeapon.IsShield)).GetGetMethod(); + } + catch (Exception ex) + { + Main.ModEntry.Logger.Log($"Error Preparing: {ex.Message}"); + return false; + } + return true; + } + private static IEnumerable Transpiler(IEnumerable instructions, ILGenerator il) + { + Label start = il.DefineLabel(); + yield return new HarmonyLib.CodeInstruction(OpCodes.Ldarg_0); + yield return new HarmonyLib.CodeInstruction(OpCodes.Ldarg_1); + yield return new HarmonyLib.CodeInstruction(OpCodes.Call, new Func(CheckShieldMaster).Method); + yield return new HarmonyLib.CodeInstruction(OpCodes.Brfalse_S, start); + yield return new HarmonyLib.CodeInstruction(OpCodes.Ret); + var skip = 0; + HarmonyLib.CodeInstruction prev = instructions.First(); + prev.labels.Add(start); + foreach (var inst in instructions.Skip(1)) + { + if (prev.opcode == OpCodes.Ldloc_1 && inst.opcode == OpCodes.Callvirt && inst.operand as MethodInfo == methodToFind) + { + // ldloc.1 + // callvirt instance bool Kingmaker.Items.ItemEntityWeapon::get_IsShield() + // brtrue.s IL_0152 + skip = 3; + } + if (skip > 0) + { + skip--; + } + else + { + yield return prev; + } + prev = inst; + } + if (skip == 0) + { + yield return prev; + } + } + + private static bool CheckShieldMaster(TwoWeaponFightingAttackPenalty component, RuleCalculateAttackBonusWithoutTarget evt) + { + ItemEntityWeapon maybeWeapon2 = evt.Initiator.Body.SecondaryHand.MaybeWeapon; +#if !PATCH21 + RuleAttackWithWeapon ruleAttackWithWeapon = evt.Reason.Rule as RuleAttackWithWeapon; + if (ruleAttackWithWeapon != null && !ruleAttackWithWeapon.IsFullAttack) + return true; +#endif + return maybeWeapon2 != null && evt.Weapon == maybeWeapon2 && maybeWeapon2.IsShield && component.Owner.Progression.Features.HasFact(ShieldMaster); + } + } +} diff --git a/CraftMagicItems/Patches/Harmony/UIUtilityIsMagicItem.cs b/CraftMagicItems/Patches/Harmony/UIUtilityIsMagicItem.cs new file mode 100644 index 0000000..1db8562 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/UIUtilityIsMagicItem.cs @@ -0,0 +1,23 @@ +using Kingmaker.Items; +using Kingmaker.UI.Common; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(UIUtility), "IsMagicItem")] + // ReSharper disable once UnusedMember.Local + public static class UIUtilityIsMagicItem + { + // ReSharper disable once UnusedMember.Local + private static void Postfix(ItemEntity item, ref bool __result) + { + if (__result == false + && item != null + && item.IsIdentified + && item is ItemEntityShield shield + && shield.WeaponComponent != null) + { + __result = Main.ItemPlusEquivalent(shield.WeaponComponent.Blueprint) > 0; + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/UIUtilityItemFillArmorEnchantmentsPatch.cs b/CraftMagicItems/Patches/Harmony/UIUtilityItemFillArmorEnchantmentsPatch.cs new file mode 100644 index 0000000..13f5bc4 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/UIUtilityItemFillArmorEnchantmentsPatch.cs @@ -0,0 +1,46 @@ +using Kingmaker.Blueprints; +using Kingmaker.Designers.Mechanics.EquipmentEnchants; +using Kingmaker.EntitySystem.Stats; +using Kingmaker.Enums; +using Kingmaker.Items; +using Kingmaker.UI.Common; +using Kingmaker.UI.Tooltip; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(UIUtilityItem), "FillArmorEnchantments")] + // ReSharper disable once UnusedMember.Local + public static class UIUtilityItemFillArmorEnchantmentsPatch + { + // ReSharper disable once UnusedMember.Local + private static void Postfix(TooltipData data, ItemEntityShield armor) + { + if (armor.IsIdentified) + { + foreach (var itemEnchantment in armor.Enchantments) + { + itemEnchantment.Blueprint.CallComponents(c => + { + if (c.Descriptor != ModifierDescriptor.ArmorEnhancement && c.Descriptor != ModifierDescriptor.ShieldEnhancement && !data.StatBonus.ContainsKey(c.Stat)) + { + data.StatBonus.Add(c.Stat, UIUtility.AddSign(c.Value)); + } + }); + + var component = itemEnchantment.Blueprint.GetComponent(); + if (component != null) + { + StatType[] saves = { StatType.SaveReflex, StatType.SaveWill, StatType.SaveFortitude }; + foreach (var save in saves) + { + if (!data.StatBonus.ContainsKey(save)) + { + data.StatBonus.Add(save, UIUtility.AddSign(component.Value)); + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/UIUtilityItemFillEnchantmentDescriptionPatch.cs b/CraftMagicItems/Patches/Harmony/UIUtilityItemFillEnchantmentDescriptionPatch.cs new file mode 100644 index 0000000..fb0b02a --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/UIUtilityItemFillEnchantmentDescriptionPatch.cs @@ -0,0 +1,157 @@ +using System; +using System.Linq; +using CraftMagicItems.Constants; +using Kingmaker.Blueprints; +using Kingmaker.Blueprints.Items.Ecnchantments; +using Kingmaker.Designers; +using Kingmaker.Designers.Mechanics.EquipmentEnchants; +using Kingmaker.Items; +using Kingmaker.UI.Common; +using Kingmaker.UI.Tooltip; +using Kingmaker.Utility; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(UIUtilityItem), "FillEnchantmentDescription")] + // ReSharper disable once UnusedMember.Local + public static class UIUtilityItemFillEnchantmentDescriptionPatch + { + // ReSharper disable once UnusedMember.Local + private static bool Prefix(ItemEntity item, TooltipData data, ref string __result) + { + string text = string.Empty; + if (item is ItemEntityShield shield && shield.IsIdentified) + { + // It appears that shields are not properly identified when found. + shield.ArmorComponent.Identify(); + shield.WeaponComponent?.Identify(); + return true; + } + else if (item.Blueprint.ItemType == ItemsFilter.ItemType.Neck && Main.ItemPlusEquivalent(item.Blueprint) > 0) + { + if (item.IsIdentified) + { + foreach (ItemEnchantment itemEnchantment in item.Enchantments) + { + itemEnchantment.Blueprint.CallComponents(c => + { + if (!data.StatBonus.ContainsKey(c.Stat)) + { + data.StatBonus.Add(c.Stat, UIUtility.AddSign(c.Value)); + } + }); + + if (!data.Texts.ContainsKey(TooltipElement.Qualities)) + { + data.Texts[TooltipElement.Qualities] = Main.Accessors.CallUIUtilityItemGetQualities(item); + } + + if (!string.IsNullOrEmpty(itemEnchantment.Blueprint.Description)) + { + text += string.Format("{0}\n", itemEnchantment.Blueprint.Name); + text = text + itemEnchantment.Blueprint.Description + "\n\n"; + } + } + + if (item.Enchantments.Any() && !data.Texts.ContainsKey(TooltipElement.Qualities)) + { + data.Texts[TooltipElement.Qualities] = GetEnhancementBonus(item); + } + + if (GetItemEnhancementBonus(item) > 0) + { + data.Texts[TooltipElement.Enhancement] = GetEnhancementBonus(item); + } + } + __result = text; + return false; + } + else + { + return true; + } + } + + private static string GetEnhancementBonus(ItemEntity item) + { + if (!item.IsIdentified) + { + return string.Empty; + } + int itemEnhancementBonus = GetItemEnhancementBonus(item); + return (itemEnhancementBonus == 0) ? string.Empty : UIUtility.AddSign(itemEnhancementBonus); + } + + public static int GetItemEnhancementBonus(ItemEntity item) + { + return item.Enchantments.SelectMany((ItemEnchantment f) => f.SelectComponents()).Aggregate(0, (int s, EquipmentWeaponTypeEnhancement e) => s + e.Enhancement); + } + + private static void Postfix(ItemEntity item, TooltipData data, ref string __result) + { + if (item is ItemEntityShield shield) + { + if (shield.WeaponComponent != null) + { + TooltipData tmp = new TooltipData(); + string result = Main.Accessors.CallUIUtilityItemFillEnchantmentDescription(shield.WeaponComponent, tmp); + if (!string.IsNullOrEmpty(result)) + { + __result += $"{LocalizedStringBlueprints.ShieldBashLocalized}\n"; + __result += result; + } + + data.Texts[TooltipElement.AttackType] = tmp.Texts[TooltipElement.AttackType]; + data.Texts[TooltipElement.ProficiencyGroup] = tmp.Texts[TooltipElement.ProficiencyGroup]; + if (tmp.Texts.ContainsKey(TooltipElement.Qualities) && !string.IsNullOrEmpty(tmp.Texts[TooltipElement.Qualities])) + { + if (data.Texts.ContainsKey(TooltipElement.Qualities)) + { + data.Texts[TooltipElement.Qualities] += $", {LocalizedStringBlueprints.ShieldBashLocalized}: {tmp.Texts[TooltipElement.Qualities]}"; + } + else + { + data.Texts[TooltipElement.Qualities] = $"{LocalizedStringBlueprints.ShieldBashLocalized}: {tmp.Texts[TooltipElement.Qualities]}"; + } + } + + data.Texts[TooltipElement.Damage] = tmp.Texts[TooltipElement.Damage]; + if (tmp.Texts.ContainsKey(TooltipElement.EquipDamage)) + { + data.Texts[TooltipElement.EquipDamage] = tmp.Texts[TooltipElement.EquipDamage]; + } + if (tmp.Texts.ContainsKey(TooltipElement.PhysicalDamage)) + { + data.Texts[TooltipElement.PhysicalDamage] = tmp.Texts[TooltipElement.PhysicalDamage]; + data.PhysicalDamage = tmp.PhysicalDamage; + } + data.Energy = tmp.Energy; + data.OtherDamage = tmp.OtherDamage; + data.Texts[TooltipElement.Range] = tmp.Texts[TooltipElement.Range]; + data.Texts[TooltipElement.CriticalHit] = tmp.Texts[TooltipElement.CriticalHit]; + if (tmp.Texts.ContainsKey(TooltipElement.Enhancement)) + { + data.Texts[TooltipElement.Enhancement] = tmp.Texts[TooltipElement.Enhancement]; + } + } + + if (GameHelper.GetItemEnhancementBonus(shield.ArmorComponent) > 0) + { + if (data.Texts.ContainsKey(TooltipElement.Damage)) + { + data.Texts[Enum.GetValues(typeof(TooltipElement)).Cast().Max() + 1] = UIUtility.AddSign(GameHelper.GetItemEnhancementBonus(shield.ArmorComponent)); + } + else + { + data.Texts[TooltipElement.Enhancement] = UIUtility.AddSign(GameHelper.GetItemEnhancementBonus(shield.ArmorComponent)); + } + } + } + + if (data.Texts.ContainsKey(TooltipElement.Qualities)) + { + data.Texts[TooltipElement.Qualities] = data.Texts[TooltipElement.Qualities].Replace(" ,", ","); + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/UIUtilityItemGetQualitiesPatch.cs b/CraftMagicItems/Patches/Harmony/UIUtilityItemGetQualitiesPatch.cs new file mode 100644 index 0000000..d1d9d3e --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/UIUtilityItemGetQualitiesPatch.cs @@ -0,0 +1,36 @@ +using Kingmaker.Blueprints.Root; +using Kingmaker.Enums; +using Kingmaker.Items; +using Kingmaker.UI.Common; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(UIUtilityItem), "GetQualities")] + // ReSharper disable once UnusedMember.Local + public static class UIUtilityItemGetQualitiesPatch + { + // ReSharper disable once UnusedMember.Local + private static void Postfix(ItemEntity item, ref string __result) + { + if (!item.IsIdentified) + { + return; + } + + ItemEntityWeapon itemEntityWeapon = item as ItemEntityWeapon; + if (itemEntityWeapon == null) + { + return; + } + + WeaponCategory category = itemEntityWeapon.Blueprint.Category; + if (category.HasSubCategory(WeaponSubCategory.Finessable) && Main.IsOversized(itemEntityWeapon.Blueprint)) + { + __result = __result.Replace(LocalizedTexts.Instance.WeaponSubCategories.GetText(WeaponSubCategory.Finessable), ""); + __result = __result.Replace(", ,", ","); + char[] charsToTrim = { ',', ' ' }; + __result = __result.Trim(charsToTrim); + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/UnitUseSpellsOnRestGetUnitWithMaxDamagePatch.cs b/CraftMagicItems/Patches/Harmony/UnitUseSpellsOnRestGetUnitWithMaxDamagePatch.cs new file mode 100644 index 0000000..fa486de --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/UnitUseSpellsOnRestGetUnitWithMaxDamagePatch.cs @@ -0,0 +1,25 @@ +using Kingmaker.EntitySystem.Entities; +using Kingmaker.UnitLogic; +using Kingmaker.UnitLogic.Parts; + +namespace CraftMagicItems.Patches.Harmony +{ + /// + /// Owlcat's code doesn't filter out undamaged characters, so it will always return someone. This meant that with the "auto-cast healing" camping + /// option enabled on, healers would burn all their spell slots healing undamaged characters when they started resting, leaving them no spells to cast + /// when crafting. Change it so it returns null if the most damaged character is undamaged. + /// + [HarmonyLib.HarmonyPatch(typeof(UnitUseSpellsOnRest), "GetUnitWithMaxDamage")] + // ReSharper disable once UnusedMember.Local + public static class UnitUseSpellsOnRestGetUnitWithMaxDamagePatch + { + // ReSharper disable once UnusedMember.Local + private static void Postfix(ref UnitEntityData __result) + { + if (__result.Damage == 0 && (UnitPartDualCompanion.GetPair(__result)?.Damage ?? 0) == 0) + { + __result = null; + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/UnitViewHandSlotDataWeaponScalePatch.cs b/CraftMagicItems/Patches/Harmony/UnitViewHandSlotDataWeaponScalePatch.cs new file mode 100644 index 0000000..e5cd8f6 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/UnitViewHandSlotDataWeaponScalePatch.cs @@ -0,0 +1,36 @@ +using System.Linq; +using CraftMagicItems.Constants; +using Kingmaker.Blueprints; +using Kingmaker.Items; +using Kingmaker.Utility; +using Kingmaker.View.Equipment; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(UnitViewHandSlotData), "OwnerWeaponScale", HarmonyLib.MethodType.Getter)] + public static class UnitViewHandSlotDataWeaponScalePatch + { + private static void Postfix(UnitViewHandSlotData __instance, ref float __result) + { + if (__instance.VisibleItem is ItemEntityWeapon weapon && !weapon.Blueprint.AssetGuid.Contains(",visual=")) + { + var enchantment = Main.GetEnchantments(weapon.Blueprint).FirstOrDefault(e => e.AssetGuid.StartsWith(ItemQualityBlueprints.OversizedGuid)); + if (enchantment != null) + { + var component = enchantment.GetComponent(); + if (component != null) + { + if (component.SizeCategoryChange > 0) + { + __result *= 4.0f / 3.0f; + } + else if (component.SizeCategoryChange < 0) + { + __result *= 0.75f; + } + } + } + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/WeaponBaseSizeChange.cs b/CraftMagicItems/Patches/Harmony/WeaponBaseSizeChange.cs similarity index 72% rename from CraftMagicItems/WeaponBaseSizeChange.cs rename to CraftMagicItems/Patches/Harmony/WeaponBaseSizeChange.cs index f4a0128..e69c9df 100644 --- a/CraftMagicItems/WeaponBaseSizeChange.cs +++ b/CraftMagicItems/Patches/Harmony/WeaponBaseSizeChange.cs @@ -1,16 +1,11 @@ +using CraftMagicItems.Localization; using Kingmaker.Blueprints; -using Kingmaker.Blueprints.Items.Ecnchantments; using Kingmaker.Blueprints.Items.Weapons; using Kingmaker.EntitySystem.Stats; -using Kingmaker.Enums; -using Kingmaker.Items; -using Kingmaker.PubSubSystem; using Kingmaker.RuleSystem; -using Kingmaker.RuleSystem.Rules; -using UnityEngine; -using Object = UnityEngine.Object; -namespace CraftMagicItems { +namespace CraftMagicItems.Patches.Harmony +{ [ComponentName("Weapon Base Size Change")] [AllowMultipleComponents] /** @@ -19,8 +14,7 @@ namespace CraftMagicItems { public class WeaponBaseSizeChange : GameLogicComponent { public int SizeCategoryChange; - [Harmony12.HarmonyPatch(typeof(BlueprintItemWeapon), "BaseDamage", Harmony12.MethodType.Getter)] - // ReSharper disable once UnusedMember.Local + [HarmonyLib.HarmonyPatch(typeof(BlueprintItemWeapon), "BaseDamage", HarmonyLib.MethodType.Getter)] private static class BlueprintItemWeaponBaseDamage { private static void Postfix(BlueprintItemWeapon __instance, ref DiceFormula __result) { foreach (var enchantment in __instance.Enchantments) { @@ -33,8 +27,7 @@ private static void Postfix(BlueprintItemWeapon __instance, ref DiceFormula __re } } - [Harmony12.HarmonyPatch(typeof(BlueprintItemWeapon), "AttackBonusStat", Harmony12.MethodType.Getter)] - // ReSharper disable once UnusedMember.Local + [HarmonyLib.HarmonyPatch(typeof(BlueprintItemWeapon), "AttackBonusStat", HarmonyLib.MethodType.Getter)] private static class BlueprintItemWeaponAttackBonusStat { private static void Postfix(BlueprintItemWeapon __instance, ref StatType __result) { foreach (var enchantment in __instance.Enchantments) { @@ -49,9 +42,7 @@ private static void Postfix(BlueprintItemWeapon __instance, ref StatType __resul } } - - [Harmony12.HarmonyPatch(typeof(BlueprintItemWeapon), "IsTwoHanded", Harmony12.MethodType.Getter)] - // ReSharper disable once UnusedMember.Local + [HarmonyLib.HarmonyPatch(typeof(BlueprintItemWeapon), "IsTwoHanded", HarmonyLib.MethodType.Getter)] private static class BlueprintItemWeaponIsTwoHanded { private static void Postfix(BlueprintItemWeapon __instance, ref bool __result) { foreach (var enchantment in __instance.Enchantments) { @@ -68,8 +59,7 @@ private static void Postfix(BlueprintItemWeapon __instance, ref bool __result) { } } - [Harmony12.HarmonyPatch(typeof(BlueprintItemWeapon), "IsLight", Harmony12.MethodType.Getter)] - // ReSharper disable once UnusedMember.Local + [HarmonyLib.HarmonyPatch(typeof(BlueprintItemWeapon), "IsLight", HarmonyLib.MethodType.Getter)] private static class BlueprintItemWeaponIsLight { private static void Postfix(BlueprintItemWeapon __instance, ref bool __result) { foreach (var enchantment in __instance.Enchantments) { @@ -86,8 +76,28 @@ private static void Postfix(BlueprintItemWeapon __instance, ref bool __result) { } } - [Harmony12.HarmonyPatch(typeof(BlueprintItemWeapon), "SubtypeName", Harmony12.MethodType.Getter)] - // ReSharper disable once UnusedMember.Local + [HarmonyLib.HarmonyPatch(typeof(BlueprintItemWeapon), "IsOneHandedWhichCanBeUsedWithTwoHands", HarmonyLib.MethodType.Getter)] + private static class BlueprintItemWeaponIsOneHandedWhichCanBeUsedWithTwoHands { + private static void Postfix(BlueprintItemWeapon __instance, ref bool __result) { + if (__instance.Type.AttackType == AttackType.Melee && !__instance.Type.IsNatural && !__instance.Type.IsUnarmed) { + foreach (var enchantment in __instance.Enchantments) { + var component = enchantment.GetComponent(); + if (component != null) { + if (component.SizeCategoryChange == 1 && __instance.Type.IsLight) { + __result = true; + } else if ((component.SizeCategoryChange == -1 && __instance.Type.IsTwoHanded)) { + __result = true; + } else { + __result = false; + } + break; + } + } + } + } + } + + [HarmonyLib.HarmonyPatch(typeof(BlueprintItemWeapon), "SubtypeName", HarmonyLib.MethodType.Getter)] private static class BlueprintItemWeaponSubtypeName { private static void Postfix(BlueprintItemWeapon __instance, ref string __result) { foreach (var enchantment in __instance.Enchantments) { diff --git a/CraftMagicItems/Patches/Harmony/WeaponConditionalDamageDiceOnEventAboutToTriggerPatch.cs b/CraftMagicItems/Patches/Harmony/WeaponConditionalDamageDiceOnEventAboutToTriggerPatch.cs new file mode 100644 index 0000000..5f0c2f3 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/WeaponConditionalDamageDiceOnEventAboutToTriggerPatch.cs @@ -0,0 +1,62 @@ +using System.Linq; +using Kingmaker.Blueprints.Items.Ecnchantments; +using Kingmaker.Designers.Mechanics.Facts; +using Kingmaker.Designers.Mechanics.WeaponEnchants; +using Kingmaker.RuleSystem.Rules.Damage; +using Kingmaker.Utility; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(WeaponConditionalDamageDice), "OnEventAboutToTrigger")] + // ReSharper disable once UnusedMember.Local + public static class WeaponConditionalDamageDiceOnEventAboutToTriggerPatch + { + // ReSharper disable once UnusedMember.Local + private static bool Prefix(WeaponConditionalDamageDice __instance, RulePrepareDamage evt) + { + if (__instance is ItemEnchantmentLogic logic) + { + if (evt.DamageBundle.WeaponDamage == null) + { + return false; + } + + if (__instance.IsBane) + { + if (logic.Owner.Enchantments.Any((ItemEnchantment e) => e.Get())) + { + return false; + } + } + + if (__instance.CheckWielder) + { + using (logic.Enchantment.Context.GetDataScope(logic.Owner.Wielder.Unit)) + { + if (Main.EquipmentEnchantmentValid(evt.DamageBundle.Weapon, logic.Owner) && __instance.Conditions.Check(null)) + { + BaseDamage damage = __instance.Damage.CreateDamage(); + evt.DamageBundle.Add(damage); + } + } + } + else + { + using (logic.Enchantment.Context.GetDataScope(evt.Target)) + { + if (Main.EquipmentEnchantmentValid(evt.DamageBundle.Weapon, logic.Owner) && __instance.Conditions.Check(null)) + { + BaseDamage damage2 = __instance.Damage.CreateDamage(); + evt.DamageBundle.Add(damage2); + } + } + } + return false; + } + else + { + return true; + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/WeaponConditionalEnhancementBonusOnEventAboutToTriggerRuleCalculateAttackBonusPatch.cs b/CraftMagicItems/Patches/Harmony/WeaponConditionalEnhancementBonusOnEventAboutToTriggerRuleCalculateAttackBonusPatch.cs new file mode 100644 index 0000000..06b9289 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/WeaponConditionalEnhancementBonusOnEventAboutToTriggerRuleCalculateAttackBonusPatch.cs @@ -0,0 +1,55 @@ +using System; +using System.Linq; +using Kingmaker.Blueprints.Items.Ecnchantments; +using Kingmaker.Designers.Mechanics.Facts; +using Kingmaker.Designers.Mechanics.WeaponEnchants; +using Kingmaker.RuleSystem.Rules; +using Kingmaker.Utility; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(WeaponConditionalEnhancementBonus), "OnEventAboutToTrigger", new Type[] { typeof(RuleCalculateAttackBonus) })] + // ReSharper disable once UnusedMember.Local + public static class WeaponConditionalEnhancementBonusOnEventAboutToTriggerRuleCalculateAttackBonusPatch + { + // ReSharper disable once UnusedMember.Local + private static bool Prefix(WeaponConditionalEnhancementBonus __instance, RuleCalculateAttackBonus evt) + { + if (__instance is ItemEnchantmentLogic logic) + { + if (__instance.IsBane) + { + if (logic.Owner.Enchantments.Any((ItemEnchantment e) => e.Get())) + { + return false; + } + } + if (__instance.CheckWielder) + { + using (logic.Enchantment.Context.GetDataScope(evt.Initiator)) + { + if (Main.EquipmentEnchantmentValid(evt.Weapon, logic.Owner) && __instance.Conditions.Check(null)) + { + evt.AddBonus(__instance.EnhancementBonus, logic.Fact); + } + } + } + else + { + using (logic.Enchantment.Context.GetDataScope(evt.Target)) + { + if (Main.EquipmentEnchantmentValid(evt.Weapon, logic.Owner) && __instance.Conditions.Check(null)) + { + evt.AddBonus(__instance.EnhancementBonus, logic.Fact); + } + } + } + return false; + } + else + { + return true; + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/WeaponConditionalEnhancementBonusOnEventAboutToTriggerRuleCalculateWeaponStatsPatch.cs b/CraftMagicItems/Patches/Harmony/WeaponConditionalEnhancementBonusOnEventAboutToTriggerRuleCalculateWeaponStatsPatch.cs new file mode 100644 index 0000000..033f705 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/WeaponConditionalEnhancementBonusOnEventAboutToTriggerRuleCalculateWeaponStatsPatch.cs @@ -0,0 +1,60 @@ +using System; +using System.Linq; +using Kingmaker.Blueprints.Items.Ecnchantments; +using Kingmaker.Designers.Mechanics.Facts; +using Kingmaker.Designers.Mechanics.WeaponEnchants; +using Kingmaker.RuleSystem.Rules; +using Kingmaker.Utility; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(WeaponConditionalEnhancementBonus), "OnEventAboutToTrigger", new Type[] { typeof(RuleCalculateWeaponStats) })] + // ReSharper disable once UnusedMember.Local + public static class WeaponConditionalEnhancementBonusOnEventAboutToTriggerRuleCalculateWeaponStatsPatch + { + // ReSharper disable once UnusedMember.Local + private static bool Prefix(WeaponConditionalEnhancementBonus __instance, RuleCalculateWeaponStats evt) + { + if (__instance is ItemEnchantmentLogic logic) + { + if (__instance.IsBane) + { + if (logic.Owner.Enchantments.Any((ItemEnchantment e) => e.Get())) + { + return false; + } + } + + if (__instance.CheckWielder) + { + using (logic.Enchantment.Context.GetDataScope(evt.Initiator)) + { + if (Main.EquipmentEnchantmentValid(evt.Weapon, logic.Owner) && __instance.Conditions.Check(null)) + { + evt.AddBonusDamage(__instance.EnhancementBonus); + evt.Enhancement += __instance.EnhancementBonus; + evt.EnhancementTotal += __instance.EnhancementBonus; + } + } + } + else if (evt.AttackWithWeapon != null) + { + using (logic.Enchantment.Context.GetDataScope(evt.AttackWithWeapon.Target)) + { + if (Main.EquipmentEnchantmentValid(evt.Weapon, logic.Owner) && __instance.Conditions.Check(null)) + { + evt.AddBonusDamage(__instance.EnhancementBonus); + evt.Enhancement += __instance.EnhancementBonus; + evt.EnhancementTotal += __instance.EnhancementBonus; + } + } + } + return false; + } + else + { + return true; + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/WeaponDamageAgainstAlignmentOnEventAboutToTriggerPatch.cs b/CraftMagicItems/Patches/Harmony/WeaponDamageAgainstAlignmentOnEventAboutToTriggerPatch.cs new file mode 100644 index 0000000..b738c55 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/WeaponDamageAgainstAlignmentOnEventAboutToTriggerPatch.cs @@ -0,0 +1,41 @@ +using Kingmaker.Blueprints.Items.Ecnchantments; +using Kingmaker.Designers.Mechanics.Facts; +using Kingmaker.Enums; +using Kingmaker.RuleSystem; +using Kingmaker.RuleSystem.Rules.Damage; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(WeaponDamageAgainstAlignment), "OnEventAboutToTrigger")] + // ReSharper disable once UnusedMember.Local + public static class WeaponDamageAgainstAlignmentOnEventAboutToTriggerPatch + { + // ReSharper disable once UnusedMember.Local + private static bool Prefix(WeaponDamageAgainstAlignment __instance, RulePrepareDamage evt) + { + if (__instance is ItemEnchantmentLogic logic) + { + if (evt.DamageBundle.WeaponDamage == null) + { + return false; + } + evt.DamageBundle.WeaponDamage.AddAlignment(__instance.WeaponAlignment); + + if (evt.Target.Descriptor.Alignment.Value.HasComponent(__instance.EnemyAlignment) + && Main.EquipmentEnchantmentValid(evt.DamageBundle.Weapon, logic.Owner)) + { + int rollsCount = __instance.Value.DiceCountValue.Calculate(logic.Context); + int bonusDamage = __instance.Value.BonusValue.Calculate(logic.Context); + EnergyDamage energyDamage = new EnergyDamage(new DiceFormula(rollsCount, __instance.Value.DiceType), __instance.DamageType); + energyDamage.AddBonusTargetRelated(bonusDamage); + evt.DamageBundle.Add(energyDamage); + } + return false; + } + else + { + return true; + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/WeaponEnergyBurstOnEventAboutToTriggerPatch.cs b/CraftMagicItems/Patches/Harmony/WeaponEnergyBurstOnEventAboutToTriggerPatch.cs new file mode 100644 index 0000000..d1a14fe --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/WeaponEnergyBurstOnEventAboutToTriggerPatch.cs @@ -0,0 +1,39 @@ +using System; +using Kingmaker; +using Kingmaker.Blueprints.Items.Ecnchantments; +using Kingmaker.Designers.Mechanics.Facts; +using Kingmaker.RuleSystem; +using Kingmaker.RuleSystem.Rules; +using Kingmaker.RuleSystem.Rules.Damage; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(WeaponEnergyBurst), "OnEventAboutToTrigger")] + // ReSharper disable once UnusedMember.Local + public static class WeaponEnergyBurstOnEventAboutToTriggerPatch + { + // ReSharper disable once UnusedMember.Local + private static bool Prefix(WeaponEnergyBurst __instance, RuleDealDamage evt) + { + if (__instance is ItemEnchantmentLogic logic) + { + if (logic.Owner == null || evt.AttackRoll == null || !evt.AttackRoll.IsCriticalConfirmed || evt.AttackRoll.FortificationNegatesCriticalHit || evt.DamageBundle.WeaponDamage == null) + { + return false; + } + + if (Main.EquipmentEnchantmentValid(evt.DamageBundle.Weapon, logic.Owner)) + { + RuleCalculateWeaponStats ruleCalculateWeaponStats = Rulebook.Trigger(new RuleCalculateWeaponStats(Game.Instance.DefaultUnit, evt.DamageBundle.Weapon, null)); + DiceFormula dice = new DiceFormula(Math.Max(ruleCalculateWeaponStats.CriticalMultiplier - 1, 1), __instance.Dice); + evt.DamageBundle.Add(new EnergyDamage(dice, __instance.Element)); + } + return false; + } + else + { + return true; + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/WeaponEnergyDamageDiceOnEventAboutToTriggerPatch.cs b/CraftMagicItems/Patches/Harmony/WeaponEnergyDamageDiceOnEventAboutToTriggerPatch.cs new file mode 100644 index 0000000..334c5cc --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/WeaponEnergyDamageDiceOnEventAboutToTriggerPatch.cs @@ -0,0 +1,38 @@ +using Kingmaker.Blueprints.Items.Ecnchantments; +using Kingmaker.Designers.Mechanics.Facts; +using Kingmaker.RuleSystem.Rules; +using Kingmaker.RuleSystem.Rules.Damage; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(WeaponEnergyDamageDice), "OnEventAboutToTrigger")] + // ReSharper disable once UnusedMember.Local + public static class WeaponEnergyDamageDiceOnEventAboutToTriggerPatch + { + // ReSharper disable once UnusedMember.Local + private static bool Prefix(WeaponEnergyDamageDice __instance, RuleCalculateWeaponStats evt) + { + if (__instance is ItemEnchantmentLogic logic) + { + if (Main.EquipmentEnchantmentValid(evt.Weapon, logic.Owner)) + { + DamageDescription item = new DamageDescription + { + TypeDescription = new DamageTypeDescription + { + Type = DamageType.Energy, + Energy = __instance.Element + }, + Dice = __instance.EnergyDamageDice + }; + evt.DamageDescription.Add(item); + } + return false; + } + else + { + return true; + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/WeaponExtraAttackOnEventAboutToTriggerPatch.cs b/CraftMagicItems/Patches/Harmony/WeaponExtraAttackOnEventAboutToTriggerPatch.cs new file mode 100644 index 0000000..5c47046 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/WeaponExtraAttackOnEventAboutToTriggerPatch.cs @@ -0,0 +1,34 @@ +using Kingmaker.Blueprints.Items.Ecnchantments; +using Kingmaker.Designers.Mechanics.Facts; +using Kingmaker.Items; +using Kingmaker.RuleSystem.Rules; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(WeaponExtraAttack), "OnEventAboutToTrigger")] + // ReSharper disable once UnusedMember.Local + public static class WeaponExtraAttackOnEventAboutToTriggerPatch + { + // ReSharper disable once UnusedMember.Local + private static bool Prefix(WeaponExtraAttack __instance, RuleCalculateAttacksCount evt) + { + if (__instance is ItemEnchantmentLogic logic) + { + if (logic.Owner is ItemEntityWeapon) + { + evt.AddExtraAttacks(__instance.Number, __instance.Haste, __instance.Owner); + } + else if (evt.Initiator.GetFirstWeapon() != null + && (evt.Initiator.GetFirstWeapon().Blueprint.IsNatural || evt.Initiator.GetFirstWeapon().Blueprint.IsUnarmed)) + { + evt.AddExtraAttacks(__instance.Number, __instance.Haste); + } + return false; + } + else + { + return true; + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/WeaponParametersAttackBonusOnEventAboutToTriggerPatch.cs b/CraftMagicItems/Patches/Harmony/WeaponParametersAttackBonusOnEventAboutToTriggerPatch.cs new file mode 100644 index 0000000..9628a37 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/WeaponParametersAttackBonusOnEventAboutToTriggerPatch.cs @@ -0,0 +1,26 @@ +using Kingmaker.Designers.Mechanics.Facts; +using Kingmaker.Enums; +using Kingmaker.RuleSystem.Rules; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(WeaponParametersAttackBonus), "OnEventAboutToTrigger")] + // ReSharper disable once UnusedMember.Local + public static class WeaponParametersAttackBonusOnEventAboutToTriggerPatch + { + private static bool Prefix(WeaponParametersAttackBonus __instance, RuleCalculateAttackBonusWithoutTarget evt) + { + if (evt.Weapon != null + && __instance.OnlyFinessable + && evt.Weapon.Blueprint.Type.Category.HasSubCategory(WeaponSubCategory.Finessable) + && Main.IsOversized(evt.Weapon.Blueprint)) + { + return false; + } + else + { + return true; + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/WeaponParametersDamageBonusOnEventAboutToTriggerPatch.cs b/CraftMagicItems/Patches/Harmony/WeaponParametersDamageBonusOnEventAboutToTriggerPatch.cs new file mode 100644 index 0000000..f3e39c9 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/WeaponParametersDamageBonusOnEventAboutToTriggerPatch.cs @@ -0,0 +1,27 @@ +using System; +using Kingmaker.Designers.Mechanics.Facts; +using Kingmaker.Enums; +using Kingmaker.RuleSystem.Rules; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(WeaponParametersDamageBonus), "OnEventAboutToTrigger", new Type[] { typeof(RuleCalculateWeaponStats) })] + // ReSharper disable once UnusedMember.Local + public static class WeaponParametersDamageBonusOnEventAboutToTriggerPatch + { + private static bool Prefix(WeaponParametersDamageBonus __instance, RuleCalculateWeaponStats evt) + { + if (evt.Weapon != null + && __instance.OnlyFinessable + && evt.Weapon.Blueprint.Type.Category.HasSubCategory(WeaponSubCategory.Finessable) + && Main.IsOversized(evt.Weapon.Blueprint)) + { + return false; + } + else + { + return true; + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/Harmony/WeaponRealityOnEventAboutToTriggerPatch.cs b/CraftMagicItems/Patches/Harmony/WeaponRealityOnEventAboutToTriggerPatch.cs new file mode 100644 index 0000000..1321b33 --- /dev/null +++ b/CraftMagicItems/Patches/Harmony/WeaponRealityOnEventAboutToTriggerPatch.cs @@ -0,0 +1,32 @@ +using Kingmaker.Blueprints.Items.Ecnchantments; +using Kingmaker.Designers.Mechanics.Facts; +using Kingmaker.RuleSystem.Rules.Damage; + +namespace CraftMagicItems.Patches.Harmony +{ + [HarmonyLib.HarmonyPatch(typeof(WeaponReality), "OnEventAboutToTrigger")] + // ReSharper disable once UnusedMember.Local + public static class WeaponRealityOnEventAboutToTriggerPatch + { + // ReSharper disable once UnusedMember.Local + private static bool Prefix(WeaponReality __instance, RulePrepareDamage evt) + { + if (__instance is ItemEnchantmentLogic logic) + { + if (evt.DamageBundle.WeaponDamage == null) + { + return false; + } + if (Main.EquipmentEnchantmentValid(evt.DamageBundle.Weapon, logic.Owner)) + { + evt.DamageBundle.WeaponDamage.Reality |= __instance.Reality; + } + return false; + } + else + { + return true; + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/WeaponSizeChange.cs b/CraftMagicItems/Patches/Harmony/WeaponSizeChange.cs similarity index 85% rename from CraftMagicItems/WeaponSizeChange.cs rename to CraftMagicItems/Patches/Harmony/WeaponSizeChange.cs index eec8317..09d6e9a 100644 --- a/CraftMagicItems/WeaponSizeChange.cs +++ b/CraftMagicItems/Patches/Harmony/WeaponSizeChange.cs @@ -1,17 +1,16 @@ using Kingmaker.Blueprints; -using Kingmaker.Blueprints.Items.Ecnchantments; using Kingmaker.Blueprints.Items.Weapons; using Kingmaker.Enums; -using Kingmaker.PubSubSystem; using Kingmaker.RuleSystem; using Kingmaker.RuleSystem.Rules; -namespace CraftMagicItems { +namespace CraftMagicItems.Patches.Harmony +{ [ComponentName("Weapon Size Change")] public class WeaponSizeChange : GameLogicComponent { public int SizeCategoryChange; - [Harmony12.HarmonyPatch(typeof(BlueprintItemWeapon), "BaseDamage", Harmony12.MethodType.Getter)] + [HarmonyLib.HarmonyPatch(typeof(BlueprintItemWeapon), "BaseDamage", HarmonyLib.MethodType.Getter)] // ReSharper disable once UnusedMember.Local private static class BlueprintItemWeaponBaseDamage { private static void Postfix(BlueprintItemWeapon __instance, ref DiceFormula __result) { @@ -25,7 +24,7 @@ private static void Postfix(BlueprintItemWeapon __instance, ref DiceFormula __re } } - [Harmony12.HarmonyPatch(typeof(RuleCalculateWeaponStats), "WeaponSize", Harmony12.MethodType.Getter)] + [HarmonyLib.HarmonyPatch(typeof(RuleCalculateWeaponStats), "WeaponSize", HarmonyLib.MethodType.Getter)] // ReSharper disable once UnusedMember.Local private static class RuleCalculateWeaponStatsWeaponSizePatch { private static void Prefix(RuleCalculateWeaponStats __instance, ref int ___m_SizeShift, ref int __state, ref Size __result) { diff --git a/CraftMagicItems/Patches/HarmonyPatcher.cs b/CraftMagicItems/Patches/HarmonyPatcher.cs new file mode 100644 index 0000000..aab19f3 --- /dev/null +++ b/CraftMagicItems/Patches/HarmonyPatcher.cs @@ -0,0 +1,93 @@ +using System; +using System.Linq; +using System.Reflection; +using Kingmaker.Utility; + +namespace CraftMagicItems.Patches +{ + /// Class that performs the Harmony patching + public class HarmonyPatcher + { + /// that logs error messages + protected Action LogError; + + /// Harmony instance used to patch code + protected HarmonyLib.Harmony HarmonyInstance; + + /// Definition constructor + /// that logs error messages + public HarmonyPatcher(Action logger) + { + HarmonyInstance = new HarmonyLib.Harmony("kingmaker.craftMagicItems"); + LogError = logger; + } + + /// + /// Patches all classes in the assembly decorated with , + /// starting in the order of the methods named in . + /// + /// + /// Ordered array of method names that should be patched in this order before any + /// other methods are patched. + /// + public void PatchAllOrdered(params MethodPatch[] orderedMethods) + { + foreach (var method in orderedMethods) + { + method.Patch(HarmonyInstance); + } + HarmonyInstance.PatchAll(Assembly.GetExecutingAssembly()); + } + + /// + /// Unpatches all classes in the assembly decorated with , + /// except the ones whose method names match any in . + /// + /// Array of method names that should be not be unpatched + public void UnpatchAllExcept(params MethodPatch[] exceptMethods) + { + if (HarmonyInstance != null) + { + try + { + foreach (var method in HarmonyInstance.GetPatchedMethods().ToArray()) + { + var patchInfo = HarmonyLib.Harmony.GetPatchInfo(method); + if (patchInfo.Owners.Contains(HarmonyInstance.Id)) + { + var methodPatches = exceptMethods.Where(m => m.MatchOriginal(method)); + if (methodPatches.Count() > 0) + { + foreach (var patch in patchInfo.Prefixes) + { + if (!methodPatches.Any(m => m.MatchPrefix(patch.PatchMethod))) + { + HarmonyInstance.Unpatch(method, patch.PatchMethod); + } + } + foreach (var patch in patchInfo.Postfixes) + { + if (!methodPatches.Any(m => m.MatchPostfix(patch.PatchMethod))) + { + HarmonyInstance.Unpatch(method, patch.PatchMethod); + } + } + HarmonyInstance.Unpatch(method, HarmonyLib.HarmonyPatchType.Finalizer, HarmonyInstance.Id); + HarmonyInstance.Unpatch(method, HarmonyLib.HarmonyPatchType.Transpiler, HarmonyInstance.Id); + HarmonyInstance.Unpatch(method, HarmonyLib.HarmonyPatchType.ReversePatch, HarmonyInstance.Id); + } + else + { + HarmonyInstance.Unpatch(method, HarmonyLib.HarmonyPatchType.All, HarmonyInstance.Id); + } + } + } + } + catch (Exception e) + { + Main.ModEntry.Logger.Error($"Exception during Un-patching: {e}"); + } + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/IkPatch.cs b/CraftMagicItems/Patches/IkPatch.cs new file mode 100644 index 0000000..dc4deea --- /dev/null +++ b/CraftMagicItems/Patches/IkPatch.cs @@ -0,0 +1,23 @@ +namespace CraftMagicItems.Patches +{ + /// Structure defining visual X/Y/Z-axis adjustments to weapons to patch the existing game + public struct IkPatch + { + /// Constructor + /// Unique ID of the blueprint to patch + /// X-axis adjustment + /// Y-axis adjustment + /// Z-axis adjustment + public IkPatch(string uuid, float x, float y, float z) + { + BlueprintId = uuid; + X = x; + Y = y; + Z = z; + } + + public string BlueprintId; + + public float X, Y, Z; + } +} diff --git a/CraftMagicItems/Patches/LeftHandVisualDisplayPatcher.cs b/CraftMagicItems/Patches/LeftHandVisualDisplayPatcher.cs new file mode 100644 index 0000000..00828ab --- /dev/null +++ b/CraftMagicItems/Patches/LeftHandVisualDisplayPatcher.cs @@ -0,0 +1,34 @@ +using CraftMagicItems.Constants; +using Kingmaker.Blueprints; +using Kingmaker.Blueprints.Items.Weapons; +using Kingmaker.View.Equipment; +using UnityEngine; + +namespace CraftMagicItems.Patches +{ + /// Class that patches the visual display for left-handed Ik Targets + public static class LeftHandVisualDisplayPatcher + { + /// Patches the for left-handed characters + public static void PatchLeftHandedWeaponModels() + { + foreach (var patch in VisualAdjustmentPatches.LeftHandedWeaponPatchList) + { + var weapon = ResourcesLibrary.TryGetBlueprint(patch.BlueprintId); + if (weapon != null) + { + var model = weapon.VisualParameters.Model; + var equipmentOffsets = model.GetComponent(); + + var locator = new GameObject(); + locator.transform.SetParent(model.transform); + locator.transform.localPosition = new Vector3(patch.X, patch.Y, patch.Z); + locator.transform.localEulerAngles = new Vector3(0.0f, 0.0f, 0.0f); + locator.transform.localScale = new Vector3(1.0f, 1.0f, 1.0f); + + equipmentOffsets.IkTargetLeftHand = locator.transform; + } + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/Patches/MethodPatch.cs b/CraftMagicItems/Patches/MethodPatch.cs new file mode 100644 index 0000000..7911fb5 --- /dev/null +++ b/CraftMagicItems/Patches/MethodPatch.cs @@ -0,0 +1,39 @@ +using HarmonyLib; +using System.Reflection; + +namespace CraftMagicItems.Patches +{ + public struct MethodPatch + { + public MethodPatch(MethodBase original, HarmonyLib.HarmonyMethod prefix = null, HarmonyLib.HarmonyMethod postfix = null) + { + m_original = original; + m_prefix = prefix; + m_postfix = postfix; + } + + public MethodInfo Patch(HarmonyLib.Harmony instance) + { + return instance.Patch(m_original, m_prefix, m_postfix); + } + + public bool MatchOriginal(MethodBase method) + { + return m_original != null & m_original == method; + } + + public bool MatchPrefix(MethodBase method) + { + return m_prefix != null && m_prefix.method == method; + } + + public bool MatchPostfix(MethodBase method) + { + return m_postfix != null && m_postfix.method == method; + } + + MethodBase m_original; + HarmonyMethod m_prefix; + HarmonyMethod m_postfix; + } +} \ No newline at end of file diff --git a/CraftMagicItems/PrerequisiteCasterLevel.cs b/CraftMagicItems/PrerequisiteCasterLevel.cs index 0bccb1b..0f3a09c 100644 --- a/CraftMagicItems/PrerequisiteCasterLevel.cs +++ b/CraftMagicItems/PrerequisiteCasterLevel.cs @@ -1,3 +1,4 @@ +using CraftMagicItems.Localization; using Kingmaker.Blueprints.Classes.Prerequisites; using Kingmaker.Localization; using Kingmaker.UnitLogic; diff --git a/CraftMagicItems/Settings.cs b/CraftMagicItems/Settings.cs new file mode 100644 index 0000000..5d72ac4 --- /dev/null +++ b/CraftMagicItems/Settings.cs @@ -0,0 +1,28 @@ +using UnityModManagerNet; + +namespace CraftMagicItems +{ + /// Settings for the Crafting Mod + public class Settings : UnityModManager.ModSettings + { + public const int MagicCraftingProgressPerDay = 500; + public const int MundaneCraftingProgressPerDay = 5; + + public bool CraftingCostsNoGold; + public bool IgnoreCraftingFeats; + public bool CraftingTakesNoTime; + public float CraftingPriceScale = 1; + public bool CraftAtFullSpeedWhileAdventuring; + public bool CasterLevelIsSinglePrerequisite; + public bool IgnoreFeatCasterLevelRestriction; + public bool IgnorePlusTenItemMaximum; + public bool CustomCraftRate; + public int MagicCraftingRate = MagicCraftingProgressPerDay; + public int MundaneCraftingRate = MundaneCraftingProgressPerDay; + + public override void Save(UnityModManager.ModEntry modEntry) + { + Save(this, modEntry); + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/SustenanceEnchantment.cs b/CraftMagicItems/SustenanceEnchantment.cs index 43469de..358e7d6 100644 --- a/CraftMagicItems/SustenanceEnchantment.cs +++ b/CraftMagicItems/SustenanceEnchantment.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using CraftMagicItems.Localization; using Kingmaker; #if PATCH21 using Kingmaker.Assets.UI.Context; @@ -17,12 +18,15 @@ using Kingmaker.UnitLogic.Buffs.Blueprints; using Kingmaker.UnitLogic.Mechanics; -namespace CraftMagicItems { +namespace CraftMagicItems.Enchantments +{ // A living character with a SustenanceFact does not need rations when they camp, and can perform two camp roles. - public class SustenanceFact : BlueprintBuff { + public class SustenanceFact : BlueprintBuff + { } - public class SustenanceEnchantment : BlueprintItemEnchantment { + public class SustenanceEnchantment : BlueprintItemEnchantment + { private static readonly SustenanceEnchantment BlueprintSustenanceEnchantment = CreateInstance(); private const string SustenanceEnchantmentGuid = "8eb9d1c94b1e4894a88c228aa71b79e5#CraftMagicItems(sustenanceEnchantment)"; private static readonly SustenanceFact BlueprintSustenanceFact = CreateInstance(); @@ -30,46 +34,50 @@ public class SustenanceEnchantment : BlueprintItemEnchantment { private static bool initialised; - [Harmony12.HarmonyPatch(typeof(MainMenu), "Start")] - public static class MainMenuStartPatch { - private static void AddBlueprint(string guid, BlueprintScriptableObject blueprint) { - Main.Accessors.SetBlueprintScriptableObjectAssetGuid(blueprint, guid); + [HarmonyLib.HarmonyPatch(typeof(MainMenu), "Start")] + public static class MainMenuStartPatch + { + private static void AddBlueprint(string guid, BlueprintScriptableObject blueprint) + { + Main.Accessors.SetBlueprintScriptableObjectAssetGuid(blueprint) = guid; ResourcesLibrary.LibraryObject.BlueprintsByAssetId?.Add(guid, blueprint); ResourcesLibrary.LibraryObject.GetAllBlueprints()?.Add(blueprint); } // ReSharper disable once UnusedMember.Local - public static void Postfix() { - if (!initialised) { + public static void Postfix() + { + if (!initialised) + { initialised = true; AddBlueprint(SustenanceEnchantmentGuid, BlueprintSustenanceEnchantment); AddBlueprint(SustenanceFactGuid, BlueprintSustenanceFact); - Main.Accessors.SetBlueprintItemEnchantmentEnchantName(BlueprintSustenanceEnchantment, - new L10NString("craftMagicItems-enchantment-sustenance-name")); - Main.Accessors.SetBlueprintItemEnchantmentDescription(BlueprintSustenanceEnchantment, - new L10NString("craftMagicItems-enchantment-sustenance-description")); - Main.Accessors.SetBlueprintItemEnchantmentPrefix(BlueprintSustenanceEnchantment, new L10NString("")); - Main.Accessors.SetBlueprintItemEnchantmentSuffix(BlueprintSustenanceEnchantment, new L10NString("")); - Main.Accessors.SetBlueprintItemEnchantmentEnchantmentCost(BlueprintSustenanceEnchantment, 1); - Main.Accessors.SetBlueprintItemEnchantmentEnchantmentIdentifyDC(BlueprintSustenanceEnchantment, 5); + Main.Accessors.SetBlueprintItemEnchantmentEnchantName(BlueprintSustenanceEnchantment) = + new L10NString("craftMagicItems-enchantment-sustenance-name"); + Main.Accessors.SetBlueprintItemEnchantmentDescription(BlueprintSustenanceEnchantment) = + new L10NString("craftMagicItems-enchantment-sustenance-description"); + Main.Accessors.SetBlueprintItemEnchantmentPrefix(BlueprintSustenanceEnchantment) = new L10NString(""); + Main.Accessors.SetBlueprintItemEnchantmentSuffix(BlueprintSustenanceEnchantment) = new L10NString(""); + Main.Accessors.SetBlueprintItemEnchantmentEnchantmentCost(BlueprintSustenanceEnchantment) = 1; + Main.Accessors.SetBlueprintItemEnchantmentEnchantmentIdentifyDC(BlueprintSustenanceEnchantment) = 5; var addSustenanceFact = CreateInstance(); addSustenanceFact.Blueprint = BlueprintSustenanceFact; addSustenanceFact.name = "AddUnitFactEquipment-SustenanceFact"; - BlueprintSustenanceEnchantment.ComponentsArray = new BlueprintComponent[] {addSustenanceFact}; + BlueprintSustenanceEnchantment.ComponentsArray = new BlueprintComponent[] { addSustenanceFact }; Main.Accessors.SetBlueprintBuffFlags(BlueprintSustenanceFact, 2 + 8); // Enum is private... 2 = HiddenInUi, 8 = StayOnDeath BlueprintSustenanceFact.Stacking = StackingType.Replace; BlueprintSustenanceFact.Frequency = DurationRate.Rounds; BlueprintSustenanceFact.FxOnStart = new PrefabLink(); BlueprintSustenanceFact.FxOnRemove = new PrefabLink(); - Main.Accessors.SetBlueprintUnitFactDisplayName(BlueprintSustenanceFact, new L10NString("craftMagicItems-enchantment-sustenance-name")); - Main.Accessors.SetBlueprintUnitFactDescription(BlueprintSustenanceFact, - new L10NString("craftMagicItems-enchantment-sustenance-description")); + Main.Accessors.SetBlueprintUnitFactDisplayName(BlueprintSustenanceFact) = new L10NString("craftMagicItems-enchantment-sustenance-name"); + Main.Accessors.SetBlueprintUnitFactDescription(BlueprintSustenanceFact) = + new L10NString("craftMagicItems-enchantment-sustenance-description"); } } } #if PATCH21 - [Harmony12.HarmonyPatch(typeof(MainMenuUiContext), "Initialize")] + [HarmonyLib.HarmonyPatch(typeof(MainMenuUiContext), "Initialize")] private static class MainMenuUiContextInitializePatch { private static void Postfix() { MainMenuStartPatch.Postfix(); @@ -77,38 +85,46 @@ private static void Postfix() { } #endif - private static bool UnitHasSustenance(UnitEntityData unit) { + private static bool UnitHasSustenance(UnitEntityData unit) + { return unit?.Descriptor.GetFact(BlueprintSustenanceFact) != null; } - [Harmony12.HarmonyPatch(typeof(RestController), "CalculateNeededRations")] + [HarmonyLib.HarmonyPatch(typeof(RestController), "CalculateNeededRations")] // ReSharper disable once UnusedMember.Local - private static class RestControllerCalculateNeededRationsPatch { + private static class RestControllerCalculateNeededRationsPatch + { // ReSharper disable once UnusedMember.Local - private static void Postfix(ref int __result) { + private static void Postfix(ref int __result) + { var sustenanceCount = Game.Instance.Player.Party.NotDead().Count(UnitHasSustenance); __result = Math.Max(0, __result - sustenanceCount); } } - private static int CountRoles(UnitEntityData unit) { + private static int CountRoles(UnitEntityData unit) + { var roles = (Game.Instance.Player.Camping.Builders.Contains(unit) ? 1 : 0) + (Game.Instance.Player.Camping.Hunters.Contains(unit) ? 1 : 0) + (Game.Instance.Player.Camping.Cookers.Contains(unit) ? 1 : 0) + (Game.Instance.Player.Camping.Special.Contains(unit) ? 1 : 0); - foreach (var guardShift in Game.Instance.Player.Camping.Guards) { + foreach (var guardShift in Game.Instance.Player.Camping.Guards) + { roles += guardShift.Contains(unit) ? 1 : 0; } return roles; } - [Harmony12.HarmonyPatch(typeof(CampManager), "RemoveAllCompanionRoles")] + [HarmonyLib.HarmonyPatch(typeof(CampManager), "RemoveAllCompanionRoles")] // ReSharper disable once UnusedMember.Local - private static class CampManagerRemoveAllCompanionRolesPatch { + private static class CampManagerRemoveAllCompanionRolesPatch + { // ReSharper disable once UnusedMember.Local - private static bool Prefix(UnitEntityData unit) { - if (UnitHasSustenance(unit)) { + private static bool Prefix(UnitEntityData unit) + { + if (UnitHasSustenance(unit)) + { // Only return true (and thus remove their roles) if they're already doing 2 roles. return CountRoles(unit) >= 2; } @@ -117,15 +133,18 @@ private static bool Prefix(UnitEntityData unit) { } } - [Harmony12.HarmonyPatch(typeof(MemberUIBody), "CheckHasRole")] - private static class MemberUiBodyCheckHasRolePatch { + [HarmonyLib.HarmonyPatch(typeof(MemberUIBody), "CheckHasRole")] + private static class MemberUiBodyCheckHasRolePatch + { // ReSharper disable once UnusedMember.Local - private static bool Prefix(MemberUIBody __instance, ref bool __result) { + private static bool Prefix(MemberUIBody __instance, ref bool __result) + { var unit = __instance.CharacterSlot.Unit(); - if (UnitHasSustenance(unit) && CountRoles(unit) < 2) { + if (UnitHasSustenance(unit) && CountRoles(unit) < 2) + { // The unit can still be assigned to another role. __instance.HasRole = false; - Harmony12.Traverse.Create(__instance).Method("SetupRoleView").GetValue(); + HarmonyLib.Traverse.Create(__instance).Method("SetupRoleView").GetValue(); __result = false; return false; } @@ -134,18 +153,23 @@ private static bool Prefix(MemberUIBody __instance, ref bool __result) { } } - private static List FindBestRoleToDrop(UnitEntityData unit, List current, List best) { + private static List FindBestRoleToDrop(UnitEntityData unit, List current, List best) + { return current.Contains(unit) && (best == null || current.Count > best.Count) ? current : best; } - [Harmony12.HarmonyPatch(typeof(CampingState), "CleanupRoles")] + [HarmonyLib.HarmonyPatch(typeof(CampingState), "CleanupRoles")] // ReSharper disable once UnusedMember.Local - private static class CampingStateCleanupRolesPatch { + private static class CampingStateCleanupRolesPatch + { // ReSharper disable once UnusedMember.Local - private static void Postfix() { + private static void Postfix() + { // Ensure that anyone assigned to multiple roles actually has Sustenance - foreach (var unit in Game.Instance.Player.Party) { - if (CountRoles(unit) > 1 && !UnitHasSustenance(unit)) { + foreach (var unit in Game.Instance.Player.Party) + { + if (CountRoles(unit) > 1 && !UnitHasSustenance(unit)) + { // Need to drop one role - prefer roles that others are doing. #if PATCH21 var roleToDrop = FindBestRoleToDrop(unit, Game.Instance.Player.Camping.Builders.ToList(), null); @@ -160,7 +184,8 @@ private static void Postfix() { roleToDrop = FindBestRoleToDrop(unit, Game.Instance.Player.Camping.Hunters, roleToDrop); roleToDrop = FindBestRoleToDrop(unit, Game.Instance.Player.Camping.Cookers, roleToDrop); roleToDrop = FindBestRoleToDrop(unit, Game.Instance.Player.Camping.Special, roleToDrop); - foreach (var guardShift in Game.Instance.Player.Camping.Guards) { + foreach (var guardShift in Game.Instance.Player.Camping.Guards) + { roleToDrop = FindBestRoleToDrop(unit, guardShift, roleToDrop); } #endif @@ -171,12 +196,15 @@ private static void Postfix() { } } - [Harmony12.HarmonyPatch(typeof(CampingState), "GetRolesCount")] + [HarmonyLib.HarmonyPatch(typeof(CampingState), "GetRolesCount")] // ReSharper disable once UnusedMember.Local - private static class RestControllerIsTiredPatch { + private static class RestControllerIsTiredPatch + { // ReSharper disable once UnusedMember.Local - private static void Postfix(UnitEntityData unit, ref int __result) { - if (UnitHasSustenance(unit)) { + private static void Postfix(UnitEntityData unit, ref int __result) + { + if (UnitHasSustenance(unit)) + { __result = Math.Min(1, __result); } } diff --git a/CraftMagicItems/UI/BattleLog/BattleLogFactory.cs b/CraftMagicItems/UI/BattleLog/BattleLogFactory.cs new file mode 100644 index 0000000..1ccafc3 --- /dev/null +++ b/CraftMagicItems/UI/BattleLog/BattleLogFactory.cs @@ -0,0 +1,35 @@ +using System; + +namespace CraftMagicItems.UI.BattleLog +{ + /// Factory for + public static class BattleLogFactory + { + private static Func construction; + + /// Static constructor + static BattleLogFactory() + { + Reset(); + } + + /// Resets the constructed instance to + public static void Reset() + { + SetConstructor(() => { return new KingmakerBattleLog(); }); + } + + /// Sets the constructed instance to + public static void SetConstructor(Func constructor) + { + construction = constructor; + } + + /// Constructs an instance of + /// An instance of + public static IBattleLog GetBattleLog() + { + return construction(); + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/UI/BattleLog/IBattleLog.cs b/CraftMagicItems/UI/BattleLog/IBattleLog.cs new file mode 100644 index 0000000..fbdba91 --- /dev/null +++ b/CraftMagicItems/UI/BattleLog/IBattleLog.cs @@ -0,0 +1,15 @@ +using UnityEngine; + +namespace CraftMagicItems.UI.BattleLog +{ + /// Interface that defines logging of messages to the Battle Log in Kingmaker + /// Used for mocking the battle log for tests and such + public interface IBattleLog + { + /// Adds a message to the battle log + /// Message to add + /// Secondary object or for the tooltip to display + /// to use to render + void AddBattleLogMessage(System.String message, System.Object tooltip = null, Color? color = null); + } +} \ No newline at end of file diff --git a/CraftMagicItems/UI/BattleLog/KingMakerBattleLog.cs b/CraftMagicItems/UI/BattleLog/KingMakerBattleLog.cs new file mode 100644 index 0000000..eb38b97 --- /dev/null +++ b/CraftMagicItems/UI/BattleLog/KingMakerBattleLog.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Kingmaker; +using Kingmaker.Blueprints.Root.Strings.GameLog; +using Kingmaker.UI.Log; +using UnityEngine; + +namespace CraftMagicItems.UI.BattleLog +{ + /// Class that drives logging of messages to the Battle Log in Kingmaker + public class KingmakerBattleLog : IBattleLog + { + /// Adds a message to the battle log + /// Message to add + /// Secondary object or for the tooltip to display + /// to use to render + public void AddBattleLogMessage(string message, object tooltip = null, Color? color = null) + { +#if PATCH21 + var data = new LogItemData(message, color ?? GameLogStrings.Instance.DefaultColor, tooltip, PrefixIcon.None, new List { LogChannel.Combat }); +#else + var data = new LogDataManager.LogItemData(message, color ?? GameLogStrings.Instance.DefaultColor, tooltip, PrefixIcon.None); +#endif + if (Game.Instance.UI.BattleLogManager) + { + Game.Instance.UI.BattleLogManager.LogView.AddLogEntry(data); + } + else + { + Main.PendingLogItems.Add(data); + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/UI/OpenSection.cs b/CraftMagicItems/UI/OpenSection.cs new file mode 100644 index 0000000..d000ab7 --- /dev/null +++ b/CraftMagicItems/UI/OpenSection.cs @@ -0,0 +1,12 @@ +namespace CraftMagicItems.UI +{ + /// Enumeration of the sections of the UI that can be rendered + public enum OpenSection + { + CraftMagicItemsSection, + CraftMundaneItemsSection, + ProjectsSection, + FeatsSection, + CheatsSection + } +} \ No newline at end of file diff --git a/CraftMagicItems/UI/Sections/CheatSectionRenderer.cs b/CraftMagicItems/UI/Sections/CheatSectionRenderer.cs new file mode 100644 index 0000000..d83f61f --- /dev/null +++ b/CraftMagicItems/UI/Sections/CheatSectionRenderer.cs @@ -0,0 +1,116 @@ +using CraftMagicItems.UI.UnityModManager; + +namespace CraftMagicItems.UI.Sections +{ + /// User Interface renderer into Unity Mod Manager for the cheats section + public class CheatSectionRenderer : ICheatSectionRenderer + { + /// Renders a checkbox indicating whether crafting costs any gold and returns its current UI value + /// Current value + /// The value of the UI checkbox + public bool Evaluate_CraftingCostsNoGold(bool currentSetting) + { + return UmmUiRenderer.RenderCheckbox("Crafting costs no gold and no material components.", currentSetting); + } + + /// Renders a selection to Unity Mod Manager for the options for custom crafting price adjustments and returns the currently selected value + /// Label to display for the selection UI control + /// Collection of options that the user can select from + /// The index of the selection that Unity currently has registered + public int Evaluate_CraftingCostSelection(string priceLabel, string[] craftingPriceStrings) + { + return Main.DrawSelectionUserInterfaceElements(priceLabel, craftingPriceStrings, 4); + } + + /// Renders a slider to Unity Mod Manager for the custom crafting % cost. + /// Current setting to display on the control + /// The selection that the UI currently has registered + public float Evaluate_CustomCraftingCostSlider(float currentSetting) + { + float result = UmmUiRenderer.RenderFloatSlider("Custom Cost Factor: ", currentSetting * 100, 0, 500); + return result / 100; + } + + /// Renders a warning that price disparity between custom items crafts to non-custom items will have a selling cost disparity between crafting and sale. + public void RenderOnly_WarningAboutCustomItemVanillaItemCostDisparity() + { + UmmUiRenderer.RenderLabelRow( + "Note: The sale price of custom crafted items will also be scaled by this factor, but vanilla items crafted by this mod" + + " will continue to use Owlcat's sale price, creating a price difference between the cost of crafting and sale price."); + } + + /// Renders a checkbox indicating whether crafting should ignore feats + /// Current value + /// The value of the UI checkbox + public bool Evaluate_IgnoreCraftingFeats(bool currentSetting) + { + return UmmUiRenderer.RenderCheckbox("Crafting does not require characters to take crafting feats.", currentSetting); + } + + /// Renders a checkbox indicating whether crafting should take time to complete or not + /// Current value + /// The value of the UI checkbox + public bool Evaluate_CraftingTakesNoTime(bool currentSetting) + { + return UmmUiRenderer.RenderCheckbox("Crafting takes no time to complete.", currentSetting); + } + + /// Renders a checkbox indicating whether crafting should take a non-standard rate or time + /// Current value + /// The value of the UI checkbox + public bool Evaluate_CustomCraftRate(bool currentSetting) + { + return UmmUiRenderer.RenderCheckbox("Craft at a non-standard rate.", currentSetting); + } + + /// Renders a slider to Unity Mod Manager for the custom magic crafting rate. + /// Current setting to display on the control + /// The selection that the UI currently has registered + public int Evaluate_MagicCraftingRateSlider(int currentSetting) + { + var maxMagicRate = ((currentSetting + 1000) / 1000) * 1000; + return UmmUiRenderer.RenderIntSlider("Magic Item Crafting Rate", currentSetting, 1, maxMagicRate); + } + + /// Renders a slider to Unity Mod Manager for the custom mundane crafting rate. + /// Current setting to display on the control + /// The selection that the UI currently has registered + public int Evaluate_MundaneCraftingRateSlider(int currentSetting) + { + var maxMundaneRate = ((currentSetting + 10) / 10) * 10; + return UmmUiRenderer.RenderIntSlider("Mundane Item Crafting Rate", currentSetting, 1, maxMundaneRate); + } + + /// Renders a checkbox indicating whether missing caster levels should combine into a single prerequisite (compared to the default of 1 prerequisite per missing level) + /// Current value + /// The value of the UI checkbox + public bool Evaluate_CasterLevelIsSinglePrerequisite(bool currentSetting) + { + return UmmUiRenderer.RenderCheckbox("When crafting, a Caster Level less than the prerequisite counts as a single missing prerequisite.", currentSetting); + } + + /// Renders a checkbox indicating whether characters should craft at full rate while travelling (compared to 25% rate while travelling) + /// Current value + /// The value of the UI checkbox + public bool Evaluate_CraftAtFullSpeedWhileAdventuring(bool currentSetting) + { + return UmmUiRenderer.RenderCheckbox("Characters craft at full speed while adventuring (instead of 25% speed).", currentSetting); + } + + /// Renders a checkbox indicating whether weapons and armor should be allowed to exceed the +10 enchantment value + /// Current value + /// The value of the UI checkbox + public bool Evaluate_IgnorePlusTenItemMaximum(bool currentSetting) + { + return UmmUiRenderer.RenderCheckbox("Ignore the rule that limits arms and armor to a maximum of +10 equivalent.", currentSetting); + } + + /// Renders a checkbox indicating whether crafting feats can ignore caster level prerequisites + /// Current value + /// The value of the UI checkbox + public bool Evaluate_IgnoreFeatCasterLevelRestriction(bool currentSetting) + { + return UmmUiRenderer.RenderCheckbox("Ignore the crafting feat Caster Level prerequisites when learning feats.", currentSetting); + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/UI/Sections/CheatSectionRendererFactory.cs b/CraftMagicItems/UI/Sections/CheatSectionRendererFactory.cs new file mode 100644 index 0000000..f31d08c --- /dev/null +++ b/CraftMagicItems/UI/Sections/CheatSectionRendererFactory.cs @@ -0,0 +1,35 @@ +using System; + +namespace CraftMagicItems.UI.Sections +{ + /// Factory for + public static class CheatSectionRendererFactory + { + private static Func construction; + + /// Static constructor + static CheatSectionRendererFactory() + { + Reset(); + } + + /// Resets the constructed instance to + public static void Reset() + { + SetConstructor(() => { return new CheatSectionRenderer(); }); + } + + /// Sets the constructed instance to + public static void SetConstructor(Func constructor) + { + construction = constructor; + } + + /// Constructs an instance of + /// An instance of + public static ICheatSectionRenderer GetCheatSectionRenderer() + { + return construction(); + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/UI/Sections/FeatReassignmentSectionRenderer.cs b/CraftMagicItems/UI/Sections/FeatReassignmentSectionRenderer.cs new file mode 100644 index 0000000..3a838ed --- /dev/null +++ b/CraftMagicItems/UI/Sections/FeatReassignmentSectionRenderer.cs @@ -0,0 +1,43 @@ +using CraftMagicItems.UI.UnityModManager; + +namespace CraftMagicItems.UI.Sections +{ + /// User Interface renderer into Unity Mod Manager for the feat reassignment section + public class FeatReassignmentSectionRenderer : IFeatReassignmentSectionRenderer + { + /// Renders a warning that the current character does not qualify for any crafting feats + /// Name of character currently being evaluated + public void RenderOnly_Warning_NoCraftingFeatQualifications(string characterName) + { + UmmUiRenderer.RenderLabelRow($"{characterName} does not currently qualify for any crafting feats."); + } + + /// Renders a message describing how to use the section + public void RenderOnly_UsageExplanation() + { + UmmUiRenderer.RenderLabelRow("Use this section to reassign previous feat choices for this character to crafting feats. Warning: This is a one-way assignment!"); + } + + /// Renders a selection to Unity Mod Manager for the options for selecting a missing casting feat + /// Collection of feat names that are missing from the currently selected character + /// The selected index of the feats + public int Evaluate_MissingFeatSelection(string[] featOptions) + { + return Main.DrawSelectionUserInterfaceElements("Feat to learn", featOptions, 6); + } + + /// Renders a label and button on their own line for selecting a current feat to be replaced by the selected replacement crafting feat + /// Name of the existing feat to potentially be replaced + /// Name of the feat to potentially replace + /// True if the button is clicked, otherwise false + public bool Evaluate_LearnFeatButton(string existingFeat, string replacementFeat) + { + UmmUiRenderer.RenderHorizontalStart(); + UmmUiRenderer.RenderLabel($"Feat: {existingFeat}", false); + var selection = UmmUiRenderer.RenderButton($"<- {replacementFeat}", false); + UmmUiRenderer.RenderHorizontalEnd(); + + return selection; + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/UI/Sections/FeatReassignmentSectionRendererFactory.cs b/CraftMagicItems/UI/Sections/FeatReassignmentSectionRendererFactory.cs new file mode 100644 index 0000000..d4e5622 --- /dev/null +++ b/CraftMagicItems/UI/Sections/FeatReassignmentSectionRendererFactory.cs @@ -0,0 +1,35 @@ +using System; + +namespace CraftMagicItems.UI.Sections +{ + /// Factory for + public static class FeatReassignmentSectionRendererFactory + { + private static Func construction; + + /// Static constructor + static FeatReassignmentSectionRendererFactory() + { + Reset(); + } + + /// Resets the constructed instance to + public static void Reset() + { + SetConstructor(() => { return new FeatReassignmentSectionRenderer(); }); + } + + /// Sets the constructed instance to + public static void SetConstructor(Func constructor) + { + construction = constructor; + } + + /// Constructs an instance of + /// An instance of + public static IFeatReassignmentSectionRenderer GetFeatReassignmentSectionRenderer() + { + return construction(); + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/UI/Sections/ICheatSectionRenderer.cs b/CraftMagicItems/UI/Sections/ICheatSectionRenderer.cs new file mode 100644 index 0000000..c39b7ed --- /dev/null +++ b/CraftMagicItems/UI/Sections/ICheatSectionRenderer.cs @@ -0,0 +1,72 @@ +using System; + +namespace CraftMagicItems.UI.Sections +{ + /// Interface defining the I/O around the user interface for the cheats section and its returned user input values + public interface ICheatSectionRenderer + { + /// Renders a checkbox indicating whether missing caster levels should combine into a single prerequisite (compared to the default of 1 prerequisite per missing level) + /// Current value + /// The value of the UI checkbox + bool Evaluate_CasterLevelIsSinglePrerequisite(bool currentSetting); + + /// Renders a checkbox indicating whether characters should craft at full rate while travelling (compared to 25% rate while travelling) + /// Current value + /// The value of the UI checkbox + bool Evaluate_CraftAtFullSpeedWhileAdventuring(bool currentSetting); + + /// Renders a selection for the options for custom crafting price adjustments and returns the currently selected value + /// Label to display for the selection UI control + /// Collection of options that the user can select from + /// The index of the current selection in the UI control + int Evaluate_CraftingCostSelection(string priceLabel, string[] craftingPriceStrings); + + /// Renders a checkbox indicating whether crafting costs any gold and returns its current UI value + /// Current value + /// The value of the UI checkbox + bool Evaluate_CraftingCostsNoGold(bool currentSetting); + + /// Renders a checkbox indicating whether crafting should take time to complete or not + /// Current value + /// The value of the UI checkbox + bool Evaluate_CraftingTakesNoTime(bool currentSetting); + + /// Renders a slider to Unity Mod Manager for the custom crafting % cost. + /// Current setting to display on the control + /// The selection that the UI currently has registered + float Evaluate_CustomCraftingCostSlider(float currentSetting); + + /// Renders a checkbox indicating whether crafting should take a non-standard rate or time + /// Current value + /// The value of the UI checkbox + bool Evaluate_CustomCraftRate(bool currentSetting); + + /// Renders a checkbox indicating whether crafting should ignore feats + /// Current value + /// The value of the UI checkbox + bool Evaluate_IgnoreCraftingFeats(bool currentSetting); + + /// Renders a checkbox indicating whether crafting feats can ignore caster level prerequisites + /// Current value + /// The value of the UI checkbox + bool Evaluate_IgnoreFeatCasterLevelRestriction(bool currentSetting); + + /// Renders a checkbox indicating whether weapons and armor should be allowed to exceed the +10 enchantment value + /// Current value + /// The value of the UI checkbox + bool Evaluate_IgnorePlusTenItemMaximum(bool currentSetting); + + /// Renders a slider to Unity Mod Manager for the custom magic crafting rate. + /// Current setting to display on the control + /// The selection that the UI currently has registered + int Evaluate_MagicCraftingRateSlider(int currentSetting); + + /// Renders a slider to Unity Mod Manager for the custom mundane crafting rate. + /// Current setting to display on the control + /// The selection that the UI currently has registered + int Evaluate_MundaneCraftingRateSlider(int currentSetting); + + /// Renders a warning that price disparity between custom items crafts to non-custom items will have a selling cost disparity between crafting and sale. + void RenderOnly_WarningAboutCustomItemVanillaItemCostDisparity(); + } +} \ No newline at end of file diff --git a/CraftMagicItems/UI/Sections/IFeatReassignmentSectionRenderer.cs b/CraftMagicItems/UI/Sections/IFeatReassignmentSectionRenderer.cs new file mode 100644 index 0000000..fa46fbb --- /dev/null +++ b/CraftMagicItems/UI/Sections/IFeatReassignmentSectionRenderer.cs @@ -0,0 +1,24 @@ +namespace CraftMagicItems.UI.Sections +{ + /// Interface defining the I/O around the user interface for the feat reassignment section and its returned user input + public interface IFeatReassignmentSectionRenderer + { + /// Renders a label and button for selecting a current feat to be replaced by the selected replacement crafting feat + /// Name of the existing feat to potentially be replaced + /// Name of the feat to potentially replace + /// True if the button is clicked, otherwise false + bool Evaluate_LearnFeatButton(string existingFeat, string replacementFeat); + + /// Renders a selection for the options for selecting a missing casting feat + /// Collection of feat names that are missing from the currently selected character + /// The selected index of the feats + int Evaluate_MissingFeatSelection(string[] featOptions); + + /// Renders a message describing how to use the section + void RenderOnly_UsageExplanation(); + + /// Renders a warning that the current character does not qualify for any crafting feats + /// Name of character currently being evaluated + void RenderOnly_Warning_NoCraftingFeatQualifications(string characterName); + } +} \ No newline at end of file diff --git a/CraftMagicItems/UI/UnityModManager/UmmUiRenderer.cs b/CraftMagicItems/UI/UnityModManager/UmmUiRenderer.cs new file mode 100644 index 0000000..738ce3a --- /dev/null +++ b/CraftMagicItems/UI/UnityModManager/UmmUiRenderer.cs @@ -0,0 +1,172 @@ +using UnityEngine; + +namespace CraftMagicItems.UI.UnityModManager +{ + /// Class that handles the Unity Mod Manager UI rendering + public class UmmUiRenderer + { + /// Renders a checkbox in Unity Mod Manager + /// Label for the checkbox + /// Currently selected value + /// The inverse of when the button is clicked, otherwise + public static bool RenderCheckbox(string label, bool value) + { + GUILayout.BeginHorizontal(); + var text = value ? "✔" : "✖"; + var color = value ? "green" : "red"; + if (GUILayout.Button($"{text} {label}", GUILayout.ExpandWidth(false))) + { + value = !value; + } + + GUILayout.EndHorizontal(); + + return value; + } + + /// Renders a text box on screen in Unity Mod Manager for the item's customized name + /// Default value for the item's name + /// Currently-selected custom name + /// The updated text as entered by user, otherwise + public static string RenderCustomNameField(string defaultValue, string selectedCustomName) + { + GUILayout.BeginHorizontal(); + GUILayout.Label("Name: ", GUILayout.ExpandWidth(false)); + if (string.IsNullOrEmpty(selectedCustomName)) + { + selectedCustomName = defaultValue; + } + + selectedCustomName = GUILayout.TextField(selectedCustomName, GUILayout.Width(300)); + if (selectedCustomName.Trim().Length == 0) + { + selectedCustomName = null; + } + + GUILayout.EndHorizontal(); + + return selectedCustomName; + } + + /// Renders an integer selection slider + /// Label for the slider + /// Initial value + /// Minimum possible value + /// Maximum possible value + /// Returns the value selected by the user, clamped and rounded after rendering controls to the screen + public static int RenderIntSlider(string label, int value, int min, int max) + { + value = Mathf.Clamp(value, min, max); + var newValue = RenderFloatSlider(label, value, min, max); + + return Mathf.RoundToInt(newValue); + } + + /// Renders an integer selection slider + /// Label for the slider + /// Initial value + /// Minimum possible value + /// Maximum possible value + /// Returns the value selected by the user, clamped and rounded after rendering controls to the screen + public static float RenderFloatSlider(string label, float value, float min, float max) + { + GUILayout.BeginHorizontal(); + GUILayout.Label(label, GUILayout.ExpandWidth(false)); + var newValue = GUILayout.HorizontalSlider(value, min, max, GUILayout.Width(300)); + GUILayout.Label(newValue.ToString(), GUILayout.ExpandWidth(false)); + GUILayout.EndHorizontal(); + + return newValue; + } + + /// Renders a toggle-able section selection in Unity Mod Manager for the user to show/hide + /// Label for the toggle + /// Flag indicating whether the toggle is active + /// Whether the toggle is currently active in Unity Mod Manager + public static bool RenderToggleSection(string label, bool value) + { + GUILayout.BeginVertical("box"); + GUILayout.BeginHorizontal(); + bool toggledOn = GUILayout.Toggle(value, " " + label + ""); + + GUILayout.EndHorizontal(); + GUILayout.EndVertical(); + return toggledOn; + } + + /// Renders a Label control as its own line in Unity Mod Manager + /// Text to be displayed + public static void RenderLabelRow(string label) + { + GUILayout.BeginHorizontal(); + GUILayout.Label(label); + GUILayout.EndHorizontal(); + } + + /// Renders a Label control in Unity Mod Manager + /// Text to be displayed + public static void RenderLabel(string label) + { + GUILayout.Label(label); + } + + /// Renders a Label control in Unity Mod Manager + /// Text to be displayed + /// Should the label be expanded + public static void RenderLabel(string label, bool expandWidth) + { + GUILayout.Label(label, GUILayout.ExpandWidth(expandWidth)); + } + + /// Renders a button control in Unity Mod Manager + /// Text to be displayed on the control + /// True if the button is clicked, otherwise false + public static bool RenderButton(string label) + { + return GUILayout.Button(label); + } + + /// Renders a button control in Unity Mod Manager + /// Text to be displayed on the control + /// Should the control be expanded + /// True if the button is clicked, otherwise false + public static bool RenderButton(string label, bool expandWidth) + { + return GUILayout.Button(label, GUILayout.ExpandWidth(expandWidth)); + } + + /// Renders a selection of to Unity Mod Manager + /// Label for the selection + /// Options for the selection + /// Index within to use for current selection + /// How many elements to fit in the horizontal direction + /// Space the UI by 20 pixels? + /// The new index of the selection + public static int RenderSelection(string label, string[] options, int optionsIndex, int horizontalCount, bool addSpace) + { + if (addSpace) + { + GUILayout.Space(20); + } + + GUILayout.BeginHorizontal(); + GUILayout.Label(label, GUILayout.ExpandWidth(false)); + var newIndex = GUILayout.SelectionGrid(optionsIndex, options, horizontalCount); + GUILayout.EndHorizontal(); + + return newIndex; + } + + /// In the rendered Unity Mod Manager UI, sets the location of the UI control to the next horizontal alignment + public static void RenderHorizontalStart() + { + GUILayout.BeginHorizontal(); + } + + /// In the rendered Unity Mod Manager UI, sets the end of a horizontal alignment + public static void RenderHorizontalEnd() + { + GUILayout.EndHorizontal(); + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/UI/UserInterfaceEventHandlingLogic.cs b/CraftMagicItems/UI/UserInterfaceEventHandlingLogic.cs new file mode 100644 index 0000000..f37dc03 --- /dev/null +++ b/CraftMagicItems/UI/UserInterfaceEventHandlingLogic.cs @@ -0,0 +1,142 @@ +using System; +using System.Linq; +using CraftMagicItems.Constants; +using CraftMagicItems.Localization; +using CraftMagicItems.UI.Sections; +using Kingmaker.Blueprints; +using Kingmaker.Blueprints.Classes; +using Kingmaker.UI.ActionBar; +using Kingmaker.UnitLogic.FactLogic; + +namespace CraftMagicItems.UI +{ + /// Class that performs User Interface rendering and takes the results freom such and performs logical operations based on the UI state + public static class UserInterfaceEventHandlingLogic + { + /// Collection of containing the display text for various pricing guidelines + private static readonly string[] CraftingPriceStrings = + { + "100% (Owlcat prices)", + "200% (Tabletop prices)", + "Custom" + }; + + /// Renders the Cheats section and retrieves the values specified by its rendered UI + /// instance used to render controls and return current values + /// to default to and to read from + /// Text to render for the price + public static void RenderCheatsSectionAndUpdateSettings(ICheatSectionRenderer renderer, Settings modSettings, string priceLabel) + { + modSettings.CraftingCostsNoGold = renderer.Evaluate_CraftingCostsNoGold(modSettings.CraftingCostsNoGold); + if (!modSettings.CraftingCostsNoGold) + { + var selectedCustomPriceScaleIndex = renderer.Evaluate_CraftingCostSelection(priceLabel, CraftingPriceStrings); + if (selectedCustomPriceScaleIndex == 2) //if user selected "Custom" + { + modSettings.CraftingPriceScale = renderer.Evaluate_CustomCraftingCostSlider(modSettings.CraftingPriceScale); + } + else + { + //index 0 = 100%; index 1 = 200% + modSettings.CraftingPriceScale = 1 + selectedCustomPriceScaleIndex; + } + + if (selectedCustomPriceScaleIndex != 0) + { + renderer.RenderOnly_WarningAboutCustomItemVanillaItemCostDisparity(); + } + } + + modSettings.IgnoreCraftingFeats = renderer.Evaluate_IgnoreCraftingFeats(modSettings.IgnoreCraftingFeats); + modSettings.CraftingTakesNoTime = renderer.Evaluate_CraftingTakesNoTime(modSettings.CraftingTakesNoTime); + if (!modSettings.CraftingTakesNoTime) + { + modSettings.CustomCraftRate = renderer.Evaluate_CustomCraftRate(modSettings.CustomCraftRate); + if (modSettings.CustomCraftRate) + { + modSettings.MagicCraftingRate = renderer.Evaluate_MagicCraftingRateSlider(modSettings.MagicCraftingRate); + modSettings.MundaneCraftingRate = renderer.Evaluate_MundaneCraftingRateSlider(modSettings.MundaneCraftingRate); + } + else + { + modSettings.MagicCraftingRate = Settings.MagicCraftingProgressPerDay; + modSettings.MundaneCraftingRate = Settings.MundaneCraftingProgressPerDay; + } + } + + modSettings.CasterLevelIsSinglePrerequisite = renderer.Evaluate_CasterLevelIsSinglePrerequisite(modSettings.CasterLevelIsSinglePrerequisite); + modSettings.CraftAtFullSpeedWhileAdventuring = renderer.Evaluate_CraftAtFullSpeedWhileAdventuring(modSettings.CraftAtFullSpeedWhileAdventuring); + modSettings.IgnorePlusTenItemMaximum = renderer.Evaluate_IgnorePlusTenItemMaximum(modSettings.IgnorePlusTenItemMaximum); + modSettings.IgnoreFeatCasterLevelRestriction = renderer.Evaluate_IgnoreFeatCasterLevelRestriction(modSettings.IgnoreFeatCasterLevelRestriction); + } + + /// Renders the section for feat reassignment and handles user selections + /// instance that handles rendering of controls + public static void RenderFeatReassignmentSection(IFeatReassignmentSectionRenderer renderer) + { + var caster = Main.GetSelectedCrafter(false); + if (caster == null) + { + return; + } + + var casterLevel = Main.CharacterCasterLevel(caster.Descriptor); + var missingFeats = Main.LoadedData.ItemCraftingData + .Where(data => data.FeatGuid != null && !Main.CharacterHasFeat(caster, data.FeatGuid) && data.MinimumCasterLevel <= casterLevel) + .ToArray(); + if (missingFeats.Length == 0) + { + renderer.RenderOnly_Warning_NoCraftingFeatQualifications(caster.CharacterName); + return; + } + + renderer.RenderOnly_UsageExplanation(); + var featOptions = missingFeats.Select(data => new L10NString(data.NameId).ToString()).ToArray(); + var selectedFeatToLearn = renderer.Evaluate_MissingFeatSelection(featOptions); + var learnFeatData = missingFeats[selectedFeatToLearn]; + var learnFeat = ResourcesLibrary.TryGetBlueprint(learnFeatData.FeatGuid); + if (learnFeat == null) + { + throw new Exception($"Unable to find feat with guid {learnFeatData.FeatGuid}"); + } + + var removedFeatIndex = 0; + foreach (var feature in caster.Descriptor.Progression.Features) + { + if (!feature.Blueprint.HideInUI && feature.Blueprint.HasGroup(Features.CraftingFeatGroups) + && (feature.SourceProgression != null || feature.SourceRace != null)) + { + if (renderer.Evaluate_LearnFeatButton(feature.Name, learnFeat.Name)) + { + var currentRank = feature.Rank; + caster.Descriptor.Progression.ReplaceFeature(feature.Blueprint, learnFeat); + if (currentRank == 1) + { + foreach (var addFact in feature.SelectComponents((AddFacts addFacts) => true)) + { + addFact.OnFactDeactivate(); + } + + caster.Descriptor.Progression.Features.RemoveFact(feature); + } + + var addedFeature = caster.Descriptor.Progression.Features.AddFeature(learnFeat); + addedFeature.Source = feature.Source; + + var mFacts = caster.Descriptor.Progression.Features.RawFacts; + if (removedFeatIndex < mFacts.Count) + { + // Move the new feat to the place in the list originally occupied by the removed one. + mFacts.Remove(addedFeature); + mFacts.Insert(removedFeatIndex, addedFeature); + } + + ActionBarManager.Instance.HandleAbilityRemoved(null); + } + } + + removedFeatIndex++; + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItems/WildEnchantment.cs b/CraftMagicItems/WildEnchantment.cs index 46730fe..5e812dc 100644 --- a/CraftMagicItems/WildEnchantment.cs +++ b/CraftMagicItems/WildEnchantment.cs @@ -1,3 +1,4 @@ +using CraftMagicItems.Localization; using Kingmaker; #if PATCH21 using Kingmaker.Assets.UI.Context; @@ -15,105 +16,130 @@ using Kingmaker.UnitLogic.Buffs.Blueprints; using Kingmaker.UnitLogic.Mechanics; -namespace CraftMagicItems { - public class WildFact : BlueprintBuff { - public WildFact() { +namespace CraftMagicItems.Enchantments +{ + public class WildFact : BlueprintBuff + { + public WildFact() + { Main.Accessors.SetBlueprintBuffFlags(this, 2 + 8); // Enum is private... 2 = HiddenInUi, 8 = StayOnDeath Stacking = StackingType.Replace; Frequency = DurationRate.Rounds; FxOnStart = new PrefabLink(); FxOnRemove = new PrefabLink(); - Main.Accessors.SetBlueprintUnitFactDisplayName(this, new L10NString("craftMagicItems-enchantment-wild-name")); - Main.Accessors.SetBlueprintUnitFactDescription(this, new L10NString("craftMagicItems-enchantment-wild-description")); + Main.Accessors.SetBlueprintUnitFactDisplayName(this) = new L10NString("craftMagicItems-enchantment-wild-name"); + Main.Accessors.SetBlueprintUnitFactDescription(this) = new L10NString("craftMagicItems-enchantment-wild-description"); } } - public class WildEnchantmentLogic : AddUnitFactEquipment { + public class WildEnchantmentLogic : AddUnitFactEquipment + { private const string WildShapeTurnBackAbilityGuid = "a2cb181ee69860b46b82844a3a8569b8"; private ModifiableValue.Modifier modifier; - public WildEnchantmentLogic() { + public WildEnchantmentLogic() + { name = "AddUnitFactEquipment-WildFact"; } - private bool IsOwnerWildShaped() { + private bool IsOwnerWildShaped() + { var wildShapeTurnBackAbility = ResourcesLibrary.TryGetBlueprint(WildShapeTurnBackAbilityGuid); return Owner.Owner.HasFact(wildShapeTurnBackAbility); } - private void ApplyModifier() { + private void ApplyModifier() + { // Apply AC modifier - if (Owner.Owner == null || modifier != null) { + if (Owner.Owner == null || modifier != null) + { return; } var stat = Owner.Owner.Stats.GetStat(StatType.AC); - switch (Owner) { - case ItemEntityArmor armor: { - var acBonus = armor.Blueprint.ArmorBonus + GameHelper.GetItemEnhancementBonus(armor); - modifier = stat.AddItemModifier(acBonus, armor, ModifierDescriptor.Armor); - break; - } - case ItemEntityShield shield: { - var acBonus = shield.Blueprint.ArmorComponent.ArmorBonus + GameHelper.GetItemEnhancementBonus(shield.ArmorComponent); - modifier = stat.AddItemModifier(acBonus, shield, ModifierDescriptor.Shield); - break; - } + switch (Owner) + { + case ItemEntityArmor armor: + { + var acBonus = armor.Blueprint.ArmorBonus + GameHelper.GetItemEnhancementBonus(armor); + modifier = stat.AddItemModifier(acBonus, armor, ModifierDescriptor.Armor); + break; + } + case ItemEntityShield shield: + { + var acBonus = shield.Blueprint.ArmorComponent.ArmorBonus + GameHelper.GetItemEnhancementBonus(shield.ArmorComponent); + modifier = stat.AddItemModifier(acBonus, shield, ModifierDescriptor.Shield); + break; + } } } - public override void OnFactActivate() { + public override void OnFactActivate() + { base.OnFactActivate(); - if (!IsOwnerWildShaped() && modifier != null) { + if (!IsOwnerWildShaped() && modifier != null) + { modifier.Remove(); modifier = null; } } - public override void OnFactDeactivate() { - if (IsOwnerWildShaped()) { + public override void OnFactDeactivate() + { + if (IsOwnerWildShaped()) + { ApplyModifier(); - } else { + } + else + { base.OnFactDeactivate(); } } - public override void PostLoad() { + public override void PostLoad() + { base.PostLoad(); - if (IsOwnerWildShaped()) { + if (IsOwnerWildShaped()) + { ApplyModifier(); } } } - public class WildEnchantment : BlueprintItemEnchantment { + public class WildEnchantment : BlueprintItemEnchantment + { private const string WildEnchantmentGuid = "dd0e096412423d646929d9b945fd6d4c#CraftMagicItems(wildEnchantment)"; private const string WildFactGuid = "28384b1d7e25c8743b8bbfc56211ac8c#CraftMagicItems(wildFact)"; private static bool initialised; - public WildEnchantment() { + public WildEnchantment() + { this.name = "Wild"; - Main.Accessors.SetBlueprintItemEnchantmentEnchantName(this, new L10NString("craftMagicItems-enchantment-wild-name")); - Main.Accessors.SetBlueprintItemEnchantmentDescription(this, new L10NString("craftMagicItems-enchantment-wild-description")); - Main.Accessors.SetBlueprintItemEnchantmentPrefix(this, new L10NString("")); - Main.Accessors.SetBlueprintItemEnchantmentSuffix(this, new L10NString("")); - Main.Accessors.SetBlueprintItemEnchantmentEnchantmentCost(this, 1); - Main.Accessors.SetBlueprintItemEnchantmentEnchantmentIdentifyDC(this, 5); + Main.Accessors.SetBlueprintItemEnchantmentEnchantName(this) = new L10NString("craftMagicItems-enchantment-wild-name"); + Main.Accessors.SetBlueprintItemEnchantmentDescription(this) = new L10NString("craftMagicItems-enchantment-wild-description"); + Main.Accessors.SetBlueprintItemEnchantmentPrefix(this) = new L10NString(""); + Main.Accessors.SetBlueprintItemEnchantmentSuffix(this) = new L10NString(""); + Main.Accessors.SetBlueprintItemEnchantmentEnchantmentCost(this) = 1; + Main.Accessors.SetBlueprintItemEnchantmentEnchantmentIdentifyDC(this) = 5; } - [Harmony12.HarmonyPatch(typeof(MainMenu), "Start")] + [HarmonyLib.HarmonyPatch(typeof(MainMenu), "Start")] // ReSharper disable once UnusedMember.Local - public static class MainMenuStartPatch { - private static void AddBlueprint(string guid, BlueprintScriptableObject blueprint) { - Main.Accessors.SetBlueprintScriptableObjectAssetGuid(blueprint, guid); + public static class MainMenuStartPatch + { + private static void AddBlueprint(string guid, BlueprintScriptableObject blueprint) + { + Main.Accessors.SetBlueprintScriptableObjectAssetGuid(blueprint) = guid; ResourcesLibrary.LibraryObject.BlueprintsByAssetId?.Add(guid, blueprint); ResourcesLibrary.LibraryObject.GetAllBlueprints()?.Add(blueprint); } // ReSharper disable once UnusedMember.Local - public static void Postfix() { - if (!initialised) { + public static void Postfix() + { + if (!initialised) + { initialised = true; var blueprintWildEnchantment = CreateInstance(); AddBlueprint(WildEnchantmentGuid, blueprintWildEnchantment); @@ -121,14 +147,17 @@ public static void Postfix() { AddBlueprint(WildFactGuid, blueprintWildFact); var wildEnchantmentLogic = CreateInstance(); wildEnchantmentLogic.Blueprint = blueprintWildFact; - blueprintWildEnchantment.ComponentsArray = new BlueprintComponent[] {wildEnchantmentLogic}; + blueprintWildEnchantment.ComponentsArray = new BlueprintComponent[] { wildEnchantmentLogic }; } } } + #if PATCH21 - [Harmony12.HarmonyPatch(typeof(MainMenuUiContext), "Initialize")] - private static class MainMenuUiContextInitializePatch { - private static void Postfix() { + [HarmonyLib.HarmonyPatch(typeof(MainMenuUiContext), "Initialize")] + private static class MainMenuUiContextInitializePatch + { + private static void Postfix() + { MainMenuStartPatch.Postfix(); } } diff --git a/CraftMagicItemsTests/Constants/VisualAdjustmentPatchesTests.cs b/CraftMagicItemsTests/Constants/VisualAdjustmentPatchesTests.cs new file mode 100644 index 0000000..14854c8 --- /dev/null +++ b/CraftMagicItemsTests/Constants/VisualAdjustmentPatchesTests.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Linq; +using CraftMagicItems.Constants; +using CraftMagicItems.Patches; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace CraftMagicItemsTests.Constants +{ + [TestClass] + public class VisualAdjustmentPatchesTests + { + [TestMethod] + public void IkPatchList_Exposes_DuelingSword() + { + AssertBlueprintInIkPatchCollection("a6f7e3dc443ff114ba68b4648fd33e9f", VisualAdjustmentPatches.LeftHandedWeaponPatchList); + } + + [TestMethod] + public void IkPatchList_Exposes_Tongi() + { + AssertBlueprintInIkPatchCollection("13fa38737d46c9e4abc7f4d74aaa59c3", VisualAdjustmentPatches.LeftHandedWeaponPatchList); + } + + [TestMethod] + public void IkPatchList_Exposes_Falcata() + { + AssertBlueprintInIkPatchCollection("1af5621e2ae551e42bd1dd6744d98639", VisualAdjustmentPatches.LeftHandedWeaponPatchList); + } + + [TestMethod] + public void IkPatchList_Exposes_Estoc() + { + AssertBlueprintInIkPatchCollection("d516765b3c2904e4a939749526a52a9a", VisualAdjustmentPatches.LeftHandedWeaponPatchList); + } + + [TestMethod] + public void IkPatchList_Exposes_Rapier() + { + AssertBlueprintInIkPatchCollection("2ece38f30500f454b8569136221e55b0", VisualAdjustmentPatches.LeftHandedWeaponPatchList); + } + + [TestMethod] + public void IkPatchList_Exposes_HeavyPick() + { + AssertBlueprintInIkPatchCollection("a492410f3d65f744c892faf09daad84a", VisualAdjustmentPatches.LeftHandedWeaponPatchList); + } + + [TestMethod] + public void IkPatchList_Exposes_Trident() + { + AssertBlueprintInIkPatchCollection("6ff66364e0a2c89469c2e52ebb46365e", VisualAdjustmentPatches.LeftHandedWeaponPatchList); + } + + [TestMethod] + public void IkPatchList_Exposes_HeavyMace() + { + AssertBlueprintInIkPatchCollection("d5a167f0f0208dd439ec7481e8989e21", VisualAdjustmentPatches.LeftHandedWeaponPatchList); + } + + [TestMethod] + public void IkPatchList_Exposes_HeavyFlail() + { + AssertBlueprintInIkPatchCollection("8fefb7e0da38b06408f185e29372c703", VisualAdjustmentPatches.LeftHandedWeaponPatchList); + } + + private void AssertBlueprintInIkPatchCollection(string uuid, IEnumerable collection) + { + IkPatch instance = collection.Single(patch => patch.BlueprintId == uuid); + Assert.AreNotEqual(default, instance); + } + } +} \ No newline at end of file diff --git a/CraftMagicItemsTests/CraftMagicItemsTests.csproj b/CraftMagicItemsTests/CraftMagicItemsTests.csproj new file mode 100644 index 0000000..86e91eb --- /dev/null +++ b/CraftMagicItemsTests/CraftMagicItemsTests.csproj @@ -0,0 +1,111 @@ + + + + + + Debug + AnyCPU + {FDC2E2FA-48A9-4F1B-9C9B-1B1D43628884} + Library + Properties + CraftMagicItemsTests + CraftMagicItemsTests + v4.6 + 512 + {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + 15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages + False + UnitTest + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE;PATCH20 + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE;PATCH20 + prompt + 4 + + + true + bin\Debug %282.1%29\ + TRACE;DEBUG;PATCH21 + full + AnyCPU + 7.3 + prompt + MinimumRecommendedRules.ruleset + + + bin\Release %282.1%29\ + TRACE;PATCH21 + true + AnyCPU + 7.3 + prompt + MinimumRecommendedRules.ruleset + + + + ..\packages\Castle.Core.4.4.0\lib\net45\Castle.Core.dll + + + ..\packages\MSTest.TestFramework.2.1.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + + + ..\packages\MSTest.TestFramework.2.1.0\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + + + ..\packages\Moq.4.14.4\lib\net45\Moq.dll + + + + + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.0\lib\netstandard1.0\System.Runtime.CompilerServices.Unsafe.dll + + + ..\packages\System.Threading.Tasks.Extensions.4.5.1\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll + + + + + + + + + + + + + + + + + {9379c37f-b226-4f81-893d-372f0cceaae5} + CraftMagicItems + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + \ No newline at end of file diff --git a/CraftMagicItemsTests/MainRenderTests.cs b/CraftMagicItemsTests/MainRenderTests.cs new file mode 100644 index 0000000..c2f5fb6 --- /dev/null +++ b/CraftMagicItemsTests/MainRenderTests.cs @@ -0,0 +1,15 @@ +using System; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using CraftMagicItems; + +namespace CraftMagicItemsTests +{ + [TestClass] + public class MainRenderTests + { + [TestMethod] + public void RenderMultiLayerSkills_Works() + { + } + } +} \ No newline at end of file diff --git a/CraftMagicItemsTests/Properties/AssemblyInfo.cs b/CraftMagicItemsTests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..f87436d --- /dev/null +++ b/CraftMagicItemsTests/Properties/AssemblyInfo.cs @@ -0,0 +1,20 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +[assembly: AssemblyTitle("CraftMagicItemsUnitTests")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("CraftMagicItemsUnitTests")] +[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +[assembly: ComVisible(false)] + +[assembly: Guid("fdc2e2fa-48a9-4f1b-9c9b-1b1d43628884")] + +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/CraftMagicItemsTests/UI/BattleLog/BattleLogFactoryTests.cs b/CraftMagicItemsTests/UI/BattleLog/BattleLogFactoryTests.cs new file mode 100644 index 0000000..101d5ff --- /dev/null +++ b/CraftMagicItemsTests/UI/BattleLog/BattleLogFactoryTests.cs @@ -0,0 +1,64 @@ +using System; +using CraftMagicItems.UI.BattleLog; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace CraftMagicItemsTests.UI.BattleLog +{ + /// Test class for + [TestClass] + public class BattleLogFactoryTests + { + [TestMethod] + public void BattleLogFactory_Defaults_KingmakerBattleLog() + { + //control + BattleLogFactory.Reset(); + + //invocation + var instance = BattleLogFactory.GetBattleLog(); + + //validation + Assert.AreEqual(typeof(KingmakerBattleLog), instance.GetType(), $"Expected an instance of {nameof(KingmakerBattleLog)} to be returned."); + } + + [TestMethod] + public void SetConstructor_Works() + { + //control + Func mockConstructor = () => { return new Mock().Object; }; + BattleLogFactory.SetConstructor(mockConstructor); + + //invocation + var instance = BattleLogFactory.GetBattleLog(); + + //validation + Assert.AreNotEqual(typeof(KingmakerBattleLog), instance.GetType(), $"Expected an instance of {nameof(KingmakerBattleLog)} to be returned."); + } + + [TestMethod] + public void GetBattleLog_Works() + { + //invocation + var instance = BattleLogFactory.GetBattleLog(); + + //validation + Assert.IsNotNull(instance, $"Expected an instance of {nameof(IBattleLog)} to be returned."); + } + + [TestMethod] + public void Reset_Works() + { + //control + Func mockConstructor = () => { return new Mock().Object; }; + BattleLogFactory.SetConstructor(mockConstructor); + BattleLogFactory.Reset(); + + //invocation + var instance = BattleLogFactory.GetBattleLog(); + + //validation + Assert.AreEqual(typeof(KingmakerBattleLog), instance.GetType(), $"Expected an instance of {nameof(KingmakerBattleLog)} to be returned."); + } + } +} \ No newline at end of file diff --git a/CraftMagicItemsTests/UI/Sections/CheatSectionRendererFactoryTests.cs b/CraftMagicItemsTests/UI/Sections/CheatSectionRendererFactoryTests.cs new file mode 100644 index 0000000..59f9480 --- /dev/null +++ b/CraftMagicItemsTests/UI/Sections/CheatSectionRendererFactoryTests.cs @@ -0,0 +1,64 @@ +using System; +using CraftMagicItems.UI.Sections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace CraftMagicItemsTests.UI.Sections +{ + /// Test class for + [TestClass] + public class CheatSectionRendererFactoryTests + { + [TestMethod] + public void CheatSectionRendererFactory_Defaults_CheatSectionRenderer() + { + //control + CheatSectionRendererFactory.Reset(); + + //invocation + var instance = CheatSectionRendererFactory.GetCheatSectionRenderer(); + + //validation + Assert.AreEqual(typeof(CheatSectionRenderer), instance.GetType(), $"Expected an instance of {nameof(CheatSectionRenderer)} to be returned."); + } + + [TestMethod] + public void SetConstructor_Works() + { + //control + Func mockConstructor = () => { return new Mock().Object; }; + CheatSectionRendererFactory.SetConstructor(mockConstructor); + + //invocation + var instance = CheatSectionRendererFactory.GetCheatSectionRenderer(); + + //validation + Assert.AreNotEqual(typeof(CheatSectionRenderer), instance.GetType(), $"Expected an instance of {nameof(CheatSectionRenderer)} to be returned."); + } + + [TestMethod] + public void GetCheatSectionRenderer_Works() + { + //invocation + var instance = CheatSectionRendererFactory.GetCheatSectionRenderer(); + + //validation + Assert.IsNotNull(instance, $"Expected an instance of {nameof(ICheatSectionRenderer)} to be returned."); + } + + [TestMethod] + public void Reset_Works() + { + //control + Func mockConstructor = () => { return new Mock().Object; }; + CheatSectionRendererFactory.SetConstructor(mockConstructor); + CheatSectionRendererFactory.Reset(); + + //invocation + var instance = CheatSectionRendererFactory.GetCheatSectionRenderer(); + + //validation + Assert.AreEqual(typeof(CheatSectionRenderer), instance.GetType(), $"Expected an instance of {nameof(CheatSectionRenderer)} to be returned."); + } + } +} \ No newline at end of file diff --git a/CraftMagicItemsTests/UI/Sections/FeatReassignmentSectionRendererFactoryTests.cs b/CraftMagicItemsTests/UI/Sections/FeatReassignmentSectionRendererFactoryTests.cs new file mode 100644 index 0000000..f64c088 --- /dev/null +++ b/CraftMagicItemsTests/UI/Sections/FeatReassignmentSectionRendererFactoryTests.cs @@ -0,0 +1,64 @@ +using System; +using CraftMagicItems.UI.Sections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace CraftMagicItemsTests.UI.Sections +{ + /// Test class for + [TestClass] + public class FeatReassignmentSectionRendererFactoryTests + { + [TestMethod] + public void FeatReassignmentSectionRendererFactory_Defaults_FeatReassignmentSectionRenderer() + { + //control + FeatReassignmentSectionRendererFactory.Reset(); + + //invocation + var instance = FeatReassignmentSectionRendererFactory.GetFeatReassignmentSectionRenderer(); + + //validation + Assert.AreEqual(typeof(FeatReassignmentSectionRenderer), instance.GetType(), $"Expected an instance of {nameof(FeatReassignmentSectionRenderer)} to be returned."); + } + + [TestMethod] + public void SetConstructor_Works() + { + //control + Func mockConstructor = () => { return new Mock().Object; }; + FeatReassignmentSectionRendererFactory.SetConstructor(mockConstructor); + + //invocation + var instance = FeatReassignmentSectionRendererFactory.GetFeatReassignmentSectionRenderer(); + + //validation + Assert.AreNotEqual(typeof(FeatReassignmentSectionRenderer), instance.GetType(), $"Expected an instance of {nameof(FeatReassignmentSectionRenderer)} to be returned."); + } + + [TestMethod] + public void GetFeatReassignmentSectionRenderer_Works() + { + //invocation + var instance = FeatReassignmentSectionRendererFactory.GetFeatReassignmentSectionRenderer(); + + //validation + Assert.IsNotNull(instance, $"Expected an instance of {nameof(IFeatReassignmentSectionRenderer)} to be returned."); + } + + [TestMethod] + public void Reset_Works() + { + //control + Func mockConstructor = () => { return new Mock().Object; }; + FeatReassignmentSectionRendererFactory.SetConstructor(mockConstructor); + FeatReassignmentSectionRendererFactory.Reset(); + + //invocation + var instance = FeatReassignmentSectionRendererFactory.GetFeatReassignmentSectionRenderer(); + + //validation + Assert.AreEqual(typeof(FeatReassignmentSectionRenderer), instance.GetType(), $"Expected an instance of {nameof(FeatReassignmentSectionRenderer)} to be returned."); + } + } +} \ No newline at end of file diff --git a/CraftMagicItemsTests/UI/UserInterfaceEventHandlingLogicTests.cs b/CraftMagicItemsTests/UI/UserInterfaceEventHandlingLogicTests.cs new file mode 100644 index 0000000..8f52136 --- /dev/null +++ b/CraftMagicItemsTests/UI/UserInterfaceEventHandlingLogicTests.cs @@ -0,0 +1,549 @@ +using System; +using CraftMagicItems; +using CraftMagicItems.UI; +using CraftMagicItems.UI.Sections; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace CraftMagicItemsTests.UI +{ + /// Test class for + public class UserInterfaceEventHandlingLogicTests + { + /// Test class for + [TestClass] + public class RenderCheatsSectionAndUpdateSettingsTests + { + [TestMethod] + public void Reads_CraftingCostsNoGold() + { + string priceLabel = "irrelevant"; + string[] priceOptions = new[] { String.Empty, String.Empty, String.Empty }; + + /************ + * Test 1 * + ************/ + //control + Settings settings = new Settings { CraftingCostsNoGold = false }; + Mock renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CraftingCostsNoGold(It.IsAny())).Returns(true); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(true, settings.CraftingCostsNoGold); + + /************ + * Test 2 * + ************/ + //control + settings = new Settings { CraftingCostsNoGold = true }; + renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CraftingCostsNoGold(It.IsAny())).Returns(false); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(false, settings.CraftingCostsNoGold); + } + + [TestMethod] + public void DoesNotRead_CraftingPriceScale_When_CraftingCostsNoGold() + { + string priceLabel = "irrelevant"; + string[] priceOptions = new[] { String.Empty, String.Empty, String.Empty }; + int initial = -10000000; + + //control + Settings settings = new Settings + { + CraftingPriceScale = initial, + }; + Mock renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CraftingCostsNoGold(It.IsAny())).Returns(true); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(initial, settings.CraftingPriceScale); + } + + [TestMethod] + public void DoesNotInvoke_WarningAboutCustomItemVanillaItemCostDisparity_When_CraftingCostsNoGold() + { + string priceLabel = "irrelevant"; + string[] priceOptions = new[] { String.Empty, String.Empty, String.Empty }; + + //control + bool invokedWarning = false; + Action setInvoked = () => { invokedWarning = true; }; + + Mock renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CraftingCostsNoGold(It.IsAny())).Returns(true); + renderer.Setup(r => r.RenderOnly_WarningAboutCustomItemVanillaItemCostDisparity()).Callback(setInvoked); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, new Settings(), priceLabel); + + //validation + Assert.AreEqual(false, invokedWarning); + } + + [TestMethod] + public void Reads_CraftingCostSelection() + { + string priceLabel = "irrelevant"; + string[] priceOptions = new[] { String.Empty, String.Empty, String.Empty }; + + //control + bool invokedWarning = false; + Action setInvoked = () => { invokedWarning = true; }; + + Mock renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CraftingCostsNoGold(It.IsAny())).Returns(false); + renderer.Setup(r => r.Evaluate_CraftingCostSelection(It.IsAny(), It.IsAny())).Callback(setInvoked); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, new Settings(), priceLabel); + + //validation + Assert.AreEqual(true, invokedWarning); + } + + [TestMethod] + public void Reads_CustomCraftingCostSlider() + { + string priceLabel = "irrelevant"; + string[] priceOptions = new[] { String.Empty, String.Empty, String.Empty }; + + //control + Settings settings = new Settings { CraftingPriceScale = -4 }; + Mock renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CraftingCostsNoGold(It.IsAny())).Returns(false); + renderer.Setup(r => r.Evaluate_CraftingCostSelection(It.IsAny(), It.IsAny())).Returns(2); + renderer.Setup(r => r.Evaluate_CustomCraftingCostSlider(It.IsAny())).Returns(4000); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(4000, settings.CraftingPriceScale); + } + + [TestMethod] + public void Reads_CraftingPriceScale_From_CraftingCostSelection() + { + string priceLabel = "irrelevant"; + string[] priceOptions = new[] { String.Empty, String.Empty, String.Empty }; + + /************ + * Test 1 * + ************/ + //control + Settings settings = new Settings { CraftingPriceScale = -4 }; + Mock renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CraftingCostsNoGold(It.IsAny())).Returns(false); + renderer.Setup(r => r.Evaluate_CraftingCostSelection(It.IsAny(), It.IsAny())).Returns(1); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(2, settings.CraftingPriceScale); + + /************ + * Test 2 * + ************/ + //control + settings = new Settings { CraftingPriceScale = -4 }; + renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CraftingCostsNoGold(It.IsAny())).Returns(false); + renderer.Setup(r => r.Evaluate_CraftingCostSelection(It.IsAny(), It.IsAny())).Returns(0); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(1, settings.CraftingPriceScale); + } + + [TestMethod] + public void Invokes_WarningAboutCustomItemVanillaItemCostDisparity_When_CraftingCostsGold() + { + string priceLabel = "irrelevant"; + string[] priceOptions = new[] { String.Empty, String.Empty, String.Empty }; + + //control + bool invokedWarning = false; + Action setInvoked = () => { invokedWarning = true; }; + + Mock renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CraftingCostsNoGold(It.IsAny())).Returns(false); + renderer.Setup(r => r.Evaluate_CraftingCostSelection(It.IsAny(), It.IsAny())).Returns(-1); + renderer.Setup(r => r.RenderOnly_WarningAboutCustomItemVanillaItemCostDisparity()).Callback(setInvoked); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, new Settings(), priceLabel); + + //validation + Assert.AreEqual(true, invokedWarning); + } + + [TestMethod] + public void Reads_IgnoreCraftingFeats() + { + string priceLabel = "irrelevant"; + string[] priceOptions = new[] { String.Empty, String.Empty, String.Empty }; + + /************ + * Test 1 * + ************/ + //control + Settings settings = new Settings { IgnoreCraftingFeats = false }; + Mock renderer = new Mock(); + renderer.Setup(r => r.Evaluate_IgnoreCraftingFeats(It.IsAny())).Returns(true); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(true, settings.IgnoreCraftingFeats); + + /************ + * Test 2 * + ************/ + //control + settings = new Settings { IgnoreCraftingFeats = true }; + renderer = new Mock(); + renderer.Setup(r => r.Evaluate_IgnoreCraftingFeats(It.IsAny())).Returns(false); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(false, settings.IgnoreCraftingFeats); + } + + [TestMethod] + public void Reads_CraftingTakesNoTime() + { + string priceLabel = "irrelevant"; + string[] priceOptions = new[] { String.Empty, String.Empty, String.Empty }; + + /************ + * Test 1 * + ************/ + //control + Settings settings = new Settings { CraftingTakesNoTime = false }; + Mock renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CraftingTakesNoTime(It.IsAny())).Returns(true); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(true, settings.CraftingTakesNoTime); + + /************ + * Test 2 * + ************/ + //control + settings = new Settings { CraftingTakesNoTime = true }; + renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CraftingTakesNoTime(It.IsAny())).Returns(false); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(false, settings.CraftingTakesNoTime); + } + + [TestMethod] + public void DoesNotRead_CustomCraftRate_When_CraftingTakesNoTime() + { + string priceLabel = "irrelevant"; + string[] priceOptions = new[] { String.Empty, String.Empty, String.Empty }; + int initialMagicCraftRate = -10000000; + int initialMundaneCraftRate = 9348458; + bool initialCustomCraftRate = true; + + //control + Settings settings = new Settings + { + MagicCraftingRate = initialMagicCraftRate, + MundaneCraftingRate = initialMundaneCraftRate, + CustomCraftRate = initialCustomCraftRate, + }; + Mock renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CraftingTakesNoTime(It.IsAny())).Returns(true); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(initialMagicCraftRate, settings.MagicCraftingRate); + Assert.AreEqual(initialMundaneCraftRate, settings.MundaneCraftingRate); + Assert.AreEqual(initialCustomCraftRate, settings.CustomCraftRate); + } + + [TestMethod] + public void Reads_CustomCraftRate() + { + string priceLabel = "irrelevant"; + string[] priceOptions = new[] { String.Empty, String.Empty, String.Empty }; + + /************ + * Test 1 * + ************/ + //control + Settings settings = new Settings { CustomCraftRate = false }; + Mock renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CraftingTakesNoTime(It.IsAny())).Returns(false); + renderer.Setup(r => r.Evaluate_CustomCraftRate(It.IsAny())).Returns(true); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(true, settings.CustomCraftRate); + + /************ + * Test 2 * + ************/ + //control + settings = new Settings { CustomCraftRate = true }; + renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CraftingTakesNoTime(It.IsAny())).Returns(false); + renderer.Setup(r => r.Evaluate_CustomCraftRate(It.IsAny())).Returns(false); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(false, settings.CustomCraftRate); + } + + [TestMethod] + public void Reads_MagicCraftingRate() + { + string priceLabel = "irrelevant"; + string[] priceOptions = new[] { String.Empty, String.Empty, String.Empty }; + + //control + Settings settings = new Settings { MagicCraftingRate = -8 }; + Mock renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CraftingTakesNoTime(It.IsAny())).Returns(false); + renderer.Setup(r => r.Evaluate_CustomCraftRate(It.IsAny())).Returns(true); + renderer.Setup(r => r.Evaluate_MagicCraftingRateSlider(It.IsAny())).Returns(7); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(7, settings.MagicCraftingRate); + } + + [TestMethod] + public void Reads_MundaneCraftingRate() + { + string priceLabel = "irrelevant"; + string[] priceOptions = new[] { String.Empty, String.Empty, String.Empty }; + + //control + Settings settings = new Settings { MundaneCraftingRate = -23412345 }; + Mock renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CraftingTakesNoTime(It.IsAny())).Returns(false); + renderer.Setup(r => r.Evaluate_CustomCraftRate(It.IsAny())).Returns(true); + renderer.Setup(r => r.Evaluate_MundaneCraftingRateSlider(It.IsAny())).Returns(12); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(12, settings.MundaneCraftingRate); + } + + [TestMethod] + public void Defaults_MagicCraftingRate() + { + string priceLabel = "irrelevant"; + string[] priceOptions = new[] { String.Empty, String.Empty, String.Empty }; + + //control + Settings settings = new Settings { MagicCraftingRate = -8 }; + Mock renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CraftingTakesNoTime(It.IsAny())).Returns(false); + renderer.Setup(r => r.Evaluate_CustomCraftRate(It.IsAny())).Returns(false); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(Settings.MagicCraftingProgressPerDay, settings.MagicCraftingRate); + } + + [TestMethod] + public void Defaults_MundaneCraftingRate() + { + string priceLabel = "irrelevant"; + string[] priceOptions = new[] { String.Empty, String.Empty, String.Empty }; + + //control + Settings settings = new Settings { MundaneCraftingRate = -23412345 }; + Mock renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CraftingTakesNoTime(It.IsAny())).Returns(false); + renderer.Setup(r => r.Evaluate_CustomCraftRate(It.IsAny())).Returns(false); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(Settings.MundaneCraftingProgressPerDay, settings.MundaneCraftingRate); + } + + [TestMethod] + public void Reads_CasterLevelIsSinglePrerequisite() + { + string priceLabel = "irrelevant"; + string[] priceOptions = new[] { String.Empty, String.Empty, String.Empty }; + + /************ + * Test 1 * + ************/ + //control + Settings settings = new Settings { CasterLevelIsSinglePrerequisite = false }; + Mock renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CasterLevelIsSinglePrerequisite(It.IsAny())).Returns(true); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(true, settings.CasterLevelIsSinglePrerequisite); + + /************ + * Test 2 * + ************/ + //control + settings = new Settings { CasterLevelIsSinglePrerequisite = true }; + renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CasterLevelIsSinglePrerequisite(It.IsAny())).Returns(false); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(false, settings.CasterLevelIsSinglePrerequisite); + } + + [TestMethod] + public void Reads_CraftAtFullSpeedWhileAdventuring() + { + string priceLabel = "irrelevant"; + string[] priceOptions = new[] { String.Empty, String.Empty, String.Empty }; + + /************ + * Test 1 * + ************/ + //control + Settings settings = new Settings { CraftAtFullSpeedWhileAdventuring = false }; + Mock renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CraftAtFullSpeedWhileAdventuring(It.IsAny())).Returns(true); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(true, settings.CraftAtFullSpeedWhileAdventuring); + + /************ + * Test 2 * + ************/ + //control + settings = new Settings { CraftAtFullSpeedWhileAdventuring = true }; + renderer = new Mock(); + renderer.Setup(r => r.Evaluate_CraftAtFullSpeedWhileAdventuring(It.IsAny())).Returns(false); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(false, settings.CraftAtFullSpeedWhileAdventuring); + } + + [TestMethod] + public void Reads_IgnorePlusTenItemMaximum() + { + string priceLabel = "irrelevant"; + string[] priceOptions = new[] { String.Empty, String.Empty, String.Empty }; + + /************ + * Test 1 * + ************/ + //control + Settings settings = new Settings { IgnorePlusTenItemMaximum = false }; + Mock renderer = new Mock(); + renderer.Setup(r => r.Evaluate_IgnorePlusTenItemMaximum(It.IsAny())).Returns(true); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(true, settings.IgnorePlusTenItemMaximum); + + /************ + * Test 2 * + ************/ + //control + settings = new Settings { IgnorePlusTenItemMaximum = true }; + renderer = new Mock(); + renderer.Setup(r => r.Evaluate_IgnorePlusTenItemMaximum(It.IsAny())).Returns(false); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(false, settings.IgnorePlusTenItemMaximum); + } + + [TestMethod] + public void Reads_IgnoreFeatCasterLevelRestriction() + { + string priceLabel = "irrelevant"; + string[] priceOptions = new[] { String.Empty, String.Empty, String.Empty }; + + /************ + * Test 1 * + ************/ + //control + Settings settings = new Settings { IgnoreFeatCasterLevelRestriction = false }; + Mock renderer = new Mock(); + renderer.Setup(r => r.Evaluate_IgnoreFeatCasterLevelRestriction(It.IsAny())).Returns(true); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(true, settings.IgnoreFeatCasterLevelRestriction); + + /************ + * Test 2 * + ************/ + //control + settings = new Settings { IgnoreFeatCasterLevelRestriction = true }; + renderer = new Mock(); + renderer.Setup(r => r.Evaluate_IgnoreFeatCasterLevelRestriction(It.IsAny())).Returns(false); + + //invocation + UserInterfaceEventHandlingLogic.RenderCheatsSectionAndUpdateSettings(renderer.Object, settings, priceLabel); + + //validation + Assert.AreEqual(false, settings.IgnoreFeatCasterLevelRestriction); + } + } + } +} \ No newline at end of file diff --git a/CraftMagicItemsTests/packages.config b/CraftMagicItemsTests/packages.config new file mode 100644 index 0000000..469ed16 --- /dev/null +++ b/CraftMagicItemsTests/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Repository.json b/Repository.json index 785188a..0ed9a40 100644 --- a/Repository.json +++ b/Repository.json @@ -3,7 +3,7 @@ [ { "Id": "CraftMagicItems", - "Version": "1.11.1" + "Version": "2.0.0" } ] } \ No newline at end of file diff --git a/info.json b/info.json index 19f1506..f3c6fe1 100644 --- a/info.json +++ b/info.json @@ -2,8 +2,8 @@ "Id": "CraftMagicItems", "DisplayName": "Craft Magic Items", "Author": "RobRendell", - "Version": "1.11.1", - "ManagerVersion": "0.14.1", + "Version": "2.0.0", + "ManagerVersion": "0.22.9", "EntryMethod": "CraftMagicItems.Main.Load", "HomePage": "https://www.nexusmods.com/pathfinderkingmaker/mods/54", "Repository": "https://raw.githubusercontent.com/bfennema/OwlcatKingmakerModCraftMagicItems/master/Repository.json"