[Add] GameLoop base

This commit is contained in:
2026-03-18 09:13:48 +07:00
parent c819c0d045
commit 537ae1ce5c
28 changed files with 997 additions and 40 deletions
+7
View File
@@ -11,6 +11,7 @@ using YachtDice.Modifiers.Definition;
using YachtDice.Modifiers.Pipeline;
using YachtDice.Modifiers.Runtime;
using YachtDice.Player;
using YachtDice.Run;
using YachtDice.Scoring;
using YachtDice.Shop;
using YachtDice.UI;
@@ -25,6 +26,7 @@ namespace YachtDice.DI
[SerializeField] private CategoryCatalog categoryCatalog;
[SerializeField] private DiceCatalog diceCatalog;
[SerializeField] private ShopCatalog shopCatalog;
[SerializeField] private RunBalanceConfigSO runBalanceConfig;
[Header("Scene References")]
[SerializeField] private ScoringSystem scoringSystem;
@@ -45,6 +47,7 @@ namespace YachtDice.DI
builder.RegisterInstance(categoryCatalog);
builder.RegisterInstance(diceCatalog);
builder.RegisterInstance(shopCatalog);
builder.RegisterInstance(runBalanceConfig != null ? runBalanceConfig : RunBalanceConfigSO.CreateDefault());
// Core modifier services
builder.Register<ModifierRegistry>(Lifetime.Singleton)
@@ -62,6 +65,10 @@ namespace YachtDice.DI
// Shop
builder.Register<ShopModel>(Lifetime.Singleton);
// Run loop
builder.Register<StoredRollBank>(Lifetime.Singleton);
builder.Register<RunLoopService>(Lifetime.Singleton);
// Presentation services
builder.Register<IGameSaveService, GameSaveService>(Lifetime.Singleton);
builder.Register<IScoreSummaryService, ScoreSummaryService>(Lifetime.Singleton);
+138 -36
View File
@@ -2,58 +2,105 @@ using System;
using UnityEngine;
using VContainer;
using YachtDice.Categories;
using YachtDice.Run;
using YachtDice.Scoring;
namespace YachtDice.Game
{
public class GameLoopController : MonoBehaviour
{
[Header("Settings")]
[SerializeField] private int maxRollsPerTurn = 3;
private DiceManager _diceManager;
private RunLoopService _runLoopService;
private ScoringSystem _scoringSystem;
public int CurrentRoll { get; private set; }
public int CurrentTurn { get; private set; }
public int MaxRollsPerTurn => maxRollsPerTurn;
public int CurrentRoll => _runLoopService != null ? _runLoopService.State.CurrentRoll : 0;
public int CurrentTurn => _runLoopService != null ? _runLoopService.State.StageNumber : 0;
public int CurrentBet => _runLoopService != null ? _runLoopService.State.BetIndex : 0;
public int CurrentStage => _runLoopService != null ? _runLoopService.State.StageNumber : 0;
public int CurrentStageTarget => _runLoopService != null ? _runLoopService.State.CurrentStageTarget : 0;
public int CurrentBaseQuota => _runLoopService != null ? _runLoopService.State.BaseQuota : 0;
public int StoredRolls => _runLoopService != null ? _runLoopService.State.StoredRolls : 0;
public int MaxRollsPerTurn => _runLoopService != null ? _runLoopService.State.CurrentStageRollBudget : 0;
public RunPhase CurrentPhase => _runLoopService != null ? _runLoopService.State.Phase : RunPhase.None;
public bool CanRoll => CurrentRoll < maxRollsPerTurn && !_diceManager.IsAnyRolling;
public bool CanScore => CurrentRoll > 0 && !_diceManager.IsAnyRolling;
public bool IsGameOver => _scoringSystem.IsComplete;
public bool CanRoll => _runLoopService != null && _runLoopService.CanRoll() && !_diceManager.IsAnyRolling;
public bool CanScore => _runLoopService != null && CurrentPhase == RunPhase.CategorySelection && !_diceManager.IsAnyRolling;
public bool IsGameOver => _runLoopService != null && _runLoopService.State.IsFailed;
public bool IsShopOpen => CurrentPhase == RunPhase.Shop;
public event Action<int> OnTurnStarted;
public event Action<int> OnRollComplete;
public event Action<CategoryDefinition, int> OnScored;
public event Action<int> OnGameOver;
public event Action<int> OnBetStarted;
public event Action OnShopOpened;
public event Action OnShopClosed;
public event Action<int> OnStoredRollsChanged;
public event Action<int> OnCurrencyChanged;
public event Action<int> OnQuotaChanged;
public event Action<int, int> OnCycleCompleted;
public event Action<RunPhase> OnPhaseChanged;
[Inject]
public void Construct(DiceManager diceManager, ScoringSystem scoringSystem)
public void Construct(DiceManager diceManager, RunLoopService runLoopService, ScoringSystem scoringSystem)
{
this._diceManager = diceManager;
this._scoringSystem = scoringSystem;
_diceManager = diceManager;
_runLoopService = runLoopService;
_scoringSystem = scoringSystem;
_runLoopService.OnStageStarted += HandleStageStarted;
_runLoopService.OnStageCleared += HandleStageCleared;
_runLoopService.OnRunFailed += HandleRunFailed;
_runLoopService.OnBetStarted += HandleBetStarted;
_runLoopService.OnShopOpened += HandleShopOpened;
_runLoopService.OnStoredRollsChanged += HandleStoredRollsChanged;
_runLoopService.OnCurrencyChanged += HandleCurrencyChanged;
_runLoopService.OnQuotaChanged += HandleQuotaChanged;
_runLoopService.OnCycleCompleted += HandleCycleCompleted;
_runLoopService.OnPhaseChanged += HandlePhaseChanged;
}
private void OnDestroy()
{
if (_runLoopService == null)
return;
_runLoopService.OnStageStarted -= HandleStageStarted;
_runLoopService.OnStageCleared -= HandleStageCleared;
_runLoopService.OnRunFailed -= HandleRunFailed;
_runLoopService.OnBetStarted -= HandleBetStarted;
_runLoopService.OnShopOpened -= HandleShopOpened;
_runLoopService.OnStoredRollsChanged -= HandleStoredRollsChanged;
_runLoopService.OnCurrencyChanged -= HandleCurrencyChanged;
_runLoopService.OnQuotaChanged -= HandleQuotaChanged;
_runLoopService.OnCycleCompleted -= HandleCycleCompleted;
_runLoopService.OnPhaseChanged -= HandlePhaseChanged;
}
public void StartNewGame()
{
_scoringSystem.ResetScorecard();
CurrentTurn = 0;
StartNewTurn();
_diceManager.UnlockAll();
_runLoopService.StartNewRun();
}
private void StartNewTurn()
public void CompleteShop()
{
CurrentTurn++;
CurrentRoll = 0;
_diceManager.UnlockAll();
OnTurnStarted?.Invoke(CurrentTurn);
if (!IsShopOpen)
return;
_runLoopService.CompleteShop();
OnShopClosed?.Invoke();
}
public void Roll()
{
if (!CanRoll) return;
if (!CanRoll)
return;
if (!_runLoopService.TryBeginRoll())
return;
CurrentRoll++;
_diceManager.OnAllDiceSettled += HandleAllDiceSettled;
_diceManager.RollUnlocked();
}
@@ -61,6 +108,7 @@ namespace YachtDice.Game
private void HandleAllDiceSettled()
{
_diceManager.OnAllDiceSettled -= HandleAllDiceSettled;
_runLoopService.NotifyRollResolved(_diceManager.GetDice());
OnRollComplete?.Invoke(CurrentRoll);
}
@@ -71,25 +119,79 @@ namespace YachtDice.Game
_diceManager.ToggleLock(index);
}
public void ScoreInCategory(CategoryDefinition category)
public bool CanScoreCategory(CategoryDefinition category)
{
if (!CanScore) return;
if (_scoringSystem.IsCategoryUsed(category)) return;
return _runLoopService != null && _runLoopService.CanScoreCategory(_diceManager.GetDice(), category);
}
var dice = _diceManager.GetDice();
var result = _scoringSystem.ScoreCategory(dice, category);
public ScoreResult PreviewCategory(CategoryDefinition category)
{
return _runLoopService.PreviewScore(_diceManager.GetDice(), category);
}
public async void ScoreInCategory(CategoryDefinition category)
{
if (!CanScore)
return;
await _runLoopService.TryScoreCategoryAsync(_diceManager.GetDice(), category);
}
public bool CanOpenShopManually()
{
return IsShopOpen;
}
private void HandleStageStarted(RunStageState stage)
{
_diceManager.UnlockAll();
OnTurnStarted?.Invoke(stage.Index + 1);
}
private void HandleStageCleared(RunStageState stage, CategoryDefinition category, ScoreResult result)
{
_diceManager.UnlockAll();
OnScored?.Invoke(category, result.FinalScore);
}
if (_scoringSystem.IsComplete)
{
var total = _scoringSystem.TotalScore;
OnGameOver?.Invoke(total);
}
else
{
StartNewTurn();
}
private void HandleRunFailed(RunState state)
{
OnGameOver?.Invoke(_scoringSystem != null ? _scoringSystem.TotalScore : 0);
}
private void HandleBetStarted(RunState state)
{
OnBetStarted?.Invoke(state.BetIndex);
}
private void HandleShopOpened(RunState state)
{
OnShopOpened?.Invoke();
}
private void HandleStoredRollsChanged(int value)
{
OnStoredRollsChanged?.Invoke(value);
}
private void HandleCurrencyChanged(int value)
{
OnCurrencyChanged?.Invoke(value);
}
private void HandleQuotaChanged(int value)
{
OnQuotaChanged?.Invoke(value);
}
private void HandleCycleCompleted(int bonus, int storedRolls)
{
OnCycleCompleted?.Invoke(bonus, storedRolls);
}
private void HandlePhaseChanged(RunPhase phase)
{
OnPhaseChanged?.Invoke(phase);
}
}
}
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a4fce5c4a7443cb418371ed455b52146
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+48
View File
@@ -0,0 +1,48 @@
using UnityEngine;
namespace YachtDice.Run
{
[CreateAssetMenu(fileName = "RunBalanceConfig", menuName = "YachtDice/Run/Balance Config")]
public class RunBalanceConfigSO : ScriptableObject
{
[Header("Quota")]
[SerializeField] private int startingBaseQuota = 30;
[SerializeField] private int quotaGrowthPerCycle = 30;
[Header("Stage Targets")]
[SerializeField] private float[] stageTargetMultipliers = { 1f, 1.5f, 2.5f };
[Header("Rewards")]
[SerializeField] private int stageClearReward = 25;
[SerializeField] private int cycleStoredRollBonusMultiplier = 10;
[Header("Roll Economy")]
[SerializeField] private int baseRollsPerStage = 3;
public int StartingBaseQuota => startingBaseQuota;
public int QuotaGrowthPerCycle => quotaGrowthPerCycle;
public int StageClearReward => stageClearReward;
public int CycleStoredRollBonusMultiplier => cycleStoredRollBonusMultiplier;
public int BaseRollsPerStage => baseRollsPerStage;
public int StageCount => stageTargetMultipliers != null ? stageTargetMultipliers.Length : 0;
public int GetStageTarget(int baseQuota, int stageIndex)
{
if (stageTargetMultipliers == null || stageTargetMultipliers.Length == 0)
return baseQuota;
var clampedIndex = Mathf.Clamp(stageIndex, 0, stageTargetMultipliers.Length - 1);
return Mathf.CeilToInt(baseQuota * stageTargetMultipliers[clampedIndex]);
}
public int GetNextQuota(int currentQuota)
{
return currentQuota + quotaGrowthPerCycle;
}
public static RunBalanceConfigSO CreateDefault()
{
return CreateInstance<RunBalanceConfigSO>();
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5917f01e056f9c24988a1e0e726becc0
+293
View File
@@ -0,0 +1,293 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using YachtDice.Categories;
using YachtDice.Dice;
using YachtDice.Economy;
using YachtDice.Scoring;
namespace YachtDice.Run
{
public sealed class RunLoopService
{
private readonly RunBalanceConfigSO _config;
private readonly ScoringSystem _scoringSystem;
private readonly CurrencyBank _currencyBank;
private readonly StoredRollBank _storedRollBank;
private readonly RunState _state = new();
public RunLoopService(
RunBalanceConfigSO config,
ScoringSystem scoringSystem,
CurrencyBank currencyBank,
StoredRollBank storedRollBank)
{
_config = config != null ? config : RunBalanceConfigSO.CreateDefault();
_scoringSystem = scoringSystem;
_currencyBank = currencyBank;
_storedRollBank = storedRollBank;
if (_storedRollBank != null)
_storedRollBank.OnChanged += HandleStoredRollsChanged;
if (_currencyBank != null)
_currencyBank.OnBalanceChanged += HandleCurrencyChanged;
}
public RunState State => _state;
public int CurrentCurrency => _currencyBank != null ? _currencyBank.Balance : 0;
public event Action<RunState> OnRunStarted;
public event Action<RunState> OnBetStarted;
public event Action<RunState> OnShopOpened;
public event Action<RunStageState> OnStageStarted;
public event Action<RunStageState, CategoryDefinition, ScoreResult> OnStageCleared;
public event Action<RunStageState> OnStageFailed;
public event Action<int> OnStoredRollsChanged;
public event Action<int> OnCurrencyChanged;
public event Action<int, int> OnCycleCompleted;
public event Action<int> OnQuotaChanged;
public event Action<RunState> OnRunFailed;
public event Action<RunPhase> OnPhaseChanged;
public event Action OnCategoriesRefreshed;
public void StartNewRun()
{
_scoringSystem.ResetScorecard();
_storedRollBank.Reset();
_state.BaseQuota = _config.StartingBaseQuota;
_state.BetIndex = 0;
_state.StageIndex = 0;
_state.CurrentRoll = 0;
_state.CurrentStageRollBudget = _config.BaseRollsPerStage;
_state.CurrentStageTarget = _config.GetStageTarget(_state.BaseQuota, 0);
_state.StoredRolls = _storedRollBank.Value;
_state.IsActive = true;
_state.IsFailed = false;
SetPhase(RunPhase.None);
OnRunStarted?.Invoke(_state);
StartNextBet();
}
public void CompleteShop()
{
if (_state.Phase != RunPhase.Shop || !_state.IsActive)
return;
StartStage(0);
}
public bool CanRoll()
{
if (!_state.IsActive || _state.IsFailed)
return false;
return _state.Phase == RunPhase.StageStart
|| _state.Phase == RunPhase.CategorySelection
|| _state.Phase == RunPhase.Rolling;
}
public bool TryBeginRoll()
{
if (!CanRoll())
return false;
if (_state.CurrentRoll >= _state.CurrentStageRollBudget)
{
if (!_storedRollBank.TrySpend(1))
return false;
_state.CurrentStageRollBudget += 1;
_state.StoredRolls = _storedRollBank.Value;
}
_state.CurrentRoll += 1;
SetPhase(RunPhase.Rolling);
return true;
}
public void NotifyRollResolved(IReadOnlyList<IDice> dice)
{
if (!_state.IsActive || _state.IsFailed || _state.Phase != RunPhase.Rolling)
return;
SetPhase(RunPhase.CategorySelection);
if (!HasRemainingRollCapacity() && !HasAnyScorableCategory(dice))
FailCurrentStage();
}
public bool CanScoreCategory(IReadOnlyList<IDice> dice, CategoryDefinition category)
{
if (!_state.IsActive || _state.IsFailed || _state.Phase != RunPhase.CategorySelection)
return false;
if (category == null || _scoringSystem.IsCategoryUsed(category))
return false;
if (_state.CurrentRoll <= 0)
return false;
var preview = PreviewScore(dice, category);
return preview.FinalScore >= _state.CurrentStageTarget;
}
public ScoreResult PreviewScore(IReadOnlyList<IDice> dice, CategoryDefinition category)
{
return _scoringSystem.PreviewScore(
dice,
category,
_state.CurrentRoll,
GetProgressOrdinal(),
CurrentCurrency);
}
public bool HasAnyScorableCategory(IReadOnlyList<IDice> dice)
{
var catalog = _scoringSystem.Catalog;
if (catalog == null)
return false;
var categories = catalog.All;
for (var i = 0; i < categories.Count; i++)
{
if (CanScoreCategory(dice, categories[i]))
return true;
}
return false;
}
public bool HasRemainingRollCapacity()
{
return _state.CurrentRoll < _state.CurrentStageRollBudget || _storedRollBank.Value > 0;
}
public async UniTask<bool> TryScoreCategoryAsync(IReadOnlyList<IDice> dice, CategoryDefinition category)
{
if (!CanScoreCategory(dice, category))
return false;
var clearedStage = GetCurrentStageState();
var result = await _scoringSystem.ScoreCategoryAsync(
dice,
category,
_state.CurrentRoll,
GetProgressOrdinal(),
CurrentCurrency);
var remainingRolls = Math.Max(0, _state.CurrentStageRollBudget - _state.CurrentRoll);
if (remainingRolls > 0)
_storedRollBank.Add(remainingRolls);
if (_config.StageClearReward > 0 && _currencyBank != null)
_currencyBank.Add(_config.StageClearReward);
SetPhase(RunPhase.StageResolved);
OnStageCleared?.Invoke(clearedStage, category, result);
if (_state.StageIndex >= _config.StageCount - 1)
{
ResolveBet();
}
else
{
StartStage(_state.StageIndex + 1);
}
return true;
}
public void FailCurrentStage()
{
if (!_state.IsActive || _state.IsFailed)
return;
var failedStage = GetCurrentStageState();
_state.IsActive = false;
_state.IsFailed = true;
SetPhase(RunPhase.RunFailed);
OnStageFailed?.Invoke(failedStage);
OnRunFailed?.Invoke(_state);
}
private void StartNextBet()
{
_state.BetIndex += 1;
_state.StageIndex = 0;
_state.CurrentRoll = 0;
_state.CurrentStageRollBudget = _config.BaseRollsPerStage;
_state.CurrentStageTarget = _config.GetStageTarget(_state.BaseQuota, 0);
SetPhase(RunPhase.StartBet);
OnCategoriesRefreshed?.Invoke();
OnBetStarted?.Invoke(_state);
SetPhase(RunPhase.Shop);
OnShopOpened?.Invoke(_state);
}
private void StartStage(int stageIndex)
{
_state.StageIndex = stageIndex;
_state.CurrentRoll = 0;
_state.CurrentStageRollBudget = _config.BaseRollsPerStage;
_state.CurrentStageTarget = _config.GetStageTarget(_state.BaseQuota, stageIndex);
SetPhase(RunPhase.StageStart);
OnStageStarted?.Invoke(GetCurrentStageState());
}
private void ResolveBet()
{
SetPhase(RunPhase.BetResolved);
if (_scoringSystem.IsComplete)
{
var bonus = _storedRollBank.Value * _config.CycleStoredRollBonusMultiplier;
if (bonus > 0 && _currencyBank != null)
_currencyBank.Add(bonus);
OnCycleCompleted?.Invoke(bonus, _storedRollBank.Value);
_scoringSystem.ResetScorecard();
var nextQuota = _config.GetNextQuota(_state.BaseQuota);
_state.BaseQuota = nextQuota;
OnQuotaChanged?.Invoke(nextQuota);
SetPhase(RunPhase.CycleResolved);
}
StartNextBet();
}
private RunStageState GetCurrentStageState()
{
return new RunStageState(_state.StageIndex, (RunStageType)Math.Min(_state.StageIndex, 2), _state.CurrentStageTarget);
}
private int GetProgressOrdinal()
{
return ((_state.BetIndex - 1) * Math.Max(1, _config.StageCount)) + _state.StageIndex + 1;
}
private void SetPhase(RunPhase phase)
{
_state.Phase = phase;
OnPhaseChanged?.Invoke(phase);
}
private void HandleStoredRollsChanged(int value)
{
_state.StoredRolls = value;
OnStoredRollsChanged?.Invoke(value);
}
private void HandleCurrencyChanged(int value)
{
OnCurrencyChanged?.Invoke(value);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 303eb3c5c68f41b4183853b80f5bdf0d
+17
View File
@@ -0,0 +1,17 @@
namespace YachtDice.Run
{
public enum RunPhase
{
None,
StartBet,
Shop,
StageStart,
Rolling,
CategorySelection,
StageResolved,
BetResolved,
CycleResolved,
RunFailed,
RunCompleted,
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3538676a7c72a1f4088981a1a33c6126
+19
View File
@@ -0,0 +1,19 @@
using System;
namespace YachtDice.Run
{
[Serializable]
public readonly struct RunStageState
{
public RunStageState(int index, RunStageType stageType, int target)
{
Index = index;
StageType = stageType;
Target = target;
}
public int Index { get; }
public RunStageType StageType { get; }
public int Target { get; }
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: efe1d4387ae5ef94da49a7fcd867a3cb
+9
View File
@@ -0,0 +1,9 @@
namespace YachtDice.Run
{
public enum RunStageType
{
Base = 0,
Mid = 1,
Final = 2,
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 279fbb0d4676dcf4eb0224a8535f8a86
+24
View File
@@ -0,0 +1,24 @@
using System;
namespace YachtDice.Run
{
[Serializable]
public sealed class RunState
{
public int BaseQuota;
public int BetIndex;
public int StageIndex;
public int CurrentStageTarget;
public int CurrentRoll;
public int CurrentStageRollBudget;
public int StoredRolls;
public bool IsActive;
public bool IsFailed;
public RunPhase Phase;
public int StageNumber => StageIndex + 1;
public bool IsInShop => Phase == RunPhase.Shop;
public bool IsCategorySelection => Phase == RunPhase.CategorySelection;
public bool IsRollingState => Phase == RunPhase.StageStart || Phase == RunPhase.Rolling || Phase == RunPhase.CategorySelection;
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 308eb21a80d1c2a4283e6de6aac0d026
+39
View File
@@ -0,0 +1,39 @@
using System;
namespace YachtDice.Run
{
public sealed class StoredRollBank
{
public int Value { get; private set; }
public event Action<int> OnChanged;
public void Reset()
{
SetValue(0);
}
public void Add(int amount)
{
if (amount <= 0)
return;
SetValue(Value + amount);
}
public bool TrySpend(int amount)
{
if (amount <= 0 || Value < amount)
return false;
SetValue(Value - amount);
return true;
}
private void SetValue(int value)
{
Value = Math.Max(0, value);
OnChanged?.Invoke(Value);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e46140b5dd26bfc469334aabd4ab51b8
+14
View File
@@ -54,6 +54,20 @@ namespace YachtDice.Shop
shopView.Show();
}
public void Open()
{
if (shopView == null) return;
shopView.Show();
}
public void Close()
{
if (shopView == null) return;
shopView.Hide();
}
public bool IsOpen => shopView != null && shopView.IsVisible;
private void HandleBuyClicked(IShopItem item)
{
_model.TryPurchase(item);
@@ -0,0 +1,220 @@
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using YachtDice.Categories;
using YachtDice.Dice;
using YachtDice.Economy;
using YachtDice.Run;
using YachtDice.Scoring;
namespace YachtDice.Tests
{
public sealed class RunLoopServiceTests
{
private readonly List<Object> _createdAssets = new();
private readonly IReadOnlyList<IDice> _emptyDice = new IDice[0];
private CurrencyBank _currencyBank;
private ScoringSystem _scoringSystem;
private StoredRollBank _storedRollBank;
private RunBalanceConfigSO _config;
[SetUp]
public void SetUp()
{
var bankGo = new GameObject("CurrencyBank");
_currencyBank = bankGo.AddComponent<CurrencyBank>();
_currencyBank.SetBalance(0);
var scoringGo = new GameObject("ScoringSystem");
_scoringSystem = scoringGo.AddComponent<ScoringSystem>();
_storedRollBank = new StoredRollBank();
_config = RunBalanceConfigSO.CreateDefault();
_createdAssets.Add(_config);
}
[TearDown]
public void TearDown()
{
foreach (var scoring in Object.FindObjectsByType<ScoringSystem>(FindObjectsSortMode.None))
Object.DestroyImmediate(scoring.gameObject);
foreach (var bank in Object.FindObjectsByType<CurrencyBank>(FindObjectsSortMode.None))
Object.DestroyImmediate(bank.gameObject);
for (var i = 0; i < _createdAssets.Count; i++)
{
if (_createdAssets[i] != null)
Object.DestroyImmediate(_createdAssets[i]);
}
_createdAssets.Clear();
}
[Test]
public void StartNewRun_EntersShopWithConfigQuota()
{
var categories = CreateFixedCatalog(100, 100, 100);
var service = CreateService(categories);
service.StartNewRun();
Assert.AreEqual(30, service.State.BaseQuota);
Assert.AreEqual(1, service.State.BetIndex);
Assert.AreEqual(RunPhase.Shop, service.State.Phase);
Assert.AreEqual(0, service.State.StoredRolls);
}
[Test]
public void ClearedStage_AwardsCurrencyAndBanksUnusedRolls()
{
var categories = CreateFixedCatalog(100, 100, 100);
var service = CreateService(categories);
service.StartNewRun();
service.CompleteShop();
service.TryBeginRoll();
service.NotifyRollResolved(_emptyDice);
var cleared = service.TryScoreCategoryAsync(_emptyDice, categories.All[0]).GetAwaiter().GetResult();
Assert.IsTrue(cleared);
Assert.AreEqual(25, _currencyBank.Balance);
Assert.AreEqual(2, service.State.StoredRolls);
Assert.AreEqual(2, service.State.StageNumber);
Assert.AreEqual(RunPhase.StageStart, service.State.Phase);
}
[Test]
public void StoredRolls_CanBeConsumedOnLaterStage()
{
var categories = CreateFixedCatalog(100, 100, 100);
var service = CreateService(categories);
service.StartNewRun();
service.CompleteShop();
service.TryBeginRoll();
service.NotifyRollResolved(_emptyDice);
service.TryScoreCategoryAsync(_emptyDice, categories.All[0]).GetAwaiter().GetResult();
for (var i = 0; i < 4; i++)
{
Assert.IsTrue(service.TryBeginRoll());
service.NotifyRollResolved(_emptyDice);
}
service.TryScoreCategoryAsync(_emptyDice, categories.All[1]).GetAwaiter().GetResult();
Assert.AreEqual(1, service.State.StoredRolls);
Assert.AreEqual(50, _currencyBank.Balance);
}
[Test]
public void CompletedCycle_GrantsBonusRaisesQuotaAndStartsNextBet()
{
var categories = CreateFixedCatalog(100, 100, 100);
var service = CreateService(categories);
service.StartNewRun();
service.CompleteShop();
ClearStageInOneRoll(service, categories.All[0]);
ClearStageInOneRoll(service, categories.All[1]);
ClearStageInOneRoll(service, categories.All[2]);
Assert.AreEqual(135, _currencyBank.Balance);
Assert.AreEqual(60, service.State.BaseQuota);
Assert.AreEqual(2, service.State.BetIndex);
Assert.AreEqual(RunPhase.Shop, service.State.Phase);
Assert.AreEqual(6, service.State.StoredRolls);
Assert.AreEqual(0, _scoringSystem.CategoriesFilledCount);
}
[Test]
public void FailedStage_EndsRunWhenNoTargetCanBeMet()
{
var categories = CreateFixedCatalog(10);
var service = CreateService(categories);
service.StartNewRun();
service.CompleteShop();
for (var i = 0; i < 3; i++)
{
Assert.IsTrue(service.TryBeginRoll());
service.NotifyRollResolved(_emptyDice);
}
Assert.IsTrue(service.State.IsFailed);
Assert.AreEqual(RunPhase.RunFailed, service.State.Phase);
}
[Test]
public void Shop_ReopensOnlyAtStartOfNextBet()
{
var categories = CreateFixedCatalog(100, 100, 100, 100);
var service = CreateService(categories);
service.StartNewRun();
Assert.AreEqual(RunPhase.Shop, service.State.Phase);
service.CompleteShop();
ClearStageInOneRoll(service, categories.All[0]);
Assert.AreEqual(RunPhase.StageStart, service.State.Phase);
ClearStageInOneRoll(service, categories.All[1]);
Assert.AreEqual(RunPhase.StageStart, service.State.Phase);
ClearStageInOneRoll(service, categories.All[2]);
Assert.AreEqual(RunPhase.Shop, service.State.Phase);
Assert.AreEqual(2, service.State.BetIndex);
}
private RunLoopService CreateService(CategoryCatalog catalog)
{
_scoringSystem.Construct(null, null, catalog, _currencyBank);
return new RunLoopService(_config, _scoringSystem, _currencyBank, _storedRollBank);
}
private CategoryCatalog CreateFixedCatalog(params int[] scores)
{
var categories = new List<CategoryDefinition>();
for (var i = 0; i < scores.Length; i++)
{
var category = FixedScoreCategory.Create($"cat_{i}", scores[i]);
_createdAssets.Add(category);
categories.Add(category);
}
var catalog = CategoryCatalog.CreateForTest(categories);
_createdAssets.Add(catalog);
return catalog;
}
private void ClearStageInOneRoll(RunLoopService service, CategoryDefinition category)
{
Assert.IsTrue(service.TryBeginRoll());
service.NotifyRollResolved(_emptyDice);
Assert.IsTrue(service.TryScoreCategoryAsync(_emptyDice, category).GetAwaiter().GetResult());
}
private sealed class FixedScoreCategory : CategoryDefinition
{
[SerializeField] private int fixedScore;
public override int Calculate(IReadOnlyList<IDice> dice)
{
return fixedScore;
}
public static FixedScoreCategory Create(string id, int score)
{
var category = CreateInstance<FixedScoreCategory>();
category.SetTestData(id, id);
category.fixedScore = score;
return category;
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 10f09d1ab020a9f498748f79ae8c5730
+5
View File
@@ -82,6 +82,11 @@ namespace YachtDice.UI
rollButtonText.text = $"Бросок {currentRoll + 1}/{maxRolls}";
}
public void SetRollButtonInteractable(bool interactable)
{
rollButton.interactable = interactable;
}
public void ResetForNewTurn()
{
for (int i = 0; i < diceValues.Length; i++)
+12
View File
@@ -42,12 +42,24 @@ namespace YachtDice.UI
turnText.text = $"Ход {turn} / {maxTurns}";
}
public void SetRunInfoText(string text)
{
if (turnText != null)
turnText.text = text;
}
public void SetCurrencyText(int amount)
{
if (currencyText != null)
currencyText.text = amount.ToString();
}
public void SetShopButtonInteractable(bool interactable)
{
if (shopButton != null)
shopButton.interactable = interactable;
}
public void ShowGameOver(int finalScore)
{
gameOverPanel.SetActive(true);
+1 -1
View File
@@ -63,7 +63,7 @@ namespace YachtDice.UI
private void Start()
{
_dicePanelPresenter = new DicePanelPresenter(dicePanelView, _gameLoopController, _diceManager);
_scoreCardPresenter = new ScoreCardPresenter(scoreCardView, _categoryCatalog, _scoringSystem, _diceManager);
_scoreCardPresenter = new ScoreCardPresenter(scoreCardView, _gameLoopController, _categoryCatalog, _scoringSystem, _diceManager);
_gameInfoPresenter = new GameInfoPresenter(gameInfoView);
_gameFlowPresenter = new GameFlowPresenter(
@@ -69,6 +69,12 @@ namespace YachtDice.UI.Presentation
_view.SetDiceLocked(index, isLocked);
}
public void SetRollingEnabled(bool enabled)
{
_view.SetRollButtonInteractable(enabled);
_view.SetDiceInteractable(enabled && _gameLoopController.CurrentRoll > 0);
}
private void HandleRollClicked()
{
RollClicked?.Invoke();
@@ -4,6 +4,7 @@ using YachtDice.Economy;
using YachtDice.Game;
using YachtDice.Inventory;
using YachtDice.Player;
using YachtDice.Run;
using YachtDice.Shop;
namespace YachtDice.UI.Presentation
@@ -57,6 +58,13 @@ namespace YachtDice.UI.Presentation
_gameLoopController.OnRollComplete += HandleRollComplete;
_gameLoopController.OnScored += HandleScored;
_gameLoopController.OnGameOver += HandleGameOver;
_gameLoopController.OnBetStarted += HandleBetStarted;
_gameLoopController.OnShopOpened += HandleShopOpened;
_gameLoopController.OnShopClosed += HandleShopClosed;
_gameLoopController.OnStoredRollsChanged += HandleStoredRollsChanged;
_gameLoopController.OnQuotaChanged += HandleQuotaChanged;
_gameLoopController.OnCycleCompleted += HandleCycleCompleted;
_gameLoopController.OnPhaseChanged += HandlePhaseChanged;
_dicePanelPresenter.RollClicked += HandleRollClicked;
_dicePanelPresenter.DiceToggled += HandleDiceToggled;
@@ -70,6 +78,7 @@ namespace YachtDice.UI.Presentation
_saveService.Load();
_gameInfoPresenter.SetCurrencyText(_currencyBank.Balance);
_gameInfoPresenter.SetShopButtonInteractable(false);
_gameLoopController.StartNewGame();
}
@@ -79,6 +88,13 @@ namespace YachtDice.UI.Presentation
_gameLoopController.OnRollComplete -= HandleRollComplete;
_gameLoopController.OnScored -= HandleScored;
_gameLoopController.OnGameOver -= HandleGameOver;
_gameLoopController.OnBetStarted -= HandleBetStarted;
_gameLoopController.OnShopOpened -= HandleShopOpened;
_gameLoopController.OnShopClosed -= HandleShopClosed;
_gameLoopController.OnStoredRollsChanged -= HandleStoredRollsChanged;
_gameLoopController.OnQuotaChanged -= HandleQuotaChanged;
_gameLoopController.OnCycleCompleted -= HandleCycleCompleted;
_gameLoopController.OnPhaseChanged -= HandlePhaseChanged;
_dicePanelPresenter.RollClicked -= HandleRollClicked;
_dicePanelPresenter.DiceToggled -= HandleDiceToggled;
@@ -95,8 +111,9 @@ namespace YachtDice.UI.Presentation
private void HandleTurnStarted(int turn)
{
_gameInfoPresenter.SetTurnText(turn, _categoryCatalog.Count);
UpdateRunInfoText();
_dicePanelPresenter.ResetForNewTurn();
_dicePanelPresenter.SetRollingEnabled(true);
_scoreCardPresenter.ClearAllPreviews();
}
@@ -110,6 +127,7 @@ namespace YachtDice.UI.Presentation
{
_scoreCardPresenter.SetCategoryScored(category, finalScore);
_scoreCardPresenter.UpdateTotalDisplay(_scoreSummaryService.Calculate());
UpdateRunInfoText();
_saveService.Save();
}
@@ -118,6 +136,8 @@ namespace YachtDice.UI.Presentation
_dicePanelPresenter.HandleGameOver();
_scoreCardPresenter.SetAllInteractable(false);
_gameInfoPresenter.ShowGameOver(_scoreSummaryService.Calculate().DisplayTotal);
_shopController.Close();
_gameInfoPresenter.SetShopButtonInteractable(false);
_saveService.Save();
}
@@ -142,6 +162,8 @@ namespace YachtDice.UI.Presentation
private void HandleNewGameClicked()
{
_gameInfoPresenter.HideGameOver();
_shopController.Close();
_gameInfoPresenter.SetShopButtonInteractable(false);
_scoreCardPresenter.ResetAll();
_dicePanelPresenter.ResetForNewGame();
_gameLoopController.StartNewGame();
@@ -149,7 +171,18 @@ namespace YachtDice.UI.Presentation
private void HandleShopClicked()
{
_shopController.ToggleVisibility();
if (!_gameLoopController.CanOpenShopManually())
return;
if (_shopController.IsOpen)
{
_shopController.Close();
_gameLoopController.CompleteShop();
}
else
{
_shopController.Open();
}
}
private void HandleInventoryClicked()
@@ -160,11 +193,63 @@ namespace YachtDice.UI.Presentation
private void HandleCurrencyChanged(int newBalance)
{
_gameInfoPresenter.SetCurrencyText(newBalance);
UpdateRunInfoText();
}
private void HandlePlayerChangedForSave()
{
_saveService.Save();
}
private void HandleBetStarted(int betIndex)
{
UpdateRunInfoText();
}
private void HandleShopOpened()
{
_shopController.Open();
_gameInfoPresenter.SetShopButtonInteractable(true);
_dicePanelPresenter.SetRollingEnabled(false);
UpdateRunInfoText();
}
private void HandleShopClosed()
{
_shopController.Close();
_gameInfoPresenter.SetShopButtonInteractable(false);
UpdateRunInfoText();
}
private void HandleStoredRollsChanged(int value)
{
UpdateRunInfoText();
}
private void HandleQuotaChanged(int value)
{
UpdateRunInfoText();
}
private void HandleCycleCompleted(int bonus, int storedRolls)
{
_scoreCardPresenter.ResetAll();
_scoreCardPresenter.UpdateTotalDisplay(_scoreSummaryService.Calculate());
UpdateRunInfoText();
}
private void HandlePhaseChanged(RunPhase phase)
{
if (phase != RunPhase.Shop)
_gameInfoPresenter.SetShopButtonInteractable(false);
UpdateRunInfoText();
}
private void UpdateRunInfoText()
{
var info = $"Bet {_gameLoopController.CurrentBet} | Stage {_gameLoopController.CurrentStage}/3 | Target {_gameLoopController.CurrentStageTarget} | Quota {_gameLoopController.CurrentBaseQuota} | Bank {_gameLoopController.StoredRolls}";
_gameInfoPresenter.SetRunInfoText(info);
}
}
}
@@ -34,11 +34,21 @@ namespace YachtDice.UI.Presentation
_view.SetTurnText(turn, maxTurns);
}
public void SetRunInfoText(string text)
{
_view.SetRunInfoText(text);
}
public void SetCurrencyText(int amount)
{
_view.SetCurrencyText(amount);
}
public void SetShopButtonInteractable(bool interactable)
{
_view.SetShopButtonInteractable(interactable);
}
public void ShowGameOver(int finalScore)
{
_view.ShowGameOver(finalScore);
@@ -9,6 +9,7 @@ namespace YachtDice.UI.Presentation
public sealed class ScoreCardPresenter : IDisposable
{
private readonly ScoreCardView _view;
private readonly GameLoopController _gameLoopController;
private readonly CategoryCatalog _categoryCatalog;
private readonly ScoringSystem _scoringSystem;
private readonly DiceManager _diceManager;
@@ -17,11 +18,13 @@ namespace YachtDice.UI.Presentation
public ScoreCardPresenter(
ScoreCardView view,
GameLoopController gameLoopController,
CategoryCatalog categoryCatalog,
ScoringSystem scoringSystem,
DiceManager diceManager)
{
_view = view;
_gameLoopController = gameLoopController;
_categoryCatalog = categoryCatalog;
_scoringSystem = scoringSystem;
_diceManager = diceManager;
@@ -55,11 +58,12 @@ namespace YachtDice.UI.Presentation
if (_scoringSystem.IsCategoryUsed(category))
continue;
var result = _scoringSystem.PreviewScore(dice, category);
var result = _gameLoopController.PreviewCategory(category);
previews[category] = result.FinalScore;
}
_view.UpdatePreviews(previews);
SetAvailableCategoriesInteractable();
}
public void SetCategoryScored(CategoryDefinition category, int finalScore)
@@ -72,6 +76,19 @@ namespace YachtDice.UI.Presentation
_view.SetAllInteractable(interactable);
}
public void SetAvailableCategoriesInteractable()
{
var allCategories = _categoryCatalog.All;
for (var i = 0; i < allCategories.Count; i++)
{
var category = allCategories[i];
if (_scoringSystem.IsCategoryUsed(category))
continue;
_view.SetCategoryInteractable(category, _gameLoopController.CanScoreCategory(category));
}
}
public void UpdateTotalDisplay(ScoreSummary summary)
{
_view.UpdateTotalDisplay(summary.DisplayTotal);
+6
View File
@@ -74,6 +74,12 @@ namespace YachtDice.UI
t.SetInteractable(interactable);
}
public void SetCategoryInteractable(CategoryDefinition category, bool interactable)
{
if (_categoryToRowIndex != null && _categoryToRowIndex.TryGetValue(category, out int index))
categoryRows[index].SetInteractable(interactable);
}
public void UpdateTotalDisplay(int totalScore)
{
totalScoreText.text = totalScore.ToString();