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