feat: 스킬-패턴 자동 동기화 추가
- Data_Pattern_{name}가 Data_Skill_{name}_{seq}를 순번대로 찾아 시퀀스 스텝을 자동 재구성하도록 추가
- SkillData asset 변경 시 BossPatternData를 다시 맞추는 에디터 postprocessor를 추가
- 드로그 리빌드 경로와 콤보-기본기1 패턴 자산을 새 자동 동기화 규칙에 맞게 정리
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1352,6 +1352,7 @@ namespace Colosseum.Editor
|
||||
}
|
||||
|
||||
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
||||
pattern.RefreshPatternStepsFromMatchingSkills();
|
||||
EditorUtility.SetDirty(pattern);
|
||||
return pattern;
|
||||
}
|
||||
|
||||
101
Assets/_Game/Scripts/Editor/SkillPatternDataMatcher.cs
Normal file
101
Assets/_Game/Scripts/Editor/SkillPatternDataMatcher.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 494e62541f2df8e189120345438d02d3
|
||||
Reference in New Issue
Block a user