From 0f9b162061bc28f03b284745a8a250538d1850d6 Mon Sep 17 00:00:00 2001 From: Konstantin Dyachenko Date: Sun, 1 Mar 2026 11:46:50 +0700 Subject: [PATCH] [Refactor] Replace hardcoded categories with data-driven SO system and abstract dice - Add abstract dice system (IDie interface, DieDefinitionSO, StandardDieSO, DieInstance) to support future custom dice types while keeping backward compat via int[] DiceValues - Replace YachtCategory enum and CategoryScorer switch with CategoryDefinitionSO hierarchy: SumOfValueCategorySO, NOfAKindCategorySO, FullHouseCategorySO, StraightCategorySO, SumAllCategorySO - Add CategoryCatalogSO for ordered category collections and DiceCheckUtility for shared logic - Refactor ScoringSystem, Views, GameManager, GameController to use SO references - Update CategoryCondition modifier to use SO reference instead of enum - Update all editor tests to use SO-based categories and DieInstance Co-Authored-By: Claude Opus 4.6 --- .../Definition/CategoryCatalogSO.cs | 48 ++++++ .../Definition/CategoryDefinitionSO.cs | 42 +++++ .../Definition/FullHouseCategorySO.cs | 34 ++++ .../Definition/NOfAKindCategorySO.cs | 47 ++++++ .../Definition/StraightCategorySO.cs | 39 +++++ .../Categories/Definition/SumAllCategorySO.cs | 30 ++++ .../Definition/SumOfValueCategorySO.cs | 38 +++++ Assets/Scripts/Categories/DiceCheckUtility.cs | 78 ++++++++++ Assets/Scripts/DI/GameLifetimeScope.cs | 7 +- Assets/Scripts/Dice/DiceRoller.cs | 4 + Assets/Scripts/Dice/DieDefinitionSO.cs | 36 +++++ Assets/Scripts/Dice/DieInstance.cs | 27 ++++ Assets/Scripts/Dice/IDie.cs | 15 ++ Assets/Scripts/Dice/StandardDieSO.cs | 33 ++++ Assets/Scripts/Game/DiceManager.cs | 40 +++-- Assets/Scripts/Game/GameManager.cs | 11 +- .../Scripts/Inventory/InventoryController.cs | 3 +- .../Modifiers/Conditions/CategoryCondition.cs | 6 +- .../Scripts/Modifiers/Core/ModifierContext.cs | 18 ++- Assets/Scripts/Scoring/CategoryScorer.cs | 82 ---------- Assets/Scripts/Scoring/CategoryScorer.cs.meta | 2 - Assets/Scripts/Scoring/ScoreResult.cs | 9 +- Assets/Scripts/Scoring/ScoringSystem.cs | 51 +++--- Assets/Scripts/Scoring/YachtCategory.cs | 22 --- Assets/Scripts/Scoring/YachtCategory.cs.meta | 2 - .../Tests/Editor/ModifierEffectTests.cs | 51 +++--- .../Tests/Editor/ModifierPipelineTests.cs | 76 +++++---- .../Tests/Editor/ScoringSystemTests.cs | 145 ++++++++++++++++-- Assets/Scripts/UI/CategoryRowView.cs | 12 +- Assets/Scripts/UI/GameController.cs | 75 ++++----- Assets/Scripts/UI/ScoreCardView.cs | 60 ++++---- 31 files changed, 845 insertions(+), 298 deletions(-) create mode 100644 Assets/Scripts/Categories/Definition/CategoryCatalogSO.cs create mode 100644 Assets/Scripts/Categories/Definition/CategoryDefinitionSO.cs create mode 100644 Assets/Scripts/Categories/Definition/FullHouseCategorySO.cs create mode 100644 Assets/Scripts/Categories/Definition/NOfAKindCategorySO.cs create mode 100644 Assets/Scripts/Categories/Definition/StraightCategorySO.cs create mode 100644 Assets/Scripts/Categories/Definition/SumAllCategorySO.cs create mode 100644 Assets/Scripts/Categories/Definition/SumOfValueCategorySO.cs create mode 100644 Assets/Scripts/Categories/DiceCheckUtility.cs create mode 100644 Assets/Scripts/Dice/DieDefinitionSO.cs create mode 100644 Assets/Scripts/Dice/DieInstance.cs create mode 100644 Assets/Scripts/Dice/IDie.cs create mode 100644 Assets/Scripts/Dice/StandardDieSO.cs delete mode 100644 Assets/Scripts/Scoring/CategoryScorer.cs delete mode 100644 Assets/Scripts/Scoring/CategoryScorer.cs.meta delete mode 100644 Assets/Scripts/Scoring/YachtCategory.cs delete mode 100644 Assets/Scripts/Scoring/YachtCategory.cs.meta diff --git a/Assets/Scripts/Categories/Definition/CategoryCatalogSO.cs b/Assets/Scripts/Categories/Definition/CategoryCatalogSO.cs new file mode 100644 index 0000000..4120902 --- /dev/null +++ b/Assets/Scripts/Categories/Definition/CategoryCatalogSO.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace YachtDice.Categories +{ + /// + /// Каталог всех доступных категорий. + /// Порядок определяет порядок отображения в UI. + /// Аналог ModifierCatalogSO. + /// + [CreateAssetMenu(fileName = "CategoryCatalog", menuName = "YachtDice/Categories/Catalog")] + public class CategoryCatalogSO : ScriptableObject + { + [SerializeField] private List categories = new(); + + public IReadOnlyList All => categories; + public int Count => categories.Count; + + public CategoryDefinitionSO FindById(string id) + { + for (int i = 0; i < categories.Count; i++) + { + if (categories[i] != null && categories[i].Id == id) + return categories[i]; + } + return null; + } + + public int IndexOf(CategoryDefinitionSO def) + { + for (int i = 0; i < categories.Count; i++) + { + if (categories[i] == def) + return i; + } + return -1; + } + +#if UNITY_EDITOR + public static CategoryCatalogSO CreateForTest(List defs) + { + var catalog = CreateInstance(); + catalog.categories = defs ?? new List(); + return catalog; + } +#endif + } +} diff --git a/Assets/Scripts/Categories/Definition/CategoryDefinitionSO.cs b/Assets/Scripts/Categories/Definition/CategoryDefinitionSO.cs new file mode 100644 index 0000000..66e92c3 --- /dev/null +++ b/Assets/Scripts/Categories/Definition/CategoryDefinitionSO.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using UnityEngine; +using YachtDice.Dice; + +namespace YachtDice.Categories +{ + /// + /// Абстрактное определение категории для скоринга. + /// Каждая категория знает как вычислить очки по набору дайсов. + /// + public abstract class CategoryDefinitionSO : ScriptableObject + { + [Header("Identity")] + [SerializeField] private string id; + [SerializeField] private string displayName; + [SerializeField, TextArea] private string description; + [SerializeField] private Sprite icon; + + [Header("Section")] + [SerializeField] private bool isUpperSection; + + public string Id => id; + public string DisplayName => displayName; + public string Description => description; + public Sprite Icon => icon; + public bool IsUpperSection => isUpperSection; + + /// + /// Вычисляет очки для данного набора дайсов. + /// + public abstract int Calculate(IReadOnlyList dice); + +#if UNITY_EDITOR + public void SetTestData(string testId, string testDisplayName, bool upperSection = false) + { + id = testId; + displayName = testDisplayName; + isUpperSection = upperSection; + } +#endif + } +} diff --git a/Assets/Scripts/Categories/Definition/FullHouseCategorySO.cs b/Assets/Scripts/Categories/Definition/FullHouseCategorySO.cs new file mode 100644 index 0000000..a209919 --- /dev/null +++ b/Assets/Scripts/Categories/Definition/FullHouseCategorySO.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using UnityEngine; +using YachtDice.Dice; + +namespace YachtDice.Categories +{ + /// + /// Категория Фулл-хаус: 3 одинаковых + 2 одинаковых. + /// При совпадении возвращает фиксированное число очков. + /// + [CreateAssetMenu(fileName = "FullHouseCategory", menuName = "YachtDice/Categories/Full House")] + public class FullHouseCategorySO : CategoryDefinitionSO + { + [Header("Scoring")] + [Tooltip("Фиксированное число очков за фулл-хаус")] + [SerializeField] private int fixedScore = 25; + + public override int Calculate(IReadOnlyList dice) + { + int[] values = DiceCheckUtility.ExtractValues(dice); + return DiceCheckUtility.IsFullHouse(values) ? fixedScore : 0; + } + +#if UNITY_EDITOR + public static FullHouseCategorySO CreateForTest(string id, string displayName, int score = 25) + { + var so = CreateInstance(); + so.SetTestData(id, displayName); + so.fixedScore = score; + return so; + } +#endif + } +} diff --git a/Assets/Scripts/Categories/Definition/NOfAKindCategorySO.cs b/Assets/Scripts/Categories/Definition/NOfAKindCategorySO.cs new file mode 100644 index 0000000..b22d72f --- /dev/null +++ b/Assets/Scripts/Categories/Definition/NOfAKindCategorySO.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using UnityEngine; +using YachtDice.Dice; + +namespace YachtDice.Categories +{ + /// + /// Категория N-одинаковых: проверяет наличие N дайсов с одинаковым значением. + /// При успехе возвращает сумму всех дайсов или фиксированное число очков. + /// Используется для Тройки (3, сумма), Каре (4, сумма), Яхты (5, fixed=50). + /// + [CreateAssetMenu(fileName = "NOfAKindCategory", menuName = "YachtDice/Categories/N Of A Kind")] + public class NOfAKindCategorySO : CategoryDefinitionSO + { + [Header("Scoring")] + [Tooltip("Сколько одинаковых дайсов требуется")] + [SerializeField, Range(2, 6)] private int requiredCount = 3; + + [Tooltip("Использовать фиксированное число очков вместо суммы")] + [SerializeField] private bool useFixedScore; + + [Tooltip("Фиксированное число очков (если useFixedScore = true)")] + [SerializeField] private int fixedScore; + + public override int Calculate(IReadOnlyList dice) + { + int[] values = DiceCheckUtility.ExtractValues(dice); + + if (!DiceCheckUtility.NOfAKind(values, requiredCount)) + return 0; + + return useFixedScore ? fixedScore : DiceCheckUtility.Sum(values); + } + +#if UNITY_EDITOR + public static NOfAKindCategorySO CreateForTest(string id, string displayName, int count, bool fixedScoreMode = false, int score = 0) + { + var so = CreateInstance(); + so.SetTestData(id, displayName); + so.requiredCount = count; + so.useFixedScore = fixedScoreMode; + so.fixedScore = score; + return so; + } +#endif + } +} diff --git a/Assets/Scripts/Categories/Definition/StraightCategorySO.cs b/Assets/Scripts/Categories/Definition/StraightCategorySO.cs new file mode 100644 index 0000000..069a5fb --- /dev/null +++ b/Assets/Scripts/Categories/Definition/StraightCategorySO.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using UnityEngine; +using YachtDice.Dice; + +namespace YachtDice.Categories +{ + /// + /// Категория Стрит: последовательность заданной длины. + /// При совпадении возвращает фиксированное число очков. + /// Малый стрит: runLength=4, fixedScore=30. Большой стрит: runLength=5, fixedScore=40. + /// + [CreateAssetMenu(fileName = "StraightCategory", menuName = "YachtDice/Categories/Straight")] + public class StraightCategorySO : CategoryDefinitionSO + { + [Header("Scoring")] + [Tooltip("Требуемая длина последовательности")] + [SerializeField, Range(3, 6)] private int runLength = 4; + + [Tooltip("Фиксированное число очков")] + [SerializeField] private int fixedScore = 30; + + public override int Calculate(IReadOnlyList dice) + { + int[] values = DiceCheckUtility.ExtractValues(dice); + return DiceCheckUtility.HasStraightRun(values, runLength) ? fixedScore : 0; + } + +#if UNITY_EDITOR + public static StraightCategorySO CreateForTest(string id, string displayName, int run, int score) + { + var so = CreateInstance(); + so.SetTestData(id, displayName); + so.runLength = run; + so.fixedScore = score; + return so; + } +#endif + } +} diff --git a/Assets/Scripts/Categories/Definition/SumAllCategorySO.cs b/Assets/Scripts/Categories/Definition/SumAllCategorySO.cs new file mode 100644 index 0000000..031464b --- /dev/null +++ b/Assets/Scripts/Categories/Definition/SumAllCategorySO.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using UnityEngine; +using YachtDice.Dice; + +namespace YachtDice.Categories +{ + /// + /// Категория «Шанс»: суммирует все дайсы без условий. + /// + [CreateAssetMenu(fileName = "SumAllCategory", menuName = "YachtDice/Categories/Sum All (Chance)")] + public class SumAllCategorySO : CategoryDefinitionSO + { + public override int Calculate(IReadOnlyList dice) + { + int sum = 0; + for (int i = 0; i < dice.Count; i++) + sum += dice[i].Value; + return sum; + } + +#if UNITY_EDITOR + public static SumAllCategorySO CreateForTest(string id, string displayName) + { + var so = CreateInstance(); + so.SetTestData(id, displayName); + return so; + } +#endif + } +} diff --git a/Assets/Scripts/Categories/Definition/SumOfValueCategorySO.cs b/Assets/Scripts/Categories/Definition/SumOfValueCategorySO.cs new file mode 100644 index 0000000..e10a415 --- /dev/null +++ b/Assets/Scripts/Categories/Definition/SumOfValueCategorySO.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using UnityEngine; +using YachtDice.Dice; + +namespace YachtDice.Categories +{ + /// + /// Категория верхней секции: суммирует все дайсы с заданным значением. + /// Используется для Единиц (1), Двоек (2), ... Шестёрок (6). + /// + [CreateAssetMenu(fileName = "SumOfValueCategory", menuName = "YachtDice/Categories/Sum Of Value")] + public class SumOfValueCategorySO : CategoryDefinitionSO + { + [Header("Scoring")] + [Tooltip("Значение грани для суммирования (1-6)")] + [SerializeField, Range(1, 6)] private int targetValue = 1; + + public int TargetValue => targetValue; + + public override int Calculate(IReadOnlyList dice) + { + int sum = 0; + for (int i = 0; i < dice.Count; i++) + if (dice[i].Value == targetValue) sum += targetValue; + return sum; + } + +#if UNITY_EDITOR + public static SumOfValueCategorySO CreateForTest(string id, string displayName, int target) + { + var so = CreateInstance(); + so.SetTestData(id, displayName, upperSection: true); + so.targetValue = target; + return so; + } +#endif + } +} diff --git a/Assets/Scripts/Categories/DiceCheckUtility.cs b/Assets/Scripts/Categories/DiceCheckUtility.cs new file mode 100644 index 0000000..05e4417 --- /dev/null +++ b/Assets/Scripts/Categories/DiceCheckUtility.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using YachtDice.Dice; + +namespace YachtDice.Categories +{ + /// + /// Статические хелперы для проверки комбинаций дайсов. + /// Перенесены из CategoryScorer для переиспользования в конкретных SO-категориях. + /// + public static class DiceCheckUtility + { + /// Извлекает массив значений из абстрактных дайсов. + public static int[] ExtractValues(IReadOnlyList dice) + { + int[] values = new int[dice.Count]; + for (int i = 0; i < dice.Count; i++) + values[i] = dice[i].Value; + return values; + } + + /// Сумма дайсов, показывающих конкретное значение. + public static int SumOfValue(int[] values, int target) + { + int sum = 0; + for (int i = 0; i < values.Length; i++) + if (values[i] == target) sum += target; + return sum; + } + + /// Сумма всех дайсов. + public static int Sum(int[] values) + { + int sum = 0; + for (int i = 0; i < values.Length; i++) sum += values[i]; + return sum; + } + + /// Есть ли N или более одинаковых значений. + public static bool NOfAKind(int[] values, int n) + { + int[] counts = new int[7]; + for (int i = 0; i < values.Length; i++) counts[values[i]]++; + for (int v = 1; v <= 6; v++) + if (counts[v] >= n) return true; + return false; + } + + /// Проверяет фулл-хаус (3 + 2 одинаковых). + public static bool IsFullHouse(int[] values) + { + int[] counts = new int[7]; + for (int i = 0; i < values.Length; i++) counts[values[i]]++; + bool hasTwo = false, hasThree = false; + for (int v = 1; v <= 6; v++) + { + if (counts[v] == 2) hasTwo = true; + if (counts[v] == 3) hasThree = true; + } + return hasTwo && hasThree; + } + + /// Есть ли последовательность заданной длины. + public static bool HasStraightRun(int[] values, int runLength) + { + bool[] present = new bool[7]; + for (int i = 0; i < values.Length; i++) present[values[i]] = true; + + int consecutive = 0; + for (int v = 1; v <= 6; v++) + { + consecutive = present[v] ? consecutive + 1 : 0; + if (consecutive >= runLength) return true; + } + return false; + } + } +} diff --git a/Assets/Scripts/DI/GameLifetimeScope.cs b/Assets/Scripts/DI/GameLifetimeScope.cs index e4367cb..4bdb6de 100644 --- a/Assets/Scripts/DI/GameLifetimeScope.cs +++ b/Assets/Scripts/DI/GameLifetimeScope.cs @@ -1,6 +1,7 @@ using UnityEngine; using VContainer; using VContainer.Unity; +using YachtDice.Categories; using YachtDice.Economy; using YachtDice.Events; using YachtDice.Game; @@ -14,6 +15,7 @@ namespace YachtDice.DI public class GameLifetimeScope : LifetimeScope { [SerializeField] private ModifierCatalogSO modifierCatalog; + [SerializeField] private CategoryCatalogSO categoryCatalog; [Header("Scene References")] [SerializeField] private ScoringSystem scoringSystem; @@ -23,8 +25,9 @@ namespace YachtDice.DI protected override void Configure(IContainerBuilder builder) { - // SO catalog + // SO catalogs builder.RegisterInstance(modifierCatalog); + builder.RegisterInstance(categoryCatalog); // Core modifier services builder.Register(Lifetime.Singleton); @@ -38,4 +41,4 @@ namespace YachtDice.DI builder.RegisterComponent(diceManager); } } -} \ No newline at end of file +} diff --git a/Assets/Scripts/Dice/DiceRoller.cs b/Assets/Scripts/Dice/DiceRoller.cs index 9c9be7b..8506779 100644 --- a/Assets/Scripts/Dice/DiceRoller.cs +++ b/Assets/Scripts/Dice/DiceRoller.cs @@ -14,6 +14,10 @@ namespace YachtDice.Dice [Header("References")] [SerializeField] private Dice dice; [SerializeField] private Rigidbody rb; + [SerializeField] private DieDefinitionSO definition; + + /// Определение типа дайса (назначается в инспекторе). + public DieDefinitionSO Definition => definition; [Header("Throw Settings")] [Tooltip("Сила подброса вверх")] diff --git a/Assets/Scripts/Dice/DieDefinitionSO.cs b/Assets/Scripts/Dice/DieDefinitionSO.cs new file mode 100644 index 0000000..789d755 --- /dev/null +++ b/Assets/Scripts/Dice/DieDefinitionSO.cs @@ -0,0 +1,36 @@ +using UnityEngine; + +namespace YachtDice.Dice +{ + /// + /// Абстрактное определение типа дайса. + /// Наследники описывают конкретные виды (стандартный d6, специальные и т.д.). + /// + public abstract class DieDefinitionSO : ScriptableObject + { + [Header("Identity")] + [SerializeField] private string id; + [SerializeField] private string displayName; + [SerializeField] private Sprite icon; + + public string Id => id; + public string DisplayName => displayName; + public Sprite Icon => icon; + + /// Количество граней. + public abstract int FaceCount { get; } + + /// Возвращает массив всех возможных значений граней. + public abstract int[] GetFaceValues(); + +#if UNITY_EDITOR + public static T CreateForTest(string id, string displayName = null) where T : DieDefinitionSO + { + var so = CreateInstance(); + so.id = id; + so.displayName = displayName ?? id; + return so; + } +#endif + } +} diff --git a/Assets/Scripts/Dice/DieInstance.cs b/Assets/Scripts/Dice/DieInstance.cs new file mode 100644 index 0000000..4136957 --- /dev/null +++ b/Assets/Scripts/Dice/DieInstance.cs @@ -0,0 +1,27 @@ +namespace YachtDice.Dice +{ + /// + /// Рантайм-состояние одного дайса. + /// Хранит текущее значение верхней грани и ссылку на определение типа. + /// + public class DieInstance : IDie + { + public DieDefinitionSO Definition { get; } + public int Value { get; set; } + public bool IsLocked { get; set; } + + public DieInstance(DieDefinitionSO definition) + { + Definition = definition; + Value = 0; + IsLocked = false; + } + + public DieInstance(DieDefinitionSO definition, int initialValue) + { + Definition = definition; + Value = initialValue; + IsLocked = false; + } + } +} diff --git a/Assets/Scripts/Dice/IDie.cs b/Assets/Scripts/Dice/IDie.cs new file mode 100644 index 0000000..073d8a9 --- /dev/null +++ b/Assets/Scripts/Dice/IDie.cs @@ -0,0 +1,15 @@ +namespace YachtDice.Dice +{ + /// + /// Минимальный контракт для любого дайса. + /// Каждый дайс всегда имеет текущее значение (верхняя грань) и определение типа. + /// + public interface IDie + { + /// Текущее значение верхней грани. + int Value { get; } + + /// Определение типа дайса (ScriptableObject). + DieDefinitionSO Definition { get; } + } +} diff --git a/Assets/Scripts/Dice/StandardDieSO.cs b/Assets/Scripts/Dice/StandardDieSO.cs new file mode 100644 index 0000000..f192a21 --- /dev/null +++ b/Assets/Scripts/Dice/StandardDieSO.cs @@ -0,0 +1,33 @@ +using UnityEngine; + +namespace YachtDice.Dice +{ + /// + /// Стандартный дайс с настраиваемыми значениями граней. + /// По умолчанию — классический d6 (1-6). + /// + [CreateAssetMenu(fileName = "StandardDie", menuName = "YachtDice/Dice/Standard Die")] + public class StandardDieSO : DieDefinitionSO + { + [Header("Configuration")] + [SerializeField] private int[] faceValues = { 1, 2, 3, 4, 5, 6 }; + + public override int FaceCount => faceValues.Length; + + public override int[] GetFaceValues() + { + int[] copy = new int[faceValues.Length]; + System.Array.Copy(faceValues, copy, faceValues.Length); + return copy; + } + +#if UNITY_EDITOR + public static StandardDieSO CreateStandardD6ForTest() + { + var so = CreateForTest("standard_d6", "Стандартный d6"); + so.faceValues = new[] { 1, 2, 3, 4, 5, 6 }; + return so; + } +#endif + } +} diff --git a/Assets/Scripts/Game/DiceManager.cs b/Assets/Scripts/Game/DiceManager.cs index f7eec67..ba4114b 100644 --- a/Assets/Scripts/Game/DiceManager.cs +++ b/Assets/Scripts/Game/DiceManager.cs @@ -14,32 +14,37 @@ namespace YachtDice.Game public int DiceCount => diceRollers.Count; - private bool[] locked; - private int[] currentValues; + private DieInstance[] diceInstances; private int pendingCount; private void Awake() { int count = diceRollers.Count; - locked = new bool[count]; - currentValues = new int[count]; + diceInstances = new DieInstance[count]; + + for (int i = 0; i < count; i++) + { + var definition = diceRollers[i].Definition; + diceInstances[i] = new DieInstance(definition); + } } - public bool IsLocked(int index) => locked[index]; + public bool IsLocked(int index) => diceInstances[index].IsLocked; public void ToggleLock(int index) { - locked[index] = !locked[index]; + diceInstances[index].IsLocked = !diceInstances[index].IsLocked; } public void SetLocked(int index, bool isLocked) { - locked[index] = isLocked; + diceInstances[index].IsLocked = isLocked; } public void UnlockAll() { - for (int i = 0; i < locked.Length; i++) locked[i] = false; + for (int i = 0; i < diceInstances.Length; i++) + diceInstances[i].IsLocked = false; } public void RollUnlocked() @@ -51,7 +56,7 @@ namespace YachtDice.Game for (int i = 0; i < diceRollers.Count; i++) { - if (locked[i]) continue; + if (diceInstances[i].IsLocked) continue; pendingCount++; int capturedIndex = i; @@ -59,7 +64,7 @@ namespace YachtDice.Game void Handler(int value) { diceRollers[capturedIndex].OnRollFinished -= Handler; - currentValues[capturedIndex] = value; + diceInstances[capturedIndex].Value = value; OnDieSettled?.Invoke(capturedIndex, value); pendingCount--; @@ -75,14 +80,19 @@ namespace YachtDice.Game OnAllDiceSettled?.Invoke(); } + /// Возвращает абстрактный список дайсов (основной API). + public IReadOnlyList GetDice() => diceInstances; + + /// Возвращает копию текущих значений (обратная совместимость). public int[] GetCurrentValues() { - int[] copy = new int[currentValues.Length]; - Array.Copy(currentValues, copy, currentValues.Length); - return copy; + int[] values = new int[diceInstances.Length]; + for (int i = 0; i < diceInstances.Length; i++) + values[i] = diceInstances[i].Value; + return values; } - public int GetValue(int index) => currentValues[index]; + public int GetValue(int index) => diceInstances[index].Value; public bool IsAnyRolling { @@ -100,7 +110,7 @@ namespace YachtDice.Game { var diceComponent = diceRollers[i].GetComponent(); if (diceComponent != null && diceComponent.TryGetTopValue(out int val)) - currentValues[i] = val; + diceInstances[i].Value = val; } } } diff --git a/Assets/Scripts/Game/GameManager.cs b/Assets/Scripts/Game/GameManager.cs index 8c82cc9..196cbf1 100644 --- a/Assets/Scripts/Game/GameManager.cs +++ b/Assets/Scripts/Game/GameManager.cs @@ -1,5 +1,6 @@ using System; using UnityEngine; +using YachtDice.Categories; using YachtDice.Scoring; namespace YachtDice.Game @@ -22,7 +23,7 @@ namespace YachtDice.Game public event Action OnTurnStarted; public event Action OnRollComplete; - public event Action OnScored; + public event Action OnScored; public event Action OnGameOver; private void Start() @@ -75,15 +76,15 @@ namespace YachtDice.Game Debug.Log($"Dice {index + 1} (value={diceManager.GetValue(index)}): {(isLocked ? "LOCKED" : "UNLOCKED")}"); } - public void ScoreInCategory(YachtCategory category) + public void ScoreInCategory(CategoryDefinitionSO category) { if (!CanScore) return; if (scoringSystem.IsCategoryUsed(category)) return; - int[] values = diceManager.GetCurrentValues(); - ScoreResult result = scoringSystem.ScoreCategory(values, category); + var dice = diceManager.GetDice(); + ScoreResult result = scoringSystem.ScoreCategory(dice, category); - Debug.Log($"Scored {category}: base={result.BaseScore}, " + + Debug.Log($"Scored {category.DisplayName}: base={result.BaseScore}, " + $"bonus=+{result.FlatBonus}, mult=x{result.Multiplier:F1}, " + $"FINAL={result.FinalScore} | Total={scoringSystem.TotalScore}"); diff --git a/Assets/Scripts/Inventory/InventoryController.cs b/Assets/Scripts/Inventory/InventoryController.cs index 33dd263..6e620d0 100644 --- a/Assets/Scripts/Inventory/InventoryController.cs +++ b/Assets/Scripts/Inventory/InventoryController.cs @@ -1,4 +1,5 @@ using UnityEngine; +using YachtDice.Categories; using YachtDice.Economy; using YachtDice.Modifiers.Runtime; using YachtDice.Scoring; @@ -73,7 +74,7 @@ namespace YachtDice.Inventory RefreshView(); } - private void HandleCategoryConfirmed(YachtCategory category, ScoreResult result) + private void HandleCategoryConfirmed(CategoryDefinitionSO category, ScoreResult result) { model.ConsumeUseOnActive(); } diff --git a/Assets/Scripts/Modifiers/Conditions/CategoryCondition.cs b/Assets/Scripts/Modifiers/Conditions/CategoryCondition.cs index 98612da..486345f 100644 --- a/Assets/Scripts/Modifiers/Conditions/CategoryCondition.cs +++ b/Assets/Scripts/Modifiers/Conditions/CategoryCondition.cs @@ -1,15 +1,15 @@ using UnityEngine; +using YachtDice.Categories; using YachtDice.Modifiers.Core; using YachtDice.Modifiers.Definition; using YachtDice.Modifiers.Runtime; -using YachtDice.Scoring; namespace YachtDice.Modifiers.Conditions { [CreateAssetMenu(fileName = "CategoryCondition", menuName = "YachtDice/Modifiers/Conditions/Category")] public class CategoryCondition : ConditionSO { - [SerializeField] private YachtCategory requiredCategory; + [SerializeField] private CategoryDefinitionSO requiredCategory; public override bool Evaluate(ModifierContext context, ModifierInstance instance) { @@ -17,7 +17,7 @@ namespace YachtDice.Modifiers.Conditions } #if UNITY_EDITOR - public static CategoryCondition CreateForTest(YachtCategory category) + public static CategoryCondition CreateForTest(CategoryDefinitionSO category) { var so = CreateInstance(); so.requiredCategory = category; diff --git a/Assets/Scripts/Modifiers/Core/ModifierContext.cs b/Assets/Scripts/Modifiers/Core/ModifierContext.cs index bd5addf..a01ffb9 100644 --- a/Assets/Scripts/Modifiers/Core/ModifierContext.cs +++ b/Assets/Scripts/Modifiers/Core/ModifierContext.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; using UnityEngine; +using YachtDice.Categories; +using YachtDice.Dice; using YachtDice.Modifiers.Runtime; using YachtDice.Scoring; @@ -12,8 +14,15 @@ namespace YachtDice.Modifiers.Core public int FlatBonus; public float Multiplier = 1f; public float PostMultiplier = 1f; + + /// Абстрактные дайсы (основной API). + public IReadOnlyList Dice; + + /// Значения дайсов (обратная совместимость с существующими модификаторами). public int[] DiceValues; - public YachtCategory Category; + + /// Категория, в которую записывается результат. + public CategoryDefinitionSO Category; // Game state (read-only snapshot) public int CurrentRoll; @@ -46,8 +55,8 @@ namespace YachtDice.Modifiers.Core public static ModifierContext CreateForScoring( int baseScore, - int[] diceValues, - YachtCategory category, + IReadOnlyList dice, + CategoryDefinitionSO category, int currentRoll, int currentTurn, int playerCurrency, @@ -56,7 +65,8 @@ namespace YachtDice.Modifiers.Core return new ModifierContext { BaseScore = baseScore, - DiceValues = diceValues, + Dice = dice, + DiceValues = DiceCheckUtility.ExtractValues(dice), Category = category, CurrentRoll = currentRoll, CurrentTurn = currentTurn, diff --git a/Assets/Scripts/Scoring/CategoryScorer.cs b/Assets/Scripts/Scoring/CategoryScorer.cs deleted file mode 100644 index 0b66d3c..0000000 --- a/Assets/Scripts/Scoring/CategoryScorer.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System; - -namespace YachtDice.Scoring -{ - public static class CategoryScorer - { - public static int Calculate(int[] dice, YachtCategory category) - { - if (dice == null || dice.Length != 5) - throw new ArgumentException("Exactly 5 dice values required."); - - return category switch - { - YachtCategory.Ones => SumOfValue(dice, 1), - YachtCategory.Twos => SumOfValue(dice, 2), - YachtCategory.Threes => SumOfValue(dice, 3), - YachtCategory.Fours => SumOfValue(dice, 4), - YachtCategory.Fives => SumOfValue(dice, 5), - YachtCategory.Sixes => SumOfValue(dice, 6), - YachtCategory.ThreeOfAKind => NOfAKind(dice, 3) ? Sum(dice) : 0, - YachtCategory.FourOfAKind => NOfAKind(dice, 4) ? Sum(dice) : 0, - YachtCategory.FullHouse => IsFullHouse(dice) ? 25 : 0, - YachtCategory.SmallStraight => HasStraightRun(dice, 4) ? 30 : 0, - YachtCategory.LargeStraight => HasStraightRun(dice, 5) ? 40 : 0, - YachtCategory.Yacht => NOfAKind(dice, 5) ? 50 : 0, - YachtCategory.Chance => Sum(dice), - _ => 0 - }; - } - - private static int SumOfValue(int[] dice, int target) - { - int sum = 0; - for (int i = 0; i < dice.Length; i++) - if (dice[i] == target) sum += target; - return sum; - } - - private static int Sum(int[] dice) - { - int sum = 0; - for (int i = 0; i < dice.Length; i++) sum += dice[i]; - return sum; - } - - private static bool NOfAKind(int[] dice, int n) - { - int[] counts = new int[7]; - for (int i = 0; i < dice.Length; i++) counts[dice[i]]++; - for (int v = 1; v <= 6; v++) - if (counts[v] >= n) return true; - return false; - } - - private static bool IsFullHouse(int[] dice) - { - int[] counts = new int[7]; - for (int i = 0; i < dice.Length; i++) counts[dice[i]]++; - bool hasTwo = false, hasThree = false; - for (int v = 1; v <= 6; v++) - { - if (counts[v] == 2) hasTwo = true; - if (counts[v] == 3) hasThree = true; - } - return hasTwo && hasThree; - } - - private static bool HasStraightRun(int[] dice, int runLength) - { - bool[] present = new bool[7]; - for (int i = 0; i < dice.Length; i++) present[dice[i]] = true; - - int consecutive = 0; - for (int v = 1; v <= 6; v++) - { - consecutive = present[v] ? consecutive + 1 : 0; - if (consecutive >= runLength) return true; - } - return false; - } - } -} diff --git a/Assets/Scripts/Scoring/CategoryScorer.cs.meta b/Assets/Scripts/Scoring/CategoryScorer.cs.meta deleted file mode 100644 index afefd3e..0000000 --- a/Assets/Scripts/Scoring/CategoryScorer.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 2fa2cb346fa82b846a3cf52c47ca9cda \ No newline at end of file diff --git a/Assets/Scripts/Scoring/ScoreResult.cs b/Assets/Scripts/Scoring/ScoreResult.cs index 3f34fe9..88751f3 100644 --- a/Assets/Scripts/Scoring/ScoreResult.cs +++ b/Assets/Scripts/Scoring/ScoreResult.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; using UnityEngine; +using YachtDice.Categories; +using YachtDice.Dice; namespace YachtDice.Scoring { @@ -10,18 +13,18 @@ namespace YachtDice.Scoring public int FlatBonus; public float Multiplier; public int[] DiceValues; - public YachtCategory Category; + public CategoryDefinitionSO Category; public int FinalScore => Mathf.FloorToInt((BaseScore + FlatBonus) * Multiplier); - public static ScoreResult Create(int baseScore, int[] diceValues, YachtCategory category) + public static ScoreResult Create(int baseScore, IReadOnlyList dice, CategoryDefinitionSO category) { return new ScoreResult { BaseScore = baseScore, FlatBonus = 0, Multiplier = 1f, - DiceValues = diceValues, + DiceValues = DiceCheckUtility.ExtractValues(dice), Category = category }; } diff --git a/Assets/Scripts/Scoring/ScoringSystem.cs b/Assets/Scripts/Scoring/ScoringSystem.cs index b44a216..a257338 100644 --- a/Assets/Scripts/Scoring/ScoringSystem.cs +++ b/Assets/Scripts/Scoring/ScoringSystem.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using Cysharp.Threading.Tasks; using UnityEngine; using VContainer; +using YachtDice.Categories; +using YachtDice.Dice; using YachtDice.Events; using YachtDice.Modifiers.Core; using YachtDice.Modifiers.Runtime; @@ -11,26 +13,30 @@ namespace YachtDice.Scoring { public class ScoringSystem : MonoBehaviour { - public event Action OnCategoryScored; + public event Action OnCategoryScored; public event Action OnAllCategoriesScored; - public event Action OnCategoryConfirmed; + public event Action OnCategoryConfirmed; - private readonly Dictionary scorecard = new(); - private readonly HashSet usedCategories = new(); + private readonly Dictionary scorecard = new(); + private readonly HashSet usedCategories = new(); private GameEventBus eventBus; private ModifierRegistry modifierRegistry; + private CategoryCatalogSO catalog; [Inject] - public void Construct(GameEventBus eventBus, ModifierRegistry modifierRegistry) + public void Construct(GameEventBus eventBus, ModifierRegistry modifierRegistry, CategoryCatalogSO catalog) { this.eventBus = eventBus; this.modifierRegistry = modifierRegistry; + this.catalog = catalog; } - public bool IsCategoryUsed(YachtCategory category) => usedCategories.Contains(category); + public CategoryCatalogSO Catalog => catalog; - public int GetCategoryScore(YachtCategory category) + public bool IsCategoryUsed(CategoryDefinitionSO category) => usedCategories.Contains(category); + + public int GetCategoryScore(CategoryDefinitionSO category) { return scorecard.TryGetValue(category, out int score) ? score : -1; } @@ -47,20 +53,20 @@ namespace YachtDice.Scoring public int CategoriesFilledCount => usedCategories.Count; - public int TotalCategoryCount => Enum.GetValues(typeof(YachtCategory)).Length; + public int TotalCategoryCount => catalog != null ? catalog.Count : 0; public bool IsComplete => CategoriesFilledCount >= TotalCategoryCount; - public ScoreResult PreviewScore(int[] diceValues, YachtCategory category, + public ScoreResult PreviewScore(IReadOnlyList dice, CategoryDefinitionSO category, int currentRoll = 0, int currentTurn = 0, int playerCurrency = 0) { - int baseScore = CategoryScorer.Calculate(diceValues, category); + int baseScore = category.Calculate(dice); if (eventBus == null || modifierRegistry == null) - return ScoreResult.Create(baseScore, diceValues, category); + return ScoreResult.Create(baseScore, dice, category); var context = ModifierContext.CreateForScoring( - baseScore, diceValues, category, + baseScore, dice, category, currentRoll, currentTurn, playerCurrency, modifierRegistry.Active); @@ -69,19 +75,19 @@ namespace YachtDice.Scoring return context.ToScoreResult(); } - public async UniTask ScoreCategoryAsync(int[] diceValues, YachtCategory category, + public async UniTask ScoreCategoryAsync(IReadOnlyList dice, CategoryDefinitionSO category, int currentRoll, int currentTurn, int playerCurrency) { if (usedCategories.Contains(category)) - throw new InvalidOperationException($"Category {category} has already been scored."); + throw new InvalidOperationException($"Category {category.DisplayName} has already been scored."); - int baseScore = CategoryScorer.Calculate(diceValues, category); + int baseScore = category.Calculate(dice); ModifierContext context; if (eventBus != null && modifierRegistry != null) { context = ModifierContext.CreateForScoring( - baseScore, diceValues, category, + baseScore, dice, category, currentRoll, currentTurn, playerCurrency, modifierRegistry.Active); @@ -92,7 +98,8 @@ namespace YachtDice.Scoring context = new ModifierContext { BaseScore = baseScore, - DiceValues = diceValues, + Dice = dice, + DiceValues = DiceCheckUtility.ExtractValues(dice), Category = category, }; } @@ -111,18 +118,18 @@ namespace YachtDice.Scoring return result; } - public ScoreResult ScoreCategory(int[] diceValues, YachtCategory category) + public ScoreResult ScoreCategory(IReadOnlyList dice, CategoryDefinitionSO category) { if (usedCategories.Contains(category)) - throw new InvalidOperationException($"Category {category} has already been scored."); + throw new InvalidOperationException($"Category {category.DisplayName} has already been scored."); - int baseScore = CategoryScorer.Calculate(diceValues, category); + int baseScore = category.Calculate(dice); ModifierContext context = null; if (eventBus != null && modifierRegistry != null) { context = ModifierContext.CreateForScoring( - baseScore, diceValues, category, + baseScore, dice, category, 0, 0, 0, modifierRegistry.Active); @@ -133,7 +140,7 @@ namespace YachtDice.Scoring if (context != null) result = context.ToScoreResult(); else - result = ScoreResult.Create(baseScore, diceValues, category); + result = ScoreResult.Create(baseScore, dice, category); int finalScore = result.FinalScore; scorecard[category] = finalScore; diff --git a/Assets/Scripts/Scoring/YachtCategory.cs b/Assets/Scripts/Scoring/YachtCategory.cs deleted file mode 100644 index 9f7bacf..0000000 --- a/Assets/Scripts/Scoring/YachtCategory.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace YachtDice.Scoring -{ - public enum YachtCategory - { - // Upper Section - Ones, - Twos, - Threes, - Fours, - Fives, - Sixes, - - // Lower Section - ThreeOfAKind, - FourOfAKind, - FullHouse, - SmallStraight, - LargeStraight, - Yacht, - Chance - } -} diff --git a/Assets/Scripts/Scoring/YachtCategory.cs.meta b/Assets/Scripts/Scoring/YachtCategory.cs.meta deleted file mode 100644 index 7160118..0000000 --- a/Assets/Scripts/Scoring/YachtCategory.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: b90683b5d8ef0d24c8c050d7fd0eb75d \ No newline at end of file diff --git a/Assets/Scripts/Tests/Editor/ModifierEffectTests.cs b/Assets/Scripts/Tests/Editor/ModifierEffectTests.cs index c09ae42..e43e949 100644 --- a/Assets/Scripts/Tests/Editor/ModifierEffectTests.cs +++ b/Assets/Scripts/Tests/Editor/ModifierEffectTests.cs @@ -1,5 +1,6 @@ using NUnit.Framework; using UnityEngine; +using YachtDice.Categories; using YachtDice.Modifiers.Core; using YachtDice.Modifiers.Definition; using YachtDice.Modifiers.Effects; @@ -10,19 +11,33 @@ namespace YachtDice.Tests { public class ModifierEffectTests { + private CategoryDefinitionSO testCategory; + + [SetUp] + public void SetUp() + { + testCategory = SumAllCategorySO.CreateForTest("chance", "Шанс"); + } + + [TearDown] + public void TearDown() + { + Object.DestroyImmediate(testCategory); + } + private ModifierInstance CreateInstance(string id = "test") { var def = ModifierDefinitionSO.CreateForTest(id, null); return new ModifierInstance(def); } - private ModifierContext CreateContext(int baseScore, int[] dice, YachtCategory category) + private ModifierContext CreateContext(int baseScore, int[] dice) { return new ModifierContext { BaseScore = baseScore, DiceValues = dice, - Category = category, + Category = testCategory, }; } @@ -32,7 +47,7 @@ namespace YachtDice.Tests public void AddPerDieEffect_CountsMatchingDice() { var effect = AddPerDieEffect.CreateForTest(10, targetDieValue: 1); - var ctx = CreateContext(5, new[] { 1, 1, 3, 4, 1 }, YachtCategory.Ones); + var ctx = CreateContext(5, new[] { 1, 1, 3, 4, 1 }); var inst = CreateInstance(); effect.Apply(ctx, inst).GetAwaiter().GetResult(); @@ -44,7 +59,7 @@ namespace YachtDice.Tests public void AddPerDieEffect_ZeroTarget_CountsAllDice() { var effect = AddPerDieEffect.CreateForTest(2, targetDieValue: 0); - var ctx = CreateContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); + var ctx = CreateContext(10, new[] { 1, 2, 3, 4, 5 }); var inst = CreateInstance(); effect.Apply(ctx, inst).GetAwaiter().GetResult(); @@ -56,7 +71,7 @@ namespace YachtDice.Tests public void AddPerDieEffect_NoMatches_ZeroBonus() { var effect = AddPerDieEffect.CreateForTest(10, targetDieValue: 6); - var ctx = CreateContext(5, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); + var ctx = CreateContext(5, new[] { 1, 2, 3, 4, 5 }); var inst = CreateInstance(); effect.Apply(ctx, inst).GetAwaiter().GetResult(); @@ -68,7 +83,7 @@ namespace YachtDice.Tests public void AddPerDieEffect_ScalesWithStacks() { var effect = AddPerDieEffect.CreateForTest(10, targetDieValue: 1); - var ctx = CreateContext(5, new[] { 1, 1, 3, 4, 1 }, YachtCategory.Ones); + var ctx = CreateContext(5, new[] { 1, 1, 3, 4, 1 }); var inst = CreateInstance(); inst.Stacks = 2; @@ -83,7 +98,7 @@ namespace YachtDice.Tests public void AddFlatScoreEffect_AddsFlat() { var effect = AddFlatScoreEffect.CreateForTest(15); - var ctx = CreateContext(25, new[] { 3, 3, 2, 2, 2 }, YachtCategory.FullHouse); + var ctx = CreateContext(25, new[] { 3, 3, 2, 2, 2 }); var inst = CreateInstance(); effect.Apply(ctx, inst).GetAwaiter().GetResult(); @@ -95,7 +110,7 @@ namespace YachtDice.Tests public void AddFlatScoreEffect_ScalesWithStacks() { var effect = AddFlatScoreEffect.CreateForTest(15); - var ctx = CreateContext(25, new[] { 3, 3, 2, 2, 2 }, YachtCategory.FullHouse); + var ctx = CreateContext(25, new[] { 3, 3, 2, 2, 2 }); var inst = CreateInstance(); inst.Stacks = 3; @@ -110,7 +125,7 @@ namespace YachtDice.Tests public void MultiplyPerDieEffect_MultipliesPerMatch() { var effect = MultiplyPerDieEffect.CreateForTest(2f, targetDieValue: 6); - var ctx = CreateContext(18, new[] { 6, 6, 6, 1, 2 }, YachtCategory.Sixes); + var ctx = CreateContext(18, new[] { 6, 6, 6, 1, 2 }); var inst = CreateInstance(); effect.Apply(ctx, inst).GetAwaiter().GetResult(); @@ -122,7 +137,7 @@ namespace YachtDice.Tests public void MultiplyPerDieEffect_NoMatches_MultiplierUnchanged() { var effect = MultiplyPerDieEffect.CreateForTest(3f, targetDieValue: 6); - var ctx = CreateContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); + var ctx = CreateContext(10, new[] { 1, 2, 3, 4, 5 }); var inst = CreateInstance(); effect.Apply(ctx, inst).GetAwaiter().GetResult(); @@ -136,7 +151,7 @@ namespace YachtDice.Tests public void MultiplyScoreEffect_MultipliesOnce() { var effect = MultiplyScoreEffect.CreateForTest(1.5f); - var ctx = CreateContext(50, new[] { 6, 6, 6, 6, 6 }, YachtCategory.Yacht); + var ctx = CreateContext(50, new[] { 6, 6, 6, 6, 6 }); var inst = CreateInstance(); effect.Apply(ctx, inst).GetAwaiter().GetResult(); @@ -148,7 +163,7 @@ namespace YachtDice.Tests public void MultiplyScoreEffect_ScalesWithStacks() { var effect = MultiplyScoreEffect.CreateForTest(2f); - var ctx = CreateContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); + var ctx = CreateContext(10, new[] { 1, 2, 3, 4, 5 }); var inst = CreateInstance(); inst.Stacks = 3; @@ -164,7 +179,7 @@ namespace YachtDice.Tests public void PostMultiplyEffect_MultipliesPostMultiplier() { var effect = PostMultiplyEffect.CreateForTest(2f); - var ctx = CreateContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); + var ctx = CreateContext(10, new[] { 1, 2, 3, 4, 5 }); var inst = CreateInstance(); effect.Apply(ctx, inst).GetAwaiter().GetResult(); @@ -178,7 +193,7 @@ namespace YachtDice.Tests public void AddCurrencyEffect_AddsToCurrencyDelta() { var effect = AddCurrencyEffect.CreateForTest(25); - var ctx = CreateContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); + var ctx = CreateContext(10, new[] { 1, 2, 3, 4, 5 }); var inst = CreateInstance(); effect.Apply(ctx, inst).GetAwaiter().GetResult(); @@ -190,7 +205,7 @@ namespace YachtDice.Tests public void AddCurrencyEffect_ScalesWithStacks() { var effect = AddCurrencyEffect.CreateForTest(25); - var ctx = CreateContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); + var ctx = CreateContext(10, new[] { 1, 2, 3, 4, 5 }); var inst = CreateInstance(); inst.Stacks = 2; @@ -205,7 +220,7 @@ namespace YachtDice.Tests public void ConsumeChargeEffect_DecrementsRemainingUses() { var effect = ConsumeChargeEffect.CreateForTest(1); - var ctx = CreateContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); + var ctx = CreateContext(10, new[] { 1, 2, 3, 4, 5 }); var def = ModifierDefinitionSO.CreateForTest("limited", null, hasLimitedUses: true, maxUses: 3); var inst = new ModifierInstance(def); @@ -219,7 +234,7 @@ namespace YachtDice.Tests public void ConsumeChargeEffect_IgnoresPermanent() { var effect = ConsumeChargeEffect.CreateForTest(1); - var ctx = CreateContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); + var ctx = CreateContext(10, new[] { 1, 2, 3, 4, 5 }); var inst = CreateInstance(); // permanent (no limited uses) effect.Apply(ctx, inst).GetAwaiter().GetResult(); @@ -232,7 +247,7 @@ namespace YachtDice.Tests [Test] public void FinalScore_CombinesBaseAndFlatAndMultiplier() { - var ctx = CreateContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); + var ctx = CreateContext(10, new[] { 1, 2, 3, 4, 5 }); ctx.FlatBonus = 5; ctx.Multiplier = 2f; ctx.PostMultiplier = 1.5f; diff --git a/Assets/Scripts/Tests/Editor/ModifierPipelineTests.cs b/Assets/Scripts/Tests/Editor/ModifierPipelineTests.cs index 4d8fb74..aff9dd4 100644 --- a/Assets/Scripts/Tests/Editor/ModifierPipelineTests.cs +++ b/Assets/Scripts/Tests/Editor/ModifierPipelineTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using NUnit.Framework; using UnityEngine; +using YachtDice.Categories; using YachtDice.Modifiers.Conditions; using YachtDice.Modifiers.Core; using YachtDice.Modifiers.Definition; @@ -16,12 +17,38 @@ namespace YachtDice.Tests private ModifierRegistry registry; private ModifierPipeline pipeline; + // Тестовые категории + private CategoryDefinitionSO chanceCategory; + private CategoryDefinitionSO fullHouseCategory; + private CategoryDefinitionSO onesCategory; + private CategoryDefinitionSO threesCategory; + private CategoryDefinitionSO foursCategory; + private CategoryDefinitionSO sixesCategory; + [SetUp] public void SetUp() { registry = new ModifierRegistry(10); pipeline = new ModifierPipeline(registry); - pipeline.TracingEnabled = false; // disable debug logs during tests + pipeline.TracingEnabled = false; + + chanceCategory = SumAllCategorySO.CreateForTest("chance", "Шанс"); + fullHouseCategory = FullHouseCategorySO.CreateForTest("full_house", "Фулл-хаус"); + onesCategory = SumOfValueCategorySO.CreateForTest("ones", "Единицы", 1); + threesCategory = SumOfValueCategorySO.CreateForTest("threes", "Тройки", 3); + foursCategory = SumOfValueCategorySO.CreateForTest("fours", "Четвёрки", 4); + sixesCategory = SumOfValueCategorySO.CreateForTest("sixes", "Шестёрки", 6); + } + + [TearDown] + public void TearDown() + { + Object.DestroyImmediate(chanceCategory); + Object.DestroyImmediate(fullHouseCategory); + Object.DestroyImmediate(onesCategory); + Object.DestroyImmediate(threesCategory); + Object.DestroyImmediate(foursCategory); + Object.DestroyImmediate(sixesCategory); } private ModifierDefinitionSO CreateDef(string id, @@ -40,7 +67,7 @@ namespace YachtDice.Tests registry.TryActivate(inst); } - private ModifierContext CreateScoringContext(int baseScore, int[] dice, YachtCategory category) + private ModifierContext CreateScoringContext(int baseScore, int[] dice, CategoryDefinitionSO category) { return new ModifierContext { @@ -64,10 +91,10 @@ namespace YachtDice.Tests var mulDef = CreateDef("mul", TriggerType.OnCategoryScored, null, new List { mulEffect }); - RegisterAndActivate(mulDef); // registered first, but multiplicative phase - RegisterAndActivate(addDef); // registered second, but additive phase + RegisterAndActivate(mulDef); + RegisterAndActivate(addDef); - var ctx = CreateScoringContext(20, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); + var ctx = CreateScoringContext(20, new[] { 1, 2, 3, 4, 5 }, chanceCategory); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); // (20 + 10) * 2 = 60 @@ -88,7 +115,7 @@ namespace YachtDice.Tests RegisterAndActivate(postDef); RegisterAndActivate(mulDef); - var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); + var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, chanceCategory); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); // (10 + 0) * 2 * 3 = 60 @@ -100,7 +127,7 @@ namespace YachtDice.Tests [Test] public void Execute_ConditionFails_SkipsEffect() { - var condition = CategoryCondition.CreateForTest(YachtCategory.FullHouse); + var condition = CategoryCondition.CreateForTest(fullHouseCategory); var effect = AddFlatScoreEffect.CreateForTest(100); var def = CreateDef("fh-bonus", TriggerType.OnCategoryScored, @@ -110,7 +137,7 @@ namespace YachtDice.Tests RegisterAndActivate(def); // Scoring Ones, not FullHouse — condition should fail - var ctx = CreateScoringContext(5, new[] { 1, 1, 1, 1, 1 }, YachtCategory.Ones); + var ctx = CreateScoringContext(5, new[] { 1, 1, 1, 1, 1 }, onesCategory); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); Assert.AreEqual(0, result.FlatBonus); @@ -120,7 +147,7 @@ namespace YachtDice.Tests [Test] public void Execute_ConditionPasses_AppliesEffect() { - var condition = CategoryCondition.CreateForTest(YachtCategory.FullHouse); + var condition = CategoryCondition.CreateForTest(fullHouseCategory); var effect = AddFlatScoreEffect.CreateForTest(15); var def = CreateDef("fh-bonus", TriggerType.OnCategoryScored, @@ -129,7 +156,7 @@ namespace YachtDice.Tests RegisterAndActivate(def); - var ctx = CreateScoringContext(25, new[] { 3, 3, 3, 2, 2 }, YachtCategory.FullHouse); + var ctx = CreateScoringContext(25, new[] { 3, 3, 3, 2, 2 }, fullHouseCategory); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); Assert.AreEqual(15, result.FlatBonus); @@ -147,8 +174,7 @@ namespace YachtDice.Tests RegisterAndActivate(def); - // Fire OnCategoryScored, not OnTurnStart - var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); + var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, chanceCategory); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); Assert.AreEqual(0, result.FlatBonus); @@ -180,12 +206,9 @@ namespace YachtDice.Tests RegisterAndActivate(def1); // dice: [3, 3, 3, 1, 2] — 3 threes - var ctx = CreateScoringContext(9, new[] { 3, 3, 3, 1, 2 }, YachtCategory.Threes); + var ctx = CreateScoringContext(9, new[] { 3, 3, 3, 1, 2 }, threesCategory); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); - // Additive phase: perDieAdd (+2*3=6) + flatAdd (+10) → FlatBonus = 16 - // Multiplicative phase: perDieMul (1.5^3=3.375) then finalMul (*2) → Multiplier = 6.75 - // FinalScore = floor((9 + 16) * 6.75) = floor(168.75) = 168 Assert.AreEqual(16, result.FlatBonus); Assert.AreEqual(168, result.FinalScore); } @@ -195,7 +218,7 @@ namespace YachtDice.Tests [Test] public void Execute_NoActiveModifiers_NoChange() { - var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); + var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, chanceCategory); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); Assert.AreEqual(10, result.FinalScore); @@ -210,10 +233,9 @@ namespace YachtDice.Tests var def = CreateDef("inactive", TriggerType.OnCategoryScored, null, new List { effect }); - // Add but don't activate registry.Add(def); - var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); + var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, chanceCategory); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); Assert.AreEqual(0, result.FlatBonus); @@ -233,7 +255,7 @@ namespace YachtDice.Tests RegisterAndActivate(def); - var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); + var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, chanceCategory); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); Assert.AreEqual(10, result.FlatBonus); @@ -254,7 +276,7 @@ namespace YachtDice.Tests RegisterAndActivate(def); - var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); + var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, chanceCategory); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); Assert.IsNotNull(result.DebugLog); @@ -276,13 +298,13 @@ namespace YachtDice.Tests RegisterAndActivate(def); // Only 2 sixes — condition requires 3 - var ctx = CreateScoringContext(12, new[] { 6, 6, 1, 2, 3 }, YachtCategory.Sixes); + var ctx = CreateScoringContext(12, new[] { 6, 6, 1, 2, 3 }, sixesCategory); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); Assert.AreEqual(0, result.FlatBonus); // 3 sixes — condition passes - var ctx2 = CreateScoringContext(18, new[] { 6, 6, 6, 1, 2 }, YachtCategory.Sixes); + var ctx2 = CreateScoringContext(18, new[] { 6, 6, 6, 1, 2 }, sixesCategory); var result2 = pipeline.Execute(TriggerType.OnCategoryScored, ctx2).GetAwaiter().GetResult(); Assert.AreEqual(100, result2.FlatBonus); @@ -303,13 +325,13 @@ namespace YachtDice.Tests RegisterAndActivate(def); // Below threshold - var ctx = CreateScoringContext(15, new[] { 3, 3, 3, 3, 3 }, YachtCategory.Threes); + var ctx = CreateScoringContext(15, new[] { 3, 3, 3, 3, 3 }, threesCategory); var result = pipeline.Execute(TriggerType.OnCategoryScored, ctx).GetAwaiter().GetResult(); Assert.AreEqual(1f, result.Multiplier); // At threshold - var ctx2 = CreateScoringContext(20, new[] { 4, 4, 4, 4, 4 }, YachtCategory.Fours); + var ctx2 = CreateScoringContext(20, new[] { 4, 4, 4, 4, 4 }, foursCategory); var result2 = pipeline.Execute(TriggerType.OnCategoryScored, ctx2).GetAwaiter().GetResult(); Assert.AreEqual(2f, result2.Multiplier); @@ -320,7 +342,7 @@ namespace YachtDice.Tests [Test] public void ToScoreResult_ConvertsCorrectly() { - var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); + var ctx = CreateScoringContext(10, new[] { 1, 2, 3, 4, 5 }, chanceCategory); ctx.FlatBonus = 5; ctx.Multiplier = 2f; ctx.PostMultiplier = 1.5f; @@ -330,7 +352,7 @@ namespace YachtDice.Tests Assert.AreEqual(10, sr.BaseScore); Assert.AreEqual(5, sr.FlatBonus); Assert.AreEqual(3f, sr.Multiplier, 0.001f); // 2 * 1.5 - Assert.AreEqual(YachtCategory.Chance, sr.Category); + Assert.AreEqual(chanceCategory, sr.Category); } } } diff --git a/Assets/Scripts/Tests/Editor/ScoringSystemTests.cs b/Assets/Scripts/Tests/Editor/ScoringSystemTests.cs index 661716a..55cdf8b 100644 --- a/Assets/Scripts/Tests/Editor/ScoringSystemTests.cs +++ b/Assets/Scripts/Tests/Editor/ScoringSystemTests.cs @@ -1,15 +1,35 @@ +using System.Collections.Generic; using NUnit.Framework; using UnityEngine; +using YachtDice.Categories; +using YachtDice.Dice; using YachtDice.Scoring; namespace YachtDice.Tests { public class ScoringSystemTests { - private ScoringSystem CreateScoringSystem() + private CategoryDefinitionSO yachtCategory; + private CategoryDefinitionSO onesCategory; + private CategoryDefinitionSO twosCategory; + private CategoryDefinitionSO chanceCategory; + private CategoryCatalogSO catalog; + private DieDefinitionSO standardDie; + + [SetUp] + public void SetUp() { - var go = new GameObject("ScoringSystem"); - return go.AddComponent(); + standardDie = DieDefinitionSO.CreateForTest("d6", "d6"); + + yachtCategory = NOfAKindCategorySO.CreateForTest("yacht", "Яхта", 5, fixedScoreMode: true, score: 50); + onesCategory = SumOfValueCategorySO.CreateForTest("ones", "Единицы", 1); + twosCategory = SumOfValueCategorySO.CreateForTest("twos", "Двойки", 2); + chanceCategory = SumAllCategorySO.CreateForTest("chance", "Шанс"); + + catalog = CategoryCatalogSO.CreateForTest(new List + { + onesCategory, twosCategory, yachtCategory, chanceCategory + }); } [TearDown] @@ -17,13 +37,35 @@ namespace YachtDice.Tests { foreach (var go in Object.FindObjectsByType(FindObjectsSortMode.None)) Object.DestroyImmediate(go.gameObject); + + Object.DestroyImmediate(yachtCategory); + Object.DestroyImmediate(onesCategory); + Object.DestroyImmediate(twosCategory); + Object.DestroyImmediate(chanceCategory); + Object.DestroyImmediate(catalog); + Object.DestroyImmediate(standardDie); + } + + private ScoringSystem CreateScoringSystem() + { + var go = new GameObject("ScoringSystem"); + return go.AddComponent(); + } + + private IReadOnlyList CreateDice(params int[] values) + { + var dice = new DieInstance[values.Length]; + for (int i = 0; i < values.Length; i++) + dice[i] = new DieInstance(standardDie, values[i]); + return dice; } [Test] public void ScoreCategory_WithNoModifiers_CalculatesBaseOnly() { var system = CreateScoringSystem(); - var result = system.ScoreCategory(new[] { 6, 6, 6, 6, 6 }, YachtCategory.Yacht); + var dice = CreateDice(6, 6, 6, 6, 6); + var result = system.ScoreCategory(dice, yachtCategory); Assert.AreEqual(50, result.BaseScore); Assert.AreEqual(0, result.FlatBonus); @@ -35,7 +77,7 @@ namespace YachtDice.Tests public void ScoreCategory_FiresOnCategoryConfirmed() { var system = CreateScoringSystem(); - YachtCategory firedCategory = (YachtCategory)(-1); + CategoryDefinitionSO firedCategory = null; ScoreResult firedResult = default; system.OnCategoryConfirmed += (cat, res) => @@ -44,9 +86,10 @@ namespace YachtDice.Tests firedResult = res; }; - system.ScoreCategory(new[] { 1, 1, 1, 1, 1 }, YachtCategory.Ones); + var dice = CreateDice(1, 1, 1, 1, 1); + system.ScoreCategory(dice, onesCategory); - Assert.AreEqual(YachtCategory.Ones, firedCategory); + Assert.AreEqual(onesCategory, firedCategory); Assert.AreEqual(5, firedResult.BaseScore); } @@ -54,17 +97,19 @@ namespace YachtDice.Tests public void ScoreCategory_PreventsDuplicateCategory() { var system = CreateScoringSystem(); - system.ScoreCategory(new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); + var dice = CreateDice(1, 2, 3, 4, 5); + system.ScoreCategory(dice, chanceCategory); Assert.Throws(() => - system.ScoreCategory(new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance)); + system.ScoreCategory(dice, chanceCategory)); } [Test] public void PreviewScore_WithNoModifiers_CalculatesBaseOnly() { var system = CreateScoringSystem(); - var result = system.PreviewScore(new[] { 1, 2, 3, 4, 5 }, YachtCategory.Chance); + var dice = CreateDice(1, 2, 3, 4, 5); + var result = system.PreviewScore(dice, chanceCategory); Assert.AreEqual(15, result.BaseScore); Assert.AreEqual(0, result.FlatBonus); @@ -75,8 +120,8 @@ namespace YachtDice.Tests public void TotalScore_SumsAllScoredCategories() { var system = CreateScoringSystem(); - system.ScoreCategory(new[] { 1, 1, 1, 1, 1 }, YachtCategory.Ones); - system.ScoreCategory(new[] { 2, 2, 2, 2, 2 }, YachtCategory.Twos); + system.ScoreCategory(CreateDice(1, 1, 1, 1, 1), onesCategory); + system.ScoreCategory(CreateDice(2, 2, 2, 2, 2), twosCategory); Assert.AreEqual(15, system.TotalScore); // 5 + 10 } @@ -85,12 +130,84 @@ namespace YachtDice.Tests public void ResetScorecard_ClearsAll() { var system = CreateScoringSystem(); - system.ScoreCategory(new[] { 1, 1, 1, 1, 1 }, YachtCategory.Ones); + system.ScoreCategory(CreateDice(1, 1, 1, 1, 1), onesCategory); system.ResetScorecard(); Assert.AreEqual(0, system.TotalScore); - Assert.IsFalse(system.IsCategoryUsed(YachtCategory.Ones)); + Assert.IsFalse(system.IsCategoryUsed(onesCategory)); + } + + // ── Category SO Unit Tests ────────────────────────────────── + + [Test] + public void SumOfValueCategory_SumsCorrectly() + { + var dice = CreateDice(3, 3, 3, 1, 2); + var cat = SumOfValueCategorySO.CreateForTest("threes", "Тройки", 3); + + Assert.AreEqual(9, cat.Calculate(dice)); + + Object.DestroyImmediate(cat); + } + + [Test] + public void NOfAKindCategory_ThreeOfAKind_ReturnsSumOrZero() + { + var cat = NOfAKindCategorySO.CreateForTest("three_of_a_kind", "Тройка", 3); + + Assert.AreEqual(17, cat.Calculate(CreateDice(4, 4, 4, 3, 2))); // sum = 17 + Assert.AreEqual(0, cat.Calculate(CreateDice(1, 2, 3, 4, 5))); // no 3-of-a-kind + + Object.DestroyImmediate(cat); + } + + [Test] + public void NOfAKindCategory_Yacht_ReturnsFixedScore() + { + Assert.AreEqual(50, yachtCategory.Calculate(CreateDice(6, 6, 6, 6, 6))); + Assert.AreEqual(0, yachtCategory.Calculate(CreateDice(6, 6, 6, 6, 1))); + } + + [Test] + public void FullHouseCategory_CalculatesCorrectly() + { + var cat = FullHouseCategorySO.CreateForTest("fh", "Фулл-хаус", 25); + + Assert.AreEqual(25, cat.Calculate(CreateDice(3, 3, 3, 2, 2))); + Assert.AreEqual(0, cat.Calculate(CreateDice(3, 3, 3, 3, 2))); + + Object.DestroyImmediate(cat); + } + + [Test] + public void StraightCategory_SmallStraight() + { + var cat = StraightCategorySO.CreateForTest("ss", "Малый стрит", 4, 30); + + Assert.AreEqual(30, cat.Calculate(CreateDice(1, 2, 3, 4, 6))); + Assert.AreEqual(0, cat.Calculate(CreateDice(1, 2, 3, 5, 6))); + + Object.DestroyImmediate(cat); + } + + [Test] + public void StraightCategory_LargeStraight() + { + var cat = StraightCategorySO.CreateForTest("ls", "Большой стрит", 5, 40); + + Assert.AreEqual(40, cat.Calculate(CreateDice(1, 2, 3, 4, 5))); + Assert.AreEqual(40, cat.Calculate(CreateDice(2, 3, 4, 5, 6))); + Assert.AreEqual(0, cat.Calculate(CreateDice(1, 2, 3, 4, 6))); + + Object.DestroyImmediate(cat); + } + + [Test] + public void SumAllCategory_SumsEverything() + { + Assert.AreEqual(15, chanceCategory.Calculate(CreateDice(1, 2, 3, 4, 5))); + Assert.AreEqual(30, chanceCategory.Calculate(CreateDice(6, 6, 6, 6, 6))); } } } diff --git a/Assets/Scripts/UI/CategoryRowView.cs b/Assets/Scripts/UI/CategoryRowView.cs index f2de289..8f3d6f7 100644 --- a/Assets/Scripts/UI/CategoryRowView.cs +++ b/Assets/Scripts/UI/CategoryRowView.cs @@ -2,7 +2,7 @@ using System; using UnityEngine; using UnityEngine.UI; using TMPro; -using YachtDice.Scoring; +using YachtDice.Categories; namespace YachtDice.UI { @@ -21,16 +21,16 @@ namespace YachtDice.UI [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 CategoryDefinitionSO category; private bool isUsed; - public event Action OnCategorySelected; + public event Action OnCategorySelected; - public void Initialize(YachtCategory cat, string displayName) + public void Initialize(CategoryDefinitionSO categoryDef) { - category = cat; + category = categoryDef; isUsed = false; - categoryNameText.text = displayName; + categoryNameText.text = categoryDef.DisplayName; previewText.text = ""; recordedScoreText.text = "-"; selectButton.onClick.AddListener(HandleClick); diff --git a/Assets/Scripts/UI/GameController.cs b/Assets/Scripts/UI/GameController.cs index 1911c37..8b57dcb 100644 --- a/Assets/Scripts/UI/GameController.cs +++ b/Assets/Scripts/UI/GameController.cs @@ -1,7 +1,8 @@ -using System; using System.Collections.Generic; using UnityEngine; using VContainer; +using YachtDice.Categories; +using YachtDice.Dice; using YachtDice.Game; using YachtDice.Scoring; using YachtDice.Economy; @@ -34,33 +35,25 @@ namespace YachtDice.UI [SerializeField] private int maxRollsPerTurn = 3; [SerializeField] private int maxActiveModifierSlots = 5; - 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; - private ModifierRegistry modifierRegistry; + private CategoryCatalogSO categoryCatalog; private InventoryModel inventoryModel; private ShopModel shopModel; [Inject] - public void Construct(ModifierRegistry modifierRegistry) + public void Construct(ModifierRegistry modifierRegistry, CategoryCatalogSO categoryCatalog) { this.modifierRegistry = modifierRegistry; + this.categoryCatalog = categoryCatalog; } // ── Lifecycle ────────────────────────────────────────────── private void Awake() { - totalCategoryCount = Enum.GetValues(typeof(YachtCategory)).Length; - // Model → Controller gameManager.OnTurnStarted += HandleTurnStarted; gameManager.OnRollComplete += HandleRollComplete; @@ -83,6 +76,9 @@ namespace YachtDice.UI private void Start() { + // Инициализируем скоркарту из каталога категорий + scoreCardView.Initialize(categoryCatalog); + InitializeModifierSystems(); } @@ -197,6 +193,7 @@ namespace YachtDice.UI private void HandleTurnStarted(int turn) { + int totalCategoryCount = categoryCatalog.Count; gameInfoView.SetTurnText(turn, totalCategoryCount); dicePanelView.ResetForNewTurn(); dicePanelView.SetRollButtonState(true, 0, maxRollsPerTurn); @@ -212,7 +209,7 @@ namespace YachtDice.UI int[] values = diceManager.GetCurrentValues(); dicePanelView.SetAllDiceValues(values); - UpdatePreviewScores(values); + UpdatePreviewScores(); } private void HandleDieSettled(int index, int value) @@ -220,7 +217,7 @@ namespace YachtDice.UI dicePanelView.SetDieValue(index, value); } - private void HandleScored(YachtCategory category, int finalScore) + private void HandleScored(CategoryDefinitionSO category, int finalScore) { scoreCardView.SetCategoryScored(category, finalScore); UpdateTotalDisplay(); @@ -262,7 +259,7 @@ namespace YachtDice.UI dicePanelView.SetDieLocked(index, isLocked); } - private void HandleCategorySelected(YachtCategory category) + private void HandleCategorySelected(CategoryDefinitionSO category) { if (!gameManager.CanScore) return; if (scoringSystem.IsCategoryUsed(category)) return; @@ -317,17 +314,19 @@ namespace YachtDice.UI // ── Helpers ──────────────────────────────────────────────── - private void UpdatePreviewScores(int[] diceValues) + private void UpdatePreviewScores() { - var previews = new Dictionary(); - var categories = (YachtCategory[])Enum.GetValues(typeof(YachtCategory)); + var dice = diceManager.GetDice(); + var previews = new Dictionary(); + var allCategories = categoryCatalog.All; - for (int i = 0; i < categories.Length; i++) + for (int i = 0; i < allCategories.Count; i++) { - if (scoringSystem.IsCategoryUsed(categories[i])) continue; + var cat = allCategories[i]; + if (scoringSystem.IsCategoryUsed(cat)) continue; - ScoreResult result = scoringSystem.PreviewScore(diceValues, categories[i]); - previews[categories[i]] = result.FinalScore; + ScoreResult result = scoringSystem.PreviewScore(dice, cat); + previews[cat] = result.FinalScore; } scoreCardView.UpdatePreviews(previews); @@ -335,29 +334,33 @@ namespace YachtDice.UI 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; - } - + int upperSum = CalculateUpperSum(); bool hasBonus = upperSum >= UpperBonusThreshold; int displayTotal = CalculateDisplayTotal(); scoreCardView.UpdateTotalDisplay(displayTotal, upperSum, hasBonus); } + private int CalculateUpperSum() + { + int upperSum = 0; + var allCategories = categoryCatalog.All; + + for (int i = 0; i < allCategories.Count; i++) + { + if (!allCategories[i].IsUpperSection) continue; + + int catScore = scoringSystem.GetCategoryScore(allCategories[i]); + if (catScore >= 0) upperSum += catScore; + } + + return upperSum; + } + 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; - } + int upperSum = CalculateUpperSum(); if (upperSum >= UpperBonusThreshold) total += UpperBonusValue; diff --git a/Assets/Scripts/UI/ScoreCardView.cs b/Assets/Scripts/UI/ScoreCardView.cs index d089304..fbfb584 100644 --- a/Assets/Scripts/UI/ScoreCardView.cs +++ b/Assets/Scripts/UI/ScoreCardView.cs @@ -2,13 +2,13 @@ using System; using System.Collections.Generic; using UnityEngine; using TMPro; -using YachtDice.Scoring; +using YachtDice.Categories; namespace YachtDice.UI { public class ScoreCardView : MonoBehaviour { - [Header("Category Rows (in YachtCategory enum order)")] + [Header("Category Rows (порядок соответствует каталогу)")] [SerializeField] private List categoryRows = new(); [Header("Summary")] @@ -16,48 +16,41 @@ namespace YachtDice.UI [SerializeField] private TMP_Text upperBonusText; [SerializeField] private TMP_Text totalScoreText; - public event Action OnCategorySelected; + public event Action OnCategorySelected; - private static readonly string[] CategoryNames = + private CategoryCatalogSO catalog; + private Dictionary categoryToRowIndex; + + /// + /// Инициализирует скоркарту из каталога категорий. + /// Вызывается из GameController после DI. + /// + public void Initialize(CategoryCatalogSO categoryCatalog) { - "Единицы", - "Двойки", - "Тройки", - "Четвёрки", - "Пятёрки", - "Шестёрки", - "Тройка", - "Каре", - "Фулл-хаус", - "Малый стрит", - "Большой стрит", - "Яхта", - "Шанс" - }; + catalog = categoryCatalog; + categoryToRowIndex = new Dictionary(); - private YachtCategory[] _allCategories; + var all = catalog.All; + int count = Mathf.Min(categoryRows.Count, all.Count); - private void Awake() - { - _allCategories = (YachtCategory[])Enum.GetValues(typeof(YachtCategory)); - - for (int i = 0; i < categoryRows.Count && i < _allCategories.Length; i++) + for (int i = 0; i < count; i++) { - categoryRows[i].Initialize(_allCategories[i], CategoryNames[i]); + categoryRows[i].Initialize(all[i]); categoryRows[i].OnCategorySelected += HandleCategorySelected; + categoryToRowIndex[all[i]] = i; } UpdateTotalDisplay(0, 0, false); } - public void UpdatePreviews(Dictionary previews) + public void UpdatePreviews(Dictionary previews) { - for (int i = 0; i < categoryRows.Count && i < _allCategories.Length; i++) + foreach (var kvp in previews) { - if (previews.TryGetValue(_allCategories[i], out int preview)) + if (categoryToRowIndex.TryGetValue(kvp.Key, out int rowIndex)) { - categoryRows[i].ShowPreview(preview); - categoryRows[i].SetInteractable(true); + categoryRows[rowIndex].ShowPreview(kvp.Value); + categoryRows[rowIndex].SetInteractable(true); } } } @@ -71,10 +64,9 @@ namespace YachtDice.UI } } - public void SetCategoryScored(YachtCategory category, int score) + public void SetCategoryScored(CategoryDefinitionSO category, int score) { - int index = (int)category; - if (index >= 0 && index < categoryRows.Count) + if (categoryToRowIndex.TryGetValue(category, out int index)) categoryRows[index].SetRecordedScore(score); } @@ -99,7 +91,7 @@ namespace YachtDice.UI UpdateTotalDisplay(0, 0, false); } - private void HandleCategorySelected(YachtCategory category) + private void HandleCategorySelected(CategoryDefinitionSO category) { OnCategorySelected?.Invoke(category); }