2026-01-29 06:58:38 +00:00
|
|
|
|
using UnityEngine;
|
2026-01-30 07:45:11 +00:00
|
|
|
|
using System.Collections;
|
2026-01-29 06:58:38 +00:00
|
|
|
|
|
|
|
|
|
|
public class PlayerAttack : MonoBehaviour
|
|
|
|
|
|
{
|
|
|
|
|
|
[Header("--- 참조 ---")]
|
|
|
|
|
|
[SerializeField] private PlayerInteraction interaction;
|
|
|
|
|
|
[SerializeField] private Stats stats;
|
|
|
|
|
|
[SerializeField] private PlayerAnimator pAnim;
|
|
|
|
|
|
[SerializeField] private PlayerHealth playerHealth;
|
2026-01-31 13:07:35 +00:00
|
|
|
|
[SerializeField] private WeaponHitBox weaponHitBox; // ⚔️ 낫에 붙은 히트박스
|
2026-01-29 06:58:38 +00:00
|
|
|
|
|
2026-01-29 16:11:10 +00:00
|
|
|
|
[Header("--- 설정 ---")]
|
2026-01-30 07:45:11 +00:00
|
|
|
|
[SerializeField] private float attackCooldown = 0.4f;
|
2026-01-30 06:30:27 +00:00
|
|
|
|
[SerializeField] private float fullChargeTime = 2f;
|
2026-01-31 13:07:35 +00:00
|
|
|
|
[SerializeField] private float postComboDelay = 1.2f;
|
2026-01-29 06:58:38 +00:00
|
|
|
|
|
|
|
|
|
|
private float _lastAttackTime, _chargeTimer;
|
2026-01-30 07:45:11 +00:00
|
|
|
|
private bool _isCharging, _canAttack = true, _isAttacking = false;
|
2026-01-31 13:07:35 +00:00
|
|
|
|
private int _comboCount = 0;
|
2026-01-29 06:58:38 +00:00
|
|
|
|
|
|
|
|
|
|
public float ChargeProgress => Mathf.Clamp01(_chargeTimer / fullChargeTime);
|
|
|
|
|
|
public bool IsCharging => _isCharging;
|
2026-01-30 07:45:11 +00:00
|
|
|
|
public bool IsAttacking => _isAttacking;
|
|
|
|
|
|
|
2026-01-29 06:58:38 +00:00
|
|
|
|
private void Update()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_isCharging)
|
|
|
|
|
|
{
|
|
|
|
|
|
_chargeTimer += Time.deltaTime;
|
2026-01-30 07:45:11 +00:00
|
|
|
|
if (Input.GetMouseButtonDown(1)) { CancelCharging(); }
|
2026-01-29 06:58:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void PerformNormalAttack()
|
|
|
|
|
|
{
|
2026-01-30 07:45:11 +00:00
|
|
|
|
if (!_canAttack || (playerHealth != null && playerHealth.isHit)) return;
|
2026-01-29 06:58:38 +00:00
|
|
|
|
if (interaction.CurrentWeapon == null || pAnim == null) return;
|
|
|
|
|
|
if (Time.time < _lastAttackTime + attackCooldown) return;
|
|
|
|
|
|
|
2026-01-31 13:07:35 +00:00
|
|
|
|
_isAttacking = true;
|
2026-02-01 15:49:12 +00:00
|
|
|
|
_comboCount = (_comboCount % 3) + 1; // 1 -> 2 -> 3 순환
|
2026-01-30 07:45:11 +00:00
|
|
|
|
|
2026-01-29 06:58:38 +00:00
|
|
|
|
pAnim.TriggerAttack();
|
|
|
|
|
|
_lastAttackTime = Time.time;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 13:07:35 +00:00
|
|
|
|
public void StartWeaponCollision()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (weaponHitBox != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.Log("<color=yellow>[Attack]</color> 낫 공격 판정 ON!");
|
|
|
|
|
|
weaponHitBox.EnableHitBox(20f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void StopWeaponCollision()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (weaponHitBox != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
Debug.Log("<color=yellow>[Attack]</color> 낫 공격 판정 OFF!");
|
|
|
|
|
|
weaponHitBox.DisableHitBox();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-01 15:49:12 +00:00
|
|
|
|
// ⭐ [수정 완료] 오직 3타 막타일 때만 카메라 연출을 실행합니다.
|
2026-01-30 06:30:27 +00:00
|
|
|
|
public void OnAttackShake()
|
|
|
|
|
|
{
|
2026-01-30 07:45:11 +00:00
|
|
|
|
if (CinemachineShake.Instance == null) return;
|
|
|
|
|
|
|
2026-02-01 15:49:12 +00:00
|
|
|
|
// 3번째 콤보일 때만 묵직하게 흔들어줍니다.
|
2026-01-31 13:07:35 +00:00
|
|
|
|
if (_comboCount == 3)
|
2026-01-30 07:45:11 +00:00
|
|
|
|
{
|
2026-02-01 15:49:12 +00:00
|
|
|
|
Debug.Log("<color=orange>[Shake]</color> 3타 막타 카메라 연출 실행!");
|
|
|
|
|
|
CinemachineShake.Instance.HitSlow(0.2f, 0.05f); // 히트 슬로우
|
|
|
|
|
|
CinemachineShake.Instance.CameraKick(10f); // 카메라 킥
|
|
|
|
|
|
CinemachineShake.Instance.ShakeAttack(); // 진동
|
2026-01-30 07:45:11 +00:00
|
|
|
|
}
|
2026-02-01 15:49:12 +00:00
|
|
|
|
// 🚫 else 구문을 삭제하여 1, 2타 시에는 아무것도 하지 않습니다.
|
2026-01-30 07:45:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void OnAttackEnd()
|
|
|
|
|
|
{
|
2026-02-01 15:49:12 +00:00
|
|
|
|
StopWeaponCollision();
|
2026-01-31 13:07:35 +00:00
|
|
|
|
if (_comboCount == 3) { StartCoroutine(PostComboRecovery()); }
|
|
|
|
|
|
else { _isAttacking = false; }
|
2026-01-30 06:30:27 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-30 07:45:11 +00:00
|
|
|
|
private IEnumerator PostComboRecovery()
|
2026-01-29 06:58:38 +00:00
|
|
|
|
{
|
2026-01-30 07:45:11 +00:00
|
|
|
|
_canAttack = false;
|
2026-01-31 13:07:35 +00:00
|
|
|
|
_isAttacking = true;
|
2026-01-30 07:45:11 +00:00
|
|
|
|
yield return new WaitForSeconds(postComboDelay);
|
|
|
|
|
|
_canAttack = true;
|
2026-01-31 13:07:35 +00:00
|
|
|
|
_isAttacking = false;
|
|
|
|
|
|
_comboCount = 0;
|
2026-01-29 06:58:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-31 13:07:35 +00:00
|
|
|
|
public void CancelCharging()
|
2026-01-29 06:58:38 +00:00
|
|
|
|
{
|
2026-01-30 07:45:11 +00:00
|
|
|
|
_isCharging = false;
|
|
|
|
|
|
_chargeTimer = 0f;
|
|
|
|
|
|
if (pAnim != null) pAnim.SetCharging(false);
|
|
|
|
|
|
if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(false);
|
2026-01-29 06:58:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void ReleaseAttack()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!_isCharging || interaction.CurrentWeapon == null) return;
|
2026-01-31 13:07:35 +00:00
|
|
|
|
|
2026-01-29 06:58:38 +00:00
|
|
|
|
pAnim.TriggerThrow();
|
|
|
|
|
|
pAnim.SetCharging(false);
|
|
|
|
|
|
|
2026-01-31 13:07:35 +00:00
|
|
|
|
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
|
|
|
|
|
|
Plane groundPlane = new Plane(Vector3.up, transform.position);
|
|
|
|
|
|
float rayDistance;
|
|
|
|
|
|
Vector3 targetDirection = transform.forward;
|
|
|
|
|
|
|
|
|
|
|
|
if (groundPlane.Raycast(ray, out rayDistance))
|
|
|
|
|
|
{
|
|
|
|
|
|
Vector3 pointOnGround = ray.GetPoint(rayDistance);
|
|
|
|
|
|
targetDirection = (pointOnGround - transform.position).normalized;
|
|
|
|
|
|
targetDirection.y = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (targetDirection != Vector3.zero) transform.rotation = Quaternion.LookRotation(targetDirection);
|
|
|
|
|
|
|
|
|
|
|
|
GameObject weaponObj = interaction.CurrentWeapon.gameObject;
|
|
|
|
|
|
weaponObj.transform.SetParent(null);
|
|
|
|
|
|
|
|
|
|
|
|
int lv = _chargeTimer >= 2f ? 3 : (_chargeTimer >= 1f ? 2 : 1);
|
|
|
|
|
|
float throwForce = interaction.CurrentWeapon.Config.GetForce(lv);
|
|
|
|
|
|
float currentSpread = interaction.CurrentWeapon.Config.GetSpread(lv);
|
|
|
|
|
|
Vector3 finalThrowDir = Quaternion.Euler(0, Random.Range(-currentSpread, currentSpread), 0) * targetDirection;
|
|
|
|
|
|
|
|
|
|
|
|
interaction.CurrentWeapon.OnThrown(finalThrowDir, throwForce, lv, stats);
|
2026-01-29 06:58:38 +00:00
|
|
|
|
|
2026-01-30 07:45:11 +00:00
|
|
|
|
if (CinemachineShake.Instance != null)
|
2026-01-29 06:58:38 +00:00
|
|
|
|
{
|
2026-01-30 07:45:11 +00:00
|
|
|
|
CinemachineShake.Instance.HitSlow(0.15f, 0.3f);
|
|
|
|
|
|
CinemachineShake.Instance.SetZoom(false);
|
|
|
|
|
|
CinemachineShake.Instance.ShakeAttack();
|
2026-01-29 06:58:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interaction.ClearCurrentWeapon();
|
|
|
|
|
|
stats.ResetWeight();
|
|
|
|
|
|
_isCharging = false;
|
|
|
|
|
|
_chargeTimer = 0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void StartCharging()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (playerHealth != null && playerHealth.isHit) return;
|
|
|
|
|
|
if (interaction.CurrentWeapon == null) return;
|
|
|
|
|
|
_isCharging = true;
|
|
|
|
|
|
_chargeTimer = 0f;
|
|
|
|
|
|
pAnim.SetCharging(true);
|
2026-01-30 07:45:11 +00:00
|
|
|
|
if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(true);
|
2026-01-29 06:58:38 +00:00
|
|
|
|
}
|
2026-02-01 15:49:12 +00:00
|
|
|
|
|
2026-01-31 13:07:35 +00:00
|
|
|
|
private void OnDrawGizmos()
|
|
|
|
|
|
{
|
2026-02-01 15:49:12 +00:00
|
|
|
|
Gizmos.color = Color.red;
|
2026-01-31 13:07:35 +00:00
|
|
|
|
if (TryGetComponent<BoxCollider>(out var box))
|
|
|
|
|
|
{
|
|
|
|
|
|
Gizmos.matrix = transform.localToWorldMatrix;
|
|
|
|
|
|
Gizmos.DrawWireCube(box.center, box.size);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-29 06:58:38 +00:00
|
|
|
|
}
|