diff --git a/README.md b/README.md index e816360..c19daa5 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ Rolemaster support: - Open-ended percentile expressions such as `d100!+85` - Conditional `FumbleRange` handling for open-ended percentile skills and skill-group defaults - Persisted and validated automatic retry toggle for open-ended percentile skills; only eligible Rolemaster skills can enable it -- Automatic retry windows for eligible open-ended skills: results `77-90` retry once with `+5`, and results `91-110` retry once with `+10` +- Automatic retry windows for eligible open-ended skills: results `76-90` retry once with `+5`, and results `91-110` retry once with `+10` - Open-ended high chaining and low-end subtraction with ordered die metadata in roll detail - Compact log badges and summaries for open-ended, retry, and fumble-related events, including `Retry +5` and `Retry +10` diff --git a/RpgRoller.Tests/Api/RolemasterApiTests.cs b/RpgRoller.Tests/Api/RolemasterApiTests.cs index f1f209a..f5ea31c 100644 --- a/RpgRoller.Tests/Api/RolemasterApiTests.cs +++ b/RpgRoller.Tests/Api/RolemasterApiTests.cs @@ -85,7 +85,7 @@ public sealed class RolemasterApiTests(WebApplicationFactory factory) : [Fact] public async Task RolemasterAutoRetryRolls_AppearInLogPageAndDetail() { - using var factory = CreateFactory(68, 42, 90, 32, 68); + using var factory = CreateFactory(66, 42, 90, 32, 65); using var client = factory.CreateClient(new() { AllowAutoRedirect = false }); await RegisterAsync(client, "rolemaster-retry-api", "Password123", "Rolemaster Retry Api"); @@ -104,7 +104,7 @@ public sealed class RolemasterApiTests(WebApplicationFactory factory) : var detail = await GetAsync(client, $"/api/rolls/{retryFiveRoll.RollId}"); Assert.Equal(57, retryFiveRoll.Result); - Assert.Equal("68+10=78; retry(+5): 42+10=52; final=57", retryFiveRoll.Breakdown); + Assert.Equal("66+10=76; retry(+5): 42+10=52; final=57", retryFiveRoll.Breakdown); Assert.Collection(retryFiveRoll.Dice, die => { Assert.Equal(1, die.Attempt); @@ -118,16 +118,16 @@ public sealed class RolemasterApiTests(WebApplicationFactory factory) : Assert.Equal(43, retryTenRoll.Result); Assert.Equal("90+1=91; retry(+10): 32+1=33; final=43", retryTenRoll.Breakdown); - Assert.Equal(78, disabledRoll.Result); - Assert.Equal("68+10=78", disabledRoll.Breakdown); + Assert.Equal(75, disabledRoll.Result); + Assert.Equal("65+10=75", disabledRoll.Breakdown); Assert.All(disabledRoll.Dice, die => Assert.Null(die.Attempt)); Assert.Equal(3, logPage.Entries.Length); - Assert.Equal("68 | open-ended | retry +5", logPage.Entries[0].SummaryText); - Assert.Equal(["rs5"], Assert.IsType(logPage.Entries[0].EventBadges)); + Assert.Equal("66 | open-ended | retry +5", logPage.Entries[0].SummaryText); + Assert.Equal(["r66", "rs5"], Assert.IsType(logPage.Entries[0].EventBadges)); Assert.Equal("90 | open-ended | retry +10", logPage.Entries[1].SummaryText); Assert.Equal(["rs10"], Assert.IsType(logPage.Entries[1].EventBadges)); - Assert.Equal("68 | open-ended", logPage.Entries[2].SummaryText); + Assert.Equal("65 | open-ended", logPage.Entries[2].SummaryText); Assert.Null(logPage.Entries[2].EventBadges); Assert.Equal(retryFiveRoll.Breakdown, detail.Breakdown); diff --git a/RpgRoller.Tests/PayloadBudgetTests.cs b/RpgRoller.Tests/PayloadBudgetTests.cs index 9a6eab2..88977b9 100644 --- a/RpgRoller.Tests/PayloadBudgetTests.cs +++ b/RpgRoller.Tests/PayloadBudgetTests.cs @@ -132,7 +132,7 @@ public sealed class PayloadBudgetTests [Fact] public void RolemasterRollDetailPayload_StaysWithinBudget_AndRetryMetadataRemainsLazy() { - using var harness = ServiceTestSupport.CreateHarness(68, 42); + using var harness = ServiceTestSupport.CreateHarness(66, 42); var service = harness.Service; service.Register("gm-rm-detail-budget", "Password123", "GM"); @@ -148,7 +148,7 @@ public sealed class PayloadBudgetTests var roll = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public")); var logPage = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, limit: 5)); var detail = ServiceTestSupport.GetValue(service.GetRollDetail(gmSession, roll.RollId)); - Assert.Equal("68 | open-ended | retry +5", Assert.Single(logPage.Entries).SummaryText); + Assert.Equal("66 | open-ended | retry +5", Assert.Single(logPage.Entries).SummaryText); AssertPayloadWithinBudget(detail, 4 * 1024, "rolemaster roll detail"); @@ -161,7 +161,7 @@ public sealed class PayloadBudgetTests Assert.DoesNotContain("\"sequence\"", logPageJson, StringComparison.Ordinal); Assert.DoesNotContain("\"breakdown\"", logPageJson, StringComparison.Ordinal); Assert.Contains("\"kind\":\"rolemaster-open-ended-initial\"", detailJson, StringComparison.Ordinal); - Assert.Contains("\"signedContribution\":68", detailJson, StringComparison.Ordinal); + Assert.Contains("\"signedContribution\":66", detailJson, StringComparison.Ordinal); Assert.Contains("\"attempt\":1", detailJson, StringComparison.Ordinal); Assert.Contains("\"attempt\":2", detailJson, StringComparison.Ordinal); } diff --git a/RpgRoller.Tests/Services/ServiceHelperExtractionTests.cs b/RpgRoller.Tests/Services/ServiceHelperExtractionTests.cs index 926711d..0276ad1 100644 --- a/RpgRoller.Tests/Services/ServiceHelperExtractionTests.cs +++ b/RpgRoller.Tests/Services/ServiceHelperExtractionTests.cs @@ -72,11 +72,11 @@ public sealed class ServiceHelperExtractionTests [Fact] public void RolemasterRetryPolicy_ResolvesRetryBandsAndMarkers() { - Assert.Equal(5, RolemasterRetryPolicy.ResolveAutoRetryBonus(77)); + Assert.Equal(5, RolemasterRetryPolicy.ResolveAutoRetryBonus(76)); Assert.Equal(5, RolemasterRetryPolicy.ResolveAutoRetryBonus(90)); Assert.Equal(10, RolemasterRetryPolicy.ResolveAutoRetryBonus(91)); Assert.Equal(10, RolemasterRetryPolicy.ResolveAutoRetryBonus(110)); - Assert.Null(RolemasterRetryPolicy.ResolveAutoRetryBonus(76)); + Assert.Null(RolemasterRetryPolicy.ResolveAutoRetryBonus(75)); Assert.Null(RolemasterRetryPolicy.ResolveAutoRetryBonus(111)); Assert.Equal(5, RolemasterRetryPolicy.TryExtractRetryBonus("68+10=78; retry(+5): 42+10=52; final=57")); Assert.Equal(10, RolemasterRetryPolicy.TryExtractRetryBonus("90+1=91; retry(+10): 32+1=33; final=43")); diff --git a/RpgRoller.Tests/Services/ServiceRolemasterRollTests.cs b/RpgRoller.Tests/Services/ServiceRolemasterRollTests.cs index 549d497..c325ae3 100644 --- a/RpgRoller.Tests/Services/ServiceRolemasterRollTests.cs +++ b/RpgRoller.Tests/Services/ServiceRolemasterRollTests.cs @@ -178,7 +178,7 @@ public sealed class ServiceRolemasterRollTests [Fact] public void RollSkill_RolemasterAutoRetryPlusFive_UsesRetryResultAndMarksAttempts() { - using var harness = ServiceTestSupport.CreateHarness(68, 42); + using var harness = ServiceTestSupport.CreateHarness(66, 42); var service = harness.Service; service.Register("gm-retry-five", "Password123", "GM"); @@ -192,17 +192,17 @@ public sealed class ServiceRolemasterRollTests var logEntry = Assert.Single(ServiceTestSupport.GetValue(service.GetCampaignLogPage(session, campaign.Id, limit: 5)).Entries); Assert.Equal(57, roll.Result); - Assert.Equal("68+10=78; retry(+5): 42+10=52; final=57", roll.Breakdown); - Assert.Equal("68 | open-ended | retry +5", logEntry.SummaryText); - Assert.Equal(["rs5"], Assert.IsType(logEntry.EventBadges)); + Assert.Equal("66+10=76; retry(+5): 42+10=52; final=57", roll.Breakdown); + Assert.Equal("66 | open-ended | retry +5", logEntry.SummaryText); + Assert.Equal(["r66", "rs5"], Assert.IsType(logEntry.EventBadges)); Assert.Equal(roll.Breakdown, detail.Breakdown); Assert.Collection(detail.Dice, die => { - Assert.Equal(68, die.Roll); + Assert.Equal(66, die.Roll); Assert.Equal(1, die.Sequence); Assert.Equal(1, die.Attempt); Assert.Equal(RollDieKinds.RolemasterOpenEndedInitial, die.Kind); - Assert.Equal(68, die.SignedContribution); + Assert.Equal(66, die.SignedContribution); }, die => { Assert.Equal(42, die.Roll); @@ -238,7 +238,7 @@ public sealed class ServiceRolemasterRollTests [Fact] public void RollSkill_RolemasterAutoRetryDisabled_KeepsOriginalResult() { - using var harness = ServiceTestSupport.CreateHarness(68); + using var harness = ServiceTestSupport.CreateHarness(65); var service = harness.Service; service.Register("gm-retry-off", "Password123", "GM"); @@ -250,9 +250,9 @@ public sealed class ServiceRolemasterRollTests var roll = ServiceTestSupport.GetValue(service.RollSkill(session, skill.Id, "public")); var logEntry = Assert.Single(ServiceTestSupport.GetValue(service.GetCampaignLogPage(session, campaign.Id, limit: 5)).Entries); - Assert.Equal(78, roll.Result); - Assert.Equal("68+10=78", roll.Breakdown); - Assert.Equal("68 | open-ended", logEntry.SummaryText); + Assert.Equal(75, roll.Result); + Assert.Equal("65+10=75", roll.Breakdown); + Assert.Equal("65 | open-ended", logEntry.SummaryText); Assert.Null(logEntry.EventBadges); Assert.All(roll.Dice, die => Assert.Null(die.Attempt)); } diff --git a/RpgRoller.Tests/Services/ServiceRollHelperTests.cs b/RpgRoller.Tests/Services/ServiceRollHelperTests.cs index ab0727d..65610cc 100644 --- a/RpgRoller.Tests/Services/ServiceRollHelperTests.cs +++ b/RpgRoller.Tests/Services/ServiceRollHelperTests.cs @@ -20,7 +20,7 @@ public sealed class ServiceRollHelperTests Assert.Equal("0=0", RollBreakdownFormatter.BuildBreakdown([], 0, 0)); Assert.Equal("97+96+45+85=323", RollBreakdownFormatter.BuildRolemasterOpenEndedBreakdown(97, [96, 45], false, 85, 323)); Assert.Equal("(05) -97 -100 -12 +85 = -124", RollBreakdownFormatter.BuildRolemasterOpenEndedBreakdown(5, [97, 100, 12], true, 85, -124)); - Assert.Equal("68+10=78; retry(+5): 42+10=52; final=57", RollBreakdownFormatter.BuildRolemasterRetryBreakdown("68+10=78", 5, "42+10=52", 57)); + Assert.Equal("66+10=76; retry(+5): 42+10=52; final=57", RollBreakdownFormatter.BuildRolemasterRetryBreakdown("66+10=76", 5, "42+10=52", 57)); Assert.Equal("05", RollBreakdownFormatter.FormatRolemasterTriggerRoll(5)); } @@ -29,19 +29,19 @@ public sealed class ServiceRollHelperTests { var d6Dice = new[] { new RollDieResult(6, true, false, true, false, false), new RollDieResult(1, false, true, true, false, false) }; var rolemasterDice = new[] { new RollDieResult(5, false, false, false, false, false, 1, RollDieKinds.RolemasterOpenEndedInitial), new RollDieResult(97, false, false, false, false, false, 2, RollDieKinds.RolemasterOpenEndedLowSubtract, -97), new RollDieResult(100, false, false, false, false, false, 3, RollDieKinds.RolemasterOpenEndedLowSubtract, -100) }; - var retryDice = new[] { new RollDieResult(68, false, false, false, false, false, 1, RollDieKinds.RolemasterOpenEndedInitial, 68, 1), new RollDieResult(42, false, false, false, false, false, 1, RollDieKinds.RolemasterOpenEndedInitial, 42, 2) }; - const string retryBreakdown = "68+10=78; retry(+5): 42+10=52; final=57"; + var retryDice = new[] { new RollDieResult(66, false, false, false, false, false, 1, RollDieKinds.RolemasterOpenEndedInitial, 66, 1), new RollDieResult(42, false, false, false, false, false, 1, RollDieKinds.RolemasterOpenEndedInitial, 42, 2) }; + const string retryBreakdown = "66+10=76; retry(+5): 42+10=52; final=57"; Assert.Equal("1d20+5", CampaignLogSummaryBuilder.ExtractCustomRollExpression("1d20+5 => 20+5=25", " => ")); Assert.Null(CampaignLogSummaryBuilder.ExtractCustomRollExpression("20+5=25", " => ")); Assert.Equal("6, 1", CampaignLogSummaryBuilder.BuildCompactLogSummary(d6Dice)); Assert.Equal("(05) -97 -100 | open-ended low", CampaignLogSummaryBuilder.BuildCompactLogSummary(rolemasterDice)); - Assert.Equal("68 | open-ended | retry +5", CampaignLogSummaryBuilder.BuildCompactLogSummary(retryDice, retryBreakdown)); + Assert.Equal("66 | open-ended | retry +5", CampaignLogSummaryBuilder.BuildCompactLogSummary(retryDice, retryBreakdown)); Assert.Equal("No detail available.", CampaignLogSummaryBuilder.BuildCompactLogSummary([])); Assert.Equal(["w6", "w1"], Assert.IsType(CampaignLogSummaryBuilder.BuildCompactLogEventBadges(RulesetKind.D6, null, d6Dice))); Assert.Equal(["n20"], Assert.IsType(CampaignLogSummaryBuilder.BuildCompactLogEventBadges(RulesetKind.Dnd5e, "1d20+5", [new(20, false, false, false, false, false)]))); Assert.Equal(["rf", "r100"], Assert.IsType(CampaignLogSummaryBuilder.BuildCompactLogEventBadges(RulesetKind.Rolemaster, "d100!+85", rolemasterDice))); - Assert.Equal(["rs5"], Assert.IsType(CampaignLogSummaryBuilder.BuildCompactLogEventBadges(RulesetKind.Rolemaster, "d100!+10", retryDice, retryBreakdown))); + Assert.Equal(["r66", "rs5"], Assert.IsType(CampaignLogSummaryBuilder.BuildCompactLogEventBadges(RulesetKind.Rolemaster, "d100!+10", retryDice, retryBreakdown))); } [Fact] diff --git a/RpgRoller/Services/RolemasterRetryPolicy.cs b/RpgRoller/Services/RolemasterRetryPolicy.cs index 169ccbb..ce18fdd 100644 --- a/RpgRoller/Services/RolemasterRetryPolicy.cs +++ b/RpgRoller/Services/RolemasterRetryPolicy.cs @@ -4,7 +4,7 @@ public static class RolemasterRetryPolicy { public static int? ResolveAutoRetryBonus(int firstResult) { - if (firstResult is >= 77 and <= 90) + if (firstResult is >= 76 and <= 90) return 5; if (firstResult is >= 91 and <= 110) diff --git a/TASKS.md b/TASKS.md index 87d125b..89fa649 100644 --- a/TASKS.md +++ b/TASKS.md @@ -8,7 +8,7 @@ This ExecPlan is a living document. The sections `Progress`, `Surprises & Discov After this change, a Rolemaster skill can opt into an automatic retry when its first result lands in specific retry bands. The player will be able to toggle that behavior while creating or editing a Rolemaster open-ended skill, roll the skill, and then see the retry clearly in the campaign log card through a special badge and readable summary text. The detailed roll view will still show enough information to explain why the retry happened and what final result was recorded. -For this feature, an eligible retry result means a Rolemaster open-ended percentile skill roll whose first fully evaluated result, including the skill expression modifier and any low-end subtraction chain, lands in one of the retry windows before any retry bonus is applied. This plan preserves the user-provided thresholds exactly: results `77` through `90` grant a retry with `+5`; results `91` through `110` grant a retry with `+10`. +For this feature, an eligible retry result means a Rolemaster open-ended percentile skill roll whose first fully evaluated result, including the skill expression modifier and any low-end subtraction chain, lands in one of the retry windows before any retry bonus is applied. This plan preserves the user-provided thresholds exactly: results `76` through `90` grant a retry with `+5`; results `91` through `110` grant a retry with `+10`. ## Progress @@ -37,7 +37,7 @@ For this feature, an eligible retry result means a Rolemaster open-ended percent Rationale: The request explicitly asks for “a toggle for a rolemaster skill.” A skill-group default would widen scope into template inheritance and create unclear behavior for ad hoc custom rolls. Date/Author: 2026-04-04 / Codex -- Decision: The retry windows are interpreted literally from the request: `77-90 => +5`, `91-110 => +10`. A result of `111` counts as success and doesn't need to be retried. +- Decision: The retry windows are interpreted literally from the request: `76-90 => +5`, `91-110 => +10`. A result of `111` counts as success and doesn't need to be retried. Rationale: The user gave concrete inclusive and exclusive bounds. Preserving those exact bounds avoids silently changing game rules inside the plan. Date/Author: 2026-04-04 / Codex