Skip to content

Commit e09ff88

Browse files
committed
chore(release): merge dev into main for v0.2.14
2 parents 5369d69 + 76bf796 commit e09ff88

12 files changed

Lines changed: 322 additions & 28 deletions
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using Godot;
2+
using MegaCrit.Sts2.Core.Entities.Cards;
3+
using MegaCrit.Sts2.Core.Models;
4+
using MegaCrit.Sts2.Core.Nodes.Cards;
5+
6+
namespace STS2RitsuLib.CardPiles
7+
{
8+
/// <summary>
9+
/// Common subset of data exposed by mod card pile flight contexts.
10+
/// </summary>
11+
public interface IModCardPileFlightContext
12+
{
13+
/// <summary>
14+
/// Definition associated with the flight request.
15+
/// </summary>
16+
ModCardPileDefinition Definition { get; }
17+
18+
/// <summary>
19+
/// Ritsulib's default position for this request.
20+
/// </summary>
21+
Vector2 DefaultPosition { get; }
22+
23+
/// <summary>
24+
/// Source pile for this request, when applicable.
25+
/// </summary>
26+
CardPile? StartPile { get; }
27+
28+
/// <summary>
29+
/// Destination pile for this request, when applicable.
30+
/// </summary>
31+
CardPile? TargetPile { get; }
32+
33+
/// <summary>
34+
/// Live card node involved in the flight, when available.
35+
/// </summary>
36+
NCard? CardNode { get; }
37+
38+
/// <summary>
39+
/// Card model involved in the flight, when available.
40+
/// </summary>
41+
CardModel? CardModel { get; }
42+
}
43+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using MegaCrit.Sts2.Core.Entities.Cards;
2+
using MegaCrit.Sts2.Core.Models;
3+
4+
namespace STS2RitsuLib.CardPiles
5+
{
6+
internal static class ModCardPileFlightHistory
7+
{
8+
private static readonly Dictionary<CardModel, CardPile> LastRemovedPileByCard = [];
9+
10+
internal static void RecordRemoved(CardPile pile, CardModel card)
11+
{
12+
if (pile == null || card == null)
13+
return;
14+
LastRemovedPileByCard[card] = pile;
15+
}
16+
17+
internal static CardPile? TryGetLastRemovedPile(CardModel card)
18+
{
19+
return card == null ? null : LastRemovedPileByCard.GetValueOrDefault(card);
20+
}
21+
22+
internal static void Clear()
23+
{
24+
LastRemovedPileByCard.Clear();
25+
}
26+
}
27+
}
Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,34 @@
11
using Godot;
22
using MegaCrit.Sts2.Core.Entities.Cards;
3+
using MegaCrit.Sts2.Core.Models;
4+
using MegaCrit.Sts2.Core.Nodes.Cards;
35

46
namespace STS2RitsuLib.CardPiles
57
{
68
/// <summary>
79
/// Context passed to <see cref="ModCardPileSpec.FlightStartPositionResolver" /> when shuffle-style fly
810
/// visuals need a source/start position for a mod pile.
911
/// </summary>
10-
public sealed class ModCardPileFlightStartContext
12+
public sealed class ModCardPileFlightStartContext : IModCardPileFlightContext
1113
{
1214
internal ModCardPileFlightStartContext(
1315
ModCardPileDefinition definition,
1416
CardPile startPile,
1517
CardPile targetPile,
16-
Vector2 defaultStartPosition)
18+
Vector2 defaultStartPosition,
19+
NCard? cardNode = null)
1720
{
1821
Definition = definition;
1922
StartPile = startPile;
2023
TargetPile = targetPile;
2124
DefaultStartPosition = defaultStartPosition;
25+
CardNode = cardNode;
2226
}
2327

2428
/// <summary>
25-
/// Definition of the source pile.
29+
/// Ritsulib's default start position for this request.
2630
/// </summary>
27-
public ModCardPileDefinition Definition { get; }
31+
public Vector2 DefaultStartPosition { get; }
2832

2933
/// <summary>
3034
/// Source pile for this shuffle fly visual.
@@ -37,8 +41,17 @@ internal ModCardPileFlightStartContext(
3741
public CardPile TargetPile { get; }
3842

3943
/// <summary>
40-
/// Ritsulib's default start position for this request.
44+
/// Definition of the source pile.
4145
/// </summary>
42-
public Vector2 DefaultStartPosition { get; }
46+
public ModCardPileDefinition Definition { get; }
47+
48+
/// <inheritdoc />
49+
public Vector2 DefaultPosition => DefaultStartPosition;
50+
51+
/// <inheritdoc />
52+
public NCard? CardNode { get; }
53+
54+
/// <inheritdoc />
55+
public CardModel? CardModel => CardNode?.Model;
4356
}
4457
}

CardPiles/ModCardPileFlightTargetContext.cs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using Godot;
2+
using MegaCrit.Sts2.Core.Entities.Cards;
3+
using MegaCrit.Sts2.Core.Models;
24
using MegaCrit.Sts2.Core.Nodes.Cards;
35

46
namespace STS2RitsuLib.CardPiles
@@ -7,7 +9,7 @@ namespace STS2RitsuLib.CardPiles
79
/// Context passed to <see cref="ModCardPileSpec.FlightTargetPositionResolver" /> each time a card
810
/// requests a fly-in target position for a mod pile.
911
/// </summary>
10-
public sealed class ModCardPileFlightTargetContext
12+
public sealed class ModCardPileFlightTargetContext : IModCardPileFlightContext
1113
{
1214
internal ModCardPileFlightTargetContext(
1315
ModCardPileDefinition definition,
@@ -19,6 +21,11 @@ internal ModCardPileFlightTargetContext(
1921
DefaultTargetPosition = defaultTargetPosition;
2022
}
2123

24+
/// <summary>
25+
/// Ritsulib's default target position for this request.
26+
/// </summary>
27+
public Vector2 DefaultTargetPosition { get; }
28+
2229
/// <summary>
2330
/// Definition of the target pile.
2431
/// </summary>
@@ -29,9 +36,16 @@ internal ModCardPileFlightTargetContext(
2936
/// </summary>
3037
public NCard? CardNode { get; }
3138

32-
/// <summary>
33-
/// Ritsulib's default target position for this request.
34-
/// </summary>
35-
public Vector2 DefaultTargetPosition { get; }
39+
/// <inheritdoc />
40+
public Vector2 DefaultPosition => DefaultTargetPosition;
41+
42+
/// <inheritdoc />
43+
public CardPile? StartPile => null;
44+
45+
/// <inheritdoc />
46+
public CardPile? TargetPile => null;
47+
48+
/// <inheritdoc />
49+
public CardModel? CardModel => CardNode?.Model;
3650
}
3751
}

CardPiles/ModCardPileLayout.cs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,41 +52,55 @@ private static Vector2 GetDefaultTargetPosition(ModCardPileDefinition definition
5252
var fallback = FallbackPosition();
5353

5454
if (definition.Anchor.Kind == ModCardPileAnchorKind.Custom)
55-
return definition.Anchor.CustomPosition + definition.Anchor.Offset;
55+
return ApplyCardNodeOffset(definition.Anchor.CustomPosition + definition.Anchor.Offset, node);
5656

5757
var button = ModCardPileButtonRegistry.TryGetButton(definition);
5858
if (button != null && button.IsInsideTree())
59-
return button.GlobalPosition + button.Size * 0.5f + definition.Anchor.Offset;
59+
return ApplyCardNodeOffset(button.GlobalPosition + button.Size * 0.5f + definition.Anchor.Offset, node);
6060

6161
var extraHand = ModCardPileButtonRegistry.TryGetExtraHand(definition);
6262
if (extraHand != null && extraHand.IsInsideTree())
63-
return extraHand.GlobalPosition + extraHand.Size * 0.5f + definition.Anchor.Offset;
63+
return ApplyCardNodeOffset(extraHand.GlobalPosition + extraHand.Size * 0.5f + definition.Anchor.Offset,
64+
node);
6465

6566
if (definition.Style == ModCardPileUiStyle.TopBarDeck)
6667
{
6768
var deck = NRun.Instance?.GlobalUi?.TopBar?.Deck;
6869
if (deck != null)
69-
return deck.GlobalPosition + deck.Size * 0.5f + new Vector2(-120f, 0f) + definition.Anchor.Offset;
70+
return ApplyCardNodeOffset(
71+
deck.GlobalPosition + deck.Size * 0.5f + new Vector2(-120f, 0f) + definition.Anchor.Offset,
72+
node);
7073
}
7174

7275
if (!CombatManager.Instance.IsInProgress || NCombatRoom.Instance?.Ui == null)
73-
return fallback + definition.Anchor.Offset;
76+
return ApplyCardNodeOffset(fallback + definition.Anchor.Offset, node);
7477

7578
var ui = NCombatRoom.Instance.Ui;
7679
return definition.Style switch
7780
{
7881
ModCardPileUiStyle.BottomLeft =>
79-
ui.DrawPile.GlobalPosition + ui.DrawPile.Size * 0.5f + new Vector2(0f, -140f) +
80-
definition.Anchor.Offset,
82+
ApplyCardNodeOffset(
83+
ui.DrawPile.GlobalPosition + ui.DrawPile.Size * 0.5f + new Vector2(0f, -140f) +
84+
definition.Anchor.Offset,
85+
node),
8186
ModCardPileUiStyle.BottomRight =>
82-
ui.ExhaustPile.GlobalPosition + ui.ExhaustPile.Size * 0.5f + new Vector2(-140f, 0f) +
83-
definition.Anchor.Offset,
87+
ApplyCardNodeOffset(
88+
ui.ExhaustPile.GlobalPosition + ui.ExhaustPile.Size * 0.5f + new Vector2(-140f, 0f) +
89+
definition.Anchor.Offset,
90+
node),
8491
ModCardPileUiStyle.ExtraHand =>
85-
new Vector2(fallback.X - (node?.Size.X ?? 0f) * 0.5f, fallback.Y - 260f) + definition.Anchor.Offset,
86-
_ => fallback + definition.Anchor.Offset,
92+
ApplyCardNodeOffset(new Vector2(fallback.X, fallback.Y - 260f) + definition.Anchor.Offset, node),
93+
_ => ApplyCardNodeOffset(fallback + definition.Anchor.Offset, node),
8794
};
8895
}
8996

97+
private static Vector2 ApplyCardNodeOffset(Vector2 centerPosition, NCard? node)
98+
{
99+
if (node == null)
100+
return centerPosition;
101+
return centerPosition - node.Size * 0.5f;
102+
}
103+
90104
private static Vector2 FallbackPosition()
91105
{
92106
var game = NGame.Instance;
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System.Reflection;
2+
using Godot;
3+
using HarmonyLib;
4+
using MegaCrit.Sts2.Core.Entities.Cards;
5+
using MegaCrit.Sts2.Core.Nodes.Cards;
6+
using MegaCrit.Sts2.Core.Nodes.Vfx;
7+
using STS2RitsuLib.Patching.Models;
8+
9+
namespace STS2RitsuLib.CardPiles.Patches
10+
{
11+
/// <summary>
12+
/// When a card fly visual is spawned after the card has already been reparented, the node may no
13+
/// longer carry enough "old pile" context to choose a correct start position. This patch consults
14+
/// <see cref="ModCardPileFlightHistory" /> (fed by <see cref="CardPile.CardRemoved" />) to recover the
15+
/// source pile and applies <see cref="ModCardPileSpec.FlightStartPositionResolver" /> when the source
16+
/// pile is a mod pile.
17+
/// </summary>
18+
public sealed class ModCardPileCardFlyVfxStartPositionPatch : IPatchMethod
19+
{
20+
private static readonly FieldInfo? StartPositionField =
21+
AccessTools.Field(typeof(NCardFlyVfx), "_startPos");
22+
23+
/// <inheritdoc />
24+
public static string PatchId => "ritsulib_mod_pile_card_fly_vfx_start_position";
25+
26+
/// <inheritdoc />
27+
public static string Description => "Allow mod piles to customize NCardFlyVfx start positions";
28+
29+
/// <inheritdoc />
30+
public static bool IsCritical => false;
31+
32+
/// <inheritdoc />
33+
public static ModPatchTarget[] GetTargets()
34+
{
35+
return [new(typeof(NCardFlyVfx), nameof(NCardFlyVfx.Create))];
36+
}
37+
38+
// ReSharper disable InconsistentNaming
39+
/// <summary>
40+
/// Post-processes the created fly vfx and overwrites its start position when the recovered
41+
/// source pile is a mod pile with a start-position resolver.
42+
/// </summary>
43+
public static void Postfix(NCard card, Vector2 end, bool isAddingToPile, string trailPath,
44+
ref NCardFlyVfx? __result)
45+
{
46+
if (__result == null || StartPositionField == null)
47+
return;
48+
49+
var model = card.Model;
50+
if (model == null)
51+
return;
52+
53+
var oldPile = ModCardPileFlightHistory.TryGetLastRemovedPile(model);
54+
if (oldPile == null)
55+
return;
56+
if (!ModCardPileRegistry.TryGetByPileType(oldPile.Type, out var definition))
57+
return;
58+
59+
var resolver = definition.FlightStartPositionResolver;
60+
if (resolver == null)
61+
return;
62+
63+
var targetPile = model.Pile ?? oldPile;
64+
var defaultStartPosition = ModCardPileLayout.GetTargetPosition(definition, card);
65+
var context =
66+
new ModCardPileFlightStartContext(definition, oldPile, targetPile, defaultStartPosition, card);
67+
var resolved = resolver(context) ?? defaultStartPosition;
68+
69+
StartPositionField.SetValue(__result, resolved);
70+
card.GlobalPosition = resolved;
71+
}
72+
// ReSharper restore InconsistentNaming
73+
}
74+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using MegaCrit.Sts2.Core.Entities.Cards;
2+
using MegaCrit.Sts2.Core.Models;
3+
using STS2RitsuLib.Patching.Models;
4+
5+
namespace STS2RitsuLib.CardPiles.Patches
6+
{
7+
/// <summary>
8+
/// Records the last pile a card was removed from, to help flight/VFX patches recover "old pile"
9+
/// information when the call-site does not provide it.
10+
/// </summary>
11+
public sealed class ModCardPileFlightHistoryCardPilePatch : IPatchMethod
12+
{
13+
/// <inheritdoc />
14+
public static string PatchId => "ritsulib_mod_pile_flight_history_card_pile_remove";
15+
16+
/// <inheritdoc />
17+
public static string Description => "Track last removed CardPile for flight start recovery";
18+
19+
/// <inheritdoc />
20+
public static bool IsCritical => false;
21+
22+
/// <inheritdoc />
23+
public static ModPatchTarget[] GetTargets()
24+
{
25+
return
26+
[
27+
new(typeof(CardPile), nameof(CardPile.RemoveInternal), [typeof(CardModel), typeof(bool)]),
28+
];
29+
}
30+
31+
// ReSharper disable InconsistentNaming
32+
/// <summary>
33+
/// Records the pile a card was removed from so later VFX patches can recover the "old pile"
34+
/// context when it is not provided by the call-site.
35+
/// </summary>
36+
public static void Prefix(CardPile __instance, CardModel card, bool silent)
37+
{
38+
if (silent)
39+
return;
40+
ModCardPileFlightHistory.RecordRemoved(__instance, card);
41+
}
42+
// ReSharper restore InconsistentNaming
43+
}
44+
}

0 commit comments

Comments
 (0)