- Data_Pattern_{name}가 Data_Skill_{name}_{seq}를 순번대로 찾아 시퀀스 스텝을 자동 재구성하도록 추가
- SkillData asset 변경 시 BossPatternData를 다시 맞추는 에디터 postprocessor를 추가
- 드로그 리빌드 경로와 콤보-기본기1 패턴 자산을 새 자동 동기화 규칙에 맞게 정리
336 lines
12 KiB
C#
336 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
|
|
using UnityEngine;
|
|
|
|
using Colosseum.Abnormalities;
|
|
using Colosseum.Skills;
|
|
|
|
namespace Colosseum.AI
|
|
{
|
|
/// <summary>
|
|
/// 패턴의 대분류. grace period 판단에 사용됩니다.
|
|
/// </summary>
|
|
public enum PatternCategory
|
|
{
|
|
/// <summary>기본 패턴 — grace period 제한 없음</summary>
|
|
Basic,
|
|
/// <summary>대형 패턴 — basicLoopCount 이후 사용 가능</summary>
|
|
Big,
|
|
/// <summary>징벌 패턴 — 항상 허용, bigPattern 카운터 리셋</summary>
|
|
Punish,
|
|
}
|
|
|
|
/// <summary>
|
|
/// 패턴의 타겟 해석 방식
|
|
/// </summary>
|
|
public enum TargetResolveMode
|
|
{
|
|
/// <summary>타겟 해석 불필요 (시그니처 등 내부 처리)</summary>
|
|
None,
|
|
/// <summary>가장 위협도가 높은 근접 대상</summary>
|
|
HighestThreat,
|
|
/// <summary>기동 패턴 전용 타겟</summary>
|
|
Mobility,
|
|
/// <summary>유틸리티 패턴 전용 타겟</summary>
|
|
Utility,
|
|
}
|
|
|
|
public enum PatternStepType { Skill, Wait, ChargeWait }
|
|
|
|
/// <summary>
|
|
/// ChargeWait 스텝의 차단 관련 설정 데이터입니다.
|
|
/// 충전 대기 중 플레이어가 누적 피해를 충족하면 차단 성공으로 처리됩니다.
|
|
/// </summary>
|
|
[System.Serializable]
|
|
public class ChargeStepData
|
|
{
|
|
[Header("차단 조건")]
|
|
[Tooltip("차단에 필요한 누적 피해 비율 (보스 최대 체력 기준)")]
|
|
[Range(0f, 1f)]
|
|
[SerializeField] private float requiredDamageRatio = 0.1f;
|
|
|
|
[Header("전조 효과")]
|
|
[Tooltip("충전 중 부여할 전조 이상상태 (루핑 VFX 등)")]
|
|
[SerializeField] private AbnormalityData telegraphAbnormality;
|
|
|
|
[Header("차단 성공 효과")]
|
|
[Tooltip("차단 성공 시 보스 경직 시간")]
|
|
[Min(0f)]
|
|
[SerializeField] private float staggerDuration = 2f;
|
|
|
|
public float RequiredDamageRatio => requiredDamageRatio;
|
|
public AbnormalityData TelegraphAbnormality => telegraphAbnormality;
|
|
public float StaggerDuration => staggerDuration;
|
|
}
|
|
|
|
[System.Serializable]
|
|
public class PatternStep
|
|
{
|
|
public PatternStepType Type = PatternStepType.Skill;
|
|
public SkillData Skill;
|
|
[Min(0f)] public float Duration = 0.5f;
|
|
|
|
[Tooltip("ChargeWait 타입 전용 차단 설정")]
|
|
public ChargeStepData ChargeData;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 보스 패턴 데이터. 순서대로 실행할 스텝(스킬 또는 대기) 목록과 쿨타임을 정의합니다.
|
|
/// </summary>
|
|
[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;
|
|
|
|
[Header("패턴 특성")]
|
|
[Tooltip("패턴 분류 — grace period 판단에 사용")]
|
|
[SerializeField] private PatternCategory category = PatternCategory.Basic;
|
|
[Tooltip("시그니처 패턴 여부 — 현재 사용되지 않음 (ChargeWait 스텝으로 대체)")]
|
|
[SerializeField] private bool isSignature;
|
|
[Tooltip("근접 패턴 여부 — meleePatternCounter 갱신")]
|
|
[SerializeField] private bool isMelee;
|
|
[Tooltip("타겟 해석 방식")]
|
|
[SerializeField] private TargetResolveMode targetMode = TargetResolveMode.HighestThreat;
|
|
|
|
[Header("스텝 순서")]
|
|
[SerializeField] private List<PatternStep> steps = new List<PatternStep>();
|
|
|
|
[Header("쿨타임")]
|
|
[Min(0f)]
|
|
[Tooltip("패턴 완료 후 다시 사용 가능해지기까지의 시간")]
|
|
[SerializeField] private float cooldown = 5f;
|
|
|
|
[Header("페이즈 제한")]
|
|
[Min(1)]
|
|
[Tooltip("이 패턴을 사용하기 시작하는 최소 페이즈 (1=Phase 1부터)")]
|
|
[SerializeField] private int minPhase = 1;
|
|
|
|
[Header("조건부 점프")]
|
|
[Tooltip("점프 스텝에서 대상을 찾지 못하면 해당 스텝을 스킵하고 패턴을 종료합니다 (조합 패턴용)")]
|
|
[SerializeField] private bool skipJumpStepOnNoTarget = false;
|
|
|
|
public string PatternName => patternName;
|
|
public PatternCategory Category => category;
|
|
public bool IsSignature => isSignature;
|
|
public bool IsMelee => isMelee;
|
|
public TargetResolveMode TargetMode => targetMode;
|
|
public IReadOnlyList<PatternStep> Steps => steps;
|
|
public float Cooldown => cooldown;
|
|
public int MinPhase => minPhase;
|
|
public bool SkipJumpStepOnNoTarget => skipJumpStepOnNoTarget;
|
|
|
|
/// <summary>
|
|
/// 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
|
|
}
|
|
}
|