Generalize Rolemaster standard dice parsing
This commit is contained in:
@@ -62,10 +62,10 @@ 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`
|
- Rolemaster expression validation now accepts generic standard expressions such as `d10`, `2d10+48`, `15d10`, and `d100-15`; `d100!+85` remains the special open-ended percentile form
|
||||||
- Rolemaster open-ended percentile skills and skill-group defaults now persist a nullable `FumbleRange` field, while D6 and D&D rows migrate forward unchanged
|
- Rolemaster open-ended percentile skills and skill-group defaults now persist a nullable `FumbleRange` field, while D6 and D&D rows migrate forward unchanged
|
||||||
- Rolemaster create/edit forms now expose a ruleset-aware roll-type selector with synchronized canonical expressions and conditional `FumbleRange` inputs for open-ended percentile skills and skill groups
|
- Rolemaster create/edit forms now keep the expression authoritative, show generic Rolemaster syntax help, and reveal `FumbleRange` only when the expression is an open-ended percentile roll
|
||||||
- Rolemaster roll execution now supports initiative (`2d10+x`), standard percentile (`d100+x`), and open-ended percentile (`d100!+x`) with recursive high-end chaining and low-end subtraction based on `FumbleRange`; low-end trigger rolls are shown for auditability but do not count toward the total
|
- Rolemaster roll execution now supports generic standard Rolemaster rolls (`NdS+x`, with implicit count `1` for `dS`) plus open-ended percentile (`d100!+x`) with recursive high-end chaining and low-end subtraction based on `FumbleRange`; low-end trigger rolls are shown for auditability but do not count toward the total
|
||||||
- Compact campaign-log summaries stay dense for Rolemaster rolls, while lazy-loaded roll detail includes ordered die metadata for each open-ended follow-up step
|
- Compact campaign-log summaries stay dense for Rolemaster rolls, while lazy-loaded roll detail includes ordered die metadata for each open-ended follow-up step
|
||||||
- Startup migration coverage is validated against a copied temp-file instance of `RpgRoller/App_Data/rpgroller.development.db` before feature work is considered complete
|
- Startup migration coverage is validated against a copied temp-file instance of `RpgRoller/App_Data/rpgroller.development.db` before feature work is considered complete
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ public sealed class RolemasterApiTests : ApiTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task RolemasterRollEndpoints_ExecuteInitiativeAndPercentile()
|
public async Task RolemasterRollEndpoints_ExecuteGenericRolemasterExpressions()
|
||||||
{
|
{
|
||||||
using var factory = CreateFactory(8, 6, 74);
|
using var factory = CreateFactory(8, 6, 74);
|
||||||
using var client = factory.CreateClient(new() { AllowAutoRedirect = false });
|
using var client = factory.CreateClient(new() { AllowAutoRedirect = false });
|
||||||
@@ -17,7 +17,7 @@ public sealed class RolemasterApiTests : ApiTestBase
|
|||||||
|
|
||||||
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(client, "/api/campaigns", new("Rolemaster", "rolemaster"));
|
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(client, "/api/campaigns", new("Rolemaster", "rolemaster"));
|
||||||
var character = await PostAsync<CreateCharacterRequest, CharacterSummary>(client, "/api/characters", new("Hero", campaign.Id));
|
var character = await PostAsync<CreateCharacterRequest, CharacterSummary>(client, "/api/characters", new("Hero", campaign.Id));
|
||||||
var initiative = await PostAsync<CreateSkillRequest, SkillSummary>(client, $"/api/characters/{character.Id}/skills", new("Initiative", "2d10+48", 0, false));
|
var initiative = await PostAsync<CreateSkillRequest, SkillSummary>(client, $"/api/characters/{character.Id}/skills", new("Healing", "2d10+48", 0, false));
|
||||||
var perception = await PostAsync<CreateSkillRequest, SkillSummary>(client, $"/api/characters/{character.Id}/skills", new("Perception", "d100-15", 0, false));
|
var perception = await PostAsync<CreateSkillRequest, SkillSummary>(client, $"/api/characters/{character.Id}/skills", new("Perception", "d100-15", 0, false));
|
||||||
|
|
||||||
var initiativeRoll = await PostAsync<RollSkillRequest, RollResult>(client, $"/api/skills/{initiative.Id}/roll", new("public"));
|
var initiativeRoll = await PostAsync<RollSkillRequest, RollResult>(client, $"/api/skills/{initiative.Id}/roll", new("public"));
|
||||||
@@ -26,15 +26,15 @@ public sealed class RolemasterApiTests : ApiTestBase
|
|||||||
|
|
||||||
Assert.Equal(62, initiativeRoll.Result);
|
Assert.Equal(62, initiativeRoll.Result);
|
||||||
Assert.Equal("8+6+48=62", initiativeRoll.Breakdown);
|
Assert.Equal("8+6+48=62", initiativeRoll.Breakdown);
|
||||||
Assert.All(initiativeRoll.Dice, die => Assert.Equal(RollDieKinds.RolemasterInitiative, die.Kind));
|
Assert.All(initiativeRoll.Dice, die => Assert.Equal(RollDieKinds.RolemasterStandard, die.Kind));
|
||||||
|
|
||||||
Assert.Equal(59, percentileRoll.Result);
|
Assert.Equal(59, percentileRoll.Result);
|
||||||
Assert.Equal("74-15=59", percentileRoll.Breakdown);
|
Assert.Equal("74-15=59", percentileRoll.Breakdown);
|
||||||
Assert.Equal(RollDieKinds.RolemasterPercentile, Assert.Single(percentileRoll.Dice).Kind);
|
Assert.Equal(RollDieKinds.RolemasterStandard, Assert.Single(percentileRoll.Dice).Kind);
|
||||||
|
|
||||||
Assert.Equal(2, logPage.Entries.Length);
|
Assert.Equal(2, logPage.Entries.Length);
|
||||||
Assert.Equal("8 + 6 | initiative", logPage.Entries[0].SummaryText);
|
Assert.Equal("8 + 6 | rolemaster", logPage.Entries[0].SummaryText);
|
||||||
Assert.Equal("74 | percentile", logPage.Entries[1].SummaryText);
|
Assert.Equal("74 | rolemaster", logPage.Entries[1].SummaryText);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ public sealed class DiceRulesTests
|
|||||||
|
|
||||||
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 rolemasterImplicitSingle = DiceRules.ParseExpression(RulesetKind.Rolemaster, "d10");
|
||||||
|
var rolemasterManyDice = DiceRules.ParseExpression(RulesetKind.Rolemaster, "15d10-15");
|
||||||
var rolemasterPercentile = DiceRules.ParseExpression(RulesetKind.Rolemaster, "d100+4");
|
var rolemasterPercentile = DiceRules.ParseExpression(RulesetKind.Rolemaster, "d100+4");
|
||||||
var rolemasterOpenEnded = DiceRules.ParseExpression(RulesetKind.Rolemaster, "1d100!+85");
|
var rolemasterOpenEnded = DiceRules.ParseExpression(RulesetKind.Rolemaster, "1d100!+85");
|
||||||
var emptyExpression = DiceRules.ParseExpression(RulesetKind.Dnd5e, "");
|
var emptyExpression = DiceRules.ParseExpression(RulesetKind.Dnd5e, "");
|
||||||
@@ -21,13 +22,14 @@ public sealed class DiceRulesTests
|
|||||||
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 negativeDndModifier = DiceRules.ParseExpression(RulesetKind.Dnd5e, "1d20-1");
|
||||||
var invalidRolemasterFormat = DiceRules.ParseExpression(RulesetKind.Rolemaster, "2d12+1");
|
var invalidRolemasterOpenEndedFormat = DiceRules.ParseExpression(RulesetKind.Rolemaster, "2d10!+1");
|
||||||
var tooNegativeRolemasterModifier = DiceRules.ParseExpression(RulesetKind.Rolemaster, "d100-1001");
|
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(rolemasterImplicitSingle.Succeeded);
|
||||||
|
Assert.True(rolemasterManyDice.Succeeded);
|
||||||
Assert.True(rolemasterPercentile.Succeeded);
|
Assert.True(rolemasterPercentile.Succeeded);
|
||||||
Assert.True(rolemasterOpenEnded.Succeeded);
|
Assert.True(rolemasterOpenEnded.Succeeded);
|
||||||
Assert.False(emptyExpression.Succeeded);
|
Assert.False(emptyExpression.Succeeded);
|
||||||
@@ -36,14 +38,16 @@ public sealed class DiceRulesTests
|
|||||||
Assert.False(tooManySides.Succeeded);
|
Assert.False(tooManySides.Succeeded);
|
||||||
Assert.False(tooLargeModifier.Succeeded);
|
Assert.False(tooLargeModifier.Succeeded);
|
||||||
Assert.False(negativeDndModifier.Succeeded);
|
Assert.False(negativeDndModifier.Succeeded);
|
||||||
Assert.False(invalidRolemasterFormat.Succeeded);
|
Assert.False(invalidRolemasterOpenEndedFormat.Succeeded);
|
||||||
Assert.False(tooNegativeRolemasterModifier.Succeeded);
|
Assert.False(tooNegativeRolemasterModifier.Succeeded);
|
||||||
Assert.False(unknownRulesetExpression.Succeeded);
|
Assert.False(unknownRulesetExpression.Succeeded);
|
||||||
|
|
||||||
Assert.Equal("2d10-15", rolemasterInitiative.Value!.Canonical);
|
Assert.Equal("d10", rolemasterImplicitSingle.Value!.Canonical);
|
||||||
Assert.Equal(DiceExpressionKind.RolemasterInitiative, rolemasterInitiative.Value.Kind);
|
Assert.Equal(DiceExpressionKind.RolemasterStandard, rolemasterImplicitSingle.Value.Kind);
|
||||||
|
Assert.Equal("15d10-15", rolemasterManyDice.Value!.Canonical);
|
||||||
|
Assert.Equal(DiceExpressionKind.RolemasterStandard, rolemasterManyDice.Value.Kind);
|
||||||
Assert.Equal("d100+4", rolemasterPercentile.Value!.Canonical);
|
Assert.Equal("d100+4", rolemasterPercentile.Value!.Canonical);
|
||||||
Assert.Equal(DiceExpressionKind.RolemasterPercentile, rolemasterPercentile.Value.Kind);
|
Assert.Equal(DiceExpressionKind.RolemasterStandard, rolemasterPercentile.Value.Kind);
|
||||||
Assert.Equal("d100!+85", rolemasterOpenEnded.Value!.Canonical);
|
Assert.Equal("d100!+85", rolemasterOpenEnded.Value!.Canonical);
|
||||||
Assert.Equal(DiceExpressionKind.RolemasterOpenEndedPercentile, rolemasterOpenEnded.Value.Kind);
|
Assert.Equal(DiceExpressionKind.RolemasterOpenEndedPercentile, rolemasterOpenEnded.Value.Kind);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ namespace RpgRoller.Tests;
|
|||||||
public sealed class ServiceRolemasterRollTests
|
public sealed class ServiceRolemasterRollTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RollSkill_RolemasterInitiative_ComputesTotalAndTagsDice()
|
public void RollSkill_RolemasterStandardMultiDie_ComputesTotalAndTagsDice()
|
||||||
{
|
{
|
||||||
using var harness = ServiceTestSupport.CreateHarness(7, 10);
|
using var harness = ServiceTestSupport.CreateHarness(7, 10);
|
||||||
var service = harness.Service;
|
var service = harness.Service;
|
||||||
@@ -12,34 +12,34 @@ public sealed class ServiceRolemasterRollTests
|
|||||||
var session = ServiceTestSupport.GetValue(service.Login("gm-init", "Password123")).SessionToken;
|
var session = ServiceTestSupport.GetValue(service.Login("gm-init", "Password123")).SessionToken;
|
||||||
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(session, "Rolemaster", "rolemaster"));
|
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(session, "Rolemaster", "rolemaster"));
|
||||||
var character = ServiceTestSupport.GetValue(service.CreateCharacter(session, "Hero", campaign.Id));
|
var character = ServiceTestSupport.GetValue(service.CreateCharacter(session, "Hero", campaign.Id));
|
||||||
var skill = ServiceTestSupport.GetValue(service.CreateSkill(session, character.Id, "Initiative", "2d10+48", 0, false));
|
var skill = ServiceTestSupport.GetValue(service.CreateSkill(session, character.Id, "Healing", "2d10+48", 0, false));
|
||||||
|
|
||||||
var roll = ServiceTestSupport.GetValue(service.RollSkill(session, skill.Id, "public"));
|
var roll = ServiceTestSupport.GetValue(service.RollSkill(session, skill.Id, "public"));
|
||||||
var logPage = ServiceTestSupport.GetValue(service.GetCampaignLogPage(session, campaign.Id, limit: 5));
|
var logPage = ServiceTestSupport.GetValue(service.GetCampaignLogPage(session, campaign.Id, limit: 5));
|
||||||
|
|
||||||
Assert.Equal(65, roll.Result);
|
Assert.Equal(65, roll.Result);
|
||||||
Assert.Equal("7+10+48=65", roll.Breakdown);
|
Assert.Equal("7+10+48=65", roll.Breakdown);
|
||||||
Assert.Equal("7 + 10 | initiative", Assert.Single(logPage.Entries).SummaryText);
|
Assert.Equal("7 + 10 | rolemaster", Assert.Single(logPage.Entries).SummaryText);
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
roll.Dice,
|
roll.Dice,
|
||||||
die =>
|
die =>
|
||||||
{
|
{
|
||||||
Assert.Equal(7, die.Roll);
|
Assert.Equal(7, die.Roll);
|
||||||
Assert.Equal(1, die.Sequence);
|
Assert.Equal(1, die.Sequence);
|
||||||
Assert.Equal(RollDieKinds.RolemasterInitiative, die.Kind);
|
Assert.Equal(RollDieKinds.RolemasterStandard, die.Kind);
|
||||||
Assert.Equal(7, die.SignedContribution);
|
Assert.Equal(7, die.SignedContribution);
|
||||||
},
|
},
|
||||||
die =>
|
die =>
|
||||||
{
|
{
|
||||||
Assert.Equal(10, die.Roll);
|
Assert.Equal(10, die.Roll);
|
||||||
Assert.Equal(2, die.Sequence);
|
Assert.Equal(2, die.Sequence);
|
||||||
Assert.Equal(RollDieKinds.RolemasterInitiative, die.Kind);
|
Assert.Equal(RollDieKinds.RolemasterStandard, die.Kind);
|
||||||
Assert.Equal(10, die.SignedContribution);
|
Assert.Equal(10, die.SignedContribution);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RollSkill_RolemasterPercentile_ComputesTotalAndTagsDice()
|
public void RollSkill_RolemasterStandardSingleDie_ComputesTotalAndTagsDice()
|
||||||
{
|
{
|
||||||
using var harness = ServiceTestSupport.CreateHarness(73);
|
using var harness = ServiceTestSupport.CreateHarness(73);
|
||||||
var service = harness.Service;
|
var service = harness.Service;
|
||||||
@@ -55,12 +55,12 @@ public sealed class ServiceRolemasterRollTests
|
|||||||
|
|
||||||
Assert.Equal(58, roll.Result);
|
Assert.Equal(58, roll.Result);
|
||||||
Assert.Equal("73-15=58", roll.Breakdown);
|
Assert.Equal("73-15=58", roll.Breakdown);
|
||||||
Assert.Equal("73 | percentile", Assert.Single(logPage.Entries).SummaryText);
|
Assert.Equal("73 | rolemaster", Assert.Single(logPage.Entries).SummaryText);
|
||||||
|
|
||||||
var die = Assert.Single(roll.Dice);
|
var die = Assert.Single(roll.Dice);
|
||||||
Assert.Equal(73, die.Roll);
|
Assert.Equal(73, die.Roll);
|
||||||
Assert.Equal(1, die.Sequence);
|
Assert.Equal(1, die.Sequence);
|
||||||
Assert.Equal(RollDieKinds.RolemasterPercentile, die.Kind);
|
Assert.Equal(RollDieKinds.RolemasterStandard, die.Kind);
|
||||||
Assert.Equal(73, die.SignedContribution);
|
Assert.Equal(73, die.SignedContribution);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,8 +48,6 @@ public sealed class SkillFormModel
|
|||||||
public int WildDice { get; set; }
|
public int WildDice { get; set; }
|
||||||
public bool AllowFumble { get; set; }
|
public bool AllowFumble { get; set; }
|
||||||
public int? FumbleRange { get; set; }
|
public int? FumbleRange { get; set; }
|
||||||
public string RolemasterRollType { get; set; } = HomeControls.RulesetFormHelpers.RolemasterRollTypes.Initiative;
|
|
||||||
public int RolemasterModifier { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class SkillGroupFormModel
|
public sealed class SkillGroupFormModel
|
||||||
@@ -60,8 +58,6 @@ public sealed class SkillGroupFormModel
|
|||||||
public int WildDice { get; set; }
|
public int WildDice { get; set; }
|
||||||
public bool AllowFumble { get; set; }
|
public bool AllowFumble { get; set; }
|
||||||
public int? FumbleRange { get; set; }
|
public int? FumbleRange { get; set; }
|
||||||
public string RolemasterRollType { get; set; } = HomeControls.RulesetFormHelpers.RolemasterRollTypes.Initiative;
|
|
||||||
public int RolemasterModifier { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum HomeViewMode
|
public enum HomeViewMode
|
||||||
|
|||||||
@@ -173,16 +173,6 @@
|
|||||||
}
|
}
|
||||||
else if (IsRolemasterRuleset)
|
else if (IsRolemasterRuleset)
|
||||||
{
|
{
|
||||||
<label for="skill-group-rolemaster-roll-type">Prototype roll type</label>
|
|
||||||
<select id="skill-group-rolemaster-roll-type" value="@SkillGroupState.Model.RolemasterRollType" @onchange="OnSkillGroupRolemasterRollTypeChanged">
|
|
||||||
<option value="@RulesetFormHelpers.RolemasterRollTypes.Initiative">Initiative</option>
|
|
||||||
<option value="@RulesetFormHelpers.RolemasterRollTypes.Percentile">Percentile</option>
|
|
||||||
<option value="@RulesetFormHelpers.RolemasterRollTypes.OpenEndedPercentile">Open-ended percentile</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label for="skill-group-rolemaster-modifier">Prototype modifier</label>
|
|
||||||
<input id="skill-group-rolemaster-modifier" type="number" step="1" value="@SkillGroupState.Model.RolemasterModifier" @oninput="OnSkillGroupRolemasterModifierChanged"/>
|
|
||||||
|
|
||||||
@if (IsSkillGroupRolemasterOpenEnded)
|
@if (IsSkillGroupRolemasterOpenEnded)
|
||||||
{
|
{
|
||||||
<label for="skill-group-fumble-range">Prototype fumble range</label>
|
<label for="skill-group-fumble-range">Prototype fumble range</label>
|
||||||
@@ -224,8 +214,6 @@
|
|||||||
SkillGroupInputId="skill-create-group"
|
SkillGroupInputId="skill-create-group"
|
||||||
WildDiceInputId="skill-create-wild-dice"
|
WildDiceInputId="skill-create-wild-dice"
|
||||||
AllowFumbleInputId="skill-create-allow-fumble"
|
AllowFumbleInputId="skill-create-allow-fumble"
|
||||||
RolemasterRollTypeInputId="skill-create-rolemaster-roll-type"
|
|
||||||
RolemasterModifierInputId="skill-create-rolemaster-modifier"
|
|
||||||
FumbleRangeInputId="skill-create-fumble-range"
|
FumbleRangeInputId="skill-create-fumble-range"
|
||||||
InitialModel="CreateSkillInitialModel"
|
InitialModel="CreateSkillInitialModel"
|
||||||
FormVersion="CreateSkillFormVersion"
|
FormVersion="CreateSkillFormVersion"
|
||||||
@@ -246,8 +234,6 @@
|
|||||||
SkillGroupInputId="skill-edit-group"
|
SkillGroupInputId="skill-edit-group"
|
||||||
WildDiceInputId="skill-edit-wild-dice"
|
WildDiceInputId="skill-edit-wild-dice"
|
||||||
AllowFumbleInputId="skill-edit-allow-fumble"
|
AllowFumbleInputId="skill-edit-allow-fumble"
|
||||||
RolemasterRollTypeInputId="skill-edit-rolemaster-roll-type"
|
|
||||||
RolemasterModifierInputId="skill-edit-rolemaster-modifier"
|
|
||||||
FumbleRangeInputId="skill-edit-fumble-range"
|
FumbleRangeInputId="skill-edit-fumble-range"
|
||||||
InitialModel="EditSkillInitialModel"
|
InitialModel="EditSkillInitialModel"
|
||||||
FormVersion="EditSkillFormVersion"
|
FormVersion="EditSkillFormVersion"
|
||||||
|
|||||||
@@ -22,13 +22,11 @@ public partial class CharacterPanel
|
|||||||
SkillGroupId = selectedGroup?.Id.ToString() ?? string.Empty,
|
SkillGroupId = selectedGroup?.Id.ToString() ?? string.Empty,
|
||||||
WildDice = selectedGroup?.WildDice ?? (IsD6Ruleset ? 1 : 0),
|
WildDice = selectedGroup?.WildDice ?? (IsD6Ruleset ? 1 : 0),
|
||||||
AllowFumble = selectedGroup?.AllowFumble ?? IsD6Ruleset,
|
AllowFumble = selectedGroup?.AllowFumble ?? IsD6Ruleset,
|
||||||
FumbleRange = selectedGroup?.FumbleRange,
|
FumbleRange = selectedGroup?.FumbleRange
|
||||||
RolemasterRollType = RulesetFormHelpers.InferRolemasterRollType(selectedGroup?.DiceRollDefinition),
|
|
||||||
RolemasterModifier = RulesetFormHelpers.InferRolemasterModifier(selectedGroup?.DiceRollDefinition)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (IsRolemasterRuleset && string.IsNullOrWhiteSpace(CreateSkillInitialModel.DiceRollDefinition))
|
if (IsRolemasterRuleset && string.IsNullOrWhiteSpace(CreateSkillInitialModel.DiceRollDefinition))
|
||||||
CreateSkillInitialModel.DiceRollDefinition = RulesetFormHelpers.BuildRolemasterExpression(CreateSkillInitialModel.RolemasterRollType, CreateSkillInitialModel.RolemasterModifier);
|
CreateSkillInitialModel.DiceRollDefinition = "d100";
|
||||||
|
|
||||||
CreateSkillFormVersion++;
|
CreateSkillFormVersion++;
|
||||||
ShowCreateSkillModal = true;
|
ShowCreateSkillModal = true;
|
||||||
@@ -45,9 +43,7 @@ public partial class CharacterPanel
|
|||||||
SkillGroupId = skill.SkillGroupId?.ToString() ?? string.Empty,
|
SkillGroupId = skill.SkillGroupId?.ToString() ?? string.Empty,
|
||||||
WildDice = skill.WildDice,
|
WildDice = skill.WildDice,
|
||||||
AllowFumble = skill.AllowFumble,
|
AllowFumble = skill.AllowFumble,
|
||||||
FumbleRange = skill.FumbleRange,
|
FumbleRange = skill.FumbleRange
|
||||||
RolemasterRollType = RulesetFormHelpers.InferRolemasterRollType(skill.DiceRollDefinition),
|
|
||||||
RolemasterModifier = RulesetFormHelpers.InferRolemasterModifier(skill.DiceRollDefinition)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
EditSkillFormVersion++;
|
EditSkillFormVersion++;
|
||||||
@@ -113,10 +109,8 @@ public partial class CharacterPanel
|
|||||||
SkillGroupState.Model.WildDice = IsD6Ruleset ? 1 : 0;
|
SkillGroupState.Model.WildDice = IsD6Ruleset ? 1 : 0;
|
||||||
SkillGroupState.Model.AllowFumble = IsD6Ruleset;
|
SkillGroupState.Model.AllowFumble = IsD6Ruleset;
|
||||||
SkillGroupState.Model.FumbleRange = null;
|
SkillGroupState.Model.FumbleRange = null;
|
||||||
SkillGroupState.Model.RolemasterRollType = RulesetFormHelpers.RolemasterRollTypes.Initiative;
|
|
||||||
SkillGroupState.Model.RolemasterModifier = 0;
|
|
||||||
if (IsRolemasterRuleset)
|
if (IsRolemasterRuleset)
|
||||||
SynchronizeSkillGroupExpression();
|
SkillGroupState.Model.DiceRollDefinition = "d100";
|
||||||
SkillGroupState.ResetValidation();
|
SkillGroupState.ResetValidation();
|
||||||
ShowCreateSkillGroupModal = true;
|
ShowCreateSkillGroupModal = true;
|
||||||
}
|
}
|
||||||
@@ -130,8 +124,6 @@ public partial class CharacterPanel
|
|||||||
SkillGroupState.Model.WildDice = skillGroup.WildDice;
|
SkillGroupState.Model.WildDice = skillGroup.WildDice;
|
||||||
SkillGroupState.Model.AllowFumble = skillGroup.AllowFumble;
|
SkillGroupState.Model.AllowFumble = skillGroup.AllowFumble;
|
||||||
SkillGroupState.Model.FumbleRange = skillGroup.FumbleRange;
|
SkillGroupState.Model.FumbleRange = skillGroup.FumbleRange;
|
||||||
SkillGroupState.Model.RolemasterRollType = RulesetFormHelpers.InferRolemasterRollType(skillGroup.DiceRollDefinition);
|
|
||||||
SkillGroupState.Model.RolemasterModifier = RulesetFormHelpers.InferRolemasterModifier(skillGroup.DiceRollDefinition);
|
|
||||||
NormalizeSkillGroupFumbleRange();
|
NormalizeSkillGroupFumbleRange();
|
||||||
SkillGroupState.ResetValidation();
|
SkillGroupState.ResetValidation();
|
||||||
ShowEditSkillGroupModal = true;
|
ShowEditSkillGroupModal = true;
|
||||||
@@ -325,37 +317,8 @@ public partial class CharacterPanel
|
|||||||
{
|
{
|
||||||
SkillGroupState.Model.DiceRollDefinition = args.Value?.ToString() ?? string.Empty;
|
SkillGroupState.Model.DiceRollDefinition = args.Value?.ToString() ?? string.Empty;
|
||||||
if (IsRolemasterRuleset)
|
if (IsRolemasterRuleset)
|
||||||
{
|
|
||||||
SkillGroupState.Model.RolemasterRollType = RulesetFormHelpers.InferRolemasterRollType(SkillGroupState.Model.DiceRollDefinition);
|
|
||||||
SkillGroupState.Model.RolemasterModifier = RulesetFormHelpers.InferRolemasterModifier(SkillGroupState.Model.DiceRollDefinition);
|
|
||||||
NormalizeSkillGroupFumbleRange();
|
NormalizeSkillGroupFumbleRange();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSkillGroupRolemasterRollTypeChanged(ChangeEventArgs args)
|
|
||||||
{
|
|
||||||
SkillGroupState.Model.RolemasterRollType = args.Value?.ToString() ?? RulesetFormHelpers.RolemasterRollTypes.Initiative;
|
|
||||||
NormalizeSkillGroupFumbleRange();
|
|
||||||
SynchronizeSkillGroupExpression();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSkillGroupRolemasterModifierChanged(ChangeEventArgs args)
|
|
||||||
{
|
|
||||||
var rawValue = args.Value?.ToString();
|
|
||||||
if (!int.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var modifier))
|
|
||||||
modifier = 0;
|
|
||||||
|
|
||||||
SkillGroupState.Model.RolemasterModifier = modifier;
|
|
||||||
SynchronizeSkillGroupExpression();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SynchronizeSkillGroupExpression()
|
|
||||||
{
|
|
||||||
if (!IsRolemasterRuleset)
|
|
||||||
return;
|
|
||||||
|
|
||||||
SkillGroupState.Model.DiceRollDefinition = RulesetFormHelpers.BuildRolemasterExpression(SkillGroupState.Model.RolemasterRollType, SkillGroupState.Model.RolemasterModifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void NormalizeSkillGroupFumbleRange()
|
private void NormalizeSkillGroupFumbleRange()
|
||||||
{
|
{
|
||||||
@@ -376,9 +339,9 @@ public partial class CharacterPanel
|
|||||||
|
|
||||||
private bool IsD6Ruleset => RulesetFormHelpers.IsD6(SelectedCampaignRulesetId);
|
private bool IsD6Ruleset => RulesetFormHelpers.IsD6(SelectedCampaignRulesetId);
|
||||||
private bool IsRolemasterRuleset => RulesetFormHelpers.IsRolemaster(SelectedCampaignRulesetId);
|
private bool IsRolemasterRuleset => RulesetFormHelpers.IsRolemaster(SelectedCampaignRulesetId);
|
||||||
private bool IsSkillGroupRolemasterOpenEnded => string.Equals(SkillGroupState.Model.RolemasterRollType, RulesetFormHelpers.RolemasterRollTypes.OpenEndedPercentile, StringComparison.OrdinalIgnoreCase);
|
private bool IsSkillGroupRolemasterOpenEnded => RulesetFormHelpers.IsRolemasterOpenEndedExpression(SkillGroupState.Model.DiceRollDefinition);
|
||||||
private string SkillGroupExpressionHelpText => IsRolemasterRuleset
|
private string SkillGroupExpressionHelpText => IsRolemasterRuleset
|
||||||
? $"{RulesetFormHelpers.RolemasterExampleText(SkillGroupState.Model.RolemasterRollType)}. Negative modifiers are allowed."
|
? $"{RulesetFormHelpers.RolemasterExampleText()}. Negative modifiers are allowed."
|
||||||
: "Enter the default expression for skills created in this group.";
|
: "Enter the default expression for skills created in this group.";
|
||||||
|
|
||||||
private bool ShowCreateSkillModal { get; set; }
|
private bool ShowCreateSkillModal { get; set; }
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ public partial class RollDiceStrip
|
|||||||
|
|
||||||
switch (die.Kind)
|
switch (die.Kind)
|
||||||
{
|
{
|
||||||
|
case RollDieKinds.RolemasterStandard:
|
||||||
|
classes.Add("rolemaster-standard");
|
||||||
|
break;
|
||||||
case RollDieKinds.RolemasterInitiative:
|
case RollDieKinds.RolemasterInitiative:
|
||||||
classes.Add("rolemaster-initiative");
|
classes.Add("rolemaster-initiative");
|
||||||
break;
|
break;
|
||||||
@@ -97,6 +100,9 @@ public partial class RollDiceStrip
|
|||||||
|
|
||||||
switch (die.Kind)
|
switch (die.Kind)
|
||||||
{
|
{
|
||||||
|
case RollDieKinds.RolemasterStandard:
|
||||||
|
labels.Add("Rolemaster roll");
|
||||||
|
break;
|
||||||
case RollDieKinds.RolemasterInitiative:
|
case RollDieKinds.RolemasterInitiative:
|
||||||
labels.Add("Rolemaster initiative");
|
labels.Add("Rolemaster initiative");
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
|
||||||
using RpgRoller.Domain;
|
using RpgRoller.Domain;
|
||||||
using RpgRoller.Services;
|
using RpgRoller.Services;
|
||||||
|
|
||||||
@@ -15,13 +14,6 @@ internal static class RulesetFormHelpers
|
|||||||
public const string Rolemaster = "rolemaster";
|
public const string Rolemaster = "rolemaster";
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static class RolemasterRollTypes
|
|
||||||
{
|
|
||||||
public const string Initiative = "initiative";
|
|
||||||
public const string Percentile = "percentile";
|
|
||||||
public const string OpenEndedPercentile = "open-ended-percentile";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsD6(string? rulesetId)
|
public static bool IsD6(string? rulesetId)
|
||||||
{
|
{
|
||||||
return string.Equals(rulesetId, RulesetIds.D6, StringComparison.OrdinalIgnoreCase);
|
return string.Equals(rulesetId, RulesetIds.D6, StringComparison.OrdinalIgnoreCase);
|
||||||
@@ -32,34 +24,12 @@ internal static class RulesetFormHelpers
|
|||||||
return string.Equals(rulesetId, RulesetIds.Rolemaster, StringComparison.OrdinalIgnoreCase);
|
return string.Equals(rulesetId, RulesetIds.Rolemaster, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string InferRolemasterRollType(string? expression)
|
public static bool IsRolemasterOpenEndedExpression(string? expression)
|
||||||
{
|
{
|
||||||
var parseResult = TryParseRolemasterExpression(expression);
|
var parseResult = TryParseRolemasterExpression(expression);
|
||||||
if (!parseResult.Succeeded || parseResult.Value is null)
|
return parseResult.Succeeded &&
|
||||||
return RolemasterRollTypes.Initiative;
|
parseResult.Value is not null &&
|
||||||
|
parseResult.Value.Kind == DiceExpressionKind.RolemasterOpenEndedPercentile;
|
||||||
return parseResult.Value.Kind switch
|
|
||||||
{
|
|
||||||
DiceExpressionKind.RolemasterPercentile => RolemasterRollTypes.Percentile,
|
|
||||||
DiceExpressionKind.RolemasterOpenEndedPercentile => RolemasterRollTypes.OpenEndedPercentile,
|
|
||||||
_ => RolemasterRollTypes.Initiative
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int InferRolemasterModifier(string? expression)
|
|
||||||
{
|
|
||||||
var parseResult = TryParseRolemasterExpression(expression);
|
|
||||||
return parseResult.Succeeded && parseResult.Value is not null ? parseResult.Value.Modifier : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string BuildRolemasterExpression(string? rollType, int modifier)
|
|
||||||
{
|
|
||||||
return rollType switch
|
|
||||||
{
|
|
||||||
RolemasterRollTypes.Percentile => $"d100{FormatModifier(modifier)}",
|
|
||||||
RolemasterRollTypes.OpenEndedPercentile => $"d100!{FormatModifier(modifier)}",
|
|
||||||
_ => $"2d10{FormatModifier(modifier)}"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string DescribeRolemasterExpression(string expression, int? fumbleRange)
|
public static string DescribeRolemasterExpression(string expression, int? fumbleRange)
|
||||||
@@ -70,22 +40,16 @@ internal static class RulesetFormHelpers
|
|||||||
|
|
||||||
return parseResult.Value.Kind switch
|
return parseResult.Value.Kind switch
|
||||||
{
|
{
|
||||||
DiceExpressionKind.RolemasterPercentile => $"Percentile: {parseResult.Value.Canonical}",
|
|
||||||
DiceExpressionKind.RolemasterOpenEndedPercentile => fumbleRange.HasValue
|
DiceExpressionKind.RolemasterOpenEndedPercentile => fumbleRange.HasValue
|
||||||
? $"Open-ended percentile: {parseResult.Value.Canonical}, fumble <= {fumbleRange.Value}"
|
? $"Open-ended percentile: {parseResult.Value.Canonical}, fumble <= {fumbleRange.Value}"
|
||||||
: $"Open-ended percentile: {parseResult.Value.Canonical}",
|
: $"Open-ended percentile: {parseResult.Value.Canonical}",
|
||||||
_ => $"Initiative: {parseResult.Value.Canonical}"
|
_ => $"Rolemaster: {parseResult.Value.Canonical}"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string RolemasterExampleText(string? rollType)
|
public static string RolemasterExampleText()
|
||||||
{
|
{
|
||||||
return rollType switch
|
return "Examples: d10, 15d10, d100-15, d100!+85";
|
||||||
{
|
|
||||||
RolemasterRollTypes.Percentile => "Example: d100+48",
|
|
||||||
RolemasterRollTypes.OpenEndedPercentile => "Example: d100!+85",
|
|
||||||
_ => "Example: 2d10+48"
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ServiceResult<DiceExpression> TryParseRolemasterExpression(string? expression)
|
private static ServiceResult<DiceExpression> TryParseRolemasterExpression(string? expression)
|
||||||
@@ -95,9 +59,4 @@ internal static class RulesetFormHelpers
|
|||||||
|
|
||||||
return DiceRules.ParseExpression(RulesetKind.Rolemaster, expression);
|
return DiceRules.ParseExpression(RulesetKind.Rolemaster, expression);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatModifier(int modifier)
|
|
||||||
{
|
|
||||||
return modifier >= 0 ? $"+{modifier}" : modifier.ToString(CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,16 +47,6 @@
|
|||||||
}
|
}
|
||||||
else if (IsRolemasterRuleset)
|
else if (IsRolemasterRuleset)
|
||||||
{
|
{
|
||||||
<label for="@RolemasterRollTypeInputId">Roll type</label>
|
|
||||||
<select id="@RolemasterRollTypeInputId" value="@FormState.Model.RolemasterRollType" @onchange="OnRolemasterRollTypeChanged">
|
|
||||||
<option value="@RulesetFormHelpers.RolemasterRollTypes.Initiative">Initiative</option>
|
|
||||||
<option value="@RulesetFormHelpers.RolemasterRollTypes.Percentile">Percentile</option>
|
|
||||||
<option value="@RulesetFormHelpers.RolemasterRollTypes.OpenEndedPercentile">Open-ended percentile</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<label for="@RolemasterModifierInputId">Modifier</label>
|
|
||||||
<input id="@RolemasterModifierInputId" type="number" step="1" value="@FormState.Model.RolemasterModifier" @oninput="OnRolemasterModifierChanged"/>
|
|
||||||
|
|
||||||
@if (IsRolemasterOpenEndedSelected)
|
@if (IsRolemasterOpenEndedSelected)
|
||||||
{
|
{
|
||||||
<label for="@FumbleRangeInputId">Fumble range</label>
|
<label for="@FumbleRangeInputId">Fumble range</label>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
using RpgRoller.Contracts;
|
using RpgRoller.Contracts;
|
||||||
@@ -21,8 +20,6 @@ public partial class SkillFormModal
|
|||||||
FormState.Model.WildDice = InitialModel.WildDice;
|
FormState.Model.WildDice = InitialModel.WildDice;
|
||||||
FormState.Model.AllowFumble = InitialModel.AllowFumble;
|
FormState.Model.AllowFumble = InitialModel.AllowFumble;
|
||||||
FormState.Model.FumbleRange = InitialModel.FumbleRange;
|
FormState.Model.FumbleRange = InitialModel.FumbleRange;
|
||||||
FormState.Model.RolemasterRollType = InitialModel.RolemasterRollType;
|
|
||||||
FormState.Model.RolemasterModifier = InitialModel.RolemasterModifier;
|
|
||||||
SynchronizeRulesetSpecificFields();
|
SynchronizeRulesetSpecificFields();
|
||||||
FormState.ResetValidation();
|
FormState.ResetValidation();
|
||||||
AppliedFormVersion = FormVersion;
|
AppliedFormVersion = FormVersion;
|
||||||
@@ -117,31 +114,14 @@ public partial class SkillFormModal
|
|||||||
{
|
{
|
||||||
FormState.Model.DiceRollDefinition = args.Value?.ToString() ?? string.Empty;
|
FormState.Model.DiceRollDefinition = args.Value?.ToString() ?? string.Empty;
|
||||||
if (IsRolemasterRuleset)
|
if (IsRolemasterRuleset)
|
||||||
SynchronizeRolemasterInputsFromExpression();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRolemasterRollTypeChanged(ChangeEventArgs args)
|
|
||||||
{
|
|
||||||
FormState.Model.RolemasterRollType = args.Value?.ToString() ?? RulesetFormHelpers.RolemasterRollTypes.Initiative;
|
|
||||||
NormalizeRolemasterFumbleRange();
|
NormalizeRolemasterFumbleRange();
|
||||||
SynchronizeRolemasterExpression();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRolemasterModifierChanged(ChangeEventArgs args)
|
|
||||||
{
|
|
||||||
var rawValue = args.Value?.ToString();
|
|
||||||
if (!int.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var modifier))
|
|
||||||
modifier = 0;
|
|
||||||
|
|
||||||
FormState.Model.RolemasterModifier = modifier;
|
|
||||||
SynchronizeRolemasterExpression();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsD6Ruleset => RulesetFormHelpers.IsD6(RulesetId);
|
private bool IsD6Ruleset => RulesetFormHelpers.IsD6(RulesetId);
|
||||||
private bool IsRolemasterRuleset => RulesetFormHelpers.IsRolemaster(RulesetId);
|
private bool IsRolemasterRuleset => RulesetFormHelpers.IsRolemaster(RulesetId);
|
||||||
private bool IsRolemasterOpenEndedSelected => string.Equals(FormState.Model.RolemasterRollType, RulesetFormHelpers.RolemasterRollTypes.OpenEndedPercentile, StringComparison.OrdinalIgnoreCase);
|
private bool IsRolemasterOpenEndedSelected => RulesetFormHelpers.IsRolemasterOpenEndedExpression(FormState.Model.DiceRollDefinition);
|
||||||
private string ExpressionHelpText => IsRolemasterRuleset
|
private string ExpressionHelpText => IsRolemasterRuleset
|
||||||
? $"{RulesetFormHelpers.RolemasterExampleText(FormState.Model.RolemasterRollType)}. Negative modifiers are allowed."
|
? $"{RulesetFormHelpers.RolemasterExampleText()}. Negative modifiers are allowed."
|
||||||
: "Enter the dice expression used for this skill.";
|
: "Enter the dice expression used for this skill.";
|
||||||
|
|
||||||
private void SynchronizeRulesetSpecificFields()
|
private void SynchronizeRulesetSpecificFields()
|
||||||
@@ -149,24 +129,9 @@ public partial class SkillFormModal
|
|||||||
if (!IsRolemasterRuleset)
|
if (!IsRolemasterRuleset)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
SynchronizeRolemasterInputsFromExpression();
|
|
||||||
NormalizeRolemasterFumbleRange();
|
NormalizeRolemasterFumbleRange();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SynchronizeRolemasterInputsFromExpression()
|
|
||||||
{
|
|
||||||
FormState.Model.RolemasterRollType = RulesetFormHelpers.InferRolemasterRollType(FormState.Model.DiceRollDefinition);
|
|
||||||
FormState.Model.RolemasterModifier = RulesetFormHelpers.InferRolemasterModifier(FormState.Model.DiceRollDefinition);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SynchronizeRolemasterExpression()
|
|
||||||
{
|
|
||||||
if (!IsRolemasterRuleset)
|
|
||||||
return;
|
|
||||||
|
|
||||||
FormState.Model.DiceRollDefinition = RulesetFormHelpers.BuildRolemasterExpression(FormState.Model.RolemasterRollType, FormState.Model.RolemasterModifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void NormalizeRolemasterFumbleRange()
|
private void NormalizeRolemasterFumbleRange()
|
||||||
{
|
{
|
||||||
if (!IsRolemasterRuleset)
|
if (!IsRolemasterRuleset)
|
||||||
@@ -220,12 +185,6 @@ public partial class SkillFormModal
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public string AllowFumbleInputId { get; set; } = "skill-fumble";
|
public string AllowFumbleInputId { get; set; } = "skill-fumble";
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public string RolemasterRollTypeInputId { get; set; } = "skill-rolemaster-roll-type";
|
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public string RolemasterModifierInputId { get; set; } = "skill-rolemaster-modifier";
|
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string FumbleRangeInputId { get; set; } = "skill-fumble-range";
|
public string FumbleRangeInputId { get; set; } = "skill-fumble-range";
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ public sealed record RollSkillRequest(string Visibility);
|
|||||||
|
|
||||||
public static class RollDieKinds
|
public static class RollDieKinds
|
||||||
{
|
{
|
||||||
|
public const string RolemasterStandard = "rolemaster-standard";
|
||||||
public const string RolemasterInitiative = "rolemaster-initiative";
|
public const string RolemasterInitiative = "rolemaster-initiative";
|
||||||
public const string RolemasterPercentile = "rolemaster-percentile";
|
public const string RolemasterPercentile = "rolemaster-percentile";
|
||||||
public const string RolemasterOpenEndedInitial = "rolemaster-open-ended-initial";
|
public const string RolemasterOpenEndedInitial = "rolemaster-open-ended-initial";
|
||||||
|
|||||||
@@ -93,8 +93,7 @@ public sealed class RollLogEntry
|
|||||||
public enum DiceExpressionKind
|
public enum DiceExpressionKind
|
||||||
{
|
{
|
||||||
Standard,
|
Standard,
|
||||||
RolemasterInitiative,
|
RolemasterStandard,
|
||||||
RolemasterPercentile,
|
|
||||||
RolemasterOpenEndedPercentile
|
RolemasterOpenEndedPercentile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,32 +78,30 @@ public static partial class DiceRules
|
|||||||
|
|
||||||
private static ServiceResult<DiceExpression> ParseRolemaster(string expression)
|
private static ServiceResult<DiceExpression> ParseRolemaster(string expression)
|
||||||
{
|
{
|
||||||
var initiativeMatch = RolemasterInitiativeRegex().Match(expression);
|
var match = RolemasterRegex().Match(expression);
|
||||||
if (initiativeMatch.Success)
|
if (!match.Success)
|
||||||
{
|
return ServiceResult<DiceExpression>.Failure("invalid_expression", "Expected Rolemaster format like d10+4, 15d10, d100-15, or d100!+85.");
|
||||||
var modifier = ParseModifier(initiativeMatch.Groups["modifier"].Value);
|
|
||||||
var validation = ValidateDiceParts(2, 10, modifier, -MaxModifier, MaxModifier);
|
var countValue = match.Groups["count"].Value;
|
||||||
|
var diceCount = string.IsNullOrEmpty(countValue) ? 1 : int.Parse(countValue);
|
||||||
|
var sides = int.Parse(match.Groups["sides"].Value);
|
||||||
|
var modifier = ParseModifier(match.Groups["modifier"].Value);
|
||||||
|
var validation = ValidateDiceParts(diceCount, sides, modifier, -MaxModifier, MaxModifier);
|
||||||
if (!validation.Succeeded)
|
if (!validation.Succeeded)
|
||||||
return ServiceResult<DiceExpression>.Failure(validation.Error!.Code, validation.Error.Message);
|
return ServiceResult<DiceExpression>.Failure(validation.Error!.Code, validation.Error.Message);
|
||||||
|
|
||||||
return ServiceResult<DiceExpression>.Success(new(2, 10, modifier, $"2d10{FormatModifier(modifier)}", DiceExpressionKind.RolemasterInitiative));
|
var isOpenEnded = match.Groups["openEnded"].Success;
|
||||||
}
|
if (isOpenEnded && (diceCount != 1 || sides != 100))
|
||||||
|
|
||||||
var percentileMatch = RolemasterPercentileRegex().Match(expression);
|
|
||||||
if (percentileMatch.Success)
|
|
||||||
{
|
{
|
||||||
var modifier = ParseModifier(percentileMatch.Groups["modifier"].Value);
|
return ServiceResult<DiceExpression>.Failure(
|
||||||
var validation = ValidateDiceParts(1, 100, modifier, -MaxModifier, MaxModifier);
|
"invalid_expression",
|
||||||
if (!validation.Succeeded)
|
"Open-ended Rolemaster rolls must use d100! with an implicit or explicit dice count of 1.");
|
||||||
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.");
|
var countPrefix = diceCount == 1 ? string.Empty : diceCount.ToString();
|
||||||
|
var canonical = $"{countPrefix}d{sides}{(isOpenEnded ? "!" : string.Empty)}{FormatModifier(modifier)}";
|
||||||
|
var kind = isOpenEnded ? DiceExpressionKind.RolemasterOpenEndedPercentile : DiceExpressionKind.RolemasterStandard;
|
||||||
|
return ServiceResult<DiceExpression>.Success(new(diceCount, sides, modifier, canonical, kind));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ServiceResult<bool> ValidateDiceParts(int diceCount, int sides, int modifier, int minModifier = 0, int maxModifier = MaxModifier)
|
private static ServiceResult<bool> ValidateDiceParts(int diceCount, int sides, int modifier, int minModifier = 0, int maxModifier = MaxModifier)
|
||||||
@@ -141,11 +139,8 @@ 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)]
|
[GeneratedRegex("^(?<count>\\d+)?d(?<sides>\\d+)(?<openEnded>!)?(?<modifier>[+-]\\d+)?$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase)]
|
||||||
private static partial Regex RolemasterInitiativeRegex();
|
private static partial Regex RolemasterRegex();
|
||||||
|
|
||||||
[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;
|
||||||
@@ -155,6 +150,6 @@ public static partial class DiceRules
|
|||||||
[
|
[
|
||||||
(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")
|
(RulesetKind.Rolemaster, "rolemaster", "Rolemaster", "countdSides(+/-modifier), e.g. d10, 15d10, d100-15, d100!+85")
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -949,8 +949,7 @@ public sealed class GameService : IGameService
|
|||||||
{
|
{
|
||||||
return expression.Kind switch
|
return expression.Kind switch
|
||||||
{
|
{
|
||||||
DiceExpressionKind.RolemasterInitiative => ComputeRolemasterInitiativeRoll(expression),
|
DiceExpressionKind.RolemasterStandard => ComputeRolemasterStandardRoll(expression),
|
||||||
DiceExpressionKind.RolemasterPercentile => ComputeRolemasterPercentileRoll(expression),
|
|
||||||
DiceExpressionKind.RolemasterOpenEndedPercentile => ComputeRolemasterOpenEndedRoll(expression, skill.FumbleRange.GetValueOrDefault()),
|
DiceExpressionKind.RolemasterOpenEndedPercentile => ComputeRolemasterOpenEndedRoll(expression, skill.FumbleRange.GetValueOrDefault()),
|
||||||
_ => ComputeStandardRoll(expression)
|
_ => ComputeStandardRoll(expression)
|
||||||
};
|
};
|
||||||
@@ -975,7 +974,7 @@ public sealed class GameService : IGameService
|
|||||||
return (total, BuildBreakdown(diceValues, expression.Modifier, total), dice);
|
return (total, BuildBreakdown(diceValues, expression.Modifier, total), dice);
|
||||||
}
|
}
|
||||||
|
|
||||||
private (int Total, string Breakdown, IReadOnlyList<RollDieResult> Dice) ComputeRolemasterInitiativeRoll(DiceExpression expression)
|
private (int Total, string Breakdown, IReadOnlyList<RollDieResult> Dice) ComputeRolemasterStandardRoll(DiceExpression expression)
|
||||||
{
|
{
|
||||||
var diceValues = new int[expression.DiceCount];
|
var diceValues = new int[expression.DiceCount];
|
||||||
var dice = new RollDieResult[expression.DiceCount];
|
var dice = new RollDieResult[expression.DiceCount];
|
||||||
@@ -984,25 +983,13 @@ public sealed class GameService : IGameService
|
|||||||
{
|
{
|
||||||
var value = m_DiceRoller.Roll(expression.Sides);
|
var value = m_DiceRoller.Roll(expression.Sides);
|
||||||
diceValues[i] = value;
|
diceValues[i] = value;
|
||||||
dice[i] = CreateRolemasterDie(value, i + 1, RollDieKinds.RolemasterInitiative, value);
|
dice[i] = CreateRolemasterDie(value, i + 1, RollDieKinds.RolemasterStandard, value);
|
||||||
total += value;
|
total += value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (total, BuildBreakdown(diceValues, expression.Modifier, total), dice);
|
return (total, BuildBreakdown(diceValues, expression.Modifier, total), dice);
|
||||||
}
|
}
|
||||||
|
|
||||||
private (int Total, string Breakdown, IReadOnlyList<RollDieResult> Dice) ComputeRolemasterPercentileRoll(DiceExpression expression)
|
|
||||||
{
|
|
||||||
var roll = m_DiceRoller.Roll(expression.Sides);
|
|
||||||
var total = roll + expression.Modifier;
|
|
||||||
var dice = new[]
|
|
||||||
{
|
|
||||||
CreateRolemasterDie(roll, 1, RollDieKinds.RolemasterPercentile, roll)
|
|
||||||
};
|
|
||||||
|
|
||||||
return (total, BuildBreakdown([roll], expression.Modifier, total), dice);
|
|
||||||
}
|
|
||||||
|
|
||||||
private (int Total, string Breakdown, IReadOnlyList<RollDieResult> Dice) ComputeRolemasterOpenEndedRoll(DiceExpression expression, int fumbleRange)
|
private (int Total, string Breakdown, IReadOnlyList<RollDieResult> Dice) ComputeRolemasterOpenEndedRoll(DiceExpression expression, int fumbleRange)
|
||||||
{
|
{
|
||||||
var initialRoll = m_DiceRoller.Roll(expression.Sides);
|
var initialRoll = m_DiceRoller.Roll(expression.Sides);
|
||||||
@@ -1427,6 +1414,15 @@ public sealed class GameService : IGameService
|
|||||||
return $"{openEndedInitial.Roll} | open-ended";
|
return $"{openEndedInitial.Roll} | open-ended";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (dice.Any(die => string.Equals(die.Kind, RollDieKinds.RolemasterStandard, StringComparison.Ordinal)))
|
||||||
|
{
|
||||||
|
var preview = string.Join(" + ", dice.Take(3).Select(die => die.Roll.ToString()));
|
||||||
|
if (dice.Count > 3)
|
||||||
|
preview = $"{preview} + ...";
|
||||||
|
|
||||||
|
return $"{preview} | rolemaster";
|
||||||
|
}
|
||||||
|
|
||||||
if (dice.Any(die => string.Equals(die.Kind, RollDieKinds.RolemasterInitiative, StringComparison.Ordinal)))
|
if (dice.Any(die => string.Equals(die.Kind, RollDieKinds.RolemasterInitiative, StringComparison.Ordinal)))
|
||||||
return $"{string.Join(" + ", dice.Select(die => die.Roll.ToString()))} | initiative";
|
return $"{string.Join(" + ", dice.Select(die => die.Roll.ToString()))} | initiative";
|
||||||
|
|
||||||
@@ -1438,7 +1434,8 @@ public sealed class GameService : IGameService
|
|||||||
|
|
||||||
private static bool IsRolemasterDieKind(string? kind)
|
private static bool IsRolemasterDieKind(string? kind)
|
||||||
{
|
{
|
||||||
return kind is RollDieKinds.RolemasterInitiative or
|
return kind is RollDieKinds.RolemasterStandard or
|
||||||
|
RollDieKinds.RolemasterInitiative or
|
||||||
RollDieKinds.RolemasterPercentile or
|
RollDieKinds.RolemasterPercentile or
|
||||||
RollDieKinds.RolemasterOpenEndedInitial or
|
RollDieKinds.RolemasterOpenEndedInitial or
|
||||||
RollDieKinds.RolemasterOpenEndedHigh or
|
RollDieKinds.RolemasterOpenEndedHigh or
|
||||||
|
|||||||
58
TASKS.md
58
TASKS.md
@@ -2,13 +2,12 @@
|
|||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
Extend the app with a new `rolemaster` ruleset that supports three Rolemaster-oriented skill roll types:
|
Extend the app with a new `rolemaster` ruleset that supports generic standard Rolemaster expressions plus the special open-ended percentile case:
|
||||||
|
|
||||||
1. Initiative: `2d10 + x`
|
1. Standard expressions: `NdS + x`, with an implicit dice count of `1` when no digit appears before `d`
|
||||||
2. Standard percentile: non-open-ended `1d100 + x`
|
2. Open-ended percentile: `d100! + x` with Rolemaster low-end and high-end behavior
|
||||||
3. Open-ended percentile: `1d100 + x` with Rolemaster low-end and high-end behavior
|
|
||||||
|
|
||||||
The initial scope is only roll definition, validation, execution, logging, and UI support for these three roll types. This does not include Rolemaster attack tables, critical tables, resistance rolls, maneuver tables, or character sheet math beyond the explicit roll modifier entered for a skill.
|
The initial scope is only roll definition, validation, execution, logging, and UI support for generic standard Rolemaster rolls and open-ended percentile rolls. This does not include Rolemaster attack tables, critical tables, resistance rolls, maneuver tables, or character sheet math beyond the explicit roll modifier entered for a skill.
|
||||||
|
|
||||||
## Scope Assumptions
|
## Scope Assumptions
|
||||||
|
|
||||||
@@ -19,8 +18,7 @@ The initial scope is only roll definition, validation, execution, logging, and U
|
|||||||
- If the first roll is `96` or higher, roll another high-ended `d100` recursively and add it.
|
- If the first roll is `96` or higher, roll another high-ended `d100` recursively and add it.
|
||||||
- If the first roll is less than or equal to the skill's configured fumble range, roll another high-ended `d100` recursively and subtract it.
|
- If the first roll is less than or equal to the skill's configured fumble range, roll another high-ended `d100` recursively and subtract it.
|
||||||
- Recursive follow-up rolls are high-ended only.
|
- Recursive follow-up rolls are high-ended only.
|
||||||
- Standard percentile rolls do not open-end and do not use a fumble range.
|
- Standard Rolemaster rolls do not open-end and do not use a fumble range.
|
||||||
- Initiative rolls are simple `2d10 + x` rolls with no open-ended behavior.
|
|
||||||
- The modifier `x` should be an integer.
|
- The modifier `x` should be an integer.
|
||||||
- Negative modifiers are supported for Rolemaster only.
|
- Negative modifiers are supported for Rolemaster only.
|
||||||
- D6 and D&D 5e keep their current non-negative modifier rules unless separately re-scoped in a future change.
|
- D6 and D&D 5e keep their current non-negative modifier rules unless separately re-scoped in a future change.
|
||||||
@@ -35,11 +33,12 @@ The initial scope is only roll definition, validation, execution, logging, and U
|
|||||||
- Roll breakdown text format for low-ended subtraction: `12 (03) -97 -44 +15 = -114`
|
- Roll breakdown text format for low-ended subtraction: `12 (03) -97 -44 +15 = -114`
|
||||||
- Skill groups need to support Rolemaster defaults for all of these fields, to stay consistent with current prototype behavior
|
- Skill groups need to support Rolemaster defaults for all of these fields, to stay consistent with current prototype behavior
|
||||||
- Rolemaster uses expression parsing with canonical syntax:
|
- Rolemaster uses expression parsing with canonical syntax:
|
||||||
- initiative: `2d10+48`
|
- standard roll with implicit count: `d10`
|
||||||
|
- standard roll with explicit count: `15d10`
|
||||||
- standard percentile: `d100+4`
|
- standard percentile: `d100+4`
|
||||||
- open-ended percentile: `d100!+85`
|
- open-ended percentile: `d100!+85`
|
||||||
- negative modifiers are valid only for Rolemaster, for example `d100-15`
|
- negative modifiers are valid only for Rolemaster, for example `d100-15`
|
||||||
- `15d10` e.g. for spells indicating "heals 1d10/lvl hit points".
|
- open-ended syntax is only valid for `d100!`
|
||||||
|
|
||||||
## Architecture Guardrails
|
## Architecture Guardrails
|
||||||
|
|
||||||
@@ -108,12 +107,9 @@ Affected areas:
|
|||||||
Current validation is split between generic expression parsing and D6 option validation. Rolemaster needs its own validation layer.
|
Current validation is split between generic expression parsing and D6 option validation. Rolemaster needs its own validation layer.
|
||||||
|
|
||||||
Validation rules to add:
|
Validation rules to add:
|
||||||
- `RolemasterInitiative`
|
- `RolemasterStandard`
|
||||||
- fixed base roll `2d10`
|
- supports generic `NdS` syntax with any supported side count
|
||||||
- integer modifier required
|
- assumes a dice count of `1` when the count is omitted before `d`
|
||||||
- no fumble range
|
|
||||||
- `RolemasterPercentile`
|
|
||||||
- fixed base roll `1d100`
|
|
||||||
- integer modifier required
|
- integer modifier required
|
||||||
- no fumble range
|
- no fumble range
|
||||||
- `RolemasterOpenEndedPercentile`
|
- `RolemasterOpenEndedPercentile`
|
||||||
@@ -130,13 +126,14 @@ Tasks:
|
|||||||
- Replace or extend `ValidateSkillDefinition`.
|
- Replace or extend `ValidateSkillDefinition`.
|
||||||
- Extend `DiceRules.ParseExpression` with a Rolemaster-focused parser/validator path.
|
- Extend `DiceRules.ParseExpression` with a Rolemaster-focused parser/validator path.
|
||||||
- Add canonical Rolemaster expression parsing rules:
|
- Add canonical Rolemaster expression parsing rules:
|
||||||
- `2d10+48` and `2d10-15` for initiative
|
- `d10`, `2d10+48`, and `15d10-15` for generic standard rolls
|
||||||
- `d100+4` and `d100-20` for standard percentile
|
- `d100+4` and `d100-20` for standard percentile
|
||||||
- `d100!+85` and `d100!-15` for open-ended percentile
|
- `d100!+85` and `d100!-15` for open-ended percentile
|
||||||
|
- reject open-ended syntax for non-percentile or multi-die expressions such as `2d10!+1`
|
||||||
- Keep Rolemaster fumble range validation separate from the expression because it is configured independently.
|
- Keep Rolemaster fumble range validation separate from the expression because it is configured independently.
|
||||||
- Add canonical display formatting for Rolemaster skills, for example:
|
- Add canonical display formatting for Rolemaster skills, for example:
|
||||||
- `Initiative: 2d10+15`
|
- `Rolemaster: d10`
|
||||||
- `Percentile: 1d100+48`
|
- `Rolemaster: 15d10+15`
|
||||||
- `Open-ended percentile: OE 1d100+48, fumble <= 5`
|
- `Open-ended percentile: OE 1d100+48, fumble <= 5`
|
||||||
- Add explicit regression tests that D6 / D&D still reject negative modifiers after Rolemaster support lands.
|
- Add explicit regression tests that D6 / D&D still reject negative modifiers after Rolemaster support lands.
|
||||||
|
|
||||||
@@ -154,8 +151,7 @@ Add a new Rolemaster execution path alongside the existing D6 and standard roll
|
|||||||
Recommended internal shape:
|
Recommended internal shape:
|
||||||
- `ComputeRoll` dispatches by ruleset and roll kind.
|
- `ComputeRoll` dispatches by ruleset and roll kind.
|
||||||
- Add dedicated methods:
|
- Add dedicated methods:
|
||||||
- `ComputeRolemasterInitiativeRoll`
|
- `ComputeRolemasterStandardRoll`
|
||||||
- `ComputeRolemasterPercentileRoll`
|
|
||||||
- `ComputeRolemasterOpenEndedRoll`
|
- `ComputeRolemasterOpenEndedRoll`
|
||||||
- helper `RollRolemasterHighOpenEndedChain`
|
- helper `RollRolemasterHighOpenEndedChain`
|
||||||
|
|
||||||
@@ -231,14 +227,9 @@ Tasks:
|
|||||||
|
|
||||||
Recommended frontend model changes:
|
Recommended frontend model changes:
|
||||||
- Introduce ruleset-aware form state rather than raw D6 toggles.
|
- Introduce ruleset-aware form state rather than raw D6 toggles.
|
||||||
- Keep "Expression" as the primary input, but provide Rolemaster-specific help text, examples, and validation when the selected campaign ruleset is `rolemaster`.
|
- Keep "Expression" as the primary input, and provide Rolemaster-specific help text, examples, and validation when the selected campaign ruleset is `rolemaster`.
|
||||||
- Add a "Roll type" selector with options:
|
- Show fumble range input only when the current Rolemaster expression is an open-ended percentile roll.
|
||||||
- `Initiative`
|
- Use progressive disclosure so only the fields relevant to the current Rolemaster expression are shown.
|
||||||
- `Percentile`
|
|
||||||
- `Open-ended percentile`
|
|
||||||
- Keep the selector and expression input synchronized so users can either choose a type or see the parsed/canonical expression clearly.
|
|
||||||
- Show fumble range input only for open-ended percentile.
|
|
||||||
- Use progressive disclosure so only the fields relevant to the chosen Rolemaster roll type are shown.
|
|
||||||
- Add inline validation on blur and submit, visible required indicators, and `aria-live` / alert semantics for validation errors.
|
- Add inline validation on blur and submit, visible required indicators, and `aria-live` / alert semantics for validation errors.
|
||||||
- Preserve the existing app visual language instead of introducing a separate Rolemaster-specific theme.
|
- Preserve the existing app visual language instead of introducing a separate Rolemaster-specific theme.
|
||||||
|
|
||||||
@@ -287,7 +278,7 @@ Service-level tests to add:
|
|||||||
- rejection of D6-only options on Rolemaster skills
|
- rejection of D6-only options on Rolemaster skills
|
||||||
- rejection of negative modifiers on D6 / D&D skills
|
- rejection of negative modifiers on D6 / D&D skills
|
||||||
- acceptance of negative modifiers on Rolemaster expressions
|
- acceptance of negative modifiers on Rolemaster expressions
|
||||||
- initiative roll math
|
- generic standard Rolemaster roll math
|
||||||
- standard percentile roll math
|
- standard percentile roll math
|
||||||
- open-ended high roll with one extra roll
|
- open-ended high roll with one extra roll
|
||||||
- open-ended high roll with recursive extra rolls
|
- open-ended high roll with recursive extra rolls
|
||||||
@@ -350,7 +341,7 @@ Recommended implementation order:
|
|||||||
- Keep existing D6 / D&D behavior green.
|
- Keep existing D6 / D&D behavior green.
|
||||||
|
|
||||||
#### Iteration 3: roll engine
|
#### Iteration 3: roll engine
|
||||||
- Implement initiative and standard percentile execution.
|
- Implement generic standard and standard percentile execution.
|
||||||
- Implement open-ended percentile execution with recursive high-end chaining and low-end subtraction.
|
- Implement open-ended percentile execution with recursive high-end chaining and low-end subtraction.
|
||||||
- Extend die-result metadata and breakdown formatting.
|
- Extend die-result metadata and breakdown formatting.
|
||||||
- Add focused service tests for roll math and payload/detail serialization compatibility.
|
- Add focused service tests for roll math and payload/detail serialization compatibility.
|
||||||
@@ -382,10 +373,9 @@ Recommended implementation order:
|
|||||||
|
|
||||||
- A campaign can be created with ruleset id `rolemaster`.
|
- A campaign can be created with ruleset id `rolemaster`.
|
||||||
- Rolemaster skill groups and skills can be created and edited through API and UI.
|
- Rolemaster skill groups and skills can be created and edited through API and UI.
|
||||||
- Supported Rolemaster roll types are limited to:
|
- Supported Rolemaster roll types are:
|
||||||
- initiative `2d10 + x`
|
- generic standard rolls `NdS + x`, with implicit dice count `1` for `dS`
|
||||||
- standard percentile `1d100 + x`
|
- open-ended percentile `d100! + x` with fumble range
|
||||||
- open-ended percentile `1d100 + x` with fumble range
|
|
||||||
- Open-ended percentile rolls correctly:
|
- Open-ended percentile rolls correctly:
|
||||||
- add recursively on first-roll `96+`
|
- add recursively on first-roll `96+`
|
||||||
- subtract a recursive high-end chain on first-roll `<= fumble range`
|
- subtract a recursive high-end chain on first-roll `<= fumble range`
|
||||||
|
|||||||
@@ -108,9 +108,9 @@ test("Rolemaster UI exposes conditional create and edit fields", async ({ page,
|
|||||||
await page.getByRole("menuitem", { name: "Play" }).click();
|
await page.getByRole("menuitem", { name: "Play" }).click();
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Add group" }).click();
|
await page.getByRole("button", { name: "Add group" }).click();
|
||||||
await expect(page.locator("#skill-group-rolemaster-roll-type")).toBeVisible();
|
|
||||||
await expect(page.locator("#skill-group-wild-dice")).toHaveCount(0);
|
await expect(page.locator("#skill-group-wild-dice")).toHaveCount(0);
|
||||||
await page.locator("#skill-group-rolemaster-roll-type").selectOption("open-ended-percentile");
|
await expect(page.locator("#skill-group-expression")).toHaveValue("d100");
|
||||||
|
await page.locator("#skill-group-expression").fill("d100!+15");
|
||||||
await expect(page.locator("#skill-group-fumble-range")).toBeVisible();
|
await expect(page.locator("#skill-group-fumble-range")).toBeVisible();
|
||||||
await page.locator("#skill-group-fumble-range").fill("");
|
await page.locator("#skill-group-fumble-range").fill("");
|
||||||
await page.getByRole("button", { name: "Create Group" }).click();
|
await page.getByRole("button", { name: "Create Group" }).click();
|
||||||
@@ -118,16 +118,17 @@ test("Rolemaster UI exposes conditional create and edit fields", async ({ page,
|
|||||||
await page.getByRole("button", { name: "Cancel" }).click();
|
await page.getByRole("button", { name: "Cancel" }).click();
|
||||||
|
|
||||||
await page.getByRole("button", { name: "Add skill" }).first().click();
|
await page.getByRole("button", { name: "Add skill" }).first().click();
|
||||||
await expect(page.locator("#skill-create-rolemaster-roll-type")).toBeVisible();
|
await expect(page.locator("#skill-create-expression")).toHaveValue("d100!+15");
|
||||||
await page.locator("#skill-create-rolemaster-roll-type").selectOption("percentile");
|
await page.locator("#skill-create-expression").fill("15d10");
|
||||||
await expect(page.locator("#skill-create-fumble-range")).toHaveCount(0);
|
await expect(page.locator("#skill-create-fumble-range")).toHaveCount(0);
|
||||||
await page.locator("#skill-create-rolemaster-roll-type").selectOption("open-ended-percentile");
|
await page.locator("#skill-create-expression").fill("d100!+25");
|
||||||
await expect(page.locator("#skill-create-fumble-range")).toBeVisible();
|
await expect(page.locator("#skill-create-fumble-range")).toBeVisible();
|
||||||
await page.getByRole("button", { name: "Cancel" }).click();
|
await page.getByRole("button", { name: "Cancel" }).click();
|
||||||
|
|
||||||
await page.locator("button[title='Edit skill']").first().click();
|
await page.locator("button[title='Edit skill']").first().click();
|
||||||
await expect(page.locator("#skill-edit-expression")).toHaveValue("d100!+25");
|
await expect(page.locator("#skill-edit-expression")).toHaveValue("d100!+25");
|
||||||
await expect(page.locator("#skill-edit-rolemaster-roll-type")).toHaveValue("open-ended-percentile");
|
|
||||||
await expect(page.locator("#skill-edit-fumble-range")).toHaveValue("5");
|
await expect(page.locator("#skill-edit-fumble-range")).toHaveValue("5");
|
||||||
|
await page.locator("#skill-edit-expression").fill("d10");
|
||||||
|
await expect(page.locator("#skill-edit-fumble-range")).toHaveCount(0);
|
||||||
await page.getByRole("button", { name: "Cancel" }).click();
|
await page.getByRole("button", { name: "Cancel" }).click();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user