diff --git a/Assets/_Game/Scripts/Editor/PlayerSkillDebugMenu.cs b/Assets/_Game/Scripts/Editor/PlayerSkillDebugMenu.cs index 600979ea..0b0622e4 100644 --- a/Assets/_Game/Scripts/Editor/PlayerSkillDebugMenu.cs +++ b/Assets/_Game/Scripts/Editor/PlayerSkillDebugMenu.cs @@ -1,4 +1,5 @@ using System.Text; +using System.Collections.Generic; using Colosseum.Enemy; using Colosseum.Player; @@ -20,6 +21,15 @@ namespace Colosseum.Editor private const string HealSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_치유.asset"; private const string AreaHealSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_광역치유.asset"; private const string ShieldSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_보호막.asset"; + private const string SlashSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_베기.asset"; + private const string PierceSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_찌르기.asset"; + private const string SpinSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_회전베기.asset"; + private const string DashSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_돌진.asset"; + private const string ProjectileSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_투사체.asset"; + private const string TauntSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_도발.asset"; + private const string GuardSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_방어태세.asset"; + private const string IronWallSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_철벽.asset"; + private const string EvadeSkillPath = "Assets/_Game/Data/Skills/Data_Skill_Player_구르기.asset"; private const string StunAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_Stun.asset"; private const string SilenceAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_Silence.asset"; private const string MarkAbnormalityPath = "Assets/_Game/Data/Abnormalities/Data_Abnormality_Player_집행자의낙인.asset"; @@ -171,6 +181,81 @@ namespace Colosseum.Editor Debug.Log($"[Debug] HUD 이상상태 요약 | {playerHud.CurrentAbnormalitySummary}"); } + [MenuItem("Tools/Colosseum/Debug/Apply Tank Loadout")] + private static void ApplyTankLoadout() + { + ApplyLoadout( + "탱커", + SlashSkillPath, + TauntSkillPath, + GuardSkillPath, + DashSkillPath, + IronWallSkillPath, + PierceSkillPath, + EvadeSkillPath); + } + + [MenuItem("Tools/Colosseum/Debug/Apply Support Loadout")] + private static void ApplySupportLoadout() + { + ApplyLoadout( + "지원", + SlashSkillPath, + HealSkillPath, + AreaHealSkillPath, + ShieldSkillPath, + DashSkillPath, + ProjectileSkillPath, + EvadeSkillPath); + } + + [MenuItem("Tools/Colosseum/Debug/Apply DPS Loadout")] + private static void ApplyDpsLoadout() + { + ApplyLoadout( + "딜러", + SlashSkillPath, + PierceSkillPath, + SpinSkillPath, + DashSkillPath, + ProjectileSkillPath, + ShieldSkillPath, + EvadeSkillPath); + } + + [MenuItem("Tools/Colosseum/Debug/Log Local Skill Loadout")] + private static void LogLocalSkillLoadout() + { + if (!EditorApplication.isPlaying) + { + Debug.LogWarning("[Debug] 플레이 모드에서만 사용할 수 있습니다."); + return; + } + + PlayerSkillInput localSkillInput = FindLocalSkillInput(); + if (localSkillInput == null) + { + Debug.LogWarning("[Debug] 로컬 PlayerSkillInput을 찾지 못했습니다."); + return; + } + + string[] slotNames = { "L", "R", "1", "2", "3", "4", "Ctrl" }; + int[] slotOrder = { 0, 1, 2, 3, 4, 5, 6 }; + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < slotOrder.Length; i++) + { + SkillData skill = localSkillInput.GetSkill(slotOrder[i]); + builder.Append(slotNames[i]); + builder.Append(": "); + builder.Append(skill != null ? skill.SkillName : "(비어 있음)"); + + if (i < slotOrder.Length - 1) + builder.Append(" | "); + } + + Debug.Log($"[Debug] 로컬 스킬 구성 | {builder}"); + } + private static PlayerSkillInput FindLocalSkillInput() { PlayerSkillInput[] skillInputs = Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None); @@ -274,5 +359,37 @@ namespace Colosseum.Editor abnormalityManager.ApplyAbnormality(abnormality, abnormalityManager.gameObject); } + + private static void ApplyLoadout(string loadoutName, params string[] skillPaths) + { + if (!EditorApplication.isPlaying) + { + Debug.LogWarning("[Debug] 플레이 모드에서만 사용할 수 있습니다."); + return; + } + + PlayerSkillInput localSkillInput = FindLocalSkillInput(); + if (localSkillInput == null) + { + Debug.LogWarning("[Debug] 로컬 PlayerSkillInput을 찾지 못했습니다."); + return; + } + + List skills = new List(skillPaths.Length); + for (int i = 0; i < skillPaths.Length; i++) + { + SkillData skill = AssetDatabase.LoadAssetAtPath(skillPaths[i]); + if (skill == null) + { + Debug.LogWarning($"[Debug] 스킬 에셋을 찾지 못했습니다: {skillPaths[i]}"); + return; + } + + skills.Add(skill); + } + + localSkillInput.SetSkills(skills); + Debug.Log($"[Debug] {loadoutName} 프리셋을 적용했습니다."); + } } } diff --git a/Assets/_Game/Scripts/Player/PlayerSkillInput.cs b/Assets/_Game/Scripts/Player/PlayerSkillInput.cs index 226d82f0..1e99b449 100644 --- a/Assets/_Game/Scripts/Player/PlayerSkillInput.cs +++ b/Assets/_Game/Scripts/Player/PlayerSkillInput.cs @@ -1,6 +1,8 @@ using UnityEngine; using UnityEngine.InputSystem; using Unity.Netcode; +using System; +using System.Collections.Generic; using Colosseum.Skills; using Colosseum.Weapons; @@ -14,9 +16,11 @@ namespace Colosseum.Player [RequireComponent(typeof(PlayerActionState))] public class PlayerSkillInput : NetworkBehaviour { + private const int ExpectedSkillSlotCount = 7; + [Header("Skill Slots")] [Tooltip("각 슬롯에 등록할 스킬 데이터 (6개 + 추가 슬롯)")] - [SerializeField] private SkillData[] skillSlots = new SkillData[7]; + [SerializeField] private SkillData[] skillSlots = new SkillData[ExpectedSkillSlotCount]; [Header("References")] [Tooltip("SkillController (없으면 자동 검색)")] @@ -32,8 +36,15 @@ namespace Colosseum.Player public SkillData[] SkillSlots => skillSlots; + /// + /// 스킬 슬롯 구성이 변경되었을 때 호출됩니다. + /// + public event Action OnSkillSlotsChanged; + public override void OnNetworkSpawn() { + EnsureSkillSlotCapacity(); + if (!IsOwner) { enabled = false; @@ -99,6 +110,16 @@ namespace Colosseum.Player CleanupInputActions(); } + private void Awake() + { + EnsureSkillSlotCapacity(); + } + + private void OnValidate() + { + EnsureSkillSlotCapacity(); + } + private void OnEnable() { if (IsOwner && inputActions != null) @@ -107,6 +128,28 @@ namespace Colosseum.Player } } + /// + /// 기존 프리팹이나 씬 직렬화 데이터가 6칸으로 남아 있어도 + /// 긴급 회피 슬롯까지 포함한 7칸 구성을 항상 보장합니다. + /// + 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) @@ -227,6 +270,8 @@ namespace Colosseum.Player /// public SkillData GetSkill(int slotIndex) { + EnsureSkillSlotCapacity(); + if (slotIndex < 0 || slotIndex >= skillSlots.Length) return null; return skillSlots[slotIndex]; @@ -237,10 +282,37 @@ namespace Colosseum.Player /// public void SetSkill(int slotIndex, SkillData skill) { + EnsureSkillSlotCapacity(); + if (slotIndex < 0 || slotIndex >= skillSlots.Length) return; skillSlots[slotIndex] = skill; + OnSkillSlotsChanged?.Invoke(); + } + + /// + /// 전체 스킬 슬롯을 한 번에 갱신합니다. + /// + public void SetSkills(IReadOnlyList 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(); } /// diff --git a/Assets/_Game/Scripts/UI/SkillQuickSlotUI.cs b/Assets/_Game/Scripts/UI/SkillQuickSlotUI.cs index 2b862688..021fb2f4 100644 --- a/Assets/_Game/Scripts/UI/SkillQuickSlotUI.cs +++ b/Assets/_Game/Scripts/UI/SkillQuickSlotUI.cs @@ -44,6 +44,14 @@ namespace Colosseum.UI FindLocalPlayer(); } + private void OnDestroy() + { + if (playerSkillInput != null) + { + playerSkillInput.OnSkillSlotsChanged -= HandleSkillSlotsChanged; + } + } + /// /// 자식 슬롯을 자동 수집해 프리팹 중첩 구조 변경에도 참조가 유지되도록 합니다. /// @@ -212,10 +220,9 @@ namespace Colosseum.UI { if (player.IsOwner) { - playerSkillInput = player; + SetPlayer(player); networkController = player.GetComponent(); Debug.Log($"[SkillQuickSlotUI] Found local player: {player.name}"); - InitializeSlots(); return; } } @@ -294,8 +301,19 @@ namespace Colosseum.UI /// public void SetPlayer(PlayerSkillInput player) { + if (playerSkillInput != null) + { + playerSkillInput.OnSkillSlotsChanged -= HandleSkillSlotsChanged; + } + playerSkillInput = player; networkController = player?.GetComponent(); + + if (playerSkillInput != null) + { + playerSkillInput.OnSkillSlotsChanged += HandleSkillSlotsChanged; + } + InitializeSlots(); } @@ -312,5 +330,10 @@ namespace Colosseum.UI string keyLabel = displayIndex < keyLabels.Length ? keyLabels[displayIndex] : (displayIndex + 1).ToString(); skillSlots[displayIndex].Initialize(slotIndex, skill, keyLabel); } + + private void HandleSkillSlotsChanged() + { + InitializeSlots(); + } } }