feat: 패시브 트리 프로토타입 구현

- 패시브 트리/노드/프리셋 데이터와 카탈로그 참조 구조를 추가하고 Resources 의존을 Data/Passives 자산 구조로 정리
- 플레이어 런타임, 전투 계수, 프리셋 적용, 멀티플레이 동기화 경로에 패시브 적용 로직 연결
- 프리팹 기반 패시브 트리 UI와 노드 아이콘/프리셋/상세 패널 흐름을 추가하고 HUD에 연동
- 패시브 디버그/부트스트랩 메뉴와 UI 프리팹 재생성 경로를 추가
This commit is contained in:
2026-03-26 22:59:39 +09:00
parent 13d1949ded
commit 8d1e97d01a
89 changed files with 10848 additions and 68 deletions

View File

@@ -0,0 +1,630 @@
using System.IO;
using TMPro;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.UI;
using Colosseum.Passives;
using Colosseum.UI;
namespace Colosseum.Editor
{
/// <summary>
/// 패시브 트리 uGUI 프리팹을 재생성하는 에디터 유틸리티입니다.
/// </summary>
public static class PassiveTreeUiPrefabBuilder
{
private const string PrefabFolder = "Assets/_Game/Prefabs/UI/PassiveTree";
private const string ViewPrefabPath = PrefabFolder + "/UI_PassiveTreeView.prefab";
private const string NodePrefabPath = PrefabFolder + "/UI_PassiveTreeNode.prefab";
private const string PlayerHudPrefabPath = "Assets/_Game/Prefabs/UI/UI_PlayerResources.prefab";
private const string PreferredFontAssetPath = "Assets/_Game/Fonts/TMP/TMP_MaruBuri.asset";
private const string NormalIconSpritePath = "Assets/_Game/Icons/Icon_Passive_Normal.png";
private const string SpecialIconSpritePath = "Assets/_Game/Icons/Icon_Passive_Special.png";
private const string PassiveCatalogAssetPath = "Assets/_Game/Data/Passives/Data_PassivePrototypeCatalog.asset";
private static readonly Color PanelBackgroundColor = new(0.07f, 0.07f, 0.10f, 0.98f);
private static readonly Color SectionBackgroundColor = new(0.12f, 0.12f, 0.16f, 0.96f);
private static readonly Color SectionOverlayColor = new(0f, 0f, 0f, 0.12f);
private static readonly Color ToggleButtonColor = new(0.18f, 0.21f, 0.16f, 0.96f);
private static readonly Color CloseButtonColor = new(0.34f, 0.20f, 0.20f, 0.96f);
private static readonly Color ButtonColor = new(0.20f, 0.20f, 0.24f, 0.96f);
private static readonly Color TextColor = new(0.90f, 0.88f, 0.82f, 1f);
private static readonly Color NodeColor = new(0.16f, 0.16f, 0.18f, 0.98f);
private static readonly Color NodeOutlineColor = new(0f, 0f, 0f, 0.45f);
[MenuItem("Tools/Colosseum/Passives/Rebuild Passive UI Prefabs")]
public static void RebuildPassiveUiPrefabs()
{
EnsureFolders();
PassiveTreeNodeView nodePrefab = BuildNodePrefab();
PassiveTreeViewReferences viewPrefab = BuildViewPrefab();
BindPlayerHudPrefab(viewPrefab, nodePrefab);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("[PassiveTreeUiPrefabBuilder] 패시브 UI 프리팹 재생성을 완료했습니다.");
}
private static void EnsureFolders()
{
if (!AssetDatabase.IsValidFolder("Assets/_Game/Prefabs/UI"))
{
AssetDatabase.CreateFolder("Assets/_Game/Prefabs", "UI");
}
if (!AssetDatabase.IsValidFolder(PrefabFolder))
{
AssetDatabase.CreateFolder("Assets/_Game/Prefabs/UI", "PassiveTree");
}
}
private static PassiveTreeNodeView BuildNodePrefab()
{
GameObject root = new("UI_PassiveTreeNode", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(Button), typeof(Outline), typeof(PassiveTreeNodeView));
try
{
RectTransform rect = root.GetComponent<RectTransform>();
rect.sizeDelta = new Vector2(70f, 70f);
Image background = root.GetComponent<Image>();
background.sprite = LoadPassiveNodeSprite(false);
background.type = Image.Type.Simple;
background.preserveAspect = true;
background.color = NodeColor;
Outline outline = root.GetComponent<Outline>();
outline.effectColor = NodeOutlineColor;
outline.effectDistance = new Vector2(2f, 2f);
Button button = root.GetComponent<Button>();
button.targetGraphic = background;
GameObject innerObject = CreateUiObject("InnerIcon", root.transform, typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
RectTransform innerRect = innerObject.GetComponent<RectTransform>();
StretchRect(innerRect);
innerRect.offsetMin = new Vector2(10f, 10f);
innerRect.offsetMax = new Vector2(-10f, -10f);
Image innerImage = innerObject.GetComponent<Image>();
innerImage.sprite = LoadPassiveNodeSprite(false);
innerImage.type = Image.Type.Simple;
innerImage.preserveAspect = true;
innerImage.color = new Color(0.32f, 0.32f, 0.34f, 0.36f);
innerImage.raycastTarget = false;
PassiveTreeNodeView references = root.GetComponent<PassiveTreeNodeView>();
SetSerializedReference(references, "rootRect", rect);
SetSerializedReference(references, "backgroundImage", background);
SetSerializedReference(references, "innerImage", innerImage);
SetSerializedReference(references, "button", button);
SetSerializedReference(references, "outline", outline);
GameObject saved = SavePrefab(root, NodePrefabPath);
return saved.GetComponent<PassiveTreeNodeView>();
}
finally
{
Object.DestroyImmediate(root);
}
}
private static PassiveTreeViewReferences BuildViewPrefab()
{
GameObject root = new("UI_PassiveTreeView", typeof(RectTransform), typeof(PassiveTreeViewReferences));
try
{
RectTransform rootRect = root.GetComponent<RectTransform>();
StretchRect(rootRect);
Button toggleButton = CreateButton(root.transform, "Button_PassiveTree", "패시브", new Vector2(110f, 40f), 20f, FontStyles.Bold, ToggleButtonColor, TextAnchor.MiddleCenter);
RectTransform toggleRect = toggleButton.GetComponent<RectTransform>();
toggleRect.anchorMin = new Vector2(1f, 0f);
toggleRect.anchorMax = new Vector2(1f, 0f);
toggleRect.pivot = new Vector2(1f, 0f);
toggleRect.anchoredPosition = new Vector2(-132f, 48f);
GameObject overlayRootObject = CreateUiObject("OverlayRoot", root.transform, typeof(RectTransform));
RectTransform overlayRootRect = overlayRootObject.GetComponent<RectTransform>();
StretchRect(overlayRootRect);
overlayRootObject.SetActive(false);
Image backdrop = AddImage(overlayRootObject, "Backdrop", new Color(0f, 0f, 0f, 0.52f));
StretchRect(backdrop.rectTransform);
GameObject panelObject = CreateUiObject("Panel", overlayRootObject.transform, typeof(RectTransform), typeof(Image), typeof(VerticalLayoutGroup));
RectTransform panelRect = panelObject.GetComponent<RectTransform>();
panelRect.anchorMin = new Vector2(0.5f, 0.5f);
panelRect.anchorMax = new Vector2(0.5f, 0.5f);
panelRect.pivot = new Vector2(0.5f, 0.5f);
panelRect.sizeDelta = new Vector2(1380f, 820f);
Image panelImage = panelObject.GetComponent<Image>();
panelImage.sprite = GetBuiltinSprite();
panelImage.type = Image.Type.Sliced;
panelImage.color = PanelBackgroundColor;
VerticalLayoutGroup panelLayout = panelObject.GetComponent<VerticalLayoutGroup>();
panelLayout.padding = new RectOffset(22, 22, 22, 22);
panelLayout.spacing = 16f;
panelLayout.childAlignment = TextAnchor.UpperLeft;
panelLayout.childControlWidth = true;
panelLayout.childControlHeight = true;
panelLayout.childForceExpandWidth = true;
panelLayout.childForceExpandHeight = false;
BuildHeader(panelObject.transform, out TextMeshProUGUI pointsSummaryText, out Button closeButton);
BuildBody(panelObject.transform,
out TextMeshProUGUI selectionSummaryText,
out Button nonePresetButton,
out Button defensePresetButton,
out Button supportPresetButton,
out Button attackPresetButton,
out Button clearButton,
out RectTransform graphRect,
out RectTransform connectionLayer,
out RectTransform nodeLayer,
out RectTransform detailContent,
out TextMeshProUGUI detailText,
out Button selectNodeButton,
out TextMeshProUGUI selectNodeButtonLabel);
BuildFooter(panelObject.transform, out TextMeshProUGUI statusText);
PassiveTreeViewReferences references = root.GetComponent<PassiveTreeViewReferences>();
SetSerializedReference(references, "rootRect", rootRect);
SetSerializedReference(references, "overlayRoot", overlayRootObject);
SetSerializedReference(references, "panelRect", panelRect);
SetSerializedReference(references, "toggleButton", toggleButton);
SetSerializedReference(references, "toggleButtonLabel", toggleButton.GetComponentInChildren<TextMeshProUGUI>(true));
SetSerializedReference(references, "pointsSummaryText", pointsSummaryText);
SetSerializedReference(references, "closeButton", closeButton);
SetSerializedReference(references, "selectionSummaryText", selectionSummaryText);
SetSerializedReference(references, "nonePresetButton", nonePresetButton);
SetSerializedReference(references, "defensePresetButton", defensePresetButton);
SetSerializedReference(references, "supportPresetButton", supportPresetButton);
SetSerializedReference(references, "attackPresetButton", attackPresetButton);
SetSerializedReference(references, "clearButton", clearButton);
SetSerializedReference(references, "graphRect", graphRect);
SetSerializedReference(references, "connectionLayer", connectionLayer);
SetSerializedReference(references, "nodeLayer", nodeLayer);
SetSerializedReference(references, "detailContent", detailContent);
SetSerializedReference(references, "detailText", detailText);
SetSerializedReference(references, "selectNodeButton", selectNodeButton);
SetSerializedReference(references, "selectNodeButtonLabel", selectNodeButtonLabel);
SetSerializedReference(references, "statusText", statusText);
GameObject saved = SavePrefab(root, ViewPrefabPath);
return saved.GetComponent<PassiveTreeViewReferences>();
}
finally
{
Object.DestroyImmediate(root);
}
}
private static void BindPlayerHudPrefab(PassiveTreeViewReferences viewPrefab, PassiveTreeNodeView nodePrefab)
{
GameObject root = PrefabUtility.LoadPrefabContents(PlayerHudPrefabPath);
try
{
PassiveTreeUI passiveTreeUi = root.GetComponent<PassiveTreeUI>();
if (passiveTreeUi == null)
{
passiveTreeUi = root.AddComponent<PassiveTreeUI>();
}
SerializedObject passiveTreeUiObject = new(passiveTreeUi);
passiveTreeUiObject.FindProperty("viewPrefab").objectReferenceValue = viewPrefab;
passiveTreeUiObject.FindProperty("nodePrefab").objectReferenceValue = nodePrefab;
passiveTreeUiObject.FindProperty("passivePrototypeCatalog").objectReferenceValue = LoadPassivePrototypeCatalog();
passiveTreeUiObject.FindProperty("normalNodeSprite").objectReferenceValue = LoadPassiveNodeSprite(false);
passiveTreeUiObject.FindProperty("specialNodeSprite").objectReferenceValue = LoadPassiveNodeSprite(true);
passiveTreeUiObject.FindProperty("innerNodeSprite").objectReferenceValue = LoadPassiveNodeSprite(false);
passiveTreeUiObject.ApplyModifiedPropertiesWithoutUndo();
PrefabUtility.SaveAsPrefabAsset(root, PlayerHudPrefabPath);
}
finally
{
PrefabUtility.UnloadPrefabContents(root);
}
}
private static void BuildHeader(Transform parent, out TextMeshProUGUI pointsSummaryText, out Button closeButton)
{
GameObject headerObject = CreateUiObject("Header", parent, typeof(RectTransform), typeof(Image), typeof(LayoutElement), typeof(HorizontalLayoutGroup));
RectTransform headerRect = headerObject.GetComponent<RectTransform>();
headerObject.GetComponent<LayoutElement>().minHeight = 54f;
Image headerImage = headerObject.GetComponent<Image>();
headerImage.sprite = GetBuiltinSprite();
headerImage.type = Image.Type.Sliced;
headerImage.color = SectionBackgroundColor;
HorizontalLayoutGroup headerLayout = headerObject.GetComponent<HorizontalLayoutGroup>();
headerLayout.padding = new RectOffset(14, 14, 8, 8);
headerLayout.spacing = 10f;
headerLayout.childAlignment = TextAnchor.MiddleLeft;
headerLayout.childControlWidth = true;
headerLayout.childControlHeight = true;
headerLayout.childForceExpandWidth = false;
headerLayout.childForceExpandHeight = false;
GameObject titleGroup = CreateUiObject("TitleGroup", headerObject.transform, typeof(RectTransform), typeof(LayoutElement), typeof(VerticalLayoutGroup));
titleGroup.GetComponent<LayoutElement>().flexibleWidth = 1f;
VerticalLayoutGroup titleLayout = titleGroup.GetComponent<VerticalLayoutGroup>();
titleLayout.spacing = 0f;
titleLayout.childAlignment = TextAnchor.MiddleLeft;
titleLayout.childControlWidth = true;
titleLayout.childControlHeight = true;
titleLayout.childForceExpandWidth = true;
titleLayout.childForceExpandHeight = false;
TextMeshProUGUI title = CreateText(titleGroup.transform, "Label_Title", 24f, TextAlignmentOptions.Left, FontStyles.Bold);
title.text = "패시브 트리";
title.textWrappingMode = TextWrappingModes.NoWrap;
pointsSummaryText = CreateText(headerObject.transform, "Label_Points", 17f, TextAlignmentOptions.Right, FontStyles.Normal);
pointsSummaryText.text = string.Empty;
pointsSummaryText.textWrappingMode = TextWrappingModes.NoWrap;
pointsSummaryText.gameObject.AddComponent<LayoutElement>().preferredWidth = 420f;
closeButton = CreateButton(headerObject.transform, "Button_Close", "닫기", new Vector2(84f, 34f), 18f, FontStyles.Bold, CloseButtonColor, TextAnchor.MiddleCenter);
}
private static void BuildBody(
Transform parent,
out TextMeshProUGUI selectionSummaryText,
out Button nonePresetButton,
out Button defensePresetButton,
out Button supportPresetButton,
out Button attackPresetButton,
out Button clearButton,
out RectTransform graphRect,
out RectTransform connectionLayer,
out RectTransform nodeLayer,
out RectTransform detailContent,
out TextMeshProUGUI detailText,
out Button selectNodeButton,
out TextMeshProUGUI selectNodeButtonLabel)
{
GameObject bodyObject = CreateUiObject("Body", parent, typeof(RectTransform), typeof(LayoutElement), typeof(HorizontalLayoutGroup));
LayoutElement bodyLayoutElement = bodyObject.GetComponent<LayoutElement>();
bodyLayoutElement.flexibleHeight = 1f;
HorizontalLayoutGroup bodyLayout = bodyObject.GetComponent<HorizontalLayoutGroup>();
bodyLayout.spacing = 16f;
bodyLayout.childAlignment = TextAnchor.UpperLeft;
bodyLayout.childControlWidth = true;
bodyLayout.childControlHeight = true;
bodyLayout.childForceExpandWidth = true;
bodyLayout.childForceExpandHeight = true;
RectTransform graphSection = CreateSectionRoot(bodyObject.transform, "Section_Graph", 0f);
graphSection.GetComponent<LayoutElement>().flexibleWidth = 1f;
graphSection.GetComponent<LayoutElement>().minWidth = 760f;
CreateSectionTitle(graphSection, "트리 그래프");
GameObject graphSurface = CreateUiObject("GraphSurface", graphSection, typeof(RectTransform), typeof(LayoutElement), typeof(Image));
graphRect = graphSurface.GetComponent<RectTransform>();
LayoutElement graphLayout = graphSurface.GetComponent<LayoutElement>();
graphLayout.flexibleHeight = 1f;
graphLayout.minHeight = 560f;
Image graphImage = graphSurface.GetComponent<Image>();
graphImage.sprite = GetBuiltinSprite();
graphImage.type = Image.Type.Sliced;
graphImage.color = SectionOverlayColor;
connectionLayer = CreateUiObject("Connections", graphSurface.transform, typeof(RectTransform)).GetComponent<RectTransform>();
StretchRect(connectionLayer);
nodeLayer = CreateUiObject("Nodes", graphSurface.transform, typeof(RectTransform)).GetComponent<RectTransform>();
StretchRect(nodeLayer);
RectTransform rightSection = CreateSectionRoot(bodyObject.transform, "Section_Right", 360f);
CreateSectionTitle(rightSection, "노드 상세");
RectTransform detailScroll = CreateScrollView(rightSection, "Scroll_Detail", out detailContent);
LayoutElement detailScrollLayout = detailScroll.gameObject.AddComponent<LayoutElement>();
detailScrollLayout.flexibleHeight = 1f;
detailScrollLayout.minHeight = 300f;
detailText = CreateText(detailContent, "Label_Detail", 19f, TextAlignmentOptions.TopLeft, FontStyles.Normal);
detailText.textWrappingMode = TextWrappingModes.Normal;
detailText.overflowMode = TextOverflowModes.Overflow;
detailText.margin = new Vector4(18f, 8f, 18f, 14f);
detailText.extraPadding = true;
detailText.gameObject.AddComponent<LayoutElement>().minHeight = 300f;
selectNodeButton = CreateButton(rightSection, "Button_SelectNode", "노드 선택", new Vector2(0f, 58f), 21f, FontStyles.Bold, ButtonColor, TextAnchor.MiddleCenter);
selectNodeButtonLabel = selectNodeButton.GetComponentInChildren<TextMeshProUGUI>(true);
CreateDivider(rightSection, "Divider_Actions");
selectionSummaryText = CreateInfoText(rightSection, "Label_SelectionSummary", 132f);
CreateSectionTitle(rightSection, "프리셋");
GameObject presetGrid = CreateUiObject("PresetGrid", rightSection, typeof(RectTransform), typeof(GridLayoutGroup));
GridLayoutGroup presetLayout = presetGrid.GetComponent<GridLayoutGroup>();
presetLayout.cellSize = new Vector2(154f, 56f);
presetLayout.spacing = new Vector2(8f, 8f);
presetLayout.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
presetLayout.constraintCount = 2;
nonePresetButton = CreateButton(presetGrid.transform, "Button_Preset_None", "패시브 없음", Vector2.zero, 18f, FontStyles.Normal, ButtonColor, TextAnchor.MiddleCenter);
defensePresetButton = CreateButton(presetGrid.transform, "Button_Preset_Defense", "방어형 패시브", Vector2.zero, 18f, FontStyles.Normal, ButtonColor, TextAnchor.MiddleCenter);
supportPresetButton = CreateButton(presetGrid.transform, "Button_Preset_Support", "지원형 패시브", Vector2.zero, 18f, FontStyles.Normal, ButtonColor, TextAnchor.MiddleCenter);
attackPresetButton = CreateButton(presetGrid.transform, "Button_Preset_Attack", "공격형 패시브", Vector2.zero, 18f, FontStyles.Normal, ButtonColor, TextAnchor.MiddleCenter);
clearButton = CreateButton(rightSection, "Button_Clear", "전체 초기화", new Vector2(0f, 52f), 18f, FontStyles.Bold, ButtonColor, TextAnchor.MiddleCenter);
}
private static void BuildFooter(Transform parent, out TextMeshProUGUI statusText)
{
GameObject footerObject = CreateUiObject("Footer", parent, typeof(RectTransform), typeof(Image), typeof(LayoutElement), typeof(HorizontalLayoutGroup));
footerObject.GetComponent<LayoutElement>().minHeight = 42f;
Image footerImage = footerObject.GetComponent<Image>();
footerImage.sprite = GetBuiltinSprite();
footerImage.type = Image.Type.Sliced;
footerImage.color = SectionBackgroundColor;
HorizontalLayoutGroup footerLayout = footerObject.GetComponent<HorizontalLayoutGroup>();
footerLayout.padding = new RectOffset(16, 16, 8, 8);
footerLayout.childAlignment = TextAnchor.MiddleLeft;
footerLayout.childControlWidth = true;
footerLayout.childControlHeight = true;
footerLayout.childForceExpandWidth = true;
footerLayout.childForceExpandHeight = true;
statusText = CreateText(footerObject.transform, "Label_Status", 17f, TextAlignmentOptions.MidlineLeft, FontStyles.Normal);
statusText.text = string.Empty;
statusText.textWrappingMode = TextWrappingModes.NoWrap;
statusText.overflowMode = TextOverflowModes.Ellipsis;
footerObject.SetActive(false);
}
private static RectTransform CreateSectionRoot(Transform parent, string name, float preferredWidth)
{
GameObject sectionObject = CreateUiObject(name, parent, typeof(RectTransform), typeof(Image), typeof(LayoutElement), typeof(VerticalLayoutGroup));
RectTransform rect = sectionObject.GetComponent<RectTransform>();
LayoutElement layoutElement = sectionObject.GetComponent<LayoutElement>();
layoutElement.flexibleHeight = 1f;
layoutElement.preferredWidth = preferredWidth;
if (preferredWidth > 0f)
{
layoutElement.minWidth = preferredWidth;
layoutElement.flexibleWidth = 0f;
}
Image sectionImage = sectionObject.GetComponent<Image>();
sectionImage.sprite = GetBuiltinSprite();
sectionImage.type = Image.Type.Sliced;
sectionImage.color = SectionBackgroundColor;
VerticalLayoutGroup layout = sectionObject.GetComponent<VerticalLayoutGroup>();
layout.padding = new RectOffset(16, 16, 16, 16);
layout.spacing = 12f;
layout.childAlignment = TextAnchor.UpperLeft;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = true;
layout.childForceExpandHeight = false;
return rect;
}
private static TextMeshProUGUI CreateSectionTitle(RectTransform parent, string text)
{
TextMeshProUGUI label = CreateText(parent, "Label_Title", 28f, TextAlignmentOptions.Left, FontStyles.Bold);
label.text = text;
return label;
}
private static TextMeshProUGUI CreateInfoText(RectTransform parent, string name, float minHeight)
{
GameObject containerObject = CreateUiObject($"{name}_Container", parent, typeof(RectTransform), typeof(Image), typeof(LayoutElement), typeof(VerticalLayoutGroup));
RectTransform containerRect = containerObject.GetComponent<RectTransform>();
containerObject.GetComponent<LayoutElement>().minHeight = minHeight;
Image containerImage = containerObject.GetComponent<Image>();
containerImage.sprite = GetBuiltinSprite();
containerImage.type = Image.Type.Sliced;
containerImage.color = SectionOverlayColor;
VerticalLayoutGroup layout = containerObject.GetComponent<VerticalLayoutGroup>();
layout.padding = new RectOffset(16, 16, 12, 12);
layout.childAlignment = TextAnchor.UpperLeft;
layout.childControlWidth = true;
layout.childControlHeight = true;
layout.childForceExpandWidth = true;
layout.childForceExpandHeight = false;
TextMeshProUGUI label = CreateText(containerRect, name, 18f, TextAlignmentOptions.TopLeft, FontStyles.Normal);
label.textWrappingMode = TextWrappingModes.Normal;
label.overflowMode = TextOverflowModes.Overflow;
label.margin = new Vector4(14f, 6f, 14f, 10f);
label.extraPadding = true;
return label;
}
private static void CreateDivider(Transform parent, string name)
{
GameObject dividerObject = CreateUiObject(name, parent, typeof(RectTransform), typeof(Image), typeof(LayoutElement));
dividerObject.GetComponent<Image>().color = new Color(0f, 0f, 0f, 0.22f);
LayoutElement layout = dividerObject.GetComponent<LayoutElement>();
layout.minHeight = 2f;
layout.preferredHeight = 2f;
}
private static RectTransform CreateScrollView(Transform parent, string name, out RectTransform content)
{
GameObject root = CreateUiObject(name, parent, typeof(RectTransform), typeof(Image), typeof(ScrollRect));
RectTransform rootRect = root.GetComponent<RectTransform>();
Image rootImage = root.GetComponent<Image>();
rootImage.sprite = GetBuiltinSprite();
rootImage.type = Image.Type.Sliced;
rootImage.color = SectionOverlayColor;
ScrollRect scrollRect = root.GetComponent<ScrollRect>();
scrollRect.horizontal = false;
scrollRect.vertical = true;
scrollRect.scrollSensitivity = 30f;
GameObject viewportObject = CreateUiObject("Viewport", root.transform, typeof(RectTransform), typeof(Image), typeof(Mask));
RectTransform viewport = viewportObject.GetComponent<RectTransform>();
StretchRect(viewport);
viewportObject.GetComponent<Image>().color = new Color(1f, 1f, 1f, 0.01f);
viewportObject.GetComponent<Mask>().showMaskGraphic = false;
GameObject contentObject = CreateUiObject("Content", viewportObject.transform, typeof(RectTransform), typeof(VerticalLayoutGroup), typeof(ContentSizeFitter));
content = contentObject.GetComponent<RectTransform>();
content.anchorMin = new Vector2(0f, 1f);
content.anchorMax = new Vector2(1f, 1f);
content.pivot = new Vector2(0f, 1f);
VerticalLayoutGroup contentLayout = contentObject.GetComponent<VerticalLayoutGroup>();
contentLayout.padding = new RectOffset(16, 16, 14, 14);
contentLayout.spacing = 8f;
contentLayout.childAlignment = TextAnchor.UpperLeft;
contentLayout.childControlWidth = true;
contentLayout.childControlHeight = true;
contentLayout.childForceExpandWidth = true;
contentLayout.childForceExpandHeight = false;
ContentSizeFitter fitter = contentObject.GetComponent<ContentSizeFitter>();
fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
fitter.horizontalFit = ContentSizeFitter.FitMode.Unconstrained;
scrollRect.viewport = viewport;
scrollRect.content = content;
return rootRect;
}
private static Button CreateButton(Transform parent, string name, string labelText, Vector2 size, float fontSize, FontStyles fontStyle, Color color, TextAnchor anchor)
{
GameObject buttonObject = CreateUiObject(name, parent, typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(Button));
RectTransform rect = buttonObject.GetComponent<RectTransform>();
if (size.sqrMagnitude > 0f)
{
rect.sizeDelta = size;
}
Image image = buttonObject.GetComponent<Image>();
image.sprite = GetBuiltinSprite();
image.type = Image.Type.Sliced;
image.color = color;
Button button = buttonObject.GetComponent<Button>();
button.targetGraphic = image;
TextMeshProUGUI label = CreateText(buttonObject.transform, "Label", fontSize, ToTmpAlignment(anchor), fontStyle);
label.text = labelText;
StretchRect(label.rectTransform);
label.color = Color.white;
label.textWrappingMode = TextWrappingModes.Normal;
return button;
}
private static TextMeshProUGUI CreateText(Transform parent, string name, float fontSize, TextAlignmentOptions alignment, FontStyles fontStyle)
{
GameObject textObject = CreateUiObject(name, parent, typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI));
RectTransform rect = textObject.GetComponent<RectTransform>();
rect.anchorMin = new Vector2(0f, 1f);
rect.anchorMax = new Vector2(1f, 1f);
rect.pivot = new Vector2(0f, 1f);
TextMeshProUGUI text = textObject.GetComponent<TextMeshProUGUI>();
text.font = LoadPreferredFontAsset();
text.fontSize = fontSize;
text.fontStyle = fontStyle;
text.color = TextColor;
text.alignment = alignment;
text.textWrappingMode = TextWrappingModes.Normal;
text.extraPadding = true;
text.raycastTarget = false;
return text;
}
private static TMP_FontAsset LoadPreferredFontAsset()
{
TMP_FontAsset preferredFont = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(PreferredFontAssetPath);
if (preferredFont != null)
return preferredFont;
return TMP_Settings.defaultFontAsset;
}
private static Sprite LoadPassiveNodeSprite(bool special)
{
string assetPath = special ? SpecialIconSpritePath : NormalIconSpritePath;
return AssetDatabase.LoadAssetAtPath<Sprite>(assetPath);
}
private static PassivePrototypeCatalogData LoadPassivePrototypeCatalog()
{
return AssetDatabase.LoadAssetAtPath<PassivePrototypeCatalogData>(PassiveCatalogAssetPath);
}
private static Image AddImage(GameObject gameObject, string childName, Color color)
{
GameObject imageObject = CreateUiObject(childName, gameObject.transform, typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
Image image = imageObject.GetComponent<Image>();
image.sprite = GetBuiltinSprite();
image.type = Image.Type.Sliced;
image.color = color;
return image;
}
private static GameObject CreateUiObject(string name, Transform parent, params System.Type[] componentTypes)
{
GameObject gameObject = new(name, componentTypes);
gameObject.transform.SetParent(parent, false);
return gameObject;
}
private static void StretchRect(RectTransform rectTransform)
{
rectTransform.anchorMin = Vector2.zero;
rectTransform.anchorMax = Vector2.one;
rectTransform.offsetMin = Vector2.zero;
rectTransform.offsetMax = Vector2.zero;
rectTransform.anchoredPosition = Vector2.zero;
rectTransform.sizeDelta = Vector2.zero;
}
private static void SetSerializedReference(Object target, string propertyName, Object value)
{
SerializedObject serializedObject = new(target);
serializedObject.FindProperty(propertyName).objectReferenceValue = value;
serializedObject.ApplyModifiedPropertiesWithoutUndo();
}
private static GameObject SavePrefab(GameObject root, string path)
{
PrefabUtility.SaveAsPrefabAsset(root, path);
return AssetDatabase.LoadAssetAtPath<GameObject>(path);
}
private static Sprite GetBuiltinSprite()
{
return AssetDatabase.GetBuiltinExtraResource<Sprite>("UI/Skin/UISprite.psd");
}
private static TextAlignmentOptions ToTmpAlignment(TextAnchor anchor)
{
return anchor switch
{
TextAnchor.MiddleCenter => TextAlignmentOptions.Center,
TextAnchor.MiddleLeft => TextAlignmentOptions.MidlineLeft,
_ => TextAlignmentOptions.Center,
};
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 99f2c00c8ea8e7947978c046303348b4

View File

@@ -0,0 +1,787 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using Colosseum.Passives;
using Colosseum.Player;
using Colosseum.Skills;
using Colosseum.Stats;
using Colosseum.UI;
namespace Colosseum.Editor
{
/// <summary>
/// 패시브 트리 프로토타입 에셋 생성 및 디버그 적용 메뉴입니다.
/// </summary>
public static class PlayerPassiveDebugMenu
{
private const string DataFolderPath = "Assets/_Game/Data";
private const string PassiveFolderPath = "Assets/_Game/Data/Passives";
private const string PassiveNodeFolderPath = "Assets/_Game/Data/Passives/Nodes";
private const string PassivePresetFolderPath = "Assets/_Game/Data/Passives/Presets";
private const string PassiveCatalogAssetPath = PassiveFolderPath + "/Data_PassivePrototypeCatalog.asset";
private const string PlayerPrefabPath = "Assets/_Game/Prefabs/Player/Prefab_Player_Default.prefab";
private const string PlayerResourcesPrefabPath = "Assets/_Game/Prefabs/UI/UI_PlayerResources.prefab";
private const string PassiveTreeAssetPath = PassiveFolderPath + "/Data_PassiveTree_Player_Prototype.asset";
private const string NonePresetAssetPath = PassivePresetFolderPath + "/Data_PassivePreset_Player_None.asset";
private const string DefensePresetAssetPath = PassivePresetFolderPath + "/Data_PassivePreset_Player_Tank.asset";
private const string SupportPresetAssetPath = PassivePresetFolderPath + "/Data_PassivePreset_Player_Support.asset";
private const string AttackPresetAssetPath = PassivePresetFolderPath + "/Data_PassivePreset_Player_Dps.asset";
private const string HubNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Hub.asset";
private const string DefenseEntryNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Tank_Entry.asset";
private const string DefenseCoreNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Tank_Core.asset";
private const string DefenseCapstoneNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Tank_Capstone.asset";
private const string SupportEntryNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Support_Entry.asset";
private const string SupportCoreNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Support_Core.asset";
private const string SupportCapstoneNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Support_Capstone.asset";
private const string AttackEntryNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Dps_Entry.asset";
private const string AttackCoreNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Dps_Core.asset";
private const string AttackCapstoneNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Dps_Capstone.asset";
private const string AttackDefenseBridgeNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_AttackDefense_Bridge.asset";
private const string DefenseSupportBridgeNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_DefenseSupport_Bridge.asset";
private const string SupportAttackBridgeNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_SupportAttack_Bridge.asset";
[MenuItem("Tools/Colosseum/Debug/Passive/Bootstrap Prototype Assets")]
private static void BootstrapPrototypeAssets()
{
EnsureFolder("Assets/_Game", "Data");
EnsureFolder(DataFolderPath, "Passives");
EnsureFolder(PassiveFolderPath, "Nodes");
EnsureFolder(PassiveFolderPath, "Presets");
PassiveNodeData hubNode = CreateOrLoadNode(HubNodeAssetPath);
PassiveNodeData defenseEntryNode = CreateOrLoadNode(DefenseEntryNodeAssetPath);
PassiveNodeData defenseCoreNode = CreateOrLoadNode(DefenseCoreNodeAssetPath);
PassiveNodeData defenseCapstoneNode = CreateOrLoadNode(DefenseCapstoneNodeAssetPath);
PassiveNodeData supportEntryNode = CreateOrLoadNode(SupportEntryNodeAssetPath);
PassiveNodeData supportCoreNode = CreateOrLoadNode(SupportCoreNodeAssetPath);
PassiveNodeData supportCapstoneNode = CreateOrLoadNode(SupportCapstoneNodeAssetPath);
PassiveNodeData attackEntryNode = CreateOrLoadNode(AttackEntryNodeAssetPath);
PassiveNodeData attackCoreNode = CreateOrLoadNode(AttackCoreNodeAssetPath);
PassiveNodeData attackCapstoneNode = CreateOrLoadNode(AttackCapstoneNodeAssetPath);
PassiveNodeData attackDefenseBridgeNode = CreateOrLoadNode(AttackDefenseBridgeNodeAssetPath);
PassiveNodeData defenseSupportBridgeNode = CreateOrLoadNode(DefenseSupportBridgeNodeAssetPath);
PassiveNodeData supportAttackBridgeNode = CreateOrLoadNode(SupportAttackBridgeNodeAssetPath);
ConfigureNode(
hubNode,
"hub",
"중심 허브",
"패시브 트리의 시작점입니다.",
PassiveNodeBranch.Common,
PassiveNodeKind.Hub,
PassiveAxisMask.None,
0,
0,
new Vector2(0f, 0f),
new PassiveNodeData[0],
new[] { attackEntryNode, defenseEntryNode, supportEntryNode },
new PassiveEffectConfig[0]);
ConfigureNode(
attackEntryNode,
"attack_entry",
"공세 적응",
"공격 축의 출발점으로, 기본 화력 계열 스탯을 끌어올립니다.",
PassiveNodeBranch.Attack,
PassiveNodeKind.Axis,
PassiveAxisMask.Attack,
1,
1,
new Vector2(0f, 0.34f),
new[] { hubNode },
new[] { hubNode, attackCoreNode, attackDefenseBridgeNode, supportAttackBridgeNode },
new[]
{
PassiveEffectConfig.CreateStat(StatType.Strength, StatModType.PercentAdd, 0.1f),
PassiveEffectConfig.CreateStat(StatType.Dexterity, StatModType.PercentAdd, 0.1f),
PassiveEffectConfig.CreateStat(StatType.Intelligence, StatModType.PercentAdd, 0.1f),
});
ConfigureNode(
attackCoreNode,
"attack_core",
"집중 공세",
"공격 스킬 계열의 핵심 화력을 강화합니다.",
PassiveNodeBranch.Attack,
PassiveNodeKind.Axis,
PassiveAxisMask.Attack,
2,
1,
new Vector2(0f, 0.6f),
new[] { attackEntryNode },
new[] { attackEntryNode, attackCapstoneNode },
new[]
{
PassiveEffectConfig.CreateScalar(PassiveEffectType.DamageMultiplier, 1.12f, SkillRoleType.Attack),
});
ConfigureNode(
attackCapstoneNode,
"attack_capstone",
"집행 증폭",
"공격 축 완성 노드로, 공격 계열 고위력 기술의 기여도를 강화합니다.",
PassiveNodeBranch.Attack,
PassiveNodeKind.Capstone,
PassiveAxisMask.Attack,
3,
2,
new Vector2(0f, 0.84f),
new[] { attackCoreNode },
new[] { attackCoreNode },
new[]
{
PassiveEffectConfig.CreateScalar(PassiveEffectType.DamageMultiplier, 1.10f, SkillRoleType.Attack),
PassiveEffectConfig.CreateScalar(PassiveEffectType.ManaCostMultiplier, 0.90f, SkillRoleType.Attack),
});
ConfigureNode(
defenseEntryNode,
"defense_entry",
"전열 적응",
"방어 축의 출발점으로, 전열 유지에 필요한 생존력을 확보합니다.",
PassiveNodeBranch.Defense,
PassiveNodeKind.Axis,
PassiveAxisMask.Defense,
1,
1,
new Vector2(-0.34f, -0.1f),
new[] { hubNode },
new[] { hubNode, defenseCoreNode, attackDefenseBridgeNode, defenseSupportBridgeNode },
new[]
{
PassiveEffectConfig.CreateStat(StatType.Vitality, StatModType.PercentAdd, 0.2f),
});
ConfigureNode(
defenseCoreNode,
"defense_core",
"방호 숙련",
"위협 유지와 보호막 수혜량을 함께 강화합니다.",
PassiveNodeBranch.Defense,
PassiveNodeKind.Axis,
PassiveAxisMask.Defense,
2,
1,
new Vector2(-0.58f, -0.34f),
new[] { defenseEntryNode },
new[] { defenseEntryNode, defenseCapstoneNode },
new[]
{
PassiveEffectConfig.CreateScalar(PassiveEffectType.ThreatGeneratedMultiplier, 1.30f),
PassiveEffectConfig.CreateScalar(PassiveEffectType.ShieldReceivedMultiplier, 1.15f),
});
ConfigureNode(
defenseCapstoneNode,
"defense_capstone",
"철벽 유지",
"받는 피해를 낮춰 전열 유지력을 완성합니다.",
PassiveNodeBranch.Defense,
PassiveNodeKind.Capstone,
PassiveAxisMask.Defense,
3,
2,
new Vector2(-0.82f, -0.58f),
new[] { defenseCoreNode },
new[] { defenseCoreNode },
new[]
{
PassiveEffectConfig.CreateScalar(PassiveEffectType.IncomingDamageMultiplier, 0.88f),
});
ConfigureNode(
supportEntryNode,
"support_entry",
"구호 적응",
"지원 축의 출발점으로, 회복 기반 능력을 높입니다.",
PassiveNodeBranch.Support,
PassiveNodeKind.Axis,
PassiveAxisMask.Support,
1,
1,
new Vector2(0.34f, -0.1f),
new[] { hubNode },
new[] { hubNode, supportCoreNode, defenseSupportBridgeNode, supportAttackBridgeNode },
new[]
{
PassiveEffectConfig.CreateStat(StatType.Wisdom, StatModType.PercentAdd, 0.2f),
});
ConfigureNode(
supportCoreNode,
"support_core",
"조율 숙련",
"회복과 보호막 부여 효율을 함께 강화합니다.",
PassiveNodeBranch.Support,
PassiveNodeKind.Axis,
PassiveAxisMask.Support,
2,
1,
new Vector2(0.58f, -0.34f),
new[] { supportEntryNode },
new[] { supportEntryNode, supportCapstoneNode },
new[]
{
PassiveEffectConfig.CreateScalar(PassiveEffectType.HealMultiplier, 1.15f, SkillRoleType.Support),
PassiveEffectConfig.CreateScalar(PassiveEffectType.ShieldDoneMultiplier, 1.15f, SkillRoleType.Support),
});
ConfigureNode(
supportCapstoneNode,
"support_capstone",
"마력 순환",
"최대 마나와 유지 효율을 함께 높입니다.",
PassiveNodeBranch.Support,
PassiveNodeKind.Capstone,
PassiveAxisMask.Support,
3,
2,
new Vector2(0.82f, -0.58f),
new[] { supportCoreNode },
new[] { supportCoreNode },
new[]
{
PassiveEffectConfig.CreateStat(StatType.Spirit, StatModType.PercentAdd, 0.2f),
PassiveEffectConfig.CreateScalar(PassiveEffectType.ManaCostMultiplier, 0.85f, SkillRoleType.Support),
});
ConfigureNode(
attackDefenseBridgeNode,
"attack_defense_bridge",
"압박 방벽",
"공격과 방어를 연결하는 브릿지로, 압박 유지력과 전투 안정성을 함께 챙깁니다.",
PassiveNodeBranch.Bridge,
PassiveNodeKind.Bridge,
PassiveAxisMask.Attack | PassiveAxisMask.Defense,
2,
1,
new Vector2(-0.24f, 0.14f),
new[] { attackEntryNode, defenseEntryNode },
new[] { attackEntryNode, defenseEntryNode },
new[]
{
PassiveEffectConfig.CreateScalar(PassiveEffectType.DamageMultiplier, 1.06f, SkillRoleType.Attack),
PassiveEffectConfig.CreateScalar(PassiveEffectType.IncomingDamageMultiplier, 0.95f),
});
ConfigureNode(
defenseSupportBridgeNode,
"defense_support_bridge",
"수호 순환",
"방어와 지원을 연결하는 브릿지로, 보호막과 회복 기여를 함께 끌어올립니다.",
PassiveNodeBranch.Bridge,
PassiveNodeKind.Bridge,
PassiveAxisMask.Defense | PassiveAxisMask.Support,
2,
1,
new Vector2(0f, -0.28f),
new[] { defenseEntryNode, supportEntryNode },
new[] { defenseEntryNode, supportEntryNode },
new[]
{
PassiveEffectConfig.CreateScalar(PassiveEffectType.ShieldReceivedMultiplier, 1.10f),
PassiveEffectConfig.CreateScalar(PassiveEffectType.HealMultiplier, 1.08f, SkillRoleType.Support),
});
ConfigureNode(
supportAttackBridgeNode,
"support_attack_bridge",
"전술 증폭",
"지원과 공격을 연결하는 브릿지로, 화력과 유지 효율을 함께 보조합니다.",
PassiveNodeBranch.Bridge,
PassiveNodeKind.Bridge,
PassiveAxisMask.Support | PassiveAxisMask.Attack,
2,
1,
new Vector2(0.24f, 0.14f),
new[] { supportEntryNode, attackEntryNode },
new[] { supportEntryNode, attackEntryNode },
new[]
{
PassiveEffectConfig.CreateScalar(PassiveEffectType.DamageMultiplier, 1.06f, SkillRoleType.Attack),
PassiveEffectConfig.CreateScalar(PassiveEffectType.ManaCostMultiplier, 0.95f),
});
PassiveTreeData tree = CreateOrLoadTree();
ConfigureTree(
tree,
"player_prototype_tree",
"플레이어 패시브 프로토타입",
"공격 / 방어 / 지원 3축과 브릿지 노드로 구성된 드로그전 밸런싱 검증용 트리입니다.",
8,
new[]
{
hubNode,
attackEntryNode,
attackCoreNode,
attackCapstoneNode,
defenseEntryNode,
defenseCoreNode,
defenseCapstoneNode,
supportEntryNode,
supportCoreNode,
supportCapstoneNode,
attackDefenseBridgeNode,
defenseSupportBridgeNode,
supportAttackBridgeNode,
});
CreateOrUpdatePreset(
NonePresetAssetPath,
"패시브 없음",
"비교 기준선 확보용 프리셋입니다.",
tree,
new[] { hubNode });
CreateOrUpdatePreset(
DefensePresetAssetPath,
"방어형 패시브",
"방어 축 완성과 함께 공격/지원 브릿지를 가볍게 여는 프리셋입니다.",
tree,
new[]
{
hubNode,
defenseEntryNode,
defenseCoreNode,
defenseCapstoneNode,
attackEntryNode,
supportEntryNode,
attackDefenseBridgeNode,
defenseSupportBridgeNode,
});
CreateOrUpdatePreset(
SupportPresetAssetPath,
"지원형 패시브",
"지원 축 완성과 함께 공격/방어 브릿지를 가볍게 여는 프리셋입니다.",
tree,
new[]
{
hubNode,
supportEntryNode,
supportCoreNode,
supportCapstoneNode,
defenseEntryNode,
attackEntryNode,
defenseSupportBridgeNode,
supportAttackBridgeNode,
});
CreateOrUpdatePreset(
AttackPresetAssetPath,
"공격형 패시브",
"공격 축 완성과 함께 방어/지원 브릿지를 가볍게 여는 프리셋입니다.",
tree,
new[]
{
hubNode,
attackEntryNode,
attackCoreNode,
attackCapstoneNode,
defenseEntryNode,
supportEntryNode,
attackDefenseBridgeNode,
supportAttackBridgeNode,
});
PassivePrototypeCatalogData catalog = CreateOrLoadCatalog();
ConfigureCatalog(
catalog,
tree,
AssetDatabase.LoadAssetAtPath<PassivePresetData>(NonePresetAssetPath),
AssetDatabase.LoadAssetAtPath<PassivePresetData>(DefensePresetAssetPath),
AssetDatabase.LoadAssetAtPath<PassivePresetData>(SupportPresetAssetPath),
AssetDatabase.LoadAssetAtPath<PassivePresetData>(AttackPresetAssetPath));
BindPrototypeCatalogToPrefabs(catalog);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log("[Passive] 프로토타입 패시브 에셋 생성을 완료했습니다.");
}
[MenuItem("Tools/Colosseum/Debug/Passive/Apply Local None")]
private static void ApplyLocalNone()
{
ApplyLocalPreset(NonePresetAssetPath, "패시브 없음");
}
[MenuItem("Tools/Colosseum/Debug/Passive/Apply Local Defense")]
private static void ApplyLocalDefense()
{
ApplyLocalPreset(DefensePresetAssetPath, "방어형 패시브");
}
[MenuItem("Tools/Colosseum/Debug/Passive/Apply Local Support")]
private static void ApplyLocalSupport()
{
ApplyLocalPreset(SupportPresetAssetPath, "지원형 패시브");
}
[MenuItem("Tools/Colosseum/Debug/Passive/Apply Local Attack")]
private static void ApplyLocalAttack()
{
ApplyLocalPreset(AttackPresetAssetPath, "공격형 패시브");
}
[MenuItem("Tools/Colosseum/Debug/Passive/Apply Owner Presets To All Players")]
private static void ApplyOwnerPresetsToAllPlayers()
{
if (!EditorApplication.isPlaying)
{
Debug.LogWarning("[Passive] 플레이 모드에서만 사용할 수 있습니다.");
return;
}
PlayerNetworkController[] players = Object.FindObjectsByType<PlayerNetworkController>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
if (players == null || players.Length == 0)
{
Debug.LogWarning("[Passive] PlayerNetworkController를 찾지 못했습니다.");
return;
}
int appliedCount = 0;
for (int i = 0; i < players.Length; i++)
{
PlayerNetworkController player = players[i];
if (player != null && player.TryApplyPrototypePassivePresetForOwner())
{
appliedCount++;
}
}
Debug.Log($"[Passive] 역할별 패시브 프리셋 적용 완료 | Applied={appliedCount}");
}
[MenuItem("Tools/Colosseum/Debug/Passive/Log Local Passive Summary")]
private static void LogLocalPassiveSummary()
{
if (!EditorApplication.isPlaying)
{
Debug.LogWarning("[Passive] 플레이 모드에서만 사용할 수 있습니다.");
return;
}
PlayerNetworkController localNetworkController = FindLocalNetworkController();
if (localNetworkController == null)
{
Debug.LogWarning("[Passive] 로컬 PlayerNetworkController를 찾지 못했습니다.");
return;
}
Debug.Log(localNetworkController.BuildPassiveSummary());
}
private static void ApplyLocalPreset(string presetPath, string label)
{
if (!EditorApplication.isPlaying)
{
Debug.LogWarning("[Passive] 플레이 모드에서만 사용할 수 있습니다.");
return;
}
PlayerNetworkController localNetworkController = FindLocalNetworkController();
if (localNetworkController == null)
{
Debug.LogWarning("[Passive] 로컬 PlayerNetworkController를 찾지 못했습니다.");
return;
}
PassivePresetData preset = AssetDatabase.LoadAssetAtPath<PassivePresetData>(presetPath);
if (preset == null)
{
Debug.LogWarning($"[Passive] 패시브 프리셋을 찾지 못했습니다: {presetPath}");
return;
}
if (!localNetworkController.DebugApplyPassivePreset(preset))
{
Debug.LogWarning($"[Passive] {label} 적용에 실패했습니다. 호스트/서버 플레이 모드인지 확인하세요.");
return;
}
Debug.Log($"[Passive] {label} 적용 완료");
}
private static PlayerNetworkController FindLocalNetworkController()
{
PlayerNetworkController[] players = Object.FindObjectsByType<PlayerNetworkController>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
for (int i = 0; i < players.Length; i++)
{
PlayerNetworkController player = players[i];
if (player != null && player.IsOwner)
return player;
}
return null;
}
private static void EnsureFolder(string parentFolder, string childFolderName)
{
string fullPath = $"{parentFolder}/{childFolderName}";
if (AssetDatabase.IsValidFolder(fullPath))
return;
AssetDatabase.CreateFolder(parentFolder, childFolderName);
}
private static PassiveTreeData CreateOrLoadTree()
{
PassiveTreeData tree = AssetDatabase.LoadAssetAtPath<PassiveTreeData>(PassiveTreeAssetPath);
if (tree != null)
return tree;
tree = ScriptableObject.CreateInstance<PassiveTreeData>();
AssetDatabase.CreateAsset(tree, PassiveTreeAssetPath);
return tree;
}
private static PassivePrototypeCatalogData CreateOrLoadCatalog()
{
PassivePrototypeCatalogData catalog = AssetDatabase.LoadAssetAtPath<PassivePrototypeCatalogData>(PassiveCatalogAssetPath);
if (catalog != null)
return catalog;
catalog = ScriptableObject.CreateInstance<PassivePrototypeCatalogData>();
AssetDatabase.CreateAsset(catalog, PassiveCatalogAssetPath);
return catalog;
}
private static PassiveNodeData CreateOrLoadNode(string assetPath)
{
PassiveNodeData node = AssetDatabase.LoadAssetAtPath<PassiveNodeData>(assetPath);
if (node != null)
return node;
node = ScriptableObject.CreateInstance<PassiveNodeData>();
AssetDatabase.CreateAsset(node, assetPath);
return node;
}
private static void ConfigureTree(
PassiveTreeData tree,
string treeId,
string treeName,
string description,
int initialPoints,
IReadOnlyList<PassiveNodeData> nodes)
{
SerializedObject serializedTree = new SerializedObject(tree);
serializedTree.FindProperty("treeId").stringValue = treeId;
serializedTree.FindProperty("treeName").stringValue = treeName;
serializedTree.FindProperty("description").stringValue = description;
serializedTree.FindProperty("initialPoints").intValue = initialPoints;
SerializedProperty nodeProperty = serializedTree.FindProperty("nodes");
nodeProperty.arraySize = nodes != null ? nodes.Count : 0;
for (int i = 0; i < nodeProperty.arraySize; i++)
{
nodeProperty.GetArrayElementAtIndex(i).objectReferenceValue = nodes[i];
}
serializedTree.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(tree);
}
private static void ConfigureNode(
PassiveNodeData node,
string nodeId,
string displayName,
string description,
PassiveNodeBranch branch,
PassiveNodeKind nodeKind,
PassiveAxisMask axisMask,
int tier,
int cost,
Vector2 layoutPosition,
IReadOnlyList<PassiveNodeData> prerequisiteNodes,
IReadOnlyList<PassiveNodeData> connectedNodes,
IReadOnlyList<PassiveEffectConfig> effects)
{
SerializedObject serializedNode = new SerializedObject(node);
serializedNode.FindProperty("nodeId").stringValue = nodeId;
serializedNode.FindProperty("displayName").stringValue = displayName;
serializedNode.FindProperty("description").stringValue = description;
serializedNode.FindProperty("branch").enumValueIndex = (int)branch;
serializedNode.FindProperty("nodeKind").enumValueIndex = (int)nodeKind;
serializedNode.FindProperty("axisMask").intValue = (int)axisMask;
serializedNode.FindProperty("tier").intValue = tier;
serializedNode.FindProperty("cost").intValue = cost;
serializedNode.FindProperty("layoutPosition").vector2Value = layoutPosition;
SerializedProperty prerequisiteProperty = serializedNode.FindProperty("prerequisiteNodes");
prerequisiteProperty.arraySize = prerequisiteNodes != null ? prerequisiteNodes.Count : 0;
for (int i = 0; i < prerequisiteProperty.arraySize; i++)
{
prerequisiteProperty.GetArrayElementAtIndex(i).objectReferenceValue = prerequisiteNodes[i];
}
SerializedProperty connectedProperty = serializedNode.FindProperty("connectedNodes");
connectedProperty.arraySize = connectedNodes != null ? connectedNodes.Count : 0;
for (int i = 0; i < connectedProperty.arraySize; i++)
{
connectedProperty.GetArrayElementAtIndex(i).objectReferenceValue = connectedNodes[i];
}
SerializedProperty effectProperty = serializedNode.FindProperty("effects");
effectProperty.arraySize = effects != null ? effects.Count : 0;
for (int i = 0; i < effectProperty.arraySize; i++)
{
SerializedProperty effectEntry = effectProperty.GetArrayElementAtIndex(i);
PassiveEffectConfig effectConfig = effects[i];
effectEntry.FindPropertyRelative("effectType").enumValueIndex = (int)effectConfig.EffectType;
effectEntry.FindPropertyRelative("statType").enumValueIndex = (int)effectConfig.StatType;
effectEntry.FindPropertyRelative("modType").enumValueIndex = (int)effectConfig.ModType;
effectEntry.FindPropertyRelative("value").floatValue = effectConfig.Value;
effectEntry.FindPropertyRelative("skillRoleMask").intValue = (int)effectConfig.SkillRoleMask;
}
serializedNode.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(node);
}
private static void CreateOrUpdatePreset(
string assetPath,
string presetName,
string description,
PassiveTreeData tree,
IReadOnlyList<PassiveNodeData> selectedNodes)
{
PassivePresetData preset = AssetDatabase.LoadAssetAtPath<PassivePresetData>(assetPath);
if (preset == null)
{
preset = ScriptableObject.CreateInstance<PassivePresetData>();
AssetDatabase.CreateAsset(preset, assetPath);
}
SerializedObject serializedPreset = new SerializedObject(preset);
serializedPreset.FindProperty("presetName").stringValue = presetName;
serializedPreset.FindProperty("description").stringValue = description;
serializedPreset.FindProperty("tree").objectReferenceValue = tree;
SerializedProperty selectedNodesProperty = serializedPreset.FindProperty("selectedNodes");
selectedNodesProperty.arraySize = selectedNodes != null ? selectedNodes.Count : 0;
for (int i = 0; i < selectedNodesProperty.arraySize; i++)
{
selectedNodesProperty.GetArrayElementAtIndex(i).objectReferenceValue = selectedNodes[i];
}
serializedPreset.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(preset);
}
private static void ConfigureCatalog(
PassivePrototypeCatalogData catalog,
PassiveTreeData tree,
PassivePresetData nonePreset,
PassivePresetData defensePreset,
PassivePresetData supportPreset,
PassivePresetData attackPreset)
{
if (catalog == null)
return;
SerializedObject serializedCatalog = new SerializedObject(catalog);
serializedCatalog.FindProperty("prototypeTree").objectReferenceValue = tree;
serializedCatalog.FindProperty("nonePreset").objectReferenceValue = nonePreset;
serializedCatalog.FindProperty("defensePreset").objectReferenceValue = defensePreset;
serializedCatalog.FindProperty("supportPreset").objectReferenceValue = supportPreset;
serializedCatalog.FindProperty("attackPreset").objectReferenceValue = attackPreset;
serializedCatalog.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(catalog);
}
private static void BindPrototypeCatalogToPrefabs(PassivePrototypeCatalogData catalog)
{
BindCatalogToPlayerPrefab(catalog);
BindCatalogToPlayerResourcesPrefab(catalog);
}
private static void BindCatalogToPlayerPrefab(PassivePrototypeCatalogData catalog)
{
GameObject root = PrefabUtility.LoadPrefabContents(PlayerPrefabPath);
try
{
PlayerNetworkController controller = root.GetComponent<PlayerNetworkController>();
if (controller == null)
return;
SerializedObject serializedController = new SerializedObject(controller);
serializedController.FindProperty("passivePrototypeCatalog").objectReferenceValue = catalog;
if (serializedController.FindProperty("passiveTree").objectReferenceValue == null)
{
serializedController.FindProperty("passiveTree").objectReferenceValue = catalog != null ? catalog.PrototypeTree : null;
}
serializedController.ApplyModifiedPropertiesWithoutUndo();
PrefabUtility.SaveAsPrefabAsset(root, PlayerPrefabPath);
}
finally
{
PrefabUtility.UnloadPrefabContents(root);
}
}
private static void BindCatalogToPlayerResourcesPrefab(PassivePrototypeCatalogData catalog)
{
GameObject root = PrefabUtility.LoadPrefabContents(PlayerResourcesPrefabPath);
try
{
PassiveTreeUI passiveTreeUi = root.GetComponent<PassiveTreeUI>();
if (passiveTreeUi == null)
return;
SerializedObject serializedPassiveTreeUi = new SerializedObject(passiveTreeUi);
serializedPassiveTreeUi.FindProperty("passivePrototypeCatalog").objectReferenceValue = catalog;
serializedPassiveTreeUi.ApplyModifiedPropertiesWithoutUndo();
PrefabUtility.SaveAsPrefabAsset(root, PlayerResourcesPrefabPath);
}
finally
{
PrefabUtility.UnloadPrefabContents(root);
}
}
private readonly struct PassiveEffectConfig
{
public PassiveEffectConfig(
PassiveEffectType effectType,
StatType statType,
StatModType modType,
float value,
SkillRoleType skillRoleMask)
{
EffectType = effectType;
StatType = statType;
ModType = modType;
Value = value;
SkillRoleMask = skillRoleMask;
}
public PassiveEffectType EffectType { get; }
public StatType StatType { get; }
public StatModType ModType { get; }
public float Value { get; }
public SkillRoleType SkillRoleMask { get; }
public static PassiveEffectConfig CreateScalar(
PassiveEffectType effectType,
float value,
SkillRoleType skillRoleMask = SkillRoleType.All)
{
return new PassiveEffectConfig(effectType, StatType.Vitality, StatModType.Flat, value, skillRoleMask);
}
public static PassiveEffectConfig CreateStat(
StatType statType,
StatModType modType,
float value)
{
return new PassiveEffectConfig(PassiveEffectType.StatModifier, statType, modType, value, SkillRoleType.All);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9934975f568477d4191a9bcbcfd01f0a