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(); } } }