- 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>
380 lines
14 KiB
C#
380 lines
14 KiB
C#
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using UnityEditor;
|
||
using UnityEditor.UIElements;
|
||
using UnityEngine;
|
||
using UnityEngine.UIElements;
|
||
using BaseGames.Dialogue;
|
||
|
||
namespace BaseGames.Editor.Dialogue
|
||
{
|
||
/// <summary>
|
||
/// 对话变体预览窗口。
|
||
/// 给定一个 DialogueSequenceSO,模拟世界状态标志的开关组合,
|
||
/// 实时显示各条件变体是否满足,并高亮胜出的变体。
|
||
/// 菜单:BaseGames/Dialogue/Variant Preview
|
||
/// </summary>
|
||
public class DialogueVariantPreviewWindow : EditorWindow
|
||
{
|
||
private DialogueSequenceSO _target;
|
||
private readonly HashSet<string> _enabledFlags = new(System.StringComparer.Ordinal);
|
||
private readonly List<string> _allFlags = new();
|
||
|
||
private ObjectField _targetField;
|
||
private VisualElement _flagContainer;
|
||
private VisualElement _resultContainer;
|
||
|
||
private static readonly Color ColWin = new(0.20f, 0.75f, 0.35f, 1f);
|
||
private static readonly Color ColFail = new(0.55f, 0.55f, 0.55f, 1f);
|
||
private static readonly Color ColOverride = new(0.70f, 0.70f, 0.25f, 1f);
|
||
private static readonly Color ColBlocked = new(0.85f, 0.35f, 0.30f, 1f);
|
||
|
||
[MenuItem("BaseGames/Dialogue/Variant Preview")]
|
||
public static void Open()
|
||
{
|
||
var win = GetWindow<DialogueVariantPreviewWindow>("对话变体预览");
|
||
win.minSize = new Vector2(480, 400);
|
||
}
|
||
|
||
/// <summary>从外部打开并预填目标 SO。</summary>
|
||
public static void OpenWith(DialogueSequenceSO target)
|
||
{
|
||
var win = GetWindow<DialogueVariantPreviewWindow>("对话变体预览");
|
||
win.minSize = new Vector2(480, 400);
|
||
win.SetTarget(target);
|
||
}
|
||
|
||
private void CreateGUI()
|
||
{
|
||
_mockReader = new MockFlagReader(_enabledFlags);
|
||
rootVisualElement.style.paddingLeft = 10;
|
||
rootVisualElement.style.paddingRight = 10;
|
||
rootVisualElement.style.paddingTop = 10;
|
||
rootVisualElement.style.paddingBottom = 10;
|
||
|
||
// ── 标题栏 ──
|
||
var header = new Label("对话变体预览工具");
|
||
header.style.fontSize = 14;
|
||
header.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||
header.style.marginBottom = 8;
|
||
rootVisualElement.Add(header);
|
||
|
||
var desc = new Label("在模拟的世界状态标志组合下,预览哪个条件变体会被选中。");
|
||
desc.style.fontSize = 11;
|
||
desc.style.opacity = 0.6f;
|
||
desc.style.marginBottom = 10;
|
||
rootVisualElement.Add(desc);
|
||
|
||
// ── 目标选择器 ──
|
||
_targetField = new ObjectField("对话序列 SO")
|
||
{
|
||
objectType = typeof(DialogueSequenceSO),
|
||
allowSceneObjects = false
|
||
};
|
||
_targetField.value = _target;
|
||
_targetField.RegisterValueChangedCallback(evt =>
|
||
{
|
||
SetTarget(evt.newValue as DialogueSequenceSO);
|
||
});
|
||
rootVisualElement.Add(_targetField);
|
||
|
||
rootVisualElement.Add(MakeDivider());
|
||
|
||
// ── 标志模拟区 ──
|
||
var flagHeader = new Label("模拟世界状态标志");
|
||
flagHeader.style.fontSize = 12;
|
||
flagHeader.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||
flagHeader.style.marginBottom = 4;
|
||
rootVisualElement.Add(flagHeader);
|
||
|
||
_flagContainer = new VisualElement();
|
||
rootVisualElement.Add(_flagContainer);
|
||
|
||
rootVisualElement.Add(MakeDivider());
|
||
|
||
// ── 变体结果区 ──
|
||
var resultHeader = new Label("变体求值结果");
|
||
resultHeader.style.fontSize = 12;
|
||
resultHeader.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||
resultHeader.style.marginBottom = 4;
|
||
rootVisualElement.Add(resultHeader);
|
||
|
||
var scrollView = new ScrollView(ScrollViewMode.Vertical);
|
||
scrollView.style.flexGrow = 1;
|
||
rootVisualElement.Add(scrollView);
|
||
|
||
_resultContainer = new VisualElement();
|
||
scrollView.Add(_resultContainer);
|
||
|
||
Rebuild();
|
||
}
|
||
|
||
private void SetTarget(DialogueSequenceSO target)
|
||
{
|
||
_target = target;
|
||
_enabledFlags.Clear();
|
||
if (_targetField != null && _targetField.value != target)
|
||
_targetField.SetValueWithoutNotify(target);
|
||
Rebuild();
|
||
}
|
||
|
||
// ── 重建 ─────────────────────────────────────────────────────────────
|
||
|
||
private void Rebuild()
|
||
{
|
||
RebuildFlagToggles();
|
||
RebuildResults();
|
||
}
|
||
|
||
private void RebuildFlagToggles()
|
||
{
|
||
if (_flagContainer == null) return;
|
||
_flagContainer.Clear();
|
||
_allFlags.Clear();
|
||
|
||
if (_target == null || _target.variants == null || _target.variants.Length == 0)
|
||
{
|
||
var empty = new Label(_target == null
|
||
? "(请选择一个 DialogueSequenceSO)"
|
||
: "(该序列无条件变体,无需模拟)");
|
||
empty.style.opacity = 0.5f;
|
||
empty.style.fontSize = 11;
|
||
_flagContainer.Add(empty);
|
||
return;
|
||
}
|
||
|
||
// 收集所有变体中涉及的 Flag
|
||
var flagSet = new HashSet<string>(System.StringComparer.Ordinal);
|
||
foreach (var v in _target.variants)
|
||
{
|
||
if (v.requiredFlags != null)
|
||
foreach (var f in v.requiredFlags)
|
||
if (!string.IsNullOrEmpty(f)) flagSet.Add(f);
|
||
}
|
||
_allFlags.AddRange(flagSet.OrderBy(x => x));
|
||
|
||
if (_allFlags.Count == 0)
|
||
{
|
||
var empty = new Label("(变体未使用任何 requiredFlags)");
|
||
empty.style.opacity = 0.5f;
|
||
empty.style.fontSize = 11;
|
||
_flagContainer.Add(empty);
|
||
return;
|
||
}
|
||
|
||
// 全选 / 全不选 快速按钮
|
||
var btnRow = new VisualElement();
|
||
btnRow.style.flexDirection = FlexDirection.Row;
|
||
btnRow.style.marginBottom = 4;
|
||
|
||
var btnAll = new Button(() =>
|
||
{
|
||
foreach (var f in _allFlags) _enabledFlags.Add(f);
|
||
Rebuild();
|
||
}) { text = "全选" };
|
||
btnAll.style.fontSize = 10;
|
||
btnAll.style.height = 18;
|
||
btnRow.Add(btnAll);
|
||
|
||
var btnNone = new Button(() =>
|
||
{
|
||
_enabledFlags.Clear();
|
||
Rebuild();
|
||
}) { text = "全不选" };
|
||
btnNone.style.fontSize = 10;
|
||
btnNone.style.height = 18;
|
||
btnRow.Add(btnNone);
|
||
|
||
_flagContainer.Add(btnRow);
|
||
|
||
// 每个 Flag 对应一个 Toggle
|
||
foreach (var flag in _allFlags)
|
||
{
|
||
bool isOn = _enabledFlags.Contains(flag);
|
||
var toggle = new Toggle(flag) { value = isOn };
|
||
toggle.style.fontSize = 11;
|
||
toggle.RegisterValueChangedCallback(evt =>
|
||
{
|
||
if (evt.newValue) _enabledFlags.Add(flag);
|
||
else _enabledFlags.Remove(flag);
|
||
RebuildResults();
|
||
});
|
||
_flagContainer.Add(toggle);
|
||
}
|
||
}
|
||
|
||
private void RebuildResults()
|
||
{
|
||
if (_resultContainer == null) return;
|
||
_resultContainer.Clear();
|
||
|
||
if (_target == null)
|
||
return;
|
||
|
||
if (_target.variants == null || _target.variants.Length == 0)
|
||
{
|
||
var msg = new Label("(序列无条件变体,直接使用本序列默认台词)");
|
||
msg.style.opacity = 0.5f;
|
||
msg.style.fontSize = 11;
|
||
_resultContainer.Add(msg);
|
||
return;
|
||
}
|
||
|
||
bool winnerFound = false;
|
||
|
||
for (int i = 0; i < _target.variants.Length; i++)
|
||
{
|
||
var variant = _target.variants[i];
|
||
var row = BuildVariantRow(i, variant, winnerFound);
|
||
_resultContainer.Add(row);
|
||
|
||
if (!winnerFound && EvaluateVariant(variant))
|
||
winnerFound = true;
|
||
}
|
||
|
||
// 若无变体胜出,提示将回退到本序列默认台词
|
||
if (!winnerFound)
|
||
{
|
||
var fallback = new Label("↳ 无变体满足,将使用本序列默认台词(无变体覆盖)");
|
||
fallback.style.fontSize = 11;
|
||
fallback.style.opacity = 0.6f;
|
||
fallback.style.marginTop = 4;
|
||
_resultContainer.Add(fallback);
|
||
}
|
||
}
|
||
|
||
private VisualElement BuildVariantRow(int index, DialogueSequenceSO.ConditionalVariant variant, bool higherWon)
|
||
{
|
||
bool condMet = EvaluateVariant(variant);
|
||
bool isWinner = condMet && !higherWon;
|
||
|
||
var card = new VisualElement();
|
||
card.style.borderLeftWidth = 3;
|
||
card.style.paddingLeft = 8;
|
||
card.style.paddingRight = 8;
|
||
card.style.paddingTop = 5;
|
||
card.style.paddingBottom = 5;
|
||
card.style.marginBottom = 4;
|
||
card.style.backgroundColor = new StyleColor(new Color(0.18f, 0.18f, 0.18f, 1f));
|
||
|
||
Color borderColor;
|
||
string statusText;
|
||
Color statusColor;
|
||
|
||
if (isWinner)
|
||
{
|
||
borderColor = ColWin;
|
||
statusText = "✓ 胜出";
|
||
statusColor = ColWin;
|
||
}
|
||
else if (condMet)
|
||
{
|
||
borderColor = ColOverride;
|
||
statusText = "⏩ 被更高优先级覆盖";
|
||
statusColor = ColOverride;
|
||
}
|
||
else
|
||
{
|
||
borderColor = ColFail;
|
||
statusText = "✗ 条件不满足";
|
||
statusColor = ColFail;
|
||
}
|
||
card.style.borderLeftColor = new StyleColor(borderColor);
|
||
|
||
// 标题行
|
||
var titleRow = new VisualElement();
|
||
titleRow.style.flexDirection = FlexDirection.Row;
|
||
titleRow.style.alignItems = Align.Center;
|
||
titleRow.style.marginBottom = 3;
|
||
|
||
var idxLabel = new Label($"变体 {index}");
|
||
idxLabel.style.fontSize = 11;
|
||
idxLabel.style.flexGrow = 1;
|
||
idxLabel.style.unityFontStyleAndWeight = isWinner ? FontStyle.Bold : FontStyle.Normal;
|
||
titleRow.Add(idxLabel);
|
||
|
||
var seqName = new Label(variant.sequence != null ? variant.sequence.name : "(未设置序列)");
|
||
seqName.style.fontSize = 10;
|
||
seqName.style.opacity = 0.6f;
|
||
seqName.style.width = 160;
|
||
titleRow.Add(seqName);
|
||
|
||
var statusLabel = new Label(statusText);
|
||
statusLabel.style.fontSize = 10;
|
||
statusLabel.style.color = new StyleColor(statusColor);
|
||
statusLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||
titleRow.Add(statusLabel);
|
||
card.Add(titleRow);
|
||
|
||
// 逻辑类型
|
||
var logicLabel = new Label($"逻辑:{variant.logic}");
|
||
logicLabel.style.fontSize = 10;
|
||
logicLabel.style.opacity = 0.5f;
|
||
card.Add(logicLabel);
|
||
|
||
// 条件详情
|
||
if (variant.requiredFlags != null && variant.requiredFlags.Length > 0)
|
||
{
|
||
foreach (var flag in variant.requiredFlags)
|
||
{
|
||
if (string.IsNullOrEmpty(flag)) continue;
|
||
bool flagOn = _enabledFlags.Contains(flag);
|
||
var flagRow = new VisualElement();
|
||
flagRow.style.flexDirection = FlexDirection.Row;
|
||
flagRow.style.alignItems = Align.Center;
|
||
flagRow.style.marginTop = 1;
|
||
|
||
var icon = new Label(flagOn ? "✓" : "✗");
|
||
icon.style.fontSize = 10;
|
||
icon.style.color = new StyleColor(flagOn ? ColWin : ColBlocked);
|
||
icon.style.width = 16;
|
||
flagRow.Add(icon);
|
||
|
||
var flagLabel = new Label(flag);
|
||
flagLabel.style.fontSize = 10;
|
||
flagRow.Add(flagLabel);
|
||
|
||
card.Add(flagRow);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
var noFlags = new Label("(无 requiredFlags — 无条件激活)");
|
||
noFlags.style.fontSize = 10;
|
||
noFlags.style.opacity = 0.5f;
|
||
card.Add(noFlags);
|
||
}
|
||
|
||
return card;
|
||
}
|
||
|
||
private bool EvaluateVariant(DialogueSequenceSO.ConditionalVariant variant)
|
||
{
|
||
// 使用 DialogueSequenceSO.CheckVariant 统一变体求值逻辑,避免重复实现
|
||
return _target != null && _target.CheckVariant(variant, _mockReader);
|
||
}
|
||
|
||
/// <summary>将 _enabledFlags 包装为 IWorldStateReader,供 CheckVariant 调用。</summary>
|
||
private MockFlagReader _mockReader;
|
||
|
||
private sealed class MockFlagReader : BaseGames.Core.IWorldStateReader
|
||
{
|
||
private readonly System.Collections.Generic.HashSet<string> _flags;
|
||
public MockFlagReader(System.Collections.Generic.HashSet<string> flags) => _flags = flags;
|
||
public bool HasFlag(string key) => _flags.Contains(key);
|
||
}
|
||
|
||
// ── 辅助 ─────────────────────────────────────────────────────────────
|
||
|
||
private static VisualElement MakeDivider()
|
||
{
|
||
var d = new VisualElement();
|
||
d.style.height = 1;
|
||
d.style.backgroundColor = new StyleColor(new Color(0.35f, 0.35f, 0.35f, 0.5f));
|
||
d.style.marginTop = 6;
|
||
d.style.marginBottom = 6;
|
||
return d;
|
||
}
|
||
}
|
||
}
|