From 40f0a2814e77f537176a1f863cedd68350c746b4 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Mon, 13 Apr 2026 00:58:57 -0400
Subject: [PATCH 01/41] Add thread storm
---
SSMP/Animation/AnimationClip.cs | 1 +
SSMP/Animation/AnimationManager.cs | 41 ++-
.../Effects/SilkSkills/BaseSilkSkill.cs | 60 ++++
.../Effects/SilkSkills/ThreadStorm.cs | 257 ++++++++++++++++++
SSMP/Util/AudioUtil.cs | 49 ++++
5 files changed, 402 insertions(+), 6 deletions(-)
create mode 100644 SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
create mode 100644 SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
diff --git a/SSMP/Animation/AnimationClip.cs b/SSMP/Animation/AnimationClip.cs
index c7e54e40..b35b2292 100644
--- a/SSMP/Animation/AnimationClip.cs
+++ b/SSMP/Animation/AnimationClip.cs
@@ -505,6 +505,7 @@ internal enum AnimationClip {
///
AirSphereAttack,
AirSphereEnd,
+ AirSphereRefresh,
///
/// Sharpdart
///
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index 0d3bc0a2..fc79a3e7 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using SSMP.Animation.Effects;
+using SSMP.Animation.Effects.SilkSkills;
using SSMP.Collection;
using SSMP.Fsm;
using SSMP.Game;
@@ -385,6 +386,7 @@ internal class AnimationManager {
{ "AirSphere Attack", AnimationClip.AirSphereAttack },
{ "AirSphere End", AnimationClip.AirSphereEnd },
+ { "AirSphere Refresh", AnimationClip.AirSphereRefresh },
{ "Silk Charge Antic", AnimationClip.SilkChargeAntic },
{ "Silk Charge", AnimationClip.SilkCharge },
@@ -655,13 +657,15 @@ internal class AnimationManager {
{ AnimationClip.BindChargeHealBurst, BindBurst.Instance },
{ AnimationClip.BindBurstAir, BindBurst.Instance },
{ AnimationClip.RageBindBurst, BindBurst.Instance },
- { AnimationClip.Death, new Death() }
+ { AnimationClip.Death, new Death() },
+ { AnimationClip.AirSphereAttack, new ThreadStorm() }
};
private static readonly Dictionary SubAnimationEffects = new() {
{ AnimationClip.WitchTentacles, BindBurst.Instance },
{ AnimationClip.ShamanCancel, new Bind { BindState = Bind.State.ShamanCancel } },
- { AnimationClip.BindInterrupt, BindInterrupt.Instance }
+ { AnimationClip.BindInterrupt, BindInterrupt.Instance },
+ { AnimationClip.AirSphereRefresh, new ThreadStorm() },
};
///
@@ -767,9 +771,9 @@ public void RegisterHooks() {
EventHooks.HeroControllerDie += OnDeath;
// Register FSM hooks for certain bind actions
- HeroController.OnHeroInstanceSet += CreateBindHooks;
+ HeroController.OnHeroInstanceSet += CreateHeroHooksHooks;
if (HeroController.SilentInstance != null) {
- CreateBindHooks(HeroController.instance);
+ CreateHeroHooksHooks(HeroController.instance);
}
@@ -794,7 +798,7 @@ public void RegisterHooks() {
public void DeregisterHooks() {
SceneManager.activeSceneChanged -= OnSceneChange;
- HeroController.OnHeroInstanceSet -= CreateBindHooks;
+ HeroController.OnHeroInstanceSet -= CreateHeroHooksHooks;
FsmStateActionInjector.UninjectAll();
// On.HeroAnimationController.Play -= HeroAnimationControllerOnPlay;
// On.HeroAnimationController.PlayFromFrame -= HeroAnimationControllerOnPlayFromFrame;
@@ -1049,7 +1053,7 @@ private void OnAnimationEvent(tk2dSpriteAnimationClip clip) {
/// Creates hooks for the Witch Tentacles and Shaman Cancel states in
/// the Bind fsm once the HeroController is ready.
///
- private void CreateBindHooks(HeroController hc) {
+ private void CreateHeroHooksHooks(HeroController hc) {
// Initialize warding bell FSM if it isn't already.
// This fills it in with the template
var bellFsm = HeroController.instance.bellBindFSM;
@@ -1075,6 +1079,17 @@ private void CreateBindHooks(HeroController hc) {
var bindInterrupt = bindFsm.GetState("Remove Silk?");
FsmStateActionInjector.Inject(bindInterrupt, OnBindInterrupt, 2);
+
+
+ // Silk skill injections
+ var silkSkillFsm = hc.silkSpecialFSM;
+ if (silkSkillFsm == null) {
+ Logger.Warn("Unable to find Silk Skill FSM to hook.");
+ return;
+ }
+
+ var threadStormExtend = silkSkillFsm.GetState("Extend");
+ FsmStateActionInjector.Inject(threadStormExtend, OnThreadStormExtend, 0);
}
///
@@ -1106,6 +1121,20 @@ private void OnBindInterrupt(PlayMakerFSM fsm) {
OnAnimationEvent(dummyClip);
}
+ private void OnThreadStormExtend(PlayMakerFSM fsm) {
+ //var dummyClip = new tk2dSpriteAnimationClip();
+ //dummyClip.name = "AirSphere Attack";
+ //dummyClip.wrapMode = tk2dSpriteAnimationClip.WrapMode.Once;
+ //if (_lastAnimationClip == dummyClip.name) {
+ // dummyClip.name = "AirSphere Attack";
+ //}
+ //OnAnimationEvent(dummyClip);
+
+ var effectInfo = ThreadStorm.GetEffectFlags();
+ _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.AirSphereAttack, 0, effectInfo);
+
+ }
+
// ///
// /// Callback method on the HeroAnimationController#Play method.
// ///
diff --git a/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs b/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
new file mode 100644
index 00000000..78911339
--- /dev/null
+++ b/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
@@ -0,0 +1,60 @@
+using System.Diagnostics.CodeAnalysis;
+using SSMP.Internals;
+using SSMP.Util;
+using UnityEngine;
+using Logger = SSMP.Logging.Logger;
+using Object = UnityEngine.Object;
+
+namespace SSMP.Animation.Effects.SilkSkills;
+
+internal abstract class BaseSilkSkill : DamageAnimationEffect {
+
+ private const string SilkSkillsObjectName = "Special Attacks";
+ private static GameObject? _localSilkAttacks;
+
+ public override byte[]? GetEffectInfo() {
+ return null;
+ }
+
+ public abstract override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo);
+
+ protected static PlayMakerFSM GetSkillFSM() {
+ var fsm = HeroController.instance.silkSpecialFSM;
+ if (fsm == null) {
+ throw new System.Exception("Unable to obtain Silk Skill FSM");
+ }
+
+ if (!fsm.Fsm.Initialized) {
+ fsm.Init();
+ }
+
+ return fsm;
+ }
+
+ protected static bool TryGetLocalSilkAttacks([MaybeNullWhen(false)] out GameObject localSilkAttacks) {
+ // Find local silk skills
+ if (_localSilkAttacks == null) {
+ _localSilkAttacks = HeroController.instance.gameObject.FindGameObjectInChildren(SilkSkillsObjectName);
+ if (_localSilkAttacks == null) {
+ Logger.Warn("Unable to find local Silk Silks object");
+ localSilkAttacks = null;
+ return false;
+ }
+ }
+
+ // Find existing attacks
+ localSilkAttacks = _localSilkAttacks;
+ return true;
+ }
+
+ protected static GameObject TryGetPlayerSilkAttacks(GameObject playerObject) {
+ var silkAttacks = playerObject.FindGameObjectInChildren(SilkSkillsObjectName);
+ if (silkAttacks == null) {
+ silkAttacks = new GameObject(SilkSkillsObjectName);
+ silkAttacks.transform.SetParentReset(playerObject.transform);
+ }
+
+ return silkAttacks;
+ }
+
+}
diff --git a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
new file mode 100644
index 00000000..24533185
--- /dev/null
+++ b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
@@ -0,0 +1,257 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading;
+using GlobalSettings;
+using HutongGames.PlayMaker;
+using HutongGames.PlayMaker.Actions;
+using Org.BouncyCastle.Bcpg;
+using SSMP.Internals;
+using SSMP.Util;
+using UnityEngine;
+using Logger = SSMP.Logging.Logger;
+using Object = UnityEngine.Object;
+
+namespace SSMP.Animation.Effects.SilkSkills;
+
+internal class ThreadStorm : BaseSilkSkill {
+ private const string SkillObjectName = "Sphere Ball";
+
+ private static GameObject? _localThreadStorm;
+
+ private static Dictionary _playerExtensions = new();
+
+ public static byte[] GetEffectFlags() {
+ var voltFilament = ToolItemManager.GetToolByName("Zap Imbuement");
+
+ return new byte[] {
+ (byte)(voltFilament.IsEquipped ? 1 : 0)
+ };
+ }
+
+ public override byte[]? GetEffectInfo() {
+ return GetEffectFlags();
+ }
+
+ public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ var volt = effectInfo is [1];
+ var isShaman = crestType == CrestType.Shaman;
+
+ // Update number of extensions
+ var playerId = playerObject.GetInstanceID();
+ var extensions = _playerExtensions.GetValueOrDefault(playerId, 0);
+ _playerExtensions[playerId] = extensions + 1;
+
+ // Play extension if applicable
+ if (extensions > 0) {
+ Logger.Info("Playing extension");
+ MonoBehaviourUtil.Instance.StartCoroutine(PlayStormExtension(playerObject));
+ return;
+ }
+
+ // Otherwise play the setup animation
+ MonoBehaviourUtil.Instance.StartCoroutine(PlayStormSetup(playerObject, volt, isShaman));
+ }
+
+ private IEnumerator PlayStormExtension(GameObject playerObject) {
+ if (!TryGetThreadStorm(playerObject, out var threadStorm)) {
+ yield break;
+ }
+
+ // The animation is kinda broken as it replays for each extension. Keep this unless we use a dedicated packet
+ var playerAnimator = playerObject.GetComponent();
+ if (playerAnimator) {
+ playerAnimator.Stop();
+ }
+
+ // Restart animation
+ var animator = threadStorm.GetComponentInChildren();
+ if (animator == null) {
+ ForceStop(threadStorm);
+ yield break;
+ }
+
+ //animator.PlayFromFrame("AirSphere", 0);
+
+ // Scale up
+ var curveScale = threadStorm.GetComponent();
+ if (curveScale != null) {
+ curveScale.enabled = false;
+ curveScale.enabled = true;
+ curveScale.StartAnimation();
+ }
+
+ yield return new WaitForSeconds(0.65f);
+
+ AttemptStop(playerObject, threadStorm);
+ }
+
+ private IEnumerator PlayStormSetup(GameObject playerObject, bool volt, bool isShaman) {
+ if (!TryGetThreadStorm(playerObject, out var threadStorm)) {
+ yield break;
+ }
+
+ threadStorm.SetActive(true);
+
+ // Set volt filament effect
+ var voltObject = threadStorm
+ .FindGameObjectInChildren("Ball")?
+ .FindGameObjectInChildren("thread_sphere_effect_zap");
+
+ if (voltObject) {
+ voltObject.SetActive(false);
+ voltObject.SetActive(volt);
+ }
+
+ // Enable shaman crest effect
+ var shamanRune = threadStorm.FindGameObjectInChildren("Shaman Rune");
+ if (shamanRune) {
+ shamanRune.SetActive(isShaman);
+ }
+
+ // Play antic noise
+ var fsm = GetSkillFSM();
+ var anticAudio = fsm.GetAction("A Sphere Antic", 2);
+ if (anticAudio != null) {
+ AudioUtil.PlayAudio(anticAudio, playerObject);
+ }
+
+ // Set the damager
+ var damager = threadStorm.FindGameObjectInChildren("Ball");
+ if (damager) {
+ SetDamageHeroState(damager, 1);
+ damager.SetActive(true);
+ } else {
+ Logger.Warn("Unable to set damager for Thread Storm");
+ }
+
+ // Play silk audio
+ var audio = threadStorm.GetComponent();
+ audio.Play();
+
+ // Play the effect
+ MonoBehaviourUtil.Instance.StartCoroutine(PlayStormExtension(playerObject));
+ }
+
+ private static void AttemptStop(GameObject playerObject, GameObject threadStorm) {
+ var playerId = playerObject.GetInstanceID();
+ var extensions = _playerExtensions.GetValueOrDefault(playerId, 1);
+ _playerExtensions[playerId] = Mathf.Max(0, extensions - 1);
+
+ if (extensions > 1) {
+ return;
+ }
+
+ ForceStop(threadStorm);
+
+ // Play ending audio
+ var fsm = GetSkillFSM();
+ var endAudio = fsm.GetFirstAction("A Sphere End");
+ AudioUtil.PlayAudio(endAudio, playerObject);
+ }
+
+ private static void ForceStop(GameObject threadStorm) {
+ var audio = threadStorm.GetComponent();
+ audio.Stop();
+ threadStorm.SetActive(false);
+
+ }
+
+ private static bool TryGetThreadStorm(
+ GameObject playerObject,
+ [MaybeNullWhen(false)] out GameObject threadStorm
+ ) {
+ // Find existing thread storm
+ var parent = TryGetPlayerSilkAttacks(playerObject);
+ threadStorm = parent.FindGameObjectInChildren(SkillObjectName);
+ if (threadStorm) {
+ return true;
+ }
+
+ // Not found, locate it on the player
+ var localStorm = _localThreadStorm;
+ if (localStorm == null) {
+ // Get local silk attacks
+ if (!TryGetLocalSilkAttacks(out var localSilkAttacks)) {
+ return false;
+ }
+
+ // Find the thread storm
+ localStorm = localSilkAttacks.FindGameObjectInChildren(SkillObjectName);
+ if (localStorm == null) {
+ Logger.Warn("Unable to get local Thread Storm object");
+ return false;
+ }
+
+ _localThreadStorm = localStorm;
+ }
+
+ // Copy to the player object
+ threadStorm = Object.Instantiate(localStorm, parent.transform);
+ threadStorm.name = SkillObjectName;
+
+ // Remove FSM
+ if (threadStorm.TryGetComponent(out var fsm)) {
+ Object.Destroy(fsm);
+ }
+
+ // Remove shaman manager
+ if (threadStorm.TryGetComponent(out var globalRuneEffect)) {
+ Object.DestroyImmediate(globalRuneEffect);
+ }
+
+ if (threadStorm.TryGetComponent(out var checker)) {
+ Object.DestroyImmediate(checker);
+ }
+
+ if (threadStorm.TryGetComponent(out var register)) {
+ Object.DestroyImmediate(register);
+ }
+
+
+ // Set up shaman crest effects
+ var shamanRune = threadStorm.FindGameObjectInChildren("Shaman Rune");
+ if (shamanRune) {
+ if (shamanRune.TryGetComponent(out var runeEffect)) {
+ // Copy particles
+ var preParticles = runeEffect.runeSpawnEffect;
+ if (preParticles != null) {
+ var postParticles = EffectUtils.SpawnGlobalPoolObject(preParticles, shamanRune.transform, 0, true);
+ if (postParticles) {
+ postParticles.transform.localPosition = Vector3.zero;
+ postParticles.transform.localScale = new Vector3(3.5f, 3.5f, 1);
+ }
+ }
+
+ // Remove shaman manager
+ Object.DestroyImmediate(runeEffect);
+ }
+
+ var bloom = shamanRune.FindGameObjectInChildren("Shaman Rune Camera Bloom");
+ if (bloom) Object.DestroyImmediate(bloom);
+ }
+
+ // Set up scale animation. It plays when enabled.
+ var curveScale = threadStorm.AddComponent();
+ curveScale.duration = 0.3f;
+ curveScale.playOnEnable = false;
+ curveScale.curve = new([
+ new Keyframe(0, 1),
+ new Keyframe(0.5f, 2f),
+ new Keyframe(1, 1),
+ ]);
+ curveScale.OnStart = new UnityEngine.Events.UnityEvent();
+ curveScale.OnStop = new UnityEngine.Events.UnityEvent();
+ curveScale.framerate = 30;
+ curveScale.isRealtime = true;
+ curveScale.playOnEnable = true;
+ curveScale.enabled = false;
+ curveScale.offset = new Vector3(0.1f, 0.1f, 0.1f);
+
+
+ threadStorm.SetActive(true);
+
+ return true;
+ }
+}
diff --git a/SSMP/Util/AudioUtil.cs b/SSMP/Util/AudioUtil.cs
index 7abc6431..3d12b741 100644
--- a/SSMP/Util/AudioUtil.cs
+++ b/SSMP/Util/AudioUtil.cs
@@ -122,6 +122,33 @@ GameObject playerObject
}
}
+ ///
+ /// Play a random audio clip from the given FSM action positionally at the given player object's position.
+ ///
+ /// The action instance from an FSM.
+ /// The player object to play the audio at.
+ public static void PlayAudio(
+ PlayRandomAudioClipTable playAudioClip,
+ GameObject playerObject
+ ) {
+ var audioClipTable = playAudioClip.Table.value as RandomAudioClipTable;
+ if (audioClipTable == null) {
+ Logger.Warn("Audio clip table for PlayRandomAudioClipTableV2 is null");
+ return;
+ }
+
+ var position = playerObject.transform.position;
+
+ if (playAudioClip.AudioPlayerPrefab.Value) {
+ audioClipTable.SpawnAndPlayOneShot(
+ playAudioClip.AudioPlayerPrefab.value as AudioSource,
+ position
+ );
+ } else {
+ audioClipTable.SpawnAndPlayOneShot(position);
+ }
+ }
+
///
/// Play the audio from a action relative to the given player object.
///
@@ -143,4 +170,26 @@ GameObject playerObject
audioSource.PlayOneShot(clip);
}
+
+ ///
+ /// Play the given audio event positionally at the given player object's position with the given audio clip.
+ /// And destroy it after it is done playing.
+ ///
+ /// The audio clip to play.
+ /// The player object to play the audio at.
+ public static void PlayAudio(
+ AudioClip audioClip,
+ GameObject playerObject
+ ) {
+ var audioEvent = new AudioEvent {
+ Clip = audioClip,
+ PitchMin = 1,
+ PitchMax = 1,
+ Volume = 1
+ };
+
+ var position = playerObject.transform.position;
+
+ audioEvent.SpawnAndPlayOneShot(position);
+ }
}
From 57e075061c441488e1176b9e3e76f73bd3eac42d Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Mon, 13 Apr 2026 01:10:14 -0400
Subject: [PATCH 02/41] finalize logic and components for thread storm
---
SSMP/Animation/AnimationManager.cs | 2 +-
.../Effects/SilkSkills/ThreadStorm.cs | 55 ++++++++++++-------
2 files changed, 36 insertions(+), 21 deletions(-)
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index fc79a3e7..2676ed6c 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -1131,7 +1131,7 @@ private void OnThreadStormExtend(PlayMakerFSM fsm) {
//OnAnimationEvent(dummyClip);
var effectInfo = ThreadStorm.GetEffectFlags();
- _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.AirSphereAttack, 0, effectInfo);
+ _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.AirSphereRefresh, 0, effectInfo);
}
diff --git a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
index 24533185..5e16ee84 100644
--- a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
+++ b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
@@ -1,12 +1,7 @@
-using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
-using System.Threading;
-using GlobalSettings;
-using HutongGames.PlayMaker;
using HutongGames.PlayMaker.Actions;
-using Org.BouncyCastle.Bcpg;
using SSMP.Internals;
using SSMP.Util;
using UnityEngine;
@@ -54,17 +49,15 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
MonoBehaviourUtil.Instance.StartCoroutine(PlayStormSetup(playerObject, volt, isShaman));
}
+ ///
+ /// Plays the main loop of the Thread Storm attack
+ ///
+ /// The player object that used the attack
private IEnumerator PlayStormExtension(GameObject playerObject) {
if (!TryGetThreadStorm(playerObject, out var threadStorm)) {
yield break;
}
- // The animation is kinda broken as it replays for each extension. Keep this unless we use a dedicated packet
- var playerAnimator = playerObject.GetComponent();
- if (playerAnimator) {
- playerAnimator.Stop();
- }
-
// Restart animation
var animator = threadStorm.GetComponentInChildren();
if (animator == null) {
@@ -72,7 +65,7 @@ private IEnumerator PlayStormExtension(GameObject playerObject) {
yield break;
}
- //animator.PlayFromFrame("AirSphere", 0);
+ animator.PlayFromFrame("AirSphere", 0);
// Scale up
var curveScale = threadStorm.GetComponent();
@@ -87,6 +80,12 @@ private IEnumerator PlayStormExtension(GameObject playerObject) {
AttemptStop(playerObject, threadStorm);
}
+ ///
+ /// Initializes and activates the Thread Storm attack, setting up sub-effects
+ ///
+ /// The player object that used the attack.
+ /// Determines if the volt filament effect should be enabled.
+ /// Determines if the shaman crest effect should be displayed.
private IEnumerator PlayStormSetup(GameObject playerObject, bool volt, bool isShaman) {
if (!TryGetThreadStorm(playerObject, out var threadStorm)) {
yield break;
@@ -95,9 +94,8 @@ private IEnumerator PlayStormSetup(GameObject playerObject, bool volt, bool isSh
threadStorm.SetActive(true);
// Set volt filament effect
- var voltObject = threadStorm
- .FindGameObjectInChildren("Ball")?
- .FindGameObjectInChildren("thread_sphere_effect_zap");
+ var damager = threadStorm.FindGameObjectInChildren("Ball");
+ var voltObject = damager?.FindGameObjectInChildren("thread_sphere_effect_zap");
if (voltObject) {
voltObject.SetActive(false);
@@ -118,7 +116,7 @@ private IEnumerator PlayStormSetup(GameObject playerObject, bool volt, bool isSh
}
// Set the damager
- var damager = threadStorm.FindGameObjectInChildren("Ball");
+
if (damager) {
SetDamageHeroState(damager, 1);
damager.SetActive(true);
@@ -126,23 +124,31 @@ private IEnumerator PlayStormSetup(GameObject playerObject, bool volt, bool isSh
Logger.Warn("Unable to set damager for Thread Storm");
}
- // Play silk audio
+ // Play looping silk audio
var audio = threadStorm.GetComponent();
audio.Play();
- // Play the effect
+ // Play the main effect
MonoBehaviourUtil.Instance.StartCoroutine(PlayStormExtension(playerObject));
}
+ ///
+ /// Stops the effect if no more extensions have been received
+ ///
+ /// The player object that used the attack
+ /// The thread storm effect object
private static void AttemptStop(GameObject playerObject, GameObject threadStorm) {
+ // Decrement extension count
var playerId = playerObject.GetInstanceID();
var extensions = _playerExtensions.GetValueOrDefault(playerId, 1);
_playerExtensions[playerId] = Mathf.Max(0, extensions - 1);
+ // There are more extensions, don't deactivate yet
if (extensions > 1) {
return;
}
+ // Stop the effect
ForceStop(threadStorm);
// Play ending audio
@@ -151,6 +157,10 @@ private static void AttemptStop(GameObject playerObject, GameObject threadStorm)
AudioUtil.PlayAudio(endAudio, playerObject);
}
+ ///
+ /// Forces the thread storm to stop
+ ///
+ /// The thread storm effect's object
private static void ForceStop(GameObject threadStorm) {
var audio = threadStorm.GetComponent();
audio.Stop();
@@ -158,6 +168,12 @@ private static void ForceStop(GameObject threadStorm) {
}
+ ///
+ /// Gets or creates the Thread Storm effect
+ ///
+ /// The object of the player that used the attack
+ /// The found or created Thread Storm effect object
+ /// false if threadStorm could not be created
private static bool TryGetThreadStorm(
GameObject playerObject,
[MaybeNullWhen(false)] out GameObject threadStorm
@@ -196,7 +212,7 @@ private static bool TryGetThreadStorm(
Object.Destroy(fsm);
}
- // Remove shaman manager
+ // Remove components that could interfere
if (threadStorm.TryGetComponent(out var globalRuneEffect)) {
Object.DestroyImmediate(globalRuneEffect);
}
@@ -249,7 +265,6 @@ private static bool TryGetThreadStorm(
curveScale.enabled = false;
curveScale.offset = new Vector3(0.1f, 0.1f, 0.1f);
-
threadStorm.SetActive(true);
return true;
From c8ccfe5c4c762eabc1e7ad6af524359cf3f848c4 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Mon, 13 Apr 2026 02:08:22 -0400
Subject: [PATCH 03/41] Add silk spear
---
SSMP/Animation/AnimationManager.cs | 3 +-
.../Effects/SilkSkills/BaseSilkSkill.cs | 16 +-
.../Animation/Effects/SilkSkills/SilkSpear.cs | 148 ++++++++++++++++++
.../Effects/SilkSkills/ThreadStorm.cs | 16 +-
4 files changed, 166 insertions(+), 17 deletions(-)
create mode 100644 SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index 2676ed6c..51ab0241 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -658,7 +658,8 @@ internal class AnimationManager {
{ AnimationClip.BindBurstAir, BindBurst.Instance },
{ AnimationClip.RageBindBurst, BindBurst.Instance },
{ AnimationClip.Death, new Death() },
- { AnimationClip.AirSphereAttack, new ThreadStorm() }
+ { AnimationClip.NeedleThrowThrowing, new SilkSpear() },
+ { AnimationClip.AirSphereAttack, new ThreadStorm() },
};
private static readonly Dictionary SubAnimationEffects = new() {
diff --git a/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs b/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
index 78911339..f37e0ca1 100644
--- a/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
+++ b/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
@@ -12,8 +12,20 @@ internal abstract class BaseSilkSkill : DamageAnimationEffect {
private const string SilkSkillsObjectName = "Special Attacks";
private static GameObject? _localSilkAttacks;
+ public static byte[] GetEffectFlags() {
+ var voltFilament = ToolItemManager.GetToolByName("Zap Imbuement");
+
+ return new byte[] {
+ (byte)(voltFilament.IsEquipped ? 1 : 0)
+ };
+ }
+
public override byte[]? GetEffectInfo() {
- return null;
+ return GetEffectFlags();
+ }
+
+ protected bool IsVolt(byte[]? effectInfo) {
+ return effectInfo is [1];
}
public abstract override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo);
@@ -47,7 +59,7 @@ protected static bool TryGetLocalSilkAttacks([MaybeNullWhen(false)] out GameObje
return true;
}
- protected static GameObject TryGetPlayerSilkAttacks(GameObject playerObject) {
+ protected static GameObject GetPlayerSilkAttacks(GameObject playerObject) {
var silkAttacks = playerObject.FindGameObjectInChildren(SilkSkillsObjectName);
if (silkAttacks == null) {
silkAttacks = new GameObject(SilkSkillsObjectName);
diff --git a/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs b/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
new file mode 100644
index 00000000..57773eeb
--- /dev/null
+++ b/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
@@ -0,0 +1,148 @@
+using HutongGames.PlayMaker.Actions;
+using SSMP.Internals;
+using SSMP.Util;
+using UnityEngine;
+using Logger = SSMP.Logging.Logger;
+using Object = UnityEngine.Object;
+
+namespace SSMP.Animation.Effects.SilkSkills;
+
+internal class SilkSpear : BaseSilkSkill {
+ private const string SpearObjectName = "Needle Throw";
+ public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ var spear = GetSilkSpear(playerObject);
+ if (!spear) return;
+
+ var parent = spear.FindGameObjectInChildren("needle_throw_simple");
+ if (!parent) return;
+
+
+ // Set volt settings
+ var volt = IsVolt(effectInfo);
+
+ var zapThread = parent
+ .FindGameObjectInChildren("thread")?
+ .FindGameObjectInChildren("zap thread");
+
+ if (zapThread) zapThread.SetActive(volt);
+
+ var needle = parent.FindGameObjectInChildren("needle");
+
+ var zapNeedle = needle?.FindGameObjectInChildren("Zap Effect Activator");
+ if (zapNeedle) {
+ zapNeedle.SetActive(volt);
+ zapNeedle.SetActiveChildren(volt);
+ }
+
+ // Set shaman settings
+ var isShaman = crestType == CrestType.Shaman;
+
+ var shamanParent = parent.FindGameObjectInChildren("Rune Effect Activator");
+ if (shamanParent) {
+ shamanParent.SetActive(isShaman);
+
+ var shamanRune = shamanParent.FindGameObjectInChildren("Shaman Rune");
+ if (shamanRune) {
+ shamanRune.SetActive(isShaman);
+
+ var zapRune = shamanRune.FindGameObjectInChildren("Zap Rune");
+ if (zapRune) zapRune.SetActive(volt);
+ }
+ }
+
+ // Set damager
+ var damager = needle?.FindGameObjectInChildren("Needle Damage");
+ if (damager) {
+ SetDamageHeroState(damager, 1);
+ } else {
+ Logger.Warn("Unable to set damager for Silk Spear");
+ }
+
+ // Enable spear
+ spear.SetActive(false);
+ spear.SetActive(true);
+
+ // Play audio
+ var fsm = GetSkillFSM();
+
+ var anticAudio = fsm.GetAction("A Sphere Antic", 2);
+ if (anticAudio != null) AudioUtil.PlayAudio(anticAudio, playerObject);
+
+ var throwAudio = fsm.GetAction("Start Throw", 1);
+ if (throwAudio != null) AudioUtil.PlayAudio(throwAudio, playerObject);
+
+ if (volt) {
+ var voltAudio = fsm.GetAction("Silkspear Zap FX", 1);
+ if (voltAudio != null) AudioUtil.PlayAudio(voltAudio, playerObject);
+ }
+ }
+
+ private static GameObject? GetSilkSpear(GameObject playerObject) {
+ // Find existing silk spear
+ var silkAttacks = GetPlayerSilkAttacks(playerObject);
+ var spear = silkAttacks.FindGameObjectInChildren(SpearObjectName);
+ if (spear) return spear;
+
+ // Find on own silk attacks
+ if (!TryGetLocalSilkAttacks(out var localSilkAttacks)) {
+ return null;
+ }
+
+ var localSpear = localSilkAttacks.FindGameObjectInChildren(SpearObjectName);
+ if (localSpear == null) {
+ return null;
+ }
+
+ // Create new spear
+ spear = Object.Instantiate(localSpear, silkAttacks.transform);
+ spear.name = SpearObjectName;
+
+ if (spear.TryGetComponent(out var toolChecker)) {
+ Object.DestroyImmediate(toolChecker);
+ }
+
+
+ // Remove potentially disruptive components
+ var runes = spear.GetComponentsInChildren();
+ foreach (var rune in runes) {
+ Object.DestroyImmediate(rune);
+ }
+
+ var camFollows = spear.GetComponentsInChildren();
+ foreach (var camFollow in camFollows) {
+ Object.Destroy(camFollow);
+ }
+
+ var child = spear.FindGameObjectInChildren("needle_throw_simple");
+ if (child) {
+ if (child.TryGetComponent(out var cam)) {
+ Object.DestroyImmediate(cam);
+ }
+
+ if (child.TryGetComponent(out var anim)) {
+ Object.DestroyImmediate(anim);
+ }
+
+ var bloom1 = child
+ .FindGameObjectInChildren("Rune Effect Activator")?
+ .FindGameObjectInChildren("Shaman Rune")?
+ .FindGameObjectInChildren("Rune Effect")?
+ .FindGameObjectInChildren("Shaman Rune")?
+ .FindGameObjectInChildren("Shaman Rune Camera Bloom");
+
+ var bloom2 = child
+ .FindGameObjectInChildren("Rune Effect Activator")?
+ .FindGameObjectInChildren("Shaman Rune")?
+ .FindGameObjectInChildren("Zap Rune")?
+ .FindGameObjectInChildren("Rune Effect")?
+ .FindGameObjectInChildren("Shaman Rune")?
+ .FindGameObjectInChildren("Shaman Rune Camera Bloom");
+
+ if (bloom1) {
+ Object.Destroy(bloom1);
+ }
+ }
+
+ return spear;
+ }
+}
diff --git a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
index 5e16ee84..3daa6a81 100644
--- a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
+++ b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
@@ -17,20 +17,8 @@ internal class ThreadStorm : BaseSilkSkill {
private static Dictionary _playerExtensions = new();
- public static byte[] GetEffectFlags() {
- var voltFilament = ToolItemManager.GetToolByName("Zap Imbuement");
-
- return new byte[] {
- (byte)(voltFilament.IsEquipped ? 1 : 0)
- };
- }
-
- public override byte[]? GetEffectInfo() {
- return GetEffectFlags();
- }
-
public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
- var volt = effectInfo is [1];
+ var volt = IsVolt(effectInfo);
var isShaman = crestType == CrestType.Shaman;
// Update number of extensions
@@ -179,7 +167,7 @@ private static bool TryGetThreadStorm(
[MaybeNullWhen(false)] out GameObject threadStorm
) {
// Find existing thread storm
- var parent = TryGetPlayerSilkAttacks(playerObject);
+ var parent = GetPlayerSilkAttacks(playerObject);
threadStorm = parent.FindGameObjectInChildren(SkillObjectName);
if (threadStorm) {
return true;
From b8b1b64fe7777679981dc2fdb2c2989da3bbbd8a Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Tue, 14 Apr 2026 00:40:28 -0400
Subject: [PATCH 04/41] sharp dart
---
SSMP/Animation/AnimationManager.cs | 2 +
.../Effects/SilkSkills/BaseSilkSkill.cs | 9 +
.../Animation/Effects/SilkSkills/SharpDart.cs | 171 ++++++++++++++++++
.../Animation/Effects/SilkSkills/SilkSpear.cs | 4 +-
.../Effects/SilkSkills/ThreadStorm.cs | 7 +-
5 files changed, 184 insertions(+), 9 deletions(-)
create mode 100644 SSMP/Animation/Effects/SilkSkills/SharpDart.cs
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index 51ab0241..f23c5d3f 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -660,6 +660,8 @@ internal class AnimationManager {
{ AnimationClip.Death, new Death() },
{ AnimationClip.NeedleThrowThrowing, new SilkSpear() },
{ AnimationClip.AirSphereAttack, new ThreadStorm() },
+ { AnimationClip.SilkCharge, new SharpDart() },
+ { AnimationClip.SilkChargeZap, new SharpDart { Volt = true } },
};
private static readonly Dictionary SubAnimationEffects = new() {
diff --git a/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs b/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
index f37e0ca1..a7a5ac55 100644
--- a/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
+++ b/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
+using HutongGames.PlayMaker.Actions;
using SSMP.Internals;
using SSMP.Util;
using UnityEngine;
@@ -69,4 +70,12 @@ protected static GameObject GetPlayerSilkAttacks(GameObject playerObject) {
return silkAttacks;
}
+ protected static void PlayHornetAttackSound(GameObject playerObject) {
+ var fsm = GetSkillFSM();
+ var anticAudio = fsm.GetAction("A Sphere Antic", 2);
+ if (anticAudio != null) {
+ AudioUtil.PlayAudio(anticAudio, playerObject);
+ }
+ }
+
}
diff --git a/SSMP/Animation/Effects/SilkSkills/SharpDart.cs b/SSMP/Animation/Effects/SilkSkills/SharpDart.cs
new file mode 100644
index 00000000..84249cd9
--- /dev/null
+++ b/SSMP/Animation/Effects/SilkSkills/SharpDart.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+using SSMP.Internals;
+using SSMP.Util;
+using UnityEngine;
+using Logger = SSMP.Logging.Logger;
+using Object = UnityEngine.Object;
+
+namespace SSMP.Animation.Effects.SilkSkills;
+
+internal class SharpDart : BaseSilkSkill {
+ private const string DashBurstName = "Silk Charge DashBurst";
+ private const string ZapParticlesName = "Silk Charge Particles Zap";
+ private const string DashDamagerName = "Silk Charge Damager";
+
+ public bool Volt = false;
+
+ public override void Play(GameObject playerObject, CrestType crestType, byte[] effectInfo) {
+ var isShaman = crestType == CrestType.Shaman;
+ MonoBehaviourUtil.Instance.StartCoroutine(Play(playerObject, isShaman));
+ }
+
+ private IEnumerator Play(GameObject playerObject, bool isShaman) {
+ if (TryGetDamager(playerObject, out var damager)) {
+ SetDamageHeroState(damager);
+ damager.SetActive(true);
+
+ var rune = damager.FindGameObjectInChildren("Shaman Rune");
+ if (rune) {
+ rune.SetActive(false);
+ rune.SetActive(isShaman);
+ }
+ }
+
+ if (TryGetDashBurst(playerObject, out var dashBurst)) {
+ dashBurst.SetActive(false);
+ dashBurst.SetActive(true);
+ }
+
+ if (Volt && TryGetParticles(playerObject, out var particles)) {
+ particles.SetActive(false);
+ particles.SetActive(true);
+ }
+
+ // Play sound effects
+ PlayHornetAttackSound(playerObject);
+
+ var fsm = GetSkillFSM();
+ var chargeAntic = fsm.GetAction("Silk Charge Begin", 5);
+ if (chargeAntic != null) AudioUtil.PlayAudio(chargeAntic, playerObject);
+
+ if (Volt) {
+ var voltNoise = fsm.GetFirstAction("Silk Charge Zap FX");
+ if (voltNoise != null) AudioUtil.PlayAudio(voltNoise, playerObject);
+ }
+
+ // Brief pause for ending sound
+ yield return new WaitForSeconds(0.2f);
+
+ var chargeFull = fsm.GetAction("Silk Charge Start", 12);
+ if (chargeFull != null) AudioUtil.PlayAudio(chargeFull, playerObject);
+ }
+
+ private bool TryGetDashBurst(GameObject playerObject, [MaybeNullWhen(false)] out GameObject dashBurst) {
+ // Find existing object
+ var attacks = GetPlayerSilkAttacks(playerObject);
+ dashBurst = attacks.FindGameObjectInChildren(DashBurstName);
+ if (dashBurst) {
+ return true;
+ }
+
+ // Copy from local attacks
+ if (!TryGetLocalSilkAttacks(out var localSilkAttacks)) {
+ return false;
+ }
+
+ var localDashBurst = localSilkAttacks.FindGameObjectInChildren(DashBurstName);
+ if (!localDashBurst) {
+ return false;
+ }
+
+ dashBurst = Object.Instantiate(localDashBurst, attacks.transform);
+ dashBurst.name = DashBurstName;
+ dashBurst.transform.localScale = new Vector3(0.75f, 0.75f, 0.75f);
+
+ return true;
+ }
+
+ private bool TryGetDamager(GameObject playerObject, [MaybeNullWhen(false)] out GameObject damager) {
+ // Find existing object
+ var attacks = GetPlayerSilkAttacks(playerObject);
+ damager = attacks.FindGameObjectInChildren(DashDamagerName);
+ if (damager) {
+ return true;
+ }
+
+ // Copy from local attacks
+ if (!TryGetLocalSilkAttacks(out var localSilkAttacks)) {
+ return false;
+ }
+
+ var localDamager = localSilkAttacks.FindGameObjectInChildren(DashDamagerName);
+ if (!localDamager) {
+ return false;
+ }
+
+ damager = Object.Instantiate(localDamager, attacks.transform);
+ damager.name = DashDamagerName;
+
+ var delay = damager.AddComponent();
+ delay.time = 0.3f;
+
+ var worm = damager.FindGameObjectInChildren("Worm Worrier");
+ if (worm) {
+ Object.Destroy(worm);
+ }
+
+ var runes = damager.GetComponentsInChildren();
+ foreach (var rune in runes) {
+ Object.DestroyImmediate(rune);
+ }
+
+ var runeBloom = damager
+ .FindGameObjectInChildren("Shaman Rune")?
+ .FindGameObjectInChildren("Shaman Rune Camera Bloom");
+
+ if (runeBloom) {
+ Object.DestroyImmediate(runeBloom);
+ }
+
+ return true;
+ }
+
+ private bool TryGetParticles(GameObject playerObject, [MaybeNullWhen(false)] out GameObject particles) {
+ var effects = playerObject.FindGameObjectInChildren("Effects");
+ if (!effects) {
+ particles = null;
+ return false;
+ }
+
+ particles = effects.FindGameObjectInChildren(ZapParticlesName);
+
+ if (particles) {
+ return true;
+ }
+
+ var localParticles = HeroController.instance.gameObject
+ .FindGameObjectInChildren("Effects")?
+ .FindGameObjectInChildren(ZapParticlesName);
+
+ if (!localParticles) {
+ return false;
+ }
+
+ particles = Object.Instantiate(localParticles, effects.transform);
+ particles.name = ZapParticlesName;
+
+ if (particles.TryGetComponent(out var system)) {
+ var emission = system.emission;
+ emission.enabled = true;
+ }
+
+ var delay = particles.AddComponent();
+ delay.time = 0.3f;
+
+ return true;
+ }
+}
diff --git a/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs b/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
index 57773eeb..82e1ee72 100644
--- a/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
+++ b/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
@@ -63,11 +63,9 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
spear.SetActive(true);
// Play audio
+ PlayHornetAttackSound(playerObject);
var fsm = GetSkillFSM();
- var anticAudio = fsm.GetAction("A Sphere Antic", 2);
- if (anticAudio != null) AudioUtil.PlayAudio(anticAudio, playerObject);
-
var throwAudio = fsm.GetAction("Start Throw", 1);
if (throwAudio != null) AudioUtil.PlayAudio(throwAudio, playerObject);
diff --git a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
index 3daa6a81..9ef2c8b6 100644
--- a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
+++ b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
@@ -97,14 +97,9 @@ private IEnumerator PlayStormSetup(GameObject playerObject, bool volt, bool isSh
}
// Play antic noise
- var fsm = GetSkillFSM();
- var anticAudio = fsm.GetAction("A Sphere Antic", 2);
- if (anticAudio != null) {
- AudioUtil.PlayAudio(anticAudio, playerObject);
- }
+ PlayHornetAttackSound(playerObject);
// Set the damager
-
if (damager) {
SetDamageHeroState(damager, 1);
damager.SetActive(true);
From 9f5982502a83031717cb520066b9948346cac463 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Wed, 15 Apr 2026 00:29:32 -0400
Subject: [PATCH 05/41] Add cross stitch
---
SSMP/Animation/AnimationEffect.cs | 10 +
SSMP/Animation/AnimationManager.cs | 5 +
SSMP/Animation/Effects/Death.cs | 5 +-
.../Effects/SilkSkills/BaseSilkSkill.cs | 22 ++
.../Effects/SilkSkills/CrossStitch.cs | 243 ++++++++++++++++++
.../Animation/Effects/SilkSkills/SharpDart.cs | 6 +-
SSMP/Util/AudioUtil.cs | 28 ++
7 files changed, 313 insertions(+), 6 deletions(-)
create mode 100644 SSMP/Animation/Effects/SilkSkills/CrossStitch.cs
diff --git a/SSMP/Animation/AnimationEffect.cs b/SSMP/Animation/AnimationEffect.cs
index 56e6960a..1c23c7cd 100644
--- a/SSMP/Animation/AnimationEffect.cs
+++ b/SSMP/Animation/AnimationEffect.cs
@@ -40,4 +40,14 @@ protected static void ChangeAttackDirection(GameObject targetObject, float direc
var directionVar = damageFsm.FsmVariables.GetFsmFloat("direction");
directionVar.Value = direction;
}
+
+ ///
+ /// "Hides" the player character by stopping its animation and setting its sprite to a small texture
+ ///
+ /// The player to be hidden.
+ protected static void HidePlayer(GameObject playerObject) {
+ // "hide" the player (assign a very small texture)
+ playerObject.GetComponent().Stop();
+ playerObject.GetComponent().SetSprite("wall_puff0004");
+ }
}
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index f23c5d3f..aea9e429 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -658,10 +658,15 @@ internal class AnimationManager {
{ AnimationClip.BindBurstAir, BindBurst.Instance },
{ AnimationClip.RageBindBurst, BindBurst.Instance },
{ AnimationClip.Death, new Death() },
+
+ // Silk Skills
{ AnimationClip.NeedleThrowThrowing, new SilkSpear() },
{ AnimationClip.AirSphereAttack, new ThreadStorm() },
{ AnimationClip.SilkCharge, new SharpDart() },
{ AnimationClip.SilkChargeZap, new SharpDart { Volt = true } },
+ { AnimationClip.ParryStance, CrossStitch.StartingInstance },
+ { AnimationClip.ParryStanceGround, CrossStitch.StartingInstance },
+ { AnimationClip.ParryClash, new CrossStitch() }
};
private static readonly Dictionary SubAnimationEffects = new() {
diff --git a/SSMP/Animation/Effects/Death.cs b/SSMP/Animation/Effects/Death.cs
index 4adef61b..3f726b25 100644
--- a/SSMP/Animation/Effects/Death.cs
+++ b/SSMP/Animation/Effects/Death.cs
@@ -101,9 +101,8 @@ private static IEnumerator PlayFrostDeath(GameObject playerObject) {
Object.DestroyImmediate(shaker);
}
- // "hide" the player (assign a very small texture)
- playerObject.GetComponent().Stop();
- playerObject.GetComponent().SetSprite("wall_puff0004");
+ // Hide the player. They're shown as a separate frosted object in the next step
+ HidePlayer(playerObject);
// Spawn the frosted hornet object
const int frostedDuration = 3;
diff --git a/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs b/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
index a7a5ac55..59818066 100644
--- a/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
+++ b/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
@@ -78,4 +78,26 @@ protected static void PlayHornetAttackSound(GameObject playerObject) {
}
}
+ protected static bool FindOrCreateAttack(GameObject playerObject, string name, out GameObject? attack) {
+ // Find existing object
+ var attacks = GetPlayerSilkAttacks(playerObject);
+ attack = attacks.FindGameObjectInChildren(name);
+ if (attack) {
+ return false;
+ }
+
+ // Copy from local attacks
+ if (!TryGetLocalSilkAttacks(out var localSilkAttacks)) {
+ return false;
+ }
+
+ var localClash = localSilkAttacks.FindGameObjectInChildren(name);
+ if (!localClash) {
+ return false;
+ }
+
+ attack = Object.Instantiate(localClash, attacks.transform);
+ attack.name = name;
+ return true;
+ }
}
diff --git a/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs b/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs
new file mode 100644
index 00000000..d3a1937d
--- /dev/null
+++ b/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs
@@ -0,0 +1,243 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using HutongGames.PlayMaker;
+using HutongGames.PlayMaker.Actions;
+using Mono.WebBrowser;
+using SSMP.Internals;
+using SSMP.Util;
+using UnityEngine;
+using Logger = SSMP.Logging.Logger;
+using Object = UnityEngine.Object;
+
+namespace SSMP.Animation.Effects.SilkSkills;
+
+internal class CrossStitch : BaseSilkSkill {
+ private const string ParryStanceFlashName = "Parry Stance Flash";
+ private const string ParryClashName = "Parry Clash Effect";
+ private const string ParrySlashName = "Parry Slash Effect";
+ private const string ParryZapSlashName = "Parry Slash Effect Zap";
+
+ public bool IsStarting = false;
+
+ public static CrossStitch StartingInstance = new() { IsStarting = true };
+
+ public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ var isShaman = crestType == CrestType.Shaman;
+ var isVolt = IsVolt(effectInfo);
+
+ // Determine which animation to play
+ if (IsStarting) {
+ PlayStart(playerObject, isShaman, isVolt);
+ return;
+ }
+
+ // Play the clash, then play the dash
+ MonoBehaviourUtil.Instance.StartCoroutine(PlayClash(playerObject, isShaman, isVolt));
+ }
+
+ private void PlayStart(GameObject playerObject, bool isShaman, bool isVolt) {
+ var fsm = GetSkillFSM();
+
+ // Play normal hornet noise
+ var normalAudio = fsm.GetFirstAction("Parry Start");
+ if (normalAudio != null) AudioUtil.PlayAudio(normalAudio, playerObject);
+
+ // Play stance audio
+ var stanceAudio = fsm.GetAction("Parry Start", 15);
+ if (stanceAudio != null) AudioUtil.PlayAudio(stanceAudio, playerObject);
+
+ // Enable thread (or zap thread)
+ if (TryGetParryThread(playerObject, isVolt, out var thread)) {
+ thread.SetActive(false);
+ thread.SetActive(true);
+ }
+
+ // Enable stance flash
+ if (TryGetStanceFlash(playerObject, out var flash)) {
+ flash.SetActiveChildren(isShaman);
+
+ flash.SetActive(false);
+ flash.SetActive(true);
+ }
+ }
+
+ private IEnumerator PlayClash(GameObject playerObject, bool isShaman, bool isVolt) {
+ // Play parry audio
+ var fsm = GetSkillFSM();
+ var clashAudio = fsm.GetFirstAction("Parry Clash");
+ if (clashAudio != null) AudioUtil.PlayAudio(clashAudio, playerObject);
+
+ // Activate clash object
+ if (TryGetClashEffect(playerObject, out var clash)) {
+ clash.SetActiveChildren(isShaman);
+
+ clash.SetActive(false);
+ clash.SetActive(true);
+ }
+
+ yield return new WaitForSeconds(0.5f);
+
+ // Play dash effect
+ MonoBehaviourUtil.Instance.StartCoroutine(PlayDash(playerObject, isShaman, isVolt));
+ }
+
+ private IEnumerator PlayDash(GameObject playerObject, bool isShaman, bool isVolt) {
+ // Play louder sound
+ PlayHornetAttackSound(playerObject);
+
+ // Hide the player during animation
+ HidePlayer(playerObject);
+
+ // Get appropriate effect object from FSM (Parry Cross Slash)
+ if (TryGetSlashEffect(playerObject, isVolt, out var slash)) {
+ slash.SetActive(false);
+ slash.SetActive(true);
+
+ var runes = slash.FindGameObjectInChildren("Runes");
+
+ if (runes != null) {
+ runes.SetActive(isShaman);
+ }
+
+ var damager = slash.FindGameObjectInChildren("Enemy_Damager");
+ if (damager != null) {
+ SetDamageHeroState(damager);
+ } else {
+ Logger.Warn("Unable to set damager for Cross Stitch");
+ }
+ } else {
+ Logger.Warn("Unable to set damager for Cross Stitch");
+ }
+
+ // Wait for animation to finish
+ yield return new WaitForSeconds(0.75f);
+
+ // Deactivate effect object
+ if (slash) {
+ slash.SetActive(false);
+ }
+ }
+
+ private bool TryGetParryThread(GameObject playerObject, bool zap, [MaybeNullWhen(false)] out GameObject thread) {
+ // Find existing object
+ var name = zap ? "Parry Thread Zap" : "Parry Thread";
+ var created = FindOrCreateAttack(playerObject, name, out var threadObj);
+ if (threadObj == null) {
+ thread = null;
+ return false;
+ }
+
+ thread = threadObj;
+ if (!created) return true;
+
+ if (zap) {
+ var bloom = thread.FindGameObjectInChildren("light_effect_v02 (2)");
+ if (bloom) {
+ Object.Destroy(bloom);
+ }
+
+ thread.SetActiveChildren(true);
+ thread.SetActive(false);
+ }
+
+ return true;
+ }
+
+ private bool TryGetStanceFlash(GameObject playerObject, [MaybeNullWhen(false)] out GameObject flash) {
+ var created = FindOrCreateAttack(playerObject, ParryStanceFlashName, out var flashObj);
+ if (flashObj == null) {
+ flash = null;
+ return false;
+ }
+
+ flash = flashObj;
+ if (!created) return true;
+
+ var runes = flash.GetComponentsInChildren();
+ foreach (var rune in runes) {
+ Object.DestroyImmediate(rune);
+ }
+
+ var runeGlow = flash.FindGameObjectInChildren("Shaman Flash Glow");
+
+ if (runeGlow) {
+ Object.DestroyImmediate(runeGlow);
+ }
+
+ return true;
+ }
+
+ private bool TryGetClashEffect(GameObject playerObject, [MaybeNullWhen(false)] out GameObject clash) {
+ var created = FindOrCreateAttack(playerObject, ParryClashName, out var clashObj);
+ if (clashObj == null) {
+ clash = null;
+ return false;
+ }
+
+ clash = clashObj;
+ if (!created) return true;
+
+ var runes = clash.GetComponentsInChildren();
+ foreach (var rune in runes) {
+ Object.DestroyImmediate(rune);
+ }
+
+ return true;
+ }
+
+ private bool TryGetSlashEffect(GameObject playerObject, bool isZap, [MaybeNullWhen(false)] out GameObject slash) {
+ var name = isZap ? ParryZapSlashName : ParrySlashName;
+
+ var attacks = GetPlayerSilkAttacks(playerObject);
+ slash = attacks.FindGameObjectInChildren(name);
+
+ if (slash) {
+ return true;
+ }
+
+ var fsm = GetSkillFSM();
+ var boolTest = fsm.GetAction("Parry Cross Slash", 8);
+ if (boolTest == null) {
+ return false;
+ }
+
+ GameObject localSlashObj;
+ if (isZap) {
+ localSlashObj = boolTest.TrueGameObject.Value;
+ } else {
+ localSlashObj = boolTest.FalseGameObject.Value;
+ }
+
+ slash = Object.Instantiate(localSlashObj, attacks.transform);
+ slash.name = name;
+ slash.transform.localPosition = new Vector3(1, 1, 0);
+
+ var runes = slash.GetComponentsInChildren();
+ foreach (var rune in runes) {
+ Object.DestroyImmediate(rune);
+ }
+
+ var haze = slash.FindGameObjectInChildren("haze2");
+ if (haze != null) {
+ Object.Destroy(haze);
+ }
+
+ var runeFlash = slash
+ .FindGameObjectInChildren("Runes")?
+ .FindGameObjectInChildren("Runes Flash");
+
+ if (runeFlash != null) {
+ Object.Destroy(runeFlash);
+ }
+
+ // Enable damage collider
+ var damager = slash.FindGameObjectInChildren("Enemy_Damager");
+ if (damager != null && damager.TryGetComponent(out var collider)) {
+ collider.enabled = true;
+ }
+
+ return true;
+ }
+}
diff --git a/SSMP/Animation/Effects/SilkSkills/SharpDart.cs b/SSMP/Animation/Effects/SilkSkills/SharpDart.cs
index 84249cd9..7ef63e8d 100644
--- a/SSMP/Animation/Effects/SilkSkills/SharpDart.cs
+++ b/SSMP/Animation/Effects/SilkSkills/SharpDart.cs
@@ -18,12 +18,12 @@ internal class SharpDart : BaseSilkSkill {
public bool Volt = false;
- public override void Play(GameObject playerObject, CrestType crestType, byte[] effectInfo) {
+ public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
var isShaman = crestType == CrestType.Shaman;
- MonoBehaviourUtil.Instance.StartCoroutine(Play(playerObject, isShaman));
+ MonoBehaviourUtil.Instance.StartCoroutine(PlayEffect(playerObject, isShaman));
}
- private IEnumerator Play(GameObject playerObject, bool isShaman) {
+ private IEnumerator PlayEffect(GameObject playerObject, bool isShaman) {
if (TryGetDamager(playerObject, out var damager)) {
SetDamageHeroState(damager);
damager.SetActive(true);
diff --git a/SSMP/Util/AudioUtil.cs b/SSMP/Util/AudioUtil.cs
index 3d12b741..3744ae29 100644
--- a/SSMP/Util/AudioUtil.cs
+++ b/SSMP/Util/AudioUtil.cs
@@ -122,6 +122,34 @@ GameObject playerObject
}
}
+ ///
+ /// Play a random audio clip from the given FSM action positionally at the given player object's position.
+ ///
+ /// The action instance from an FSM.
+ /// The player object to play the audio at.
+ public static void PlayAudio(
+ PlayRandomAudioClipTableV3 playAudioClip,
+ GameObject playerObject
+ ) {
+ var audioClipTable = playAudioClip.Table.value as RandomAudioClipTable;
+ if (audioClipTable == null) {
+ Logger.Warn("Audio clip table for PlayRandomAudioClipTableV3 is null");
+ return;
+ }
+
+ var position = playerObject.transform.position;
+
+ if (playAudioClip.AudioPlayerPrefab.Value) {
+ audioClipTable.SpawnAndPlayOneShot(
+ playAudioClip.AudioPlayerPrefab.value as AudioSource,
+ position,
+ playAudioClip.ForcePlay.value
+ );
+ } else {
+ audioClipTable.SpawnAndPlayOneShot(position, playAudioClip.ForcePlay.value);
+ }
+ }
+
///
/// Play a random audio clip from the given FSM action positionally at the given player object's position.
///
From cffd6127b68ffd17f352f893974c803162d407bd Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Wed, 15 Apr 2026 02:24:30 -0400
Subject: [PATCH 06/41] Add other players to sonar
---
SSMP/Animation/AnimationManager.cs | 86 ++++++++++++++++++-
SSMP/Animation/Effects/SilkSkills/RuneRage.cs | 13 +++
SSMP/Game/Client/ClientManager.cs | 2 +-
3 files changed, 97 insertions(+), 4 deletions(-)
create mode 100644 SSMP/Animation/Effects/SilkSkills/RuneRage.cs
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index aea9e429..41cb26c0 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -11,6 +11,7 @@
using SSMP.Internals;
using SSMP.Networking.Client;
using SSMP.Util;
+using UnityEngine;
using UnityEngine.SceneManagement;
using Logger = SSMP.Logging.Logger;
@@ -666,7 +667,8 @@ internal class AnimationManager {
{ AnimationClip.SilkChargeZap, new SharpDart { Volt = true } },
{ AnimationClip.ParryStance, CrossStitch.StartingInstance },
{ AnimationClip.ParryStanceGround, CrossStitch.StartingInstance },
- { AnimationClip.ParryClash, new CrossStitch() }
+ { AnimationClip.ParryClash, new CrossStitch() },
+ { AnimationClip.SilkBombAntic, new RuneRage() }
};
private static readonly Dictionary SubAnimationEffects = new() {
@@ -686,6 +688,12 @@ internal class AnimationManager {
///
private readonly PlayerManager _playerManager;
+ ///
+ ///
+ private readonly Dictionary _playerData;
+
+ private ServerSettings _serverSettings;
+
///
/// The last animation clip sent.
///
@@ -738,11 +746,12 @@ internal class AnimationManager {
public AnimationManager(
NetClient netClient,
- PlayerManager playerManager
+ PlayerManager playerManager,
+ Dictionary playerData
) {
_netClient = netClient;
_playerManager = playerManager;
-
+ _playerData = playerData;
// _chargedEffectStopwatch = new Stopwatch();
// _chargedEndEffectStopwatch = new Stopwatch();
}
@@ -751,6 +760,8 @@ PlayerManager playerManager
/// Initialize the animation manager by registering packet handlers and initializing animation effects.
///
public void Initialize(ServerSettings serverSettings) {
+ _serverSettings = serverSettings;
+
// Set the server settings for all animation effects
foreach (var effect in AnimationEffects.Values) {
effect.SetServerSettings(serverSettings);
@@ -1098,8 +1109,22 @@ private void CreateHeroHooksHooks(HeroController hc) {
var threadStormExtend = silkSkillFsm.GetState("Extend");
FsmStateActionInjector.Inject(threadStormExtend, OnThreadStormExtend, 0);
+
+ var sonarBuildArray = silkSkillFsm.GetState("Build Enemy Array For Ping");
+ FsmStateActionInjector.Inject(sonarBuildArray, OnBuildRuneRagePingArray, 0);
+
+ //var enemiesIn = silkSkillFsm.GetState("Enemies In Ping Array?");
+ //FsmStateActionInjector.Inject(enemiesIn, LogEnemyList, 0);
}
+ //private void LogEnemyList(PlayMakerFSM fsm) {
+ // var arr = fsm.FsmVariables.ArrayVariables[0];
+ // foreach (var value in arr.Values) {
+ // if (value is GameObject go)
+ // Logger.Info(go.name);
+ // }
+ //}
+
///
/// Animation subanimation hook for the Witch Tentacles FSM state
///
@@ -1143,6 +1168,61 @@ private void OnThreadStormExtend(PlayMakerFSM fsm) {
}
+ private void OnBuildRuneRagePingArray(PlayMakerFSM fsm) {
+ // If we are not connected, there is nothing to send to
+ if (!_netClient.IsConnected) {
+ return;
+ }
+
+ // Sonar tracker for Rune Rage
+ var sonarObject = HeroController.instance.gameObject
+ .FindGameObjectInChildren("Special Attacks")?
+ .FindGameObjectInChildren("Sonar Enemy Tracker");
+
+ if (sonarObject == null) return;
+
+ var sonar = sonarObject.GetComponent();
+ if (sonar == null) return;
+
+ var sonarCollider = sonarObject.GetComponent();
+ if (sonarCollider == null) return;
+
+ var radius = sonarCollider.radius * sonar.transform.GetScaleX();
+
+ if (!_serverSettings.IsPvpEnabled) return;
+
+ var playersInSonar = sonar.insideObjectsList;
+
+ // Find all players within sonar
+ foreach (var player in _playerData.Values) {
+ if (!player.IsInLocalScene || !player.PlayerObject) {
+ continue;
+ }
+
+ if (_serverSettings.TeamsEnabled && player.Team == _playerManager.LocalPlayerTeam) {
+ continue;
+ }
+
+ var collider = player.PlayerObject.GetComponent();
+ if (!collider) continue;
+
+ var closestPlayerPoint = collider.ClosestPoint(sonarCollider.transform.position);
+ Logger.Info(closestPlayerPoint.ToString());
+
+ var distanceFromCenter = Vector2.Distance(closestPlayerPoint, sonarCollider.transform.position);
+ Logger.Info(distanceFromCenter.ToString());
+ Logger.Info(radius.ToString());
+
+ if (distanceFromCenter <= radius) {
+ playersInSonar.AddIfNotPresent(player.PlayerObject);
+ Logger.Info($"Player {player.Username} is in the sonar!");
+ } else {
+ playersInSonar.Remove(player.PlayerObject);
+ Logger.Info("Not touching");
+ }
+ }
+ }
+
// ///
// /// Callback method on the HeroAnimationController#Play method.
// ///
diff --git a/SSMP/Animation/Effects/SilkSkills/RuneRage.cs b/SSMP/Animation/Effects/SilkSkills/RuneRage.cs
new file mode 100644
index 00000000..a2293f5a
--- /dev/null
+++ b/SSMP/Animation/Effects/SilkSkills/RuneRage.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using SSMP.Internals;
+using UnityEngine;
+
+namespace SSMP.Animation.Effects.SilkSkills;
+
+internal class RuneRage : BaseSilkSkill {
+ public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ return;
+ }
+}
diff --git a/SSMP/Game/Client/ClientManager.cs b/SSMP/Game/Client/ClientManager.cs
index 49e2b998..83bbb766 100644
--- a/SSMP/Game/Client/ClientManager.cs
+++ b/SSMP/Game/Client/ClientManager.cs
@@ -229,7 +229,7 @@ ModSettings modSettings
_playerData = new Dictionary();
_playerManager = new PlayerManager(serverSettings, _playerData);
- _animationManager = new AnimationManager(netClient, _playerManager);
+ _animationManager = new AnimationManager(netClient, _playerManager, _playerData);
_mapManager = new MapManager(netClient, serverSettings);
_entityManager = new EntityManager(netClient);
From 8305f36c791c4b144c25dd7d398ea2041898bfa8 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Thu, 16 Apr 2026 22:41:09 -0400
Subject: [PATCH 07/41] Add rune rage
---
SSMP/Animation/AnimationClip.cs | 1 +
SSMP/Animation/AnimationManager.cs | 119 +++--
SSMP/Animation/Effects/EffectUtils.cs | 1 +
.../Effects/SilkSkills/CrossStitch.cs | 12 +-
SSMP/Animation/Effects/SilkSkills/RuneRage.cs | 426 +++++++++++++++++-
5 files changed, 512 insertions(+), 47 deletions(-)
diff --git a/SSMP/Animation/AnimationClip.cs b/SSMP/Animation/AnimationClip.cs
index b35b2292..a190c8b1 100644
--- a/SSMP/Animation/AnimationClip.cs
+++ b/SSMP/Animation/AnimationClip.cs
@@ -522,6 +522,7 @@ internal enum AnimationClip {
SilkBombAntic,
SilkBombAnticQ,
SilkBombLoop,
+ SilkBombLocations,
SilkBombRecover,
SitCraft,
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index 41cb26c0..38848829 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
+using HutongGames.PlayMaker.Actions;
using SSMP.Animation.Effects;
using SSMP.Animation.Effects.SilkSkills;
using SSMP.Collection;
@@ -401,6 +402,7 @@ internal class AnimationManager {
{ "Silk Bomb Antic Q", AnimationClip.SilkBombAnticQ },
{ "Silk Bomb Loop", AnimationClip.SilkBombLoop },
{ "Silk Bomb Recover", AnimationClip.SilkBombRecover },
+ { "Silk Bomb Locations", AnimationClip.SilkBombLocations },
{ "Sit Craft", AnimationClip.SitCraft },
{ "Sit Craft Silk", AnimationClip.SitCraftSilk },
@@ -668,7 +670,7 @@ internal class AnimationManager {
{ AnimationClip.ParryStance, CrossStitch.StartingInstance },
{ AnimationClip.ParryStanceGround, CrossStitch.StartingInstance },
{ AnimationClip.ParryClash, new CrossStitch() },
- { AnimationClip.SilkBombAntic, new RuneRage() }
+ { AnimationClip.SilkBombAntic, new RuneRage { IsAntic = true } }
};
private static readonly Dictionary SubAnimationEffects = new() {
@@ -676,6 +678,7 @@ internal class AnimationManager {
{ AnimationClip.ShamanCancel, new Bind { BindState = Bind.State.ShamanCancel } },
{ AnimationClip.BindInterrupt, BindInterrupt.Instance },
{ AnimationClip.AirSphereRefresh, new ThreadStorm() },
+ { AnimationClip.SilkBombLocations, new RuneRage() }
};
///
@@ -714,6 +717,8 @@ internal class AnimationManager {
///
private bool _dashHasEnded = true;
+ private List _runeRagePositions = new();
+
// ///
// /// Whether the player has sent that they stopped crystal dashing.
// ///
@@ -1110,20 +1115,19 @@ private void CreateHeroHooksHooks(HeroController hc) {
var threadStormExtend = silkSkillFsm.GetState("Extend");
FsmStateActionInjector.Inject(threadStormExtend, OnThreadStormExtend, 0);
- var sonarBuildArray = silkSkillFsm.GetState("Build Enemy Array For Ping");
- FsmStateActionInjector.Inject(sonarBuildArray, OnBuildRuneRagePingArray, 0);
+ // Rune Rage injections
+ var sonarBuildArray = silkSkillFsm.GetState("Build Enemy Array");
+ FsmStateActionInjector.Inject(sonarBuildArray, OnBuildRuneRageArray, 0);
- //var enemiesIn = silkSkillFsm.GetState("Enemies In Ping Array?");
- //FsmStateActionInjector.Inject(enemiesIn, LogEnemyList, 0);
- }
+ var blastEnemy = silkSkillFsm.GetState("Blast Enemy");
+ FsmStateActionInjector.Inject(blastEnemy, OnBlastEnemy, 4);
- //private void LogEnemyList(PlayMakerFSM fsm) {
- // var arr = fsm.FsmVariables.ArrayVariables[0];
- // foreach (var value in arr.Values) {
- // if (value is GameObject go)
- // Logger.Info(go.name);
- // }
- //}
+ var blastRandom = silkSkillFsm.GetState("Random Blasts");
+ FsmStateActionInjector.Inject(blastRandom, OnBlastRandom, 3);
+
+ var blastFinished = silkSkillFsm.GetState("Silk Bomb Recover");
+ FsmStateActionInjector.Inject(blastFinished, OnBlastFinished, 0);
+ }
///
/// Animation subanimation hook for the Witch Tentacles FSM state
@@ -1155,43 +1159,47 @@ private void OnBindInterrupt(PlayMakerFSM fsm) {
}
private void OnThreadStormExtend(PlayMakerFSM fsm) {
- //var dummyClip = new tk2dSpriteAnimationClip();
- //dummyClip.name = "AirSphere Attack";
- //dummyClip.wrapMode = tk2dSpriteAnimationClip.WrapMode.Once;
- //if (_lastAnimationClip == dummyClip.name) {
- // dummyClip.name = "AirSphere Attack";
- //}
- //OnAnimationEvent(dummyClip);
-
- var effectInfo = ThreadStorm.GetEffectFlags();
+ var effectInfo = BaseSilkSkill.GetEffectFlags();
_netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.AirSphereRefresh, 0, effectInfo);
}
- private void OnBuildRuneRagePingArray(PlayMakerFSM fsm) {
+ ///
+ /// Influences Rune Rage to be able to target players that can be attacked
+ ///
+ private void OnBuildRuneRageArray(PlayMakerFSM fsm) {
// If we are not connected, there is nothing to send to
if (!_netClient.IsConnected) {
return;
}
- // Sonar tracker for Rune Rage
+ _runeRagePositions.Clear();
+
+ // Find tracker for Rune Rage
var sonarObject = HeroController.instance.gameObject
.FindGameObjectInChildren("Special Attacks")?
.FindGameObjectInChildren("Sonar Enemy Tracker");
if (sonarObject == null) return;
+ // Get needed components
var sonar = sonarObject.GetComponent();
if (sonar == null) return;
var sonarCollider = sonarObject.GetComponent();
if (sonarCollider == null) return;
- var radius = sonarCollider.radius * sonar.transform.GetScaleX();
+ // Remove any old player objects
+ sonar.Refresh();
- if (!_serverSettings.IsPvpEnabled) return;
+ // If PvP is off, remove any players that might be inside
+ if (!_serverSettings.IsPvpEnabled) {
+ return;
+ }
+
+ var radius = sonarCollider.radius * sonar.transform.GetScaleX();
- var playersInSonar = sonar.insideObjectsList;
+ var inSonar = sonar.insideObjectsList;
// Find all players within sonar
foreach (var player in _playerData.Values) {
@@ -1199,30 +1207,69 @@ private void OnBuildRuneRagePingArray(PlayMakerFSM fsm) {
continue;
}
- if (_serverSettings.TeamsEnabled && player.Team == _playerManager.LocalPlayerTeam) {
+ // Don't bother to target players on same team
+ if (_serverSettings.TeamsEnabled && player.Team == _playerManager.LocalPlayerTeam && player.Team != Team.None) {
+ inSonar.Remove(player.PlayerObject);
continue;
}
var collider = player.PlayerObject.GetComponent();
if (!collider) continue;
+ // Determine if the player is within the sonar circle
var closestPlayerPoint = collider.ClosestPoint(sonarCollider.transform.position);
- Logger.Info(closestPlayerPoint.ToString());
-
var distanceFromCenter = Vector2.Distance(closestPlayerPoint, sonarCollider.transform.position);
- Logger.Info(distanceFromCenter.ToString());
- Logger.Info(radius.ToString());
if (distanceFromCenter <= radius) {
- playersInSonar.AddIfNotPresent(player.PlayerObject);
- Logger.Info($"Player {player.Username} is in the sonar!");
+ inSonar.AddIfNotPresent(player.PlayerObject);
} else {
- playersInSonar.Remove(player.PlayerObject);
- Logger.Info("Not touching");
+ inSonar.Remove(player.PlayerObject);
}
}
}
+ ///
+ /// Intercepts the spawn locations for targeted Rune Rages
+ ///
+ private void OnBlastEnemy(PlayMakerFSM fsm) {
+ // At this point the rune cluster has been created with a position and a given offset from the object.
+ // This position is relative to the player.
+ var spawnPosition = fsm.FsmVariables.FindFsmVector3("Shift Pos");
+ if (spawnPosition != null) {
+ var position = RuneRage.EncodeRunePosition(spawnPosition.Value + HeroController.instance.transform.position);
+ _runeRagePositions.AddRange(position);
+ }
+ }
+
+ ///
+ /// Intercepts the spawn locations for random Rune Rages
+ ///
+ private void OnBlastRandom(PlayMakerFSM fsm) {
+ // If there aren't enough targets, rune rage will spawn up to 3 other blasts
+ var spawnRadial = fsm.GetFirstAction("Random Blasts");
+ if (spawnRadial != null) {
+ // Get the positions of spawned blasts
+ var positions = spawnRadial.tempPosStore;
+
+ // Add to collection of positions
+ foreach (var position in positions) {
+ var encodedPosition = RuneRage.EncodeRunePosition((Vector3)position);
+ _runeRagePositions.AddRange(encodedPosition);
+ }
+ }
+ }
+
+ ///
+ /// Animation hook to send Rune Rage positions
+ ///
+ private void OnBlastFinished(PlayMakerFSM fsm) {
+ var effectInfo = BaseSilkSkill.GetEffectFlags().ToList();
+ effectInfo.AddRange(_runeRagePositions);
+
+ _runeRagePositions.Clear();
+ _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.SilkBombLocations, 0, effectInfo.ToArray());
+ }
+
// ///
// /// Callback method on the HeroAnimationController#Play method.
// ///
diff --git a/SSMP/Animation/Effects/EffectUtils.cs b/SSMP/Animation/Effects/EffectUtils.cs
index 8abd61fa..189b8d93 100644
--- a/SSMP/Animation/Effects/EffectUtils.cs
+++ b/SSMP/Animation/Effects/EffectUtils.cs
@@ -16,6 +16,7 @@ public static void SafelyRemoveAutoRecycle(GameObject obj) {
var recycler = obj.GetComponent();
if (recycler != null) {
// Stop listeners before destroying
+ AutoRecycleSelf.activeRecyclers.Remove(recycler);
recycler.recycleTimerRunning = false;
recycler.subbed = false;
diff --git a/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs b/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs
index d3a1937d..4c477682 100644
--- a/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs
+++ b/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs
@@ -1,10 +1,6 @@
-using System;
using System.Collections;
-using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
-using HutongGames.PlayMaker;
using HutongGames.PlayMaker.Actions;
-using Mono.WebBrowser;
using SSMP.Internals;
using SSMP.Util;
using UnityEngine;
@@ -120,7 +116,7 @@ private IEnumerator PlayDash(GameObject playerObject, bool isShaman, bool isVolt
}
}
- private bool TryGetParryThread(GameObject playerObject, bool zap, [MaybeNullWhen(false)] out GameObject thread) {
+ private static bool TryGetParryThread(GameObject playerObject, bool zap, [MaybeNullWhen(false)] out GameObject thread) {
// Find existing object
var name = zap ? "Parry Thread Zap" : "Parry Thread";
var created = FindOrCreateAttack(playerObject, name, out var threadObj);
@@ -145,7 +141,7 @@ private bool TryGetParryThread(GameObject playerObject, bool zap, [MaybeNullWhen
return true;
}
- private bool TryGetStanceFlash(GameObject playerObject, [MaybeNullWhen(false)] out GameObject flash) {
+ private static bool TryGetStanceFlash(GameObject playerObject, [MaybeNullWhen(false)] out GameObject flash) {
var created = FindOrCreateAttack(playerObject, ParryStanceFlashName, out var flashObj);
if (flashObj == null) {
flash = null;
@@ -169,7 +165,7 @@ private bool TryGetStanceFlash(GameObject playerObject, [MaybeNullWhen(false)] o
return true;
}
- private bool TryGetClashEffect(GameObject playerObject, [MaybeNullWhen(false)] out GameObject clash) {
+ private static bool TryGetClashEffect(GameObject playerObject, [MaybeNullWhen(false)] out GameObject clash) {
var created = FindOrCreateAttack(playerObject, ParryClashName, out var clashObj);
if (clashObj == null) {
clash = null;
@@ -187,7 +183,7 @@ private bool TryGetClashEffect(GameObject playerObject, [MaybeNullWhen(false)] o
return true;
}
- private bool TryGetSlashEffect(GameObject playerObject, bool isZap, [MaybeNullWhen(false)] out GameObject slash) {
+ private static bool TryGetSlashEffect(GameObject playerObject, bool isZap, [MaybeNullWhen(false)] out GameObject slash) {
var name = isZap ? ParryZapSlashName : ParrySlashName;
var attacks = GetPlayerSilkAttacks(playerObject);
diff --git a/SSMP/Animation/Effects/SilkSkills/RuneRage.cs b/SSMP/Animation/Effects/SilkSkills/RuneRage.cs
index a2293f5a..80236811 100644
--- a/SSMP/Animation/Effects/SilkSkills/RuneRage.cs
+++ b/SSMP/Animation/Effects/SilkSkills/RuneRage.cs
@@ -1,13 +1,433 @@
-using System;
+using System.Collections;
using System.Collections.Generic;
-using System.Text;
+using System.Diagnostics.CodeAnalysis;
+using HutongGames.PlayMaker.Actions;
using SSMP.Internals;
+using SSMP.Util;
using UnityEngine;
+using Logger = SSMP.Logging.Logger;
+using Object = UnityEngine.Object;
namespace SSMP.Animation.Effects.SilkSkills;
internal class RuneRage : BaseSilkSkill {
+ ///
+ /// Name of the game object for the normal antic.
+ ///
+ private const string AnticName = "attack_bomb_cast_antic_thread";
+
+ ///
+ /// Name of the game object for the volt filament antic.
+ ///
+ private const string AnticVoltName = "antic_thread_zap";
+
+ ///
+ /// Scale used to keep a higher level of precision when converting a float to a byte
+ ///
+ private const int PositionScale = 9;
+
+ ///
+ /// Offset to keep negative values when converting an sbyte to a byte
+ ///
+ private const int PositionOffset = sbyte.MaxValue;
+
+
+ ///
+ /// Keeps track of if this instance is made to run the antic or not.
+ ///
+ public bool IsAntic = false;
+
+ ///
+ /// Cached object for Hornet's normal Rune Rage burst
+ ///
+ private static GameObject? _localRuneBlast;
+
+ ///
+ /// Cached object for Hornet's volt filament Rune Rage burst
+ ///
+ private static GameObject? _localRuneBlastVolt;
+
+ ///
+ /// Cached transform of an object that templates how a Rune Rage Cluster should be laid out
+ ///
+ private static Transform? _clusterSpawnTemplate;
+
+ ///
public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
- return;
+ var isVolt = IsVolt(effectInfo);
+ var isShaman = crestType == CrestType.Shaman;
+
+ // Play antic if appropriate
+ if (IsAntic) {
+ PlayRageAntic(playerObject, isVolt);
+ return;
+ }
+
+ // Decode rune positions from effect info
+ var positions = DecodeRunePositions(effectInfo, playerObject);
+
+ // Check volt status
+ if (effectInfo?.Length > 0 && effectInfo[0] == 1) {
+ isVolt = true;
+ }
+
+ PlaySonar(playerObject, isVolt, isShaman);
+
+ // There are runes to spawn. Do at the same time as the sonar.
+ if (positions.Count > 0) {
+ PlayRuneRage(positions, isVolt, isShaman);
+ }
+ }
+
+ ///
+ /// Plays the silk-based antic for Rune Rage
+ ///
+ /// The player using the effect
+ /// If the volt filament effects should be used
+ private static void PlayRageAntic(GameObject playerObject, bool isVolt) {
+ PlayHornetAttackSound(playerObject);
+
+ // Play antic
+ if (TryGetAntic(playerObject, out var antic)) {
+ antic.SetActive(false);
+ antic.SetActive(true);
+
+ var volt = antic.FindGameObjectInChildren(AnticVoltName);
+ if (volt != null) {
+ volt.SetActive(false);
+ volt.SetActive(isVolt);
+ }
+ }
+
+ var fsm = GetSkillFSM();
+
+ // Play audio in S Bomb Zap FX 1
+ if (isVolt) {
+ var voltAntic = fsm.GetFirstAction("S Bomb Zap FX");
+ if (voltAntic != null) {
+ AudioUtil.PlayAudio(voltAntic, playerObject);
+ }
+ }
+
+ // Play audio in Silk Bomb Start
+ var runeAnticAudio = fsm.GetAction("Silk Bomb Start", 15);
+ if (runeAnticAudio != null) {
+ AudioUtil.PlayAudio(runeAnticAudio, playerObject);
+ }
+ }
+
+ ///
+ /// Plays the sonar blast effect. Purely visual.
+ ///
+ /// The player using the effect
+ /// If the volt filament effects should be used
+ /// If the shaman crest effects should be used
+ private void PlaySonar(GameObject playerObject, bool isVolt, bool isShaman) {
+ var fsm = GetSkillFSM();
+
+ // Play audio in Initial Silk Cost
+ var runeBurstAudio = fsm.GetFirstAction("Initial Silk Cost");
+ if (runeBurstAudio != null) {
+ AudioUtil.PlayAudio(runeBurstAudio, playerObject);
+ }
+
+ // Play audio in S Bomb Volt FX 2
+ if (isVolt) {
+ var zapAudioBug = fsm.GetFirstAction("S Bomb Zap FX 2");
+ if (zapAudioBug != null) {
+ AudioUtil.PlayAudio(zapAudioBug, playerObject);
+ }
+ }
+
+ // Spawn sonar, picking the right one for the volt filament setting
+ var sonarPicker = fsm.GetFirstAction("Sonar Cast Effects");
+ if (sonarPicker == null) return;
+
+ GameObject localSonar;
+
+ if (isVolt) {
+ localSonar = sonarPicker.TrueGameObject.Value;
+ } else {
+ localSonar = sonarPicker.FalseGameObject.Value;
+ }
+
+ // Spawn and remove components
+ var sonarParent = EffectUtils.SpawnGlobalPoolObject(localSonar, playerObject.transform, 2f);
+ if (sonarParent != null) {
+ var cam = sonarParent.GetComponentInChildren();
+ if (cam != null) {
+ Object.DestroyImmediate(cam);
+ }
+ }
+
+ if (!isShaman) return;
+
+ // Spawn shaman effect and remove components
+ var localShaman = fsm.GetAction("Sonar Cast Effects", 6);
+ if (localShaman == null) return;
+
+ var shaman = EffectUtils.SpawnGlobalPoolObject(localShaman, playerObject.transform, 0.7f);
+ if (shaman == null) return;
+
+ if (shaman.TryGetComponent(out var rune)) {
+ Object.DestroyImmediate(rune);
+ }
+
+ if (shaman.TryGetComponent(out var camEffect)) {
+ Object.DestroyImmediate(camEffect);
+ }
+ }
+
+ ///
+ /// Spawns clusters of Rune Rage blasts at the given positions
+ ///
+ /// The positions to spawn blast clusters at
+ /// If the volt filament effects should be used
+ /// If the shaman crest effects should be used
+ private void PlayRuneRage(List positions, bool isVolt, bool isShaman) {
+ // Generate spawn template
+ // Template layout looks like . * .
+ if (_clusterSpawnTemplate == null) {
+ _clusterSpawnTemplate = new GameObject().transform;
+ var firstBlast = new GameObject().transform;
+ var secondBlast = new GameObject().transform;
+ var thirdBlast = new GameObject().transform;
+
+ firstBlast.SetParentReset(_clusterSpawnTemplate);
+ firstBlast.localPosition = new Vector3(1.732f, -1, 0);
+ firstBlast.SetRotation2D(240);
+
+ secondBlast.SetParentReset(_clusterSpawnTemplate);
+ secondBlast.SetLocalPositionY(2);
+
+ thirdBlast.SetParent(_clusterSpawnTemplate);
+ thirdBlast.localPosition = new Vector3(-1.732f, -1, 0);
+ thirdBlast.SetRotation2D(-240);
+
+ Object.DontDestroyOnLoad(_clusterSpawnTemplate.gameObject);
+ }
+
+ // Spawn clusters at each position
+ foreach (var position in positions) {
+ MonoBehaviourUtil.Instance.StartCoroutine(PlayRuneCluster(position, isVolt, isShaman));
+ }
+ }
+
+ ///
+ /// Gets the Rune Rage antic effect
+ ///
+ /// The player that is using the antic
+ /// The found or created antic
+ ///
+ private static bool TryGetAntic(GameObject playerObject, [MaybeNullWhen(false)] out GameObject antic) {
+ // Find or create the antic object
+ var created = FindOrCreateAttack(playerObject, AnticName, out var anticObj);
+ if (anticObj == null) {
+ antic = null;
+ return false;
+ }
+
+ antic = anticObj;
+ if (!created) return true;
+
+ // Remove problematic component from newly created object
+ if (antic.TryGetComponent(out var checker)) {
+ Object.DestroyImmediate(checker);
+ }
+
+ return true;
+ }
+
+ ///
+ /// Gets the prefab for a single Rune Rage blast
+ ///
+ /// If the volt filament effects should be used
+ /// The blast prefab, if found.
+ private static GameObject? TryGetLocalBlast(bool isVolt) {
+ // Return existing if possible
+ if (isVolt) {
+ if (_localRuneBlastVolt) return _localRuneBlastVolt;
+ } else if (_localRuneBlast) {
+ return _localRuneBlast;
+ }
+
+ // Get the rune cluster object from the FSM
+ var fsm = GetSkillFSM();
+ var cluster = fsm.GetFirstAction("Blast Enemy").gameObject.Value;
+
+ if (cluster == null) return null;
+
+ // Find the individual rune from within the cluster FSM
+ var clusterFsm = cluster.LocateMyFSM("Control");
+ if (clusterFsm == null) return null;
+
+ var blaster = clusterFsm.GetFirstAction("Do Explosions");
+ if (blaster == null) return null;
+
+ // Fill out both blast prefabs
+ _localRuneBlastVolt = blaster.TrueGameObject.Value;
+ _localRuneBlast = blaster.FalseGameObject.Value;
+
+ // Return correct blast
+ if (isVolt) {
+ return _localRuneBlastVolt;
+ } else {
+ return _localRuneBlast;
+ }
+ }
+
+ ///
+ /// Spawns a cluster of three Rune Rage blasts
+ ///
+ /// The initial position to spawn the cluster
+ /// If the volt filament effects should be used
+ /// If the shaman crest effects should be used
+ private IEnumerator PlayRuneCluster(Vector3 position, bool isVolt, bool isShaman) {
+ if (!_clusterSpawnTemplate) yield break;
+
+ // Create local version of template and set the position
+ var spawnTemplate = Object.Instantiate(_clusterSpawnTemplate);
+ spawnTemplate.position = position;
+
+ // Change the local rotation for a bit more variety
+ spawnTemplate.SetLocalRotation2D(Random.Range(0, 360));
+
+ var offset = new Vector3(1.75f, 1.75f);
+
+ // Spawn 3 runes per cluster, each one with a random offset from their spawn point
+ for (var i = 0; i < spawnTemplate.childCount; i++) {
+ var blastPosition = spawnTemplate.GetChild(i).position;
+ blastPosition += offset.RandomInRange();
+
+ CreateBlast(spawnTemplate, isVolt, isShaman, blastPosition);
+
+ // Delay next rune by a little bit
+ var waitTime = Random.Range(0.15f, 0.2f);
+ yield return new WaitForSeconds(waitTime);
+ }
+
+ Object.Destroy(spawnTemplate.gameObject);
+ }
+
+ ///
+ /// Spawns a single Rune Rage blast
+ ///
+ /// The initial parent to use for spawning
+ /// If the volt filament effects should be used
+ /// If the shaman crest effects should be used
+ /// The final position of the blast
+ private void CreateBlast(Transform spawnTransform, bool isVolt, bool isShaman, Vector3 position) {
+ var localBlast = TryGetLocalBlast(isVolt);
+ if (localBlast == null) return;
+
+ // Create copy of blast and set position
+ var blast = EffectUtils.SpawnGlobalPoolObject(localBlast, spawnTransform, 3);
+ if (blast == null) return;
+
+ blast.transform.position = position;
+
+ // Add damager
+ var damager = blast
+ .FindGameObjectInChildren("Blast")?
+ .FindGameObjectInChildren("damager");
+
+ if (damager != null) {
+ SetDamageHeroState(damager, 1);
+ }
+
+ // Remove extra recycling component
+ if (blast.TryGetComponent(out var recycler)) {
+ recycler.resetActions = null;
+ }
+
+ // Set shaman effect
+ if (blast.TryGetComponent(out var rune)) {
+ var runeObj = rune.rune;
+ Object.DestroyImmediate(rune);
+
+ if (runeObj) {
+ runeObj.SetActive(isShaman);
+
+ // Remove extra bloom object
+ if (isShaman) {
+ var bloom = runeObj.FindGameObjectInChildren("Shaman Rune Camera Bloom");
+ if (bloom) {
+ bloom.SetActive(false);
+ }
+ }
+ }
+
+ }
+
+ // Remove camera controller
+ var cam = blast.GetComponentInChildren();
+ if (cam) {
+ cam.enabled = false;
+ }
+
+ // Cull while offscreen (fixes bug)
+ var animator = blast.GetComponentInChildren();
+ if (animator) {
+ animator.cullingMode = AnimatorCullingMode.AlwaysAnimate;
+ }
+
+ // Remove recycle action
+ var fsm = blast.LocateMyFSM("Control");
+ var end = fsm.GetState("End");
+ end.Actions = [];
+ end.SaveActions();
+ }
+
+ ///
+ /// Converts a Vector3 to an array of bytes that are within a margin of error of the original X and Y values.
+ /// To be used for encoding Rune Rage cluster positions
+ ///
+ /// The position of the Rune Rage cluster
+ ///
+ internal static byte[] EncodeRunePosition(Vector3 runePosition) {
+ var hornetPosition = HeroController.instance.transform.position;
+
+ // The position of a cluster is always clamped to within +- ~13 units of Hornet.
+ // Get position relative to the player and offset by max value of sbyte.
+ // This allows us to keep negative values while using a byte.
+ // Multiplying by a larger number also allows higher precision.
+ var diffX = (byte)((runePosition.x - hornetPosition.x) * PositionScale + PositionOffset);
+ var diffY = (byte)((runePosition.y - hornetPosition.y) * PositionScale + PositionOffset);
+
+ return [
+ diffX,
+ diffY
+ ];
+ }
+
+ ///
+ /// Converts an array of bytes to Vector3s, using the reverse of the algorithm in .
+ /// To be used for decoding Rune Rage cluster positions
+ ///
+ /// The raw positions in byte form
+ /// The player that used the effect. Cluster positions are relative to this player.
+ ///
+ private static List DecodeRunePositions(byte[]? info, GameObject playerObject) {
+ if (info == null || info.Length < 3) {
+ return [];
+ }
+
+ var positions = new List();
+ var playerPosition = playerObject.transform.position;
+
+ // Loop through all xy pairs
+ for (var i = 1; i < info.Length - 1; i += 2) {
+ // Restore sbyte from byte, then convert to float for division
+ var x = (float)info[i] - PositionOffset;
+ var y = (float)info[i + 1] - PositionOffset;
+
+ // Restore original scale
+ var position = new Vector3(x / PositionScale, y / PositionScale, 0) ;
+
+ // Convert relative position to global position
+ positions.Add(position + playerPosition);
+ }
+
+ return positions;
}
}
From b2b60aa1b132fcca45d5f42b3bcbebe1663e4e87 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Fri, 17 Apr 2026 14:05:47 -0400
Subject: [PATCH 08/41] Add trailing pale nails
---
SSMP/Animation/AnimationManager.cs | 3 +-
.../Animation/Effects/SilkSkills/PaleNails.cs | 257 ++++++++++++++++++
2 files changed, 259 insertions(+), 1 deletion(-)
create mode 100644 SSMP/Animation/Effects/SilkSkills/PaleNails.cs
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index 38848829..15c7a423 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -670,7 +670,8 @@ internal class AnimationManager {
{ AnimationClip.ParryStance, CrossStitch.StartingInstance },
{ AnimationClip.ParryStanceGround, CrossStitch.StartingInstance },
{ AnimationClip.ParryClash, new CrossStitch() },
- { AnimationClip.SilkBombAntic, new RuneRage { IsAntic = true } }
+ { AnimationClip.SilkBombAntic, new RuneRage { IsAntic = true } },
+ { AnimationClip.AirSphereAntic, new PaleNails { IsAntic = true } }
};
private static readonly Dictionary SubAnimationEffects = new() {
diff --git a/SSMP/Animation/Effects/SilkSkills/PaleNails.cs b/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
new file mode 100644
index 00000000..55a77ab5
--- /dev/null
+++ b/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
@@ -0,0 +1,257 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using HutongGames.PlayMaker;
+using HutongGames.PlayMaker.Actions;
+using SSMP.Internals;
+using SSMP.Util;
+using UnityEngine;
+using Logger = SSMP.Logging.Logger;
+using Object = UnityEngine.Object;
+
+namespace SSMP.Animation.Effects.SilkSkills;
+
+internal class PaleNails : BaseSilkSkill {
+
+ private const string AnticName = "Hornet_finger_blade_cast_silk";
+
+ private const string NailName = "Hornet Finger Blade {0}";
+
+ private const int NailCount = 3;
+
+ public bool IsAntic = false;
+
+ private struct PlayerNails {
+ public GameObject[] Trailing;
+ public GameObject[] Firing;
+ }
+
+ private Dictionary _playerNails = new();
+
+ public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ var isVolt = IsVolt(effectInfo);
+ var isShaman = crestType == CrestType.Shaman;
+
+ MonoBehaviourUtil.Instance.StartCoroutine(PlayAntic(playerObject.gameObject, isVolt, isShaman));
+ }
+
+ private IEnumerator PlayAntic(GameObject playerObject, bool isVolt, bool isShaman) {
+ PlayHornetAttackSound(playerObject);
+
+ var fsm = GetSkillFSM();
+
+ // Play main antic
+ if (TryGetAntic(playerObject, out var antic)) {
+ antic.SetActive(false);
+ antic.SetActive(true);
+
+ var volt = antic
+ .FindGameObjectInChildren("offset")?
+ .FindGameObjectInChildren("zap");
+
+ if (volt) {
+ volt.SetActive(false);
+ volt.SetActive(isVolt);
+ }
+ }
+
+ // Play volt audio
+ if (isVolt) {
+ var voltAudio = fsm.GetFirstAction("Boss Needle Zap FX");
+ AudioUtil.PlayAudio(voltAudio, playerObject);
+ }
+
+ // Wait for animation to finish
+ yield return new WaitForSeconds(0.2f);
+
+ // Play summon audio
+ var needleAudio = fsm.GetFirstAction("BossNeedle Cast");
+ AudioUtil.PlayAudio(needleAudio, playerObject);
+
+ var localNail = fsm.GetFirstAction("BossNeedle Cast");
+
+ // Summon nails
+ var nails = new GameObject[NailCount];
+
+ for (var i = 0; i < NailCount; i++) {
+ var nail = EffectUtils.SpawnGlobalPoolObject(localNail, playerObject.transform, 10)!;
+ nail.name = string.Format(NailName, i);
+
+ nails[i] = nail;
+ }
+
+ // Set up FSMs for each nail
+ for (var i = 0; i < NailCount; i++) {
+ SetupNailFsm(playerObject, nails, i);
+ }
+
+ var id = playerObject.GetInstanceID();
+ if (!_playerNails.TryGetValue(id, out var playerNails)) {
+ playerNails = new PlayerNails();
+ }
+
+ playerNails.Trailing = nails;
+
+ _playerNails[id] = playerNails;
+ }
+
+ private void SetupNailFsm(GameObject playerObject, GameObject[] nails, int index) {
+ var nail = nails[index];
+ var fsm = nail.LocateMyFSM("Control");
+ if (fsm == null) return;
+
+ FixFsmForUse(fsm, playerObject);
+
+ string position;
+ GameObject buddy1;
+ GameObject buddy2;
+
+ if (index == 0) {
+ position = "TOP1";
+ buddy1 = nails[1];
+ buddy2 = nails[2];
+ } else if (index == 1) {
+ position = "MID1";
+ buddy1 = nails[0];
+ buddy2 = nails[2];
+ } else {
+ position = "BOT1";
+ buddy1 = nails[0];
+ buddy2 = nails[1];
+ }
+
+ fsm.Fsm.Variables.GetFsmGameObject("Buddy 1").Value = buddy1;
+ fsm.Fsm.Variables.GetFsmGameObject("Buddy 2").Value = buddy2;
+
+ fsm.Fsm.Event(position);
+ }
+
+ private bool FixFsmForUse(PlayMakerFSM fsm, GameObject playerObject) {
+ //fsm.Init();
+
+ fsm.enabled = false;
+
+ const string followLeftName = "Follow HeroFacingLeft";
+ const string followRightName = "Follow HeroFacingRight";
+
+ // Set FSM variables
+ var target = fsm.FsmVariables.FindFsmGameObject("Target");
+ target.Value = playerObject;
+
+ var wallTrueVar = new FsmFloat { Value = 1 };
+
+ var wallTrackCount = new FsmInt { Value = 0 };
+ var wallTrackTest = new FsmEnum { Value = Extensions.IntTest.LessThan };
+
+
+ // Set offset
+ if (playerObject.transform.GetScaleX() == -1) {
+ var setAngle = fsm.GetFirstAction("Set Top 1");
+ setAngle?.floatValue = 180 - setAngle.floatValue.Value;
+
+ setAngle = fsm.GetFirstAction("Set Mid 1");
+ setAngle?.floatValue = 180 - setAngle.floatValue.Value;
+
+ setAngle = fsm.GetFirstAction("Set Bot 1");
+ setAngle?.floatValue = 180 - setAngle.floatValue.Value;
+ }
+
+
+ // Set follow targets
+ var flyTo = fsm.GetFirstAction(followLeftName);
+ flyTo?.target = target;
+
+ flyTo = fsm.GetFirstAction(followRightName);
+ flyTo?.target = target;
+
+ // Set scale target
+ var getScale = fsm.GetAction(followLeftName, 4);
+ getScale?.gameObject.gameObject = target;
+
+ getScale = fsm.GetAction(followLeftName, 5);
+ getScale?.gameObject.gameObject = target;
+
+ getScale = fsm.GetAction(followRightName, 4);
+ getScale?.gameObject.gameObject = target;
+
+ getScale = fsm.GetAction(followRightName, 5);
+ getScale?.gameObject.gameObject = target;
+
+ // Remove wall checks
+ var wallCheck = fsm.GetAction(followLeftName, 7);
+ wallCheck?.trueValue = wallTrueVar;
+
+ wallCheck = fsm.GetAction(followRightName, 7);
+ wallCheck?.trueValue = wallTrueVar;
+
+ // Remove track trigger
+ var trackTrigger = fsm.GetAction(followLeftName, 12);
+
+ trackTrigger?.Count = wallTrackCount;
+ trackTrigger?.Test = wallTrackTest;
+
+ trackTrigger = fsm.GetAction(followRightName, 12);
+ trackTrigger?.Count = wallTrackCount;
+ trackTrigger?.Test = wallTrackTest;
+
+ // Remove transitions
+ var left = fsm.GetState(followLeftName);
+ left.Transitions = [
+ left.Transitions[0],
+ left.Transitions[5]
+ ];
+
+ var right = fsm.GetState(followRightName);
+ right.Transitions = [
+ right.Transitions[0],
+ right.Transitions[5],
+ ];
+
+ fsm.enabled = true;
+ return true;
+ }
+
+ private GameObject GetNailParent(GameObject playerObject) {
+ var attacks = GetPlayerSilkAttacks(playerObject);
+
+ const string parentName = "Pale Nails";
+ var nails = attacks.FindGameObjectInChildren(parentName);
+ if (nails == null) {
+ nails = new GameObject(parentName);
+ nails.transform.SetParentReset(attacks.transform);
+ }
+
+ return nails;
+ }
+
+ private bool TryGetAntic(GameObject playerObject, [MaybeNullWhen(false)] out GameObject antic) {
+ // Find existing first
+ var effects = playerObject.FindGameObjectInChildren("Effects");
+ if (effects == null) {
+ antic = null;
+ return false;
+ }
+
+ antic = effects.FindGameObjectInChildren(AnticName);
+ if (antic != null) {
+ return true;
+ }
+
+ var localAntic = HeroController.instance.gameObject
+ .FindGameObjectInChildren("Effects")?
+ .FindGameObjectInChildren(AnticName);
+
+ if (localAntic == null) {
+ return false;
+ }
+
+ antic = Object.Instantiate(localAntic, effects.transform);
+ antic.name = AnticName;
+
+ if (antic.TryGetComponent(out var checker)) {
+ Object.DestroyImmediate(checker);
+ }
+
+ return true;
+ }
+}
From 6a0eba2df235a3669139c6bb2836fa227012afee Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Fri, 17 Apr 2026 14:59:41 -0400
Subject: [PATCH 09/41] standardize component removal
---
SSMP/Animation/DamageAnimationEffect.cs | 8 +---
SSMP/Animation/Effects/Bind.cs | 2 -
SSMP/Animation/Effects/BindBurst.cs | 11 +----
SSMP/Animation/Effects/BindInterrupt.cs | 14 +------
SSMP/Animation/Effects/Death.cs | 5 +--
.../Effects/SilkSkills/CrossStitch.cs | 28 +++----------
.../Animation/Effects/SilkSkills/PaleNails.cs | 4 +-
SSMP/Animation/Effects/SilkSkills/RuneRage.cs | 23 +++--------
.../Animation/Effects/SilkSkills/SharpDart.cs | 10 +----
.../Animation/Effects/SilkSkills/SilkSpear.cs | 27 +++----------
.../Effects/SilkSkills/ThreadStorm.cs | 21 ++--------
SSMP/Util/GameObjectUtil.cs | 40 ++++++++++++++++++-
12 files changed, 69 insertions(+), 124 deletions(-)
diff --git a/SSMP/Animation/DamageAnimationEffect.cs b/SSMP/Animation/DamageAnimationEffect.cs
index aedc240d..811fb446 100644
--- a/SSMP/Animation/DamageAnimationEffect.cs
+++ b/SSMP/Animation/DamageAnimationEffect.cs
@@ -1,4 +1,5 @@
using SSMP.Internals;
+using SSMP.Util;
using UnityEngine;
using UnityEngine.Events;
@@ -47,12 +48,7 @@ protected static DamageHero AddDamageHeroComponent(GameObject target, int damage
///
/// The target game object to detach the component from.
protected static void RemoveDamageHeroComponent(GameObject target) {
- var damageHero = target.GetComponent();
- if (damageHero == null) {
- return;
- }
-
- Object.DestroyImmediate(damageHero);
+ target.DestroyComponent();
}
///
diff --git a/SSMP/Animation/Effects/Bind.cs b/SSMP/Animation/Effects/Bind.cs
index d3d9174d..e9619ecc 100644
--- a/SSMP/Animation/Effects/Bind.cs
+++ b/SSMP/Animation/Effects/Bind.cs
@@ -142,8 +142,6 @@ private void PlayNormalStart(GameObject bindEffects, Flags flags) {
var bindSilkAnimator = bindSilkObj.GetComponent();
- Logger.Info("Playing Bind Silk animation");
-
if (flags.QuickBind) {
bindSilkAnimator.Play(bindSilkAnimator.GetClipByName("Bind Silk Quick"));
} else {
diff --git a/SSMP/Animation/Effects/BindBurst.cs b/SSMP/Animation/Effects/BindBurst.cs
index 193caa5a..3416720d 100644
--- a/SSMP/Animation/Effects/BindBurst.cs
+++ b/SSMP/Animation/Effects/BindBurst.cs
@@ -103,11 +103,7 @@ private void PlayMaggotCleanse(GameObject bindEffects, GameObject playerObject)
}
// Remove camera and haze components
- var shaker = mirror.GetComponentInChildren();
- if (shaker != null) {
- Object.DestroyImmediate(shaker);
- }
-
+ mirror.DestroyComponentsInChildren();
mirror.DestroyGameObjectInChildren("haze2");
return mirror;
@@ -201,10 +197,7 @@ private void PlayWitchEnd(GameObject bindEffects) {
// Remove camera controls if object was created
if (effectWasCreated) {
- var shaker = witchBind.GetComponent();
- if (shaker != null) {
- Object.DestroyImmediate(shaker);
- }
+ witchBind.DestroyComponent();
}
// Toggle damage depending on if PVP is on or not
diff --git a/SSMP/Animation/Effects/BindInterrupt.cs b/SSMP/Animation/Effects/BindInterrupt.cs
index fe8b9798..4c82e41f 100644
--- a/SSMP/Animation/Effects/BindInterrupt.cs
+++ b/SSMP/Animation/Effects/BindInterrupt.cs
@@ -56,19 +56,13 @@ private void PlayBindInterrupt(GameObject bindEffects) {
// Remove haze and camera controls
burst.DestroyGameObjectInChildren("haze2");
-
- var shaker = burst.GetComponentInChildren();
- if (shaker != null) {
- Object.DestroyImmediate(shaker);
- }
+ burst.DestroyComponentsInChildren();
}
///
/// Creates a Warding Bell explosion
///
private void PlayBellExplode(GameObject bindEffects) {
- Logger.Debug("Playing Bell Burst");
-
// Locate warding bell FSM
var bellFsm = HeroController.instance.bellBindFSM;
@@ -92,11 +86,7 @@ private void PlayBellExplode(GameObject bindEffects) {
// Remove camera control and haze
- var shaker = bindBell.GetComponentInChildren();
- if (shaker != null) {
- Object.DestroyImmediate(shaker);
- }
-
+ bindBell.DestroyComponentsInChildren();
bindBell.DestroyGameObjectInChildren("haze2 (1)");
// Add hitbox if appropriate
diff --git a/SSMP/Animation/Effects/Death.cs b/SSMP/Animation/Effects/Death.cs
index 3f726b25..06303296 100644
--- a/SSMP/Animation/Effects/Death.cs
+++ b/SSMP/Animation/Effects/Death.cs
@@ -97,9 +97,8 @@ private static IEnumerator PlayFrostDeath(GameObject playerObject) {
playerObject.transform,
5
);
- if (destroyEffects != null && destroyEffects.TryGetComponent(out var shaker)) {
- Object.DestroyImmediate(shaker);
- }
+
+ destroyEffects?.DestroyComponent();
// Hide the player. They're shown as a separate frosted object in the next step
HidePlayer(playerObject);
diff --git a/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs b/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs
index 4c477682..99cb4acd 100644
--- a/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs
+++ b/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs
@@ -151,16 +151,8 @@ private static bool TryGetStanceFlash(GameObject playerObject, [MaybeNullWhen(fa
flash = flashObj;
if (!created) return true;
- var runes = flash.GetComponentsInChildren();
- foreach (var rune in runes) {
- Object.DestroyImmediate(rune);
- }
-
- var runeGlow = flash.FindGameObjectInChildren("Shaman Flash Glow");
-
- if (runeGlow) {
- Object.DestroyImmediate(runeGlow);
- }
+ flash.DestroyComponentsInChildren();
+ flash.DestroyGameObjectInChildren("Shaman Flash Glow");
return true;
}
@@ -175,10 +167,7 @@ private static bool TryGetClashEffect(GameObject playerObject, [MaybeNullWhen(fa
clash = clashObj;
if (!created) return true;
- var runes = clash.GetComponentsInChildren();
- foreach (var rune in runes) {
- Object.DestroyImmediate(rune);
- }
+ clash.DestroyComponentsInChildren();
return true;
}
@@ -210,15 +199,8 @@ private static bool TryGetSlashEffect(GameObject playerObject, bool isZap, [Mayb
slash.name = name;
slash.transform.localPosition = new Vector3(1, 1, 0);
- var runes = slash.GetComponentsInChildren();
- foreach (var rune in runes) {
- Object.DestroyImmediate(rune);
- }
-
- var haze = slash.FindGameObjectInChildren("haze2");
- if (haze != null) {
- Object.Destroy(haze);
- }
+ slash.DestroyComponentsInChildren();
+ slash.DestroyGameObjectInChildren("haze2");
var runeFlash = slash
.FindGameObjectInChildren("Runes")?
diff --git a/SSMP/Animation/Effects/SilkSkills/PaleNails.cs b/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
index 55a77ab5..5cbd9a87 100644
--- a/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
+++ b/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
@@ -248,9 +248,7 @@ private bool TryGetAntic(GameObject playerObject, [MaybeNullWhen(false)] out Gam
antic = Object.Instantiate(localAntic, effects.transform);
antic.name = AnticName;
- if (antic.TryGetComponent(out var checker)) {
- Object.DestroyImmediate(checker);
- }
+ antic.DestroyComponent();
return true;
}
diff --git a/SSMP/Animation/Effects/SilkSkills/RuneRage.cs b/SSMP/Animation/Effects/SilkSkills/RuneRage.cs
index 80236811..2da2568b 100644
--- a/SSMP/Animation/Effects/SilkSkills/RuneRage.cs
+++ b/SSMP/Animation/Effects/SilkSkills/RuneRage.cs
@@ -154,10 +154,7 @@ private void PlaySonar(GameObject playerObject, bool isVolt, bool isShaman) {
// Spawn and remove components
var sonarParent = EffectUtils.SpawnGlobalPoolObject(localSonar, playerObject.transform, 2f);
if (sonarParent != null) {
- var cam = sonarParent.GetComponentInChildren();
- if (cam != null) {
- Object.DestroyImmediate(cam);
- }
+ sonarParent.DestroyComponentsInChildren();
}
if (!isShaman) return;
@@ -169,13 +166,8 @@ private void PlaySonar(GameObject playerObject, bool isVolt, bool isShaman) {
var shaman = EffectUtils.SpawnGlobalPoolObject(localShaman, playerObject.transform, 0.7f);
if (shaman == null) return;
- if (shaman.TryGetComponent(out var rune)) {
- Object.DestroyImmediate(rune);
- }
-
- if (shaman.TryGetComponent(out var camEffect)) {
- Object.DestroyImmediate(camEffect);
- }
+ shaman.DestroyComponent();
+ shaman.DestroyComponent();
}
///
@@ -231,9 +223,7 @@ private static bool TryGetAntic(GameObject playerObject, [MaybeNullWhen(false)]
if (!created) return true;
// Remove problematic component from newly created object
- if (antic.TryGetComponent(out var checker)) {
- Object.DestroyImmediate(checker);
- }
+ antic.DestroyComponent();
return true;
}
@@ -360,10 +350,7 @@ private void CreateBlast(Transform spawnTransform, bool isVolt, bool isShaman, V
}
// Remove camera controller
- var cam = blast.GetComponentInChildren();
- if (cam) {
- cam.enabled = false;
- }
+ blast.DestroyComponentsInChildren();
// Cull while offscreen (fixes bug)
var animator = blast.GetComponentInChildren();
diff --git a/SSMP/Animation/Effects/SilkSkills/SharpDart.cs b/SSMP/Animation/Effects/SilkSkills/SharpDart.cs
index 7ef63e8d..b5162106 100644
--- a/SSMP/Animation/Effects/SilkSkills/SharpDart.cs
+++ b/SSMP/Animation/Effects/SilkSkills/SharpDart.cs
@@ -113,15 +113,9 @@ private bool TryGetDamager(GameObject playerObject, [MaybeNullWhen(false)] out G
var delay = damager.AddComponent();
delay.time = 0.3f;
- var worm = damager.FindGameObjectInChildren("Worm Worrier");
- if (worm) {
- Object.Destroy(worm);
- }
+ damager.DestroyGameObjectInChildren("Worm Worrier");
- var runes = damager.GetComponentsInChildren();
- foreach (var rune in runes) {
- Object.DestroyImmediate(rune);
- }
+ damager.DestroyComponentsInChildren();
var runeBloom = damager
.FindGameObjectInChildren("Shaman Rune")?
diff --git a/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs b/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
index 82e1ee72..f0f1a153 100644
--- a/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
+++ b/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
@@ -95,31 +95,14 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
spear = Object.Instantiate(localSpear, silkAttacks.transform);
spear.name = SpearObjectName;
- if (spear.TryGetComponent(out var toolChecker)) {
- Object.DestroyImmediate(toolChecker);
- }
-
-
- // Remove potentially disruptive components
- var runes = spear.GetComponentsInChildren();
- foreach (var rune in runes) {
- Object.DestroyImmediate(rune);
- }
-
- var camFollows = spear.GetComponentsInChildren();
- foreach (var camFollow in camFollows) {
- Object.Destroy(camFollow);
- }
+ spear.DestroyComponent();
+ spear.DestroyComponentsInChildren();
+ spear.DestroyComponentsInChildren();
var child = spear.FindGameObjectInChildren("needle_throw_simple");
if (child) {
- if (child.TryGetComponent(out var cam)) {
- Object.DestroyImmediate(cam);
- }
-
- if (child.TryGetComponent(out var anim)) {
- Object.DestroyImmediate(anim);
- }
+ child.DestroyComponent();
+ child.DestroyComponent();
var bloom1 = child
.FindGameObjectInChildren("Rune Effect Activator")?
diff --git a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
index 9ef2c8b6..b7d2ea5e 100644
--- a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
+++ b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
@@ -190,24 +190,11 @@ private static bool TryGetThreadStorm(
threadStorm = Object.Instantiate(localStorm, parent.transform);
threadStorm.name = SkillObjectName;
- // Remove FSM
- if (threadStorm.TryGetComponent(out var fsm)) {
- Object.Destroy(fsm);
- }
-
// Remove components that could interfere
- if (threadStorm.TryGetComponent(out var globalRuneEffect)) {
- Object.DestroyImmediate(globalRuneEffect);
- }
-
- if (threadStorm.TryGetComponent(out var checker)) {
- Object.DestroyImmediate(checker);
- }
-
- if (threadStorm.TryGetComponent(out var register)) {
- Object.DestroyImmediate(register);
- }
-
+ threadStorm.DestroyComponent();
+ threadStorm.DestroyComponent();
+ threadStorm.DestroyComponent();
+ threadStorm.DestroyComponent();
// Set up shaman crest effects
var shamanRune = threadStorm.FindGameObjectInChildren("Shaman Rune");
diff --git a/SSMP/Util/GameObjectUtil.cs b/SSMP/Util/GameObjectUtil.cs
index a1a54bcf..af167518 100644
--- a/SSMP/Util/GameObjectUtil.cs
+++ b/SSMP/Util/GameObjectUtil.cs
@@ -70,7 +70,45 @@ public static List GetChildren(this GameObject gameObject) {
return children;
}
-
+
+ ///
+ /// Attempts to remove a component from a given GameObject
+ ///
+ /// The component type to remove
+ /// The object to remove the component from
+ /// true if the component was removed
+ public static bool DestroyComponent(this GameObject gameObject) where T : Component {
+ if (gameObject == null) {
+ return false;
+ }
+
+ if (gameObject.TryGetComponent(out var component)) {
+ Object.DestroyImmediate(component);
+ return true;
+ }
+
+ return false;
+ }
+
+ ///
+ /// Attempts to remove all components of a given type from a GameObject and all of its children
+ ///
+ /// The type of component to remove
+ /// The parent object
+ /// true if any components were removed
+ public static bool DestroyComponentsInChildren(this GameObject gameObject) where T : Component {
+ if (gameObject == null) {
+ return false;
+ }
+
+ var components = gameObject.GetComponentsInChildren();
+ foreach (var component in components) {
+ Object.DestroyImmediate(component);
+ }
+
+ return components.Length > 0;
+ }
+
///
/// Find an inactive GameObject with the given name.
///
From 39f780a79386ede1474db5b2f8d2a81c7d947bfc Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Fri, 17 Apr 2026 15:02:09 -0400
Subject: [PATCH 10/41] standardize object destruction
---
SSMP/Animation/Effects/SilkSkills/CrossStitch.cs | 5 +----
SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs | 3 +--
2 files changed, 2 insertions(+), 6 deletions(-)
diff --git a/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs b/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs
index 99cb4acd..c2285c0a 100644
--- a/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs
+++ b/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs
@@ -129,10 +129,7 @@ private static bool TryGetParryThread(GameObject playerObject, bool zap, [MaybeN
if (!created) return true;
if (zap) {
- var bloom = thread.FindGameObjectInChildren("light_effect_v02 (2)");
- if (bloom) {
- Object.Destroy(bloom);
- }
+ thread.DestroyGameObjectInChildren("light_effect_v02 (2)");
thread.SetActiveChildren(true);
thread.SetActive(false);
diff --git a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
index b7d2ea5e..2d513175 100644
--- a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
+++ b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
@@ -214,8 +214,7 @@ private static bool TryGetThreadStorm(
Object.DestroyImmediate(runeEffect);
}
- var bloom = shamanRune.FindGameObjectInChildren("Shaman Rune Camera Bloom");
- if (bloom) Object.DestroyImmediate(bloom);
+ shamanRune.DestroyGameObjectInChildren("Shaman Rune Camera Bloom");
}
// Set up scale animation. It plays when enabled.
From 59996c4bbd2ff6aa8cc517866ff97790477ba36f Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Fri, 17 Apr 2026 18:30:09 -0400
Subject: [PATCH 11/41] Ensure fsm injections are applied to all copies of
nails
---
SSMP/Animation/AnimationManager.cs | 97 ++++++++++++++++++-
.../Animation/Effects/SilkSkills/PaleNails.cs | 21 ++--
SSMP/Fsm/FsmActionInjectorComponent.cs | 93 ++++++++++++++++++
SSMP/Fsm/FsmStateActionInjector.cs | 29 ++++--
4 files changed, 217 insertions(+), 23 deletions(-)
create mode 100644 SSMP/Fsm/FsmActionInjectorComponent.cs
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index 15c7a423..8d76164b 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -1113,10 +1113,11 @@ private void CreateHeroHooksHooks(HeroController hc) {
return;
}
+ // Thread strom
var threadStormExtend = silkSkillFsm.GetState("Extend");
FsmStateActionInjector.Inject(threadStormExtend, OnThreadStormExtend, 0);
- // Rune Rage injections
+ // Rune Rage
var sonarBuildArray = silkSkillFsm.GetState("Build Enemy Array");
FsmStateActionInjector.Inject(sonarBuildArray, OnBuildRuneRageArray, 0);
@@ -1128,6 +1129,47 @@ private void CreateHeroHooksHooks(HeroController hc) {
var blastFinished = silkSkillFsm.GetState("Silk Bomb Recover");
FsmStateActionInjector.Inject(blastFinished, OnBlastFinished, 0);
+
+ // Pale Nails
+ var nailObject = silkSkillFsm.GetAction("BossNeedle Cast", 5)?.gameObject.Value;
+ var nailFsm = nailObject?.LocateMyFSM("Control");
+ if (nailObject == null || nailFsm == null) {
+ Logger.Warn("Unable to find Pale Nail FSM to hook.");
+ return;
+ }
+
+ // Find all existing pale nails
+ List nails = [
+ nailObject
+ ];
+
+ if (ObjectPool.instance.pooledObjects.TryGetValue(nailObject, out var globalPoolNails)) {
+ nails.AddRange(globalPoolNails);
+ }
+
+ // Add injector components to all current instances of nails
+ foreach (var nail in nails) {
+ var injector = nail.AddComponent();
+ List injections = [
+ new FsmActionInjectorComponent.Injection {
+ FsmName = nailFsm.FsmName,
+ ActionIndex = 12,
+ FsmStateName = "Follow HeroFacingLeft",
+ Hook = OnPaleNailAttackCheck,
+ HookName = "Nail Attack"
+ },
+
+ new FsmActionInjectorComponent.Injection {
+ FsmName = nailFsm.FsmName,
+ ActionIndex = 12,
+ FsmStateName = "Follow HeroFacingRight",
+ Hook = OnPaleNailAttackCheck,
+ HookName = "Nail Attack"
+ }
+ ];
+
+ injector.SetInjections(injections);
+ }
}
///
@@ -1271,6 +1313,59 @@ private void OnBlastFinished(PlayMakerFSM fsm) {
_netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.SilkBombLocations, 0, effectInfo.ToArray());
}
+ ///
+ /// Influences Pail Nails to be able to target players that can be attacked
+ ///
+ private void OnPaleNailAttackCheck(PlayMakerFSM fsm) {
+ // If we are not connected, there is nothing to send to
+ if (!_netClient.IsConnected) {
+ return;
+ }
+
+ var sonarObject = fsm.gameObject;
+
+ // Get needed components
+ var sonar = sonarObject.GetComponentInChildren();
+ if (sonar == null) return;
+
+ var sonarCollider = sonarObject.GetComponentInChildren();
+ if (sonarCollider == null) return;
+
+ // Remove any old player objects
+ sonar.Refresh();
+
+ // Don't bother targeting if PvP is off
+ if (!_serverSettings.IsPvpEnabled) {
+ return;
+ }
+
+ var radius = sonarCollider.radius * sonar.transform.GetScaleX();
+ var inSonar = sonar.insideObjectsList;
+
+ // Find all players within sonar
+ foreach (var player in _playerData.Values) {
+ if (!player.IsInLocalScene || !player.PlayerObject) {
+ continue;
+ }
+
+ // Don't bother to target players on same team
+ if (_serverSettings.TeamsEnabled && player.Team == _playerManager.LocalPlayerTeam && player.Team != Team.None) {
+ continue;
+ }
+
+ var collider = player.PlayerObject.GetComponent();
+ if (!collider) continue;
+
+ // Determine if the player is within the sonar circle and is not obstructed
+ var closestPlayerPoint = collider.ClosestPoint(sonarCollider.transform.position);
+ var distanceFromCenter = Vector2.Distance(closestPlayerPoint, sonarCollider.transform.position);
+
+ if (distanceFromCenter <= radius && sonar.IsCounted(collider.gameObject)) {
+ inSonar.AddIfNotPresent(player.PlayerObject);
+ }
+ }
+ }
+
// ///
// /// Callback method on the HeroAnimationController#Play method.
// ///
diff --git a/SSMP/Animation/Effects/SilkSkills/PaleNails.cs b/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
index 5cbd9a87..9954e336 100644
--- a/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
+++ b/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
@@ -3,6 +3,7 @@
using System.Diagnostics.CodeAnalysis;
using HutongGames.PlayMaker;
using HutongGames.PlayMaker.Actions;
+using SSMP.Fsm;
using SSMP.Internals;
using SSMP.Util;
using UnityEngine;
@@ -184,6 +185,13 @@ private bool FixFsmForUse(PlayMakerFSM fsm, GameObject playerObject) {
wallCheck = fsm.GetAction(followRightName, 7);
wallCheck?.trueValue = wallTrueVar;
+ // Remove hook
+ var hook = fsm.GetAction(followLeftName, 12);
+ hook?.Uninject();
+
+ hook = fsm.GetAction(followRightName, 12);
+ hook?.Uninject();
+
// Remove track trigger
var trackTrigger = fsm.GetAction(followLeftName, 12);
@@ -211,19 +219,6 @@ private bool FixFsmForUse(PlayMakerFSM fsm, GameObject playerObject) {
return true;
}
- private GameObject GetNailParent(GameObject playerObject) {
- var attacks = GetPlayerSilkAttacks(playerObject);
-
- const string parentName = "Pale Nails";
- var nails = attacks.FindGameObjectInChildren(parentName);
- if (nails == null) {
- nails = new GameObject(parentName);
- nails.transform.SetParentReset(attacks.transform);
- }
-
- return nails;
- }
-
private bool TryGetAntic(GameObject playerObject, [MaybeNullWhen(false)] out GameObject antic) {
// Find existing first
var effects = playerObject.FindGameObjectInChildren("Effects");
diff --git a/SSMP/Fsm/FsmActionInjectorComponent.cs b/SSMP/Fsm/FsmActionInjectorComponent.cs
new file mode 100644
index 00000000..04e5c493
--- /dev/null
+++ b/SSMP/Fsm/FsmActionInjectorComponent.cs
@@ -0,0 +1,93 @@
+using System;
+using System.Collections.Generic;
+using SSMP.Util;
+using UnityEngine;
+
+namespace SSMP.Fsm;
+
+///
+/// A component to copy FSM injections between objects. An instantiated object can't copy Actions, so this bridges the gap.
+///
+internal class FsmActionInjectorComponent : MonoBehaviour {
+ private static Dictionary> _allInjections = [];
+
+ private List _injections = [];
+
+ private bool _injected = false;
+
+ [SerializeField]
+ private int _injectionIndex;
+
+ public void Awake() {
+ _injected = false;
+
+ if (_allInjections.TryGetValue(_injectionIndex, out var injections)) {
+ _injections = injections;
+ TryDoInjection();
+ }
+ }
+
+ ///
+ /// Sets the injections for this component and any future copies of it
+ ///
+ /// The injections for this Object and any future copies
+ public void SetInjections(List injections) {
+ // Set injections and index
+ _injections = injections;
+ _injectionIndex = _allInjections.Count + 1;
+
+ // Add to static collection of injections and inject
+ _allInjections.Add(_injectionIndex, injections);
+ TryDoInjection();
+ }
+
+ private void TryDoInjection() {
+ // Check if injections need to happen
+ if (_injected) return;
+ if (_injections.Count == 0) return;
+
+ foreach (var injection in _injections) {
+ // Ensure injection is set up correctly
+ if (injection.FsmName == null) return;
+ if (injection.Hook == null) return;
+ if (injection.FsmStateName == null) return;
+
+ // Locate and patch the FSM
+ var fsm = gameObject.LocateMyFSM(injection.FsmName);
+ var state = fsm.GetState(injection.FsmStateName);
+
+ FsmStateActionInjector.Inject(state, injection.Hook, injection.ActionIndex, injection.HookName);
+ }
+
+ _injected = true;
+ }
+
+ [Serializable]
+ public class Injection {
+ ///
+ /// The name of the FSM to inject
+ ///
+ public required string FsmName;
+
+ ///
+ /// An optional name for the hook
+ ///
+ public string HookName = "Fsm Injection";
+
+ ///
+ /// The state on the FSM to inject
+ ///
+ public required string FsmStateName;
+
+ ///
+ /// The index to place the hook action
+ ///
+ public int ActionIndex;
+
+ ///
+ /// The hook to run when the action index is reached
+ ///
+ [NonSerialized]
+ public required Action Hook;
+ }
+}
diff --git a/SSMP/Fsm/FsmStateActionInjector.cs b/SSMP/Fsm/FsmStateActionInjector.cs
index 034ffb9e..f26750ab 100644
--- a/SSMP/Fsm/FsmStateActionInjector.cs
+++ b/SSMP/Fsm/FsmStateActionInjector.cs
@@ -7,18 +7,24 @@ namespace SSMP.Fsm;
internal sealed class FsmStateActionInjector : FsmStateAction {
private static Action? _onUninject;
+
private Action? _onStateEnter;
- private FsmStateActionInjector(FsmState state, Action onEnter) {
- Init(state);
- _onStateEnter = onEnter;
- _onUninject += Uninject;
- }
///
/// Injects a delegate action into an FSM state
///
private void DoInjection(int index) {
var stateActions = State.Actions.ToList();
+
+ if (index < stateActions.Count) {
+ // Replace existing hooks with this one
+ var atIndex = stateActions[index];
+ if (atIndex is FsmStateActionInjector injector && injector.Name == Name) {
+ injector._onStateEnter = _onStateEnter;
+ return;
+ }
+ }
+
stateActions.Insert(index, this);
State.Actions = stateActions.ToArray();
State.SaveActions();
@@ -40,8 +46,8 @@ public void Uninject() {
///
public override void OnEnter() {
if (_onStateEnter != null) {
- try {
- _onStateEnter.Invoke(Fsm.FsmComponent);
+ try {
+ _onStateEnter?.Invoke(Fsm.FsmComponent);
} catch (Exception e) {
Logger.Error(e.ToString());
}
@@ -55,13 +61,18 @@ public override void OnEnter() {
/// The FSM state into which the action will be injected.
/// An action to execute when the state is entered.
/// The index at which to inject the action within the state's action list. Defaults to 0.
+ /// A unique name for the injected FSM Action
/// The injected action.
- public static FsmStateActionInjector Inject(FsmState state, Action onEnter, int actionIndex = 0) {
+ public static FsmStateActionInjector Inject(FsmState state, Action onEnter, int actionIndex = 0, string name = "Fsm Injection") {
if (state == null) {
throw new NullReferenceException("Received null state when injecting FSM");
}
- var action = new FsmStateActionInjector(state, onEnter);
+ var action = new FsmStateActionInjector {
+ State = state,
+ _onStateEnter = onEnter,
+ Name = name
+ };
action.DoInjection(actionIndex);
return action;
From 238cd2240dfa74cc5c1099c578f4d45da1177123 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Fri, 17 Apr 2026 23:40:31 -0400
Subject: [PATCH 12/41] finish pale nails
---
SSMP/Animation/AnimationClip.cs | 1 +
SSMP/Animation/AnimationManager.cs | 39 +-
.../Animation/Effects/SilkSkills/PaleNails.cs | 389 +++++++++++++++---
SSMP/Util/AudioUtil.cs | 22 +
4 files changed, 400 insertions(+), 51 deletions(-)
diff --git a/SSMP/Animation/AnimationClip.cs b/SSMP/Animation/AnimationClip.cs
index a190c8b1..0f6a9604 100644
--- a/SSMP/Animation/AnimationClip.cs
+++ b/SSMP/Animation/AnimationClip.cs
@@ -143,6 +143,7 @@ internal enum AnimationClip {
AirSphereRepeatAntic,
SilkBossNeedleCast,
+ SilkBossNeedleFire,
Taunt,
TauntBack,
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index 8d76164b..e12c2617 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -178,6 +178,7 @@ internal class AnimationManager {
{ "AirSphere Dissipate", AnimationClip.AirSphereDissipate },
{ "AirSphere RepeatAntic", AnimationClip.AirSphereRepeatAntic },
{ "Silk Boss Needle Cast", AnimationClip.SilkBossNeedleCast },
+ { "Silk Boss Needle Fire", AnimationClip.SilkBossNeedleFire },
{ "Taunt", AnimationClip.Taunt },
{ "Taunt Back", AnimationClip.TauntBack },
{ "Taunt Back End", AnimationClip.TauntBackEnd },
@@ -679,7 +680,8 @@ internal class AnimationManager {
{ AnimationClip.ShamanCancel, new Bind { BindState = Bind.State.ShamanCancel } },
{ AnimationClip.BindInterrupt, BindInterrupt.Instance },
{ AnimationClip.AirSphereRefresh, new ThreadStorm() },
- { AnimationClip.SilkBombLocations, new RuneRage() }
+ { AnimationClip.SilkBombLocations, new RuneRage() },
+ { AnimationClip.SilkBossNeedleFire, new PaleNails() }
};
///
@@ -1153,18 +1155,26 @@ private void CreateHeroHooksHooks(HeroController hc) {
List injections = [
new FsmActionInjectorComponent.Injection {
FsmName = nailFsm.FsmName,
- ActionIndex = 12,
FsmStateName = "Follow HeroFacingLeft",
+ ActionIndex = 12,
Hook = OnPaleNailAttackCheck,
- HookName = "Nail Attack"
+ HookName = "Nail Target"
},
new FsmActionInjectorComponent.Injection {
FsmName = nailFsm.FsmName,
- ActionIndex = 12,
FsmStateName = "Follow HeroFacingRight",
+ ActionIndex = 12,
Hook = OnPaleNailAttackCheck,
- HookName = "Nail Attack"
+ HookName = "Nail Target"
+ },
+
+ new FsmActionInjectorComponent.Injection {
+ FsmName = nailFsm.FsmName,
+ FsmStateName = "Fire Antic",
+ ActionIndex = 0,
+ Hook = OnPaleNailFire,
+ HookName = "Nail Fire"
}
];
@@ -1366,6 +1376,25 @@ private void OnPaleNailAttackCheck(PlayMakerFSM fsm) {
}
}
+ ///
+ /// Sends an update to fire a set of nails at a specific target
+ ///
+ private void OnPaleNailFire(PlayMakerFSM fsm) {
+ // Only get info from one needle position (the first one)
+ var needleOffset = fsm.FsmVariables.GetFsmVector3("Offset");
+ if (needleOffset.Value.x != 3) return;
+
+ // Don't send if no target, it'll go off on its own.
+ var target = fsm.FsmVariables.FindFsmGameObject("Target").Value;
+ if (target == null) {
+ return;
+ }
+
+ // Send nail target info
+ var effectInfo = PaleNails.EncodeTargetInfo(target);
+ _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.SilkBossNeedleFire, 0, effectInfo);
+ }
+
// ///
// /// Callback method on the HeroAnimationController#Play method.
// ///
diff --git a/SSMP/Animation/Effects/SilkSkills/PaleNails.cs b/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
index 9954e336..57460de3 100644
--- a/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
+++ b/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
@@ -1,13 +1,14 @@
+using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using System.Linq;
using HutongGames.PlayMaker;
using HutongGames.PlayMaker.Actions;
using SSMP.Fsm;
using SSMP.Internals;
using SSMP.Util;
using UnityEngine;
-using Logger = SSMP.Logging.Logger;
using Object = UnityEngine.Object;
namespace SSMP.Animation.Effects.SilkSkills;
@@ -16,32 +17,131 @@ internal class PaleNails : BaseSilkSkill {
private const string AnticName = "Hornet_finger_blade_cast_silk";
- private const string NailName = "Hornet Finger Blade {0}";
-
private const int NailCount = 3;
- public bool IsAntic = false;
+ private const int PositionOffset = short.MaxValue;
- private struct PlayerNails {
- public GameObject[] Trailing;
- public GameObject[] Firing;
- }
+ private const int PositionScale = 5;
- private Dictionary _playerNails = new();
+ public bool IsAntic = false;
+
+ private static Dictionary> _playerNails = new();
+ ///
public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
var isVolt = IsVolt(effectInfo);
var isShaman = crestType == CrestType.Shaman;
- MonoBehaviourUtil.Instance.StartCoroutine(PlayAntic(playerObject.gameObject, isVolt, isShaman));
+ // Play summon antic if appropriate
+ if (IsAntic) {
+ MonoBehaviourUtil.Instance.StartCoroutine(PlayAntic(playerObject.gameObject, isVolt, isShaman));
+ return;
+ }
+
+ // At this point, the firing animation will be played
+
+ // Get existing nails
+ var id = playerObject.GetInstanceID();
+ if (!_playerNails.TryGetValue(id, out var playerNails)) {
+ playerNails = [];
+ }
+
+ // No nails to fire
+ if (playerNails.Count == 0) {
+ return;
+ }
+
+ // Find first nails that aren't despawned
+ var nails = playerNails[0];
+ playerNails.RemoveAt(0);
+ while (nails[0] == null && playerNails.Count > 0) {
+ nails = playerNails[0];
+ playerNails.RemoveAt(0);
+ }
+
+ if (nails.Any(obj => obj == null)) {
+ return;
+ }
+
+ _playerNails[id] = playerNails;
+
+ // Decode target position. If can't decode, play the unguided variant
+ var targetInfo = DecodeTargetInfo(effectInfo);
+ if (!targetInfo.HasValue) {
+ PlayNailFireUnguided(nails, isVolt);
+ return;
+ }
+
+ // Fire at target position
+ var target = FindTarget(targetInfo.Value);
+ MonoBehaviourUtil.Instance.StartCoroutine(PlayNailFireTargeted(target, nails, isVolt));
}
- private IEnumerator PlayAntic(GameObject playerObject, bool isVolt, bool isShaman) {
- PlayHornetAttackSound(playerObject);
+ ///
+ /// Fires a set of nails at a given target
+ ///
+ /// The target object to fire at
+ /// The set of nails to fire
+ /// If the volt filament effect should be used
+ private static IEnumerator PlayNailFireTargeted(GameObject target, GameObject[] nails, bool isVolt) {
+ // Play audio
+ if (isVolt) PlayVoltAudio(nails[0]);
+
+ // Fire each nail
+ foreach (var nail in nails) {
+ // Nail has already been despawned
+ if (nail == null) {
+ yield break;
+ }
+
+ // Set the target
+ var fsm = nail.LocateMyFSM("Control");
+ fsm.FsmVariables.GetFsmGameObject("Target").Value = target;
+
+ // Send it at the target and wait a bit before sending the next
+ fsm.Fsm.Event("FOLLOW BUDDY");
+
+ yield return new WaitForSeconds(0.1f);
+ }
+ }
+
+ ///
+ /// Fires off a set of nails at their current positions
+ ///
+ /// The set of nails to fire
+ /// If the volt filament effect should be used
+ private static void PlayNailFireUnguided(GameObject[] nails, bool isVolt) {
+ // Play audio
+ if (isVolt) PlayVoltAudio(nails[0]);
+
+ // Fire each nail
+ foreach (var nail in nails) {
+ // Nail has already been despawned
+ if (nail == null) {
+ continue;
+ }
+ // Remove any target
+ var fsm = nail.LocateMyFSM("Control");
+ fsm.FsmVariables.GetFsmGameObject("Target").Value = null;
+
+ // Send it off into the world immediately
+ fsm.Fsm.Event("FOLLOW BUDDY");
+ }
+ }
+
+ ///
+ /// Plays the nail summoning antic effect
+ ///
+ /// The player that summoned the nails
+ /// If the volt filament effect should be used
+ /// If the shaman crest effects should be used
+ ///
+ private IEnumerator PlayAntic(GameObject playerObject, bool isVolt, bool isShaman) {
var fsm = GetSkillFSM();
// Play main antic
+ PlayHornetAttackSound(playerObject);
if (TryGetAntic(playerObject, out var antic)) {
antic.SetActive(false);
antic.SetActive(true);
@@ -75,38 +175,104 @@ private IEnumerator PlayAntic(GameObject playerObject, bool isVolt, bool isShama
var nails = new GameObject[NailCount];
for (var i = 0; i < NailCount; i++) {
+ // Spawn it in
var nail = EffectUtils.SpawnGlobalPoolObject(localNail, playerObject.transform, 10)!;
- nail.name = string.Format(NailName, i);
+ nail.transform.localScale = Vector3.one;
+
+ // Set the damage state
+ var damager = nail.FindGameObjectInChildren("Enemy Damager");
+ if (damager) {
+ SetDamageHeroState(damager, 1);
+ }
+
+ // Remove interfering components
+ nail.DestroyComponentsInChildren();
+ nail.DestroyComponent();
+ nail.DestroyComponent();
+
+ // Remove shaman effects (completely if not shaman crest)
+ var shaman = nail
+ .FindGameObjectInChildren("Sprite")?
+ .FindGameObjectInChildren("Rune Parent");
+
+ if (shaman) {
+ if (isShaman) {
+ shaman.SetActiveChildren(true);
+
+ var shamanSpawn = shaman.FindGameObjectInChildren("Shaman Rune Spawn");
+ shamanSpawn?.DestroyGameObjectInChildren("Shaman Rune Camera Bloom");
+
+ var shamanFire = shaman.FindGameObjectInChildren("Shaman Rune Fire");
+ shamanFire?.DestroyGameObjectInChildren("Shaman Rune Camera Bloom");
+ } else {
+ Object.Destroy(shaman);
+ }
+ }
nails[i] = nail;
}
// Set up FSMs for each nail
for (var i = 0; i < NailCount; i++) {
- SetupNailFsm(playerObject, nails, i);
+ SetupNailFSM(playerObject, nails, i, isVolt);
}
+ // Store nails for firing later
var id = playerObject.GetInstanceID();
if (!_playerNails.TryGetValue(id, out var playerNails)) {
- playerNails = new PlayerNails();
+ playerNails = [];
}
- playerNails.Trailing = nails;
+ playerNails.Add(nails);
_playerNails[id] = playerNails;
+
+ // Wait for nail hover time to expire
+ yield return new WaitForSeconds(1.8f);
+
+ // Nails may have been fired, don't refire
+ if (!_playerNails[id].Contains(nails)) yield break;
+
+ // Remove from being able to fire
+ _playerNails[id].Remove(nails);
+
+ // Fire em'
+ PlayNailFireUnguided(nails, isVolt);
+ }
+
+ ///
+ /// Plays volt audio when firing
+ ///
+ /// The nail to play the sound on
+ private static void PlayVoltAudio(GameObject nail) {
+ if (nail == null) return;
+
+ // Play audio
+ var fsm = nail.LocateMyFSM("Control");
+ var audio = fsm.GetFirstAction("Zap FX");
+ if (audio != null) AudioUtil.PlayAudio(audio, nail);
}
- private void SetupNailFsm(GameObject playerObject, GameObject[] nails, int index) {
+ ///
+ /// Sets FSM variables to help them fly smoothly
+ ///
+ /// The player that summoned the nails
+ /// All nails in the set, for reference
+ /// The index of the nail in the set
+ /// If the volt filament effect should be used
+ private static void SetupNailFSM(GameObject playerObject, GameObject[] nails, int index, bool isVolt) {
+ // Fix the FSM
var nail = nails[index];
var fsm = nail.LocateMyFSM("Control");
if (fsm == null) return;
- FixFsmForUse(fsm, playerObject);
+ FixFsmForUse(fsm, playerObject, isVolt);
string position;
GameObject buddy1;
GameObject buddy2;
+ // Set nail buddy and event
if (index == 0) {
position = "TOP1";
buddy1 = nails[1];
@@ -127,56 +293,44 @@ private void SetupNailFsm(GameObject playerObject, GameObject[] nails, int index
fsm.Fsm.Event(position);
}
- private bool FixFsmForUse(PlayMakerFSM fsm, GameObject playerObject) {
- //fsm.Init();
-
+ ///
+ /// Changes the control FSM for use with a non-local player.
+ ///
+ /// The FSM of the nail
+ /// The player that summoned the nail
+ /// If the volt filament effect should be used
+ private static void FixFsmForUse(PlayMakerFSM fsm, GameObject playerObject, bool isVolt) {
fsm.enabled = false;
const string followLeftName = "Follow HeroFacingLeft";
const string followRightName = "Follow HeroFacingRight";
// Set FSM variables
- var target = fsm.FsmVariables.FindFsmGameObject("Target");
- target.Value = playerObject;
+ var hero = new FsmGameObject { Value = playerObject };
var wallTrueVar = new FsmFloat { Value = 1 };
-
var wallTrackCount = new FsmInt { Value = 0 };
var wallTrackTest = new FsmEnum { Value = Extensions.IntTest.LessThan };
-
- // Set offset
- if (playerObject.transform.GetScaleX() == -1) {
- var setAngle = fsm.GetFirstAction("Set Top 1");
- setAngle?.floatValue = 180 - setAngle.floatValue.Value;
-
- setAngle = fsm.GetFirstAction("Set Mid 1");
- setAngle?.floatValue = 180 - setAngle.floatValue.Value;
-
- setAngle = fsm.GetFirstAction("Set Bot 1");
- setAngle?.floatValue = 180 - setAngle.floatValue.Value;
- }
-
-
// Set follow targets
var flyTo = fsm.GetFirstAction(followLeftName);
- flyTo?.target = target;
+ flyTo?.target = hero;
flyTo = fsm.GetFirstAction(followRightName);
- flyTo?.target = target;
+ flyTo?.target = hero;
// Set scale target
var getScale = fsm.GetAction(followLeftName, 4);
- getScale?.gameObject.gameObject = target;
+ getScale?.gameObject.gameObject = hero;
getScale = fsm.GetAction(followLeftName, 5);
- getScale?.gameObject.gameObject = target;
+ getScale?.gameObject.gameObject = hero;
getScale = fsm.GetAction(followRightName, 4);
- getScale?.gameObject.gameObject = target;
+ getScale?.gameObject.gameObject = hero;
getScale = fsm.GetAction(followRightName, 5);
- getScale?.gameObject.gameObject = target;
+ getScale?.gameObject.gameObject = hero;
// Remove wall checks
var wallCheck = fsm.GetAction(followLeftName, 7);
@@ -185,13 +339,16 @@ private bool FixFsmForUse(PlayMakerFSM fsm, GameObject playerObject) {
wallCheck = fsm.GetAction(followRightName, 7);
wallCheck?.trueValue = wallTrueVar;
- // Remove hook
+ // Remove hooks
var hook = fsm.GetAction(followLeftName, 12);
hook?.Uninject();
hook = fsm.GetAction(followRightName, 12);
hook?.Uninject();
+ hook = fsm.GetAction("Fire Antic", 0);
+ hook?.Uninject();
+
// Remove track trigger
var trackTrigger = fsm.GetAction(followLeftName, 12);
@@ -215,10 +372,53 @@ private bool FixFsmForUse(PlayMakerFSM fsm, GameObject playerObject) {
right.Transitions[5],
];
+ // Set volt state
+ SetVolt(fsm, isVolt, "Init", 8);
+ SetVolt(fsm, isVolt, "Init", 11);
+ SetVolt(fsm, isVolt, "Fire Antic", 7);
+ SetVolt(fsm, isVolt, "Launch", 6);
+ SetVolt(fsm, isVolt, "Launch NoTarget", 11);
+
+ var burst = fsm.GetFirstAction("Burst");
+ if (burst != null) {
+ if (isVolt) {
+ burst.isFalse = burst.isTrue;
+ } else {
+ burst.isTrue = burst.isFalse;
+ }
+ }
+
fsm.enabled = true;
- return true;
}
+ ///
+ /// Sets the animation strings for volt/non-volt to the same value, depending on the volt status
+ ///
+ /// The FSM of the nail
+ /// If the volt filament effect should be used
+ /// The name of the state to change
+ /// The index of the action
+ private static void SetVolt(PlayMakerFSM fsm, bool isVolt, string state, int index) {
+ // Get the consumer of the volt state
+ var boolToString = fsm.GetAction(state, index);
+ if (boolToString == null) {
+ return;
+ }
+
+ // Ensure that the value is always the same
+ if (isVolt) {
+ boolToString.falseString = boolToString.trueString;
+ } else {
+ boolToString.trueString = boolToString.falseString;
+ }
+ }
+
+ ///
+ /// Gets the antic effect for summoning nails
+ ///
+ /// The player summoning nails
+ /// The antic, if found
+ /// true if the antic was found
private bool TryGetAntic(GameObject playerObject, [MaybeNullWhen(false)] out GameObject antic) {
// Find existing first
var effects = playerObject.FindGameObjectInChildren("Effects");
@@ -232,6 +432,7 @@ private bool TryGetAntic(GameObject playerObject, [MaybeNullWhen(false)] out Gam
return true;
}
+ // Create from local effects
var localAntic = HeroController.instance.gameObject
.FindGameObjectInChildren("Effects")?
.FindGameObjectInChildren(AnticName);
@@ -240,6 +441,7 @@ private bool TryGetAntic(GameObject playerObject, [MaybeNullWhen(false)] out Gam
return false;
}
+ // Set name and remove components
antic = Object.Instantiate(localAntic, effects.transform);
antic.name = AnticName;
@@ -247,4 +449,99 @@ private bool TryGetAntic(GameObject playerObject, [MaybeNullWhen(false)] out Gam
return true;
}
+
+ ///
+ /// Encodes a target object's position into a byte[].
+ /// Also includes the volt filament status and if the target is a player or not.
+ ///
+ /// The target of a nail
+ /// The bytes to send over the network
+ public static byte[] EncodeTargetInfo(GameObject target) {
+ var position = target.transform.position;
+ var isPlayer = target.GetComponent() != null;
+
+ // Convert floats to ushorts
+ var x = (ushort) ((position.x * PositionScale) + PositionOffset);
+ var y = (ushort) ((position.y * PositionScale) + PositionOffset);
+
+ // Split shorts into bytes
+ return [
+ GetEffectFlags()[0], // get volt status
+ (byte)(x & 0xFF),
+ (byte)(x >> 8),
+ (byte)(y & 0xFF),
+ (byte)(y >> 8),
+ (byte)(isPlayer ? 1 : 0)
+ ];
+
+ }
+
+ ///
+ /// Converts a byte[] to information about a nail target
+ ///
+ /// The effect info received over the network
+ /// The information about the target, or null if no target.
+ public static NailTarget? DecodeTargetInfo(byte[]? info) {
+ if (info == null || info.Length < 6) return null;
+
+ var isVolt = info[0] == 1;
+
+ // Convert two bytes to ushorts, then to floats.
+ // Offset to restore the range to a short.
+ var x = (float) BitConverter.ToUInt16([info[1], info[2]], 0) - PositionOffset;
+ var y = (float) BitConverter.ToUInt16([info[3], info[4]], 0) - PositionOffset;
+
+ // Undo precision scaling
+ var position = new Vector2(x / PositionScale, y / PositionScale);
+ var isPlayer = info[5] == 1;
+
+ return new NailTarget {
+ IsPlayer = isPlayer,
+ IsVolt = isVolt,
+ Position = position,
+ };
+ }
+
+ ///
+ /// Finds the target of a nail. If a suitable enemy or player wasn't found, the nails will target a new object in the given position.
+ ///
+ /// The target to find
+ /// The target object
+ public GameObject FindTarget(NailTarget target) {
+ // Find all players and enemies within 2.5 units of the target
+ var mask = LayerMask.GetMask("Player", "Default", "Enemies");
+ var inside = Physics2D.OverlapBoxAll(target.Position, new Vector2(2.5f, 2.5f), 0, mask);
+
+ // Prioritize player
+ if (target.IsPlayer) {
+ var player = inside.FirstOrDefault(obj => (bool)obj.GetComponent() || (bool)obj.GetComponent());
+ if (player) {
+ return player.gameObject;
+ }
+ }
+
+ // Find an enemy to target if possible
+ var firstEnemy = inside.FirstOrDefault(obj => obj.gameObject.layer == (int) GlobalEnums.PhysLayers.ENEMIES);
+ if (firstEnemy) {
+ return firstEnemy.gameObject;
+ }
+
+ // Otherwise just create a placeholder that expires
+ var targetObj = new GameObject();
+ targetObj.transform.position = target.Position;
+ targetObj.DestroyAfterTime(5);
+
+ return targetObj;
+ }
+
+ ///
+ /// Information about where/what a nail is targeting
+ ///
+ internal struct NailTarget {
+ public Vector2 Position;
+
+ public bool IsPlayer;
+
+ public bool IsVolt;
+ }
}
diff --git a/SSMP/Util/AudioUtil.cs b/SSMP/Util/AudioUtil.cs
index 3744ae29..cd09d6a0 100644
--- a/SSMP/Util/AudioUtil.cs
+++ b/SSMP/Util/AudioUtil.cs
@@ -199,6 +199,28 @@ GameObject playerObject
audioSource.PlayOneShot(clip);
}
+ ///
+ /// Play the audio from a action relative to the given player object.
+ ///
+ /// The audio player action.
+ /// The game object for the player.
+ public static void PlayAudio(
+ PlayAudioEventRandom action,
+ GameObject playerObject
+ ) {
+ var clips = action.audioClipsArray;
+ if (clips == null || clips.Length == 0) {
+ return;
+ }
+
+ var audioSource = GetAudioSourceObject(playerObject);
+
+ audioSource.pitch = UnityEngine.Random.Range(action.pitchMin.value, action.pitchMax.value);
+ audioSource.volume = action.volume.value;
+
+ audioSource.PlayOneShot(clips.GetRandomElement());
+ }
+
///
/// Play the given audio event positionally at the given player object's position with the given audio clip.
/// And destroy it after it is done playing.
From 84717d668c5b298d3994b027d9c1de632acfe26c Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Sat, 18 Apr 2026 01:48:58 -0400
Subject: [PATCH 13/41] add double jump feathers
---
SSMP/Animation/AnimationManager.cs | 3 ++-
SSMP/Animation/Effects/DoubleJump.cs | 23 +++++++++++++++++++++++
2 files changed, 25 insertions(+), 1 deletion(-)
create mode 100644 SSMP/Animation/Effects/DoubleJump.cs
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index e12c2617..6d3f38ec 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -662,6 +662,7 @@ internal class AnimationManager {
{ AnimationClip.BindBurstAir, BindBurst.Instance },
{ AnimationClip.RageBindBurst, BindBurst.Instance },
{ AnimationClip.Death, new Death() },
+ { AnimationClip.DoubleJump, new DoubleJump() },
// Silk Skills
{ AnimationClip.NeedleThrowThrowing, new SilkSpear() },
@@ -672,7 +673,7 @@ internal class AnimationManager {
{ AnimationClip.ParryStanceGround, CrossStitch.StartingInstance },
{ AnimationClip.ParryClash, new CrossStitch() },
{ AnimationClip.SilkBombAntic, new RuneRage { IsAntic = true } },
- { AnimationClip.AirSphereAntic, new PaleNails { IsAntic = true } }
+ { AnimationClip.AirSphereAntic, new PaleNails { IsAntic = true } },
};
private static readonly Dictionary SubAnimationEffects = new() {
diff --git a/SSMP/Animation/Effects/DoubleJump.cs b/SSMP/Animation/Effects/DoubleJump.cs
new file mode 100644
index 00000000..deedcde6
--- /dev/null
+++ b/SSMP/Animation/Effects/DoubleJump.cs
@@ -0,0 +1,23 @@
+using SSMP.Internals;
+using SSMP.Util;
+using UnityEngine;
+
+namespace SSMP.Animation.Effects;
+
+internal class DoubleJump : AnimationEffect {
+ ///
+ public override byte[]? GetEffectInfo() {
+ return null;
+ }
+
+ ///
+ public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ var localEffect = HeroController.instance.doubleJumpEffectPrefab;
+
+ var effect = EffectUtils.SpawnGlobalPoolObject(localEffect, playerObject.transform, 2f);
+ if (effect == null) return;
+
+ effect.DestroyGameObjectInChildren("haze flash");
+ effect.DestroyGameObjectInChildren("jump_double_glow");
+ }
+}
From 7ff68b98b86a1ec5393452190d039adaf805d1bc Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Sat, 18 Apr 2026 01:56:09 -0400
Subject: [PATCH 14/41] fix post-spawn position
---
SSMP/Animation/Effects/EffectUtils.cs | 2 ++
SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs | 1 -
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/SSMP/Animation/Effects/EffectUtils.cs b/SSMP/Animation/Effects/EffectUtils.cs
index 189b8d93..381e3395 100644
--- a/SSMP/Animation/Effects/EffectUtils.cs
+++ b/SSMP/Animation/Effects/EffectUtils.cs
@@ -65,6 +65,8 @@ public static void SafelyRemoveAutoRecycle(GameObject obj) {
if (!keepParent) {
newObj.transform.SetParent(null);
newObj.transform.position = spawnLocation.position;
+ } else {
+ newObj.transform.localPosition = Vector3.zero;
}
newObj.SetActive(true);
diff --git a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
index 2d513175..816be2ba 100644
--- a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
+++ b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
@@ -205,7 +205,6 @@ private static bool TryGetThreadStorm(
if (preParticles != null) {
var postParticles = EffectUtils.SpawnGlobalPoolObject(preParticles, shamanRune.transform, 0, true);
if (postParticles) {
- postParticles.transform.localPosition = Vector3.zero;
postParticles.transform.localScale = new Vector3(3.5f, 3.5f, 1);
}
}
From 738bfc6675a5689a961c677dc340616f4a960028 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Sat, 18 Apr 2026 02:28:32 -0400
Subject: [PATCH 15/41] Add umbrella inflate effect
---
SSMP/Animation/AnimationManager.cs | 2 +
SSMP/Animation/Effects/DoubleJump.cs | 23 -----------
SSMP/Animation/Effects/Movement/DoubleJump.cs | 40 +++++++++++++++++++
.../Effects/Movement/UmbrellaInflate.cs | 35 ++++++++++++++++
4 files changed, 77 insertions(+), 23 deletions(-)
delete mode 100644 SSMP/Animation/Effects/DoubleJump.cs
create mode 100644 SSMP/Animation/Effects/Movement/DoubleJump.cs
create mode 100644 SSMP/Animation/Effects/Movement/UmbrellaInflate.cs
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index 6d3f38ec..8f231a82 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -2,6 +2,7 @@
using System.Linq;
using HutongGames.PlayMaker.Actions;
using SSMP.Animation.Effects;
+using SSMP.Animation.Effects.Movement;
using SSMP.Animation.Effects.SilkSkills;
using SSMP.Collection;
using SSMP.Fsm;
@@ -663,6 +664,7 @@ internal class AnimationManager {
{ AnimationClip.RageBindBurst, BindBurst.Instance },
{ AnimationClip.Death, new Death() },
{ AnimationClip.DoubleJump, new DoubleJump() },
+ { AnimationClip.UmbrellaInflateAntic, new UmbrellaInflate() },
// Silk Skills
{ AnimationClip.NeedleThrowThrowing, new SilkSpear() },
diff --git a/SSMP/Animation/Effects/DoubleJump.cs b/SSMP/Animation/Effects/DoubleJump.cs
deleted file mode 100644
index deedcde6..00000000
--- a/SSMP/Animation/Effects/DoubleJump.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using SSMP.Internals;
-using SSMP.Util;
-using UnityEngine;
-
-namespace SSMP.Animation.Effects;
-
-internal class DoubleJump : AnimationEffect {
- ///
- public override byte[]? GetEffectInfo() {
- return null;
- }
-
- ///
- public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
- var localEffect = HeroController.instance.doubleJumpEffectPrefab;
-
- var effect = EffectUtils.SpawnGlobalPoolObject(localEffect, playerObject.transform, 2f);
- if (effect == null) return;
-
- effect.DestroyGameObjectInChildren("haze flash");
- effect.DestroyGameObjectInChildren("jump_double_glow");
- }
-}
diff --git a/SSMP/Animation/Effects/Movement/DoubleJump.cs b/SSMP/Animation/Effects/Movement/DoubleJump.cs
new file mode 100644
index 00000000..95e0c6d0
--- /dev/null
+++ b/SSMP/Animation/Effects/Movement/DoubleJump.cs
@@ -0,0 +1,40 @@
+using SSMP.Internals;
+using SSMP.Util;
+using UnityEngine;
+
+namespace SSMP.Animation.Effects.Movement;
+
+internal class DoubleJump : AnimationEffect {
+ private const string JumpEffectName = "double_jump_feathers";
+
+ ///
+ public override byte[]? GetEffectInfo() {
+ return null;
+ }
+
+ ///
+ public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ var effects = playerObject.FindGameObjectInChildren("Effects");
+ if (effects == null) {
+ effects = new GameObject();
+ effects.transform.SetParentReset(playerObject.transform);
+ }
+
+ var effect = effects.FindGameObjectInChildren(JumpEffectName);
+ if (effect == null) {
+ var localEffect = HeroController.instance.doubleJumpEffectPrefab;
+ effect = EffectUtils.SpawnGlobalPoolObject(localEffect, effects.transform, 0, true);
+ if (effect == null) return;
+ effect.name = JumpEffectName;
+
+ effect.DestroyGameObjectInChildren("haze flash");
+ effect.DestroyGameObjectInChildren("jump_double_glow");
+ effect.DestroyComponent();
+ effect.SetActiveChildren(true);
+ }
+
+ effect.SetActive(false);
+ effect.SetActive(true);
+
+ }
+}
diff --git a/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs b/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs
new file mode 100644
index 00000000..2b520137
--- /dev/null
+++ b/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs
@@ -0,0 +1,35 @@
+using SSMP.Internals;
+using SSMP.Util;
+using UnityEngine;
+
+namespace SSMP.Animation.Effects.Movement;
+
+internal class UmbrellaInflate : AnimationEffect {
+ private const string UmbrellaInflateName = "umbrella_inflate_effect";
+
+ public override byte[]? GetEffectInfo() {
+ return null;
+ }
+
+ public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ var effects = playerObject.FindGameObjectInChildren("Effects");
+ if (effects == null) {
+ effects = new GameObject();
+ effects.transform.SetParentReset(playerObject.transform);
+ }
+
+ var effect = effects.FindGameObjectInChildren(UmbrellaInflateName);
+ if (effect == null) {
+ var localEffect = HeroController.instance.umbrellaEffect;
+ effect = Object.Instantiate(localEffect, effects.transform);
+ effect.name = UmbrellaInflateName;
+ effect.transform.localPosition = new Vector3(0, -0.24f, 0);
+ effect.transform.localScale = Vector3.one;
+
+ effect.DestroyGameObjectInChildren("umbrella_float_fx_burst0002");
+ }
+
+ effect.SetActive(false);
+ effect.SetActive(true);
+ }
+}
From 05a77b786a78f61a3600b847faa01e71060689b1 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Sat, 18 Apr 2026 22:39:48 -0400
Subject: [PATCH 16/41] Fix thread storm scaling
---
.../Effects/SilkSkills/ThreadStorm.cs | 45 +++++++++----------
1 file changed, 20 insertions(+), 25 deletions(-)
diff --git a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
index 816be2ba..f276e840 100644
--- a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
+++ b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
@@ -1,7 +1,6 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
-using HutongGames.PlayMaker.Actions;
using SSMP.Internals;
using SSMP.Util;
using UnityEngine;
@@ -41,7 +40,8 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
/// Plays the main loop of the Thread Storm attack
///
/// The player object that used the attack
- private IEnumerator PlayStormExtension(GameObject playerObject) {
+ /// If the extension is part of the original animation
+ private IEnumerator PlayStormExtension(GameObject playerObject, bool initial = false) {
if (!TryGetThreadStorm(playerObject, out var threadStorm)) {
yield break;
}
@@ -56,11 +56,10 @@ private IEnumerator PlayStormExtension(GameObject playerObject) {
animator.PlayFromFrame("AirSphere", 0);
// Scale up
- var curveScale = threadStorm.GetComponent();
- if (curveScale != null) {
- curveScale.enabled = false;
- curveScale.enabled = true;
- curveScale.StartAnimation();
+ var damager = threadStorm.FindGameObjectInChildren("Ball");
+ if (!initial && damager != null) {
+ damager.transform.localScale = new Vector3(1.9f, 1.9f, 1);
+ AnimateScaleReset(damager);
}
yield return new WaitForSeconds(0.65f);
@@ -80,6 +79,7 @@ private IEnumerator PlayStormSetup(GameObject playerObject, bool volt, bool isSh
}
threadStorm.SetActive(true);
+ threadStorm.transform.localScale = Vector3.one;
// Set volt filament effect
var damager = threadStorm.FindGameObjectInChildren("Ball");
@@ -101,6 +101,9 @@ private IEnumerator PlayStormSetup(GameObject playerObject, bool volt, bool isSh
// Set the damager
if (damager) {
+ damager.transform.localScale = new Vector3(0.8f, 0.8f, 1);
+ AnimateScaleReset(damager);
+
SetDamageHeroState(damager, 1);
damager.SetActive(true);
} else {
@@ -112,7 +115,15 @@ private IEnumerator PlayStormSetup(GameObject playerObject, bool volt, bool isSh
audio.Play();
// Play the main effect
- MonoBehaviourUtil.Instance.StartCoroutine(PlayStormExtension(playerObject));
+ MonoBehaviourUtil.Instance.StartCoroutine(PlayStormExtension(playerObject, true));
+ }
+
+ ///
+ /// Animates the thread storm scale back to default
+ ///
+ /// The "ball" child on the thread storm object
+ private static void AnimateScaleReset(GameObject ball) {
+ ball.transform.ScaleTo(MonoBehaviourUtil.Instance, new Vector3(1.7f, 1.7f, 1), 0.1f);
}
///
@@ -216,23 +227,7 @@ private static bool TryGetThreadStorm(
shamanRune.DestroyGameObjectInChildren("Shaman Rune Camera Bloom");
}
- // Set up scale animation. It plays when enabled.
- var curveScale = threadStorm.AddComponent();
- curveScale.duration = 0.3f;
- curveScale.playOnEnable = false;
- curveScale.curve = new([
- new Keyframe(0, 1),
- new Keyframe(0.5f, 2f),
- new Keyframe(1, 1),
- ]);
- curveScale.OnStart = new UnityEngine.Events.UnityEvent();
- curveScale.OnStop = new UnityEngine.Events.UnityEvent();
- curveScale.framerate = 30;
- curveScale.isRealtime = true;
- curveScale.playOnEnable = true;
- curveScale.enabled = false;
- curveScale.offset = new Vector3(0.1f, 0.1f, 0.1f);
-
+ threadStorm.SetActive(false);
threadStorm.SetActive(true);
return true;
From 02f3e71e363289d28c4ef92508424a563b792cbb Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Sat, 18 Apr 2026 23:13:25 -0400
Subject: [PATCH 17/41] fix pale nail animation interference and rapidfiring
---
SSMP/Animation/AnimationManager.cs | 2 +-
.../Animation/Effects/SilkSkills/PaleNails.cs | 68 ++++++++-----------
2 files changed, 29 insertions(+), 41 deletions(-)
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index 8f231a82..dc605ed9 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -675,7 +675,7 @@ internal class AnimationManager {
{ AnimationClip.ParryStanceGround, CrossStitch.StartingInstance },
{ AnimationClip.ParryClash, new CrossStitch() },
{ AnimationClip.SilkBombAntic, new RuneRage { IsAntic = true } },
- { AnimationClip.AirSphereAntic, new PaleNails { IsAntic = true } },
+ { AnimationClip.SilkBossNeedleCast, new PaleNails { IsAntic = true } },
};
private static readonly Dictionary SubAnimationEffects = new() {
diff --git a/SSMP/Animation/Effects/SilkSkills/PaleNails.cs b/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
index 57460de3..9a473f3b 100644
--- a/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
+++ b/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
@@ -25,7 +25,7 @@ internal class PaleNails : BaseSilkSkill {
public bool IsAntic = false;
- private static Dictionary> _playerNails = new();
+ private static Dictionary _playerNails = new();
///
public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
@@ -42,39 +42,25 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
// Get existing nails
var id = playerObject.GetInstanceID();
- if (!_playerNails.TryGetValue(id, out var playerNails)) {
- playerNails = [];
- }
-
- // No nails to fire
- if (playerNails.Count == 0) {
+ if (!_playerNails.TryGetValue(id, out var nails) || nails.Length == 0) {
return;
}
- // Find first nails that aren't despawned
- var nails = playerNails[0];
- playerNails.RemoveAt(0);
- while (nails[0] == null && playerNails.Count > 0) {
- nails = playerNails[0];
- playerNails.RemoveAt(0);
- }
-
+ // Ensure nails aren't despawned
if (nails.Any(obj => obj == null)) {
return;
}
- _playerNails[id] = playerNails;
-
// Decode target position. If can't decode, play the unguided variant
var targetInfo = DecodeTargetInfo(effectInfo);
if (!targetInfo.HasValue) {
- PlayNailFireUnguided(nails, isVolt);
+ PlayNailFireUnguided(nails, isVolt, id);
return;
}
// Fire at target position
var target = FindTarget(targetInfo.Value);
- MonoBehaviourUtil.Instance.StartCoroutine(PlayNailFireTargeted(target, nails, isVolt));
+ MonoBehaviourUtil.Instance.StartCoroutine(PlayNailFireTargeted(target, nails, isVolt, id));
}
///
@@ -83,12 +69,17 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
/// The target object to fire at
/// The set of nails to fire
/// If the volt filament effect should be used
- private static IEnumerator PlayNailFireTargeted(GameObject target, GameObject[] nails, bool isVolt) {
+ /// The 'id' of the player firing the nails
+ private static IEnumerator PlayNailFireTargeted(GameObject target, GameObject[] nails, bool isVolt, int playerId) {
+ // Copy nails
+ GameObject[] playerNails = [.. nails];
+ _playerNails[playerId] = [];
+
// Play audio
- if (isVolt) PlayVoltAudio(nails[0]);
+ if (isVolt) PlayVoltAudio(playerNails[0]);
// Fire each nail
- foreach (var nail in nails) {
+ foreach (var nail in playerNails) {
// Nail has already been despawned
if (nail == null) {
yield break;
@@ -110,7 +101,8 @@ private static IEnumerator PlayNailFireTargeted(GameObject target, GameObject[]
///
/// The set of nails to fire
/// If the volt filament effect should be used
- private static void PlayNailFireUnguided(GameObject[] nails, bool isVolt) {
+ /// The 'id' of the player that summoned the nails
+ private static void PlayNailFireUnguided(GameObject[] nails, bool isVolt, int playerId) {
// Play audio
if (isVolt) PlayVoltAudio(nails[0]);
@@ -118,7 +110,7 @@ private static void PlayNailFireUnguided(GameObject[] nails, bool isVolt) {
foreach (var nail in nails) {
// Nail has already been despawned
if (nail == null) {
- continue;
+ return;
}
// Remove any target
@@ -128,6 +120,9 @@ private static void PlayNailFireUnguided(GameObject[] nails, bool isVolt) {
// Send it off into the world immediately
fsm.Fsm.Event("FOLLOW BUDDY");
}
+
+ // Clear nails
+ _playerNails[playerId] = [];
}
///
@@ -140,6 +135,12 @@ private static void PlayNailFireUnguided(GameObject[] nails, bool isVolt) {
private IEnumerator PlayAntic(GameObject playerObject, bool isVolt, bool isShaman) {
var fsm = GetSkillFSM();
+ // Fire existing nails
+ var id = playerObject.GetInstanceID();
+ if (_playerNails.TryGetValue(id, out var existingNails)) {
+ PlayNailFireUnguided(existingNails, isVolt, id);
+ }
+
// Play main antic
PlayHornetAttackSound(playerObject);
if (TryGetAntic(playerObject, out var antic)) {
@@ -218,26 +219,13 @@ private IEnumerator PlayAntic(GameObject playerObject, bool isVolt, bool isShama
}
// Store nails for firing later
- var id = playerObject.GetInstanceID();
- if (!_playerNails.TryGetValue(id, out var playerNails)) {
- playerNails = [];
- }
-
- playerNails.Add(nails);
-
- _playerNails[id] = playerNails;
+ _playerNails[id] = nails;
// Wait for nail hover time to expire
yield return new WaitForSeconds(1.8f);
-
- // Nails may have been fired, don't refire
- if (!_playerNails[id].Contains(nails)) yield break;
-
- // Remove from being able to fire
- _playerNails[id].Remove(nails);
-
+
// Fire em'
- PlayNailFireUnguided(nails, isVolt);
+ PlayNailFireUnguided(nails, isVolt, id);
}
///
From d2148adb2b67306874482dcb9f45a4505c3ca7f5 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Sun, 19 Apr 2026 00:45:34 -0400
Subject: [PATCH 18/41] Add sawtooth circlet
---
SSMP/Animation/AnimationManager.cs | 2 +-
SSMP/Animation/Effects/Movement/DoubleJump.cs | 14 ++-
.../Effects/Movement/UmbrellaInflate.cs | 102 +++++++++++++++++-
3 files changed, 113 insertions(+), 5 deletions(-)
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index dc605ed9..d5c6ae5f 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -664,7 +664,7 @@ internal class AnimationManager {
{ AnimationClip.RageBindBurst, BindBurst.Instance },
{ AnimationClip.Death, new Death() },
{ AnimationClip.DoubleJump, new DoubleJump() },
- { AnimationClip.UmbrellaInflateAntic, new UmbrellaInflate() },
+ { AnimationClip.UmbrellaInflate, UmbrellaInflate.Instance },
// Silk Skills
{ AnimationClip.NeedleThrowThrowing, new SilkSpear() },
diff --git a/SSMP/Animation/Effects/Movement/DoubleJump.cs b/SSMP/Animation/Effects/Movement/DoubleJump.cs
index 95e0c6d0..b88d2d66 100644
--- a/SSMP/Animation/Effects/Movement/DoubleJump.cs
+++ b/SSMP/Animation/Effects/Movement/DoubleJump.cs
@@ -4,22 +4,26 @@
namespace SSMP.Animation.Effects.Movement;
-internal class DoubleJump : AnimationEffect {
+internal class DoubleJump : DamageAnimationEffect {
private const string JumpEffectName = "double_jump_feathers";
///
public override byte[]? GetEffectInfo() {
- return null;
+ return [
+ (byte)(ToolItemManager.IsToolEquipped("Brolly Spike") ? 1 : 0)
+ ];
}
///
public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ // Find or create effects
var effects = playerObject.FindGameObjectInChildren("Effects");
if (effects == null) {
effects = new GameObject();
effects.transform.SetParentReset(playerObject.transform);
}
+ // Find or create jump effect
var effect = effects.FindGameObjectInChildren(JumpEffectName);
if (effect == null) {
var localEffect = HeroController.instance.doubleJumpEffectPrefab;
@@ -27,14 +31,20 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
if (effect == null) return;
effect.name = JumpEffectName;
+ // Remove components from newly created object
effect.DestroyGameObjectInChildren("haze flash");
effect.DestroyGameObjectInChildren("jump_double_glow");
effect.DestroyComponent();
effect.SetActiveChildren(true);
}
+ // Refresh effect
effect.SetActive(false);
effect.SetActive(true);
+ // Play sawtooth circlet if appropriate
+ if (effectInfo is [1]) {
+ UmbrellaInflate.Instance.PlayCirclet(playerObject);
+ }
}
}
diff --git a/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs b/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs
index 2b520137..abe2ab7d 100644
--- a/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs
+++ b/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs
@@ -1,27 +1,42 @@
+using System.Diagnostics.CodeAnalysis;
+using HutongGames.PlayMaker.Actions;
using SSMP.Internals;
using SSMP.Util;
using UnityEngine;
namespace SSMP.Animation.Effects.Movement;
-internal class UmbrellaInflate : AnimationEffect {
+internal class UmbrellaInflate : DamageAnimationEffect {
private const string UmbrellaInflateName = "umbrella_inflate_effect";
+ private const string SpikedCircletName = "Tool_brolly_spike";
+
+ private static GameObject? _localCirclet;
+
+ public static UmbrellaInflate Instance = new();
+
+ ///
public override byte[]? GetEffectInfo() {
- return null;
+ return [
+ (byte)(ToolItemManager.IsToolEquipped("Brolly Spike") ? 1 : 0)
+ ];
}
+ ///
public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ // Get or create effects
var effects = playerObject.FindGameObjectInChildren("Effects");
if (effects == null) {
effects = new GameObject();
effects.transform.SetParentReset(playerObject.transform);
}
+ // Find or create inflate object
var effect = effects.FindGameObjectInChildren(UmbrellaInflateName);
if (effect == null) {
var localEffect = HeroController.instance.umbrellaEffect;
effect = Object.Instantiate(localEffect, effects.transform);
+
effect.name = UmbrellaInflateName;
effect.transform.localPosition = new Vector3(0, -0.24f, 0);
effect.transform.localScale = Vector3.one;
@@ -29,7 +44,90 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
effect.DestroyGameObjectInChildren("umbrella_float_fx_burst0002");
}
+ // Refresh particles
effect.SetActive(false);
effect.SetActive(true);
+
+ // Enable sawtooth circlet if appropriate
+ if (effectInfo is [1]) {
+ PlayCirclet(playerObject);
+ }
+
+ }
+
+ ///
+ /// Attempts to find or create the sawtooth circlet object
+ ///
+ /// The player using the circlet
+ /// The circlet, if found
+ /// true if the circlet was found
+ private static bool TryGetCirclet(GameObject playerObject, [MaybeNullWhen(false)] out GameObject circlet) {
+ // Find or create effects
+ var effects = playerObject.FindGameObjectInChildren("Effects");
+ if (effects == null) {
+ effects = new GameObject("Effects");
+ effects.transform.SetParentReset(playerObject.transform);
+ }
+
+ // Find existing circlet
+ circlet = effects.FindGameObjectInChildren(SpikedCircletName);
+ if (circlet != null) {
+ return true;
+ }
+
+ // Locate circlet
+ if (_localCirclet == null) {
+ var brollyFsm = HeroController.instance.fsm_brollyControl;
+ var spawner = brollyFsm.GetFirstAction("Damager?");
+ _localCirclet = spawner.gameObject.Value;
+ }
+
+ // Spawn in a new circlet
+ circlet = EffectUtils.SpawnGlobalPoolObject(_localCirclet, effects.transform, 0, true);
+ if (circlet == null) {
+ return false;
+ }
+
+ circlet.name = SpikedCircletName;
+ circlet.transform.localPosition = new Vector3(0, 0, -0.02f);
+ circlet.transform.localScale = new Vector3(-1, -1, 1);
+ circlet.DestroyComponent();
+ circlet.tag = "Untagged";
+
+ return true;
+ }
+
+ ///
+ /// Plays the sawtooth circlet
+ ///
+ /// The player using the circlet
+ public void PlayCirclet(GameObject playerObject) {
+ // Get the circlet
+ if (!TryGetCirclet(playerObject, out var circlet)) {
+ return;
+ }
+
+ // Set the damager
+ var damagerParent = circlet
+ .FindGameObjectInChildren("Brolly_spike_position")?
+ .FindGameObjectInChildren("brolly_slash_enemy_damager");
+
+ var damagerRight = damagerParent?.FindGameObjectInChildren("Damager R");
+ var damagerLeft = damagerParent?.FindGameObjectInChildren("Damager L");
+
+ if (damagerRight != null) SetDamageHeroState(damagerRight, 1);
+ if (damagerLeft != null) SetDamageHeroState(damagerLeft, 1);
+
+ // Refresh the circlet
+ circlet.SetActive(false);
+ circlet.SetActive(true);
+
+ // Play audio
+ var audioFsm = _localCirclet.LocateMyFSM("brolly_spike_cooldown_check").FsmTemplate.fsm;
+
+ var audioAction = audioFsm.GetState("Check").Actions[4];
+ if (audioAction is PlayAudioEvent audio) {
+ AudioUtil.PlayAudio(audio, playerObject);
+ }
}
}
From 438bc1b440a3a408cf6160bae6f55da0033392a4 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Sun, 19 Apr 2026 17:11:31 -0400
Subject: [PATCH 19/41] move movement effects to other branch
---
SSMP/Animation/AnimationManager.cs | 2 -
SSMP/Animation/Effects/Movement/DoubleJump.cs | 50 -------
.../Effects/Movement/UmbrellaInflate.cs | 133 ------------------
3 files changed, 185 deletions(-)
delete mode 100644 SSMP/Animation/Effects/Movement/DoubleJump.cs
delete mode 100644 SSMP/Animation/Effects/Movement/UmbrellaInflate.cs
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index d5c6ae5f..8157605b 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -663,8 +663,6 @@ internal class AnimationManager {
{ AnimationClip.BindBurstAir, BindBurst.Instance },
{ AnimationClip.RageBindBurst, BindBurst.Instance },
{ AnimationClip.Death, new Death() },
- { AnimationClip.DoubleJump, new DoubleJump() },
- { AnimationClip.UmbrellaInflate, UmbrellaInflate.Instance },
// Silk Skills
{ AnimationClip.NeedleThrowThrowing, new SilkSpear() },
diff --git a/SSMP/Animation/Effects/Movement/DoubleJump.cs b/SSMP/Animation/Effects/Movement/DoubleJump.cs
deleted file mode 100644
index b88d2d66..00000000
--- a/SSMP/Animation/Effects/Movement/DoubleJump.cs
+++ /dev/null
@@ -1,50 +0,0 @@
-using SSMP.Internals;
-using SSMP.Util;
-using UnityEngine;
-
-namespace SSMP.Animation.Effects.Movement;
-
-internal class DoubleJump : DamageAnimationEffect {
- private const string JumpEffectName = "double_jump_feathers";
-
- ///
- public override byte[]? GetEffectInfo() {
- return [
- (byte)(ToolItemManager.IsToolEquipped("Brolly Spike") ? 1 : 0)
- ];
- }
-
- ///
- public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
- // Find or create effects
- var effects = playerObject.FindGameObjectInChildren("Effects");
- if (effects == null) {
- effects = new GameObject();
- effects.transform.SetParentReset(playerObject.transform);
- }
-
- // Find or create jump effect
- var effect = effects.FindGameObjectInChildren(JumpEffectName);
- if (effect == null) {
- var localEffect = HeroController.instance.doubleJumpEffectPrefab;
- effect = EffectUtils.SpawnGlobalPoolObject(localEffect, effects.transform, 0, true);
- if (effect == null) return;
- effect.name = JumpEffectName;
-
- // Remove components from newly created object
- effect.DestroyGameObjectInChildren("haze flash");
- effect.DestroyGameObjectInChildren("jump_double_glow");
- effect.DestroyComponent();
- effect.SetActiveChildren(true);
- }
-
- // Refresh effect
- effect.SetActive(false);
- effect.SetActive(true);
-
- // Play sawtooth circlet if appropriate
- if (effectInfo is [1]) {
- UmbrellaInflate.Instance.PlayCirclet(playerObject);
- }
- }
-}
diff --git a/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs b/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs
deleted file mode 100644
index abe2ab7d..00000000
--- a/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs
+++ /dev/null
@@ -1,133 +0,0 @@
-using System.Diagnostics.CodeAnalysis;
-using HutongGames.PlayMaker.Actions;
-using SSMP.Internals;
-using SSMP.Util;
-using UnityEngine;
-
-namespace SSMP.Animation.Effects.Movement;
-
-internal class UmbrellaInflate : DamageAnimationEffect {
- private const string UmbrellaInflateName = "umbrella_inflate_effect";
-
- private const string SpikedCircletName = "Tool_brolly_spike";
-
- private static GameObject? _localCirclet;
-
- public static UmbrellaInflate Instance = new();
-
- ///
- public override byte[]? GetEffectInfo() {
- return [
- (byte)(ToolItemManager.IsToolEquipped("Brolly Spike") ? 1 : 0)
- ];
- }
-
- ///
- public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
- // Get or create effects
- var effects = playerObject.FindGameObjectInChildren("Effects");
- if (effects == null) {
- effects = new GameObject();
- effects.transform.SetParentReset(playerObject.transform);
- }
-
- // Find or create inflate object
- var effect = effects.FindGameObjectInChildren(UmbrellaInflateName);
- if (effect == null) {
- var localEffect = HeroController.instance.umbrellaEffect;
- effect = Object.Instantiate(localEffect, effects.transform);
-
- effect.name = UmbrellaInflateName;
- effect.transform.localPosition = new Vector3(0, -0.24f, 0);
- effect.transform.localScale = Vector3.one;
-
- effect.DestroyGameObjectInChildren("umbrella_float_fx_burst0002");
- }
-
- // Refresh particles
- effect.SetActive(false);
- effect.SetActive(true);
-
- // Enable sawtooth circlet if appropriate
- if (effectInfo is [1]) {
- PlayCirclet(playerObject);
- }
-
- }
-
- ///
- /// Attempts to find or create the sawtooth circlet object
- ///
- /// The player using the circlet
- /// The circlet, if found
- /// true if the circlet was found
- private static bool TryGetCirclet(GameObject playerObject, [MaybeNullWhen(false)] out GameObject circlet) {
- // Find or create effects
- var effects = playerObject.FindGameObjectInChildren("Effects");
- if (effects == null) {
- effects = new GameObject("Effects");
- effects.transform.SetParentReset(playerObject.transform);
- }
-
- // Find existing circlet
- circlet = effects.FindGameObjectInChildren(SpikedCircletName);
- if (circlet != null) {
- return true;
- }
-
- // Locate circlet
- if (_localCirclet == null) {
- var brollyFsm = HeroController.instance.fsm_brollyControl;
- var spawner = brollyFsm.GetFirstAction("Damager?");
- _localCirclet = spawner.gameObject.Value;
- }
-
- // Spawn in a new circlet
- circlet = EffectUtils.SpawnGlobalPoolObject(_localCirclet, effects.transform, 0, true);
- if (circlet == null) {
- return false;
- }
-
- circlet.name = SpikedCircletName;
- circlet.transform.localPosition = new Vector3(0, 0, -0.02f);
- circlet.transform.localScale = new Vector3(-1, -1, 1);
- circlet.DestroyComponent();
- circlet.tag = "Untagged";
-
- return true;
- }
-
- ///
- /// Plays the sawtooth circlet
- ///
- /// The player using the circlet
- public void PlayCirclet(GameObject playerObject) {
- // Get the circlet
- if (!TryGetCirclet(playerObject, out var circlet)) {
- return;
- }
-
- // Set the damager
- var damagerParent = circlet
- .FindGameObjectInChildren("Brolly_spike_position")?
- .FindGameObjectInChildren("brolly_slash_enemy_damager");
-
- var damagerRight = damagerParent?.FindGameObjectInChildren("Damager R");
- var damagerLeft = damagerParent?.FindGameObjectInChildren("Damager L");
-
- if (damagerRight != null) SetDamageHeroState(damagerRight, 1);
- if (damagerLeft != null) SetDamageHeroState(damagerLeft, 1);
-
- // Refresh the circlet
- circlet.SetActive(false);
- circlet.SetActive(true);
-
- // Play audio
- var audioFsm = _localCirclet.LocateMyFSM("brolly_spike_cooldown_check").FsmTemplate.fsm;
-
- var audioAction = audioFsm.GetState("Check").Actions[4];
- if (audioAction is PlayAudioEvent audio) {
- AudioUtil.PlayAudio(audio, playerObject);
- }
- }
-}
From b116226cd4b6a350cc503db03b6f89b05daa1533 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Sun, 19 Apr 2026 23:32:16 -0400
Subject: [PATCH 20/41] make silk spear stop short if needed
---
SSMP/Animation/AnimationManager.cs | 1 -
.../Animation/Effects/SilkSkills/SilkSpear.cs | 69 ++++++++++++++++++-
2 files changed, 67 insertions(+), 3 deletions(-)
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index 8157605b..26d3d037 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -2,7 +2,6 @@
using System.Linq;
using HutongGames.PlayMaker.Actions;
using SSMP.Animation.Effects;
-using SSMP.Animation.Effects.Movement;
using SSMP.Animation.Effects.SilkSkills;
using SSMP.Collection;
using SSMP.Fsm;
diff --git a/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs b/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
index f0f1a153..6d57b9f7 100644
--- a/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
+++ b/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
@@ -1,4 +1,5 @@
-using HutongGames.PlayMaker.Actions;
+using System.Collections;
+using System.Linq;
using SSMP.Internals;
using SSMP.Util;
using UnityEngine;
@@ -8,8 +9,14 @@
namespace SSMP.Animation.Effects.SilkSkills;
internal class SilkSpear : BaseSilkSkill {
+ ///
+ /// The object name of the silk spear
+ ///
private const string SpearObjectName = "Needle Throw";
+
+ ///
public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ // Get silk spear and the object that most child objects are in
var spear = GetSilkSpear(playerObject);
if (!spear) return;
@@ -54,6 +61,7 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
var damager = needle?.FindGameObjectInChildren("Needle Damage");
if (damager) {
SetDamageHeroState(damager, 1);
+ MonoBehaviourUtil.Instance.StartCoroutine(PlayPossibleThunk(playerObject, spear, damager));
} else {
Logger.Warn("Unable to set damager for Silk Spear");
}
@@ -73,8 +81,60 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
var voltAudio = fsm.GetAction("Silkspear Zap FX", 1);
if (voltAudio != null) AudioUtil.PlayAudio(voltAudio, playerObject);
}
+
+ }
+
+ ///
+ /// Waits for the spear to collide with terrain. If it does, it'll stop short.
+ ///
+ /// The player who fired the spear
+ /// The spear
+ /// The spear's damager
+ ///
+ private static IEnumerator PlayPossibleThunk(GameObject playerObject, GameObject spear, GameObject damager) {
+ var collider = damager.GetComponent();
+ var animator = spear.GetComponentInChildren();
+
+ yield return null;
+
+ // Try to thunk as long as the spear is doing damage
+ while (collider.isActiveAndEnabled) {
+
+ // Find a terrain collider within the bounds of the spear
+ var y = spear.transform.position.y;
+ var collisions = Physics2D.LinecastAll(new Vector2(collider.bounds.min.x, y), new Vector2(collider.bounds.max.x, y), LayerMask.GetMask("Terrain"));
+
+ var found = collisions.FirstOrDefault(c => c.collider.gameObject.tag != "Piercable Terrain" && c.collider.gameObject.layer == 8);
+
+ // A terrain collider was found, do the thunk
+ if (found) {
+ yield return null;
+
+ // Don't stop short if < 6 units away
+ if (damager.transform.position.x.IsWithinTolerance(6, playerObject.transform.position.x)) {
+ animator.Play("Thunk");
+ }
+
+ // Either way, do the thunk particles
+ var thunk = spear.FindGameObjectInChildren("Needle Thunk");
+ if (thunk) {
+ thunk.SetActive(false);
+ thunk.transform.position = found.point;
+ thunk.SetActive(true);
+ }
+
+ yield break;
+ }
+
+ yield return null;
+ }
}
+ ///
+ /// Attempts to find the silk spear for the player
+ ///
+ /// The player using the spear
+ /// The spear, if found
private static GameObject? GetSilkSpear(GameObject playerObject) {
// Find existing silk spear
var silkAttacks = GetPlayerSilkAttacks(playerObject);
@@ -95,10 +155,12 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
spear = Object.Instantiate(localSpear, silkAttacks.transform);
spear.name = SpearObjectName;
+ // Remove components
spear.DestroyComponent();
spear.DestroyComponentsInChildren();
spear.DestroyComponentsInChildren();
+ // Remove specific children and their components
var child = spear.FindGameObjectInChildren("needle_throw_simple");
if (child) {
child.DestroyComponent();
@@ -122,8 +184,11 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
if (bloom1) {
Object.Destroy(bloom1);
}
+ if (bloom2) {
+ Object.Destroy(bloom2);
+ }
}
-
+
return spear;
}
}
From 395076d7d66f7747ed18c14314eb1a83917f199c Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Mon, 20 Apr 2026 01:19:08 -0400
Subject: [PATCH 21/41] cleanup and docs
---
.../Effects/SilkSkills/BaseSilkSkill.cs | 83 ++++++++----
.../Effects/SilkSkills/CrossStitch.cs | 105 +++++++++++----
.../Animation/Effects/SilkSkills/PaleNails.cs | 47 +++++--
SSMP/Animation/Effects/SilkSkills/RuneRage.cs | 13 +-
.../Animation/Effects/SilkSkills/SharpDart.cs | 126 +++++++++---------
.../Animation/Effects/SilkSkills/SilkSpear.cs | 20 +--
.../Effects/SilkSkills/ThreadStorm.cs | 50 +++----
7 files changed, 267 insertions(+), 177 deletions(-)
diff --git a/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs b/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
index 59818066..f669e067 100644
--- a/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
+++ b/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
@@ -1,6 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using HutongGames.PlayMaker.Actions;
-using SSMP.Internals;
using SSMP.Util;
using UnityEngine;
using Logger = SSMP.Logging.Logger;
@@ -9,28 +8,45 @@
namespace SSMP.Animation.Effects.SilkSkills;
internal abstract class BaseSilkSkill : DamageAnimationEffect {
-
+ ///
+ /// The name of the silk skills parent
+ ///
private const string SilkSkillsObjectName = "Special Attacks";
- private static GameObject? _localSilkAttacks;
+ ///
+ /// Cached object with silk skills
+ ///
+ private static GameObject? _localSilkSkills;
+
+ ///
+ /// See . Determines if the player is using the Volt Filament
+ ///
public static byte[] GetEffectFlags() {
var voltFilament = ToolItemManager.GetToolByName("Zap Imbuement");
- return new byte[] {
+ return [
(byte)(voltFilament.IsEquipped ? 1 : 0)
- };
+ ];
}
+ ///
public override byte[]? GetEffectInfo() {
return GetEffectFlags();
}
+ ///
+ /// Determines if the player was using the Volt Filament
+ ///
+ /// The effect info sent with the animation
+ /// true if the player used Volt Filament
protected bool IsVolt(byte[]? effectInfo) {
return effectInfo is [1];
}
- public abstract override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo);
-
+ ///
+ /// Gets the Silk Skill FSM
+ ///
+ /// The found FSM
protected static PlayMakerFSM GetSkillFSM() {
var fsm = HeroController.instance.silkSpecialFSM;
if (fsm == null) {
@@ -44,23 +60,33 @@ protected static PlayMakerFSM GetSkillFSM() {
return fsm;
}
- protected static bool TryGetLocalSilkAttacks([MaybeNullWhen(false)] out GameObject localSilkAttacks) {
+ ///
+ /// Attempts to find the local silk skills object
+ ///
+ /// The silk skills object, if found
+ /// true if the object was found
+ protected static bool TryGetLocalSilkSkills([MaybeNullWhen(false)] out GameObject localSilkSkills) {
// Find local silk skills
- if (_localSilkAttacks == null) {
- _localSilkAttacks = HeroController.instance.gameObject.FindGameObjectInChildren(SilkSkillsObjectName);
- if (_localSilkAttacks == null) {
+ if (_localSilkSkills == null) {
+ _localSilkSkills = HeroController.instance.gameObject.FindGameObjectInChildren(SilkSkillsObjectName);
+ if (_localSilkSkills == null) {
Logger.Warn("Unable to find local Silk Silks object");
- localSilkAttacks = null;
+ localSilkSkills = null;
return false;
}
}
// Find existing attacks
- localSilkAttacks = _localSilkAttacks;
+ localSilkSkills = _localSilkSkills;
return true;
}
- protected static GameObject GetPlayerSilkAttacks(GameObject playerObject) {
+ ///
+ /// Gets the silk skills object on a player
+ ///
+ /// The player to find silk skill son
+ /// The found silk skills object
+ protected static GameObject GetPlayerSilkSkills(GameObject playerObject) {
var silkAttacks = playerObject.FindGameObjectInChildren(SilkSkillsObjectName);
if (silkAttacks == null) {
silkAttacks = new GameObject(SilkSkillsObjectName);
@@ -70,6 +96,10 @@ protected static GameObject GetPlayerSilkAttacks(GameObject playerObject) {
return silkAttacks;
}
+ ///
+ /// Plays a loud attack sound
+ ///
+ /// The player to play the sound on
protected static void PlayHornetAttackSound(GameObject playerObject) {
var fsm = GetSkillFSM();
var anticAudio = fsm.GetAction("A Sphere Antic", 2);
@@ -78,26 +108,33 @@ protected static void PlayHornetAttackSound(GameObject playerObject) {
}
}
- protected static bool FindOrCreateAttack(GameObject playerObject, string name, out GameObject? attack) {
+ ///
+ /// Attempts to find or create a silk skill object.
+ ///
+ /// The player using the skill
+ /// The name of the skill object
+ /// The found or created skill object
+ /// true if the skill was created, false if it already existed or wasn't found
+ protected static bool FindOrCreateSkill(GameObject playerObject, string name, out GameObject? skill) {
// Find existing object
- var attacks = GetPlayerSilkAttacks(playerObject);
- attack = attacks.FindGameObjectInChildren(name);
- if (attack) {
+ var skills = GetPlayerSilkSkills(playerObject);
+ skill = skills.FindGameObjectInChildren(name);
+ if (skill) {
return false;
}
// Copy from local attacks
- if (!TryGetLocalSilkAttacks(out var localSilkAttacks)) {
+ if (!TryGetLocalSilkSkills(out var localSilkAttacks)) {
return false;
}
- var localClash = localSilkAttacks.FindGameObjectInChildren(name);
- if (!localClash) {
+ var localSkill = localSilkAttacks.FindGameObjectInChildren(name);
+ if (!localSkill) {
return false;
}
- attack = Object.Instantiate(localClash, attacks.transform);
- attack.name = name;
+ skill = Object.Instantiate(localSkill, skills.transform);
+ skill.name = name;
return true;
}
}
diff --git a/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs b/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs
index c2285c0a..f5a31128 100644
--- a/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs
+++ b/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs
@@ -10,15 +10,18 @@
namespace SSMP.Animation.Effects.SilkSkills;
internal class CrossStitch : BaseSilkSkill {
- private const string ParryStanceFlashName = "Parry Stance Flash";
- private const string ParryClashName = "Parry Clash Effect";
- private const string ParrySlashName = "Parry Slash Effect";
- private const string ParryZapSlashName = "Parry Slash Effect Zap";
+ ///
+ /// Determines if this instance is for the starting animation
+ ///
public bool IsStarting = false;
+ ///
+ /// Reference to an instance for the starting animation
+ ///
public static CrossStitch StartingInstance = new() { IsStarting = true };
+ ///
public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
var isShaman = crestType == CrestType.Shaman;
var isVolt = IsVolt(effectInfo);
@@ -33,6 +36,12 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
MonoBehaviourUtil.Instance.StartCoroutine(PlayClash(playerObject, isShaman, isVolt));
}
+ ///
+ /// Plays the parry preparation animation
+ ///
+ /// The player who used the skill
+ /// If shaman effects should be used
+ /// If volt filament effects should be used
private void PlayStart(GameObject playerObject, bool isShaman, bool isVolt) {
var fsm = GetSkillFSM();
@@ -44,7 +53,7 @@ private void PlayStart(GameObject playerObject, bool isShaman, bool isVolt) {
var stanceAudio = fsm.GetAction("Parry Start", 15);
if (stanceAudio != null) AudioUtil.PlayAudio(stanceAudio, playerObject);
- // Enable thread (or zap thread)
+ // Enable thread (or volt thread)
if (TryGetParryThread(playerObject, isVolt, out var thread)) {
thread.SetActive(false);
thread.SetActive(true);
@@ -59,6 +68,12 @@ private void PlayStart(GameObject playerObject, bool isShaman, bool isVolt) {
}
}
+ ///
+ /// Plays the post-hit clash effect, then the dash.
+ ///
+ /// The player who used the skill
+ /// If shaman effects should be used
+ /// If volt filament effects should be used
private IEnumerator PlayClash(GameObject playerObject, bool isShaman, bool isVolt) {
// Play parry audio
var fsm = GetSkillFSM();
@@ -79,6 +94,12 @@ private IEnumerator PlayClash(GameObject playerObject, bool isShaman, bool isVol
MonoBehaviourUtil.Instance.StartCoroutine(PlayDash(playerObject, isShaman, isVolt));
}
+ ///
+ /// Plays the cross stitch dashing animation that does damage (if appropriate)
+ ///
+ /// The player who used the skill
+ /// If shaman effects should be used
+ /// If volt filament effects should be used
private IEnumerator PlayDash(GameObject playerObject, bool isShaman, bool isVolt) {
// Play louder sound
PlayHornetAttackSound(playerObject);
@@ -86,17 +107,16 @@ private IEnumerator PlayDash(GameObject playerObject, bool isShaman, bool isVolt
// Hide the player during animation
HidePlayer(playerObject);
- // Get appropriate effect object from FSM (Parry Cross Slash)
+ // Get appropriate effect object
if (TryGetSlashEffect(playerObject, isVolt, out var slash)) {
slash.SetActive(false);
slash.SetActive(true);
+ // Enable shaman effect if appropriate
var runes = slash.FindGameObjectInChildren("Runes");
+ runes?.SetActive(isShaman);
- if (runes != null) {
- runes.SetActive(isShaman);
- }
-
+ // Add damager
var damager = slash.FindGameObjectInChildren("Enemy_Damager");
if (damager != null) {
SetDamageHeroState(damager);
@@ -116,10 +136,17 @@ private IEnumerator PlayDash(GameObject playerObject, bool isShaman, bool isVolt
}
}
- private static bool TryGetParryThread(GameObject playerObject, bool zap, [MaybeNullWhen(false)] out GameObject thread) {
+ ///
+ /// Attempts to get the parry thread effect for the preparation animation
+ ///
+ /// The player using the skill
+ /// If volt filament effects should be used
+ /// The effect, if found
+ /// true if found
+ private static bool TryGetParryThread(GameObject playerObject, bool isVolt, [MaybeNullWhen(false)] out GameObject thread) {
// Find existing object
- var name = zap ? "Parry Thread Zap" : "Parry Thread";
- var created = FindOrCreateAttack(playerObject, name, out var threadObj);
+ var name = isVolt ? "Parry Thread Zap" : "Parry Thread";
+ var created = FindOrCreateSkill(playerObject, name, out var threadObj);
if (threadObj == null) {
thread = null;
return false;
@@ -128,7 +155,8 @@ private static bool TryGetParryThread(GameObject playerObject, bool zap, [MaybeN
thread = threadObj;
if (!created) return true;
- if (zap) {
+ // Remove existing components if applicable
+ if (isVolt) {
thread.DestroyGameObjectInChildren("light_effect_v02 (2)");
thread.SetActiveChildren(true);
@@ -138,8 +166,15 @@ private static bool TryGetParryThread(GameObject playerObject, bool zap, [MaybeN
return true;
}
+ ///
+ /// Attempts to get the flash effect for the preparation animation
+ ///
+ /// The player using the skill
+ /// The effect, if found
+ /// true if found
private static bool TryGetStanceFlash(GameObject playerObject, [MaybeNullWhen(false)] out GameObject flash) {
- var created = FindOrCreateAttack(playerObject, ParryStanceFlashName, out var flashObj);
+ // Find or create
+ var created = FindOrCreateSkill(playerObject, "Parry Stance Flash", out var flashObj);
if (flashObj == null) {
flash = null;
return false;
@@ -148,14 +183,22 @@ private static bool TryGetStanceFlash(GameObject playerObject, [MaybeNullWhen(fa
flash = flashObj;
if (!created) return true;
+ // Destroy components/children if created
flash.DestroyComponentsInChildren();
flash.DestroyGameObjectInChildren("Shaman Flash Glow");
return true;
}
+ ///
+ /// Attempts to get the parry activation clash effect
+ ///
+ /// The player using the skill
+ /// The effect, if found
+ /// true if found
private static bool TryGetClashEffect(GameObject playerObject, [MaybeNullWhen(false)] out GameObject clash) {
- var created = FindOrCreateAttack(playerObject, ParryClashName, out var clashObj);
+ // Find or create
+ var created = FindOrCreateSkill(playerObject, "Parry Clash Effect", out var clashObj);
if (clashObj == null) {
clash = null;
return false;
@@ -164,21 +207,31 @@ private static bool TryGetClashEffect(GameObject playerObject, [MaybeNullWhen(fa
clash = clashObj;
if (!created) return true;
+ // Remove components if created
clash.DestroyComponentsInChildren();
return true;
}
- private static bool TryGetSlashEffect(GameObject playerObject, bool isZap, [MaybeNullWhen(false)] out GameObject slash) {
- var name = isZap ? ParryZapSlashName : ParrySlashName;
-
- var attacks = GetPlayerSilkAttacks(playerObject);
+ ///
+ /// Attempts to get the parry activation slash dash effect
+ ///
+ /// The player using the skill
+ /// If volt filament effects should be used
+ /// The effect, if found
+ /// true if found
+ private static bool TryGetSlashEffect(GameObject playerObject, bool isVolt, [MaybeNullWhen(false)] out GameObject slash) {
+ var name = isVolt ? "Parry Slash Effect Zap" : "Parry Slash Effect";
+
+ // Find existing slash
+ var attacks = GetPlayerSilkSkills(playerObject);
slash = attacks.FindGameObjectInChildren(name);
if (slash) {
return true;
}
+ // Get the correct slash
var fsm = GetSkillFSM();
var boolTest = fsm.GetAction("Parry Cross Slash", 8);
if (boolTest == null) {
@@ -186,12 +239,13 @@ private static bool TryGetSlashEffect(GameObject playerObject, bool isZap, [Mayb
}
GameObject localSlashObj;
- if (isZap) {
+ if (isVolt) {
localSlashObj = boolTest.TrueGameObject.Value;
} else {
localSlashObj = boolTest.FalseGameObject.Value;
}
+ // Create an instance of it, then prepare
slash = Object.Instantiate(localSlashObj, attacks.transform);
slash.name = name;
slash.transform.localPosition = new Vector3(1, 1, 0);
@@ -199,13 +253,8 @@ private static bool TryGetSlashEffect(GameObject playerObject, bool isZap, [Mayb
slash.DestroyComponentsInChildren();
slash.DestroyGameObjectInChildren("haze2");
- var runeFlash = slash
- .FindGameObjectInChildren("Runes")?
- .FindGameObjectInChildren("Runes Flash");
-
- if (runeFlash != null) {
- Object.Destroy(runeFlash);
- }
+ slash.FindGameObjectInChildren("Runes")?
+ .DestroyGameObjectInChildren("Runes Flash");
// Enable damage collider
var damager = slash.FindGameObjectInChildren("Enemy_Damager");
diff --git a/SSMP/Animation/Effects/SilkSkills/PaleNails.cs b/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
index 9a473f3b..2f639d22 100644
--- a/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
+++ b/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
@@ -14,18 +14,35 @@
namespace SSMP.Animation.Effects.SilkSkills;
internal class PaleNails : BaseSilkSkill {
-
+ ///
+ /// The name of the blade summoning effect object
+ ///
private const string AnticName = "Hornet_finger_blade_cast_silk";
+ ///
+ /// The number of nails to summon
+ ///
private const int NailCount = 3;
+ ///
+ /// Offset to keep negative values when converting a short to a ushort
+ ///
private const int PositionOffset = short.MaxValue;
+ ///
+ /// Scale used to keep a higher level of precision when converting a float to a short
+ ///
private const int PositionScale = 5;
+ ///
+ /// Determines if this animation is for the summoning antic
+ ///
public bool IsAntic = false;
- private static Dictionary _playerNails = new();
+ ///
+ /// Cached nail objects for players. Used when firing.
+ ///
+ private static readonly Dictionary PlayerNails = [];
///
public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
@@ -42,11 +59,11 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
// Get existing nails
var id = playerObject.GetInstanceID();
- if (!_playerNails.TryGetValue(id, out var nails) || nails.Length == 0) {
+ if (!PlayerNails.TryGetValue(id, out var nails) || nails.Length == 0) {
return;
}
- // Ensure nails aren't despawned
+ // Ensure nails aren't de-spawned
if (nails.Any(obj => obj == null)) {
return;
}
@@ -71,16 +88,16 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
/// If the volt filament effect should be used
/// The 'id' of the player firing the nails
private static IEnumerator PlayNailFireTargeted(GameObject target, GameObject[] nails, bool isVolt, int playerId) {
- // Copy nails
+ // Copy nails and clear array before they can be fired by anything else
GameObject[] playerNails = [.. nails];
- _playerNails[playerId] = [];
+ PlayerNails[playerId] = [];
// Play audio
if (isVolt) PlayVoltAudio(playerNails[0]);
// Fire each nail
foreach (var nail in playerNails) {
- // Nail has already been despawned
+ // A nail has already de-spawned
if (nail == null) {
yield break;
}
@@ -108,7 +125,7 @@ private static void PlayNailFireUnguided(GameObject[] nails, bool isVolt, int pl
// Fire each nail
foreach (var nail in nails) {
- // Nail has already been despawned
+ // Nail has already de-spawned
if (nail == null) {
return;
}
@@ -122,7 +139,7 @@ private static void PlayNailFireUnguided(GameObject[] nails, bool isVolt, int pl
}
// Clear nails
- _playerNails[playerId] = [];
+ PlayerNails[playerId] = [];
}
///
@@ -137,7 +154,7 @@ private IEnumerator PlayAntic(GameObject playerObject, bool isVolt, bool isShama
// Fire existing nails
var id = playerObject.GetInstanceID();
- if (_playerNails.TryGetValue(id, out var existingNails)) {
+ if (PlayerNails.TryGetValue(id, out var existingNails)) {
PlayNailFireUnguided(existingNails, isVolt, id);
}
@@ -219,7 +236,7 @@ private IEnumerator PlayAntic(GameObject playerObject, bool isVolt, bool isShama
}
// Store nails for firing later
- _playerNails[id] = nails;
+ PlayerNails[id] = nails;
// Wait for nail hover time to expire
yield return new WaitForSeconds(1.8f);
@@ -256,11 +273,11 @@ private static void SetupNailFSM(GameObject playerObject, GameObject[] nails, in
FixFsmForUse(fsm, playerObject, isVolt);
+ // Set nail buddy and event
string position;
GameObject buddy1;
GameObject buddy2;
- // Set nail buddy and event
if (index == 0) {
position = "TOP1";
buddy1 = nails[1];
@@ -448,13 +465,13 @@ public static byte[] EncodeTargetInfo(GameObject target) {
var position = target.transform.position;
var isPlayer = target.GetComponent() != null;
- // Convert floats to ushorts
+ // Convert floats to ushorts, scaling to preserve some precision, and offsetting to keep negative values
var x = (ushort) ((position.x * PositionScale) + PositionOffset);
var y = (ushort) ((position.y * PositionScale) + PositionOffset);
// Split shorts into bytes
return [
- GetEffectFlags()[0], // get volt status
+ GetEffectFlags()[0], // include volt status
(byte)(x & 0xFF),
(byte)(x >> 8),
(byte)(y & 0xFF),
@@ -502,6 +519,8 @@ public GameObject FindTarget(NailTarget target) {
// Prioritize player
if (target.IsPlayer) {
+ // CoroutineCancelComponent is on all player objects and nothing else.
+ // May be a good idea to create a custom component specifically for identifying players.
var player = inside.FirstOrDefault(obj => (bool)obj.GetComponent() || (bool)obj.GetComponent());
if (player) {
return player.gameObject;
diff --git a/SSMP/Animation/Effects/SilkSkills/RuneRage.cs b/SSMP/Animation/Effects/SilkSkills/RuneRage.cs
index 2da2568b..a2978370 100644
--- a/SSMP/Animation/Effects/SilkSkills/RuneRage.cs
+++ b/SSMP/Animation/Effects/SilkSkills/RuneRage.cs
@@ -5,7 +5,6 @@
using SSMP.Internals;
using SSMP.Util;
using UnityEngine;
-using Logger = SSMP.Logging.Logger;
using Object = UnityEngine.Object;
namespace SSMP.Animation.Effects.SilkSkills;
@@ -101,7 +100,7 @@ private static void PlayRageAntic(GameObject playerObject, bool isVolt) {
var fsm = GetSkillFSM();
- // Play audio in S Bomb Zap FX 1
+ // Play volt audio
if (isVolt) {
var voltAntic = fsm.GetFirstAction("S Bomb Zap FX");
if (voltAntic != null) {
@@ -109,7 +108,7 @@ private static void PlayRageAntic(GameObject playerObject, bool isVolt) {
}
}
- // Play audio in Silk Bomb Start
+ // Play normal audio
var runeAnticAudio = fsm.GetAction("Silk Bomb Start", 15);
if (runeAnticAudio != null) {
AudioUtil.PlayAudio(runeAnticAudio, playerObject);
@@ -125,13 +124,13 @@ private static void PlayRageAntic(GameObject playerObject, bool isVolt) {
private void PlaySonar(GameObject playerObject, bool isVolt, bool isShaman) {
var fsm = GetSkillFSM();
- // Play audio in Initial Silk Cost
+ // Play general audio
var runeBurstAudio = fsm.GetFirstAction("Initial Silk Cost");
if (runeBurstAudio != null) {
AudioUtil.PlayAudio(runeBurstAudio, playerObject);
}
- // Play audio in S Bomb Volt FX 2
+ // Play volt audio
if (isVolt) {
var zapAudioBug = fsm.GetFirstAction("S Bomb Zap FX 2");
if (zapAudioBug != null) {
@@ -178,7 +177,7 @@ private void PlaySonar(GameObject playerObject, bool isVolt, bool isShaman) {
/// If the shaman crest effects should be used
private void PlayRuneRage(List positions, bool isVolt, bool isShaman) {
// Generate spawn template
- // Template layout looks like . * .
+ // Template layout kinda looks like . * .
if (_clusterSpawnTemplate == null) {
_clusterSpawnTemplate = new GameObject().transform;
var firstBlast = new GameObject().transform;
@@ -213,7 +212,7 @@ private void PlayRuneRage(List positions, bool isVolt, bool isShaman) {
///
private static bool TryGetAntic(GameObject playerObject, [MaybeNullWhen(false)] out GameObject antic) {
// Find or create the antic object
- var created = FindOrCreateAttack(playerObject, AnticName, out var anticObj);
+ var created = FindOrCreateSkill(playerObject, AnticName, out var anticObj);
if (anticObj == null) {
antic = null;
return false;
diff --git a/SSMP/Animation/Effects/SilkSkills/SharpDart.cs b/SSMP/Animation/Effects/SilkSkills/SharpDart.cs
index b5162106..779b6548 100644
--- a/SSMP/Animation/Effects/SilkSkills/SharpDart.cs
+++ b/SSMP/Animation/Effects/SilkSkills/SharpDart.cs
@@ -1,29 +1,36 @@
-using System;
using System.Collections;
-using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
-using System.Text;
using SSMP.Internals;
using SSMP.Util;
using UnityEngine;
-using Logger = SSMP.Logging.Logger;
using Object = UnityEngine.Object;
namespace SSMP.Animation.Effects.SilkSkills;
internal class SharpDart : BaseSilkSkill {
- private const string DashBurstName = "Silk Charge DashBurst";
- private const string ZapParticlesName = "Silk Charge Particles Zap";
- private const string DashDamagerName = "Silk Charge Damager";
-
+ ///
+ /// The name of the volt particles object
+ ///
+ private const string VoltParticlesName = "Silk Charge Particles Zap";
+
+ ///
+ /// If this instance of sharp dart is for the volt filament variant
+ ///
public bool Volt = false;
+ ///
public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
var isShaman = crestType == CrestType.Shaman;
MonoBehaviourUtil.Instance.StartCoroutine(PlayEffect(playerObject, isShaman));
}
+ ///
+ /// Plays the sharp dart effect
+ ///
+ /// The player using the skill
+ /// If the shaman crest effects should be used
private IEnumerator PlayEffect(GameObject playerObject, bool isShaman) {
+ // Set up damager
if (TryGetDamager(playerObject, out var damager)) {
SetDamageHeroState(damager);
damager.SetActive(true);
@@ -35,26 +42,29 @@ private IEnumerator PlayEffect(GameObject playerObject, bool isShaman) {
}
}
+ // Play dash burst
if (TryGetDashBurst(playerObject, out var dashBurst)) {
dashBurst.SetActive(false);
dashBurst.SetActive(true);
}
- if (Volt && TryGetParticles(playerObject, out var particles)) {
- particles.SetActive(false);
- particles.SetActive(true);
- }
+ var fsm = GetSkillFSM();
// Play sound effects
PlayHornetAttackSound(playerObject);
- var fsm = GetSkillFSM();
var chargeAntic = fsm.GetAction("Silk Charge Begin", 5);
if (chargeAntic != null) AudioUtil.PlayAudio(chargeAntic, playerObject);
+ // Play volt effects
if (Volt) {
var voltNoise = fsm.GetFirstAction("Silk Charge Zap FX");
if (voltNoise != null) AudioUtil.PlayAudio(voltNoise, playerObject);
+
+ if (TryGetParticles(playerObject, out var particles)) {
+ particles.SetActive(false);
+ particles.SetActive(true);
+ }
}
// Brief pause for ending sound
@@ -64,52 +74,41 @@ private IEnumerator PlayEffect(GameObject playerObject, bool isShaman) {
if (chargeFull != null) AudioUtil.PlayAudio(chargeFull, playerObject);
}
- private bool TryGetDashBurst(GameObject playerObject, [MaybeNullWhen(false)] out GameObject dashBurst) {
- // Find existing object
- var attacks = GetPlayerSilkAttacks(playerObject);
- dashBurst = attacks.FindGameObjectInChildren(DashBurstName);
+ ///
+ /// Attempts to get the dash burst effect
+ ///
+ /// The player using the skill
+ /// The effect, if found
+ /// true if the effect was found
+ private static bool TryGetDashBurst(GameObject playerObject, [MaybeNullWhen(false)] out GameObject dashBurst) {
+ FindOrCreateSkill(playerObject, "Silk Charge DashBurst", out dashBurst);
+
if (dashBurst) {
+ dashBurst.transform.localScale = new Vector3(0.75f, 0.75f, 0.75f);
return true;
}
- // Copy from local attacks
- if (!TryGetLocalSilkAttacks(out var localSilkAttacks)) {
- return false;
- }
-
- var localDashBurst = localSilkAttacks.FindGameObjectInChildren(DashBurstName);
- if (!localDashBurst) {
- return false;
- }
-
- dashBurst = Object.Instantiate(localDashBurst, attacks.transform);
- dashBurst.name = DashBurstName;
- dashBurst.transform.localScale = new Vector3(0.75f, 0.75f, 0.75f);
-
- return true;
+ return false;
}
- private bool TryGetDamager(GameObject playerObject, [MaybeNullWhen(false)] out GameObject damager) {
- // Find existing object
- var attacks = GetPlayerSilkAttacks(playerObject);
- damager = attacks.FindGameObjectInChildren(DashDamagerName);
- if (damager) {
- return true;
- }
-
- // Copy from local attacks
- if (!TryGetLocalSilkAttacks(out var localSilkAttacks)) {
+ ///
+ /// Attempts to get the damage object
+ ///
+ /// The player using the skill
+ /// The damager, if found
+ /// true if the damager was found
+ private static bool TryGetDamager(GameObject playerObject, [MaybeNullWhen(false)] out GameObject damager) {
+ // Find or create effect
+ var created = FindOrCreateSkill(playerObject, "Silk Charge Damager", out damager);
+ if (!damager) {
return false;
}
- var localDamager = localSilkAttacks.FindGameObjectInChildren(DashDamagerName);
- if (!localDamager) {
- return false;
+ if (!created) {
+ return true;
}
- damager = Object.Instantiate(localDamager, attacks.transform);
- damager.name = DashDamagerName;
-
+ // Set up effect if created
var delay = damager.AddComponent();
delay.time = 0.3f;
@@ -117,41 +116,46 @@ private bool TryGetDamager(GameObject playerObject, [MaybeNullWhen(false)] out G
damager.DestroyComponentsInChildren();
- var runeBloom = damager
- .FindGameObjectInChildren("Shaman Rune")?
- .FindGameObjectInChildren("Shaman Rune Camera Bloom");
-
- if (runeBloom) {
- Object.DestroyImmediate(runeBloom);
- }
+ damager.FindGameObjectInChildren("Shaman Rune")?
+ .DestroyGameObjectInChildren("Shaman Rune Camera Bloom");
return true;
}
- private bool TryGetParticles(GameObject playerObject, [MaybeNullWhen(false)] out GameObject particles) {
+ ///
+ /// Attempts to get the volt particles
+ ///
+ /// The player using the skill
+ /// The particles, if found
+ /// true if the particles were found
+ private static bool TryGetParticles(GameObject playerObject, [MaybeNullWhen(false)] out GameObject particles) {
+ // Find effects
var effects = playerObject.FindGameObjectInChildren("Effects");
if (!effects) {
- particles = null;
- return false;
+ effects = new GameObject("Effects");
+ effects.transform.SetParentReset(playerObject.transform);
}
- particles = effects.FindGameObjectInChildren(ZapParticlesName);
+ // Find existing effect
+ particles = effects.FindGameObjectInChildren(VoltParticlesName);
if (particles) {
return true;
}
+ // Create new effect
var localParticles = HeroController.instance.gameObject
.FindGameObjectInChildren("Effects")?
- .FindGameObjectInChildren(ZapParticlesName);
+ .FindGameObjectInChildren(VoltParticlesName);
if (!localParticles) {
return false;
}
particles = Object.Instantiate(localParticles, effects.transform);
- particles.name = ZapParticlesName;
+ particles.name = VoltParticlesName;
+ // Set up components
if (particles.TryGetComponent(out var system)) {
var emission = system.emission;
emission.enabled = true;
diff --git a/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs b/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
index 6d57b9f7..db11ed1a 100644
--- a/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
+++ b/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
@@ -27,18 +27,18 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
// Set volt settings
var volt = IsVolt(effectInfo);
- var zapThread = parent
+ var voltThread = parent
.FindGameObjectInChildren("thread")?
.FindGameObjectInChildren("zap thread");
- if (zapThread) zapThread.SetActive(volt);
+ if (voltThread) voltThread.SetActive(volt);
var needle = parent.FindGameObjectInChildren("needle");
- var zapNeedle = needle?.FindGameObjectInChildren("Zap Effect Activator");
- if (zapNeedle) {
- zapNeedle.SetActive(volt);
- zapNeedle.SetActiveChildren(volt);
+ var voltNeedle = needle?.FindGameObjectInChildren("Zap Effect Activator");
+ if (voltNeedle) {
+ voltNeedle.SetActive(volt);
+ voltNeedle.SetActiveChildren(volt);
}
// Set shaman settings
@@ -52,8 +52,8 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
if (shamanRune) {
shamanRune.SetActive(isShaman);
- var zapRune = shamanRune.FindGameObjectInChildren("Zap Rune");
- if (zapRune) zapRune.SetActive(volt);
+ var voltRune = shamanRune.FindGameObjectInChildren("Zap Rune");
+ if (voltRune) voltRune.SetActive(volt);
}
}
@@ -137,12 +137,12 @@ private static IEnumerator PlayPossibleThunk(GameObject playerObject, GameObject
/// The spear, if found
private static GameObject? GetSilkSpear(GameObject playerObject) {
// Find existing silk spear
- var silkAttacks = GetPlayerSilkAttacks(playerObject);
+ var silkAttacks = GetPlayerSilkSkills(playerObject);
var spear = silkAttacks.FindGameObjectInChildren(SpearObjectName);
if (spear) return spear;
// Find on own silk attacks
- if (!TryGetLocalSilkAttacks(out var localSilkAttacks)) {
+ if (!TryGetLocalSilkSkills(out var localSilkAttacks)) {
return null;
}
diff --git a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
index f276e840..21af7466 100644
--- a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
+++ b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
@@ -10,20 +10,22 @@
namespace SSMP.Animation.Effects.SilkSkills;
internal class ThreadStorm : BaseSilkSkill {
- private const string SkillObjectName = "Sphere Ball";
- private static GameObject? _localThreadStorm;
-
- private static Dictionary _playerExtensions = new();
+ ///
+ /// A reference for players currently extending their thread storms.
+ /// Used to prevent the effect from disappearing early.
+ ///
+ private static readonly Dictionary PlayerExtensions = [];
+ ///
public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
var volt = IsVolt(effectInfo);
var isShaman = crestType == CrestType.Shaman;
// Update number of extensions
var playerId = playerObject.GetInstanceID();
- var extensions = _playerExtensions.GetValueOrDefault(playerId, 0);
- _playerExtensions[playerId] = extensions + 1;
+ var extensions = PlayerExtensions.GetValueOrDefault(playerId, 0);
+ PlayerExtensions[playerId] = extensions + 1;
// Play extension if applicable
if (extensions > 0) {
@@ -134,8 +136,8 @@ private static void AnimateScaleReset(GameObject ball) {
private static void AttemptStop(GameObject playerObject, GameObject threadStorm) {
// Decrement extension count
var playerId = playerObject.GetInstanceID();
- var extensions = _playerExtensions.GetValueOrDefault(playerId, 1);
- _playerExtensions[playerId] = Mathf.Max(0, extensions - 1);
+ var extensions = PlayerExtensions.GetValueOrDefault(playerId, 1);
+ PlayerExtensions[playerId] = Mathf.Max(0, extensions - 1);
// There are more extensions, don't deactivate yet
if (extensions > 1) {
@@ -159,7 +161,6 @@ private static void ForceStop(GameObject threadStorm) {
var audio = threadStorm.GetComponent();
audio.Stop();
threadStorm.SetActive(false);
-
}
///
@@ -172,34 +173,15 @@ private static bool TryGetThreadStorm(
GameObject playerObject,
[MaybeNullWhen(false)] out GameObject threadStorm
) {
- // Find existing thread storm
- var parent = GetPlayerSilkAttacks(playerObject);
- threadStorm = parent.FindGameObjectInChildren(SkillObjectName);
- if (threadStorm) {
- return true;
+ // Find or create effect
+ var created = FindOrCreateSkill(playerObject, "Sphere Ball", out threadStorm);
+ if (!threadStorm) {
+ return false;
}
- // Not found, locate it on the player
- var localStorm = _localThreadStorm;
- if (localStorm == null) {
- // Get local silk attacks
- if (!TryGetLocalSilkAttacks(out var localSilkAttacks)) {
- return false;
- }
-
- // Find the thread storm
- localStorm = localSilkAttacks.FindGameObjectInChildren(SkillObjectName);
- if (localStorm == null) {
- Logger.Warn("Unable to get local Thread Storm object");
- return false;
- }
-
- _localThreadStorm = localStorm;
+ if (created) {
+ return true;
}
-
- // Copy to the player object
- threadStorm = Object.Instantiate(localStorm, parent.transform);
- threadStorm.name = SkillObjectName;
// Remove components that could interfere
threadStorm.DestroyComponent();
From d7b5f7850135530aa181b774fbe435976ec8e0a6 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Mon, 20 Apr 2026 01:25:56 -0400
Subject: [PATCH 22/41] more docs
---
SSMP/Animation/AnimationManager.cs | 53 +++++++++++++++++-------------
SSMP/Game/Client/ClientManager.cs | 2 +-
2 files changed, 31 insertions(+), 24 deletions(-)
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index 26d3d037..3d896c72 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -755,11 +755,14 @@ internal class AnimationManager {
public AnimationManager(
NetClient netClient,
PlayerManager playerManager,
+ ServerSettings serverSettings,
Dictionary playerData
) {
_netClient = netClient;
_playerManager = playerManager;
_playerData = playerData;
+ _serverSettings = serverSettings;
+
// _chargedEffectStopwatch = new Stopwatch();
// _chargedEndEffectStopwatch = new Stopwatch();
}
@@ -798,9 +801,9 @@ public void RegisterHooks() {
EventHooks.HeroControllerDie += OnDeath;
// Register FSM hooks for certain bind actions
- HeroController.OnHeroInstanceSet += CreateHeroHooksHooks;
+ HeroController.OnHeroInstanceSet += CreateHeroHooks;
if (HeroController.SilentInstance != null) {
- CreateHeroHooksHooks(HeroController.instance);
+ CreateHeroHooks(HeroController.instance);
}
@@ -825,7 +828,7 @@ public void RegisterHooks() {
public void DeregisterHooks() {
SceneManager.activeSceneChanged -= OnSceneChange;
- HeroController.OnHeroInstanceSet -= CreateHeroHooksHooks;
+ HeroController.OnHeroInstanceSet -= CreateHeroHooks;
FsmStateActionInjector.UninjectAll();
// On.HeroAnimationController.Play -= HeroAnimationControllerOnPlay;
// On.HeroAnimationController.PlayFromFrame -= HeroAnimationControllerOnPlayFromFrame;
@@ -1080,7 +1083,7 @@ private void OnAnimationEvent(tk2dSpriteAnimationClip clip) {
/// Creates hooks for the Witch Tentacles and Shaman Cancel states in
/// the Bind fsm once the HeroController is ready.
///
- private void CreateHeroHooksHooks(HeroController hc) {
+ private void CreateHeroHooks(HeroController hc) {
// Initialize warding bell FSM if it isn't already.
// This fills it in with the template
var bellFsm = HeroController.instance.bellBindFSM;
@@ -1092,20 +1095,20 @@ private void CreateHeroHooksHooks(HeroController hc) {
var heroFsms = hc.GetComponents();
var bindFsm = heroFsms.FirstOrDefault(fsm => fsm.FsmName == "Bind");
- if (bindFsm == null) {
+ if (bindFsm != null) {
+ // Find FSM states to inject
+ var tentacles = bindFsm.GetState("Witch Tentancles!"); // no that's not a typo... at least on my end
+ FsmStateActionInjector.Inject(tentacles, OnWitchTentacles, 4);
+
+ var shamanCancel = bindFsm.GetState("Shaman Air Cancel");
+ FsmStateActionInjector.Inject(shamanCancel, OnShamanCancel);
+
+ var bindInterrupt = bindFsm.GetState("Remove Silk?");
+ FsmStateActionInjector.Inject(bindInterrupt, OnBindInterrupt, 2);
+ } else {
Logger.Warn("Unable to find Bind FSM to hook.");
- return;
}
- // Find FSM states to inject
- var tentacles = bindFsm.GetState("Witch Tentancles!"); // no that's not a typo... at least on my end
- FsmStateActionInjector.Inject(tentacles, OnWitchTentacles, 4);
-
- var shamanCancel = bindFsm.GetState("Shaman Air Cancel");
- FsmStateActionInjector.Inject(shamanCancel, OnShamanCancel);
-
- var bindInterrupt = bindFsm.GetState("Remove Silk?");
- FsmStateActionInjector.Inject(bindInterrupt, OnBindInterrupt, 2);
// Silk skill injections
@@ -1124,13 +1127,13 @@ private void CreateHeroHooksHooks(HeroController hc) {
FsmStateActionInjector.Inject(sonarBuildArray, OnBuildRuneRageArray, 0);
var blastEnemy = silkSkillFsm.GetState("Blast Enemy");
- FsmStateActionInjector.Inject(blastEnemy, OnBlastEnemy, 4);
+ FsmStateActionInjector.Inject(blastEnemy, OnRuneBlastEnemy, 4);
var blastRandom = silkSkillFsm.GetState("Random Blasts");
- FsmStateActionInjector.Inject(blastRandom, OnBlastRandom, 3);
+ FsmStateActionInjector.Inject(blastRandom, OnRuneBlastRandom, 3);
var blastFinished = silkSkillFsm.GetState("Silk Bomb Recover");
- FsmStateActionInjector.Inject(blastFinished, OnBlastFinished, 0);
+ FsmStateActionInjector.Inject(blastFinished, OnRuneBlastFinished, 0);
// Pale Nails
var nailObject = silkSkillFsm.GetAction("BossNeedle Cast", 5)?.gameObject.Value;
@@ -1195,12 +1198,14 @@ private void OnWitchTentacles(PlayMakerFSM fsm) {
///
/// Animation subanimation hook for the Shaman Air Cancel FSM state
///
+
private void OnShamanCancel(PlayMakerFSM fsm) {
var dummyClip = new tk2dSpriteAnimationClip();
dummyClip.name = "Shaman Cancel";
dummyClip.wrapMode = tk2dSpriteAnimationClip.WrapMode.Once;
OnAnimationEvent(dummyClip);
}
+
///
/// Animation subanimation hook for interrupted binds
///
@@ -1211,10 +1216,12 @@ private void OnBindInterrupt(PlayMakerFSM fsm) {
OnAnimationEvent(dummyClip);
}
+ ///
+ /// Animation subanimation hook for extending a thread storm
+ ///
private void OnThreadStormExtend(PlayMakerFSM fsm) {
var effectInfo = BaseSilkSkill.GetEffectFlags();
_netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.AirSphereRefresh, 0, effectInfo);
-
}
///
@@ -1245,7 +1252,7 @@ private void OnBuildRuneRageArray(PlayMakerFSM fsm) {
// Remove any old player objects
sonar.Refresh();
- // If PvP is off, remove any players that might be inside
+ // No need to target if pvp is off
if (!_serverSettings.IsPvpEnabled) {
return;
}
@@ -1284,7 +1291,7 @@ private void OnBuildRuneRageArray(PlayMakerFSM fsm) {
///
/// Intercepts the spawn locations for targeted Rune Rages
///
- private void OnBlastEnemy(PlayMakerFSM fsm) {
+ private void OnRuneBlastEnemy(PlayMakerFSM fsm) {
// At this point the rune cluster has been created with a position and a given offset from the object.
// This position is relative to the player.
var spawnPosition = fsm.FsmVariables.FindFsmVector3("Shift Pos");
@@ -1297,7 +1304,7 @@ private void OnBlastEnemy(PlayMakerFSM fsm) {
///
/// Intercepts the spawn locations for random Rune Rages
///
- private void OnBlastRandom(PlayMakerFSM fsm) {
+ private void OnRuneBlastRandom(PlayMakerFSM fsm) {
// If there aren't enough targets, rune rage will spawn up to 3 other blasts
var spawnRadial = fsm.GetFirstAction("Random Blasts");
if (spawnRadial != null) {
@@ -1315,7 +1322,7 @@ private void OnBlastRandom(PlayMakerFSM fsm) {
///
/// Animation hook to send Rune Rage positions
///
- private void OnBlastFinished(PlayMakerFSM fsm) {
+ private void OnRuneBlastFinished(PlayMakerFSM fsm) {
var effectInfo = BaseSilkSkill.GetEffectFlags().ToList();
effectInfo.AddRange(_runeRagePositions);
diff --git a/SSMP/Game/Client/ClientManager.cs b/SSMP/Game/Client/ClientManager.cs
index 83bbb766..e6deabce 100644
--- a/SSMP/Game/Client/ClientManager.cs
+++ b/SSMP/Game/Client/ClientManager.cs
@@ -229,7 +229,7 @@ ModSettings modSettings
_playerData = new Dictionary();
_playerManager = new PlayerManager(serverSettings, _playerData);
- _animationManager = new AnimationManager(netClient, _playerManager, _playerData);
+ _animationManager = new AnimationManager(netClient, _playerManager, serverSettings, _playerData);
_mapManager = new MapManager(netClient, serverSettings);
_entityManager = new EntityManager(netClient);
From e3401b9b876bb27e218073dca4b503b95eadf1a4 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Mon, 20 Apr 2026 01:33:47 -0400
Subject: [PATCH 23/41] linting!
---
SSMP/Fsm/FsmActionInjectorComponent.cs | 8 ++++----
SSMP/Fsm/FsmStateActionInjector.cs | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/SSMP/Fsm/FsmActionInjectorComponent.cs b/SSMP/Fsm/FsmActionInjectorComponent.cs
index 04e5c493..723f622f 100644
--- a/SSMP/Fsm/FsmActionInjectorComponent.cs
+++ b/SSMP/Fsm/FsmActionInjectorComponent.cs
@@ -9,7 +9,7 @@ namespace SSMP.Fsm;
/// A component to copy FSM injections between objects. An instantiated object can't copy Actions, so this bridges the gap.
///
internal class FsmActionInjectorComponent : MonoBehaviour {
- private static Dictionary> _allInjections = [];
+ private static readonly Dictionary> AllInjections = [];
private List _injections = [];
@@ -21,7 +21,7 @@ internal class FsmActionInjectorComponent : MonoBehaviour {
public void Awake() {
_injected = false;
- if (_allInjections.TryGetValue(_injectionIndex, out var injections)) {
+ if (AllInjections.TryGetValue(_injectionIndex, out var injections)) {
_injections = injections;
TryDoInjection();
}
@@ -34,10 +34,10 @@ public void Awake() {
public void SetInjections(List injections) {
// Set injections and index
_injections = injections;
- _injectionIndex = _allInjections.Count + 1;
+ _injectionIndex = AllInjections.Count + 1;
// Add to static collection of injections and inject
- _allInjections.Add(_injectionIndex, injections);
+ AllInjections.Add(_injectionIndex, injections);
TryDoInjection();
}
diff --git a/SSMP/Fsm/FsmStateActionInjector.cs b/SSMP/Fsm/FsmStateActionInjector.cs
index f26750ab..2e528879 100644
--- a/SSMP/Fsm/FsmStateActionInjector.cs
+++ b/SSMP/Fsm/FsmStateActionInjector.cs
@@ -46,7 +46,7 @@ public void Uninject() {
///
public override void OnEnter() {
if (_onStateEnter != null) {
- try {
+ try {
_onStateEnter?.Invoke(Fsm.FsmComponent);
} catch (Exception e) {
Logger.Error(e.ToString());
From 43421a75c23f1f8a124d7256b890f5096ace449b Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Mon, 20 Apr 2026 02:15:20 -0400
Subject: [PATCH 24/41] fix weird bug where audio wouldn't play
---
.../Effects/SilkSkills/ThreadStorm.cs | 23 +++++++++++++------
1 file changed, 16 insertions(+), 7 deletions(-)
diff --git a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
index 21af7466..73958f9c 100644
--- a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
+++ b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
@@ -1,6 +1,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
+using HutongGames.PlayMaker.Actions;
using SSMP.Internals;
using SSMP.Util;
using UnityEngine;
@@ -29,7 +30,6 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
// Play extension if applicable
if (extensions > 0) {
- Logger.Info("Playing extension");
MonoBehaviourUtil.Instance.StartCoroutine(PlayStormExtension(playerObject));
return;
}
@@ -64,6 +64,13 @@ private IEnumerator PlayStormExtension(GameObject playerObject, bool initial = f
AnimateScaleReset(damager);
}
+ // Play audio
+ var fsm = GetSkillFSM();
+ var extendAudio = fsm.GetFirstAction("Extend");
+ if (extendAudio.oneShotClip.Value is AudioClip clip) {
+ AudioUtil.PlayAudio(clip, playerObject);
+ }
+
yield return new WaitForSeconds(0.65f);
AttemptStop(playerObject, threadStorm);
@@ -73,9 +80,9 @@ private IEnumerator PlayStormExtension(GameObject playerObject, bool initial = f
/// Initializes and activates the Thread Storm attack, setting up sub-effects
///
/// The player object that used the attack.
- /// Determines if the volt filament effect should be enabled.
+ /// Determines if the volt filament effect should be enabled.
/// Determines if the shaman crest effect should be displayed.
- private IEnumerator PlayStormSetup(GameObject playerObject, bool volt, bool isShaman) {
+ private IEnumerator PlayStormSetup(GameObject playerObject, bool isVolt, bool isShaman) {
if (!TryGetThreadStorm(playerObject, out var threadStorm)) {
yield break;
}
@@ -89,7 +96,7 @@ private IEnumerator PlayStormSetup(GameObject playerObject, bool volt, bool isSh
if (voltObject) {
voltObject.SetActive(false);
- voltObject.SetActive(volt);
+ voltObject.SetActive(isVolt);
}
// Enable shaman crest effect
@@ -113,9 +120,6 @@ private IEnumerator PlayStormSetup(GameObject playerObject, bool volt, bool isSh
}
// Play looping silk audio
- var audio = threadStorm.GetComponent();
- audio.Play();
-
// Play the main effect
MonoBehaviourUtil.Instance.StartCoroutine(PlayStormExtension(playerObject, true));
}
@@ -189,6 +193,11 @@ private static bool TryGetThreadStorm(
threadStorm.DestroyComponent();
threadStorm.DestroyComponent();
+ // Play looping silk audio
+ if (threadStorm.TryGetComponent(out var audio)) {
+ audio.playOnAwake = true;
+ }
+
// Set up shaman crest effects
var shamanRune = threadStorm.FindGameObjectInChildren("Shaman Rune");
if (shamanRune) {
From 1580039842ab595921316c9f4024663ecbb1751e Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Mon, 20 Apr 2026 16:59:55 -0400
Subject: [PATCH 25/41] add damage customization
---
README.md | 41 ++++
SSMP/Animation/DamageAnimationEffect.cs | 2 +-
SSMP/Animation/Effects/BindBurst.cs | 3 +-
SSMP/Animation/Effects/BindInterrupt.cs | 6 +-
SSMP/Animation/Effects/DashSlash.cs | 2 +-
SSMP/Animation/Effects/NeedleStrike.cs | 2 +-
.../Effects/SilkSkills/BaseSilkSkill.cs | 20 ++
.../Effects/SilkSkills/CrossStitch.cs | 2 +-
.../Animation/Effects/SilkSkills/PaleNails.cs | 4 +-
SSMP/Animation/Effects/SilkSkills/RuneRage.cs | 2 +-
.../Animation/Effects/SilkSkills/SharpDart.cs | 2 +-
.../Animation/Effects/SilkSkills/SilkSpear.cs | 14 +-
.../Effects/SilkSkills/ThreadStorm.cs | 5 +-
SSMP/Animation/Effects/SlashBase.cs | 2 +-
SSMP/Game/Settings/ServerSettings.cs | 212 +++++++++++++-----
15 files changed, 243 insertions(+), 76 deletions(-)
diff --git a/README.md b/README.md
index a08f8b80..67d3dab0 100644
--- a/README.md
+++ b/README.md
@@ -116,6 +116,47 @@ All names for the settings are case-insensitive, but are written in case for cla
- `AllowSkins`: Whether player skins are allowed.
If disabled, players will not be able to use a skin locally, nor will it be transmitted to other players.
- Aliases: `skins`
+- `NeedleDamage`: The number of masks of damage that a player's needle swing deals.
+ - Aliases: `needledmg`
+ - Default: 1
+- `NeedleStrikeDamage`: The number of masks of damage that Needle Strikes deal.
+ - Aliases: `needlestrikedmg`, `strikedmg`, `artdmg`
+ - Default: 2
+- `VoltFilamentDamage`: The number of extra half-masks of damage that silk skills with volt filament should deal.
+ Damage is rounded down after modifiers.
+ - Aliases: `voltfilamentdmg`, `voltdmg`, `filamentdmg`, `voltmodifier`, `voltmod`
+ - Default: 1
+- `ShamanDamage`: The number of extra half-masks of damage that silk skills on shaman crest should deal.
+ Damage is rounded down after modifiers.
+ - Aliases: `shamandmg`, `shamanmodifier`, `shamanmod`
+ - Default: 1
+- `CrossStitchDamage`: The number of masks of damage that Cross Stitch deals.
+ - Aliases: `crossstitchdmg`, `stitchdmg`, `parrydmg`
+ - Default: 1
+- `PaleNailsDamage`: The number of masks of damage that Pale Nails deals.
+ - Aliases: `palenailsdmg`, `palenaildmg`, `paledmg`
+ - Default: 1
+- `RuneRageDamage`: The number of masks of damage that Rune Rage deals.
+ - Aliases: `runeragedmg`
+ - Default: 1
+- `SharpDartDamage`: The number of masks of damage that Sharpdart deals.
+ - Aliases: `sharpdartdmg`, `dartdmg`
+ - Default: 1
+- `SilkSpearDamage`: The number of masks of damage that Silk Spear deals.
+ - Aliases: `silkspeardmg`, `speardmg`
+ - Default: 1
+- `ThreadStormDamage`: The number of masks of damage that Thread Storm deals.
+ - Aliases: `threadstormdmg`, `stormdmg`
+ - Default: 1
+- `WardingBellDamage`: The number of masks of damage that the Warding Bell deals.
+ - Aliases: `wardingbelldmg`, `bindbelldmg`, `belldmg`
+ - Default: 1
+- `ClawMirrorDamage`: The number of masks of damage that the base Claw Mirror deals.
+ - Aliases: `clawmirrordmg`, `mirrordmg`, `mirror1dmg`
+ - Default: 1
+- `ClawMirrorUpgradedDamage`: The number of masks of damage that the upgraded Claw Mirrors deal.
+ - Aliases: `clawmirrorupgradeddmg`, `mirror2dmg`
+ - Default: 1
### Skins
The system for skins is currently not implemented entirely.
diff --git a/SSMP/Animation/DamageAnimationEffect.cs b/SSMP/Animation/DamageAnimationEffect.cs
index 811fb446..be519ea4 100644
--- a/SSMP/Animation/DamageAnimationEffect.cs
+++ b/SSMP/Animation/DamageAnimationEffect.cs
@@ -55,7 +55,7 @@ protected static void RemoveDamageHeroComponent(GameObject target) {
/// Adds or removes a component from the given game object,
/// depending on the PVP and team settings.
///
- /// The target game object to detatch the component to.
+ /// The target game object to attach or remove the component from.
/// The number of mask of damage it should deal.
/// The component that was added if PVP was turned on
protected DamageHero? SetDamageHeroState(GameObject target, int damage = 1) {
diff --git a/SSMP/Animation/Effects/BindBurst.cs b/SSMP/Animation/Effects/BindBurst.cs
index 3416720d..e136b00b 100644
--- a/SSMP/Animation/Effects/BindBurst.cs
+++ b/SSMP/Animation/Effects/BindBurst.cs
@@ -144,7 +144,8 @@ private void PlayMirror(GameObject playerObject, bool upgraded) {
damager.layer = (int) GlobalEnums.PhysLayers.HERO_ATTACK;
- var damageComponent = AddDamageHeroComponent(damager);
+ var damage = upgraded ? ServerSettings.ClawMirrorUpgradedDamage : ServerSettings.ClawMirrorDamage;
+ var damageComponent = AddDamageHeroComponent(damager, damage);
damageComponent.hazardType = GlobalEnums.HazardType.EXPLOSION;
}
}
diff --git a/SSMP/Animation/Effects/BindInterrupt.cs b/SSMP/Animation/Effects/BindInterrupt.cs
index 4c82e41f..0c015260 100644
--- a/SSMP/Animation/Effects/BindInterrupt.cs
+++ b/SSMP/Animation/Effects/BindInterrupt.cs
@@ -71,6 +71,10 @@ private void PlayBellExplode(GameObject bindEffects) {
return;
}
+ if (bellFsm.FsmStates.Length == 1) {
+ bellFsm.Init();
+ }
+
var stateName = "Burst";
var spawner = bellFsm.GetFirstAction(stateName);
@@ -93,7 +97,7 @@ private void PlayBellExplode(GameObject bindEffects) {
if (ServerSettings.IsPvpEnabled && ShouldDoDamage) {
var damager = bindBell.FindGameObjectInChildren("damager");
if (damager != null) {
- AddDamageHeroComponent(damager);
+ AddDamageHeroComponent(damager, ServerSettings.WardingBellDamage);
} else {
Logger.Warn("Unable to add damager to warding bell burst");
}
diff --git a/SSMP/Animation/Effects/DashSlash.cs b/SSMP/Animation/Effects/DashSlash.cs
index 38ba9467..223ed9cb 100644
--- a/SSMP/Animation/Effects/DashSlash.cs
+++ b/SSMP/Animation/Effects/DashSlash.cs
@@ -109,7 +109,7 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
ApplyLongclawMultiplier(longclaw, SlashType.Dash, slashObj, scale);
if (ServerSettings.IsPvpEnabled && ShouldDoDamage) {
- AddDamageHeroComponent(slashObj);
+ AddDamageHeroComponent(slashObj, ServerSettings.NeedleDamage);
}
// TODO: Nail imbuement (see OnPlaySlash in NailAttackBase.cs)
diff --git a/SSMP/Animation/Effects/NeedleStrike.cs b/SSMP/Animation/Effects/NeedleStrike.cs
index 7dd5ca4a..28f3ade8 100644
--- a/SSMP/Animation/Effects/NeedleStrike.cs
+++ b/SSMP/Animation/Effects/NeedleStrike.cs
@@ -131,7 +131,7 @@ out _
return;
}
- int? damage = ServerSettings.IsPvpEnabled && ShouldDoDamage ? 1 : null;
+ int? damage = ServerSettings.IsPvpEnabled && ShouldDoDamage ? ServerSettings.NeedleStrikeDamage : null;
Logger.Info($"NeedleStrike animation effect for crest: {crestType}");
diff --git a/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs b/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
index f669e067..4b376aab 100644
--- a/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
+++ b/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
@@ -137,4 +137,24 @@ protected static bool FindOrCreateSkill(GameObject playerObject, string name, ou
skill.name = name;
return true;
}
+
+ ///
+ /// with a calculated damage amount for silk skills
+ ///
+ /// The target game object to attach or remove the component from.
+ /// The base silk skill damage
+ /// If the Volt Filament is equipped
+ /// If the player is using the Shaman Crest
+ protected DamageHero? SetDamageHeroStateCalculated(GameObject damager, int baseDamage, bool isVolt, bool isShaman) {
+ float damage = baseDamage;
+ if (isVolt) {
+ damage += (float) ServerSettings.VoltFilamentDamage / 2;
+ }
+
+ if (isShaman) {
+ damage += (float) ServerSettings.ShamanDamage / 2;
+ }
+
+ return SetDamageHeroState(damager, (int) damage);
+ }
}
diff --git a/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs b/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs
index f5a31128..3c545a94 100644
--- a/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs
+++ b/SSMP/Animation/Effects/SilkSkills/CrossStitch.cs
@@ -119,7 +119,7 @@ private IEnumerator PlayDash(GameObject playerObject, bool isShaman, bool isVolt
// Add damager
var damager = slash.FindGameObjectInChildren("Enemy_Damager");
if (damager != null) {
- SetDamageHeroState(damager);
+ SetDamageHeroStateCalculated(damager, ServerSettings.CrossStitchDamage, isVolt, isShaman);
} else {
Logger.Warn("Unable to set damager for Cross Stitch");
}
diff --git a/SSMP/Animation/Effects/SilkSkills/PaleNails.cs b/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
index 2f639d22..6691c84b 100644
--- a/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
+++ b/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
@@ -154,7 +154,7 @@ private IEnumerator PlayAntic(GameObject playerObject, bool isVolt, bool isShama
// Fire existing nails
var id = playerObject.GetInstanceID();
- if (PlayerNails.TryGetValue(id, out var existingNails)) {
+ if (PlayerNails.TryGetValue(id, out var existingNails) && existingNails.Length > 0) {
PlayNailFireUnguided(existingNails, isVolt, id);
}
@@ -200,7 +200,7 @@ private IEnumerator PlayAntic(GameObject playerObject, bool isVolt, bool isShama
// Set the damage state
var damager = nail.FindGameObjectInChildren("Enemy Damager");
if (damager) {
- SetDamageHeroState(damager, 1);
+ SetDamageHeroStateCalculated(damager, ServerSettings.PaleNailsDamage, isVolt, isShaman);
}
// Remove interfering components
diff --git a/SSMP/Animation/Effects/SilkSkills/RuneRage.cs b/SSMP/Animation/Effects/SilkSkills/RuneRage.cs
index a2978370..c4a3dfde 100644
--- a/SSMP/Animation/Effects/SilkSkills/RuneRage.cs
+++ b/SSMP/Animation/Effects/SilkSkills/RuneRage.cs
@@ -321,7 +321,7 @@ private void CreateBlast(Transform spawnTransform, bool isVolt, bool isShaman, V
.FindGameObjectInChildren("damager");
if (damager != null) {
- SetDamageHeroState(damager, 1);
+ SetDamageHeroStateCalculated(damager, ServerSettings.RuneRageDamage, isVolt, isShaman);
}
// Remove extra recycling component
diff --git a/SSMP/Animation/Effects/SilkSkills/SharpDart.cs b/SSMP/Animation/Effects/SilkSkills/SharpDart.cs
index 779b6548..8fd8d03c 100644
--- a/SSMP/Animation/Effects/SilkSkills/SharpDart.cs
+++ b/SSMP/Animation/Effects/SilkSkills/SharpDart.cs
@@ -32,7 +32,7 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
private IEnumerator PlayEffect(GameObject playerObject, bool isShaman) {
// Set up damager
if (TryGetDamager(playerObject, out var damager)) {
- SetDamageHeroState(damager);
+ SetDamageHeroStateCalculated(damager, ServerSettings.SharpDartDamage, Volt, isShaman);
damager.SetActive(true);
var rune = damager.FindGameObjectInChildren("Shaman Rune");
diff --git a/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs b/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
index db11ed1a..f9069644 100644
--- a/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
+++ b/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
@@ -25,20 +25,20 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
// Set volt settings
- var volt = IsVolt(effectInfo);
+ var isVolt = IsVolt(effectInfo);
var voltThread = parent
.FindGameObjectInChildren("thread")?
.FindGameObjectInChildren("zap thread");
- if (voltThread) voltThread.SetActive(volt);
+ if (voltThread) voltThread.SetActive(isVolt);
var needle = parent.FindGameObjectInChildren("needle");
var voltNeedle = needle?.FindGameObjectInChildren("Zap Effect Activator");
if (voltNeedle) {
- voltNeedle.SetActive(volt);
- voltNeedle.SetActiveChildren(volt);
+ voltNeedle.SetActive(isVolt);
+ voltNeedle.SetActiveChildren(isVolt);
}
// Set shaman settings
@@ -53,14 +53,14 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
shamanRune.SetActive(isShaman);
var voltRune = shamanRune.FindGameObjectInChildren("Zap Rune");
- if (voltRune) voltRune.SetActive(volt);
+ if (voltRune) voltRune.SetActive(isVolt);
}
}
// Set damager
var damager = needle?.FindGameObjectInChildren("Needle Damage");
if (damager) {
- SetDamageHeroState(damager, 1);
+ SetDamageHeroStateCalculated(damager, ServerSettings.SilkSpearDamage, isVolt, isShaman);
MonoBehaviourUtil.Instance.StartCoroutine(PlayPossibleThunk(playerObject, spear, damager));
} else {
Logger.Warn("Unable to set damager for Silk Spear");
@@ -77,7 +77,7 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
var throwAudio = fsm.GetAction("Start Throw", 1);
if (throwAudio != null) AudioUtil.PlayAudio(throwAudio, playerObject);
- if (volt) {
+ if (isVolt) {
var voltAudio = fsm.GetAction("Silkspear Zap FX", 1);
if (voltAudio != null) AudioUtil.PlayAudio(voltAudio, playerObject);
}
diff --git a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
index 73958f9c..fe6d2776 100644
--- a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
+++ b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
@@ -113,13 +113,12 @@ private IEnumerator PlayStormSetup(GameObject playerObject, bool isVolt, bool is
damager.transform.localScale = new Vector3(0.8f, 0.8f, 1);
AnimateScaleReset(damager);
- SetDamageHeroState(damager, 1);
+ SetDamageHeroStateCalculated(damager, ServerSettings.ThreadStormDamage, isVolt, isShaman);
damager.SetActive(true);
} else {
Logger.Warn("Unable to set damager for Thread Storm");
}
- // Play looping silk audio
// Play the main effect
MonoBehaviourUtil.Instance.StartCoroutine(PlayStormExtension(playerObject, true));
}
@@ -183,7 +182,7 @@ private static bool TryGetThreadStorm(
return false;
}
- if (created) {
+ if (!created) {
return true;
}
diff --git a/SSMP/Animation/Effects/SlashBase.cs b/SSMP/Animation/Effects/SlashBase.cs
index 115fc3bd..43c0aa15 100644
--- a/SSMP/Animation/Effects/SlashBase.cs
+++ b/SSMP/Animation/Effects/SlashBase.cs
@@ -186,7 +186,7 @@ protected void Play(GameObject playerObject, SlashType type, CrestType crestType
}
if (ServerSettings.IsPvpEnabled && ShouldDoDamage) {
- AddDamageHeroComponent(slashObj);
+ AddDamageHeroComponent(slashObj, ServerSettings.NeedleDamage);
}
// TODO: nail imbued from NailAttackBase
diff --git a/SSMP/Game/Settings/ServerSettings.cs b/SSMP/Game/Settings/ServerSettings.cs
index fb4414fe..df835ede 100644
--- a/SSMP/Game/Settings/ServerSettings.cs
+++ b/SSMP/Game/Settings/ServerSettings.cs
@@ -89,61 +89,163 @@ public bool AllowSkins {
// [SettingAlias("parries")]
// [ModMenuSetting("Parries", "Whether parrying certain player attacks is possible")]
// public bool AllowParries { get; set; } = true;
- //
- // ///
- // [SettingAlias("naildmg")]
- // [ModMenuSetting("Nail Damage", "The number of masks of damage that a player's nail swing deals")]
- // public byte NailDamage { get; set; } = 1;
- //
- // ///
- // [SettingAlias("elegydmg")]
- // [ModMenuSetting("Grubberfly's Elegy Damage", "The number of masks of damage that Grubberfly's Elegy deals")]
- // public byte GrubberflyElegyDamage { get; set; } = 1;
- //
- // ///
- // [SettingAlias("vsdmg", "fireballdamage", "fireballdmg")]
- // [ModMenuSetting("Vengeful Spirit Damage", "The number of masks of damage that Vengeful Spirit deals")]
- // public byte VengefulSpiritDamage { get; set; } = 1;
- //
- // ///
- // [SettingAlias("shadesouldmg")]
- // [ModMenuSetting("Shade Soul Damage", "The number of masks of damage that Shade Soul deals")]
- // public byte ShadeSoulDamage { get; set; } = 2;
- //
- // ///
- // [SettingAlias("desolatedivedmg", "ddivedmg")]
- // [ModMenuSetting("Desolate Dive Damage", "The number of masks of damage that Desolate Dive deals")]
- // public byte DesolateDiveDamage { get; set; } = 1;
- //
- // ///
- // [SettingAlias("descendingdarkdmg", "ddarkdmg")]
- // [ModMenuSetting("Descending Dark Damage", "The number of masks of damage that Descending Dark deals")]
- // public byte DescendingDarkDamage { get; set; } = 2;
- //
- // ///
- // [SettingAlias("howlingwraithsdamage", "howlingwraithsdmg", "wraithsdmg")]
- // [ModMenuSetting("Howling Wraiths Damage", "The number of masks of damage that Howling Wraiths deals")]
- // public byte HowlingWraithDamage { get; set; } = 1;
- //
- // ///
- // [SettingAlias("abyssshriekdmg", "shriekdmg")]
- // [ModMenuSetting("Abyss Shriek Damage", "The number of masks of damage that Abyss Shriek deals")]
- // public byte AbyssShriekDamage { get; set; } = 2;
- //
- // ///
- // [SettingAlias("greatslashdmg")]
- // [ModMenuSetting("Great Slash Damage", "The number of masks of damage that Great Slash deals")]
- // public byte GreatSlashDamage { get; set; } = 2;
- //
- // ///
- // [SettingAlias("dashslashdmg")]
- // [ModMenuSetting("Dash Slash Damage", "The number of masks of damage that Dash Slash deals")]
- // public byte DashSlashDamage { get; set; } = 2;
- //
- // ///
- // [SettingAlias("cycloneslashdmg", "cyclonedmg")]
- // [ModMenuSetting("Cyclone Slash Damage", "The number of masks of damage that Cyclone Slash deals")]
- // public byte CycloneSlashDamage { get; set; } = 1;
+
+ ///
+ [SettingAlias("needledmg")]
+ [ModMenuSetting("Needle Damage", "The number of masks of damage that a player's needle swing deals")]
+ public byte NeedleDamage {
+ get;
+ init {
+ if (field == value) return;
+ field = value;
+ ChangeEvent?.Invoke(nameof(NeedleDamage));
+ }
+ } = 1;
+
+ ///
+ [SettingAlias("needlestrikedmg", "strikedmg", "artdmg")]
+ [ModMenuSetting("Needle Strike Damage", "The number of masks of damage that Needle Strikes deal")]
+ public byte NeedleStrikeDamage {
+ get;
+ init {
+ if (field == value) return;
+ field = value;
+ ChangeEvent?.Invoke(nameof(NeedleStrikeDamage));
+ }
+ } = 2;
+
+ ///
+ [SettingAlias("voltfilamentdmg", "voltdmg", "filamentdmg", "voltmodifier", "voltmod")]
+ [ModMenuSetting("Volt Filament Damage Modifier (Half)", "The number of extra half-masks of damage that silk skills with volt filament should deal")]
+ public byte VoltFilamentDamage {
+ get;
+ init {
+ if (field == value) return;
+ field = value;
+ ChangeEvent?.Invoke(nameof(VoltFilamentDamage));
+ }
+ } = 1;
+
+ ///
+ [SettingAlias("shamandmg", "shamanmodifier", "shamanmod")]
+ [ModMenuSetting("Shaman Crest Damage Modifier (Half)", "The number of extra half-masks of damage that silk skills on shaman crest should deal")]
+ public byte ShamanDamage {
+ get;
+ init {
+ if (field == value) return;
+ field = value;
+ ChangeEvent?.Invoke(nameof(ShamanDamage));
+ }
+ } = 1;
+
+ ///
+ [SettingAlias("crossstitchdmg", "stitchdmg", "parrydmg")]
+ [ModMenuSetting("Cross Stitch Damage", "The number of masks of damage that Cross Stitch deals")]
+ public byte CrossStitchDamage {
+ get;
+ init {
+ if (field == value) return;
+ field = value;
+ ChangeEvent?.Invoke(nameof(CrossStitchDamage));
+ }
+ } = 1;
+
+ ///
+ [SettingAlias("palenailsdmg", "palenaildmg", "paledmg")]
+ [ModMenuSetting("Pale Nails Damage", "The number of masks of damage that Pale Nails deals")]
+ public byte PaleNailsDamage {
+ get;
+ init {
+ if (field == value) return;
+ field = value;
+ ChangeEvent?.Invoke(nameof(PaleNailsDamage));
+ }
+ } = 1;
+
+ ///
+ [SettingAlias("runeragedmg")]
+ [ModMenuSetting("Rune Rage Damage", "The number of masks of damage that Rune Rage deals")]
+ public byte RuneRageDamage {
+ get;
+ init {
+ if (field == value) return;
+ field = value;
+ ChangeEvent?.Invoke(nameof(RuneRageDamage));
+ }
+ } = 1;
+
+ ///
+ [SettingAlias("sharpdartdmg", "dartdmg")]
+ [ModMenuSetting("Sharpdart Damage", "The number of masks of damage that Sharpdart deals")]
+ public byte SharpDartDamage {
+ get;
+ init {
+ if (field == value) return;
+ field = value;
+ ChangeEvent?.Invoke(nameof(SharpDartDamage));
+ }
+ } = 1;
+
+ ///
+ [SettingAlias("silkspeardmg", "speardmg")]
+ [ModMenuSetting("Silk Spear Damage", "The number of masks of damage that Silk Spear deals")]
+ public byte SilkSpearDamage {
+ get;
+ init {
+ if (field == value) return;
+ field = value;
+ ChangeEvent?.Invoke(nameof(SilkSpearDamage));
+ }
+ } = 1;
+
+ ///
+ [SettingAlias("threadstormdmg", "stormdmg")]
+ [ModMenuSetting("Thread Storm Damage", "The number of masks of damage that Thread Storm deals")]
+ public byte ThreadStormDamage {
+ get;
+ init {
+ if (field == value) return;
+ field = value;
+ ChangeEvent?.Invoke(nameof(ThreadStormDamage));
+ }
+ } = 1;
+
+ ///
+ [SettingAlias("wardingbelldmg", "bindbelldmg", "belldmg")]
+ [ModMenuSetting("Warding Bell Damage", "The number of masks of damage that the Warding Bell deals")]
+ public byte WardingBellDamage {
+ get;
+ init {
+ if (field == value) return;
+ field = value;
+ ChangeEvent?.Invoke(nameof(WardingBellDamage));
+ }
+ } = 1;
+
+ ///
+ [SettingAlias("clawmirrordmg", "mirrordmg", "mirror1dmg")]
+ [ModMenuSetting("Claw Mirror Damage", "The number of masks of damage that the base Claw Mirror deals")]
+ public byte ClawMirrorDamage {
+ get;
+ init {
+ if (field == value) return;
+ field = value;
+ ChangeEvent?.Invoke(nameof(ClawMirrorDamage));
+ }
+ } = 1;
+
+ ///
+ [SettingAlias("clawmirrorupgradeddmg", "mirror2dmg")]
+ [ModMenuSetting("Claw Mirror Upgraded Damage", "The number of masks of damage that the upgraded Claw Mirror deals")]
+ public byte ClawMirrorUpgradedDamage {
+ get;
+ init {
+ if (field == value) return;
+ field = value;
+ ChangeEvent?.Invoke(nameof(ClawMirrorUpgradedDamage));
+ }
+ } = 1;
+
//
// ///
// [SettingAlias("sporeshroomdmg")]
From 4a23848658db06977b30ef1792d4539cf8435b64 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Mon, 20 Apr 2026 17:54:23 -0400
Subject: [PATCH 26/41] Revert "move movement effects to other branch"
This reverts commit 438bc1b440a3a408cf6160bae6f55da0033392a4.
---
SSMP/Animation/AnimationManager.cs | 2 +
SSMP/Animation/Effects/Movement/DoubleJump.cs | 50 +++++++
.../Effects/Movement/UmbrellaInflate.cs | 133 ++++++++++++++++++
3 files changed, 185 insertions(+)
create mode 100644 SSMP/Animation/Effects/Movement/DoubleJump.cs
create mode 100644 SSMP/Animation/Effects/Movement/UmbrellaInflate.cs
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index 3d896c72..5b0cb533 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -662,6 +662,8 @@ internal class AnimationManager {
{ AnimationClip.BindBurstAir, BindBurst.Instance },
{ AnimationClip.RageBindBurst, BindBurst.Instance },
{ AnimationClip.Death, new Death() },
+ { AnimationClip.DoubleJump, new DoubleJump() },
+ { AnimationClip.UmbrellaInflate, UmbrellaInflate.Instance },
// Silk Skills
{ AnimationClip.NeedleThrowThrowing, new SilkSpear() },
diff --git a/SSMP/Animation/Effects/Movement/DoubleJump.cs b/SSMP/Animation/Effects/Movement/DoubleJump.cs
new file mode 100644
index 00000000..b88d2d66
--- /dev/null
+++ b/SSMP/Animation/Effects/Movement/DoubleJump.cs
@@ -0,0 +1,50 @@
+using SSMP.Internals;
+using SSMP.Util;
+using UnityEngine;
+
+namespace SSMP.Animation.Effects.Movement;
+
+internal class DoubleJump : DamageAnimationEffect {
+ private const string JumpEffectName = "double_jump_feathers";
+
+ ///
+ public override byte[]? GetEffectInfo() {
+ return [
+ (byte)(ToolItemManager.IsToolEquipped("Brolly Spike") ? 1 : 0)
+ ];
+ }
+
+ ///
+ public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ // Find or create effects
+ var effects = playerObject.FindGameObjectInChildren("Effects");
+ if (effects == null) {
+ effects = new GameObject();
+ effects.transform.SetParentReset(playerObject.transform);
+ }
+
+ // Find or create jump effect
+ var effect = effects.FindGameObjectInChildren(JumpEffectName);
+ if (effect == null) {
+ var localEffect = HeroController.instance.doubleJumpEffectPrefab;
+ effect = EffectUtils.SpawnGlobalPoolObject(localEffect, effects.transform, 0, true);
+ if (effect == null) return;
+ effect.name = JumpEffectName;
+
+ // Remove components from newly created object
+ effect.DestroyGameObjectInChildren("haze flash");
+ effect.DestroyGameObjectInChildren("jump_double_glow");
+ effect.DestroyComponent();
+ effect.SetActiveChildren(true);
+ }
+
+ // Refresh effect
+ effect.SetActive(false);
+ effect.SetActive(true);
+
+ // Play sawtooth circlet if appropriate
+ if (effectInfo is [1]) {
+ UmbrellaInflate.Instance.PlayCirclet(playerObject);
+ }
+ }
+}
diff --git a/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs b/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs
new file mode 100644
index 00000000..abe2ab7d
--- /dev/null
+++ b/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs
@@ -0,0 +1,133 @@
+using System.Diagnostics.CodeAnalysis;
+using HutongGames.PlayMaker.Actions;
+using SSMP.Internals;
+using SSMP.Util;
+using UnityEngine;
+
+namespace SSMP.Animation.Effects.Movement;
+
+internal class UmbrellaInflate : DamageAnimationEffect {
+ private const string UmbrellaInflateName = "umbrella_inflate_effect";
+
+ private const string SpikedCircletName = "Tool_brolly_spike";
+
+ private static GameObject? _localCirclet;
+
+ public static UmbrellaInflate Instance = new();
+
+ ///
+ public override byte[]? GetEffectInfo() {
+ return [
+ (byte)(ToolItemManager.IsToolEquipped("Brolly Spike") ? 1 : 0)
+ ];
+ }
+
+ ///
+ public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ // Get or create effects
+ var effects = playerObject.FindGameObjectInChildren("Effects");
+ if (effects == null) {
+ effects = new GameObject();
+ effects.transform.SetParentReset(playerObject.transform);
+ }
+
+ // Find or create inflate object
+ var effect = effects.FindGameObjectInChildren(UmbrellaInflateName);
+ if (effect == null) {
+ var localEffect = HeroController.instance.umbrellaEffect;
+ effect = Object.Instantiate(localEffect, effects.transform);
+
+ effect.name = UmbrellaInflateName;
+ effect.transform.localPosition = new Vector3(0, -0.24f, 0);
+ effect.transform.localScale = Vector3.one;
+
+ effect.DestroyGameObjectInChildren("umbrella_float_fx_burst0002");
+ }
+
+ // Refresh particles
+ effect.SetActive(false);
+ effect.SetActive(true);
+
+ // Enable sawtooth circlet if appropriate
+ if (effectInfo is [1]) {
+ PlayCirclet(playerObject);
+ }
+
+ }
+
+ ///
+ /// Attempts to find or create the sawtooth circlet object
+ ///
+ /// The player using the circlet
+ /// The circlet, if found
+ /// true if the circlet was found
+ private static bool TryGetCirclet(GameObject playerObject, [MaybeNullWhen(false)] out GameObject circlet) {
+ // Find or create effects
+ var effects = playerObject.FindGameObjectInChildren("Effects");
+ if (effects == null) {
+ effects = new GameObject("Effects");
+ effects.transform.SetParentReset(playerObject.transform);
+ }
+
+ // Find existing circlet
+ circlet = effects.FindGameObjectInChildren(SpikedCircletName);
+ if (circlet != null) {
+ return true;
+ }
+
+ // Locate circlet
+ if (_localCirclet == null) {
+ var brollyFsm = HeroController.instance.fsm_brollyControl;
+ var spawner = brollyFsm.GetFirstAction("Damager?");
+ _localCirclet = spawner.gameObject.Value;
+ }
+
+ // Spawn in a new circlet
+ circlet = EffectUtils.SpawnGlobalPoolObject(_localCirclet, effects.transform, 0, true);
+ if (circlet == null) {
+ return false;
+ }
+
+ circlet.name = SpikedCircletName;
+ circlet.transform.localPosition = new Vector3(0, 0, -0.02f);
+ circlet.transform.localScale = new Vector3(-1, -1, 1);
+ circlet.DestroyComponent();
+ circlet.tag = "Untagged";
+
+ return true;
+ }
+
+ ///
+ /// Plays the sawtooth circlet
+ ///
+ /// The player using the circlet
+ public void PlayCirclet(GameObject playerObject) {
+ // Get the circlet
+ if (!TryGetCirclet(playerObject, out var circlet)) {
+ return;
+ }
+
+ // Set the damager
+ var damagerParent = circlet
+ .FindGameObjectInChildren("Brolly_spike_position")?
+ .FindGameObjectInChildren("brolly_slash_enemy_damager");
+
+ var damagerRight = damagerParent?.FindGameObjectInChildren("Damager R");
+ var damagerLeft = damagerParent?.FindGameObjectInChildren("Damager L");
+
+ if (damagerRight != null) SetDamageHeroState(damagerRight, 1);
+ if (damagerLeft != null) SetDamageHeroState(damagerLeft, 1);
+
+ // Refresh the circlet
+ circlet.SetActive(false);
+ circlet.SetActive(true);
+
+ // Play audio
+ var audioFsm = _localCirclet.LocateMyFSM("brolly_spike_cooldown_check").FsmTemplate.fsm;
+
+ var audioAction = audioFsm.GetState("Check").Actions[4];
+ if (audioAction is PlayAudioEvent audio) {
+ AudioUtil.PlayAudio(audio, playerObject);
+ }
+ }
+}
From 5cfae836e6d575cf309ea5c1e303e878e283632d Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Mon, 20 Apr 2026 18:42:05 -0400
Subject: [PATCH 27/41] move circlet to its own effect
---
SSMP/Animation/AnimationEffect.cs | 52 ++++++++-
SSMP/Animation/DamageAnimationEffect.cs | 14 ++-
SSMP/Animation/Effects/Movement/DoubleJump.cs | 9 +-
.../Effects/Movement/UmbrellaInflate.cs | 106 ++----------------
.../Effects/SilkSkills/BaseSilkSkill.cs | 2 +-
SSMP/Animation/Effects/Tools/BaseTool.cs | 9 ++
.../Effects/Tools/SawtoothCirclet.cs | 101 +++++++++++++++++
7 files changed, 186 insertions(+), 107 deletions(-)
create mode 100644 SSMP/Animation/Effects/Tools/BaseTool.cs
create mode 100644 SSMP/Animation/Effects/Tools/SawtoothCirclet.cs
diff --git a/SSMP/Animation/AnimationEffect.cs b/SSMP/Animation/AnimationEffect.cs
index 1c23c7cd..00192ec8 100644
--- a/SSMP/Animation/AnimationEffect.cs
+++ b/SSMP/Animation/AnimationEffect.cs
@@ -1,5 +1,6 @@
using SSMP.Game.Settings;
using SSMP.Internals;
+using SSMP.Util;
using UnityEngine;
namespace SSMP.Animation;
@@ -25,7 +26,7 @@ public void SetServerSettings(ServerSettings serverSettings) {
}
///
- /// Locate the damages_enemy FSM and change the attack direction to the given direciton. This will ensure that
+ /// Locate the damages_enemy FSM and change the attack direction to the given direction. This will ensure that
/// enemies are getting knocked back in the correct direction from remote player's attacks.
///
/// The target GameObject to change.
@@ -50,4 +51,53 @@ protected static void HidePlayer(GameObject playerObject) {
playerObject.GetComponent().Stop();
playerObject.GetComponent().SetSprite("wall_puff0004");
}
+
+ ///
+ /// Gets the Effects object for a given player
+ ///
+ /// The player using the effect
+ /// The player's effects object
+ protected static GameObject GetPlayerEffects(GameObject playerObject) {
+ var effects = playerObject.FindGameObjectInChildren("Effects");
+ if (effects == null) {
+ effects = new GameObject("Effects");
+ effects.transform.SetParentReset(playerObject.transform);
+ }
+
+ return effects;
+ }
+
+ ///
+ /// Attempts to get or create an effect from the Effects sub-object
+ ///
+ /// The player using the effect
+ /// The name of the effect object
+ /// The effect, if found or created
+ /// true if created, false otherwise
+ protected static bool TryGetEffect(GameObject playerObject, string effectName, out GameObject? effect) {
+ // Find or create effects for player
+ var effects = GetPlayerEffects(playerObject);
+
+ // Find existing effect
+ effect = effects.FindGameObjectInChildren(effectName);
+ if (effect != null) {
+ return false;
+ }
+
+ // Create new effect
+ var localEffects = HeroController.instance.gameObject.FindGameObjectInChildren("Effects");
+ if (localEffects == null) {
+ return false;
+ }
+
+ var localEffect = localEffects.FindGameObjectInChildren(effectName);
+ if (localEffect == null) {
+ return false;
+ }
+
+ effect = Object.Instantiate(localEffect, effects.transform);
+ effect.name = effectName;
+
+ return true;
+ }
}
diff --git a/SSMP/Animation/DamageAnimationEffect.cs b/SSMP/Animation/DamageAnimationEffect.cs
index be519ea4..1fa5d192 100644
--- a/SSMP/Animation/DamageAnimationEffect.cs
+++ b/SSMP/Animation/DamageAnimationEffect.cs
@@ -59,7 +59,19 @@ protected static void RemoveDamageHeroComponent(GameObject target) {
/// The number of mask of damage it should deal.
/// The component that was added if PVP was turned on
protected DamageHero? SetDamageHeroState(GameObject target, int damage = 1) {
- if (ServerSettings.IsPvpEnabled && ShouldDoDamage) {
+ return SetDamageHeroState(target, ServerSettings.IsPvpEnabled && ShouldDoDamage, damage);
+ }
+
+ ///
+ /// Adds or removes a component from the given game object,
+ /// depending on the PVP and team settings.
+ ///
+ /// The target game object to attach or remove the component from.
+ /// The number of mask of damage it should deal.
+ /// If the damager should be enabled or not
+ /// The component that was added if PVP was turned on
+ protected static DamageHero? SetDamageHeroState(GameObject target, bool doDamage, int damage = 1) {
+ if (doDamage) {
return AddDamageHeroComponent(target, damage);
}
diff --git a/SSMP/Animation/Effects/Movement/DoubleJump.cs b/SSMP/Animation/Effects/Movement/DoubleJump.cs
index b88d2d66..95c5c4d1 100644
--- a/SSMP/Animation/Effects/Movement/DoubleJump.cs
+++ b/SSMP/Animation/Effects/Movement/DoubleJump.cs
@@ -1,3 +1,4 @@
+using SSMP.Animation.Effects.Tools;
using SSMP.Internals;
using SSMP.Util;
using UnityEngine;
@@ -17,11 +18,7 @@ internal class DoubleJump : DamageAnimationEffect {
///
public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
// Find or create effects
- var effects = playerObject.FindGameObjectInChildren("Effects");
- if (effects == null) {
- effects = new GameObject();
- effects.transform.SetParentReset(playerObject.transform);
- }
+ var effects = GetPlayerEffects(playerObject);
// Find or create jump effect
var effect = effects.FindGameObjectInChildren(JumpEffectName);
@@ -44,7 +41,7 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
// Play sawtooth circlet if appropriate
if (effectInfo is [1]) {
- UmbrellaInflate.Instance.PlayCirclet(playerObject);
+ SawtoothCirclet.PlayCirclet(playerObject, ShouldDoDamage && ServerSettings.IsPvpEnabled);
}
}
}
diff --git a/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs b/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs
index abe2ab7d..8b673695 100644
--- a/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs
+++ b/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs
@@ -1,5 +1,4 @@
-using System.Diagnostics.CodeAnalysis;
-using HutongGames.PlayMaker.Actions;
+using SSMP.Animation.Effects.Tools;
using SSMP.Internals;
using SSMP.Util;
using UnityEngine;
@@ -7,13 +6,6 @@
namespace SSMP.Animation.Effects.Movement;
internal class UmbrellaInflate : DamageAnimationEffect {
- private const string UmbrellaInflateName = "umbrella_inflate_effect";
-
- private const string SpikedCircletName = "Tool_brolly_spike";
-
- private static GameObject? _localCirclet;
-
- public static UmbrellaInflate Instance = new();
///
public override byte[]? GetEffectInfo() {
@@ -24,20 +16,14 @@ internal class UmbrellaInflate : DamageAnimationEffect {
///
public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
- // Get or create effects
- var effects = playerObject.FindGameObjectInChildren("Effects");
- if (effects == null) {
- effects = new GameObject();
- effects.transform.SetParentReset(playerObject.transform);
+ // Get or create effect
+ var created = TryGetEffect(playerObject, "umbrella_inflate_effect", out var effect);
+ if (!effect) {
+ return;
}
- // Find or create inflate object
- var effect = effects.FindGameObjectInChildren(UmbrellaInflateName);
- if (effect == null) {
- var localEffect = HeroController.instance.umbrellaEffect;
- effect = Object.Instantiate(localEffect, effects.transform);
-
- effect.name = UmbrellaInflateName;
+ // Set up effect if created
+ if (!created) {
effect.transform.localPosition = new Vector3(0, -0.24f, 0);
effect.transform.localScale = Vector3.one;
@@ -50,84 +36,8 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
// Enable sawtooth circlet if appropriate
if (effectInfo is [1]) {
- PlayCirclet(playerObject);
- }
-
- }
-
- ///
- /// Attempts to find or create the sawtooth circlet object
- ///
- /// The player using the circlet
- /// The circlet, if found
- /// true if the circlet was found
- private static bool TryGetCirclet(GameObject playerObject, [MaybeNullWhen(false)] out GameObject circlet) {
- // Find or create effects
- var effects = playerObject.FindGameObjectInChildren("Effects");
- if (effects == null) {
- effects = new GameObject("Effects");
- effects.transform.SetParentReset(playerObject.transform);
- }
-
- // Find existing circlet
- circlet = effects.FindGameObjectInChildren(SpikedCircletName);
- if (circlet != null) {
- return true;
+ SawtoothCirclet.PlayCirclet(playerObject, ShouldDoDamage && ServerSettings.IsPvpEnabled);
}
- // Locate circlet
- if (_localCirclet == null) {
- var brollyFsm = HeroController.instance.fsm_brollyControl;
- var spawner = brollyFsm.GetFirstAction("Damager?");
- _localCirclet = spawner.gameObject.Value;
- }
-
- // Spawn in a new circlet
- circlet = EffectUtils.SpawnGlobalPoolObject(_localCirclet, effects.transform, 0, true);
- if (circlet == null) {
- return false;
- }
-
- circlet.name = SpikedCircletName;
- circlet.transform.localPosition = new Vector3(0, 0, -0.02f);
- circlet.transform.localScale = new Vector3(-1, -1, 1);
- circlet.DestroyComponent();
- circlet.tag = "Untagged";
-
- return true;
- }
-
- ///
- /// Plays the sawtooth circlet
- ///
- /// The player using the circlet
- public void PlayCirclet(GameObject playerObject) {
- // Get the circlet
- if (!TryGetCirclet(playerObject, out var circlet)) {
- return;
- }
-
- // Set the damager
- var damagerParent = circlet
- .FindGameObjectInChildren("Brolly_spike_position")?
- .FindGameObjectInChildren("brolly_slash_enemy_damager");
-
- var damagerRight = damagerParent?.FindGameObjectInChildren("Damager R");
- var damagerLeft = damagerParent?.FindGameObjectInChildren("Damager L");
-
- if (damagerRight != null) SetDamageHeroState(damagerRight, 1);
- if (damagerLeft != null) SetDamageHeroState(damagerLeft, 1);
-
- // Refresh the circlet
- circlet.SetActive(false);
- circlet.SetActive(true);
-
- // Play audio
- var audioFsm = _localCirclet.LocateMyFSM("brolly_spike_cooldown_check").FsmTemplate.fsm;
-
- var audioAction = audioFsm.GetState("Check").Actions[4];
- if (audioAction is PlayAudioEvent audio) {
- AudioUtil.PlayAudio(audio, playerObject);
- }
}
}
diff --git a/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs b/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
index 4b376aab..174c0a71 100644
--- a/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
+++ b/SSMP/Animation/Effects/SilkSkills/BaseSilkSkill.cs
@@ -139,7 +139,7 @@ protected static bool FindOrCreateSkill(GameObject playerObject, string name, ou
}
///
- /// with a calculated damage amount for silk skills
+ /// with a calculated damage amount for silk skills
///
/// The target game object to attach or remove the component from.
/// The base silk skill damage
diff --git a/SSMP/Animation/Effects/Tools/BaseTool.cs b/SSMP/Animation/Effects/Tools/BaseTool.cs
new file mode 100644
index 00000000..f2e34a6e
--- /dev/null
+++ b/SSMP/Animation/Effects/Tools/BaseTool.cs
@@ -0,0 +1,9 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace SSMP.Animation.Effects.Tools;
+
+internal abstract class BaseTool : DamageAnimationEffect {
+
+}
diff --git a/SSMP/Animation/Effects/Tools/SawtoothCirclet.cs b/SSMP/Animation/Effects/Tools/SawtoothCirclet.cs
new file mode 100644
index 00000000..aa9d9c3f
--- /dev/null
+++ b/SSMP/Animation/Effects/Tools/SawtoothCirclet.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Text;
+using HutongGames.PlayMaker.Actions;
+using SSMP.Internals;
+using SSMP.Util;
+using UnityEngine;
+
+namespace SSMP.Animation.Effects.Tools;
+
+internal class SawtoothCirclet : BaseTool {
+
+ private const string SpikedCircletName = "Tool_brolly_spike";
+
+ private static GameObject? _localCirclet;
+
+ public override byte[]? GetEffectInfo() {
+ return null;
+ }
+
+ public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ PlayCirclet(playerObject, ShouldDoDamage && ServerSettings.IsPvpEnabled);
+ }
+ ///
+ /// Plays the sawtooth circlet
+ ///
+ /// The player using the circlet
+ /// If the circlet should do damage
+ public static void PlayCirclet(GameObject playerObject, bool doDamage) {
+ // Get the circlet
+ if (!TryGetCirclet(playerObject, out var circlet)) {
+ return;
+ }
+
+ // Set the damager
+ var damagerParent = circlet
+ .FindGameObjectInChildren("Brolly_spike_position")?
+ .FindGameObjectInChildren("brolly_slash_enemy_damager");
+
+ var damagerRight = damagerParent?.FindGameObjectInChildren("Damager R");
+ var damagerLeft = damagerParent?.FindGameObjectInChildren("Damager L");
+
+ if (damagerRight != null) SetDamageHeroState(damagerRight, doDamage, 1);
+ if (damagerLeft != null) SetDamageHeroState(damagerLeft, doDamage, 1);
+
+ // Refresh the circlet
+ circlet.SetActive(false);
+ circlet.SetActive(true);
+
+ // Play audio
+ var audioFsm = _localCirclet.LocateMyFSM("brolly_spike_cooldown_check").FsmTemplate.fsm;
+
+ var audioAction = audioFsm.GetState("Check").Actions[4];
+ if (audioAction is PlayAudioEvent audio) {
+ AudioUtil.PlayAudio(audio, playerObject);
+ }
+ }
+
+ ///
+ /// Attempts to find or create the sawtooth circlet object
+ ///
+ /// The player using the circlet
+ /// The circlet, if found
+ /// true if the circlet was found
+ private static bool TryGetCirclet(GameObject playerObject, [MaybeNullWhen(false)] out GameObject circlet) {
+ // Find or create effects
+ var effects = playerObject.FindGameObjectInChildren("Effects");
+ if (effects == null) {
+ effects = new GameObject("Effects");
+ effects.transform.SetParentReset(playerObject.transform);
+ }
+
+ // Find existing circlet
+ circlet = effects.FindGameObjectInChildren(SpikedCircletName);
+ if (circlet != null) {
+ return true;
+ }
+
+ // Locate circlet
+ if (_localCirclet == null) {
+ var brollyFsm = HeroController.instance.fsm_brollyControl;
+ var spawner = brollyFsm.GetFirstAction("Damager?");
+ _localCirclet = spawner.gameObject.Value;
+ }
+
+ // Spawn in a new circlet
+ circlet = EffectUtils.SpawnGlobalPoolObject(_localCirclet, effects.transform, 0, true);
+ if (circlet == null) {
+ return false;
+ }
+
+ circlet.name = SpikedCircletName;
+ circlet.transform.localPosition = new Vector3(0, 0, -0.02f);
+ circlet.transform.localScale = new Vector3(-1, -1, 1);
+ circlet.DestroyComponent();
+ circlet.tag = "Untagged";
+
+ return true;
+ }
+}
From 6ea97fb6a9d2338870f6dd1b1978c2eb41195b5a Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Mon, 20 Apr 2026 18:46:12 -0400
Subject: [PATCH 28/41] Set up configurable damage
---
SSMP/Animation/DamageAnimationEffect.cs | 2 +-
SSMP/Animation/Effects/Movement/DoubleJump.cs | 2 +-
.../Effects/Movement/UmbrellaInflate.cs | 2 +-
.../Effects/Tools/SawtoothCirclet.cs | 20 ++++++++++++++-----
SSMP/Game/Settings/ServerSettings.cs | 12 +++++++++++
5 files changed, 30 insertions(+), 8 deletions(-)
diff --git a/SSMP/Animation/DamageAnimationEffect.cs b/SSMP/Animation/DamageAnimationEffect.cs
index 1fa5d192..6d254dbd 100644
--- a/SSMP/Animation/DamageAnimationEffect.cs
+++ b/SSMP/Animation/DamageAnimationEffect.cs
@@ -71,7 +71,7 @@ protected static void RemoveDamageHeroComponent(GameObject target) {
/// If the damager should be enabled or not
/// The component that was added if PVP was turned on
protected static DamageHero? SetDamageHeroState(GameObject target, bool doDamage, int damage = 1) {
- if (doDamage) {
+ if (doDamage && damage > 0) {
return AddDamageHeroComponent(target, damage);
}
diff --git a/SSMP/Animation/Effects/Movement/DoubleJump.cs b/SSMP/Animation/Effects/Movement/DoubleJump.cs
index 95c5c4d1..ca1d7c38 100644
--- a/SSMP/Animation/Effects/Movement/DoubleJump.cs
+++ b/SSMP/Animation/Effects/Movement/DoubleJump.cs
@@ -41,7 +41,7 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
// Play sawtooth circlet if appropriate
if (effectInfo is [1]) {
- SawtoothCirclet.PlayCirclet(playerObject, ShouldDoDamage && ServerSettings.IsPvpEnabled);
+ SawtoothCirclet.PlayCirclet(playerObject, ShouldDoDamage && ServerSettings.IsPvpEnabled, ServerSettings);
}
}
}
diff --git a/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs b/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs
index 8b673695..549e80de 100644
--- a/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs
+++ b/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs
@@ -36,7 +36,7 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
// Enable sawtooth circlet if appropriate
if (effectInfo is [1]) {
- SawtoothCirclet.PlayCirclet(playerObject, ShouldDoDamage && ServerSettings.IsPvpEnabled);
+ SawtoothCirclet.PlayCirclet(playerObject, ShouldDoDamage && ServerSettings.IsPvpEnabled, ServerSettings);
}
}
diff --git a/SSMP/Animation/Effects/Tools/SawtoothCirclet.cs b/SSMP/Animation/Effects/Tools/SawtoothCirclet.cs
index aa9d9c3f..e2266bb4 100644
--- a/SSMP/Animation/Effects/Tools/SawtoothCirclet.cs
+++ b/SSMP/Animation/Effects/Tools/SawtoothCirclet.cs
@@ -3,6 +3,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
using HutongGames.PlayMaker.Actions;
+using SSMP.Game.Settings;
using SSMP.Internals;
using SSMP.Util;
using UnityEngine;
@@ -10,24 +11,32 @@
namespace SSMP.Animation.Effects.Tools;
internal class SawtoothCirclet : BaseTool {
-
+ ///
+ /// The name of the sawtooth circlet object
+ ///
private const string SpikedCircletName = "Tool_brolly_spike";
+ ///
+ /// Cached reference to the local sawtooth circlet object
+ ///
private static GameObject? _localCirclet;
+ ///
public override byte[]? GetEffectInfo() {
return null;
}
+ ///
public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
- PlayCirclet(playerObject, ShouldDoDamage && ServerSettings.IsPvpEnabled);
+ PlayCirclet(playerObject, ShouldDoDamage && ServerSettings.IsPvpEnabled, ServerSettings);
}
+
///
/// Plays the sawtooth circlet
///
/// The player using the circlet
/// If the circlet should do damage
- public static void PlayCirclet(GameObject playerObject, bool doDamage) {
+ public static void PlayCirclet(GameObject playerObject, bool doDamage, ServerSettings serverSettings) {
// Get the circlet
if (!TryGetCirclet(playerObject, out var circlet)) {
return;
@@ -41,8 +50,9 @@ public static void PlayCirclet(GameObject playerObject, bool doDamage) {
var damagerRight = damagerParent?.FindGameObjectInChildren("Damager R");
var damagerLeft = damagerParent?.FindGameObjectInChildren("Damager L");
- if (damagerRight != null) SetDamageHeroState(damagerRight, doDamage, 1);
- if (damagerLeft != null) SetDamageHeroState(damagerLeft, doDamage, 1);
+ var damage = serverSettings.SawtoothCircletDamage;
+ if (damagerRight != null) SetDamageHeroState(damagerRight, doDamage, damage);
+ if (damagerLeft != null) SetDamageHeroState(damagerLeft, doDamage, damage);
// Refresh the circlet
circlet.SetActive(false);
diff --git a/SSMP/Game/Settings/ServerSettings.cs b/SSMP/Game/Settings/ServerSettings.cs
index df835ede..5c11f2c8 100644
--- a/SSMP/Game/Settings/ServerSettings.cs
+++ b/SSMP/Game/Settings/ServerSettings.cs
@@ -246,6 +246,18 @@ public byte ClawMirrorUpgradedDamage {
}
} = 1;
+ ///
+ [SettingAlias("sawtoothcircletdmg", "circletdmg", "umbrelladmg")]
+ [ModMenuSetting("Sawtooth Circlet Damage", "The number of masks of damage that Sawtooth Circlet deals")]
+ public byte SawtoothCircletDamage {
+ get;
+ init {
+ if (field == value) return;
+ field = value;
+ ChangeEvent?.Invoke(nameof(SawtoothCircletDamage));
+ }
+ } = 1;
+
//
// ///
// [SettingAlias("sporeshroomdmg")]
From c2255fa03d238a09092ef0d0ee0a3703a64bfb29 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Tue, 21 Apr 2026 13:37:25 -0400
Subject: [PATCH 29/41] Add magnetite dice
---
SSMP/Animation/AnimationClip.cs | 1 +
SSMP/Animation/AnimationManager.cs | 98 ++++++++++++-------
SSMP/Animation/Effects/Tools/MagnetiteDice.cs | 73 ++++++++++++++
3 files changed, 136 insertions(+), 36 deletions(-)
create mode 100644 SSMP/Animation/Effects/Tools/MagnetiteDice.cs
diff --git a/SSMP/Animation/AnimationClip.cs b/SSMP/Animation/AnimationClip.cs
index 0f6a9604..cf490104 100644
--- a/SSMP/Animation/AnimationClip.cs
+++ b/SSMP/Animation/AnimationClip.cs
@@ -768,4 +768,5 @@ internal enum AnimationClip {
WitchTentacles,
ShamanCancel,
BindInterrupt,
+ MagnetiteDice
}
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index 5b0cb533..5aad75c5 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -2,7 +2,9 @@
using System.Linq;
using HutongGames.PlayMaker.Actions;
using SSMP.Animation.Effects;
+using SSMP.Animation.Effects.Movement;
using SSMP.Animation.Effects.SilkSkills;
+using SSMP.Animation.Effects.Tools;
using SSMP.Collection;
using SSMP.Fsm;
using SSMP.Game;
@@ -623,7 +625,8 @@ internal class AnimationManager {
{ "Witch Tentacles!", AnimationClip.WitchTentacles },
{ "Shaman Cancel", AnimationClip.ShamanCancel },
- { "Bind Fail Burst", AnimationClip.BindInterrupt }
+ { "Bind Fail Burst", AnimationClip.BindInterrupt },
+ { "Magnetite Dice", AnimationClip.MagnetiteDice }
};
///
@@ -663,7 +666,7 @@ internal class AnimationManager {
{ AnimationClip.RageBindBurst, BindBurst.Instance },
{ AnimationClip.Death, new Death() },
{ AnimationClip.DoubleJump, new DoubleJump() },
- { AnimationClip.UmbrellaInflate, UmbrellaInflate.Instance },
+ { AnimationClip.UmbrellaInflate, new UmbrellaInflate() },
// Silk Skills
{ AnimationClip.NeedleThrowThrowing, new SilkSpear() },
@@ -681,9 +684,14 @@ internal class AnimationManager {
{ AnimationClip.WitchTentacles, BindBurst.Instance },
{ AnimationClip.ShamanCancel, new Bind { BindState = Bind.State.ShamanCancel } },
{ AnimationClip.BindInterrupt, BindInterrupt.Instance },
+
+ // Silk Skills
{ AnimationClip.AirSphereRefresh, new ThreadStorm() },
{ AnimationClip.SilkBombLocations, new RuneRage() },
- { AnimationClip.SilkBossNeedleFire, new PaleNails() }
+ { AnimationClip.SilkBossNeedleFire, new PaleNails() },
+
+ // Tools
+ { AnimationClip.MagnetiteDice, new MagnetiteDice() }
};
///
@@ -832,6 +840,8 @@ public void DeregisterHooks() {
HeroController.OnHeroInstanceSet -= CreateHeroHooks;
FsmStateActionInjector.UninjectAll();
+
+ MagnetiteDice.Unhook();
// On.HeroAnimationController.Play -= HeroAnimationControllerOnPlay;
// On.HeroAnimationController.PlayFromFrame -= HeroAnimationControllerOnPlayFromFrame;
@@ -1093,6 +1103,9 @@ private void CreateHeroHooks(HeroController hc) {
bellFsm.Init();
}
+ CreateSkillHooks();
+ CreateToolHooks();
+
// Find bind FSM
var heroFsms = hc.GetComponents();
@@ -1110,17 +1123,52 @@ private void CreateHeroHooks(HeroController hc) {
} else {
Logger.Warn("Unable to find Bind FSM to hook.");
}
+ }
+ ///
+ /// Animation subanimation hook for the Witch Tentacles FSM state
+ ///
+ private void OnWitchTentacles(PlayMakerFSM fsm) {
+ var dummyClip = new tk2dSpriteAnimationClip();
+ dummyClip.name = "Witch Tentacles!";
+ dummyClip.wrapMode = tk2dSpriteAnimationClip.WrapMode.Once;
+ OnAnimationEvent(dummyClip);
+ }
+ ///
+ /// Animation subanimation hook for the Shaman Air Cancel FSM state
+ ///
+
+ private void OnShamanCancel(PlayMakerFSM fsm) {
+ var dummyClip = new tk2dSpriteAnimationClip();
+ dummyClip.name = "Shaman Cancel";
+ dummyClip.wrapMode = tk2dSpriteAnimationClip.WrapMode.Once;
+ OnAnimationEvent(dummyClip);
+ }
+
+ ///
+ /// Animation subanimation hook for interrupted binds
+ ///
+ private void OnBindInterrupt(PlayMakerFSM fsm) {
+ var dummyClip = new tk2dSpriteAnimationClip();
+ dummyClip.name = "Bind Fail Burst";
+ dummyClip.wrapMode = tk2dSpriteAnimationClip.WrapMode.Once;
+ OnAnimationEvent(dummyClip);
+ }
+
+ ///
+ /// Creates hooks for silk skills
+ ///
+ private void CreateSkillHooks() {
// Silk skill injections
- var silkSkillFsm = hc.silkSpecialFSM;
+ var silkSkillFsm = HeroController.instance.silkSpecialFSM;
if (silkSkillFsm == null) {
Logger.Warn("Unable to find Silk Skill FSM to hook.");
return;
}
- // Thread strom
+ // Thread storm
var threadStormExtend = silkSkillFsm.GetState("Extend");
FsmStateActionInjector.Inject(threadStormExtend, OnThreadStormExtend, 0);
@@ -1187,37 +1235,6 @@ private void CreateHeroHooks(HeroController hc) {
}
}
- ///
- /// Animation subanimation hook for the Witch Tentacles FSM state
- ///
- private void OnWitchTentacles(PlayMakerFSM fsm) {
- var dummyClip = new tk2dSpriteAnimationClip();
- dummyClip.name = "Witch Tentacles!";
- dummyClip.wrapMode = tk2dSpriteAnimationClip.WrapMode.Once;
- OnAnimationEvent(dummyClip);
- }
-
- ///
- /// Animation subanimation hook for the Shaman Air Cancel FSM state
- ///
-
- private void OnShamanCancel(PlayMakerFSM fsm) {
- var dummyClip = new tk2dSpriteAnimationClip();
- dummyClip.name = "Shaman Cancel";
- dummyClip.wrapMode = tk2dSpriteAnimationClip.WrapMode.Once;
- OnAnimationEvent(dummyClip);
- }
-
- ///
- /// Animation subanimation hook for interrupted binds
- ///
- private void OnBindInterrupt(PlayMakerFSM fsm) {
- var dummyClip = new tk2dSpriteAnimationClip();
- dummyClip.name = "Bind Fail Burst";
- dummyClip.wrapMode = tk2dSpriteAnimationClip.WrapMode.Once;
- OnAnimationEvent(dummyClip);
- }
-
///
/// Animation subanimation hook for extending a thread storm
///
@@ -1404,6 +1421,15 @@ private void OnPaleNailFire(PlayMakerFSM fsm) {
_netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.SilkBossNeedleFire, 0, effectInfo);
}
+
+ private void CreateToolHooks() {
+ MagnetiteDice.Hook(OnDiceEnable);
+ }
+
+ private void OnDiceEnable() {
+ _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.MagnetiteDice, 0);
+ }
+
// ///
// /// Callback method on the HeroAnimationController#Play method.
// ///
diff --git a/SSMP/Animation/Effects/Tools/MagnetiteDice.cs b/SSMP/Animation/Effects/Tools/MagnetiteDice.cs
new file mode 100644
index 00000000..1cef5772
--- /dev/null
+++ b/SSMP/Animation/Effects/Tools/MagnetiteDice.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections;
+using SSMP.Internals;
+using SSMP.Util;
+using UnityEngine;
+using Object = UnityEngine.Object;
+
+namespace SSMP.Animation.Effects.Tools;
+
+internal class MagnetiteDice : BaseTool {
+
+ private const string DiceName = "dice_shield_effect";
+
+ public override byte[]? GetEffectInfo() {
+ return null;
+ }
+
+ public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ var effects = GetPlayerEffects(playerObject);
+ var dice = effects.FindGameObjectInChildren(DiceName);
+
+ if (dice == null) {
+ var localDice = HeroController.instance.spawnedLuckyDiceShieldEffect;
+ if (localDice == null) return;
+
+ dice = Object.Instantiate(localDice, effects.transform);
+ dice.transform.localPosition = new Vector3(0, 0, -0.02f);
+ dice.transform.localScale = new Vector3(0.5f, 0.5f, 1);
+
+ dice.DestroyComponent();
+ dice.DestroyGameObjectInChildren("Vignette Cutout");
+ }
+
+ dice.SetActive(false);
+ dice.SetActive(true);
+ }
+
+ ///
+ /// Adds a hook for when the dice are enabled
+ ///
+ /// The hook to run
+ public static void Hook(Action onTrigger) {
+ // Create coroutine since we have to wait for the prefab to be set
+ static IEnumerator DoHook(Action onTrigger) {
+ // Wait for prefab to be spawned
+ yield return null;
+
+ var prefab = HeroController.instance.spawnedLuckyDiceShieldEffect;
+ if (prefab == null) {
+ yield break;
+ }
+
+ // Add enable hook
+ var hook = prefab.AddComponent();
+
+ hook.Enabled += onTrigger;
+ }
+ MonoBehaviourUtil.Instance.StartCoroutine(DoHook(onTrigger));
+ }
+
+ ///
+ /// Removes the hook from the dice
+ ///
+ public static void Unhook() {
+ var prefab = HeroController.instance.spawnedLuckyDiceShieldEffect;
+ if (prefab == null) {
+ return;
+ }
+
+ prefab.DestroyComponent();
+ }
+
+}
From 077ec9d9a917ad2f5a74975d74e66cd46e6c0f1d Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Tue, 21 Apr 2026 15:32:15 -0400
Subject: [PATCH 30/41] Add flea brew
---
SSMP/Animation/AnimationClip.cs | 3 +-
SSMP/Animation/AnimationManager.cs | 15 +-
SSMP/Animation/Effects/Tools/BaseTool.cs | 5 +-
SSMP/Animation/Effects/Tools/FleaBrew.cs | 143 ++++++++++++++++++
SSMP/Animation/Effects/Tools/MagnetiteDice.cs | 2 +-
SSMP/Game/Client/PlayerManager.cs | 6 +
SSMP/Game/Settings/ServerSettings.cs | 12 ++
7 files changed, 181 insertions(+), 5 deletions(-)
create mode 100644 SSMP/Animation/Effects/Tools/FleaBrew.cs
diff --git a/SSMP/Animation/AnimationClip.cs b/SSMP/Animation/AnimationClip.cs
index cf490104..bdb3ff3a 100644
--- a/SSMP/Animation/AnimationClip.cs
+++ b/SSMP/Animation/AnimationClip.cs
@@ -768,5 +768,6 @@ internal enum AnimationClip {
WitchTentacles,
ShamanCancel,
BindInterrupt,
- MagnetiteDice
+ MagnetiteDice,
+ FleaBrew
}
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index 5aad75c5..2f9189db 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -626,7 +626,8 @@ internal class AnimationManager {
{ "Witch Tentacles!", AnimationClip.WitchTentacles },
{ "Shaman Cancel", AnimationClip.ShamanCancel },
{ "Bind Fail Burst", AnimationClip.BindInterrupt },
- { "Magnetite Dice", AnimationClip.MagnetiteDice }
+ { "Magnetite Dice", AnimationClip.MagnetiteDice },
+ { "Flea Brew", AnimationClip.FleaBrew }
};
///
@@ -691,7 +692,8 @@ internal class AnimationManager {
{ AnimationClip.SilkBossNeedleFire, new PaleNails() },
// Tools
- { AnimationClip.MagnetiteDice, new MagnetiteDice() }
+ { AnimationClip.MagnetiteDice, new MagnetiteDice() },
+ { AnimationClip.FleaBrew, new FleaBrew() },
};
///
@@ -1424,12 +1426,21 @@ private void OnPaleNailFire(PlayMakerFSM fsm) {
private void CreateToolHooks() {
MagnetiteDice.Hook(OnDiceEnable);
+
+ var toolFsm = HeroController.instance.toolsFSM;
+ var brewBurst = toolFsm.GetState("Flea Brew Burst");
+ FsmStateActionInjector.Inject(brewBurst, OnFleaBrew, 0, "Flea Brew");
}
private void OnDiceEnable() {
_netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.MagnetiteDice, 0);
}
+ private void OnFleaBrew(PlayMakerFSM fsm) {
+ var effectInfo = FleaBrew.Instance.GetEffectInfo();
+ _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.FleaBrew, 0, effectInfo);
+ }
+
// ///
// /// Callback method on the HeroAnimationController#Play method.
// ///
diff --git a/SSMP/Animation/Effects/Tools/BaseTool.cs b/SSMP/Animation/Effects/Tools/BaseTool.cs
index f2e34a6e..73c0ee53 100644
--- a/SSMP/Animation/Effects/Tools/BaseTool.cs
+++ b/SSMP/Animation/Effects/Tools/BaseTool.cs
@@ -1,9 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
+using GlobalSettings;
namespace SSMP.Animation.Effects.Tools;
internal abstract class BaseTool : DamageAnimationEffect {
-
+ protected static bool HasPoison() {
+ return Gameplay.PoisonPouchTool.IsEquipped;
+ }
}
diff --git a/SSMP/Animation/Effects/Tools/FleaBrew.cs b/SSMP/Animation/Effects/Tools/FleaBrew.cs
new file mode 100644
index 00000000..53588031
--- /dev/null
+++ b/SSMP/Animation/Effects/Tools/FleaBrew.cs
@@ -0,0 +1,143 @@
+using System.Collections;
+using System.Collections.Generic;
+using GlobalSettings;
+using SSMP.Internals;
+using SSMP.Util;
+using UnityEngine;
+
+namespace SSMP.Animation.Effects.Tools;
+
+internal class FleaBrew : BaseTool {
+ ///
+ /// Cached reference to a modified version of the poisoned flea brew trail
+ ///
+ private static GameObject? _modifiedPoisonTrail;
+
+ ///
+ /// Cached values of sprite flashes for flea brews
+ ///
+ private static readonly Dictionary BrewFlashes = [];
+
+ ///
+ /// Instance of the effect class
+ ///
+ public static FleaBrew Instance = new();
+
+ ///
+ public override byte[]? GetEffectInfo() {
+ return [
+ (byte) (HasPoison() ? 1 : 0)
+ ];
+ }
+
+ ///
+ public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ var hc = HeroController.instance;
+ var isPoison = effectInfo is [1];
+
+ // Play audio
+ var fsm = hc.toolsFSM;
+ if (fsm != null) {
+ var audio = fsm.GetFirstAction("Flea Brew Burst");
+ if (audio != null) AudioUtil.PlayAudio(audio, playerObject);
+ }
+
+ // Start particles
+ var duration = hc.QUICKENING_DURATION;
+ var localPrefab = isPoison ? hc.quickeningPoisonEffectPrefab : hc.quickeningEffectPrefab;
+
+ var particles = EffectUtils.SpawnGlobalPoolObject(localPrefab.gameObject, playerObject.transform, duration, true);
+ if (particles == null) return;
+
+ // Set up poison clouds
+ if (isPoison && ShouldDoDamage && ServerSettings.IsPvpEnabled) {
+ SetPoisonTrail(particles);
+ }
+
+ // Play sprite flash
+ if (!playerObject.TryGetComponent(out var flash)) {
+ return;
+ }
+
+ // See if previous effect is playing
+ var id = playerObject.GetInstanceID();
+ if (BrewFlashes.TryGetValue(id, out var prevHandle)) {
+ if (flash.IsFlashing(true, prevHandle)) {
+ flash.CancelRepeatingFlash(prevHandle);
+ }
+ }
+
+ // Start new flash
+ var color = isPoison ? Gameplay.PoisonPouchHeroTintColour : new Color(1f, 0.85f, 0.47f, 1f);
+ var flashHandle = flash.Flash(color, 0.7f, 0.2f, 0.01f, 0.22f, 0f, repeating: true, 0, 1, requireExplicitCancel: true);
+ BrewFlashes[id] = flashHandle;
+
+ MonoBehaviourUtil.Instance.StartCoroutine(StopBrewFlash(playerObject, flashHandle));
+
+ }
+
+ ///
+ /// Stops the flew brew sprite flash
+ ///
+ /// The player that used the tool
+ /// The flash's handle
+ private static IEnumerator StopBrewFlash(GameObject playerObject, SpriteFlash.FlashHandle handle) {
+ // Wait for effect to end
+ yield return new WaitForSeconds(HeroController.instance.QUICKENING_DURATION);
+
+ // Cancel flash
+ if (!playerObject.TryGetComponent(out var flash)) {
+ yield break;
+ }
+
+ flash.CancelRepeatingFlash(handle);
+ }
+
+ ///
+ /// Sets up a poison trail that deals damage
+ ///
+ /// The poisoned flea brew particle spawner
+ private void SetPoisonTrail(GameObject particles) {
+ // Find the prefab spawner
+ var spawnerObj = particles.FindGameObjectInChildren("Trail Spawner");
+ if (!spawnerObj) return;
+
+ if (!spawnerObj.TryGetComponent(out var spawner)) {
+ return;
+ }
+
+ // Set up a modified version of the prefab
+ if (!_modifiedPoisonTrail) {
+ var prefab = spawner.prefab;
+ if (!prefab) return;
+
+ _modifiedPoisonTrail = EffectUtils.SpawnGlobalPoolObject(prefab, particles.transform, 0, false);
+ if (!_modifiedPoisonTrail) return;
+
+ _modifiedPoisonTrail.SetActive(false);
+ _modifiedPoisonTrail.name = "Hornet Poison Trail Modified";
+
+ // Re-add recycler so that it de-spawns.
+ // Since this is a new object, it won't override the other pool.
+ var recycler = _modifiedPoisonTrail.AddComponent();
+ recycler.afterEvent = GlobalEnums.AfterEvent.TIME;
+ recycler.timeToWait = 1.1f;
+
+ // Set the damager
+ var damager = _modifiedPoisonTrail.FindGameObjectInChildren("damager");
+ if (damager) {
+ AddDamageHeroComponent(damager, ServerSettings.PoisonBrewDamage);
+ damager.layer = 17;
+ }
+ } else {
+ // Ensure the damage amount is correct every time the brew is used
+ var damagerObj = _modifiedPoisonTrail.FindGameObjectInChildren("damager");
+ if (damagerObj && damagerObj.TryGetComponent(out var damager)) {
+ damager.damageDealt = ServerSettings.PoisonBrewDamage;
+ }
+ }
+
+ // Set the spawner's prefab
+ spawner.prefab = _modifiedPoisonTrail;
+ }
+}
diff --git a/SSMP/Animation/Effects/Tools/MagnetiteDice.cs b/SSMP/Animation/Effects/Tools/MagnetiteDice.cs
index 1cef5772..b5070b7e 100644
--- a/SSMP/Animation/Effects/Tools/MagnetiteDice.cs
+++ b/SSMP/Animation/Effects/Tools/MagnetiteDice.cs
@@ -62,7 +62,7 @@ static IEnumerator DoHook(Action onTrigger) {
/// Removes the hook from the dice
///
public static void Unhook() {
- var prefab = HeroController.instance.spawnedLuckyDiceShieldEffect;
+ var prefab = HeroController.SilentInstance?.spawnedLuckyDiceShieldEffect;
if (prefab == null) {
return;
}
diff --git a/SSMP/Game/Client/PlayerManager.cs b/SSMP/Game/Client/PlayerManager.cs
index 13d77718..f3f48a5b 100644
--- a/SSMP/Game/Client/PlayerManager.cs
+++ b/SSMP/Game/Client/PlayerManager.cs
@@ -202,6 +202,12 @@ private void TryCreatePlayerPool() {
rigidbody.gravityScale = 0;
rigidbody.bodyType = RigidbodyType2D.Kinematic;
+ // Set up sprite flash. Must be deactivated beforehand to fill properties in awake
+ playerPrefab.SetActive(false);
+ var flash = playerPrefab.AddComponent();
+ flash.parents = [];
+ flash.children = [];
+
// Add some extra gameObjects related to animation effects
new GameObject("Attacks").transform.SetParent(playerPrefab.transform);
new GameObject("Bind Effects").transform.SetParent(playerPrefab.transform);
diff --git a/SSMP/Game/Settings/ServerSettings.cs b/SSMP/Game/Settings/ServerSettings.cs
index 5c11f2c8..6b23d12e 100644
--- a/SSMP/Game/Settings/ServerSettings.cs
+++ b/SSMP/Game/Settings/ServerSettings.cs
@@ -258,6 +258,18 @@ public byte SawtoothCircletDamage {
}
} = 1;
+ ///
+ [SettingAlias("poisonbrewdmg", "brewdmg", "fleabrewdmg")]
+ [ModMenuSetting("Sawtooth Circlet Damage", "The number of masks of damage that a cloud from the poisoned Flea Brew deals")]
+ public byte PoisonBrewDamage {
+ get;
+ init {
+ if (field == value) return;
+ field = value;
+ ChangeEvent?.Invoke(nameof(PoisonBrewDamage));
+ }
+ } = 1;
+
//
// ///
// [SettingAlias("sporeshroomdmg")]
From 672f8fc5339147397c3b894f9697c3c2ae387d42 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Tue, 21 Apr 2026 18:41:33 -0400
Subject: [PATCH 31/41] add fractured mask
---
SSMP/Animation/AnimationClip.cs | 3 +-
SSMP/Animation/AnimationManager.cs | 20 ++++++++++--
SSMP/Animation/Effects/Tools/FracturedMask.cs | 31 +++++++++++++++++++
3 files changed, 51 insertions(+), 3 deletions(-)
create mode 100644 SSMP/Animation/Effects/Tools/FracturedMask.cs
diff --git a/SSMP/Animation/AnimationClip.cs b/SSMP/Animation/AnimationClip.cs
index bdb3ff3a..f9dd4129 100644
--- a/SSMP/Animation/AnimationClip.cs
+++ b/SSMP/Animation/AnimationClip.cs
@@ -769,5 +769,6 @@ internal enum AnimationClip {
ShamanCancel,
BindInterrupt,
MagnetiteDice,
- FleaBrew
+ FleaBrew,
+ FracturedMask
}
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index 2f9189db..b437037c 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -627,7 +627,8 @@ internal class AnimationManager {
{ "Shaman Cancel", AnimationClip.ShamanCancel },
{ "Bind Fail Burst", AnimationClip.BindInterrupt },
{ "Magnetite Dice", AnimationClip.MagnetiteDice },
- { "Flea Brew", AnimationClip.FleaBrew }
+ { "Flea Brew", AnimationClip.FleaBrew },
+ { "Fractured Mask", AnimationClip.FracturedMask }
};
///
@@ -693,7 +694,8 @@ internal class AnimationManager {
// Tools
{ AnimationClip.MagnetiteDice, new MagnetiteDice() },
- { AnimationClip.FleaBrew, new FleaBrew() },
+ { AnimationClip.FleaBrew, FleaBrew.Instance },
+ { AnimationClip.FracturedMask, new FracturedMask() }
};
///
@@ -1430,6 +1432,16 @@ private void CreateToolHooks() {
var toolFsm = HeroController.instance.toolsFSM;
var brewBurst = toolFsm.GetState("Flea Brew Burst");
FsmStateActionInjector.Inject(brewBurst, OnFleaBrew, 0, "Flea Brew");
+
+ var maskFsm = HeroController.instance.gameObject
+ .FindGameObjectInChildren("Charm Effects")?
+ .FindGameObjectInChildren("Fractured Mask Break")?
+ .LocateMyFSM("Spawn Effect");
+
+ if (maskFsm) {
+ var maskEffect = maskFsm.GetState("Instantiate Effect");
+ FsmStateActionInjector.Inject(maskEffect, OnFracturedMaskBreak, 0, "Fractured Mask Break");
+ }
}
private void OnDiceEnable() {
@@ -1441,6 +1453,10 @@ private void OnFleaBrew(PlayMakerFSM fsm) {
_netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.FleaBrew, 0, effectInfo);
}
+ private void OnFracturedMaskBreak(PlayMakerFSM fsm) {
+ _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.FracturedMask, 0);
+ }
+
// ///
// /// Callback method on the HeroAnimationController#Play method.
// ///
diff --git a/SSMP/Animation/Effects/Tools/FracturedMask.cs b/SSMP/Animation/Effects/Tools/FracturedMask.cs
new file mode 100644
index 00000000..1578402b
--- /dev/null
+++ b/SSMP/Animation/Effects/Tools/FracturedMask.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using HutongGames.PlayMaker.Actions;
+using SSMP.Internals;
+using SSMP.Util;
+using UnityEngine;
+
+namespace SSMP.Animation.Effects.Tools;
+
+internal class FracturedMask : BaseTool {
+ ///
+ public override byte[]? GetEffectInfo() {
+ return null;
+ }
+
+ ///
+ public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ var fsm = HeroController.instance.gameObject
+ .FindGameObjectInChildren("Charm Effects")?
+ .FindGameObjectInChildren("Fractured Mask Break")?
+ .LocateMyFSM("Spawn Effect");
+
+ if (fsm == null) return;
+
+ var localMaskShatter = fsm.GetFirstAction("Instantiate Effect");
+ var mask = EffectUtils.SpawnGlobalPoolObject(localMaskShatter.gameObject.Value, playerObject.transform, 5);
+
+ mask?.DestroyComponent();
+ }
+}
From 98c3de5c503e81ab548cac28557fdce4298b7889 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Tue, 21 Apr 2026 22:56:26 -0400
Subject: [PATCH 32/41] add magma bell
---
SSMP/Animation/AnimationClip.cs | 3 +-
SSMP/Animation/AnimationManager.cs | 47 ++++++++++++-
SSMP/Animation/Effects/Tools/MagmaBell.cs | 86 +++++++++++++++++++++++
SSMP/Hooks/EventHooks.cs | 21 ++++++
4 files changed, 153 insertions(+), 4 deletions(-)
create mode 100644 SSMP/Animation/Effects/Tools/MagmaBell.cs
diff --git a/SSMP/Animation/AnimationClip.cs b/SSMP/Animation/AnimationClip.cs
index f9dd4129..caf9239e 100644
--- a/SSMP/Animation/AnimationClip.cs
+++ b/SSMP/Animation/AnimationClip.cs
@@ -770,5 +770,6 @@ internal enum AnimationClip {
BindInterrupt,
MagnetiteDice,
FleaBrew,
- FracturedMask
+ FracturedMask,
+ MagmaBell
}
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index b437037c..4b9db05b 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -628,7 +628,8 @@ internal class AnimationManager {
{ "Bind Fail Burst", AnimationClip.BindInterrupt },
{ "Magnetite Dice", AnimationClip.MagnetiteDice },
{ "Flea Brew", AnimationClip.FleaBrew },
- { "Fractured Mask", AnimationClip.FracturedMask }
+ { "Fractured Mask", AnimationClip.FracturedMask },
+ { "Magma Bell", AnimationClip.MagmaBell }
};
///
@@ -695,7 +696,8 @@ internal class AnimationManager {
// Tools
{ AnimationClip.MagnetiteDice, new MagnetiteDice() },
{ AnimationClip.FleaBrew, FleaBrew.Instance },
- { AnimationClip.FracturedMask, new FracturedMask() }
+ { AnimationClip.FracturedMask, new FracturedMask() },
+ { AnimationClip.MagmaBell, new MagmaBell() }
};
///
@@ -820,6 +822,7 @@ public void RegisterHooks() {
CreateHeroHooks(HeroController.instance);
}
+ EventHooks.UseLavaBell += OnMagmaBell;
// Register a callback so we know when the dash has finished
// On.HeroController.CancelDash += HeroControllerOnCancelDash;
@@ -846,6 +849,9 @@ public void DeregisterHooks() {
FsmStateActionInjector.UninjectAll();
MagnetiteDice.Unhook();
+
+ EventHooks.HeroControllerDie -= OnDeath;
+ EventHooks.UseLavaBell -= OnMagmaBell;
// On.HeroAnimationController.Play -= HeroAnimationControllerOnPlay;
// On.HeroAnimationController.PlayFromFrame -= HeroAnimationControllerOnPlayFromFrame;
@@ -1425,7 +1431,9 @@ private void OnPaleNailFire(PlayMakerFSM fsm) {
_netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.SilkBossNeedleFire, 0, effectInfo);
}
-
+ ///
+ /// Creates hooks for tools
+ ///
private void CreateToolHooks() {
MagnetiteDice.Hook(OnDiceEnable);
@@ -1444,19 +1452,52 @@ private void CreateToolHooks() {
}
}
+ ///
+ /// Hook for the magnetite dice being triggered
+ ///
private void OnDiceEnable() {
+ // If we are not connected, there is nothing to send to
+ if (!_netClient.IsConnected) {
+ return;
+ }
+
_netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.MagnetiteDice, 0);
}
private void OnFleaBrew(PlayMakerFSM fsm) {
+ // If we are not connected, there is nothing to send to
+ if (!_netClient.IsConnected) {
+ return;
+ }
+
var effectInfo = FleaBrew.Instance.GetEffectInfo();
_netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.FleaBrew, 0, effectInfo);
}
+ ///
+ /// Hook for the fractured mask being triggered
+ ///
private void OnFracturedMaskBreak(PlayMakerFSM fsm) {
+ // If we are not connected, there is nothing to send to
+ if (!_netClient.IsConnected) {
+ return;
+ }
+
_netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.FracturedMask, 0);
}
+ ///
+ /// Hook for the magma bell being triggered
+ ///
+ private void OnMagmaBell() {
+ // If we are not connected, there is nothing to send to
+ if (!_netClient.IsConnected) {
+ return;
+ }
+ Logger.Info("magma bell sending");
+ _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.MagmaBell, 0);
+ }
+
// ///
// /// Callback method on the HeroAnimationController#Play method.
// ///
diff --git a/SSMP/Animation/Effects/Tools/MagmaBell.cs b/SSMP/Animation/Effects/Tools/MagmaBell.cs
new file mode 100644
index 00000000..614b90bd
--- /dev/null
+++ b/SSMP/Animation/Effects/Tools/MagmaBell.cs
@@ -0,0 +1,86 @@
+using System.Collections;
+using GlobalSettings;
+using SSMP.Internals;
+using SSMP.Util;
+using UnityEngine;
+
+namespace SSMP.Animation.Effects.Tools;
+
+internal class MagmaBell : BaseTool {
+ ///
+ /// Name of the magma bell starting object name
+ ///
+ private const string MagmaBellStartName = "Magma Bell Start";
+
+ ///
+ /// Name of the magma bell recharging object name
+ ///
+ private const string MagmaBellRechargeName = "Magma Bell Recharge";
+
+ ///
+ public override byte[]? GetEffectInfo() {
+ return null;
+ }
+
+ ///
+ public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ // two parts, when hit and when recovering after some delay
+
+ // Find existing effect
+ var effects = GetPlayerEffects(playerObject);
+ var magmaStart = effects.FindGameObjectInChildren(MagmaBellStartName);
+
+ // Create effect if needed
+ if (!magmaStart) {
+ var prefab = HeroController.instance.lavaBellEffectPrefab;
+
+ magmaStart = EffectUtils.SpawnGlobalPoolObject(prefab, effects.transform, 0, true);
+ if (!magmaStart) return;
+
+ magmaStart.transform.localPosition = Vector3.zero;
+ magmaStart.transform.localScale = new Vector3(0.5f, 0.5f, 1);
+ magmaStart.name = MagmaBellStartName;
+ magmaStart.DestroyComponent();
+ }
+
+ // Toggle effect
+ magmaStart.SetActive(false);
+ magmaStart.SetActive(true);
+
+ // Start the recharge effect
+ MonoBehaviourUtil.Instance.StartCoroutine(PlayRecharge(playerObject));
+ }
+
+ ///
+ /// Plays the recharge animation
+ ///
+ ///
+ ///
+ private static IEnumerator PlayRecharge(GameObject playerObject) {
+ // Wait for bell to recharge
+ yield return new WaitForSeconds(Gameplay.LavaBellCooldownTime - 1);
+
+ // Player has exited the scene
+ if (!playerObject.activeInHierarchy) yield break;
+
+ // Find existing effect
+ var effects = GetPlayerEffects(playerObject);
+ var magmaRecharge = effects.FindGameObjectInChildren(MagmaBellRechargeName);
+
+ // Create effect if needed
+ if (!magmaRecharge) {
+ var prefab = HeroController.instance.lavaBellRechargeEffectPrefab;
+
+ magmaRecharge = EffectUtils.SpawnGlobalPoolObject(prefab, effects.transform, 0, true);
+ if (!magmaRecharge) yield break;
+
+ magmaRecharge.transform.localPosition = Vector3.zero;
+ magmaRecharge.name = MagmaBellRechargeName;
+ magmaRecharge.DestroyComponent();
+ }
+
+ // Toggle effect
+ magmaRecharge.SetActive(false);
+ magmaRecharge.SetActive(true);
+ }
+}
diff --git a/SSMP/Hooks/EventHooks.cs b/SSMP/Hooks/EventHooks.cs
index a1954aa7..e70485a5 100644
--- a/SSMP/Hooks/EventHooks.cs
+++ b/SSMP/Hooks/EventHooks.cs
@@ -81,6 +81,11 @@ public static class EventHooks {
///
private static Hook? _heroControllerDieHook;
+ ///
+ /// Hook for HeroControllerUseLavaBell
+ ///
+ private static Hook? _heroControllerUseLavaBellHook;
+
///
/// Hook for GameMap.PositionCompassAndCorpse.
///
@@ -173,6 +178,11 @@ public static class EventHooks {
///
public static event Action? HeroControllerDie;
+ ///
+ /// Event that is called when HeroController.UseLavaBell is called.
+ ///
+ public static event Action? UseLavaBell;
+
///
/// Event that is called when GameMap.PositionCompassAndCorpse is called.
///
@@ -303,6 +313,11 @@ internal static void Initialize() {
OnHeroControllerDie
);
+ _heroControllerUseLavaBellHook = new Hook(
+ typeof(HeroController).GetMethod(nameof(HeroController.UseLavaBell), BindingFlags),
+ OnUseLavaBell
+ );
+
_gameMapPositionCompassAndCorpseHook = new Hook(
typeof(GameMap).GetMethod(nameof(GameMap.PositionCompassAndCorpse)),
OnGameMapPositionCompassAndCorpse
@@ -475,6 +490,12 @@ bool frostDeath
return orig(self, nonLethal, frostDeath);
}
+ private static void OnUseLavaBell(Action orig, HeroController self) {
+ orig(self);
+
+ UseLavaBell?.Invoke();
+ }
+
private static void OnGameMapPositionCompassAndCorpse(Action orig, GameMap self) {
orig(self);
From f7faa3f75ae75282e04a81ecdcc1f6f07efec80b Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Thu, 30 Apr 2026 20:59:04 -0400
Subject: [PATCH 33/41] fix setting name
---
SSMP/Game/Settings/ServerSettings.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/SSMP/Game/Settings/ServerSettings.cs b/SSMP/Game/Settings/ServerSettings.cs
index 6b23d12e..bf2c3cc0 100644
--- a/SSMP/Game/Settings/ServerSettings.cs
+++ b/SSMP/Game/Settings/ServerSettings.cs
@@ -260,7 +260,7 @@ public byte SawtoothCircletDamage {
///
[SettingAlias("poisonbrewdmg", "brewdmg", "fleabrewdmg")]
- [ModMenuSetting("Sawtooth Circlet Damage", "The number of masks of damage that a cloud from the poisoned Flea Brew deals")]
+ [ModMenuSetting("Poisoned Flea Brew Damage", "The number of masks of damage that a cloud from the poisoned Flea Brew deals")]
public byte PoisonBrewDamage {
get;
init {
From a8e803ad49e8177ed7eb7f8ec48078fd6bda2cca Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Thu, 30 Apr 2026 21:44:01 -0400
Subject: [PATCH 34/41] fix removed comment
---
SSMP/Animation/AnimationEffect.cs | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/SSMP/Animation/AnimationEffect.cs b/SSMP/Animation/AnimationEffect.cs
index 00192ec8..d915b4d1 100644
--- a/SSMP/Animation/AnimationEffect.cs
+++ b/SSMP/Animation/AnimationEffect.cs
@@ -43,7 +43,9 @@ protected static void ChangeAttackDirection(GameObject targetObject, float direc
}
///
- /// "Hides" the player character by stopping its animation and setting its sprite to a small texture
+ /// "Hides" the player character by stopping its animation and setting its sprite to a small texture. Since the
+ /// sprite is overridden next time an animation is played, the player will be unhidden when we receive the next
+ /// animation for that player.
///
/// The player to be hidden.
protected static void HidePlayer(GameObject playerObject) {
From 121ad3ded4344a42ab623973a622b7eec1ce09f8 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Thu, 30 Apr 2026 22:00:11 -0400
Subject: [PATCH 35/41] Undo accidental reverts
---
SSMP/Animation/AnimationManager.cs | 51 ++++++++++---------
.../Animation/Effects/SilkSkills/PaleNails.cs | 7 ---
SSMP/Animation/Effects/SilkSkills/RuneRage.cs | 8 ---
.../Animation/Effects/SilkSkills/SilkSpear.cs | 1 -
.../Effects/SilkSkills/ThreadStorm.cs | 1 -
SSMP/Fsm/FsmActionInjectorComponent.cs | 5 --
SSMP/Game/Settings/ServerSettings.cs | 12 -----
7 files changed, 27 insertions(+), 58 deletions(-)
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index b2ca09fb..58aac888 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -1188,13 +1188,13 @@ private void CreateSkillHooks() {
return;
}
- // Thread storm
+ // Thread Storm
var threadStormExtend = silkSkillFsm.GetState("Extend");
- FsmStateActionInjector.Inject(threadStormExtend, OnThreadStormExtend, 0);
+ FsmStateActionInjector.Inject(threadStormExtend, OnThreadStormExtend);
// Rune Rage
var sonarBuildArray = silkSkillFsm.GetState("Build Enemy Array");
- FsmStateActionInjector.Inject(sonarBuildArray, OnBuildRuneRageArray, 0);
+ FsmStateActionInjector.Inject(sonarBuildArray, OnBuildRuneRageArray);
var blastEnemy = silkSkillFsm.GetState("Blast Enemy");
FsmStateActionInjector.Inject(blastEnemy, OnRuneBlastEnemy, 4);
@@ -1203,7 +1203,7 @@ private void CreateSkillHooks() {
FsmStateActionInjector.Inject(blastRandom, OnRuneBlastRandom, 3);
var blastFinished = silkSkillFsm.GetState("Silk Bomb Recover");
- FsmStateActionInjector.Inject(blastFinished, OnRuneBlastFinished, 0);
+ FsmStateActionInjector.Inject(blastFinished, OnRuneBlastFinished);
// Pale Nails
var nailObject = silkSkillFsm.GetAction("BossNeedle Cast", 5)?.gameObject.Value;
@@ -1226,28 +1226,28 @@ private void CreateSkillHooks() {
foreach (var nail in nails) {
var injector = nail.AddComponent();
List injections = [
- new FsmActionInjectorComponent.Injection {
- FsmName = nailFsm.FsmName,
- FsmStateName = "Follow HeroFacingLeft",
- ActionIndex = 12,
+ new () {
+ fsmName = nailFsm.FsmName,
+ fsmStateName = "Follow HeroFacingLeft",
+ actionIndex = 12,
Hook = OnPaleNailAttackCheck,
- HookName = "Nail Target"
+ hookName = "Nail Target"
},
- new FsmActionInjectorComponent.Injection {
- FsmName = nailFsm.FsmName,
- FsmStateName = "Follow HeroFacingRight",
- ActionIndex = 12,
+ new () {
+ fsmName = nailFsm.FsmName,
+ fsmStateName = "Follow HeroFacingRight",
+ actionIndex = 12,
Hook = OnPaleNailAttackCheck,
- HookName = "Nail Target"
+ hookName = "Nail Target"
},
- new FsmActionInjectorComponent.Injection {
- FsmName = nailFsm.FsmName,
- FsmStateName = "Fire Antic",
- ActionIndex = 0,
+ new () {
+ fsmName = nailFsm.FsmName,
+ fsmStateName = "Fire Antic",
+ actionIndex = 0,
Hook = OnPaleNailFire,
- HookName = "Nail Fire"
+ hookName = "Nail Fire"
}
];
@@ -1441,7 +1441,7 @@ private void OnPaleNailFire(PlayMakerFSM fsm) {
}
///
- /// Creates hooks for tools
+ /// Creates hooks for tools.
///
private void CreateToolHooks() {
MagnetiteDice.Hook(OnDiceEnable);
@@ -1462,7 +1462,7 @@ private void CreateToolHooks() {
}
///
- /// Hook for the magnetite dice being triggered
+ /// Hook for the magnetite dice being triggered.
///
private void OnDiceEnable() {
// If we are not connected, there is nothing to send to
@@ -1473,6 +1473,9 @@ private void OnDiceEnable() {
_netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.MagnetiteDice, 0);
}
+ ///
+ /// Hook for the flea brew being triggered.
+ ///
private void OnFleaBrew(PlayMakerFSM fsm) {
// If we are not connected, there is nothing to send to
if (!_netClient.IsConnected) {
@@ -1484,7 +1487,7 @@ private void OnFleaBrew(PlayMakerFSM fsm) {
}
///
- /// Hook for the fractured mask being triggered
+ /// Hook for the fractured mask being triggered.
///
private void OnFracturedMaskBreak(PlayMakerFSM fsm) {
// If we are not connected, there is nothing to send to
@@ -1496,14 +1499,14 @@ private void OnFracturedMaskBreak(PlayMakerFSM fsm) {
}
///
- /// Hook for the magma bell being triggered
+ /// Hook for the magma bell being triggered.
///
private void OnMagmaBell() {
// If we are not connected, there is nothing to send to
if (!_netClient.IsConnected) {
return;
}
- Logger.Info("magma bell sending");
+
_netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.MagmaBell, 0);
}
diff --git a/SSMP/Animation/Effects/SilkSkills/PaleNails.cs b/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
index 319dbfac..e70ef343 100644
--- a/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
+++ b/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
@@ -388,13 +388,11 @@ private static void FixFsmForUse(PlayMakerFSM fsm, GameObject playerObject, bool
SetVolt(fsm, isVolt, "Launch NoTarget", 11);
var burst = fsm.GetFirstAction("Burst");
- if (burst != null) {
if (isVolt) {
burst.isFalse = burst.isTrue;
} else {
burst.isTrue = burst.isFalse;
}
- }
fsm.enabled = true;
}
@@ -492,8 +490,6 @@ public static byte[] EncodeTargetInfo(GameObject target) {
private static NailTarget? DecodeTargetInfo(byte[]? info) {
if (info == null || info.Length < 6) return null;
- var isVolt = info[0] == 1;
-
// Convert two bytes to ushorts, then to floats.
// Offset to restore the range to a short.
var x = (float) BitConverter.ToUInt16([info[1], info[2]], 0) - PositionOffset;
@@ -557,9 +553,6 @@ private static GameObject FindTarget(NailTarget target) {
///
private struct NailTarget {
public Vector2 Position;
-
public bool IsPlayer;
-
- public bool IsVolt;
}
}
diff --git a/SSMP/Animation/Effects/SilkSkills/RuneRage.cs b/SSMP/Animation/Effects/SilkSkills/RuneRage.cs
index 7f532489..e680a263 100644
--- a/SSMP/Animation/Effects/SilkSkills/RuneRage.cs
+++ b/SSMP/Animation/Effects/SilkSkills/RuneRage.cs
@@ -106,10 +106,8 @@ private static void PlayRageAntic(GameObject playerObject, bool isVolt) {
// Play volt audio
if (isVolt) {
var voltAntic = fsm.GetFirstAction("S Bomb Zap FX");
- if (voltAntic != null) {
AudioUtil.PlayAudio(voltAntic, playerObject);
}
- }
// Play normal audio
var runeAnticAudio = fsm.GetAction("Silk Bomb Start", 15);
@@ -129,21 +127,16 @@ private static void PlaySonar(GameObject playerObject, bool isVolt, bool isShama
// Play general audio
var runeBurstAudio = fsm.GetFirstAction("Initial Silk Cost");
- if (runeBurstAudio != null) {
AudioUtil.PlayAudio(runeBurstAudio, playerObject);
- }
// Play volt audio
if (isVolt) {
var zapAudioBug = fsm.GetFirstAction("S Bomb Zap FX 2");
- if (zapAudioBug != null) {
AudioUtil.PlayAudio(zapAudioBug, playerObject);
}
- }
// Spawn sonar, picking the right one for the volt filament setting
var sonarPicker = fsm.GetFirstAction("Sonar Cast Effects");
- if (sonarPicker == null) return;
GameObject localSonar;
@@ -253,7 +246,6 @@ private static bool TryGetAntic(GameObject playerObject, [MaybeNullWhen(false)]
if (clusterFsm == null) return null;
var blaster = clusterFsm.GetFirstAction("Do Explosions");
- if (blaster == null) return null;
// Fill out both blast prefabs
_localRuneBlastVolt = blaster.TrueGameObject.Value;
diff --git a/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs b/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
index b35c0437..689f9c6d 100644
--- a/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
+++ b/SSMP/Animation/Effects/SilkSkills/SilkSpear.cs
@@ -101,7 +101,6 @@ private static IEnumerator PlayPossibleThunk(GameObject playerObject, GameObject
// Try to thunk as long as the spear is doing damage
while (collider.isActiveAndEnabled) {
-
// Find a terrain collider within the bounds of the spear
var y = spear.transform.position.y;
// ReSharper disable once Unity.PreferNonAllocApi
diff --git a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
index 1e46c7ad..b19f3b28 100644
--- a/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
+++ b/SSMP/Animation/Effects/SilkSkills/ThreadStorm.cs
@@ -14,7 +14,6 @@ namespace SSMP.Animation.Effects.SilkSkills;
/// Effect class for the Thread Storm Silk Skill.
///
internal class ThreadStorm : BaseSilkSkill {
-
///
/// A reference for players currently extending their thread storms.
/// Used to prevent the effect from disappearing early.
diff --git a/SSMP/Fsm/FsmActionInjectorComponent.cs b/SSMP/Fsm/FsmActionInjectorComponent.cs
index b765b2e0..4245a9e9 100644
--- a/SSMP/Fsm/FsmActionInjectorComponent.cs
+++ b/SSMP/Fsm/FsmActionInjectorComponent.cs
@@ -63,11 +63,6 @@ private void TryDoInjection() {
if (_injections.Count == 0) return;
foreach (var injection in _injections) {
- // Ensure injection is set up correctly
- if (injection.FsmName == null) return;
- if (injection.Hook == null) return;
- if (injection.FsmStateName == null) return;
-
// Locate and patch the FSM
var fsm = gameObject.LocateMyFSM(injection.fsmName);
var state = fsm.GetState(injection.fsmStateName);
diff --git a/SSMP/Game/Settings/ServerSettings.cs b/SSMP/Game/Settings/ServerSettings.cs
index 49261b6e..e15c95f6 100644
--- a/SSMP/Game/Settings/ServerSettings.cs
+++ b/SSMP/Game/Settings/ServerSettings.cs
@@ -84,18 +84,6 @@ public bool AllowSkins {
ChangeEvent?.Invoke(nameof(AllowSkins));
}
} = true;
-
- ///
- [SettingAlias("needledmg")]
- [ModMenuSetting("Needle Damage", "The number of masks of damage that a player's needle swing deals")]
- public byte NeedleDamage {
- get;
- init {
- if (field == value) return;
- field = value;
- ChangeEvent?.Invoke(nameof(NeedleDamage));
- }
- } = 1;
// ///
// [SettingAlias("parries")]
From 0d894d38418384fe271023281d790db86e8885f6 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Thu, 30 Apr 2026 22:11:02 -0400
Subject: [PATCH 36/41] comments
---
SSMP/Animation/Effects/Tools/BaseTool.cs | 4 ++++
SSMP/Animation/Effects/Tools/FleaBrew.cs | 16 ++++++++--------
SSMP/Animation/Effects/Tools/FracturedMask.cs | 5 ++---
SSMP/Animation/Effects/Tools/MagmaBell.cs | 11 +++++------
SSMP/Animation/Effects/Tools/MagnetiteDice.cs | 16 ++++++++++++----
.../Animation/Effects/Tools/SawtoothCirclet.cs | 18 +++++++++---------
6 files changed, 40 insertions(+), 30 deletions(-)
diff --git a/SSMP/Animation/Effects/Tools/BaseTool.cs b/SSMP/Animation/Effects/Tools/BaseTool.cs
index 73c0ee53..210385f7 100644
--- a/SSMP/Animation/Effects/Tools/BaseTool.cs
+++ b/SSMP/Animation/Effects/Tools/BaseTool.cs
@@ -6,6 +6,10 @@
namespace SSMP.Animation.Effects.Tools;
internal abstract class BaseTool : DamageAnimationEffect {
+ ///
+ /// Determines whether the players tools have the Pollip Pouch poison effect.
+ ///
+ /// True if the player has the Pollip Pouch equipped.
protected static bool HasPoison() {
return Gameplay.PoisonPouchTool.IsEquipped;
}
diff --git a/SSMP/Animation/Effects/Tools/FleaBrew.cs b/SSMP/Animation/Effects/Tools/FleaBrew.cs
index 53588031..a5f6a593 100644
--- a/SSMP/Animation/Effects/Tools/FleaBrew.cs
+++ b/SSMP/Animation/Effects/Tools/FleaBrew.cs
@@ -9,17 +9,17 @@ namespace SSMP.Animation.Effects.Tools;
internal class FleaBrew : BaseTool {
///
- /// Cached reference to a modified version of the poisoned flea brew trail
+ /// Cached reference to a modified version of the poisoned flea brew trail.
///
private static GameObject? _modifiedPoisonTrail;
///
- /// Cached values of sprite flashes for flea brews
+ /// Cached values of sprite flashes for flea brews.
///
private static readonly Dictionary BrewFlashes = [];
///
- /// Instance of the effect class
+ /// Instance of the effect class.
///
public static FleaBrew Instance = new();
@@ -77,10 +77,10 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
}
///
- /// Stops the flew brew sprite flash
+ /// Stops the flew brew sprite flash.
///
- /// The player that used the tool
- /// The flash's handle
+ /// The player that used the tool.
+ /// The flash's handle.
private static IEnumerator StopBrewFlash(GameObject playerObject, SpriteFlash.FlashHandle handle) {
// Wait for effect to end
yield return new WaitForSeconds(HeroController.instance.QUICKENING_DURATION);
@@ -94,9 +94,9 @@ private static IEnumerator StopBrewFlash(GameObject playerObject, SpriteFlash.Fl
}
///
- /// Sets up a poison trail that deals damage
+ /// Sets up a poison trail that deals damage.
///
- /// The poisoned flea brew particle spawner
+ /// The poisoned flea brew particle spawner.
private void SetPoisonTrail(GameObject particles) {
// Find the prefab spawner
var spawnerObj = particles.FindGameObjectInChildren("Trail Spawner");
diff --git a/SSMP/Animation/Effects/Tools/FracturedMask.cs b/SSMP/Animation/Effects/Tools/FracturedMask.cs
index 1578402b..6a652435 100644
--- a/SSMP/Animation/Effects/Tools/FracturedMask.cs
+++ b/SSMP/Animation/Effects/Tools/FracturedMask.cs
@@ -1,6 +1,3 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
using HutongGames.PlayMaker.Actions;
using SSMP.Internals;
using SSMP.Util;
@@ -16,6 +13,7 @@ internal class FracturedMask : BaseTool {
///
public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ // Find effect
var fsm = HeroController.instance.gameObject
.FindGameObjectInChildren("Charm Effects")?
.FindGameObjectInChildren("Fractured Mask Break")?
@@ -23,6 +21,7 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
if (fsm == null) return;
+ // Spawn in the shatter particles
var localMaskShatter = fsm.GetFirstAction("Instantiate Effect");
var mask = EffectUtils.SpawnGlobalPoolObject(localMaskShatter.gameObject.Value, playerObject.transform, 5);
diff --git a/SSMP/Animation/Effects/Tools/MagmaBell.cs b/SSMP/Animation/Effects/Tools/MagmaBell.cs
index 614b90bd..4f768952 100644
--- a/SSMP/Animation/Effects/Tools/MagmaBell.cs
+++ b/SSMP/Animation/Effects/Tools/MagmaBell.cs
@@ -8,12 +8,12 @@ namespace SSMP.Animation.Effects.Tools;
internal class MagmaBell : BaseTool {
///
- /// Name of the magma bell starting object name
+ /// Name of the magma bell starting object name.
///
private const string MagmaBellStartName = "Magma Bell Start";
///
- /// Name of the magma bell recharging object name
+ /// Name of the magma bell recharging object name.
///
private const string MagmaBellRechargeName = "Magma Bell Recharge";
@@ -52,15 +52,14 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
}
///
- /// Plays the recharge animation
+ /// Plays the recharge animation.
///
- ///
- ///
+ /// The player to use the animation on.
private static IEnumerator PlayRecharge(GameObject playerObject) {
// Wait for bell to recharge
yield return new WaitForSeconds(Gameplay.LavaBellCooldownTime - 1);
- // Player has exited the scene
+ // Player has exited the scene, don't play.
if (!playerObject.activeInHierarchy) yield break;
// Find existing effect
diff --git a/SSMP/Animation/Effects/Tools/MagnetiteDice.cs b/SSMP/Animation/Effects/Tools/MagnetiteDice.cs
index b5070b7e..66e55ddc 100644
--- a/SSMP/Animation/Effects/Tools/MagnetiteDice.cs
+++ b/SSMP/Animation/Effects/Tools/MagnetiteDice.cs
@@ -8,17 +8,23 @@
namespace SSMP.Animation.Effects.Tools;
internal class MagnetiteDice : BaseTool {
+ ///
+ /// Name of the magnetite dice effect object.
+ ///
private const string DiceName = "dice_shield_effect";
+ ///
public override byte[]? GetEffectInfo() {
return null;
}
-
+ ///
public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ // Get existing effect
var effects = GetPlayerEffects(playerObject);
var dice = effects.FindGameObjectInChildren(DiceName);
+ // Set up effect if needed
if (dice == null) {
var localDice = HeroController.instance.spawnedLuckyDiceShieldEffect;
if (localDice == null) return;
@@ -31,14 +37,15 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
dice.DestroyGameObjectInChildren("Vignette Cutout");
}
+ // Toggle effect
dice.SetActive(false);
dice.SetActive(true);
}
///
- /// Adds a hook for when the dice are enabled
+ /// Adds a hook for when the dice are enabled.
///
- /// The hook to run
+ /// The hook to run.
public static void Hook(Action onTrigger) {
// Create coroutine since we have to wait for the prefab to be set
static IEnumerator DoHook(Action onTrigger) {
@@ -55,11 +62,12 @@ static IEnumerator DoHook(Action onTrigger) {
hook.Enabled += onTrigger;
}
+
MonoBehaviourUtil.Instance.StartCoroutine(DoHook(onTrigger));
}
///
- /// Removes the hook from the dice
+ /// Removes the hook from the dice.
///
public static void Unhook() {
var prefab = HeroController.SilentInstance?.spawnedLuckyDiceShieldEffect;
diff --git a/SSMP/Animation/Effects/Tools/SawtoothCirclet.cs b/SSMP/Animation/Effects/Tools/SawtoothCirclet.cs
index e2266bb4..74131ad4 100644
--- a/SSMP/Animation/Effects/Tools/SawtoothCirclet.cs
+++ b/SSMP/Animation/Effects/Tools/SawtoothCirclet.cs
@@ -12,12 +12,12 @@ namespace SSMP.Animation.Effects.Tools;
internal class SawtoothCirclet : BaseTool {
///
- /// The name of the sawtooth circlet object
+ /// The name of the sawtooth circlet object.
///
private const string SpikedCircletName = "Tool_brolly_spike";
///
- /// Cached reference to the local sawtooth circlet object
+ /// Cached reference to the local sawtooth circlet object.
///
private static GameObject? _localCirclet;
@@ -32,10 +32,10 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
}
///
- /// Plays the sawtooth circlet
+ /// Plays the sawtooth circlet.
///
- /// The player using the circlet
- /// If the circlet should do damage
+ /// The player using the circlet.
+ /// If the circlet should do damage.
public static void PlayCirclet(GameObject playerObject, bool doDamage, ServerSettings serverSettings) {
// Get the circlet
if (!TryGetCirclet(playerObject, out var circlet)) {
@@ -68,11 +68,11 @@ public static void PlayCirclet(GameObject playerObject, bool doDamage, ServerSet
}
///
- /// Attempts to find or create the sawtooth circlet object
+ /// Attempts to find or create the sawtooth circlet object.
///
- /// The player using the circlet
- /// The circlet, if found
- /// true if the circlet was found
+ /// The player using the circlet.
+ /// The circlet, if found.
+ /// true if the circlet was found.
private static bool TryGetCirclet(GameObject playerObject, [MaybeNullWhen(false)] out GameObject circlet) {
// Find or create effects
var effects = playerObject.FindGameObjectInChildren("Effects");
From 1fc3cb6eeb9d698268113dde361809ad39c49cca Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Thu, 30 Apr 2026 22:12:13 -0400
Subject: [PATCH 37/41] fix style
---
SSMP/Animation/AnimationManager.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index 58aac888..14030c09 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -1226,7 +1226,7 @@ private void CreateSkillHooks() {
foreach (var nail in nails) {
var injector = nail.AddComponent();
List injections = [
- new () {
+ new() {
fsmName = nailFsm.FsmName,
fsmStateName = "Follow HeroFacingLeft",
actionIndex = 12,
@@ -1234,7 +1234,7 @@ private void CreateSkillHooks() {
hookName = "Nail Target"
},
- new () {
+ new() {
fsmName = nailFsm.FsmName,
fsmStateName = "Follow HeroFacingRight",
actionIndex = 12,
@@ -1242,7 +1242,7 @@ private void CreateSkillHooks() {
hookName = "Nail Target"
},
- new () {
+ new() {
fsmName = nailFsm.FsmName,
fsmStateName = "Fire Antic",
actionIndex = 0,
From ee216c80d20cdd782070e48d65ca997d05e11923 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Fri, 1 May 2026 19:20:19 -0400
Subject: [PATCH 38/41] Add component to identify damaging tools
---
SSMP/Animation/DamageAnimationEffect.cs | 5 ++++-
SSMP/Util/EffectOwnerComponent.cs | 13 +++++++++++++
2 files changed, 17 insertions(+), 1 deletion(-)
create mode 100644 SSMP/Util/EffectOwnerComponent.cs
diff --git a/SSMP/Animation/DamageAnimationEffect.cs b/SSMP/Animation/DamageAnimationEffect.cs
index 6d254dbd..4dcd184e 100644
--- a/SSMP/Animation/DamageAnimationEffect.cs
+++ b/SSMP/Animation/DamageAnimationEffect.cs
@@ -29,7 +29,7 @@ public void SetShouldDoDamage(bool shouldDoDamage) {
}
///
- /// Adds a component to the given game object that deals the given damage when the player
+ /// Adds and components to the given game object that deals the given damage when the player
/// collides with it.
///
/// The target game object to attach the component to.
@@ -40,6 +40,9 @@ protected static DamageHero AddDamageHeroComponent(GameObject target, int damage
damageHero.damageDealt = damage;
damageHero.OnDamagedHero = new UnityEvent();
+ var identifier = target.AddComponentIfNotPresent();
+ identifier.Owner = target;
+
return damageHero;
}
diff --git a/SSMP/Util/EffectOwnerComponent.cs b/SSMP/Util/EffectOwnerComponent.cs
new file mode 100644
index 00000000..16636b0b
--- /dev/null
+++ b/SSMP/Util/EffectOwnerComponent.cs
@@ -0,0 +1,13 @@
+using UnityEngine;
+
+namespace SSMP.Util;
+
+///
+/// Associates an effect object with the player object it belongs to
+///
+public class EffectOwnerComponent : MonoBehaviour {
+ ///
+ /// The owner of the effect
+ ///
+ public GameObject? Owner;
+}
From fca8350b0475ede87499c8d1fa79f930fd035cdd Mon Sep 17 00:00:00 2001
From: Extremelyd1 <10898310+Extremelyd1@users.noreply.github.com>
Date: Fri, 8 May 2026 20:04:36 +0200
Subject: [PATCH 39/41] Fix formatting, comments, and naming
---
SSMP/Animation/AnimationEffect.cs | 17 ++++----
SSMP/Animation/AnimationManager.cs | 10 ++---
SSMP/Animation/DamageAnimationEffect.cs | 9 ++--
.../{UmbrellaInflate.cs => DriftersCloak.cs} | 7 +++-
.../{DoubleJump.cs => FaydownCloak.cs} | 10 ++++-
SSMP/Animation/Effects/Tools/BaseTool.cs | 8 ++--
SSMP/Animation/Effects/Tools/FleaBrew.cs | 41 ++++++++++++++-----
SSMP/Animation/Effects/Tools/FracturedMask.cs | 9 +++-
SSMP/Animation/Effects/Tools/MagmaBell.cs | 5 ++-
SSMP/Animation/Effects/Tools/MagnetiteDice.cs | 6 ++-
.../Effects/Tools/SawtoothCirclet.cs | 35 ++++++----------
SSMP/Api/Server/IServerSettings.cs | 10 +++++
SSMP/Util/EffectOwnerComponent.cs | 4 +-
13 files changed, 107 insertions(+), 64 deletions(-)
rename SSMP/Animation/Effects/Movement/{UmbrellaInflate.cs => DriftersCloak.cs} (84%)
rename SSMP/Animation/Effects/Movement/{DoubleJump.cs => FaydownCloak.cs} (82%)
diff --git a/SSMP/Animation/AnimationEffect.cs b/SSMP/Animation/AnimationEffect.cs
index d915b4d1..e3306b28 100644
--- a/SSMP/Animation/AnimationEffect.cs
+++ b/SSMP/Animation/AnimationEffect.cs
@@ -55,10 +55,11 @@ protected static void HidePlayer(GameObject playerObject) {
}
///
- /// Gets the Effects object for a given player
+ /// Gets the Effects object for a given player object. If the player object does not have an Effects object, it
+ /// will be created.
///
- /// The player using the effect
- /// The player's effects object
+ /// The player object for the player using the effect.
+ /// The player's effects object.
protected static GameObject GetPlayerEffects(GameObject playerObject) {
var effects = playerObject.FindGameObjectInChildren("Effects");
if (effects == null) {
@@ -70,12 +71,12 @@ protected static GameObject GetPlayerEffects(GameObject playerObject) {
}
///
- /// Attempts to get or create an effect from the Effects sub-object
+ /// Attempts to get or create an effect from the Effects sub-object.
///
- /// The player using the effect
- /// The name of the effect object
- /// The effect, if found or created
- /// true if created, false otherwise
+ /// The player object for the player using the effect.
+ /// The name of the effect object.
+ /// The effect, if found or created.
+ /// True if created, false otherwise.
protected static bool TryGetEffect(GameObject playerObject, string effectName, out GameObject? effect) {
// Find or create effects for player
var effects = GetPlayerEffects(playerObject);
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index 14030c09..bb1b969e 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -668,8 +668,8 @@ internal class AnimationManager {
{ AnimationClip.BindBurstAir, BindBurst.Instance },
{ AnimationClip.RageBindBurst, BindBurst.Instance },
{ AnimationClip.Death, new Death() },
- { AnimationClip.DoubleJump, new DoubleJump() },
- { AnimationClip.UmbrellaInflate, new UmbrellaInflate() },
+ { AnimationClip.DoubleJump, new FaydownCloak() },
+ { AnimationClip.UmbrellaInflate, new DriftersCloak() },
// Silk Skills
{ AnimationClip.NeedleThrowThrowing, new SilkSpear() },
@@ -1470,7 +1470,7 @@ private void OnDiceEnable() {
return;
}
- _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.MagnetiteDice, 0);
+ _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.MagnetiteDice);
}
///
@@ -1495,7 +1495,7 @@ private void OnFracturedMaskBreak(PlayMakerFSM fsm) {
return;
}
- _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.FracturedMask, 0);
+ _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.FracturedMask);
}
///
@@ -1507,7 +1507,7 @@ private void OnMagmaBell() {
return;
}
- _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.MagmaBell, 0);
+ _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.MagmaBell);
}
// ///
diff --git a/SSMP/Animation/DamageAnimationEffect.cs b/SSMP/Animation/DamageAnimationEffect.cs
index 4dcd184e..1866cffc 100644
--- a/SSMP/Animation/DamageAnimationEffect.cs
+++ b/SSMP/Animation/DamageAnimationEffect.cs
@@ -29,8 +29,9 @@ public void SetShouldDoDamage(bool shouldDoDamage) {
}
///
- /// Adds and components to the given game object that deals the given damage when the player
- /// collides with it.
+ /// Adds a component to the given game object that deals the given damage when the player
+ /// collides with it. Also adds a component that indicates the owner of this
+ /// object.
///
/// The target game object to attach the component to.
/// The number of mask of damage it should deal.
@@ -50,7 +51,7 @@ protected static DamageHero AddDamageHeroComponent(GameObject target, int damage
/// Removes a component from the given game object.
///
/// The target game object to detach the component from.
- protected static void RemoveDamageHeroComponent(GameObject target) {
+ private static void RemoveDamageHeroComponent(GameObject target) {
target.DestroyComponent();
}
@@ -73,7 +74,7 @@ protected static void RemoveDamageHeroComponent(GameObject target) {
/// The number of mask of damage it should deal.
/// If the damager should be enabled or not
/// The component that was added if PVP was turned on
- protected static DamageHero? SetDamageHeroState(GameObject target, bool doDamage, int damage = 1) {
+ public static DamageHero? SetDamageHeroState(GameObject target, bool doDamage, int damage = 1) {
if (doDamage && damage > 0) {
return AddDamageHeroComponent(target, damage);
}
diff --git a/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs b/SSMP/Animation/Effects/Movement/DriftersCloak.cs
similarity index 84%
rename from SSMP/Animation/Effects/Movement/UmbrellaInflate.cs
rename to SSMP/Animation/Effects/Movement/DriftersCloak.cs
index 549e80de..1f8d913f 100644
--- a/SSMP/Animation/Effects/Movement/UmbrellaInflate.cs
+++ b/SSMP/Animation/Effects/Movement/DriftersCloak.cs
@@ -5,10 +5,13 @@
namespace SSMP.Animation.Effects.Movement;
-internal class UmbrellaInflate : DamageAnimationEffect {
+///
+/// Class for the animation effect of Drifter's Cloak (slow fall, ride updrafts).
+///
+internal class DriftersCloak : DamageAnimationEffect {
///
- public override byte[]? GetEffectInfo() {
+ public override byte[] GetEffectInfo() {
return [
(byte)(ToolItemManager.IsToolEquipped("Brolly Spike") ? 1 : 0)
];
diff --git a/SSMP/Animation/Effects/Movement/DoubleJump.cs b/SSMP/Animation/Effects/Movement/FaydownCloak.cs
similarity index 82%
rename from SSMP/Animation/Effects/Movement/DoubleJump.cs
rename to SSMP/Animation/Effects/Movement/FaydownCloak.cs
index ca1d7c38..28d7aa06 100644
--- a/SSMP/Animation/Effects/Movement/DoubleJump.cs
+++ b/SSMP/Animation/Effects/Movement/FaydownCloak.cs
@@ -5,11 +5,17 @@
namespace SSMP.Animation.Effects.Movement;
-internal class DoubleJump : DamageAnimationEffect {
+///
+/// Class for the animation effect of Faydown Cloak (double jump).
+///
+internal class FaydownCloak : DamageAnimationEffect {
+ ///
+ /// The name of the game object for the effect for checking if it exists on a player object already.
+ ///
private const string JumpEffectName = "double_jump_feathers";
///
- public override byte[]? GetEffectInfo() {
+ public override byte[] GetEffectInfo() {
return [
(byte)(ToolItemManager.IsToolEquipped("Brolly Spike") ? 1 : 0)
];
diff --git a/SSMP/Animation/Effects/Tools/BaseTool.cs b/SSMP/Animation/Effects/Tools/BaseTool.cs
index 210385f7..ee24af3c 100644
--- a/SSMP/Animation/Effects/Tools/BaseTool.cs
+++ b/SSMP/Animation/Effects/Tools/BaseTool.cs
@@ -1,15 +1,15 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
using GlobalSettings;
namespace SSMP.Animation.Effects.Tools;
+///
+/// Base class for animations effects of tools that can deal damage to other players.
+///
internal abstract class BaseTool : DamageAnimationEffect {
///
/// Determines whether the players tools have the Pollip Pouch poison effect.
///
- /// True if the player has the Pollip Pouch equipped.
+ /// True if the player has the Pollip Pouch equipped, otherwise false.
protected static bool HasPoison() {
return Gameplay.PoisonPouchTool.IsEquipped;
}
diff --git a/SSMP/Animation/Effects/Tools/FleaBrew.cs b/SSMP/Animation/Effects/Tools/FleaBrew.cs
index a5f6a593..5e6838c2 100644
--- a/SSMP/Animation/Effects/Tools/FleaBrew.cs
+++ b/SSMP/Animation/Effects/Tools/FleaBrew.cs
@@ -7,6 +7,9 @@
namespace SSMP.Animation.Effects.Tools;
+///
+/// Class for the tool effect of Flea Brew (attack buff).
+///
internal class FleaBrew : BaseTool {
///
/// Cached reference to a modified version of the poisoned flea brew trail.
@@ -14,17 +17,17 @@ internal class FleaBrew : BaseTool {
private static GameObject? _modifiedPoisonTrail;
///
- /// Cached values of sprite flashes for flea brews.
+ /// Cached values of sprite flashes for Flea Brews.
///
private static readonly Dictionary BrewFlashes = [];
///
/// Instance of the effect class.
///
- public static FleaBrew Instance = new();
+ public static readonly FleaBrew Instance = new();
///
- public override byte[]? GetEffectInfo() {
+ public override byte[] GetEffectInfo() {
return [
(byte) (HasPoison() ? 1 : 0)
];
@@ -39,14 +42,19 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
var fsm = hc.toolsFSM;
if (fsm != null) {
var audio = fsm.GetFirstAction("Flea Brew Burst");
- if (audio != null) AudioUtil.PlayAudio(audio, playerObject);
+ AudioUtil.PlayAudio(audio, playerObject);
}
// Start particles
var duration = hc.QUICKENING_DURATION;
var localPrefab = isPoison ? hc.quickeningPoisonEffectPrefab : hc.quickeningEffectPrefab;
- var particles = EffectUtils.SpawnGlobalPoolObject(localPrefab.gameObject, playerObject.transform, duration, true);
+ var particles = EffectUtils.SpawnGlobalPoolObject(
+ localPrefab.gameObject,
+ playerObject.transform,
+ duration,
+ true
+ );
if (particles == null) return;
// Set up poison clouds
@@ -69,7 +77,18 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
// Start new flash
var color = isPoison ? Gameplay.PoisonPouchHeroTintColour : new Color(1f, 0.85f, 0.47f, 1f);
- var flashHandle = flash.Flash(color, 0.7f, 0.2f, 0.01f, 0.22f, 0f, repeating: true, 0, 1, requireExplicitCancel: true);
+ var flashHandle = flash.Flash(
+ color,
+ 0.7f,
+ 0.2f,
+ 0.01f,
+ 0.22f,
+ 0f,
+ repeating: true,
+ 0,
+ 1,
+ requireExplicitCancel: true
+ );
BrewFlashes[id] = flashHandle;
MonoBehaviourUtil.Instance.StartCoroutine(StopBrewFlash(playerObject, flashHandle));
@@ -77,7 +96,7 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
}
///
- /// Stops the flew brew sprite flash.
+ /// Stops the Flea Brew sprite flash.
///
/// The player that used the tool.
/// The flash's handle.
@@ -96,7 +115,7 @@ private static IEnumerator StopBrewFlash(GameObject playerObject, SpriteFlash.Fl
///
/// Sets up a poison trail that deals damage.
///
- /// The poisoned flea brew particle spawner.
+ /// The poisoned Flea Brew particle spawner.
private void SetPoisonTrail(GameObject particles) {
// Find the prefab spawner
var spawnerObj = particles.FindGameObjectInChildren("Trail Spawner");
@@ -111,14 +130,14 @@ private void SetPoisonTrail(GameObject particles) {
var prefab = spawner.prefab;
if (!prefab) return;
- _modifiedPoisonTrail = EffectUtils.SpawnGlobalPoolObject(prefab, particles.transform, 0, false);
+ _modifiedPoisonTrail = EffectUtils.SpawnGlobalPoolObject(prefab, particles.transform, 0);
if (!_modifiedPoisonTrail) return;
_modifiedPoisonTrail.SetActive(false);
_modifiedPoisonTrail.name = "Hornet Poison Trail Modified";
- // Re-add recycler so that it de-spawns.
- // Since this is a new object, it won't override the other pool.
+ // Re-add recycler so that it de-spawns
+ // Since this is a new object, it won't override the other pool
var recycler = _modifiedPoisonTrail.AddComponent();
recycler.afterEvent = GlobalEnums.AfterEvent.TIME;
recycler.timeToWait = 1.1f;
diff --git a/SSMP/Animation/Effects/Tools/FracturedMask.cs b/SSMP/Animation/Effects/Tools/FracturedMask.cs
index 6a652435..b4ca4ea9 100644
--- a/SSMP/Animation/Effects/Tools/FracturedMask.cs
+++ b/SSMP/Animation/Effects/Tools/FracturedMask.cs
@@ -5,6 +5,9 @@
namespace SSMP.Animation.Effects.Tools;
+///
+/// Class for the tool effect of Fractured Mask (extra health point).
+///
internal class FracturedMask : BaseTool {
///
public override byte[]? GetEffectInfo() {
@@ -23,7 +26,11 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
// Spawn in the shatter particles
var localMaskShatter = fsm.GetFirstAction("Instantiate Effect");
- var mask = EffectUtils.SpawnGlobalPoolObject(localMaskShatter.gameObject.Value, playerObject.transform, 5);
+ var mask = EffectUtils.SpawnGlobalPoolObject(
+ localMaskShatter.gameObject.Value,
+ playerObject.transform,
+ 5
+ );
mask?.DestroyComponent();
}
diff --git a/SSMP/Animation/Effects/Tools/MagmaBell.cs b/SSMP/Animation/Effects/Tools/MagmaBell.cs
index 4f768952..7f51780d 100644
--- a/SSMP/Animation/Effects/Tools/MagmaBell.cs
+++ b/SSMP/Animation/Effects/Tools/MagmaBell.cs
@@ -6,6 +6,9 @@
namespace SSMP.Animation.Effects.Tools;
+///
+/// Class for the tool effect of Magma Bell (fire protection).
+///
internal class MagmaBell : BaseTool {
///
/// Name of the magma bell starting object name.
@@ -24,7 +27,7 @@ internal class MagmaBell : BaseTool {
///
public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
- // two parts, when hit and when recovering after some delay
+ // Two parts: 1. when hit and 2. when recovering after some delay
// Find existing effect
var effects = GetPlayerEffects(playerObject);
diff --git a/SSMP/Animation/Effects/Tools/MagnetiteDice.cs b/SSMP/Animation/Effects/Tools/MagnetiteDice.cs
index 66e55ddc..e6199a91 100644
--- a/SSMP/Animation/Effects/Tools/MagnetiteDice.cs
+++ b/SSMP/Animation/Effects/Tools/MagnetiteDice.cs
@@ -7,17 +7,20 @@
namespace SSMP.Animation.Effects.Tools;
+///
+/// Class for the tool effect of Magnetite Dice (chance to negate damage).
+///
internal class MagnetiteDice : BaseTool {
///
/// Name of the magnetite dice effect object.
///
-
private const string DiceName = "dice_shield_effect";
///
public override byte[]? GetEffectInfo() {
return null;
}
+
///
public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
// Get existing effect
@@ -77,5 +80,4 @@ public static void Unhook() {
prefab.DestroyComponent();
}
-
}
diff --git a/SSMP/Animation/Effects/Tools/SawtoothCirclet.cs b/SSMP/Animation/Effects/Tools/SawtoothCirclet.cs
index 74131ad4..01157ec9 100644
--- a/SSMP/Animation/Effects/Tools/SawtoothCirclet.cs
+++ b/SSMP/Animation/Effects/Tools/SawtoothCirclet.cs
@@ -1,41 +1,32 @@
-using System;
-using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
-using System.Text;
using HutongGames.PlayMaker.Actions;
using SSMP.Game.Settings;
-using SSMP.Internals;
using SSMP.Util;
using UnityEngine;
namespace SSMP.Animation.Effects.Tools;
-internal class SawtoothCirclet : BaseTool {
+///
+/// Class for the tool effect of Sawtooth Circlet (damage when double jumping or gliding).
+/// This is a static class because it is only statically called from other effects (Faydown Cloak and Drifter's Cloak).
+///
+internal static class SawtoothCirclet {
///
- /// The name of the sawtooth circlet object.
+ /// The name of the Sawtooth Circlet object.
///
private const string SpikedCircletName = "Tool_brolly_spike";
///
- /// Cached reference to the local sawtooth circlet object.
+ /// Cached reference to the local Sawtooth Circlet object.
///
private static GameObject? _localCirclet;
- ///
- public override byte[]? GetEffectInfo() {
- return null;
- }
-
- ///
- public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
- PlayCirclet(playerObject, ShouldDoDamage && ServerSettings.IsPvpEnabled, ServerSettings);
- }
-
///
- /// Plays the sawtooth circlet.
+ /// Plays the Sawtooth Circlet animation.
///
/// The player using the circlet.
/// If the circlet should do damage.
+ /// The server settings for retrieving the damage it should deal.
public static void PlayCirclet(GameObject playerObject, bool doDamage, ServerSettings serverSettings) {
// Get the circlet
if (!TryGetCirclet(playerObject, out var circlet)) {
@@ -51,8 +42,8 @@ public static void PlayCirclet(GameObject playerObject, bool doDamage, ServerSet
var damagerLeft = damagerParent?.FindGameObjectInChildren("Damager L");
var damage = serverSettings.SawtoothCircletDamage;
- if (damagerRight != null) SetDamageHeroState(damagerRight, doDamage, damage);
- if (damagerLeft != null) SetDamageHeroState(damagerLeft, doDamage, damage);
+ if (damagerRight != null) DamageAnimationEffect.SetDamageHeroState(damagerRight, doDamage, damage);
+ if (damagerLeft != null) DamageAnimationEffect.SetDamageHeroState(damagerLeft, doDamage, damage);
// Refresh the circlet
circlet.SetActive(false);
@@ -68,11 +59,11 @@ public static void PlayCirclet(GameObject playerObject, bool doDamage, ServerSet
}
///
- /// Attempts to find or create the sawtooth circlet object.
+ /// Attempts to find or create the Sawtooth Circlet object.
///
/// The player using the circlet.
/// The circlet, if found.
- /// true if the circlet was found.
+ /// True if the circlet was found, otherwise false.
private static bool TryGetCirclet(GameObject playerObject, [MaybeNullWhen(false)] out GameObject circlet) {
// Find or create effects
var effects = playerObject.FindGameObjectInChildren("Effects");
diff --git a/SSMP/Api/Server/IServerSettings.cs b/SSMP/Api/Server/IServerSettings.cs
index 28d8c361..fab4e5bb 100644
--- a/SSMP/Api/Server/IServerSettings.cs
+++ b/SSMP/Api/Server/IServerSettings.cs
@@ -105,4 +105,14 @@ public interface IServerSettings {
/// The number of masks of damage that the upgraded Claw Mirror deals.
///
byte ClawMirrorUpgradedDamage { get; }
+
+ ///
+ /// The number of masks of damage that Sawtooth Circlet deals.
+ ///
+ byte SawtoothCircletDamage { get; }
+
+ ///
+ /// The number of masks of damage that a cloud from the poisoned Flea Brew deals.
+ ///
+ byte PoisonBrewDamage { get; }
}
diff --git a/SSMP/Util/EffectOwnerComponent.cs b/SSMP/Util/EffectOwnerComponent.cs
index 16636b0b..36a6c928 100644
--- a/SSMP/Util/EffectOwnerComponent.cs
+++ b/SSMP/Util/EffectOwnerComponent.cs
@@ -3,11 +3,11 @@
namespace SSMP.Util;
///
-/// Associates an effect object with the player object it belongs to
+/// Associates an effect object with the player object it belongs to.
///
public class EffectOwnerComponent : MonoBehaviour {
///
- /// The owner of the effect
+ /// The owner of the effect.
///
public GameObject? Owner;
}
From ecb50b38b17ff1cbb93b417f66e5c3d4c27816ea Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Tue, 12 May 2026 17:21:56 -0400
Subject: [PATCH 40/41] Stop certain effects when benching
---
SSMP/Animation/AnimationClip.cs | 3 +-
SSMP/Animation/AnimationManager.cs | 27 ++++++++++
SSMP/Animation/Effects/Bench.cs | 20 ++++++++
.../Animation/Effects/SilkSkills/PaleNails.cs | 33 +++++++++++--
SSMP/Animation/Effects/Tools/FleaBrew.cs | 49 +++++++++++++++----
SSMP/Animation/Effects/Tools/MagmaBell.cs | 2 +-
6 files changed, 119 insertions(+), 15 deletions(-)
create mode 100644 SSMP/Animation/Effects/Bench.cs
diff --git a/SSMP/Animation/AnimationClip.cs b/SSMP/Animation/AnimationClip.cs
index caf9239e..724cb698 100644
--- a/SSMP/Animation/AnimationClip.cs
+++ b/SSMP/Animation/AnimationClip.cs
@@ -771,5 +771,6 @@ internal enum AnimationClip {
MagnetiteDice,
FleaBrew,
FracturedMask,
- MagmaBell
+ MagmaBell,
+ Bench
}
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index bb1b969e..cae45e67 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -623,6 +623,8 @@ internal class AnimationManager {
{ "Wound Double Strike", AnimationClip.WoundDoubleStrike },
{ "Wound Zap", AnimationClip.WoundZap },
+ { "Bench", AnimationClip.Bench },
+
{ "Witch Tentacles!", AnimationClip.WitchTentacles },
{ "Shaman Cancel", AnimationClip.ShamanCancel },
{ "Bind Fail Burst", AnimationClip.BindInterrupt },
@@ -687,6 +689,7 @@ internal class AnimationManager {
{ AnimationClip.WitchTentacles, BindBurst.Instance },
{ AnimationClip.ShamanCancel, new Bind { BindState = Bind.State.ShamanCancel } },
{ AnimationClip.BindInterrupt, BindInterrupt.Instance },
+ { AnimationClip.Bench, new Bench() },
// Silk Skills
{ AnimationClip.AirSphereRefresh, new ThreadStorm() },
@@ -860,6 +863,12 @@ public void DeregisterHooks() {
EventHooks.HeroControllerDie -= OnDeath;
EventHooks.UseLavaBell -= OnMagmaBell;
+
+ // Remove listener for benching
+ var eventRegister = HeroController.SilentInstance?.gameObject.GetComponents().FirstOrDefault(r => r.SubscribedEvent == "BENCHREST START");
+ if (eventRegister) {
+ eventRegister.ReceivedEvent -= OnBench;
+ }
// On.HeroAnimationController.Play -= HeroAnimationControllerOnPlay;
// On.HeroAnimationController.PlayFromFrame -= HeroAnimationControllerOnPlayFromFrame;
@@ -1141,6 +1150,12 @@ private void CreateHeroHooks(HeroController hc) {
} else {
Logger.Warn("Unable to find Bind FSM to hook.");
}
+
+ // Add listener for benching
+ var eventRegister = hc.gameObject.GetComponents().FirstOrDefault(r => r.SubscribedEvent == "BENCHREST START");
+ if (eventRegister) {
+ eventRegister.ReceivedEvent += OnBench;
+ }
}
///
@@ -1510,6 +1525,18 @@ private void OnMagmaBell() {
_netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.MagmaBell);
}
+ ///
+ /// Hook for resting on a bench.
+ ///
+ private void OnBench() {
+ // If we are not connected, there is nothing to send to
+ if (!_netClient.IsConnected) {
+ return;
+ }
+
+ _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.Bench);
+ }
+
// ///
// /// Callback method on the HeroAnimationController#Play method.
// ///
diff --git a/SSMP/Animation/Effects/Bench.cs b/SSMP/Animation/Effects/Bench.cs
new file mode 100644
index 00000000..e59933c2
--- /dev/null
+++ b/SSMP/Animation/Effects/Bench.cs
@@ -0,0 +1,20 @@
+using SSMP.Animation.Effects.SilkSkills;
+using SSMP.Animation.Effects.Tools;
+using SSMP.Internals;
+using UnityEngine;
+
+namespace SSMP.Animation.Effects;
+
+internal class Bench : AnimationEffect {
+ ///
+ public override byte[]? GetEffectInfo() {
+ return null;
+ }
+
+ ///
+ public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
+ // Stop all tool/silk skill effects
+ PaleNails.DespawnAllPlayerNails(playerObject);
+ FleaBrew.StopBrew(playerObject);
+ }
+}
diff --git a/SSMP/Animation/Effects/SilkSkills/PaleNails.cs b/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
index e70ef343..59287007 100644
--- a/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
+++ b/SSMP/Animation/Effects/SilkSkills/PaleNails.cs
@@ -145,6 +145,29 @@ private static void PlayNailFireUnguided(GameObject[] nails, bool isVolt, int pl
PlayerNails[playerId] = [];
}
+ ///
+ /// De-spawns all sets of nails belonging to a given player.
+ ///
+ /// The player object with nails to de-spawn.
+ public static void DespawnAllPlayerNails(GameObject playerObject) {
+ // Get existing nails
+ var id = playerObject.GetInstanceID();
+ if (!PlayerNails.TryGetValue(id, out var nails) || nails.Length == 0) {
+ return;
+ }
+
+ // Send bench event to all nails
+ foreach (var nail in nails) {
+ if (nail == null) continue;
+
+ var fsm = nail.LocateMyFSM("Control");
+ fsm.Fsm.Event("BENCHREST");
+ }
+
+ // Clear nails
+ PlayerNails[id] = [];
+ }
+
///
/// Plays the nail summoning antic effect.
///
@@ -370,14 +393,16 @@ private static void FixFsmForUse(PlayMakerFSM fsm, GameObject playerObject, bool
// Remove transitions
var left = fsm.GetState(followLeftName);
left.Transitions = [
- left.Transitions[0],
- left.Transitions[5]
+ left.Transitions[0], // TURN
+ left.Transitions[3], // BENCHREST
+ left.Transitions[5] // FOLLOW BUDDY
];
var right = fsm.GetState(followRightName);
right.Transitions = [
- right.Transitions[0],
- right.Transitions[5],
+ right.Transitions[0], // TURN
+ right.Transitions[3], // BENCHREST
+ right.Transitions[5] // FOLLOW BUDDY
];
// Set volt state
diff --git a/SSMP/Animation/Effects/Tools/FleaBrew.cs b/SSMP/Animation/Effects/Tools/FleaBrew.cs
index 5e6838c2..131cbf1c 100644
--- a/SSMP/Animation/Effects/Tools/FleaBrew.cs
+++ b/SSMP/Animation/Effects/Tools/FleaBrew.cs
@@ -11,6 +11,8 @@ namespace SSMP.Animation.Effects.Tools;
/// Class for the tool effect of Flea Brew (attack buff).
///
internal class FleaBrew : BaseTool {
+ private const string ParticlesName = "Flea Brew Particles";
+
///
/// Cached reference to a modified version of the poisoned flea brew trail.
///
@@ -52,11 +54,13 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
var particles = EffectUtils.SpawnGlobalPoolObject(
localPrefab.gameObject,
playerObject.transform,
- duration,
+ duration,
true
);
if (particles == null) return;
+ particles.name = ParticlesName;
+
// Set up poison clouds
if (isPoison && ShouldDoDamage && ServerSettings.IsPvpEnabled) {
SetPoisonTrail(particles);
@@ -91,25 +95,52 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
);
BrewFlashes[id] = flashHandle;
- MonoBehaviourUtil.Instance.StartCoroutine(StopBrewFlash(playerObject, flashHandle));
+ MonoBehaviourUtil.Instance.StartCoroutine(StopBrewFlashAfterDelay(playerObject, flashHandle));
+
+ }
+
+ ///
+ /// Stops the Flea Brew flashing and particles.
+ ///
+ /// The player object with the Flea Brew animation.
+ public static void StopBrew(GameObject playerObject) {
+ var id = playerObject.GetInstanceID();
+ if (!BrewFlashes.TryGetValue(id, out var handle)) {
+ return;
+ }
+
+ StopBrew(playerObject, handle);
+ }
+
+ ///
+ /// Stops the Flea Brew flashing and particles.
+ ///
+ /// The player object with the Flea Brew animation.
+ /// The current sprite flash handle.
+ private static void StopBrew(GameObject playerObject, SpriteFlash.FlashHandle handle) {
+ // Stop sprite flash
+ if (playerObject.TryGetComponent(out var flash)) {
+ flash.CancelRepeatingFlash(handle);
+ }
+ // Stop particles
+ var particles = playerObject.FindGameObjectInChildren(ParticlesName);
+ if (particles) {
+ Object.Destroy(particles);
+ }
}
///
- /// Stops the Flea Brew sprite flash.
+ /// Stops the Flea Brew sprite flash after a delay.
///
/// The player that used the tool.
/// The flash's handle.
- private static IEnumerator StopBrewFlash(GameObject playerObject, SpriteFlash.FlashHandle handle) {
+ private static IEnumerator StopBrewFlashAfterDelay(GameObject playerObject, SpriteFlash.FlashHandle handle) {
// Wait for effect to end
yield return new WaitForSeconds(HeroController.instance.QUICKENING_DURATION);
// Cancel flash
- if (!playerObject.TryGetComponent(out var flash)) {
- yield break;
- }
-
- flash.CancelRepeatingFlash(handle);
+ StopBrew(playerObject, handle);
}
///
diff --git a/SSMP/Animation/Effects/Tools/MagmaBell.cs b/SSMP/Animation/Effects/Tools/MagmaBell.cs
index 7f51780d..d5d88458 100644
--- a/SSMP/Animation/Effects/Tools/MagmaBell.cs
+++ b/SSMP/Animation/Effects/Tools/MagmaBell.cs
@@ -60,7 +60,7 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
/// The player to use the animation on.
private static IEnumerator PlayRecharge(GameObject playerObject) {
// Wait for bell to recharge
- yield return new WaitForSeconds(Gameplay.LavaBellCooldownTime - 1);
+ yield return new WaitForSeconds(Gameplay.LavaBellCooldownTime - 1.25f);
// Player has exited the scene, don't play.
if (!playerObject.activeInHierarchy) yield break;
From 11e5679569db2c324432f1ca4cf7b174126fa6f3 Mon Sep 17 00:00:00 2001
From: BobbyTheCatfish <46359040+BobbyTheCatfish@users.noreply.github.com>
Date: Thu, 28 May 2026 22:33:03 -0400
Subject: [PATCH 41/41] split bell recharge
---
SSMP/Animation/AnimationManager.cs | 15 +++++++-
SSMP/Animation/Effects/Tools/MagmaBell.cs | 46 +++++++++++++++++------
2 files changed, 48 insertions(+), 13 deletions(-)
diff --git a/SSMP/Animation/AnimationManager.cs b/SSMP/Animation/AnimationManager.cs
index cae45e67..049d0b17 100644
--- a/SSMP/Animation/AnimationManager.cs
+++ b/SSMP/Animation/AnimationManager.cs
@@ -1460,6 +1460,7 @@ private void OnPaleNailFire(PlayMakerFSM fsm) {
///
private void CreateToolHooks() {
MagnetiteDice.Hook(OnDiceEnable);
+ MagmaBell.HookRecharge(OnMagmaBellRecharge);
var toolFsm = HeroController.instance.toolsFSM;
var brewBurst = toolFsm.GetState("Flea Brew Burst");
@@ -1522,7 +1523,19 @@ private void OnMagmaBell() {
return;
}
- _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.MagmaBell);
+ _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.MagmaBell, 0, [0]);
+ }
+
+ ///
+ /// Hook for the magma bell being triggered.
+ ///
+ private void OnMagmaBellRecharge() {
+ // If we are not connected, there is nothing to send to
+ if (!_netClient.IsConnected) {
+ return;
+ }
+
+ _netClient.UpdateManager.UpdatePlayerAnimation(AnimationClip.MagmaBell, 0, [1]);
}
///
diff --git a/SSMP/Animation/Effects/Tools/MagmaBell.cs b/SSMP/Animation/Effects/Tools/MagmaBell.cs
index d5d88458..2e712ed4 100644
--- a/SSMP/Animation/Effects/Tools/MagmaBell.cs
+++ b/SSMP/Animation/Effects/Tools/MagmaBell.cs
@@ -1,5 +1,5 @@
+using System;
using System.Collections;
-using GlobalSettings;
using SSMP.Internals;
using SSMP.Util;
using UnityEngine;
@@ -20,6 +20,8 @@ internal class MagmaBell : BaseTool {
///
private const string MagmaBellRechargeName = "Magma Bell Recharge";
+ public static readonly MagmaBell Instance = new();
+
///
public override byte[]? GetEffectInfo() {
return null;
@@ -28,6 +30,11 @@ internal class MagmaBell : BaseTool {
///
public override void Play(GameObject playerObject, CrestType crestType, byte[]? effectInfo) {
// Two parts: 1. when hit and 2. when recovering after some delay
+ if (effectInfo is [1]) {
+ PlayRecharge(playerObject);
+ return;
+ }
+
// Find existing effect
var effects = GetPlayerEffects(playerObject);
@@ -49,22 +56,13 @@ public override void Play(GameObject playerObject, CrestType crestType, byte[]?
// Toggle effect
magmaStart.SetActive(false);
magmaStart.SetActive(true);
-
- // Start the recharge effect
- MonoBehaviourUtil.Instance.StartCoroutine(PlayRecharge(playerObject));
}
///
/// Plays the recharge animation.
///
/// The player to use the animation on.
- private static IEnumerator PlayRecharge(GameObject playerObject) {
- // Wait for bell to recharge
- yield return new WaitForSeconds(Gameplay.LavaBellCooldownTime - 1.25f);
-
- // Player has exited the scene, don't play.
- if (!playerObject.activeInHierarchy) yield break;
-
+ private static void PlayRecharge(GameObject playerObject) {
// Find existing effect
var effects = GetPlayerEffects(playerObject);
var magmaRecharge = effects.FindGameObjectInChildren(MagmaBellRechargeName);
@@ -74,7 +72,7 @@ private static IEnumerator PlayRecharge(GameObject playerObject) {
var prefab = HeroController.instance.lavaBellRechargeEffectPrefab;
magmaRecharge = EffectUtils.SpawnGlobalPoolObject(prefab, effects.transform, 0, true);
- if (!magmaRecharge) yield break;
+ if (!magmaRecharge) return;
magmaRecharge.transform.localPosition = Vector3.zero;
magmaRecharge.name = MagmaBellRechargeName;
@@ -85,4 +83,28 @@ private static IEnumerator PlayRecharge(GameObject playerObject) {
magmaRecharge.SetActive(false);
magmaRecharge.SetActive(true);
}
+
+ ///
+ /// Adds a hook for when the bell is recharged.
+ ///
+ /// The hook to run.
+ public static void HookRecharge(Action onTrigger) {
+ // Create coroutine since we have to wait for the prefab to be set
+ static IEnumerator DoHook(Action onTrigger) {
+ // Wait for prefab to be spawned
+ yield return null;
+
+ var prefab = HeroController.instance.spawnedLavaBellRechargeEffect;
+ if (prefab == null) {
+ yield break;
+ }
+
+ // Add enable hook
+ var hook = prefab.AddComponent();
+
+ hook.Enabled += onTrigger;
+ }
+
+ MonoBehaviourUtil.Instance.StartCoroutine(DoHook(onTrigger));
+ }
}