using System; using Unity.Netcode; using UnityEngine; using Northbound.Data; namespace Northbound { public class Building : NetworkBehaviour, IDamageable, IVisionProvider, ITeamMember, IHealthProvider, IInteractable { [Header("References")] public TowerData buildingData; [Header("Runtime Info")] public Vector3Int gridPosition; public int rotation; // 0-3 (0=0°, 1=90°, 2=180°, 3=270°) [Header("Team")] [Tooltip("건물의 팀 (플레이어/적대세력/몬스터/중립)")] public TeamType initialTeam = TeamType.Player; [Header("Ownership (for pre-placed buildings)")] [Tooltip("씬에 미리 배치된 건물의 경우 여기서 소유자 설정 (0 = 중립, 1+ = 플레이어 ID)")] public ulong initialOwnerId = 0; [Tooltip("사전 배치 건물인가요? 체크하면 initialOwnerId를 사용합니다")] public bool useInitialOwner = false; [Header("Health")] public bool showHealthBar = true; public GameObject healthBarPrefab; [Header("Visual Effects")] public GameObject destroyEffectPrefab; public GameObject damageEffectPrefab; public Transform effectSpawnPoint; [Header("Debug")] public bool showGridBounds = true; public Color gridBoundsColor = Color.cyan; // 현재 체력 private NetworkVariable _currentHealth = new NetworkVariable( 0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); // 건물 소유자 (사전 배치 건물 또는 동적 건물 모두 지원) private NetworkVariable _ownerId = new NetworkVariable( 0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); // 건물 팀 private NetworkVariable _team = new NetworkVariable( TeamType.Neutral, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); // 이벤트 public event Action OnHealthChanged; // (current, max) public event Action OnDestroyed; public event Action OnTeamChanged; private UnitHealthBar _healthBar; private float _lastRegenTime; // 업그레이드 관련 private NetworkVariable _isUpgrading = new NetworkVariable( false, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); private NetworkVariable _upgradeProgress = new NetworkVariable( 0f, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); private NetworkVariable _upgradeTargetId = new NetworkVariable( 0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); private float _lastInteractionTime; private const float INTERACTION_COOLDOWN = 0.5f; public override void OnNetworkSpawn() { base.OnNetworkSpawn(); if (IsServer) { // 체력 초기화 if (_currentHealth.Value == 0) { _currentHealth.Value = buildingData != null ? buildingData.maxHealth : 100; } // 팀 초기화 if (_team.Value == TeamType.Neutral) { _team.Value = initialTeam; } // 소유자 초기화 (사전 배치 건물 체크) if (useInitialOwner && _ownerId.Value == 0) { _ownerId.Value = initialOwnerId; // Debug.Log($"[Building] 사전 배치 건물 '{buildingData?.buildingName ?? gameObject.name}' 소유자: {initialOwnerId}, 팀: {_team.Value}"); } else if (!useInitialOwner && _ownerId.Value == 0) { // 동적 생성 건물은 NetworkObject의 Owner 사용 _ownerId.Value = OwnerClientId; } _lastRegenTime = Time.time; // FogOfWar 시스템에 시야 제공자로 등록 if (buildingData != null && buildingData.providesVision) { FogOfWarSystem.Instance?.RegisterVisionProvider(this); } } // 이벤트 구독 _currentHealth.OnValueChanged += OnHealthValueChanged; _team.OnValueChanged += OnTeamValueChanged; _upgradeProgress.OnValueChanged += OnUpgradeProgressChanged; _isUpgrading.OnValueChanged += OnUpgradingChanged; // 체력바 생성 if (showHealthBar && healthBarPrefab != null) { CreateHealthBar(); } // 초기 체력 UI 업데이트 UpdateHealthUI(); UpdateTeamVisuals(); } public override void OnNetworkDespawn() { _currentHealth.OnValueChanged -= OnHealthValueChanged; _team.OnValueChanged -= OnTeamValueChanged; _upgradeProgress.OnValueChanged -= OnUpgradeProgressChanged; _isUpgrading.OnValueChanged -= OnUpgradingChanged; // FogOfWar 시스템에서 제거 if (IsServer && buildingData != null && buildingData.providesVision) { FogOfWarSystem.Instance?.UnregisterVisionProvider(this); } } private void Update() { if (!IsServer || buildingData == null) return; // 자동 체력 회복 if (buildingData.autoRegenerate && _currentHealth.Value < buildingData.maxHealth) { if (Time.time - _lastRegenTime >= 1f) { int regenAmount = Mathf.Min(buildingData.regenPerSecond, buildingData.maxHealth - _currentHealth.Value); _currentHealth.Value += regenAmount; _lastRegenTime = Time.time; } } } /// /// 건물 초기화 (BuildingManager가 동적 생성 시 호출) /// public void Initialize(TowerData data, Vector3Int gridPos, int rot, ulong ownerId, TeamType team = TeamType.Player) { buildingData = data; gridPosition = gridPos; rotation = rot; // 이미 스폰된 경우 if (IsServer && IsSpawned) { _currentHealth.Value = data.maxHealth; _ownerId.Value = ownerId; _team.Value = team; // 시야 제공자 등록 if (data.providesVision) { FogOfWarSystem.Instance?.RegisterVisionProvider(this); } } } /// /// 건물 소유권 변경 (점령 등) /// public void SetOwner(ulong newOwnerId, TeamType newTeam) { if (!IsServer) return; _ownerId.Value = newOwnerId; _team.Value = newTeam; // 시야 제공자 재등록 (소유자가 바뀌었으므로) if (buildingData != null && buildingData.providesVision) { FogOfWarSystem.Instance?.UnregisterVisionProvider(this); FogOfWarSystem.Instance?.RegisterVisionProvider(this); } } #region ITeamMember Implementation public TeamType GetTeam() => _team.Value; public bool IsDead() => _currentHealth.Value <= 0; public void SetTeam(TeamType team) { if (!IsServer) return; _team.Value = team; } private void OnTeamValueChanged(TeamType previousValue, TeamType newValue) { OnTeamChanged?.Invoke(newValue); UpdateTeamVisuals(); } private void UpdateTeamVisuals() { // 팀 색상으로 건물 외곽선이나 이펙트 변경 가능 // 예: Renderer의 emission 색상 변경 Color teamColor = TeamManager.GetTeamColor(_team.Value); // 여기에 실제 비주얼 업데이트 로직 추가 // 예: outline shader, emission, particle system 색상 등 } #endregion #region IVisionProvider Implementation public ulong GetOwnerId() => _ownerId.Value; public float GetVisionRange() { return buildingData != null ? buildingData.visionRange : 0f; } public Transform GetTransform() => transform; public bool IsActive() { // 건물이 스폰되어 있고, 파괴되지 않았으며, 시야 제공 설정이 켜져있어야 함 return IsSpawned && !IsDestroyed() && buildingData != null && buildingData.providesVision; } #endregion #region IDamageable Implementation public void TakeDamage(int damage, ulong attackerId) { if (!IsServer) { // 클라이언트는 서버에 요청 TakeDamageServerRpc(damage, attackerId); return; } // 무적 건물 if (buildingData != null && buildingData.isIndestructible) { return; } // 이미 파괴됨 if (_currentHealth.Value <= 0) return; // 공격자의 팀 확인 (팀 공격 방지) var attackerObj = NetworkManager.Singleton.SpawnManager.SpawnedObjects[attackerId]; var attackerTeamMember = attackerObj?.GetComponent(); if (attackerTeamMember != null) { if (!TeamManager.CanAttack(attackerTeamMember, this)) { return; } } // 데미지 적용 int actualDamage = Mathf.Min(damage, _currentHealth.Value); _currentHealth.Value -= actualDamage; // 데미지 이펙트 ShowDamageEffectClientRpc(); // 체력이 0이 되면 파괴 if (_currentHealth.Value <= 0) { DestroyBuilding(attackerId); } } [Rpc(SendTo.Server)] private void TakeDamageServerRpc(int damage, ulong attackerId) { TakeDamage(damage, attackerId); } #endregion #region Health Management /// /// 건물 파괴 /// private void DestroyBuilding(ulong attackerId) { if (!IsServer) return; // 파괴 이벤트 발생 OnDestroyed?.Invoke(); NotifyDestroyedClientRpc(); // 파괴 이펙트 ShowDestroyEffectClientRpc(); // FogOfWar 시스템에서 제거 if (buildingData != null && buildingData.providesVision) { FogOfWarSystem.Instance?.UnregisterVisionProvider(this); } // BuildingManager에서 제거 if (BuildingManager.Instance != null) { BuildingManager.Instance.RemoveBuilding(this); } // 네트워크 오브젝트 파괴 (약간의 딜레이) Invoke(nameof(DespawnBuilding), 0.5f); } private void DespawnBuilding() { if (IsServer && NetworkObject != null) { NetworkObject.Despawn(true); } } /// /// 체력 회복 /// public void Heal(int amount) { if (!IsServer) return; if (buildingData == null) return; int healAmount = Mathf.Min(amount, buildingData.maxHealth - _currentHealth.Value); _currentHealth.Value += healAmount; } /// /// 현재 체력 /// public int GetCurrentHealth() => _currentHealth.Value; /// /// 최대 체력 /// public int GetMaxHealth() => buildingData != null ? buildingData.maxHealth : 100; /// /// 체력 비율 (0.0 ~ 1.0) /// public float GetHealthPercentage() { int maxHealth = GetMaxHealth(); return maxHealth > 0 ? (float)_currentHealth.Value / maxHealth : 0f; } /// /// 파괴되었는지 여부 /// public bool IsDestroyed() => _currentHealth.Value <= 0; #endregion #region Health UI private void CreateHealthBar() { if (_healthBar != null) return; if (healthBarPrefab == null) { return; } GameObject healthBarObj = Instantiate(healthBarPrefab, transform); _healthBar = healthBarObj.GetComponent(); if (_healthBar != null) { _healthBar.Initialize(this); } } private void UpdateHealthUI() { if (_healthBar != null) { int current = _currentHealth.Value; int max = GetMaxHealth(); _healthBar.UpdateHealth(current, max); } OnHealthChanged?.Invoke(_currentHealth.Value, GetMaxHealth()); } private void OnHealthValueChanged(int previousValue, int newValue) { UpdateHealthUI(); } #endregion #region Visual Effects [ClientRpc] private void ShowDamageEffectClientRpc() { if (damageEffectPrefab != null) { Transform spawnPoint = effectSpawnPoint != null ? effectSpawnPoint : transform; GameObject effect = Instantiate(damageEffectPrefab, spawnPoint.position, spawnPoint.rotation); Destroy(effect, 2f); } } [ClientRpc] private void ShowDestroyEffectClientRpc() { if (destroyEffectPrefab != null) { Transform spawnPoint = effectSpawnPoint != null ? effectSpawnPoint : transform; GameObject effect = Instantiate(destroyEffectPrefab, spawnPoint.position, spawnPoint.rotation); Destroy(effect, 3f); } } [ClientRpc] private void NotifyDestroyedClientRpc() { if (!IsServer) { OnDestroyed?.Invoke(); } } #endregion #region Grid Bounds /// /// Gets the grid-based bounds (from TowerData width/length/height) /// This is used for placement validation, NOT the actual collider bounds /// Bounds are slightly shrunk to allow adjacent buildings to touch /// public Bounds GetGridBounds() { if (buildingData == null) return new Bounds(transform.position, Vector3.one); Vector3 gridSize = buildingData.GetSize(rotation); // Shrink slightly to allow buildings to be adjacent without Intersects() returning true Vector3 shrunkSize = gridSize - Vector3.one * 0.01f; return new Bounds(transform.position + Vector3.up * gridSize.y * 0.5f, shrunkSize); } /// /// Legacy method, use GetGridBounds() instead /// public Bounds GetBounds() { return GetGridBounds(); } #endregion #region IInteractable Implementation (Upgrade) /// /// 상호작용 가능 여부 /// public bool CanInteract(ulong playerId) { // 쿨다운 확인 if (Time.time - _lastInteractionTime < INTERACTION_COOLDOWN) return false; // 같은 팀만 업그레이드 가능 TeamType team = GetPlayerTeam(playerId); if (team != _team.Value) return false; // 업그레이드 중이면 진행 가능 if (_isUpgrading.Value) return true; // 업그레이드 시작 가능 여부 확인 if (buildingData == null || buildingData.upgradeTo == 0) return false; TowerData upgradeData = GetUpgradeData(); if (upgradeData == null) return false; // 자원이 충분한지 확인 var coreResourceManager = CoreResourceManager.Instance; if (coreResourceManager == null || !coreResourceManager.CanAfford(upgradeData.mana)) return false; return true; } /// /// 상호작용 실행 (업그레이드) /// public void Interact(ulong playerId) { if (!CanInteract(playerId)) return; // 서버에 업그레이드 요청 RequestUpgradeServerRpc(playerId); } [Rpc(SendTo.Server)] private void RequestUpgradeServerRpc(ulong playerId) { if (!CanInteract(playerId)) return; _lastInteractionTime = Time.time; // 업그레이드가 시작되지 않았으면 시작만 하고 종료 if (!_isUpgrading.Value) { TowerData upgradeData = GetUpgradeData(); if (upgradeData == null) return; // 자원 확인 및 소비 var coreResourceManager = CoreResourceManager.Instance; if (coreResourceManager == null || !coreResourceManager.CanAfford(upgradeData.mana)) return; coreResourceManager.SpendResources(upgradeData.mana); // 업그레이드 시작 - 토대로 교체 StartUpgradeToFoundation(upgradeData); return; } // 이미 업그레이드 중이면 플레이어의 manpower만큼 진행 float playerManpower = GetPlayerManpower(playerId); _upgradeProgress.Value += playerManpower; // 완료 체크 TowerData targetData = BuildingManager.Instance?.GetTowerDataById(_upgradeTargetId.Value); if (targetData != null && _upgradeProgress.Value >= targetData.manpower) { CompleteUpgrade(targetData); } } /// /// 업그레이드 시작 - 건물을 토대로 교체 /// private void StartUpgradeToFoundation(TowerData upgradeData) { if (!IsServer) return; if (BuildingManager.Instance == null) return; if (BuildingManager.Instance.foundationPrefab == null) return; // 현재 건물 정보 저장 Vector3Int gridPos = gridPosition; int rot = rotation; ulong owner = _ownerId.Value; TeamType team = _team.Value; // BuildingManager에서 건물 제거 BuildingManager.Instance.RemoveBuilding(this); // FogOfWar 시스템에서 제거 if (buildingData != null && buildingData.providesVision) { FogOfWarSystem.Instance?.UnregisterVisionProvider(this); } // 현재 건물 제거 NetworkObject.Despawn(true); // 토대 생성 Vector3 worldPosition = BuildingManager.Instance.GridToWorld(gridPos); GameObject foundationObj = Instantiate(BuildingManager.Instance.foundationPrefab, worldPosition + upgradeData.placementOffset, Quaternion.Euler(0, rot * 90f, 0)); NetworkObject foundationNetObj = foundationObj.GetComponent(); if (foundationNetObj != null) { foundationNetObj.Spawn(); BuildingFoundation foundation = foundationObj.GetComponent(); if (foundation != null) { foundation.Initialize(upgradeData, gridPos, rot, owner, team); BuildingManager.Instance.RegisterFoundation(foundation); } } else { Destroy(foundationObj); } } /// /// 업그레이드 완료 /// private void CompleteUpgrade(TowerData upgradeData) { if (!IsServer) return; BuildingManager.Instance?.UpgradeBuilding(this, upgradeData); } /// /// 상호작용 프롬프트 텍스트 /// public string GetInteractionPrompt() { // 업그레이드 중이면 진행도 표시 if (_isUpgrading.Value) { TowerData targetData = BuildingManager.Instance?.GetTowerDataById(_upgradeTargetId.Value); if (targetData != null) { float percentage = (_upgradeProgress.Value / targetData.manpower) * 100f; return $"[E] Upgrading to {targetData.buildingName} ({percentage:F0}%)"; } return "[E] Upgrading..."; } TowerData upgradeData = GetUpgradeData(); if (upgradeData == null) return ""; var coreResourceManager = CoreResourceManager.Instance; bool canAfford = coreResourceManager != null && coreResourceManager.CanAfford(upgradeData.mana); string costText = canAfford ? $"{upgradeData.mana}" : $"{upgradeData.mana}"; return $"[E] Upgrade to {upgradeData.buildingName} ({costText})"; } public string GetInteractionAnimation() { return "Build"; // 건설 애니메이션 재사용 } public EquipmentData GetEquipmentData() { return buildingData?.constructionEquipment; } Transform IInteractable.GetTransform() { return transform; } /// /// 업그레이드 대상 TowerData 반환 /// public TowerData GetUpgradeData() { if (buildingData == null || buildingData.upgradeTo == 0) return null; if (BuildingManager.Instance == null) return null; return BuildingManager.Instance.GetTowerDataById(buildingData.upgradeTo); } /// /// 플레이어의 팀 가져오기 /// private TeamType GetPlayerTeam(ulong playerId) { if (NetworkManager.Singleton != null && NetworkManager.Singleton.ConnectedClients != null) { if (NetworkManager.Singleton.ConnectedClients.TryGetValue(playerId, out var client)) { if (client.PlayerObject != null) { var teamMember = client.PlayerObject.GetComponent(); if (teamMember != null) { return teamMember.GetTeam(); } } } } return TeamType.Player; } /// /// 플레이어의 manpower 가져오기 /// private float GetPlayerManpower(ulong playerId) { if (NetworkManager.Singleton != null && NetworkManager.Singleton.ConnectedClients != null) { if (NetworkManager.Singleton.ConnectedClients.TryGetValue(playerId, out var client)) { if (client.PlayerObject != null) { var playerInteraction = client.PlayerObject.GetComponent(); if (playerInteraction != null) { return playerInteraction.WorkPower; } } } } return 10f; // 기본값 } private void OnUpgradeProgressChanged(float oldValue, float newValue) { // 업그레이드 진행 UI 업데이트 (필요시 구현) } private void OnUpgradingChanged(bool oldValue, bool newValue) { // 업그레이드 상태 변경 시 처리 (필요시 구현) } #endregion #region Gizmos private void OnDrawGizmos() { if (!showGridBounds || buildingData == null) return; Bounds bounds = GetGridBounds(); // 팀 색상으로 표시 Color teamColor = Application.isPlaying ? TeamManager.GetTeamColor(_team.Value) : TeamManager.GetTeamColor(initialTeam); Gizmos.color = new Color(teamColor.r, teamColor.g, teamColor.b, 0.3f); Gizmos.DrawWireCube(bounds.center, bounds.size); } private void OnDrawGizmosSelected() { if (buildingData == null) return; Bounds bounds = GetGridBounds(); Gizmos.color = Color.yellow; Gizmos.DrawWireCube(bounds.center, bounds.size); // Draw grid position if (BuildingManager.Instance != null) { Vector3 worldPos = BuildingManager.Instance.GridToWorld(gridPosition); Gizmos.color = Color.magenta; Gizmos.DrawSphere(worldPos, 0.2f); } // Draw vision range (if provides vision) if (buildingData.providesVision) { Gizmos.color = Color.cyan; Gizmos.DrawWireSphere(transform.position, buildingData.visionRange); } // Draw team info label #if UNITY_EDITOR if (Application.isPlaying) { string teamName = TeamManager.GetTeamName(_team.Value); UnityEditor.Handles.Label(transform.position + Vector3.up * 3f, $"Owner: {_ownerId.Value}\nTeam: {teamName}"); } else if (useInitialOwner) { string teamName = TeamManager.GetTeamName(initialTeam); UnityEditor.Handles.Label(transform.position + Vector3.up * 3f, $"Initial Owner: {initialOwnerId}\nTeam: {teamName}"); } #endif } #endregion } }