using System; using System.Threading; using Cysharp.Threading.Tasks; using DanroJump.Audio; using DanroJump.Gameplay; using DanroJump.Hardware.Com; using DanroJump.Prizes; using DanroJump.Prizes.Qr; using DanroJump.SceneFlow; using DanroJump.Settings; using UnityEngine; using UnityEngine.UIElements; using Zenject; namespace DanroJump.UI.Gameplay { /// /// Отображает runtime-показатели игрового забега в UI Toolkit HUD. /// public sealed class GameplayHudController : MonoBehaviour { [SerializeField] private VisualTreeAsset visualTreeAsset; [SerializeField] private PanelSettings panelSettings; [SerializeField] private int sortingOrder = 10; [SerializeField] private string scoreLabelName = "ScoreValue"; [SerializeField] private string heightLabelName = "HeightValue"; [SerializeField] private string bestHeightLabelName = "BestHeightValue"; [SerializeField] private string livesLabelName = "LivesValue"; [SerializeField] private string coinsLabelName = "CoinsValue"; [SerializeField] private string deathsLabelName = "DeathsValue"; [SerializeField] private string stateLabelName = "StateValue"; [SerializeField] private string developmentStatsRootName = "RightStats"; private UIDocument document; private GameplaySession session; private IPrizeFlowService prizeFlow; private IPrizeSessionStateService prizeSessionState; private IPrizeQrService prizeQrService; private ISceneFlowService sceneFlow; private IReadOnlyServiceSettings serviceSettings; private IGameAudioService audioService; private IComSystem comSystem; private PrizeResultOverlayWindow prizeResultOverlay; private ContinueGameOverlayWindow continueGameOverlay; private VisualElement prizeOverlayRoot; private VisualElement continueOverlayRoot; private VisualElement developmentStatsRoot; private Label scoreLabel; private Label heightLabel; private Label bestHeightLabel; private Label livesLabel; private Label coinsLabel; private Label deathsLabel; private Label stateLabel; private CancellationTokenSource refreshCancellation; private CancellationTokenSource qrTimeoutCancellation; private CancellationTokenSource physicalDispenseCancellation; private bool isSubscribedToSession; private bool isSubscribedToPrizeFlow; /// /// Получает игровую сессию из DI-контейнера сцены. /// [Inject] public void Construct( GameplaySession injectedSession, [InjectOptional] IPrizeFlowService injectedPrizeFlow = null, [InjectOptional] IPrizeSessionStateService injectedPrizeSessionState = null, [InjectOptional] IPrizeQrService injectedPrizeQrService = null, [InjectOptional] ISceneFlowService injectedSceneFlow = null, [InjectOptional] IReadOnlyServiceSettings injectedServiceSettings = null, [InjectOptional] IGameAudioService injectedAudioService = null, [InjectOptional] IComSystem injectedComSystem = null) { prizeFlow = injectedPrizeFlow; prizeSessionState = injectedPrizeSessionState; prizeQrService = injectedPrizeQrService; sceneFlow = injectedSceneFlow; serviceSettings = injectedServiceSettings; audioService = injectedAudioService; comSystem = injectedComSystem; SetSession(injectedSession); SubscribeToPrizeFlow(); } private void Start() { EnsureDocument(); BindLabels(); BindPrizeOverlay(); BindContinueOverlay(); ShowLastPrizeResultIfNeeded(); Refresh(); } /// /// Подписывается на изменения сессии и запускает кадровую подстраховку обновления HUD. /// private void OnEnable() { SubscribeToSession(); SubscribeToPrizeFlow(); if (refreshCancellation == null) { refreshCancellation = new CancellationTokenSource(); RefreshContinuouslyAsync(refreshCancellation.Token).Forget(Debug.LogException); } Refresh(); } /// /// Останавливает обновление HUD и снимает подписку с игровой сессии. /// private void OnDisable() { if (refreshCancellation != null) { refreshCancellation.Cancel(); refreshCancellation.Dispose(); refreshCancellation = null; } if (qrTimeoutCancellation != null) { qrTimeoutCancellation.Cancel(); qrTimeoutCancellation.Dispose(); qrTimeoutCancellation = null; } if (physicalDispenseCancellation != null) { physicalDispenseCancellation.Cancel(); physicalDispenseCancellation.Dispose(); physicalDispenseCancellation = null; } UnsubscribeFromSession(); UnsubscribeFromPrizeFlow(); prizeResultOverlay?.Dispose(); prizeResultOverlay = null; prizeOverlayRoot = null; continueGameOverlay?.Dispose(); continueGameOverlay = null; continueOverlayRoot = null; } /// /// Создает или настраивает UIDocument, чтобы HUD мог жить на отдельном GameObject без ручной сборки. /// private void EnsureDocument() { if (document == null && !TryGetComponent(out document)) { document = gameObject.AddComponent(); } if (document == null) { return; } document.visualTreeAsset = visualTreeAsset; document.panelSettings = panelSettings; document.sortingOrder = sortingOrder; } /// /// Находит текстовые поля в UXML по именам, заданным в инспекторе. /// private void BindLabels() { var root = document != null ? document.rootVisualElement : null; if (root == null) { return; } developmentStatsRoot = root.Q(developmentStatsRootName); scoreLabel = FindLabel(root, scoreLabelName); heightLabel = FindLabel(root, heightLabelName); bestHeightLabel = FindLabel(root, bestHeightLabelName); livesLabel = FindLabel(root, livesLabelName); coinsLabel = FindLabel(root, coinsLabelName); deathsLabel = FindLabel(root, deathsLabelName); stateLabel = FindLabel(root, stateLabelName); } private void BindPrizeOverlay() { var root = document != null ? document.rootVisualElement : null; var overlayRoot = root?.Q("PrizeResultOverlay"); if (overlayRoot == null) { return; } if (prizeResultOverlay != null && prizeOverlayRoot == overlayRoot) { return; } prizeResultOverlay?.Dispose(); prizeOverlayRoot = overlayRoot; prizeResultOverlay = new PrizeResultOverlayWindow(overlayRoot, audioService); prizeResultOverlay.ReturnRequested += HandlePrizeReturnRequested; } private void BindContinueOverlay() { var root = document != null ? document.rootVisualElement : null; var overlayRoot = root?.Q("ContinueGameOverlay"); if (overlayRoot == null) { return; } if (continueGameOverlay != null && continueOverlayRoot == overlayRoot) { return; } continueGameOverlay?.Dispose(); continueOverlayRoot = overlayRoot; continueGameOverlay = new ContinueGameOverlayWindow(overlayRoot, audioService); continueGameOverlay.ContinueRequested += HandleContinueRequested; continueGameOverlay.MenuRequested += HandleContinueMenuRequested; } /// /// Обновляет все значения HUD из текущей игровой сессии. /// private void Refresh() { RefreshDevelopmentStatsVisibility(); if (session == null) { SetLabel(stateLabel, "WAITING"); return; } SetLabel(scoreLabel, session.Score.ToString()); SetLabel(heightLabel, $"{session.MaxRunHeight:0} м"); SetLabel(bestHeightLabel, $"{session.RecordHeight:0} м"); SetLabel(livesLabel, session.RemainingLives.ToString()); SetLabel(coinsLabel, session.Coins.ToString()); SetLabel(deathsLabel, session.DeathCount.ToString()); SetLabel(stateLabel, FormatState(session.State)); RefreshContinueOverlay(); } /// /// Безопасно обновляет текст label, если элемент найден в UXML. /// private static void SetLabel(Label label, string value) { if (label != null) { label.text = value; } } /// /// Ищет Label по имени, заданному в настройках контроллера. /// private static Label FindLabel(VisualElement root, string labelName) { return root != null && !string.IsNullOrEmpty(labelName) ? root.Q