From 72aae85afd52e826c61be8b8f882440edee3407f Mon Sep 17 00:00:00 2001 From: dal4segno Date: Fri, 10 Apr 2026 11:37:20 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=8A=A4=ED=82=AC-=ED=8C=A8=ED=84=B4?= =?UTF-8?q?=20=EC=9E=90=EB=8F=99=20=EB=8F=99=EA=B8=B0=ED=99=94=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Data_Pattern_{name}가 Data_Skill_{name}_{seq}를 순번대로 찾아 시퀀스 스텝을 자동 재구성하도록 추가 - SkillData asset 변경 시 BossPatternData를 다시 맞추는 에디터 postprocessor를 추가 - 드로그 리빌드 경로와 콤보-기본기1 패턴 자산을 새 자동 동기화 규칙에 맞게 정리 --- .../Data_Pattern_Drog_콤보-기본기1.asset | 15 +- Assets/_Game/Scripts/AI/BossPatternData.cs | 210 +++++++++++++++++- .../Scripts/Editor/RebuildDrogCombatAssets.cs | 1 + .../Scripts/Editor/SkillPatternDataMatcher.cs | 101 +++++++++ .../Editor/SkillPatternDataMatcher.cs.meta | 2 + 5 files changed, 324 insertions(+), 5 deletions(-) create mode 100644 Assets/_Game/Scripts/Editor/SkillPatternDataMatcher.cs create mode 100644 Assets/_Game/Scripts/Editor/SkillPatternDataMatcher.cs.meta diff --git a/Assets/_Game/Data/Patterns/Data_Pattern_Drog_콤보-기본기1.asset b/Assets/_Game/Data/Patterns/Data_Pattern_Drog_콤보-기본기1.asset index 11aa8c9b..62dc21ca 100644 --- a/Assets/_Game/Data/Patterns/Data_Pattern_Drog_콤보-기본기1.asset +++ b/Assets/_Game/Data/Patterns/Data_Pattern_Drog_콤보-기본기1.asset @@ -22,16 +22,23 @@ MonoBehaviour: Skill: {fileID: 11400000, guid: 19675febf4943e06b87c83e7d8517e3c, type: 2} Duration: 0 ChargeData: - requiredDamageRatio: 0 + requiredDamageRatio: 0.1 telegraphAbnormality: {fileID: 0} - staggerDuration: 0 + staggerDuration: 2 - Type: 0 Skill: {fileID: 11400000, guid: ff1a135feff0d1999892a94317128bcf, type: 2} Duration: 0 ChargeData: - requiredDamageRatio: 0 + requiredDamageRatio: 0.1 telegraphAbnormality: {fileID: 0} - staggerDuration: 0 + staggerDuration: 2 + - Type: 0 + Skill: {fileID: 11400000, guid: ef31b6ee182b8ce4cb7a0abf35daa91e, type: 2} + Duration: 0 + ChargeData: + requiredDamageRatio: 0.1 + telegraphAbnormality: {fileID: 0} + staggerDuration: 2 cooldown: 2.5 minPhase: 1 skipJumpStepOnNoTarget: 0 diff --git a/Assets/_Game/Scripts/AI/BossPatternData.cs b/Assets/_Game/Scripts/AI/BossPatternData.cs index 1129b6a2..a1b590eb 100644 --- a/Assets/_Game/Scripts/AI/BossPatternData.cs +++ b/Assets/_Game/Scripts/AI/BossPatternData.cs @@ -1,6 +1,8 @@ -using UnityEngine; +using System; using System.Collections.Generic; +using UnityEngine; + using Colosseum.Abnormalities; using Colosseum.Skills; @@ -79,6 +81,12 @@ namespace Colosseum.AI [CreateAssetMenu(fileName = "NewBossPattern", menuName = "Colosseum/Boss Pattern")] public class BossPatternData : ScriptableObject { +#if UNITY_EDITOR + private const string PatternAssetPrefix = "Data_Pattern_"; + private const string SkillAssetPrefix = "Data_Skill_"; + private const string SkillSearchFolder = "Assets/_Game/Data/Skills"; +#endif + [Header("패턴 정보")] [SerializeField] private string patternName; @@ -123,5 +131,205 @@ namespace Colosseum.AI /// Big 패턴인지 반환합니다 (grace period 대상). /// public bool IsBigPattern => category == PatternCategory.Big; + +#if UNITY_EDITOR + /// + /// 패턴 자산 이름을 기준으로 시퀀스 스킬 스텝을 자동 동기화합니다. + /// Data_Pattern_{이름} 패턴은 Data_Skill_{이름}_{순번} 스킬들을 찾아 앞쪽 스킬 스텝 구간을 재구성합니다. + /// + private void OnValidate() + { + RefreshPatternStepsFromMatchingSkills(); + } + + /// + /// 매칭되는 시퀀스 스킬을 찾아 패턴 스텝에 반영합니다. + /// + public bool RefreshPatternStepsFromMatchingSkills() + { + if (!name.StartsWith(PatternAssetPrefix, StringComparison.Ordinal)) + return false; + + string patternKey = name.Substring(PatternAssetPrefix.Length); + if (string.IsNullOrEmpty(patternKey)) + return false; + + List matchingSkills = FindMatchingSkills(patternKey); + if (matchingSkills.Count == 0) + return false; + + int insertionIndex = -1; + var preservedSteps = new List(steps.Count); + + for (int i = 0; i < steps.Count; i++) + { + PatternStep step = steps[i]; + if (IsMatchingSkillStep(step, patternKey)) + { + if (insertionIndex < 0) + insertionIndex = preservedSteps.Count; + + continue; + } + + preservedSteps.Add(step); + } + + if (insertionIndex < 0) + insertionIndex = 0; + + var rebuiltSteps = new List(preservedSteps.Count + matchingSkills.Count); + + for (int i = 0; i < insertionIndex; i++) + rebuiltSteps.Add(preservedSteps[i]); + + for (int i = 0; i < matchingSkills.Count; i++) + { + rebuiltSteps.Add(new PatternStep + { + Type = PatternStepType.Skill, + Skill = matchingSkills[i], + Duration = 0f, + ChargeData = null, + }); + } + + for (int i = insertionIndex; i < preservedSteps.Count; i++) + rebuiltSteps.Add(preservedSteps[i]); + + if (AreStepsEquivalent(steps, rebuiltSteps)) + return false; + + steps = rebuiltSteps; + UnityEditor.EditorUtility.SetDirty(this); + return true; + } + + /// + /// 패턴 이름과 매칭되는 시퀀스 스킬 목록을 순번 기준으로 수집합니다. + /// + private static List FindMatchingSkills(string patternKey) + { + string[] guids = UnityEditor.AssetDatabase.FindAssets("t:SkillData", new[] { SkillSearchFolder }); + var matchedSkills = new List<(SkillData skill, int order)>(); + + for (int i = 0; i < guids.Length; i++) + { + string path = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[i]); + SkillData skill = UnityEditor.AssetDatabase.LoadAssetAtPath(path); + if (skill == null) + continue; + + if (!TryGetMatchingSkillOrder(skill.name, patternKey, out int order)) + continue; + + matchedSkills.Add((skill, order)); + } + + matchedSkills.Sort((a, b) => a.order.CompareTo(b.order)); + + var orderedSkills = new List(matchedSkills.Count); + int previousOrder = int.MinValue; + + for (int i = 0; i < matchedSkills.Count; i++) + { + if (matchedSkills[i].order == previousOrder) + continue; + + orderedSkills.Add(matchedSkills[i].skill); + previousOrder = matchedSkills[i].order; + } + + return orderedSkills; + } + + /// + /// 지정한 스텝이 현재 패턴 이름에 대응하는 시퀀스 스킬 스텝인지 확인합니다. + /// + private static bool IsMatchingSkillStep(PatternStep step, string patternKey) + { + return step != null + && step.Type == PatternStepType.Skill + && step.Skill != null + && TryGetMatchingSkillOrder(step.Skill.name, patternKey, out _); + } + + /// + /// 스킬 자산 이름에서 패턴과 매칭되는 순번을 추출합니다. + /// + private static bool TryGetMatchingSkillOrder(string skillAssetName, string patternKey, out int order) + { + order = 0; + + if (string.IsNullOrEmpty(skillAssetName) || string.IsNullOrEmpty(patternKey)) + return false; + + string prefix = $"{SkillAssetPrefix}{patternKey}_"; + if (!skillAssetName.StartsWith(prefix, StringComparison.Ordinal)) + return false; + + string suffix = skillAssetName.Substring(prefix.Length); + return int.TryParse(suffix, out order); + } + + /// + /// 현재 스텝 구성과 재구성 결과가 같은지 비교합니다. + /// + private static bool AreStepsEquivalent(IReadOnlyList currentSteps, IReadOnlyList rebuiltSteps) + { + if (ReferenceEquals(currentSteps, rebuiltSteps)) + return true; + + if (currentSteps == null || rebuiltSteps == null) + return false; + + if (currentSteps.Count != rebuiltSteps.Count) + return false; + + for (int i = 0; i < currentSteps.Count; i++) + { + PatternStep current = currentSteps[i]; + PatternStep rebuilt = rebuiltSteps[i]; + + if (current == null || rebuilt == null) + { + if (current != rebuilt) + return false; + + continue; + } + + if (current.Type != rebuilt.Type) + return false; + + if (current.Skill != rebuilt.Skill) + return false; + + if (!Mathf.Approximately(current.Duration, rebuilt.Duration)) + return false; + + if (!AreChargeDataEquivalent(current.ChargeData, rebuilt.ChargeData)) + return false; + } + + return true; + } + + /// + /// ChargeWait 보조 데이터를 비교합니다. + /// + private static bool AreChargeDataEquivalent(ChargeStepData current, ChargeStepData rebuilt) + { + if (ReferenceEquals(current, rebuilt)) + return true; + + if (current == null || rebuilt == null) + return false; + + return Mathf.Approximately(current.RequiredDamageRatio, rebuilt.RequiredDamageRatio) + && current.TelegraphAbnormality == rebuilt.TelegraphAbnormality + && Mathf.Approximately(current.StaggerDuration, rebuilt.StaggerDuration); + } +#endif } } diff --git a/Assets/_Game/Scripts/Editor/RebuildDrogCombatAssets.cs b/Assets/_Game/Scripts/Editor/RebuildDrogCombatAssets.cs index a52353e1..0b4fdaeb 100644 --- a/Assets/_Game/Scripts/Editor/RebuildDrogCombatAssets.cs +++ b/Assets/_Game/Scripts/Editor/RebuildDrogCombatAssets.cs @@ -1352,6 +1352,7 @@ namespace Colosseum.Editor } serializedObject.ApplyModifiedPropertiesWithoutUndo(); + pattern.RefreshPatternStepsFromMatchingSkills(); EditorUtility.SetDirty(pattern); return pattern; } diff --git a/Assets/_Game/Scripts/Editor/SkillPatternDataMatcher.cs b/Assets/_Game/Scripts/Editor/SkillPatternDataMatcher.cs new file mode 100644 index 00000000..6cc145bf --- /dev/null +++ b/Assets/_Game/Scripts/Editor/SkillPatternDataMatcher.cs @@ -0,0 +1,101 @@ +using System; + +using UnityEditor; + +using Colosseum.AI; + +namespace Colosseum.Editor +{ + /// + /// SkillData 자산이 변경되면 이름 규칙에 맞는 BossPatternData 스텝 구성을 자동 갱신합니다. + /// + public class SkillPatternDataMatcher : AssetPostprocessor + { + private const string SkillsFolderPath = "Assets/_Game/Data/Skills"; + private const string PatternsFolderPath = "Assets/_Game/Data/Patterns"; + + private static void OnPostprocessAllAssets( + string[] importedAssets, + string[] deletedAssets, + string[] movedAssets, + string[] movedFromAssetPaths) + { + if (!HasSkillAssetChange(importedAssets, deletedAssets, movedAssets, movedFromAssetPaths)) + return; + + RefreshAllPatternSteps(); + } + + /// + /// 에디터가 다시 로드될 때 기존 패턴과 스킬 자산 정합성을 한 번 점검합니다. + /// + [InitializeOnLoadMethod] + private static void RefreshPatternsOnEditorLoad() + { + EditorApplication.delayCall += RefreshAllPatternSteps; + } + + /// + /// 모든 BossPatternData를 순회하며 시퀀스 스킬 스텝을 갱신합니다. + /// + public static void RefreshAllPatternSteps() + { + string[] patternGuids = AssetDatabase.FindAssets("t:BossPatternData", new[] { PatternsFolderPath }); + int refreshedCount = 0; + + for (int i = 0; i < patternGuids.Length; i++) + { + string path = AssetDatabase.GUIDToAssetPath(patternGuids[i]); + BossPatternData pattern = AssetDatabase.LoadAssetAtPath(path); + if (pattern == null) + continue; + + if (pattern.RefreshPatternStepsFromMatchingSkills()) + refreshedCount++; + } + + if (refreshedCount > 0) + AssetDatabase.SaveAssets(); + } + + /// + /// SkillData 관련 자산이 추가/삭제/이동되었는지 확인합니다. + /// + private static bool HasSkillAssetChange( + string[] importedAssets, + string[] deletedAssets, + string[] movedAssets, + string[] movedFromAssetPaths) + { + for (int i = 0; i < importedAssets.Length; i++) + { + if (IsSkillAssetPath(importedAssets[i])) + return true; + } + + for (int i = 0; i < deletedAssets.Length; i++) + { + if (IsSkillAssetPath(deletedAssets[i])) + return true; + } + + for (int i = 0; i < movedAssets.Length; i++) + { + if (IsSkillAssetPath(movedAssets[i]) || IsSkillAssetPath(movedFromAssetPaths[i])) + return true; + } + + return false; + } + + /// + /// 지정한 경로가 SkillData 자동 매칭 대상 폴더인지 확인합니다. + /// + private static bool IsSkillAssetPath(string assetPath) + { + return assetPath != null + && assetPath.StartsWith(SkillsFolderPath, StringComparison.Ordinal) + && assetPath.EndsWith(".asset", StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/Assets/_Game/Scripts/Editor/SkillPatternDataMatcher.cs.meta b/Assets/_Game/Scripts/Editor/SkillPatternDataMatcher.cs.meta new file mode 100644 index 00000000..3056bc4f --- /dev/null +++ b/Assets/_Game/Scripts/Editor/SkillPatternDataMatcher.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 494e62541f2df8e189120345438d02d3 \ No newline at end of file