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}
|
Skill: {fileID: 11400000, guid: 19675febf4943e06b87c83e7d8517e3c, type: 2}
|
||||||
Duration: 0
|
Duration: 0
|
||||||
ChargeData:
|
ChargeData:
|
||||||
requiredDamageRatio: 0
|
requiredDamageRatio: 0.1
|
||||||
telegraphAbnormality: {fileID: 0}
|
telegraphAbnormality: {fileID: 0}
|
||||||
staggerDuration: 0
|
staggerDuration: 2
|
||||||
- Type: 0
|
- Type: 0
|
||||||
Skill: {fileID: 11400000, guid: ff1a135feff0d1999892a94317128bcf, type: 2}
|
Skill: {fileID: 11400000, guid: ff1a135feff0d1999892a94317128bcf, type: 2}
|
||||||
Duration: 0
|
Duration: 0
|
||||||
ChargeData:
|
ChargeData:
|
||||||
requiredDamageRatio: 0
|
requiredDamageRatio: 0.1
|
||||||
telegraphAbnormality: {fileID: 0}
|
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
|
cooldown: 2.5
|
||||||
minPhase: 1
|
minPhase: 1
|
||||||
skipJumpStepOnNoTarget: 0
|
skipJumpStepOnNoTarget: 0
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using UnityEngine;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
using Colosseum.Abnormalities;
|
using Colosseum.Abnormalities;
|
||||||
using Colosseum.Skills;
|
using Colosseum.Skills;
|
||||||
|
|
||||||
@@ -79,6 +81,12 @@ namespace Colosseum.AI
|
|||||||
[CreateAssetMenu(fileName = "NewBossPattern", menuName = "Colosseum/Boss Pattern")]
|
[CreateAssetMenu(fileName = "NewBossPattern", menuName = "Colosseum/Boss Pattern")]
|
||||||
public class BossPatternData : ScriptableObject
|
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("패턴 정보")]
|
[Header("패턴 정보")]
|
||||||
[SerializeField] private string patternName;
|
[SerializeField] private string patternName;
|
||||||
|
|
||||||
@@ -123,5 +131,205 @@ namespace Colosseum.AI
|
|||||||
/// Big 패턴인지 반환합니다 (grace period 대상).
|
/// Big 패턴인지 반환합니다 (grace period 대상).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsBigPattern => category == PatternCategory.Big;
|
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();
|
serializedObject.ApplyModifiedPropertiesWithoutUndo();
|
||||||
|
pattern.RefreshPatternStepsFromMatchingSkills();
|
||||||
EditorUtility.SetDirty(pattern);
|
EditorUtility.SetDirty(pattern);
|
||||||
return 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