- PlayerNetworkController에 Revive(healthPercent) 메서드와 OnRevived 이벤트 추가 - ReviveEffect 스킬 이펙트 구현 (Area/Ally 타겟팅, healthPercent로 체력 복구) - GameManager가 OnRevived 구독하여 alivePlayers 동적 복구 - GameManager.Update에서 나중에 스폰된 플레이어 자동 구독 (MPP 대응) - SkillCancelReason에 Revive 추가 - 부활 스킬/이펙트 ScriptableObject 에셋 생성 (치유 클립 임시 사용) - PlayerSkillDebugMenu에 즉사/부활/리스폰/스폰/Client1 스킬 디버그 메뉴 추가 - PlayerAbnormalityDebugHUD에 부활 버튼 추가 - DebugExecuteSkillAsServer에 실패 원인 로그 추가 - AGENTS.md에 코드 변경 후 force reload 규칙 추가
769 lines
27 KiB
C#
769 lines
27 KiB
C#
using UnityEngine;
|
|
using UnityEngine.InputSystem;
|
|
using Unity.Netcode;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
|
|
using Colosseum.Skills;
|
|
using Colosseum.Passives;
|
|
using Colosseum.Weapons;
|
|
|
|
#if UNITY_EDITOR
|
|
using System.IO;
|
|
using System.Text.RegularExpressions;
|
|
|
|
using UnityEditor;
|
|
#endif
|
|
|
|
namespace Colosseum.Player
|
|
{
|
|
/// <summary>
|
|
/// 플레이어 스킬 입력 처리.
|
|
/// 논타겟 방식: 입력 시 즉시 스킬 시전
|
|
/// </summary>
|
|
[RequireComponent(typeof(PlayerActionState))]
|
|
public class PlayerSkillInput : NetworkBehaviour
|
|
{
|
|
private const int ExpectedSkillSlotCount = 7;
|
|
|
|
#if UNITY_EDITOR
|
|
private static readonly string[] TankLoadoutPaths =
|
|
{
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_베기.asset",
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_도발.asset",
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_방어태세.asset",
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_보호막.asset",
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_철벽.asset",
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_찌르기.asset",
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_구르기.asset",
|
|
};
|
|
|
|
private static readonly string[] SupportLoadoutPaths =
|
|
{
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_베기.asset",
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_치유.asset",
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_광역치유.asset",
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_보호막.asset",
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_방어태세.asset",
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_투사체.asset",
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_부활.asset",
|
|
};
|
|
|
|
private static readonly string[] DpsLoadoutPaths =
|
|
{
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_베기.asset",
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_찌르기.asset",
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_젬테스트공격.asset",
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_투사체.asset",
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_보호막.asset",
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_방어태세.asset",
|
|
"Assets/_Game/Data/Skills/Data_Skill_Player_구르기.asset",
|
|
};
|
|
#endif
|
|
|
|
[Header("Skill Slots")]
|
|
[Tooltip("각 슬롯에 등록할 스킬 데이터 (6개 + 추가 슬롯)")]
|
|
[SerializeField] private SkillData[] skillSlots = new SkillData[ExpectedSkillSlotCount];
|
|
[Tooltip("각 슬롯의 베이스 스킬 + 젬 조합")]
|
|
[SerializeField] private SkillLoadoutEntry[] skillLoadoutEntries = new SkillLoadoutEntry[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;
|
|
private bool gameplayInputEnabled = true;
|
|
|
|
public SkillData[] SkillSlots => skillSlots;
|
|
public SkillLoadoutEntry[] SkillLoadoutEntries => skillLoadoutEntries;
|
|
|
|
/// <summary>
|
|
/// 스킬 슬롯 구성이 변경되었을 때 호출됩니다.
|
|
/// </summary>
|
|
public event Action OnSkillSlotsChanged;
|
|
|
|
public override void OnNetworkSpawn()
|
|
{
|
|
EnsureSkillSlotCapacity();
|
|
EnsureSkillLoadoutCapacity();
|
|
SyncLegacySkillsToLoadoutEntries();
|
|
EnsureRuntimeReferences();
|
|
ApplyEditorMultiplayerLoadoutIfNeeded();
|
|
|
|
if (!IsOwner)
|
|
{
|
|
enabled = false;
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
SetGameplayInputEnabled(true);
|
|
}
|
|
|
|
public override void OnNetworkDespawn()
|
|
{
|
|
CleanupInputActions();
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
CleanupInputActions();
|
|
}
|
|
|
|
private void Awake()
|
|
{
|
|
EnsureSkillSlotCapacity();
|
|
EnsureSkillLoadoutCapacity();
|
|
SyncLegacySkillsToLoadoutEntries();
|
|
}
|
|
|
|
private void OnValidate()
|
|
{
|
|
EnsureSkillSlotCapacity();
|
|
EnsureSkillLoadoutCapacity();
|
|
SyncLegacySkillsToLoadoutEntries();
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
if (IsOwner && inputActions != null)
|
|
{
|
|
SetGameplayInputEnabled(gameplayInputEnabled);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 로컬 플레이어의 스킬 입력을 일시적으로 차단하거나 복구합니다.
|
|
/// </summary>
|
|
public void SetGameplayInputEnabled(bool enabled)
|
|
{
|
|
gameplayInputEnabled = enabled;
|
|
|
|
if (!IsOwner || inputActions == null)
|
|
return;
|
|
|
|
if (enabled)
|
|
{
|
|
inputActions.Player.Enable();
|
|
return;
|
|
}
|
|
|
|
inputActions.Player.Disable();
|
|
}
|
|
|
|
/// <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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 슬롯별 로드아웃 엔트리 배열을 보정합니다.
|
|
/// </summary>
|
|
private void EnsureSkillLoadoutCapacity()
|
|
{
|
|
if (skillLoadoutEntries == null || skillLoadoutEntries.Length != ExpectedSkillSlotCount)
|
|
{
|
|
SkillLoadoutEntry[] resizedEntries = new SkillLoadoutEntry[ExpectedSkillSlotCount];
|
|
if (skillLoadoutEntries != null)
|
|
{
|
|
int copyCount = Mathf.Min(skillLoadoutEntries.Length, resizedEntries.Length);
|
|
for (int i = 0; i < copyCount; i++)
|
|
{
|
|
resizedEntries[i] = skillLoadoutEntries[i];
|
|
}
|
|
}
|
|
|
|
skillLoadoutEntries = resizedEntries;
|
|
}
|
|
|
|
for (int i = 0; i < skillLoadoutEntries.Length; i++)
|
|
{
|
|
if (skillLoadoutEntries[i] == null)
|
|
skillLoadoutEntries[i] = new SkillLoadoutEntry();
|
|
|
|
skillLoadoutEntries[i].EnsureGemSlotCapacity();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 기존 SkillData 직렬화와 새 로드아웃 엔트리 구조를 동기화합니다.
|
|
/// </summary>
|
|
private void SyncLegacySkillsToLoadoutEntries()
|
|
{
|
|
EnsureSkillSlotCapacity();
|
|
EnsureSkillLoadoutCapacity();
|
|
|
|
for (int i = 0; i < skillSlots.Length; i++)
|
|
{
|
|
SkillLoadoutEntry entry = skillLoadoutEntries[i];
|
|
SkillData legacySkill = skillSlots[i];
|
|
|
|
if (entry.BaseSkill == null && legacySkill != null)
|
|
{
|
|
entry.SetBaseSkill(legacySkill);
|
|
}
|
|
else if (legacySkill == null && entry.BaseSkill != null)
|
|
{
|
|
skillSlots[i] = entry.BaseSkill;
|
|
}
|
|
else if (entry.BaseSkill != legacySkill)
|
|
{
|
|
skillSlots[i] = entry.BaseSkill;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CleanupInputActions()
|
|
{
|
|
if (inputActions != null)
|
|
{
|
|
inputActions.Player.Disable();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 스킬 입력 처리
|
|
/// </summary>
|
|
private void OnSkillInput(int slotIndex)
|
|
{
|
|
if (!gameplayInputEnabled)
|
|
return;
|
|
|
|
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
|
|
return;
|
|
|
|
SkillLoadoutEntry loadoutEntry = GetSkillLoadout(slotIndex);
|
|
SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null;
|
|
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(loadoutEntry);
|
|
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;
|
|
|
|
SkillLoadoutEntry loadoutEntry = GetSkillLoadout(slotIndex);
|
|
SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null;
|
|
if (skill == null) return;
|
|
|
|
// 서버에서 다시 검증
|
|
if (actionState != null && !actionState.CanStartSkill(skill))
|
|
return;
|
|
|
|
if (skillController.IsExecutingSkill || skillController.IsOnCooldown(skill))
|
|
return;
|
|
|
|
// 마나 비용 체크 (무기 배율 적용)
|
|
float actualManaCost = GetActualManaCost(loadoutEntry);
|
|
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;
|
|
|
|
SkillLoadoutEntry loadoutEntry = GetSkillLoadout(slotIndex);
|
|
SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null;
|
|
if (skill == null) return;
|
|
|
|
// 모든 클라이언트에서 스킬 실행 (애니메이션 포함)
|
|
skillController.ExecuteSkill(loadoutEntry);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 무기 마나 배율이 적용된 실제 마나 비용 계산
|
|
/// </summary>
|
|
private float GetActualManaCost(SkillLoadoutEntry loadoutEntry)
|
|
{
|
|
if (loadoutEntry == null || loadoutEntry.BaseSkill == null) return 0f;
|
|
|
|
float baseCost = loadoutEntry.GetResolvedManaCost();
|
|
float multiplier = weaponEquipment != null ? weaponEquipment.ManaCostMultiplier : 1f;
|
|
float passiveMultiplier = PassiveRuntimeModifierUtility.GetManaCostMultiplier(gameObject, loadoutEntry.BaseSkill);
|
|
|
|
return baseCost * multiplier * passiveMultiplier;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 스킬 슬롯 접근자
|
|
/// </summary>
|
|
public SkillData GetSkill(int slotIndex)
|
|
{
|
|
EnsureSkillSlotCapacity();
|
|
EnsureSkillLoadoutCapacity();
|
|
SyncLegacySkillsToLoadoutEntries();
|
|
|
|
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
|
|
return null;
|
|
return skillLoadoutEntries[slotIndex] != null ? skillLoadoutEntries[slotIndex].BaseSkill : skillSlots[slotIndex];
|
|
}
|
|
|
|
/// <summary>
|
|
/// 슬롯 엔트리 접근자
|
|
/// </summary>
|
|
public SkillLoadoutEntry GetSkillLoadout(int slotIndex)
|
|
{
|
|
EnsureSkillSlotCapacity();
|
|
EnsureSkillLoadoutCapacity();
|
|
SyncLegacySkillsToLoadoutEntries();
|
|
|
|
if (slotIndex < 0 || slotIndex >= skillLoadoutEntries.Length)
|
|
return null;
|
|
|
|
return skillLoadoutEntries[slotIndex];
|
|
}
|
|
|
|
/// <summary>
|
|
/// 스킬 슬롯 변경
|
|
/// </summary>
|
|
public void SetSkill(int slotIndex, SkillData skill)
|
|
{
|
|
EnsureSkillSlotCapacity();
|
|
|
|
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
|
|
return;
|
|
|
|
skillSlots[slotIndex] = skill;
|
|
skillLoadoutEntries[slotIndex].SetBaseSkill(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];
|
|
skillLoadoutEntries[i].SetBaseSkill(skills[i]);
|
|
}
|
|
|
|
for (int i = count; i < skillSlots.Length; i++)
|
|
{
|
|
skillSlots[i] = null;
|
|
skillLoadoutEntries[i].SetBaseSkill(null);
|
|
}
|
|
|
|
OnSkillSlotsChanged?.Invoke();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 슬롯 엔트리를 직접 설정합니다.
|
|
/// </summary>
|
|
public void SetSkillLoadout(int slotIndex, SkillLoadoutEntry loadoutEntry)
|
|
{
|
|
EnsureSkillSlotCapacity();
|
|
EnsureSkillLoadoutCapacity();
|
|
|
|
if (slotIndex < 0 || slotIndex >= skillLoadoutEntries.Length)
|
|
return;
|
|
|
|
skillLoadoutEntries[slotIndex] = loadoutEntry != null ? loadoutEntry.CreateCopy() : new SkillLoadoutEntry();
|
|
skillLoadoutEntries[slotIndex].EnsureGemSlotCapacity();
|
|
skillLoadoutEntries[slotIndex].SanitizeInvalidGems(true);
|
|
skillSlots[slotIndex] = skillLoadoutEntries[slotIndex].BaseSkill;
|
|
OnSkillSlotsChanged?.Invoke();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 특정 슬롯의 특정 젬 슬롯을 갱신합니다.
|
|
/// </summary>
|
|
public void SetSkillGem(int slotIndex, int gemSlotIndex, SkillGemData gem)
|
|
{
|
|
EnsureSkillSlotCapacity();
|
|
EnsureSkillLoadoutCapacity();
|
|
|
|
if (slotIndex < 0 || slotIndex >= skillLoadoutEntries.Length)
|
|
return;
|
|
|
|
skillLoadoutEntries[slotIndex].SetGem(gemSlotIndex, gem);
|
|
skillSlots[slotIndex] = skillLoadoutEntries[slotIndex].BaseSkill;
|
|
OnSkillSlotsChanged?.Invoke();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 슬롯 엔트리 전체를 한 번에 갱신합니다.
|
|
/// </summary>
|
|
public void SetSkillLoadouts(IReadOnlyList<SkillLoadoutEntry> loadouts)
|
|
{
|
|
EnsureSkillSlotCapacity();
|
|
EnsureSkillLoadoutCapacity();
|
|
|
|
if (loadouts == null)
|
|
return;
|
|
|
|
int count = Mathf.Min(skillLoadoutEntries.Length, loadouts.Count);
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
skillLoadoutEntries[i] = loadouts[i] != null ? loadouts[i].CreateCopy() : new SkillLoadoutEntry();
|
|
skillLoadoutEntries[i].EnsureGemSlotCapacity();
|
|
skillLoadoutEntries[i].SanitizeInvalidGems(true);
|
|
skillSlots[i] = skillLoadoutEntries[i].BaseSkill;
|
|
}
|
|
|
|
for (int i = count; i < skillLoadoutEntries.Length; i++)
|
|
{
|
|
skillLoadoutEntries[i] = new SkillLoadoutEntry();
|
|
skillLoadoutEntries[i].EnsureGemSlotCapacity();
|
|
skillSlots[i] = null;
|
|
}
|
|
|
|
OnSkillSlotsChanged?.Invoke();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 프리셋 기반으로 슬롯 엔트리를 일괄 적용합니다.
|
|
/// </summary>
|
|
public void ApplyLoadoutPreset(PlayerLoadoutPreset preset)
|
|
{
|
|
if (preset == null)
|
|
return;
|
|
|
|
preset.EnsureSlotCapacity();
|
|
SetSkillLoadouts(preset.Slots);
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
EnsureRuntimeReferences();
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 서버 권한에서 특정 슬롯 스킬을 강제로 실행합니다.
|
|
/// 멀티플레이 테스트 시 원격 플레이어 스킬을 호스트에서 검증할 때 사용합니다.
|
|
/// </summary>
|
|
public bool DebugExecuteSkillAsServer(int slotIndex)
|
|
{
|
|
if (!IsServer)
|
|
{
|
|
Debug.LogWarning($"[Debug] DebugExecuteSkillAsServer 실패: 서버가 아님 (IsServer=false)");
|
|
return false;
|
|
}
|
|
|
|
EnsureRuntimeReferences();
|
|
|
|
if (slotIndex < 0 || slotIndex >= skillSlots.Length)
|
|
{
|
|
Debug.LogWarning($"[Debug] DebugExecuteSkillAsServer 실패: 슬롯 인덱스 범위 초과 (slotIndex={slotIndex}, length={skillSlots.Length})");
|
|
return false;
|
|
}
|
|
|
|
SkillLoadoutEntry loadoutEntry = GetSkillLoadout(slotIndex);
|
|
SkillData skill = loadoutEntry != null ? loadoutEntry.BaseSkill : null;
|
|
if (skill == null)
|
|
{
|
|
Debug.LogWarning($"[Debug] DebugExecuteSkillAsServer 실패: 슬롯 {slotIndex}이 비어있음");
|
|
return false;
|
|
}
|
|
|
|
if (actionState != null && !actionState.CanStartSkill(skill))
|
|
{
|
|
Debug.LogWarning($"[Debug] DebugExecuteSkillAsServer 실패: CanStartSkill=false (IsDead={actionState.IsDead}, CanUseSkills={actionState.CanUseSkills})");
|
|
return false;
|
|
}
|
|
|
|
if (skillController == null || skillController.IsExecutingSkill || skillController.IsOnCooldown(skill))
|
|
{
|
|
string reason = skillController == null ? "skillController=null" :
|
|
skillController.IsExecutingSkill ? "스킬 실행 중" :
|
|
skillController.IsOnCooldown(skill) ? "쿨다운 중" : "";
|
|
Debug.LogWarning($"[Debug] DebugExecuteSkillAsServer 실패: {reason}");
|
|
return false;
|
|
}
|
|
|
|
float actualManaCost = GetActualManaCost(loadoutEntry);
|
|
if (networkController != null && networkController.Mana < actualManaCost)
|
|
{
|
|
Debug.LogWarning($"[Debug] DebugExecuteSkillAsServer 실패: MP 부족 (need={actualManaCost:F1}, have={networkController.Mana:F1})");
|
|
return false;
|
|
}
|
|
|
|
if (networkController != null && actualManaCost > 0f)
|
|
{
|
|
networkController.UseManaRpc(actualManaCost);
|
|
}
|
|
|
|
BroadcastSkillExecutionRpc(slotIndex);
|
|
Debug.Log($"[Debug] DebugExecuteSkillAsServer 성공: Slot={slotIndex}, Skill={skill.SkillName}");
|
|
return true;
|
|
}
|
|
|
|
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>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 로컬/원격 여부와 관계없이 런타임 참조를 보정합니다.
|
|
/// 서버에서 원격 플레이어 스킬을 디버그 실행할 때도 동일한 검증 경로를 쓰기 위해 필요합니다.
|
|
/// </summary>
|
|
private void EnsureRuntimeReferences()
|
|
{
|
|
if (skillController == null)
|
|
{
|
|
skillController = GetComponent<SkillController>();
|
|
}
|
|
|
|
if (networkController == null)
|
|
{
|
|
networkController = GetComponent<PlayerNetworkController>();
|
|
}
|
|
|
|
if (weaponEquipment == null)
|
|
{
|
|
weaponEquipment = GetComponent<WeaponEquipment>();
|
|
}
|
|
|
|
if (actionState == null)
|
|
{
|
|
actionState = GetOrCreateActionState();
|
|
}
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
/// <summary>
|
|
/// MPP 환경에서는 메인 에디터에 탱커, 가상 플레이어 복제본에 지원 프리셋을 자동 적용합니다.
|
|
/// </summary>
|
|
private void ApplyEditorMultiplayerLoadoutIfNeeded()
|
|
{
|
|
if (!ShouldApplyMppmLoadout())
|
|
return;
|
|
|
|
string[] loadoutPaths = GetMppmLoadoutPathsForOwner();
|
|
List<SkillData> loadout = LoadSkillAssets(loadoutPaths);
|
|
if (loadout == null || loadout.Count == 0)
|
|
return;
|
|
|
|
SetSkills(loadout);
|
|
EnsureRuntimeReferences();
|
|
if (networkController != null)
|
|
{
|
|
networkController.TryApplyPrototypePassivePresetForOwner();
|
|
}
|
|
Debug.Log($"[MPP] 자동 프리셋 적용: {GetMppmLoadoutLabel()} (OwnerClientId={OwnerClientId})");
|
|
}
|
|
|
|
private static bool ShouldApplyMppmLoadout()
|
|
{
|
|
string systemDataPath = GetMppmSystemDataPath();
|
|
if (string.IsNullOrEmpty(systemDataPath) || !File.Exists(systemDataPath))
|
|
return false;
|
|
|
|
string json = File.ReadAllText(systemDataPath);
|
|
if (!json.Contains("\"IsMppmActive\": true", StringComparison.Ordinal))
|
|
return false;
|
|
|
|
return Regex.Matches(json, "\"Active\"\\s*:\\s*true").Count > 1;
|
|
}
|
|
|
|
private static string GetMppmSystemDataPath()
|
|
{
|
|
string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, ".."));
|
|
if (IsVirtualProjectCloneInEditor())
|
|
return Path.GetFullPath(Path.Combine(projectRoot, "..", "SystemData.json"));
|
|
|
|
return Path.Combine(projectRoot, "Library", "VP", "SystemData.json");
|
|
}
|
|
|
|
private static bool IsVirtualProjectCloneInEditor()
|
|
{
|
|
string[] arguments = Environment.GetCommandLineArgs();
|
|
for (int i = 0; i < arguments.Length; i++)
|
|
{
|
|
if (string.Equals(arguments[i], "--virtual-project-clone", StringComparison.OrdinalIgnoreCase))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static List<SkillData> LoadSkillAssets(IReadOnlyList<string> assetPaths)
|
|
{
|
|
List<SkillData> skills = new List<SkillData>(assetPaths.Count);
|
|
for (int i = 0; i < assetPaths.Count; i++)
|
|
{
|
|
SkillData skill = AssetDatabase.LoadAssetAtPath<SkillData>(assetPaths[i]);
|
|
if (skill == null)
|
|
{
|
|
Debug.LogWarning($"[MPP] 스킬 에셋을 찾지 못했습니다: {assetPaths[i]}");
|
|
return null;
|
|
}
|
|
|
|
skills.Add(skill);
|
|
}
|
|
|
|
return skills;
|
|
}
|
|
|
|
private string[] GetMppmLoadoutPathsForOwner()
|
|
{
|
|
return OwnerClientId switch
|
|
{
|
|
0 => TankLoadoutPaths,
|
|
1 => SupportLoadoutPaths,
|
|
_ => DpsLoadoutPaths,
|
|
};
|
|
}
|
|
|
|
private string GetMppmLoadoutLabel()
|
|
{
|
|
return OwnerClientId switch
|
|
{
|
|
0 => "탱커",
|
|
1 => "지원",
|
|
_ => "딜러",
|
|
};
|
|
}
|
|
#endif
|
|
}
|
|
}
|