Extract service rule helpers
This commit is contained in:
18
RpgRoller/Services/CustomRollOptionsResolver.cs
Normal file
18
RpgRoller/Services/CustomRollOptionsResolver.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using RpgRoller.Domain;
|
||||
|
||||
namespace RpgRoller.Services;
|
||||
|
||||
public static class CustomRollOptionsResolver
|
||||
{
|
||||
private const int DefaultCustomD6WildDice = 1;
|
||||
private const bool DefaultCustomD6AllowFumble = true;
|
||||
|
||||
public static (int WildDice, bool AllowFumble, int? FumbleRange) Resolve(RulesetKind ruleset)
|
||||
{
|
||||
return ruleset switch
|
||||
{
|
||||
RulesetKind.D6 => (DefaultCustomD6WildDice, DefaultCustomD6AllowFumble, null),
|
||||
_ => (0, false, null)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -282,14 +282,14 @@ public sealed class GameService : IGameService
|
||||
if (!m_UsersById.TryGetValue(userId, out var targetUser))
|
||||
return ServiceResult<AdminUserSummary>.Failure("user_not_found", "User was not found.");
|
||||
|
||||
var normalizedRoles = NormalizeRoles(roles);
|
||||
var normalizedRoles = RoleSerializer.Normalize(roles);
|
||||
if (normalizedRoles.Any(role => !string.Equals(role, UserRoles.Admin, StringComparison.Ordinal)))
|
||||
return ServiceResult<AdminUserSummary>.Failure("invalid_role", "Unsupported role.");
|
||||
|
||||
if (user.Id == targetUser.Id && !normalizedRoles.Contains(UserRoles.Admin, StringComparer.Ordinal))
|
||||
return ServiceResult<AdminUserSummary>.Failure("forbidden", "You cannot remove your own admin role.");
|
||||
|
||||
targetUser.Roles = SerializeRoles(normalizedRoles);
|
||||
targetUser.Roles = RoleSerializer.Serialize(normalizedRoles);
|
||||
PersistStateLocked();
|
||||
return ServiceResult<AdminUserSummary>.Success(ToAdminUserSummary(targetUser));
|
||||
}
|
||||
@@ -516,7 +516,7 @@ public sealed class GameService : IGameService
|
||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||
return ServiceResult<SkillGroupSummary>.Failure("forbidden", "Only the owner or GM can manage skill groups.");
|
||||
|
||||
var prototypeValidation = ValidateSkillDefinition(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||
var prototypeValidation = SkillDefinitionValidator.Validate(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||
if (!prototypeValidation.Succeeded)
|
||||
return ServiceResult<SkillGroupSummary>.Failure(prototypeValidation.Error!.Code, prototypeValidation.Error.Message);
|
||||
|
||||
@@ -560,7 +560,7 @@ public sealed class GameService : IGameService
|
||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||
return ServiceResult<SkillGroupSummary>.Failure("forbidden", "Only the owner or GM can manage skill groups.");
|
||||
|
||||
var prototypeValidation = ValidateSkillDefinition(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||
var prototypeValidation = SkillDefinitionValidator.Validate(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||
if (!prototypeValidation.Succeeded)
|
||||
return ServiceResult<SkillGroupSummary>.Failure(prototypeValidation.Error!.Code, prototypeValidation.Error.Message);
|
||||
|
||||
@@ -625,7 +625,7 @@ public sealed class GameService : IGameService
|
||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
||||
|
||||
var skillValidation = ValidateSkillDefinition(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||
var skillValidation = SkillDefinitionValidator.Validate(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||
if (!skillValidation.Succeeded)
|
||||
return ServiceResult<SkillSummary>.Failure(skillValidation.Error!.Code, skillValidation.Error.Message);
|
||||
|
||||
@@ -674,7 +674,7 @@ public sealed class GameService : IGameService
|
||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
||||
|
||||
var skillValidation = ValidateSkillDefinition(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||
var skillValidation = SkillDefinitionValidator.Validate(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||
if (!skillValidation.Succeeded)
|
||||
return ServiceResult<SkillSummary>.Failure(skillValidation.Error!.Code, skillValidation.Error.Message);
|
||||
|
||||
@@ -764,7 +764,7 @@ public sealed class GameService : IGameService
|
||||
if (!parsedExpression.Succeeded)
|
||||
return ServiceResult<RollResult>.Failure(parsedExpression.Error!.Code, parsedExpression.Error.Message);
|
||||
|
||||
var parsedVisibility = ParseVisibility(visibility);
|
||||
var parsedVisibility = RollVisibilityParser.Parse(visibility);
|
||||
if (!parsedVisibility.Succeeded)
|
||||
return ServiceResult<RollResult>.Failure(parsedVisibility.Error!.Code, parsedVisibility.Error.Message);
|
||||
|
||||
@@ -794,11 +794,11 @@ public sealed class GameService : IGameService
|
||||
if (!parsedExpression.Succeeded)
|
||||
return ServiceResult<RollResult>.Failure(parsedExpression.Error!.Code, parsedExpression.Error.Message);
|
||||
|
||||
var parsedVisibility = ParseVisibility(visibility);
|
||||
var parsedVisibility = RollVisibilityParser.Parse(visibility);
|
||||
if (!parsedVisibility.Succeeded)
|
||||
return ServiceResult<RollResult>.Failure(parsedVisibility.Error!.Code, parsedVisibility.Error.Message);
|
||||
|
||||
var (wildDice, allowFumble, fumbleRange) = ResolveCustomRollOptions(campaign.Ruleset);
|
||||
var (wildDice, allowFumble, fumbleRange) = CustomRollOptionsResolver.Resolve(campaign.Ruleset);
|
||||
var roll = ComputeRoll(campaign.Ruleset, parsedExpression.Value!, wildDice, allowFumble, fumbleRange);
|
||||
return RecordRollLocked(user, campaign, character, CustomRollSkillId, parsedVisibility.Value, roll, parsedExpression.Value!.Canonical);
|
||||
}
|
||||
@@ -893,66 +893,6 @@ public sealed class GameService : IGameService
|
||||
}
|
||||
}
|
||||
|
||||
private static ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)> ValidateSkillDefinition(RulesetKind ruleset, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange)
|
||||
{
|
||||
var expressionValidation = DiceRules.ParseExpression(ruleset, diceRollDefinition);
|
||||
if (!expressionValidation.Succeeded)
|
||||
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)>.Failure(expressionValidation.Error!.Code, expressionValidation.Error.Message);
|
||||
|
||||
var optionsValidation = ValidateSkillOptions(ruleset, expressionValidation.Value!, wildDice, allowFumble, fumbleRange);
|
||||
if (!optionsValidation.Succeeded)
|
||||
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)>.Failure(optionsValidation.Error!.Code, optionsValidation.Error.Message);
|
||||
|
||||
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)>.Success((expressionValidation.Value!.Canonical, optionsValidation.Value!.WildDice, optionsValidation.Value.AllowFumble, optionsValidation.Value.FumbleRange));
|
||||
}
|
||||
|
||||
private static ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)> ValidateSkillOptions(RulesetKind ruleset, DiceExpression expression, int wildDice, bool allowFumble, int? fumbleRange)
|
||||
{
|
||||
if (wildDice < 0 || wildDice > 50)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_wild_dice", "Wild dice must be between 0 and 50.");
|
||||
|
||||
if (ruleset == RulesetKind.D6)
|
||||
{
|
||||
if (wildDice < 1)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_wild_dice", "D6 skills require at least one wild die.");
|
||||
|
||||
if (fumbleRange.HasValue)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_fumble_range", "Fumble range is only supported for Rolemaster open-ended percentile skills.");
|
||||
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Success((wildDice, allowFumble, null));
|
||||
}
|
||||
|
||||
if (ruleset == RulesetKind.Rolemaster)
|
||||
{
|
||||
if (wildDice != 0)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_wild_dice", "Rolemaster skills do not support wild dice.");
|
||||
|
||||
if (allowFumble)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_allow_fumble", "Rolemaster skills do not use the D6 allow-fumble option.");
|
||||
|
||||
if (expression.Kind == DiceExpressionKind.RolemasterOpenEndedPercentile)
|
||||
{
|
||||
if (!fumbleRange.HasValue)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_fumble_range", "Rolemaster open-ended percentile skills require a fumble range.");
|
||||
|
||||
if (fumbleRange < 0 || fumbleRange >= 96)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_fumble_range", "Fumble range must be between 0 and 95.");
|
||||
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Success((0, false, fumbleRange));
|
||||
}
|
||||
|
||||
if (fumbleRange.HasValue)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_fumble_range", "Fumble range is only valid for Rolemaster open-ended percentile skills.");
|
||||
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Success((0, false, null));
|
||||
}
|
||||
|
||||
if (fumbleRange.HasValue)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_fumble_range", "Fumble range is only supported for Rolemaster open-ended percentile skills.");
|
||||
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Success((0, false, null));
|
||||
}
|
||||
|
||||
private (int Total, string Breakdown, IReadOnlyList<RollDieResult> Dice) ComputeRoll(RulesetKind ruleset, DiceExpression expression, int wildDice, bool allowFumble, int? fumbleRange)
|
||||
{
|
||||
if (ruleset == RulesetKind.D6)
|
||||
@@ -1201,17 +1141,6 @@ public sealed class GameService : IGameService
|
||||
return ServiceResult<Guid?>.Success(skillGroup.Id);
|
||||
}
|
||||
|
||||
private ServiceResult<RollVisibility> ParseVisibility(string visibility)
|
||||
{
|
||||
if (string.Equals(visibility, "public", StringComparison.OrdinalIgnoreCase))
|
||||
return ServiceResult<RollVisibility>.Success(RollVisibility.Public);
|
||||
|
||||
if (string.Equals(visibility, "private", StringComparison.OrdinalIgnoreCase))
|
||||
return ServiceResult<RollVisibility>.Success(RollVisibility.Private);
|
||||
|
||||
return ServiceResult<RollVisibility>.Failure("invalid_visibility", "Visibility must be 'public' or 'private'.");
|
||||
}
|
||||
|
||||
private ServiceResult<RollResult> RecordRollLocked(
|
||||
UserAccount user,
|
||||
Campaign campaign,
|
||||
@@ -1242,15 +1171,6 @@ public sealed class GameService : IGameService
|
||||
return ServiceResult<RollResult>.Success(ToRollResult(entry, roll.Dice));
|
||||
}
|
||||
|
||||
private static (int WildDice, bool AllowFumble, int? FumbleRange) ResolveCustomRollOptions(RulesetKind ruleset)
|
||||
{
|
||||
return ruleset switch
|
||||
{
|
||||
RulesetKind.D6 => (DefaultCustomD6WildDice, DefaultCustomD6AllowFumble, null),
|
||||
_ => (0, false, null)
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatLoggedBreakdown(Guid skillId, string canonicalExpression, string breakdown)
|
||||
{
|
||||
return skillId == CustomRollSkillId
|
||||
@@ -1275,12 +1195,12 @@ public sealed class GameService : IGameService
|
||||
|
||||
private static UserSummary ToUserSummary(UserAccount user)
|
||||
{
|
||||
return new(user.Id, user.Username, user.DisplayName, ParseRoles(user.Roles));
|
||||
return new(user.Id, user.Username, user.DisplayName, RoleSerializer.Parse(user.Roles));
|
||||
}
|
||||
|
||||
private static AdminUserSummary ToAdminUserSummary(UserAccount user)
|
||||
{
|
||||
return new(user.Id, user.Username, user.DisplayName, ParseRoles(user.Roles));
|
||||
return new(user.Id, user.Username, user.DisplayName, RoleSerializer.Parse(user.Roles));
|
||||
}
|
||||
|
||||
private static CampaignOption ToCampaignOption(Campaign campaign)
|
||||
@@ -1702,29 +1622,9 @@ public sealed class GameService : IGameService
|
||||
TouchRosterLocked(campaignId);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> ParseRoles(string serializedRoles)
|
||||
{
|
||||
return NormalizeRoles(serializedRoles.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries));
|
||||
}
|
||||
|
||||
private static string SerializeRoles(IReadOnlyList<string> roles)
|
||||
{
|
||||
return string.Join(",", NormalizeRoles(roles));
|
||||
}
|
||||
|
||||
private static string[] NormalizeRoles(IEnumerable<string> roles)
|
||||
{
|
||||
return roles
|
||||
.Where(role => !string.IsNullOrWhiteSpace(role))
|
||||
.Select(role => role.Trim().ToLowerInvariant())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(role => role, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static bool UserHasRoleLocked(UserAccount user, string role)
|
||||
{
|
||||
return ParseRoles(user.Roles).Contains(role, StringComparer.OrdinalIgnoreCase);
|
||||
return RoleSerializer.HasRole(user.Roles, role);
|
||||
}
|
||||
|
||||
private UserSession CreateSession(Guid userId)
|
||||
@@ -1854,7 +1754,7 @@ public sealed class GameService : IGameService
|
||||
UsernameNormalized = normalizedUsername,
|
||||
PasswordHash = user.PasswordHash,
|
||||
DisplayName = user.DisplayName,
|
||||
Roles = string.IsNullOrWhiteSpace(user.Roles) ? string.Empty : SerializeRoles(ParseRoles(user.Roles)),
|
||||
Roles = string.IsNullOrWhiteSpace(user.Roles) ? string.Empty : RoleSerializer.Serialize(RoleSerializer.Parse(user.Roles)),
|
||||
ActiveCharacterId = user.ActiveCharacterId
|
||||
};
|
||||
m_UsersById[storedUser.Id] = storedUser;
|
||||
@@ -2018,8 +1918,6 @@ public sealed class GameService : IGameService
|
||||
|
||||
private const int CampaignLogHistoryWindowSize = 100;
|
||||
private const int CampaignLogLivePageSize = 25;
|
||||
private const int DefaultCustomD6WildDice = 1;
|
||||
private const bool DefaultCustomD6AllowFumble = true;
|
||||
private const string CustomRollBreakdownSeparator = " => ";
|
||||
private static readonly Guid CustomRollSkillId = Guid.Empty;
|
||||
private const string CustomRollLabel = "Custom roll";
|
||||
|
||||
29
RpgRoller/Services/RoleSerializer.cs
Normal file
29
RpgRoller/Services/RoleSerializer.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
namespace RpgRoller.Services;
|
||||
|
||||
public static class RoleSerializer
|
||||
{
|
||||
public static IReadOnlyList<string> Parse(string serializedRoles)
|
||||
{
|
||||
return Normalize(serializedRoles.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries));
|
||||
}
|
||||
|
||||
public static string Serialize(IReadOnlyList<string> roles)
|
||||
{
|
||||
return string.Join(",", Normalize(roles));
|
||||
}
|
||||
|
||||
public static string[] Normalize(IEnumerable<string> roles)
|
||||
{
|
||||
return roles
|
||||
.Where(role => !string.IsNullOrWhiteSpace(role))
|
||||
.Select(role => role.Trim().ToLowerInvariant())
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(role => role, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static bool HasRole(string serializedRoles, string role)
|
||||
{
|
||||
return Parse(serializedRoles).Contains(role, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
17
RpgRoller/Services/RollVisibilityParser.cs
Normal file
17
RpgRoller/Services/RollVisibilityParser.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using RpgRoller.Domain;
|
||||
|
||||
namespace RpgRoller.Services;
|
||||
|
||||
public static class RollVisibilityParser
|
||||
{
|
||||
public static ServiceResult<RollVisibility> Parse(string visibility)
|
||||
{
|
||||
if (string.Equals(visibility, "public", StringComparison.OrdinalIgnoreCase))
|
||||
return ServiceResult<RollVisibility>.Success(RollVisibility.Public);
|
||||
|
||||
if (string.Equals(visibility, "private", StringComparison.OrdinalIgnoreCase))
|
||||
return ServiceResult<RollVisibility>.Success(RollVisibility.Private);
|
||||
|
||||
return ServiceResult<RollVisibility>.Failure("invalid_visibility", "Visibility must be 'public' or 'private'.");
|
||||
}
|
||||
}
|
||||
80
RpgRoller/Services/SkillDefinitionValidator.cs
Normal file
80
RpgRoller/Services/SkillDefinitionValidator.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
using RpgRoller.Domain;
|
||||
|
||||
namespace RpgRoller.Services;
|
||||
|
||||
public static class SkillDefinitionValidator
|
||||
{
|
||||
public static ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)> Validate(
|
||||
RulesetKind ruleset,
|
||||
string diceRollDefinition,
|
||||
int wildDice,
|
||||
bool allowFumble,
|
||||
int? fumbleRange)
|
||||
{
|
||||
var expressionValidation = DiceRules.ParseExpression(ruleset, diceRollDefinition);
|
||||
if (!expressionValidation.Succeeded)
|
||||
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)>.Failure(expressionValidation.Error!.Code, expressionValidation.Error.Message);
|
||||
|
||||
var optionsValidation = ValidateOptions(ruleset, expressionValidation.Value!, wildDice, allowFumble, fumbleRange);
|
||||
if (!optionsValidation.Succeeded)
|
||||
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)>.Failure(optionsValidation.Error!.Code, optionsValidation.Error.Message);
|
||||
|
||||
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)>.Success((
|
||||
expressionValidation.Value!.Canonical,
|
||||
optionsValidation.Value!.WildDice,
|
||||
optionsValidation.Value.AllowFumble,
|
||||
optionsValidation.Value.FumbleRange));
|
||||
}
|
||||
|
||||
private static ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)> ValidateOptions(
|
||||
RulesetKind ruleset,
|
||||
DiceExpression expression,
|
||||
int wildDice,
|
||||
bool allowFumble,
|
||||
int? fumbleRange)
|
||||
{
|
||||
if (wildDice < 0 || wildDice > 50)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_wild_dice", "Wild dice must be between 0 and 50.");
|
||||
|
||||
if (ruleset == RulesetKind.D6)
|
||||
{
|
||||
if (wildDice < 1)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_wild_dice", "D6 skills require at least one wild die.");
|
||||
|
||||
if (fumbleRange.HasValue)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_fumble_range", "Fumble range is only supported for Rolemaster open-ended percentile skills.");
|
||||
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Success((wildDice, allowFumble, null));
|
||||
}
|
||||
|
||||
if (ruleset == RulesetKind.Rolemaster)
|
||||
{
|
||||
if (wildDice != 0)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_wild_dice", "Rolemaster skills do not support wild dice.");
|
||||
|
||||
if (allowFumble)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_allow_fumble", "Rolemaster skills do not use the D6 allow-fumble option.");
|
||||
|
||||
if (expression.Kind == DiceExpressionKind.RolemasterOpenEndedPercentile)
|
||||
{
|
||||
if (!fumbleRange.HasValue)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_fumble_range", "Rolemaster open-ended percentile skills require a fumble range.");
|
||||
|
||||
if (fumbleRange < 0 || fumbleRange >= 96)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_fumble_range", "Fumble range must be between 0 and 95.");
|
||||
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Success((0, false, fumbleRange));
|
||||
}
|
||||
|
||||
if (fumbleRange.HasValue)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_fumble_range", "Fumble range is only valid for Rolemaster open-ended percentile skills.");
|
||||
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Success((0, false, null));
|
||||
}
|
||||
|
||||
if (fumbleRange.HasValue)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_fumble_range", "Fumble range is only supported for Rolemaster open-ended percentile skills.");
|
||||
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Success((0, false, null));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user