using UnityEngine; using DG.Tweening; using AibisDream.Kit; using AibisDream.Framework; namespace AibisDream.FixSystem { public class CablePanel : MonoBehaviour, IOperatorPanel { private const int SortingOrderRetracted = 41; private const float RetractDuration = 0.22f; #region 组件索引 [Header("插线组件索引")] [SerializeField] private Plug plugRef; [SerializeField] private Cable cableRef; // 旧视觉线缆,已由 PhysicCable 统一取代,Start 时停用 [SerializeField] private PhysicCable physicCableRef; [SerializeField] private Transform cableRootPos; [SerializeField] private CableReel cableReelRef; [Header("灯光系统")] [SerializeField] private CablePanelLight panelLight; public Plug PlugRef => plugRef; public Cable CableRef => cableRef; public PhysicCable PhysicCableRef => physicCableRef; public Transform CableRootPos => cableRootPos; public CableReel CableReelRef => cableReelRef; public CablePanelLight PanelLight => panelLight; public BodyModuleSystem BodyModuleSystem { get; private set; } #endregion #region 系统状态 public bool isCableRetracted = true; // 线缆是否收起 // 与 isCableRetracted(当前可交互状态)分开记录最后一次指令的目标, // 防止相反方向或外部 DOKill 触发旧动画回调,把线缆恢复到错误状态。 private bool targetCableRetracted = true; [SerializeField] private Transform retractedPos; // 收起位置 private ISocket curSocket; // 放线动画期间插头由 DOTween 驱动,暂停"插头跟随绳末端" private bool suppressPlugFollow; private Tween retractTween; #endregion private void Start() { InitComponentRefs(); InitSystem(); // 单线方案:旧视觉线缆整体停用,所有状态都走 PhysicCable if (cableRef != null) { cableRef.gameObject.SetActive(false); } InitCableVisualFromPrefab(); } private void OnDestroy() { plugRef.OnInsertSocket -= OnInsertSocket; plugRef.OnPullUpSocket -= OnPullUpSocket; plugRef.OnPlugPointerDown -= OnPlugPoinerDown; } private void InitComponentRefs() { BodyModuleSystem = FixSystemCenter.SystemDic.Get(); } public void InitLight(bool isLightOn) { panelLight?.Initialize(isLightOn); } private void InitSystem() { plugRef.OnInsertSocket += OnInsertSocket; plugRef.OnPullUpSocket += OnPullUpSocket; plugRef.OnPlugPointerDown += OnPlugPoinerDown; } private void InitCableVisualFromPrefab() { targetCableRetracted = isCableRetracted; if (isCableRetracted) { SetRetractCable(); } else { DropDownCableImmediate(); } } public bool TryGetPluggedModuleName(out string moduleName) { if (curSocket is BodyModule bodyModule) { moduleName = bodyModule.data.moduleName; return !string.IsNullOrEmpty(moduleName); } moduleName = null; return false; } /// 读档:同步写回线缆与插头终态,无动画、无 Yarn、无 PlugIn 事件。 public void ApplyCableSnapshot(bool retracted, string pluggedModuleName) { SilentPullUpSocket(); if (retracted) { if (ShouldSkipRetractOnRestore()) { if (isCableRetracted) { DropDownCableImmediate(); } curSocket = null; return; } SetRetractCable(); curSocket = null; return; } DropDownCableImmediate(); if (!string.IsNullOrEmpty(pluggedModuleName)) { RestorePlugToModule(pluggedModuleName); } else { curSocket = null; } } public void DropDownCableImmediate() { targetCableRetracted = false; retractTween?.Kill(false); retractTween = null; PlugRef.transform.DOKill(false); PlugRef.PullUpSocketSilent(); CableReelRef.ResetRotation(); PlugRef.RestoreAtStartPositionImmediate(); physicCableRef.ShowFree(true); isCableRetracted = false; } private void RestorePlugToModule(string moduleName) { var module = BodyModuleSystem?.FindBodyModuleByName(moduleName); if (module == null) { Debug.LogWarning($"[CablePanel] RestorePlugToModule: 未找到模块 {moduleName},插头留在初始位。"); curSocket = null; physicCableRef.ShowFree(true); return; } curSocket = module; var socketPos = module.GetSocketPos(); PlugRef.RestoreInsertedAt(socketPos); physicCableRef.PlugInto(socketPos, true); BodyModuleSystem.CloseModuleSlots(null); } private void SilentPullUpSocket() { if (curSocket == null) return; curSocket.PlugOut(); curSocket = null; PlugRef.SetSpriteVisible(true); } private static bool ShouldSkipRetractOnRestore() { return FixSystemCenter.Instance != null && FixSystemCenter.Director.HasMode && FixSystemCenter.Director.CurrentMode == FixSceneMode.BodyModule; } private void Update() { // 仅拖拽时由拉力驱动出线口倾斜;插接/松弛不预判朝向插头。 // GetTautness 已扣除送线设计余量,绷紧时才会转。 if (CableReelRef != null && physicCableRef != null && physicCableRef.State != PhysicCable.CableState.Hidden) { bool active = physicCableRef.State == PhysicCable.CableState.Dragging; float tautness = active ? physicCableRef.GetTautness() : 0f; CableReelRef.UpdateRotation(active, tautness); } } private void LateUpdate() { if (suppressPlugFollow || physicCableRef == null) return; // 闲置/拖拽时插头挂在绳末端,位置与朝向都由绳给出 var state = physicCableRef.State; if (state == PhysicCable.CableState.Free || state == PhysicCable.CableState.Dragging || state == PhysicCable.CableState.Collapsing) { physicCableRef.ApplyPlugPose(plugRef.transform, plugRef.PlugRoot); plugRef.ApplyFeedbackOffset(); } } /// /// 从插孔拔出(若已插入),放回初始可插线位:线缆放下、可拖拽,不收起。 /// public void ResetPlug() { bool wasPlugged = IsPlugInSocket(); if (wasPlugged) { PlugRef.PullUpSocket(); } BodyModuleSystem.CloseModuleSlots(null); // 已在初始可插线位且未收起:只关槽,不重复放线/音效 if (!wasPlugged && !isCableRetracted && !targetCableRetracted) { return; } DropDownCableImmediate(); AudioManager.Instance.PlaySfx("event:/ActionFB/mods_close"); AudioManager.Instance.PlaySfx("event:/FollowInput/plugdrop"); } // ------------------------------ 插头事件 ------------------------------ private void OnInsertSocket(ISocket socket) { var isSameSocket = curSocket == socket; curSocket = socket; physicCableRef.PlugInto(socket.GetSocketPos()); AudioManager.Instance.PlaySfx("event:/FollowInput/plugin"); if (!isSameSocket && !EventSystemEx.Instance.isLocked && socket is BodyModule bodyModule) { // 插入模块时触发插入事件 EnumEventSystem.Global.Send(EventEnum.PlugIn); } } private void OnPullUpSocket() { bool wasPlugged = curSocket != null; curSocket?.PlugOut(); AudioManager.Instance.PlaySfx("event:/FollowInput/plugout"); // 只要插头确实从 Socket 拔出,就通知 Screen 清空。 // 空闲状态下的防御性 PullUpSocket 调用不会误触发该事件。 if (wasPlugged) { EnumEventSystem.Global.Send(EventEnum.PlugOut); } curSocket = null; } private void OnPlugPoinerDown() { Transform unplugSocket = curSocket?.GetSocketPos(); bool wasPlugged = IsPlugInSocket(); if (wasPlugged) PlugRef.PullUpSocket(); // 末端钉到拖拽目标,按需送线;指针位置随 OnDrag 持续更新。 // 排序不随拖拽切换:线缆与插头常驻 FixTop 高位(原型画序固定不变) physicCableRef.BeginDrag(PlugRef.transform.position); if (wasPlugged) { physicCableRef.StartUnplugResistance(unplugSocket); } // 打开插孔 BodyModuleSystem.OpenModuleSlots(); } /// 拖拽中更新指针位置(世界坐标),由 Plug.OnDrag 调用。 public void UpdateDragPointer(Vector3 pointerWorldPos) { physicCableRef.SetPointer(pointerWorldPos); } /// 结束拖拽:关闭除指定保留模块外的插孔盖;不指定时全部关闭。 public void ClearDragHighlight(BodyModule preservedModule = null) { BodyModuleSystem?.CloseModuleSlots(preservedModule); } /// 松手且未插中插孔:解除钉住,线缆自动回收,插头挂回绳末端。 public void ReleasePlug() { ClearDragHighlight(); physicCableRef.EndDrag(); } // ------------------------------ 系统状态 ------------------------------ public bool IsPlugInSocket() { return curSocket != null; } // ------------------------------ 线缆操作 ------------------------------ /// /// 寻找最接近的Module /// /// 源位置 /// 最近的Module /// 是否能找到目标范围内的Module public bool TryFindClosestModule(Vector3 sourcePos, out BodyModule targetModule) { return BodyModuleSystem.TryFindClosestModule(sourcePos, out targetModule); } /// 收起线缆:无论是否插在模块上,统一收到线盘位。 public void PullUpCable() { bool wasPlugged = IsPlugInSocket(); if (wasPlugged) { PlugRef.PullUpSocket(); } BodyModuleSystem.CloseModuleSlots(null); // 已在收起或正在收起,且本次无需拔插:不重复收线动画/音效 if (targetCableRetracted && !wasPlugged) { isCableRetracted = true; return; } targetCableRetracted = true; isCableRetracted = true; retractTween?.Kill(false); retractTween = null; PlugRef.transform.DOKill(false); PlugRef.CancelFeedbackTween(); if (physicCableRef.State == PhysicCable.CableState.Hidden) { ApplyRetractedState(); return; } 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"); } private void CompleteRetractIfCurrent() { if (!targetCableRetracted) return; retractTween = null; ApplyRetractedState(); isCableRetracted = true; } public void DropDownCable() { if (!isCableRetracted && !targetCableRetracted) return; targetCableRetracted = false; retractTween?.Kill(false); retractTween = null; PlugRef.PullUpSocket(); PlugRef.transform.DOKill(false); CableReelRef.ResetRotation(); suppressPlugFollow = true; PlugRef.transform.DOMove(PlugRef.GetStartPos(), 0.1f).OnComplete(CompleteDropIfCurrent) .OnKill(CompleteDropIfCurrent); } private void CompleteDropIfCurrent() { if (targetCableRetracted) return; suppressPlugFollow = false; physicCableRef.ShowFree(true); isCableRetracted = false; } /// 应用收起状态的视觉与组件设置(不含动画、不含 isCableRetracted 更新) private void ApplyRetractedState() { physicCableRef.Hide(); suppressPlugFollow = false; PlugRef.CancelFeedbackTween(); PlugRef.transform.position = retractedPos.position; PlugRef.transform.rotation = retractedPos.rotation; var plugSprite = PlugRef.GetComponent(); plugSprite.sortingLayerName = CableReelRef.GetComponent().sortingLayerName; plugSprite.sortingOrder = SortingOrderRetracted; } public void SetRetractCable() { targetCableRetracted = true; isCableRetracted = true; retractTween?.Kill(false); retractTween = null; PlugRef.transform.DOKill(false); ApplyRetractedState(); } #region 灯光系统 - 供外部调用 /// /// 启动系统 /// public System.Collections.IEnumerator StartSystem() { if (panelLight != null) { yield return panelLight.StartSystem(); } } /// /// 更新呼吸灯 /// public void UpdateBreathingLight(LightState state) { panelLight?.UpdateBreathingLight(state); } /// /// 更新ECG波形 /// public void UpdateECGWave(LightState state) { panelLight?.UpdateECGWave(state); } /// /// 获取当前波形类型 /// public RobotECGWave.WaveType GetCurrentWaveType() { return panelLight != null ? panelLight.GetCurrentWaveType() : RobotECGWave.WaveType.RobotECG; } /// /// 设置波形类型 /// public void SetWaveType(RobotECGWave.WaveType waveType) { panelLight?.SetWaveType(waveType); } #endregion public void Init() { } public void Open() { } public void Close() { } } }