Merge branch 'claude/nifty-turing'

This commit is contained in:
2026-02-27 03:45:03 +07:00
5 changed files with 536 additions and 0 deletions
+85
View File
@@ -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<YachtCategory> 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);
}
}
+101
View File
@@ -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<int> 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();
}
}
+197
View File
@@ -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<YachtCategory, int>();
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;
}
}
+44
View File
@@ -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();
}
}
+109
View File
@@ -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<CategoryRowView> categoryRows = new();
[Header("Summary")]
[SerializeField] private TMP_Text upperSumText;
[SerializeField] private TMP_Text upperBonusText;
[SerializeField] private TMP_Text totalScoreText;
public event Action<YachtCategory> 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<YachtCategory, int> 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;
}
}