333 lines
11 KiB
C#
333 lines
11 KiB
C#
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<GameObject> creepPrefabs = new List<GameObject>();
|
|
|
|
[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<EnemyUnit> _spawnedCreeps = new List<EnemyUnit>();
|
|
private ResourcePickup _resourcePickup;
|
|
private readonly Dictionary<EnemyUnit, System.Action<ulong>> _deathHandlers = new Dictionary<EnemyUnit, System.Action<ulong>>();
|
|
|
|
// 중복 경고 방지용
|
|
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<GameObject> 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<GameObject> affordableCreeps = new List<GameObject>();
|
|
|
|
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<ResourcePickup>();
|
|
if (_resourcePickup == null)
|
|
{
|
|
Destroy(pickup);
|
|
return;
|
|
}
|
|
|
|
// 캠프 강도에 비례하여 리소스 양 설정
|
|
_resourcePickup.resourceAmount = Mathf.RoundToInt(baseResourceAmount * _campStrength);
|
|
|
|
// NetworkObject 추가 및 스폰
|
|
NetworkObject networkObj = pickup.GetComponent<NetworkObject>();
|
|
if (networkObj == null)
|
|
{
|
|
networkObj = pickup.AddComponent<NetworkObject>();
|
|
}
|
|
|
|
networkObj.SpawnWithOwnership(NetworkManager.Singleton.LocalClientId);
|
|
|
|
// NetworkVariable을 통해 가시성 동기화 (기본적으로 숨김)
|
|
_resourcePickup.SetVisible(false);
|
|
}
|
|
|
|
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)
|
|
{
|
|
// NetworkVariable이 자동으로 클라이언트에 동기화됨
|
|
_resourcePickup.SetVisible(true);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 캠프 내 모든 크립에게 경고 전파
|
|
/// </summary>
|
|
/// <param name="target">감지된 타겟</param>
|
|
/// <param name="alertingCreep">경고를 보낸 크립 (자신에게는 다시 보내지 않음)</param>
|
|
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<EnemyAIController>();
|
|
if (aiController == null) continue;
|
|
|
|
// 경고를 보낸 크립은 제외 (이미 타겟을 설정했음)
|
|
if (aiController == alertingCreep) continue;
|
|
|
|
aiController.ReceiveAlert(target);
|
|
}
|
|
}
|
|
|
|
private CreepData GetCreepDataFromPrefab(GameObject prefab)
|
|
{
|
|
if (prefab == null) return null;
|
|
|
|
CreepDataComponent component = prefab.GetComponent<CreepDataComponent>();
|
|
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<FogOfWarVisibility>() == null)
|
|
{
|
|
var visibility = creep.AddComponent<FogOfWarVisibility>();
|
|
visibility.showInExploredAreas = false;
|
|
visibility.updateInterval = 0.2f;
|
|
}
|
|
|
|
NetworkObject networkObj = creep.GetComponent<NetworkObject>();
|
|
if (networkObj == null)
|
|
{
|
|
networkObj = creep.AddComponent<NetworkObject>();
|
|
}
|
|
|
|
// EnemyUnit 참조 저장 및 이벤트 구독
|
|
EnemyUnit enemyUnit = creep.GetComponent<EnemyUnit>();
|
|
if (enemyUnit != null)
|
|
{
|
|
_spawnedCreeps.Add(enemyUnit);
|
|
// Dictionary에 핸들러 저장 (메모리 누수 방지)
|
|
System.Action<ulong> handler = (killerId) => HandleCreepDeath(enemyUnit);
|
|
_deathHandlers[enemyUnit] = handler;
|
|
enemyUnit.OnDeath += handler;
|
|
}
|
|
|
|
// EnemyAIController에 캠프 참조 설정 (경고 전파 시스템용)
|
|
EnemyAIController aiController = creep.GetComponent<EnemyAIController>();
|
|
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);
|
|
}
|
|
}
|
|
}
|