Refactor campaign payload loading
This commit is contained in:
@@ -99,6 +99,7 @@ dotnet dotnet-ef migrations add <MigrationName> --project RpgRoller/RpgRoller.cs
|
||||
|
||||
- Runtime frontend is Blazor Server with interactive components.
|
||||
- Browser interop is in `RpgRoller/wwwroot/js/rpgroller-api.js`.
|
||||
- Workspace campaign data is loaded in bounded slices: visible campaign summaries, a selected campaign roster, a selected character sheet, and the 100 most recent visible log entries.
|
||||
- OpenAPI contract source remains at `openapi/RpgRoller.json`.
|
||||
|
||||
## Test and Coverage
|
||||
|
||||
@@ -17,7 +17,7 @@ public sealed class CampaignApiTests : ApiTestBase
|
||||
await RegisterAsync(gmClient, "gm", "Password123", "Game Master");
|
||||
await LoginAsync(gmClient, "gm", "Password123");
|
||||
|
||||
var campaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(gmClient, "/api/campaigns", new("Alpha Campaign", "dnd5e"));
|
||||
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(gmClient, "/api/campaigns", new("Alpha Campaign", "dnd5e"));
|
||||
|
||||
var gmCharacter = await PostAsync<CreateCharacterRequest, CharacterSummary>(gmClient, "/api/characters", new("Arin", campaign.Id));
|
||||
Assert.Equal("Game Master", gmCharacter.OwnerDisplayName);
|
||||
@@ -39,17 +39,25 @@ public sealed class CampaignApiTests : ApiTestBase
|
||||
var invalidSkill = await gmClient.PostAsJsonAsync($"/api/characters/{gmCharacter.Id}/skills", new CreateSkillRequest("Broken", "5D+4", 0, false));
|
||||
Assert.Equal(HttpStatusCode.BadRequest, invalidSkill.StatusCode);
|
||||
|
||||
var details = await GetAsync<CampaignDetails>(gmClient, $"/api/campaigns/{campaign.Id}");
|
||||
var details = await GetAsync<CampaignRoster>(gmClient, $"/api/campaigns/{campaign.Id}");
|
||||
Assert.Equal(campaign.Id, details.Id);
|
||||
Assert.Single(details.Characters);
|
||||
Assert.Equal("Game Master", details.Characters[0].OwnerDisplayName);
|
||||
|
||||
var summaries = await GetAsync<IReadOnlyList<CampaignSummary>>(gmClient, "/api/campaigns");
|
||||
Assert.Single(summaries);
|
||||
Assert.Equal(1, summaries[0].CharacterCount);
|
||||
|
||||
var sheet = await GetAsync<CharacterSheet>(gmClient, $"/api/characters/{gmCharacter.Id}/sheet");
|
||||
Assert.Single(sheet.Skills);
|
||||
Assert.Equal(updatedSkill.Id, sheet.Skills[0].Id);
|
||||
|
||||
var currentCampaignCharacters = await GetAsync<IReadOnlyList<CharacterSummary>>(gmClient, "/api/characters");
|
||||
Assert.Single(currentCampaignCharacters);
|
||||
Assert.Equal(gmCharacter.Id, currentCampaignCharacters[0].Id);
|
||||
Assert.Equal("Game Master", currentCampaignCharacters[0].OwnerDisplayName);
|
||||
|
||||
var otherCampaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(gmClient, "/api/campaigns", new("Beta Campaign", "d6"));
|
||||
var otherCampaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(gmClient, "/api/campaigns", new("Beta Campaign", "d6"));
|
||||
|
||||
var updatedCharacter = await PutAsync<UpdateCharacterRequest, CharacterSummary>(gmClient, $"/api/characters/{gmCharacter.Id}", new("Arin Updated", otherCampaign.Id));
|
||||
|
||||
@@ -74,7 +82,7 @@ public sealed class CampaignApiTests : ApiTestBase
|
||||
await RegisterAsync(receiverClient, "receiver2", "Password123", "Receiver");
|
||||
await LoginAsync(receiverClient, "receiver2", "Password123");
|
||||
|
||||
var campaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(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 createdGroup = await PostAsync<CreateSkillGroupRequest, SkillGroupSummary>(ownerClient, $"/api/characters/{character.Id}/skill-groups", new("Combat", "2D+1", 1, true));
|
||||
@@ -103,7 +111,7 @@ public sealed class CampaignApiTests : ApiTestBase
|
||||
Assert.Equal("Grouped Hero", transferResult.Name);
|
||||
Assert.Equal("Receiver", transferResult.OwnerDisplayName);
|
||||
|
||||
var gmCampaignView = await GetAsync<CampaignDetails>(gmClient, $"/api/campaigns/{campaign.Id}");
|
||||
var gmCampaignView = await GetAsync<CampaignRoster>(gmClient, $"/api/campaigns/{campaign.Id}");
|
||||
var gmViewedCharacter = Assert.Single(gmCampaignView.Characters, c => c.Id == character.Id);
|
||||
Assert.Equal("Receiver", gmViewedCharacter.OwnerDisplayName);
|
||||
|
||||
@@ -139,7 +147,7 @@ public sealed class CampaignApiTests : ApiTestBase
|
||||
var promotedPlayer = await PutAsync<UpdateUserRolesRequest, AdminUserSummary>(adminClient, $"/api/admin/users/{player.Id}/roles", new([ "admin" ]));
|
||||
Assert.Contains(promotedPlayer.Roles, role => string.Equals(role, "admin", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var campaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(gmClient, "/api/campaigns", new("Disposable Campaign", "d6"));
|
||||
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(gmClient, "/api/campaigns", new("Disposable Campaign", "d6"));
|
||||
var character = await PostAsync<CreateCharacterRequest, CharacterSummary>(playerClient, "/api/characters", new("Disposable Hero", campaign.Id));
|
||||
var skill = await PostAsync<CreateSkillRequest, SkillSummary>(playerClient, $"/api/characters/{character.Id}/skills", new("Stealth", "2D+1", 1, true));
|
||||
_ = await PostAsync<RollSkillRequest, RollResult>(playerClient, $"/api/skills/{skill.Id}/roll", new("public"));
|
||||
@@ -213,10 +221,10 @@ public sealed class CampaignApiTests : ApiTestBase
|
||||
await RegisterAsync(playerClient, "player-options", "Password123", "Player");
|
||||
await LoginAsync(playerClient, "player-options", "Password123");
|
||||
|
||||
var firstCampaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(gmClient, "/api/campaigns", new("Alpha Visible", "d6"));
|
||||
var secondCampaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(otherGmClient, "/api/campaigns", new("Beta Available", "d6"));
|
||||
var firstCampaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(gmClient, "/api/campaigns", new("Alpha Visible", "d6"));
|
||||
var secondCampaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(otherGmClient, "/api/campaigns", new("Beta Available", "d6"));
|
||||
|
||||
var playerVisibleCampaigns = await GetAsync<IReadOnlyList<CampaignDetails>>(playerClient, "/api/campaigns");
|
||||
var playerVisibleCampaigns = await GetAsync<IReadOnlyList<CampaignSummary>>(playerClient, "/api/campaigns");
|
||||
Assert.Empty(playerVisibleCampaigns);
|
||||
|
||||
var playerCampaignOptions = await GetAsync<IReadOnlyList<CampaignOption>>(playerClient, "/api/campaigns/options");
|
||||
@@ -246,7 +254,7 @@ public sealed class CampaignApiTests : ApiTestBase
|
||||
await RegisterAsync(otherClient, "other-delete", "Password123", "Other");
|
||||
await LoginAsync(otherClient, "other-delete", "Password123");
|
||||
|
||||
var campaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(gmClient, "/api/campaigns", new("Deletion Campaign", "d6"));
|
||||
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(gmClient, "/api/campaigns", new("Deletion Campaign", "d6"));
|
||||
var ownerCharacter = await PostAsync<CreateCharacterRequest, CharacterSummary>(ownerClient, "/api/characters", new("Owner Character", campaign.Id));
|
||||
var otherCharacter = await PostAsync<CreateCharacterRequest, CharacterSummary>(otherClient, "/api/characters", new("Other Character", campaign.Id));
|
||||
|
||||
@@ -262,7 +270,43 @@ public sealed class CampaignApiTests : ApiTestBase
|
||||
var adminDelete = await adminClient.DeleteAsync($"/api/characters/{otherCharacter.Id}");
|
||||
Assert.Equal(HttpStatusCode.OK, adminDelete.StatusCode);
|
||||
|
||||
var campaignAfterDeletes = await GetAsync<CampaignDetails>(gmClient, $"/api/campaigns/{campaign.Id}");
|
||||
var campaignAfterDeletes = await GetAsync<CampaignRoster>(gmClient, $"/api/campaigns/{campaign.Id}");
|
||||
Assert.Empty(campaignAfterDeletes.Characters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CampaignLog_ReturnsMostRecentHundredEntries()
|
||||
{
|
||||
using var factory = CreateFactory();
|
||||
using var gmClient = factory.CreateClient(new() { AllowAutoRedirect = false });
|
||||
using var playerClient = factory.CreateClient(new() { AllowAutoRedirect = false });
|
||||
|
||||
await RegisterAsync(gmClient, "gm-log-cap", "Password123", "GM");
|
||||
await LoginAsync(gmClient, "gm-log-cap", "Password123");
|
||||
|
||||
await RegisterAsync(playerClient, "player-log-cap", "Password123", "Player");
|
||||
await LoginAsync(playerClient, "player-log-cap", "Password123");
|
||||
|
||||
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(gmClient, "/api/campaigns", new("Log Cap", "d6"));
|
||||
var character = await PostAsync<CreateCharacterRequest, CharacterSummary>(playerClient, "/api/characters", new("Roller", campaign.Id));
|
||||
var skill = await PostAsync<CreateSkillRequest, SkillSummary>(playerClient, $"/api/characters/{character.Id}/skills", new("Stealth", "2D+1", 1, true));
|
||||
|
||||
var rollIds = new List<Guid>();
|
||||
for (var i = 0; i < 105; i++)
|
||||
{
|
||||
var roll = await PostAsync<RollSkillRequest, RollResult>(playerClient, $"/api/skills/{skill.Id}/roll", new("public"));
|
||||
rollIds.Add(roll.RollId);
|
||||
}
|
||||
|
||||
var log = await GetAsync<IReadOnlyList<CampaignLogEntry>>(gmClient, $"/api/campaigns/{campaign.Id}/log");
|
||||
Assert.Equal(100, log.Count);
|
||||
Assert.Equal(rollIds[5], log[0].RollId);
|
||||
Assert.Equal(rollIds[^1], log[^1].RollId);
|
||||
Assert.All(log, entry =>
|
||||
{
|
||||
Assert.False(string.IsNullOrWhiteSpace(entry.CharacterName));
|
||||
Assert.False(string.IsNullOrWhiteSpace(entry.SkillName));
|
||||
Assert.False(string.IsNullOrWhiteSpace(entry.RollerDisplayName));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace RpgRoller.Tests;
|
||||
|
||||
public sealed class FrontendHostTests : ApiTestBase
|
||||
@@ -21,24 +18,4 @@ public sealed class FrontendHostTests : ApiTestBase
|
||||
Assert.Contains("_framework/blazor.web.js", html);
|
||||
Assert.Contains("Connecting...", html);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BlazorHub_AllowsLargerInteropPayloads()
|
||||
{
|
||||
using var factory = CreateFactory();
|
||||
var componentHubType = Type.GetType("Microsoft.AspNetCore.Components.Server.ComponentHub, Microsoft.AspNetCore.Components.Server");
|
||||
Assert.NotNull(componentHubType);
|
||||
|
||||
var hubOptionsType = typeof(HubOptions<>).MakeGenericType(componentHubType);
|
||||
var optionsType = typeof(IOptions<>).MakeGenericType(hubOptionsType);
|
||||
var options = factory.Services.GetService(optionsType);
|
||||
Assert.NotNull(options);
|
||||
|
||||
var value = optionsType.GetProperty("Value")!.GetValue(options);
|
||||
Assert.NotNull(value);
|
||||
|
||||
var maximumReceiveMessageSize = (long?)hubOptionsType.GetProperty("MaximumReceiveMessageSize")!.GetValue(value);
|
||||
|
||||
Assert.Equal(256 * 1024, maximumReceiveMessageSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ public sealed class RollVisibilityApiTests : ApiTestBase
|
||||
|
||||
await RegisterAsync(gmClient, "gm", "Password123", "GM");
|
||||
await LoginAsync(gmClient, "gm", "Password123");
|
||||
var campaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(gmClient, "/api/campaigns", new("Main", "d6"));
|
||||
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(gmClient, "/api/campaigns", new("Main", "d6"));
|
||||
|
||||
await RegisterAsync(playerClient, "player", "Password123", "Player");
|
||||
await LoginAsync(playerClient, "player", "Password123");
|
||||
@@ -68,4 +68,4 @@ public sealed class RollVisibilityApiTests : ApiTestBase
|
||||
var unauthorizedWithInvalidSession = await anonymousClient.SendAsync(invalidSessionRequest);
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, unauthorizedWithInvalidSession.StatusCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@ public sealed class SystemApiTests : ApiTestBase
|
||||
await RegisterAsync(client, "sse", "Password123", "Sse User");
|
||||
await LoginAsync(client, "sse", "Password123");
|
||||
|
||||
var campaign = await PostAsync<CreateCampaignRequest, CampaignDetails>(client, "/api/campaigns", new("SSE", "d6"));
|
||||
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(client, "/api/campaigns", new("SSE", "d6"));
|
||||
|
||||
var sseResponse = await client.GetAsync($"/api/events/state?campaignId={campaign.Id}", HttpCompletionOption.ResponseHeadersRead);
|
||||
Assert.Equal(HttpStatusCode.OK, sseResponse.StatusCode);
|
||||
Assert.Equal("text/event-stream", sseResponse.Content.Headers.ContentType?.MediaType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ public sealed class ServiceCampaignTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetCampaign_ForNonGmParticipant_ReturnsCampaignCharactersAndSkills()
|
||||
public void GetCampaignAndCharacterSheet_ForNonGmParticipant_ReturnCampaignRosterAndSheet()
|
||||
{
|
||||
using var harness = ServiceTestSupport.CreateHarness();
|
||||
var service = harness.Service;
|
||||
@@ -120,7 +120,11 @@ public sealed class ServiceCampaignTests
|
||||
Assert.Equal(2, ownerView.Characters.Count);
|
||||
Assert.Contains(ownerView.Characters, character => character.Id == ownerCharacter.Id);
|
||||
Assert.Contains(ownerView.Characters, character => character.Id == otherCharacter.Id);
|
||||
Assert.Equal(2, ownerView.Skills.Count);
|
||||
Assert.Contains(ownerView.Skills, skill => skill.Id == ownerSkill.Id);
|
||||
|
||||
var ownerSheet = ServiceTestSupport.GetValue(service.GetCharacterSheet(ownerSession, ownerCharacter.Id));
|
||||
var otherSheet = ServiceTestSupport.GetValue(service.GetCharacterSheet(ownerSession, otherCharacter.Id));
|
||||
Assert.Single(ownerSheet.Skills);
|
||||
Assert.Contains(ownerSheet.Skills, skill => skill.Id == ownerSkill.Id);
|
||||
Assert.Single(otherSheet.Skills);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,17 +45,19 @@ public sealed class ServiceSkillGroupAndOwnershipTests
|
||||
var deletedGroup = ServiceTestSupport.GetValue(service.DeleteSkillGroup(ownerSession, renamedGroup.Id));
|
||||
Assert.True(deletedGroup);
|
||||
|
||||
var afterGroupDelete = ServiceTestSupport.GetValue(service.GetCampaign(ownerSession, campaign.Id));
|
||||
Assert.DoesNotContain(afterGroupDelete.SkillGroups, group => group.Id == renamedGroup.Id);
|
||||
Assert.Contains(afterGroupDelete.SkillGroups, group => group.Id == otherGroup.Id);
|
||||
Assert.Null(afterGroupDelete.Skills.Single(s => s.Id == regroupedSkill.Id).SkillGroupId);
|
||||
var ownerSheetAfterGroupDelete = ServiceTestSupport.GetValue(service.GetCharacterSheet(ownerSession, ownerCharacter.Id));
|
||||
var otherSheetAfterGroupDelete = ServiceTestSupport.GetValue(service.GetCharacterSheet(ownerSession, otherCharacter.Id));
|
||||
Assert.DoesNotContain(ownerSheetAfterGroupDelete.SkillGroups, group => group.Id == renamedGroup.Id);
|
||||
Assert.Contains(otherSheetAfterGroupDelete.SkillGroups, group => group.Id == otherGroup.Id);
|
||||
Assert.Null(ownerSheetAfterGroupDelete.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.GetCharacterSheet(ownerSession, ownerCharacter.Id));
|
||||
var otherView = ServiceTestSupport.GetValue(service.GetCharacterSheet(ownerSession, otherCharacter.Id));
|
||||
Assert.DoesNotContain(ownerView.SkillGroups, group => group.Id == renamedGroup.Id);
|
||||
Assert.Contains(ownerView.SkillGroups, group => group.Id == otherGroup.Id);
|
||||
Assert.Contains(otherView.SkillGroups, group => group.Id == otherGroup.Id);
|
||||
Assert.DoesNotContain(ownerView.Skills, skillSummary => skillSummary.Id == regroupedSkill.Id);
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,12 @@ internal static class CharacterEndpoints
|
||||
return ApiResultMapper.ToApiResult(result);
|
||||
});
|
||||
|
||||
group.MapGet("/characters/{characterId:guid}/sheet", (Guid characterId, HttpContext context, IGameService game) =>
|
||||
{
|
||||
var result = game.GetCharacterSheet(context.GetRequiredSessionToken(), characterId);
|
||||
return ApiResultMapper.ToApiResult(result);
|
||||
});
|
||||
|
||||
group.MapGet("/users/usernames", (HttpContext context, IGameService game) =>
|
||||
{
|
||||
var result = game.GetUsernames(context.GetRequiredSessionToken());
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
@foreach (var entry in CampaignLog)
|
||||
{
|
||||
<li class="log-entry @LogEntryCssClass(entry)">
|
||||
<p><strong>@RollerLabel(entry)</strong> rolled <strong>@SkillLabel(entry.SkillId)</strong> with
|
||||
<strong>@CharacterLabel(entry.CharacterId)</strong></p>
|
||||
<p><strong>@RollerLabel(entry)</strong> rolled <strong>@entry.SkillName</strong> with
|
||||
<strong>@entry.CharacterName</strong></p>
|
||||
<p class="roll-total inline">@entry.Result</p>
|
||||
<RollDiceStrip Dice="entry.Dice" AriaLabel="Log roll dice"/>
|
||||
<p>@entry.Breakdown</p>
|
||||
|
||||
@@ -48,12 +48,6 @@ public partial class CampaignLogPanel
|
||||
[Parameter]
|
||||
public Func<CampaignLogEntry, string> RollerLabel { get; set; } = _ => string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public Func<Guid, string> SkillLabel { get; set; } = _ => string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public Func<Guid, string> CharacterLabel { get; set; } = _ => string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public Func<CampaignLogEntry, string> LogEntryCssClass { get; set; } = _ => string.Empty;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<select id="campaign-select" @onchange="CampaignSelectionChanged">
|
||||
@foreach (var campaign in Campaigns)
|
||||
{
|
||||
<option value="@campaign.Id" selected="@(campaign.Id == SelectedCampaignId)">@campaign.Name (@campaign.RulesetId), GM: @campaign.Gm.DisplayName, @campaign.Characters.Count characters</option>
|
||||
<option value="@campaign.Id" selected="@(campaign.Id == SelectedCampaignId)">@campaign.Name (@campaign.RulesetId), GM: @campaign.Gm.DisplayName, @campaign.CharacterCount characters</option>
|
||||
}
|
||||
</select>
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ public partial class CampaignManagementPanel
|
||||
IsCreatingCampaign = true;
|
||||
try
|
||||
{
|
||||
var campaign = await ApiClient.RequestAsync<CampaignDetails>("POST", "/api/campaigns", new CreateCampaignRequest(CampaignState.Model.Name.Trim(), CampaignState.Model.RulesetId));
|
||||
var campaign = await ApiClient.RequestAsync<CampaignSummary>("POST", "/api/campaigns", new CreateCampaignRequest(CampaignState.Model.Name.Trim(), CampaignState.Model.RulesetId));
|
||||
|
||||
CampaignState.Model.Name = string.Empty;
|
||||
ShowCreateCampaignModal = false;
|
||||
@@ -72,13 +72,13 @@ public partial class CampaignManagementPanel
|
||||
private bool ShowCreateCampaignModal { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public IReadOnlyList<CampaignDetails> Campaigns { get; set; } = [];
|
||||
public IReadOnlyList<CampaignSummary> Campaigns { get; set; } = [];
|
||||
|
||||
[Parameter]
|
||||
public Guid? SelectedCampaignId { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public CampaignDetails? SelectedCampaign { get; set; }
|
||||
public CampaignRoster? SelectedCampaign { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public IReadOnlyList<RulesetDefinition> Rulesets { get; set; } = [];
|
||||
|
||||
@@ -285,7 +285,7 @@ public partial class CharacterPanel
|
||||
public bool IsCampaignDataLoading { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public CampaignDetails? SelectedCampaign { get; set; }
|
||||
public CampaignRoster? SelectedCampaign { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Guid? SelectedCharacterId { get; set; }
|
||||
|
||||
@@ -61,8 +61,6 @@
|
||||
IsCampaignDataLoading="IsCampaignDataLoading"
|
||||
CampaignLog="PlayVisibleCampaignLog"
|
||||
RollerLabel="RollerLabel"
|
||||
SkillLabel="SkillLabel"
|
||||
CharacterLabel="CharacterLabel"
|
||||
LogEntryCssClass="LogEntryCssClass"
|
||||
VisibilityLabel="VisibilityLabel"
|
||||
VisibilityBadgeCssClass="VisibilityBadgeCssClass"/>
|
||||
|
||||
@@ -143,7 +143,7 @@ public partial class Workspace : IAsyncDisposable
|
||||
|
||||
private async Task ReloadCampaignsAsync(Guid? preferredCampaignId)
|
||||
{
|
||||
var campaigns = await ApiClient.RequestAsync<IReadOnlyList<CampaignDetails>>("GET", "/api/campaigns");
|
||||
var campaigns = await ApiClient.RequestAsync<IReadOnlyList<CampaignSummary>>("GET", "/api/campaigns");
|
||||
Campaigns = campaigns.OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
|
||||
if (Campaigns.Count == 0)
|
||||
@@ -173,6 +173,8 @@ public partial class Workspace : IAsyncDisposable
|
||||
if (!SelectedCampaignId.HasValue)
|
||||
{
|
||||
SelectedCampaign = null;
|
||||
SelectedCharacterSkills = [];
|
||||
SelectedCharacterSkillGroups = [];
|
||||
CampaignLog = [];
|
||||
SelectedCharacterId = null;
|
||||
ConnectionState = "offline";
|
||||
@@ -183,9 +185,18 @@ public partial class Workspace : IAsyncDisposable
|
||||
try
|
||||
{
|
||||
var campaignId = SelectedCampaignId.Value;
|
||||
SelectedCampaign = await ApiClient.RequestAsync<CampaignDetails>("GET", $"/api/campaigns/{campaignId}");
|
||||
CampaignLog = (await ApiClient.RequestAsync<IReadOnlyList<CampaignLogEntry>>("GET", $"/api/campaigns/{campaignId}/log")).ToList();
|
||||
SelectedCampaign = await ApiClient.RequestAsync<CampaignRoster>("GET", $"/api/campaigns/{campaignId}");
|
||||
SyncSelectedCharacter();
|
||||
|
||||
if (IsPlayScreen && PlaySelectedCharacterId.HasValue && SelectedCharacterId != PlaySelectedCharacterId)
|
||||
SelectedCharacterId = PlaySelectedCharacterId;
|
||||
|
||||
await RefreshSelectedCharacterSheetAsync();
|
||||
|
||||
CampaignLog = IsPlayScreen
|
||||
? (await ApiClient.RequestAsync<IReadOnlyList<CampaignLogEntry>>("GET", $"/api/campaigns/{campaignId}/log")).ToList()
|
||||
: [];
|
||||
|
||||
await EnsureSelectedCharacterActiveAsync();
|
||||
}
|
||||
catch (ApiRequestException ex) when (ex.StatusCode == 401)
|
||||
@@ -238,6 +249,12 @@ public partial class Workspace : IAsyncDisposable
|
||||
await PersistScreenPreferenceAsync(CurrentScreen);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
if (User is not null)
|
||||
{
|
||||
await RefreshCampaignScopeAsync();
|
||||
await SyncStateEventsAsync();
|
||||
}
|
||||
|
||||
if (IsAdminScreen)
|
||||
{
|
||||
await EnsureAdminUsersLoadedAsync();
|
||||
@@ -521,6 +538,7 @@ public partial class Workspace : IAsyncDisposable
|
||||
private async Task SelectCharacterAsync(Guid characterId)
|
||||
{
|
||||
SelectedCharacterId = characterId;
|
||||
await RefreshSelectedCharacterSheetAsync();
|
||||
await EnsureSelectedCharacterActiveAsync();
|
||||
}
|
||||
|
||||
@@ -559,6 +577,24 @@ public partial class Workspace : IAsyncDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshSelectedCharacterSheetAsync()
|
||||
{
|
||||
if (!SelectedCharacterId.HasValue || SelectedCampaign is null || !IsPlayScreen)
|
||||
{
|
||||
SelectedCharacterSkills = [];
|
||||
SelectedCharacterSkillGroups = [];
|
||||
return;
|
||||
}
|
||||
|
||||
var sheet = await ApiClient.RequestAsync<CharacterSheet>("GET", $"/api/characters/{SelectedCharacterId.Value}/sheet");
|
||||
SelectedCharacterSkillGroups = sheet.SkillGroups
|
||||
.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
SelectedCharacterSkills = sheet.Skills
|
||||
.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task OnSkillCreatedAsync(Guid _)
|
||||
{
|
||||
await RefreshCampaignScopeAsync();
|
||||
@@ -715,7 +751,7 @@ public partial class Workspace : IAsyncDisposable
|
||||
|
||||
private async Task SyncStateEventsAsync()
|
||||
{
|
||||
if (User is null || !SelectedCampaignId.HasValue)
|
||||
if (User is null || !SelectedCampaignId.HasValue || IsAdminScreen)
|
||||
{
|
||||
await StopStateEventsAsync();
|
||||
ConnectionState = "offline";
|
||||
@@ -795,16 +831,6 @@ public partial class Workspace : IAsyncDisposable
|
||||
return string.IsNullOrWhiteSpace(ownerDisplayName) ? "Unknown owner" : ownerDisplayName;
|
||||
}
|
||||
|
||||
private string CharacterLabel(Guid characterId)
|
||||
{
|
||||
return SelectedCampaign?.Characters.FirstOrDefault(c => c.Id == characterId)?.Name ?? "Hidden character";
|
||||
}
|
||||
|
||||
private string SkillLabel(Guid skillId)
|
||||
{
|
||||
return SelectedCampaign?.Skills.FirstOrDefault(s => s.Id == skillId)?.Name ?? "Hidden skill";
|
||||
}
|
||||
|
||||
private string SkillDefinitionLabel(SkillSummary skill)
|
||||
{
|
||||
if (!string.Equals(SelectedCampaign?.RulesetId, "d6", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -822,7 +848,7 @@ public partial class Workspace : IAsyncDisposable
|
||||
if (SelectedCampaign is not null && entry.RollerUserId == SelectedCampaign.Gm.Id)
|
||||
return "GM";
|
||||
|
||||
return "Participant";
|
||||
return entry.RollerDisplayName;
|
||||
}
|
||||
|
||||
private string VisibilityLabel(CampaignLogEntry entry)
|
||||
@@ -866,6 +892,8 @@ public partial class Workspace : IAsyncDisposable
|
||||
SelectedCampaign = null;
|
||||
Campaigns = [];
|
||||
CharacterCampaignOptions = [];
|
||||
SelectedCharacterSkills = [];
|
||||
SelectedCharacterSkillGroups = [];
|
||||
CampaignLog = [];
|
||||
SelectedCharacterId = null;
|
||||
LastRoll = null;
|
||||
@@ -934,9 +962,11 @@ public partial class Workspace : IAsyncDisposable
|
||||
private UserSummary? User { get; set; }
|
||||
private Guid? ActiveCharacterId { get; set; }
|
||||
private Guid? SelectedCampaignId { get; set; }
|
||||
private CampaignDetails? SelectedCampaign { get; set; }
|
||||
private List<CampaignDetails> Campaigns { get; set; } = [];
|
||||
private CampaignRoster? SelectedCampaign { get; set; }
|
||||
private List<CampaignSummary> Campaigns { get; set; } = [];
|
||||
private List<CampaignOption> CharacterCampaignOptions { get; set; } = [];
|
||||
private List<SkillSummary> SelectedCharacterSkills { get; set; } = [];
|
||||
private List<SkillGroupSummary> SelectedCharacterSkillGroups { get; set; } = [];
|
||||
private List<CampaignLogEntry> CampaignLog { get; set; } = [];
|
||||
private List<RulesetDefinition> Rulesets { get; set; } = [];
|
||||
private List<AdminUserSummary> AdminUsers { get; set; } = [];
|
||||
@@ -973,12 +1003,12 @@ public partial class Workspace : IAsyncDisposable
|
||||
[Parameter]
|
||||
public EventCallback<string?> LoggedOut { get; set; }
|
||||
|
||||
private string? SelectedCampaignName => SelectedCampaign?.Name;
|
||||
private string? SelectedCampaignName => SelectedCampaign?.Name ?? Campaigns.FirstOrDefault(campaign => campaign.Id == SelectedCampaignId)?.Name;
|
||||
|
||||
private CharacterSummary? SelectedCharacter =>
|
||||
SelectedCampaign?.Characters.FirstOrDefault(c => c.Id == SelectedCharacterId);
|
||||
|
||||
private CampaignDetails? PlaySelectedCampaign
|
||||
private CampaignRoster? PlaySelectedCampaign
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -986,27 +1016,18 @@ public partial class Workspace : IAsyncDisposable
|
||||
return null;
|
||||
|
||||
if (User is null)
|
||||
return new CampaignDetails(SelectedCampaign.Id, SelectedCampaign.Name, SelectedCampaign.RulesetId, SelectedCampaign.Gm, [], [], []);
|
||||
return new CampaignRoster(SelectedCampaign.Id, SelectedCampaign.Name, SelectedCampaign.RulesetId, SelectedCampaign.Gm, []);
|
||||
|
||||
var ownedCharacters = SelectedCampaign.Characters
|
||||
.Where(character => character.OwnerUserId == User.Id)
|
||||
.ToList();
|
||||
var ownedCharacterIds = ownedCharacters.Select(character => character.Id).ToHashSet();
|
||||
var ownedSkillGroups = SelectedCampaign.SkillGroups
|
||||
.Where(group => ownedCharacterIds.Contains(group.CharacterId))
|
||||
.ToList();
|
||||
var ownedSkills = SelectedCampaign.Skills
|
||||
.Where(skill => ownedCharacterIds.Contains(skill.CharacterId))
|
||||
.ToList();
|
||||
|
||||
return new CampaignDetails(
|
||||
return new CampaignRoster(
|
||||
SelectedCampaign.Id,
|
||||
SelectedCampaign.Name,
|
||||
SelectedCampaign.RulesetId,
|
||||
SelectedCampaign.Gm,
|
||||
ownedCharacters,
|
||||
ownedSkillGroups,
|
||||
ownedSkills);
|
||||
ownedCharacters);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1038,20 +1059,10 @@ public partial class Workspace : IAsyncDisposable
|
||||
private Guid? PlaySelectedCharacterId => PlaySelectedCharacter?.Id;
|
||||
|
||||
private List<SkillSummary> PlaySelectedCharacterSkills =>
|
||||
PlaySelectedCampaign is null || !PlaySelectedCharacterId.HasValue
|
||||
? []
|
||||
: PlaySelectedCampaign.Skills
|
||||
.Where(skill => skill.CharacterId == PlaySelectedCharacterId.Value)
|
||||
.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
PlaySelectedCampaign is null || !PlaySelectedCharacterId.HasValue ? [] : SelectedCharacterSkills;
|
||||
|
||||
private List<SkillGroupSummary> PlaySelectedCharacterSkillGroups =>
|
||||
PlaySelectedCampaign is null || !PlaySelectedCharacterId.HasValue
|
||||
? []
|
||||
: PlaySelectedCampaign.SkillGroups
|
||||
.Where(group => group.CharacterId == PlaySelectedCharacterId.Value)
|
||||
.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
PlaySelectedCampaign is null || !PlaySelectedCharacterId.HasValue ? [] : SelectedCharacterSkillGroups;
|
||||
|
||||
private List<CampaignLogEntry> PlayVisibleCampaignLog =>
|
||||
User is null
|
||||
@@ -1079,12 +1090,6 @@ public partial class Workspace : IAsyncDisposable
|
||||
return user.Roles.Contains(UserRoles.Admin, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private List<SkillSummary> SelectedCharacterSkills =>
|
||||
SelectedCampaign is null || !SelectedCharacterId.HasValue ? [] : SelectedCampaign.Skills.Where(skill => skill.CharacterId == SelectedCharacterId.Value).OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
|
||||
private List<SkillGroupSummary> SelectedCharacterSkillGroups =>
|
||||
SelectedCampaign is null || !SelectedCharacterId.HasValue ? [] : SelectedCampaign.SkillGroups.Where(group => group.CharacterId == SelectedCharacterId.Value).OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
|
||||
private bool IsPlayScreen => string.Equals(CurrentScreen, ScreenPlay, StringComparison.OrdinalIgnoreCase);
|
||||
private bool IsManagementScreen => string.Equals(CurrentScreen, ScreenManagement, StringComparison.OrdinalIgnoreCase);
|
||||
private bool IsAdminScreen => string.Equals(CurrentScreen, ScreenAdmin, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
@@ -20,7 +20,9 @@ public sealed record RulesetDefinition(string Id, string Name, string DiceSyntax
|
||||
|
||||
public sealed record CreateCampaignRequest(string Name, string RulesetId);
|
||||
|
||||
public sealed record CampaignDetails(Guid Id, string Name, string RulesetId, UserSummary Gm, IReadOnlyList<CharacterSummary> Characters, IReadOnlyList<SkillGroupSummary> SkillGroups, IReadOnlyList<SkillSummary> Skills);
|
||||
public sealed record CampaignSummary(Guid Id, string Name, string RulesetId, UserSummary Gm, int CharacterCount);
|
||||
|
||||
public sealed record CampaignRoster(Guid Id, string Name, string RulesetId, UserSummary Gm, IReadOnlyList<CharacterSummary> Characters);
|
||||
|
||||
public sealed record CampaignOption(Guid Id, string Name);
|
||||
|
||||
@@ -48,4 +50,6 @@ public sealed record RollDieResult(int Roll, bool Crit, bool Fumble, bool Wild,
|
||||
|
||||
public sealed record RollResult(Guid RollId, Guid CampaignId, Guid CharacterId, Guid SkillId, Guid RollerUserId, string Visibility, int Result, string Breakdown, IReadOnlyList<RollDieResult> Dice, DateTimeOffset TimestampUtc);
|
||||
|
||||
public sealed record CampaignLogEntry(Guid RollId, Guid CampaignId, Guid CharacterId, Guid SkillId, Guid RollerUserId, string Visibility, int Result, string Breakdown, IReadOnlyList<RollDieResult> Dice, DateTimeOffset TimestampUtc);
|
||||
public sealed record CharacterSheet(Guid CharacterId, IReadOnlyList<SkillGroupSummary> SkillGroups, IReadOnlyList<SkillSummary> Skills);
|
||||
|
||||
public sealed record CampaignLogEntry(Guid RollId, Guid CampaignId, Guid CharacterId, string CharacterName, Guid SkillId, string SkillName, Guid RollerUserId, string RollerDisplayName, string Visibility, int Result, string Breakdown, IReadOnlyList<RollDieResult> Dice, DateTimeOffset TimestampUtc);
|
||||
|
||||
@@ -4,9 +4,7 @@ using RpgRoller.Hosting;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddRpgRollerCore(builder.Configuration, builder.Environment);
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents()
|
||||
.AddHubOptions(options => options.MaximumReceiveMessageSize = 256 * 1024);
|
||||
builder.Services.AddRazorComponents().AddInteractiveServerComponents();
|
||||
builder.Services.AddScoped<RpgRollerApiClient>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
@@ -128,20 +128,20 @@ public sealed class GameService : IGameService
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<CampaignDetails> CreateCampaign(string sessionToken, string name, string rulesetId)
|
||||
public ServiceResult<CampaignSummary> CreateCampaign(string sessionToken, string name, string rulesetId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return ServiceResult<CampaignDetails>.Failure("invalid_campaign_name", "Campaign name is required.");
|
||||
return ServiceResult<CampaignSummary>.Failure("invalid_campaign_name", "Campaign name is required.");
|
||||
|
||||
var ruleset = DiceRules.TryParseRulesetId(rulesetId);
|
||||
if (ruleset is null)
|
||||
return ServiceResult<CampaignDetails>.Failure("invalid_ruleset", "Unknown ruleset.");
|
||||
return ServiceResult<CampaignSummary>.Failure("invalid_ruleset", "Unknown ruleset.");
|
||||
|
||||
lock (m_Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<CampaignDetails>.Failure("unauthorized", "You must be logged in.");
|
||||
return ServiceResult<CampaignSummary>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
var campaign = new Campaign
|
||||
{
|
||||
@@ -154,17 +154,17 @@ public sealed class GameService : IGameService
|
||||
|
||||
m_CampaignsById[campaign.Id] = campaign;
|
||||
PersistStateLocked();
|
||||
return ServiceResult<CampaignDetails>.Success(ToCampaignDetails(campaign));
|
||||
return ServiceResult<CampaignSummary>.Success(ToCampaignSummary(campaign));
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<IReadOnlyList<CampaignDetails>> GetCampaigns(string sessionToken)
|
||||
public ServiceResult<IReadOnlyList<CampaignSummary>> GetCampaigns(string sessionToken)
|
||||
{
|
||||
lock (m_Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<IReadOnlyList<CampaignDetails>>.Failure("unauthorized", "You must be logged in.");
|
||||
return ServiceResult<IReadOnlyList<CampaignSummary>>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
IEnumerable<Campaign> visibleCampaigns;
|
||||
if (UserHasRoleLocked(user, UserRoles.Admin))
|
||||
@@ -180,9 +180,9 @@ public sealed class GameService : IGameService
|
||||
visibleCampaigns = campaignIds.Select(campaignId => m_CampaignsById[campaignId]);
|
||||
}
|
||||
|
||||
var results = visibleCampaigns.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).Select(ToCampaignDetails).ToArray();
|
||||
var results = visibleCampaigns.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).Select(ToCampaignSummary).ToArray();
|
||||
|
||||
return ServiceResult<IReadOnlyList<CampaignDetails>>.Success(results);
|
||||
return ServiceResult<IReadOnlyList<CampaignSummary>>.Success(results);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,24 +203,16 @@ public sealed class GameService : IGameService
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<CampaignDetails> GetCampaign(string sessionToken, Guid campaignId)
|
||||
public ServiceResult<CampaignRoster> GetCampaign(string sessionToken, Guid campaignId)
|
||||
{
|
||||
lock (m_Gate)
|
||||
{
|
||||
var context = ResolveContextLocked(sessionToken, campaignId);
|
||||
if (!context.Succeeded)
|
||||
return ServiceResult<CampaignDetails>.Failure(context.Error!.Code, context.Error.Message);
|
||||
return ServiceResult<CampaignRoster>.Failure(context.Error!.Code, context.Error.Message);
|
||||
|
||||
var (_, campaign) = context.Value;
|
||||
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 ServiceResult<CampaignDetails>.Success(new(campaign.Id, campaign.Name, DiceRules.ToRulesetId(campaign.Ruleset), ToUserSummary(gm), characters, skillGroups, skills));
|
||||
return ServiceResult<CampaignRoster>.Success(ToCampaignRoster(campaign));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,6 +710,27 @@ public sealed class GameService : IGameService
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<CharacterSheet> GetCharacterSheet(string sessionToken, Guid characterId)
|
||||
{
|
||||
lock (m_Gate)
|
||||
{
|
||||
var user = ResolveUserLocked(sessionToken);
|
||||
if (user is null)
|
||||
return ServiceResult<CharacterSheet>.Failure("unauthorized", "You must be logged in.");
|
||||
|
||||
if (!m_CharactersById.TryGetValue(characterId, out var character))
|
||||
return ServiceResult<CharacterSheet>.Failure("character_not_found", "Character was not found.");
|
||||
|
||||
if (!TryResolveCharacterCampaignLocked(character, out var campaign, out var campaignError))
|
||||
return ServiceResult<CharacterSheet>.Failure(campaignError!.Code, campaignError.Message);
|
||||
|
||||
if (!CanViewCampaignLocked(user.Id, campaign.Id))
|
||||
return ServiceResult<CharacterSheet>.Failure("forbidden", "You are not a participant in this campaign.");
|
||||
|
||||
return ServiceResult<CharacterSheet>.Success(ToCharacterSheet(character.Id));
|
||||
}
|
||||
}
|
||||
|
||||
public ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility)
|
||||
{
|
||||
lock (m_Gate)
|
||||
@@ -776,7 +789,16 @@ public sealed class GameService : IGameService
|
||||
return ServiceResult<IReadOnlyList<CampaignLogEntry>>.Failure(context.Error!.Code, context.Error.Message);
|
||||
|
||||
var (user, campaign) = context.Value!;
|
||||
var entries = m_RollLog.Where(r => r.CampaignId == campaign.Id).Where(r => r.Visibility == RollVisibility.Public || r.RollerUserId == user.Id || campaign.GmUserId == user.Id).OrderBy(r => r.TimestampUtc).ThenBy(r => r.Id).Select(ToLogEntry).ToArray();
|
||||
var entries = m_RollLog
|
||||
.Where(r => r.CampaignId == campaign.Id)
|
||||
.Where(r => r.Visibility == RollVisibility.Public || r.RollerUserId == user.Id || campaign.GmUserId == user.Id)
|
||||
.OrderByDescending(r => r.TimestampUtc)
|
||||
.ThenByDescending(r => r.Id)
|
||||
.Take(CampaignLogPageSize)
|
||||
.OrderBy(r => r.TimestampUtc)
|
||||
.ThenBy(r => r.Id)
|
||||
.Select(ToLogEntry)
|
||||
.ToArray();
|
||||
|
||||
return ServiceResult<IReadOnlyList<CampaignLogEntry>>.Success(entries);
|
||||
}
|
||||
@@ -989,18 +1011,39 @@ public sealed class GameService : IGameService
|
||||
return new(campaign.Id, campaign.Name);
|
||||
}
|
||||
|
||||
private CampaignDetails ToCampaignDetails(Campaign campaign)
|
||||
private CampaignSummary ToCampaignSummary(Campaign campaign)
|
||||
{
|
||||
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();
|
||||
var gm = m_UsersById[campaign.GmUserId];
|
||||
var characterCount = m_CharactersById.Values.Count(character => character.CampaignId == campaign.Id);
|
||||
return new(campaign.Id, campaign.Name, DiceRules.ToRulesetId(campaign.Ruleset), ToUserSummary(gm), characterCount);
|
||||
}
|
||||
|
||||
return new(campaign.Id, campaign.Name, DiceRules.ToRulesetId(campaign.Ruleset), ToUserSummary(gm), characters, skillGroups, skills);
|
||||
}
|
||||
private CampaignRoster ToCampaignRoster(Campaign campaign)
|
||||
{
|
||||
var gm = m_UsersById[campaign.GmUserId];
|
||||
var characters = m_CharactersById.Values
|
||||
.Where(character => character.CampaignId == campaign.Id)
|
||||
.OrderBy(character => character.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(ToCharacterSummary)
|
||||
.ToArray();
|
||||
|
||||
return new(campaign.Id, campaign.Name, DiceRules.ToRulesetId(campaign.Ruleset), ToUserSummary(gm), characters);
|
||||
}
|
||||
|
||||
private CharacterSheet ToCharacterSheet(Guid characterId)
|
||||
{
|
||||
var skillGroups = m_SkillGroupsById.Values
|
||||
.Where(group => group.CharacterId == characterId)
|
||||
.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(ToSkillGroupSummary)
|
||||
.ToArray();
|
||||
var skills = m_SkillsById.Values
|
||||
.Where(skill => skill.CharacterId == characterId)
|
||||
.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(ToSkillSummary)
|
||||
.ToArray();
|
||||
|
||||
return new(characterId, skillGroups, skills);
|
||||
}
|
||||
|
||||
private CharacterSummary ToCharacterSummary(Character character)
|
||||
@@ -1024,11 +1067,27 @@ public sealed class GameService : IGameService
|
||||
return new(entry.Id, entry.CampaignId, entry.CharacterId, entry.SkillId, entry.RollerUserId, entry.Visibility == RollVisibility.Public ? "public" : "private", entry.Result, entry.Breakdown, dice, entry.TimestampUtc);
|
||||
}
|
||||
|
||||
private static CampaignLogEntry ToLogEntry(RollLogEntry entry)
|
||||
private CampaignLogEntry ToLogEntry(RollLogEntry entry)
|
||||
{
|
||||
var dice = DeserializeDice(entry.Dice);
|
||||
var characterName = m_CharactersById.TryGetValue(entry.CharacterId, out var character) ? character.Name : "Unknown character";
|
||||
var skillName = m_SkillsById.TryGetValue(entry.SkillId, out var skill) ? skill.Name : "Unknown skill";
|
||||
var rollerDisplayName = ResolveOwnerDisplayName(entry.RollerUserId);
|
||||
|
||||
return new(entry.Id, entry.CampaignId, entry.CharacterId, entry.SkillId, entry.RollerUserId, entry.Visibility == RollVisibility.Public ? "public" : "private", entry.Result, entry.Breakdown, dice, entry.TimestampUtc);
|
||||
return new(
|
||||
entry.Id,
|
||||
entry.CampaignId,
|
||||
entry.CharacterId,
|
||||
characterName,
|
||||
entry.SkillId,
|
||||
skillName,
|
||||
entry.RollerUserId,
|
||||
rollerDisplayName,
|
||||
entry.Visibility == RollVisibility.Public ? "public" : "private",
|
||||
entry.Result,
|
||||
entry.Breakdown,
|
||||
dice,
|
||||
entry.TimestampUtc);
|
||||
}
|
||||
|
||||
private static string SerializeDice(IReadOnlyList<RollDieResult> dice)
|
||||
@@ -1385,6 +1444,7 @@ public sealed class GameService : IGameService
|
||||
};
|
||||
}
|
||||
|
||||
private const int CampaignLogPageSize = 100;
|
||||
private static readonly JsonSerializerOptions DiceJsonOptions = new(JsonSerializerDefaults.Web);
|
||||
private readonly Dictionary<Guid, Campaign> m_CampaignsById = [];
|
||||
private readonly Dictionary<Guid, Character> m_CharactersById = [];
|
||||
|
||||
@@ -12,10 +12,10 @@ public interface IGameService
|
||||
UserSummary? GetUserBySession(string sessionToken);
|
||||
ServiceResult<MeResponse> GetMe(string sessionToken);
|
||||
|
||||
ServiceResult<CampaignDetails> CreateCampaign(string sessionToken, string name, string rulesetId);
|
||||
ServiceResult<IReadOnlyList<CampaignDetails>> GetCampaigns(string sessionToken);
|
||||
ServiceResult<CampaignSummary> CreateCampaign(string sessionToken, string name, string rulesetId);
|
||||
ServiceResult<IReadOnlyList<CampaignSummary>> GetCampaigns(string sessionToken);
|
||||
ServiceResult<IReadOnlyList<CampaignOption>> GetCharacterCampaignOptions(string sessionToken);
|
||||
ServiceResult<CampaignDetails> GetCampaign(string sessionToken, Guid campaignId);
|
||||
ServiceResult<CampaignRoster> GetCampaign(string sessionToken, Guid campaignId);
|
||||
ServiceResult<bool> DeleteCampaign(string sessionToken, Guid campaignId);
|
||||
ServiceResult<IReadOnlyList<string>> GetUsernames(string sessionToken);
|
||||
ServiceResult<IReadOnlyList<AdminUserSummary>> GetUsers(string sessionToken);
|
||||
@@ -34,6 +34,7 @@ public interface IGameService
|
||||
ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null);
|
||||
ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null);
|
||||
ServiceResult<bool> DeleteSkill(string sessionToken, Guid skillId);
|
||||
ServiceResult<CharacterSheet> GetCharacterSheet(string sessionToken, Guid characterId);
|
||||
|
||||
ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility);
|
||||
ServiceResult<IReadOnlyList<CampaignLogEntry>> GetCampaignLog(string sessionToken, Guid campaignId);
|
||||
|
||||
Reference in New Issue
Block a user