using System.Linq;
using AibisDream.Framework;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using DG.Tweening;
using System.Collections;
namespace AibisDream.MiniGame.HuoShan
{
///
/// 发条控制器 - 实现发条的拖拽旋转交互
/// 功能:拖拽旋转,最大260°,缓慢回位,触发事件
/// 交互通过 EventTriggerEx 统一管理,支持 EventSystemEx.isLocked 和 IInteraction 状态
/// 在发条 GameObject 上添加 EventTriggerEx + Collider2D 即可接入;未添加则回退到 Input+Raycast
///
public class CrankController : MonoBehaviour, IInteraction
{
[Header("发条配置")]
[Tooltip("最大旋转角度(度)")]
[SerializeField] private float maxRotationAngle = 260f;
[Tooltip("回位时长(秒),从满弦回到 0 的匀速时间,与滑片滑动时长匹配")]
[SerializeField] private float returnDuration = 3f;
[Tooltip("触发事件的最小角度(度)")]
[SerializeField] private float triggerAngle = 240f;
[Tooltip("拖拽阻力,值越大越难拖动")]
[SerializeField] private float dragResistance = 0.1f;
[Tooltip("是否自动返回初始位置")]
[SerializeField] private bool autoReturn = true;
[Header("事件" )]
[Tooltip("发条达到最大角度并释放时触发")]
public UnityEvent onCrankComplete;
[Tooltip("发条旋转时触发(参数:当前角度0-1)")]
public UnityEvent onCrankRotate;
[Tooltip("发条开始拖拽时触发")]
public UnityEvent onCrankStartDrag;
[Header("逆时针转圈表现")]
[Tooltip("逆时针转一圈的动画时长(秒)")]
[SerializeField] private float spinOnceDuration = 1f;
[Tooltip("逆时针转一圈完成后触发")]
public UnityEvent onSpinOnceComplete;
[Header("圆环/中心配置")]
[Tooltip("环中心(内环旋转中心),配置后发条将沿环轨道移动;不填则原地旋转")]
[SerializeField] private Transform ringCenter;
[Tooltip("环半径(发条到中心的距离),0 则从初始位置自动计算")]
[SerializeField] private float ringRadius = 0f;
[Tooltip("起始角度偏移(度),0=右侧3点钟方向")]
[SerializeField] private float startAngleOffset = 0f;
[Tooltip("面向圆心时的旋转偏移(度),用于调整图片朝向,不同 sprite 的正面方向不同")]
[SerializeField] private float facingRotationOffset = 0f;
[Tooltip("角度正方向:勾选=顺时针为正,不勾选=逆时针为正")]
[SerializeField] private bool clockwisePositive = false;
[Header("视觉反馈")]
[Tooltip("发条旋转的 Transform(手柄 visuals)")]
[SerializeField] private Transform crankTransform;
[Tooltip("用于检测点击的 Collider2D,不填则尝试从 crankTransform 获取")]
[SerializeField] private Collider2D interactionCollider;
[Tooltip("拖拽时的缩放效果")]
[SerializeField] private float dragScale = 1.05f;
[Tooltip("缩放动画时长")]
[SerializeField] private float scaleDuration = 0.1f;
[Header("交互提示")]
[Tooltip("高亮目标:发条 Sprite(材质需含 _ShineFade);不填则使用 crankTransform 上 SpriteRenderer")]
[SerializeField] private SpriteRenderer interactionHighlightSprite;
[Tooltip("开始拖拽发条时是否自动关闭高亮")]
[SerializeField] private bool clearHighlightWhenDragStarts = true;
[Header("上弦判定")]
[Tooltip("进入成功范围后吸附到终点的时长")]
[SerializeField] private float completionSnapDuration = 0.15f;
[Tooltip("当前轮未上满多少次后显示 Yarn 文本提示")]
[SerializeField] private int failedAttemptHintThreshold = 2;
[Tooltip("连续未上满后调用的 Yarn 节点")]
[SerializeField] private string failedAttemptHintNode = "发条操作提示";
private static readonly int ShineFadeId = Shader.PropertyToID("_ShineFade");
private SpriteRenderer _shineHighlightSprite;
private Vector3 _dragStartPosition;
private float _dragStartAngle; // 按下时鼠标相对中心的角度
private float _dragStartRotation; // 按下时发条的角度
private float _lastMouseAngle; // 上一帧鼠标角度,用于累计增量
private float _currentRotation = 0f;
private bool _isDragging = false;
private bool _hasTriggered = false;
private Camera _mainCamera;
private Vector3 _initialScale;
private Tweener _scaleTween;
private Tweener _returnTween;
private Tweener _spinTween;
private Tweener _completeTween;
private float _spinAngleOffset; // 逆时针转圈时的附加角度(0~360)
private bool _isSpinning; // 正在播放逆时针转圈
private float _effectiveRadius; // 实际使用的环半径
private bool _isReturning;
private bool _isCompleting;
private float _returnSpeedPerSec; // 回位时的度/秒,匀速
private int _failedAttemptCount;
private bool _failedAttemptHintShown;
private EventTriggerEx _eventTrigger;
private bool _useEventTrigger;
#region 生命周期
private void Awake()
{
_mainCamera = Camera.main;
if (crankTransform != null)
{
_initialScale = crankTransform.localScale;
}
else
{
_initialScale = transform.localScale;
crankTransform = transform;
}
maxRotationAngle = Mathf.Max(0.01f, maxRotationAngle);
dragResistance = Mathf.Clamp01(dragResistance);
// 完成判定不能超过画面上的旋转终点。
// 旧逻辑会在配置越界时悄悄改为 80%,导致画面、手感和判定使用不同的角度。
if (triggerAngle > maxRotationAngle)
{
Debug.LogWarning($"[CrankController] triggerAngle ({triggerAngle}°) 超过 maxRotationAngle ({maxRotationAngle}°),已对齐到旋转终点", this);
triggerAngle = maxRotationAngle;
}
triggerAngle = Mathf.Clamp(triggerAngle, 0f, maxRotationAngle);
// 未配置交互碰撞体时,尝试从 crankTransform 获取
if (interactionCollider == null && crankTransform != null)
{
interactionCollider = crankTransform.GetComponent();
#if UNITY_EDITOR
if (interactionCollider == null)
{
Debug.LogWarning($"[CrankController] 未找到 Collider2D!请在「发条」或其父物体上添加 Collider2D(如 CircleCollider2D),或在 interactionCollider 中手动指定。", this);
}
#endif
}
// 计算环半径:优先使用配置值,否则从发条到 ringCenter 的距离
if (ringCenter != null && crankTransform != null)
{
_effectiveRadius = ringRadius > 0f
? ringRadius
: Vector2.Distance(
new Vector2(crankTransform.position.x, crankTransform.position.y),
new Vector2(ringCenter.position.x, ringCenter.position.y));
if (_effectiveRadius < 0.01f) _effectiveRadius = 1f; // 防止除零
}
else
{
_effectiveRadius = 0f; // 原地旋转模式
}
// 事件目标:Collider 所在的对象(可能与 CrankController 不同,如手柄在子物体)
var eventTargetGo = interactionCollider != null ? interactionCollider.gameObject : (crankTransform != null ? crankTransform.gameObject : gameObject);
_eventTrigger = eventTargetGo.GetComponent();
if (_eventTrigger == null && eventTargetGo.GetComponent() != null)
{
if (eventTargetGo != gameObject)
{
var bridge = eventTargetGo.GetComponent();
if (bridge == null) eventTargetGo.AddComponent();
}
_eventTrigger = eventTargetGo.AddComponent();
}
if (_eventTrigger != null)
{
_useEventTrigger = true;
EnsureEventTriggerEntries();
_eventTrigger.Register(EventTriggerType.PointerDown, OnEventPointerDown);
_eventTrigger.Register(EventTriggerType.PointerUp, OnEventPointerUp);
_eventTrigger.Register(EventTriggerType.Drag, OnEventDrag);
}
EnsureInteractionHighlightSprite();
SetInteractionHighlightVisible(false);
}
private void EnsureInteractionHighlightSprite()
{
if (interactionHighlightSprite != null)
{
_shineHighlightSprite = interactionHighlightSprite;
return;
}
if (crankTransform != null)
_shineHighlightSprite = crankTransform.GetComponent()
?? crankTransform.GetComponentInChildren(true);
}
///
/// 控制发条交互提示高亮(材质 _ShineFade)。由外部脚本、Yarn 命令等在合适时机调用。
///
public void SetInteractionHighlightVisible(bool visible)
{
EnsureInteractionHighlightSprite();
if (_shineHighlightSprite == null) return;
var mat = _shineHighlightSprite.material;
if (mat == null || !mat.HasProperty(ShineFadeId)) return;
mat.SetFloat(ShineFadeId, visible ? 1f : 0f);
}
///
/// 确保 EventTrigger 有需要的触发类型(若使用 EventTriggerEx 管理交互)
///
private void EnsureEventTriggerEntries()
{
if (_eventTrigger == null) return;
var triggers = _eventTrigger.triggers;
var needed = new[] { EventTriggerType.PointerDown, EventTriggerType.PointerUp, EventTriggerType.Drag };
foreach (var t in needed)
{
if (triggers.Any(e => e.eventID == t)) continue;
triggers.Add(new EventTrigger.Entry { eventID = t });
}
}
private void OnEnable()
{
ResetFailedAttempts();
ResetCrank();
}
#if UNITY_EDITOR
private void OnDrawGizmosSelected()
{
if (ringCenter == null) return;
float r = ringRadius > 0f ? ringRadius : 0f;
if (r <= 0f && crankTransform != null && !Application.isPlaying)
{
r = Vector2.Distance(
new Vector2(crankTransform.position.x, crankTransform.position.y),
new Vector2(ringCenter.position.x, ringCenter.position.y));
}
if (r < 0.01f) return;
Vector3 center = ringCenter.position;
Gizmos.color = new Color(0.3f, 0.9f, 0.55f, 0.6f);
const int segments = 64;
for (int i = 0; i < segments; i++)
{
float a0 = (float)i / segments * 2f * Mathf.PI;
float a1 = (float)(i + 1) / segments * 2f * Mathf.PI;
Vector3 p0 = center + new Vector3(Mathf.Cos(a0) * r, Mathf.Sin(a0) * r, center.z);
Vector3 p1 = center + new Vector3(Mathf.Cos(a1) * r, Mathf.Sin(a1) * r, center.z);
Gizmos.DrawLine(p0, p1);
}
// 起点 / 终点标记(红=起点,橙=终点)
float startVisual = startAngleOffset;
float endVisual = clockwisePositive
? startAngleOffset - maxRotationAngle
: startAngleOffset + maxRotationAngle;
float startRadian = startVisual * Mathf.Deg2Rad;
float endRadian = endVisual * Mathf.Deg2Rad;
Vector3 startPos = center + new Vector3(Mathf.Cos(startRadian) * r, Mathf.Sin(startRadian) * r, center.z);
Vector3 endPos = center + new Vector3(Mathf.Cos(endRadian) * r, Mathf.Sin(endRadian) * r, center.z);
Gizmos.color = new Color(1f, 0f, 0f, 0.9f);
Gizmos.DrawWireSphere(startPos, 0.08f);
Gizmos.color = new Color(1f, 0.55f, 0f, 0.9f);
Gizmos.DrawWireSphere(endPos, 0.08f);
Gizmos.color = new Color(1f, 0.5f, 0f, 0.8f);
Gizmos.DrawWireSphere(center, 0.05f);
}
#endif
private void Update()
{
if (_useEventTrigger)
{
// 交互由 EventTriggerEx 驱动,此处仅处理回位
}
else
{
HandleMouseInputFallback();
}
HandleReturn();
}
#endregion
#region 输入处理(EventTriggerEx 驱动 / 回退)
private void OnEventPointerDown(BaseEventData eventData)
{
if (eventData is PointerEventData { button: PointerEventData.InputButton.Left })
StartDrag();
}
private void OnEventPointerUp(BaseEventData eventData)
{
if (eventData is PointerEventData { button: PointerEventData.InputButton.Left })
EndDrag();
}
private void OnEventDrag(BaseEventData eventData)
{
if (_isDragging && eventData is PointerEventData pointerData)
ContinueDrag(pointerData.position);
}
///
/// 回退:无 EventTriggerEx 时使用 Input + Raycast(如 prefab 未迁移场景)
///
private void HandleMouseInputFallback()
{
if (Input.GetMouseButtonDown(0))
{
Ray ray = _mainCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit2D hit = Physics2D.GetRayIntersection(ray, float.MaxValue);
if (hit.collider != null && IsValidHitTarget(hit.collider.gameObject))
{
StartDrag();
}
}
if (_isDragging)
{
if (Input.GetMouseButton(0))
{
ContinueDrag(Input.mousePosition);
}
else if (Input.GetMouseButtonUp(0))
{
EndDrag();
}
}
}
///
/// 获取旋转中心(环中心优先,否则用发条父节点)
///
private Vector3 GetRotationPivot()
{
if (ringCenter != null) return ringCenter.position;
if (crankTransform != null && crankTransform.parent != null) return crankTransform.parent.position;
return crankTransform != null ? crankTransform.position : transform.position;
}
///
/// 应用发条的位置和旋转。配置了 ringCenter 时沿环轨道移动,否则原地旋转。
///
private void ApplyCrankTransform(float angle)
{
if (crankTransform == null) return;
// 顺时针为正时,视觉上角度取反(数学角度逆时针为正)
float visualAngle = clockwisePositive ? -angle : angle;
float positionAngle = startAngleOffset + visualAngle + _spinAngleOffset;
if (ringCenter != null && _effectiveRadius > 0f)
{
// 沿环轨道移动:更新世界坐标位置
float radian = positionAngle * Mathf.Deg2Rad;
Vector3 center = ringCenter.position;
float z = crankTransform.position.z;
Vector3 position = center + new Vector3(
Mathf.Cos(radian) * _effectiveRadius,
Mathf.Sin(radian) * _effectiveRadius,
z
);
crankTransform.position = position;
// 始终面向圆心(径向朝内),加 offset 调整 sprite 正面朝向
Vector3 toCenter = center - position;
float radialAngle = Mathf.Atan2(toCenter.y, toCenter.x) * Mathf.Rad2Deg;
crankTransform.rotation = Quaternion.Euler(0f, 0f, radialAngle + facingRotationOffset);
}
else
{
// 原地旋转:-visualAngle 为基础朝向,+_spinAngleOffset 为逆时针转圈叠加
crankTransform.localRotation = Quaternion.Euler(0f, 0f, -visualAngle + _spinAngleOffset);
}
}
///
/// 检查点击目标是否有效(本物体、发条、交互碰撞体或其子物体)
///
private bool IsValidHitTarget(GameObject hitGo)
{
if (hitGo == gameObject) return true;
if (crankTransform != null && (hitGo == crankTransform.gameObject || hitGo.transform.IsChildOf(crankTransform))) return true;
if (interactionCollider != null && hitGo == interactionCollider.gameObject) return true;
return false;
}
#endregion
#region 拖拽逻辑
private void StartDrag()
{
if (clearHighlightWhenDragStarts)
SetInteractionHighlightVisible(false);
_isDragging = true;
_dragStartPosition = Input.mousePosition;
_dragStartRotation = _currentRotation;
_dragStartAngle = GetMouseAngle();
_lastMouseAngle = _dragStartAngle;
_hasTriggered = false;
if (_returnTween != null)
{
_returnTween.Kill();
_returnTween = null;
}
if (_spinTween != null)
{
_spinTween.Kill();
_spinTween = null;
_spinAngleOffset = 0f;
_isSpinning = false;
}
_isReturning = false;
// 拖拽缩放效果
if (_scaleTween != null) _scaleTween.Kill();
_scaleTween = crankTransform.DOScale(_initialScale * dragScale, scaleDuration)
.SetEase(Ease.OutQuad);
onCrankStartDrag?.Invoke();
}
///
/// 获取鼠标相对环中心的角度(0-360)
///
private float GetMouseAngle(Vector3? screenPos = null)
{
Vector3 pivotWorld = GetRotationPivot();
Vector3 screenCenter = _mainCamera.WorldToScreenPoint(pivotWorld);
Vector3 pos = screenPos ?? Input.mousePosition;
float a = Mathf.Atan2(pos.y - screenCenter.y, pos.x - screenCenter.x) * Mathf.Rad2Deg;
return a < 0f ? a + 360f : a;
}
private void ContinueDrag(Vector3 screenPosition)
{
if (_isCompleting) return;
// 每帧累计角度增量,支持超过 180° 的拖拽(原先用总 delta 会在 180° 处反转)
float currentAngle = GetMouseAngle(screenPosition);
float frameDelta = currentAngle - _lastMouseAngle;
// 仅对单帧小增量做 0/360 边界 wrap
if (frameDelta > 180f) frameDelta -= 360f;
else if (frameDelta < -180f) frameDelta += 360f;
_lastMouseAngle = currentAngle;
if (clockwisePositive) frameDelta = -frameDelta;
float targetRotation = _currentRotation + frameDelta * (1f - dragResistance);
// 拖拽一帧跨过起点/终点时直接夹到边界。
// 若拒绝越界的整帧移动,当触发角度等于最大角度时就可能永远到不了终点。
_currentRotation = Mathf.Clamp(targetRotation, 0f, maxRotationAngle);
ApplyCrankTransform(_currentRotation);
float normalizedRotation = _currentRotation / maxRotationAngle;
onCrankRotate?.Invoke(normalizedRotation);
// 检查是否达到触发角度
if (_currentRotation >= triggerAngle && !_hasTriggered)
{
_hasTriggered = true;
Debug.Log($"[CrankController] 达到触发角度!当前角度: {_currentRotation:F1}°, 触发角度: {triggerAngle}°", this);
BeginAutoComplete();
}
}
private void BeginAutoComplete()
{
if (_isCompleting) return;
_isCompleting = true;
_isDragging = false;
_isReturning = false;
_failedAttemptCount = 0;
_failedAttemptHintShown = false;
_scaleTween?.Kill();
_scaleTween = crankTransform.DOScale(_initialScale, scaleDuration).SetEase(Ease.OutQuad);
_completeTween?.Kill();
_completeTween = DOTween.To(
() => _currentRotation,
value =>
{
_currentRotation = value;
ApplyCrankTransform(_currentRotation);
onCrankRotate?.Invoke(_currentRotation / maxRotationAngle);
},
maxRotationAngle,
Mathf.Max(0.01f, completionSnapDuration))
// OutBack 会超过 maxRotationAngle,导致实际画面角度大于配置上限。
.SetEase(Ease.OutCubic)
.OnComplete(() =>
{
_completeTween = null;
_isCompleting = false;
onCrankComplete?.Invoke();
});
}
private void EndDrag()
{
if (_isCompleting) return;
_isDragging = false;
// 恢复缩放
if (_scaleTween != null) _scaleTween.Kill();
_scaleTween = crankTransform.DOScale(_initialScale, scaleDuration)
.SetEase(Ease.OutQuad);
if (!_hasTriggered || _currentRotation < triggerAngle)
{
_failedAttemptCount++;
Debug.Log($"[CrankController] EndDrag 未触发:hasTriggered={_hasTriggered}, currentRotation={_currentRotation:F1}°, triggerAngle={triggerAngle}°", this);
if (!_failedAttemptHintShown && _failedAttemptCount >= Mathf.Max(1, failedAttemptHintThreshold))
{
_failedAttemptHintShown = true;
if (!string.IsNullOrEmpty(failedAttemptHintNode))
DialogController.Instance?.StartDialogNode(failedAttemptHintNode);
}
}
}
#endregion
#region 回位逻辑
private void HandleReturn()
{
if (_isCompleting) return;
if (!_isDragging && autoReturn && _currentRotation > 0f && !_isReturning && !_isSpinning)
{
_isReturning = true;
_returnSpeedPerSec = maxRotationAngle / Mathf.Max(0.01f, returnDuration);
}
if (_isReturning && !_isDragging)
{
float step = _returnSpeedPerSec * Time.deltaTime;
_currentRotation = Mathf.MoveTowards(_currentRotation, 0f, step);
ApplyCrankTransform(_currentRotation);
onCrankRotate?.Invoke(_currentRotation / maxRotationAngle);
if (_currentRotation <= 0f)
{
_currentRotation = 0f;
_isReturning = false;
}
}
}
#endregion
#region 公共方法
public void ResetCrank()
{
if (_returnTween != null)
{
_returnTween.Kill();
_returnTween = null;
}
if (_spinTween != null)
{
_spinTween.Kill();
_spinTween = null;
}
if (_completeTween != null)
{
_completeTween.Kill();
_completeTween = null;
}
_spinAngleOffset = 0f;
_isSpinning = false;
_isReturning = false;
_isCompleting = false;
_isDragging = false;
_hasTriggered = false;
ResetFailedAttempts();
_currentRotation = 0f;
if (crankTransform != null)
{
ApplyCrankTransform(0f);
crankTransform.localScale = _initialScale;
}
}
private void ResetFailedAttempts()
{
_failedAttemptCount = 0;
_failedAttemptHintShown = false;
}
private void OnDestroy()
{
_scaleTween?.Kill();
_returnTween?.Kill();
_spinTween?.Kill();
_completeTween?.Kill();
}
///
/// 逆时针转一圈的表现(可被 Yarn 命令、事件等触发)
///
/// 动画时长,不传则使用 spinOnceDuration
public void PlaySpinCounterClockwise(float? duration = null)
{
float d = duration ?? spinOnceDuration;
if (d <= 0f) d = 0.5f;
if (_spinTween != null)
{
_spinTween.Kill();
_spinTween = null;
}
_spinAngleOffset = 0f;
_isSpinning = true;
_spinTween = DOTween.To(
() => _spinAngleOffset,
value =>
{
_spinAngleOffset = value;
ApplyCrankTransform(_currentRotation);
},
360f,
d
).SetEase(Ease.Linear).OnComplete(() =>
{
_spinAngleOffset = 0f;
_spinTween = null;
_isSpinning = false;
ApplyCrankTransform(_currentRotation);
onSpinOnceComplete?.Invoke();
});
}
///
/// 手动旋转到指定角度(用于动画或程序化控制)
///
public void RotateTo(float angle, float duration = 0.5f)
{
angle = Mathf.Clamp(angle, 0f, maxRotationAngle);
if (_returnTween != null)
{
_returnTween.Kill();
_returnTween = null;
}
DOTween.To(
() => _currentRotation,
value => {
_currentRotation = value;
ApplyCrankTransform(_currentRotation);
float normalizedRotation = _currentRotation / maxRotationAngle;
onCrankRotate?.Invoke(normalizedRotation);
},
angle,
duration
).SetEase(Ease.OutCubic);
}
///
/// 获取当前旋转角度(0-1标准化)
///
public float GetNormalizedRotation()
{
return _currentRotation / maxRotationAngle;
}
///
/// 获取当前旋转角度(度)
///
public float GetCurrentAngle()
{
return _currentRotation;
}
///
/// 是否正在拖拽中
///
public bool IsDragging => _isDragging;
///
/// 是否已达到触发角度
///
public bool HasTriggered => _hasTriggered;
///
/// 是否正在播放逆时针转圈
///
public bool IsSpinning => _isSpinning;
///
/// 回位时长(秒),供滑片等配合使用
///
public float ReturnDuration => returnDuration;
///
/// 设置回位时长(由 SalesManager 同步每次转动时间)
///
public void SetReturnDuration(float duration)
{
returnDuration = Mathf.Max(0.01f, duration);
}
#endregion
#region IInteraction(EventTriggerEx 统一交互)
public bool IsActive => true;
public bool IsAvailable => true;
public GameObject GetGameObject() => gameObject;
#endregion
}
///
/// 发条交互桥接:挂在 Collider 所在子物体上,将 IInteraction 转交给父级 CrankController
///
public class CrankInteractionBridge : MonoBehaviour, IInteraction
{
private CrankController _controller;
private void Awake()
{
_controller = GetComponentInParent();
}
public bool IsActive => _controller?.IsActive ?? true;
public bool IsAvailable => _controller?.IsAvailable ?? true;
public GameObject GetGameObject() => gameObject;
}
}