调整 Directive 逻辑并添加章节之间的承接功能

This commit is contained in:
SepComet 2026-04-24 14:15:40 +08:00
parent facfe7a437
commit d557153e28
18 changed files with 164999 additions and 163916 deletions

4
.gitignore vendored
View File

@ -100,4 +100,6 @@ crashlytics-build.properties
/.omc
/.omx
/.dotnet_home
/.dotnet
/.dotnet
/.claude
/.codex

View File

@ -1 +0,0 @@
{"session_id":"980cf702-c5aa-462f-a54f-9cf7f5a5882d","transcript_path":"C:\\Users\\September\\.claude\\projects\\D--Learn-GameLearn-UnityProjects-Biography-of-Li-Jian\\980cf702-c5aa-462f-a54f-9cf7f5a5882d.jsonl","cwd":"D:\\Learn\\GameLearn\\UnityProjects\\Biography of Li Jian","model":{"id":"MiniMax-M2.7-highspeed","display_name":"MiniMax-M2.7-highspeed"},"workspace":{"current_dir":"D:\\Learn\\GameLearn\\UnityProjects\\Biography of Li Jian","project_dir":"D:\\Learn\\GameLearn\\UnityProjects\\Biography of Li Jian","added_dirs":[]},"version":"2.1.114","output_style":{"name":"default"},"cost":{"total_cost_usd":0.45526774999999997,"total_duration_ms":743042,"total_api_duration_ms":19399,"total_lines_added":0,"total_lines_removed":0},"context_window":{"total_input_tokens":31113,"total_output_tokens":1126,"context_window_size":200000,"current_usage":{"input_tokens":17274,"output_tokens":521,"cache_creation_input_tokens":0,"cache_read_input_tokens":26409},"used_percentage":22,"remaining_percentage":78},"exceeds_200k_tokens":false}

View File

@ -1,6 +0,0 @@
{
"timestamp": "2026-04-18T09:34:00.712Z",
"backgroundTasks": [],
"sessionStartTimestamp": "2026-04-18T09:33:32.703Z",
"sessionId": "1b0f5c6b-460e-48ef-9f2b-eb516bfbee3e"
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,8 @@
fileFormatVersion: 2
guid: 99d811b0183246646a2ce8df996f4bca
guid: 98380886ef3ddab4e913ead8133a2e42
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 0
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -10,7 +10,7 @@ MonoBehaviour:
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 71c1514a6bd24e1e882cebbe1904ce04, type: 3}
m_Name: simfang SDF
m_Name: simfangTMP
m_EditorClassIdentifier:
hashCode: 1985928552
material: {fileID: 7425184593944002063}

View File

@ -55,6 +55,15 @@ namespace CustomComponent
private int _nextChapterTitleRequestId = 1;
private bool _chapterTitleShownAfterInit;
[Header("章节标题过场")]
[SerializeField] [Min(0f)] private float _chapterTitleDimFadeInDuration = 0.2f;
[SerializeField] [Min(0f)] private float _chapterTitlePreBlackDuration = 0.25f;
[SerializeField] [Min(0f)] private float _chapterTitleTitleFadeInDelay = 0f;
[SerializeField] [Min(0f)] private float _chapterTitleFadeInDuration = 0.3f;
[SerializeField] [Min(0f)] private float _chapterTitleHoldDuration = 1.2f;
[SerializeField] [Min(0f)] private float _chapterTitleFadeOutDuration = 0.3f;
[SerializeField] [Min(0f)] private float _chapterTitleDimFadeOutDuration = 0.2f;
private int _currentChapterId;
private int _currentLineIndex = -1;
private bool _isInitialized;
@ -572,7 +581,14 @@ namespace CustomComponent
ChapterId = _currentChapterId,
RequestId = requestId,
Title = BuildChapterTitle(_currentChapterId),
Subtitle = subtitle
Subtitle = subtitle,
DimFadeInDuration = _chapterTitleDimFadeInDuration,
PreBlackDuration = _chapterTitlePreBlackDuration,
TitleFadeInDelay = _chapterTitleTitleFadeInDelay,
TitleFadeInDuration = _chapterTitleFadeInDuration,
HoldDuration = _chapterTitleHoldDuration,
TitleFadeOutDuration = _chapterTitleFadeOutDuration,
DimFadeOutDuration = _chapterTitleDimFadeOutDuration
};
int? formSerialId = GameEntry.UI.OpenUIForm(UIFormId.ChapterTitleForm, context);

View File

@ -1,14 +1,22 @@
using System.Collections;
using System.Collections.Generic;
using DataTable;
using Event;
using GameFramework.DataTable;
using GameFramework.Event;
using GameFramework.Fsm;
using GameFramework.Procedure;
using UnityEngine;
using UI;
using UnityGameFramework.Runtime;
namespace Procedure
{
public class ProcedureMain : ProcedureBase
{
private const int InitialChapterId = 1;
private const int InitialDialogId = 1001;
private const int DialogChapterDivisor = 1000;
private IDataTable<DRDialog> _dtDialog;
public override bool UseNativeDialog => false;
protected override void OnEnter(IFsm<IProcedureManager> procedureOwner)
@ -16,8 +24,104 @@ namespace Procedure
base.OnEnter(procedureOwner);
AIChatEntryRuntime.EnsureOpen();
GameEntry.Dialog.Init(1);
GameEntry.Dialog.StartDialog(1001);
GameEntry.Event.Subscribe(StoryChapterEndedEventArgs.EventId, OnStoryChapterEnded);
GameEntry.Dialog.Init(InitialChapterId);
GameEntry.Dialog.StartDialog(InitialDialogId);
}
protected override void OnLeave(IFsm<IProcedureManager> procedureOwner, bool isShutdown)
{
GameEntry.Event.Unsubscribe(StoryChapterEndedEventArgs.EventId, OnStoryChapterEnded);
base.OnLeave(procedureOwner, isShutdown);
}
private void OnStoryChapterEnded(object sender, GameEventArgs e)
{
if (!(e is StoryChapterEndedEventArgs args))
{
return;
}
int currentChapterId = args.ChapterId > 0
? args.ChapterId
: (GameEntry.Dialog != null ? GameEntry.Dialog.CurrentChapterId : 0);
int nextChapterId = currentChapterId + 1;
if (nextChapterId <= 0)
{
return;
}
if (!TryResolveFirstDialogId(nextChapterId, out int nextDialogId))
{
Log.Info("ProcedureMain chapter switch stopped. No dialog found for chapter '{0}'.",
nextChapterId.ToString());
return;
}
if (GameEntry.Dialog == null)
{
Log.Warning("ProcedureMain chapter switch failed. Dialog component is missing.");
return;
}
if (!GameEntry.Dialog.Init(nextChapterId))
{
Log.Warning("ProcedureMain chapter switch failed. Init chapter '{0}' failed.",
nextChapterId.ToString());
return;
}
if (!GameEntry.Dialog.StartDialog(nextDialogId))
{
Log.Warning("ProcedureMain chapter switch failed. Start dialog '{0}' failed.",
nextDialogId.ToString());
}
}
private bool TryResolveFirstDialogId(int chapterId, out int dialogId)
{
dialogId = 0;
if (chapterId <= 0)
{
return false;
}
if (_dtDialog == null)
{
_dtDialog = GameEntry.DataTable.GetDataTable<DRDialog>();
}
if (_dtDialog == null)
{
Log.Warning("ProcedureMain chapter switch failed. Data table DRDialog is missing.");
return false;
}
DRDialog[] dialogRows = _dtDialog.GetDataRows((a, b) => a.Id.CompareTo(b.Id));
for (int i = 0; i < dialogRows.Length; i++)
{
DRDialog row = dialogRows[i];
if (row == null)
{
continue;
}
if (ParseChapterIdFromDialogId(row.Id) != chapterId)
{
continue;
}
dialogId = row.Id;
return dialogId > 0;
}
return false;
}
private static int ParseChapterIdFromDialogId(int dialogId)
{
return dialogId / DialogChapterDivisor;
}
}
}

View File

@ -6,5 +6,14 @@ namespace UI
public int RequestId = 0;
public string Title = string.Empty;
public string Subtitle = string.Empty;
// < 0 表示使用 ChapterTitleForm 上的默认配置。
public float DimFadeInDuration = -1f;
public float PreBlackDuration = -1f;
public float TitleFadeInDelay = -1f;
public float TitleFadeInDuration = -1f;
public float HoldDuration = -1f;
public float TitleFadeOutDuration = -1f;
public float DimFadeOutDuration = -1f;
}
}

View File

@ -10,23 +10,23 @@ namespace UI
public class AIChatForm : UGuiForm
{
[SerializeField] private TMP_Text _returnButtonText;
[SerializeField] private TMP_InputField _inputField;
[SerializeField] private RectTransform _dialogContent;
[SerializeField] private TMP_Text _titleText;
[SerializeField] private ScrollRect _historyScrollRect;
[SerializeField] private AIDialogItem _aiDialogItemPrefab;
[SerializeField] private PlayerDialogItem _playerDialogItemPrefab;
[SerializeField] private HorizonSelectGroup _languageSelectGroup;
[SerializeField] private string _returnButtonNormalText = "<sprite name=\"KEYBOARD_Esc\"> Back";
[SerializeField] private string _returnButtonHoverText = "<sprite name=\"KEYBOARD_Esc\"><u> Back </u>";
private readonly List<Component> _dialogItems = new List<Component>();

View File

@ -13,14 +13,25 @@ namespace UI
[SerializeField] private Image _dimImage;
[SerializeField] private TMP_Text _titleText;
[SerializeField] private TMP_Text _subtitleText;
[SerializeField] private float _dimFadeInDuration = 0.2f;
[SerializeField] private float _preBlackDuration = 0.25f;
[SerializeField] private float _titleFadeInDelay = 0f;
[SerializeField] private float _fadeInDuration = 0.3f;
[SerializeField] private float _holdDuration = 1.2f;
[SerializeField] private float _fadeOutDuration = 0.3f;
[SerializeField] private float _dimFadeOutDuration = 0.2f;
private Coroutine _playCoroutine;
private int _requestId;
private int _chapterId;
private bool _hasPublishedCompletion;
private float _runtimeDimFadeInDuration;
private float _runtimePreBlackDuration;
private float _runtimeTitleFadeInDelay;
private float _runtimeTitleFadeInDuration;
private float _runtimeHoldDuration;
private float _runtimeTitleFadeOutDuration;
private float _runtimeDimFadeOutDuration;
protected override void OnOpen(object userData)
{
@ -33,7 +44,8 @@ namespace UI
protected override void OnClose(bool isShutdown, object userData)
{
StopPlay();
SetContentAlpha(0f);
SetDimAlpha(0f);
SetTitleAlpha(0f);
if (!isShutdown)
{
PublishCompletionIfNeeded();
@ -55,6 +67,11 @@ namespace UI
_contentGroup = CreateContentGroup(rootRect);
}
if (_contentGroup != null)
{
_contentGroup.alpha = 1f;
}
RectTransform contentRect = _contentGroup != null && _contentGroup.transform is RectTransform contentTransform
? contentTransform
: rootRect;
@ -105,6 +122,7 @@ namespace UI
private void ApplyContext(ChapterTitleFormContext context)
{
_hasPublishedCompletion = false;
ResetRuntimeTimings(context);
if (context == null)
{
@ -157,10 +175,17 @@ namespace UI
private IEnumerator PlayRoutine()
{
SetContentAlpha(0f);
yield return FadeAlpha(0f, 1f, _fadeInDuration);
yield return WaitUnscaled(_holdDuration);
yield return FadeAlpha(1f, 0f, _fadeOutDuration);
SetDimAlpha(0f);
SetTitleAlpha(0f);
yield return FadeDimAlpha(0f, 1f, _runtimeDimFadeInDuration);
yield return WaitUnscaled(_runtimePreBlackDuration);
yield return WaitUnscaled(_runtimeTitleFadeInDelay);
yield return FadeTitleAlpha(0f, 1f, _runtimeTitleFadeInDuration);
yield return WaitUnscaled(_runtimeHoldDuration);
yield return FadeTitleAlpha(1f, 0f, _runtimeTitleFadeOutDuration);
yield return FadeDimAlpha(1f, 0f, _runtimeDimFadeOutDuration);
_playCoroutine = null;
PublishCompletionIfNeeded();
@ -170,12 +195,12 @@ namespace UI
}
}
private IEnumerator FadeAlpha(float from, float to, float duration)
private IEnumerator FadeDimAlpha(float from, float to, float duration)
{
float safeDuration = Mathf.Max(0f, duration);
if (safeDuration <= 0f)
{
SetContentAlpha(to);
SetDimAlpha(to);
yield break;
}
@ -184,11 +209,32 @@ namespace UI
{
elapsed += Time.unscaledDeltaTime;
float t = Mathf.Clamp01(elapsed / safeDuration);
SetContentAlpha(Mathf.Lerp(from, to, t));
SetDimAlpha(Mathf.Lerp(from, to, t));
yield return null;
}
SetContentAlpha(to);
SetDimAlpha(to);
}
private IEnumerator FadeTitleAlpha(float from, float to, float duration)
{
float safeDuration = Mathf.Max(0f, duration);
if (safeDuration <= 0f)
{
SetTitleAlpha(to);
yield break;
}
float elapsed = 0f;
while (elapsed < safeDuration)
{
elapsed += Time.unscaledDeltaTime;
float t = Mathf.Clamp01(elapsed / safeDuration);
SetTitleAlpha(Mathf.Lerp(from, to, t));
yield return null;
}
SetTitleAlpha(to);
}
private static IEnumerator WaitUnscaled(float duration)
@ -207,14 +253,57 @@ namespace UI
}
}
private void SetContentAlpha(float alpha)
private void SetDimAlpha(float alpha)
{
if (_contentGroup == null)
if (_dimImage == null)
{
return;
}
_contentGroup.alpha = Mathf.Clamp01(alpha);
Color color = _dimImage.color;
color.a = Mathf.Clamp01(alpha);
_dimImage.color = color;
}
private void SetTitleAlpha(float alpha)
{
float clampedAlpha = Mathf.Clamp01(alpha);
SetTextAlpha(_titleText, clampedAlpha);
SetTextAlpha(_subtitleText, clampedAlpha);
}
private static void SetTextAlpha(TMP_Text text, float alpha)
{
if (text == null)
{
return;
}
Color color = text.color;
color.a = alpha;
text.color = color;
}
private void ResetRuntimeTimings(ChapterTitleFormContext context)
{
_runtimeDimFadeInDuration = ResolveDuration(context != null ? context.DimFadeInDuration : -1f,
_dimFadeInDuration);
_runtimePreBlackDuration = ResolveDuration(context != null ? context.PreBlackDuration : -1f,
_preBlackDuration);
_runtimeTitleFadeInDelay = ResolveDuration(context != null ? context.TitleFadeInDelay : -1f,
_titleFadeInDelay);
_runtimeTitleFadeInDuration = ResolveDuration(context != null ? context.TitleFadeInDuration : -1f,
_fadeInDuration);
_runtimeHoldDuration = ResolveDuration(context != null ? context.HoldDuration : -1f, _holdDuration);
_runtimeTitleFadeOutDuration = ResolveDuration(context != null ? context.TitleFadeOutDuration : -1f,
_fadeOutDuration);
_runtimeDimFadeOutDuration = ResolveDuration(context != null ? context.DimFadeOutDuration : -1f,
_dimFadeOutDuration);
}
private static float ResolveDuration(float overrideDuration, float fallbackDuration)
{
return overrideDuration >= 0f ? overrideDuration : Mathf.Max(0f, fallbackDuration);
}
private void PublishCompletionIfNeeded()

View File

@ -14,5 +14,5 @@ MonoBehaviour:
m_EditorClassIdentifier:
_enabled: 1
_triggerType: 0
_triggerId: 1005
_chapterId: 2
_triggerId: 1006
_chapterId: 1

View File

@ -15,4 +15,4 @@ MonoBehaviour:
_enabled: 1
_triggerType: 0
_triggerId: 2006
_chapterId: 3
_chapterId: 2

View File

@ -15,4 +15,4 @@ MonoBehaviour:
_enabled: 1
_triggerType: 0
_triggerId: 3005
_chapterId: 4
_chapterId: 3

View File

@ -15,4 +15,4 @@ MonoBehaviour:
_enabled: 1
_triggerType: 0
_triggerId: 4004
_chapterId: 0
_chapterId: 4

View File

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 77968dfed7b61334db617d490bb05477
guid: 0d041269ac2f63d47bc5f189d90dc73d
TextureImporter:
internalIDToNameTable: []
externalObjects: {}

20
openspec/config.yaml Normal file
View File

@ -0,0 +1,20 @@
schema: spec-driven
# Project context (optional)
# This is shown to AI when creating artifacts.
# Add your tech stack, conventions, style guides, domain knowledge, etc.
# Example:
# context: |
# Tech stack: TypeScript, React, Node.js
# We use conventional commits
# Domain: e-commerce platform
# Per-artifact rules (optional)
# Add custom rules for specific artifacts.
# Example:
# rules:
# proposal:
# - Keep proposals under 500 words
# - Always include a "Non-goals" section
# tasks:
# - Break tasks into chunks of max 2 hours