- QuestSO: Add ValidateBranchCycles() DFS detection for branches[].nextQuest loop - QuestSO: Mark three legacy prerequisite fields with v2.0 removal warning in Tooltip - IQuestManager: Add QuestLockReason enum + QuestLockInfo struct (strongly-typed lock info) - IQuestManager: Add GetQuestLockInfo() method to interface; GetQuestLockReason() now delegates to it - IQuestEventSource: Add OnQuestStateChanged(questId, oldState, newState) unified event - QuestManager: Implement GetQuestLockInfo(); fire OnQuestStateChanged on all state transitions - DialogueManager: Add one-frame yield in HandleChoices before ShowChoices (skip-debounce fix) - DialogueManager: Increment _playbackId in ForceEnd() to invalidate residual choice callbacks - DialogueSequenceSO: Add UNITY_EDITOR debug log in TryGetActiveVariant on variant match - WorldStateRegistry: Add OnBatchStateChanged event + BatchMark() batch-write API - DialogueModule: List badge shows warning indicator for unconditional-shadowing variants - DialogueModule: BuildVariantsCard shows logic mode (AND/OR) alongside flag conditions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
205 lines
8.3 KiB
C#
205 lines
8.3 KiB
C#
using System;
|
|
using UnityEditor;
|
|
using UnityEditor.UIElements;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
using BaseGames.Enemies;
|
|
|
|
namespace BaseGames.Editor.Modules
|
|
{
|
|
/// <summary>
|
|
/// DataHub 敌人模块 —— Tab 切换管理 EnemyStatsSO 和 LootTableSO。
|
|
/// </summary>
|
|
public class EnemyModule : IDataModule, IDataModuleOrdered
|
|
{
|
|
private const string StatsFolder = "Assets/_Game/Data/Enemies/Stats";
|
|
private const string LootFolder = "Assets/_Game/Data/Enemies/Loot";
|
|
|
|
public string ModuleId => "enemy";
|
|
public string DisplayName => "敌人";
|
|
public string IconName => null;
|
|
public int DisplayOrder => 30;
|
|
|
|
private int _activeTab = 0; // 0=Stats, 1=Loot
|
|
|
|
private SoListPane<EnemyStatsSO> _statsPane;
|
|
private SoListPane<LootTableSO> _lootPane;
|
|
private VisualElement _listContainer;
|
|
private Action<UnityEngine.Object> _onSelected;
|
|
|
|
private DetailHeader _header;
|
|
private EnemyStatsSO _selectedStats;
|
|
private LootTableSO _selectedLoot;
|
|
|
|
public void Initialize()
|
|
{
|
|
_statsPane = new SoListPane<EnemyStatsSO>(StatsFolder, "ENM_");
|
|
_statsPane.SelectionChanged = s => { _selectedStats = s; _onSelected?.Invoke(s); };
|
|
|
|
_lootPane = new SoListPane<LootTableSO>(LootFolder, "ENM_Loot_");
|
|
_lootPane.SelectionChanged = l => { _selectedLoot = l; _onSelected?.Invoke(l); };
|
|
}
|
|
|
|
public void BuildListPane(VisualElement container, Action<UnityEngine.Object> onSelected)
|
|
{
|
|
_onSelected = onSelected;
|
|
_listContainer = container;
|
|
|
|
container.style.flexDirection = FlexDirection.Column;
|
|
|
|
// Tab bar
|
|
var tabBar = new VisualElement();
|
|
tabBar.style.flexDirection = FlexDirection.Row;
|
|
tabBar.style.borderBottomWidth = 1;
|
|
tabBar.style.borderBottomColor = new StyleColor(new Color(0.5f, 0.5f, 0.5f, 0.3f));
|
|
container.Add(tabBar);
|
|
|
|
var btnStats = BuildTabBtn("属性 (Stats)", 0, tabBar);
|
|
var btnLoot = BuildTabBtn("掉落 (Loot)", 1, tabBar);
|
|
|
|
// 列表区域占位
|
|
var listArea = new VisualElement();
|
|
listArea.style.flexGrow = 1;
|
|
container.Add(listArea);
|
|
|
|
ShowTab(0, listArea, new[] { btnStats, btnLoot });
|
|
|
|
btnStats.clicked += () => ShowTab(0, listArea, new[] { btnStats, btnLoot });
|
|
btnLoot.clicked += () => ShowTab(1, listArea, new[] { btnStats, btnLoot });
|
|
|
|
_statsPane.Refresh();
|
|
_lootPane.Refresh();
|
|
}
|
|
|
|
public void BuildDetailPane(VisualElement container, UnityEngine.Object selected)
|
|
{
|
|
_header = new DetailHeader();
|
|
_header.SetAsset(selected);
|
|
_header.RenameRequested += name => OnRenameRequested(selected, name);
|
|
container.Add(_header);
|
|
|
|
if (selected == null) return;
|
|
|
|
if (selected is EnemyStatsSO stats)
|
|
{
|
|
container.Add(BuildStatsCard(stats));
|
|
container.Add(BuildActionBar(stats, StatsFolder, _statsPane));
|
|
container.Add(SkillModule.MakeDivider());
|
|
var insp = new InspectorElement(stats); container.Add(insp);
|
|
}
|
|
else if (selected is LootTableSO loot)
|
|
{
|
|
container.Add(BuildLootCard(loot));
|
|
container.Add(BuildActionBar(loot, LootFolder, _lootPane));
|
|
container.Add(SkillModule.MakeDivider());
|
|
var insp = new InspectorElement(loot); container.Add(insp);
|
|
}
|
|
}
|
|
|
|
public void OnActivated()
|
|
{
|
|
_statsPane?.Refresh();
|
|
_lootPane?.Refresh();
|
|
}
|
|
|
|
// ── 内部 ─────────────────────────────────────────────────────────────
|
|
|
|
private Button BuildTabBtn(string text, int tabIdx, VisualElement bar)
|
|
{
|
|
var btn = new Button { text = text };
|
|
btn.style.flexGrow = 1;
|
|
btn.style.paddingTop = 5;
|
|
btn.style.paddingBottom = 5;
|
|
btn.style.borderTopLeftRadius = 0;
|
|
btn.style.borderTopRightRadius = 0;
|
|
btn.style.borderBottomLeftRadius = 0;
|
|
btn.style.borderBottomRightRadius = 0;
|
|
btn.style.borderLeftWidth = 0;
|
|
btn.style.borderRightWidth = 0;
|
|
btn.style.borderTopWidth = 0;
|
|
btn.style.borderBottomWidth = 0;
|
|
btn.style.backgroundColor = new StyleColor(Color.clear);
|
|
btn.userData = tabIdx;
|
|
bar.Add(btn);
|
|
return btn;
|
|
}
|
|
|
|
private void ShowTab(int tab, VisualElement area, Button[] tabBtns)
|
|
{
|
|
_activeTab = tab;
|
|
area.Clear();
|
|
|
|
for (int i = 0; i < tabBtns.Length; i++)
|
|
{
|
|
if (i == tab)
|
|
{
|
|
tabBtns[i].style.borderBottomWidth = 2;
|
|
tabBtns[i].style.borderBottomColor = new StyleColor(new Color(0.4f, 0.65f, 1f, 1f));
|
|
tabBtns[i].style.opacity = 1f;
|
|
}
|
|
else
|
|
{
|
|
tabBtns[i].style.borderBottomWidth = 0;
|
|
tabBtns[i].style.opacity = 0.65f;
|
|
}
|
|
}
|
|
|
|
if (tab == 0) { _statsPane.style.flexGrow = 1; area.Add(_statsPane); }
|
|
else { _lootPane.style.flexGrow = 1; area.Add(_lootPane); }
|
|
}
|
|
|
|
private void OnRenameRequested(UnityEngine.Object asset, string newName)
|
|
{
|
|
var (ok, err) = AssetOperations.Rename(asset, newName);
|
|
if (!ok) EditorUtility.DisplayDialog("重命名失败", err, "确定");
|
|
else
|
|
{
|
|
_header.SetAsset(asset);
|
|
if (_activeTab == 0) _statsPane.Invalidate();
|
|
else _lootPane.Invalidate();
|
|
}
|
|
}
|
|
|
|
private static VisualElement BuildStatsCard(EnemyStatsSO s)
|
|
{
|
|
var card = SkillModule.MakeCard();
|
|
SkillModule.AddChip(card, "HP", s.MaxHP.ToString());
|
|
SkillModule.AddChip(card, "防御", s.Defense.ToString());
|
|
SkillModule.AddChip(card, "移速", $"{s.WalkSpeed}/{s.RunSpeed}");
|
|
SkillModule.AddChip(card, "攻击", s.AttackDamage.ToString());
|
|
SkillModule.AddChip(card, "感知", $"{s.DetectRange}m");
|
|
return card;
|
|
}
|
|
|
|
private static VisualElement BuildLootCard(LootTableSO l)
|
|
{
|
|
var card = SkillModule.MakeCard();
|
|
SkillModule.AddChip(card, "掉落项", (l.Entries?.Length ?? 0).ToString());
|
|
SkillModule.AddChip(card, "灵珠保底", $"{l.GuaranteedLingZhuMin}-{l.GuaranteedLingZhuMax}");
|
|
return card;
|
|
}
|
|
|
|
private VisualElement BuildActionBar<T>(T asset, string folder, SoListPane<T> pane)
|
|
where T : ScriptableObject
|
|
{
|
|
var bar = SkillModule.MakeActionBar();
|
|
new Button(() => { EditorGUIUtility.PingObject(asset); Selection.activeObject = asset; })
|
|
{ text = "定位" }.AlsoAddTo(bar);
|
|
new Button(() => { var c = AssetOperations.Clone(asset, folder); if (c != null) pane.Refresh(c); })
|
|
{ text = "克隆..." }.AlsoAddTo(bar);
|
|
var del = new Button(() => { if (AssetOperations.Delete(asset)) pane.Refresh(null); }) { text = "删除" };
|
|
del.style.borderLeftColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
|
|
del.style.borderRightColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
|
|
del.style.borderTopColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
|
|
del.style.borderBottomColor = new StyleColor(new Color(0.8f, 0.3f, 0.3f, 0.6f));
|
|
del.style.borderLeftWidth = 1;
|
|
del.style.borderRightWidth = 1;
|
|
del.style.borderTopWidth = 1;
|
|
del.style.borderBottomWidth = 1;
|
|
del.style.marginLeft = 8;
|
|
del.AlsoAddTo(bar);
|
|
return bar;
|
|
}
|
|
}
|
|
}
|