diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/FLine.cs b/Assets/Scripts/Fishing/New/View/FishingLine/FLine.cs
index e69de29bb..3d27cf64c 100644
--- a/Assets/Scripts/Fishing/New/View/FishingLine/FLine.cs
+++ b/Assets/Scripts/Fishing/New/View/FishingLine/FLine.cs
@@ -0,0 +1,1045 @@
+using System;
+using UnityEngine;
+using System.Collections.Generic;
+using NBC;
+
+namespace NBF
+{
+ public struct FLineNodeConstraintState
+ {
+ public Vector3 TargetPosition;
+ public float PlanarStrength;
+ public float UpwardStrength;
+ public float DownwardStrength;
+ public float MaxCorrection;
+ }
+
+ ///
+ /// 为 FLine 节点提供附加软约束。求解器只依赖这个通用接口,不依赖具体玩法组件。
+ ///
+ public interface IFLineNodeConstraintProvider
+ {
+ bool TryGetConstraintState(out FLineNodeConstraintState state);
+ }
+
+ public enum LineType
+ {
+ Spinning,
+ SpinningFloat,
+ Hand,
+ HandDouble,
+ }
+
+ public class FLine : FGearBase
+ {
+ [Header("基本参数设置")] public LineType LineType;
+ [Header("连接点配置")] [SerializeField] private Transform anchorTransform;
+ [SerializeField] private List lineNodes = new List();
+
+ [Header("物理参数")] [SerializeField] private float positionCorrectionForce = 100f;
+ [SerializeField] private float dampingCoefficient = 10f;
+ [SerializeField] private int constraintIterations = 10;
+ [SerializeField] private bool useMassWeighting = true;
+
+ [Header("动态间距设置")] [SerializeField] private float defaultTransitionSpeed = 2f; // 默认长度变化速度(单位/秒)
+
+ [Header("调试")] [SerializeField] private bool showDebugInfo = true;
+
+ private readonly List _constraints = new List();
+
+ private readonly List _nodeConstraintProviders =
+ new List();
+
+ [System.Serializable]
+ public class ConnectionConstraint
+ {
+ public Rigidbody bodyA;
+ public Rigidbody bodyB;
+ public float maxDistance;
+ public float minDistance;
+ public float currentDistance;
+ public Vector3 direction;
+
+ // 动态目标距离(用于平滑过渡)
+ public float targetMaxDistance;
+ public float targetMinDistance;
+ public bool hasPendingTransition;
+ public bool hasPendingMaxTransition;
+ public bool hasPendingMinTransition;
+ public float maxTransitionSpeed;
+ public float minTransitionSpeed;
+
+ public ConnectionConstraint(Rigidbody a, Rigidbody b, float maxDist, float minDist = 0f)
+ {
+ bodyA = a;
+ bodyB = b;
+ maxDistance = maxDist;
+ minDistance = minDist;
+ targetMaxDistance = maxDist;
+ targetMinDistance = minDist;
+ hasPendingTransition = false;
+ hasPendingMaxTransition = false;
+ hasPendingMinTransition = false;
+ maxTransitionSpeed = 0f;
+ minTransitionSpeed = 0f;
+ }
+
+ public void UpdateCurrentState()
+ {
+ if (bodyA && bodyB)
+ {
+ Vector3 delta = bodyB.position - bodyA.position;
+ currentDistance = delta.magnitude;
+ direction = currentDistance > 0.0001f ? delta.normalized : Vector3.right;
+ }
+ }
+ }
+
+ void Awake()
+ {
+ BuildConstraints();
+ }
+
+ protected override void OnInit()
+ {
+ anchorTransform = Rod.Asset.lineConnector;
+ }
+
+ void Start()
+ {
+ if (_constraints.Count == 0)
+ {
+ Debug.LogWarning("FLine需要至少2个有效节点");
+ enabled = false;
+ }
+ }
+
+ void FixedUpdate()
+ {
+ if (!enabled || _constraints.Count == 0) return;
+
+ UpdateAnchorNode();
+ // 更新动态过渡
+ UpdateTransitions();
+
+ for (int iteration = 0; iteration < constraintIterations; iteration++)
+ {
+ ApplyDistanceConstraints();
+ ApplyNodeConstraints();
+ }
+
+ ApplyDamping();
+ UpdateBreakCountdown(Time.fixedDeltaTime);
+ }
+
+ private void BuildConstraints()
+ {
+ _constraints.Clear();
+ _nodeConstraintProviders.Clear();
+ if (lineNodes.Count < 2) return;
+
+ for (int i = 0; i < lineNodes.Count; i++)
+ {
+ if (lineNodes[i])
+ {
+ lineNodes[i].AttachToCable(this);
+ _nodeConstraintProviders.Add(CollectNodeConstraintProviders(lineNodes[i]));
+ }
+ else
+ {
+ _nodeConstraintProviders.Add(Array.Empty());
+ }
+ }
+
+ // 创建约束
+ for (int i = 0; i < lineNodes.Count - 1; i++)
+ {
+ FLineLogicNode currentNode = lineNodes[i];
+ FLineLogicNode nextNode = lineNodes[i + 1];
+ Rigidbody bodyA = currentNode ? currentNode.Rigidbody : null;
+ Rigidbody bodyB = nextNode ? nextNode.Rigidbody : null;
+
+ if (bodyA != null && bodyB != null)
+ {
+ var constraint = new ConnectionConstraint(
+ bodyA,
+ bodyB,
+ currentNode.NextSegmentMaxLength,
+ currentNode.NextSegmentMinLength
+ );
+ _constraints.Add(constraint);
+ }
+ }
+ }
+
+ private FLineLogicNode GetSegmentNode(int segmentIndex)
+ {
+ if (segmentIndex < 0 || segmentIndex >= lineNodes.Count - 1)
+ {
+ return null;
+ }
+
+ return lineNodes[segmentIndex];
+ }
+
+ private Rigidbody GetBodyAt(int nodeIndex)
+ {
+ if (nodeIndex < 0 || nodeIndex >= lineNodes.Count)
+ {
+ return null;
+ }
+
+ FLineLogicNode node = lineNodes[nodeIndex];
+ return node ? node.Rigidbody : null;
+ }
+
+ private IFLineNodeConstraintProvider[] GetNodeConstraintProvidersAt(int nodeIndex)
+ {
+ if (nodeIndex < 0 || nodeIndex >= _nodeConstraintProviders.Count)
+ {
+ return Array.Empty();
+ }
+
+ return _nodeConstraintProviders[nodeIndex];
+ }
+
+ private static IFLineNodeConstraintProvider[] CollectNodeConstraintProviders(FLineLogicNode node)
+ {
+ MonoBehaviour[] behaviours = node.GetComponents();
+ List providers = new List();
+
+ for (int i = 0; i < behaviours.Length; i++)
+ {
+ MonoBehaviour behaviour = behaviours[i];
+ if (behaviour is IFLineNodeConstraintProvider provider)
+ {
+ providers.Add(provider);
+ }
+ }
+
+ return providers.ToArray();
+ }
+
+ private void SyncSegmentMaxLength(int segmentIndex, float maxLength)
+ {
+ FLineLogicNode node = GetSegmentNode(segmentIndex);
+ if (node)
+ {
+ node.NextSegmentMaxLength = maxLength;
+ }
+ }
+
+ private void SyncSegmentMinLength(int segmentIndex, float minLength)
+ {
+ FLineLogicNode node = GetSegmentNode(segmentIndex);
+ if (node)
+ {
+ node.NextSegmentMinLength = minLength;
+ }
+ }
+
+ private bool ContainsBody(Rigidbody targetBody)
+ {
+ for (int i = 0; i < lineNodes.Count; i++)
+ {
+ if (GetBodyAt(i) == targetBody)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ ///
+ /// 更新所有活跃的过渡
+ ///
+ private void UpdateTransitions()
+ {
+ float deltaTime = Time.fixedDeltaTime;
+
+ for (int i = 0; i < _constraints.Count; i++)
+ {
+ var constraint = _constraints[i];
+
+ if (constraint.hasPendingMaxTransition)
+ {
+ float nextMaxDistance = Mathf.MoveTowards(
+ constraint.maxDistance,
+ constraint.targetMaxDistance,
+ constraint.maxTransitionSpeed * deltaTime
+ );
+ constraint.maxDistance = nextMaxDistance;
+ SyncSegmentMaxLength(i, nextMaxDistance);
+
+ if (Mathf.Abs(nextMaxDistance - constraint.targetMaxDistance) < 0.0001f)
+ {
+ constraint.maxDistance = constraint.targetMaxDistance;
+ constraint.hasPendingMaxTransition = false;
+ SyncSegmentMaxLength(i, constraint.targetMaxDistance);
+ }
+ }
+
+ if (constraint.hasPendingMinTransition)
+ {
+ float nextMinDistance = Mathf.MoveTowards(
+ constraint.minDistance,
+ constraint.targetMinDistance,
+ constraint.minTransitionSpeed * deltaTime
+ );
+ constraint.minDistance = nextMinDistance;
+ SyncSegmentMinLength(i, nextMinDistance);
+
+ if (Mathf.Abs(nextMinDistance - constraint.targetMinDistance) < 0.0001f)
+ {
+ constraint.minDistance = constraint.targetMinDistance;
+ constraint.hasPendingMinTransition = false;
+ SyncSegmentMinLength(i, constraint.targetMinDistance);
+ }
+ }
+
+ constraint.hasPendingTransition =
+ constraint.hasPendingMaxTransition || constraint.hasPendingMinTransition;
+ }
+ }
+
+ private void ApplyDistanceConstraints()
+ {
+ for (int i = 0; i < _constraints.Count; i++)
+ {
+ var constraint = _constraints[i];
+ if (!constraint.bodyA || !constraint.bodyB) continue;
+
+ constraint.UpdateCurrentState();
+
+ float currentDist = constraint.currentDistance;
+ float maxDist = constraint.maxDistance;
+ float minDist = constraint.minDistance;
+
+ float error = 0f;
+ bool needCorrection = false;
+
+ if (currentDist > maxDist)
+ {
+ error = currentDist - maxDist;
+ needCorrection = true;
+ }
+
+ if (!needCorrection || Mathf.Abs(error) < 0.0001f) continue;
+
+ float invMassA = constraint.bodyA.isKinematic ? 0f : 1f / constraint.bodyA.mass;
+ float invMassB = constraint.bodyB.isKinematic ? 0f : 1f / constraint.bodyB.mass;
+ float totalInvMass = invMassA + invMassB;
+
+ if (totalInvMass < 0.0001f) continue;
+
+ float weightA = useMassWeighting ? (invMassA / totalInvMass) : 0.5f;
+ float weightB = useMassWeighting ? (invMassB / totalInvMass) : 0.5f;
+
+ Vector3 correction = constraint.direction * error;
+ Vector3 positionCorrectionA = correction * weightA;
+ Vector3 positionCorrectionB = -correction * weightB;
+
+ constraint.bodyA.position += positionCorrectionA;
+ constraint.bodyB.position += positionCorrectionB;
+
+ Vector3 velocityCorrectionA = positionCorrectionA / Time.fixedDeltaTime;
+ Vector3 velocityCorrectionB = positionCorrectionB / Time.fixedDeltaTime;
+
+ constraint.bodyA.AddForce(velocityCorrectionA * constraint.bodyA.mass, ForceMode.Impulse);
+ constraint.bodyB.AddForce(velocityCorrectionB * constraint.bodyB.mass, ForceMode.Impulse);
+ }
+ }
+
+ private void ApplyNodeConstraints()
+ {
+ for (int i = 0; i < lineNodes.Count; i++)
+ {
+ Rigidbody body = GetBodyAt(i);
+ IFLineNodeConstraintProvider[] providers = GetNodeConstraintProvidersAt(i);
+
+ if (!body || body.isKinematic || providers.Length == 0)
+ {
+ continue;
+ }
+
+ for (int providerIndex = 0; providerIndex < providers.Length; providerIndex++)
+ {
+ IFLineNodeConstraintProvider provider = providers[providerIndex];
+ if (provider == null)
+ {
+ continue;
+ }
+
+ if (provider is Behaviour behaviour && !behaviour.isActiveAndEnabled)
+ {
+ continue;
+ }
+
+ if (!provider.TryGetConstraintState(out FLineNodeConstraintState constraint))
+ {
+ continue;
+ }
+
+ ApplyWorldTargetConstraint(body, constraint);
+ }
+ }
+ }
+
+ private static void ApplyWorldTargetConstraint(Rigidbody body, FLineNodeConstraintState constraint)
+ {
+ Vector3 delta = constraint.TargetPosition - body.position;
+ Vector3 planarDelta = Vector3.ProjectOnPlane(delta, Vector3.up);
+ Vector3 correction = planarDelta * Mathf.Clamp01(constraint.PlanarStrength);
+
+ float verticalStrength = delta.y >= 0f
+ ? Mathf.Clamp01(constraint.UpwardStrength)
+ : Mathf.Clamp01(constraint.DownwardStrength);
+ correction.y = delta.y * verticalStrength;
+
+ float maxCorrection = Mathf.Max(0f, constraint.MaxCorrection);
+ if (maxCorrection > 0f && correction.magnitude > maxCorrection)
+ {
+ correction = correction.normalized * maxCorrection;
+ }
+
+ if (correction.sqrMagnitude < 1e-8f)
+ {
+ return;
+ }
+
+ body.position += correction;
+
+ Vector3 velocityCorrection = correction / Mathf.Max(Time.fixedDeltaTime, 0.0001f);
+ body.AddForce(velocityCorrection * body.mass, ForceMode.Impulse);
+ }
+
+ private void ApplyDamping()
+ {
+ for (int i = 0; i < _constraints.Count; i++)
+ {
+ var constraint = _constraints[i];
+ if (!constraint.bodyA || !constraint.bodyB) continue;
+
+ if (constraint.currentDistance <= constraint.maxDistance) continue;
+
+ Vector3 relativeVelocity = constraint.bodyB.linearVelocity - constraint.bodyA.linearVelocity;
+ float velocityInConstraintDir = Vector3.Dot(relativeVelocity, constraint.direction);
+
+ if (velocityInConstraintDir > 0)
+ {
+ float dampingForce = -velocityInConstraintDir * dampingCoefficient;
+ Vector3 dampingImpulse = constraint.direction * dampingForce * Time.fixedDeltaTime;
+
+ constraint.bodyA.AddForce(-dampingImpulse * constraint.bodyA.mass, ForceMode.Impulse);
+ constraint.bodyB.AddForce(dampingImpulse * constraint.bodyB.mass, ForceMode.Impulse);
+ }
+ }
+ }
+
+ private void UpdateAnchorNode()
+ {
+ if (anchorTransform == null || lineNodes.Count < 1)
+ {
+ return;
+ }
+
+ var startNode = lineNodes[0].Rigidbody;
+ startNode.transform.SetPositionAndRotation(anchorTransform.position, anchorTransform.rotation);
+
+ if (!startNode.isKinematic)
+ {
+ startNode.linearVelocity = Vector3.zero;
+ startNode.angularVelocity = Vector3.zero;
+ }
+ }
+
+ #region 动态间距修改接口
+
+ public void SetLenght(float length)
+ {
+ SetSegmentMaxLength(0, length);
+ }
+
+ ///
+ /// 按速度过渡某段的最大距离
+ ///
+ public void SetSegmentMaxLength(int segmentIndex, float targetLength, float transitionSpeed = 2f)
+ {
+ if (segmentIndex < 0 || segmentIndex >= _constraints.Count) return;
+
+ targetLength = Mathf.Max(0.01f, targetLength);
+ float speed = Mathf.Max(0.01f, transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed);
+
+ var constraint = _constraints[segmentIndex];
+ constraint.targetMaxDistance = targetLength;
+ constraint.maxTransitionSpeed = speed;
+ constraint.hasPendingMaxTransition = Mathf.Abs(constraint.maxDistance - targetLength) >= 0.0001f;
+ constraint.hasPendingTransition = constraint.hasPendingMaxTransition || constraint.hasPendingMinTransition;
+ }
+
+ ///
+ /// 按速度过渡某段的最小距离
+ ///
+ public void SetSegmentMinLength(int segmentIndex, float targetLength, float transitionSpeed = -1)
+ {
+ if (segmentIndex < 0 || segmentIndex >= _constraints.Count) return;
+
+ targetLength = Mathf.Max(0f, targetLength);
+ float speed = Mathf.Max(0.01f, transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed);
+
+ var constraint = _constraints[segmentIndex];
+ constraint.targetMinDistance = targetLength;
+ constraint.minTransitionSpeed = speed;
+ constraint.hasPendingMinTransition = Mathf.Abs(constraint.minDistance - targetLength) >= 0.0001f;
+ constraint.hasPendingTransition = constraint.hasPendingMaxTransition || constraint.hasPendingMinTransition;
+ }
+
+ ///
+ /// 同时按速度过渡某段的最大和最小距离
+ ///
+ public void SetSegmentLengths(int segmentIndex, float targetMaxLength, float targetMinLength,
+ float transitionSpeed = -1)
+ {
+ if (segmentIndex < 0 || segmentIndex >= _constraints.Count) return;
+
+ targetMaxLength = Mathf.Max(0.01f, targetMaxLength);
+ targetMinLength = Mathf.Max(0f, targetMinLength);
+ float speed = Mathf.Max(0.01f, transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed);
+
+ var constraint = _constraints[segmentIndex];
+ constraint.targetMaxDistance = targetMaxLength;
+ constraint.targetMinDistance = targetMinLength;
+ constraint.maxTransitionSpeed = speed;
+ constraint.minTransitionSpeed = speed;
+ constraint.hasPendingMaxTransition = Mathf.Abs(constraint.maxDistance - targetMaxLength) >= 0.0001f;
+ constraint.hasPendingMinTransition = Mathf.Abs(constraint.minDistance - targetMinLength) >= 0.0001f;
+ constraint.hasPendingTransition = constraint.hasPendingMaxTransition || constraint.hasPendingMinTransition;
+ }
+
+ ///
+ /// 扩展整个绳索(所有段均匀缩放)
+ ///
+ public void ExtendAllSegments(float scaleFactor, float transitionSpeed = -1)
+ {
+ float speed = transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed;
+
+ for (int i = 0; i < _constraints.Count; i++)
+ {
+ var constraint = _constraints[i];
+ float newMaxLength = constraint.maxDistance * scaleFactor;
+ float newMinLength = constraint.minDistance * scaleFactor;
+
+ SetSegmentLengths(i, newMaxLength, newMinLength, speed);
+ }
+ }
+
+ ///
+ /// 收缩/拉紧绳索(所有段向目标长度过渡)
+ ///
+ public void TightenAllSegments(float targetLength, float transitionSpeed = -1)
+ {
+ float speed = transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed;
+
+ for (int i = 0; i < _constraints.Count; i++)
+ {
+ var constraint = _constraints[i];
+ SetSegmentLengths(i, targetLength, Mathf.Min(constraint.minDistance, targetLength), speed);
+ }
+ }
+
+ ///
+ /// 对特定段施加拉力(减小最大距离)
+ ///
+ public void PullSegment(int segmentIndex, float amount, float transitionSpeed = -1)
+ {
+ if (segmentIndex >= 0 && segmentIndex < _constraints.Count)
+ {
+ var constraint = _constraints[segmentIndex];
+ float newMaxLength = Mathf.Max(0.01f, constraint.targetMaxDistance - amount);
+ SetSegmentMaxLength(segmentIndex, newMaxLength, transitionSpeed);
+ }
+ }
+
+ ///
+ /// 放松特定段(增加最大距离)
+ ///
+ public void RelaxSegment(int segmentIndex, float amount, float transitionSpeed = -1)
+ {
+ if (segmentIndex >= 0 && segmentIndex < _constraints.Count)
+ {
+ var constraint = _constraints[segmentIndex];
+ float newMaxLength = constraint.targetMaxDistance + amount;
+ SetSegmentMaxLength(segmentIndex, newMaxLength, transitionSpeed);
+ }
+ }
+
+ ///
+ /// 移除指定段的过渡
+ ///
+ private void RemoveExistingTransition(int segmentIndex, bool removeMax, bool removeMin)
+ {
+ if (segmentIndex < 0 || segmentIndex >= _constraints.Count)
+ {
+ return;
+ }
+
+ var constraint = _constraints[segmentIndex];
+ if (removeMax)
+ {
+ constraint.hasPendingMaxTransition = false;
+ constraint.targetMaxDistance = constraint.maxDistance;
+ }
+
+ if (removeMin)
+ {
+ constraint.hasPendingMinTransition = false;
+ constraint.targetMinDistance = constraint.minDistance;
+ }
+
+ constraint.hasPendingTransition = constraint.hasPendingMaxTransition || constraint.hasPendingMinTransition;
+ }
+
+ ///
+ /// 取消所有正在进行的过渡
+ ///
+ public void CancelAllTransitions()
+ {
+ foreach (var constraint in _constraints)
+ {
+ constraint.hasPendingTransition = false;
+ constraint.hasPendingMaxTransition = false;
+ constraint.hasPendingMinTransition = false;
+ constraint.targetMaxDistance = constraint.maxDistance;
+ constraint.targetMinDistance = constraint.minDistance;
+ }
+ }
+
+ ///
+ /// 取消指定段的过渡
+ ///
+ public void CancelTransition(int segmentIndex)
+ {
+ RemoveExistingTransition(segmentIndex, true, true);
+ if (segmentIndex < _constraints.Count)
+ {
+ _constraints[segmentIndex].hasPendingTransition = false;
+ }
+ }
+
+ ///
+ /// 获取某个段是否正在进行过渡
+ ///
+ public bool IsSegmentTransitioning(int segmentIndex)
+ {
+ if (segmentIndex >= 0 && segmentIndex < _constraints.Count)
+ {
+ return _constraints[segmentIndex].hasPendingTransition;
+ }
+
+ return false;
+ }
+
+ ///
+ /// 获取某段当前的目标最大距离
+ ///
+ public float GetTargetMaxLength(int segmentIndex)
+ {
+ if (segmentIndex >= 0 && segmentIndex < _constraints.Count)
+ {
+ return _constraints[segmentIndex].targetMaxDistance;
+ }
+
+ return -1f;
+ }
+
+ ///
+ /// 获取某段当前的目标最小距离
+ ///
+ public float GetTargetMinLength(int segmentIndex)
+ {
+ if (segmentIndex >= 0 && segmentIndex < _constraints.Count)
+ {
+ return _constraints[segmentIndex].targetMinDistance;
+ }
+
+ return -1f;
+ }
+
+ ///
+ /// 设置默认长度变化速度
+ ///
+ public void SetDefaultTransitionSpeed(float speed)
+ {
+ defaultTransitionSpeed = Mathf.Max(0.01f, speed);
+ }
+
+ #endregion
+
+ #region 极限判定
+
+ ///
+ /// 当前逻辑链总长度超出配置总长度的部分,小于等于零时记为 0。
+ ///
+ [Header("Limit Detection")]
+ public float CurrentStretchLength { get; private set; }
+
+ ///
+ /// 总长度
+ ///
+ public float TotalLength { get; private set; }
+
+ [Min(0f)]
+ // 极限判定的长度容差,允许链路在总长或单段长度上存在少量误差。
+ [SerializeField]
+ private float lengthLimitTolerance = 0.01f;
+
+ [Min(0f)]
+ // 达到极限后,只有当前超长值大于该阈值时,才开始进入断线候选计时。
+ [SerializeField]
+ private float breakStretchThreshold = 0.3f;
+
+ [Min(0f)]
+ // UI 百分比开始起算的最小超长值;低于或等于该值时统一按 0% 处理。
+ [SerializeField]
+ private float breakStretchPercentMinThreshold = 0.06f;
+
+ [Min(0f)]
+ // 断线候选状态允许持续的最大时间;超过后会发出一次断线消息。
+ [SerializeField]
+ private float breakLimitDuration = 3f;
+
+ ///
+ /// 当鱼线达到断线条件时发出的一次性消息。
+ /// 外部可订阅该事件,在回调中执行切线、播放表现或状态切换。
+ ///
+ public event Action OnLineBreakRequested;
+
+ ///
+ /// 当前是否处于极限状态。
+ /// 只要整链超出总长度容差,或任一逻辑段超出单段容差,即认为到达极限。
+ ///
+ public bool IsAtLimit { get; private set; }
+
+ ///
+ /// 当前断线候选状态的累计时间。
+ /// 只有在处于极限状态,且 CurrentStretchLength 大于断线阈值时才会累加;否则重置为 0。
+ ///
+ public float LimitStateTime { get; private set; }
+
+ ///
+ /// 当前极限断线消息是否已经发出过。
+ /// 在退出断线候选状态前只会发一次,避免重复通知。
+ ///
+ public bool HasBreakNotificationSent { get; private set; }
+
+ ///
+ /// 当前拉力极限百分比。
+ /// 当超长值小于等于 breakStretchPercentMinThreshold 时为 0;
+ /// 当超长值大于等于 breakStretchThreshold 时为 100;
+ /// 中间区间按线性比例映射,供 UI 显示使用。
+ ///
+ public float CurrentBreakStretchPercent => EvaluateBreakStretchPercent(CurrentStretchLength);
+
+ ///
+ /// 当前是否正在进行断线候选计时。
+ ///
+ public bool IsBreakCountdownActive => IsAtLimit && CurrentStretchLength > breakStretchThreshold;
+
+ private float EvaluateBreakStretchPercent(float stretchLength)
+ {
+ var percentMinThreshold = Mathf.Max(lengthLimitTolerance, breakStretchPercentMinThreshold);
+
+ if (stretchLength <= percentMinThreshold)
+ {
+ return 0f;
+ }
+
+ if (stretchLength >= breakStretchThreshold)
+ {
+ return 100f;
+ }
+
+ if (breakStretchThreshold <= percentMinThreshold)
+ {
+ return 100f;
+ }
+
+ return Mathf.InverseLerp(percentMinThreshold, breakStretchThreshold, stretchLength) * 100f;
+ }
+
+ private void SetLimitState(bool isAtLimit)
+ {
+ IsAtLimit = isAtLimit;
+ }
+
+ private void UpdateBreakCountdown(float deltaTime)
+ {
+ if (lineNodes.Count < 2)
+ {
+ SetLimitState(false);
+ ResetLimitState();
+ return;
+ }
+
+ var startNode = lineNodes[0];
+ var endNode = lineNodes[^1];
+ TotalLength = 0;
+ foreach (var node in lineNodes)
+ {
+ if (node.NodeType == FLineLogicNodeType.End) continue;
+ TotalLength += node.NextSegmentMaxLength;
+ }
+
+ var realLen = Vector3.Distance(startNode.transform.position, endNode.transform.position);
+ CurrentStretchLength = realLen - TotalLength;
+ if (CurrentStretchLength < 0f)
+ {
+ CurrentStretchLength = 0f;
+ }
+
+ SetLimitState(CurrentStretchLength > lengthLimitTolerance);
+
+ if (CurrentStretchLength > 1)
+ {
+ Log.Error($"水电费 realLen={realLen} TotalLength={TotalLength}");
+ }
+
+ if (!IsBreakCountdownActive)
+ {
+ LimitStateTime = 0f;
+ HasBreakNotificationSent = false;
+ return;
+ }
+
+ LimitStateTime += Mathf.Max(0f, deltaTime);
+ if (HasBreakNotificationSent || LimitStateTime < breakLimitDuration)
+ {
+ return;
+ }
+
+ HasBreakNotificationSent = true;
+ NotifyLineBreakRequested();
+ }
+
+ ///
+ /// 发出鱼线达到断线条件的消息。
+ /// 这里预留给外部订阅,当前不在求解器内部直接执行断线逻辑。
+ ///
+ private void NotifyLineBreakRequested()
+ {
+ OnLineBreakRequested?.Invoke(this);
+ }
+
+ private void ResetLimitState()
+ {
+ CurrentStretchLength = 0f;
+ IsAtLimit = false;
+ LimitStateTime = 0f;
+ HasBreakNotificationSent = false;
+ }
+
+ #endregion
+
+ #region 公共接口
+
+ ///
+ /// 获取一个节点
+ ///
+ ///
+ ///
+ public FLineLogicNode GetNode(FLineLogicNodeType type)
+ {
+ foreach (var node in lineNodes)
+ {
+ if (node.NodeType == type)
+ {
+ return node;
+ }
+ }
+
+ return null;
+ }
+
+
+ public float GetCurrentSegmentLength(int segmentIndex)
+ {
+ if (segmentIndex >= 0 && segmentIndex < _constraints.Count)
+ {
+ _constraints[segmentIndex].UpdateCurrentState();
+ return _constraints[segmentIndex].currentDistance;
+ }
+
+ return -1f;
+ }
+
+ public void AddConnectedBody(Rigidbody rb, float maxLength = 1f, float minLength = 0f)
+ {
+ if (rb == null)
+ {
+ return;
+ }
+
+ FLineLogicNode node = rb.GetComponent();
+ if (!node)
+ {
+ Debug.LogWarning("AddConnectedBody需要目标刚体上挂有FLineLogicNode");
+ return;
+ }
+
+ if (!lineNodes.Contains(node))
+ {
+ if (lineNodes.Count > 0 && lineNodes[lineNodes.Count - 1])
+ {
+ lineNodes[lineNodes.Count - 1].SetSegmentLengths(maxLength, minLength);
+ }
+
+ node.AttachToCable(this);
+ lineNodes.Add(node);
+ BuildConstraints();
+ enabled = _constraints.Count > 0;
+ }
+ }
+
+ public void RemoveConnectedBody(Rigidbody rb)
+ {
+ if (rb == null)
+ {
+ return;
+ }
+
+ for (int i = 0; i < lineNodes.Count; i++)
+ {
+ if (GetBodyAt(i) == rb)
+ {
+ lineNodes.RemoveAt(i);
+ BuildConstraints();
+ enabled = _constraints.Count > 0;
+ break;
+ }
+ }
+ }
+
+ public List GetConnectedBodies()
+ {
+ List bodies = new List(lineNodes.Count);
+ for (int i = 0; i < lineNodes.Count; i++)
+ {
+ bodies.Add(GetBodyAt(i));
+ }
+
+ return bodies;
+ }
+
+ public List GetLineNodes()
+ {
+ return new List(lineNodes);
+ }
+
+ public bool TryGetAdjacentBodies(FLineLogicNode node, out Rigidbody previousBody, out Rigidbody nextBody)
+ {
+ previousBody = null;
+ nextBody = null;
+
+ if (!node)
+ {
+ return false;
+ }
+
+ int index = lineNodes.IndexOf(node);
+ if (index < 0)
+ {
+ return false;
+ }
+
+ if (index > 0 && lineNodes[index - 1])
+ {
+ previousBody = lineNodes[index - 1].Rigidbody;
+ }
+
+ if (index < lineNodes.Count - 1 && lineNodes[index + 1])
+ {
+ nextBody = lineNodes[index + 1].Rigidbody;
+ }
+
+ return previousBody || nextBody;
+ }
+
+ public void ApplyForceAtBody(Rigidbody targetBody, Vector3 force, ForceMode mode = ForceMode.Force)
+ {
+ if (targetBody && ContainsBody(targetBody))
+ {
+ targetBody.AddForce(force, mode);
+ }
+ }
+
+ public float GetSegmentMaxLength(int segmentIndex)
+ {
+ FLineLogicNode node = GetSegmentNode(segmentIndex);
+ if (node)
+ return node.NextSegmentMaxLength;
+
+ return -1f;
+ }
+
+ #endregion
+
+ #region 可视化调试
+
+ void OnDrawGizmos()
+ {
+ if (!showDebugInfo || lineNodes.Count < 2) return;
+
+ for (int i = 0; i < lineNodes.Count - 1; i++)
+ {
+ Rigidbody bodyA = GetBodyAt(i);
+ Rigidbody bodyB = GetBodyAt(i + 1);
+ FLineLogicNode segmentNode = GetSegmentNode(i);
+
+ if (bodyA != null && bodyB != null)
+ {
+ float currentDist = Vector3.Distance(bodyA.position, bodyB.position);
+ float maxDist = segmentNode ? segmentNode.NextSegmentMaxLength : 1f;
+ float minDist = segmentNode ? segmentNode.NextSegmentMinLength : 0f;
+
+ // 颜色:如果正在过渡用橙色,绿色=正常范围,红色=超出最大距离,蓝色=小于最小距离
+ if (IsSegmentTransitioning(i))
+ Gizmos.color = new Color(1f, 0.5f, 0f); // 橙色表示正在过渡
+ else if (currentDist > maxDist)
+ Gizmos.color = Color.red;
+ else if (currentDist < minDist && minDist > 0)
+ Gizmos.color = Color.blue;
+ else
+ Gizmos.color = Color.green;
+
+ Gizmos.DrawLine(bodyA.position, bodyB.position);
+
+ Vector3 midPoint = (bodyA.position + bodyB.position) * 0.5f;
+ Gizmos.DrawWireSphere(midPoint, 0.1f);
+
+#if UNITY_EDITOR
+ string info = $"距离:{currentDist:F2}";
+ if (IsSegmentTransitioning(i))
+ info += $"\n过渡中→{GetTargetMaxLength(i):F2}";
+ else
+ info += $" [最大:{maxDist:F2}";
+ info += $"]";
+ UnityEditor.Handles.Label(midPoint, info);
+#endif
+ }
+ }
+
+ Gizmos.color = Color.red;
+ for (int i = 0; i < lineNodes.Count; i++)
+ {
+ Rigidbody body = GetBodyAt(i);
+ if (body != null)
+ {
+ Gizmos.DrawSphere(body.position, 0.05f);
+ }
+ }
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/Fishing/New/View/FishingLine/FLine.cs.meta b/Assets/Scripts/Fishing/New/View/FishingLine/FLine.cs.meta
index 0eab8bdce..970afcb8d 100644
--- a/Assets/Scripts/Fishing/New/View/FishingLine/FLine.cs.meta
+++ b/Assets/Scripts/Fishing/New/View/FishingLine/FLine.cs.meta
@@ -1,2 +1,3 @@
fileFormatVersion: 2
-guid: 356b3a6fd883b8b4486025abfa664de9
\ No newline at end of file
+guid: c7095cf554c345839173044e4786b0ba
+timeCreated: 1776948821
\ No newline at end of file
diff --git a/Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs b/Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs
deleted file mode 100644
index e25706981..000000000
--- a/Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs
+++ /dev/null
@@ -1,1044 +0,0 @@
-using System;
-using UnityEngine;
-using System.Collections.Generic;
-using NBC;
-
-namespace NBF
-{
- public struct FLineNodeConstraintState
- {
- public Vector3 TargetPosition;
- public float PlanarStrength;
- public float UpwardStrength;
- public float DownwardStrength;
- public float MaxCorrection;
- }
-
- ///
- /// 为 FLine 节点提供附加软约束。求解器只依赖这个通用接口,不依赖具体玩法组件。
- ///
- public interface IFLineNodeConstraintProvider
- {
- bool TryGetConstraintState(out FLineNodeConstraintState state);
- }
-
- public enum LineType
- {
- Spinning,
- SpinningFloat,
- Hand,
- HandDouble,
- }
-
- public class FLine : FGearBase
- {
- [Header("基本参数设置")] public LineType LineType;
- [Header("连接点配置")] [SerializeField] private Transform anchorTransform;
- [SerializeField] private List lineNodes = new List();
-
- [Header("物理参数")] [SerializeField] private float positionCorrectionForce = 100f;
- [SerializeField] private float dampingCoefficient = 10f;
- [SerializeField] private int constraintIterations = 10;
- [SerializeField] private bool useMassWeighting = true;
-
- [Header("动态间距设置")] [SerializeField] private float defaultTransitionSpeed = 2f; // 默认长度变化速度(单位/秒)
-
- [Header("调试")] [SerializeField] private bool showDebugInfo = true;
-
- private readonly List _constraints = new List();
-
- private readonly List _nodeConstraintProviders =
- new List();
-
- [System.Serializable]
- public class ConnectionConstraint
- {
- public Rigidbody bodyA;
- public Rigidbody bodyB;
- public float maxDistance;
- public float minDistance;
- public float currentDistance;
- public Vector3 direction;
-
- // 动态目标距离(用于平滑过渡)
- public float targetMaxDistance;
- public float targetMinDistance;
- public bool hasPendingTransition;
- public bool hasPendingMaxTransition;
- public bool hasPendingMinTransition;
- public float maxTransitionSpeed;
- public float minTransitionSpeed;
-
- public ConnectionConstraint(Rigidbody a, Rigidbody b, float maxDist, float minDist = 0f)
- {
- bodyA = a;
- bodyB = b;
- maxDistance = maxDist;
- minDistance = minDist;
- targetMaxDistance = maxDist;
- targetMinDistance = minDist;
- hasPendingTransition = false;
- hasPendingMaxTransition = false;
- hasPendingMinTransition = false;
- maxTransitionSpeed = 0f;
- minTransitionSpeed = 0f;
- }
-
- public void UpdateCurrentState()
- {
- if (bodyA && bodyB)
- {
- Vector3 delta = bodyB.position - bodyA.position;
- currentDistance = delta.magnitude;
- direction = currentDistance > 0.0001f ? delta.normalized : Vector3.right;
- }
- }
- }
-
- void Awake()
- {
- BuildConstraints();
- }
-
- protected override void OnInit()
- {
- }
-
- void Start()
- {
- if (_constraints.Count == 0)
- {
- Debug.LogWarning("FLine需要至少2个有效节点");
- enabled = false;
- }
- }
-
- void FixedUpdate()
- {
- if (!enabled || _constraints.Count == 0) return;
-
- UpdateAnchorNode();
- // 更新动态过渡
- UpdateTransitions();
-
- for (int iteration = 0; iteration < constraintIterations; iteration++)
- {
- ApplyDistanceConstraints();
- ApplyNodeConstraints();
- }
-
- ApplyDamping();
- UpdateBreakCountdown(Time.fixedDeltaTime);
- }
-
- private void BuildConstraints()
- {
- _constraints.Clear();
- _nodeConstraintProviders.Clear();
- if (lineNodes.Count < 2) return;
-
- for (int i = 0; i < lineNodes.Count; i++)
- {
- if (lineNodes[i])
- {
- lineNodes[i].AttachToCable(this);
- _nodeConstraintProviders.Add(CollectNodeConstraintProviders(lineNodes[i]));
- }
- else
- {
- _nodeConstraintProviders.Add(Array.Empty());
- }
- }
-
- // 创建约束
- for (int i = 0; i < lineNodes.Count - 1; i++)
- {
- FLineLogicNode currentNode = lineNodes[i];
- FLineLogicNode nextNode = lineNodes[i + 1];
- Rigidbody bodyA = currentNode ? currentNode.Rigidbody : null;
- Rigidbody bodyB = nextNode ? nextNode.Rigidbody : null;
-
- if (bodyA != null && bodyB != null)
- {
- var constraint = new ConnectionConstraint(
- bodyA,
- bodyB,
- currentNode.NextSegmentMaxLength,
- currentNode.NextSegmentMinLength
- );
- _constraints.Add(constraint);
- }
- }
- }
-
- private FLineLogicNode GetSegmentNode(int segmentIndex)
- {
- if (segmentIndex < 0 || segmentIndex >= lineNodes.Count - 1)
- {
- return null;
- }
-
- return lineNodes[segmentIndex];
- }
-
- private Rigidbody GetBodyAt(int nodeIndex)
- {
- if (nodeIndex < 0 || nodeIndex >= lineNodes.Count)
- {
- return null;
- }
-
- FLineLogicNode node = lineNodes[nodeIndex];
- return node ? node.Rigidbody : null;
- }
-
- private IFLineNodeConstraintProvider[] GetNodeConstraintProvidersAt(int nodeIndex)
- {
- if (nodeIndex < 0 || nodeIndex >= _nodeConstraintProviders.Count)
- {
- return Array.Empty();
- }
-
- return _nodeConstraintProviders[nodeIndex];
- }
-
- private static IFLineNodeConstraintProvider[] CollectNodeConstraintProviders(FLineLogicNode node)
- {
- MonoBehaviour[] behaviours = node.GetComponents();
- List providers = new List();
-
- for (int i = 0; i < behaviours.Length; i++)
- {
- MonoBehaviour behaviour = behaviours[i];
- if (behaviour is IFLineNodeConstraintProvider provider)
- {
- providers.Add(provider);
- }
- }
-
- return providers.ToArray();
- }
-
- private void SyncSegmentMaxLength(int segmentIndex, float maxLength)
- {
- FLineLogicNode node = GetSegmentNode(segmentIndex);
- if (node)
- {
- node.NextSegmentMaxLength = maxLength;
- }
- }
-
- private void SyncSegmentMinLength(int segmentIndex, float minLength)
- {
- FLineLogicNode node = GetSegmentNode(segmentIndex);
- if (node)
- {
- node.NextSegmentMinLength = minLength;
- }
- }
-
- private bool ContainsBody(Rigidbody targetBody)
- {
- for (int i = 0; i < lineNodes.Count; i++)
- {
- if (GetBodyAt(i) == targetBody)
- {
- return true;
- }
- }
-
- return false;
- }
-
- ///
- /// 更新所有活跃的过渡
- ///
- private void UpdateTransitions()
- {
- float deltaTime = Time.fixedDeltaTime;
-
- for (int i = 0; i < _constraints.Count; i++)
- {
- var constraint = _constraints[i];
-
- if (constraint.hasPendingMaxTransition)
- {
- float nextMaxDistance = Mathf.MoveTowards(
- constraint.maxDistance,
- constraint.targetMaxDistance,
- constraint.maxTransitionSpeed * deltaTime
- );
- constraint.maxDistance = nextMaxDistance;
- SyncSegmentMaxLength(i, nextMaxDistance);
-
- if (Mathf.Abs(nextMaxDistance - constraint.targetMaxDistance) < 0.0001f)
- {
- constraint.maxDistance = constraint.targetMaxDistance;
- constraint.hasPendingMaxTransition = false;
- SyncSegmentMaxLength(i, constraint.targetMaxDistance);
- }
- }
-
- if (constraint.hasPendingMinTransition)
- {
- float nextMinDistance = Mathf.MoveTowards(
- constraint.minDistance,
- constraint.targetMinDistance,
- constraint.minTransitionSpeed * deltaTime
- );
- constraint.minDistance = nextMinDistance;
- SyncSegmentMinLength(i, nextMinDistance);
-
- if (Mathf.Abs(nextMinDistance - constraint.targetMinDistance) < 0.0001f)
- {
- constraint.minDistance = constraint.targetMinDistance;
- constraint.hasPendingMinTransition = false;
- SyncSegmentMinLength(i, constraint.targetMinDistance);
- }
- }
-
- constraint.hasPendingTransition =
- constraint.hasPendingMaxTransition || constraint.hasPendingMinTransition;
- }
- }
-
- private void ApplyDistanceConstraints()
- {
- for (int i = 0; i < _constraints.Count; i++)
- {
- var constraint = _constraints[i];
- if (!constraint.bodyA || !constraint.bodyB) continue;
-
- constraint.UpdateCurrentState();
-
- float currentDist = constraint.currentDistance;
- float maxDist = constraint.maxDistance;
- float minDist = constraint.minDistance;
-
- float error = 0f;
- bool needCorrection = false;
-
- if (currentDist > maxDist)
- {
- error = currentDist - maxDist;
- needCorrection = true;
- }
-
- if (!needCorrection || Mathf.Abs(error) < 0.0001f) continue;
-
- float invMassA = constraint.bodyA.isKinematic ? 0f : 1f / constraint.bodyA.mass;
- float invMassB = constraint.bodyB.isKinematic ? 0f : 1f / constraint.bodyB.mass;
- float totalInvMass = invMassA + invMassB;
-
- if (totalInvMass < 0.0001f) continue;
-
- float weightA = useMassWeighting ? (invMassA / totalInvMass) : 0.5f;
- float weightB = useMassWeighting ? (invMassB / totalInvMass) : 0.5f;
-
- Vector3 correction = constraint.direction * error;
- Vector3 positionCorrectionA = correction * weightA;
- Vector3 positionCorrectionB = -correction * weightB;
-
- constraint.bodyA.position += positionCorrectionA;
- constraint.bodyB.position += positionCorrectionB;
-
- Vector3 velocityCorrectionA = positionCorrectionA / Time.fixedDeltaTime;
- Vector3 velocityCorrectionB = positionCorrectionB / Time.fixedDeltaTime;
-
- constraint.bodyA.AddForce(velocityCorrectionA * constraint.bodyA.mass, ForceMode.Impulse);
- constraint.bodyB.AddForce(velocityCorrectionB * constraint.bodyB.mass, ForceMode.Impulse);
- }
- }
-
- private void ApplyNodeConstraints()
- {
- for (int i = 0; i < lineNodes.Count; i++)
- {
- Rigidbody body = GetBodyAt(i);
- IFLineNodeConstraintProvider[] providers = GetNodeConstraintProvidersAt(i);
-
- if (!body || body.isKinematic || providers.Length == 0)
- {
- continue;
- }
-
- for (int providerIndex = 0; providerIndex < providers.Length; providerIndex++)
- {
- IFLineNodeConstraintProvider provider = providers[providerIndex];
- if (provider == null)
- {
- continue;
- }
-
- if (provider is Behaviour behaviour && !behaviour.isActiveAndEnabled)
- {
- continue;
- }
-
- if (!provider.TryGetConstraintState(out FLineNodeConstraintState constraint))
- {
- continue;
- }
-
- ApplyWorldTargetConstraint(body, constraint);
- }
- }
- }
-
- private static void ApplyWorldTargetConstraint(Rigidbody body, FLineNodeConstraintState constraint)
- {
- Vector3 delta = constraint.TargetPosition - body.position;
- Vector3 planarDelta = Vector3.ProjectOnPlane(delta, Vector3.up);
- Vector3 correction = planarDelta * Mathf.Clamp01(constraint.PlanarStrength);
-
- float verticalStrength = delta.y >= 0f
- ? Mathf.Clamp01(constraint.UpwardStrength)
- : Mathf.Clamp01(constraint.DownwardStrength);
- correction.y = delta.y * verticalStrength;
-
- float maxCorrection = Mathf.Max(0f, constraint.MaxCorrection);
- if (maxCorrection > 0f && correction.magnitude > maxCorrection)
- {
- correction = correction.normalized * maxCorrection;
- }
-
- if (correction.sqrMagnitude < 1e-8f)
- {
- return;
- }
-
- body.position += correction;
-
- Vector3 velocityCorrection = correction / Mathf.Max(Time.fixedDeltaTime, 0.0001f);
- body.AddForce(velocityCorrection * body.mass, ForceMode.Impulse);
- }
-
- private void ApplyDamping()
- {
- for (int i = 0; i < _constraints.Count; i++)
- {
- var constraint = _constraints[i];
- if (!constraint.bodyA || !constraint.bodyB) continue;
-
- if (constraint.currentDistance <= constraint.maxDistance) continue;
-
- Vector3 relativeVelocity = constraint.bodyB.linearVelocity - constraint.bodyA.linearVelocity;
- float velocityInConstraintDir = Vector3.Dot(relativeVelocity, constraint.direction);
-
- if (velocityInConstraintDir > 0)
- {
- float dampingForce = -velocityInConstraintDir * dampingCoefficient;
- Vector3 dampingImpulse = constraint.direction * dampingForce * Time.fixedDeltaTime;
-
- constraint.bodyA.AddForce(-dampingImpulse * constraint.bodyA.mass, ForceMode.Impulse);
- constraint.bodyB.AddForce(dampingImpulse * constraint.bodyB.mass, ForceMode.Impulse);
- }
- }
- }
-
- private void UpdateAnchorNode()
- {
- if (anchorTransform == null || lineNodes.Count < 1)
- {
- return;
- }
-
- var startNode = lineNodes[0].Rigidbody;
- startNode.transform.SetPositionAndRotation(anchorTransform.position, anchorTransform.rotation);
-
- if (!startNode.isKinematic)
- {
- startNode.linearVelocity = Vector3.zero;
- startNode.angularVelocity = Vector3.zero;
- }
- }
-
- #region 动态间距修改接口
-
- public void SetLenght(float length)
- {
- SetSegmentMaxLength(0, length);
- }
-
- ///
- /// 按速度过渡某段的最大距离
- ///
- public void SetSegmentMaxLength(int segmentIndex, float targetLength, float transitionSpeed = 2f)
- {
- if (segmentIndex < 0 || segmentIndex >= _constraints.Count) return;
-
- targetLength = Mathf.Max(0.01f, targetLength);
- float speed = Mathf.Max(0.01f, transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed);
-
- var constraint = _constraints[segmentIndex];
- constraint.targetMaxDistance = targetLength;
- constraint.maxTransitionSpeed = speed;
- constraint.hasPendingMaxTransition = Mathf.Abs(constraint.maxDistance - targetLength) >= 0.0001f;
- constraint.hasPendingTransition = constraint.hasPendingMaxTransition || constraint.hasPendingMinTransition;
- }
-
- ///
- /// 按速度过渡某段的最小距离
- ///
- public void SetSegmentMinLength(int segmentIndex, float targetLength, float transitionSpeed = -1)
- {
- if (segmentIndex < 0 || segmentIndex >= _constraints.Count) return;
-
- targetLength = Mathf.Max(0f, targetLength);
- float speed = Mathf.Max(0.01f, transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed);
-
- var constraint = _constraints[segmentIndex];
- constraint.targetMinDistance = targetLength;
- constraint.minTransitionSpeed = speed;
- constraint.hasPendingMinTransition = Mathf.Abs(constraint.minDistance - targetLength) >= 0.0001f;
- constraint.hasPendingTransition = constraint.hasPendingMaxTransition || constraint.hasPendingMinTransition;
- }
-
- ///
- /// 同时按速度过渡某段的最大和最小距离
- ///
- public void SetSegmentLengths(int segmentIndex, float targetMaxLength, float targetMinLength,
- float transitionSpeed = -1)
- {
- if (segmentIndex < 0 || segmentIndex >= _constraints.Count) return;
-
- targetMaxLength = Mathf.Max(0.01f, targetMaxLength);
- targetMinLength = Mathf.Max(0f, targetMinLength);
- float speed = Mathf.Max(0.01f, transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed);
-
- var constraint = _constraints[segmentIndex];
- constraint.targetMaxDistance = targetMaxLength;
- constraint.targetMinDistance = targetMinLength;
- constraint.maxTransitionSpeed = speed;
- constraint.minTransitionSpeed = speed;
- constraint.hasPendingMaxTransition = Mathf.Abs(constraint.maxDistance - targetMaxLength) >= 0.0001f;
- constraint.hasPendingMinTransition = Mathf.Abs(constraint.minDistance - targetMinLength) >= 0.0001f;
- constraint.hasPendingTransition = constraint.hasPendingMaxTransition || constraint.hasPendingMinTransition;
- }
-
- ///
- /// 扩展整个绳索(所有段均匀缩放)
- ///
- public void ExtendAllSegments(float scaleFactor, float transitionSpeed = -1)
- {
- float speed = transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed;
-
- for (int i = 0; i < _constraints.Count; i++)
- {
- var constraint = _constraints[i];
- float newMaxLength = constraint.maxDistance * scaleFactor;
- float newMinLength = constraint.minDistance * scaleFactor;
-
- SetSegmentLengths(i, newMaxLength, newMinLength, speed);
- }
- }
-
- ///
- /// 收缩/拉紧绳索(所有段向目标长度过渡)
- ///
- public void TightenAllSegments(float targetLength, float transitionSpeed = -1)
- {
- float speed = transitionSpeed > 0 ? transitionSpeed : defaultTransitionSpeed;
-
- for (int i = 0; i < _constraints.Count; i++)
- {
- var constraint = _constraints[i];
- SetSegmentLengths(i, targetLength, Mathf.Min(constraint.minDistance, targetLength), speed);
- }
- }
-
- ///
- /// 对特定段施加拉力(减小最大距离)
- ///
- public void PullSegment(int segmentIndex, float amount, float transitionSpeed = -1)
- {
- if (segmentIndex >= 0 && segmentIndex < _constraints.Count)
- {
- var constraint = _constraints[segmentIndex];
- float newMaxLength = Mathf.Max(0.01f, constraint.targetMaxDistance - amount);
- SetSegmentMaxLength(segmentIndex, newMaxLength, transitionSpeed);
- }
- }
-
- ///
- /// 放松特定段(增加最大距离)
- ///
- public void RelaxSegment(int segmentIndex, float amount, float transitionSpeed = -1)
- {
- if (segmentIndex >= 0 && segmentIndex < _constraints.Count)
- {
- var constraint = _constraints[segmentIndex];
- float newMaxLength = constraint.targetMaxDistance + amount;
- SetSegmentMaxLength(segmentIndex, newMaxLength, transitionSpeed);
- }
- }
-
- ///
- /// 移除指定段的过渡
- ///
- private void RemoveExistingTransition(int segmentIndex, bool removeMax, bool removeMin)
- {
- if (segmentIndex < 0 || segmentIndex >= _constraints.Count)
- {
- return;
- }
-
- var constraint = _constraints[segmentIndex];
- if (removeMax)
- {
- constraint.hasPendingMaxTransition = false;
- constraint.targetMaxDistance = constraint.maxDistance;
- }
-
- if (removeMin)
- {
- constraint.hasPendingMinTransition = false;
- constraint.targetMinDistance = constraint.minDistance;
- }
-
- constraint.hasPendingTransition = constraint.hasPendingMaxTransition || constraint.hasPendingMinTransition;
- }
-
- ///
- /// 取消所有正在进行的过渡
- ///
- public void CancelAllTransitions()
- {
- foreach (var constraint in _constraints)
- {
- constraint.hasPendingTransition = false;
- constraint.hasPendingMaxTransition = false;
- constraint.hasPendingMinTransition = false;
- constraint.targetMaxDistance = constraint.maxDistance;
- constraint.targetMinDistance = constraint.minDistance;
- }
- }
-
- ///
- /// 取消指定段的过渡
- ///
- public void CancelTransition(int segmentIndex)
- {
- RemoveExistingTransition(segmentIndex, true, true);
- if (segmentIndex < _constraints.Count)
- {
- _constraints[segmentIndex].hasPendingTransition = false;
- }
- }
-
- ///
- /// 获取某个段是否正在进行过渡
- ///
- public bool IsSegmentTransitioning(int segmentIndex)
- {
- if (segmentIndex >= 0 && segmentIndex < _constraints.Count)
- {
- return _constraints[segmentIndex].hasPendingTransition;
- }
-
- return false;
- }
-
- ///
- /// 获取某段当前的目标最大距离
- ///
- public float GetTargetMaxLength(int segmentIndex)
- {
- if (segmentIndex >= 0 && segmentIndex < _constraints.Count)
- {
- return _constraints[segmentIndex].targetMaxDistance;
- }
-
- return -1f;
- }
-
- ///
- /// 获取某段当前的目标最小距离
- ///
- public float GetTargetMinLength(int segmentIndex)
- {
- if (segmentIndex >= 0 && segmentIndex < _constraints.Count)
- {
- return _constraints[segmentIndex].targetMinDistance;
- }
-
- return -1f;
- }
-
- ///
- /// 设置默认长度变化速度
- ///
- public void SetDefaultTransitionSpeed(float speed)
- {
- defaultTransitionSpeed = Mathf.Max(0.01f, speed);
- }
-
- #endregion
-
- #region 极限判定
-
- ///
- /// 当前逻辑链总长度超出配置总长度的部分,小于等于零时记为 0。
- ///
- [Header("Limit Detection")]
- public float CurrentStretchLength { get; private set; }
-
- ///
- /// 总长度
- ///
- public float TotalLength { get; private set; }
-
- [Min(0f)]
- // 极限判定的长度容差,允许链路在总长或单段长度上存在少量误差。
- [SerializeField]
- private float lengthLimitTolerance = 0.01f;
-
- [Min(0f)]
- // 达到极限后,只有当前超长值大于该阈值时,才开始进入断线候选计时。
- [SerializeField]
- private float breakStretchThreshold = 0.3f;
-
- [Min(0f)]
- // UI 百分比开始起算的最小超长值;低于或等于该值时统一按 0% 处理。
- [SerializeField]
- private float breakStretchPercentMinThreshold = 0.06f;
-
- [Min(0f)]
- // 断线候选状态允许持续的最大时间;超过后会发出一次断线消息。
- [SerializeField]
- private float breakLimitDuration = 3f;
-
- ///
- /// 当鱼线达到断线条件时发出的一次性消息。
- /// 外部可订阅该事件,在回调中执行切线、播放表现或状态切换。
- ///
- public event Action OnLineBreakRequested;
-
- ///
- /// 当前是否处于极限状态。
- /// 只要整链超出总长度容差,或任一逻辑段超出单段容差,即认为到达极限。
- ///
- public bool IsAtLimit { get; private set; }
-
- ///
- /// 当前断线候选状态的累计时间。
- /// 只有在处于极限状态,且 CurrentStretchLength 大于断线阈值时才会累加;否则重置为 0。
- ///
- public float LimitStateTime { get; private set; }
-
- ///
- /// 当前极限断线消息是否已经发出过。
- /// 在退出断线候选状态前只会发一次,避免重复通知。
- ///
- public bool HasBreakNotificationSent { get; private set; }
-
- ///
- /// 当前拉力极限百分比。
- /// 当超长值小于等于 breakStretchPercentMinThreshold 时为 0;
- /// 当超长值大于等于 breakStretchThreshold 时为 100;
- /// 中间区间按线性比例映射,供 UI 显示使用。
- ///
- public float CurrentBreakStretchPercent => EvaluateBreakStretchPercent(CurrentStretchLength);
-
- ///
- /// 当前是否正在进行断线候选计时。
- ///
- public bool IsBreakCountdownActive => IsAtLimit && CurrentStretchLength > breakStretchThreshold;
-
- private float EvaluateBreakStretchPercent(float stretchLength)
- {
- var percentMinThreshold = Mathf.Max(lengthLimitTolerance, breakStretchPercentMinThreshold);
-
- if (stretchLength <= percentMinThreshold)
- {
- return 0f;
- }
-
- if (stretchLength >= breakStretchThreshold)
- {
- return 100f;
- }
-
- if (breakStretchThreshold <= percentMinThreshold)
- {
- return 100f;
- }
-
- return Mathf.InverseLerp(percentMinThreshold, breakStretchThreshold, stretchLength) * 100f;
- }
-
- private void SetLimitState(bool isAtLimit)
- {
- IsAtLimit = isAtLimit;
- }
-
- private void UpdateBreakCountdown(float deltaTime)
- {
- if (lineNodes.Count < 2)
- {
- SetLimitState(false);
- ResetLimitState();
- return;
- }
-
- var startNode = lineNodes[0];
- var endNode = lineNodes[^1];
- TotalLength = 0;
- foreach (var node in lineNodes)
- {
- if (node.NodeType == FLineLogicNodeType.End) continue;
- TotalLength += node.NextSegmentMaxLength;
- }
-
- var realLen = Vector3.Distance(startNode.transform.position, endNode.transform.position);
- CurrentStretchLength = realLen - TotalLength;
- if (CurrentStretchLength < 0f)
- {
- CurrentStretchLength = 0f;
- }
-
- SetLimitState(CurrentStretchLength > lengthLimitTolerance);
-
- if (CurrentStretchLength > 1)
- {
- Log.Error($"水电费 realLen={realLen} TotalLength={TotalLength}");
- }
-
- if (!IsBreakCountdownActive)
- {
- LimitStateTime = 0f;
- HasBreakNotificationSent = false;
- return;
- }
-
- LimitStateTime += Mathf.Max(0f, deltaTime);
- if (HasBreakNotificationSent || LimitStateTime < breakLimitDuration)
- {
- return;
- }
-
- HasBreakNotificationSent = true;
- NotifyLineBreakRequested();
- }
-
- ///
- /// 发出鱼线达到断线条件的消息。
- /// 这里预留给外部订阅,当前不在求解器内部直接执行断线逻辑。
- ///
- private void NotifyLineBreakRequested()
- {
- OnLineBreakRequested?.Invoke(this);
- }
-
- private void ResetLimitState()
- {
- CurrentStretchLength = 0f;
- IsAtLimit = false;
- LimitStateTime = 0f;
- HasBreakNotificationSent = false;
- }
-
- #endregion
-
- #region 公共接口
-
- ///
- /// 获取一个节点
- ///
- ///
- ///
- public FLineLogicNode GetNode(FLineLogicNodeType type)
- {
- foreach (var node in lineNodes)
- {
- if (node.NodeType == type)
- {
- return node;
- }
- }
-
- return null;
- }
-
-
- public float GetCurrentSegmentLength(int segmentIndex)
- {
- if (segmentIndex >= 0 && segmentIndex < _constraints.Count)
- {
- _constraints[segmentIndex].UpdateCurrentState();
- return _constraints[segmentIndex].currentDistance;
- }
-
- return -1f;
- }
-
- public void AddConnectedBody(Rigidbody rb, float maxLength = 1f, float minLength = 0f)
- {
- if (rb == null)
- {
- return;
- }
-
- FLineLogicNode node = rb.GetComponent();
- if (!node)
- {
- Debug.LogWarning("AddConnectedBody需要目标刚体上挂有FLineLogicNode");
- return;
- }
-
- if (!lineNodes.Contains(node))
- {
- if (lineNodes.Count > 0 && lineNodes[lineNodes.Count - 1])
- {
- lineNodes[lineNodes.Count - 1].SetSegmentLengths(maxLength, minLength);
- }
-
- node.AttachToCable(this);
- lineNodes.Add(node);
- BuildConstraints();
- enabled = _constraints.Count > 0;
- }
- }
-
- public void RemoveConnectedBody(Rigidbody rb)
- {
- if (rb == null)
- {
- return;
- }
-
- for (int i = 0; i < lineNodes.Count; i++)
- {
- if (GetBodyAt(i) == rb)
- {
- lineNodes.RemoveAt(i);
- BuildConstraints();
- enabled = _constraints.Count > 0;
- break;
- }
- }
- }
-
- public List GetConnectedBodies()
- {
- List bodies = new List(lineNodes.Count);
- for (int i = 0; i < lineNodes.Count; i++)
- {
- bodies.Add(GetBodyAt(i));
- }
-
- return bodies;
- }
-
- public List GetLineNodes()
- {
- return new List(lineNodes);
- }
-
- public bool TryGetAdjacentBodies(FLineLogicNode node, out Rigidbody previousBody, out Rigidbody nextBody)
- {
- previousBody = null;
- nextBody = null;
-
- if (!node)
- {
- return false;
- }
-
- int index = lineNodes.IndexOf(node);
- if (index < 0)
- {
- return false;
- }
-
- if (index > 0 && lineNodes[index - 1])
- {
- previousBody = lineNodes[index - 1].Rigidbody;
- }
-
- if (index < lineNodes.Count - 1 && lineNodes[index + 1])
- {
- nextBody = lineNodes[index + 1].Rigidbody;
- }
-
- return previousBody || nextBody;
- }
-
- public void ApplyForceAtBody(Rigidbody targetBody, Vector3 force, ForceMode mode = ForceMode.Force)
- {
- if (targetBody && ContainsBody(targetBody))
- {
- targetBody.AddForce(force, mode);
- }
- }
-
- public float GetSegmentMaxLength(int segmentIndex)
- {
- FLineLogicNode node = GetSegmentNode(segmentIndex);
- if (node)
- return node.NextSegmentMaxLength;
-
- return -1f;
- }
-
- #endregion
-
- #region 可视化调试
-
- void OnDrawGizmos()
- {
- if (!showDebugInfo || lineNodes.Count < 2) return;
-
- for (int i = 0; i < lineNodes.Count - 1; i++)
- {
- Rigidbody bodyA = GetBodyAt(i);
- Rigidbody bodyB = GetBodyAt(i + 1);
- FLineLogicNode segmentNode = GetSegmentNode(i);
-
- if (bodyA != null && bodyB != null)
- {
- float currentDist = Vector3.Distance(bodyA.position, bodyB.position);
- float maxDist = segmentNode ? segmentNode.NextSegmentMaxLength : 1f;
- float minDist = segmentNode ? segmentNode.NextSegmentMinLength : 0f;
-
- // 颜色:如果正在过渡用橙色,绿色=正常范围,红色=超出最大距离,蓝色=小于最小距离
- if (IsSegmentTransitioning(i))
- Gizmos.color = new Color(1f, 0.5f, 0f); // 橙色表示正在过渡
- else if (currentDist > maxDist)
- Gizmos.color = Color.red;
- else if (currentDist < minDist && minDist > 0)
- Gizmos.color = Color.blue;
- else
- Gizmos.color = Color.green;
-
- Gizmos.DrawLine(bodyA.position, bodyB.position);
-
- Vector3 midPoint = (bodyA.position + bodyB.position) * 0.5f;
- Gizmos.DrawWireSphere(midPoint, 0.1f);
-
-#if UNITY_EDITOR
- string info = $"距离:{currentDist:F2}";
- if (IsSegmentTransitioning(i))
- info += $"\n过渡中→{GetTargetMaxLength(i):F2}";
- else
- info += $" [最大:{maxDist:F2}";
- info += $"]";
- UnityEditor.Handles.Label(midPoint, info);
-#endif
- }
- }
-
- Gizmos.color = Color.red;
- for (int i = 0; i < lineNodes.Count; i++)
- {
- Rigidbody body = GetBodyAt(i);
- if (body != null)
- {
- Gizmos.DrawSphere(body.position, 0.05f);
- }
- }
- }
-
- #endregion
- }
-}
\ No newline at end of file
diff --git a/Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs.meta b/Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs.meta
deleted file mode 100644
index 970afcb8d..000000000
--- a/Assets/Scripts/Fishing/New/View/Player/Tackle/FLine.cs.meta
+++ /dev/null
@@ -1,3 +0,0 @@
-fileFormatVersion: 2
-guid: c7095cf554c345839173044e4786b0ba
-timeCreated: 1776948821
\ No newline at end of file
diff --git a/Assets/Scripts/Fishing/New/View/Player/Tackle/FRod.cs b/Assets/Scripts/Fishing/New/View/Player/Tackle/FRod.cs
index 1ce4e3c68..5ef0d95ef 100644
--- a/Assets/Scripts/Fishing/New/View/Player/Tackle/FRod.cs
+++ b/Assets/Scripts/Fishing/New/View/Player/Tackle/FRod.cs
@@ -241,7 +241,7 @@ namespace NBF
var solver = Instantiate(lineSolverPrefab, GearRoot);
solver.transform.position = Asset.lineConnector.position;
solver.transform.rotation = Asset.lineConnector.rotation;
- var indexNames = new[] { "fishing line float set", "fishing line spinning" };
+ var indexNames = new[] { "LineHand", "LineLure" };
var path =
$"Assets/ResRaw/Prefabs/Line/{indexNames[currentLineTypeIndex]}.prefab";
var prefab = Assets.Load(path);
diff --git a/UserSettings/EditorUserSettings.asset b/UserSettings/EditorUserSettings.asset
index c70116ffe..ba55fa1bf 100644
--- a/UserSettings/EditorUserSettings.asset
+++ b/UserSettings/EditorUserSettings.asset
@@ -15,32 +15,32 @@ EditorUserSettings:
value: 2550581500
flags: 0
RecentlyUsedSceneGuid-0:
- value: 55540305570d0f0e0c5e5e2115710d44174e4e2b7b7e77662f2d1c61b5b06069
- flags: 0
- RecentlyUsedSceneGuid-1:
- value: 5452500303515f0a5f5b5a7445775e46401519787c717f677d784860e3b1676c
- flags: 0
- RecentlyUsedSceneGuid-2:
value: 050402550007590a0f565f2714200c44144e492f2f70753175711f66e0b8303c
flags: 0
- RecentlyUsedSceneGuid-3:
+ RecentlyUsedSceneGuid-1:
value: 06070c5f5c075c5e5e085476427a0a44474e1c2f7f7a73362f2d4d36b5b1633d
flags: 0
- RecentlyUsedSceneGuid-4:
+ RecentlyUsedSceneGuid-2:
value: 0005505f515750595e5f5f23412507441216497f2d7f24367e711c64b6b86c61
flags: 0
- RecentlyUsedSceneGuid-5:
+ RecentlyUsedSceneGuid-3:
value: 54070c5452075002590c0871127b5a4443161c2f797176312c2f1e6bb1b4353d
flags: 0
- RecentlyUsedSceneGuid-6:
+ RecentlyUsedSceneGuid-4:
value: 5309035757065a0a54575f7216265c4444151d28792e72627d2f1935bbb8673a
flags: 0
- RecentlyUsedSceneGuid-7:
+ RecentlyUsedSceneGuid-5:
value: 00050c5150005f5f54560f2640270d4410161c28282b72357e7c4835e4b63760
flags: 0
- RecentlyUsedSceneGuid-8:
+ RecentlyUsedSceneGuid-6:
value: 06090c5f54015f5a0f085b7b11765d444e4e1e287429773178704561b3b23561
flags: 0
+ RecentlyUsedSceneGuid-7:
+ value: 0257035f51050d090f0f5d734521094414164e797e7a20667d7a4536e0e36461
+ flags: 0
+ RecentlyUsedSceneGuid-8:
+ value: 07060c5454040c0a545b547240700a441216417e7f2e7268752c4966b4b0663d
+ flags: 0
RecentlyUsedSceneGuid-9:
value: 5505015f5c515a085f5b092149760f441716407a787d7564287b1b36e7e1366e
flags: 0