diff --git a/Assets/Prefabs/Bubbles/Dream Option.prefab b/Assets/Prefabs/Bubbles/Dream Option.prefab index ff0206b56..b6471cedf 100644 --- a/Assets/Prefabs/Bubbles/Dream Option.prefab +++ b/Assets/Prefabs/Bubbles/Dream Option.prefab @@ -143,15 +143,17 @@ MonoBehaviour: text: {fileID: 6343862642438606438} button: {fileID: 3624986506224747444} arrow: {fileID: 2138736795402918014} + box: {fileID: 6027689100706873528} colorBlock: - m_NormalColor: {r: 1, g: 1, b: 1, a: 1} - m_HighlightedColor: {r: 0.4, g: 0.4, b: 0.4, a: 1} - m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} - m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} - m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_NormalColor: {r: 0, g: 0, b: 0, a: 1} + m_HighlightedColor: {r: 0.11764706, g: 0.11764706, b: 0.11764706, a: 1} + m_PressedColor: {r: 0.039215688, g: 0.039215688, b: 0.039215688, a: 1} + m_SelectedColor: {r: 0, g: 0, b: 0, a: 1} + m_DisabledColor: {r: 0.039215688, g: 0.039215688, b: 0.039215688, a: 0.49019608} m_ColorMultiplier: 0 m_FadeDuration: 0 - selectedColor: {r: 0.5471698, g: 0.5471698, b: 0.5471698, a: 1} + exploredNormalColor: {r: 0.43137255, g: 0.43137255, b: 0.43137255, a: 1} + normalColor: {r: 1, g: 1, b: 1, a: 1} --- !u!114 &9163890225660695161 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/Dialog System/DialogController.cs b/Assets/Scripts/Dialog System/DialogController.cs index 2e8f9ba08..cd2dd40ce 100644 --- a/Assets/Scripts/Dialog System/DialogController.cs +++ b/Assets/Scripts/Dialog System/DialogController.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using AibisDream.Kit; using UnityEngine; using UnityEngine.Events; @@ -11,6 +12,8 @@ namespace AibisDream { private DialogueRunner _dialogueRunner; + public DialogueRunner DialogueRunner => _dialogueRunner; + [SerializeField] private LineAdvanceInput lineAdvanceInput; private void Start() @@ -132,6 +135,37 @@ namespace AibisDream #endregion + /// + /// 返回当前正在执行的节点名与节点 tags;尚未启动 / 异常时返回 (null, 空数组)。 + /// + public (string nodeName, IReadOnlyList tags) GetCurrentNodeContext() + { + if (_dialogueRunner == null) + { + return (null, Array.Empty()); + } + + var dialogue = _dialogueRunner.Dialogue; + if (dialogue == null) + { + return (null, Array.Empty()); + } + + var nodeName = dialogue.CurrentNode; + if (string.IsNullOrEmpty(nodeName)) + { + return (null, Array.Empty()); + } + + var tagsHeader = dialogue.GetHeaderValue(nodeName, "tags"); + if (string.IsNullOrWhiteSpace(tagsHeader)) + { + return (nodeName, Array.Empty()); + } + + return (nodeName, tagsHeader.Split(Array.Empty(), StringSplitOptions.RemoveEmptyEntries)); + } + #region 推进方式修改 public BindProperty advanceMode = new(new AdvanceMode()); diff --git a/Assets/Scripts/Dialog System/OptionExploreTracker.cs b/Assets/Scripts/Dialog System/OptionExploreTracker.cs new file mode 100644 index 000000000..bd54d0ee7 --- /dev/null +++ b/Assets/Scripts/Dialog System/OptionExploreTracker.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AibisDream.Framework; + +namespace AibisDream +{ + /// + /// 选项已探索状态追踪(内存态,由 OptionView 私有持有)。 + /// + public class OptionExploreTracker + { + private sealed class OptionRecord + { + public bool explored; + public string[] deps = Array.Empty(); + } + + private static readonly string[] EmptyDeps = Array.Empty(); + + private readonly Dictionary _records = new(); + private readonly Dictionary> _varToOptions = new(); + private string _currentExecutingLineId; + + public void Bind() + { + EnumEventSystem.Global.Register(StorageEvent.VariableSet, OnVariableSet); + EnumEventSystem.Global.Register(DialogEventEnum.OptionSelected, OnOptionSelected); + EnumEventSystem.Global.Register(DialogEventEnum.OptionShow, OnOptionShow); + EnumEventSystem.Global.Register(GameLoopEnum.GameStart, Clear); + EnumEventSystem.Global.Register(GameLoopEnum.GameQuit, Clear); + EnumEventSystem.Global.Register(EventEnum.NextYarn, Clear); + } + + public void Unbind() + { + EnumEventSystem.Global.UnRegister(StorageEvent.VariableSet, OnVariableSet); + EnumEventSystem.Global.UnRegister(DialogEventEnum.OptionSelected, OnOptionSelected); + EnumEventSystem.Global.UnRegister(DialogEventEnum.OptionShow, OnOptionShow); + EnumEventSystem.Global.UnRegister(GameLoopEnum.GameStart, Clear); + EnumEventSystem.Global.UnRegister(GameLoopEnum.GameQuit, Clear); + EnumEventSystem.Global.UnRegister(EventEnum.NextYarn, Clear); + } + + public (bool isTrackable, bool isExplored) Evaluate( + string lineId, + IReadOnlyList metadata, + string currentNodeName, + IReadOnlyList nodeTags) + { + if (string.IsNullOrEmpty(lineId)) + { + return (false, false); + } + + ParseMetadata(metadata, out var forceTrack, out var forceNoTrack, out var deps); + var isTrackable = ResolveTrackable(forceTrack, forceNoTrack, deps, nodeTags); + if (!isTrackable) + { + return (false, false); + } + + if (!_records.TryGetValue(lineId, out var rec)) + { + rec = new OptionRecord { explored = false, deps = deps }; + _records[lineId] = rec; + RegisterVarIndex(lineId, deps); + } + else + { + UpdateDeps(lineId, rec, deps); + } + + return (true, rec.explored); + } + + private void OnOptionSelected(LineInfo info) + { + if (!info.isOption || string.IsNullOrEmpty(info.lineId)) + { + return; + } + + if (_records.TryGetValue(info.lineId, out var rec)) + { + rec.explored = true; + _currentExecutingLineId = info.lineId; + } + } + + private void OnVariableSet(VariableItem item) + { + if (item.variableName == null || !_varToOptions.TryGetValue(item.variableName, out var lineIds)) + { + return; + } + + foreach (var lineId in lineIds) + { + if (lineId == _currentExecutingLineId) + { + continue; + } + + if (_records.TryGetValue(lineId, out var rec)) + { + rec.explored = false; + } + } + } + + private void OnOptionShow() + { + _currentExecutingLineId = null; + } + + private void Clear() + { + _records.Clear(); + _varToOptions.Clear(); + _currentExecutingLineId = null; + } + + private static void ParseMetadata( + IReadOnlyList metadata, + out bool forceTrack, + out bool forceNoTrack, + out string[] deps) + { + forceTrack = false; + forceNoTrack = false; + deps = EmptyDeps; + + if (metadata == null) + { + return; + } + + for (var i = 0; i < metadata.Count; i++) + { + var tag = metadata[i]; + if (string.IsNullOrEmpty(tag)) + { + continue; + } + + if (tag == "track") + { + forceTrack = true; + } + else if (tag == "notrack") + { + forceNoTrack = true; + } + else if (tag.StartsWith("deps:", StringComparison.Ordinal)) + { + deps = tag.Substring(5).Split(',') + .Select(v => v.Trim()) + .Where(v => v.Length > 0) + .Select(NormalizeVariableName) + .ToArray(); + } + } + } + + private static string NormalizeVariableName(string name) + { + return name.StartsWith("$") ? name : $"${name}"; + } + + private static bool ResolveTrackable( + bool forceTrack, + bool forceNoTrack, + string[] deps, + IReadOnlyList nodeTags) + { + if (forceNoTrack) + { + return false; + } + + if (forceTrack || deps.Length > 0) + { + return true; + } + + if (nodeTags == null) + { + return false; + } + + for (var i = 0; i < nodeTags.Count; i++) + { + if (nodeTags[i] == "hub" || nodeTags[i] == "detour") + { + return true; + } + } + + return false; + } + + private void RegisterVarIndex(string lineId, string[] deps) + { + foreach (var dep in deps) + { + if (!_varToOptions.TryGetValue(dep, out var lineIds)) + { + lineIds = new HashSet(); + _varToOptions[dep] = lineIds; + } + + lineIds.Add(lineId); + } + } + + private void UpdateDeps(string lineId, OptionRecord rec, string[] deps) + { + if (ReferenceEquals(rec.deps, deps) || (rec.deps.Length == deps.Length && rec.deps.SequenceEqual(deps))) + { + return; + } + + foreach (var oldDep in rec.deps) + { + if (_varToOptions.TryGetValue(oldDep, out var oldSet)) + { + oldSet.Remove(lineId); + } + } + + rec.deps = deps; + RegisterVarIndex(lineId, deps); + } + } +} diff --git a/Assets/Scripts/Dialog System/OptionExploreTracker.cs.meta b/Assets/Scripts/Dialog System/OptionExploreTracker.cs.meta new file mode 100644 index 000000000..bee2598e6 --- /dev/null +++ b/Assets/Scripts/Dialog System/OptionExploreTracker.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a7c3e91f4b2d48c6a1f9056d8e3b7c42 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Dialog System/OptionView.cs b/Assets/Scripts/Dialog System/OptionView.cs index 542a15909..ba96e1f76 100644 --- a/Assets/Scripts/Dialog System/OptionView.cs +++ b/Assets/Scripts/Dialog System/OptionView.cs @@ -1,5 +1,7 @@ #nullable enable +using System; +using System.Collections.Generic; using System.Threading.Tasks; using AibisDream.Framework; using UnityEngine; @@ -14,6 +16,20 @@ namespace AibisDream [SerializeField] private OnelineOptions onelineOptions; [SerializeField] private DialogViewType defaultViewType = DialogViewType.StandardBubble; + private OptionExploreTracker? _tracker; + + private void Awake() + { + _tracker = new OptionExploreTracker(); + _tracker.Bind(); + } + + private void OnDestroy() + { + _tracker?.Unbind(); + _tracker = null; + } + public override YarnTask OnDialogueStartedAsync() { return YarnTask.CompletedTask; @@ -29,6 +45,7 @@ namespace AibisDream { return YarnTask.CompletedTask; } + public void LoadBubbles(BubbleSlotGroupData data) { defaultViewType = data.dialogViewType; @@ -38,10 +55,17 @@ namespace AibisDream { EnumEventSystem.Global.Send(DialogEventEnum.OptionShow); var optionSelectedSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var options = DialogOption.GeneOptions(dialogueOptions, optionId => - { - optionSelectedSource.TrySetResult(optionId); - }); + + var (nodeName, tags) = DialogController.Instance != null + ? DialogController.Instance.GetCurrentNodeContext() + : (null, (IReadOnlyList)Array.Empty()); + + var options = DialogOption.GeneOptions( + dialogueOptions, + _tracker, + nodeName, + tags, + optionId => optionSelectedSource.TrySetResult(optionId)); var viewType = GetCurrentViewType(options); @@ -105,4 +129,4 @@ namespace AibisDream centerOptions.HideOptions(); } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/UI/DialogUI/Options/DialogOption.cs b/Assets/Scripts/UI/DialogUI/Options/DialogOption.cs index 7818dd74e..3b55ad10f 100644 --- a/Assets/Scripts/UI/DialogUI/Options/DialogOption.cs +++ b/Assets/Scripts/UI/DialogUI/Options/DialogOption.cs @@ -1,6 +1,6 @@ using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; using AibisDream.Framework; using AibisDream.Kit; using AibisDream.Utility; @@ -15,6 +15,9 @@ namespace AibisDream private DialogueOption _option; public bool IsAvailable => _option.IsAvailable; + public bool IsTrackable { get; private set; } + public bool IsExplored { get; private set; } + public string LineId => _option.TextID; public string Line => _option.Line.TextWithoutCharacterName.Text; public CharacterVo Character => _option.Line.GetCharacterVo(); @@ -45,13 +48,34 @@ namespace AibisDream }; } - public static DialogOption[] GeneOptions(DialogueOption[] dialogueOptions, Action onOptionSelected) + public static DialogOption[] GeneOptions( + DialogueOption[] dialogueOptions, + OptionExploreTracker tracker, + string nodeName, + IReadOnlyList nodeTags, + Action onOptionSelected) { - return dialogueOptions - .Select(item => new DialogOption { _option = item, _onOptionSelected = onOptionSelected }).ToArray(); + var tags = nodeTags ?? Array.Empty(); + + return dialogueOptions.Select(item => + { + var lineId = item.TextID; + var metadata = item.Line.Metadata ?? Array.Empty(); + var (isTrackable, isExplored) = tracker != null + ? tracker.Evaluate(lineId, metadata, nodeName, tags) + : (false, false); + + return new DialogOption + { + _option = item, + _onOptionSelected = onOptionSelected, + IsTrackable = isTrackable, + IsExplored = isExplored + }; + }).ToArray(); } } - + [Serializable] public struct TextParam { @@ -59,4 +83,4 @@ namespace AibisDream public float lineSpace; public int maxCharNum; } -} \ No newline at end of file +} diff --git a/Assets/Scripts/UI/DialogUI/Options/OnelineOption.cs b/Assets/Scripts/UI/DialogUI/Options/OnelineOption.cs index bf9ae8eab..a769a9de4 100644 --- a/Assets/Scripts/UI/DialogUI/Options/OnelineOption.cs +++ b/Assets/Scripts/UI/DialogUI/Options/OnelineOption.cs @@ -11,12 +11,15 @@ namespace AibisDream [SerializeField] private TMP_Text text; [SerializeField] private BaseButton button; [SerializeField] private Image arrow; + [SerializeField] private Image box; [Header("颜色配置")] [SerializeField] private ColorBlock colorBlock; - [Header("已选择颜色")] - [SerializeField] private Color selectedColor; + [Header("文字颜色")] + [SerializeField] private Color exploredNormalColor = new(0.45f, 0.45f, 0.45f, 1f); + [SerializeField] private Color normalColor = Color.white; + private DialogOption _option; public void Init() @@ -30,18 +33,20 @@ namespace AibisDream if (!option.IsAvailable) { - // 不可用,设置为不可选 button.interactable = false; text.SetText($"{option.Line}"); return; } + button.interactable = true; text.SetText(option.Line); button.onClick.AddListener(() => { option.Select(); hide?.Invoke(); }); + + OnStateTransition(SelectionType.Normal, true); } private void OnStateTransition(SelectionType state, bool instant) @@ -49,37 +54,46 @@ namespace AibisDream switch (state) { case SelectionType.Normal: - arrow.SetAlpha(0f); - SetColor(colorBlock.normalColor); + // arrow.SetAlpha(0f); + SetBoxColor(colorBlock.normalColor); break; case SelectionType.Highlighted: - arrow.SetAlpha(1f); - SetColor(colorBlock.highlightedColor); + // arrow.SetAlpha(1f); + SetBoxColor(colorBlock.highlightedColor); break; case SelectionType.Pressed: - arrow.SetAlpha(1f); - SetColor(colorBlock.pressedColor); + // arrow.SetAlpha(1f); + SetBoxColor(colorBlock.pressedColor); break; case SelectionType.Selected: - arrow.SetAlpha(1f); - SetColor(colorBlock.selectedColor); + // arrow.SetAlpha(1f); + SetBoxColor(colorBlock.selectedColor); break; case SelectionType.Disabled: - arrow.SetAlpha(0f); - SetColor(colorBlock.disabledColor); + // arrow.SetAlpha(0f); + SetBoxColor(colorBlock.disabledColor); break; } + + SetTextColor(GetNormalColor()); + } + + private void SetBoxColor(Color color) + { + box.color = color; } private Color GetNormalColor() { - return colorBlock.normalColor; + return _option.IsTrackable && _option.IsExplored + ? exploredNormalColor + : normalColor; } - private void SetColor(Color color) + private void SetTextColor(Color color) { arrow.color = color; text.color = color; } } -} \ No newline at end of file +}