Files
Colosseum/Assets/_Game/Scripts/Player/PlayerSkillInput.cs
dal4segno 8d21922e2f feat: 플레이어 역할별 스킬 프리셋 적용 경로 추가
- 탱커, 지원, 딜러 기본 슬롯 프리셋을 로컬 플레이어에 즉시 적용하는 디버그 메뉴를 추가
- PlayerSkillInput에 7칸 슬롯 자동 보정과 일괄 갱신 API를 넣어 기존 프리팹 직렬화와 호환되도록 정리
- SkillQuickSlotUI가 스킬 슬롯 변경 이벤트를 구독해 프리셋 적용 시 액션바가 즉시 갱신되도록 보강
- 플레이 검증에서 탱커, 지원, 딜러 프리셋이 모두 기대한 슬롯 순서와 Ctrl 회피 슬롯까지 정상 적용되는 것을 확인
2026-03-25 00:14:22 +09:00

379 lines
12 KiB
C#

using UnityEngine;
using UnityEngine.InputSystem;
using Unity.Netcode;
using System;
using System.Collections.Generic;
using Colosseum.Skills;
using Colosseum.Weapons;
namespace Colosseum.Player
{
/// <summary>
/// 플레이어 스킬 입력 처리.
/// 논타겟 방식: 입력 시 즉시 스킬 시전
/// </summary>
[RequireComponent(typeof(PlayerActionState))]
public class PlayerSkillInput : NetworkBehaviour
{
private const int ExpectedSkillSlotCount = 7;
[Header("Skill Slots")]
[Tooltip("각 슬롯에 등록할 스킬 데이터 (6개 + 추가 슬롯)")]
[SerializeField] private SkillData[] skillSlots = new SkillData[ExpectedSkillSlotCount];
[Header("References")]
[Tooltip("SkillController (없으면 자동 검색)")]
[SerializeField] private SkillController skillController;
[Tooltip("PlayerNetworkController (없으면 자동 검색)")]
[SerializeField] private PlayerNetworkController networkController;
[Tooltip("WeaponEquipment (없으면 자동 검색)")]
[SerializeField] private WeaponEquipment weaponEquipment;
[Tooltip("행동 상태 관리자 (없으면 자동 검색)")]
[SerializeField] private PlayerActionState actionState;
private InputSystem_Actions inputActions;
public SkillData[] SkillSlots => skillSlots;
/// <summary>
/// 스킬 슬롯 구성이 변경되었을 때 호출됩니다.
/// </summary>
public event Action OnSkillSlotsChanged;
public override void OnNetworkSpawn()
{
EnsureSkillSlotCapacity();
if (!IsOwner)
{
enabled = false;
return;
}
// SkillController 참조 확인
if (skillController == null)
{
skillController = GetComponent<SkillController>();
if (skillController == null)
{
Debug.LogError("PlayerSkillInput: SkillController not found!");
enabled = false;
return;
}
}
// PlayerNetworkController 참조 확인
if (networkController == null)
{
networkController = GetComponent<PlayerNetworkController>();
}
// WeaponEquipment 참조 확인
if (weaponEquipment == null)
{
weaponEquipment = GetComponent<WeaponEquipment>();
}
if (actionState == null)
{
actionState = GetOrCreateActionState();
}
InitializeInputActions();
}
private void InitializeInputActions()
{
if (inputActions == null)
{
inputActions = new InputSystem_Actions();
inputActions.Player.Skill1.performed += OnSkill1Performed;
inputActions.Player.Skill2.performed += OnSkill2Performed;
inputActions.Player.Skill3.performed += OnSkill3Performed;
inputActions.Player.Skill4.performed += OnSkill4Performed;
inputActions.Player.Skill5.performed += OnSkill5Performed;
inputActions.Player.Skill6.performed += OnSkill6Performed;
inputActions.Player.Evade.performed += OnEvadePerformed;
}
inputActions.Player.Enable();
}
public override void OnNetworkDespawn()
{
CleanupInputActions();
}
private void OnDisable()
{
CleanupInputActions();
}
private void Awake()
{
EnsureSkillSlotCapacity();
}
private void OnValidate()
{
EnsureSkillSlotCapacity();
}
private void OnEnable()
{
if (IsOwner && inputActions != null)
{
inputActions.Player.Enable();
}
}
/// <summary>
/// 기존 프리팹이나 씬 직렬화 데이터가 6칸으로 남아 있어도
/// 긴급 회피 슬롯까지 포함한 7칸 구성을 항상 보장합니다.
/// </summary>
private void EnsureSkillSlotCapacity()
{
if (skillSlots != null && skillSlots.Length == ExpectedSkillSlotCount)
return;
SkillData[] resizedSlots = new SkillData[ExpectedSkillSlotCount];
if (skillSlots != null)
{
int copyCount = Mathf.Min(skillSlots.Length, resizedSlots.Length);
for (int i = 0; i < copyCount; i++)
{
resizedSlots[i] = skillSlots[i];
}
}
skillSlots = resizedSlots;
}
private void CleanupInputActions()
{
if (inputActions != null)
{
inputActions.Player.Disable();
}
}
/// <summary>
/// 스킬 입력 처리
/// </summary>
private void OnSkillInput(int slotIndex)
{
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
return;
SkillData skill = skillSlots[slotIndex];
if (skill == null)
{
Debug.Log($"Skill slot {slotIndex + 1} is empty");
return;
}
// 사망 상태 체크
if (actionState != null && !actionState.CanStartSkill(skill))
return;
// 로컬 체크 (빠른 피드백용)
if (skillController.IsExecutingSkill)
{
Debug.Log($"Already executing skill");
return;
}
if (skillController.IsOnCooldown(skill))
{
Debug.Log($"Skill {skill.SkillName} is on cooldown");
return;
}
// 마나 비용 체크 (무기 배율 적용)
float actualManaCost = GetActualManaCost(skill);
if (networkController != null && networkController.Mana < actualManaCost)
{
Debug.Log($"Not enough mana for skill: {skill.SkillName}");
return;
}
// 서버에 스킬 실행 요청
RequestSkillExecutionRpc(slotIndex);
}
/// <summary>
/// 서버에 스킬 실행 요청
/// </summary>
[Rpc(SendTo.Server)]
private void RequestSkillExecutionRpc(int slotIndex)
{
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
return;
SkillData skill = skillSlots[slotIndex];
if (skill == null) return;
// 서버에서 다시 검증
if (actionState != null && !actionState.CanStartSkill(skill))
return;
if (skillController.IsExecutingSkill || skillController.IsOnCooldown(skill))
return;
// 마나 비용 체크 (무기 배율 적용)
float actualManaCost = GetActualManaCost(skill);
if (networkController != null && networkController.Mana < actualManaCost)
return;
// 마나 소모 (무기 배율 적용)
if (networkController != null && actualManaCost > 0)
{
networkController.UseManaRpc(actualManaCost);
}
// 모든 클라이언트에 스킬 실행 전파
BroadcastSkillExecutionRpc(slotIndex);
}
/// <summary>
/// 모든 클라이언트에 스킬 실행 전파
/// </summary>
[Rpc(SendTo.ClientsAndHost)]
private void BroadcastSkillExecutionRpc(int slotIndex)
{
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
return;
SkillData skill = skillSlots[slotIndex];
if (skill == null) return;
// 모든 클라이언트에서 스킬 실행 (애니메이션 포함)
skillController.ExecuteSkill(skill);
}
/// <summary>
/// 무기 마나 배율이 적용된 실제 마나 비용 계산
/// </summary>
private float GetActualManaCost(SkillData skill)
{
if (skill == null) return 0f;
float baseCost = skill.ManaCost;
float multiplier = weaponEquipment != null ? weaponEquipment.ManaCostMultiplier : 1f;
return baseCost * multiplier;
}
/// <summary>
/// 스킬 슬롯 접근자
/// </summary>
public SkillData GetSkill(int slotIndex)
{
EnsureSkillSlotCapacity();
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
return null;
return skillSlots[slotIndex];
}
/// <summary>
/// 스킬 슬롯 변경
/// </summary>
public void SetSkill(int slotIndex, SkillData skill)
{
EnsureSkillSlotCapacity();
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
return;
skillSlots[slotIndex] = skill;
OnSkillSlotsChanged?.Invoke();
}
/// <summary>
/// 전체 스킬 슬롯을 한 번에 갱신합니다.
/// </summary>
public void SetSkills(IReadOnlyList<SkillData> skills)
{
EnsureSkillSlotCapacity();
if (skills == null)
return;
int count = Mathf.Min(skillSlots.Length, skills.Count);
for (int i = 0; i < count; i++)
{
skillSlots[i] = skills[i];
}
for (int i = count; i < skillSlots.Length; i++)
{
skillSlots[i] = null;
}
OnSkillSlotsChanged?.Invoke();
}
/// <summary>
/// 남은 쿨타임 조회
/// </summary>
public float GetRemainingCooldown(int slotIndex)
{
SkillData skill = GetSkill(slotIndex);
if (skill == null) return 0f;
return skillController.GetRemainingCooldown(skill);
}
/// <summary>
/// 스킬 사용 가능 여부
/// </summary>
public bool CanUseSkill(int slotIndex)
{
SkillData skill = GetSkill(slotIndex);
if (skill == null) return false;
if (actionState != null && !actionState.CanStartSkill(skill))
return false;
return !skillController.IsOnCooldown(skill) && !skillController.IsExecutingSkill;
}
/// <summary>
/// 디버그용 스킬 시전 진입점입니다.
/// 로컬 플레이어 검증 시 지정한 슬롯의 스킬을 즉시 요청합니다.
/// </summary>
public void DebugCastSkill(int slotIndex)
{
if (!IsOwner)
return;
OnSkillInput(slotIndex);
}
private void OnSkill1Performed(InputAction.CallbackContext context) => OnSkillInput(0);
private void OnSkill2Performed(InputAction.CallbackContext context) => OnSkillInput(1);
private void OnSkill3Performed(InputAction.CallbackContext context) => OnSkillInput(2);
private void OnSkill4Performed(InputAction.CallbackContext context) => OnSkillInput(3);
private void OnSkill5Performed(InputAction.CallbackContext context) => OnSkillInput(4);
private void OnSkill6Performed(InputAction.CallbackContext context) => OnSkillInput(5);
private void OnEvadePerformed(InputAction.CallbackContext context) => OnSkillInput(6);
private PlayerActionState GetOrCreateActionState()
{
var foundState = GetComponent<PlayerActionState>();
if (foundState != null)
return foundState;
return gameObject.AddComponent<PlayerActionState>();
}
}
}