119 lines
5.5 KiB
C#
119 lines
5.5 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using WL.Combat;
|
|
|
|
namespace WL.Player
|
|
{
|
|
/// <summary>
|
|
/// 적 유닛의 체력·피격 판정. 플레이어 자동 탐색·공격의 대상이 된다.
|
|
/// 태그 "Enemy" + 이 컴포넌트 조합으로 식별한다.
|
|
///
|
|
/// 체력 수치는 CombatSettings(ScriptableObject)에서 읽는다(C45). 에셋이 비어 있을 때만
|
|
/// 인스펙터의 fallbackMaxHp 를 쓴다.
|
|
/// 사망 연출은 EnemyController 가 Died 이벤트를 받아 처리한다(구독자가 없으면 즉시 비활성화).
|
|
/// </summary>
|
|
public class Enemy : MonoBehaviour
|
|
{
|
|
/// <summary>활성 상태인 모든 적. 매 프레임 FindObjectsByType 를 피하기 위한 등록부.</summary>
|
|
public static readonly List<Enemy> All = new List<Enemy>();
|
|
|
|
[Header("수치 설정 (ScriptableObject)")]
|
|
[SerializeField] private CombatSettings combatSettings;
|
|
|
|
[Header("스탯 (에셋이 없을 때만 쓰는 대체값)")]
|
|
[Tooltip("CombatSettings 가 비어 있을 때 쓰는 최대 체력")]
|
|
[SerializeField] private float fallbackMaxHp = 10f;
|
|
[Tooltip("피격 판정 중심의 발밑 기준 높이(m). 조준·거리 계산에 사용")]
|
|
[SerializeField] private float bodyCenterHeight = 1.0f;
|
|
[Tooltip("사망 연출을 처리할 구독자(EnemyController)가 없을 때 오브젝트를 비활성화한다")]
|
|
[SerializeField] private bool deactivateOnDeath = true;
|
|
[Tooltip("체력이 줄지 않고 죽지 않는다. 피격 플래시·데미지 숫자는 그대로 표시 — 이펙트 확인용 더미 봇 (PD #717 ④)")]
|
|
[SerializeField] private bool invincible = false;
|
|
|
|
[Header("상태 (읽기 전용)")]
|
|
[SerializeField] private float currentHp;
|
|
|
|
/// <summary>체력이 0 이 된 순간 1회 발생. EnemyController 가 사망 연출을 맡는다.</summary>
|
|
public event System.Action<Enemy> Died;
|
|
/// <summary>어떤 적이든 죽은 순간 1회 발생(전역). PlayerProgress 가 경험치 지급에 쓴다 (PD #727).</summary>
|
|
public static event System.Action<Enemy> AnyDied;
|
|
/// <summary>피해를 받은 순간 발생(사망 타격 포함). 인자 = 이번 피해량.</summary>
|
|
public event System.Action<Enemy, float> Damaged;
|
|
|
|
private bool _dieFired;
|
|
|
|
public CombatSettings Settings { get { return combatSettings; } }
|
|
public float MaxHp { get { return combatSettings != null ? combatSettings.enemyHp : fallbackMaxHp; } }
|
|
public float CurrentHp { get { return currentHp; } }
|
|
public bool IsAlive { get { return currentHp > 0f; } }
|
|
public Vector3 BodyCenter { get { return transform.position + Vector3.up * bodyCenterHeight; } }
|
|
|
|
private void Awake()
|
|
{
|
|
currentHp = MaxHp;
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
if (!All.Contains(this)) All.Add(this);
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
All.Remove(this);
|
|
}
|
|
|
|
/// <summary>무적(더미 봇)인가. 체력이 줄지 않고 죽지 않는다.</summary>
|
|
public bool IsInvincible { get { return invincible; } }
|
|
|
|
public void TakeDamage(float amount)
|
|
{
|
|
if (!IsAlive) return;
|
|
// PD #717 ④ — 무적 더미는 체력을 깎지 않는다. 연출(플래시·데미지 숫자)은 그대로 태운다.
|
|
if (!invincible) currentHp = Mathf.Max(0f, currentHp - amount);
|
|
Debug.Log("[Enemy] " + name + " 피격 " + amount.ToString("F0") + " -> HP " + currentHp.ToString("F0") + "/" + MaxHp.ToString("F0")
|
|
+ (invincible ? " (무적 더미 — 체력 불변)" : ""), this);
|
|
|
|
// 데미지 숫자 연출 (PD #702-①) — 스포너가 없으면 조용히 무시된다
|
|
WL.UI.DamagePopupSpawner.Spawn(BodyCenter, amount, WL.UI.DamagePopupSpawner.Kind.ToEnemy);
|
|
|
|
// 피격 하이라이트(HitFlash)·행동 반응(EnemyController)이 이 이벤트를 구독한다
|
|
if (Damaged != null) Damaged(this, amount);
|
|
if (currentHp <= 0f) Die();
|
|
}
|
|
|
|
private void Die()
|
|
{
|
|
if (_dieFired) return;
|
|
_dieFired = true;
|
|
// 죽은 순간 탐색 대상에서 즉시 제외한다(연출이 끝날 때까지 남아 있으면 플레이어가 시체를 계속 때린다).
|
|
All.Remove(this);
|
|
Debug.Log("[Enemy] " + name + " 사망 — 연출 시작", this);
|
|
if (AnyDied != null) AnyDied(this);
|
|
if (Died != null) { Died(this); return; }
|
|
if (deactivateOnDeath) gameObject.SetActive(false);
|
|
}
|
|
|
|
/// <summary>지정 위치에서 range 안에 있는 가장 가까운 생존 적을 찾는다.</summary>
|
|
public static Enemy FindNearestAlive(Vector3 from, float range)
|
|
{
|
|
Enemy best = null;
|
|
float bestSqr = range * range;
|
|
for (int i = 0; i < All.Count; i++)
|
|
{
|
|
var e = All[i];
|
|
if (e == null || !e.IsAlive) continue;
|
|
float sqr = (e.transform.position - from).sqrMagnitude;
|
|
if (sqr <= bestSqr) { bestSqr = sqr; best = e; }
|
|
}
|
|
return best;
|
|
}
|
|
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
Gizmos.color = new Color(1f, 0.3f, 0.3f, 0.9f);
|
|
Gizmos.DrawWireSphere(transform.position + Vector3.up * bodyCenterHeight, 0.5f);
|
|
}
|
|
}
|
|
}
|