using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; using BaseGames.Dialogue; namespace BaseGames.Editor.Dialogue { /// /// 对话变体预览窗口。 /// 给定一个 DialogueSequenceSO,模拟世界状态标志的开关组合, /// 实时显示各条件变体是否满足,并高亮胜出的变体。 /// 菜单:BaseGames/Dialogue/Variant Preview /// public class DialogueVariantPreviewWindow : EditorWindow { private DialogueSequenceSO _target; private readonly HashSet _enabledFlags = new(System.StringComparer.Ordinal); private readonly List _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("对话变体预览"); win.minSize = new Vector2(480, 400); } /// 从外部打开并预填目标 SO。 public static void OpenWith(DialogueSequenceSO target) { var win = GetWindow("对话变体预览"); 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(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); } /// 将 _enabledFlags 包装为 IWorldStateReader,供 CheckVariant 调用。 private MockFlagReader _mockReader; private sealed class MockFlagReader : BaseGames.Core.IWorldStateReader { private readonly System.Collections.Generic.HashSet _flags; public MockFlagReader(System.Collections.Generic.HashSet 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; } } }