Files
Northbound/Assets/Scripts/EnemyPortal.cs

311 lines
9.2 KiB
C#

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<int> _currentHealth = new NetworkVariable<int>(
0,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
);
private NetworkVariable<TeamType> _team = new NetworkVariable<TeamType>(
TeamType.Neutral,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
);
[System.Serializable]
public class MonsterEntry
{
public GameObject prefab;
}
[Header("Spawn Settings")]
[Tooltip("몬스터 프리팹 목록 (Editor에서 자동 로드 가능)")]
[SerializeField] private List<MonsterEntry> 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($"<color=cyan>[EnemyPortal] 포털 스폰됨 (팀: {TeamManager.GetTeamName(_team.Value)}, 체력: {_currentHealth.Value}/{maxHealth})</color>");
}
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<MonsterDataComponent>();
if (component != null)
{
return component.monsterData;
}
return null;
}
private void SpawnEnemy(GameObject prefab)
{
if (!IsServer) return;
GameObject enemy = Instantiate(prefab, transform);
if (enemy.GetComponent<FogOfWarVisibility>() == null)
{
var visibility = enemy.AddComponent<FogOfWarVisibility>();
visibility.showInExploredAreas = false;
visibility.updateInterval = 0.2f;
}
var netObj = enemy.GetComponent<NetworkObject>();
netObj.Spawn(true);
Debug.Log($"<color=cyan>[EnemyPortal] {enemy.name} 스폰됨 - OwnerClientId: {netObj.OwnerClientId}, IsServer: {IsServer}</color>");
}
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<ITeamMember>();
if (attackerTeamMember != null)
{
if (!TeamManager.CanAttack(attackerTeamMember, this))
{
Debug.Log($"<color=yellow>[EnemyPortal] {TeamManager.GetTeamName(attackerTeamMember.GetTeam())} 팀은 {TeamManager.GetTeamName(_team.Value)} 팀 포털을 공격할 수 없습니다.</color>");
return;
}
}
}
// 데미지 적용
int actualDamage = Mathf.Min(damage, _currentHealth.Value);
_currentHealth.Value -= actualDamage;
Debug.Log($"<color=red>[EnemyPortal] 포털이 {actualDamage} 데미지를 받았습니다. 남은 체력: {_currentHealth.Value}/{maxHealth}</color>");
// 데미지 이펙트
ShowDamageEffectClientRpc();
// 체력이 0이 되면 파괴
if (_currentHealth.Value <= 0)
{
DestroyPortal(attackerId);
}
}
private void DestroyPortal(ulong attackerId)
{
if (!IsServer) return;
Debug.Log($"<color=red>[EnemyPortal] 포털이 파괴되었습니다! (공격자: {attackerId})</color>");
// 파괴 이펙트
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
}