Implement phase 4 critical table imports

This commit is contained in:
2026-03-14 03:27:14 +01:00
parent a391a1421a
commit b2f61c3d73
17 changed files with 1280 additions and 474 deletions

View File

@@ -4,7 +4,7 @@ namespace RolemasterDb.ImportTool.Tests;
public sealed class StandardCriticalTableParserIntegrationTests
{
private static readonly string[] ExpectedPhase3Slugs =
private static readonly string[] ExpectedEnabledSlugs =
[
"arcane-aether",
"arcane-nether",
@@ -16,20 +16,25 @@ public sealed class StandardCriticalTableParserIntegrationTests
"heat",
"impact",
"krush",
"large_creature_magic",
"large_creature_weapon",
"ma-strikes",
"ma-sweeps",
"mana",
"puncture",
"slash",
"subdual",
"super_large_creature_weapon",
"tiny",
"unbalance"
];
private static readonly PdfXmlExtractor Extractor = new();
private static readonly StandardCriticalTableParser Parser = new();
private static readonly StandardCriticalTableParser StandardParser = new();
private static readonly VariantColumnCriticalTableParser VariantColumnParser = new();
private static readonly GroupedVariantCriticalTableParser GroupedVariantParser = new();
public static IEnumerable<object[]> EnabledStandardTables() =>
public static IEnumerable<object[]> EnabledTables() =>
LoadManifest().Tables
.Where(item => item.Enabled)
.OrderBy(item => item.Slug, StringComparer.Ordinal)
@@ -37,18 +42,22 @@ public sealed class StandardCriticalTableParserIntegrationTests
public static IEnumerable<object[]> RepresentativeCells()
{
yield return ["slash", "71-75", "A", "Blow falls on lower leg"];
yield return ["puncture", "66", "C", "Strike shatters foe's knee"];
yield return ["ballistic-shrapnel", "86-90", "E", "destroy his heart"];
yield return ["arcane-aether", "96-99", "E", "smoking pulp"];
yield return ["ma-strikes", "96-99", "E", "drives bone into brain"];
yield return ["mana", "96-99", "E", "momentarily transformed"];
yield return ["mana", "100", "E", "Mana consumes everything"];
yield return ["tiny", "100", "E", "Vein and artery severed"];
yield return new object[] { "slash", null!, "71-75", "A", "Blow falls on lower leg" };
yield return new object[] { "puncture", null!, "66", "C", "Strike shatters foe's knee" };
yield return new object[] { "ballistic-shrapnel", null!, "86-90", "E", "destroy his heart" };
yield return new object[] { "arcane-aether", null!, "96-99", "E", "smoking pulp" };
yield return new object[] { "ma-strikes", null!, "96-99", "E", "drives bone into brain" };
yield return new object[] { "mana", null!, "96-99", "E", "momentarily transformed" };
yield return new object[] { "mana", null!, "100", "E", "Mana consumes everything" };
yield return new object[] { "tiny", null!, "100", "E", "Vein and artery severed" };
yield return new object[] { "large_creature_weapon", null!, "01-05", "NORMAL", "Weapon shatters" };
yield return new object[] { "super_large_creature_weapon", null!, "31-40", "SLAYING", "Boom! Solid without question" };
yield return new object[] { "large_creature_magic", "large", "251+", "NORMAL", "Foe lowers his eyes within your reach" };
yield return new object[] { "large_creature_magic", "super_large", "251+", "SLAYING", "Blast goes in through foe's eye" };
}
[Fact]
public void Manifest_enables_the_phase_3_standard_table_set()
public void Manifest_enables_the_phase_4_table_set()
{
var manifest = LoadManifest();
var enabledTables = manifest.Tables
@@ -56,25 +65,29 @@ public sealed class StandardCriticalTableParserIntegrationTests
.OrderBy(item => item.Slug, StringComparer.Ordinal)
.ToList();
Assert.Equal(ExpectedPhase3Slugs, enabledTables.Select(item => item.Slug));
Assert.Equal(ExpectedEnabledSlugs, enabledTables.Select(item => item.Slug));
Assert.All(enabledTables, entry =>
{
Assert.Equal("standard", entry.Family);
Assert.Equal("xml", entry.ExtractionMethod);
Assert.True(File.Exists(Path.Combine(GetRepositoryRoot(), entry.PdfPath)), $"Missing source PDF for '{entry.Slug}'.");
});
Assert.Equal("variant_column", enabledTables.Single(item => item.Slug == "large_creature_weapon").Family);
Assert.Equal("variant_column", enabledTables.Single(item => item.Slug == "super_large_creature_weapon").Family);
Assert.Equal("grouped_variant", enabledTables.Single(item => item.Slug == "large_creature_magic").Family);
}
[Theory]
[MemberData(nameof(EnabledStandardTables))]
public async Task Enabled_standard_tables_extract_and_parse_successfully(CriticalImportManifestEntry entry)
[MemberData(nameof(EnabledTables))]
public async Task Enabled_tables_extract_and_parse_successfully(CriticalImportManifestEntry entry)
{
var parseResult = await LoadParseResultAsync(entry);
var expectedGroupCount = Math.Max(parseResult.Table.Groups.Count, 1);
Assert.True(parseResult.ValidationReport.IsValid, string.Join(Environment.NewLine, parseResult.ValidationReport.Errors));
Assert.Equal(5, parseResult.Table.Columns.Count);
Assert.NotEmpty(parseResult.Table.Columns);
Assert.NotEmpty(parseResult.Table.RollBands);
Assert.Equal(parseResult.ValidationReport.RowCount * 5, parseResult.ValidationReport.CellCount);
Assert.Equal(parseResult.ValidationReport.RowCount * parseResult.Table.Columns.Count * expectedGroupCount, parseResult.ValidationReport.CellCount);
Assert.Equal(parseResult.ValidationReport.CellCount, parseResult.Table.Results.Count);
}
@@ -82,6 +95,7 @@ public sealed class StandardCriticalTableParserIntegrationTests
[MemberData(nameof(RepresentativeCells))]
public async Task Representative_cells_keep_expected_descriptions(
string slug,
string? groupKey,
string rollBandLabel,
string columnKey,
string expectedSnippet)
@@ -89,6 +103,7 @@ public sealed class StandardCriticalTableParserIntegrationTests
var entry = LoadManifest().Tables.Single(item => string.Equals(item.Slug, slug, StringComparison.Ordinal));
var parseResult = await LoadParseResultAsync(entry);
var result = parseResult.Table.Results.Single(item =>
string.Equals(item.GroupKey, groupKey, StringComparison.Ordinal) &&
string.Equals(item.RollBandLabel, rollBandLabel, StringComparison.Ordinal) &&
string.Equals(item.ColumnKey, columnKey, StringComparison.Ordinal));
@@ -101,6 +116,7 @@ public sealed class StandardCriticalTableParserIntegrationTests
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, "56-60", StringComparison.Ordinal) &&
string.Equals(item.ColumnKey, "A", StringComparison.Ordinal));
@@ -113,9 +129,11 @@ public sealed class StandardCriticalTableParserIntegrationTests
var entry = LoadManifest().Tables.Single(item => string.Equals(item.Slug, "mana", StringComparison.Ordinal));
var parseResult = await LoadParseResultAsync(entry);
var row96E = parseResult.Table.Results.Single(item =>
item.GroupKey is null &&
string.Equals(item.RollBandLabel, "96-99", StringComparison.Ordinal) &&
string.Equals(item.ColumnKey, "E", StringComparison.Ordinal));
var row100E = parseResult.Table.Results.Single(item =>
item.GroupKey is null &&
string.Equals(item.RollBandLabel, "100", StringComparison.Ordinal) &&
string.Equals(item.ColumnKey, "E", StringComparison.Ordinal));
@@ -130,6 +148,7 @@ public sealed class StandardCriticalTableParserIntegrationTests
var entry = LoadManifest().Tables.Single(item => string.Equals(item.Slug, "mana", StringComparison.Ordinal));
var parseResult = await LoadParseResultAsync(entry);
var row100C = parseResult.Table.Results.Single(item =>
item.GroupKey is null &&
string.Equals(item.RollBandLabel, "100", StringComparison.Ordinal) &&
string.Equals(item.ColumnKey, "C", StringComparison.Ordinal));
@@ -143,9 +162,11 @@ public sealed class StandardCriticalTableParserIntegrationTests
var entry = LoadManifest().Tables.Single(item => string.Equals(item.Slug, "mana", StringComparison.Ordinal));
var parseResult = await LoadParseResultAsync(entry);
var row71A = parseResult.Table.Results.Single(item =>
item.GroupKey is null &&
string.Equals(item.RollBandLabel, "71-75", StringComparison.Ordinal) &&
string.Equals(item.ColumnKey, "A", StringComparison.Ordinal));
var row71B = parseResult.Table.Results.Single(item =>
item.GroupKey is null &&
string.Equals(item.RollBandLabel, "71-75", StringComparison.Ordinal) &&
string.Equals(item.ColumnKey, "B", StringComparison.Ordinal));
@@ -159,9 +180,11 @@ public sealed class StandardCriticalTableParserIntegrationTests
var entry = LoadManifest().Tables.Single(item => string.Equals(item.Slug, "mana", StringComparison.Ordinal));
var parseResult = await LoadParseResultAsync(entry);
var row71D = parseResult.Table.Results.Single(item =>
item.GroupKey is null &&
string.Equals(item.RollBandLabel, "71-75", StringComparison.Ordinal) &&
string.Equals(item.ColumnKey, "D", StringComparison.Ordinal));
var row71E = parseResult.Table.Results.Single(item =>
item.GroupKey is null &&
string.Equals(item.RollBandLabel, "71-75", StringComparison.Ordinal) &&
string.Equals(item.ColumnKey, "E", StringComparison.Ordinal));
@@ -175,9 +198,11 @@ public sealed class StandardCriticalTableParserIntegrationTests
var entry = LoadManifest().Tables.Single(item => string.Equals(item.Slug, "mana", StringComparison.Ordinal));
var parseResult = await LoadParseResultAsync(entry);
var row91B = parseResult.Table.Results.Single(item =>
item.GroupKey is null &&
string.Equals(item.RollBandLabel, "91-95", StringComparison.Ordinal) &&
string.Equals(item.ColumnKey, "B", StringComparison.Ordinal));
var row91C = parseResult.Table.Results.Single(item =>
item.GroupKey is null &&
string.Equals(item.RollBandLabel, "91-95", StringComparison.Ordinal) &&
string.Equals(item.ColumnKey, "C", StringComparison.Ordinal));
@@ -191,9 +216,11 @@ public sealed class StandardCriticalTableParserIntegrationTests
var entry = LoadManifest().Tables.Single(item => string.Equals(item.Slug, "mana", StringComparison.Ordinal));
var parseResult = await LoadParseResultAsync(entry);
var row86B = parseResult.Table.Results.Single(item =>
item.GroupKey is null &&
string.Equals(item.RollBandLabel, "86-90", StringComparison.Ordinal) &&
string.Equals(item.ColumnKey, "B", StringComparison.Ordinal));
var row86C = parseResult.Table.Results.Single(item =>
item.GroupKey is null &&
string.Equals(item.RollBandLabel, "86-90", StringComparison.Ordinal) &&
string.Equals(item.ColumnKey, "C", StringComparison.Ordinal));
@@ -201,7 +228,28 @@ public sealed class StandardCriticalTableParserIntegrationTests
Assert.Contains("+16H - 8", row86C.RawAffixText, StringComparison.Ordinal);
}
private static async Task<StandardCriticalTableParseResult> LoadParseResultAsync(CriticalImportManifestEntry entry)
[Fact]
public async Task Grouped_magic_table_keeps_large_and_super_large_groups_distinct()
{
var entry = LoadManifest().Tables.Single(item => string.Equals(item.Slug, "large_creature_magic", StringComparison.Ordinal));
var parseResult = await LoadParseResultAsync(entry);
Assert.Equal(["large", "super_large"], parseResult.Table.Groups.Select(item => item.GroupKey));
var largeNormal = parseResult.Table.Results.Single(item =>
string.Equals(item.GroupKey, "large", StringComparison.Ordinal) &&
string.Equals(item.RollBandLabel, "251+", StringComparison.Ordinal) &&
string.Equals(item.ColumnKey, "NORMAL", StringComparison.Ordinal));
var superSlaying = parseResult.Table.Results.Single(item =>
string.Equals(item.GroupKey, "super_large", StringComparison.Ordinal) &&
string.Equals(item.RollBandLabel, "251+", StringComparison.Ordinal) &&
string.Equals(item.ColumnKey, "SLAYING", StringComparison.Ordinal));
Assert.DoesNotContain("Blast goes in through foe's eye", largeNormal.DescriptionText, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Blast goes in through foe's eye", superSlaying.DescriptionText, StringComparison.OrdinalIgnoreCase);
}
private static async Task<CriticalTableParseResult> LoadParseResultAsync(CriticalImportManifestEntry entry)
{
var xmlPath = Path.Combine(GetArtifactCacheRoot(), $"{entry.Slug}.xml");
@@ -211,7 +259,13 @@ public sealed class StandardCriticalTableParserIntegrationTests
}
var xmlContent = await File.ReadAllTextAsync(xmlPath);
return Parser.Parse(entry, xmlContent);
return entry.Family switch
{
"standard" => StandardParser.Parse(entry, xmlContent),
"variant_column" => VariantColumnParser.Parse(entry, xmlContent),
"grouped_variant" => GroupedVariantParser.Parse(entry, xmlContent),
_ => throw new InvalidOperationException($"Unsupported manifest family '{entry.Family}'.")
};
}
private static CriticalImportManifest LoadManifest() =>