diff --git a/Assets/BossRoom/GameData/Action/Imp/ImpBaseAttack.asset b/Assets/BossRoom/GameData/Action/Imp/ImpBaseAttack.asset index da63bc034..f6450fbec 100644 --- a/Assets/BossRoom/GameData/Action/Imp/ImpBaseAttack.asset +++ b/Assets/BossRoom/GameData/Action/Imp/ImpBaseAttack.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ba7dbcb8c32a01bdacdf8a5591b7a17bf0d68547e61b0376533608123a31b26 +oid sha256:f1181a174a045df822dd160e56a2603edbd3148f9f8f29c202e37aa76afd11a2 size 864 diff --git a/Assets/BossRoom/Models/CharacterSetController_Tank.overrideController b/Assets/BossRoom/Models/CharacterSetController_Tank.overrideController index 4a8195ae1..30d8e82a4 100644 --- a/Assets/BossRoom/Models/CharacterSetController_Tank.overrideController +++ b/Assets/BossRoom/Models/CharacterSetController_Tank.overrideController @@ -21,3 +21,5 @@ AnimatorOverrideController: m_OverrideClip: {fileID: -6239216113759591374, guid: 2115c4661f55eff45a5a0f91fc0a12f0, type: 3} - m_OriginalClip: {fileID: -5612658629409835226, guid: 2115c4661f55eff45a5a0f91fc0a12f0, type: 3} m_OverrideClip: {fileID: -5612658629409835226, guid: 2115c4661f55eff45a5a0f91fc0a12f0, type: 3} + - m_OriginalClip: {fileID: -4428684883894617094, guid: 2115c4661f55eff45a5a0f91fc0a12f0, type: 3} + m_OverrideClip: {fileID: -3419257869308726280, guid: 2115c4661f55eff45a5a0f91fc0a12f0, type: 3} diff --git a/Assets/BossRoom/Scripts/Client/Game/Action/ActionFX.cs b/Assets/BossRoom/Scripts/Client/Game/Action/ActionFX.cs index 2bc8f15ad..a4218f680 100644 --- a/Assets/BossRoom/Scripts/Client/Game/Action/ActionFX.cs +++ b/Assets/BossRoom/Scripts/Client/Game/Action/ActionFX.cs @@ -13,6 +13,11 @@ public abstract class ActionFX : ActionBase /// public const string k_DefaultHitReact = "HitReact1"; + /// + /// True if this actionFX began running immediately, prior to getting a confirmation from the server. + /// + public bool Anticipated { get; protected set; } + public ActionFX(ref ActionRequestData data, ClientCharacterVisualization parent) : base(ref data) { m_Parent = parent; @@ -21,8 +26,16 @@ public ActionFX(ref ActionRequestData data, ClientCharacterVisualization parent) /// /// Starts the ActionFX. Derived classes may return false if they wish to end immediately without their Update being called. /// + /// + /// Derived class should be sure to call base.Start() in their implementation, but note that this resets "Anticipated" to false. + /// /// true to play, false to be immediately cleaned up. - public abstract bool Start(); + public virtual bool Start() + { + Anticipated = false; //once you start for real you are no longer an anticipated action. + TimeStarted = UnityEngine.Time.time; + return true; + } public abstract bool Update(); @@ -54,17 +67,65 @@ public static ActionFX MakeActionFX(ref ActionRequestData data, ClientCharacterV case ActionLogic.RangedFXTargeted: return new FXProjectileTargetedActionFX(ref data, parent); case ActionLogic.Trample: return new TrampleActionFX(ref data, parent); case ActionLogic.AoE: return new AoeActionFX(ref data, parent); - case ActionLogic.Stunned: return new AnimationOnlyActionFX(ref data, parent); case ActionLogic.Target: return new TargetActionFX(ref data, parent); + case ActionLogic.ChargedShield: case ActionLogic.ChargedLaunchProjectile: return new ChargedActionFX(ref data, parent); + case ActionLogic.StealthMode: return new StealthModeActionFX(ref data, parent); + + case ActionLogic.Stunned: + case ActionLogic.LaunchProjectile: + case ActionLogic.Revive: + case ActionLogic.Emote: return new AnimationOnlyActionFX(ref data, parent); + default: throw new System.NotImplementedException(); } } + /// + /// Should this ActionFX be created anticipatively on the owning client? + /// + /// The ActionVisualization that would be playing this ActionFX. + /// The request being sent to the server + /// If true ActionVisualization should pre-emptively create the ActionFX on the owning client, before hearing back from the server. + public static bool ShouldAnticipate(ActionVisualization parent, ref ActionRequestData data) + { + if( !parent.Parent.CanPerformActions ) { return false; } + + var actionDescription = GameDataSource.Instance.ActionDataByType[data.ActionTypeEnum]; + + //for actions with ShouldClose set, we check our range locally. If we are out of range, we shouldn't anticipate, as we will + //need to execute a ChaseAction (synthesized on the server) prior to actually playing the skill. + bool isTargetEligible = true; + if( data.ShouldClose == true ) + { + ulong targetId = (data.TargetIds != null && data.TargetIds.Length > 0) ? data.TargetIds[0] : 0; + if( MLAPI.Spawning.NetworkSpawnManager.SpawnedObjects.TryGetValue(targetId, out MLAPI.NetworkObject networkObject ) ) + { + float rangeSquared = actionDescription.Range * actionDescription.Range; + isTargetEligible = (networkObject.transform.position - parent.Parent.transform.position).sqrMagnitude < rangeSquared; + } + } + + //at present all Actionts anticipate except for the Target action, which runs a single instance on the client and is + //responsible for action anticipation on its own. + return isTargetEligible && actionDescription.Logic != ActionLogic.Target; + } + public virtual void OnAnimEvent(string id) { } public virtual void OnStoppedChargingUp() { } + + /// + /// Called when the action is being "anticipated" on the client. For example, if you are the owner of a tank and you swing your hammer, + /// you get this call immediately on the client, before the server round-trip. + /// Overriders should always call the base class in their implementation! + /// + public virtual void AnticipateAction() + { + Anticipated = true; + TimeStarted = UnityEngine.Time.time; + } } } diff --git a/Assets/BossRoom/Scripts/Client/Game/Action/ActionVisualization.cs b/Assets/BossRoom/Scripts/Client/Game/Action/ActionVisualization.cs index 602ea6138..dabf0fcbc 100644 --- a/Assets/BossRoom/Scripts/Client/Game/Action/ActionVisualization.cs +++ b/Assets/BossRoom/Scripts/Client/Game/Action/ActionVisualization.cs @@ -9,14 +9,19 @@ namespace BossRoom.Visual /// public class ActionVisualization { - private List m_PlayingActions; + private List m_PlayingActions = new List(); + + /// + /// Don't let anticipated actionFXs persist longer than this. This is a safeguard against scenarios + /// where we never get a confirmed action for an action we anticipated. + /// + private const float k_AnticipationTimeoutSeconds = 1; public ClientCharacterVisualization Parent { get; private set; } public ActionVisualization(ClientCharacterVisualization parent) { Parent = parent; - m_PlayingActions = new List(); } public void Update() @@ -27,15 +32,24 @@ public void Update() var action = m_PlayingActions[i]; bool keepGoing = action.Update(); bool expirable = action.Description.DurationSeconds > 0f; //non-positive value is a sentinel indicating the duration is indefinite. - bool timeExpired = expirable && (Time.time - action.TimeStarted) >= action.Description.DurationSeconds; - if (!keepGoing || timeExpired) + bool timeExpired = expirable && action.TimeRunning >= action.Description.DurationSeconds; + bool timedOut = action.Anticipated && action.TimeRunning >= k_AnticipationTimeoutSeconds; + if (!keepGoing || timeExpired || timedOut) { - action.End(); + if (timedOut) { action.Cancel(); } //an anticipated action that timed out shouldn't get its End called. It is canceled instead. + else { action.End(); } + m_PlayingActions.RemoveAt(i); } } } + //helper wrapper for a FindIndex call on m_PlayingActions. + private int FindAction(ActionType action, bool anticipatedOnly ) + { + return m_PlayingActions.FindIndex(a => a.Description.ActionTypeEnum == action && (!anticipatedOnly || a.Anticipated)); + } + public void OnAnimEvent(string id) { foreach (var actionFX in m_PlayingActions) @@ -52,22 +66,55 @@ public void OnStoppedChargingUp() } } - public void PlayAction(ref ActionRequestData data) + /// + /// Called on the client that owns the Character when the player triggers an action. This allows actions to immediately start playing feedback. + /// + /// + /// + /// What is Action Anticipation and what problem does it solve? In short, it lets Actions run logic the moment the input event that triggers them + /// is detected on the local client. The purpose of this is to help mask latency. Because this demo is server authoritative, the default behavior is + /// to only see feedback for your input after a server-client roundtrip. Somewhere over 200ms of round-trip latency, this starts to feel oppressively sluggish. + /// To combat this, you can play visual effects immediately. For example, MeleeActionFX plays both its weapon swing and applies a hit react to the target, + /// without waiting to hear from the server. This can lead to discrepancies when the server doesn't think the target was hit, but on the net, will feel + /// more responsive. + /// + /// An important concept of Action Anticipation is that it is opportunistic--it doesn't make any strong guarantees. You don't get an anticipated + /// action animation if you are already animating in some way, as one example. Another complexity is that you don't know if the server will actually + /// let you play all the actions that you've requested--some may get thrown away, e.g. because you have too many actions in your queue. What this means + /// is that Anticipated Actions (actions that have been constructed but not started) won't match up perfectly with actual approved delivered actions from + /// the server. For that reason, it must always be fine to receive PlayAction and not have an anticipated action already started (this is true for playback + /// Characters belonging to the server and other characters anyway). It also means we need to handle the case where we created an Anticipated Action, but + /// never got a confirmation--actions like that need to eventually get discarded. + /// + /// Another important aspect of Anticipated Actions is that they are an "opt-in" system. You must call base.Start in your Start implementation, but other than + /// that, if you don't have a good way to implement an Anticipation for your action, you don't have to do anything. In this case, that action will play + /// "normally" (with visual feedback starting when the server's action broadcast reaches the client). Every action type will have its own particular set of + /// problems to solve to sell the anticipation effect. For example, in this demo, the mage base attack (FXProjectileTargetedActionFX) just plays the attack animation + /// anticipatively, but it could be revised to create and drive the mage bolt effect as well--leaving only damage to arrive in true server time. + /// + /// How to implement your own Anticipation logic: + /// 1. Isolate the visual feedback you want play anticipatively in a private helper method on your ActionFX, like "PlayAttackAnim". + /// 2. Override ActionFX.AnticipateAction. Be sure to call base.AnticipateAction, as well as play your visual logic (like PlayAttackAnim). + /// 3. In your Start method, be sure to call base.Start (note that this will reset the "Anticipated" field to false). + /// 4. In Start, check if the action was Anticipated. If NOT, then play call your PlayAttackAnim method. + /// + /// + /// The Action that is being requested. + public void AnticipateAction(ref ActionRequestData data) { - ActionDescription actionDesc = GameDataSource.Instance.ActionDataByType[data.ActionTypeEnum]; - - //Do Trivial Actions (actions that just require playing a single animation, and don't require any state tracking). - switch (actionDesc.Logic) + if (!Parent.IsAnimating && ActionFX.ShouldAnticipate(this, ref data)) { - case ActionLogic.LaunchProjectile: - case ActionLogic.Revive: - case ActionLogic.Emote: - Parent.OurAnimator.SetTrigger(actionDesc.Anim); - return; + var actionFX = ActionFX.MakeActionFX(ref data, Parent); + actionFX.AnticipateAction(); + m_PlayingActions.Add(actionFX); } + } - var actionFX = ActionFX.MakeActionFX(ref data, Parent); - actionFX.TimeStarted = Time.time; + public void PlayAction(ref ActionRequestData data) + { + var anticipatedActionIndex = FindAction(data.ActionTypeEnum, true); + + var actionFX = anticipatedActionIndex>=0 ? m_PlayingActions[anticipatedActionIndex] : ActionFX.MakeActionFX(ref data, Parent); if (actionFX.Start()) { m_PlayingActions.Add(actionFX); @@ -85,7 +132,7 @@ public void CancelAllActions() public void CancelAllActionsOfType(ActionType actionType) { - for (int i = m_PlayingActions.Count-1; i >=0; --i) + for (int i = m_PlayingActions.Count - 1; i >= 0; --i) { if (m_PlayingActions[i].Description.ActionTypeEnum == actionType) { @@ -100,7 +147,7 @@ public void CancelAllActionsOfType(ActionType actionType) /// public void CancelAll() { - foreach( var action in m_PlayingActions ) + foreach (var action in m_PlayingActions) { action.Cancel(); } diff --git a/Assets/BossRoom/Scripts/Client/Game/Action/AnimationOnlyActionFX.cs b/Assets/BossRoom/Scripts/Client/Game/Action/AnimationOnlyActionFX.cs index 5840262de..d25db4b7c 100644 --- a/Assets/BossRoom/Scripts/Client/Game/Action/AnimationOnlyActionFX.cs +++ b/Assets/BossRoom/Scripts/Client/Game/Action/AnimationOnlyActionFX.cs @@ -13,16 +13,32 @@ public AnimationOnlyActionFX(ref ActionRequestData data, ClientCharacterVisualiz public override bool Start() { - m_Parent.OurAnimator.SetTrigger(Description.Anim); + if( !Anticipated ) + { + PlayStartAnim(); + } + + base.Start(); return true; } + private void PlayStartAnim() + { + m_Parent.OurAnimator.SetTrigger(Description.Anim); + } + + public override void AnticipateAction() + { + base.AnticipateAction(); + PlayStartAnim(); + } + public override bool Update() { return ActionConclusion.Continue; } - public override void End() + public override void Cancel() { if (!string.IsNullOrEmpty(Description.Anim2)) { diff --git a/Assets/BossRoom/Scripts/Client/Game/Action/AoeActionFX.cs b/Assets/BossRoom/Scripts/Client/Game/Action/AoeActionFX.cs index 8e6f27ec4..d13eae627 100644 --- a/Assets/BossRoom/Scripts/Client/Game/Action/AoeActionFX.cs +++ b/Assets/BossRoom/Scripts/Client/Game/Action/AoeActionFX.cs @@ -11,6 +11,7 @@ public AoeActionFX(ref ActionRequestData data, ClientCharacterVisualization pare public override bool Start() { + base.Start(); m_Parent.OurAnimator.SetTrigger(Description.Anim); GameObject.Instantiate(Description.Spawns[0], m_Data.Position, Quaternion.identity); return ActionConclusion.Stop; diff --git a/Assets/BossRoom/Scripts/Client/Game/Action/ChargedActionFX.cs b/Assets/BossRoom/Scripts/Client/Game/Action/ChargedActionFX.cs index 96aeaf7cd..744c23628 100644 --- a/Assets/BossRoom/Scripts/Client/Game/Action/ChargedActionFX.cs +++ b/Assets/BossRoom/Scripts/Client/Game/Action/ChargedActionFX.cs @@ -27,6 +27,7 @@ public ChargedActionFX(ref ActionRequestData data, ClientCharacterVisualization public override bool Start() { + base.Start(); m_Parent.OurAnimator.SetTrigger(Description.Anim); if (Description.Spawns.Length > 0) diff --git a/Assets/BossRoom/Scripts/Client/Game/Action/FXProjectileTargetedActionFX.cs b/Assets/BossRoom/Scripts/Client/Game/Action/FXProjectileTargetedActionFX.cs index 8905a42fd..5867e151c 100644 --- a/Assets/BossRoom/Scripts/Client/Game/Action/FXProjectileTargetedActionFX.cs +++ b/Assets/BossRoom/Scripts/Client/Game/Action/FXProjectileTargetedActionFX.cs @@ -25,6 +25,8 @@ public FXProjectileTargetedActionFX(ref ActionRequestData data, ClientCharacterV public override bool Start() { + bool wasAnticipated = Anticipated; + base.Start(); m_Target = GetTarget(); if (HasTarget() && m_Target == null) { @@ -44,10 +46,19 @@ public override bool Start() m_Projectile = SpawnAndInitializeProjectile(); // animate shooting the projectile - m_Parent.OurAnimator.SetTrigger(Description.Anim); + if( !wasAnticipated ) + { + PlayFireAnim(); + } + return true; } + private void PlayFireAnim() + { + m_Parent.OurAnimator.SetTrigger(Description.Anim); + } + public override bool Update() { // we keep going until the projectile's duration ends @@ -128,5 +139,11 @@ private FXProjectile SpawnAndInitializeProjectile() projectile.Initialize(m_Parent.transform.position, m_Target?.transform, m_Data.Position, Description.ExecTimeSeconds, m_ProjectileDuration); return projectile; } + + public override void AnticipateAction() + { + base.AnticipateAction(); + PlayFireAnim(); + } } } diff --git a/Assets/BossRoom/Scripts/Client/Game/Action/MeleeActionFX.cs b/Assets/BossRoom/Scripts/Client/Game/Action/MeleeActionFX.cs index 259cee892..2c11cae1b 100644 --- a/Assets/BossRoom/Scripts/Client/Game/Action/MeleeActionFX.cs +++ b/Assets/BossRoom/Scripts/Client/Game/Action/MeleeActionFX.cs @@ -21,7 +21,12 @@ public MeleeActionFX(ref ActionRequestData data, ClientCharacterVisualization pa public override bool Start() { - m_Parent.OurAnimator.SetTrigger(Description.Anim); + if( !Anticipated) + { + PlayAnim(); + } + + base.Start(); return true; } @@ -45,6 +50,11 @@ public override void End() PlayHitReact(); } + private void PlayAnim() + { + m_Parent.OurAnimator.SetTrigger(Description.Anim); + } + private void PlayHitReact() { if (m_ImpactPlayed) { return; } @@ -74,5 +84,14 @@ private void PlayHitReact() //in the future we may do another physics check to handle the case where a target "ran under our weapon". //But for now, if the original target is no longer present, then we just don't play our hit react on anything. } + + public override void AnticipateAction() + { + base.AnticipateAction(); + + //note: because the hit-react is driven from the animation, this means we can anticipatively trigger a hit-react too. That + //will make combat feel responsive, but of course the actual damage won't be applied until the server tells us about it. + PlayAnim(); + } } } diff --git a/Assets/BossRoom/Scripts/Client/Game/Action/StealthModeActionFX.cs b/Assets/BossRoom/Scripts/Client/Game/Action/StealthModeActionFX.cs index 2f76ba430..917eae680 100644 --- a/Assets/BossRoom/Scripts/Client/Game/Action/StealthModeActionFX.cs +++ b/Assets/BossRoom/Scripts/Client/Game/Action/StealthModeActionFX.cs @@ -27,6 +27,7 @@ public StealthModeActionFX(ref ActionRequestData data, ClientCharacterVisualizat public override bool Start() { + base.Start(); m_Parent.OurAnimator.SetTrigger(Description.Anim); return true; } diff --git a/Assets/BossRoom/Scripts/Client/Game/Action/TargetActionFX.cs b/Assets/BossRoom/Scripts/Client/Game/Action/TargetActionFX.cs index e3a081ec5..bb6a94f28 100644 --- a/Assets/BossRoom/Scripts/Client/Game/Action/TargetActionFX.cs +++ b/Assets/BossRoom/Scripts/Client/Game/Action/TargetActionFX.cs @@ -13,6 +13,7 @@ public class TargetActionFX : ActionFX { private GameObject m_TargetReticule; private ulong m_CurrentTarget; + private ulong m_NewTarget; private NetworkCharacterState m_ParentState; private const float k_ReticuleGroundHeight = 0.2f; @@ -23,15 +24,34 @@ public TargetActionFX(ref ActionRequestData data, ClientCharacterVisualization p public override bool Start() { + base.Start(); m_ParentState = m_Parent.Parent.GetComponent(); + + m_ParentState.TargetId.OnValueChanged += OnTargetChanged; + m_ParentState.GetComponent().ActionInputEvent += OnActionInput; + return true; } + private void OnTargetChanged(ulong oldTarget, ulong newTarget ) + { + m_NewTarget = newTarget; + } + + private void OnActionInput(ActionRequestData data ) + { + //this method runs on the owning client, and allows us to anticipate our new target for purposes of FX visualization. + if( data.ActionTypeEnum == ActionType.GeneralTarget ) + { + m_NewTarget = data.TargetIds[0]; + } + } + public override bool Update() { - if( m_CurrentTarget != m_ParentState.TargetId.Value ) + if( m_CurrentTarget != m_NewTarget ) { - m_CurrentTarget = m_ParentState.TargetId.Value; + m_CurrentTarget = m_NewTarget; if (NetworkSpawnManager.SpawnedObjects.TryGetValue(m_CurrentTarget, out NetworkObject targetObject ) ) { @@ -89,6 +109,12 @@ private void ValidateReticule(NetworkObject targetObject) public override void Cancel() { GameObject.Destroy(m_TargetReticule); + + m_ParentState.TargetId.OnValueChanged -= OnTargetChanged; + if( m_ParentState.TryGetComponent(out Client.ClientInputSender inputSender)) + { + inputSender.ActionInputEvent -= OnActionInput; + } } } diff --git a/Assets/BossRoom/Scripts/Client/Game/Action/TrampleActionFX.cs b/Assets/BossRoom/Scripts/Client/Game/Action/TrampleActionFX.cs index def39ff0d..bfd36086c 100644 --- a/Assets/BossRoom/Scripts/Client/Game/Action/TrampleActionFX.cs +++ b/Assets/BossRoom/Scripts/Client/Game/Action/TrampleActionFX.cs @@ -37,6 +37,7 @@ public TrampleActionFX(ref ActionRequestData data, ClientCharacterVisualization public override bool Start() { + base.Start(); m_Parent.OurAnimator.SetTrigger(Description.Anim); return true; } diff --git a/Assets/BossRoom/Scripts/Client/Game/Character/ClientCharacterVisualization.cs b/Assets/BossRoom/Scripts/Client/Game/Character/ClientCharacterVisualization.cs index ad6bc8159..67384ca08 100644 --- a/Assets/BossRoom/Scripts/Client/Game/Character/ClientCharacterVisualization.cs +++ b/Assets/BossRoom/Scripts/Client/Game/Character/ClientCharacterVisualization.cs @@ -51,6 +51,8 @@ public class ClientCharacterVisualization : NetworkBehaviour event Action Destroyed; + public bool CanPerformActions { get { return m_NetState.CanPerformActions; } } + /// public override void NetworkStart() { @@ -120,6 +122,7 @@ public override void NetworkStart() if( Parent.TryGetComponent(out ClientInputSender inputSender)) { + inputSender.ActionInputEvent += OnActionInput; inputSender.ClientMoveEvent += OnMoveInput; } } @@ -143,6 +146,11 @@ public override void NetworkStart() } } + private void OnActionInput(ActionRequestData data) + { + m_ActionViz.AnticipateAction(ref data); + } + private void OnMoveInput(Vector3 position) { if( !IsAnimating ) @@ -182,6 +190,7 @@ private void OnDestroy() if (Parent != null && Parent.TryGetComponent(out ClientInputSender sender)) { + sender.ActionInputEvent -= OnActionInput; sender.ClientMoveEvent -= OnMoveInput; } } diff --git a/Assets/BossRoom/Scripts/Client/Game/Character/ClientInputSender.cs b/Assets/BossRoom/Scripts/Client/Game/Character/ClientInputSender.cs index 0a415344d..fedd459d3 100644 --- a/Assets/BossRoom/Scripts/Client/Game/Character/ClientInputSender.cs +++ b/Assets/BossRoom/Scripts/Client/Game/Character/ClientInputSender.cs @@ -20,6 +20,7 @@ public class ClientInputSender : NetworkBehaviour //upstream network conservation. This matters when holding down your mouse button to move. private const float k_MoveSendRateSeconds = 0.05f; //20 fps. + private const float k_TargetMoveTimeout = 0.45f; //prevent moves for this long after targeting someone (helps prevent walking to the guy you clicked). private float m_LastSentMove; @@ -33,6 +34,11 @@ public class ClientInputSender : NetworkBehaviour private NetworkCharacterState m_NetworkCharacter; + /// + /// This event fires at the time when an action request is sent to the server. + /// + public Action ActionInputEvent; + /// /// This describes how a skill was requested. Skills requested via mouse click will do raycasts to determine their target; skills requested /// in other matters will use the stateful target stored in NetworkCharacterState. @@ -216,6 +222,7 @@ private void PerformSkill(ActionType actionType, SkillTriggerStyle triggerStyle, //Don't trigger our move logic for another 500ms. This protects us from moving just because we clicked on them to target them. m_LastSentMove = Time.time + k_TargetMoveTimeout; + ActionInputEvent?.Invoke(playerAction); m_NetworkCharacter.RecvDoActionServerRPC(playerAction); } else if(actionType != ActionType.GeneralTarget ) @@ -223,7 +230,10 @@ private void PerformSkill(ActionType actionType, SkillTriggerStyle triggerStyle, // clicked on nothing... perform a "miss" attack on the spot they clicked on var data = new ActionRequestData(); PopulateSkillRequest(k_CachedHit[0].point, actionType, ref data); + + ActionInputEvent?.Invoke(data); m_NetworkCharacter.RecvDoActionServerRPC(data); + } } diff --git a/Assets/BossRoom/Scripts/Shared/Game/Entity/NetworkCharacterState.cs b/Assets/BossRoom/Scripts/Shared/Game/Entity/NetworkCharacterState.cs index e63394a58..2314999ee 100644 --- a/Assets/BossRoom/Scripts/Shared/Game/Entity/NetworkCharacterState.cs +++ b/Assets/BossRoom/Scripts/Shared/Game/Entity/NetworkCharacterState.cs @@ -99,6 +99,18 @@ public int HitPoints public bool IsValidTarget { get { return NetworkLifeState.Value != LifeState.Dead; } } + /// + /// Returns true if the Character is currently in a state where it can play actions, false otherwise. + /// + public bool CanPerformActions + { + get + { + //TODO: stun state should be tracked in NetworkCharacterState, and then also checked here. + return NetworkLifeState.Value == LifeState.Alive; + } + } + /// /// The CharacterData object associated with this Character. This is the static game data that defines its attack skills, HP, etc. ///