同步
This commit is contained in:
@@ -213,7 +213,7 @@ Material:
|
|||||||
- _Dst: 10
|
- _Dst: 10
|
||||||
- _DstBlend: 0
|
- _DstBlend: 0
|
||||||
- _DstBlendAlpha: 0
|
- _DstBlendAlpha: 0
|
||||||
- _EdgeValue: 0.6926449
|
- _EdgeValue: 0.91334355
|
||||||
- _EnvironmentReflections: 1
|
- _EnvironmentReflections: 1
|
||||||
- _FNLfanxiangkaiguan: 0
|
- _FNLfanxiangkaiguan: 0
|
||||||
- _Face: 1
|
- _Face: 1
|
||||||
@@ -258,7 +258,7 @@ Material:
|
|||||||
- _Mask_scale: 1
|
- _Mask_scale: 1
|
||||||
- _Metallic: 0
|
- _Metallic: 0
|
||||||
- _OcclusionStrength: 1
|
- _OcclusionStrength: 1
|
||||||
- _Opacity: 0.3073551
|
- _Opacity: 0.08665645
|
||||||
- _Parallax: 0.005
|
- _Parallax: 0.005
|
||||||
- _Pass: 0
|
- _Pass: 0
|
||||||
- _QueueOffset: 0
|
- _QueueOffset: 0
|
||||||
|
|||||||
BIN
Assets/FR2_Cache.asset
LFS
BIN
Assets/FR2_Cache.asset
LFS
Binary file not shown.
17
Assets/Plugins/Easy Save 3/EasySave3.asmdef
Normal file
17
Assets/Plugins/Easy Save 3/EasySave3.asmdef
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "EasySave3",
|
||||||
|
"rootNamespace": "",
|
||||||
|
"references": [
|
||||||
|
"GUID:6055be8ebefd69e48b49212b09b47b2f",
|
||||||
|
"GUID:21b0c8d1703a94250bfac916590cea4f"
|
||||||
|
],
|
||||||
|
"includePlatforms": [],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": false,
|
||||||
|
"precompiledReferences": [],
|
||||||
|
"autoReferenced": true,
|
||||||
|
"defineConstraints": [],
|
||||||
|
"versionDefines": [],
|
||||||
|
"noEngineReferences": false
|
||||||
|
}
|
||||||
7
Assets/Plugins/Easy Save 3/EasySave3.asmdef.meta
Normal file
7
Assets/Plugins/Easy Save 3/EasySave3.asmdef.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: cfcd2ce455f8d1944942cdd919ecaa60
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -2,7 +2,7 @@ fileFormatVersion: 2
|
|||||||
guid: 600dbc665993148f7b59ae7356fe862e
|
guid: 600dbc665993148f7b59ae7356fe862e
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
timeCreated: 1474041532
|
timeCreated: 1474041532
|
||||||
licenseType: Store
|
licenseType: Free
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
userData:
|
userData:
|
||||||
assetBundleName:
|
assetBundleName:
|
||||||
|
|||||||
18
Assets/Plugins/Easy Save 3/Editor/EasySave3Editor.asmdef
Normal file
18
Assets/Plugins/Easy Save 3/Editor/EasySave3Editor.asmdef
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "EasySave3Editor",
|
||||||
|
"rootNamespace": "",
|
||||||
|
"references": [
|
||||||
|
"GUID:cfcd2ce455f8d1944942cdd919ecaa60"
|
||||||
|
],
|
||||||
|
"includePlatforms": [
|
||||||
|
"Editor"
|
||||||
|
],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": false,
|
||||||
|
"precompiledReferences": [],
|
||||||
|
"autoReferenced": true,
|
||||||
|
"defineConstraints": [],
|
||||||
|
"versionDefines": [],
|
||||||
|
"noEngineReferences": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: eb59b970a7d779141b50532b8cafd00f
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
Binary file not shown.
@@ -10024,6 +10024,9 @@ MonoBehaviour:
|
|||||||
- Name:
|
- Name:
|
||||||
Entry: 8
|
Entry: 8
|
||||||
Data:
|
Data:
|
||||||
|
- Name: updateScheduler
|
||||||
|
Entry: 6
|
||||||
|
Data:
|
||||||
songPlayer: {fileID: 1150174748}
|
songPlayer: {fileID: 1150174748}
|
||||||
cameraManager: {fileID: 1150174742}
|
cameraManager: {fileID: 1150174742}
|
||||||
noteJudgeManager: {fileID: 1332204027}
|
noteJudgeManager: {fileID: 1332204027}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -29,7 +29,7 @@ namespace Ichni.RhythmGame
|
|||||||
this.splineRenderer.doubleSided = true;
|
this.splineRenderer.doubleSided = true;
|
||||||
this.splineRenderer.clipFrom = 0;
|
this.splineRenderer.clipFrom = 0;
|
||||||
this.splineRenderer.clipTo = 1;
|
this.splineRenderer.clipTo = 1;
|
||||||
this.splineRenderer.updateMethod = SplineUser.UpdateMethod.Update;
|
this.splineRenderer.updateMethod = SplineUser.UpdateMethod.LateUpdate;
|
||||||
this.meshRenderer.material = renderMaterial;
|
this.meshRenderer.material = renderMaterial;
|
||||||
this.splineRenderer.color = Color.white;
|
this.splineRenderer.color = Color.white;
|
||||||
this.uvRotation = 0f;
|
this.uvRotation = 0f;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ namespace Ichni.RhythmGame
|
|||||||
this.pathGenerator.doubleSided = true;
|
this.pathGenerator.doubleSided = true;
|
||||||
this.pathGenerator.clipFrom = 0;
|
this.pathGenerator.clipFrom = 0;
|
||||||
this.pathGenerator.clipTo = 1;
|
this.pathGenerator.clipTo = 1;
|
||||||
this.pathGenerator.updateMethod = SplineUser.UpdateMethod.Update;
|
this.pathGenerator.updateMethod = SplineUser.UpdateMethod.LateUpdate;
|
||||||
this.meshRenderer.material = renderMaterial;
|
this.meshRenderer.material = renderMaterial;
|
||||||
this.pathGenerator.color = Color.white;
|
this.pathGenerator.color = Color.white;
|
||||||
this.uvRotation = 90f;
|
this.uvRotation = 90f;
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ namespace Ichni.RhythmGame
|
|||||||
this.surface.doubleSided = true;
|
this.surface.doubleSided = true;
|
||||||
this.surface.clipFrom = 0;
|
this.surface.clipFrom = 0;
|
||||||
this.surface.clipTo = 1;
|
this.surface.clipTo = 1;
|
||||||
this.surface.updateMethod = SplineUser.UpdateMethod.Update;
|
this.surface.updateMethod = SplineUser.UpdateMethod.LateUpdate;
|
||||||
this.meshRenderer.material = renderMaterial;
|
this.meshRenderer.material = renderMaterial;
|
||||||
this.surface.color = Color.white;
|
this.surface.color = Color.white;
|
||||||
this.surface.uvRotation = 90;
|
this.surface.uvRotation = 90;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ namespace Ichni.RhythmGame
|
|||||||
this.tubeGenerator.spline = track.trackPathSubmodule.path;
|
this.tubeGenerator.spline = track.trackPathSubmodule.path;
|
||||||
this.tubeGenerator.clipFrom = 0;
|
this.tubeGenerator.clipFrom = 0;
|
||||||
this.tubeGenerator.clipTo = 1;
|
this.tubeGenerator.clipTo = 1;
|
||||||
this.tubeGenerator.updateMethod = SplineUser.UpdateMethod.Update;
|
this.tubeGenerator.updateMethod = SplineUser.UpdateMethod.LateUpdate;
|
||||||
this.meshRenderer.material = renderMaterial;
|
this.meshRenderer.material = renderMaterial;
|
||||||
this.tubeGenerator.color = Color.white;
|
this.tubeGenerator.color = Color.white;
|
||||||
this.tubeGenerator.uvRotation = 90;
|
this.tubeGenerator.uvRotation = 90;
|
||||||
|
|||||||
10
Assets/Scripts/Manager/AudioManager.cs
Normal file
10
Assets/Scripts/Manager/AudioManager.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using Sirenix.OdinInspector;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Ichni
|
||||||
|
{
|
||||||
|
public class AudioManager : SLSUtilities.WwiseAssistance.AudioManager
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Scripts/Manager/AudioManager.cs.meta
Normal file
2
Assets/Scripts/Manager/AudioManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0aca77fb4d2655046a9ed2ccd2c14321
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using I2.Loc;
|
//using I2.Loc;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace SLSUtilities.General
|
namespace SLSUtilities.General
|
||||||
@@ -7,10 +7,10 @@ namespace SLSUtilities.General
|
|||||||
{
|
{
|
||||||
public static string Localize(this string original)
|
public static string Localize(this string original)
|
||||||
{
|
{
|
||||||
if (LocalizationManager.TryGetTranslation(original, out string translated))
|
/*if (LocalizationManager.TryGetTranslation(original, out string translated))
|
||||||
{
|
{
|
||||||
return translated;
|
return translated;
|
||||||
}
|
}*/
|
||||||
|
|
||||||
return original;
|
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 System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using AK.Wwise;
|
using AK.Wwise;
|
||||||
using Ichni;
|
|
||||||
using Lean.Pool;
|
using Lean.Pool;
|
||||||
using Sirenix.OdinInspector;
|
using Sirenix.OdinInspector;
|
||||||
using SLSUtilities.General;
|
using SLSUtilities.General;
|
||||||
@@ -15,10 +14,7 @@ namespace SLSUtilities.WwiseAssistance
|
|||||||
public GameObject audioPoint;
|
public GameObject audioPoint;
|
||||||
public List<Bank> soundBanks;
|
public List<Bank> soundBanks;
|
||||||
|
|
||||||
[Title("Subsystems")]
|
private static Dictionary<string, uint> _trackedPlayingIDs = new Dictionary<string, uint>();
|
||||||
public SongPlayer backgroundMusicManager;
|
|
||||||
|
|
||||||
private static Dictionary<string, uint> TrackedPlayingIDs = new Dictionary<string, uint>();
|
|
||||||
|
|
||||||
protected override void Awake()
|
protected override void Awake()
|
||||||
{
|
{
|
||||||
@@ -88,7 +84,7 @@ namespace SLSUtilities.WwiseAssistance
|
|||||||
public void StopAll()
|
public void StopAll()
|
||||||
{
|
{
|
||||||
AkUnitySoundEngine.StopAll();
|
AkUnitySoundEngine.StopAll();
|
||||||
TrackedPlayingIDs.Clear();
|
_trackedPlayingIDs.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +94,7 @@ namespace SLSUtilities.WwiseAssistance
|
|||||||
{
|
{
|
||||||
if (playingID != AkUnitySoundEngine.AK_INVALID_PLAYING_ID)
|
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)
|
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(
|
AkUnitySoundEngine.ExecuteActionOnPlayingID(
|
||||||
AkActionOnEventType.AkActionOnEventType_Pause,
|
AkActionOnEventType.AkActionOnEventType_Pause,
|
||||||
@@ -141,7 +137,7 @@ namespace SLSUtilities.WwiseAssistance
|
|||||||
|
|
||||||
public static void PauseAllTrackedEvents(int fadeOutMs = 0)
|
public static void PauseAllTrackedEvents(int fadeOutMs = 0)
|
||||||
{
|
{
|
||||||
foreach (var playingID in TrackedPlayingIDs.Values)
|
foreach (var playingID in _trackedPlayingIDs.Values)
|
||||||
{
|
{
|
||||||
AkUnitySoundEngine.ExecuteActionOnPlayingID(
|
AkUnitySoundEngine.ExecuteActionOnPlayingID(
|
||||||
AkActionOnEventType.AkActionOnEventType_Pause,
|
AkActionOnEventType.AkActionOnEventType_Pause,
|
||||||
@@ -154,7 +150,7 @@ namespace SLSUtilities.WwiseAssistance
|
|||||||
// --- 【新增】核心控制:继续 ---
|
// --- 【新增】核心控制:继续 ---
|
||||||
public static void Resume(string trackingKey, int fadeInMs = 0)
|
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(
|
AkUnitySoundEngine.ExecuteActionOnPlayingID(
|
||||||
AkActionOnEventType.AkActionOnEventType_Resume,
|
AkActionOnEventType.AkActionOnEventType_Resume,
|
||||||
@@ -166,7 +162,7 @@ namespace SLSUtilities.WwiseAssistance
|
|||||||
|
|
||||||
public static void ResumeAllTrackedEvents(int fadeInMs = 0)
|
public static void ResumeAllTrackedEvents(int fadeInMs = 0)
|
||||||
{
|
{
|
||||||
foreach (var playingID in TrackedPlayingIDs.Values)
|
foreach (var playingID in _trackedPlayingIDs.Values)
|
||||||
{
|
{
|
||||||
AkUnitySoundEngine.ExecuteActionOnPlayingID(
|
AkUnitySoundEngine.ExecuteActionOnPlayingID(
|
||||||
AkActionOnEventType.AkActionOnEventType_Resume,
|
AkActionOnEventType.AkActionOnEventType_Resume,
|
||||||
@@ -181,10 +177,10 @@ namespace SLSUtilities.WwiseAssistance
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static void Stop(string trackingKey, int fadeOutMs = 0)
|
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);
|
Stop(playingID, fadeOutMs);
|
||||||
TrackedPlayingIDs.Remove(trackingKey);
|
_trackedPlayingIDs.Remove(trackingKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,11 +189,11 @@ namespace SLSUtilities.WwiseAssistance
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static void StopAllTrackedEvents(int fadeOutMs = 0)
|
public static void StopAllTrackedEvents(int fadeOutMs = 0)
|
||||||
{
|
{
|
||||||
foreach (var playingID in TrackedPlayingIDs.Values)
|
foreach (var playingID in _trackedPlayingIDs.Values)
|
||||||
{
|
{
|
||||||
Stop(playingID, fadeOutMs);
|
Stop(playingID, fadeOutMs);
|
||||||
}
|
}
|
||||||
TrackedPlayingIDs.Clear();
|
_trackedPlayingIDs.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
206
Packages/dev.yarnspinner.unity/.editorconfig
Normal file
206
Packages/dev.yarnspinner.unity/.editorconfig
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
# Remove the line below if you want to inherit .editorconfig settings from higher directories
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# C# files
|
||||||
|
[*.cs]
|
||||||
|
|
||||||
|
#### Core EditorConfig Options ####
|
||||||
|
|
||||||
|
# Indentation and spacing
|
||||||
|
indent_size = 4
|
||||||
|
indent_style = space
|
||||||
|
tab_width = 4
|
||||||
|
|
||||||
|
# New line preferences
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
#### .NET Coding Conventions ####
|
||||||
|
|
||||||
|
# Organize usings
|
||||||
|
dotnet_separate_import_directive_groups = false
|
||||||
|
dotnet_sort_system_directives_first = false
|
||||||
|
|
||||||
|
# this. and Me. preferences
|
||||||
|
dotnet_style_qualification_for_event = false:silent
|
||||||
|
dotnet_style_qualification_for_field = false:silent
|
||||||
|
dotnet_style_qualification_for_method = false:silent
|
||||||
|
dotnet_style_qualification_for_property = false:silent
|
||||||
|
|
||||||
|
# Language keywords vs BCL types preferences
|
||||||
|
dotnet_style_predefined_type_for_locals_parameters_members = true:silent
|
||||||
|
dotnet_style_predefined_type_for_member_access = true:silent
|
||||||
|
|
||||||
|
# Parentheses preferences
|
||||||
|
dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
|
||||||
|
dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
|
||||||
|
dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
|
||||||
|
dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
|
||||||
|
|
||||||
|
# Modifier preferences
|
||||||
|
dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
|
||||||
|
|
||||||
|
# Expression-level preferences
|
||||||
|
dotnet_style_coalesce_expression = true:suggestion
|
||||||
|
dotnet_style_collection_initializer = true:suggestion
|
||||||
|
dotnet_style_explicit_tuple_names = true:suggestion
|
||||||
|
dotnet_style_null_propagation = true:suggestion
|
||||||
|
dotnet_style_object_initializer = true:suggestion
|
||||||
|
dotnet_style_prefer_auto_properties = true:silent
|
||||||
|
dotnet_style_prefer_compound_assignment = true:suggestion
|
||||||
|
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
|
||||||
|
dotnet_style_prefer_conditional_expression_over_return = true:silent
|
||||||
|
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
|
||||||
|
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
||||||
|
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
||||||
|
dotnet_style_prefer_simplified_interpolation = true:suggestion
|
||||||
|
|
||||||
|
# Field preferences
|
||||||
|
dotnet_style_readonly_field = true:suggestion
|
||||||
|
|
||||||
|
# Parameter preferences
|
||||||
|
dotnet_code_quality_unused_parameters = all:suggestion
|
||||||
|
|
||||||
|
#### C# Coding Conventions ####
|
||||||
|
|
||||||
|
# var preferences
|
||||||
|
csharp_style_var_elsewhere = false:silent
|
||||||
|
csharp_style_var_for_built_in_types = false:silent
|
||||||
|
csharp_style_var_when_type_is_apparent = false:silent
|
||||||
|
|
||||||
|
# Expression-bodied members
|
||||||
|
csharp_style_expression_bodied_accessors = true:silent
|
||||||
|
csharp_style_expression_bodied_constructors = false:silent
|
||||||
|
csharp_style_expression_bodied_indexers = true:silent
|
||||||
|
csharp_style_expression_bodied_lambdas = true:silent
|
||||||
|
csharp_style_expression_bodied_local_functions = false:silent
|
||||||
|
csharp_style_expression_bodied_methods = false:silent
|
||||||
|
csharp_style_expression_bodied_operators = false:silent
|
||||||
|
csharp_style_expression_bodied_properties = true:silent
|
||||||
|
|
||||||
|
# Pattern matching preferences
|
||||||
|
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||||
|
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
||||||
|
csharp_style_prefer_switch_expression = true:suggestion
|
||||||
|
|
||||||
|
# Null-checking preferences
|
||||||
|
csharp_style_conditional_delegate_call = true:suggestion
|
||||||
|
|
||||||
|
# Modifier preferences
|
||||||
|
csharp_prefer_static_local_function = true:suggestion
|
||||||
|
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent
|
||||||
|
|
||||||
|
# Code-block preferences
|
||||||
|
csharp_prefer_braces = true:silent
|
||||||
|
csharp_prefer_simple_using_statement = true:suggestion
|
||||||
|
|
||||||
|
# Expression-level preferences
|
||||||
|
csharp_prefer_simple_default_expression = true:suggestion
|
||||||
|
csharp_style_deconstructed_variable_declaration = true:suggestion
|
||||||
|
csharp_style_inlined_variable_declaration = true:suggestion
|
||||||
|
csharp_style_pattern_local_over_anonymous_function = true:suggestion
|
||||||
|
csharp_style_prefer_index_operator = true:suggestion
|
||||||
|
csharp_style_prefer_range_operator = true:suggestion
|
||||||
|
csharp_style_throw_expression = true:suggestion
|
||||||
|
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
|
||||||
|
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
|
||||||
|
|
||||||
|
# 'using' directive preferences
|
||||||
|
csharp_using_directive_placement = outside_namespace:silent
|
||||||
|
|
||||||
|
#### C# Formatting Rules ####
|
||||||
|
|
||||||
|
# New line preferences
|
||||||
|
csharp_new_line_before_catch = true
|
||||||
|
csharp_new_line_before_else = true
|
||||||
|
csharp_new_line_before_finally = true
|
||||||
|
csharp_new_line_before_members_in_anonymous_types = true
|
||||||
|
csharp_new_line_before_members_in_object_initializers = true
|
||||||
|
csharp_new_line_before_open_brace = all
|
||||||
|
csharp_new_line_between_query_expression_clauses = true
|
||||||
|
|
||||||
|
# Indentation preferences
|
||||||
|
csharp_indent_block_contents = true
|
||||||
|
csharp_indent_braces = false
|
||||||
|
csharp_indent_case_contents = true
|
||||||
|
csharp_indent_case_contents_when_block = true
|
||||||
|
csharp_indent_labels = one_less_than_current
|
||||||
|
csharp_indent_switch_labels = true
|
||||||
|
|
||||||
|
# Space preferences
|
||||||
|
csharp_space_after_cast = false
|
||||||
|
csharp_space_after_colon_in_inheritance_clause = true
|
||||||
|
csharp_space_after_comma = true
|
||||||
|
csharp_space_after_dot = false
|
||||||
|
csharp_space_after_keywords_in_control_flow_statements = true
|
||||||
|
csharp_space_after_semicolon_in_for_statement = true
|
||||||
|
csharp_space_around_binary_operators = before_and_after
|
||||||
|
csharp_space_around_declaration_statements = false
|
||||||
|
csharp_space_before_colon_in_inheritance_clause = true
|
||||||
|
csharp_space_before_comma = false
|
||||||
|
csharp_space_before_dot = false
|
||||||
|
csharp_space_before_open_square_brackets = false
|
||||||
|
csharp_space_before_semicolon_in_for_statement = false
|
||||||
|
csharp_space_between_empty_square_brackets = false
|
||||||
|
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||||
|
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
||||||
|
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||||
|
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||||
|
csharp_space_between_method_declaration_name_and_open_parenthesis = false
|
||||||
|
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||||
|
csharp_space_between_parentheses = false
|
||||||
|
csharp_space_between_square_brackets = false
|
||||||
|
|
||||||
|
# Wrapping preferences
|
||||||
|
csharp_preserve_single_line_blocks = true
|
||||||
|
csharp_preserve_single_line_statements = true
|
||||||
|
|
||||||
|
#### Naming styles ####
|
||||||
|
|
||||||
|
# Naming rules
|
||||||
|
|
||||||
|
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
|
||||||
|
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||||
|
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||||
|
|
||||||
|
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
|
||||||
|
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
||||||
|
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
|
||||||
|
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
||||||
|
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||||
|
|
||||||
|
# Symbol specifications
|
||||||
|
|
||||||
|
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||||
|
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.interface.required_modifiers =
|
||||||
|
|
||||||
|
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
||||||
|
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.types.required_modifiers =
|
||||||
|
|
||||||
|
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||||
|
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||||
|
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||||
|
|
||||||
|
# Naming styles
|
||||||
|
|
||||||
|
dotnet_naming_style.pascal_case.required_prefix =
|
||||||
|
dotnet_naming_style.pascal_case.required_suffix =
|
||||||
|
dotnet_naming_style.pascal_case.word_separator =
|
||||||
|
dotnet_naming_style.pascal_case.capitalization = pascal_case
|
||||||
|
|
||||||
|
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||||
|
dotnet_naming_style.begins_with_i.required_suffix =
|
||||||
|
dotnet_naming_style.begins_with_i.word_separator =
|
||||||
|
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||||
|
|
||||||
|
csharp_style_implicit_object_creation_when_type_is_apparent = false
|
||||||
|
csharp_style_prefer_not_pattern = false
|
||||||
|
dotnet_style_readonly_field = false
|
||||||
|
dotnet_style_object_initializer = false
|
||||||
|
|
||||||
|
# RS2008: Enable analyzer release tracking
|
||||||
|
dotnet_diagnostic.RS2008.severity = none
|
||||||
5
Packages/dev.yarnspinner.unity/.github/FUNDING.yml
vendored
Normal file
5
Packages/dev.yarnspinner.unity/.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
patreon: secretlab
|
||||||
|
github: YarnSpinnerTool
|
||||||
|
custom: ['https://yarnspinner.itch.io', 'https://assetstore.unity.com/packages/tools/behavior-ai/yarn-spinner-for-unity-the-friendly-dialogue-and-narrative-tool-267061']
|
||||||
25
Packages/dev.yarnspinner.unity/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
25
Packages/dev.yarnspinner.unity/.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve!
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees:
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**What is the current behavior?**
|
||||||
|
<!-- Please describe what you're seeing. -->
|
||||||
|
|
||||||
|
**Please provide the steps to reproduce, and if possible a minimal demo of the problem**:
|
||||||
|
<!-- Please give us as much detail as you can, so that we can reproduce the issue. If possible, please consider uploading a demo project that demonstrates the problem. -->
|
||||||
|
|
||||||
|
**What is the expected behavior?**
|
||||||
|
<!-- What do you expect to see instead of what's happening now? -->
|
||||||
|
|
||||||
|
**Please tell us about your environment:**
|
||||||
|
|
||||||
|
- Yarn Spinner Version:
|
||||||
|
- Unity Version:
|
||||||
|
|
||||||
|
**Other information**
|
||||||
|
<!-- For example, a detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context... -->
|
||||||
20
Packages/dev.yarnspinner.unity/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
Packages/dev.yarnspinner.unity/.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for Yarn Spinner!
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
<!-- A clear and concise description of what you want to happen. -->
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
<!-- Add any other context or screenshots about the feature request here. -->
|
||||||
58
Packages/dev.yarnspinner.unity/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
58
Packages/dev.yarnspinner.unity/.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
* **Please check if the pull request fulfills these requirements**
|
||||||
|
- [ ] Tests for the changes have been added (for bug fixes / features)
|
||||||
|
- [ ] Does it pass all existing unit tests without modification?
|
||||||
|
- If not, what did you change?
|
||||||
|
- If you altered it significantly, what coverage issue did you fix?
|
||||||
|
- [ ] Docs have been added / updated (for bug fixes / features)
|
||||||
|
- [ ] CHANGELOG.md has been updated to describe this change
|
||||||
|
|
||||||
|
<!-- Please also consider adding yourself to CONTRIBUTORS.md as part of your pull request. We'd like to recognise you for your efforts! -->
|
||||||
|
|
||||||
|
<!-- To update the documentation on yarnspinner.dev, please submit a pull request to the documentation repository at https://github.com/YarnSpinnerTool/Docs. -->
|
||||||
|
|
||||||
|
* **What kind of change does this pull request introduce?**
|
||||||
|
|
||||||
|
- [ ] Bug Fix
|
||||||
|
- [ ] Feature
|
||||||
|
- [ ] Something else
|
||||||
|
|
||||||
|
* **What is the current behavior?**
|
||||||
|
|
||||||
|
<!-- If you are fixing a known bug, you can also link to an open issue here. -->
|
||||||
|
|
||||||
|
* **What is the new behavior (if this is a feature change)?**
|
||||||
|
|
||||||
|
<!-- Please describe, in as much detail as you can, what your pull request changes in Yarn Spinner. -->
|
||||||
|
|
||||||
|
* **Does this pull request introduce a breaking change?**
|
||||||
|
|
||||||
|
<!-- What changes might users need to make in their application due to this PR? -->
|
||||||
|
|
||||||
|
* **Other information**:
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
Ideas:
|
||||||
|
|
||||||
|
- Performance?
|
||||||
|
- Does this drastically change performance characteristics, or simply allow for optimizations?
|
||||||
|
- Does this performance involve:
|
||||||
|
- Disk access
|
||||||
|
- CPU time
|
||||||
|
- Memory layout optimization (Lx caching etc)
|
||||||
|
- Might there be a use case where you are encouraged to be unperformant by default?
|
||||||
|
- What optimizations did you consider but left out due to time and/or complexity?
|
||||||
|
- Usability?
|
||||||
|
- If your change is to a sample, is it accessible? We don't follow standards like WCAG but any easy wins should be taken.
|
||||||
|
- Does it make it easier to use our API? If not, what annoyance mitigations have you taken/considered?
|
||||||
|
- Who will be affected?
|
||||||
|
- Is this an internal change, or something meant to be consumed by the end user?
|
||||||
|
- If you add API changes, is it easily upgradable from the previous version?
|
||||||
|
- What indirect consequence might be annoying to the end user?
|
||||||
|
- For what reasons would you say this is justified? E.g. super annoying to use in the first place, tightening up undefined behavior etc.
|
||||||
|
- What do you think will be controversial, if any?
|
||||||
|
- How would you describe the cause of the problem and changes to non-technical users if at all possible?
|
||||||
|
|
||||||
|
Feel free to take any all or none of these points as relevant, though we recommend you read through each point. They're just to get you started!
|
||||||
|
|
||||||
|
-->
|
||||||
38
Packages/dev.yarnspinner.unity/.github/RELEASE_TEMPLATE.md
vendored
Normal file
38
Packages/dev.yarnspinner.unity/.github/RELEASE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<!-- RELEASE_TEMPLATE.md is not a formally supported file used by GitHub. This file is used by .github/workflows/release.yml to add a release notes preface. -->
|
||||||
|
|
||||||
|
Yarn Spinner is made possible by your generous patronage. Please consider supporting Yarn Spinner's development by [becoming a patron](https://patreon.com/secretlab), or by buying a copy of Yarn Spinner on [itch.io](https://yarnspinner.itch.io/yarn-spinner) or the [Unity Asset Store](https://assetstore.unity.com/packages/tools/behavior-ai/yarn-spinner-for-unity-267061)!
|
||||||
|
|
||||||
|
<a href="https://patreon.com/secretlab"><img width="200" src="https://user-images.githubusercontent.com/901768/71883373-6f40ff80-318a-11ea-9d3a-01f1f58cb39e.png"></a>
|
||||||
|
|
||||||
|
## 👩🚒 Getting Help
|
||||||
|
|
||||||
|
There are several places you can go to get help with Yarn Spinner.
|
||||||
|
|
||||||
|
* Join the [Yarn Spinner Discord](https://discord.gg/yarnspinner).
|
||||||
|
* Talk to us via [BlueSky](http://bsky.app/profile/yarnspinner.dev) or [Mastodon](https://team.yarnspinner.dev/@yarnspinner)
|
||||||
|
* To report a bug, [file an issue on GitHub](https://github.com/YarnSpinnerTool/YarnSpinner-Unity/issues/new?labels=bug+beta&template=bug_report.md&title=).
|
||||||
|
|
||||||
|
## 📦 How To Install Yarn Spinner
|
||||||
|
|
||||||
|
To install the most recent release of Yarn Spinner for Unity, please see the [Installation Instructions](https://docs.yarnspinner.dev/using-yarnspinner-with-unity/installation-and-setup) in the Yarn Spinner documentation.
|
||||||
|
|
||||||
|
If you want to install _this_ particular version of Yarn Spinner for Unity, follow these steps:
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Installing Yarn Spinner for Unity {RELEASE_TAG} from Git</summary>
|
||||||
|
<p>
|
||||||
|
|
||||||
|
* Open the Window menu, and choose Package Manager.
|
||||||
|
* If you already have any previous version of the Yarn Spinner package installed, remove it.
|
||||||
|
* Click the `+` button, and click *Add package from git URL...*
|
||||||
|
* Enter the following URL:
|
||||||
|
* `https://github.com/YarnSpinnerTool/YarnSpinner-Unity.git#{RELEASE_TAG}`
|
||||||
|
|
||||||
|
Each release will have a different URL. To upgrade to future versions of Yarn Spinner, you will need to uninstall the package, and reinstall using the new URL.
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
|
||||||
|
## 📜 Changes
|
||||||
|
|
||||||
|
|
||||||
51
Packages/dev.yarnspinner.unity/.github/get-version.sh
vendored
Normal file
51
Packages/dev.yarnspinner.unity/.github/get-version.sh
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Get info about the current commit
|
||||||
|
most_recent_tag=$(git describe --tags --match="v*" --abbrev=0)
|
||||||
|
commits_since_tag=$(git rev-list $most_recent_tag..HEAD | wc -l | awk '{$1=$1};1')
|
||||||
|
sha=$(git log -1 --format=%H)
|
||||||
|
short_sha=$(git log -1 --format=%h)
|
||||||
|
branch=$(git rev-parse --abbrev-ref HEAD)
|
||||||
|
|
||||||
|
# A regex for extracting data from a version number: major, minor, patch,
|
||||||
|
# [prerelease]
|
||||||
|
REGEX='v(\d+)\.(\d+)\.(\d+)(-.*)?'
|
||||||
|
|
||||||
|
raw_version=${1:-"$most_recent_tag"}
|
||||||
|
|
||||||
|
# Extract the data from the version number
|
||||||
|
major=$(echo $raw_version | perl -pe "s|$REGEX|\1|" )
|
||||||
|
minor=$(echo $raw_version | perl -pe "s|$REGEX|\2|" )
|
||||||
|
patch=$(echo $raw_version | perl -pe "s|$REGEX|\3|" )
|
||||||
|
prerelease=$(echo $raw_version | perl -pe "s|$REGEX|\4|" )
|
||||||
|
|
||||||
|
# Calculate the semver from the version (should be the same as the version, but
|
||||||
|
# just in case)
|
||||||
|
SemVer="$major.$minor.$patch$prerelease"
|
||||||
|
|
||||||
|
# If there are any commits since the current tag and we aren't overriding our
|
||||||
|
# version, add that note
|
||||||
|
if [ "$commits_since_tag" -gt 0 -a -z "$1" ]; then
|
||||||
|
SemVer="$SemVer+$commits_since_tag"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create the version strings we'll write into the AssemblyInfo files
|
||||||
|
OutputAssemblyVersion=$(echo "$major.$minor.$patch.$commits_since_tag" | perl -pe "s|\/|\\\/|" )
|
||||||
|
OutputAssemblyInformationalVersion=$(echo "$SemVer.Branch.$branch.Sha.$sha" | perl -pe "s|\/|\\\/|" )
|
||||||
|
OutputAssemblyFileVersion=$(echo "$major.$minor.$patch.$commits_since_tag" | perl -pe "s|\/|\\\/|" )
|
||||||
|
|
||||||
|
# Update the AssemblyInfo.cs files
|
||||||
|
for infoFile in $(find . -name "AssemblyInfo.cs"); do
|
||||||
|
perl -pi -e "s/AssemblyVersion\(\".*\"\)/AssemblyVersion(\"$OutputAssemblyVersion\")/" $infoFile
|
||||||
|
perl -pi -e "s/AssemblyInformationalVersion\(\".*\"\)/AssemblyInformationalVersion(\"$OutputAssemblyInformationalVersion\")/" $infoFile
|
||||||
|
perl -pi -e "s/AssemblyFileVersion\(\".*\"\)/AssemblyFileVersion(\"$OutputAssemblyFileVersion\")/" $infoFile
|
||||||
|
done
|
||||||
|
|
||||||
|
# If we're running in GitHub Workflows, output our calculated SemVer
|
||||||
|
if [[ -n $GITHUB_OUTPUT ]]; then
|
||||||
|
echo "SemVer=$SemVer" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "ShortSha=$short_sha" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Log our SemVer
|
||||||
|
echo $SemVer
|
||||||
13
Packages/dev.yarnspinner.unity/.github/update-package-version.sh
vendored
Normal file
13
Packages/dev.yarnspinner.unity/.github/update-package-version.sh
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
#/bin/bash
|
||||||
|
|
||||||
|
if [ ! -d ".git" ]; then
|
||||||
|
echo "This script must be run in the root of the repository."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION=$(.github/get-version.sh $@)
|
||||||
|
|
||||||
|
jq ".version=\"$VERSION\"" package.json > package.json.tmp
|
||||||
|
mv package.json.tmp package.json
|
||||||
|
|
||||||
|
echo "Updated package version to $VERSION"
|
||||||
102
Packages/dev.yarnspinner.unity/.github/workflows/broad_version_test.yml
vendored
Normal file
102
Packages/dev.yarnspinner.unity/.github/workflows/broad_version_test.yml
vendored
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
name: Run Tests on Broad Version Range 🌶
|
||||||
|
|
||||||
|
env:
|
||||||
|
ACTIONS_RUNNER_DEBUG: true
|
||||||
|
ACTIONS_STEP_DEBUG: true
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
buildAndTestForSomePlatforms:
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}
|
||||||
|
cancel-in-progress: true # Cancel other jobs if another one arrives
|
||||||
|
name: Test on ${{ matrix.unityVersion }} for ${{ matrix.targetPlatform }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
max-parallel: 1 # Only run one at a time, to prevent license contention
|
||||||
|
matrix:
|
||||||
|
projectPath:
|
||||||
|
- YarnSpinner
|
||||||
|
unityVersion:
|
||||||
|
- 2021.3.0f1
|
||||||
|
- 2021.3.32f1
|
||||||
|
- 2022.1.0f1
|
||||||
|
- 2022.1.24f1
|
||||||
|
- 2022.2.0f1
|
||||||
|
- 2022.2.21f1
|
||||||
|
- 2022.3.0f1
|
||||||
|
- 2022.3.13f1
|
||||||
|
- 2023.1.0f1
|
||||||
|
- 2023.1.20f1
|
||||||
|
targetPlatform:
|
||||||
|
# - StandaloneOSX # Build a macOS standalone (Intel 64-bit).
|
||||||
|
- StandaloneWindows64 # Build a Windows 64-bit standalone.
|
||||||
|
# - StandaloneLinux64 # Build a Linux 64-bit standalone.
|
||||||
|
# - iOS # Build an iOS player.
|
||||||
|
# - Android # Build an Android player.
|
||||||
|
# - WebGL # WebGL.
|
||||||
|
steps:
|
||||||
|
- name: Create empty Unity project
|
||||||
|
run: |
|
||||||
|
mkdir -p ${{ matrix.projectPath }}/Assets
|
||||||
|
mkdir -p ${{ matrix.projectPath }}/ProjectSettings
|
||||||
|
mkdir -p ${{ matrix.projectPath }}/Packages
|
||||||
|
|
||||||
|
# Add the Unity Input System package, and configure the new project to use
|
||||||
|
# both the Input System and the legacy Input Manager.
|
||||||
|
- name: Add Input System package
|
||||||
|
run: |
|
||||||
|
cat <<EOF > ${{ matrix.projectPath }}/ProjectSettings/ProjectSettings.asset
|
||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!129 &1
|
||||||
|
PlayerSettings:
|
||||||
|
activeInputHandler: 2
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat <<EOF > ${{ matrix.projectPath }}/Packages/manifest.json
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"com.unity.inputsystem": "1.0.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Check out to Packages/YarnSpinner
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
path: ${{ matrix.projectPath }}/Packages/YarnSpinner
|
||||||
|
|
||||||
|
- name: Fetch from Cache
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ${{ matrix.projectPath }}/Library
|
||||||
|
key: Library-${{ matrix.projectPath }}-${{ matrix.targetPlatform }}-${{ matrix.unityVersion }}-${{ hashFiles(matrix.projectPath) }}
|
||||||
|
restore-keys: |
|
||||||
|
Library-${{ matrix.projectPath }}-${{ matrix.targetPlatform }}-${{ matrix.unityVersion }}-
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
uses: game-ci/unity-test-runner@v4
|
||||||
|
id: testRunner
|
||||||
|
env:
|
||||||
|
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
|
||||||
|
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
|
||||||
|
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
|
||||||
|
with:
|
||||||
|
projectPath: ${{ matrix.projectPath }}
|
||||||
|
unityVersion: ${{ matrix.unityVersion }}
|
||||||
|
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
checkName: 'Test Results ${{ matrix.unityVersion }}-${{ matrix.targetPlatform }}'
|
||||||
|
# customParameters: -quit
|
||||||
|
|
||||||
|
- name: Upload test results
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: Test results (edit + play, ${{ matrix.unityVersion }}-${{ matrix.targetPlatform }}
|
||||||
|
# path: ${{ steps.testRunner.outputs.artifactsPath }}
|
||||||
|
path: artifacts
|
||||||
37
Packages/dev.yarnspinner.unity/.github/workflows/release.yml
vendored
Normal file
37
Packages/dev.yarnspinner.unity/.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: Create Release 📦
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*.*.*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Create Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Extract release notes
|
||||||
|
id: extract-release-notes
|
||||||
|
uses: ffurrer2/extract-release-notes@v1
|
||||||
|
- name: Read release notes preface
|
||||||
|
id: release_preface
|
||||||
|
uses: bluwy/substitute-string-action@v1
|
||||||
|
with:
|
||||||
|
_input-file: .github/RELEASE_TEMPLATE.md
|
||||||
|
_format-key: '{key}'
|
||||||
|
RELEASE_TAG: ${{ github.ref_name }}
|
||||||
|
- name: Create release
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref }}
|
||||||
|
release_name: ${{ github.ref }}
|
||||||
|
draft: true
|
||||||
|
prerelease: true
|
||||||
|
body: |
|
||||||
|
${{ steps.release_preface.outputs.result }}
|
||||||
|
${{ steps.extract-release-notes.outputs.release_notes }}
|
||||||
|
|
||||||
157
Packages/dev.yarnspinner.unity/.github/workflows/test.yml
vendored
Normal file
157
Packages/dev.yarnspinner.unity/.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
name: Run Tests 🧪
|
||||||
|
|
||||||
|
env:
|
||||||
|
ACTIONS_RUNNER_DEBUG: true
|
||||||
|
ACTIONS_STEP_DEBUG: true
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- "feature/**"
|
||||||
|
- "release/**"
|
||||||
|
paths:
|
||||||
|
- "Editor/**"
|
||||||
|
- "Runtime/**"
|
||||||
|
- "Samples~/**"
|
||||||
|
- "Tests/**"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
buildAndTestForSomePlatforms:
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ matrix.unityVersion }}-${{ matrix.unityLocalisation }}-${{ matrix.uniTask }}
|
||||||
|
cancel-in-progress: true # Cancel other jobs if another one arrives
|
||||||
|
name: ${{ matrix.unityVersion }} (${{ matrix.targetPlatform }}, ${{ matrix.unityLocalisation && 'with unity loc' || 'no unity loc' }}, ${{ matrix.uniTask && 'with unitask' || 'no unitask' }})
|
||||||
|
runs-on: [self-hosted, linux]
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
# max-parallel: 1 # Only run one at a time, to prevent license contention
|
||||||
|
matrix:
|
||||||
|
projectPath:
|
||||||
|
- YarnSpinner
|
||||||
|
unityVersion:
|
||||||
|
- 2022.3.45f1
|
||||||
|
- 2023.2.12f1
|
||||||
|
- 6000.0.54f1
|
||||||
|
- 6000.1.10f1
|
||||||
|
unityLocalisation:
|
||||||
|
- true
|
||||||
|
- false
|
||||||
|
uniTask:
|
||||||
|
- true
|
||||||
|
- false
|
||||||
|
targetPlatform:
|
||||||
|
# - StandaloneOSX # Build a macOS standalone (Intel 64-bit).
|
||||||
|
# - StandaloneWindows64 # Build a Windows 64-bit standalone.
|
||||||
|
- StandaloneLinux64 # Build a Linux 64-bit standalone.
|
||||||
|
# - iOS # Build an iOS player.
|
||||||
|
# - Android # Build an Android player.
|
||||||
|
# - WebGL # WebGL.
|
||||||
|
steps:
|
||||||
|
- name: Create empty Unity project
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir -p ${{ matrix.projectPath }}/Assets
|
||||||
|
mkdir -p ${{ matrix.projectPath }}/ProjectSettings
|
||||||
|
mkdir -p ${{ matrix.projectPath }}/Packages
|
||||||
|
mkdir -p output
|
||||||
|
|
||||||
|
# Add the Unity Input System package, and configure the new project to use
|
||||||
|
# both the Input System and the legacy Input Manager.
|
||||||
|
- name: Add Input System package
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
cat <<EOF > ${{ matrix.projectPath }}/ProjectSettings/ProjectSettings.asset
|
||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!129 &1
|
||||||
|
PlayerSettings:
|
||||||
|
activeInputHandler: 2
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat <<EOF > ${{ matrix.projectPath }}/Packages/manifest.json
|
||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"com.unity.inputsystem": "1.11.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Select correct TMP Essentials package
|
||||||
|
if [[ ${{matrix.unityVersion}} == "2022"* ]]; then
|
||||||
|
TMP_VERSION="ugui-1.0.0"
|
||||||
|
else
|
||||||
|
TMP_VERSION="ugui-2.0.0"
|
||||||
|
fi
|
||||||
|
echo "Installing TMP Essentials for $TMP_VERSION"
|
||||||
|
|
||||||
|
# Add the correct version of the TMP Essentials package to package manifest
|
||||||
|
MANIFEST_PATH=${{ matrix.projectPath }}/Packages/manifest.json
|
||||||
|
jq ".dependencies += {\"dev.yarnspinner.tmp-essentials\": \"https://github.com/desplesda/dev.yarnspinner.tmp-essentials.git#$TMP_VERSION\"}" "$MANIFEST_PATH" > manifest.json
|
||||||
|
mv manifest.json "$MANIFEST_PATH"
|
||||||
|
|
||||||
|
- name: Add Unity Localisation
|
||||||
|
if: ${{ matrix.unityLocalisation }}
|
||||||
|
run: |
|
||||||
|
# Add Unity Localisation package to package manifest
|
||||||
|
MANIFEST_PATH=${{ matrix.projectPath }}/Packages/manifest.json
|
||||||
|
jq '.dependencies += {"com.unity.localization": "1.3.2"}' "$MANIFEST_PATH" > manifest.json
|
||||||
|
mv manifest.json "$MANIFEST_PATH"
|
||||||
|
|
||||||
|
- name: Add UniTask Package
|
||||||
|
if: ${{ matrix.uniTask }}
|
||||||
|
run: |
|
||||||
|
# Add UniTask package to package manifest
|
||||||
|
MANIFEST_PATH=${{ matrix.projectPath }}/Packages/manifest.json
|
||||||
|
jq '.dependencies += {"com.cysharp.unitask": "https://github.com/Cysharp/UniTask.git?path=src/UniTask/Assets/Plugins/UniTask"}' "$MANIFEST_PATH" > manifest.json
|
||||||
|
mv manifest.json "$MANIFEST_PATH"
|
||||||
|
|
||||||
|
- name: Check out to Packages/YarnSpinner
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
path: ${{ matrix.projectPath }}/Packages/dev.yarnspinner.unity
|
||||||
|
|
||||||
|
- name: Run edit mode tests
|
||||||
|
run: |
|
||||||
|
docker run \
|
||||||
|
--rm \
|
||||||
|
-v ./${{ matrix.projectPath }}:/project \
|
||||||
|
-v ./output:/output \
|
||||||
|
-e TEST_MODE=EditMode \
|
||||||
|
--hostname YS-Linux-Build \
|
||||||
|
yarnspinner/unity-${{ matrix.unityVersion }}
|
||||||
|
|
||||||
|
- name: Run play mode tests
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
docker run \
|
||||||
|
--rm \
|
||||||
|
-v ./${{ matrix.projectPath }}:/project \
|
||||||
|
-v ./output:/output \
|
||||||
|
-e TEST_MODE=PlayMode \
|
||||||
|
--hostname YS-Linux-Build \
|
||||||
|
yarnspinner/unity-${{ matrix.unityVersion }}
|
||||||
|
|
||||||
|
- name: Generate HTML test report (Play Mode)
|
||||||
|
uses: rjtngit/nunit-html-action@v1
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
inputXmlPath: output/TestResults-PlayMode.xml
|
||||||
|
outputHtmlPath: output/TestResults-PlayMode.html
|
||||||
|
|
||||||
|
- name: Generate HTML test report (Edit Mode)
|
||||||
|
uses: rjtngit/nunit-html-action@v1
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
inputXmlPath: output/TestResults-EditMode.xml
|
||||||
|
outputHtmlPath: output/TestResults-EditMode.html
|
||||||
|
|
||||||
|
- name: Upload test results
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
if: always()
|
||||||
|
with:
|
||||||
|
name: Test results (edit + play, ${{ matrix.unityVersion }} ${{ matrix.targetPlatform }} ${{ matrix.unityLocalisation && 'with-unity-loc' || 'no-unity-loc' }} ${{ matrix.uniTask && 'with-unitask' || 'no-unitask' }})
|
||||||
|
# path: ${{ steps.testRunner.outputs.artifactsPath }}
|
||||||
|
path: ./output
|
||||||
98
Packages/dev.yarnspinner.unity/.github/workflows/update_dlls.yml
vendored
Normal file
98
Packages/dev.yarnspinner.unity/.github/workflows/update_dlls.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
name: Update DLLs 📚
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update_dlls:
|
||||||
|
name: Update Yarn Spinner DLLs
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
# We need to be able to:
|
||||||
|
# 1. create a branch in a repo ('contents'), and
|
||||||
|
# 2. create a pull request using that branch ('pull-requests')
|
||||||
|
pull-requests: write
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Yarn Spinner for Unity
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
path: YarnSpinner-Unity
|
||||||
|
- name: Checkout Yarn Spinner
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
repository: YarnSpinnerTool/YarnSpinner
|
||||||
|
path: YarnSpinner
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v1
|
||||||
|
with:
|
||||||
|
dotnet-version: |
|
||||||
|
6.0.x
|
||||||
|
9.0.x
|
||||||
|
|
||||||
|
- name: Fetch all commits
|
||||||
|
run: git fetch --unshallow
|
||||||
|
working-directory: ./YarnSpinner
|
||||||
|
|
||||||
|
- name: Install dotnet-assembly-alias
|
||||||
|
run: dotnet tool install -g Alias
|
||||||
|
|
||||||
|
# Update the assembly info for this build of YS, so that the About window is
|
||||||
|
# appropriate
|
||||||
|
- name: Execute GitVersion
|
||||||
|
id: version # step id used as reference for output values
|
||||||
|
run: ./get-version.sh
|
||||||
|
working-directory: ./YarnSpinner
|
||||||
|
|
||||||
|
- name: Restore dependencies
|
||||||
|
run: dotnet restore
|
||||||
|
working-directory: ./YarnSpinner
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: dotnet build --no-restore --configuration Release
|
||||||
|
working-directory: ./YarnSpinner
|
||||||
|
|
||||||
|
# Don't proceed unless we're including a build of Yarn Spinner that passes
|
||||||
|
# its tests.
|
||||||
|
- name: Test
|
||||||
|
run: dotnet test --no-build --configuration Release --verbosity normal
|
||||||
|
working-directory: ./YarnSpinner
|
||||||
|
|
||||||
|
# We need to copy dependency DLLs into the project, but if a Unity project
|
||||||
|
# contains multiple DLLs with the same name (even from a package), that's an
|
||||||
|
# error. This causes problems for users who want to use, for example,
|
||||||
|
# Google.Protobuf (especially if they want to use a different version).
|
||||||
|
#
|
||||||
|
# Our solution partly involves renaming the dependency DLLs to have the
|
||||||
|
# prefix 'Yarn.', and updating all references to these renamed DLLs, using
|
||||||
|
# dotnet-assembly-alias
|
||||||
|
# (https://github.com/getsentry/dotnet-assembly-alias/). For more
|
||||||
|
# information on this fix, see
|
||||||
|
# https://github.com/YarnSpinnerTool/YarnSpinner-Unity/issues/15#issuecomment-1036162152.
|
||||||
|
- name: Rename vendored DLLs
|
||||||
|
run: |
|
||||||
|
assemblyalias --target-directory "YarnSpinner/YarnSpinner.Compiler/bin/Release/netstandard2.0/" --prefix "Yarn." --assemblies-to-alias "Antlr*;Csv*;Google*;"
|
||||||
|
assemblyalias --target-directory "YarnSpinner/YarnSpinner.Compiler/bin/Release/netstandard2.0/" --internalize --prefix "Yarn." --assemblies-to-alias "System*;Microsoft.Bcl*;Microsoft.Extensions*"
|
||||||
|
|
||||||
|
# Copy all of the dependency DLLs into the YarnSpinner-Unity repo, except
|
||||||
|
# for Microsoft.CSharp.dll (which is provided by Unity, so including it
|
||||||
|
# would cause an error.)
|
||||||
|
- name: Copy DLLs
|
||||||
|
run: |
|
||||||
|
cp -v YarnSpinner/YarnSpinner.Compiler/bin/Release/netstandard2.0/*.dll YarnSpinner-Unity/Runtime/DLLs
|
||||||
|
cp -v YarnSpinner/YarnSpinner.Compiler/bin/Release/netstandard2.0/*.pdb YarnSpinner-Unity/Runtime/DLLs
|
||||||
|
cp -v YarnSpinner/YarnSpinner.Compiler/bin/Release/netstandard2.0/*.xml YarnSpinner-Unity/Runtime/DLLs
|
||||||
|
rm -fv YarnSpinner-Unity/Runtime/DLLs/Microsoft.CSharp.dll
|
||||||
|
|
||||||
|
# Make the PR against YarnSpinner-Unity that merges this change
|
||||||
|
- name: Create pull request
|
||||||
|
uses: peter-evans/create-pull-request@v3
|
||||||
|
with:
|
||||||
|
path: ./YarnSpinner-Unity
|
||||||
|
commit-message: Update Yarn Spinner DLLs to YarnSpinnerTool/YarnSpinner@${{ steps.version.outputs.ShortSha }}
|
||||||
|
branch: update-dlls-${{ steps.version.outputs.ShortSha }}
|
||||||
|
title: Update Yarn Spinner DLLs to latest (${{ steps.version.outputs.ShortSha }})
|
||||||
|
body: |
|
||||||
|
This is an automated PR made by @${{ github.actor }} that updates the precompiled Yarn Spinner DLLs (and their dependencies) to YarnSpinnerTool/YarnSpinner@${{ steps.version.outputs.ShortSha }} (v${{ steps.version.outputs.SemVer }}).
|
||||||
21
Packages/dev.yarnspinner.unity/.pkgignore
Normal file
21
Packages/dev.yarnspinner.unity/.pkgignore
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Ignore the Samples symlink, which links to Samples~. This symlink exists
|
||||||
|
# so that the distributed package ships its samples in a folder called
|
||||||
|
# Samples~ (which Unity will ignore, and users can import into their projects
|
||||||
|
# when they want to), while the sameples are visible in the package when
|
||||||
|
# developing.
|
||||||
|
Samples
|
||||||
|
Samples.meta
|
||||||
|
|
||||||
|
# Ignore markdown files when packaging for asset stores - they'll be converted
|
||||||
|
# into PDFs or other more readable formats.
|
||||||
|
*.md
|
||||||
|
*.md.meta
|
||||||
|
|
||||||
|
# Ignore test files, which don't need to be shipped (they're not needed by
|
||||||
|
# end-users)
|
||||||
|
Tests
|
||||||
|
Tests.meta
|
||||||
|
|
||||||
|
# Ignore the file that defines which way we install the samples - the
|
||||||
|
# appropriate setting will be manually copied in.
|
||||||
|
YarnPackageImporter.SamplesInstallApproach.cs
|
||||||
1258
Packages/dev.yarnspinner.unity/CHANGELOG.md
Normal file
1258
Packages/dev.yarnspinner.unity/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
7
Packages/dev.yarnspinner.unity/CHANGELOG.md.meta
Normal file
7
Packages/dev.yarnspinner.unity/CHANGELOG.md.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: db3ac383f37b340b593e3cd5d16b4cf5
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
76
Packages/dev.yarnspinner.unity/CODE_OF_CONDUCT.md
Normal file
76
Packages/dev.yarnspinner.unity/CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as
|
||||||
|
contributors and maintainers pledge to making participation in our project and
|
||||||
|
our community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||||
|
level of experience, education, socio-economic status, nationality, personal
|
||||||
|
appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
|
advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic
|
||||||
|
address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable
|
||||||
|
behavior and are expected to take appropriate and fair corrective action in
|
||||||
|
response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||||
|
permanently any contributor for other behaviors that they deem inappropriate,
|
||||||
|
threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces
|
||||||
|
when an individual is representing the project or its community. Examples of
|
||||||
|
representing a project or community include using an official project e-mail
|
||||||
|
address, posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event. Representation of a project may be
|
||||||
|
further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported by contacting the project team at yarnspinner@secretlab.com.au. All
|
||||||
|
complaints will be reviewed and investigated and will result in a response that
|
||||||
|
is deemed necessary and appropriate to the circumstances. The project team is
|
||||||
|
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
|
Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||||
|
faith may face temporary or permanent repercussions as determined by other
|
||||||
|
members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||||
|
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see
|
||||||
|
https://www.contributor-covenant.org/faq
|
||||||
7
Packages/dev.yarnspinner.unity/CODE_OF_CONDUCT.md.meta
Normal file
7
Packages/dev.yarnspinner.unity/CODE_OF_CONDUCT.md.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5cfcecef19b8f44c8a2e435d277c5a04
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
43
Packages/dev.yarnspinner.unity/CONTRIBUTING.md
Normal file
43
Packages/dev.yarnspinner.unity/CONTRIBUTING.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Contributing to Yarn Spinner
|
||||||
|
|
||||||
|
Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great.
|
||||||
|
|
||||||
|
## How to send in your contributions
|
||||||
|
|
||||||
|
There are many ways you can send your contributions to Yarn Spinner. You can either **report a bug**, or you can make the changes yourself and **submit a pull request**!
|
||||||
|
|
||||||
|
### Reporting bugs and opening issues
|
||||||
|
|
||||||
|
Please [report bugs](https://github.com/YarnSpinnerTool/YarnSpinner-Unity/issues) and open issues generously. Don't be afraid that your idea is silly, or you're reporting a duplicate. We're happy to hear from you. Seriously.
|
||||||
|
|
||||||
|
> ***Please Note:*** Yarn Spinner is written by volunteers. If you encounter a problem while using it, we'll do our best to help you, but neither the authors, or Secret Lab Pty. Ltd. can offer any support.
|
||||||
|
|
||||||
|
### Submitting a pull request
|
||||||
|
|
||||||
|
* [Fork](https://github.com/YarnSpinnerTool/YarnSpinner-Unity/fork) and clone the repository
|
||||||
|
* Create a new branch: git checkout -b my-branch-name
|
||||||
|
* Make your changes
|
||||||
|
* Push to your fork and [submit a pull request](https://github.com/YarnSpinnerTool/YarnSpinner-Unity/compare)
|
||||||
|
* Pat your self on the back and wait for your pull request to be reviewed.
|
||||||
|
|
||||||
|
If you're unfamiliar with how pull requests work, [GitHub's documentation on them](https://help.github.com/articles/using-pull-requests/) is very good.
|
||||||
|
|
||||||
|
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
|
||||||
|
|
||||||
|
* Update the documentation as necessary, as well as making code changes.
|
||||||
|
* Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
|
||||||
|
* [Write a good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html).
|
||||||
|
|
||||||
|
### Unity & LTS support
|
||||||
|
|
||||||
|
As we are a group of volunteers, we can only officially support the versions of Unity that are actively supported by Unity themselves. Please check the official [LTS FAQ](https://support.unity.com/hc/en-us/articles/4403332003348-What-is-a-Unity-LTS-Long-Term-Support-version-and-what-can-I-expect-from-it-) for more up-to-date information. Currently, the minimum supported version is **2019.4**.
|
||||||
|
|
||||||
|
However, if you would like to submit contributions to fix support for unsupported versions, please go ahead and do so, but do note that we cannot guarantee that it continues to work for that version.
|
||||||
|
|
||||||
|
### Branches
|
||||||
|
|
||||||
|
All of Yarn Spinner's in-progress work happens on the `main` branch. When we make releases, we create a new tag from `main`. Larger features are developed on their own branch, and then merged to `main` when ready.
|
||||||
|
|
||||||
|
### Code and other contributions
|
||||||
|
|
||||||
|
Contributions to Yarn Spinner (via pull request or otherwise) must be licensed under the MIT license.
|
||||||
7
Packages/dev.yarnspinner.unity/CONTRIBUTING.md.meta
Normal file
7
Packages/dev.yarnspinner.unity/CONTRIBUTING.md.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9e079b710417049b2915eacde0b91e1b
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
21
Packages/dev.yarnspinner.unity/CONTRIBUTORS.md
Normal file
21
Packages/dev.yarnspinner.unity/CONTRIBUTORS.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Contributors
|
||||||
|
|
||||||
|
The following people have contributed to the development of Yarn Spinner. If you submit a pull request, please add your name to the list below.
|
||||||
|
|
||||||
|
* 2015-ongoing: Secret Lab Team - Dr Jon Manning and Dr Paris Buttfield-Addison <lab@secretlab.com.au>
|
||||||
|
* 2017: Rev Peter Lawler <relwalretep@gmail.com>
|
||||||
|
* 2017: Dr Tim 'McJones' Nugent <tim@lonely.coffee>
|
||||||
|
* 2018: Damon 'demanrisu' Reece <de@coy.ninja>
|
||||||
|
* 2019: Tamme Schichler <tamme@schichler.dev>
|
||||||
|
* 2020: @Schroedingers-Cat, Robert Yang (https://debacle.us)
|
||||||
|
* 2021: Jonathan MacAlpine <apocriva@gmail.com>
|
||||||
|
* 2021: Shane Marks (https://necrosoftgames.com)
|
||||||
|
* 2021: @andiCR, Andrés Cartín (andres@treeinteractivecr.com)
|
||||||
|
* 2021: Shane Duan <github@xsduan.com>
|
||||||
|
* 2022: Bernardo Vecchia Stein <jkhulw@sidhion.com>
|
||||||
|
* 2023: ChocolaMint (https://chocola-mint.github.io/)
|
||||||
|
* 2023: Mitch Zais <https://github.com/Invertex>
|
||||||
|
* 2023: Thomas Ingram (https://vertx.xyz)
|
||||||
|
* 2023: Isaac Berman (https://github.com/bermanisaac)
|
||||||
|
* 20??: Mars Buttfield-Addison (https://github.com/TheMartianLife)
|
||||||
|
* 2025: Jay Xavier Peet (https://github.com/JayPeet)
|
||||||
7
Packages/dev.yarnspinner.unity/CONTRIBUTORS.md.meta
Normal file
7
Packages/dev.yarnspinner.unity/CONTRIBUTORS.md.meta
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9db75e8c06dd1484bb6f0c05ecc00b7f
|
||||||
|
TextScriptImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/dev.yarnspinner.unity/Editor.meta
Normal file
8
Packages/dev.yarnspinner.unity/Editor.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c3b025f9df1834d5e91c6873ece463b1
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
8
Packages/dev.yarnspinner.unity/Editor/Analysis.meta
Normal file
8
Packages/dev.yarnspinner.unity/Editor/Analysis.meta
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 991bb6a4238c84092a59ac23d54fda95
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
1025
Packages/dev.yarnspinner.unity/Editor/Analysis/Action.cs
Normal file
1025
Packages/dev.yarnspinner.unity/Editor/Analysis/Action.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ad1e0218baeab43be922ae40dbfb3ade
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/*
|
||||||
|
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
using Yarn.Unity;
|
||||||
|
|
||||||
|
namespace Yarn.Unity.Editor
|
||||||
|
{
|
||||||
|
public static class ActionSourceCodeGenerator
|
||||||
|
{
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a path in the current project that can be used for storing
|
||||||
|
/// manually-generated Yarn Action registration code.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This property checks to see if a file exists in the Assets folder
|
||||||
|
/// that is both named "YarnActionRegistration.cs", and contains a
|
||||||
|
/// marker indicating that it was generated by Yarn Spinner's code
|
||||||
|
/// generation systems. If this is found, the path to the file is
|
||||||
|
/// returned. Otherwise, the path
|
||||||
|
/// <c>Assets/YarnActionRegistration.cs</c> is returned.
|
||||||
|
/// </remarks>
|
||||||
|
public static string GeneratedSourcePath
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
const string YarnRegistrationFileName = "YarnActionRegistration.cs";
|
||||||
|
const string DefaultOutputFilePath = "Assets/" + YarnRegistrationFileName;
|
||||||
|
|
||||||
|
// Note the lack of a closing parenthesis in this string - we
|
||||||
|
// only want to check to see if it was generated by
|
||||||
|
// "YarnActionAnalyzer", not any specific version of that
|
||||||
|
// analyzer
|
||||||
|
const string YarnGeneratedCodeSignature = "GeneratedCode(\"YarnActionAnalyzer\"";
|
||||||
|
|
||||||
|
var existingFile = System.IO.Directory.EnumerateFiles(System.Environment.CurrentDirectory, YarnRegistrationFileName, System.IO.SearchOption.AllDirectories).FirstOrDefault();
|
||||||
|
|
||||||
|
if (existingFile == null)
|
||||||
|
{
|
||||||
|
return DefaultOutputFilePath;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var text = System.IO.File.ReadAllText(existingFile);
|
||||||
|
return text.Contains(YarnGeneratedCodeSignature)
|
||||||
|
? existingFile
|
||||||
|
: DefaultOutputFilePath;
|
||||||
|
}
|
||||||
|
catch (System.Exception e)
|
||||||
|
{
|
||||||
|
// Something happened while checking the file. Return
|
||||||
|
// our default, and log that we encountered a problem.
|
||||||
|
Debug.LogWarning($"Can't check to see if {existingFile} is a valid action registration script, using {DefaultOutputFilePath} instead: {e}");
|
||||||
|
return DefaultOutputFilePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates and imports a C# source code file in the project
|
||||||
|
/// containing Yarn Action registration code at the path indicated by
|
||||||
|
/// <see cref="GeneratedSourcePath"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method should not be called in projects where Unity has support
|
||||||
|
/// for source generators (i.e. Unity 2021.2 and later).
|
||||||
|
/// </remarks>
|
||||||
|
public static void GenerateYarnActionSourceCode()
|
||||||
|
{
|
||||||
|
var analysis = new Yarn.Unity.ActionAnalyser.Analyser("Assets");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var actions = analysis.GetActions();
|
||||||
|
var source = Yarn.Unity.ActionAnalyser.Analyser.GenerateRegistrationFileSource(actions);
|
||||||
|
|
||||||
|
var path = GeneratedSourcePath;
|
||||||
|
|
||||||
|
System.IO.File.WriteAllText(path, source);
|
||||||
|
UnityEditor.AssetDatabase.ImportAsset(path);
|
||||||
|
|
||||||
|
Debug.Log($"Generated Yarn command and function registration code at {path}");
|
||||||
|
}
|
||||||
|
catch (Yarn.Unity.ActionAnalyser.AnalyserException e)
|
||||||
|
{
|
||||||
|
Debug.LogError($"Error generating source code: " + e.InnerException.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: dabf0558de47a486199baa9e568783ce
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,625 @@
|
|||||||
|
/*
|
||||||
|
Yarn Spinner is licensed to you under the terms found in the file LICENSE.md.
|
||||||
|
*/
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.CodeAnalysis;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp;
|
||||||
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||||
|
using Microsoft.CodeAnalysis.Text;
|
||||||
|
using Yarn.Unity.ActionAnalyser;
|
||||||
|
using YarnAction = Yarn.Unity.ActionAnalyser.Action;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
[Generator]
|
||||||
|
public class ActionRegistrationSourceGenerator : ISourceGenerator
|
||||||
|
{
|
||||||
|
const string YarnSpinnerUnityAssemblyName = "YarnSpinner.Unity";
|
||||||
|
const string DebugLoggingPreprocessorSymbol = "YARN_SOURCE_GENERATION_DEBUG_LOGGING";
|
||||||
|
const string IncludeTestCommands = "YARN_INCLUDE_TEST_COMMANDS";
|
||||||
|
const string MinimumUnityVersionPreprocessorSymbol = "UNITY_2021_2_OR_NEWER";
|
||||||
|
|
||||||
|
public static string? GetProjectRoot(GeneratorExecutionContext context)
|
||||||
|
{
|
||||||
|
// We need to know if the settings are configured to not perform codegen
|
||||||
|
// to link attributed methods. This is kinda annoying because the path
|
||||||
|
// root of the project settings and the root path of this process are
|
||||||
|
// *very* different. So, what we do is we use the included Compilation
|
||||||
|
// Assembly additional file that Unity gives us. This file, if opened,
|
||||||
|
// has the path of the Unity project, which we can then use to get the
|
||||||
|
// settings. If any stage of this fails, then we bail out and assume
|
||||||
|
// that codegen is desired.
|
||||||
|
|
||||||
|
// Try and find any additional files passed to the context
|
||||||
|
if (!context.AdditionalFiles.Any())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One of those files is (AssemblyName).[Unity]AdditionalFile.txt, and it
|
||||||
|
// contains the path to the project
|
||||||
|
var relevantFiles = context.AdditionalFiles.Where(
|
||||||
|
i => i.Path.Contains($"{context.Compilation.AssemblyName}.AdditionalFile.txt")
|
||||||
|
|| i.Path.Contains($"{context.Compilation.AssemblyName}.UnityAdditionalFile.txt")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!relevantFiles.Any())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var assemblyRelevantFile = relevantFiles.First();
|
||||||
|
|
||||||
|
// The file needs to exist on disk
|
||||||
|
if (!File.Exists(assemblyRelevantFile.Path))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Attempt to read it - it should contain the path to the project directory
|
||||||
|
var projectPath = File.ReadAllText(assemblyRelevantFile.Path);
|
||||||
|
if (Directory.Exists(projectPath))
|
||||||
|
{
|
||||||
|
// If this directory exists, we're done
|
||||||
|
return projectPath;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
// We encountered a problem while testing
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(GeneratorExecutionContext context)
|
||||||
|
{
|
||||||
|
using var output = GetOutput(context);
|
||||||
|
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
|
||||||
|
output.WriteLine(DateTime.Now);
|
||||||
|
|
||||||
|
Yarn.Unity.Editor.YarnSpinnerProjectSettings? settings = null;
|
||||||
|
var projectPath = GetProjectRoot(context);
|
||||||
|
|
||||||
|
if (projectPath != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fullPath = Path.Combine(projectPath, Yarn.Unity.Editor.YarnSpinnerProjectSettings.YarnSpinnerProjectSettingsPath);
|
||||||
|
output.WriteLine($"Attempting to read settings file at {fullPath}");
|
||||||
|
|
||||||
|
settings = Yarn.Unity.Editor.YarnSpinnerProjectSettings.GetOrCreateSettings(projectPath, output);
|
||||||
|
if (!settings.automaticallyLinkAttributedYarnCommandsAndFunctions)
|
||||||
|
{
|
||||||
|
output.WriteLine("Skipping codegen due to settings.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
output.WriteLine($"Unable to determine Yarn settings, settings values will be ignored and codegen will occur: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.WriteLine($"Unable to determine project location on disk. Settings values will be ignored and codegen will occur");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasCriticalActionErrors = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
output.WriteLine("Source code generation for assembly " + context.Compilation.AssemblyName);
|
||||||
|
|
||||||
|
if (context.AdditionalFiles.Any())
|
||||||
|
{
|
||||||
|
output.WriteLine($"Additional files:");
|
||||||
|
foreach (var item in context.AdditionalFiles)
|
||||||
|
{
|
||||||
|
output.WriteLine(" " + item.Path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output.WriteLine("Referenced assemblies for this compilation:");
|
||||||
|
foreach (var referencedAssembly in context.Compilation.ReferencedAssemblyNames)
|
||||||
|
{
|
||||||
|
output.WriteLine(" - " + referencedAssembly.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool compilationReferencesYarnSpinner = context.Compilation.ReferencedAssemblyNames
|
||||||
|
.Any(name => name.Name == YarnSpinnerUnityAssemblyName);
|
||||||
|
|
||||||
|
if (compilationReferencesYarnSpinner == false)
|
||||||
|
{
|
||||||
|
// This compilation doesn't reference YarnSpinner.Unity. Any
|
||||||
|
// code that we generate that references symbols in that
|
||||||
|
// assembly won't work.
|
||||||
|
output.WriteLine($"Assembly {context.Compilation.AssemblyName} doesn't reference {YarnSpinnerUnityAssemblyName}. Not generating any code for it.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.WriteLine("Preprocessor Symbols: ");
|
||||||
|
foreach (var symbol in context.ParseOptions.PreprocessorSymbolNames)
|
||||||
|
{
|
||||||
|
output.WriteLine("- " + symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't generate source code if we're not targeting at least Unity
|
||||||
|
// 2021.2. (Unity will not invoke this DLL as a source code
|
||||||
|
// generator until at least this version, but other tools like
|
||||||
|
// OmniSharp might.)
|
||||||
|
if (!context.ParseOptions.PreprocessorSymbolNames.Contains(MinimumUnityVersionPreprocessorSymbol))
|
||||||
|
{
|
||||||
|
output.WriteLine($"Not generating code for assembly {context.Compilation.AssemblyName} because this assembly is not being built for Unity 2021.2 or newer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't generate source code for certain Yarn Spinner provided
|
||||||
|
// assemblies - these always manually register any actions in them.
|
||||||
|
var prefixesToIgnore = new List<string>()
|
||||||
|
{
|
||||||
|
"YarnSpinner.Unity",
|
||||||
|
"YarnSpinner.Editor",
|
||||||
|
};
|
||||||
|
|
||||||
|
// But DO generate source code for the Samples assembly and the Test assembly
|
||||||
|
var prefixesToKeep = new List<string>()
|
||||||
|
{
|
||||||
|
"YarnSpinner.Unity.Samples",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Additionally, if we're building for unit tests, include the Yarn
|
||||||
|
// Spinner unit tests assembly.
|
||||||
|
if (context.ParseOptions.PreprocessorSymbolNames.Contains(IncludeTestCommands))
|
||||||
|
{
|
||||||
|
prefixesToKeep.Add("YarnSpinner.Unity.Tests");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.Compilation.AssemblyName == null)
|
||||||
|
{
|
||||||
|
output.WriteLine("Not generating registration code, because the provided AssemblyName is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefixesToIgnore.Any(prefix => context.Compilation.AssemblyName.StartsWith(prefix)) && !prefixesToKeep.Any(prefix => context.Compilation.AssemblyName.StartsWith(prefix)))
|
||||||
|
{
|
||||||
|
output.WriteLine($"Not generating registration code for {context.Compilation.AssemblyName}: we've been told to exclude it, because its name begins with one of these prefixes: {string.Join(", ", prefixesToIgnore)}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(context.Compilation is CSharpCompilation compilation))
|
||||||
|
{
|
||||||
|
// This is not a C# compilation, so we can't do analysis.
|
||||||
|
output.WriteLine($"Stopping code generation because compilation is not a {nameof(CSharpCompilation)}.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var actions = new List<YarnAction>();
|
||||||
|
foreach (var tree in compilation.SyntaxTrees)
|
||||||
|
{
|
||||||
|
actions.AddRange(Analyser.GetActions(compilation, tree, output));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actions.Count() == 0)
|
||||||
|
{
|
||||||
|
output.WriteLine($"Didn't find any Yarn Actions in {context.Compilation.AssemblyName}. Not generating any source code for it.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// validating and logging all the actions
|
||||||
|
foreach (var action in actions)
|
||||||
|
{
|
||||||
|
if (action == null)
|
||||||
|
{
|
||||||
|
output.WriteLine($"Action is null??");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var diagnostics = action.Validate(compilation, output);
|
||||||
|
foreach (var diagnostic in diagnostics)
|
||||||
|
{
|
||||||
|
context.ReportDiagnostic(diagnostic);
|
||||||
|
if (diagnostic.Severity == DiagnosticSeverity.Warning || diagnostic.Severity == DiagnosticSeverity.Error)
|
||||||
|
{
|
||||||
|
output.WriteLine($"Flagging '{action.Name}' ({action.MethodName}): {diagnostic}");
|
||||||
|
action.ContainsErrors = true;
|
||||||
|
|
||||||
|
if (diagnostic.Severity == DiagnosticSeverity.Error)
|
||||||
|
{
|
||||||
|
hasCriticalActionErrors = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Commands are parsed as whitespace, so spaces in the command name
|
||||||
|
// would render the command un-callable.
|
||||||
|
if (action.Name.Any(x => Char.IsWhiteSpace(x)))
|
||||||
|
{
|
||||||
|
var descriptor = new DiagnosticDescriptor(
|
||||||
|
"YS1002",
|
||||||
|
$"Yarn {action.Type} methods must have a valid name",
|
||||||
|
"YarnCommand and YarnFunction methods follow existing ID rules for Yarn. \"{0}\" is invalid.",
|
||||||
|
"Yarn Spinner",
|
||||||
|
DiagnosticSeverity.Warning,
|
||||||
|
true,
|
||||||
|
"[YarnCommand] and [YarnFunction] attributed methods must follow Yarn ID rules so that Yarn scripts can reference them.",
|
||||||
|
"https://docs.yarnspinner.dev/using-yarnspinner-with-unity/creating-commands-functions");
|
||||||
|
context.ReportDiagnostic(Microsoft.CodeAnalysis.Diagnostic.Create(
|
||||||
|
descriptor,
|
||||||
|
action.Declaration?.GetLocation(),
|
||||||
|
action.Name
|
||||||
|
));
|
||||||
|
action.ContainsErrors = true;
|
||||||
|
output.WriteLine($"Action {action.MethodIdentifierName} will be flagged due to it's name {action.Name}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.WriteLine($"Action {action.Name}: {action.SourceFileName}:{action.Declaration?.GetLocation()?.GetLineSpan().StartLinePosition.Line} ({action.Type})");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasCriticalActionErrors)
|
||||||
|
{
|
||||||
|
stopwatch.Stop();
|
||||||
|
output.WriteLine($"Critical issues were encountered in the actions, aborting code generation, stopping analysis after {stopwatch.Elapsed.TotalMilliseconds}ms");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
output.Write($"Generating source code...");
|
||||||
|
|
||||||
|
var source = Analyser.GenerateRegistrationFileSource(actions);
|
||||||
|
|
||||||
|
output.WriteLine($"Done.");
|
||||||
|
|
||||||
|
SourceText sourceText = SourceText.From(source, Encoding.UTF8);
|
||||||
|
|
||||||
|
output.Write($"Writing generated source...");
|
||||||
|
|
||||||
|
DumpGeneratedFile(context, source);
|
||||||
|
|
||||||
|
output.WriteLine($"Done.");
|
||||||
|
|
||||||
|
context.AddSource($"YarnActionRegistration-{compilation.AssemblyName}.Generated.cs", sourceText);
|
||||||
|
|
||||||
|
if (settings != null)
|
||||||
|
{
|
||||||
|
if (settings.generateYSLSFile)
|
||||||
|
{
|
||||||
|
output.Write($"Generating ysls...");
|
||||||
|
// generating the ysls
|
||||||
|
|
||||||
|
IEnumerable<string> commandJSON = actions.Where(a => a.Type == ActionType.Command).Select(a => a.ToJSON());
|
||||||
|
IEnumerable<string> functionJSON = actions.Where(a => a.Type == ActionType.Function).Select(a => a.ToJSON());
|
||||||
|
|
||||||
|
var ysls = "{" +
|
||||||
|
@"""version"":2," +
|
||||||
|
$@"""commands"":[{string.Join(",", commandJSON)}]," +
|
||||||
|
$@"""functions"":[{string.Join(",", functionJSON)}]" +
|
||||||
|
"}";
|
||||||
|
|
||||||
|
output.WriteLine($"Done.");
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(projectPath))
|
||||||
|
{
|
||||||
|
output.Write($"Writing generated ysls...");
|
||||||
|
|
||||||
|
var fullPath = Path.Combine(projectPath, Yarn.Unity.Editor.YarnSpinnerProjectSettings.YarnSpinnerAssemblyGeneratedYSLSPath(compilation.AssemblyName));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
System.IO.File.WriteAllText(fullPath, ysls);
|
||||||
|
output.WriteLine($"Done.");
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
output.WriteLine($"Unable to write ysls to disk: {e.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.WriteLine("unable to identify project path, ysls will not be written to disk");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.WriteLine($"skipping ysls generation due to settings");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
output.WriteLine($"skipping ysls generation due to settings not being found");
|
||||||
|
}
|
||||||
|
|
||||||
|
stopwatch.Stop();
|
||||||
|
output.WriteLine($"Source code generation completed in {stopwatch.Elapsed.TotalMilliseconds}ms");
|
||||||
|
return;
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
output.WriteLine($"{e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MethodDeclarationSyntax GenerateLoggingMethod(string methodName, string sourceExpression, string prefix)
|
||||||
|
{
|
||||||
|
return SyntaxFactory.MethodDeclaration(
|
||||||
|
SyntaxFactory.PredefinedType(
|
||||||
|
SyntaxFactory.Token(SyntaxKind.VoidKeyword)),
|
||||||
|
SyntaxFactory.Identifier(methodName))
|
||||||
|
.WithModifiers(
|
||||||
|
SyntaxFactory.TokenList(
|
||||||
|
new[]{
|
||||||
|
SyntaxFactory.Token(SyntaxKind.PublicKeyword),
|
||||||
|
SyntaxFactory.Token(SyntaxKind.StaticKeyword)}))
|
||||||
|
.WithBody(
|
||||||
|
SyntaxFactory.Block(
|
||||||
|
SyntaxFactory.LocalDeclarationStatement(
|
||||||
|
SyntaxFactory.VariableDeclaration(
|
||||||
|
SyntaxFactory.GenericName(
|
||||||
|
SyntaxFactory.Identifier("IEnumerable"))
|
||||||
|
.WithTypeArgumentList(
|
||||||
|
SyntaxFactory.TypeArgumentList(
|
||||||
|
SyntaxFactory.SingletonSeparatedList<TypeSyntax>(
|
||||||
|
SyntaxFactory.PredefinedType(
|
||||||
|
SyntaxFactory.Token(SyntaxKind.StringKeyword))))))
|
||||||
|
.WithVariables(
|
||||||
|
SyntaxFactory.SingletonSeparatedList<VariableDeclaratorSyntax>(
|
||||||
|
SyntaxFactory.VariableDeclarator(
|
||||||
|
SyntaxFactory.Identifier("source"))
|
||||||
|
.WithInitializer(
|
||||||
|
SyntaxFactory.EqualsValueClause(
|
||||||
|
SyntaxFactory.ParseExpression(sourceExpression)))))),
|
||||||
|
SyntaxFactory.LocalDeclarationStatement(
|
||||||
|
SyntaxFactory.VariableDeclaration(
|
||||||
|
SyntaxFactory.IdentifierName(
|
||||||
|
SyntaxFactory.Identifier(
|
||||||
|
SyntaxFactory.TriviaList(),
|
||||||
|
SyntaxKind.VarKeyword,
|
||||||
|
"var",
|
||||||
|
"var",
|
||||||
|
SyntaxFactory.TriviaList())))
|
||||||
|
.WithVariables(
|
||||||
|
SyntaxFactory.SingletonSeparatedList<VariableDeclaratorSyntax>(
|
||||||
|
SyntaxFactory.VariableDeclarator(
|
||||||
|
SyntaxFactory.Identifier("prefix"))
|
||||||
|
.WithInitializer(
|
||||||
|
SyntaxFactory.EqualsValueClause(
|
||||||
|
SyntaxFactory.LiteralExpression(
|
||||||
|
SyntaxKind.StringLiteralExpression,
|
||||||
|
SyntaxFactory.Literal(prefix))))))),
|
||||||
|
SyntaxFactory.ExpressionStatement(
|
||||||
|
SyntaxFactory.InvocationExpression(
|
||||||
|
SyntaxFactory.MemberAccessExpression(
|
||||||
|
SyntaxKind.SimpleMemberAccessExpression,
|
||||||
|
SyntaxFactory.IdentifierName("Debug"),
|
||||||
|
SyntaxFactory.IdentifierName("Log")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.WithArgumentList(
|
||||||
|
SyntaxFactory.ArgumentList(
|
||||||
|
SyntaxFactory.SingletonSeparatedList<ArgumentSyntax>(
|
||||||
|
SyntaxFactory.Argument(
|
||||||
|
SyntaxFactory.InterpolatedStringExpression(
|
||||||
|
SyntaxFactory.Token(SyntaxKind.InterpolatedVerbatimStringStartToken)
|
||||||
|
)
|
||||||
|
.WithContents(
|
||||||
|
SyntaxFactory.List<InterpolatedStringContentSyntax>(
|
||||||
|
new InterpolatedStringContentSyntax[]{
|
||||||
|
SyntaxFactory.Interpolation(
|
||||||
|
SyntaxFactory.IdentifierName("prefix")
|
||||||
|
),
|
||||||
|
SyntaxFactory.InterpolatedStringText()
|
||||||
|
.WithTextToken(
|
||||||
|
SyntaxFactory.Token(
|
||||||
|
SyntaxFactory.TriviaList(),
|
||||||
|
SyntaxKind.InterpolatedStringTextToken,
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
SyntaxFactory.TriviaList()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
SyntaxFactory.Interpolation(
|
||||||
|
SyntaxFactory.InvocationExpression(
|
||||||
|
SyntaxFactory.MemberAccessExpression(
|
||||||
|
SyntaxKind.SimpleMemberAccessExpression,
|
||||||
|
SyntaxFactory.PredefinedType(
|
||||||
|
SyntaxFactory.Token(SyntaxKind.StringKeyword)
|
||||||
|
),
|
||||||
|
SyntaxFactory.IdentifierName("Join")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.WithArgumentList(
|
||||||
|
SyntaxFactory.ArgumentList(
|
||||||
|
SyntaxFactory.SeparatedList<ArgumentSyntax>(
|
||||||
|
new SyntaxNodeOrToken[]{
|
||||||
|
SyntaxFactory.Argument(
|
||||||
|
SyntaxFactory.LiteralExpression(
|
||||||
|
SyntaxKind.CharacterLiteralExpression,
|
||||||
|
SyntaxFactory.Literal(';')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
SyntaxFactory.Token(SyntaxKind.CommaToken),
|
||||||
|
SyntaxFactory.Argument(
|
||||||
|
SyntaxFactory.IdentifierName("source")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.NormalizeWhitespace();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MethodDeclarationSyntax GenerateSingleLogMethod(string methodName, string text, string prefix)
|
||||||
|
{
|
||||||
|
return SyntaxFactory.MethodDeclaration(
|
||||||
|
SyntaxFactory.PredefinedType(
|
||||||
|
SyntaxFactory.Token(SyntaxKind.VoidKeyword)
|
||||||
|
),
|
||||||
|
SyntaxFactory.Identifier(methodName)
|
||||||
|
)
|
||||||
|
.WithModifiers(
|
||||||
|
SyntaxFactory.TokenList(
|
||||||
|
new[]{
|
||||||
|
SyntaxFactory.Token(SyntaxKind.PublicKeyword),
|
||||||
|
SyntaxFactory.Token(SyntaxKind.StaticKeyword)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.WithBody(
|
||||||
|
SyntaxFactory.Block(
|
||||||
|
SyntaxFactory.SingletonList<StatementSyntax>(
|
||||||
|
SyntaxFactory.ExpressionStatement(
|
||||||
|
SyntaxFactory.InvocationExpression(
|
||||||
|
SyntaxFactory.MemberAccessExpression(
|
||||||
|
SyntaxKind.SimpleMemberAccessExpression,
|
||||||
|
SyntaxFactory.IdentifierName("Debug"),
|
||||||
|
SyntaxFactory.IdentifierName("Log")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.WithArgumentList(
|
||||||
|
SyntaxFactory.ArgumentList(
|
||||||
|
SyntaxFactory.SingletonSeparatedList<ArgumentSyntax>(
|
||||||
|
SyntaxFactory.Argument(
|
||||||
|
SyntaxFactory.InterpolatedStringExpression(
|
||||||
|
SyntaxFactory.Token(SyntaxKind.InterpolatedStringStartToken)
|
||||||
|
)
|
||||||
|
.WithContents(
|
||||||
|
SyntaxFactory.List<InterpolatedStringContentSyntax>(
|
||||||
|
new InterpolatedStringContentSyntax[]{
|
||||||
|
SyntaxFactory.Interpolation(
|
||||||
|
SyntaxFactory.LiteralExpression(
|
||||||
|
SyntaxKind.StringLiteralExpression,
|
||||||
|
SyntaxFactory.Literal(prefix)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
SyntaxFactory.InterpolatedStringText()
|
||||||
|
.WithTextToken(
|
||||||
|
SyntaxFactory.Token(
|
||||||
|
SyntaxFactory.TriviaList(),
|
||||||
|
SyntaxKind.InterpolatedStringTextToken,
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
SyntaxFactory.TriviaList()
|
||||||
|
)
|
||||||
|
),
|
||||||
|
SyntaxFactory.Interpolation(
|
||||||
|
SyntaxFactory.LiteralExpression(
|
||||||
|
SyntaxKind.StringLiteralExpression,
|
||||||
|
SyntaxFactory.Literal(text)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.NormalizeWhitespace();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize(GeneratorInitializationContext context)
|
||||||
|
{
|
||||||
|
context.RegisterForSyntaxNotifications(() => new ClassDeclarationSyntaxReceiver());
|
||||||
|
}
|
||||||
|
|
||||||
|
static string GetTemporaryPath(GeneratorExecutionContext context)
|
||||||
|
{
|
||||||
|
string tempPath;
|
||||||
|
var rootPath = GetProjectRoot(context);
|
||||||
|
if (rootPath != null)
|
||||||
|
{
|
||||||
|
tempPath = Path.Combine(rootPath, "Logs", "Packages", "dev.yarnspinner.unity");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tempPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "dev.yarnspinner.logs");
|
||||||
|
}
|
||||||
|
|
||||||
|
// we need to make the logs folder, but this can potentially fail
|
||||||
|
// if it does fail then we will just chuck the logs inside the tmp folder
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(tempPath))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(tempPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
tempPath = System.IO.Path.GetTempPath();
|
||||||
|
}
|
||||||
|
return tempPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Yarn.Unity.ILogger GetOutput(GeneratorExecutionContext context)
|
||||||
|
{
|
||||||
|
if (GetShouldLogToFile(context))
|
||||||
|
{
|
||||||
|
var tempPath = ActionRegistrationSourceGenerator.GetTemporaryPath(context);
|
||||||
|
|
||||||
|
var path = System.IO.Path.Combine(tempPath, $"{nameof(ActionRegistrationSourceGenerator)}-{context.Compilation.AssemblyName}.txt");
|
||||||
|
var outFile = System.IO.File.Open(path, System.IO.FileMode.Create);
|
||||||
|
|
||||||
|
return new Yarn.Unity.FileLogger(new System.IO.StreamWriter(outFile));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return new Yarn.Unity.NullLogger();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool GetShouldLogToFile(GeneratorExecutionContext context)
|
||||||
|
{
|
||||||
|
return context.ParseOptions.PreprocessorSymbolNames.Contains(DebugLoggingPreprocessorSymbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DumpGeneratedFile(GeneratorExecutionContext context, string text)
|
||||||
|
{
|
||||||
|
if (GetShouldLogToFile(context))
|
||||||
|
{
|
||||||
|
var tempPath = ActionRegistrationSourceGenerator.GetTemporaryPath(context);
|
||||||
|
var path = System.IO.Path.Combine(tempPath, $"{nameof(ActionRegistrationSourceGenerator)}-{context.Compilation.AssemblyName}.cs");
|
||||||
|
System.IO.File.WriteAllText(path, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class ClassDeclarationSyntaxReceiver : ISyntaxReceiver
|
||||||
|
{
|
||||||
|
public List<ClassDeclarationSyntax> Classes { get; private set; } = new List<ClassDeclarationSyntax>();
|
||||||
|
|
||||||
|
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
|
||||||
|
{
|
||||||
|
// Business logic to decide what we're interested in goes here
|
||||||
|
if (syntaxNode is ClassDeclarationSyntax cds)
|
||||||
|
{
|
||||||
|
Classes.Add(cds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user