同步
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
using I2.Loc;
|
||||
//using I2.Loc;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSUtilities.General
|
||||
@@ -7,10 +7,10 @@ namespace SLSUtilities.General
|
||||
{
|
||||
public static string Localize(this string original)
|
||||
{
|
||||
if (LocalizationManager.TryGetTranslation(original, out string translated))
|
||||
/*if (LocalizationManager.TryGetTranslation(original, out string translated))
|
||||
{
|
||||
return translated;
|
||||
}
|
||||
}*/
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
8
Assets/Scripts/SLSUtilities/Narrative.meta
Normal file
8
Assets/Scripts/SLSUtilities/Narrative.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1356e0f522232cc4d8407d114731b30d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/SLSUtilities/Narrative/Base.meta
Normal file
8
Assets/Scripts/SLSUtilities/Narrative/Base.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d41106686f65de4ab351d68b4889afa
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/SLSUtilities/Narrative/Base/Data.meta
Normal file
8
Assets/Scripts/SLSUtilities/Narrative/Base/Data.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eb73d2cd16307f942b13d90f04e73802
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Collections.Generic;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSUtilities.Narrative
|
||||
{
|
||||
[CreateAssetMenu(fileName = "New Character Data", menuName = "SLSUtilities/Story System/Character Data")]
|
||||
public class CharacterData : SerializedScriptableObject
|
||||
{
|
||||
[TitleGroup("角色档案", "Yarn Spinner 角色视觉与差分配置", Alignment = TitleAlignments.Centered)]
|
||||
|
||||
[BoxGroup("角色档案/基础信息 (Basic Info)")]
|
||||
[LabelText("角色名称 (Yarn 识别码)")]
|
||||
[Tooltip("在 C# 逻辑与场景注册中使用的标准唯一英文 ID(例如:'Player' 或 'Guide')。")]
|
||||
public string nameKey;
|
||||
|
||||
[BoxGroup("角色档案/基础信息 (Basic Info)")]
|
||||
[LabelText("显示名称 (Display Name)")]
|
||||
[Tooltip("在 Yarn 对话文本中显示的本地化名称(例如中文:'引导者')。用于将文本说话人匹配到标准英文 ID。")]
|
||||
public List<string> alias;
|
||||
|
||||
[BoxGroup("角色档案/立绘差分 (Portraits)", centerLabel: true)]
|
||||
[LabelText("默认立绘 (Default Portrait)")]
|
||||
[PreviewField(70, ObjectFieldAlignment.Left)]
|
||||
[Tooltip("当 Yarn 台词没有指定 #mood 标签时,显示的默认角色立绘。")]
|
||||
public Sprite defaultPortrait;
|
||||
|
||||
[BoxGroup("角色档案/立绘差分 (Portraits)")]
|
||||
[LabelText("表情差分库 (Mood Expressions)")]
|
||||
[DictionaryDrawerSettings(KeyLabel = "表情标签 (如 Happy, Sad)", ValueLabel = "对应的立绘 (Sprite)", DisplayMode = DictionaryDisplayOptions.ExpandedFoldout)]
|
||||
[Tooltip("在此配置各种表情对应的立绘。在 Yarn 中使用 #mood:标签名 来触发。")]
|
||||
public Dictionary<string, Sprite> expressions = new Dictionary<string, Sprite>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8ad44d17e97acb747b3b7649aa6d3661
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Collections.Generic;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace SLSUtilities.Narrative
|
||||
{
|
||||
[CreateAssetMenu(fileName = "New Keyword Data", menuName = "SLSUtilities/Story System/Keyword Data")]
|
||||
public class KeywordData : SerializedScriptableObject
|
||||
{
|
||||
[TitleGroup("关键词档案", "剧情百科中的词条配置", Alignment = TitleAlignments.Centered)]
|
||||
|
||||
[BoxGroup("关键词档案/基础信息 (Basic Info)")]
|
||||
[LabelText("关键词 (Primary Keyword)")]
|
||||
[Tooltip("主要的关键词文本,将在台词中被自动识别并高亮。")]
|
||||
public string keyword;
|
||||
|
||||
[BoxGroup("关键词档案/基础信息 (Basic Info)")]
|
||||
[LabelText("别名 (Aliases)")]
|
||||
[Tooltip("该关键词的其他写法或简称,同样会被自动识别。例如:'灵能者'的别名可以是'灵能师'、'Psion'。")]
|
||||
public List<string> aliases = new List<string>();
|
||||
|
||||
[BoxGroup("关键词档案/词条内容 (Content)")]
|
||||
[LabelText("解释文本 (Description)")]
|
||||
[Tooltip("当玩家悬停时显示的解释内容。如果文本中包含其他已注册的关键词,会自动生成嵌套链接。")]
|
||||
public string description;
|
||||
|
||||
/// <summary>
|
||||
/// 返回所有可触发该词条的文本(主关键词 + 所有别名)。
|
||||
/// </summary>
|
||||
public IEnumerable<string> GetAllTriggerWords()
|
||||
{
|
||||
yield return keyword;
|
||||
foreach (var alias in aliases)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(alias))
|
||||
{
|
||||
yield return alias;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 088b2e3aa8e7aad43b9a0230097676de
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSUtilities.Narrative
|
||||
{
|
||||
/// <summary>
|
||||
/// 一个 NarrativeEntry 对应一个“剧情触发源”(如一个 NPC、一个区域、一个道具)。
|
||||
/// 它包含一个有序的条件→节点路由列表。
|
||||
/// </summary>
|
||||
[CreateAssetMenu(fileName = "NarrativeEntry", menuName = "SLSUtilities/Story System/Narrative Entry")]
|
||||
public class NarrativeEntry : SerializedScriptableObject
|
||||
{
|
||||
[TitleGroup("Identity", "剧情触发源身份标识", Alignment = TitleAlignments.Centered)]
|
||||
[Required]
|
||||
[Tooltip("全局唯一标识,对应 NPC 或触发器的 storyId (如 'OldMan', 'Forest_Gate')")]
|
||||
public string storyId;
|
||||
|
||||
[TitleGroup("Routing Rules", "路由规则表 (从上到下评估,首个满足的生效)", Alignment = TitleAlignments.Centered)]
|
||||
[ListDrawerSettings(ShowPaging = true, NumberOfItemsPerPage = 10)]
|
||||
public List<NarrativeRoute> routes = new List<NarrativeRoute>();
|
||||
|
||||
[TitleGroup("Fallback", "兜底处理 (当所有路由条件都不满足时播什么)", Alignment = TitleAlignments.Centered)]
|
||||
[Tooltip("兜底节点(可为空,为空则不播放任何对话)")]
|
||||
public string fallbackNode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单条路由规则:描述备注 + 条件列表 + 目标 Yarn 节点。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class NarrativeRoute
|
||||
{
|
||||
[LabelText("描述 (仅编辑器备注)")]
|
||||
[Required]
|
||||
[Tooltip("例如:'第一次见面'、'已购买提灯后'")]
|
||||
public string editorNote;
|
||||
|
||||
[LabelText("条件列表 (全部满足才匹配)")]
|
||||
[ListDrawerSettings(ShowIndexLabels = false)]
|
||||
public List<NarrativeCondition> conditions = new List<NarrativeCondition>();
|
||||
|
||||
[LabelText("目标 Yarn 节点")]
|
||||
[Required]
|
||||
[Tooltip("满足上述条件时播放的 Yarn Node 名称 (如 'OldMan_FirstMeet')")]
|
||||
public string targetNode;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单个变量匹配条件:变量类型 + 变量名 + 比较方式 + 目标值。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class NarrativeCondition
|
||||
{
|
||||
public enum ConditionType
|
||||
{
|
||||
Bool,
|
||||
Int,
|
||||
Float,
|
||||
String
|
||||
}
|
||||
|
||||
public enum CompareOp
|
||||
{
|
||||
[LabelText("==")] Equal,
|
||||
[LabelText("!=")] NotEqual,
|
||||
[LabelText(">")] Greater,
|
||||
[LabelText(">=")] GreaterOrEqual,
|
||||
[LabelText("<")] Less,
|
||||
[LabelText("<=")] LessOrEqual
|
||||
}
|
||||
|
||||
[HorizontalGroup("Cond", Width = 70)]
|
||||
[HideLabel]
|
||||
public ConditionType type = ConditionType.Bool;
|
||||
|
||||
[HorizontalGroup("Cond")]
|
||||
[HideLabel]
|
||||
[Required]
|
||||
[Tooltip("StorySystem 变量名 (如 'has_lantern')")]
|
||||
public string key;
|
||||
|
||||
[HorizontalGroup("Cond", Width = 60)]
|
||||
[HideLabel]
|
||||
public CompareOp op = CompareOp.Equal;
|
||||
|
||||
[HorizontalGroup("Cond")]
|
||||
[HideLabel]
|
||||
[Required]
|
||||
[Tooltip("对比的值,布尔值请填 true/false,其他按相应格式填写")]
|
||||
public string value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7033ff7e5a062be4ca2763804709b342
|
||||
@@ -0,0 +1,181 @@
|
||||
using System.Collections.Generic;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
using UnityEditor;
|
||||
#endif
|
||||
|
||||
namespace SLSUtilities.Narrative
|
||||
{
|
||||
[CreateAssetMenu(fileName = "Story Project Database", menuName = "SLSUtilities/Story System/Story Project Database")]
|
||||
public class StoryProjectDatabase : SerializedScriptableObject
|
||||
{
|
||||
// 巧妙利用 Odin 的嵌套分组语法:"父分组/子分组"
|
||||
// 这样就可以让 TitleGroup 作为父节点显示在最上方,而 BoxGroup 嵌套在其中,解决了 Title 被包裹在 Box 里的问题。
|
||||
[TitleGroup("全局剧情数据库", "集中管理所有剧情相关数据的注册中心", Alignment = TitleAlignments.Centered)]
|
||||
|
||||
[ListDrawerSettings(ShowIndexLabels = true, ListElementLabelName = "nameKey")]
|
||||
[BoxGroup("全局剧情数据库/角色档案 (Characters)")]
|
||||
[LabelText("角色档案列表 (Character Profiles)")]
|
||||
public List<CharacterData> characters = new List<CharacterData>();
|
||||
|
||||
[ListDrawerSettings(ShowIndexLabels = true)]
|
||||
[BoxGroup("全局剧情数据库/变量数据 (Variables)")]
|
||||
[LabelText("变量数据组 (Variable Groups)")]
|
||||
public List<VariableData> variables = new List<VariableData>();
|
||||
|
||||
[ListDrawerSettings(ShowIndexLabels = true, ListElementLabelName = "keyword")]
|
||||
[BoxGroup("全局剧情数据库/已注册的资产 (Registered Assets)")]
|
||||
[LabelText("关键词词条 (Keywords)")]
|
||||
public List<KeywordData> keywords = new List<KeywordData>();
|
||||
|
||||
[ListDrawerSettings(ShowIndexLabels = true, ListElementLabelName = "storyId")]
|
||||
[BoxGroup("全局剧情数据库/剧情入口 (Narrative Entries)")]
|
||||
[LabelText("剧情入口路由表 (Narrative Entries)")]
|
||||
public List<NarrativeEntry> narrativeEntries = new List<NarrativeEntry>();
|
||||
|
||||
[TitleGroup("Yarn File Export Settings", "NPC 变量定义文件导出配置", Alignment = TitleAlignments.Centered)]
|
||||
[FolderPath]
|
||||
[Required("请指定 Yarn 文件的导出目标文件夹!")]
|
||||
[Tooltip("生成的 NPC_IDs.yarn 文件的保存目录(建议放在 Yarn 脚本文件夹下)")]
|
||||
public string exportFolder = "Assets/Story";
|
||||
|
||||
[Button("生成 NPC_IDs.yarn (Generate)", ButtonSizes.Medium)]
|
||||
[GUIColor(0.2f, 0.8f, 0.4f)]
|
||||
public void GenerateNpcIdsYarnFile()
|
||||
{
|
||||
if (string.IsNullOrEmpty(exportFolder))
|
||||
{
|
||||
Debug.LogError("[StoryProjectDatabase] 导出失败:未指定有效的导出文件夹路径!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (characters == null || characters.Count == 0)
|
||||
{
|
||||
Debug.LogWarning("[StoryProjectDatabase] 角色列表为空,已取消生成。");
|
||||
return;
|
||||
}
|
||||
|
||||
// 确保目标导出目录存在
|
||||
if (!System.IO.Directory.Exists(exportFolder))
|
||||
{
|
||||
try
|
||||
{
|
||||
System.IO.Directory.CreateDirectory(exportFolder);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"[StoryProjectDatabase] 无法创建目标文件夹 '{exportFolder}': {ex.Message}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
string filePath = System.IO.Path.Combine(exportFolder, "NPC_IDs.yarn");
|
||||
try
|
||||
{
|
||||
using (System.IO.StreamWriter writer = new System.IO.StreamWriter(filePath, false, System.Text.Encoding.UTF8))
|
||||
{
|
||||
writer.WriteLine("// ===========================================================================");
|
||||
writer.WriteLine("// 自动生成的 NPC 英文标准名 ID 变量定义文件。");
|
||||
writer.WriteLine("// 供 VS Code Yarn Spinner 插件进行命令变量补全(例如:输入 $NPC_ 触发提示)。");
|
||||
writer.WriteLine("// 警告:该文件为程序自动生成,请勿在此文件中手动编辑或添加内容。");
|
||||
writer.WriteLine("// ===========================================================================");
|
||||
writer.WriteLine();
|
||||
|
||||
// 必须将 declare 声明置于有效的 Node 结构内,防止编译器在解析无 Node 的文件时抛出 Token 识别错误(token recognition error at: '<')
|
||||
writer.WriteLine("title: NPC_IDs");
|
||||
writer.WriteLine("---");
|
||||
|
||||
foreach (var charData in characters)
|
||||
{
|
||||
if (charData == null || string.IsNullOrWhiteSpace(charData.nameKey)) continue;
|
||||
|
||||
string trimmedId = charData.nameKey.Trim();
|
||||
// 生成格式如:<<declare $NPC_OldMan = "OldMan">>
|
||||
writer.WriteLine($"<<declare $NPC_{trimmedId} = \"{trimmedId}\">>");
|
||||
}
|
||||
|
||||
writer.WriteLine("===");
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
// 刷新 AssetDatabase,让 Unity 编辑器立刻加载生成的 .yarn 资源
|
||||
UnityEditor.AssetDatabase.Refresh();
|
||||
#endif
|
||||
Debug.Log($"[StoryProjectDatabase] 成功生成/更新 NPC 声明文件: '{filePath}'");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError($"[StoryProjectDatabase] 导出 NPC_IDs.yarn 失败: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[Button("自动扫描并注册数据 (Auto-Scan Directory)", ButtonSizes.Large, Icon = SdfIconType.Search)]
|
||||
[GUIColor(0.4f, 0.8f, 1f)]
|
||||
[PropertyTooltip("自动在当前数据库所在的文件夹(及其子文件夹)中寻找所有的 CharacterData、VariableData、KeywordData 和 NarrativeEntry,并自动填入上方的列表中。")]
|
||||
public void AutoPopulate()
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
// 获取当前 Database 资产所在的目录路径
|
||||
string dbPath = AssetDatabase.GetAssetPath(this);
|
||||
if (string.IsNullOrEmpty(dbPath))
|
||||
{
|
||||
Debug.LogWarning("[StorySystem] 请先将数据库资产 (Database) 保存到项目目录中。");
|
||||
return;
|
||||
}
|
||||
string searchDirectory = System.IO.Path.GetDirectoryName(dbPath);
|
||||
|
||||
// 搜索 CharacterData
|
||||
string[] charGuids = AssetDatabase.FindAssets($"t:{nameof(CharacterData)}", new[] { searchDirectory });
|
||||
characters.Clear();
|
||||
foreach (var guid in charGuids)
|
||||
{
|
||||
var assetPath = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var charData = AssetDatabase.LoadAssetAtPath<CharacterData>(assetPath);
|
||||
if (charData != null && !characters.Contains(charData))
|
||||
characters.Add(charData);
|
||||
}
|
||||
|
||||
// 搜索 VariableData
|
||||
string[] varGuids = AssetDatabase.FindAssets($"t:{nameof(VariableData)}", new[] { searchDirectory });
|
||||
variables.Clear();
|
||||
foreach (var guid in varGuids)
|
||||
{
|
||||
var assetPath = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var varData = AssetDatabase.LoadAssetAtPath<VariableData>(assetPath);
|
||||
if (varData != null && !variables.Contains(varData))
|
||||
variables.Add(varData);
|
||||
}
|
||||
|
||||
// 搜索 KeywordData
|
||||
string[] kwGuids = AssetDatabase.FindAssets($"t:{nameof(KeywordData)}", new[] { searchDirectory });
|
||||
keywords.Clear();
|
||||
foreach (var guid in kwGuids)
|
||||
{
|
||||
var assetPath = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var kwData = AssetDatabase.LoadAssetAtPath<KeywordData>(assetPath);
|
||||
if (kwData != null && !keywords.Contains(kwData))
|
||||
keywords.Add(kwData);
|
||||
}
|
||||
|
||||
// 搜索 NarrativeEntry
|
||||
string[] entryGuids = AssetDatabase.FindAssets($"t:{nameof(NarrativeEntry)}", new[] { searchDirectory });
|
||||
narrativeEntries.Clear();
|
||||
foreach (var guid in entryGuids)
|
||||
{
|
||||
var assetPath = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var entryData = AssetDatabase.LoadAssetAtPath<NarrativeEntry>(assetPath);
|
||||
if (entryData != null && !narrativeEntries.Contains(entryData))
|
||||
narrativeEntries.Add(entryData);
|
||||
}
|
||||
|
||||
EditorUtility.SetDirty(this);
|
||||
AssetDatabase.SaveAssets();
|
||||
Debug.Log($"[StorySystem] 自动扫描完成!共注册了 {characters.Count} 个角色档案、{variables.Count} 个变量数据组、{keywords.Count} 个关键词词条 和 {narrativeEntries.Count} 个剧情入口路由表。");
|
||||
#else
|
||||
Debug.LogWarning("自动扫描 (AutoPopulate) 只能在 Unity 编辑器环境下运行。");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7e578280b10e24b40859914cec13cf78
|
||||
@@ -0,0 +1,24 @@
|
||||
using System.Collections.Generic;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSUtilities.Narrative
|
||||
{
|
||||
[CreateAssetMenu(fileName = "StoryVariableData", menuName = "SLSUtilities/Story System/Variable Data")]
|
||||
public class VariableData : SerializedScriptableObject
|
||||
{
|
||||
[Title("变量数据", titleAlignment: TitleAlignments.Centered)]
|
||||
|
||||
[Searchable]
|
||||
public Dictionary<string, bool> boolVariables = new Dictionary<string, bool>();
|
||||
|
||||
[Searchable]
|
||||
public Dictionary<string, int> intVariables = new Dictionary<string, int>();
|
||||
|
||||
[Searchable]
|
||||
public Dictionary<string, float> floatVariables = new Dictionary<string, float>();
|
||||
|
||||
[Searchable]
|
||||
public Dictionary<string, string> stringVariables = new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f93ca4ba1ee871b4b944a077b0ec2fab
|
||||
@@ -0,0 +1,173 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSUtilities.Narrative
|
||||
{
|
||||
/// <summary>
|
||||
/// 条件匹配评估工具类。
|
||||
/// 读取并比对 StorySystem.Variables 中的运行时状态,支持多种类型的变量与运算符。
|
||||
/// </summary>
|
||||
public static class NarrativeConditionEvaluator
|
||||
{
|
||||
/// <summary>
|
||||
/// 评估一系列条件是否全部满足 (AND 关系)。
|
||||
/// </summary>
|
||||
public static bool Evaluate(List<NarrativeCondition> conditions)
|
||||
{
|
||||
if (conditions == null || conditions.Count == 0)
|
||||
return true; // 无条件默认满足
|
||||
|
||||
if (StorySystem.Variables == null)
|
||||
{
|
||||
Debug.LogWarning("[StorySystem] 评估条件失败:StorySystem.Variables 尚未初始化。");
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var cond in conditions)
|
||||
{
|
||||
if (string.IsNullOrEmpty(cond.key))
|
||||
{
|
||||
Debug.LogWarning("[StorySystem] 条件配置错误:变量 key 为空。");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EvaluateCondition(cond))
|
||||
return false; // 任何一个条件不满足,直接返回 false
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool EvaluateCondition(NarrativeCondition cond)
|
||||
{
|
||||
switch (cond.type)
|
||||
{
|
||||
case NarrativeCondition.ConditionType.Bool:
|
||||
return EvaluateBool(cond);
|
||||
case NarrativeCondition.ConditionType.Int:
|
||||
return EvaluateInt(cond);
|
||||
case NarrativeCondition.ConditionType.Float:
|
||||
return EvaluateFloat(cond);
|
||||
case NarrativeCondition.ConditionType.String:
|
||||
return EvaluateString(cond);
|
||||
default:
|
||||
Debug.LogWarning($"[StorySystem] 未知的条件类型: {cond.type}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool EvaluateBool(NarrativeCondition cond)
|
||||
{
|
||||
bool currentVal = false;
|
||||
if (!StorySystem.Variables.boolVariables.TryGetValue(cond.key, out currentVal))
|
||||
{
|
||||
Debug.LogWarning($"[StorySystem] 变量未找到: Bool '{cond.key}',使用默认值 false 匹配。");
|
||||
}
|
||||
|
||||
if (!bool.TryParse(cond.value, out bool targetVal))
|
||||
{
|
||||
Debug.LogError($"[StorySystem] 无法将目标值 '{cond.value}' 解析为 Bool。条件键: {cond.key}");
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (cond.op)
|
||||
{
|
||||
case NarrativeCondition.CompareOp.Equal:
|
||||
return currentVal == targetVal;
|
||||
case NarrativeCondition.CompareOp.NotEqual:
|
||||
return currentVal != targetVal;
|
||||
default:
|
||||
Debug.LogWarning($"[StorySystem] 布尔类型不支持比较运算符: {cond.op},必须为 Equal 或 NotEqual。");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool EvaluateInt(NarrativeCondition cond)
|
||||
{
|
||||
int currentVal = 0;
|
||||
if (!StorySystem.Variables.intVariables.TryGetValue(cond.key, out currentVal))
|
||||
{
|
||||
Debug.LogWarning($"[StorySystem] 变量未找到: Int '{cond.key}',使用默认值 0 匹配。");
|
||||
}
|
||||
|
||||
if (!int.TryParse(cond.value, out int targetVal))
|
||||
{
|
||||
Debug.LogError($"[StorySystem] 无法将目标值 '{cond.value}' 解析为 Int。条件键: {cond.key}");
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (cond.op)
|
||||
{
|
||||
case NarrativeCondition.CompareOp.Equal:
|
||||
return currentVal == targetVal;
|
||||
case NarrativeCondition.CompareOp.NotEqual:
|
||||
return currentVal != targetVal;
|
||||
case NarrativeCondition.CompareOp.Greater:
|
||||
return currentVal > targetVal;
|
||||
case NarrativeCondition.CompareOp.GreaterOrEqual:
|
||||
return currentVal >= targetVal;
|
||||
case NarrativeCondition.CompareOp.Less:
|
||||
return currentVal < targetVal;
|
||||
case NarrativeCondition.CompareOp.LessOrEqual:
|
||||
return currentVal <= targetVal;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool EvaluateFloat(NarrativeCondition cond)
|
||||
{
|
||||
float currentVal = 0f;
|
||||
if (!StorySystem.Variables.floatVariables.TryGetValue(cond.key, out currentVal))
|
||||
{
|
||||
Debug.LogWarning($"[StorySystem] 变量未找到: Float '{cond.key}',使用默认值 0.0 匹配。");
|
||||
}
|
||||
|
||||
if (!float.TryParse(cond.value, out float targetVal))
|
||||
{
|
||||
Debug.LogError($"[StorySystem] 无法将目标值 '{cond.value}' 解析为 Float。条件键: {cond.key}");
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (cond.op)
|
||||
{
|
||||
case NarrativeCondition.CompareOp.Equal:
|
||||
return Mathf.Approximately(currentVal, targetVal);
|
||||
case NarrativeCondition.CompareOp.NotEqual:
|
||||
return !Mathf.Approximately(currentVal, targetVal);
|
||||
case NarrativeCondition.CompareOp.Greater:
|
||||
return currentVal > targetVal;
|
||||
case NarrativeCondition.CompareOp.GreaterOrEqual:
|
||||
return currentVal >= targetVal;
|
||||
case NarrativeCondition.CompareOp.Less:
|
||||
return currentVal < targetVal;
|
||||
case NarrativeCondition.CompareOp.LessOrEqual:
|
||||
return currentVal <= targetVal;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool EvaluateString(NarrativeCondition cond)
|
||||
{
|
||||
string currentVal = string.Empty;
|
||||
if (!StorySystem.Variables.stringVariables.TryGetValue(cond.key, out currentVal))
|
||||
{
|
||||
Debug.LogWarning($"[StorySystem] 变量未找到: String '{cond.key}',使用默认空字符串匹配。");
|
||||
}
|
||||
|
||||
string targetVal = cond.value ?? string.Empty;
|
||||
|
||||
switch (cond.op)
|
||||
{
|
||||
case NarrativeCondition.CompareOp.Equal:
|
||||
return currentVal == targetVal;
|
||||
case NarrativeCondition.CompareOp.NotEqual:
|
||||
return currentVal != targetVal;
|
||||
default:
|
||||
Debug.LogWarning($"[StorySystem] 字符串类型不支持比较运算符: {cond.op},必须为 Equal 或 NotEqual。");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1021ec61ce2442d49bb15ecfe6dd73e9
|
||||
@@ -0,0 +1,75 @@
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSUtilities.Narrative
|
||||
{
|
||||
/// <summary>
|
||||
/// 通用剧情触发器基类。
|
||||
/// 仅负责管理触发状态(oneShot, hasFired)与全局故事 ID(storyId)。
|
||||
/// 具体触发的时机与条件由派生类(子类)自行重写并定义。
|
||||
/// </summary>
|
||||
public abstract class NarrativeTrigger : MonoBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// 全局静态触发事件。当任何剧情触发器被激活时触发。
|
||||
/// 用于彻底解耦 SLSUtilities 核心程序集与 MainGame 层的 StoryDirector。
|
||||
/// </summary>
|
||||
public static System.Action<string> OnNarrativeTriggerFired;
|
||||
|
||||
[TitleGroup("Trigger Settings", "剧情触发器核心配置", Alignment = TitleAlignments.Centered)]
|
||||
|
||||
[SerializeField]
|
||||
[Required]
|
||||
[Tooltip("要触发的 NarrativeEntry 的 storyId")]
|
||||
protected string storyId;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("是否仅能触发一次")]
|
||||
protected bool oneShot = true;
|
||||
|
||||
[ShowInInspector]
|
||||
[ReadOnly]
|
||||
[Tooltip("该触发器当前是否已经激活过")]
|
||||
protected bool hasFired;
|
||||
|
||||
/// <summary>获取触发的目标故事 ID。</summary>
|
||||
public string StoryId => storyId;
|
||||
|
||||
/// <summary>是否是一次性触发器。</summary>
|
||||
public bool OneShot => oneShot;
|
||||
|
||||
/// <summary>该触发器是否已经激活过。</summary>
|
||||
public bool HasFired => hasFired;
|
||||
|
||||
/// <summary>
|
||||
/// 激活触发器,通知订阅者启动剧情。
|
||||
/// </summary>
|
||||
[Button("测试触发 (Fire)", ButtonSizes.Small)]
|
||||
[GUIColor(0.3f, 0.8f, 1f)]
|
||||
public virtual void Fire()
|
||||
{
|
||||
if (oneShot && hasFired)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(storyId))
|
||||
{
|
||||
Debug.LogWarning($"[NarrativeTrigger] {gameObject.name} 触发失败:未配置 storyId。");
|
||||
return;
|
||||
}
|
||||
|
||||
hasFired = true;
|
||||
OnNarrativeTriggerFired?.Invoke(storyId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置触发状态(允许在 oneShot 模式下重新触发)。
|
||||
/// </summary>
|
||||
[Button("重置激活状态 (Reset)", ButtonSizes.Small)]
|
||||
public virtual void ResetTrigger()
|
||||
{
|
||||
hasFired = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9bc93394b40fd74c8df5412cb2e8992
|
||||
102
Assets/Scripts/SLSUtilities/Narrative/Base/StorySystem.cs
Normal file
102
Assets/Scripts/SLSUtilities/Narrative/Base/StorySystem.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System.Collections.Generic;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.General;
|
||||
using UnityEngine;
|
||||
|
||||
namespace SLSUtilities.Narrative
|
||||
{
|
||||
public partial class StorySystem : Singleton<StorySystem>
|
||||
{
|
||||
public StoryProjectDatabase database;
|
||||
|
||||
public static VariableCollection Variables;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
Initialize(true);
|
||||
LoadVariables();
|
||||
}
|
||||
}
|
||||
|
||||
public partial class StorySystem
|
||||
{
|
||||
public static StoryProjectDatabase Database => instance.database;
|
||||
}
|
||||
|
||||
public partial class StorySystem
|
||||
{
|
||||
private static string SavePath => Application.persistentDataPath + "/Story/";
|
||||
|
||||
public class VariableCollection
|
||||
{
|
||||
public Dictionary<string, bool> boolVariables = new Dictionary<string, bool>();
|
||||
|
||||
public Dictionary<string, int> intVariables = new Dictionary<string, int>();
|
||||
|
||||
public Dictionary<string, float> floatVariables = new Dictionary<string, float>();
|
||||
|
||||
public Dictionary<string, string> stringVariables = new Dictionary<string, string>();
|
||||
|
||||
public void LoadFromData()
|
||||
{
|
||||
List<VariableData> variableDataList = Database.variables;
|
||||
foreach (VariableData variableData in variableDataList)
|
||||
{
|
||||
foreach (KeyValuePair<string, bool> boolVar in variableData.boolVariables)
|
||||
{
|
||||
if(!boolVariables.TryAdd(boolVar.Key, boolVar.Value))
|
||||
{
|
||||
Debug.LogWarning($"[StorySystem] 变量加载警告:布尔变量 '{boolVar.Key}' 已存在,跳过重复项。");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, int> intVar in variableData.intVariables)
|
||||
{
|
||||
if(!intVariables.TryAdd(intVar.Key, intVar.Value))
|
||||
{
|
||||
Debug.LogWarning($"[StorySystem] 变量加载警告:整数变量 '{intVar.Key}' 已存在,跳过重复项。");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, float> floatVar in variableData.floatVariables)
|
||||
{
|
||||
if(!floatVariables.TryAdd(floatVar.Key, floatVar.Value))
|
||||
{
|
||||
Debug.LogWarning($"[StorySystem] 变量加载警告:浮点变量 '{floatVar.Key}' 已存在,跳过重复项。");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, string> stringVar in variableData.stringVariables)
|
||||
{
|
||||
if (!stringVariables.TryAdd(stringVar.Key, stringVar.Value))
|
||||
{
|
||||
Debug.LogWarning($"[StorySystem] 变量加载警告:字符串变量 '{stringVar.Key}' 已存在,跳过重复项。");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Button("保存变量数据 (Save Variables)", ButtonSizes.Small, Icon = SdfIconType.Save)]
|
||||
public void SaveVariables()
|
||||
{
|
||||
string variablesSavePath = SavePath + "variables.json";
|
||||
ES3.Save("Variables", Variables, variablesSavePath);
|
||||
}
|
||||
|
||||
public void LoadVariables()
|
||||
{
|
||||
string variablesSavePath = SavePath + "variables.json";
|
||||
if (!ES3.FileExists(variablesSavePath))
|
||||
{
|
||||
Variables = new VariableCollection();
|
||||
Variables.LoadFromData();
|
||||
ES3.Save("Variables", Variables, variablesSavePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
Variables = ES3.Load<VariableCollection>("Variables", variablesSavePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bc7844de67aeb4b47b1eef29edaec8eb
|
||||
95
Assets/Scripts/SLSUtilities/Narrative/Base/YarnFunctions.cs
Normal file
95
Assets/Scripts/SLSUtilities/Narrative/Base/YarnFunctions.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using System.Collections.Generic;
|
||||
using Yarn.Unity;
|
||||
|
||||
namespace SLSUtilities.Narrative
|
||||
{
|
||||
public static partial class YarnFunctions
|
||||
{
|
||||
[YarnCommand("set_bool")]
|
||||
public static void Yarn_SetBool(string key, bool value)
|
||||
{
|
||||
StorySystem.Variables.boolVariables[key] = value;
|
||||
}
|
||||
|
||||
[YarnFunction("get_bool")]
|
||||
public static bool Yarn_GetBool(string key)
|
||||
{
|
||||
return StorySystem.Variables.boolVariables.GetValueOrDefault(key, false);
|
||||
}
|
||||
|
||||
[YarnCommand("set_int")]
|
||||
public static void Yarn_SetInt(string key, int value)
|
||||
{
|
||||
StorySystem.Variables.intVariables[key] = value;
|
||||
}
|
||||
|
||||
[YarnCommand("modify_int")]
|
||||
public static void Yarn_ModifyInt(string key, int modification)
|
||||
{
|
||||
int currentValue = StorySystem.Variables.intVariables.GetValueOrDefault(key, 0);
|
||||
StorySystem.Variables.intVariables[key] = currentValue + modification;
|
||||
}
|
||||
|
||||
[YarnFunction("get_int")]
|
||||
public static int Yarn_GetInt(string key)
|
||||
{
|
||||
return StorySystem.Variables.intVariables.GetValueOrDefault(key, 0);
|
||||
}
|
||||
|
||||
[YarnCommand("set_float")]
|
||||
public static void Yarn_SetFloat(string key, float value)
|
||||
{
|
||||
StorySystem.Variables.floatVariables[key] = value;
|
||||
}
|
||||
|
||||
[YarnCommand("modify_float")]
|
||||
public static void Yarn_ModifyFloat(string key, float modification)
|
||||
{
|
||||
float currentValue = StorySystem.Variables.floatVariables.GetValueOrDefault(key, 0f);
|
||||
StorySystem.Variables.floatVariables[key] = currentValue + modification;
|
||||
}
|
||||
|
||||
[YarnFunction("get_float")]
|
||||
public static float Yarn_GetFloat(string key)
|
||||
{
|
||||
return StorySystem.Variables.floatVariables.GetValueOrDefault(key, 0f);
|
||||
}
|
||||
|
||||
[YarnCommand("set_string")]
|
||||
public static void Yarn_SetString(string key, string value)
|
||||
{
|
||||
StorySystem.Variables.stringVariables[key] = value;
|
||||
}
|
||||
|
||||
[YarnFunction("get_string")]
|
||||
public static string Yarn_GetString(string key)
|
||||
{
|
||||
return StorySystem.Variables.stringVariables.GetValueOrDefault(key, "");
|
||||
}
|
||||
}
|
||||
|
||||
public static partial class YarnFunctions
|
||||
{
|
||||
[YarnCommand("log")]
|
||||
public static void Log(string message, string logType)
|
||||
{
|
||||
logType = logType.ToLower();
|
||||
|
||||
switch (logType)
|
||||
{
|
||||
case "info":
|
||||
UnityEngine.Debug.Log(message);
|
||||
break;
|
||||
case "warning":
|
||||
UnityEngine.Debug.LogWarning(message);
|
||||
break;
|
||||
case "error":
|
||||
UnityEngine.Debug.LogError(message);
|
||||
break;
|
||||
default:
|
||||
UnityEngine.Debug.Log(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fd615694e3d8cc842a6c75bc3956a926
|
||||
8
Assets/Scripts/SLSUtilities/Narrative/Editor.meta
Normal file
8
Assets/Scripts/SLSUtilities/Narrative/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6b5e90c1f5a684a4793d38b5a7f11c13
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,32 @@
|
||||
using SLSUtilities.Narrative.UI;
|
||||
using UnityEditor;
|
||||
using Yarn.Unity.Editor;
|
||||
|
||||
namespace SLSUtilities.Narrative.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// AdvancedLinePresenter 的自定义 Inspector。
|
||||
/// 继承自 Yarn 的 YarnEditor,完整复现 LinePresenter 的
|
||||
/// [ShowIf]、[Group]、[MustNotBeNull] 等属性驱动的显示效果。
|
||||
/// </summary>
|
||||
[CanEditMultipleObjects]
|
||||
[CustomEditor(typeof(AdvancedLinePresenter))]
|
||||
public class AdvancedLinePresenterEditor : YarnEditor { }
|
||||
|
||||
/// <summary>
|
||||
/// AdvancedLineAdvancer 的自定义 Inspector。
|
||||
/// 继承自 Yarn 的 YarnEditor,完整复现 LineAdvancer 的
|
||||
/// InputMode 条件显示等效果。
|
||||
/// </summary>
|
||||
[CanEditMultipleObjects]
|
||||
[CustomEditor(typeof(AdvancedLineAdvancer))]
|
||||
public class AdvancedLineAdvancerEditor : YarnEditor { }
|
||||
|
||||
/// <summary>
|
||||
/// AdvancedOptionsPresenter 的自定义 Inspector。
|
||||
/// 完整复现 OptionsPresenter 的属性驱动效果。
|
||||
/// </summary>
|
||||
[CanEditMultipleObjects]
|
||||
[CustomEditor(typeof(AdvancedOptionsPresenter))]
|
||||
public class AdvancedOptionsPresenterEditor : YarnEditor { }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a5fb6371856a1c4dad725a24657bb10
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "SLSUtilities.StorySystem.Editor",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:43bb4e992d9b32b4bbb25402b41e80a0",
|
||||
"GUID:22a86856172c06146a539eeb9c9c67f5",
|
||||
"GUID:34aa492b82754644eac2f903cd496268",
|
||||
"GUID:3a299c53e4c683b4eb4a04c9ad9e648f"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1f17ca624957b754baae4c01af60c96e
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
92
Assets/Scripts/SLSUtilities/Narrative/Editor/YslsMerger.cs
Normal file
92
Assets/Scripts/SLSUtilities/Narrative/Editor/YslsMerger.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace SLSUtilities.Narrative.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Yarn Spinner 自动 YSLS 合并器。
|
||||
/// 自动将来自不同程序集(Assembly-CSharp、SLSUtilities等)生成的多个 YSLS 声明文件
|
||||
/// 合并成一个单一的 combined.ysls.json 文件,输出到项目根目录下,
|
||||
/// 从而完美解决 VS Code 中 Yarn Spinner 插件只能同时加载一个 definitions 文件的限制!
|
||||
/// </summary>
|
||||
public static class YslsMerger
|
||||
{
|
||||
[MenuItem("Tools/Yarn Spinner/Merge YSLS Files")]
|
||||
public static void MergeYslsFiles()
|
||||
{
|
||||
string packagePath = Path.Combine(Directory.GetCurrentDirectory(), "ProjectSettings", "Packages", "dev.yarnspinner");
|
||||
if (!Directory.Exists(packagePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string combinedFileName = "combined.ysls.json";
|
||||
string outputRootPath = Path.Combine(packagePath, combinedFileName);
|
||||
|
||||
try
|
||||
{
|
||||
// 搜索所有以 -generated.ysls.json 结尾的声明文件
|
||||
string[] files = Directory.GetFiles(packagePath, "*-generated.ysls.json");
|
||||
if (files.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
JArray allCommands = new JArray();
|
||||
JArray allFunctions = new JArray();
|
||||
int version = 2;
|
||||
|
||||
foreach (string file in files)
|
||||
{
|
||||
if (Path.GetFileName(file) == combinedFileName) continue;
|
||||
|
||||
string json = File.ReadAllText(file);
|
||||
if (string.IsNullOrEmpty(json)) continue;
|
||||
|
||||
JObject root = JObject.Parse(json);
|
||||
|
||||
if (root.TryGetValue("version", out JToken vToken))
|
||||
{
|
||||
version = Mathf.Max(version, vToken.Value<int>());
|
||||
}
|
||||
|
||||
if (root.TryGetValue("commands", out JToken cToken) && cToken is JArray commandsArray)
|
||||
{
|
||||
foreach (var cmd in commandsArray)
|
||||
{
|
||||
allCommands.Add(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
if (root.TryGetValue("functions", out JToken fToken) && fToken is JArray functionsArray)
|
||||
{
|
||||
foreach (var func in functionsArray)
|
||||
{
|
||||
allFunctions.Add(func);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组装合并后的 JObject
|
||||
JObject combined = new JObject
|
||||
{
|
||||
["version"] = version,
|
||||
["commands"] = allCommands,
|
||||
["functions"] = allFunctions
|
||||
};
|
||||
|
||||
// 写入项目根目录
|
||||
File.WriteAllText(outputRootPath, combined.ToString(Newtonsoft.Json.Formatting.Indented));
|
||||
Debug.Log($"[YSLS Merger] 成功将 {files.Length} 个 YSLS 声明文件合并至: '{outputRootPath}'。现在您可以在 VS Code 中一键加载此合并文件!");
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
// 自动合并失败通常发生在初始无缓存时,作警告处理,不中断编辑体验
|
||||
Debug.LogWarning($"[YSLS Merger] 自动合并 YSLS 文件失败(这通常是由于 Yarn Spinner 尚未生成初始的 ysls 缓存文件导致的,属于正常现象):{ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7178a694f7924194ea3b787839ddd162
|
||||
8
Assets/Scripts/SLSUtilities/Narrative/Prefabs.meta
Normal file
8
Assets/Scripts/SLSUtilities/Narrative/Prefabs.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8704ac67b33279943a43abe2a294147a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,857 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!1 &822921496015337489
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 2451722883515327516}
|
||||
- component: {fileID: 5821298512207332447}
|
||||
- component: {fileID: 8003291120468737892}
|
||||
- component: {fileID: 8538959725662554642}
|
||||
m_Layer: 5
|
||||
m_Name: PinIndicatorNoTitle
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &2451722883515327516
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 822921496015337489}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1735022670865247660}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 1, y: 1}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: -10, y: -10}
|
||||
m_SizeDelta: {x: 15, y: 15}
|
||||
m_Pivot: {x: 0.5, y: 0.49999997}
|
||||
--- !u!222 &5821298512207332447
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 822921496015337489}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &8003291120468737892
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 822921496015337489}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: c6dee682f807c194aba431fb54cc6d7d, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!114 &8538959725662554642
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 822921496015337489}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 1
|
||||
m_MinWidth: -1
|
||||
m_MinHeight: -1
|
||||
m_PreferredWidth: -1
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!1 &919898932274239217
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 8932679771255679868}
|
||||
- component: {fileID: 2302293284727336602}
|
||||
- component: {fileID: 2108095605505189335}
|
||||
m_Layer: 5
|
||||
m_Name: TitleText
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &8932679771255679868
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 919898932274239217}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4204169304323323032}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0.5}
|
||||
m_AnchorMax: {x: 1, y: 0.5}
|
||||
m_AnchoredPosition: {x: 10, y: 0}
|
||||
m_SizeDelta: {x: -75, y: 25}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &2302293284727336602
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 919898932274239217}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &2108095605505189335
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 919898932274239217}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_text: Title
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: ac4df5038ff71a34a993c7c9fa7316fd, type: 2}
|
||||
m_sharedMaterial: {fileID: 5688540820414838853, guid: ac4df5038ff71a34a993c7c9fa7316fd, type: 2}
|
||||
m_fontSharedMaterials: []
|
||||
m_fontMaterial: {fileID: 0}
|
||||
m_fontMaterials: []
|
||||
m_fontColor32:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_enableVertexGradient: 0
|
||||
m_colorMode: 3
|
||||
m_fontColorGradient:
|
||||
topLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
topRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_fontColorGradientPreset: {fileID: 0}
|
||||
m_spriteAsset: {fileID: 0}
|
||||
m_tintAllSprites: 0
|
||||
m_StyleSheet: {fileID: 0}
|
||||
m_TextStyleHashCode: -1183493901
|
||||
m_overrideHtmlColors: 0
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 24
|
||||
m_fontSizeBase: 24
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 0
|
||||
m_fontSizeMin: 18
|
||||
m_fontSizeMax: 72
|
||||
m_fontStyle: 1
|
||||
m_HorizontalAlignment: 1
|
||||
m_VerticalAlignment: 512
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 0
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
m_horizontalMapping: 0
|
||||
m_verticalMapping: 0
|
||||
m_uvLineOffset: 0
|
||||
m_geometrySortingOrder: 0
|
||||
m_IsTextObjectScaleStatic: 0
|
||||
m_VertexBufferAutoSizeReduction: 0
|
||||
m_useMaxVisibleDescender: 1
|
||||
m_pageToDisplay: 1
|
||||
m_margin: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_isUsingLegacyAnimationComponent: 0
|
||||
m_isVolumetricText: 0
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!1 &1288486415013319065
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 5727255906185222562}
|
||||
- component: {fileID: 2084483938339261636}
|
||||
- component: {fileID: 1815344004216971213}
|
||||
m_Layer: 5
|
||||
m_Name: PinIndicator
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &5727255906185222562
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1288486415013319065}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4204169304323323032}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 1, y: 0.5}
|
||||
m_AnchorMax: {x: 1, y: 0.5}
|
||||
m_AnchoredPosition: {x: -15, y: 0}
|
||||
m_SizeDelta: {x: 25, y: 25}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &2084483938339261636
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1288486415013319065}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &1815344004216971213
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1288486415013319065}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: c6dee682f807c194aba431fb54cc6d7d, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!1 &1770448389014005609
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 4204169304323323032}
|
||||
m_Layer: 5
|
||||
m_Name: TitleBar
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &4204169304323323032
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 1770448389014005609}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 4664076838088352918}
|
||||
- {fileID: 8932679771255679868}
|
||||
- {fileID: 259397633913720669}
|
||||
- {fileID: 5727255906185222562}
|
||||
m_Father: {fileID: 332895709820465634}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 300, y: 30}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!1 &2304233746971670081
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 4664076838088352918}
|
||||
- component: {fileID: 1051253791116523245}
|
||||
- component: {fileID: 5038012897209242806}
|
||||
m_Layer: 5
|
||||
m_Name: icon
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &4664076838088352918
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2304233746971670081}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4204169304323323032}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0.5}
|
||||
m_AnchorMax: {x: 0, y: 0.5}
|
||||
m_AnchoredPosition: {x: 25, y: 0}
|
||||
m_SizeDelta: {x: 25, y: 25}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &1051253791116523245
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2304233746971670081}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &5038012897209242806
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2304233746971670081}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: b72c64868a74dd04e9b605a346744fb5, type: 3}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
--- !u!1 &3156910686184609508
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 332895709820465634}
|
||||
- component: {fileID: 945386332789889429}
|
||||
- component: {fileID: 7828897958224438855}
|
||||
- component: {fileID: 8391361806937792526}
|
||||
- component: {fileID: 5156175450215433438}
|
||||
- component: {fileID: 7593135083786652588}
|
||||
m_Layer: 5
|
||||
m_Name: TooltipPanel
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &332895709820465634
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3156910686184609508}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 4204169304323323032}
|
||||
- {fileID: 1735022670865247660}
|
||||
m_Father: {fileID: 0}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0.5, y: 0.5}
|
||||
m_AnchorMax: {x: 0.5, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &945386332789889429
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3156910686184609508}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: b9480658e91a4d548a18e05958b5605f, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: SLSUtilities.StorySystem::SLSUtilities.StorySystem.UI.KeywordPanel
|
||||
titleBarContainer: {fileID: 4204169304323323032}
|
||||
titleText: {fileID: 2108095605505189335}
|
||||
iconImage: {fileID: 5038012897209242806}
|
||||
titlePin: {fileID: 1288486415013319065}
|
||||
descriptionContainer: {fileID: 1735022670865247660}
|
||||
descriptionText: {fileID: 284395028850057396}
|
||||
descriptionPin: {fileID: 822921496015337489}
|
||||
--- !u!114 &7828897958224438855
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3156910686184609508}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 59f8146938fff824cb5fd77236b75775, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.VerticalLayoutGroup
|
||||
m_Padding:
|
||||
m_Left: 0
|
||||
m_Right: 0
|
||||
m_Top: 0
|
||||
m_Bottom: 0
|
||||
m_ChildAlignment: 1
|
||||
m_Spacing: 0
|
||||
m_ChildForceExpandWidth: 0
|
||||
m_ChildForceExpandHeight: 0
|
||||
m_ChildControlWidth: 0
|
||||
m_ChildControlHeight: 0
|
||||
m_ChildScaleWidth: 0
|
||||
m_ChildScaleHeight: 0
|
||||
m_ReverseArrangement: 0
|
||||
--- !u!222 &8391361806937792526
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3156910686184609508}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &5156175450215433438
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3156910686184609508}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.25, g: 0.25, b: 0.25, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 21300000, guid: cca3ca807d25c1844b79eeb77e944d05, type: 3}
|
||||
m_Type: 1
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 8
|
||||
--- !u!114 &7593135083786652588
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3156910686184609508}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.ContentSizeFitter
|
||||
m_HorizontalFit: 2
|
||||
m_VerticalFit: 1
|
||||
--- !u!1 &3804837773971309440
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 1735022670865247660}
|
||||
- component: {fileID: 7312414472714823656}
|
||||
- component: {fileID: 5296572890931267466}
|
||||
- component: {fileID: 8110114097174124860}
|
||||
m_Layer: 5
|
||||
m_Name: Description
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &1735022670865247660
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3804837773971309440}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children:
|
||||
- {fileID: 789625586990900961}
|
||||
- {fileID: 2451722883515327516}
|
||||
m_Father: {fileID: 332895709820465634}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 150, y: -50}
|
||||
m_SizeDelta: {x: 300, y: 40}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &7312414472714823656
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3804837773971309440}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &5296572890931267466
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3804837773971309440}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
|
||||
m_IgnoreLayout: 0
|
||||
m_MinWidth: 300
|
||||
m_MinHeight: 40
|
||||
m_PreferredWidth: -1
|
||||
m_PreferredHeight: -1
|
||||
m_FlexibleWidth: -1
|
||||
m_FlexibleHeight: -1
|
||||
m_LayoutPriority: 1
|
||||
--- !u!114 &8110114097174124860
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 3804837773971309440}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.ContentSizeFitter
|
||||
m_HorizontalFit: 2
|
||||
m_VerticalFit: 0
|
||||
--- !u!1 &6532964554051255018
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 789625586990900961}
|
||||
- component: {fileID: 4476208540601885936}
|
||||
- component: {fileID: 284395028850057396}
|
||||
m_Layer: 5
|
||||
m_Name: Description
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &789625586990900961
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6532964554051255018}
|
||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 1735022670865247660}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: -20, y: 0}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &4476208540601885936
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6532964554051255018}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &284395028850057396
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6532964554051255018}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_text: Description
|
||||
m_isRightToLeft: 0
|
||||
m_fontAsset: {fileID: 11400000, guid: ac4df5038ff71a34a993c7c9fa7316fd, type: 2}
|
||||
m_sharedMaterial: {fileID: 5688540820414838853, guid: ac4df5038ff71a34a993c7c9fa7316fd, type: 2}
|
||||
m_fontSharedMaterials: []
|
||||
m_fontMaterial: {fileID: 0}
|
||||
m_fontMaterials: []
|
||||
m_fontColor32:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontColor: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_enableVertexGradient: 0
|
||||
m_colorMode: 3
|
||||
m_fontColorGradient:
|
||||
topLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
topRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
|
||||
bottomRight: {r: 1, g: 1, b: 1, a: 1}
|
||||
m_fontColorGradientPreset: {fileID: 0}
|
||||
m_spriteAsset: {fileID: 0}
|
||||
m_tintAllSprites: 0
|
||||
m_StyleSheet: {fileID: 0}
|
||||
m_TextStyleHashCode: -1183493901
|
||||
m_overrideHtmlColors: 0
|
||||
m_faceColor:
|
||||
serializedVersion: 2
|
||||
rgba: 4294967295
|
||||
m_fontSize: 24
|
||||
m_fontSizeBase: 24
|
||||
m_fontWeight: 400
|
||||
m_enableAutoSizing: 0
|
||||
m_fontSizeMin: 18
|
||||
m_fontSizeMax: 72
|
||||
m_fontStyle: 0
|
||||
m_HorizontalAlignment: 1
|
||||
m_VerticalAlignment: 256
|
||||
m_textAlignment: 65535
|
||||
m_characterSpacing: 0
|
||||
m_characterHorizontalScale: 1
|
||||
m_wordSpacing: 0
|
||||
m_lineSpacing: 0
|
||||
m_lineSpacingMax: 0
|
||||
m_paragraphSpacing: 0
|
||||
m_charWidthMaxAdj: 0
|
||||
m_TextWrappingMode: 1
|
||||
m_wordWrappingRatios: 0.4
|
||||
m_overflowMode: 0
|
||||
m_linkedTextComponent: {fileID: 0}
|
||||
parentLinkedComponent: {fileID: 0}
|
||||
m_enableKerning: 0
|
||||
m_ActiveFontFeatures: 6e72656b
|
||||
m_enableExtraPadding: 0
|
||||
checkPaddingRequired: 0
|
||||
m_isRichText: 1
|
||||
m_EmojiFallbackSupport: 1
|
||||
m_parseCtrlCharacters: 1
|
||||
m_isOrthographic: 1
|
||||
m_isCullingEnabled: 0
|
||||
m_horizontalMapping: 0
|
||||
m_verticalMapping: 0
|
||||
m_uvLineOffset: 0
|
||||
m_geometrySortingOrder: 0
|
||||
m_IsTextObjectScaleStatic: 0
|
||||
m_VertexBufferAutoSizeReduction: 0
|
||||
m_useMaxVisibleDescender: 1
|
||||
m_pageToDisplay: 1
|
||||
m_margin: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_isUsingLegacyAnimationComponent: 0
|
||||
m_isVolumetricText: 0
|
||||
m_hasFontAssetChanged: 0
|
||||
m_baseMaterial: {fileID: 0}
|
||||
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
|
||||
--- !u!1 &6789644190267702728
|
||||
GameObject:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
serializedVersion: 6
|
||||
m_Component:
|
||||
- component: {fileID: 259397633913720669}
|
||||
- component: {fileID: 5309614983325892601}
|
||||
- component: {fileID: 1610999632492459883}
|
||||
m_Layer: 5
|
||||
m_Name: Separator
|
||||
m_TagString: Untagged
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
--- !u!224 &259397633913720669
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6789644190267702728}
|
||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||
m_ConstrainProportionsScale: 0
|
||||
m_Children: []
|
||||
m_Father: {fileID: 4204169304323323032}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0.5}
|
||||
m_AnchorMax: {x: 1, y: 0.5}
|
||||
m_AnchoredPosition: {x: 0, y: -15}
|
||||
m_SizeDelta: {x: 0, y: 1}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!222 &5309614983325892601
|
||||
CanvasRenderer:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6789644190267702728}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &1610999632492459883
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 6789644190267702728}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
|
||||
m_Material: {fileID: 0}
|
||||
m_Color: {r: 0.5, g: 0.5, b: 0.5, a: 1}
|
||||
m_RaycastTarget: 1
|
||||
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
|
||||
m_Maskable: 1
|
||||
m_OnCullStateChanged:
|
||||
m_PersistentCalls:
|
||||
m_Calls: []
|
||||
m_Sprite: {fileID: 0}
|
||||
m_Type: 0
|
||||
m_PreserveAspect: 0
|
||||
m_FillCenter: 1
|
||||
m_FillMethod: 4
|
||||
m_FillAmount: 1
|
||||
m_FillClockwise: 1
|
||||
m_FillOrigin: 0
|
||||
m_UseSpriteMesh: 0
|
||||
m_PixelsPerUnitMultiplier: 1
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dc9a7ff16d23a2c41a0152ce105060be
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "SLSUtilities.Narrative",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:43bb4e992d9b32b4bbb25402b41e80a0",
|
||||
"GUID:34aa492b82754644eac2f903cd496268",
|
||||
"GUID:6055be8ebefd69e48b49212b09b47b2f",
|
||||
"GUID:75469ad4d38634e559750d17036d5f7c",
|
||||
"GUID:cfcd2ce455f8d1944942cdd919ecaa60"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 22a86856172c06146a539eeb9c9c67f5
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/SLSUtilities/Narrative/UI.meta
Normal file
8
Assets/Scripts/SLSUtilities/Narrative/UI.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 46e87422871a96e42be74af6d585b634
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,26 @@
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
using Yarn.Unity;
|
||||
|
||||
namespace SLSUtilities.Narrative.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 高级输入控制器,继承自官方 LineAdvancer。
|
||||
/// 在保留所有原生输入处理逻辑的基础上,
|
||||
/// 当关键词 Tooltip 窗口处于打开状态时,阻断本帧的台词推进输入。
|
||||
/// </summary>
|
||||
public class AdvancedLineAdvancer : LineAdvancer
|
||||
{
|
||||
protected override void RequestLineHurryUpInternal()
|
||||
{
|
||||
if (KeywordTooltipUI.IsBlockingDialogueInput) return;
|
||||
base.RequestLineHurryUpInternal();
|
||||
}
|
||||
|
||||
public override void RequestNextLine()
|
||||
{
|
||||
if (KeywordTooltipUI.IsBlockingDialogueInput) return;
|
||||
base.RequestNextLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c3b5f02f9e109140b36d9e3bad02271
|
||||
@@ -0,0 +1,305 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using Yarn.Unity;
|
||||
using Yarn.Markup;
|
||||
|
||||
namespace SLSUtilities.Narrative.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 高级台词展现层,继承自官方 LinePresenter。
|
||||
/// 在保留所有原生功能(打字机、淡入淡出、LineAdvancer 状态机)的基础上,
|
||||
/// 扩展了角色立绘/头像切换、关键词高亮与悬停百科等功能。
|
||||
/// </summary>
|
||||
public class AdvancedLinePresenter : LinePresenter
|
||||
{
|
||||
[TitleGroup("立绘系统 (Portrait System)", Alignment = TitleAlignments.Centered)]
|
||||
|
||||
[BoxGroup("立绘系统 (Portrait System)/UI 引用")]
|
||||
[Tooltip("用于显示角色立绘/头像的 Image 组件")]
|
||||
[SerializeField] private Image portraitImage;
|
||||
|
||||
[BoxGroup("立绘系统 (Portrait System)/UI 引用")]
|
||||
[Tooltip("立绘的容器节点。当没有立绘时整体隐藏,避免空白区域占位")]
|
||||
[SerializeField] private GameObject portraitContainer;
|
||||
|
||||
[TitleGroup("关键词系统 (Keyword System)", Alignment = TitleAlignments.Centered)]
|
||||
|
||||
[BoxGroup("关键词系统 (Keyword System)/设置")]
|
||||
[LabelText("启用关键词高亮")]
|
||||
[Tooltip("是否在台词中自动识别并高亮关键词")]
|
||||
[SerializeField] private bool enableKeywordHighlight = true;
|
||||
|
||||
[BoxGroup("关键词系统 (Keyword System)/设置")]
|
||||
[ShowIf(nameof(enableKeywordHighlight))]
|
||||
[LabelText("高亮颜色 (Highlight Color)")]
|
||||
[Tooltip("关键词文本的高亮颜色")]
|
||||
[SerializeField] private Color keywordHighlightColor = new Color(0.67f, 0.87f, 1f); // #AADDFF
|
||||
|
||||
// 缓存是否已构建的标记,避免每句台词重复构建
|
||||
private bool _keywordCacheBuilt = false;
|
||||
|
||||
// 当前台词的 Markup 解析结果,保存到 PostProcessDisplayText 使用
|
||||
private MarkupParseResult _currentLineMarkup;
|
||||
|
||||
/// <summary>最后一次说话的角色名称(Yarn 脚本中的 CharacterName)</summary>
|
||||
public static string LastSpeakerName { get; set; }
|
||||
|
||||
/// <summary>当行内 Markup 标记请求播放动画时触发此事件</summary>
|
||||
public static event Action<string, string> OnPlayAnimationRequested;
|
||||
|
||||
/// <summary>当行内 Markup 标记请求停止动画时触发此事件</summary>
|
||||
public static event Action<string> OnStopAnimationRequested;
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 生命周期
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public override YarnTask OnDialogueStartedAsync()
|
||||
{
|
||||
// 在对话开始时构建关键词缓存
|
||||
if (enableKeywordHighlight && StorySystem.Database != null)
|
||||
{
|
||||
KeywordProcessor.BuildCache(StorySystem.Database.keywords);
|
||||
_keywordCacheBuilt = true;
|
||||
}
|
||||
|
||||
return base.OnDialogueStartedAsync();
|
||||
}
|
||||
|
||||
public override YarnTask OnDialogueCompleteAsync()
|
||||
{
|
||||
_keywordCacheBuilt = false;
|
||||
LastSpeakerName = null; // 对话结束时置空
|
||||
return base.OnDialogueCompleteAsync();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 台词处理
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public override async YarnTask RunLineAsync(LocalizedLine line, LineCancellationToken token)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(line.CharacterName))
|
||||
{
|
||||
LastSpeakerName = line.CharacterName; // 记录当前说话角色
|
||||
}
|
||||
|
||||
// 确保打字机已通过 IAsyncTypewriter 装饰器包裹,防止其在开始打字时抹除关键词富文本标签。
|
||||
// 基类 Awake 必然已实例化 Typewriter,在此将其替换为我们的富文本打字机包装器。
|
||||
if (Typewriter != null && !(Typewriter is KeywordTypewriterWrapper))
|
||||
{
|
||||
Typewriter = new KeywordTypewriterWrapper(Typewriter, markup => KeywordProcessor.ProcessWithMarkup(markup));
|
||||
}
|
||||
|
||||
// 保存当前台词的 Markup,供 PostProcessDisplayText 使用
|
||||
_currentLineMarkup = line.TextWithoutCharacterName;
|
||||
|
||||
// 提取并解析 Yarn Markup 行内动作标记 (方案 B)
|
||||
if (line.TextWithoutCharacterName.Attributes != null)
|
||||
{
|
||||
foreach (var attribute in line.TextWithoutCharacterName.Attributes)
|
||||
{
|
||||
if (attribute.Name == "anim" || attribute.Name == "play_animation")
|
||||
{
|
||||
if (attribute.Properties.TryGetValue(attribute.Name, out var animValue))
|
||||
{
|
||||
string animName = animValue.StringValue;
|
||||
// 默认指向当前发言说话人,并解析为标准英文 ID
|
||||
string targetNpc = ResolveStandardCharacterId(line.CharacterName);
|
||||
|
||||
// 同时也支持显式指定其他 NPC,例如 [anim="PushButton" npc="SLS"/]
|
||||
if (attribute.Properties.TryGetValue("npc", out var npcValue))
|
||||
{
|
||||
targetNpc = ResolveStandardCharacterId(npcValue.StringValue);
|
||||
}
|
||||
|
||||
OnPlayAnimationRequested?.Invoke(animName, targetNpc);
|
||||
}
|
||||
}
|
||||
else if (attribute.Name == "stop_anim" || attribute.Name == "stop_animation")
|
||||
{
|
||||
string targetNpc = ResolveStandardCharacterId(line.CharacterName);
|
||||
if (attribute.Properties.TryGetValue("npc", out var npcValue))
|
||||
{
|
||||
targetNpc = ResolveStandardCharacterId(npcValue.StringValue);
|
||||
}
|
||||
|
||||
OnStopAnimationRequested?.Invoke(targetNpc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 在台词展示之前更新立绘
|
||||
UpdatePortrait(line);
|
||||
|
||||
// 调用父类处理所有其余逻辑(文本设置、打字机、淡入淡出、等待输入)
|
||||
// 父类会在 Typewriter.PrepareForContent 之后调用 PostProcessDisplayText()
|
||||
await base.RunLineAsync(line, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 由父类 LinePresenter 在 Typewriter.PrepareForContent 之后调用。
|
||||
/// 此时文本已完整设置到 TMP 组件上,但打字机尚未开始逐字展示。
|
||||
/// 我们在此注入关键词的 link 标签。
|
||||
/// </summary>
|
||||
protected override void PostProcessDisplayText()
|
||||
{
|
||||
if (!enableKeywordHighlight || !_keywordCacheBuilt) return;
|
||||
if (lineText == null) return;
|
||||
|
||||
// 使用基于 [kw] Markup 标签的处理,替代正则自动扫描
|
||||
lineText.text = KeywordProcessor.ProcessWithMarkup(_currentLineMarkup);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 立绘系统
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
private void UpdatePortrait(LocalizedLine line)
|
||||
{
|
||||
if (portraitImage == null) return;
|
||||
|
||||
string characterName = line.CharacterName;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(characterName) || StorySystem.Database == null)
|
||||
{
|
||||
SetPortraitVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
CharacterData charData = null;
|
||||
foreach (var c in StorySystem.Database.characters)
|
||||
{
|
||||
if (c != null && (string.Equals(c.nameKey, characterName, StringComparison.OrdinalIgnoreCase) ||
|
||||
c.alias.Contains(characterName)))
|
||||
{
|
||||
charData = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (charData == null)
|
||||
{
|
||||
SetPortraitVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
YarnTagParser.Parse(line.Metadata, out var kvTags, out _);
|
||||
kvTags.TryGetValue("mood", out string mood);
|
||||
|
||||
Sprite targetSprite = ResolvePortraitSprite(charData, mood);
|
||||
|
||||
if (targetSprite == null)
|
||||
{
|
||||
SetPortraitVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
portraitImage.sprite = targetSprite;
|
||||
SetPortraitVisible(true);
|
||||
}
|
||||
|
||||
private static Sprite ResolvePortraitSprite(CharacterData charData, string mood)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(mood) &&
|
||||
charData.expressions != null &&
|
||||
charData.expressions.TryGetValue(mood, out Sprite moodSprite) &&
|
||||
moodSprite != null)
|
||||
{
|
||||
return moodSprite;
|
||||
}
|
||||
|
||||
return charData.defaultPortrait;
|
||||
}
|
||||
|
||||
private void SetPortraitVisible(bool visible)
|
||||
{
|
||||
if (portraitContainer != null)
|
||||
portraitContainer.SetActive(visible);
|
||||
else if (portraitImage != null)
|
||||
portraitImage.enabled = visible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 Yarn 传回的本地化说话人名字(如 "引导者")反向解析为系统内部注册的标准英文 ID(如 "Guide")。
|
||||
/// </summary>
|
||||
private string ResolveStandardCharacterId(string speakerName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(speakerName) || StorySystem.Database == null)
|
||||
return speakerName;
|
||||
|
||||
foreach (var c in StorySystem.Database.characters)
|
||||
{
|
||||
if (c == null) continue;
|
||||
|
||||
// 1. 若已经是标准英文名 (nameKey),直接返回
|
||||
if (string.Equals(c.nameKey, speakerName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return c.nameKey;
|
||||
}
|
||||
|
||||
// 2. 若匹配到显示名称 (displayName),则返回对应的标准英文 ID (nameKey)
|
||||
if (c.alias != null && c.alias.Contains(speakerName))
|
||||
{
|
||||
return c.nameKey;
|
||||
}
|
||||
}
|
||||
|
||||
return speakerName; // 未匹配到则保留原样
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 关键词打字机包装器。
|
||||
/// 解决官方打字机在 PrepareForContent 和 RunTypewriter 时,
|
||||
/// 会强行用 plainText 覆盖 text 组件,导致我们注入的富文本高亮标签被抹除的问题。
|
||||
/// </summary>
|
||||
public class KeywordTypewriterWrapper : IAsyncTypewriter
|
||||
{
|
||||
private readonly IAsyncTypewriter _inner;
|
||||
private readonly Func<MarkupParseResult, string> _processMarkupFunc;
|
||||
|
||||
public KeywordTypewriterWrapper(IAsyncTypewriter inner, Func<MarkupParseResult, string> processMarkupFunc)
|
||||
{
|
||||
_inner = inner;
|
||||
_processMarkupFunc = processMarkupFunc;
|
||||
}
|
||||
|
||||
public TMPro.TMP_Text? TextElement
|
||||
{
|
||||
get => _inner.TextElement;
|
||||
set => _inner.TextElement = value;
|
||||
}
|
||||
|
||||
public List<IActionMarkupHandler> ActionMarkupHandlers => _inner.ActionMarkupHandlers;
|
||||
|
||||
public void PrepareForContent(MarkupParseResult line)
|
||||
{
|
||||
_inner.PrepareForContent(line);
|
||||
if (TextElement != null)
|
||||
{
|
||||
TextElement.text = _processMarkupFunc(line);
|
||||
}
|
||||
}
|
||||
|
||||
public async YarnTask RunTypewriter(MarkupParseResult line, System.Threading.CancellationToken cancellationToken)
|
||||
{
|
||||
var task = _inner.RunTypewriter(line, cancellationToken);
|
||||
if (TextElement != null)
|
||||
{
|
||||
TextElement.text = _processMarkupFunc(line);
|
||||
}
|
||||
await task;
|
||||
if (TextElement != null)
|
||||
{
|
||||
TextElement.text = _processMarkupFunc(line);
|
||||
}
|
||||
}
|
||||
|
||||
public void ContentWillDismiss() => _inner.ContentWillDismiss();
|
||||
public void ContentDidDismiss() => _inner.ContentDidDismiss();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1762f73b835dbd24f934d49bcb0c3c8f
|
||||
128
Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedOptionItem.cs
Normal file
128
Assets/Scripts/SLSUtilities/Narrative/UI/AdvancedOptionItem.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using Yarn.Unity;
|
||||
using Yarn.Markup;
|
||||
using TMPro;
|
||||
|
||||
namespace SLSUtilities.Narrative.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 高级选项项,继承自官方 OptionItem。
|
||||
/// 支持解析选项中的 #desc: 和 #fail: 元数据标签,
|
||||
/// 通过 OptionTooltipUI 显示选项提示,
|
||||
/// 以及使用 [kw] Yarn Markup 标签高亮关键词。
|
||||
/// </summary>
|
||||
public class AdvancedOptionItem : OptionItem
|
||||
{
|
||||
public string TooltipDesc { get; private set; }
|
||||
public string TooltipFail { get; private set; }
|
||||
|
||||
public TMP_Text GetTextComponent() => text;
|
||||
|
||||
// 追踪当前是否是由鼠标指针触发的选中状态
|
||||
// OnPointerEnter 在 OnSelect 之前同步设置此标记,OnDeselect 清除
|
||||
private bool _pointerEntered = false;
|
||||
|
||||
public override DialogueOption Option
|
||||
{
|
||||
get => base.Option;
|
||||
set
|
||||
{
|
||||
// 调用基类 setter:设置 _option、interactable 和 ApplyStyle
|
||||
// 注意:基类也会设置 text.text,我们稍后会覆盖它
|
||||
base.Option = value;
|
||||
|
||||
// 解析 Tooltip 元数据标签 (#desc: / #fail:)
|
||||
TooltipDesc = null;
|
||||
TooltipFail = null;
|
||||
|
||||
if (value.Line.Metadata != null)
|
||||
{
|
||||
YarnTagParser.Parse(value.Line.Metadata, out var kvTags, out _);
|
||||
|
||||
if (kvTags.TryGetValue("desc", out string desc))
|
||||
{
|
||||
TooltipDesc = desc.Replace("_", " ");
|
||||
}
|
||||
|
||||
if (kvTags.TryGetValue("fail", out string fail))
|
||||
{
|
||||
TooltipFail = fail.Replace("_", " ");
|
||||
}
|
||||
}
|
||||
|
||||
// 选项文本高亮处理:
|
||||
// 仅通过 ProcessWithMarkup 处理手动标记的 [kw] 或 [kw id="..."] 标签。
|
||||
// 不进行正则自动扫描(只高亮明确被 [kw] 标记的部分,与台词表现一致)。
|
||||
if (text != null)
|
||||
{
|
||||
string processed = KeywordProcessor.ProcessWithMarkup(value.Line.TextWithoutCharacterName);
|
||||
|
||||
if (disabledStrikeThrough && !value.IsAvailable)
|
||||
{
|
||||
processed = $"<s>{processed}</s>";
|
||||
}
|
||||
|
||||
text.text = processed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
// 在调用 base.OnPointerEnter(其中会同步调用 OnSelect)之前
|
||||
// 设置标记,使 OnSelect 能知道这是鼠标触发的
|
||||
_pointerEntered = true;
|
||||
base.OnPointerEnter(eventData);
|
||||
}
|
||||
|
||||
public override void OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
base.OnPointerExit(eventData);
|
||||
// 鼠标离开后清除,但不关闭 Tooltip(由 OptionTooltipUI 的文本区域检测控制)
|
||||
_pointerEntered = false;
|
||||
}
|
||||
|
||||
public override void OnPointerClick(PointerEventData eventData)
|
||||
{
|
||||
// 只有当点击的是鼠标左键时,才允许选择并推进此选项,阻断鼠标右键的选择触发
|
||||
if (eventData.button == PointerEventData.InputButton.Left)
|
||||
{
|
||||
base.OnPointerClick(eventData);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnSelect(BaseEventData eventData)
|
||||
{
|
||||
base.OnSelect(eventData);
|
||||
|
||||
// 将"是否为鼠标触发"传递给 OptionTooltipUI,用于决定定位方式
|
||||
OptionTooltipUI.Instance?.OnOptionSelected(this, _pointerEntered);
|
||||
}
|
||||
|
||||
public override void OnDeselect(BaseEventData eventData)
|
||||
{
|
||||
base.OnDeselect(eventData);
|
||||
_pointerEntered = false;
|
||||
OptionTooltipUI.Instance?.OnOptionDeselected(this);
|
||||
}
|
||||
|
||||
protected override void OnEnable()
|
||||
{
|
||||
base.OnEnable();
|
||||
if (KeywordTooltipUI.Instance != null && text != null)
|
||||
{
|
||||
KeywordTooltipUI.Instance.RegisterExternalText(text);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDisable()
|
||||
{
|
||||
base.OnDisable();
|
||||
if (KeywordTooltipUI.Instance != null && text != null)
|
||||
{
|
||||
KeywordTooltipUI.Instance.UnregisterExternalText(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3965a9528049b914baeeff5e76f39162
|
||||
@@ -0,0 +1,74 @@
|
||||
using UnityEngine;
|
||||
using Yarn.Unity;
|
||||
using Sirenix.OdinInspector;
|
||||
|
||||
namespace SLSUtilities.Narrative.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 高级选项展现层,继承自官方 OptionsPresenter。
|
||||
/// </summary>
|
||||
public class AdvancedOptionsPresenter : OptionsPresenter
|
||||
{
|
||||
[TitleGroup("Advanced Settings", Alignment = TitleAlignments.Centered)]
|
||||
[BoxGroup("Advanced Settings/Prefabs")]
|
||||
[Required("需要指定 AdvancedOptionItem 预制体")]
|
||||
[SerializeField] private AdvancedOptionItem advancedOptionViewPrefab;
|
||||
|
||||
public override YarnTask OnDialogueStartedAsync()
|
||||
{
|
||||
// 建立关键词缓存,保证 ProcessWithMarkup 能正确查找关键词数据
|
||||
// (不依赖 AdvancedLinePresenter 是否已经运行)
|
||||
if (StorySystem.Database != null)
|
||||
KeywordProcessor.BuildCache(StorySystem.Database.keywords);
|
||||
|
||||
return base.OnDialogueStartedAsync();
|
||||
}
|
||||
|
||||
public override YarnTask<DialogueOption?> RunOptionsAsync(DialogueOption[] dialogueOptions, LineCancellationToken cancellationToken)
|
||||
{
|
||||
// 过滤掉不可用且带有 "hide" 或 "#hide" 标签的选项
|
||||
var filteredOptions = new System.Collections.Generic.List<DialogueOption>();
|
||||
foreach (var option in dialogueOptions)
|
||||
{
|
||||
bool shouldHide = false;
|
||||
if (!option.IsAvailable && option.Line != null && option.Line.Metadata != null)
|
||||
{
|
||||
foreach (var tag in option.Line.Metadata)
|
||||
{
|
||||
if (tag.Equals("hide", System.StringComparison.OrdinalIgnoreCase) ||
|
||||
tag.Equals("#hide", System.StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
shouldHide = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldHide)
|
||||
{
|
||||
filteredOptions.Add(option);
|
||||
}
|
||||
}
|
||||
|
||||
return base.RunOptionsAsync(filteredOptions.ToArray(), cancellationToken);
|
||||
}
|
||||
|
||||
protected override OptionItem CreateNewOptionView()
|
||||
{
|
||||
var targetTransform = canvasGroup != null ? canvasGroup.transform : this.transform;
|
||||
|
||||
var optionView = Instantiate(advancedOptionViewPrefab, targetTransform, false);
|
||||
|
||||
if (optionView == null)
|
||||
{
|
||||
Debug.LogError("Failed to instantiate advancedOptionViewPrefab.");
|
||||
return null;
|
||||
}
|
||||
|
||||
optionView.transform.SetAsLastSibling();
|
||||
optionView.gameObject.SetActive(false);
|
||||
|
||||
return optionView;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7e64bee4ae3276841b0949789632d0b4
|
||||
263
Assets/Scripts/SLSUtilities/Narrative/UI/KeywordProcessor.cs
Normal file
263
Assets/Scripts/SLSUtilities/Narrative/UI/KeywordProcessor.cs
Normal file
@@ -0,0 +1,263 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using UnityEngine;
|
||||
using Yarn.Markup;
|
||||
|
||||
namespace SLSUtilities.Narrative.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 关键词文本处理器。
|
||||
/// 扫描台词文本,将已注册的关键词包裹为 TMP <link> 标签,
|
||||
/// 使其可被悬停检测和高亮显示。
|
||||
/// </summary>
|
||||
public static class KeywordProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// 关键词的 link ID 前缀,用于与其他 link 类型区分。
|
||||
/// 例如:<link="kw:灵能者">
|
||||
/// </summary>
|
||||
public const string LinkPrefix = "kw:";
|
||||
|
||||
// 已编译的关键词匹配正则(所有触发词按长度降序排列)
|
||||
private static Regex _keywordPattern;
|
||||
|
||||
// 触发词 → KeywordData 的映射表(大小写不敏感)
|
||||
private static Dictionary<string, KeywordData> _triggerLookup;
|
||||
|
||||
// 主关键词 → KeywordData 的映射表(用于通过 link ID 反查)
|
||||
private static Dictionary<string, KeywordData> _primaryLookup;
|
||||
|
||||
// 用于识别 RichText 标签的正则,避免在标签内部匹配关键词
|
||||
private static readonly Regex RichTextTagPattern =
|
||||
new Regex(@"<[^>]+>", RegexOptions.Compiled);
|
||||
|
||||
/// <summary>
|
||||
/// 从数据库构建关键词缓存。
|
||||
/// 应在对话开始时调用一次,或在关键词列表变更后重新调用。
|
||||
/// </summary>
|
||||
public static void BuildCache(List<KeywordData> keywords)
|
||||
{
|
||||
_triggerLookup = new Dictionary<string, KeywordData>(System.StringComparer.OrdinalIgnoreCase);
|
||||
_primaryLookup = new Dictionary<string, KeywordData>(System.StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (keywords == null || keywords.Count == 0)
|
||||
{
|
||||
_keywordPattern = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var allTriggers = new List<string>();
|
||||
|
||||
foreach (var kw in keywords)
|
||||
{
|
||||
if (kw == null || string.IsNullOrWhiteSpace(kw.keyword)) continue;
|
||||
|
||||
// 注册主关键词到 primaryLookup
|
||||
if (!_primaryLookup.ContainsKey(kw.keyword))
|
||||
_primaryLookup[kw.keyword] = kw;
|
||||
|
||||
// 注册所有触发词(主关键词 + 别名)到 triggerLookup
|
||||
foreach (var trigger in kw.GetAllTriggerWords())
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(trigger) && !_triggerLookup.ContainsKey(trigger))
|
||||
{
|
||||
_triggerLookup[trigger] = kw;
|
||||
allTriggers.Add(trigger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allTriggers.Count == 0)
|
||||
{
|
||||
_keywordPattern = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 按长度降序排列,确保"灵能者协会"优先于"灵能者"匹配
|
||||
allTriggers.Sort((a, b) => b.Length.CompareTo(a.Length));
|
||||
|
||||
// 构建正则:将所有触发词用 | 连接
|
||||
var escapedTriggers = allTriggers.Select(Regex.Escape);
|
||||
string pattern = string.Join("|", escapedTriggers);
|
||||
|
||||
_keywordPattern = new Regex(pattern, RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理台词文本:扫描并将匹配的关键词包裹为带高亮样式的 TMP link 标签。
|
||||
/// 会自动跳过已有的 RichText 标签内部,避免破坏现有排版。
|
||||
/// </summary>
|
||||
/// <param name="rawText">原始台词文本(可能已包含 RichText 标签)</param>
|
||||
/// <param name="highlightColor">关键词高亮颜色(十六进制,如 "#AADDFF")</param>
|
||||
/// <returns>处理后的文本</returns>
|
||||
public static string Process(string rawText, string highlightColor = "#AADDFF")
|
||||
{
|
||||
if (_keywordPattern == null || string.IsNullOrEmpty(rawText))
|
||||
return rawText;
|
||||
|
||||
// 将文本按"RichText 标签"和"纯文本"交替切分,
|
||||
// 只对纯文本段进行关键词替换,标签段原样保留。
|
||||
var result = new StringBuilder(rawText.Length * 2);
|
||||
int lastIndex = 0;
|
||||
|
||||
foreach (Match tagMatch in RichTextTagPattern.Matches(rawText))
|
||||
{
|
||||
// 处理标签前的纯文本段
|
||||
if (tagMatch.Index > lastIndex)
|
||||
{
|
||||
string segment = rawText.Substring(lastIndex, tagMatch.Index - lastIndex);
|
||||
result.Append(ReplaceKeywordsInSegment(segment, highlightColor));
|
||||
}
|
||||
|
||||
// 标签本身原样追加
|
||||
result.Append(tagMatch.Value);
|
||||
lastIndex = tagMatch.Index + tagMatch.Length;
|
||||
}
|
||||
|
||||
// 处理最后一个标签之后的剩余纯文本
|
||||
if (lastIndex < rawText.Length)
|
||||
{
|
||||
string remaining = rawText.Substring(lastIndex);
|
||||
result.Append(ReplaceKeywordsInSegment(remaining, highlightColor));
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 基于 Yarn Markup 属性处理台词/选项文本:
|
||||
/// 将 [kw] 和 [kw id="主关键词"] 标签转换为 TMP link/style 标签。
|
||||
///
|
||||
/// Yarn 标签语法:
|
||||
/// [kw]迎雾森林[/kw] —— 关键词 = 标签内的文本
|
||||
/// [kw id="迎雾森林"]林地[/kw] —— 关键词 = “迎雾森林”,显示文本 = “林地”
|
||||
/// </summary>
|
||||
/// <param name="markup">来自 Yarn 的 MarkupParseResult(LocalizedLine.TextWithoutCharacterName 等)</param>
|
||||
/// <returns>包含 TMP 忌工标签的处理后字符串</returns>
|
||||
public static string ProcessWithMarkup(MarkupParseResult markup)
|
||||
{
|
||||
string plainText = markup.Text;
|
||||
if (string.IsNullOrEmpty(plainText)) return plainText;
|
||||
|
||||
// 收集所有 "kw" 属性
|
||||
var kwAttributes = new List<MarkupAttribute>();
|
||||
foreach (var attr in markup.Attributes)
|
||||
{
|
||||
if (attr.Name == "kw")
|
||||
kwAttributes.Add(attr);
|
||||
}
|
||||
|
||||
// 没有关键词标签,直接返回原始文本
|
||||
if (kwAttributes.Count == 0) return plainText;
|
||||
|
||||
// 按位置降序排列,从字符串末尾开始插入,避免早期插入造成索引偏移
|
||||
kwAttributes.Sort((a, b) => b.Position.CompareTo(a.Position));
|
||||
|
||||
var sb = new StringBuilder(plainText);
|
||||
|
||||
foreach (var attr in kwAttributes)
|
||||
{
|
||||
// 确定关键词:优先使用 id 属性,否则取标签覆盖的文本
|
||||
string keyword;
|
||||
if (attr.Properties.TryGetValue("id", out var idProp))
|
||||
{
|
||||
keyword = idProp.StringValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
keyword = plainText.Substring(attr.Position, attr.Length);
|
||||
}
|
||||
|
||||
// 在数据库中查找该关键词,找不到则跳过(不插入任何标签)
|
||||
if (FindByPrimaryKeyword(keyword) == null) continue;
|
||||
|
||||
// 先插入闭合标签(索引较大),再插入开放标签
|
||||
sb.Insert(attr.Position + attr.Length, "</link></style>");
|
||||
sb.Insert(attr.Position, $"<style=\"kw\"><link=\"{LinkPrefix}{keyword}\">");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理关键词的解释文本(用于嵌套 Tooltip)。
|
||||
/// 与 Process 相同,但会排除自身关键词,避免自引用循环。
|
||||
/// </summary>
|
||||
public static string ProcessDescription(string description, string excludeKeyword,
|
||||
string highlightColor = "#AADDFF")
|
||||
{
|
||||
if (_keywordPattern == null || string.IsNullOrEmpty(description))
|
||||
return description;
|
||||
|
||||
var result = new StringBuilder(description.Length * 2);
|
||||
int lastIndex = 0;
|
||||
|
||||
foreach (Match tagMatch in RichTextTagPattern.Matches(description))
|
||||
{
|
||||
if (tagMatch.Index > lastIndex)
|
||||
{
|
||||
string segment = description.Substring(lastIndex, tagMatch.Index - lastIndex);
|
||||
result.Append(ReplaceKeywordsInSegment(segment, highlightColor, excludeKeyword));
|
||||
}
|
||||
result.Append(tagMatch.Value);
|
||||
lastIndex = tagMatch.Index + tagMatch.Length;
|
||||
}
|
||||
|
||||
if (lastIndex < description.Length)
|
||||
{
|
||||
string remaining = description.Substring(lastIndex);
|
||||
result.Append(ReplaceKeywordsInSegment(remaining, highlightColor, excludeKeyword));
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 link ID(去掉 "kw:" 前缀后的主关键词)反查对应的 KeywordData。
|
||||
/// </summary>
|
||||
public static KeywordData FindByPrimaryKeyword(string primaryKeyword)
|
||||
{
|
||||
if (_primaryLookup == null || string.IsNullOrEmpty(primaryKeyword))
|
||||
return null;
|
||||
|
||||
_primaryLookup.TryGetValue(primaryKeyword, out var result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 TMP link ID 字符串中提取主关键词。
|
||||
/// 例如输入 "kw:灵能者",返回 "灵能者"。
|
||||
/// 如果不是关键词类型的 link,返回 null。
|
||||
/// </summary>
|
||||
public static string ExtractKeywordFromLinkId(string linkId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(linkId) || !linkId.StartsWith(LinkPrefix))
|
||||
return null;
|
||||
|
||||
return linkId.Substring(LinkPrefix.Length);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
private static string ReplaceKeywordsInSegment(string segment, string color,
|
||||
string excludeKeyword = null)
|
||||
{
|
||||
return _keywordPattern.Replace(segment, match =>
|
||||
{
|
||||
if (!_triggerLookup.TryGetValue(match.Value, out var kwData))
|
||||
return match.Value;
|
||||
|
||||
// 排除自身关键词(用于嵌套 Tooltip 防止自引用)
|
||||
if (excludeKeyword != null &&
|
||||
string.Equals(kwData.keyword, excludeKeyword, System.StringComparison.OrdinalIgnoreCase))
|
||||
return match.Value;
|
||||
|
||||
// 包裹为 TMP link 标签
|
||||
// link ID 格式: "kw:主关键词"
|
||||
return $"<style=\"kw\"><link=\"{LinkPrefix}{kwData.keyword}\">{match.Value}</link></style>";
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 045da86a92e1064419ec0949cf6a3c51
|
||||
337
Assets/Scripts/SLSUtilities/Narrative/UI/KeywordTooltipUI.cs
Normal file
337
Assets/Scripts/SLSUtilities/Narrative/UI/KeywordTooltipUI.cs
Normal file
@@ -0,0 +1,337 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Sirenix.OdinInspector;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace SLSUtilities.Narrative.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 关键词浮动窗口管理器。
|
||||
/// 检测鼠标在 TMP 文本上悬停的 link 标签,弹出关键词解释窗口。
|
||||
/// 支持嵌套窗口、右键固定、点击外部关闭,以及左键关闭时阻断台词推进。
|
||||
///
|
||||
/// 面板的实际内容显示、定位和固定状态由 <see cref="TooltipPanel"/> 组件管理,
|
||||
/// 本类仅负责悬停检测、生命周期编排和输入分发。
|
||||
/// </summary>
|
||||
public class KeywordTooltipUI : MonoBehaviour
|
||||
{
|
||||
// ---------------------------------------------------------------
|
||||
// 静态属性:供 LineAdvancer 查询是否需要阻断本帧输入
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// 当任意 Tooltip 窗口处于打开状态时为 true。
|
||||
/// LineAdvancer 应在处理台词推进前检查此值。
|
||||
/// </summary>
|
||||
public static bool IsBlockingDialogueInput { get; private set; }
|
||||
|
||||
public static KeywordTooltipUI Instance { get; private set; }
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Inspector 配置
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[TitleGroup("核心引用 (Core References)", Alignment = TitleAlignments.Centered)]
|
||||
|
||||
[BoxGroup("核心引用 (Core References)/UI")]
|
||||
[Required("需要指定 Tooltip 面板的 Prefab(必须挂载 TooltipPanel 组件)")]
|
||||
[SerializeField] private GameObject tooltipPanelPrefab;
|
||||
|
||||
[BoxGroup("核心引用 (Core References)/UI")]
|
||||
[Required("Tooltip 生成的父级容器(RectTransform)")]
|
||||
[SerializeField] private RectTransform tooltipContainer;
|
||||
|
||||
[BoxGroup("核心引用 (Core References)/UI")]
|
||||
[Tooltip("主台词文本组件,用于检测鼠标悬停的关键词链接")]
|
||||
[SerializeField] private TMP_Text mainLineText;
|
||||
|
||||
[BoxGroup("核心引用 (Core References)/UI")]
|
||||
[Tooltip("渲染 Canvas 的摄像机。Screen Space Overlay 模式下留空")]
|
||||
[SerializeField] private Camera uiCamera;
|
||||
|
||||
[TitleGroup("行为设置 (Behavior Settings)", Alignment = TitleAlignments.Centered)]
|
||||
|
||||
[BoxGroup("行为设置 (Behavior Settings)/定位")]
|
||||
[Tooltip("Tooltip 左下角相对于鼠标的屏幕像素偏移量")]
|
||||
[SerializeField] private Vector2 tooltipOffset = new Vector2(12f, 12f);
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 内部状态
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
// 所有当前打开的 Tooltip 面板(包含固定的和悬停的)
|
||||
private readonly List<TooltipPanel> _openPanels = new List<TooltipPanel>();
|
||||
|
||||
// 当前唯一的悬停 Tooltip(未固定,跟随鼠标)
|
||||
private TooltipPanel _hoverPanel;
|
||||
|
||||
// 上一帧检测到的悬停关键词
|
||||
private string _lastHoveredKeyword;
|
||||
|
||||
// 供外部(如 OptionTooltipUI)注册的额外检测文本
|
||||
private readonly List<TMP_Text> _externalTexts = new List<TMP_Text>();
|
||||
|
||||
/// <summary>
|
||||
/// 当前是否有未固定的悬停面板
|
||||
/// </summary>
|
||||
public bool HasHoverPanel => _hoverPanel != null;
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Unity 生命周期
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
CloseAllTooltips();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
HandleHoverDetection();
|
||||
HandleClickInput();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 悬停检测
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
private void HandleHoverDetection()
|
||||
{
|
||||
Vector2 mousePos = Mouse.current.position.ReadValue();
|
||||
|
||||
// 检测当前鼠标命中的关键词链接
|
||||
string hoveredKeyword = DetectHoveredKeyword(mousePos);
|
||||
|
||||
// 如果鼠标没命中链接,但在当前 Hover 面板内,保持悬停状态不变
|
||||
bool mouseInsideHoverPanel = _hoverPanel != null &&
|
||||
_hoverPanel.ContainsScreenPoint(mousePos, uiCamera);
|
||||
|
||||
if (mouseInsideHoverPanel && hoveredKeyword == null)
|
||||
{
|
||||
// 鼠标从链接移到了 Tooltip 面板 → 保持显示,不移动位置
|
||||
return;
|
||||
}
|
||||
|
||||
// 悬停目标变化 → 刷新 Hover Tooltip
|
||||
if (hoveredKeyword != _lastHoveredKeyword)
|
||||
{
|
||||
_lastHoveredKeyword = hoveredKeyword;
|
||||
CloseHoverTooltip();
|
||||
|
||||
if (!string.IsNullOrEmpty(hoveredKeyword))
|
||||
{
|
||||
// 如果该关键词已经有固定窗口存在 → 不创建新的 Hover
|
||||
if (!HasPinnedPanelForKeyword(hoveredKeyword))
|
||||
{
|
||||
var kwData = KeywordProcessor.FindByPrimaryKeyword(hoveredKeyword);
|
||||
if (kwData != null)
|
||||
_hoverPanel = SpawnPanel(kwData, mousePos, pinned: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 跟随鼠标更新位置(仅对 Hover 面板)
|
||||
if (_hoverPanel != null && !_hoverPanel.IsPinned)
|
||||
_hoverPanel.PositionAtScreenPoint(mousePos, tooltipOffset);
|
||||
}
|
||||
|
||||
private string DetectHoveredKeyword(Vector2 mousePos)
|
||||
{
|
||||
// 先检测主台词文本
|
||||
string kw = DetectLinkAt(mainLineText, mousePos);
|
||||
if (kw != null) return kw;
|
||||
|
||||
// 再检测所有已打开面板内的描述文本(支持嵌套)
|
||||
foreach (var panel in _openPanels)
|
||||
{
|
||||
kw = DetectLinkAt(panel.DescriptionText, mousePos);
|
||||
if (kw != null) return kw;
|
||||
}
|
||||
|
||||
// 最后检测外部注册的文本(如选项文本)
|
||||
foreach (var extText in _externalTexts)
|
||||
{
|
||||
kw = DetectLinkAt(extText, mousePos);
|
||||
if (kw != null) return kw;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 点击输入处理
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
private void HandleClickInput()
|
||||
{
|
||||
bool leftClick = Mouse.current.leftButton.wasPressedThisFrame;
|
||||
bool rightClick = Mouse.current.rightButton.wasPressedThisFrame;
|
||||
|
||||
if (!leftClick && !rightClick) return;
|
||||
if (_openPanels.Count == 0) return;
|
||||
|
||||
Vector2 mousePos = Mouse.current.position.ReadValue();
|
||||
|
||||
// 检测是否右键点击在关键词链接上 → 固定 Hover 面板
|
||||
if (rightClick)
|
||||
{
|
||||
string clickedKeyword = DetectHoveredKeyword(mousePos);
|
||||
if (!string.IsNullOrEmpty(clickedKeyword) &&
|
||||
_hoverPanel != null &&
|
||||
_hoverPanel.Keyword == clickedKeyword)
|
||||
{
|
||||
PinHoverPanel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 检测是否点击在任意 Tooltip 面板内部 → 如果是则不处理
|
||||
if (IsMouseInsideAnyPanel(mousePos)) return;
|
||||
|
||||
// 点击在所有 Tooltip 外部 → 关闭所有 Tooltip
|
||||
CloseAllTooltips();
|
||||
|
||||
// 左键关闭时,本帧阻断台词推进(下一帧自动解除)
|
||||
if (leftClick)
|
||||
{
|
||||
IsBlockingDialogueInput = true;
|
||||
StartCoroutine(UnblockNextFrame());
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 面板生命周期
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
private TooltipPanel SpawnPanel(KeywordData data, Vector2 screenPos, bool pinned)
|
||||
{
|
||||
if (tooltipPanelPrefab == null || tooltipContainer == null) return null;
|
||||
|
||||
var panelGO = Instantiate(tooltipPanelPrefab, tooltipContainer);
|
||||
var panel = panelGO.GetComponent<TooltipPanel>();
|
||||
|
||||
if (panel == null)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[KeywordTooltipUI] Tooltip Prefab 上缺少 TooltipPanel 组件!" +
|
||||
$"请确保 Prefab '{tooltipPanelPrefab.name}' 挂载了 TooltipPanel 脚本。",
|
||||
tooltipPanelPrefab);
|
||||
Destroy(panelGO);
|
||||
return null;
|
||||
}
|
||||
|
||||
panel.Initialize(data, pinned);
|
||||
panel.PositionAtScreenPoint(screenPos, tooltipOffset);
|
||||
|
||||
_openPanels.Add(panel);
|
||||
IsBlockingDialogueInput = true;
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
private void PinHoverPanel()
|
||||
{
|
||||
if (_hoverPanel == null) return;
|
||||
|
||||
_hoverPanel.Pin();
|
||||
|
||||
_hoverPanel = null;
|
||||
_lastHoveredKeyword = null;
|
||||
}
|
||||
|
||||
private void CloseHoverTooltip()
|
||||
{
|
||||
if (_hoverPanel == null) return;
|
||||
|
||||
_openPanels.Remove(_hoverPanel);
|
||||
if (_hoverPanel.gameObject != null)
|
||||
Destroy(_hoverPanel.gameObject);
|
||||
|
||||
_hoverPanel = null;
|
||||
|
||||
if (_openPanels.Count == 0)
|
||||
IsBlockingDialogueInput = false;
|
||||
}
|
||||
|
||||
private void CloseAllTooltips()
|
||||
{
|
||||
foreach (var panel in _openPanels)
|
||||
{
|
||||
if (panel != null && panel.gameObject != null)
|
||||
Destroy(panel.gameObject);
|
||||
}
|
||||
_openPanels.Clear();
|
||||
_hoverPanel = null;
|
||||
_lastHoveredKeyword = null;
|
||||
IsBlockingDialogueInput = false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 查询方法
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// 检查指定关键词是否已有固定的面板存在。
|
||||
/// 用于避免为同一个关键词生成重复的 Hover 面板。
|
||||
/// </summary>
|
||||
private bool HasPinnedPanelForKeyword(string keyword)
|
||||
{
|
||||
foreach (var panel in _openPanels)
|
||||
{
|
||||
if (panel.IsPinned && panel.Keyword == keyword)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 工具方法
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
private string DetectLinkAt(TMP_Text tmpText, Vector2 screenPos)
|
||||
{
|
||||
if (tmpText == null) return null;
|
||||
|
||||
int linkIndex = TMP_TextUtilities.FindIntersectingLink(tmpText, screenPos, uiCamera);
|
||||
if (linkIndex < 0) return null;
|
||||
|
||||
string linkId = tmpText.textInfo.linkInfo[linkIndex].GetLinkID();
|
||||
return KeywordProcessor.ExtractKeywordFromLinkId(linkId);
|
||||
}
|
||||
|
||||
private bool IsMouseInsideAnyPanel(Vector2 screenPos)
|
||||
{
|
||||
foreach (var panel in _openPanels)
|
||||
{
|
||||
if (panel != null && panel.ContainsScreenPoint(screenPos, uiCamera))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private IEnumerator UnblockNextFrame()
|
||||
{
|
||||
yield return null;
|
||||
IsBlockingDialogueInput = false;
|
||||
}
|
||||
|
||||
public void RegisterExternalText(TMP_Text text)
|
||||
{
|
||||
if (text != null && !_externalTexts.Contains(text))
|
||||
_externalTexts.Add(text);
|
||||
}
|
||||
|
||||
public void UnregisterExternalText(TMP_Text text)
|
||||
{
|
||||
if (text != null)
|
||||
_externalTexts.Remove(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 78e77e9842502d948928cf5cb7c814d7
|
||||
330
Assets/Scripts/SLSUtilities/Narrative/UI/OptionTooltipUI.cs
Normal file
330
Assets/Scripts/SLSUtilities/Narrative/UI/OptionTooltipUI.cs
Normal file
@@ -0,0 +1,330 @@
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
using Sirenix.OdinInspector;
|
||||
|
||||
namespace SLSUtilities.Narrative.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 选项悬停提示面板管理器。
|
||||
/// - 鼠标模式:仅在鼠标悬停于选项"文本区域"(非空白行区域)时显示,跟随鼠标移动。
|
||||
/// - 键盘模式:在选项文本的右上角处固定显示。
|
||||
/// - 支持右键固定和点击外部关闭。
|
||||
/// </summary>
|
||||
public class OptionTooltipUI : MonoBehaviour
|
||||
{
|
||||
public static OptionTooltipUI Instance { get; private set; }
|
||||
|
||||
[TitleGroup("核心引用 (Core References)", Alignment = TitleAlignments.Centered)]
|
||||
|
||||
[BoxGroup("核心引用 (Core References)/UI")]
|
||||
[Required("需要指定 Tooltip 面板的 Prefab(必须挂载 TooltipPanel 组件)")]
|
||||
[SerializeField] private GameObject tooltipPanelPrefab;
|
||||
|
||||
[BoxGroup("核心引用 (Core References)/UI")]
|
||||
[Required("Tooltip 生成的父级容器(RectTransform)")]
|
||||
[SerializeField] private RectTransform tooltipContainer;
|
||||
|
||||
[BoxGroup("核心引用 (Core References)/UI")]
|
||||
[Tooltip("渲染 Canvas 的摄像机。Screen Space Overlay 模式下留空")]
|
||||
[SerializeField] private Camera uiCamera;
|
||||
|
||||
[TitleGroup("行为设置 (Behavior Settings)", Alignment = TitleAlignments.Centered)]
|
||||
[BoxGroup("行为设置 (Behavior Settings)/定位")]
|
||||
[Tooltip("Tooltip 左下角相对于鼠标(或键盘时文本右上角)的像素偏移量")]
|
||||
[SerializeField] private Vector2 tooltipOffset = new Vector2(12f, 12f);
|
||||
|
||||
[BoxGroup("行为设置 (Behavior Settings)/定位")]
|
||||
[Tooltip("鼠标检测的边缘容差像素数。\n较大值可避免中文全角标点符号边缘闪烁,较小值则更精确地限制在文字内。")]
|
||||
[Range(0f, 20f)]
|
||||
[SerializeField] private float textBoundsTolerance = 6f;
|
||||
|
||||
private TooltipPanel _hoverPanel;
|
||||
private AdvancedOptionItem _hoverItem;
|
||||
|
||||
// 当前悬停是否由鼠标触发(false = 键盘触发)
|
||||
private bool _isMouseSelection;
|
||||
|
||||
// 已固定的选项 Tooltip
|
||||
private readonly List<TooltipPanel> _pinnedPanels = new List<TooltipPanel>();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
CloseHoverPanel();
|
||||
CloseAllPinnedPanels();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
HandleHoverPanelVisibility();
|
||||
HandleClickInput();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 公开接口(由 AdvancedOptionItem 调用)
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
public void OnOptionSelected(AdvancedOptionItem item, bool isMouseTriggered)
|
||||
{
|
||||
_hoverItem = item;
|
||||
_isMouseSelection = isMouseTriggered;
|
||||
|
||||
string textToShow = item.Option.IsAvailable ? item.TooltipDesc : item.TooltipFail;
|
||||
if (string.IsNullOrWhiteSpace(textToShow))
|
||||
{
|
||||
CloseHoverPanel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (HasPinnedPanelForOption(textToShow))
|
||||
{
|
||||
CloseHoverPanel();
|
||||
return;
|
||||
}
|
||||
|
||||
CloseHoverPanel();
|
||||
_hoverPanel = SpawnPanel(textToShow);
|
||||
|
||||
if (_isMouseSelection)
|
||||
{
|
||||
// 鼠标模式:初始位置对齐鼠标,后续每帧跟随
|
||||
Vector2 mousePos = Mouse.current.position.ReadValue();
|
||||
_hoverPanel.PositionAtScreenPoint(mousePos, tooltipOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 键盘模式:定位在文本右上角处
|
||||
PositionPanelAtTextTopRight(_hoverPanel, item.GetTextComponent());
|
||||
}
|
||||
}
|
||||
|
||||
public void OnOptionDeselected(AdvancedOptionItem item)
|
||||
{
|
||||
if (_hoverItem == item)
|
||||
CloseHoverPanel();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 每帧更新(悬停面板可见性与定位)
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
private void HandleHoverPanelVisibility()
|
||||
{
|
||||
if (_hoverPanel == null) return;
|
||||
|
||||
// 当选项文本中出现了关键词且玩家正在选中关键词时,隐去未固定的选项 Tooltip
|
||||
bool isHoveringKeyword = KeywordTooltipUI.Instance != null && KeywordTooltipUI.Instance.HasHoverPanel;
|
||||
if (isHoveringKeyword)
|
||||
{
|
||||
_hoverPanel.gameObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isMouseSelection)
|
||||
{
|
||||
Vector2 mousePos = Mouse.current.position.ReadValue();
|
||||
|
||||
// 使用 textBounds 检测文本渲染边界,避免全角标点符号字形间隙造成闪烁
|
||||
var textComp = _hoverItem?.GetTextComponent();
|
||||
bool mouseOverText = IsMouseOverTextArea(textComp, mousePos);
|
||||
|
||||
if (mouseOverText)
|
||||
{
|
||||
_hoverPanel.gameObject.SetActive(true);
|
||||
// 每帧跟随鼠标
|
||||
_hoverPanel.PositionAtScreenPoint(mousePos, tooltipOffset);
|
||||
}
|
||||
else
|
||||
{
|
||||
_hoverPanel.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 键盘模式:始终显示,位置固定在文本右上角(无需每帧更新)
|
||||
_hoverPanel.gameObject.SetActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 点击输入处理
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
private void HandleClickInput()
|
||||
{
|
||||
bool leftClick = Mouse.current.leftButton.wasPressedThisFrame;
|
||||
bool rightClick = Mouse.current.rightButton.wasPressedThisFrame;
|
||||
|
||||
if (!leftClick && !rightClick) return;
|
||||
|
||||
Vector2 mousePos = Mouse.current.position.ReadValue();
|
||||
|
||||
// 右键固定:当 hover panel 可见时,右键单击在选项区域内将其固定
|
||||
if (rightClick && _hoverPanel != null && _hoverPanel.gameObject.activeSelf)
|
||||
{
|
||||
bool isHoveringKeyword = KeywordTooltipUI.Instance != null && KeywordTooltipUI.Instance.HasHoverPanel;
|
||||
if (!isHoveringKeyword)
|
||||
{
|
||||
// 使用与悬停检测相同的 textBounds 方式
|
||||
var textComp = _hoverItem?.GetTextComponent();
|
||||
bool clickOverText = IsMouseOverTextArea(textComp, mousePos);
|
||||
bool clickOverPanel = _hoverPanel.ContainsScreenPoint(mousePos, uiCamera);
|
||||
|
||||
if (clickOverText || clickOverPanel)
|
||||
{
|
||||
PinHoverPanel();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 点击外部关闭所有已固定的选项 Tooltip
|
||||
if (_pinnedPanels.Count > 0)
|
||||
{
|
||||
bool clickedInside = false;
|
||||
|
||||
foreach (var panel in _pinnedPanels)
|
||||
{
|
||||
if (panel.ContainsScreenPoint(mousePos, uiCamera))
|
||||
{
|
||||
clickedInside = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!clickedInside && _hoverPanel != null && _hoverPanel.ContainsScreenPoint(mousePos, uiCamera))
|
||||
clickedInside = true;
|
||||
|
||||
if (!clickedInside)
|
||||
CloseAllPinnedPanels();
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 面板生命周期
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
private void PinHoverPanel()
|
||||
{
|
||||
if (_hoverPanel == null) return;
|
||||
|
||||
_hoverPanel.Pin();
|
||||
_pinnedPanels.Add(_hoverPanel);
|
||||
_hoverPanel = null;
|
||||
}
|
||||
|
||||
private TooltipPanel SpawnPanel(string description)
|
||||
{
|
||||
if (tooltipPanelPrefab == null || tooltipContainer == null) return null;
|
||||
|
||||
var panelGO = Instantiate(tooltipPanelPrefab, tooltipContainer);
|
||||
var panel = panelGO.GetComponent<TooltipPanel>();
|
||||
|
||||
// 创建临时 KeywordData,内容为选项说明(无标题)
|
||||
var data = ScriptableObject.CreateInstance<KeywordData>();
|
||||
data.keyword = string.Empty;
|
||||
// 选项说明中也支持嵌套关键词:由 Initialize 内的 ProcessDescription 自动处理
|
||||
data.description = description;
|
||||
|
||||
panel.Initialize(data, false);
|
||||
|
||||
Destroy(data);
|
||||
return panel;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 定位工具
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// 将面板定位在 TMP 文本组件的右上角处(用于键盘模式)。
|
||||
/// </summary>
|
||||
private void PositionPanelAtTextTopRight(TooltipPanel panel, TMP_Text textComp)
|
||||
{
|
||||
if (panel == null || textComp == null) return;
|
||||
|
||||
Vector3[] corners = new Vector3[4];
|
||||
textComp.rectTransform.GetWorldCorners(corners);
|
||||
// corners 顺序:0=BL, 1=TL, 2=TR, 3=BR(屏幕坐标,Overlay模式)
|
||||
// 对于非 Overlay 模式,使用 WorldToScreenPoint 转换
|
||||
Vector2 screenPos = uiCamera != null
|
||||
? RectTransformUtility.WorldToScreenPoint(uiCamera, corners[2])
|
||||
: new Vector2(corners[2].x, corners[2].y);
|
||||
|
||||
panel.PositionAtScreenPoint(screenPos, tooltipOffset);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 文本区域检测工具
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// 检测鼠标屏幕坐标是否处于 TMP 文本的实际渲染边界矩形内。
|
||||
/// 使用 textBounds(字形渲染包围盒)而非 FindIntersectingCharacter,
|
||||
/// 可避免中文全角标点符号字形内空白区域导致的闪烁问题。
|
||||
/// </summary>
|
||||
private bool IsMouseOverTextArea(TMP_Text textComp, Vector2 screenMousePos)
|
||||
{
|
||||
if (textComp == null) return false;
|
||||
|
||||
// 确保 TMPro 网格在当下完成同步刷新,以获得 100% 准确的渲染包围盒,彻底阻断首帧零包围盒渲染计算闪烁
|
||||
textComp.ForceMeshUpdate();
|
||||
|
||||
// 将屏幕坐标转换为 TMP RectTransform 的局部坐标
|
||||
if (!RectTransformUtility.ScreenPointToLocalPointInRectangle(
|
||||
textComp.rectTransform, screenMousePos, uiCamera, out Vector2 localPoint))
|
||||
return false;
|
||||
|
||||
// textBounds 是 TMP 实际渲染内容的包围盒(局部坐标),
|
||||
// 比 RectTransform 本身更精确,且不受字符个体差异影响
|
||||
Bounds bounds = textComp.textBounds;
|
||||
|
||||
// 加入可配置容差,避免全角标点字形边缘闪烁
|
||||
return localPoint.x >= bounds.min.x - textBoundsTolerance
|
||||
&& localPoint.x <= bounds.max.x + textBoundsTolerance
|
||||
&& localPoint.y >= bounds.min.y - textBoundsTolerance
|
||||
&& localPoint.y <= bounds.max.y + textBoundsTolerance;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 查询工具
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
private bool HasPinnedPanelForOption(string description)
|
||||
{
|
||||
// 简单比较原始描述文本(未处理),避免二次处理比较问题
|
||||
foreach (var panel in _pinnedPanels)
|
||||
{
|
||||
if (panel != null && panel.DescriptionText != null &&
|
||||
panel.DescriptionText.text.Contains(description.Substring(0, Mathf.Min(description.Length, 10))))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void CloseHoverPanel()
|
||||
{
|
||||
if (_hoverPanel != null && _hoverPanel.gameObject != null)
|
||||
Destroy(_hoverPanel.gameObject);
|
||||
_hoverPanel = null;
|
||||
}
|
||||
|
||||
private void CloseAllPinnedPanels()
|
||||
{
|
||||
foreach (var panel in _pinnedPanels)
|
||||
{
|
||||
if (panel != null && panel.gameObject != null)
|
||||
Destroy(panel.gameObject);
|
||||
}
|
||||
_pinnedPanels.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dab16e5156d7d044b83244748ea29ab7
|
||||
BIN
Assets/Scripts/SLSUtilities/Narrative/UI/Text StyleSheet.asset
LFS
Normal file
BIN
Assets/Scripts/SLSUtilities/Narrative/UI/Text StyleSheet.asset
LFS
Normal file
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2aa296e25917c8b468ed9c19f3c90b38
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
269
Assets/Scripts/SLSUtilities/Narrative/UI/TooltipPanel.cs
Normal file
269
Assets/Scripts/SLSUtilities/Narrative/UI/TooltipPanel.cs
Normal file
@@ -0,0 +1,269 @@
|
||||
using Sirenix.OdinInspector;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace SLSUtilities.Narrative.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 关键词浮动面板组件。
|
||||
/// 挂载在 Tooltip Prefab 根节点上,负责管理单个面板的内容显示、
|
||||
/// 固定状态及屏幕定位。
|
||||
/// </summary>
|
||||
public class TooltipPanel : MonoBehaviour
|
||||
{
|
||||
// ---------------------------------------------------------------
|
||||
// Inspector 配置(Prefab 内拖拽赋值)
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
[TitleGroup("面板引用 (Panel References)", Alignment = TitleAlignments.Centered)]
|
||||
|
||||
[BoxGroup("面板引用 (Panel References)/标题行")]
|
||||
[LabelText("标题栏容器")]
|
||||
[SerializeField] private RectTransform titleBarContainer;
|
||||
|
||||
[BoxGroup("面板引用 (Panel References)/标题行")]
|
||||
[LabelText("标题文本")]
|
||||
[SerializeField] private TMP_Text titleText;
|
||||
|
||||
[BoxGroup("面板引用 (Panel References)/标题行")]
|
||||
[LabelText("关键词图标")]
|
||||
[SerializeField] private Image iconImage;
|
||||
|
||||
[FormerlySerializedAs("pinIndicator")]
|
||||
[BoxGroup("面板引用 (Panel References)/标题行")]
|
||||
[LabelText("固定指示器")]
|
||||
[SerializeField] private GameObject titlePin;
|
||||
|
||||
[BoxGroup("面板引用 (Panel References)/描述行")]
|
||||
[LabelText("描述栏容器")]
|
||||
[SerializeField] private RectTransform descriptionContainer;
|
||||
|
||||
[BoxGroup("面板引用 (Panel References)/描述行")]
|
||||
[LabelText("描述文本")]
|
||||
[SerializeField] private TMP_Text descriptionText;
|
||||
|
||||
[BoxGroup("面板引用 (Panel References)/描述行")]
|
||||
[LabelText("固定指示器")]
|
||||
[SerializeField] private GameObject descriptionPin;
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 公开属性
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// 此面板对应的主关键词。
|
||||
/// </summary>
|
||||
public string Keyword { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 该面板是否已被固定(右键固定后不再跟随鼠标,且不会因移开鼠标而关闭)。
|
||||
/// </summary>
|
||||
public bool IsPinned { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 面板的 RectTransform 引用,供外部定位和碰撞检测。
|
||||
/// </summary>
|
||||
public RectTransform Rect { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 面板内的描述文本组件引用,供外部检测嵌套链接。
|
||||
/// </summary>
|
||||
public TMP_Text DescriptionText => descriptionText;
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 生命周期
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
Rect = GetComponent<RectTransform>();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 初始化
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// 初始化面板内容。由 KeywordTooltipUI 在实例化后调用。
|
||||
/// </summary>
|
||||
/// <param name="data">关键词数据</param>
|
||||
/// <param name="pinned">是否初始即为固定状态</param>
|
||||
public void Initialize(KeywordData data, bool pinned)
|
||||
{
|
||||
Keyword = data.keyword;
|
||||
IsPinned = pinned;
|
||||
|
||||
// 1. 标题与标题栏显隐控制
|
||||
bool hasTitle = !string.IsNullOrEmpty(data.keyword);
|
||||
if (titleText != null)
|
||||
titleText.text = data.keyword;
|
||||
|
||||
if (titleBarContainer != null)
|
||||
titleBarContainer.gameObject.SetActive(hasTitle);
|
||||
|
||||
// 2. 描述(经过关键词处理,支持嵌套链接,排除自身防止自引用)
|
||||
if (descriptionText != null)
|
||||
{
|
||||
string processed = KeywordProcessor.ProcessDescription(
|
||||
data.description, data.keyword);
|
||||
|
||||
// 性能与排版双重防御:
|
||||
// 获取或动态添加 LayoutElement 元素。在 Horizontal Layout Group 中,
|
||||
// 如果不使用 LayoutElement.preferredWidth 限制,子节点的 TMP_Text 组件
|
||||
// 会被 Layout 强制拉伸压缩至其 Minimum Width(即单个中文字符宽度,产生“过窄”Bug)。
|
||||
var textLayout = descriptionText.GetComponent<LayoutElement>();
|
||||
if (textLayout == null)
|
||||
textLayout = descriptionText.gameObject.AddComponent<LayoutElement>();
|
||||
|
||||
var containerLayout = descriptionContainer != null ? descriptionContainer.GetComponent<LayoutElement>() : null;
|
||||
if (descriptionContainer != null && containerLayout == null)
|
||||
containerLayout = descriptionContainer.gameObject.AddComponent<LayoutElement>();
|
||||
|
||||
// 暂时关闭自动换行以计算其“自然无换行的 preferredWidth”
|
||||
descriptionText.textWrappingMode = TextWrappingModes.NoWrap;
|
||||
descriptionText.text = processed;
|
||||
|
||||
// 强制更新 TMPro 字形数据以获取精确的 preferredWidth
|
||||
descriptionText.ForceMeshUpdate();
|
||||
float preferredWidth = descriptionText.preferredWidth;
|
||||
|
||||
// 左右各缩减 10 像素,所以 padding 占用共 20 像素
|
||||
float paddingWidth = 20f;
|
||||
|
||||
if (preferredWidth > 980f)
|
||||
{
|
||||
// 超过 980 像素,限制文本 preferredWidth 为 980,并启用自动折行
|
||||
textLayout.preferredWidth = 980f;
|
||||
descriptionText.textWrappingMode = TextWrappingModes.Normal;
|
||||
|
||||
if (containerLayout != null)
|
||||
containerLayout.preferredWidth = 1000f; // 容器宽度 = 980px + 20px padding
|
||||
}
|
||||
else
|
||||
{
|
||||
// 在 980 像素内,紧贴真实内容宽度展示,不进行折行
|
||||
textLayout.preferredWidth = preferredWidth;
|
||||
descriptionText.textWrappingMode = TextWrappingModes.NoWrap;
|
||||
|
||||
if (containerLayout != null)
|
||||
containerLayout.preferredWidth = preferredWidth + paddingWidth;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 固定指示器与固定状态
|
||||
UpdatePinIndicator();
|
||||
|
||||
// 4. 性能优化:只在内容加载、文本大小发生改变时强制刷新一次 UI 布局,防止跟随鼠标时每帧高频刷新
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(Rect);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 固定 / 取消固定
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// 将面板设为固定状态。固定后不再跟随鼠标,且不会因移开鼠标而自动关闭。
|
||||
/// </summary>
|
||||
public void Pin()
|
||||
{
|
||||
IsPinned = true;
|
||||
UpdatePinIndicator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 取消固定状态。
|
||||
/// </summary>
|
||||
public void Unpin()
|
||||
{
|
||||
IsPinned = false;
|
||||
UpdatePinIndicator();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 屏幕定位
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// 将面板定位到指定的屏幕坐标。
|
||||
/// 默认情况下,面板左下角与鼠标对齐;
|
||||
/// 在靠近屏幕边缘时,会自动调整到合适位置。
|
||||
/// </summary>
|
||||
/// <param name="screenPos">鼠标屏幕坐标</param>
|
||||
/// <param name="offset">基础偏移量</param>
|
||||
public void PositionAtScreenPoint(Vector2 screenPos, Vector2 offset)
|
||||
{
|
||||
if (Rect == null) return;
|
||||
|
||||
float panelWidth = Rect.rect.width;
|
||||
float panelHeight = Rect.rect.height;
|
||||
|
||||
// 视觉边缘细节:引入 16 像素的安全屏幕边缘 padding,防止边缘阴影或外发光被物理截边
|
||||
float safeMargin = 16f;
|
||||
|
||||
// 基础定位:面板左下角对齐鼠标位置(鼠标在面板的左下角)
|
||||
// screenPos 即面板的左下角坐标,再加一个小偏移
|
||||
float posX = screenPos.x + offset.x;
|
||||
float posY = screenPos.y + offset.y;
|
||||
|
||||
// 边缘自适应 ─ 右边界
|
||||
if (panelWidth > 0 && posX + panelWidth > Screen.width - safeMargin)
|
||||
{
|
||||
// 面板会超出右侧 → 改为右下角对齐鼠标(面板在鼠标左侧)
|
||||
posX = screenPos.x - panelWidth - Mathf.Abs(offset.x);
|
||||
}
|
||||
|
||||
// 边缘自适应 ─ 左边界
|
||||
if (posX < safeMargin)
|
||||
{
|
||||
posX = safeMargin;
|
||||
}
|
||||
|
||||
// 边缘自适应 ─ 上边界
|
||||
if (posY + panelHeight > Screen.height - safeMargin)
|
||||
{
|
||||
// 面板会超出上方 → 向下调整
|
||||
posY = Screen.height - panelHeight - safeMargin;
|
||||
}
|
||||
|
||||
// 边缘自适应 ─ 下边界
|
||||
if (posY < safeMargin)
|
||||
{
|
||||
posY = safeMargin;
|
||||
}
|
||||
|
||||
// 设置 Pivot 为左下角 (0, 0) 以匹配我们的定位逻辑
|
||||
Rect.pivot = new Vector2(0f, 0f);
|
||||
Rect.position = new Vector2(posX, posY);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 碰撞检测
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// 检测指定的屏幕坐标是否在面板区域内。
|
||||
/// </summary>
|
||||
public bool ContainsScreenPoint(Vector2 screenPos, Camera uiCamera)
|
||||
{
|
||||
return Rect != null &&
|
||||
RectTransformUtility.RectangleContainsScreenPoint(Rect, screenPos, uiCamera);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// 内部方法
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
private void UpdatePinIndicator()
|
||||
{
|
||||
bool hasTitle = !string.IsNullOrEmpty(Keyword);
|
||||
|
||||
if (titlePin != null)
|
||||
titlePin.SetActive(IsPinned && hasTitle);
|
||||
|
||||
if (descriptionPin != null)
|
||||
descriptionPin.SetActive(IsPinned && !hasTitle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b9480658e91a4d548a18e05958b5605f
|
||||
23
Assets/Scripts/SLSUtilities/SLSUtilities.asmdef
Normal file
23
Assets/Scripts/SLSUtilities/SLSUtilities.asmdef
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "SLSUtilities",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:174bc9f391f1b7f4292b3824c5019a21",
|
||||
"GUID:560b04d1a97f54a4e82edc0cbbb69285",
|
||||
"GUID:34aa492b82754644eac2f903cd496268",
|
||||
"GUID:6055be8ebefd69e48b49212b09b47b2f",
|
||||
"GUID:75469ad4d38634e559750d17036d5f7c",
|
||||
"GUID:cfcd2ce455f8d1944942cdd919ecaa60",
|
||||
"GUID:8017400dc3a8d3c4e8a805361276efd0",
|
||||
"GUID:bf41a3c927b459f40a6588443b81113c"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
7
Assets/Scripts/SLSUtilities/SLSUtilities.asmdef.meta
Normal file
7
Assets/Scripts/SLSUtilities/SLSUtilities.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 43bb4e992d9b32b4bbb25402b41e80a0
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using AK.Wwise;
|
||||
using Ichni;
|
||||
using Lean.Pool;
|
||||
using Sirenix.OdinInspector;
|
||||
using SLSUtilities.General;
|
||||
@@ -15,10 +14,7 @@ namespace SLSUtilities.WwiseAssistance
|
||||
public GameObject audioPoint;
|
||||
public List<Bank> soundBanks;
|
||||
|
||||
[Title("Subsystems")]
|
||||
public SongPlayer backgroundMusicManager;
|
||||
|
||||
private static Dictionary<string, uint> TrackedPlayingIDs = new Dictionary<string, uint>();
|
||||
private static Dictionary<string, uint> _trackedPlayingIDs = new Dictionary<string, uint>();
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
@@ -88,7 +84,7 @@ namespace SLSUtilities.WwiseAssistance
|
||||
public void StopAll()
|
||||
{
|
||||
AkUnitySoundEngine.StopAll();
|
||||
TrackedPlayingIDs.Clear();
|
||||
_trackedPlayingIDs.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +94,7 @@ namespace SLSUtilities.WwiseAssistance
|
||||
{
|
||||
if (playingID != AkUnitySoundEngine.AK_INVALID_PLAYING_ID)
|
||||
{
|
||||
TrackedPlayingIDs[trackingKey] = playingID;
|
||||
_trackedPlayingIDs[trackingKey] = playingID;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,7 +125,7 @@ namespace SLSUtilities.WwiseAssistance
|
||||
|
||||
public static void Pause(string trackingKey, int fadeOutMs = 0)
|
||||
{
|
||||
if (TrackedPlayingIDs.TryGetValue(trackingKey, out uint playingID))
|
||||
if (_trackedPlayingIDs.TryGetValue(trackingKey, out uint playingID))
|
||||
{
|
||||
AkUnitySoundEngine.ExecuteActionOnPlayingID(
|
||||
AkActionOnEventType.AkActionOnEventType_Pause,
|
||||
@@ -141,7 +137,7 @@ namespace SLSUtilities.WwiseAssistance
|
||||
|
||||
public static void PauseAllTrackedEvents(int fadeOutMs = 0)
|
||||
{
|
||||
foreach (var playingID in TrackedPlayingIDs.Values)
|
||||
foreach (var playingID in _trackedPlayingIDs.Values)
|
||||
{
|
||||
AkUnitySoundEngine.ExecuteActionOnPlayingID(
|
||||
AkActionOnEventType.AkActionOnEventType_Pause,
|
||||
@@ -154,7 +150,7 @@ namespace SLSUtilities.WwiseAssistance
|
||||
// --- 【新增】核心控制:继续 ---
|
||||
public static void Resume(string trackingKey, int fadeInMs = 0)
|
||||
{
|
||||
if (TrackedPlayingIDs.TryGetValue(trackingKey, out uint playingID))
|
||||
if (_trackedPlayingIDs.TryGetValue(trackingKey, out uint playingID))
|
||||
{
|
||||
AkUnitySoundEngine.ExecuteActionOnPlayingID(
|
||||
AkActionOnEventType.AkActionOnEventType_Resume,
|
||||
@@ -166,7 +162,7 @@ namespace SLSUtilities.WwiseAssistance
|
||||
|
||||
public static void ResumeAllTrackedEvents(int fadeInMs = 0)
|
||||
{
|
||||
foreach (var playingID in TrackedPlayingIDs.Values)
|
||||
foreach (var playingID in _trackedPlayingIDs.Values)
|
||||
{
|
||||
AkUnitySoundEngine.ExecuteActionOnPlayingID(
|
||||
AkActionOnEventType.AkActionOnEventType_Resume,
|
||||
@@ -181,10 +177,10 @@ namespace SLSUtilities.WwiseAssistance
|
||||
/// </summary>
|
||||
public static void Stop(string trackingKey, int fadeOutMs = 0)
|
||||
{
|
||||
if (TrackedPlayingIDs.TryGetValue(trackingKey, out uint playingID))
|
||||
if (_trackedPlayingIDs.TryGetValue(trackingKey, out uint playingID))
|
||||
{
|
||||
Stop(playingID, fadeOutMs);
|
||||
TrackedPlayingIDs.Remove(trackingKey);
|
||||
_trackedPlayingIDs.Remove(trackingKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,11 +189,11 @@ namespace SLSUtilities.WwiseAssistance
|
||||
/// </summary>
|
||||
public static void StopAllTrackedEvents(int fadeOutMs = 0)
|
||||
{
|
||||
foreach (var playingID in TrackedPlayingIDs.Values)
|
||||
foreach (var playingID in _trackedPlayingIDs.Values)
|
||||
{
|
||||
Stop(playingID, fadeOutMs);
|
||||
}
|
||||
TrackedPlayingIDs.Clear();
|
||||
_trackedPlayingIDs.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user