From d06ad786459d47664acdb3d6390044aa7c79670f Mon Sep 17 00:00:00 2001 From: Konstantin Dyachenko Date: Fri, 27 Feb 2026 03:44:37 +0700 Subject: [PATCH] [Add] MVC UI for Yacht Dice scorecard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit View layer: CategoryRowView (reusable x13 row with preview/recorded score display), ScoreCardView (full scorecard panel with Russian category names, upper bonus tracking), DicePanelView (5 dice buttons with lock toggle + roll counter), GameInfoView (turn display + game over overlay). Controller layer: GameController bridges Model and View — subscribes to model events in Awake() to catch GameManager.Start(), routes UI clicks to game logic, computes preview scores for all unfilled categories after each roll, handles upper section bonus (63+ = +35). Co-Authored-By: Claude Opus 4.6 --- Assets/Scripts/UI/CategoryRowView.cs | 85 ++++++++++++ Assets/Scripts/UI/DicePanelView.cs | 101 ++++++++++++++ Assets/Scripts/UI/GameController.cs | 197 +++++++++++++++++++++++++++ Assets/Scripts/UI/GameInfoView.cs | 44 ++++++ Assets/Scripts/UI/ScoreCardView.cs | 109 +++++++++++++++ 5 files changed, 536 insertions(+) create mode 100644 Assets/Scripts/UI/CategoryRowView.cs create mode 100644 Assets/Scripts/UI/DicePanelView.cs create mode 100644 Assets/Scripts/UI/GameController.cs create mode 100644 Assets/Scripts/UI/GameInfoView.cs create mode 100644 Assets/Scripts/UI/ScoreCardView.cs diff --git a/Assets/Scripts/UI/CategoryRowView.cs b/Assets/Scripts/UI/CategoryRowView.cs new file mode 100644 index 0000000..42e612c --- /dev/null +++ b/Assets/Scripts/UI/CategoryRowView.cs @@ -0,0 +1,85 @@ +using System; +using UnityEngine; +using UnityEngine.UI; +using TMPro; + +public sealed class CategoryRowView : MonoBehaviour +{ + [Header("UI Elements")] + [SerializeField] private TMP_Text categoryNameText; + [SerializeField] private TMP_Text scoreText; + [SerializeField] private Button selectButton; + [SerializeField] private Image background; + + [Header("Colors")] + [SerializeField] private Color normalColor = new Color(0.95f, 0.95f, 0.95f, 1f); + [SerializeField] private Color usedColor = new Color(0.75f, 0.75f, 0.75f, 1f); + [SerializeField] private Color previewPositiveColor = new Color(0.85f, 1f, 0.85f, 1f); + [SerializeField] private Color previewZeroColor = new Color(1f, 0.88f, 0.88f, 1f); + + private YachtCategory category; + private bool isUsed; + + public event Action OnCategorySelected; + + public void Initialize(YachtCategory cat, string displayName) + { + category = cat; + isUsed = false; + categoryNameText.text = displayName; + scoreText.text = ""; + selectButton.onClick.AddListener(HandleClick); + SetInteractable(false); + background.color = normalColor; + } + + public void ShowPreview(int previewScore) + { + if (isUsed) return; + scoreText.text = previewScore.ToString(); + background.color = previewScore > 0 ? previewPositiveColor : previewZeroColor; + } + + public void HidePreview() + { + if (isUsed) return; + scoreText.text = ""; + background.color = normalColor; + } + + public void SetRecordedScore(int score) + { + isUsed = true; + scoreText.text = score.ToString(); + SetInteractable(false); + background.color = usedColor; + } + + public void SetInteractable(bool interactable) + { + if (isUsed) + { + selectButton.interactable = false; + return; + } + selectButton.interactable = interactable; + } + + public void ResetRow() + { + isUsed = false; + scoreText.text = ""; + SetInteractable(false); + background.color = normalColor; + } + + private void HandleClick() + { + OnCategorySelected?.Invoke(category); + } + + private void OnDestroy() + { + selectButton.onClick.RemoveListener(HandleClick); + } +} diff --git a/Assets/Scripts/UI/DicePanelView.cs b/Assets/Scripts/UI/DicePanelView.cs new file mode 100644 index 0000000..31c49c0 --- /dev/null +++ b/Assets/Scripts/UI/DicePanelView.cs @@ -0,0 +1,101 @@ +using System; +using UnityEngine; +using UnityEngine.UI; +using TMPro; + +public sealed class DicePanelView : MonoBehaviour +{ + [Header("Dice Display")] + [SerializeField] private Button[] diceButtons = new Button[5]; + [SerializeField] private TMP_Text[] diceValueTexts = new TMP_Text[5]; + [SerializeField] private Image[] diceBackgrounds = new Image[5]; + + [Header("Roll")] + [SerializeField] private Button rollButton; + [SerializeField] private TMP_Text rollButtonText; + + [Header("Colors")] + [SerializeField] private Color unlockedColor = Color.white; + [SerializeField] private Color lockedColor = new Color(1f, 0.85f, 0.4f, 1f); + + public event Action OnDiceToggled; + public event Action OnRollClicked; + + private void Awake() + { + for (int i = 0; i < diceButtons.Length; i++) + { + int capturedIndex = i; + diceButtons[i].onClick.AddListener(() => OnDiceToggled?.Invoke(capturedIndex)); + } + + rollButton.onClick.AddListener(() => OnRollClicked?.Invoke()); + + for (int i = 0; i < diceValueTexts.Length; i++) + { + diceValueTexts[i].text = "?"; + diceBackgrounds[i].color = unlockedColor; + diceButtons[i].interactable = false; + } + + SetRollButtonState(true, 0, 3); + } + + public void SetDieValue(int index, int value) + { + if (index >= 0 && index < diceValueTexts.Length) + diceValueTexts[index].text = value.ToString(); + } + + public void SetAllDiceValues(int[] values) + { + for (int i = 0; i < values.Length && i < diceValueTexts.Length; i++) + diceValueTexts[i].text = values[i].ToString(); + } + + public void SetDieLocked(int index, bool isLocked) + { + if (index >= 0 && index < diceBackgrounds.Length) + diceBackgrounds[index].color = isLocked ? lockedColor : unlockedColor; + } + + public void SetDiceInteractable(bool interactable) + { + for (int i = 0; i < diceButtons.Length; i++) + diceButtons[i].interactable = interactable; + } + + public void SetRollButtonState(bool interactable, int currentRoll, int maxRolls) + { + rollButton.interactable = interactable; + + if (currentRoll >= maxRolls) + rollButtonText.text = "Выберите категорию"; + else + rollButtonText.text = $"Бросок {currentRoll + 1}/{maxRolls}"; + } + + public void ResetForNewTurn() + { + for (int i = 0; i < diceValueTexts.Length; i++) + { + diceValueTexts[i].text = "?"; + diceBackgrounds[i].color = unlockedColor; + diceButtons[i].interactable = false; + } + } + + public void ResetForNewGame() + { + ResetForNewTurn(); + SetRollButtonState(true, 0, 3); + } + + private void OnDestroy() + { + for (int i = 0; i < diceButtons.Length; i++) + diceButtons[i].onClick.RemoveAllListeners(); + + rollButton.onClick.RemoveAllListeners(); + } +} diff --git a/Assets/Scripts/UI/GameController.cs b/Assets/Scripts/UI/GameController.cs new file mode 100644 index 0000000..a431dd2 --- /dev/null +++ b/Assets/Scripts/UI/GameController.cs @@ -0,0 +1,197 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +public sealed class GameController : MonoBehaviour +{ + [Header("Model")] + [SerializeField] private GameManager gameManager; + [SerializeField] private ScoringSystem scoringSystem; + [SerializeField] private DiceManager diceManager; + + [Header("Views")] + [SerializeField] private ScoreCardView scoreCardView; + [SerializeField] private DicePanelView dicePanelView; + [SerializeField] private GameInfoView gameInfoView; + + [Header("Settings")] + [SerializeField] private int maxRollsPerTurn = 3; + + private static readonly YachtCategory[] UpperCategories = + { + YachtCategory.Ones, YachtCategory.Twos, YachtCategory.Threes, + YachtCategory.Fours, YachtCategory.Fives, YachtCategory.Sixes + }; + + private const int UpperBonusThreshold = 63; + private const int UpperBonusValue = 35; + + private int totalCategoryCount; + + // ── Lifecycle ────────────────────────────────────────────── + + private void Awake() + { + totalCategoryCount = Enum.GetValues(typeof(YachtCategory)).Length; + + // Model → Controller + gameManager.OnTurnStarted += HandleTurnStarted; + gameManager.OnRollComplete += HandleRollComplete; + gameManager.OnScored += HandleScored; + gameManager.OnGameOver += HandleGameOver; + diceManager.OnDieSettled += HandleDieSettled; + + // View → Controller + scoreCardView.OnCategorySelected += HandleCategorySelected; + dicePanelView.OnRollClicked += HandleRollClicked; + dicePanelView.OnDiceToggled += HandleDiceToggled; + gameInfoView.OnNewGameClicked += HandleNewGameClicked; + } + + private void OnDestroy() + { + gameManager.OnTurnStarted -= HandleTurnStarted; + gameManager.OnRollComplete -= HandleRollComplete; + gameManager.OnScored -= HandleScored; + gameManager.OnGameOver -= HandleGameOver; + diceManager.OnDieSettled -= HandleDieSettled; + + scoreCardView.OnCategorySelected -= HandleCategorySelected; + dicePanelView.OnRollClicked -= HandleRollClicked; + dicePanelView.OnDiceToggled -= HandleDiceToggled; + gameInfoView.OnNewGameClicked -= HandleNewGameClicked; + } + + // ── Model Event Handlers ────────────────────────────────── + + private void HandleTurnStarted(int turn) + { + gameInfoView.SetTurnText(turn, totalCategoryCount); + dicePanelView.ResetForNewTurn(); + dicePanelView.SetRollButtonState(true, 0, maxRollsPerTurn); + scoreCardView.ClearAllPreviews(); + } + + private void HandleRollComplete(int rollNumber) + { + bool canRollAgain = gameManager.CanRoll; + dicePanelView.SetRollButtonState(canRollAgain, rollNumber, maxRollsPerTurn); + dicePanelView.SetDiceInteractable(true); + + int[] values = diceManager.GetCurrentValues(); + dicePanelView.SetAllDiceValues(values); + + UpdatePreviewScores(values); + } + + private void HandleDieSettled(int index, int value) + { + dicePanelView.SetDieValue(index, value); + } + + private void HandleScored(YachtCategory category, int finalScore) + { + scoreCardView.SetCategoryScored(category, finalScore); + UpdateTotalDisplay(); + } + + private void HandleGameOver(int totalScore) + { + dicePanelView.SetRollButtonState(false, maxRollsPerTurn, maxRollsPerTurn); + dicePanelView.SetDiceInteractable(false); + scoreCardView.SetAllInteractable(false); + + int displayTotal = CalculateDisplayTotal(); + gameInfoView.ShowGameOver(displayTotal); + } + + // ── View Event Handlers ─────────────────────────────────── + + private void HandleRollClicked() + { + if (!gameManager.CanRoll) return; + + dicePanelView.SetRollButtonState(false, gameManager.CurrentRoll, maxRollsPerTurn); + dicePanelView.SetDiceInteractable(false); + scoreCardView.SetAllInteractable(false); + + gameManager.Roll(); + } + + private void HandleDiceToggled(int index) + { + if (gameManager.CurrentRoll == 0) return; + if (diceManager.IsAnyRolling) return; + + gameManager.ToggleDiceLock(index); + + bool isLocked = diceManager.IsLocked(index); + dicePanelView.SetDieLocked(index, isLocked); + } + + private void HandleCategorySelected(YachtCategory category) + { + if (!gameManager.CanScore) return; + if (scoringSystem.IsCategoryUsed(category)) return; + + gameManager.ScoreInCategory(category); + } + + private void HandleNewGameClicked() + { + gameInfoView.HideGameOver(); + scoreCardView.ResetAll(); + dicePanelView.ResetForNewGame(); + gameManager.StartNewGame(); + } + + // ── Helpers ──────────────────────────────────────────────── + + private void UpdatePreviewScores(int[] diceValues) + { + var previews = new Dictionary(); + var categories = (YachtCategory[])Enum.GetValues(typeof(YachtCategory)); + + for (int i = 0; i < categories.Length; i++) + { + if (scoringSystem.IsCategoryUsed(categories[i])) continue; + + ScoreResult result = scoringSystem.PreviewScore(diceValues, categories[i]); + previews[categories[i]] = result.FinalScore; + } + + scoreCardView.UpdatePreviews(previews); + } + + private void UpdateTotalDisplay() + { + int upperSum = 0; + for (int i = 0; i < UpperCategories.Length; i++) + { + int catScore = scoringSystem.GetCategoryScore(UpperCategories[i]); + if (catScore >= 0) upperSum += catScore; + } + + bool hasBonus = upperSum >= UpperBonusThreshold; + int displayTotal = CalculateDisplayTotal(); + + scoreCardView.UpdateTotalDisplay(displayTotal, upperSum, hasBonus); + } + + private int CalculateDisplayTotal() + { + int total = scoringSystem.TotalScore; + + int upperSum = 0; + for (int i = 0; i < UpperCategories.Length; i++) + { + int catScore = scoringSystem.GetCategoryScore(UpperCategories[i]); + if (catScore >= 0) upperSum += catScore; + } + + if (upperSum >= UpperBonusThreshold) + total += UpperBonusValue; + + return total; + } +} diff --git a/Assets/Scripts/UI/GameInfoView.cs b/Assets/Scripts/UI/GameInfoView.cs new file mode 100644 index 0000000..e8b4e84 --- /dev/null +++ b/Assets/Scripts/UI/GameInfoView.cs @@ -0,0 +1,44 @@ +using System; +using UnityEngine; +using UnityEngine.UI; +using TMPro; + +public sealed class GameInfoView : MonoBehaviour +{ + [Header("Turn Info")] + [SerializeField] private TMP_Text turnText; + + [Header("Game Over Overlay")] + [SerializeField] private GameObject gameOverPanel; + [SerializeField] private TMP_Text finalScoreText; + [SerializeField] private Button newGameButton; + + public event Action OnNewGameClicked; + + private void Awake() + { + newGameButton.onClick.AddListener(() => OnNewGameClicked?.Invoke()); + gameOverPanel.SetActive(false); + } + + public void SetTurnText(int turn, int maxTurns) + { + turnText.text = $"Ход {turn} / {maxTurns}"; + } + + public void ShowGameOver(int finalScore) + { + gameOverPanel.SetActive(true); + finalScoreText.text = $"Итого: {finalScore}"; + } + + public void HideGameOver() + { + gameOverPanel.SetActive(false); + } + + private void OnDestroy() + { + newGameButton.onClick.RemoveAllListeners(); + } +} diff --git a/Assets/Scripts/UI/ScoreCardView.cs b/Assets/Scripts/UI/ScoreCardView.cs new file mode 100644 index 0000000..684ee63 --- /dev/null +++ b/Assets/Scripts/UI/ScoreCardView.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using TMPro; + +public sealed class ScoreCardView : MonoBehaviour +{ + [Header("Category Rows (in YachtCategory enum order)")] + [SerializeField] private List categoryRows = new(); + + [Header("Summary")] + [SerializeField] private TMP_Text upperSumText; + [SerializeField] private TMP_Text upperBonusText; + [SerializeField] private TMP_Text totalScoreText; + + public event Action OnCategorySelected; + + private static readonly string[] CategoryNames = + { + "Единицы", + "Двойки", + "Тройки", + "Четвёрки", + "Пятёрки", + "Шестёрки", + "Тройка", + "Каре", + "Фулл-хаус", + "Малый стрит", + "Большой стрит", + "Яхта", + "Шанс" + }; + + private YachtCategory[] allCategories; + + private void Awake() + { + allCategories = (YachtCategory[])Enum.GetValues(typeof(YachtCategory)); + + for (int i = 0; i < categoryRows.Count && i < allCategories.Length; i++) + { + categoryRows[i].Initialize(allCategories[i], CategoryNames[i]); + categoryRows[i].OnCategorySelected += HandleCategorySelected; + } + + UpdateTotalDisplay(0, 0, false); + } + + public void UpdatePreviews(Dictionary previews) + { + for (int i = 0; i < categoryRows.Count && i < allCategories.Length; i++) + { + if (previews.TryGetValue(allCategories[i], out int preview)) + { + categoryRows[i].ShowPreview(preview); + categoryRows[i].SetInteractable(true); + } + } + } + + public void ClearAllPreviews() + { + for (int i = 0; i < categoryRows.Count; i++) + { + categoryRows[i].HidePreview(); + categoryRows[i].SetInteractable(false); + } + } + + public void SetCategoryScored(YachtCategory category, int score) + { + int index = (int)category; + if (index >= 0 && index < categoryRows.Count) + categoryRows[index].SetRecordedScore(score); + } + + public void SetAllInteractable(bool interactable) + { + for (int i = 0; i < categoryRows.Count; i++) + categoryRows[i].SetInteractable(interactable); + } + + public void UpdateTotalDisplay(int totalScore, int upperSum, bool hasUpperBonus) + { + totalScoreText.text = totalScore.ToString(); + upperSumText.text = $"{upperSum} / 63"; + upperBonusText.text = hasUpperBonus ? "+35" : "---"; + } + + public void ResetAll() + { + for (int i = 0; i < categoryRows.Count; i++) + categoryRows[i].ResetRow(); + + UpdateTotalDisplay(0, 0, false); + } + + private void HandleCategorySelected(YachtCategory category) + { + OnCategorySelected?.Invoke(category); + } + + private void OnDestroy() + { + for (int i = 0; i < categoryRows.Count; i++) + categoryRows[i].OnCategorySelected -= HandleCategorySelected; + } +}