Files
Northbound/Assets/Scripts/AttackAction.cs
dal4segno d066290607 플레이어 체력 자연 회복 기능 추가
전투 상태 감지 기능
player stat으로 관리 가능
2026-02-25 21:09:19 +09:00

285 lines
8.7 KiB
C#

using Unity.Netcode;
using UnityEngine;
namespace Northbound
{
/// <summary>
/// 액션 - 공격 (팀 시스템 + 장비 시스템 적용)
/// </summary>
public class AttackAction : NetworkBehaviour, IAction
{
[Header("Attack Settings")]
public float attackCooldown = 0.5f;
public LayerMask attackableLayer = ~0;
[Header("Animation")]
public string attackAnimationTrigger = "Attack";
public bool useAnimationEvents = true;
public bool blockDuringAnimation = true;
[Header("Equipment")]
public bool useEquipment = true;
public EquipmentData equipmentData;
[Header("Visual")]
public GameObject attackEffectPrefab;
public Transform attackPoint;
private float _lastAttackTime;
private Animator _animator;
private ITeamMember _teamMember;
private EquipmentSocket _equipmentSocket;
private bool _isAttacking = false;
private bool _isWeaponEquipped = false;
private PlayerStats _playerStats;
private NetworkPlayerController _networkPlayerController;
private void Awake()
{
_animator = GetComponent<Animator>();
_teamMember = GetComponent<ITeamMember>();
_equipmentSocket = GetComponent<EquipmentSocket>();
_playerStats = GetComponent<PlayerStats>();
_networkPlayerController = GetComponent<NetworkPlayerController>();
}
public bool CanExecute(ulong playerId)
{
if (blockDuringAnimation && _isAttacking)
return false;
return Time.time - _lastAttackTime >= attackCooldown;
}
public void Execute(ulong playerId)
{
if (!CanExecute(playerId))
return;
_lastAttackTime = Time.time;
_isAttacking = true;
// 장비 장착 (애니메이션 이벤트 사용 안 할 경우)
if (!useAnimationEvents && useEquipment && !_isWeaponEquipped)
{
if (equipmentData != null && equipmentData.attachOnStart)
{
AttachWeapon();
}
}
// 애니메이션 재생
PlayAttackAnimation();
// 애니메이션이 없으면 즉시 공격 실행
if (_animator == null || string.IsNullOrEmpty(attackAnimationTrigger))
{
PerformAttack();
_isAttacking = false;
}
}
private void PerformAttack()
{
Vector3 attackOrigin = attackPoint != null ? attackPoint.position : transform.position;
Collider[] hits = Physics.OverlapSphere(attackOrigin, GetAttackRange(), attackableLayer);
foreach (Collider hit in hits)
{
if (hit.transform.root == transform.root)
continue;
var targetDamageable = hit.GetComponent<IDamageable>();
var targetTeamMember = hit.GetComponent<ITeamMember>();
if (targetDamageable != null)
{
if (_teamMember != null && targetTeamMember != null)
{
if (!TeamManager.CanAttack(_teamMember, targetTeamMember))
{
continue;
}
}
var netObj = hit.GetComponent<NetworkObject>();
if (netObj != null)
{
AttackServerRpc(NetworkObjectId, netObj.NetworkObjectId);
}
}
}
}
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void AttackServerRpc(ulong attackerNetworkId, ulong targetNetworkId)
{
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(targetNetworkId, out NetworkObject targetObj))
{
var damageable = targetObj.GetComponent<IDamageable>();
damageable?.TakeDamage(_playerStats?.GetDamage() ?? 10, attackerNetworkId);
// 공격자를 전투 상태로 기록 (체력 회복 방지)
if (_networkPlayerController != null)
{
_networkPlayerController.MarkInCombat();
}
}
}
private void PlayAttackAnimation()
{
if (_animator != null && !string.IsNullOrEmpty(attackAnimationTrigger))
{
_animator.SetTrigger(attackAnimationTrigger);
}
if (attackEffectPrefab != null && attackPoint != null)
{
GameObject effect = Instantiate(attackEffectPrefab, attackPoint.position, attackPoint.rotation);
Destroy(effect, 1f);
}
}
// ========================================
// Animation Event 함수들
// ========================================
public void OnEquipWeapon()
{
if (!useAnimationEvents || !useEquipment) return;
AttachWeapon();
}
public void OnEquipWeapon(string socketName)
{
if (!useAnimationEvents || !useEquipment) return;
AttachWeapon(socketName);
}
public void OnUnequipWeapon()
{
if (!useAnimationEvents || !useEquipment) return;
DetachWeapon();
}
public void OnUnequipWeapon(string socketName)
{
if (!useAnimationEvents || !useEquipment) return;
DetachWeapon(socketName);
}
public void OnAttackHit()
{
// 로컬 플레이어만 공격 수행 (중복 데미지 방지)
if (_networkPlayerController != null && !_networkPlayerController.IsLocalPlayer)
return;
PerformAttack();
}
public void OnAttackComplete()
{
_isAttacking = false;
if (useEquipment && equipmentData != null && equipmentData.detachOnEnd && !equipmentData.keepEquipped)
{
DetachWeapon();
}
}
// ========================================
// 장비 관리 함수들
// ========================================
private void AttachWeapon(string socketName = null)
{
if (_equipmentSocket == null || equipmentData == null)
{
Debug.LogWarning("[AttackAction] EquipmentSocket 또는 EquipmentData가 없습니다.");
return;
}
if (equipmentData.equipmentPrefab == null)
{
Debug.LogWarning("[AttackAction] 무기 프리팹이 설정되지 않았습니다.");
return;
}
string socket = socketName ?? equipmentData.socketName;
_equipmentSocket.AttachToSocket(socket, equipmentData.equipmentPrefab);
_isWeaponEquipped = true;
}
private void DetachWeapon(string socketName = null)
{
if (_equipmentSocket == null)
return;
string socket = socketName ?? equipmentData?.socketName;
if (!string.IsNullOrEmpty(socket))
{
_equipmentSocket.DetachFromSocket(socket);
_isWeaponEquipped = false;
}
}
public void EquipWeapon()
{
if (!_isWeaponEquipped && useEquipment)
{
AttachWeapon();
}
}
public void UnequipWeapon()
{
if (_isWeaponEquipped)
{
DetachWeapon();
}
}
public string GetActionName()
{
return "Attack";
}
public string GetActionAnimation()
{
return attackAnimationTrigger;
}
public EquipmentData GetEquipmentData()
{
return equipmentData;
}
private void OnDrawGizmosSelected()
{
Vector3 attackOrigin = attackPoint != null ? attackPoint.position : transform.position;
Gizmos.color = Color.red;
Gizmos.DrawWireSphere(attackOrigin, GetAttackRange());
}
public override void OnDestroy()
{
// 무기 정리
if (_isWeaponEquipped)
{
DetachWeapon();
}
base.OnDestroy();
}
public bool IsAttacking => _isAttacking;
/// <summary>
/// 공격 범위 반환 (PlayerStats 우선)
/// </summary>
public float GetAttackRange() => _playerStats?.GetAttackRange() ?? 2f;
}
}