Fix Rolemaster low-end open roll math
This commit is contained in:
@@ -64,7 +64,7 @@ Gameplay capabilities now include:
|
|||||||
- 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 currently recognizes `2d10+48`, `d100+4`, and `d100!+85`, including Rolemaster-only negative modifiers such as `d100-15`
|
||||||
- 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 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`
|
- 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
|
||||||
- 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
|
||||||
|
|
||||||
|
|||||||
@@ -54,9 +54,9 @@ public sealed class RolemasterApiTests : ApiTestBase
|
|||||||
var logPage = await GetAsync<CampaignLogPage>(client, $"/api/campaigns/{campaign.Id}/log/page?limit=5");
|
var logPage = await GetAsync<CampaignLogPage>(client, $"/api/campaigns/{campaign.Id}/log/page?limit=5");
|
||||||
var detail = await GetAsync<CampaignRollDetail>(client, $"/api/rolls/{roll.RollId}");
|
var detail = await GetAsync<CampaignRollDetail>(client, $"/api/rolls/{roll.RollId}");
|
||||||
|
|
||||||
Assert.Equal(-119, roll.Result);
|
Assert.Equal(-124, roll.Result);
|
||||||
Assert.Equal("5-(97+100+12)+85=-119", roll.Breakdown);
|
Assert.Equal("(05) -97 -100 -12 +85 = -124", roll.Breakdown);
|
||||||
Assert.Equal("5 - (97 + 100 + 12) | open-ended low", Assert.Single(logPage.Entries).SummaryText);
|
Assert.Equal("(05) -97 -100 -12 | open-ended low", Assert.Single(logPage.Entries).SummaryText);
|
||||||
Assert.Equal(roll.Breakdown, detail.Breakdown);
|
Assert.Equal(roll.Breakdown, detail.Breakdown);
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
detail.Dice,
|
detail.Dice,
|
||||||
@@ -64,7 +64,7 @@ public sealed class RolemasterApiTests : ApiTestBase
|
|||||||
{
|
{
|
||||||
Assert.Equal(RollDieKinds.RolemasterOpenEndedInitial, die.Kind);
|
Assert.Equal(RollDieKinds.RolemasterOpenEndedInitial, die.Kind);
|
||||||
Assert.Equal(1, die.Sequence);
|
Assert.Equal(1, die.Sequence);
|
||||||
Assert.Equal(5, die.SignedContribution);
|
Assert.Null(die.SignedContribution);
|
||||||
},
|
},
|
||||||
die =>
|
die =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -127,9 +127,9 @@ public sealed class ServiceRolemasterRollTests
|
|||||||
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(-119, roll.Result);
|
Assert.Equal(-124, roll.Result);
|
||||||
Assert.Equal("5-(97+100+12)+85=-119", roll.Breakdown);
|
Assert.Equal("(05) -97 -100 -12 +85 = -124", roll.Breakdown);
|
||||||
Assert.Equal("5 - (97 + 100 + 12) | open-ended low", Assert.Single(logPage.Entries).SummaryText);
|
Assert.Equal("(05) -97 -100 -12 | open-ended low", Assert.Single(logPage.Entries).SummaryText);
|
||||||
Assert.Collection(
|
Assert.Collection(
|
||||||
roll.Dice,
|
roll.Dice,
|
||||||
die =>
|
die =>
|
||||||
@@ -137,7 +137,7 @@ public sealed class ServiceRolemasterRollTests
|
|||||||
Assert.Equal(5, die.Roll);
|
Assert.Equal(5, die.Roll);
|
||||||
Assert.Equal(1, die.Sequence);
|
Assert.Equal(1, die.Sequence);
|
||||||
Assert.Equal(RollDieKinds.RolemasterOpenEndedInitial, die.Kind);
|
Assert.Equal(RollDieKinds.RolemasterOpenEndedInitial, die.Kind);
|
||||||
Assert.Equal(5, die.SignedContribution);
|
Assert.Null(die.SignedContribution);
|
||||||
},
|
},
|
||||||
die =>
|
die =>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ public partial class RollDiceStrip
|
|||||||
|
|
||||||
private static string RollDieDisplay(RollDieResult die)
|
private static string RollDieDisplay(RollDieResult die)
|
||||||
{
|
{
|
||||||
|
if (string.Equals(die.Kind, RollDieKinds.RolemasterOpenEndedInitial, StringComparison.Ordinal) && !die.SignedContribution.HasValue)
|
||||||
|
return $"({die.Roll:00})";
|
||||||
|
|
||||||
return die.Kind switch
|
return die.Kind switch
|
||||||
{
|
{
|
||||||
RollDieKinds.RolemasterOpenEndedHigh => $"+{die.Roll}",
|
RollDieKinds.RolemasterOpenEndedHigh => $"+{die.Roll}",
|
||||||
@@ -101,7 +104,7 @@ public partial class RollDiceStrip
|
|||||||
labels.Add("Rolemaster percentile");
|
labels.Add("Rolemaster percentile");
|
||||||
break;
|
break;
|
||||||
case RollDieKinds.RolemasterOpenEndedInitial:
|
case RollDieKinds.RolemasterOpenEndedInitial:
|
||||||
labels.Add("Rolemaster open-ended initial");
|
labels.Add(die.SignedContribution.HasValue ? "Rolemaster open-ended initial" : "Rolemaster low-end trigger (ignored in total)");
|
||||||
break;
|
break;
|
||||||
case RollDieKinds.RolemasterOpenEndedHigh:
|
case RollDieKinds.RolemasterOpenEndedHigh:
|
||||||
labels.Add($"Rolemaster high follow-up (+{die.Roll})");
|
labels.Add($"Rolemaster high follow-up (+{die.Roll})");
|
||||||
|
|||||||
@@ -1007,12 +1007,13 @@ public sealed class GameService : IGameService
|
|||||||
{
|
{
|
||||||
var initialRoll = m_DiceRoller.Roll(expression.Sides);
|
var initialRoll = m_DiceRoller.Roll(expression.Sides);
|
||||||
var followUpRolls = new List<int>();
|
var followUpRolls = new List<int>();
|
||||||
|
int? initialContribution = initialRoll <= fumbleRange ? null : initialRoll;
|
||||||
var dice = new List<RollDieResult>
|
var dice = new List<RollDieResult>
|
||||||
{
|
{
|
||||||
CreateRolemasterDie(initialRoll, 1, RollDieKinds.RolemasterOpenEndedInitial, initialRoll)
|
CreateRolemasterDie(initialRoll, 1, RollDieKinds.RolemasterOpenEndedInitial, initialContribution)
|
||||||
};
|
};
|
||||||
|
|
||||||
var baseTotal = initialRoll;
|
var baseTotal = initialRoll <= fumbleRange ? 0 : initialRoll;
|
||||||
var subtractFollowUps = false;
|
var subtractFollowUps = false;
|
||||||
if (initialRoll >= 96)
|
if (initialRoll >= 96)
|
||||||
{
|
{
|
||||||
@@ -1134,7 +1135,7 @@ public sealed class GameService : IGameService
|
|||||||
return followUpRolls;
|
return followUpRolls;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RollDieResult CreateRolemasterDie(int roll, int sequence, string kind, int signedContribution)
|
private static RollDieResult CreateRolemasterDie(int roll, int sequence, string kind, int? signedContribution)
|
||||||
{
|
{
|
||||||
return new(roll, false, false, false, false, kind == RollDieKinds.RolemasterOpenEndedHigh, sequence, kind, signedContribution);
|
return new(roll, false, false, false, false, kind == RollDieKinds.RolemasterOpenEndedHigh, sequence, kind, signedContribution);
|
||||||
}
|
}
|
||||||
@@ -1150,6 +1151,18 @@ public sealed class GameService : IGameService
|
|||||||
|
|
||||||
private static string BuildRolemasterOpenEndedBreakdown(int initialRoll, IReadOnlyList<int> followUpRolls, bool subtractFollowUps, int modifier, int total)
|
private static string BuildRolemasterOpenEndedBreakdown(int initialRoll, IReadOnlyList<int> followUpRolls, bool subtractFollowUps, int modifier, int total)
|
||||||
{
|
{
|
||||||
|
if (subtractFollowUps)
|
||||||
|
{
|
||||||
|
var segments = new List<string> { $"({FormatRolemasterTriggerRoll(initialRoll)})" };
|
||||||
|
segments.AddRange(followUpRolls.Select(roll => $"-{roll}"));
|
||||||
|
if (modifier > 0)
|
||||||
|
segments.Add($"+{modifier}");
|
||||||
|
else if (modifier < 0)
|
||||||
|
segments.Add(modifier.ToString());
|
||||||
|
|
||||||
|
return $"{string.Join(" ", segments)} = {total}";
|
||||||
|
}
|
||||||
|
|
||||||
var core = initialRoll.ToString();
|
var core = initialRoll.ToString();
|
||||||
if (followUpRolls.Count > 0)
|
if (followUpRolls.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -1160,6 +1173,11 @@ public sealed class GameService : IGameService
|
|||||||
return BuildModifierBreakdown(core, modifier, total);
|
return BuildModifierBreakdown(core, modifier, total);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string FormatRolemasterTriggerRoll(int roll)
|
||||||
|
{
|
||||||
|
return roll.ToString("00");
|
||||||
|
}
|
||||||
|
|
||||||
private static string BuildModifierBreakdown(string core, int modifier, int total)
|
private static string BuildModifierBreakdown(string core, int modifier, int total)
|
||||||
{
|
{
|
||||||
return modifier switch
|
return modifier switch
|
||||||
@@ -1404,7 +1422,7 @@ public sealed class GameService : IGameService
|
|||||||
.Select(die => die.Roll.ToString())
|
.Select(die => die.Roll.ToString())
|
||||||
.ToArray();
|
.ToArray();
|
||||||
if (lowFollowUps.Length > 0)
|
if (lowFollowUps.Length > 0)
|
||||||
return $"{openEndedInitial.Roll} - ({string.Join(" + ", lowFollowUps)}) | open-ended low";
|
return $"({FormatRolemasterTriggerRoll(openEndedInitial.Roll)}) {string.Join(" ", lowFollowUps.Select(roll => $"-{roll}"))} | open-ended low";
|
||||||
|
|
||||||
return $"{openEndedInitial.Roll} | open-ended";
|
return $"{openEndedInitial.Roll} | open-ended";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user