using System.Collections; using System.Collections.Generic; using Unity.BossRoom.ConnectionManagement; using Unity.BossRoom.Gameplay.Actions; using Unity.BossRoom.Gameplay.Configuration; using Unity.BossRoom.Gameplay.GameplayObjects.Character.AI; using Unity.Multiplayer.Samples.BossRoom; using Unity.Netcode; using UnityEngine; using UnityEngine.Serialization; using Action = Unity.BossRoom.Gameplay.Actions.Action; namespace Unity.BossRoom.Gameplay.GameplayObjects.Character { /// /// Contains all NetworkVariables, RPCs and server-side logic of a character. /// This class was separated in two to keep client and server context self contained. This way you don't have to continuously ask yourself if code is running client or server side. /// [RequireComponent(typeof(NetworkHealthState), typeof(NetworkLifeState), typeof(NetworkAvatarGuidState))] public class ServerCharacter : NetworkBehaviour, ITargetable { [FormerlySerializedAs("m_ClientVisualization")] [SerializeField] ClientCharacter m_ClientCharacter; public ClientCharacter clientCharacter => m_ClientCharacter; [SerializeField] CharacterClass m_CharacterClass; public CharacterClass CharacterClass { get { if (m_CharacterClass == null) { m_CharacterClass = m_State.RegisteredAvatar.CharacterClass; } return m_CharacterClass; } set => m_CharacterClass = value; } /// Indicates how the character's movement should be depicted. public NetworkVariable MovementStatus { get; } = new NetworkVariable(); public NetworkVariable HeldNetworkObject { get; } = new NetworkVariable(); /// /// Indicates whether this character is in "stealth mode" (invisible to monsters and other players). /// public NetworkVariable IsStealthy { get; } = new NetworkVariable(); public NetworkHealthState NetHealthState { get; private set; } /// /// The active target of this character. /// public NetworkVariable TargetId { get; } = new NetworkVariable(); /// /// Current HP. This value is populated at startup time from CharacterClass data. /// public int HitPoints { get => NetHealthState.HitPoints.Value; private set => NetHealthState.HitPoints.Value = value; } public NetworkLifeState NetLifeState { get; private set; } /// /// Current LifeState. Only Players should enter the FAINTED state. /// public LifeState LifeState { get => NetLifeState.LifeState.Value; private set => NetLifeState.LifeState.Value = value; } /// /// Returns true if this Character is an NPC. /// public bool IsNpc => CharacterClass.IsNpc; public bool IsValidTarget => LifeState != LifeState.Dead; /// /// Returns true if the Character is currently in a state where it can play actions, false otherwise. /// public bool CanPerformActions => LifeState == LifeState.Alive; /// /// Character Type. This value is populated during character selection. /// public CharacterTypeEnum CharacterType => CharacterClass.CharacterType; private ServerActionPlayer m_ServerActionPlayer; /// /// The Character's ActionPlayer. This is mainly exposed for use by other Actions. In particular, users are discouraged from /// calling 'PlayAction' directly on this, as the ServerCharacter has certain game-level checks it performs in its own wrapper. /// public ServerActionPlayer ActionPlayer => m_ServerActionPlayer; [SerializeField] [Tooltip("If set to false, an NPC character will be denied its brain (won't attack or chase players)")] private bool m_BrainEnabled = true; [SerializeField] [Tooltip("Setting negative value disables destroying object after it is killed.")] private float m_KilledDestroyDelaySeconds = 3.0f; [SerializeField] [Tooltip("If set, the ServerCharacter will automatically play the StartingAction when it is created. ")] private Action m_StartingAction; [SerializeField] DamageReceiver m_DamageReceiver; [SerializeField] ServerCharacterMovement m_Movement; public ServerCharacterMovement Movement => m_Movement; [SerializeField] PhysicsWrapper m_PhysicsWrapper; public PhysicsWrapper physicsWrapper => m_PhysicsWrapper; [SerializeField] ServerAnimationHandler m_ServerAnimationHandler; public ServerAnimationHandler serverAnimationHandler => m_ServerAnimationHandler; private AIBrain m_AIBrain; NetworkAvatarGuidState m_State; public ulong? PendingSwapRequest { get; set; } public int? TargetPlatformId { get; private set; } = null; public void SetTargetPlatform(int platformId) { TargetPlatformId = platformId; } public void ClearTargetPlatform() { TargetPlatformId = null; } private void OnTriggerEnter(Collider other) { if (TargetPlatformId.HasValue && other.TryGetComponent(out var platform)) { if (platform.PlatformID == TargetPlatformId.Value) { if (!platform.IsOccupied) { // platform.Occupy(this); Debug.Log($"{name} successfully occupied Platform {platform.PlatformID}."); } else { Debug.Log($"{name} couldn't occupy Platform {platform.PlatformID}. Becoming a crow!"); } ClearTargetPlatform(); } } } void Awake() { m_ServerActionPlayer = new ServerActionPlayer(this); NetLifeState = GetComponent(); NetHealthState = GetComponent(); m_State = GetComponent(); } [Rpc(SendTo.Everyone)] private void ShowSwapConfirmationPanelClientRpc() { if (NetworkManager.Singleton.LocalClientId == OwnerClientId) { // Show the confirmation panel for the specific player var panel = FindObjectOfType(); if (panel != null) { panel.ShowPanel(this); // Pass the current ServerCharacter reference } else { Debug.LogError("SwapConfirmationPanel not found in the scene!"); } } } [Rpc(SendTo.Server, RequireOwnership = false)] public void NotifySwapRequestRpc(ulong initiatingPlayerId) { PendingSwapRequest = initiatingPlayerId; // Notify all clients except the server, filtered by target ShowSwapConfirmationPanelClientRpc(); Debug.Log($"Swap request received from Player {initiatingPlayerId}. Waiting for confirmation."); } [Rpc(SendTo.Server, RequireOwnership = false)] public void NotifySwapDecisionRpc(bool isAccepted) { if (!PendingSwapRequest.HasValue) return; ulong initiatingPlayerId = PendingSwapRequest.Value; if (isAccepted) { if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(initiatingPlayerId, out var initiatingPlayerObj) && initiatingPlayerObj.TryGetComponent(out ServerCharacter initiatingPlayer)) { // Call InitiateSwap directly InitiateSwap(initiatingPlayer, this); Debug.Log($"Swap confirmed: {initiatingPlayer.name} and {this.name} are swapping."); } } else { Debug.Log($"Swap request denied by {this.name}."); } PendingSwapRequest = null; } public void InitiateSwap(ServerCharacter initiatingPlayer, ServerCharacter targetPlayer) { int initiatingPlatformId = PlatformManager.Instance.GetPlatformOccupiedByPlayer(initiatingPlayer).PlatformID; int targetPlatformId = PlatformManager.Instance.GetPlatformOccupiedByPlayer(targetPlayer).PlatformID; initiatingPlayer.SetTargetPlatform(targetPlatformId); targetPlayer.SetTargetPlatform(initiatingPlatformId); Vector3 initiatingPlayerPosition = initiatingPlayer.physicsWrapper.Transform.position; Vector3 targetPlayerPosition = this.physicsWrapper.Transform.position; // Execute the swap initiatingPlayer.ServerSendCharacterInputRpc(targetPlayerPosition); this.ServerSendCharacterInputRpc(initiatingPlayerPosition); Debug.Log($"Swap initiated: {initiatingPlayer.name} -> Platform {targetPlatformId}, {targetPlayer.name} -> Platform {initiatingPlatformId}."); } public override void OnNetworkSpawn() { if (!IsServer) { enabled = false; } else { NetLifeState.LifeState.OnValueChanged += OnLifeStateChanged; m_DamageReceiver.DamageReceived += ReceiveHP; m_DamageReceiver.CollisionEntered += CollisionEntered; if (IsNpc) { m_AIBrain = new AIBrain(this, m_ServerActionPlayer); } if (m_StartingAction != null) { var startingAction = new ActionRequestData() { ActionID = m_StartingAction.ActionID }; PlayAction(ref startingAction); } InitializeHitPoints(); } } public override void OnNetworkDespawn() { NetLifeState.LifeState.OnValueChanged -= OnLifeStateChanged; if (m_DamageReceiver) { m_DamageReceiver.DamageReceived -= ReceiveHP; m_DamageReceiver.CollisionEntered -= CollisionEntered; } } /// /// RPC to send inputs for this character from a client to a server. /// /// The position which this character should move towards. [Rpc(SendTo.Server)] public void ServerSendCharacterInputRpc(Vector3 movementTarget) { if (LifeState == LifeState.Alive && !m_Movement.IsPerformingForcedMovement()) { // if we're currently playing an interruptible action, interrupt it! if (m_ServerActionPlayer.GetActiveActionInfo(out ActionRequestData data)) { if (GameDataSource.Instance.GetActionPrototypeByID(data.ActionID).Config.ActionInterruptible) { m_ServerActionPlayer.ClearActions(false); } } m_ServerActionPlayer.CancelRunningActionsByLogic(ActionLogic.Target, true); //clear target on move. m_Movement.SetMovementTarget(movementTarget); } } // ACTION SYSTEM /// /// Client->Server RPC that sends a request to play an action. /// /// Data about which action to play and its associated details. [Rpc(SendTo.Server)] public void ServerPlayActionRpc(ActionRequestData data) { ActionRequestData data1 = data; if (!GameDataSource.Instance.GetActionPrototypeByID(data1.ActionID).Config.IsFriendly) { // notify running actions that we're using a new attack. (e.g. so Stealth can cancel itself) ActionPlayer.OnGameplayActivity(Action.GameplayActivity.UsingAttackAction); } PlayAction(ref data1); } // UTILITY AND SPECIAL-PURPOSE RPCs /// /// Called on server when the character's client decides they have stopped "charging up" an attack. /// [Rpc(SendTo.Server)] public void ServerStopChargingUpRpc() { m_ServerActionPlayer.OnGameplayActivity(Action.GameplayActivity.StoppedChargingUp); } void InitializeHitPoints() { HitPoints = CharacterClass.BaseHP.Value; if (!IsNpc) { SessionPlayerData? sessionPlayerData = SessionManager.Instance.GetPlayerData(OwnerClientId); if (sessionPlayerData is { HasCharacterSpawned: true }) { HitPoints = sessionPlayerData.Value.CurrentHitPoints; if (HitPoints <= 0) { LifeState = LifeState.Fainted; } } } } /// /// Play a sequence of actions! /// public void PlayAction(ref ActionRequestData action) { //the character needs to be alive in order to be able to play actions if (LifeState == LifeState.Alive && !m_Movement.IsPerformingForcedMovement()) { if (action.CancelMovement) { m_Movement.CancelMove(); } m_ServerActionPlayer.PlayAction(ref action); } } void OnLifeStateChanged(LifeState prevLifeState, LifeState lifeState) { if (lifeState != LifeState.Alive) { m_ServerActionPlayer.ClearActions(true); m_Movement.CancelMove(); } } IEnumerator KilledDestroyProcess() { yield return new WaitForSeconds(m_KilledDestroyDelaySeconds); if (NetworkObject != null) { NetworkObject.Despawn(true); } } /// /// Receive an HP change from somewhere. Could be healing or damage. /// /// Person dishing out this damage/healing. Can be null. /// The HP to receive. Positive value is healing. Negative is damage. void ReceiveHP(ServerCharacter inflicter, int HP) { //to our own effects, and modify the damage or healing as appropriate. But in this game, we just take it straight. if (HP > 0) { m_ServerActionPlayer.OnGameplayActivity(Action.GameplayActivity.Healed); float healingMod = m_ServerActionPlayer.GetBuffedValue(Action.BuffableValue.PercentHealingReceived); HP = (int)(HP * healingMod); } else { #if UNITY_EDITOR || DEVELOPMENT_BUILD // Don't apply damage if god mode is on if (NetLifeState.IsGodMode.Value) { return; } #endif m_ServerActionPlayer.OnGameplayActivity(Action.GameplayActivity.AttackedByEnemy); float damageMod = m_ServerActionPlayer.GetBuffedValue(Action.BuffableValue.PercentDamageReceived); HP = (int)(HP * damageMod); serverAnimationHandler.NetworkAnimator.SetTrigger("HitReact1"); } HitPoints = Mathf.Clamp(HitPoints + HP, 0, CharacterClass.BaseHP.Value); if (m_AIBrain != null) { //let the brain know about the modified amount of damage we received. m_AIBrain.ReceiveHP(inflicter, HP); } //we can't currently heal a dead character back to Alive state. //that's handled by a separate function. if (HitPoints <= 0) { if (IsNpc) { if (m_KilledDestroyDelaySeconds >= 0.0f && LifeState != LifeState.Dead) { StartCoroutine(KilledDestroyProcess()); } LifeState = LifeState.Dead; } else { LifeState = LifeState.Fainted; } m_ServerActionPlayer.ClearActions(false); } } /// /// Determines a gameplay variable for this character. The value is determined /// by the character's active Actions. /// /// /// public float GetBuffedValue(Action.BuffableValue buffType) { return m_ServerActionPlayer.GetBuffedValue(buffType); } /// /// Receive a Life State change that brings Fainted characters back to Alive state. /// /// Person reviving the character. /// The HP to set to a newly revived character. public void Revive(ServerCharacter inflicter, int HP) { if (LifeState == LifeState.Fainted) { HitPoints = Mathf.Clamp(HP, 0, CharacterClass.BaseHP.Value); NetLifeState.LifeState.Value = LifeState.Alive; } } void Update() { m_ServerActionPlayer.OnUpdate(); if (m_AIBrain != null && LifeState == LifeState.Alive && m_BrainEnabled) { m_AIBrain.Update(); } } void CollisionEntered(Collision collision) { if (m_ServerActionPlayer != null) { m_ServerActionPlayer.CollisionEntered(collision); } } /// /// This character's AIBrain. Will be null if this is not an NPC. /// public AIBrain AIBrain { get { return m_AIBrain; } } } }