Files
zeling_v2/Assets/_Game/Scripts/Editor/Enemies/EnemyDataWindow.cs
2026-05-20 19:55:11 +08:00

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