feat: Round 48 narrative systems improvements

- 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>
This commit is contained in:
2026-05-25 00:05:15 +08:00
parent 446fd5dcd0
commit 6eaa83dc71
72 changed files with 7080 additions and 373 deletions

View File

@@ -37,6 +37,9 @@ namespace BaseGames.Editor
private IDataModule _activeModule;
private VisualElement _navSidebar;
// NavBar 搜索:按 DisplayName 过滤可见模块
private string _navFilter = "";
private readonly Dictionary<string, Button> _navButtons = new();
// 缓存:列表区和详情区引用(由 TwoPaneSplitView 子节点提供)
private VisualElement _listWrapper;
@@ -70,16 +73,31 @@ namespace BaseGames.Editor
private void RegisterModules()
{
_modules.Clear();
_modules.Add(new WeaponModule());
_modules.Add(new SkillModule());
_modules.Add(new EnemyModule());
_modules.Add(new FormModule());
_modules.Add(new BossSkillModule());
_modules.Add(new CharmModule());
_modules.Add(new StreamingModule());
_modules.Add(new DialogueModule());
_modules.Add(new QuestModule());
_modules.Add(new ActorModule());
// 自动发现所有实现 IDataModule 的非抽象类型(排除接口/抽象类本身)
// TypeCache 仅在 UnityEditor 中可用,零运行时开销
var moduleTypes = UnityEditor.TypeCache.GetTypesDerivedFrom<IDataModule>();
var instances = new List<IDataModule>(moduleTypes.Count);
foreach (var t in moduleTypes)
{
if (t.IsAbstract || t.IsInterface) continue;
try { instances.Add((IDataModule)Activator.CreateInstance(t)); }
catch (Exception ex)
{
Debug.LogWarning($"[DataHubWindow] 无法实例化模块 {t.Name}: {ex.Message}");
}
}
// 按 DisplayOrder 升序排列(若模块实现了 IDataModuleOrdered 则使用其值,否则默认 0
instances.Sort((a, b) =>
{
int oa = a is IDataModuleOrdered ao ? ao.DisplayOrder : 0;
int ob = b is IDataModuleOrdered bo ? bo.DisplayOrder : 0;
int c = oa.CompareTo(ob);
return c != 0 ? c : string.Compare(a.DisplayName, b.DisplayName, StringComparison.OrdinalIgnoreCase);
});
_modules.AddRange(instances);
}
// ── 布局 ─────────────────────────────────────────────────────────────
@@ -136,13 +154,31 @@ namespace BaseGames.Editor
title.style.fontSize = 10;
title.style.opacity = 0.5f;
title.style.paddingLeft = 10;
title.style.marginBottom = 6;
title.style.marginBottom = 4;
title.style.unityFontStyleAndWeight = FontStyle.Bold;
sidebar.Add(title);
// 搜索框
var searchField = new TextField();
searchField.style.marginLeft = 6;
searchField.style.marginRight = 6;
searchField.style.marginBottom = 6;
searchField.tooltip = "按模块名称过滤";
// 自定义占位符效果
if (string.IsNullOrEmpty(searchField.value))
searchField.value = "";
searchField.RegisterValueChangedCallback(evt =>
{
_navFilter = evt.newValue ?? "";
ApplyNavFilter();
});
sidebar.Add(searchField);
_navButtons.Clear();
foreach (var module in _modules)
{
var btn = BuildNavItem(module);
_navButtons[module.ModuleId] = btn;
sidebar.Add(btn);
}
@@ -154,6 +190,18 @@ namespace BaseGames.Editor
return sidebar;
}
private void ApplyNavFilter()
{
foreach (var module in _modules)
{
if (!_navButtons.TryGetValue(module.ModuleId, out var btn)) continue;
bool visible = string.IsNullOrEmpty(_navFilter) ||
module.DisplayName.IndexOf(_navFilter, System.StringComparison.OrdinalIgnoreCase) >= 0 ||
module.ModuleId.IndexOf(_navFilter, System.StringComparison.OrdinalIgnoreCase) >= 0;
btn.style.display = visible ? DisplayStyle.Flex : DisplayStyle.None;
}
}
private Button BuildNavItem(IDataModule module)
{
var btn = new Button(() => ActivateModule(module));