Persist Rolemaster fumble range
This commit is contained in:
@@ -63,6 +63,8 @@ Gameplay capabilities now include:
|
|||||||
- Shared top header control across all authenticated workspace screens (play, campaign management, admin)
|
- Shared top header control across all authenticated workspace screens (play, campaign management, admin)
|
||||||
- Admin user management is integrated into workspace screen toggles (`Play`, `Campaign Management`, `Admin`)
|
- Admin user management is integrated into workspace screen toggles (`Play`, `Campaign Management`, `Admin`)
|
||||||
- Rolemaster expression validation currently recognizes `2d10+48`, `d100+4`, and `d100!+85`, including Rolemaster-only negative modifiers such as `d100-15`
|
- Rolemaster expression validation currently recognizes `2d10+48`, `d100+4`, and `d100!+85`, including Rolemaster-only negative modifiers such as `d100-15`
|
||||||
|
- Rolemaster open-ended percentile skills and skill-group defaults now persist a nullable `FumbleRange` field, while D6 and D&D rows migrate forward unchanged
|
||||||
|
- Startup migration coverage is validated against a copied temp-file instance of `RpgRoller/App_Data/rpgroller.development.db` before feature work is considered complete
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,35 @@ public sealed class CampaignApiTests : ApiTestBase
|
|||||||
Assert.Equal("rolemaster", campaign.RulesetId);
|
Assert.Equal("rolemaster", campaign.RulesetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RolemasterSkillDefinitions_RoundTripFumbleRangeThroughApi()
|
||||||
|
{
|
||||||
|
using var factory = CreateFactory(88, 42, 17);
|
||||||
|
using var gmClient = factory.CreateClient(new() { AllowAutoRedirect = false });
|
||||||
|
|
||||||
|
await RegisterAsync(gmClient, "gm-rm-skill", "Password123", "Game Master");
|
||||||
|
await LoginAsync(gmClient, "gm-rm-skill", "Password123");
|
||||||
|
|
||||||
|
var campaign = await PostAsync<CreateCampaignRequest, CampaignSummary>(gmClient, "/api/campaigns", new("Shadow World", "rolemaster"));
|
||||||
|
var character = await PostAsync<CreateCharacterRequest, CharacterSummary>(gmClient, "/api/characters", new("Kalen", campaign.Id));
|
||||||
|
|
||||||
|
var missingFumbleRange = await gmClient.PostAsJsonAsync($"/api/characters/{character.Id}/skills", new CreateSkillRequest("Bad Open Ended", "d100!+35", 0, false));
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, missingFumbleRange.StatusCode);
|
||||||
|
|
||||||
|
var group = await PostAsync<CreateSkillGroupRequest, SkillGroupSummary>(gmClient, $"/api/characters/{character.Id}/skill-groups", new("Perception", "d100!+15", 0, false, 5));
|
||||||
|
Assert.Equal(5, group.FumbleRange);
|
||||||
|
|
||||||
|
var skill = await PostAsync<CreateSkillRequest, SkillSummary>(gmClient, $"/api/characters/{character.Id}/skills", new("Awareness", "d100!+35", 0, false, group.Id, 3));
|
||||||
|
Assert.Equal(3, skill.FumbleRange);
|
||||||
|
|
||||||
|
var updatedSkill = await PutAsync<UpdateSkillRequest, SkillSummary>(gmClient, $"/api/skills/{skill.Id}", new("Awareness", "d100!+45", 0, false, group.Id, 4));
|
||||||
|
Assert.Equal(4, updatedSkill.FumbleRange);
|
||||||
|
|
||||||
|
var sheet = await GetAsync<CharacterSheet>(gmClient, $"/api/characters/{character.Id}/sheet");
|
||||||
|
Assert.Equal(5, Assert.Single(sheet.SkillGroups).FumbleRange);
|
||||||
|
Assert.Equal(4, Assert.Single(sheet.Skills).FumbleRange);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SkillGroupsAndOwnerTransfer_WorkThroughApi()
|
public async Task SkillGroupsAndOwnerTransfer_WorkThroughApi()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using Microsoft.Data.Sqlite;
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using RpgRoller.Data;
|
using RpgRoller.Data;
|
||||||
using RpgRoller.Hosting;
|
using RpgRoller.Hosting;
|
||||||
|
|
||||||
@@ -125,6 +127,16 @@ public sealed class HostingCoverageTests
|
|||||||
|
|
||||||
Assert.Contains("WildDice", columns);
|
Assert.Contains("WildDice", columns);
|
||||||
Assert.Contains("AllowFumble", columns);
|
Assert.Contains("AllowFumble", columns);
|
||||||
|
Assert.Contains("FumbleRange", columns);
|
||||||
|
|
||||||
|
using var skillGroupsTableInfoCommand = verifyConnection.CreateCommand();
|
||||||
|
skillGroupsTableInfoCommand.CommandText = "PRAGMA table_info('SkillGroups');";
|
||||||
|
using var skillGroupsTableInfoReader = skillGroupsTableInfoCommand.ExecuteReader();
|
||||||
|
var skillGroupColumns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
while (skillGroupsTableInfoReader.Read())
|
||||||
|
skillGroupColumns.Add(skillGroupsTableInfoReader.GetString(1));
|
||||||
|
|
||||||
|
Assert.Contains("FumbleRange", skillGroupColumns);
|
||||||
|
|
||||||
using var rollTableInfoCommand = verifyConnection.CreateCommand();
|
using var rollTableInfoCommand = verifyConnection.CreateCommand();
|
||||||
rollTableInfoCommand.CommandText = "PRAGMA table_info('RollLogEntries');";
|
rollTableInfoCommand.CommandText = "PRAGMA table_info('RollLogEntries');";
|
||||||
@@ -183,5 +195,130 @@ public sealed class HostingCoverageTests
|
|||||||
rolesHistoryCommand.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = '20260226160859_AddAuthorizationRolesAndCampaignDeletion';";
|
rolesHistoryCommand.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = '20260226160859_AddAuthorizationRolesAndCampaignDeletion';";
|
||||||
var rolesHistoryCount = Convert.ToInt32(rolesHistoryCommand.ExecuteScalar());
|
var rolesHistoryCount = Convert.ToInt32(rolesHistoryCommand.ExecuteScalar());
|
||||||
Assert.Equal(1, rolesHistoryCount);
|
Assert.Equal(1, rolesHistoryCount);
|
||||||
|
|
||||||
|
using var rolemasterHistoryCommand = verifyConnection.CreateCommand();
|
||||||
|
rolemasterHistoryCommand.CommandText = "SELECT COUNT(*) FROM \"__EFMigrationsHistory\" WHERE \"MigrationId\" = '20260402222501_AddRolemasterFumbleRange';";
|
||||||
|
var rolemasterHistoryCount = Convert.ToInt32(rolemasterHistoryCommand.ExecuteScalar());
|
||||||
|
Assert.Equal(1, rolemasterHistoryCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void InitializeRpgRollerState_MigratesCopiedDevelopmentDatabaseAndPreservesD6Rolling()
|
||||||
|
{
|
||||||
|
var sourceDbPath = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "RpgRoller", "App_Data", "rpgroller.development.db");
|
||||||
|
var copiedDbPath = Path.Combine(Path.GetTempPath(), $"rpgroller-dev-copy-{Guid.NewGuid():N}.db");
|
||||||
|
File.Copy(Path.GetFullPath(sourceDbPath), copiedDbPath, overwrite: true);
|
||||||
|
|
||||||
|
Guid skillId;
|
||||||
|
Guid ownerUserId;
|
||||||
|
Guid characterId;
|
||||||
|
var campaignCountBefore = 0;
|
||||||
|
var skillCountBefore = 0;
|
||||||
|
using (var connection = new SqliteConnection($"Data Source={copiedDbPath}"))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
using var countsCommand = connection.CreateCommand();
|
||||||
|
countsCommand.CommandText = """
|
||||||
|
SELECT (SELECT COUNT(*) FROM Campaigns),
|
||||||
|
(SELECT COUNT(*) FROM Skills);
|
||||||
|
""";
|
||||||
|
using var countsReader = countsCommand.ExecuteReader();
|
||||||
|
Assert.True(countsReader.Read());
|
||||||
|
campaignCountBefore = countsReader.GetInt32(0);
|
||||||
|
skillCountBefore = countsReader.GetInt32(1);
|
||||||
|
|
||||||
|
using var existingSkillCommand = connection.CreateCommand();
|
||||||
|
existingSkillCommand.CommandText = """
|
||||||
|
SELECT s.Id, c.OwnerUserId, c.Id
|
||||||
|
FROM Skills s
|
||||||
|
INNER JOIN Characters c ON c.Id = s.CharacterId
|
||||||
|
INNER JOIN Campaigns cp ON cp.Id = c.CampaignId
|
||||||
|
WHERE cp.Ruleset = 'D6'
|
||||||
|
ORDER BY s.Name
|
||||||
|
LIMIT 1;
|
||||||
|
""";
|
||||||
|
using var existingSkillReader = existingSkillCommand.ExecuteReader();
|
||||||
|
Assert.True(existingSkillReader.Read());
|
||||||
|
skillId = Guid.Parse(existingSkillReader.GetString(0));
|
||||||
|
ownerUserId = Guid.Parse(existingSkillReader.GetString(1));
|
||||||
|
characterId = Guid.Parse(existingSkillReader.GetString(2));
|
||||||
|
|
||||||
|
using var sessionCommand = connection.CreateCommand();
|
||||||
|
sessionCommand.CommandText = """
|
||||||
|
INSERT INTO Sessions ("Token", "UserId", "CreatedAtUtc")
|
||||||
|
VALUES ($token, $userId, $createdAtUtc);
|
||||||
|
""";
|
||||||
|
var tokenParameter = sessionCommand.CreateParameter();
|
||||||
|
tokenParameter.ParameterName = "$token";
|
||||||
|
tokenParameter.Value = "migration-test-session";
|
||||||
|
sessionCommand.Parameters.Add(tokenParameter);
|
||||||
|
|
||||||
|
var userParameter = sessionCommand.CreateParameter();
|
||||||
|
userParameter.ParameterName = "$userId";
|
||||||
|
userParameter.Value = ownerUserId.ToString();
|
||||||
|
sessionCommand.Parameters.Add(userParameter);
|
||||||
|
|
||||||
|
var createdAtParameter = sessionCommand.CreateParameter();
|
||||||
|
createdAtParameter.ParameterName = "$createdAtUtc";
|
||||||
|
createdAtParameter.Value = DateTimeOffset.UtcNow.ToString("O");
|
||||||
|
sessionCommand.Parameters.Add(createdAtParameter);
|
||||||
|
|
||||||
|
_ = sessionCommand.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
|
||||||
|
{
|
||||||
|
ContentRootPath = Path.GetTempPath(),
|
||||||
|
EnvironmentName = Environments.Development
|
||||||
|
});
|
||||||
|
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
["ConnectionStrings:RpgRoller"] = $"Data Source={copiedDbPath}"
|
||||||
|
});
|
||||||
|
builder.Services.AddRpgRollerCore(builder.Configuration, builder.Environment);
|
||||||
|
|
||||||
|
using var app = builder.Build();
|
||||||
|
app.InitializeRpgRollerState();
|
||||||
|
|
||||||
|
using var scope = app.Services.CreateScope();
|
||||||
|
var game = scope.ServiceProvider.GetRequiredService<IGameService>();
|
||||||
|
var rollResult = game.RollSkill("migration-test-session", skillId, "public");
|
||||||
|
Assert.True(rollResult.Succeeded);
|
||||||
|
Assert.NotEmpty(ServiceTestSupport.GetValue(rollResult).Dice);
|
||||||
|
|
||||||
|
var migratedSheet = ServiceTestSupport.GetValue(game.GetCharacterSheet("migration-test-session", characterId));
|
||||||
|
Assert.Contains(migratedSheet.Skills, skill => skill.Id == skillId);
|
||||||
|
|
||||||
|
using var verifyConnection = new SqliteConnection($"Data Source={copiedDbPath}");
|
||||||
|
verifyConnection.Open();
|
||||||
|
|
||||||
|
using var countsAfterCommand = verifyConnection.CreateCommand();
|
||||||
|
countsAfterCommand.CommandText = """
|
||||||
|
SELECT (SELECT COUNT(*) FROM Campaigns),
|
||||||
|
(SELECT COUNT(*) FROM Skills);
|
||||||
|
""";
|
||||||
|
using var countsAfterReader = countsAfterCommand.ExecuteReader();
|
||||||
|
Assert.True(countsAfterReader.Read());
|
||||||
|
Assert.Equal(campaignCountBefore, countsAfterReader.GetInt32(0));
|
||||||
|
Assert.Equal(skillCountBefore, countsAfterReader.GetInt32(1));
|
||||||
|
|
||||||
|
using var skillsTableInfoCommand = verifyConnection.CreateCommand();
|
||||||
|
skillsTableInfoCommand.CommandText = "PRAGMA table_info('Skills');";
|
||||||
|
using var skillsTableInfoReader = skillsTableInfoCommand.ExecuteReader();
|
||||||
|
var skillColumns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
while (skillsTableInfoReader.Read())
|
||||||
|
skillColumns.Add(skillsTableInfoReader.GetString(1));
|
||||||
|
|
||||||
|
Assert.Contains("FumbleRange", skillColumns);
|
||||||
|
|
||||||
|
using var skillGroupsTableInfoCommand = verifyConnection.CreateCommand();
|
||||||
|
skillGroupsTableInfoCommand.CommandText = "PRAGMA table_info('SkillGroups');";
|
||||||
|
using var skillGroupsTableInfoReader = skillGroupsTableInfoCommand.ExecuteReader();
|
||||||
|
var skillGroupColumns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
while (skillGroupsTableInfoReader.Read())
|
||||||
|
skillGroupColumns.Add(skillGroupsTableInfoReader.GetString(1));
|
||||||
|
|
||||||
|
Assert.Contains("FumbleRange", skillGroupColumns);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,4 +92,31 @@ public sealed class ServicePersistenceTests
|
|||||||
Assert.False(invalidExpressionHarness.Service.RollSkill(ownerSession, skill.Id, "public").Succeeded);
|
Assert.False(invalidExpressionHarness.Service.RollSkill(ownerSession, skill.Id, "public").Succeeded);
|
||||||
Assert.False(service.GetCampaignLog(string.Empty, campaign.Id).Succeeded);
|
Assert.False(service.GetCampaignLog(string.Empty, campaign.Id).Succeeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RolemasterFumbleRange_PersistsAcrossDatabaseReload()
|
||||||
|
{
|
||||||
|
using var harness = ServiceTestSupport.CreateHarness();
|
||||||
|
var service = harness.Service;
|
||||||
|
|
||||||
|
service.Register("gm-rm-persist", "Password123", "GM");
|
||||||
|
service.Register("owner-rm-persist", "Password123", "Owner");
|
||||||
|
|
||||||
|
var gmSession = ServiceTestSupport.GetValue(service.Login("gm-rm-persist", "Password123")).SessionToken;
|
||||||
|
var ownerSession = ServiceTestSupport.GetValue(service.Login("owner-rm-persist", "Password123")).SessionToken;
|
||||||
|
|
||||||
|
var campaign = ServiceTestSupport.GetValue(service.CreateCampaign(gmSession, "Rolemaster Persistence", "rolemaster"));
|
||||||
|
var character = ServiceTestSupport.GetValue(service.CreateCharacter(ownerSession, "Loremaster", campaign.Id));
|
||||||
|
var group = ServiceTestSupport.GetValue(service.CreateSkillGroup(ownerSession, character.Id, "Perception", "d100!+25", 0, false, 5));
|
||||||
|
var skill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, character.Id, "Read Runes", "d100!+35", 0, false, group.Id, 3));
|
||||||
|
|
||||||
|
using var reloadedHarness = ServiceTestSupport.CreateHarnessFromPath(harness.DbPath);
|
||||||
|
var reloadedSheet = ServiceTestSupport.GetValue(reloadedHarness.Service.GetCharacterSheet(ownerSession, character.Id));
|
||||||
|
|
||||||
|
var reloadedGroup = Assert.Single(reloadedSheet.SkillGroups, current => current.Id == group.Id);
|
||||||
|
Assert.Equal(5, reloadedGroup.FumbleRange);
|
||||||
|
|
||||||
|
var reloadedSkill = Assert.Single(reloadedSheet.Skills, current => current.Id == skill.Id);
|
||||||
|
Assert.Equal(3, reloadedSkill.FumbleRange);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,19 +195,34 @@ public sealed class ServiceSkillGroupAndOwnershipTests
|
|||||||
var negativeDndSkill = service.CreateSkill(ownerSession, dndCharacter.Id, "Invalid", "1d20-1", 0, false);
|
var negativeDndSkill = service.CreateSkill(ownerSession, dndCharacter.Id, "Invalid", "1d20-1", 0, false);
|
||||||
Assert.False(negativeDndSkill.Succeeded);
|
Assert.False(negativeDndSkill.Succeeded);
|
||||||
|
|
||||||
var rolemasterGroup = ServiceTestSupport.GetValue(service.CreateSkillGroup(ownerSession, rolemasterCharacter.Id, "Initiative", "2d10-15", 3, true));
|
var invalidRolemasterOptions = service.CreateSkillGroup(ownerSession, rolemasterCharacter.Id, "Invalid", "2d10-15", 3, true);
|
||||||
Assert.Equal("2d10-15", rolemasterGroup.DiceRollDefinition);
|
Assert.False(invalidRolemasterOptions.Succeeded);
|
||||||
|
|
||||||
|
var rolemasterGroup = ServiceTestSupport.GetValue(service.CreateSkillGroup(ownerSession, rolemasterCharacter.Id, "Awareness", "d100!+15", 0, false, 5));
|
||||||
|
Assert.Equal("d100!+15", rolemasterGroup.DiceRollDefinition);
|
||||||
Assert.Equal(0, rolemasterGroup.WildDice);
|
Assert.Equal(0, rolemasterGroup.WildDice);
|
||||||
Assert.False(rolemasterGroup.AllowFumble);
|
Assert.False(rolemasterGroup.AllowFumble);
|
||||||
|
Assert.Equal(5, rolemasterGroup.FumbleRange);
|
||||||
|
|
||||||
var percentileSkill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, rolemasterCharacter.Id, "Perception", "1d100-20", 4, true));
|
var percentileWithFumbleRange = service.CreateSkill(ownerSession, rolemasterCharacter.Id, "Bad Percentile", "1d100-20", 0, false, null, 5);
|
||||||
|
Assert.False(percentileWithFumbleRange.Succeeded);
|
||||||
|
|
||||||
|
var percentileSkill = ServiceTestSupport.GetValue(service.CreateSkill(ownerSession, rolemasterCharacter.Id, "Perception", "1d100-20", 0, false, rolemasterGroup.Id));
|
||||||
Assert.Equal("d100-20", percentileSkill.DiceRollDefinition);
|
Assert.Equal("d100-20", percentileSkill.DiceRollDefinition);
|
||||||
Assert.Equal(0, percentileSkill.WildDice);
|
Assert.Equal(0, percentileSkill.WildDice);
|
||||||
Assert.False(percentileSkill.AllowFumble);
|
Assert.False(percentileSkill.AllowFumble);
|
||||||
|
Assert.Null(percentileSkill.FumbleRange);
|
||||||
|
|
||||||
var openEndedSkill = ServiceTestSupport.GetValue(service.UpdateSkill(ownerSession, percentileSkill.Id, "Perception", "1d100!+85", 5, true));
|
var missingOpenEndedFumbleRange = service.UpdateSkill(ownerSession, percentileSkill.Id, "Perception", "1d100!+85", 0, false, rolemasterGroup.Id);
|
||||||
|
Assert.False(missingOpenEndedFumbleRange.Succeeded);
|
||||||
|
|
||||||
|
var invalidOpenEndedFumbleRange = service.UpdateSkill(ownerSession, percentileSkill.Id, "Perception", "1d100!+85", 0, false, rolemasterGroup.Id, 96);
|
||||||
|
Assert.False(invalidOpenEndedFumbleRange.Succeeded);
|
||||||
|
|
||||||
|
var openEndedSkill = ServiceTestSupport.GetValue(service.UpdateSkill(ownerSession, percentileSkill.Id, "Perception", "1d100!+85", 0, false, rolemasterGroup.Id, 5));
|
||||||
Assert.Equal("d100!+85", openEndedSkill.DiceRollDefinition);
|
Assert.Equal("d100!+85", openEndedSkill.DiceRollDefinition);
|
||||||
Assert.Equal(0, openEndedSkill.WildDice);
|
Assert.Equal(0, openEndedSkill.WildDice);
|
||||||
Assert.False(openEndedSkill.AllowFumble);
|
Assert.False(openEndedSkill.AllowFumble);
|
||||||
|
Assert.Equal(5, openEndedSkill.FumbleRange);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,11 +87,11 @@ public sealed class WorkspaceQueryServiceTests
|
|||||||
public ServiceResult<bool> DeleteCharacter(string sessionToken, Guid characterId) => 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<bool> ActivateCharacter(string sessionToken, Guid characterId) => throw new NotSupportedException();
|
||||||
public ServiceResult<IReadOnlyList<CharacterSummary>> GetOwnCharacters(string sessionToken) => 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) => 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) => 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<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) => 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) => 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<bool> DeleteSkill(string sessionToken, Guid skillId) => throw new NotSupportedException();
|
||||||
public ServiceResult<CharacterSheet> GetCharacterSheet(string sessionToken, Guid characterId) => 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> RollSkill(string sessionToken, Guid skillId, string visibility) => throw new NotSupportedException();
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ internal static class SkillEndpoints
|
|||||||
{
|
{
|
||||||
group.MapPost("/characters/{characterId:guid}/skills", (Guid characterId, CreateSkillRequest request, HttpContext context, IGameService game) =>
|
group.MapPost("/characters/{characterId:guid}/skills", (Guid characterId, CreateSkillRequest request, HttpContext context, IGameService game) =>
|
||||||
{
|
{
|
||||||
var result = game.CreateSkill(context.GetRequiredSessionToken(), characterId, request.Name, request.DiceRollDefinition, request.WildDice, request.AllowFumble, request.SkillGroupId);
|
var result = game.CreateSkill(context.GetRequiredSessionToken(), characterId, request.Name, request.DiceRollDefinition, request.WildDice, request.AllowFumble, request.SkillGroupId, request.FumbleRange);
|
||||||
return ApiResultMapper.ToApiResult(result);
|
return ApiResultMapper.ToApiResult(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
group.MapPut("/skills/{skillId:guid}", (Guid skillId, UpdateSkillRequest request, HttpContext context, IGameService game) =>
|
group.MapPut("/skills/{skillId:guid}", (Guid skillId, UpdateSkillRequest request, HttpContext context, IGameService game) =>
|
||||||
{
|
{
|
||||||
var result = game.UpdateSkill(context.GetRequiredSessionToken(), skillId, request.Name, request.DiceRollDefinition, request.WildDice, request.AllowFumble, request.SkillGroupId);
|
var result = game.UpdateSkill(context.GetRequiredSessionToken(), skillId, request.Name, request.DiceRollDefinition, request.WildDice, request.AllowFumble, request.SkillGroupId, request.FumbleRange);
|
||||||
return ApiResultMapper.ToApiResult(result);
|
return ApiResultMapper.ToApiResult(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -27,13 +27,13 @@ internal static class SkillEndpoints
|
|||||||
|
|
||||||
group.MapPost("/characters/{characterId:guid}/skill-groups", (Guid characterId, CreateSkillGroupRequest request, HttpContext context, IGameService game) =>
|
group.MapPost("/characters/{characterId:guid}/skill-groups", (Guid characterId, CreateSkillGroupRequest request, HttpContext context, IGameService game) =>
|
||||||
{
|
{
|
||||||
var result = game.CreateSkillGroup(context.GetRequiredSessionToken(), characterId, request.Name, request.DiceRollDefinition, request.WildDice, request.AllowFumble);
|
var result = game.CreateSkillGroup(context.GetRequiredSessionToken(), characterId, request.Name, request.DiceRollDefinition, request.WildDice, request.AllowFumble, request.FumbleRange);
|
||||||
return ApiResultMapper.ToApiResult(result);
|
return ApiResultMapper.ToApiResult(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
group.MapPut("/skill-groups/{skillGroupId:guid}", (Guid skillGroupId, UpdateSkillGroupRequest request, HttpContext context, IGameService game) =>
|
group.MapPut("/skill-groups/{skillGroupId:guid}", (Guid skillGroupId, UpdateSkillGroupRequest request, HttpContext context, IGameService game) =>
|
||||||
{
|
{
|
||||||
var result = game.UpdateSkillGroup(context.GetRequiredSessionToken(), skillGroupId, request.Name, request.DiceRollDefinition, request.WildDice, request.AllowFumble);
|
var result = game.UpdateSkillGroup(context.GetRequiredSessionToken(), skillGroupId, request.Name, request.DiceRollDefinition, request.WildDice, request.AllowFumble, request.FumbleRange);
|
||||||
return ApiResultMapper.ToApiResult(result);
|
return ApiResultMapper.ToApiResult(result);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ public sealed class SkillFormModel
|
|||||||
public string SkillGroupId { get; set; } = string.Empty;
|
public string SkillGroupId { get; set; } = string.Empty;
|
||||||
public int WildDice { get; set; }
|
public int WildDice { get; set; }
|
||||||
public bool AllowFumble { get; set; }
|
public bool AllowFumble { get; set; }
|
||||||
|
public int? FumbleRange { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class SkillGroupFormModel
|
public sealed class SkillGroupFormModel
|
||||||
@@ -54,6 +55,7 @@ public sealed class SkillGroupFormModel
|
|||||||
public string DiceRollDefinition { get; set; } = string.Empty;
|
public string DiceRollDefinition { get; set; } = string.Empty;
|
||||||
public int WildDice { get; set; }
|
public int WildDice { get; set; }
|
||||||
public bool AllowFumble { get; set; }
|
public bool AllowFumble { get; set; }
|
||||||
|
public int? FumbleRange { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum HomeViewMode
|
public enum HomeViewMode
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ public partial class CharacterPanel
|
|||||||
DiceRollDefinition = selectedGroup?.DiceRollDefinition ?? string.Empty,
|
DiceRollDefinition = selectedGroup?.DiceRollDefinition ?? string.Empty,
|
||||||
SkillGroupId = selectedGroup?.Id.ToString() ?? string.Empty,
|
SkillGroupId = selectedGroup?.Id.ToString() ?? string.Empty,
|
||||||
WildDice = selectedGroup?.WildDice ?? (IsD6 ? 1 : 0),
|
WildDice = selectedGroup?.WildDice ?? (IsD6 ? 1 : 0),
|
||||||
AllowFumble = selectedGroup?.AllowFumble ?? IsD6
|
AllowFumble = selectedGroup?.AllowFumble ?? IsD6,
|
||||||
|
FumbleRange = selectedGroup?.FumbleRange
|
||||||
};
|
};
|
||||||
|
|
||||||
CreateSkillFormVersion++;
|
CreateSkillFormVersion++;
|
||||||
@@ -35,7 +36,8 @@ public partial class CharacterPanel
|
|||||||
DiceRollDefinition = skill.DiceRollDefinition,
|
DiceRollDefinition = skill.DiceRollDefinition,
|
||||||
SkillGroupId = skill.SkillGroupId?.ToString() ?? string.Empty,
|
SkillGroupId = skill.SkillGroupId?.ToString() ?? string.Empty,
|
||||||
WildDice = skill.WildDice,
|
WildDice = skill.WildDice,
|
||||||
AllowFumble = skill.AllowFumble
|
AllowFumble = skill.AllowFumble,
|
||||||
|
FumbleRange = skill.FumbleRange
|
||||||
};
|
};
|
||||||
|
|
||||||
EditSkillFormVersion++;
|
EditSkillFormVersion++;
|
||||||
@@ -99,6 +101,7 @@ public partial class CharacterPanel
|
|||||||
SkillGroupState.Model.DiceRollDefinition = string.Empty;
|
SkillGroupState.Model.DiceRollDefinition = string.Empty;
|
||||||
SkillGroupState.Model.WildDice = IsD6 ? 1 : 0;
|
SkillGroupState.Model.WildDice = IsD6 ? 1 : 0;
|
||||||
SkillGroupState.Model.AllowFumble = IsD6;
|
SkillGroupState.Model.AllowFumble = IsD6;
|
||||||
|
SkillGroupState.Model.FumbleRange = null;
|
||||||
SkillGroupState.ResetValidation();
|
SkillGroupState.ResetValidation();
|
||||||
ShowCreateSkillGroupModal = true;
|
ShowCreateSkillGroupModal = true;
|
||||||
}
|
}
|
||||||
@@ -110,6 +113,7 @@ public partial class CharacterPanel
|
|||||||
SkillGroupState.Model.DiceRollDefinition = skillGroup.DiceRollDefinition;
|
SkillGroupState.Model.DiceRollDefinition = skillGroup.DiceRollDefinition;
|
||||||
SkillGroupState.Model.WildDice = skillGroup.WildDice;
|
SkillGroupState.Model.WildDice = skillGroup.WildDice;
|
||||||
SkillGroupState.Model.AllowFumble = skillGroup.AllowFumble;
|
SkillGroupState.Model.AllowFumble = skillGroup.AllowFumble;
|
||||||
|
SkillGroupState.Model.FumbleRange = skillGroup.FumbleRange;
|
||||||
SkillGroupState.ResetValidation();
|
SkillGroupState.ResetValidation();
|
||||||
ShowEditSkillGroupModal = true;
|
ShowEditSkillGroupModal = true;
|
||||||
}
|
}
|
||||||
@@ -155,7 +159,8 @@ public partial class CharacterPanel
|
|||||||
SkillGroupState.Model.Name.Trim(),
|
SkillGroupState.Model.Name.Trim(),
|
||||||
SkillGroupState.Model.DiceRollDefinition.Trim(),
|
SkillGroupState.Model.DiceRollDefinition.Trim(),
|
||||||
SkillGroupState.Model.WildDice,
|
SkillGroupState.Model.WildDice,
|
||||||
SkillGroupState.Model.AllowFumble));
|
SkillGroupState.Model.AllowFumble,
|
||||||
|
SkillGroupState.Model.FumbleRange));
|
||||||
CloseSkillGroupModals();
|
CloseSkillGroupModals();
|
||||||
await SkillGroupCreated.InvokeAsync(createdGroup.Id);
|
await SkillGroupCreated.InvokeAsync(createdGroup.Id);
|
||||||
}
|
}
|
||||||
@@ -202,7 +207,8 @@ public partial class CharacterPanel
|
|||||||
SkillGroupState.Model.Name.Trim(),
|
SkillGroupState.Model.Name.Trim(),
|
||||||
SkillGroupState.Model.DiceRollDefinition.Trim(),
|
SkillGroupState.Model.DiceRollDefinition.Trim(),
|
||||||
SkillGroupState.Model.WildDice,
|
SkillGroupState.Model.WildDice,
|
||||||
SkillGroupState.Model.AllowFumble));
|
SkillGroupState.Model.AllowFumble,
|
||||||
|
SkillGroupState.Model.FumbleRange));
|
||||||
CloseSkillGroupModals();
|
CloseSkillGroupModals();
|
||||||
await SkillGroupUpdated.InvokeAsync(updatedGroup.Id);
|
await SkillGroupUpdated.InvokeAsync(updatedGroup.Id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ public partial class SkillFormModal
|
|||||||
FormState.Model.SkillGroupId = InitialModel.SkillGroupId;
|
FormState.Model.SkillGroupId = InitialModel.SkillGroupId;
|
||||||
FormState.Model.WildDice = InitialModel.WildDice;
|
FormState.Model.WildDice = InitialModel.WildDice;
|
||||||
FormState.Model.AllowFumble = InitialModel.AllowFumble;
|
FormState.Model.AllowFumble = InitialModel.AllowFumble;
|
||||||
|
FormState.Model.FumbleRange = InitialModel.FumbleRange;
|
||||||
FormState.ResetValidation();
|
FormState.ResetValidation();
|
||||||
AppliedFormVersion = FormVersion;
|
AppliedFormVersion = FormVersion;
|
||||||
PendingNameFocus = AutoFocusName;
|
PendingNameFocus = AutoFocusName;
|
||||||
@@ -66,7 +67,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));
|
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
|
||||||
{
|
{
|
||||||
@@ -76,7 +77,7 @@ public partial class SkillFormModal
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
skill = await ApiClient.RequestAsync<SkillSummary>("POST", $"/api/characters/{SelectedCharacterId.Value}/skills", new CreateSkillRequest(FormState.Model.Name.Trim(), FormState.Model.DiceRollDefinition.Trim(), FormState.Model.WildDice, FormState.Model.AllowFumble, skillGroupId));
|
skill = await ApiClient.RequestAsync<SkillSummary>("POST", $"/api/characters/{SelectedCharacterId.Value}/skills", new CreateSkillRequest(FormState.Model.Name.Trim(), FormState.Model.DiceRollDefinition.Trim(), FormState.Model.WildDice, FormState.Model.AllowFumble, skillGroupId, FormState.Model.FumbleRange));
|
||||||
}
|
}
|
||||||
|
|
||||||
await SkillSaved.InvokeAsync(skill.Id);
|
await SkillSaved.InvokeAsync(skill.Id);
|
||||||
|
|||||||
@@ -34,17 +34,17 @@ public sealed record UpdateCharacterRequest(string Name, Guid? CampaignId, strin
|
|||||||
|
|
||||||
public sealed record CharacterSummary(Guid Id, string Name, Guid OwnerUserId, Guid? CampaignId, string OwnerDisplayName);
|
public sealed record CharacterSummary(Guid Id, string Name, Guid OwnerUserId, Guid? CampaignId, string OwnerDisplayName);
|
||||||
|
|
||||||
public sealed record CreateSkillRequest(string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, Guid? SkillGroupId = null);
|
public sealed record CreateSkillRequest(string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, Guid? SkillGroupId = null, int? FumbleRange = null);
|
||||||
|
|
||||||
public sealed record UpdateSkillRequest(string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, Guid? SkillGroupId = null);
|
public sealed record UpdateSkillRequest(string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, Guid? SkillGroupId = null, int? FumbleRange = null);
|
||||||
|
|
||||||
public sealed record SkillGroupSummary(Guid Id, Guid CharacterId, string Name, string DiceRollDefinition, int WildDice, bool AllowFumble);
|
public sealed record SkillGroupSummary(Guid Id, Guid CharacterId, string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, int? FumbleRange);
|
||||||
|
|
||||||
public sealed record CreateSkillGroupRequest(string Name, string DiceRollDefinition, int WildDice, bool AllowFumble);
|
public sealed record CreateSkillGroupRequest(string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, int? FumbleRange = null);
|
||||||
|
|
||||||
public sealed record UpdateSkillGroupRequest(string Name, string DiceRollDefinition, int WildDice, bool AllowFumble);
|
public sealed record UpdateSkillGroupRequest(string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, int? FumbleRange = null);
|
||||||
|
|
||||||
public sealed record SkillSummary(Guid Id, Guid CharacterId, Guid? SkillGroupId, string Name, string DiceRollDefinition, int WildDice, bool AllowFumble);
|
public sealed record SkillSummary(Guid Id, Guid CharacterId, Guid? SkillGroupId, string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, int? FumbleRange);
|
||||||
|
|
||||||
public sealed record RollSkillRequest(string Visibility);
|
public sealed record RollSkillRequest(string Visibility);
|
||||||
|
|
||||||
@@ -52,9 +52,9 @@ public sealed record RollDieResult(int Roll, bool Crit, bool Fumble, bool Wild,
|
|||||||
|
|
||||||
public sealed record RollResult(Guid RollId, Guid CampaignId, Guid CharacterId, Guid SkillId, Guid RollerUserId, string Visibility, int Result, string Breakdown, IReadOnlyList<RollDieResult> Dice, DateTimeOffset TimestampUtc);
|
public sealed record RollResult(Guid RollId, Guid CampaignId, Guid CharacterId, Guid SkillId, Guid RollerUserId, string Visibility, int Result, string Breakdown, IReadOnlyList<RollDieResult> Dice, DateTimeOffset TimestampUtc);
|
||||||
|
|
||||||
public sealed record CharacterSheetSkillGroup(Guid Id, string Name, string DiceRollDefinition, int WildDice, bool AllowFumble);
|
public sealed record CharacterSheetSkillGroup(Guid Id, string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, int? FumbleRange);
|
||||||
|
|
||||||
public sealed record CharacterSheetSkill(Guid Id, Guid? SkillGroupId, string Name, string DiceRollDefinition, int WildDice, bool AllowFumble);
|
public sealed record CharacterSheetSkill(Guid Id, Guid? SkillGroupId, string Name, string DiceRollDefinition, int WildDice, bool AllowFumble, int? FumbleRange);
|
||||||
|
|
||||||
public sealed record CharacterSheet(Guid CharacterId, CharacterSheetSkillGroup[] SkillGroups, CharacterSheetSkill[] Skills);
|
public sealed record CharacterSheet(Guid CharacterId, CharacterSheetSkillGroup[] SkillGroups, CharacterSheetSkill[] Skills);
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ public sealed class RpgRollerDbContext : DbContext
|
|||||||
entity.Property(x => x.DiceRollDefinition).IsRequired().HasMaxLength(128);
|
entity.Property(x => x.DiceRollDefinition).IsRequired().HasMaxLength(128);
|
||||||
entity.Property(x => x.WildDice).IsRequired();
|
entity.Property(x => x.WildDice).IsRequired();
|
||||||
entity.Property(x => x.AllowFumble).IsRequired();
|
entity.Property(x => x.AllowFumble).IsRequired();
|
||||||
|
entity.Property(x => x.FumbleRange).IsRequired(false);
|
||||||
entity.HasIndex(x => x.CharacterId);
|
entity.HasIndex(x => x.CharacterId);
|
||||||
entity.HasIndex(x => x.SkillGroupId);
|
entity.HasIndex(x => x.SkillGroupId);
|
||||||
});
|
});
|
||||||
@@ -65,6 +66,7 @@ public sealed class RpgRollerDbContext : DbContext
|
|||||||
entity.Property(x => x.DiceRollDefinition).IsRequired().HasMaxLength(128);
|
entity.Property(x => x.DiceRollDefinition).IsRequired().HasMaxLength(128);
|
||||||
entity.Property(x => x.WildDice).IsRequired();
|
entity.Property(x => x.WildDice).IsRequired();
|
||||||
entity.Property(x => x.AllowFumble).IsRequired();
|
entity.Property(x => x.AllowFumble).IsRequired();
|
||||||
|
entity.Property(x => x.FumbleRange).IsRequired(false);
|
||||||
entity.HasIndex(x => x.CharacterId);
|
entity.HasIndex(x => x.CharacterId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ public sealed class SkillGroup
|
|||||||
public required string DiceRollDefinition { get; set; }
|
public required string DiceRollDefinition { get; set; }
|
||||||
public required int WildDice { get; set; }
|
public required int WildDice { get; set; }
|
||||||
public required bool AllowFumble { get; set; }
|
public required bool AllowFumble { get; set; }
|
||||||
|
public int? FumbleRange { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class Skill
|
public sealed class Skill
|
||||||
@@ -72,6 +73,7 @@ public sealed class Skill
|
|||||||
public required string DiceRollDefinition { get; set; }
|
public required string DiceRollDefinition { get; set; }
|
||||||
public required int WildDice { get; set; }
|
public required int WildDice { get; set; }
|
||||||
public required bool AllowFumble { get; set; }
|
public required bool AllowFumble { get; set; }
|
||||||
|
public int? FumbleRange { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class RollLogEntry
|
public sealed class RollLogEntry
|
||||||
|
|||||||
264
RpgRoller/Migrations/20260402222501_AddRolemasterFumbleRange.Designer.cs
generated
Normal file
264
RpgRoller/Migrations/20260402222501_AddRolemasterFumbleRange.Designer.cs
generated
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using RpgRoller.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace RpgRoller.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(RpgRollerDbContext))]
|
||||||
|
[Migration("20260402222501_AddRolemasterFumbleRange")]
|
||||||
|
partial class AddRolemasterFumbleRange
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.2");
|
||||||
|
|
||||||
|
modelBuilder.Entity("RpgRoller.Domain.Campaign", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("GmUserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Ruleset")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<long>("Version")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("GmUserId");
|
||||||
|
|
||||||
|
b.ToTable("Campaigns");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("RpgRoller.Domain.Character", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid?>("CampaignId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("OwnerUserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CampaignId");
|
||||||
|
|
||||||
|
b.HasIndex("OwnerUserId");
|
||||||
|
|
||||||
|
b.ToTable("Characters");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("RpgRoller.Domain.RollLogEntry", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Breakdown")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("CampaignId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("CharacterId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Dice")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Result")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("RollerUserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("SkillId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("TimestampUtc")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Visibility")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CampaignId");
|
||||||
|
|
||||||
|
b.HasIndex("CharacterId");
|
||||||
|
|
||||||
|
b.HasIndex("RollerUserId");
|
||||||
|
|
||||||
|
b.HasIndex("SkillId");
|
||||||
|
|
||||||
|
b.ToTable("RollLogEntries");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("RpgRoller.Domain.Skill", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("AllowFumble")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("CharacterId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DiceRollDefinition")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("FumbleRange")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid?>("SkillGroupId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("WildDice")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CharacterId");
|
||||||
|
|
||||||
|
b.HasIndex("SkillGroupId");
|
||||||
|
|
||||||
|
b.ToTable("Skills");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("RpgRoller.Domain.SkillGroup", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<bool>("AllowFumble")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("CharacterId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DiceRollDefinition")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("FumbleRange")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("WildDice")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CharacterId");
|
||||||
|
|
||||||
|
b.ToTable("SkillGroups");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("RpgRoller.Domain.UserAccount", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid?>("ActiveCharacterId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Roles")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Username")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("UsernameNormalized")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UsernameNormalized")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("RpgRoller.Domain.UserSession", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Token")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset>("CreatedAtUtc")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("UserId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Token");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Sessions");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace RpgRoller.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddRolemasterFumbleRange : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "FumbleRange",
|
||||||
|
table: "Skills",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "FumbleRange",
|
||||||
|
table: "SkillGroups",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "FumbleRange",
|
||||||
|
table: "Skills");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "FumbleRange",
|
||||||
|
table: "SkillGroups");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -138,6 +138,9 @@ namespace RpgRoller.Migrations
|
|||||||
.HasMaxLength(128)
|
.HasMaxLength(128)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("FumbleRange")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(128)
|
.HasMaxLength(128)
|
||||||
@@ -175,6 +178,9 @@ namespace RpgRoller.Migrations
|
|||||||
.HasMaxLength(128)
|
.HasMaxLength(128)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int?>("FumbleRange")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("Name")
|
b.Property<string>("Name")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(128)
|
.HasMaxLength(128)
|
||||||
|
|||||||
@@ -496,7 +496,7 @@ public sealed class GameService : IGameService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<SkillGroupSummary> CreateSkillGroup(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble)
|
public ServiceResult<SkillGroupSummary> CreateSkillGroup(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
return ServiceResult<SkillGroupSummary>.Failure("invalid_skill_group_name", "Skill group name is required.");
|
return ServiceResult<SkillGroupSummary>.Failure("invalid_skill_group_name", "Skill group name is required.");
|
||||||
@@ -516,7 +516,7 @@ public sealed class GameService : IGameService
|
|||||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||||
return ServiceResult<SkillGroupSummary>.Failure("forbidden", "Only the owner or GM can manage skill groups.");
|
return ServiceResult<SkillGroupSummary>.Failure("forbidden", "Only the owner or GM can manage skill groups.");
|
||||||
|
|
||||||
var prototypeValidation = ValidateSkillDefinition(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble);
|
var prototypeValidation = ValidateSkillDefinition(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||||
if (!prototypeValidation.Succeeded)
|
if (!prototypeValidation.Succeeded)
|
||||||
return ServiceResult<SkillGroupSummary>.Failure(prototypeValidation.Error!.Code, prototypeValidation.Error.Message);
|
return ServiceResult<SkillGroupSummary>.Failure(prototypeValidation.Error!.Code, prototypeValidation.Error.Message);
|
||||||
|
|
||||||
@@ -527,7 +527,8 @@ public sealed class GameService : IGameService
|
|||||||
Name = name.Trim(),
|
Name = name.Trim(),
|
||||||
DiceRollDefinition = prototypeValidation.Value!.CanonicalExpression,
|
DiceRollDefinition = prototypeValidation.Value!.CanonicalExpression,
|
||||||
WildDice = prototypeValidation.Value.WildDice,
|
WildDice = prototypeValidation.Value.WildDice,
|
||||||
AllowFumble = prototypeValidation.Value.AllowFumble
|
AllowFumble = prototypeValidation.Value.AllowFumble,
|
||||||
|
FumbleRange = prototypeValidation.Value.FumbleRange
|
||||||
};
|
};
|
||||||
|
|
||||||
m_SkillGroupsById[group.Id] = group;
|
m_SkillGroupsById[group.Id] = group;
|
||||||
@@ -538,7 +539,7 @@ public sealed class GameService : IGameService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<SkillGroupSummary> UpdateSkillGroup(string sessionToken, Guid skillGroupId, string name, string diceRollDefinition, int wildDice, bool allowFumble)
|
public ServiceResult<SkillGroupSummary> UpdateSkillGroup(string sessionToken, Guid skillGroupId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
return ServiceResult<SkillGroupSummary>.Failure("invalid_skill_group_name", "Skill group name is required.");
|
return ServiceResult<SkillGroupSummary>.Failure("invalid_skill_group_name", "Skill group name is required.");
|
||||||
@@ -559,7 +560,7 @@ public sealed class GameService : IGameService
|
|||||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||||
return ServiceResult<SkillGroupSummary>.Failure("forbidden", "Only the owner or GM can manage skill groups.");
|
return ServiceResult<SkillGroupSummary>.Failure("forbidden", "Only the owner or GM can manage skill groups.");
|
||||||
|
|
||||||
var prototypeValidation = ValidateSkillDefinition(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble);
|
var prototypeValidation = ValidateSkillDefinition(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||||
if (!prototypeValidation.Succeeded)
|
if (!prototypeValidation.Succeeded)
|
||||||
return ServiceResult<SkillGroupSummary>.Failure(prototypeValidation.Error!.Code, prototypeValidation.Error.Message);
|
return ServiceResult<SkillGroupSummary>.Failure(prototypeValidation.Error!.Code, prototypeValidation.Error.Message);
|
||||||
|
|
||||||
@@ -567,6 +568,7 @@ public sealed class GameService : IGameService
|
|||||||
group.DiceRollDefinition = prototypeValidation.Value!.CanonicalExpression;
|
group.DiceRollDefinition = prototypeValidation.Value!.CanonicalExpression;
|
||||||
group.WildDice = prototypeValidation.Value.WildDice;
|
group.WildDice = prototypeValidation.Value.WildDice;
|
||||||
group.AllowFumble = prototypeValidation.Value.AllowFumble;
|
group.AllowFumble = prototypeValidation.Value.AllowFumble;
|
||||||
|
group.FumbleRange = prototypeValidation.Value.FumbleRange;
|
||||||
TouchCharacterLocked(campaign.Id, character.Id);
|
TouchCharacterLocked(campaign.Id, character.Id);
|
||||||
|
|
||||||
PersistStateLocked();
|
PersistStateLocked();
|
||||||
@@ -603,7 +605,7 @@ public sealed class GameService : IGameService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null)
|
public ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
return ServiceResult<SkillSummary>.Failure("invalid_skill_name", "Skill name is required.");
|
return ServiceResult<SkillSummary>.Failure("invalid_skill_name", "Skill name is required.");
|
||||||
@@ -623,7 +625,7 @@ public sealed class GameService : IGameService
|
|||||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||||
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
||||||
|
|
||||||
var skillValidation = ValidateSkillDefinition(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble);
|
var skillValidation = ValidateSkillDefinition(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||||
if (!skillValidation.Succeeded)
|
if (!skillValidation.Succeeded)
|
||||||
return ServiceResult<SkillSummary>.Failure(skillValidation.Error!.Code, skillValidation.Error.Message);
|
return ServiceResult<SkillSummary>.Failure(skillValidation.Error!.Code, skillValidation.Error.Message);
|
||||||
|
|
||||||
@@ -639,7 +641,8 @@ public sealed class GameService : IGameService
|
|||||||
Name = name.Trim(),
|
Name = name.Trim(),
|
||||||
DiceRollDefinition = skillValidation.Value!.CanonicalExpression,
|
DiceRollDefinition = skillValidation.Value!.CanonicalExpression,
|
||||||
WildDice = skillValidation.Value.WildDice,
|
WildDice = skillValidation.Value.WildDice,
|
||||||
AllowFumble = skillValidation.Value.AllowFumble
|
AllowFumble = skillValidation.Value.AllowFumble,
|
||||||
|
FumbleRange = skillValidation.Value.FumbleRange
|
||||||
};
|
};
|
||||||
|
|
||||||
m_SkillsById[skill.Id] = skill;
|
m_SkillsById[skill.Id] = skill;
|
||||||
@@ -650,7 +653,7 @@ public sealed class GameService : IGameService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null)
|
public ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(name))
|
if (string.IsNullOrWhiteSpace(name))
|
||||||
return ServiceResult<SkillSummary>.Failure("invalid_skill_name", "Skill name is required.");
|
return ServiceResult<SkillSummary>.Failure("invalid_skill_name", "Skill name is required.");
|
||||||
@@ -671,7 +674,7 @@ public sealed class GameService : IGameService
|
|||||||
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
if (!CanEditCharacterLocked(user.Id, character, campaign))
|
||||||
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
return ServiceResult<SkillSummary>.Failure("forbidden", "Only the owner or GM can edit skills.");
|
||||||
|
|
||||||
var skillValidation = ValidateSkillDefinition(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble);
|
var skillValidation = ValidateSkillDefinition(campaign.Ruleset, diceRollDefinition, wildDice, allowFumble, fumbleRange);
|
||||||
if (!skillValidation.Succeeded)
|
if (!skillValidation.Succeeded)
|
||||||
return ServiceResult<SkillSummary>.Failure(skillValidation.Error!.Code, skillValidation.Error.Message);
|
return ServiceResult<SkillSummary>.Failure(skillValidation.Error!.Code, skillValidation.Error.Message);
|
||||||
|
|
||||||
@@ -683,6 +686,7 @@ public sealed class GameService : IGameService
|
|||||||
skill.DiceRollDefinition = skillValidation.Value!.CanonicalExpression;
|
skill.DiceRollDefinition = skillValidation.Value!.CanonicalExpression;
|
||||||
skill.WildDice = skillValidation.Value.WildDice;
|
skill.WildDice = skillValidation.Value.WildDice;
|
||||||
skill.AllowFumble = skillValidation.Value.AllowFumble;
|
skill.AllowFumble = skillValidation.Value.AllowFumble;
|
||||||
|
skill.FumbleRange = skillValidation.Value.FumbleRange;
|
||||||
skill.SkillGroupId = resolvedSkillGroupId.Value;
|
skill.SkillGroupId = resolvedSkillGroupId.Value;
|
||||||
TouchCharacterLocked(campaign.Id, character.Id);
|
TouchCharacterLocked(campaign.Id, character.Id);
|
||||||
|
|
||||||
@@ -876,33 +880,64 @@ public sealed class GameService : IGameService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble)> ValidateSkillDefinition(RulesetKind ruleset, string diceRollDefinition, int wildDice, bool allowFumble)
|
private static ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)> ValidateSkillDefinition(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)
|
||||||
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble)>.Failure(expressionValidation.Error!.Code, expressionValidation.Error.Message);
|
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)>.Failure(expressionValidation.Error!.Code, expressionValidation.Error.Message);
|
||||||
|
|
||||||
var optionsValidation = ValidateSkillOptions(ruleset, wildDice, allowFumble);
|
var optionsValidation = ValidateSkillOptions(ruleset, expressionValidation.Value!, wildDice, allowFumble, fumbleRange);
|
||||||
if (!optionsValidation.Succeeded)
|
if (!optionsValidation.Succeeded)
|
||||||
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble)>.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)>.Success((expressionValidation.Value!.Canonical, optionsValidation.Value!.WildDice, optionsValidation.Value.AllowFumble));
|
return ServiceResult<(string CanonicalExpression, int WildDice, bool AllowFumble, int? FumbleRange)>.Success((expressionValidation.Value!.Canonical, optionsValidation.Value!.WildDice, optionsValidation.Value.AllowFumble, optionsValidation.Value.FumbleRange));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ServiceResult<(int WildDice, bool AllowFumble)> ValidateSkillOptions(RulesetKind ruleset, int wildDice, bool allowFumble)
|
private static ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)> ValidateSkillOptions(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)>.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.");
|
||||||
|
|
||||||
if (ruleset == RulesetKind.D6)
|
if (ruleset == RulesetKind.D6)
|
||||||
{
|
{
|
||||||
if (wildDice < 1)
|
if (wildDice < 1)
|
||||||
return ServiceResult<(int WildDice, bool AllowFumble)>.Failure("invalid_wild_dice", "D6 skills require at least one wild die.");
|
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_wild_dice", "D6 skills require at least one wild die.");
|
||||||
|
|
||||||
return ServiceResult<(int WildDice, bool AllowFumble)>.Success((wildDice, allowFumble));
|
if (fumbleRange.HasValue)
|
||||||
|
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_fumble_range", "Fumble range is only supported for Rolemaster open-ended percentile skills.");
|
||||||
|
|
||||||
|
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Success((wildDice, allowFumble, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ServiceResult<(int WildDice, bool AllowFumble)>.Success((0, false));
|
if (ruleset == RulesetKind.Rolemaster)
|
||||||
|
{
|
||||||
|
if (wildDice != 0)
|
||||||
|
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_wild_dice", "Rolemaster skills do not support wild dice.");
|
||||||
|
|
||||||
|
if (allowFumble)
|
||||||
|
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_allow_fumble", "Rolemaster skills do not use the D6 allow-fumble option.");
|
||||||
|
|
||||||
|
if (expression.Kind == DiceExpressionKind.RolemasterOpenEndedPercentile)
|
||||||
|
{
|
||||||
|
if (!fumbleRange.HasValue)
|
||||||
|
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_fumble_range", "Rolemaster open-ended percentile skills require a fumble range.");
|
||||||
|
|
||||||
|
if (fumbleRange < 0 || fumbleRange >= 96)
|
||||||
|
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_fumble_range", "Fumble range must be between 0 and 95.");
|
||||||
|
|
||||||
|
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Success((0, false, fumbleRange));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fumbleRange.HasValue)
|
||||||
|
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_fumble_range", "Fumble range is only valid for Rolemaster open-ended percentile skills.");
|
||||||
|
|
||||||
|
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Success((0, false, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fumbleRange.HasValue)
|
||||||
|
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Failure("invalid_fumble_range", "Fumble range is only supported for Rolemaster open-ended percentile skills.");
|
||||||
|
|
||||||
|
return ServiceResult<(int WildDice, bool AllowFumble, int? FumbleRange)>.Success((0, false, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
private (int Total, string Breakdown, IReadOnlyList<RollDieResult> Dice) ComputeRoll(RulesetKind ruleset, DiceExpression expression, Skill skill)
|
private (int Total, string Breakdown, IReadOnlyList<RollDieResult> Dice) ComputeRoll(RulesetKind ruleset, DiceExpression expression, Skill skill)
|
||||||
@@ -1139,22 +1174,22 @@ public sealed class GameService : IGameService
|
|||||||
|
|
||||||
private static CharacterSheetSkillGroup ToCharacterSheetSkillGroup(SkillGroup skillGroup)
|
private static CharacterSheetSkillGroup ToCharacterSheetSkillGroup(SkillGroup skillGroup)
|
||||||
{
|
{
|
||||||
return new(skillGroup.Id, skillGroup.Name, skillGroup.DiceRollDefinition, skillGroup.WildDice, skillGroup.AllowFumble);
|
return new(skillGroup.Id, skillGroup.Name, skillGroup.DiceRollDefinition, skillGroup.WildDice, skillGroup.AllowFumble, skillGroup.FumbleRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SkillGroupSummary ToSkillGroupSummary(SkillGroup skillGroup)
|
private static SkillGroupSummary ToSkillGroupSummary(SkillGroup skillGroup)
|
||||||
{
|
{
|
||||||
return new(skillGroup.Id, skillGroup.CharacterId, skillGroup.Name, skillGroup.DiceRollDefinition, skillGroup.WildDice, skillGroup.AllowFumble);
|
return new(skillGroup.Id, skillGroup.CharacterId, skillGroup.Name, skillGroup.DiceRollDefinition, skillGroup.WildDice, skillGroup.AllowFumble, skillGroup.FumbleRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CharacterSheetSkill ToCharacterSheetSkill(Skill skill)
|
private static CharacterSheetSkill ToCharacterSheetSkill(Skill skill)
|
||||||
{
|
{
|
||||||
return new(skill.Id, skill.SkillGroupId, skill.Name, skill.DiceRollDefinition, skill.WildDice, skill.AllowFumble);
|
return new(skill.Id, skill.SkillGroupId, skill.Name, skill.DiceRollDefinition, skill.WildDice, skill.AllowFumble, skill.FumbleRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SkillSummary ToSkillSummary(Skill skill)
|
private static SkillSummary ToSkillSummary(Skill skill)
|
||||||
{
|
{
|
||||||
return new(skill.Id, skill.CharacterId, skill.SkillGroupId, skill.Name, skill.DiceRollDefinition, skill.WildDice, skill.AllowFumble);
|
return new(skill.Id, skill.CharacterId, skill.SkillGroupId, skill.Name, skill.DiceRollDefinition, skill.WildDice, skill.AllowFumble, skill.FumbleRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RollResult ToRollResult(RollLogEntry entry, IReadOnlyList<RollDieResult> dice)
|
private static RollResult ToRollResult(RollLogEntry entry, IReadOnlyList<RollDieResult> dice)
|
||||||
@@ -1658,7 +1693,8 @@ public sealed class GameService : IGameService
|
|||||||
Name = skill.Name,
|
Name = skill.Name,
|
||||||
DiceRollDefinition = skill.DiceRollDefinition,
|
DiceRollDefinition = skill.DiceRollDefinition,
|
||||||
WildDice = skill.WildDice,
|
WildDice = skill.WildDice,
|
||||||
AllowFumble = skill.AllowFumble
|
AllowFumble = skill.AllowFumble,
|
||||||
|
FumbleRange = skill.FumbleRange
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1671,7 +1707,8 @@ public sealed class GameService : IGameService
|
|||||||
Name = skillGroup.Name,
|
Name = skillGroup.Name,
|
||||||
DiceRollDefinition = skillGroup.DiceRollDefinition,
|
DiceRollDefinition = skillGroup.DiceRollDefinition,
|
||||||
WildDice = skillGroup.WildDice,
|
WildDice = skillGroup.WildDice,
|
||||||
AllowFumble = skillGroup.AllowFumble
|
AllowFumble = skillGroup.AllowFumble,
|
||||||
|
FumbleRange = skillGroup.FumbleRange
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ public interface IGameService
|
|||||||
ServiceResult<bool> ActivateCharacter(string sessionToken, Guid characterId);
|
ServiceResult<bool> ActivateCharacter(string sessionToken, Guid characterId);
|
||||||
ServiceResult<IReadOnlyList<CharacterSummary>> GetOwnCharacters(string sessionToken);
|
ServiceResult<IReadOnlyList<CharacterSummary>> GetOwnCharacters(string sessionToken);
|
||||||
|
|
||||||
ServiceResult<SkillGroupSummary> CreateSkillGroup(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble);
|
ServiceResult<SkillGroupSummary> CreateSkillGroup(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null);
|
||||||
ServiceResult<SkillGroupSummary> UpdateSkillGroup(string sessionToken, Guid skillGroupId, string name, string diceRollDefinition, int wildDice, bool allowFumble);
|
ServiceResult<SkillGroupSummary> UpdateSkillGroup(string sessionToken, Guid skillGroupId, string name, string diceRollDefinition, int wildDice, bool allowFumble, int? fumbleRange = null);
|
||||||
ServiceResult<bool> DeleteSkillGroup(string sessionToken, Guid skillGroupId);
|
ServiceResult<bool> DeleteSkillGroup(string sessionToken, Guid skillGroupId);
|
||||||
ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null);
|
ServiceResult<SkillSummary> CreateSkill(string sessionToken, Guid characterId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null);
|
||||||
ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null);
|
ServiceResult<SkillSummary> UpdateSkill(string sessionToken, Guid skillId, string name, string diceRollDefinition, int wildDice, bool allowFumble, Guid? skillGroupId = null, int? fumbleRange = null);
|
||||||
ServiceResult<bool> DeleteSkill(string sessionToken, Guid skillId);
|
ServiceResult<bool> DeleteSkill(string sessionToken, Guid skillId);
|
||||||
ServiceResult<CharacterSheet> GetCharacterSheet(string sessionToken, Guid characterId);
|
ServiceResult<CharacterSheet> GetCharacterSheet(string sessionToken, Guid characterId);
|
||||||
|
|
||||||
|
|||||||
@@ -900,6 +900,16 @@
|
|||||||
},
|
},
|
||||||
"allowFumble": {
|
"allowFumble": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"skillGroupId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"fumbleRange": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"nullable": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -924,6 +934,16 @@
|
|||||||
},
|
},
|
||||||
"allowFumble": {
|
"allowFumble": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"skillGroupId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"fumbleRange": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"nullable": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -944,6 +964,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"format": "uuid"
|
"format": "uuid"
|
||||||
},
|
},
|
||||||
|
"skillGroupId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -956,6 +981,11 @@
|
|||||||
},
|
},
|
||||||
"allowFumble": {
|
"allowFumble": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"fumbleRange": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int32",
|
||||||
|
"nullable": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
Reference in New Issue
Block a user