Skip to content

Commit 402db00

Browse files
committed
feat(OrbManagement): introduce mod orb random pool and value display policies
- Added interfaces for controlling orb selection in random generation and value label display. - Implemented patches to integrate mod orbs into the random orb pool and manage their display modes. - Enhanced the ModOrbTemplate class to support new policies, allowing for greater customization of orb behavior. - Updated documentation to reflect the new features and their usage in modding scenarios.
1 parent 0ea133c commit 402db00

3 files changed

Lines changed: 279 additions & 1 deletion

File tree

RitsuLibFramework.PatcherSetup.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,7 @@ private static void RegisterContentAssetPatches()
279279
patcher.RegisterPatch<OrbIconPatch>();
280280
patcher.RegisterPatch<OrbSpritePathPatch>();
281281
patcher.RegisterPatch<OrbAssetPathsPatch>();
282+
patcher.RegisterPatch<NOrbValueDisplayPolicyPatch>();
282283

283284
patcher.RegisterPatch<PotionImagePathPatch>();
284285
patcher.RegisterPatch<PotionTexturePatch>();
@@ -402,6 +403,7 @@ private static void RegisterContentRegistryPatches()
402403
patcher.RegisterPatch<RelicCollectionActListPatch>();
403404
patcher.RegisterPatch<AllPowersPatch>();
404405
patcher.RegisterPatch<AllOrbsPatch>();
406+
patcher.RegisterPatch<OrbModelRandomPoolPolicyPatch>();
405407
patcher.RegisterPatch<AllSharedCardPoolsPatch>();
406408
patcher.RegisterPatch<AllSharedEventsPatch>();
407409
patcher.RegisterPatch<AllEventsPatch>();

Scaffolding/Content/ModOrbTemplate.cs

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Globalization;
12
using Godot;
23
using MegaCrit.Sts2.Core.HoverTips;
34
using MegaCrit.Sts2.Core.Models;
@@ -6,13 +7,103 @@
67

78
namespace STS2RitsuLib.Scaffolding.Content
89
{
10+
/// <summary>
11+
/// Controls whether an orb can be selected by vanilla random-orb generation such as <c>OrbModel.GetRandomOrb</c>.
12+
/// 控制充能球是否可被原版随机充能球生成(例如 <c>OrbModel.GetRandomOrb</c>)选中。
13+
/// </summary>
14+
public interface IModOrbRandomPoolPolicy
15+
{
16+
/// <summary>
17+
/// When true, RitsuLib adds this registered orb to the random-orb candidate pool. Defaults to false on
18+
/// <see cref="ModOrbTemplate" /> so custom orbs do not appear in random effects unless the author opts in.
19+
/// 为 true 时,RitsuLib 会把此已注册充能球加入随机充能球候选池。<see cref="ModOrbTemplate" />
20+
/// 默认为 false,因此自定义充能球不会在作者未选择加入时出现在随机效果中。
21+
/// </summary>
22+
bool AllowInRandomOrbPool { get; }
23+
}
24+
25+
/// <summary>
26+
/// Value-label display mode for combat orb nodes. RitsuLib uses the vanilla <c>%PassiveAmount</c> and
27+
/// <c>%EvokeAmount</c> labels without repositioning them; a single visible label uses the vanilla single-value
28+
/// layout, while two visible labels use the vanilla stacked layout.
29+
/// 战斗充能球节点的数值标签显示模式。RitsuLib 使用原版 <c>%PassiveAmount</c> 与
30+
/// <c>%EvokeAmount</c> 标签,不重新定位;单个可见标签使用原版单数值布局,两个可见标签使用原版堆叠布局。
31+
/// </summary>
32+
public enum ModOrbValueDisplayMode
33+
{
34+
/// <summary>
35+
/// Keep the base game's built-in type checks.
36+
/// 保留游戏内建的类型判定。
37+
/// </summary>
38+
Vanilla = 0,
39+
40+
/// <summary>
41+
/// Hide both passive and evoke value labels.
42+
/// 隐藏被动和激发两个数值标签。
43+
/// </summary>
44+
Hidden = 1,
45+
46+
/// <summary>
47+
/// Match the normal vanilla orb behavior: show the passive value normally and the evoke value while the orb is
48+
/// previewed as evoking. Only one label is visible at a time, so it uses the vanilla single-value layout.
49+
/// 匹配普通原版充能球行为:平时显示被动值;当充能球被预览为激发时显示激发值。一次只显示一个标签,
50+
/// 因此使用原版单数值布局。
51+
/// </summary>
52+
Contextual = 2,
53+
54+
/// <summary>
55+
/// Always show only the passive value text in the vanilla single-value layout.
56+
/// 始终在原版单数值布局中显示被动值文本。
57+
/// </summary>
58+
SinglePassive = 3,
59+
60+
/// <summary>
61+
/// Always show only the evoke value text in the vanilla single-value layout.
62+
/// 始终在原版单数值布局中显示激发值文本。
63+
/// </summary>
64+
SingleEvoke = 4,
65+
66+
/// <summary>
67+
/// Match the vanilla <c>DarkOrb</c> behavior: show passive and evoke value labels together in the vanilla
68+
/// stacked layout.
69+
/// 匹配原版 <c>DarkOrb</c> 行为:在原版堆叠布局中同时显示被动值和激发值标签。
70+
/// </summary>
71+
Both = 5,
72+
}
73+
74+
/// <summary>
75+
/// Controls the passive/evoke value labels rendered on an orb node.
76+
/// 控制充能球节点上渲染的被动/激发数值标签。
77+
/// </summary>
78+
public interface IModOrbValueDisplayPolicy
79+
{
80+
/// <summary>
81+
/// Label visibility behavior. Use <see cref="ModOrbValueDisplayMode.Vanilla" /> to leave the game decision intact.
82+
/// 标签可见性行为。使用 <see cref="ModOrbValueDisplayMode.Vanilla" /> 可保留游戏原判定。
83+
/// </summary>
84+
ModOrbValueDisplayMode ValueDisplayMode { get; }
85+
86+
/// <summary>
87+
/// Text rendered in the passive label when it is visible.
88+
/// 被动标签可见时渲染的文本。
89+
/// </summary>
90+
string PassiveValueDisplayText { get; }
91+
92+
/// <summary>
93+
/// Text rendered in the evoke label when it is visible.
94+
/// 激发标签可见时渲染的文本。
95+
/// </summary>
96+
string EvokeValueDisplayText { get; }
97+
}
98+
999
/// <summary>
10100
/// Base <see cref="OrbModel" /> for mods: keyword hover tips, dimmed UI color default, and
11101
/// <see cref="IModOrbAssetOverrides" /> paths and optional <see cref="TryCreateOrbSprite" />.
12102
/// Mod 充能球的基础 <see cref="OrbModel" />:提供关键词悬浮提示、变暗 UI 颜色默认值、
13103
/// <see cref="IModOrbAssetOverrides" /> 路径,以及可选的 <see cref="TryCreateOrbSprite" />。
14104
/// </summary>
15-
public abstract class ModOrbTemplate : OrbModel, IModOrbAssetOverrides, IModOrbSpriteFactory
105+
public abstract class ModOrbTemplate : OrbModel, IModOrbAssetOverrides, IModOrbSpriteFactory,
106+
IModOrbRandomPoolPolicy, IModOrbValueDisplayPolicy
16107
{
17108
/// <summary>
18109
/// Keyword ids surfaced on this orb's hover tips. <b>Display-only</b>: unlike
@@ -52,11 +143,23 @@ public abstract class ModOrbTemplate : OrbModel, IModOrbAssetOverrides, IModOrbS
52143
/// <inheritdoc />
53144
public virtual string? CustomVisualsScenePath => AssetProfile.VisualsScenePath;
54145

146+
/// <inheritdoc />
147+
public virtual bool AllowInRandomOrbPool => false;
148+
55149
Node2D? IModOrbSpriteFactory.TryCreateOrbSprite()
56150
{
57151
return TryCreateOrbSprite();
58152
}
59153

154+
/// <inheritdoc />
155+
public virtual ModOrbValueDisplayMode ValueDisplayMode => ModOrbValueDisplayMode.Vanilla;
156+
157+
/// <inheritdoc />
158+
public virtual string PassiveValueDisplayText => PassiveVal.ToString("0", CultureInfo.InvariantCulture);
159+
160+
/// <inheritdoc />
161+
public virtual string EvokeValueDisplayText => EvokeVal.ToString("0", CultureInfo.InvariantCulture);
162+
60163
/// <summary>
61164
/// Non-null node replaces the scene from <see cref="CustomVisualsScenePath" />; provide Spine and animations
62165
/// compatible with <c>CreateSprite</c> callers if required.
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
using System.Reflection;
2+
using HarmonyLib;
3+
using MegaCrit.Sts2.addons.mega_text;
4+
using MegaCrit.Sts2.Core.Models;
5+
using MegaCrit.Sts2.Core.Nodes.Orbs;
6+
using MegaCrit.Sts2.Core.Random;
7+
using STS2RitsuLib.Patching.Models;
8+
9+
namespace STS2RitsuLib.Scaffolding.Content.Patches
10+
{
11+
internal static class ModOrbRandomPoolPolicyRuntime
12+
{
13+
private static readonly FieldInfo? VanillaValidOrbsField = AccessTools.Field(typeof(OrbModel), "_validOrbs");
14+
15+
public static bool HasModdedCandidates()
16+
{
17+
var vanillaIds = GetVanillaRandomOrbIds();
18+
return vanillaIds.Length != 0 && ModelDb.Orbs.Any(orb => ShouldAddFromModelDbOrbs(vanillaIds, orb));
19+
}
20+
21+
public static bool TryGetRandomOrb(Rng rng, out OrbModel orb)
22+
{
23+
var vanillaIds = GetVanillaRandomOrbIds();
24+
if (vanillaIds.Length == 0)
25+
{
26+
orb = null!;
27+
return false;
28+
}
29+
30+
var candidates = BuildCandidates(vanillaIds);
31+
var selected = rng.NextItem(candidates);
32+
if (selected == null)
33+
{
34+
orb = null!;
35+
return false;
36+
}
37+
38+
orb = selected;
39+
return true;
40+
}
41+
42+
private static ModelId[] GetVanillaRandomOrbIds()
43+
{
44+
return VanillaValidOrbsField?.GetValue(null) as ModelId[] ?? [];
45+
}
46+
47+
private static OrbModel[] BuildCandidates(IReadOnlyCollection<ModelId> vanillaIds)
48+
{
49+
return vanillaIds
50+
.Select(static id => ModelDb.GetByIdOrNull<OrbModel>(id))
51+
.OfType<OrbModel>()
52+
.Concat(ModelDb.Orbs.Where(orb => ShouldAddFromModelDbOrbs(vanillaIds, orb)))
53+
.DistinctBy(static orb => orb.Id)
54+
.ToArray();
55+
}
56+
57+
private static bool ShouldAddFromModelDbOrbs(IReadOnlyCollection<ModelId> vanillaIds, OrbModel orb)
58+
{
59+
return !vanillaIds.Contains(orb.Id) &&
60+
orb is IModOrbRandomPoolPolicy { AllowInRandomOrbPool: true };
61+
}
62+
}
63+
64+
/// <summary>
65+
/// Replaces <see cref="OrbModel.GetRandomOrb" /> only when registered mod orbs opt in to the random pool.
66+
/// 仅当已注册 mod 充能球选择加入随机池时替换 <see cref="OrbModel.GetRandomOrb" />。
67+
/// </summary>
68+
public sealed class OrbModelRandomPoolPolicyPatch : IPatchMethod
69+
{
70+
/// <inheritdoc />
71+
public static string PatchId => "orb_model_random_pool_policy";
72+
73+
/// <inheritdoc />
74+
public static bool IsCritical => false;
75+
76+
/// <inheritdoc />
77+
public static string Description => "Include opt-in mod orbs in OrbModel.GetRandomOrb";
78+
79+
/// <inheritdoc />
80+
public static ModPatchTarget[] GetTargets()
81+
{
82+
return [new(typeof(OrbModel), nameof(OrbModel.GetRandomOrb), [typeof(Rng)])];
83+
}
84+
85+
/// <summary>
86+
/// Uses the vanilla random pool plus registered <see cref="IModOrbRandomPoolPolicy" /> opt-in candidates.
87+
/// 使用原版随机池,加上已注册且通过 <see cref="IModOrbRandomPoolPolicy" /> 选择加入的候选项。
88+
/// </summary>
89+
[HarmonyAfter(Const.BaseLibHarmonyId, Const.FrameworkContentRegistryHarmonyId)]
90+
[HarmonyPriority(Priority.Last)]
91+
// ReSharper disable once InconsistentNaming
92+
public static bool Prefix(Rng rng, ref OrbModel __result)
93+
{
94+
if (!ModOrbRandomPoolPolicyRuntime.HasModdedCandidates())
95+
return true;
96+
97+
if (!ModOrbRandomPoolPolicyRuntime.TryGetRandomOrb(rng, out var orb))
98+
return true;
99+
100+
__result = orb;
101+
return false;
102+
}
103+
}
104+
105+
internal static class ModOrbValueDisplayPolicyRuntime
106+
{
107+
private static readonly FieldInfo? PassiveLabelField = AccessTools.Field(typeof(NOrb), "_passiveLabel");
108+
private static readonly FieldInfo? EvokeLabelField = AccessTools.Field(typeof(NOrb), "_evokeLabel");
109+
110+
public static void Apply(NOrb node, bool isEvoking)
111+
{
112+
if (node.Model is not IModOrbValueDisplayPolicy policy ||
113+
policy.ValueDisplayMode == ModOrbValueDisplayMode.Vanilla)
114+
return;
115+
116+
var passiveLabel = PassiveLabelField?.GetValue(node) as MegaLabel;
117+
var evokeLabel = EvokeLabelField?.GetValue(node) as MegaLabel;
118+
if (passiveLabel == null || evokeLabel == null)
119+
return;
120+
121+
var (showPassive, showEvoke) = policy.ValueDisplayMode switch
122+
{
123+
ModOrbValueDisplayMode.Hidden => (false, false),
124+
ModOrbValueDisplayMode.Contextual => (!isEvoking, isEvoking),
125+
ModOrbValueDisplayMode.SinglePassive => (true, false),
126+
ModOrbValueDisplayMode.SingleEvoke => (false, true),
127+
ModOrbValueDisplayMode.Both => (true, true),
128+
_ => (passiveLabel.Visible, evokeLabel.Visible),
129+
};
130+
131+
passiveLabel.Visible = showPassive;
132+
evokeLabel.Visible = showEvoke;
133+
if (showPassive)
134+
passiveLabel.SetTextAutoSize(policy.PassiveValueDisplayText);
135+
if (showEvoke)
136+
evokeLabel.SetTextAutoSize(policy.EvokeValueDisplayText);
137+
}
138+
}
139+
140+
/// <summary>
141+
/// Applies <see cref="IModOrbValueDisplayPolicy" /> after vanilla orb visual refresh.
142+
/// 在原版充能球视觉刷新之后应用 <see cref="IModOrbValueDisplayPolicy" />。
143+
/// </summary>
144+
public sealed class NOrbValueDisplayPolicyPatch : IPatchMethod
145+
{
146+
/// <inheritdoc />
147+
public static string PatchId => "norb_value_display_policy";
148+
149+
/// <inheritdoc />
150+
public static bool IsCritical => false;
151+
152+
/// <inheritdoc />
153+
public static string Description => "Allow mod orbs to control passive/evoke value labels";
154+
155+
/// <inheritdoc />
156+
public static ModPatchTarget[] GetTargets()
157+
{
158+
return [new(typeof(NOrb), nameof(NOrb.UpdateVisuals), [typeof(bool)])];
159+
}
160+
161+
/// <summary>
162+
/// Reconciles labels with the mod orb's requested display mode.
163+
/// 按 mod 充能球请求的显示模式同步标签。
164+
/// </summary>
165+
[HarmonyAfter(Const.BaseLibHarmonyId)]
166+
[HarmonyPriority(Priority.Last)]
167+
// ReSharper disable once InconsistentNaming
168+
public static void Postfix(NOrb __instance, bool isEvoking)
169+
{
170+
ModOrbValueDisplayPolicyRuntime.Apply(__instance, isEvoking);
171+
}
172+
}
173+
}

0 commit comments

Comments
 (0)