Refactor campaign payload loading

This commit is contained in:
2026-04-01 23:06:46 +02:00
parent 1c8cb71cb4
commit 8561c6643a
19 changed files with 246 additions and 152 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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; } = [];

View File

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

View File

@@ -61,8 +61,6 @@
IsCampaignDataLoading="IsCampaignDataLoading"
CampaignLog="PlayVisibleCampaignLog"
RollerLabel="RollerLabel"
SkillLabel="SkillLabel"
CharacterLabel="CharacterLabel"
LogEntryCssClass="LogEntryCssClass"
VisibilityLabel="VisibilityLabel"
VisibilityBadgeCssClass="VisibilityBadgeCssClass"/>

View File

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

View File

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

View File

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

View File

@@ -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 = [];

View File

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