From 2bc5241ff130d52096112122d467cd99fa96385c Mon Sep 17 00:00:00 2001 From: dal4segno Date: Sat, 28 Mar 2026 15:09:56 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=9F=B0=ED=83=80=EC=9E=84=20=EB=B3=B4?= =?UTF-8?q?=EC=8A=A4=20=EB=94=94=EB=B2=84=EA=B7=B8=20=ED=8C=A8=EB=84=90=20?= =?UTF-8?q?=EB=B0=8F=20MCP=20=EC=BB=A4=EB=A7=A8=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - HUD 우측 하단 접이식 디버그 패널 (DebugPanelUI): 보스 HP 슬라이더/직접입력/프리셋, 페이즈 전환, 리스폰, 보호막, 이상상태 적용 - MCP execute_menu_item으로 호출 가능한 MenuItem 커맨드 (DebugBossMenuItems): HP/페이즈/보호막 제어, 상태 조회, 임의 값 설정 지원 - BossEnemyEditor 인스펙터 HP 조작 확장: 퍼센트 슬라이더 및 직접 HP 입력 추가 - Test 씬 UI Canvas 하위 DebugPanel GameObject 배치 - UNITY_EDITOR || DEVELOPMENT_BUILD 가드로 릴리즈 빌드 미포함 --- Assets/Scenes/Test.unity | 53 ++ .../_Game/Scripts/Editor/BossEnemyEditor.cs | 33 + .../Scripts/Editor/DebugBossMenuItems.cs | 255 ++++++++ .../Scripts/Editor/DebugBossMenuItems.cs.meta | 2 + Assets/_Game/Scripts/UI/DebugPanelUI.cs | 617 ++++++++++++++++++ Assets/_Game/Scripts/UI/DebugPanelUI.cs.meta | 2 + 6 files changed, 962 insertions(+) create mode 100644 Assets/_Game/Scripts/Editor/DebugBossMenuItems.cs create mode 100644 Assets/_Game/Scripts/Editor/DebugBossMenuItems.cs.meta create mode 100644 Assets/_Game/Scripts/UI/DebugPanelUI.cs create mode 100644 Assets/_Game/Scripts/UI/DebugPanelUI.cs.meta diff --git a/Assets/Scenes/Test.unity b/Assets/Scenes/Test.unity index fb36720a..e5d12b08 100644 --- a/Assets/Scenes/Test.unity +++ b/Assets/Scenes/Test.unity @@ -1184,6 +1184,7 @@ RectTransform: - {fileID: 678443228} - {fileID: 1221067101607693524} - {fileID: 1943804129} + - {fileID: 985852648} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_AnchorMin: {x: 0, y: 0} @@ -5040,6 +5041,58 @@ Transform: m_CorrespondingSourceObject: {fileID: 7132605379903659868, guid: 5b4ac53b97612ae4392b84786de0d50d, type: 3} m_PrefabInstance: {fileID: 539760736} m_PrefabAsset: {fileID: 0} +--- !u!1 &985852647 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 985852648} + - component: {fileID: 985852649} + m_Layer: 0 + m_Name: DebugPanel + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &985852648 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 985852647} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 260528176} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &985852649 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 985852647} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 7b7611f77d92f8e41bfe5dfb5ac1768f, type: 3} + m_Name: + m_EditorClassIdentifier: Colosseum.Game::Colosseum.UI.DebugPanelUI + stunAbnormalityData: {fileID: 0} + silenceAbnormalityData: {fileID: 0} + panelWidth: 280 + panelMaxHeight: 500 --- !u!1001 &989227100 PrefabInstance: m_ObjectHideFlags: 0 diff --git a/Assets/_Game/Scripts/Editor/BossEnemyEditor.cs b/Assets/_Game/Scripts/Editor/BossEnemyEditor.cs index c1ccfe4d..1f066d05 100644 --- a/Assets/_Game/Scripts/Editor/BossEnemyEditor.cs +++ b/Assets/_Game/Scripts/Editor/BossEnemyEditor.cs @@ -17,6 +17,8 @@ namespace Colosseum.Editor private bool showThreatInfo = true; private bool showDebugTools = true; private int selectedPhaseIndex = 0; + private float debugHPPercent = 1f; + private float debugHPValue = 0f; private void OnEnable() { @@ -214,6 +216,37 @@ namespace Colosseum.Editor // HP 조작 EditorGUILayout.LabelField("HP 조작", EditorStyles.boldLabel); + EditorGUI.indentLevel++; + + // 현재 HP 표시 + EditorGUILayout.LabelField("현재", + $"{boss.CurrentHealth:F0} / {boss.MaxHealth:F0} ({(boss.MaxHealth > 0 ? boss.CurrentHealth / boss.MaxHealth * 100f : 0f):F1}%)"); + + // 퍼센트 슬라이더 + EditorGUI.BeginChangeCheck(); + debugHPPercent = EditorGUILayout.Slider("퍼센트", debugHPPercent, 0f, 1f); + if (EditorGUI.EndChangeCheck()) + { + SetBossHP(debugHPPercent); + } + + // 직접 HP 값 입력 + EditorGUILayout.BeginHorizontal(); + debugHPValue = EditorGUILayout.FloatField("직접 입력", debugHPValue); + EditorGUILayout.LabelField($"/ {boss.MaxHealth:F0}", GUILayout.Width(80)); + if (GUILayout.Button("적용", GUILayout.Width(60))) + { + float clamped = Mathf.Clamp(debugHPValue, 0f, boss.MaxHealth); + float percent = boss.MaxHealth > 0 ? clamped / boss.MaxHealth : 0f; + debugHPPercent = percent; + SetBossHP(percent); + } + EditorGUILayout.EndHorizontal(); + + EditorGUI.indentLevel--; + + // 빠른 HP 설정 버튼 + EditorGUILayout.Space(3); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("HP 10%")) { diff --git a/Assets/_Game/Scripts/Editor/DebugBossMenuItems.cs b/Assets/_Game/Scripts/Editor/DebugBossMenuItems.cs new file mode 100644 index 00000000..e7371767 --- /dev/null +++ b/Assets/_Game/Scripts/Editor/DebugBossMenuItems.cs @@ -0,0 +1,255 @@ +using System.Text; + +using Colosseum.Enemy; +using Colosseum.Abnormalities; + +using UnityEditor; +using UnityEngine; +using UnityEngine.Networking; + +namespace Colosseum.Editor +{ + /// + /// MCP (execute_menu_item) 또는 에디터 메뉴에서 호출 가능한 보스 디버그 커맨드. + /// 파라미터가 필요한 연산은 static 필드를 설정한 뒤 Custom MenuItem을 호출합니다. + /// + public static class DebugBossMenuItems + { + 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"; + + // ── MCP가 설정 후 Custom 메뉴 호출하는 파라미터 필드 ── + + /// + /// Set Boss HP Custom에서 사용할 퍼센트 (0.0 ~ 1.0). + /// MCP는 script_apply_edits로 이 값을 변경한 뒤 Custom 메뉴를 호출합니다. + /// + public static float customHPPercent = 0.5f; + + /// + /// Force Phase Custom에서 사용할 페이즈 인덱스 (0-based). + /// + public static int customPhaseIndex = 0; + + /// + /// Apply Shield Custom에서 사용할 보호막 수치. + /// + public static float customShieldAmount = 1000f; + + // ── 보스 HP ── + + [MenuItem("Tools/Colosseum/Debug/Boss/Set HP 10%")] + private static void SetHP10() => SetBossHP(0.1f); + + [MenuItem("Tools/Colosseum/Debug/Boss/Set HP 25%")] + private static void SetHP25() => SetBossHP(0.25f); + + [MenuItem("Tools/Colosseum/Debug/Boss/Set HP 50%")] + private static void SetHP50() => SetBossHP(0.5f); + + [MenuItem("Tools/Colosseum/Debug/Boss/Set HP 75%")] + private static void SetHP75() => SetBossHP(0.75f); + + [MenuItem("Tools/Colosseum/Debug/Boss/Set HP 100%")] + private static void SetHP100() => SetBossHP(1f); + + [MenuItem("Tools/Colosseum/Debug/Boss/Set HP Custom")] + private static void SetHPCustom() => SetBossHP(Mathf.Clamp01(customHPPercent)); + + [MenuItem("Tools/Colosseum/Debug/Boss/Full Heal")] + private static void FullHeal() + { + BossEnemy boss = FindBoss(); + if (boss == null) return; + boss.Heal(boss.MaxHealth); + Debug.Log($"[Debug] 보스 HP 풀회복 | HP={boss.CurrentHealth:F0}/{boss.MaxHealth:F0}"); + } + + // ── 보스 제어 ── + + [MenuItem("Tools/Colosseum/Debug/Boss/Force Phase 0")] + private static void ForcePhase0() => ForcePhase(0); + + [MenuItem("Tools/Colosseum/Debug/Boss/Force Phase 1")] + private static void ForcePhase1() => ForcePhase(1); + + [MenuItem("Tools/Colosseum/Debug/Boss/Force Phase 2")] + private static void ForcePhase2() => ForcePhase(2); + + [MenuItem("Tools/Colosseum/Debug/Boss/Force Phase Custom")] + private static void ForcePhaseCustom() => ForcePhase(customPhaseIndex); + + [MenuItem("Tools/Colosseum/Debug/Boss/Restart Current Phase")] + private static void RestartPhase() + { + BossEnemy boss = FindBoss(); + if (boss == null) return; + boss.RestartCurrentPhase(); + Debug.Log($"[Debug] 보스 현재 페이즈 재시작 | Phase={boss.CurrentPhaseIndex}"); + } + + [MenuItem("Tools/Colosseum/Debug/Boss/Respawn")] + private static void Respawn() + { + BossEnemy boss = FindBoss(); + if (boss == null) return; + boss.Respawn(); + Debug.Log($"[Debug] 보스 리스폰 | HP={boss.CurrentHealth:F0}/{boss.MaxHealth:F0}"); + } + + // ── 보호막 ── + + [MenuItem("Tools/Colosseum/Debug/Boss/Apply Shield 500")] + private static void ApplyShield500() => ApplyShield(500f); + + [MenuItem("Tools/Colosseum/Debug/Boss/Apply Shield 1000")] + private static void ApplyShield1000() => ApplyShield(1000f); + + [MenuItem("Tools/Colosseum/Debug/Boss/Apply Shield 5000")] + private static void ApplyShield5000() => ApplyShield(5000f); + + [MenuItem("Tools/Colosseum/Debug/Boss/Apply Shield Custom")] + private static void ApplyShieldCustom() => ApplyShield(Mathf.Max(0f, customShieldAmount)); + + // ── 이상상태 ── + + [MenuItem("Tools/Colosseum/Debug/Boss/Apply Stun")] + private static void ApplyStun() + { + BossEnemy boss = FindBoss(); + if (boss == null) return; + + AbnormalityData data = AssetDatabase.LoadAssetAtPath(StunAbnormalityPath); + if (data == null) + { + Debug.LogWarning($"[Debug] 기절 에셋을 찾지 못했습니다: {StunAbnormalityPath}"); + return; + } + + AbnormalityManager am = boss.GetComponent(); + if (am != null) + { + am.ApplyAbnormality(data, boss.gameObject); + Debug.Log($"[Debug] 보스 기절 적용 | {data.abnormalityName}"); + } + } + + [MenuItem("Tools/Colosseum/Debug/Boss/Apply Silence")] + private static void ApplySilence() + { + BossEnemy boss = FindBoss(); + if (boss == null) return; + + AbnormalityData data = AssetDatabase.LoadAssetAtPath(SilenceAbnormalityPath); + if (data == null) + { + Debug.LogWarning($"[Debug] 침묵 에셋을 찾지 못했습니다: {SilenceAbnormalityPath}"); + return; + } + + AbnormalityManager am = boss.GetComponent(); + if (am != null) + { + am.ApplyAbnormality(data, boss.gameObject); + Debug.Log($"[Debug] 보스 침묵 적용 | {data.abnormalityName}"); + } + } + + [MenuItem("Tools/Colosseum/Debug/Boss/Clear Threat")] + private static void ClearThreat() + { + BossEnemy boss = FindBoss(); + if (boss == null) return; + boss.ClearAllThreat(); + Debug.Log("[Debug] 보스 위협 초기화"); + } + + // ── 상태 조회 ── + + [MenuItem("Tools/Colosseum/Debug/Boss/Log Status")] + private static void LogStatus() + { + BossEnemy boss = FindBoss(); + if (boss == null) return; + + StringBuilder sb = new StringBuilder(); + sb.AppendLine($"[Debug] 보스 상태 | {boss.name}"); + sb.AppendLine($" HP: {boss.CurrentHealth:F0} / {boss.MaxHealth:F0} ({(boss.MaxHealth > 0 ? boss.CurrentHealth / boss.MaxHealth * 100f : 0f):F1}%)"); + sb.AppendLine($" Shield: {boss.Shield:F0}"); + sb.AppendLine($" Phase: {boss.CurrentPhaseIndex + 1} / {boss.TotalPhases}"); + sb.AppendLine($" IsDead: {boss.IsDead}"); + + if (boss.CurrentPhase != null) + sb.AppendLine($" PhaseName: {boss.CurrentPhase.PhaseName}"); + + Debug.Log(sb.ToString()); + } + + // ── 내부 구현 ── + + /// + /// 활성 보스 찾기 + /// + private static BossEnemy FindBoss() + { + if (!EditorApplication.isPlaying) + { + Debug.LogWarning("[Debug] 플레이 모드에서만 사용할 수 있습니다."); + return null; + } + + BossEnemy boss = BossEnemy.ActiveBoss != null + ? BossEnemy.ActiveBoss + : Object.FindFirstObjectByType(); + + if (boss == null) + Debug.LogWarning("[Debug] 활성 보스를 찾지 못했습니다."); + + return boss; + } + + /// + /// 보스 HP를 퍼센트로 설정 + /// + private static void SetBossHP(float percent) + { + BossEnemy boss = FindBoss(); + if (boss == null) return; + + float targetHP = boss.MaxHealth * percent; + float diff = boss.CurrentHealth - targetHP; + + if (diff > 0f) + boss.TakeDamage(diff); + else if (diff < 0f) + boss.Heal(-diff); + + Debug.Log($"[Debug] 보스 HP 설정 {percent * 100f:F0}% | HP={boss.CurrentHealth:F0}/{boss.MaxHealth:F0}"); + } + + /// + /// 페이즈 강제 전환 + /// + private static void ForcePhase(int index) + { + BossEnemy boss = FindBoss(); + if (boss == null) return; + + index = Mathf.Clamp(index, 0, Mathf.Max(0, boss.TotalPhases - 1)); + boss.ForcePhaseTransition(index); + Debug.Log($"[Debug] 보스 페이즈 강제 전환 | Phase={index}"); + } + + /// + /// 보호막 적용 + /// + private static void ApplyShield(float amount) + { + BossEnemy boss = FindBoss(); + if (boss == null) return; + + boss.ApplyShield(amount, 30f); + Debug.Log($"[Debug] 보스 보호막 적용 | Amount={amount:F0} | Total={boss.Shield:F0}"); + } + } +} diff --git a/Assets/_Game/Scripts/Editor/DebugBossMenuItems.cs.meta b/Assets/_Game/Scripts/Editor/DebugBossMenuItems.cs.meta new file mode 100644 index 00000000..5253fae8 --- /dev/null +++ b/Assets/_Game/Scripts/Editor/DebugBossMenuItems.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b710fae394e79d44fa88104e3412c04a \ No newline at end of file diff --git a/Assets/_Game/Scripts/UI/DebugPanelUI.cs b/Assets/_Game/Scripts/UI/DebugPanelUI.cs new file mode 100644 index 00000000..8a270bc9 --- /dev/null +++ b/Assets/_Game/Scripts/UI/DebugPanelUI.cs @@ -0,0 +1,617 @@ +#if UNITY_EDITOR || DEVELOPMENT_BUILD + +using System; + +using UnityEngine; +using UnityEngine.UI; +using TMPro; + +using Unity.Netcode; + +using Colosseum.Enemy; +using Colosseum.Abnormalities; + +namespace Colosseum.UI +{ + /// + /// 런타임 디버그 패널 UI. + /// 호스트(서버)에서 보스 HP, 페이즈, 보호막 등을 조작할 수 있는 접이식 HUD 패널입니다. + /// + public class DebugPanelUI : MonoBehaviour + { + [Header("Abnormality Data (Runtime)")] + [Tooltip("기절 이상상태 데이터")] + [SerializeField] private AbnormalityData stunAbnormalityData; + [Tooltip("침묵 이상상태 데이터")] + [SerializeField] private AbnormalityData silenceAbnormalityData; + + [Header("Settings")] + [Tooltip("패널 너비")] + [SerializeField] private float panelWidth = 280f; + [Tooltip("패널 최대 높이")] + [SerializeField] private float panelMaxHeight = 500f; + + // 보스 캐시 + private BossEnemy cachedBoss; + private bool suppressSliderCallback; + + // UI 참조 + private GameObject toggleButtonObject; + private GameObject panelRoot; + private bool isPanelOpen; + + // 보스 HP UI + private TMP_Text hpInfoText; + private Slider hpSlider; + private Image hpSliderFillImage; + private TMP_InputField hpInputField; + + // 보스 제어 UI + private TMP_InputField phaseInputField; + + // 보호막 UI + private TMP_InputField shieldAmountField; + + private void Awake() + { + BuildUI(); + } + + private void Start() + { + if (panelRoot != null) + panelRoot.SetActive(false); + } + + private void Update() + { + RefreshBoss(); + if (cachedBoss == null) + return; + + UpdateHPDisplay(); + } + + /// + /// 보스 참조 새로고침 + /// + private void RefreshBoss() + { + if (cachedBoss != null && cachedBoss.gameObject.activeInHierarchy) + return; + + cachedBoss = BossEnemy.ActiveBoss; + } + + /// + /// 보스 없음 여부 반환 + /// + private bool NoBoss => cachedBoss == null; + + /// + /// 서버 권한 확인 + /// + private bool IsHost => NetworkManager.Singleton != null && NetworkManager.Singleton.IsServer; + + // ────────────────────────────────────────────────── + // UI 빌드 + // ────────────────────────────────────────────────── + + /// + /// 전체 UI 트리 생성 + /// + private void BuildUI() + { + BuildToggleButton(); + BuildPanel(); + } + + /// + /// 토글 버튼 생성 (우측 하단 고정) + /// + private void BuildToggleButton() + { + toggleButtonObject = new GameObject("DebugToggle", + typeof(RectTransform), typeof(Image), typeof(Button)); + toggleButtonObject.transform.SetParent(transform, false); + + RectTransform r = toggleButtonObject.GetComponent(); + r.anchorMin = new Vector2(1f, 0f); + r.anchorMax = new Vector2(1f, 0f); + r.pivot = new Vector2(1f, 0f); + r.anchoredPosition = new Vector2(-10f, 10f); + r.sizeDelta = new Vector2(80f, 36f); + + toggleButtonObject.GetComponent().color = new Color(0.15f, 0.15f, 0.15f, 0.92f); + + TMP_Text label = MakeTextChild("Label", toggleButtonObject.transform); + label.text = "Debug"; + label.fontSize = 16f; + label.fontStyle = FontStyles.Bold; + label.alignment = TextAlignmentOptions.Center; + label.color = new Color(0.8f, 0.8f, 0.8f); + + toggleButtonObject.GetComponent