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)); + } }