# Rolemaster Automatic Retry This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. `PLANS.md` is checked into the repo at `PLANS.md`. This document must be maintained in accordance with that file. ## Purpose / Big Picture 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`. ## Progress - [x] (2026-04-04 23:52Z) Reviewed `PLANS.md` and the current Rolemaster roll, skill-form, API, and log-card code paths. - [x] (2026-04-04 23:52Z) Authored this ExecPlan in `TASKS.md`. - [x] (2026-04-14 20:45Z) Added persisted `RolemasterAutoRetry` wiring through the skill model, API contracts, DTOs, in-memory state, clone helpers, EF mapping, and the `20260414204309_AddRolemasterAutoRetry` migration. - [x] (2026-04-14 21:20Z) Implemented one-shot Rolemaster automatic retry execution, persisted retry-aware breakdown text, attempt-tagged dice detail, and compact `rs5`/`rs10` log badges plus retry summary text. - [x] (2026-04-14 20:45Z) Updated the Blazor skill create/edit flows so the automatic retry toggle appears only for Rolemaster open-ended skills and is cleared when the expression stops qualifying. - [ ] Add or update unit, API, persistence, payload-budget, and browser tests that prove the feature end to end. - [ ] Update `README.md`, run `pwsh ./scripts/ci-local.ps1`, and commit the finished implementation. ## Surprises & Discoveries - Observation: The current Rolemaster engine has only two special open-ended branches: high open-ended chaining and low-end subtraction. There is no second-attempt concept today. Evidence: `RpgRoller/Services/RolemasterRollEngine.cs` calls `RollHighOpenEndedChain` for high rolls and for low-end subtraction, then immediately formats the final total. - Observation: Compact log badges are not stored anywhere. They are recalculated when a log page is read, so any retry signal must be derivable from persisted roll data. Evidence: `RpgRoller/Services/GameRollService.cs` calls `CampaignLogSummaryBuilder.BuildCompactLogEventBadges(...)` while building `CampaignLogListEntry`; `RollLogEntry` does not persist badges. - Observation: The Rolemaster-specific skill UI already hides and normalizes invalid options when the expression is not open-ended percentile, which gives this feature a natural home. Evidence: `RpgRoller/Components/Pages/HomeControls/SkillFormModal.razor.cs` and `CharacterPanel.razor.cs` already clear `FumbleRange` when the selected Rolemaster expression is not open-ended. ## Decision Log - Decision: The retry toggle is per skill, not per skill group, and custom rolls do not participate. 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. 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 - Decision: The retry window is evaluated from the first complete Rolemaster skill result, after the original expression modifier and any low-end subtraction chain are applied, but before any retry bonus is applied. Rationale: The request speaks about “if the result is ...” rather than about a raw trigger die. Using the user-visible result keeps the rule understandable in the UI and in tests. Date/Author: 2026-04-04 / Codex - Decision: An eligible roll triggers exactly one automatic retry. The retry does not recurse again even if the retry result also lands in a retry band. Rationale: Recursive retries would make the feature hard to explain, hard to test, and far removed from the user’s “automatic retry” request. Date/Author: 2026-04-04 / Codex - Decision: The final stored roll result becomes the retried result plus the retry bonus, while the original first result remains visible in the breakdown and log summary. Rationale: An automatic retry should materially change the outcome, not merely annotate the failed first attempt. Keeping the first attempt visible preserves auditability. Date/Author: 2026-04-04 / Codex - Decision: The feature uses “retry” terminology throughout the docs and code, with the persisted Boolean named `RolemasterAutoRetry`. Rationale: The user explicitly rejected “skipp” as unclear. `RolemasterAutoRetry` keeps the toggle readable in code, API payloads, and UI text. Date/Author: 2026-04-14 / Codex ## Outcomes & Retrospective Milestones 1 and 2 are complete. The repo now persists and validates a per-skill `RolemasterAutoRetry` toggle, executes one automatic retry for eligible Rolemaster open-ended percentile results, records retry-aware breakdown text and attempt-tagged dice detail, and surfaces retry summaries plus `rs5` or `rs10` badges in the compact log. Browser coverage and final end-to-end polish still remain before the feature is complete. ## Context and Orientation `RpgRoller` is an ASP.NET Core plus Blazor Server application. The gameplay state lives in memory as plain domain objects under `RpgRoller/Domain`, is persisted through EF Core and SQLite under `RpgRoller/Data` and `RpgRoller/Hosting`, and is exposed through Minimal API endpoints under `RpgRoller/Api`. Blazor UI components under `RpgRoller/Components` render the authenticated workspace and call those APIs. The Rolemaster roll implementation is isolated well enough that this feature can be added without disturbing D6 or D&D 5e. `RpgRoller/Services/RolemasterRollEngine.cs` currently handles both ordinary Rolemaster rolls and open-ended percentile rolls. `RpgRoller/Services/RollEngine.cs` dispatches by ruleset. `RpgRoller/Services/GameRollService.cs` records rolls, persists them, and builds campaign-log list entries. Skill configuration flows through several layers. The domain model types are `RpgRoller/Domain/GameModels.cs`. EF Core maps them in `RpgRoller/Data/RpgRollerDbContext.cs`. API request and response records live in `RpgRoller/Contracts/ApiContracts.cs`. The backend service methods that validate and save skill edits live in `RpgRoller/Services/GameSkillService.cs` and `RpgRoller/Services/SkillDefinitionValidator.cs`. Blazor form state models live in `RpgRoller/Components/Pages/Home.Models.cs`. The create and edit skill modal lives in `RpgRoller/Components/Pages/HomeControls/SkillFormModal.razor` and `.razor.cs`. Character-panel group editing lives in `RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor` and `.razor.cs`. Campaign log cards are compact list entries, not full detail records. The compact card summary text and badge codes are composed in `RpgRoller/Services/CampaignLogSummaryBuilder.cs` and rendered in `RpgRoller/Components/Pages/HomeControls/CampaignLogPanel.razor` and `.razor.cs`. Roll detail dice chips are rendered by `RpgRoller/Components/Pages/HomeControls/RollDiceStrip.razor.cs`. If retry metadata needs to survive app restarts, it must either be encoded in the persisted dice JSON or in the persisted breakdown string because `RollLogEntry` currently stores only `Result`, `Breakdown`, and serialized `Dice`. ## Plan of Work Start by extending the skill model so the retry toggle has a place to live. Add a non-nullable Boolean property named `RolemasterAutoRetry` to `Skill` in `RpgRoller/Domain/GameModels.cs`. Thread that property through the DTO surface in `RpgRoller/Contracts/ApiContracts.cs` by extending `CreateSkillRequest`, `UpdateSkillRequest`, `SkillSummary`, and `CharacterSheetSkill`. Keep skill groups unchanged. Update `RpgRoller/Services/GameDtoMapper.cs` so summaries and character sheets include the new flag. Update cloning or state-copy helpers that copy `Skill` objects, including `RpgRoller/Services/GameStateCloneFactory.cs`, so the flag persists through load/save cycles. Add a database migration for the new column. Update `RpgRoller/Data/RpgRollerDbContext.cs` so EF Core maps `RolemasterAutoRetry` as a required Boolean with a default value of `false`. Generate a migration under `RpgRoller/Migrations` that adds the column to `Skills` with a safe default. Do not widen this migration to unrelated schema changes. If existing migration coverage fixtures or history assertions mention the newest migration id, update them so startup migration tests remain accurate. Once the property exists, tighten validation. Extend `RpgRoller/Services/SkillDefinitionValidator.cs` so its return value includes the retry flag. Validation must accept `RolemasterAutoRetry = true` only when the ruleset is Rolemaster and the parsed expression kind is `RolemasterOpenEndedPercentile`. For every other ruleset or expression kind, the backend must reject `true` with a specific validation error such as `invalid_rolemaster_retry`. When the flag is false, behavior must remain unchanged. Update `RpgRoller/Services/GameSkillService.cs`, `RpgRoller/Services/IGameService.cs`, and `RpgRoller/Api/SkillEndpoints.cs` so skill creation and update calls carry the extra argument all the way through. Implement the retry rule in a dedicated helper instead of burying threshold math inside the roll engine. Add a new backend helper file, for example `RpgRoller/Services/RolemasterRetryPolicy.cs`, with a small API such as `public static int? ResolveSkippRetryBonus(int firstResult)`. This helper must return `5`, `10`, or `null` according to the exact windows described earlier. Put the thresholds here so both tests and the roll engine read the same source of truth. Then extend the Rolemaster roll engine. Change `RpgRoller/Services/RollEngine.cs` so Rolemaster rolls can receive the new per-skill toggle. Change `RpgRoller/Services/RolemasterRollEngine.cs` so open-ended percentile rolls do the following: compute the first attempt exactly as today; evaluate `RolemasterRetryPolicy.ResolveSkippRetryBonus(firstResult)`; if the toggle is off or the policy returns `null`, return the original result unchanged; otherwise perform one more full roll of the same parsed expression, calculate that second attempt’s result exactly as a normal Rolemaster open-ended roll, add the retry bonus to that second attempt, and store that number as the final roll result. Make the retry understandable in persisted output. Introduce an optional `Attempt` property on `RollDieResult` in `RpgRoller/Contracts/ApiContracts.cs` so dice from the original roll can be tagged as attempt `1` and retry dice as attempt `2`. Keep the existing `Sequence` property semantics within each attempt. Update `RolemasterRollEngine` to set `Attempt = 1` for the original dice and `Attempt = 2` for retry dice. Update `RpgRoller/Components/Pages/HomeControls/RollDiceStrip.razor.cs` so the generated title text mentions “attempt 1” or “retry attempt” when `Attempt` is present. This keeps the detail view legible without redesigning the dice strip layout. The breakdown string must become parseable and human-readable. Extend `RpgRoller/Services/RollBreakdownFormatter.cs` with a formatter dedicated to retry output. Preserve the current breakdown text for each individual attempt, then combine them into a single breakdown string shaped like: ; retry(+5): ; final= or ; retry(+10): ; final= This format is intentionally simple. It is readable in the UI, survives persistence without new tables, and gives `CampaignLogSummaryBuilder` a stable marker to detect retry badges later. Do not change non-retry breakdown formatting. Update the compact campaign-log helpers to surface the new special result. `RpgRoller/Services/CampaignLogSummaryBuilder.cs` should accept the stored breakdown when building badges and compact summaries. Add badge codes `rs5` and `rs10` for “Retry +5” and “Retry +10”. When a retry marker is present in the breakdown, append a short retry note to the Rolemaster compact summary, for example `| retry +5` or `| retry +10`, while keeping existing `rf`, `r66`, and `r100` behavior intact. Update `RpgRoller/Components/Pages/HomeControls/CampaignLogPanel.razor.cs` so those new badge codes render as visible labels on the log card. After the backend shape is stable, wire the UI toggle. Extend `SkillFormModel` in `RpgRoller/Components/Pages/Home.Models.cs` with `RolemasterAutoRetry`. Update `SkillFormModal.razor.cs` so the form copies the value from `InitialModel`, validates it only for Rolemaster open-ended expressions, and clears it automatically when the skill expression becomes invalid for retry. Update `SkillFormModal.razor` to show a checkbox only in the Rolemaster open-ended branch, near the fumble-range input, with concise help text that explains the exact windows. Update `CharacterPanel.razor.cs` so create and edit skill dialogs pass the property in their initial models and request payloads. Do not add a corresponding group-level control. Expose the setting in the workspace read model so the user can see it again after save. Extend `WorkspaceState.SkillDefinitionLabel(...)` and `RulesetFormHelpers.DescribeRolemasterExpression(...)` as needed so a Rolemaster open-ended skill with retry enabled renders a label that includes the retry rule in compact form, for example `Open-ended percentile: d100!+15, fumble <= 5, auto retry`. Keep the label short enough that existing layout remains intact. Finally, update documentation. `README.md` must describe the new Rolemaster skill option, the retry windows, and the fact that the campaign log now shows retry badges. If an example command or screenshot-free narrative is needed, keep it textual and current rather than writing a historical change note. ## Milestones ### Milestone 1: Persist and validate the skill toggle At the end of this milestone, the repository can create, read, update, persist, and reload a Rolemaster skill with the retry flag, but no roll behavior changes yet. The new property exists in the domain model, EF Core schema, API contracts, service signatures, DTOs, and Blazor form state. Validation rejects impossible combinations such as D6 plus retry enabled or Rolemaster `d10` plus retry enabled. Run the service and API tests that cover skill validation and persistence from `D:\Code\RpgRoller`: dotnet test RpgRoller.Tests/RpgRoller.Tests.csproj --filter "FullyQualifiedName~ServiceHelperExtractionTests|FullyQualifiedName~ServicePersistenceTests|FullyQualifiedName~WorkspaceQueryServiceTests|FullyQualifiedName~CampaignApiTests" Acceptance is that new tests prove the flag survives round trips and that stale invalid values are rejected before any roll logic is touched. ### Milestone 2: Execute and record automatic retry At the end of this milestone, an eligible Rolemaster open-ended skill roll automatically performs one retry and stores a final result based on the retry attempt plus the policy bonus. Non-eligible or disabled skills still behave exactly as before. The detailed breakdown string explains the first attempt, the retry bonus, the retry attempt, and the final result. The detail dice strip distinguishes original and retry attempts through titles. Run targeted Rolemaster service and API tests from `D:\Code\RpgRoller`: dotnet test RpgRoller.Tests/RpgRoller.Tests.csproj --filter "FullyQualifiedName~ServiceRolemasterRollTests|FullyQualifiedName~RolemasterApiTests|FullyQualifiedName~PayloadBudgetTests" Acceptance is that there are explicit tests for a `+5` retry case, a `+10` retry case, and a disabled-skill case that proves the old result path remains unchanged. ### Milestone 3: Surface the retry in the workspace and lock the behavior At the end of this milestone, the create and edit skill UI exposes the toggle only when valid, saved skills show the toggle again when reopened, log cards display retry badges, and browser smoke coverage proves the experience without manual clicking. Documentation is updated and the full local parity script passes. Run from `D:\Code\RpgRoller`: pwsh ./scripts/ci-local.ps1 Acceptance is that the run finishes successfully, the new browser test proves the checkbox and badge behavior, and no unrelated payload-budget or smoke-test regressions appear. ## Concrete Steps Work from `D:\Code\RpgRoller`. 1. Edit the domain model and contracts first so every later compiler error points toward the remaining call sites. Update `RpgRoller/Domain/GameModels.cs`, `RpgRoller/Contracts/ApiContracts.cs`, `RpgRoller/Services/GameDtoMapper.cs`, `RpgRoller/Services/GameStateCloneFactory.cs`, `RpgRoller/Services/IGameService.cs`, `RpgRoller/Services/GameSkillService.cs`, and `RpgRoller/Api/SkillEndpoints.cs`. 2. Add the EF Core schema update in `RpgRoller/Data/RpgRollerDbContext.cs`, generate the migration in `RpgRoller/Migrations`, and update any migration-history assertions in `RpgRoller.Tests/HostingCoverageTests.cs` if they depend on the latest migration name. 3. Add backend validation and retry policy code. Extend `RpgRoller/Services/SkillDefinitionValidator.cs`. Add `RpgRoller/Services/RolemasterRetryPolicy.cs`. Update `RpgRoller/Services/RollEngine.cs`, `RpgRoller/Services/RolemasterRollEngine.cs`, and `RpgRoller/Services/RollBreakdownFormatter.cs`. 4. Update compact log helpers and UI consumers. Change `RpgRoller/Services/CampaignLogSummaryBuilder.cs`, `RpgRoller/Services/GameRollService.cs`, `RpgRoller/Components/Pages/HomeControls/CampaignLogPanel.razor.cs`, and `RpgRoller/Components/Pages/HomeControls/RollDiceStrip.razor.cs`. 5. Update form models and skill forms. Change `RpgRoller/Components/Pages/Home.Models.cs`, `RpgRoller/Components/Pages/HomeControls/SkillFormModal.razor`, `RpgRoller/Components/Pages/HomeControls/SkillFormModal.razor.cs`, `RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor.cs`, `RpgRoller/Components/Pages/WorkspaceState.cs`, and `RpgRoller/Components/Pages/HomeControls/RulesetFormHelpers.cs`. 6. Add or update tests before running the full parity script. The expected primary test files are `RpgRoller.Tests/Services/ServiceRolemasterRollTests.cs`, `RpgRoller.Tests/Services/ServiceHelperExtractionTests.cs`, `RpgRoller.Tests/Services/ServicePersistenceTests.cs`, `RpgRoller.Tests/Services/ServiceRollHelperTests.cs`, `RpgRoller.Tests/Services/WorkspaceStateTests.cs`, `RpgRoller.Tests/Api/RolemasterApiTests.cs`, and `tests/e2e/smoke.spec.js`. 7. Update `README.md`, run the full local parity script, inspect `git status`, and commit with a brief message only after all validations pass. Expected final command transcript: ==> Run tests Passed! - Failed: 0, Passed: , Skipped: 0, Total: ==> Enforce coverage thresholds Line coverage: Branch coverage: ==> Run Playwright smoke test CI checks passed. ## Validation and Acceptance Validation is complete only when all of the following are true. The backend proves rule correctness. There must be a unit test where the first attempt result is `78` and the stored final result comes from a retry with `+5`. There must be another where the first attempt result is `96` or another value inside the second band and the stored final result comes from a retry with `+10`. There must be a test where the skill toggle is disabled and an otherwise eligible first result still does not retry. The persistence layer proves round-trip safety. A test must create a skill with the toggle enabled, persist the database, reload state, and confirm the skill still exposes `RolemasterAutoRetry = true`. The API layer proves contract shape. A test must create and update a Rolemaster open-ended skill through HTTP and confirm the toggle round-trips through `SkillSummary`, `CharacterSheet`, and roll results. Invalid combinations must return a concrete API error rather than silently coercing the value. The compact log proves user-visible behavior. A service or API test must show that a retried roll produces `rs5` or `rs10` in the log entry badges and that the summary text includes a retry marker. A browser test must create or use a retry-enabled Rolemaster skill, roll it, and verify that the log card shows the retry badge without expanding the detail row first. The whole repo must still pass the local parity script: pwsh ./scripts/ci-local.ps1 ## Idempotence and Recovery This plan is safe to execute incrementally. Compiler errors after the first contract changes are expected because the repository is strongly typed; fix the next call site rather than backing out partial edits. If the EF Core migration step is blocked because the development app or tests are holding the SQLite file open, stop the running `dotnet` process, retry the migration command once, and continue. This repo explicitly allows that recovery step after database changes. If a partial implementation leaves the feature half-wired, the safe recovery path is to keep the new property and validation in place, set the toggle to default `false`, and continue forward until tests pass. Do not delete unrelated user changes from the worktree to recover. ## Artifacts and Notes The key code paths to revisit while implementing are these: RpgRoller/Services/RolemasterRollEngine.cs RpgRoller/Services/CampaignLogSummaryBuilder.cs RpgRoller/Components/Pages/HomeControls/SkillFormModal.razor.cs RpgRoller/Components/Pages/HomeControls/CampaignLogPanel.razor.cs The intended retry badge mapping is: rs5 -> Retry +5 rs10 -> Retry +10 The intended breakdown marker is: ; retry(+5): ; retry(+10): These marker strings are chosen so they can be detected cheaply without adding another persisted table or JSON blob. ## Interfaces and Dependencies At the end of the implementation, these interfaces and shapes must exist. In `RpgRoller/Domain/GameModels.cs`, `Skill` must expose: public bool RolemasterAutoRetry { get; set; } In `RpgRoller/Contracts/ApiContracts.cs`, these records must include the new Boolean: public sealed record CreateSkillRequest(..., int? FumbleRange = null, bool RolemasterAutoRetry = false); public sealed record UpdateSkillRequest(..., int? FumbleRange = null, bool RolemasterAutoRetry = false); public sealed record SkillSummary(..., int? FumbleRange, bool RolemasterAutoRetry); public sealed record CharacterSheetSkill(..., int? FumbleRange, bool RolemasterAutoRetry); `RollDieResult` must gain an optional attempt marker: public int? Attempt { get; init; } In `RpgRoller/Services/RolemasterRetryPolicy.cs`, define: public static int? ResolveSkippRetryBonus(int firstResult) In `RpgRoller/Services/SkillDefinitionValidator.cs`, the validation result tuple must carry the retry flag so backend callers do not need to re-derive validity from raw request data. In `RpgRoller/Services/RollEngine.cs`, the Rolemaster dispatch signature must carry the skill toggle through to `RolemasterRollEngine`. In `RpgRoller/Services/RolemasterRollEngine.cs`, the open-ended roll path must still return: (int Total, string Breakdown, IReadOnlyList Dice) but it must now build that tuple from one or two attempts depending on the retry policy. Revision note: created this ExecPlan on 2026-04-04 because the user requested a repository-local execution plan in `TASKS.md` before implementation.