feat: 스킬-패턴 자동 동기화 추가

- Data_Pattern_{name}가 Data_Skill_{name}_{seq}를 순번대로 찾아 시퀀스 스텝을 자동 재구성하도록 추가
- SkillData asset 변경 시 BossPatternData를 다시 맞추는 에디터 postprocessor를 추가
- 드로그 리빌드 경로와 콤보-기본기1 패턴 자산을 새 자동 동기화 규칙에 맞게 정리
This commit is contained in:
2026-04-10 11:37:20 +09:00
parent f6f7eaaef2
commit 72aae85afd
5 changed files with 324 additions and 5 deletions

View File

@@ -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

View File

@@ -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 대상).
/// </summary>
public bool IsBigPattern => category == PatternCategory.Big;
#if UNITY_EDITOR
/// <summary>
/// 패턴 자산 이름을 기준으로 시퀀스 스킬 스텝을 자동 동기화합니다.
/// Data_Pattern_{이름} 패턴은 Data_Skill_{이름}_{순번} 스킬들을 찾아 앞쪽 스킬 스텝 구간을 재구성합니다.
/// </summary>
private void OnValidate()
{
RefreshPatternStepsFromMatchingSkills();
}
/// <summary>
/// 매칭되는 시퀀스 스킬을 찾아 패턴 스텝에 반영합니다.
/// </summary>
public bool RefreshPatternStepsFromMatchingSkills()
{
if (!name.StartsWith(PatternAssetPrefix, StringComparison.Ordinal))
return false;
string patternKey = name.Substring(PatternAssetPrefix.Length);
if (string.IsNullOrEmpty(patternKey))
return false;
List<SkillData> matchingSkills = FindMatchingSkills(patternKey);
if (matchingSkills.Count == 0)
return false;
int insertionIndex = -1;
var preservedSteps = new List<PatternStep>(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<PatternStep>(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;
}
/// <summary>
/// 패턴 이름과 매칭되는 시퀀스 스킬 목록을 순번 기준으로 수집합니다.
/// </summary>
private static List<SkillData> 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<SkillData>(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<SkillData>(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;
}
/// <summary>
/// 지정한 스텝이 현재 패턴 이름에 대응하는 시퀀스 스킬 스텝인지 확인합니다.
/// </summary>
private static bool IsMatchingSkillStep(PatternStep step, string patternKey)
{
return step != null
&& step.Type == PatternStepType.Skill
&& step.Skill != null
&& TryGetMatchingSkillOrder(step.Skill.name, patternKey, out _);
}
/// <summary>
/// 스킬 자산 이름에서 패턴과 매칭되는 순번을 추출합니다.
/// </summary>
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);
}
/// <summary>
/// 현재 스텝 구성과 재구성 결과가 같은지 비교합니다.
/// </summary>
private static bool AreStepsEquivalent(IReadOnlyList<PatternStep> currentSteps, IReadOnlyList<PatternStep> 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;
}
/// <summary>
/// ChargeWait 보조 데이터를 비교합니다.
/// </summary>
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
}
}

View File

@@ -1352,6 +1352,7 @@ namespace Colosseum.Editor
}
serializedObject.ApplyModifiedPropertiesWithoutUndo();
pattern.RefreshPatternStepsFromMatchingSkills();
EditorUtility.SetDirty(pattern);
return pattern;
}

View File

@@ -0,0 +1,101 @@
using System;
using UnityEditor;
using Colosseum.AI;
namespace Colosseum.Editor
{
/// <summary>
/// SkillData 자산이 변경되면 이름 규칙에 맞는 BossPatternData 스텝 구성을 자동 갱신합니다.
/// </summary>
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();
}
/// <summary>
/// 에디터가 다시 로드될 때 기존 패턴과 스킬 자산 정합성을 한 번 점검합니다.
/// </summary>
[InitializeOnLoadMethod]
private static void RefreshPatternsOnEditorLoad()
{
EditorApplication.delayCall += RefreshAllPatternSteps;
}
/// <summary>
/// 모든 BossPatternData를 순회하며 시퀀스 스킬 스텝을 갱신합니다.
/// </summary>
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<BossPatternData>(path);
if (pattern == null)
continue;
if (pattern.RefreshPatternStepsFromMatchingSkills())
refreshedCount++;
}
if (refreshedCount > 0)
AssetDatabase.SaveAssets();
}
/// <summary>
/// SkillData 관련 자산이 추가/삭제/이동되었는지 확인합니다.
/// </summary>
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;
}
/// <summary>
/// 지정한 경로가 SkillData 자동 매칭 대상 폴더인지 확인합니다.
/// </summary>
private static bool IsSkillAssetPath(string assetPath)
{
return assetPath != null
&& assetPath.StartsWith(SkillsFolderPath, StringComparison.Ordinal)
&& assetPath.EndsWith(".asset", StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 494e62541f2df8e189120345438d02d3