using Northbound; using Northbound.Data; using System.Collections.Generic; using Unity.Netcode; using UnityEngine; public class EnemyPortal : NetworkBehaviour, IDamageable, ITeamMember { [Header("Team Settings")] [Tooltip("포털의 팀 (Hostile 또는 Monster)")] [SerializeField] private TeamType portalTeam = TeamType.Hostile; [Header("Health Settings")] [Tooltip("최대 체력")] [SerializeField] private int maxHealth = 500; [Header("Visual Effects")] [SerializeField] private GameObject damageEffectPrefab; [SerializeField] private GameObject destroyEffectPrefab; [SerializeField] private Transform effectSpawnPoint; private NetworkVariable _currentHealth = new NetworkVariable( 0, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); private NetworkVariable _team = new NetworkVariable( TeamType.Neutral, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); [System.Serializable] public class MonsterEntry { public GameObject prefab; } [Header("Spawn Settings")] [Tooltip("몬스터 프리팹 목록 (Editor에서 자동 로드 가능)")] [SerializeField] private List monsterEntries = new(); [Header("Cost Settings")] [Tooltip("시간당 코스트 시작 값")] [SerializeField] private float initialCost = 4f; [Tooltip("사이클마다 코스트 증가율 (%)")] [SerializeField] private float costIncreaseRate = 10f; private float currentCost; public override void OnNetworkSpawn() { base.OnNetworkSpawn(); if (IsServer) { // 체력 초기화 if (_currentHealth.Value == 0) { _currentHealth.Value = maxHealth; } // 팀 초기화 if (_team.Value == TeamType.Neutral) { _team.Value = portalTeam; } Debug.Log($"[EnemyPortal] 포털 스폰됨 (팀: {TeamManager.GetTeamName(_team.Value)}, 체력: {_currentHealth.Value}/{maxHealth})"); } GlobalTimer.Instance.OnCycleStart += OnCycleStart; } public override void OnNetworkDespawn() { GlobalTimer.Instance.OnCycleStart -= OnCycleStart; // 체력 변경 이벤트 정리 (나중에 추가할 경우 대비) base.OnNetworkDespawn(); } void Start() { currentCost = initialCost; } private void OnCycleStart(int cycleNumber) { SpawnMonsters(); IncreaseCost(); } private void SpawnMonsters() { float remainingCost = currentCost; int spawnedCount = 0; while (remainingCost > 0 && monsterEntries.Count > 0) { MonsterEntry selectedEntry = SelectMonsterByWeight(); MonsterData monsterData = GetMonsterDataFromPrefab(selectedEntry.prefab); if (monsterData == null) { Debug.LogWarning($"[EnemyPortal] Could not find MonsterData on {selectedEntry.prefab.name}"); continue; } if (monsterData.cost > remainingCost) { if (!CanSpawnAnyMonster(remainingCost)) { break; } continue; } SpawnEnemy(selectedEntry.prefab); remainingCost -= monsterData.cost; spawnedCount++; } if (spawnedCount > 0) { Debug.Log($"[EnemyPortal] Spawned {spawnedCount} monsters (Cost used: {currentCost - remainingCost:F2})"); } } private bool CanSpawnAnyMonster(float remainingCost) { foreach (var entry in monsterEntries) { MonsterData monsterData = GetMonsterDataFromPrefab(entry.prefab); if (monsterData != null && monsterData.cost <= remainingCost) { return true; } } return false; } private MonsterEntry SelectMonsterByWeight() { float totalWeight = 0f; foreach (var entry in monsterEntries) { MonsterData monsterData = GetMonsterDataFromPrefab(entry.prefab); if (monsterData != null) { totalWeight += monsterData.weight; } } float randomValue = Random.Range(0f, totalWeight); float cumulativeWeight = 0f; foreach (var entry in monsterEntries) { MonsterData monsterData = GetMonsterDataFromPrefab(entry.prefab); if (monsterData != null) { cumulativeWeight += monsterData.weight; if (randomValue <= cumulativeWeight) { return entry; } } } return monsterEntries[0]; } private MonsterData GetMonsterDataFromPrefab(GameObject prefab) { if (prefab == null) return null; MonsterDataComponent component = prefab.GetComponent(); if (component != null) { return component.monsterData; } return null; } private void SpawnEnemy(GameObject prefab) { if (!IsServer) return; GameObject enemy = Instantiate(prefab, transform); if (enemy.GetComponent() == null) { var visibility = enemy.AddComponent(); visibility.showInExploredAreas = false; visibility.updateInterval = 0.2f; } var netObj = enemy.GetComponent(); netObj.Spawn(true); Debug.Log($"[EnemyPortal] {enemy.name} 스폰됨 - OwnerClientId: {netObj.OwnerClientId}, IsServer: {IsServer}"); } private void IncreaseCost() { currentCost *= (1f + costIncreaseRate / 100f); } #region ITeamMember Implementation public TeamType GetTeam() => _team.Value; public void SetTeam(TeamType team) { if (!IsServer) return; _team.Value = team; } #endregion #region IDamageable Implementation public void TakeDamage(int damage, ulong attackerId) { if (!IsServer) return; // 이미 파괴됨 if (_currentHealth.Value <= 0) return; // 공격자의 팀 확인 if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(attackerId, out NetworkObject attackerObj)) { var attackerTeamMember = attackerObj.GetComponent(); if (attackerTeamMember != null) { if (!TeamManager.CanAttack(attackerTeamMember, this)) { Debug.Log($"[EnemyPortal] {TeamManager.GetTeamName(attackerTeamMember.GetTeam())} 팀은 {TeamManager.GetTeamName(_team.Value)} 팀 포털을 공격할 수 없습니다."); return; } } } // 데미지 적용 int actualDamage = Mathf.Min(damage, _currentHealth.Value); _currentHealth.Value -= actualDamage; Debug.Log($"[EnemyPortal] 포털이 {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{maxHealth}"); // 데미지 이펙트 ShowDamageEffectClientRpc(); // 체력이 0이 되면 파괴 if (_currentHealth.Value <= 0) { DestroyPortal(attackerId); } } private void DestroyPortal(ulong attackerId) { if (!IsServer) return; Debug.Log($"[EnemyPortal] 포털이 파괴되었습니다! (공격자: {attackerId})"); // 파괴 이펙트 ShowDestroyEffectClientRpc(); // 몬스터 스폰 중지 (이벤트 구독 해제) GlobalTimer.Instance.OnCycleStart -= OnCycleStart; // 네트워크 오브젝트 파괴 (약간의 딜레이) Invoke(nameof(DespawnPortal), 1.0f); } private void DespawnPortal() { if (IsServer && NetworkObject != null) { NetworkObject.Despawn(true); } } [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); } } #endregion }