Files
aibis-dream/Assets/Scripts/MiniGame/HuoShan/SalesSystem/KnobController.cs
T
2026-07-25 15:57:03 +08:00

969 lines
33 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 UnityEngine.Serialization;
using Yarn.Unity;
namespace AibisDream.MiniGame.HuoShan
{
/// <summary>
/// 处理器旋钮控制器 — 完整移植 Web_EmotionWave glow-tube 分支的交互体验
/// 交互通过 EventTriggerEx 统一管理,支持 EventSystemEx.isLocked 和 IInteraction 状态
/// 核心特性:累计旋转、输入阻力、噪声颤抖、回弹前蓄力预兆、底座发热变色
/// 保留原有公共接口(事件、Yarn 命令、功率查询)
/// 在旋钮 GameObject 上添加 EventTriggerEx + Collider2D 即可接入;未添加则回退到 Input+Raycast
/// </summary>
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;
[Tooltip("辉光管兼作进度条:勾选则以「失败阈值」为满量程,旋钮一转管体就明显变短;不勾选按整圈量程")]
[SerializeField] private bool glowFillUsesFailThreshold = true;
[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("旋钮本体 SpriteRenderer(切 收起/展开/激活);不填则用 knobPlate / knobTransform")]
[SerializeField] private SpriteRenderer knobVisual;
[Tooltip("收起(盖住)")]
[SerializeField] private Sprite collapsedSprite;
[Tooltip("展开(打开后待机)")]
[SerializeField] private Sprite expandedSprite;
[Tooltip("激活(提示可交互 / 高亮)")]
[SerializeField] private Sprite activeSprite;
[Tooltip("开合切图过渡时长(Yarn open/close_knob_lid 等待用;切图本身即时)")]
[SerializeField] private float lidAnimDuration = 0.4f;
[Tooltip("旧版盖子子物体(步进模式/旋钮/盖子)。有三态图后会自动隐藏,避免遮挡")]
[FormerlySerializedAs("knobLid")]
[SerializeField] private Transform legacyKnobLid;
[Header("交互提示")]
[Tooltip("高亮目标:仅当未配置 activeSprite 时回退改材质 _ShineFade;有激活图时高亮=切到激活态")]
[SerializeField] private SpriteRenderer interactionHighlightSprite;
[Tooltip("开始拖拽旋钮时是否退出激活态(回到展开)")]
[SerializeField] private bool clearHighlightWhenDragStarts = true;
[Header("事件")]
public UnityEvent onPowerAdjustmentStart;
public UnityEvent<float> onPowerChanged;
public UnityEvent<float> onPowerAdjustmentFail;
public UnityEvent<float> 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 EventTriggerEx _eventTrigger;
private bool _useEventTrigger;
private const string CollapsedSpriteName = "旋钮收起";
private const string ExpandedSpriteName = "旋钮展开";
private const string ActiveSpriteName = "旋钮激活状态";
private const string LegacyRingProgressName = "灯带进度条";
private static readonly int ShineFadeId = Shader.PropertyToID("_ShineFade");
private SpriteRenderer _shineHighlightSprite;
private bool _isLidOpen;
private bool _highlightVisible;
private KnobAppearance _appearance = KnobAppearance.Collapsed;
private enum KnobAppearance
{
Collapsed,
Expanded,
Active
}
#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<Collider2D>();
if (infoPanel != null) infoPanel.SetActive(false);
// 事件目标:Collider 所在的对象(可能与 KnobController 不同,如旋钮 visuals 在子物体)
var eventTargetGo = interactionCollider != null ? interactionCollider.gameObject : (knobTransform != null ? knobTransform.gameObject : gameObject);
_eventTrigger = eventTargetGo.GetComponent<EventTriggerEx>();
if (_eventTrigger == null && eventTargetGo.GetComponent<Collider2D>() != null)
{
if (eventTargetGo != gameObject)
{
var bridge = eventTargetGo.GetComponent<KnobInteractionBridge>();
if (bridge == null) eventTargetGo.AddComponent<KnobInteractionBridge>();
}
_eventTrigger = eventTargetGo.AddComponent<EventTriggerEx>();
}
if (_eventTrigger != null)
{
_useEventTrigger = true;
EnsureEventTriggerEntries();
_eventTrigger.Register(EventTriggerType.PointerDown, OnEventPointerDown);
_eventTrigger.Register(EventTriggerType.PointerUp, OnEventPointerUp);
_eventTrigger.Register(EventTriggerType.Drag, OnEventDrag);
}
EnsureKnobVisual();
EnsureKnobStateSprites();
DisableLegacyKnobLid();
ApplyKnobAppearance(KnobAppearance.Collapsed, force: true);
DisableLegacyRingProgress();
EnsureGlowTubeEffect();
EnsureInteractionHighlightSprite();
SetInteractionHighlightVisible(false);
}
private void EnsureKnobVisual()
{
if (knobVisual != null) return;
if (knobPlate != null)
{
knobVisual = knobPlate;
return;
}
if (knobTransform != null)
knobVisual = knobTransform.GetComponent<SpriteRenderer>();
}
private void EnsureKnobStateSprites()
{
EnsureKnobVisual();
if (knobVisual == null || knobVisual.sprite == null) return;
var sprites = Resources.FindObjectsOfTypeAll<Sprite>();
Sprite FindByName(string spriteName)
{
for (int i = 0; i < sprites.Length; i++)
{
var s = sprites[i];
if (s != null && s.name == spriteName && s.texture == knobVisual.sprite.texture)
return s;
}
return null;
}
if (collapsedSprite == null) collapsedSprite = FindByName(CollapsedSpriteName);
if (expandedSprite == null) expandedSprite = FindByName(ExpandedSpriteName);
if (activeSprite == null) activeSprite = FindByName(ActiveSpriteName);
}
private void DisableLegacyKnobLid()
{
if (legacyKnobLid == null && knobTransform != null)
{
var lid = knobTransform.Find("盖子");
if (lid != null) legacyKnobLid = lid;
}
if (legacyKnobLid == null)
{
var t = transform;
while (t != null)
{
if (t.name == "SaleView" || t.name == "SellView" || t.name == "SalelView")
{
legacyKnobLid = t.Find("步进模式/旋钮/盖子");
break;
}
t = t.parent;
}
}
if (legacyKnobLid != null)
legacyKnobLid.gameObject.SetActive(false);
}
private void EnsureInteractionHighlightSprite()
{
if (interactionHighlightSprite != null)
{
_shineHighlightSprite = interactionHighlightSprite;
return;
}
EnsureKnobVisual();
if (knobVisual != null)
{
_shineHighlightSprite = knobVisual;
return;
}
if (knobPlate != null)
{
_shineHighlightSprite = knobPlate;
return;
}
if (knobTransform != null)
_shineHighlightSprite = knobTransform.GetComponent<SpriteRenderer>();
}
/// <summary>
/// 控制旋钮交互提示。有激活图时切到「激活」态;否则回退改材质 _ShineFade。
/// </summary>
public void SetInteractionHighlightVisible(bool visible)
{
_highlightVisible = visible;
if (activeSprite != null || expandedSprite != null || collapsedSprite != null)
{
if (visible && _isLidOpen)
ApplyKnobAppearance(KnobAppearance.Active);
else if (_isLidOpen)
ApplyKnobAppearance(KnobAppearance.Expanded);
else
ApplyKnobAppearance(KnobAppearance.Collapsed);
return;
}
EnsureInteractionHighlightSprite();
if (_shineHighlightSprite == null) return;
var mat = _shineHighlightSprite.material;
if (mat == null || !mat.HasProperty(ShineFadeId)) return;
mat.SetFloat(ShineFadeId, visible ? 1f : 0f);
}
private void ApplyKnobAppearance(KnobAppearance appearance, bool force = false)
{
if (!force && _appearance == appearance) return;
_appearance = appearance;
EnsureKnobVisual();
if (knobVisual == null) return;
Sprite target = appearance switch
{
KnobAppearance.Collapsed => collapsedSprite != null ? collapsedSprite : knobVisual.sprite,
KnobAppearance.Expanded => expandedSprite != null ? expandedSprite : knobVisual.sprite,
KnobAppearance.Active => activeSprite != null ? activeSprite
: (expandedSprite != null ? expandedSprite : knobVisual.sprite),
_ => knobVisual.sprite
};
if (target != null)
knobVisual.sprite = target;
}
/// <summary>
/// 旧版环形灯带进度条已废弃(进度改由辉光管长度承担),场景里残留的节点直接隐藏。
/// </summary>
private void DisableLegacyRingProgress()
{
if (knobTransform == null) return;
var ring = knobTransform.Find(LegacyRingProgressName);
if (ring == null && knobTransform.parent != null)
ring = knobTransform.parent.Find(LegacyRingProgressName);
if (ring != null)
ring.gameObject.SetActive(false);
}
private void EnsureGlowTubeEffect()
{
if (glowTubeEffect != null) return;
var t = transform;
while (t != null)
{
if (t.name == "SaleView" || t.name == "SellView" || t.name == "SalelView")
{
glowTubeEffect = t.GetComponentInChildren<GlowTubeEffect>(true);
break;
}
t = t.parent;
}
if (glowTubeEffect == null)
glowTubeEffect = FindObjectOfType<GlowTubeEffect>(true);
}
/// <summary>辉光管剩余量:1=满管,0=空管。旋钮拧到失败阈值时正好排空。</summary>
private float GetGlowFill()
{
float span = glowFillUsesFailThreshold ? Mathf.Max(0.01f, failThreshold) : 1f;
return Mathf.Clamp01(1f - _knobValue / span);
}
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();
if (glowTubeEffect != null)
glowTubeEffect.SetFillImmediate(GetGlowFill());
}
private void Update()
{
float dt = Time.deltaTime;
_animTime += dt;
// 强制使用 EventTriggerEx,不使用 Input+Raycast 回退
// if (!_useEventTrigger)
// HandleMouseInputFallback();
UpdateResistancePulse(dt);
UpdateKnobVisuals(dt);
UpdateGlowTube();
}
#endregion
#region InputEventTriggerEx 驱动 / 回退)
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 IInteractionEventTriggerEx 统一交互)
public bool IsActive => !_isLocked;
/// <summary>
/// 不可用时(锁定、回弹中、已失败/成功)EventTriggerEx 将拦截交互
/// </summary>
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;
}
/// <summary>输入阻力:转得越多,鼠标驱动力越弱</summary>
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;
// 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));
}
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));
// 辉光管兼作进度条:管长(fill)与亮度同源,拧旋钮时管体从右往左退且整体变暗
float fill = GetGlowFill();
_displayIntensity = Mathf.Max(0.03f, fill) + _resistancePulse + Mathf.Max(0f, overshoot);
_displayIntensity = Mathf.Min(1.5f, _displayIntensity);
if (glowTubeEffect != null)
{
glowTubeEffect.SetFill(fill);
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<RectTransform>();
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<RectTransform>();
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<KnobController>();
if (ctrl == null) ctrl = FindObjectOfType<KnobController>();
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<KnobController>();
if (ctrl == null) ctrl = FindObjectOfType<KnobController>();
ctrl?.Reset(clearFailCount: false);
}
/// <summary>
/// 锁定旋钮(交互禁止)。由 Yarn 在达到允许重试次数后调用。
/// 用法: <<lock_knob>>
/// </summary>
[YarnCommand("lock_knob")]
public static void LockKnob()
{
var ctrl = FixSystemCenter.SystemDic.Get<KnobController>();
if (ctrl == null) ctrl = FindObjectOfType<KnobController>();
ctrl?.Lock();
}
/// <summary>
/// 解锁旋钮(交互解禁)。
/// 用法: <<unlock_knob>>
/// </summary>
[YarnCommand("unlock_knob")]
public static void UnlockKnob()
{
var ctrl = FixSystemCenter.SystemDic.Get<KnobController>();
if (ctrl == null) ctrl = FindObjectOfType<KnobController>();
ctrl?.Unlock();
}
#endregion
#region Public API
/// <summary>
/// 打开旋钮:切到「展开」图(替代旧盖子平移)。
/// </summary>
public void OpenLid(float duration = 0f)
{
_isLidOpen = true;
if (_highlightVisible)
ApplyKnobAppearance(KnobAppearance.Active, force: true);
else
ApplyKnobAppearance(KnobAppearance.Expanded, force: true);
}
/// <summary>
/// 收起旋钮:切到「收起」图。
/// </summary>
public void CloseLid(float duration = 0f)
{
_isLidOpen = false;
_highlightVisible = false;
ApplyKnobAppearance(KnobAppearance.Collapsed, force: true);
}
public float GetCurrentPower() => _currentPower;
public bool HasFailed() => _hasFailed;
public float KnobValue => _knobValue;
public float DisplayIntensity => _displayIntensity;
/// <summary>
/// 重置旋钮状态。clearFailCount=true 时同时清零失败次数(如进入 processor 模式时)。
/// </summary>
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;
CloseLid();
}
else if (_isLidOpen)
{
SetInteractionHighlightVisible(_highlightVisible);
}
UpdateFromKnobAngle();
if (glowTubeEffect != null)
glowTubeEffect.SetFillImmediate(GetGlowFill());
HideInfoPanel();
Debug.Log("[KnobController] Reset");
}
/// <summary>
/// 交互禁止:锁定旋钮,禁止用户操作。由 Yarn 在达到允许重试次数后调用。
/// </summary>
public void Lock()
{
_isLocked = true;
Debug.Log("[KnobController] Locked");
}
/// <summary>
/// 交互解禁:解锁旋钮。通常由 Reset 内部调用,也可单独调用。
/// </summary>
public void Unlock()
{
_isLocked = false;
}
public void SetMinAcceptablePower(float minPower)
{
failThreshold = 1f - Mathf.Clamp01(minPower / 100f);
}
public void ForceSuccess()
{
_hasSucceeded = true;
onPowerAdjustmentSuccess?.Invoke(_currentPower);
}
#endregion
}
/// <summary>
/// 旋钮交互桥接:挂在 Collider 所在子物体上,将 IInteraction 转交给父级 KnobController
/// </summary>
public class KnobInteractionBridge : MonoBehaviour, IInteraction
{
private KnobController _controller;
private void Awake()
{
_controller = GetComponentInParent<KnobController>();
}
public bool IsActive => _controller?.IsActive ?? true;
public bool IsAvailable => _controller?.IsAvailable ?? true;
public GameObject GetGameObject() => gameObject;
}
}