352 lines
12 KiB
C#
352 lines
12 KiB
C#
using System;
|
|
using AibisDream.Framework;
|
|
using AibisDream.Kit;
|
|
using AibisDream.Utility;
|
|
using DG.Tweening;
|
|
using UnityEngine;
|
|
using UnityEngine.EventSystems;
|
|
|
|
namespace AibisDream.FixSystem
|
|
{
|
|
[RequireComponent(typeof(EventTriggerEx))]
|
|
public class Plug : MonoBehaviour, IInteraction
|
|
{
|
|
private static readonly int ShineFade = Shader.PropertyToID("_ShineFade");
|
|
|
|
// 插头常态排序:与线缆(Tools/47)同层且在其上,拖拽不再切层——
|
|
// 原型里插头永远画在线之上、画序不随交互变化;整套线缆系统被外部元素
|
|
// 遮挡时也必须整体一起被盖,不能插头浮出而线身沉底。
|
|
// Tools 整层高于 FixTop(屏幕 UI Canvas 在 FixTop/100),Tools/48 是插头
|
|
// 历史沿用且验证过不被面板遮挡的位置;剪线演出(Tools/50+)仍可盖住整套线缆
|
|
private const string NormalSortingLayer = "Tools";
|
|
private const int NormalSortingOrder = 48;
|
|
|
|
#region 插头当前信息
|
|
|
|
[SerializeField] private Vector3 startPosOffset = new(0.3f, -2f, 0);
|
|
private Vector3 _startPos;
|
|
|
|
private readonly Quaternion _defaultRotation = Quaternion.Euler(0, 0, 180);
|
|
|
|
#endregion
|
|
|
|
#region 索引
|
|
|
|
private CablePanel cablePanel;
|
|
private SpriteRenderer _sprite;
|
|
private Transform _plugRootPos;
|
|
private EventTriggerEx _trigger;
|
|
|
|
public Transform PlugRoot => _plugRootPos;
|
|
|
|
#endregion
|
|
|
|
public event Action<ISocket> OnInsertSocket;
|
|
public event Action OnPullUpSocket;
|
|
public event Action OnPlugPointerDown;
|
|
|
|
private bool _isDragging;
|
|
private bool _isInserting;
|
|
private Tween _insertTween;
|
|
private Tween _unplugTween;
|
|
private Vector3 _feedbackOffset;
|
|
|
|
[Header("插入手感")]
|
|
[SerializeField] private float insertWindupDistance = 0.08f;
|
|
[SerializeField] private float insertWindupDuration = 0.045f;
|
|
[SerializeField] private float insertSnapDuration = 0.11f;
|
|
[SerializeField] private float insertSnapOvershoot = 1.45f;
|
|
[SerializeField] private float insertPunchScale = 0.045f;
|
|
|
|
[Header("拔出手感")]
|
|
[SerializeField] private float unplugPopDistance = 0.13f;
|
|
[SerializeField] private float unplugPopDuration = 0.07f;
|
|
[SerializeField] private float unplugReturnDuration = 0.12f;
|
|
[SerializeField] private float unplugPunchScale = 0.065f;
|
|
|
|
private void Awake()
|
|
{
|
|
InitComponentRef();
|
|
EventRegister();
|
|
_startPos = transform.position + startPosOffset;
|
|
|
|
// 预制体里序列化的还是旧的 Tools 层,启动时归一化;收起态由 CablePanel 之后覆盖
|
|
ApplyNormalSorting();
|
|
}
|
|
|
|
private void InitComponentRef()
|
|
{
|
|
cablePanel = transform.GetComponentInParent<CablePanel>(true);
|
|
_sprite = GetComponent<SpriteRenderer>();
|
|
_plugRootPos = transform.Find("Plug Root Pos");
|
|
_trigger = GetComponent<EventTriggerEx>();
|
|
|
|
// 处理插头高亮
|
|
_sprite.material.SetFloat(ShineFade, GlobalVariableKit.IsPlugHighLight ? 1 : 0);
|
|
}
|
|
|
|
private void EventRegister()
|
|
{
|
|
_trigger.Register(EventTriggerType.Drag, OnDrag);
|
|
_trigger.Register(EventTriggerType.PointerDown, OnPointerDown);
|
|
_trigger.Register(EventTriggerType.PointerUp, OnPointerUp);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 插入某个socket
|
|
/// </summary>
|
|
/// <param name="targetSocket">目标socket</param>
|
|
private void InsertSocket(ISocket targetSocket)
|
|
{
|
|
if (_isInserting) return;
|
|
_isInserting = true;
|
|
|
|
cablePanel.ClearDragHighlight();
|
|
ClearFeedbackOffset();
|
|
|
|
var targetPos = targetSocket.GetSocketPos().position;
|
|
_insertTween?.Kill();
|
|
transform.DOKill(false);
|
|
transform.localScale = Vector3.one;
|
|
|
|
Vector3 insertDir = targetPos - transform.position;
|
|
insertDir.z = 0;
|
|
if (insertDir.sqrMagnitude < 1e-4f) insertDir = transform.right;
|
|
insertDir.Normalize();
|
|
|
|
var sequence = DOTween.Sequence();
|
|
if (insertWindupDistance > 0f && insertWindupDuration > 0f)
|
|
{
|
|
sequence.Append(transform.DOMove(transform.position - insertDir * insertWindupDistance, insertWindupDuration)
|
|
.SetEase(Ease.OutSine));
|
|
}
|
|
|
|
sequence.Append(transform.DOMove(targetPos, insertSnapDuration)
|
|
.SetEase(Ease.OutBack, insertSnapOvershoot))
|
|
.Insert(0f, transform.DOPunchScale(Vector3.one * insertPunchScale,
|
|
insertWindupDuration + insertSnapDuration, 7, 0.45f))
|
|
.OnUpdate(() => cablePanel.UpdateDragPointer(transform.position))
|
|
.OnComplete(() =>
|
|
{
|
|
targetSocket.PlugIn();
|
|
_sprite.enabled = false;
|
|
_isInserting = false;
|
|
OnInsertSocket?.Invoke(targetSocket);
|
|
});
|
|
_insertTween = sequence;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 从某个Socket拔出来
|
|
/// </summary>
|
|
public void PullUpSocket()
|
|
{
|
|
_insertTween?.Kill();
|
|
_isInserting = false;
|
|
|
|
_sprite.enabled = true;
|
|
ApplyNormalSorting();
|
|
|
|
transform.DOKill(false);
|
|
transform.localScale = Vector3.one;
|
|
PlayUnplugFeedback();
|
|
|
|
OnPullUpSocket?.Invoke();
|
|
}
|
|
|
|
/// <summary>读档/静默:仅恢复插头精灵可见,不触发 CablePanel 事件。</summary>
|
|
public void PullUpSocketSilent()
|
|
{
|
|
ClearFeedbackOffset();
|
|
_sprite.enabled = true;
|
|
ApplyNormalSorting();
|
|
}
|
|
|
|
/// <summary>恢复插头常态排序(收起态会被 CablePanel 压到线盘下方,放下时调回)。</summary>
|
|
public void ApplyNormalSorting()
|
|
{
|
|
_sprite.sortingLayerName = NormalSortingLayer;
|
|
_sprite.sortingOrder = NormalSortingOrder;
|
|
}
|
|
|
|
/// <summary>读档:无动画回到初始位。</summary>
|
|
public void RestoreAtStartPositionImmediate()
|
|
{
|
|
ClearFeedbackOffset();
|
|
transform.rotation = _defaultRotation;
|
|
transform.position = _startPos;
|
|
}
|
|
|
|
/// <summary>读档:无动画插入指定 socket 的视觉终态。</summary>
|
|
public void RestoreInsertedAt(Transform socketPos)
|
|
{
|
|
if (socketPos == null) return;
|
|
ClearFeedbackOffset();
|
|
transform.position = socketPos.position;
|
|
_sprite.enabled = false;
|
|
}
|
|
|
|
public void SetSpriteVisible(bool visible)
|
|
{
|
|
_sprite.enabled = visible;
|
|
}
|
|
|
|
public void ApplyFeedbackOffset()
|
|
{
|
|
if (_feedbackOffset.sqrMagnitude <= 1e-6f) return;
|
|
transform.position += _feedbackOffset;
|
|
}
|
|
|
|
private void PlayUnplugFeedback()
|
|
{
|
|
_unplugTween?.Kill();
|
|
_feedbackOffset = Vector3.zero;
|
|
|
|
Vector3 releaseDir = GetUnplugReleaseDirection();
|
|
Vector3 popOffset = releaseDir * Mathf.Max(0f, unplugPopDistance);
|
|
var sequence = DOTween.Sequence();
|
|
|
|
if (popOffset.sqrMagnitude > 1e-6f && unplugPopDuration > 0f)
|
|
{
|
|
sequence.Append(DOTween.To(() => _feedbackOffset, value => _feedbackOffset = value,
|
|
popOffset, unplugPopDuration)
|
|
.SetEase(Ease.OutCubic));
|
|
}
|
|
|
|
if (unplugReturnDuration > 0f)
|
|
{
|
|
sequence.Append(DOTween.To(() => _feedbackOffset, value => _feedbackOffset = value,
|
|
Vector3.zero, unplugReturnDuration)
|
|
.SetEase(Ease.OutBack, 1.25f));
|
|
}
|
|
else
|
|
{
|
|
sequence.AppendCallback(() => _feedbackOffset = Vector3.zero);
|
|
}
|
|
|
|
if (unplugPunchScale > 0f)
|
|
{
|
|
sequence.Insert(0f, transform.DOPunchScale(Vector3.one * unplugPunchScale,
|
|
unplugPopDuration + unplugReturnDuration, 7, 0.42f));
|
|
}
|
|
|
|
sequence.OnKill(() => _feedbackOffset = Vector3.zero)
|
|
.OnComplete(() => _feedbackOffset = Vector3.zero);
|
|
_unplugTween = sequence;
|
|
}
|
|
|
|
private Vector3 GetUnplugReleaseDirection()
|
|
{
|
|
Vector3 releaseDir = Vector3.zero;
|
|
if (cablePanel != null && cablePanel.CableRootPos != null)
|
|
{
|
|
releaseDir = cablePanel.CableRootPos.position - transform.position;
|
|
}
|
|
|
|
releaseDir.z = 0;
|
|
if (releaseDir.sqrMagnitude < 1e-4f)
|
|
{
|
|
releaseDir = -transform.right;
|
|
releaseDir.z = 0;
|
|
}
|
|
|
|
return releaseDir.sqrMagnitude > 1e-4f ? releaseDir.normalized : Vector3.left;
|
|
}
|
|
|
|
private void ClearFeedbackOffset()
|
|
{
|
|
_unplugTween?.Kill();
|
|
_feedbackOffset = Vector3.zero;
|
|
transform.localScale = Vector3.one;
|
|
}
|
|
|
|
private void OnDrag(BaseEventData eventData)
|
|
{
|
|
if (!_isDragging) return;
|
|
|
|
if (eventData is PointerEventData pointerData)
|
|
{
|
|
var pointerWorldPos = CameraKit.GetMouseWorldPos(pointerData.position);
|
|
cablePanel.UpdateDragPointer(pointerWorldPos);
|
|
cablePanel.UpdateDragProximity(pointerWorldPos);
|
|
}
|
|
}
|
|
|
|
private void OnPointerDown(BaseEventData eventData)
|
|
{
|
|
if (EventSystemEx.Instance.isLocked) return;
|
|
if (eventData is PointerEventData { button: PointerEventData.InputButton.Right })
|
|
return;
|
|
|
|
ApplyNormalSorting();
|
|
|
|
OnPlugPointerDown?.Invoke();
|
|
|
|
TryCloseHighlight();
|
|
|
|
_isDragging = true;
|
|
|
|
AudioManager.Instance.PlaySfx("event:/FollowInput/plugpick");
|
|
}
|
|
|
|
private void TryCloseHighlight()
|
|
{
|
|
if (!GlobalVariableKit.IsPlugHighLight) return;
|
|
GlobalVariableKit.IsPlugHighLight = false;
|
|
_sprite.material.SetFloat(ShineFade, 0);
|
|
}
|
|
|
|
private void OnPointerUp(BaseEventData eventData)
|
|
{
|
|
if (EventSystemEx.Instance.isLocked)
|
|
{
|
|
_isDragging = false;
|
|
return;
|
|
}
|
|
if (eventData is PointerEventData { button: PointerEventData.InputButton.Right })
|
|
{
|
|
return;
|
|
}
|
|
|
|
// 停止拖拽
|
|
_isDragging = false;
|
|
|
|
// 寻找可吸附的Module
|
|
if (cablePanel.TryFindClosestModule(transform.position, out var targetModule))
|
|
{
|
|
InsertSocket(targetModule);
|
|
}
|
|
else
|
|
{
|
|
ReturnToStartPosition();
|
|
}
|
|
}
|
|
|
|
public void ReturnToStartPosition()
|
|
{
|
|
// 解除末端钉住,线缆自动回收到最短长度,插头挂在绳末端跟着回去
|
|
cablePanel.ReleasePlug();
|
|
}
|
|
|
|
void OnDrawGizmosSelected()
|
|
{
|
|
Gizmos.color = Color.yellow;
|
|
Gizmos.DrawWireSphere(transform.position, 0.5f);
|
|
}
|
|
|
|
public bool IsActive => true;
|
|
|
|
public bool IsAvailable => !cablePanel.isCableRetracted;
|
|
|
|
public GameObject GetGameObject()
|
|
{
|
|
return gameObject;
|
|
}
|
|
|
|
public Vector3 GetStartPos()
|
|
{
|
|
return _startPos;
|
|
}
|
|
}
|
|
}
|