Add skill-group prototypes, delete flows, and per-group skill creation UX
This commit is contained in:
@@ -46,7 +46,8 @@ Backend state persistence:
|
|||||||
|
|
||||||
Gameplay capabilities now include:
|
Gameplay capabilities now include:
|
||||||
|
|
||||||
- Skill groups per character (create, rename, and assign/reassign skills to groups)
|
- Skill groups per character with skill prototypes (create/edit/delete groups, assign/reassign skills, and prefill new skill forms from group defaults)
|
||||||
|
- Skill and skill-group deletion flows
|
||||||
- GM-driven character owner transfer within campaign management flows
|
- GM-driven character owner transfer within campaign management flows
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|||||||
@@ -73,9 +73,12 @@ public sealed class CampaignApiTests : ApiTestBase
|
|||||||
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(gmClient, "/api/campaigns", new("Grouped Campaign", "d6"));
|
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(gmClient, "/api/campaigns", new("Grouped Campaign", "d6"));
|
||||||
var character = await PostAsync<CreateCharacterRequest, CharacterSummary>(ownerClient, "/api/characters", new("Grouped Hero", campaign.Id));
|
var character = await PostAsync<CreateCharacterRequest, CharacterSummary>(ownerClient, "/api/characters", new("Grouped Hero", campaign.Id));
|
||||||
|
|
||||||
var createdGroup = await PostAsync<CreateSkillGroupRequest, SkillGroupSummary>(ownerClient, $"/api/characters/{character.Id}/skill-groups", new("Combat"));
|
var createdGroup = await PostAsync<CreateSkillGroupRequest, SkillGroupSummary>(ownerClient, $"/api/characters/{character.Id}/skill-groups", new("Combat", "2D+1", 1, true));
|
||||||
var renamedGroup = await PutAsync<UpdateSkillGroupRequest, SkillGroupSummary>(gmClient, $"/api/skill-groups/{createdGroup.Id}", new("Battle"));
|
var renamedGroup = await PutAsync<UpdateSkillGroupRequest, SkillGroupSummary>(gmClient, $"/api/skill-groups/{createdGroup.Id}", new("Battle", "3D+2", 2, false));
|
||||||
Assert.Equal("Battle", renamedGroup.Name);
|
Assert.Equal("Battle", renamedGroup.Name);
|
||||||
|
Assert.Equal("3D+2", renamedGroup.DiceRollDefinition);
|
||||||
|
Assert.Equal(2, renamedGroup.WildDice);
|
||||||
|
Assert.False(renamedGroup.AllowFumble);
|
||||||
|
|
||||||
var groupedSkill = await PostAsync<CreateSkillRequest, SkillSummary>(ownerClient, $"/api/characters/{character.Id}/skills", new("Strike", "2D+1", 1, true, renamedGroup.Id));
|
var groupedSkill = await PostAsync<CreateSkillRequest, SkillSummary>(ownerClient, $"/api/characters/{character.Id}/skills", new("Strike", "2D+1", 1, true, renamedGroup.Id));
|
||||||
Assert.Equal(renamedGroup.Id, groupedSkill.SkillGroupId);
|
Assert.Equal(renamedGroup.Id, groupedSkill.SkillGroupId);
|
||||||
@@ -83,6 +86,15 @@ public sealed class CampaignApiTests : ApiTestBase
|
|||||||
var ungroupedSkill = await PutAsync<UpdateSkillRequest, SkillSummary>(ownerClient, $"/api/skills/{groupedSkill.Id}", new("Strike", "2D+1", 1, true, null));
|
var ungroupedSkill = await PutAsync<UpdateSkillRequest, SkillSummary>(ownerClient, $"/api/skills/{groupedSkill.Id}", new("Strike", "2D+1", 1, true, null));
|
||||||
Assert.Null(ungroupedSkill.SkillGroupId);
|
Assert.Null(ungroupedSkill.SkillGroupId);
|
||||||
|
|
||||||
|
var groupedAgainSkill = await PutAsync<UpdateSkillRequest, SkillSummary>(ownerClient, $"/api/skills/{groupedSkill.Id}", new("Strike", "2D+1", 1, true, renamedGroup.Id));
|
||||||
|
Assert.Equal(renamedGroup.Id, groupedAgainSkill.SkillGroupId);
|
||||||
|
|
||||||
|
var deleteSkill = await ownerClient.DeleteAsync($"/api/skills/{groupedAgainSkill.Id}");
|
||||||
|
Assert.Equal(HttpStatusCode.OK, deleteSkill.StatusCode);
|
||||||
|
|
||||||
|
var deleteGroup = await ownerClient.DeleteAsync($"/api/skill-groups/{renamedGroup.Id}");
|
||||||
|
Assert.Equal(HttpStatusCode.OK, deleteGroup.StatusCode);
|
||||||
|
|
||||||
var transferResult = await PutAsync<UpdateCharacterRequest, CharacterSummary>(gmClient, $"/api/characters/{character.Id}", new("Grouped Hero", campaign.Id, "receiver2"));
|
var transferResult = await PutAsync<UpdateCharacterRequest, CharacterSummary>(gmClient, $"/api/characters/{character.Id}", new("Grouped Hero", campaign.Id, "receiver2"));
|
||||||
Assert.Equal("Grouped Hero", transferResult.Name);
|
Assert.Equal("Grouped Hero", transferResult.Name);
|
||||||
|
|
||||||
@@ -93,6 +105,6 @@ public sealed class CampaignApiTests : ApiTestBase
|
|||||||
Assert.Equal(HttpStatusCode.OK, receiverActivate.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, receiverActivate.StatusCode);
|
||||||
|
|
||||||
var details = await GetAsync<CampaignDetails>(gmClient, $"/api/campaigns/{campaign.Id}");
|
var details = await GetAsync<CampaignDetails>(gmClient, $"/api/campaigns/{campaign.Id}");
|
||||||
Assert.Contains(details.SkillGroups, group => group.Id == renamedGroup.Id);
|
Assert.DoesNotContain(details.SkillGroups, group => group.Id == renamedGroup.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,25 +20,41 @@ public sealed class ServiceSkillGroupAndOwnershipTests
|
|||||||
var ownerCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Owner Char", campaign.Id));
|
var ownerCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Owner Char", campaign.Id));
|
||||||
var otherCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(otherSession, "Other Char", campaign.Id));
|
var otherCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(otherSession, "Other Char", campaign.Id));
|
||||||
|
|
||||||
var ownerGroup = ServiceTestSupport.GetValue(service.CreateSkillGroup(ownerSession, ownerCharacter.Id, "Combat"));
|
var ownerGroup = ServiceTestSupport.GetValue(service.CreateSkillGroup(ownerSession, ownerCharacter.Id, "Combat", "2D+1", 1, true));
|
||||||
Assert.False(service.CreateSkillGroup(ownerSession, otherCharacter.Id, "Not Allowed").Succeeded);
|
Assert.False(service.CreateSkillGroup(ownerSession, otherCharacter.Id, "Not Allowed", "2D+1", 1, true).Succeeded);
|
||||||
|
|
||||||
Assert.False(service.UpdateSkillGroup(otherSession, ownerGroup.Id, "Renamed by Other").Succeeded);
|
Assert.False(service.UpdateSkillGroup(otherSession, ownerGroup.Id, "Renamed by Other", "2D+1", 1, true).Succeeded);
|
||||||
var renamedGroup = ServiceTestSupport.GetValue(service.UpdateSkillGroup(gmSession, ownerGroup.Id, "Battle"));
|
var renamedGroup = ServiceTestSupport.GetValue(service.UpdateSkillGroup(gmSession, ownerGroup.Id, "Battle", "3D+2", 2, false));
|
||||||
Assert.Equal("Battle", renamedGroup.Name);
|
Assert.Equal("Battle", renamedGroup.Name);
|
||||||
|
Assert.Equal("3D+2", renamedGroup.DiceRollDefinition);
|
||||||
|
Assert.Equal(2, renamedGroup.WildDice);
|
||||||
|
Assert.False(renamedGroup.AllowFumble);
|
||||||
|
|
||||||
var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, "Strike", "2D+1", 1, true, renamedGroup.Id));
|
var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, "Strike", "2D+1", 1, true, renamedGroup.Id));
|
||||||
Assert.Equal(renamedGroup.Id, skill.SkillGroupId);
|
Assert.Equal(renamedGroup.Id, skill.SkillGroupId);
|
||||||
|
|
||||||
var otherGroup = ServiceTestSupport.GetValue(service.CreateSkillGroup(otherSession, otherCharacter.Id, "Other Group"));
|
var otherGroup = ServiceTestSupport.GetValue(service.CreateSkillGroup(otherSession, otherCharacter.Id, "Other Group", "2D+1", 1, true));
|
||||||
Assert.False(service.UpdateSkill(ownerSession, skill.Id, "Strike", "2D+1", 1, true, otherGroup.Id).Succeeded);
|
Assert.False(service.UpdateSkill(ownerSession, skill.Id, "Strike", "2D+1", 1, true, otherGroup.Id).Succeeded);
|
||||||
|
|
||||||
var ungroupedSkill = ServiceTestSupport.GetValue(service.UpdateSkill(ownerSession, skill.Id, "Strike", "2D+1", 1, true, null));
|
var ungroupedSkill = ServiceTestSupport.GetValue(service.UpdateSkill(ownerSession, skill.Id, "Strike", "2D+1", 1, true, null));
|
||||||
Assert.Null(ungroupedSkill.SkillGroupId);
|
Assert.Null(ungroupedSkill.SkillGroupId);
|
||||||
|
|
||||||
|
var regroupedSkill = ServiceTestSupport.GetValue(service.UpdateSkill(ownerSession, skill.Id, "Strike", "2D+1", 1, true, renamedGroup.Id));
|
||||||
|
Assert.Equal(renamedGroup.Id, regroupedSkill.SkillGroupId);
|
||||||
|
|
||||||
|
var deletedGroup = ServiceTestSupport.GetValue(service.DeleteSkillGroup(ownerSession, renamedGroup.Id));
|
||||||
|
Assert.True(deletedGroup);
|
||||||
|
|
||||||
|
var afterGroupDelete = ServiceTestSupport.GetValue(service.GetCampaign(ownerSession, campaign.Id));
|
||||||
|
Assert.Empty(afterGroupDelete.SkillGroups);
|
||||||
|
Assert.Null(afterGroupDelete.Skills.Single(s => s.Id == regroupedSkill.Id).SkillGroupId);
|
||||||
|
|
||||||
|
var deletedSkill = ServiceTestSupport.GetValue(service.DeleteSkill(ownerSession, regroupedSkill.Id));
|
||||||
|
Assert.True(deletedSkill);
|
||||||
|
|
||||||
var ownerView = ServiceTestSupport.GetValue(service.GetCampaign(ownerSession, campaign.Id));
|
var ownerView = ServiceTestSupport.GetValue(service.GetCampaign(ownerSession, campaign.Id));
|
||||||
Assert.Single(ownerView.SkillGroups);
|
Assert.Empty(ownerView.SkillGroups);
|
||||||
Assert.Equal(ownerCharacter.Id, ownerView.SkillGroups[0].CharacterId);
|
Assert.Empty(ownerView.Skills);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -19,15 +19,27 @@ internal static class SkillEndpoints
|
|||||||
return ApiResultMapper.ToApiResult(result);
|
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) =>
|
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);
|
return ApiResultMapper.ToApiResult(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
group.MapPut("/skill-groups/{skillGroupId:guid}", (Guid skillGroupId, UpdateSkillGroupRequest request, HttpContext context, IGameService game) =>
|
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);
|
return ApiResultMapper.ToApiResult(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ public sealed class SkillFormModel
|
|||||||
public sealed class SkillGroupFormModel
|
public sealed class SkillGroupFormModel
|
||||||
{
|
{
|
||||||
public string Name { get; set; } = string.Empty;
|
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
|
public enum HomeViewMode
|
||||||
|
|||||||
@@ -45,14 +45,6 @@
|
|||||||
class="muted">| Owner: @OwnerLabel(SelectedCharacter.OwnerUserId) | Campaign: @SelectedCampaign.Name</span>
|
class="muted">| Owner: @OwnerLabel(SelectedCharacter.OwnerUserId) | Campaign: @SelectedCampaign.Name</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="chip-toolbar">
|
<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>
|
<label class="visibility-control" for="roll-visibility">Visibility</label>
|
||||||
<select id="roll-visibility" value="@(RollVisibility == "private" ? "private" : "public")" @onchange="OnRollVisibilityChangedAsync">
|
<select id="roll-visibility" value="@(RollVisibility == "private" ? "private" : "public")" @onchange="OnRollVisibilityChangedAsync">
|
||||||
<option value="public">Public</option>
|
<option value="public">Public</option>
|
||||||
@@ -74,15 +66,26 @@
|
|||||||
<div class="skill-group-block">
|
<div class="skill-group-block">
|
||||||
<div class="skill-group-head">
|
<div class="skill-group-head">
|
||||||
<strong>@group.Name</strong>
|
<strong>@group.Name</strong>
|
||||||
<button
|
<div class="skill-chip-actions">
|
||||||
type="button"
|
<button
|
||||||
class="chip-button"
|
type="button"
|
||||||
title="Rename skill group"
|
class="chip-button"
|
||||||
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
title="Edit skill group"
|
||||||
@onclick="() => OpenEditSkillGroupModal(group)">
|
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
||||||
<span aria-hidden="true">✎</span>
|
@onclick="() => OpenEditSkillGroupModal(group)">
|
||||||
<span class="sr-only">Rename @group.Name</span>
|
<span aria-hidden="true">✎</span>
|
||||||
</button>
|
<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>
|
</div>
|
||||||
@if (groupSkills.Count == 0)
|
@if (groupSkills.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -115,61 +118,93 @@
|
|||||||
<span aria-hidden="true">⚄</span>
|
<span aria-hidden="true">⚄</span>
|
||||||
<span class="sr-only">Roll @skill.Name</span>
|
<span class="sr-only">Roll @skill.Name</span>
|
||||||
</button>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="chip-button"
|
class="chip-button"
|
||||||
title="Edit skill"
|
title="Delete skill"
|
||||||
disabled="@(IsMutating || !CanEditSkill(skill))"
|
disabled="@(IsMutating || !CanEditSkill(skill))"
|
||||||
@onclick="() => OpenEditSkillModal(skill)">
|
@onclick="() => DeleteSkillAsync(skill.Id)">
|
||||||
<span aria-hidden="true">✎</span>
|
<span aria-hidden="true">✕</span>
|
||||||
<span class="sr-only">Edit @skill.Name</span>
|
<span class="sr-only">Delete @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>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
}
|
}
|
||||||
<div class="skill-list">
|
<div class="skill-group-block">
|
||||||
<button
|
<div class="skill-group-head">
|
||||||
type="button"
|
<strong>Ungrouped</strong>
|
||||||
class="skill-item create-skill-item"
|
</div>
|
||||||
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
@if (ungroupedSkills.Count == 0)
|
||||||
@onclick="OpenCreateSkillModal">
|
{
|
||||||
<span class="skill-create-icon" aria-hidden="true">+</span>
|
<p class="empty">No ungrouped skills.</p>
|
||||||
<span>Add skill</span>
|
}
|
||||||
</button>
|
<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>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="add-row-button"
|
||||||
|
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
||||||
|
@onclick="OpenCreateSkillGroupModal">[+] Add group
|
||||||
|
</button>
|
||||||
</article>
|
</article>
|
||||||
}
|
}
|
||||||
<div class="character-panel-fill" aria-hidden="true"></div>
|
<div class="character-panel-fill" aria-hidden="true"></div>
|
||||||
@@ -180,7 +215,7 @@
|
|||||||
{
|
{
|
||||||
<div class="modal-overlay" role="presentation">
|
<div class="modal-overlay" role="presentation">
|
||||||
<section class="modal-card" role="dialog" aria-modal="true" aria-label="Skill Group">
|
<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))
|
@if (!string.IsNullOrWhiteSpace(SkillGroupState.ErrorMessage))
|
||||||
{
|
{
|
||||||
<p class="form-error">@SkillGroupState.ErrorMessage</p>
|
<p class="form-error">@SkillGroupState.ErrorMessage</p>
|
||||||
@@ -192,10 +227,36 @@
|
|||||||
{
|
{
|
||||||
<p class="field-error">@groupNameError</p>
|
<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))
|
@if (SkillGroupState.Errors.TryGetValue("character", out var characterError))
|
||||||
{
|
{
|
||||||
<p class="field-error">@characterError</p>
|
<p class="field-error">@characterError</p>
|
||||||
}
|
}
|
||||||
|
@if (SkillGroupState.Errors.TryGetValue("group", out var groupError))
|
||||||
|
{
|
||||||
|
<p class="field-error">@groupError</p>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="inline-actions">
|
<div class="inline-actions">
|
||||||
<button type="submit" disabled="@(IsMutating || IsSubmittingSkillGroup)">@(ShowEditSkillGroupModal ? "Save Group" : "Create Group")</button>
|
<button type="submit" disabled="@(IsMutating || IsSubmittingSkillGroup)">@(ShowEditSkillGroupModal ? "Save Group" : "Create Group")</button>
|
||||||
<button type="button" class="ghost" disabled="@(IsMutating || IsSubmittingSkillGroup)" @onclick="CloseSkillGroupModals">Cancel</button>
|
<button type="button" class="ghost" disabled="@(IsMutating || IsSubmittingSkillGroup)" @onclick="CloseSkillGroupModals">Cancel</button>
|
||||||
|
|||||||
@@ -7,15 +7,19 @@ namespace RpgRoller.Components.Pages.HomeControls;
|
|||||||
[ExcludeFromCodeCoverage]
|
[ExcludeFromCodeCoverage]
|
||||||
public partial class CharacterPanel
|
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()
|
CreateSkillInitialModel = new()
|
||||||
{
|
{
|
||||||
Name = string.Empty,
|
Name = string.Empty,
|
||||||
DiceRollDefinition = string.Empty,
|
DiceRollDefinition = selectedGroup?.DiceRollDefinition ?? string.Empty,
|
||||||
SkillGroupId = string.Empty,
|
SkillGroupId = selectedGroup?.Id.ToString() ?? string.Empty,
|
||||||
WildDice = IsD6 ? 1 : 0,
|
WildDice = selectedGroup?.WildDice ?? (IsD6 ? 1 : 0),
|
||||||
AllowFumble = IsD6
|
AllowFumble = selectedGroup?.AllowFumble ?? IsD6
|
||||||
};
|
};
|
||||||
|
|
||||||
CreateSkillFormVersion++;
|
CreateSkillFormVersion++;
|
||||||
@@ -71,6 +75,9 @@ public partial class CharacterPanel
|
|||||||
private void OpenCreateSkillGroupModal()
|
private void OpenCreateSkillGroupModal()
|
||||||
{
|
{
|
||||||
SkillGroupState.Model.Name = string.Empty;
|
SkillGroupState.Model.Name = string.Empty;
|
||||||
|
SkillGroupState.Model.DiceRollDefinition = string.Empty;
|
||||||
|
SkillGroupState.Model.WildDice = IsD6 ? 1 : 0;
|
||||||
|
SkillGroupState.Model.AllowFumble = IsD6;
|
||||||
SkillGroupState.ResetValidation();
|
SkillGroupState.ResetValidation();
|
||||||
ShowCreateSkillGroupModal = true;
|
ShowCreateSkillGroupModal = true;
|
||||||
}
|
}
|
||||||
@@ -79,6 +86,9 @@ public partial class CharacterPanel
|
|||||||
{
|
{
|
||||||
EditingSkillGroupId = skillGroup.Id;
|
EditingSkillGroupId = skillGroup.Id;
|
||||||
SkillGroupState.Model.Name = skillGroup.Name;
|
SkillGroupState.Model.Name = skillGroup.Name;
|
||||||
|
SkillGroupState.Model.DiceRollDefinition = skillGroup.DiceRollDefinition;
|
||||||
|
SkillGroupState.Model.WildDice = skillGroup.WildDice;
|
||||||
|
SkillGroupState.Model.AllowFumble = skillGroup.AllowFumble;
|
||||||
SkillGroupState.ResetValidation();
|
SkillGroupState.ResetValidation();
|
||||||
ShowEditSkillGroupModal = true;
|
ShowEditSkillGroupModal = true;
|
||||||
}
|
}
|
||||||
@@ -98,6 +108,12 @@ public partial class CharacterPanel
|
|||||||
if (string.IsNullOrWhiteSpace(SkillGroupState.Model.Name))
|
if (string.IsNullOrWhiteSpace(SkillGroupState.Model.Name))
|
||||||
SkillGroupState.Errors["name"] = "Skill group name is required.";
|
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)
|
if (!SelectedCharacterId.HasValue)
|
||||||
SkillGroupState.Errors["character"] = "Select a character first.";
|
SkillGroupState.Errors["character"] = "Select a character first.";
|
||||||
|
|
||||||
@@ -111,7 +127,14 @@ public partial class CharacterPanel
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var selectedCharacterId = SelectedCharacterId!.Value;
|
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();
|
CloseSkillGroupModals();
|
||||||
await SkillGroupCreated.InvokeAsync(createdGroup.Id);
|
await SkillGroupCreated.InvokeAsync(createdGroup.Id);
|
||||||
}
|
}
|
||||||
@@ -132,6 +155,12 @@ public partial class CharacterPanel
|
|||||||
if (string.IsNullOrWhiteSpace(SkillGroupState.Model.Name))
|
if (string.IsNullOrWhiteSpace(SkillGroupState.Model.Name))
|
||||||
SkillGroupState.Errors["name"] = "Skill group name is required.";
|
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)
|
if (!EditingSkillGroupId.HasValue)
|
||||||
SkillGroupState.Errors["group"] = "Select a skill group first.";
|
SkillGroupState.Errors["group"] = "Select a skill group first.";
|
||||||
|
|
||||||
@@ -145,7 +174,14 @@ public partial class CharacterPanel
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var editingSkillGroupId = EditingSkillGroupId!.Value;
|
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();
|
CloseSkillGroupModals();
|
||||||
await SkillGroupUpdated.InvokeAsync(updatedGroup.Id);
|
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)
|
private static string InitialsFor(string value)
|
||||||
{
|
{
|
||||||
var words = value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
var words = value.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
@@ -250,6 +312,15 @@ public partial class CharacterPanel
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback<Guid> SkillGroupUpdated { get; set; }
|
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]
|
[Parameter]
|
||||||
public EventCallback<Guid> RollRequested { get; set; }
|
public EventCallback<Guid> RollRequested { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,9 @@
|
|||||||
SkillUpdated="OnSkillUpdatedAsync"
|
SkillUpdated="OnSkillUpdatedAsync"
|
||||||
SkillGroupCreated="OnSkillGroupCreatedAsync"
|
SkillGroupCreated="OnSkillGroupCreatedAsync"
|
||||||
SkillGroupUpdated="OnSkillGroupUpdatedAsync"
|
SkillGroupUpdated="OnSkillGroupUpdatedAsync"
|
||||||
|
SkillDeleted="OnSkillDeletedAsync"
|
||||||
|
SkillGroupDeleted="OnSkillGroupDeletedAsync"
|
||||||
|
ErrorOccurred="OnCharacterPanelErrorAsync"
|
||||||
RollRequested="RollSkillAsync"/>
|
RollRequested="RollSkillAsync"/>
|
||||||
|
|
||||||
<CampaignLogPanel
|
<CampaignLogPanel
|
||||||
|
|||||||
@@ -392,6 +392,24 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
SetStatus("Skill group updated.", false);
|
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)
|
private async Task RollSkillAsync(Guid skillId)
|
||||||
{
|
{
|
||||||
if (SelectedCampaign is null)
|
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 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);
|
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.HasKey(x => x.Id);
|
||||||
entity.Property(x => x.Name).IsRequired().HasMaxLength(128);
|
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);
|
entity.HasIndex(x => x.CharacterId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ public sealed class SkillGroup
|
|||||||
public required Guid Id { get; init; }
|
public required Guid Id { get; init; }
|
||||||
public required Guid CharacterId { get; set; }
|
public required Guid CharacterId { get; set; }
|
||||||
public required string Name { 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
|
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()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("AllowFumble")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<Guid>("CharacterId")
|
b.Property<Guid>("CharacterId")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DiceRollDefinition")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(128)
|
.HasMaxLength(128)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("WildDice")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("CharacterId");
|
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))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
return ServiceResult<SkillGroupSummary>.Failure("invalid_skill_group_name", "Skill group name is required.");
|
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))
|
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||||
return ServiceResult<SkillGroupSummary>.Failure("forbidden", "Only the owner or GM can manage skill groups.");
|
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
|
var group = new SkillGroup
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
CharacterId = character.Id,
|
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;
|
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))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
return ServiceResult<SkillGroupSummary>.Failure("invalid_skill_group_name", "Skill group name is required.");
|
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))
|
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||||
return ServiceResult<SkillGroupSummary>.Failure("forbidden", "Only the owner or GM can manage skill groups.");
|
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.Name = name.Trim();
|
||||||
|
group.DiceRollDefinition = prototypeValidation.Value!.CanonicalExpression;
|
||||||
|
group.WildDice = prototypeValidation.Value.WildDice;
|
||||||
|
group.AllowFumble = prototypeValidation.Value.AllowFumble;
|
||||||
TouchCampaignLocked(campaign.Id);
|
TouchCampaignLocked(campaign.Id);
|
||||||
|
|
||||||
PersistStateLocked();
|
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)
|
public ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
@@ -400,13 +441,9 @@ public sealed class GameService : IGameService
|
|||||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||||
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
||||||
|
|
||||||
var expressionValidation = DiceRules.ParseExpression(campaign.Ruleset, diceRollDefinition);
|
var skillValidation = ValidateSkillDefinition(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble);
|
||||||
if (!expressionValidation.Succeeded)
|
if (!skillValidation.Succeeded)
|
||||||
return ServiceResult<SkillSummary>.Failure(expressionValidation.Error!.Code, expressionValidation.Error.Message);
|
return ServiceResult<SkillSummary>.Failure(skillValidation.Error!.Code, skillValidation.Error.Message);
|
||||||
|
|
||||||
var optionsValidation = ValidateSkillOptions(campaign.Ruleset, wildDice, allowFumble);
|
|
||||||
if (!optionsValidation.Succeeded)
|
|
||||||
return ServiceResult<SkillSummary>.Failure(optionsValidation.Error!.Code, optionsValidation.Error.Message);
|
|
||||||
|
|
||||||
var resolvedSkillGroupId = ResolveSkillGroupForSkillChangeLocked(skillGroupId, character.Id);
|
var resolvedSkillGroupId = ResolveSkillGroupForSkillChangeLocked(skillGroupId, character.Id);
|
||||||
if (!resolvedSkillGroupId.Succeeded)
|
if (!resolvedSkillGroupId.Succeeded)
|
||||||
@@ -418,9 +455,9 @@ public sealed class GameService : IGameService
|
|||||||
CharacterId = character.Id,
|
CharacterId = character.Id,
|
||||||
SkillGroupId = resolvedSkillGroupId.Value,
|
SkillGroupId = resolvedSkillGroupId.Value,
|
||||||
Name = name.Trim(),
|
Name = name.Trim(),
|
||||||
DiceRollDefinition = expressionValidation.Value!.Canonical,
|
DiceRollDefinition = skillValidation.Value!.CanonicalExpression,
|
||||||
WildDice = optionsValidation.Value!.WildDice,
|
WildDice = skillValidation.Value.WildDice,
|
||||||
AllowFumble = optionsValidation.Value.AllowFumble
|
AllowFumble = skillValidation.Value.AllowFumble
|
||||||
};
|
};
|
||||||
|
|
||||||
m_SkillsById[skill.Id] = skill;
|
m_SkillsById[skill.Id] = skill;
|
||||||
@@ -450,22 +487,18 @@ public sealed class GameService : IGameService
|
|||||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||||
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
||||||
|
|
||||||
var expressionValidation = DiceRules.ParseExpression(campaign.Ruleset, diceRollDefinition);
|
var skillValidation = ValidateSkillDefinition(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble);
|
||||||
if (!expressionValidation.Succeeded)
|
if (!skillValidation.Succeeded)
|
||||||
return ServiceResult<SkillSummary>.Failure(expressionValidation.Error!.Code, expressionValidation.Error.Message);
|
return ServiceResult<SkillSummary>.Failure(skillValidation.Error!.Code, skillValidation.Error.Message);
|
||||||
|
|
||||||
var optionsValidation = ValidateSkillOptions(campaign.Ruleset, wildDice, allowFumble);
|
|
||||||
if (!optionsValidation.Succeeded)
|
|
||||||
return ServiceResult<SkillSummary>.Failure(optionsValidation.Error!.Code, optionsValidation.Error.Message);
|
|
||||||
|
|
||||||
var resolvedSkillGroupId = ResolveSkillGroupForSkillChangeLocked(skillGroupId, character.Id);
|
var resolvedSkillGroupId = ResolveSkillGroupForSkillChangeLocked(skillGroupId, character.Id);
|
||||||
if (!resolvedSkillGroupId.Succeeded)
|
if (!resolvedSkillGroupId.Succeeded)
|
||||||
return ServiceResult<SkillSummary>.Failure(resolvedSkillGroupId.Error!.Code, resolvedSkillGroupId.Error.Message);
|
return ServiceResult<SkillSummary>.Failure(resolvedSkillGroupId.Error!.Code, resolvedSkillGroupId.Error.Message);
|
||||||
|
|
||||||
skill.Name = name.Trim();
|
skill.Name = name.Trim();
|
||||||
skill.DiceRollDefinition = expressionValidation.Value!.Canonical;
|
skill.DiceRollDefinition = skillValidation.Value!.CanonicalExpression;
|
||||||
skill.WildDice = optionsValidation.Value!.WildDice;
|
skill.WildDice = skillValidation.Value.WildDice;
|
||||||
skill.AllowFumble = optionsValidation.Value.AllowFumble;
|
skill.AllowFumble = skillValidation.Value.AllowFumble;
|
||||||
skill.SkillGroupId = resolvedSkillGroupId.Value;
|
skill.SkillGroupId = resolvedSkillGroupId.Value;
|
||||||
TouchCampaignLocked(campaign.Id);
|
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)
|
public ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility)
|
||||||
{
|
{
|
||||||
lock (m_Gate)
|
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)
|
private static ServiceResult<(int WildDice, bool AllowFumble)> ValidateSkillOptions(RulesetKind ruleset, int wildDice, bool allowFumble)
|
||||||
{
|
{
|
||||||
if (wildDice < 0 || wildDice > 50)
|
if (wildDice < 0 || wildDice > 50)
|
||||||
@@ -732,7 +802,7 @@ public sealed class GameService : IGameService
|
|||||||
|
|
||||||
private static SkillGroupSummary ToSkillGroupSummary(SkillGroup skillGroup)
|
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)
|
private static SkillSummary ToSkillSummary(Skill skill)
|
||||||
@@ -992,7 +1062,10 @@ public sealed class GameService : IGameService
|
|||||||
{
|
{
|
||||||
Id = skillGroup.Id,
|
Id = skillGroup.Id,
|
||||||
CharacterId = skillGroup.CharacterId,
|
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<bool> ActivateCharacter(string sessionToken, Guid characterId);
|
||||||
ServiceResult<IReadOnlyList<CharacterSummary>> GetCurrentCampaignCharacters(string sessionToken);
|
ServiceResult<IReadOnlyList<CharacterSummary>> GetCurrentCampaignCharacters(string sessionToken);
|
||||||
|
|
||||||
ServiceResult<SkillGroupSummary> CreateSkillGroup(string sessionToken, Guid characterId, 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);
|
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> 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<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<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility);
|
||||||
ServiceResult<IReadOnlyList<CampaignLogEntry>> GetCampaignLog(string sessionToken, Guid campaignId);
|
ServiceResult<IReadOnlyList<CampaignLogEntry>> GetCampaignLog(string sessionToken, Guid campaignId);
|
||||||
|
|||||||
Reference in New Issue
Block a user