diff --git a/.gitignore b/.gitignore
index 61e43d76..a51be1cc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -351,4 +351,3 @@ Tools/TranslationTool/translation_issues_report_*
.claude
tmpclaude-*
CLAUDE.md
-INDEX.md
diff --git a/CSharpSourceCode/CampaignMechanics/Careers/TORCareerPerkCampaignBehavior.cs b/CSharpSourceCode/CampaignMechanics/Careers/TORCareerPerkCampaignBehavior.cs
index 5f7d16c8..25b09da3 100644
--- a/CSharpSourceCode/CampaignMechanics/Careers/TORCareerPerkCampaignBehavior.cs
+++ b/CSharpSourceCode/CampaignMechanics/Careers/TORCareerPerkCampaignBehavior.cs
@@ -9,6 +9,7 @@
using TaleWorlds.Core;
using TaleWorlds.Library;
using TaleWorlds.Localization;
+using TOR_Core.CampaignMechanics.Crafting;
using TOR_Core.CharacterDevelopment;
using TOR_Core.CharacterDevelopment.CareerSystem;
using TOR_Core.Extensions;
diff --git a/CSharpSourceCode/CampaignMechanics/Crafting/CraftingModelsExtensions.cs b/CSharpSourceCode/CampaignMechanics/Crafting/CraftingModelsExtensions.cs
new file mode 100644
index 00000000..ba8dcc20
--- /dev/null
+++ b/CSharpSourceCode/CampaignMechanics/Crafting/CraftingModelsExtensions.cs
@@ -0,0 +1,24 @@
+using System.Linq;
+using TaleWorlds.CampaignSystem;
+using TOR_Core.CampaignMechanics.Crafting.Models;
+
+namespace TOR_Core.CampaignMechanics.Crafting
+{
+ ///
+ /// Shortcuts to Campaign.Current.Models.GetGameModels().OfType for the models
+ /// this module owns - colocated here rather than in the shared Extensions/GameModelsExtensions
+ /// so that Framework layer doesn't need to know this module's concrete model types.
+ ///
+ public static class CraftingModelsExtensions
+ {
+ public static TORSmithingModel GetSmithingModel(this GameModels models)
+ {
+ return models.GetGameModels().OfType().LastOrDefault();
+ }
+
+ public static TOREnchantmentIngredientsModel GetEnchantmentIngredientModel(this GameModels models)
+ {
+ return models.GetGameModels().OfType().LastOrDefault();
+ }
+ }
+}
diff --git a/CSharpSourceCode/CampaignMechanics/Crafting/CraftingModule.cs b/CSharpSourceCode/CampaignMechanics/Crafting/CraftingModule.cs
new file mode 100644
index 00000000..cf7ff55c
--- /dev/null
+++ b/CSharpSourceCode/CampaignMechanics/Crafting/CraftingModule.cs
@@ -0,0 +1,39 @@
+using TaleWorlds.CampaignSystem;
+using TaleWorlds.Core;
+using TaleWorlds.MountAndBlade;
+using TOR_Core.CampaignMechanics.Crafting.Models;
+using TOR_Core.Framework;
+
+namespace TOR_Core.CampaignMechanics.Crafting
+{
+ ///
+ /// Registration entry point for the Crafting module (weapon/armor enchanting,
+ /// artisan-district item duplication, related loot/models), so SubModule.cs only needs
+ /// one call per lifecycle hook instead of one line per behavior/model.
+ ///
+ [TORModule]
+ public class CraftingModule : ITORModule
+ {
+ public void OnSubModuleLoad() { }
+
+ public void RegisterCampaignBehaviors(CampaignGameStarter starter)
+ {
+ starter.AddBehavior(new EnchanterTownBehavior());
+ starter.AddBehavior(new TORArtisanDistrictCampaignBehavior());
+ starter.AddBehavior(new PriestBehavior());
+ starter.AddBehavior(new EnchantmentIngredientLootCampaignBehavior());
+ starter.AddBehavior(new LootCampaignBehavior());
+ }
+
+ public void RegisterModels(IGameStarter gameStarterObject)
+ {
+ gameStarterObject.AddModel(new TORSmithingModel());
+ gameStarterObject.AddModel(new TOREnchantmentIngredientsModel());
+ gameStarterObject.AddModel(new TOREnchantmentCraftingModel());
+ }
+
+ public void RegisterMissionBehaviors(Mission mission) { }
+
+ public void RegisterGameObjectTypes(Game game) { }
+ }
+}
diff --git a/CSharpSourceCode/HarmonyPatches/CraftingPatches.cs b/CSharpSourceCode/CampaignMechanics/Crafting/CraftingPatches.cs
similarity index 98%
rename from CSharpSourceCode/HarmonyPatches/CraftingPatches.cs
rename to CSharpSourceCode/CampaignMechanics/Crafting/CraftingPatches.cs
index b76e5dcd..73868358 100644
--- a/CSharpSourceCode/HarmonyPatches/CraftingPatches.cs
+++ b/CSharpSourceCode/CampaignMechanics/Crafting/CraftingPatches.cs
@@ -7,12 +7,11 @@
using TaleWorlds.CampaignSystem.CampaignBehaviors;
using TaleWorlds.CampaignSystem.ViewModelCollection.WeaponCrafting.WeaponDesign;
using TaleWorlds.Core;
-using TOR_Core.CampaignMechanics.Crafting;
+using TOR_Core.CampaignMechanics.Crafting.Models;
using TOR_Core.Extensions;
-using TOR_Core.Models;
using TOR_Core.Utilities;
-namespace TOR_Core.HarmonyPatches
+namespace TOR_Core.CampaignMechanics.Crafting
{
[HarmonyPatch]
public static class CraftingPatches
diff --git a/CSharpSourceCode/Extensions/UI/CraftingVMExtension.cs b/CSharpSourceCode/CampaignMechanics/Crafting/CraftingVMExtension.cs
similarity index 96%
rename from CSharpSourceCode/Extensions/UI/CraftingVMExtension.cs
rename to CSharpSourceCode/CampaignMechanics/Crafting/CraftingVMExtension.cs
index a0b7252c..7dc2149b 100644
--- a/CSharpSourceCode/Extensions/UI/CraftingVMExtension.cs
+++ b/CSharpSourceCode/CampaignMechanics/Crafting/CraftingVMExtension.cs
@@ -2,8 +2,10 @@
using TaleWorlds.CampaignSystem.ViewModelCollection.WeaponCrafting;
using TaleWorlds.Core.ViewModelCollection.Information;
using TaleWorlds.Library;
+using TOR_Core.Extensions;
+using TOR_Core.Extensions.UI;
-namespace TOR_Core.Extensions.UI
+namespace TOR_Core.CampaignMechanics.Crafting
{
[ViewModelExtension(typeof(CraftingVM))]
public class CraftingVMExtension : BaseViewModelExtension
diff --git a/CSharpSourceCode/CampaignMechanics/Crafting/EnchanterTownBehavior.cs b/CSharpSourceCode/CampaignMechanics/Crafting/EnchanterTownBehavior.cs
index 96f146a9..b112deb4 100644
--- a/CSharpSourceCode/CampaignMechanics/Crafting/EnchanterTownBehavior.cs
+++ b/CSharpSourceCode/CampaignMechanics/Crafting/EnchanterTownBehavior.cs
@@ -13,14 +13,12 @@
using TaleWorlds.Library;
using TaleWorlds.ObjectSystem;
using TaleWorlds.TwoDimension;
-using TOR_Core.CampaignMechanics.Crafting;
-using TOR_Core.CharacterDevelopment;
using TOR_Core.Extensions;
-using TOR_Core.Items;
+using TOR_Core.Framework;
using TOR_Core.Utilities;
using static TOR_Core.Utilities.TORConstants;
-namespace TOR_Core.CampaignMechanics.SpellTrainers;
+namespace TOR_Core.CampaignMechanics.Crafting;
public class EnchanterTownBehavior : CampaignBehaviorBase
{
@@ -278,6 +276,11 @@ private bool HasEnchanterAccess(string culture)
return false;
}
+ if (CraftingCareerHooks.EnchanterAccessGrants.Exists(grant => grant(Hero.MainHero, culture)))
+ {
+ return true;
+ }
+
switch (culture)
{
case TORConstants.Cultures.EMPIRE:
@@ -306,11 +309,6 @@ private bool HasEnchanterAccess(string culture)
return HasMatchingEnchanterCompanion(x => x.Culture.StringId == TORConstants.Cultures.BRETONNIA && x.IsSpellCaster());
case TORConstants.Cultures.ASRAI:
- if (Hero.MainHero.HasCareer(TORCareers.Spellsinger))
- {
- return true;
- }
-
if (Hero.MainHero.Culture.StringId == TORConstants.Cultures.ASRAI &&
Hero.MainHero.IsSpellCaster() &&
Hero.MainHero.GetKnownLoreCount() > 0)
@@ -586,37 +584,10 @@ string GetManual()
void AddBeginnerBlueprintsForEnchantment()
{
- var career = Hero.MainHero.GetCareer();
-
- if (Hero.MainHero.IsSpellCaster() && career == TORCareers.ImperialMagister)
+ foreach (var grantor in CraftingCareerHooks.BeginnerBlueprintGrantors)
{
- if (Hero.MainHero.HasKnownLore("LoreOfDeath"))
- Hero.MainHero.AddEnchantmentBlueprint("emp_enchant_shyish_whisper", true);
-
- if (Hero.MainHero.HasKnownLore("LoreOfMetal"))
- Hero.MainHero.AddEnchantmentBlueprint("emp_enchant_chamon_whisper", true);
-
- if (Hero.MainHero.HasKnownLore("LoreOfLight"))
- Hero.MainHero.AddEnchantmentBlueprint("emp_enchant_hysh_whisper", true);
-
- if (Hero.MainHero.HasKnownLore("LoreOfHeavens"))
- Hero.MainHero.AddEnchantmentBlueprint("emp_enchant_azyr_whisper", true);
-
- if (Hero.MainHero.HasKnownLore("LoreOfBeasts"))
- Hero.MainHero.AddEnchantmentBlueprint("emp_enchant_ghur_whisper", true);
-
- if (Hero.MainHero.HasKnownLore("LoreOfLife"))
- Hero.MainHero.AddEnchantmentBlueprint("emp_enchant_ghyran_whisper", true);
-
- if (Hero.MainHero.HasKnownLore("LoreOfFire"))
- Hero.MainHero.AddEnchantmentBlueprint("emp_enchant_aqshy_whisper", true);
+ grantor(Hero.MainHero);
}
-
- if (Hero.MainHero.IsSpellCaster() && Hero.MainHero.HasCareer(TORCareers.GrailDamsel))
- if (Hero.MainHero.HasKnownLore("LoreOfLife"))
- Hero.MainHero.AddEnchantmentBlueprint("emp_enchant_ghyran_whisper", true);
-
- if (Hero.MainHero.HasCareer(TORCareers.Runelord)) Hero.MainHero.AddEnchantmentBlueprint("dw_rune_stone", true);
}
bool ConditionForDonation()
diff --git a/CSharpSourceCode/CampaignMechanics/Crafting/EnchantingIngredientVM.cs b/CSharpSourceCode/CampaignMechanics/Crafting/EnchantingIngredientVM.cs
index b1b1e391..f63ce8eb 100644
--- a/CSharpSourceCode/CampaignMechanics/Crafting/EnchantingIngredientVM.cs
+++ b/CSharpSourceCode/CampaignMechanics/Crafting/EnchantingIngredientVM.cs
@@ -1,7 +1,6 @@
using TaleWorlds.Core;
using TaleWorlds.Core.ViewModelCollection.Information;
using TaleWorlds.Library;
-using TOR_Core.Items;
namespace TOR_Core.CampaignMechanics.Crafting
{
diff --git a/CSharpSourceCode/CampaignMechanics/Crafting/EnchantingVM.cs b/CSharpSourceCode/CampaignMechanics/Crafting/EnchantingVM.cs
index c6a11116..f12b00ad 100644
--- a/CSharpSourceCode/CampaignMechanics/Crafting/EnchantingVM.cs
+++ b/CSharpSourceCode/CampaignMechanics/Crafting/EnchantingVM.cs
@@ -6,9 +6,9 @@
using TaleWorlds.Core;
using TaleWorlds.Library;
using TaleWorlds.LinQuick;
+using TOR_Core.CampaignMechanics.Crafting.Models;
using TOR_Core.Extensions;
using TOR_Core.Items;
-using TOR_Core.Models;
using TOR_Core.Utilities;
namespace TOR_Core.CampaignMechanics.Crafting
diff --git a/CSharpSourceCode/Items/InventoryUseScripts/EnchantmentBlueprintScript.cs b/CSharpSourceCode/CampaignMechanics/Crafting/EnchantmentBlueprintScript.cs
similarity index 97%
rename from CSharpSourceCode/Items/InventoryUseScripts/EnchantmentBlueprintScript.cs
rename to CSharpSourceCode/CampaignMechanics/Crafting/EnchantmentBlueprintScript.cs
index d78a1f97..d4853342 100644
--- a/CSharpSourceCode/Items/InventoryUseScripts/EnchantmentBlueprintScript.cs
+++ b/CSharpSourceCode/CampaignMechanics/Crafting/EnchantmentBlueprintScript.cs
@@ -8,9 +8,11 @@
using TaleWorlds.SaveSystem;
using TOR_Core.CharacterDevelopment;
using TOR_Core.Extensions;
+using TOR_Core.Items;
+using TOR_Core.Items.InventoryUseScripts;
using TOR_Core.Utilities;
-namespace TOR_Core.Items.InventoryUseScripts;
+namespace TOR_Core.CampaignMechanics.Crafting;
public class EnchantmentBlueprintScript : BaseInventoryUseScript
{
diff --git a/CSharpSourceCode/CampaignMechanics/Crafting/EnchantmentIngredientLootCampaignBehavior.cs b/CSharpSourceCode/CampaignMechanics/Crafting/EnchantmentIngredientLootCampaignBehavior.cs
index d9b0817c..b11c79db 100644
--- a/CSharpSourceCode/CampaignMechanics/Crafting/EnchantmentIngredientLootCampaignBehavior.cs
+++ b/CSharpSourceCode/CampaignMechanics/Crafting/EnchantmentIngredientLootCampaignBehavior.cs
@@ -8,7 +8,6 @@
using TaleWorlds.Core;
using TaleWorlds.LinQuick;
using TOR_Core.Extensions;
-using TOR_Core.Items;
namespace TOR_Core.CampaignMechanics.Crafting;
///
diff --git a/CSharpSourceCode/CampaignMechanics/Crafting/EnchantmentShopHelper.cs b/CSharpSourceCode/CampaignMechanics/Crafting/EnchantmentShopHelper.cs
index bb6376b0..a792daab 100644
--- a/CSharpSourceCode/CampaignMechanics/Crafting/EnchantmentShopHelper.cs
+++ b/CSharpSourceCode/CampaignMechanics/Crafting/EnchantmentShopHelper.cs
@@ -35,6 +35,7 @@ public static void OpenEnchantmentRecipeShop(List prefixList, string cul
MBInformationManager.ShowMultiSelectionInquiry(inquirydata, true);
}
+ //Note that RequiredSkillValue is not only gating purchasability, but it's also setting the cost for custom resources.
private readonly record struct PurchasableBlueprint(ItemObject Item, string BlueprintId, SkillObject RequiredSkill, int RequiredSkillValue, string Restriction, List EligibleHeroes);
private static List GetPurchasableBlueprints(List prefixList)
diff --git a/CSharpSourceCode/CampaignMechanics/Crafting/MODULE.md b/CSharpSourceCode/CampaignMechanics/Crafting/MODULE.md
new file mode 100644
index 00000000..eb8cf09a
--- /dev/null
+++ b/CSharpSourceCode/CampaignMechanics/Crafting/MODULE.md
@@ -0,0 +1,78 @@
+# Crafting — Module Reference
+
+Quick per-class index of `CampaignMechanics/Crafting/`. For the narrative version (how the
+pieces fit together) see [`CLAUDE.md`](./CLAUDE.md) in this folder; this file is a flat
+lookup table instead.
+
+## Registration
+
+| Class | File | What it does |
+|---|---|---|
+| `CraftingModule` | `CraftingModule.cs` | `[TORModule] : ITORModule` — registers this module's campaign behaviors (`EnchanterTownBehavior`, `TORArtisanDistrictCampaignBehavior`, `PriestBehavior`, `EnchantmentIngredientLootCampaignBehavior`, `LootCampaignBehavior`) and models (`TORSmithingModel`, `TOREnchantmentIngredientsModel`, `TOREnchantmentCraftingModel`) with `SubModule`. |
+
+## Campaign behaviors
+
+| Class | File | What it does |
+|---|---|---|
+| `EnchanterTownBehavior` | `EnchanterTownBehavior.cs` | Town-service entry point: maps culture → enchanter NPC template/dialogs and opens `EnchantingScreen`. |
+| `TORArtisanDistrictCampaignBehavior` (+ `TorItemDuplicationData`, `TorItemBeingCraftedData`) | `TORArtisanDistrictCampaignBehavior.cs` | Artisan-district service: tracks duplicated/enchanted items (via `TORCampaignEvents.ItemDuplicated`) and queued item-crafting state; removes the vanilla smithy menu and rearranges town menus in its place. |
+| `PriestBehavior` | `PriestBehavior.cs` | Town-service entry point for priest-blessed enchantments: maps culture/cult → priest NPC template and opens the blessing-flavored enchanting shop. |
+| `EnchantmentIngredientLootCampaignBehavior` | `EnchantmentIngredientLootCampaignBehavior.cs` | Adds enchanting ingredients to post-battle loot (`OnCollectLootsItemsEvent`) by summing per-enemy drop factors from `TOREnchantmentIngredientsModel`; also clears any leftover ingredient stock from settlements daily. |
+| `LootCampaignBehavior` | `LootCampaignBehavior.cs` | Adds/removes magical (item-trait) loot to/from the lootable pool, including pruning items no longer used by the player or companions. |
+
+## Models
+
+| Class | File | What it does |
+|---|---|---|
+| `TORSmithingModel` (`: DefaultSmithingModel`) | `Models/TORSmithingModel.cs` | Vanilla-smithing overrides: hides crafting templates with no accessible pieces (`ValidateHiddenCraftingTemplates`), filters NPC smithing orders by culture-appropriate weapon category, and applies career/religion energy-cost and refining-formula modifiers via `CraftingCareerHooks`. |
+| `TOREnchantmentIngredientsModel` | `Models/TOREnchantmentIngredientsModel.cs` | Enchanting-ingredient economy: per-ingredient custom-resource cost, drop factor per defeated character/context, and the random+career-bonus roll that turns a battle's drop score into a loot amount. |
+| `TOREnchantmentCraftingModel` | `Models/TOREnchantmentCraftingModel.cs` | Enchanting-session limits: max simultaneous enchantments/blessings a party of heroes can apply (career/perk/culture-driven), and the effective ingredient cost of a trait after career cost-reduction hooks. |
+
+## Static helpers
+
+| Class | File | What it does |
+|---|---|---|
+| `EnchantmentHelper` | `EnchantmentHelper.cs` | Blueprint data/eligibility queries (who can learn a blueprint, is it already known/in inventory) and `CreateEnchantedItem`/`CreateItemCopy` — clones an `ItemObject` with a fresh id and applies chosen traits/`ItemModifier`. |
+| `EnchantmentShopHelper` | `EnchantmentShopHelper.cs` | Builds and shows the "learn a blueprint" purchase inquiry (eligible heroes, gold/custom-resource cost, requirement text) on top of `EnchantmentHelper`'s data. |
+| `CraftingModelsExtensions` | `CraftingModelsExtensions.cs` | `GameModels` extension methods (`GetSmithingModel()`, `GetEnchantmentIngredientModel()`) — this module's equivalent of the shared `GameModelsExtensions`, kept local so `Framework` doesn't need to know these concrete model types. |
+| `TorEnchantingIngredients` (+ `TorTradeGoodType` enum) | `TorEnchantingIngredients.cs` | Static lookup of the six enchanting-ingredient `ItemObject`s (Arcane Scroll, Blessed Water, Dragon Blood, Amber Crystal, Warpstone Dust, Gem Stone), loaded once per campaign via `LoadIngredients()`. |
+
+## View-models / UI
+
+| Class | File | What it does |
+|---|---|---|
+| `EnchantingVM` | `EnchantingVM.cs` | Root view-model for the enchanting screen: item list, trait list/selection, ingredient list, preview tableau. |
+| `EnchantableItemVM` | `EnchantableItemVM.cs` | One equippable item eligible for enchanting, shown in `EnchantingVM`'s item list. |
+| `EnchantableTraitVM` | `EnchantableTraitVM.cs` | One selectable enchantment trait/effect, with name/icon/description/tooltip and a selection callback. |
+| `EnchantingIngredientVM` | `EnchantingIngredientVM.cs` | One ingredient row: current vs. pending amount, name/icon, tooltip. |
+| `EnchantingIngredientWidget` (internal) | `EnchantingIngredientWidget.cs` | `: RichTextWidget` — renders an ingredient amount in red when it goes negative (insufficient stock). |
+| `EnchantingItemTableauVM` | `EnchantingItemTableauVM.cs` | 3D item-preview tableau data (item id, banner code, item-modifier id) for the equipment being enchanted. |
+| `CraftingVMExtension` | `CraftingVMExtension.cs` | `[ViewModelExtension(typeof(CraftingVM))]` — adds a "Refine All" button (repeats refinement until stamina/materials run out) to the vanilla smithing-refinement screen. |
+| `RefinementVMExtension` | `RefinementVMExtension.cs` | `[ViewModelExtension(typeof(RefinementVM))]` — backs `CraftingVMExtension`'s "Refine All": computes the max repeatable-refinement count from stamina and material stock and executes them in a loop. |
+
+## Screen / state
+
+| Class | File | What it does |
+|---|---|---|
+| `EnchantingScreen` (`: ScreenBase, IGameStateListener`) | `EnchantingScreen.cs` | Gauntlet screen that hosts `EnchantingVM`; opened via `EnchantingScreen.Open()`. |
+| `EnchantingState` (`: GameState`) | `EnchantingState.cs` | Minimal `GameState` paired with `EnchantingScreen` via `[GameStateScreen]`. |
+
+## Item scripts
+
+| Class | File | What it does |
+|---|---|---|
+| `EnchantmentBlueprintScript` (`: BaseInventoryUseScript`) | `EnchantmentBlueprintScript.cs` | Inventory-use script attached to blueprint items: on use, offers eligible party heroes (by skill/attribute/lore requirement) the chance to learn the referenced enchantment blueprint. |
+
+## Harmony patches
+
+| Class | File | What it does |
+|---|---|---|
+| `CraftingPatches` | `CraftingPatches.cs` | Filters the vanilla crafting-category popup and daily NPC smithing-order generation down to TOR's valid/culture-appropriate `CraftingTemplate`s; re-initializes saved crafted items before the game strips non-ready objects. |
+
+## See also
+
+- `Items/TorEnchantingIngredients` usage and `Items/ItemTrait` — the underlying enchantment
+ data model this UI drives.
+- `CraftingCareerHooks` (in `Framework/`) — the career/religion cost-reduction and loot-bonus
+ hooks `TORSmithingModel`/`TOREnchantmentCraftingModel`/`TOREnchantmentIngredientsModel`
+ call into, kept out of this module so Crafting doesn't need to know Career/Religion types.
diff --git a/CSharpSourceCode/Models/TOREnchantmentCraftingModel.cs b/CSharpSourceCode/CampaignMechanics/Crafting/Models/TOREnchantmentCraftingModel.cs
similarity index 68%
rename from CSharpSourceCode/Models/TOREnchantmentCraftingModel.cs
rename to CSharpSourceCode/CampaignMechanics/Crafting/Models/TOREnchantmentCraftingModel.cs
index 0ca8bb7f..aac67f90 100644
--- a/CSharpSourceCode/Models/TOREnchantmentCraftingModel.cs
+++ b/CSharpSourceCode/CampaignMechanics/Crafting/Models/TOREnchantmentCraftingModel.cs
@@ -1,16 +1,16 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using TaleWorlds.CampaignSystem;
using TaleWorlds.Core;
-using TOR_Core.AbilitySystem;
using TOR_Core.CharacterDevelopment;
using TOR_Core.CharacterDevelopment.CareerSystem;
using TOR_Core.Extensions;
+using TOR_Core.Framework;
using TOR_Core.Items;
using TOR_Core.Utilities;
+using TOR_Core.CampaignMechanics.Crafting;
-namespace TOR_Core.Models;
+namespace TOR_Core.CampaignMechanics.Crafting.Models;
public class TOREnchantmentCraftingModel : GameModel
{
@@ -57,20 +57,19 @@ public int GetEffectiveIngredientAmount(List heroes, ItemTrait itemTrait,
{
if (hero.HasKnownEnchantmentBlueprint(itemTrait.ItemTraitStringId))
{
+ // Note: this call is a different kind of coupling than the module-specific
+ // hooks below - PassiveEffectType is a generic, career-agnostic dispatch
+ // mechanism (CareerHelper doesn't know "Grail Damsel"/"Necrarch"/"Runelord" by
+ // name, it just applies every choice tagged EnchantmentCostReduction), it's
+ // just currently misplaced under CharacterDevelopment/CareerSystem alongside
+ // genuinely Careers-specific content. Left as-is; properly fixing it means
+ // relocating CareerHelper/PassiveEffectType to Framework, a separate, larger
+ // move with a much bigger blast radius (CareerHelper is used everywhere).
CharacterDevelopment.CareerSystem.CareerHelper.ApplyBasicCareerPassives(hero, ref explainedNumber, PassiveEffectType.EnchantmentCostReduction, true);
- // Greylord: For every known spell, reduce enchantment cost by 1%
- if (hero.HasCareerChoice("ForbiddenScrollsOfSapheryPassive3"))
+ foreach (var factor in CraftingCareerHooks.EnchantmentCostReductionFactors)
{
- var choice = TORCareerChoices.GetChoice("ForbiddenScrollsOfSapheryPassive3");
- if (choice != null)
- {
- var spellCount = hero.GetExtendedInfo().AllAbilities
- .Select(AbilityFactory.GetTemplate)
- .Count(ability => ability != null && ability.IsSpell);
- var reductionPercent = spellCount * choice.GetPassiveValue();
- explainedNumber.AddFactor(reductionPercent / 100f);
- }
+ explainedNumber.AddFactor(factor(hero));
}
}
}
diff --git a/CSharpSourceCode/Models/TOREnchantmentIngredientsModel.cs b/CSharpSourceCode/CampaignMechanics/Crafting/Models/TOREnchantmentIngredientsModel.cs
similarity index 95%
rename from CSharpSourceCode/Models/TOREnchantmentIngredientsModel.cs
rename to CSharpSourceCode/CampaignMechanics/Crafting/Models/TOREnchantmentIngredientsModel.cs
index 26a89805..1eea7b66 100644
--- a/CSharpSourceCode/Models/TOREnchantmentIngredientsModel.cs
+++ b/CSharpSourceCode/CampaignMechanics/Crafting/Models/TOREnchantmentIngredientsModel.cs
@@ -2,12 +2,12 @@
using TaleWorlds.CampaignSystem;
using TaleWorlds.CampaignSystem.MapEvents;
using TaleWorlds.Core;
-using TOR_Core.CharacterDevelopment;
+using TOR_Core.CampaignMechanics.Crafting;
using TOR_Core.Extensions;
-using TOR_Core.Items;
+using TOR_Core.Framework;
using TOR_Core.Utilities;
-namespace TOR_Core.CampaignMechanics.Crafting;
+namespace TOR_Core.CampaignMechanics.Crafting.Models;
public class TOREnchantmentIngredientsModel : GameModel
{
@@ -48,14 +48,9 @@ public int CalculateResultAmount(float dropscore, TorTradeGoodType ingredient, f
{
float careerBonus = 1f;
- // Orc Shaman enchantment loot bonus
- if (Hero.MainHero.HasCareerChoice("BonesAnFirepitzPassive3"))
+ foreach (var bonus in CraftingCareerHooks.IngredientLootBonusFactors)
{
- var choice = TORCareerChoices.GetChoice("BonesAnFirepitzPassive3");
- if (choice != null)
- {
- careerBonus += choice.GetPassiveValue(); // 0.25 for 25%
- }
+ careerBonus += bonus(Hero.MainHero);
}
return (int)(dropscore * GetDropAmplitude(ingredient) * RandomMultiplier(ingredient) * playerEarnedLootRate * careerBonus);
diff --git a/CSharpSourceCode/Models/TORSmithingModel.cs b/CSharpSourceCode/CampaignMechanics/Crafting/Models/TORSmithingModel.cs
similarity index 89%
rename from CSharpSourceCode/Models/TORSmithingModel.cs
rename to CSharpSourceCode/CampaignMechanics/Crafting/Models/TORSmithingModel.cs
index 2cc8a9d2..681ea4e7 100644
--- a/CSharpSourceCode/Models/TORSmithingModel.cs
+++ b/CSharpSourceCode/CampaignMechanics/Crafting/Models/TORSmithingModel.cs
@@ -5,11 +5,12 @@
using TaleWorlds.CampaignSystem.GameComponents;
using TaleWorlds.Core;
using TaleWorlds.Library;
-using TOR_Core.CharacterDevelopment;
using TOR_Core.Extensions;
+using TOR_Core.Framework;
using TOR_Core.Utilities;
+using RefiningFormula = TaleWorlds.Core.Crafting.RefiningFormula;
-namespace TOR_Core.Models
+namespace TOR_Core.CampaignMechanics.Crafting.Models
{
///
/// Categories for weapon templates used in culture-based order filtering.
@@ -132,7 +133,7 @@ public virtual bool IsCultureAppropriateOrder(string templateId, string cultureI
return false;
}
- public override int GetEnergyCostForRefining(ref Crafting.RefiningFormula refineFormula, Hero hero)
+ public override int GetEnergyCostForRefining(ref RefiningFormula refineFormula, Hero hero)
{
var value = base.GetEnergyCostForRefining(ref refineFormula, hero);
return ApplyEnergyCostModifiers(value, hero);
@@ -152,15 +153,14 @@ public override int GetEnergyCostForSmithing(ItemObject item, Hero hero)
private int ApplyEnergyCostModifiers(int value, Hero hero)
{
- if (hero.HasCareer(TORCareers.Runelord))
+ foreach (var modifier in CraftingCareerHooks.EnergyCostModifiers)
{
- if (Hero.MainHero.HasCareerChoice("ForgefireBurningPassive3"))
- {
- var reduction = value * 0.4f;
- value -= (int)MathF.Round(reduction);
- }
+ value = modifier(hero, value);
}
+ // Note: this one is a Religion-module coupling (cult_of_grungni is a blessing, not
+ // a career), a separate kind of coupling from the Careers one this pass addresses -
+ // left as-is.
if (hero.PartyBelongedTo != null && hero.PartyBelongedTo.HasBlessing("cult_of_grungni"))
{
var reduction = value * 0.25f;
@@ -170,34 +170,22 @@ private int ApplyEnergyCostModifiers(int value, Hero hero)
return value;
}
- public override IEnumerable GetRefiningFormulas(
+ public override IEnumerable GetRefiningFormulas(
Hero weaponsmith)
{
var values = base.GetRefiningFormulas(weaponsmith);
-
-
- if (weaponsmith.HasCareer(TORCareers.Runelord))
+ if (CraftingCareerHooks.RefiningFormulaModifiers.Count > 0)
{
- var newValues = new List();
+ var newValues = new List();
foreach (var value in values)
{
- if (weaponsmith.HasCareerChoice("ForgefireBurningPassive1") && value.Output == CraftingMaterials.Charcoal)
+ var modified = value;
+ foreach (var modifier in CraftingCareerHooks.RefiningFormulaModifiers)
{
- var entry = new Crafting.RefiningFormula(value.Input1, value.Input1Count, value.Input2, value.Input2Count, value.Output,
- value.OutputCount + 1);
- newValues.Add(entry);
- continue;
- }
- if (weaponsmith.HasCareerChoice("ForgefireBurningPassive2") && value.Output is CraftingMaterials.Iron1 or CraftingMaterials.Iron2 or CraftingMaterials.Iron3 or CraftingMaterials.Iron4)
- {
-
- var entry = new Crafting.RefiningFormula(value.Input1, value.Input1Count, value.Input2, value.Input2Count, value.Output,
- value.OutputCount * 2);
- newValues.Add(entry);
- continue;
+ modified = modifier(weaponsmith, modified);
}
- newValues.Add(value);
+ newValues.Add(modified);
}
values = newValues;
diff --git a/CSharpSourceCode/Extensions/UI/RefinementVMExtension.cs b/CSharpSourceCode/CampaignMechanics/Crafting/RefinementVMExtension.cs
similarity index 98%
rename from CSharpSourceCode/Extensions/UI/RefinementVMExtension.cs
rename to CSharpSourceCode/CampaignMechanics/Crafting/RefinementVMExtension.cs
index a3ab6ada..833efa2a 100644
--- a/CSharpSourceCode/Extensions/UI/RefinementVMExtension.cs
+++ b/CSharpSourceCode/CampaignMechanics/Crafting/RefinementVMExtension.cs
@@ -6,8 +6,10 @@
using TaleWorlds.CampaignSystem.ViewModelCollection.WeaponCrafting.Refinement;
using TaleWorlds.Library;
using TaleWorlds.MountAndBlade.View;
+using TOR_Core.Extensions;
+using TOR_Core.Extensions.UI;
-namespace TOR_Core.Extensions.UI
+namespace TOR_Core.CampaignMechanics.Crafting
{
[ViewModelExtension(typeof(RefinementVM))]
public class RefinementVMExtension : BaseViewModelExtension
diff --git a/CSharpSourceCode/Items/TorEnchantingIngredients.cs b/CSharpSourceCode/CampaignMechanics/Crafting/TorEnchantingIngredients.cs
similarity index 98%
rename from CSharpSourceCode/Items/TorEnchantingIngredients.cs
rename to CSharpSourceCode/CampaignMechanics/Crafting/TorEnchantingIngredients.cs
index 3be7273d..55558e06 100644
--- a/CSharpSourceCode/Items/TorEnchantingIngredients.cs
+++ b/CSharpSourceCode/CampaignMechanics/Crafting/TorEnchantingIngredients.cs
@@ -2,7 +2,7 @@
using TaleWorlds.CampaignSystem;
using TaleWorlds.Core;
-namespace TOR_Core.Items
+namespace TOR_Core.CampaignMechanics.Crafting
{
public static class TorEnchantingIngredients
{
diff --git a/CSharpSourceCode/CharacterDevelopment/CareerSystem/CareerButton/RunelordCareerButtonBehavior.cs b/CSharpSourceCode/CharacterDevelopment/CareerSystem/CareerButton/RunelordCareerButtonBehavior.cs
index 41b9cdc0..72c26e0c 100644
--- a/CSharpSourceCode/CharacterDevelopment/CareerSystem/CareerButton/RunelordCareerButtonBehavior.cs
+++ b/CSharpSourceCode/CharacterDevelopment/CareerSystem/CareerButton/RunelordCareerButtonBehavior.cs
@@ -5,6 +5,7 @@
using TaleWorlds.Core;
using TaleWorlds.LinQuick;
using TaleWorlds.Localization;
+using TOR_Core.CampaignMechanics.Crafting;
using TOR_Core.Extensions;
using TOR_Core.Items;
using TOR_Core.Utilities;
diff --git a/CSharpSourceCode/CharacterDevelopment/CareerSystem/CraftingCareerHookRegistrations.cs b/CSharpSourceCode/CharacterDevelopment/CareerSystem/CraftingCareerHookRegistrations.cs
new file mode 100644
index 00000000..1ba0a6f0
--- /dev/null
+++ b/CSharpSourceCode/CharacterDevelopment/CareerSystem/CraftingCareerHookRegistrations.cs
@@ -0,0 +1,139 @@
+using System.Linq;
+using TaleWorlds.CampaignSystem;
+using TaleWorlds.Core;
+using TaleWorlds.Library;
+using TOR_Core.AbilitySystem;
+using TOR_Core.Extensions;
+using TOR_Core.Framework;
+using TOR_Core.Utilities;
+using RefiningFormula = TaleWorlds.Core.Crafting.RefiningFormula;
+
+namespace TOR_Core.CharacterDevelopment.CareerSystem
+{
+ ///
+ /// The one place CharacterDevelopment/CareerSystem is allowed to know about
+ /// CampaignMechanics/Crafting: pushes each career's enchanting/smithing-specific effects
+ /// into Crafting's CraftingCareerHooks extension points, so Crafting's own code never
+ /// references a Career/CareerChoice by name. Called once from SubModule.BeginGameStart,
+ /// after TORCareers/TORCareerChoices exist (the closures below resolve lazily at call
+ /// time regardless, so exact ordering doesn't actually matter).
+ ///
+ public static class CraftingCareerHookRegistrations
+ {
+ public static void RegisterAll()
+ {
+ RegisterGreyLordEnchantmentCostReduction();
+ RegisterOrcShamanIngredientLootBonus();
+ RegisterBeginnerBlueprintGrants();
+ RegisterSpellsingerEnchanterAccess();
+ RegisterRunelordSmithingBonuses();
+ }
+
+ ///
+ /// Grey Lord's "ForbiddenScrollsOfSapheryPassive3": -1% enchantment cost per known spell.
+ /// Tagged PassiveEffectType.Special in GreyLordCareerChoices.cs (opts out of the generic
+ /// CareerHelper dispatch since it needs a per-hero-computed multiplier, not a flat value).
+ ///
+ private static void RegisterGreyLordEnchantmentCostReduction()
+ {
+ CraftingCareerHooks.EnchantmentCostReductionFactors.Add(hero =>
+ {
+ if (!hero.HasCareerChoice("ForbiddenScrollsOfSapheryPassive3")) return 0f;
+
+ var choice = TORCareerChoices.GetChoice("ForbiddenScrollsOfSapheryPassive3");
+ if (choice == null) return 0f;
+
+ var spellCount = hero.GetExtendedInfo().AllAbilities
+ .Select(AbilityFactory.GetTemplate)
+ .Count(ability => ability != null && ability.IsSpell);
+
+ return spellCount * choice.GetPassiveValue() / 100f;
+ });
+ }
+
+ ///
+ /// Orc Shaman's "BonesAnFirepitzPassive3": +25% enchantment-ingredient loot. Also tagged
+ /// PassiveEffectType.Special (evaluated as an additive bonus factor on Hero.MainHero
+ /// specifically, not the generic per-choice dispatch).
+ ///
+ private static void RegisterOrcShamanIngredientLootBonus()
+ {
+ CraftingCareerHooks.IngredientLootBonusFactors.Add(hero =>
+ {
+ if (!hero.HasCareerChoice("BonesAnFirepitzPassive3")) return 0f;
+
+ var choice = TORCareerChoices.GetChoice("BonesAnFirepitzPassive3");
+ return choice?.GetPassiveValue() ?? 0f;
+ });
+ }
+
+ ///
+ /// Free beginner enchantment blueprints granted just by having picked a relevant career -
+ /// Imperial Magister (per known Lore), Grail Damsel (Lore of Life), Runelord (rune stone).
+ ///
+ private static void RegisterBeginnerBlueprintGrants()
+ {
+ CraftingCareerHooks.BeginnerBlueprintGrantors.Add(hero =>
+ {
+ if (hero.IsSpellCaster() && hero.GetCareer() == TORCareers.ImperialMagister)
+ {
+ if (hero.HasKnownLore("LoreOfDeath")) hero.AddEnchantmentBlueprint("emp_enchant_shyish_whisper", true);
+ if (hero.HasKnownLore("LoreOfMetal")) hero.AddEnchantmentBlueprint("emp_enchant_chamon_whisper", true);
+ if (hero.HasKnownLore("LoreOfLight")) hero.AddEnchantmentBlueprint("emp_enchant_hysh_whisper", true);
+ if (hero.HasKnownLore("LoreOfHeavens")) hero.AddEnchantmentBlueprint("emp_enchant_azyr_whisper", true);
+ if (hero.HasKnownLore("LoreOfBeasts")) hero.AddEnchantmentBlueprint("emp_enchant_ghur_whisper", true);
+ if (hero.HasKnownLore("LoreOfLife")) hero.AddEnchantmentBlueprint("emp_enchant_ghyran_whisper", true);
+ if (hero.HasKnownLore("LoreOfFire")) hero.AddEnchantmentBlueprint("emp_enchant_aqshy_whisper", true);
+ }
+
+ if (hero.IsSpellCaster() && hero.HasCareer(TORCareers.GrailDamsel))
+ if (hero.HasKnownLore("LoreOfLife"))
+ hero.AddEnchantmentBlueprint("emp_enchant_ghyran_whisper", true);
+
+ if (hero.HasCareer(TORCareers.Runelord)) hero.AddEnchantmentBlueprint("dw_rune_stone", true);
+ });
+ }
+
+ ///
+ /// Spellsinger gets Asrai-culture enchanter access regardless of the hero's own culture
+ /// or a matching companion (the normal rule Crafting itself enforces).
+ ///
+ private static void RegisterSpellsingerEnchanterAccess()
+ {
+ CraftingCareerHooks.EnchanterAccessGrants.Add((hero, culture) =>
+ culture == TORConstants.Cultures.ASRAI && hero.HasCareer(TORCareers.Spellsinger));
+ }
+
+ ///
+ /// Runelord: -40% smithing energy cost with ForgefireBurningPassive3; ForgefireBurningPassive1
+ /// grants +1 Charcoal from refining, ForgefireBurningPassive2 doubles Iron refining output.
+ ///
+ private static void RegisterRunelordSmithingBonuses()
+ {
+ CraftingCareerHooks.EnergyCostModifiers.Add((hero, value) =>
+ {
+ if (hero.HasCareer(TORCareers.Runelord) && Hero.MainHero.HasCareerChoice("ForgefireBurningPassive3"))
+ {
+ var reduction = value * 0.4f;
+ value -= (int)MathF.Round(reduction);
+ }
+ return value;
+ });
+
+ CraftingCareerHooks.RefiningFormulaModifiers.Add((hero, formula) =>
+ {
+ if (!hero.HasCareer(TORCareers.Runelord)) return formula;
+
+ if (hero.HasCareerChoice("ForgefireBurningPassive1") && formula.Output == CraftingMaterials.Charcoal)
+ {
+ return new RefiningFormula(formula.Input1, formula.Input1Count, formula.Input2, formula.Input2Count, formula.Output, formula.OutputCount + 1);
+ }
+ if (hero.HasCareerChoice("ForgefireBurningPassive2") && formula.Output is CraftingMaterials.Iron1 or CraftingMaterials.Iron2 or CraftingMaterials.Iron3 or CraftingMaterials.Iron4)
+ {
+ return new RefiningFormula(formula.Input1, formula.Input1Count, formula.Input2, formula.Input2Count, formula.Output, formula.OutputCount * 2);
+ }
+ return formula;
+ });
+ }
+ }
+}
diff --git a/CSharpSourceCode/Extensions/GameModelsExtensions.cs b/CSharpSourceCode/Extensions/GameModelsExtensions.cs
index 98f63178..d8292962 100644
--- a/CSharpSourceCode/Extensions/GameModelsExtensions.cs
+++ b/CSharpSourceCode/Extensions/GameModelsExtensions.cs
@@ -1,6 +1,5 @@
using System.Linq;
using TaleWorlds.CampaignSystem;
-using TOR_Core.CampaignMechanics.Crafting;
using TOR_Core.Models;
namespace TOR_Core.Extensions
@@ -22,11 +21,6 @@ public static TORCustomResourceModel GetCustomResourceModel(this GameModels mode
return models.GetGameModels().OfType().LastOrDefault();
}
- public static TOREnchantmentIngredientsModel GetEnchantmentIngredientModel(this GameModels models)
- {
- return models.GetGameModels().OfType().LastOrDefault();
- }
-
public static TORCompanionTrainingModel GetCompanionTrainingModel(this GameModels models)
{
return models.GetGameModels().OfType().LastOrDefault();
@@ -46,10 +40,5 @@ public static TORSiegeEngineCalculationModel GetSiegeEngineCalculationModel(this
{
return models.GetGameModels().OfType().LastOrDefault();
}
-
- public static TORSmithingModel GetSmithingModel(this GameModels models)
- {
- return models.GetGameModels().OfType().LastOrDefault();
- }
}
}
\ No newline at end of file
diff --git a/CSharpSourceCode/Extensions/HeroExtensions.cs b/CSharpSourceCode/Extensions/HeroExtensions.cs
index 44010f5d..dace2903 100644
--- a/CSharpSourceCode/Extensions/HeroExtensions.cs
+++ b/CSharpSourceCode/Extensions/HeroExtensions.cs
@@ -21,6 +21,7 @@
using FaceGen = TaleWorlds.Core.FaceGen;
using LogLevel = NLog.LogLevel;
using static TOR_Core.Utilities.TORConstants;
+using TOR_Core.CampaignMechanics.Crafting;
namespace TOR_Core.Extensions
{
diff --git a/CSharpSourceCode/Framework/CraftingCareerHooks.cs b/CSharpSourceCode/Framework/CraftingCareerHooks.cs
new file mode 100644
index 00000000..b9f571db
--- /dev/null
+++ b/CSharpSourceCode/Framework/CraftingCareerHooks.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using TaleWorlds.CampaignSystem;
+using RefiningFormula = TaleWorlds.Core.Crafting.RefiningFormula;
+
+namespace TOR_Core.Framework
+{
+ ///
+ /// Shared contract between CampaignMechanics/Crafting and whichever module wants to
+ /// influence enchanting/smithing (Careers, chiefly) - lives here rather than inside
+ /// Crafting's own folder so that neither module has to reference the other's namespace:
+ /// Crafting depends on this (Framework) to consume the hooks, and the module contributing
+ /// effects (today, CharacterDevelopment/CareerSystem/CraftingCareerHookRegistrations,
+ /// called once from SubModule.BeginGameStart after the career registries exist) depends on
+ /// this (Framework) to populate them. Nothing under CampaignMechanics/Crafting/ references
+ /// CharacterDevelopment/CareerSystem for career-specific content any more (the one
+ /// remaining reference, CareerHelper.ApplyBasicCareerPassives(...,
+ /// PassiveEffectType.EnchantmentCostReduction, ...) in TOREnchantmentCraftingModel, is a
+ /// different case - see its comment), and CraftingCareerHookRegistrations never references
+ /// CampaignMechanics/Crafting either.
+ ///
+ public static class CraftingCareerHooks
+ {
+ /// Additional multiplicative cost-reduction factors for enchanting (folded in via ExplainedNumber.AddFactor), evaluated once per hero contributing to an enchantment.
+ public static readonly List> EnchantmentCostReductionFactors = new();
+
+ /// Additional bonus factors for enchantment-ingredient loot amount (added to the base 1.0 multiplier), evaluated for Hero.MainHero.
+ public static readonly List> IngredientLootBonusFactors = new();
+
+ /// Grants free beginner enchantment blueprints; called once per Hero.MainHero when the enchanter town service opens.
+ public static readonly List> BeginnerBlueprintGrantors = new();
+
+ /// Extra "can use this culture's enchanter" checks, called as (Hero.MainHero, cultureId); true from any one grants access regardless of the normal culture/companion rule.
+ public static readonly List> EnchanterAccessGrants = new();
+
+ /// Additional energy-cost modifiers for smithing/refining/smelting, applied as (hero, runningCost) -> modifiedCost, chained in registration order.
+ public static readonly List> EnergyCostModifiers = new();
+
+ /// Per-formula modifiers for the refining-formula list, applied as (hero, formula) -> (possibly modified) formula, chained in registration order.
+ public static readonly List> RefiningFormulaModifiers = new();
+ }
+}
diff --git a/CSharpSourceCode/Framework/ITORModule.cs b/CSharpSourceCode/Framework/ITORModule.cs
new file mode 100644
index 00000000..58d5f3df
--- /dev/null
+++ b/CSharpSourceCode/Framework/ITORModule.cs
@@ -0,0 +1,27 @@
+using TaleWorlds.CampaignSystem;
+using TaleWorlds.Core;
+using TaleWorlds.MountAndBlade;
+
+namespace TOR_Core.Framework
+{
+ ///
+ /// Registration class for module. We do this so every module is self-contained.
+ ///
+ public interface ITORModule
+ {
+ /// Template/XML loading, Harmony setup specific to this module, etc. Called from SubModule.OnSubModuleLoad.
+ void OnSubModuleLoad();
+
+ /// starter.AddBehavior(...) for every CampaignBehaviorBase this module owns. Called from SubModule.InitializeGameStarter.
+ void RegisterCampaignBehaviors(CampaignGameStarter starter);
+
+ /// gameStarterObject.AddModel(...) for every GameModel this module owns. Called from SubModule.OnGameStart.
+ void RegisterModels(IGameStarter gameStarterObject);
+
+ /// mission.AddMissionBehavior(...) for every mission behavior this module owns. Called from SubModule.OnMissionBehaviorInitialize.
+ void RegisterMissionBehaviors(Mission mission);
+
+ /// game.ObjectManager.RegisterType(...) for every custom object type this module owns. Called from SubModule.BeginGameStart.
+ void RegisterGameObjectTypes(Game game);
+ }
+}
diff --git a/CSharpSourceCode/Framework/TORModuleAttribute.cs b/CSharpSourceCode/Framework/TORModuleAttribute.cs
new file mode 100644
index 00000000..785de9b9
--- /dev/null
+++ b/CSharpSourceCode/Framework/TORModuleAttribute.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace TOR_Core.Framework
+{
+ ///
+ /// Marks an implementation as a module for future
+ /// reflection-based discovery/registration (see docs/vertical-slicing-proposal.md) -
+ /// the same pattern Extensions/UI's ViewModelExtensionManager already uses for
+ /// [ViewModelExtension]. No discovery/registry consumes this attribute yet; modules are
+ /// still registered by an explicit call from SubModule.cs. Tagging a module with this
+ /// now costs nothing and documents intent for when that registry is built.
+ ///
+ [AttributeUsage(AttributeTargets.Class)]
+ public class TORModuleAttribute : Attribute
+ {
+ }
+}
diff --git a/CSharpSourceCode/HarmonyPatches/ViewModelPatches.cs b/CSharpSourceCode/HarmonyPatches/ViewModelPatches.cs
index d898c699..c97fa34e 100644
--- a/CSharpSourceCode/HarmonyPatches/ViewModelPatches.cs
+++ b/CSharpSourceCode/HarmonyPatches/ViewModelPatches.cs
@@ -8,6 +8,7 @@
using TaleWorlds.Library;
using TaleWorlds.LinQuick;
using TaleWorlds.MountAndBlade;
+using TOR_Core.CampaignMechanics.Crafting;
using TOR_Core.CharacterDevelopment;
using TOR_Core.Extensions;
using TOR_Core.Extensions.UI;
diff --git a/CSharpSourceCode/INDEX.md b/CSharpSourceCode/INDEX.md
new file mode 100644
index 00000000..8ad305cf
--- /dev/null
+++ b/CSharpSourceCode/INDEX.md
@@ -0,0 +1,134 @@
+# TOR_Core — Index
+
+**The Old Realms** is a Mount & Blade II: Bannerlord total-conversion mod bringing Games
+Workshop's *Warhammer Fantasy Battles* setting to Bannerlord. This is `TOR_Core`, the main
+C# gameplay-logic module (there are sibling modules — `TOR_Armory` for art/data assets,
+`TOR_Environment`, and Bannerlord's own `Native`/`SandBox`/`StoryMode`/`CustomBattle` —
+referenced by the launch args in `TOR_Core.csproj`, but not present in this source tree).
+
+Every folder in this tree (down to leaf subfolders) has its own `CLAUDE.md` with details;
+this file is the map of how they fit together. Start here, then drill into the folder that
+owns the system you're touching.
+
+## Orientation
+
+- **Entry point**: `SubModule.cs` (`TOR_Core.SubModule : MBSubModuleBase`). Read this file
+ first when you need to know "where does X get registered/initialized" — it is the single
+ place that lists every `CampaignBehaviorBase`, `GameModel`, and mission behavior the mod
+ adds, plus Harmony setup and startup ordering. If a system isn't wired up, it starts here.
+- **Project file**: `TOR_Core.csproj` — old-style (non-SDK) .NET Framework 4.8 project;
+ every source file must be listed in a `` element or it won't build.
+ References are DLLs from the installed game (`../../../bin/Win64_Shipping_Client/`) and
+ sibling modules (`Native`, `SandBox`, `StoryMode`, `CustomBattle`) — this is a Bannerlord
+ mod, not a standalone app; you cannot build/run it without a Bannerlord installation.
+- **`lib/`** — two vendored DLLs (`ink-engine-runtime.dll`, `ink_compiler.dll`) for the
+ `Ink/` narrative-scripting integration. **`obj/`**/**`bin/`** — build output, ignore.
+- **`Properties/`** — just `AssemblyInfo.cs`.
+
+## Architectural patterns you'll see everywhere
+
+- **`CampaignBehaviorBase` per mechanic** — almost every gameplay system in
+ `CampaignMechanics/` is one behavior class registered in `SubModule.InitializeGameStarter`.
+ To find how a mechanic starts, grep its behavior class name in `SubModule.cs`.
+- **`GameModel` overrides** — `Models/` replaces vanilla formulas one at a time
+ (`TORXyzModel : DefaultXyzModel`), registered in `SubModule.OnGameStart`. To change a
+ formula, find the matching model here before writing a Harmony patch.
+- **Harmony patches** (`HarmonyPatches/`) are the fallback for anything vanilla doesn't
+ expose a model/behavior/virtual method for. Most patch at `OnSubModuleLoad`; a few need
+ `[HarmonyPatchCategory("LatePatches")]` to run after `Game.Current`'s text manager exists.
+- **XML-defined template data + a static loader/factory**: `AbilityTemplate`
+ (`AbilitySystem/AbilityFactory`), `TriggeredEffectTemplate`
+ (`BattleMechanics/TriggeredEffect/TriggeredEffectManager`), `StatusEffectTemplate`
+ (`BattleMechanics/StatusEffect/StatusEffectManager`), `ItemTrait`
+ (`Items/ItemTraitManager`), `ReligionObject`/`CareerObject`/`CareerChoiceObject`
+ (native `MBObjectManager` + XML). All loaded once in `SubModule.OnSubModuleLoad`/
+ `BeginGameStart`. If you need to add a new spell/effect/trait/career-choice, you're
+ almost always adding a data entry + maybe one script class, not new infrastructure.
+- **Utility-AI** (`BattleMechanics/AI`) — behaviors implement `IAgentBehavior` and score
+ themselves via `Axis`/`ScoringFunctions`; `DecisionManager` picks the best score. This
+ pattern is specific to spellcaster AI (`CastingAI/`) but the primitives
+ (`CommonAIFunctions/`) are reusable.
+- **Extension methods over subclassing** — `Extensions/` adds behavior to vanilla types
+ (`Agent`, `Hero`, `CharacterObject`, ...) as static extension methods rather than wrapper
+ classes, since most vanilla types aren't designed to be subclassed.
+- **Two ways to attach "extra data" to a vanilla object**:
+ - Runtime/campaign side data → `Extensions/ExtendedInfoSystem` (side dictionaries keyed
+ by string id, e.g. `hero.GetExtendedInfo()`).
+ - Extra bindable UI properties on a vanilla `ViewModel` → `Extensions/UI`'s
+ `BaseViewModelExtension`/`ViewModelExtensionManager` (reflection-based property/command
+ forwarding, registered via `[ViewModelExtension]`).
+- **Save compatibility**: every persisted custom type must be registered in a
+ `SaveableTypeDefiner` (mainly `SaveGameSystem/TORSaveableTypeDefiner`, but a few
+ behaviors define their own inline) with a **stable, never-reused** numeric id — see that
+ file's own warning before adding or renumbering one.
+
+## The Warhammer domain model (so the folder docs make sense)
+
+- **Cultures** (`Utilities/TORConstants.Cultures`) map onto (and often reuse the game-object
+ slot of) vanilla Bannerlord cultures: Empire (`empire`), Bretonnia (`vlandia`), Sylvania
+ (`khuzait`), Mousillon (`mousillon`), Asrai/Wood Elves (`battania`), Eonir/Wood Elves
+ (`eonir`), Dawi/Dwarfs (`sturgia`), Greenskin (`aserai`) — plus Druchii, Beastmen, Chaos,
+ and several bandit-culture reskins. `Cultures.All` lists the 8 main playable ones.
+- **Magic**: Winds-of-Magic **Spells** (Lores: Fire/Light/Heavens/Life/Metal/Beasts/Death,
+ High Magic, Dark Magic, Necromancy, Big Waaagh) and Dwarf **Rune Magic** are one system
+ (`AbilitySystem`); priestly **Prayers** are a parallel system tied to
+ `CampaignMechanics/Religion`; each **Career** has its own unique signature
+ `CareerAbility`. All three share the same `Ability`/`AbilityTemplate` runtime.
+- **Careers** (`CharacterDevelopment/CareerSystem`) are Warhammer-flavored "prestige
+ classes" — Grail Knight, Black Grail Knight, Grail Damsel, Knight of the Old World,
+ Witch Hunter, Warrior Priest (+ of Ulric), Blood Knight, Vampire Count, Necromancer,
+ Necrarch, Imperial Magister, Waywatcher, Spellsinger, Warden, Grey Lord, Mercenary,
+ Ironbreaker, Runelord, Slayer, Orc Boss, Orc Shaman — each with a perk tree
+ (`CareerSystem/Choices`), a signature ability, and sometimes a special roster-screen
+ button (`CareerSystem/CareerButton`).
+- **Per-culture "second currency"** (`CampaignMechanics/CustomResources`): Prestige
+ (Empire), Chivalry (Bretonnia), DarkEnergy (Sylvania/Mousillon), ForestHarmony (Asrai),
+ CouncilFavor (Eonir), OathGold (Dawi), Teef/Waaagh (Greenskin).
+- **Bespoke settlement types** (`CampaignMechanics/TORCustomSettlement`): Chaos Portal,
+ Herdstone, Slaver Camp, Troll Cave (all raider-spawning lairs), Cursed Site, Oak of Ages,
+ Shrine, World Roots.
+- **Damage/effects pipeline**: an `Ability`/weapon-hit fires a
+ `BattleMechanics/TriggeredEffect` → resolves target set → applies damage via
+ `BattleMechanics/DamageSystem/TORDamageHelper` and/or a
+ `BattleMechanics/StatusEffect` → both get scaled by `Models/TORAbilityModel`
+ (skill/perk effectiveness) and `CharacterDevelopment/CareerSystem/CareerHelper`
+ (career passives) along the way.
+
+## Top-level folder map
+
+| Folder | What it owns |
+|---|---|
+| `AbilitySystem/` | Spells, prayers, career abilities: the shared casting/effect runtime. |
+| `Audio/` | Standalone file-based sound playback (ambient sounds). |
+| `BattleMechanics/` | In-mission mechanics: AI, status effects, triggered effects, artillery, banners, firearms, dismemberment, voice, arena modes. |
+| `CampaignMechanics/` | Every campaign-map mechanic (largest folder): factions, careers, religion, custom resources, diplomacy, crafting, custom settlements, quests-adjacent behaviors. |
+| `CharacterDevelopment/` | Skills, perks, traits, attributes, and the Career data model. |
+| `Extensions/` | Extension methods on vanilla types; the ExtendedInfo side-data system; the ViewModel-extension UI injection system. |
+| `GameManagers/` | Early campaign bootstrapping, hotkeys, shader-related game managers. |
+| `HarmonyPatches/` | Every Harmony patch, grouped by what vanilla system they touch. |
+| `Ink/` | Branching-narrative integration (Inkle's Ink language). |
+| `Items/` | Item traits/enchantments, weapon on-hit scripts, inventory-use scripts. |
+| `Missions/` | Mission-open factory methods + scripted one-off fight controllers. |
+| `Models/` | `GameModel` overrides (vanilla formula replacements) + Custom Battle variants. |
+| `Quests/` | `QuestBase` quest classes, including Career storyline quests. |
+| `SaveGameSystem/` | Save-type registration (`SaveableTypeDefiner`). |
+| `Utilities/` | Cross-cutting static helpers (config, paths, constants, math, logging). |
+
+## Where to look for a given task
+
+- **"Add/tune a spell or prayer"** → `AbilitySystem/` (template + maybe a new
+ `AbilityScript`), `AbilitySystem/Spells/LoreObject` if it's a new Lore.
+- **"Add a new status effect / DOT / buff"** → `BattleMechanics/StatusEffect`.
+- **"A weapon should do something special on hit"** → `Items/WeaponHitScripts` +
+ `Items/ItemTrait`.
+- **"Add/tune a Career perk or its passive"** → `CharacterDevelopment/CareerSystem/Choices`.
+- **"Change how damage/resistance math works"** → `BattleMechanics/DamageSystem` +
+ `Models/TORAgentApplyDamageModel`/`TORAbilityModel`.
+- **"Add a new campaign mechanic/town service"** → a new `CampaignBehaviorBase` under
+ `CampaignMechanics/`, registered in `SubModule.InitializeGameStarter`.
+- **"Vanilla formula needs to behave differently"** → check `Models/` first; only reach for
+ `HarmonyPatches/` if there's no model hook for it.
+- **"AI isn't casting/behaving right"** → `BattleMechanics/AI/CastingAI` (spellcasters) or
+ `BattleMechanics/AI/TeamAI` (formation/team AI).
+- **"Add UI to an existing vanilla screen"** → `Extensions/UI` (`BaseViewModelExtension`)
+ rather than a Harmony patch on the screen class, if at all possible.
diff --git a/CSharpSourceCode/Items/ItemTrait.cs b/CSharpSourceCode/Items/ItemTrait.cs
index 02e5d40f..3ed00346 100644
--- a/CSharpSourceCode/Items/ItemTrait.cs
+++ b/CSharpSourceCode/Items/ItemTrait.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Xml.Serialization;
+using TOR_Core.CampaignMechanics.Crafting;
using TOR_Core.Extensions.ExtendedInfoSystem;
using static TaleWorlds.Core.ItemObject;
diff --git a/CSharpSourceCode/Models/TORFaithModel.cs b/CSharpSourceCode/Models/TORFaithModel.cs
index b72227c8..43f90747 100644
--- a/CSharpSourceCode/Models/TORFaithModel.cs
+++ b/CSharpSourceCode/Models/TORFaithModel.cs
@@ -4,11 +4,11 @@
using TaleWorlds.CampaignSystem;
using TaleWorlds.CampaignSystem.Party;
using TaleWorlds.Core;
+using TOR_Core.CampaignMechanics.Crafting;
using TOR_Core.CampaignMechanics.Religion;
using TOR_Core.CharacterDevelopment;
using TOR_Core.Extensions;
using TOR_Core.Extensions.ExtendedInfoSystem;
-using TOR_Core.Items;
using TOR_Core.Utilities;
namespace TOR_Core.Models
diff --git a/CSharpSourceCode/SubModule.cs b/CSharpSourceCode/SubModule.cs
index 4063acfb..bf03ad3c 100644
--- a/CSharpSourceCode/SubModule.cs
+++ b/CSharpSourceCode/SubModule.cs
@@ -167,7 +167,6 @@ protected override void InitializeGameStarter(Game game, IGameStarter starterObj
starter.AddBehavior(new TORCaptivityCampaignBehavior());
starter.AddBehavior(new AssimilationCampaignBehavior());
starter.AddBehavior(new SpellTrainerInTownBehavior());
- starter.AddBehavior(new EnchanterTownBehavior());
starter.AddBehavior(new MasterEngineerTownBehaviour());
starter.AddBehavior(new PrestigeNobleTownBehavior());
starter.AddBehavior(new EonirFavorEnvoyTownBehavior());
@@ -194,12 +193,9 @@ protected override void InitializeGameStarter(Game game, IGameStarter starterObj
starter.AddBehavior(new TORStartupBehavior());
starter.AddBehavior(new TORKingdomDecisionsCampaignBehavior());
starter.AddBehavior(new TORAllianceWarBehavior());
- starter.AddBehavior(new TORArtisanDistrictCampaignBehavior());
- starter.AddBehavior(new PriestBehavior());
starter.AddBehavior(new SkillTrainerBehavior());
- starter.AddBehavior(new EnchantmentIngredientLootCampaignBehavior());
- starter.AddBehavior(new LootCampaignBehavior());
starter.AddBehavior(new OathGoldBehavior());
+ new CraftingModule().RegisterCampaignBehaviors(starter);
starter.AddBehavior(new TeefBehavior());
starter.AddBehavior(new WaaaghBehavior());
starter.AddBehavior(new GreenskinBrawlBehavior());
@@ -277,11 +273,9 @@ protected override void OnGameStart(Game game, IGameStarter gameStarterObject)
gameStarterObject.AddModel(new TOREquipmentSelectionModel());
gameStarterObject.AddModel(new TOREncounterModel());
gameStarterObject.AddModel(new TORVolunteerModel());
- gameStarterObject.AddModel(new TORSmithingModel());
- gameStarterObject.AddModel(new TOREnchantmentIngredientsModel());
+ new CraftingModule().RegisterModels(gameStarterObject);
gameStarterObject.AddModel(new TORCompanionTrainingModel());
gameStarterObject.AddModel(new TORVillageProductionCalculatorModel());
- gameStarterObject.AddModel(new TOREnchantmentCraftingModel());
gameStarterObject.AddModel(new TORCampaignTimeModel());
gameStarterObject.AddModel(new TORSiegeEngineCalculationModel());
gameStarterObject.AddModel(new TORHiringCompatibilityModel());
@@ -367,6 +361,7 @@ public override void BeginGameStart(Game game)
_ = new TORCareers();
_ = new TORCareerChoiceGroups();
_ = new TORCareerChoices();
+ CraftingCareerHookRegistrations.RegisterAll();
_ = new TORCampaignEvents();
MBObjectManager.Instance.LoadXML("Religions", false);
diff --git a/CSharpSourceCode/TOR_Core.csproj b/CSharpSourceCode/TOR_Core.csproj
index 950f8581..ef8a38c9 100644
--- a/CSharpSourceCode/TOR_Core.csproj
+++ b/CSharpSourceCode/TOR_Core.csproj
@@ -506,6 +506,10 @@
+
+
+
+
@@ -515,12 +519,18 @@
+
+
+
+
+
+
@@ -653,6 +663,7 @@
+
@@ -716,16 +727,17 @@
-
-
+
+
+
@@ -746,7 +758,6 @@
-
@@ -791,14 +802,12 @@
-
-
@@ -850,8 +859,6 @@
-
-
@@ -877,7 +884,6 @@
-
diff --git a/ModuleData/tor_custom_xmls/tor_itemtraits.xml b/ModuleData/tor_custom_xmls/tor_itemtraits.xml
index b243c493..300368a7 100644
--- a/ModuleData/tor_custom_xmls/tor_itemtraits.xml
+++ b/ModuleData/tor_custom_xmls/tor_itemtraits.xml
@@ -2436,7 +2436,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_ghur_wildform}"3% physical resistance, 15% chance to receive a fleeting 3% physical resistance bonus for 10 seconds upon receiving damage. Stacks up to 3 times"
-
+
emp_enchant_ghur_wildform
Spellcraft
@@ -2450,7 +2450,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_ghur_savagery}"10% extra damage done as physical, 10% chance to receive a fleeting 2% attack speed bonus for 20 seconds upon dealing damge. Stacks up to 5 times"
-
+
emp_enchant_ghur_savagery
Spellcraft
@@ -2464,7 +2464,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_ghur_crows}"10% extra damage done as physical, 10% chance to summon vicious crows in a 3m radius, inflicting 2 frost damage per second for 20 seconds"
-
+
emp_enchant_ghur_crows
Spellcraft
@@ -2478,7 +2478,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_ghur_echo}"15% extra damage done as physical, 15% extra missile speed, 10% to reduce movement speed of nearby enemies by 20% for 20 seconds upon dealing damage"
-
+
emp_enchant_ghur_echo
Spellcraft
@@ -2492,7 +2492,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_ghur_whisper}"10% extra damage done as physical"
-
+
emp_enchant_ghur_whisper
Spellcraft
@@ -2506,7 +2506,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_hysh_sanctuary}"3% physical resistance, 5% chance to snare nearby enemies with a net of Hysh for 20 seconds upon receiving damage"
-
+
emp_enchant_hysh_sanctuary
Spellcraft
@@ -2520,7 +2520,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_hysh_brilliance}"15% chance to trigger a burst of light upon blocking damage. Reduces attack speed of nearby enemies within 3m for 20 seconds"
-
+
emp_enchant_hysh_brilliance
Spellcraft
@@ -2534,7 +2534,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_hysh_timewarp}"20% extra damage done as magical, 10% chance to receive a 40% movement speed buff for 3 seconds upon dealing damage"
-
+
emp_enchant_hysh_timewarp
Spellcraft
@@ -2548,7 +2548,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_hysh_clarity}"3 max Winds of Magic"
-
+
emp_enchant_hysh_clarity
Spellcraft
@@ -2562,7 +2562,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_hysh_whisper}"10% extra damage done as magical"
-
+
emp_enchant_hysh_whisper
Spellcraft
@@ -2576,7 +2576,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_hysh_providence}"5% magic resistance, 10 bonus skillpoints to Spellcraft"
-
+
emp_enchant_hysh_providence
Spellcraft
@@ -2590,7 +2590,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_ghyran_brambles}"Deal 25 physical damage to enemies blocked in melee -10% fire resistance"
-
+
emp_enchant_ghyran_brambles
Spellcraft
@@ -2604,7 +2604,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_ghyran_bloom}"6 extra HP, -10% fire resistance, 15% chance to regen 2HP per second for 5 seconds upon receiving damage"
-
+
emp_enchant_ghyran_bloom
Spellcraft
@@ -2618,7 +2618,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_ghyran_cloak}"25% extra healing rate when travelling"
-
+
emp_enchant_ghyran_cloak
Spellcraft
@@ -2632,7 +2632,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_ghyran_tendrils}"15% physical resistance, 25% to snare enemies within 3m with powerful roots for 20 seconds upon taking damage"
-
+
emp_enchant_ghyran_tendrils
Spellcraft
@@ -2646,7 +2646,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_ghyran_whisper}"3 extra HP, -15% fire resistance"
-
+
emp_enchant_ghyran_whisper
Spellcraft
@@ -2660,7 +2660,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_ghyran_intuition}"3 max Winds of Magic, -25% fire resistance"
-
+
emp_enchant_ghyran_intuition
Spellcraft
@@ -2674,7 +2674,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_aqshy_hearth}"7% bonus to fire damage, -25% fire resistance"
-
+
emp_enchant_aqshy_hearth
Spellcraft
@@ -2688,7 +2688,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_aqshy_firebreath}"25% extra damage done as fire, 20% to summon a fiery explosion dealing 60 fire damage in a 3m radius and inflicting burning (4 fire damage per second for 5 seconds)"
-
+
emp_enchant_aqshy_firebreath
Spellcraft
@@ -2702,7 +2702,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_aqshy_cinders}"15% extra damage done as fire, 15% chance to inflict burning on nearby enemies in a 3m radius (4 fire damage per second for 5 seconds) upon dealing damage"
-
+
emp_enchant_aqshy_cinders
Spellcraft
@@ -2716,7 +2716,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_aqshy_rage}"40% extra damage done as fire, -15% fire resistance"
-
+
emp_enchant_aqshy_rage
Spellcraft
@@ -2730,7 +2730,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_aqshy_whisper}"12% extra damage done as fire"
-
+
emp_enchant_aqshy_whisper
Spellcraft
@@ -2744,7 +2744,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_aqshy_fury}"Damaging burning enemies inflicts an extra 50 fire damage"
-
+
emp_enchant_aqshy_fury
Spellcraft
@@ -2758,7 +2758,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_aqshy_curtain}"20% fire resistance, 15% chance to inflict fire DoT on blocked enemies (4 fire damage per second for 5 seconds). This DoT effect can spread onto enemies and allies alike"
-
+
emp_enchant_aqshy_curtain
Spellcraft
@@ -2772,7 +2772,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_chamon_transmutation}"20% extra damage done as fire, projectiles penetrate targets"
-
+
emp_enchant_chamon_transmutation
Spellcraft
@@ -2786,7 +2786,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_chamon_aegis}"20% extra shield HP, 5% physical resistance, 50% chance to decrease physical resistance of nearby enemies by 40% for 5 sec upon blocking damage"
-
+
emp_enchant_chamon_aegis
Spellcraft
@@ -2800,7 +2800,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_chamon_laws}"50% extra armour penetration"
-
+
emp_enchant_chamon_laws
Spellcraft
@@ -2814,7 +2814,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_chamon_feathers_lead}"8% physical resistance, 5% penalty to movement speed"
-
+
emp_enchant_chamon_feathers_lead
Spellcraft
@@ -2828,7 +2828,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_chamon_lead_feathers}"3% penalty to physical resistance, 5% bonus to movement speed"
-
+
emp_enchant_chamon_lead_feathers
Spellcraft
@@ -2842,7 +2842,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_chamon_forge}"30% extra reload speed, 10% extra damage done as magical"
-
+
emp_enchant_chamon_forge
Spellcraft
@@ -2856,7 +2856,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_chamon_whisper}"3% physical resistance"
-
+
emp_enchant_chamon_whisper
Spellcraft
@@ -2870,7 +2870,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_chamon_crucible}"20% extra damage done as magical, weapon gains the Cleave trait"
-
+
emp_enchant_chamon_crucible
Spellcraft
@@ -2884,7 +2884,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_azyr_wind}"10% extra projectile speed"
-
+
emp_enchant_azyr_wind
Spellcraft
@@ -2898,7 +2898,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_azyr_harmony}"4% bonus to lightning damage"
-
+
emp_enchant_azyr_harmony
Spellcraft
@@ -2912,7 +2912,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_azyr_azure_mirror}"2% physical resistance, deal 15 lightning damage and knockdown to enemies attacking you in melee within 2m"
-
+
emp_enchant_azyr_azure_mirror
Spellcraft
@@ -2926,7 +2926,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_azyr_gale}"15% extra armour penetration, 15% chance to summon a gust of wind knocking down enemies in a 3m radius dealing an immediate 10 lightning damage"
-
+
emp_enchant_azyr_gale
Spellcraft
@@ -2940,7 +2940,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_azyr_foresight}"Gain 15% Physical Resistance and 15% bonus Lightning damage. Your attacks have a 15% chance to inspire nearby allies, granting them the same Lightning bonus for 20 seconds"
-
+
emp_enchant_azyr_foresight
Spellcraft
@@ -2954,7 +2954,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_azyr_divination}"5% magic resistance, 7% extra party travel speed"
-
+
emp_enchant_azyr_divination
Spellcraft
@@ -2968,7 +2968,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_azyr_whisper}"3% magic resistance, 3% extra party travel speed"
-
+
emp_enchant_azyr_whisper
Spellcraft
@@ -2982,7 +2982,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_shyish_pale_grip}"7% bonus to magic damage, -3 HP"
-
+
emp_enchant_shyish_pale_grip
Spellcraft
@@ -2996,7 +2996,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_shyish_messengers}"5% magic resistance, 10% chance to inflict horrific visions in a 7m radius upon taking damage. Nearby enemies suffer 4 magical damage per second for 20 seconds"
-
+
emp_enchant_shyish_messengers
Spellcraft
@@ -3010,7 +3010,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_shyish_blight}"25% extra damage done as magical, 10% chance to reduce physical resistance of struck enemies by 25% for 5 seconds"
-
+
emp_enchant_shyish_blight
Spellcraft
@@ -3024,7 +3024,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_shyish_pall}"15% extra damage done as magical, gain a stacking 10% magic resistance bonus upon killing enemies. Stacks up to 5 times and fades after a short while"
-
+
emp_enchant_shyish_pall
Spellcraft
@@ -3038,7 +3038,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_shyish_withering}"10% extra damage done as magical, 10% chance to reduce movement speed of the struck enemy by 25% for 5 seconds"
-
+
emp_enchant_shyish_withering
Spellcraft
@@ -3052,7 +3052,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_shyish_whisper}"12% extra damage done as magical"
-
+
emp_enchant_shyish_whisper
Spellcraft
@@ -3066,7 +3066,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_enchant_shyish_deathsight}"Blocking hits of Undead units carries a 50% chance to apply an 'Exorcism', debuffing enemies making them 33% more vulnerable to Magical damage"
-
+
emp_enchant_shyish_deathsight
Spellcraft
@@ -3080,7 +3080,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_blessing_sigmar_beacon}"10% magic resistance, 15% extra prayer radius"
-
+
emp_blessing_sigmar_beacon
Faith
@@ -3094,7 +3094,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_blessing_sigmar_retribution}"Gain a fleeting 10% bonus to holy damage when blocking blows of Undead and Daemons. Stacks up to 5 times, each lasting 15 seconds"
-
+
emp_blessing_sigmar_retribution
Faith
@@ -3108,7 +3108,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_blessing_sigmar_soulfire}"6% magic resistance, 3 extra HP, 10% chance to emit a damaging wave of holy energy dealing 35 damage in a 7m radius whenever attacked by Undead and Daemons in melee"
-
+
emp_blessing_sigmar_soulfire
Faith
@@ -3122,7 +3122,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_blessing_sigmar_light}"6% magic resistance, 3 extra HP, 10% chance to gain 2HP per second for 20 seconds whenever attacked by Undead and Daemons in melee"
-
+
emp_blessing_sigmar_light
Faith
@@ -3136,7 +3136,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_blessing_sigmar_exorcism}"20% extra damage done as holy, 10% chance to deal 100% bonus damage against Undead and Daemons"
-
+
emp_blessing_sigmar_exorcism
Faith
@@ -3150,7 +3150,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_blessing_sigmar_justice}"12% extra damage done as holy"
-
+
emp_blessing_sigmar_justice
Faith
@@ -3164,7 +3164,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_blessing_ulric_wrath}"350% extra shield damage, 15% to receive a fleeting 3% attack speed bonus upon dealing damage. Stacks up to 5 times, each lasting 15 seconds"
-
+
emp_blessing_ulric_wrath
Faith
@@ -3178,7 +3178,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_blessing_ulric_wolf_god_bite}"10% extra swing speed, 15% to receive a fleeting 6% bonus to frost damage upon dealing damage. Stacks up to 5 times each lasting 15 seconds"
-
+
emp_blessing_ulric_wolf_god_bite
Faith
@@ -3192,7 +3192,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_blessing_ulric_chill}"15% extra damage done as frost"
-
+
emp_blessing_ulric_chill
Faith
@@ -3206,7 +3206,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_blessing_ulric_frenzy}"5% physical resistance, receiving melee damage carries a 15% chance in a 3m radius to give allies a 15% attack speed increase for 20 seconds"
-
+
emp_blessing_ulric_frenzy
Faith
@@ -3220,7 +3220,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_blessing_ulric_winterfather_gift}"5% magic resistance, 20 bonus skillpoints to Two Handed"
-
+
emp_blessing_ulric_winterfather_gift
Faith
@@ -3234,7 +3234,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_blessing_shallya_mercy}"6 extra HP"
-
+
emp_blessing_shallya_mercy
Faith
@@ -3248,7 +3248,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_blessing_shallya_pacifism}"Mercy is piety. Chance to heal 2 HP for 5 seconds when knocking out enemies"
-
+
emp_blessing_shallya_pacifism
Faith
@@ -3262,7 +3262,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_blessing_shallya_purity}"50% extra healing rate when travelling"
-
+
emp_blessing_shallya_purity
Faith
@@ -3276,7 +3276,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_blessing_shallya_compassion}"-50% shield HP, completely heals the wearer and nearby allies upon shield destruction"
-
+
emp_blessing_shallya_compassion
Faith
@@ -3290,7 +3290,7 @@
Invalid
-1
{=str_tor_itemtraits_emp_blessing_shallya_martyrdom}"3 extra HP, taking damage has a 33% chance to heal nearby friendly units for 2HP for 6 seconds in a 3m radius"
-
+
emp_blessing_shallya_martyrdom
Faith
@@ -3304,7 +3304,7 @@
Invalid
-1
{=str_tor_itemtraits_bret_blessing_eerie_touch}"2% physical resistance, 25 bonus skillpoints to Riding"
-
+
bret_blessing_eerie_touch
Faith
@@ -3318,7 +3318,7 @@
Invalid
-1
{=str_tor_itemtraits_bret_blessing_fey_guidance}"10% extra party travel speed, 5% magic resistance"
-
+
bret_blessing_fey_guidance
Faith
@@ -3332,7 +3332,7 @@
Invalid
-1
{=str_tor_itemtraits_bret_blessing_grail_legacy}"30% extra damage done as holy, weapon gains the Cleave trait"
-
+
bret_blessing_grail_legacy
Faith
@@ -3346,7 +3346,7 @@
Invalid
-1
{=str_tor_itemtraits_bret_blessing_lady_ward}"10% magic resistance, Once per battle fatal wounds will revive you with 50% HP"
-
+
bret_blessing_lady_ward
Faith
@@ -3360,7 +3360,7 @@
Invalid
-1
{=str_tor_itemtraits_bret_blessing_scourge_evil}"5% extra swing speed, 50% chance to deal 40 holy damage in a 3m radius against Undead and Daemons"
-
+
bret_blessing_scourge_evil
Faith
@@ -3374,7 +3374,7 @@
Invalid
-1
{=str_tor_itemtraits_bret_blessing_shield_faith}"10% physical resistance, blocked hits reduce the cooldown of your Career Ability by 1 second"
-
+
bret_blessing_shield_faith
Faith
@@ -3388,7 +3388,7 @@
Invalid
-1
{=str_tor_itemtraits_bret_blessing_pilgrim_tenacity}"2 extra HP"
-
+
bret_blessing_pilgrim_tenacity
Faith
@@ -3402,7 +3402,7 @@
Invalid
-1
{=str_tor_itemtraits_bret_blessing_mists_sacred_lake}"3% physical resistance, 15% extra prayer and spell radius"
-
+
bret_blessing_mists_sacred_lake
Faith
@@ -3416,7 +3416,7 @@
Invalid
-1
{=str_tor_itemtraits_bret_blessing_wisdom_virtue}"3 extra HP, 5% magic resistance"
-
+
bret_blessing_wisdom_virtue
Faith
@@ -3430,7 +3430,7 @@
Invalid
-1
{=str_tor_itemtraits_vc_enchant_bonds_darkness}"5% bonus to magic damage, summon 3 Skeleton Warriors for every enemy felled in melee"
-
+
vc_enchant_bonds_darkness
Spellcraft
@@ -3444,7 +3444,7 @@
Invalid
-1
{=str_tor_itemtraits_vc_enchant_call_beyond}"10% bonus to magic damage, summon 3 Grave Guard for every enemy felled in melee"
-
+
vc_enchant_call_beyond
Spellcraft
@@ -3458,7 +3458,7 @@
Invalid
-1
{=str_tor_itemtraits_vc_enchant_unhallowed_pact}"-5% physical resistance, 33% extra spell radius"
-
+
vc_enchant_unhallowed_pact
Spellcraft
@@ -3472,7 +3472,7 @@
Invalid
-1
{=str_tor_itemtraits_vc_enchant_drinker_blood}"15% extra damage done as magical. Recover 1 HP for every instance of damage dealt in melee, triggers below 50% HP"
-
+
vc_enchant_drinker_blood
Spellcraft
@@ -3486,7 +3486,7 @@
Invalid
-1
{=str_tor_itemtraits_vc_enchant_nightshroud}"5% magic resistance, 15% chance to reduce attack speed and damage of nearby enemies upon receiving damage"
-
+
vc_enchant_nightshroud
Spellcraft
@@ -3500,7 +3500,7 @@
Invalid
-1
{=str_tor_itemtraits_vc_enchant_ethereal_whispers}"5% physical resistance, upon receiving ranged damage gain 50% ranged damage immunity for 20 seconds"
-
+