Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -351,4 +351,3 @@ Tools/TranslationTool/translation_issues_report_*
.claude
tmpclaude-*
CLAUDE.md
INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Linq;
using TaleWorlds.CampaignSystem;
using TOR_Core.CampaignMechanics.Crafting.Models;

namespace TOR_Core.CampaignMechanics.Crafting
{
/// <summary>
/// Shortcuts to Campaign.Current.Models.GetGameModels().OfType<X> 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.
/// </summary>
public static class CraftingModelsExtensions
{
public static TORSmithingModel GetSmithingModel(this GameModels models)
{
return models.GetGameModels().OfType<TORSmithingModel>().LastOrDefault();
}

public static TOREnchantmentIngredientsModel GetEnchantmentIngredientModel(this GameModels models)
{
return models.GetGameModels().OfType<TOREnchantmentIngredientsModel>().LastOrDefault();
}
}
}
39 changes: 39 additions & 0 deletions CSharpSourceCode/CampaignMechanics/Crafting/CraftingModule.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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.
/// </summary>
[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) { }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using TaleWorlds.Core;
using TaleWorlds.Core.ViewModelCollection.Information;
using TaleWorlds.Library;
using TOR_Core.Items;

namespace TOR_Core.CampaignMechanics.Crafting
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
using TaleWorlds.Core;
using TaleWorlds.LinQuick;
using TOR_Core.Extensions;
using TOR_Core.Items;

namespace TOR_Core.CampaignMechanics.Crafting;
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public static void OpenEnchantmentRecipeShop(List<string> 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<Hero> EligibleHeroes);

private static List<PurchasableBlueprint> GetPurchasableBlueprints(List<string> prefixList)
Expand Down
78 changes: 78 additions & 0 deletions CSharpSourceCode/CampaignMechanics/Crafting/MODULE.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -57,20 +57,19 @@ public int GetEffectiveIngredientAmount(List<Hero> 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));
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
Expand Down
Loading