摄像机区域的架构改动
This commit is contained in:
306
Assets/_Game/Scripts/Editor/Enemies/BossSkillSequenceWindow.cs
Normal file
306
Assets/_Game/Scripts/Editor/Enemies/BossSkillSequenceWindow.cs
Normal file
@@ -0,0 +1,306 @@
|
||||
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/Tools/Boss Skill Sequence Viewer")]
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d47145d394333184eb3ff822e3c4aa4d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
347
Assets/_Game/Scripts/Editor/Enemies/EnemyDataWindow.cs
Normal file
347
Assets/_Game/Scripts/Editor/Enemies/EnemyDataWindow.cs
Normal file
@@ -0,0 +1,347 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
using BaseGames.Enemies;
|
||||
|
||||
namespace BaseGames.Editor.Enemies
|
||||
{
|
||||
/// <summary>
|
||||
/// 敌人数据管理窗口(W-05)。
|
||||
/// 技术:UI Toolkit TwoPaneSplitView + 手动标签页。
|
||||
/// 菜单:BaseGames / Data / Enemy Data Manager
|
||||
///
|
||||
/// 左栏:可搜索的 EnemyStatsSO 列表 + [新建] 按钮。
|
||||
/// 右栏两个标签页:
|
||||
/// Stats — EnemyStatsSO 完整属性编辑
|
||||
/// Loot — LootTableSO 浏览与编辑
|
||||
/// </summary>
|
||||
public class EnemyDataWindow : EditorWindow
|
||||
{
|
||||
private static readonly StyleSheet _sharedUSS;
|
||||
|
||||
static EnemyDataWindow()
|
||||
{
|
||||
_sharedUSS = AssetDatabase.LoadAssetAtPath<StyleSheet>(
|
||||
"Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss");
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Data/Enemy Data Manager", priority = 102)]
|
||||
public static void Open()
|
||||
{
|
||||
var wnd = GetWindow<EnemyDataWindow>();
|
||||
wnd.titleContent = new GUIContent("Enemy Data Manager");
|
||||
wnd.minSize = new Vector2(720, 420);
|
||||
}
|
||||
|
||||
// ── 状态 ─────────────────────────────────────────────────────────────
|
||||
private List<EnemyStatsSO> _enemies = new();
|
||||
private List<EnemyStatsSO> _filtered = new();
|
||||
private List<LootTableSO> _lootTables = new();
|
||||
private List<LootTableSO> _lootFiltered = new();
|
||||
|
||||
private ListView _enemyList;
|
||||
private ListView _lootList;
|
||||
private VisualElement _detailRoot; // Stats 标签页 Loot 详情区
|
||||
private ScrollView _lootDetailRoot; // Loot 标签页 LootTable 详情区
|
||||
private VisualElement _tabStats;
|
||||
private VisualElement _tabLoot;
|
||||
private Button _btnStats;
|
||||
private Button _btnLoot;
|
||||
private string _searchText = "";
|
||||
private string _lootSearchText = "";
|
||||
private int _activeTab = 0; // 0=Stats, 1=Loot
|
||||
|
||||
private InspectorElement _statsInspector;
|
||||
private InspectorElement _lootInspector;
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────────
|
||||
|
||||
public void CreateGUI()
|
||||
{
|
||||
if (_sharedUSS != null)
|
||||
rootVisualElement.styleSheets.Add(_sharedUSS);
|
||||
|
||||
// Toolbar
|
||||
var toolbar = new Toolbar();
|
||||
var searchField = new ToolbarSearchField { style = { flexGrow = 1 } };
|
||||
searchField.RegisterValueChangedCallback(e => { _searchText = e.newValue; RefreshEnemyFilter(); });
|
||||
searchField.tooltip = "按名称 / ID 过滤 EnemyStatsSO 列表";
|
||||
toolbar.Add(searchField);
|
||||
|
||||
var btnCreate = new ToolbarButton(CreateNewEnemyStats) { text = "+ 新建敌人" };
|
||||
var btnRefresh = new ToolbarButton(RefreshAll) { text = "↺" };
|
||||
btnRefresh.tooltip = "重新扫描 Project 中的资产";
|
||||
toolbar.Add(btnCreate);
|
||||
toolbar.Add(btnRefresh);
|
||||
rootVisualElement.Add(toolbar);
|
||||
|
||||
// Split view
|
||||
var split = new TwoPaneSplitView(0, 230, TwoPaneSplitViewOrientation.Horizontal);
|
||||
|
||||
// ── 左栏:敌人列表 ────────────────────────────────────────────
|
||||
var leftPane = new VisualElement { style = { minWidth = 150 } };
|
||||
_enemyList = new ListView
|
||||
{
|
||||
selectionType = SelectionType.Single,
|
||||
fixedItemHeight = 22,
|
||||
makeItem = MakeEnemyItem,
|
||||
bindItem = BindEnemyItem,
|
||||
style = { flexGrow = 1 },
|
||||
};
|
||||
_enemyList.selectionChanged += OnEnemySelected;
|
||||
leftPane.Add(_enemyList);
|
||||
split.Add(leftPane);
|
||||
|
||||
// ── 右栏:标签页 + 内容 ───────────────────────────────────────
|
||||
var rightPane = new VisualElement { style = { flexGrow = 1 } };
|
||||
|
||||
// 标签页按钮栏
|
||||
var tabBar = new VisualElement();
|
||||
tabBar.AddToClassList("tab-bar");
|
||||
|
||||
_btnStats = new Button(() => ActivateTab(0)) { text = "Stats" };
|
||||
_btnLoot = new Button(() => ActivateTab(1)) { text = "Loot Table" };
|
||||
_btnStats.AddToClassList("tab-button");
|
||||
_btnLoot.AddToClassList("tab-button");
|
||||
tabBar.Add(_btnStats);
|
||||
tabBar.Add(_btnLoot);
|
||||
rightPane.Add(tabBar);
|
||||
|
||||
// Stats 面板
|
||||
_tabStats = new ScrollView { style = { flexGrow = 1 } };
|
||||
_tabStats.AddToClassList("detail-panel");
|
||||
rightPane.Add(_tabStats);
|
||||
|
||||
// Loot 面板(初始隐藏)
|
||||
_tabLoot = BuildLootPanel();
|
||||
_tabLoot.style.display = DisplayStyle.None;
|
||||
rightPane.Add(_tabLoot);
|
||||
|
||||
split.Add(rightPane);
|
||||
rootVisualElement.Add(split);
|
||||
|
||||
ActivateTab(0);
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
private void OnFocus() => RefreshAll();
|
||||
|
||||
// ── 标签页切换 ────────────────────────────────────────────────────────
|
||||
|
||||
private void ActivateTab(int tab)
|
||||
{
|
||||
_activeTab = tab;
|
||||
|
||||
_tabStats.style.display = tab == 0 ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
_tabLoot.style.display = tab == 1 ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
|
||||
_btnStats.EnableInClassList("tab-button--active", tab == 0);
|
||||
_btnLoot.EnableInClassList("tab-button--active", tab == 1);
|
||||
}
|
||||
|
||||
// ── 敌人列表 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void RefreshAll()
|
||||
{
|
||||
_enemies = EditorScaffoldUtils.FindAllAssetsOfType<EnemyStatsSO>();
|
||||
_enemies.Sort((a, b) => string.Compare(a.name, b.name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
_lootTables = EditorScaffoldUtils.FindAllAssetsOfType<LootTableSO>();
|
||||
_lootTables.Sort((a, b) => string.Compare(a.name, b.name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
RefreshEnemyFilter();
|
||||
RefreshLootFilter();
|
||||
}
|
||||
|
||||
private void RefreshEnemyFilter()
|
||||
{
|
||||
_filtered = string.IsNullOrEmpty(_searchText)
|
||||
? new List<EnemyStatsSO>(_enemies)
|
||||
: _enemies.Where(e => e != null &&
|
||||
e.name.Contains(_searchText, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
_enemyList.itemsSource = _filtered;
|
||||
_enemyList.Rebuild();
|
||||
}
|
||||
|
||||
private static VisualElement MakeEnemyItem()
|
||||
{
|
||||
var label = new Label();
|
||||
label.AddToClassList("list-item");
|
||||
return label;
|
||||
}
|
||||
|
||||
private void BindEnemyItem(VisualElement element, int index)
|
||||
{
|
||||
var label = (Label)element;
|
||||
var enemy = _filtered.Count > index ? _filtered[index] : null;
|
||||
label.text = enemy != null ? enemy.name : "(null)";
|
||||
}
|
||||
|
||||
private void OnEnemySelected(IEnumerable<object> items)
|
||||
{
|
||||
_tabStats.Clear();
|
||||
_statsInspector = null;
|
||||
|
||||
var enemy = items.FirstOrDefault() as EnemyStatsSO;
|
||||
if (enemy == null) return;
|
||||
|
||||
// 数值快览条
|
||||
BuildStatsPreview(enemy);
|
||||
|
||||
// 完整属性编辑
|
||||
_statsInspector = new InspectorElement(enemy);
|
||||
_tabStats.Add(_statsInspector);
|
||||
|
||||
// 操作按钮
|
||||
var btnRow = new VisualElement();
|
||||
btnRow.AddToClassList("action-buttons");
|
||||
btnRow.Add(new Button(() => EditorScaffoldUtils.PingAndSelect(enemy)) { text = "在 Project 中定位" });
|
||||
btnRow.Add(new Button(() => Selection.activeObject = enemy) { text = "在 Inspector 中打开" });
|
||||
btnRow.Add(new Button(() => CloneEnemy(enemy)) { text = "克隆为变体…" });
|
||||
_tabStats.Add(btnRow);
|
||||
}
|
||||
|
||||
private void BuildStatsPreview(EnemyStatsSO e)
|
||||
{
|
||||
var row = new VisualElement();
|
||||
row.AddToClassList("stats-preview");
|
||||
|
||||
void Stat(string label, string val)
|
||||
{
|
||||
row.Add(new Label(label) { style = { color = new Color(0.65f, 0.65f, 0.65f), marginRight = 3 } });
|
||||
row.Add(new Label(val) { style = { marginRight = 14, unityFontStyleAndWeight = FontStyle.Bold } });
|
||||
}
|
||||
|
||||
Stat("HP:", $"{e.MaxHP}");
|
||||
Stat("DEF:", $"{e.Defense}");
|
||||
Stat("ATK:", $"{e.AttackDamage}");
|
||||
Stat("SPD:", $"{e.WalkSpeed}/{e.RunSpeed}");
|
||||
Stat("范围:", $"{e.AttackRange:F1}");
|
||||
Stat("视野:", $"{e.DetectRange:F1}");
|
||||
|
||||
_tabStats.Add(row);
|
||||
}
|
||||
|
||||
private void CloneEnemy(EnemyStatsSO source)
|
||||
{
|
||||
string name = source.name;
|
||||
string clone = EditorUtility.SaveFilePanelInProject(
|
||||
"克隆敌人配置", $"{name}_Clone", "asset",
|
||||
"选择克隆 EnemyStatsSO 的保存路径");
|
||||
if (string.IsNullOrEmpty(clone)) return;
|
||||
|
||||
var asset = Instantiate(source);
|
||||
AssetDatabase.CreateAsset(asset, clone);
|
||||
AssetDatabase.SaveAssets();
|
||||
EditorScaffoldUtils.PingAndSelect(asset);
|
||||
RefreshAll();
|
||||
}
|
||||
|
||||
// ── Loot Table 面板 ───────────────────────────────────────────────────
|
||||
|
||||
private VisualElement BuildLootPanel()
|
||||
{
|
||||
var container = new VisualElement { style = { flexGrow = 1 } };
|
||||
|
||||
// Loot 搜索栏
|
||||
var lootToolbar = new Toolbar();
|
||||
var lootSearch = new ToolbarSearchField { style = { flexGrow = 1 } };
|
||||
lootSearch.RegisterValueChangedCallback(e => { _lootSearchText = e.newValue; RefreshLootFilter(); });
|
||||
lootSearch.tooltip = "过滤 LootTableSO 列表";
|
||||
lootToolbar.Add(lootSearch);
|
||||
|
||||
var btnCreateLoot = new ToolbarButton(CreateNewLootTable) { text = "+ 新建 LootTable" };
|
||||
lootToolbar.Add(btnCreateLoot);
|
||||
container.Add(lootToolbar);
|
||||
|
||||
// 左右分割:Loot 列表 + Loot 详情
|
||||
var lootSplit = new TwoPaneSplitView(0, 200, TwoPaneSplitViewOrientation.Horizontal);
|
||||
|
||||
var lootLeft = new VisualElement { style = { minWidth = 120 } };
|
||||
_lootList = new ListView
|
||||
{
|
||||
selectionType = SelectionType.Single,
|
||||
fixedItemHeight = 22,
|
||||
makeItem = () => { var l = new Label(); l.AddToClassList("list-item"); return l; },
|
||||
bindItem = (el, idx) =>
|
||||
{
|
||||
var lbl = (Label)el;
|
||||
var loot = _lootFiltered.Count > idx ? _lootFiltered[idx] : null;
|
||||
lbl.text = loot?.name ?? "(null)";
|
||||
},
|
||||
style = { flexGrow = 1 },
|
||||
};
|
||||
_lootList.selectionChanged += OnLootSelected;
|
||||
lootLeft.Add(_lootList);
|
||||
lootSplit.Add(lootLeft);
|
||||
|
||||
_lootDetailRoot = new ScrollView { style = { flexGrow = 1 } };
|
||||
_lootDetailRoot.AddToClassList("detail-panel");
|
||||
lootSplit.Add(_lootDetailRoot);
|
||||
|
||||
container.Add(lootSplit);
|
||||
return container;
|
||||
}
|
||||
|
||||
private void RefreshLootFilter()
|
||||
{
|
||||
_lootFiltered = string.IsNullOrEmpty(_lootSearchText)
|
||||
? new List<LootTableSO>(_lootTables)
|
||||
: _lootTables.Where(l => l != null &&
|
||||
l.name.Contains(_lootSearchText, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
_lootList.itemsSource = _lootFiltered;
|
||||
_lootList.Rebuild();
|
||||
}
|
||||
|
||||
private void OnLootSelected(IEnumerable<object> items)
|
||||
{
|
||||
_lootDetailRoot.Clear();
|
||||
_lootInspector = null;
|
||||
|
||||
var loot = items.FirstOrDefault() as LootTableSO;
|
||||
if (loot == null) return;
|
||||
|
||||
var title = new Label($"Loot:{loot.name}")
|
||||
{
|
||||
style = { fontSize = 13, unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 6 }
|
||||
};
|
||||
_lootDetailRoot.Add(title);
|
||||
|
||||
// 简要统计
|
||||
int entryCount = loot.Entries?.Length ?? 0;
|
||||
_lootDetailRoot.Add(new Label($"条目数:{entryCount} 保底 LingZhu:{loot.GuaranteedLingZhuMin}–{loot.GuaranteedLingZhuMax}")
|
||||
{
|
||||
style = { color = new Color(0.7f, 0.7f, 0.7f), marginBottom = 4 }
|
||||
});
|
||||
|
||||
_lootInspector = new InspectorElement(loot);
|
||||
_lootDetailRoot.Add(_lootInspector);
|
||||
|
||||
var btnRow = new VisualElement();
|
||||
btnRow.AddToClassList("action-buttons");
|
||||
btnRow.Add(new Button(() => EditorScaffoldUtils.PingAndSelect(loot)) { text = "在 Project 中定位" });
|
||||
btnRow.Add(new Button(() => Selection.activeObject = loot) { text = "在 Inspector 中打开" });
|
||||
_lootDetailRoot.Add(btnRow);
|
||||
}
|
||||
|
||||
// ── 新建资产 ──────────────────────────────────────────────────────────
|
||||
|
||||
private void CreateNewEnemyStats()
|
||||
{
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<EnemyStatsSO>(
|
||||
"Assets/_Game/Data/Enemies", "EnemyStatsSO_New");
|
||||
if (asset != null) RefreshAll();
|
||||
}
|
||||
|
||||
private void CreateNewLootTable()
|
||||
{
|
||||
var asset = EditorScaffoldUtils.CreateSOAsset<LootTableSO>(
|
||||
"Assets/_Game/Data/Enemies", "LootTableSO_New");
|
||||
if (asset != null) RefreshAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/Enemies/EnemyDataWindow.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Enemies/EnemyDataWindow.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3a95bf3e8be76e44881b0efa6a42f753
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user