From 46a63f9e066ac994bab99e6641779ff7b38b8bda Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Sun, 5 Apr 2026 01:32:52 +0200 Subject: [PATCH] Code Cleanup --- README.md | 1 + RpgRoller.Tests/Api/AuthApiTests.cs | 2 +- RpgRoller.Tests/Api/CampaignApiTests.cs | 6 +- RpgRoller.Tests/Api/FrontendHostTests.cs | 2 +- .../Api/ResponseCompressionApiTests.cs | 2 +- RpgRoller.Tests/Api/RolemasterApiTests.cs | 54 ++--- RpgRoller.Tests/Api/RollVisibilityApiTests.cs | 2 +- RpgRoller.Tests/Api/SystemApiTests.cs | 2 +- RpgRoller.Tests/HostingCoverageTests.cs | 25 +- RpgRoller.Tests/PayloadBudgetTests.cs | 5 +- RpgRoller.Tests/Services/DiceRulesTests.cs | 2 +- .../ServiceAdminAndCampaignDeletionTests.cs | 4 +- RpgRoller.Tests/Services/ServiceAuthTests.cs | 2 +- .../Services/ServiceCampaignTests.cs | 2 +- .../Services/ServiceHelperExtractionTests.cs | 2 +- .../Services/ServicePersistenceTests.cs | 2 +- .../Services/ServiceRolemasterRollTests.cs | 139 +++++------ .../Services/ServiceRollHelperTests.cs | 106 ++++---- .../Services/ServiceSharedHelperTests.cs | 42 ++-- .../ServiceSkillGroupAndOwnershipTests.cs | 6 +- .../Services/ServiceSkillRollTests.cs | 2 +- .../ServiceStateInfrastructureTests.cs | 49 +++- .../Services/WorkspaceQueryServiceTests.cs | 228 ++++++++++++++---- .../Services/WorkspaceStateTests.cs | 43 ++-- RpgRoller.Tests/Support/ApiTestBase.cs | 2 +- RpgRoller/Api/AdminEndpoints.cs | 6 +- RpgRoller/Api/ApiEndpointRegistration.cs | 2 +- RpgRoller/Api/ApiResultMapper.cs | 2 +- RpgRoller/Api/CampaignEndpoints.cs | 2 +- RpgRoller/Api/CharacterEndpoints.cs | 2 +- RpgRoller/Api/RollEndpoints.cs | 3 +- RpgRoller/Api/SkillEndpoints.cs | 2 +- RpgRoller/Api/StateEventEndpoints.cs | 13 +- RpgRoller/Components/App.razor | 6 +- RpgRoller/Components/Layout/MainLayout.razor | 2 +- RpgRoller/Components/Pages/Home.Models.cs | 2 +- RpgRoller/Components/Pages/Home.razor | 2 +- RpgRoller/Components/Pages/Home.razor.cs | 2 +- .../Pages/HomeControls/AdminHome.razor | 2 +- .../Pages/HomeControls/AdminHome.razor.cs | 45 ++-- .../Pages/HomeControls/AppHeader.razor | 10 +- .../Pages/HomeControls/AppHeader.razor.cs | 2 +- .../Pages/HomeControls/AuthSection.razor | 2 +- .../Pages/HomeControls/CampaignLogPanel.razor | 59 ++--- .../HomeControls/CampaignLogPanel.razor.cs | 213 ++++++++-------- .../CampaignManagementPanel.razor | 2 +- .../CampaignManagementPanel.razor.cs | 2 +- .../HomeControls/CharacterFormModal.razor | 2 +- .../HomeControls/CharacterFormModal.razor.cs | 4 +- .../Pages/HomeControls/CharacterPanel.razor | 11 +- .../HomeControls/CharacterPanel.razor.cs | 39 +-- .../Pages/HomeControls/RollDiceStrip.razor | 2 +- .../Pages/HomeControls/RollDiceStrip.razor.cs | 9 +- .../Pages/HomeControls/RulesetFormHelpers.cs | 12 +- .../Pages/HomeControls/SkillFormModal.razor | 2 +- .../HomeControls/SkillFormModal.razor.cs | 20 +- .../Pages/HomeControls/SkillGroupBlock.razor | 2 +- .../HomeControls/SkillGroupBlock.razor.cs | 2 +- RpgRoller/Components/Pages/Workspace.razor | 10 +- RpgRoller/Components/Pages/Workspace.razor.cs | 132 +++++----- .../Pages/WorkspaceAdminCoordinator.cs | 21 +- .../Pages/WorkspaceCampaignCoordinator.cs | 21 +- .../WorkspaceCampaignScopeCoordinator.cs | 23 +- .../Pages/WorkspaceFeedbackService.cs | 8 +- .../Pages/WorkspaceLiveStateController.cs | 20 +- .../Pages/WorkspacePlayCoordinator.cs | 36 +-- .../Pages/WorkspaceSessionCoordinator.cs | 48 ++-- RpgRoller/Components/Pages/WorkspaceState.cs | 80 +++--- RpgRoller/Components/Pages/WorkspaceToast.cs | 2 +- RpgRoller/Components/Routes.razor | 2 +- RpgRoller/Components/RpgRollerApiClient.cs | 2 +- RpgRoller/Components/WorkspaceQueryService.cs | 4 +- .../WorkspaceSessionTokenAccessor.cs | 6 +- RpgRoller/Components/_Imports.razor | 2 +- RpgRoller/Contracts/ApiContracts.cs | 5 +- RpgRoller/Contracts/RpgRollerJson.cs | 2 +- .../RpgRollerJsonSerializerContext.cs | 2 +- RpgRoller/Data/RpgRollerDbContext.cs | 2 +- RpgRoller/Domain/GameModels.cs | 2 +- .../Hosting/ServiceCollectionExtensions.cs | 2 +- RpgRoller/Hosting/SqliteDatabaseFile.cs | 2 +- RpgRoller/Hosting/SqliteSchemaUpgrader.cs | 2 +- RpgRoller/Program.cs | 2 +- .../Services/CampaignLogSummaryBuilder.cs | 28 +-- .../Services/CustomRollOptionsResolver.cs | 8 +- RpgRoller/Services/D6RollEngine.cs | 2 +- RpgRoller/Services/DiceRules.cs | 8 +- RpgRoller/Services/GameAuthService.cs | 2 +- RpgRoller/Services/GameAuthorization.cs | 9 +- RpgRoller/Services/GameCampaignService.cs | 13 +- RpgRoller/Services/GameCharacterService.cs | 18 +- RpgRoller/Services/GameContextResolver.cs | 8 +- RpgRoller/Services/GameDtoMapper.cs | 72 +----- RpgRoller/Services/GamePersistenceService.cs | 2 +- RpgRoller/Services/GameRollService.cs | 53 +--- RpgRoller/Services/GameService.cs | 7 +- RpgRoller/Services/GameSkillService.cs | 2 +- RpgRoller/Services/GameStateCloneFactory.cs | 2 +- RpgRoller/Services/GameStateStore.cs | 28 +-- .../Services/GameUserAdministrationService.cs | 47 +--- RpgRoller/Services/IGameService.cs | 2 +- RpgRoller/Services/RoleSerializer.cs | 9 +- RpgRoller/Services/RolemasterRollEngine.cs | 19 +- RpgRoller/Services/RollBreakdownFormatter.cs | 2 +- RpgRoller/Services/RollEngine.cs | 2 +- RpgRoller/Services/RollVisibilityParser.cs | 2 +- .../Services/SkillDefinitionValidator.cs | 22 +- RpgRoller/Services/StandardRollEngine.cs | 2 +- scripts/ci-local.ps1 | 37 ++- 109 files changed, 939 insertions(+), 1125 deletions(-) diff --git a/README.md b/README.md index 51887b7..186567c 100644 --- a/README.md +++ b/README.md @@ -167,5 +167,6 @@ SQLite migration rule: ```powershell pwsh ./scripts/ci-local.ps1 ``` +- `scripts/ci-local.ps1` writes coverage collector output to a unique temporary results directory outside the repo, reads coverage from there, removes that directory at the end of the run, and sweeps stray `coverage.cobertura.xml` files from `RpgRoller.Tests/TestResults`. - Regression tests enforce payload budgets for character sheet reads, initial and incremental campaign log loads, roll mutation responses, and lazy-loaded Rolemaster roll detail payloads. - `RpgRoller.Tests/coverlet.runsettings` measures the full `RpgRoller` backend assembly. diff --git a/RpgRoller.Tests/Api/AuthApiTests.cs b/RpgRoller.Tests/Api/AuthApiTests.cs index 1852b52..151f7e0 100644 --- a/RpgRoller.Tests/Api/AuthApiTests.cs +++ b/RpgRoller.Tests/Api/AuthApiTests.cs @@ -48,4 +48,4 @@ public sealed class AuthApiTests : ApiTestBase var usernames = await GetAsync>(client, "/api/users/usernames"); Assert.Equal(["amy", "bob", "zoe"], usernames); } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/Api/CampaignApiTests.cs b/RpgRoller.Tests/Api/CampaignApiTests.cs index f1b7db3..e316ca6 100644 --- a/RpgRoller.Tests/Api/CampaignApiTests.cs +++ b/RpgRoller.Tests/Api/CampaignApiTests.cs @@ -138,7 +138,7 @@ public sealed class CampaignApiTests : ApiTestBase var groupedSkill = await PostAsync(ownerClient, $"/api/characters/{character.Id}/skills", new("Strike", "2D+1", 1, true, renamedGroup.Id)); Assert.Equal(renamedGroup.Id, groupedSkill.SkillGroupId); - var ungroupedSkill = await PutAsync(ownerClient, $"/api/skills/{groupedSkill.Id}", new("Strike", "2D+1", 1, true, null)); + var ungroupedSkill = await PutAsync(ownerClient, $"/api/skills/{groupedSkill.Id}", new("Strike", "2D+1", 1, true)); Assert.Null(ungroupedSkill.SkillGroupId); var groupedAgainSkill = await PutAsync(ownerClient, $"/api/skills/{groupedSkill.Id}", new("Strike", "2D+1", 1, true, renamedGroup.Id)); @@ -187,7 +187,7 @@ public sealed class CampaignApiTests : ApiTestBase Assert.Contains(adminEntry.Roles, role => string.Equals(role, "admin", StringComparison.OrdinalIgnoreCase)); Assert.Empty(playerEntry.Roles); - var promotedPlayer = await PutAsync(adminClient, $"/api/admin/users/{player.Id}/roles", new([ "admin" ])); + var promotedPlayer = await PutAsync(adminClient, $"/api/admin/users/{player.Id}/roles", new(["admin"])); Assert.Contains(promotedPlayer.Roles, role => string.Equals(role, "admin", StringComparison.OrdinalIgnoreCase)); var campaign = await PostAsync(gmClient, "/api/campaigns", new("Disposable Campaign", "d6")); @@ -404,4 +404,4 @@ public sealed class CampaignApiTests : ApiTestBase Assert.Equal(latestRoll.Breakdown, detail.Breakdown); Assert.NotEmpty(detail.Dice); } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/Api/FrontendHostTests.cs b/RpgRoller.Tests/Api/FrontendHostTests.cs index b7801c3..774a713 100644 --- a/RpgRoller.Tests/Api/FrontendHostTests.cs +++ b/RpgRoller.Tests/Api/FrontendHostTests.cs @@ -18,4 +18,4 @@ public sealed class FrontendHostTests : ApiTestBase Assert.Contains("_framework/blazor.web.js", html); Assert.Contains("Connecting...", html); } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/Api/ResponseCompressionApiTests.cs b/RpgRoller.Tests/Api/ResponseCompressionApiTests.cs index 83fe974..c989a30 100644 --- a/RpgRoller.Tests/Api/ResponseCompressionApiTests.cs +++ b/RpgRoller.Tests/Api/ResponseCompressionApiTests.cs @@ -24,4 +24,4 @@ public sealed class ResponseCompressionApiTests : ApiTestBase response.EnsureSuccessStatusCode(); Assert.Contains("gzip", response.Content.Headers.ContentEncoding); } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/Api/RolemasterApiTests.cs b/RpgRoller.Tests/Api/RolemasterApiTests.cs index 892f694..392bd6f 100644 --- a/RpgRoller.Tests/Api/RolemasterApiTests.cs +++ b/RpgRoller.Tests/Api/RolemasterApiTests.cs @@ -61,36 +61,28 @@ public sealed class RolemasterApiTests : ApiTestBase 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.Collection(eventBadges, badge => Assert.Equal("rf", badge), badge => Assert.Equal("r100", badge)); Assert.Equal(roll.Breakdown, detail.Breakdown); - Assert.Collection( - detail.Dice, - die => - { - Assert.Equal(RollDieKinds.RolemasterOpenEndedInitial, die.Kind); - Assert.Equal(1, die.Sequence); - Assert.Null(die.SignedContribution); - }, - die => - { - Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind); - Assert.Equal(2, die.Sequence); - Assert.Equal(-97, die.SignedContribution); - }, - die => - { - Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind); - Assert.Equal(3, die.Sequence); - Assert.Equal(-100, die.SignedContribution); - }, - die => - { - Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind); - Assert.Equal(4, die.Sequence); - Assert.Equal(-12, die.SignedContribution); - }); + Assert.Collection(detail.Dice, die => + { + Assert.Equal(RollDieKinds.RolemasterOpenEndedInitial, die.Kind); + Assert.Equal(1, die.Sequence); + Assert.Null(die.SignedContribution); + }, die => + { + Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind); + Assert.Equal(2, die.Sequence); + Assert.Equal(-97, die.SignedContribution); + }, die => + { + Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind); + Assert.Equal(3, die.Sequence); + Assert.Equal(-100, die.SignedContribution); + }, die => + { + Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind); + Assert.Equal(4, die.Sequence); + Assert.Equal(-12, die.SignedContribution); + }); } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/Api/RollVisibilityApiTests.cs b/RpgRoller.Tests/Api/RollVisibilityApiTests.cs index 1eaf1ba..4392f9a 100644 --- a/RpgRoller.Tests/Api/RollVisibilityApiTests.cs +++ b/RpgRoller.Tests/Api/RollVisibilityApiTests.cs @@ -97,4 +97,4 @@ public sealed class RollVisibilityApiTests : ApiTestBase var unauthorizedWithInvalidSession = await anonymousClient.SendAsync(invalidSessionRequest); Assert.Equal(HttpStatusCode.Unauthorized, unauthorizedWithInvalidSession.StatusCode); } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/Api/SystemApiTests.cs b/RpgRoller.Tests/Api/SystemApiTests.cs index 569e122..6ed8171 100644 --- a/RpgRoller.Tests/Api/SystemApiTests.cs +++ b/RpgRoller.Tests/Api/SystemApiTests.cs @@ -26,4 +26,4 @@ public sealed class SystemApiTests : ApiTestBase Assert.Equal(HttpStatusCode.OK, sseResponse.StatusCode); Assert.Equal("text/event-stream", sseResponse.Content.Headers.ContentType?.MediaType); } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/HostingCoverageTests.cs b/RpgRoller.Tests/HostingCoverageTests.cs index bf36d05..6f2b3d0 100644 --- a/RpgRoller.Tests/HostingCoverageTests.cs +++ b/RpgRoller.Tests/HostingCoverageTests.cs @@ -1,5 +1,5 @@ -using Microsoft.Data.Sqlite; using Microsoft.AspNetCore.Builder; +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; @@ -218,12 +218,8 @@ public sealed class HostingCoverageTests using var db = new RpgRollerDbContext(options); var migrator = db.GetService(); - var charactersScript = migrator.GenerateScript( - fromMigration: "20260226131003_AddSkillGroupPrototypes", - toMigration: "20260226160859_AddAuthorizationRolesAndCampaignDeletion"); - var rolesScript = migrator.GenerateScript( - fromMigration: "20260226160859_AddAuthorizationRolesAndCampaignDeletion", - toMigration: "20260226170000_AddAuthorizationRoles"); + var charactersScript = migrator.GenerateScript("20260226131003_AddSkillGroupPrototypes", "20260226160859_AddAuthorizationRolesAndCampaignDeletion"); + var rolesScript = migrator.GenerateScript("20260226160859_AddAuthorizationRolesAndCampaignDeletion", "20260226170000_AddAuthorizationRoles"); Assert.Contains("""CREATE TABLE "ef_temp_Characters" (""", charactersScript); Assert.DoesNotContain("""ALTER TABLE "Users" ADD "Roles" TEXT NOT NULL DEFAULT 'admin';""", charactersScript); @@ -359,7 +355,7 @@ public sealed class HostingCoverageTests { var sourceDbPath = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "RpgRoller", "App_Data", "rpgroller.development.db"); var copiedDbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-dev-copy-{Guid.NewGuid():N}.db"); - File.Copy(Path.GetFullPath(sourceDbPath), copiedDbPath, overwrite: true); + File.Copy(Path.GetFullPath(sourceDbPath), copiedDbPath, true); Guid skillId; Guid ownerUserId; @@ -427,10 +423,7 @@ public sealed class HostingCoverageTests builder.Logging.AddFilter("Microsoft.AspNetCore", LogLevel.Warning); builder.Logging.AddFilter("Microsoft.EntityFrameworkCore", LogLevel.Warning); builder.Logging.AddFilter("Microsoft.Hosting", LogLevel.Warning); - builder.Configuration.AddInMemoryCollection(new Dictionary - { - ["ConnectionStrings:RpgRoller"] = $"Data Source={copiedDbPath}" - }); + builder.Configuration.AddInMemoryCollection(new Dictionary { ["ConnectionStrings:RpgRoller"] = $"Data Source={copiedDbPath}" }); builder.Services.AddRpgRollerCore(builder.Configuration, builder.Environment); using var app = builder.Build(); @@ -450,9 +443,9 @@ public sealed class HostingCoverageTests using var countsAfterCommand = verifyConnection.CreateCommand(); countsAfterCommand.CommandText = """ - SELECT (SELECT COUNT(*) FROM Campaigns), - (SELECT COUNT(*) FROM Skills); - """; + SELECT (SELECT COUNT(*) FROM Campaigns), + (SELECT COUNT(*) FROM Skills); + """; using var countsAfterReader = countsAfterCommand.ExecuteReader(); Assert.True(countsAfterReader.Read()); Assert.Equal(campaignCountBefore, countsAfterReader.GetInt32(0)); @@ -481,4 +474,4 @@ public sealed class HostingCoverageTests var authorizationRolesHistoryCount = Convert.ToInt32(authorizationRolesHistoryCommand.ExecuteScalar()); Assert.Equal(1, authorizationRolesHistoryCount); } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/PayloadBudgetTests.cs b/RpgRoller.Tests/PayloadBudgetTests.cs index 3216e3d..a0af4ea 100644 --- a/RpgRoller.Tests/PayloadBudgetTests.cs +++ b/RpgRoller.Tests/PayloadBudgetTests.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using RpgRoller.Contracts; namespace RpgRoller.Tests; @@ -133,7 +132,7 @@ public sealed class PayloadBudgetTests [Fact] public void RolemasterRollDetailPayload_StaysWithinBudget_AndRolemasterMetadataRemainsLazy() { - using var harness = ServiceTestSupport.CreateHarness([96, 100, 100, 100, 100, 97, 12]); + using var harness = ServiceTestSupport.CreateHarness(96, 100, 100, 100, 100, 97, 12); var service = harness.Service; service.Register("gm-rm-detail-budget", "Password123", "GM"); @@ -192,4 +191,4 @@ public sealed class PayloadBudgetTests } private static readonly JsonSerializerOptions SerializerOptions = RpgRollerJson.CreateSerializerOptions(); -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/Services/DiceRulesTests.cs b/RpgRoller.Tests/Services/DiceRulesTests.cs index 0dc2a4f..7200377 100644 --- a/RpgRoller.Tests/Services/DiceRulesTests.cs +++ b/RpgRoller.Tests/Services/DiceRulesTests.cs @@ -56,4 +56,4 @@ public sealed class DiceRulesTests Assert.Equal("rolemaster", DiceRules.ToRulesetId(RulesetKind.Rolemaster)); Assert.Throws(() => DiceRules.ToRulesetId((RulesetKind)99)); } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/Services/ServiceAdminAndCampaignDeletionTests.cs b/RpgRoller.Tests/Services/ServiceAdminAndCampaignDeletionTests.cs index 5ed2690..aec9755 100644 --- a/RpgRoller.Tests/Services/ServiceAdminAndCampaignDeletionTests.cs +++ b/RpgRoller.Tests/Services/ServiceAdminAndCampaignDeletionTests.cs @@ -1,5 +1,3 @@ -using RpgRoller.Domain; - namespace RpgRoller.Tests; public sealed class ServiceAdminAndCampaignDeletionTests @@ -139,4 +137,4 @@ public sealed class ServiceAdminAndCampaignDeletionTests Assert.DoesNotContain(db.Characters, character => character.Id == gmCharacterOutsideOwnedCampaign.Id); } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/Services/ServiceAuthTests.cs b/RpgRoller.Tests/Services/ServiceAuthTests.cs index d68ab92..b63cd73 100644 --- a/RpgRoller.Tests/Services/ServiceAuthTests.cs +++ b/RpgRoller.Tests/Services/ServiceAuthTests.cs @@ -74,4 +74,4 @@ public sealed class ServiceAuthTests var usernames = ServiceTestSupport.GetValue(service.GetUsernames(session)); Assert.Equal(["amy", "bob", "zoe"], usernames); } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/Services/ServiceCampaignTests.cs b/RpgRoller.Tests/Services/ServiceCampaignTests.cs index 2c102da..e4bb2b5 100644 --- a/RpgRoller.Tests/Services/ServiceCampaignTests.cs +++ b/RpgRoller.Tests/Services/ServiceCampaignTests.cs @@ -163,4 +163,4 @@ public sealed class ServiceCampaignTests Assert.Equal(updatedCharacterVersion, Assert.Single(afterRoll.CharacterVersions, version => version.CharacterId == character.Id).Version); Assert.True(afterRoll.LogVersion > afterSkillCreate.LogVersion); } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/Services/ServiceHelperExtractionTests.cs b/RpgRoller.Tests/Services/ServiceHelperExtractionTests.cs index 3dcb2ce..fe39310 100644 --- a/RpgRoller.Tests/Services/ServiceHelperExtractionTests.cs +++ b/RpgRoller.Tests/Services/ServiceHelperExtractionTests.cs @@ -64,4 +64,4 @@ public sealed class ServiceHelperExtractionTests Assert.False(invalidRolemaster.Succeeded); Assert.Equal("invalid_fumble_range", invalidRolemaster.Error!.Code); } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/Services/ServicePersistenceTests.cs b/RpgRoller.Tests/Services/ServicePersistenceTests.cs index 15e7499..319d5b3 100644 --- a/RpgRoller.Tests/Services/ServicePersistenceTests.cs +++ b/RpgRoller.Tests/Services/ServicePersistenceTests.cs @@ -119,4 +119,4 @@ public sealed class ServicePersistenceTests var reloadedSkill = Assert.Single(reloadedSheet.Skills, current => current.Id == skill.Id); Assert.Equal(3, reloadedSkill.FumbleRange); } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/Services/ServiceRolemasterRollTests.cs b/RpgRoller.Tests/Services/ServiceRolemasterRollTests.cs index 149ab10..fca01ad 100644 --- a/RpgRoller.Tests/Services/ServiceRolemasterRollTests.cs +++ b/RpgRoller.Tests/Services/ServiceRolemasterRollTests.cs @@ -20,22 +20,19 @@ public sealed class ServiceRolemasterRollTests Assert.Equal(65, roll.Result); Assert.Equal("7+10+48=65", roll.Breakdown); Assert.Equal("7 + 10 | rolemaster", Assert.Single(logPage.Entries).SummaryText); - Assert.Collection( - roll.Dice, - die => - { - Assert.Equal(7, die.Roll); - Assert.Equal(1, die.Sequence); - Assert.Equal(RollDieKinds.RolemasterStandard, die.Kind); - Assert.Equal(7, die.SignedContribution); - }, - die => - { - Assert.Equal(10, die.Roll); - Assert.Equal(2, die.Sequence); - Assert.Equal(RollDieKinds.RolemasterStandard, die.Kind); - Assert.Equal(10, die.SignedContribution); - }); + Assert.Collection(roll.Dice, die => + { + Assert.Equal(7, die.Roll); + Assert.Equal(1, die.Sequence); + Assert.Equal(RollDieKinds.RolemasterStandard, die.Kind); + Assert.Equal(7, die.SignedContribution); + }, die => + { + Assert.Equal(10, die.Roll); + Assert.Equal(2, die.Sequence); + Assert.Equal(RollDieKinds.RolemasterStandard, die.Kind); + Assert.Equal(10, die.SignedContribution); + }); } [Fact] @@ -86,32 +83,28 @@ public sealed class ServiceRolemasterRollTests 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, - die => - { - Assert.Equal(97, die.Roll); - Assert.Equal(1, die.Sequence); - Assert.Equal(RollDieKinds.RolemasterOpenEndedInitial, die.Kind); - Assert.Equal(97, die.SignedContribution); - Assert.False(die.Added); - }, - die => - { - Assert.Equal(96, die.Roll); - Assert.Equal(2, die.Sequence); - Assert.Equal(RollDieKinds.RolemasterOpenEndedHigh, die.Kind); - Assert.Equal(96, die.SignedContribution); - Assert.True(die.Added); - }, - die => - { - Assert.Equal(45, die.Roll); - Assert.Equal(3, die.Sequence); - Assert.Equal(RollDieKinds.RolemasterOpenEndedHigh, die.Kind); - Assert.Equal(45, die.SignedContribution); - Assert.True(die.Added); - }); + Assert.Collection(detail.Dice, die => + { + Assert.Equal(97, die.Roll); + Assert.Equal(1, die.Sequence); + Assert.Equal(RollDieKinds.RolemasterOpenEndedInitial, die.Kind); + Assert.Equal(97, die.SignedContribution); + Assert.False(die.Added); + }, die => + { + Assert.Equal(96, die.Roll); + Assert.Equal(2, die.Sequence); + Assert.Equal(RollDieKinds.RolemasterOpenEndedHigh, die.Kind); + Assert.Equal(96, die.SignedContribution); + Assert.True(die.Added); + }, die => + { + Assert.Equal(45, die.Roll); + Assert.Equal(3, die.Sequence); + Assert.Equal(RollDieKinds.RolemasterOpenEndedHigh, die.Kind); + Assert.Equal(45, die.SignedContribution); + Assert.True(die.Added); + }); } [Fact] @@ -134,40 +127,32 @@ public sealed class ServiceRolemasterRollTests 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 => - { - Assert.Equal(5, die.Roll); - Assert.Equal(1, die.Sequence); - Assert.Equal(RollDieKinds.RolemasterOpenEndedInitial, die.Kind); - Assert.Null(die.SignedContribution); - }, - die => - { - Assert.Equal(97, die.Roll); - Assert.Equal(2, die.Sequence); - Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind); - Assert.Equal(-97, die.SignedContribution); - }, - die => - { - Assert.Equal(100, die.Roll); - Assert.Equal(3, die.Sequence); - Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind); - Assert.Equal(-100, die.SignedContribution); - }, - die => - { - Assert.Equal(12, die.Roll); - Assert.Equal(4, die.Sequence); - Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind); - Assert.Equal(-12, die.SignedContribution); - }); + Assert.Collection(lowEventBadges, badge => Assert.Equal("rf", badge), badge => Assert.Equal("r100", badge)); + Assert.Collection(roll.Dice, die => + { + Assert.Equal(5, die.Roll); + Assert.Equal(1, die.Sequence); + Assert.Equal(RollDieKinds.RolemasterOpenEndedInitial, die.Kind); + Assert.Null(die.SignedContribution); + }, die => + { + Assert.Equal(97, die.Roll); + Assert.Equal(2, die.Sequence); + Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind); + Assert.Equal(-97, die.SignedContribution); + }, die => + { + Assert.Equal(100, die.Roll); + Assert.Equal(3, die.Sequence); + Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind); + Assert.Equal(-100, die.SignedContribution); + }, die => + { + Assert.Equal(12, die.Roll); + Assert.Equal(4, die.Sequence); + Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind); + Assert.Equal(-12, die.SignedContribution); + }); } [Fact] @@ -189,4 +174,4 @@ public sealed class ServiceRolemasterRollTests Assert.Equal("r66", badge); Assert.Equal("66 | rolemaster", logEntry.SummaryText); } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/Services/ServiceRollHelperTests.cs b/RpgRoller.Tests/Services/ServiceRollHelperTests.cs index c7a13de..297981a 100644 --- a/RpgRoller.Tests/Services/ServiceRollHelperTests.cs +++ b/RpgRoller.Tests/Services/ServiceRollHelperTests.cs @@ -1,67 +1,7 @@ -using RpgRoller.Contracts; -using RpgRoller.Domain; -using RpgRoller.Services; - namespace RpgRoller.Tests; public sealed class ServiceRollHelperTests { - [Fact] - public void RollBreakdownFormatter_FormatsStandardAndRolemasterOpenEndedBreakdowns() - { - Assert.Equal("4+5+2=11", RollBreakdownFormatter.BuildBreakdown([4, 5], 2, 11)); - Assert.Equal("0=0", RollBreakdownFormatter.BuildBreakdown([], 0, 0)); - Assert.Equal("97+96+45+85=323", RollBreakdownFormatter.BuildRolemasterOpenEndedBreakdown(97, [96, 45], false, 85, 323)); - Assert.Equal("(05) -97 -100 -12 +85 = -124", RollBreakdownFormatter.BuildRolemasterOpenEndedBreakdown(5, [97, 100, 12], true, 85, -124)); - Assert.Equal("05", RollBreakdownFormatter.FormatRolemasterTriggerRoll(5)); - } - - [Fact] - public void CampaignLogSummaryBuilder_ExtractsExpressionsAndBuildsBadgesAndSummaries() - { - var d6Dice = new[] - { - new RollDieResult(6, true, false, true, false, false), - new RollDieResult(1, false, true, true, false, false) - }; - var rolemasterDice = new[] - { - new RollDieResult(5, false, false, false, false, false, 1, RollDieKinds.RolemasterOpenEndedInitial, null), - new RollDieResult(97, false, false, false, false, false, 2, RollDieKinds.RolemasterOpenEndedLowSubtract, -97), - new RollDieResult(100, false, false, false, false, false, 3, RollDieKinds.RolemasterOpenEndedLowSubtract, -100) - }; - - Assert.Equal("1d20+5", CampaignLogSummaryBuilder.ExtractCustomRollExpression("1d20+5 => 20+5=25", " => ")); - Assert.Null(CampaignLogSummaryBuilder.ExtractCustomRollExpression("20+5=25", " => ")); - Assert.Equal("6, 1", CampaignLogSummaryBuilder.BuildCompactLogSummary(d6Dice)); - Assert.Equal("(05) -97 -100 | open-ended low", CampaignLogSummaryBuilder.BuildCompactLogSummary(rolemasterDice)); - Assert.Equal("No detail available.", CampaignLogSummaryBuilder.BuildCompactLogSummary([])); - Assert.Equal(["w6", "w1"], Assert.IsType(CampaignLogSummaryBuilder.BuildCompactLogEventBadges(RulesetKind.D6, null, d6Dice))); - Assert.Equal(["n20"], Assert.IsType(CampaignLogSummaryBuilder.BuildCompactLogEventBadges(RulesetKind.Dnd5e, "1d20+5", [new RollDieResult(20, false, false, false, false, false)]))); - Assert.Equal(["rf", "r100"], Assert.IsType(CampaignLogSummaryBuilder.BuildCompactLogEventBadges(RulesetKind.Rolemaster, "d100!+85", rolemasterDice))); - } - - [Fact] - public void RollEngine_DelegatesToRulesetSpecificEngines() - { - var engine = new RollEngine( - new StandardRollEngine(new FixedDiceRoller([7, 10])), - new D6RollEngine(new FixedDiceRoller([6, 4, 2])), - new RolemasterRollEngine(new FixedDiceRoller([97, 96, 45]))); - - var d6Roll = engine.Roll(RulesetKind.D6, new DiceExpression(2, 6, 1, "2D+1"), 1, true, null); - Assert.Equal(13, d6Roll.Total); - Assert.Equal("6+4+2+1=13", d6Roll.Breakdown); - - var standardRoll = engine.Roll(RulesetKind.Dnd5e, new DiceExpression(2, 10, 3, "2d10+3"), 0, false, null); - Assert.Equal(20, standardRoll.Total); - Assert.Equal("7+10+3=20", standardRoll.Breakdown); - - var rolemasterRoll = engine.Roll(RulesetKind.Rolemaster, new DiceExpression(1, 100, 85, "d100!+85", DiceExpressionKind.RolemasterOpenEndedPercentile), 0, false, 5); - Assert.Equal(323, rolemasterRoll.Total); - Assert.Equal("97+96+45+85=323", rolemasterRoll.Breakdown); - } - private sealed class FixedDiceRoller : IDiceRoller { public FixedDiceRoller(IEnumerable values) @@ -77,4 +17,48 @@ public sealed class ServiceRollHelperTests private readonly Queue m_Values; } -} + + [Fact] + public void RollBreakdownFormatter_FormatsStandardAndRolemasterOpenEndedBreakdowns() + { + Assert.Equal("4+5+2=11", RollBreakdownFormatter.BuildBreakdown([4, 5], 2, 11)); + Assert.Equal("0=0", RollBreakdownFormatter.BuildBreakdown([], 0, 0)); + Assert.Equal("97+96+45+85=323", RollBreakdownFormatter.BuildRolemasterOpenEndedBreakdown(97, [96, 45], false, 85, 323)); + Assert.Equal("(05) -97 -100 -12 +85 = -124", RollBreakdownFormatter.BuildRolemasterOpenEndedBreakdown(5, [97, 100, 12], true, 85, -124)); + Assert.Equal("05", RollBreakdownFormatter.FormatRolemasterTriggerRoll(5)); + } + + [Fact] + public void CampaignLogSummaryBuilder_ExtractsExpressionsAndBuildsBadgesAndSummaries() + { + var d6Dice = new[] { new RollDieResult(6, true, false, true, false, false), new RollDieResult(1, false, true, true, false, false) }; + var rolemasterDice = new[] { new RollDieResult(5, false, false, false, false, false, 1, RollDieKinds.RolemasterOpenEndedInitial), new RollDieResult(97, false, false, false, false, false, 2, RollDieKinds.RolemasterOpenEndedLowSubtract, -97), new RollDieResult(100, false, false, false, false, false, 3, RollDieKinds.RolemasterOpenEndedLowSubtract, -100) }; + + Assert.Equal("1d20+5", CampaignLogSummaryBuilder.ExtractCustomRollExpression("1d20+5 => 20+5=25", " => ")); + Assert.Null(CampaignLogSummaryBuilder.ExtractCustomRollExpression("20+5=25", " => ")); + Assert.Equal("6, 1", CampaignLogSummaryBuilder.BuildCompactLogSummary(d6Dice)); + Assert.Equal("(05) -97 -100 | open-ended low", CampaignLogSummaryBuilder.BuildCompactLogSummary(rolemasterDice)); + Assert.Equal("No detail available.", CampaignLogSummaryBuilder.BuildCompactLogSummary([])); + Assert.Equal(["w6", "w1"], Assert.IsType(CampaignLogSummaryBuilder.BuildCompactLogEventBadges(RulesetKind.D6, null, d6Dice))); + Assert.Equal(["n20"], Assert.IsType(CampaignLogSummaryBuilder.BuildCompactLogEventBadges(RulesetKind.Dnd5e, "1d20+5", [new(20, false, false, false, false, false)]))); + Assert.Equal(["rf", "r100"], Assert.IsType(CampaignLogSummaryBuilder.BuildCompactLogEventBadges(RulesetKind.Rolemaster, "d100!+85", rolemasterDice))); + } + + [Fact] + public void RollEngine_DelegatesToRulesetSpecificEngines() + { + var engine = new RollEngine(new(new FixedDiceRoller([7, 10])), new(new FixedDiceRoller([6, 4, 2])), new(new FixedDiceRoller([97, 96, 45]))); + + var d6Roll = engine.Roll(RulesetKind.D6, new(2, 6, 1, "2D+1"), 1, true, null); + Assert.Equal(13, d6Roll.Total); + Assert.Equal("6+4+2+1=13", d6Roll.Breakdown); + + var standardRoll = engine.Roll(RulesetKind.Dnd5e, new(2, 10, 3, "2d10+3"), 0, false, null); + Assert.Equal(20, standardRoll.Total); + Assert.Equal("7+10+3=20", standardRoll.Breakdown); + + var rolemasterRoll = engine.Roll(RulesetKind.Rolemaster, new(1, 100, 85, "d100!+85", DiceExpressionKind.RolemasterOpenEndedPercentile), 0, false, 5); + Assert.Equal(323, rolemasterRoll.Total); + Assert.Equal("97+96+45+85=323", rolemasterRoll.Breakdown); + } +} \ No newline at end of file diff --git a/RpgRoller.Tests/Services/ServiceSharedHelperTests.cs b/RpgRoller.Tests/Services/ServiceSharedHelperTests.cs index b8e3e27..53ea62b 100644 --- a/RpgRoller.Tests/Services/ServiceSharedHelperTests.cs +++ b/RpgRoller.Tests/Services/ServiceSharedHelperTests.cs @@ -1,7 +1,3 @@ -using RpgRoller.Contracts; -using RpgRoller.Domain; -using RpgRoller.Services; - namespace RpgRoller.Tests; public sealed class ServiceSharedHelperTests @@ -13,7 +9,7 @@ public sealed class ServiceSharedHelperTests var characterId = Guid.NewGuid(); var store = new GameStateStore(); - store.CampaignsById[campaignId] = new Campaign + store.CampaignsById[campaignId] = new() { Id = campaignId, GmUserId = Guid.NewGuid(), @@ -21,7 +17,7 @@ public sealed class ServiceSharedHelperTests Ruleset = RulesetKind.D6, Version = 1 }; - store.CharactersById[characterId] = new Character + store.CharactersById[characterId] = new() { Id = characterId, OwnerUserId = Guid.NewGuid(), @@ -65,7 +61,7 @@ public sealed class ServiceSharedHelperTests var campaignId = Guid.NewGuid(); var store = new GameStateStore(); - store.UsersById[adminId] = new UserAccount + store.UsersById[adminId] = new() { Id = adminId, Username = "admin", @@ -74,7 +70,7 @@ public sealed class ServiceSharedHelperTests DisplayName = "Admin", Roles = UserRoles.Admin }; - store.UsersById[gmId] = new UserAccount + store.UsersById[gmId] = new() { Id = gmId, Username = "gm", @@ -83,7 +79,7 @@ public sealed class ServiceSharedHelperTests DisplayName = "GM", Roles = string.Empty }; - store.UsersById[playerId] = new UserAccount + store.UsersById[playerId] = new() { Id = playerId, Username = "player", @@ -92,7 +88,7 @@ public sealed class ServiceSharedHelperTests DisplayName = "Player", Roles = string.Empty }; - store.UsersById[outsiderId] = new UserAccount + store.UsersById[outsiderId] = new() { Id = outsiderId, Username = "outsider", @@ -112,7 +108,7 @@ public sealed class ServiceSharedHelperTests }; store.CampaignsById[campaignId] = campaign; var playerCharacterId = Guid.NewGuid(); - store.CharactersById[playerCharacterId] = new Character + store.CharactersById[playerCharacterId] = new() { Id = playerCharacterId, OwnerUserId = playerId, @@ -171,7 +167,7 @@ public sealed class ServiceSharedHelperTests var campaignId = Guid.NewGuid(); var store = new GameStateStore(); - store.UsersById[userId] = new UserAccount + store.UsersById[userId] = new() { Id = userId, Username = "user", @@ -180,7 +176,7 @@ public sealed class ServiceSharedHelperTests DisplayName = "User", Roles = string.Empty }; - store.UsersById[otherUserId] = new UserAccount + store.UsersById[otherUserId] = new() { Id = otherUserId, Username = "other", @@ -189,13 +185,13 @@ public sealed class ServiceSharedHelperTests DisplayName = "Other", Roles = string.Empty }; - store.SessionsByToken["valid"] = new UserSession + store.SessionsByToken["valid"] = new() { Token = "valid", UserId = userId, CreatedAtUtc = DateTimeOffset.UtcNow }; - store.CampaignsById[campaignId] = new Campaign + store.CampaignsById[campaignId] = new() { Id = campaignId, GmUserId = otherUserId, @@ -267,7 +263,7 @@ public sealed class ServiceSharedHelperTests var rollId = Guid.NewGuid(); var store = new GameStateStore(); - store.UsersById[gmId] = new UserAccount + store.UsersById[gmId] = new() { Id = gmId, Username = "gm", @@ -276,7 +272,7 @@ public sealed class ServiceSharedHelperTests DisplayName = "GM", Roles = UserRoles.Admin }; - store.UsersById[ownerId] = new UserAccount + store.UsersById[ownerId] = new() { Id = ownerId, Username = "owner", @@ -285,7 +281,7 @@ public sealed class ServiceSharedHelperTests DisplayName = "Owner", Roles = string.Empty }; - store.UsersById[blankOwnerId] = new UserAccount + store.UsersById[blankOwnerId] = new() { Id = blankOwnerId, Username = "blank", @@ -294,7 +290,7 @@ public sealed class ServiceSharedHelperTests DisplayName = "", Roles = string.Empty }; - store.CampaignsById[campaignId] = new Campaign + store.CampaignsById[campaignId] = new() { Id = campaignId, GmUserId = gmId, @@ -302,14 +298,14 @@ public sealed class ServiceSharedHelperTests Ruleset = RulesetKind.Rolemaster, Version = 1 }; - store.CharactersById[characterId] = new Character + store.CharactersById[characterId] = new() { Id = characterId, OwnerUserId = ownerId, CampaignId = campaignId, Name = "Scout" }; - store.SkillGroupsById[skillGroupId] = new SkillGroup + store.SkillGroupsById[skillGroupId] = new() { Id = skillGroupId, CharacterId = characterId, @@ -319,7 +315,7 @@ public sealed class ServiceSharedHelperTests AllowFumble = false, FumbleRange = 5 }; - store.SkillsById[skillId] = new Skill + store.SkillsById[skillId] = new() { Id = skillId, CharacterId = characterId, @@ -384,4 +380,4 @@ public sealed class ServiceSharedHelperTests Assert.Equal("fallback", GameDtoMapper.ResolveOwnerDisplayName(store, blankOwnerId, "fallback")); Assert.Equal("fallback", GameDtoMapper.ResolveOwnerDisplayName(store, Guid.NewGuid(), "fallback")); } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/Services/ServiceSkillGroupAndOwnershipTests.cs b/RpgRoller.Tests/Services/ServiceSkillGroupAndOwnershipTests.cs index f32e9df..d0a7cc0 100644 --- a/RpgRoller.Tests/Services/ServiceSkillGroupAndOwnershipTests.cs +++ b/RpgRoller.Tests/Services/ServiceSkillGroupAndOwnershipTests.cs @@ -36,7 +36,7 @@ public sealed class ServiceSkillGroupAndOwnershipTests var otherGroup = ServiceTestSupport.GetValue(service.CreateSkillGroup(otherSession, otherCharacter.Id, "Other Group", "2D+1", 1, true)); Assert.False(service.UpdateSkill(ownerSession, skill.Id, "Strike", "2D+1", 1, true, otherGroup.Id).Succeeded); - var ungroupedSkill = ServiceTestSupport.GetValue(service.UpdateSkill(ownerSession, skill.Id, "Strike", "2D+1", 1, true, null)); + var ungroupedSkill = ServiceTestSupport.GetValue(service.UpdateSkill(ownerSession, skill.Id, "Strike", "2D+1", 1, true)); Assert.Null(ungroupedSkill.SkillGroupId); var regroupedSkill = ServiceTestSupport.GetValue(service.UpdateSkill(ownerSession, skill.Id, "Strike", "2D+1", 1, true, renamedGroup.Id)); @@ -133,7 +133,7 @@ public sealed class ServiceSkillGroupAndOwnershipTests var adminTwo = service.GetUserBySession(adminTwoSession); Assert.NotNull(adminTwo); - _ = ServiceTestSupport.GetValue(service.UpdateUserRoles(gmSession, adminTwo!.Id, [ "admin" ])); + _ = ServiceTestSupport.GetValue(service.UpdateUserRoles(gmSession, adminTwo!.Id, ["admin"])); var adminUnlink = ServiceTestSupport.GetValue(service.UpdateCharacter(adminTwoSession, character.Id, "Admin Unlink", null)); Assert.Null(adminUnlink.CampaignId); @@ -225,4 +225,4 @@ public sealed class ServiceSkillGroupAndOwnershipTests Assert.False(openEndedSkill.AllowFumble); Assert.Equal(5, openEndedSkill.FumbleRange); } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/Services/ServiceSkillRollTests.cs b/RpgRoller.Tests/Services/ServiceSkillRollTests.cs index c2797ca..1145979 100644 --- a/RpgRoller.Tests/Services/ServiceSkillRollTests.cs +++ b/RpgRoller.Tests/Services/ServiceSkillRollTests.cs @@ -264,4 +264,4 @@ public sealed class ServiceSkillRollTests var log = ServiceTestSupport.GetValue(service.GetCampaignLog(ownerSession, campaign.Id)); Assert.Equal("Custom roll", Assert.Single(log).SkillName); } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/Services/ServiceStateInfrastructureTests.cs b/RpgRoller.Tests/Services/ServiceStateInfrastructureTests.cs index d842fb0..cec0777 100644 --- a/RpgRoller.Tests/Services/ServiceStateInfrastructureTests.cs +++ b/RpgRoller.Tests/Services/ServiceStateInfrastructureTests.cs @@ -32,11 +32,48 @@ public sealed class ServiceStateInfrastructureTests Roles = "admin", ActiveCharacterId = Guid.NewGuid() }; - var session = new UserSession { Token = "token", UserId = user.Id, CreatedAtUtc = DateTimeOffset.UtcNow }; - var campaign = new Campaign { Id = Guid.NewGuid(), GmUserId = user.Id, Name = "Main", Ruleset = RulesetKind.D6, Version = 3 }; - var character = new Character { Id = Guid.NewGuid(), OwnerUserId = user.Id, CampaignId = campaign.Id, Name = "Hero" }; - var skillGroup = new SkillGroup { Id = Guid.NewGuid(), CharacterId = character.Id, Name = "Group", DiceRollDefinition = "2D+1", WildDice = 1, AllowFumble = true, FumbleRange = null }; - var skill = new Skill { Id = Guid.NewGuid(), CharacterId = character.Id, SkillGroupId = skillGroup.Id, Name = "Skill", DiceRollDefinition = "2D+2", WildDice = 1, AllowFumble = true, FumbleRange = null }; + var session = new UserSession + { + Token = "token", + UserId = user.Id, + CreatedAtUtc = DateTimeOffset.UtcNow + }; + var campaign = new Campaign + { + Id = Guid.NewGuid(), + GmUserId = user.Id, + Name = "Main", + Ruleset = RulesetKind.D6, + Version = 3 + }; + var character = new Character + { + Id = Guid.NewGuid(), + OwnerUserId = user.Id, + CampaignId = campaign.Id, + Name = "Hero" + }; + var skillGroup = new SkillGroup + { + Id = Guid.NewGuid(), + CharacterId = character.Id, + Name = "Group", + DiceRollDefinition = "2D+1", + WildDice = 1, + AllowFumble = true, + FumbleRange = null + }; + var skill = new Skill + { + Id = Guid.NewGuid(), + CharacterId = character.Id, + SkillGroupId = skillGroup.Id, + Name = "Skill", + DiceRollDefinition = "2D+2", + WildDice = 1, + AllowFumble = true, + FumbleRange = null + }; var logEntry = new RollLogEntry { Id = Guid.NewGuid(), @@ -59,4 +96,4 @@ public sealed class ServiceStateInfrastructureTests Assert.NotSame(skill, GameStateCloneFactory.CloneSkill(skill)); Assert.NotSame(logEntry, GameStateCloneFactory.CloneRollLogEntry(logEntry)); } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/Services/WorkspaceQueryServiceTests.cs b/RpgRoller.Tests/Services/WorkspaceQueryServiceTests.cs index cb01781..fe9b979 100644 --- a/RpgRoller.Tests/Services/WorkspaceQueryServiceTests.cs +++ b/RpgRoller.Tests/Services/WorkspaceQueryServiceTests.cs @@ -1,12 +1,182 @@ using Microsoft.AspNetCore.Http; using RpgRoller.Components; -using RpgRoller.Contracts; -using RpgRoller.Services; namespace RpgRoller.Tests; public sealed class WorkspaceQueryServiceTests { + private sealed class StubGameService : IGameService + { + public IReadOnlyList GetRulesets() + { + throw new NotSupportedException(); + } + + public ServiceResult Register(string username, string password, string displayName) + { + throw new NotSupportedException(); + } + + public ServiceResult<(UserSummary User, string SessionToken)> Login(string username, string password) + { + throw new NotSupportedException(); + } + + public void Logout(string sessionToken) + { + throw new NotSupportedException(); + } + + public UserSummary? GetUserBySession(string sessionToken) + { + throw new NotSupportedException(); + } + + public ServiceResult GetMe(string sessionToken) + { + return GetMeHandler(sessionToken); + } + + public ServiceResult CreateCampaign(string sessionToken, string name, string rulesetId) + { + throw new NotSupportedException(); + } + + public ServiceResult> GetCampaigns(string sessionToken) + { + return GetCampaignsHandler(sessionToken); + } + + public ServiceResult> GetCharacterCampaignOptions(string sessionToken) + { + throw new NotSupportedException(); + } + + public ServiceResult GetCampaign(string sessionToken, Guid campaignId) + { + throw new NotSupportedException(); + } + + public ServiceResult DeleteCampaign(string sessionToken, Guid campaignId) + { + throw new NotSupportedException(); + } + + public ServiceResult> GetUsernames(string sessionToken) + { + throw new NotSupportedException(); + } + + public ServiceResult> GetUsers(string sessionToken) + { + throw new NotSupportedException(); + } + + public ServiceResult UpdateUserRoles(string sessionToken, Guid userId, IReadOnlyList roles) + { + throw new NotSupportedException(); + } + + public ServiceResult DeleteUser(string sessionToken, Guid userId) + { + throw new NotSupportedException(); + } + + public ServiceResult CreateCharacter(string sessionToken, string name, Guid campaignId) + { + throw new NotSupportedException(); + } + + public ServiceResult UpdateCharacter(string sessionToken, Guid characterId, string name, Guid? campaignId, string? ownerUsername = null) + { + throw new NotSupportedException(); + } + + public ServiceResult DeleteCharacter(string sessionToken, Guid characterId) + { + throw new NotSupportedException(); + } + + public ServiceResult ActivateCharacter(string sessionToken, Guid characterId) + { + throw new NotSupportedException(); + } + + public ServiceResult> GetOwnCharacters(string sessionToken) + { + throw new NotSupportedException(); + } + + public ServiceResult CreateSkillGroup(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null) + { + throw new NotSupportedException(); + } + + public ServiceResult UpdateSkillGroup(string sessionToken, Guid skillGroupId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null) + { + throw new NotSupportedException(); + } + + public ServiceResult DeleteSkillGroup(string sessionToken, Guid skillGroupId) + { + throw new NotSupportedException(); + } + + public ServiceResult CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null) + { + throw new NotSupportedException(); + } + + public ServiceResult UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null) + { + throw new NotSupportedException(); + } + + public ServiceResult DeleteSkill(string sessionToken, Guid skillId) + { + throw new NotSupportedException(); + } + + public ServiceResult GetCharacterSheet(string sessionToken, Guid characterId) + { + throw new NotSupportedException(); + } + + public ServiceResult RollSkill(string sessionToken, Guid skillId, string visibility) + { + throw new NotSupportedException(); + } + + public ServiceResult RollCustom(string sessionToken, Guid characterId, string expression, string visibility) + { + throw new NotSupportedException(); + } + + public ServiceResult> GetCampaignLog(string sessionToken, Guid campaignId) + { + throw new NotSupportedException(); + } + + public ServiceResult GetCampaignLogPage(string sessionToken, Guid campaignId, Guid? afterRollId = null, int? limit = null) + { + throw new NotSupportedException(); + } + + public ServiceResult GetRollDetail(string sessionToken, Guid rollId) + { + throw new NotSupportedException(); + } + + public ServiceResult GetCampaignStateSnapshot(string sessionToken, Guid campaignId) + { + throw new NotSupportedException(); + } + + public Func> GetMeHandler { get; init; } = _ => ServiceResult.Failure("unexpected_call", "Unexpected GetMe call."); + + public Func>> GetCampaignsHandler { get; init; } = _ => ServiceResult>.Failure("unexpected_call", "Unexpected GetCampaigns call."); + } + [Fact] public void SessionTokenAccessor_ReadsSessionCookieFromHttpContext() { @@ -27,7 +197,7 @@ public sealed class WorkspaceQueryServiceTests GetCampaignsHandler = sessionToken => { Assert.Equal("server-session", sessionToken); - return ServiceResult>.Success([new CampaignSummary(Guid.NewGuid(), "Alpha", "d6", new CampaignGmSummary(Guid.NewGuid(), "GM"), 1)]); + return ServiceResult>.Success([new(Guid.NewGuid(), "Alpha", "d6", new(Guid.NewGuid(), "GM"), 1)]); } }; @@ -40,10 +210,7 @@ public sealed class WorkspaceQueryServiceTests [Fact] public async Task GetMeAsync_MapsUnauthorizedServiceResultToApiRequestException() { - var service = new StubGameService - { - GetMeHandler = _ => ServiceResult.Failure("unauthorized", "You must be logged in.") - }; + var service = new StubGameService { GetMeHandler = _ => ServiceResult.Failure("unauthorized", "You must be logged in.") }; var queryService = new WorkspaceQueryService(service, CreateSessionTokenAccessor("expired-session")); var exception = await Assert.ThrowsAsync(queryService.GetMeAsync); @@ -57,49 +224,6 @@ public sealed class WorkspaceQueryServiceTests { var httpContext = new DefaultHttpContext(); httpContext.Request.Headers.Cookie = $"rpgroller_session={sessionToken}"; - return new WorkspaceSessionTokenAccessor(new HttpContextAccessor { HttpContext = httpContext }); + return new(new HttpContextAccessor { HttpContext = httpContext }); } - - private sealed class StubGameService : IGameService - { - public Func> GetMeHandler { get; init; } = - _ => ServiceResult.Failure("unexpected_call", "Unexpected GetMe call."); - - public Func>> GetCampaignsHandler { get; init; } = - _ => ServiceResult>.Failure("unexpected_call", "Unexpected GetCampaigns call."); - - public IReadOnlyList GetRulesets() => throw new NotSupportedException(); - public ServiceResult Register(string username, string password, string displayName) => throw new NotSupportedException(); - public ServiceResult<(UserSummary User, string SessionToken)> Login(string username, string password) => throw new NotSupportedException(); - public void Logout(string sessionToken) => throw new NotSupportedException(); - public UserSummary? GetUserBySession(string sessionToken) => throw new NotSupportedException(); - public ServiceResult GetMe(string sessionToken) => GetMeHandler(sessionToken); - public ServiceResult CreateCampaign(string sessionToken, string name, string rulesetId) => throw new NotSupportedException(); - public ServiceResult> GetCampaigns(string sessionToken) => GetCampaignsHandler(sessionToken); - public ServiceResult> GetCharacterCampaignOptions(string sessionToken) => throw new NotSupportedException(); - public ServiceResult GetCampaign(string sessionToken, Guid campaignId) => throw new NotSupportedException(); - public ServiceResult DeleteCampaign(string sessionToken, Guid campaignId) => throw new NotSupportedException(); - public ServiceResult> GetUsernames(string sessionToken) => throw new NotSupportedException(); - public ServiceResult> GetUsers(string sessionToken) => throw new NotSupportedException(); - public ServiceResult UpdateUserRoles(string sessionToken, Guid userId, IReadOnlyList roles) => throw new NotSupportedException(); - public ServiceResult DeleteUser(string sessionToken, Guid userId) => throw new NotSupportedException(); - public ServiceResult CreateCharacter(string sessionToken, string name, Guid campaignId) => throw new NotSupportedException(); - public ServiceResult UpdateCharacter(string sessionToken, Guid characterId, string name, Guid? campaignId, string? ownerUsername = null) => throw new NotSupportedException(); - public ServiceResult DeleteCharacter(string sessionToken, Guid characterId) => throw new NotSupportedException(); - public ServiceResult ActivateCharacter(string sessionToken, Guid characterId) => throw new NotSupportedException(); - public ServiceResult> GetOwnCharacters(string sessionToken) => throw new NotSupportedException(); - public ServiceResult CreateSkillGroup(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null) => throw new NotSupportedException(); - public ServiceResult UpdateSkillGroup(string sessionToken, Guid skillGroupId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null) => throw new NotSupportedException(); - public ServiceResult DeleteSkillGroup(string sessionToken, Guid skillGroupId) => throw new NotSupportedException(); - public ServiceResult CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null) => throw new NotSupportedException(); - public ServiceResult UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null) => throw new NotSupportedException(); - public ServiceResult DeleteSkill(string sessionToken, Guid skillId) => throw new NotSupportedException(); - public ServiceResult GetCharacterSheet(string sessionToken, Guid characterId) => throw new NotSupportedException(); - public ServiceResult RollSkill(string sessionToken, Guid skillId, string visibility) => throw new NotSupportedException(); - public ServiceResult RollCustom(string sessionToken, Guid characterId, string expression, string visibility) => throw new NotSupportedException(); - public ServiceResult> GetCampaignLog(string sessionToken, Guid campaignId) => throw new NotSupportedException(); - public ServiceResult GetCampaignLogPage(string sessionToken, Guid campaignId, Guid? afterRollId = null, int? limit = null) => throw new NotSupportedException(); - public ServiceResult GetRollDetail(string sessionToken, Guid rollId) => throw new NotSupportedException(); - public ServiceResult GetCampaignStateSnapshot(string sessionToken, Guid campaignId) => throw new NotSupportedException(); - } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/Services/WorkspaceStateTests.cs b/RpgRoller.Tests/Services/WorkspaceStateTests.cs index 0dfb904..07d6913 100644 --- a/RpgRoller.Tests/Services/WorkspaceStateTests.cs +++ b/RpgRoller.Tests/Services/WorkspaceStateTests.cs @@ -1,6 +1,4 @@ using RpgRoller.Components.Pages; -using RpgRoller.Contracts; -using RpgRoller.Domain; namespace RpgRoller.Tests; @@ -14,15 +12,10 @@ public sealed class WorkspaceStateTests var otherOwnerId = Guid.NewGuid(); var state = new WorkspaceState { - User = new UserSummary(userId, "user", "User", []), - SelectedCampaign = new CampaignRoster( - Guid.NewGuid(), - "Alpha", - "d6", - new CampaignGmSummary(gmId, "GM"), - [ - new CharacterSummary(Guid.NewGuid(), "Scout", otherOwnerId, Guid.NewGuid(), "Other Owner") - ]) + User = new(userId, "user", "User", []), + SelectedCampaign = new(Guid.NewGuid(), "Alpha", "d6", new(gmId, "GM"), [ + new(Guid.NewGuid(), "Scout", otherOwnerId, Guid.NewGuid(), "Other Owner") + ]) }; Assert.Equal("You", state.OwnerLabel(userId)); @@ -35,17 +28,14 @@ public sealed class WorkspaceStateTests public void SkillDefinitionLabel_FormatsD6RolemasterAndDefaultRulesets() { var skill = new CharacterSheetSkill(Guid.NewGuid(), null, "Awareness", "d100!+15", 1, true, 5); - var state = new WorkspaceState - { - SelectedCampaign = new CampaignRoster(Guid.NewGuid(), "Alpha", "d6", new CampaignGmSummary(Guid.NewGuid(), "GM"), []) - }; + var state = new WorkspaceState { SelectedCampaign = new(Guid.NewGuid(), "Alpha", "d6", new(Guid.NewGuid(), "GM"), []) }; Assert.Equal("d100!+15, wild 1, fumble on", state.SkillDefinitionLabel(skill)); - state.SelectedCampaign = new CampaignRoster(Guid.NewGuid(), "Alpha", "rolemaster", new CampaignGmSummary(Guid.NewGuid(), "GM"), []); + state.SelectedCampaign = new(Guid.NewGuid(), "Alpha", "rolemaster", new(Guid.NewGuid(), "GM"), []); Assert.Equal("Open-ended percentile: d100!+15, fumble <= 5", state.SkillDefinitionLabel(skill)); - state.SelectedCampaign = new CampaignRoster(Guid.NewGuid(), "Alpha", "dnd5e", new CampaignGmSummary(Guid.NewGuid(), "GM"), []); + state.SelectedCampaign = new(Guid.NewGuid(), "Alpha", "dnd5e", new(Guid.NewGuid(), "GM"), []); Assert.Equal("d100!+15", state.SkillDefinitionLabel(skill)); } @@ -58,17 +48,12 @@ public sealed class WorkspaceStateTests var otherCharacter = new CharacterSummary(Guid.NewGuid(), "Other", Guid.NewGuid(), Guid.NewGuid(), "Other"); var state = new WorkspaceState { - User = new UserSummary(userId, "user", "User", []), - SelectedCampaign = new CampaignRoster( - Guid.NewGuid(), - "Alpha", - "d6", - new CampaignGmSummary(Guid.NewGuid(), "GM"), - [ownedCharacter, secondOwnedCharacter, otherCharacter]), + User = new(userId, "user", "User", []), + SelectedCampaign = new(Guid.NewGuid(), "Alpha", "d6", new(Guid.NewGuid(), "GM"), [ownedCharacter, secondOwnedCharacter, otherCharacter]), SelectedCharacterId = secondOwnedCharacter.Id, ActiveCharacterId = ownedCharacter.Id, - SelectedCharacterSkills = [new CharacterSheetSkill(Guid.NewGuid(), null, "Stealth", "2D+1", 1, true, null)], - SelectedCharacterSkillGroups = [new CharacterSheetSkillGroup(Guid.NewGuid(), "Combat", "2D+1", 1, true, null)] + SelectedCharacterSkills = [new(Guid.NewGuid(), null, "Stealth", "2D+1", 1, true, null)], + SelectedCharacterSkillGroups = [new(Guid.NewGuid(), "Combat", "2D+1", 1, true, null)] }; Assert.Equal(2, state.PlaySelectedCampaign!.Characters.Length); @@ -89,8 +74,8 @@ public sealed class WorkspaceStateTests var adminId = Guid.NewGuid(); var state = new WorkspaceState { - User = new UserSummary(adminId, "admin", "Admin", [UserRoles.Admin]), - SelectedCampaign = new CampaignRoster(Guid.NewGuid(), "Alpha", "d6", new CampaignGmSummary(adminId, "Admin"), []), + User = new(adminId, "admin", "Admin", [UserRoles.Admin]), + SelectedCampaign = new(Guid.NewGuid(), "Alpha", "d6", new(adminId, "Admin"), []), CurrentScreen = "admin", ConnectionState = "reconnecting" }; @@ -113,4 +98,4 @@ public sealed class WorkspaceStateTests Assert.Equal("ok", state.ConnectionStateCssClass); Assert.Equal("rr-app app-play", state.AppCssClass); } -} +} \ No newline at end of file diff --git a/RpgRoller.Tests/Support/ApiTestBase.cs b/RpgRoller.Tests/Support/ApiTestBase.cs index 7a5a6be..02c00d9 100644 --- a/RpgRoller.Tests/Support/ApiTestBase.cs +++ b/RpgRoller.Tests/Support/ApiTestBase.cs @@ -97,4 +97,4 @@ public abstract class ApiTestBase : IClassFixture } private readonly WebApplicationFactory m_BaseFactory; -} +} \ No newline at end of file diff --git a/RpgRoller/Api/AdminEndpoints.cs b/RpgRoller/Api/AdminEndpoints.cs index ea19748..8f27bbc 100644 --- a/RpgRoller/Api/AdminEndpoints.cs +++ b/RpgRoller/Api/AdminEndpoints.cs @@ -36,10 +36,10 @@ internal static class AdminEndpoints return TypedResults.Unauthorized(); if (!user.Roles.Contains(UserRoles.Admin, StringComparer.OrdinalIgnoreCase)) - return ApiResultMapper.ToBadRequest(new ServiceError("forbidden", "Admin role is required.")); + return ApiResultMapper.ToBadRequest(new("forbidden", "Admin role is required.")); if (string.IsNullOrWhiteSpace(databaseFile.Path) || !File.Exists(databaseFile.Path)) - return ApiResultMapper.ToBadRequest(new ServiceError("database_unavailable", "SQLite database file is not available.")); + return ApiResultMapper.ToBadRequest(new("database_unavailable", "SQLite database file is not available.")); var stream = new FileStream(databaseFile.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); return TypedResults.File(stream, "application/octet-stream", Path.GetFileName(databaseFile.Path)); @@ -47,4 +47,4 @@ internal static class AdminEndpoints return group; } -} +} \ No newline at end of file diff --git a/RpgRoller/Api/ApiEndpointRegistration.cs b/RpgRoller/Api/ApiEndpointRegistration.cs index 8c0e300..2b2acdf 100644 --- a/RpgRoller/Api/ApiEndpointRegistration.cs +++ b/RpgRoller/Api/ApiEndpointRegistration.cs @@ -18,4 +18,4 @@ public static class ApiEndpointRegistration authenticatedApi.MapRollEndpoints(); authenticatedApi.MapStateEventEndpoints(); } -} +} \ No newline at end of file diff --git a/RpgRoller/Api/ApiResultMapper.cs b/RpgRoller/Api/ApiResultMapper.cs index 48a2d36..c0ec2a3 100644 --- a/RpgRoller/Api/ApiResultMapper.cs +++ b/RpgRoller/Api/ApiResultMapper.cs @@ -21,4 +21,4 @@ internal static class ApiResultMapper { return TypedResults.BadRequest(new ApiError(error.Message, error.Code)); } -} +} \ No newline at end of file diff --git a/RpgRoller/Api/CampaignEndpoints.cs b/RpgRoller/Api/CampaignEndpoints.cs index 8b9e41e..5c421f9 100644 --- a/RpgRoller/Api/CampaignEndpoints.cs +++ b/RpgRoller/Api/CampaignEndpoints.cs @@ -51,4 +51,4 @@ internal static class CampaignEndpoints return group; } -} +} \ No newline at end of file diff --git a/RpgRoller/Api/CharacterEndpoints.cs b/RpgRoller/Api/CharacterEndpoints.cs index 8b6164e..0fb0685 100644 --- a/RpgRoller/Api/CharacterEndpoints.cs +++ b/RpgRoller/Api/CharacterEndpoints.cs @@ -51,4 +51,4 @@ internal static class CharacterEndpoints return group; } -} +} \ No newline at end of file diff --git a/RpgRoller/Api/RollEndpoints.cs b/RpgRoller/Api/RollEndpoints.cs index b05266a..64d192b 100644 --- a/RpgRoller/Api/RollEndpoints.cs +++ b/RpgRoller/Api/RollEndpoints.cs @@ -1,4 +1,3 @@ -using RpgRoller.Contracts; using RpgRoller.Services; namespace RpgRoller.Api; @@ -15,4 +14,4 @@ internal static class RollEndpoints return group; } -} +} \ No newline at end of file diff --git a/RpgRoller/Api/SkillEndpoints.cs b/RpgRoller/Api/SkillEndpoints.cs index 10824d7..9da8d33 100644 --- a/RpgRoller/Api/SkillEndpoints.cs +++ b/RpgRoller/Api/SkillEndpoints.cs @@ -57,4 +57,4 @@ internal static class SkillEndpoints return group; } -} +} \ No newline at end of file diff --git a/RpgRoller/Api/StateEventEndpoints.cs b/RpgRoller/Api/StateEventEndpoints.cs index 8ccae98..9a6b886 100644 --- a/RpgRoller/Api/StateEventEndpoints.cs +++ b/RpgRoller/Api/StateEventEndpoints.cs @@ -13,9 +13,7 @@ internal static class StateEventEndpoints var stateResult = game.GetCampaignStateSnapshot(sessionToken, campaignId); if (!stateResult.Succeeded) { - return stateResult.Error!.Code == "unauthorized" - ? TypedResults.Unauthorized() - : TypedResults.BadRequest(new ApiError(stateResult.Error.Message, stateResult.Error.Code)); + return stateResult.Error!.Code == "unauthorized" ? TypedResults.Unauthorized() : TypedResults.BadRequest(new ApiError(stateResult.Error.Message, stateResult.Error.Code)); } context.Response.Headers.CacheControl = "no-cache"; @@ -60,11 +58,8 @@ internal static class StateEventEndpoints private static Task WriteStateEventAsync(HttpResponse response, CampaignStateSnapshot snapshot) { - var characterVersions = string.Join( - ",", - snapshot.CharacterVersions.Select(version => $"{{\"characterId\":\"{version.CharacterId}\",\"version\":{version.Version}}}")); + var characterVersions = string.Join(",", snapshot.CharacterVersions.Select(version => $"{{\"characterId\":\"{version.CharacterId}\",\"version\":{version.Version}}}")); - return response.WriteAsync( - $"event: state\ndata: {{\"campaignId\":\"{snapshot.CampaignId}\",\"totalVersion\":{snapshot.TotalVersion},\"rosterVersion\":{snapshot.RosterVersion},\"logVersion\":{snapshot.LogVersion},\"characterVersions\":[{characterVersions}]}}\n\n"); + return response.WriteAsync($"event: state\ndata: {{\"campaignId\":\"{snapshot.CampaignId}\",\"totalVersion\":{snapshot.TotalVersion},\"rosterVersion\":{snapshot.RosterVersion},\"logVersion\":{snapshot.LogVersion},\"characterVersions\":[{characterVersions}]}}\n\n"); } -} +} \ No newline at end of file diff --git a/RpgRoller/Components/App.razor b/RpgRoller/Components/App.razor index 38d1c2d..db04244 100644 --- a/RpgRoller/Components/App.razor +++ b/RpgRoller/Components/App.razor @@ -22,8 +22,9 @@ @code { + [CascadingParameter] - private Microsoft.AspNetCore.Http.HttpContext? HttpContext { get; set; } + private HttpContext? HttpContext { get; set; } private string BaseHref { @@ -36,4 +37,5 @@ return pathBase.EndsWith('/') ? pathBase : $"{pathBase}/"; } } -} + +} \ No newline at end of file diff --git a/RpgRoller/Components/Layout/MainLayout.razor b/RpgRoller/Components/Layout/MainLayout.razor index a098840..817b7d0 100644 --- a/RpgRoller/Components/Layout/MainLayout.razor +++ b/RpgRoller/Components/Layout/MainLayout.razor @@ -1,4 +1,4 @@ @inherits LayoutComponentBase @attribute [ExcludeFromCodeCoverage] -@Body +@Body \ No newline at end of file diff --git a/RpgRoller/Components/Pages/Home.Models.cs b/RpgRoller/Components/Pages/Home.Models.cs index 0b08ad3..6d41d2f 100644 --- a/RpgRoller/Components/Pages/Home.Models.cs +++ b/RpgRoller/Components/Pages/Home.Models.cs @@ -70,4 +70,4 @@ public enum HomeViewMode Loading, Anonymous, Workspace -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/Home.razor b/RpgRoller/Components/Pages/Home.razor index 9c47300..a2ed9d7 100644 --- a/RpgRoller/Components/Pages/Home.razor +++ b/RpgRoller/Components/Pages/Home.razor @@ -24,4 +24,4 @@ case HomeViewMode.Workspace: break; -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/Home.razor.cs b/RpgRoller/Components/Pages/Home.razor.cs index c567613..c909aec 100644 --- a/RpgRoller/Components/Pages/Home.razor.cs +++ b/RpgRoller/Components/Pages/Home.razor.cs @@ -77,4 +77,4 @@ public partial class Home [Inject] private RpgRollerApiClient ApiClient { get; set; } = null!; -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/HomeControls/AdminHome.razor b/RpgRoller/Components/Pages/HomeControls/AdminHome.razor index 2de9760..9d980e6 100644 --- a/RpgRoller/Components/Pages/HomeControls/AdminHome.razor +++ b/RpgRoller/Components/Pages/HomeControls/AdminHome.razor @@ -68,4 +68,4 @@ - + \ No newline at end of file diff --git a/RpgRoller/Components/Pages/HomeControls/AdminHome.razor.cs b/RpgRoller/Components/Pages/HomeControls/AdminHome.razor.cs index 94f61fc..e4a6e86 100644 --- a/RpgRoller/Components/Pages/HomeControls/AdminHome.razor.cs +++ b/RpgRoller/Components/Pages/HomeControls/AdminHome.razor.cs @@ -28,9 +28,7 @@ public partial class AdminHome if (!IsCurrentUserAdmin) return; - Users = (await ApiClient.RequestAsync>("GET", "/api/admin/users")) - .OrderBy(user => user.Username, StringComparer.OrdinalIgnoreCase) - .ToList(); + Users = (await ApiClient.RequestAsync>("GET", "/api/admin/users")).OrderBy(user => user.Username, StringComparer.OrdinalIgnoreCase).ToList(); } catch (ApiRequestException ex) when (ex.StatusCode == 401) { @@ -92,10 +90,7 @@ public partial class AdminHome try { IReadOnlyList roles = HasAdminRole(user) ? Array.Empty() : [UserRoles.Admin]; - _ = await ApiClient.RequestAsync( - "PUT", - $"/api/admin/users/{user.Id}/roles", - new UpdateUserRolesRequest(roles)); + _ = await ApiClient.RequestAsync("PUT", $"/api/admin/users/{user.Id}/roles", new UpdateUserRolesRequest(roles)); await ReloadUsersAsync(); SetStatus("User roles updated.", false); @@ -138,9 +133,7 @@ public partial class AdminHome private async Task ReloadUsersAsync() { - Users = (await ApiClient.RequestAsync>("GET", "/api/admin/users")) - .OrderBy(user => user.Username, StringComparer.OrdinalIgnoreCase) - .ToList(); + Users = (await ApiClient.RequestAsync>("GET", "/api/admin/users")).OrderBy(user => user.Username, StringComparer.OrdinalIgnoreCase).ToList(); } private static bool HasAdminRole(UserSummary user) @@ -184,22 +177,32 @@ public partial class AdminHome private List Users { get; set; } = []; private string? StatusMessage { get; set; } private bool StatusIsError { get; set; } - private IReadOnlyList HeaderMenuItems - { - get + + private IReadOnlyList HeaderMenuItems => + [ + new AppHeaderMenuItem { - return - [ - new AppHeaderMenuItem { Label = "Play", IsActive = false, OnSelected = OpenPlayAsync }, - new AppHeaderMenuItem { Label = "Campaign Management", IsActive = false, OnSelected = OpenCampaignManagementAsync }, - new AppHeaderMenuItem { Label = "Admin", IsActive = true, OnSelected = OpenAdminAsync } - ]; + Label = "Play", + IsActive = false, + OnSelected = OpenPlayAsync + }, + new AppHeaderMenuItem + { + Label = "Campaign Management", + IsActive = false, + OnSelected = OpenCampaignManagementAsync + }, + new AppHeaderMenuItem + { + Label = "Admin", + IsActive = true, + OnSelected = OpenAdminAsync } - } + ]; [Parameter] public EventCallback LoggedOut { get; set; } [Parameter] public EventCallback WorkspaceRequested { get; set; } -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/HomeControls/AppHeader.razor b/RpgRoller/Components/Pages/HomeControls/AppHeader.razor index fda3095..725ef01 100644 --- a/RpgRoller/Components/Pages/HomeControls/AppHeader.razor +++ b/RpgRoller/Components/Pages/HomeControls/AppHeader.razor @@ -3,11 +3,15 @@

@Title

@if (User is null) { -

Loading user...

+

+ Loading user... +

} else { -

@User.DisplayName (@User.Username)

+

+ @User.DisplayName (@User.Username) +

} @if (ShowCampaign) { @@ -50,4 +54,4 @@ } - + \ No newline at end of file diff --git a/RpgRoller/Components/Pages/HomeControls/AppHeader.razor.cs b/RpgRoller/Components/Pages/HomeControls/AppHeader.razor.cs index 83dd5f7..0cbb37d 100644 --- a/RpgRoller/Components/Pages/HomeControls/AppHeader.razor.cs +++ b/RpgRoller/Components/Pages/HomeControls/AppHeader.razor.cs @@ -57,4 +57,4 @@ public sealed class AppHeaderMenuItem public string Label { get; init; } = string.Empty; public bool IsActive { get; init; } public Func? OnSelected { get; init; } -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/HomeControls/AuthSection.razor b/RpgRoller/Components/Pages/HomeControls/AuthSection.razor index 75955b5..95c4e93 100644 --- a/RpgRoller/Components/Pages/HomeControls/AuthSection.razor +++ b/RpgRoller/Components/Pages/HomeControls/AuthSection.razor @@ -63,4 +63,4 @@ - + \ No newline at end of file diff --git a/RpgRoller/Components/Pages/HomeControls/CampaignLogPanel.razor b/RpgRoller/Components/Pages/HomeControls/CampaignLogPanel.razor index 5128f93..bdaca84 100644 --- a/RpgRoller/Components/Pages/HomeControls/CampaignLogPanel.razor +++ b/RpgRoller/Components/Pages/HomeControls/CampaignLogPanel.razor @@ -1,5 +1,7 @@ + \ No newline at end of file diff --git a/RpgRoller/Components/Pages/HomeControls/CampaignLogPanel.razor.cs b/RpgRoller/Components/Pages/HomeControls/CampaignLogPanel.razor.cs index 0957e14..03b48ca 100644 --- a/RpgRoller/Components/Pages/HomeControls/CampaignLogPanel.razor.cs +++ b/RpgRoller/Components/Pages/HomeControls/CampaignLogPanel.razor.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; -using RpgRoller.Components; using RpgRoller.Contracts; namespace RpgRoller.Components.Pages.HomeControls; @@ -9,6 +8,8 @@ namespace RpgRoller.Components.Pages.HomeControls; [ExcludeFromCodeCoverage] public partial class CampaignLogPanel { + private sealed record EventBadgeView(string Label, string Tone); + protected override async Task OnAfterRenderAsync(bool firstRender) { var currentLastRollId = CampaignLog.LastOrDefault()?.RollId; @@ -37,6 +38,96 @@ public partial class CampaignLogPanel LastRenderedLogRollId = currentLastRollId; } + private async Task SubmitCustomRollAsync() + { + CustomRollState.ResetValidation(); + + var expression = CustomRollState.Model.Expression.Trim(); + if (string.IsNullOrWhiteSpace(expression)) + { + SetCustomRollError("Enter a roll expression first."); + return; + } + + if (!SelectedCharacterId.HasValue) + { + SetCustomRollError("Select a character to make a custom roll."); + return; + } + + IsSubmittingCustomRoll = true; + try + { + var roll = await ApiClient.RequestAsync("POST", $"/api/characters/{SelectedCharacterId.Value}/custom-rolls", new + { + expression, + visibility = NormalizedRollVisibility + }); + + CustomRollState.Model.Expression = string.Empty; + CustomRollState.ResetValidation(); + CustomRollInputVersion += 1; + await CustomRollCreated.InvokeAsync(roll); + await JS.InvokeVoidAsync("rpgRollerApi.clearInputValue", CustomRollInputRef); + await InvokeAsync(StateHasChanged); + } + catch (ApiRequestException ex) when (string.Equals(ex.ErrorCode, "invalid_expression", StringComparison.Ordinal)) + { + SetCustomRollError(ex.Message); + await InvokeAsync(StateHasChanged); + } + catch (ApiRequestException ex) + { + await ErrorOccurred.InvokeAsync(ex.Message); + } + finally + { + IsSubmittingCustomRoll = false; + } + } + + private void SetCustomRollError(string message) + { + CustomRollState.Errors["expression"] = message; + } + + private static IReadOnlyList GetEventBadges(CampaignLogListEntry entry) + { + return (entry.EventBadges ?? []).Select(ToEventBadgeView).Where(badge => badge is not null).Cast().ToArray(); + } + + private static bool HasSummary(CampaignLogListEntry entry) + { + return (entry.EventBadges?.Length ?? 0) > 0 || !string.IsNullOrWhiteSpace(entry.SummaryText); + } + + private static EventBadgeView? ToEventBadgeView(string code) + { + return code switch + { + "w6" => new("Wild 6", "positive"), + "w1" => new("Wild 1", "danger"), + "n20" => new("Nat 20", "positive"), + "n1" => new("Nat 1", "danger"), + "rf" => new("Fumble", "danger"), + "r100" => new("100", "rare"), + "r66" => new("66", "rare"), + _ => null + }; + } + + private static string LogEntryCssClass(CampaignLogListEntry entry, bool isExpanded, bool isFresh) + { + var classes = new List { entry.VisibilityStyle }; + if (isExpanded) + classes.Add("expanded"); + + if (isFresh) + classes.Add("fresh"); + + return string.Join(" ", classes); + } + [Inject] private IJSRuntime JS { get; set; } = null!; @@ -97,105 +188,6 @@ public partial class CampaignLogPanel [Parameter] public EventCallback ErrorOccurred { get; set; } - private async Task SubmitCustomRollAsync() - { - CustomRollState.ResetValidation(); - - var expression = CustomRollState.Model.Expression.Trim(); - if (string.IsNullOrWhiteSpace(expression)) - { - SetCustomRollError("Enter a roll expression first."); - return; - } - - if (!SelectedCharacterId.HasValue) - { - SetCustomRollError("Select a character to make a custom roll."); - return; - } - - IsSubmittingCustomRoll = true; - try - { - var roll = await ApiClient.RequestAsync( - "POST", - $"/api/characters/{SelectedCharacterId.Value}/custom-rolls", - new - { - expression, - visibility = NormalizedRollVisibility - }); - - CustomRollState.Model.Expression = string.Empty; - CustomRollState.ResetValidation(); - CustomRollInputVersion += 1; - await CustomRollCreated.InvokeAsync(roll); - await JS.InvokeVoidAsync("rpgRollerApi.clearInputValue", CustomRollInputRef); - await InvokeAsync(StateHasChanged); - } - catch (ApiRequestException ex) when (string.Equals(ex.ErrorCode, "invalid_expression", StringComparison.Ordinal)) - { - SetCustomRollError(ex.Message); - await InvokeAsync(StateHasChanged); - } - catch (ApiRequestException ex) - { - await ErrorOccurred.InvokeAsync(ex.Message); - } - finally - { - IsSubmittingCustomRoll = false; - } - } - - private void SetCustomRollError(string message) - { - CustomRollState.Errors["expression"] = message; - } - - private static IReadOnlyList GetEventBadges(CampaignLogListEntry entry) - { - return (entry.EventBadges ?? []) - .Select(ToEventBadgeView) - .Where(badge => badge is not null) - .Cast() - .ToArray(); - } - - private static bool HasSummary(CampaignLogListEntry entry) - { - return (entry.EventBadges?.Length ?? 0) > 0 || !string.IsNullOrWhiteSpace(entry.SummaryText); - } - - private static EventBadgeView? ToEventBadgeView(string code) - { - return code switch - { - "w6" => new EventBadgeView("Wild 6", "positive"), - "w1" => new EventBadgeView("Wild 1", "danger"), - "n20" => new EventBadgeView("Nat 20", "positive"), - "n1" => new EventBadgeView("Nat 1", "danger"), - "rf" => new EventBadgeView("Fumble", "danger"), - "r100" => new EventBadgeView("100", "rare"), - "r66" => new EventBadgeView("66", "rare"), - _ => null - }; - } - - private static string LogEntryCssClass(CampaignLogListEntry entry, bool isExpanded, bool isFresh) - { - var classes = new List { entry.VisibilityStyle }; - if (isExpanded) - classes.Add("expanded"); - - if (isFresh) - classes.Add("fresh"); - - return string.Join(" ", classes); - } - - private sealed record EventBadgeView(string Label, string Tone); - private bool HasCustomRollError => CustomRollState.Errors.ContainsKey("expression"); private string? CustomRollErrorMessage => CustomRollState.Errors.GetValueOrDefault("expression"); private bool IsCustomRollDisabled => IsCampaignDataLoading || IsMutating || IsSubmittingCustomRoll || !SelectedCharacterId.HasValue; @@ -203,24 +195,27 @@ public partial class CampaignLogPanel private string? CustomRollInputTitle => HasCustomRollError ? CustomRollErrorMessage : null; private string CustomRollErrorElementId => "custom-roll-expression-error"; private string? CustomRollInputDescribedBy => HasCustomRollError ? CustomRollErrorElementId : null; + private string CustomRollPlaceholder => SelectedCampaignRulesetId.ToLowerInvariant() switch { - RulesetFormHelpers.RulesetIds.D6 => "e.g. 5D+4", - RulesetFormHelpers.RulesetIds.Dnd5e => "e.g. 2d12+2", + RulesetFormHelpers.RulesetIds.D6 => "e.g. 5D+4", + RulesetFormHelpers.RulesetIds.Dnd5e => "e.g. 2d12+2", RulesetFormHelpers.RulesetIds.Rolemaster => "e.g. d10, 15d10, d100!+85", - _ => "Enter a roll expression" + _ => "Enter a roll expression" }; - private string CustomRollStatusText => SelectedCharacterId.HasValue && !string.IsNullOrWhiteSpace(SelectedCharacterName) - ? $"For {SelectedCharacterName} • {RollVisibilityLabel}" - : "Select a character to enable"; + + private string CustomRollStatusText => SelectedCharacterId.HasValue && !string.IsNullOrWhiteSpace(SelectedCharacterName) ? $"For {SelectedCharacterName} • {RollVisibilityLabel}" : "Select a character to enable"; + private string CustomRollHelpText => SelectedCampaignRulesetId.ToLowerInvariant() switch { - RulesetFormHelpers.RulesetIds.D6 => "Uses the campaign ruleset. D6 custom rolls use one wild die and allow fumbles.", + RulesetFormHelpers.RulesetIds.D6 => "Uses the campaign ruleset. D6 custom rolls use one wild die and allow fumbles.", RulesetFormHelpers.RulesetIds.Rolemaster => $"{RulesetFormHelpers.RolemasterExampleText()}. Logged for the selected character.", - _ => "Uses the selected campaign ruleset and current visibility." + _ => "Uses the selected campaign ruleset and current visibility." }; + private string RollVisibilityLabel => string.Equals(NormalizedRollVisibility, "private", StringComparison.OrdinalIgnoreCase) ? "Private" : "Public"; private string NormalizedRollVisibility => string.Equals(RollVisibility, "private", StringComparison.OrdinalIgnoreCase) ? "private" : "public"; + private string CustomRollExpression { get => CustomRollState.Model.Expression; @@ -231,4 +226,4 @@ public partial class CampaignLogPanel CustomRollState.ResetValidation(); } } -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/HomeControls/CampaignManagementPanel.razor b/RpgRoller/Components/Pages/HomeControls/CampaignManagementPanel.razor index 600e407..f9f84dd 100644 --- a/RpgRoller/Components/Pages/HomeControls/CampaignManagementPanel.razor +++ b/RpgRoller/Components/Pages/HomeControls/CampaignManagementPanel.razor @@ -129,4 +129,4 @@ -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/HomeControls/CampaignManagementPanel.razor.cs b/RpgRoller/Components/Pages/HomeControls/CampaignManagementPanel.razor.cs index b1f22b1..f2fa546 100644 --- a/RpgRoller/Components/Pages/HomeControls/CampaignManagementPanel.razor.cs +++ b/RpgRoller/Components/Pages/HomeControls/CampaignManagementPanel.razor.cs @@ -115,4 +115,4 @@ public partial class CampaignManagementPanel [Parameter] public EventCallback DeleteCharacterRequested { get; set; } -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/HomeControls/CharacterFormModal.razor b/RpgRoller/Components/Pages/HomeControls/CharacterFormModal.razor index 23f26d9..8a26fcb 100644 --- a/RpgRoller/Components/Pages/HomeControls/CharacterFormModal.razor +++ b/RpgRoller/Components/Pages/HomeControls/CharacterFormModal.razor @@ -48,4 +48,4 @@ -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/HomeControls/CharacterFormModal.razor.cs b/RpgRoller/Components/Pages/HomeControls/CharacterFormModal.razor.cs index f367ca7..0949972 100644 --- a/RpgRoller/Components/Pages/HomeControls/CharacterFormModal.razor.cs +++ b/RpgRoller/Components/Pages/HomeControls/CharacterFormModal.razor.cs @@ -54,9 +54,7 @@ public partial class CharacterFormModal character = await ApiClient.RequestAsync("PUT", $"/api/characters/{EditingCharacterId.Value}", new UpdateCharacterRequest(FormState.Model.Name.Trim(), campaignId, ownerUsername)); } else - { character = await ApiClient.RequestAsync("POST", "/api/characters", new CreateCharacterRequest(FormState.Model.Name.Trim(), campaignId!.Value)); - } await CharacterSaved.InvokeAsync(character.CampaignId); } @@ -121,4 +119,4 @@ public partial class CharacterFormModal [Parameter] public EventCallback CancelRequested { get; set; } -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor b/RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor index 5ae40d9..a937bd0 100644 --- a/RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor +++ b/RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor @@ -41,8 +41,12 @@ Edit character -

@SelectedCharacter.Name | Owner: @OwnerLabel(SelectedCharacter.OwnerUserId) | Campaign: @SelectedCampaign.Name +

+ @SelectedCharacter.Name + + | Owner: @OwnerLabel(SelectedCharacter.OwnerUserId) | Campaign: @SelectedCampaign.Name +

@@ -130,6 +134,7 @@ } + } @@ -242,4 +247,4 @@ AvailableSkillGroups="SelectedCharacterSkillGroups" IsMutating="IsMutating" SkillSaved="OnSkillUpdatedAsync" - CancelRequested="CloseSkillModals"/> + CancelRequested="CloseSkillModals"/> \ No newline at end of file diff --git a/RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor.cs b/RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor.cs index d6abd77..5288816 100644 --- a/RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor.cs +++ b/RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Globalization; using Microsoft.AspNetCore.Components; using RpgRoller.Contracts; @@ -10,9 +9,7 @@ public partial class CharacterPanel { private void OpenCreateSkillModal(Guid? skillGroupId = null) { - var selectedGroup = skillGroupId.HasValue - ? SelectedCharacterSkillGroups.FirstOrDefault(group => group.Id == skillGroupId.Value) - : null; + var selectedGroup = skillGroupId.HasValue ? SelectedCharacterSkillGroups.FirstOrDefault(group => group.Id == skillGroupId.Value) : null; CreateSkillInitialModel = new() { @@ -156,9 +153,7 @@ public partial class CharacterPanel SkillGroupState.Errors["fumbleRange"] = "Open-ended Rolemaster groups require a fumble range."; } else - { SkillGroupState.Model.FumbleRange = null; - } if (!IsD6Ruleset) { @@ -179,15 +174,7 @@ public partial class CharacterPanel try { var selectedCharacterId = SelectedCharacterId!.Value; - var createdGroup = await ApiClient.RequestAsync( - "POST", - $"/api/characters/{selectedCharacterId}/skill-groups", - new CreateSkillGroupRequest( - SkillGroupState.Model.Name.Trim(), - SkillGroupState.Model.DiceRollDefinition.Trim(), - SkillGroupState.Model.WildDice, - SkillGroupState.Model.AllowFumble, - SkillGroupState.Model.FumbleRange)); + var createdGroup = await ApiClient.RequestAsync("POST", $"/api/characters/{selectedCharacterId}/skill-groups", new CreateSkillGroupRequest(SkillGroupState.Model.Name.Trim(), SkillGroupState.Model.DiceRollDefinition.Trim(), SkillGroupState.Model.WildDice, SkillGroupState.Model.AllowFumble, SkillGroupState.Model.FumbleRange)); CloseSkillGroupModals(); await SkillGroupCreated.InvokeAsync(createdGroup.Id); } @@ -220,9 +207,7 @@ public partial class CharacterPanel SkillGroupState.Errors["fumbleRange"] = "Open-ended Rolemaster groups require a fumble range."; } else - { SkillGroupState.Model.FumbleRange = null; - } if (!IsD6Ruleset) { @@ -243,15 +228,7 @@ public partial class CharacterPanel try { var editingSkillGroupId = EditingSkillGroupId!.Value; - var updatedGroup = await ApiClient.RequestAsync( - "PUT", - $"/api/skill-groups/{editingSkillGroupId}", - new UpdateSkillGroupRequest( - SkillGroupState.Model.Name.Trim(), - SkillGroupState.Model.DiceRollDefinition.Trim(), - SkillGroupState.Model.WildDice, - SkillGroupState.Model.AllowFumble, - SkillGroupState.Model.FumbleRange)); + var updatedGroup = await ApiClient.RequestAsync("PUT", $"/api/skill-groups/{editingSkillGroupId}", new UpdateSkillGroupRequest(SkillGroupState.Model.Name.Trim(), SkillGroupState.Model.DiceRollDefinition.Trim(), SkillGroupState.Model.WildDice, SkillGroupState.Model.AllowFumble, SkillGroupState.Model.FumbleRange)); CloseSkillGroupModals(); await SkillGroupUpdated.InvokeAsync(updatedGroup.Id); } @@ -297,8 +274,7 @@ public partial class CharacterPanel return true; var filter = SkillFilterText.Trim(); - return skill.Name.Contains(filter, StringComparison.OrdinalIgnoreCase) || - skill.DiceRollDefinition.Contains(filter, StringComparison.OrdinalIgnoreCase); + return skill.Name.Contains(filter, StringComparison.OrdinalIgnoreCase) || skill.DiceRollDefinition.Contains(filter, StringComparison.OrdinalIgnoreCase); } private static string InitialsFor(string value) @@ -340,9 +316,8 @@ public partial class CharacterPanel private bool IsD6Ruleset => RulesetFormHelpers.IsD6(SelectedCampaignRulesetId); private bool IsRolemasterRuleset => RulesetFormHelpers.IsRolemaster(SelectedCampaignRulesetId); private bool IsSkillGroupRolemasterOpenEnded => RulesetFormHelpers.IsRolemasterOpenEndedExpression(SkillGroupState.Model.DiceRollDefinition); - private string SkillGroupExpressionHelpText => IsRolemasterRuleset - ? $"{RulesetFormHelpers.RolemasterExampleText()}. Negative modifiers are allowed." - : "Enter the default expression for skills created in this group."; + + private string SkillGroupExpressionHelpText => IsRolemasterRuleset ? $"{RulesetFormHelpers.RolemasterExampleText()}. Negative modifiers are allowed." : "Enter the default expression for skills created in this group."; private bool ShowCreateSkillModal { get; set; } private bool ShowEditSkillModal { get; set; } @@ -432,4 +407,4 @@ public partial class CharacterPanel [Parameter] public EventCallback RollRequested { get; set; } -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/HomeControls/RollDiceStrip.razor b/RpgRoller/Components/Pages/HomeControls/RollDiceStrip.razor index 6e9977d..aaf5c4d 100644 --- a/RpgRoller/Components/Pages/HomeControls/RollDiceStrip.razor +++ b/RpgRoller/Components/Pages/HomeControls/RollDiceStrip.razor @@ -6,4 +6,4 @@ @RollDieDisplay(die) }
-} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/HomeControls/RollDiceStrip.razor.cs b/RpgRoller/Components/Pages/HomeControls/RollDiceStrip.razor.cs index 33802c6..9f21cec 100644 --- a/RpgRoller/Components/Pages/HomeControls/RollDiceStrip.razor.cs +++ b/RpgRoller/Components/Pages/HomeControls/RollDiceStrip.razor.cs @@ -38,7 +38,7 @@ public partial class RollDiceStrip return die.Kind switch { - _ => RollDieGlyph(die.Roll) + _ => RollDieGlyph(die.Roll) }; } @@ -81,10 +81,7 @@ public partial class RollDiceStrip private static bool IsRolemasterDie(RollDieResult die) { - return die.Kind is RollDieKinds.RolemasterStandard or - RollDieKinds.RolemasterOpenEndedInitial or - RollDieKinds.RolemasterOpenEndedHigh or - RollDieKinds.RolemasterOpenEndedLowSubtract; + return die.Kind is RollDieKinds.RolemasterStandard or RollDieKinds.RolemasterOpenEndedInitial or RollDieKinds.RolemasterOpenEndedHigh or RollDieKinds.RolemasterOpenEndedLowSubtract; } private static string RollDieTitle(RollDieResult die) @@ -132,4 +129,4 @@ public partial class RollDiceStrip [Parameter] public string AriaLabel { get; set; } = "Rolled dice"; -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/HomeControls/RulesetFormHelpers.cs b/RpgRoller/Components/Pages/HomeControls/RulesetFormHelpers.cs index d3c2727..a761ca8 100644 --- a/RpgRoller/Components/Pages/HomeControls/RulesetFormHelpers.cs +++ b/RpgRoller/Components/Pages/HomeControls/RulesetFormHelpers.cs @@ -27,9 +27,7 @@ internal static class RulesetFormHelpers public static bool IsRolemasterOpenEndedExpression(string? expression) { var parseResult = TryParseRolemasterExpression(expression); - return parseResult.Succeeded && - parseResult.Value is not null && - parseResult.Value.Kind == DiceExpressionKind.RolemasterOpenEndedPercentile; + return parseResult.Succeeded && parseResult.Value is not null && parseResult.Value.Kind == DiceExpressionKind.RolemasterOpenEndedPercentile; } public static string DescribeRolemasterExpression(string expression, int? fumbleRange) @@ -40,10 +38,8 @@ internal static class RulesetFormHelpers return parseResult.Value.Kind switch { - DiceExpressionKind.RolemasterOpenEndedPercentile => fumbleRange.HasValue - ? $"Open-ended percentile: {parseResult.Value.Canonical}, fumble <= {fumbleRange.Value}" - : $"Open-ended percentile: {parseResult.Value.Canonical}", - _ => $"Rolemaster: {parseResult.Value.Canonical}" + DiceExpressionKind.RolemasterOpenEndedPercentile => fumbleRange.HasValue ? $"Open-ended percentile: {parseResult.Value.Canonical}, fumble <= {fumbleRange.Value}" : $"Open-ended percentile: {parseResult.Value.Canonical}", + _ => $"Rolemaster: {parseResult.Value.Canonical}" }; } @@ -59,4 +55,4 @@ internal static class RulesetFormHelpers return DiceRules.ParseExpression(RulesetKind.Rolemaster, expression); } -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/HomeControls/SkillFormModal.razor b/RpgRoller/Components/Pages/HomeControls/SkillFormModal.razor index 321c051..0373c20 100644 --- a/RpgRoller/Components/Pages/HomeControls/SkillFormModal.razor +++ b/RpgRoller/Components/Pages/HomeControls/SkillFormModal.razor @@ -65,4 +65,4 @@ -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/HomeControls/SkillFormModal.razor.cs b/RpgRoller/Components/Pages/HomeControls/SkillFormModal.razor.cs index 12b9a42..17d4a4d 100644 --- a/RpgRoller/Components/Pages/HomeControls/SkillFormModal.razor.cs +++ b/RpgRoller/Components/Pages/HomeControls/SkillFormModal.razor.cs @@ -1,6 +1,5 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.AspNetCore.Components; -using Microsoft.AspNetCore.Components.Web; using RpgRoller.Contracts; namespace RpgRoller.Components.Pages.HomeControls; @@ -54,9 +53,7 @@ public partial class SkillFormModal FormState.Errors["fumbleRange"] = "Open-ended Rolemaster skills require a fumble range."; } else - { FormState.Model.FumbleRange = null; - } if (!IsD6Ruleset) { @@ -84,9 +81,7 @@ public partial class SkillFormModal { SkillSummary skill; if (EditingSkillId.HasValue) - { skill = await ApiClient.RequestAsync("PUT", $"/api/skills/{EditingSkillId.Value}", new UpdateSkillRequest(FormState.Model.Name.Trim(), FormState.Model.DiceRollDefinition.Trim(), FormState.Model.WildDice, FormState.Model.AllowFumble, skillGroupId, FormState.Model.FumbleRange)); - } else { if (!SelectedCharacterId.HasValue) @@ -117,13 +112,6 @@ public partial class SkillFormModal NormalizeRolemasterFumbleRange(); } - private bool IsD6Ruleset => RulesetFormHelpers.IsD6(RulesetId); - private bool IsRolemasterRuleset => RulesetFormHelpers.IsRolemaster(RulesetId); - private bool IsRolemasterOpenEndedSelected => RulesetFormHelpers.IsRolemasterOpenEndedExpression(FormState.Model.DiceRollDefinition); - private string ExpressionHelpText => IsRolemasterRuleset - ? $"{RulesetFormHelpers.RolemasterExampleText()}. Negative modifiers are allowed." - : "Enter the dice expression used for this skill."; - private void SynchronizeRulesetSpecificFields() { if (!IsRolemasterRuleset) @@ -149,6 +137,12 @@ public partial class SkillFormModal FormState.Model.FumbleRange = null; } + private bool IsD6Ruleset => RulesetFormHelpers.IsD6(RulesetId); + private bool IsRolemasterRuleset => RulesetFormHelpers.IsRolemaster(RulesetId); + private bool IsRolemasterOpenEndedSelected => RulesetFormHelpers.IsRolemasterOpenEndedExpression(FormState.Model.DiceRollDefinition); + + private string ExpressionHelpText => IsRolemasterRuleset ? $"{RulesetFormHelpers.RolemasterExampleText()}. Negative modifiers are allowed." : "Enter the dice expression used for this skill."; + [Inject] private RpgRollerApiClient ApiClient { get; set; } = null!; @@ -214,4 +208,4 @@ public partial class SkillFormModal [Parameter] public EventCallback CancelRequested { get; set; } -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/HomeControls/SkillGroupBlock.razor b/RpgRoller/Components/Pages/HomeControls/SkillGroupBlock.razor index 0b9abc6..d271225 100644 --- a/RpgRoller/Components/Pages/HomeControls/SkillGroupBlock.razor +++ b/RpgRoller/Components/Pages/HomeControls/SkillGroupBlock.razor @@ -77,4 +77,4 @@ Add skill - + \ No newline at end of file diff --git a/RpgRoller/Components/Pages/HomeControls/SkillGroupBlock.razor.cs b/RpgRoller/Components/Pages/HomeControls/SkillGroupBlock.razor.cs index ac3f4b4..3783818 100644 --- a/RpgRoller/Components/Pages/HomeControls/SkillGroupBlock.razor.cs +++ b/RpgRoller/Components/Pages/HomeControls/SkillGroupBlock.razor.cs @@ -57,4 +57,4 @@ public partial class SkillGroupBlock [Parameter] public EventCallback DeleteGroupRequested { get; set; } -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/Workspace.razor b/RpgRoller/Components/Pages/Workspace.razor index 9f6eaba..81ecb15 100644 --- a/RpgRoller/Components/Pages/Workspace.razor +++ b/RpgRoller/Components/Pages/Workspace.razor @@ -73,13 +73,15 @@ GetRollDetailError="Play.GetRollDetailError" CustomRollCreated="Play.OnCustomRollCreatedAsync" ErrorOccurred="Play.OnCampaignLogPanelErrorAsync"/> - + } @@ -214,4 +216,4 @@ AllowOwnerEdit="State.CanEditCharacterOwner" AvailableUsernames="State.KnownUsernames" CharacterSaved="Campaigns.OnCharacterUpdatedAsync" - CancelRequested="Campaigns.CloseCharacterModals"/> + CancelRequested="Campaigns.CloseCharacterModals"/> \ No newline at end of file diff --git a/RpgRoller/Components/Pages/Workspace.razor.cs b/RpgRoller/Components/Pages/Workspace.razor.cs index 786cf05..2300c4b 100644 --- a/RpgRoller/Components/Pages/Workspace.razor.cs +++ b/RpgRoller/Components/Pages/Workspace.razor.cs @@ -20,10 +20,16 @@ public partial class Workspace : IAsyncDisposable } [JSInvokable] - public Task OnStateEventReceived(CampaignStateSnapshot state) => Live.OnStateEventReceivedAsync(state); + public Task OnStateEventReceived(CampaignStateSnapshot state) + { + return Live.OnStateEventReceivedAsync(state); + } [JSInvokable] - public Task OnConnectionStateChanged(string state) => Live.OnConnectionStateChangedAsync(state); + public Task OnConnectionStateChanged(string state) + { + return Live.OnConnectionStateChangedAsync(state); + } public async ValueTask DisposeAsync() { @@ -31,13 +37,25 @@ public partial class Workspace : IAsyncDisposable DotNetRef?.Dispose(); } - private bool CanEditCharacter(CharacterSummary character) => Campaigns.CanEditCharacter(character); + private bool CanEditCharacter(CharacterSummary character) + { + return Campaigns.CanEditCharacter(character); + } - private void ClearAuthenticatedState() => Session.ClearAuthenticatedState(); + private void ClearAuthenticatedState() + { + Session.ClearAuthenticatedState(); + } - private Task EnsureAdminUsersLoadedAsync() => Admin.EnsureAdminUsersLoadedAsync(); + private Task EnsureAdminUsersLoadedAsync() + { + return Admin.EnsureAdminUsersLoadedAsync(); + } - private Task StopStateEventsAsync() => Live.StopStateEventsAsync(); + private Task StopStateEventsAsync() + { + return Live.StopStateEventsAsync(); + } private async Task StartStateEventsCoreAsync(Guid campaignId) { @@ -86,76 +104,19 @@ public partial class Workspace : IAsyncDisposable private WorkspaceState State { get; } = new(); - private WorkspaceCampaignScopeCoordinator Scope => m_Scope ??= new( - State, - Feedback, - JS, - WorkspaceQuery, - Play.EnsureSelectedCharacterActiveAsync, - Play.RefreshSelectedCharacterSheetAsync, - Play.RefreshCampaignLogAsync, - Play.ResetCampaignLogDetailState, - Play.ResetCampaignStateTracking, - ClearAuthenticatedState, - StopStateEventsAsync, - message => LoggedOut.InvokeAsync(message)); + private WorkspaceCampaignScopeCoordinator Scope => m_Scope ??= new(State, Feedback, JS, WorkspaceQuery, Play.EnsureSelectedCharacterActiveAsync, Play.RefreshSelectedCharacterSheetAsync, Play.RefreshCampaignLogAsync, Play.ResetCampaignLogDetailState, Play.ResetCampaignStateTracking, ClearAuthenticatedState, StopStateEventsAsync, message => LoggedOut.InvokeAsync(message)); - private WorkspaceLiveStateController Live => m_Live ??= new( - State, - Feedback, - StartStateEventsCoreAsync, - StopStateEventsCoreAsync, - Scope.RefreshCampaignRosterAsync, - Play.RefreshSelectedCharacterSheetAsync, - Play.RefreshCampaignLogAsync, - () => InvokeAsync(StateHasChanged)); + private WorkspaceLiveStateController Live => m_Live ??= new(State, Feedback, StartStateEventsCoreAsync, StopStateEventsCoreAsync, Scope.RefreshCampaignRosterAsync, Play.RefreshSelectedCharacterSheetAsync, Play.RefreshCampaignLogAsync, () => InvokeAsync(StateHasChanged)); - private WorkspacePlayCoordinator Play => m_Play ??= new( - State, - Feedback, - ApiClient, - WorkspaceQuery, - CanEditCharacter, - () => InvokeAsync(StateHasChanged)); + private WorkspacePlayCoordinator Play => m_Play ??= new(State, Feedback, ApiClient, WorkspaceQuery, CanEditCharacter, () => InvokeAsync(StateHasChanged)); - private WorkspaceCampaignCoordinator Campaigns => m_Campaigns ??= new( - State, - Feedback, - JS, - ApiClient, - Session.LoadKnownUsernamesAsync, - Scope.ReloadCampaignsAsync, - Scope.ReloadCharacterCampaignOptionsAsync, - Scope.RefreshCampaignScopeAsync, - Live.SyncStateEventsAsync); + private WorkspaceCampaignCoordinator Campaigns => m_Campaigns ??= new(State, Feedback, JS, ApiClient, Session.LoadKnownUsernamesAsync, Scope.ReloadCampaignsAsync, Scope.ReloadCharacterCampaignOptionsAsync, Scope.RefreshCampaignScopeAsync, Live.SyncStateEventsAsync); - private WorkspaceAdminCoordinator Admin => m_Admin ??= new( - State, - Feedback, - JS, - ApiClient, - WorkspaceQuery, - ClearAuthenticatedState, - StopStateEventsAsync, - message => LoggedOut.InvokeAsync(message)); + private WorkspaceAdminCoordinator Admin => m_Admin ??= new(State, Feedback, JS, ApiClient, WorkspaceQuery, ClearAuthenticatedState, StopStateEventsAsync, message => LoggedOut.InvokeAsync(message)); private WorkspaceFeedbackService Feedback => m_Feedback ??= new(State, () => InvokeAsync(StateHasChanged)); - private WorkspaceSessionCoordinator Session => m_Session ??= new( - State, - Feedback, - JS, - ApiClient, - WorkspaceQuery, - Scope.ReloadCampaignsAsync, - Scope.ReloadCharacterCampaignOptionsAsync, - Scope.RefreshCampaignScopeAsync, - Live.SyncStateEventsAsync, - Live.StopStateEventsAsync, - EnsureAdminUsersLoadedAsync, - Play.ResetCampaignLogDetailState, - () => InvokeAsync(StateHasChanged), - message => LoggedOut.InvokeAsync(message)); + private WorkspaceSessionCoordinator Session => m_Session ??= new(State, Feedback, JS, ApiClient, WorkspaceQuery, Scope.ReloadCampaignsAsync, Scope.ReloadCharacterCampaignOptionsAsync, Scope.RefreshCampaignScopeAsync, Live.SyncStateEventsAsync, Live.StopStateEventsAsync, EnsureAdminUsersLoadedAsync, Play.ResetCampaignLogDetailState, () => InvokeAsync(StateHasChanged), message => LoggedOut.InvokeAsync(message)); private IReadOnlyList HeaderMenuItems { @@ -163,12 +124,27 @@ public partial class Workspace : IAsyncDisposable { var items = new List { - new() { Label = "Play", IsActive = State.IsPlayScreen, OnSelected = () => Session.SwitchScreenAsync("play") }, - new() { Label = "Campaign Management", IsActive = State.IsManagementScreen, OnSelected = () => Session.SwitchScreenAsync("management") } + new() + { + Label = "Play", + IsActive = State.IsPlayScreen, + OnSelected = () => Session.SwitchScreenAsync("play") + }, + new() + { + Label = "Campaign Management", + IsActive = State.IsManagementScreen, + OnSelected = () => Session.SwitchScreenAsync("management") + } }; if (State.IsCurrentUserAdmin) - items.Add(new AppHeaderMenuItem { Label = "Admin", IsActive = State.IsAdminScreen, OnSelected = () => Session.SwitchScreenAsync(ScreenAdmin) }); + items.Add(new() + { + Label = "Admin", + IsActive = State.IsAdminScreen, + OnSelected = () => Session.SwitchScreenAsync(ScreenAdmin) + }); return items; } @@ -178,12 +154,12 @@ public partial class Workspace : IAsyncDisposable private DotNetObjectReference? DotNetRef { get; set; } private const string ScreenAdmin = "admin"; - - private WorkspaceCampaignScopeCoordinator? m_Scope; + private WorkspaceAdminCoordinator? m_Admin; + private WorkspaceCampaignCoordinator? m_Campaigns; + private WorkspaceFeedbackService? m_Feedback; private WorkspaceLiveStateController? m_Live; private WorkspacePlayCoordinator? m_Play; - private WorkspaceCampaignCoordinator? m_Campaigns; - private WorkspaceAdminCoordinator? m_Admin; - private WorkspaceFeedbackService? m_Feedback; + + private WorkspaceCampaignScopeCoordinator? m_Scope; private WorkspaceSessionCoordinator? m_Session; -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/WorkspaceAdminCoordinator.cs b/RpgRoller/Components/Pages/WorkspaceAdminCoordinator.cs index 73cef30..1478168 100644 --- a/RpgRoller/Components/Pages/WorkspaceAdminCoordinator.cs +++ b/RpgRoller/Components/Pages/WorkspaceAdminCoordinator.cs @@ -8,15 +8,7 @@ namespace RpgRoller.Components.Pages; [ExcludeFromCodeCoverage] public sealed class WorkspaceAdminCoordinator { - public WorkspaceAdminCoordinator( - WorkspaceState state, - WorkspaceFeedbackService feedback, - IJSRuntime js, - RpgRollerApiClient apiClient, - WorkspaceQueryService workspaceQuery, - Action clearAuthenticatedState, - Func stopStateEventsAsync, - Func onLoggedOutAsync) + public WorkspaceAdminCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, IJSRuntime js, RpgRollerApiClient apiClient, WorkspaceQueryService workspaceQuery, Action clearAuthenticatedState, Func stopStateEventsAsync, Func onLoggedOutAsync) { m_State = state; m_Feedback = feedback; @@ -63,10 +55,7 @@ public sealed class WorkspaceAdminCoordinator try { IReadOnlyList roles = HasAdminRole(user) ? Array.Empty() : [UserRoles.Admin]; - _ = await m_ApiClient.RequestAsync( - "PUT", - $"/api/admin/users/{user.Id}/roles", - new UpdateUserRolesRequest(roles)); + _ = await m_ApiClient.RequestAsync("PUT", $"/api/admin/users/{user.Id}/roles", new UpdateUserRolesRequest(roles)); await ReloadAdminUsersAsync(); m_Feedback.SetStatus("User roles updated.", false); @@ -109,9 +98,7 @@ public sealed class WorkspaceAdminCoordinator private async Task ReloadAdminUsersAsync() { - m_State.AdminUsers = (await m_WorkspaceQuery.GetAdminUsersAsync()) - .OrderBy(user => user.Username, StringComparer.OrdinalIgnoreCase) - .ToList(); + m_State.AdminUsers = (await m_WorkspaceQuery.GetAdminUsersAsync()).OrderBy(user => user.Username, StringComparer.OrdinalIgnoreCase).ToList(); m_State.HasLoadedAdminUsers = true; } @@ -129,4 +116,4 @@ public sealed class WorkspaceAdminCoordinator private readonly WorkspaceState m_State; private readonly Func m_StopStateEventsAsync; private readonly WorkspaceQueryService m_WorkspaceQuery; -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/WorkspaceCampaignCoordinator.cs b/RpgRoller/Components/Pages/WorkspaceCampaignCoordinator.cs index 0548d45..fef01f0 100644 --- a/RpgRoller/Components/Pages/WorkspaceCampaignCoordinator.cs +++ b/RpgRoller/Components/Pages/WorkspaceCampaignCoordinator.cs @@ -8,16 +8,7 @@ namespace RpgRoller.Components.Pages; [ExcludeFromCodeCoverage] public sealed class WorkspaceCampaignCoordinator { - public WorkspaceCampaignCoordinator( - WorkspaceState state, - WorkspaceFeedbackService feedback, - IJSRuntime js, - RpgRollerApiClient apiClient, - Func loadKnownUsernamesAsync, - Func reloadCampaignsAsync, - Func reloadCharacterCampaignOptionsAsync, - Func refreshCampaignScopeAsync, - Func syncStateEventsAsync) + public WorkspaceCampaignCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, IJSRuntime js, RpgRollerApiClient apiClient, Func loadKnownUsernamesAsync, Func reloadCampaignsAsync, Func reloadCharacterCampaignOptionsAsync, Func refreshCampaignScopeAsync, Func syncStateEventsAsync) { m_State = state; m_Feedback = feedback; @@ -179,15 +170,15 @@ public sealed class WorkspaceCampaignCoordinator return m_State.User is not null && (character.OwnerUserId == m_State.User.Id || m_State.IsCurrentUserAdmin); } + private const string CampaignSessionKey = "campaign"; + private readonly RpgRollerApiClient m_ApiClient; private readonly WorkspaceFeedbackService m_Feedback; private readonly IJSRuntime m_JS; private readonly Func m_LoadKnownUsernamesAsync; - private readonly Func m_ReloadCharacterCampaignOptionsAsync; - private readonly Func m_ReloadCampaignsAsync; private readonly Func m_RefreshCampaignScopeAsync; + private readonly Func m_ReloadCampaignsAsync; + private readonly Func m_ReloadCharacterCampaignOptionsAsync; private readonly WorkspaceState m_State; private readonly Func m_SyncStateEventsAsync; - - private const string CampaignSessionKey = "campaign"; -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/WorkspaceCampaignScopeCoordinator.cs b/RpgRoller/Components/Pages/WorkspaceCampaignScopeCoordinator.cs index fdb5fbc..52e2056 100644 --- a/RpgRoller/Components/Pages/WorkspaceCampaignScopeCoordinator.cs +++ b/RpgRoller/Components/Pages/WorkspaceCampaignScopeCoordinator.cs @@ -1,25 +1,12 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.JSInterop; -using RpgRoller.Contracts; namespace RpgRoller.Components.Pages; [ExcludeFromCodeCoverage] public sealed class WorkspaceCampaignScopeCoordinator { - public WorkspaceCampaignScopeCoordinator( - WorkspaceState state, - WorkspaceFeedbackService feedback, - IJSRuntime js, - WorkspaceQueryService workspaceQuery, - Func ensureSelectedCharacterActiveAsync, - Func refreshSelectedCharacterSheetAsync, - Func refreshCampaignLogAsync, - Action resetCampaignLogDetailState, - Action resetCampaignStateTracking, - Action clearAuthenticatedState, - Func stopStateEventsAsync, - Func onLoggedOutAsync) + public WorkspaceCampaignScopeCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, IJSRuntime js, WorkspaceQueryService workspaceQuery, Func ensureSelectedCharacterActiveAsync, Func refreshSelectedCharacterSheetAsync, Func refreshCampaignLogAsync, Action resetCampaignLogDetailState, Action resetCampaignStateTracking, Action clearAuthenticatedState, Func stopStateEventsAsync, Func onLoggedOutAsync) { m_State = state; m_Feedback = feedback; @@ -147,6 +134,9 @@ public sealed class WorkspaceCampaignScopeCoordinator m_State.SelectedCharacterId = m_State.SelectedCampaign.Characters[0].Id; } + private const string CampaignSessionKey = "campaign"; + private const string MobilePanelSessionKey = "play-panel"; + private readonly Action m_ClearAuthenticatedState; private readonly Func m_EnsureSelectedCharacterActiveAsync; private readonly WorkspaceFeedbackService m_Feedback; @@ -159,7 +149,4 @@ public sealed class WorkspaceCampaignScopeCoordinator private readonly WorkspaceState m_State; private readonly Func m_StopStateEventsAsync; private readonly WorkspaceQueryService m_WorkspaceQuery; - - private const string CampaignSessionKey = "campaign"; - private const string MobilePanelSessionKey = "play-panel"; -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/WorkspaceFeedbackService.cs b/RpgRoller/Components/Pages/WorkspaceFeedbackService.cs index c660c6a..82f58c5 100644 --- a/RpgRoller/Components/Pages/WorkspaceFeedbackService.cs +++ b/RpgRoller/Components/Pages/WorkspaceFeedbackService.cs @@ -27,7 +27,7 @@ public sealed class WorkspaceFeedbackService private void AddToast(string message, bool isError) { var toastId = Guid.NewGuid(); - m_State.Toasts.Add(new WorkspaceToast(toastId, message, isError)); + m_State.Toasts.Add(new(toastId, message, isError)); _ = DismissToastLaterAsync(toastId); } @@ -47,8 +47,8 @@ public sealed class WorkspaceFeedbackService } } + private const int ToastDurationMs = 3200; + private readonly Func m_RequestRefreshAsync; private readonly WorkspaceState m_State; - - private const int ToastDurationMs = 3200; -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/WorkspaceLiveStateController.cs b/RpgRoller/Components/Pages/WorkspaceLiveStateController.cs index 6343809..fbf80f4 100644 --- a/RpgRoller/Components/Pages/WorkspaceLiveStateController.cs +++ b/RpgRoller/Components/Pages/WorkspaceLiveStateController.cs @@ -6,15 +6,7 @@ namespace RpgRoller.Components.Pages; [ExcludeFromCodeCoverage] public sealed class WorkspaceLiveStateController { - public WorkspaceLiveStateController( - WorkspaceState state, - WorkspaceFeedbackService feedback, - Func startStateEventsAsync, - Func stopStateEventsCoreAsync, - Func refreshCampaignRosterAsync, - Func refreshSelectedCharacterSheetAsync, - Func refreshCampaignLogAsync, - Func requestRefreshAsync) + public WorkspaceLiveStateController(WorkspaceState state, WorkspaceFeedbackService feedback, Func startStateEventsAsync, Func stopStateEventsCoreAsync, Func refreshCampaignRosterAsync, Func refreshSelectedCharacterSheetAsync, Func refreshCampaignLogAsync, Func requestRefreshAsync) { m_State = state; m_Feedback = feedback; @@ -53,9 +45,7 @@ public sealed class WorkspaceLiveStateController await m_RefreshCampaignRosterAsync(); var selectedCharacterChanged = previousSelectedCharacterId != m_State.SelectedCharacterId; - var selectedCharacterVersionChanged = m_State.IsPlayScreen && - !selectedCharacterChanged && - GetCharacterVersion(state, m_State.SelectedCharacterId) != previousSelectedCharacterVersion; + var selectedCharacterVersionChanged = m_State.IsPlayScreen && !selectedCharacterChanged && GetCharacterVersion(state, m_State.SelectedCharacterId) != previousSelectedCharacterVersion; if (m_State.IsPlayScreen && (selectedCharacterChanged || selectedCharacterVersionChanged)) await m_RefreshSelectedCharacterSheetAsync(); @@ -116,9 +106,7 @@ public sealed class WorkspaceLiveStateController if (!characterId.HasValue) return 0; - return snapshot.CharacterVersions - .FirstOrDefault(version => version.CharacterId == characterId.Value) - ?.Version ?? 0; + return snapshot.CharacterVersions.FirstOrDefault(version => version.CharacterId == characterId.Value)?.Version ?? 0; } private readonly WorkspaceFeedbackService m_Feedback; @@ -129,4 +117,4 @@ public sealed class WorkspaceLiveStateController private readonly Func m_StartStateEventsAsync; private readonly WorkspaceState m_State; private readonly Func m_StopStateEventsCoreAsync; -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/WorkspacePlayCoordinator.cs b/RpgRoller/Components/Pages/WorkspacePlayCoordinator.cs index 1d726ba..81c3769 100644 --- a/RpgRoller/Components/Pages/WorkspacePlayCoordinator.cs +++ b/RpgRoller/Components/Pages/WorkspacePlayCoordinator.cs @@ -6,13 +6,7 @@ namespace RpgRoller.Components.Pages; [ExcludeFromCodeCoverage] public sealed class WorkspacePlayCoordinator { - public WorkspacePlayCoordinator( - WorkspaceState state, - WorkspaceFeedbackService feedback, - RpgRollerApiClient apiClient, - WorkspaceQueryService workspaceQuery, - Func canEditCharacter, - Func requestRefreshAsync) + public WorkspacePlayCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, RpgRollerApiClient apiClient, WorkspaceQueryService workspaceQuery, Func canEditCharacter, Func requestRefreshAsync) { m_State = state; m_Feedback = feedback; @@ -36,9 +30,7 @@ public sealed class WorkspacePlayCoordinator var page = await m_WorkspaceQuery.GetCampaignLogPageAsync(m_State.SelectedCampaignId.Value, afterRollId, CampaignLogWindowSize); Guid? newestRollId = null; if (!afterRollId.HasValue || page.ResetRequired) - { m_State.CampaignLog = page.Entries.ToList(); - } else if (page.Entries.Length > 0) { m_State.CampaignLog.AddRange(page.Entries); @@ -47,14 +39,8 @@ public sealed class WorkspacePlayCoordinator } var shouldAutoExpandNewest = afterRollId.HasValue && page.Entries.Length > 0; - if (!shouldAutoExpandNewest && - !afterRollId.HasValue && - m_State.CurrentCampaignState is not null && - previousLogCount == 0 && - page.Entries.Length > 0) - { + if (!shouldAutoExpandNewest && !afterRollId.HasValue && m_State.CurrentCampaignState is not null && previousLogCount == 0 && page.Entries.Length > 0) shouldAutoExpandNewest = true; - } if (shouldAutoExpandNewest) { @@ -63,9 +49,7 @@ public sealed class WorkspacePlayCoordinator m_State.FreshCampaignLogRollId = newestRollId; } else if (!afterRollId.HasValue) - { m_State.FreshCampaignLogRollId = null; - } m_State.CampaignLogCursor = page.Cursor ?? afterRollId; TrimCampaignLogDetails(); @@ -91,12 +75,8 @@ public sealed class WorkspacePlayCoordinator } var sheet = await m_WorkspaceQuery.GetCharacterSheetAsync(m_State.SelectedCharacterId.Value); - m_State.SelectedCharacterSkillGroups = sheet.SkillGroups - .OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase) - .ToList(); - m_State.SelectedCharacterSkills = sheet.Skills - .OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase) - .ToList(); + m_State.SelectedCharacterSkillGroups = sheet.SkillGroups.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase).ToList(); + m_State.SelectedCharacterSkills = sheet.Skills.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase).ToList(); } public Task EnsureSelectedCharacterActiveAsync() @@ -338,15 +318,15 @@ public sealed class WorkspacePlayCoordinator private static CampaignRollDetail ToCampaignRollDetail(RollResult roll) { - return new CampaignRollDetail(roll.RollId, roll.Breakdown, roll.Dice.ToArray()); + return new(roll.RollId, roll.Breakdown, roll.Dice.ToArray()); } + private const int CampaignLogWindowSize = 25; + private readonly RpgRollerApiClient m_ApiClient; private readonly Func m_CanEditCharacter; private readonly WorkspaceFeedbackService m_Feedback; private readonly Func m_RequestRefreshAsync; private readonly WorkspaceState m_State; private readonly WorkspaceQueryService m_WorkspaceQuery; - - private const int CampaignLogWindowSize = 25; -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/WorkspaceSessionCoordinator.cs b/RpgRoller/Components/Pages/WorkspaceSessionCoordinator.cs index f4d2795..22b93a7 100644 --- a/RpgRoller/Components/Pages/WorkspaceSessionCoordinator.cs +++ b/RpgRoller/Components/Pages/WorkspaceSessionCoordinator.cs @@ -5,21 +5,7 @@ namespace RpgRoller.Components.Pages; public sealed class WorkspaceSessionCoordinator { - public WorkspaceSessionCoordinator( - WorkspaceState state, - WorkspaceFeedbackService feedback, - IJSRuntime js, - RpgRollerApiClient apiClient, - WorkspaceQueryService workspaceQuery, - Func reloadCampaignsAsync, - Func reloadCharacterCampaignOptionsAsync, - Func refreshCampaignScopeAsync, - Func syncStateEventsAsync, - Func stopStateEventsAsync, - Func ensureAdminUsersLoadedAsync, - Action resetCampaignLogDetailState, - Func requestRefreshAsync, - Func onLoggedOutAsync) + public WorkspaceSessionCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, IJSRuntime js, RpgRollerApiClient apiClient, WorkspaceQueryService workspaceQuery, Func reloadCampaignsAsync, Func reloadCharacterCampaignOptionsAsync, Func refreshCampaignScopeAsync, Func syncStateEventsAsync, Func stopStateEventsAsync, Func ensureAdminUsersLoadedAsync, Action resetCampaignLogDetailState, Func requestRefreshAsync, Func onLoggedOutAsync) { m_State = state; m_Feedback = feedback; @@ -278,21 +264,6 @@ public sealed class WorkspaceSessionCoordinator return exception.Message.Contains("JavaScript interop calls cannot be issued", StringComparison.OrdinalIgnoreCase); } - private readonly RpgRollerApiClient m_ApiClient; - private readonly WorkspaceFeedbackService m_Feedback; - private readonly Func m_EnsureAdminUsersLoadedAsync; - private readonly IJSRuntime m_JS; - private readonly Func m_OnLoggedOutAsync; - private readonly Func m_ReloadCharacterCampaignOptionsAsync; - private readonly Func m_ReloadCampaignsAsync; - private readonly Action m_ResetCampaignLogDetailState; - private readonly Func m_RefreshCampaignScopeAsync; - private readonly Func m_RequestRefreshAsync; - private readonly WorkspaceState m_State; - private readonly Func m_StopStateEventsAsync; - private readonly Func m_SyncStateEventsAsync; - private readonly WorkspaceQueryService m_WorkspaceQuery; - private const string ScreenPlay = "play"; private const string ScreenManagement = "management"; private const string ScreenAdmin = "admin"; @@ -300,4 +271,19 @@ public sealed class WorkspaceSessionCoordinator private const string CampaignSessionKey = "campaign"; private const string MobilePanelSessionKey = "play-panel"; private const string RollVisibilitySessionKey = "roll-visibility"; -} + + private readonly RpgRollerApiClient m_ApiClient; + private readonly Func m_EnsureAdminUsersLoadedAsync; + private readonly WorkspaceFeedbackService m_Feedback; + private readonly IJSRuntime m_JS; + private readonly Func m_OnLoggedOutAsync; + private readonly Func m_RefreshCampaignScopeAsync; + private readonly Func m_ReloadCampaignsAsync; + private readonly Func m_ReloadCharacterCampaignOptionsAsync; + private readonly Func m_RequestRefreshAsync; + private readonly Action m_ResetCampaignLogDetailState; + private readonly WorkspaceState m_State; + private readonly Func m_StopStateEventsAsync; + private readonly Func m_SyncStateEventsAsync; + private readonly WorkspaceQueryService m_WorkspaceQuery; +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/WorkspaceState.cs b/RpgRoller/Components/Pages/WorkspaceState.cs index abe75ea..71895a2 100644 --- a/RpgRoller/Components/Pages/WorkspaceState.cs +++ b/RpgRoller/Components/Pages/WorkspaceState.cs @@ -1,11 +1,41 @@ +using RpgRoller.Components.Pages.HomeControls; using RpgRoller.Contracts; using RpgRoller.Domain; -using RpgRoller.Components.Pages.HomeControls; namespace RpgRoller.Components.Pages; public sealed class WorkspaceState { + public string OwnerLabel(Guid ownerUserId) + { + if (User is not null && ownerUserId == User.Id) + return "You"; + + if (SelectedCampaign is null) + return "Unknown owner"; + + if (ownerUserId == SelectedCampaign.Gm.Id) + return $"{SelectedCampaign.Gm.DisplayName} (GM)"; + + var ownerDisplayName = SelectedCampaign.Characters.Where(character => character.OwnerUserId == ownerUserId).Select(character => character.OwnerDisplayName).FirstOrDefault(displayName => !string.IsNullOrWhiteSpace(displayName)); + + return string.IsNullOrWhiteSpace(ownerDisplayName) ? "Unknown owner" : ownerDisplayName; + } + + public string SkillDefinitionLabel(CharacterSheetSkill skill) + { + if (!string.Equals(SelectedCampaign?.RulesetId, "d6", StringComparison.OrdinalIgnoreCase)) + { + if (string.Equals(SelectedCampaign?.RulesetId, RulesetFormHelpers.RulesetIds.Rolemaster, StringComparison.OrdinalIgnoreCase)) + return RulesetFormHelpers.DescribeRolemasterExpression(skill.DiceRollDefinition, skill.FumbleRange); + + return skill.DiceRollDefinition; + } + + var fumbleLabel = skill.AllowFumble ? "fumble on" : "fumble off"; + return $"{skill.DiceRollDefinition}, wild {skill.WildDice}, {fumbleLabel}"; + } + public UserSummary? User { get; set; } public Guid? ActiveCharacterId { get; set; } public Guid? SelectedCampaignId { get; set; } @@ -66,18 +96,11 @@ public sealed class WorkspaceState return null; if (User is null) - return new CampaignRoster(SelectedCampaign.Id, SelectedCampaign.Name, SelectedCampaign.RulesetId, SelectedCampaign.Gm, []); + return new(SelectedCampaign.Id, SelectedCampaign.Name, SelectedCampaign.RulesetId, SelectedCampaign.Gm, []); - var ownedCharacters = SelectedCampaign.Characters - .Where(character => character.OwnerUserId == User.Id) - .ToArray(); + var ownedCharacters = SelectedCampaign.Characters.Where(character => character.OwnerUserId == User.Id).ToArray(); - return new CampaignRoster( - SelectedCampaign.Id, - SelectedCampaign.Name, - SelectedCampaign.RulesetId, - SelectedCampaign.Gm, - ownedCharacters); + return new(SelectedCampaign.Id, SelectedCampaign.Name, SelectedCampaign.RulesetId, SelectedCampaign.Gm, ownedCharacters); } } @@ -148,37 +171,4 @@ public sealed class WorkspaceState }; public string AppCssClass => IsPlayScreen ? "rr-app app-play" : "rr-app"; - - public string OwnerLabel(Guid ownerUserId) - { - if (User is not null && ownerUserId == User.Id) - return "You"; - - if (SelectedCampaign is null) - return "Unknown owner"; - - if (ownerUserId == SelectedCampaign.Gm.Id) - return $"{SelectedCampaign.Gm.DisplayName} (GM)"; - - var ownerDisplayName = SelectedCampaign.Characters - .Where(character => character.OwnerUserId == ownerUserId) - .Select(character => character.OwnerDisplayName) - .FirstOrDefault(displayName => !string.IsNullOrWhiteSpace(displayName)); - - return string.IsNullOrWhiteSpace(ownerDisplayName) ? "Unknown owner" : ownerDisplayName; - } - - public string SkillDefinitionLabel(CharacterSheetSkill skill) - { - if (!string.Equals(SelectedCampaign?.RulesetId, "d6", StringComparison.OrdinalIgnoreCase)) - { - if (string.Equals(SelectedCampaign?.RulesetId, RulesetFormHelpers.RulesetIds.Rolemaster, StringComparison.OrdinalIgnoreCase)) - return RulesetFormHelpers.DescribeRolemasterExpression(skill.DiceRollDefinition, skill.FumbleRange); - - return skill.DiceRollDefinition; - } - - var fumbleLabel = skill.AllowFumble ? "fumble on" : "fumble off"; - return $"{skill.DiceRollDefinition}, wild {skill.WildDice}, {fumbleLabel}"; - } -} +} \ No newline at end of file diff --git a/RpgRoller/Components/Pages/WorkspaceToast.cs b/RpgRoller/Components/Pages/WorkspaceToast.cs index 1af114b..daee8a5 100644 --- a/RpgRoller/Components/Pages/WorkspaceToast.cs +++ b/RpgRoller/Components/Pages/WorkspaceToast.cs @@ -1,3 +1,3 @@ namespace RpgRoller.Components.Pages; -public sealed record WorkspaceToast(Guid Id, string Message, bool IsError); +public sealed record WorkspaceToast(Guid Id, string Message, bool IsError); \ No newline at end of file diff --git a/RpgRoller/Components/Routes.razor b/RpgRoller/Components/Routes.razor index aef77a6..b31fe8b 100644 --- a/RpgRoller/Components/Routes.razor +++ b/RpgRoller/Components/Routes.razor @@ -6,4 +6,4 @@ - + \ No newline at end of file diff --git a/RpgRoller/Components/RpgRollerApiClient.cs b/RpgRoller/Components/RpgRollerApiClient.cs index 42d5751..5b75e24 100644 --- a/RpgRoller/Components/RpgRollerApiClient.cs +++ b/RpgRoller/Components/RpgRollerApiClient.cs @@ -53,4 +53,4 @@ public sealed class ApiRequestException : Exception public int StatusCode { get; } public string? ErrorCode { get; } -} +} \ No newline at end of file diff --git a/RpgRoller/Components/WorkspaceQueryService.cs b/RpgRoller/Components/WorkspaceQueryService.cs index 62f96df..917b1e7 100644 --- a/RpgRoller/Components/WorkspaceQueryService.cs +++ b/RpgRoller/Components/WorkspaceQueryService.cs @@ -82,9 +82,9 @@ public sealed class WorkspaceQueryService private static ApiRequestException ToApiRequestException(ServiceError error) { var statusCode = error.Code == "unauthorized" ? 401 : 400; - return new ApiRequestException(statusCode, error.Message, error.Code); + return new(statusCode, error.Message, error.Code); } private readonly IGameService m_GameService; private readonly WorkspaceSessionTokenAccessor m_SessionTokenAccessor; -} +} \ No newline at end of file diff --git a/RpgRoller/Components/WorkspaceSessionTokenAccessor.cs b/RpgRoller/Components/WorkspaceSessionTokenAccessor.cs index 6bf47e7..81fc55f 100644 --- a/RpgRoller/Components/WorkspaceSessionTokenAccessor.cs +++ b/RpgRoller/Components/WorkspaceSessionTokenAccessor.cs @@ -10,9 +10,7 @@ public sealed class WorkspaceSessionTokenAccessor if (httpContext is null) return; - if (httpContext.Items.TryGetValue(SessionTokenItemKey, out var storedToken) && - storedToken is string sessionToken && - !string.IsNullOrWhiteSpace(sessionToken)) + if (httpContext.Items.TryGetValue(SessionTokenItemKey, out var storedToken) && storedToken is string sessionToken && !string.IsNullOrWhiteSpace(sessionToken)) { m_SessionToken = sessionToken; return; @@ -32,4 +30,4 @@ public sealed class WorkspaceSessionTokenAccessor private const string SessionTokenItemKey = "__rpgroller.session-token"; private readonly string? m_SessionToken; -} +} \ No newline at end of file diff --git a/RpgRoller/Components/_Imports.razor b/RpgRoller/Components/_Imports.razor index e928c57..c44c86e 100644 --- a/RpgRoller/Components/_Imports.razor +++ b/RpgRoller/Components/_Imports.razor @@ -6,4 +6,4 @@ @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.JSInterop -@using static Microsoft.AspNetCore.Components.Web.RenderMode +@using static Microsoft.AspNetCore.Components.Web.RenderMode \ No newline at end of file diff --git a/RpgRoller/Contracts/ApiContracts.cs b/RpgRoller/Contracts/ApiContracts.cs index ed987c9..5257387 100644 --- a/RpgRoller/Contracts/ApiContracts.cs +++ b/RpgRoller/Contracts/ApiContracts.cs @@ -115,7 +115,8 @@ public sealed record CampaignLogListEntry( string VisibilityStyle, int Result, string SummaryText, - [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string[]? EventBadges, + [property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + string[]? EventBadges, DateTimeOffset TimestampUtc); public sealed record CampaignRollDetail(Guid RollId, string Breakdown, RollDieResult[] Dice); @@ -124,4 +125,4 @@ public sealed record CharacterStateVersion(Guid CharacterId, long Version); public sealed record CampaignStateSnapshot(Guid CampaignId, long TotalVersion, long RosterVersion, long LogVersion, IReadOnlyList CharacterVersions); -public sealed record CampaignLogPage(CampaignLogListEntry[] Entries, Guid? Cursor, bool HasMore, bool ResetRequired); +public sealed record CampaignLogPage(CampaignLogListEntry[] Entries, Guid? Cursor, bool HasMore, bool ResetRequired); \ No newline at end of file diff --git a/RpgRoller/Contracts/RpgRollerJson.cs b/RpgRoller/Contracts/RpgRollerJson.cs index b15cb10..16033b8 100644 --- a/RpgRoller/Contracts/RpgRollerJson.cs +++ b/RpgRoller/Contracts/RpgRollerJson.cs @@ -16,4 +16,4 @@ public static class RpgRollerJson if (!options.TypeInfoResolverChain.Contains(RpgRollerJsonSerializerContext.Default)) options.TypeInfoResolverChain.Insert(0, RpgRollerJsonSerializerContext.Default); } -} +} \ No newline at end of file diff --git a/RpgRoller/Contracts/RpgRollerJsonSerializerContext.cs b/RpgRoller/Contracts/RpgRollerJsonSerializerContext.cs index 7960dfc..e55949e 100644 --- a/RpgRoller/Contracts/RpgRollerJsonSerializerContext.cs +++ b/RpgRoller/Contracts/RpgRollerJsonSerializerContext.cs @@ -56,4 +56,4 @@ namespace RpgRoller.Contracts; [JsonSerializable(typeof(UserSummary))] public partial class RpgRollerJsonSerializerContext : JsonSerializerContext { -} +} \ No newline at end of file diff --git a/RpgRoller/Data/RpgRollerDbContext.cs b/RpgRoller/Data/RpgRollerDbContext.cs index 72cd927..853a943 100644 --- a/RpgRoller/Data/RpgRollerDbContext.cs +++ b/RpgRoller/Data/RpgRollerDbContext.cs @@ -91,4 +91,4 @@ public sealed class RpgRollerDbContext : DbContext public DbSet Skills => Set(); public DbSet SkillGroups => Set(); public DbSet RollLogEntries => Set(); -} +} \ No newline at end of file diff --git a/RpgRoller/Domain/GameModels.cs b/RpgRoller/Domain/GameModels.cs index 4b64cb3..4dc9992 100644 --- a/RpgRoller/Domain/GameModels.cs +++ b/RpgRoller/Domain/GameModels.cs @@ -96,4 +96,4 @@ public enum DiceExpressionKind RolemasterOpenEndedPercentile } -public sealed record DiceExpression(int DiceCount, int Sides, int Modifier, string Canonical, DiceExpressionKind Kind = DiceExpressionKind.Standard); +public sealed record DiceExpression(int DiceCount, int Sides, int Modifier, string Canonical, DiceExpressionKind Kind = DiceExpressionKind.Standard); \ No newline at end of file diff --git a/RpgRoller/Hosting/ServiceCollectionExtensions.cs b/RpgRoller/Hosting/ServiceCollectionExtensions.cs index d8cd9f0..8d5d60e 100644 --- a/RpgRoller/Hosting/ServiceCollectionExtensions.cs +++ b/RpgRoller/Hosting/ServiceCollectionExtensions.cs @@ -40,4 +40,4 @@ public static class ServiceCollectionExtensions if (!string.IsNullOrWhiteSpace(directory)) Directory.CreateDirectory(directory); } -} +} \ No newline at end of file diff --git a/RpgRoller/Hosting/SqliteDatabaseFile.cs b/RpgRoller/Hosting/SqliteDatabaseFile.cs index 6ec8aef..26ebf87 100644 --- a/RpgRoller/Hosting/SqliteDatabaseFile.cs +++ b/RpgRoller/Hosting/SqliteDatabaseFile.cs @@ -1,3 +1,3 @@ namespace RpgRoller.Hosting; -public sealed record SqliteDatabaseFile(string? Path); +public sealed record SqliteDatabaseFile(string? Path); \ No newline at end of file diff --git a/RpgRoller/Hosting/SqliteSchemaUpgrader.cs b/RpgRoller/Hosting/SqliteSchemaUpgrader.cs index a69e642..6c4f76a 100644 --- a/RpgRoller/Hosting/SqliteSchemaUpgrader.cs +++ b/RpgRoller/Hosting/SqliteSchemaUpgrader.cs @@ -133,4 +133,4 @@ public static class SqliteSchemaUpgrader private const string CharactersCampaignDeletionMigrationId = "20260226160859_AddAuthorizationRolesAndCampaignDeletion"; private const string AuthorizationRolesMigrationId = "20260226170000_AddAuthorizationRoles"; private const string ProductVersion = "10.0.2"; -} +} \ No newline at end of file diff --git a/RpgRoller/Program.cs b/RpgRoller/Program.cs index aa81dd5..ee74e04 100644 --- a/RpgRoller/Program.cs +++ b/RpgRoller/Program.cs @@ -41,4 +41,4 @@ app.MapStaticAssets(); app.MapRazorComponents().AddInteractiveServerRenderMode(); app.Run(); -public partial class Program; +public partial class Program; \ No newline at end of file diff --git a/RpgRoller/Services/CampaignLogSummaryBuilder.cs b/RpgRoller/Services/CampaignLogSummaryBuilder.cs index 32438e7..0e33a53 100644 --- a/RpgRoller/Services/CampaignLogSummaryBuilder.cs +++ b/RpgRoller/Services/CampaignLogSummaryBuilder.cs @@ -35,12 +35,7 @@ public static class CampaignLogSummaryBuilder break; case RulesetKind.Rolemaster: - AddBadgeIfMissing( - badges, - dice.Any(die => - string.Equals(die.Kind, RollDieKinds.RolemasterOpenEndedInitial, StringComparison.Ordinal) && - !die.SignedContribution.HasValue), - "rf"); + AddBadgeIfMissing(badges, dice.Any(die => string.Equals(die.Kind, RollDieKinds.RolemasterOpenEndedInitial, StringComparison.Ordinal) && !die.SignedContribution.HasValue), "rf"); AddBadgeIfMissing(badges, dice.Any(die => die.Roll == 100), "r100"); AddBadgeIfMissing(badges, dice.Any(die => die.Roll == 66), "r66"); break; @@ -63,17 +58,11 @@ public static class CampaignLogSummaryBuilder var openEndedInitial = dice.FirstOrDefault(die => string.Equals(die.Kind, RollDieKinds.RolemasterOpenEndedInitial, StringComparison.Ordinal)); if (openEndedInitial is not null) { - var highFollowUps = dice - .Where(die => string.Equals(die.Kind, RollDieKinds.RolemasterOpenEndedHigh, StringComparison.Ordinal)) - .Select(die => die.Roll.ToString()) - .ToArray(); + var highFollowUps = dice.Where(die => string.Equals(die.Kind, RollDieKinds.RolemasterOpenEndedHigh, StringComparison.Ordinal)).Select(die => die.Roll.ToString()).ToArray(); if (highFollowUps.Length > 0) return $"{openEndedInitial.Roll} + {string.Join(" + ", highFollowUps)} | open-ended high"; - var lowFollowUps = dice - .Where(die => string.Equals(die.Kind, RollDieKinds.RolemasterOpenEndedLowSubtract, StringComparison.Ordinal)) - .Select(die => die.Roll.ToString()) - .ToArray(); + var lowFollowUps = dice.Where(die => string.Equals(die.Kind, RollDieKinds.RolemasterOpenEndedLowSubtract, StringComparison.Ordinal)).Select(die => die.Roll.ToString()).ToArray(); if (lowFollowUps.Length > 0) return $"({RollBreakdownFormatter.FormatRolemasterTriggerRoll(openEndedInitial.Roll)}) {string.Join(" ", lowFollowUps.Select(roll => $"-{roll}"))} | open-ended low"; @@ -91,10 +80,7 @@ public static class CampaignLogSummaryBuilder private static bool IsRolemasterDieKind(string? kind) { - return kind is RollDieKinds.RolemasterStandard or - RollDieKinds.RolemasterOpenEndedInitial or - RollDieKinds.RolemasterOpenEndedHigh or - RollDieKinds.RolemasterOpenEndedLowSubtract; + return kind is RollDieKinds.RolemasterStandard or RollDieKinds.RolemasterOpenEndedInitial or RollDieKinds.RolemasterOpenEndedHigh or RollDieKinds.RolemasterOpenEndedLowSubtract; } private static void AddBadgeIfMissing(List badges, bool condition, string code) @@ -108,8 +94,6 @@ public static class CampaignLogSummaryBuilder private static bool IsSingleD20Expression(string expression) { var parsedExpression = DiceRules.ParseExpression(RulesetKind.Dnd5e, expression); - return parsedExpression.Succeeded && - parsedExpression.Value!.DiceCount == 1 && - parsedExpression.Value.Sides == 20; + return parsedExpression.Succeeded && parsedExpression.Value!.DiceCount == 1 && parsedExpression.Value.Sides == 20; } -} +} \ No newline at end of file diff --git a/RpgRoller/Services/CustomRollOptionsResolver.cs b/RpgRoller/Services/CustomRollOptionsResolver.cs index 16d7a9a..f68f224 100644 --- a/RpgRoller/Services/CustomRollOptionsResolver.cs +++ b/RpgRoller/Services/CustomRollOptionsResolver.cs @@ -4,9 +4,6 @@ namespace RpgRoller.Services; public static class CustomRollOptionsResolver { - private const int DefaultCustomD6WildDice = 1; - private const bool DefaultCustomD6AllowFumble = true; - public static (int WildDice, bool AllowFumble, int? FumbleRange) Resolve(RulesetKind ruleset) { return ruleset switch @@ -15,4 +12,7 @@ public static class CustomRollOptionsResolver _ => (0, false, null) }; } -} + + private const int DefaultCustomD6WildDice = 1; + private const bool DefaultCustomD6AllowFumble = true; +} \ No newline at end of file diff --git a/RpgRoller/Services/D6RollEngine.cs b/RpgRoller/Services/D6RollEngine.cs index 5953561..36f21db 100644 --- a/RpgRoller/Services/D6RollEngine.cs +++ b/RpgRoller/Services/D6RollEngine.cs @@ -91,4 +91,4 @@ public sealed class D6RollEngine } private readonly IDiceRoller m_DiceRoller; -} +} \ No newline at end of file diff --git a/RpgRoller/Services/DiceRules.cs b/RpgRoller/Services/DiceRules.cs index 157c325..3e1de43 100644 --- a/RpgRoller/Services/DiceRules.cs +++ b/RpgRoller/Services/DiceRules.cs @@ -86,16 +86,14 @@ public static partial class DiceRules var diceCount = string.IsNullOrEmpty(countValue) ? 1 : int.Parse(countValue); var sides = int.Parse(match.Groups["sides"].Value); var modifier = ParseModifier(match.Groups["modifier"].Value); - var validation = ValidateDiceParts(diceCount, sides, modifier, -MaxModifier, MaxModifier); + var validation = ValidateDiceParts(diceCount, sides, modifier, -MaxModifier); if (!validation.Succeeded) return ServiceResult.Failure(validation.Error!.Code, validation.Error.Message); var isOpenEnded = match.Groups["openEnded"].Success; if (isOpenEnded && (diceCount != 1 || sides != 100)) { - return ServiceResult.Failure( - "invalid_expression", - "Open-ended Rolemaster rolls must use d100! with an implicit or explicit dice count of 1."); + return ServiceResult.Failure("invalid_expression", "Open-ended Rolemaster rolls must use d100! with an implicit or explicit dice count of 1."); } var countPrefix = diceCount == 1 ? string.Empty : diceCount.ToString(); @@ -152,4 +150,4 @@ public static partial class DiceRules (RulesetKind.Dnd5e, "dnd5e", "D&D 5e", "countdSides(+modifier), e.g. 2d12+2"), (RulesetKind.Rolemaster, "rolemaster", "Rolemaster", "countdSides(+/-modifier), e.g. d10, 15d10, d100-15, d100!+85") ]; -} +} \ No newline at end of file diff --git a/RpgRoller/Services/GameAuthService.cs b/RpgRoller/Services/GameAuthService.cs index 300959f..a9b3cc5 100644 --- a/RpgRoller/Services/GameAuthService.cs +++ b/RpgRoller/Services/GameAuthService.cs @@ -141,4 +141,4 @@ public sealed class GameAuthService private readonly IPasswordHasher m_PasswordHasher; private readonly GamePersistenceService m_PersistenceService; private readonly GameStateStore m_StateStore; -} +} \ No newline at end of file diff --git a/RpgRoller/Services/GameAuthorization.cs b/RpgRoller/Services/GameAuthorization.cs index 86f5703..1eb7324 100644 --- a/RpgRoller/Services/GameAuthorization.cs +++ b/RpgRoller/Services/GameAuthorization.cs @@ -18,9 +18,7 @@ public static class GameAuthorization if (campaign.GmUserId == actorUserId) return true; - return stateStore.CharactersById.Values.Any(character => - character.CampaignId == campaignId && - character.OwnerUserId == actorUserId); + return stateStore.CharactersById.Values.Any(character => character.CampaignId == campaignId && character.OwnerUserId == actorUserId); } public static bool CanEditCharacter(Guid actorUserId, Character character, Campaign campaign) @@ -30,7 +28,6 @@ public static class GameAuthorization public static bool CanViewRoll(GameStateStore stateStore, Guid actorUserId, Campaign campaign, RollLogEntry entry) { - return CanViewCampaign(stateStore, actorUserId, campaign.Id) && - (entry.Visibility == RollVisibility.Public || entry.RollerUserId == actorUserId || campaign.GmUserId == actorUserId); + return CanViewCampaign(stateStore, actorUserId, campaign.Id) && (entry.Visibility == RollVisibility.Public || entry.RollerUserId == actorUserId || campaign.GmUserId == actorUserId); } -} +} \ No newline at end of file diff --git a/RpgRoller/Services/GameCampaignService.cs b/RpgRoller/Services/GameCampaignService.cs index bbf60fa..0895b48 100644 --- a/RpgRoller/Services/GameCampaignService.cs +++ b/RpgRoller/Services/GameCampaignService.cs @@ -49,11 +49,7 @@ public sealed class GameCampaignService if (user is null) return ServiceResult>.Failure("unauthorized", "You must be logged in."); - var results = m_StateStore.CampaignsById.Values - .Where(campaign => GameAuthorization.CanViewCampaign(m_StateStore, user.Id, campaign.Id)) - .OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase) - .Select(campaign => GameDtoMapper.ToCampaignSummary(m_StateStore, campaign)) - .ToArray(); + var results = m_StateStore.CampaignsById.Values.Where(campaign => GameAuthorization.CanViewCampaign(m_StateStore, user.Id, campaign.Id)).OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).Select(campaign => GameDtoMapper.ToCampaignSummary(m_StateStore, campaign)).ToArray(); return ServiceResult>.Success(results); } @@ -67,10 +63,7 @@ public sealed class GameCampaignService if (user is null) return ServiceResult>.Failure("unauthorized", "You must be logged in."); - var options = m_StateStore.CampaignsById.Values - .OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase) - .Select(GameDtoMapper.ToCampaignOption) - .ToArray(); + var options = m_StateStore.CampaignsById.Values.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).Select(GameDtoMapper.ToCampaignOption).ToArray(); return ServiceResult>.Success(options); } @@ -124,4 +117,4 @@ public sealed class GameCampaignService private readonly GamePersistenceService m_PersistenceService; private readonly GameStateStore m_StateStore; -} +} \ No newline at end of file diff --git a/RpgRoller/Services/GameCharacterService.cs b/RpgRoller/Services/GameCharacterService.cs index b64e710..7c15186 100644 --- a/RpgRoller/Services/GameCharacterService.cs +++ b/RpgRoller/Services/GameCharacterService.cs @@ -62,9 +62,7 @@ public sealed class GameCharacterService var isOwner = character.OwnerUserId == user.Id; var isAdmin = GameAuthorization.HasRole(user, UserRoles.Admin); - var isSourceGm = character.CampaignId.HasValue && - m_StateStore.CampaignsById.TryGetValue(character.CampaignId.Value, out var sourceCampaign) && - sourceCampaign.GmUserId == user.Id; + var isSourceGm = character.CampaignId.HasValue && m_StateStore.CampaignsById.TryGetValue(character.CampaignId.Value, out var sourceCampaign) && sourceCampaign.GmUserId == user.Id; var isTargetGm = targetCampaign is not null && targetCampaign.GmUserId == user.Id; if (!isOwner && !isAdmin && !isSourceGm && !isTargetGm) return ServiceResult.Failure("forbidden", "Only the owner, GM, or admin can edit this character."); @@ -85,12 +83,8 @@ public sealed class GameCharacterService return ServiceResult.Failure("forbidden", "Only the GM or admin can change character owner."); character.OwnerUserId = targetOwnerUserId; - if (character.OwnerUserId != previousOwnerUserId && - m_StateStore.UsersById.TryGetValue(previousOwnerUserId, out var previousOwner) && - previousOwner.ActiveCharacterId == character.Id) - { + if (character.OwnerUserId != previousOwnerUserId && m_StateStore.UsersById.TryGetValue(previousOwnerUserId, out var previousOwner) && previousOwner.ActiveCharacterId == character.Id) previousOwner.ActiveCharacterId = null; - } } if (sourceCampaignId != character.CampaignId) @@ -158,11 +152,7 @@ public sealed class GameCharacterService if (user is null) return ServiceResult>.Failure("unauthorized", "You must be logged in."); - var characters = m_StateStore.CharactersById.Values - .Where(character => character.OwnerUserId == user.Id) - .OrderBy(character => character.Name, StringComparer.OrdinalIgnoreCase) - .Select(character => GameDtoMapper.ToCharacterSummary(m_StateStore, character)) - .ToArray(); + var characters = m_StateStore.CharactersById.Values.Where(character => character.OwnerUserId == user.Id).OrderBy(character => character.Name, StringComparer.OrdinalIgnoreCase).Select(character => GameDtoMapper.ToCharacterSummary(m_StateStore, character)).ToArray(); return ServiceResult>.Success(characters); } @@ -200,4 +190,4 @@ public sealed class GameCharacterService private readonly GamePersistenceService m_PersistenceService; private readonly GameStateStore m_StateStore; -} +} \ No newline at end of file diff --git a/RpgRoller/Services/GameContextResolver.cs b/RpgRoller/Services/GameContextResolver.cs index f5df96f..6c1eb02 100644 --- a/RpgRoller/Services/GameContextResolver.cs +++ b/RpgRoller/Services/GameContextResolver.cs @@ -33,11 +33,9 @@ public static class GameContextResolver public static bool TryResolveCharacterCampaignLocked(GameStateStore stateStore, Character character, out Campaign campaign, out ServiceError? error) { campaign = default!; - if (!character.CampaignId.HasValue || - !stateStore.CampaignsById.TryGetValue(character.CampaignId.Value, out var resolvedCampaign) || - resolvedCampaign is null) + if (!character.CampaignId.HasValue || !stateStore.CampaignsById.TryGetValue(character.CampaignId.Value, out var resolvedCampaign) || resolvedCampaign is null) { - error = new ServiceError("character_not_in_campaign", "Character is not linked to a campaign."); + error = new("character_not_in_campaign", "Character is not linked to a campaign."); return false; } @@ -45,4 +43,4 @@ public static class GameContextResolver error = null; return true; } -} +} \ No newline at end of file diff --git a/RpgRoller/Services/GameDtoMapper.cs b/RpgRoller/Services/GameDtoMapper.cs index a0f21d0..fd2b081 100644 --- a/RpgRoller/Services/GameDtoMapper.cs +++ b/RpgRoller/Services/GameDtoMapper.cs @@ -24,19 +24,15 @@ public static class GameDtoMapper { var gm = stateStore.UsersById[campaign.GmUserId]; var characterCount = stateStore.CharactersById.Values.Count(character => character.CampaignId == campaign.Id); - return new(campaign.Id, campaign.Name, DiceRules.ToRulesetId(campaign.Ruleset), new CampaignGmSummary(gm.Id, gm.DisplayName), characterCount); + return new(campaign.Id, campaign.Name, DiceRules.ToRulesetId(campaign.Ruleset), new(gm.Id, gm.DisplayName), characterCount); } public static CampaignRoster ToCampaignRoster(GameStateStore stateStore, Campaign campaign) { var gm = stateStore.UsersById[campaign.GmUserId]; - var characters = stateStore.CharactersById.Values - .Where(character => character.CampaignId == campaign.Id) - .OrderBy(character => character.Name, StringComparer.OrdinalIgnoreCase) - .Select(character => ToCharacterSummary(stateStore, character)) - .ToArray(); + var characters = stateStore.CharactersById.Values.Where(character => character.CampaignId == campaign.Id).OrderBy(character => character.Name, StringComparer.OrdinalIgnoreCase).Select(character => ToCharacterSummary(stateStore, character)).ToArray(); - return new(campaign.Id, campaign.Name, DiceRules.ToRulesetId(campaign.Ruleset), new CampaignGmSummary(gm.Id, gm.DisplayName), characters); + return new(campaign.Id, campaign.Name, DiceRules.ToRulesetId(campaign.Ruleset), new(gm.Id, gm.DisplayName), characters); } public static CharacterSummary ToCharacterSummary(GameStateStore stateStore, Character character) @@ -46,16 +42,8 @@ public static class GameDtoMapper public static CharacterSheet ToCharacterSheet(GameStateStore stateStore, Guid characterId) { - var skillGroups = stateStore.SkillGroupsById.Values - .Where(group => group.CharacterId == characterId) - .OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase) - .Select(ToCharacterSheetSkillGroup) - .ToArray(); - var skills = stateStore.SkillsById.Values - .Where(skill => skill.CharacterId == characterId) - .OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase) - .Select(ToCharacterSheetSkill) - .ToArray(); + var skillGroups = stateStore.SkillGroupsById.Values.Where(group => group.CharacterId == characterId).OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase).Select(ToCharacterSheetSkillGroup).ToArray(); + var skills = stateStore.SkillsById.Values.Where(skill => skill.CharacterId == characterId).OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase).Select(ToCharacterSheetSkill).ToArray(); return new(characterId, skillGroups, skills); } @@ -77,43 +65,12 @@ public static class GameDtoMapper public static CampaignLogEntry ToCampaignLogEntry(RollLogEntry entry, string characterName, string skillName, string rollerDisplayName, IReadOnlyList dice) { - 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); + 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); } - public static CampaignLogListEntry ToCampaignLogListEntry( - RollLogEntry entry, - string characterName, - string skillName, - string rollerLabel, - string visibilityLabel, - string visibilityStyle, - string summaryText, - string[]? eventBadges) + public static CampaignLogListEntry ToCampaignLogListEntry(RollLogEntry entry, string characterName, string skillName, string rollerLabel, string visibilityLabel, string visibilityStyle, string summaryText, string[]? eventBadges) { - return new( - entry.Id, - characterName, - skillName, - rollerLabel, - visibilityLabel, - visibilityStyle, - entry.Result, - summaryText, - eventBadges, - entry.TimestampUtc); + return new(entry.Id, characterName, skillName, rollerLabel, visibilityLabel, visibilityStyle, entry.Result, summaryText, eventBadges, entry.TimestampUtc); } public static CampaignRollDetail ToCampaignRollDetail(RollLogEntry entry, RollDieResult[] dice) @@ -124,19 +81,14 @@ public static class GameDtoMapper public static CampaignStateSnapshot ToCampaignStateSnapshot(GameStateStore stateStore, Guid campaignId) { var state = stateStore.GetOrCreateCampaignStateLocked(campaignId); - var characterVersions = state.CharacterVersions - .OrderBy(version => version.Key) - .Select(version => new CharacterStateVersion(version.Key, version.Value)) - .ToArray(); + var characterVersions = state.CharacterVersions.OrderBy(version => version.Key).Select(version => new CharacterStateVersion(version.Key, version.Value)).ToArray(); - return new CampaignStateSnapshot(campaignId, state.TotalVersion, state.RosterVersion, state.LogVersion, characterVersions); + return new(campaignId, state.TotalVersion, state.RosterVersion, state.LogVersion, characterVersions); } public static string ResolveOwnerDisplayName(GameStateStore stateStore, Guid ownerUserId, string fallback) { - return stateStore.UsersById.TryGetValue(ownerUserId, out var user) && !string.IsNullOrWhiteSpace(user.DisplayName) - ? user.DisplayName - : fallback; + return stateStore.UsersById.TryGetValue(ownerUserId, out var user) && !string.IsNullOrWhiteSpace(user.DisplayName) ? user.DisplayName : fallback; } private static CharacterSheetSkillGroup ToCharacterSheetSkillGroup(SkillGroup skillGroup) @@ -148,4 +100,4 @@ public static class GameDtoMapper { return new(skill.Id, skill.SkillGroupId, skill.Name, skill.DiceRollDefinition, skill.WildDice, skill.AllowFumble, skill.FumbleRange); } -} +} \ No newline at end of file diff --git a/RpgRoller/Services/GamePersistenceService.cs b/RpgRoller/Services/GamePersistenceService.cs index 603e2cf..1adc4d0 100644 --- a/RpgRoller/Services/GamePersistenceService.cs +++ b/RpgRoller/Services/GamePersistenceService.cs @@ -106,4 +106,4 @@ public sealed class GamePersistenceService private readonly IDbContextFactory m_DbContextFactory; private readonly GameStateStore m_StateStore; -} +} \ No newline at end of file diff --git a/RpgRoller/Services/GameRollService.cs b/RpgRoller/Services/GameRollService.cs index 7a5c755..b2e148f 100644 --- a/RpgRoller/Services/GameRollService.cs +++ b/RpgRoller/Services/GameRollService.cs @@ -11,10 +11,7 @@ public sealed class GameRollService m_StateStore = stateStore; m_PersistenceService = persistenceService; m_DiceRoller = diceRoller; - m_RollEngine = new( - new StandardRollEngine(diceRoller), - new D6RollEngine(diceRoller), - new RolemasterRollEngine(diceRoller)); + m_RollEngine = new(new(diceRoller), new(diceRoller), new(diceRoller)); } public ServiceResult RollSkill(string sessionToken, Guid skillId, string visibility) @@ -88,10 +85,7 @@ public sealed class GameRollService return ServiceResult>.Failure(context.Error!.Code, context.Error.Message); var (user, campaign) = context.Value!; - var entries = GetVisibleCampaignLogEntriesLocked(user, campaign) - .TakeLast(CampaignLogHistoryWindowSize) - .Select(ToLogEntry) - .ToArray(); + var entries = GetVisibleCampaignLogEntriesLocked(user, campaign).TakeLast(CampaignLogHistoryWindowSize).Select(ToLogEntry).ToArray(); return ServiceResult>.Success(entries); } @@ -112,28 +106,28 @@ public sealed class GameRollService if (!afterRollId.HasValue) { var initialEntries = visibleEntries.TakeLast(pageSize).Select(entry => ToLogListEntry(user, campaign, entry)).ToArray(); - return ServiceResult.Success(new CampaignLogPage(initialEntries, initialEntries.LastOrDefault()?.RollId, visibleEntries.Length > pageSize, false)); + return ServiceResult.Success(new(initialEntries, initialEntries.LastOrDefault()?.RollId, visibleEntries.Length > pageSize, false)); } var afterIndex = Array.FindIndex(visibleEntries, entry => entry.Id == afterRollId.Value); if (afterIndex < 0) { var replacementEntries = visibleEntries.TakeLast(pageSize).Select(entry => ToLogListEntry(user, campaign, entry)).ToArray(); - return ServiceResult.Success(new CampaignLogPage(replacementEntries, replacementEntries.LastOrDefault()?.RollId, visibleEntries.Length > pageSize, true)); + return ServiceResult.Success(new(replacementEntries, replacementEntries.LastOrDefault()?.RollId, visibleEntries.Length > pageSize, true)); } var newEntries = visibleEntries.Skip(afterIndex + 1).ToArray(); if (newEntries.Length == 0) - return ServiceResult.Success(new CampaignLogPage([], afterRollId, false, false)); + return ServiceResult.Success(new([], afterRollId, false, false)); if (newEntries.Length > pageSize) { var replacementEntries = newEntries.TakeLast(pageSize).Select(entry => ToLogListEntry(user, campaign, entry)).ToArray(); - return ServiceResult.Success(new CampaignLogPage(replacementEntries, replacementEntries[^1].RollId, true, true)); + return ServiceResult.Success(new(replacementEntries, replacementEntries[^1].RollId, true, true)); } var appendedEntries = newEntries.Select(entry => ToLogListEntry(user, campaign, entry)).ToArray(); - return ServiceResult.Success(new CampaignLogPage(appendedEntries, appendedEntries[^1].RollId, false, false)); + return ServiceResult.Success(new(appendedEntries, appendedEntries[^1].RollId, false, false)); } } @@ -168,14 +162,7 @@ public sealed class GameRollService } } - private ServiceResult RecordRollLocked( - UserAccount user, - Campaign campaign, - Character character, - Guid skillId, - RollVisibility visibility, - (int Total, string Breakdown, IReadOnlyList Dice) roll, - string canonicalExpression) + private ServiceResult RecordRollLocked(UserAccount user, Campaign campaign, Character character, Guid skillId, RollVisibility visibility, (int Total, string Breakdown, IReadOnlyList Dice) roll, string canonicalExpression) { var entry = new RollLogEntry { @@ -200,18 +187,12 @@ public sealed class GameRollService private static string FormatLoggedBreakdown(Guid skillId, string canonicalExpression, string breakdown) { - return skillId == CustomRollSkillId - ? $"{canonicalExpression}{CustomRollBreakdownSeparator}{breakdown}" - : breakdown; + return skillId == CustomRollSkillId ? $"{canonicalExpression}{CustomRollBreakdownSeparator}{breakdown}" : breakdown; } private IEnumerable GetVisibleCampaignLogEntriesLocked(UserAccount user, Campaign campaign) { - return m_StateStore.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); + return m_StateStore.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); } private CampaignLogEntry ToLogEntry(RollLogEntry entry) @@ -232,15 +213,7 @@ public sealed class GameRollService var loggedExpression = ResolveLoggedExpression(entry); var eventBadges = CampaignLogSummaryBuilder.BuildCompactLogEventBadges(campaign.Ruleset, loggedExpression, dice); - return GameDtoMapper.ToCampaignLogListEntry( - entry, - characterName, - skillName, - ResolveLogRollerLabel(user, campaign, entry), - ResolveLogVisibilityLabel(user, campaign, entry), - ResolveLogVisibilityStyle(user, campaign, entry), - CampaignLogSummaryBuilder.BuildCompactLogSummary(dice), - eventBadges); + return GameDtoMapper.ToCampaignLogListEntry(entry, characterName, skillName, ResolveLogRollerLabel(user, campaign, entry), ResolveLogVisibilityLabel(user, campaign, entry), ResolveLogVisibilityStyle(user, campaign, entry), CampaignLogSummaryBuilder.BuildCompactLogSummary(dice), eventBadges); } private static string SerializeDice(IReadOnlyList dice) @@ -323,11 +296,11 @@ public sealed class GameRollService private const int CampaignLogHistoryWindowSize = 100; private const int CampaignLogLivePageSize = 25; private const string CustomRollBreakdownSeparator = " => "; - private static readonly Guid CustomRollSkillId = Guid.Empty; private const string CustomRollLabel = "Custom roll"; + private static readonly Guid CustomRollSkillId = Guid.Empty; private static readonly JsonSerializerOptions DiceJsonOptions = RpgRollerJson.CreateSerializerOptions(); private readonly IDiceRoller m_DiceRoller; private readonly GamePersistenceService m_PersistenceService; private readonly RollEngine m_RollEngine; private readonly GameStateStore m_StateStore; -} +} \ No newline at end of file diff --git a/RpgRoller/Services/GameService.cs b/RpgRoller/Services/GameService.cs index 9f68f26..5a3993a 100644 --- a/RpgRoller/Services/GameService.cs +++ b/RpgRoller/Services/GameService.cs @@ -20,7 +20,9 @@ public sealed class GameService : IGameService m_UserAdministrationService = new(m_StateStore, m_PersistenceService); m_PersistenceService.LoadStateFromDatabase(); lock (m_StateStore.Gate) + { m_StateStore.RebuildCampaignStateLocked(); + } } public IReadOnlyList GetRulesets() @@ -188,12 +190,13 @@ public sealed class GameService : IGameService return m_RollService.GetCampaignStateSnapshot(sessionToken, campaignId); } + private readonly GameAuthService m_AuthService; + private readonly GameCampaignService m_CampaignService; private readonly GameCharacterService m_CharacterService; - private readonly GameAuthService m_AuthService; private readonly GamePersistenceService m_PersistenceService; private readonly GameRollService m_RollService; private readonly GameSkillService m_SkillService; private readonly GameStateStore m_StateStore; private readonly GameUserAdministrationService m_UserAdministrationService; -} +} \ No newline at end of file diff --git a/RpgRoller/Services/GameSkillService.cs b/RpgRoller/Services/GameSkillService.cs index b32cdc3..61bd9d5 100644 --- a/RpgRoller/Services/GameSkillService.cs +++ b/RpgRoller/Services/GameSkillService.cs @@ -273,4 +273,4 @@ public sealed class GameSkillService private readonly GamePersistenceService m_PersistenceService; private readonly GameStateStore m_StateStore; -} +} \ No newline at end of file diff --git a/RpgRoller/Services/GameStateCloneFactory.cs b/RpgRoller/Services/GameStateCloneFactory.cs index 803214a..a2978ca 100644 --- a/RpgRoller/Services/GameStateCloneFactory.cs +++ b/RpgRoller/Services/GameStateCloneFactory.cs @@ -96,4 +96,4 @@ public static class GameStateCloneFactory TimestampUtc = entry.TimestampUtc }; } -} +} \ No newline at end of file diff --git a/RpgRoller/Services/GameStateStore.cs b/RpgRoller/Services/GameStateStore.cs index c313499..0900971 100644 --- a/RpgRoller/Services/GameStateStore.cs +++ b/RpgRoller/Services/GameStateStore.cs @@ -4,22 +4,11 @@ namespace RpgRoller.Services; public sealed class GameStateStore { - public object Gate { get; } = new(); - public Dictionary CampaignsById { get; } = []; - public Dictionary CampaignStateById { get; } = []; - public Dictionary CharactersById { get; } = []; - public List RollLog { get; } = []; - public Dictionary SessionsByToken { get; } = new(StringComparer.Ordinal); - public Dictionary SkillGroupsById { get; } = []; - public Dictionary SkillsById { get; } = []; - public Dictionary UserIdsByUsername { get; } = new(StringComparer.Ordinal); - public Dictionary UsersById { get; } = []; - public GameCampaignStateTracker GetOrCreateCampaignStateLocked(Guid campaignId) { if (!CampaignStateById.TryGetValue(campaignId, out var state)) { - state = new GameCampaignStateTracker(); + state = new(); CampaignStateById[campaignId] = state; } @@ -31,7 +20,7 @@ public sealed class GameStateStore CampaignStateById.Clear(); foreach (var campaignId in CampaignsById.Keys) - CampaignStateById[campaignId] = new GameCampaignStateTracker(); + CampaignStateById[campaignId] = new(); foreach (var character in CharactersById.Values.Where(character => character.CampaignId.HasValue)) AddCharacterStateLocked(character.CampaignId, character.Id); @@ -83,6 +72,17 @@ public sealed class GameStateStore state.TotalVersion += 1; state.LogVersion += 1; } + + public object Gate { get; } = new(); + public Dictionary CampaignsById { get; } = []; + public Dictionary CampaignStateById { get; } = []; + public Dictionary CharactersById { get; } = []; + public List RollLog { get; } = []; + public Dictionary SessionsByToken { get; } = new(StringComparer.Ordinal); + public Dictionary SkillGroupsById { get; } = []; + public Dictionary SkillsById { get; } = []; + public Dictionary UserIdsByUsername { get; } = new(StringComparer.Ordinal); + public Dictionary UsersById { get; } = []; } public sealed class GameCampaignStateTracker @@ -91,4 +91,4 @@ public sealed class GameCampaignStateTracker public long RosterVersion { get; set; } = 1; public long LogVersion { get; set; } = 1; public Dictionary CharacterVersions { get; } = []; -} +} \ No newline at end of file diff --git a/RpgRoller/Services/GameUserAdministrationService.cs b/RpgRoller/Services/GameUserAdministrationService.cs index 6b8eb64..df2e489 100644 --- a/RpgRoller/Services/GameUserAdministrationService.cs +++ b/RpgRoller/Services/GameUserAdministrationService.cs @@ -19,10 +19,7 @@ public sealed class GameUserAdministrationService if (user is null) return ServiceResult>.Failure("unauthorized", "You must be logged in."); - var usernames = m_StateStore.UsersById.Values - .Select(account => account.Username) - .OrderBy(username => username, StringComparer.OrdinalIgnoreCase) - .ToArray(); + var usernames = m_StateStore.UsersById.Values.Select(account => account.Username).OrderBy(username => username, StringComparer.OrdinalIgnoreCase).ToArray(); return ServiceResult>.Success(usernames); } @@ -39,10 +36,7 @@ public sealed class GameUserAdministrationService if (!GameAuthorization.HasRole(user, UserRoles.Admin)) return ServiceResult>.Failure("forbidden", "Admin role is required."); - var users = m_StateStore.UsersById.Values - .OrderBy(account => account.Username, StringComparer.OrdinalIgnoreCase) - .Select(GameDtoMapper.ToAdminUserSummary) - .ToArray(); + var users = m_StateStore.UsersById.Values.OrderBy(account => account.Username, StringComparer.OrdinalIgnoreCase).Select(GameDtoMapper.ToAdminUserSummary).ToArray(); return ServiceResult>.Success(users); } @@ -92,32 +86,20 @@ public sealed class GameUserAdministrationService if (!m_StateStore.UsersById.TryGetValue(userId, out var targetUser)) return ServiceResult.Failure("user_not_found", "User was not found."); - var gmCampaignIds = m_StateStore.CampaignsById.Values - .Where(campaign => campaign.GmUserId == targetUser.Id) - .Select(campaign => campaign.Id) - .ToArray(); + var gmCampaignIds = m_StateStore.CampaignsById.Values.Where(campaign => campaign.GmUserId == targetUser.Id).Select(campaign => campaign.Id).ToArray(); var gmCampaignIdSet = gmCampaignIds.ToHashSet(); - var preservedCharacterIds = m_StateStore.CharactersById.Values - .Where(character => character.CampaignId.HasValue && gmCampaignIdSet.Contains(character.CampaignId.Value)) - .Select(character => character.Id) - .ToHashSet(); + var preservedCharacterIds = m_StateStore.CharactersById.Values.Where(character => character.CampaignId.HasValue && gmCampaignIdSet.Contains(character.CampaignId.Value)).Select(character => character.Id).ToHashSet(); foreach (var campaignId in gmCampaignIds) DeleteCampaignLocked(campaignId); - var ownedCharacterIds = m_StateStore.CharactersById.Values - .Where(character => character.OwnerUserId == targetUser.Id && !preservedCharacterIds.Contains(character.Id)) - .Select(character => character.Id) - .ToArray(); + var ownedCharacterIds = m_StateStore.CharactersById.Values.Where(character => character.OwnerUserId == targetUser.Id && !preservedCharacterIds.Contains(character.Id)).Select(character => character.Id).ToArray(); foreach (var characterId in ownedCharacterIds) DeleteCharacterLocked(characterId); m_StateStore.RollLog.RemoveAll(entry => entry.RollerUserId == targetUser.Id); - var staleSessions = m_StateStore.SessionsByToken.Values - .Where(session => session.UserId == targetUser.Id) - .Select(session => session.Token) - .ToArray(); + var staleSessions = m_StateStore.SessionsByToken.Values.Where(session => session.UserId == targetUser.Id).Select(session => session.Token).ToArray(); foreach (var token in staleSessions) m_StateStore.SessionsByToken.Remove(token); @@ -134,10 +116,7 @@ public sealed class GameUserAdministrationService if (!m_StateStore.CampaignsById.Remove(campaignId)) return; - var affectedCharacterIds = m_StateStore.CharactersById.Values - .Where(character => character.CampaignId == campaignId) - .Select(character => character.Id) - .ToArray(); + var affectedCharacterIds = m_StateStore.CharactersById.Values.Where(character => character.CampaignId == campaignId).Select(character => character.Id).ToArray(); foreach (var characterId in affectedCharacterIds) m_StateStore.CharactersById[characterId].CampaignId = null; @@ -153,17 +132,11 @@ public sealed class GameUserAdministrationService var campaignId = character.CampaignId; m_StateStore.CharactersById.Remove(characterId); - var skillGroupIds = m_StateStore.SkillGroupsById.Values - .Where(group => group.CharacterId == characterId) - .Select(group => group.Id) - .ToHashSet(); + var skillGroupIds = m_StateStore.SkillGroupsById.Values.Where(group => group.CharacterId == characterId).Select(group => group.Id).ToHashSet(); foreach (var skillGroupId in skillGroupIds) m_StateStore.SkillGroupsById.Remove(skillGroupId); - var skillIds = m_StateStore.SkillsById.Values - .Where(skill => skill.CharacterId == characterId) - .Select(skill => skill.Id) - .ToHashSet(); + var skillIds = m_StateStore.SkillsById.Values.Where(skill => skill.CharacterId == characterId).Select(skill => skill.Id).ToHashSet(); foreach (var skillId in skillIds) m_StateStore.SkillsById.Remove(skillId); @@ -178,4 +151,4 @@ public sealed class GameUserAdministrationService private readonly GamePersistenceService m_PersistenceService; private readonly GameStateStore m_StateStore; -} +} \ No newline at end of file diff --git a/RpgRoller/Services/IGameService.cs b/RpgRoller/Services/IGameService.cs index 838981a..052021c 100644 --- a/RpgRoller/Services/IGameService.cs +++ b/RpgRoller/Services/IGameService.cs @@ -43,4 +43,4 @@ public interface IGameService ServiceResult GetRollDetail(string sessionToken, Guid rollId); ServiceResult GetCampaignStateSnapshot(string sessionToken, Guid campaignId); -} +} \ No newline at end of file diff --git a/RpgRoller/Services/RoleSerializer.cs b/RpgRoller/Services/RoleSerializer.cs index 4341611..e2fc3ea 100644 --- a/RpgRoller/Services/RoleSerializer.cs +++ b/RpgRoller/Services/RoleSerializer.cs @@ -14,16 +14,11 @@ public static class RoleSerializer public static string[] Normalize(IEnumerable roles) { - return roles - .Where(role => !string.IsNullOrWhiteSpace(role)) - .Select(role => role.Trim().ToLowerInvariant()) - .Distinct(StringComparer.Ordinal) - .OrderBy(role => role, StringComparer.Ordinal) - .ToArray(); + return roles.Where(role => !string.IsNullOrWhiteSpace(role)).Select(role => role.Trim().ToLowerInvariant()).Distinct(StringComparer.Ordinal).OrderBy(role => role, StringComparer.Ordinal).ToArray(); } public static bool HasRole(string serializedRoles, string role) { return Parse(serializedRoles).Contains(role, StringComparer.OrdinalIgnoreCase); } -} +} \ No newline at end of file diff --git a/RpgRoller/Services/RolemasterRollEngine.cs b/RpgRoller/Services/RolemasterRollEngine.cs index d8dfbb5..772dec3 100644 --- a/RpgRoller/Services/RolemasterRollEngine.cs +++ b/RpgRoller/Services/RolemasterRollEngine.cs @@ -15,7 +15,7 @@ public sealed class RolemasterRollEngine return expression.Kind switch { DiceExpressionKind.RolemasterOpenEndedPercentile => RollOpenEnded(expression, fumbleRange.GetValueOrDefault()), - _ => RollStandard(expression) + _ => RollStandard(expression) }; } @@ -40,22 +40,19 @@ public sealed class RolemasterRollEngine var initialRoll = m_DiceRoller.Roll(expression.Sides); var followUpRolls = new List(); int? initialContribution = initialRoll <= fumbleRange ? null : initialRoll; - var dice = new List - { - CreateRolemasterDie(initialRoll, 1, RollDieKinds.RolemasterOpenEndedInitial, initialContribution) - }; + var dice = new List { CreateRolemasterDie(initialRoll, 1, RollDieKinds.RolemasterOpenEndedInitial, initialContribution) }; var baseTotal = initialRoll <= fumbleRange ? 0 : initialRoll; var subtractFollowUps = false; if (initialRoll >= 96) { - followUpRolls.AddRange(RollHighOpenEndedChain(dice, sequenceStart: 2, subtract: false)); + followUpRolls.AddRange(RollHighOpenEndedChain(dice, 2, false)); baseTotal += followUpRolls.Sum(); } else if (initialRoll <= fumbleRange) { subtractFollowUps = true; - followUpRolls.AddRange(RollHighOpenEndedChain(dice, sequenceStart: 2, subtract: true)); + followUpRolls.AddRange(RollHighOpenEndedChain(dice, 2, true)); baseTotal -= followUpRolls.Sum(); } @@ -73,11 +70,7 @@ public sealed class RolemasterRollEngine { var roll = m_DiceRoller.Roll(100); followUpRolls.Add(roll); - dice.Add(CreateRolemasterDie( - roll, - sequence, - subtract ? RollDieKinds.RolemasterOpenEndedLowSubtract : RollDieKinds.RolemasterOpenEndedHigh, - subtract ? -roll : roll)); + dice.Add(CreateRolemasterDie(roll, sequence, subtract ? RollDieKinds.RolemasterOpenEndedLowSubtract : RollDieKinds.RolemasterOpenEndedHigh, subtract ? -roll : roll)); sequence += 1; if (roll < 96) @@ -93,4 +86,4 @@ public sealed class RolemasterRollEngine } private readonly IDiceRoller m_DiceRoller; -} +} \ No newline at end of file diff --git a/RpgRoller/Services/RollBreakdownFormatter.cs b/RpgRoller/Services/RollBreakdownFormatter.cs index ade34d9..cdcb5ff 100644 --- a/RpgRoller/Services/RollBreakdownFormatter.cs +++ b/RpgRoller/Services/RollBreakdownFormatter.cs @@ -49,4 +49,4 @@ public static class RollBreakdownFormatter _ => $"{core}={total}" }; } -} +} \ No newline at end of file diff --git a/RpgRoller/Services/RollEngine.cs b/RpgRoller/Services/RollEngine.cs index 5896535..45d34ef 100644 --- a/RpgRoller/Services/RollEngine.cs +++ b/RpgRoller/Services/RollEngine.cs @@ -26,4 +26,4 @@ public sealed class RollEngine private readonly D6RollEngine m_D6RollEngine; private readonly RolemasterRollEngine m_RolemasterRollEngine; private readonly StandardRollEngine m_StandardRollEngine; -} +} \ No newline at end of file diff --git a/RpgRoller/Services/RollVisibilityParser.cs b/RpgRoller/Services/RollVisibilityParser.cs index 146d0a7..f1eb2a9 100644 --- a/RpgRoller/Services/RollVisibilityParser.cs +++ b/RpgRoller/Services/RollVisibilityParser.cs @@ -14,4 +14,4 @@ public static class RollVisibilityParser return ServiceResult.Failure("invalid_visibility", "Visibility must be 'public' or 'private'."); } -} +} \ No newline at end of file diff --git a/RpgRoller/Services/SkillDefinitionValidator.cs b/RpgRoller/Services/SkillDefinitionValidator.cs index 0c2525c..b4659fb 100644 --- a/RpgRoller/Services/SkillDefinitionValidator.cs +++ b/RpgRoller/Services/SkillDefinitionValidator.cs @@ -4,12 +4,7 @@ namespace RpgRoller.Services; public static class SkillDefinitionValidator { - public static ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)> Validate( - RulesetKind ruleset, - string diceRollDefinition, - int wildDice, - bool allowFumble, - int? fumbleRange) + public static ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)> Validate(RulesetKind ruleset, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange) { var expressionValidation = DiceRules.ParseExpression(ruleset, diceRollDefinition); if (!expressionValidation.Succeeded) @@ -19,19 +14,10 @@ public static class SkillDefinitionValidator if (!optionsValidation.Succeeded) return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)>.Failure(optionsValidation.Error!.Code, optionsValidation.Error.Message); - return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)>.Success(( - expressionValidation.Value!.Canonical, - optionsValidation.Value!.WildDice, - optionsValidation.Value.AllowFumble, - optionsValidation.Value.FumbleRange)); + return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)>.Success((expressionValidation.Value!.Canonical, optionsValidation.Value!.WildDice, optionsValidation.Value.AllowFumble, optionsValidation.Value.FumbleRange)); } - private static ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)> ValidateOptions( - RulesetKind ruleset, - DiceExpression expression, - int wildDice, - bool allowFumble, - int? fumbleRange) + private static ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)> ValidateOptions(RulesetKind ruleset, DiceExpression expression, int wildDice, bool allowFumble, int? fumbleRange) { if (wildDice < 0 || wildDice > 50) return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_wild_dice", "Wild dice must be between 0 and 50."); @@ -77,4 +63,4 @@ public static class SkillDefinitionValidator return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Success((0, false, null)); } -} +} \ No newline at end of file diff --git a/RpgRoller/Services/StandardRollEngine.cs b/RpgRoller/Services/StandardRollEngine.cs index 1e07302..098d244 100644 --- a/RpgRoller/Services/StandardRollEngine.cs +++ b/RpgRoller/Services/StandardRollEngine.cs @@ -27,4 +27,4 @@ public sealed class StandardRollEngine } private readonly IDiceRoller m_DiceRoller; -} +} \ No newline at end of file diff --git a/scripts/ci-local.ps1 b/scripts/ci-local.ps1 index 1373177..5e18c27 100644 --- a/scripts/ci-local.ps1 +++ b/scripts/ci-local.ps1 @@ -20,11 +20,36 @@ function Invoke-Step { } } +function Remove-TestCoverageArtifacts { + param( + [Parameter(Mandatory = $true)][string]$ResultsRoot + ) + + if (-not (Test-Path $ResultsRoot)) { + return + } + + Get-ChildItem -Path $ResultsRoot -Recurse -File -Filter "coverage.cobertura.xml" -ErrorAction SilentlyContinue | + Remove-Item -Force -ErrorAction SilentlyContinue + + Get-ChildItem -Path $ResultsRoot -Recurse -Directory -ErrorAction SilentlyContinue | + Sort-Object FullName -Descending | + ForEach-Object { + if (-not (Get-ChildItem -Path $_.FullName -Force -ErrorAction SilentlyContinue | Select-Object -First 1)) { + Remove-Item -Path $_.FullName -Force -ErrorAction SilentlyContinue + } + } +} + $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $repoRoot = Split-Path -Parent $scriptDir +$testResultsRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("RpgRoller.TestResults." + [Guid]::NewGuid().ToString("N")) +$repoCoverageResultsRoot = Join-Path $repoRoot "RpgRoller.Tests\TestResults" Push-Location $repoRoot try { + Remove-TestCoverageArtifacts -ResultsRoot $repoCoverageResultsRoot + if (-not $SkipDotnetRestore) { Invoke-Step -Name "Restore .NET solution" -Action { dotnet restore RpgRoller.sln --verbosity minimal @@ -47,15 +72,15 @@ try { Invoke-Step -Name "Run tests" -Action { if ($SkipBuild) { - dotnet test RpgRoller.Tests/RpgRoller.Tests.csproj --verbosity minimal --collect:"XPlat Code Coverage" --settings RpgRoller.Tests/coverlet.runsettings + dotnet test RpgRoller.Tests/RpgRoller.Tests.csproj --verbosity minimal --collect:"XPlat Code Coverage" --settings RpgRoller.Tests/coverlet.runsettings --results-directory $testResultsRoot } else { - dotnet test RpgRoller.Tests/RpgRoller.Tests.csproj --no-build --verbosity minimal --collect:"XPlat Code Coverage" --settings RpgRoller.Tests/coverlet.runsettings + dotnet test RpgRoller.Tests/RpgRoller.Tests.csproj --no-build --verbosity minimal --collect:"XPlat Code Coverage" --settings RpgRoller.Tests/coverlet.runsettings --results-directory $testResultsRoot } } Invoke-Step -Name "Enforce coverage thresholds" -Action { - pwsh ./scripts/check-coverage.ps1 -MinLineRate 0.90 -MinBranchRate 0.70 + pwsh ./scripts/check-coverage.ps1 -MinLineRate 0.90 -MinBranchRate 0.70 -ResultsRoot $testResultsRoot } if (-not $SkipPlaywright) { @@ -67,5 +92,11 @@ try { Write-Host "CI checks passed." } finally { + if (Test-Path $testResultsRoot) { + Remove-Item -Path $testResultsRoot -Recurse -Force -ErrorAction SilentlyContinue + } + + Remove-TestCoverageArtifacts -ResultsRoot $repoCoverageResultsRoot + Pop-Location }