Implement phase 6 critical effect normalization

This commit is contained in:
2026-03-14 11:31:13 +01:00
parent 35c250666f
commit 521f0ff8d5
29 changed files with 932 additions and 55 deletions

View File

@@ -1,6 +1,8 @@
using Microsoft.EntityFrameworkCore;
using RolemasterDb.App.Data;
using RolemasterDb.App.Domain;
using RolemasterDb.App.Features;
using RolemasterDb.ImportTool.Parsing;
namespace RolemasterDb.ImportTool.Tests;
@@ -303,7 +305,112 @@ public sealed class StandardCriticalTableParserIntegrationTests
}
[Fact]
public async Task Loader_upgrades_existing_sqlite_and_persists_branch_rows()
public async Task Slash_base_affixes_are_normalized_into_effects()
{
var entry = LoadManifest().Tables.Single(item => string.Equals(item.Slug, "slash", StringComparison.Ordinal));
var parseResult = await LoadParseResultAsync(entry);
var result = parseResult.Table.Results.Single(item =>
item.GroupKey is null &&
string.Equals(item.RollBandLabel, "51-55", StringComparison.Ordinal) &&
string.Equals(item.ColumnKey, "D", StringComparison.Ordinal));
Assert.Equal("+5H π 3∫ (-15)", result.RawAffixText);
Assert.Collection(
result.Effects,
effect =>
{
Assert.Equal(CriticalEffectCodes.DirectHits, effect.EffectCode);
Assert.Equal(5, effect.ValueInteger);
},
effect =>
{
Assert.Equal(CriticalEffectCodes.MustParryRounds, effect.EffectCode);
Assert.Equal(1, effect.DurationRounds);
},
effect =>
{
Assert.Equal(CriticalEffectCodes.BleedPerRound, effect.EffectCode);
Assert.Equal(3, effect.PerRound);
},
effect =>
{
Assert.Equal(CriticalEffectCodes.FoePenalty, effect.EffectCode);
Assert.Equal(-15, effect.Modifier);
});
}
[Fact]
public async Task Slash_branch_affixes_are_normalized_into_effects()
{
var entry = LoadManifest().Tables.Single(item => string.Equals(item.Slug, "slash", StringComparison.Ordinal));
var parseResult = await LoadParseResultAsync(entry);
var result = parseResult.Table.Results.Single(item =>
item.GroupKey is null &&
string.Equals(item.RollBandLabel, "36-45", StringComparison.Ordinal) &&
string.Equals(item.ColumnKey, "B", StringComparison.Ordinal));
var withGreaves = result.Branches.Single(item => string.Equals(item.ConditionKey, "with_leg_greaves", StringComparison.Ordinal));
var withoutGreaves = result.Branches.Single(item => string.Equals(item.ConditionKey, "without_leg_greaves", StringComparison.Ordinal));
Assert.Collection(
withGreaves.Effects,
effect =>
{
Assert.Equal(CriticalEffectCodes.DirectHits, effect.EffectCode);
Assert.Equal(2, effect.ValueInteger);
},
effect =>
{
Assert.Equal(CriticalEffectCodes.MustParryRounds, effect.EffectCode);
Assert.Equal(1, effect.DurationRounds);
});
Assert.Collection(
withoutGreaves.Effects,
effect =>
{
Assert.Equal(CriticalEffectCodes.DirectHits, effect.EffectCode);
Assert.Equal(2, effect.ValueInteger);
},
effect =>
{
Assert.Equal(CriticalEffectCodes.BleedPerRound, effect.EffectCode);
Assert.Equal(1, effect.PerRound);
});
}
[Fact]
public async Task Mana_affixes_use_footer_legend_for_stun_and_powerpoint_modification()
{
var entry = LoadManifest().Tables.Single(item => string.Equals(item.Slug, "mana", StringComparison.Ordinal));
var parseResult = await LoadParseResultAsync(entry);
var result = parseResult.Table.Results.Single(item =>
item.GroupKey is null &&
string.Equals(item.RollBandLabel, "21-35", StringComparison.Ordinal) &&
string.Equals(item.ColumnKey, "C", StringComparison.Ordinal));
Assert.Equal("+8H -  - +(2d10-18)P", result.RawAffixText);
Assert.Collection(
result.Effects,
effect =>
{
Assert.Equal(CriticalEffectCodes.DirectHits, effect.EffectCode);
Assert.Equal(8, effect.ValueInteger);
},
effect =>
{
Assert.Equal(CriticalEffectCodes.StunnedRounds, effect.EffectCode);
Assert.Equal(1, effect.DurationRounds);
},
effect =>
{
Assert.Equal(CriticalEffectCodes.PowerPointModifier, effect.EffectCode);
Assert.Equal("2d10-18", effect.ValueExpression);
});
}
[Fact]
public async Task Loader_upgrades_existing_sqlite_and_persists_branch_rows_and_effects()
{
var entry = LoadManifest().Tables.Single(item => string.Equals(item.Slug, "slash", StringComparison.Ordinal));
var parseResult = await LoadParseResultAsync(entry);
@@ -318,6 +425,7 @@ public sealed class StandardCriticalTableParserIntegrationTests
.Include(item => item.CriticalColumn)
.Include(item => item.CriticalRollBand)
.Include(item => item.Branches)
.ThenInclude(item => item.Effects)
.SingleAsync(item =>
item.CriticalTable.Slug == "slash" &&
item.CriticalColumn.ColumnKey == "B" &&
@@ -327,6 +435,34 @@ public sealed class StandardCriticalTableParserIntegrationTests
Assert.Equal(2, result.Branches.Count);
Assert.Contains(result.Branches, item => item.ConditionKey == "with_leg_greaves" && item.RawAffixText == "+2H π");
Assert.Contains(result.Branches, item => item.ConditionKey == "without_leg_greaves" && item.RawAffixText == "+2H ∫");
Assert.Contains(result.Branches.SelectMany(item => item.Effects), item => item.EffectCode == CriticalEffectCodes.MustParryRounds);
Assert.Contains(result.Branches.SelectMany(item => item.Effects), item => item.EffectCode == CriticalEffectCodes.BleedPerRound);
}
[Fact]
public async Task Lookup_service_returns_effects_for_results_and_branches()
{
var slashEntry = LoadManifest().Tables.Single(item => string.Equals(item.Slug, "slash", StringComparison.Ordinal));
var manaEntry = LoadManifest().Tables.Single(item => string.Equals(item.Slug, "mana", StringComparison.Ordinal));
var slashParseResult = await LoadParseResultAsync(slashEntry);
var manaParseResult = await LoadParseResultAsync(manaEntry);
var databasePath = CreateTemporaryDatabaseCopy();
var loader = new CriticalImportLoader(databasePath);
await loader.LoadAsync(slashParseResult.Table);
await loader.LoadAsync(manaParseResult.Table);
var factory = CreateDbContextFactory(databasePath);
var lookupService = new LookupService(factory);
var slashResponse = await lookupService.LookupCriticalAsync(new CriticalLookupRequest("slash", "B", 40, null));
var manaResponse = await lookupService.LookupCriticalAsync(new CriticalLookupRequest("mana", "C", 30, null));
Assert.NotNull(slashResponse);
Assert.NotNull(manaResponse);
Assert.Contains(slashResponse!.Branches, branch => branch.ConditionKey == "with_leg_greaves" && branch.Effects.Any(effect => effect.EffectCode == CriticalEffectCodes.MustParryRounds));
Assert.Contains(slashResponse.Branches, branch => branch.ConditionKey == "without_leg_greaves" && branch.Effects.Any(effect => effect.EffectCode == CriticalEffectCodes.BleedPerRound));
Assert.Contains(manaResponse!.Effects, effect => effect.EffectCode == CriticalEffectCodes.PowerPointModifier && effect.ValueExpression == "2d10-18");
}
private static async Task<CriticalTableParseResult> LoadParseResultAsync(CriticalImportManifestEntry entry)
@@ -367,6 +503,15 @@ public sealed class StandardCriticalTableParserIntegrationTests
return new RolemasterDbContext(options);
}
private static IDbContextFactory<RolemasterDbContext> CreateDbContextFactory(string databasePath)
{
var options = new DbContextOptionsBuilder<RolemasterDbContext>()
.UseSqlite($"Data Source={databasePath}")
.Options;
return new TestRolemasterDbContextFactory(options);
}
private static string CreateTemporaryDatabaseCopy()
{
var databasePath = Path.Combine(GetArtifactCacheRoot(), $"rolemaster-{Guid.NewGuid():N}.db");

View File

@@ -0,0 +1,13 @@
using Microsoft.EntityFrameworkCore;
using RolemasterDb.App.Data;
namespace RolemasterDb.ImportTool.Tests;
internal sealed class TestRolemasterDbContextFactory(DbContextOptions<RolemasterDbContext> options) : IDbContextFactory<RolemasterDbContext>
{
public RolemasterDbContext CreateDbContext() => new(options);
public Task<RolemasterDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default) =>
Task.FromResult(CreateDbContext());
}