同步
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user