Add rolemaster auto retry skill toggle
This commit is contained in:
@@ -9,13 +9,13 @@ internal static class SkillEndpoints
|
||||
{
|
||||
group.MapPost("/characters/{characterId:guid}/skills", (Guid characterId, CreateSkillRequest request, HttpContext context, IGameService game) =>
|
||||
{
|
||||
var result = game.CreateSkill(context.GetRequiredSessionToken(), characterId, request.Name, request.DiceRollDefinition, request.WildDice, request.AllowFumble, request.SkillGroupId, request.FumbleRange);
|
||||
var result = game.CreateSkill(context.GetRequiredSessionToken(), characterId, request.Name, request.DiceRollDefinition, request.WildDice, request.AllowFumble, request.SkillGroupId, request.FumbleRange, request.RolemasterAutoRetry);
|
||||
return ApiResultMapper.ToApiResult(result);
|
||||
});
|
||||
|
||||
group.MapPut("/skills/{skillId:guid}", (Guid skillId, UpdateSkillRequest request, HttpContext context, IGameService game) =>
|
||||
{
|
||||
var result = game.UpdateSkill(context.GetRequiredSessionToken(), skillId, request.Name, request.DiceRollDefinition, request.WildDice, request.AllowFumble, request.SkillGroupId, request.FumbleRange);
|
||||
var result = game.UpdateSkill(context.GetRequiredSessionToken(), skillId, request.Name, request.DiceRollDefinition, request.WildDice, request.AllowFumble, request.SkillGroupId, request.FumbleRange, request.RolemasterAutoRetry);
|
||||
return ApiResultMapper.ToApiResult(result);
|
||||
});
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ public sealed class SkillFormModel
|
||||
public int WildDice { get; set; }
|
||||
public bool AllowFumble { get; set; }
|
||||
public int? FumbleRange { get; set; }
|
||||
public bool RolemasterAutoRetry { get; set; }
|
||||
}
|
||||
|
||||
public sealed class SkillGroupFormModel
|
||||
|
||||
@@ -19,7 +19,8 @@ public partial class CharacterPanel
|
||||
SkillGroupId = selectedGroup?.Id.ToString() ?? string.Empty,
|
||||
WildDice = selectedGroup?.WildDice ?? (IsD6Ruleset ? 1 : 0),
|
||||
AllowFumble = selectedGroup?.AllowFumble ?? IsD6Ruleset,
|
||||
FumbleRange = selectedGroup?.FumbleRange
|
||||
FumbleRange = selectedGroup?.FumbleRange,
|
||||
RolemasterAutoRetry = false
|
||||
};
|
||||
|
||||
if (IsRolemasterRuleset && string.IsNullOrWhiteSpace(CreateSkillInitialModel.DiceRollDefinition))
|
||||
@@ -40,7 +41,8 @@ public partial class CharacterPanel
|
||||
SkillGroupId = skill.SkillGroupId?.ToString() ?? string.Empty,
|
||||
WildDice = skill.WildDice,
|
||||
AllowFumble = skill.AllowFumble,
|
||||
FumbleRange = skill.FumbleRange
|
||||
FumbleRange = skill.FumbleRange,
|
||||
RolemasterAutoRetry = skill.RolemasterAutoRetry
|
||||
};
|
||||
|
||||
EditSkillFormVersion++;
|
||||
|
||||
@@ -30,7 +30,7 @@ internal static class RulesetFormHelpers
|
||||
return parseResult.Succeeded && parseResult.Value is not null && parseResult.Value.Kind == DiceExpressionKind.RolemasterOpenEndedPercentile;
|
||||
}
|
||||
|
||||
public static string DescribeRolemasterExpression(string expression, int? fumbleRange)
|
||||
public static string DescribeRolemasterExpression(string expression, int? fumbleRange, bool rolemasterAutoRetry = false)
|
||||
{
|
||||
var parseResult = TryParseRolemasterExpression(expression);
|
||||
if (!parseResult.Succeeded || parseResult.Value is null)
|
||||
@@ -38,7 +38,7 @@ internal static class RulesetFormHelpers
|
||||
|
||||
return parseResult.Value.Kind switch
|
||||
{
|
||||
DiceExpressionKind.RolemasterOpenEndedPercentile => fumbleRange.HasValue ? $"Open-ended percentile: {parseResult.Value.Canonical}, fumble <= {fumbleRange.Value}" : $"Open-ended percentile: {parseResult.Value.Canonical}",
|
||||
DiceExpressionKind.RolemasterOpenEndedPercentile => DescribeOpenEndedExpression(parseResult.Value.Canonical, fumbleRange, rolemasterAutoRetry),
|
||||
_ => $"Rolemaster: {parseResult.Value.Canonical}"
|
||||
};
|
||||
}
|
||||
@@ -55,4 +55,17 @@ internal static class RulesetFormHelpers
|
||||
|
||||
return DiceRules.ParseExpression(RulesetKind.Rolemaster, expression);
|
||||
}
|
||||
|
||||
private static string DescribeOpenEndedExpression(string canonicalExpression, int? fumbleRange, bool rolemasterAutoRetry)
|
||||
{
|
||||
var parts = new List<string> { $"Open-ended percentile: {canonicalExpression}" };
|
||||
|
||||
if (fumbleRange.HasValue)
|
||||
parts.Add($"fumble <= {fumbleRange.Value}");
|
||||
|
||||
if (rolemasterAutoRetry)
|
||||
parts.Add("auto retry");
|
||||
|
||||
return string.Join(", ", parts);
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,10 @@
|
||||
{
|
||||
<p class="field-error">@fumbleRangeError</p>
|
||||
}
|
||||
|
||||
<label for="skill-auto-retry">Automatic retry</label>
|
||||
<input id="skill-auto-retry" type="checkbox" @bind="FormState.Model.RolemasterAutoRetry"/>
|
||||
<p class="field-help">When later enabled in rolling, retry bands are 77-90 and 91-110.</p>
|
||||
}
|
||||
}
|
||||
<div class="inline-actions">
|
||||
|
||||
@@ -19,6 +19,7 @@ public partial class SkillFormModal
|
||||
FormState.Model.WildDice = InitialModel.WildDice;
|
||||
FormState.Model.AllowFumble = InitialModel.AllowFumble;
|
||||
FormState.Model.FumbleRange = InitialModel.FumbleRange;
|
||||
FormState.Model.RolemasterAutoRetry = InitialModel.RolemasterAutoRetry;
|
||||
SynchronizeRulesetSpecificFields();
|
||||
FormState.ResetValidation();
|
||||
AppliedFormVersion = FormVersion;
|
||||
@@ -53,7 +54,10 @@ public partial class SkillFormModal
|
||||
FormState.Errors["fumbleRange"] = "Open-ended Rolemaster skills require a fumble range.";
|
||||
}
|
||||
else
|
||||
{
|
||||
FormState.Model.FumbleRange = null;
|
||||
FormState.Model.RolemasterAutoRetry = false;
|
||||
}
|
||||
|
||||
if (!IsD6Ruleset)
|
||||
{
|
||||
@@ -81,7 +85,7 @@ public partial class SkillFormModal
|
||||
{
|
||||
SkillSummary skill;
|
||||
if (EditingSkillId.HasValue)
|
||||
skill = await ApiClient.RequestAsync<SkillSummary>("PUT", $"/api/skills/{EditingSkillId.Value}", new UpdateSkillRequest(FormState.Model.Name.Trim(), FormState.Model.DiceRollDefinition.Trim(), FormState.Model.WildDice, FormState.Model.AllowFumble, skillGroupId, FormState.Model.FumbleRange));
|
||||
skill = await ApiClient.RequestAsync<SkillSummary>("PUT", $"/api/skills/{EditingSkillId.Value}", new UpdateSkillRequest(FormState.Model.Name.Trim(), FormState.Model.DiceRollDefinition.Trim(), FormState.Model.WildDice, FormState.Model.AllowFumble, skillGroupId, FormState.Model.FumbleRange, FormState.Model.RolemasterAutoRetry));
|
||||
else
|
||||
{
|
||||
if (!SelectedCharacterId.HasValue)
|
||||
@@ -90,7 +94,7 @@ public partial class SkillFormModal
|
||||
return;
|
||||
}
|
||||
|
||||
skill = await ApiClient.RequestAsync<SkillSummary>("POST", $"/api/characters/{SelectedCharacterId.Value}/skills", new CreateSkillRequest(FormState.Model.Name.Trim(), FormState.Model.DiceRollDefinition.Trim(), FormState.Model.WildDice, FormState.Model.AllowFumble, skillGroupId, FormState.Model.FumbleRange));
|
||||
skill = await ApiClient.RequestAsync<SkillSummary>("POST", $"/api/characters/{SelectedCharacterId.Value}/skills", new CreateSkillRequest(FormState.Model.Name.Trim(), FormState.Model.DiceRollDefinition.Trim(), FormState.Model.WildDice, FormState.Model.AllowFumble, skillGroupId, FormState.Model.FumbleRange, FormState.Model.RolemasterAutoRetry));
|
||||
}
|
||||
|
||||
await SkillSaved.InvokeAsync(skill.Id);
|
||||
@@ -115,7 +119,10 @@ public partial class SkillFormModal
|
||||
private void SynchronizeRulesetSpecificFields()
|
||||
{
|
||||
if (!IsRolemasterRuleset)
|
||||
{
|
||||
FormState.Model.RolemasterAutoRetry = false;
|
||||
return;
|
||||
}
|
||||
|
||||
NormalizeRolemasterFumbleRange();
|
||||
}
|
||||
@@ -135,6 +142,7 @@ public partial class SkillFormModal
|
||||
}
|
||||
|
||||
FormState.Model.FumbleRange = null;
|
||||
FormState.Model.RolemasterAutoRetry = false;
|
||||
}
|
||||
|
||||
private bool IsD6Ruleset => RulesetFormHelpers.IsD6(RulesetId);
|
||||
|
||||
@@ -27,7 +27,7 @@ public sealed class WorkspaceState
|
||||
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 RulesetFormHelpers.DescribeRolemasterExpression(skill.DiceRollDefinition, skill.FumbleRange, skill.RolemasterAutoRetry);
|
||||
|
||||
return skill.DiceRollDefinition;
|
||||
}
|
||||
|
||||
@@ -36,9 +36,9 @@ public sealed record UpdateCharacterRequest(string Name, Guid? CampaignId, strin
|
||||
|
||||
public sealed record CharacterSummary(Guid Id, string Name, Guid OwnerUserId, Guid? CampaignId, string OwnerDisplayName);
|
||||
|
||||
public sealed record CreateSkillRequest(string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, Guid? SkillGroupId = null, int? FumbleRange = null);
|
||||
public sealed record CreateSkillRequest(string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, Guid? SkillGroupId = null, int? FumbleRange = null, bool RolemasterAutoRetry = false);
|
||||
|
||||
public sealed record UpdateSkillRequest(string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, Guid? SkillGroupId = null, int? FumbleRange = null);
|
||||
public sealed record UpdateSkillRequest(string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, Guid? SkillGroupId = null, int? FumbleRange = null, bool RolemasterAutoRetry = false);
|
||||
|
||||
public sealed record SkillGroupSummary(Guid Id, Guid CharacterId, string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, int? FumbleRange);
|
||||
|
||||
@@ -46,7 +46,7 @@ public sealed record CreateSkillGroupRequest(string Name, string DiceRollDefinit
|
||||
|
||||
public sealed record UpdateSkillGroupRequest(string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, int? FumbleRange = null);
|
||||
|
||||
public sealed record SkillSummary(Guid Id, Guid CharacterId, Guid? SkillGroupId, string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, int? FumbleRange);
|
||||
public sealed record SkillSummary(Guid Id, Guid CharacterId, Guid? SkillGroupId, string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry);
|
||||
|
||||
public sealed record RollSkillRequest(string Visibility);
|
||||
|
||||
@@ -100,7 +100,7 @@ public sealed record RollResult(Guid RollId, Guid CampaignId, Guid CharacterId,
|
||||
|
||||
public sealed record CharacterSheetSkillGroup(Guid Id, string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, int? FumbleRange);
|
||||
|
||||
public sealed record CharacterSheetSkill(Guid Id, Guid? SkillGroupId, string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, int? FumbleRange);
|
||||
public sealed record CharacterSheetSkill(Guid Id, Guid? SkillGroupId, string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry);
|
||||
|
||||
public sealed record CharacterSheet(Guid CharacterId, CharacterSheetSkillGroup[] SkillGroups, CharacterSheetSkill[] Skills);
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ public sealed class RpgRollerDbContext(DbContextOptions<RpgRollerDbContext> opti
|
||||
entity.Property(x => x.WildDice).IsRequired();
|
||||
entity.Property(x => x.AllowFumble).IsRequired();
|
||||
entity.Property(x => x.FumbleRange).IsRequired(false);
|
||||
entity.Property(x => x.RolemasterAutoRetry).IsRequired().HasDefaultValue(false);
|
||||
entity.HasIndex(x => x.CharacterId);
|
||||
entity.HasIndex(x => x.SkillGroupId);
|
||||
});
|
||||
|
||||
@@ -74,6 +74,7 @@ public sealed class Skill
|
||||
public required int WildDice { get; set; }
|
||||
public required bool AllowFumble { get; set; }
|
||||
public int? FumbleRange { get; set; }
|
||||
public bool RolemasterAutoRetry { get; set; }
|
||||
}
|
||||
|
||||
public sealed class RollLogEntry
|
||||
|
||||
269
RpgRoller/Migrations/20260414204309_AddRolemasterAutoRetry.Designer.cs
generated
Normal file
269
RpgRoller/Migrations/20260414204309_AddRolemasterAutoRetry.Designer.cs
generated
Normal file
@@ -0,0 +1,269 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using RpgRoller.Data;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RpgRoller.Migrations
|
||||
{
|
||||
[DbContext(typeof(RpgRollerDbContext))]
|
||||
[Migration("20260414204309_AddRolemasterAutoRetry")]
|
||||
partial class AddRolemasterAutoRetry
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||
|
||||
modelBuilder.Entity("RpgRoller.Domain.Campaign", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("GmUserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Ruleset")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<long>("Version")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("GmUserId");
|
||||
|
||||
b.ToTable("Campaigns");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RpgRoller.Domain.Character", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("CampaignId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("OwnerUserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CampaignId");
|
||||
|
||||
b.HasIndex("OwnerUserId");
|
||||
|
||||
b.ToTable("Characters");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RpgRoller.Domain.RollLogEntry", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Breakdown")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("CampaignId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("CharacterId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Dice")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("Result")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("RollerUserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("SkillId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("TimestampUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Visibility")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CampaignId");
|
||||
|
||||
b.HasIndex("CharacterId");
|
||||
|
||||
b.HasIndex("RollerUserId");
|
||||
|
||||
b.HasIndex("SkillId");
|
||||
|
||||
b.ToTable("RollLogEntries");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RpgRoller.Domain.Skill", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("AllowFumble")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("CharacterId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DiceRollDefinition")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("FumbleRange")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("RolemasterAutoRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<Guid?>("SkillGroupId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("WildDice")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CharacterId");
|
||||
|
||||
b.HasIndex("SkillGroupId");
|
||||
|
||||
b.ToTable("Skills");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RpgRoller.Domain.SkillGroup", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("AllowFumble")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<Guid>("CharacterId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DiceRollDefinition")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("FumbleRange")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("WildDice")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CharacterId");
|
||||
|
||||
b.ToTable("SkillGroups");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RpgRoller.Domain.UserAccount", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("ActiveCharacterId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Roles")
|
||||
.IsRequired()
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("UsernameNormalized")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UsernameNormalized")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("RpgRoller.Domain.UserSession", b =>
|
||||
{
|
||||
b.Property<string>("Token")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.HasKey("Token");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("Sessions");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RpgRoller.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddRolemasterAutoRetry : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "RolemasterAutoRetry",
|
||||
table: "Skills",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "RolemasterAutoRetry",
|
||||
table: "Skills");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,6 +146,11 @@ namespace RpgRoller.Migrations
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("RolemasterAutoRetry")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<Guid?>("SkillGroupId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ public static class GameDtoMapper
|
||||
|
||||
public static SkillSummary ToSkillSummary(Skill skill)
|
||||
{
|
||||
return new(skill.Id, skill.CharacterId, skill.SkillGroupId, skill.Name, skill.DiceRollDefinition, skill.WildDice, skill.AllowFumble, skill.FumbleRange);
|
||||
return new(skill.Id, skill.CharacterId, skill.SkillGroupId, skill.Name, skill.DiceRollDefinition, skill.WildDice, skill.AllowFumble, skill.FumbleRange, skill.RolemasterAutoRetry);
|
||||
}
|
||||
|
||||
public static RollResult ToRollResult(RollLogEntry entry, IReadOnlyList<RollDieResult> dice)
|
||||
@@ -98,6 +98,6 @@ public static class GameDtoMapper
|
||||
|
||||
private static CharacterSheetSkill ToCharacterSheetSkill(Skill skill)
|
||||
{
|
||||
return new(skill.Id, skill.SkillGroupId, skill.Name, skill.DiceRollDefinition, skill.WildDice, skill.AllowFumble, skill.FumbleRange);
|
||||
return new(skill.Id, skill.SkillGroupId, skill.Name, skill.DiceRollDefinition, skill.WildDice, skill.AllowFumble, skill.FumbleRange, skill.RolemasterAutoRetry);
|
||||
}
|
||||
}
|
||||
@@ -140,14 +140,14 @@ public sealed class GameService : IGameService
|
||||
return m_SkillService.DeleteSkillGroup(sessionToken, skillGroupId);
|
||||
}
|
||||
|
||||
public ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null)
|
||||
public ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null, bool rolemasterAutoRetry = false)
|
||||
{
|
||||
return m_SkillService.CreateSkill(sessionToken, characterId, name, diceRollDefinition, wildDice, allowFumble, skillGroupId, fumbleRange);
|
||||
return m_SkillService.CreateSkill(sessionToken, characterId, name, diceRollDefinition, wildDice, allowFumble, skillGroupId, fumbleRange, rolemasterAutoRetry);
|
||||
}
|
||||
|
||||
public ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null)
|
||||
public ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null, bool rolemasterAutoRetry = false)
|
||||
{
|
||||
return m_SkillService.UpdateSkill(sessionToken, skillId, name, diceRollDefinition, wildDice, allowFumble, skillGroupId, fumbleRange);
|
||||
return m_SkillService.UpdateSkill(sessionToken, skillId, name, diceRollDefinition, wildDice, allowFumble, skillGroupId, fumbleRange, rolemasterAutoRetry);
|
||||
}
|
||||
|
||||
public ServiceResult<bool> DeleteSkill(string sessionToken, Guid skillId)
|
||||
|
||||
@@ -114,7 +114,7 @@ public sealed class GameSkillService(GameStateStore stateStore, GamePersistenceS
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null)
|
||||
public ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null, bool rolemasterAutoRetry = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return ServiceResult<SkillSummary>.Failure("invalid_skill_name", "Skill name is required.");
|
||||
@@ -134,7 +134,7 @@ public sealed class GameSkillService(GameStateStore stateStore, GamePersistenceS
|
||||
if (!GameAuthorization.CanEditCharacter(user.Id, character, campaign))
|
||||
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
||||
|
||||
var skillValidation = SkillDefinitionValidator.Validate(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||
var skillValidation = SkillDefinitionValidator.Validate(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange, rolemasterAutoRetry);
|
||||
if (!skillValidation.Succeeded)
|
||||
return ServiceResult<SkillSummary>.Failure(skillValidation.Error!.Code, skillValidation.Error.Message);
|
||||
|
||||
@@ -151,7 +151,8 @@ public sealed class GameSkillService(GameStateStore stateStore, GamePersistenceS
|
||||
DiceRollDefinition = skillValidation.Value.CanonicalExpression,
|
||||
WildDice = skillValidation.Value.WildDice,
|
||||
AllowFumble = skillValidation.Value.AllowFumble,
|
||||
FumbleRange = skillValidation.Value.FumbleRange
|
||||
FumbleRange = skillValidation.Value.FumbleRange,
|
||||
RolemasterAutoRetry = skillValidation.Value.RolemasterAutoRetry
|
||||
};
|
||||
|
||||
stateStore.SkillsById[skill.Id] = skill;
|
||||
@@ -162,7 +163,7 @@ public sealed class GameSkillService(GameStateStore stateStore, GamePersistenceS
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null)
|
||||
public ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null, bool rolemasterAutoRetry = false)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return ServiceResult<SkillSummary>.Failure("invalid_skill_name", "Skill name is required.");
|
||||
@@ -183,7 +184,7 @@ public sealed class GameSkillService(GameStateStore stateStore, GamePersistenceS
|
||||
if (!GameAuthorization.CanEditCharacter(user.Id, character, campaign))
|
||||
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
||||
|
||||
var skillValidation = SkillDefinitionValidator.Validate(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||
var skillValidation = SkillDefinitionValidator.Validate(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange, rolemasterAutoRetry);
|
||||
if (!skillValidation.Succeeded)
|
||||
return ServiceResult<SkillSummary>.Failure(skillValidation.Error!.Code, skillValidation.Error.Message);
|
||||
|
||||
@@ -196,6 +197,7 @@ public sealed class GameSkillService(GameStateStore stateStore, GamePersistenceS
|
||||
skill.WildDice = skillValidation.Value.WildDice;
|
||||
skill.AllowFumble = skillValidation.Value.AllowFumble;
|
||||
skill.FumbleRange = skillValidation.Value.FumbleRange;
|
||||
skill.RolemasterAutoRetry = skillValidation.Value.RolemasterAutoRetry;
|
||||
skill.SkillGroupId = resolvedSkillGroupId.Value;
|
||||
stateStore.TouchCharacterLocked(campaign.Id, character.Id);
|
||||
|
||||
|
||||
@@ -62,7 +62,8 @@ public static class GameStateCloneFactory
|
||||
DiceRollDefinition = skill.DiceRollDefinition,
|
||||
WildDice = skill.WildDice,
|
||||
AllowFumble = skill.AllowFumble,
|
||||
FumbleRange = skill.FumbleRange
|
||||
FumbleRange = skill.FumbleRange,
|
||||
RolemasterAutoRetry = skill.RolemasterAutoRetry
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -31,8 +31,8 @@ public interface IGameService
|
||||
ServiceResult<SkillGroupSummary> CreateSkillGroup(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null);
|
||||
ServiceResult<SkillGroupSummary> UpdateSkillGroup(string sessionToken, Guid skillGroupId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null);
|
||||
ServiceResult<bool> DeleteSkillGroup(string sessionToken, Guid skillGroupId);
|
||||
ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null);
|
||||
ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null);
|
||||
ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null, bool rolemasterAutoRetry = false);
|
||||
ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null, bool rolemasterAutoRetry = false);
|
||||
ServiceResult<bool> DeleteSkill(string sessionToken, Guid skillId);
|
||||
ServiceResult<CharacterSheet> GetCharacterSheet(string sessionToken, Guid characterId);
|
||||
|
||||
|
||||
@@ -4,63 +4,72 @@ namespace RpgRoller.Services;
|
||||
|
||||
public static class SkillDefinitionValidator
|
||||
{
|
||||
public static ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)> Validate(RulesetKind ruleset, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange)
|
||||
public static ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)> Validate(RulesetKind ruleset, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange, bool rolemasterAutoRetry = false)
|
||||
{
|
||||
var expressionValidation = DiceRules.ParseExpression(ruleset, diceRollDefinition);
|
||||
if (!expressionValidation.Succeeded)
|
||||
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)>.Failure(expressionValidation.Error!.Code, expressionValidation.Error.Message);
|
||||
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)>.Failure(expressionValidation.Error!.Code, expressionValidation.Error.Message);
|
||||
|
||||
var optionsValidation = ValidateOptions(ruleset, expressionValidation.Value!, wildDice, allowFumble, fumbleRange);
|
||||
var optionsValidation = ValidateOptions(ruleset, expressionValidation.Value!, wildDice, allowFumble, fumbleRange, rolemasterAutoRetry);
|
||||
if (!optionsValidation.Succeeded)
|
||||
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)>.Failure(optionsValidation.Error!.Code, optionsValidation.Error.Message);
|
||||
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)>.Failure(optionsValidation.Error!.Code, optionsValidation.Error.Message);
|
||||
|
||||
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)>.Success((expressionValidation.Value!.Canonical, optionsValidation.Value.WildDice, optionsValidation.Value.AllowFumble, optionsValidation.Value.FumbleRange));
|
||||
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)>.Success((expressionValidation.Value!.Canonical, optionsValidation.Value.WildDice, optionsValidation.Value.AllowFumble, optionsValidation.Value.FumbleRange, optionsValidation.Value.RolemasterAutoRetry));
|
||||
}
|
||||
|
||||
private static ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)> ValidateOptions(RulesetKind ruleset, DiceExpression expression, int wildDice, bool allowFumble, int? fumbleRange)
|
||||
private static ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)> ValidateOptions(RulesetKind ruleset, DiceExpression expression, int wildDice, bool allowFumble, int? fumbleRange, bool rolemasterAutoRetry)
|
||||
{
|
||||
if (wildDice < 0 || wildDice > 50)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_wild_dice", "Wild dice must be between 0 and 50.");
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)>.Failure("invalid_wild_dice", "Wild dice must be between 0 and 50.");
|
||||
|
||||
if (ruleset == RulesetKind.D6)
|
||||
{
|
||||
if (wildDice < 1)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_wild_dice", "D6 skills require at least one wild die.");
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)>.Failure("invalid_wild_dice", "D6 skills require at least one wild die.");
|
||||
|
||||
if (fumbleRange.HasValue)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_fumble_range", "Fumble range is only supported for Rolemaster open-ended percentile skills.");
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)>.Failure("invalid_fumble_range", "Fumble range is only supported for Rolemaster open-ended percentile skills.");
|
||||
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Success((wildDice, allowFumble, null));
|
||||
if (rolemasterAutoRetry)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)>.Failure("invalid_rolemaster_retry", "Automatic retry is only supported for Rolemaster open-ended percentile skills.");
|
||||
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)>.Success((wildDice, allowFumble, null, false));
|
||||
}
|
||||
|
||||
if (ruleset == RulesetKind.Rolemaster)
|
||||
{
|
||||
if (wildDice != 0)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_wild_dice", "Rolemaster skills do not support wild dice.");
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)>.Failure("invalid_wild_dice", "Rolemaster skills do not support wild dice.");
|
||||
|
||||
if (allowFumble)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_allow_fumble", "Rolemaster skills do not use the D6 allow-fumble option.");
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)>.Failure("invalid_allow_fumble", "Rolemaster skills do not use the D6 allow-fumble option.");
|
||||
|
||||
if (expression.Kind == DiceExpressionKind.RolemasterOpenEndedPercentile)
|
||||
{
|
||||
if (!fumbleRange.HasValue)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_fumble_range", "Rolemaster open-ended percentile skills require a fumble range.");
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)>.Failure("invalid_fumble_range", "Rolemaster open-ended percentile skills require a fumble range.");
|
||||
|
||||
if (fumbleRange < 0 || fumbleRange >= 96)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_fumble_range", "Fumble range must be between 0 and 95.");
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)>.Failure("invalid_fumble_range", "Fumble range must be between 0 and 95.");
|
||||
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Success((0, false, fumbleRange));
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)>.Success((0, false, fumbleRange, rolemasterAutoRetry));
|
||||
}
|
||||
|
||||
if (fumbleRange.HasValue)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_fumble_range", "Fumble range is only valid for Rolemaster open-ended percentile skills.");
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)>.Failure("invalid_fumble_range", "Fumble range is only valid for Rolemaster open-ended percentile skills.");
|
||||
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Success((0, false, null));
|
||||
if (rolemasterAutoRetry)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)>.Failure("invalid_rolemaster_retry", "Automatic retry is only supported for Rolemaster open-ended percentile skills.");
|
||||
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)>.Success((0, false, null, false));
|
||||
}
|
||||
|
||||
if (fumbleRange.HasValue)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_fumble_range", "Fumble range is only supported for Rolemaster open-ended percentile skills.");
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)>.Failure("invalid_fumble_range", "Fumble range is only supported for Rolemaster open-ended percentile skills.");
|
||||
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Success((0, false, null));
|
||||
if (rolemasterAutoRetry)
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)>.Failure("invalid_rolemaster_retry", "Automatic retry is only supported for Rolemaster open-ended percentile skills.");
|
||||
|
||||
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange, bool RolemasterAutoRetry)>.Success((0, false, null, false));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user