剧情+对话完善

This commit is contained in:
SoulliesOfficial
2026-07-21 15:24:42 -04:00
parent 8f230831e9
commit 810d019619
161 changed files with 7271 additions and 1893 deletions

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Ichni.Menu.UI;
using TMPro;
using UnityEngine;
using UnityEngine.Localization;
@@ -44,14 +45,18 @@ namespace Ichni.Story.UI
private sealed class RuntimeMarker
{
public StoryTimelineMarkerDefinition definition;
public StoryBlockDefinition anchorDefinition;
public StoryBlockView anchorView;
public StoryTimelineMarkerView view;
}
private readonly List<RuntimeMarker> _markers = new List<RuntimeMarker>();
private StoryTreeController _treeController;
private ScrollRect _subscribedScrollRect;
// StoryPage 在进入对话时会被 FadeOut 并暂时 SetActive(false)。
// 因此需要将“应监听的 ScrollRect”与“当前是否已挂接监听”分开保存
// OnDisable 只解除事件OnEnable 再恢复同一个目标的事件,不能丢失该引用。
private ScrollRect _storyScrollRect;
private bool _isScrollRectSubscribed;
// 重复刻度的初始视觉状态。它们只在当前章节 Timeline 存在期间有效,
// Clear 时会恢复,避免重复进入章节后不断累积额外宽度。
@@ -64,12 +69,17 @@ namespace Ichni.Story.UI
private void OnEnable()
{
LocalizationSettings.SelectedLocaleChanged += OnSelectedLocaleChanged;
SubscribeToStoryScrollRect();
// StoryPage 从对话界面返回后Canvas 与 StoryTree 会在同一帧重新启用。
// 立即按锚定 Block 的当前实际位置刷新一次,避免首帧沿用淡出前的坐标。
RefreshMarkerPositions();
}
private void OnDisable()
{
LocalizationSettings.SelectedLocaleChanged -= OnSelectedLocaleChanged;
SubscribeScrollRect(null);
UnsubscribeFromStoryScrollRect();
}
/// <summary>
@@ -80,7 +90,7 @@ namespace Ichni.Story.UI
{
Clear();
_treeController = treeController;
SubscribeScrollRect(storyScrollRect);
SetStoryScrollRect(storyScrollRect);
if (markerContainer == null || markerPrefab == null || storyData == null)
{
@@ -121,6 +131,7 @@ namespace Ichni.Story.UI
_markers.Clear();
_treeController = null;
SetStoryScrollRect(null);
if (progressText != null)
{
@@ -142,6 +153,8 @@ namespace Ichni.Story.UI
StoryBlockState state = marker.anchorView.state;
bool visible = state == StoryBlockState.Current || state == StoryBlockState.Completed;
marker.view.SetVisible(visible);
marker.view.SetInteractable(visible && _treeController != null &&
_treeController.HasRollbackSnapshot(marker.definition.markerId));
if (state == StoryBlockState.Completed && marker.definition.contributesToMainProgress)
{
@@ -344,16 +357,47 @@ namespace Ichni.Story.UI
view.name = $"StoryTimelineMarker_{marker.markerId}";
view.SetLabel(GetLocalizedLabel(marker.labelKey));
_markers.Add(new RuntimeMarker
RuntimeMarker runtimeMarker = new RuntimeMarker
{
definition = marker,
anchorDefinition = anchorDefinition,
anchorView = anchorView,
view = view
});
};
view.BindClick(() => RequestRollback(runtimeMarker));
_markers.Add(runtimeMarker);
return true;
}
/// <summary>
/// 点击可回滚 Marker 后显示通用 SelectionBox。实际恢复由 StoryTreeController 处理,
/// 因而 Timeline 只承担 UI 入口,不保存任何独立剧情状态。
/// </summary>
private void RequestRollback(RuntimeMarker marker)
{
if (marker == null || _treeController == null ||
!_treeController.HasRollbackSnapshot(marker.definition.markerId))
{
return;
}
MessageUIPage messageUIPage = MessageUIPage.instance;
if (messageUIPage == null)
{
Debug.LogWarning("[StoryTimeline] MessageUIPage 未就绪,无法显示剧情回滚确认框。");
return;
}
string markerLabel = GetLocalizedLabel(marker.definition.labelKey);
messageUIPage.ShowSelectionBox(
"重开本章节",
$"将重置“{markerLabel}”所在列及其之后的剧情选择与进度。歌曲成绩、解锁内容、设置和其它章节不会受到影响。",
new List<SelectionBoxOption>
{
new SelectionBoxOption("重开此时间点", () => _treeController.TryRollbackToMarker(marker.definition.markerId)),
new SelectionBoxOption("取消", () => { })
});
}
/// <summary>
/// 使用 StoryTimeline 指定的 Unity Localization String Table 解析 Label Key。
/// String Table 或条目尚未配置时回退显示 Key确保开发阶段仍能从 UI 上定位缺失翻译。
@@ -385,23 +429,54 @@ namespace Ichni.Story.UI
}
}
private void SubscribeScrollRect(ScrollRect scrollRect)
/// <summary>
/// 设置当前章节 StoryTree 使用的 ScrollRect。
/// 该引用在 Timeline 暂时禁用期间仍会保留,以便 StoryPage 恢复显示时重新建立位置同步。
/// </summary>
private void SetStoryScrollRect(ScrollRect scrollRect)
{
if (_subscribedScrollRect == scrollRect)
if (_storyScrollRect == scrollRect)
{
return;
}
if (_subscribedScrollRect != null)
UnsubscribeFromStoryScrollRect();
_storyScrollRect = scrollRect;
SubscribeToStoryScrollRect();
}
/// <summary>
/// 当 Timeline 处于启用状态时挂接 StoryTree 的拖动回调。
/// 使用独立标志防止 OnEnable、Build 等生命周期入口重复注册同一个监听。
/// </summary>
private void SubscribeToStoryScrollRect()
{
if (_isScrollRectSubscribed || !isActiveAndEnabled || _storyScrollRect == null)
{
_subscribedScrollRect.onValueChanged.RemoveListener(OnStoryTreeScrolled);
return;
}
_subscribedScrollRect = scrollRect;
if (_subscribedScrollRect != null)
_storyScrollRect.onValueChanged.AddListener(OnStoryTreeScrolled);
_isScrollRectSubscribed = true;
}
/// <summary>
/// 仅移除运行时事件监听,不清空 <see cref="_storyScrollRect"/>。
/// 对话期间 StoryPage 被临时隐藏后OnEnable 可据此自动恢复 Marker 与 Block 的同步。
/// </summary>
private void UnsubscribeFromStoryScrollRect()
{
if (!_isScrollRectSubscribed)
{
_subscribedScrollRect.onValueChanged.AddListener(OnStoryTreeScrolled);
return;
}
if (_storyScrollRect != null)
{
_storyScrollRect.onValueChanged.RemoveListener(OnStoryTreeScrolled);
}
_isScrollRectSubscribed = false;
}
private void OnStoryTreeScrolled(Vector2 _)