Add skill-group prototypes, delete flows, and per-group skill creation UX
This commit is contained in:
@@ -19,15 +19,27 @@ internal static class SkillEndpoints
|
||||
return ApiResultMapper.ToApiResult(result);
|
||||
});
|
||||
|
||||
group.MapDelete("/skills/{skillId:guid}", (Guid skillId, HttpContext context, IGameService game) =>
|
||||
{
|
||||
var result = game.DeleteSkill(context.GetRequiredSessionToken(), skillId);
|
||||
return ApiResultMapper.ToApiResult(result);
|
||||
});
|
||||
|
||||
group.MapPost("/characters/{characterId:guid}/skill-groups", (Guid characterId, CreateSkillGroupRequest request, HttpContext context, IGameService game) =>
|
||||
{
|
||||
var result = game.CreateSkillGroup(context.GetRequiredSessionToken(), characterId, request.Name);
|
||||
var result = game.CreateSkillGroup(context.GetRequiredSessionToken(), characterId, request.Name, request.DiceRollDefinition, request.WildDice, request.AllowFumble);
|
||||
return ApiResultMapper.ToApiResult(result);
|
||||
});
|
||||
|
||||
group.MapPut("/skill-groups/{skillGroupId:guid}", (Guid skillGroupId, UpdateSkillGroupRequest request, HttpContext context, IGameService game) =>
|
||||
{
|
||||
var result = game.UpdateSkillGroup(context.GetRequiredSessionToken(), skillGroupId, request.Name);
|
||||
var result = game.UpdateSkillGroup(context.GetRequiredSessionToken(), skillGroupId, request.Name, request.DiceRollDefinition, request.WildDice, request.AllowFumble);
|
||||
return ApiResultMapper.ToApiResult(result);
|
||||
});
|
||||
|
||||
group.MapDelete("/skill-groups/{skillGroupId:guid}", (Guid skillGroupId, HttpContext context, IGameService game) =>
|
||||
{
|
||||
var result = game.DeleteSkillGroup(context.GetRequiredSessionToken(), skillGroupId);
|
||||
return ApiResultMapper.ToApiResult(result);
|
||||
});
|
||||
|
||||
|
||||
@@ -51,6 +51,9 @@ public sealed class SkillFormModel
|
||||
public sealed class SkillGroupFormModel
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string DiceRollDefinition { get; set; } = string.Empty;
|
||||
public int WildDice { get; set; }
|
||||
public bool AllowFumble { get; set; }
|
||||
}
|
||||
|
||||
public enum HomeViewMode
|
||||
|
||||
@@ -45,14 +45,6 @@
|
||||
class="muted">| Owner: @OwnerLabel(SelectedCharacter.OwnerUserId) | Campaign: @SelectedCampaign.Name</span>
|
||||
</h3>
|
||||
<div class="chip-toolbar">
|
||||
<button type="button"
|
||||
class="chip-button"
|
||||
title="Add skill group"
|
||||
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
||||
@onclick="OpenCreateSkillGroupModal">
|
||||
<span aria-hidden="true">+</span>
|
||||
<span class="sr-only">Add skill group</span>
|
||||
</button>
|
||||
<label class="visibility-control" for="roll-visibility">Visibility</label>
|
||||
<select id="roll-visibility" value="@(RollVisibility == "private" ? "private" : "public")" @onchange="OnRollVisibilityChangedAsync">
|
||||
<option value="public">Public</option>
|
||||
@@ -74,15 +66,26 @@
|
||||
<div class="skill-group-block">
|
||||
<div class="skill-group-head">
|
||||
<strong>@group.Name</strong>
|
||||
<button
|
||||
type="button"
|
||||
class="chip-button"
|
||||
title="Rename skill group"
|
||||
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
||||
@onclick="() => OpenEditSkillGroupModal(group)">
|
||||
<span aria-hidden="true">✎</span>
|
||||
<span class="sr-only">Rename @group.Name</span>
|
||||
</button>
|
||||
<div class="skill-chip-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="chip-button"
|
||||
title="Edit skill group"
|
||||
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
||||
@onclick="() => OpenEditSkillGroupModal(group)">
|
||||
<span aria-hidden="true">✎</span>
|
||||
<span class="sr-only">Edit @group.Name</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="chip-button"
|
||||
title="Delete skill group"
|
||||
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
||||
@onclick="() => DeleteSkillGroupAsync(group.Id)">
|
||||
<span aria-hidden="true">✕</span>
|
||||
<span class="sr-only">Delete @group.Name</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (groupSkills.Count == 0)
|
||||
{
|
||||
@@ -115,61 +118,93 @@
|
||||
<span aria-hidden="true">⚄</span>
|
||||
<span class="sr-only">Roll @skill.Name</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (ungroupedSkills.Count > 0)
|
||||
{
|
||||
<div class="skill-group-block">
|
||||
<div class="skill-group-head">
|
||||
<strong>Ungrouped</strong>
|
||||
</div>
|
||||
<div class="skill-list">
|
||||
@foreach (var skill in ungroupedSkills)
|
||||
{
|
||||
<div class="skill-item">
|
||||
<div class="skill-details">
|
||||
<strong>@skill.Name</strong>
|
||||
<span>@SkillDefinitionLabel(skill)</span>
|
||||
</div>
|
||||
<div class="skill-chip-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="chip-button"
|
||||
title="Edit skill"
|
||||
title="Delete skill"
|
||||
disabled="@(IsMutating || !CanEditSkill(skill))"
|
||||
@onclick="() => OpenEditSkillModal(skill)">
|
||||
<span aria-hidden="true">✎</span>
|
||||
<span class="sr-only">Edit @skill.Name</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="chip-button"
|
||||
title="Roll skill"
|
||||
disabled="@(IsMutating || !CanRollSkill(skill))"
|
||||
@onclick="() => RollSkillAsync(skill.Id)">
|
||||
<span aria-hidden="true">⚄</span>
|
||||
<span class="sr-only">Roll @skill.Name</span>
|
||||
@onclick="() => DeleteSkillAsync(skill.Id)">
|
||||
<span aria-hidden="true">✕</span>
|
||||
<span class="sr-only">Delete @skill.Name</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
class="skill-item create-skill-item"
|
||||
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
||||
@onclick="() => OpenCreateSkillModal(group.Id)">
|
||||
<span class="skill-create-icon" aria-hidden="true">+</span>
|
||||
<span>Add skill</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="skill-list">
|
||||
<button
|
||||
type="button"
|
||||
class="skill-item create-skill-item"
|
||||
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
||||
@onclick="OpenCreateSkillModal">
|
||||
<span class="skill-create-icon" aria-hidden="true">+</span>
|
||||
<span>Add skill</span>
|
||||
</button>
|
||||
<div class="skill-group-block">
|
||||
<div class="skill-group-head">
|
||||
<strong>Ungrouped</strong>
|
||||
</div>
|
||||
@if (ungroupedSkills.Count == 0)
|
||||
{
|
||||
<p class="empty">No ungrouped skills.</p>
|
||||
}
|
||||
<div class="skill-list">
|
||||
@foreach (var skill in ungroupedSkills)
|
||||
{
|
||||
<div class="skill-item">
|
||||
<div class="skill-details">
|
||||
<strong>@skill.Name</strong>
|
||||
<span>@SkillDefinitionLabel(skill)</span>
|
||||
</div>
|
||||
<div class="skill-chip-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="chip-button"
|
||||
title="Edit skill"
|
||||
disabled="@(IsMutating || !CanEditSkill(skill))"
|
||||
@onclick="() => OpenEditSkillModal(skill)">
|
||||
<span aria-hidden="true">✎</span>
|
||||
<span class="sr-only">Edit @skill.Name</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="chip-button"
|
||||
title="Roll skill"
|
||||
disabled="@(IsMutating || !CanRollSkill(skill))"
|
||||
@onclick="() => RollSkillAsync(skill.Id)">
|
||||
<span aria-hidden="true">⚄</span>
|
||||
<span class="sr-only">Roll @skill.Name</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="chip-button"
|
||||
title="Delete skill"
|
||||
disabled="@(IsMutating || !CanEditSkill(skill))"
|
||||
@onclick="() => DeleteSkillAsync(skill.Id)">
|
||||
<span aria-hidden="true">✕</span>
|
||||
<span class="sr-only">Delete @skill.Name</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
class="skill-item create-skill-item"
|
||||
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
||||
@onclick="() => OpenCreateSkillModal(null)">
|
||||
<span class="skill-create-icon" aria-hidden="true">+</span>
|
||||
<span>Add skill</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="add-row-button"
|
||||
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
||||
@onclick="OpenCreateSkillGroupModal">[+] Add group
|
||||
</button>
|
||||
</article>
|
||||
}
|
||||
<div class="character-panel-fill" aria-hidden="true"></div>
|
||||
@@ -180,7 +215,7 @@
|
||||
{
|
||||
<div class="modal-overlay" role="presentation">
|
||||
<section class="modal-card" role="dialog" aria-modal="true" aria-label="Skill Group">
|
||||
<h2>@(ShowEditSkillGroupModal ? "Rename Skill Group" : "Create Skill Group")</h2>
|
||||
<h2>@(ShowEditSkillGroupModal ? "Edit Skill Group" : "Create Skill Group")</h2>
|
||||
@if (!string.IsNullOrWhiteSpace(SkillGroupState.ErrorMessage))
|
||||
{
|
||||
<p class="form-error">@SkillGroupState.ErrorMessage</p>
|
||||
@@ -192,10 +227,36 @@
|
||||
{
|
||||
<p class="field-error">@groupNameError</p>
|
||||
}
|
||||
|
||||
<label for="skill-group-expression">Prototype expression</label>
|
||||
<input id="skill-group-expression" @bind="SkillGroupState.Model.DiceRollDefinition" @bind:event="oninput"/>
|
||||
@if (SkillGroupState.Errors.TryGetValue("diceRollDefinition", out var expressionError))
|
||||
{
|
||||
<p class="field-error">@expressionError</p>
|
||||
}
|
||||
|
||||
@if (IsD6)
|
||||
{
|
||||
<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"/>
|
||||
@if (SkillGroupState.Errors.TryGetValue("wildDice", out var wildDiceError))
|
||||
{
|
||||
<p class="field-error">@wildDiceError</p>
|
||||
}
|
||||
|
||||
<label for="skill-group-allow-fumble">Prototype allow fumble</label>
|
||||
<input id="skill-group-allow-fumble" type="checkbox" @bind="SkillGroupState.Model.AllowFumble"/>
|
||||
}
|
||||
|
||||
@if (SkillGroupState.Errors.TryGetValue("character", out var characterError))
|
||||
{
|
||||
<p class="field-error">@characterError</p>
|
||||
}
|
||||
@if (SkillGroupState.Errors.TryGetValue("group", out var groupError))
|
||||
{
|
||||
<p class="field-error">@groupError</p>
|
||||
}
|
||||
|
||||
<div class="inline-actions">
|
||||
<button type="submit" disabled="@(IsMutating || IsSubmittingSkillGroup)">@(ShowEditSkillGroupModal ? "Save Group" : "Create Group")</button>
|
||||
<button type="button" class="ghost" disabled="@(IsMutating || IsSubmittingSkillGroup)" @onclick="CloseSkillGroupModals">Cancel</button>
|
||||
|
||||
@@ -7,15 +7,19 @@ namespace RpgRoller.Components.Pages.HomeControls;
|
||||
[ExcludeFromCodeCoverage]
|
||||
public partial class CharacterPanel
|
||||
{
|
||||
private void OpenCreateSkillModal()
|
||||
private void OpenCreateSkillModal(Guid? skillGroupId = null)
|
||||
{
|
||||
var selectedGroup = skillGroupId.HasValue
|
||||
? SelectedCharacterSkillGroups.FirstOrDefault(group => group.Id == skillGroupId.Value)
|
||||
: null;
|
||||
|
||||
CreateSkillInitialModel = new()
|
||||
{
|
||||
Name = string.Empty,
|
||||
DiceRollDefinition = string.Empty,
|
||||
SkillGroupId = string.Empty,
|
||||
WildDice = IsD6 ? 1 : 0,
|
||||
AllowFumble = IsD6
|
||||
DiceRollDefinition = selectedGroup?.DiceRollDefinition ?? string.Empty,
|
||||
SkillGroupId = selectedGroup?.Id.ToString() ?? string.Empty,
|
||||
WildDice = selectedGroup?.WildDice ?? (IsD6 ? 1 : 0),
|
||||
AllowFumble = selectedGroup?.AllowFumble ?? IsD6
|
||||
};
|
||||
|
||||
CreateSkillFormVersion++;
|
||||
@@ -71,6 +75,9 @@ public partial class CharacterPanel
|
||||
private void OpenCreateSkillGroupModal()
|
||||
{
|
||||
SkillGroupState.Model.Name = string.Empty;
|
||||
SkillGroupState.Model.DiceRollDefinition = string.Empty;
|
||||
SkillGroupState.Model.WildDice = IsD6 ? 1 : 0;
|
||||
SkillGroupState.Model.AllowFumble = IsD6;
|
||||
SkillGroupState.ResetValidation();
|
||||
ShowCreateSkillGroupModal = true;
|
||||
}
|
||||
@@ -79,6 +86,9 @@ public partial class CharacterPanel
|
||||
{
|
||||
EditingSkillGroupId = skillGroup.Id;
|
||||
SkillGroupState.Model.Name = skillGroup.Name;
|
||||
SkillGroupState.Model.DiceRollDefinition = skillGroup.DiceRollDefinition;
|
||||
SkillGroupState.Model.WildDice = skillGroup.WildDice;
|
||||
SkillGroupState.Model.AllowFumble = skillGroup.AllowFumble;
|
||||
SkillGroupState.ResetValidation();
|
||||
ShowEditSkillGroupModal = true;
|
||||
}
|
||||
@@ -98,6 +108,12 @@ public partial class CharacterPanel
|
||||
if (string.IsNullOrWhiteSpace(SkillGroupState.Model.Name))
|
||||
SkillGroupState.Errors["name"] = "Skill group name is required.";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(SkillGroupState.Model.DiceRollDefinition))
|
||||
SkillGroupState.Errors["diceRollDefinition"] = "Expression is required.";
|
||||
|
||||
if (IsD6 && SkillGroupState.Model.WildDice < 1)
|
||||
SkillGroupState.Errors["wildDice"] = "D6 groups require at least one wild die.";
|
||||
|
||||
if (!SelectedCharacterId.HasValue)
|
||||
SkillGroupState.Errors["character"] = "Select a character first.";
|
||||
|
||||
@@ -111,7 +127,14 @@ public partial class CharacterPanel
|
||||
try
|
||||
{
|
||||
var selectedCharacterId = SelectedCharacterId!.Value;
|
||||
var createdGroup = await ApiClient.RequestAsync<SkillGroupSummary>("POST", $"/api/characters/{selectedCharacterId}/skill-groups", new CreateSkillGroupRequest(SkillGroupState.Model.Name.Trim()));
|
||||
var createdGroup = await ApiClient.RequestAsync<SkillGroupSummary>(
|
||||
"POST",
|
||||
$"/api/characters/{selectedCharacterId}/skill-groups",
|
||||
new CreateSkillGroupRequest(
|
||||
SkillGroupState.Model.Name.Trim(),
|
||||
SkillGroupState.Model.DiceRollDefinition.Trim(),
|
||||
SkillGroupState.Model.WildDice,
|
||||
SkillGroupState.Model.AllowFumble));
|
||||
CloseSkillGroupModals();
|
||||
await SkillGroupCreated.InvokeAsync(createdGroup.Id);
|
||||
}
|
||||
@@ -132,6 +155,12 @@ public partial class CharacterPanel
|
||||
if (string.IsNullOrWhiteSpace(SkillGroupState.Model.Name))
|
||||
SkillGroupState.Errors["name"] = "Skill group name is required.";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(SkillGroupState.Model.DiceRollDefinition))
|
||||
SkillGroupState.Errors["diceRollDefinition"] = "Expression is required.";
|
||||
|
||||
if (IsD6 && SkillGroupState.Model.WildDice < 1)
|
||||
SkillGroupState.Errors["wildDice"] = "D6 groups require at least one wild die.";
|
||||
|
||||
if (!EditingSkillGroupId.HasValue)
|
||||
SkillGroupState.Errors["group"] = "Select a skill group first.";
|
||||
|
||||
@@ -145,7 +174,14 @@ public partial class CharacterPanel
|
||||
try
|
||||
{
|
||||
var editingSkillGroupId = EditingSkillGroupId!.Value;
|
||||
var updatedGroup = await ApiClient.RequestAsync<SkillGroupSummary>("PUT", $"/api/skill-groups/{editingSkillGroupId}", new UpdateSkillGroupRequest(SkillGroupState.Model.Name.Trim()));
|
||||
var updatedGroup = await ApiClient.RequestAsync<SkillGroupSummary>(
|
||||
"PUT",
|
||||
$"/api/skill-groups/{editingSkillGroupId}",
|
||||
new UpdateSkillGroupRequest(
|
||||
SkillGroupState.Model.Name.Trim(),
|
||||
SkillGroupState.Model.DiceRollDefinition.Trim(),
|
||||
SkillGroupState.Model.WildDice,
|
||||
SkillGroupState.Model.AllowFumble));
|
||||
CloseSkillGroupModals();
|
||||
await SkillGroupUpdated.InvokeAsync(updatedGroup.Id);
|
||||
}
|
||||
@@ -159,6 +195,32 @@ public partial class CharacterPanel
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteSkillAsync(Guid skillId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ApiClient.RequestAsync<bool>("DELETE", $"/api/skills/{skillId}");
|
||||
await SkillDeleted.InvokeAsync(skillId);
|
||||
}
|
||||
catch (ApiRequestException ex)
|
||||
{
|
||||
await ErrorOccurred.InvokeAsync(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteSkillGroupAsync(Guid skillGroupId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ApiClient.RequestAsync<bool>("DELETE", $"/api/skill-groups/{skillGroupId}");
|
||||
await SkillGroupDeleted.InvokeAsync(skillGroupId);
|
||||
}
|
||||
catch (ApiRequestException ex)
|
||||
{
|
||||
await ErrorOccurred.InvokeAsync(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static string InitialsFor(string value)
|
||||
{
|
||||
var words = value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
@@ -250,6 +312,15 @@ public partial class CharacterPanel
|
||||
[Parameter]
|
||||
public EventCallback<Guid> SkillGroupUpdated { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<Guid> SkillDeleted { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<Guid> SkillGroupDeleted { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<string> ErrorOccurred { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<Guid> RollRequested { get; set; }
|
||||
}
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
SkillUpdated="OnSkillUpdatedAsync"
|
||||
SkillGroupCreated="OnSkillGroupCreatedAsync"
|
||||
SkillGroupUpdated="OnSkillGroupUpdatedAsync"
|
||||
SkillDeleted="OnSkillDeletedAsync"
|
||||
SkillGroupDeleted="OnSkillGroupDeletedAsync"
|
||||
ErrorOccurred="OnCharacterPanelErrorAsync"
|
||||
RollRequested="RollSkillAsync"/>
|
||||
|
||||
<CampaignLogPanel
|
||||
|
||||
@@ -392,6 +392,24 @@ public partial class Workspace : IAsyncDisposable
|
||||
SetStatus("Skill group updated.", false);
|
||||
}
|
||||
|
||||
private async Task OnSkillDeletedAsync(Guid _)
|
||||
{
|
||||
await RefreshCampaignScopeAsync();
|
||||
SetStatus("Skill deleted.", false);
|
||||
}
|
||||
|
||||
private async Task OnSkillGroupDeletedAsync(Guid _)
|
||||
{
|
||||
await RefreshCampaignScopeAsync();
|
||||
SetStatus("Skill group deleted.", false);
|
||||
}
|
||||
|
||||
private Task OnCharacterPanelErrorAsync(string message)
|
||||
{
|
||||
SetStatus(message, true);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task RollSkillAsync(Guid skillId)
|
||||
{
|
||||
if (SelectedCampaign is null)
|
||||
|
||||
@@ -30,11 +30,11 @@ public sealed record CreateSkillRequest(string Name, string DiceRollDefinition,
|
||||
|
||||
public sealed record UpdateSkillRequest(string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, Guid? SkillGroupId = null);
|
||||
|
||||
public sealed record SkillGroupSummary(Guid Id, Guid CharacterId, string Name);
|
||||
public sealed record SkillGroupSummary(Guid Id, Guid CharacterId, string Name, string DiceRollDefinition, int WildDice, bool AllowFumble);
|
||||
|
||||
public sealed record CreateSkillGroupRequest(string Name);
|
||||
public sealed record CreateSkillGroupRequest(string Name, string DiceRollDefinition, int WildDice, bool AllowFumble);
|
||||
|
||||
public sealed record UpdateSkillGroupRequest(string Name);
|
||||
public sealed record UpdateSkillGroupRequest(string Name, string DiceRollDefinition, int WildDice, bool AllowFumble);
|
||||
|
||||
public sealed record SkillSummary(Guid Id, Guid CharacterId, Guid? SkillGroupId, string Name, string DiceRollDefinition, int WildDice, bool AllowFumble);
|
||||
|
||||
|
||||
@@ -61,6 +61,9 @@ public sealed class RpgRollerDbContext : DbContext
|
||||
{
|
||||
entity.HasKey(x => x.Id);
|
||||
entity.Property(x => x.Name).IsRequired().HasMaxLength(128);
|
||||
entity.Property(x => x.DiceRollDefinition).IsRequired().HasMaxLength(128);
|
||||
entity.Property(x => x.WildDice).IsRequired();
|
||||
entity.Property(x => x.AllowFumble).IsRequired();
|
||||
entity.HasIndex(x => x.CharacterId);
|
||||
});
|
||||
|
||||
|
||||
@@ -51,6 +51,9 @@ public sealed class SkillGroup
|
||||
public required Guid Id { get; init; }
|
||||
public required Guid CharacterId { get; set; }
|
||||
public required string Name { get; set; }
|
||||
public required string DiceRollDefinition { get; set; }
|
||||
public required int WildDice { get; set; }
|
||||
public required bool AllowFumble { get; set; }
|
||||
}
|
||||
|
||||
public sealed class Skill
|
||||
|
||||
253
RpgRoller/Migrations/20260226131003_AddSkillGroupPrototypes.Designer.cs
generated
Normal file
253
RpgRoller/Migrations/20260226131003_AddSkillGroupPrototypes.Designer.cs
generated
Normal file
@@ -0,0 +1,253 @@
|
||||
// <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("20260226131003_AddSkillGroupPrototypes")]
|
||||
partial class AddSkillGroupPrototypes
|
||||
{
|
||||
/// <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<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
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<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>("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,87 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace RpgRoller.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddSkillGroupPrototypes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "AllowFumble",
|
||||
table: "SkillGroups",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "DiceRollDefinition",
|
||||
table: "SkillGroups",
|
||||
type: "TEXT",
|
||||
maxLength: 128,
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "WildDice",
|
||||
table: "SkillGroups",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
UPDATE SkillGroups
|
||||
SET DiceRollDefinition = CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM Characters c
|
||||
INNER JOIN Campaigns cp ON cp.Id = c.CampaignId
|
||||
WHERE c.Id = SkillGroups.CharacterId
|
||||
AND cp.Ruleset = 'D6')
|
||||
THEN '1D'
|
||||
ELSE '1d20'
|
||||
END,
|
||||
WildDice = CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM Characters c
|
||||
INNER JOIN Campaigns cp ON cp.Id = c.CampaignId
|
||||
WHERE c.Id = SkillGroups.CharacterId
|
||||
AND cp.Ruleset = 'D6')
|
||||
THEN 1
|
||||
ELSE 0
|
||||
END,
|
||||
AllowFumble = CASE
|
||||
WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM Characters c
|
||||
INNER JOIN Campaigns cp ON cp.Id = c.CampaignId
|
||||
WHERE c.Id = SkillGroups.CharacterId
|
||||
AND cp.Ruleset = 'D6')
|
||||
THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
WHERE DiceRollDefinition = '';
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AllowFumble",
|
||||
table: "SkillGroups");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DiceRollDefinition",
|
||||
table: "SkillGroups");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "WildDice",
|
||||
table: "SkillGroups");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -164,14 +164,25 @@ namespace RpgRoller.Migrations
|
||||
.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<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("WildDice")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CharacterId");
|
||||
|
||||
@@ -322,7 +322,7 @@ public sealed class GameService : IGameService
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<SkillGroupSummary> CreateSkillGroup(string sessionToken, Guid characterId, string name)
|
||||
public ServiceResult<SkillGroupSummary> CreateSkillGroup(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return ServiceResult<SkillGroupSummary>.Failure("invalid_skill_group_name", "Skill group name is required.");
|
||||
@@ -340,11 +340,18 @@ public sealed class GameService : IGameService
|
||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||
return ServiceResult<SkillGroupSummary>.Failure("forbidden", "Only the owner or GM can manage skill groups.");
|
||||
|
||||
var prototypeValidation = ValidateSkillDefinition(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble);
|
||||
if (!prototypeValidation.Succeeded)
|
||||
return ServiceResult<SkillGroupSummary>.Failure(prototypeValidation.Error!.Code, prototypeValidation.Error.Message);
|
||||
|
||||
var group = new SkillGroup
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CharacterId = character.Id,
|
||||
Name = name.Trim()
|
||||
Name = name.Trim(),
|
||||
DiceRollDefinition = prototypeValidation.Value!.CanonicalExpression,
|
||||
WildDice = prototypeValidation.Value.WildDice,
|
||||
AllowFumble = prototypeValidation.Value.AllowFumble
|
||||
};
|
||||
|
||||
m_SkillGroupsById[group.Id] = group;
|
||||
@@ -355,7 +362,7 @@ public sealed class GameService : IGameService
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<SkillGroupSummary> UpdateSkillGroup(string sessionToken, Guid skillGroupId, string name)
|
||||
public ServiceResult<SkillGroupSummary> UpdateSkillGroup(string sessionToken, Guid skillGroupId, string name, string diceRollDefinition, int wildDice, bool allowFumble)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return ServiceResult<SkillGroupSummary>.Failure("invalid_skill_group_name", "Skill group name is required.");
|
||||
@@ -374,7 +381,14 @@ public sealed class GameService : IGameService
|
||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||
return ServiceResult<SkillGroupSummary>.Failure("forbidden", "Only the owner or GM can manage skill groups.");
|
||||
|
||||
var prototypeValidation = ValidateSkillDefinition(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble);
|
||||
if (!prototypeValidation.Succeeded)
|
||||
return ServiceResult<SkillGroupSummary>.Failure(prototypeValidation.Error!.Code, prototypeValidation.Error.Message);
|
||||
|
||||
group.Name = name.Trim();
|
||||
group.DiceRollDefinition = prototypeValidation.Value!.CanonicalExpression;
|
||||
group.WildDice = prototypeValidation.Value.WildDice;
|
||||
group.AllowFumble = prototypeValidation.Value.AllowFumble;
|
||||
TouchCampaignLocked(campaign.Id);
|
||||
|
||||
PersistStateLocked();
|
||||
@@ -382,6 +396,33 @@ public sealed class GameService : IGameService
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<bool> DeleteSkillGroup(string sessionToken, Guid skillGroupId)
|
||||
{
|
||||
lock (m_Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
if (!m_SkillGroupsById.TryGetValue(skillGroupId, out var group))
|
||||
return ServiceResult<bool>.Failure("skill_group_not_found", "Skill group was not found.");
|
||||
|
||||
var character = m_CharactersById[group.CharacterId];
|
||||
var campaign = m_CampaignsById[character.CampaignId];
|
||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||
return ServiceResult<bool>.Failure("forbidden", "Only the owner or GM can manage skill groups.");
|
||||
|
||||
foreach (var skill in m_SkillsById.Values.Where(skill => skill.SkillGroupId == group.Id))
|
||||
skill.SkillGroupId = null;
|
||||
|
||||
m_SkillGroupsById.Remove(group.Id);
|
||||
TouchCampaignLocked(campaign.Id);
|
||||
|
||||
PersistStateLocked();
|
||||
return ServiceResult<bool>.Success(true);
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
@@ -400,13 +441,9 @@ public sealed class GameService : IGameService
|
||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
||||
|
||||
var expressionValidation = DiceRules.ParseExpression(campaign.Ruleset, diceRollDefinition);
|
||||
if (!expressionValidation.Succeeded)
|
||||
return ServiceResult<SkillSummary>.Failure(expressionValidation.Error!.Code, expressionValidation.Error.Message);
|
||||
|
||||
var optionsValidation = ValidateSkillOptions(campaign.Ruleset, wildDice, allowFumble);
|
||||
if (!optionsValidation.Succeeded)
|
||||
return ServiceResult<SkillSummary>.Failure(optionsValidation.Error!.Code, optionsValidation.Error.Message);
|
||||
var skillValidation = ValidateSkillDefinition(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble);
|
||||
if (!skillValidation.Succeeded)
|
||||
return ServiceResult<SkillSummary>.Failure(skillValidation.Error!.Code, skillValidation.Error.Message);
|
||||
|
||||
var resolvedSkillGroupId = ResolveSkillGroupForSkillChangeLocked(skillGroupId, character.Id);
|
||||
if (!resolvedSkillGroupId.Succeeded)
|
||||
@@ -418,9 +455,9 @@ public sealed class GameService : IGameService
|
||||
CharacterId = character.Id,
|
||||
SkillGroupId = resolvedSkillGroupId.Value,
|
||||
Name = name.Trim(),
|
||||
DiceRollDefinition = expressionValidation.Value!.Canonical,
|
||||
WildDice = optionsValidation.Value!.WildDice,
|
||||
AllowFumble = optionsValidation.Value.AllowFumble
|
||||
DiceRollDefinition = skillValidation.Value!.CanonicalExpression,
|
||||
WildDice = skillValidation.Value.WildDice,
|
||||
AllowFumble = skillValidation.Value.AllowFumble
|
||||
};
|
||||
|
||||
m_SkillsById[skill.Id] = skill;
|
||||
@@ -450,22 +487,18 @@ public sealed class GameService : IGameService
|
||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
||||
|
||||
var expressionValidation = DiceRules.ParseExpression(campaign.Ruleset, diceRollDefinition);
|
||||
if (!expressionValidation.Succeeded)
|
||||
return ServiceResult<SkillSummary>.Failure(expressionValidation.Error!.Code, expressionValidation.Error.Message);
|
||||
|
||||
var optionsValidation = ValidateSkillOptions(campaign.Ruleset, wildDice, allowFumble);
|
||||
if (!optionsValidation.Succeeded)
|
||||
return ServiceResult<SkillSummary>.Failure(optionsValidation.Error!.Code, optionsValidation.Error.Message);
|
||||
var skillValidation = ValidateSkillDefinition(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble);
|
||||
if (!skillValidation.Succeeded)
|
||||
return ServiceResult<SkillSummary>.Failure(skillValidation.Error!.Code, skillValidation.Error.Message);
|
||||
|
||||
var resolvedSkillGroupId = ResolveSkillGroupForSkillChangeLocked(skillGroupId, character.Id);
|
||||
if (!resolvedSkillGroupId.Succeeded)
|
||||
return ServiceResult<SkillSummary>.Failure(resolvedSkillGroupId.Error!.Code, resolvedSkillGroupId.Error.Message);
|
||||
|
||||
skill.Name = name.Trim();
|
||||
skill.DiceRollDefinition = expressionValidation.Value!.Canonical;
|
||||
skill.WildDice = optionsValidation.Value!.WildDice;
|
||||
skill.AllowFumble = optionsValidation.Value.AllowFumble;
|
||||
skill.DiceRollDefinition = skillValidation.Value!.CanonicalExpression;
|
||||
skill.WildDice = skillValidation.Value.WildDice;
|
||||
skill.AllowFumble = skillValidation.Value.AllowFumble;
|
||||
skill.SkillGroupId = resolvedSkillGroupId.Value;
|
||||
TouchCampaignLocked(campaign.Id);
|
||||
|
||||
@@ -474,6 +507,30 @@ public sealed class GameService : IGameService
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<bool> DeleteSkill(string sessionToken, Guid skillId)
|
||||
{
|
||||
lock (m_Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<bool>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
if (!m_SkillsById.TryGetValue(skillId, out var skill))
|
||||
return ServiceResult<bool>.Failure("skill_not_found", "Skill was not found.");
|
||||
|
||||
var character = m_CharactersById[skill.CharacterId];
|
||||
var campaign = m_CampaignsById[character.CampaignId];
|
||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||
return ServiceResult<bool>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
||||
|
||||
m_SkillsById.Remove(skill.Id);
|
||||
TouchCampaignLocked(campaign.Id);
|
||||
|
||||
PersistStateLocked();
|
||||
return ServiceResult<bool>.Success(true);
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility)
|
||||
{
|
||||
lock (m_Gate)
|
||||
@@ -548,6 +605,19 @@ public sealed class GameService : IGameService
|
||||
}
|
||||
}
|
||||
|
||||
private static ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble)> ValidateSkillDefinition(RulesetKind ruleset, string diceRollDefinition, int wildDice, bool allowFumble)
|
||||
{
|
||||
var expressionValidation = DiceRules.ParseExpression(ruleset, diceRollDefinition);
|
||||
if (!expressionValidation.Succeeded)
|
||||
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble)>.Failure(expressionValidation.Error!.Code, expressionValidation.Error.Message);
|
||||
|
||||
var optionsValidation = ValidateSkillOptions(ruleset, wildDice, allowFumble);
|
||||
if (!optionsValidation.Succeeded)
|
||||
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble)>.Failure(optionsValidation.Error!.Code, optionsValidation.Error.Message);
|
||||
|
||||
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble)>.Success((expressionValidation.Value!.Canonical, optionsValidation.Value!.WildDice, optionsValidation.Value.AllowFumble));
|
||||
}
|
||||
|
||||
private static ServiceResult<(int WildDice, bool AllowFumble)> ValidateSkillOptions(RulesetKind ruleset, int wildDice, bool allowFumble)
|
||||
{
|
||||
if (wildDice < 0 || wildDice > 50)
|
||||
@@ -732,7 +802,7 @@ public sealed class GameService : IGameService
|
||||
|
||||
private static SkillGroupSummary ToSkillGroupSummary(SkillGroup skillGroup)
|
||||
{
|
||||
return new(skillGroup.Id, skillGroup.CharacterId, skillGroup.Name);
|
||||
return new(skillGroup.Id, skillGroup.CharacterId, skillGroup.Name, skillGroup.DiceRollDefinition, skillGroup.WildDice, skillGroup.AllowFumble);
|
||||
}
|
||||
|
||||
private static SkillSummary ToSkillSummary(Skill skill)
|
||||
@@ -992,7 +1062,10 @@ public sealed class GameService : IGameService
|
||||
{
|
||||
Id = skillGroup.Id,
|
||||
CharacterId = skillGroup.CharacterId,
|
||||
Name = skillGroup.Name
|
||||
Name = skillGroup.Name,
|
||||
DiceRollDefinition = skillGroup.DiceRollDefinition,
|
||||
WildDice = skillGroup.WildDice,
|
||||
AllowFumble = skillGroup.AllowFumble
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,12 @@ public interface IGameService
|
||||
ServiceResult<bool> ActivateCharacter(string sessionToken, Guid characterId);
|
||||
ServiceResult<IReadOnlyList<CharacterSummary>> GetCurrentCampaignCharacters(string sessionToken);
|
||||
|
||||
ServiceResult<SkillGroupSummary> CreateSkillGroup(string sessionToken, Guid characterId, string name);
|
||||
ServiceResult<SkillGroupSummary> UpdateSkillGroup(string sessionToken, Guid skillGroupId, string name);
|
||||
ServiceResult<SkillGroupSummary> CreateSkillGroup(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble);
|
||||
ServiceResult<SkillGroupSummary> UpdateSkillGroup(string sessionToken, Guid skillGroupId, string name, string diceRollDefinition, int wildDice, bool allowFumble);
|
||||
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);
|
||||
ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null);
|
||||
ServiceResult<bool> DeleteSkill(string sessionToken, Guid skillId);
|
||||
|
||||
ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility);
|
||||
ServiceResult<IReadOnlyList<CampaignLogEntry>> GetCampaignLog(string sessionToken, Guid campaignId);
|
||||
|
||||
Reference in New Issue
Block a user