Implement d6 wild dice/fumble skills and die-state rolls

This commit is contained in:
2026-02-26 08:26:12 +01:00
parent 0f44cc466b
commit 11ab7c959b
22 changed files with 560 additions and 50 deletions

View File

@@ -79,6 +79,7 @@ public partial class Home
private SkillSummary? SelectedSkill => SelectedCampaign?.Skills.FirstOrDefault(s => s.Id == SelectedSkillId);
private string? ActiveCharacterName => SelectedCampaign?.Characters.FirstOrDefault(c => c.Id == ActiveCharacterId)?.Name;
private bool IsCurrentUserGm => SelectedCampaign is not null && User is not null && SelectedCampaign.Gm.Id == User.Id;
private bool IsSelectedCampaignD6 => string.Equals(SelectedCampaign?.RulesetId, "d6", StringComparison.OrdinalIgnoreCase);
private List<SkillSummary> SelectedCharacterSkills =>
SelectedCampaign is null || !SelectedCharacterId.HasValue
@@ -640,6 +641,8 @@ public partial class Home
{
SkillForm.Name = string.Empty;
SkillForm.DiceRollDefinition = string.Empty;
SkillForm.WildDice = IsSelectedCampaignD6 ? 1 : 0;
SkillForm.AllowFumble = IsSelectedCampaignD6;
SkillErrors.Clear();
SkillFormError = null;
ShowCreateSkillModal = true;
@@ -655,6 +658,8 @@ public partial class Home
EditingSkillId = SelectedSkill.Id;
EditSkillForm.Name = SelectedSkill.Name;
EditSkillForm.DiceRollDefinition = SelectedSkill.DiceRollDefinition;
EditSkillForm.WildDice = SelectedSkill.WildDice;
EditSkillForm.AllowFumble = SelectedSkill.AllowFumble;
EditSkillErrors.Clear();
EditSkillFormError = null;
ShowEditSkillModal = true;
@@ -688,6 +693,11 @@ public partial class Home
SkillErrors["diceRollDefinition"] = "Expression is required.";
}
if (IsSelectedCampaignD6 && SkillForm.WildDice < 1)
{
SkillErrors["wildDice"] = "D6 skills require at least one wild die.";
}
if (SkillErrors.Count > 0)
{
SkillFormError = "Resolve validation issues before submitting.";
@@ -697,7 +707,10 @@ public partial class Home
IsMutating = true;
try
{
_ = await RequestAsync<SkillSummary>("POST", $"/api/characters/{SelectedCharacter.Id}/skills", new CreateSkillRequest(SkillForm.Name.Trim(), SkillForm.DiceRollDefinition.Trim()));
_ = await RequestAsync<SkillSummary>(
"POST",
$"/api/characters/{SelectedCharacter.Id}/skills",
new CreateSkillRequest(SkillForm.Name.Trim(), SkillForm.DiceRollDefinition.Trim(), SkillForm.WildDice, SkillForm.AllowFumble));
CloseSkillModals();
await RefreshCampaignScopeAsync();
SetStatus("Skill created.", false);
@@ -733,6 +746,11 @@ public partial class Home
EditSkillErrors["diceRollDefinition"] = "Expression is required.";
}
if (IsSelectedCampaignD6 && EditSkillForm.WildDice < 1)
{
EditSkillErrors["wildDice"] = "D6 skills require at least one wild die.";
}
if (EditSkillErrors.Count > 0)
{
EditSkillFormError = "Resolve validation issues before submitting.";
@@ -742,7 +760,10 @@ public partial class Home
IsMutating = true;
try
{
var updatedSkill = await RequestAsync<SkillSummary>("PUT", $"/api/skills/{EditingSkillId.Value}", new UpdateSkillRequest(EditSkillForm.Name.Trim(), EditSkillForm.DiceRollDefinition.Trim()));
var updatedSkill = await RequestAsync<SkillSummary>(
"PUT",
$"/api/skills/{EditingSkillId.Value}",
new UpdateSkillRequest(EditSkillForm.Name.Trim(), EditSkillForm.DiceRollDefinition.Trim(), EditSkillForm.WildDice, EditSkillForm.AllowFumble));
SelectedSkillId = updatedSkill.Id;
CloseSkillModals();
await RefreshCampaignScopeAsync();
@@ -985,6 +1006,93 @@ public partial class Home
return SelectedCampaign?.Skills.FirstOrDefault(s => s.Id == skillId)?.Name ?? "Hidden skill";
}
private string SkillDefinitionLabel(SkillSummary skill)
{
if (!string.Equals(SelectedCampaign?.RulesetId, "d6", StringComparison.OrdinalIgnoreCase))
{
return skill.DiceRollDefinition;
}
var fumble = skill.AllowFumble ? "fumble on" : "fumble off";
return $"{skill.DiceRollDefinition} | wild {skill.WildDice}, {fumble}";
}
private static string RollDieGlyph(int roll)
{
return roll switch
{
1 => "\u2680",
2 => "\u2681",
3 => "\u2682",
4 => "\u2683",
5 => "\u2684",
6 => "\u2685",
_ => roll.ToString()
};
}
private static string RollDieCssClass(RollDieResult die)
{
var classes = new List<string> { "die-chip" };
if (die.Wild)
{
classes.Add("wild");
}
if (die.Crit)
{
classes.Add("crit");
}
if (die.Fumble)
{
classes.Add("fumble");
}
if (die.Removed)
{
classes.Add("removed");
}
if (die.Added)
{
classes.Add("added");
}
return string.Join(" ", classes);
}
private static string RollDieTitle(RollDieResult die)
{
var labels = new List<string> { $"Roll {die.Roll}" };
if (die.Wild)
{
labels.Add("wild");
}
if (die.Crit)
{
labels.Add("critical");
}
if (die.Fumble)
{
labels.Add("fumble");
}
if (die.Removed)
{
labels.Add("removed");
}
if (die.Added)
{
labels.Add("added");
}
return string.Join(", ", labels);
}
private string RollerLabel(CampaignLogEntry entry)
{
if (User is not null && entry.RollerUserId == User.Id)
@@ -1134,6 +1242,8 @@ public partial class Home
{
public string Name { get; set; } = string.Empty;
public string DiceRollDefinition { get; set; } = string.Empty;
public int WildDice { get; set; }
public bool AllowFumble { get; set; }
}
private sealed class JsApiResponse