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,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);
}
}
}
}