Storyline+Dialog初步

This commit is contained in:
SoulliesOfficial
2026-07-05 16:08:23 -04:00
parent afa8a56e1d
commit d031afd075
464 changed files with 25716 additions and 4209 deletions

View File

@@ -1,447 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Ichni.Story.UI;
using Sirenix.OdinInspector;
using SLSUtilities.General;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Serialization;
namespace Ichni.Story
{
public partial class DialogManager : SerializedMonoBehaviour
{
public static DialogManager instance;
public List<TextAsset> dialogTextAssets;
public bool isPlayingDialog;
public bool isPlayingChoice;
public string currentDialog;
public int currentDialogSentenceIndex;
public string currentFinalType;
public Dictionary<string, List<string>> functionDictionary;
public Dictionary<string, List<DialogSentence>> dialogDictionary;
public Dictionary<string, ChoiceGroup> choiceDictionary;
public Dictionary<string, List<Condition>> conditionDictionary;
public List<UnityAction> dialogEndActions;
private string currentLoadingDialog;
public DialogUIPage dialogUIPage;
private void Awake()
{
instance = this;
}
public void SetDialog(string dialogName)
{
string chapter = ChapterSelectionManager.instance.currentChapter.chapterIndex;
TextAsset dialog = Resources.Load<TextAsset>("Story/" + chapter + "/Dialogs/" + dialogName);
dialogUIPage.dialogContentFrame.ClearAllSentences();
SetDialog(new List<TextAsset> { dialog });
}
public void SetDialog(List<TextAsset> dialogFiles, string dialogParagraphName = "")
{
dialogUIPage.FadeIn();
isPlayingDialog = true;
currentDialog = "NULL";
dialogEndActions = new List<UnityAction>();
LoadDialog(dialogFiles, out string firstHeader);
currentDialog = dialogParagraphName == "" ? firstHeader : dialogParagraphName;
}
public void PlayNextDialogParagraph(string nextDialog, bool invokeFunctions = true)
{
currentDialog = nextDialog;
currentDialogSentenceIndex = 0;
if (invokeFunctions && functionDictionary.TryGetValue(currentDialog, out List<string> functionList))
{
functionList.ForEach(x => StoryInterpreters.FunctionInterpreter.Eval(x));
}
if (choiceDictionary.ContainsKey(currentDialog))
{
currentFinalType = "Choice";
}
else if (conditionDictionary.ContainsKey(currentDialog))
{
currentFinalType = "Condition";
}
else
{
currentFinalType = "None";
}
}
public void PlayDialog()
{
if(currentDialog == "NULL")
{
throw new Exception("Current dialog is NULL");
}
if (isPlayingChoice)
{
return;
}
if (dialogDictionary[currentDialog].Count > 0 && currentDialogSentenceIndex < dialogDictionary[currentDialog].Count)
{
DialogSentence currentSentence = dialogDictionary[currentDialog][currentDialogSentenceIndex];
string interpretedContent = currentSentence.GetInterpretedContent();
dialogUIPage.dialogContentFrame.PlaySentence(currentSentence.characterName, interpretedContent);
currentDialogSentenceIndex++;
if (currentDialogSentenceIndex <= dialogDictionary[currentDialog].Count)
{
return;
}
}
if(currentDialogSentenceIndex >= dialogDictionary[currentDialog].Count)
{
if (currentFinalType == "Choice")
{
isPlayingChoice = true;
dialogUIPage.dialogContentFrame.PlayChoice(choiceDictionary[currentDialog]);
return;
}
if (currentFinalType == "Condition")
{
foreach (var condition in conditionDictionary[currentDialog])
{
if (condition.GetConditionResult())
{
PlayNextDialogParagraph(condition.nextDialogName);
return;
}
}
}
}
if (currentFinalType == "None" && currentDialogSentenceIndex >= dialogDictionary[currentDialog].Count)
{
isPlayingDialog = false;
dialogUIPage.FadeOut();
if (StoryManager.instance.storyline.currentBlock.state == StoryBlockState.Current)
{
StoryManager.instance.storyline.currentBlock.state = StoryBlockState.Completed;
StoryManager.instance.storyUIPage.messageBox.Clear();
dialogEndActions.ForEach(action => action.Invoke());
StoryManager.instance.storyUIPage.messageBox.SetUp();
StoryManager.instance.storyline.SaveStoryline(ChapterSelectionManager.instance.currentChapter.chapterIndex);
Debug.Log("Dialog completed, setting block state to Completed.");
}
}
}
public void RevealDialog()
{
string finalType;
int max = 0;
Debug.Log($"Revealing dialog: {currentDialog}, currentFinalType: {currentFinalType}");
do
{
finalType = currentFinalType;
currentDialogSentenceIndex = 0;
foreach (DialogSentence sentence in dialogDictionary[currentDialog])
{
string interpretedContent = sentence.GetInterpretedContent();
dialogUIPage.dialogContentFrame.PlaySentence(sentence.characterName, interpretedContent);
currentDialogSentenceIndex++;
}
if (finalType == "Choice")
{
ChoiceGroup choiceGroup = choiceDictionary[currentDialog];
int choiceIndex = GameSaveManager.instance.StorySaveModule.selectedChoices[choiceGroup.choiceName];
dialogUIPage.dialogContentFrame.SelectChoice(choiceGroup, choiceIndex);
}
if (finalType == "Condition")
{
foreach (var condition in conditionDictionary[currentDialog])
{
if (condition.GetConditionResult())
{
PlayNextDialogParagraph(condition.nextDialogName, false);
}
}
}
max++;
if (max > 1024)
{
throw new Exception("An infinite loop may detected in dialog parsing. Please check the dialog structure.");
}
} while (finalType != "None");
}
}
public partial class DialogManager
{
public void LoadDialog(List<TextAsset> dialogFiles, out string firstHeader)
{
ClearDictionaries();
firstHeader = string.Empty;
dialogTextAssets = dialogFiles;
List<string> dialogLines = new List<string>();
foreach (TextAsset textAsset in dialogTextAssets)
{
dialogLines.AddRange(ExtractValidFragments(textAsset.text));
}
dialogLines.RemoveAll(line => line.Trim() == "");
//dialogLines.ForEach(Debug.Log);
foreach (string line in dialogLines)
{
if (!ParseHeader(line))
{
if (!ParseChoiceModule(line))
{
if (!ParseConditionModule(line))
{
if (!ParseDialogSentence(line))
{
throw new Exception($"Invalid dialog line: {line}"); // 抛出异常,提示不合法的对话行
}
}
}
}
else
{
if (firstHeader == string.Empty)
{
firstHeader = currentDialog;
}
}
}
//dialogDictionary.RemoveWhere((header, sentences) => sentences == null || sentences.Count == 0);
choiceDictionary.RemoveWhere((header, choices) => choices == null || choices.choices.Count == 0);
conditionDictionary.RemoveWhere((header, conditions) => conditions == null || conditions.Count == 0);
}
/// <summary>
/// 从原始大文本中提取所有以 '$' 开头的有效片段,忽略以 '#' 开头的注释片段。
/// 拆分依据:每当遇到 '$' 或 '#' 字符,即视为一个新片段的起始。
/// </summary>
/// <param name="inputText">未分割的完整文本(可能包含任意换行或连续内容)。</param>
/// <returns>剥离首 '$' 后的有效文本列表。</returns>
public static List<string> ExtractValidFragments(string inputText)
{
if (inputText == null)
{
throw new ArgumentNullException(nameof(inputText));
}
// 正则:(?<prefix>[$#]) // 片段起始前缀
// (?<content>.*? ) // 非贪婪捕获所有内容
// (?=(?:[$#])|\z) // 直到下一个 '$'、'#' 或文末
const string pattern = @"(?<prefix>[$#])(?<content>.*?)(?=(?:[$#])|\z)";
MatchCollection matches = Regex.Matches(inputText, pattern, RegexOptions.Singleline);
var result = new List<string>(matches.Count);
foreach (Match m in matches)
{
char prefix = m.Groups["prefix"].Value[0];
string content = m.Groups["content"].Value;
if (prefix == '$')
{
result.Add(content.Trim());
}
// prefix == '#' 时自动忽略
}
return result;
}
}
public partial class DialogManager
{
public void ClearDictionaries()
{
dialogDictionary.Clear();
choiceDictionary.Clear();
conditionDictionary.Clear();
functionDictionary.Clear();
}
public bool ParseHeader(string line)
{
//格式:[currentLoadingDialog]{Function0();Function1();Function2();}
line = line.Trim();
string dialogTitle = line.Split("{")[0];
if (dialogTitle[0] != '[' || dialogTitle[^1] != ']')
{
return false;
}
currentLoadingDialog = dialogTitle.Replace("[", "").Replace("]", "");
dialogDictionary.Add(currentLoadingDialog, new List<DialogSentence>());
//choiceDictionary.Add(currentLoadingDialog, new ChoiceGroup("Error"));
conditionDictionary.Add(currentLoadingDialog, new List<Condition>());
if (currentDialog == "NULL")
{
currentDialog = currentLoadingDialog;
}
if (!line.Contains("{")) // 这个Header没有函数需要执行
{
return true;
}
string functions = line.Split("{")[1];
if (functions.Contains("}"))
{
functions = functions.Split("}")[0];
}
else
{
throw new System.Exception("Dialog header's function list must be enclosed in {}.");
}
functions = functions.Replace(" ", "").Replace("\n", "").Replace("\r", "").Trim(); //忽略空格,换行
List<string> functionList = functions.Split(';').ToList(); //分割函数
functionList = functionList.Where(x => !string.IsNullOrEmpty(x)).ToList(); //去除空函数
functionDictionary.Add(currentLoadingDialog, functionList);
return true;
}
public bool ParseDialogSentence(string line)
{
//speakerName:sentence
string[] sentenceData;
if (line.Contains(":"))
{
sentenceData = line.Split(":", 2);
}
else
{
return false;
}
string character = sentenceData[0];
string speakerName = character;
DialogSentence dialogSentence = new DialogSentence
{
characterName = speakerName.Trim(),
content = sentenceData[1].Trim()
};
dialogDictionary[currentLoadingDialog].Add(dialogSentence);
return true;
}
public bool ParseChoiceModule(string line)
{
//$Choice(ChoiceName){
//choiceText0->[nextDialogName0];
//choiceText1->[nextDialogName1];
//}
line = line.Trim();
if (line.Contains("Choice"))
{
string[] choiceModuleData = line.Split('{');
string choiceName = choiceModuleData[0].Split('(')[1].Replace(")", "").Trim();
ChoiceGroup choiceGroup = new ChoiceGroup(choiceName);
string[] choiceData = choiceModuleData[1].Split(';');
for (var index = 0; index < choiceData.Length - 1; index++)
{
choiceData[index] = choiceData[index].Replace(" ", "").Replace("\n", "").Replace("\r", "").Trim();
string choiceText = choiceData[index].Split("->[")[0].Trim();
string nextDialogName = choiceData[index].Split("->[")[1].Replace("]", "").Trim();
choiceGroup.choices.Add(new Choice(choiceText, nextDialogName));
}
choiceDictionary[currentLoadingDialog] = choiceGroup;
return true;
}
return false;
}
/// <summary>
/// 解析条件模块
/// </summary>
/// <param name="line"></param>
/// <returns></returns>
private bool ParseConditionModule(string line)
{
//$Condition{
//conditionSentence0->[nextDialogName0];
//conditionSentence1->[nextDialogName1];
//}
if (line.Contains("Condition"))
{
string[] conditionModuleData = line.Split('{');
List<Condition> conditions = new List<Condition>();
string[] conditionData = conditionModuleData[1].Split(';');
for (var index = 0; index < conditionData.Length - 1; index++)
{
conditionData[index] = conditionData[index].Replace(" ", "").Replace("\n", "").Replace("\r", "").Trim();
Condition condition = new Condition
{
conditionSentence = conditionData[index].Split("->[")[0].Trim(),
nextDialogName = conditionData[index].Split("->[")[1].Replace("]", "").Trim(),
};
conditions.Add(condition);
}
conditionDictionary[currentLoadingDialog] = conditions;
return true;
}
return false;
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 9e89b9d7eea97734baa166072b050239
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,106 +0,0 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEngine;
namespace Ichni.Story
{
public class DialogSentence
{
public string content;
public string audioEventName;
public string characterName;
public DialogSentence()
{
}
public DialogSentence(string content, string audioEventName, string characterName)
{
this.content = content;
this.audioEventName = audioEventName;
this.characterName = characterName;
}
/// <summary>
/// 匹配{@FUNCTION},解析函数并返回解析后的语句内容。
/// </summary>
public string GetInterpretedContent()
{
List<string> parts = new List<string>();
Regex regex = new Regex(@"\{\@.*?\}");
int lastIndex = 0;
foreach (Match match in regex.Matches(content))
{
if (match.Index > lastIndex)
{
parts.Add(content.Substring(lastIndex, match.Index - lastIndex));
}
parts.Add(match.Value);
lastIndex = match.Index + match.Length;
}
if (lastIndex < content.Length)
{
parts.Add(content.Substring(lastIndex));
}
for (int i = 0; i < parts.Count; i++)
{
if (parts[i].StartsWith("{@") && parts[i].EndsWith("}"))
{
string expression = parts[i].Substring(2, parts[i].Length - 3);
object result = StoryInterpreters.FunctionInterpreter.Eval(expression);
parts[i] = result.ToString();
}
}
return string.Join("", parts);
}
}
public class ChoiceGroup
{
public string choiceName;
public List<Choice> choices;
public ChoiceGroup(string choiceName)
{
this.choiceName = choiceName;
this.choices = new List<Choice>();
}
}
public class Choice
{
public string choiceText;
public string nextDialogName;
public Choice(string choiceText, string nextDialogName)
{
this.choiceText = choiceText;
this.nextDialogName = nextDialogName;
}
}
public class Condition
{
public string conditionSentence;
public string nextDialogName;
public bool GetConditionResult()
{
bool result = StoryInterpreters.ConditionInterpreter.Eval<bool>(conditionSentence);
return result;
}
}
public class DialogCharacter
{
public string name;
public string title;
public Dictionary<string, Sprite> emotions;
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: d32abf19c3d0f4546b2b86f9fcc4e117
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,104 +0,0 @@
using System;
using System.Collections.Generic;
using DynamicExpresso;
using Ichni.Menu;
using Ichni.Story;
using Ichni.Story.UI;
using UnityEngine;
namespace Ichni.Story
{
public static partial class StoryInterpreters
{
public static readonly Interpreter FunctionInterpreter;
public static readonly Interpreter ConditionInterpreter;
static StoryInterpreters()
{
FunctionInterpreter = new Interpreter();
ConditionInterpreter = new Interpreter();
SetFunctionInterpreter();
SetConditionInterpreter();
}
static void SetFunctionInterpreter()
{
FunctionInterpreter.SetFunction("SetVariable", new Action<string, int>(SetStoryVariable));
FunctionInterpreter.SetFunction("GetVariable", new Func<string, int>(GetStoryVariable));
FunctionInterpreter.SetFunction("GenerateDialogBlock", new Action<string>(GenerateDialogBlock));
FunctionInterpreter.SetFunction("GenerateSongBlock", new Action<string>(GenerateSongBlock));
FunctionInterpreter.SetFunction("SetUnlockKey", new Action<string>(SetUnlockKey));
}
static void SetConditionInterpreter()
{
ConditionInterpreter.SetFunction("GetVariable", new Func<string, int>(GetStoryVariable));
}
}
public static partial class StoryInterpreters
{
/// <summary>
/// 设置全局变量的值
/// </summary>
static void SetStoryVariable(string variableName, int value)
{
GameSaveManager.instance.StorySaveModule.storyVariables[variableName] = value;
}
/// <summary>
/// 获取全局变量的值
/// </summary>
static int GetStoryVariable(string variableName)
{
if (GameSaveManager.instance.StorySaveModule.storyVariables.TryGetValue(variableName, out int value))
{
return value;
}
throw new ArgumentException($"Global variable '{variableName}' not found.");
}
}
public static partial class StoryInterpreters
{
static void GenerateDialogBlock(string blockName)
{
StoryBlockUIBase currentBlock = StoryManager.instance.storyline.currentBlock;
Vector2 positionOffset = new Vector2(500, 0);
DialogBlockUI newBlock = StoryManager.instance.storyline.GenerateDialogBlock(blockName, currentBlock.blockPosition + positionOffset, StoryBlockState.Current);
StoryManager.instance.storyline.GenerateConnector(currentBlock, newBlock);
StoryManager.instance.storyline.SetUpBackground();
StoryManager.instance.storyline.connectors.ForEach(c => c.SetCurve());
}
static void GenerateSongBlock(string blockName)
{
StoryBlockUIBase currentBlock = StoryManager.instance.storyline.currentBlock;
Vector2 positionOffset = new Vector2(500, 0);
SongBlockUI newBlock = StoryManager.instance.storyline.GenerateSongBlock(blockName, currentBlock.blockPosition + positionOffset, StoryBlockState.Current);
StoryManager.instance.storyline.GenerateConnector(currentBlock, newBlock);
}
static void SetUnlockKey(string key)
{
if (GameSaveManager.instance.SongSaveModule.storyUnlockKeys.Add(key))
{
GameSaveManager.instance.SongSaveModule.SaveStoryUnlockKeys();
DialogManager.instance.dialogEndActions.Add(() =>
{
ChapterSelectionUnit currentChapter = ChapterSelectionManager.instance.currentChapter;
List<string> unlockedSongs = currentChapter.GetRelatedSongNamesOfUnlockKey(key);
foreach (string songName in unlockedSongs)
{
StoryManager.instance.storyUIPage.messageBox.AddInfo(
"Message/Unlock_Song_Title", "Message/Unlock_Song",
() => StoryManager.instance.storyUIPage.messageBox.SetParameter("SongName", songName));
}
});
}
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: dd119fb631aebb548a637407d15c5eea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,43 +0,0 @@
using System;
using System.Collections.Generic;
using Ichni.Story.UI;
using Ichni.UI;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.Serialization;
namespace Ichni.Story
{
public partial class StoryManager : SerializedMonoBehaviour
{
public static StoryManager instance;
[FormerlySerializedAs("storylineDisplay")] public Storyline storyline;
public StoryUIPage storyUIPage;
public Dictionary<string, StoryData> storyDatas;
void Awake()
{
instance = this;
}
}
public partial class StoryManager
{
[Button]
public void ClearAllStorySave()
{
GameSaveManager.instance.StorySaveModule.ClearAllStoryline();
GameSaveManager.instance.SongSaveModule.ClearStoryKeys();
}
}
public enum StoryBlockState
{
Locked,
Current,
Completed
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: e58008720832d5045ada32d1161faa69
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,106 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.Serialization;
namespace Ichni.Story
{
[CreateAssetMenu(fileName = "StoryData", menuName = "Ichni/Story/StoryData")]
public class StoryData : SerializedScriptableObject
{
public List<DialogBlockData> dialogBlockDatas; // 剧情单元格名称列表
public List<SongBlockData> songBlockDatas; // 音乐单元格名称列表
public List<TutorialBlockData> tutorialBlockDatas; // 教程单元格名称列表
public List<InitialBlockData> initialBlocks; // 初始剧情单元格列表,包含所有初始剧情单元格的名称
public StoryBlockData GetDataByName(string blockName, out Type dataType)
{
foreach (var block in tutorialBlockDatas.Where(block => block.blockName == blockName))
{
dataType = typeof(TutorialBlockData);
return block;
}
foreach (var block in songBlockDatas.Where(block => block.blockName == blockName))
{
dataType = typeof(SongBlockData);
return block;
}
foreach (var block in dialogBlockDatas.Where(block => block.blockName == blockName))
{
dataType = typeof(DialogBlockData);
return block;
}
throw new ArgumentException($"No block found with name: {blockName}");
}
}
[InlineProperty]
[Serializable]
public class InitialBlockData
{
public string blockName;
public StoryBlockState initialState; // 初始状态
public Vector2 blockPosition; // 初始位置
public List<string> nextBlocks; // 下一步可选的剧情单元格名称列表
}
[InlineProperty]
[Serializable]
public class StoryBlockData
{
[FoldoutGroup("$blockName", true)]
public string blockName;
[FoldoutGroup("$blockName")]
public string blockID;
[FoldoutGroup("$blockName")]
public Vector2 blockSize;
}
[InlineProperty]
[Serializable]
public class TutorialBlockData : StoryBlockData
{
[FoldoutGroup("$blockName")]
public string tutorialName;
public TutorialBlockData()
{
this.blockSize = new Vector2(400, 200);
}
}
[InlineProperty]
[Serializable]
public class DialogBlockData : StoryBlockData
{
[FoldoutGroup("$blockName")]
public string dialogTitle;
public DialogBlockData()
{
this.blockSize = new Vector2(400, 200);
}
}
[InlineProperty]
[Serializable]
public class SongBlockData : StoryBlockData
{
[FoldoutGroup("$blockName")]
public string songName;
public SongBlockData()
{
this.blockSize = new Vector2(400, 200);
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 7ab917c50249812429ebd44d6574497c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,39 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using Ichni.Story.UI;
using SLSUtilities.General;
using UnityEngine;
using UnityEngine.UI.Extensions;
namespace Ichni.Story
{
public class BlockConnectorUI : MonoBehaviour
{
public UILineRenderer curve;
public StoryBlockUIBase startBlock;
public StoryBlockUIBase endBlock;
public void SetCurve(StoryBlockUIBase startBlock = null, StoryBlockUIBase endBlock = null)
{
this.startBlock ??= startBlock;
this.endBlock ??= endBlock;
if(this.startBlock == null || this.endBlock == null)
{
Debug.LogWarning("Start or end block is not set for the curve.");
return;
}
Vector2 startPosition = SpaceConverter.GetLocalUIPosition(this.startBlock.outPort, GetComponent<RectTransform>());
Vector2 endPosition = SpaceConverter.GetLocalUIPosition(this.endBlock.inPort, GetComponent<RectTransform>());
Vector2 mid1 = (startPosition + endPosition) / 2;
Vector2 mid2 = (startPosition + endPosition) / 2;
mid1.y = startPosition.y;
mid2.y = endPosition.y;
curve.Points = new Vector2[] { startPosition, /*mid1*/ mid2, endPosition};
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 98bbf463dca50cc43961c0bb2b8d4f22
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,53 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Ichni.Story.UI
{
public class DialogBlockUI : StoryBlockUIBase
{
public string blockTitle;
public TMP_Text titleText;
public Button button;
public List<ChoiceGroupUI> choiceGroups;
public void Initialize(string blockName, Vector2 position, Vector2 positionOffset,
Vector2 size, StoryBlockState state, string blockTitle)
{
base.Initialize(blockName, position, positionOffset, size, state);
this.blockTitle = blockTitle;
titleText.text = blockTitle;
button.onClick.AddListener(() =>
{
state = this.state;
if(state == StoryBlockState.Locked) return;
StoryManager.instance.storyline.currentBlock = this;
if (state == StoryBlockState.Current)
{
DialogManager.instance.SetDialog(blockName);
DialogManager.instance.PlayNextDialogParagraph(DialogManager.instance.currentDialog);
}
else if (state == StoryBlockState.Completed)
{
DialogManager.instance.SetDialog(blockName);
DialogManager.instance.PlayNextDialogParagraph(DialogManager.instance.currentDialog, false);
DialogManager.instance.RevealDialog();
}
});
}
public override StoryBlockSave GetBlockSave()
{
return new DialogBlockSave(blockName, blockPosition, state);
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: b976128eb3ec59e4e866dbb610441706
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,76 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Ichni.Menu;
using Ichni.RhythmGame;
using TMPro;
using UnityEngine;
using UnityEngine.Serialization;
using UnityEngine.UI;
namespace Ichni.Story.UI
{
public class SongBlockUI : StoryBlockUIBase
{
public string songName;
public Button button;
public TMP_Text songNameText;
public RectTransform beatmapStatusMarkContainer;
public GameObject beatmapStatusMarkPrefab;
public void Initialize(string blockName, Vector2 position, Vector2 positionOffset,
Vector2 size, StoryBlockState state, string songName)
{
base.Initialize(blockName, position, positionOffset, size, state);
this.songName = songName;
songNameText.text = songName;
button.onClick.AddListener(() =>
{
//MenuManager.instance.prepareUIPage.SetUpPrepareUIPage(songName);
//MenuManager.instance.prepareUIPage.FadeIn();
});
SetUpBeatmapStatusMarks();
}
public override StoryBlockSave GetBlockSave()
{
return new SongBlockSave(blockName, blockPosition, state);
}
public void SetUpBeatmapStatusMarks()
{
SongStatusSave songStatusSave = GameSaveManager.instance.SongSaveModule.songStatusSaves[songName];
string chapter = ChapterSelectionManager.instance.currentChapter.chapterIndex;
ChapterSelectionUnit cpt = ChapterSelectionManager.instance.chapters.First(c => c.chapterIndex == chapter);
SongItemData song = cpt.songs.First(s => s.songName == this.songName);
for (var index = 0; index < song.difficultyDataList.Count; index++)
{
var difficulty = song.difficultyDataList[index];
var beatmapSave = songStatusSave.beatmapSaves[index];
if (beatmapSave.isAllPerfect)
{
GameObject mark = Instantiate(beatmapStatusMarkPrefab, beatmapStatusMarkContainer);
mark.GetComponent<Image>().color = difficulty.color;
mark.transform.GetChild(0).GetComponent<TMP_Text>().color = difficulty.color;
mark.transform.GetChild(0).GetComponent<TMP_Text>().text = "AP";
break;
}
if (beatmapSave.isFullCombo)
{
GameObject mark = Instantiate(beatmapStatusMarkPrefab, beatmapStatusMarkContainer);
mark.GetComponent<Image>().color = difficulty.color;
mark.transform.GetChild(0).GetComponent<TMP_Text>().color = difficulty.color;
mark.transform.GetChild(0).GetComponent<TMP_Text>().text = "FC";
break;
}
}
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 5efe0a39fe908354e9ab6d1edfdb8843
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,30 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
namespace Ichni.Story.UI
{
public abstract class StoryBlockUIBase : MonoBehaviour
{
public string blockName;
public Vector2 blockPosition;
public StoryBlockState state;
public RectTransform blockRect;
public RectTransform inPort;
public RectTransform outPort;
protected void Initialize(string blockName, Vector2 position, Vector2 positionOffset, Vector2 size, StoryBlockState state)
{
this.blockName = blockName;
this.blockPosition = position;
this.state = state;
blockRect.anchoredPosition = position + positionOffset;
blockRect.sizeDelta = size;
}
public abstract StoryBlockSave GetBlockSave();
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: d489f986a1eea1e4c938658f0fd468ca
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,52 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using Ichni.Menu;
using SLSUtilities.WwiseAssistance;
using TMPro;
using UniRx;
using UnityEngine;
using UnityEngine.UI;
namespace Ichni.Story.UI
{
public class TutorialBlockUI : StoryBlockUIBase
{
public Button button;
public string tutorialName;
public TMP_Text tutorialNameText;
public void Initialize(string blockName, Vector2 position, Vector2 positionOffset, Vector2 size, StoryBlockState state, string tutorialName)
{
base.Initialize(blockName, position, positionOffset, size, state);
this.tutorialName = tutorialName;
tutorialNameText.text = tutorialName;
button.onClick.AddListener(EnterTutorial);
}
public override StoryBlockSave GetBlockSave()
{
return new TutorialBlockSave(blockName, blockPosition, state);
}
private void EnterTutorial()
{
ChapterSelectionUnit chapter = ChapterSelectionManager.instance.currentChapter;
SongItemData song = ChapterSelectionManager.instance.tutorialCollection.songs[chapter.chapterIndex];
DifficultyData difficulty = song.difficultyDataList[0];
InformationTransistor.instance.SetInformation(chapter, song, difficulty);
InformationTransistor.instance.isReturnedFromTutorial = true;
InformationTransistor.instance.isReturnedFromGame = false;
AudioManager.Post(AK.EVENTS.ENTERTOGAME);
SongSelectionManager.instance.StopPreviewSong();
DOTween.KillAll();
Observable.Timer(TimeSpan.FromSeconds(0.6f)).Subscribe(_ => { MenuManager.instance.EnterGameScene(); });
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 821e50e1519236a46b03eb1f005acebe
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,11 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Ichni.Story.UI
{
public class ChoiceButtonUI : MonoBehaviour
{
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 90f34f59ff260c44796d71c51b7c0ee6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,45 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using I2.Loc;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Ichni.Story.UI
{
public class ChoiceGroupUI : MonoBehaviour
{
public GameObject choiceButtonPrefab;
public RectTransform container;
public List<Button> choiceButtonList;
public string choiceName;
public int choiceIndex;
public void Initialize(ChoiceGroup choiceGroup)
{
this.choiceName = choiceGroup.choiceName;
choiceButtonList = new List<Button>();
for (var index = 0; index < choiceGroup.choices.Count; index++)
{
var choice = choiceGroup.choices[index];
int cIndex = index; // Capture the current index for the listener
GameObject choiceButton = Instantiate(choiceButtonPrefab, container);
choiceButton.GetComponentInChildren<Localize>().SetTerm(ChapterSelectionManager.instance.currentChapter.chapterIndex + "/" + choice.choiceText);
choiceButton.GetComponent<Button>().onClick.AddListener(() =>
{
DialogManager.instance.PlayNextDialogParagraph(choice.nextDialogName);
DialogManager.instance.isPlayingChoice = false;
choiceButtonList.ForEach(b => b.interactable = false);
DialogManager.instance.PlayDialog();
this.choiceIndex = cIndex;
GameSaveManager.instance.StorySaveModule.selectedChoices[choiceName] = cIndex;
});
choiceButtonList.Add(choiceButton.GetComponent<Button>());
}
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: a653477cd0de8794b810214793b04cc9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,81 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using DG.Tweening;
using DG.Tweening.Core;
using DG.Tweening.Plugins.Options;
using I2.Loc;
using Ichni.Story.UI;
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Serialization;
using UnityEngine.UI;
namespace Ichni.Story
{
public class DialogContentFrame : MonoBehaviour, IPointerClickHandler
{
public GameObject textPrefab;
public GameObject choiceGroupPrefab;
public RectTransform dialogContentContainer;
public List<DialogTextUI> dialogTexts;
public List<ChoiceGroupUI> choiceGroups;
public void PlaySentence(string speakerName, string content)
{
DialogTextUI dialogTextUI = Instantiate(textPrefab, dialogContentContainer).GetComponent<DialogTextUI>();
dialogTextUI.speakerNameText.SetTerm("Characters/" + speakerName);
dialogTextUI.contentText.SetTerm(ChapterSelectionManager.instance.currentChapter.chapterIndex +"/" +content);
dialogTexts.Add(dialogTextUI);
}
public ChoiceGroupUI PlayChoice(ChoiceGroup choiceGroup)
{
ChoiceGroupUI choiceGroupUI = Instantiate(choiceGroupPrefab, dialogContentContainer).GetComponent<ChoiceGroupUI>();
choiceGroupUI.Initialize(choiceGroup);
choiceGroups.Add(choiceGroupUI);
return choiceGroupUI;
}
public void SelectChoice(ChoiceGroup choiceGroup, int index)
{
ChoiceGroupUI choiceGroupUI = PlayChoice(choiceGroup);
for (var buttonIndex = 0; buttonIndex < choiceGroupUI.choiceButtonList.Count; buttonIndex++)
{
Button b = choiceGroupUI.choiceButtonList[buttonIndex];
b.interactable = false;
if (buttonIndex == index)
{
b.image.color = Color.red;
}
}
DialogManager.instance.PlayNextDialogParagraph(choiceGroup.choices[index].nextDialogName, false);
}
public void ClearAllSentences()
{
foreach (DialogTextUI dialogText in dialogTexts)
{
Destroy(dialogText.gameObject);
}
foreach (ChoiceGroupUI choiceGroup in choiceGroups)
{
Destroy(choiceGroup.gameObject);
}
dialogTexts.Clear();
choiceGroups.Clear();
}
public void OnPointerClick(PointerEventData eventData)
{
DialogManager.instance.PlayDialog();
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 14874d8a3e4a31941879415479892501
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,16 +0,0 @@
using System.Collections;
using System.Collections.Generic;
using I2.Loc;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Ichni.Story.UI
{
public class DialogTextUI : MonoBehaviour
{
public Image background;
public Localize speakerNameText;
public Localize contentText;
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 831ccff3dc06bfc4884663d623af866d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,26 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Ichni.UI;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.Serialization;
using UnityEngine.UI;
namespace Ichni.Story.UI
{
public class DialogUIPage : UIPageBase
{
public Button closeButton;
public DialogContentFrame dialogContentFrame;
private void Start()
{
closeButton.onClick.AddListener(() =>
{
FadeOut();
dialogContentFrame.ClearAllSentences();
});
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 17254192719abee4f9222246fd403de5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,272 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.Serialization;
namespace Ichni.Story.UI
{
public partial class Storyline : MonoBehaviour
{
[Header("UI References")]
public RectTransform content; // Content of ScrollRect
[FormerlySerializedAs("textBlockPrefab")] public GameObject dialogBlockPrefab; // Prefab of UI Node
public GameObject musicBlockPrefab;
public GameObject tutorialBlockPrefab;
public GameObject connectionCurvePrefab; // Prefab of connection curve
[Header("Layout Settings")]
public float marginLeft = 50f; // additive space on the left
public float marginRight = 50f; // Extra space on the right
public float marginTop = 50f; // Extra space on the top
public float marginBottom = 50f; // Extra space on the bottom
public RectTransform connectionContainer;
public StoryBlockUIBase currentBlock;
public List<StoryBlockUIBase> storyBlocks;
public List<DialogBlockUI> dialogBlocks;
public List<SongBlockUI> songBlocks;
public List<TutorialBlockUI> tutorialBlocks;
public List<BlockConnectorUI> connectors;
private void Start()
{
storyBlocks = new List<StoryBlockUIBase>();
dialogBlocks = new List<DialogBlockUI>();
songBlocks = new List<SongBlockUI>();
tutorialBlocks = new List<TutorialBlockUI>();
connectors = new List<BlockConnectorUI>();
SetUpStoryline(ChapterSelectionManager.instance.currentChapter.chapterIndex);
SetUpBackground();
connectionContainer.SetParent(content);
connectionContainer.SetAsFirstSibling();
}
}
public partial class Storyline
{
public TutorialBlockUI GenerateTutorialBlock(string blockName, Vector2 position, StoryBlockState state)
{
TutorialBlockUI block = Instantiate(tutorialBlockPrefab, content).GetComponent<TutorialBlockUI>();
StoryData storyData = StoryManager.instance.storyDatas[ChapterSelectionManager.instance.currentChapter.chapterIndex];
TutorialBlockData blockData = storyData.tutorialBlockDatas.FirstOrDefault(data => data.blockName == blockName);
if (blockData == null) throw new KeyNotFoundException("There is no block with name " + blockName);
block.Initialize(blockData.blockName, position, new Vector2(marginLeft, 0), blockData.blockSize, state, blockData.tutorialName);
storyBlocks.Add(block);
tutorialBlocks.Add(block);
return block;
}
public DialogBlockUI GenerateDialogBlock(string blockName, Vector2 position, StoryBlockState state)
{
DialogBlockUI block = Instantiate(dialogBlockPrefab, content).GetComponent<DialogBlockUI>();
StoryData storyData = StoryManager.instance.storyDatas[ChapterSelectionManager.instance.currentChapter.chapterIndex];
DialogBlockData blockData = storyData.dialogBlockDatas.FirstOrDefault(data => data.blockName == blockName);
if (blockData == null) throw new KeyNotFoundException("There is no block with name " + blockName);
block.Initialize(blockData.blockName, position, new Vector2(marginLeft, 0), blockData.blockSize, state, blockData.dialogTitle);
storyBlocks.Add(block);
dialogBlocks.Add(block);
return block;
}
public SongBlockUI GenerateSongBlock(string blockName, Vector2 position, StoryBlockState state)
{
SongBlockUI block = Instantiate(musicBlockPrefab, content).GetComponent<SongBlockUI>();
StoryData storyData = StoryManager.instance.storyDatas[ChapterSelectionManager.instance.currentChapter.chapterIndex];
SongBlockData blockData = storyData.songBlockDatas.FirstOrDefault(data => data.blockName == blockName);
if (blockData == null) throw new KeyNotFoundException("There is no block with name " + blockName);
block.Initialize(blockName,position,new Vector2(marginLeft, 0), blockData.blockSize, state, blockData.songName);
storyBlocks.Add(block);
songBlocks.Add(block);
return block;
}
public void GenerateConnector(StoryBlockUIBase startBlock, StoryBlockUIBase endBlock)
{
BlockConnectorUI connector = Instantiate(connectionCurvePrefab, connectionContainer).GetComponent<BlockConnectorUI>();
connector.SetCurve(startBlock, endBlock);
connectors.Add(connector);
}
public void GenerateConnector(string startBlockName, string endBlockName)
{
StoryBlockUIBase startBlock = storyBlocks.FirstOrDefault(block => block.blockName == startBlockName);
StoryBlockUIBase endBlock = storyBlocks.FirstOrDefault(block => block.blockName == endBlockName);
GenerateConnector(startBlock, endBlock);
}
}
public partial class Storyline
{
private void ClearStoryline()
{
foreach (var block in storyBlocks)
{
Destroy(block.gameObject);
}
storyBlocks.Clear();
dialogBlocks.Clear();
songBlocks.Clear();
tutorialBlocks.Clear();
foreach (var connector in connectors)
{
Destroy(connector.gameObject);
}
connectors.Clear();
content.sizeDelta = Vector2.zero;
}
public void SetUpBackground()
{
float maxRight = float.MinValue;
foreach (var block in storyBlocks)
{
float rightEdge = block.blockRect.anchoredPosition.x + block.blockRect.sizeDelta.x * 0.5f;
if (rightEdge > maxRight)
{
maxRight = rightEdge;
}
}
maxRight += marginRight;
if (maxRight < 2560f)
{
maxRight = 2560f;
}
float lowY = float.MaxValue;
foreach (var block in storyBlocks)
{
float bottomEdge = block.blockRect.anchoredPosition.y - block.blockRect.sizeDelta.y * 0.5f;
if (bottomEdge < lowY)
{
lowY = bottomEdge;
}
}
float maxHeight = Mathf.Abs(lowY) + marginTop + marginBottom;
if (maxHeight < 1440f)
{
maxHeight = 1440f;
}
content.sizeDelta = new Vector2(maxRight, maxHeight);
//connectionContainer.sizeDelta = new Vector2(maxRight, maxHeight);
}
}
public partial class Storyline
{
public void SetUpStoryline(string chapterIndex)
{
if (GameSaveManager.instance.StorySaveModule.IsNewStoryline(chapterIndex))
{
ResetStory(chapterIndex);
return;
}
GameSaveManager.instance.StorySaveModule.LoadStoryline(chapterIndex);
foreach (var blockSave in GameSaveManager.instance.StorySaveModule.tutorialBlockSaves[chapterIndex])
{
GenerateTutorialBlock(blockSave.blockName, blockSave.position, blockSave.state);
}
foreach (var blockSave in GameSaveManager.instance.StorySaveModule.songBlockSaves[chapterIndex])
{
GenerateSongBlock(blockSave.blockName, blockSave.position, blockSave.state);
}
foreach (var blockSave in GameSaveManager.instance.StorySaveModule.dialogBlockSaves[chapterIndex])
{
GenerateDialogBlock(blockSave.blockName, blockSave.position, blockSave.state);
}
foreach (var connectorSave in GameSaveManager.instance.StorySaveModule.connectorSaves[chapterIndex])
{
GenerateConnector(connectorSave.startBlockName, connectorSave.endBlockName);
}
}
[Button]
public void SaveStoryline(string chapterName)
{
List<TutorialBlockSave> tutorialBlockSaves =
tutorialBlocks.Select(block => block.GetBlockSave() as TutorialBlockSave).ToList();
List<SongBlockSave> songBlockSaves =
songBlocks.Select(block => block.GetBlockSave() as SongBlockSave).ToList();
List<DialogBlockSave> dialogBlockSaves =
dialogBlocks.Select(block => block.GetBlockSave() as DialogBlockSave).ToList();
List<BlockConnectorSave> connectorSaves =
connectors.Select(connector => new BlockConnectorSave(connector.startBlock.blockName, connector.endBlock.blockName)).ToList();
GameSaveManager.instance.StorySaveModule.SaveStoryline(
chapterName, tutorialBlockSaves, songBlockSaves, dialogBlockSaves, connectorSaves);
}
[Button]
public void ResetStory(string chapterName)
{
ClearStoryline();
StoryData storyData = StoryManager.instance.storyDatas[chapterName];
List<InitialBlockData> initialBlocks = storyData.initialBlocks;
foreach (InitialBlockData blockData in initialBlocks)
{
storyData.GetDataByName(blockData.blockName, out Type dataType);
if (dataType == typeof(TutorialBlockData))
{
GenerateTutorialBlock(blockData.blockName, blockData.blockPosition, blockData.initialState);
}
else if (dataType == typeof(DialogBlockData))
{
GenerateDialogBlock(blockData.blockName, blockData.blockPosition, blockData.initialState);
}
else if (dataType == typeof(SongBlockData))
{
GenerateSongBlock(blockData.blockName, blockData.blockPosition, blockData.initialState);
}
}
foreach (InitialBlockData blockData in initialBlocks)
{
foreach (string nextBlockName in blockData.nextBlocks)
{
GenerateConnector(blockData.blockName, nextBlockName);
}
}
SetUpBackground();
SaveStoryline(chapterName);
}
}
}

View File

@@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 42d37c15aeeaf1d4abbdc13962bb8b70
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: