Code Cleanup
This commit is contained in:
@@ -167,5 +167,6 @@ SQLite migration rule:
|
|||||||
```powershell
|
```powershell
|
||||||
pwsh ./scripts/ci-local.ps1
|
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.
|
- 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.
|
- `RpgRoller.Tests/coverlet.runsettings` measures the full `RpgRoller` backend assembly.
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ public sealed class CampaignApiTests : ApiTestBase
|
|||||||
var groupedSkill = await PostAsync<CreateSkillRequest, SkillSummary>(ownerClient, $"/api/characters/{character.Id}/skills", new("Strike", "2D+1", 1, true, renamedGroup.Id));
|
var groupedSkill = await PostAsync<CreateSkillRequest, SkillSummary>(ownerClient, $"/api/characters/{character.Id}/skills", new("Strike", "2D+1", 1, true, renamedGroup.Id));
|
||||||
Assert.Equal(renamedGroup.Id, groupedSkill.SkillGroupId);
|
Assert.Equal(renamedGroup.Id, groupedSkill.SkillGroupId);
|
||||||
|
|
||||||
var ungroupedSkill = await PutAsync<UpdateSkillRequest, SkillSummary>(ownerClient, $"/api/skills/{groupedSkill.Id}", new("Strike", "2D+1", 1, true, null));
|
var ungroupedSkill = await PutAsync<UpdateSkillRequest, SkillSummary>(ownerClient, $"/api/skills/{groupedSkill.Id}", new("Strike", "2D+1", 1, true));
|
||||||
Assert.Null(ungroupedSkill.SkillGroupId);
|
Assert.Null(ungroupedSkill.SkillGroupId);
|
||||||
|
|
||||||
var groupedAgainSkill = await PutAsync<UpdateSkillRequest, SkillSummary>(ownerClient, $"/api/skills/{groupedSkill.Id}", new("Strike", "2D+1", 1, true, renamedGroup.Id));
|
var groupedAgainSkill = await PutAsync<UpdateSkillRequest, SkillSummary>(ownerClient, $"/api/skills/{groupedSkill.Id}", new("Strike", "2D+1", 1, true, renamedGroup.Id));
|
||||||
|
|||||||
@@ -61,32 +61,24 @@ public sealed class RolemasterApiTests : ApiTestBase
|
|||||||
var logEntry = Assert.Single(logPage.Entries);
|
var logEntry = Assert.Single(logPage.Entries);
|
||||||
Assert.Equal("(05) -97 -100 -12 | open-ended low", logEntry.SummaryText);
|
Assert.Equal("(05) -97 -100 -12 | open-ended low", logEntry.SummaryText);
|
||||||
var eventBadges = Assert.IsType<string[]>(logEntry.EventBadges);
|
var eventBadges = Assert.IsType<string[]>(logEntry.EventBadges);
|
||||||
Assert.Collection(
|
Assert.Collection(eventBadges, badge => Assert.Equal("rf", badge), badge => Assert.Equal("r100", badge));
|
||||||
eventBadges,
|
|
||||||
badge => Assert.Equal("rf", badge),
|
|
||||||
badge => Assert.Equal("r100", badge));
|
|
||||||
Assert.Equal(roll.Breakdown, detail.Breakdown);
|
Assert.Equal(roll.Breakdown, detail.Breakdown);
|
||||||
Assert.Collection(
|
Assert.Collection(detail.Dice, die =>
|
||||||
detail.Dice,
|
|
||||||
die =>
|
|
||||||
{
|
{
|
||||||
Assert.Equal(RollDieKinds.RolemasterOpenEndedInitial, die.Kind);
|
Assert.Equal(RollDieKinds.RolemasterOpenEndedInitial, die.Kind);
|
||||||
Assert.Equal(1, die.Sequence);
|
Assert.Equal(1, die.Sequence);
|
||||||
Assert.Null(die.SignedContribution);
|
Assert.Null(die.SignedContribution);
|
||||||
},
|
}, die =>
|
||||||
die =>
|
|
||||||
{
|
{
|
||||||
Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind);
|
Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind);
|
||||||
Assert.Equal(2, die.Sequence);
|
Assert.Equal(2, die.Sequence);
|
||||||
Assert.Equal(-97, die.SignedContribution);
|
Assert.Equal(-97, die.SignedContribution);
|
||||||
},
|
}, die =>
|
||||||
die =>
|
|
||||||
{
|
{
|
||||||
Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind);
|
Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind);
|
||||||
Assert.Equal(3, die.Sequence);
|
Assert.Equal(3, die.Sequence);
|
||||||
Assert.Equal(-100, die.SignedContribution);
|
Assert.Equal(-100, die.SignedContribution);
|
||||||
},
|
}, die =>
|
||||||
die =>
|
|
||||||
{
|
{
|
||||||
Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind);
|
Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind);
|
||||||
Assert.Equal(4, die.Sequence);
|
Assert.Equal(4, die.Sequence);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
using Microsoft.Data.Sqlite;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
@@ -218,12 +218,8 @@ public sealed class HostingCoverageTests
|
|||||||
|
|
||||||
using var db = new RpgRollerDbContext(options);
|
using var db = new RpgRollerDbContext(options);
|
||||||
var migrator = db.GetService<IMigrator>();
|
var migrator = db.GetService<IMigrator>();
|
||||||
var charactersScript = migrator.GenerateScript(
|
var charactersScript = migrator.GenerateScript("20260226131003_AddSkillGroupPrototypes", "20260226160859_AddAuthorizationRolesAndCampaignDeletion");
|
||||||
fromMigration: "20260226131003_AddSkillGroupPrototypes",
|
var rolesScript = migrator.GenerateScript("20260226160859_AddAuthorizationRolesAndCampaignDeletion", "20260226170000_AddAuthorizationRoles");
|
||||||
toMigration: "20260226160859_AddAuthorizationRolesAndCampaignDeletion");
|
|
||||||
var rolesScript = migrator.GenerateScript(
|
|
||||||
fromMigration: "20260226160859_AddAuthorizationRolesAndCampaignDeletion",
|
|
||||||
toMigration: "20260226170000_AddAuthorizationRoles");
|
|
||||||
|
|
||||||
Assert.Contains("""CREATE TABLE "ef_temp_Characters" (""", charactersScript);
|
Assert.Contains("""CREATE TABLE "ef_temp_Characters" (""", charactersScript);
|
||||||
Assert.DoesNotContain("""ALTER TABLE "Users" ADD "Roles" TEXT NOT NULL DEFAULT 'admin';""", 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 sourceDbPath = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "RpgRoller", "App_Data", "rpgroller.development.db");
|
||||||
var copiedDbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-dev-copy-{Guid.NewGuid():N}.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 skillId;
|
||||||
Guid ownerUserId;
|
Guid ownerUserId;
|
||||||
@@ -427,10 +423,7 @@ public sealed class HostingCoverageTests
|
|||||||
builder.Logging.AddFilter("Microsoft.AspNetCore", LogLevel.Warning);
|
builder.Logging.AddFilter("Microsoft.AspNetCore", LogLevel.Warning);
|
||||||
builder.Logging.AddFilter("Microsoft.EntityFrameworkCore", LogLevel.Warning);
|
builder.Logging.AddFilter("Microsoft.EntityFrameworkCore", LogLevel.Warning);
|
||||||
builder.Logging.AddFilter("Microsoft.Hosting", LogLevel.Warning);
|
builder.Logging.AddFilter("Microsoft.Hosting", LogLevel.Warning);
|
||||||
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?> { ["ConnectionStrings:RpgRoller"] = $"Data Source={copiedDbPath}" });
|
||||||
{
|
|
||||||
["ConnectionStrings:RpgRoller"] = $"Data Source={copiedDbPath}"
|
|
||||||
});
|
|
||||||
builder.Services.AddRpgRollerCore(builder.Configuration, builder.Environment);
|
builder.Services.AddRpgRollerCore(builder.Configuration, builder.Environment);
|
||||||
|
|
||||||
using var app = builder.Build();
|
using var app = builder.Build();
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using RpgRoller.Contracts;
|
|
||||||
|
|
||||||
namespace RpgRoller.Tests;
|
namespace RpgRoller.Tests;
|
||||||
|
|
||||||
@@ -133,7 +132,7 @@ public sealed class PayloadBudgetTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void RolemasterRollDetailPayload_StaysWithinBudget_AndRolemasterMetadataRemainsLazy()
|
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;
|
var service = harness.Service;
|
||||||
|
|
||||||
service.Register("gm-rm-detail-budget", "Password123", "GM");
|
service.Register("gm-rm-detail-budget", "Password123", "GM");
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using RpgRoller.Domain;
|
|
||||||
|
|
||||||
namespace RpgRoller.Tests;
|
namespace RpgRoller.Tests;
|
||||||
|
|
||||||
public sealed class ServiceAdminAndCampaignDeletionTests
|
public sealed class ServiceAdminAndCampaignDeletionTests
|
||||||
|
|||||||
@@ -20,16 +20,13 @@ public sealed class ServiceRolemasterRollTests
|
|||||||
Assert.Equal(65, roll.Result);
|
Assert.Equal(65, roll.Result);
|
||||||
Assert.Equal("7+10+48=65", roll.Breakdown);
|
Assert.Equal("7+10+48=65", roll.Breakdown);
|
||||||
Assert.Equal("7 + 10 | rolemaster", Assert.Single(logPage.Entries).SummaryText);
|
Assert.Equal("7 + 10 | rolemaster", Assert.Single(logPage.Entries).SummaryText);
|
||||||
Assert.Collection(
|
Assert.Collection(roll.Dice, die =>
|
||||||
roll.Dice,
|
|
||||||
die =>
|
|
||||||
{
|
{
|
||||||
Assert.Equal(7, die.Roll);
|
Assert.Equal(7, die.Roll);
|
||||||
Assert.Equal(1, die.Sequence);
|
Assert.Equal(1, die.Sequence);
|
||||||
Assert.Equal(RollDieKinds.RolemasterStandard, die.Kind);
|
Assert.Equal(RollDieKinds.RolemasterStandard, die.Kind);
|
||||||
Assert.Equal(7, die.SignedContribution);
|
Assert.Equal(7, die.SignedContribution);
|
||||||
},
|
}, die =>
|
||||||
die =>
|
|
||||||
{
|
{
|
||||||
Assert.Equal(10, die.Roll);
|
Assert.Equal(10, die.Roll);
|
||||||
Assert.Equal(2, die.Sequence);
|
Assert.Equal(2, die.Sequence);
|
||||||
@@ -86,25 +83,21 @@ public sealed class ServiceRolemasterRollTests
|
|||||||
Assert.Equal("97 + 96 + 45 | open-ended high", Assert.Single(logPage.Entries).SummaryText);
|
Assert.Equal("97 + 96 + 45 | open-ended high", Assert.Single(logPage.Entries).SummaryText);
|
||||||
Assert.Null(Assert.Single(logPage.Entries).EventBadges);
|
Assert.Null(Assert.Single(logPage.Entries).EventBadges);
|
||||||
Assert.Equal(roll.Breakdown, detail.Breakdown);
|
Assert.Equal(roll.Breakdown, detail.Breakdown);
|
||||||
Assert.Collection(
|
Assert.Collection(detail.Dice, die =>
|
||||||
detail.Dice,
|
|
||||||
die =>
|
|
||||||
{
|
{
|
||||||
Assert.Equal(97, die.Roll);
|
Assert.Equal(97, die.Roll);
|
||||||
Assert.Equal(1, die.Sequence);
|
Assert.Equal(1, die.Sequence);
|
||||||
Assert.Equal(RollDieKinds.RolemasterOpenEndedInitial, die.Kind);
|
Assert.Equal(RollDieKinds.RolemasterOpenEndedInitial, die.Kind);
|
||||||
Assert.Equal(97, die.SignedContribution);
|
Assert.Equal(97, die.SignedContribution);
|
||||||
Assert.False(die.Added);
|
Assert.False(die.Added);
|
||||||
},
|
}, die =>
|
||||||
die =>
|
|
||||||
{
|
{
|
||||||
Assert.Equal(96, die.Roll);
|
Assert.Equal(96, die.Roll);
|
||||||
Assert.Equal(2, die.Sequence);
|
Assert.Equal(2, die.Sequence);
|
||||||
Assert.Equal(RollDieKinds.RolemasterOpenEndedHigh, die.Kind);
|
Assert.Equal(RollDieKinds.RolemasterOpenEndedHigh, die.Kind);
|
||||||
Assert.Equal(96, die.SignedContribution);
|
Assert.Equal(96, die.SignedContribution);
|
||||||
Assert.True(die.Added);
|
Assert.True(die.Added);
|
||||||
},
|
}, die =>
|
||||||
die =>
|
|
||||||
{
|
{
|
||||||
Assert.Equal(45, die.Roll);
|
Assert.Equal(45, die.Roll);
|
||||||
Assert.Equal(3, die.Sequence);
|
Assert.Equal(3, die.Sequence);
|
||||||
@@ -134,34 +127,26 @@ public sealed class ServiceRolemasterRollTests
|
|||||||
var logEntry = Assert.Single(logPage.Entries);
|
var logEntry = Assert.Single(logPage.Entries);
|
||||||
Assert.Equal("(05) -97 -100 -12 | open-ended low", logEntry.SummaryText);
|
Assert.Equal("(05) -97 -100 -12 | open-ended low", logEntry.SummaryText);
|
||||||
var lowEventBadges = Assert.IsType<string[]>(logEntry.EventBadges);
|
var lowEventBadges = Assert.IsType<string[]>(logEntry.EventBadges);
|
||||||
Assert.Collection(
|
Assert.Collection(lowEventBadges, badge => Assert.Equal("rf", badge), badge => Assert.Equal("r100", badge));
|
||||||
lowEventBadges,
|
Assert.Collection(roll.Dice, die =>
|
||||||
badge => Assert.Equal("rf", badge),
|
|
||||||
badge => Assert.Equal("r100", badge));
|
|
||||||
Assert.Collection(
|
|
||||||
roll.Dice,
|
|
||||||
die =>
|
|
||||||
{
|
{
|
||||||
Assert.Equal(5, die.Roll);
|
Assert.Equal(5, die.Roll);
|
||||||
Assert.Equal(1, die.Sequence);
|
Assert.Equal(1, die.Sequence);
|
||||||
Assert.Equal(RollDieKinds.RolemasterOpenEndedInitial, die.Kind);
|
Assert.Equal(RollDieKinds.RolemasterOpenEndedInitial, die.Kind);
|
||||||
Assert.Null(die.SignedContribution);
|
Assert.Null(die.SignedContribution);
|
||||||
},
|
}, die =>
|
||||||
die =>
|
|
||||||
{
|
{
|
||||||
Assert.Equal(97, die.Roll);
|
Assert.Equal(97, die.Roll);
|
||||||
Assert.Equal(2, die.Sequence);
|
Assert.Equal(2, die.Sequence);
|
||||||
Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind);
|
Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind);
|
||||||
Assert.Equal(-97, die.SignedContribution);
|
Assert.Equal(-97, die.SignedContribution);
|
||||||
},
|
}, die =>
|
||||||
die =>
|
|
||||||
{
|
{
|
||||||
Assert.Equal(100, die.Roll);
|
Assert.Equal(100, die.Roll);
|
||||||
Assert.Equal(3, die.Sequence);
|
Assert.Equal(3, die.Sequence);
|
||||||
Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind);
|
Assert.Equal(RollDieKinds.RolemasterOpenEndedLowSubtract, die.Kind);
|
||||||
Assert.Equal(-100, die.SignedContribution);
|
Assert.Equal(-100, die.SignedContribution);
|
||||||
},
|
}, die =>
|
||||||
die =>
|
|
||||||
{
|
{
|
||||||
Assert.Equal(12, die.Roll);
|
Assert.Equal(12, die.Roll);
|
||||||
Assert.Equal(4, die.Sequence);
|
Assert.Equal(4, die.Sequence);
|
||||||
|
|||||||
@@ -1,67 +1,7 @@
|
|||||||
using RpgRoller.Contracts;
|
|
||||||
using RpgRoller.Domain;
|
|
||||||
using RpgRoller.Services;
|
|
||||||
|
|
||||||
namespace RpgRoller.Tests;
|
namespace RpgRoller.Tests;
|
||||||
|
|
||||||
public sealed class ServiceRollHelperTests
|
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<string[]>(CampaignLogSummaryBuilder.BuildCompactLogEventBadges(RulesetKind.D6, null, d6Dice)));
|
|
||||||
Assert.Equal(["n20"], Assert.IsType<string[]>(CampaignLogSummaryBuilder.BuildCompactLogEventBadges(RulesetKind.Dnd5e, "1d20+5", [new RollDieResult(20, false, false, false, false, false)])));
|
|
||||||
Assert.Equal(["rf", "r100"], Assert.IsType<string[]>(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
|
private sealed class FixedDiceRoller : IDiceRoller
|
||||||
{
|
{
|
||||||
public FixedDiceRoller(IEnumerable<int> values)
|
public FixedDiceRoller(IEnumerable<int> values)
|
||||||
@@ -77,4 +17,48 @@ public sealed class ServiceRollHelperTests
|
|||||||
|
|
||||||
private readonly Queue<int> m_Values;
|
private readonly Queue<int> 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<string[]>(CampaignLogSummaryBuilder.BuildCompactLogEventBadges(RulesetKind.D6, null, d6Dice)));
|
||||||
|
Assert.Equal(["n20"], Assert.IsType<string[]>(CampaignLogSummaryBuilder.BuildCompactLogEventBadges(RulesetKind.Dnd5e, "1d20+5", [new(20, false, false, false, false, false)])));
|
||||||
|
Assert.Equal(["rf", "r100"], Assert.IsType<string[]>(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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
using RpgRoller.Contracts;
|
|
||||||
using RpgRoller.Domain;
|
|
||||||
using RpgRoller.Services;
|
|
||||||
|
|
||||||
namespace RpgRoller.Tests;
|
namespace RpgRoller.Tests;
|
||||||
|
|
||||||
public sealed class ServiceSharedHelperTests
|
public sealed class ServiceSharedHelperTests
|
||||||
@@ -13,7 +9,7 @@ public sealed class ServiceSharedHelperTests
|
|||||||
var characterId = Guid.NewGuid();
|
var characterId = Guid.NewGuid();
|
||||||
var store = new GameStateStore();
|
var store = new GameStateStore();
|
||||||
|
|
||||||
store.CampaignsById[campaignId] = new Campaign
|
store.CampaignsById[campaignId] = new()
|
||||||
{
|
{
|
||||||
Id = campaignId,
|
Id = campaignId,
|
||||||
GmUserId = Guid.NewGuid(),
|
GmUserId = Guid.NewGuid(),
|
||||||
@@ -21,7 +17,7 @@ public sealed class ServiceSharedHelperTests
|
|||||||
Ruleset = RulesetKind.D6,
|
Ruleset = RulesetKind.D6,
|
||||||
Version = 1
|
Version = 1
|
||||||
};
|
};
|
||||||
store.CharactersById[characterId] = new Character
|
store.CharactersById[characterId] = new()
|
||||||
{
|
{
|
||||||
Id = characterId,
|
Id = characterId,
|
||||||
OwnerUserId = Guid.NewGuid(),
|
OwnerUserId = Guid.NewGuid(),
|
||||||
@@ -65,7 +61,7 @@ public sealed class ServiceSharedHelperTests
|
|||||||
var campaignId = Guid.NewGuid();
|
var campaignId = Guid.NewGuid();
|
||||||
var store = new GameStateStore();
|
var store = new GameStateStore();
|
||||||
|
|
||||||
store.UsersById[adminId] = new UserAccount
|
store.UsersById[adminId] = new()
|
||||||
{
|
{
|
||||||
Id = adminId,
|
Id = adminId,
|
||||||
Username = "admin",
|
Username = "admin",
|
||||||
@@ -74,7 +70,7 @@ public sealed class ServiceSharedHelperTests
|
|||||||
DisplayName = "Admin",
|
DisplayName = "Admin",
|
||||||
Roles = UserRoles.Admin
|
Roles = UserRoles.Admin
|
||||||
};
|
};
|
||||||
store.UsersById[gmId] = new UserAccount
|
store.UsersById[gmId] = new()
|
||||||
{
|
{
|
||||||
Id = gmId,
|
Id = gmId,
|
||||||
Username = "gm",
|
Username = "gm",
|
||||||
@@ -83,7 +79,7 @@ public sealed class ServiceSharedHelperTests
|
|||||||
DisplayName = "GM",
|
DisplayName = "GM",
|
||||||
Roles = string.Empty
|
Roles = string.Empty
|
||||||
};
|
};
|
||||||
store.UsersById[playerId] = new UserAccount
|
store.UsersById[playerId] = new()
|
||||||
{
|
{
|
||||||
Id = playerId,
|
Id = playerId,
|
||||||
Username = "player",
|
Username = "player",
|
||||||
@@ -92,7 +88,7 @@ public sealed class ServiceSharedHelperTests
|
|||||||
DisplayName = "Player",
|
DisplayName = "Player",
|
||||||
Roles = string.Empty
|
Roles = string.Empty
|
||||||
};
|
};
|
||||||
store.UsersById[outsiderId] = new UserAccount
|
store.UsersById[outsiderId] = new()
|
||||||
{
|
{
|
||||||
Id = outsiderId,
|
Id = outsiderId,
|
||||||
Username = "outsider",
|
Username = "outsider",
|
||||||
@@ -112,7 +108,7 @@ public sealed class ServiceSharedHelperTests
|
|||||||
};
|
};
|
||||||
store.CampaignsById[campaignId] = campaign;
|
store.CampaignsById[campaignId] = campaign;
|
||||||
var playerCharacterId = Guid.NewGuid();
|
var playerCharacterId = Guid.NewGuid();
|
||||||
store.CharactersById[playerCharacterId] = new Character
|
store.CharactersById[playerCharacterId] = new()
|
||||||
{
|
{
|
||||||
Id = playerCharacterId,
|
Id = playerCharacterId,
|
||||||
OwnerUserId = playerId,
|
OwnerUserId = playerId,
|
||||||
@@ -171,7 +167,7 @@ public sealed class ServiceSharedHelperTests
|
|||||||
var campaignId = Guid.NewGuid();
|
var campaignId = Guid.NewGuid();
|
||||||
var store = new GameStateStore();
|
var store = new GameStateStore();
|
||||||
|
|
||||||
store.UsersById[userId] = new UserAccount
|
store.UsersById[userId] = new()
|
||||||
{
|
{
|
||||||
Id = userId,
|
Id = userId,
|
||||||
Username = "user",
|
Username = "user",
|
||||||
@@ -180,7 +176,7 @@ public sealed class ServiceSharedHelperTests
|
|||||||
DisplayName = "User",
|
DisplayName = "User",
|
||||||
Roles = string.Empty
|
Roles = string.Empty
|
||||||
};
|
};
|
||||||
store.UsersById[otherUserId] = new UserAccount
|
store.UsersById[otherUserId] = new()
|
||||||
{
|
{
|
||||||
Id = otherUserId,
|
Id = otherUserId,
|
||||||
Username = "other",
|
Username = "other",
|
||||||
@@ -189,13 +185,13 @@ public sealed class ServiceSharedHelperTests
|
|||||||
DisplayName = "Other",
|
DisplayName = "Other",
|
||||||
Roles = string.Empty
|
Roles = string.Empty
|
||||||
};
|
};
|
||||||
store.SessionsByToken["valid"] = new UserSession
|
store.SessionsByToken["valid"] = new()
|
||||||
{
|
{
|
||||||
Token = "valid",
|
Token = "valid",
|
||||||
UserId = userId,
|
UserId = userId,
|
||||||
CreatedAtUtc = DateTimeOffset.UtcNow
|
CreatedAtUtc = DateTimeOffset.UtcNow
|
||||||
};
|
};
|
||||||
store.CampaignsById[campaignId] = new Campaign
|
store.CampaignsById[campaignId] = new()
|
||||||
{
|
{
|
||||||
Id = campaignId,
|
Id = campaignId,
|
||||||
GmUserId = otherUserId,
|
GmUserId = otherUserId,
|
||||||
@@ -267,7 +263,7 @@ public sealed class ServiceSharedHelperTests
|
|||||||
var rollId = Guid.NewGuid();
|
var rollId = Guid.NewGuid();
|
||||||
var store = new GameStateStore();
|
var store = new GameStateStore();
|
||||||
|
|
||||||
store.UsersById[gmId] = new UserAccount
|
store.UsersById[gmId] = new()
|
||||||
{
|
{
|
||||||
Id = gmId,
|
Id = gmId,
|
||||||
Username = "gm",
|
Username = "gm",
|
||||||
@@ -276,7 +272,7 @@ public sealed class ServiceSharedHelperTests
|
|||||||
DisplayName = "GM",
|
DisplayName = "GM",
|
||||||
Roles = UserRoles.Admin
|
Roles = UserRoles.Admin
|
||||||
};
|
};
|
||||||
store.UsersById[ownerId] = new UserAccount
|
store.UsersById[ownerId] = new()
|
||||||
{
|
{
|
||||||
Id = ownerId,
|
Id = ownerId,
|
||||||
Username = "owner",
|
Username = "owner",
|
||||||
@@ -285,7 +281,7 @@ public sealed class ServiceSharedHelperTests
|
|||||||
DisplayName = "Owner",
|
DisplayName = "Owner",
|
||||||
Roles = string.Empty
|
Roles = string.Empty
|
||||||
};
|
};
|
||||||
store.UsersById[blankOwnerId] = new UserAccount
|
store.UsersById[blankOwnerId] = new()
|
||||||
{
|
{
|
||||||
Id = blankOwnerId,
|
Id = blankOwnerId,
|
||||||
Username = "blank",
|
Username = "blank",
|
||||||
@@ -294,7 +290,7 @@ public sealed class ServiceSharedHelperTests
|
|||||||
DisplayName = "",
|
DisplayName = "",
|
||||||
Roles = string.Empty
|
Roles = string.Empty
|
||||||
};
|
};
|
||||||
store.CampaignsById[campaignId] = new Campaign
|
store.CampaignsById[campaignId] = new()
|
||||||
{
|
{
|
||||||
Id = campaignId,
|
Id = campaignId,
|
||||||
GmUserId = gmId,
|
GmUserId = gmId,
|
||||||
@@ -302,14 +298,14 @@ public sealed class ServiceSharedHelperTests
|
|||||||
Ruleset = RulesetKind.Rolemaster,
|
Ruleset = RulesetKind.Rolemaster,
|
||||||
Version = 1
|
Version = 1
|
||||||
};
|
};
|
||||||
store.CharactersById[characterId] = new Character
|
store.CharactersById[characterId] = new()
|
||||||
{
|
{
|
||||||
Id = characterId,
|
Id = characterId,
|
||||||
OwnerUserId = ownerId,
|
OwnerUserId = ownerId,
|
||||||
CampaignId = campaignId,
|
CampaignId = campaignId,
|
||||||
Name = "Scout"
|
Name = "Scout"
|
||||||
};
|
};
|
||||||
store.SkillGroupsById[skillGroupId] = new SkillGroup
|
store.SkillGroupsById[skillGroupId] = new()
|
||||||
{
|
{
|
||||||
Id = skillGroupId,
|
Id = skillGroupId,
|
||||||
CharacterId = characterId,
|
CharacterId = characterId,
|
||||||
@@ -319,7 +315,7 @@ public sealed class ServiceSharedHelperTests
|
|||||||
AllowFumble = false,
|
AllowFumble = false,
|
||||||
FumbleRange = 5
|
FumbleRange = 5
|
||||||
};
|
};
|
||||||
store.SkillsById[skillId] = new Skill
|
store.SkillsById[skillId] = new()
|
||||||
{
|
{
|
||||||
Id = skillId,
|
Id = skillId,
|
||||||
CharacterId = characterId,
|
CharacterId = characterId,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public sealed class ServiceSkillGroupAndOwnershipTests
|
|||||||
var otherGroup = ServiceTestSupport.GetValue(service.CreateSkillGroup(otherSession, otherCharacter.Id, "Other Group", "2D+1", 1, true));
|
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);
|
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);
|
Assert.Null(ungroupedSkill.SkillGroupId);
|
||||||
|
|
||||||
var regroupedSkill = ServiceTestSupport.GetValue(service.UpdateSkill(ownerSession, skill.Id, "Strike", "2D+1", 1, true, renamedGroup.Id));
|
var regroupedSkill = ServiceTestSupport.GetValue(service.UpdateSkill(ownerSession, skill.Id, "Strike", "2D+1", 1, true, renamedGroup.Id));
|
||||||
|
|||||||
@@ -32,11 +32,48 @@ public sealed class ServiceStateInfrastructureTests
|
|||||||
Roles = "admin",
|
Roles = "admin",
|
||||||
ActiveCharacterId = Guid.NewGuid()
|
ActiveCharacterId = Guid.NewGuid()
|
||||||
};
|
};
|
||||||
var session = new UserSession { Token = "token", UserId = user.Id, CreatedAtUtc = DateTimeOffset.UtcNow };
|
var session = new UserSession
|
||||||
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" };
|
Token = "token",
|
||||||
var skillGroup = new SkillGroup { Id = Guid.NewGuid(), CharacterId = character.Id, Name = "Group", DiceRollDefinition = "2D+1", WildDice = 1, AllowFumble = true, FumbleRange = null };
|
UserId = user.Id,
|
||||||
var skill = new Skill { Id = Guid.NewGuid(), CharacterId = character.Id, SkillGroupId = skillGroup.Id, Name = "Skill", DiceRollDefinition = "2D+2", WildDice = 1, AllowFumble = true, FumbleRange = null };
|
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
|
var logEntry = new RollLogEntry
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
|
|||||||
@@ -1,12 +1,182 @@
|
|||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using RpgRoller.Components;
|
using RpgRoller.Components;
|
||||||
using RpgRoller.Contracts;
|
|
||||||
using RpgRoller.Services;
|
|
||||||
|
|
||||||
namespace RpgRoller.Tests;
|
namespace RpgRoller.Tests;
|
||||||
|
|
||||||
public sealed class WorkspaceQueryServiceTests
|
public sealed class WorkspaceQueryServiceTests
|
||||||
{
|
{
|
||||||
|
private sealed class StubGameService : IGameService
|
||||||
|
{
|
||||||
|
public IReadOnlyList<RulesetDefinition> GetRulesets()
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<UserSummary> 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<MeResponse> GetMe(string sessionToken)
|
||||||
|
{
|
||||||
|
return GetMeHandler(sessionToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<CampaignSummary> CreateCampaign(string sessionToken, string name, string rulesetId)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<IReadOnlyList<CampaignSummary>> GetCampaigns(string sessionToken)
|
||||||
|
{
|
||||||
|
return GetCampaignsHandler(sessionToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<IReadOnlyList<CampaignOption>> GetCharacterCampaignOptions(string sessionToken)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<CampaignRoster> GetCampaign(string sessionToken, Guid campaignId)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<bool> DeleteCampaign(string sessionToken, Guid campaignId)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<IReadOnlyList<string>> GetUsernames(string sessionToken)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<IReadOnlyList<AdminUserSummary>> GetUsers(string sessionToken)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<AdminUserSummary> UpdateUserRoles(string sessionToken, Guid userId, IReadOnlyList<string> roles)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<bool> DeleteUser(string sessionToken, Guid userId)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<CharacterSummary> CreateCharacter(string sessionToken, string name, Guid campaignId)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<CharacterSummary> UpdateCharacter(string sessionToken, Guid characterId, string name, Guid? campaignId, string? ownerUsername = null)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<bool> DeleteCharacter(string sessionToken, Guid characterId)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<bool> ActivateCharacter(string sessionToken, Guid characterId)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<IReadOnlyList<CharacterSummary>> GetOwnCharacters(string sessionToken)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<SkillGroupSummary> CreateSkillGroup(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<SkillGroupSummary> UpdateSkillGroup(string sessionToken, Guid skillGroupId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<bool> DeleteSkillGroup(string sessionToken, Guid skillGroupId)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<bool> DeleteSkill(string sessionToken, Guid skillId)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<CharacterSheet> GetCharacterSheet(string sessionToken, Guid characterId)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<RollResult> RollCustom(string sessionToken, Guid characterId, string expression, string visibility)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<IReadOnlyList<CampaignLogEntry>> GetCampaignLog(string sessionToken, Guid campaignId)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<CampaignLogPage> GetCampaignLogPage(string sessionToken, Guid campaignId, Guid? afterRollId = null, int? limit = null)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<CampaignRollDetail> GetRollDetail(string sessionToken, Guid rollId)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceResult<CampaignStateSnapshot> GetCampaignStateSnapshot(string sessionToken, Guid campaignId)
|
||||||
|
{
|
||||||
|
throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Func<string, ServiceResult<MeResponse>> GetMeHandler { get; init; } = _ => ServiceResult<MeResponse>.Failure("unexpected_call", "Unexpected GetMe call.");
|
||||||
|
|
||||||
|
public Func<string, ServiceResult<IReadOnlyList<CampaignSummary>>> GetCampaignsHandler { get; init; } = _ => ServiceResult<IReadOnlyList<CampaignSummary>>.Failure("unexpected_call", "Unexpected GetCampaigns call.");
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SessionTokenAccessor_ReadsSessionCookieFromHttpContext()
|
public void SessionTokenAccessor_ReadsSessionCookieFromHttpContext()
|
||||||
{
|
{
|
||||||
@@ -27,7 +197,7 @@ public sealed class WorkspaceQueryServiceTests
|
|||||||
GetCampaignsHandler = sessionToken =>
|
GetCampaignsHandler = sessionToken =>
|
||||||
{
|
{
|
||||||
Assert.Equal("server-session", sessionToken);
|
Assert.Equal("server-session", sessionToken);
|
||||||
return ServiceResult<IReadOnlyList<CampaignSummary>>.Success([new CampaignSummary(Guid.NewGuid(), "Alpha", "d6", new CampaignGmSummary(Guid.NewGuid(), "GM"), 1)]);
|
return ServiceResult<IReadOnlyList<CampaignSummary>>.Success([new(Guid.NewGuid(), "Alpha", "d6", new(Guid.NewGuid(), "GM"), 1)]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -40,10 +210,7 @@ public sealed class WorkspaceQueryServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetMeAsync_MapsUnauthorizedServiceResultToApiRequestException()
|
public async Task GetMeAsync_MapsUnauthorizedServiceResultToApiRequestException()
|
||||||
{
|
{
|
||||||
var service = new StubGameService
|
var service = new StubGameService { GetMeHandler = _ => ServiceResult<MeResponse>.Failure("unauthorized", "You must be logged in.") };
|
||||||
{
|
|
||||||
GetMeHandler = _ => ServiceResult<MeResponse>.Failure("unauthorized", "You must be logged in.")
|
|
||||||
};
|
|
||||||
|
|
||||||
var queryService = new WorkspaceQueryService(service, CreateSessionTokenAccessor("expired-session"));
|
var queryService = new WorkspaceQueryService(service, CreateSessionTokenAccessor("expired-session"));
|
||||||
var exception = await Assert.ThrowsAsync<ApiRequestException>(queryService.GetMeAsync);
|
var exception = await Assert.ThrowsAsync<ApiRequestException>(queryService.GetMeAsync);
|
||||||
@@ -57,49 +224,6 @@ public sealed class WorkspaceQueryServiceTests
|
|||||||
{
|
{
|
||||||
var httpContext = new DefaultHttpContext();
|
var httpContext = new DefaultHttpContext();
|
||||||
httpContext.Request.Headers.Cookie = $"rpgroller_session={sessionToken}";
|
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<string, ServiceResult<MeResponse>> GetMeHandler { get; init; } =
|
|
||||||
_ => ServiceResult<MeResponse>.Failure("unexpected_call", "Unexpected GetMe call.");
|
|
||||||
|
|
||||||
public Func<string, ServiceResult<IReadOnlyList<CampaignSummary>>> GetCampaignsHandler { get; init; } =
|
|
||||||
_ => ServiceResult<IReadOnlyList<CampaignSummary>>.Failure("unexpected_call", "Unexpected GetCampaigns call.");
|
|
||||||
|
|
||||||
public IReadOnlyList<RulesetDefinition> GetRulesets() => throw new NotSupportedException();
|
|
||||||
public ServiceResult<UserSummary> 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<MeResponse> GetMe(string sessionToken) => GetMeHandler(sessionToken);
|
|
||||||
public ServiceResult<CampaignSummary> CreateCampaign(string sessionToken, string name, string rulesetId) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<IReadOnlyList<CampaignSummary>> GetCampaigns(string sessionToken) => GetCampaignsHandler(sessionToken);
|
|
||||||
public ServiceResult<IReadOnlyList<CampaignOption>> GetCharacterCampaignOptions(string sessionToken) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<CampaignRoster> GetCampaign(string sessionToken, Guid campaignId) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<bool> DeleteCampaign(string sessionToken, Guid campaignId) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<IReadOnlyList<string>> GetUsernames(string sessionToken) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<IReadOnlyList<AdminUserSummary>> GetUsers(string sessionToken) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<AdminUserSummary> UpdateUserRoles(string sessionToken, Guid userId, IReadOnlyList<string> roles) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<bool> DeleteUser(string sessionToken, Guid userId) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<CharacterSummary> CreateCharacter(string sessionToken, string name, Guid campaignId) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<CharacterSummary> UpdateCharacter(string sessionToken, Guid characterId, string name, Guid? campaignId, string? ownerUsername = null) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<bool> DeleteCharacter(string sessionToken, Guid characterId) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<bool> ActivateCharacter(string sessionToken, Guid characterId) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<IReadOnlyList<CharacterSummary>> GetOwnCharacters(string sessionToken) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<SkillGroupSummary> CreateSkillGroup(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<SkillGroupSummary> UpdateSkillGroup(string sessionToken, Guid skillGroupId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<bool> DeleteSkillGroup(string sessionToken, Guid skillGroupId) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<bool> DeleteSkill(string sessionToken, Guid skillId) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<CharacterSheet> GetCharacterSheet(string sessionToken, Guid characterId) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<RollResult> RollCustom(string sessionToken, Guid characterId, string expression, string visibility) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<IReadOnlyList<CampaignLogEntry>> GetCampaignLog(string sessionToken, Guid campaignId) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<CampaignLogPage> GetCampaignLogPage(string sessionToken, Guid campaignId, Guid? afterRollId = null, int? limit = null) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<CampaignRollDetail> GetRollDetail(string sessionToken, Guid rollId) => throw new NotSupportedException();
|
|
||||||
public ServiceResult<CampaignStateSnapshot> GetCampaignStateSnapshot(string sessionToken, Guid campaignId) => throw new NotSupportedException();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using RpgRoller.Components.Pages;
|
using RpgRoller.Components.Pages;
|
||||||
using RpgRoller.Contracts;
|
|
||||||
using RpgRoller.Domain;
|
|
||||||
|
|
||||||
namespace RpgRoller.Tests;
|
namespace RpgRoller.Tests;
|
||||||
|
|
||||||
@@ -14,14 +12,9 @@ public sealed class WorkspaceStateTests
|
|||||||
var otherOwnerId = Guid.NewGuid();
|
var otherOwnerId = Guid.NewGuid();
|
||||||
var state = new WorkspaceState
|
var state = new WorkspaceState
|
||||||
{
|
{
|
||||||
User = new UserSummary(userId, "user", "User", []),
|
User = new(userId, "user", "User", []),
|
||||||
SelectedCampaign = new CampaignRoster(
|
SelectedCampaign = new(Guid.NewGuid(), "Alpha", "d6", new(gmId, "GM"), [
|
||||||
Guid.NewGuid(),
|
new(Guid.NewGuid(), "Scout", otherOwnerId, Guid.NewGuid(), "Other Owner")
|
||||||
"Alpha",
|
|
||||||
"d6",
|
|
||||||
new CampaignGmSummary(gmId, "GM"),
|
|
||||||
[
|
|
||||||
new CharacterSummary(Guid.NewGuid(), "Scout", otherOwnerId, Guid.NewGuid(), "Other Owner")
|
|
||||||
])
|
])
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,17 +28,14 @@ public sealed class WorkspaceStateTests
|
|||||||
public void SkillDefinitionLabel_FormatsD6RolemasterAndDefaultRulesets()
|
public void SkillDefinitionLabel_FormatsD6RolemasterAndDefaultRulesets()
|
||||||
{
|
{
|
||||||
var skill = new CharacterSheetSkill(Guid.NewGuid(), null, "Awareness", "d100!+15", 1, true, 5);
|
var skill = new CharacterSheetSkill(Guid.NewGuid(), null, "Awareness", "d100!+15", 1, true, 5);
|
||||||
var state = new WorkspaceState
|
var state = new WorkspaceState { SelectedCampaign = new(Guid.NewGuid(), "Alpha", "d6", new(Guid.NewGuid(), "GM"), []) };
|
||||||
{
|
|
||||||
SelectedCampaign = new CampaignRoster(Guid.NewGuid(), "Alpha", "d6", new CampaignGmSummary(Guid.NewGuid(), "GM"), [])
|
|
||||||
};
|
|
||||||
|
|
||||||
Assert.Equal("d100!+15, wild 1, fumble on", state.SkillDefinitionLabel(skill));
|
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));
|
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));
|
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 otherCharacter = new CharacterSummary(Guid.NewGuid(), "Other", Guid.NewGuid(), Guid.NewGuid(), "Other");
|
||||||
var state = new WorkspaceState
|
var state = new WorkspaceState
|
||||||
{
|
{
|
||||||
User = new UserSummary(userId, "user", "User", []),
|
User = new(userId, "user", "User", []),
|
||||||
SelectedCampaign = new CampaignRoster(
|
SelectedCampaign = new(Guid.NewGuid(), "Alpha", "d6", new(Guid.NewGuid(), "GM"), [ownedCharacter, secondOwnedCharacter, otherCharacter]),
|
||||||
Guid.NewGuid(),
|
|
||||||
"Alpha",
|
|
||||||
"d6",
|
|
||||||
new CampaignGmSummary(Guid.NewGuid(), "GM"),
|
|
||||||
[ownedCharacter, secondOwnedCharacter, otherCharacter]),
|
|
||||||
SelectedCharacterId = secondOwnedCharacter.Id,
|
SelectedCharacterId = secondOwnedCharacter.Id,
|
||||||
ActiveCharacterId = ownedCharacter.Id,
|
ActiveCharacterId = ownedCharacter.Id,
|
||||||
SelectedCharacterSkills = [new CharacterSheetSkill(Guid.NewGuid(), null, "Stealth", "2D+1", 1, true, null)],
|
SelectedCharacterSkills = [new(Guid.NewGuid(), null, "Stealth", "2D+1", 1, true, null)],
|
||||||
SelectedCharacterSkillGroups = [new CharacterSheetSkillGroup(Guid.NewGuid(), "Combat", "2D+1", 1, true, null)]
|
SelectedCharacterSkillGroups = [new(Guid.NewGuid(), "Combat", "2D+1", 1, true, null)]
|
||||||
};
|
};
|
||||||
|
|
||||||
Assert.Equal(2, state.PlaySelectedCampaign!.Characters.Length);
|
Assert.Equal(2, state.PlaySelectedCampaign!.Characters.Length);
|
||||||
@@ -89,8 +74,8 @@ public sealed class WorkspaceStateTests
|
|||||||
var adminId = Guid.NewGuid();
|
var adminId = Guid.NewGuid();
|
||||||
var state = new WorkspaceState
|
var state = new WorkspaceState
|
||||||
{
|
{
|
||||||
User = new UserSummary(adminId, "admin", "Admin", [UserRoles.Admin]),
|
User = new(adminId, "admin", "Admin", [UserRoles.Admin]),
|
||||||
SelectedCampaign = new CampaignRoster(Guid.NewGuid(), "Alpha", "d6", new CampaignGmSummary(adminId, "Admin"), []),
|
SelectedCampaign = new(Guid.NewGuid(), "Alpha", "d6", new(adminId, "Admin"), []),
|
||||||
CurrentScreen = "admin",
|
CurrentScreen = "admin",
|
||||||
ConnectionState = "reconnecting"
|
ConnectionState = "reconnecting"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ internal static class AdminEndpoints
|
|||||||
return TypedResults.Unauthorized();
|
return TypedResults.Unauthorized();
|
||||||
|
|
||||||
if (!user.Roles.Contains(UserRoles.Admin, StringComparer.OrdinalIgnoreCase))
|
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))
|
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);
|
var stream = new FileStream(databaseFile.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||||
return TypedResults.File(stream, "application/octet-stream", Path.GetFileName(databaseFile.Path));
|
return TypedResults.File(stream, "application/octet-stream", Path.GetFileName(databaseFile.Path));
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using RpgRoller.Contracts;
|
|
||||||
using RpgRoller.Services;
|
using RpgRoller.Services;
|
||||||
|
|
||||||
namespace RpgRoller.Api;
|
namespace RpgRoller.Api;
|
||||||
|
|||||||
@@ -13,9 +13,7 @@ internal static class StateEventEndpoints
|
|||||||
var stateResult = game.GetCampaignStateSnapshot(sessionToken, campaignId);
|
var stateResult = game.GetCampaignStateSnapshot(sessionToken, campaignId);
|
||||||
if (!stateResult.Succeeded)
|
if (!stateResult.Succeeded)
|
||||||
{
|
{
|
||||||
return stateResult.Error!.Code == "unauthorized"
|
return stateResult.Error!.Code == "unauthorized" ? TypedResults.Unauthorized() : TypedResults.BadRequest(new ApiError(stateResult.Error.Message, stateResult.Error.Code));
|
||||||
? TypedResults.Unauthorized()
|
|
||||||
: TypedResults.BadRequest(new ApiError(stateResult.Error.Message, stateResult.Error.Code));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context.Response.Headers.CacheControl = "no-cache";
|
context.Response.Headers.CacheControl = "no-cache";
|
||||||
@@ -60,11 +58,8 @@ internal static class StateEventEndpoints
|
|||||||
|
|
||||||
private static Task WriteStateEventAsync(HttpResponse response, CampaignStateSnapshot snapshot)
|
private static Task WriteStateEventAsync(HttpResponse response, CampaignStateSnapshot snapshot)
|
||||||
{
|
{
|
||||||
var characterVersions = string.Join(
|
var characterVersions = string.Join(",", snapshot.CharacterVersions.Select(version => $"{{\"characterId\":\"{version.CharacterId}\",\"version\":{version.Version}}}"));
|
||||||
",",
|
|
||||||
snapshot.CharacterVersions.Select(version => $"{{\"characterId\":\"{version.CharacterId}\",\"version\":{version.Version}}}"));
|
|
||||||
|
|
||||||
return response.WriteAsync(
|
return response.WriteAsync($"event: state\ndata: {{\"campaignId\":\"{snapshot.CampaignId}\",\"totalVersion\":{snapshot.TotalVersion},\"rosterVersion\":{snapshot.RosterVersion},\"logVersion\":{snapshot.LogVersion},\"characterVersions\":[{characterVersions}]}}\n\n");
|
||||||
$"event: state\ndata: {{\"campaignId\":\"{snapshot.CampaignId}\",\"totalVersion\":{snapshot.TotalVersion},\"rosterVersion\":{snapshot.RosterVersion},\"logVersion\":{snapshot.LogVersion},\"characterVersions\":[{characterVersions}]}}\n\n");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,8 +22,9 @@
|
|||||||
</html>
|
</html>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
private Microsoft.AspNetCore.Http.HttpContext? HttpContext { get; set; }
|
private HttpContext? HttpContext { get; set; }
|
||||||
|
|
||||||
private string BaseHref
|
private string BaseHref
|
||||||
{
|
{
|
||||||
@@ -36,4 +37,5 @@
|
|||||||
return pathBase.EndsWith('/') ? pathBase : $"{pathBase}/";
|
return pathBase.EndsWith('/') ? pathBase : $"{pathBase}/";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -28,9 +28,7 @@ public partial class AdminHome
|
|||||||
if (!IsCurrentUserAdmin)
|
if (!IsCurrentUserAdmin)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Users = (await ApiClient.RequestAsync<IReadOnlyList<AdminUserSummary>>("GET", "/api/admin/users"))
|
Users = (await ApiClient.RequestAsync<IReadOnlyList<AdminUserSummary>>("GET", "/api/admin/users")).OrderBy(user => user.Username, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
.OrderBy(user => user.Username, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
}
|
}
|
||||||
catch (ApiRequestException ex) when (ex.StatusCode == 401)
|
catch (ApiRequestException ex) when (ex.StatusCode == 401)
|
||||||
{
|
{
|
||||||
@@ -92,10 +90,7 @@ public partial class AdminHome
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
IReadOnlyList<string> roles = HasAdminRole(user) ? Array.Empty<string>() : [UserRoles.Admin];
|
IReadOnlyList<string> roles = HasAdminRole(user) ? Array.Empty<string>() : [UserRoles.Admin];
|
||||||
_ = await ApiClient.RequestAsync<AdminUserSummary>(
|
_ = await ApiClient.RequestAsync<AdminUserSummary>("PUT", $"/api/admin/users/{user.Id}/roles", new UpdateUserRolesRequest(roles));
|
||||||
"PUT",
|
|
||||||
$"/api/admin/users/{user.Id}/roles",
|
|
||||||
new UpdateUserRolesRequest(roles));
|
|
||||||
|
|
||||||
await ReloadUsersAsync();
|
await ReloadUsersAsync();
|
||||||
SetStatus("User roles updated.", false);
|
SetStatus("User roles updated.", false);
|
||||||
@@ -138,9 +133,7 @@ public partial class AdminHome
|
|||||||
|
|
||||||
private async Task ReloadUsersAsync()
|
private async Task ReloadUsersAsync()
|
||||||
{
|
{
|
||||||
Users = (await ApiClient.RequestAsync<IReadOnlyList<AdminUserSummary>>("GET", "/api/admin/users"))
|
Users = (await ApiClient.RequestAsync<IReadOnlyList<AdminUserSummary>>("GET", "/api/admin/users")).OrderBy(user => user.Username, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
.OrderBy(user => user.Username, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool HasAdminRole(UserSummary user)
|
private static bool HasAdminRole(UserSummary user)
|
||||||
@@ -184,18 +177,28 @@ public partial class AdminHome
|
|||||||
private List<AdminUserSummary> Users { get; set; } = [];
|
private List<AdminUserSummary> Users { get; set; } = [];
|
||||||
private string? StatusMessage { get; set; }
|
private string? StatusMessage { get; set; }
|
||||||
private bool StatusIsError { get; set; }
|
private bool StatusIsError { get; set; }
|
||||||
private IReadOnlyList<AppHeaderMenuItem> HeaderMenuItems
|
|
||||||
{
|
private IReadOnlyList<AppHeaderMenuItem> HeaderMenuItems =>
|
||||||
get
|
|
||||||
{
|
|
||||||
return
|
|
||||||
[
|
[
|
||||||
new AppHeaderMenuItem { Label = "Play", IsActive = false, OnSelected = OpenPlayAsync },
|
new AppHeaderMenuItem
|
||||||
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]
|
[Parameter]
|
||||||
public EventCallback<string?> LoggedOut { get; set; }
|
public EventCallback<string?> LoggedOut { get; set; }
|
||||||
|
|||||||
@@ -3,11 +3,15 @@
|
|||||||
<h1>@Title</h1>
|
<h1>@Title</h1>
|
||||||
@if (User is null)
|
@if (User is null)
|
||||||
{
|
{
|
||||||
<p class="header-identity"><strong>Loading user...</strong></p>
|
<p class="header-identity">
|
||||||
|
<strong>Loading user...</strong>
|
||||||
|
</p>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<p class="header-identity"><strong>@User.DisplayName</strong> <span class="muted">(@User.Username)</span></p>
|
<p class="header-identity">
|
||||||
|
<strong>@User.DisplayName</strong> <span class="muted">(@User.Username)</span>
|
||||||
|
</p>
|
||||||
}
|
}
|
||||||
@if (ShowCampaign)
|
@if (ShowCampaign)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<aside @ref="LogPanelRef" class="card log-panel">
|
<aside @ref="LogPanelRef" class="card log-panel">
|
||||||
<div class="section-head"><h2>Campaign Log</h2></div>
|
<div class="section-head">
|
||||||
|
<h2>Campaign Log</h2>
|
||||||
|
</div>
|
||||||
<div @ref="LogFeedRef" class="log-panel-feed">
|
<div @ref="LogFeedRef" class="log-panel-feed">
|
||||||
@if (IsCampaignDataLoading)
|
@if (IsCampaignDataLoading)
|
||||||
{
|
{
|
||||||
@@ -47,9 +49,12 @@
|
|||||||
}
|
}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
<span class="log-meta"><span class="badge @entry.VisibilityStyle">@entry.VisibilityLabel</span>
|
<span class="log-meta">
|
||||||
|
<span class="badge @entry.VisibilityStyle">@entry.VisibilityLabel</span>
|
||||||
<time
|
<time
|
||||||
title="@entry.TimestampUtc.ToString("O")">@entry.TimestampUtc.ToLocalTime().ToString("g")</time>
|
title="@entry.TimestampUtc.ToString("O")">
|
||||||
|
@entry.TimestampUtc.ToLocalTime().ToString("g")
|
||||||
|
</time>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@if (isExpanded)
|
@if (isExpanded)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
using RpgRoller.Components;
|
|
||||||
using RpgRoller.Contracts;
|
using RpgRoller.Contracts;
|
||||||
|
|
||||||
namespace RpgRoller.Components.Pages.HomeControls;
|
namespace RpgRoller.Components.Pages.HomeControls;
|
||||||
@@ -9,6 +8,8 @@ namespace RpgRoller.Components.Pages.HomeControls;
|
|||||||
[ExcludeFromCodeCoverage]
|
[ExcludeFromCodeCoverage]
|
||||||
public partial class CampaignLogPanel
|
public partial class CampaignLogPanel
|
||||||
{
|
{
|
||||||
|
private sealed record EventBadgeView(string Label, string Tone);
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
var currentLastRollId = CampaignLog.LastOrDefault()?.RollId;
|
var currentLastRollId = CampaignLog.LastOrDefault()?.RollId;
|
||||||
@@ -37,6 +38,96 @@ public partial class CampaignLogPanel
|
|||||||
LastRenderedLogRollId = currentLastRollId;
|
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<RollResult>("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<EventBadgeView> GetEventBadges(CampaignLogListEntry entry)
|
||||||
|
{
|
||||||
|
return (entry.EventBadges ?? []).Select(ToEventBadgeView).Where(badge => badge is not null).Cast<EventBadgeView>().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<string> { entry.VisibilityStyle };
|
||||||
|
if (isExpanded)
|
||||||
|
classes.Add("expanded");
|
||||||
|
|
||||||
|
if (isFresh)
|
||||||
|
classes.Add("fresh");
|
||||||
|
|
||||||
|
return string.Join(" ", classes);
|
||||||
|
}
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private IJSRuntime JS { get; set; } = null!;
|
private IJSRuntime JS { get; set; } = null!;
|
||||||
|
|
||||||
@@ -97,105 +188,6 @@ public partial class CampaignLogPanel
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public EventCallback<string> ErrorOccurred { get; set; }
|
public EventCallback<string> 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<RollResult>(
|
|
||||||
"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<EventBadgeView> GetEventBadges(CampaignLogListEntry entry)
|
|
||||||
{
|
|
||||||
return (entry.EventBadges ?? [])
|
|
||||||
.Select(ToEventBadgeView)
|
|
||||||
.Where(badge => badge is not null)
|
|
||||||
.Cast<EventBadgeView>()
|
|
||||||
.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<string> { 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 bool HasCustomRollError => CustomRollState.Errors.ContainsKey("expression");
|
||||||
private string? CustomRollErrorMessage => CustomRollState.Errors.GetValueOrDefault("expression");
|
private string? CustomRollErrorMessage => CustomRollState.Errors.GetValueOrDefault("expression");
|
||||||
private bool IsCustomRollDisabled => IsCampaignDataLoading || IsMutating || IsSubmittingCustomRoll || !SelectedCharacterId.HasValue;
|
private bool IsCustomRollDisabled => IsCampaignDataLoading || IsMutating || IsSubmittingCustomRoll || !SelectedCharacterId.HasValue;
|
||||||
@@ -203,6 +195,7 @@ public partial class CampaignLogPanel
|
|||||||
private string? CustomRollInputTitle => HasCustomRollError ? CustomRollErrorMessage : null;
|
private string? CustomRollInputTitle => HasCustomRollError ? CustomRollErrorMessage : null;
|
||||||
private string CustomRollErrorElementId => "custom-roll-expression-error";
|
private string CustomRollErrorElementId => "custom-roll-expression-error";
|
||||||
private string? CustomRollInputDescribedBy => HasCustomRollError ? CustomRollErrorElementId : null;
|
private string? CustomRollInputDescribedBy => HasCustomRollError ? CustomRollErrorElementId : null;
|
||||||
|
|
||||||
private string CustomRollPlaceholder => SelectedCampaignRulesetId.ToLowerInvariant() switch
|
private string CustomRollPlaceholder => SelectedCampaignRulesetId.ToLowerInvariant() switch
|
||||||
{
|
{
|
||||||
RulesetFormHelpers.RulesetIds.D6 => "e.g. 5D+4",
|
RulesetFormHelpers.RulesetIds.D6 => "e.g. 5D+4",
|
||||||
@@ -210,17 +203,19 @@ public partial class CampaignLogPanel
|
|||||||
RulesetFormHelpers.RulesetIds.Rolemaster => "e.g. d10, 15d10, d100!+85",
|
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}"
|
private string CustomRollStatusText => SelectedCharacterId.HasValue && !string.IsNullOrWhiteSpace(SelectedCharacterName) ? $"For {SelectedCharacterName} • {RollVisibilityLabel}" : "Select a character to enable";
|
||||||
: "Select a character to enable";
|
|
||||||
private string CustomRollHelpText => SelectedCampaignRulesetId.ToLowerInvariant() switch
|
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.",
|
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 RollVisibilityLabel => string.Equals(NormalizedRollVisibility, "private", StringComparison.OrdinalIgnoreCase) ? "Private" : "Public";
|
||||||
private string NormalizedRollVisibility => string.Equals(RollVisibility, "private", StringComparison.OrdinalIgnoreCase) ? "private" : "public";
|
private string NormalizedRollVisibility => string.Equals(RollVisibility, "private", StringComparison.OrdinalIgnoreCase) ? "private" : "public";
|
||||||
|
|
||||||
private string CustomRollExpression
|
private string CustomRollExpression
|
||||||
{
|
{
|
||||||
get => CustomRollState.Model.Expression;
|
get => CustomRollState.Model.Expression;
|
||||||
|
|||||||
@@ -54,9 +54,7 @@ public partial class CharacterFormModal
|
|||||||
character = await ApiClient.RequestAsync<CharacterSummary>("PUT", $"/api/characters/{EditingCharacterId.Value}", new UpdateCharacterRequest(FormState.Model.Name.Trim(), campaignId, ownerUsername));
|
character = await ApiClient.RequestAsync<CharacterSummary>("PUT", $"/api/characters/{EditingCharacterId.Value}", new UpdateCharacterRequest(FormState.Model.Name.Trim(), campaignId, ownerUsername));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
|
||||||
character = await ApiClient.RequestAsync<CharacterSummary>("POST", "/api/characters", new CreateCharacterRequest(FormState.Model.Name.Trim(), campaignId!.Value));
|
character = await ApiClient.RequestAsync<CharacterSummary>("POST", "/api/characters", new CreateCharacterRequest(FormState.Model.Name.Trim(), campaignId!.Value));
|
||||||
}
|
|
||||||
|
|
||||||
await CharacterSaved.InvokeAsync(character.CampaignId);
|
await CharacterSaved.InvokeAsync(character.CampaignId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,8 +41,12 @@
|
|||||||
<span aria-hidden="true" class="emoji">✏️</span>
|
<span aria-hidden="true" class="emoji">✏️</span>
|
||||||
<span class="sr-only">Edit character</span>
|
<span class="sr-only">Edit character</span>
|
||||||
</button>
|
</button>
|
||||||
<h3 class="skills-heading">@SelectedCharacter.Name <span
|
<h3 class="skills-heading">
|
||||||
class="muted">| Owner: @OwnerLabel(SelectedCharacter.OwnerUserId) | Campaign: @SelectedCampaign.Name</span>
|
@SelectedCharacter.Name
|
||||||
|
<span
|
||||||
|
class="muted">
|
||||||
|
| Owner: @OwnerLabel(SelectedCharacter.OwnerUserId) | Campaign: @SelectedCampaign.Name
|
||||||
|
</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="skill-filter-wrap">
|
<div class="skill-filter-wrap">
|
||||||
<label class="sr-only" for="skill-filter-input">Filter skills</label>
|
<label class="sr-only" for="skill-filter-input">Filter skills</label>
|
||||||
@@ -130,6 +134,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</article>
|
</article>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="character-panel-fill" aria-hidden="true"></div>
|
<div class="character-panel-fill" aria-hidden="true"></div>
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using RpgRoller.Contracts;
|
using RpgRoller.Contracts;
|
||||||
|
|
||||||
@@ -10,9 +9,7 @@ public partial class CharacterPanel
|
|||||||
{
|
{
|
||||||
private void OpenCreateSkillModal(Guid? skillGroupId = null)
|
private void OpenCreateSkillModal(Guid? skillGroupId = null)
|
||||||
{
|
{
|
||||||
var selectedGroup = skillGroupId.HasValue
|
var selectedGroup = skillGroupId.HasValue ? SelectedCharacterSkillGroups.FirstOrDefault(group => group.Id == skillGroupId.Value) : null;
|
||||||
? SelectedCharacterSkillGroups.FirstOrDefault(group => group.Id == skillGroupId.Value)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
CreateSkillInitialModel = new()
|
CreateSkillInitialModel = new()
|
||||||
{
|
{
|
||||||
@@ -156,9 +153,7 @@ public partial class CharacterPanel
|
|||||||
SkillGroupState.Errors["fumbleRange"] = "Open-ended Rolemaster groups require a fumble range.";
|
SkillGroupState.Errors["fumbleRange"] = "Open-ended Rolemaster groups require a fumble range.";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
|
||||||
SkillGroupState.Model.FumbleRange = null;
|
SkillGroupState.Model.FumbleRange = null;
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsD6Ruleset)
|
if (!IsD6Ruleset)
|
||||||
{
|
{
|
||||||
@@ -179,15 +174,7 @@ public partial class CharacterPanel
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var selectedCharacterId = SelectedCharacterId!.Value;
|
var selectedCharacterId = SelectedCharacterId!.Value;
|
||||||
var createdGroup = await ApiClient.RequestAsync<SkillGroupSummary>(
|
var createdGroup = await ApiClient.RequestAsync<SkillGroupSummary>("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));
|
||||||
"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();
|
CloseSkillGroupModals();
|
||||||
await SkillGroupCreated.InvokeAsync(createdGroup.Id);
|
await SkillGroupCreated.InvokeAsync(createdGroup.Id);
|
||||||
}
|
}
|
||||||
@@ -220,9 +207,7 @@ public partial class CharacterPanel
|
|||||||
SkillGroupState.Errors["fumbleRange"] = "Open-ended Rolemaster groups require a fumble range.";
|
SkillGroupState.Errors["fumbleRange"] = "Open-ended Rolemaster groups require a fumble range.";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
|
||||||
SkillGroupState.Model.FumbleRange = null;
|
SkillGroupState.Model.FumbleRange = null;
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsD6Ruleset)
|
if (!IsD6Ruleset)
|
||||||
{
|
{
|
||||||
@@ -243,15 +228,7 @@ public partial class CharacterPanel
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var editingSkillGroupId = EditingSkillGroupId!.Value;
|
var editingSkillGroupId = EditingSkillGroupId!.Value;
|
||||||
var updatedGroup = await ApiClient.RequestAsync<SkillGroupSummary>(
|
var updatedGroup = await ApiClient.RequestAsync<SkillGroupSummary>("PUT", $"/api/skill-groups/{editingSkillGroupId}", new UpdateSkillGroupRequest(SkillGroupState.Model.Name.Trim(), SkillGroupState.Model.DiceRollDefinition.Trim(), SkillGroupState.Model.WildDice, SkillGroupState.Model.AllowFumble, SkillGroupState.Model.FumbleRange));
|
||||||
"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();
|
CloseSkillGroupModals();
|
||||||
await SkillGroupUpdated.InvokeAsync(updatedGroup.Id);
|
await SkillGroupUpdated.InvokeAsync(updatedGroup.Id);
|
||||||
}
|
}
|
||||||
@@ -297,8 +274,7 @@ public partial class CharacterPanel
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
var filter = SkillFilterText.Trim();
|
var filter = SkillFilterText.Trim();
|
||||||
return skill.Name.Contains(filter, StringComparison.OrdinalIgnoreCase) ||
|
return skill.Name.Contains(filter, StringComparison.OrdinalIgnoreCase) || skill.DiceRollDefinition.Contains(filter, StringComparison.OrdinalIgnoreCase);
|
||||||
skill.DiceRollDefinition.Contains(filter, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string InitialsFor(string value)
|
private static string InitialsFor(string value)
|
||||||
@@ -340,9 +316,8 @@ public partial class CharacterPanel
|
|||||||
private bool IsD6Ruleset => RulesetFormHelpers.IsD6(SelectedCampaignRulesetId);
|
private bool IsD6Ruleset => RulesetFormHelpers.IsD6(SelectedCampaignRulesetId);
|
||||||
private bool IsRolemasterRuleset => RulesetFormHelpers.IsRolemaster(SelectedCampaignRulesetId);
|
private bool IsRolemasterRuleset => RulesetFormHelpers.IsRolemaster(SelectedCampaignRulesetId);
|
||||||
private bool IsSkillGroupRolemasterOpenEnded => RulesetFormHelpers.IsRolemasterOpenEndedExpression(SkillGroupState.Model.DiceRollDefinition);
|
private bool IsSkillGroupRolemasterOpenEnded => RulesetFormHelpers.IsRolemasterOpenEndedExpression(SkillGroupState.Model.DiceRollDefinition);
|
||||||
private string SkillGroupExpressionHelpText => IsRolemasterRuleset
|
|
||||||
? $"{RulesetFormHelpers.RolemasterExampleText()}. Negative modifiers are allowed."
|
private string SkillGroupExpressionHelpText => IsRolemasterRuleset ? $"{RulesetFormHelpers.RolemasterExampleText()}. Negative modifiers are allowed." : "Enter the default expression for skills created in this group.";
|
||||||
: "Enter the default expression for skills created in this group.";
|
|
||||||
|
|
||||||
private bool ShowCreateSkillModal { get; set; }
|
private bool ShowCreateSkillModal { get; set; }
|
||||||
private bool ShowEditSkillModal { get; set; }
|
private bool ShowEditSkillModal { get; set; }
|
||||||
|
|||||||
@@ -81,10 +81,7 @@ public partial class RollDiceStrip
|
|||||||
|
|
||||||
private static bool IsRolemasterDie(RollDieResult die)
|
private static bool IsRolemasterDie(RollDieResult die)
|
||||||
{
|
{
|
||||||
return die.Kind is RollDieKinds.RolemasterStandard or
|
return die.Kind is RollDieKinds.RolemasterStandard or RollDieKinds.RolemasterOpenEndedInitial or RollDieKinds.RolemasterOpenEndedHigh or RollDieKinds.RolemasterOpenEndedLowSubtract;
|
||||||
RollDieKinds.RolemasterOpenEndedInitial or
|
|
||||||
RollDieKinds.RolemasterOpenEndedHigh or
|
|
||||||
RollDieKinds.RolemasterOpenEndedLowSubtract;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string RollDieTitle(RollDieResult die)
|
private static string RollDieTitle(RollDieResult die)
|
||||||
|
|||||||
@@ -27,9 +27,7 @@ internal static class RulesetFormHelpers
|
|||||||
public static bool IsRolemasterOpenEndedExpression(string? expression)
|
public static bool IsRolemasterOpenEndedExpression(string? expression)
|
||||||
{
|
{
|
||||||
var parseResult = TryParseRolemasterExpression(expression);
|
var parseResult = TryParseRolemasterExpression(expression);
|
||||||
return parseResult.Succeeded &&
|
return parseResult.Succeeded && parseResult.Value is not null && parseResult.Value.Kind == DiceExpressionKind.RolemasterOpenEndedPercentile;
|
||||||
parseResult.Value is not null &&
|
|
||||||
parseResult.Value.Kind == DiceExpressionKind.RolemasterOpenEndedPercentile;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string DescribeRolemasterExpression(string expression, int? fumbleRange)
|
public static string DescribeRolemasterExpression(string expression, int? fumbleRange)
|
||||||
@@ -40,9 +38,7 @@ internal static class RulesetFormHelpers
|
|||||||
|
|
||||||
return parseResult.Value.Kind switch
|
return parseResult.Value.Kind switch
|
||||||
{
|
{
|
||||||
DiceExpressionKind.RolemasterOpenEndedPercentile => fumbleRange.HasValue
|
DiceExpressionKind.RolemasterOpenEndedPercentile => fumbleRange.HasValue ? $"Open-ended percentile: {parseResult.Value.Canonical}, fumble <= {fumbleRange.Value}" : $"Open-ended percentile: {parseResult.Value.Canonical}",
|
||||||
? $"Open-ended percentile: {parseResult.Value.Canonical}, fumble <= {fumbleRange.Value}"
|
|
||||||
: $"Open-ended percentile: {parseResult.Value.Canonical}",
|
|
||||||
_ => $"Rolemaster: {parseResult.Value.Canonical}"
|
_ => $"Rolemaster: {parseResult.Value.Canonical}"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
|
||||||
using RpgRoller.Contracts;
|
using RpgRoller.Contracts;
|
||||||
|
|
||||||
namespace RpgRoller.Components.Pages.HomeControls;
|
namespace RpgRoller.Components.Pages.HomeControls;
|
||||||
@@ -54,9 +53,7 @@ public partial class SkillFormModal
|
|||||||
FormState.Errors["fumbleRange"] = "Open-ended Rolemaster skills require a fumble range.";
|
FormState.Errors["fumbleRange"] = "Open-ended Rolemaster skills require a fumble range.";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
|
||||||
FormState.Model.FumbleRange = null;
|
FormState.Model.FumbleRange = null;
|
||||||
}
|
|
||||||
|
|
||||||
if (!IsD6Ruleset)
|
if (!IsD6Ruleset)
|
||||||
{
|
{
|
||||||
@@ -84,9 +81,7 @@ public partial class SkillFormModal
|
|||||||
{
|
{
|
||||||
SkillSummary skill;
|
SkillSummary skill;
|
||||||
if (EditingSkillId.HasValue)
|
if (EditingSkillId.HasValue)
|
||||||
{
|
|
||||||
skill = await ApiClient.RequestAsync<SkillSummary>("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));
|
skill = await ApiClient.RequestAsync<SkillSummary>("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
|
else
|
||||||
{
|
{
|
||||||
if (!SelectedCharacterId.HasValue)
|
if (!SelectedCharacterId.HasValue)
|
||||||
@@ -117,13 +112,6 @@ public partial class SkillFormModal
|
|||||||
NormalizeRolemasterFumbleRange();
|
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()
|
private void SynchronizeRulesetSpecificFields()
|
||||||
{
|
{
|
||||||
if (!IsRolemasterRuleset)
|
if (!IsRolemasterRuleset)
|
||||||
@@ -149,6 +137,12 @@ public partial class SkillFormModal
|
|||||||
FormState.Model.FumbleRange = null;
|
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]
|
[Inject]
|
||||||
private RpgRollerApiClient ApiClient { get; set; } = null!;
|
private RpgRollerApiClient ApiClient { get; set; } = null!;
|
||||||
|
|
||||||
|
|||||||
@@ -76,10 +76,12 @@
|
|||||||
</main>
|
</main>
|
||||||
<nav class="mobile-bottom-nav" aria-label="Play panel selector">
|
<nav class="mobile-bottom-nav" aria-label="Play panel selector">
|
||||||
<button type="button" class="switch @(State.MobilePanel == "character" ? "active" : string.Empty)"
|
<button type="button" class="switch @(State.MobilePanel == "character" ? "active" : string.Empty)"
|
||||||
@onclick='() => Scope.SetMobilePanelAsync("character")'>Character
|
@onclick='() => Scope.SetMobilePanelAsync("character")'>
|
||||||
|
Character
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="switch @(State.MobilePanel == "log" ? "active" : string.Empty)"
|
<button type="button" class="switch @(State.MobilePanel == "log" ? "active" : string.Empty)"
|
||||||
@onclick='() => Scope.SetMobilePanelAsync("log")'>Log
|
@onclick='() => Scope.SetMobilePanelAsync("log")'>
|
||||||
|
Log
|
||||||
</button>
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,10 +20,16 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
[JSInvokable]
|
[JSInvokable]
|
||||||
public Task OnStateEventReceived(CampaignStateSnapshot state) => Live.OnStateEventReceivedAsync(state);
|
public Task OnStateEventReceived(CampaignStateSnapshot state)
|
||||||
|
{
|
||||||
|
return Live.OnStateEventReceivedAsync(state);
|
||||||
|
}
|
||||||
|
|
||||||
[JSInvokable]
|
[JSInvokable]
|
||||||
public Task OnConnectionStateChanged(string state) => Live.OnConnectionStateChangedAsync(state);
|
public Task OnConnectionStateChanged(string state)
|
||||||
|
{
|
||||||
|
return Live.OnConnectionStateChangedAsync(state);
|
||||||
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
@@ -31,13 +37,25 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
DotNetRef?.Dispose();
|
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)
|
private async Task StartStateEventsCoreAsync(Guid campaignId)
|
||||||
{
|
{
|
||||||
@@ -86,76 +104,19 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
|
|
||||||
private WorkspaceState State { get; } = new();
|
private WorkspaceState State { get; } = new();
|
||||||
|
|
||||||
private WorkspaceCampaignScopeCoordinator Scope => m_Scope ??= 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));
|
||||||
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(
|
private WorkspaceLiveStateController Live => m_Live ??= new(State, Feedback, StartStateEventsCoreAsync, StopStateEventsCoreAsync, Scope.RefreshCampaignRosterAsync, Play.RefreshSelectedCharacterSheetAsync, Play.RefreshCampaignLogAsync, () => InvokeAsync(StateHasChanged));
|
||||||
State,
|
|
||||||
Feedback,
|
|
||||||
StartStateEventsCoreAsync,
|
|
||||||
StopStateEventsCoreAsync,
|
|
||||||
Scope.RefreshCampaignRosterAsync,
|
|
||||||
Play.RefreshSelectedCharacterSheetAsync,
|
|
||||||
Play.RefreshCampaignLogAsync,
|
|
||||||
() => InvokeAsync(StateHasChanged));
|
|
||||||
|
|
||||||
private WorkspacePlayCoordinator Play => m_Play ??= new(
|
private WorkspacePlayCoordinator Play => m_Play ??= new(State, Feedback, ApiClient, WorkspaceQuery, CanEditCharacter, () => InvokeAsync(StateHasChanged));
|
||||||
State,
|
|
||||||
Feedback,
|
|
||||||
ApiClient,
|
|
||||||
WorkspaceQuery,
|
|
||||||
CanEditCharacter,
|
|
||||||
() => InvokeAsync(StateHasChanged));
|
|
||||||
|
|
||||||
private WorkspaceCampaignCoordinator Campaigns => m_Campaigns ??= new(
|
private WorkspaceCampaignCoordinator Campaigns => m_Campaigns ??= new(State, Feedback, JS, ApiClient, Session.LoadKnownUsernamesAsync, Scope.ReloadCampaignsAsync, Scope.ReloadCharacterCampaignOptionsAsync, Scope.RefreshCampaignScopeAsync, Live.SyncStateEventsAsync);
|
||||||
State,
|
|
||||||
Feedback,
|
|
||||||
JS,
|
|
||||||
ApiClient,
|
|
||||||
Session.LoadKnownUsernamesAsync,
|
|
||||||
Scope.ReloadCampaignsAsync,
|
|
||||||
Scope.ReloadCharacterCampaignOptionsAsync,
|
|
||||||
Scope.RefreshCampaignScopeAsync,
|
|
||||||
Live.SyncStateEventsAsync);
|
|
||||||
|
|
||||||
private WorkspaceAdminCoordinator Admin => m_Admin ??= new(
|
private WorkspaceAdminCoordinator Admin => m_Admin ??= new(State, Feedback, JS, ApiClient, WorkspaceQuery, ClearAuthenticatedState, StopStateEventsAsync, message => LoggedOut.InvokeAsync(message));
|
||||||
State,
|
|
||||||
Feedback,
|
|
||||||
JS,
|
|
||||||
ApiClient,
|
|
||||||
WorkspaceQuery,
|
|
||||||
ClearAuthenticatedState,
|
|
||||||
StopStateEventsAsync,
|
|
||||||
message => LoggedOut.InvokeAsync(message));
|
|
||||||
|
|
||||||
private WorkspaceFeedbackService Feedback => m_Feedback ??= new(State, () => InvokeAsync(StateHasChanged));
|
private WorkspaceFeedbackService Feedback => m_Feedback ??= new(State, () => InvokeAsync(StateHasChanged));
|
||||||
|
|
||||||
private WorkspaceSessionCoordinator Session => m_Session ??= new(
|
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));
|
||||||
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<AppHeaderMenuItem> HeaderMenuItems
|
private IReadOnlyList<AppHeaderMenuItem> HeaderMenuItems
|
||||||
{
|
{
|
||||||
@@ -163,12 +124,27 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
{
|
{
|
||||||
var items = new List<AppHeaderMenuItem>
|
var items = new List<AppHeaderMenuItem>
|
||||||
{
|
{
|
||||||
new() { Label = "Play", IsActive = State.IsPlayScreen, OnSelected = () => Session.SwitchScreenAsync("play") },
|
new()
|
||||||
new() { Label = "Campaign Management", IsActive = State.IsManagementScreen, OnSelected = () => Session.SwitchScreenAsync("management") }
|
{
|
||||||
|
Label = "Play",
|
||||||
|
IsActive = State.IsPlayScreen,
|
||||||
|
OnSelected = () => Session.SwitchScreenAsync("play")
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Label = "Campaign Management",
|
||||||
|
IsActive = State.IsManagementScreen,
|
||||||
|
OnSelected = () => Session.SwitchScreenAsync("management")
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (State.IsCurrentUserAdmin)
|
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;
|
return items;
|
||||||
}
|
}
|
||||||
@@ -178,12 +154,12 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private DotNetObjectReference<Workspace>? DotNetRef { get; set; }
|
private DotNetObjectReference<Workspace>? DotNetRef { get; set; }
|
||||||
|
|
||||||
private const string ScreenAdmin = "admin";
|
private const string ScreenAdmin = "admin";
|
||||||
|
private WorkspaceAdminCoordinator? m_Admin;
|
||||||
private WorkspaceCampaignScopeCoordinator? m_Scope;
|
private WorkspaceCampaignCoordinator? m_Campaigns;
|
||||||
|
private WorkspaceFeedbackService? m_Feedback;
|
||||||
private WorkspaceLiveStateController? m_Live;
|
private WorkspaceLiveStateController? m_Live;
|
||||||
private WorkspacePlayCoordinator? m_Play;
|
private WorkspacePlayCoordinator? m_Play;
|
||||||
private WorkspaceCampaignCoordinator? m_Campaigns;
|
|
||||||
private WorkspaceAdminCoordinator? m_Admin;
|
private WorkspaceCampaignScopeCoordinator? m_Scope;
|
||||||
private WorkspaceFeedbackService? m_Feedback;
|
|
||||||
private WorkspaceSessionCoordinator? m_Session;
|
private WorkspaceSessionCoordinator? m_Session;
|
||||||
}
|
}
|
||||||
@@ -8,15 +8,7 @@ namespace RpgRoller.Components.Pages;
|
|||||||
[ExcludeFromCodeCoverage]
|
[ExcludeFromCodeCoverage]
|
||||||
public sealed class WorkspaceAdminCoordinator
|
public sealed class WorkspaceAdminCoordinator
|
||||||
{
|
{
|
||||||
public WorkspaceAdminCoordinator(
|
public WorkspaceAdminCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, IJSRuntime js, RpgRollerApiClient apiClient, WorkspaceQueryService workspaceQuery, Action clearAuthenticatedState, Func<Task> stopStateEventsAsync, Func<string?, Task> onLoggedOutAsync)
|
||||||
WorkspaceState state,
|
|
||||||
WorkspaceFeedbackService feedback,
|
|
||||||
IJSRuntime js,
|
|
||||||
RpgRollerApiClient apiClient,
|
|
||||||
WorkspaceQueryService workspaceQuery,
|
|
||||||
Action clearAuthenticatedState,
|
|
||||||
Func<Task> stopStateEventsAsync,
|
|
||||||
Func<string?, Task> onLoggedOutAsync)
|
|
||||||
{
|
{
|
||||||
m_State = state;
|
m_State = state;
|
||||||
m_Feedback = feedback;
|
m_Feedback = feedback;
|
||||||
@@ -63,10 +55,7 @@ public sealed class WorkspaceAdminCoordinator
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
IReadOnlyList<string> roles = HasAdminRole(user) ? Array.Empty<string>() : [UserRoles.Admin];
|
IReadOnlyList<string> roles = HasAdminRole(user) ? Array.Empty<string>() : [UserRoles.Admin];
|
||||||
_ = await m_ApiClient.RequestAsync<AdminUserSummary>(
|
_ = await m_ApiClient.RequestAsync<AdminUserSummary>("PUT", $"/api/admin/users/{user.Id}/roles", new UpdateUserRolesRequest(roles));
|
||||||
"PUT",
|
|
||||||
$"/api/admin/users/{user.Id}/roles",
|
|
||||||
new UpdateUserRolesRequest(roles));
|
|
||||||
|
|
||||||
await ReloadAdminUsersAsync();
|
await ReloadAdminUsersAsync();
|
||||||
m_Feedback.SetStatus("User roles updated.", false);
|
m_Feedback.SetStatus("User roles updated.", false);
|
||||||
@@ -109,9 +98,7 @@ public sealed class WorkspaceAdminCoordinator
|
|||||||
|
|
||||||
private async Task ReloadAdminUsersAsync()
|
private async Task ReloadAdminUsersAsync()
|
||||||
{
|
{
|
||||||
m_State.AdminUsers = (await m_WorkspaceQuery.GetAdminUsersAsync())
|
m_State.AdminUsers = (await m_WorkspaceQuery.GetAdminUsersAsync()).OrderBy(user => user.Username, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
.OrderBy(user => user.Username, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
m_State.HasLoadedAdminUsers = true;
|
m_State.HasLoadedAdminUsers = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,7 @@ namespace RpgRoller.Components.Pages;
|
|||||||
[ExcludeFromCodeCoverage]
|
[ExcludeFromCodeCoverage]
|
||||||
public sealed class WorkspaceCampaignCoordinator
|
public sealed class WorkspaceCampaignCoordinator
|
||||||
{
|
{
|
||||||
public WorkspaceCampaignCoordinator(
|
public WorkspaceCampaignCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, IJSRuntime js, RpgRollerApiClient apiClient, Func<Task> loadKnownUsernamesAsync, Func<Guid?, Task> reloadCampaignsAsync, Func<Task> reloadCharacterCampaignOptionsAsync, Func<Task> refreshCampaignScopeAsync, Func<Task> syncStateEventsAsync)
|
||||||
WorkspaceState state,
|
|
||||||
WorkspaceFeedbackService feedback,
|
|
||||||
IJSRuntime js,
|
|
||||||
RpgRollerApiClient apiClient,
|
|
||||||
Func<Task> loadKnownUsernamesAsync,
|
|
||||||
Func<Guid?, Task> reloadCampaignsAsync,
|
|
||||||
Func<Task> reloadCharacterCampaignOptionsAsync,
|
|
||||||
Func<Task> refreshCampaignScopeAsync,
|
|
||||||
Func<Task> syncStateEventsAsync)
|
|
||||||
{
|
{
|
||||||
m_State = state;
|
m_State = state;
|
||||||
m_Feedback = feedback;
|
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);
|
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 RpgRollerApiClient m_ApiClient;
|
||||||
private readonly WorkspaceFeedbackService m_Feedback;
|
private readonly WorkspaceFeedbackService m_Feedback;
|
||||||
private readonly IJSRuntime m_JS;
|
private readonly IJSRuntime m_JS;
|
||||||
private readonly Func<Task> m_LoadKnownUsernamesAsync;
|
private readonly Func<Task> m_LoadKnownUsernamesAsync;
|
||||||
private readonly Func<Task> m_ReloadCharacterCampaignOptionsAsync;
|
|
||||||
private readonly Func<Guid?, Task> m_ReloadCampaignsAsync;
|
|
||||||
private readonly Func<Task> m_RefreshCampaignScopeAsync;
|
private readonly Func<Task> m_RefreshCampaignScopeAsync;
|
||||||
|
private readonly Func<Guid?, Task> m_ReloadCampaignsAsync;
|
||||||
|
private readonly Func<Task> m_ReloadCharacterCampaignOptionsAsync;
|
||||||
private readonly WorkspaceState m_State;
|
private readonly WorkspaceState m_State;
|
||||||
private readonly Func<Task> m_SyncStateEventsAsync;
|
private readonly Func<Task> m_SyncStateEventsAsync;
|
||||||
|
|
||||||
private const string CampaignSessionKey = "campaign";
|
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,12 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
using RpgRoller.Contracts;
|
|
||||||
|
|
||||||
namespace RpgRoller.Components.Pages;
|
namespace RpgRoller.Components.Pages;
|
||||||
|
|
||||||
[ExcludeFromCodeCoverage]
|
[ExcludeFromCodeCoverage]
|
||||||
public sealed class WorkspaceCampaignScopeCoordinator
|
public sealed class WorkspaceCampaignScopeCoordinator
|
||||||
{
|
{
|
||||||
public WorkspaceCampaignScopeCoordinator(
|
public WorkspaceCampaignScopeCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, IJSRuntime js, WorkspaceQueryService workspaceQuery, Func<Task> ensureSelectedCharacterActiveAsync, Func<Task> refreshSelectedCharacterSheetAsync, Func<Guid?, Task> refreshCampaignLogAsync, Action resetCampaignLogDetailState, Action resetCampaignStateTracking, Action clearAuthenticatedState, Func<Task> stopStateEventsAsync, Func<string?, Task> onLoggedOutAsync)
|
||||||
WorkspaceState state,
|
|
||||||
WorkspaceFeedbackService feedback,
|
|
||||||
IJSRuntime js,
|
|
||||||
WorkspaceQueryService workspaceQuery,
|
|
||||||
Func<Task> ensureSelectedCharacterActiveAsync,
|
|
||||||
Func<Task> refreshSelectedCharacterSheetAsync,
|
|
||||||
Func<Guid?, Task> refreshCampaignLogAsync,
|
|
||||||
Action resetCampaignLogDetailState,
|
|
||||||
Action resetCampaignStateTracking,
|
|
||||||
Action clearAuthenticatedState,
|
|
||||||
Func<Task> stopStateEventsAsync,
|
|
||||||
Func<string?, Task> onLoggedOutAsync)
|
|
||||||
{
|
{
|
||||||
m_State = state;
|
m_State = state;
|
||||||
m_Feedback = feedback;
|
m_Feedback = feedback;
|
||||||
@@ -147,6 +134,9 @@ public sealed class WorkspaceCampaignScopeCoordinator
|
|||||||
m_State.SelectedCharacterId = m_State.SelectedCampaign.Characters[0].Id;
|
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 Action m_ClearAuthenticatedState;
|
||||||
private readonly Func<Task> m_EnsureSelectedCharacterActiveAsync;
|
private readonly Func<Task> m_EnsureSelectedCharacterActiveAsync;
|
||||||
private readonly WorkspaceFeedbackService m_Feedback;
|
private readonly WorkspaceFeedbackService m_Feedback;
|
||||||
@@ -159,7 +149,4 @@ public sealed class WorkspaceCampaignScopeCoordinator
|
|||||||
private readonly WorkspaceState m_State;
|
private readonly WorkspaceState m_State;
|
||||||
private readonly Func<Task> m_StopStateEventsAsync;
|
private readonly Func<Task> m_StopStateEventsAsync;
|
||||||
private readonly WorkspaceQueryService m_WorkspaceQuery;
|
private readonly WorkspaceQueryService m_WorkspaceQuery;
|
||||||
|
|
||||||
private const string CampaignSessionKey = "campaign";
|
|
||||||
private const string MobilePanelSessionKey = "play-panel";
|
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ public sealed class WorkspaceFeedbackService
|
|||||||
private void AddToast(string message, bool isError)
|
private void AddToast(string message, bool isError)
|
||||||
{
|
{
|
||||||
var toastId = Guid.NewGuid();
|
var toastId = Guid.NewGuid();
|
||||||
m_State.Toasts.Add(new WorkspaceToast(toastId, message, isError));
|
m_State.Toasts.Add(new(toastId, message, isError));
|
||||||
_ = DismissToastLaterAsync(toastId);
|
_ = DismissToastLaterAsync(toastId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,8 +47,8 @@ public sealed class WorkspaceFeedbackService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const int ToastDurationMs = 3200;
|
||||||
|
|
||||||
private readonly Func<Task> m_RequestRefreshAsync;
|
private readonly Func<Task> m_RequestRefreshAsync;
|
||||||
private readonly WorkspaceState m_State;
|
private readonly WorkspaceState m_State;
|
||||||
|
|
||||||
private const int ToastDurationMs = 3200;
|
|
||||||
}
|
}
|
||||||
@@ -6,15 +6,7 @@ namespace RpgRoller.Components.Pages;
|
|||||||
[ExcludeFromCodeCoverage]
|
[ExcludeFromCodeCoverage]
|
||||||
public sealed class WorkspaceLiveStateController
|
public sealed class WorkspaceLiveStateController
|
||||||
{
|
{
|
||||||
public WorkspaceLiveStateController(
|
public WorkspaceLiveStateController(WorkspaceState state, WorkspaceFeedbackService feedback, Func<Guid, Task> startStateEventsAsync, Func<Task> stopStateEventsCoreAsync, Func<Task> refreshCampaignRosterAsync, Func<Task> refreshSelectedCharacterSheetAsync, Func<Guid?, Task> refreshCampaignLogAsync, Func<Task> requestRefreshAsync)
|
||||||
WorkspaceState state,
|
|
||||||
WorkspaceFeedbackService feedback,
|
|
||||||
Func<Guid, Task> startStateEventsAsync,
|
|
||||||
Func<Task> stopStateEventsCoreAsync,
|
|
||||||
Func<Task> refreshCampaignRosterAsync,
|
|
||||||
Func<Task> refreshSelectedCharacterSheetAsync,
|
|
||||||
Func<Guid?, Task> refreshCampaignLogAsync,
|
|
||||||
Func<Task> requestRefreshAsync)
|
|
||||||
{
|
{
|
||||||
m_State = state;
|
m_State = state;
|
||||||
m_Feedback = feedback;
|
m_Feedback = feedback;
|
||||||
@@ -53,9 +45,7 @@ public sealed class WorkspaceLiveStateController
|
|||||||
await m_RefreshCampaignRosterAsync();
|
await m_RefreshCampaignRosterAsync();
|
||||||
|
|
||||||
var selectedCharacterChanged = previousSelectedCharacterId != m_State.SelectedCharacterId;
|
var selectedCharacterChanged = previousSelectedCharacterId != m_State.SelectedCharacterId;
|
||||||
var selectedCharacterVersionChanged = m_State.IsPlayScreen &&
|
var selectedCharacterVersionChanged = m_State.IsPlayScreen && !selectedCharacterChanged && GetCharacterVersion(state, m_State.SelectedCharacterId) != previousSelectedCharacterVersion;
|
||||||
!selectedCharacterChanged &&
|
|
||||||
GetCharacterVersion(state, m_State.SelectedCharacterId) != previousSelectedCharacterVersion;
|
|
||||||
|
|
||||||
if (m_State.IsPlayScreen && (selectedCharacterChanged || selectedCharacterVersionChanged))
|
if (m_State.IsPlayScreen && (selectedCharacterChanged || selectedCharacterVersionChanged))
|
||||||
await m_RefreshSelectedCharacterSheetAsync();
|
await m_RefreshSelectedCharacterSheetAsync();
|
||||||
@@ -116,9 +106,7 @@ public sealed class WorkspaceLiveStateController
|
|||||||
if (!characterId.HasValue)
|
if (!characterId.HasValue)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
return snapshot.CharacterVersions
|
return snapshot.CharacterVersions.FirstOrDefault(version => version.CharacterId == characterId.Value)?.Version ?? 0;
|
||||||
.FirstOrDefault(version => version.CharacterId == characterId.Value)
|
|
||||||
?.Version ?? 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly WorkspaceFeedbackService m_Feedback;
|
private readonly WorkspaceFeedbackService m_Feedback;
|
||||||
|
|||||||
@@ -6,13 +6,7 @@ namespace RpgRoller.Components.Pages;
|
|||||||
[ExcludeFromCodeCoverage]
|
[ExcludeFromCodeCoverage]
|
||||||
public sealed class WorkspacePlayCoordinator
|
public sealed class WorkspacePlayCoordinator
|
||||||
{
|
{
|
||||||
public WorkspacePlayCoordinator(
|
public WorkspacePlayCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, RpgRollerApiClient apiClient, WorkspaceQueryService workspaceQuery, Func<CharacterSummary, bool> canEditCharacter, Func<Task> requestRefreshAsync)
|
||||||
WorkspaceState state,
|
|
||||||
WorkspaceFeedbackService feedback,
|
|
||||||
RpgRollerApiClient apiClient,
|
|
||||||
WorkspaceQueryService workspaceQuery,
|
|
||||||
Func<CharacterSummary, bool> canEditCharacter,
|
|
||||||
Func<Task> requestRefreshAsync)
|
|
||||||
{
|
{
|
||||||
m_State = state;
|
m_State = state;
|
||||||
m_Feedback = feedback;
|
m_Feedback = feedback;
|
||||||
@@ -36,9 +30,7 @@ public sealed class WorkspacePlayCoordinator
|
|||||||
var page = await m_WorkspaceQuery.GetCampaignLogPageAsync(m_State.SelectedCampaignId.Value, afterRollId, CampaignLogWindowSize);
|
var page = await m_WorkspaceQuery.GetCampaignLogPageAsync(m_State.SelectedCampaignId.Value, afterRollId, CampaignLogWindowSize);
|
||||||
Guid? newestRollId = null;
|
Guid? newestRollId = null;
|
||||||
if (!afterRollId.HasValue || page.ResetRequired)
|
if (!afterRollId.HasValue || page.ResetRequired)
|
||||||
{
|
|
||||||
m_State.CampaignLog = page.Entries.ToList();
|
m_State.CampaignLog = page.Entries.ToList();
|
||||||
}
|
|
||||||
else if (page.Entries.Length > 0)
|
else if (page.Entries.Length > 0)
|
||||||
{
|
{
|
||||||
m_State.CampaignLog.AddRange(page.Entries);
|
m_State.CampaignLog.AddRange(page.Entries);
|
||||||
@@ -47,14 +39,8 @@ public sealed class WorkspacePlayCoordinator
|
|||||||
}
|
}
|
||||||
|
|
||||||
var shouldAutoExpandNewest = afterRollId.HasValue && page.Entries.Length > 0;
|
var shouldAutoExpandNewest = afterRollId.HasValue && page.Entries.Length > 0;
|
||||||
if (!shouldAutoExpandNewest &&
|
if (!shouldAutoExpandNewest && !afterRollId.HasValue && m_State.CurrentCampaignState is not null && previousLogCount == 0 && page.Entries.Length > 0)
|
||||||
!afterRollId.HasValue &&
|
|
||||||
m_State.CurrentCampaignState is not null &&
|
|
||||||
previousLogCount == 0 &&
|
|
||||||
page.Entries.Length > 0)
|
|
||||||
{
|
|
||||||
shouldAutoExpandNewest = true;
|
shouldAutoExpandNewest = true;
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldAutoExpandNewest)
|
if (shouldAutoExpandNewest)
|
||||||
{
|
{
|
||||||
@@ -63,9 +49,7 @@ public sealed class WorkspacePlayCoordinator
|
|||||||
m_State.FreshCampaignLogRollId = newestRollId;
|
m_State.FreshCampaignLogRollId = newestRollId;
|
||||||
}
|
}
|
||||||
else if (!afterRollId.HasValue)
|
else if (!afterRollId.HasValue)
|
||||||
{
|
|
||||||
m_State.FreshCampaignLogRollId = null;
|
m_State.FreshCampaignLogRollId = null;
|
||||||
}
|
|
||||||
|
|
||||||
m_State.CampaignLogCursor = page.Cursor ?? afterRollId;
|
m_State.CampaignLogCursor = page.Cursor ?? afterRollId;
|
||||||
TrimCampaignLogDetails();
|
TrimCampaignLogDetails();
|
||||||
@@ -91,12 +75,8 @@ public sealed class WorkspacePlayCoordinator
|
|||||||
}
|
}
|
||||||
|
|
||||||
var sheet = await m_WorkspaceQuery.GetCharacterSheetAsync(m_State.SelectedCharacterId.Value);
|
var sheet = await m_WorkspaceQuery.GetCharacterSheetAsync(m_State.SelectedCharacterId.Value);
|
||||||
m_State.SelectedCharacterSkillGroups = sheet.SkillGroups
|
m_State.SelectedCharacterSkillGroups = sheet.SkillGroups.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase)
|
m_State.SelectedCharacterSkills = sheet.Skills.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
.ToList();
|
|
||||||
m_State.SelectedCharacterSkills = sheet.Skills
|
|
||||||
.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task EnsureSelectedCharacterActiveAsync()
|
public Task EnsureSelectedCharacterActiveAsync()
|
||||||
@@ -338,15 +318,15 @@ public sealed class WorkspacePlayCoordinator
|
|||||||
|
|
||||||
private static CampaignRollDetail ToCampaignRollDetail(RollResult roll)
|
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 RpgRollerApiClient m_ApiClient;
|
||||||
private readonly Func<CharacterSummary, bool> m_CanEditCharacter;
|
private readonly Func<CharacterSummary, bool> m_CanEditCharacter;
|
||||||
private readonly WorkspaceFeedbackService m_Feedback;
|
private readonly WorkspaceFeedbackService m_Feedback;
|
||||||
private readonly Func<Task> m_RequestRefreshAsync;
|
private readonly Func<Task> m_RequestRefreshAsync;
|
||||||
private readonly WorkspaceState m_State;
|
private readonly WorkspaceState m_State;
|
||||||
private readonly WorkspaceQueryService m_WorkspaceQuery;
|
private readonly WorkspaceQueryService m_WorkspaceQuery;
|
||||||
|
|
||||||
private const int CampaignLogWindowSize = 25;
|
|
||||||
}
|
}
|
||||||
@@ -5,21 +5,7 @@ namespace RpgRoller.Components.Pages;
|
|||||||
|
|
||||||
public sealed class WorkspaceSessionCoordinator
|
public sealed class WorkspaceSessionCoordinator
|
||||||
{
|
{
|
||||||
public WorkspaceSessionCoordinator(
|
public WorkspaceSessionCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, IJSRuntime js, RpgRollerApiClient apiClient, WorkspaceQueryService workspaceQuery, Func<Guid?, Task> reloadCampaignsAsync, Func<Task> reloadCharacterCampaignOptionsAsync, Func<Task> refreshCampaignScopeAsync, Func<Task> syncStateEventsAsync, Func<Task> stopStateEventsAsync, Func<Task> ensureAdminUsersLoadedAsync, Action resetCampaignLogDetailState, Func<Task> requestRefreshAsync, Func<string?, Task> onLoggedOutAsync)
|
||||||
WorkspaceState state,
|
|
||||||
WorkspaceFeedbackService feedback,
|
|
||||||
IJSRuntime js,
|
|
||||||
RpgRollerApiClient apiClient,
|
|
||||||
WorkspaceQueryService workspaceQuery,
|
|
||||||
Func<Guid?, Task> reloadCampaignsAsync,
|
|
||||||
Func<Task> reloadCharacterCampaignOptionsAsync,
|
|
||||||
Func<Task> refreshCampaignScopeAsync,
|
|
||||||
Func<Task> syncStateEventsAsync,
|
|
||||||
Func<Task> stopStateEventsAsync,
|
|
||||||
Func<Task> ensureAdminUsersLoadedAsync,
|
|
||||||
Action resetCampaignLogDetailState,
|
|
||||||
Func<Task> requestRefreshAsync,
|
|
||||||
Func<string?, Task> onLoggedOutAsync)
|
|
||||||
{
|
{
|
||||||
m_State = state;
|
m_State = state;
|
||||||
m_Feedback = feedback;
|
m_Feedback = feedback;
|
||||||
@@ -278,21 +264,6 @@ public sealed class WorkspaceSessionCoordinator
|
|||||||
return exception.Message.Contains("JavaScript interop calls cannot be issued", StringComparison.OrdinalIgnoreCase);
|
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<Task> m_EnsureAdminUsersLoadedAsync;
|
|
||||||
private readonly IJSRuntime m_JS;
|
|
||||||
private readonly Func<string?, Task> m_OnLoggedOutAsync;
|
|
||||||
private readonly Func<Task> m_ReloadCharacterCampaignOptionsAsync;
|
|
||||||
private readonly Func<Guid?, Task> m_ReloadCampaignsAsync;
|
|
||||||
private readonly Action m_ResetCampaignLogDetailState;
|
|
||||||
private readonly Func<Task> m_RefreshCampaignScopeAsync;
|
|
||||||
private readonly Func<Task> m_RequestRefreshAsync;
|
|
||||||
private readonly WorkspaceState m_State;
|
|
||||||
private readonly Func<Task> m_StopStateEventsAsync;
|
|
||||||
private readonly Func<Task> m_SyncStateEventsAsync;
|
|
||||||
private readonly WorkspaceQueryService m_WorkspaceQuery;
|
|
||||||
|
|
||||||
private const string ScreenPlay = "play";
|
private const string ScreenPlay = "play";
|
||||||
private const string ScreenManagement = "management";
|
private const string ScreenManagement = "management";
|
||||||
private const string ScreenAdmin = "admin";
|
private const string ScreenAdmin = "admin";
|
||||||
@@ -300,4 +271,19 @@ public sealed class WorkspaceSessionCoordinator
|
|||||||
private const string CampaignSessionKey = "campaign";
|
private const string CampaignSessionKey = "campaign";
|
||||||
private const string MobilePanelSessionKey = "play-panel";
|
private const string MobilePanelSessionKey = "play-panel";
|
||||||
private const string RollVisibilitySessionKey = "roll-visibility";
|
private const string RollVisibilitySessionKey = "roll-visibility";
|
||||||
|
|
||||||
|
private readonly RpgRollerApiClient m_ApiClient;
|
||||||
|
private readonly Func<Task> m_EnsureAdminUsersLoadedAsync;
|
||||||
|
private readonly WorkspaceFeedbackService m_Feedback;
|
||||||
|
private readonly IJSRuntime m_JS;
|
||||||
|
private readonly Func<string?, Task> m_OnLoggedOutAsync;
|
||||||
|
private readonly Func<Task> m_RefreshCampaignScopeAsync;
|
||||||
|
private readonly Func<Guid?, Task> m_ReloadCampaignsAsync;
|
||||||
|
private readonly Func<Task> m_ReloadCharacterCampaignOptionsAsync;
|
||||||
|
private readonly Func<Task> m_RequestRefreshAsync;
|
||||||
|
private readonly Action m_ResetCampaignLogDetailState;
|
||||||
|
private readonly WorkspaceState m_State;
|
||||||
|
private readonly Func<Task> m_StopStateEventsAsync;
|
||||||
|
private readonly Func<Task> m_SyncStateEventsAsync;
|
||||||
|
private readonly WorkspaceQueryService m_WorkspaceQuery;
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,41 @@
|
|||||||
|
using RpgRoller.Components.Pages.HomeControls;
|
||||||
using RpgRoller.Contracts;
|
using RpgRoller.Contracts;
|
||||||
using RpgRoller.Domain;
|
using RpgRoller.Domain;
|
||||||
using RpgRoller.Components.Pages.HomeControls;
|
|
||||||
|
|
||||||
namespace RpgRoller.Components.Pages;
|
namespace RpgRoller.Components.Pages;
|
||||||
|
|
||||||
public sealed class WorkspaceState
|
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 UserSummary? User { get; set; }
|
||||||
public Guid? ActiveCharacterId { get; set; }
|
public Guid? ActiveCharacterId { get; set; }
|
||||||
public Guid? SelectedCampaignId { get; set; }
|
public Guid? SelectedCampaignId { get; set; }
|
||||||
@@ -66,18 +96,11 @@ public sealed class WorkspaceState
|
|||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (User is 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
|
var ownedCharacters = SelectedCampaign.Characters.Where(character => character.OwnerUserId == User.Id).ToArray();
|
||||||
.Where(character => character.OwnerUserId == User.Id)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return new CampaignRoster(
|
return new(SelectedCampaign.Id, SelectedCampaign.Name, SelectedCampaign.RulesetId, SelectedCampaign.Gm, ownedCharacters);
|
||||||
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 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}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,7 @@ public sealed class WorkspaceQueryService
|
|||||||
private static ApiRequestException ToApiRequestException(ServiceError error)
|
private static ApiRequestException ToApiRequestException(ServiceError error)
|
||||||
{
|
{
|
||||||
var statusCode = error.Code == "unauthorized" ? 401 : 400;
|
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 IGameService m_GameService;
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ public sealed class WorkspaceSessionTokenAccessor
|
|||||||
if (httpContext is null)
|
if (httpContext is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (httpContext.Items.TryGetValue(SessionTokenItemKey, out var storedToken) &&
|
if (httpContext.Items.TryGetValue(SessionTokenItemKey, out var storedToken) && storedToken is string sessionToken && !string.IsNullOrWhiteSpace(sessionToken))
|
||||||
storedToken is string sessionToken &&
|
|
||||||
!string.IsNullOrWhiteSpace(sessionToken))
|
|
||||||
{
|
{
|
||||||
m_SessionToken = sessionToken;
|
m_SessionToken = sessionToken;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -115,7 +115,8 @@ public sealed record CampaignLogListEntry(
|
|||||||
string VisibilityStyle,
|
string VisibilityStyle,
|
||||||
int Result,
|
int Result,
|
||||||
string SummaryText,
|
string SummaryText,
|
||||||
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] string[]? EventBadges,
|
[property: JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
string[]? EventBadges,
|
||||||
DateTimeOffset TimestampUtc);
|
DateTimeOffset TimestampUtc);
|
||||||
|
|
||||||
public sealed record CampaignRollDetail(Guid RollId, string Breakdown, RollDieResult[] Dice);
|
public sealed record CampaignRollDetail(Guid RollId, string Breakdown, RollDieResult[] Dice);
|
||||||
|
|||||||
@@ -35,12 +35,7 @@ public static class CampaignLogSummaryBuilder
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
case RulesetKind.Rolemaster:
|
case RulesetKind.Rolemaster:
|
||||||
AddBadgeIfMissing(
|
AddBadgeIfMissing(badges, dice.Any(die => string.Equals(die.Kind, RollDieKinds.RolemasterOpenEndedInitial, StringComparison.Ordinal) && !die.SignedContribution.HasValue), "rf");
|
||||||
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 == 100), "r100");
|
||||||
AddBadgeIfMissing(badges, dice.Any(die => die.Roll == 66), "r66");
|
AddBadgeIfMissing(badges, dice.Any(die => die.Roll == 66), "r66");
|
||||||
break;
|
break;
|
||||||
@@ -63,17 +58,11 @@ public static class CampaignLogSummaryBuilder
|
|||||||
var openEndedInitial = dice.FirstOrDefault(die => string.Equals(die.Kind, RollDieKinds.RolemasterOpenEndedInitial, StringComparison.Ordinal));
|
var openEndedInitial = dice.FirstOrDefault(die => string.Equals(die.Kind, RollDieKinds.RolemasterOpenEndedInitial, StringComparison.Ordinal));
|
||||||
if (openEndedInitial is not null)
|
if (openEndedInitial is not null)
|
||||||
{
|
{
|
||||||
var highFollowUps = dice
|
var highFollowUps = dice.Where(die => string.Equals(die.Kind, RollDieKinds.RolemasterOpenEndedHigh, StringComparison.Ordinal)).Select(die => die.Roll.ToString()).ToArray();
|
||||||
.Where(die => string.Equals(die.Kind, RollDieKinds.RolemasterOpenEndedHigh, StringComparison.Ordinal))
|
|
||||||
.Select(die => die.Roll.ToString())
|
|
||||||
.ToArray();
|
|
||||||
if (highFollowUps.Length > 0)
|
if (highFollowUps.Length > 0)
|
||||||
return $"{openEndedInitial.Roll} + {string.Join(" + ", highFollowUps)} | open-ended high";
|
return $"{openEndedInitial.Roll} + {string.Join(" + ", highFollowUps)} | open-ended high";
|
||||||
|
|
||||||
var lowFollowUps = dice
|
var lowFollowUps = dice.Where(die => string.Equals(die.Kind, RollDieKinds.RolemasterOpenEndedLowSubtract, StringComparison.Ordinal)).Select(die => die.Roll.ToString()).ToArray();
|
||||||
.Where(die => string.Equals(die.Kind, RollDieKinds.RolemasterOpenEndedLowSubtract, StringComparison.Ordinal))
|
|
||||||
.Select(die => die.Roll.ToString())
|
|
||||||
.ToArray();
|
|
||||||
if (lowFollowUps.Length > 0)
|
if (lowFollowUps.Length > 0)
|
||||||
return $"({RollBreakdownFormatter.FormatRolemasterTriggerRoll(openEndedInitial.Roll)}) {string.Join(" ", lowFollowUps.Select(roll => $"-{roll}"))} | open-ended low";
|
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)
|
private static bool IsRolemasterDieKind(string? kind)
|
||||||
{
|
{
|
||||||
return kind is RollDieKinds.RolemasterStandard or
|
return kind is RollDieKinds.RolemasterStandard or RollDieKinds.RolemasterOpenEndedInitial or RollDieKinds.RolemasterOpenEndedHigh or RollDieKinds.RolemasterOpenEndedLowSubtract;
|
||||||
RollDieKinds.RolemasterOpenEndedInitial or
|
|
||||||
RollDieKinds.RolemasterOpenEndedHigh or
|
|
||||||
RollDieKinds.RolemasterOpenEndedLowSubtract;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddBadgeIfMissing(List<string> badges, bool condition, string code)
|
private static void AddBadgeIfMissing(List<string> badges, bool condition, string code)
|
||||||
@@ -108,8 +94,6 @@ public static class CampaignLogSummaryBuilder
|
|||||||
private static bool IsSingleD20Expression(string expression)
|
private static bool IsSingleD20Expression(string expression)
|
||||||
{
|
{
|
||||||
var parsedExpression = DiceRules.ParseExpression(RulesetKind.Dnd5e, expression);
|
var parsedExpression = DiceRules.ParseExpression(RulesetKind.Dnd5e, expression);
|
||||||
return parsedExpression.Succeeded &&
|
return parsedExpression.Succeeded && parsedExpression.Value!.DiceCount == 1 && parsedExpression.Value.Sides == 20;
|
||||||
parsedExpression.Value!.DiceCount == 1 &&
|
|
||||||
parsedExpression.Value.Sides == 20;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,9 +4,6 @@ namespace RpgRoller.Services;
|
|||||||
|
|
||||||
public static class CustomRollOptionsResolver
|
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)
|
public static (int WildDice, bool AllowFumble, int? FumbleRange) Resolve(RulesetKind ruleset)
|
||||||
{
|
{
|
||||||
return ruleset switch
|
return ruleset switch
|
||||||
@@ -15,4 +12,7 @@ public static class CustomRollOptionsResolver
|
|||||||
_ => (0, false, null)
|
_ => (0, false, null)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const int DefaultCustomD6WildDice = 1;
|
||||||
|
private const bool DefaultCustomD6AllowFumble = true;
|
||||||
}
|
}
|
||||||
@@ -86,16 +86,14 @@ public static partial class DiceRules
|
|||||||
var diceCount = string.IsNullOrEmpty(countValue) ? 1 : int.Parse(countValue);
|
var diceCount = string.IsNullOrEmpty(countValue) ? 1 : int.Parse(countValue);
|
||||||
var sides = int.Parse(match.Groups["sides"].Value);
|
var sides = int.Parse(match.Groups["sides"].Value);
|
||||||
var modifier = ParseModifier(match.Groups["modifier"].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)
|
if (!validation.Succeeded)
|
||||||
return ServiceResult<DiceExpression>.Failure(validation.Error!.Code, validation.Error.Message);
|
return ServiceResult<DiceExpression>.Failure(validation.Error!.Code, validation.Error.Message);
|
||||||
|
|
||||||
var isOpenEnded = match.Groups["openEnded"].Success;
|
var isOpenEnded = match.Groups["openEnded"].Success;
|
||||||
if (isOpenEnded && (diceCount != 1 || sides != 100))
|
if (isOpenEnded && (diceCount != 1 || sides != 100))
|
||||||
{
|
{
|
||||||
return ServiceResult<DiceExpression>.Failure(
|
return ServiceResult<DiceExpression>.Failure("invalid_expression", "Open-ended Rolemaster rolls must use d100! with an implicit or explicit dice count of 1.");
|
||||||
"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();
|
var countPrefix = diceCount == 1 ? string.Empty : diceCount.ToString();
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ public static class GameAuthorization
|
|||||||
if (campaign.GmUserId == actorUserId)
|
if (campaign.GmUserId == actorUserId)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return stateStore.CharactersById.Values.Any(character =>
|
return stateStore.CharactersById.Values.Any(character => character.CampaignId == campaignId && character.OwnerUserId == actorUserId);
|
||||||
character.CampaignId == campaignId &&
|
|
||||||
character.OwnerUserId == actorUserId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool CanEditCharacter(Guid actorUserId, Character character, Campaign campaign)
|
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)
|
public static bool CanViewRoll(GameStateStore stateStore, Guid actorUserId, Campaign campaign, RollLogEntry entry)
|
||||||
{
|
{
|
||||||
return CanViewCampaign(stateStore, actorUserId, campaign.Id) &&
|
return CanViewCampaign(stateStore, actorUserId, campaign.Id) && (entry.Visibility == RollVisibility.Public || entry.RollerUserId == actorUserId || campaign.GmUserId == actorUserId);
|
||||||
(entry.Visibility == RollVisibility.Public || entry.RollerUserId == actorUserId || campaign.GmUserId == actorUserId);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,11 +49,7 @@ public sealed class GameCampaignService
|
|||||||
if (user is null)
|
if (user is null)
|
||||||
return ServiceResult<IReadOnlyList<CampaignSummary>>.Failure("unauthorized", "You must be logged in.");
|
return ServiceResult<IReadOnlyList<CampaignSummary>>.Failure("unauthorized", "You must be logged in.");
|
||||||
|
|
||||||
var results = m_StateStore.CampaignsById.Values
|
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();
|
||||||
.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<IReadOnlyList<CampaignSummary>>.Success(results);
|
return ServiceResult<IReadOnlyList<CampaignSummary>>.Success(results);
|
||||||
}
|
}
|
||||||
@@ -67,10 +63,7 @@ public sealed class GameCampaignService
|
|||||||
if (user is null)
|
if (user is null)
|
||||||
return ServiceResult<IReadOnlyList<CampaignOption>>.Failure("unauthorized", "You must be logged in.");
|
return ServiceResult<IReadOnlyList<CampaignOption>>.Failure("unauthorized", "You must be logged in.");
|
||||||
|
|
||||||
var options = m_StateStore.CampaignsById.Values
|
var options = m_StateStore.CampaignsById.Values.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).Select(GameDtoMapper.ToCampaignOption).ToArray();
|
||||||
.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.Select(GameDtoMapper.ToCampaignOption)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return ServiceResult<IReadOnlyList<CampaignOption>>.Success(options);
|
return ServiceResult<IReadOnlyList<CampaignOption>>.Success(options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,9 +62,7 @@ public sealed class GameCharacterService
|
|||||||
|
|
||||||
var isOwner = character.OwnerUserId == user.Id;
|
var isOwner = character.OwnerUserId == user.Id;
|
||||||
var isAdmin = GameAuthorization.HasRole(user, UserRoles.Admin);
|
var isAdmin = GameAuthorization.HasRole(user, UserRoles.Admin);
|
||||||
var isSourceGm = character.CampaignId.HasValue &&
|
var isSourceGm = character.CampaignId.HasValue && m_StateStore.CampaignsById.TryGetValue(character.CampaignId.Value, out var sourceCampaign) && sourceCampaign.GmUserId == user.Id;
|
||||||
m_StateStore.CampaignsById.TryGetValue(character.CampaignId.Value, out var sourceCampaign) &&
|
|
||||||
sourceCampaign.GmUserId == user.Id;
|
|
||||||
var isTargetGm = targetCampaign is not null && targetCampaign.GmUserId == user.Id;
|
var isTargetGm = targetCampaign is not null && targetCampaign.GmUserId == user.Id;
|
||||||
if (!isOwner && !isAdmin && !isSourceGm && !isTargetGm)
|
if (!isOwner && !isAdmin && !isSourceGm && !isTargetGm)
|
||||||
return ServiceResult<CharacterSummary>.Failure("forbidden", "Only the owner, GM, or admin can edit this character.");
|
return ServiceResult<CharacterSummary>.Failure("forbidden", "Only the owner, GM, or admin can edit this character.");
|
||||||
@@ -85,13 +83,9 @@ public sealed class GameCharacterService
|
|||||||
return ServiceResult<CharacterSummary>.Failure("forbidden", "Only the GM or admin can change character owner.");
|
return ServiceResult<CharacterSummary>.Failure("forbidden", "Only the GM or admin can change character owner.");
|
||||||
|
|
||||||
character.OwnerUserId = targetOwnerUserId;
|
character.OwnerUserId = targetOwnerUserId;
|
||||||
if (character.OwnerUserId != previousOwnerUserId &&
|
if (character.OwnerUserId != previousOwnerUserId && m_StateStore.UsersById.TryGetValue(previousOwnerUserId, out var previousOwner) && previousOwner.ActiveCharacterId == character.Id)
|
||||||
m_StateStore.UsersById.TryGetValue(previousOwnerUserId, out var previousOwner) &&
|
|
||||||
previousOwner.ActiveCharacterId == character.Id)
|
|
||||||
{
|
|
||||||
previousOwner.ActiveCharacterId = null;
|
previousOwner.ActiveCharacterId = null;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (sourceCampaignId != character.CampaignId)
|
if (sourceCampaignId != character.CampaignId)
|
||||||
{
|
{
|
||||||
@@ -158,11 +152,7 @@ public sealed class GameCharacterService
|
|||||||
if (user is null)
|
if (user is null)
|
||||||
return ServiceResult<IReadOnlyList<CharacterSummary>>.Failure("unauthorized", "You must be logged in.");
|
return ServiceResult<IReadOnlyList<CharacterSummary>>.Failure("unauthorized", "You must be logged in.");
|
||||||
|
|
||||||
var characters = m_StateStore.CharactersById.Values
|
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();
|
||||||
.Where(character => character.OwnerUserId == user.Id)
|
|
||||||
.OrderBy(character => character.Name, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.Select(character => GameDtoMapper.ToCharacterSummary(m_StateStore, character))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return ServiceResult<IReadOnlyList<CharacterSummary>>.Success(characters);
|
return ServiceResult<IReadOnlyList<CharacterSummary>>.Success(characters);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,11 +33,9 @@ public static class GameContextResolver
|
|||||||
public static bool TryResolveCharacterCampaignLocked(GameStateStore stateStore, Character character, out Campaign campaign, out ServiceError? error)
|
public static bool TryResolveCharacterCampaignLocked(GameStateStore stateStore, Character character, out Campaign campaign, out ServiceError? error)
|
||||||
{
|
{
|
||||||
campaign = default!;
|
campaign = default!;
|
||||||
if (!character.CampaignId.HasValue ||
|
if (!character.CampaignId.HasValue || !stateStore.CampaignsById.TryGetValue(character.CampaignId.Value, out var resolvedCampaign) || resolvedCampaign is null)
|
||||||
!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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,19 +24,15 @@ public static class GameDtoMapper
|
|||||||
{
|
{
|
||||||
var gm = stateStore.UsersById[campaign.GmUserId];
|
var gm = stateStore.UsersById[campaign.GmUserId];
|
||||||
var characterCount = stateStore.CharactersById.Values.Count(character => character.CampaignId == campaign.Id);
|
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)
|
public static CampaignRoster ToCampaignRoster(GameStateStore stateStore, Campaign campaign)
|
||||||
{
|
{
|
||||||
var gm = stateStore.UsersById[campaign.GmUserId];
|
var gm = stateStore.UsersById[campaign.GmUserId];
|
||||||
var characters = stateStore.CharactersById.Values
|
var characters = stateStore.CharactersById.Values.Where(character => character.CampaignId == campaign.Id).OrderBy(character => character.Name, StringComparer.OrdinalIgnoreCase).Select(character => ToCharacterSummary(stateStore, character)).ToArray();
|
||||||
.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)
|
public static CharacterSummary ToCharacterSummary(GameStateStore stateStore, Character character)
|
||||||
@@ -46,16 +42,8 @@ public static class GameDtoMapper
|
|||||||
|
|
||||||
public static CharacterSheet ToCharacterSheet(GameStateStore stateStore, Guid characterId)
|
public static CharacterSheet ToCharacterSheet(GameStateStore stateStore, Guid characterId)
|
||||||
{
|
{
|
||||||
var skillGroups = stateStore.SkillGroupsById.Values
|
var skillGroups = stateStore.SkillGroupsById.Values.Where(group => group.CharacterId == characterId).OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase).Select(ToCharacterSheetSkillGroup).ToArray();
|
||||||
.Where(group => group.CharacterId == characterId)
|
var skills = stateStore.SkillsById.Values.Where(skill => skill.CharacterId == characterId).OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase).Select(ToCharacterSheetSkill).ToArray();
|
||||||
.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);
|
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<RollDieResult> dice)
|
public static CampaignLogEntry ToCampaignLogEntry(RollLogEntry entry, string characterName, string skillName, string rollerDisplayName, IReadOnlyList<RollDieResult> dice)
|
||||||
{
|
{
|
||||||
return new(
|
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);
|
||||||
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(
|
public static CampaignLogListEntry ToCampaignLogListEntry(RollLogEntry entry, string characterName, string skillName, string rollerLabel, string visibilityLabel, string visibilityStyle, string summaryText, string[]? eventBadges)
|
||||||
RollLogEntry entry,
|
|
||||||
string characterName,
|
|
||||||
string skillName,
|
|
||||||
string rollerLabel,
|
|
||||||
string visibilityLabel,
|
|
||||||
string visibilityStyle,
|
|
||||||
string summaryText,
|
|
||||||
string[]? eventBadges)
|
|
||||||
{
|
{
|
||||||
return new(
|
return new(entry.Id, characterName, skillName, rollerLabel, visibilityLabel, visibilityStyle, entry.Result, summaryText, eventBadges, entry.TimestampUtc);
|
||||||
entry.Id,
|
|
||||||
characterName,
|
|
||||||
skillName,
|
|
||||||
rollerLabel,
|
|
||||||
visibilityLabel,
|
|
||||||
visibilityStyle,
|
|
||||||
entry.Result,
|
|
||||||
summaryText,
|
|
||||||
eventBadges,
|
|
||||||
entry.TimestampUtc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CampaignRollDetail ToCampaignRollDetail(RollLogEntry entry, RollDieResult[] dice)
|
public static CampaignRollDetail ToCampaignRollDetail(RollLogEntry entry, RollDieResult[] dice)
|
||||||
@@ -124,19 +81,14 @@ public static class GameDtoMapper
|
|||||||
public static CampaignStateSnapshot ToCampaignStateSnapshot(GameStateStore stateStore, Guid campaignId)
|
public static CampaignStateSnapshot ToCampaignStateSnapshot(GameStateStore stateStore, Guid campaignId)
|
||||||
{
|
{
|
||||||
var state = stateStore.GetOrCreateCampaignStateLocked(campaignId);
|
var state = stateStore.GetOrCreateCampaignStateLocked(campaignId);
|
||||||
var characterVersions = state.CharacterVersions
|
var characterVersions = state.CharacterVersions.OrderBy(version => version.Key).Select(version => new CharacterStateVersion(version.Key, version.Value)).ToArray();
|
||||||
.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)
|
public static string ResolveOwnerDisplayName(GameStateStore stateStore, Guid ownerUserId, string fallback)
|
||||||
{
|
{
|
||||||
return stateStore.UsersById.TryGetValue(ownerUserId, out var user) && !string.IsNullOrWhiteSpace(user.DisplayName)
|
return stateStore.UsersById.TryGetValue(ownerUserId, out var user) && !string.IsNullOrWhiteSpace(user.DisplayName) ? user.DisplayName : fallback;
|
||||||
? user.DisplayName
|
|
||||||
: fallback;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CharacterSheetSkillGroup ToCharacterSheetSkillGroup(SkillGroup skillGroup)
|
private static CharacterSheetSkillGroup ToCharacterSheetSkillGroup(SkillGroup skillGroup)
|
||||||
|
|||||||
@@ -11,10 +11,7 @@ public sealed class GameRollService
|
|||||||
m_StateStore = stateStore;
|
m_StateStore = stateStore;
|
||||||
m_PersistenceService = persistenceService;
|
m_PersistenceService = persistenceService;
|
||||||
m_DiceRoller = diceRoller;
|
m_DiceRoller = diceRoller;
|
||||||
m_RollEngine = new(
|
m_RollEngine = new(new(diceRoller), new(diceRoller), new(diceRoller));
|
||||||
new StandardRollEngine(diceRoller),
|
|
||||||
new D6RollEngine(diceRoller),
|
|
||||||
new RolemasterRollEngine(diceRoller));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility)
|
public ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility)
|
||||||
@@ -88,10 +85,7 @@ public sealed class GameRollService
|
|||||||
return ServiceResult<IReadOnlyList<CampaignLogEntry>>.Failure(context.Error!.Code, context.Error.Message);
|
return ServiceResult<IReadOnlyList<CampaignLogEntry>>.Failure(context.Error!.Code, context.Error.Message);
|
||||||
|
|
||||||
var (user, campaign) = context.Value!;
|
var (user, campaign) = context.Value!;
|
||||||
var entries = GetVisibleCampaignLogEntriesLocked(user, campaign)
|
var entries = GetVisibleCampaignLogEntriesLocked(user, campaign).TakeLast(CampaignLogHistoryWindowSize).Select(ToLogEntry).ToArray();
|
||||||
.TakeLast(CampaignLogHistoryWindowSize)
|
|
||||||
.Select(ToLogEntry)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return ServiceResult<IReadOnlyList<CampaignLogEntry>>.Success(entries);
|
return ServiceResult<IReadOnlyList<CampaignLogEntry>>.Success(entries);
|
||||||
}
|
}
|
||||||
@@ -112,28 +106,28 @@ public sealed class GameRollService
|
|||||||
if (!afterRollId.HasValue)
|
if (!afterRollId.HasValue)
|
||||||
{
|
{
|
||||||
var initialEntries = visibleEntries.TakeLast(pageSize).Select(entry => ToLogListEntry(user, campaign, entry)).ToArray();
|
var initialEntries = visibleEntries.TakeLast(pageSize).Select(entry => ToLogListEntry(user, campaign, entry)).ToArray();
|
||||||
return ServiceResult<CampaignLogPage>.Success(new CampaignLogPage(initialEntries, initialEntries.LastOrDefault()?.RollId, visibleEntries.Length > pageSize, false));
|
return ServiceResult<CampaignLogPage>.Success(new(initialEntries, initialEntries.LastOrDefault()?.RollId, visibleEntries.Length > pageSize, false));
|
||||||
}
|
}
|
||||||
|
|
||||||
var afterIndex = Array.FindIndex(visibleEntries, entry => entry.Id == afterRollId.Value);
|
var afterIndex = Array.FindIndex(visibleEntries, entry => entry.Id == afterRollId.Value);
|
||||||
if (afterIndex < 0)
|
if (afterIndex < 0)
|
||||||
{
|
{
|
||||||
var replacementEntries = visibleEntries.TakeLast(pageSize).Select(entry => ToLogListEntry(user, campaign, entry)).ToArray();
|
var replacementEntries = visibleEntries.TakeLast(pageSize).Select(entry => ToLogListEntry(user, campaign, entry)).ToArray();
|
||||||
return ServiceResult<CampaignLogPage>.Success(new CampaignLogPage(replacementEntries, replacementEntries.LastOrDefault()?.RollId, visibleEntries.Length > pageSize, true));
|
return ServiceResult<CampaignLogPage>.Success(new(replacementEntries, replacementEntries.LastOrDefault()?.RollId, visibleEntries.Length > pageSize, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
var newEntries = visibleEntries.Skip(afterIndex + 1).ToArray();
|
var newEntries = visibleEntries.Skip(afterIndex + 1).ToArray();
|
||||||
if (newEntries.Length == 0)
|
if (newEntries.Length == 0)
|
||||||
return ServiceResult<CampaignLogPage>.Success(new CampaignLogPage([], afterRollId, false, false));
|
return ServiceResult<CampaignLogPage>.Success(new([], afterRollId, false, false));
|
||||||
|
|
||||||
if (newEntries.Length > pageSize)
|
if (newEntries.Length > pageSize)
|
||||||
{
|
{
|
||||||
var replacementEntries = newEntries.TakeLast(pageSize).Select(entry => ToLogListEntry(user, campaign, entry)).ToArray();
|
var replacementEntries = newEntries.TakeLast(pageSize).Select(entry => ToLogListEntry(user, campaign, entry)).ToArray();
|
||||||
return ServiceResult<CampaignLogPage>.Success(new CampaignLogPage(replacementEntries, replacementEntries[^1].RollId, true, true));
|
return ServiceResult<CampaignLogPage>.Success(new(replacementEntries, replacementEntries[^1].RollId, true, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
var appendedEntries = newEntries.Select(entry => ToLogListEntry(user, campaign, entry)).ToArray();
|
var appendedEntries = newEntries.Select(entry => ToLogListEntry(user, campaign, entry)).ToArray();
|
||||||
return ServiceResult<CampaignLogPage>.Success(new CampaignLogPage(appendedEntries, appendedEntries[^1].RollId, false, false));
|
return ServiceResult<CampaignLogPage>.Success(new(appendedEntries, appendedEntries[^1].RollId, false, false));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,14 +162,7 @@ public sealed class GameRollService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ServiceResult<RollResult> RecordRollLocked(
|
private ServiceResult<RollResult> RecordRollLocked(UserAccount user, Campaign campaign, Character character, Guid skillId, RollVisibility visibility, (int Total, string Breakdown, IReadOnlyList<RollDieResult> Dice) roll, string canonicalExpression)
|
||||||
UserAccount user,
|
|
||||||
Campaign campaign,
|
|
||||||
Character character,
|
|
||||||
Guid skillId,
|
|
||||||
RollVisibility visibility,
|
|
||||||
(int Total, string Breakdown, IReadOnlyList<RollDieResult> Dice) roll,
|
|
||||||
string canonicalExpression)
|
|
||||||
{
|
{
|
||||||
var entry = new RollLogEntry
|
var entry = new RollLogEntry
|
||||||
{
|
{
|
||||||
@@ -200,18 +187,12 @@ public sealed class GameRollService
|
|||||||
|
|
||||||
private static string FormatLoggedBreakdown(Guid skillId, string canonicalExpression, string breakdown)
|
private static string FormatLoggedBreakdown(Guid skillId, string canonicalExpression, string breakdown)
|
||||||
{
|
{
|
||||||
return skillId == CustomRollSkillId
|
return skillId == CustomRollSkillId ? $"{canonicalExpression}{CustomRollBreakdownSeparator}{breakdown}" : breakdown;
|
||||||
? $"{canonicalExpression}{CustomRollBreakdownSeparator}{breakdown}"
|
|
||||||
: breakdown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<RollLogEntry> GetVisibleCampaignLogEntriesLocked(UserAccount user, Campaign campaign)
|
private IEnumerable<RollLogEntry> GetVisibleCampaignLogEntriesLocked(UserAccount user, Campaign campaign)
|
||||||
{
|
{
|
||||||
return m_StateStore.RollLog
|
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);
|
||||||
.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)
|
private CampaignLogEntry ToLogEntry(RollLogEntry entry)
|
||||||
@@ -232,15 +213,7 @@ public sealed class GameRollService
|
|||||||
var loggedExpression = ResolveLoggedExpression(entry);
|
var loggedExpression = ResolveLoggedExpression(entry);
|
||||||
var eventBadges = CampaignLogSummaryBuilder.BuildCompactLogEventBadges(campaign.Ruleset, loggedExpression, dice);
|
var eventBadges = CampaignLogSummaryBuilder.BuildCompactLogEventBadges(campaign.Ruleset, loggedExpression, dice);
|
||||||
|
|
||||||
return GameDtoMapper.ToCampaignLogListEntry(
|
return GameDtoMapper.ToCampaignLogListEntry(entry, characterName, skillName, ResolveLogRollerLabel(user, campaign, entry), ResolveLogVisibilityLabel(user, campaign, entry), ResolveLogVisibilityStyle(user, campaign, entry), CampaignLogSummaryBuilder.BuildCompactLogSummary(dice), eventBadges);
|
||||||
entry,
|
|
||||||
characterName,
|
|
||||||
skillName,
|
|
||||||
ResolveLogRollerLabel(user, campaign, entry),
|
|
||||||
ResolveLogVisibilityLabel(user, campaign, entry),
|
|
||||||
ResolveLogVisibilityStyle(user, campaign, entry),
|
|
||||||
CampaignLogSummaryBuilder.BuildCompactLogSummary(dice),
|
|
||||||
eventBadges);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string SerializeDice(IReadOnlyList<RollDieResult> dice)
|
private static string SerializeDice(IReadOnlyList<RollDieResult> dice)
|
||||||
@@ -323,8 +296,8 @@ public sealed class GameRollService
|
|||||||
private const int CampaignLogHistoryWindowSize = 100;
|
private const int CampaignLogHistoryWindowSize = 100;
|
||||||
private const int CampaignLogLivePageSize = 25;
|
private const int CampaignLogLivePageSize = 25;
|
||||||
private const string CustomRollBreakdownSeparator = " => ";
|
private const string CustomRollBreakdownSeparator = " => ";
|
||||||
private static readonly Guid CustomRollSkillId = Guid.Empty;
|
|
||||||
private const string CustomRollLabel = "Custom roll";
|
private const string CustomRollLabel = "Custom roll";
|
||||||
|
private static readonly Guid CustomRollSkillId = Guid.Empty;
|
||||||
private static readonly JsonSerializerOptions DiceJsonOptions = RpgRollerJson.CreateSerializerOptions();
|
private static readonly JsonSerializerOptions DiceJsonOptions = RpgRollerJson.CreateSerializerOptions();
|
||||||
private readonly IDiceRoller m_DiceRoller;
|
private readonly IDiceRoller m_DiceRoller;
|
||||||
private readonly GamePersistenceService m_PersistenceService;
|
private readonly GamePersistenceService m_PersistenceService;
|
||||||
|
|||||||
@@ -20,8 +20,10 @@ public sealed class GameService : IGameService
|
|||||||
m_UserAdministrationService = new(m_StateStore, m_PersistenceService);
|
m_UserAdministrationService = new(m_StateStore, m_PersistenceService);
|
||||||
m_PersistenceService.LoadStateFromDatabase();
|
m_PersistenceService.LoadStateFromDatabase();
|
||||||
lock (m_StateStore.Gate)
|
lock (m_StateStore.Gate)
|
||||||
|
{
|
||||||
m_StateStore.RebuildCampaignStateLocked();
|
m_StateStore.RebuildCampaignStateLocked();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public IReadOnlyList<RulesetDefinition> GetRulesets()
|
public IReadOnlyList<RulesetDefinition> GetRulesets()
|
||||||
{
|
{
|
||||||
@@ -188,9 +190,10 @@ public sealed class GameService : IGameService
|
|||||||
return m_RollService.GetCampaignStateSnapshot(sessionToken, campaignId);
|
return m_RollService.GetCampaignStateSnapshot(sessionToken, campaignId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly GameAuthService m_AuthService;
|
||||||
|
|
||||||
private readonly GameCampaignService m_CampaignService;
|
private readonly GameCampaignService m_CampaignService;
|
||||||
private readonly GameCharacterService m_CharacterService;
|
private readonly GameCharacterService m_CharacterService;
|
||||||
private readonly GameAuthService m_AuthService;
|
|
||||||
private readonly GamePersistenceService m_PersistenceService;
|
private readonly GamePersistenceService m_PersistenceService;
|
||||||
private readonly GameRollService m_RollService;
|
private readonly GameRollService m_RollService;
|
||||||
private readonly GameSkillService m_SkillService;
|
private readonly GameSkillService m_SkillService;
|
||||||
|
|||||||
@@ -4,22 +4,11 @@ namespace RpgRoller.Services;
|
|||||||
|
|
||||||
public sealed class GameStateStore
|
public sealed class GameStateStore
|
||||||
{
|
{
|
||||||
public object Gate { get; } = new();
|
|
||||||
public Dictionary<Guid, Campaign> CampaignsById { get; } = [];
|
|
||||||
public Dictionary<Guid, GameCampaignStateTracker> CampaignStateById { get; } = [];
|
|
||||||
public Dictionary<Guid, Character> CharactersById { get; } = [];
|
|
||||||
public List<RollLogEntry> RollLog { get; } = [];
|
|
||||||
public Dictionary<string, UserSession> SessionsByToken { get; } = new(StringComparer.Ordinal);
|
|
||||||
public Dictionary<Guid, SkillGroup> SkillGroupsById { get; } = [];
|
|
||||||
public Dictionary<Guid, Skill> SkillsById { get; } = [];
|
|
||||||
public Dictionary<string, Guid> UserIdsByUsername { get; } = new(StringComparer.Ordinal);
|
|
||||||
public Dictionary<Guid, UserAccount> UsersById { get; } = [];
|
|
||||||
|
|
||||||
public GameCampaignStateTracker GetOrCreateCampaignStateLocked(Guid campaignId)
|
public GameCampaignStateTracker GetOrCreateCampaignStateLocked(Guid campaignId)
|
||||||
{
|
{
|
||||||
if (!CampaignStateById.TryGetValue(campaignId, out var state))
|
if (!CampaignStateById.TryGetValue(campaignId, out var state))
|
||||||
{
|
{
|
||||||
state = new GameCampaignStateTracker();
|
state = new();
|
||||||
CampaignStateById[campaignId] = state;
|
CampaignStateById[campaignId] = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +20,7 @@ public sealed class GameStateStore
|
|||||||
CampaignStateById.Clear();
|
CampaignStateById.Clear();
|
||||||
|
|
||||||
foreach (var campaignId in CampaignsById.Keys)
|
foreach (var campaignId in CampaignsById.Keys)
|
||||||
CampaignStateById[campaignId] = new GameCampaignStateTracker();
|
CampaignStateById[campaignId] = new();
|
||||||
|
|
||||||
foreach (var character in CharactersById.Values.Where(character => character.CampaignId.HasValue))
|
foreach (var character in CharactersById.Values.Where(character => character.CampaignId.HasValue))
|
||||||
AddCharacterStateLocked(character.CampaignId, character.Id);
|
AddCharacterStateLocked(character.CampaignId, character.Id);
|
||||||
@@ -83,6 +72,17 @@ public sealed class GameStateStore
|
|||||||
state.TotalVersion += 1;
|
state.TotalVersion += 1;
|
||||||
state.LogVersion += 1;
|
state.LogVersion += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public object Gate { get; } = new();
|
||||||
|
public Dictionary<Guid, Campaign> CampaignsById { get; } = [];
|
||||||
|
public Dictionary<Guid, GameCampaignStateTracker> CampaignStateById { get; } = [];
|
||||||
|
public Dictionary<Guid, Character> CharactersById { get; } = [];
|
||||||
|
public List<RollLogEntry> RollLog { get; } = [];
|
||||||
|
public Dictionary<string, UserSession> SessionsByToken { get; } = new(StringComparer.Ordinal);
|
||||||
|
public Dictionary<Guid, SkillGroup> SkillGroupsById { get; } = [];
|
||||||
|
public Dictionary<Guid, Skill> SkillsById { get; } = [];
|
||||||
|
public Dictionary<string, Guid> UserIdsByUsername { get; } = new(StringComparer.Ordinal);
|
||||||
|
public Dictionary<Guid, UserAccount> UsersById { get; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class GameCampaignStateTracker
|
public sealed class GameCampaignStateTracker
|
||||||
|
|||||||
@@ -19,10 +19,7 @@ public sealed class GameUserAdministrationService
|
|||||||
if (user is null)
|
if (user is null)
|
||||||
return ServiceResult<IReadOnlyList<string>>.Failure("unauthorized", "You must be logged in.");
|
return ServiceResult<IReadOnlyList<string>>.Failure("unauthorized", "You must be logged in.");
|
||||||
|
|
||||||
var usernames = m_StateStore.UsersById.Values
|
var usernames = m_StateStore.UsersById.Values.Select(account => account.Username).OrderBy(username => username, StringComparer.OrdinalIgnoreCase).ToArray();
|
||||||
.Select(account => account.Username)
|
|
||||||
.OrderBy(username => username, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return ServiceResult<IReadOnlyList<string>>.Success(usernames);
|
return ServiceResult<IReadOnlyList<string>>.Success(usernames);
|
||||||
}
|
}
|
||||||
@@ -39,10 +36,7 @@ public sealed class GameUserAdministrationService
|
|||||||
if (!GameAuthorization.HasRole(user, UserRoles.Admin))
|
if (!GameAuthorization.HasRole(user, UserRoles.Admin))
|
||||||
return ServiceResult<IReadOnlyList<AdminUserSummary>>.Failure("forbidden", "Admin role is required.");
|
return ServiceResult<IReadOnlyList<AdminUserSummary>>.Failure("forbidden", "Admin role is required.");
|
||||||
|
|
||||||
var users = m_StateStore.UsersById.Values
|
var users = m_StateStore.UsersById.Values.OrderBy(account => account.Username, StringComparer.OrdinalIgnoreCase).Select(GameDtoMapper.ToAdminUserSummary).ToArray();
|
||||||
.OrderBy(account => account.Username, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.Select(GameDtoMapper.ToAdminUserSummary)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
return ServiceResult<IReadOnlyList<AdminUserSummary>>.Success(users);
|
return ServiceResult<IReadOnlyList<AdminUserSummary>>.Success(users);
|
||||||
}
|
}
|
||||||
@@ -92,32 +86,20 @@ public sealed class GameUserAdministrationService
|
|||||||
if (!m_StateStore.UsersById.TryGetValue(userId, out var targetUser))
|
if (!m_StateStore.UsersById.TryGetValue(userId, out var targetUser))
|
||||||
return ServiceResult<bool>.Failure("user_not_found", "User was not found.");
|
return ServiceResult<bool>.Failure("user_not_found", "User was not found.");
|
||||||
|
|
||||||
var gmCampaignIds = m_StateStore.CampaignsById.Values
|
var gmCampaignIds = m_StateStore.CampaignsById.Values.Where(campaign => campaign.GmUserId == targetUser.Id).Select(campaign => campaign.Id).ToArray();
|
||||||
.Where(campaign => campaign.GmUserId == targetUser.Id)
|
|
||||||
.Select(campaign => campaign.Id)
|
|
||||||
.ToArray();
|
|
||||||
var gmCampaignIdSet = gmCampaignIds.ToHashSet();
|
var gmCampaignIdSet = gmCampaignIds.ToHashSet();
|
||||||
var preservedCharacterIds = m_StateStore.CharactersById.Values
|
var preservedCharacterIds = m_StateStore.CharactersById.Values.Where(character => character.CampaignId.HasValue && gmCampaignIdSet.Contains(character.CampaignId.Value)).Select(character => character.Id).ToHashSet();
|
||||||
.Where(character => character.CampaignId.HasValue && gmCampaignIdSet.Contains(character.CampaignId.Value))
|
|
||||||
.Select(character => character.Id)
|
|
||||||
.ToHashSet();
|
|
||||||
|
|
||||||
foreach (var campaignId in gmCampaignIds)
|
foreach (var campaignId in gmCampaignIds)
|
||||||
DeleteCampaignLocked(campaignId);
|
DeleteCampaignLocked(campaignId);
|
||||||
|
|
||||||
var ownedCharacterIds = m_StateStore.CharactersById.Values
|
var ownedCharacterIds = m_StateStore.CharactersById.Values.Where(character => character.OwnerUserId == targetUser.Id && !preservedCharacterIds.Contains(character.Id)).Select(character => character.Id).ToArray();
|
||||||
.Where(character => character.OwnerUserId == targetUser.Id && !preservedCharacterIds.Contains(character.Id))
|
|
||||||
.Select(character => character.Id)
|
|
||||||
.ToArray();
|
|
||||||
foreach (var characterId in ownedCharacterIds)
|
foreach (var characterId in ownedCharacterIds)
|
||||||
DeleteCharacterLocked(characterId);
|
DeleteCharacterLocked(characterId);
|
||||||
|
|
||||||
m_StateStore.RollLog.RemoveAll(entry => entry.RollerUserId == targetUser.Id);
|
m_StateStore.RollLog.RemoveAll(entry => entry.RollerUserId == targetUser.Id);
|
||||||
|
|
||||||
var staleSessions = m_StateStore.SessionsByToken.Values
|
var staleSessions = m_StateStore.SessionsByToken.Values.Where(session => session.UserId == targetUser.Id).Select(session => session.Token).ToArray();
|
||||||
.Where(session => session.UserId == targetUser.Id)
|
|
||||||
.Select(session => session.Token)
|
|
||||||
.ToArray();
|
|
||||||
foreach (var token in staleSessions)
|
foreach (var token in staleSessions)
|
||||||
m_StateStore.SessionsByToken.Remove(token);
|
m_StateStore.SessionsByToken.Remove(token);
|
||||||
|
|
||||||
@@ -134,10 +116,7 @@ public sealed class GameUserAdministrationService
|
|||||||
if (!m_StateStore.CampaignsById.Remove(campaignId))
|
if (!m_StateStore.CampaignsById.Remove(campaignId))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var affectedCharacterIds = m_StateStore.CharactersById.Values
|
var affectedCharacterIds = m_StateStore.CharactersById.Values.Where(character => character.CampaignId == campaignId).Select(character => character.Id).ToArray();
|
||||||
.Where(character => character.CampaignId == campaignId)
|
|
||||||
.Select(character => character.Id)
|
|
||||||
.ToArray();
|
|
||||||
foreach (var characterId in affectedCharacterIds)
|
foreach (var characterId in affectedCharacterIds)
|
||||||
m_StateStore.CharactersById[characterId].CampaignId = null;
|
m_StateStore.CharactersById[characterId].CampaignId = null;
|
||||||
|
|
||||||
@@ -153,17 +132,11 @@ public sealed class GameUserAdministrationService
|
|||||||
var campaignId = character.CampaignId;
|
var campaignId = character.CampaignId;
|
||||||
m_StateStore.CharactersById.Remove(characterId);
|
m_StateStore.CharactersById.Remove(characterId);
|
||||||
|
|
||||||
var skillGroupIds = m_StateStore.SkillGroupsById.Values
|
var skillGroupIds = m_StateStore.SkillGroupsById.Values.Where(group => group.CharacterId == characterId).Select(group => group.Id).ToHashSet();
|
||||||
.Where(group => group.CharacterId == characterId)
|
|
||||||
.Select(group => group.Id)
|
|
||||||
.ToHashSet();
|
|
||||||
foreach (var skillGroupId in skillGroupIds)
|
foreach (var skillGroupId in skillGroupIds)
|
||||||
m_StateStore.SkillGroupsById.Remove(skillGroupId);
|
m_StateStore.SkillGroupsById.Remove(skillGroupId);
|
||||||
|
|
||||||
var skillIds = m_StateStore.SkillsById.Values
|
var skillIds = m_StateStore.SkillsById.Values.Where(skill => skill.CharacterId == characterId).Select(skill => skill.Id).ToHashSet();
|
||||||
.Where(skill => skill.CharacterId == characterId)
|
|
||||||
.Select(skill => skill.Id)
|
|
||||||
.ToHashSet();
|
|
||||||
foreach (var skillId in skillIds)
|
foreach (var skillId in skillIds)
|
||||||
m_StateStore.SkillsById.Remove(skillId);
|
m_StateStore.SkillsById.Remove(skillId);
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,7 @@ public static class RoleSerializer
|
|||||||
|
|
||||||
public static string[] Normalize(IEnumerable<string> roles)
|
public static string[] Normalize(IEnumerable<string> roles)
|
||||||
{
|
{
|
||||||
return roles
|
return roles.Where(role => !string.IsNullOrWhiteSpace(role)).Select(role => role.Trim().ToLowerInvariant()).Distinct(StringComparer.Ordinal).OrderBy(role => role, StringComparer.Ordinal).ToArray();
|
||||||
.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)
|
public static bool HasRole(string serializedRoles, string role)
|
||||||
|
|||||||
@@ -40,22 +40,19 @@ public sealed class RolemasterRollEngine
|
|||||||
var initialRoll = m_DiceRoller.Roll(expression.Sides);
|
var initialRoll = m_DiceRoller.Roll(expression.Sides);
|
||||||
var followUpRolls = new List<int>();
|
var followUpRolls = new List<int>();
|
||||||
int? initialContribution = initialRoll <= fumbleRange ? null : initialRoll;
|
int? initialContribution = initialRoll <= fumbleRange ? null : initialRoll;
|
||||||
var dice = new List<RollDieResult>
|
var dice = new List<RollDieResult> { CreateRolemasterDie(initialRoll, 1, RollDieKinds.RolemasterOpenEndedInitial, initialContribution) };
|
||||||
{
|
|
||||||
CreateRolemasterDie(initialRoll, 1, RollDieKinds.RolemasterOpenEndedInitial, initialContribution)
|
|
||||||
};
|
|
||||||
|
|
||||||
var baseTotal = initialRoll <= fumbleRange ? 0 : initialRoll;
|
var baseTotal = initialRoll <= fumbleRange ? 0 : initialRoll;
|
||||||
var subtractFollowUps = false;
|
var subtractFollowUps = false;
|
||||||
if (initialRoll >= 96)
|
if (initialRoll >= 96)
|
||||||
{
|
{
|
||||||
followUpRolls.AddRange(RollHighOpenEndedChain(dice, sequenceStart: 2, subtract: false));
|
followUpRolls.AddRange(RollHighOpenEndedChain(dice, 2, false));
|
||||||
baseTotal += followUpRolls.Sum();
|
baseTotal += followUpRolls.Sum();
|
||||||
}
|
}
|
||||||
else if (initialRoll <= fumbleRange)
|
else if (initialRoll <= fumbleRange)
|
||||||
{
|
{
|
||||||
subtractFollowUps = true;
|
subtractFollowUps = true;
|
||||||
followUpRolls.AddRange(RollHighOpenEndedChain(dice, sequenceStart: 2, subtract: true));
|
followUpRolls.AddRange(RollHighOpenEndedChain(dice, 2, true));
|
||||||
baseTotal -= followUpRolls.Sum();
|
baseTotal -= followUpRolls.Sum();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,11 +70,7 @@ public sealed class RolemasterRollEngine
|
|||||||
{
|
{
|
||||||
var roll = m_DiceRoller.Roll(100);
|
var roll = m_DiceRoller.Roll(100);
|
||||||
followUpRolls.Add(roll);
|
followUpRolls.Add(roll);
|
||||||
dice.Add(CreateRolemasterDie(
|
dice.Add(CreateRolemasterDie(roll, sequence, subtract ? RollDieKinds.RolemasterOpenEndedLowSubtract : RollDieKinds.RolemasterOpenEndedHigh, subtract ? -roll : roll));
|
||||||
roll,
|
|
||||||
sequence,
|
|
||||||
subtract ? RollDieKinds.RolemasterOpenEndedLowSubtract : RollDieKinds.RolemasterOpenEndedHigh,
|
|
||||||
subtract ? -roll : roll));
|
|
||||||
|
|
||||||
sequence += 1;
|
sequence += 1;
|
||||||
if (roll < 96)
|
if (roll < 96)
|
||||||
|
|||||||
@@ -4,12 +4,7 @@ namespace RpgRoller.Services;
|
|||||||
|
|
||||||
public static class SkillDefinitionValidator
|
public static class SkillDefinitionValidator
|
||||||
{
|
{
|
||||||
public static ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)> Validate(
|
public static ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)> Validate(RulesetKind ruleset, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange)
|
||||||
RulesetKind ruleset,
|
|
||||||
string diceRollDefinition,
|
|
||||||
int wildDice,
|
|
||||||
bool allowFumble,
|
|
||||||
int? fumbleRange)
|
|
||||||
{
|
{
|
||||||
var expressionValidation = DiceRules.ParseExpression(ruleset, diceRollDefinition);
|
var expressionValidation = DiceRules.ParseExpression(ruleset, diceRollDefinition);
|
||||||
if (!expressionValidation.Succeeded)
|
if (!expressionValidation.Succeeded)
|
||||||
@@ -19,19 +14,10 @@ public static class SkillDefinitionValidator
|
|||||||
if (!optionsValidation.Succeeded)
|
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)>.Failure(optionsValidation.Error!.Code, optionsValidation.Error.Message);
|
||||||
|
|
||||||
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)>.Success((
|
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)>.Success((expressionValidation.Value!.Canonical, optionsValidation.Value!.WildDice, optionsValidation.Value.AllowFumble, optionsValidation.Value.FumbleRange));
|
||||||
expressionValidation.Value!.Canonical,
|
|
||||||
optionsValidation.Value!.WildDice,
|
|
||||||
optionsValidation.Value.AllowFumble,
|
|
||||||
optionsValidation.Value.FumbleRange));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)> ValidateOptions(
|
private static ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)> ValidateOptions(RulesetKind ruleset, DiceExpression expression, int wildDice, bool allowFumble, int? fumbleRange)
|
||||||
RulesetKind ruleset,
|
|
||||||
DiceExpression expression,
|
|
||||||
int wildDice,
|
|
||||||
bool allowFumble,
|
|
||||||
int? fumbleRange)
|
|
||||||
{
|
{
|
||||||
if (wildDice < 0 || wildDice > 50)
|
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.");
|
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_wild_dice", "Wild dice must be between 0 and 50.");
|
||||||
|
|||||||
@@ -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
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
$repoRoot = Split-Path -Parent $scriptDir
|
$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
|
Push-Location $repoRoot
|
||||||
try {
|
try {
|
||||||
|
Remove-TestCoverageArtifacts -ResultsRoot $repoCoverageResultsRoot
|
||||||
|
|
||||||
if (-not $SkipDotnetRestore) {
|
if (-not $SkipDotnetRestore) {
|
||||||
Invoke-Step -Name "Restore .NET solution" -Action {
|
Invoke-Step -Name "Restore .NET solution" -Action {
|
||||||
dotnet restore RpgRoller.sln --verbosity minimal
|
dotnet restore RpgRoller.sln --verbosity minimal
|
||||||
@@ -47,15 +72,15 @@ try {
|
|||||||
|
|
||||||
Invoke-Step -Name "Run tests" -Action {
|
Invoke-Step -Name "Run tests" -Action {
|
||||||
if ($SkipBuild) {
|
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 {
|
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 {
|
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) {
|
if (-not $SkipPlaywright) {
|
||||||
@@ -67,5 +92,11 @@ try {
|
|||||||
Write-Host "CI checks passed."
|
Write-Host "CI checks passed."
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
if (Test-Path $testResultsRoot) {
|
||||||
|
Remove-Item -Path $testResultsRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
Remove-TestCoverageArtifacts -ResultsRoot $repoCoverageResultsRoot
|
||||||
|
|
||||||
Pop-Location
|
Pop-Location
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user