You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
601 lines
26 KiB
C#
601 lines
26 KiB
C#
using System;
|
|
using Unity.BossRoom.Gameplay.Actions;
|
|
using Unity.BossRoom.Gameplay.Configuration;
|
|
using Unity.BossRoom.Gameplay.GameplayObjects;
|
|
using Unity.BossRoom.Gameplay.GameplayObjects.Character;
|
|
using Unity.Netcode;
|
|
using UnityEngine;
|
|
using UnityEngine.AI;
|
|
using UnityEngine.Assertions;
|
|
using UnityEngine.EventSystems;
|
|
|
|
namespace Unity.BossRoom.Gameplay.UserInput
|
|
{
|
|
/// <summary>
|
|
/// Captures inputs for a character on a client and sends them to the server.
|
|
/// </summary>
|
|
[RequireComponent(typeof(ServerCharacter))]
|
|
public class ClientInputSender : NetworkBehaviour
|
|
{
|
|
const float k_MouseInputRaycastDistance = 100f;
|
|
|
|
//The movement input rate is capped at 40ms (or 25 fps). This provides a nice balance between responsiveness and
|
|
//upstream network conservation. This matters when holding down your mouse button to move.
|
|
const float k_MoveSendRateSeconds = 0.04f; //25 fps.
|
|
|
|
const float k_TargetMoveTimeout = 0.45f; //prevent moves for this long after targeting someone (helps prevent walking to the guy you clicked).
|
|
|
|
float m_LastSentMove;
|
|
|
|
// Cache raycast hit array so that we can use non alloc raycasts
|
|
readonly RaycastHit[] k_CachedHit = new RaycastHit[4];
|
|
|
|
// This is basically a constant but layer masks cannot be created in the constructor, that's why it's assigned int Awake.
|
|
LayerMask m_GroundLayerMask;
|
|
|
|
LayerMask m_ActionLayerMask;
|
|
|
|
const float k_MaxNavMeshDistance = 1f;
|
|
|
|
RaycastHitComparer m_RaycastHitComparer;
|
|
|
|
[SerializeField]
|
|
ServerCharacter m_ServerCharacter;
|
|
|
|
/// <summary>
|
|
/// This event fires at the time when an action request is sent to the server.
|
|
/// </summary>
|
|
public event Action<ActionRequestData> ActionInputEvent;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public enum SkillTriggerStyle
|
|
{
|
|
None, //no skill was triggered.
|
|
MouseClick, //skill was triggered via mouse-click implying you should do a raycast from the mouse position to find a target.
|
|
Keyboard, //skill was triggered via a Keyboard press, implying target should be taken from the active target.
|
|
KeyboardRelease, //represents a released key.
|
|
UI, //skill was triggered from the UI, and similar to Keyboard, target should be inferred from the active target.
|
|
UIRelease, //represents letting go of the mouse-button on a UI button
|
|
}
|
|
|
|
bool IsReleaseStyle(SkillTriggerStyle style)
|
|
{
|
|
return style == SkillTriggerStyle.KeyboardRelease || style == SkillTriggerStyle.UIRelease;
|
|
}
|
|
|
|
/// <summary>
|
|
/// This struct essentially relays the call params of RequestAction to FixedUpdate. Recall that we may need to do raycasts
|
|
/// as part of doing the action, and raycasts done outside of FixedUpdate can give inconsistent results (otherwise we would
|
|
/// just expose PerformAction as a public method, and let it be called in whatever scoped it liked.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Reference: https://answers.unity.com/questions/1141633/why-does-fixedupdate-work-when-update-doesnt.html
|
|
/// </remarks>
|
|
struct ActionRequest
|
|
{
|
|
public SkillTriggerStyle TriggerStyle;
|
|
public ActionID RequestedActionID;
|
|
public ulong TargetId;
|
|
}
|
|
|
|
/// <summary>
|
|
/// List of ActionRequests that have been received since the last FixedUpdate ran. This is a static array, to avoid allocs, and
|
|
/// because we don't really want to let this list grow indefinitely.
|
|
/// </summary>
|
|
readonly ActionRequest[] m_ActionRequests = new ActionRequest[5];
|
|
|
|
/// <summary>
|
|
/// Number of ActionRequests that have been queued since the last FixedUpdate.
|
|
/// </summary>
|
|
int m_ActionRequestCount;
|
|
|
|
BaseActionInput m_CurrentSkillInput;
|
|
|
|
bool m_MoveRequest;
|
|
|
|
Camera m_MainCamera;
|
|
|
|
public event Action<Vector3> ClientMoveEvent;
|
|
|
|
/// <summary>
|
|
/// Convenience getter that returns our CharacterData
|
|
/// </summary>
|
|
CharacterClass CharacterClass => m_ServerCharacter.CharacterClass;
|
|
|
|
[SerializeField]
|
|
PhysicsWrapper m_PhysicsWrapper;
|
|
|
|
public ActionState actionState1 { get; private set; }
|
|
|
|
public ActionState actionState2 { get; private set; }
|
|
|
|
public ActionState actionState3 { get; private set; }
|
|
|
|
public System.Action action1ModifiedCallback;
|
|
|
|
ServerCharacter m_TargetServerCharacter;
|
|
|
|
void Awake()
|
|
{
|
|
m_MainCamera = Camera.main;
|
|
}
|
|
|
|
public override void OnNetworkSpawn()
|
|
{
|
|
if (!IsClient || !IsOwner)
|
|
{
|
|
enabled = false;
|
|
// dont need to do anything else if not the owner
|
|
return;
|
|
}
|
|
|
|
m_ServerCharacter.TargetId.OnValueChanged += OnTargetChanged;
|
|
m_ServerCharacter.HeldNetworkObject.OnValueChanged += OnHeldNetworkObjectChanged;
|
|
|
|
if (CharacterClass.Skill1 &&
|
|
GameDataSource.Instance.TryGetActionPrototypeByID(CharacterClass.Skill1.ActionID, out var action1))
|
|
{
|
|
actionState1 = new ActionState() { actionID = action1.ActionID, selectable = true };
|
|
}
|
|
if (CharacterClass.Skill2 &&
|
|
GameDataSource.Instance.TryGetActionPrototypeByID(CharacterClass.Skill2.ActionID, out var action2))
|
|
{
|
|
actionState2 = new ActionState() { actionID = action2.ActionID, selectable = true };
|
|
}
|
|
if (CharacterClass.Skill3 &&
|
|
GameDataSource.Instance.TryGetActionPrototypeByID(CharacterClass.Skill3.ActionID, out var action3))
|
|
{
|
|
actionState3 = new ActionState() { actionID = action3.ActionID, selectable = true };
|
|
}
|
|
|
|
m_GroundLayerMask = LayerMask.GetMask(new[] { "Ground" });
|
|
m_ActionLayerMask = LayerMask.GetMask(new[] { "PCs", "NPCs", "Ground" });
|
|
|
|
m_RaycastHitComparer = new RaycastHitComparer();
|
|
}
|
|
|
|
public override void OnNetworkDespawn()
|
|
{
|
|
if (m_ServerCharacter)
|
|
{
|
|
m_ServerCharacter.TargetId.OnValueChanged -= OnTargetChanged;
|
|
m_ServerCharacter.HeldNetworkObject.OnValueChanged -= OnHeldNetworkObjectChanged;
|
|
}
|
|
|
|
if (m_TargetServerCharacter)
|
|
{
|
|
m_TargetServerCharacter.NetLifeState.LifeState.OnValueChanged -= OnTargetLifeStateChanged;
|
|
}
|
|
}
|
|
|
|
void OnTargetChanged(ulong previousValue, ulong newValue)
|
|
{
|
|
if (m_TargetServerCharacter)
|
|
{
|
|
m_TargetServerCharacter.NetLifeState.LifeState.OnValueChanged -= OnTargetLifeStateChanged;
|
|
}
|
|
|
|
m_TargetServerCharacter = null;
|
|
|
|
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(newValue, out var selection) &&
|
|
selection.TryGetComponent(out m_TargetServerCharacter))
|
|
{
|
|
m_TargetServerCharacter.NetLifeState.LifeState.OnValueChanged += OnTargetLifeStateChanged;
|
|
}
|
|
|
|
UpdateAction1();
|
|
}
|
|
|
|
void OnHeldNetworkObjectChanged(ulong previousValue, ulong newValue)
|
|
{
|
|
UpdateAction1();
|
|
}
|
|
|
|
void OnTargetLifeStateChanged(LifeState previousValue, LifeState newValue)
|
|
{
|
|
UpdateAction1();
|
|
}
|
|
|
|
void FinishSkill()
|
|
{
|
|
m_CurrentSkillInput = null;
|
|
}
|
|
|
|
void SendInput(ActionRequestData action)
|
|
{
|
|
ActionInputEvent?.Invoke(action);
|
|
m_ServerCharacter.ServerPlayActionRpc(action);
|
|
}
|
|
|
|
void FixedUpdate()
|
|
{
|
|
//play all ActionRequests, in FIFO order.
|
|
for (int i = 0; i < m_ActionRequestCount; ++i)
|
|
{
|
|
if (m_CurrentSkillInput != null)
|
|
{
|
|
//actions requested while input is active are discarded, except for "Release" requests, which go through.
|
|
if (IsReleaseStyle(m_ActionRequests[i].TriggerStyle))
|
|
{
|
|
m_CurrentSkillInput.OnReleaseKey();
|
|
}
|
|
}
|
|
else if (!IsReleaseStyle(m_ActionRequests[i].TriggerStyle))
|
|
{
|
|
var actionPrototype = GameDataSource.Instance.GetActionPrototypeByID(m_ActionRequests[i].RequestedActionID);
|
|
if (actionPrototype.Config.ActionInput != null)
|
|
{
|
|
var skillPlayer = Instantiate(actionPrototype.Config.ActionInput);
|
|
skillPlayer.Initiate(m_ServerCharacter, m_PhysicsWrapper.Transform.position, actionPrototype.ActionID, SendInput, FinishSkill);
|
|
m_CurrentSkillInput = skillPlayer;
|
|
}
|
|
else
|
|
{
|
|
PerformSkill(actionPrototype.ActionID, m_ActionRequests[i].TriggerStyle, m_ActionRequests[i].TargetId);
|
|
}
|
|
}
|
|
}
|
|
|
|
m_ActionRequestCount = 0;
|
|
|
|
if (EventSystem.current.currentSelectedGameObject != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_MoveRequest)
|
|
{
|
|
m_MoveRequest = false;
|
|
if ((Time.time - m_LastSentMove) > k_MoveSendRateSeconds)
|
|
{
|
|
m_LastSentMove = Time.time;
|
|
var ray = m_MainCamera.ScreenPointToRay(UnityEngine.Input.mousePosition);
|
|
|
|
var groundHits = Physics.RaycastNonAlloc(ray,
|
|
k_CachedHit,
|
|
k_MouseInputRaycastDistance,
|
|
m_GroundLayerMask);
|
|
|
|
if (groundHits > 0)
|
|
{
|
|
if (groundHits > 1)
|
|
{
|
|
// sort hits by distance
|
|
Array.Sort(k_CachedHit, 0, groundHits, m_RaycastHitComparer);
|
|
}
|
|
|
|
// verify point is indeed on navmesh surface
|
|
if (NavMesh.SamplePosition(k_CachedHit[0].point,
|
|
out var hit,
|
|
k_MaxNavMeshDistance,
|
|
NavMesh.AllAreas))
|
|
{
|
|
m_ServerCharacter.ServerSendCharacterInputRpc(hit.position);
|
|
|
|
//Send our client only click request
|
|
ClientMoveEvent?.Invoke(hit.position);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Perform a skill in response to some input trigger. This is the common method to which all input-driven skill plays funnel.
|
|
/// </summary>
|
|
/// <param name="actionID">The action you want to play. Note that "Skill1" may be overriden contextually depending on the target.</param>
|
|
/// <param name="triggerStyle">What sort of input triggered this skill?</param>
|
|
/// <param name="targetId">(optional) Pass in a specific networkID to target for this action</param>
|
|
void PerformSkill(ActionID actionID, SkillTriggerStyle triggerStyle, ulong targetId = 0)
|
|
{
|
|
Transform hitTransform = null;
|
|
|
|
if (targetId != 0)
|
|
{
|
|
// if a targetId is given, try to find the object
|
|
NetworkObject targetNetObj;
|
|
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(targetId, out targetNetObj))
|
|
{
|
|
hitTransform = targetNetObj.transform;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// otherwise try to find an object under the input position
|
|
int numHits = 0;
|
|
if (triggerStyle == SkillTriggerStyle.MouseClick)
|
|
{
|
|
var ray = m_MainCamera.ScreenPointToRay(UnityEngine.Input.mousePosition);
|
|
numHits = Physics.RaycastNonAlloc(ray, k_CachedHit, k_MouseInputRaycastDistance, m_ActionLayerMask);
|
|
}
|
|
|
|
int networkedHitIndex = -1;
|
|
for (int i = 0; i < numHits; i++)
|
|
{
|
|
if (k_CachedHit[i].transform.GetComponentInParent<NetworkObject>())
|
|
{
|
|
networkedHitIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
hitTransform = networkedHitIndex >= 0 ? k_CachedHit[networkedHitIndex].transform : null;
|
|
}
|
|
|
|
if (GetActionRequestForTarget(hitTransform, actionID, triggerStyle, out ActionRequestData playerAction))
|
|
{
|
|
//Don't trigger our move logic for a while. This protects us from moving just because we clicked on them to target them.
|
|
m_LastSentMove = Time.time + k_TargetMoveTimeout;
|
|
|
|
SendInput(playerAction);
|
|
}
|
|
else if (!GameDataSource.Instance.GetActionPrototypeByID(actionID).IsGeneralTargetAction)
|
|
{
|
|
// clicked on nothing... perform an "untargeted" attack on the spot they clicked on.
|
|
// (Different Actions will deal with this differently. For some, like archer arrows, this will fire an arrow
|
|
// in the desired direction. For others, like mage's bolts, this will fire a "miss" projectile at the spot clicked on.)
|
|
|
|
var data = new ActionRequestData();
|
|
PopulateSkillRequest(k_CachedHit[0].point, actionID, ref data);
|
|
|
|
SendInput(data);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// When you right-click on something you will want to do contextually different things. For example you might attack an enemy,
|
|
/// but revive a friend. You might also decide to do nothing (e.g. right-clicking on a friend who hasn't FAINTED).
|
|
/// </summary>
|
|
/// <param name="hit">The Transform of the entity we clicked on, or null if none.</param>
|
|
/// <param name="actionID">The Action to build for</param>
|
|
/// <param name="triggerStyle">How did this skill play get triggered? Mouse, Keyboard, UI etc.</param>
|
|
/// <param name="resultData">Out parameter that will be filled with the resulting action, if any.</param>
|
|
/// <returns>true if we should play an action, false otherwise. </returns>
|
|
bool GetActionRequestForTarget(Transform hit, ActionID actionID, SkillTriggerStyle triggerStyle, out ActionRequestData resultData)
|
|
{
|
|
resultData = new ActionRequestData();
|
|
|
|
var targetNetObj = hit != null ? hit.GetComponentInParent<NetworkObject>() : null;
|
|
|
|
//if we can't get our target from the submitted hit transform, get it from our stateful target in our ServerCharacter.
|
|
if (!targetNetObj && !GameDataSource.Instance.GetActionPrototypeByID(actionID).IsGeneralTargetAction)
|
|
{
|
|
ulong targetId = m_ServerCharacter.TargetId.Value;
|
|
NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(targetId, out targetNetObj);
|
|
}
|
|
|
|
//sanity check that this is indeed a valid target.
|
|
if (targetNetObj == null || !ActionUtils.IsValidTarget(targetNetObj.NetworkObjectId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (targetNetObj.TryGetComponent<ServerCharacter>(out var serverCharacter))
|
|
{
|
|
//Skill1 may be contextually overridden if it was generated from a mouse-click.
|
|
if (actionID == CharacterClass.Skill1.ActionID && triggerStyle == SkillTriggerStyle.MouseClick)
|
|
{
|
|
if (!serverCharacter.IsNpc && serverCharacter.LifeState == LifeState.Fainted)
|
|
{
|
|
//right-clicked on a downed ally--change the skill play to Revive.
|
|
actionID = GameDataSource.Instance.ReviveActionPrototype.ActionID;
|
|
}
|
|
}
|
|
}
|
|
|
|
Vector3 targetHitPoint;
|
|
if (PhysicsWrapper.TryGetPhysicsWrapper(targetNetObj.NetworkObjectId, out var movementContainer))
|
|
{
|
|
targetHitPoint = movementContainer.Transform.position;
|
|
}
|
|
else
|
|
{
|
|
targetHitPoint = targetNetObj.transform.position;
|
|
}
|
|
|
|
// record our target in case this action uses that info (non-targeted attacks will ignore this)
|
|
resultData.ActionID = actionID;
|
|
resultData.TargetIds = new ulong[] { targetNetObj.NetworkObjectId };
|
|
PopulateSkillRequest(targetHitPoint, actionID, ref resultData);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Populates the ActionRequestData with additional information. The TargetIds of the action should already be set before calling this.
|
|
/// </summary>
|
|
/// <param name="hitPoint">The point in world space where the click ray hit the target.</param>
|
|
/// <param name="actionID">The action to perform (will be stamped on the resultData)</param>
|
|
/// <param name="resultData">The ActionRequestData to be filled out with additional information.</param>
|
|
void PopulateSkillRequest(Vector3 hitPoint, ActionID actionID, ref ActionRequestData resultData)
|
|
{
|
|
resultData.ActionID = actionID;
|
|
var actionConfig = GameDataSource.Instance.GetActionPrototypeByID(actionID).Config;
|
|
|
|
//most skill types should implicitly close distance. The ones that don't are explicitly set to false in the following switch.
|
|
resultData.ShouldClose = true;
|
|
|
|
// figure out the Direction in case we want to send it
|
|
Vector3 offset = hitPoint - m_PhysicsWrapper.Transform.position;
|
|
offset.y = 0;
|
|
Vector3 direction = offset.normalized;
|
|
|
|
switch (actionConfig.Logic)
|
|
{
|
|
//for projectile logic, infer the direction from the click position.
|
|
case ActionLogic.LaunchProjectile:
|
|
resultData.Direction = direction;
|
|
resultData.ShouldClose = false; //why? Because you could be lining up a shot, hoping to hit other people between you and your target. Moving you would be quite invasive.
|
|
return;
|
|
case ActionLogic.Melee:
|
|
resultData.Direction = direction;
|
|
return;
|
|
case ActionLogic.Target:
|
|
resultData.ShouldClose = false;
|
|
return;
|
|
case ActionLogic.Emote:
|
|
resultData.CancelMovement = true;
|
|
return;
|
|
case ActionLogic.RangedFXTargeted:
|
|
resultData.Position = hitPoint;
|
|
return;
|
|
case ActionLogic.DashAttack:
|
|
resultData.Position = hitPoint;
|
|
return;
|
|
case ActionLogic.PickUp:
|
|
resultData.CancelMovement = true;
|
|
resultData.ShouldQueue = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request an action be performed. This will occur on the next FixedUpdate.
|
|
/// </summary>
|
|
/// <param name="actionID"> The action you'd like to perform. </param>
|
|
/// <param name="triggerStyle"> What input style triggered this action. </param>
|
|
/// <param name="targetId"> NetworkObjectId of target. </param>
|
|
public void RequestAction(ActionID actionID, SkillTriggerStyle triggerStyle, ulong targetId = 0)
|
|
{
|
|
Assert.IsNotNull(GameDataSource.Instance.GetActionPrototypeByID(actionID),
|
|
$"Action with actionID {actionID} must be contained in the Action prototypes of GameDataSource!");
|
|
|
|
if (m_ActionRequestCount < m_ActionRequests.Length)
|
|
{
|
|
m_ActionRequests[m_ActionRequestCount].RequestedActionID = actionID;
|
|
m_ActionRequests[m_ActionRequestCount].TriggerStyle = triggerStyle;
|
|
m_ActionRequests[m_ActionRequestCount].TargetId = targetId;
|
|
m_ActionRequestCount++;
|
|
}
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
if (Input.GetKeyDown(KeyCode.Alpha1) && CharacterClass.Skill1)
|
|
{
|
|
RequestAction(actionState1.actionID, SkillTriggerStyle.Keyboard);
|
|
}
|
|
else if (Input.GetKeyUp(KeyCode.Alpha1) && CharacterClass.Skill1)
|
|
{
|
|
RequestAction(actionState1.actionID, SkillTriggerStyle.KeyboardRelease);
|
|
}
|
|
if (Input.GetKeyDown(KeyCode.Alpha2) && CharacterClass.Skill2)
|
|
{
|
|
RequestAction(actionState2.actionID, SkillTriggerStyle.Keyboard);
|
|
}
|
|
else if (Input.GetKeyUp(KeyCode.Alpha2) && CharacterClass.Skill2)
|
|
{
|
|
RequestAction(actionState2.actionID, SkillTriggerStyle.KeyboardRelease);
|
|
}
|
|
if (Input.GetKeyDown(KeyCode.Alpha3) && CharacterClass.Skill3)
|
|
{
|
|
RequestAction(actionState3.actionID, SkillTriggerStyle.Keyboard);
|
|
}
|
|
else if (Input.GetKeyUp(KeyCode.Alpha3) && CharacterClass.Skill3)
|
|
{
|
|
RequestAction(actionState3.actionID, SkillTriggerStyle.KeyboardRelease);
|
|
}
|
|
|
|
if (Input.GetKeyDown(KeyCode.Alpha5))
|
|
{
|
|
RequestAction(GameDataSource.Instance.Emote1ActionPrototype.ActionID, SkillTriggerStyle.Keyboard);
|
|
}
|
|
if (Input.GetKeyDown(KeyCode.Alpha6))
|
|
{
|
|
RequestAction(GameDataSource.Instance.Emote2ActionPrototype.ActionID, SkillTriggerStyle.Keyboard);
|
|
}
|
|
if (Input.GetKeyDown(KeyCode.Alpha7))
|
|
{
|
|
RequestAction(GameDataSource.Instance.Emote3ActionPrototype.ActionID, SkillTriggerStyle.Keyboard);
|
|
}
|
|
if (Input.GetKeyDown(KeyCode.Alpha8))
|
|
{
|
|
RequestAction(GameDataSource.Instance.Emote4ActionPrototype.ActionID, SkillTriggerStyle.Keyboard);
|
|
}
|
|
|
|
if (!EventSystem.current.IsPointerOverGameObject() && m_CurrentSkillInput == null)
|
|
{
|
|
//IsPointerOverGameObject() is a simple way to determine if the mouse is over a UI element. If it is, we don't perform mouse input logic,
|
|
//to model the button "blocking" mouse clicks from falling through and interacting with the world.
|
|
|
|
if (Input.GetMouseButtonDown(1))
|
|
{
|
|
RequestAction(CharacterClass.Skill1.ActionID, SkillTriggerStyle.MouseClick);
|
|
}
|
|
|
|
if (Input.GetMouseButtonDown(0))
|
|
{
|
|
RequestAction(GameDataSource.Instance.GeneralTargetActionPrototype.ActionID, SkillTriggerStyle.MouseClick);
|
|
}
|
|
else if (Input.GetMouseButton(0))
|
|
{
|
|
m_MoveRequest = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
void UpdateAction1()
|
|
{
|
|
var isHoldingNetworkObject =
|
|
NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(m_ServerCharacter.HeldNetworkObject.Value,
|
|
out var heldNetworkObject);
|
|
|
|
NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(m_ServerCharacter.TargetId.Value,
|
|
out var selection);
|
|
|
|
var isSelectable = true;
|
|
if (isHoldingNetworkObject)
|
|
{
|
|
// show drop!
|
|
|
|
actionState1.actionID = GameDataSource.Instance.DropActionPrototype.ActionID;
|
|
}
|
|
else if ((m_ServerCharacter.TargetId.Value != 0
|
|
&& selection != null
|
|
&& selection.TryGetComponent(out PickUpState pickUpState))
|
|
)
|
|
{
|
|
// special case: targeting a pickup-able item or holding a pickup object
|
|
|
|
actionState1.actionID = GameDataSource.Instance.PickUpActionPrototype.ActionID;
|
|
}
|
|
else if (m_ServerCharacter.TargetId.Value != 0
|
|
&& selection != null
|
|
&& selection.NetworkObjectId != m_ServerCharacter.NetworkObjectId
|
|
&& selection.TryGetComponent(out ServerCharacter charState)
|
|
&& !charState.IsNpc)
|
|
{
|
|
// special case: when we have a player selected, we change the meaning of the basic action
|
|
// we have another player selected! In that case we want to reflect that our basic Action is a Revive, not an attack!
|
|
// But we need to know if the player is alive... if so, the button should be disabled (for better player communication)
|
|
|
|
actionState1.actionID = GameDataSource.Instance.ReviveActionPrototype.ActionID;
|
|
isSelectable = charState.NetLifeState.LifeState.Value != LifeState.Alive;
|
|
}
|
|
else
|
|
{
|
|
actionState1.SetActionState(CharacterClass.Skill1.ActionID);
|
|
}
|
|
|
|
actionState1.selectable = isSelectable;
|
|
|
|
action1ModifiedCallback?.Invoke();
|
|
}
|
|
|
|
public class ActionState
|
|
{
|
|
public ActionID actionID { get; internal set; }
|
|
|
|
public bool selectable { get; internal set; }
|
|
|
|
internal void SetActionState(ActionID newActionID, bool isSelectable = true)
|
|
{
|
|
actionID = newActionID;
|
|
selectable = isSelectable;
|
|
}
|
|
}
|
|
}
|
|
}
|