Files
zeling_v2/Assets/_Game/Scripts/Editor/Enemies/BossSkillSequenceWindow.cs
Joywayer 82ce9ff09a refactor(editor): reorganize Editor directory and unify menu hierarchy
File directory changes (mirror Scripts/ module structure):
- AbilityTypeDrawer.cs         → Equipment/
- CharacterWizardWindow.cs     → Character/
- FormEditorWindow.cs          → Player/
- GMToolWindow.cs              → Tools/
- SOManagerWindow.cs           → Tools/
- Map/MapRoomDataEditor.cs     → World/Map/
- Navigation/ (root)           → Enemies/Navigation/
- Achievements/                → Progression/

Menu hierarchy changes (BaseGames/ top-level):
- Data/: +Character Wizard (from Tools/), +Boss Skill Sequence (from Tools/)
- Addressables/: +Addressable Batch Tool, +Asset Reference Graph, +Validate Address Keys (from Tools/Verification/)
- Scene/Setup/: +Boot Flow Wizard, +Scaffold *, +Auto-Open Persistent (from Tools/)
- Scene/: +Camera Area Setup (from Camera/), +Bake All NavSurfaces (from Tools/)
- Events/: +Event Bus Monitor, +Event Chain Viewer, +Create/Reimport Event Channels (from Tools/)
- Tools/Validation/: +Validate All SOs, +Apply/Validate Script Order (from Tools/ flat)
- Tools/Maintenance/: +Missing Scripts/*, +Physics2D Layer Matrix/* (from Tools/ flat)

Result: BaseGames/Tools/ reduced from 16 flat items to 4 items + 2 submenus

Docs: update AssetFolderSpec §12 editor tool table with new menu paths

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-20 11:52:17 +08:00

307 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using BaseGames.Boss;
namespace BaseGames.Editor
{
/// <summary>
/// Boss 技能序列甘特图可视化窗口(架构 23_BossSkillModule §12
/// 菜单BaseGames/Tools/Boss Skill Sequence Viewer
///
/// 功能:
/// - 拖放 BossSkillSO 或 SkillSequenceSO 资产加载
/// - 甘特图Windup黄色→ Active红色→ Recovery灰色各阶段时序条
/// - VulnerabilityWindow 绿色覆盖层TriggerDelay 偏移 + Duration 宽度)
/// - DurationNormalized &lt; 0.1 时阶段条变红警告
/// - 点击阶段条高亮对应 AttackPatternSOEditorGUIUtility.PingObject
/// </summary>
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/Data/Boss Skill Sequence", priority = 110)]
public static void OpenWindow()
{
var win = GetWindow<BossSkillSequenceWindow>("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();
}
}
}