Merge branch 'develop' into feature/增加结尾保存节点

This commit is contained in:
2026-07-26 20:00:11 +08:00
67 changed files with 12565 additions and 3734 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";
@@ -20,6 +20,9 @@ public class HeatMapController : MonoBehaviour
public RectTransform imageToMove; // 需要移动的Image
public Transform imageToRotate; // 需要旋转的Image
[Header("拖拽")]
[Tooltip("在不改变视觉和布局的前提下,向左右/上下扩展拖拽命中范围。")]
[SerializeField] private Vector2 dragHitAreaExpansion = new Vector2(5.2f, 0.3f);
[SerializeField, Min(0f)] private float fanStopDuration = 0.15f;
[Tooltip("相对 Sprite 矩形中心的额外旋转轴偏移(本地空间,单位与 Transform 一致)。用于校准扇毂不在图心时的偏心。")]
[SerializeField] private Vector2 fanPivotLocalOffset;
@@ -227,10 +230,31 @@ public class HeatMapController : MonoBehaviour
void Start()
{
bodyModuleSystem = FindObjectOfType<BodyModuleSystem>();
ApplyDragHitArea();
EnsureFanPivot();
SetDataToMaterial();
}
private void ApplyDragHitArea()
{
if (heatmapGameobject == null)
return;
var dragGraphic = heatmapGameobject.GetComponent<Graphic>();
if (dragGraphic == null)
{
Debug.LogWarning("[HeatMapController] 拖拽对象缺少 Graphic,无法扩展拖拽命中范围。", heatmapGameobject);
return;
}
float horizontal = Mathf.Max(0f, dragHitAreaExpansion.x);
float vertical = Mathf.Max(0f, dragHitAreaExpansion.y);
// Graphic 使用负 raycastPadding 扩展命中区;不会改变 RectTransform
// 因而不会移动右上角按钮或拉伸热力图内容。
dragGraphic.raycastPadding = new Vector4(-horizontal, -vertical, -horizontal, -vertical);
}
void OnDestroy()
{
_temptureTween?.Kill();
@@ -21,6 +21,9 @@ namespace AibisDream.FixSystem
{
// 获取CablePanel引用
cablePanel = transform.GetComponentInParent<CablePanel>();
// 兼容尚未重新保存的旧场景/预制体:新增序列化字段缺失时仍使用设计默认值。
if (maxHalfAngle <= 0f) maxHalfAngle = 60f;
if (returnSpeed <= 0f) returnSpeed = 4f;
currentAngle = transform.eulerAngles.z;
// 获取SpriteRenderer并计算半径
@@ -62,10 +65,24 @@ namespace AibisDream.FixSystem
UpdateRotation(true);
}
/// <summary>
/// 供旧 CableSystem 使用:以插头位置驱动线盘,并在收线时缓动回正。
/// </summary>
public void UpdateRotation(Transform plugTransform, bool active)
{
UpdateRotation(plugTransform, active, 1f);
}
/// <summary>拖拽/插接中朝拉力方向施加扭矩;闲置时缓动回正。</summary>
public void UpdateRotation(bool active, float tautness = 1f)
{
if (cablePanel == null || cablePanel.PlugRef == null) return;
UpdateRotation(cablePanel.PlugRef.transform, active, tautness);
}
private void UpdateRotation(Transform plugTransform, bool active, float tautness)
{
if (plugTransform == null) return;
if (!active)
{
@@ -85,7 +102,7 @@ namespace AibisDream.FixSystem
Vector2 center = transform.position;
Vector2 outlet = outletPos;
Vector2 force = (Vector2)cablePanel.PlugRef.transform.position - outlet;
Vector2 force = (Vector2)plugTransform.position - outlet;
Vector2 r = outlet - center;
float torque = (r.x * force.y - r.y * force.x) * tautness;
@@ -108,4 +125,4 @@ namespace AibisDream.FixSystem
UpdateOutletPosition();
}
}
}
}
@@ -7,6 +7,10 @@ namespace AibisDream.FixSystem
public class CableSystem : MonoBehaviour
{
private const string CableRetractedKey = "$global.cableRetracted";
private const float RetractResetDuration = 0.06f;
private const float PullUpDuration = 0.1f;
private DG.Tweening.Sequence _pullUpSequence;
#region
@@ -94,10 +98,10 @@ namespace AibisDream.FixSystem
private void Update()
{
// 获取线缆方向并更新转盘旋转
// 以正下方为中心驱动线盘旋转;CableReel 内部限制为左右各 60°。
if (CableRef != null && CableReelRef != null)
{
CableReelRef.UpdateRotation();
CableReelRef.UpdateRotation(PlugRef != null ? PlugRef.transform : null, !isCableRetracted);
}
}
@@ -138,25 +142,46 @@ namespace AibisDream.FixSystem
public void PullUpCable()
{
if (isCableRetracted) return;
if (isCableRetracted || _pullUpSequence != null) return;
//禁用plug交互
Disable_plugInput();
ResetPlug();
// PlugRef.SetPhysicCableActive(false);
// 强制收线不能复用 ResetPlugReturnToStartPosition 会启动线缆的自动回收,
// 紧接着再 DOMove 会让两个流程争抢插头位置,插在插孔上时尤其像直接平移消失。
PlugRef.PullUpSocket();
BodyModuleSystem.CloseModuleSlots(null);
AudioManager.Instance.PlaySfx("event:/ActionFB/mods_close");
AudioManager.Instance.PlaySfx("event:/FollowInput/plugdrop");
PlugRef.GetComponent<SpriteRenderer>().sortingLayerName =
CableReelRef.GetComponent<SpriteRenderer>().sortingLayerName;
PlugRef.GetComponent<SpriteRenderer>().sortingOrder = 41;
PlugRef.transform.DOMove(retractedPos.position, 0.1f).OnComplete(() =>
{
PhysicCableRef.GetComponent<LineRenderer>().enabled = false;
isCableRetracted = true;
SaveCableState();
});
_pullUpSequence?.Kill();
PlugRef.transform.DOKill(false);
_pullUpSequence = DOTween.Sequence()
// 先快速从插孔复位,消除“跨场景直线吸走”的观感。
.Append(PlugRef.transform.DOMove(PlugRef.GetStartPos(), RetractResetDuration)
.SetEase(Ease.OutCubic))
// 再由线盘向上收走。
.Append(PlugRef.transform.DOMove(retractedPos.position, PullUpDuration)
.SetEase(Ease.InCubic))
.Join(PlugRef.transform.DORotateQuaternion(retractedPos.rotation, PullUpDuration)
.SetEase(Ease.InCubic))
.OnComplete(() =>
{
_pullUpSequence = null;
PhysicCableRef.GetComponent<LineRenderer>().enabled = false;
isCableRetracted = true;
SaveCableState();
});
}
public void DropDownCable()
{
if (!isCableRetracted) return;
if (!isCableRetracted && _pullUpSequence == null) return;
_pullUpSequence?.Kill();
_pullUpSequence = null;
PlugRef.PullUpSocket();
CableReelRef.ResetRotation();
PlugRef.transform.DOMove(PlugRef.GetStartPos(), 0.1f).OnComplete(() =>
@@ -174,6 +199,9 @@ namespace AibisDream.FixSystem
public void SetRetractCable()
{
_pullUpSequence?.Kill();
_pullUpSequence = null;
// 禁用物理线缆和普通线缆
PhysicCableRef.GetComponent<LineRenderer>().enabled = false;
CableRef.HideCable();
@@ -187,4 +215,4 @@ namespace AibisDream.FixSystem
isCableRetracted = true;
}
}
}
}
@@ -37,6 +37,7 @@ public class CableMeshRenderer : MonoBehaviour
private float _tipTaperFraction; // 末端收窄段占全长比例(0 关闭)
private float _tipTaperScale = 1f;
private Color _environmentTint = Color.white;
private readonly List<Vector3> _vertices = new(1024);
private readonly List<Color32> _colors = new(1024);
@@ -69,6 +70,14 @@ public class CableMeshRenderer : MonoBehaviour
_bands = bands ?? System.Array.Empty<Band>();
}
/// <summary>
/// 设置动态网格的环境光色调。RGB 会调制各色带,Alpha 保持原值。
/// </summary>
public void SetEnvironmentTint(Color tint)
{
_environmentTint = tint;
}
/// <summary>末端宽度收窄:最后 fraction 段从全宽线性收到 scale 倍(线头略细于孔圈)。</summary>
public void SetTipTaper(float fraction, float scale)
{
@@ -124,7 +133,11 @@ public class CableMeshRenderer : MonoBehaviour
{
int baseIndex = _vertices.Count;
float half = band.width * 0.5f;
Color32 color = band.color;
Color32 color = new Color(
band.color.r * _environmentTint.r,
band.color.g * _environmentTint.g,
band.color.b * _environmentTint.b,
band.color.a);
for (int i = start; i < count; i++)
{
@@ -128,10 +128,10 @@ public class PhysicCable : MonoBehaviour
private Vector3 _insertImpactDir;
private Vector3 _insertImpactNormal;
// 收拢(Collapsing)状态:当前形状纯插值收缩到目标点,时长由外部(如强制收起指令)指定
// 收拢(Collapsing)状态:沿当前线径从尾端逐段吸回出线口
private Vector3[] _collapseStart;
private Vector3 _collapseTarget;
private float _collapseStartLength;
private float[] _collapseDistances;
private float _collapsePathLength;
private float _collapseDuration;
private float _collapseElapsed;
@@ -239,7 +239,7 @@ public class PhysicCable : MonoBehaviour
}
/// <summary>
/// 强制收起:不走拖拽物理,从当前形状纯运动学插值收缩到 target(出线口收纳位)。
/// 强制收起:不走拖拽物理,保留当前曲线路径并从尾端逐段缩短到 target(出线口)。
/// 用于指令强制收起等需要在极短、精确可控时长内完成的场景,避免拖拽跟随参数(为秒级手动拖拽标定)
/// 在几帧内来不及收敛而产生的甩鞭/抖动。
/// </summary>
@@ -248,10 +248,18 @@ public class PhysicCable : MonoBehaviour
EnsureInit();
_collapseStart ??= new Vector3[_n];
for (int i = 0; i < _n; i++) _collapseStart[i] = _pts[i].pos;
_collapseDistances ??= new float[_n];
_collapsePathLength = 0f;
_collapseStart[0] = FlattenZ(target);
_collapseDistances[0] = 0f;
for (int i = 1; i < _n; i++)
{
_collapseStart[i] = _pts[i].pos;
_collapsePathLength += Vector3.Distance(_collapseStart[i - 1], _collapseStart[i]);
_collapseDistances[i] = _collapsePathLength;
}
_collapseTarget = FlattenZ(target);
_collapseStartLength = _length;
_collapseDuration = Mathf.Max(0.0001f, duration);
_collapseElapsed = 0f;
@@ -492,20 +500,38 @@ public class PhysicCable : MonoBehaviour
UpdatePlugAngle(plugged);
}
/// <summary>Collapsing 状态的纯运动学插值:所有点从收拢起点直接 Lerp 到目标点,不经过 Verlet/约束求解。</summary>
/// <summary>
/// Collapsing 状态沿收线前的曲线逐段截短。线身形状不做整体缩放,
/// 只有尾端沿原路径倒退,形成被线盘吸入的观感。
/// </summary>
private void SimulateCollapse(float dt)
{
_collapseElapsed += dt;
float t = Mathf.Clamp01(_collapseElapsed / _collapseDuration);
float eased = t * t; // ease-in:起步慢、收尾快,贴近"被吸回线盘"的观感
float eased = t * t * (3f - 2f * t);
float remainingLength = _collapsePathLength * (1f - eased);
int segment = 1;
for (int i = 0; i < _n; i++)
{
Vector3 pos = Vector3.Lerp(_collapseStart[i], _collapseTarget, eased);
float distance = remainingLength * (i / (float)(_n - 1));
while (segment < _n - 1 && _collapseDistances[segment] < distance)
{
segment++;
}
int previous = segment - 1;
float segmentLength = _collapseDistances[segment] - _collapseDistances[previous];
float segmentT = segmentLength > 1e-5f
? (distance - _collapseDistances[previous]) / segmentLength
: 0f;
Vector3 pos = Vector3.Lerp(_collapseStart[previous], _collapseStart[segment], segmentT);
_pts[i].pos = pos;
_pts[i].prev = pos;
}
_length = Mathf.Lerp(_collapseStartLength, 0f, eased);
_length = remainingLength;
SyncPlugAngleToTail();
}
private Vector3 ApplyUnplugResistance(Vector3 rawPointer)
@@ -699,6 +725,10 @@ public class PhysicCable : MonoBehaviour
}
// 插接时线缆末端由平头切面改为圆头端帽(线自身的圆头,同旧 LineRenderer),读作"插进孔里"
var panelLight = AibisDream.FixSystem.FixPanelSystem.Instance?.PanelLight;
_cableMesh.SetEnvironmentTint(panelLight != null
? panelLight.CurrentEnvironmentTint
: Color.white);
_cableMesh.UpdateMesh(src, count, FindTailStart(count), State == CableState.Plugged);
}
@@ -32,6 +32,8 @@ namespace AibisDream.FixSystem
private const string PANEL_LIGHT_OFF_SFX = "event:/ActionFB/light_flicker_off";
private const float MIN_FLICKER_INTERVAL = 3f;
private const float MAX_FLICKER_INTERVAL = 8f;
private const float ENVIRONMENT_COLOR_INFLUENCE = 0.65f;
private const float ENVIRONMENT_INTENSITY_INFLUENCE = 0.35f;
#endregion
@@ -87,6 +89,38 @@ namespace AibisDream.FixSystem
private Tween lightStateTransitionTween;
private LightState currentState = LightState.Normal;
/// <summary>
/// 当前环境灯相对于 Normal 状态的实时色调。
/// 用于无法直接接收 URP 2D Light 的动态网格等渲染对象。
/// </summary>
public Color CurrentEnvironmentTint
{
get
{
if (environmentLight == null) return Color.white;
float currentIntensity = Mathf.Max(0f, environmentLight.intensity);
if (currentIntensity <= Mathf.Epsilon) return Color.black;
float intensityScale = currentIntensity /
Mathf.Max(0.01f, normalEnvironmentIntensity);
Color lightColor = environmentLight.color;
Color softenedColor = Color.Lerp(
Color.white,
new Color(lightColor.r, lightColor.g, lightColor.b, 1f),
ENVIRONMENT_COLOR_INFLUENCE);
float softenedIntensity = Mathf.Lerp(
1f,
intensityScale,
ENVIRONMENT_INTENSITY_INFLUENCE);
return new Color(
softenedColor.r * softenedIntensity,
softenedColor.g * softenedIntensity,
softenedColor.b * softenedIntensity,
1f);
}
}
#endregion
#region
@@ -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>
@@ -8,7 +8,7 @@ namespace AibisDream.FixSystem
public class CablePanel : MonoBehaviour, IOperatorPanel
{
private const int SortingOrderRetracted = 41;
private const float RetractDuration = 0.18f; // 强制收起:插头 DOMove 与绳收拢共用时长
private const float RetractDuration = 0.22f;
#region
@@ -44,8 +44,9 @@ namespace AibisDream.FixSystem
private ISocket curSocket;
// 线动画期间插头由 DOTween 驱动,暂停"插头跟随绳末端"
// 线动画期间插头由 DOTween 驱动,暂停"插头跟随绳末端"
private bool suppressPlugFollow;
private Tween retractTween;
#endregion
@@ -152,6 +153,8 @@ namespace AibisDream.FixSystem
public void DropDownCableImmediate()
{
targetCableRetracted = false;
retractTween?.Kill(false);
retractTween = null;
PlugRef.transform.DOKill(false);
PlugRef.PullUpSocketSilent();
CableReelRef.ResetRotation();
@@ -196,14 +199,15 @@ namespace AibisDream.FixSystem
private void Update()
{
// 拖拽/插接时转盘朝拉力方向转动,闲置时缓动回正
// 拖拽/插接时转盘朝插头方向转动,闲置时缓动回正
// 不再要求线缆完全绷紧,否则送线会让 tautness 长时间为 0,视觉上看不到旋转。
if (CableReelRef != null && physicCableRef != null
&& physicCableRef.State != PhysicCable.CableState.Hidden)
{
bool active = physicCableRef.State == PhysicCable.CableState.Dragging
|| physicCableRef.State == PhysicCable.CableState.Plugged;
float tautness = active ? physicCableRef.GetTautness() : 0f;
CableReelRef.UpdateRotation(active, tautness);
|| physicCableRef.State == PhysicCable.CableState.Plugged
|| physicCableRef.State == PhysicCable.CableState.Collapsing;
CableReelRef.UpdateRotation(active);
}
}
@@ -213,7 +217,9 @@ namespace AibisDream.FixSystem
// 闲置/拖拽时插头挂在绳末端,位置与朝向都由绳给出
var state = physicCableRef.State;
if (state == PhysicCable.CableState.Free || state == PhysicCable.CableState.Dragging)
if (state == PhysicCable.CableState.Free
|| state == PhysicCable.CableState.Dragging
|| state == PhysicCable.CableState.Collapsing)
{
physicCableRef.ApplyPlugPose(plugRef.transform, plugRef.PlugRoot);
plugRef.ApplyFeedbackOffset();
@@ -355,7 +361,10 @@ namespace AibisDream.FixSystem
targetCableRetracted = true;
isCableRetracted = true;
retractTween?.Kill(false);
retractTween = null;
PlugRef.transform.DOKill(false);
PlugRef.CancelFeedbackTween();
if (physicCableRef.State == PhysicCable.CableState.Hidden)
{
@@ -363,11 +372,17 @@ namespace AibisDream.FixSystem
return;
}
suppressPlugFollow = true;
physicCableRef.CollapseTo(retractedPos.position, RetractDuration);
PlugRef.transform.DOMove(retractedPos.position, RetractDuration)
.OnComplete(CompleteRetractIfCurrent)
.OnKill(CompleteRetractIfCurrent);
suppressPlugFollow = false;
physicCableRef.CollapseTo(physicCableRef.startTransform.position, RetractDuration);
retractTween = DOVirtual.DelayedCall(RetractDuration, CompleteRetractIfCurrent)
.OnKill(() =>
{
// DelayedCall 正常完成后会自动 Kill;此时回调已经把字段清空,不重复结算。
if (retractTween != null)
{
CompleteRetractIfCurrent();
}
});
AudioManager.Instance.PlaySfx("event:/ActionFB/mods_close");
AudioManager.Instance.PlaySfx("event:/FollowInput/plugdrop");
@@ -376,6 +391,7 @@ namespace AibisDream.FixSystem
private void CompleteRetractIfCurrent()
{
if (!targetCableRetracted) return;
retractTween = null;
ApplyRetractedState();
isCableRetracted = true;
}
@@ -385,6 +401,8 @@ namespace AibisDream.FixSystem
if (!isCableRetracted && !targetCableRetracted) return;
targetCableRetracted = false;
retractTween?.Kill(false);
retractTween = null;
PlugRef.PullUpSocket();
PlugRef.transform.DOKill(false);
CableReelRef.ResetRotation();
@@ -421,6 +439,8 @@ namespace AibisDream.FixSystem
{
targetCableRetracted = true;
isCableRetracted = true;
retractTween?.Kill(false);
retractTween = null;
PlugRef.transform.DOKill(false);
ApplyRetractedState();
}
@@ -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}";
}
}
}
@@ -32,7 +32,14 @@ namespace AibisDream.FixSystem
// 切换到深度表达相机
yield return CameraKit.Instance.SwitchCamera(CameraEnum.ExpressionDeep);
yield return UIManager.Instance.GetPanel<ScreenTransitionPanel>().FadeOutAsync(0.5f);
yield return TimelineCenter.Instance.PlayTimelineAsync("火山表达模块/火山表达深入");
if (expressionManager.HasScreenPresentation)
{
yield return expressionManager.PlayScreenDeepAndWait();
}
else
{
yield return TimelineCenter.Instance.PlayTimelineAsync("火山表达模块/火山表达深入");
}
BubbleSlotKit.Instance.LoadBubbles(BubbleSlotEnum.Expression);
}
@@ -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);
}
}
}
@@ -16,6 +16,7 @@ namespace AibisDream
[SerializeField] private LanguageParticleManager particleManager;
[SerializeField] private GameObject expressionView;
[SerializeField] private LogReleasePresentationController logReleasePresentation;
[SerializeField] private ExpressionScreenPresentationController screenPresentation;
[Header("默认配置")]
[SerializeField] private List<string> defaultAnxietyPhrases = new List<string>
@@ -111,9 +112,15 @@ namespace AibisDream
logReleasePresentation = GetComponent<LogReleasePresentationController>();
}
if (screenPresentation == null)
{
screenPresentation = GetComponent<ExpressionScreenPresentationController>();
}
if (particleManager != null)
{
particleManager.SetPresentationController(logReleasePresentation);
particleManager.SetScreenPresentationController(screenPresentation);
}
}
@@ -123,6 +130,7 @@ namespace AibisDream
/// </summary>
public void OpenView()
{
screenPresentation?.ResetImmediate();
// 先设置所有初始状态再打开
if (particleManager != null)
{
@@ -230,6 +238,16 @@ namespace AibisDream
yield return particleManager.WaitUntilExpressionFlowReady();
}
internal IEnumerator PlayScreenDeepAndWait()
{
if (screenPresentation == null)
yield break;
yield return screenPresentation.PlayDeepAndWait();
}
internal bool HasScreenPresentation => screenPresentation != null;
/// <summary>
/// 从聚焦保持态触发稳定化(概率递增 → 文字锁定 → 排列)
/// </summary>
@@ -256,6 +274,7 @@ namespace AibisDream
/// </summary>
public void CloseView()
{
screenPresentation?.ResetImmediate();
ResetScreenGlitchState();
ApplyScreenGlitchProperties();
@@ -0,0 +1,492 @@
using System.Collections;
using AibisDream.Utility;
using TMPro;
using UnityEngine;
using UnityEngine.Localization.Components;
using UnityEngine.UI;
namespace AibisDream.MiniGame.Language
{
/// <summary>
/// Drives the full-frame Huoshan expression screen while keeping readable text in TMP.
/// The sprite sheet uses the original 384x216 Aseprite timing and the existing world-space
/// Canvas remains the source of truth for interaction and dynamic language particles.
/// </summary>
public sealed class ExpressionScreenPresentationController : MonoBehaviour
{
public enum ScreenState
{
Reset,
Deep,
Panel,
Output,
Floating,
Click,
FocusHidden
}
private const int DeepFirst = 0;
private const int DeepLast = 14;
private const int UiFlashFirst = 15;
private const int UiFlashLast = 29;
private const int StatusRevealFrame = 21;
private const int RingFirst = 30;
private const int RingLast = 40;
private const int OutputFirst = 41;
private const int OutputLast = 47;
private const int OutputTextRevealFrame = 45;
private const int FloatingFrame = 48;
private const int ClickFirst = 49;
private const int ClickLast = 52;
private const string ObjectiveLocalizationKey = "huoshan_expression_objective";
private const string OutputLocalizationKey = "huoshan_expression_output";
[Header("Frame Animation")]
[SerializeField] private SpriteRenderer screenRenderer;
[SerializeField] private Sprite[] frames = new Sprite[53];
[SerializeField] private Vector3 screenLocalPosition = new Vector3(0.68f, -8.95f, 0f);
[SerializeField] private Vector3 screenLocalScale = new Vector3(1.1f, 1.1f, 1f);
[Header("Existing TMP / Interaction")]
[SerializeField] private TMP_Text integrationProgressText;
[SerializeField] private TMP_Text interferenceCountText;
[SerializeField] private Button outputButton;
[SerializeField] private TMP_FontAsset screenFont;
[Header("Status TMP (screen-space world units)")]
[SerializeField] private Vector2 integrationStatusPosition = new Vector2(-8.75f, 4.15f);
[SerializeField] private Vector2 interferenceStatusPosition = new Vector2(-8.75f, 3.08f);
[SerializeField] private Vector2 statusPanelSize = new Vector2(4.55f, 0.6f);
[SerializeField] private Vector4 statusTextMargins = new Vector4(0.16f, 0.05f, 0.08f, 0.05f);
[SerializeField] private float statusFontSize = 0.26f;
[SerializeField] private Color screenTextColor = new Color(0.78f, 0.91f, 1f, 1f);
[Header("Output Button Hit Area")]
[SerializeField] private Vector2 outputButtonPosition = new Vector2(4.86f, -0.065f);
[SerializeField] private Vector2 outputButtonSize = new Vector2(2.72f, 1.13f);
[Header("Objective TMP (384x216 reference)")]
[SerializeField] private Vector2 objectivePosition = new Vector2(-8.18f, -4.08f);
[SerializeField] private Vector2 objectiveSize = new Vector2(7.4f, 0.95f);
[SerializeField] private float objectiveFontSize = 0.28f;
[SerializeField] private Color objectiveColor = new Color(0.78f, 0.91f, 1f, 1f);
private int playbackGeneration;
private TMP_Text objectiveText;
private TMP_Text outputText;
private LocalizeStringEvent objectiveLocalization;
private LocalizeStringEvent outputLocalization;
private Material objectiveMaterial;
private ScreenState state = ScreenState.Reset;
public ScreenState State => state;
private void Awake()
{
ResolveSceneReferences();
ConfigureExistingUiAsTextOnly();
CreateObjectiveText();
BindStaticLocalization();
ResetImmediate();
}
private void ResolveSceneReferences()
{
if (screenRenderer == null)
{
SpriteRenderer[] renderers = GetComponentsInChildren<SpriteRenderer>(true);
foreach (SpriteRenderer candidate in renderers)
{
if (candidate != null && candidate.gameObject.name == "火山释放log界面")
{
screenRenderer = candidate;
break;
}
}
}
if (outputButton != null)
{
outputText = outputButton.GetComponentInChildren<TMP_Text>(true);
}
if (screenRenderer != null)
{
screenRenderer.transform.localPosition = screenLocalPosition;
screenRenderer.transform.localScale = screenLocalScale;
}
}
private void ConfigureExistingUiAsTextOnly()
{
MakeParentImageTransparent(integrationProgressText);
MakeParentImageTransparent(interferenceCountText);
ConfigureStatusText(integrationProgressText, integrationStatusPosition);
ConfigureStatusText(interferenceCountText, interferenceStatusPosition);
ConfigureOutputText();
if (outputButton != null && outputButton.targetGraphic != null)
{
Color color = outputButton.targetGraphic.color;
color.a = 0f;
outputButton.targetGraphic.color = color;
outputButton.targetGraphic.raycastTarget = true;
}
}
private void ConfigureStatusText(TMP_Text text, Vector2 panelPosition)
{
if (text == null)
return;
if (screenFont != null)
text.font = screenFont;
text.fontSize = statusFontSize;
text.fontStyle = FontStyles.Normal;
text.color = screenTextColor;
text.alignment = TextAlignmentOptions.MidlineLeft;
text.enableWordWrapping = false;
text.overflowMode = TextOverflowModes.Truncate;
text.margin = statusTextMargins;
text.raycastTarget = false;
if (text.transform.parent is RectTransform panelRect)
{
panelRect.anchorMin = new Vector2(0.5f, 0.5f);
panelRect.anchorMax = new Vector2(0.5f, 0.5f);
panelRect.pivot = new Vector2(0f, 0.5f);
panelRect.anchoredPosition = panelPosition;
panelRect.sizeDelta = statusPanelSize;
}
RectTransform textRect = text.rectTransform;
textRect.anchorMin = Vector2.zero;
textRect.anchorMax = Vector2.one;
textRect.pivot = new Vector2(0.5f, 0.5f);
textRect.anchoredPosition = Vector2.zero;
textRect.sizeDelta = Vector2.zero;
}
private void ConfigureOutputText()
{
if (outputButton == null)
return;
RectTransform buttonRect = outputButton.transform as RectTransform;
if (buttonRect != null)
{
buttonRect.anchorMin = new Vector2(0.5f, 0.5f);
buttonRect.anchorMax = new Vector2(0.5f, 0.5f);
buttonRect.pivot = new Vector2(0f, 0.5f);
buttonRect.anchoredPosition = outputButtonPosition;
buttonRect.sizeDelta = outputButtonSize;
}
if (outputText == null)
return;
if (screenFont != null)
outputText.font = screenFont;
outputText.fontStyle = FontStyles.Normal;
outputText.color = screenTextColor;
outputText.alignment = TextAlignmentOptions.Center;
outputText.enableWordWrapping = false;
outputText.overflowMode = TextOverflowModes.Truncate;
RectTransform textRect = outputText.rectTransform;
textRect.anchorMin = Vector2.zero;
textRect.anchorMax = Vector2.one;
textRect.pivot = new Vector2(0.5f, 0.5f);
textRect.anchoredPosition = Vector2.zero;
textRect.sizeDelta = Vector2.zero;
}
private static void MakeParentImageTransparent(TMP_Text text)
{
if (text == null || text.transform.parent == null)
return;
Image image = text.transform.parent.GetComponent<Image>();
if (image == null)
return;
Color color = image.color;
color.a = 0f;
image.color = color;
image.raycastTarget = false;
}
private void CreateObjectiveText()
{
Canvas canvas = integrationProgressText != null
? integrationProgressText.GetComponentInParent<Canvas>()
: GetComponentInChildren<Canvas>(true);
if (canvas == null)
{
Debug.LogError("[ExpressionScreenPresentation] ExpressView 下未找到 World Space Canvas,任务描述无法创建。");
return;
}
Transform existing = canvas.transform.Find("Expression Objective (TMP)");
if (existing != null)
{
objectiveText = existing.GetComponent<TMP_Text>();
return;
}
var objectiveObject = new GameObject(
"Expression Objective (TMP)",
typeof(RectTransform),
typeof(CanvasRenderer),
typeof(TextMeshProUGUI));
objectiveObject.layer = canvas.gameObject.layer;
RectTransform rect = objectiveObject.GetComponent<RectTransform>();
rect.SetParent(canvas.transform, false);
rect.anchorMin = new Vector2(0.5f, 0.5f);
rect.anchorMax = new Vector2(0.5f, 0.5f);
rect.pivot = new Vector2(0f, 0.5f);
rect.anchoredPosition = objectivePosition;
rect.sizeDelta = objectiveSize;
rect.SetAsFirstSibling();
var text = objectiveObject.GetComponent<TextMeshProUGUI>();
text.font = screenFont;
if (integrationProgressText != null && integrationProgressText.fontSharedMaterial != null)
{
objectiveMaterial = new Material(integrationProgressText.fontSharedMaterial)
{
name = "Huoshan Expression Objective TMP (Runtime)"
};
text.fontSharedMaterial = objectiveMaterial;
}
text.fontSize = objectiveFontSize;
text.fontStyle = FontStyles.Normal;
text.color = objectiveColor;
text.alignment = TextAlignmentOptions.MidlineLeft;
text.enableWordWrapping = true;
text.overflowMode = TextOverflowModes.Truncate;
text.raycastTarget = false;
objectiveText = text;
}
private void BindStaticLocalization()
{
objectiveLocalization = BindLocalizedText(objectiveText, ObjectiveLocalizationKey);
outputLocalization = BindLocalizedText(outputText, OutputLocalizationKey);
}
private static LocalizeStringEvent BindLocalizedText(TMP_Text text, string entryKey)
{
if (text == null)
return null;
LocalizeStringEvent localize = text.GetComponent<LocalizeStringEvent>();
if (localize == null)
localize = text.gameObject.AddComponent<LocalizeStringEvent>();
localize.StringReference.SetReference(ConstRef.UITextTable, entryKey);
localize.OnUpdateString.RemoveAllListeners();
localize.OnUpdateString.AddListener(value => text.text = value);
localize.StringReference.RefreshString();
return localize;
}
public IEnumerator PlayDeepAndWait()
{
int generation = BeginPlayback();
SetOverlayVisibility(false, false, false);
yield return PlayRangeAndWait(DeepFirst, DeepLast, generation);
if (generation != playbackGeneration)
yield break;
state = ScreenState.Deep;
}
public IEnumerator PlayPanelAndWait()
{
int generation = BeginPlayback();
yield return PlayRangeAndWait(UiFlashFirst, UiFlashLast, generation, StatusRevealFrame, ShowStatusAndObjective);
if (generation != playbackGeneration)
yield break;
yield return PlayRangeAndWait(RingFirst, RingLast, generation);
if (generation != playbackGeneration)
yield break;
ShowStatusAndObjective();
state = ScreenState.Panel;
}
public IEnumerator PlayOutputAndWait()
{
int generation = BeginPlayback();
SetOutputTextVisible(false);
yield return PlayRangeAndWait(
OutputFirst,
OutputLast,
generation,
OutputTextRevealFrame,
() => SetOutputTextVisible(true));
if (generation != playbackGeneration)
yield break;
SetFrame(FloatingFrame);
SetOutputTextVisible(true);
state = ScreenState.Floating;
}
public IEnumerator PlayClickAndWait()
{
int generation = BeginPlayback();
SetOutputTextVisible(true);
yield return PlayRangeAndWait(ClickFirst, ClickLast, generation);
if (generation != playbackGeneration)
yield break;
state = ScreenState.Click;
}
public void PrepareForNewRound()
{
StopPlayback();
SetFrame(DeepFirst);
SetOverlayVisibility(false, false, false);
state = ScreenState.Reset;
}
public void HideOverlayForFocus()
{
SetOverlayVisibility(false, false, false);
state = ScreenState.FocusHidden;
}
public void ResetImmediate()
{
StopPlayback();
SetFrame(DeepFirst);
SetOverlayVisibility(false, false, false);
if (outputButton != null)
{
outputButton.interactable = false;
outputButton.gameObject.SetActive(false);
}
state = ScreenState.Reset;
}
private int BeginPlayback()
{
StopPlayback();
return playbackGeneration;
}
private IEnumerator PlayRangeAndWait(
int firstFrame,
int lastFrame,
int generation,
int revealFrame = -1,
System.Action revealAction = null)
{
for (int frameIndex = firstFrame; frameIndex <= lastFrame; frameIndex++)
{
if (generation != playbackGeneration)
yield break;
SetFrame(frameIndex);
if (frameIndex == revealFrame)
revealAction?.Invoke();
yield return new WaitForSeconds(GetFrameDuration(frameIndex));
}
}
private void SetFrame(int frameIndex)
{
if (screenRenderer == null || frames == null ||
frameIndex < 0 || frameIndex >= frames.Length || frames[frameIndex] == null)
{
return;
}
screenRenderer.gameObject.SetActive(true);
screenRenderer.enabled = true;
screenRenderer.sprite = frames[frameIndex];
}
private static float GetFrameDuration(int frameIndex)
{
switch (frameIndex)
{
case 0:
case 29:
case 47:
case 48:
return 0.3f;
case 16:
case 17:
case 21:
case 25:
case 49:
case 50:
case 51:
case 52:
return 0.05f;
default:
return 0.1f;
}
}
private void ShowStatusAndObjective()
{
SetOverlayVisibility(true, true, false);
}
private void SetOverlayVisibility(bool statusVisible, bool objectiveVisible, bool outputVisible)
{
SetStatusTextVisible(integrationProgressText, statusVisible);
SetStatusTextVisible(interferenceCountText, statusVisible);
if (objectiveText != null)
objectiveText.gameObject.SetActive(objectiveVisible);
SetOutputTextVisible(outputVisible);
}
private static void SetStatusTextVisible(TMP_Text text, bool visible)
{
if (text == null)
return;
if (visible)
{
var canvasGroup = text.GetComponentInParent<CanvasGroup>();
if (canvasGroup != null)
canvasGroup.alpha = 1f;
}
if (text.transform.parent != null)
text.transform.parent.gameObject.SetActive(visible);
text.gameObject.SetActive(visible);
}
private void SetOutputTextVisible(bool visible)
{
if (outputText != null)
outputText.gameObject.SetActive(visible);
}
private void StopPlayback()
{
playbackGeneration++;
}
private void OnDestroy()
{
if (objectiveLocalization != null)
objectiveLocalization.OnUpdateString.RemoveAllListeners();
if (outputLocalization != null)
outputLocalization.OnUpdateString.RemoveAllListeners();
if (objectiveMaterial != null)
Destroy(objectiveMaterial);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5e1287ca2a7d4d2d90c1a668c0f5bb82
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -5,7 +5,10 @@ using System.Linq;
using DG.Tweening;
using TMPro;
using UnityEngine.UI;
using UnityEngine.Localization;
using UnityEngine.Localization.Settings;
using AibisDream;
using AibisDream.Utility;
namespace AibisDream.MiniGame.Language
{
@@ -134,6 +137,7 @@ namespace AibisDream.MiniGame.Language
[SerializeField] private Button focusSequenceButton;
[Tooltip("火山释放log界面,触发 focus 时对其 SpriteRenderer 做淡出,重新开始时恢复")]
[SerializeField] private SpriteRenderer releaseLogSpriteRenderer;
[SerializeField] private ExpressionScreenPresentationController screenPresentation;
[Tooltip("开场表达panel Timeline 名称(DirectorName 或 DirectorName/AddressableKey),播完才算表达流程完成")]
[SerializeField] private string expressionPanelTimelineName = "火山表达模块/火山表达panel";
[Tooltip("火山表达确认 Timeline 名称,播完才解锁按钮交互")]
@@ -250,6 +254,11 @@ namespace AibisDream.MiniGame.Language
private float lieEdgeJitter;
private float truthGatherProgress;
private LogReleasePresentationController presentationController;
private const string IntegrationLocalizationKey = "huoshan_expression_integration";
private const string InterferenceLocalizationKey = "huoshan_expression_interference";
private string integrationStatusFormat = "{0}% 语言整合完成度";
private string interferenceStatusFormat = "{0} 处异常思绪仍在干扰表达";
private Coroutine statusLocalizationRoutine;
private const string LieCompressionTweenId = "HuoshanLieEdgeCompression";
private const string TruthGatherTweenId = "HuoshanTruthGather";
private const string FocusInterferenceTweenId = "HuoshanFocusInterference";
@@ -286,9 +295,51 @@ namespace AibisDream.MiniGame.Language
private void Start()
{
ResolveScreenPresentation();
LocalizationSettings.SelectedLocaleChanged += HandleLocaleChanged;
statusLocalizationRoutine = StartCoroutine(RefreshStatusLocalization());
// Start 中不再自动初始化,等待外部调用 InitializeSystem
}
private void ResolveScreenPresentation()
{
if (screenPresentation == null)
screenPresentation = GetComponentInParent<ExpressionScreenPresentationController>(true);
}
private void HandleLocaleChanged(Locale locale)
{
if (!isActiveAndEnabled)
return;
if (statusLocalizationRoutine != null)
StopCoroutine(statusLocalizationRoutine);
statusLocalizationRoutine = StartCoroutine(RefreshStatusLocalization());
}
private IEnumerator RefreshStatusLocalization()
{
yield return LocalizationSettings.InitializationOperation;
var integrationOperation = LocalizationSettings.StringDatabase.GetLocalizedStringAsync(
ConstRef.UITextTable,
IntegrationLocalizationKey);
yield return integrationOperation;
if (!string.IsNullOrEmpty(integrationOperation.Result))
integrationStatusFormat = integrationOperation.Result;
var interferenceOperation = LocalizationSettings.StringDatabase.GetLocalizedStringAsync(
ConstRef.UITextTable,
InterferenceLocalizationKey);
yield return interferenceOperation;
if (!string.IsNullOrEmpty(interferenceOperation.Result))
interferenceStatusFormat = interferenceOperation.Result;
statusLocalizationRoutine = null;
if (isInitialized)
UpdateStatusUI();
}
/// <summary>
/// 初始化系统(可以由外部调用,用于配置游戏参数)
/// </summary>
@@ -373,6 +424,8 @@ namespace AibisDream.MiniGame.Language
if (!isInitialized)
yield break;
ResolveScreenPresentation();
screenPresentation?.PrepareForNewRound();
StopFocusTransitionRoutine();
KillCompletionUiTweens();
resolveTotalDurationOverride = -1f;
@@ -601,6 +654,10 @@ namespace AibisDream.MiniGame.Language
// 确保 Canvas 配置正确
if (worldCanvas != null)
{
RectTransform rectTransform = worldCanvas.GetComponent<RectTransform>();
if (rectTransform != null)
rectTransform.sizeDelta = new Vector2(canvasSize, canvasSize);
// 设置 WorldSpace Canvas 的相机
if (worldCanvas.worldCamera == null)
{
@@ -1286,7 +1343,15 @@ namespace AibisDream.MiniGame.Language
private IEnumerator PlayConfirmTimelineAndUnlockButton()
{
if (!string.IsNullOrEmpty(expressionConfirmTimelineName) && TimelineCenter.Instance != null)
ResolveScreenPresentation();
if (screenPresentation != null)
{
yield return screenPresentation.PlayOutputAndWait();
if (screenPresentation == null ||
screenPresentation.State != ExpressionScreenPresentationController.ScreenState.Floating)
yield break;
}
else if (!string.IsNullOrEmpty(expressionConfirmTimelineName) && TimelineCenter.Instance != null)
{
yield return TimelineCenter.Instance.PlayTimelineAsync(expressionConfirmTimelineName);
}
@@ -1303,6 +1368,29 @@ namespace AibisDream.MiniGame.Language
if (completionPhase != CompletionPhase.Completed)
return;
if (focusSequenceButton != null)
focusSequenceButton.interactable = false;
ResolveScreenPresentation();
if (screenPresentation != null)
{
StartCoroutine(PlayClickThenBeginFocus());
return;
}
BeginFocusSequence();
}
private IEnumerator PlayClickThenBeginFocus()
{
yield return screenPresentation.PlayClickAndWait();
if (screenPresentation == null ||
screenPresentation.State != ExpressionScreenPresentationController.ScreenState.Click)
{
yield break;
}
screenPresentation.HideOverlayForFocus();
BeginFocusSequence();
}
@@ -1837,6 +1925,11 @@ namespace AibisDream.MiniGame.Language
presentationController = controller;
}
public void SetScreenPresentationController(ExpressionScreenPresentationController controller)
{
screenPresentation = controller;
}
public void BeginLieEdgeCompression(float duration, float jitterAmount)
{
if (completionPhase != CompletionPhase.FocusHolding)
@@ -3153,7 +3246,20 @@ namespace AibisDream.MiniGame.Language
/// </summary>
private IEnumerator PlayExpressionPanelThenReady()
{
if (!string.IsNullOrEmpty(expressionPanelTimelineName) && TimelineCenter.Instance != null)
ResolveScreenPresentation();
if (screenPresentation != null)
{
if (languageDeepPanel1 != null)
languageDeepPanel1.SetActive(true);
if (languageDeepPanel2 != null)
languageDeepPanel2.SetActive(true);
yield return screenPresentation.PlayPanelAndWait();
if (screenPresentation == null ||
screenPresentation.State != ExpressionScreenPresentationController.ScreenState.Panel)
yield break;
}
else if (!string.IsNullOrEmpty(expressionPanelTimelineName) && TimelineCenter.Instance != null)
{
// 火山表达 panel timeline 驱动 panel1/2,播放前需先激活(若之前被 fade 过,需重置 alpha)
if (languageDeepPanel1 != null)
@@ -3301,13 +3407,13 @@ namespace AibisDream.MiniGame.Language
{
float ratio = CalculateIntegrationRatio();
int percent = Mathf.RoundToInt(ratio * 100f);
integrationProgressText.text = $"{percent}% 语言整合完成度";
integrationProgressText.text = string.Format(integrationStatusFormat, percent);
}
if (interferenceCountText != null)
{
int interferenceCount = CountInterferenceNodes();
interferenceCountText.text = $"{interferenceCount} 处异常思绪仍在干扰表达";
interferenceCountText.text = string.Format(interferenceStatusFormat, interferenceCount);
}
}
@@ -3409,6 +3515,11 @@ namespace AibisDream.MiniGame.Language
}
}
private void OnDestroy()
{
LocalizationSettings.SelectedLocaleChanged -= HandleLocaleChanged;
}
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
@@ -182,7 +182,8 @@ namespace AibisDream.MiniGame.Language
/// <summary>
/// 记忆淡入(非阻塞):透明度在 fadeDuration 内显现,画面同时按 pushSpeed 持续推近;
/// targetOpacity 是本次淡入的目标透明度。horizontalSpeed 为旧 Yarn 兼容参数,基础层不再横移。
/// 争夺段的 jolt / tear / impact 仍会叠加。
/// 进记忆前可用 glitch_transition 铺 noisebegin 时会把 noise 淡出,改由记忆材质
/// _SimGlitch / Analog 承接轻微失真。preserveGlitch 已忽略。争夺段 jolt/tear/impact 仍叠加。
/// </summary>
[YarnCommand("expression_memory_begin")]
public static void ExpressionMemoryBegin(
@@ -106,6 +106,11 @@ namespace AibisDream.MiniGame.Language
[SerializeField] private Color lieBackdropColor = new Color(0.02f, 0.05f, 0.09f, 0.82f);
[Tooltip("梳理完成后目标粒子与真话字的颜色;玩法阶段仍用粒子管理器原色。")]
[SerializeField] private Color presentationResolvedColor = Color.white;
[Header("Truth Attack")]
[Tooltip("攻击词命中时覆盖记忆层的瞬时怒火色。使用深红而非粉红,避免与攻击文字争夺可读性。")]
[SerializeField] private Color truthAttackFlashColor = new Color(0.48f, 0.01f, 0.025f, 1f);
[Tooltip("攻击强度从 0 到 1 时,整屏红色冲击的透明度范围。")]
[SerializeField] private Vector2 truthAttackFlashAlpha = new Vector2(0.14f, 0.40f);
[SerializeField] private int screenGlowSortingOrder = 8;
[SerializeField] private int screenTextSortingOrder = 18;
@@ -342,9 +347,8 @@ namespace AibisDream.MiniGame.Language
state = PresentationState.Memory;
// 兜底:跳过 face_fade_in 直进记忆时,目标字仍切到演出白。
particleManager?.ApplyPresentationResolvedVisuals(presentationResolvedColor);
// 记忆失真全部走 HuoshanMemory 材质;清掉 noise 叠层残留
// preserveGlitch 已弃用:进记忆失真走 HuoshanMemory 材质,不再保留 noise 叠层。
_ = preserveGlitch;
SilenceNoiseOverlayIfPresent();
SpriteRenderer incoming = activeMemoryRenderer == memoryRendererA ? memoryRendererB : memoryRendererA;
SpriteRenderer outgoing = activeMemoryRenderer;
@@ -365,6 +369,10 @@ namespace AibisDream.MiniGame.Language
float resolvedFadeDuration = fadeDuration > 0f
? Mathf.Max(0.01f, fadeDuration)
: memoryFadeDuration;
// 进记忆前的 glitch_transition noise → 透明叠层后淡出;
// 记忆材质 _SimGlitch / Analog 随透明度一起浮现,完成交接。
ReleaseNoiseOverlayIntoMemory(resolvedFadeDuration);
SetMemoryOpacity(incoming, 0f);
TweenMemoryOpacity(
incoming,
@@ -1433,6 +1441,13 @@ namespace AibisDream.MiniGame.Language
PlayMemoryTear(
Mathf.Lerp(0.08f, 0.58f, resolvedImpact),
Mathf.Lerp(0.14f, 0.36f, resolvedImpact));
PulseTruthAttackFlash(
Mathf.Lerp(
truthAttackFlashAlpha.x,
truthAttackFlashAlpha.y,
resolvedImpact),
Mathf.Lerp(0.035f, 0.018f, resolvedImpact),
Mathf.Lerp(0.18f, 0.30f, resolvedImpact));
if (particleManager != null)
yield return particleManager.PlayTruthAttackAndWait(text, duration, resolvedImpact);
@@ -2080,11 +2095,37 @@ namespace AibisDream.MiniGame.Language
if (glitchController == null)
return;
// OpaqueBackground 即使 Intensity=0 仍会写满不透明黑底。
// 记忆阶段切到 TransparentOverlay,避免盖住画面。
glitchController.SetCompositeMode(SpriteNoiseGlitchController.CompositeMode.TransparentOverlay);
glitchController.Intensity = 0f;
glitchController.SetAmbientNoise(0f, 320f, 0f);
glitchController.ClearCalmField(true);
}
/// <summary>
/// 进记忆交接:把进场前的 noise 从「不透明黑底」切成透明叠层并淡出,
/// 让记忆材质自身的 _SimGlitch / Analog 接上失真,而不是突然掐断或继续盖住画面。
/// </summary>
private void ReleaseNoiseOverlayIntoMemory(float memoryFadeDuration)
{
if (glitchController == null)
return;
glitchController.SetCompositeMode(SpriteNoiseGlitchController.CompositeMode.TransparentOverlay);
glitchController.SetAmbientNoise(0f, 320f, 0f);
glitchController.ClearCalmField(true);
float releaseDuration = Mathf.Clamp(memoryFadeDuration * 0.35f, 0.12f, 0.55f);
if (glitchController.Intensity <= 0.001f)
{
glitchController.Intensity = 0f;
return;
}
glitchController.TransitionTo(0f, releaseDuration);
}
private void ResetNoiseOverlayForGlitchTransition()
{
if (glitchController == null)
@@ -2322,6 +2363,11 @@ namespace AibisDream.MiniGame.Language
{
GameObject obj = new GameObject(objectName);
obj.transform.SetParent(runtimeRoot, false);
// 与粒子 / 屏幕同属 HuoshanScreen,进入 VolFx huoshan 合成;
// 留在 Default 会被 sortingOrder 10 的不透明 noise 盖住。
int huoshanScreen = LayerMask.NameToLayer("HuoshanScreen");
if (huoshanScreen >= 0)
obj.layer = huoshanScreen;
SpriteRenderer renderer = obj.AddComponent<SpriteRenderer>();
renderer.sharedMaterial = memoryMaterial;
renderer.sortingOrder = 0;
@@ -2634,6 +2680,30 @@ namespace AibisDream.MiniGame.Language
sequence.OnComplete(StopScreenDim);
}
private void PulseTruthAttackFlash(float alpha, float riseDuration, float fallDuration)
{
if (screenDimRenderer == null)
return;
screenDimRenderer.DOKill();
screenDimRenderer.enabled = true;
screenDimRenderer.gameObject.SetActive(true);
screenDimRenderer.color = new Color(
truthAttackFlashColor.r,
truthAttackFlashColor.g,
truthAttackFlashColor.b,
0f);
Sequence sequence = DOTween.Sequence().SetTarget(this);
sequence.Append(screenDimRenderer.DOFade(
Mathf.Clamp01(alpha),
Mathf.Max(0.01f, riseDuration)));
sequence.Append(screenDimRenderer.DOFade(
0f,
Mathf.Max(0.01f, fallDuration)));
sequence.OnComplete(StopScreenDim);
}
private void HoldTruthBreakBlackout(float alpha)
{
if (screenDimRenderer == null)
@@ -15,6 +15,10 @@ namespace AibisDream.MiniGame.HuoShan
[Tooltip("效果区域参考(取其 SpriteRenderer bounds),不填则用自身")]
[SerializeField] private SpriteRenderer areaReference;
[Tooltip("相对参考区域向内收缩比例,避免辉光贴满边框轻微溢出")]
[Range(0f, 0.2f)]
[SerializeField] private float areaInset = 0.04f;
[Header("像素网格")]
[Tooltip("横向像素格数(纵向按区域纵横比自动推算)")]
[SerializeField] private int pixelColumns = 72;
@@ -201,7 +205,10 @@ namespace AibisDream.MiniGame.HuoShan
if (reference != null)
{
var b = reference.bounds;
_areaHalfSize = new Vector2(Mathf.Max(0.01f, b.extents.x), Mathf.Max(0.01f, b.extents.y));
float inset = 1f - Mathf.Clamp01(areaInset);
_areaHalfSize = new Vector2(
Mathf.Max(0.01f, b.extents.x * inset),
Mathf.Max(0.01f, b.extents.y * inset));
}
}
@@ -340,7 +347,8 @@ namespace AibisDream.MiniGame.HuoShan
float x = Random.value < sparkEdgeBias
? Mathf.Max(0f, _fill - Random.value * edgeHotWidth * 0.8f) // 前沿迸溅
: Random.value * _fill;
float y = 0.5f + (Random.value - 0.5f) * 0.75f;
// 略收纵向散布,避免火花贴边后冲出管体外框
float y = 0.5f + (Random.value - 0.5f) * 0.62f;
float outward = y >= 0.5f ? 1f : -1f;
float life = Mathf.Max(0.05f, sparkLife * (0.55f + Random.value * 0.9f));
@@ -351,7 +359,7 @@ namespace AibisDream.MiniGame.HuoShan
Y = y,
// 略偏左:被抑制的方向
VX = (Random.value - 0.5f) * 0.55f - 0.12f,
VY = outward * (0.3f + Random.value * 1.1f),
VY = outward * (0.22f + Random.value * 0.85f),
Life = life,
MaxLife = life
};
@@ -432,7 +440,7 @@ namespace AibisDream.MiniGame.HuoShan
// 每行前沿单独抖动:右端像火焰一样翻腾,而不是一条硬边
float boilN = Mathf.PerlinNoise(py * 0.35f + _seedY, _time * 6f) - 0.5f;
float fillEdge = _fill + boilN * edgeBoil * 2f;
float fillEdge = Mathf.Min(1f, _fill + boilN * edgeBoil * 2f);
for (int px = 0; px < w; px++)
{
+3 -3
View File
@@ -155,7 +155,7 @@ Postflight 校验
Provider 使用 `ISyncSnapshotProvider``IAsyncSnapshotProvider` 显式声明同步/异步恢复;异步 Provider 必须把等待过程返回给编排层,禁止内部 fire-and-forget。
## 快照 JSON 结构(schemaVersion = 1
## 快照 JSON 结构(schemaVersion = 2
| 字段 | 含义 |
| --- | --- |
@@ -200,11 +200,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();
@@ -94,6 +94,11 @@ namespace AibisDream
_kite.PlayAccent(profile);
}
public IEnumerator FallKiteOutOfFrame(float duration = 1.1f)
{
yield return _kite.FallOutOfFrame(duration);
}
public IEnumerator HideKite(float duration = 1f)
{
yield return _kite.Hide(duration);
@@ -75,6 +75,14 @@ namespace AibisDream
yield return new WaitForSeconds(leadDuration);
}
[YarnCommand("kite_fall_out")]
public static IEnumerator KiteFallOut(float duration = 1.1f)
{
var controller = Resolve();
if (controller == null) yield break;
yield return controller.FallKiteOutOfFrame(duration);
}
[YarnCommand("kite_hide")]
public static IEnumerator KiteHide(float duration = 1f)
{
@@ -62,7 +62,17 @@ namespace AibisDream
height = Mathf.Clamp(height, 0f, KiteMaxHeight);
if (IsShowing)
{
SetState(height, wind, duration);
if (_driver.IsRunning)
{
SetState(height, wind, duration);
}
else
{
_kiteRenderer.transform.localPosition = Vector3.zero;
_kiteRenderer.transform.localRotation = Quaternion.identity;
_kiteRenderer.SetAlpha(KiteRigDriver.Alpha(height));
_driver.Wake(height, wind);
}
yield break;
}
@@ -210,6 +220,36 @@ namespace AibisDream
_accentSequence = sequence;
}
public IEnumerator FallOutOfFrame(float duration = 1.1f)
{
if (!IsShowing || _kiteRenderer == null || _driver == null) yield break;
_accentSequence?.Kill();
_accentSequence = null;
_driver.ResetAndSleep();
var transform = _kiteRenderer.transform;
float frameHeight = _baseRenderer != null && _baseRenderer.sprite != null
? _baseRenderer.sprite.bounds.size.y
: 6f;
float fallDistance = Mathf.Max(frameHeight * 1.1f, 3f);
Vector3 target = transform.localPosition + Vector3.down * fallDistance;
float fallDuration = Mathf.Max(0.05f, duration);
var sequence = DOTween.Sequence();
sequence.Append(transform.DOLocalMove(target, fallDuration)
.SetEase(Ease.InQuad));
sequence.Join(transform.DOLocalRotate(new Vector3(-4f, 2f, 8f), fallDuration)
.SetEase(Ease.InSine));
_accentSequence = sequence;
yield return sequence.WaitForCompletion();
if (_accentSequence != sequence) yield break;
_kiteRenderer.SetAlpha(0f);
_accentSequence = null;
}
public IEnumerator Hide(float duration = 1f)
{
ClearImmediate();
@@ -24,6 +24,7 @@ namespace AibisDream
private Coroutine _animationCoroutine;
private Sequence _presentationSequence;
private int _blurTweenVersion;
private int _presentationTweenVersion;
public bool IsAnimationPlaying => _animationCoroutine != null;
public string AnimationPrefix { get; private set; }
@@ -131,6 +132,7 @@ namespace AibisDream
public IEnumerator AnimatePresentation(float scaleMultiplier, float targetAlpha, float duration)
{
if (_renderer == null) yield break;
var tweenVersion = ++_presentationTweenVersion;
KillPresentationTweens();
_renderer.gameObject.SetActive(true);
TargetScaleMultiplier = Mathf.Max(0.01f, scaleMultiplier);
@@ -139,6 +141,7 @@ namespace AibisDream
var targetScale = ScaleFor(TargetScaleMultiplier);
if (duration <= 0f)
{
if (tweenVersion != _presentationTweenVersion) yield break;
ApplyPresentationImmediate(TargetScaleMultiplier, TargetAlpha);
yield break;
}
@@ -151,12 +154,20 @@ namespace AibisDream
_presentationSequence.Join(
_renderer.DOFade(TargetAlpha, duration).SetEase(Ease.InOutSine));
yield return _presentationSequence.WaitForCompletion();
// 被 set_door_closeup / 新 tween 打断后,不要回写 scale。
if (tweenVersion != _presentationTweenVersion) yield break;
_presentationSequence = null;
var transform = _renderer.transform;
transform.localPosition = _baseLocalPosition;
transform.localRotation = _baseLocalRotation;
transform.localScale = targetScale;
_renderer.SetAlpha(TargetAlpha);
}
public void ApplyPresentationImmediate(float scaleMultiplier, float alpha)
{
if (_renderer == null) return;
_presentationTweenVersion++;
KillPresentationTweens();
TargetScaleMultiplier = Mathf.Max(0.01f, scaleMultiplier);
TargetAlpha = Mathf.Clamp01(alpha);
@@ -169,18 +180,22 @@ namespace AibisDream
public void SetSpriteImmediate(Sprite sprite)
{
if (_renderer != null && sprite != null)
_renderer.sprite = sprite;
if (_renderer == null || sprite == null) return;
// Null-assign forces URP SpriteRenderer to refresh _MainTex in its property block.
_renderer.sprite = null;
_renderer.sprite = sprite;
}
public void StopPresentationAnimation()
{
_presentationTweenVersion++;
KillPresentationTweens();
}
public void ResetPresentationImmediate()
{
if (_renderer == null) return;
_presentationTweenVersion++;
KillPresentationTweens();
TargetScaleMultiplier = 1f;
TargetAlpha = _baseAlpha;
@@ -255,8 +270,17 @@ namespace AibisDream
amount = Mathf.Clamp01(amount);
if (amount <= 0.001f)
{
_renderer.SetPropertyBlock(null);
// SetPropertyBlock(null) can drop SpriteRenderer's internal _MainTex on URP.
// Clear blur overrides, restore base material, then force a sprite rebind.
_renderer.sharedMaterial = _baseMaterial;
_propertyBlock ??= new MaterialPropertyBlock();
_renderer.GetPropertyBlock(_propertyBlock);
_propertyBlock.SetFloat(BlurAmountId, 0f);
_propertyBlock.SetFloat(BlurSizeId, 0f);
_propertyBlock.SetFloat(UseSoftFocusId, 0f);
_renderer.SetPropertyBlock(_propertyBlock);
_renderer.SetPropertyBlock(null);
RebindSpriteTexture(_renderer);
return;
}
if (_focusMaterial == null)
@@ -270,9 +294,21 @@ namespace AibisDream
_propertyBlock.SetFloat(BlurAmountId, amount);
_propertyBlock.SetFloat(BlurSizeId, Mathf.Max(0f, blurSize));
_propertyBlock.SetFloat(UseSoftFocusId, 1f);
// Keep sprite texture in the block so URP SpriteRenderer does not fall back to white.
if (_renderer.sprite != null && _renderer.sprite.texture != null)
_propertyBlock.SetTexture("_MainTex", _renderer.sprite.texture);
_renderer.SetPropertyBlock(_propertyBlock);
}
private static void RebindSpriteTexture(SpriteRenderer renderer)
{
if (renderer == null) return;
var sprite = renderer.sprite;
if (sprite == null) return;
renderer.sprite = null;
renderer.sprite = sprite;
}
private Vector3 ScaleFor(float multiplier)
{
return new Vector3(
+145 -52
View File
@@ -7,18 +7,29 @@ using UnityEngine;
namespace AibisDream
{
/// <summary>
/// Day2 门演出。可见门框与门后内容分层如下,勿混用:
/// <list type="bullet">
/// <item><b>可见门框(近景)</b>:只用 SpriteShowcase 大图(白色门框/金色门框)。
/// 缩放/透明度走 set_door_closeup / tween_door_closeup,不要再 show_large_sprite「门框特写」叠一层。</item>
/// <item><b>Door/background</b>HDR 白底或门后内容,从大图透明洞透出。</item>
/// <item><b>Door/Frame</b>:场景里保持 inactive,缩放与近景大图不同;仅作 terminalShapeReference / 贴图同步,不要 SetActive 当第二层门框。</item>
/// <item><b>远景整门立绘</b>hide_dream_door + show_large_sprite「D2S门」,不走本系统 Frame。</item>
/// </list>
/// </summary>
public class DreamDoorSystem : Singleton<DreamDoorSystem>
{
[Header("Door Root")]
[Tooltip("Door 根节点。默认仅 background 和 Frame 显示,整个 Door 初始隐藏。show_door 命令激活后显示。")]
[Tooltip("Door 根节点。show_door / listen_view 时激活;内含 background、海浪等,不含「可见近景门框」。")]
[SerializeField] private Transform doorRoot;
[Header("Door Sprites")]
[SerializeField] private SpriteRenderer glassSea;
[SerializeField] private SpriteRenderer codeSea;
[SerializeField] private SpriteRenderer flashOverlay;
[Tooltip("仅放门的静态 background不要把 glassSea/codeSea/overlay 拖进来。")]
[Tooltip("门后 HDR 白底等。不要把 glassSea/codeSea/overlay 拖进来。")]
[SerializeField] private Renderer[] backgroundRenderers;
[Tooltip("场景 Frame:默认 inactive,勿当作近景可见门框。可与 terminalShapeReference 共用。")]
[SerializeField] private Renderer[] frameRenderers;
[SerializeField] private Sprite whiteDoorFrameSprite;
[SerializeField] private Sprite seaDoorFrameSprite;
@@ -48,12 +59,13 @@ namespace AibisDream
[SerializeField] private SpriteRenderer terminalShapeReference;
[SerializeField] private Vector3 seaCollapseScale = new(0.9f, 0.84f, 1f);
[SerializeField] private Vector3 doorSnapScale = new(0.95f, 1.06f, 1f);
[SerializeField] private Ease morphEase = Ease.InOutCubic;
[SerializeField] private Ease morphEase = Ease.OutBack;
[SerializeField] private FrameHidePhase frameHidePhase = FrameHidePhase.OnTerminal;
[SerializeField] private float doorSnapDuration = 0.2f;
[SerializeField] private float backgroundCutDelay = 0.04f;
[SerializeField] private float preMorphHoldDuration = 0.16f;
[SerializeField] private float postMorphBootDelay = 0.12f;
[Tooltip("变形前停顿,稍长一点更有「蓄力→啪」的顿挫。")]
[SerializeField] private float preMorphHoldDuration = 0.22f;
[SerializeField] private float postMorphBootDelay = 0.06f;
[SerializeField] private float referenceSnapMorphProgress = 0.08f;
[SerializeField] private Vector2 referenceSnapPadding = new(0.18f, 0.18f);
[Tooltip("终端变形时 sceneBlackOverlay 淡入目标透明度(背景压暗)")]
@@ -63,6 +75,12 @@ namespace AibisDream
[SerializeField] private float blackoutFadeOutDuration = 0.12f;
[Tooltip("变形为终端开始时并行播放的 TimelineTimelineCenter 名称,如 姐姐/头痛;留空则不播)")]
[SerializeField] private string morphStartTimelineName = "姐姐/Glitch";
[Tooltip("门框→终端变形时长。偏短配合 OutBack 更容易做出顿挫。")]
[SerializeField, Min(0.05f)] private float morphDuration = 0.45f;
[Header("Ambient")]
[Tooltip("变门/终端时立刻关掉的场景背景层(如星空)。须在 HideLargeFrame 之前关掉,否则大图一收会穿帮。")]
[SerializeField] private GameObject[] hideOnTerminal;
private Transform _codeSeaTransform;
private Vector3 _codeSeaOriginalLocalPosition;
@@ -78,6 +96,7 @@ namespace AibisDream
private string _glassSeaCurrentStateName;
private MaterialPropertyBlock _codeSeaPropertyBlock;
private bool _codeSeaPlaybackActive;
private Coroutine _closeupCoroutine;
private static readonly int WaveStartTimeId = Shader.PropertyToID("_WaveStartTime");
private static readonly int WavePhaseId = Shader.PropertyToID("_WavePhase");
private static readonly int WaveSpeedId = Shader.PropertyToID("_WaveSpeed");
@@ -120,7 +139,8 @@ namespace AibisDream
glassSea.gameObject.SetActive(false);
}
SetDoorFrameSprite(hasSea: false);
SyncSceneFrameSprite(hasSea: false);
KeepSceneFrameInactive();
if (codeSea != null)
{
@@ -173,7 +193,8 @@ namespace AibisDream
}
/// <summary>
/// 进入“仔细听”的门近景。首次是白底门,之后直接显示门后的金色海浪。
/// 门近景:大图显示白/金色门框 + Door 内 background/海浪。
/// Yarn 无需再先 show_large_sprite「门框特写」(会被本命令覆盖)。
/// </summary>
public IEnumerator ShowListenView(bool showSea, float duration = 0f)
{
@@ -181,8 +202,7 @@ namespace AibisDream
EnsureDoorBaseVisible(showSea);
ResetCodeSeaImmediate();
ResetTerminalImmediate();
Day2SleepPresentationController.Instance?.SetLargePresentationImmediate(1f, 1f);
ShowLargeFrame(hasSea: showSea, scale: 1f, alpha: 1f);
if (showSea && glassSea != null)
{
@@ -261,6 +281,7 @@ namespace AibisDream
/// </summary>
public IEnumerator TweenListenCloseup(float scale, float alpha, float duration)
{
StopCloseupCoroutine();
var presentation = Day2SleepPresentationController.Instance;
if (presentation == null)
yield break;
@@ -268,13 +289,43 @@ namespace AibisDream
yield return presentation.AnimateLargePresentation(scale, alpha, duration);
}
/// <summary>异步近景;再次调用 set/tween/show_framed 时会取消上一次。</summary>
public void TweenListenCloseupAsync(float scale, float alpha, float duration)
{
StopCloseupCoroutine();
if (Day2SleepPresentationController.Instance == null)
return;
_closeupCoroutine = StartCoroutine(TweenListenCloseupRoutine(scale, alpha, duration));
}
public void SetListenCloseup(float scale, float alpha)
{
StopCloseupCoroutine();
Day2SleepPresentationController.Instance?.SetLargePresentationImmediate(scale, alpha);
}
private IEnumerator TweenListenCloseupRoutine(float scale, float alpha, float duration)
{
var presentation = Day2SleepPresentationController.Instance;
if (presentation == null)
yield break;
yield return presentation.AnimateLargePresentation(scale, alpha, duration);
_closeupCoroutine = null;
}
private void StopCloseupCoroutine()
{
if (_closeupCoroutine != null)
{
StopCoroutine(_closeupCoroutine);
_closeupCoroutine = null;
}
Day2SleepPresentationController.Instance?.StopLargePresentationAnimation();
}
/// <summary>
/// 全白遮罩下调用的瞬时状态切换:隐藏门底门框,仅显示全屏金色海浪。
/// 全白:隐藏门底与大图门框,仅全屏金色海浪。
/// </summary>
public void ShowGlassSeaFullscreen()
{
@@ -282,23 +333,25 @@ namespace AibisDream
doorRoot.gameObject.SetActive(true);
SetBackgroundVisible(false);
SetFrameVisible(false);
HideLargeFrame();
ResetCodeSeaImmediate();
EnsureGlassSeaVisible(glassSeaStateName);
}
/// <summary>
/// 全白遮罩下调用的瞬时状态切换:恢复门底和门框,金色海浪留在门后。
/// 全白时:恢复大图门框,金色海浪留在门后。
/// </summary>
public void ShowGlassSeaFramed()
{
StopCloseupCoroutine();
ResetCodeSeaImmediate();
EnsureDoorBaseVisible(hasSea: true);
ShowLargeFrame(hasSea: true, scale: 1f, alpha: 1f);
EnsureGlassSeaVisible(glassSeaFramedStateName);
}
/// <summary>
/// 全白时切到无门的全屏 CodeSea;状态职责与 ShowGlassSeaFullscreen 一致
/// 全白时:隐藏门底与大图门框,全屏 CodeSea
/// </summary>
public void ShowCodeSeaFullscreen()
{
@@ -308,20 +361,23 @@ namespace AibisDream
var restartPlayback = _presentationPhase != DoorPresentationPhase.CodeSea
|| !_codeSeaPlaybackActive;
SetBackgroundVisible(false);
SetFrameVisible(false);
HideLargeFrame();
ResetGlassSeaImmediate();
EnsureCodeSeaVisible(restartPlayback);
}
/// <summary>
/// 全白时恢复门框CodeSea 保持显示在门内。
/// 全白时恢复大图白色门框(scale=1CodeSea 在门内。
/// 会取消进行中的 tween_door_closeup_async,避免停在 4x 放大。
/// </summary>
public void ShowCodeSeaFramed()
{
var restartPlayback = _presentationPhase != DoorPresentationPhase.CodeSea
|| !_codeSeaPlaybackActive;
StopCloseupCoroutine();
ResetGlassSeaImmediate();
EnsureDoorBaseVisible(hasSea: false);
ShowLargeFrame(hasSea: false, scale: 1f, alpha: 1f);
EnsureCodeSeaFramedVisible(restartPlayback);
}
@@ -362,7 +418,7 @@ namespace AibisDream
_presentationPhase = DoorPresentationPhase.CodeSea;
if (frameHidePhase == FrameHidePhase.OnCodeSea)
SetFrameVisible(false);
HideLargeFrame();
var seq = DOTween.Sequence();
seq.Append(codeSea.DOFade(1f, Mathf.Max(0f, duration)).SetEase(Ease.InOutSine));
@@ -408,6 +464,9 @@ namespace AibisDream
{
StopAllEffects();
// 先关星空等远景层,再收大图;否则 D2S门 一隐星空会露出来穿帮。
HideAmbientForTerminal();
if (terminalUI != null)
{
terminalUI.ResetVisualState();
@@ -422,7 +481,7 @@ namespace AibisDream
}
SetBackgroundVisible(false);
SetFrameVisible(false);
HideLargeFrame();
if (glassSea != null)
{
@@ -454,11 +513,12 @@ namespace AibisDream
if (terminalUI != null)
{
float morph = 0f;
// OutBack 带轻微过冲,配合变短的 morphDuration,落地更有顿挫。
var morphTween = DOTween.To(() => morph, v =>
{
morph = v;
terminalUI.MorphProgress = v;
}, 1f, 0.82f).SetEase(morphEase);
}, 1f, Mathf.Max(0.05f, morphDuration)).SetEase(morphEase);
if (!string.IsNullOrEmpty(morphStartTimelineName))
TimelineCenter.Instance.PlayTimeline(morphStartTimelineName);
@@ -467,6 +527,7 @@ namespace AibisDream
sceneBlackOverlay.DOFade(blackoutAlpha, Mathf.Max(0.01f, blackoutFadeInDuration));
yield return morphTween.WaitForCompletion();
terminalUI.MorphProgress = 1f;
}
ResetDoorShellScale();
@@ -507,6 +568,7 @@ namespace AibisDream
public void StopAllEffects()
{
StopCloseupCoroutine();
DOTween.Kill(glassSea);
DOTween.Kill(_codeSeaTransform);
DOTween.Kill(codeSea);
@@ -527,8 +589,8 @@ namespace AibisDream
ResetListenTransitionImmediate();
HideDoor();
SetBackgroundVisible(true);
SetFrameVisible(true);
SetDoorFrameSprite(hasSea: false);
SyncSceneFrameSprite(hasSea: false);
KeepSceneFrameInactive();
ResetDoorShellScale();
if (glassSea != null)
@@ -566,7 +628,7 @@ namespace AibisDream
if (sceneBlackOverlay != null)
sceneBlackOverlay.SetAlpha(0);
Day2SleepPresentationController.Instance?.HideLargePresentationImmediate();
HideLargeFrame();
_presentationPhase = DoorPresentationPhase.Hidden;
}
@@ -575,17 +637,77 @@ namespace AibisDream
#region Presentation State
/// <summary>
/// 打开 Door 根与门后背景;不同时显示场景 Frame(近景门框只走大图)。
/// </summary>
private void EnsureDoorBaseVisible(bool hasSea = false)
{
if (doorRoot != null)
doorRoot.gameObject.SetActive(true);
// 梦境「遮罩」是纯黑全屏;不关会从门框透明洞透出黑底,盖住 HDR 白光。
SpriteShowcase.Instance?.HideBackgroundImmediate();
SetBackgroundVisible(true);
SetFrameVisible(true);
SetDoorFrameSprite(hasSea);
KeepSceneFrameInactive();
SyncSceneFrameSprite(hasSea);
ResetDoorShellScale();
}
/// <summary>可见门框:大图贴白/金色门框并设缩放透明度。</summary>
private void ShowLargeFrame(bool hasSea, float scale, float alpha)
{
var targetSprite = hasSea ? seaDoorFrameSprite : whiteDoorFrameSprite;
SyncSceneFrameSprite(hasSea);
var presentation = Day2SleepPresentationController.Instance;
if (presentation == null) return;
if (targetSprite != null)
presentation.SetLargeSpriteImmediate(targetSprite);
presentation.SetLargePresentationImmediate(scale, alpha);
}
private void HideLargeFrame()
{
Day2SleepPresentationController.Instance?.HideLargePresentationImmediate();
}
/// <summary>只同步场景 Frame 贴图,不激活(供 morph 参考一致)。</summary>
private void SyncSceneFrameSprite(bool hasSea)
{
var targetSprite = hasSea ? seaDoorFrameSprite : whiteDoorFrameSprite;
if (targetSprite == null || frameRenderers == null) return;
foreach (var frameRenderer in frameRenderers)
{
if (frameRenderer is SpriteRenderer spriteRenderer)
spriteRenderer.sprite = targetSprite;
}
}
/// <summary>
/// 场景 Frame 必须保持 inactive。其 localScale≈0.73,若被激活会叠出一层「缩小门框」。
/// </summary>
private void KeepSceneFrameInactive()
{
if (frameRenderers == null) return;
foreach (var frameRenderer in frameRenderers)
{
if (frameRenderer == null) continue;
if (frameRenderer.gameObject.activeSelf)
frameRenderer.gameObject.SetActive(false);
frameRenderer.enabled = true;
}
}
private void HideAmbientForTerminal()
{
if (hideOnTerminal == null) return;
foreach (var go in hideOnTerminal)
{
if (go != null && go.activeSelf)
go.SetActive(false);
}
}
private void ResetGlassSeaImmediate()
{
DOTween.Kill(glassSea);
@@ -832,35 +954,6 @@ namespace AibisDream
#endregion
private void SetFrameVisible(bool visible)
{
if (frameRenderers == null) return;
foreach (var frameRenderer in frameRenderers)
{
if (frameRenderer != null)
frameRenderer.enabled = visible;
}
}
private void SetDoorFrameSprite(bool hasSea)
{
var targetSprite = hasSea ? seaDoorFrameSprite : whiteDoorFrameSprite;
if (targetSprite == null)
return;
if (frameRenderers != null)
{
foreach (var frameRenderer in frameRenderers)
{
if (frameRenderer is SpriteRenderer spriteRenderer)
spriteRenderer.sprite = targetSprite;
}
}
Day2SleepPresentationController.Instance?.SetLargeSpriteImmediate(targetSprite);
}
private void SetBackgroundVisible(bool visible)
{
if (backgroundRenderers == null) return;
@@ -72,7 +72,8 @@ namespace AibisDream
{
var system = ResolveSystem();
if (system == null) return;
system.StartCoroutine(system.TweenListenCloseup(scale, alpha, duration));
// 走系统内可取消入口,避免多个 async closeup 叠跑把 scale 卡在放大态。
system.TweenListenCloseupAsync(scale, alpha, duration);
}
[YarnCommand("set_door_closeup")]
@@ -30,6 +30,14 @@ namespace AibisDream
private bool IsSmallShowing => _spriteRenderer != null && _spriteRenderer.gameObject.activeSelf;
private bool IsLargeShowing => _largeSpriteRenderer != null && _largeSpriteRenderer.gameObject.activeSelf;
/// <summary>
/// 立即隐藏全屏遮罩(黑底)。门近景透明区域需要透出 Door HDR 白底时调用。
/// </summary>
public void HideBackgroundImmediate()
{
SetBackgroundImmediate(false);
}
public override void OnSingletonInit()
{
ClearSmallImmediate();
@@ -126,6 +134,16 @@ namespace AibisDream
yield break;
}
// Force _MainTex rebind: assigning the same Sprite after SetPropertyBlock(null)
// can leave URP Sprite-Lit with a null MainTex (pure white).
var assigned = _largeSpriteRenderer.sprite;
_largeSpriteRenderer.sprite = null;
_largeSpriteRenderer.sprite = assigned;
// 门近景 closeup 可能把 large 留在 2x/4x;换图时强制回到 1x。
Day2SleepPresentationController.Instance?.ResetLargePresentationImmediate();
_largeSpriteRenderer.gameObject.SetActive(true);
_currentLargePicName = NormalizeDreamPicName(picName);
if (CommonUtil.IsZero(duration))
_largeSpriteRenderer.SetAlpha(1f);
@@ -109,6 +109,7 @@ namespace AibisDream
return;
}
EnsureStarfieldActive();
director.Stop();
SetTimelineTime(0f);
}
@@ -120,6 +121,8 @@ namespace AibisDream
yield break;
}
EnsureStarfieldActive();
float startTime;
float endTime;
switch (attempt)
@@ -159,6 +162,7 @@ namespace AibisDream
yield break;
}
EnsureStarfieldActive();
director.Pause();
SetTimelineTime(blurFadeStart);
director.Play();
@@ -195,9 +199,23 @@ namespace AibisDream
return;
}
// Fiction 等路径可能只 play_timeline、不走 starfield_prepare
// Timeline 只 key 了「海浪」的 Active,不会打开「星空」。
if (director.state == PlayState.Playing)
EnsureStarfieldActive();
ApplyAtTime((float)director.time);
}
/// <summary>
/// 场景「星空」默认 inactive(与「海浪」一致)。演出开始时打开。
/// </summary>
private void EnsureStarfieldActive()
{
if (starfieldRenderer != null && !starfieldRenderer.gameObject.activeSelf)
starfieldRenderer.gameObject.SetActive(true);
}
private bool TryValidateReferences()
{
if (director == null)
@@ -145,7 +145,8 @@ namespace AibisDream.UI
public float MorphProgress
{
get => morphProgress;
set => morphProgress = Mathf.Clamp01(value);
// 允许略超 1,配合 OutBack 过冲;收束时由门系统写回 1。
set => morphProgress = Mathf.Clamp(value, 0f, 1.35f);
}
#endregion
@@ -254,8 +255,7 @@ namespace AibisDream.UI
public Vector2 EvaluateFrameSize(float progress)
{
float frameProgress = Mathf.SmoothStep(0f, 1f, Mathf.Clamp01(progress));
return Vector2.Lerp(morphStartSize, terminalSize, frameProgress);
return Vector2.LerpUnclamped(morphStartSize, terminalSize, progress);
}
[ContextMenu("Preview/Frame Only")]
@@ -299,9 +299,10 @@ namespace AibisDream.UI
if (Alpha <= 0.01f) return;
float frameProgress = Mathf.SmoothStep(0f, 1f, MorphProgress);
// 不用 SmoothStep 二次柔化;允许 >1 以保留 OutBack 过冲顿挫。
float frameProgress = MorphProgress;
float textReveal = frameProgress >= 0.999f ? 1f : 0f;
Vector2 currentSize = Vector2.Lerp(morphStartSize, terminalSize, frameProgress);
Vector2 currentSize = Vector2.LerpUnclamped(morphStartSize, terminalSize, frameProgress);
using (Draw.Command(cam))
{
@@ -1007,9 +1008,9 @@ namespace AibisDream.UI
private void DrawEditorScenePreview()
{
float frameProgress = Mathf.SmoothStep(0f, 1f, MorphProgress);
float frameProgress = MorphProgress;
float textReveal = frameProgress >= 0.999f ? 1f : 0f;
Vector2 currentSize = Vector2.Lerp(morphStartSize, terminalSize, frameProgress);
Vector2 currentSize = Vector2.LerpUnclamped(morphStartSize, terminalSize, frameProgress);
Matrix4x4 previousMatrix = Handles.matrix;
Color previousColor = Handles.color;