fix: 任务面板重构

This commit is contained in:
2026-07-26 18:07:34 +08:00
parent 0a1adf1558
commit 3cdedb155c
10 changed files with 386 additions and 94 deletions
-1
View File
@@ -56,7 +56,6 @@ namespace AibisDream.Utility
public const string CutLinePrefabName = "Prefab/CutLine";
public const string TaskItemName = "Prefab/TaskItem";
public const string ActorPrefabName = "Prefab/ActorAnima";
public const string SpriteActorPrefabName = "Prefab/SpriteActor";
@@ -102,7 +102,7 @@ namespace AibisDream.FixSystem
isStartupPrepared = true;
targetLightState = LightState.Normal;
ApplySystemOnImmediate(false);
taskPanel?.HidePanelImmediate();
taskPanel?.SetVisible(false, immediate: true);
}
/// <summary>
@@ -205,7 +205,7 @@ namespace AibisDream.FixSystem
if (taskPanel != null)
{
taskPanel.CaptureSnapshot(out dto.isTaskPanelVisible, dto.tasks);
taskPanel.CaptureSnapshot(out dto.isTaskPanelRequestedVisible, dto.tasks);
}
return dto;
@@ -235,7 +235,7 @@ namespace AibisDream.FixSystem
GetRepairPanel<CablePanel>()?.ApplyCableSnapshot(dto.isCableRetracted, dto.pluggedModuleName);
taskPanel?.ApplySnapshot(dto.isTaskPanelVisible, dto.tasks, immediate: true);
taskPanel?.ApplySnapshot(dto.isTaskPanelRequestedVisible, dto.tasks);
}
/// <summary>读档终态:跳过 StartAllSystems 动画,直接写面板壳层状态。</summary>
@@ -1,121 +1,153 @@
using System.Collections.Generic;
using AibisDream.Framework;
using System.Collections.Generic;
using AibisDream.SaveSystem;
using AibisDream.Utility;
using TMPro;
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;
namespace AibisDream.FixSystem
{
public class TaskPanel : MonoBehaviour, ILineView
{
private const float HiddenLocalX = -10.32f;
private const float ShownLocalX = -7.12f;
#region
[Header("面板位置")]
[SerializeField] private Transform taskRoot;
[SerializeField] private Transform taskListParent;
private GameObject _taskItemPrefab;
private readonly Dictionary<string, GameObject> _taskDict = new();
[SerializeField] private Transform hiddenAnchor;
[SerializeField] private Transform shownAnchor;
#endregion
[Header("任务列表")]
[SerializeField] private RectTransform taskListParent;
[SerializeField] private TaskItem taskItemPrefab;
[Header("动画")]
[SerializeField, Min(0f)] private float animationDuration = 1f;
[SerializeField] private Ease animationEase = Ease.OutQuad;
private readonly List<TaskEntry> _tasks = new();
private Tween _moveTween;
private bool _requestedVisible;
private bool _temporarilyHidden;
public bool RequestedVisible => _requestedVisible;
public bool EffectiveVisible => _requestedVisible && !_temporarilyHidden;
private sealed class TaskEntry
{
public string LineId;
public string Text;
public TaskItem View;
}
private void Awake()
{
_taskItemPrefab = ResourceKit.LoadAssetSync<GameObject>(ConstRef.TaskItemName);
ValidateConfiguration();
}
public bool IsPanelVisible => Mathf.Approximately(taskRoot.localPosition.x, ShownLocalX);
public void ShowPanel()
private void OnEnable()
{
taskRoot.DOLocalMoveX(ShownLocalX, 1f);
RefreshVisual(immediate: true);
}
/// <summary>动画收起面板(与 ShowPanel 对称)。</summary>
public void HidePanel()
private void OnDisable()
{
taskRoot.DOKill();
taskRoot.DOLocalMoveX(HiddenLocalX, 1f);
KillMoveTween();
}
public void HidePanelImmediate()
public void SetVisible(bool visible, bool immediate = false)
{
taskRoot.DOKill();
SetPanelVisibleImmediate(false);
if (_requestedVisible == visible)
{
if (immediate)
{
RefreshVisual(immediate: true);
}
return;
}
var wasEffectivelyVisible = EffectiveVisible;
_requestedVisible = visible;
if (wasEffectivelyVisible != EffectiveVisible || immediate)
{
RefreshVisual(immediate);
}
}
private bool _restoreVisibleAfterCollapse;
/// <summary>临时收起面板(如进入 Memory 模式),记录之前是否展开,供 RestoreCollapsedPanel 恢复。</summary>
public void CollapsePanelTemporary(bool immediate = false)
public void SetTemporarilyHidden(bool hidden, bool immediate = false)
{
_restoreVisibleAfterCollapse = IsPanelVisible;
if (!_restoreVisibleAfterCollapse) return;
if (immediate) HidePanelImmediate();
else HidePanel();
}
if (_temporarilyHidden == hidden)
{
if (immediate)
{
RefreshVisual(immediate: true);
}
/// <summary>按 CollapsePanelTemporary 记录的状态恢复面板(之前未展开则不做任何事)。</summary>
public void RestoreCollapsedPanel()
{
if (!_restoreVisibleAfterCollapse) return;
_restoreVisibleAfterCollapse = false;
ShowPanel();
return;
}
var wasEffectivelyVisible = EffectiveVisible;
_temporarilyHidden = hidden;
if (wasEffectivelyVisible != EffectiveVisible || immediate)
{
RefreshVisual(immediate);
}
}
public void CompleteTask(string lineId)
{
if (_taskDict.Remove(lineId, out var taskItem))
var index = FindTaskIndex(lineId);
if (index < 0)
{
Destroy(taskItem);
return;
}
DestroyTaskView(_tasks[index].View);
_tasks.RemoveAt(index);
MarkTaskLayoutDirty();
}
public void ClearTasks()
{
foreach (var task in _taskDict)
foreach (var task in _tasks)
{
Destroy(task.Value);
DestroyTaskView(task.View);
}
_taskDict.Clear();
_tasks.Clear();
MarkTaskLayoutDirty();
}
public void RunLine(LineSyncToken token)
{
if (_taskDict.ContainsKey(token.lineInfo.lineId))
var lineId = token.lineInfo.lineId;
if (string.IsNullOrWhiteSpace(lineId))
{
EnumEventSystem.Global.Send(DialogEventEnum.LineShown);
token.ForceAdvance();
return;
Debug.LogWarning($"[{nameof(TaskPanel)}] 忽略缺少 lineId 的任务文本。", this);
}
else if (FindTaskIndex(lineId) < 0)
{
AddTaskItem(lineId, token.lineInfo.lineText);
}
AddTaskItem(token.lineInfo.lineId, $"\u25cf {token.lineInfo.lineText}");
token.TextStart();
token.TextShown();
token.ForceAdvance();
CompleteLineImmediately(token);
}
public void CaptureSnapshot(out bool isPanelVisible, List<TaskEntrySnapshotDto> tasks)
public void CaptureSnapshot(out bool requestedVisible, List<TaskEntrySnapshotDto> tasks)
{
isPanelVisible = IsPanelVisible;
requestedVisible = RequestedVisible;
tasks.Clear();
foreach (var kv in _taskDict)
foreach (var task in _tasks)
{
var text = kv.Value.GetComponent<TMP_Text>()?.text ?? string.Empty;
tasks.Add(new TaskEntrySnapshotDto
{
lineId = kv.Key,
text = text
lineId = task.LineId,
text = task.Text
});
}
}
public void ApplySnapshot(bool isPanelVisible, List<TaskEntrySnapshotDto> tasks, bool immediate)
public void ApplySnapshot(bool requestedVisible, List<TaskEntrySnapshotDto> tasks)
{
ClearTasks();
@@ -123,40 +155,208 @@ namespace AibisDream.FixSystem
{
foreach (var entry in tasks)
{
if (string.IsNullOrEmpty(entry.lineId))
if (entry == null || string.IsNullOrWhiteSpace(entry.lineId))
{
Debug.LogWarning($"[{nameof(TaskPanel)}] 跳过缺少 lineId 的任务快照。", this);
continue;
}
AddTaskItem(entry.lineId, entry.text);
if (FindTaskIndex(entry.lineId) >= 0)
{
Debug.LogWarning($"[{nameof(TaskPanel)}] 跳过重复任务快照 '{entry.lineId}'。", this);
continue;
}
AddTaskItem(entry.lineId, entry.text ?? string.Empty);
}
}
if (immediate)
SetVisible(requestedVisible, immediate: true);
}
private void RefreshVisual(bool immediate)
{
KillMoveTween();
var visible = EffectiveVisible;
if (visible)
{
taskRoot.DOKill();
SetPanelVisibleImmediate(isPanelVisible);
SetTaskTextVisible(true);
MarkTaskLayoutDirty();
}
else if (isPanelVisible)
if (taskRoot == null || hiddenAnchor == null || shownAnchor == null)
{
ShowPanel();
if (!visible)
{
SetTaskTextVisible(false);
}
return;
}
var targetPosition = visible
? shownAnchor.localPosition
: hiddenAnchor.localPosition;
if (immediate || animationDuration <= 0f)
{
taskRoot.localPosition = targetPosition;
if (!visible)
{
SetTaskTextVisible(false);
}
return;
}
var tween = taskRoot
.DOLocalMove(targetPosition, animationDuration)
.SetEase(animationEase);
_moveTween = tween;
tween.OnComplete(() =>
{
if (_moveTween != tween)
{
return;
}
_moveTween = null;
if (!EffectiveVisible)
{
SetTaskTextVisible(false);
}
});
}
private void AddTaskItem(string lineId, string text)
{
var taskItem = Instantiate(_taskItemPrefab, taskListParent);
var task = taskItem.GetComponent<TMP_Text>();
task.text = text;
_taskDict[lineId] = taskItem;
LayoutRebuilder.ForceRebuildLayoutImmediate(taskListParent.GetComponent<RectTransform>());
TaskItem view = null;
if (taskItemPrefab == null || taskListParent == null)
{
Debug.LogError(
$"[{nameof(TaskPanel)}] 无法创建任务 '{lineId}'TaskItem Prefab 或任务列表父节点未配置。",
this);
}
else
{
view = Instantiate(taskItemPrefab, taskListParent);
view.SetText(text);
}
_tasks.Add(new TaskEntry
{
LineId = lineId,
Text = text,
View = view
});
MarkTaskLayoutDirty();
}
private void SetPanelVisibleImmediate(bool visible)
private int FindTaskIndex(string lineId)
{
var pos = taskRoot.localPosition;
pos.x = visible ? ShownLocalX : HiddenLocalX;
taskRoot.localPosition = pos;
if (string.IsNullOrEmpty(lineId))
{
return -1;
}
for (var i = 0; i < _tasks.Count; i++)
{
if (_tasks[i].LineId == lineId)
{
return i;
}
}
return -1;
}
private static void CompleteLineImmediately(LineSyncToken token)
{
token.TextStart();
token.TextShown();
token.ForceAdvance();
}
private static void DestroyTaskView(TaskItem view)
{
if (view == null)
{
return;
}
view.gameObject.SetActive(false);
Destroy(view.gameObject);
}
private void SetTaskTextVisible(bool visible)
{
if (taskListParent != null && taskListParent.gameObject.activeSelf != visible)
{
taskListParent.gameObject.SetActive(visible);
}
}
private void MarkTaskLayoutDirty()
{
if (taskListParent != null)
{
LayoutRebuilder.MarkLayoutForRebuild(taskListParent);
}
}
private void KillMoveTween()
{
if (_moveTween == null)
{
return;
}
var tween = _moveTween;
_moveTween = null;
tween.Kill();
}
private void ValidateConfiguration()
{
if (taskRoot == null)
{
Debug.LogError($"[{nameof(TaskPanel)}] {name} 未配置 Task Root。", this);
}
if (hiddenAnchor == null)
{
Debug.LogError($"[{nameof(TaskPanel)}] {name} 未配置 Hidden Anchor。", this);
}
if (shownAnchor == null)
{
Debug.LogError($"[{nameof(TaskPanel)}] {name} 未配置 Shown Anchor。", this);
}
if (taskRoot != null
&& hiddenAnchor != null
&& shownAnchor != null
&& (taskRoot.parent != hiddenAnchor.parent || taskRoot.parent != shownAnchor.parent))
{
Debug.LogError($"[{nameof(TaskPanel)}] {name} 的 Task Root 与两个 Anchor 必须拥有同一个父节点。", this);
}
if (taskListParent == null)
{
Debug.LogError($"[{nameof(TaskPanel)}] {name} 未配置任务列表父节点。", this);
}
if (taskItemPrefab == null)
{
Debug.LogError($"[{nameof(TaskPanel)}] {name} 未配置 TaskItem Prefab。", this);
}
else if (!taskItemPrefab.IsConfigured)
{
Debug.LogError($"[{nameof(TaskPanel)}] {name} 的 TaskItem Prefab 未绑定文本组件。", this);
}
}
}
}
@@ -37,7 +37,7 @@ namespace AibisDream
[YarnCommand("show_task_panel")]
public static void ShowTaskPanel()
{
TaskPanel.ShowPanel();
TaskPanel?.SetVisible(true);
}
#endregion
@@ -1,13 +1,23 @@
using TMPro;
using UnityEngine;
namespace AibisDream
namespace AibisDream.FixSystem
{
public class TaskItem : MonoBehaviour
{
[SerializeField] private TMP_Text label;
public bool IsConfigured => label != null;
public void SetText(string value)
{
GetComponent<TMP_Text>().text = $"\u25cf {value}";
if (label == null)
{
Debug.LogError($"[{nameof(TaskItem)}] {name} 未绑定文本组件。", this);
return;
}
label.text = $"\u25cf {value}";
}
}
}
@@ -106,7 +106,7 @@ namespace AibisDream.FixSystem
float duration = 0.5f;
var memorySystem = FixSystemCenter.SystemDic.Get<MemoryProcess>();
// 进入Memory前收起Task面板
FixPanelSystem.Instance.TaskPanel?.CollapsePanelTemporary();
FixPanelSystem.Instance.TaskPanel?.SetTemporarilyHidden(true);
// 先切BodyModule
if (!CameraKit.Instance.EqualToCameraState(CameraEnum.BodyModule))
{
@@ -126,7 +126,7 @@ namespace AibisDream.FixSystem
{
var memorySystem = FixSystemCenter.SystemDic.Get<MemoryProcess>();
FixPanelSystem.Instance.TaskPanel?.CollapsePanelTemporary(immediate: true);
FixPanelSystem.Instance.TaskPanel?.SetTemporarilyHidden(true, immediate: true);
yield return memorySystem.DropMemoryProjector();
FixPanelSystem.GetRepairPanel<CablePanel>()?.PullUpCable();
yield return CameraKit.Instance.SwitchCamera(CameraEnum.Memory, true);
@@ -145,7 +145,7 @@ namespace AibisDream.FixSystem
yield return CameraKit.Instance.SwitchCamera(CameraEnum.BodyModule);
memorySystem.PullMemoryProjector();
// 退出Memory后恢复Task面板
FixPanelSystem.Instance.TaskPanel?.RestoreCollapsedPanel();
FixPanelSystem.Instance.TaskPanel?.SetTemporarilyHidden(false);
}
}
}
+3 -3
View File
@@ -146,7 +146,7 @@ Postflight 校验
Provider 使用 `ISyncSnapshotProvider``IAsyncSnapshotProvider` 显式声明同步/异步恢复;异步 Provider 必须把等待过程返回给编排层,禁止内部 fire-and-forget。
## 快照 JSON 结构(schemaVersion = 1
## 快照 JSON 结构(schemaVersion = 2
| 字段 | 含义 |
| --- | --- |
@@ -191,11 +191,11 @@ Fix 场景各 State 的 `Enter()` 通常包含相机过渡、Timeline 播放、F
| --- | --- | --- | --- |
| `punchTape` | `PunchTapeSnapshotDto` | 63 | 打孔带收藏 |
| `fix` | `FixSnapshotDto` | 65 | FixSceneDirector 宏观模式(state + args |
| `fixPanel` | `FixPanelSnapshotDto` | 66 | FixPanel 壳层 + 线缆/插头 + TaskPanel(展开状态与任务列表) |
| `fixPanel` | `FixPanelSnapshotDto` | 66 | FixPanel 壳层 + 线缆/插头 + TaskPanel请求展开状态与任务列表) |
| `bodyModule` | `BodyModuleSnapshotDto` | 67 | 插线模块物理态 |
| `eye` | `EyeSnapshotDto` | 68 | Eye 叙事阶段 |
**还原顺序**punchTape → fixcue)→ fixPanel → bodyModule → eye → showcase → day2SleepPresentation → playTool → screen。`fixPanel` 须在 `fix` 之后,以覆盖 Cue `EnterImmediate` 中的 `ResetPlug`;D2 动态表现须在通用 Showcase 图片之后恢复,才能叠加虚焦、缩放或场景专属动画。
**还原顺序**punchTape → fixcue)→ fixPanel → bodyModule → eye → showcase → day2SleepPresentation → playTool → screen。`fixPanel` 须在 `fix` 之后,以覆盖 Cue `EnterImmediate` 中的 `ResetPlug`,并让 Memory Cue 先建立 TaskPanel 临时隐藏状态;D2 动态表现须在通用 Showcase 图片之后恢复,才能叠加虚焦、缩放或场景专属动画。
**维修场景门控**`FixSceneSnapshotHelper`):上述 section 仅在 `FixSystemCenter.Instance != null` 时 Capture/Restore。
+4 -4
View File
@@ -9,7 +9,7 @@ namespace AibisDream.SaveSystem
/// </summary>
public static class SaveSnapshotSchema
{
public const int CurrentVersion = 1;
public const int CurrentVersion = 2;
}
/// <summary>
@@ -269,7 +269,7 @@ namespace AibisDream.SaveSystem
/// <summary>Yarn 行 id,如 <c>line:01dade5</c>。</summary>
public string lineId;
/// <summary>存档时 UI 显示的完整文本(含 <c>● </c> 前缀。</summary>
/// <summary>存档时已完成 substitutions 的本地化文本,不含 UI 圆点前缀。</summary>
public string text;
}
@@ -290,8 +290,8 @@ namespace AibisDream.SaveSystem
/// <summary>插头插入的 BodyModule 名(data.moduleName);null/空 = 未插入。</summary>
public string pluggedModuleName;
/// <summary>TaskPanel 是否处于展开位(<c>show_task_panel</c> 后)。</summary>
public bool isTaskPanelVisible;
/// <summary>剧情是否要求 TaskPanel 展开;Memory 等临时隐藏状态不写入存档。</summary>
public bool isTaskPanelRequestedVisible;
/// <summary>TaskPanel 当前未完成任务列表(保持插入顺序)。</summary>
public List<TaskEntrySnapshotDto> tasks = new();