using System; using Unity.Netcode; using UnityEngine; namespace Northbound { public class Building : DamageableNetworkBehaviour, IVisionProvider, ITeamMember { [Header("Building Data")] public BuildingData buildingData; [Header("Runtime Info")] public Vector3Int gridPosition; public int rotation; // 0-3 (0=0°, 1=90°, 2=180°, 3=270°) [Header("Team")] [Tooltip("Building team (Player/Enemy/Monster/Neutral)")] public TeamType initialTeam = TeamType.Player; [Header("Ownership (for pre-placed buildings)")] [Tooltip("For pre-placed buildings, set owner here (0 = neutral, 1+ = player ID)")] public ulong initialOwnerId = 0; [Tooltip("Is this a pre-placed building? If checked, uses initialOwnerId")] public bool useInitialOwner = false; [Header("Health Bar")] public GameObject healthBarPrefab; [Header("Debug")] public bool showGridBounds = true; public Color gridBoundsColor = Color.cyan; private NetworkVariable _ownerId = new NetworkVariable( 0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); private NetworkVariable _team = new NetworkVariable( TeamType.Neutral, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); public event Action OnTeamChanged; private BuildingHealthBar _healthBar; private float _lastRegenTime; private bool _isInitialized = false; public override void OnNetworkSpawn() { base.OnNetworkSpawn(); if (IsOwner) { if (maxHealth == 0) { maxHealth = buildingData != null ? buildingData.maxHealth : 100; _currentHealth.Value = maxHealth; } if (_team.Value == TeamType.Neutral) { _team.Value = initialTeam; } if (useInitialOwner && _ownerId.Value == 0) { _ownerId.Value = initialOwnerId; } else if (!useInitialOwner && _ownerId.Value == 0) { _ownerId.Value = OwnerClientId; } _lastRegenTime = Time.time; if (buildingData != null && buildingData.providesVision) { FogOfWarSystem.Instance?.RegisterVisionProvider(this); } } _team.OnValueChanged += OnTeamValueChanged; if (showHealthBar && healthBarPrefab != null) { base.InitializeHealthBar(); } UpdateHealthUI(); UpdateTeamVisuals(); } public override void OnNetworkDespawn() { base.OnNetworkDespawn(); _team.OnValueChanged -= OnTeamValueChanged; if (IsOwner && buildingData != null && buildingData.providesVision) { FogOfWarSystem.Instance?.UnregisterVisionProvider(this); } } private void Update() { if (!IsOwner || buildingData == null) return; if (buildingData.autoRegenerate && _currentHealth.Value < maxHealth) { if (Time.time - _lastRegenTime >= 1f) { int regenAmount = Mathf.Min(buildingData.regenPerSecond, maxHealth - _currentHealth.Value); _currentHealth.Value += regenAmount; _lastRegenTime = Time.time; } } } public void Initialize(BuildingData data, Vector3Int gridPos, int rot, ulong ownerId, TeamType team = TeamType.Player) { buildingData = data; gridPosition = gridPos; rotation = rot; if (IsOwner && IsSpawned) { maxHealth = data.maxHealth; _currentHealth.Value = maxHealth; _ownerId.Value = ownerId; _team.Value = team; if (data.providesVision) { FogOfWarSystem.Instance?.RegisterVisionProvider(this); } } _isInitialized = true; } public void SetOwner(ulong newOwnerId, TeamType newTeam) { if (!IsOwner) { SetOwnerServerRpc(newOwnerId, newTeam); return; } SetOwnerServerRpc(newOwnerId, newTeam); } [ServerRpc] private void SetOwnerServerRpc(ulong newOwnerId, TeamType newTeam) { ulong previousOwner = _ownerId.Value; TeamType previousTeam = _team.Value; _ownerId.Value = newOwnerId; _team.Value = newTeam; Debug.Log($"[Building] {buildingData?.buildingName ?? "Building"} ownership changed: {previousOwner} → {newOwnerId}, team: {TeamManager.GetTeamName(previousTeam)} → {TeamManager.GetTeamName(newTeam)}"); if (buildingData != null && buildingData.providesVision) { FogOfWarSystem.Instance?.UnregisterVisionProvider(this); FogOfWarSystem.Instance?.RegisterVisionProvider(this); } } #region ITeamMember Implementation public TeamType GetTeam() => _team.Value; public void SetTeam(TeamType team) { if (!IsOwner) { SetTeamServerRpc(team); return; } SetTeamServerRpc(team); } [ServerRpc] private void SetTeamServerRpc(TeamType team) { _team.Value = team; } private void OnTeamValueChanged(TeamType previousValue, TeamType newValue) { OnTeamChanged?.Invoke(newValue); UpdateTeamVisuals(); Debug.Log($"[Building] {buildingData?.buildingName ?? "Building"} team changed: {TeamManager.GetTeamName(previousValue)} → {TeamManager.GetTeamName(newValue)}"); } private void UpdateTeamVisuals() { Color teamColor = TeamManager.GetTeamColor(_team.Value); } #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 && !IsDead() && buildingData != null && buildingData.providesVision; } #endregion #region IDamageable Overrides protected override void Die(ulong killerId) { base.Die(killerId); if (!IsOwner) return; Debug.Log($"[Building] {buildingData?.buildingName ?? "Building"} ({TeamManager.GetTeamName(_team.Value)}) destroyed! Attacker: {killerId}"); InvokeOnDestroyed(); NotifyDestroyedClientRpc(); if (buildingData != null && buildingData.providesVision) { FogOfWarSystem.Instance?.UnregisterVisionProvider(this); } if (BuildingManager.Instance != null) { BuildingManager.Instance.RemoveBuilding(this); } Invoke(nameof(DespawnBuilding), 0.5f); } private void DespawnBuilding() { if (IsOwner && NetworkObject != null) { NetworkObject.Despawn(true); } } [ClientRpc] private void NotifyDestroyedClientRpc() { if (!IsOwner) { InvokeOnDestroyed(); } } public override void TakeDamage(int damage, ulong attackerId) { if (!IsOwner) { TakeDamageOwnerRpc(damage, attackerId); return; } if (buildingData != null && buildingData.isIndestructible) { Debug.Log($"[Building] {buildingData.buildingName} is indestructible."); return; } var attackerObj = NetworkManager.Singleton.SpawnManager.SpawnedObjects[attackerId]; var attackerTeamMember = attackerObj?.GetComponent(); if (attackerTeamMember != null) { if (!TeamManager.CanAttack(attackerTeamMember, this)) { Debug.Log($"[Building] {TeamManager.GetTeamName(attackerTeamMember.GetTeam())} team cannot attack {TeamManager.GetTeamName(_team.Value)} team."); return; } } base.TakeDamage(damage, attackerId); } [Rpc(SendTo.Owner)] private void TakeDamageOwnerRpc(int damage, ulong attackerId) { TakeDamage(damage, attackerId); } #endregion #region Health Management public new int GetMaxHealth() { return buildingData != null ? buildingData.maxHealth : 100; } public new float GetHealthPercentage() { int maxHp = GetMaxHealth(); return maxHp > 0 ? (float)_currentHealth.Value / maxHp : 0f; } public bool IsDestroyed() => _currentHealth.Value <= 0; #endregion #region Health UI protected override void InitializeHealthBar() { if (_healthBar != null) return; if (healthBarPrefab != null) { GameObject healthBarObj = Instantiate(healthBarPrefab, transform); _healthBar = healthBarObj.GetComponent(); if (_healthBar != null) { _healthBar.Initialize(this); } } } protected override void UpdateHealthUI() { if (_healthBar != null) { _healthBar.UpdateHealth(_currentHealth.Value, GetMaxHealth()); } InvokeOnHealthChanged(_currentHealth.Value, GetMaxHealth()); } #endregion #region Grid Bounds public Bounds GetGridBounds() { if (buildingData == null) return new Bounds(transform.position, Vector3.one); Vector3 gridSize = buildingData.GetSize(rotation); Vector3 shrunkSize = gridSize - Vector3.one * 0.01f; return new Bounds(transform.position + Vector3.up * gridSize.y * 0.5f, shrunkSize); } public Bounds GetBounds() { return GetGridBounds(); } #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); Gizmos.color = teamColor; Gizmos.DrawWireSphere(transform.position + Vector3.up * 2f, 0.5f); UnityEditor.Handles.Label(transform.position + Vector3.up * 3f, $"{buildingData.buildingName}\nHP: {_currentHealth.Value}/{maxHealth}"); } #endregion } }