using System.Collections;
using System.Linq;
using AibisDream;
using AibisDream.Framework;
using AibisDream.FixSystem;
using DG.Tweening;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using Yarn.Unity;
namespace AibisDream.MiniGame.HuoShan
{
///
/// 处理器旋钮控制器 — 完整移植 Web_EmotionWave glow-tube 分支的交互体验
/// 交互通过 EventTriggerEx 统一管理,支持 EventSystemEx.isLocked 和 IInteraction 状态
/// 核心特性:累计旋转、输入阻力、噪声颤抖、回弹前蓄力预兆、底座发热变色
/// 保留原有公共接口(事件、Yarn 命令、功率查询)
/// 在旋钮 GameObject 上添加 EventTriggerEx + Collider2D 即可接入;未添加则回退到 Input+Raycast
///
public class KnobController : MonoBehaviour, IInteraction
{
#region Serialized Fields
[Header("旋钮 Transform")]
[Tooltip("旋钮旋转的 Transform")]
[SerializeField] private Transform knobTransform;
[Tooltip("旋钮底座 SpriteRenderer(发热变色)")]
[SerializeField] private SpriteRenderer knobPlate;
[Tooltip("功率指示器 Transform(旋钮上的标记线)")]
[SerializeField] private Transform powerIndicator;
[Tooltip("用于检测点击的 Collider2D")]
[SerializeField] private Collider2D interactionCollider;
[Header("旋转参数")]
[Tooltip("最大累计圈数")]
[SerializeField] private int maxTurns = 10;
[Tooltip("旋转方向:勾选则顺时针拧=降功率(与 Web 原型一致)")]
[SerializeField] private bool clockwiseDecreasesPower = true;
[Tooltip("停手后到回弹的酝酿时间 (秒)")]
[SerializeField] private float idleDelay = 0.8f;
[Tooltip("回弹基础时长 (秒)")]
[SerializeField] private float snapbackDuration = 0.8f;
[Header("辉光管")]
[Tooltip("辉光管粒子效果(不填则从 SaleView 下查找)")]
[SerializeField] private GlowTubeEffect glowTubeEffect;
[Header("信息面板")]
[SerializeField] private GameObject infoPanel;
[SerializeField] private UnityEngine.UI.Text infoText;
[SerializeField] private string failMessage = "表达模块拒绝了未被处理的波形";
[SerializeField] private float panelShowDuration = 0.3f;
[SerializeField] private float panelHideDuration = 0.2f;
[Header("Yarn")]
[Tooltip("单次失败时跳转的 Yarn 节点(Yarn 根据 $knobFailCount 决定:重试 reset_knob 或 lock_knob 后跳 failNode)")]
[SerializeField] private string singleFailNode = "处理器单次调节失败";
[Header("失败判定")]
[Tooltip("旋转到最大角度的百分比后判定为失败(0-1)")]
[SerializeField] private float failThreshold = 0.3f;
[Header("旋钮盖子")]
[Tooltip("旋钮盖子 Transform(步进模式/旋钮/盖子),不填则尝试自动查找")]
[SerializeField] private Transform knobLid;
[Tooltip("打开盖子时的位移偏移(LocalSpace)")]
[SerializeField] private Vector3 lidOpenOffset = new Vector3(0f, 2f, 0f);
[Tooltip("盖子开合动画时长")]
[SerializeField] private float lidAnimDuration = 0.4f;
[Header("交互提示")]
[Tooltip("高亮目标:旋钮本体 Sprite(一般为 knobPlate,材质需含 _ShineFade);不填则使用 knobPlate,再回退为 knobTransform 上 SpriteRenderer(不含盖子子物体)")]
[SerializeField] private SpriteRenderer interactionHighlightSprite;
[Tooltip("开始拖拽旋钮时是否自动关闭高亮")]
[SerializeField] private bool clearHighlightWhenDragStarts = true;
[Header("环形进度条")]
[Tooltip("灯带进度条 SpriteRenderer;不填则尝试自动查找")]
[SerializeField] private SpriteRenderer ringProgressRenderer;
[Tooltip("自动查找的灯带节点名")]
[SerializeField] private string ringProgressChildName = "灯带进度条";
[Tooltip("环形填充起始角度,0=右,90=上")]
[SerializeField] private float ringProgressStartAngle = 90f;
[Tooltip("勾选则按顺时针方向填充")]
[SerializeField] private bool ringProgressClockwise = true;
[Header("事件")]
public UnityEvent onPowerAdjustmentStart;
public UnityEvent onPowerChanged;
public UnityEvent onPowerAdjustmentFail;
public UnityEvent onPowerAdjustmentSuccess;
[Tooltip("回弹完成时触发")]
public UnityEvent onSnapbackComplete;
#endregion
#region Private State
private float _maxAngle;
private float _knobAngle;
private float _knobValue;
private float _intensity = 1f;
private float _displayIntensity = 1f;
private bool _isDragging;
private bool _isSnappingBack;
private bool _snapbackLocked;
// Tremor
private float _tremorAmount;
private float _tremorTarget;
private float _seedA;
private float _seedB;
// Idle / warning
private float _idleStartTime;
private bool _isIdle;
private Coroutine _idleCoroutine;
// Overshoot
private float _overshootIntensity;
private float _overshootDecay;
// Resistance pulse
private float _resistancePulse;
private float _nextPulseTime;
private float _animTime;
private float _prevMouseAngle;
private Camera _mainCamera;
private bool _hasFailed;
private bool _hasSucceeded;
private float _currentPower = 100f;
private int _failCount;
private bool _isLocked;
private Color _plateOriginalColor;
private Vector3 _lidClosedPosition;
private Tweener _lidTween;
private EventTriggerEx _eventTrigger;
private bool _useEventTrigger;
private Material _ringProgressMaterial;
private const string RingProgressShaderName = "AibisDream/SpriteRadialProgress";
private const string RingProgressShaderResourcePath = "Shader/SpriteRadialProgress";
private static readonly int MainTexId = Shader.PropertyToID("_MainTex");
private static readonly int ProgressId = Shader.PropertyToID("_Progress");
private static readonly int StartAngleId = Shader.PropertyToID("_StartAngle");
private static readonly int FillClockwiseId = Shader.PropertyToID("_FillClockwise");
private static readonly int ShineFadeId = Shader.PropertyToID("_ShineFade");
private SpriteRenderer _shineHighlightSprite;
#endregion
#region Lifecycle
private void Awake()
{
_mainCamera = Camera.main;
_maxAngle = 360f * maxTurns;
_seedA = Random.Range(0f, 1000f);
_seedB = Random.Range(0f, 1000f);
if (knobTransform == null) knobTransform = transform;
if (interactionCollider == null && knobTransform != null)
interactionCollider = knobTransform.GetComponent();
if (knobPlate != null) _plateOriginalColor = knobPlate.color;
if (infoPanel != null) infoPanel.SetActive(false);
// 事件目标:Collider 所在的对象(可能与 KnobController 不同,如旋钮 visuals 在子物体)
var eventTargetGo = interactionCollider != null ? interactionCollider.gameObject : (knobTransform != null ? knobTransform.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);
}
EnsureKnobLid();
if (knobLid != null)
_lidClosedPosition = knobLid.localPosition;
EnsureRingProgress();
EnsureInteractionHighlightSprite();
SetInteractionHighlightVisible(false);
}
private void EnsureInteractionHighlightSprite()
{
if (interactionHighlightSprite != null)
{
_shineHighlightSprite = interactionHighlightSprite;
return;
}
if (knobPlate != null)
{
_shineHighlightSprite = knobPlate;
return;
}
if (knobTransform != null)
_shineHighlightSprite = knobTransform.GetComponent();
}
///
/// 控制旋钮交互提示高亮(材质 _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);
}
///
/// 查找盖子 Transform(步进模式/旋钮/盖子),未配置时自动按路径查找
///
private void EnsureKnobLid()
{
if (knobLid != null) return;
var t = transform;
while (t != null)
{
if (t.name == "SaleView" || t.name == "SellView")
{
knobLid = t.Find("步进模式/旋钮/盖子");
if (knobLid != null)
Debug.Log("[KnobController] 自动找到盖子:步进模式/旋钮/盖子", this);
break;
}
t = t.parent;
}
}
private void EnsureRingProgress()
{
if (ringProgressRenderer == null && knobTransform != null && !string.IsNullOrEmpty(ringProgressChildName))
{
var ring = knobTransform.Find(ringProgressChildName);
if (ring != null)
ringProgressRenderer = ring.GetComponent();
}
if (ringProgressRenderer == null)
return;
Shader shader = Resources.Load(RingProgressShaderResourcePath);
if (shader == null)
shader = Shader.Find(RingProgressShaderName);
if (shader == null)
{
#if UNITY_EDITOR
Debug.LogWarning($"[KnobController] 未找到环形进度条 Shader:{RingProgressShaderName}", this);
#endif
return;
}
_ringProgressMaterial = new Material(shader)
{
name = $"{ringProgressRenderer.gameObject.name}_RingProgress_Mat"
};
if (ringProgressRenderer.sprite != null)
_ringProgressMaterial.SetTexture(MainTexId, ringProgressRenderer.sprite.texture);
ringProgressRenderer.material = _ringProgressMaterial;
ApplyRingProgressMaterialProperties();
UpdateRingProgress(_knobValue);
}
private void ApplyRingProgressMaterialProperties()
{
if (_ringProgressMaterial == null)
return;
_ringProgressMaterial.SetFloat(StartAngleId, ringProgressStartAngle);
_ringProgressMaterial.SetFloat(FillClockwiseId, ringProgressClockwise ? 1f : 0f);
}
private void UpdateRingProgress(float progress)
{
if (_ringProgressMaterial == null)
return;
if (ringProgressRenderer != null && ringProgressRenderer.sprite != null)
_ringProgressMaterial.SetTexture(MainTexId, ringProgressRenderer.sprite.texture);
_ringProgressMaterial.SetFloat(ProgressId, Mathf.Clamp01(progress));
}
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 Start()
{
UpdateFromKnobAngle();
}
private void Update()
{
float dt = Time.deltaTime;
_animTime += dt;
// 强制使用 EventTriggerEx,不使用 Input+Raycast 回退
// if (!_useEventTrigger)
// HandleMouseInputFallback();
UpdateResistancePulse(dt);
UpdateKnobVisuals(dt);
UpdateGlowTube();
}
#endregion
#region Input(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);
}
private void HandleMouseInputFallback()
{
if (Input.GetMouseButtonDown(0))
{
if (_snapbackLocked) return;
Ray ray = _mainCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit2D hit = Physics2D.GetRayIntersection(ray, float.MaxValue);
if (hit.collider != null && IsValidHit(hit.collider))
{
StartDrag();
}
}
if (_isDragging)
{
if (Input.GetMouseButton(0))
ContinueDrag(Input.mousePosition);
else
EndDrag();
}
}
#endregion
#region IInteraction(EventTriggerEx 统一交互)
public bool IsActive => !_isLocked;
///
/// 不可用时(锁定、回弹中、已失败/成功)EventTriggerEx 将拦截交互
///
public bool IsAvailable => !_isLocked && !_snapbackLocked && !_hasFailed && !_hasSucceeded;
public GameObject GetGameObject() => gameObject;
#endregion
private bool IsValidHit(Collider2D col)
{
if (col == interactionCollider) return true;
if (col.gameObject == gameObject) return true;
if (knobTransform != null && col.transform.IsChildOf(knobTransform)) return true;
return false;
}
private float GetMouseAngleDeg(Vector3? screenPos = null)
{
Vector3 screenCenter = _mainCamera.WorldToScreenPoint(knobTransform.position);
Vector3 mp = screenPos ?? Input.mousePosition;
return Mathf.Atan2(mp.y - screenCenter.y, mp.x - screenCenter.x) * Mathf.Rad2Deg;
}
/// 输入阻力:转得越多,鼠标驱动力越弱
private float GetInputResistance()
{
return 1f - _knobValue * 0.88f;
}
#region Drag
private void StartDrag()
{
if (_hasFailed || _hasSucceeded || _isLocked) return;
if (clearHighlightWhenDragStarts)
SetInteractionHighlightVisible(false);
_isDragging = true;
_prevMouseAngle = GetMouseAngleDeg(Input.mousePosition);
ClearIdleState();
onPowerAdjustmentStart?.Invoke();
}
private void ContinueDrag(Vector3 screenPosition)
{
if (_snapbackLocked || _hasFailed || _hasSucceeded || _isLocked) return;
float cur = GetMouseAngleDeg(screenPosition);
float delta = cur - _prevMouseAngle;
if (delta > 180f) delta -= 360f;
if (delta < -180f) delta += 360f;
_prevMouseAngle = cur;
if (clockwiseDecreasesPower) delta = -delta;
delta *= GetInputResistance();
_knobAngle = Mathf.Clamp(_knobAngle + delta, 0f, _maxAngle);
UpdateFromKnobAngle();
ClearIdleState();
StartIdleCountdown();
}
private void EndDrag()
{
if (!_isDragging) return;
_isDragging = false;
if (_knobAngle > 10f)
StartIdleCountdown();
}
#endregion
#region Core State
private void UpdateFromKnobAngle()
{
_knobAngle = Mathf.Clamp(_knobAngle, 0f, _maxAngle);
_knobValue = _knobAngle / _maxAngle;
UpdateRingProgress(_knobValue);
// Intensity: cubic falloff
_intensity = Mathf.Pow(1f - _knobValue, 3f);
_intensity = Mathf.Max(0.03f, _intensity);
_tremorTarget = _knobValue * _knobValue * 3.5f;
_currentPower = _intensity * 100f;
onPowerChanged?.Invoke(_currentPower);
// Fail check
if (_knobValue >= failThreshold && !_hasFailed && !_hasSucceeded)
{
TriggerFailure();
}
}
#endregion
#region Idle / Warning
private void ClearIdleState()
{
if (_idleCoroutine != null)
{
StopCoroutine(_idleCoroutine);
_idleCoroutine = null;
}
_isIdle = false;
_idleStartTime = 0f;
}
private void StartIdleCountdown()
{
ClearIdleState();
_isIdle = true;
_idleStartTime = Time.time;
_idleCoroutine = StartCoroutine(IdleSnapbackRoutine());
}
private IEnumerator IdleSnapbackRoutine()
{
yield return new WaitForSeconds(idleDelay);
TriggerSnapback();
}
private float GetWarningLevel()
{
if (!_isIdle || _knobAngle < 10f) return 0f;
float elapsed = Time.time - _idleStartTime;
return Mathf.Pow(Mathf.Min(1f, elapsed / idleDelay), 2.8f);
}
#endregion
#region Snapback
private void TriggerSnapback()
{
if (_knobAngle < 5f) return;
_isSnappingBack = true;
_snapbackLocked = true;
_isIdle = false;
StartCoroutine(SnapbackRoutine());
}
private IEnumerator SnapbackRoutine()
{
float snapFrom = _knobAngle;
float turns = snapFrom / 360f;
float dur = Mathf.Clamp(snapbackDuration * (turns / 3f), 0.5f, 1.2f);
float t0 = Time.time;
while (true)
{
float elapsed = Time.time - t0;
float p = Mathf.Clamp01(elapsed / dur);
// Charge-then-burst easing
float eased;
if (p < 0.08f)
{
eased = p * 0.02f;
}
else
{
float t = (p - 0.08f) / 0.92f;
eased = 0.0016f + 0.9984f * (1f - Mathf.Pow(1f - t, 5f));
}
_knobAngle = snapFrom * (1f - eased);
UpdateFromKnobAngle();
_tremorTarget = 10f + turns * 3f;
if (p >= 1f) break;
yield return null;
}
_knobAngle = 0f;
UpdateFromKnobAngle();
_overshootIntensity = Mathf.Min(0.55f, 0.2f + turns * 0.05f);
_overshootDecay = 0f;
_isSnappingBack = false;
_tremorTarget = 0f;
if (glowTubeEffect != null)
glowTubeEffect.TriggerPulse(_overshootIntensity);
yield return new WaitForSeconds(0.4f);
_snapbackLocked = false;
// 若是失败触发的回弹,回弹完成后再触发失败事件和 UI
if (_hasFailed)
{
ShowInfoPanel(failMessage);
onPowerAdjustmentFail?.Invoke(_currentPower);
if (!string.IsNullOrEmpty(singleFailNode))
DialogController.Instance?.StartDialogNode(singleFailNode);
Debug.Log($"[KnobController] Snapback complete, failure events triggered");
}
onSnapbackComplete?.Invoke();
}
#endregion
#region Visuals
private void UpdateKnobVisuals(float dt)
{
// Smooth tremor
_tremorAmount += (_tremorTarget - _tremorAmount) * Mathf.Min(1f, dt * 14f);
float warning = GetWarningLevel();
float warningTremor = warning * warning * 15f;
float totalTremor = _tremorAmount + warningTremor;
// Rotation + tremor offset
float tremor = 0f;
if (totalTremor > 0.05f)
{
tremor = (Mathf.PerlinNoise(_animTime * 50f + _seedA, 0f) * 2f - 1f) * 0.6f
+ (Mathf.PerlinNoise(_animTime * 120f + _seedB, 7f) * 2f - 1f) * 0.4f;
tremor *= totalTremor;
}
knobTransform.localRotation = Quaternion.Euler(0f, 0f, -(_knobAngle + tremor));
// Plate heat coloring
if (knobPlate != null)
{
float pressure = _knobValue;
float warmth = Mathf.Min(1f, pressure + warning * 0.4f);
if (warmth > 0.03f)
{
float r = Mathf.Lerp(_plateOriginalColor.r, 1f, warmth * 0.6f);
float g = Mathf.Lerp(_plateOriginalColor.g, _plateOriginalColor.g * 0.7f, warmth * 0.5f);
float b = Mathf.Lerp(_plateOriginalColor.b, _plateOriginalColor.b * 0.5f, warmth * 0.6f);
float a = _plateOriginalColor.a;
knobPlate.color = new Color(r, g, b, a);
}
else
{
knobPlate.color = _plateOriginalColor;
}
}
}
private void UpdateResistancePulse(float dt)
{
if (_intensity < 0.5f && _knobAngle > 100f && !_isSnappingBack)
{
float suppression = 1f - _intensity;
if (_animTime > _nextPulseTime)
{
_resistancePulse = Mathf.Max(_resistancePulse,
suppression * (0.25f + Random.value * 0.5f));
_nextPulseTime = _animTime + 0.2f + Random.value * (2.5f - suppression * 2f);
}
}
float warnPulse = GetWarningLevel();
if (warnPulse > 0.2f)
{
float wp = warnPulse * 0.5f * (0.5f + 0.5f * Mathf.Sin(_animTime * 18f * warnPulse));
if (wp > 0f) _resistancePulse = Mathf.Max(_resistancePulse, wp);
}
_resistancePulse *= Mathf.Exp(-dt * 9f);
if (_resistancePulse < 0.008f) _resistancePulse = 0f;
}
private void UpdateGlowTube()
{
_overshootDecay += Time.deltaTime * 3f;
float overshoot = _overshootIntensity * Mathf.Exp(-_overshootDecay * 2.5f)
* (1f + Mathf.Sin(_overshootDecay * 40f) * 0.5f * Mathf.Exp(-_overshootDecay * 1.5f));
// 辉光与环形进度一致:用线性剩余量驱动粒子,避免 Pow(...,3) 导致中段过暗、与灯带进度脱节
float glowBase = Mathf.Max(0.03f, 1f - _knobValue);
_displayIntensity = glowBase + _resistancePulse + Mathf.Max(0f, overshoot);
_displayIntensity = Mathf.Min(1.5f, _displayIntensity);
if (glowTubeEffect != null)
{
glowTubeEffect.SetIntensity(_displayIntensity);
if (_resistancePulse > 0.1f)
glowTubeEffect.TriggerPulse(_resistancePulse * 0.5f);
}
}
#endregion
#region Failure / Info Panel
private void TriggerFailure()
{
_failCount++;
YarnVariableStorage.Instance?.SetValue("$knobFailCount", (float)_failCount);
_hasFailed = true;
Debug.Log($"[KnobController] Failure #{_failCount} at {_currentPower:F1}%, waiting for snapback...");
// 不立即触发 UI/事件,等旋钮回弹完成后再触发
TriggerSnapback();
}
private void ShowInfoPanel(string message)
{
if (infoPanel == null || infoText == null) return;
infoText.text = message;
infoPanel.SetActive(true);
var rt = infoPanel.GetComponent();
if (rt != null)
{
Vector3 orig = rt.anchoredPosition;
rt.anchoredPosition = orig + Vector3.up * 100f;
rt.DOAnchorPos(orig, panelShowDuration).SetEase(Ease.OutCubic);
}
}
private void HideInfoPanel()
{
if (infoPanel == null) return;
var rt = infoPanel.GetComponent();
if (rt != null)
{
var end = rt.anchoredPosition + Vector2.up * 100f;
rt.DOAnchorPos(end, panelHideDuration)
.SetEase(Ease.InCubic)
.OnComplete(() => infoPanel.SetActive(false));
}
else
{
infoPanel.SetActive(false);
}
}
#endregion
#region Yarn Commands
[YarnCommand("adjust_power")]
public static IEnumerator AdjustPower(float targetPower)
{
var ctrl = FixSystemCenter.SystemDic.Get();
if (ctrl == null) ctrl = FindObjectOfType();
if (ctrl != null)
{
float targetAngle = (1f - Mathf.Clamp01(targetPower / 100f)) * ctrl._maxAngle;
ctrl._knobAngle = targetAngle;
ctrl.UpdateFromKnobAngle();
yield return new WaitForSeconds(1f);
}
}
[YarnCommand("reset_knob")]
public static void ResetKnob()
{
var ctrl = FixSystemCenter.SystemDic.Get();
if (ctrl == null) ctrl = FindObjectOfType();
ctrl?.Reset(clearFailCount: false);
}
///
/// 锁定旋钮(交互禁止)。由 Yarn 在达到允许重试次数后调用。
/// 用法: <>
///
[YarnCommand("lock_knob")]
public static void LockKnob()
{
var ctrl = FixSystemCenter.SystemDic.Get();
if (ctrl == null) ctrl = FindObjectOfType();
ctrl?.Lock();
}
///
/// 解锁旋钮(交互解禁)。
/// 用法: <>
///
[YarnCommand("unlock_knob")]
public static void UnlockKnob()
{
var ctrl = FixSystemCenter.SystemDic.Get();
if (ctrl == null) ctrl = FindObjectOfType();
ctrl?.Unlock();
}
#endregion
#region Public API
///
/// 打开旋钮盖子(DOTween 平移动画)
///
public void OpenLid(float duration = 0f)
{
if (knobLid == null) return;
float d = duration > 0f ? duration : lidAnimDuration;
if (_lidTween != null) _lidTween.Kill();
Vector3 openPos = _lidClosedPosition + lidOpenOffset;
_lidTween = knobLid.DOLocalMove(openPos, d).SetEase(Ease.OutCubic)
.OnKill(() => _lidTween = null);
}
///
/// 盖上旋钮盖子(DOTween 平移回位)
///
public void CloseLid(float duration = 0f)
{
if (knobLid == null) return;
float d = duration > 0f ? duration : lidAnimDuration;
if (_lidTween != null) _lidTween.Kill();
_lidTween = knobLid.DOLocalMove(_lidClosedPosition, d).SetEase(Ease.OutCubic)
.OnKill(() => _lidTween = null);
}
public float GetCurrentPower() => _currentPower;
public bool HasFailed() => _hasFailed;
public float KnobValue => _knobValue;
public float DisplayIntensity => _displayIntensity;
///
/// 重置旋钮状态。clearFailCount=true 时同时清零失败次数(如进入 processor 模式时)。
///
public void Reset(bool clearFailCount = true)
{
ClearIdleState();
_hasFailed = false;
_hasSucceeded = false;
_isDragging = false;
_isSnappingBack = false;
_snapbackLocked = false;
_knobAngle = 0f;
_tremorAmount = 0f;
_tremorTarget = 0f;
_resistancePulse = 0f;
_overshootIntensity = 0f;
_isLocked = false;
if (clearFailCount) _failCount = 0;
UpdateFromKnobAngle();
if (knobPlate != null) knobPlate.color = _plateOriginalColor;
HideInfoPanel();
Debug.Log("[KnobController] Reset");
}
///
/// 交互禁止:锁定旋钮,禁止用户操作。由 Yarn 在达到允许重试次数后调用。
///
public void Lock()
{
_isLocked = true;
Debug.Log("[KnobController] Locked");
}
///
/// 交互解禁:解锁旋钮。通常由 Reset 内部调用,也可单独调用。
///
public void Unlock()
{
_isLocked = false;
}
public void SetMinAcceptablePower(float minPower)
{
failThreshold = 1f - Mathf.Clamp01(minPower / 100f);
}
public void ForceSuccess()
{
_hasSucceeded = true;
onPowerAdjustmentSuccess?.Invoke(_currentPower);
}
private void OnDestroy()
{
if (_ringProgressMaterial == null) return;
if (Application.isPlaying)
Destroy(_ringProgressMaterial);
else
DestroyImmediate(_ringProgressMaterial);
}
#endregion
}
///
/// 旋钮交互桥接:挂在 Collider 所在子物体上,将 IInteraction 转交给父级 KnobController
///
public class KnobInteractionBridge : MonoBehaviour, IInteraction
{
private KnobController _controller;
private void Awake()
{
_controller = GetComponentInParent();
}
public bool IsActive => _controller?.IsActive ?? true;
public bool IsAvailable => _controller?.IsAvailable ?? true;
public GameObject GetGameObject() => gameObject;
}
}