311 lines
9.2 KiB
C#
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
|
|
}
|