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
153 changes: 153 additions & 0 deletions Abstracts/CustomMonsterModel.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Override this or place your scene at res://scenes/creature_visuals/class_name.tscn
/// </summary>
public virtual string? CustomVisualPath => null;

public virtual string? CustomAttackSfx => null;
public virtual string? CustomCastSfx => null;
public virtual string? CustomDeathSfx => null;


/// <summary>
/// By default, will convert a scene containing the necessary nodes into a NCreatureVisuals even if it is not one.
/// </summary>
/// <returns></returns>
public virtual NCreatureVisuals? CreateCustomVisuals() {
string? path = (CustomVisualPath ?? VisualsPath);
if (path == null) return null;
return GodotUtils.CreatureVisualsFromScene(path);
}


/// <summary>
/// Override and return a CreatureAnimator if you need to set up states that differ from the default for the monster.
/// Using <seealso cref="SetupAnimationState"/> is suggested.
/// </summary>
/// <returns></returns>
public virtual CreatureAnimator? SetupCustomAnimationStates(MegaSprite controller)
{
return null;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="controller"></param>
/// <param name="idleName"></param>
/// <param name="deadName"></param>
/// <param name="deadLoop"></param>
/// <param name="hitName"></param>
/// <param name="hitLoop"></param>
/// <param name="attackName"></param>
/// <param name="attackLoop"></param>
/// <param name="castName"></param>
/// <param name="castLoop"></param>
/// <returns></returns>
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;
}
}
39 changes: 39 additions & 0 deletions Abstracts/CustomPetModel.cs
Original file line number Diff line number Diff line change
@@ -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{
/// <summary>
/// Override this or place your scene at res://scenes/creature_visuals/class_name.tscn
/// </summary>
public new virtual string? CustomVisualPath => null;

public override int MinInitialHp => 9999;

public override int MaxInitialHp => 9999;

public override bool IsHealthBarVisible => false;

/// <summary>
/// By default, will convert a scene containing the necessary nodes into a NCreatureVisuals even if it is not one.
/// </summary>
/// <returns></returns>
public virtual NCreatureVisuals? CreateCustomVisuals()
{
if (CustomVisualPath == null) return null;
return GodotUtils.CreatureVisualsFromScene(CustomVisualPath);
}

protected override MonsterMoveStateMachine GenerateMoveStateMachine()
{
MoveState nothingState = new MoveState("NOTHING_MOVE", (IReadOnlyList<Creature> _) => Task.CompletedTask);
nothingState.FollowUpState = nothingState;
return new MonsterMoveStateMachine([nothingState], nothingState);
}
}
112 changes: 67 additions & 45 deletions Patches/Content/CustomAnimationPatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<AnimationPlayer>(visualNodeRoot)?.UseAnimationPlayer(animName, trigger) != null)
return false;
if (FindNode<AnimatedSprite2D>(visualNodeRoot)?.UseAnimatedSprite2D(animName, trigger) != null)
return false;
}

private static AnimationPlayer? FindAnimationPlayer(Node root)
{
return root.GetNodeOrNull<AnimationPlayer>("AnimationPlayer")
?? root.GetNodeOrNull<AnimationPlayer>("Visuals/AnimationPlayer")
?? root.GetNodeOrNull<AnimationPlayer>("Body/AnimationPlayer")
?? SearchRecursive(root);
}
if (SearchRecursive<AnimationPlayer>(visualNodeRoot)?.UseAnimationPlayer(animName, trigger) != null)
return false;
if (SearchRecursive<AnimatedSprite2D>(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<T>(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<T>(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<T>(child);
if (found != null) return found;
}

return null;
}
}
}