From 27656f29ecb5e5a81949e1907034e26d65a04ddf Mon Sep 17 00:00:00 2001 From: Seth McLeod Date: Thu, 16 Jul 2026 12:40:04 -0400 Subject: [PATCH] fix: resolve energy icons in custom keyword tooltips outside of a run Rich keyword tooltips passed an empty energyPrefix, which makes EnergyIconsFormatter fall back to RunManager.GetLocalCharacterEnergyIconPrefix(). That returns null when there is no RunState, so custom energy icons rendered correctly during a run but fell back to the colorless icon in the card library opened from the main menu (and logged "No energy prefix found for EnergyIconsFormatter!"). The game resolves this from the model rather than the run: CardModel, PowerModel, PotionModel, RelicModel and EnchantmentModel all populate energyPrefix with EnergyIconHelper.GetPrefix(this), which falls back to the model's own pool and so works outside a run. HoverTipFactory.FromKeyword only receives the keyword, so let the keyword declare its pool: [KeywordProperties(AutoKeywordPosition.None, Pool = typeof(MyCardPool))] public static CardKeyword MyKeyword; Keywords that don't declare a pool keep the previous behaviour. Fixes #337 --- Patches/Content/CustomEnums.cs | 18 ++++++++++++++++-- Patches/Localization/CustomTooltips.cs | 19 +++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/Patches/Content/CustomEnums.cs b/Patches/Content/CustomEnums.cs index b0efd7c3..0f223858 100644 --- a/Patches/Content/CustomEnums.cs +++ b/Patches/Content/CustomEnums.cs @@ -53,6 +53,14 @@ public KeywordPropertiesAttribute(AutoKeywordPosition position, bool richKeyword public AutoKeywordPosition Position { get; } public bool RichKeyword { get; } + + /// + /// The pool whose energy icon the keyword's tooltip should use, e.g. typeof(MyCardPool). + /// Only relevant for rich keywords whose description contains energy icons. + /// If not supplied, the icon is resolved from the character being played, which fails outside of a run + /// (in the card library the tooltip falls back to the colorless icon). + /// + public Type? Pool { get; set; } } public enum AutoKeywordPosition @@ -71,7 +79,12 @@ public readonly struct KeywordInfo(string key) public readonly string Key = key; public required AutoKeywordPosition AutoPosition { get; init; } public required bool RichKeyword { get; init; } - + + /// + /// Pool supplying the energy icon for this keyword's tooltip. Null if the keyword did not declare one. + /// + public Type? PoolType { get; init; } + public static implicit operator string(KeywordInfo info) => info.Key; } @@ -315,7 +328,8 @@ static void FindAndGenerate() CustomKeywords.KeywordIDs.Add((int) key, new(keywordId) { AutoPosition = autoPosition, - RichKeyword = poolAttribute?.RichKeyword ?? true + RichKeyword = poolAttribute?.RichKeyword ?? true, + PoolType = poolAttribute?.Pool }); continue; } diff --git a/Patches/Localization/CustomTooltips.cs b/Patches/Localization/CustomTooltips.cs index 3bea90c7..bcd94121 100644 --- a/Patches/Localization/CustomTooltips.cs +++ b/Patches/Localization/CustomTooltips.cs @@ -1,8 +1,10 @@ using BaseLib.Patches.Content; using HarmonyLib; using MegaCrit.Sts2.Core.Entities.Cards; +using MegaCrit.Sts2.Core.Helpers; using MegaCrit.Sts2.Core.HoverTips; using MegaCrit.Sts2.Core.Localization; +using MegaCrit.Sts2.Core.Models; namespace BaseLib.Patches.Localization; @@ -23,13 +25,26 @@ public static bool CustomKeyword(CardKeyword keyword, ref IHoverTip __result) if (info.RichKeyword) { LocString description = keyword.GetDescription(); - description.Add("energyPrefix", ""); + description.Add("energyPrefix", GetEnergyPrefix(info)); __result = new HoverTip(keyword.GetTitle(), description); return false; } } - + return true; } + + /// + /// Resolve the keyword's energy icon from the pool it declared, the way the game resolves it for cards and + /// powers. An empty prefix makes EnergyIconsFormatter fall back to the character being played, which does not + /// exist outside of a run, so the tooltip would show the colorless icon in the card library. + /// + private static string GetEnergyPrefix(CustomKeywords.KeywordInfo info) + { + if (info.PoolType == null) return ""; + + AbstractModel? pool = ModelDb.GetByIdOrNull(ModelDb.GetId(info.PoolType)); + return pool is IPoolModel ? EnergyIconHelper.GetPrefix(pool) : ""; + } } } \ No newline at end of file