From 2d1bf9b9b73da418263d9f343dde02286474fbc4 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Thu, 26 Feb 2026 09:22:29 +0100 Subject: [PATCH] Refactor Home UI controls and add dice to campaign log entries --- FAQ.md | 6 +- FRONTEND_PROGRESS.md | 4 +- README.md | 7 +- RpgRoller.Tests/Api/RollVisibilityApiTests.cs | 3 + RpgRoller.Tests/HostingCoverageTests.cs | 18 +- .../Services/ServiceSkillRollTests.cs | 2 + RpgRoller/Components/Pages/Home.razor | 159 +++---------- RpgRoller/Components/Pages/Home.razor.cs | 156 ++++--------- .../Pages/HomeControls/CampaignLogPanel.razor | 56 +++++ .../Pages/HomeControls/CharacterPanel.razor | 186 +++++++++++++++ .../Pages/HomeControls/RollDiceStrip.razor | 97 ++++++++ RpgRoller/Contracts/ApiContracts.cs | 1 + RpgRoller/Data/RpgRollerDbContext.cs | 1 + RpgRoller/Domain/GameModels.cs | 1 + .../20260226100000_AddRollLogDice.Designer.cs | 216 ++++++++++++++++++ .../20260226100000_AddRollLogDice.cs | 29 +++ .../RpgRollerDbContextModelSnapshot.cs | 4 + RpgRoller/Services/GameService.cs | 29 +++ RpgRoller/wwwroot/styles.css | 39 ++++ UX.md | 15 +- 20 files changed, 774 insertions(+), 255 deletions(-) create mode 100644 RpgRoller/Components/Pages/HomeControls/CampaignLogPanel.razor create mode 100644 RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor create mode 100644 RpgRoller/Components/Pages/HomeControls/RollDiceStrip.razor create mode 100644 RpgRoller/Migrations/20260226100000_AddRollLogDice.Designer.cs create mode 100644 RpgRoller/Migrations/20260226100000_AddRollLogDice.cs diff --git a/FAQ.md b/FAQ.md index a469677..2dfe717 100644 --- a/FAQ.md +++ b/FAQ.md @@ -62,4 +62,8 @@ d6 skills now store two explicit options: - `wildDice`: number of wild dice for the skill - `allowFumble`: whether wild dice rolling `1` can trigger fumble removal -Roll responses also include per-die state flags (`crit`, `fumble`, `wild`, `removed`, `added`) so the frontend can render the full die-by-die outcome, not just the final total. +Roll responses and campaign log entries include per-die state flags (`crit`, `fumble`, `wild`, `removed`, `added`) so the frontend can render the full die-by-die outcome, not just the final total. + +## How is the active character chosen in the Play screen? + +There is no separate activate button in Play. The selected character in the character picker is treated as active context and the UI syncs that choice to the backend for owned characters. diff --git a/FRONTEND_PROGRESS.md b/FRONTEND_PROGRESS.md index 238a73a..9a59d07 100644 --- a/FRONTEND_PROGRESS.md +++ b/FRONTEND_PROGRESS.md @@ -15,8 +15,8 @@ Tracking against `UX.md` tasks and decisions. | 9.1 App load + session restore | Implemented | Health check on load, rulesets/session load, unauthorized session reset, API unhealthy retry banner. | | 9.2 Authentication view | Implemented | Register/login cards, required validation, register password length check, server-error display. | | 9.3 Shared authenticated header | Implemented | User chip, campaign/active context, connection state, screen switch, refresh, logout. | -| 9.4 Play screen character column | Implemented | Character icon tabs, sheet, modal edit/create flows, activate action, skill list, d6 skill options (wild/fumble), roll controls, and die-state visualized last roll card. | -| 9.5 Play screen log column | Implemented | Chronological feed, private/public badges, private perspective styles (roller vs GM), local time + ISO tooltip. | +| 9.4 Play screen character column | Implemented | Character icon tabs, sheet, modal edit/create flows, selected-tab-as-active behavior, skill list, d6 skill options (wild/fumble), roll controls, and die-state visualized last roll card. | +| 9.5 Play screen log column | Implemented | Chronological feed, private/public badges, private perspective styles (roller vs GM), per-entry dice visualization with die-state flags, local time + ISO tooltip. | | 9.6 Campaign management screen | Implemented | Campaign selector/summary, create form, details card, character management actions with modal edit pattern. | | 9.7 Tablet/mobile bottom bar | Implemented | `Character` / `Log` panel switch in play screen and per-tab session persistence. | | 10 Validation and error UX | Partially implemented | Required-field and common API errors are mapped; message/code-specific mapping is limited by current API exposing only text messages. | diff --git a/README.md b/README.md index ff8e852..ed6683d 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,8 @@ Backend: Frontend: - `RpgRoller/Components/`: Blazor root app, routes, layout and page components -- `RpgRoller/Components/Pages/Home.razor(.cs)`: main UX implementation for auth/play/management screens +- `RpgRoller/Components/Pages/Home.razor(.cs)`: page composition + app state orchestration +- `RpgRoller/Components/Pages/HomeControls/`: play-screen UI controls extracted from `Home.razor` to reduce churn - `RpgRoller/wwwroot/js/rpgroller-api.js`: browser-side API + SSE + session storage interop for Blazor - `RpgRoller/wwwroot/styles.css`: responsive UX styling and theme tokens @@ -106,10 +107,10 @@ dotnet dotnet-ef migrations add --project RpgRoller/RpgRoller.cs - registration, login, logout - play screen and campaign management screen switch - campaign creation and selection - - character create/edit/activate via modal forms + - character create/edit via modal forms, with picker selection treated as active character context - skill create/edit via modal forms including d6 wild dice + allow-fumble controls - public/private rolling and campaign log viewing - - die-state visualization in Last Roll (critical, fumble, wild, removed, added) + - die-state visualization in Last Roll and Campaign Log (critical, fumble, wild, removed, added) - responsive play UX: - desktop two-column (character + log) - tablet/mobile panel switching with bottom tab bar (`Character` / `Log`) diff --git a/RpgRoller.Tests/Api/RollVisibilityApiTests.cs b/RpgRoller.Tests/Api/RollVisibilityApiTests.cs index ebb429d..b1e5042 100644 --- a/RpgRoller.Tests/Api/RollVisibilityApiTests.cs +++ b/RpgRoller.Tests/Api/RollVisibilityApiTests.cs @@ -60,13 +60,16 @@ public sealed class RollVisibilityApiTests : ApiTestBase var gmLog = await GetAsync>(gmClient, $"/api/campaigns/{campaign.Id}/log"); Assert.Equal(2, gmLog.Count); + Assert.All(gmLog, entry => Assert.NotEmpty(entry.Dice)); var playerLog = await GetAsync>(playerClient, $"/api/campaigns/{campaign.Id}/log"); Assert.Equal(2, playerLog.Count); + Assert.All(playerLog, entry => Assert.NotEmpty(entry.Dice)); var observerLog = await GetAsync>(observerClient, $"/api/campaigns/{campaign.Id}/log"); Assert.Single(observerLog); Assert.Equal("public", observerLog[0].Visibility); + Assert.NotEmpty(observerLog[0].Dice); await RegisterAsync(outsiderClient, "outsider", "Password123", "Outsider"); await LoginAsync(outsiderClient, "outsider", "Password123"); diff --git a/RpgRoller.Tests/HostingCoverageTests.cs b/RpgRoller.Tests/HostingCoverageTests.cs index c8d1dc2..9e96d1a 100644 --- a/RpgRoller.Tests/HostingCoverageTests.cs +++ b/RpgRoller.Tests/HostingCoverageTests.cs @@ -32,7 +32,7 @@ public sealed class HostingCoverageTests } [Fact] - public void SqliteSchemaUpgrader_MigratesLegacySkillsSchema() + public void SqliteSchemaUpgrader_MigratesLegacySchema() { var dbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-legacy-upgrade-{Guid.NewGuid():N}.db"); var connectionString = $"Data Source={dbPath}"; @@ -119,6 +119,17 @@ public sealed class HostingCoverageTests Assert.Contains("WildDice", columns); Assert.Contains("AllowFumble", columns); + using var rollTableInfoCommand = verifyConnection.CreateCommand(); + rollTableInfoCommand.CommandText = "PRAGMA table_info('RollLogEntries');"; + using var rollTableInfoReader = rollTableInfoCommand.ExecuteReader(); + var rollColumns = new HashSet(StringComparer.OrdinalIgnoreCase); + while (rollTableInfoReader.Read()) + { + rollColumns.Add(rollTableInfoReader.GetString(1)); + } + + Assert.Contains("Dice", rollColumns); + using var historyCommand = verifyConnection.CreateCommand(); historyCommand.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = '20260226084000_InitialSchema';"; var historyCount = Convert.ToInt32(historyCommand.ExecuteScalar()); @@ -128,5 +139,10 @@ public sealed class HostingCoverageTests modelSyncHistoryCommand.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = '20260226090000_ModelSync';"; var modelSyncHistoryCount = Convert.ToInt32(modelSyncHistoryCommand.ExecuteScalar()); Assert.Equal(1, modelSyncHistoryCount); + + using var rollDiceHistoryCommand = verifyConnection.CreateCommand(); + rollDiceHistoryCommand.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = '20260226100000_AddRollLogDice';"; + var rollDiceHistoryCount = Convert.ToInt32(rollDiceHistoryCommand.ExecuteScalar()); + Assert.Equal(1, rollDiceHistoryCount); } } diff --git a/RpgRoller.Tests/Services/ServiceSkillRollTests.cs b/RpgRoller.Tests/Services/ServiceSkillRollTests.cs index c878784..5b69b7a 100644 --- a/RpgRoller.Tests/Services/ServiceSkillRollTests.cs +++ b/RpgRoller.Tests/Services/ServiceSkillRollTests.cs @@ -65,6 +65,8 @@ public sealed class ServiceSkillRollTests Assert.Equal(2, ServiceTestSupport.GetValue(ownerLog).Count); Assert.Equal(2, ServiceTestSupport.GetValue(gmLog).Count); Assert.False(outsiderLog.Succeeded); + Assert.All(ServiceTestSupport.GetValue(ownerLog), entry => Assert.NotEmpty(entry.Dice)); + Assert.All(ServiceTestSupport.GetValue(gmLog), entry => Assert.NotEmpty(entry.Dice)); var version = service.GetCampaignVersion(ownerSession, campaign.Id); var missingVersion = service.GetCampaignVersion(ownerSession, Guid.NewGuid()); diff --git a/RpgRoller/Components/Pages/Home.razor b/RpgRoller/Components/Pages/Home.razor index 4d81d69..0fb91d5 100644 --- a/RpgRoller/Components/Pages/Home.razor +++ b/RpgRoller/Components/Pages/Home.razor @@ -1,7 +1,8 @@ @page "/" @implements IAsyncDisposable +@using RpgRoller.Components.Pages.HomeControls -
+

@LiveAnnouncement

@if (!IsInitialized) @@ -122,130 +123,39 @@ @if (CurrentScreen == "play") {
-
-

Character Context

- @if (IsCampaignDataLoading) - { -
- } - else if (SelectedCampaign is null) - { -

No campaign selected. Choose one in Campaign Management.

- } - else if (SelectedCampaign.Characters.Count == 0) - { -

No characters in this campaign yet.

- } - else - { -
- @foreach (var character in SelectedCampaign.Characters) - { - var isSelectedCharacter = SelectedCharacterId == character.Id; - - } -
- @if (SelectedCharacter is not null) - { -
-

@SelectedCharacter.Name

-

Owner: @OwnerLabel(SelectedCharacter.OwnerUserId)

-

Campaign: @SelectedCampaign.Name

- @if (SelectedCharacter.Id == ActiveCharacterId) - { - Active - } -
- - -
-
-
-
-

Skills

-
- - -
-
- @if (SelectedCharacterSkills.Count == 0) - { -

No skills for this character yet.

- } - else - { -
- @foreach (var skill in SelectedCharacterSkills) - { - var isSelectedSkill = SelectedSkillId == skill.Id; - - } -
- } -
- - - -
-
- } - } -
-

Last Roll

- @if (LastRoll is null) - { -

No roll yet.

- } - else - { -

@LastRoll.Result

- @if (LastRoll.Dice.Count > 0) - { -
- @foreach (var die in LastRoll.Dice) - { - @RollDieGlyph(die.Roll) - } -
- } -

@LastRoll.Breakdown

-

@LastRoll.Visibility

- } -
-
+ - +