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

374 lines
13 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 Transform firePoint;
[SerializeField] private PlayerAnimator pAnim;
2026-02-06 04:20:12 +00:00
2026-02-09 14:49:44 +00:00
[Header("--- 스탯 참조 ---")]
[SerializeField] private Stats playerStats;
2026-02-06 04:20:12 +00:00
[Header("--- 일반 공격 (좌클릭) ---")]
[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-09 14:49:44 +00:00
[Header("--- 에임 보정 설정 ---")]
[SerializeField] private bool enableAutoAim = true;
[SerializeField] private float autoAimRange = 15f;
[SerializeField] private float autoAimAngle = 45f;
2026-02-06 09:27:08 +00:00
[Range(0f, 1f)]
2026-02-09 14:49:44 +00:00
[SerializeField] private float aimAssistStrength = 0.4f;
[SerializeField] private LayerMask enemyLayer;
[SerializeField] private bool onlyMaxCharge = true;
// ============================================
// [NEW] 파티클 기반 화살 시스템
// ============================================
[Header("--- 파티클 화살 시스템 ---")]
[SerializeField] private GameObject defaultProjectilePrefab; // 기본 발사체 프리팹
private GameObject _currentProjectilePrefab;
private ArrowElementType _currentElementType = ArrowElementType.None;
private float _currentElementDamage = 0f;
private float _currentElementDuration = 0f;
2026-02-06 09:27:08 +00:00
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-09 14:49:44 +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);
2026-02-09 14:49:44 +00:00
// [NEW] 외부에서 현재 속성 확인용
public ArrowElementType CurrentElement => _currentElementType;
public GameObject CurrentProjectilePrefab => _currentProjectilePrefab;
2026-02-06 04:20:12 +00:00
private void Start()
{
2026-02-09 14:49:44 +00:00
if (playerStats == null) playerStats = GetComponentInParent<Stats>();
// 기본 발사체 프리팹 설정
if (_currentProjectilePrefab == null)
_currentProjectilePrefab = defaultProjectilePrefab;
2026-02-06 04:20:12 +00:00
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)
{
2026-02-09 14:49:44 +00:00
if (stage.chargeTime > calculatedMaxTime) calculatedMaxTime = stage.chargeTime;
2026-02-06 09:27:08 +00:00
}
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()
{
2026-02-09 14:49:44 +00:00
if (_isCharging) _chargeTimer += Time.deltaTime;
2026-01-29 06:58:38 +00:00
}
2026-02-09 14:49:44 +00:00
// ============================================
// [NEW] 화살 장착 (SwapArrow 대체)
// ============================================
/// <summary>
/// ArrowPickup에서 습득 시 호출합니다.
/// 파티클 프리팹 + 속성 정보를 저장합니다.
/// </summary>
public void SetCurrentArrow(ArrowData data)
{
if (data.projectilePrefab != null)
_currentProjectilePrefab = data.projectilePrefab;
else
_currentProjectilePrefab = defaultProjectilePrefab;
_currentElementType = data.elementType;
_currentElementDamage = data.elementDamage;
_currentElementDuration = data.elementDuration;
Debug.Log($"화살 장착: [{data.arrowName}] 속성={data.elementType}, " +
$"속성데미지={data.elementDamage}, 지속시간={data.elementDuration}s");
}
/// <summary>
/// 기본 화살로 초기화
/// </summary>
public void ResetArrow()
{
_currentProjectilePrefab = defaultProjectilePrefab;
_currentElementType = ArrowElementType.None;
_currentElementDamage = 0f;
_currentElementDuration = 0f;
Debug.Log("화살 초기화: 기본 화살");
}
// ============================================
// 일반 공격 (좌클릭) — 기존 로직 100% 유지
// ============================================
2026-01-29 06:58:38 +00:00
public void PerformNormalAttack()
{
2026-02-09 14:49:44 +00:00
if (Time.time < _lastAttackTime + attackCooldown || _isAttacking) return;
2026-01-29 06:58:38 +00:00
2026-02-09 14:49:44 +00:00
_pendingDamage = (playerStats != null) ? playerStats.TotalAttackDamage : 10f;
2026-02-06 04:20:12 +00:00
_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();
StartCoroutine(AttackRoutine());
2026-01-30 07:45:11 +00:00
}
2026-02-09 14:49:44 +00:00
// ============================================
// 차징 공격 (우클릭) — 기존 로직 100% 유지
// ============================================
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;
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;
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 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-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-09 14:49:44 +00:00
float baseDmg = (playerStats != null) ? playerStats.TotalAttackDamage : 10f;
_pendingDamage = baseDmg * currentStage.damageMult;
2026-02-06 04:20:12 +00:00
_pendingSpeed = normalSpeed * currentStage.rangeMult;
_pendingRange = normalRange * currentStage.rangeMult;
2026-01-31 13:07:35 +00:00
2026-02-09 14:49:44 +00:00
bool isMaxCharge = _chargeTimer >= maxChargeTime * 0.95f;
2026-02-06 09:27:08 +00:00
_pendingShootDirection = GetShootDirection(isMaxCharge);
2026-02-06 04:20:12 +00:00
if (pAnim != null) pAnim.TriggerThrow();
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-09 14:49:44 +00:00
// ============================================
// 에임 보정 시스템 — 기존 로직 100% 유지
// ============================================
2026-02-06 09:27:08 +00:00
private Vector3 GetMouseDirection()
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
2026-02-09 14:49:44 +00:00
Plane firePlane = new Plane(Vector3.up, firePoint.position);
2026-02-06 09:27:08 +00:00
2026-02-09 14:49:44 +00:00
if (firePlane.Raycast(ray, out float distance))
2026-02-06 09:27:08 +00:00
{
Vector3 worldMousePos = ray.GetPoint(distance);
Vector3 direction = (worldMousePos - firePoint.position).normalized;
2026-02-09 14:49:44 +00:00
direction.y = 0;
2026-02-06 09:27:08 +00:00
return direction;
}
return transform.forward;
}
private Vector3 GetShootDirection(bool isMaxCharge)
{
Vector3 mouseDir = GetMouseDirection();
2026-02-09 14:49:44 +00:00
if (!enableAutoAim || (onlyMaxCharge && !isMaxCharge)) return mouseDir;
2026-02-06 09:27:08 +00:00
Transform bestTarget = FindBestTarget(mouseDir);
if (bestTarget != null)
{
Vector3 targetPos = bestTarget.position + Vector3.up * 1.2f;
Vector3 targetDir = (targetPos - firePoint.position).normalized;
2026-02-09 14:49:44 +00:00
targetDir.y = 0;
2026-02-06 09:27:08 +00:00
Vector3 assistedDir = Vector3.Lerp(mouseDir, targetDir, aimAssistStrength);
assistedDir.Normalize();
return assistedDir;
}
return mouseDir;
}
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);
2026-02-09 14:49:44 +00:00
if (angle > autoAimAngle) continue;
2026-02-06 09:27:08 +00:00
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;
}
2026-02-09 14:49:44 +00:00
// ============================================
// [MODIFIED] 화살 생성 — 파티클 프리팹 발사
// ============================================
/// <summary>
/// 애니메이션 이벤트에서 호출됩니다.
/// 파티클 프리팹을 Instantiate하고 PlayerArrow 컴포넌트로 이동/충돌을 처리합니다.
/// </summary>
2026-02-06 04:20:12 +00:00
public void OnShootArrow()
{
2026-02-09 14:49:44 +00:00
if (_currentProjectilePrefab == null || firePoint == null)
2026-02-06 09:27:08 +00:00
{
2026-02-09 14:49:44 +00:00
Debug.LogWarning("발사체 프리팹 또는 firePoint가 없습니다!");
2026-02-06 09:27:08 +00:00
return;
}
2026-01-29 06:58:38 +00:00
2026-02-09 14:49:44 +00:00
// 파티클 프리팹을 발사 위치에 생성
Quaternion rotation = Quaternion.LookRotation(_pendingShootDirection);
GameObject projectile = Instantiate(_currentProjectilePrefab, firePoint.position, rotation);
// PlayerArrow 컴포넌트 확인/추가 후 초기화
PlayerArrow arrowScript = projectile.GetComponent<PlayerArrow>();
if (arrowScript == null)
2026-01-29 06:58:38 +00:00
{
2026-02-09 14:49:44 +00:00
arrowScript = projectile.AddComponent<PlayerArrow>();
2026-01-29 06:58:38 +00:00
}
2026-02-09 14:49:44 +00:00
// 발사 정보 + 속성 정보 전달
arrowScript.Initialize(
_pendingDamage,
_pendingSpeed,
_pendingRange,
_pendingShootDirection,
_currentElementType,
_currentElementDamage,
_currentElementDuration
);
2026-01-29 06:58:38 +00:00
}
2026-02-09 14:49:44 +00:00
// ============================================
// 유틸리티 — 기존 로직 100% 유지
// ============================================
2026-02-06 05:32:48 +00:00
public void OnAttackEnd()
{
_isAttacking = false;
ResetChargingEffects();
}
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-09 14:49:44 +00:00
// [DEPRECATED] 하위 호환성을 위해 남겨둠
[System.Obsolete("SetCurrentArrow(ArrowData)를 사용하세요.")]
2026-02-06 09:27:08 +00:00
public void SwapArrow(GameObject newArrow)
{
if (newArrow == null) return;
2026-02-09 14:49:44 +00:00
Debug.LogWarning("SwapArrow()는 더 이상 사용되지 않습니다. SetCurrentArrow()를 사용하세요.");
2026-02-06 09:27:08 +00:00
}
2026-02-06 04:20:12 +00:00
public void StartWeaponCollision() { }
public void StopWeaponCollision() { }
2026-02-06 09:27:08 +00:00
2026-02-09 14:49:44 +00:00
// ============================================
// Gizmo 디버그 — 기존 로직 100% 유지
// ============================================
2026-02-06 09:27:08 +00:00
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);
}
}
}
2026-01-29 06:58:38 +00:00
}