301 lines
8.8 KiB
C#
301 lines
8.8 KiB
C#
using Unity.Netcode;
|
|
using UnityEngine;
|
|
|
|
namespace Northbound
|
|
{
|
|
/// <summary>
|
|
/// 액션 - 공격 (팀 시스템 + 장비 시스템 적용)
|
|
/// </summary>
|
|
public class AttackAction : NetworkBehaviour, IAction
|
|
{
|
|
[Header("Attack Settings")]
|
|
public float attackRange = 2f;
|
|
public int attackDamage = 10;
|
|
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 NetworkVariable<bool> _isAttacking = new NetworkVariable<bool>(
|
|
false,
|
|
NetworkVariableReadPermission.Everyone,
|
|
NetworkVariableWritePermission.Owner
|
|
);
|
|
private bool _isWeaponEquipped = false;
|
|
private float _attackStartTime;
|
|
private const float ATTACK_TIMEOUT = 2f; // Auto-reset if animation event doesn't fire
|
|
|
|
private void Awake()
|
|
{
|
|
_animator = GetComponent<Animator>();
|
|
_teamMember = GetComponent<ITeamMember>();
|
|
_equipmentSocket = GetComponent<EquipmentSocket>();
|
|
}
|
|
|
|
public bool CanExecute(ulong playerId)
|
|
{
|
|
if (blockDuringAnimation && _isAttacking.Value)
|
|
return false;
|
|
|
|
return Time.time - _lastAttackTime >= attackCooldown;
|
|
}
|
|
|
|
public void Execute(ulong playerId)
|
|
{
|
|
if (!CanExecute(playerId))
|
|
return;
|
|
|
|
_lastAttackTime = Time.time;
|
|
_attackStartTime = Time.time;
|
|
|
|
// Owner writes directly (Owner permission on _isAttacking)
|
|
_isAttacking.Value = true;
|
|
|
|
// 장비 장착 (애니메이션 이벤트 사용 안 할 경우)
|
|
if (!useAnimationEvents && useEquipment && !_isWeaponEquipped)
|
|
{
|
|
if (equipmentData != null && equipmentData.attachOnStart)
|
|
{
|
|
AttachWeapon();
|
|
}
|
|
}
|
|
|
|
// 애니메이션 재생
|
|
PlayAttackAnimation();
|
|
|
|
// 애니메이션이 없으면 즉시 공격 실행
|
|
if (_animator == null || string.IsNullOrEmpty(attackAnimationTrigger))
|
|
{
|
|
PerformAttack();
|
|
_isAttacking.Value = false;
|
|
}
|
|
}
|
|
|
|
private void PerformAttack()
|
|
{
|
|
Vector3 attackOrigin = attackPoint != null ? attackPoint.position : transform.position;
|
|
Collider[] hits = Physics.OverlapSphere(attackOrigin, attackRange, 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(attackDamage, attackerNetworkId);
|
|
}
|
|
}
|
|
|
|
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()
|
|
{
|
|
PerformAttack();
|
|
}
|
|
|
|
public void OnAttackComplete()
|
|
{
|
|
if (IsOwner)
|
|
{
|
|
_isAttacking.Value = false;
|
|
}
|
|
|
|
if (useEquipment && equipmentData != null && equipmentData.detachOnEnd && !equipmentData.keepEquipped)
|
|
{
|
|
DetachWeapon();
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// 장비 관리 함수들
|
|
// ========================================
|
|
|
|
private void AttachWeapon(string socketName = null)
|
|
{
|
|
if (_equipmentSocket == null || equipmentData == null)
|
|
return;
|
|
|
|
if (equipmentData.equipmentPrefab == null)
|
|
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, attackRange);
|
|
}
|
|
|
|
public override void OnNetworkSpawn()
|
|
{
|
|
base.OnNetworkSpawn();
|
|
|
|
// Subscribe to attack state changes
|
|
_isAttacking.OnValueChanged += OnAttackStateChanged;
|
|
}
|
|
|
|
public override void OnNetworkDespawn()
|
|
{
|
|
_isAttacking.OnValueChanged -= OnAttackStateChanged;
|
|
base.OnNetworkDespawn();
|
|
}
|
|
|
|
private void OnAttackStateChanged(bool previousValue, bool newValue)
|
|
{
|
|
}
|
|
|
|
public void Update()
|
|
{
|
|
if (!IsOwner) return;
|
|
|
|
if (_isAttacking.Value && Time.time - _attackStartTime > ATTACK_TIMEOUT)
|
|
{
|
|
_isAttacking.Value = false;
|
|
}
|
|
}
|
|
|
|
public override void OnDestroy()
|
|
{
|
|
// 무기 정리
|
|
if (_isWeaponEquipped)
|
|
{
|
|
DetachWeapon();
|
|
}
|
|
|
|
base.OnDestroy();
|
|
}
|
|
|
|
public bool IsAttacking => _isAttacking.Value;
|
|
}
|
|
} |