From 14cb4f8ab6ccc802d5d1d50f0e291087ed0729fc Mon Sep 17 00:00:00 2001 From: Syra 3 <25936043+SyraLessThanThree@users.noreply.github.com> Date: Sun, 22 Mar 2026 07:14:11 +0100 Subject: [PATCH 1/6] Generalizing CustomAnimationPatch.cs and adding AnimatedSprite2D support. --- Patches/Content/CustomAnimationPatch.cs | 63 ++++++++++++++++++------- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/Patches/Content/CustomAnimationPatch.cs b/Patches/Content/CustomAnimationPatch.cs index 1f0fa47..1fa721d 100644 --- a/Patches/Content/CustomAnimationPatch.cs +++ b/Patches/Content/CustomAnimationPatch.cs @@ -12,10 +12,7 @@ class CustomAnimationPatch public static bool Prefix(NCreature __instance, string trigger) { if (__instance.HasSpineAnimation) return true; - - var animPlayer = FindAnimationPlayer(__instance.Visuals); - if (animPlayer == null) return false; - + var animName = trigger switch { CreatureAnimator.idleTrigger => "idle", @@ -26,31 +23,63 @@ public static bool Prefix(NCreature __instance, string trigger) _ => trigger.ToLowerInvariant() }; - if (animPlayer.CurrentAnimation.Equals(animName) || animPlayer.CurrentAnimation.Equals(trigger)) - animPlayer.Stop(); + NCreatureVisuals visualNodeRoot = __instance.Visuals; + + var animPlayer = FindNode(visualNodeRoot); + if (animPlayer != null) { + UseAnimationPlayer(animPlayer, animName, trigger); + return false; + } + var animSprite = FindNode(visualNodeRoot); + if (animSprite != null) { + UseAnimatedSprite2D(animSprite, animName, trigger); + return false; + } + + animPlayer ??= SearchRecursive(visualNodeRoot); + if (animPlayer != null) { + UseAnimationPlayer(animPlayer, animName, trigger); + return false; + } + animSprite ??= SearchRecursive(visualNodeRoot); + if (animSprite != null) { + UseAnimatedSprite2D(animSprite, animName, trigger); + return false; + } + return false; + } + + private static void UseAnimatedSprite2D(AnimatedSprite2D animSprite, string animName, string trigger) + { + if (animSprite.SpriteFrames.HasAnimation(animName)) + animSprite.Play(animName); + else if (animSprite.SpriteFrames.HasAnimation(trigger)) + animSprite.Play(trigger); + } + + private static void UseAnimationPlayer(AnimationPlayer animPlayer, string animName, string trigger) + { if (animPlayer.HasAnimation(animName)) animPlayer.Play(animName); else if (animPlayer.HasAnimation(trigger)) animPlayer.Play(trigger); - - return false; } - private static AnimationPlayer? FindAnimationPlayer(Node root) + private static T? FindNode(Node root, string? name = null ) where T : Node? { - return root.GetNodeOrNull("AnimationPlayer") - ?? root.GetNodeOrNull("Visuals/AnimationPlayer") - ?? root.GetNodeOrNull("Body/AnimationPlayer") - ?? SearchRecursive(root); + name = name ?? nameof(T); + return root.GetNodeOrNull(name) + ?? root.GetNodeOrNull("Visuals/"+name) + ?? root.GetNodeOrNull("Body/"+name); } - private static AnimationPlayer? SearchRecursive(Node node) + private static T? SearchRecursive(Node parent) where T : Node? { - foreach (var child in node.GetChildren()) + foreach (var child in parent.GetChildren()) { - if (child is AnimationPlayer player) return player; - var found = SearchRecursive(child); + if (child is T nodeToFind) return nodeToFind; + var found = SearchRecursive(child); if (found != null) return found; } return null; From 62b6bf1c44172189c5933775118b6fffd4929f41 Mon Sep 17 00:00:00 2001 From: Syra 3 <25936043+SyraLessThanThree@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:44:40 +0100 Subject: [PATCH 2/6] Master Merge Resolve --- Patches/Content/CustomAnimationPatch.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Patches/Content/CustomAnimationPatch.cs b/Patches/Content/CustomAnimationPatch.cs index 1fa721d..df634ca 100644 --- a/Patches/Content/CustomAnimationPatch.cs +++ b/Patches/Content/CustomAnimationPatch.cs @@ -60,6 +60,9 @@ private static void UseAnimatedSprite2D(AnimatedSprite2D animSprite, string anim private static void UseAnimationPlayer(AnimationPlayer animPlayer, string animName, string trigger) { + if (animPlayer.CurrentAnimation.Equals(animName) || animPlayer.CurrentAnimation.Equals(trigger)) + animPlayer.Stop(); + if (animPlayer.HasAnimation(animName)) animPlayer.Play(animName); else if (animPlayer.HasAnimation(trigger)) From f46139ba86570f3391346a8cce1c9cc869d79da7 Mon Sep 17 00:00:00 2001 From: Alchyr Date: Sun, 22 Mar 2026 05:40:28 -0700 Subject: [PATCH 3/6] restructure --- Patches/Content/CustomAnimationPatch.cs | 138 +++++++++++------------- 1 file changed, 64 insertions(+), 74 deletions(-) diff --git a/Patches/Content/CustomAnimationPatch.cs b/Patches/Content/CustomAnimationPatch.cs index df634ca..b97d09d 100644 --- a/Patches/Content/CustomAnimationPatch.cs +++ b/Patches/Content/CustomAnimationPatch.cs @@ -3,90 +3,80 @@ using MegaCrit.Sts2.Core.Animation; using MegaCrit.Sts2.Core.Nodes.Combat; -namespace BaseLib.Patches.Content +namespace BaseLib.Patches.Content; + +[HarmonyPatch(typeof(NCreature), nameof(NCreature.SetAnimationTrigger))] +static class CustomAnimationPatch { - [HarmonyPatch(typeof(NCreature), nameof(NCreature.SetAnimationTrigger))] - class CustomAnimationPatch + [HarmonyPrefix] + public static bool Prefix(NCreature __instance, string trigger) { - [HarmonyPrefix] - public static bool Prefix(NCreature __instance, string trigger) - { - if (__instance.HasSpineAnimation) return true; + if (__instance.HasSpineAnimation) return true; - var animName = trigger switch - { - CreatureAnimator.idleTrigger => "idle", - CreatureAnimator.attackTrigger => "attack", - CreatureAnimator.castTrigger => "cast", - CreatureAnimator.hitTrigger => "hurt", - CreatureAnimator.deathTrigger => "die", - _ => trigger.ToLowerInvariant() - }; + var animName = trigger switch + { + CreatureAnimator.idleTrigger => "idle", + CreatureAnimator.attackTrigger => "attack", + CreatureAnimator.castTrigger => "cast", + CreatureAnimator.hitTrigger => "hurt", + CreatureAnimator.deathTrigger => "die", + _ => trigger.ToLowerInvariant() + }; - NCreatureVisuals visualNodeRoot = __instance.Visuals; - - var animPlayer = FindNode(visualNodeRoot); - if (animPlayer != null) { - UseAnimationPlayer(animPlayer, animName, trigger); - return false; - } - var animSprite = FindNode(visualNodeRoot); - if (animSprite != null) { - UseAnimatedSprite2D(animSprite, animName, trigger); - return false; - } - - animPlayer ??= SearchRecursive(visualNodeRoot); - if (animPlayer != null) { - UseAnimationPlayer(animPlayer, animName, trigger); - return false; - } - animSprite ??= SearchRecursive(visualNodeRoot); - if (animSprite != null) { - UseAnimatedSprite2D(animSprite, animName, trigger); - return false; - } - + var visualNodeRoot = __instance.Visuals; + + if (FindNode(visualNodeRoot)?.UseAnimationPlayer(animName, trigger) != null) + return false; + if (FindNode(visualNodeRoot)?.UseAnimatedSprite2D(animName, trigger) != null) return false; - } - private static void UseAnimatedSprite2D(AnimatedSprite2D animSprite, string animName, string trigger) - { - if (animSprite.SpriteFrames.HasAnimation(animName)) - animSprite.Play(animName); - else if (animSprite.SpriteFrames.HasAnimation(trigger)) - animSprite.Play(trigger); - } + if (SearchRecursive(visualNodeRoot)?.UseAnimationPlayer(animName, trigger) != null) + return false; + if (SearchRecursive(visualNodeRoot)?.UseAnimatedSprite2D(animName, trigger) != null) + return false; + + return true; + } - private static void UseAnimationPlayer(AnimationPlayer animPlayer, string animName, string trigger) - { - if (animPlayer.CurrentAnimation.Equals(animName) || animPlayer.CurrentAnimation.Equals(trigger)) - animPlayer.Stop(); + private static AnimatedSprite2D UseAnimatedSprite2D(this AnimatedSprite2D animSprite, string animName, string trigger) + { + if (animSprite.SpriteFrames.HasAnimation(animName)) + animSprite.Play(animName); + else if (animSprite.SpriteFrames.HasAnimation(trigger)) + animSprite.Play(trigger); + return animSprite; + } - if (animPlayer.HasAnimation(animName)) - animPlayer.Play(animName); - else if (animPlayer.HasAnimation(trigger)) - animPlayer.Play(trigger); - } + private static AnimationPlayer UseAnimationPlayer(this AnimationPlayer animPlayer, string animName, string trigger) + { + if (animPlayer.CurrentAnimation.Equals(animName) || animPlayer.CurrentAnimation.Equals(trigger)) + animPlayer.Stop(); - private static T? FindNode(Node root, string? name = null ) where T : Node? - { - name = name ?? nameof(T); - return root.GetNodeOrNull(name) - ?? root.GetNodeOrNull("Visuals/"+name) - ?? root.GetNodeOrNull("Body/"+name); - } + if (animPlayer.HasAnimation(animName)) + animPlayer.Play(animName); + else if (animPlayer.HasAnimation(trigger)) + animPlayer.Play(trigger); - private static T? SearchRecursive(Node parent) where T : Node? + return animPlayer; + } + + private static T? FindNode(Node root, string? name = null) where T : Node? + { + name ??= nameof(T); + var n = root.GetNodeOrNull(name) + ?? root.GetNodeOrNull("Visuals/" + name) + ?? root.GetNodeOrNull("Body/" + name); + return n as T; + } + + private static T? SearchRecursive(Node parent) where T : Node? + { + foreach (var child in parent.GetChildren()) { - foreach (var child in parent.GetChildren()) - { - if (child is T nodeToFind) return nodeToFind; - var found = SearchRecursive(child); - if (found != null) return found; - } - return null; + if (child is T nodeToFind) return nodeToFind; + var found = SearchRecursive(child); + if (found != null) return found; } - + return null; } -} +} \ No newline at end of file From 24b2104a5ec665f7a398024fdabace7b5a7f1a69 Mon Sep 17 00:00:00 2001 From: Syra 3 <25936043+SyraLessThanThree@users.noreply.github.com> Date: Sun, 22 Mar 2026 07:38:20 +0100 Subject: [PATCH 4/6] Barebones CustomMonsterModel.cs and CustomPetModel.cs --- Abstracts/CustomMonsterModel.cs | 37 +++++++++++++++++++++++++++++++ Abstracts/CustomPetModel.cs | 39 +++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 Abstracts/CustomMonsterModel.cs create mode 100644 Abstracts/CustomPetModel.cs diff --git a/Abstracts/CustomMonsterModel.cs b/Abstracts/CustomMonsterModel.cs new file mode 100644 index 0000000..4258be0 --- /dev/null +++ b/Abstracts/CustomMonsterModel.cs @@ -0,0 +1,37 @@ +using BaseLib.Utils; +using MegaCrit.Sts2.Core.Animation; +using MegaCrit.Sts2.Core.Bindings.MegaSpine; +using MegaCrit.Sts2.Core.Models; +using MegaCrit.Sts2.Core.Nodes.Combat; + +namespace BaseLib.Abstracts; + +public abstract class CustomMonsterModel : MonsterModel, ICustomModel +{ + /// + /// Override this or place your scene at res://scenes/creature_visuals/class_name.tscn + /// + public virtual string? CustomVisualPath => null; + + + /// + /// By default, will convert a scene containing the necessary nodes into a NCreatureVisuals even if it is not one. + /// + /// + public virtual NCreatureVisuals? CreateCustomVisuals() + { + if (CustomVisualPath == null) return null; + return GodotUtils.CreatureVisualsFromScene(CustomVisualPath); + } + + + /// + /// Override and return a CreatureAnimator if you need to set up states that differ from the default for your character. + /// Using is suggested. + /// + /// + public virtual CreatureAnimator? SetupCustomAnimationStates(MegaSprite controller) + { + return null; + } +} \ No newline at end of file diff --git a/Abstracts/CustomPetModel.cs b/Abstracts/CustomPetModel.cs new file mode 100644 index 0000000..6dbd7e5 --- /dev/null +++ b/Abstracts/CustomPetModel.cs @@ -0,0 +1,39 @@ +using BaseLib.Utils; +using MegaCrit.Sts2.Core.Animation; +using MegaCrit.Sts2.Core.Bindings.MegaSpine; +using MegaCrit.Sts2.Core.Entities.Creatures; +using MegaCrit.Sts2.Core.Models; +using MegaCrit.Sts2.Core.MonsterMoves.MonsterMoveStateMachine; +using MegaCrit.Sts2.Core.Nodes.Combat; + +namespace BaseLib.Abstracts; + +public class CustomPetModel: CustomMonsterModel ,ICustomModel{ + /// + /// Override this or place your scene at res://scenes/creature_visuals/class_name.tscn + /// + public new virtual string? CustomVisualPath => null; + + public override int MinInitialHp => 9999; + + public override int MaxInitialHp => 9999; + + public override bool IsHealthBarVisible => false; + + /// + /// By default, will convert a scene containing the necessary nodes into a NCreatureVisuals even if it is not one. + /// + /// + public virtual NCreatureVisuals? CreateCustomVisuals() + { + if (CustomVisualPath == null) return null; + return GodotUtils.CreatureVisualsFromScene(CustomVisualPath); + } + + protected override MonsterMoveStateMachine GenerateMoveStateMachine() + { + MoveState nothingState = new MoveState("NOTHING_MOVE", (IReadOnlyList _) => Task.CompletedTask); + nothingState.FollowUpState = nothingState; + return new MonsterMoveStateMachine([nothingState], nothingState); + } +} \ No newline at end of file From 6ffd808518ac8b340084e19e9ca10d7a03cef77e Mon Sep 17 00:00:00 2001 From: Syra 3 <25936043+SyraLessThanThree@users.noreply.github.com> Date: Sun, 22 Mar 2026 10:24:22 +0100 Subject: [PATCH 5/6] In CustomMonsterModel.CreateCustomVisuals Include both CustomVisualPath and VisualsPath as a backup --- Abstracts/CustomMonsterModel.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Abstracts/CustomMonsterModel.cs b/Abstracts/CustomMonsterModel.cs index 4258be0..6d38471 100644 --- a/Abstracts/CustomMonsterModel.cs +++ b/Abstracts/CustomMonsterModel.cs @@ -1,5 +1,8 @@ using BaseLib.Utils; +using Godot; +using HarmonyLib; using MegaCrit.Sts2.Core.Animation; +using MegaCrit.Sts2.Core.Assets; using MegaCrit.Sts2.Core.Bindings.MegaSpine; using MegaCrit.Sts2.Core.Models; using MegaCrit.Sts2.Core.Nodes.Combat; @@ -18,10 +21,10 @@ public abstract class CustomMonsterModel : MonsterModel, ICustomModel /// By default, will convert a scene containing the necessary nodes into a NCreatureVisuals even if it is not one. /// /// - public virtual NCreatureVisuals? CreateCustomVisuals() - { - if (CustomVisualPath == null) return null; - return GodotUtils.CreatureVisualsFromScene(CustomVisualPath); + public virtual NCreatureVisuals? CreateCustomVisuals() { + string? path = (CustomVisualPath ?? VisualsPath); + if (path == null) return null; + return GodotUtils.CreatureVisualsFromScene(path); } From 42d57cb1e4c166212bb0a83558600849c0dc1ad9 Mon Sep 17 00:00:00 2001 From: Syra 3 <25936043+SyraLessThanThree@users.noreply.github.com> Date: Sun, 22 Mar 2026 14:38:58 +0100 Subject: [PATCH 6/6] Monster SetupCustomAnimationStates copied mostly from CustomCharacterModel --- Abstracts/CustomMonsterModel.cs | 115 +++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/Abstracts/CustomMonsterModel.cs b/Abstracts/CustomMonsterModel.cs index 6d38471..58461f7 100644 --- a/Abstracts/CustomMonsterModel.cs +++ b/Abstracts/CustomMonsterModel.cs @@ -4,7 +4,9 @@ using MegaCrit.Sts2.Core.Animation; using MegaCrit.Sts2.Core.Assets; using MegaCrit.Sts2.Core.Bindings.MegaSpine; +using MegaCrit.Sts2.Core.Entities.Creatures; using MegaCrit.Sts2.Core.Models; +using MegaCrit.Sts2.Core.MonsterMoves.MonsterMoveStateMachine; using MegaCrit.Sts2.Core.Nodes.Combat; namespace BaseLib.Abstracts; @@ -16,6 +18,10 @@ public abstract class CustomMonsterModel : MonsterModel, ICustomModel /// public virtual string? CustomVisualPath => null; + public virtual string? CustomAttackSfx => null; + public virtual string? CustomCastSfx => null; + public virtual string? CustomDeathSfx => null; + /// /// By default, will convert a scene containing the necessary nodes into a NCreatureVisuals even if it is not one. @@ -29,7 +35,7 @@ public abstract class CustomMonsterModel : MonsterModel, ICustomModel /// - /// Override and return a CreatureAnimator if you need to set up states that differ from the default for your character. + /// Override and return a CreatureAnimator if you need to set up states that differ from the default for the monster. /// Using is suggested. /// /// @@ -37,4 +43,111 @@ public abstract class CustomMonsterModel : MonsterModel, ICustomModel { return null; } + + /// + /// If you have a spine animation without all the required animations, + /// use this method to set up a controller that will use animations of your choice for each animation. + /// Any omitted animation parameters will default to the idle animation. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static CreatureAnimator SetupAnimationState(MegaSprite controller, string idleName, + string? deadName = null, bool deadLoop = false, + string? hitName = null, bool hitLoop = false, + string? attackName = null, bool attackLoop = false, + string? castName = null, bool castLoop = false) + { + var idleAnim = new AnimState(idleName, true); + var deadAnim = deadName == null ? idleAnim : new AnimState(deadName, deadLoop); + var hitAnim = hitName == null ? idleAnim : + new AnimState(hitName, hitLoop) + { + NextState = idleAnim + }; + var attackAnim = attackName == null ? idleAnim : + new AnimState(attackName, attackLoop) + { + NextState = idleAnim + }; + var castAnim = castName == null ? idleAnim : + new AnimState(castName, castLoop) + { + NextState = idleAnim + }; + + var animator = new CreatureAnimator(idleAnim, controller); + + animator.AddAnyState("Idle", idleAnim); + animator.AddAnyState("Dead", deadAnim); + animator.AddAnyState("Hit", hitAnim); + animator.AddAnyState("Attack", attackAnim); + animator.AddAnyState("Cast", castAnim); + + return animator; + } +} + +[HarmonyPatch(typeof(MonsterModel), nameof(MonsterModel.GenerateAnimator))] +class GenerateAnimatorPatchMonster +{ + [HarmonyPrefix] + static bool CustomAnimator(MonsterModel __instance, MegaSprite controller, ref CreatureAnimator? __result) + { + if (__instance is not CustomMonsterModel customMon) + return true; + + __result = customMon.SetupCustomAnimationStates(controller); + return __result == null; + } +} + +[HarmonyPatch(typeof(MonsterModel), "AttackSfx", MethodType.Getter)] +class AttackSfxMonster +{ + [HarmonyPrefix] + static bool Custom(MonsterModel __instance, ref string? __result) + { + if (__instance is not CustomMonsterModel customMon) + return true; + + __result = customMon.CustomAttackSfx; + return __result == null; + } +} + +[HarmonyPatch(typeof(MonsterModel), "CastSfx", MethodType.Getter)] +class CastSfxMonster +{ + [HarmonyPrefix] + static bool Custom(MonsterModel __instance, ref string? __result) + { + if (__instance is not CustomMonsterModel customMon) + return true; + + __result = customMon.CustomCastSfx; + return __result == null; + } +} + +[HarmonyPatch(typeof(MonsterModel), "DeathSfx", MethodType.Getter)] +class DeathSfxMonster +{ + [HarmonyPrefix] + static bool Custom(MonsterModel __instance, ref string? __result) + { + if (__instance is not CustomMonsterModel customMon) + return true; + + __result = customMon.CustomDeathSfx; + return __result == null; + } } \ No newline at end of file