Cleanup campaign management ux

This commit is contained in:
2026-02-26 14:56:56 +01:00
parent 83151d81fd
commit 0cb41dd004
17 changed files with 55 additions and 113 deletions

View File

@@ -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);
} }
} }

View File

@@ -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");

View File

@@ -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);

View File

@@ -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);
} }

View File

@@ -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())
{ {

View File

@@ -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);
}); });

View File

@@ -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)"

View File

@@ -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; }

View File

@@ -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; }

View File

@@ -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>
} }

View File

@@ -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; }

View File

@@ -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"

View File

@@ -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; }

View File

@@ -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);

View File

@@ -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)

View File

@@ -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);

View File

@@ -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": {