using UnityEngine; using System.Collections; using System.Collections.Generic; public class PlayerAttack : MonoBehaviour { [Header("--- 활 설정 ---")] [SerializeField] private Transform firePoint; [SerializeField] private PlayerAnimator pAnim; [Header("--- 스탯 참조 ---")] [SerializeField] private Stats playerStats; [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 chargeStages; [Header("--- 에임 보정 설정 ---")] [SerializeField] private bool enableAutoAim = true; [SerializeField] private float autoAimRange = 15f; [SerializeField] private float autoAimAngle = 45f; [Range(0f, 1f)] [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; private float _lastAttackTime; private float _chargeTimer; private bool _isCharging = false; private bool _isAttacking = false; private bool _waitForRelease = false; private float _pendingDamage; private float _pendingSpeed; private float _pendingRange; private Vector3 _pendingShootDirection; public bool IsCharging => _isCharging; public bool IsAttacking { get => _isAttacking; set => _isAttacking = value; } public float ChargeProgress => Mathf.Clamp01(_chargeTimer / maxChargeTime); // [NEW] 외부에서 현재 속성 확인용 public ArrowElementType CurrentElement => _currentElementType; public GameObject CurrentProjectilePrefab => _currentProjectilePrefab; private void Start() { if (playerStats == null) playerStats = GetComponentInParent(); // 기본 발사체 프리팹 설정 if (_currentProjectilePrefab == null) _currentProjectilePrefab = defaultProjectilePrefab; if (chargeStages == null || chargeStages.Count == 0) { chargeStages = new List { 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 } }; } float calculatedMaxTime = 0f; foreach (var stage in chargeStages) { if (stage.chargeTime > calculatedMaxTime) calculatedMaxTime = stage.chargeTime; } maxChargeTime = Mathf.Max(calculatedMaxTime, 0.1f); } private void Update() { if (_isCharging) _chargeTimer += Time.deltaTime; } // ============================================ // [NEW] 화살 장착 (SwapArrow 대체) // ============================================ /// /// ArrowPickup에서 습득 시 호출합니다. /// 파티클 프리팹 + 속성 정보를 저장합니다. /// 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"); } /// /// 기본 화살로 초기화 /// public void ResetArrow() { _currentProjectilePrefab = defaultProjectilePrefab; _currentElementType = ArrowElementType.None; _currentElementDamage = 0f; _currentElementDuration = 0f; Debug.Log("화살 초기화: 기본 화살"); } // ============================================ // 일반 공격 (좌클릭) — 기존 로직 100% 유지 // ============================================ public void PerformNormalAttack() { if (Time.time < _lastAttackTime + attackCooldown || _isAttacking) return; _pendingDamage = (playerStats != null) ? playerStats.TotalAttackDamage : 10f; _pendingSpeed = normalSpeed; _pendingRange = normalRange; _pendingShootDirection = GetMouseDirection(); _lastAttackTime = Time.time; if (pAnim != null) pAnim.TriggerThrow(); StartCoroutine(AttackRoutine()); } // ============================================ // 차징 공격 (우클릭) — 기존 로직 100% 유지 // ============================================ public void StartCharging() { if (_waitForRelease) return; _isCharging = true; _chargeTimer = 0f; if (pAnim != null) pAnim.SetCharging(true); if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(true); } private void ResetChargingEffects() { _isCharging = false; _chargeTimer = 0f; if (pAnim != null) pAnim.SetCharging(false); if (CinemachineShake.Instance != null) CinemachineShake.Instance.SetZoom(false); } public void CancelCharging() { ResetChargingEffects(); _waitForRelease = false; } public void ReleaseAttack() { if (!_isCharging) return; ChargeStage currentStage = chargeStages[0]; foreach (var stage in chargeStages) { if (_chargeTimer >= stage.chargeTime) currentStage = stage; } float baseDmg = (playerStats != null) ? playerStats.TotalAttackDamage : 10f; _pendingDamage = baseDmg * currentStage.damageMult; _pendingSpeed = normalSpeed * currentStage.rangeMult; _pendingRange = normalRange * currentStage.rangeMult; bool isMaxCharge = _chargeTimer >= maxChargeTime * 0.95f; _pendingShootDirection = GetShootDirection(isMaxCharge); if (pAnim != null) pAnim.TriggerThrow(); _waitForRelease = true; _lastAttackTime = Time.time; StartCoroutine(AttackRoutine()); } // ============================================ // 에임 보정 시스템 — 기존 로직 100% 유지 // ============================================ private Vector3 GetMouseDirection() { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); Plane firePlane = new Plane(Vector3.up, firePoint.position); if (firePlane.Raycast(ray, out float distance)) { Vector3 worldMousePos = ray.GetPoint(distance); Vector3 direction = (worldMousePos - firePoint.position).normalized; direction.y = 0; return direction; } return transform.forward; } 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; 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); 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; } // ============================================ // [MODIFIED] 화살 생성 — 파티클 프리팹 발사 // ============================================ /// /// 애니메이션 이벤트에서 호출됩니다. /// 파티클 프리팹을 Instantiate하고 PlayerArrow 컴포넌트로 이동/충돌을 처리합니다. /// public void OnShootArrow() { if (_currentProjectilePrefab == null || firePoint == null) { Debug.LogWarning("발사체 프리팹 또는 firePoint가 없습니다!"); return; } // 파티클 프리팹을 발사 위치에 생성 Quaternion rotation = Quaternion.LookRotation(_pendingShootDirection); GameObject projectile = Instantiate(_currentProjectilePrefab, firePoint.position, rotation); // PlayerArrow 컴포넌트 확인/추가 후 초기화 PlayerArrow arrowScript = projectile.GetComponent(); if (arrowScript == null) { arrowScript = projectile.AddComponent(); } // 발사 정보 + 속성 정보 전달 arrowScript.Initialize( _pendingDamage, _pendingSpeed, _pendingRange, _pendingShootDirection, _currentElementType, _currentElementDamage, _currentElementDuration ); } // ============================================ // 유틸리티 — 기존 로직 100% 유지 // ============================================ public void OnAttackEnd() { _isAttacking = false; ResetChargingEffects(); } private IEnumerator AttackRoutine() { _isAttacking = true; yield return new WaitForSeconds(0.6f); if (_isAttacking) { _isAttacking = false; ResetChargingEffects(); } } // [DEPRECATED] 하위 호환성을 위해 남겨둠 [System.Obsolete("SetCurrentArrow(ArrowData)를 사용하세요.")] public void SwapArrow(GameObject newArrow) { if (newArrow == null) return; Debug.LogWarning("SwapArrow()는 더 이상 사용되지 않습니다. SetCurrentArrow()를 사용하세요."); } public void StartWeaponCollision() { } public void StopWeaponCollision() { } // ============================================ // Gizmo 디버그 — 기존 로직 100% 유지 // ============================================ 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); } } } }