Merge branch '7.27night春晖优化' into 'develop'

fix(fix-system): 修正线盘松弛预判朝向与绷紧判定

See merge request aibis-dream/aibis-dream!801
This commit is contained in:
2026-07-27 17:16:14 +00:00
19 changed files with 3720 additions and 4089 deletions
@@ -92,10 +92,11 @@ namespace AibisDream.FixSystem
return;
}
// 线缆还有余量时不驱动转盘,避免"还没拉紧就先转"
// 松弛时不驱动转盘,并缓动回正,避免出线口对着插头“预判”朝向
if (tautness <= 0f)
{
angularVelocity *= Mathf.Exp(-angularDamping * Time.deltaTime);
angularVelocity = 0f;
currentAngle = Mathf.LerpAngle(currentAngle, 0f, returnSpeed * Time.deltaTime);
ApplyRotation();
return;
}
@@ -96,16 +96,21 @@ public class PhysicCable : MonoBehaviour
public float CurrentLength => _length;
/// <summary>0=完全松弛,1=绷直。线盘只在真正拉紧后才应转动。</summary>
public float GetTautness(float slackMargin = 0.35f)
/// <summary>
/// 0=完全松弛,1=绷直。已扣除送线故意留的余量(1.04×直线距 + lengthMargin),
/// 避免「有设计余量就永远不转」;线盘只在真正被拽时才应转动。
/// </summary>
public float GetTautness(float extraSlackMargin = 0.2f)
{
if (State != CableState.Dragging && State != CableState.Plugged) return 0f;
if (_pts == null || startTransform == null) return 0f;
float straightDist = Vector3.Distance(startTransform.position, FlattenZ(EndPosition));
float slack = _length - straightDist;
if (slack >= slackMargin) return 0f;
return 1f - Mathf.Clamp01(slack / slackMargin);
float baselineLength = straightDist * 1.04f + lengthMargin;
float extraSlack = _length - baselineLength;
if (extraSlack >= extraSlackMargin) return 0f;
if (extraSlack <= 0f) return 1f;
return 1f - extraSlack / extraSlackMargin;
}
private struct Particle
@@ -199,15 +199,14 @@ namespace AibisDream.FixSystem
private void Update()
{
// 拖拽/插接时转盘朝插头方向转动,闲置时缓动回正
// 不再要求线缆完全绷紧,否则送线会让 tautness 长时间为 0,视觉上看不到旋转。
// 拖拽时由拉力驱动出线口倾斜;插接/松弛不预判朝向插头
// GetTautness 已扣除送线设计余量,绷紧时才会转。
if (CableReelRef != null && physicCableRef != null
&& physicCableRef.State != PhysicCable.CableState.Hidden)
{
bool active = physicCableRef.State == PhysicCable.CableState.Dragging
|| physicCableRef.State == PhysicCable.CableState.Plugged
|| physicCableRef.State == PhysicCable.CableState.Collapsing;
CableReelRef.UpdateRotation(active);
bool active = physicCableRef.State == PhysicCable.CableState.Dragging;
float tautness = active ? physicCableRef.GetTautness() : 0f;
CableReelRef.UpdateRotation(active, tautness);
}
}
@@ -125,9 +125,9 @@ namespace AibisDream.MiniGame.Language
[Tooltip("可选:场景中一块与屏幕内框对齐的 RectTransformWorld Space Canvas 子物体即可)。不指定则用粒子用的 movementBoundscanvasSize - textMargin")]
[SerializeField] private RectTransform completionScreenClipRect;
[Tooltip("在裁剪区基础上再向内缩进的世界单位距离,避免放大后的字半个挂在边外")]
[SerializeField] private float completionClipEdgePadding = 0.2f;
[SerializeField] private float completionClipEdgePadding = 0.35f;
[Tooltip("屏幕内容的最小安全边距;最终边距还会根据字形、光晕、圆环和晃动幅度动态增大。")]
[SerializeField, Min(0f)] private float minimumScreenMargin = 0.12f;
[SerializeField, Min(0f)] private float minimumScreenMargin = 0.2f;
[Tooltip("将目标粒子拉回屏幕裁剪区时的平滑时间(越大越柔和,略增加拖尾感)")]
[SerializeField, Range(0.02f, 0.55f)] private float completionPositionSmoothTime = 0.2f;
@@ -1051,24 +1051,30 @@ namespace AibisDream.MiniGame.Language
{
float padding = Mathf.Max(minimumScreenMargin, completionClipEdgePadding);
float glyphExtent = 0f;
float maxScale = 1f;
foreach (CandidateParticle particle in targetParticles)
{
if (particle == null)
continue;
maxScale = Mathf.Max(maxScale, particle.transform.localScale.x, particle.transform.localScale.y);
if (particle.textMesh != null && particle.textMesh.renderer != null)
{
Bounds textBounds = particle.textMesh.renderer.bounds;
glyphExtent = Mathf.Max(glyphExtent, textBounds.extents.x, textBounds.extents.y);
}
}
float zoomedGlyph = glyphExtent * Mathf.Max(1f, focusZoomScale);
float shakeExtent = Mathf.Max(shuffleDistance, nonTargetShakeIntensity, completionPositionSmoothTime * 0.1f);
// renderer.bounds 已含当前缩放;未放大到 focusZoom 时再预留缩放余量,避免重复乘 zoom
float resolvedGlyph = maxScale >= focusZoomScale * 0.9f
? glyphExtent
: glyphExtent * Mathf.Max(1f, focusZoomScale);
// 抖动/漂浮是局部偏移,硬钳制根节点后仍可能露出字形一角,留一点余量即可
float floatAllowance = Mathf.Max(0.04f, focusInterferenceShakeAmplitude * 0.35f);
float horizontal = Mathf.Max(padding, zoomedGlyph, shakeExtent);
float horizontal = padding + resolvedGlyph + floatAllowance;
return new Vector2(horizontal, horizontal);
}
@@ -1635,8 +1641,10 @@ namespace AibisDream.MiniGame.Language
Mathf.Lerp(1f, 0.12f, gather);
Vector3 edgeJitter = CalculateLieEdgeJitter(visual, compression);
visual.scatterOffset = Vector3.Lerp(visual.scatterOffset, Vector3.zero, Time.deltaTime * 5.5f);
visual.particle.transform.position =
compressedPosition + shuffleOffset + edgeJitter + visual.scatterOffset;
// 硬钳制:shuffle/jitter 会顶出裁剪区,仅靠 LateUpdate SmoothDamp 追不上,字会贴边甚至出屏
visual.particle.transform.position = ClampPositionToBounds(
compressedPosition + shuffleOffset + edgeJitter + visual.scatterOffset,
GetCompletionClipBounds());
// 锁字判定用原始进度:target=0.92 时阈值 0.95 的最后一个字永远锁不住
UpdateTruthGatherLock(visual, truthGatherProgress, visualCount);
@@ -1843,7 +1851,9 @@ namespace AibisDream.MiniGame.Language
Vector3 dynamicTarget = GetDynamicArrangementTarget(visual, arrangementProgress, localTime);
Vector3 targetPosition = Vector3.Lerp(visual.focusPosition, dynamicTarget, Mathf.SmoothStep(0f, 1f, arrangementProgress));
Vector3 shuffleOffset = CalculateShuffleOffset(visual, arrangementProgress, localTime);
visual.particle.transform.position = targetPosition + shuffleOffset;
visual.particle.transform.position = ClampPositionToBounds(
targetPosition + shuffleOffset,
GetCompletionClipBounds());
if (!visual.stabilized && probabilityProgress >= 1f)
{
@@ -25,10 +25,10 @@ namespace AibisDream.MiniGame.HuoShan
[SerializeField] private float innerRadius = 1.5f;
[Tooltip("滑片滑动弧度范围(度)")]
[SerializeField] private float slideAngleRange = 330f;
[SerializeField] private float slideAngleRange = 340.8f;
[Tooltip("滑片起始角度偏移(度);270=正上方起点")]
[SerializeField] private float startAngleOffset = 270f;
[Tooltip("滑片起始角度偏移(度);必须与进入步进模式动画结束时滑片停的位置一致,否则接管控制时会跳位")]
[SerializeField] private float startAngleOffset = 259.6f;
[Header("滑片配置")]
[Tooltip("滑片Transform")]
@@ -119,6 +119,7 @@ namespace AibisDream
/// <summary>
/// 换目标时的失焦:只做效果,不眨眼(眨眼仅在想象上色 FadeIn)。
/// CanImagineColor 下若颜色已是失焦态,仍需补 BlurIn,否则未上色过的目标会一直保持清晰。
/// </summary>
private void ApplyUnfocusedEffectSilent(EyeTarget target)
{
@@ -130,6 +131,7 @@ namespace AibisDream
if (_currentState == EyeColorState.CanImagineColor
&& target.IsAtUnfocusedColorState(visualSettings))
{
target.BlurIn(GetEffectTransitionDuration());
return;
}