Cleanup campaign management ux
This commit is contained in:
@@ -15,7 +15,7 @@ public sealed class CampaignApiTests : ApiTestBase
|
|||||||
await RegisterAsync(gmClient, "gm", "Password123", "Game Master");
|
await RegisterAsync(gmClient, "gm", "Password123", "Game Master");
|
||||||
await LoginAsync(gmClient, "gm", "Password123");
|
await LoginAsync(gmClient, "gm", "Password123");
|
||||||
|
|
||||||
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(gmClient, "/api/campaigns", new("Alpha Campaign", "dnd5e"));
|
var campaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(gmClient, "/api/campaigns", new("Alpha Campaign", "dnd5e"));
|
||||||
|
|
||||||
var gmCharacter = await PostAsync<CreateCharacterRequest, CharacterSummary>(gmClient, "/api/characters", new("Arin", campaign.Id));
|
var gmCharacter = await PostAsync<CreateCharacterRequest, CharacterSummary>(gmClient, "/api/characters", new("Arin", campaign.Id));
|
||||||
|
|
||||||
@@ -38,14 +38,13 @@ public sealed class CampaignApiTests : ApiTestBase
|
|||||||
|
|
||||||
var details = await GetAsync<CampaignDetails>(gmClient, $"/api/campaigns/{campaign.Id}");
|
var details = await GetAsync<CampaignDetails>(gmClient, $"/api/campaigns/{campaign.Id}");
|
||||||
Assert.Equal(campaign.Id, details.Id);
|
Assert.Equal(campaign.Id, details.Id);
|
||||||
Assert.Single(details.Characters);
|
Assert.Equal(1, details.Characters);
|
||||||
Assert.Single(details.Skills);
|
|
||||||
|
|
||||||
var currentCampaignCharacters = await GetAsync<IReadOnlyList<CharacterSummary>>(gmClient, "/api/characters/current-campaign");
|
var currentCampaignCharacters = await GetAsync<IReadOnlyList<CharacterSummary>>(gmClient, "/api/characters/current-campaign");
|
||||||
Assert.Single(currentCampaignCharacters);
|
Assert.Single(currentCampaignCharacters);
|
||||||
Assert.Equal(gmCharacter.Id, currentCampaignCharacters[0].Id);
|
Assert.Equal(gmCharacter.Id, currentCampaignCharacters[0].Id);
|
||||||
|
|
||||||
var otherCampaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(gmClient, "/api/campaigns", new("Beta Campaign", "d6"));
|
var otherCampaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(gmClient, "/api/campaigns", new("Beta Campaign", "d6"));
|
||||||
|
|
||||||
var updatedCharacter = await PutAsync<UpdateCharacterRequest, CharacterSummary>(gmClient, $"/api/characters/{gmCharacter.Id}", new("Arin Updated", otherCampaign.Id));
|
var updatedCharacter = await PutAsync<UpdateCharacterRequest, CharacterSummary>(gmClient, $"/api/characters/{gmCharacter.Id}", new("Arin Updated", otherCampaign.Id));
|
||||||
|
|
||||||
@@ -70,7 +69,7 @@ public sealed class CampaignApiTests : ApiTestBase
|
|||||||
await RegisterAsync(receiverClient, "receiver2", "Password123", "Receiver");
|
await RegisterAsync(receiverClient, "receiver2", "Password123", "Receiver");
|
||||||
await LoginAsync(receiverClient, "receiver2", "Password123");
|
await LoginAsync(receiverClient, "receiver2", "Password123");
|
||||||
|
|
||||||
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(gmClient, "/api/campaigns", new("Grouped Campaign", "d6"));
|
var campaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(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", "2D+1", 1, true));
|
var createdGroup = await PostAsync<CreateSkillGroupRequest, SkillGroupSummary>(ownerClient, $"/api/characters/{character.Id}/skill-groups", new("Combat", "2D+1", 1, true));
|
||||||
@@ -103,8 +102,5 @@ public sealed class CampaignApiTests : ApiTestBase
|
|||||||
|
|
||||||
var receiverActivate = await receiverClient.PostAsync($"/api/characters/{character.Id}/activate", null);
|
var receiverActivate = await receiverClient.PostAsync($"/api/characters/{character.Id}/activate", null);
|
||||||
Assert.Equal(HttpStatusCode.OK, receiverActivate.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, receiverActivate.StatusCode);
|
||||||
|
|
||||||
var details = await GetAsync<CampaignDetails>(gmClient, $"/api/campaigns/{campaign.Id}");
|
|
||||||
Assert.DoesNotContain(details.SkillGroups, group => group.Id == renamedGroup.Id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ public sealed class RollVisibilityApiTests : ApiTestBase
|
|||||||
|
|
||||||
await RegisterAsync(gmClient, "gm", "Password123", "GM");
|
await RegisterAsync(gmClient, "gm", "Password123", "GM");
|
||||||
await LoginAsync(gmClient, "gm", "Password123");
|
await LoginAsync(gmClient, "gm", "Password123");
|
||||||
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(gmClient, "/api/campaigns", new("Main", "d6"));
|
var campaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(gmClient, "/api/campaigns", new("Main", "d6"));
|
||||||
|
|
||||||
await RegisterAsync(playerClient, "player", "Password123", "Player");
|
await RegisterAsync(playerClient, "player", "Password123", "Player");
|
||||||
await LoginAsync(playerClient, "player", "Password123");
|
await LoginAsync(playerClient, "player", "Password123");
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public sealed class SystemApiTests : ApiTestBase
|
|||||||
await RegisterAsync(client, "sse", "Password123", "Sse User");
|
await RegisterAsync(client, "sse", "Password123", "Sse User");
|
||||||
await LoginAsync(client, "sse", "Password123");
|
await LoginAsync(client, "sse", "Password123");
|
||||||
|
|
||||||
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(client, "/api/campaigns", new("SSE", "d6"));
|
var campaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(client, "/api/campaigns", new("SSE", "d6"));
|
||||||
|
|
||||||
var sseResponse = await client.GetAsync($"/api/events/state?campaignId={campaign.Id}", HttpCompletionOption.ResponseHeadersRead);
|
var sseResponse = await client.GetAsync($"/api/events/state?campaignId={campaign.Id}", HttpCompletionOption.ResponseHeadersRead);
|
||||||
Assert.Equal(HttpStatusCode.OK, sseResponse.StatusCode);
|
Assert.Equal(HttpStatusCode.OK, sseResponse.StatusCode);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public sealed class ServiceCampaignTests
|
|||||||
var activateSuccess = service.ActivateCharacter(gmSession, character.Id);
|
var activateSuccess = service.ActivateCharacter(gmSession, character.Id);
|
||||||
Assert.True(activateSuccess.Succeeded);
|
Assert.True(activateSuccess.Succeeded);
|
||||||
|
|
||||||
var currentCharacters = service.GetCurrentCampaignCharacters(gmSession);
|
var currentCharacters = service.GetOwnCharacters(gmSession);
|
||||||
Assert.True(currentCharacters.Succeeded);
|
Assert.True(currentCharacters.Succeeded);
|
||||||
Assert.Single(ServiceTestSupport.GetValue(currentCharacters));
|
Assert.Single(ServiceTestSupport.GetValue(currentCharacters));
|
||||||
|
|
||||||
@@ -45,7 +45,7 @@ public sealed class ServiceCampaignTests
|
|||||||
service.Register("user", "Password123", "User");
|
service.Register("user", "Password123", "User");
|
||||||
var sessionToken = ServiceTestSupport.GetValue(service.Login("user", "Password123")).SessionToken;
|
var sessionToken = ServiceTestSupport.GetValue(service.Login("user", "Password123")).SessionToken;
|
||||||
|
|
||||||
var result = service.GetCurrentCampaignCharacters(sessionToken);
|
var result = service.GetOwnCharacters(sessionToken);
|
||||||
Assert.False(result.Succeeded);
|
Assert.False(result.Succeeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public sealed class ServicePersistenceTests
|
|||||||
Assert.False(service.UpdateCharacter(ownerSession, Guid.NewGuid(), "Renamed", campaign.Id).Succeeded);
|
Assert.False(service.UpdateCharacter(ownerSession, Guid.NewGuid(), "Renamed", campaign.Id).Succeeded);
|
||||||
Assert.False(service.ActivateCharacter(string.Empty, ownerCharacter.Id).Succeeded);
|
Assert.False(service.ActivateCharacter(string.Empty, ownerCharacter.Id).Succeeded);
|
||||||
Assert.False(service.ActivateCharacter(gmSession, ownerCharacter.Id).Succeeded);
|
Assert.False(service.ActivateCharacter(gmSession, ownerCharacter.Id).Succeeded);
|
||||||
Assert.False(service.GetCurrentCampaignCharacters(string.Empty).Succeeded);
|
Assert.False(service.GetOwnCharacters(string.Empty).Succeeded);
|
||||||
Assert.False(service.CreateSkill(string.Empty, ownerCharacter.Id, "Stealth", "2D+1", 1, true).Succeeded);
|
Assert.False(service.CreateSkill(string.Empty, ownerCharacter.Id, "Stealth", "2D+1", 1, true).Succeeded);
|
||||||
Assert.False(service.CreateSkill(ownerSession, Guid.NewGuid(), "Stealth", "2D+1", 1, true).Succeeded);
|
Assert.False(service.CreateSkill(ownerSession, Guid.NewGuid(), "Stealth", "2D+1", 1, true).Succeeded);
|
||||||
Assert.False(service.CreateSkill(otherSession, ownerCharacter.Id, "Stealth", "2D+1", 1, true).Succeeded);
|
Assert.False(service.CreateSkill(otherSession, ownerCharacter.Id, "Stealth", "2D+1", 1, true).Succeeded);
|
||||||
@@ -67,7 +67,7 @@ public sealed class ServicePersistenceTests
|
|||||||
using var staleCurrentHarness = ServiceTestSupport.CreateHarnessFromPath(harness.DbPath, 2, 3, 4);
|
using var staleCurrentHarness = ServiceTestSupport.CreateHarnessFromPath(harness.DbPath, 2, 3, 4);
|
||||||
var staleCurrentService = staleCurrentHarness.Service;
|
var staleCurrentService = staleCurrentHarness.Service;
|
||||||
|
|
||||||
var staleCurrentCampaign = staleCurrentService.GetCurrentCampaignCharacters(ownerSession);
|
var staleCurrentCampaign = staleCurrentService.GetOwnCharacters(ownerSession);
|
||||||
Assert.False(staleCurrentCampaign.Succeeded);
|
Assert.False(staleCurrentCampaign.Succeeded);
|
||||||
using (var db = harness.CreateDbContext())
|
using (var db = harness.CreateDbContext())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ internal static class CharacterEndpoints
|
|||||||
return ApiResultMapper.ToApiResult(result);
|
return ApiResultMapper.ToApiResult(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
group.MapGet("/characters/current-campaign", (HttpContext context, IGameService game) =>
|
group.MapGet("/characters", (HttpContext context, IGameService game) =>
|
||||||
{
|
{
|
||||||
var result = game.GetCurrentCampaignCharacters(context.GetRequiredSessionToken());
|
var result = game.GetOwnCharacters(context.GetRequiredSessionToken());
|
||||||
return ApiResultMapper.ToApiResult(result);
|
return ApiResultMapper.ToApiResult(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,27 +9,15 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<label for="campaign-select">Campaign</label>
|
<label for="campaign-select">Current campaign</label>
|
||||||
<select id="campaign-select" @onchange="CampaignSelectionChanged">
|
<select id="campaign-select" @onchange="CampaignSelectionChanged">
|
||||||
@foreach (var campaign in Campaigns)
|
@foreach (var campaign in Campaigns)
|
||||||
{
|
{
|
||||||
<option value="@campaign.Id" selected="@(campaign.Id == SelectedCampaignId)">@campaign.Name (@campaign.RulesetId)</option>
|
<option value="@campaign.Id" selected="@(campaign.Id == SelectedCampaignId)">@campaign.Name (@campaign.RulesetId), GM: @campaign.Gm.DisplayName, @campaign.Characters.Count characters</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (SelectedCampaign is null)
|
|
||||||
{
|
|
||||||
<p class="empty">No campaign selected.</p>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<p>Name: <strong>@SelectedCampaign.Name</strong></p>
|
|
||||||
<p>Ruleset: <strong>@SelectedCampaign.RulesetId</strong></p>
|
|
||||||
<p>GM: <strong>@SelectedCampaign.Gm.DisplayName</strong> <span class="muted">(@SelectedCampaign.Gm.Username)</span></p>
|
|
||||||
<p>Characters visible: <strong>@SelectedCampaign.Characters.Count</strong></p>
|
|
||||||
}
|
|
||||||
|
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="add-row-button"
|
class="add-row-button"
|
||||||
disabled="@(IsMutating || IsCreatingCampaign)"
|
disabled="@(IsMutating || IsCreatingCampaign)"
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ public partial class CampaignManagementPanel
|
|||||||
IsCreatingCampaign = true;
|
IsCreatingCampaign = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var campaign = await ApiClient.RequestAsync<CampaignSummary>("POST", "/api/campaigns", new CreateCampaignRequest(CampaignState.Model.Name.Trim(), CampaignState.Model.RulesetId));
|
var campaign = await ApiClient.RequestAsync<CampaignDetails>("POST", "/api/campaigns", new CreateCampaignRequest(CampaignState.Model.Name.Trim(), CampaignState.Model.RulesetId));
|
||||||
|
|
||||||
CampaignState.Model.Name = string.Empty;
|
CampaignState.Model.Name = string.Empty;
|
||||||
ShowCreateCampaignModal = false;
|
ShowCreateCampaignModal = false;
|
||||||
@@ -72,7 +72,7 @@ public partial class CampaignManagementPanel
|
|||||||
private bool ShowCreateCampaignModal { get; set; }
|
private bool ShowCreateCampaignModal { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public IReadOnlyList<CampaignSummary> Campaigns { get; set; } = [];
|
public IReadOnlyList<CampaignDetails> Campaigns { get; set; } = [];
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Guid? SelectedCampaignId { get; set; }
|
public Guid? SelectedCampaignId { get; set; }
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ public partial class CharacterFormModal
|
|||||||
public Guid? EditingCharacterId { get; set; }
|
public Guid? EditingCharacterId { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public IReadOnlyList<CampaignSummary> Campaigns { get; set; } = [];
|
public IReadOnlyList<CampaignDetails> Campaigns { get; set; } = [];
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool IsMutating { get; set; }
|
public bool IsMutating { get; set; }
|
||||||
|
|||||||
@@ -113,7 +113,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="chip-button"
|
class="chip-button"
|
||||||
title="Roll skill"
|
title="Roll skill"
|
||||||
disabled="@(IsMutating || !CanRollSkill(skill))"
|
disabled="@(IsMutating)"
|
||||||
@onclick="() => RollSkillAsync(skill.Id)">
|
@onclick="() => RollSkillAsync(skill.Id)">
|
||||||
<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>
|
||||||
@@ -171,7 +171,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="chip-button"
|
class="chip-button"
|
||||||
title="Roll skill"
|
title="Roll skill"
|
||||||
disabled="@(IsMutating || !CanRollSkill(skill))"
|
disabled="@(IsMutating)"
|
||||||
@onclick="() => RollSkillAsync(skill.Id)">
|
@onclick="() => RollSkillAsync(skill.Id)">
|
||||||
<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>
|
||||||
@@ -203,7 +203,9 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="add-row-button"
|
class="add-row-button"
|
||||||
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
disabled="@(IsMutating || !CanEditCharacter(SelectedCharacter))"
|
||||||
@onclick="OpenCreateSkillGroupModal">[+] Add group
|
@onclick="OpenCreateSkillGroupModal">
|
||||||
|
<span class="skill-create-icon" aria-hidden="true">+</span>
|
||||||
|
<span>Add group</span>
|
||||||
</button>
|
</button>
|
||||||
</article>
|
</article>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,9 +291,6 @@ public partial class CharacterPanel
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public Func<SkillSummary, bool> CanEditSkill { get; set; } = _ => false;
|
public Func<SkillSummary, bool> CanEditSkill { get; set; } = _ => false;
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public Func<SkillSummary, bool> CanRollSkill { get; set; } = _ => false;
|
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback<Guid> CharacterSelected { get; set; }
|
public EventCallback<Guid> CharacterSelected { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,6 @@
|
|||||||
SkillDefinitionLabel="SkillDefinitionLabel"
|
SkillDefinitionLabel="SkillDefinitionLabel"
|
||||||
CanEditCharacter="CanEditCharacter"
|
CanEditCharacter="CanEditCharacter"
|
||||||
CanEditSkill="CanEditSkill"
|
CanEditSkill="CanEditSkill"
|
||||||
CanRollSkill="CanRollSkill"
|
|
||||||
CharacterSelected="SelectCharacterAsync"
|
CharacterSelected="SelectCharacterAsync"
|
||||||
EditCharacterRequested="OpenEditCharacterModal"
|
EditCharacterRequested="OpenEditCharacterModal"
|
||||||
SkillCreated="OnSkillCreatedAsync"
|
SkillCreated="OnSkillCreatedAsync"
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
|
|
||||||
private async Task ReloadCampaignsAsync(Guid? preferredCampaignId)
|
private async Task ReloadCampaignsAsync(Guid? preferredCampaignId)
|
||||||
{
|
{
|
||||||
var campaigns = await ApiClient.RequestAsync<IReadOnlyList<CampaignSummary>>("GET", "/api/campaigns");
|
var campaigns = await ApiClient.RequestAsync<IReadOnlyList<CampaignDetails>>("GET", "/api/campaigns");
|
||||||
Campaigns = campaigns.OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
Campaigns = campaigns.OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
|
||||||
if (Campaigns.Count == 0)
|
if (Campaigns.Count == 0)
|
||||||
@@ -191,30 +191,6 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ManualRefreshAsync()
|
|
||||||
{
|
|
||||||
if (IsMutating)
|
|
||||||
return;
|
|
||||||
|
|
||||||
IsMutating = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await CheckHealthAsync();
|
|
||||||
var reloaded = await ReloadAuthenticatedSessionAsync(SelectedCampaignId);
|
|
||||||
if (!reloaded)
|
|
||||||
{
|
|
||||||
await LoggedOut.InvokeAsync("Session expired. Please log in again.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SetStatus("Campaign data refreshed.", false);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
IsMutating = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task LogoutAsync()
|
private async Task LogoutAsync()
|
||||||
{
|
{
|
||||||
if (IsMutating)
|
if (IsMutating)
|
||||||
@@ -435,23 +411,10 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectedSkill = SelectedCampaign.Skills.FirstOrDefault(skill => skill.Id == skillId);
|
|
||||||
if (selectedSkill is null)
|
|
||||||
{
|
|
||||||
SetStatus("Skill is no longer available. Refresh campaign data.", true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!CanRollSkill(selectedSkill))
|
|
||||||
{
|
|
||||||
SetStatus("You are not allowed to roll this skill.", true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
IsMutating = true;
|
IsMutating = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
LastRoll = await ApiClient.RequestAsync<RollResult>("POST", $"/api/skills/{selectedSkill.Id}/roll", new RollSkillRequest(RollVisibility));
|
LastRoll = await ApiClient.RequestAsync<RollResult>("POST", $"/api/skills/{skillId}/roll", new RollSkillRequest(RollVisibility));
|
||||||
|
|
||||||
await RefreshCampaignScopeAsync();
|
await RefreshCampaignScopeAsync();
|
||||||
SetStatus("Roll recorded.", false);
|
SetStatus("Roll recorded.", false);
|
||||||
@@ -487,11 +450,6 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
return character is not null && CanEditCharacter(character);
|
return character is not null && CanEditCharacter(character);
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanRollSkill(SkillSummary skill)
|
|
||||||
{
|
|
||||||
return CanEditSkill(skill);
|
|
||||||
}
|
|
||||||
|
|
||||||
[JSInvokable]
|
[JSInvokable]
|
||||||
public async Task OnStateEventReceived(long _)
|
public async Task OnStateEventReceived(long _)
|
||||||
{
|
{
|
||||||
@@ -736,7 +694,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private Guid? ActiveCharacterId { get; set; }
|
private Guid? ActiveCharacterId { get; set; }
|
||||||
private Guid? SelectedCampaignId { get; set; }
|
private Guid? SelectedCampaignId { get; set; }
|
||||||
private CampaignDetails? SelectedCampaign { get; set; }
|
private CampaignDetails? SelectedCampaign { get; set; }
|
||||||
private List<CampaignSummary> Campaigns { get; set; } = [];
|
private List<CampaignDetails> Campaigns { get; set; } = [];
|
||||||
private List<CampaignLogEntry> CampaignLog { get; set; } = [];
|
private List<CampaignLogEntry> CampaignLog { get; set; } = [];
|
||||||
private List<RulesetDefinition> Rulesets { get; set; } = [];
|
private List<RulesetDefinition> Rulesets { get; set; } = [];
|
||||||
private Guid? SelectedCharacterId { get; set; }
|
private Guid? SelectedCharacterId { get; set; }
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ public sealed record RulesetDefinition(string Id, string Name, string DiceSyntax
|
|||||||
|
|
||||||
public sealed record CreateCampaignRequest(string Name, string RulesetId);
|
public sealed record CreateCampaignRequest(string Name, string RulesetId);
|
||||||
|
|
||||||
public sealed record CampaignSummary(Guid Id, string Name, string RulesetId, Guid GmUserId);
|
|
||||||
|
|
||||||
public sealed record CampaignDetails(Guid Id, string Name, string RulesetId, UserSummary Gm, IReadOnlyList<CharacterSummary> Characters, IReadOnlyList<SkillGroupSummary> SkillGroups, IReadOnlyList<SkillSummary> Skills);
|
public sealed record CampaignDetails(Guid Id, string Name, string RulesetId, UserSummary Gm, IReadOnlyList<CharacterSummary> Characters, IReadOnlyList<SkillGroupSummary> SkillGroups, IReadOnlyList<SkillSummary> Skills);
|
||||||
|
|
||||||
public sealed record CreateCharacterRequest(string Name, Guid CampaignId);
|
public sealed record CreateCharacterRequest(string Name, Guid CampaignId);
|
||||||
|
|||||||
@@ -127,20 +127,20 @@ public sealed class GameService : IGameService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<CampaignSummary> CreateCampaign(string sessionToken, string name, string rulesetId)
|
public ServiceResult<CampaignDetails> CreateCampaign(string sessionToken, string name, string rulesetId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
return ServiceResult<CampaignSummary>.Failure("invalid_campaign_name", "Campaign name is required.");
|
return ServiceResult<CampaignDetails>.Failure("invalid_campaign_name", "Campaign name is required.");
|
||||||
|
|
||||||
var ruleset = DiceRules.TryParseRulesetId(rulesetId);
|
var ruleset = DiceRules.TryParseRulesetId(rulesetId);
|
||||||
if (ruleset is null)
|
if (ruleset is null)
|
||||||
return ServiceResult<CampaignSummary>.Failure("invalid_ruleset", "Unknown ruleset.");
|
return ServiceResult<CampaignDetails>.Failure("invalid_ruleset", "Unknown ruleset.");
|
||||||
|
|
||||||
lock (m_Gate)
|
lock (m_Gate)
|
||||||
{
|
{
|
||||||
var user = ResolveUserLocked(sessionToken);
|
var user = ResolveUserLocked(sessionToken);
|
||||||
if (user is null)
|
if (user is null)
|
||||||
return ServiceResult<CampaignSummary>.Failure("unauthorized", "You must be logged in.");
|
return ServiceResult<CampaignDetails>.Failure("unauthorized", "You must be logged in.");
|
||||||
|
|
||||||
var campaign = new Campaign
|
var campaign = new Campaign
|
||||||
{
|
{
|
||||||
@@ -153,25 +153,25 @@ public sealed class GameService : IGameService
|
|||||||
|
|
||||||
m_CampaignsById[campaign.Id] = campaign;
|
m_CampaignsById[campaign.Id] = campaign;
|
||||||
PersistStateLocked();
|
PersistStateLocked();
|
||||||
return ServiceResult<CampaignSummary>.Success(ToCampaignSummary(campaign));
|
return ServiceResult<CampaignDetails>.Success(ToCampaignDetails(campaign));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<IReadOnlyList<CampaignSummary>> GetCampaigns(string sessionToken)
|
public ServiceResult<IReadOnlyList<CampaignDetails>> GetCampaigns(string sessionToken)
|
||||||
{
|
{
|
||||||
lock (m_Gate)
|
lock (m_Gate)
|
||||||
{
|
{
|
||||||
var user = ResolveUserLocked(sessionToken);
|
var user = ResolveUserLocked(sessionToken);
|
||||||
if (user is null)
|
if (user is null)
|
||||||
return ServiceResult<IReadOnlyList<CampaignSummary>>.Failure("unauthorized", "You must be logged in.");
|
return ServiceResult<IReadOnlyList<CampaignDetails>>.Failure("unauthorized", "You must be logged in.");
|
||||||
|
|
||||||
var campaignIds = new HashSet<Guid>(m_CampaignsById.Values.Where(c => c.GmUserId == user.Id).Select(c => c.Id));
|
var campaignIds = new HashSet<Guid>(m_CampaignsById.Values.Where(c => c.GmUserId == user.Id).Select(c => c.Id));
|
||||||
foreach (var character in m_CharactersById.Values.Where(c => c.OwnerUserId == user.Id))
|
foreach (var character in m_CharactersById.Values.Where(c => c.OwnerUserId == user.Id))
|
||||||
campaignIds.Add(character.CampaignId);
|
campaignIds.Add(character.CampaignId);
|
||||||
|
|
||||||
var results = campaignIds.Select(id => m_CampaignsById[id]).OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).Select(ToCampaignSummary).ToArray();
|
var results = campaignIds.Select(id => m_CampaignsById[id]).OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).Select(ToCampaignDetails).ToArray();
|
||||||
|
|
||||||
return ServiceResult<IReadOnlyList<CampaignSummary>>.Success(results);
|
return ServiceResult<IReadOnlyList<CampaignDetails>>.Success(results);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,11 +183,9 @@ public sealed class GameService : IGameService
|
|||||||
if (!context.Succeeded)
|
if (!context.Succeeded)
|
||||||
return ServiceResult<CampaignDetails>.Failure(context.Error!.Code, context.Error.Message);
|
return ServiceResult<CampaignDetails>.Failure(context.Error!.Code, context.Error.Message);
|
||||||
|
|
||||||
var (user, campaign) = context.Value!;
|
var (_, campaign) = context.Value;
|
||||||
var gm = m_UsersById[campaign.GmUserId];
|
var gm = m_UsersById[campaign.GmUserId];
|
||||||
var isGm = campaign.GmUserId == user.Id;
|
var characters = m_CharactersById.Values.Where(c => c.CampaignId == campaign.Id).Select(ToCharacterSummary).ToList();
|
||||||
|
|
||||||
var characters = m_CharactersById.Values.Where(c => c.CampaignId == campaign.Id).Where(c => isGm || c.OwnerUserId == user.Id).OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).Select(ToCharacterSummary).ToArray();
|
|
||||||
|
|
||||||
var visibleCharacterIds = characters.Select(c => c.Id).ToHashSet();
|
var visibleCharacterIds = characters.Select(c => c.Id).ToHashSet();
|
||||||
|
|
||||||
@@ -318,7 +316,7 @@ public sealed class GameService : IGameService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<IReadOnlyList<CharacterSummary>> GetCurrentCampaignCharacters(string sessionToken)
|
public ServiceResult<IReadOnlyList<CharacterSummary>> GetOwnCharacters(string sessionToken)
|
||||||
{
|
{
|
||||||
lock (m_Gate)
|
lock (m_Gate)
|
||||||
{
|
{
|
||||||
@@ -326,10 +324,7 @@ public sealed class GameService : IGameService
|
|||||||
if (user is null)
|
if (user is null)
|
||||||
return ServiceResult<IReadOnlyList<CharacterSummary>>.Failure("unauthorized", "You must be logged in.");
|
return ServiceResult<IReadOnlyList<CharacterSummary>>.Failure("unauthorized", "You must be logged in.");
|
||||||
|
|
||||||
if (!TryGetCurrentCampaignIdLocked(user, out var campaignId))
|
var characters = m_CharactersById.Values.Where(c => c.OwnerUserId == user.Id).OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).Select(ToCharacterSummary).ToArray();
|
||||||
return ServiceResult<IReadOnlyList<CharacterSummary>>.Failure("no_active_character", "No active character is selected.");
|
|
||||||
|
|
||||||
var characters = m_CharactersById.Values.Where(c => c.CampaignId == campaignId && c.OwnerUserId == user.Id).OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).Select(ToCharacterSummary).ToArray();
|
|
||||||
|
|
||||||
return ServiceResult<IReadOnlyList<CharacterSummary>>.Success(characters);
|
return ServiceResult<IReadOnlyList<CharacterSummary>>.Success(characters);
|
||||||
}
|
}
|
||||||
@@ -803,9 +798,18 @@ public sealed class GameService : IGameService
|
|||||||
return new(user.Id, user.Username, user.DisplayName);
|
return new(user.Id, user.Username, user.DisplayName);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CampaignSummary ToCampaignSummary(Campaign campaign)
|
private CampaignDetails ToCampaignDetails(Campaign campaign)
|
||||||
{
|
{
|
||||||
return new(campaign.Id, campaign.Name, DiceRules.ToRulesetId(campaign.Ruleset), campaign.GmUserId);
|
lock (m_Gate)
|
||||||
|
{
|
||||||
|
var gm = m_UsersById[campaign.GmUserId];
|
||||||
|
var characters = m_CharactersById.Values.Where(c => c.CampaignId == campaign.Id).Select(ToCharacterSummary).ToList();
|
||||||
|
var visibleCharacterIds = characters.Select(c => c.Id).ToHashSet();
|
||||||
|
var skillGroups = m_SkillGroupsById.Values.Where(g => visibleCharacterIds.Contains(g.CharacterId)).OrderBy(g => g.Name, StringComparer.OrdinalIgnoreCase).Select(ToSkillGroupSummary).ToArray();
|
||||||
|
var skills = m_SkillsById.Values.Where(s => visibleCharacterIds.Contains(s.CharacterId)).OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase).Select(ToSkillSummary).ToArray();
|
||||||
|
|
||||||
|
return new(campaign.Id, campaign.Name, DiceRules.ToRulesetId(campaign.Ruleset), ToUserSummary(gm), characters, skillGroups, skills);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CharacterSummary ToCharacterSummary(Character character)
|
private static CharacterSummary ToCharacterSummary(Character character)
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ public interface IGameService
|
|||||||
UserSummary? GetUserBySession(string sessionToken);
|
UserSummary? GetUserBySession(string sessionToken);
|
||||||
ServiceResult<MeResponse> GetMe(string sessionToken);
|
ServiceResult<MeResponse> GetMe(string sessionToken);
|
||||||
|
|
||||||
ServiceResult<CampaignSummary> CreateCampaign(string sessionToken, string name, string rulesetId);
|
ServiceResult<CampaignDetails> CreateCampaign(string sessionToken, string name, string rulesetId);
|
||||||
ServiceResult<IReadOnlyList<CampaignSummary>> GetCampaigns(string sessionToken);
|
ServiceResult<IReadOnlyList<CampaignDetails>> GetCampaigns(string sessionToken);
|
||||||
ServiceResult<CampaignDetails> GetCampaign(string sessionToken, Guid campaignId);
|
ServiceResult<CampaignDetails> GetCampaign(string sessionToken, Guid campaignId);
|
||||||
ServiceResult<IReadOnlyList<string>> GetUsernames(string sessionToken);
|
ServiceResult<IReadOnlyList<string>> GetUsernames(string sessionToken);
|
||||||
|
|
||||||
ServiceResult<CharacterSummary> CreateCharacter(string sessionToken, string name, Guid campaignId);
|
ServiceResult<CharacterSummary> CreateCharacter(string sessionToken, string name, Guid campaignId);
|
||||||
ServiceResult<CharacterSummary> UpdateCharacter(string sessionToken, Guid characterId, string name, Guid campaignId, string? ownerUsername = null);
|
ServiceResult<CharacterSummary> UpdateCharacter(string sessionToken, Guid characterId, string name, Guid campaignId, string? ownerUsername = null);
|
||||||
ServiceResult<bool> ActivateCharacter(string sessionToken, Guid characterId);
|
ServiceResult<bool> ActivateCharacter(string sessionToken, Guid characterId);
|
||||||
ServiceResult<IReadOnlyList<CharacterSummary>> GetCurrentCampaignCharacters(string sessionToken);
|
ServiceResult<IReadOnlyList<CharacterSummary>> GetOwnCharacters(string sessionToken);
|
||||||
|
|
||||||
ServiceResult<SkillGroupSummary> CreateSkillGroup(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble);
|
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<SkillGroupSummary> UpdateSkillGroup(string sessionToken, Guid skillGroupId, string name, string diceRollDefinition, int wildDice, bool allowFumble);
|
||||||
|
|||||||
@@ -444,12 +444,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/characters/current-campaign": {
|
"/api/characters": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getCurrentCampaignCharacters",
|
"operationId": "getOwnCharacters",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "Current campaign characters owned by the user.",
|
"description": "Characters owned by the user.",
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
|
|||||||
Reference in New Issue
Block a user