feat: 런타임 보스 디버그 패널 및 MCP 커맨드 추가
- HUD 우측 하단 접이식 디버그 패널 (DebugPanelUI): 보스 HP 슬라이더/직접입력/프리셋, 페이즈 전환, 리스폰, 보호막, 이상상태 적용 - MCP execute_menu_item으로 호출 가능한 MenuItem 커맨드 (DebugBossMenuItems): HP/페이즈/보호막 제어, 상태 조회, 임의 값 설정 지원 - BossEnemyEditor 인스펙터 HP 조작 확장: 퍼센트 슬라이더 및 직접 HP 입력 추가 - Test 씬 UI Canvas 하위 DebugPanel GameObject 배치 - UNITY_EDITOR || DEVELOPMENT_BUILD 가드로 릴리즈 빌드 미포함
This commit is contained in:
@@ -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%"))
|
||||
{
|
||||
|
||||
255
Assets/_Game/Scripts/Editor/DebugBossMenuItems.cs
Normal file
255
Assets/_Game/Scripts/Editor/DebugBossMenuItems.cs
Normal file
@@ -0,0 +1,255 @@
|
||||
using System.Text;
|
||||
|
||||
using Colosseum.Enemy;
|
||||
using Colosseum.Abnormalities;
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Networking;
|
||||
|
||||
namespace Colosseum.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// MCP (execute_menu_item) 또는 에디터 메뉴에서 호출 가능한 보스 디버그 커맨드.
|
||||
/// 파라미터가 필요한 연산은 static 필드를 설정한 뒤 Custom MenuItem을 호출합니다.
|
||||
/// </summary>
|
||||
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 메뉴 호출하는 파라미터 필드 ──
|
||||
|
||||
/// <summary>
|
||||
/// Set Boss HP Custom에서 사용할 퍼센트 (0.0 ~ 1.0).
|
||||
/// MCP는 script_apply_edits로 이 값을 변경한 뒤 Custom 메뉴를 호출합니다.
|
||||
/// </summary>
|
||||
public static float customHPPercent = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Force Phase Custom에서 사용할 페이즈 인덱스 (0-based).
|
||||
/// </summary>
|
||||
public static int customPhaseIndex = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Apply Shield Custom에서 사용할 보호막 수치.
|
||||
/// </summary>
|
||||
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<AbnormalityData>(StunAbnormalityPath);
|
||||
if (data == null)
|
||||
{
|
||||
Debug.LogWarning($"[Debug] 기절 에셋을 찾지 못했습니다: {StunAbnormalityPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
AbnormalityManager am = boss.GetComponent<AbnormalityManager>();
|
||||
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<AbnormalityData>(SilenceAbnormalityPath);
|
||||
if (data == null)
|
||||
{
|
||||
Debug.LogWarning($"[Debug] 침묵 에셋을 찾지 못했습니다: {SilenceAbnormalityPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
AbnormalityManager am = boss.GetComponent<AbnormalityManager>();
|
||||
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());
|
||||
}
|
||||
|
||||
// ── 내부 구현 ──
|
||||
|
||||
/// <summary>
|
||||
/// 활성 보스 찾기
|
||||
/// </summary>
|
||||
private static BossEnemy FindBoss()
|
||||
{
|
||||
if (!EditorApplication.isPlaying)
|
||||
{
|
||||
Debug.LogWarning("[Debug] 플레이 모드에서만 사용할 수 있습니다.");
|
||||
return null;
|
||||
}
|
||||
|
||||
BossEnemy boss = BossEnemy.ActiveBoss != null
|
||||
? BossEnemy.ActiveBoss
|
||||
: Object.FindFirstObjectByType<BossEnemy>();
|
||||
|
||||
if (boss == null)
|
||||
Debug.LogWarning("[Debug] 활성 보스를 찾지 못했습니다.");
|
||||
|
||||
return boss;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 보스 HP를 퍼센트로 설정
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 페이즈 강제 전환
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 보호막 적용
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/_Game/Scripts/Editor/DebugBossMenuItems.cs.meta
Normal file
2
Assets/_Game/Scripts/Editor/DebugBossMenuItems.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b710fae394e79d44fa88104e3412c04a
|
||||
617
Assets/_Game/Scripts/UI/DebugPanelUI.cs
Normal file
617
Assets/_Game/Scripts/UI/DebugPanelUI.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 런타임 디버그 패널 UI.
|
||||
/// 호스트(서버)에서 보스 HP, 페이즈, 보호막 등을 조작할 수 있는 접이식 HUD 패널입니다.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 보스 참조 새로고침
|
||||
/// </summary>
|
||||
private void RefreshBoss()
|
||||
{
|
||||
if (cachedBoss != null && cachedBoss.gameObject.activeInHierarchy)
|
||||
return;
|
||||
|
||||
cachedBoss = BossEnemy.ActiveBoss;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 보스 없음 여부 반환
|
||||
/// </summary>
|
||||
private bool NoBoss => cachedBoss == null;
|
||||
|
||||
/// <summary>
|
||||
/// 서버 권한 확인
|
||||
/// </summary>
|
||||
private bool IsHost => NetworkManager.Singleton != null && NetworkManager.Singleton.IsServer;
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// UI 빌드
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 전체 UI 트리 생성
|
||||
/// </summary>
|
||||
private void BuildUI()
|
||||
{
|
||||
BuildToggleButton();
|
||||
BuildPanel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 토글 버튼 생성 (우측 하단 고정)
|
||||
/// </summary>
|
||||
private void BuildToggleButton()
|
||||
{
|
||||
toggleButtonObject = new GameObject("DebugToggle",
|
||||
typeof(RectTransform), typeof(Image), typeof(Button));
|
||||
toggleButtonObject.transform.SetParent(transform, false);
|
||||
|
||||
RectTransform r = toggleButtonObject.GetComponent<RectTransform>();
|
||||
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<Image>().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<Button>().onClick.AddListener(TogglePanel);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 메인 패널 생성
|
||||
/// </summary>
|
||||
private void BuildPanel()
|
||||
{
|
||||
panelRoot = new GameObject("Panel",
|
||||
typeof(RectTransform), typeof(Image), typeof(Mask), typeof(ScrollRect));
|
||||
panelRoot.transform.SetParent(transform, false);
|
||||
|
||||
RectTransform pr = panelRoot.GetComponent<RectTransform>();
|
||||
pr.anchorMin = new Vector2(1f, 0f);
|
||||
pr.anchorMax = new Vector2(1f, 0f);
|
||||
pr.pivot = new Vector2(1f, 0f);
|
||||
pr.anchoredPosition = new Vector2(-10f, 56f);
|
||||
pr.sizeDelta = new Vector2(panelWidth, panelMaxHeight);
|
||||
|
||||
panelRoot.GetComponent<Image>().color = new Color(0.08f, 0.08f, 0.08f, 0.92f);
|
||||
panelRoot.GetComponent<Mask>().showMaskGraphic = true;
|
||||
|
||||
ScrollRect scroll = panelRoot.GetComponent<ScrollRect>();
|
||||
scroll.horizontal = false;
|
||||
scroll.movementType = ScrollRect.MovementType.Clamped;
|
||||
scroll.scrollSensitivity = 30f;
|
||||
|
||||
// 뷰포트
|
||||
GameObject viewport = new GameObject("Viewport",
|
||||
typeof(RectTransform), typeof(Image));
|
||||
viewport.transform.SetParent(panelRoot.transform, false);
|
||||
|
||||
RectTransform vr = viewport.GetComponent<RectTransform>();
|
||||
vr.anchorMin = Vector2.zero;
|
||||
vr.anchorMax = Vector2.one;
|
||||
vr.sizeDelta = Vector2.zero;
|
||||
viewport.GetComponent<Image>().color = Color.clear;
|
||||
scroll.viewport = vr;
|
||||
|
||||
// 콘텐츠
|
||||
GameObject content = new GameObject("Content",
|
||||
typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter));
|
||||
content.transform.SetParent(viewport.transform, false);
|
||||
|
||||
RectTransform cr = content.GetComponent<RectTransform>();
|
||||
cr.anchorMin = new Vector2(0f, 1f);
|
||||
cr.anchorMax = new Vector2(1f, 1f);
|
||||
cr.pivot = new Vector2(0.5f, 1f);
|
||||
cr.sizeDelta = Vector2.zero;
|
||||
scroll.content = cr;
|
||||
|
||||
VerticalLayoutGroup vlg = content.GetComponent<VerticalLayoutGroup>();
|
||||
vlg.childAlignment = TextAnchor.UpperLeft;
|
||||
vlg.childControlWidth = true;
|
||||
vlg.childControlHeight = true;
|
||||
vlg.childForceExpandWidth = true;
|
||||
vlg.childForceExpandHeight = false;
|
||||
vlg.padding = new RectOffset(8, 8, 8, 8);
|
||||
vlg.spacing = 6f;
|
||||
|
||||
content.GetComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
|
||||
|
||||
// 섹션들
|
||||
BuildBossHPSection(content.transform);
|
||||
BuildBossControlSection(content.transform);
|
||||
BuildShieldSection(content.transform);
|
||||
BuildAbnormalitySection(content.transform);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 보스 HP 섹션
|
||||
/// </summary>
|
||||
private void BuildBossHPSection(Transform parent)
|
||||
{
|
||||
MakeSectionHeader("보스 HP", parent);
|
||||
|
||||
hpInfoText = MakeLabel("HP: ---", parent, 15f);
|
||||
|
||||
hpSlider = MakeSlider("HPSlider", parent, out hpSliderFillImage);
|
||||
hpSlider.onValueChanged.AddListener(OnHPSliderChanged);
|
||||
|
||||
// 직접 HP 입력
|
||||
GameObject hpRow = MakeRow(parent);
|
||||
hpInputField = MakeInputField("HPInput", hpRow.transform, "0", 100f);
|
||||
MakeButton("적용", hpRow.transform, ApplyDirectHP, 60f);
|
||||
|
||||
// 프리셋
|
||||
GameObject presetRow = MakeRow(parent);
|
||||
MakeButton("10%", presetRow.transform, () => SetBossHP(0.1f), 55f);
|
||||
MakeButton("30%", presetRow.transform, () => SetBossHP(0.3f), 55f);
|
||||
MakeButton("50%", presetRow.transform, () => SetBossHP(0.5f), 55f);
|
||||
MakeButton("100%", presetRow.transform, () => SetBossHP(1f), 55f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 보스 제어 섹션
|
||||
/// </summary>
|
||||
private void BuildBossControlSection(Transform parent)
|
||||
{
|
||||
MakeSectionHeader("보스 제어", parent);
|
||||
|
||||
GameObject phaseRow = MakeRow(parent);
|
||||
MakeLabel("페이즈:", phaseRow.transform, 14f, 50f);
|
||||
phaseInputField = MakeInputField("PhaseInput", phaseRow.transform, "0", 40f);
|
||||
MakeButton("전환", phaseRow.transform, OnForcePhase, 60f);
|
||||
|
||||
MakeButton("현재 페이즈 재시작", parent, OnRestartPhase);
|
||||
MakeButton("리스폰", parent, OnRespawn);
|
||||
MakeButton("HP 풀회복", parent, OnFullHeal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 보호막 섹션
|
||||
/// </summary>
|
||||
private void BuildShieldSection(Transform parent)
|
||||
{
|
||||
MakeSectionHeader("보호막", parent);
|
||||
|
||||
GameObject row = MakeRow(parent);
|
||||
MakeLabel("량:", row.transform, 14f, 30f);
|
||||
shieldAmountField = MakeInputField("ShieldAmt", row.transform, "1000", 60f);
|
||||
MakeButton("적용", row.transform, OnApplyShield, 60f);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이상상태 섹션
|
||||
/// </summary>
|
||||
private void BuildAbnormalitySection(Transform parent)
|
||||
{
|
||||
MakeSectionHeader("이상상태", parent);
|
||||
|
||||
MakeButton("기절", parent, OnApplyStun);
|
||||
MakeButton("침묵", parent, OnApplySilence);
|
||||
MakeButton("위협 초기화", parent, OnClearThreat);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// UI 업데이트
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// HP 디스플레이 갱신
|
||||
/// </summary>
|
||||
private void UpdateHPDisplay()
|
||||
{
|
||||
if (hpInfoText == null || hpSlider == null)
|
||||
return;
|
||||
|
||||
float current = cachedBoss.CurrentHealth;
|
||||
float max = cachedBoss.MaxHealth;
|
||||
float pct = max > 0f ? current / max : 0f;
|
||||
|
||||
hpInfoText.text = $"HP: {Mathf.CeilToInt(current)} / {Mathf.CeilToInt(max)} ({pct * 100f:F1}%)";
|
||||
|
||||
suppressSliderCallback = true;
|
||||
hpSlider.value = pct;
|
||||
suppressSliderCallback = false;
|
||||
|
||||
if (hpSliderFillImage != null)
|
||||
hpSliderFillImage.color = GetHealthColor(pct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HP 퍼센트에 따른 색상
|
||||
/// </summary>
|
||||
private static Color GetHealthColor(float pct)
|
||||
{
|
||||
if (pct > 0.6f) return new Color(0.2f, 0.8f, 0.2f);
|
||||
if (pct > 0.3f) return new Color(0.9f, 0.7f, 0.1f);
|
||||
return new Color(0.9f, 0.2f, 0.2f);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// 보스 조작
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 보스 HP를 퍼센트로 설정
|
||||
/// </summary>
|
||||
private void SetBossHP(float percent)
|
||||
{
|
||||
if (!IsHost || NoBoss) return;
|
||||
|
||||
float targetHP = cachedBoss.MaxHealth * percent;
|
||||
float diff = cachedBoss.CurrentHealth - targetHP;
|
||||
|
||||
if (diff > 0f)
|
||||
cachedBoss.TakeDamage(diff);
|
||||
else if (diff < 0f)
|
||||
cachedBoss.Heal(-diff);
|
||||
}
|
||||
|
||||
private void OnHPSliderChanged(float value)
|
||||
{
|
||||
if (suppressSliderCallback) return;
|
||||
SetBossHP(value);
|
||||
}
|
||||
|
||||
private void ApplyDirectHP()
|
||||
{
|
||||
if (NoBoss || !IsHost) return;
|
||||
if (float.TryParse(hpInputField.text, out float hp))
|
||||
{
|
||||
float clamped = Mathf.Clamp(hp, 0f, cachedBoss.MaxHealth);
|
||||
float pct = cachedBoss.MaxHealth > 0f ? clamped / cachedBoss.MaxHealth : 0f;
|
||||
SetBossHP(pct);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnForcePhase()
|
||||
{
|
||||
if (NoBoss || !IsHost) return;
|
||||
if (int.TryParse(phaseInputField.text, out int phase))
|
||||
{
|
||||
phase = Mathf.Clamp(phase, 0, Mathf.Max(0, cachedBoss.TotalPhases - 1));
|
||||
cachedBoss.ForcePhaseTransition(phase);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRestartPhase()
|
||||
{
|
||||
if (NoBoss || !IsHost) return;
|
||||
cachedBoss.RestartCurrentPhase();
|
||||
}
|
||||
|
||||
private void OnRespawn()
|
||||
{
|
||||
if (NoBoss || !IsHost) return;
|
||||
cachedBoss.Respawn();
|
||||
}
|
||||
|
||||
private void OnFullHeal()
|
||||
{
|
||||
if (NoBoss || !IsHost) return;
|
||||
cachedBoss.Heal(cachedBoss.MaxHealth);
|
||||
}
|
||||
|
||||
private void OnApplyShield()
|
||||
{
|
||||
if (NoBoss || !IsHost) return;
|
||||
if (float.TryParse(shieldAmountField.text, out float amt) && amt > 0f)
|
||||
cachedBoss.ApplyShield(amt, 30f);
|
||||
}
|
||||
|
||||
private void OnApplyStun()
|
||||
{
|
||||
if (NoBoss || !IsHost || stunAbnormalityData == null) return;
|
||||
AbnormalityManager am = cachedBoss.GetComponent<AbnormalityManager>();
|
||||
if (am != null) am.ApplyAbnormality(stunAbnormalityData, cachedBoss.gameObject);
|
||||
}
|
||||
|
||||
private void OnApplySilence()
|
||||
{
|
||||
if (NoBoss || !IsHost || silenceAbnormalityData == null) return;
|
||||
AbnormalityManager am = cachedBoss.GetComponent<AbnormalityManager>();
|
||||
if (am != null) am.ApplyAbnormality(silenceAbnormalityData, cachedBoss.gameObject);
|
||||
}
|
||||
|
||||
private void OnClearThreat()
|
||||
{
|
||||
if (NoBoss || !IsHost) return;
|
||||
cachedBoss.ClearAllThreat();
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// 토글
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
private void TogglePanel()
|
||||
{
|
||||
isPanelOpen = !isPanelOpen;
|
||||
if (panelRoot != null)
|
||||
panelRoot.SetActive(isPanelOpen);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────
|
||||
// UI 유틸리티
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
private static TMP_FontAsset DefaultFont => TMP_Settings.defaultFontAsset;
|
||||
|
||||
private static void MakeSectionHeader(string text, Transform parent)
|
||||
{
|
||||
TMP_Text h = MakeLabel(text, parent, 16f);
|
||||
h.fontStyle = FontStyles.Bold;
|
||||
h.color = new Color(0.9f, 0.9f, 0.9f);
|
||||
}
|
||||
|
||||
private static TMP_Text MakeLabel(string text, Transform parent, float fontSize, float width = 0f)
|
||||
{
|
||||
GameObject go = new GameObject("Label", typeof(RectTransform), typeof(TextMeshProUGUI));
|
||||
go.transform.SetParent(parent, false);
|
||||
|
||||
if (width > 0f)
|
||||
{
|
||||
LayoutElement le = go.AddComponent<LayoutElement>();
|
||||
le.preferredWidth = width;
|
||||
le.minWidth = width;
|
||||
}
|
||||
|
||||
TMP_Text t = go.GetComponent<TextMeshProUGUI>();
|
||||
t.text = text;
|
||||
t.fontSize = fontSize;
|
||||
t.color = new Color(0.75f, 0.75f, 0.75f);
|
||||
t.alignment = TextAlignmentOptions.Left;
|
||||
t.textWrappingMode = TextWrappingModes.NoWrap;
|
||||
if (DefaultFont != null) t.font = DefaultFont;
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
private static Button MakeButton(string text, Transform parent, Action onClick, float width = 0f)
|
||||
{
|
||||
GameObject go = new GameObject("Btn_" + text,
|
||||
typeof(RectTransform), typeof(Image), typeof(Button));
|
||||
go.transform.SetParent(parent, false);
|
||||
|
||||
LayoutElement le = go.AddComponent<LayoutElement>();
|
||||
le.preferredHeight = 28f;
|
||||
if (width > 0f) { le.preferredWidth = width; le.minWidth = width; }
|
||||
|
||||
go.GetComponent<Image>().color = new Color(0.18f, 0.18f, 0.18f, 1f);
|
||||
go.GetComponent<Button>().onClick.AddListener(new UnityEngine.Events.UnityAction(onClick));
|
||||
|
||||
// 버튼 내 텍스트
|
||||
TMP_Text lbl = new GameObject("Text", typeof(RectTransform), typeof(TextMeshProUGUI))
|
||||
.GetComponent<TextMeshProUGUI>();
|
||||
lbl.transform.SetParent(go.transform, false);
|
||||
RectTransform lr = lbl.GetComponent<RectTransform>();
|
||||
lr.anchorMin = Vector2.zero;
|
||||
lr.anchorMax = Vector2.one;
|
||||
lr.sizeDelta = Vector2.zero;
|
||||
lbl.text = text;
|
||||
lbl.fontSize = 14f;
|
||||
lbl.alignment = TextAlignmentOptions.Center;
|
||||
lbl.color = new Color(0.85f, 0.85f, 0.85f);
|
||||
lbl.textWrappingMode = TextWrappingModes.NoWrap;
|
||||
if (DefaultFont != null) lbl.font = DefaultFont;
|
||||
|
||||
return go.GetComponent<Button>();
|
||||
}
|
||||
|
||||
private static GameObject MakeRow(Transform parent)
|
||||
{
|
||||
GameObject go = new GameObject("Row", typeof(RectTransform), typeof(HorizontalLayoutGroup));
|
||||
go.transform.SetParent(parent, false);
|
||||
|
||||
HorizontalLayoutGroup hlg = go.GetComponent<HorizontalLayoutGroup>();
|
||||
hlg.childAlignment = TextAnchor.MiddleLeft;
|
||||
hlg.childControlWidth = true;
|
||||
hlg.childControlHeight = true;
|
||||
hlg.childForceExpandWidth = false;
|
||||
hlg.childForceExpandHeight = false;
|
||||
hlg.spacing = 4f;
|
||||
|
||||
go.AddComponent<LayoutElement>().preferredHeight = 28f;
|
||||
|
||||
return go;
|
||||
}
|
||||
|
||||
private static Slider MakeSlider(string name, Transform parent, out Image fillImage)
|
||||
{
|
||||
GameObject go = new GameObject(name, typeof(RectTransform), typeof(Slider));
|
||||
go.transform.SetParent(parent, false);
|
||||
|
||||
go.AddComponent<LayoutElement>().preferredHeight = 24f;
|
||||
|
||||
Slider slider = go.GetComponent<Slider>();
|
||||
slider.minValue = 0f;
|
||||
slider.maxValue = 1f;
|
||||
slider.value = 1f;
|
||||
|
||||
// 배경
|
||||
GameObject bg = new GameObject("BG", typeof(RectTransform), typeof(Image));
|
||||
bg.transform.SetParent(go.transform, false);
|
||||
RectTransform bgr = bg.GetComponent<RectTransform>();
|
||||
bgr.anchorMin = new Vector2(0f, 0.25f);
|
||||
bgr.anchorMax = new Vector2(1f, 0.75f);
|
||||
bgr.sizeDelta = Vector2.zero;
|
||||
bg.GetComponent<Image>().color = new Color(0.2f, 0.2f, 0.2f, 1f);
|
||||
slider.targetGraphic = bg.GetComponent<Image>();
|
||||
|
||||
// Fill Area + Fill
|
||||
GameObject fa = new GameObject("Fill Area", typeof(RectTransform));
|
||||
fa.transform.SetParent(go.transform, false);
|
||||
RectTransform far = fa.GetComponent<RectTransform>();
|
||||
far.anchorMin = Vector2.zero;
|
||||
far.anchorMax = Vector2.one;
|
||||
far.sizeDelta = Vector2.zero;
|
||||
|
||||
GameObject fill = new GameObject("Fill", typeof(RectTransform), typeof(Image));
|
||||
fill.transform.SetParent(fa.transform, false);
|
||||
RectTransform fr = fill.GetComponent<RectTransform>();
|
||||
fr.anchorMin = Vector2.zero;
|
||||
fr.anchorMax = Vector2.one;
|
||||
fr.sizeDelta = Vector2.zero;
|
||||
fillImage = fill.GetComponent<Image>();
|
||||
fillImage.color = new Color(0.2f, 0.8f, 0.2f);
|
||||
slider.fillRect = fr;
|
||||
|
||||
// Handle Slide Area + Handle
|
||||
GameObject hsa = new GameObject("Handle Slide Area", typeof(RectTransform));
|
||||
hsa.transform.SetParent(go.transform, false);
|
||||
RectTransform hsar = hsa.GetComponent<RectTransform>();
|
||||
hsar.anchorMin = Vector2.zero;
|
||||
hsar.anchorMax = Vector2.one;
|
||||
hsar.offsetMin = new Vector2(10f, 0f);
|
||||
hsar.offsetMax = new Vector2(-10f, 0f);
|
||||
|
||||
GameObject handle = new GameObject("Handle", typeof(RectTransform), typeof(Image));
|
||||
handle.transform.SetParent(hsa.transform, false);
|
||||
handle.GetComponent<RectTransform>().sizeDelta = new Vector2(16f, 0f);
|
||||
handle.GetComponent<Image>().color = Color.white;
|
||||
slider.handleRect = handle.GetComponent<RectTransform>();
|
||||
|
||||
return slider;
|
||||
}
|
||||
|
||||
private static TMP_InputField MakeInputField(string name, Transform parent,
|
||||
string placeholder, float width)
|
||||
{
|
||||
GameObject go = new GameObject(name, typeof(RectTransform), typeof(Image), typeof(TMP_InputField));
|
||||
go.transform.SetParent(parent, false);
|
||||
|
||||
LayoutElement le = go.AddComponent<LayoutElement>();
|
||||
le.preferredWidth = width;
|
||||
le.minWidth = width;
|
||||
le.preferredHeight = 28f;
|
||||
|
||||
Image bg = go.GetComponent<Image>();
|
||||
bg.color = new Color(0.12f, 0.12f, 0.12f, 1f);
|
||||
|
||||
TMP_InputField input = go.GetComponent<TMP_InputField>();
|
||||
input.targetGraphic = bg;
|
||||
|
||||
// Text
|
||||
TMP_Text txt = new GameObject("Text", typeof(RectTransform), typeof(TextMeshProUGUI))
|
||||
.GetComponent<TextMeshProUGUI>();
|
||||
txt.transform.SetParent(go.transform, false);
|
||||
txt.GetComponent<RectTransform>().anchorMin = Vector2.zero;
|
||||
txt.GetComponent<RectTransform>().anchorMax = Vector2.one;
|
||||
txt.GetComponent<RectTransform>().offsetMin = new Vector2(4f, 2f);
|
||||
txt.GetComponent<RectTransform>().offsetMax = new Vector2(-4f, -2f);
|
||||
txt.fontSize = 14f;
|
||||
txt.alignment = TextAlignmentOptions.MidlineLeft;
|
||||
txt.color = Color.white;
|
||||
txt.textWrappingMode = TextWrappingModes.NoWrap;
|
||||
if (DefaultFont != null) txt.font = DefaultFont;
|
||||
input.textComponent = txt;
|
||||
|
||||
// Placeholder
|
||||
TMP_Text ph = new GameObject("Placeholder", typeof(RectTransform), typeof(TextMeshProUGUI))
|
||||
.GetComponent<TextMeshProUGUI>();
|
||||
ph.transform.SetParent(go.transform, false);
|
||||
ph.GetComponent<RectTransform>().anchorMin = Vector2.zero;
|
||||
ph.GetComponent<RectTransform>().anchorMax = Vector2.one;
|
||||
ph.GetComponent<RectTransform>().offsetMin = new Vector2(4f, 2f);
|
||||
ph.GetComponent<RectTransform>().offsetMax = new Vector2(-4f, -2f);
|
||||
ph.fontSize = 14f;
|
||||
ph.alignment = TextAlignmentOptions.MidlineLeft;
|
||||
ph.color = new Color(0.5f, 0.5f, 0.5f);
|
||||
ph.text = placeholder;
|
||||
ph.textWrappingMode = TextWrappingModes.NoWrap;
|
||||
if (DefaultFont != null) ph.font = DefaultFont;
|
||||
input.placeholder = ph;
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
private static TMP_Text MakeTextChild(string name, Transform parent)
|
||||
{
|
||||
TMP_Text t = new GameObject(name, typeof(RectTransform), typeof(TextMeshProUGUI))
|
||||
.GetComponent<TextMeshProUGUI>();
|
||||
t.transform.SetParent(parent, false);
|
||||
t.GetComponent<RectTransform>().anchorMin = Vector2.zero;
|
||||
t.GetComponent<RectTransform>().anchorMax = Vector2.one;
|
||||
t.GetComponent<RectTransform>().sizeDelta = Vector2.zero;
|
||||
if (DefaultFont != null) t.font = DefaultFont;
|
||||
return t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
2
Assets/_Game/Scripts/UI/DebugPanelUI.cs.meta
Normal file
2
Assets/_Game/Scripts/UI/DebugPanelUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7b7611f77d92f8e41bfe5dfb5ac1768f
|
||||
Reference in New Issue
Block a user