using System; using Unity.BossRoom.Gameplay.GameplayObjects.Character; using Unity.Netcode; using UnityEngine; using UnityEngine.Assertions; namespace Unity.BossRoom.Gameplay.Actions { /// /// A defensive action where the character becomes resistant to damage. /// /// /// The player can hold down the button for this ability to "charge it up" and make it more effective. Once it's been /// charging for Description.ExecTimeSeconds, it reaches maximum charge. If the player is attacked by an enemy, that /// also immediately stops the charge-up. /// /// Once the charge-up stops (for any reason), the Action lasts for Description.EffectTimeSeconds before elapsing. During /// this time, all incoming damage is reduced by a percentage from 50% to 100%, depending on how "charged up" it was. /// /// When the Action is fully charged up, it provides a special additional benefit: if the boss tries to trample this /// character, the boss becomes Stunned. /// [CreateAssetMenu(menuName = "BossRoom/Actions/Charged Shield Action")] public partial class ChargedShieldAction : Action { /// /// Set once we've stopped charging up, for any reason: /// - the player has let go of the button, /// - we were attacked, /// - or the maximum charge was reached. /// private float m_StoppedChargingUpTime = 0; public override bool OnStart(ServerCharacter serverCharacter) { if (m_Data.TargetIds != null && m_Data.TargetIds.Length > 0) { NetworkObject initialTarget = NetworkManager.Singleton.SpawnManager.SpawnedObjects[m_Data.TargetIds[0]]; if (initialTarget) { // face our target, if we had one serverCharacter.physicsWrapper.Transform.LookAt(initialTarget.transform.position); } } // because this action can be visually started and stopped as often and as quickly as the player wants, it's possible // for several copies of this action to be playing at once. This can lead to situations where several // dying versions of the action raise the end-trigger, but the animator only lowers it once, leaving the trigger // in a raised state. So we'll make sure that our end-trigger isn't raised yet. (Generally a good idea anyway.) serverCharacter.serverAnimationHandler.NetworkAnimator.ResetTrigger(Config.Anim2); // raise the start trigger to start the animation loop! serverCharacter.serverAnimationHandler.NetworkAnimator.SetTrigger(Config.Anim); serverCharacter.clientCharacter.ClientPlayActionRpc(Data); return true; } public override void Reset() { base.Reset(); m_ChargeGraphics = null; m_ShieldGraphics = null; m_StoppedChargingUpTime = 0; } private bool IsChargingUp() { return m_StoppedChargingUpTime == 0; } public override bool OnUpdate(ServerCharacter clientCharacter) { if (m_StoppedChargingUpTime == 0) { // we haven't explicitly stopped charging up... but if we've reached max charge, that implicitly stops us if (TimeRunning >= Config.ExecTimeSeconds) { StopChargingUp(clientCharacter); } } // we stop once the charge-up has ended and our effect duration has elapsed return m_StoppedChargingUpTime == 0 || Time.time < (m_StoppedChargingUpTime + Config.EffectDurationSeconds); } public override bool ShouldBecomeNonBlocking() { return m_StoppedChargingUpTime != 0; } private float GetPercentChargedUp() { return ActionUtils.GetPercentChargedUp(m_StoppedChargingUpTime, TimeRunning, TimeStarted, Config.ExecTimeSeconds); } public override void BuffValue(BuffableValue buffType, ref float buffedValue) { if (buffType == BuffableValue.PercentDamageReceived) { float percentChargedUp = GetPercentChargedUp(); // the amount of damage reduction starts at 50% (for not-charged-up), then slowly increases to 100% depending on how charged-up we got float percentDamageReduction = 0.5f + ((percentChargedUp * percentChargedUp) / 2); // Now that we know how much damage to reduce it by, we need to set buffedValue to the inverse (because // it's looking for how much damage to DO, not how much to REDUCE BY). Also note how we don't just SET // buffedValue... we multiply our buff in with the current value. This lets our Action "stack" // with any other Actions that also alter this variable.) buffedValue *= 1 - percentDamageReduction; } else if (buffType == BuffableValue.ChanceToStunTramplers) { // if we are at "full charge", we stun enemies that try to trample us! if (GetPercentChargedUp() >= 1) { buffedValue = 1; } } } public override void OnGameplayActivity(ServerCharacter serverCharacter, GameplayActivity activityType) { // for this particular type of Action, being attacked immediately causes you to stop charging up if (activityType == GameplayActivity.AttackedByEnemy || activityType == GameplayActivity.StoppedChargingUp) { StopChargingUp(serverCharacter); } } public override void Cancel(ServerCharacter serverCharacter) { StopChargingUp(serverCharacter); // if stepped into invincibility, decrement invincibility counter if (Mathf.Approximately(GetPercentChargedUp(), 1f)) { serverCharacter.serverAnimationHandler.NetworkAnimator.Animator.SetInteger(Config.OtherAnimatorVariable, serverCharacter.serverAnimationHandler.NetworkAnimator.Animator.GetInteger(Config.OtherAnimatorVariable) - 1); } } private void StopChargingUp(ServerCharacter parent) { if (IsChargingUp()) { m_StoppedChargingUpTime = Time.time; parent.clientCharacter.ClientStopChargingUpRpc(GetPercentChargedUp()); parent.serverAnimationHandler.NetworkAnimator.SetTrigger(Config.Anim2); parent.serverAnimationHandler.NetworkAnimator.ResetTrigger(Config.Anim); //tell the animator controller to enter "invincibility mode" (where we don't flinch from damage) if (Mathf.Approximately(GetPercentChargedUp(), 1f)) { // increment our "invincibility counter". We use an integer count instead of a boolean because the player // can restart their shield before the first one has ended, thereby getting two stacks of invincibility. // So each active copy of the charge-up increments the invincibility counter, and the animator controller // knows anything greater than zero means we shouldn't show hit-reacts. parent.serverAnimationHandler.NetworkAnimator.Animator.SetInteger(Config.OtherAnimatorVariable, parent.serverAnimationHandler.NetworkAnimator.Animator.GetInteger(Config.OtherAnimatorVariable) + 1); } } } public override bool OnStartClient(ClientCharacter clientCharacter) { Assert.IsTrue(Config.Spawns.Length == 2, $"Found {Config.Spawns.Length} spawns for action {name}. Should be exactly 2: a charge-up particle and a fully-charged particle"); base.OnStartClient(clientCharacter); m_ChargeGraphics = InstantiateSpecialFXGraphic(Config.Spawns[0], clientCharacter.transform, true); return true; } } }