102 lines
4.5 KiB
C#
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);
|
|
}
|
|
} |