using System; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; namespace XGame.VirtualInput { public enum ActionButtonId { Primary, Skill1, Skill2, Skill3, Skill4 } public enum ActionButtonPhase { Down, Drag, HoldStart, HoldTick, HoldEnd, Tap, Release, Cancel, Blocked } public readonly struct ActionButtonEvent { public ActionButtonEvent( ActionButtonId buttonId, ActionButtonPhase phase, Vector2 screenPosition, Vector2 localVector, Vector2 direction, float distance01, float heldSeconds, bool isCanceled, int pointerId) { ButtonId = buttonId; Phase = phase; ScreenPosition = screenPosition; LocalVector = localVector; Direction = direction; Distance01 = distance01; HeldSeconds = heldSeconds; IsCanceled = isCanceled; PointerId = pointerId; } public ActionButtonId ButtonId { get; } public ActionButtonPhase Phase { get; } public Vector2 ScreenPosition { get; } public Vector2 LocalVector { get; } public Vector2 Direction { get; } public float Distance01 { get; } public float HeldSeconds { get; } public bool IsCanceled { get; } public int PointerId { get; } } public sealed class VirtualActionButton : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler { [SerializeField] public ActionButtonId ButtonId; [SerializeField] private float holdThresholdSeconds = 0.35f; [SerializeField] private float holdTickIntervalSeconds = 0.1f; [SerializeField] private float releaseDistanceThreshold01 = 0.25f; [SerializeField] private float maxDragRadiusMultiplier = 1.5f; private Image icon; private Image cooldownMask; private TMP_Text cooldownText; private TMP_Text chargeText; private GameObject aimIndicator; private RectTransform cancelArea; private RectTransform rectTransform; private Button unityButton; private CanvasGroup canvasGroup; private bool enabledByState = true; private int charges = -1; private float cooldownRemaining; private float cooldownDuration; private bool pointerDown; private int activePointerId = int.MinValue; private float pointerDownTime; private bool holdStarted; private float nextHoldTickTime; private Vector2 lastScreenPosition; private ActionButtonEvent lastDragEvent; private Vector2? screenCenterOverride; private float? screenRadiusOverride; public bool IsInteractable => enabledByState && cooldownRemaining <= 0f && charges != 0; public event Action ButtonEvent; private void Awake() { BindNodes(); RefreshVisuals(); } private void OnDisable() { CancelActivePointer(Time.unscaledTime); SetAimVisible(false); } private void Update() { if (cooldownRemaining > 0f) { cooldownRemaining = Mathf.Max(0f, cooldownRemaining - Time.unscaledDeltaTime); RefreshVisuals(); } if (pointerDown) { MaybeEmitHold(Time.unscaledTime, activePointerId, lastScreenPosition); } } public void OnPointerDown(PointerEventData eventData) { HandlePointerDown(eventData.position, eventData.pointerId, Time.unscaledTime); } public void OnDrag(PointerEventData eventData) { HandleDrag(eventData.position, eventData.pointerId, Time.unscaledTime); } public void OnPointerUp(PointerEventData eventData) { HandlePointerUp(eventData.position, eventData.pointerId, Time.unscaledTime); } public void HandlePointerDownForTest(Vector2 screenPosition, int pointerId, float now) { HandlePointerDown(screenPosition, pointerId, now); } public void HandleDragForTest(Vector2 screenPosition, int pointerId, float now) { HandleDrag(screenPosition, pointerId, now); } public void HandlePointerUpForTest(Vector2 screenPosition, int pointerId, float now) { HandlePointerUp(screenPosition, pointerId, now); } public void SetCooldown(float remainingSeconds, float durationSeconds) { cooldownRemaining = Mathf.Max(0f, remainingSeconds); cooldownDuration = Mathf.Max(0f, durationSeconds); RefreshVisuals(); } public void SetCharges(int count) { charges = count; RefreshVisuals(); } public void SetButtonEnabled(bool enabled) { enabledByState = enabled; RefreshVisuals(); } public void SetCancelArea(RectTransform area) { cancelArea = area; } public void SetIcon(Sprite sprite) { if (icon == null) { BindNodes(); } if (icon != null) { icon.sprite = sprite; icon.enabled = sprite != null; } } private void BindNodes() { rectTransform = transform as RectTransform; unityButton = GetComponent