diff --git a/Abstracts/CustomMonsterModel.cs b/Abstracts/CustomMonsterModel.cs new file mode 100644 index 0000000..58461f7 --- /dev/null +++ b/Abstracts/CustomMonsterModel.cs @@ -0,0 +1,153 @@ +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.Entities.Creatures; +using MegaCrit.Sts2.Core.Models; +using MegaCrit.Sts2.Core.MonsterMoves.MonsterMoveStateMachine; +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; + + 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. + /// + /// + public virtual NCreatureVisuals? CreateCustomVisuals() { + string? path = (CustomVisualPath ?? VisualsPath); + if (path == null) return null; + return GodotUtils.CreatureVisualsFromScene(path); + } + + + /// + /// Override and return a CreatureAnimator if you need to set up states that differ from the default for the monster. + /// Using is suggested. + /// + /// + public virtual CreatureAnimator? SetupCustomAnimationStates(MegaSprite controller) + { + 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 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 diff --git a/Patches/Content/CustomAnimationPatch.cs b/Patches/Content/CustomAnimationPatch.cs index 1f0fa47..b97d09d 100644 --- a/Patches/Content/CustomAnimationPatch.cs +++ b/Patches/Content/CustomAnimationPatch.cs @@ -3,58 +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; - - var animPlayer = FindAnimationPlayer(__instance.Visuals); - if (animPlayer == null) return false; - - var animName = trigger switch - { - CreatureAnimator.idleTrigger => "idle", - CreatureAnimator.attackTrigger => "attack", - CreatureAnimator.castTrigger => "cast", - CreatureAnimator.hitTrigger => "hurt", - CreatureAnimator.deathTrigger => "die", - _ => trigger.ToLowerInvariant() - }; - - if (animPlayer.CurrentAnimation.Equals(animName) || animPlayer.CurrentAnimation.Equals(trigger)) - animPlayer.Stop(); + if (__instance.HasSpineAnimation) return true; - if (animPlayer.HasAnimation(animName)) - animPlayer.Play(animName); - else if (animPlayer.HasAnimation(trigger)) - animPlayer.Play(trigger); + var animName = trigger switch + { + CreatureAnimator.idleTrigger => "idle", + CreatureAnimator.attackTrigger => "attack", + CreatureAnimator.castTrigger => "cast", + CreatureAnimator.hitTrigger => "hurt", + CreatureAnimator.deathTrigger => "die", + _ => trigger.ToLowerInvariant() + }; + + var visualNodeRoot = __instance.Visuals; + if (FindNode(visualNodeRoot)?.UseAnimationPlayer(animName, trigger) != null) + return false; + if (FindNode(visualNodeRoot)?.UseAnimatedSprite2D(animName, trigger) != null) return false; - } - private static AnimationPlayer? FindAnimationPlayer(Node root) - { - return root.GetNodeOrNull("AnimationPlayer") - ?? root.GetNodeOrNull("Visuals/AnimationPlayer") - ?? root.GetNodeOrNull("Body/AnimationPlayer") - ?? SearchRecursive(root); - } + if (SearchRecursive(visualNodeRoot)?.UseAnimationPlayer(animName, trigger) != null) + return false; + if (SearchRecursive(visualNodeRoot)?.UseAnimatedSprite2D(animName, trigger) != null) + return false; + + return true; + } - private static AnimationPlayer? SearchRecursive(Node node) + 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; + } + + private static AnimationPlayer UseAnimationPlayer(this 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)) + animPlayer.Play(trigger); + + 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 node.GetChildren()) - { - if (child is AnimationPlayer player) return player; - 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