Merge branch 'develop' into feature/增加结尾保存节点
This commit is contained in:
@@ -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);
|
||||
|
||||
// 强制收线不能复用 ResetPlug:ReturnToStartPosition 会启动线缆的自动回收,
|
||||
// 紧接着再 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -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 铺 noise;begin 时会把 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++)
|
||||
{
|
||||
|
||||
@@ -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 → fix(cue)→ fixPanel → bodyModule → eye → showcase → day2SleepPresentation → playTool → screen。`fixPanel` 须在 `fix` 之后,以覆盖 Cue `EnterImmediate` 中的 `ResetPlug`;D2 动态表现须在通用 Showcase 图片之后恢复,才能叠加虚焦、缩放或场景专属动画。
|
||||
**还原顺序**:punchTape → fix(cue)→ fixPanel → bodyModule → eye → showcase → day2SleepPresentation → playTool → screen。`fixPanel` 须在 `fix` 之后,以覆盖 Cue `EnterImmediate` 中的 `ResetPlug`,并让 Memory Cue 先建立 TaskPanel 临时隐藏状态;D2 动态表现须在通用 Showcase 图片之后恢复,才能叠加虚焦、缩放或场景专属动画。
|
||||
|
||||
**维修场景门控**(`FixSceneSnapshotHelper`):上述 section 仅在 `FixSystemCenter.Instance != null` 时 Capture/Restore。
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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("变形为终端开始时并行播放的 Timeline(TimelineCenter 名称,如 姐姐/头痛;留空则不播)")]
|
||||
[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=1),CodeSea 留在门内。
|
||||
/// 会取消进行中的 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;
|
||||
|
||||
Reference in New Issue
Block a user