using Northbound.Data; using System.Collections.Generic; using Unity.Netcode; using UnityEngine; namespace Northbound { public class CreepCamp : NetworkBehaviour { [Header("Camp Settings")] [Tooltip("Creep prefabs available to spawn")] [SerializeField] private List creepPrefabs = new List(); [Header("Reward Settings")] [Tooltip("Resource pickup prefab to spawn when all creeps are defeated")] [SerializeField] private GameObject resourcePickupPrefab; [Tooltip("Base resource amount multiplier (actual amount = this * camp strength)")] [SerializeField] private int baseResourceAmount = 50; private float _zPosition; private float _campStrength; private float _campCostBudget; private float _spawnRadius; private int _maxSpawnAttempts; private readonly List _spawnedCreeps = new List(); private ResourcePickup _resourcePickup; private readonly Dictionary> _deathHandlers = new Dictionary>(); // 중복 경고 방지용 private GameObject _lastAlertTarget; private float _lastAlertTime; private const float ALERT_COOLDOWN = 0.5f; public override void OnNetworkSpawn() { if (IsServer) { SpawnCreeps(); } } public override void OnNetworkDespawn() { base.OnNetworkDespawn(); if (IsServer) { // 모든 이벤트 구독 해제 foreach (var kvp in _deathHandlers) { if (kvp.Key != null && kvp.Value != null) { kvp.Key.OnDeath -= kvp.Value; } } // 리스트와 딕셔너리 비우기 _spawnedCreeps.Clear(); _deathHandlers.Clear(); } } public void InitializeCamp(float zPosition, float strengthMultiplier, float costBudget, float radius, int maxAttempts) { _zPosition = zPosition; _campStrength = strengthMultiplier; _campCostBudget = costBudget; _spawnRadius = radius; _maxSpawnAttempts = maxAttempts; } public void SetCreepPrefabs(List prefabs) { creepPrefabs.Clear(); creepPrefabs.AddRange(prefabs); } private void SpawnCreeps() { if (creepPrefabs.Count == 0) { return; } // 리소스 픽업 스폰 (비활성화 상태로) SpawnResourcePickup(); float remainingCost = _campCostBudget * _campStrength; int spawnedCount = 0; for (int attempt = 0; attempt < _maxSpawnAttempts && remainingCost > 0; attempt++) { GameObject selectedCreep = SelectCreepByCost(remainingCost); if (selectedCreep == null) { break; } CreepData creepData = GetCreepDataFromPrefab(selectedCreep); if (creepData == null) { continue; } SpawnCreep(selectedCreep); remainingCost -= creepData.cost; spawnedCount++; } } private GameObject SelectCreepByCost(float remainingCost) { List affordableCreeps = new List(); foreach (var prefab in creepPrefabs) { CreepData creepData = GetCreepDataFromPrefab(prefab); if (creepData != null && creepData.cost <= remainingCost) { affordableCreeps.Add(prefab); } } if (affordableCreeps.Count == 0) { return null; } float totalWeight = 0f; foreach (var prefab in affordableCreeps) { CreepData creepData = GetCreepDataFromPrefab(prefab); if (creepData != null) { totalWeight += creepData.weight; } } if (totalWeight == 0f) { return affordableCreeps[Random.Range(0, affordableCreeps.Count)]; } float randomValue = Random.Range(0f, totalWeight); float cumulativeWeight = 0f; foreach (var prefab in affordableCreeps) { CreepData creepData = GetCreepDataFromPrefab(prefab); if (creepData != null) { cumulativeWeight += creepData.weight; if (randomValue <= cumulativeWeight) { return prefab; } } } return affordableCreeps[Random.Range(0, affordableCreeps.Count)]; } private bool CanSpawnAnyCreep(float remainingCost) { foreach (var prefab in creepPrefabs) { CreepData creepData = GetCreepDataFromPrefab(prefab); if (creepData != null && creepData.cost <= remainingCost) { return true; } } return false; } private void SpawnResourcePickup() { if (resourcePickupPrefab == null) { return; } GameObject pickup = Instantiate(resourcePickupPrefab, transform.position, Quaternion.identity); _resourcePickup = pickup.GetComponent(); if (_resourcePickup == null) { Destroy(pickup); return; } // 캠프 강도에 비례하여 리소스 양 설정 _resourcePickup.resourceAmount = Mathf.RoundToInt(baseResourceAmount * _campStrength); // NetworkObject 추가 및 스폰 NetworkObject networkObj = pickup.GetComponent(); if (networkObj == null) { networkObj = pickup.AddComponent(); } networkObj.SpawnWithOwnership(NetworkManager.Singleton.LocalClientId); // 비활성화는 ServerRpc를 통해 처리 DisablePickupClientRpc(); } private void HandleCreepDeath(EnemyUnit deadCreep) { // 이벤트 구독 해제 (메모리 누수 방지) if (_deathHandlers.ContainsKey(deadCreep)) { if (deadCreep != null) { deadCreep.OnDeath -= _deathHandlers[deadCreep]; } _deathHandlers.Remove(deadCreep); } // 리스트에서 해당 creep 제거 _spawnedCreeps.Remove(deadCreep); // 모든 creep이 처치되었으면 ResourcePickup 활성화 if (_spawnedCreeps.Count == 0 && _resourcePickup != null) { EnableResourcePickupClientRpc(); } } /// /// 캠프 내 모든 크립에게 경고 전파 /// /// 감지된 타겟 /// 경고를 보낸 크립 (자신에게는 다시 보내지 않음) public void AlertAllCreeps(GameObject target, EnemyAIController alertingCreep) { if (!IsServer) return; if (target == null) return; // 중복 경고 방지 (짧은 시간 내 같은 타겟에 대한 경고 무시) if (_lastAlertTarget == target && Time.time - _lastAlertTime < ALERT_COOLDOWN) { return; } _lastAlertTarget = target; _lastAlertTime = Time.time; // 캠프 내 모든 크립에게 경고 전파 foreach (var enemyUnit in _spawnedCreeps) { if (enemyUnit == null) continue; EnemyAIController aiController = enemyUnit.GetComponent(); if (aiController == null) continue; // 경고를 보낸 크립은 제외 (이미 타겟을 설정했음) if (aiController == alertingCreep) continue; aiController.ReceiveAlert(target); } } [Rpc(SendTo.ClientsAndHost)] private void EnableResourcePickupClientRpc() { if (_resourcePickup != null) { _resourcePickup.gameObject.SetActive(true); } } [Rpc(SendTo.ClientsAndHost)] private void DisablePickupClientRpc() { if (_resourcePickup != null) { _resourcePickup.gameObject.SetActive(false); } } private CreepData GetCreepDataFromPrefab(GameObject prefab) { if (prefab == null) return null; CreepDataComponent component = prefab.GetComponent(); if (component != null) { return component.creepData; } return null; } private void SpawnCreep(GameObject prefab) { Vector3 spawnOffset = Random.insideUnitSphere * _spawnRadius; spawnOffset.y = 0; GameObject creep = Instantiate(prefab, transform.position + spawnOffset, Quaternion.identity); if (creep.GetComponent() == null) { var visibility = creep.AddComponent(); visibility.showInExploredAreas = false; visibility.updateInterval = 0.2f; } NetworkObject networkObj = creep.GetComponent(); if (networkObj == null) { networkObj = creep.AddComponent(); } // EnemyUnit 참조 저장 및 이벤트 구독 EnemyUnit enemyUnit = creep.GetComponent(); if (enemyUnit != null) { _spawnedCreeps.Add(enemyUnit); // Dictionary에 핸들러 저장 (메모리 누수 방지) System.Action handler = (killerId) => HandleCreepDeath(enemyUnit); _deathHandlers[enemyUnit] = handler; enemyUnit.OnDeath += handler; } // EnemyAIController에 캠프 참조 설정 (경고 전파 시스템용) EnemyAIController aiController = creep.GetComponent(); if (aiController != null) { aiController.creepCamp = this; } networkObj.SpawnWithOwnership(NetworkManager.Singleton.LocalClientId); } private void OnDrawGizmos() { Gizmos.color = Color.red; Gizmos.DrawWireSphere(transform.position, _spawnRadius); } private void OnDrawGizmosSelected() { Gizmos.color = new Color(1f, 0f, 0f, 0.3f); Gizmos.DrawSphere(transform.position, _spawnRadius); } } }