using Unity.Netcode; using UnityEngine; using System.Collections.Generic; namespace Northbound { /// /// 상호작용 대상 - 자원 채집 /// public class Resource : NetworkBehaviour, IInteractable { [Header("Resource Settings")] public int maxResources = 100; public int resourcesPerGathering = 10; public float gatheringCooldown = 2f; public string resourceName = "광석"; [Header("Resource Recharge")] public float rechargeInterval = 5f; // 충전 주기 (초) public int rechargeAmount = 10; // 주기당 충전량 [Header("Quality (Runtime)")] [SerializeField] private NetworkVariable _qualityPercentage = new NetworkVariable( 0f, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); [Tooltip("품질 보정율 (-30% ~ +30%)")] [SerializeField] private float _displayQuality = 0f; [Tooltip("품질 적용 후 최대 자원량")] [SerializeField] private int _displayMaxResources = 100; [Tooltip("품질 적용 후 충전량")] [SerializeField] private int _displayRechargeAmount = 10; private bool _isQualityInitialized = false; [Header("Animation")] public string interactionAnimationTrigger = "Mining"; // 플레이어 애니메이션 트리거 [Header("Equipment")] public EquipmentData equipmentData = new EquipmentData { socketName = "RightHand", equipmentPrefab = null, // Inspector에서 곡괭이 프리팹 할당 attachOnStart = true, detachOnEnd = true }; [Header("Visual")] public GameObject gatheringEffectPrefab; public Transform effectSpawnPoint; [Header("Worker Assignment")] public bool allowWorkerAssignment = true; [Header("Multi-worker")] public bool allowMultipleWorkers = true; // 한 자원에 여러 워커 허용 여부 public bool HasResourcesAvailable() { return _currentResources.Value > 0; } public bool CanWorkerMineResource(ulong workerId) { if (!HasResourcesAvailable()) return false; if (allowMultipleWorkers) return true; if (_currentWorkerId.Value == ulong.MaxValue) return true; return _currentWorkerId.Value == workerId; } public int TakeResourcesForWorker(int amount, ulong workerId) { if (!IsServer) return 0; if (!allowMultipleWorkers) { if (_currentWorkerId.Value != ulong.MaxValue && _currentWorkerId.Value != workerId) return 0; } int availableResources = _currentResources.Value; int actualAmount = Mathf.Min(amount, availableResources); if (actualAmount <= 0) return 0; _currentResources.Value -= actualAmount; _lastGatheringTime = Time.time; if (!allowMultipleWorkers && actualAmount > 0) { _currentWorkerId.Value = workerId; } if (_currentResources.Value <= 0 && !allowMultipleWorkers) { _currentWorkerId.Value = ulong.MaxValue; } ShowGatheringEffectClientRpc(); return actualAmount; } private NetworkVariable _currentResources = new NetworkVariable( 0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); private NetworkVariable _currentWorkerId = new NetworkVariable( ulong.MaxValue, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); private float _lastGatheringTime; private float _lastRechargeTime; public float QualityPercentage => _qualityPercentage.Value; public int ActualMaxResources { get { float multiplier = 1f + (_qualityPercentage.Value / 100f); return Mathf.RoundToInt(maxResources * multiplier); } } public int ActualRechargeAmount { get { float multiplier = 1f + (_qualityPercentage.Value / 100f); return Mathf.RoundToInt(rechargeAmount * multiplier); } } public override void OnNetworkSpawn() { _qualityPercentage.OnValueChanged += OnQualityChanged; if (IsServer) { if (!_isQualityInitialized) { _qualityPercentage.Value = Random.Range(-30f, 30f); } _currentResources.Value = ActualMaxResources; _lastRechargeTime = Time.time; _lastGatheringTime = Time.time - gatheringCooldown; } _displayQuality = _qualityPercentage.Value; UpdateDisplayValues(); } public void InitializeQuality(float qualityPercentage) { if (IsServer) { _qualityPercentage.Value = qualityPercentage; _displayQuality = qualityPercentage; _isQualityInitialized = true; } else if (IsClient) { _displayQuality = qualityPercentage; UpdateDisplayValues(); } } public override void OnNetworkDespawn() { _qualityPercentage.OnValueChanged -= OnQualityChanged; } private void OnQualityChanged(float previous, float current) { _displayQuality = current; UpdateDisplayValues(); } private void UpdateDisplayValues() { if (IsClient || IsServer) { _displayMaxResources = ActualMaxResources; _displayRechargeAmount = ActualRechargeAmount; } else { _displayMaxResources = maxResources; _displayRechargeAmount = rechargeAmount; } } private void Update() { if (!IsServer) return; // 자원 충전 로직 if (Time.time - _lastRechargeTime >= rechargeInterval) { if (_currentResources.Value < ActualMaxResources) { int rechargeAmountToAdd = Mathf.Min(ActualRechargeAmount, ActualMaxResources - _currentResources.Value); _currentResources.Value += rechargeAmountToAdd; // Debug.Log($"{resourceName} {rechargeAmountToAdd} 충전됨. 현재: {_currentResources.Value}/{ActualMaxResources}"); } _lastRechargeTime = Time.time; } } public bool CanInteract(ulong playerId) { if (_currentResources.Value <= 0) return false; if (Time.time - _lastGatheringTime < gatheringCooldown) return false; // 플레이어의 자원 공간 확인 int availableSpace = GetPlayerAvailableSpace(playerId); if (availableSpace <= 0) return false; return true; } /// /// 플레이어의 자원 공간 확인 (서버와 클라이언트 모두 지원) /// private int GetPlayerAvailableSpace(ulong playerId) { // 서버에서는 ServerResourceManager 사용 var resourceManager = ServerResourceManager.Instance; if (resourceManager != null) { return resourceManager.GetAvailableSpace(playerId); } // 클라이언트에서는 PlayerResourceInventory 사용 if (NetworkManager.Singleton != null) { var spawnedObjects = NetworkManager.Singleton.SpawnManager.SpawnedObjects; foreach (var kvp in spawnedObjects) { var controller = kvp.Value.GetComponent(); if (controller != null && controller.OwnerPlayerId == playerId) { var inventory = kvp.Value.GetComponent(); if (inventory != null) { return inventory.MaxResourceCapacity - inventory.CurrentResourceAmount; } } } } return 0; } public void Interact(ulong playerId) { AssignOrGatherResourceServerRpc(playerId, NetworkObject.NetworkObjectId); } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] private void AssignOrGatherResourceServerRpc(ulong playerId, ulong resourceId) { if (!IsServer) return; bool workerAssigned = false; if (allowWorkerAssignment) { // Find worker owned by player in Following state (server-side, not client-side) Worker assignedWorker = FindWorkerForPlayer(playerId); if (assignedWorker != null) { if ((int)assignedWorker.CurrentState == 1) // 1 = Following { assignedWorker.AssignMiningTargetServerRpc(resourceId); workerAssigned = true; ShowGatheringEffectClientRpc(); } } } if (!workerAssigned) { if (!CanInteract(playerId)) { return; } GatherResource(playerId); } } private void GatherResource(ulong playerId) { Debug.Log($"[Resource] GatherResource called - IsServer: {IsServer}, OwnerClientId: {OwnerClientId}, Current: {_currentResources.Value}"); if (!IsServer) { Debug.LogError($"[Resource] GatherResource called on CLIENT! This should not happen!"); return; } if (!CanInteract(playerId)) return; var resourceManager = ServerResourceManager.Instance; if (resourceManager == null) return; int playerAvailableSpace = resourceManager.GetAvailableSpace(playerId); int gatheredAmount = Mathf.Min( resourcesPerGathering, _currentResources.Value, playerAvailableSpace ); if (gatheredAmount <= 0) { return; } _currentResources.Value -= gatheredAmount; _lastGatheringTime = Time.time; resourceManager.AddResource(playerId, gatheredAmount); UpdatePlayerResourcesClientRpc(playerId); ShowGatheringEffectClientRpc(); } [Rpc(SendTo.ClientsAndHost)] private void UpdatePlayerResourcesClientRpc(ulong playerId) { // 해당 플레이어만 업데이트 if (NetworkManager.Singleton.LocalClientId != playerId) return; // 로컬 플레이어의 PlayerResourceInventory 찾아서 업데이트 요청 var spawnedObjects = NetworkManager.Singleton.SpawnManager.SpawnedObjects; foreach (var kvp in spawnedObjects) { var controller = kvp.Value.GetComponent(); if (controller != null && controller.IsLocalPlayer) { var inventory = kvp.Value.GetComponent(); if (inventory != null) { inventory.RequestResourceUpdateServerRpc(playerId); return; } } } } [Rpc(SendTo.NotServer)] private void ShowGatheringEffectClientRpc() { if (gatheringEffectPrefab != null && effectSpawnPoint != null) { GameObject effect = Instantiate(gatheringEffectPrefab, effectSpawnPoint.position, effectSpawnPoint.rotation); Destroy(effect, 2f); } } public string GetInteractionPrompt() { if (_currentResources.Value <= 0) return "Recharging..."; return $"[E] {resourceName} Mining ({_currentResources.Value}/{ActualMaxResources})"; } public string GetInteractionAnimation() { return interactionAnimationTrigger; } public EquipmentData GetEquipmentData() { return equipmentData; } public Transform GetTransform() { return transform; } private Worker FindWorkerForPlayer(ulong playerId) { if (NetworkManager.Singleton == null || NetworkManager.Singleton.SpawnManager == null) { return null; } var spawnedObjects = NetworkManager.Singleton.SpawnManager.SpawnedObjects; int workersFound = 0; foreach (var kvp in spawnedObjects) { var networkObj = kvp.Value; var worker = networkObj.GetComponent(); if (worker != null) { workersFound++; // Use worker's internal OwnerPlayerId, NOT NetworkObject.OwnerClientId! if (worker.OwnerPlayerId == playerId && (int)worker.CurrentState == 1) // 1 = Following { return worker; } } } return null; } } }