Add Rolemaster ruleset parsing scaffolding
This commit is contained in:
@@ -47,6 +47,7 @@ Backend state persistence:
|
|||||||
Gameplay capabilities now include:
|
Gameplay capabilities now include:
|
||||||
|
|
||||||
- Instant skill filtering in the character panel (filters live while typing and hides non-matching skills/groups)
|
- Instant skill filtering in the character panel (filters live while typing and hides non-matching skills/groups)
|
||||||
|
- Supported campaign rulesets include D6 System, D&D 5e, and Rolemaster
|
||||||
- Skill groups per character with skill prototypes (create/edit/delete groups, assign/reassign skills, and prefill new skill forms from group defaults)
|
- Skill groups per character with skill prototypes (create/edit/delete groups, assign/reassign skills, and prefill new skill forms from group defaults)
|
||||||
- Skill and skill-group deletion flows
|
- Skill and skill-group deletion flows
|
||||||
- GM-driven character owner transfer within campaign management flows
|
- GM-driven character owner transfer within campaign management flows
|
||||||
@@ -61,6 +62,7 @@ Gameplay capabilities now include:
|
|||||||
- Campaign management supports character deletion by character owner or admin
|
- Campaign management supports character deletion by character owner or admin
|
||||||
- Shared top header control across all authenticated workspace screens (play, campaign management, admin)
|
- Shared top header control across all authenticated workspace screens (play, campaign management, admin)
|
||||||
- Admin user management is integrated into workspace screen toggles (`Play`, `Campaign Management`, `Admin`)
|
- Admin user management is integrated into workspace screen toggles (`Play`, `Campaign Management`, `Admin`)
|
||||||
|
- Rolemaster expression validation currently recognizes `2d10+48`, `d100+4`, and `d100!+85`, including Rolemaster-only negative modifiers such as `d100-15`
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,20 @@ public sealed class CampaignApiTests : ApiTestBase
|
|||||||
Assert.Equal(otherCampaign.Id, updatedCharacter.CampaignId);
|
Assert.Equal(otherCampaign.Id, updatedCharacter.CampaignId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CampaignCreation_AcceptsRolemasterRuleset()
|
||||||
|
{
|
||||||
|
using var factory = CreateFactory(2, 2, 2);
|
||||||
|
using var gmClient = factory.CreateClient(new() { AllowAutoRedirect = false });
|
||||||
|
|
||||||
|
await RegisterAsync(gmClient, "gm-rm-api", "Password123", "Game Master");
|
||||||
|
await LoginAsync(gmClient, "gm-rm-api", "Password123");
|
||||||
|
|
||||||
|
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(gmClient, "/api/campaigns", new("Shadow World", "rolemaster"));
|
||||||
|
|
||||||
|
Assert.Equal("rolemaster", campaign.RulesetId);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SkillGroupsAndOwnerTransfer_WorkThroughApi()
|
public async Task SkillGroupsAndOwnerTransfer_WorkThroughApi()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ public sealed class SystemApiTests : ApiTestBase
|
|||||||
using var client = factory.CreateClient(new() { AllowAutoRedirect = false });
|
using var client = factory.CreateClient(new() { AllowAutoRedirect = false });
|
||||||
|
|
||||||
var rulesets = await GetAsync<IReadOnlyList<RulesetDefinition>>(client, "/api/rulesets");
|
var rulesets = await GetAsync<IReadOnlyList<RulesetDefinition>>(client, "/api/rulesets");
|
||||||
Assert.Equal(2, rulesets.Count);
|
Assert.Equal(3, rulesets.Count);
|
||||||
|
var rolemaster = Assert.Single(rulesets, ruleset => ruleset.Id == "rolemaster");
|
||||||
|
Assert.Equal("Rolemaster", rolemaster.Name);
|
||||||
|
|
||||||
await RegisterAsync(client, "sse", "Password123", "Sse User");
|
await RegisterAsync(client, "sse", "Password123", "Sse User");
|
||||||
await LoginAsync(client, "sse", "Password123");
|
await LoginAsync(client, "sse", "Password123");
|
||||||
|
|||||||
@@ -7,28 +7,49 @@ public sealed class DiceRulesTests
|
|||||||
{
|
{
|
||||||
Assert.Equal(RulesetKind.D6, DiceRules.TryParseRulesetId("d6"));
|
Assert.Equal(RulesetKind.D6, DiceRules.TryParseRulesetId("d6"));
|
||||||
Assert.Equal(RulesetKind.Dnd5e, DiceRules.TryParseRulesetId("dnd5e"));
|
Assert.Equal(RulesetKind.Dnd5e, DiceRules.TryParseRulesetId("dnd5e"));
|
||||||
|
Assert.Equal(RulesetKind.Rolemaster, DiceRules.TryParseRulesetId("rolemaster"));
|
||||||
Assert.Null(DiceRules.TryParseRulesetId("unknown"));
|
Assert.Null(DiceRules.TryParseRulesetId("unknown"));
|
||||||
|
|
||||||
var d6 = DiceRules.ParseExpression(RulesetKind.D6, "5D+4");
|
var d6 = DiceRules.ParseExpression(RulesetKind.D6, "5D+4");
|
||||||
var dnd = DiceRules.ParseExpression(RulesetKind.Dnd5e, "2d12+2");
|
var dnd = DiceRules.ParseExpression(RulesetKind.Dnd5e, "2d12+2");
|
||||||
|
var rolemasterInitiative = DiceRules.ParseExpression(RulesetKind.Rolemaster, "2d10-15");
|
||||||
|
var rolemasterPercentile = DiceRules.ParseExpression(RulesetKind.Rolemaster, "d100+4");
|
||||||
|
var rolemasterOpenEnded = DiceRules.ParseExpression(RulesetKind.Rolemaster, "1d100!+85");
|
||||||
var emptyExpression = DiceRules.ParseExpression(RulesetKind.Dnd5e, "");
|
var emptyExpression = DiceRules.ParseExpression(RulesetKind.Dnd5e, "");
|
||||||
var badFormat = DiceRules.ParseExpression(RulesetKind.Dnd5e, "abc");
|
var badFormat = DiceRules.ParseExpression(RulesetKind.Dnd5e, "abc");
|
||||||
var tooManyDice = DiceRules.ParseExpression(RulesetKind.D6, "51D+1");
|
var tooManyDice = DiceRules.ParseExpression(RulesetKind.D6, "51D+1");
|
||||||
var tooManySides = DiceRules.ParseExpression(RulesetKind.Dnd5e, "1d1001");
|
var tooManySides = DiceRules.ParseExpression(RulesetKind.Dnd5e, "1d1001");
|
||||||
var tooLargeModifier = DiceRules.ParseExpression(RulesetKind.Dnd5e, "1d20+1001");
|
var tooLargeModifier = DiceRules.ParseExpression(RulesetKind.Dnd5e, "1d20+1001");
|
||||||
|
var negativeDndModifier = DiceRules.ParseExpression(RulesetKind.Dnd5e, "1d20-1");
|
||||||
|
var invalidRolemasterFormat = DiceRules.ParseExpression(RulesetKind.Rolemaster, "2d12+1");
|
||||||
|
var tooNegativeRolemasterModifier = DiceRules.ParseExpression(RulesetKind.Rolemaster, "d100-1001");
|
||||||
var unknownRulesetExpression = DiceRules.ParseExpression((RulesetKind)99, "1d20+1");
|
var unknownRulesetExpression = DiceRules.ParseExpression((RulesetKind)99, "1d20+1");
|
||||||
|
|
||||||
Assert.True(d6.Succeeded);
|
Assert.True(d6.Succeeded);
|
||||||
Assert.True(dnd.Succeeded);
|
Assert.True(dnd.Succeeded);
|
||||||
|
Assert.True(rolemasterInitiative.Succeeded);
|
||||||
|
Assert.True(rolemasterPercentile.Succeeded);
|
||||||
|
Assert.True(rolemasterOpenEnded.Succeeded);
|
||||||
Assert.False(emptyExpression.Succeeded);
|
Assert.False(emptyExpression.Succeeded);
|
||||||
Assert.False(badFormat.Succeeded);
|
Assert.False(badFormat.Succeeded);
|
||||||
Assert.False(tooManyDice.Succeeded);
|
Assert.False(tooManyDice.Succeeded);
|
||||||
Assert.False(tooManySides.Succeeded);
|
Assert.False(tooManySides.Succeeded);
|
||||||
Assert.False(tooLargeModifier.Succeeded);
|
Assert.False(tooLargeModifier.Succeeded);
|
||||||
|
Assert.False(negativeDndModifier.Succeeded);
|
||||||
|
Assert.False(invalidRolemasterFormat.Succeeded);
|
||||||
|
Assert.False(tooNegativeRolemasterModifier.Succeeded);
|
||||||
Assert.False(unknownRulesetExpression.Succeeded);
|
Assert.False(unknownRulesetExpression.Succeeded);
|
||||||
|
|
||||||
|
Assert.Equal("2d10-15", rolemasterInitiative.Value!.Canonical);
|
||||||
|
Assert.Equal(DiceExpressionKind.RolemasterInitiative, rolemasterInitiative.Value.Kind);
|
||||||
|
Assert.Equal("d100+4", rolemasterPercentile.Value!.Canonical);
|
||||||
|
Assert.Equal(DiceExpressionKind.RolemasterPercentile, rolemasterPercentile.Value.Kind);
|
||||||
|
Assert.Equal("d100!+85", rolemasterOpenEnded.Value!.Canonical);
|
||||||
|
Assert.Equal(DiceExpressionKind.RolemasterOpenEndedPercentile, rolemasterOpenEnded.Value.Kind);
|
||||||
|
|
||||||
Assert.Equal("d6", DiceRules.ToRulesetId(RulesetKind.D6));
|
Assert.Equal("d6", DiceRules.ToRulesetId(RulesetKind.D6));
|
||||||
Assert.Equal("dnd5e", DiceRules.ToRulesetId(RulesetKind.Dnd5e));
|
Assert.Equal("dnd5e", DiceRules.ToRulesetId(RulesetKind.Dnd5e));
|
||||||
|
Assert.Equal("rolemaster", DiceRules.ToRulesetId(RulesetKind.Rolemaster));
|
||||||
Assert.Throws<ArgumentOutOfRangeException>(() => DiceRules.ToRulesetId((RulesetKind)99));
|
Assert.Throws<ArgumentOutOfRangeException>(() => DiceRules.ToRulesetId((RulesetKind)99));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ public sealed class ServiceCampaignTests
|
|||||||
service.Register("gm", "Password123", "GM");
|
service.Register("gm", "Password123", "GM");
|
||||||
var gmSession = ServiceTestSupport.GetValue(service.Login("gm", "Password123")).SessionToken;
|
var gmSession = ServiceTestSupport.GetValue(service.Login("gm", "Password123")).SessionToken;
|
||||||
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Name", "d6"));
|
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Name", "d6"));
|
||||||
|
var rolemasterCampaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Rolemaster Name", "rolemaster"));
|
||||||
|
|
||||||
var invalidRuleset = service.CreateCampaign(gmSession, "Name 2", "unknown");
|
var invalidRuleset = service.CreateCampaign(gmSession, "Name 2", "unknown");
|
||||||
Assert.False(invalidRuleset.Succeeded);
|
Assert.False(invalidRuleset.Succeeded);
|
||||||
|
Assert.Equal("rolemaster", rolemasterCampaign.RulesetId);
|
||||||
|
|
||||||
var noCampaignCharacter = service.CreateCharacter(gmSession, "Hero", Guid.NewGuid());
|
var noCampaignCharacter = service.CreateCharacter(gmSession, "Hero", Guid.NewGuid());
|
||||||
Assert.False(noCampaignCharacter.Succeeded);
|
Assert.False(noCampaignCharacter.Succeeded);
|
||||||
|
|||||||
@@ -174,4 +174,40 @@ public sealed class ServiceSkillGroupAndOwnershipTests
|
|||||||
var campaignAfterDeletes = ServiceTestSupport.GetValue(service.GetCampaign(gmSession, campaign.Id));
|
var campaignAfterDeletes = ServiceTestSupport.GetValue(service.GetCampaign(gmSession, campaign.Id));
|
||||||
Assert.Empty(campaignAfterDeletes.Characters);
|
Assert.Empty(campaignAfterDeletes.Characters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RolemasterSkillDefinitions_CanonicalizeAndKeepLegacyNegativeModifierRules()
|
||||||
|
{
|
||||||
|
using var harness = ServiceTestSupport.CreateHarness();
|
||||||
|
var service = harness.Service;
|
||||||
|
|
||||||
|
service.Register("gm-rm", "Password123", "GM");
|
||||||
|
service.Register("owner-rm", "Password123", "Owner");
|
||||||
|
|
||||||
|
var gmSession = ServiceTestSupport.GetValue(service.Login("gm-rm", "Password123")).SessionToken;
|
||||||
|
var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-rm", "Password123")).SessionToken;
|
||||||
|
|
||||||
|
var rolemasterCampaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Shadow World", "rolemaster"));
|
||||||
|
var dndCampaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Forgotten Realms", "dnd5e"));
|
||||||
|
var rolemasterCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Harn", rolemasterCampaign.Id));
|
||||||
|
var dndCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Mage", dndCampaign.Id));
|
||||||
|
|
||||||
|
var negativeDndSkill = service.CreateSkill(ownerSession, dndCharacter.Id, "Invalid", "1d20-1", 0, false);
|
||||||
|
Assert.False(negativeDndSkill.Succeeded);
|
||||||
|
|
||||||
|
var rolemasterGroup = ServiceTestSupport.GetValue(service.CreateSkillGroup(ownerSession, rolemasterCharacter.Id, "Initiative", "2d10-15", 3, true));
|
||||||
|
Assert.Equal("2d10-15", rolemasterGroup.DiceRollDefinition);
|
||||||
|
Assert.Equal(0, rolemasterGroup.WildDice);
|
||||||
|
Assert.False(rolemasterGroup.AllowFumble);
|
||||||
|
|
||||||
|
var percentileSkill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, rolemasterCharacter.Id, "Perception", "1d100-20", 4, true));
|
||||||
|
Assert.Equal("d100-20", percentileSkill.DiceRollDefinition);
|
||||||
|
Assert.Equal(0, percentileSkill.WildDice);
|
||||||
|
Assert.False(percentileSkill.AllowFumble);
|
||||||
|
|
||||||
|
var openEndedSkill = ServiceTestSupport.GetValue(service.UpdateSkill(ownerSession, percentileSkill.Id, "Perception", "1d100!+85", 5, true));
|
||||||
|
Assert.Equal("d100!+85", openEndedSkill.DiceRollDefinition);
|
||||||
|
Assert.Equal(0, openEndedSkill.WildDice);
|
||||||
|
Assert.False(openEndedSkill.AllowFumble);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ namespace RpgRoller.Domain;
|
|||||||
public enum RulesetKind
|
public enum RulesetKind
|
||||||
{
|
{
|
||||||
D6,
|
D6,
|
||||||
Dnd5e
|
Dnd5e,
|
||||||
|
Rolemaster
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum RollVisibility
|
public enum RollVisibility
|
||||||
@@ -87,4 +88,12 @@ public sealed class RollLogEntry
|
|||||||
public required DateTimeOffset TimestampUtc { get; init; }
|
public required DateTimeOffset TimestampUtc { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record DiceExpression(int DiceCount, int Sides, int Modifier, string Canonical);
|
public enum DiceExpressionKind
|
||||||
|
{
|
||||||
|
Standard,
|
||||||
|
RolemasterInitiative,
|
||||||
|
RolemasterPercentile,
|
||||||
|
RolemasterOpenEndedPercentile
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed record DiceExpression(int DiceCount, int Sides, int Modifier, string Canonical, DiceExpressionKind Kind = DiceExpressionKind.Standard);
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ public static partial class DiceRules
|
|||||||
if (string.Equals(rulesetId, "dnd5e", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(rulesetId, "dnd5e", StringComparison.OrdinalIgnoreCase))
|
||||||
return RulesetKind.Dnd5e;
|
return RulesetKind.Dnd5e;
|
||||||
|
|
||||||
|
if (string.Equals(rulesetId, "rolemaster", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return RulesetKind.Rolemaster;
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,9 +23,10 @@ public static partial class DiceRules
|
|||||||
{
|
{
|
||||||
return ruleset switch
|
return ruleset switch
|
||||||
{
|
{
|
||||||
RulesetKind.D6 => "d6",
|
RulesetKind.D6 => "d6",
|
||||||
RulesetKind.Dnd5e => "dnd5e",
|
RulesetKind.Dnd5e => "dnd5e",
|
||||||
_ => throw new ArgumentOutOfRangeException(nameof(ruleset), ruleset, "Unknown ruleset.")
|
RulesetKind.Rolemaster => "rolemaster",
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(ruleset), ruleset, "Unknown ruleset.")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,9 +38,10 @@ public static partial class DiceRules
|
|||||||
var trimmed = expression.Trim();
|
var trimmed = expression.Trim();
|
||||||
return ruleset switch
|
return ruleset switch
|
||||||
{
|
{
|
||||||
RulesetKind.D6 => ParseD6(trimmed),
|
RulesetKind.D6 => ParseD6(trimmed),
|
||||||
RulesetKind.Dnd5e => ParseDnd5e(trimmed),
|
RulesetKind.Dnd5e => ParseDnd5e(trimmed),
|
||||||
_ => ServiceResult<DiceExpression>.Failure("invalid_ruleset", "Unknown ruleset.")
|
RulesetKind.Rolemaster => ParseRolemaster(trimmed),
|
||||||
|
_ => ServiceResult<DiceExpression>.Failure("invalid_ruleset", "Unknown ruleset.")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +76,37 @@ public static partial class DiceRules
|
|||||||
return ServiceResult<DiceExpression>.Success(new(diceCount, sides, modifier, $"{diceCount}d{sides}{FormatModifier(modifier)}"));
|
return ServiceResult<DiceExpression>.Success(new(diceCount, sides, modifier, $"{diceCount}d{sides}{FormatModifier(modifier)}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ServiceResult<bool> ValidateDiceParts(int diceCount, int sides, int modifier)
|
private static ServiceResult<DiceExpression> ParseRolemaster(string expression)
|
||||||
|
{
|
||||||
|
var initiativeMatch = RolemasterInitiativeRegex().Match(expression);
|
||||||
|
if (initiativeMatch.Success)
|
||||||
|
{
|
||||||
|
var modifier = ParseModifier(initiativeMatch.Groups["modifier"].Value);
|
||||||
|
var validation = ValidateDiceParts(2, 10, modifier, -MaxModifier, MaxModifier);
|
||||||
|
if (!validation.Succeeded)
|
||||||
|
return ServiceResult<DiceExpression>.Failure(validation.Error!.Code, validation.Error.Message);
|
||||||
|
|
||||||
|
return ServiceResult<DiceExpression>.Success(new(2, 10, modifier, $"2d10{FormatModifier(modifier)}", DiceExpressionKind.RolemasterInitiative));
|
||||||
|
}
|
||||||
|
|
||||||
|
var percentileMatch = RolemasterPercentileRegex().Match(expression);
|
||||||
|
if (percentileMatch.Success)
|
||||||
|
{
|
||||||
|
var modifier = ParseModifier(percentileMatch.Groups["modifier"].Value);
|
||||||
|
var validation = ValidateDiceParts(1, 100, modifier, -MaxModifier, MaxModifier);
|
||||||
|
if (!validation.Succeeded)
|
||||||
|
return ServiceResult<DiceExpression>.Failure(validation.Error!.Code, validation.Error.Message);
|
||||||
|
|
||||||
|
var isOpenEnded = percentileMatch.Groups["openEnded"].Success;
|
||||||
|
var canonical = isOpenEnded ? $"d100!{FormatModifier(modifier)}" : $"d100{FormatModifier(modifier)}";
|
||||||
|
var kind = isOpenEnded ? DiceExpressionKind.RolemasterOpenEndedPercentile : DiceExpressionKind.RolemasterPercentile;
|
||||||
|
return ServiceResult<DiceExpression>.Success(new(1, 100, modifier, canonical, kind));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ServiceResult<DiceExpression>.Failure("invalid_expression", "Expected Rolemaster format like 2d10+48, d100+4, or d100!+85.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ServiceResult<bool> ValidateDiceParts(int diceCount, int sides, int modifier, int minModifier = 0, int maxModifier = MaxModifier)
|
||||||
{
|
{
|
||||||
if (diceCount < 1 || diceCount > MaxDiceCount)
|
if (diceCount < 1 || diceCount > MaxDiceCount)
|
||||||
return ServiceResult<bool>.Failure("invalid_expression", $"Dice count must be between 1 and {MaxDiceCount}.");
|
return ServiceResult<bool>.Failure("invalid_expression", $"Dice count must be between 1 and {MaxDiceCount}.");
|
||||||
@@ -79,8 +114,8 @@ public static partial class DiceRules
|
|||||||
if (sides < 2 || sides > MaxSides)
|
if (sides < 2 || sides > MaxSides)
|
||||||
return ServiceResult<bool>.Failure("invalid_expression", $"Dice sides must be between 2 and {MaxSides}.");
|
return ServiceResult<bool>.Failure("invalid_expression", $"Dice sides must be between 2 and {MaxSides}.");
|
||||||
|
|
||||||
if (modifier < 0 || modifier > MaxModifier)
|
if (modifier < minModifier || modifier > maxModifier)
|
||||||
return ServiceResult<bool>.Failure("invalid_expression", $"Modifier must be between 0 and {MaxModifier}.");
|
return ServiceResult<bool>.Failure("invalid_expression", $"Modifier must be between {minModifier} and {maxModifier}.");
|
||||||
|
|
||||||
return ServiceResult<bool>.Success(true);
|
return ServiceResult<bool>.Success(true);
|
||||||
}
|
}
|
||||||
@@ -92,7 +127,12 @@ public static partial class DiceRules
|
|||||||
|
|
||||||
private static string FormatModifier(int modifier)
|
private static string FormatModifier(int modifier)
|
||||||
{
|
{
|
||||||
return modifier > 0 ? $"+{modifier}" : string.Empty;
|
return modifier switch
|
||||||
|
{
|
||||||
|
> 0 => $"+{modifier}",
|
||||||
|
< 0 => modifier.ToString(),
|
||||||
|
_ => string.Empty
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
[GeneratedRegex("^(?<count>\\d+)D(?:\\+(?<modifier>\\d+))?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)]
|
[GeneratedRegex("^(?<count>\\d+)D(?:\\+(?<modifier>\\d+))?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)]
|
||||||
@@ -101,6 +141,12 @@ public static partial class DiceRules
|
|||||||
[GeneratedRegex("^(?<count>\\d+)d(?<sides>\\d+)(?:\\+(?<modifier>\\d+))?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)]
|
[GeneratedRegex("^(?<count>\\d+)d(?<sides>\\d+)(?:\\+(?<modifier>\\d+))?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)]
|
||||||
private static partial Regex Dnd5eRegex();
|
private static partial Regex Dnd5eRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex("^2d10(?<modifier>[+-]\\d+)?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)]
|
||||||
|
private static partial Regex RolemasterInitiativeRegex();
|
||||||
|
|
||||||
|
[GeneratedRegex("^(?:1)?d100(?<openEnded>!)?(?<modifier>[+-]\\d+)?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)]
|
||||||
|
private static partial Regex RolemasterPercentileRegex();
|
||||||
|
|
||||||
private const int MaxDiceCount = 50;
|
private const int MaxDiceCount = 50;
|
||||||
private const int MaxSides = 1000;
|
private const int MaxSides = 1000;
|
||||||
private const int MaxModifier = 1000;
|
private const int MaxModifier = 1000;
|
||||||
@@ -108,6 +154,7 @@ public static partial class DiceRules
|
|||||||
public static readonly IReadOnlyList<(RulesetKind Kind, string Id, string Name, string DiceSyntax)> SupportedRulesets =
|
public static readonly IReadOnlyList<(RulesetKind Kind, string Id, string Name, string DiceSyntax)> SupportedRulesets =
|
||||||
[
|
[
|
||||||
(RulesetKind.D6, "d6", "D6 System", "countD(+modifier), e.g. 5D+4"),
|
(RulesetKind.D6, "d6", "D6 System", "countD(+modifier), e.g. 5D+4"),
|
||||||
(RulesetKind.Dnd5e, "dnd5e", "D&D 5e", "countdSides(+modifier), e.g. 2d12+2")
|
(RulesetKind.Dnd5e, "dnd5e", "D&D 5e", "countdSides(+modifier), e.g. 2d12+2"),
|
||||||
|
(RulesetKind.Rolemaster, "rolemaster", "Rolemaster", "2d10+48, d100+4, d100!+85")
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user