Project_WL/Assets/WL/Scripts/Player/PlayerCombat.cs

114 lines
4.8 KiB
C#

using UnityEngine;
using WL.Combat;
namespace WL.Player
{
/// <summary>
/// 플레이어 전투 파라미터 + 적 탐색.
/// 시야/공격 범위는 프리팹에서 바로 보이고 고칠 수 있도록 [SerializeField] 로 두고,
/// OnDrawGizmosSelected 로 씬 뷰에 노랑(시야)·빨강(공격) 원을 그린다.
///
/// 피해량은 CombatSettings(ScriptableObject)의 3~5 랜덤을 쓴다(C45).
/// 에셋이 비어 있을 때만 아래 fallbackAttackDamage 를 쓴다.
/// </summary>
[DisallowMultipleComponent]
public class PlayerCombat : MonoBehaviour
{
[Header("수치 설정 (ScriptableObject)")]
[SerializeField] private CombatSettings combatSettings;
[Header("■ PD 조정값 — 범위 (단위 m · 현재 임시값)")]
[Tooltip("PD 조정값 · 이 반경 안의 적을 탐색한다 (임시 8m)")]
[SerializeField] private float sightRange = 8f;
[Tooltip("PD 조정값 · 이 반경 안이면 공격한다. 사격 캐릭터 기준 임시 6m")]
[SerializeField] private float attackRange = 6f;
[Header("■ PD 조정값 — 공격")]
[Tooltip("PD 조정값 · 공격 간격(초) · 임시 1.2")]
[SerializeField] private float attackCooldown = 1.2f;
[Tooltip("CombatSettings 가 비어 있을 때만 쓰는 대체 피해량")]
[SerializeField] private float fallbackAttackDamage = 4f;
[Tooltip("PD 조정값 · 공격 모션 시작 후 피해가 들어가는 시간(초) · 임시 0.25")]
[SerializeField] private float attackHitDelay = 0.25f;
[Tooltip("PD 조정값 · 공격 모션 길이를 Animator 에서 못 읽을 때 쓰는 대체 시간(초)")]
[SerializeField] private float attackFallbackDuration = 0.8f;
[Header("■ PD 조정값 — 기즈모 표시")]
[Tooltip("선택했을 때 시야(노랑)·공격(빨강) 범위를 씬 뷰에 그린다")]
[SerializeField] private bool drawRangeGizmos = true;
[Tooltip("기즈모 원을 그리는 높이(발밑 기준 m)")]
[SerializeField] private float gizmoHeight = 0.05f;
private float _cooldownTimer;
public float SightRange { get { return sightRange; } }
public float AttackRange { get { return attackRange; } }
public float AttackCooldown { get { return attackCooldown; } }
public CombatSettings Settings { get { return combatSettings; } }
/// <summary>이번 공격의 피해량. CombatSettings 가 있으면 3~5 정수 랜덤.</summary>
public float RollAttackDamage()
{
return combatSettings != null ? combatSettings.RollPlayerDamage() : fallbackAttackDamage;
}
public float AttackHitDelay { get { return attackHitDelay; } }
public float AttackFallbackDuration { get { return attackFallbackDuration; } }
public bool CanAttack { get { return _cooldownTimer <= 0f; } }
private void Update()
{
if (_cooldownTimer > 0f) _cooldownTimer -= Time.deltaTime;
}
/// <summary>시야 범위 안의 가장 가까운 생존 적. 없으면 null.</summary>
public Enemy FindTarget()
{
return Enemy.FindNearestAlive(transform.position, sightRange);
}
public bool IsInAttackRange(Enemy e)
{
if (e == null) return false;
Vector3 a = transform.position; a.y = 0f;
Vector3 b = e.transform.position; b.y = 0f;
return (b - a).sqrMagnitude <= attackRange * attackRange;
}
public void ConsumeAttack()
{
_cooldownTimer = attackCooldown;
}
/// <summary>실제 피해 적용. 사거리를 다시 확인한 뒤 넣는다.</summary>
public void ApplyDamage(Enemy e)
{
if (e == null || !e.IsAlive) return;
if (!IsInAttackRange(e)) return;
e.TakeDamage(RollAttackDamage());
}
private void OnDrawGizmosSelected()
{
if (!drawRangeGizmos) return;
Vector3 c = transform.position + Vector3.up * gizmoHeight;
Gizmos.color = new Color(1f, 0.92f, 0.16f, 1f); // 노랑 = 시야
DrawCircle(c, sightRange);
Gizmos.color = new Color(1f, 0.20f, 0.20f, 1f); // 빨강 = 공격
DrawCircle(c, attackRange);
}
private static void DrawCircle(Vector3 center, float radius)
{
const int seg = 64;
Vector3 prev = center + new Vector3(radius, 0f, 0f);
for (int i = 1; i <= seg; i++)
{
float a = (i / (float)seg) * Mathf.PI * 2f;
Vector3 cur = center + new Vector3(Mathf.Cos(a) * radius, 0f, Mathf.Sin(a) * radius);
Gizmos.DrawLine(prev, cur);
prev = cur;
}
}
}
}