using UnityEngine; using WL.Combat; namespace WL.Player { /// /// 플레이어 전투 파라미터 + 적 탐색. /// 시야/공격 범위는 프리팹에서 바로 보이고 고칠 수 있도록 [SerializeField] 로 두고, /// OnDrawGizmosSelected 로 씬 뷰에 노랑(시야)·빨강(공격) 원을 그린다. /// /// 피해량은 CombatSettings(ScriptableObject)의 3~5 랜덤을 쓴다(C45). /// 에셋이 비어 있을 때만 아래 fallbackAttackDamage 를 쓴다. /// [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; } } /// 이번 공격의 피해량. CombatSettings 가 있으면 3~5 정수 랜덤. 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; } /// 시야 범위 안의 가장 가까운 생존 적. 없으면 null. 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; } /// 실제 피해 적용. 사거리를 다시 확인한 뒤 넣는다. 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; } } } }