feat: 디버그 패널 스킬 강제 발동 및 UI 모드 토글 시스템 추가

- UIModeController: leftAlt 키로 커서 표시/게임플레이 입력 차단 토글 (공용 싱글톤)
- DebugPanelUI: 보스 스킬 강제 발동 섹션 추가 (드롭다운 + 발동/취소 버튼)
- 에디터에서 Data/Skills의 보스 이름 기반 스킬 검색, 빌드에서 패턴 슬롯 fallback
- BossCombatBehaviorContext.GetAllPatternSkills() 추가 (디버그용 스킬 목록 수집)
- TMP Settings에 한글 폰트(MaruBuri)를 fallback으로 등록
- 젬/패시브/디버그 토글 버튼을 우측 하단에 수직 정렬
- InputSystem에 UIMode 액션(leftAlt) 추가
This commit is contained in:
2026-04-01 23:14:05 +09:00
parent 3663692b9d
commit ce883e4fa3
10 changed files with 569 additions and 20 deletions

View File

@@ -1,6 +1,8 @@
#if UNITY_EDITOR || DEVELOPMENT_BUILD
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;
@@ -10,6 +12,12 @@ using Unity.Netcode;
using Colosseum.Enemy;
using Colosseum.Abnormalities;
using Colosseum.AI;
using Colosseum.Skills;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Colosseum.UI
{
@@ -35,6 +43,12 @@ namespace Colosseum.UI
private BossEnemy cachedBoss;
private bool suppressSliderCallback;
// 스킬 강제 발동
private TMP_Dropdown skillDropdown;
private List<SkillData> debugSkillList;
private SkillController debugSkillController;
private BossEnemy cachedBossForSkillDropdown;
// UI 참조
private GameObject toggleButtonObject;
private GameObject panelRoot;
@@ -61,6 +75,15 @@ namespace Colosseum.UI
{
if (panelRoot != null)
panelRoot.SetActive(false);
if (UIModeController.Instance != null)
UIModeController.Instance.OnUIModeChanged += OnUIModeChanged;
}
private void OnDestroy()
{
if (UIModeController.Instance != null)
UIModeController.Instance.OnUIModeChanged -= OnUIModeChanged;
}
private void Update()
@@ -70,6 +93,7 @@ namespace Colosseum.UI
return;
UpdateHPDisplay();
RefreshSkillDropdownIfNeeded();
}
/// <summary>
@@ -198,6 +222,7 @@ namespace Colosseum.UI
BuildBossControlSection(content.transform);
BuildShieldSection(content.transform);
BuildAbnormalitySection(content.transform);
BuildSkillForceSection(content.transform);
}
/// <summary>
@@ -267,6 +292,20 @@ namespace Colosseum.UI
MakeButton("위협 초기화", parent, OnClearThreat);
}
/// <summary>
/// 스킬 강제 발동 섹션
/// </summary>
private void BuildSkillForceSection(Transform parent)
{
MakeSectionHeader("스킬 강제 발동", parent);
skillDropdown = MakeDropdown("SkillDropdown", parent);
GameObject row = MakeRow(parent);
MakeButton("발동", row.transform, OnForceSkill, 80f);
MakeButton("취소", row.transform, OnCancelSkill, 80f);
}
// ──────────────────────────────────────────────────
// UI 업데이트
// ──────────────────────────────────────────────────
@@ -395,15 +434,154 @@ namespace Colosseum.UI
cachedBoss.ClearAllThreat();
}
// ──────────────────────────────────────────────────
// 스킬 강제 발동
// ──────────────────────────────────────────────────
/// <summary>
/// 보스가 변경되었으면 스킬 드롭다운을 갱신합니다.
/// </summary>
private void RefreshSkillDropdownIfNeeded()
{
if (skillDropdown == null)
return;
if (cachedBoss != cachedBossForSkillDropdown)
{
cachedBossForSkillDropdown = cachedBoss;
RebuildSkillDropdown();
}
}
/// <summary>
/// 드롭다운을 갱신합니다.
/// 에디터에서는 Data/Skills에서 보스 이름이 포함된 스킬을 모두 검색하고,
/// 빌드에서는 패턴 슬롯의 스킬만 표시합니다.
/// </summary>
private void RebuildSkillDropdown()
{
debugSkillController = cachedBoss != null
? cachedBoss.GetComponent<SkillController>()
: null;
if (cachedBoss == null || debugSkillController == null)
{
skillDropdown.ClearOptions();
skillDropdown.options.Add(new TMP_Dropdown.OptionData("보스 없음"));
skillDropdown.value = 0;
debugSkillList = null;
return;
}
#if UNITY_EDITOR
debugSkillList = LoadSkillsFromAssetFolder();
#else
debugSkillList = LoadSkillsFromPatternSlots();
#endif
if (debugSkillList == null || debugSkillList.Count == 0)
{
skillDropdown.ClearOptions();
skillDropdown.options.Add(new TMP_Dropdown.OptionData("스킬 없음"));
skillDropdown.value = 0;
return;
}
List<TMP_Dropdown.OptionData> options = new List<TMP_Dropdown.OptionData>();
for (int i = 0; i < debugSkillList.Count; i++)
{
string name = debugSkillList[i].SkillName;
options.Add(new TMP_Dropdown.OptionData(string.IsNullOrEmpty(name) ? $"Skill {i}" : name));
}
skillDropdown.ClearOptions();
skillDropdown.AddOptions(options);
}
#if UNITY_EDITOR
/// <summary>
/// 에디터 전용: Data/Skills에서 보스 이름이 포함된 SkillData를 모두 검색합니다.
/// </summary>
private List<SkillData> LoadSkillsFromAssetFolder()
{
string bossName = cachedBoss.gameObject.name;
string[] guids = AssetDatabase.FindAssets($"t:SkillData", new[] { "Assets/_Game/Data/Skills" });
List<SkillData> result = new List<SkillData>();
for (int i = 0; i < guids.Length; i++)
{
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
// 파일명에 보스 이름이 포함된 스킬만 필터링
if (path.Contains(bossName))
{
SkillData skill = AssetDatabase.LoadAssetAtPath<SkillData>(path);
if (skill != null)
result.Add(skill);
}
}
return result;
}
#endif
/// <summary>
/// 패턴 슬롯에서 고유 스킬을 수집합니다 (빌드용 fallback).
/// </summary>
private List<SkillData> LoadSkillsFromPatternSlots()
{
BossCombatBehaviorContext context = cachedBoss.GetComponent<BossCombatBehaviorContext>();
if (context == null)
return null;
return context.GetAllPatternSkills();
}
/// <summary>
/// 드롭다운에서 선택한 스킬을 강제 발동합니다.
/// </summary>
private void OnForceSkill()
{
if (!IsHost || NoBoss || debugSkillController == null || debugSkillList == null)
return;
int index = skillDropdown.value;
if (index < 0 || index >= debugSkillList.Count)
return;
if (debugSkillController.IsExecutingSkill)
debugSkillController.CancelSkill();
debugSkillController.ResetAllCooldowns();
debugSkillController.ExecuteSkill(debugSkillList[index]);
}
/// <summary>
/// 현재 실행 중인 스킬을 취소합니다.
/// </summary>
private void OnCancelSkill()
{
if (!IsHost || NoBoss || debugSkillController == null)
return;
if (debugSkillController.IsExecutingSkill)
debugSkillController.CancelSkill();
}
// ──────────────────────────────────────────────────
// 토글
// ──────────────────────────────────────────────────
private void TogglePanel()
{
isPanelOpen = !isPanelOpen;
if (UIModeController.Instance != null)
UIModeController.Instance.SetUIModeActive(!UIModeController.Instance.IsUIModeActive);
}
private void OnUIModeChanged(bool uiModeActive)
{
if (panelRoot != null)
panelRoot.SetActive(isPanelOpen);
panelRoot.SetActive(uiModeActive);
isPanelOpen = uiModeActive;
}
// ──────────────────────────────────────────────────
@@ -611,6 +789,148 @@ namespace Colosseum.UI
if (DefaultFont != null) t.font = DefaultFont;
return t;
}
private static TMP_Dropdown MakeDropdown(string name, Transform parent)
{
// 메인 드롭다운 오브젝트
GameObject dropdownGo = new GameObject(name, typeof(RectTransform), typeof(Image), typeof(TMP_Dropdown));
dropdownGo.transform.SetParent(parent, false);
dropdownGo.AddComponent<LayoutElement>().preferredHeight = 30f;
// 캡션 라벨
GameObject captionGo = new GameObject("Caption", typeof(RectTransform), typeof(TextMeshProUGUI));
captionGo.transform.SetParent(dropdownGo.transform, false);
RectTransform captionRt = captionGo.GetComponent<RectTransform>();
captionRt.anchorMin = Vector2.zero;
captionRt.anchorMax = Vector2.one;
captionRt.offsetMin = new Vector2(10f, 1f);
captionRt.offsetMax = new Vector2(-25f, -2f);
TMP_Text captionText = captionGo.GetComponent<TextMeshProUGUI>();
captionText.text = "스킬 선택";
captionText.fontSize = 14f;
captionText.alignment = TextAlignmentOptions.MidlineLeft;
captionText.color = Color.white;
captionText.textWrappingMode = TextWrappingModes.NoWrap;
if (DefaultFont != null) captionText.font = DefaultFont;
// 화살표
GameObject arrowGo = new GameObject("Arrow", typeof(RectTransform), typeof(Image));
arrowGo.transform.SetParent(dropdownGo.transform, false);
RectTransform arrowRt = arrowGo.GetComponent<RectTransform>();
arrowRt.anchorMin = new Vector2(1f, 0.5f);
arrowRt.anchorMax = new Vector2(1f, 0.5f);
arrowRt.sizeDelta = new Vector2(20f, 20f);
arrowRt.pivot = new Vector2(0.5f, 0.5f);
arrowRt.anchoredPosition = new Vector2(-15f, 0f);
arrowGo.GetComponent<Image>().color = new Color(0.6f, 0.6f, 0.6f);
// 템플릿 (팝업 목록)
GameObject templateGo = new GameObject("Template", typeof(RectTransform), typeof(Image), typeof(ScrollRect));
templateGo.transform.SetParent(dropdownGo.transform, false);
templateGo.SetActive(false);
RectTransform templateRt = templateGo.GetComponent<RectTransform>();
templateRt.anchorMin = new Vector2(0f, -1f);
templateRt.anchorMax = new Vector2(1f, 0f);
templateRt.sizeDelta = new Vector2(0f, 150f);
templateRt.pivot = new Vector2(0.5f, 1f);
templateGo.GetComponent<Image>().color = new Color(0.15f, 0.15f, 0.15f, 0.95f);
ScrollRect scroll = templateGo.GetComponent<ScrollRect>();
scroll.horizontal = false;
scroll.movementType = ScrollRect.MovementType.Clamped;
scroll.scrollSensitivity = 20f;
// 뷰포트
GameObject viewportGo = new GameObject("Viewport", typeof(RectTransform), typeof(Image));
viewportGo.transform.SetParent(templateGo.transform, false);
RectTransform viewportRt = viewportGo.GetComponent<RectTransform>();
viewportRt.anchorMin = Vector2.zero;
viewportRt.anchorMax = Vector2.one;
viewportRt.sizeDelta = Vector2.zero;
viewportRt.pivot = new Vector2(0f, 1f);
viewportGo.GetComponent<Image>().color = Color.clear;
scroll.viewport = viewportRt;
// 콘텐츠
GameObject contentGo = new GameObject("Content", typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter));
contentGo.transform.SetParent(viewportGo.transform, false);
RectTransform contentRt = contentGo.GetComponent<RectTransform>();
contentRt.anchorMin = new Vector2(0f, 1f);
contentRt.anchorMax = new Vector2(1f, 1f);
contentRt.pivot = new Vector2(0.5f, 1f);
contentRt.sizeDelta = Vector2.zero;
contentRt.anchoredPosition = Vector2.zero;
VerticalLayoutGroup vlg = contentGo.GetComponent<VerticalLayoutGroup>();
vlg.childAlignment = TextAnchor.MiddleLeft;
vlg.childControlWidth = true;
vlg.childControlHeight = true;
vlg.childForceExpandWidth = true;
vlg.childForceExpandHeight = false;
contentGo.GetComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
scroll.content = contentRt;
// 아이템 템플릿
GameObject itemGo = new GameObject("Item", typeof(RectTransform), typeof(Toggle));
itemGo.transform.SetParent(contentGo.transform, false);
RectTransform itemRt = itemGo.GetComponent<RectTransform>();
itemRt.anchorMin = new Vector2(0f, 0.5f);
itemRt.anchorMax = new Vector2(1f, 0.5f);
itemRt.sizeDelta = new Vector2(0f, 20f);
LayoutElement itemLe = itemGo.AddComponent<LayoutElement>();
itemLe.preferredHeight = 20f;
itemLe.minHeight = 20f;
// 아이템 배경
GameObject itemBgGo = new GameObject("Item Background", typeof(RectTransform), typeof(Image));
itemBgGo.transform.SetParent(itemGo.transform, false);
RectTransform itemBgRt = itemBgGo.GetComponent<RectTransform>();
itemBgRt.anchorMin = Vector2.zero;
itemBgRt.anchorMax = Vector2.one;
itemBgRt.sizeDelta = Vector2.zero;
itemBgGo.GetComponent<Image>().color = new Color(0.25f, 0.25f, 0.25f, 1f);
// 아이템 체크마크
GameObject checkGo = new GameObject("Item Checkmark", typeof(RectTransform), typeof(Image));
checkGo.transform.SetParent(itemGo.transform, false);
RectTransform checkRt = checkGo.GetComponent<RectTransform>();
checkRt.anchorMin = new Vector2(0f, 0.5f);
checkRt.anchorMax = new Vector2(0f, 0.5f);
checkRt.sizeDelta = new Vector2(20f, 20f);
checkRt.pivot = new Vector2(0.5f, 0.5f);
checkRt.anchoredPosition = new Vector2(10f, 0f);
// 아이템 라벨
GameObject itemLabelGo = new GameObject("Item Label", typeof(RectTransform), typeof(TextMeshProUGUI));
itemLabelGo.transform.SetParent(itemGo.transform, false);
RectTransform itemLabelRt = itemLabelGo.GetComponent<RectTransform>();
itemLabelRt.anchorMin = Vector2.zero;
itemLabelRt.anchorMax = Vector2.one;
itemLabelRt.offsetMin = new Vector2(20f, 1f);
itemLabelRt.offsetMax = new Vector2(-5f, -2f);
TMP_Text itemLabelText = itemLabelGo.GetComponent<TextMeshProUGUI>();
itemLabelText.fontSize = 14f;
itemLabelText.alignment = TextAlignmentOptions.MidlineLeft;
itemLabelText.color = Color.white;
itemLabelText.textWrappingMode = TextWrappingModes.NoWrap;
if (DefaultFont != null) itemLabelText.font = DefaultFont;
// 토글 연결
Toggle toggle = itemGo.GetComponent<Toggle>();
toggle.targetGraphic = itemBgGo.GetComponent<Image>();
toggle.graphic = checkGo.GetComponent<Image>();
toggle.isOn = false;
// 드롭다운 연결
TMP_Dropdown dropdown = dropdownGo.GetComponent<TMP_Dropdown>();
dropdown.targetGraphic = dropdownGo.GetComponent<Image>();
dropdown.captionText = captionText;
dropdown.itemText = itemLabelText;
dropdown.template = templateRt;
return dropdown;
}
}
}