using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using BaseGames.Boss;
namespace BaseGames.Editor
{
///
/// Boss 技能序列甘特图可视化窗口(架构 23_BossSkillModule §12)。
/// 菜单:BaseGames/Tools/Boss Skill Sequence Viewer
///
/// 功能:
/// - 拖放 BossSkillSO 或 SkillSequenceSO 资产加载
/// - 甘特图:Windup(黄色)→ Active(红色)→ Recovery(灰色)各阶段时序条
/// - VulnerabilityWindow 绿色覆盖层(TriggerDelay 偏移 + Duration 宽度)
/// - DurationNormalized < 0.1 时阶段条变红警告
/// - 点击阶段条高亮对应 AttackPatternSO(EditorGUIUtility.PingObject)
///
public class BossSkillSequenceWindow : EditorWindow
{
// ── State ──────────────────────────────────────────────────────────
private BossSkillSO _loadedSkill;
private SkillSequenceSO _loadedSequence;
private Vector2 _scrollPos;
// ── Layout ─────────────────────────────────────────────────────────
private const float HeaderH = 24f;
private const float RowH = 28f;
private const float LabelW = 180f;
private const float MinBarWidth = 6f;
// 时间轴宽度随窗口宽度动态调整,最小 300px
private float TimelineW => Mathf.Max(300f, position.width - LabelW - 30f);
// ── Colors ─────────────────────────────────────────────────────────
private static readonly Color ColWindup = new Color(0.95f, 0.80f, 0.10f, 0.85f);
private static readonly Color ColActive = new Color(0.90f, 0.20f, 0.15f, 0.85f);
private static readonly Color ColRecovery = new Color(0.50f, 0.50f, 0.55f, 0.70f);
private static readonly Color ColVuln = new Color(0.10f, 0.90f, 0.30f, 0.45f);
private static readonly Color ColDelay = new Color(0.25f, 0.25f, 0.30f, 0.50f);
private static readonly Color ColWarn = new Color(0.95f, 0.10f, 0.10f, 0.85f);
[MenuItem("BaseGames/Tools/Boss Skill Sequence Viewer")]
public static void OpenWindow()
{
var win = GetWindow("Boss Skill Sequence");
win.minSize = new Vector2(900, 400);
win.Show();
}
// ── GUI ────────────────────────────────────────────────────────────
private void OnGUI()
{
DrawToolbar();
if (_loadedSkill == null && _loadedSequence == null)
{
EditorGUILayout.HelpBox(
"将 BossSkillSO 或 SkillSequenceSO 资产拖放到此处,或使用上方字段加载。",
MessageType.Info);
HandleDragDrop();
return;
}
_scrollPos = EditorGUILayout.BeginScrollView(_scrollPos);
if (_loadedSkill != null)
DrawSkillTimeline(_loadedSkill);
else if (_loadedSequence != null)
DrawSequenceTimeline(_loadedSequence);
EditorGUILayout.EndScrollView();
}
// ── Toolbar ───────────────────────────────────────────────────────
private void DrawToolbar()
{
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
EditorGUILayout.LabelField("技能:", GUILayout.Width(36));
var newSkill = (BossSkillSO)EditorGUILayout.ObjectField(
_loadedSkill, typeof(BossSkillSO), false, GUILayout.Width(200));
if (newSkill != _loadedSkill)
{
_loadedSkill = newSkill;
_loadedSequence = null;
}
GUILayout.Space(12);
EditorGUILayout.LabelField("序列:", GUILayout.Width(36));
var newSeq = (SkillSequenceSO)EditorGUILayout.ObjectField(
_loadedSequence, typeof(SkillSequenceSO), false, GUILayout.Width(200));
if (newSeq != _loadedSequence)
{
_loadedSequence = newSeq;
_loadedSkill = null;
}
GUILayout.FlexibleSpace();
if (GUILayout.Button("清除", EditorStyles.toolbarButton, GUILayout.Width(50)))
{
_loadedSkill = null;
_loadedSequence = null;
}
EditorGUILayout.EndHorizontal();
}
// ── BossSkillSO 时间轴 ────────────────────────────────────────────
private void DrawSkillTimeline(BossSkillSO skill)
{
EditorGUILayout.LabelField($"技能:{skill.displayName} [{skill.skillId}]",
EditorStyles.boldLabel);
EditorGUILayout.Space(4);
if (skill.attackPatterns == null || skill.attackPatterns.Length == 0)
{
EditorGUILayout.HelpBox("此技能没有 AttackPattern。", MessageType.Warning);
return;
}
// 计算总时长
float totalDuration = 0f;
foreach (var p in skill.attackPatterns)
if (p != null) totalDuration += p.WindupDuration + p.ActiveDuration + p.RecoveryDuration;
if (totalDuration <= 0f) totalDuration = 1f;
DrawTimelineHeader(totalDuration);
float cursor = 0f;
for (int i = 0; i < skill.attackPatterns.Length; i++)
{
var pattern = skill.attackPatterns[i];
if (pattern == null) continue;
DrawPatternRow($"[{i}] {pattern.name}", pattern, ref cursor, totalDuration);
}
// 绘制 VulnerabilityWindows
if (skill.vulnerabilityWindows != null && skill.vulnerabilityWindows.Length > 0)
{
EditorGUILayout.Space(4);
EditorGUILayout.LabelField("弱点窗口(Vulnerability Windows)", EditorStyles.miniBoldLabel);
foreach (var vw in skill.vulnerabilityWindows)
DrawVulnWindowRow(vw, totalDuration);
}
}
// ── SkillSequenceSO 时间轴 ────────────────────────────────────────
private void DrawSequenceTimeline(SkillSequenceSO sequence)
{
EditorGUILayout.LabelField($"序列:{sequence.name}", EditorStyles.boldLabel);
EditorGUILayout.Space(4);
if (sequence.steps == null || sequence.steps.Length == 0)
{
EditorGUILayout.HelpBox("此序列没有步骤。", MessageType.Warning);
return;
}
// 计算总时长
float totalDuration = 0f;
foreach (var step in sequence.steps)
{
totalDuration += step.delayBeforeStep;
if (step.pattern != null)
totalDuration += step.pattern.WindupDuration + step.pattern.ActiveDuration + step.pattern.RecoveryDuration;
}
if (totalDuration <= 0f) totalDuration = 1f;
DrawTimelineHeader(totalDuration);
float cursor = 0f;
for (int i = 0; i < sequence.steps.Length; i++)
{
var step = sequence.steps[i];
// 延迟条
if (step.delayBeforeStep > 0f)
{
DrawBar($"延迟 {step.delayBeforeStep:F2}s", cursor, step.delayBeforeStep,
totalDuration, ColDelay, null);
cursor += step.delayBeforeStep;
}
if (step.pattern != null)
DrawPatternRow($"[{i}] {step.pattern.name}", step.pattern, ref cursor, totalDuration);
}
}
// ── 共用绘制方法 ──────────────────────────────────────────────────
private void DrawTimelineHeader(float totalDuration)
{
Rect headerRect = EditorGUILayout.GetControlRect(false, HeaderH);
headerRect.x += LabelW;
headerRect.width -= LabelW;
EditorGUI.DrawRect(headerRect, new Color(0.18f, 0.18f, 0.20f));
// 刻度线(每 0.5s 一条)
float step = 0.5f;
for (float t = 0; t <= totalDuration + 0.001f; t += step)
{
float x = headerRect.x + (t / totalDuration) * headerRect.width;
EditorGUI.DrawRect(new Rect(x, headerRect.y, 1f, HeaderH * 0.6f), Color.gray);
EditorGUI.LabelField(new Rect(x + 2f, headerRect.y, 40f, HeaderH),
$"{t:F1}s", new GUIStyle(EditorStyles.miniLabel) { normal = { textColor = Color.gray } });
}
}
private void DrawPatternRow(string label, AttackPatternSO pattern, ref float cursor, float totalDuration)
{
float windupDur = pattern.WindupDuration;
float activeDur = pattern.ActiveDuration;
float recoveryDur = pattern.RecoveryDuration;
float rowStart = cursor;
EditorGUILayout.BeginHorizontal(GUILayout.Height(RowH));
// 标签 + Ping
if (GUILayout.Button(label, EditorStyles.miniLabel, GUILayout.Width(LabelW), GUILayout.Height(RowH)))
EditorGUIUtility.PingObject(pattern);
Rect timelineRect = EditorGUILayout.GetControlRect(false, RowH,
GUILayout.Width(TimelineW));
// Windup
if (windupDur > 0f)
DrawBarInRect(timelineRect, cursor, windupDur, totalDuration,
windupDur / (windupDur + activeDur + recoveryDur) < 0.1f ? ColWarn : ColWindup);
cursor += windupDur;
// Active
if (activeDur > 0f)
DrawBarInRect(timelineRect, cursor, activeDur, totalDuration,
activeDur / (windupDur + activeDur + recoveryDur) < 0.1f ? ColWarn : ColActive);
cursor += activeDur;
// Recovery
if (recoveryDur > 0f)
DrawBarInRect(timelineRect, cursor, recoveryDur, totalDuration,
recoveryDur / (windupDur + activeDur + recoveryDur) < 0.1f ? ColWarn : ColRecovery);
cursor += recoveryDur;
_ = rowStart; // suppress unused warning
EditorGUILayout.EndHorizontal();
}
private void DrawVulnWindowRow(VulnerabilityWindow vw, float totalDuration)
{
string label = $"弱点:{vw.TriggerType} +{vw.TriggerDelay:F2}s / {vw.Duration:F2}s";
DrawBar(label, vw.TriggerDelay, vw.Duration, totalDuration, ColVuln, null);
}
private void DrawBar(string label, float start, float duration, float totalDuration,
Color color, AttackPatternSO pingTarget)
{
EditorGUILayout.BeginHorizontal(GUILayout.Height(RowH));
if (GUILayout.Button(label, EditorStyles.miniLabel, GUILayout.Width(LabelW), GUILayout.Height(RowH)))
{
if (pingTarget != null) EditorGUIUtility.PingObject(pingTarget);
}
Rect timelineRect = EditorGUILayout.GetControlRect(false, RowH, GUILayout.Width(TimelineW));
DrawBarInRect(timelineRect, start, duration, totalDuration, color);
EditorGUILayout.EndHorizontal();
}
private static void DrawBarInRect(Rect timeline, float start, float duration,
float totalDuration, Color color)
{
float x = timeline.x + (start / totalDuration) * timeline.width;
float w = Mathf.Max(MinBarWidth, (duration / totalDuration) * timeline.width);
EditorGUI.DrawRect(new Rect(x, timeline.y + 2f, w, timeline.height - 4f), color);
}
// ── Drag & Drop ───────────────────────────────────────────────────
private void HandleDragDrop()
{
var evt = Event.current;
if (evt.type != EventType.DragUpdated && evt.type != EventType.DragPerform) return;
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
if (evt.type == EventType.DragPerform)
{
DragAndDrop.AcceptDrag();
foreach (var obj in DragAndDrop.objectReferences)
{
if (obj is BossSkillSO skill) { _loadedSkill = skill; _loadedSequence = null; break; }
if (obj is SkillSequenceSO seq) { _loadedSequence = seq; _loadedSkill = null; break; }
}
Repaint();
}
evt.Use();
}
}
}