# Rolemaster Support Plan ## Goal Extend the app with a new `rolemaster` ruleset that supports generic standard Rolemaster expressions plus the special open-ended percentile case: 1. Standard expressions: `NdS + x`, with an implicit dice count of `1` when no digit appears before `d` 2. Open-ended percentile: `d100! + x` with Rolemaster low-end and high-end behavior 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 - A Rolemaster campaign chooses the new `rolemaster` ruleset at campaign creation time. - Rolemaster skills are ruleset-specific and should not reuse D6-only options such as `WildDice` and `AllowFumble`. - Open-ended percentile behavior is defined as: - Roll the first `d100`. - 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. - Recursive follow-up rolls are high-ended only. - Standard Rolemaster rolls do not open-end and do not use a fumble range. - The modifier `x` should be an integer. - 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. - Existing D6 and D&D 5e behavior must remain unchanged. - The migration must be validated against a copied temp-file instance of `RpgRoller/App_Data/rpgroller.development.db`, which currently contains a D6 campaign. ## Clarifications - The Rolemaster modifier should allow negative values only for Rolemaster. - The standard fumble range for open-ended percentile skills is `0-5`, but can be configured individually for each skill. - The fumble range has to be guaranteed to be less than 96. - 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 - Rolemaster uses expression parsing with canonical syntax: - standard roll with implicit count: `d10` - standard roll with explicit count: `15d10` - standard percentile: `d100+4` - open-ended percentile: `d100!+85` - negative modifiers are valid only for Rolemaster, for example `d100-15` - open-ended syntax is only valid for `d100!` ## Architecture Guardrails - Keep minimal API endpoint handlers thin and continue to place gameplay validation and roll execution in services. - Keep Blazor components focused on form state, rendering, and API orchestration; do not duplicate Rolemaster rules in component code. - Keep request/response DTOs separate from domain/persistence models. - Preserve the current lazy log-detail loading flow so Rolemaster detail stays out of hot list payloads. - Prefer additive changes to contracts and storage over replacing current D6 / D&D shapes outright. ## Backward Compatibility - Existing D6 and D&D 5e API request and response fields must remain valid throughout the Rolemaster rollout. - Existing persisted skills, skill groups, and roll log rows must continue to load without migration-time data loss. - Startup migration against the existing development SQLite database shape must succeed without data loss or post-migration behavior regressions for legacy D6 data. - Any richer die metadata must deserialize safely for old rows that only contain the current `RollDieResult` shape. - Payload budgets for roster, log page, roll mutation responses, and lazy-loaded roll detail must remain explicitly guarded by tests. ## Design Direction ### 1. Add a new ruleset Introduce `RulesetKind.Rolemaster` and expose it through `DiceRules` and campaign ruleset APIs. Tasks: - Add `Rolemaster` to `RulesetKind`. - Update `DiceRules.TryParseRulesetId`. - Update `DiceRules.ToRulesetId`. - Add `rolemaster` to `DiceRules.SupportedRulesets`. - Update tests that enumerate supported rulesets and campaign creation behavior. Affected areas: - `RpgRoller/Domain/GameModels.cs` - `RpgRoller/Services/DiceRules.cs` - `RpgRoller/Contracts/ApiContracts.cs` - related service and API tests ### 2. Refactor skill definition from "expression + D6 options" to "ruleset-aware roll definition" The current model is D6-shaped: - `DiceRollDefinition` - `WildDice` - `AllowFumble` That shape is not a clean fit for Rolemaster. The Rolemaster extension should still preserve expression parsing, but it needs a ruleset-aware internal definition so the app can validate, execute, and display Rolemaster rolls safely. Recommended model additions: - Keep existing fields working for D6 and D&D 5e. - Preserve `DiceRollDefinition` as the authoritative user-facing input for D6, D&D 5e, and Rolemaster. - Add only the extra Rolemaster fields that cannot be encoded cleanly in the expression itself, specifically the fumble range. - Centralize parsing and validation into a single ruleset-aware validator that returns a canonical internal representation. Tasks: - Define a ruleset-aware skill definition model in domain/contracts. - Keep `DiceRollDefinition` as persisted state and define canonical formatting rules for Rolemaster expressions. - Add mapping helpers so UI and API get a stable, ruleset-aware DTO. - Ensure skill groups can store the same Rolemaster defaults as skills. Affected areas: - `RpgRoller/Domain/GameModels.cs` - `RpgRoller/Contracts/ApiContracts.cs` - `RpgRoller/Services/GameService.cs` - `RpgRoller/Components/Pages/Home.Models.cs` ### 3. Add Rolemaster-specific validation Current validation is split between generic expression parsing and D6 option validation. Rolemaster needs its own validation layer. Validation rules to add: - standard Rolemaster expressions - supports generic `NdS` syntax with any supported side count - assumes a dice count of `1` when the count is omitted before `d` - integer modifier required - no fumble range - `RolemasterOpenEndedPercentile` - fixed base roll `1d100` - integer modifier required - fumble range required and validated - fumble range must be below `96` - reject D6-only options for Rolemaster skills - reject Rolemaster-only fields for D6 / D&D 5e skills - reject negative modifiers for D6 / D&D 5e expressions - accept negative modifiers for Rolemaster expressions only Tasks: - Replace or extend `ValidateSkillDefinition`. - Extend `DiceRules.ParseExpression` with a Rolemaster-focused parser/validator path. - Add canonical Rolemaster expression parsing rules: - `d10`, `2d10+48`, and `15d10-15` for generic standard rolls - `d100+4` and `d100-20` for standard 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. - Add canonical display formatting for Rolemaster skills, for example: - `Rolemaster: d10` - `Rolemaster: 15d10+15` - `Open-ended percentile: OE 1d100+48, fumble <= 5` - Add explicit regression tests that D6 / D&D still reject negative modifiers after Rolemaster support lands. Affected areas: - `RpgRoller/Services/DiceRules.cs` - `RpgRoller/Services/GameService.cs` - `RpgRoller.Tests/Services/DiceRulesTests.cs` - `RpgRoller.Tests/Services/ServiceSkillGroupAndOwnershipTests.cs` - `RpgRoller.Tests/Services/ServiceSkillRollTests.cs` ### 4. Implement Rolemaster roll execution Add a new Rolemaster execution path alongside the existing D6 and standard roll logic. Recommended internal shape: - `ComputeRoll` dispatches by ruleset and roll kind. - Add dedicated methods: - `ComputeRolemasterStandardRoll` - `ComputeRolemasterOpenEndedRoll` - helper `RollRolemasterHighOpenEndedChain` Open-ended algorithm: - Roll first `d100`. - Start total with first roll. - If first roll `>= 96`: - roll one or more high-ended follow-up `d100`s - add all follow-up rolls - Else if first roll `<= fumble range`: - roll one or more high-ended follow-up `d100`s - subtract the follow-up total - Apply modifier after base/follow-up math. - Produce a full breakdown string. - Persist enough die detail to reconstruct the event in log detail and UI. Tasks: - Extend `ComputeRoll` dispatch. - Add Rolemaster-specific roll methods. - Parse Rolemaster expressions into an internal roll definition before execution rather than branching on raw strings in multiple places. - Decide whether the current `RollDieResult` DTO is sufficient. - likely not sufficient, because it only has `Crit`, `Fumble`, `Wild`, `Removed`, `Added` - Recommended DTO extension: - `Sequence` or `Order` - `Phase` / `Kind` such as `initial`, `open-ended-high`, `open-ended-low-subtract` - `SignedContribution` or equivalent metadata for subtraction - Update log summary generation so Rolemaster rolls remain understandable in compact log entries without bloating list payloads. - Keep lazy detail expansion as the place where the full Rolemaster breakdown and rich die metadata are loaded. Affected areas: - `RpgRoller/Services/GameService.cs` - `RpgRoller/Contracts/ApiContracts.cs` - `RpgRoller/Contracts/RpgRollerJsonSerializerContext.cs` - `RpgRoller/Components/Pages/HomeControls/RollDiceStrip.razor.cs` ### 5. Update persistence and migrations Rolemaster support needs persisted structured fields for skills and skill groups, and possibly richer serialized roll-die details. Tasks: - Update `Skill` and `SkillGroup` EF models. - Update `RpgRollerDbContext` configuration. - Add EF Core migration for new Rolemaster fields. - Preserve existing data for D6 / D&D 5e campaigns. - Decide migration defaults for legacy rows. - Update `SqliteSchemaUpgrader` coverage expectations if schema assumptions change. - Verify serialized roll log payloads remain readable for old entries after DTO extension. - Ensure additive schema changes allow mixed old/new data during migration and startup upgrade. - Validate the real startup migration path against a copied temp-file instance of `RpgRoller/App_Data/rpgroller.development.db`. - Verify that the migrated copy still loads the existing D6 campaign data and that legacy D6 skills remain rollable after startup migration. Data migration recommendations: - Legacy D6 / D&D 5e skills should retain current behavior with deterministic defaults. - New Rolemaster fields should be nullable or have safe defaults during migration. - Avoid destructive migration patterns; favor additive schema evolution. - Never run destructive migration verification against the source development DB file; always copy it first and test the copy. Affected areas: - `RpgRoller/Data/RpgRollerDbContext.cs` - `RpgRoller/Migrations/*` - `RpgRoller/Hosting/SqliteSchemaUpgrader.cs` - `RpgRoller.Tests/HostingCoverageTests.cs` ### 6. Expose Rolemaster in APIs and frontend models The UI currently assumes only `IsD6` versus "not D6". That binary split will not scale to Rolemaster. Tasks: - Replace `IsD6` branching with a ruleset-aware UI model. - Extend skill and skill-group request/response contracts with Rolemaster fields. - Ensure campaign creation UI includes `Rolemaster` in the ruleset picker. - Update workspace models and display helpers so Rolemaster skill summaries render correctly. Recommended frontend model changes: - Introduce ruleset-aware form state rather than raw D6 toggles. - Keep "Expression" as the primary input, and provide Rolemaster-specific help text, examples, and validation when the selected campaign ruleset is `rolemaster`. - Show fumble range input only when the current Rolemaster expression is an open-ended percentile roll. - Use progressive disclosure so only the fields relevant to the current Rolemaster expression are shown. - 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. Affected areas: - `RpgRoller/Contracts/ApiContracts.cs` - `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` - `RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor.cs` - `RpgRoller/Components/Pages/Workspace.razor.cs` ### 7. Update roll visualization and log readability Rolemaster open-ended rolls will be hard to interpret if they reuse the current D6-oriented chip styling without extra context. Tasks: - Extend `RollDiceStrip` to distinguish: - initial percentile roll - added high-open-ended rolls - subtracted low-end follow-up rolls - Add tooltip or title text that explains each die's role in the chain. - Update compact log summaries so they surface open-ended behavior. - Ensure private/public visibility behavior remains unchanged. - Keep roll list rows dense and readable; reserve multi-line breakdown detail for expanded rows or detail fetches. Recommended display goals: - A normal percentile roll should read as simple and uncluttered. - A high-open-ended roll should clearly show chained additions. - A low-ended roll should clearly show subtraction, not just a list of numbers. Affected areas: - `RpgRoller/Components/Pages/HomeControls/RollDiceStrip.razor` - `RpgRoller/Components/Pages/HomeControls/RollDiceStrip.razor.cs` - `RpgRoller/Components/Pages/Workspace.razor.cs` - `RpgRoller/Services/GameService.cs` ### 8. Expand tests to preserve behavior and coverage This repo expects very high coverage and already has strong service/API regression tests. Rolemaster support should land with exhaustive tests, not just happy paths. Service-level tests to add: - parse and ruleset mapping for `rolemaster` - skill creation validation for each Rolemaster roll type - rejection of invalid fumble ranges - rejection of D6-only options on Rolemaster skills - rejection of negative modifiers on D6 / D&D skills - acceptance of negative modifiers on Rolemaster expressions - generic standard Rolemaster roll math - standard percentile roll math - open-ended high roll with one extra roll - open-ended high roll with recursive extra rolls - open-ended low roll with subtraction - open-ended low roll where the subtract chain itself recursively high-opens - breakdown formatting and die metadata correctness API tests to add: - create Rolemaster campaign - create/update skill groups with Rolemaster defaults - create/update Rolemaster skills - roll each Rolemaster skill type via API - verify log page and detail endpoints for Rolemaster rolls - verify old D6 / D&D contracts still round-trip without requiring Rolemaster-only fields - verify that a migrated copy of `RpgRoller/App_Data/rpgroller.development.db` still supports legacy D6 API flows after startup migration Frontend/regression checks: - Rolemaster ruleset appears in campaign creation UI - Rolemaster skill form shows correct conditional fields - roll results render correctly for open-ended cases - validation errors are announced and visible for invalid Rolemaster expressions / fumble ranges - the compact log remains readable and detail expansion shows the full Rolemaster breakdown Coverage-sensitive files likely needing tests: - `RpgRoller.Tests/Services/DiceRulesTests.cs` - `RpgRoller.Tests/Services/ServiceSkillRollTests.cs` - new dedicated `Rolemaster` service tests - `RpgRoller.Tests/Api/CampaignApiTests.cs` - `RpgRoller.Tests/HostingCoverageTests.cs` - `RpgRoller.Tests/PayloadBudgetTests.cs` ### 9. Documentation updates The repository instructions require related docs to be updated whenever behavior changes. Tasks: - Update `README.md` to list `Rolemaster` as a supported ruleset. - Document the three supported Rolemaster roll types. - Document the open-ended percentile algorithm, including: - `96+` recursive addition - `<= fumble range` recursive subtraction - Document any limits on modifier and fumble range values. - If API contracts change materially, update `openapi/RpgRoller.json`. ### 10. Delivery plan in small iterations Recommended implementation order: #### Iteration 1: ruleset plumbing - Add `Rolemaster` ruleset enum/id/display support. - Add campaign creation support. - Add parser/validator scaffolding for Rolemaster expressions and Rolemaster-only negative modifier support. - Add baseline tests for ruleset mapping and validation. #### Iteration 2: structured skill model - Preserve `DiceRollDefinition` as the canonical persisted expression and add only the extra persisted Rolemaster fields the expression cannot represent. - Add migration and schema tests. - Add startup-migration validation against a copied temp-file instance of `RpgRoller/App_Data/rpgroller.development.db`. - Update API contracts and service mapping. - Keep existing D6 / D&D behavior green. #### Iteration 3: roll engine - Implement generic standard and standard percentile execution. - Implement open-ended percentile execution with recursive high-end chaining and low-end subtraction. - Extend die-result metadata and breakdown formatting. - Add focused service tests for roll math and payload/detail serialization compatibility. #### Iteration 4: frontend editing - Update campaign ruleset selection UI. - Replace D6-only form branching with ruleset-aware skill editing. - Support Rolemaster skill groups and skills in create/edit flows. - Add accessible inline validation and progressive disclosure to the Rolemaster editor. - Verify with ephemeral Playwright. #### Iteration 5: log/detail polish and docs - Improve roll chip rendering and compact summaries for Rolemaster. - Re-run payload-budget checks and confirm lazy detail loading still holds. - Update README/OpenAPI/docs. - Run full local CI and coverage checks. ## Risks - The current skill contract is tightly coupled to D6-era fields, so a shallow patch will create long-term complexity. - The current `RollDieResult` type is too generic for clear Rolemaster open-ended auditing. - Allowing negative Rolemaster modifiers requires carefully scoping validation changes so D6/D&D rules do not loosen unintentionally. - Migration design needs care to avoid breaking legacy SQLite databases. - A migration that works on an empty test database but fails on the existing D6 production-shaped database would be a release blocker. - UI complexity will grow if ruleset-specific fields are bolted onto the current `IsD6` logic instead of replacing it with a ruleset-aware form model. - Richer Rolemaster die metadata could regress payload sizes or list readability if detail is not kept behind the existing lazy-load flow. ## Recommended Acceptance Criteria - A campaign can be created with ruleset id `rolemaster`. - Rolemaster skill groups and skills can be created and edited through API and UI. - Supported Rolemaster roll types are: - generic standard rolls `NdS + x`, with implicit dice count `1` for `dS` - open-ended percentile `d100! + x` with fumble range - Open-ended percentile rolls correctly: - add recursively on first-roll `96+` - subtract a recursive high-end chain on first-roll `<= fumble range` - Roll logs and detail views clearly show how a Rolemaster result was produced. - Existing D6 and D&D 5e flows remain unchanged. - App startup successfully migrates a copied temp-file instance of `RpgRoller/App_Data/rpgroller.development.db`. - The migrated copy preserves the existing D6 campaign data and passes legacy D6 read/roll regression checks after startup migration. - Local CI, coverage checks, and frontend verification all pass.