using Unity.Netcode; using UnityEngine; namespace Northbound { public enum WorkerState { Idle = 0, Following = 1, Mining = 2, Returning = 3, WaitingForResource = 4 } [RequireComponent(typeof(Collider))] public class Worker : NetworkBehaviour, IInteractable, ITeamMember { [Header("Worker Settings")] public int maxBagCapacity = 50; public float miningSpeed = 5f; public float followDistance = 3f; public float movementSpeed = 3.5f; public int resourcesPerMining = 5; [Header("Interaction")] public string interactionAnimationTrigger = "Recruit"; [Header("Visual")] public GameObject miningEffectPrefab; public GameObject depositEffectPrefab; public Transform effectSpawnPoint; [Header("Team")] public TeamType teamType = TeamType.Player; private NetworkVariable _currentBagResources = new NetworkVariable( 0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); private NetworkVariable _ownerPlayerId = new NetworkVariable( ulong.MaxValue, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); private NetworkVariable _currentState = new NetworkVariable( WorkerState.Idle, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); private NetworkVariable _targetResourceId = new NetworkVariable( ulong.MaxValue, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); private Resource _targetResource; private Transform _playerTransform; private Core _core; private float _lastMiningTime; private Rigidbody _rb; private Collider _collider; private Animator _animator; [Header("Animation")] public string moveAnimation = "Walk"; public string idleAnimation = "Idle"; public string mineAnimation = "Mine"; public bool useAnimations = true; public bool playAnimationsByName = false; // true = play by name, false = use bools public int CurrentBagResources => _currentBagResources.Value; public WorkerState CurrentState => _currentState.Value; public bool IsBagFull => _currentBagResources.Value >= maxBagCapacity; public ulong OwnerPlayerId => _ownerPlayerId.Value; public override void OnNetworkSpawn() { base.OnNetworkSpawn(); _rb = GetComponent(); _collider = GetComponent(); _animator = GetComponent(); if (IsServer) { _rb.constraints = RigidbodyConstraints.FreezeRotation; FindCore(); } _currentState.OnValueChanged += OnStateChanged; _ownerPlayerId.OnValueChanged += OnOwnerChanged; _targetResourceId.OnValueChanged += OnTargetResourceChanged; PlayIdleAnimation(); } public override void OnNetworkDespawn() { _currentState.OnValueChanged -= OnStateChanged; _ownerPlayerId.OnValueChanged -= OnOwnerChanged; _targetResourceId.OnValueChanged -= OnTargetResourceChanged; base.OnNetworkDespawn(); } private void FindCore() { Core[] cores = FindObjectsOfType(); if (cores.Length > 0) { _core = cores[0]; } } private void Update() { if (!IsServer) return; if (_playerTransform == null && _ownerPlayerId.Value != ulong.MaxValue) { UpdatePlayerTransform(); } switch (_currentState.Value) { case WorkerState.Following: HandleFollowing(); break; case WorkerState.Mining: HandleMining(); break; case WorkerState.Returning: HandleReturning(); break; case WorkerState.WaitingForResource: HandleWaitingForResource(); break; } } private void HandleFollowing() { if (_ownerPlayerId.Value == ulong.MaxValue) { SetState(WorkerState.Idle); PlayIdleAnimation(); return; } if (_playerTransform == null) { UpdatePlayerTransform(); } if (_playerTransform == null) { PlayIdleAnimation(); return; } float distance = Vector3.Distance(transform.position, _playerTransform.position); if (distance > followDistance) { MoveTowards(_playerTransform.position); } else { PlayIdleAnimation(); } } private void HandleMining() { if (_targetResource == null) { SetState(WorkerState.Following); PlayIdleAnimation(); return; } if (IsBagFull) { SetState(WorkerState.Returning); PlayIdleAnimation(); return; } if (!_targetResource.HasResourcesAvailable()) { SetState(WorkerState.WaitingForResource); PlayIdleAnimation(); return; } float distance = GetDistanceToCollider(_targetResource.GetComponent()); if (distance > 2f) { MoveTowards(_targetResource.transform.position); } else { PlayMineAnimation(); MineResource(); } } private void HandleReturning() { if (_core == null) { FindCore(); if (_core == null) { SetState(WorkerState.Idle); PlayIdleAnimation(); return; } } float distance = GetDistanceToCollider(_core.GetComponent()); if (distance > 2f) { MoveTowards(_core.transform.position); } else { PlayIdleAnimation(); DepositToCore(); } } private void HandleWaitingForResource() { if (_targetResource == null) { SetState(WorkerState.Following); PlayIdleAnimation(); return; } if (IsBagFull) { SetState(WorkerState.Returning); PlayIdleAnimation(); return; } if (_targetResource.HasResourcesAvailable()) { SetState(WorkerState.Mining); PlayMoveAnimation(); return; } float distance = GetDistanceToCollider(_targetResource.GetComponent()); if (distance > 3f) { MoveTowards(_targetResource.transform.position); } else { PlayIdleAnimation(); } } private float GetDistanceToCollider(Collider targetCollider) { if (targetCollider == null) return float.MaxValue; Vector3 closestPoint = targetCollider.ClosestPoint(transform.position); float distance = Vector3.Distance(transform.position, closestPoint); return distance; } private void MoveTowards(Vector3 targetPosition) { float distance = Vector3.Distance(transform.position, targetPosition); if (distance <= 0.1f) { PlayIdleAnimation(); return; } Vector3 direction = (targetPosition - transform.position).normalized; direction.y = 0; float moveDistance = Mathf.Min(movementSpeed * Time.deltaTime, distance); transform.position += direction * moveDistance; transform.rotation = Quaternion.LookRotation(direction); PlayMoveAnimation(); } private void MineResource() { if (Time.time - _lastMiningTime < miningSpeed) { return; } if (!_targetResource.CanWorkerMineResource(NetworkObject.NetworkObjectId)) { return; } int availableSpace = maxBagCapacity - _currentBagResources.Value; int mineAmount = Mathf.Min(resourcesPerMining, availableSpace); if (mineAmount > 0) { int actualAmount = _targetResource.TakeResourcesForWorker(mineAmount, NetworkObject.NetworkObjectId); _currentBagResources.Value += actualAmount; _lastMiningTime = Time.time; ShowMiningEffectClientRpc(); if (IsBagFull) { SetState(WorkerState.Returning); } } } private void DepositToCore() { if (_currentBagResources.Value > 0 && _core != null) { _core.AddResource(_currentBagResources.Value); _currentBagResources.Value = 0; ShowDepositEffectClientRpc(); } bool shouldReturnToMining = _targetResource != null && _targetResource.HasResourcesAvailable(); if (shouldReturnToMining) { SetState(WorkerState.Mining); } else if (_targetResource != null) { SetState(WorkerState.WaitingForResource); } else { SetState(WorkerState.Following); } } private void SetState(WorkerState newState) { if (IsServer) { _currentState.Value = newState; } } #region IInteractable Implementation public bool CanInteract(ulong playerId) { if (_ownerPlayerId.Value != ulong.MaxValue && _ownerPlayerId.Value != playerId) return false; return true; } public void Interact(ulong playerId) { if (!CanInteract(playerId)) return; if (_ownerPlayerId.Value == ulong.MaxValue) { RecruitWorkerServerRpc(playerId, NetworkObject.NetworkObjectId); } else if (_ownerPlayerId.Value == playerId) { StopWorkerServerRpc(); } } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] private void StopWorkerServerRpc() { _targetResourceId.Value = ulong.MaxValue; _targetResource = null; SetState(WorkerState.Following); } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] private void RecruitWorkerServerRpc(ulong playerId, ulong workerNetObjectId) { _ownerPlayerId.Value = playerId; SetState(WorkerState.Following); UpdatePlayerTransform(); } [Rpc(SendTo.ClientsAndHost)] private void SetPlayerAssignedWorkerClientRpc(ulong playerId, ulong workerNetObjectId) { var spawnedObjects = NetworkManager.Singleton.SpawnManager.SpawnedObjects; NetworkObject playerObj = null; foreach (var kvp in spawnedObjects) { if (kvp.Value != null && kvp.Value.OwnerClientId == playerId) { playerObj = kvp.Value; break; } } if (playerObj != null) { var playerInteraction = playerObj.GetComponent(); if (playerInteraction != null && playerInteraction.IsOwner) { if (spawnedObjects.TryGetValue(workerNetObjectId, out var workerObj)) { var worker = workerObj.GetComponent(); if (worker != null) { playerInteraction.assignedWorker = worker; } } } } } public string GetInteractionPrompt() { if (_ownerPlayerId.Value == ulong.MaxValue) { return "[E] Recruit Worker"; } else if (NetworkManager.Singleton != null && _ownerPlayerId.Value == NetworkManager.Singleton.LocalClientId) { string stateText = _currentState.Value switch { WorkerState.Following => "따라가는 중", WorkerState.Mining => "채굴 중", WorkerState.Returning => "반환 중", WorkerState.WaitingForResource => "자원 대기 중", _ => "대기 중" }; return $"Worker (Bag: {_currentBagResources.Value}/{maxBagCapacity}) - {stateText}"; } return "Not My Worker"; } public string GetInteractionAnimation() { return interactionAnimationTrigger; } public EquipmentData GetEquipmentData() { return null; } public Transform GetTransform() { return transform; } #endregion #region ITeamMember Implementation public TeamType GetTeam() => teamType; public void SetTeam(TeamType team) { if (!IsServer) return; teamType = team; } #endregion #region ClientRPC Effects [Rpc(SendTo.ClientsAndHost)] private void ShowMiningEffectClientRpc() { if (miningEffectPrefab != null && effectSpawnPoint != null) { GameObject effect = Instantiate(miningEffectPrefab, effectSpawnPoint.position, effectSpawnPoint.rotation); Destroy(effect, 2f); } } [Rpc(SendTo.ClientsAndHost)] private void ShowDepositEffectClientRpc() { if (depositEffectPrefab != null && effectSpawnPoint != null) { GameObject effect = Instantiate(depositEffectPrefab, effectSpawnPoint.position, effectSpawnPoint.rotation); Destroy(effect, 2f); } } #endregion #region Event Handlers private void OnStateChanged(WorkerState previousState, WorkerState newState) { if (!useAnimations || _animator == null) return; switch (newState) { case WorkerState.Following: PlayIdleAnimation(); break; case WorkerState.Idle: PlayIdleAnimation(); break; case WorkerState.Mining: PlayMoveAnimation(); break; case WorkerState.Returning: PlayMoveAnimation(); break; case WorkerState.WaitingForResource: PlayIdleAnimation(); break; } } private void OnOwnerChanged(ulong previousOwner, ulong newOwner) { if (IsServer && newOwner != ulong.MaxValue) { UpdatePlayerTransform(); } } private bool UpdatePlayerTransform() { if (!IsServer || _ownerPlayerId.Value == ulong.MaxValue) return false; if (NetworkManager.Singleton != null && NetworkManager.Singleton.SpawnManager != null) { var spawnedObjects = NetworkManager.Singleton.SpawnManager.SpawnedObjects; foreach (var kvp in spawnedObjects) { var networkObj = kvp.Value; if (networkObj != null && networkObj.OwnerClientId == _ownerPlayerId.Value) { var playerInteraction = networkObj.GetComponent(); if (playerInteraction != null && networkObj.gameObject != null) { _playerTransform = networkObj.transform; return true; } } } } return false; } private void OnTargetResourceChanged(ulong previousResource, ulong newResource) { if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(newResource, out var resourceObj)) { _targetResource = resourceObj.GetComponent(); } else { _targetResource = null; } } #endregion [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] public void AssignMiningTargetServerRpc(ulong resourceId) { if (!NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(resourceId, out var resourceObj)) { return; } var resource = resourceObj.GetComponent(); if (resource == null) { return; } _targetResourceId.Value = resourceId; _targetResource = resource; if (_ownerPlayerId.Value != ulong.MaxValue) { SetState(WorkerState.Mining); } } private void PlayIdleAnimation() { if (!useAnimations || _animator == null) return; if (playAnimationsByName) { _animator.Play(idleAnimation); } else { _animator.SetBool(moveAnimation, false); _animator.SetBool(mineAnimation, false); _animator.SetBool(idleAnimation, true); } } private void PlayMoveAnimation() { if (!useAnimations || _animator == null) return; if (playAnimationsByName) { _animator.Play(moveAnimation); } else { _animator.SetBool(idleAnimation, false); _animator.SetBool(mineAnimation, false); _animator.SetBool(moveAnimation, true); } } private void PlayMineAnimation() { if (!useAnimations || _animator == null) return; if (playAnimationsByName) { _animator.Play(mineAnimation); } else { _animator.SetBool(idleAnimation, false); _animator.SetBool(moveAnimation, false); _animator.SetBool(mineAnimation, true); } } private void OnDrawGizmosSelected() { #if UNITY_EDITOR Gizmos.color = Color.yellow; Gizmos.DrawWireSphere(transform.position, followDistance); if (_currentState.Value == WorkerState.Mining && _targetResource != null) { Gizmos.color = Color.red; Gizmos.DrawLine(transform.position, _targetResource.transform.position); } if (_currentState.Value == WorkerState.WaitingForResource && _targetResource != null) { Gizmos.color = Color.blue; Gizmos.DrawLine(transform.position, _targetResource.transform.position); } if (_currentState.Value == WorkerState.Returning && _core != null) { Gizmos.color = Color.green; Gizmos.DrawLine(transform.position, _core.transform.position); } #endif } } }