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 { /// /// 패시브 트리 프로토타입 에셋 생성 및 디버그 적용 메뉴입니다. /// 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 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() { EnsureFolder("Assets/_Game", "Data"); EnsureFolder(DataFolderPath, "Passives"); EnsureFolder(PassiveFolderPath, "Nodes"); EnsureFolder(PassiveFolderPath, "Presets"); 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, 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, attackEntryPosition, new[] { hubNode }, new[] { hubNode, attackCoreNode }, new[] { 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( attackCoreNode, "attack_core", "집중 공세", "공격 스킬 계열의 핵심 화력을 강화합니다.", PassiveNodeBranch.Attack, PassiveNodeKind.Axis, PassiveAxisMask.Attack, 3, 1, attackCorePosition, new[] { attackEntryNode }, new[] { attackEntryNode, attackStrengthNode, attackPrecisionNode, attackArcanaNode, attackCapstoneNode, attackDefenseBridgeNode, supportAttackBridgeNode }, new[] { PassiveEffectConfig.CreateScalar(PassiveEffectType.DamageMultiplier, 1.10f, SkillRoleType.Attack), }); ConfigureNode( attackCapstoneNode, "attack_capstone", "집행 증폭", "공격 축 완성 노드로, 공격 계열 고위력 기술의 기여도를 강화합니다.", PassiveNodeBranch.Attack, PassiveNodeKind.Capstone, PassiveAxisMask.Attack, 4, 2, attackCapstonePosition, new[] { attackCoreNode }, new[] { attackCoreNode }, new[] { PassiveEffectConfig.CreateScalar(PassiveEffectType.DamageMultiplier, 1.08f, SkillRoleType.Attack), PassiveEffectConfig.CreateScalar(PassiveEffectType.ManaCostMultiplier, 0.92f, SkillRoleType.Attack), }); ConfigureNode( defenseEntryNode, "defense_entry", "전열 적응", "방어 축의 출발점으로, 전열 유지에 필요한 생존력을 확보합니다.", PassiveNodeBranch.Defense, PassiveNodeKind.Axis, PassiveAxisMask.Defense, 1, 1, defenseEntryPosition, new[] { hubNode }, new[] { hubNode, defenseCoreNode }, new[] { 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( defenseCoreNode, "defense_core", "방호 숙련", "위협 유지와 보호막 수혜량을 함께 강화합니다.", PassiveNodeBranch.Defense, PassiveNodeKind.Axis, PassiveAxisMask.Defense, 3, 1, defenseCorePosition, new[] { defenseEntryNode }, new[] { defenseEntryNode, defenseFrameNode, defenseGuardNode, defenseResolveNode, defenseCapstoneNode, attackDefenseBridgeNode, defenseSupportBridgeNode }, new[] { PassiveEffectConfig.CreateScalar(PassiveEffectType.ThreatGeneratedMultiplier, 1.25f), PassiveEffectConfig.CreateScalar(PassiveEffectType.ShieldReceivedMultiplier, 1.10f), }); ConfigureNode( defenseCapstoneNode, "defense_capstone", "철벽 유지", "받는 피해를 낮춰 전열 유지력을 완성합니다.", PassiveNodeBranch.Defense, PassiveNodeKind.Capstone, PassiveAxisMask.Defense, 4, 2, defenseCapstonePosition, new[] { defenseCoreNode }, new[] { defenseCoreNode }, new[] { PassiveEffectConfig.CreateScalar(PassiveEffectType.IncomingDamageMultiplier, 0.90f), }); ConfigureNode( supportEntryNode, "support_entry", "구호 적응", "지원 축의 출발점으로, 회복 기반 능력을 높입니다.", PassiveNodeBranch.Support, PassiveNodeKind.Axis, PassiveAxisMask.Support, 1, 1, supportEntryPosition, new[] { hubNode }, new[] { hubNode, supportCoreNode }, new[] { 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( supportCoreNode, "support_core", "조율 숙련", "회복과 보호막 부여 효율을 함께 강화합니다.", PassiveNodeBranch.Support, PassiveNodeKind.Axis, PassiveAxisMask.Support, 3, 1, supportCorePosition, new[] { supportEntryNode }, new[] { supportEntryNode, supportInsightNode, supportWellNode, supportFocusNode, supportCapstoneNode, defenseSupportBridgeNode, supportAttackBridgeNode }, new[] { PassiveEffectConfig.CreateScalar(PassiveEffectType.HealMultiplier, 1.12f, SkillRoleType.Support), PassiveEffectConfig.CreateScalar(PassiveEffectType.ShieldDoneMultiplier, 1.12f, SkillRoleType.Support), }); ConfigureNode( supportCapstoneNode, "support_capstone", "마력 순환", "최대 마나와 유지 효율을 함께 높입니다.", PassiveNodeBranch.Support, PassiveNodeKind.Capstone, PassiveAxisMask.Support, 4, 2, supportCapstonePosition, new[] { supportCoreNode }, new[] { supportCoreNode }, new[] { 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, attackDefenseBridgePosition, new[] { attackCoreNode }, new[] { attackCoreNode, defenseCoreNode }, new[] { 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, defenseSupportBridgePosition, new[] { defenseCoreNode }, new[] { defenseCoreNode, supportCoreNode }, new[] { 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, supportAttackBridgePosition, new[] { supportCoreNode }, new[] { supportCoreNode, attackCoreNode }, new[] { PassiveEffectConfig.CreateStat(StatType.Wisdom, StatModType.Flat, 3f), PassiveEffectConfig.CreateStat(StatType.Intelligence, StatModType.Flat, 3f), }); PassiveTreeData tree = CreateOrLoadTree(); ConfigureTree( tree, "player_prototype_tree", "플레이어 패시브 프로토타입", "공격 / 방어 / 지원 3축에 다수의 스탯 노드와 연결 노드를 배치한 드로그전 밸런싱 검증용 트리입니다.", 12, new[] { hubNode, attackEntryNode, attackStrengthNode, attackPrecisionNode, attackArcanaNode, attackCoreNode, attackCapstoneNode, defenseEntryNode, defenseFrameNode, defenseGuardNode, defenseResolveNode, defenseCoreNode, defenseCapstoneNode, supportEntryNode, supportInsightNode, supportWellNode, supportFocusNode, supportCoreNode, supportCapstoneNode, attackDefenseBridgeNode, defenseSupportBridgeNode, supportAttackBridgeNode, }); CreateOrUpdatePreset( NonePresetAssetPath, "패시브 없음", "비교 기준선 확보용 프리셋입니다.", tree, new[] { hubNode }); CreateOrUpdatePreset( DefensePresetAssetPath, "방어형 패시브", "방어 축의 스탯 노드와 핵심 유지 노드를 우선 확보하는 프리셋입니다.", tree, new[] { hubNode, defenseEntryNode, defenseCoreNode, defenseFrameNode, defenseGuardNode, defenseResolveNode, defenseCapstoneNode, attackEntryNode, attackCoreNode, supportEntryNode, attackDefenseBridgeNode, supportCoreNode, }); CreateOrUpdatePreset( SupportPresetAssetPath, "지원형 패시브", "지원 축의 스탯 노드와 유지 효율 노드를 우선 확보하는 프리셋입니다.", tree, new[] { hubNode, supportEntryNode, supportCoreNode, supportInsightNode, supportWellNode, supportFocusNode, supportCapstoneNode, defenseEntryNode, defenseCoreNode, attackEntryNode, defenseSupportBridgeNode, attackCoreNode, }); CreateOrUpdatePreset( AttackPresetAssetPath, "공격형 패시브", "공격 축의 스탯 노드와 화력 노드를 우선 확보하는 프리셋입니다.", tree, new[] { hubNode, attackEntryNode, attackCoreNode, attackStrengthNode, attackPrecisionNode, attackArcanaNode, attackCapstoneNode, defenseEntryNode, defenseCoreNode, supportEntryNode, attackDefenseBridgeNode, supportCoreNode, }); PassivePrototypeCatalogData catalog = CreateOrLoadCatalog(); ConfigureCatalog( catalog, tree, AssetDatabase.LoadAssetAtPath(NonePresetAssetPath), AssetDatabase.LoadAssetAtPath(DefensePresetAssetPath), AssetDatabase.LoadAssetAtPath(SupportPresetAssetPath), AssetDatabase.LoadAssetAtPath(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(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(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(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(PassiveTreeAssetPath); if (tree != null) return tree; tree = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(tree, PassiveTreeAssetPath); return tree; } private static PassivePrototypeCatalogData CreateOrLoadCatalog() { PassivePrototypeCatalogData catalog = AssetDatabase.LoadAssetAtPath(PassiveCatalogAssetPath); if (catalog != null) return catalog; catalog = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(catalog, PassiveCatalogAssetPath); return catalog; } private static PassiveNodeData CreateOrLoadNode(string assetPath) { PassiveNodeData node = AssetDatabase.LoadAssetAtPath(assetPath); if (node != null) return node; node = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(node, assetPath); return node; } private static void ConfigureTree( PassiveTreeData tree, string treeId, string treeName, string description, int initialPoints, IReadOnlyList 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 prerequisiteNodes, IReadOnlyList connectedNodes, IReadOnlyList 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 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; } /// /// 2D 평면 좌표를 각도 기준으로 회전합니다. /// 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, string description, PassiveTreeData tree, IReadOnlyList selectedNodes) { PassivePresetData preset = AssetDatabase.LoadAssetAtPath(assetPath); if (preset == null) { preset = ScriptableObject.CreateInstance(); 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(); 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(); 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); } } } }