Files
RpgRoller/RpgRoller/Services/RolemasterRollEngine.cs

102 lines
4.5 KiB
C#

using RpgRoller.Contracts;
using RpgRoller.Domain;
namespace RpgRoller.Services;
public sealed class RolemasterRollEngine(IDiceRoller diceRoller)
{
public (int Total, string Breakdown, IReadOnlyList<RollDieResult> Dice) Roll(DiceExpression expression, int? fumbleRange, bool rolemasterAutoRetry)
{
return expression.Kind switch
{
DiceExpressionKind.RolemasterOpenEndedPercentile => RollOpenEnded(expression, fumbleRange.GetValueOrDefault(), rolemasterAutoRetry),
_ => RollStandard(expression)
};
}
private (int Total, string Breakdown, IReadOnlyList<RollDieResult> Dice) RollStandard(DiceExpression expression, int? attempt = null)
{
var diceValues = new int[expression.DiceCount];
var dice = new RollDieResult[expression.DiceCount];
var total = expression.Modifier;
for (var i = 0; i < expression.DiceCount; i += 1)
{
var value = diceRoller.Roll(expression.Sides);
diceValues[i] = value;
dice[i] = CreateRolemasterDie(value, i + 1, RollDieKinds.RolemasterStandard, value, attempt);
total += value;
}
return (total, RollBreakdownFormatter.BuildBreakdown(diceValues, expression.Modifier, total), dice);
}
private (int Total, string Breakdown, IReadOnlyList<RollDieResult> Dice) RollOpenEnded(DiceExpression expression, int fumbleRange, bool rolemasterAutoRetry)
{
var firstAttempt = RollOpenEndedAttempt(expression, fumbleRange);
var retryBonus = rolemasterAutoRetry ? RolemasterRetryPolicy.ResolveAutoRetryBonus(firstAttempt.Total) : null;
if (!retryBonus.HasValue)
return firstAttempt;
var retryAttempt = RollOpenEndedAttempt(expression, fumbleRange, 2);
var finalTotal = retryAttempt.Total + retryBonus.Value;
var breakdown = RollBreakdownFormatter.BuildRolemasterRetryBreakdown(firstAttempt.Breakdown, retryBonus.Value, retryAttempt.Breakdown, finalTotal);
var dice = AddAttemptMarker(firstAttempt.Dice, 1).Concat(retryAttempt.Dice).ToArray();
return (finalTotal, breakdown, dice);
}
private (int Total, string Breakdown, IReadOnlyList<RollDieResult> Dice) RollOpenEndedAttempt(DiceExpression expression, int fumbleRange, int? attempt = null)
{
var initialRoll = diceRoller.Roll(expression.Sides);
var followUpRolls = new List<int>();
int? initialContribution = initialRoll <= fumbleRange ? null : initialRoll;
var dice = new List<RollDieResult> { CreateRolemasterDie(initialRoll, 1, RollDieKinds.RolemasterOpenEndedInitial, initialContribution, attempt) };
var baseTotal = initialRoll <= fumbleRange ? 0 : initialRoll;
var subtractFollowUps = false;
if (initialRoll >= 96)
{
followUpRolls.AddRange(RollHighOpenEndedChain(dice, 2, false, attempt));
baseTotal += followUpRolls.Sum();
}
else if (initialRoll <= fumbleRange)
{
subtractFollowUps = true;
followUpRolls.AddRange(RollHighOpenEndedChain(dice, 2, true, attempt));
baseTotal -= followUpRolls.Sum();
}
var total = baseTotal + expression.Modifier;
var breakdown = RollBreakdownFormatter.BuildRolemasterOpenEndedBreakdown(initialRoll, followUpRolls, subtractFollowUps, expression.Modifier, total);
return (total, breakdown, dice);
}
private IEnumerable<int> RollHighOpenEndedChain(List<RollDieResult> dice, int sequenceStart, bool subtract, int? attempt)
{
var followUpRolls = new List<int>();
var sequence = sequenceStart;
while (true)
{
var roll = diceRoller.Roll(100);
followUpRolls.Add(roll);
dice.Add(CreateRolemasterDie(roll, sequence, subtract ? RollDieKinds.RolemasterOpenEndedLowSubtract : RollDieKinds.RolemasterOpenEndedHigh, subtract ? -roll : roll, attempt));
sequence += 1;
if (roll < 96)
break;
}
return followUpRolls;
}
private static IReadOnlyList<RollDieResult> AddAttemptMarker(IReadOnlyList<RollDieResult> dice, int attempt)
{
return dice.Select(die => die with { Attempt = attempt }).ToArray();
}
private static RollDieResult CreateRolemasterDie(int roll, int sequence, string kind, int? signedContribution, int? attempt)
{
return new(roll, false, false, false, false, kind == RollDieKinds.RolemasterOpenEndedHigh, sequence, kind, signedContribution, attempt);
}
}