feat: 패시브 트리 레이아웃 자동화 및 UI 정리

- 패시브 트리 노드 배치를 삼각형 기반 자동 배치 구조로 전환하고 축 및 브릿지 반경을 재정리\n- 패시브 UI 프리팹과 런타임 렌더링을 수정해 노드 겹침, 링크 관통, 상태별 간격 변화, 하단 여백 문제를 정리\n- 프로토타입 패시브 노드, 트리, 프리셋 자산을 재생성해 최신 레이아웃과 확장 노드 구성을 반영
This commit is contained in:
2026-03-27 04:43:14 +09:00
parent 786a38d72d
commit d78e0edabd
45 changed files with 5228 additions and 6276 deletions

View File

@@ -35,6 +35,7 @@ namespace Colosseum.Editor
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 NodeFillColor = new(0.08f, 0.08f, 0.10f, 0.98f);
private static readonly Color NodeOutlineColor = new(0f, 0f, 0f, 0.45f);
[MenuItem("Tools/Colosseum/Passives/Rebuild Passive UI Prefabs")]
@@ -71,7 +72,7 @@ namespace Colosseum.Editor
try
{
RectTransform rect = root.GetComponent<RectTransform>();
rect.sizeDelta = new Vector2(70f, 70f);
rect.sizeDelta = new Vector2(62f, 62f);
Image background = root.GetComponent<Image>();
background.sprite = LoadPassiveNodeSprite(false);
@@ -86,11 +87,23 @@ namespace Colosseum.Editor
Button button = root.GetComponent<Button>();
button.targetGraphic = background;
GameObject fillObject = CreateUiObject("Fill", root.transform, typeof(RectTransform), typeof(CanvasRenderer), typeof(Image));
RectTransform fillRect = fillObject.GetComponent<RectTransform>();
StretchRect(fillRect);
fillRect.offsetMin = new Vector2(17f, 17f);
fillRect.offsetMax = new Vector2(-17f, -17f);
Image fillImage = fillObject.GetComponent<Image>();
fillImage.sprite = GetBuiltinSprite();
fillImage.type = Image.Type.Sliced;
fillImage.color = NodeFillColor;
fillImage.raycastTarget = false;
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);
innerRect.offsetMin = new Vector2(9f, 9f);
innerRect.offsetMax = new Vector2(-9f, -9f);
Image innerImage = innerObject.GetComponent<Image>();
innerImage.sprite = LoadPassiveNodeSprite(false);
@@ -102,6 +115,7 @@ namespace Colosseum.Editor
PassiveTreeNodeView references = root.GetComponent<PassiveTreeNodeView>();
SetSerializedReference(references, "rootRect", rect);
SetSerializedReference(references, "backgroundImage", background);
SetSerializedReference(references, "fillImage", fillImage);
SetSerializedReference(references, "innerImage", innerImage);
SetSerializedReference(references, "button", button);
SetSerializedReference(references, "outline", outline);
@@ -143,7 +157,7 @@ namespace Colosseum.Editor
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);
panelRect.sizeDelta = new Vector2(1440f, 900f);
Image panelImage = panelObject.GetComponent<Image>();
panelImage.sprite = GetBuiltinSprite();
@@ -308,14 +322,14 @@ namespace Colosseum.Editor
RectTransform graphSection = CreateSectionRoot(bodyObject.transform, "Section_Graph", 0f);
graphSection.GetComponent<LayoutElement>().flexibleWidth = 1f;
graphSection.GetComponent<LayoutElement>().minWidth = 760f;
graphSection.GetComponent<LayoutElement>().minWidth = 820f;
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;
graphLayout.minHeight = 640f;
Image graphImage = graphSurface.GetComponent<Image>();
graphImage.sprite = GetBuiltinSprite();
@@ -385,7 +399,7 @@ namespace Colosseum.Editor
statusText.text = string.Empty;
statusText.textWrappingMode = TextWrappingModes.NoWrap;
statusText.overflowMode = TextOverflowModes.Ellipsis;
footerObject.SetActive(false);
footerObject.SetActive(true);
}
private static RectTransform CreateSectionRoot(Transform parent, string name, float preferredWidth)

View File

@@ -32,18 +32,35 @@ namespace Colosseum.Editor
private const string HubNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Hub.asset";
private const string DefenseEntryNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Tank_Entry.asset";
private const string DefenseFrameNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Tank_Frame.asset";
private const string DefenseGuardNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Tank_Guard.asset";
private const string DefenseResolveNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Tank_Resolve.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 SupportInsightNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Support_Insight.asset";
private const string SupportWellNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Support_Well.asset";
private const string SupportFocusNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Support_Focus.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 AttackStrengthNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Dps_Strength.asset";
private const string AttackPrecisionNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Dps_Precision.asset";
private const string AttackArcanaNodeAssetPath = PassiveNodeFolderPath + "/Data_PassiveNode_Player_Dps_Arcana.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";
private const float AxisEntryRadius = 0.24f;
private const float AxisCoreRadius = 0.56f;
private const float AxisOuterStatRadius = 0.88f;
private const float AxisCenterStatRadius = 0.78f;
private const float AxisCapstoneRadius = 1.0f;
private const float AxisStatSideOffset = 0.32f;
private const float BridgeRadius = 0.70f;
[MenuItem("Tools/Colosseum/Debug/Passive/Bootstrap Prototype Assets")]
private static void BootstrapPrototypeAssets()
{
@@ -54,23 +71,61 @@ namespace Colosseum.Editor
PassiveNodeData hubNode = CreateOrLoadNode(HubNodeAssetPath);
PassiveNodeData defenseEntryNode = CreateOrLoadNode(DefenseEntryNodeAssetPath);
PassiveNodeData defenseFrameNode = CreateOrLoadNode(DefenseFrameNodeAssetPath);
PassiveNodeData defenseGuardNode = CreateOrLoadNode(DefenseGuardNodeAssetPath);
PassiveNodeData defenseResolveNode = CreateOrLoadNode(DefenseResolveNodeAssetPath);
PassiveNodeData defenseCoreNode = CreateOrLoadNode(DefenseCoreNodeAssetPath);
PassiveNodeData defenseCapstoneNode = CreateOrLoadNode(DefenseCapstoneNodeAssetPath);
PassiveNodeData supportEntryNode = CreateOrLoadNode(SupportEntryNodeAssetPath);
PassiveNodeData supportInsightNode = CreateOrLoadNode(SupportInsightNodeAssetPath);
PassiveNodeData supportWellNode = CreateOrLoadNode(SupportWellNodeAssetPath);
PassiveNodeData supportFocusNode = CreateOrLoadNode(SupportFocusNodeAssetPath);
PassiveNodeData supportCoreNode = CreateOrLoadNode(SupportCoreNodeAssetPath);
PassiveNodeData supportCapstoneNode = CreateOrLoadNode(SupportCapstoneNodeAssetPath);
PassiveNodeData attackEntryNode = CreateOrLoadNode(AttackEntryNodeAssetPath);
PassiveNodeData attackStrengthNode = CreateOrLoadNode(AttackStrengthNodeAssetPath);
PassiveNodeData attackPrecisionNode = CreateOrLoadNode(AttackPrecisionNodeAssetPath);
PassiveNodeData attackArcanaNode = CreateOrLoadNode(AttackArcanaNodeAssetPath);
PassiveNodeData attackCoreNode = CreateOrLoadNode(AttackCoreNodeAssetPath);
PassiveNodeData attackCapstoneNode = CreateOrLoadNode(AttackCapstoneNodeAssetPath);
PassiveNodeData attackDefenseBridgeNode = CreateOrLoadNode(AttackDefenseBridgeNodeAssetPath);
PassiveNodeData defenseSupportBridgeNode = CreateOrLoadNode(DefenseSupportBridgeNodeAssetPath);
PassiveNodeData supportAttackBridgeNode = CreateOrLoadNode(SupportAttackBridgeNodeAssetPath);
Vector2 attackDirection = Vector2.up;
Vector2 defenseDirection = RotateLayout(Vector2.up, 120f);
Vector2 supportDirection = RotateLayout(Vector2.up, -120f);
Vector2 attackEntryPosition = BuildAxisLayoutPosition(attackDirection, AxisEntryRadius);
Vector2 attackCorePosition = BuildAxisLayoutPosition(attackDirection, AxisCoreRadius);
Vector2 attackStrengthPosition = BuildAxisLayoutPosition(attackDirection, AxisOuterStatRadius, AxisStatSideOffset);
Vector2 attackPrecisionPosition = BuildAxisLayoutPosition(attackDirection, AxisCenterStatRadius);
Vector2 attackArcanaPosition = BuildAxisLayoutPosition(attackDirection, AxisOuterStatRadius, -AxisStatSideOffset);
Vector2 attackCapstonePosition = BuildAxisLayoutPosition(attackDirection, AxisCapstoneRadius);
Vector2 defenseEntryPosition = BuildAxisLayoutPosition(defenseDirection, AxisEntryRadius);
Vector2 defenseCorePosition = BuildAxisLayoutPosition(defenseDirection, AxisCoreRadius);
Vector2 defenseFramePosition = BuildAxisLayoutPosition(defenseDirection, AxisOuterStatRadius, AxisStatSideOffset);
Vector2 defenseResolvePosition = BuildAxisLayoutPosition(defenseDirection, AxisCenterStatRadius);
Vector2 defenseGuardPosition = BuildAxisLayoutPosition(defenseDirection, AxisOuterStatRadius, -AxisStatSideOffset);
Vector2 defenseCapstonePosition = BuildAxisLayoutPosition(defenseDirection, AxisCapstoneRadius);
Vector2 supportEntryPosition = BuildAxisLayoutPosition(supportDirection, AxisEntryRadius);
Vector2 supportCorePosition = BuildAxisLayoutPosition(supportDirection, AxisCoreRadius);
Vector2 supportInsightPosition = BuildAxisLayoutPosition(supportDirection, AxisOuterStatRadius, AxisStatSideOffset);
Vector2 supportWellPosition = BuildAxisLayoutPosition(supportDirection, AxisCenterStatRadius);
Vector2 supportFocusPosition = BuildAxisLayoutPosition(supportDirection, AxisOuterStatRadius, -AxisStatSideOffset);
Vector2 supportCapstonePosition = BuildAxisLayoutPosition(supportDirection, AxisCapstoneRadius);
Vector2 attackDefenseBridgePosition = RotateLayout(Vector2.up * BridgeRadius, 60f);
Vector2 defenseSupportBridgePosition = RotateLayout(Vector2.up * BridgeRadius, 180f);
Vector2 supportAttackBridgePosition = RotateLayout(Vector2.up * BridgeRadius, -60f);
ConfigureNode(
hubNode,
"hub",
"중심 허브",
"패시브 트리의 시작점입니다.",
string.Empty,
string.Empty,
PassiveNodeBranch.Common,
PassiveNodeKind.Hub,
PassiveAxisMask.None,
@@ -91,14 +146,68 @@ namespace Colosseum.Editor
PassiveAxisMask.Attack,
1,
1,
new Vector2(0f, 0.34f),
attackEntryPosition,
new[] { hubNode },
new[] { hubNode, attackCoreNode, attackDefenseBridgeNode, supportAttackBridgeNode },
new[] { hubNode, attackCoreNode },
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),
PassiveEffectConfig.CreateStat(StatType.Strength, StatModType.PercentAdd, 0.05f),
PassiveEffectConfig.CreateStat(StatType.Dexterity, StatModType.PercentAdd, 0.05f),
PassiveEffectConfig.CreateStat(StatType.Intelligence, StatModType.PercentAdd, 0.05f),
});
ConfigureNode(
attackStrengthNode,
"attack_strength",
"근력 단련",
"직접적인 무기 화력을 높이기 위한 힘의 기반을 다집니다.",
PassiveNodeBranch.Attack,
PassiveNodeKind.Axis,
PassiveAxisMask.Attack,
2,
1,
attackStrengthPosition,
new[] { attackCoreNode },
new[] { attackCoreNode },
new[]
{
PassiveEffectConfig.CreateStat(StatType.Strength, StatModType.Flat, 4f),
});
ConfigureNode(
attackPrecisionNode,
"attack_precision",
"민첩 연마",
"정확한 움직임과 타격 빈도를 위해 민첩을 끌어올립니다.",
PassiveNodeBranch.Attack,
PassiveNodeKind.Axis,
PassiveAxisMask.Attack,
2,
1,
attackPrecisionPosition,
new[] { attackCoreNode },
new[] { attackCoreNode },
new[]
{
PassiveEffectConfig.CreateStat(StatType.Dexterity, StatModType.Flat, 4f),
});
ConfigureNode(
attackArcanaNode,
"attack_arcana",
"지능 예열",
"공세 축에서도 주문 계열 화력을 포기하지 않도록 지능 기반을 보강합니다.",
PassiveNodeBranch.Attack,
PassiveNodeKind.Axis,
PassiveAxisMask.Attack,
2,
1,
attackArcanaPosition,
new[] { attackCoreNode },
new[] { attackCoreNode },
new[]
{
PassiveEffectConfig.CreateStat(StatType.Intelligence, StatModType.Flat, 4f),
});
ConfigureNode(
@@ -109,14 +218,14 @@ namespace Colosseum.Editor
PassiveNodeBranch.Attack,
PassiveNodeKind.Axis,
PassiveAxisMask.Attack,
2,
3,
1,
new Vector2(0f, 0.6f),
attackCorePosition,
new[] { attackEntryNode },
new[] { attackEntryNode, attackCapstoneNode },
new[] { attackEntryNode, attackStrengthNode, attackPrecisionNode, attackArcanaNode, attackCapstoneNode, attackDefenseBridgeNode, supportAttackBridgeNode },
new[]
{
PassiveEffectConfig.CreateScalar(PassiveEffectType.DamageMultiplier, 1.12f, SkillRoleType.Attack),
PassiveEffectConfig.CreateScalar(PassiveEffectType.DamageMultiplier, 1.10f, SkillRoleType.Attack),
});
ConfigureNode(
@@ -127,15 +236,15 @@ namespace Colosseum.Editor
PassiveNodeBranch.Attack,
PassiveNodeKind.Capstone,
PassiveAxisMask.Attack,
3,
4,
2,
new Vector2(0f, 0.84f),
attackCapstonePosition,
new[] { attackCoreNode },
new[] { attackCoreNode },
new[]
{
PassiveEffectConfig.CreateScalar(PassiveEffectType.DamageMultiplier, 1.10f, SkillRoleType.Attack),
PassiveEffectConfig.CreateScalar(PassiveEffectType.ManaCostMultiplier, 0.90f, SkillRoleType.Attack),
PassiveEffectConfig.CreateScalar(PassiveEffectType.DamageMultiplier, 1.08f, SkillRoleType.Attack),
PassiveEffectConfig.CreateScalar(PassiveEffectType.ManaCostMultiplier, 0.92f, SkillRoleType.Attack),
});
ConfigureNode(
@@ -148,12 +257,66 @@ namespace Colosseum.Editor
PassiveAxisMask.Defense,
1,
1,
new Vector2(-0.34f, -0.1f),
defenseEntryPosition,
new[] { hubNode },
new[] { hubNode, defenseCoreNode, attackDefenseBridgeNode, defenseSupportBridgeNode },
new[] { hubNode, defenseCoreNode },
new[]
{
PassiveEffectConfig.CreateStat(StatType.Vitality, StatModType.PercentAdd, 0.2f),
PassiveEffectConfig.CreateStat(StatType.Vitality, StatModType.PercentAdd, 0.10f),
});
ConfigureNode(
defenseFrameNode,
"defense_frame",
"활력 축적",
"방어 축의 기본 체력을 두텁게 확보합니다.",
PassiveNodeBranch.Defense,
PassiveNodeKind.Axis,
PassiveAxisMask.Defense,
2,
1,
defenseFramePosition,
new[] { defenseCoreNode },
new[] { defenseCoreNode },
new[]
{
PassiveEffectConfig.CreateStat(StatType.Vitality, StatModType.Flat, 6f),
});
ConfigureNode(
defenseGuardNode,
"defense_guard",
"굳센 육체",
"기본 활력을 비율로 증폭해 전투 지속력을 높입니다.",
PassiveNodeBranch.Defense,
PassiveNodeKind.Axis,
PassiveAxisMask.Defense,
2,
1,
defenseGuardPosition,
new[] { defenseCoreNode },
new[] { defenseCoreNode },
new[]
{
PassiveEffectConfig.CreateStat(StatType.Vitality, StatModType.PercentAdd, 0.12f),
});
ConfigureNode(
defenseResolveNode,
"defense_resolve",
"전투 호흡",
"전열에서 스킬을 굴릴 여유를 위해 정신을 함께 보강합니다.",
PassiveNodeBranch.Defense,
PassiveNodeKind.Axis,
PassiveAxisMask.Defense,
2,
1,
defenseResolvePosition,
new[] { defenseCoreNode },
new[] { defenseCoreNode },
new[]
{
PassiveEffectConfig.CreateStat(StatType.Spirit, StatModType.Flat, 4f),
});
ConfigureNode(
@@ -164,15 +327,15 @@ namespace Colosseum.Editor
PassiveNodeBranch.Defense,
PassiveNodeKind.Axis,
PassiveAxisMask.Defense,
2,
3,
1,
new Vector2(-0.58f, -0.34f),
defenseCorePosition,
new[] { defenseEntryNode },
new[] { defenseEntryNode, defenseCapstoneNode },
new[] { defenseEntryNode, defenseFrameNode, defenseGuardNode, defenseResolveNode, defenseCapstoneNode, attackDefenseBridgeNode, defenseSupportBridgeNode },
new[]
{
PassiveEffectConfig.CreateScalar(PassiveEffectType.ThreatGeneratedMultiplier, 1.30f),
PassiveEffectConfig.CreateScalar(PassiveEffectType.ShieldReceivedMultiplier, 1.15f),
PassiveEffectConfig.CreateScalar(PassiveEffectType.ThreatGeneratedMultiplier, 1.25f),
PassiveEffectConfig.CreateScalar(PassiveEffectType.ShieldReceivedMultiplier, 1.10f),
});
ConfigureNode(
@@ -183,14 +346,14 @@ namespace Colosseum.Editor
PassiveNodeBranch.Defense,
PassiveNodeKind.Capstone,
PassiveAxisMask.Defense,
3,
4,
2,
new Vector2(-0.82f, -0.58f),
defenseCapstonePosition,
new[] { defenseCoreNode },
new[] { defenseCoreNode },
new[]
{
PassiveEffectConfig.CreateScalar(PassiveEffectType.IncomingDamageMultiplier, 0.88f),
PassiveEffectConfig.CreateScalar(PassiveEffectType.IncomingDamageMultiplier, 0.90f),
});
ConfigureNode(
@@ -203,12 +366,66 @@ namespace Colosseum.Editor
PassiveAxisMask.Support,
1,
1,
new Vector2(0.34f, -0.1f),
supportEntryPosition,
new[] { hubNode },
new[] { hubNode, supportCoreNode, defenseSupportBridgeNode, supportAttackBridgeNode },
new[] { hubNode, supportCoreNode },
new[]
{
PassiveEffectConfig.CreateStat(StatType.Wisdom, StatModType.PercentAdd, 0.2f),
PassiveEffectConfig.CreateStat(StatType.Wisdom, StatModType.PercentAdd, 0.10f),
});
ConfigureNode(
supportInsightNode,
"support_insight",
"지혜 응축",
"회복량의 기초가 되는 지혜를 직접 보강합니다.",
PassiveNodeBranch.Support,
PassiveNodeKind.Axis,
PassiveAxisMask.Support,
2,
1,
supportInsightPosition,
new[] { supportCoreNode },
new[] { supportCoreNode },
new[]
{
PassiveEffectConfig.CreateStat(StatType.Wisdom, StatModType.Flat, 4f),
});
ConfigureNode(
supportWellNode,
"support_well",
"정신 샘",
"지원 축 운용을 위해 마나 기반을 단단히 합니다.",
PassiveNodeBranch.Support,
PassiveNodeKind.Axis,
PassiveAxisMask.Support,
2,
1,
supportWellPosition,
new[] { supportCoreNode },
new[] { supportCoreNode },
new[]
{
PassiveEffectConfig.CreateStat(StatType.Spirit, StatModType.Flat, 6f),
});
ConfigureNode(
supportFocusNode,
"support_focus",
"집중 순환",
"지속적인 보조를 위해 정신을 비율로 확장합니다.",
PassiveNodeBranch.Support,
PassiveNodeKind.Axis,
PassiveAxisMask.Support,
2,
1,
supportFocusPosition,
new[] { supportCoreNode },
new[] { supportCoreNode },
new[]
{
PassiveEffectConfig.CreateStat(StatType.Spirit, StatModType.PercentAdd, 0.12f),
});
ConfigureNode(
@@ -219,15 +436,15 @@ namespace Colosseum.Editor
PassiveNodeBranch.Support,
PassiveNodeKind.Axis,
PassiveAxisMask.Support,
2,
3,
1,
new Vector2(0.58f, -0.34f),
supportCorePosition,
new[] { supportEntryNode },
new[] { supportEntryNode, supportCapstoneNode },
new[] { supportEntryNode, supportInsightNode, supportWellNode, supportFocusNode, supportCapstoneNode, defenseSupportBridgeNode, supportAttackBridgeNode },
new[]
{
PassiveEffectConfig.CreateScalar(PassiveEffectType.HealMultiplier, 1.15f, SkillRoleType.Support),
PassiveEffectConfig.CreateScalar(PassiveEffectType.ShieldDoneMultiplier, 1.15f, SkillRoleType.Support),
PassiveEffectConfig.CreateScalar(PassiveEffectType.HealMultiplier, 1.12f, SkillRoleType.Support),
PassiveEffectConfig.CreateScalar(PassiveEffectType.ShieldDoneMultiplier, 1.12f, SkillRoleType.Support),
});
ConfigureNode(
@@ -238,72 +455,72 @@ namespace Colosseum.Editor
PassiveNodeBranch.Support,
PassiveNodeKind.Capstone,
PassiveAxisMask.Support,
3,
4,
2,
new Vector2(0.82f, -0.58f),
supportCapstonePosition,
new[] { supportCoreNode },
new[] { supportCoreNode },
new[]
{
PassiveEffectConfig.CreateStat(StatType.Spirit, StatModType.PercentAdd, 0.2f),
PassiveEffectConfig.CreateScalar(PassiveEffectType.ManaCostMultiplier, 0.85f, SkillRoleType.Support),
PassiveEffectConfig.CreateScalar(PassiveEffectType.ManaCostMultiplier, 0.88f, SkillRoleType.Support),
PassiveEffectConfig.CreateScalar(PassiveEffectType.ShieldDoneMultiplier, 1.08f, 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 },
attackDefenseBridgePosition,
new[] { attackCoreNode },
new[] { attackCoreNode, defenseCoreNode },
new[]
{
PassiveEffectConfig.CreateScalar(PassiveEffectType.DamageMultiplier, 1.06f, SkillRoleType.Attack),
PassiveEffectConfig.CreateScalar(PassiveEffectType.IncomingDamageMultiplier, 0.95f),
PassiveEffectConfig.CreateStat(StatType.Strength, StatModType.Flat, 3f),
PassiveEffectConfig.CreateStat(StatType.Vitality, StatModType.Flat, 3f),
});
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 },
defenseSupportBridgePosition,
new[] { defenseCoreNode },
new[] { defenseCoreNode, supportCoreNode },
new[]
{
PassiveEffectConfig.CreateScalar(PassiveEffectType.ShieldReceivedMultiplier, 1.10f),
PassiveEffectConfig.CreateScalar(PassiveEffectType.HealMultiplier, 1.08f, SkillRoleType.Support),
PassiveEffectConfig.CreateStat(StatType.Vitality, StatModType.Flat, 3f),
PassiveEffectConfig.CreateStat(StatType.Wisdom, StatModType.Flat, 3f),
});
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 },
supportAttackBridgePosition,
new[] { supportCoreNode },
new[] { supportCoreNode, attackCoreNode },
new[]
{
PassiveEffectConfig.CreateScalar(PassiveEffectType.DamageMultiplier, 1.06f, SkillRoleType.Attack),
PassiveEffectConfig.CreateScalar(PassiveEffectType.ManaCostMultiplier, 0.95f),
PassiveEffectConfig.CreateStat(StatType.Wisdom, StatModType.Flat, 3f),
PassiveEffectConfig.CreateStat(StatType.Intelligence, StatModType.Flat, 3f),
});
PassiveTreeData tree = CreateOrLoadTree();
@@ -311,18 +528,27 @@ namespace Colosseum.Editor
tree,
"player_prototype_tree",
"플레이어 패시브 프로토타입",
"공격 / 방어 / 지원 3축과 브릿지 노드로 구성된 드로그전 밸런싱 검증용 트리입니다.",
8,
"공격 / 방어 / 지원 3축에 다수의 스탯 노드와 연결 노드를 배치한 드로그전 밸런싱 검증용 트리입니다.",
12,
new[]
{
hubNode,
attackEntryNode,
attackStrengthNode,
attackPrecisionNode,
attackArcanaNode,
attackCoreNode,
attackCapstoneNode,
defenseEntryNode,
defenseFrameNode,
defenseGuardNode,
defenseResolveNode,
defenseCoreNode,
defenseCapstoneNode,
supportEntryNode,
supportInsightNode,
supportWellNode,
supportFocusNode,
supportCoreNode,
supportCapstoneNode,
attackDefenseBridgeNode,
@@ -340,52 +566,64 @@ namespace Colosseum.Editor
CreateOrUpdatePreset(
DefensePresetAssetPath,
"방어형 패시브",
"방어 축 완성과 함께 공격/지원 브릿지를 가볍게 여는 프리셋입니다.",
"방어 축의 스탯 노드와 핵심 유지 노드를 우선 확보하는 프리셋입니다.",
tree,
new[]
{
hubNode,
defenseEntryNode,
defenseCoreNode,
defenseFrameNode,
defenseGuardNode,
defenseResolveNode,
defenseCapstoneNode,
attackEntryNode,
attackCoreNode,
supportEntryNode,
attackDefenseBridgeNode,
defenseSupportBridgeNode,
supportCoreNode,
});
CreateOrUpdatePreset(
SupportPresetAssetPath,
"지원형 패시브",
"지원 축 완성과 함께 공격/방어 브릿지를 가볍게 여는 프리셋입니다.",
"지원 축의 스탯 노드와 유지 효율 노드를 우선 확보하는 프리셋입니다.",
tree,
new[]
{
hubNode,
supportEntryNode,
supportCoreNode,
supportInsightNode,
supportWellNode,
supportFocusNode,
supportCapstoneNode,
defenseEntryNode,
defenseCoreNode,
attackEntryNode,
defenseSupportBridgeNode,
supportAttackBridgeNode,
attackCoreNode,
});
CreateOrUpdatePreset(
AttackPresetAssetPath,
"공격형 패시브",
"공격 축 완성과 함께 방어/지원 브릿지를 가볍게 여는 프리셋입니다.",
"공격 축의 스탯 노드와 화력 노드를 우선 확보하는 프리셋입니다.",
tree,
new[]
{
hubNode,
attackEntryNode,
attackCoreNode,
attackStrengthNode,
attackPrecisionNode,
attackArcanaNode,
attackCapstoneNode,
defenseEntryNode,
defenseCoreNode,
supportEntryNode,
attackDefenseBridgeNode,
supportAttackBridgeNode,
supportCoreNode,
});
PassivePrototypeCatalogData catalog = CreateOrLoadCatalog();
@@ -643,6 +881,29 @@ namespace Colosseum.Editor
EditorUtility.SetDirty(node);
}
/// <summary>
/// 기준 축 템플릿 좌표를 실제 축 방향으로 회전합니다.
/// </summary>
private static Vector2 BuildAxisLayoutPosition(Vector2 axisDirection, float radialDistance, float lateralOffset = 0f)
{
Vector2 normalizedDirection = axisDirection.normalized;
Vector2 perpendicularDirection = new Vector2(-normalizedDirection.y, normalizedDirection.x);
return normalizedDirection * radialDistance + perpendicularDirection * lateralOffset;
}
/// <summary>
/// 2D 평면 좌표를 각도 기준으로 회전합니다.
/// </summary>
private static Vector2 RotateLayout(Vector2 value, float degrees)
{
float radians = degrees * Mathf.Deg2Rad;
float cosine = Mathf.Cos(radians);
float sine = Mathf.Sin(radians);
return new Vector2(
value.x * cosine - value.y * sine,
value.x * sine + value.y * cosine);
}
private static void CreateOrUpdatePreset(
string assetPath,
string presetName,

View File

@@ -10,12 +10,14 @@ namespace Colosseum.UI
{
[SerializeField] private RectTransform rootRect;
[SerializeField] private Image backgroundImage;
[SerializeField] private Image fillImage;
[SerializeField] private Image innerImage;
[SerializeField] private Button button;
[SerializeField] private Outline outline;
public RectTransform RootRect => rootRect;
public Image BackgroundImage => backgroundImage;
public Image FillImage => fillImage;
public Image InnerImage => innerImage;
public Button Button => button;
public Outline Outline => outline;

View File

@@ -23,6 +23,7 @@ namespace Colosseum.UI
public RectTransform RectTransform;
public Button Button;
public Image Image;
public Image FillImage;
public Image InnerImage;
public Outline Outline;
}
@@ -69,9 +70,10 @@ namespace Colosseum.UI
[SerializeField] private Color statusErrorColor = new Color(1f, 0.52f, 0.45f, 1f);
[SerializeField] private Color lineColor = new Color(0.72f, 0.67f, 0.53f, 0.4f);
[SerializeField] private Color activeLineColor = new Color(0.98f, 0.9f, 0.7f, 0.92f);
[SerializeField] private float graphCenterYOffset = -118f;
private const float LeftPanelWidth = 292f;
private const float RightPanelWidth = 308f;
private const float GraphPadding = 52f;
private const float GraphPadding = 24f;
private const float ConnectionThickness = 7f;
private readonly Dictionary<PassiveNodeData, NodeVisual> nodeVisuals = new();
@@ -399,8 +401,8 @@ namespace Colosseum.UI
Rect canvasRect = canvasRectTransform.rect;
panelRectTransform.sizeDelta = new Vector2(
Mathf.Max(1120f, canvasRect.width - 48f),
Mathf.Max(680f, canvasRect.height - 48f));
Mathf.Max(1160f, canvasRect.width - 24f),
Mathf.Max(720f, canvasRect.height - 24f));
}
private void CreateToggleButton()
@@ -884,6 +886,11 @@ namespace Colosseum.UI
image.sprite = GetNodeSprite(node);
image.type = Image.Type.Simple;
image.preserveAspect = true;
Image fillImage = nodeView.FillImage;
if (fillImage != null)
{
fillImage.type = Image.Type.Sliced;
}
Image innerImage = nodeView.InnerImage;
if (innerImage != null)
{
@@ -913,6 +920,7 @@ namespace Colosseum.UI
RectTransform = rectTransform,
Button = button,
Image = image,
FillImage = fillImage,
InnerImage = innerImage,
Outline = nodeView.Outline != null ? nodeView.Outline : button.GetComponent<Outline>(),
};
@@ -943,7 +951,11 @@ namespace Colosseum.UI
}
StringBuilder builder = new StringBuilder();
builder.AppendLine($"<size=30><b>{focusedNode.DisplayName}</b></size>");
if (!string.IsNullOrWhiteSpace(focusedNode.DisplayName))
{
builder.AppendLine($"<size=30><b>{focusedNode.DisplayName}</b></size>");
}
builder.AppendLine($"<size=18>{PassivePresentationUtility.GetBranchLabel(focusedNode.Branch)} | {PassivePresentationUtility.GetNodeKindLabel(focusedNode.NodeKind)} | 축 {PassivePresentationUtility.GetAxisSummary(focusedNode.AxisMask)}</size>");
builder.AppendLine($"<size=18>비용 {focusedNode.Cost}</size>");
@@ -1091,12 +1103,16 @@ namespace Colosseum.UI
visual.Button.interactable = true;
visual.Image.sprite = GetNodeSprite(node);
visual.Image.color = fillColor;
if (visual.FillImage != null)
{
visual.FillImage.color = GetNodeFillColor(selected, selectable, focused);
}
if (visual.InnerImage != null)
{
visual.InnerImage.sprite = GetInnerNodeSprite();
visual.InnerImage.color = GetInnerNodeColor(selected, selectable, focused);
}
visual.RectTransform.localScale = selected ? new Vector3(1.05f, 1.05f, 1f) : focused ? new Vector3(1.03f, 1.03f, 1f) : Vector3.one;
visual.RectTransform.localScale = Vector3.one;
Outline outline = visual.Outline != null ? visual.Outline : visual.Button.GetComponent<Outline>();
if (outline == null)
@@ -1125,12 +1141,11 @@ namespace Colosseum.UI
private Vector2 GetGraphAnchoredPosition(Vector2 layoutPosition)
{
Rect rect = graphRectTransform != null ? graphRectTransform.rect : new Rect(0f, 0f, 640f, 480f);
float halfWidth = Mathf.Max(0f, rect.width * 0.5f - GraphPadding);
float halfHeight = Mathf.Max(0f, rect.height * 0.5f - GraphPadding);
float halfExtent = Mathf.Max(0f, Mathf.Min(rect.width, rect.height) * 0.5f - GraphPadding);
return new Vector2(
Mathf.Clamp(layoutPosition.x, -1f, 1f) * halfWidth,
Mathf.Clamp(layoutPosition.y, -1f, 1f) * halfHeight);
Mathf.Clamp(layoutPosition.x, -1f, 1f) * halfExtent,
Mathf.Clamp(layoutPosition.y, -1f, 1f) * halfExtent + graphCenterYOffset);
}
private static string BuildConnectionKey(PassiveNodeData leftNode, PassiveNodeData rightNode)
@@ -1142,7 +1157,7 @@ namespace Colosseum.UI
private Sprite GetNodeSprite(PassiveNodeData node)
{
bool useSpecialSprite = node != null && (node.NodeKind == PassiveNodeKind.Hub || node.NodeKind == PassiveNodeKind.Bridge || node.NodeKind == PassiveNodeKind.Capstone);
bool useSpecialSprite = node != null && (node.NodeKind == PassiveNodeKind.Bridge || node.NodeKind == PassiveNodeKind.Capstone);
Sprite fallbackSprite = useSpecialSprite ? normalNodeSprite : specialNodeSprite;
Sprite preferredSprite = useSpecialSprite ? specialNodeSprite : normalNodeSprite;
return preferredSprite != null ? preferredSprite : fallbackSprite;
@@ -1170,6 +1185,20 @@ namespace Colosseum.UI
return new Color(0.3f, 0.3f, 0.32f, 0.34f);
}
private static Color GetNodeFillColor(bool selected, bool selectable, bool focused)
{
if (selected)
return new Color(0.09f, 0.09f, 0.11f, 0.96f);
if (focused)
return new Color(0.08f, 0.08f, 0.10f, 0.94f);
if (selectable)
return new Color(0.07f, 0.07f, 0.09f, 0.92f);
return new Color(0.06f, 0.06f, 0.08f, 0.90f);
}
private Color GetNodeBaseColor(PassiveNodeData node)
{
if (node.Branch == PassiveNodeBranch.Bridge)
@@ -1198,19 +1227,21 @@ namespace Colosseum.UI
{
return node.NodeKind switch
{
PassiveNodeKind.Hub => new Vector2(88f, 88f),
PassiveNodeKind.Capstone => new Vector2(78f, 78f),
PassiveNodeKind.Bridge => new Vector2(64f, 64f),
_ => new Vector2(70f, 70f),
PassiveNodeKind.Hub => new Vector2(78f, 78f),
PassiveNodeKind.Capstone => new Vector2(70f, 70f),
PassiveNodeKind.Bridge => new Vector2(56f, 56f),
_ => new Vector2(62f, 62f),
};
}
private static float GetConnectionInset(PassiveNodeData node)
{
Vector2 size = GetNodeSize(node);
float radius = Mathf.Min(size.x, size.y) * 0.42f;
float radius = Mathf.Min(size.x, size.y) * 0.5f + ConnectionThickness * 0.5f + 2f;
if (node != null && node.NodeKind == PassiveNodeKind.Bridge)
radius += 3f;
radius += 4f;
else if (node != null && (node.NodeKind == PassiveNodeKind.Hub || node.NodeKind == PassiveNodeKind.Capstone))
radius += 2f;
return radius;
}
@@ -1380,7 +1411,13 @@ namespace Colosseum.UI
Transform footerTransform = statusText.transform.parent;
if (footerTransform != null)
{
footerTransform.gameObject.SetActive(hasMessage);
Image footerImage = footerTransform.GetComponent<Image>();
if (footerImage != null)
{
Color color = sectionBackgroundColor;
color.a = hasMessage ? sectionBackgroundColor.a : 0f;
footerImage.color = color;
}
}
statusText.text = hasMessage ? lastStatusMessage : string.Empty;