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>
307 lines
13 KiB
C#
307 lines
13 KiB
C#
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 < 0.1 时阶段条变红警告
|
||
/// - 点击阶段条高亮对应 AttackPatternSO(EditorGUIUtility.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();
|
||
}
|
||
}
|
||
}
|