diff --git a/README.md b/README.md index 9c0aa3d..4aed18d 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,8 @@ dotnet dotnet-ef migrations add --project RpgRoller/RpgRoller.cs - 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. - Workspace campaign data is loaded in bounded slices: visible campaign summaries, a selected campaign roster, a selected character sheet, and a 25-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. +- Campaign log rows now ship compact summary data first, including structured special-event badges for wild dice spikes, natural 1/20 results, and Rolemaster rare/fumble triggers, and lazy-load dice + breakdown detail through `/api/rolls/{rollId}` only when a row is expanded. +- Newly appended campaign-log rolls auto-expand in the play workspace, with the roll response reused as the initial detail payload for the local roller to avoid an extra detail fetch. - Hot API contracts share a source-generated `System.Text.Json` context, and HTTP JSON responses are gzip-compressed when the client advertises support. - OpenAPI contract source remains at `openapi/RpgRoller.json`. diff --git a/RpgRoller.Tests/Api/RolemasterApiTests.cs b/RpgRoller.Tests/Api/RolemasterApiTests.cs index 94ae486..892f694 100644 --- a/RpgRoller.Tests/Api/RolemasterApiTests.cs +++ b/RpgRoller.Tests/Api/RolemasterApiTests.cs @@ -34,7 +34,9 @@ public sealed class RolemasterApiTests : ApiTestBase Assert.Equal(2, logPage.Entries.Length); Assert.Equal("8 + 6 | rolemaster", logPage.Entries[0].SummaryText); + Assert.Null(logPage.Entries[0].EventBadges); Assert.Equal("74 | rolemaster", logPage.Entries[1].SummaryText); + Assert.Null(logPage.Entries[1].EventBadges); } [Fact] @@ -56,7 +58,13 @@ public sealed class RolemasterApiTests : ApiTestBase Assert.Equal(-124, roll.Result); Assert.Equal("(05) -97 -100 -12 +85 = -124", roll.Breakdown); - Assert.Equal("(05) -97 -100 -12 | open-ended low", Assert.Single(logPage.Entries).SummaryText); + var logEntry = Assert.Single(logPage.Entries); + Assert.Equal("(05) -97 -100 -12 | open-ended low", logEntry.SummaryText); + var eventBadges = Assert.IsType(logEntry.EventBadges); + Assert.Collection( + eventBadges, + badge => Assert.Equal("rf", badge), + badge => Assert.Equal("r100", badge)); Assert.Equal(roll.Breakdown, detail.Breakdown); Assert.Collection( detail.Dice, diff --git a/RpgRoller.Tests/Services/ServiceRolemasterRollTests.cs b/RpgRoller.Tests/Services/ServiceRolemasterRollTests.cs index 867975a..149ab10 100644 --- a/RpgRoller.Tests/Services/ServiceRolemasterRollTests.cs +++ b/RpgRoller.Tests/Services/ServiceRolemasterRollTests.cs @@ -56,6 +56,7 @@ public sealed class ServiceRolemasterRollTests Assert.Equal(58, roll.Result); Assert.Equal("73-15=58", roll.Breakdown); Assert.Equal("73 | rolemaster", Assert.Single(logPage.Entries).SummaryText); + Assert.Null(Assert.Single(logPage.Entries).EventBadges); var die = Assert.Single(roll.Dice); Assert.Equal(73, die.Roll); @@ -83,6 +84,7 @@ public sealed class ServiceRolemasterRollTests Assert.Equal(323, roll.Result); Assert.Equal("97+96+45+85=323", roll.Breakdown); Assert.Equal("97 + 96 + 45 | open-ended high", Assert.Single(logPage.Entries).SummaryText); + Assert.Null(Assert.Single(logPage.Entries).EventBadges); Assert.Equal(roll.Breakdown, detail.Breakdown); Assert.Collection( detail.Dice, @@ -129,7 +131,13 @@ public sealed class ServiceRolemasterRollTests Assert.Equal(-124, roll.Result); Assert.Equal("(05) -97 -100 -12 +85 = -124", roll.Breakdown); - Assert.Equal("(05) -97 -100 -12 | open-ended low", Assert.Single(logPage.Entries).SummaryText); + var logEntry = Assert.Single(logPage.Entries); + Assert.Equal("(05) -97 -100 -12 | open-ended low", logEntry.SummaryText); + var lowEventBadges = Assert.IsType(logEntry.EventBadges); + Assert.Collection( + lowEventBadges, + badge => Assert.Equal("rf", badge), + badge => Assert.Equal("r100", badge)); Assert.Collection( roll.Dice, die => @@ -161,4 +169,24 @@ public sealed class ServiceRolemasterRollTests Assert.Equal(-12, die.SignedContribution); }); } + + [Fact] + public void RollSkill_RolemasterSixtySix_AddsRareBadgeToLogSummary() + { + using var harness = ServiceTestSupport.CreateHarness(66); + var service = harness.Service; + + service.Register("gm-sixty-six", "Password123", "GM"); + var session = ServiceTestSupport.GetValue(service.Login("gm-sixty-six", "Password123")).SessionToken; + var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(session, "Rolemaster", "rolemaster")); + var character = ServiceTestSupport.GetValue(service.CreateCharacter(session, "Hero", campaign.Id)); + var skill = ServiceTestSupport.GetValue(service.CreateSkill(session, character.Id, "Perception", "d100-15", 0, false)); + + _ = ServiceTestSupport.GetValue(service.RollSkill(session, skill.Id, "public")); + var logEntry = Assert.Single(ServiceTestSupport.GetValue(service.GetCampaignLogPage(session, campaign.Id, limit: 5)).Entries); + + var badge = Assert.Single(Assert.IsType(logEntry.EventBadges)); + Assert.Equal("r66", badge); + Assert.Equal("66 | rolemaster", logEntry.SummaryText); + } } diff --git a/RpgRoller.Tests/Services/ServiceSkillRollTests.cs b/RpgRoller.Tests/Services/ServiceSkillRollTests.cs index ce09cf0..c99c767 100644 --- a/RpgRoller.Tests/Services/ServiceSkillRollTests.cs +++ b/RpgRoller.Tests/Services/ServiceSkillRollTests.cs @@ -148,6 +148,44 @@ public sealed class ServiceSkillRollTests Assert.Equal(rollIds[^1], gapPage.Cursor); } + [Fact] + public void CampaignLogPage_BuildsD6AndDndSpecialEventBadges() + { + using var harness = ServiceTestSupport.CreateHarness(6, 4, 6, 6, 2, 20, 1); + var service = harness.Service; + + service.Register("gm-special", "Password123", "GM"); + service.Register("owner-special", "Password123", "Owner"); + + var gmSession = ServiceTestSupport.GetValue(service.Login("gm-special", "Password123")).SessionToken; + var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-special", "Password123")).SessionToken; + + var d6Campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "D6 Special", "d6")); + var d6Character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Wild Hero", d6Campaign.Id)); + var d6Skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, d6Character.Id, "Stealth", "2D+1", 1, true)); + + _ = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, d6Skill.Id, "public")); + var d6Entry = Assert.Single(ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, d6Campaign.Id, limit: 5)).Entries); + var d6Badges = Assert.IsType(d6Entry.EventBadges); + Assert.Equal("w6", Assert.Single(d6Badges)); + Assert.Equal("6, 4, 6, ...", d6Entry.SummaryText); + + var dndCampaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Dnd Special", "dnd5e")); + var dndCharacter = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Natural Hero", dndCampaign.Id)); + var dndSkill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, dndCharacter.Id, "Attack", "1d20+5", 0, false)); + + _ = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, dndSkill.Id, "public")); + _ = ServiceTestSupport.GetValue(service.RollSkill(ownerSession, dndSkill.Id, "public")); + var dndEntries = ServiceTestSupport.GetValue(service.GetCampaignLogPage(gmSession, dndCampaign.Id, limit: 5)).Entries; + + var firstDndBadges = Assert.IsType(dndEntries[0].EventBadges); + Assert.Equal("n20", Assert.Single(firstDndBadges)); + Assert.Equal("20", dndEntries[0].SummaryText); + var secondDndBadges = Assert.IsType(dndEntries[1].EventBadges); + Assert.Equal("n1", Assert.Single(secondDndBadges)); + Assert.Equal("1", dndEntries[1].SummaryText); + } + [Fact] public void RollDetail_ReturnsVisibleDetailAndHidesPrivateRoll() { diff --git a/RpgRoller/Components/Pages/HomeControls/CampaignLogPanel.razor b/RpgRoller/Components/Pages/HomeControls/CampaignLogPanel.razor index d5c176a..28b9829 100644 --- a/RpgRoller/Components/Pages/HomeControls/CampaignLogPanel.razor +++ b/RpgRoller/Components/Pages/HomeControls/CampaignLogPanel.razor @@ -18,7 +18,7 @@ @foreach (var entry in CampaignLog) { var isExpanded = ExpandedRollId == entry.RollId; -
  • +