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 { /// /// 敌人数据管理窗口(W-05)。 /// 技术:UI Toolkit TwoPaneSplitView + 手动标签页。 /// 菜单:BaseGames / Data / Enemy Data Manager /// /// 左栏:可搜索的 EnemyStatsSO 列表 + [新建] 按钮。 /// 右栏两个标签页: /// Stats — EnemyStatsSO 完整属性编辑 /// Loot — LootTableSO 浏览与编辑 /// public class EnemyDataWindow : EditorWindow { private static readonly StyleSheet _sharedUSS; static EnemyDataWindow() { _sharedUSS = AssetDatabase.LoadAssetAtPath( "Assets/_Game/Scripts/Editor/UIToolkit/Editor.uss"); } [MenuItem("BaseGames/Data/Enemy Data Manager", priority = 102)] public static void Open() { var wnd = GetWindow(); wnd.titleContent = new GUIContent("Enemy Data Manager"); wnd.minSize = new Vector2(720, 420); } // ── 状态 ───────────────────────────────────────────────────────────── private List _enemies = new(); private List _filtered = new(); private List _lootTables = new(); private List _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(); _enemies.Sort((a, b) => string.Compare(a.name, b.name, StringComparison.OrdinalIgnoreCase)); _lootTables = EditorScaffoldUtils.FindAllAssetsOfType(); _lootTables.Sort((a, b) => string.Compare(a.name, b.name, StringComparison.OrdinalIgnoreCase)); RefreshEnemyFilter(); RefreshLootFilter(); } private void RefreshEnemyFilter() { _filtered = string.IsNullOrEmpty(_searchText) ? new List(_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 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(_lootTables) : _lootTables.Where(l => l != null && l.name.Contains(_lootSearchText, StringComparison.OrdinalIgnoreCase)).ToList(); _lootList.itemsSource = _lootFiltered; _lootList.Rebuild(); } private void OnLootSelected(IEnumerable 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( "Assets/_Game/Data/Enemies", "EnemyStatsSO_New"); if (asset != null) RefreshAll(); } private void CreateNewLootTable() { var asset = EditorScaffoldUtils.CreateSOAsset( "Assets/_Game/Data/Enemies", "LootTableSO_New"); if (asset != null) RefreshAll(); } } }