Skip to content
Open
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
Binary file added BaseLib/images/ui/tempHP.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions BaseLib/localization/eng/static_hover_tips.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"BASELIB-REFUND.title": "Refund",
"BASELIB-REFUND.description": "When energy is spent on this card, up to [blue]X[/blue] of that energy is refunded.",
"BASELIB-REFUND.smartDescription": "When energy is spent on this card, up to [blue]{Refund}[/blue] of that energy is refunded.",
"BASELIB-VITALITY.title": "Vitality",
"BASELIB-VITALITY.description": "Until the end of combat, prevents HP loss.",
"BASELIB-SCRY.title": "Scry",
"BASELIB-SCRY.description": "Look at the top [blue]X[/blue] cards of your [gold]Draw Pile[/gold]. You may discard any of them.",
"BASELIB-SCRY.smartDescription": "Look at the top {Scry:plural:card|[blue]{Scry:diff()}[/blue] cards} of your [gold]Draw Pile[/gold]. You may discard {Scry:plural:it|any of them}."
Expand Down
49 changes: 49 additions & 0 deletions Cards/Variables/VitalityVar.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using BaseLib.Extensions;
using BaseLib.Hooks;
using MegaCrit.Sts2.Core.Entities.Cards;
using MegaCrit.Sts2.Core.Entities.Creatures;
using MegaCrit.Sts2.Core.Localization.DynamicVars;
using MegaCrit.Sts2.Core.Models;

namespace BaseLib.Cards.Variables;

/// <summary>
/// Represents a dynamic variable for the "Vitality" keyword, responsible for calculating
/// and updating the vitality value shown on card previews, including active combat modifiers.
/// </summary>
public class VitalityVar : DynamicVar
{
/// <summary>
/// Creates a new vitality variable named <c>"Vitality"</c> and registers its hover tooltip
/// via <see cref="DynamicVarExtensions.WithTooltip{TDynamicVar}"/>, which resolves the
/// <c>BASELIB-VITALITY</c> localization entries (preferring <c>.smartDescription</c> when available).
/// </summary>
/// <param name="baseValue">The base vitality amount before modifiers are applied.</param>
public VitalityVar(decimal baseValue) : base("Vitality", baseValue)
{
this.WithTooltip();
}

/// <summary>
/// Updates the preview value of the vitality variable on the card.
/// Passes the current integer value through global vitality modification hooks if enabled.
/// </summary>
/// <param name="card">The card model displaying this variable.</param>
/// <param name="previewMode">The mode dictating how the card preview is rendered.</param>
/// <param name="target">The target creature of the card action, if any.</param>
/// <param name="runGlobalHooks">
/// If <see langword="true"/>, routes the base value through <see cref="BaseLibHooks.ModifyVitalityAmount"/>
/// to account for relics, powers, or status effects that alter vitality counts.
/// </param>
public override void UpdateCardPreview(
CardModel card,
CardPreviewMode previewMode,
Creature? target,
bool runGlobalHooks)
{
var vitalityAmount = BaseValue;
if (runGlobalHooks)
vitalityAmount = BaseLibHooks.ModifyVitalityAmount(card.CombatState, card.Owner.Creature, BaseValue, card, null, out _);
PreviewValue = vitalityAmount;
}
}
100 changes: 100 additions & 0 deletions Commands/VitalityCmd.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using BaseLib.Hooks;
using BaseLib.Patches.Features;
using MegaCrit.Sts2.Core.Combat;
using MegaCrit.Sts2.Core.Combat.History;
using MegaCrit.Sts2.Core.Commands;
using MegaCrit.Sts2.Core.Entities.Cards;
using MegaCrit.Sts2.Core.Entities.Creatures;
using MegaCrit.Sts2.Core.Entities.Players;

namespace BaseLib.Commands;

/// <summary>
/// Command utility responsible for executing the vitality mechanic process, including
/// hook modification, finding current values, and combatHistory entries.
/// </summary>
public class VitalityCmd
{
/// <summary>Make this creature gain the specified amount of vitality.</summary>
/// <param name="creature">Creature that should gain vitality.</param>
/// <param name="amount">Amount of vitality they should gain.</param>
/// <param name="cardPlay">
/// The CardPlay that caused the vitality gain.
/// Null if it was not directly caused by a card play.
/// </param>
/// <param name="fast">If true, the wait that is performed after vitality gain is very small. Should be used in scenarios
/// where vitality gain happens quickly in sequence (i.e. like Afterimage for block).</param>
/// <returns>The amount of vitality that the creature gained after all modifications were applied.</returns>
public static async Task<decimal> GainVitality(
Creature creature,
decimal amount,
CardPlay? cardPlay,
bool fast = false)
{
if (CombatManager.Instance.IsOverOrEnding)
return 0M;
var combatState = creature.CombatState;
await BaseLibHooks.BeforeVitalityGained(combatState, creature, amount, cardPlay?.Card);
var modifiedAmount = BaseLibHooks.ModifyVitalityAmount(combatState, creature, amount, cardPlay?.Card, cardPlay, out var modifiers);
modifiedAmount = Math.Max(modifiedAmount, 0M);
await BaseLibHooks.AfterModifyingVitalityAmount(combatState, modifiedAmount, cardPlay, modifiers);
if (modifiedAmount > 0M)
{
SfxCmd.Play("event:/sfx/heal");
VfxCmd.PlayOnCreatureCenter(creature, "vfx/vfx_cross_heal");
VitalityPatch.VitalityField.SetVitality(creature, (int) amount + VitalityPatch.VitalityField.GetVitality(creature));
CombatManager.Instance.History.Add(combatState, new VitalityGainedEntry((int)modifiedAmount, cardPlay, creature, combatState.RoundNumber, combatState.CurrentSide, CombatManager.Instance.History, combatState.Players));
if (fast)
await Cmd.CustomScaledWait(0.0f, 0.03f);
else
await Cmd.CustomScaledWait(0.1f, 0.25f);
}
await BaseLibHooks.AfterVitalityGained(combatState, creature, modifiedAmount, cardPlay?.Card);
return modifiedAmount;
}

/// <summary>Returns the amount vitality a specific creature has.</summary>
/// <param name="creature">Creature you're trying to get the vitality amount of.</param>
/// <returns>The amount of vitality that the creature has currently.</returns>
public static decimal Get(Creature creature)
{
return VitalityPatch.VitalityField.GetVitality(creature);
}

/// <summary>Removes all vitality a specific creature has.</summary>
/// <param name="creature">Creature you're trying to remove the vitality from.</param>
public static void RemoveAll(Creature creature)
{
VitalityPatch.VitalityField.SetVitality(creature, 0);
}

private class VitalityGainedEntry : CombatHistoryEntry
{
public int Amount { get; }

public Creature Receiver => Actor;

public CardPlay? CardPlay { get; }

public override string Description => $"{GetId(Receiver)} gained {Amount} vitality";

public VitalityGainedEntry(
int amount,
CardPlay? cardPlay,
Creature receiver,
int roundNumber,
CombatSide currentSide,
CombatHistory history,
IEnumerable<Player> players)
: base(receiver, roundNumber, currentSide, history, players)
{
Amount = amount;
CardPlay = cardPlay;
}

private static string GetId(Creature creature)
{
return !creature.IsPlayer ? creature.Monster.Id.Entry : creature.Player.Character.Id.Entry;
}
}
}
7 changes: 7 additions & 0 deletions Extensions/DynamicVarSetExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,11 @@ public static ScryVar Scry(this DynamicVarSet vard)
return (ScryVar)vard._vars[nameof(Scry)];
}

/// <summary>
/// Get the Vitality var initialized with its default name.
/// </summary>
public static VitalityVar Vitality(this DynamicVarSet vard)
{
return (VitalityVar)vard._vars[nameof(Vitality)];
}
}
105 changes: 105 additions & 0 deletions Hooks/BaseLibHooks.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using BaseLib.Abstracts;
using BaseLib.Utils;
using MegaCrit.Sts2.Core.Combat;
using MegaCrit.Sts2.Core.Entities.Cards;
using MegaCrit.Sts2.Core.Entities.Creatures;
using MegaCrit.Sts2.Core.Entities.Players;
using MegaCrit.Sts2.Core.GameActions.Multiplayer;
using MegaCrit.Sts2.Core.Models;
Expand Down Expand Up @@ -128,4 +130,107 @@ public static decimal ModifyResourceCostInCombat<T>(
out _);
return modifiedCost;
}

/// <summary>
/// Passes a scry amount through all <see cref="IModifyVitalityAmount" /> hook listeners in
/// listener order, letting each adjust the value, and reports which listeners changed it.
/// <para>
/// A listener counts as a modifier only if it changed the value it received
/// (per-step <see cref="int" /> equality): returning the input unchanged does not
/// register it in <paramref name="modifiers" />. The comparison is per listener, not
/// against the original amount.
/// Listeners whose changes cancel each other out
/// (+2 then −2) are <b>all</b> recorded, so <paramref name="modifiers" /> can be
/// non-empty even when the returned amount equals <paramref name="amount" />.
/// </para>
/// <para>
/// Outside of combat (no combat state), returns <paramref name="amount" /> unchanged
/// with an empty <paramref name="modifiers" /> set.
/// </para>
/// </summary>
/// <param name="combatState">ICombatState of the creature gaining Vitality.</param>
/// <param name="creature">The creature gaining vitality.</param>
/// <param name="amount">The original vitality amount before <see cref="ModifyVitalityAmount" /> modifiers.</param>
/// <param name="cardSource">Passes the cardSource if there is one.</param>
/// <param name="cardPlay">Similarly passes the cardPlay if there is one.</param>
/// <param name="modifiers">
/// The listeners that changed the value, for follow-up dispatch via
/// <see cref="AfterModifyingVitalityAmount" />.
/// </param>
/// <returns>
/// The final Vitality amount. Not clamped: may be zero or negative if listeners reduce it;
/// callers are expected to treat non-positive results as "no vitality" or clamp it themselves.
/// </returns>
public static decimal ModifyVitalityAmount(
ICombatState combatState,
Creature creature,
decimal amount,
CardModel? cardSource,
CardPlay? cardPlay,
out IEnumerable<IModifyVitalityAmount> modifiers)
{
var additive = HookUtils.Modify(combatState, amount, (m, a) => m.ModifyVitalityAdditive(creature, a, cardSource, cardPlay), out modifiers);
return HookUtils.ModifyMultiplicative(combatState, additive, modifiers, (m, a) => m.ModifyVitalityMultiplicative(creature, a, cardSource, cardPlay), out modifiers);
}

/// <summary>
/// Dispatches <see cref="IModifyVitalityAmount.AfterModifyingVitalityAmount" /> to each listener
/// that changed the vitality amount in the preceding <see cref="ModifyVitalityAmount" /> call -
/// e.g. to play VFX or consume a charge.
/// <para>
/// Listeners that saw the value but left it unchanged are not called. Iteration
/// follows current hook-listener order, not the order of
/// <paramref name="modifiers" />.
/// </para>
/// </summary>
/// <param name="combatState">ICombatState of the creature gaining Vitality.</param>
/// <param name="amount">The original vitality amount before <see cref="ModifyVitalityAmount" /> modifiers.</param>
/// <param name="cardPlay">Similarly passes the cardSource if there is one.</param>
/// <param name="modifiers">
/// The listeners that changed the value, for follow-up dispatch via
/// <see cref="AfterModifyingVitalityAmount" />.
/// </param>
public static Task AfterModifyingVitalityAmount(
ICombatState combatState,
decimal amount,
CardPlay? cardPlay,
IEnumerable<IModifyVitalityAmount> modifiers)
{
return HookUtils.AfterModifying(combatState, modifiers, a => a.AfterModifyingVitalityAmount(amount, cardPlay));
}


/// <summary>
/// Dispatches <see cref="IVitalityHooks.BeforeVitalityGained" /> to
/// all subscribed models before the Vitality is given.
/// </summary>
/// <param name="combatState">ICombatState of the creature gaining Vitality.</param>
/// <param name="creature">The creature gaining vitality.</param>
/// <param name="amount">The vitality amount BEFORE <see cref="ModifyVitalityAmount" /> modifiers.</param>
/// <param name="cardSource">Gives source card if there is one. </param>
public static Task BeforeVitalityGained(
ICombatState combatState,
Creature creature,
decimal amount,
CardModel? cardSource)
{
return HookUtils.Dispatch<IVitalityHooks>(combatState, a => a.BeforeVitalityGained(creature, amount, cardSource));
}

/// <summary>
/// Dispatches <see cref="IVitalityHooks.AfterVitalityGained" /> to
/// all subscribed models after the Vitality is given.
/// </summary>
/// <param name="combatState">ICombatState of the creature gaining Vitality.</param>
/// <param name="creature">The creature gaining vitality.</param>
/// <param name="amount">The vitality amount after <see cref="ModifyVitalityAmount" /> modifiers.</param>
/// <param name="cardSource">Gives source card if there is one. </param>
public static Task AfterVitalityGained(
ICombatState combatState,
Creature creature,
decimal amount,
CardModel? cardSource)
{
return HookUtils.Dispatch<IVitalityHooks>(combatState, a => a.AfterVitalityGained(creature, amount, cardSource));
}
}
77 changes: 77 additions & 0 deletions Hooks/IModifyVitalityAmount.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using MegaCrit.Sts2.Core.Entities.Cards;
using MegaCrit.Sts2.Core.Entities.Creatures;
using MegaCrit.Sts2.Core.Models;

namespace BaseLib.Hooks;

/// <summary>
/// Hook for models (relics, powers, stances, ...) that adjust the amount of vitality given.
/// Listeners are invoked in hook-listener order via
/// <see cref="BaseLibHooks.ModifyVitalityAmount" /> before it resolves.
/// </summary>
public interface IModifyVitalityAmount
{
/// <summary>
/// Returns the adjusted vitality amount. Called once per pending vitality, receiving the value as
/// modified by any earlier listeners. Always runs before <see cref="ModifyVitalityMultiplicative" /> as to not cause ordering issues.
/// <para>
/// Return <paramref name="amount" /> unchanged to opt out — doing so also excludes
/// this listener from the <see cref="AfterModifyingVitalityAmount" /> follow-up. Changing
/// the value (per-call <see cref="int" /> equality) marks this listener as a modifier
/// even if a later listener cancels the change.
/// </para>
/// <para>
/// Results are not clamped. This method must be pure with respect to game state -
/// put side effects (VFX, charge consumption) in <see cref="AfterModifyingVitalityAmount" />
/// instead, which only runs when a change was actually made.
/// </para>
/// </summary>
/// <param name="creature">The creature gaining vitality.</param>
/// <param name="amount">
/// The vitality amount so far, including earlier listeners' changes.
/// This value is clamped to a minimum of zero.
/// </param>
/// <param name="cardSource">Passes the cardSource if there is one.</param>
/// <param name="cardPlay">Similarly passes the cardPlay if there is one.</param>
/// <returns>The new vitality amount; may be lower, higher, or unchanged.</returns>
decimal ModifyVitalityAdditive(Creature creature, decimal amount, CardModel? cardSource, CardPlay? cardPlay) => 0m;

/// <summary>
/// Follow-up invoked after all listeners have run, but only on listeners whose
/// <see cref="ModifyVitalityAdditive" /> or <see cref="ModifyVitalityMultiplicative" /> changed the value they received. Use this for the
/// side effects of having modified the vitality: visuals, sounds, consuming charges,
/// decrementing counters.
/// <para>
/// The amounts describe the <b>whole</b> modification pass, not this listener's step:
/// every invoked listener receives the same pair, and
/// <paramref name="amount" /> includes other listeners' changes. A listener
/// wanting its own delta must capture the values it saw in
/// <see cref="ModifyVitalityAdditive" /> or <see cref="ModifyVitalityMultiplicative" /> themselves.
/// </para>
/// </summary>
/// <param name="amount">The original vitality amount before <see cref="ModifyVitalityAdditive" /> or <see cref="ModifyVitalityMultiplicative" /> modifiers.</param>
/// <param name="cardPlay">Similarly passes the cardSource if there is one.</param>
Task AfterModifyingVitalityAmount(decimal amount, CardPlay? cardPlay) => Task.CompletedTask;

/// <summary>
/// Returns a multiplier to alter the vitality amount by. Called once per pending vitality addition, receiving the value as
/// modified by any earlier listeners. Always runs after <see cref="ModifyVitalityAdditive" /> as to not cause ordering issues.
/// <para>
/// Return <paramref name="amount" /> unchanged to opt out — doing so also excludes
/// this listener from the <see cref="AfterModifyingVitalityAmount" /> follow-up. Changing
/// the value (per-call <see cref="int" /> equality) marks this listener as a modifier
/// even if a later listener cancels the change.
/// </para>
/// <para>
/// Results are not clamped. This method must be pure with respect to game state -
/// put side effects (VFX, charge consumption) in <see cref="AfterModifyingVitalityAmount" />
/// instead, which only runs when a change was actually made.
/// </para>
/// </summary>
/// <param name="creature">The creature gaining vitality.</param>
/// <param name="amount">The vitality amount so far, including earlier listeners' changes.</param>
/// <param name="cardSource">Passes the cardSource if there is one.</param>
/// <param name="cardPlay">Similarly passes the cardPlay if there is one.</param>
/// <returns>The <b>multiplier</b> to alter the current vitality amount by.</returns>
decimal ModifyVitalityMultiplicative(Creature creature, decimal amount, CardModel? cardSource, CardPlay? cardPlay) => 1m;
}
Loading