Projext/Assets/Scripts/Player/Combat/Attack.cs

356 lines
12 KiB
C#
Raw Normal View History

2026-01-29 06:58:38 +00:00
using UnityEngine;
2026-01-30 07:45:11 +00:00
using System.Collections;
2026-02-06 04:20:12 +00:00
using System.Collections.Generic;
2026-01-29 06:58:38 +00:00
public class PlayerAttack : MonoBehaviour
{
2026-02-06 04:20:12 +00:00
[Header("--- 활 설정 ---")]
2026-02-06 05:32:48 +00:00
[SerializeField] private GameObject arrowPrefab;
[SerializeField] private Transform firePoint;
[SerializeField] private PlayerAnimator pAnim;
2026-02-06 04:20:12 +00:00
[Header("--- 일반 공격 (좌클릭) ---")]
[SerializeField] private float normalDamage = 10f;
[SerializeField] private float normalRange = 15f;
[SerializeField] private float normalSpeed = 20f;
[SerializeField] private float attackCooldown = 0.5f;
[Header("--- 차징 공격 (우클릭) ---")]
[SerializeField] private float maxChargeTime = 2.0f;
[System.Serializable]
public struct ChargeStage
{
public float chargeTime;
public float damageMult;
public float rangeMult;
}
[SerializeField] private List<ChargeStage> chargeStages;
2026-02-02 15:02:12 +00:00
2026-02-06 09:27:08 +00:00
[Header("--- 🎯 에임 보정 설정 ---")]
[SerializeField] private bool enableAutoAim = true; // 자동 조준 켜기/끄기
[SerializeField] private float autoAimRange = 15f; // 자동 조준 감지 범위
[SerializeField] private float autoAimAngle = 45f; // 마우스 커서 기준 각도 (넓을수록 감지 잘됨)
[Range(0f, 1f)]
[Tooltip("에임 보정 강도 (0: 보정 없음, 1: 완전 자동 조준)")]
[SerializeField] private float aimAssistStrength = 0.4f; // 40% 보정 (에임핵 느낌)
[SerializeField] private LayerMask enemyLayer; // 적 레이어
[Tooltip("차징 최대 단계에서만 자동 조준 활성화")]
[SerializeField] private bool onlyMaxCharge = true; // 최대 차징에서만 작동
2026-02-06 04:20:12 +00:00
private float _lastAttackTime;
private float _chargeTimer;
private bool _isCharging = false;
private bool _isAttacking = false;
2026-02-06 05:32:48 +00:00
private bool _waitForRelease = false;
2026-02-06 04:20:12 +00:00
private float _pendingDamage;
private float _pendingSpeed;
private float _pendingRange;
2026-02-06 09:27:08 +00:00
// 🎯 발사 방향 저장
private Vector3 _pendingShootDirection;
2026-02-06 04:20:12 +00:00
public bool IsCharging => _isCharging;
2026-02-02 15:02:12 +00:00
public bool IsAttacking
{
get => _isAttacking;
set => _isAttacking = value;
}
2026-02-06 04:20:12 +00:00
public float ChargeProgress => Mathf.Clamp01(_chargeTimer / maxChargeTime);
private void Start()
{
if (chargeStages == null || chargeStages.Count == 0)
{
chargeStages = new List<ChargeStage>
{
new ChargeStage { chargeTime = 0f, damageMult = 1f, rangeMult = 1f },
new ChargeStage { chargeTime = 1f, damageMult = 1.5f, rangeMult = 1.2f },
new ChargeStage { chargeTime = 2f, damageMult = 2.5f, rangeMult = 1.5f }
};
}
2026-02-06 09:27:08 +00:00
float calculatedMaxTime = 0f;
foreach (var stage in chargeStages)
{
if (stage.chargeTime > calculatedMaxTime)
calculatedMaxTime = stage.chargeTime;
}
maxChargeTime = Mathf.Max(calculatedMaxTime, 0.1f);
2026-02-06 04:20:12 +00:00
}
2026-01-30 07:45:11 +00:00
2026-01-29 06:58:38 +00:00
private void Update()
{
if (_isCharging)
{
_chargeTimer += Time.deltaTime;
}
}
2026-02-06 05:32:48 +00:00
// --- [1] 일반 공격 ---
2026-01-29 06:58:38 +00:00
public void PerformNormalAttack()
{
if (Time.time < _lastAttackTime + attackCooldown) return;
2026-02-06 04:20:12 +00:00
if (_isAttacking) return;
2026-01-29 06:58:38 +00:00
2026-02-06 04:20:12 +00:00
_pendingDamage = normalDamage;
_pendingSpeed = normalSpeed;
_pendingRange = normalRange;
2026-01-30 07:45:11 +00:00
2026-02-06 09:27:08 +00:00
// 🎯 마우스 방향 계산 (일반 공격은 자동 조준 없음)
_pendingShootDirection = GetMouseDirection();
2026-01-29 06:58:38 +00:00
_lastAttackTime = Time.time;
2026-02-06 04:20:12 +00:00
if (pAnim != null) pAnim.TriggerThrow();
2026-01-30 07:45:11 +00:00
2026-02-06 04:20:12 +00:00
StartCoroutine(AttackRoutine());
2026-01-30 07:45:11 +00:00
}
2026-02-06 05:32:48 +00:00
// --- [2] 차징 시작 ---
2026-02-06 04:20:12 +00:00
public void StartCharging()
2026-01-30 07:45:11 +00:00
{
2026-02-06 05:32:48 +00:00
if (_waitForRelease) return;
2026-02-06 04:20:12 +00:00
_isCharging = true;
_chargeTimer = 0f;
2026-01-30 06:30:27 +00:00
2026-02-06 04:20:12 +00:00
if (pAnim != null) pAnim.SetCharging(true);
if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(true);
2026-01-29 06:58:38 +00:00
}
2026-02-06 05:32:48 +00:00
private void ResetChargingEffects()
2026-01-29 06:58:38 +00:00
{
2026-01-30 07:45:11 +00:00
_isCharging = false;
_chargeTimer = 0f;
2026-02-06 04:20:12 +00:00
2026-01-30 07:45:11 +00:00
if (pAnim != null) pAnim.SetCharging(false);
if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(false);
2026-01-29 06:58:38 +00:00
}
2026-02-06 09:27:08 +00:00
// --- [3] 차징 취소 ---
2026-02-06 05:32:48 +00:00
public void CancelCharging()
{
ResetChargingEffects();
2026-02-06 09:27:08 +00:00
_waitForRelease = false;
2026-02-06 05:32:48 +00:00
}
2026-02-06 09:27:08 +00:00
// --- [4] 차징 발사 ---
2026-01-29 06:58:38 +00:00
public void ReleaseAttack()
{
2026-02-06 04:20:12 +00:00
if (!_isCharging) return;
2026-01-29 06:58:38 +00:00
2026-02-06 04:20:12 +00:00
ChargeStage currentStage = chargeStages[0];
foreach (var stage in chargeStages)
2026-01-31 13:07:35 +00:00
{
2026-02-06 04:20:12 +00:00
if (_chargeTimer >= stage.chargeTime) currentStage = stage;
2026-01-31 13:07:35 +00:00
}
2026-02-06 04:20:12 +00:00
_pendingDamage = normalDamage * currentStage.damageMult;
_pendingSpeed = normalSpeed * currentStage.rangeMult;
_pendingRange = normalRange * currentStage.rangeMult;
2026-01-31 13:07:35 +00:00
2026-02-06 09:27:08 +00:00
// 🎯 발사 방향 계산 (차징 최대 시 자동 조준)
bool isMaxCharge = _chargeTimer >= maxChargeTime * 0.95f; // 95% 이상이면 최대 차징
_pendingShootDirection = GetShootDirection(isMaxCharge);
2026-02-06 04:20:12 +00:00
if (pAnim != null) pAnim.TriggerThrow();
2026-01-31 13:07:35 +00:00
2026-02-06 05:32:48 +00:00
_waitForRelease = true;
2026-02-06 04:20:12 +00:00
_lastAttackTime = Time.time;
StartCoroutine(AttackRoutine());
}
2026-02-06 09:27:08 +00:00
// --- [5] 🎯 마우스 방향 계산 ---
private Vector3 GetMouseDirection()
{
// 마우스 위치 → 월드 좌표
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
Plane groundPlane = new Plane(Vector3.up, transform.position);
if (groundPlane.Raycast(ray, out float distance))
{
Vector3 worldMousePos = ray.GetPoint(distance);
Vector3 direction = (worldMousePos - firePoint.position).normalized;
direction.y = 0; // 수평 발사
return direction;
}
// 실패 시 플레이어 정면
return transform.forward;
}
// --- [6] 🎯 발사 방향 계산 (에임 어시스트 포함) ---
private Vector3 GetShootDirection(bool isMaxCharge)
{
Vector3 mouseDir = GetMouseDirection();
// 자동 조준 비활성화 또는 최대 차징 아님
if (!enableAutoAim || (onlyMaxCharge && !isMaxCharge))
{
return mouseDir;
}
// 🎯 적 탐색
Transform bestTarget = FindBestTarget(mouseDir);
if (bestTarget != null)
{
// 적 방향 계산 (중심부 조준)
Vector3 targetPos = bestTarget.position + Vector3.up * 1.2f;
Vector3 targetDir = (targetPos - firePoint.position).normalized;
targetDir.y = 0; // 수평 발사
// ✨ 에임 어시스트: 마우스 방향과 적 방향을 섞음!
// aimAssistStrength = 0.4 → 마우스 60% + 적 40%
Vector3 assistedDir = Vector3.Lerp(mouseDir, targetDir, aimAssistStrength);
assistedDir.y = 0;
assistedDir.Normalize();
Debug.Log($"🎯 에임 어시스트 활성화! 타겟: {bestTarget.name}, 보정 강도: {aimAssistStrength * 100}%");
return assistedDir;
}
// 적 없으면 마우스 방향
return mouseDir;
}
// --- [7] 🎯 최적 타겟 찾기 ---
private Transform FindBestTarget(Vector3 mouseDirection)
{
Collider[] enemies = Physics.OverlapSphere(transform.position, autoAimRange, enemyLayer);
if (enemies.Length == 0) return null;
Transform bestTarget = null;
float bestScore = float.MaxValue;
foreach (var enemy in enemies)
{
Vector3 dirToEnemy = (enemy.transform.position - transform.position).normalized;
// 마우스 방향과의 각도 체크
float angle = Vector3.Angle(mouseDirection, dirToEnemy);
if (angle > autoAimAngle) continue; // 각도 범위 밖
// 거리 체크
float distance = Vector3.Distance(transform.position, enemy.transform.position);
// 점수 계산: 각도 + 거리 (낮을수록 좋음)
float score = angle * 0.5f + distance * 0.5f;
if (score < bestScore)
{
bestScore = score;
bestTarget = enemy.transform;
}
}
return bestTarget;
}
// --- [8] 이벤트: 화살 생성 ---
2026-02-06 04:20:12 +00:00
public void OnShootArrow()
{
if (arrowPrefab == null || firePoint == null) return;
2026-01-31 13:07:35 +00:00
2026-02-06 09:27:08 +00:00
// 🎯 저장된 방향으로 회전 계산
Quaternion shootRotation = Quaternion.LookRotation(_pendingShootDirection);
// 화살 생성 (계산된 방향으로)
GameObject arrow = Instantiate(arrowPrefab, firePoint.position, shootRotation);
// ArrowItem 초기화
ArrowItem arrowItem = arrow.GetComponent<ArrowItem>();
if (arrowItem != null)
{
arrowItem.Initialize(_pendingDamage, _pendingSpeed, _pendingRange);
return;
}
2026-01-29 06:58:38 +00:00
2026-02-06 09:27:08 +00:00
// 하위 호환
PlayerArrow arrowScript = arrow.GetComponent<PlayerArrow>();
2026-02-06 04:20:12 +00:00
if (arrowScript != null)
2026-01-29 06:58:38 +00:00
{
2026-02-06 04:20:12 +00:00
arrowScript.Initialize(_pendingDamage, _pendingSpeed, _pendingRange);
2026-01-29 06:58:38 +00:00
}
}
2026-02-06 09:27:08 +00:00
// --- [9] 이벤트: 공격 끝 ---
2026-02-06 05:32:48 +00:00
public void OnAttackEnd()
{
_isAttacking = false;
ResetChargingEffects();
}
2026-02-06 09:27:08 +00:00
// --- [10] 안전장치 코루틴 ---
2026-02-06 04:20:12 +00:00
private IEnumerator AttackRoutine()
2026-01-29 06:58:38 +00:00
{
2026-02-06 04:20:12 +00:00
_isAttacking = true;
2026-02-06 05:32:48 +00:00
yield return new WaitForSeconds(0.6f);
if (_isAttacking)
{
_isAttacking = false;
ResetChargingEffects();
}
2026-01-29 06:58:38 +00:00
}
2026-02-06 09:27:08 +00:00
// --- [11] 화살 교체 ---
public void SwapArrow(GameObject newArrow)
{
if (newArrow == null) return;
arrowPrefab = newArrow;
Debug.Log($"화살이 {newArrow.name}(으)로 교체되었습니다!");
}
2026-02-06 04:20:12 +00:00
public void StartWeaponCollision() { }
public void StopWeaponCollision() { }
2026-02-06 09:27:08 +00:00
// --- [12] 🎯 디버그: 감지 범위 시각화 ---
private void OnDrawGizmosSelected()
{
if (!enableAutoAim) return;
// 자동 조준 범위 (노란색 원)
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(transform.position, autoAimRange);
// 마우스 방향 (파란색 선)
if (Application.isPlaying && firePoint != null)
{
Vector3 mouseDir = GetMouseDirection();
Gizmos.color = Color.blue;
Gizmos.DrawRay(firePoint.position, mouseDir * 5f);
// 🎯 에임 어시스트 방향 (빨간색 선)
Transform target = FindBestTarget(mouseDir);
if (target != null)
{
Vector3 targetPos = target.position + Vector3.up * 1.2f;
Vector3 targetDir = (targetPos - firePoint.position).normalized;
targetDir.y = 0;
// 보정된 최종 방향
Vector3 assistedDir = Vector3.Lerp(mouseDir, targetDir, aimAssistStrength);
assistedDir.Normalize();
Gizmos.color = Color.red;
Gizmos.DrawRay(firePoint.position, assistedDir * 7f);
// 타겟 위치 표시
Gizmos.color = Color.green;
Gizmos.DrawWireSphere(targetPos, 0.3f);
}
// 감지 각도 (연한 초록색 선)
Gizmos.color = new Color(0, 1, 0, 0.3f);
Vector3 leftBound = Quaternion.Euler(0, -autoAimAngle, 0) * mouseDir;
Vector3 rightBound = Quaternion.Euler(0, autoAimAngle, 0) * mouseDir;
Gizmos.DrawRay(firePoint.position, leftBound * autoAimRange);
Gizmos.DrawRay(firePoint.position, rightBound * autoAimRange);
}
}
2026-01-29 06:58:38 +00:00
}