using Unity.Netcode; using UnityEngine; using UnityEngine.AI; namespace Northbound { /// /// 플레이어가 자원을 건내받아 게임의 전역 자원으로 관리하는 중앙 허브 /// public class Core : NetworkBehaviour, IInteractable, IDamageable, ITeamMember, IVisionProvider { [Header("Core Settings")] public int maxStorageCapacity = 1000; // 코어의 최대 저장 용량 public bool unlimitedStorage = false; // 무제한 저장소 [Header("Vision")] public float visionRange = 15f; // 코어가 제공하는 시야 범위 [Header("Health")] public int maxHealth = 1000; public GameObject damageEffectPrefab; public GameObject destroyEffectPrefab; [Header("Deposit Settings")] public bool depositAll = true; // true: 전부 건네기, false: 일부만 건네기 public int depositAmountPerInteraction = 10; // depositAll이 false일 때 한 번에 건네는 양 [Header("Animation")] public string interactionAnimationTrigger = "Deposit"; // 플레이어 애니메이션 트리거 [Header("Equipment")] public EquipmentData equipmentData = null; // 도구 필요 없음 [Header("Visual")] public GameObject depositEffectPrefab; public Transform effectSpawnPoint; private NetworkVariable _totalResources = new NetworkVariable( 0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); private NetworkVariable _currentHealth = new NetworkVariable( 0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); public int TotalResources => _totalResources.Value; public int MaxStorageCapacity => maxStorageCapacity; public int CurrentHealth => _currentHealth.Value; public int MaxHealth => maxHealth; public override void OnNetworkSpawn() { if (IsServer) { _totalResources.Value = 0; _currentHealth.Value = maxHealth; // 시야 제공자로 등록 FogOfWarSystem.Instance?.RegisterVisionProvider(this); } _currentHealth.OnValueChanged += OnHealthChanged; } public override void OnNetworkDespawn() { if (IsServer) { // 시야 제공자 등록 해제 FogOfWarSystem.Instance?.UnregisterVisionProvider(this); } _currentHealth.OnValueChanged -= OnHealthChanged; } private void OnHealthChanged(int previousValue, int newValue) { } #region ITeamMember Implementation public bool IsDead() => _currentHealth.Value <= 0; public TeamType GetTeam() { return TeamType.Player; // 코어는 플레이어 팀 } public void SetTeam(TeamType team) { // 코어의 팀은 변경할 수 없음 (항상 플레이어 팀) Debug.LogWarning("[Core] 코어의 팀은 변경할 수 없습니다."); } #endregion #region IDamageable Implementation public void TakeDamage(int damage, ulong attackerId) { if (!IsServer) return; if (_currentHealth.Value <= 0) return; int actualDamage = Mathf.Min(damage, _currentHealth.Value); _currentHealth.Value -= actualDamage; Debug.Log($"[Core] 코어가 {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{maxHealth}"); // 데미지 이펙트 ShowDamageEffectClientRpc(); // 체력이 0이 되면 게임 오버 if (_currentHealth.Value <= 0) { OnCoreDestroyed(); } } private void OnCoreDestroyed() { if (!IsServer) return; // 파괴 이펙트 ShowDestroyEffectClientRpc(); // 게임 오버 로직 (추후 구현) // GameManager.Instance?.OnGameOver(); } [Rpc(SendTo.ClientsAndHost)] private void ShowDamageEffectClientRpc() { if (damageEffectPrefab != null) { GameObject effect = Instantiate(damageEffectPrefab, transform.position + Vector3.up * 2f, Quaternion.identity); Destroy(effect, 2f); } } [Rpc(SendTo.ClientsAndHost)] private void ShowDestroyEffectClientRpc() { if (destroyEffectPrefab != null) { GameObject effect = Instantiate(destroyEffectPrefab, transform.position, Quaternion.identity); Destroy(effect, 5f); } } #endregion #region Resource Management /// /// 자원을 소비할 수 있는지 확인 /// public bool CanConsumeResource(int amount) { return _totalResources.Value >= amount; } /// /// 자원 소비 (서버에서만) /// [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] public void ConsumeResourceServerRpc(int amount) { if (!CanConsumeResource(amount)) { Debug.LogWarning($"[Core] 자원이 부족합니다. 필요: {amount}, 보유: {_totalResources.Value}"); return; } int previousAmount = _totalResources.Value; _totalResources.Value -= amount; } /// /// 자원 추가 (서버에서만) /// public void AddResource(int amount) { if (!IsServer) return; if (!unlimitedStorage) { int availableSpace = maxStorageCapacity - _totalResources.Value; amount = Mathf.Min(amount, availableSpace); } if (amount > 0) { _totalResources.Value += amount; } } #endregion #region IInteractable Implementation public bool CanInteract(ulong playerId) { // 저장소가 가득 찼는지 확인 (무제한이 아닐 때) if (!unlimitedStorage && _totalResources.Value >= maxStorageCapacity) return false; // 플레이어가 자원을 가지고 있는지 확인 // NetworkPlayerController로 찾기 (서버 소유권으로 스폰한 경우 PlayerObject가 null일 수 있음) var spawnedObjects = NetworkManager.Singleton.SpawnManager.SpawnedObjects; foreach (var kvp in spawnedObjects) { var controller = kvp.Value.GetComponent(); if (controller != null && controller.OwnerPlayerId == playerId) { var playerInventory = kvp.Value.GetComponent(); if (playerInventory != null) { return playerInventory.CurrentResourceAmount > 0; } } } return false; } public void Interact(ulong playerId) { if (!CanInteract(playerId)) return; DepositResourceServerRpc(playerId); } public string GetInteractionPrompt() { if (unlimitedStorage) { return "[E] Deposit (No Limit)"; } else { return $"[E] Deposit ({_totalResources.Value}/{maxStorageCapacity})"; } } public string GetInteractionAnimation() { return interactionAnimationTrigger; } public EquipmentData GetEquipmentData() { return equipmentData; } public Transform GetTransform() { return transform; } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] private void DepositResourceServerRpc(ulong playerId) { if (!CanInteract(playerId)) return; var resourceManager = ServerResourceManager.Instance; if (resourceManager == null) { Debug.LogWarning("ServerResourceManager 인스턴스를 찾을 수 없습니다."); return; } int playerResourceAmount = resourceManager.GetPlayerResourceAmount(playerId); if (playerResourceAmount <= 0) { return; } int depositAmount; if (depositAll) { depositAmount = playerResourceAmount; } else { depositAmount = Mathf.Min(depositAmountPerInteraction, playerResourceAmount); } if (!unlimitedStorage) { int availableSpace = maxStorageCapacity - _totalResources.Value; depositAmount = Mathf.Min(depositAmount, availableSpace); } if (depositAmount <= 0) { return; } resourceManager.RemoveResource(playerId, depositAmount); UpdatePlayerResourcesClientRpc(playerId); _totalResources.Value += depositAmount; ShowDepositEffectClientRpc(); } [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.ClientsAndHost)] private void ShowDepositEffectClientRpc() { if (depositEffectPrefab != null && effectSpawnPoint != null) { GameObject effect = Instantiate(depositEffectPrefab, effectSpawnPoint.position, effectSpawnPoint.rotation); Destroy(effect, 2f); } } #endregion #region Navigation /// /// AI가 코어에 접근할 수 있는 NavMesh 위치 반환 (콜라이더 표면 근처) /// public Vector3 GetNavMeshPosition() { Collider coreCollider = GetComponent(); if (coreCollider == null) { return transform.position; } // 1. 코어 콜라이더 표면에서 남쪽(앞쪽) 방향으로 검색 시작 // 적들이 보통 남쪽에서 오기 때문에 전면부 검색 Vector3 searchDirection = Vector3.back; // -Z 방향 (남쪽) Vector3 searchCenter = coreCollider.ClosestPoint(transform.position + searchDirection * 5f); // 2. 표면 방향으로 attackRange(약 2m)보다 조금 더 큰 거리에서 NavMesh 샘플링 NavMeshHit hit; float maxDistance = 3f; // 콜라이더 표면에서 탐색할 거리 if (NavMesh.SamplePosition(searchCenter, out hit, maxDistance, NavMesh.AllAreas)) { return hit.position; } // 3. 실패 시 코어 중심에서 더 넓게 탐색 (fallback) if (NavMesh.SamplePosition(transform.position, out hit, 10f, NavMesh.AllAreas)) { return hit.position; } Debug.LogWarning("[Core] 코어 주변에서 NavMesh를 찾을 수 없습니다."); return transform.position; } #endregion #region IVisionProvider Implementation public ulong GetOwnerId() => 0; // 코어는 모든 플레이어에게 시야 제공 public float GetVisionRange() => visionRange; Transform IVisionProvider.GetTransform() => transform; public bool IsActive() => IsSpawned && !IsDead(); TeamType IVisionProvider.GetTeam() => TeamType.Player; #endregion } }