[Add] GameLoop base
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace YachtDice.Run
|
||||
{
|
||||
public enum RunPhase
|
||||
{
|
||||
None,
|
||||
StartBet,
|
||||
Shop,
|
||||
StageStart,
|
||||
Rolling,
|
||||
CategorySelection,
|
||||
StageResolved,
|
||||
BetResolved,
|
||||
CycleResolved,
|
||||
RunFailed,
|
||||
RunCompleted,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3538676a7c72a1f4088981a1a33c6126
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: efe1d4387ae5ef94da49a7fcd867a3cb
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace YachtDice.Run
|
||||
{
|
||||
public enum RunStageType
|
||||
{
|
||||
Base = 0,
|
||||
Mid = 1,
|
||||
Final = 2,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 279fbb0d4676dcf4eb0224a8535f8a86
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 308eb21a80d1c2a4283e6de6aac0d026
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user