Split campaign log summary from detail
This commit is contained in:
@@ -101,7 +101,8 @@ dotnet dotnet-ef migrations add <MigrationName> --project RpgRoller/RpgRoller.cs
|
|||||||
- Browser interop is in `RpgRoller/wwwroot/js/rpgroller-api.js`.
|
- Browser interop is in `RpgRoller/wwwroot/js/rpgroller-api.js`.
|
||||||
- Workspace reads are resolved server-side through scoped query services; browser interop remains for browser-only concerns such as session storage, SSE wiring, and DOM helpers.
|
- Workspace reads are resolved server-side through scoped query services; browser interop remains for browser-only concerns such as session storage, SSE wiring, and DOM helpers.
|
||||||
- Live workspace refreshes now compare separate roster, per-character sheet, and log versions so unrelated state changes do not force a full roster + sheet + log reload.
|
- Live workspace refreshes now compare separate roster, per-character sheet, and log versions so unrelated state changes do not force a full roster + sheet + log reload.
|
||||||
- Workspace campaign data is loaded in bounded slices: visible campaign summaries, a selected campaign roster, a selected character sheet, and an incremental log window backed by `/api/campaigns/{campaignId}/log/page`.
|
- Workspace campaign data is loaded in bounded slices: visible campaign summaries, a selected campaign roster, a selected character sheet, and a 50-row incremental log window backed by `/api/campaigns/{campaignId}/log/page`.
|
||||||
|
- Campaign log rows now ship compact summary data first and lazy-load dice + breakdown detail through `/api/rolls/{rollId}` only when a row is expanded.
|
||||||
- OpenAPI contract source remains at `openapi/RpgRoller.json`.
|
- OpenAPI contract source remains at `openapi/RpgRoller.json`.
|
||||||
|
|
||||||
## Test and Coverage
|
## Test and Coverage
|
||||||
|
|||||||
@@ -335,12 +335,17 @@ public sealed class CampaignApiTests : ApiTestBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
var initialPage = await GetAsync<CampaignLogPage>(gmClient, $"/api/campaigns/{campaign.Id}/log/page?limit=3");
|
var initialPage = await GetAsync<CampaignLogPage>(gmClient, $"/api/campaigns/{campaign.Id}/log/page?limit=3");
|
||||||
Assert.Equal(3, initialPage.Entries.Count);
|
Assert.Equal(3, initialPage.Entries.Length);
|
||||||
Assert.Equal(rollIds[2], initialPage.Entries[0].RollId);
|
Assert.Equal(rollIds[2], initialPage.Entries[0].RollId);
|
||||||
Assert.Equal(rollIds[^1], initialPage.Entries[^1].RollId);
|
Assert.Equal(rollIds[^1], initialPage.Entries[^1].RollId);
|
||||||
Assert.Equal(rollIds[^1], initialPage.Cursor);
|
Assert.Equal(rollIds[^1], initialPage.Cursor);
|
||||||
Assert.True(initialPage.HasMore);
|
Assert.True(initialPage.HasMore);
|
||||||
Assert.False(initialPage.ResetRequired);
|
Assert.False(initialPage.ResetRequired);
|
||||||
|
Assert.All(initialPage.Entries, entry =>
|
||||||
|
{
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(entry.SummaryText));
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(entry.VisibilityLabel));
|
||||||
|
});
|
||||||
|
|
||||||
var latestRoll = await PostAsync<RollSkillRequest, RollResult>(playerClient, $"/api/skills/{skill.Id}/roll", new("public"));
|
var latestRoll = await PostAsync<RollSkillRequest, RollResult>(playerClient, $"/api/skills/{skill.Id}/roll", new("public"));
|
||||||
var incrementalPage = await GetAsync<CampaignLogPage>(gmClient, $"/api/campaigns/{campaign.Id}/log/page?afterRollId={initialPage.Cursor}&limit=3");
|
var incrementalPage = await GetAsync<CampaignLogPage>(gmClient, $"/api/campaigns/{campaign.Id}/log/page?afterRollId={initialPage.Cursor}&limit=3");
|
||||||
@@ -350,5 +355,10 @@ public sealed class CampaignApiTests : ApiTestBase
|
|||||||
Assert.Equal(latestRoll.RollId, incrementalPage.Cursor);
|
Assert.Equal(latestRoll.RollId, incrementalPage.Cursor);
|
||||||
Assert.False(incrementalPage.HasMore);
|
Assert.False(incrementalPage.HasMore);
|
||||||
Assert.False(incrementalPage.ResetRequired);
|
Assert.False(incrementalPage.ResetRequired);
|
||||||
|
|
||||||
|
var detail = await GetAsync<CampaignRollDetail>(gmClient, $"/api/rolls/{latestRoll.RollId}");
|
||||||
|
Assert.Equal(latestRoll.RollId, detail.RollId);
|
||||||
|
Assert.Equal(latestRoll.Breakdown, detail.Breakdown);
|
||||||
|
Assert.NotEmpty(detail.Dice);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ public sealed class RollVisibilityApiTests : ApiTestBase
|
|||||||
Assert.Single(observerLogPage.Entries);
|
Assert.Single(observerLogPage.Entries);
|
||||||
Assert.Equal(publicRoll.RollId, observerLogPage.Entries[0].RollId);
|
Assert.Equal(publicRoll.RollId, observerLogPage.Entries[0].RollId);
|
||||||
Assert.Equal(publicRoll.RollId, observerLogPage.Cursor);
|
Assert.Equal(publicRoll.RollId, observerLogPage.Cursor);
|
||||||
|
Assert.Equal("Public", observerLogPage.Entries[0].VisibilityLabel);
|
||||||
|
|
||||||
|
var observerPublicDetail = await GetAsync<CampaignRollDetail>(observerClient, $"/api/rolls/{publicRoll.RollId}");
|
||||||
|
Assert.Equal(publicRoll.RollId, observerPublicDetail.RollId);
|
||||||
|
Assert.NotEmpty(observerPublicDetail.Dice);
|
||||||
|
|
||||||
|
var observerPrivateDetail = await observerClient.GetAsync($"/api/rolls/{privateRoll.RollId}");
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, observerPrivateDetail.StatusCode);
|
||||||
|
|
||||||
await RegisterAsync(outsiderClient, "outsider", "Password123", "Outsider");
|
await RegisterAsync(outsiderClient, "outsider", "Password123", "Outsider");
|
||||||
await LoginAsync(outsiderClient, "outsider", "Password123");
|
await LoginAsync(outsiderClient, "outsider", "Password123");
|
||||||
@@ -61,6 +69,9 @@ public sealed class RollVisibilityApiTests : ApiTestBase
|
|||||||
var forbiddenCampaign = await outsiderClient.GetAsync($"/api/campaigns/{campaign.Id}");
|
var forbiddenCampaign = await outsiderClient.GetAsync($"/api/campaigns/{campaign.Id}");
|
||||||
Assert.Equal(HttpStatusCode.BadRequest, forbiddenCampaign.StatusCode);
|
Assert.Equal(HttpStatusCode.BadRequest, forbiddenCampaign.StatusCode);
|
||||||
|
|
||||||
|
var outsiderPublicDetail = await outsiderClient.GetAsync($"/api/rolls/{publicRoll.RollId}");
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, outsiderPublicDetail.StatusCode);
|
||||||
|
|
||||||
var invalidVisibility = await playerClient.PostAsJsonAsync($"/api/skills/{skill.Id}/roll", new RollSkillRequest("hidden"));
|
var invalidVisibility = await playerClient.PostAsJsonAsync($"/api/skills/{skill.Id}/roll", new RollSkillRequest("hidden"));
|
||||||
Assert.Equal(HttpStatusCode.BadRequest, invalidVisibility.StatusCode);
|
Assert.Equal(HttpStatusCode.BadRequest, invalidVisibility.StatusCode);
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ public sealed class ServiceCampaignTests
|
|||||||
_ = ServiceTestSupport.GetValue(service.CreateSkill(otherSession, otherCharacter.Id, "Perception", "1D+2", 1, true));
|
_ = ServiceTestSupport.GetValue(service.CreateSkill(otherSession, otherCharacter.Id, "Perception", "1D+2", 1, true));
|
||||||
|
|
||||||
var ownerView = ServiceTestSupport.GetValue(service.GetCampaign(ownerSession, campaign.Id));
|
var ownerView = ServiceTestSupport.GetValue(service.GetCampaign(ownerSession, campaign.Id));
|
||||||
Assert.Equal(2, ownerView.Characters.Count);
|
Assert.Equal(2, ownerView.Characters.Length);
|
||||||
Assert.Contains(ownerView.Characters, character => character.Id == ownerCharacter.Id);
|
Assert.Contains(ownerView.Characters, character => character.Id == ownerCharacter.Id);
|
||||||
Assert.Contains(ownerView.Characters, character => character.Id == otherCharacter.Id);
|
Assert.Contains(ownerView.Characters, character => character.Id == otherCharacter.Id);
|
||||||
|
|
||||||
|
|||||||
@@ -96,12 +96,17 @@ public sealed class ServiceSkillRollTests
|
|||||||
rollIds.Add(ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public")).RollId);
|
rollIds.Add(ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public")).RollId);
|
||||||
|
|
||||||
var initialPage = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, limit: 3));
|
var initialPage = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, limit: 3));
|
||||||
Assert.Equal(3, initialPage.Entries.Count);
|
Assert.Equal(3, initialPage.Entries.Length);
|
||||||
Assert.Equal(rollIds[2], initialPage.Entries[0].RollId);
|
Assert.Equal(rollIds[2], initialPage.Entries[0].RollId);
|
||||||
Assert.Equal(rollIds[^1], initialPage.Entries[^1].RollId);
|
Assert.Equal(rollIds[^1], initialPage.Entries[^1].RollId);
|
||||||
Assert.Equal(rollIds[^1], initialPage.Cursor);
|
Assert.Equal(rollIds[^1], initialPage.Cursor);
|
||||||
Assert.True(initialPage.HasMore);
|
Assert.True(initialPage.HasMore);
|
||||||
Assert.False(initialPage.ResetRequired);
|
Assert.False(initialPage.ResetRequired);
|
||||||
|
Assert.All(initialPage.Entries, entry =>
|
||||||
|
{
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(entry.SummaryText));
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(entry.RollerLabel));
|
||||||
|
});
|
||||||
|
|
||||||
var latestRoll = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public"));
|
var latestRoll = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public"));
|
||||||
var incrementalPage = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, initialPage.Cursor, 3));
|
var incrementalPage = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, campaign.Id, initialPage.Cursor, 3));
|
||||||
@@ -137,9 +142,49 @@ public sealed class ServiceSkillRollTests
|
|||||||
|
|
||||||
Assert.True(gapPage.ResetRequired);
|
Assert.True(gapPage.ResetRequired);
|
||||||
Assert.True(gapPage.HasMore);
|
Assert.True(gapPage.HasMore);
|
||||||
Assert.Equal(3, gapPage.Entries.Count);
|
Assert.Equal(3, gapPage.Entries.Length);
|
||||||
Assert.Equal(rollIds[3], gapPage.Entries[0].RollId);
|
Assert.Equal(rollIds[3], gapPage.Entries[0].RollId);
|
||||||
Assert.Equal(rollIds[^1], gapPage.Entries[^1].RollId);
|
Assert.Equal(rollIds[^1], gapPage.Entries[^1].RollId);
|
||||||
Assert.Equal(rollIds[^1], gapPage.Cursor);
|
Assert.Equal(rollIds[^1], gapPage.Cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RollDetail_ReturnsVisibleDetailAndHidesPrivateRoll()
|
||||||
|
{
|
||||||
|
using var harness = ServiceTestSupport.CreateHarness(6, 5, 4, 3, 2, 6);
|
||||||
|
var service = harness.Service;
|
||||||
|
|
||||||
|
service.Register("gm-detail", "Password123", "GM");
|
||||||
|
service.Register("owner-detail", "Password123", "Owner");
|
||||||
|
service.Register("observer-detail", "Password123", "Observer");
|
||||||
|
service.Register("outsider-detail", "Password123", "Outsider");
|
||||||
|
|
||||||
|
var gmSession = ServiceTestSupport.GetValue(service.Login("gm-detail", "Password123")).SessionToken;
|
||||||
|
var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-detail", "Password123")).SessionToken;
|
||||||
|
var observerSession = ServiceTestSupport.GetValue(service.Login("observer-detail", "Password123")).SessionToken;
|
||||||
|
var outsiderSession = ServiceTestSupport.GetValue(service.Login("outsider-detail", "Password123")).SessionToken;
|
||||||
|
|
||||||
|
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Detail", "d6"));
|
||||||
|
var ownerCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Hero", campaign.Id));
|
||||||
|
_ = ServiceTestSupport.GetValue(service.CreateCharacter(observerSession, "Watcher", campaign.Id));
|
||||||
|
var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, ownerCharacter.Id, "Stealth", "2D+1", 1, true));
|
||||||
|
|
||||||
|
var privateRoll = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "private"));
|
||||||
|
var publicRoll = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, skill.Id, "public"));
|
||||||
|
|
||||||
|
var gmDetail = ServiceTestSupport.GetValue(service.GetRollDetail(gmSession, privateRoll.RollId));
|
||||||
|
var ownerDetail = ServiceTestSupport.GetValue(service.GetRollDetail(ownerSession, privateRoll.RollId));
|
||||||
|
var observerPublicDetail = ServiceTestSupport.GetValue(service.GetRollDetail(observerSession, publicRoll.RollId));
|
||||||
|
var observerPrivateDetail = service.GetRollDetail(observerSession, privateRoll.RollId);
|
||||||
|
var outsiderPublicDetail = service.GetRollDetail(outsiderSession, publicRoll.RollId);
|
||||||
|
|
||||||
|
Assert.NotEmpty(gmDetail.Dice);
|
||||||
|
Assert.Equal(privateRoll.RollId, gmDetail.RollId);
|
||||||
|
Assert.Equal(privateRoll.Breakdown, ownerDetail.Breakdown);
|
||||||
|
Assert.Equal(publicRoll.RollId, observerPublicDetail.RollId);
|
||||||
|
Assert.False(observerPrivateDetail.Succeeded);
|
||||||
|
Assert.Equal("roll_not_found", observerPrivateDetail.Error!.Code);
|
||||||
|
Assert.False(outsiderPublicDetail.Succeeded);
|
||||||
|
Assert.Equal("roll_not_found", outsiderPublicDetail.Error!.Code);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public sealed class WorkspaceQueryServiceTests
|
|||||||
GetCampaignsHandler = sessionToken =>
|
GetCampaignsHandler = sessionToken =>
|
||||||
{
|
{
|
||||||
Assert.Equal("server-session", sessionToken);
|
Assert.Equal("server-session", sessionToken);
|
||||||
return ServiceResult<IReadOnlyList<CampaignSummary>>.Success([new CampaignSummary(Guid.NewGuid(), "Alpha", "d6", new UserSummary(Guid.NewGuid(), "gm", "GM", []), 1)]);
|
return ServiceResult<IReadOnlyList<CampaignSummary>>.Success([new CampaignSummary(Guid.NewGuid(), "Alpha", "d6", new CampaignGmSummary(Guid.NewGuid(), "GM"), 1)]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -97,6 +97,7 @@ public sealed class WorkspaceQueryServiceTests
|
|||||||
public ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility) => throw new NotSupportedException();
|
public ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility) => throw new NotSupportedException();
|
||||||
public ServiceResult<IReadOnlyList<CampaignLogEntry>> GetCampaignLog(string sessionToken, Guid campaignId) => throw new NotSupportedException();
|
public ServiceResult<IReadOnlyList<CampaignLogEntry>> GetCampaignLog(string sessionToken, Guid campaignId) => throw new NotSupportedException();
|
||||||
public ServiceResult<CampaignLogPage> GetCampaignLogPage(string sessionToken, Guid campaignId, Guid? afterRollId = null, int? limit = null) => throw new NotSupportedException();
|
public ServiceResult<CampaignLogPage> GetCampaignLogPage(string sessionToken, Guid campaignId, Guid? afterRollId = null, int? limit = null) => throw new NotSupportedException();
|
||||||
|
public ServiceResult<CampaignRollDetail> GetRollDetail(string sessionToken, Guid rollId) => throw new NotSupportedException();
|
||||||
public ServiceResult<CampaignStateSnapshot> GetCampaignStateSnapshot(string sessionToken, Guid campaignId) => throw new NotSupportedException();
|
public ServiceResult<CampaignStateSnapshot> GetCampaignStateSnapshot(string sessionToken, Guid campaignId) => throw new NotSupportedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public static class ApiEndpointRegistration
|
|||||||
authenticatedApi.MapCharacterEndpoints();
|
authenticatedApi.MapCharacterEndpoints();
|
||||||
authenticatedApi.MapAdminEndpoints();
|
authenticatedApi.MapAdminEndpoints();
|
||||||
authenticatedApi.MapSkillEndpoints();
|
authenticatedApi.MapSkillEndpoints();
|
||||||
|
authenticatedApi.MapRollEndpoints();
|
||||||
authenticatedApi.MapStateEventEndpoints();
|
authenticatedApi.MapStateEventEndpoints();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
RpgRoller/Api/RollEndpoints.cs
Normal file
18
RpgRoller/Api/RollEndpoints.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using RpgRoller.Contracts;
|
||||||
|
using RpgRoller.Services;
|
||||||
|
|
||||||
|
namespace RpgRoller.Api;
|
||||||
|
|
||||||
|
internal static class RollEndpoints
|
||||||
|
{
|
||||||
|
public static RouteGroupBuilder MapRollEndpoints(this RouteGroupBuilder group)
|
||||||
|
{
|
||||||
|
group.MapGet("/rolls/{rollId:guid}", (Guid rollId, HttpContext context, IGameService game) =>
|
||||||
|
{
|
||||||
|
var result = game.GetRollDetail(context.GetRequiredSessionToken(), rollId);
|
||||||
|
return ApiResultMapper.ToApiResult(result);
|
||||||
|
});
|
||||||
|
|
||||||
|
return group;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,16 +18,37 @@
|
|||||||
@foreach (var entry in CampaignLog)
|
@foreach (var entry in CampaignLog)
|
||||||
{
|
{
|
||||||
<li class="log-entry @LogEntryCssClass(entry)">
|
<li class="log-entry @LogEntryCssClass(entry)">
|
||||||
<p><strong>@RollerLabel(entry)</strong> rolled <strong>@entry.SkillName</strong> with
|
<button type="button"
|
||||||
<strong>@entry.CharacterName</strong></p>
|
class="log-entry-toggle"
|
||||||
<p class="roll-total inline">@entry.Result</p>
|
aria-expanded="@(ExpandedRollId == entry.RollId)"
|
||||||
<RollDiceStrip Dice="entry.Dice" AriaLabel="Log roll dice"/>
|
@onclick="() => ToggleRollDetailRequested.InvokeAsync(entry.RollId)">
|
||||||
<p>@entry.Breakdown</p>
|
<p><strong>@entry.RollerLabel</strong> rolled <strong>@entry.SkillName</strong> with
|
||||||
<p class="log-meta"><span
|
<strong>@entry.CharacterName</strong></p>
|
||||||
class="badge @VisibilityBadgeCssClass(entry)">@VisibilityLabel(entry)</span>
|
<p class="roll-total inline">@entry.Result</p>
|
||||||
<time
|
<p class="log-summary">@entry.SummaryText</p>
|
||||||
title="@entry.TimestampUtc.ToString("O")">@entry.TimestampUtc.ToLocalTime().ToString("g")</time>
|
<p class="log-meta"><span class="badge @entry.VisibilityStyle">@entry.VisibilityLabel</span>
|
||||||
</p>
|
<time
|
||||||
|
title="@entry.TimestampUtc.ToString("O")">@entry.TimestampUtc.ToLocalTime().ToString("g")</time>
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
@if (ExpandedRollId == entry.RollId)
|
||||||
|
{
|
||||||
|
<div class="log-detail">
|
||||||
|
@if (IsRollDetailLoading(entry.RollId))
|
||||||
|
{
|
||||||
|
<p class="muted">Loading roll detail...</p>
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrWhiteSpace(GetRollDetailError(entry.RollId)))
|
||||||
|
{
|
||||||
|
<p class="field-error">@GetRollDetailError(entry.RollId)</p>
|
||||||
|
}
|
||||||
|
else if (ResolveRollDetail(entry.RollId) is { } detail)
|
||||||
|
{
|
||||||
|
<RollDiceStrip Dice="detail.Dice" AriaLabel="Log roll dice"/>
|
||||||
|
<p>@detail.Breakdown</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -47,17 +47,25 @@ public partial class CampaignLogPanel
|
|||||||
public bool IsCampaignDataLoading { get; set; }
|
public bool IsCampaignDataLoading { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public IReadOnlyList<CampaignLogEntry> CampaignLog { get; set; } = [];
|
public IReadOnlyList<CampaignLogListEntry> CampaignLog { get; set; } = [];
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Func<CampaignLogEntry, string> RollerLabel { get; set; } = _ => string.Empty;
|
public Guid? ExpandedRollId { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Func<CampaignLogEntry, string> LogEntryCssClass { get; set; } = _ => string.Empty;
|
public EventCallback<Guid> ToggleRollDetailRequested { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Func<CampaignLogEntry, string> VisibilityLabel { get; set; } = _ => string.Empty;
|
public Func<Guid, CampaignRollDetail?> ResolveRollDetail { get; set; } = _ => null;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Func<CampaignLogEntry, string> VisibilityBadgeCssClass { get; set; } = _ => string.Empty;
|
public Func<Guid, bool> IsRollDetailLoading { get; set; } = _ => false;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Func<Guid, string?> GetRollDetailError { get; set; } = _ => null;
|
||||||
|
|
||||||
|
private static string LogEntryCssClass(CampaignLogListEntry entry)
|
||||||
|
{
|
||||||
|
return entry.VisibilityStyle;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@if (SelectedCampaign.Characters.Count == 0)
|
@if (SelectedCampaign.Characters.Length == 0)
|
||||||
{
|
{
|
||||||
<p class="empty">No characters in this campaign yet.</p>
|
<p class="empty">No characters in this campaign yet.</p>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{
|
{
|
||||||
<p class="empty">No campaign selected. Choose one in Campaign Management.</p>
|
<p class="empty">No campaign selected. Choose one in Campaign Management.</p>
|
||||||
}
|
}
|
||||||
else if (SelectedCampaign.Characters.Count == 0)
|
else if (SelectedCampaign.Characters.Length == 0)
|
||||||
{
|
{
|
||||||
<p class="empty">No characters in this campaign yet.</p>
|
<p class="empty">No characters in this campaign yet.</p>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ public partial class CharacterPanel
|
|||||||
ShowCreateSkillModal = true;
|
ShowCreateSkillModal = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenEditSkillModal(SkillSummary skill)
|
private void OpenEditSkillModal(CharacterSheetSkill skill)
|
||||||
{
|
{
|
||||||
EditingSkillId = skill.Id;
|
EditingSkillId = skill.Id;
|
||||||
EditSkillInitialModel = new()
|
EditSkillInitialModel = new()
|
||||||
@@ -78,7 +78,7 @@ public partial class CharacterPanel
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Task OnEditSkillRequestedAsync(SkillSummary skill)
|
private Task OnEditSkillRequestedAsync(CharacterSheetSkill skill)
|
||||||
{
|
{
|
||||||
OpenEditSkillModal(skill);
|
OpenEditSkillModal(skill);
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
@@ -103,7 +103,7 @@ public partial class CharacterPanel
|
|||||||
ShowCreateSkillGroupModal = true;
|
ShowCreateSkillGroupModal = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenEditSkillGroupModal(SkillGroupSummary skillGroup)
|
private void OpenEditSkillGroupModal(CharacterSheetSkillGroup skillGroup)
|
||||||
{
|
{
|
||||||
EditingSkillGroupId = skillGroup.Id;
|
EditingSkillGroupId = skillGroup.Id;
|
||||||
SkillGroupState.Model.Name = skillGroup.Name;
|
SkillGroupState.Model.Name = skillGroup.Name;
|
||||||
@@ -242,7 +242,7 @@ public partial class CharacterPanel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool SkillMatchesFilter(SkillSummary skill)
|
private bool SkillMatchesFilter(CharacterSheetSkill skill)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(SkillFilterText))
|
if (string.IsNullOrWhiteSpace(SkillFilterText))
|
||||||
return true;
|
return true;
|
||||||
@@ -297,10 +297,10 @@ public partial class CharacterPanel
|
|||||||
public bool IsMutating { get; set; }
|
public bool IsMutating { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public IReadOnlyList<SkillSummary> SelectedCharacterSkills { get; set; } = [];
|
public IReadOnlyList<CharacterSheetSkill> SelectedCharacterSkills { get; set; } = [];
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public IReadOnlyList<SkillGroupSummary> SelectedCharacterSkillGroups { get; set; } = [];
|
public IReadOnlyList<CharacterSheetSkillGroup> SelectedCharacterSkillGroups { get; set; } = [];
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool IsD6 { get; set; }
|
public bool IsD6 { get; set; }
|
||||||
@@ -315,13 +315,13 @@ public partial class CharacterPanel
|
|||||||
public Func<Guid, string> OwnerLabel { get; set; } = _ => string.Empty;
|
public Func<Guid, string> OwnerLabel { get; set; } = _ => string.Empty;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Func<SkillSummary, string> SkillDefinitionLabel { get; set; } = _ => string.Empty;
|
public Func<CharacterSheetSkill, string> SkillDefinitionLabel { get; set; } = _ => string.Empty;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Func<CharacterSummary, bool> CanEditCharacter { get; set; } = _ => false;
|
public Func<CharacterSummary, bool> CanEditCharacter { get; set; } = _ => false;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Func<SkillSummary, bool> CanEditSkill { get; set; } = _ => false;
|
public Func<CharacterSheetSkill, bool> CanEditSkill { get; set; } = _ => false;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback<Guid> CharacterSelected { get; set; }
|
public EventCallback<Guid> CharacterSelected { get; set; }
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ public partial class SkillFormModal
|
|||||||
public Guid? EditingSkillId { get; set; }
|
public Guid? EditingSkillId { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public IReadOnlyList<SkillGroupSummary> AvailableSkillGroups { get; set; } = [];
|
public IReadOnlyList<CharacterSheetSkillGroup> AvailableSkillGroups { get; set; } = [];
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool IsMutating { get; set; }
|
public bool IsMutating { get; set; }
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public partial class SkillGroupBlock
|
|||||||
public Guid? SkillGroupId { get; set; }
|
public Guid? SkillGroupId { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public IReadOnlyList<SkillSummary> Skills { get; set; } = [];
|
public IReadOnlyList<CharacterSheetSkill> Skills { get; set; } = [];
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool IsMutating { get; set; }
|
public bool IsMutating { get; set; }
|
||||||
@@ -35,16 +35,16 @@ public partial class SkillGroupBlock
|
|||||||
public bool ShowGroupActions { get; set; }
|
public bool ShowGroupActions { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Func<SkillSummary, bool> CanEditSkill { get; set; } = _ => false;
|
public Func<CharacterSheetSkill, bool> CanEditSkill { get; set; } = _ => false;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Func<SkillSummary, string> SkillDefinitionLabel { get; set; } = _ => string.Empty;
|
public Func<CharacterSheetSkill, string> SkillDefinitionLabel { get; set; } = _ => string.Empty;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback<Guid?> AddSkillRequested { get; set; }
|
public EventCallback<Guid?> AddSkillRequested { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback<SkillSummary> EditSkillRequested { get; set; }
|
public EventCallback<CharacterSheetSkill> EditSkillRequested { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback<Guid> RollSkillRequested { get; set; }
|
public EventCallback<Guid> RollSkillRequested { get; set; }
|
||||||
|
|||||||
@@ -60,11 +60,12 @@
|
|||||||
<CampaignLogPanel
|
<CampaignLogPanel
|
||||||
IsCampaignDataLoading="IsCampaignDataLoading"
|
IsCampaignDataLoading="IsCampaignDataLoading"
|
||||||
CampaignLog="PlayVisibleCampaignLog"
|
CampaignLog="PlayVisibleCampaignLog"
|
||||||
RollerLabel="RollerLabel"
|
ExpandedRollId="ExpandedCampaignLogRollId"
|
||||||
LogEntryCssClass="LogEntryCssClass"
|
ToggleRollDetailRequested="ToggleRollDetailAsync"
|
||||||
VisibilityLabel="VisibilityLabel"
|
ResolveRollDetail="ResolveRollDetail"
|
||||||
VisibilityBadgeCssClass="VisibilityBadgeCssClass"/>
|
IsRollDetailLoading="IsRollDetailLoading"
|
||||||
</main>
|
GetRollDetailError="GetRollDetailError"/>
|
||||||
|
</main>
|
||||||
<nav class="mobile-bottom-nav" aria-label="Play panel selector">
|
<nav class="mobile-bottom-nav" aria-label="Play panel selector">
|
||||||
<button type="button" class="switch @(MobilePanel == "character" ? "active" : string.Empty)"
|
<button type="button" class="switch @(MobilePanel == "character" ? "active" : string.Empty)"
|
||||||
@onclick="SetMobilePanelCharacterAsync">Character
|
@onclick="SetMobilePanelCharacterAsync">Character
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
{
|
{
|
||||||
CampaignLog = [];
|
CampaignLog = [];
|
||||||
CampaignLogCursor = null;
|
CampaignLogCursor = null;
|
||||||
|
ResetCampaignLogDetailState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +186,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
{
|
{
|
||||||
CampaignLog = page.Entries.ToList();
|
CampaignLog = page.Entries.ToList();
|
||||||
}
|
}
|
||||||
else if (page.Entries.Count > 0)
|
else if (page.Entries.Length > 0)
|
||||||
{
|
{
|
||||||
CampaignLog.AddRange(page.Entries);
|
CampaignLog.AddRange(page.Entries);
|
||||||
if (CampaignLog.Count > CampaignLogWindowSize)
|
if (CampaignLog.Count > CampaignLogWindowSize)
|
||||||
@@ -193,6 +194,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
CampaignLogCursor = page.Cursor ?? afterRollId;
|
CampaignLogCursor = page.Cursor ?? afterRollId;
|
||||||
|
TrimCampaignLogDetails();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RefreshCampaignScopeAsync()
|
private async Task RefreshCampaignScopeAsync()
|
||||||
@@ -207,6 +209,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
ConnectionState = "offline";
|
ConnectionState = "offline";
|
||||||
CurrentCampaignState = null;
|
CurrentCampaignState = null;
|
||||||
CampaignLogCursor = null;
|
CampaignLogCursor = null;
|
||||||
|
ResetCampaignLogDetailState();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -614,6 +617,35 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ToggleRollDetailAsync(Guid rollId)
|
||||||
|
{
|
||||||
|
if (ExpandedCampaignLogRollId == rollId)
|
||||||
|
{
|
||||||
|
ExpandedCampaignLogRollId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExpandedCampaignLogRollId = rollId;
|
||||||
|
CampaignLogDetailErrors.Remove(rollId);
|
||||||
|
if (CampaignLogDetails.ContainsKey(rollId) || CampaignLogDetailsLoading.Contains(rollId))
|
||||||
|
return;
|
||||||
|
|
||||||
|
CampaignLogDetailsLoading.Add(rollId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
CampaignLogDetails[rollId] = await WorkspaceQuery.GetRollDetailAsync(rollId);
|
||||||
|
}
|
||||||
|
catch (ApiRequestException ex)
|
||||||
|
{
|
||||||
|
CampaignLogDetailErrors[rollId] = ex.Message;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
CampaignLogDetailsLoading.Remove(rollId);
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task OnSkillCreatedAsync(Guid _)
|
private async Task OnSkillCreatedAsync(Guid _)
|
||||||
{
|
{
|
||||||
await RefreshSelectedCharacterSheetAsync();
|
await RefreshSelectedCharacterSheetAsync();
|
||||||
@@ -729,13 +761,12 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanEditSkill(SkillSummary skill)
|
private bool CanEditSkill(CharacterSheetSkill skill)
|
||||||
{
|
{
|
||||||
if (SelectedCampaign is null)
|
if (SelectedCharacter is null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
var character = SelectedCampaign.Characters.FirstOrDefault(c => c.Id == skill.CharacterId);
|
return CanEditCharacter(SelectedCharacter);
|
||||||
return character is not null && CanEditCharacter(character);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[JSInvokable]
|
[JSInvokable]
|
||||||
@@ -848,7 +879,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
|
|
||||||
private void SyncSelectedCharacter()
|
private void SyncSelectedCharacter()
|
||||||
{
|
{
|
||||||
if (SelectedCampaign is null || SelectedCampaign.Characters.Count == 0)
|
if (SelectedCampaign is null || SelectedCampaign.Characters.Length == 0)
|
||||||
{
|
{
|
||||||
SelectedCharacterId = null;
|
SelectedCharacterId = null;
|
||||||
return;
|
return;
|
||||||
@@ -886,7 +917,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
return string.IsNullOrWhiteSpace(ownerDisplayName) ? "Unknown owner" : ownerDisplayName;
|
return string.IsNullOrWhiteSpace(ownerDisplayName) ? "Unknown owner" : ownerDisplayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string SkillDefinitionLabel(SkillSummary skill)
|
private string SkillDefinitionLabel(CharacterSheetSkill skill)
|
||||||
{
|
{
|
||||||
if (!string.Equals(SelectedCampaign?.RulesetId, "d6", StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(SelectedCampaign?.RulesetId, "d6", StringComparison.OrdinalIgnoreCase))
|
||||||
return skill.DiceRollDefinition;
|
return skill.DiceRollDefinition;
|
||||||
@@ -895,48 +926,44 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
return $"{skill.DiceRollDefinition}, wild {skill.WildDice}, {fumbleLabel}";
|
return $"{skill.DiceRollDefinition}, wild {skill.WildDice}, {fumbleLabel}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private string RollerLabel(CampaignLogEntry entry)
|
private CampaignRollDetail? ResolveRollDetail(Guid rollId)
|
||||||
{
|
{
|
||||||
if (User is not null && entry.RollerUserId == User.Id)
|
return CampaignLogDetails.GetValueOrDefault(rollId);
|
||||||
return "You";
|
|
||||||
|
|
||||||
if (SelectedCampaign is not null && entry.RollerUserId == SelectedCampaign.Gm.Id)
|
|
||||||
return "GM";
|
|
||||||
|
|
||||||
return entry.RollerDisplayName;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string VisibilityLabel(CampaignLogEntry entry)
|
private bool IsRollDetailLoading(Guid rollId)
|
||||||
{
|
{
|
||||||
if (!string.Equals(entry.Visibility, "private", StringComparison.OrdinalIgnoreCase))
|
return CampaignLogDetailsLoading.Contains(rollId);
|
||||||
return "Public";
|
|
||||||
|
|
||||||
if (User is not null && entry.RollerUserId == User.Id)
|
|
||||||
return "Private (you)";
|
|
||||||
|
|
||||||
return IsCurrentUserGm ? "Private (GM view)" : "Private";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string VisibilityBadgeCssClass(CampaignLogEntry entry)
|
private string? GetRollDetailError(Guid rollId)
|
||||||
{
|
{
|
||||||
if (!string.Equals(entry.Visibility, "private", StringComparison.OrdinalIgnoreCase))
|
return CampaignLogDetailErrors.GetValueOrDefault(rollId);
|
||||||
return "public";
|
|
||||||
|
|
||||||
if (User is not null && entry.RollerUserId == User.Id)
|
|
||||||
return "private-self";
|
|
||||||
|
|
||||||
return IsCurrentUserGm ? "private-gm" : "private-generic";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private string LogEntryCssClass(CampaignLogEntry entry)
|
private void ResetCampaignLogDetailState()
|
||||||
{
|
{
|
||||||
if (!string.Equals(entry.Visibility, "private", StringComparison.OrdinalIgnoreCase))
|
ExpandedCampaignLogRollId = null;
|
||||||
return "public";
|
CampaignLogDetails.Clear();
|
||||||
|
CampaignLogDetailsLoading.Clear();
|
||||||
|
CampaignLogDetailErrors.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
if (User is not null && entry.RollerUserId == User.Id)
|
private void TrimCampaignLogDetails()
|
||||||
return "private-self";
|
{
|
||||||
|
var visibleRollIds = CampaignLog.Select(entry => entry.RollId).ToHashSet();
|
||||||
|
|
||||||
return IsCurrentUserGm ? "private-gm" : "private-generic";
|
foreach (var rollId in CampaignLogDetails.Keys.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray())
|
||||||
|
CampaignLogDetails.Remove(rollId);
|
||||||
|
|
||||||
|
foreach (var rollId in CampaignLogDetailsLoading.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray())
|
||||||
|
CampaignLogDetailsLoading.Remove(rollId);
|
||||||
|
|
||||||
|
foreach (var rollId in CampaignLogDetailErrors.Keys.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray())
|
||||||
|
CampaignLogDetailErrors.Remove(rollId);
|
||||||
|
|
||||||
|
if (ExpandedCampaignLogRollId.HasValue && !visibleRollIds.Contains(ExpandedCampaignLogRollId.Value))
|
||||||
|
ExpandedCampaignLogRollId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClearAuthenticatedState()
|
private void ClearAuthenticatedState()
|
||||||
@@ -951,6 +978,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
SelectedCharacterSkillGroups = [];
|
SelectedCharacterSkillGroups = [];
|
||||||
CampaignLog = [];
|
CampaignLog = [];
|
||||||
CampaignLogCursor = null;
|
CampaignLogCursor = null;
|
||||||
|
ResetCampaignLogDetailState();
|
||||||
SelectedCharacterId = null;
|
SelectedCharacterId = null;
|
||||||
LastRoll = null;
|
LastRoll = null;
|
||||||
KnownUsernames = [];
|
KnownUsernames = [];
|
||||||
@@ -1039,9 +1067,9 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private CampaignRoster? SelectedCampaign { get; set; }
|
private CampaignRoster? SelectedCampaign { get; set; }
|
||||||
private List<CampaignSummary> Campaigns { get; set; } = [];
|
private List<CampaignSummary> Campaigns { get; set; } = [];
|
||||||
private List<CampaignOption> CharacterCampaignOptions { get; set; } = [];
|
private List<CampaignOption> CharacterCampaignOptions { get; set; } = [];
|
||||||
private List<SkillSummary> SelectedCharacterSkills { get; set; } = [];
|
private List<CharacterSheetSkill> SelectedCharacterSkills { get; set; } = [];
|
||||||
private List<SkillGroupSummary> SelectedCharacterSkillGroups { get; set; } = [];
|
private List<CharacterSheetSkillGroup> SelectedCharacterSkillGroups { get; set; } = [];
|
||||||
private List<CampaignLogEntry> CampaignLog { get; set; } = [];
|
private List<CampaignLogListEntry> CampaignLog { get; set; } = [];
|
||||||
private List<RulesetDefinition> Rulesets { get; set; } = [];
|
private List<RulesetDefinition> Rulesets { get; set; } = [];
|
||||||
private List<AdminUserSummary> AdminUsers { get; set; } = [];
|
private List<AdminUserSummary> AdminUsers { get; set; } = [];
|
||||||
private Guid? SelectedCharacterId { get; set; }
|
private Guid? SelectedCharacterId { get; set; }
|
||||||
@@ -1075,6 +1103,10 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private DotNetObjectReference<Workspace>? DotNetRef { get; set; }
|
private DotNetObjectReference<Workspace>? DotNetRef { get; set; }
|
||||||
private CampaignStateSnapshot? CurrentCampaignState { get; set; }
|
private CampaignStateSnapshot? CurrentCampaignState { get; set; }
|
||||||
private Guid? CampaignLogCursor { get; set; }
|
private Guid? CampaignLogCursor { get; set; }
|
||||||
|
private Guid? ExpandedCampaignLogRollId { get; set; }
|
||||||
|
private Dictionary<Guid, CampaignRollDetail> CampaignLogDetails { get; } = [];
|
||||||
|
private HashSet<Guid> CampaignLogDetailsLoading { get; } = [];
|
||||||
|
private Dictionary<Guid, string> CampaignLogDetailErrors { get; } = [];
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback<string?> LoggedOut { get; set; }
|
public EventCallback<string?> LoggedOut { get; set; }
|
||||||
@@ -1103,7 +1135,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
SelectedCampaign.Name,
|
SelectedCampaign.Name,
|
||||||
SelectedCampaign.RulesetId,
|
SelectedCampaign.RulesetId,
|
||||||
SelectedCampaign.Gm,
|
SelectedCampaign.Gm,
|
||||||
ownedCharacters);
|
ownedCharacters.ToArray());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1111,43 +1143,37 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (PlaySelectedCampaign is null || PlaySelectedCampaign.Characters.Count == 0)
|
var playSelectedCampaign = PlaySelectedCampaign;
|
||||||
|
if (playSelectedCampaign is null || playSelectedCampaign.Characters.Length == 0)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (SelectedCharacterId.HasValue)
|
if (SelectedCharacterId.HasValue)
|
||||||
{
|
{
|
||||||
var selectedCharacter = PlaySelectedCampaign.Characters.FirstOrDefault(character => character.Id == SelectedCharacterId.Value);
|
var selectedCharacter = playSelectedCampaign.Characters.FirstOrDefault(character => character.Id == SelectedCharacterId.Value);
|
||||||
if (selectedCharacter is not null)
|
if (selectedCharacter is not null)
|
||||||
return selectedCharacter;
|
return selectedCharacter;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ActiveCharacterId.HasValue)
|
if (ActiveCharacterId.HasValue)
|
||||||
{
|
{
|
||||||
var activeCharacter = PlaySelectedCampaign.Characters.FirstOrDefault(character => character.Id == ActiveCharacterId.Value);
|
var activeCharacter = playSelectedCampaign.Characters.FirstOrDefault(character => character.Id == ActiveCharacterId.Value);
|
||||||
if (activeCharacter is not null)
|
if (activeCharacter is not null)
|
||||||
return activeCharacter;
|
return activeCharacter;
|
||||||
}
|
}
|
||||||
|
|
||||||
return PlaySelectedCampaign.Characters[0];
|
return playSelectedCampaign.Characters[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Guid? PlaySelectedCharacterId => PlaySelectedCharacter?.Id;
|
private Guid? PlaySelectedCharacterId => PlaySelectedCharacter?.Id;
|
||||||
|
|
||||||
private List<SkillSummary> PlaySelectedCharacterSkills =>
|
private List<CharacterSheetSkill> PlaySelectedCharacterSkills =>
|
||||||
PlaySelectedCampaign is null || !PlaySelectedCharacterId.HasValue ? [] : SelectedCharacterSkills;
|
PlaySelectedCampaign is null || !PlaySelectedCharacterId.HasValue ? [] : SelectedCharacterSkills;
|
||||||
|
|
||||||
private List<SkillGroupSummary> PlaySelectedCharacterSkillGroups =>
|
private List<CharacterSheetSkillGroup> PlaySelectedCharacterSkillGroups =>
|
||||||
PlaySelectedCampaign is null || !PlaySelectedCharacterId.HasValue ? [] : SelectedCharacterSkillGroups;
|
PlaySelectedCampaign is null || !PlaySelectedCharacterId.HasValue ? [] : SelectedCharacterSkillGroups;
|
||||||
|
|
||||||
private List<CampaignLogEntry> PlayVisibleCampaignLog =>
|
private List<CampaignLogListEntry> PlayVisibleCampaignLog => CampaignLog;
|
||||||
User is null
|
|
||||||
? CampaignLog.Where(entry => !string.Equals(entry.Visibility, "private", StringComparison.OrdinalIgnoreCase)).ToList()
|
|
||||||
: CampaignLog
|
|
||||||
.Where(entry =>
|
|
||||||
!string.Equals(entry.Visibility, "private", StringComparison.OrdinalIgnoreCase) ||
|
|
||||||
entry.RollerUserId == User.Id)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
private bool IsCurrentUserGm =>
|
private bool IsCurrentUserGm =>
|
||||||
SelectedCampaign is not null && User is not null && SelectedCampaign.Gm.Id == User.Id;
|
SelectedCampaign is not null && User is not null && SelectedCampaign.Gm.Id == User.Id;
|
||||||
@@ -1210,7 +1236,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private const string CampaignSessionKey = "campaign";
|
private const string CampaignSessionKey = "campaign";
|
||||||
private const string MobilePanelSessionKey = "play-panel";
|
private const string MobilePanelSessionKey = "play-panel";
|
||||||
private const string RollVisibilitySessionKey = "roll-visibility";
|
private const string RollVisibilitySessionKey = "roll-visibility";
|
||||||
private const int CampaignLogWindowSize = 100;
|
private const int CampaignLogWindowSize = 50;
|
||||||
private const int ToastDurationMs = 3200;
|
private const int ToastDurationMs = 3200;
|
||||||
|
|
||||||
private sealed record WorkspaceToast(Guid Id, string Message, bool IsError);
|
private sealed record WorkspaceToast(Guid Id, string Message, bool IsError);
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ public sealed class WorkspaceQueryService
|
|||||||
return Task.FromResult(GetValue(m_GameService.GetCampaignLogPage(GetRequiredSessionToken(), campaignId, afterRollId, limit)));
|
return Task.FromResult(GetValue(m_GameService.GetCampaignLogPage(GetRequiredSessionToken(), campaignId, afterRollId, limit)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<CampaignRollDetail> GetRollDetailAsync(Guid rollId)
|
||||||
|
{
|
||||||
|
return Task.FromResult(GetValue(m_GameService.GetRollDetail(GetRequiredSessionToken(), rollId)));
|
||||||
|
}
|
||||||
|
|
||||||
public Task<IReadOnlyList<AdminUserSummary>> GetAdminUsersAsync()
|
public Task<IReadOnlyList<AdminUserSummary>> GetAdminUsersAsync()
|
||||||
{
|
{
|
||||||
return Task.FromResult(GetValue(m_GameService.GetUsers(GetRequiredSessionToken())));
|
return Task.FromResult(GetValue(m_GameService.GetUsers(GetRequiredSessionToken())));
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ 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, UserSummary Gm, int CharacterCount);
|
public sealed record CampaignGmSummary(Guid Id, string DisplayName);
|
||||||
|
|
||||||
public sealed record CampaignRoster(Guid Id, string Name, string RulesetId, UserSummary Gm, IReadOnlyList<CharacterSummary> Characters);
|
public sealed record CampaignSummary(Guid Id, string Name, string RulesetId, CampaignGmSummary Gm, int CharacterCount);
|
||||||
|
|
||||||
|
public sealed record CampaignRoster(Guid Id, string Name, string RulesetId, CampaignGmSummary Gm, CharacterSummary[] Characters);
|
||||||
|
|
||||||
public sealed record CampaignOption(Guid Id, string Name);
|
public sealed record CampaignOption(Guid Id, string Name);
|
||||||
|
|
||||||
@@ -50,12 +52,20 @@ 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 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 CharacterSheet(Guid CharacterId, IReadOnlyList<SkillGroupSummary> SkillGroups, IReadOnlyList<SkillSummary> Skills);
|
public sealed record CharacterSheetSkillGroup(Guid Id, string Name, string DiceRollDefinition, int WildDice, bool AllowFumble);
|
||||||
|
|
||||||
|
public sealed record CharacterSheetSkill(Guid Id, Guid? SkillGroupId, string Name, string DiceRollDefinition, int WildDice, bool AllowFumble);
|
||||||
|
|
||||||
|
public sealed record CharacterSheet(Guid CharacterId, CharacterSheetSkillGroup[] SkillGroups, CharacterSheetSkill[] 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);
|
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);
|
||||||
|
|
||||||
|
public sealed record CampaignLogListEntry(Guid RollId, string CharacterName, string SkillName, string RollerLabel, string VisibilityLabel, string VisibilityStyle, int Result, string SummaryText, DateTimeOffset TimestampUtc);
|
||||||
|
|
||||||
|
public sealed record CampaignRollDetail(Guid RollId, string Breakdown, RollDieResult[] Dice);
|
||||||
|
|
||||||
public sealed record CharacterStateVersion(Guid CharacterId, long Version);
|
public sealed record CharacterStateVersion(Guid CharacterId, long Version);
|
||||||
|
|
||||||
public sealed record CampaignStateSnapshot(Guid CampaignId, long TotalVersion, long RosterVersion, long LogVersion, IReadOnlyList<CharacterStateVersion> CharacterVersions);
|
public sealed record CampaignStateSnapshot(Guid CampaignId, long TotalVersion, long RosterVersion, long LogVersion, IReadOnlyList<CharacterStateVersion> CharacterVersions);
|
||||||
|
|
||||||
public sealed record CampaignLogPage(IReadOnlyList<CampaignLogEntry> Entries, Guid? Cursor, bool HasMore, bool ResetRequired);
|
public sealed record CampaignLogPage(CampaignLogListEntry[] Entries, Guid? Cursor, bool HasMore, bool ResetRequired);
|
||||||
|
|||||||
@@ -819,14 +819,14 @@ public sealed class GameService : IGameService
|
|||||||
|
|
||||||
if (!afterRollId.HasValue)
|
if (!afterRollId.HasValue)
|
||||||
{
|
{
|
||||||
var initialEntries = visibleEntries.TakeLast(pageSize).Select(ToLogEntry).ToArray();
|
var initialEntries = visibleEntries.TakeLast(pageSize).Select(entry => ToLogListEntry(user, campaign, entry)).ToArray();
|
||||||
return ServiceResult<CampaignLogPage>.Success(new CampaignLogPage(initialEntries, initialEntries.LastOrDefault()?.RollId, visibleEntries.Length > pageSize, false));
|
return ServiceResult<CampaignLogPage>.Success(new CampaignLogPage(initialEntries, initialEntries.LastOrDefault()?.RollId, visibleEntries.Length > pageSize, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
var afterIndex = Array.FindIndex(visibleEntries, entry => entry.Id == afterRollId.Value);
|
var afterIndex = Array.FindIndex(visibleEntries, entry => entry.Id == afterRollId.Value);
|
||||||
if (afterIndex < 0)
|
if (afterIndex < 0)
|
||||||
{
|
{
|
||||||
var replacementEntries = visibleEntries.TakeLast(pageSize).Select(ToLogEntry).ToArray();
|
var replacementEntries = visibleEntries.TakeLast(pageSize).Select(entry => ToLogListEntry(user, campaign, entry)).ToArray();
|
||||||
return ServiceResult<CampaignLogPage>.Success(new CampaignLogPage(replacementEntries, replacementEntries.LastOrDefault()?.RollId, visibleEntries.Length > pageSize, true));
|
return ServiceResult<CampaignLogPage>.Success(new CampaignLogPage(replacementEntries, replacementEntries.LastOrDefault()?.RollId, visibleEntries.Length > pageSize, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -836,15 +836,34 @@ public sealed class GameService : IGameService
|
|||||||
|
|
||||||
if (newEntries.Length > pageSize)
|
if (newEntries.Length > pageSize)
|
||||||
{
|
{
|
||||||
var replacementEntries = newEntries.TakeLast(pageSize).Select(ToLogEntry).ToArray();
|
var replacementEntries = newEntries.TakeLast(pageSize).Select(entry => ToLogListEntry(user, campaign, entry)).ToArray();
|
||||||
return ServiceResult<CampaignLogPage>.Success(new CampaignLogPage(replacementEntries, replacementEntries[^1].RollId, true, true));
|
return ServiceResult<CampaignLogPage>.Success(new CampaignLogPage(replacementEntries, replacementEntries[^1].RollId, true, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
var appendedEntries = newEntries.Select(ToLogEntry).ToArray();
|
var appendedEntries = newEntries.Select(entry => ToLogListEntry(user, campaign, entry)).ToArray();
|
||||||
return ServiceResult<CampaignLogPage>.Success(new CampaignLogPage(appendedEntries, appendedEntries[^1].RollId, false, false));
|
return ServiceResult<CampaignLogPage>.Success(new CampaignLogPage(appendedEntries, appendedEntries[^1].RollId, false, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ServiceResult<CampaignRollDetail> GetRollDetail(string sessionToken, Guid rollId)
|
||||||
|
{
|
||||||
|
lock (m_Gate)
|
||||||
|
{
|
||||||
|
var user = ResolveUserLocked(sessionToken);
|
||||||
|
if (user is null)
|
||||||
|
return ServiceResult<CampaignRollDetail>.Failure("unauthorized", "You must be logged in.");
|
||||||
|
|
||||||
|
var entry = m_RollLog.FirstOrDefault(candidate => candidate.Id == rollId);
|
||||||
|
if (entry is null)
|
||||||
|
return ServiceResult<CampaignRollDetail>.Failure("roll_not_found", "Roll was not found.");
|
||||||
|
|
||||||
|
if (!m_CampaignsById.TryGetValue(entry.CampaignId, out var campaign) || !CanViewRollLocked(user, campaign, entry))
|
||||||
|
return ServiceResult<CampaignRollDetail>.Failure("roll_not_found", "Roll was not found.");
|
||||||
|
|
||||||
|
return ServiceResult<CampaignRollDetail>.Success(new CampaignRollDetail(entry.Id, entry.Breakdown, DeserializeDice(entry.Dice).ToArray()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public ServiceResult<CampaignStateSnapshot> GetCampaignStateSnapshot(string sessionToken, Guid campaignId)
|
public ServiceResult<CampaignStateSnapshot> GetCampaignStateSnapshot(string sessionToken, Guid campaignId)
|
||||||
{
|
{
|
||||||
lock (m_Gate)
|
lock (m_Gate)
|
||||||
@@ -1056,7 +1075,7 @@ public sealed class GameService : IGameService
|
|||||||
{
|
{
|
||||||
var gm = m_UsersById[campaign.GmUserId];
|
var gm = m_UsersById[campaign.GmUserId];
|
||||||
var characterCount = m_CharactersById.Values.Count(character => character.CampaignId == campaign.Id);
|
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), ToCampaignGmSummary(gm), characterCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
private CampaignRoster ToCampaignRoster(Campaign campaign)
|
private CampaignRoster ToCampaignRoster(Campaign campaign)
|
||||||
@@ -1068,7 +1087,7 @@ public sealed class GameService : IGameService
|
|||||||
.Select(ToCharacterSummary)
|
.Select(ToCharacterSummary)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
return new(campaign.Id, campaign.Name, DiceRules.ToRulesetId(campaign.Ruleset), ToUserSummary(gm), characters);
|
return new(campaign.Id, campaign.Name, DiceRules.ToRulesetId(campaign.Ruleset), ToCampaignGmSummary(gm), characters);
|
||||||
}
|
}
|
||||||
|
|
||||||
private CharacterSheet ToCharacterSheet(Guid characterId)
|
private CharacterSheet ToCharacterSheet(Guid characterId)
|
||||||
@@ -1076,12 +1095,12 @@ public sealed class GameService : IGameService
|
|||||||
var skillGroups = m_SkillGroupsById.Values
|
var skillGroups = m_SkillGroupsById.Values
|
||||||
.Where(group => group.CharacterId == characterId)
|
.Where(group => group.CharacterId == characterId)
|
||||||
.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase)
|
.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
.Select(ToSkillGroupSummary)
|
.Select(ToCharacterSheetSkillGroup)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
var skills = m_SkillsById.Values
|
var skills = m_SkillsById.Values
|
||||||
.Where(skill => skill.CharacterId == characterId)
|
.Where(skill => skill.CharacterId == characterId)
|
||||||
.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
|
.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
.Select(ToSkillSummary)
|
.Select(ToCharacterSheetSkill)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
return new(characterId, skillGroups, skills);
|
return new(characterId, skillGroups, skills);
|
||||||
@@ -1113,11 +1132,26 @@ public sealed class GameService : IGameService
|
|||||||
.ThenBy(r => r.Id);
|
.ThenBy(r => r.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CampaignGmSummary ToCampaignGmSummary(UserAccount user)
|
||||||
|
{
|
||||||
|
return new(user.Id, user.DisplayName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CharacterSheetSkillGroup ToCharacterSheetSkillGroup(SkillGroup skillGroup)
|
||||||
|
{
|
||||||
|
return new(skillGroup.Id, skillGroup.Name, skillGroup.DiceRollDefinition, skillGroup.WildDice, skillGroup.AllowFumble);
|
||||||
|
}
|
||||||
|
|
||||||
private static SkillGroupSummary ToSkillGroupSummary(SkillGroup skillGroup)
|
private static SkillGroupSummary ToSkillGroupSummary(SkillGroup skillGroup)
|
||||||
{
|
{
|
||||||
return new(skillGroup.Id, skillGroup.CharacterId, skillGroup.Name, skillGroup.DiceRollDefinition, skillGroup.WildDice, skillGroup.AllowFumble);
|
return new(skillGroup.Id, skillGroup.CharacterId, skillGroup.Name, skillGroup.DiceRollDefinition, skillGroup.WildDice, skillGroup.AllowFumble);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CharacterSheetSkill ToCharacterSheetSkill(Skill skill)
|
||||||
|
{
|
||||||
|
return new(skill.Id, skill.SkillGroupId, skill.Name, skill.DiceRollDefinition, skill.WildDice, skill.AllowFumble);
|
||||||
|
}
|
||||||
|
|
||||||
private static SkillSummary ToSkillSummary(Skill skill)
|
private static SkillSummary ToSkillSummary(Skill skill)
|
||||||
{
|
{
|
||||||
return new(skill.Id, skill.CharacterId, skill.SkillGroupId, skill.Name, skill.DiceRollDefinition, skill.WildDice, skill.AllowFumble);
|
return new(skill.Id, skill.CharacterId, skill.SkillGroupId, skill.Name, skill.DiceRollDefinition, skill.WildDice, skill.AllowFumble);
|
||||||
@@ -1151,11 +1185,90 @@ public sealed class GameService : IGameService
|
|||||||
entry.TimestampUtc);
|
entry.TimestampUtc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CampaignLogListEntry ToLogListEntry(UserAccount user, Campaign campaign, 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";
|
||||||
|
|
||||||
|
return new(
|
||||||
|
entry.Id,
|
||||||
|
characterName,
|
||||||
|
skillName,
|
||||||
|
ResolveLogRollerLabel(user, campaign, entry),
|
||||||
|
ResolveLogVisibilityLabel(user, campaign, entry),
|
||||||
|
ResolveLogVisibilityStyle(user, campaign, entry),
|
||||||
|
entry.Result,
|
||||||
|
BuildCompactLogSummary(dice),
|
||||||
|
entry.TimestampUtc);
|
||||||
|
}
|
||||||
|
|
||||||
private static string SerializeDice(IReadOnlyList<RollDieResult> dice)
|
private static string SerializeDice(IReadOnlyList<RollDieResult> dice)
|
||||||
{
|
{
|
||||||
return JsonSerializer.Serialize(dice, DiceJsonOptions);
|
return JsonSerializer.Serialize(dice, DiceJsonOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string BuildCompactLogSummary(IReadOnlyList<RollDieResult> dice)
|
||||||
|
{
|
||||||
|
if (dice.Count == 0)
|
||||||
|
return "No detail available.";
|
||||||
|
|
||||||
|
var preview = string.Join(", ", dice.Take(3).Select(die => die.Roll.ToString()));
|
||||||
|
if (dice.Count > 3)
|
||||||
|
preview = $"{preview}, ...";
|
||||||
|
|
||||||
|
var tags = new List<string>();
|
||||||
|
if (dice.Any(die => die.Wild))
|
||||||
|
tags.Add("wild");
|
||||||
|
|
||||||
|
if (dice.Any(die => die.Crit))
|
||||||
|
tags.Add("crit");
|
||||||
|
|
||||||
|
if (dice.Any(die => die.Fumble))
|
||||||
|
tags.Add("fumble");
|
||||||
|
|
||||||
|
return tags.Count == 0 ? preview : $"{preview} | {string.Join(", ", tags)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanViewRollLocked(UserAccount user, Campaign campaign, RollLogEntry entry)
|
||||||
|
{
|
||||||
|
return CanViewCampaignLocked(user.Id, campaign.Id) &&
|
||||||
|
(entry.Visibility == RollVisibility.Public || entry.RollerUserId == user.Id || campaign.GmUserId == user.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveLogRollerLabel(UserAccount user, Campaign campaign, RollLogEntry entry)
|
||||||
|
{
|
||||||
|
if (entry.RollerUserId == user.Id)
|
||||||
|
return "You";
|
||||||
|
|
||||||
|
if (entry.RollerUserId == campaign.GmUserId)
|
||||||
|
return "GM";
|
||||||
|
|
||||||
|
return ResolveOwnerDisplayName(entry.RollerUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveLogVisibilityLabel(UserAccount user, Campaign campaign, RollLogEntry entry)
|
||||||
|
{
|
||||||
|
if (entry.Visibility != RollVisibility.Private)
|
||||||
|
return "Public";
|
||||||
|
|
||||||
|
if (entry.RollerUserId == user.Id)
|
||||||
|
return "Private (you)";
|
||||||
|
|
||||||
|
return campaign.GmUserId == user.Id ? "Private (GM view)" : "Private";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveLogVisibilityStyle(UserAccount user, Campaign campaign, RollLogEntry entry)
|
||||||
|
{
|
||||||
|
if (entry.Visibility != RollVisibility.Private)
|
||||||
|
return "public";
|
||||||
|
|
||||||
|
if (entry.RollerUserId == user.Id)
|
||||||
|
return "private-self";
|
||||||
|
|
||||||
|
return campaign.GmUserId == user.Id ? "private-gm" : "private-generic";
|
||||||
|
}
|
||||||
|
|
||||||
private string ResolveOwnerDisplayName(Guid ownerUserId)
|
private string ResolveOwnerDisplayName(Guid ownerUserId)
|
||||||
{
|
{
|
||||||
return m_UsersById.TryGetValue(ownerUserId, out var owner) && !string.IsNullOrWhiteSpace(owner.DisplayName)
|
return m_UsersById.TryGetValue(ownerUserId, out var owner) && !string.IsNullOrWhiteSpace(owner.DisplayName)
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ public interface IGameService
|
|||||||
ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility);
|
ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility);
|
||||||
ServiceResult<IReadOnlyList<CampaignLogEntry>> GetCampaignLog(string sessionToken, Guid campaignId);
|
ServiceResult<IReadOnlyList<CampaignLogEntry>> GetCampaignLog(string sessionToken, Guid campaignId);
|
||||||
ServiceResult<CampaignLogPage> GetCampaignLogPage(string sessionToken, Guid campaignId, Guid? afterRollId = null, int? limit = null);
|
ServiceResult<CampaignLogPage> GetCampaignLogPage(string sessionToken, Guid campaignId, Guid? afterRollId = null, int? limit = null);
|
||||||
|
ServiceResult<CampaignRollDetail> GetRollDetail(string sessionToken, Guid rollId);
|
||||||
|
|
||||||
ServiceResult<CampaignStateSnapshot> GetCampaignStateSnapshot(string sessionToken, Guid campaignId);
|
ServiceResult<CampaignStateSnapshot> GetCampaignStateSnapshot(string sessionToken, Guid campaignId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -581,8 +581,22 @@ select:focus-visible {
|
|||||||
.log-entry {
|
.log-entry {
|
||||||
border: 1px solid #b8a37b;
|
border: 1px solid #b8a37b;
|
||||||
border-radius: 0.55rem;
|
border-radius: 0.55rem;
|
||||||
padding: 0.5rem;
|
|
||||||
background: #f8f0de;
|
background: #f8f0de;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry-toggle {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
padding: 0.5rem;
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-entry-toggle p {
|
||||||
|
margin: 0.2rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-entry.private-self {
|
.log-entry.private-self {
|
||||||
@@ -602,6 +616,16 @@ select:focus-visible {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.log-summary {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-detail {
|
||||||
|
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
padding: 0 0.5rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
|
|||||||
Reference in New Issue
Block a user