348 lines
14 KiB
C#
348 lines
14 KiB
C#
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", "ENM_New_Stats");
|
||
if (asset != null) RefreshAll();
|
||
}
|
||
|
||
private void CreateNewLootTable()
|
||
{
|
||
var asset = EditorScaffoldUtils.CreateSOAsset<LootTableSO>(
|
||
"Assets/_Game/Data/Enemies", "ENM_New_Loot");
|
||
if (asset != null) RefreshAll();
|
||
}
|
||
}
|
||
}
|