Skip to content

Commit 373eebb

Browse files
committed
feat(SecondaryResources): Add NSecondaryResourceCardCostUi and related patches
- Introduced `NSecondaryResourceCardCostUi` for displaying secondary resource card costs with customizable styles. - Added `NCardUpdateVisualsSecondaryResourceCardUiPatch` to refresh the secondary resource card UI alongside card updates. - Updated documentation to include details about the new card cost UI component.
1 parent c64b268 commit 373eebb

4 files changed

Lines changed: 387 additions & 0 deletions

File tree

Combat/SecondaryResources/Patches/SecondaryResourceUiPatches.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using MegaCrit.Sts2.Core.Combat;
22
using MegaCrit.Sts2.Core.Context;
3+
using MegaCrit.Sts2.Core.Entities.Cards;
4+
using MegaCrit.Sts2.Core.Nodes.Cards;
35
using MegaCrit.Sts2.Core.Nodes.Combat;
46
using MegaCrit.Sts2.Core.Nodes.Multiplayer;
57
using MegaCrit.Sts2.Core.Rooms;
@@ -113,4 +115,25 @@ public static void Postfix(NMultiplayerPlayerState __instance)
113115
SecondaryResourceUiRuntime.SetMultiplayerPlayerStateCombatActive(__instance, false);
114116
}
115117
}
118+
119+
internal sealed class NCardUpdateVisualsSecondaryResourceCardUiPatch : IPatchMethod
120+
{
121+
public static string PatchId => "ritsulib_secondary_resource_card_ui_update";
122+
public static string Description => "Refresh secondary-resource card UI alongside NCard.UpdateVisuals";
123+
public static bool IsCritical => false;
124+
125+
public static ModPatchTarget[] GetTargets()
126+
{
127+
return [new(typeof(NCard), nameof(NCard.UpdateVisuals), [typeof(PileType), typeof(CardPreviewMode)])];
128+
}
129+
130+
public static void Postfix(NCard __instance)
131+
{
132+
if (!ModSecondaryResourceRegistry.HasAny ||
133+
__instance.Model == null)
134+
return;
135+
136+
SecondaryResourceUiRuntime.UpdateCardUi(__instance, __instance.Model);
137+
}
138+
}
116139
}
Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
using Godot;
2+
using MegaCrit.Sts2.addons.mega_text;
3+
using MegaCrit.Sts2.Core.Assets;
4+
using MegaCrit.Sts2.Core.Helpers;
5+
using MegaCrit.Sts2.Core.Models;
6+
7+
namespace STS2RitsuLib.Combat.SecondaryResources
8+
{
9+
/// <summary>
10+
/// Visual style for the simple secondary-resource card-cost display.
11+
/// 简易次级资源卡牌费用显示节点的视觉样式。
12+
/// </summary>
13+
public sealed record SecondaryResourceCardCostUiStyle
14+
{
15+
/// <summary>
16+
/// Root size for one cost slot.
17+
/// 单个费用槽的根节点尺寸。
18+
/// </summary>
19+
public Vector2 SlotSize { get; init; } = new(48f, 48f);
20+
21+
/// <summary>
22+
/// Icon rectangle size inside one slot.
23+
/// 单个费用槽内图标矩形尺寸。
24+
/// </summary>
25+
public Vector2 IconSize { get; init; } = new(46f, 46f);
26+
27+
/// <summary>
28+
/// Offset applied to the amount label relative to the centered icon rectangle.
29+
/// 数量标签相对居中图标矩形的偏移。
30+
/// </summary>
31+
public Vector2 LabelOffset { get; init; }
32+
33+
/// <summary>
34+
/// Amount-label font size.
35+
/// 数量标签字号。
36+
/// </summary>
37+
public int FontSize { get; init; } = 28;
38+
39+
/// <summary>
40+
/// Amount-label outline size.
41+
/// 数量标签描边尺寸。
42+
/// </summary>
43+
public int OutlineSize { get; init; } = 7;
44+
45+
/// <summary>
46+
/// Cost text color when the card can pay this line.
47+
/// 卡牌可支付该行费用时的文本颜色。
48+
/// </summary>
49+
public Color AffordableColor { get; init; } = StsColors.cream;
50+
51+
/// <summary>
52+
/// Cost text color when the card cannot pay this line.
53+
/// 卡牌无法支付该行费用时的文本颜色。
54+
/// </summary>
55+
public Color UnaffordableColor { get; init; } = StsColors.red;
56+
57+
/// <summary>
58+
/// Cost text outline color when the card can pay this line.
59+
/// 卡牌可支付该行费用时的文本描边颜色。
60+
/// </summary>
61+
public Color AffordableOutlineColor { get; init; } = StsColors.defaultStarCostOutline;
62+
63+
/// <summary>
64+
/// Cost text outline color when the card cannot pay this line.
65+
/// 卡牌无法支付该行费用时的文本描边颜色。
66+
/// </summary>
67+
public Color UnaffordableOutlineColor { get; init; } = StsColors.unplayableEnergyCostOutline;
68+
69+
/// <summary>
70+
/// Texture expand mode.
71+
/// 贴图 expand mode。
72+
/// </summary>
73+
public TextureRect.ExpandModeEnum ExpandMode { get; init; } = TextureRect.ExpandModeEnum.IgnoreSize;
74+
75+
/// <summary>
76+
/// Texture stretch mode.
77+
/// 贴图 stretch mode。
78+
/// </summary>
79+
public TextureRect.StretchModeEnum StretchMode { get; init; } =
80+
TextureRect.StretchModeEnum.KeepAspectCentered;
81+
82+
/// <summary>
83+
/// Optional cost formatter. Receives the resolved payment line.
84+
/// 可选费用格式化器,参数为已解析支付行。
85+
/// </summary>
86+
public Func<SecondaryResourcePaymentLine, string>? FormatCost { get; init; }
87+
88+
/// <summary>
89+
/// Shared default style instance.
90+
/// 共享默认样式实例。
91+
/// </summary>
92+
public static SecondaryResourceCardCostUiStyle Default { get; } = new();
93+
94+
internal string Format(SecondaryResourcePaymentLine line)
95+
{
96+
return FormatCost?.Invoke(line) ?? (line.CostsX ? "X" : line.Cost.ToString());
97+
}
98+
}
99+
100+
/// <summary>
101+
/// Simple reusable card-cost display for secondary resources.
102+
/// 简易可复用的次级资源卡牌费用显示节点。
103+
/// </summary>
104+
public partial class NSecondaryResourceCardCostUi : Control
105+
{
106+
private const string DefaultLabelFontPath = "res://themes/kreon_bold_shared.tres";
107+
private CardModel? _boundCard;
108+
private SecondaryResourceDefinition? _definition;
109+
110+
private MegaLabel _label = null!;
111+
private SecondaryResourcePaymentLine? _line;
112+
private SecondaryResourcePaymentPlan? _plan;
113+
private string? _resourceId;
114+
private SecondaryResourceCardCostUiStyle _style = SecondaryResourceCardCostUiStyle.Default;
115+
private TextureRect _texture = null!;
116+
117+
/// <summary>
118+
/// Whether this node refreshes the bound card's resolved payment plan every frame.
119+
/// 该节点是否每帧刷新已绑定卡牌的支付计划。
120+
/// </summary>
121+
public bool AutoRefresh { get; set; } = true;
122+
123+
/// <summary>
124+
/// Creates and configures a card-cost display node for one secondary resource.
125+
/// 为一个次级资源创建并配置卡牌费用显示节点。
126+
/// </summary>
127+
public static NSecondaryResourceCardCostUi Create(
128+
string resourceId,
129+
SecondaryResourceCardCostUiStyle? style = null)
130+
{
131+
var node = new NSecondaryResourceCardCostUi();
132+
node.Configure(style);
133+
node.Bind(resourceId);
134+
return node;
135+
}
136+
137+
/// <summary>
138+
/// Creates and configures a card-cost display node for one secondary resource.
139+
/// 为一个次级资源创建并配置卡牌费用显示节点。
140+
/// </summary>
141+
public static NSecondaryResourceCardCostUi Create(
142+
SecondaryResourceDefinition definition,
143+
SecondaryResourceCardCostUiStyle? style = null)
144+
{
145+
var node = new NSecondaryResourceCardCostUi();
146+
node.Configure(style);
147+
node.Bind(definition);
148+
return node;
149+
}
150+
151+
/// <summary>
152+
/// Configures the visual style.
153+
/// 配置视觉样式。
154+
/// </summary>
155+
public void Configure(SecondaryResourceCardCostUiStyle? style = null)
156+
{
157+
ApplyStyle(style ?? SecondaryResourceCardCostUiStyle.Default);
158+
}
159+
160+
private void ApplyStyle(SecondaryResourceCardCostUiStyle style)
161+
{
162+
ArgumentNullException.ThrowIfNull(style);
163+
164+
_style = style;
165+
CustomMinimumSize = _style.SlotSize;
166+
Size = _style.SlotSize;
167+
168+
if (!IsNodeReady())
169+
return;
170+
171+
ApplyLayout();
172+
ApplyLabelTheme();
173+
}
174+
175+
/// <summary>
176+
/// Binds this node to one secondary resource id.
177+
/// 将该节点绑定到一个次级资源 id。
178+
/// </summary>
179+
public void Bind(string resourceId)
180+
{
181+
ArgumentException.ThrowIfNullOrWhiteSpace(resourceId);
182+
_resourceId = resourceId.Trim();
183+
184+
if (ModSecondaryResourceRegistry.TryGet(_resourceId, out var definition))
185+
Bind(definition);
186+
else if (IsNodeReady())
187+
Visible = false;
188+
}
189+
190+
/// <summary>
191+
/// Binds this node to one secondary resource definition.
192+
/// 将该节点绑定到一个次级资源定义。
193+
/// </summary>
194+
public void Bind(SecondaryResourceDefinition definition)
195+
{
196+
ArgumentNullException.ThrowIfNull(definition);
197+
_resourceId = definition.Id;
198+
_definition = definition;
199+
200+
if (!IsNodeReady())
201+
return;
202+
203+
ApplyDefinition();
204+
}
205+
206+
/// <summary>
207+
/// Refreshes from a card UI update context.
208+
/// 从卡牌 UI 更新上下文刷新。
209+
/// </summary>
210+
public void Refresh<TParent>(SecondaryResourceCardUiContext<TParent, NSecondaryResourceCardCostUi> context)
211+
where TParent : Node
212+
{
213+
Refresh(context.Card, context.Plan);
214+
}
215+
216+
/// <summary>
217+
/// Binds and refreshes this node from <paramref name="card" />.
218+
/// 绑定并根据 <paramref name="card" /> 刷新该节点。
219+
/// </summary>
220+
public void Refresh(CardModel card)
221+
{
222+
ArgumentNullException.ThrowIfNull(card);
223+
Refresh(card, SecondaryResourcePaymentResolver.Plan(card));
224+
}
225+
226+
/// <summary>
227+
/// Refreshes from a resolved payment plan.
228+
/// 根据已解析支付计划刷新。
229+
/// </summary>
230+
public void Refresh(CardModel card, SecondaryResourcePaymentPlan plan)
231+
{
232+
ArgumentNullException.ThrowIfNull(card);
233+
ArgumentNullException.ThrowIfNull(plan);
234+
235+
_boundCard = card;
236+
if (string.IsNullOrWhiteSpace(_resourceId))
237+
{
238+
Visible = false;
239+
return;
240+
}
241+
242+
if (_definition == null && ModSecondaryResourceRegistry.TryGet(_resourceId, out var definition))
243+
{
244+
_definition = definition;
245+
if (IsNodeReady())
246+
ApplyDefinition();
247+
}
248+
249+
var line = plan.Lines.FirstOrDefault(line =>
250+
string.Equals(line.ResourceId, _resourceId, StringComparison.OrdinalIgnoreCase));
251+
if (line == null)
252+
{
253+
Visible = false;
254+
return;
255+
}
256+
257+
Refresh(plan, line);
258+
}
259+
260+
/// <summary>
261+
/// Refreshes this node from the matching resolved payment line.
262+
/// 根据匹配的已解析支付行刷新该节点。
263+
/// </summary>
264+
public void Refresh(SecondaryResourcePaymentPlan plan, SecondaryResourcePaymentLine line)
265+
{
266+
ArgumentNullException.ThrowIfNull(plan);
267+
ArgumentNullException.ThrowIfNull(line);
268+
269+
_plan = plan;
270+
_line = line;
271+
272+
if (!IsNodeReady())
273+
return;
274+
275+
Visible = true;
276+
_label.SetTextAutoSize(_style.Format(line));
277+
_label.AddThemeColorOverride(ThemeConstants.Label.FontColor,
278+
line.IsAffordable ? _style.AffordableColor : _style.UnaffordableColor);
279+
_label.AddThemeColorOverride(ThemeConstants.Label.FontOutlineColor,
280+
line.IsAffordable ? _style.AffordableOutlineColor : _style.UnaffordableOutlineColor);
281+
}
282+
283+
/// <inheritdoc />
284+
public override void _Ready()
285+
{
286+
MouseFilter = MouseFilterEnum.Ignore;
287+
CustomMinimumSize = _style.SlotSize;
288+
Size = _style.SlotSize;
289+
290+
_texture = new()
291+
{
292+
MouseFilter = MouseFilterEnum.Ignore,
293+
};
294+
AddChild(_texture);
295+
296+
_label = new()
297+
{
298+
MouseFilter = MouseFilterEnum.Ignore,
299+
HorizontalAlignment = HorizontalAlignment.Center,
300+
VerticalAlignment = VerticalAlignment.Center,
301+
AutoSizeEnabled = true,
302+
MinFontSize = Math.Max(8, _style.FontSize - 10),
303+
MaxFontSize = _style.FontSize,
304+
};
305+
AddChild(_label);
306+
307+
ApplyLayout();
308+
ApplyLabelTheme();
309+
ApplyDefinition();
310+
311+
if (_plan != null && _line != null)
312+
Refresh(_plan, _line);
313+
else if (_boundCard != null)
314+
Refresh(_boundCard);
315+
else
316+
Visible = false;
317+
}
318+
319+
/// <inheritdoc />
320+
public override void _Process(double delta)
321+
{
322+
if (AutoRefresh && _boundCard != null)
323+
Refresh(_boundCard);
324+
}
325+
326+
private void ApplyLayout()
327+
{
328+
var iconPosition = (_style.SlotSize - _style.IconSize) * 0.5f;
329+
_texture.Position = iconPosition;
330+
_texture.CustomMinimumSize = _style.IconSize;
331+
_texture.Size = _style.IconSize;
332+
_texture.ExpandMode = _style.ExpandMode;
333+
_texture.StretchMode = _style.StretchMode;
334+
335+
_label.Position = iconPosition + _style.LabelOffset;
336+
_label.CustomMinimumSize = _style.IconSize;
337+
_label.Size = _style.IconSize;
338+
_label.MinFontSize = Math.Max(8, _style.FontSize - 10);
339+
_label.MaxFontSize = _style.FontSize;
340+
}
341+
342+
private void ApplyDefinition()
343+
{
344+
if (_definition == null || _texture == null)
345+
return;
346+
347+
var path = _definition.SmallIconPath ?? _definition.LargeIconPath;
348+
_texture.Texture = string.IsNullOrWhiteSpace(path) ? null : ResourceLoader.Load<Texture2D>(path);
349+
}
350+
351+
private void ApplyLabelTheme()
352+
{
353+
var font = PreloadManager.Cache.GetAsset<Font>(DefaultLabelFontPath);
354+
_label.AddThemeFontOverride(ThemeConstants.Label.Font, font);
355+
_label.AddThemeFontSizeOverride(ThemeConstants.Label.FontSize, _style.FontSize);
356+
_label.AddThemeColorOverride(ThemeConstants.Label.FontColor, _style.AffordableColor);
357+
_label.AddThemeColorOverride(ThemeConstants.Label.FontOutlineColor, _style.AffordableOutlineColor);
358+
_label.AddThemeConstantOverride(ThemeConstants.Label.OutlineSize, _style.OutlineSize);
359+
}
360+
}
361+
}

0 commit comments

Comments
 (0)