Add ruleset-aware Rolemaster editors
This commit is contained in:
@@ -64,6 +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 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 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 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
|
||||||
|
|||||||
@@ -42,20 +42,26 @@ public sealed class CharacterFormModel
|
|||||||
public sealed class SkillFormModel
|
public sealed class SkillFormModel
|
||||||
{
|
{
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string RulesetId { get; set; } = string.Empty;
|
||||||
public string DiceRollDefinition { get; set; } = string.Empty;
|
public string DiceRollDefinition { get; set; } = string.Empty;
|
||||||
public string SkillGroupId { get; set; } = string.Empty;
|
public string SkillGroupId { get; set; } = string.Empty;
|
||||||
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
|
||||||
{
|
{
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string RulesetId { get; set; } = string.Empty;
|
||||||
public string DiceRollDefinition { get; set; } = string.Empty;
|
public string DiceRollDefinition { get; set; } = string.Empty;
|
||||||
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
|
||||||
|
|||||||
@@ -152,13 +152,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<label for="skill-group-expression">Prototype expression</label>
|
<label for="skill-group-expression">Prototype expression</label>
|
||||||
<input id="skill-group-expression" @bind="SkillGroupState.Model.DiceRollDefinition" @bind:event="oninput"/>
|
<input id="skill-group-expression" value="@SkillGroupState.Model.DiceRollDefinition" @oninput="OnSkillGroupExpressionChanged"/>
|
||||||
|
<p class="field-help">@SkillGroupExpressionHelpText</p>
|
||||||
@if (SkillGroupState.Errors.TryGetValue("diceRollDefinition", out var expressionError))
|
@if (SkillGroupState.Errors.TryGetValue("diceRollDefinition", out var expressionError))
|
||||||
{
|
{
|
||||||
<p class="field-error">@expressionError</p>
|
<p class="field-error">@expressionError</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (IsD6)
|
@if (IsD6Ruleset)
|
||||||
{
|
{
|
||||||
<label for="skill-group-wild-dice">Prototype wild dice</label>
|
<label for="skill-group-wild-dice">Prototype wild dice</label>
|
||||||
<input id="skill-group-wild-dice" type="number" min="1" step="1" @bind="SkillGroupState.Model.WildDice"/>
|
<input id="skill-group-wild-dice" type="number" min="1" step="1" @bind="SkillGroupState.Model.WildDice"/>
|
||||||
@@ -170,6 +171,29 @@
|
|||||||
<label for="skill-group-allow-fumble">Prototype allow fumble</label>
|
<label for="skill-group-allow-fumble">Prototype allow fumble</label>
|
||||||
<input id="skill-group-allow-fumble" type="checkbox" @bind="SkillGroupState.Model.AllowFumble"/>
|
<input id="skill-group-allow-fumble" type="checkbox" @bind="SkillGroupState.Model.AllowFumble"/>
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
<label for="skill-group-fumble-range">Prototype fumble range</label>
|
||||||
|
<input id="skill-group-fumble-range" type="number" min="0" max="95" step="1" @bind="SkillGroupState.Model.FumbleRange"/>
|
||||||
|
<p class="field-help">Used only for open-ended percentile skills created from this group.</p>
|
||||||
|
@if (SkillGroupState.Errors.TryGetValue("fumbleRange", out var fumbleRangeError))
|
||||||
|
{
|
||||||
|
<p class="field-error">@fumbleRangeError</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@if (SkillGroupState.Errors.TryGetValue("character", out var characterError))
|
@if (SkillGroupState.Errors.TryGetValue("character", out var characterError))
|
||||||
{
|
{
|
||||||
@@ -192,7 +216,7 @@
|
|||||||
<SkillFormModal
|
<SkillFormModal
|
||||||
Visible="ShowCreateSkillModal"
|
Visible="ShowCreateSkillModal"
|
||||||
AutoFocusName="true"
|
AutoFocusName="true"
|
||||||
IsD6="IsD6"
|
RulesetId="@SelectedCampaignRulesetId"
|
||||||
Title="Create Skill"
|
Title="Create Skill"
|
||||||
SubmitLabel="Create Skill"
|
SubmitLabel="Create Skill"
|
||||||
NameInputId="skill-create-name"
|
NameInputId="skill-create-name"
|
||||||
@@ -200,6 +224,9 @@
|
|||||||
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"
|
||||||
InitialModel="CreateSkillInitialModel"
|
InitialModel="CreateSkillInitialModel"
|
||||||
FormVersion="CreateSkillFormVersion"
|
FormVersion="CreateSkillFormVersion"
|
||||||
SelectedCharacterId="SelectedCharacterId"
|
SelectedCharacterId="SelectedCharacterId"
|
||||||
@@ -211,7 +238,7 @@
|
|||||||
|
|
||||||
<SkillFormModal
|
<SkillFormModal
|
||||||
Visible="ShowEditSkillModal"
|
Visible="ShowEditSkillModal"
|
||||||
IsD6="IsD6"
|
RulesetId="@SelectedCampaignRulesetId"
|
||||||
Title="Edit Skill"
|
Title="Edit Skill"
|
||||||
SubmitLabel="Save Skill"
|
SubmitLabel="Save Skill"
|
||||||
NameInputId="skill-edit-name"
|
NameInputId="skill-edit-name"
|
||||||
@@ -219,6 +246,9 @@
|
|||||||
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"
|
||||||
InitialModel="EditSkillInitialModel"
|
InitialModel="EditSkillInitialModel"
|
||||||
FormVersion="EditSkillFormVersion"
|
FormVersion="EditSkillFormVersion"
|
||||||
SelectedCharacterId="SelectedCharacterId"
|
SelectedCharacterId="SelectedCharacterId"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Globalization;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using RpgRoller.Contracts;
|
using RpgRoller.Contracts;
|
||||||
|
|
||||||
@@ -16,13 +17,19 @@ public partial class CharacterPanel
|
|||||||
CreateSkillInitialModel = new()
|
CreateSkillInitialModel = new()
|
||||||
{
|
{
|
||||||
Name = string.Empty,
|
Name = string.Empty,
|
||||||
|
RulesetId = SelectedCampaignRulesetId,
|
||||||
DiceRollDefinition = selectedGroup?.DiceRollDefinition ?? string.Empty,
|
DiceRollDefinition = selectedGroup?.DiceRollDefinition ?? string.Empty,
|
||||||
SkillGroupId = selectedGroup?.Id.ToString() ?? string.Empty,
|
SkillGroupId = selectedGroup?.Id.ToString() ?? string.Empty,
|
||||||
WildDice = selectedGroup?.WildDice ?? (IsD6 ? 1 : 0),
|
WildDice = selectedGroup?.WildDice ?? (IsD6Ruleset ? 1 : 0),
|
||||||
AllowFumble = selectedGroup?.AllowFumble ?? IsD6,
|
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))
|
||||||
|
CreateSkillInitialModel.DiceRollDefinition = RulesetFormHelpers.BuildRolemasterExpression(CreateSkillInitialModel.RolemasterRollType, CreateSkillInitialModel.RolemasterModifier);
|
||||||
|
|
||||||
CreateSkillFormVersion++;
|
CreateSkillFormVersion++;
|
||||||
ShowCreateSkillModal = true;
|
ShowCreateSkillModal = true;
|
||||||
}
|
}
|
||||||
@@ -33,11 +40,14 @@ public partial class CharacterPanel
|
|||||||
EditSkillInitialModel = new()
|
EditSkillInitialModel = new()
|
||||||
{
|
{
|
||||||
Name = skill.Name,
|
Name = skill.Name,
|
||||||
|
RulesetId = SelectedCampaignRulesetId,
|
||||||
DiceRollDefinition = skill.DiceRollDefinition,
|
DiceRollDefinition = skill.DiceRollDefinition,
|
||||||
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++;
|
||||||
@@ -98,10 +108,15 @@ public partial class CharacterPanel
|
|||||||
private void OpenCreateSkillGroupModal()
|
private void OpenCreateSkillGroupModal()
|
||||||
{
|
{
|
||||||
SkillGroupState.Model.Name = string.Empty;
|
SkillGroupState.Model.Name = string.Empty;
|
||||||
|
SkillGroupState.Model.RulesetId = SelectedCampaignRulesetId;
|
||||||
SkillGroupState.Model.DiceRollDefinition = string.Empty;
|
SkillGroupState.Model.DiceRollDefinition = string.Empty;
|
||||||
SkillGroupState.Model.WildDice = IsD6 ? 1 : 0;
|
SkillGroupState.Model.WildDice = IsD6Ruleset ? 1 : 0;
|
||||||
SkillGroupState.Model.AllowFumble = IsD6;
|
SkillGroupState.Model.AllowFumble = IsD6Ruleset;
|
||||||
SkillGroupState.Model.FumbleRange = null;
|
SkillGroupState.Model.FumbleRange = null;
|
||||||
|
SkillGroupState.Model.RolemasterRollType = RulesetFormHelpers.RolemasterRollTypes.Initiative;
|
||||||
|
SkillGroupState.Model.RolemasterModifier = 0;
|
||||||
|
if (IsRolemasterRuleset)
|
||||||
|
SynchronizeSkillGroupExpression();
|
||||||
SkillGroupState.ResetValidation();
|
SkillGroupState.ResetValidation();
|
||||||
ShowCreateSkillGroupModal = true;
|
ShowCreateSkillGroupModal = true;
|
||||||
}
|
}
|
||||||
@@ -110,10 +125,14 @@ public partial class CharacterPanel
|
|||||||
{
|
{
|
||||||
EditingSkillGroupId = skillGroup.Id;
|
EditingSkillGroupId = skillGroup.Id;
|
||||||
SkillGroupState.Model.Name = skillGroup.Name;
|
SkillGroupState.Model.Name = skillGroup.Name;
|
||||||
|
SkillGroupState.Model.RulesetId = SelectedCampaignRulesetId;
|
||||||
SkillGroupState.Model.DiceRollDefinition = skillGroup.DiceRollDefinition;
|
SkillGroupState.Model.DiceRollDefinition = skillGroup.DiceRollDefinition;
|
||||||
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();
|
||||||
SkillGroupState.ResetValidation();
|
SkillGroupState.ResetValidation();
|
||||||
ShowEditSkillGroupModal = true;
|
ShowEditSkillGroupModal = true;
|
||||||
}
|
}
|
||||||
@@ -136,9 +155,25 @@ public partial class CharacterPanel
|
|||||||
if (string.IsNullOrWhiteSpace(SkillGroupState.Model.DiceRollDefinition))
|
if (string.IsNullOrWhiteSpace(SkillGroupState.Model.DiceRollDefinition))
|
||||||
SkillGroupState.Errors["diceRollDefinition"] = "Expression is required.";
|
SkillGroupState.Errors["diceRollDefinition"] = "Expression is required.";
|
||||||
|
|
||||||
if (IsD6 && SkillGroupState.Model.WildDice < 1)
|
if (IsD6Ruleset && SkillGroupState.Model.WildDice < 1)
|
||||||
SkillGroupState.Errors["wildDice"] = "D6 groups require at least one wild die.";
|
SkillGroupState.Errors["wildDice"] = "D6 groups require at least one wild die.";
|
||||||
|
|
||||||
|
if (IsRolemasterRuleset)
|
||||||
|
{
|
||||||
|
if (IsSkillGroupRolemasterOpenEnded && !SkillGroupState.Model.FumbleRange.HasValue)
|
||||||
|
SkillGroupState.Errors["fumbleRange"] = "Open-ended Rolemaster groups require a fumble range.";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SkillGroupState.Model.FumbleRange = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsD6Ruleset)
|
||||||
|
{
|
||||||
|
SkillGroupState.Model.WildDice = 0;
|
||||||
|
SkillGroupState.Model.AllowFumble = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!SelectedCharacterId.HasValue)
|
if (!SelectedCharacterId.HasValue)
|
||||||
SkillGroupState.Errors["character"] = "Select a character first.";
|
SkillGroupState.Errors["character"] = "Select a character first.";
|
||||||
|
|
||||||
@@ -184,9 +219,25 @@ public partial class CharacterPanel
|
|||||||
if (string.IsNullOrWhiteSpace(SkillGroupState.Model.DiceRollDefinition))
|
if (string.IsNullOrWhiteSpace(SkillGroupState.Model.DiceRollDefinition))
|
||||||
SkillGroupState.Errors["diceRollDefinition"] = "Expression is required.";
|
SkillGroupState.Errors["diceRollDefinition"] = "Expression is required.";
|
||||||
|
|
||||||
if (IsD6 && SkillGroupState.Model.WildDice < 1)
|
if (IsD6Ruleset && SkillGroupState.Model.WildDice < 1)
|
||||||
SkillGroupState.Errors["wildDice"] = "D6 groups require at least one wild die.";
|
SkillGroupState.Errors["wildDice"] = "D6 groups require at least one wild die.";
|
||||||
|
|
||||||
|
if (IsRolemasterRuleset)
|
||||||
|
{
|
||||||
|
if (IsSkillGroupRolemasterOpenEnded && !SkillGroupState.Model.FumbleRange.HasValue)
|
||||||
|
SkillGroupState.Errors["fumbleRange"] = "Open-ended Rolemaster groups require a fumble range.";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SkillGroupState.Model.FumbleRange = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsD6Ruleset)
|
||||||
|
{
|
||||||
|
SkillGroupState.Model.WildDice = 0;
|
||||||
|
SkillGroupState.Model.AllowFumble = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!EditingSkillGroupId.HasValue)
|
if (!EditingSkillGroupId.HasValue)
|
||||||
SkillGroupState.Errors["group"] = "Select a skill group first.";
|
SkillGroupState.Errors["group"] = "Select a skill group first.";
|
||||||
|
|
||||||
@@ -270,6 +321,66 @@ public partial class CharacterPanel
|
|||||||
return string.Concat(words[0][0], words[1][0]).ToUpperInvariant();
|
return string.Concat(words[0][0], words[1][0]).ToUpperInvariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnSkillGroupExpressionChanged(ChangeEventArgs args)
|
||||||
|
{
|
||||||
|
SkillGroupState.Model.DiceRollDefinition = args.Value?.ToString() ?? string.Empty;
|
||||||
|
if (IsRolemasterRuleset)
|
||||||
|
{
|
||||||
|
SkillGroupState.Model.RolemasterRollType = RulesetFormHelpers.InferRolemasterRollType(SkillGroupState.Model.DiceRollDefinition);
|
||||||
|
SkillGroupState.Model.RolemasterModifier = RulesetFormHelpers.InferRolemasterModifier(SkillGroupState.Model.DiceRollDefinition);
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
if (!IsRolemasterRuleset)
|
||||||
|
{
|
||||||
|
SkillGroupState.Model.FumbleRange = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsSkillGroupRolemasterOpenEnded)
|
||||||
|
{
|
||||||
|
SkillGroupState.Model.FumbleRange ??= 5;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SkillGroupState.Model.FumbleRange = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsD6Ruleset => RulesetFormHelpers.IsD6(SelectedCampaignRulesetId);
|
||||||
|
private bool IsRolemasterRuleset => RulesetFormHelpers.IsRolemaster(SelectedCampaignRulesetId);
|
||||||
|
private bool IsSkillGroupRolemasterOpenEnded => string.Equals(SkillGroupState.Model.RolemasterRollType, RulesetFormHelpers.RolemasterRollTypes.OpenEndedPercentile, StringComparison.OrdinalIgnoreCase);
|
||||||
|
private string SkillGroupExpressionHelpText => IsRolemasterRuleset
|
||||||
|
? $"{RulesetFormHelpers.RolemasterExampleText(SkillGroupState.Model.RolemasterRollType)}. Negative modifiers are allowed."
|
||||||
|
: "Enter the default expression for skills created in this group.";
|
||||||
|
|
||||||
private bool ShowCreateSkillModal { get; set; }
|
private bool ShowCreateSkillModal { get; set; }
|
||||||
private bool ShowEditSkillModal { get; set; }
|
private bool ShowEditSkillModal { get; set; }
|
||||||
private bool ShowCreateSkillGroupModal { get; set; }
|
private bool ShowCreateSkillGroupModal { get; set; }
|
||||||
@@ -309,7 +420,7 @@ public partial class CharacterPanel
|
|||||||
public IReadOnlyList<CharacterSheetSkillGroup> SelectedCharacterSkillGroups { get; set; } = [];
|
public IReadOnlyList<CharacterSheetSkillGroup> SelectedCharacterSkillGroups { get; set; } = [];
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool IsD6 { get; set; }
|
public string SelectedCampaignRulesetId { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string RollVisibility { get; set; } = "public";
|
public string RollVisibility { get; set; } = "public";
|
||||||
|
|||||||
103
RpgRoller/Components/Pages/HomeControls/RulesetFormHelpers.cs
Normal file
103
RpgRoller/Components/Pages/HomeControls/RulesetFormHelpers.cs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using System.Globalization;
|
||||||
|
using RpgRoller.Domain;
|
||||||
|
using RpgRoller.Services;
|
||||||
|
|
||||||
|
namespace RpgRoller.Components.Pages.HomeControls;
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
internal static class RulesetFormHelpers
|
||||||
|
{
|
||||||
|
internal static class RulesetIds
|
||||||
|
{
|
||||||
|
public const string D6 = "d6";
|
||||||
|
public const string Dnd5e = "dnd5e";
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
return string.Equals(rulesetId, RulesetIds.D6, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsRolemaster(string? rulesetId)
|
||||||
|
{
|
||||||
|
return string.Equals(rulesetId, RulesetIds.Rolemaster, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string InferRolemasterRollType(string? expression)
|
||||||
|
{
|
||||||
|
var parseResult = TryParseRolemasterExpression(expression);
|
||||||
|
if (!parseResult.Succeeded || parseResult.Value is null)
|
||||||
|
return RolemasterRollTypes.Initiative;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var parseResult = TryParseRolemasterExpression(expression);
|
||||||
|
if (!parseResult.Succeeded || parseResult.Value is null)
|
||||||
|
return expression;
|
||||||
|
|
||||||
|
return parseResult.Value.Kind switch
|
||||||
|
{
|
||||||
|
DiceExpressionKind.RolemasterPercentile => $"Percentile: {parseResult.Value.Canonical}",
|
||||||
|
DiceExpressionKind.RolemasterOpenEndedPercentile => fumbleRange.HasValue
|
||||||
|
? $"Open-ended percentile: {parseResult.Value.Canonical}, fumble <= {fumbleRange.Value}"
|
||||||
|
: $"Open-ended percentile: {parseResult.Value.Canonical}",
|
||||||
|
_ => $"Initiative: {parseResult.Value.Canonical}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string RolemasterExampleText(string? rollType)
|
||||||
|
{
|
||||||
|
return rollType switch
|
||||||
|
{
|
||||||
|
RolemasterRollTypes.Percentile => "Example: d100+48",
|
||||||
|
RolemasterRollTypes.OpenEndedPercentile => "Example: d100!+85",
|
||||||
|
_ => "Example: 2d10+48"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ServiceResult<DiceExpression> TryParseRolemasterExpression(string? expression)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(expression))
|
||||||
|
return ServiceResult<DiceExpression>.Failure("invalid_expression", "Expression is required.");
|
||||||
|
|
||||||
|
return DiceRules.ParseExpression(RulesetKind.Rolemaster, expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatModifier(int modifier)
|
||||||
|
{
|
||||||
|
return modifier >= 0 ? $"+{modifier}" : modifier.ToString(CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
<p class="field-error">@skillNameError</p>
|
<p class="field-error">@skillNameError</p>
|
||||||
}
|
}
|
||||||
<label for="@ExpressionInputId">Expression</label>
|
<label for="@ExpressionInputId">Expression</label>
|
||||||
<input id="@ExpressionInputId" @bind="FormState.Model.DiceRollDefinition" @bind:event="oninput"/>
|
<input id="@ExpressionInputId" value="@FormState.Model.DiceRollDefinition" @oninput="OnExpressionChanged"/>
|
||||||
|
<p class="field-help">@ExpressionHelpText</p>
|
||||||
@if (FormState.Errors.TryGetValue("diceRollDefinition", out var expressionError))
|
@if (FormState.Errors.TryGetValue("diceRollDefinition", out var expressionError))
|
||||||
{
|
{
|
||||||
<p class="field-error">@expressionError</p>
|
<p class="field-error">@expressionError</p>
|
||||||
@@ -32,7 +33,7 @@
|
|||||||
{
|
{
|
||||||
<p class="field-error">@skillGroupError</p>
|
<p class="field-error">@skillGroupError</p>
|
||||||
}
|
}
|
||||||
@if (IsD6)
|
@if (IsD6Ruleset)
|
||||||
{
|
{
|
||||||
<label for="@WildDiceInputId">Wild dice</label>
|
<label for="@WildDiceInputId">Wild dice</label>
|
||||||
<input id="@WildDiceInputId" type="number" min="1" step="1" @bind="FormState.Model.WildDice"/>
|
<input id="@WildDiceInputId" type="number" min="1" step="1" @bind="FormState.Model.WildDice"/>
|
||||||
@@ -44,6 +45,29 @@
|
|||||||
<label for="@AllowFumbleInputId">Allow fumble</label>
|
<label for="@AllowFumbleInputId">Allow fumble</label>
|
||||||
<input id="@AllowFumbleInputId" type="checkbox" @bind="FormState.Model.AllowFumble"/>
|
<input id="@AllowFumbleInputId" type="checkbox" @bind="FormState.Model.AllowFumble"/>
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
<label for="@FumbleRangeInputId">Fumble range</label>
|
||||||
|
<input id="@FumbleRangeInputId" type="number" min="0" max="95" step="1" @bind="FormState.Model.FumbleRange"/>
|
||||||
|
<p class="field-help">Used only for low-end open-ended rolls. Allowed range: 0 to 95.</p>
|
||||||
|
@if (FormState.Errors.TryGetValue("fumbleRange", out var fumbleRangeError))
|
||||||
|
{
|
||||||
|
<p class="field-error">@fumbleRangeError</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
<div class="inline-actions">
|
<div class="inline-actions">
|
||||||
<button type="submit" disabled="@(IsMutating || IsSubmitting)">@SubmitLabel</button>
|
<button type="submit" disabled="@(IsMutating || IsSubmitting)">@SubmitLabel</button>
|
||||||
<button type="button" class="ghost" @onclick="CancelRequested">Cancel</button>
|
<button type="button" class="ghost" @onclick="CancelRequested">Cancel</button>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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;
|
||||||
@@ -14,11 +15,15 @@ public partial class SkillFormModal
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
FormState.Model.Name = InitialModel.Name;
|
FormState.Model.Name = InitialModel.Name;
|
||||||
|
FormState.Model.RulesetId = RulesetId;
|
||||||
FormState.Model.DiceRollDefinition = InitialModel.DiceRollDefinition;
|
FormState.Model.DiceRollDefinition = InitialModel.DiceRollDefinition;
|
||||||
FormState.Model.SkillGroupId = InitialModel.SkillGroupId;
|
FormState.Model.SkillGroupId = InitialModel.SkillGroupId;
|
||||||
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();
|
||||||
FormState.ResetValidation();
|
FormState.ResetValidation();
|
||||||
AppliedFormVersion = FormVersion;
|
AppliedFormVersion = FormVersion;
|
||||||
PendingNameFocus = AutoFocusName;
|
PendingNameFocus = AutoFocusName;
|
||||||
@@ -43,9 +48,25 @@ public partial class SkillFormModal
|
|||||||
if (string.IsNullOrWhiteSpace(FormState.Model.DiceRollDefinition))
|
if (string.IsNullOrWhiteSpace(FormState.Model.DiceRollDefinition))
|
||||||
FormState.Errors["diceRollDefinition"] = "Expression is required.";
|
FormState.Errors["diceRollDefinition"] = "Expression is required.";
|
||||||
|
|
||||||
if (IsD6 && FormState.Model.WildDice < 1)
|
if (IsD6Ruleset && FormState.Model.WildDice < 1)
|
||||||
FormState.Errors["wildDice"] = "D6 skills require at least one wild die.";
|
FormState.Errors["wildDice"] = "D6 skills require at least one wild die.";
|
||||||
|
|
||||||
|
if (IsRolemasterRuleset)
|
||||||
|
{
|
||||||
|
if (IsRolemasterOpenEndedSelected && !FormState.Model.FumbleRange.HasValue)
|
||||||
|
FormState.Errors["fumbleRange"] = "Open-ended Rolemaster skills require a fumble range.";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FormState.Model.FumbleRange = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsD6Ruleset)
|
||||||
|
{
|
||||||
|
FormState.Model.WildDice = 0;
|
||||||
|
FormState.Model.AllowFumble = false;
|
||||||
|
}
|
||||||
|
|
||||||
Guid? skillGroupId = null;
|
Guid? skillGroupId = null;
|
||||||
if (!string.IsNullOrWhiteSpace(FormState.Model.SkillGroupId))
|
if (!string.IsNullOrWhiteSpace(FormState.Model.SkillGroupId))
|
||||||
{
|
{
|
||||||
@@ -92,6 +113,77 @@ public partial class SkillFormModal
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnExpressionChanged(ChangeEventArgs args)
|
||||||
|
{
|
||||||
|
FormState.Model.DiceRollDefinition = args.Value?.ToString() ?? string.Empty;
|
||||||
|
if (IsRolemasterRuleset)
|
||||||
|
SynchronizeRolemasterInputsFromExpression();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRolemasterRollTypeChanged(ChangeEventArgs args)
|
||||||
|
{
|
||||||
|
FormState.Model.RolemasterRollType = args.Value?.ToString() ?? RulesetFormHelpers.RolemasterRollTypes.Initiative;
|
||||||
|
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 IsRolemasterRuleset => RulesetFormHelpers.IsRolemaster(RulesetId);
|
||||||
|
private bool IsRolemasterOpenEndedSelected => string.Equals(FormState.Model.RolemasterRollType, RulesetFormHelpers.RolemasterRollTypes.OpenEndedPercentile, StringComparison.OrdinalIgnoreCase);
|
||||||
|
private string ExpressionHelpText => IsRolemasterRuleset
|
||||||
|
? $"{RulesetFormHelpers.RolemasterExampleText(FormState.Model.RolemasterRollType)}. Negative modifiers are allowed."
|
||||||
|
: "Enter the dice expression used for this skill.";
|
||||||
|
|
||||||
|
private void SynchronizeRulesetSpecificFields()
|
||||||
|
{
|
||||||
|
if (!IsRolemasterRuleset)
|
||||||
|
return;
|
||||||
|
|
||||||
|
SynchronizeRolemasterInputsFromExpression();
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
if (!IsRolemasterRuleset)
|
||||||
|
{
|
||||||
|
FormState.Model.FumbleRange = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsRolemasterOpenEndedSelected)
|
||||||
|
{
|
||||||
|
FormState.Model.FumbleRange ??= 5;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FormState.Model.FumbleRange = null;
|
||||||
|
}
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private RpgRollerApiClient ApiClient { get; set; } = null!;
|
private RpgRollerApiClient ApiClient { get; set; } = null!;
|
||||||
|
|
||||||
@@ -105,7 +197,7 @@ public partial class SkillFormModal
|
|||||||
public bool Visible { get; set; }
|
public bool Visible { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool IsD6 { get; set; }
|
public string RulesetId { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string Title { get; set; } = "Skill";
|
public string Title { get; set; } = "Skill";
|
||||||
@@ -128,6 +220,15 @@ 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]
|
||||||
|
public string FumbleRangeInputId { get; set; } = "skill-fumble-range";
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public SkillFormModel InitialModel { get; set; } = new();
|
public SkillFormModel InitialModel { get; set; } = new();
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
IsMutating="IsMutating"
|
IsMutating="IsMutating"
|
||||||
SelectedCharacterSkills="PlaySelectedCharacterSkills"
|
SelectedCharacterSkills="PlaySelectedCharacterSkills"
|
||||||
SelectedCharacterSkillGroups="PlaySelectedCharacterSkillGroups"
|
SelectedCharacterSkillGroups="PlaySelectedCharacterSkillGroups"
|
||||||
IsD6="IsSelectedCampaignD6"
|
SelectedCampaignRulesetId="@(PlaySelectedCampaign?.RulesetId ?? string.Empty)"
|
||||||
RollVisibility="RollVisibility"
|
RollVisibility="RollVisibility"
|
||||||
RollVisibilityChanged="OnRollVisibilityChanged"
|
RollVisibilityChanged="OnRollVisibilityChanged"
|
||||||
OwnerLabel="OwnerLabel"
|
OwnerLabel="OwnerLabel"
|
||||||
|
|||||||
@@ -920,7 +920,12 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private string SkillDefinitionLabel(CharacterSheetSkill skill)
|
private string SkillDefinitionLabel(CharacterSheetSkill skill)
|
||||||
{
|
{
|
||||||
if (!string.Equals(SelectedCampaign?.RulesetId, "d6", StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(SelectedCampaign?.RulesetId, "d6", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (string.Equals(SelectedCampaign?.RulesetId, RulesetFormHelpers.RulesetIds.Rolemaster, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return RulesetFormHelpers.DescribeRolemasterExpression(skill.DiceRollDefinition, skill.FumbleRange);
|
||||||
|
|
||||||
return skill.DiceRollDefinition;
|
return skill.DiceRollDefinition;
|
||||||
|
}
|
||||||
|
|
||||||
var fumbleLabel = skill.AllowFumble ? "fumble on" : "fumble off";
|
var fumbleLabel = skill.AllowFumble ? "fumble on" : "fumble off";
|
||||||
return $"{skill.DiceRollDefinition}, wild {skill.WildDice}, {fumbleLabel}";
|
return $"{skill.DiceRollDefinition}, wild {skill.WildDice}, {fumbleLabel}";
|
||||||
|
|||||||
@@ -222,6 +222,12 @@ select:focus-visible {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field-help {
|
||||||
|
margin: -0.1rem 0 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
.status-message {
|
.status-message {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,22 @@ async function postJson(request, url, data) {
|
|||||||
return await response.json();
|
return await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function registerAndLogin(request, username, displayName) {
|
||||||
|
await postJson(request, "/api/auth/register", {
|
||||||
|
username,
|
||||||
|
password: "Password123",
|
||||||
|
displayName
|
||||||
|
});
|
||||||
|
|
||||||
|
const loginResponse = await request.post("/api/auth/login", {
|
||||||
|
data: {
|
||||||
|
username,
|
||||||
|
password: "Password123"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(loginResponse.ok()).toBeTruthy();
|
||||||
|
}
|
||||||
|
|
||||||
test("home page loads auth entry points", async ({ page }) => {
|
test("home page loads auth entry points", async ({ page }) => {
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
|
|
||||||
@@ -20,19 +36,7 @@ test("Rolemaster open-ended roll detail renders specialized dice chips", async (
|
|||||||
const username = `rm-${Date.now()}`;
|
const username = `rm-${Date.now()}`;
|
||||||
const displayName = "Rolemaster Smoke";
|
const displayName = "Rolemaster Smoke";
|
||||||
|
|
||||||
await postJson(context.request, "/api/auth/register", {
|
await registerAndLogin(context.request, username, displayName);
|
||||||
username,
|
|
||||||
password: "Password123",
|
|
||||||
displayName
|
|
||||||
});
|
|
||||||
|
|
||||||
const loginResponse = await context.request.post("/api/auth/login", {
|
|
||||||
data: {
|
|
||||||
username,
|
|
||||||
password: "Password123"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
expect(loginResponse.ok()).toBeTruthy();
|
|
||||||
|
|
||||||
const campaign = await postJson(context.request, "/api/campaigns", {
|
const campaign = await postJson(context.request, "/api/campaigns", {
|
||||||
name: "Rolemaster Smoke",
|
name: "Rolemaster Smoke",
|
||||||
@@ -63,3 +67,67 @@ test("Rolemaster open-ended roll detail renders specialized dice chips", async (
|
|||||||
await expect(rolemasterFollowUpDice.first()).toBeVisible();
|
await expect(rolemasterFollowUpDice.first()).toBeVisible();
|
||||||
await expect(page.locator(".log-detail .roll-dice-strip")).toBeVisible();
|
await expect(page.locator(".log-detail .roll-dice-strip")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Rolemaster UI exposes conditional create and edit fields", async ({ page, context }) => {
|
||||||
|
const username = `rm-ui-${Date.now()}`;
|
||||||
|
await registerAndLogin(context.request, username, "Rolemaster UI");
|
||||||
|
|
||||||
|
const campaign = await postJson(context.request, "/api/campaigns", {
|
||||||
|
name: "Rolemaster UI Campaign",
|
||||||
|
rulesetId: "rolemaster"
|
||||||
|
});
|
||||||
|
const character = await postJson(context.request, "/api/characters", {
|
||||||
|
name: "UI Character",
|
||||||
|
campaignId: campaign.id
|
||||||
|
});
|
||||||
|
await postJson(context.request, `/api/characters/${character.id}/skill-groups`, {
|
||||||
|
name: "Awareness",
|
||||||
|
diceRollDefinition: "d100!+15",
|
||||||
|
wildDice: 0,
|
||||||
|
allowFumble: false,
|
||||||
|
fumbleRange: 5
|
||||||
|
});
|
||||||
|
await postJson(context.request, `/api/characters/${character.id}/skills`, {
|
||||||
|
name: "Perception",
|
||||||
|
diceRollDefinition: "d100!+25",
|
||||||
|
wildDice: 0,
|
||||||
|
allowFumble: false,
|
||||||
|
fumbleRange: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.goto("/");
|
||||||
|
await expect(page.locator("#workspace-screen-menu-button")).toBeVisible();
|
||||||
|
|
||||||
|
await page.locator("#workspace-screen-menu-button").click();
|
||||||
|
await page.getByRole("menuitem", { name: "Campaign Management" }).click();
|
||||||
|
await page.getByRole("button", { name: "Add campaign" }).click();
|
||||||
|
await expect(page.locator("#campaign-ruleset option[value='rolemaster']")).toHaveText("Rolemaster");
|
||||||
|
await page.getByRole("button", { name: "Cancel" }).click();
|
||||||
|
|
||||||
|
await page.locator("#workspace-screen-menu-button").click();
|
||||||
|
await page.getByRole("menuitem", { name: "Play" }).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 page.locator("#skill-group-rolemaster-roll-type").selectOption("open-ended-percentile");
|
||||||
|
await expect(page.locator("#skill-group-fumble-range")).toBeVisible();
|
||||||
|
await page.locator("#skill-group-fumble-range").fill("");
|
||||||
|
await page.getByRole("button", { name: "Create Group" }).click();
|
||||||
|
await expect(page.getByText("Open-ended Rolemaster groups require a fumble range.")).toBeVisible();
|
||||||
|
await page.getByRole("button", { name: "Cancel" }).click();
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "Add skill" }).first().click();
|
||||||
|
await expect(page.locator("#skill-create-rolemaster-roll-type")).toBeVisible();
|
||||||
|
await page.locator("#skill-create-rolemaster-roll-type").selectOption("percentile");
|
||||||
|
await expect(page.locator("#skill-create-fumble-range")).toHaveCount(0);
|
||||||
|
await page.locator("#skill-create-rolemaster-roll-type").selectOption("open-ended-percentile");
|
||||||
|
await expect(page.locator("#skill-create-fumble-range")).toBeVisible();
|
||||||
|
await page.getByRole("button", { name: "Cancel" }).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-rolemaster-roll-type")).toHaveValue("open-ended-percentile");
|
||||||
|
await expect(page.locator("#skill-edit-fumble-range")).toHaveValue("5");
|
||||||
|
await page.getByRole("button", { name: "Cancel" }).click();
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user