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
+}