diff --git a/src/RolemasterDb.App/rolemaster.db b/src/RolemasterDb.App/rolemaster.db index 050f09d..8934083 100644 Binary files a/src/RolemasterDb.App/rolemaster.db and b/src/RolemasterDb.App/rolemaster.db differ diff --git a/src/RolemasterDb.ImportTool.Tests/StandardCriticalTableParserIntegrationTests.cs b/src/RolemasterDb.ImportTool.Tests/StandardCriticalTableParserIntegrationTests.cs index 362c49f..71cd0f2 100644 --- a/src/RolemasterDb.ImportTool.Tests/StandardCriticalTableParserIntegrationTests.cs +++ b/src/RolemasterDb.ImportTool.Tests/StandardCriticalTableParserIntegrationTests.cs @@ -1,3 +1,5 @@ +using System.Linq; + using Microsoft.EntityFrameworkCore; using RolemasterDb.App.Data; @@ -115,6 +117,25 @@ public sealed class StandardCriticalTableParserIntegrationTests Assert.Contains(expectedSnippet, result.DescriptionText, StringComparison.OrdinalIgnoreCase); } + [Fact] + public async Task Arcane_aether_c_31_has_single_bleed_effect() + { + var entry = LoadManifest().Tables.Single(item => string.Equals(item.Slug, "arcane-aether", StringComparison.Ordinal)); + var parseResult = await LoadParseResultAsync(entry); + var result = parseResult.Table.Results.Single(item => + item.GroupKey is null && + string.Equals(item.RollBandLabel, "31-40", StringComparison.Ordinal) && + string.Equals(item.ColumnKey, "C", StringComparison.Ordinal)); + + var bleedEffects = result.Effects + .Where(effect => effect.EffectCode == CriticalEffectCodes.BleedPerRound) + .ToList(); + + Assert.Equal("+15H – ∑ – ∫", result.RawAffixText); + Assert.Single(bleedEffects); + Assert.Equal(1, bleedEffects.Single().PerRound); + } + [Fact] public async Task Slash_boundary_repair_keeps_56_60_a_prose_first() { diff --git a/src/RolemasterDb.ImportTool/Parsing/CriticalTableParserSupport.cs b/src/RolemasterDb.ImportTool/Parsing/CriticalTableParserSupport.cs index 088fb15..a6666ab 100644 --- a/src/RolemasterDb.ImportTool/Parsing/CriticalTableParserSupport.cs +++ b/src/RolemasterDb.ImportTool/Parsing/CriticalTableParserSupport.cs @@ -30,7 +30,7 @@ internal static class CriticalTableParserSupport var document = XDocument.Load(xmlReader); - return document.Descendants("page") + var fragments = document.Descendants("page") .SelectMany(page => { var pageNumber = int.Parse(page.Attribute("number")?.Value ?? "1"); @@ -45,6 +45,8 @@ internal static class CriticalTableParserSupport .Where(item => !string.IsNullOrWhiteSpace(item.Text)); }) .ToList(); + + return RemoveRedundantContainedFragments(fragments); } internal static List FindRowLabelFragments( @@ -263,6 +265,56 @@ internal static class CriticalTableParserSupport .Replace('’', '\'') .Trim(); + private static List RemoveRedundantContainedFragments(IReadOnlyList fragments) + { + var redundant = new HashSet(); + + foreach (var group in fragments.GroupBy(item => (item.PageNumber, item.Top, item.Height))) + { + var ordered = group + .OrderByDescending(item => item.Width) + .ThenBy(item => item.Left) + .ToList(); + + for (var index = 0; index < ordered.Count; index++) + { + var container = ordered[index]; + if (container.Text.Length <= 1) + { + continue; + } + + for (var candidateIndex = index + 1; candidateIndex < ordered.Count; candidateIndex++) + { + var candidate = ordered[candidateIndex]; + if (candidate.Width > container.Width || + !container.Text.Contains(candidate.Text, StringComparison.Ordinal) || + !IsHorizontallyContained(candidate, container)) + { + continue; + } + + redundant.Add(candidate); + } + } + } + + return fragments + .Where(item => !redundant.Contains(item)) + .ToList(); + } + + private static bool IsHorizontallyContained(XmlTextFragment candidate, XmlTextFragment container) + { + const int containmentTolerance = 1; + + var candidateRight = candidate.Left + candidate.Width; + var containerRight = container.Left + container.Width; + + return candidate.Left >= container.Left - containmentTolerance && + candidateRight <= containerRight + containmentTolerance; + } + internal static string? NormalizeConditionKey(string conditionText) { var normalized = CollapseWhitespace(conditionText)