Implement phase 6 critical effect normalization
This commit is contained in:
@@ -65,7 +65,6 @@ The current implementation supports:
|
|||||||
The current implementation does not yet support:
|
The current implementation does not yet support:
|
||||||
|
|
||||||
- OCR/image-based PDFs such as `Void.pdf`
|
- OCR/image-based PDFs such as `Void.pdf`
|
||||||
- normalized `critical_effect` population
|
|
||||||
- automatic confidence scoring beyond validation errors
|
- automatic confidence scoring beyond validation errors
|
||||||
|
|
||||||
## High-Level Architecture
|
## High-Level Architecture
|
||||||
@@ -285,9 +284,16 @@ Phase-5 notes:
|
|||||||
|
|
||||||
### Phase 6: Effect Normalization
|
### Phase 6: Effect Normalization
|
||||||
|
|
||||||
- parse symbolic affix lines into normalized effects
|
Phase 6 is complete for symbol-driven affixes.
|
||||||
- populate `critical_effect`
|
|
||||||
- gradually enrich prose-derived effects over time
|
Phase-6 notes:
|
||||||
|
|
||||||
|
- footer legends are parsed into table-specific affix metadata before effect normalization
|
||||||
|
- symbolic affix lines are normalized into `critical_effect` rows for both base results and conditional branches
|
||||||
|
- the normalized pass currently covers direct hits, must-parry, no-parry, stun, bleed, foe penalties, attacker bonuses, and `Mana` power-point modifiers
|
||||||
|
- result and branch `parsed_json` payloads now store the normalized symbol effects
|
||||||
|
- the web critical lookup now returns and renders parsed affix effects alongside the raw affix text
|
||||||
|
- prose-derived effects remain future work
|
||||||
|
|
||||||
### Phase 7: OCR and Manual Fallback
|
### Phase 7: OCR and Manual Fallback
|
||||||
|
|
||||||
@@ -501,11 +507,10 @@ The current implementation stores:
|
|||||||
- base `RawCellText`
|
- base `RawCellText`
|
||||||
- base `DescriptionText`
|
- base `DescriptionText`
|
||||||
- base `RawAffixText`
|
- base `RawAffixText`
|
||||||
- parsed conditional branches with condition text, branch prose, and branch affix text
|
- normalized base affix effects in `critical_effect`
|
||||||
|
- parsed conditional branches with condition text, branch prose, branch affix text, and normalized branch affix effects
|
||||||
- parsed conditional branches in debug artifacts and persisted SQLite rows
|
- parsed conditional branches in debug artifacts and persisted SQLite rows
|
||||||
|
|
||||||
It does not yet normalize effects into separate tables.
|
|
||||||
|
|
||||||
## Validation Rules
|
## Validation Rules
|
||||||
|
|
||||||
The current validation pass is intentionally strict.
|
The current validation pass is intentionally strict.
|
||||||
@@ -548,6 +553,7 @@ The current load path:
|
|||||||
- `critical_roll_band`
|
- `critical_roll_band`
|
||||||
- `critical_result`
|
- `critical_result`
|
||||||
- `critical_branch`
|
- `critical_branch`
|
||||||
|
- `critical_effect`
|
||||||
5. commits only after the full table is saved
|
5. commits only after the full table is saved
|
||||||
|
|
||||||
This means importer iterations can target one table without resetting unrelated database content.
|
This means importer iterations can target one table without resetting unrelated database content.
|
||||||
@@ -575,7 +581,11 @@ Important files in the current implementation:
|
|||||||
- `src/RolemasterDb.ImportTool/CriticalImportLoader.cs`
|
- `src/RolemasterDb.ImportTool/CriticalImportLoader.cs`
|
||||||
- transactional SQLite load/reset behavior
|
- transactional SQLite load/reset behavior
|
||||||
- `src/RolemasterDb.ImportTool/Parsing/CriticalCellTextParser.cs`
|
- `src/RolemasterDb.ImportTool/Parsing/CriticalCellTextParser.cs`
|
||||||
- shared base-vs-branch parsing for cell content
|
- shared base-vs-branch parsing for cell content and affix extraction
|
||||||
|
- `src/RolemasterDb.ImportTool/Parsing/AffixEffectParser.cs`
|
||||||
|
- footer-legend-aware symbol effect normalization
|
||||||
|
- `src/RolemasterDb.ImportTool/Parsing/AffixLegend.cs`
|
||||||
|
- parsed footer legend model used for affix classification and effect mapping
|
||||||
- `src/RolemasterDb.ImportTool/CriticalImportManifestLoader.cs`
|
- `src/RolemasterDb.ImportTool/CriticalImportManifestLoader.cs`
|
||||||
- manifest loading
|
- manifest loading
|
||||||
- `src/RolemasterDb.ImportTool/PdfXmlExtractor.cs`
|
- `src/RolemasterDb.ImportTool/PdfXmlExtractor.cs`
|
||||||
@@ -589,13 +599,15 @@ Important files in the current implementation:
|
|||||||
- `src/RolemasterDb.ImportTool/Parsing/ParsedCriticalCellArtifact.cs`
|
- `src/RolemasterDb.ImportTool/Parsing/ParsedCriticalCellArtifact.cs`
|
||||||
- debug cell artifact model
|
- debug cell artifact model
|
||||||
- `src/RolemasterDb.ImportTool/Parsing/ParsedCriticalBranch.cs`
|
- `src/RolemasterDb.ImportTool/Parsing/ParsedCriticalBranch.cs`
|
||||||
- parsed branch artifact model
|
- parsed branch artifact model with normalized effects
|
||||||
|
- `src/RolemasterDb.ImportTool/Parsing/ParsedCriticalEffect.cs`
|
||||||
|
- parsed effect artifact model
|
||||||
- `src/RolemasterDb.ImportTool/Parsing/ImportValidationReport.cs`
|
- `src/RolemasterDb.ImportTool/Parsing/ImportValidationReport.cs`
|
||||||
- validation output model
|
- validation output model
|
||||||
- `src/RolemasterDb.App/Data/RolemasterDbSchemaUpgrader.cs`
|
- `src/RolemasterDb.App/Data/RolemasterDbSchemaUpgrader.cs`
|
||||||
- SQLite upgrade hook for branch-table rollout
|
- SQLite upgrade hook for branch/effect-table rollout
|
||||||
- `src/RolemasterDb.App/Components/Shared/CriticalLookupResultCard.razor`
|
- `src/RolemasterDb.App/Components/Shared/CriticalLookupResultCard.razor`
|
||||||
- web rendering of base results and conditional branches
|
- web rendering of base results, conditional branches, and parsed affix effects
|
||||||
|
|
||||||
## Adding a New Table
|
## Adding a New Table
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ Recommended canonical `effect_code` values:
|
|||||||
- `bleed_per_round`
|
- `bleed_per_round`
|
||||||
- `foe_penalty`
|
- `foe_penalty`
|
||||||
- `attacker_bonus_next_round`
|
- `attacker_bonus_next_round`
|
||||||
|
- `power_point_modifier`
|
||||||
- `initiative_gain`
|
- `initiative_gain`
|
||||||
- `initiative_loss`
|
- `initiative_loss`
|
||||||
- `drop_item`
|
- `drop_item`
|
||||||
@@ -169,6 +170,11 @@ Each effect should point to either:
|
|||||||
|
|
||||||
This lets you keep the raw text but still filter/query on effects.
|
This lets you keep the raw text but still filter/query on effects.
|
||||||
|
|
||||||
|
Current implementation note:
|
||||||
|
|
||||||
|
- symbol-driven affixes are now normalized for both base results and conditional branch affixes
|
||||||
|
- `value_expression` is used when the affix contains a formula instead of a flat integer, which is currently needed for `Mana` power-point adjustments such as `+(2d10-18)P`
|
||||||
|
|
||||||
## Why this works for your lookup
|
## Why this works for your lookup
|
||||||
|
|
||||||
Your lookup target is mostly:
|
Your lookup target is mostly:
|
||||||
@@ -258,8 +264,8 @@ Current import flow:
|
|||||||
1. Create `critical_table`, `critical_group`, `critical_column`, and `critical_roll_band` from each PDF's visible axes.
|
1. Create `critical_table`, `critical_group`, `critical_column`, and `critical_roll_band` from each PDF's visible axes.
|
||||||
2. Store each base cell in `critical_result` with base raw/description/affix text.
|
2. Store each base cell in `critical_result` with base raw/description/affix text.
|
||||||
3. Split explicit conditional branches into `critical_branch`.
|
3. Split explicit conditional branches into `critical_branch`.
|
||||||
4. Return the base result plus ordered branches through the web critical lookup.
|
4. Parse symbolic affixes for both the base result and any branch affix payloads into `critical_effect`.
|
||||||
5. Parse symbolic affixes into `critical_effect` in the next phase.
|
5. Return the base result plus ordered branches and parsed affix effects through the web critical lookup.
|
||||||
6. Gradually enrich prose-derived effects such as death, blindness, paralysis, limb loss, initiative changes, and item breakage.
|
6. Gradually enrich prose-derived effects such as death, blindness, paralysis, limb loss, initiative changes, and item breakage.
|
||||||
7. Route image PDFs like `Void.pdf` through OCR before the same parser.
|
7. Route image PDFs like `Void.pdf` through OCR before the same parser.
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ create table critical_effect (
|
|||||||
target text,
|
target text,
|
||||||
value_integer integer,
|
value_integer integer,
|
||||||
value_decimal numeric(10, 2),
|
value_decimal numeric(10, 2),
|
||||||
|
value_expression text,
|
||||||
duration_rounds integer,
|
duration_rounds integer,
|
||||||
per_round integer,
|
per_round integer,
|
||||||
modifier integer,
|
modifier integer,
|
||||||
@@ -139,4 +140,3 @@ create index critical_branch_parsed_json_gin
|
|||||||
-- and c.column_key = 'C'
|
-- and c.column_key = 'C'
|
||||||
-- and 38 >= rb.min_roll
|
-- and 38 >= rb.min_roll
|
||||||
-- and (rb.max_roll is null or 38 <= rb.max_roll);
|
-- and (rb.max_roll is null or 38 <= rb.max_roll);
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,43 @@
|
|||||||
<div class="callout">
|
<div class="callout">
|
||||||
<h4>Affix Text</h4>
|
<h4>Affix Text</h4>
|
||||||
<p class="stacked-copy">@Result.AffixText</p>
|
<p class="stacked-copy">@Result.AffixText</p>
|
||||||
|
|
||||||
|
@if (Result.Effects.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="effect-stack">
|
||||||
|
<h5>Parsed Affixes</h5>
|
||||||
|
<ul class="effect-list">
|
||||||
|
@foreach (var effect in Result.Effects)
|
||||||
|
{
|
||||||
|
<li class="effect-item">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(effect.SourceText))
|
||||||
|
{
|
||||||
|
<code class="effect-token">@effect.SourceText</code>
|
||||||
|
}
|
||||||
|
<span>@FormatEffect(effect)</span>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (Result.Effects.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="callout">
|
||||||
|
<h4>Parsed Affixes</h4>
|
||||||
|
<ul class="effect-list">
|
||||||
|
@foreach (var effect in Result.Effects)
|
||||||
|
{
|
||||||
|
<li class="effect-item">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(effect.SourceText))
|
||||||
|
{
|
||||||
|
<code class="effect-token">@effect.SourceText</code>
|
||||||
|
}
|
||||||
|
<span>@FormatEffect(effect)</span>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +108,22 @@
|
|||||||
{
|
{
|
||||||
<p class="stacked-copy branch-affix">@branch.AffixText</p>
|
<p class="stacked-copy branch-affix">@branch.AffixText</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if (branch.Effects.Count > 0)
|
||||||
|
{
|
||||||
|
<ul class="effect-list branch-effects">
|
||||||
|
@foreach (var effect in branch.Effects)
|
||||||
|
{
|
||||||
|
<li class="effect-item">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(effect.SourceText))
|
||||||
|
{
|
||||||
|
<code class="effect-token">@effect.SourceText</code>
|
||||||
|
}
|
||||||
|
<span>@FormatEffect(effect)</span>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -116,4 +169,21 @@
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string FormatEffect(CriticalEffectLookupResponse effect) =>
|
||||||
|
effect.EffectCode switch
|
||||||
|
{
|
||||||
|
"direct_hits" when effect.ValueInteger is not null => $"{effect.ValueInteger} direct hits",
|
||||||
|
"must_parry_rounds" when effect.DurationRounds is not null => $"Must parry for {FormatRounds(effect.DurationRounds.Value)}",
|
||||||
|
"no_parry_rounds" when effect.DurationRounds is not null => $"No parry for {FormatRounds(effect.DurationRounds.Value)}",
|
||||||
|
"stunned_rounds" when effect.DurationRounds is not null => $"Stunned for {FormatRounds(effect.DurationRounds.Value)}",
|
||||||
|
"bleed_per_round" when effect.PerRound is not null => $"Bleeds {effect.PerRound} hits per round",
|
||||||
|
"foe_penalty" when effect.Modifier is not null => $"Foe penalty {effect.Modifier:+#;-#;0}",
|
||||||
|
"attacker_bonus_next_round" when effect.Modifier is not null => $"Attacker bonus next round {effect.Modifier:+#;-#;0}",
|
||||||
|
"power_point_modifier" when !string.IsNullOrWhiteSpace(effect.ValueExpression) => $"Foe power-point modifier {effect.ValueExpression}",
|
||||||
|
_ => effect.EffectCode.Replace('_', ' ')
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string FormatRounds(int value) =>
|
||||||
|
value == 1 ? "1 round" : $"{value} rounds";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public sealed class RolemasterDbContext(DbContextOptions<RolemasterDbContext> op
|
|||||||
public DbSet<CriticalRollBand> CriticalRollBands => Set<CriticalRollBand>();
|
public DbSet<CriticalRollBand> CriticalRollBands => Set<CriticalRollBand>();
|
||||||
public DbSet<CriticalResult> CriticalResults => Set<CriticalResult>();
|
public DbSet<CriticalResult> CriticalResults => Set<CriticalResult>();
|
||||||
public DbSet<CriticalBranch> CriticalBranches => Set<CriticalBranch>();
|
public DbSet<CriticalBranch> CriticalBranches => Set<CriticalBranch>();
|
||||||
|
public DbSet<CriticalEffect> CriticalEffects => Set<CriticalEffect>();
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -87,5 +88,17 @@ public sealed class RolemasterDbContext(DbContextOptions<RolemasterDbContext> op
|
|||||||
entity.Property(item => item.BranchKind).HasMaxLength(32);
|
entity.Property(item => item.BranchKind).HasMaxLength(32);
|
||||||
entity.Property(item => item.ConditionKey).HasMaxLength(128);
|
entity.Property(item => item.ConditionKey).HasMaxLength(128);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity<CriticalEffect>(entity =>
|
||||||
|
{
|
||||||
|
entity.HasIndex(item => item.EffectCode);
|
||||||
|
entity.HasIndex(item => item.CriticalResultId);
|
||||||
|
entity.HasIndex(item => item.CriticalBranchId);
|
||||||
|
entity.Property(item => item.EffectCode).HasMaxLength(64);
|
||||||
|
entity.Property(item => item.Target).HasMaxLength(32);
|
||||||
|
entity.Property(item => item.ValueExpression).HasMaxLength(128);
|
||||||
|
entity.Property(item => item.BodyPart).HasMaxLength(64);
|
||||||
|
entity.Property(item => item.SourceType).HasMaxLength(32);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,5 +39,52 @@ public static class RolemasterDbSchemaUpgrader
|
|||||||
ON "CriticalBranches" ("CriticalResultId", "SortOrder");
|
ON "CriticalBranches" ("CriticalResultId", "SortOrder");
|
||||||
""",
|
""",
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
|
await dbContext.Database.ExecuteSqlRawAsync(
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS "CriticalEffects" (
|
||||||
|
"Id" INTEGER NOT NULL CONSTRAINT "PK_CriticalEffects" PRIMARY KEY AUTOINCREMENT,
|
||||||
|
"CriticalResultId" INTEGER NULL,
|
||||||
|
"CriticalBranchId" INTEGER NULL,
|
||||||
|
"EffectCode" TEXT NOT NULL,
|
||||||
|
"Target" TEXT NULL,
|
||||||
|
"ValueInteger" INTEGER NULL,
|
||||||
|
"ValueDecimal" TEXT NULL,
|
||||||
|
"ValueExpression" TEXT NULL,
|
||||||
|
"DurationRounds" INTEGER NULL,
|
||||||
|
"PerRound" INTEGER NULL,
|
||||||
|
"Modifier" INTEGER NULL,
|
||||||
|
"BodyPart" TEXT NULL,
|
||||||
|
"IsPermanent" INTEGER NOT NULL,
|
||||||
|
"SourceType" TEXT NOT NULL,
|
||||||
|
"SourceText" TEXT NULL,
|
||||||
|
CONSTRAINT "FK_CriticalEffects_CriticalResults_CriticalResultId"
|
||||||
|
FOREIGN KEY ("CriticalResultId") REFERENCES "CriticalResults" ("Id") ON DELETE CASCADE,
|
||||||
|
CONSTRAINT "FK_CriticalEffects_CriticalBranches_CriticalBranchId"
|
||||||
|
FOREIGN KEY ("CriticalBranchId") REFERENCES "CriticalBranches" ("Id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
""",
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
await dbContext.Database.ExecuteSqlRawAsync(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_CriticalEffects_EffectCode"
|
||||||
|
ON "CriticalEffects" ("EffectCode");
|
||||||
|
""",
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
await dbContext.Database.ExecuteSqlRawAsync(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_CriticalEffects_CriticalResultId"
|
||||||
|
ON "CriticalEffects" ("CriticalResultId");
|
||||||
|
""",
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
await dbContext.Database.ExecuteSqlRawAsync(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS "IX_CriticalEffects_CriticalBranchId"
|
||||||
|
ON "CriticalEffects" ("CriticalBranchId");
|
||||||
|
""",
|
||||||
|
cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ public sealed class CriticalBranch
|
|||||||
public string ParsedJson { get; set; } = "{}";
|
public string ParsedJson { get; set; } = "{}";
|
||||||
public int SortOrder { get; set; }
|
public int SortOrder { get; set; }
|
||||||
public CriticalResult CriticalResult { get; set; } = null!;
|
public CriticalResult CriticalResult { get; set; } = null!;
|
||||||
|
public List<CriticalEffect> Effects { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
22
src/RolemasterDb.App/Domain/CriticalEffect.cs
Normal file
22
src/RolemasterDb.App/Domain/CriticalEffect.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
namespace RolemasterDb.App.Domain;
|
||||||
|
|
||||||
|
public sealed class CriticalEffect
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int? CriticalResultId { get; set; }
|
||||||
|
public int? CriticalBranchId { get; set; }
|
||||||
|
public string EffectCode { get; set; } = string.Empty;
|
||||||
|
public string? Target { get; set; }
|
||||||
|
public int? ValueInteger { get; set; }
|
||||||
|
public decimal? ValueDecimal { get; set; }
|
||||||
|
public string? ValueExpression { get; set; }
|
||||||
|
public int? DurationRounds { get; set; }
|
||||||
|
public int? PerRound { get; set; }
|
||||||
|
public int? Modifier { get; set; }
|
||||||
|
public string? BodyPart { get; set; }
|
||||||
|
public bool IsPermanent { get; set; }
|
||||||
|
public string SourceType { get; set; } = "symbol";
|
||||||
|
public string? SourceText { get; set; }
|
||||||
|
public CriticalResult? CriticalResult { get; set; }
|
||||||
|
public CriticalBranch? CriticalBranch { get; set; }
|
||||||
|
}
|
||||||
13
src/RolemasterDb.App/Domain/CriticalEffectCodes.cs
Normal file
13
src/RolemasterDb.App/Domain/CriticalEffectCodes.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace RolemasterDb.App.Domain;
|
||||||
|
|
||||||
|
public static class CriticalEffectCodes
|
||||||
|
{
|
||||||
|
public const string DirectHits = "direct_hits";
|
||||||
|
public const string MustParryRounds = "must_parry_rounds";
|
||||||
|
public const string NoParryRounds = "no_parry_rounds";
|
||||||
|
public const string StunnedRounds = "stunned_rounds";
|
||||||
|
public const string BleedPerRound = "bleed_per_round";
|
||||||
|
public const string FoePenalty = "foe_penalty";
|
||||||
|
public const string AttackerBonusNextRound = "attacker_bonus_next_round";
|
||||||
|
public const string PowerPointModifier = "power_point_modifier";
|
||||||
|
}
|
||||||
@@ -17,4 +17,5 @@ public sealed class CriticalResult
|
|||||||
public CriticalColumn CriticalColumn { get; set; } = null!;
|
public CriticalColumn CriticalColumn { get; set; } = null!;
|
||||||
public CriticalRollBand CriticalRollBand { get; set; } = null!;
|
public CriticalRollBand CriticalRollBand { get; set; } = null!;
|
||||||
public List<CriticalBranch> Branches { get; set; } = [];
|
public List<CriticalBranch> Branches { get; set; } = [];
|
||||||
|
public List<CriticalEffect> Effects { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace RolemasterDb.App.Features;
|
||||||
|
|
||||||
|
public sealed record CriticalEffectLookupResponse(
|
||||||
|
string EffectCode,
|
||||||
|
string? Target,
|
||||||
|
int? ValueInteger,
|
||||||
|
string? ValueExpression,
|
||||||
|
int? DurationRounds,
|
||||||
|
int? PerRound,
|
||||||
|
int? Modifier,
|
||||||
|
string? BodyPart,
|
||||||
|
bool IsPermanent,
|
||||||
|
string SourceType,
|
||||||
|
string? SourceText);
|
||||||
@@ -52,6 +52,7 @@ public sealed record CriticalBranchLookupResponse(
|
|||||||
string ConditionText,
|
string ConditionText,
|
||||||
string Description,
|
string Description,
|
||||||
string? AffixText,
|
string? AffixText,
|
||||||
|
IReadOnlyList<CriticalEffectLookupResponse> Effects,
|
||||||
string RawText,
|
string RawText,
|
||||||
int SortOrder);
|
int SortOrder);
|
||||||
|
|
||||||
@@ -73,6 +74,7 @@ public sealed record CriticalLookupResponse(
|
|||||||
string RawCellText,
|
string RawCellText,
|
||||||
string Description,
|
string Description,
|
||||||
string? AffixText,
|
string? AffixText,
|
||||||
|
IReadOnlyList<CriticalEffectLookupResponse> Effects,
|
||||||
IReadOnlyList<CriticalBranchLookupResponse> Branches,
|
IReadOnlyList<CriticalBranchLookupResponse> Branches,
|
||||||
string ParseStatus,
|
string ParseStatus,
|
||||||
string ParsedJson);
|
string ParsedJson);
|
||||||
|
|||||||
@@ -132,6 +132,21 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
|||||||
item.RawCellText,
|
item.RawCellText,
|
||||||
item.DescriptionText,
|
item.DescriptionText,
|
||||||
item.RawAffixText,
|
item.RawAffixText,
|
||||||
|
item.Effects
|
||||||
|
.OrderBy(effect => effect.Id)
|
||||||
|
.Select(effect => new CriticalEffectLookupResponse(
|
||||||
|
effect.EffectCode,
|
||||||
|
effect.Target,
|
||||||
|
effect.ValueInteger,
|
||||||
|
effect.ValueExpression,
|
||||||
|
effect.DurationRounds,
|
||||||
|
effect.PerRound,
|
||||||
|
effect.Modifier,
|
||||||
|
effect.BodyPart,
|
||||||
|
effect.IsPermanent,
|
||||||
|
effect.SourceType,
|
||||||
|
effect.SourceText))
|
||||||
|
.ToList(),
|
||||||
item.Branches
|
item.Branches
|
||||||
.OrderBy(branch => branch.SortOrder)
|
.OrderBy(branch => branch.SortOrder)
|
||||||
.Select(branch => new CriticalBranchLookupResponse(
|
.Select(branch => new CriticalBranchLookupResponse(
|
||||||
@@ -140,6 +155,21 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
|||||||
branch.ConditionText,
|
branch.ConditionText,
|
||||||
branch.DescriptionText,
|
branch.DescriptionText,
|
||||||
branch.RawAffixText,
|
branch.RawAffixText,
|
||||||
|
branch.Effects
|
||||||
|
.OrderBy(effect => effect.Id)
|
||||||
|
.Select(effect => new CriticalEffectLookupResponse(
|
||||||
|
effect.EffectCode,
|
||||||
|
effect.Target,
|
||||||
|
effect.ValueInteger,
|
||||||
|
effect.ValueExpression,
|
||||||
|
effect.DurationRounds,
|
||||||
|
effect.PerRound,
|
||||||
|
effect.Modifier,
|
||||||
|
effect.BodyPart,
|
||||||
|
effect.IsPermanent,
|
||||||
|
effect.SourceType,
|
||||||
|
effect.SourceText))
|
||||||
|
.ToList(),
|
||||||
branch.RawText,
|
branch.RawText,
|
||||||
branch.SortOrder))
|
branch.SortOrder))
|
||||||
.ToList(),
|
.ToList(),
|
||||||
|
|||||||
@@ -255,6 +255,49 @@ textarea {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.effect-stack {
|
||||||
|
margin-top: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-stack h5 {
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #75562f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0.7rem 0 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.65rem;
|
||||||
|
align-items: baseline;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 0.55rem 0.7rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(255, 252, 244, 0.72);
|
||||||
|
border: 1px solid rgba(127, 96, 55, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.effect-token {
|
||||||
|
padding: 0.12rem 0.38rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(238, 223, 193, 0.72);
|
||||||
|
color: #5b4327;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-effects {
|
||||||
|
margin-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.error-text {
|
.error-text {
|
||||||
color: #8d2b1e;
|
color: #8d2b1e;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
using RolemasterDb.App.Data;
|
using RolemasterDb.App.Data;
|
||||||
|
using RolemasterDb.App.Domain;
|
||||||
|
using RolemasterDb.App.Features;
|
||||||
using RolemasterDb.ImportTool.Parsing;
|
using RolemasterDb.ImportTool.Parsing;
|
||||||
|
|
||||||
namespace RolemasterDb.ImportTool.Tests;
|
namespace RolemasterDb.ImportTool.Tests;
|
||||||
@@ -303,7 +305,112 @@ public sealed class StandardCriticalTableParserIntegrationTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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 entry = LoadManifest().Tables.Single(item => string.Equals(item.Slug, "slash", StringComparison.Ordinal));
|
||||||
var parseResult = await LoadParseResultAsync(entry);
|
var parseResult = await LoadParseResultAsync(entry);
|
||||||
@@ -318,6 +425,7 @@ public sealed class StandardCriticalTableParserIntegrationTests
|
|||||||
.Include(item => item.CriticalColumn)
|
.Include(item => item.CriticalColumn)
|
||||||
.Include(item => item.CriticalRollBand)
|
.Include(item => item.CriticalRollBand)
|
||||||
.Include(item => item.Branches)
|
.Include(item => item.Branches)
|
||||||
|
.ThenInclude(item => item.Effects)
|
||||||
.SingleAsync(item =>
|
.SingleAsync(item =>
|
||||||
item.CriticalTable.Slug == "slash" &&
|
item.CriticalTable.Slug == "slash" &&
|
||||||
item.CriticalColumn.ColumnKey == "B" &&
|
item.CriticalColumn.ColumnKey == "B" &&
|
||||||
@@ -327,6 +435,34 @@ public sealed class StandardCriticalTableParserIntegrationTests
|
|||||||
Assert.Equal(2, result.Branches.Count);
|
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 == "with_leg_greaves" && item.RawAffixText == "+2H – π");
|
||||||
Assert.Contains(result.Branches, item => item.ConditionKey == "without_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)
|
private static async Task<CriticalTableParseResult> LoadParseResultAsync(CriticalImportManifestEntry entry)
|
||||||
@@ -367,6 +503,15 @@ public sealed class StandardCriticalTableParserIntegrationTests
|
|||||||
return new RolemasterDbContext(options);
|
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()
|
private static string CreateTemporaryDatabaseCopy()
|
||||||
{
|
{
|
||||||
var databasePath = Path.Combine(GetArtifactCacheRoot(), $"rolemaster-{Guid.NewGuid():N}.db");
|
var databasePath = Path.Combine(GetArtifactCacheRoot(), $"rolemaster-{Guid.NewGuid():N}.db");
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
using RolemasterDb.App.Data;
|
using RolemasterDb.App.Data;
|
||||||
using RolemasterDb.App.Domain;
|
using RolemasterDb.App.Domain;
|
||||||
@@ -8,6 +9,11 @@ namespace RolemasterDb.ImportTool;
|
|||||||
|
|
||||||
public sealed class CriticalImportLoader(string databasePath)
|
public sealed class CriticalImportLoader(string databasePath)
|
||||||
{
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
};
|
||||||
|
|
||||||
public async Task<int> ResetCriticalsAsync(CancellationToken cancellationToken = default)
|
public async Task<int> ResetCriticalsAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
await using var dbContext = CreateDbContext();
|
await using var dbContext = CreateDbContext();
|
||||||
@@ -17,6 +23,7 @@ public sealed class CriticalImportLoader(string databasePath)
|
|||||||
var removedTableCount = await dbContext.CriticalTables.CountAsync(cancellationToken);
|
var removedTableCount = await dbContext.CriticalTables.CountAsync(cancellationToken);
|
||||||
await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
|
await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
|
||||||
|
|
||||||
|
await dbContext.CriticalEffects.ExecuteDeleteAsync(cancellationToken);
|
||||||
await dbContext.CriticalBranches.ExecuteDeleteAsync(cancellationToken);
|
await dbContext.CriticalBranches.ExecuteDeleteAsync(cancellationToken);
|
||||||
await dbContext.CriticalResults.ExecuteDeleteAsync(cancellationToken);
|
await dbContext.CriticalResults.ExecuteDeleteAsync(cancellationToken);
|
||||||
await dbContext.CriticalGroups.ExecuteDeleteAsync(cancellationToken);
|
await dbContext.CriticalGroups.ExecuteDeleteAsync(cancellationToken);
|
||||||
@@ -88,8 +95,11 @@ public sealed class CriticalImportLoader(string databasePath)
|
|||||||
RawCellText = item.RawCellText,
|
RawCellText = item.RawCellText,
|
||||||
DescriptionText = item.DescriptionText,
|
DescriptionText = item.DescriptionText,
|
||||||
RawAffixText = item.RawAffixText,
|
RawAffixText = item.RawAffixText,
|
||||||
ParsedJson = "{}",
|
ParsedJson = SerializeParsedEffects(item.Effects),
|
||||||
ParseStatus = "raw",
|
ParseStatus = ResolveParseStatus(item.Effects, item.Branches),
|
||||||
|
Effects = item.Effects
|
||||||
|
.Select(CreateEffectEntity)
|
||||||
|
.ToList(),
|
||||||
Branches = item.Branches
|
Branches = item.Branches
|
||||||
.Select(branch => new CriticalBranch
|
.Select(branch => new CriticalBranch
|
||||||
{
|
{
|
||||||
@@ -100,8 +110,11 @@ public sealed class CriticalImportLoader(string databasePath)
|
|||||||
RawText = branch.RawText,
|
RawText = branch.RawText,
|
||||||
DescriptionText = branch.DescriptionText,
|
DescriptionText = branch.DescriptionText,
|
||||||
RawAffixText = branch.RawAffixText,
|
RawAffixText = branch.RawAffixText,
|
||||||
ParsedJson = "{}",
|
ParsedJson = SerializeParsedEffects(branch.Effects),
|
||||||
SortOrder = branch.SortOrder
|
SortOrder = branch.SortOrder,
|
||||||
|
Effects = branch.Effects
|
||||||
|
.Select(CreateEffectEntity)
|
||||||
|
.ToList()
|
||||||
})
|
})
|
||||||
.ToList()
|
.ToList()
|
||||||
})
|
})
|
||||||
@@ -138,6 +151,14 @@ public sealed class CriticalImportLoader(string databasePath)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await dbContext.CriticalEffects
|
||||||
|
.Where(item => item.CriticalBranch != null && item.CriticalBranch.CriticalResult.CriticalTableId == tableId.Value)
|
||||||
|
.ExecuteDeleteAsync(cancellationToken);
|
||||||
|
|
||||||
|
await dbContext.CriticalEffects
|
||||||
|
.Where(item => item.CriticalResult != null && item.CriticalResult.CriticalTableId == tableId.Value)
|
||||||
|
.ExecuteDeleteAsync(cancellationToken);
|
||||||
|
|
||||||
await dbContext.CriticalBranches
|
await dbContext.CriticalBranches
|
||||||
.Where(item => item.CriticalResult.CriticalTableId == tableId.Value)
|
.Where(item => item.CriticalResult.CriticalTableId == tableId.Value)
|
||||||
.ExecuteDeleteAsync(cancellationToken);
|
.ExecuteDeleteAsync(cancellationToken);
|
||||||
@@ -162,4 +183,32 @@ public sealed class CriticalImportLoader(string databasePath)
|
|||||||
.Where(item => item.Id == tableId.Value)
|
.Where(item => item.Id == tableId.Value)
|
||||||
.ExecuteDeleteAsync(cancellationToken);
|
.ExecuteDeleteAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CriticalEffect CreateEffectEntity(ParsedCriticalEffect effect) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
EffectCode = effect.EffectCode,
|
||||||
|
Target = effect.Target,
|
||||||
|
ValueInteger = effect.ValueInteger,
|
||||||
|
ValueExpression = effect.ValueExpression,
|
||||||
|
DurationRounds = effect.DurationRounds,
|
||||||
|
PerRound = effect.PerRound,
|
||||||
|
Modifier = effect.Modifier,
|
||||||
|
BodyPart = effect.BodyPart,
|
||||||
|
IsPermanent = effect.IsPermanent,
|
||||||
|
SourceType = effect.SourceType,
|
||||||
|
SourceText = effect.SourceText
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string SerializeParsedEffects(IReadOnlyList<ParsedCriticalEffect> effects) =>
|
||||||
|
effects.Count == 0
|
||||||
|
? "{}"
|
||||||
|
: JsonSerializer.Serialize(new { effects }, JsonOptions);
|
||||||
|
|
||||||
|
private static string ResolveParseStatus(
|
||||||
|
IReadOnlyList<ParsedCriticalEffect> effects,
|
||||||
|
IReadOnlyList<ParsedCriticalBranch> branches) =>
|
||||||
|
effects.Count > 0 || branches.Any(branch => branch.Effects.Count > 0)
|
||||||
|
? "partial"
|
||||||
|
: "raw";
|
||||||
}
|
}
|
||||||
|
|||||||
281
src/RolemasterDb.ImportTool/Parsing/AffixEffectParser.cs
Normal file
281
src/RolemasterDb.ImportTool/Parsing/AffixEffectParser.cs
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
using RolemasterDb.App.Domain;
|
||||||
|
|
||||||
|
namespace RolemasterDb.ImportTool.Parsing;
|
||||||
|
|
||||||
|
internal static class AffixEffectParser
|
||||||
|
{
|
||||||
|
private const string FoeTarget = "foe";
|
||||||
|
|
||||||
|
private static readonly Regex DirectHitsRegex = new(@"[+-]\s*\d+\s*H\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex PowerPointModifierRegex = new(@"\+\s*\((?<expression>[^)]+)\)\s*P\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex ModifierRegex = new(@"\((?<noise>[^0-9+\-)]*)(?<sign>[+-])\s*(?<value>\d+)\)", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
internal static IReadOnlyList<ParsedCriticalEffect> Parse(string? rawAffixText, AffixLegend affixLegend)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(rawAffixText))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var effects = new List<ParsedCriticalEffect>();
|
||||||
|
|
||||||
|
foreach (var rawLine in rawAffixText.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||||
|
{
|
||||||
|
ParseLine(CriticalTableParserSupport.CollapseWhitespace(rawLine), affixLegend, effects);
|
||||||
|
}
|
||||||
|
|
||||||
|
return effects;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParseLine(string line, AffixLegend affixLegend, List<ParsedCriticalEffect> effects)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(line) || line is "-" or "–" or "—")
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var consumedRanges = new List<(int Start, int End)>();
|
||||||
|
var matchedEffects = new List<(int Index, ParsedCriticalEffect Effect)>();
|
||||||
|
|
||||||
|
AddMatches(
|
||||||
|
DirectHitsRegex.Matches(line),
|
||||||
|
matchedEffects,
|
||||||
|
consumedRanges,
|
||||||
|
match =>
|
||||||
|
{
|
||||||
|
var hits = ParseSignedInteger(match.Value);
|
||||||
|
return new ParsedCriticalEffect(
|
||||||
|
CriticalEffectCodes.DirectHits,
|
||||||
|
FoeTarget,
|
||||||
|
hits,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
"symbol",
|
||||||
|
NormalizeToken(match.Value));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (affixLegend.SupportsPowerPointModifier)
|
||||||
|
{
|
||||||
|
AddMatches(
|
||||||
|
PowerPointModifierRegex.Matches(line),
|
||||||
|
matchedEffects,
|
||||||
|
consumedRanges,
|
||||||
|
match => new ParsedCriticalEffect(
|
||||||
|
CriticalEffectCodes.PowerPointModifier,
|
||||||
|
FoeTarget,
|
||||||
|
null,
|
||||||
|
CriticalTableParserSupport.CollapseWhitespace(match.Groups["expression"].Value),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
"symbol",
|
||||||
|
NormalizeToken(match.Value)));
|
||||||
|
}
|
||||||
|
|
||||||
|
AddMatches(
|
||||||
|
ModifierRegex.Matches(line),
|
||||||
|
matchedEffects,
|
||||||
|
consumedRanges,
|
||||||
|
match =>
|
||||||
|
{
|
||||||
|
var modifier = BuildModifier(match);
|
||||||
|
if (modifier is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modifier.Value < 0 && affixLegend.SupportsFoePenalty)
|
||||||
|
{
|
||||||
|
return new ParsedCriticalEffect(
|
||||||
|
CriticalEffectCodes.FoePenalty,
|
||||||
|
FoeTarget,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
modifier.Value,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
"symbol",
|
||||||
|
NormalizeToken(match.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modifier.Value > 0 && affixLegend.SupportsAttackerBonus)
|
||||||
|
{
|
||||||
|
return new ParsedCriticalEffect(
|
||||||
|
CriticalEffectCodes.AttackerBonusNextRound,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
modifier.Value,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
"symbol",
|
||||||
|
NormalizeToken(match.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
var symbolClusterRegex = CreateSymbolClusterRegex(affixLegend.EffectSymbols);
|
||||||
|
if (symbolClusterRegex is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Match match in symbolClusterRegex.Matches(line))
|
||||||
|
{
|
||||||
|
if (!match.Success || OverlapsConsumedRange(match, consumedRanges))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var magnitude = match.Groups["count"].Success
|
||||||
|
? int.Parse(match.Groups["count"].Value)
|
||||||
|
: 1;
|
||||||
|
var matchedText = NormalizeToken(match.Value);
|
||||||
|
|
||||||
|
foreach (var symbol in match.Groups["symbols"].Value.Select(character => character.ToString()))
|
||||||
|
{
|
||||||
|
var effectCode = affixLegend.ResolveEffectCode(symbol);
|
||||||
|
if (effectCode is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
matchedEffects.Add((match.Index, CreateSymbolEffect(effectCode, magnitude, matchedText)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchedEffects.Count > 0)
|
||||||
|
{
|
||||||
|
effects.AddRange(matchedEffects
|
||||||
|
.OrderBy(item => item.Index)
|
||||||
|
.Select(item => item.Effect));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ParsedCriticalEffect CreateSymbolEffect(string effectCode, int magnitude, string sourceText) =>
|
||||||
|
effectCode switch
|
||||||
|
{
|
||||||
|
CriticalEffectCodes.MustParryRounds => new ParsedCriticalEffect(
|
||||||
|
effectCode,
|
||||||
|
FoeTarget,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
magnitude,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
"symbol",
|
||||||
|
sourceText),
|
||||||
|
CriticalEffectCodes.NoParryRounds => new ParsedCriticalEffect(
|
||||||
|
effectCode,
|
||||||
|
FoeTarget,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
magnitude,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
"symbol",
|
||||||
|
sourceText),
|
||||||
|
CriticalEffectCodes.StunnedRounds => new ParsedCriticalEffect(
|
||||||
|
effectCode,
|
||||||
|
FoeTarget,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
magnitude,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
"symbol",
|
||||||
|
sourceText),
|
||||||
|
CriticalEffectCodes.BleedPerRound => new ParsedCriticalEffect(
|
||||||
|
effectCode,
|
||||||
|
FoeTarget,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
magnitude,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
"symbol",
|
||||||
|
sourceText),
|
||||||
|
_ => throw new InvalidOperationException($"Unsupported symbol effect code '{effectCode}'.")
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Regex? CreateSymbolClusterRegex(IReadOnlySet<string> symbols)
|
||||||
|
{
|
||||||
|
if (symbols.Count == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var escapedSymbols = string.Concat(symbols.Select(Regex.Escape));
|
||||||
|
return new Regex(
|
||||||
|
$@"(?<![A-Za-z0-9])(?:(?<count>\d+)\s*)?(?<symbols>[{escapedSymbols}]+)",
|
||||||
|
RegexOptions.Compiled);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddMatches(
|
||||||
|
MatchCollection matches,
|
||||||
|
List<(int Index, ParsedCriticalEffect Effect)> matchedEffects,
|
||||||
|
List<(int Start, int End)> consumedRanges,
|
||||||
|
Func<Match, ParsedCriticalEffect?> createEffect)
|
||||||
|
{
|
||||||
|
foreach (Match match in matches)
|
||||||
|
{
|
||||||
|
if (!match.Success || OverlapsConsumedRange(match, consumedRanges))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
consumedRanges.Add((match.Index, match.Index + match.Length));
|
||||||
|
|
||||||
|
var effect = createEffect(match);
|
||||||
|
if (effect is not null)
|
||||||
|
{
|
||||||
|
matchedEffects.Add((match.Index, effect));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool OverlapsConsumedRange(Match match, IReadOnlyList<(int Start, int End)> consumedRanges) =>
|
||||||
|
consumedRanges.Any(range => match.Index < range.End && range.Start < match.Index + match.Length);
|
||||||
|
|
||||||
|
private static int ParseSignedInteger(string value) =>
|
||||||
|
int.Parse(value.Replace(" ", string.Empty, StringComparison.Ordinal).Replace("H", string.Empty, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
private static int? BuildModifier(Match match)
|
||||||
|
{
|
||||||
|
if (!int.TryParse(match.Groups["value"].Value, out var absoluteValue))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Equals(match.Groups["sign"].Value, "-", StringComparison.Ordinal)
|
||||||
|
? -absoluteValue
|
||||||
|
: absoluteValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeToken(string value) =>
|
||||||
|
CriticalTableParserSupport.CollapseWhitespace(value)
|
||||||
|
.Replace(" +", "+", StringComparison.Ordinal)
|
||||||
|
.Replace("( ", "(", StringComparison.Ordinal)
|
||||||
|
.Replace(" )", ")", StringComparison.Ordinal);
|
||||||
|
}
|
||||||
50
src/RolemasterDb.ImportTool/Parsing/AffixLegend.cs
Normal file
50
src/RolemasterDb.ImportTool/Parsing/AffixLegend.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
namespace RolemasterDb.ImportTool.Parsing;
|
||||||
|
|
||||||
|
internal sealed class AffixLegend
|
||||||
|
{
|
||||||
|
public static AffixLegend Empty { get; } = new(
|
||||||
|
new Dictionary<string, string>(StringComparer.Ordinal),
|
||||||
|
[],
|
||||||
|
supportsFoePenalty: false,
|
||||||
|
supportsAttackerBonus: false,
|
||||||
|
supportsPowerPointModifier: false);
|
||||||
|
|
||||||
|
public AffixLegend(
|
||||||
|
IReadOnlyDictionary<string, string> symbolEffects,
|
||||||
|
IReadOnlyCollection<string> classificationOnlySymbols,
|
||||||
|
bool supportsFoePenalty,
|
||||||
|
bool supportsAttackerBonus,
|
||||||
|
bool supportsPowerPointModifier)
|
||||||
|
{
|
||||||
|
SymbolEffects = new Dictionary<string, string>(symbolEffects, StringComparer.Ordinal);
|
||||||
|
EffectSymbols = new HashSet<string>(SymbolEffects.Keys, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var classificationSymbols = new HashSet<string>(EffectSymbols, StringComparer.Ordinal);
|
||||||
|
foreach (var symbol in classificationOnlySymbols)
|
||||||
|
{
|
||||||
|
classificationSymbols.Add(symbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClassificationSymbols = classificationSymbols;
|
||||||
|
SupportsFoePenalty = supportsFoePenalty;
|
||||||
|
SupportsAttackerBonus = supportsAttackerBonus;
|
||||||
|
SupportsPowerPointModifier = supportsPowerPointModifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<string, string> SymbolEffects { get; }
|
||||||
|
|
||||||
|
public IReadOnlySet<string> EffectSymbols { get; }
|
||||||
|
|
||||||
|
public IReadOnlySet<string> ClassificationSymbols { get; }
|
||||||
|
|
||||||
|
public bool SupportsFoePenalty { get; }
|
||||||
|
|
||||||
|
public bool SupportsAttackerBonus { get; }
|
||||||
|
|
||||||
|
public bool SupportsPowerPointModifier { get; }
|
||||||
|
|
||||||
|
public string? ResolveEffectCode(string symbol) =>
|
||||||
|
SymbolEffects.TryGetValue(symbol, out var effectCode)
|
||||||
|
? effectCode
|
||||||
|
: null;
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ internal sealed class CriticalCellParseContent(
|
|||||||
string rawCellText,
|
string rawCellText,
|
||||||
string descriptionText,
|
string descriptionText,
|
||||||
string? rawAffixText,
|
string? rawAffixText,
|
||||||
|
IReadOnlyList<ParsedCriticalEffect> effects,
|
||||||
IReadOnlyList<ParsedCriticalBranch> branches,
|
IReadOnlyList<ParsedCriticalBranch> branches,
|
||||||
IReadOnlyList<string> validationErrors)
|
IReadOnlyList<string> validationErrors)
|
||||||
{
|
{
|
||||||
@@ -12,6 +13,7 @@ internal sealed class CriticalCellParseContent(
|
|||||||
public string RawCellText { get; } = rawCellText;
|
public string RawCellText { get; } = rawCellText;
|
||||||
public string DescriptionText { get; } = descriptionText;
|
public string DescriptionText { get; } = descriptionText;
|
||||||
public string? RawAffixText { get; } = rawAffixText;
|
public string? RawAffixText { get; } = rawAffixText;
|
||||||
|
public IReadOnlyList<ParsedCriticalEffect> Effects { get; } = effects;
|
||||||
public IReadOnlyList<ParsedCriticalBranch> Branches { get; } = branches;
|
public IReadOnlyList<ParsedCriticalBranch> Branches { get; } = branches;
|
||||||
public IReadOnlyList<string> ValidationErrors { get; } = validationErrors;
|
public IReadOnlyList<string> ValidationErrors { get; } = validationErrors;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ namespace RolemasterDb.ImportTool.Parsing;
|
|||||||
|
|
||||||
internal static class CriticalCellTextParser
|
internal static class CriticalCellTextParser
|
||||||
{
|
{
|
||||||
internal static CriticalCellParseContent Parse(IReadOnlyList<string> lines, ISet<string> affixLegendSymbols)
|
internal static CriticalCellParseContent Parse(IReadOnlyList<string> lines, AffixLegend affixLegend)
|
||||||
{
|
{
|
||||||
var validationErrors = new List<string>();
|
var validationErrors = new List<string>();
|
||||||
var branchStartIndexes = FindBranchStartIndexes(lines);
|
var branchStartIndexes = FindBranchStartIndexes(lines);
|
||||||
var baseLineCount = branchStartIndexes.Count == 0 ? lines.Count : branchStartIndexes[0];
|
var baseLineCount = branchStartIndexes.Count == 0 ? lines.Count : branchStartIndexes[0];
|
||||||
var baseLines = lines.Take(baseLineCount).ToList();
|
var baseLines = lines.Take(baseLineCount).ToList();
|
||||||
var branches = new List<ParsedCriticalBranch>();
|
var branches = new List<ParsedCriticalBranch>();
|
||||||
|
var affixLegendSymbols = affixLegend.ClassificationSymbols;
|
||||||
|
|
||||||
validationErrors.AddRange(ValidateSegmentCount(baseLines, affixLegendSymbols, "Base content"));
|
validationErrors.AddRange(ValidateSegmentCount(baseLines, affixLegendSymbols, "Base content"));
|
||||||
|
|
||||||
@@ -22,18 +23,19 @@ internal static class CriticalCellTextParser
|
|||||||
branches.Add(ParseBranch(
|
branches.Add(ParseBranch(
|
||||||
lines.Skip(startIndex).Take(endIndex - startIndex).ToList(),
|
lines.Skip(startIndex).Take(endIndex - startIndex).ToList(),
|
||||||
branchIndex + 1,
|
branchIndex + 1,
|
||||||
affixLegendSymbols,
|
affixLegend,
|
||||||
validationErrors));
|
validationErrors));
|
||||||
}
|
}
|
||||||
|
|
||||||
var (rawCellText, descriptionText, rawAffixText) = BuildTextSections(baseLines, affixLegendSymbols);
|
var (rawCellText, descriptionText, rawAffixText) = BuildTextSections(baseLines, affixLegendSymbols);
|
||||||
return new CriticalCellParseContent(baseLines, rawCellText, descriptionText, rawAffixText, branches, validationErrors);
|
var effects = AffixEffectParser.Parse(rawAffixText, affixLegend);
|
||||||
|
return new CriticalCellParseContent(baseLines, rawCellText, descriptionText, rawAffixText, effects, branches, validationErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ParsedCriticalBranch ParseBranch(
|
private static ParsedCriticalBranch ParseBranch(
|
||||||
IReadOnlyList<string> branchLines,
|
IReadOnlyList<string> branchLines,
|
||||||
int sortOrder,
|
int sortOrder,
|
||||||
ISet<string> affixLegendSymbols,
|
AffixLegend affixLegend,
|
||||||
List<string> validationErrors)
|
List<string> validationErrors)
|
||||||
{
|
{
|
||||||
var firstLine = branchLines[0];
|
var firstLine = branchLines[0];
|
||||||
@@ -56,9 +58,11 @@ internal static class CriticalCellTextParser
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var affixLegendSymbols = affixLegend.ClassificationSymbols;
|
||||||
validationErrors.AddRange(ValidateSegmentCount(payloadLines, affixLegendSymbols, $"Branch '{conditionText}'"));
|
validationErrors.AddRange(ValidateSegmentCount(payloadLines, affixLegendSymbols, $"Branch '{conditionText}'"));
|
||||||
|
|
||||||
var (_, descriptionText, rawAffixText) = BuildTextSections(payloadLines, affixLegendSymbols);
|
var (_, descriptionText, rawAffixText) = BuildTextSections(payloadLines, affixLegendSymbols);
|
||||||
|
var effects = AffixEffectParser.Parse(rawAffixText, affixLegend);
|
||||||
return new ParsedCriticalBranch(
|
return new ParsedCriticalBranch(
|
||||||
"conditional",
|
"conditional",
|
||||||
CriticalTableParserSupport.NormalizeConditionKey(conditionText),
|
CriticalTableParserSupport.NormalizeConditionKey(conditionText),
|
||||||
@@ -66,6 +70,7 @@ internal static class CriticalCellTextParser
|
|||||||
string.Join(Environment.NewLine, branchLines),
|
string.Join(Environment.NewLine, branchLines),
|
||||||
descriptionText,
|
descriptionText,
|
||||||
rawAffixText,
|
rawAffixText,
|
||||||
|
effects,
|
||||||
sortOrder);
|
sortOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +91,7 @@ internal static class CriticalCellTextParser
|
|||||||
|
|
||||||
private static IReadOnlyList<string> ValidateSegmentCount(
|
private static IReadOnlyList<string> ValidateSegmentCount(
|
||||||
IReadOnlyList<string> lines,
|
IReadOnlyList<string> lines,
|
||||||
ISet<string> affixLegendSymbols,
|
IReadOnlySet<string> affixLegendSymbols,
|
||||||
string scope)
|
string scope)
|
||||||
{
|
{
|
||||||
if (lines.Count == 0)
|
if (lines.Count == 0)
|
||||||
@@ -102,7 +107,7 @@ internal static class CriticalCellTextParser
|
|||||||
|
|
||||||
private static (string RawText, string DescriptionText, string? RawAffixText) BuildTextSections(
|
private static (string RawText, string DescriptionText, string? RawAffixText) BuildTextSections(
|
||||||
IReadOnlyList<string> lines,
|
IReadOnlyList<string> lines,
|
||||||
ISet<string> affixLegendSymbols)
|
IReadOnlySet<string> affixLegendSymbols)
|
||||||
{
|
{
|
||||||
var rawText = string.Join(Environment.NewLine, lines);
|
var rawText = string.Join(Environment.NewLine, lines);
|
||||||
var rawAffixLines = lines.Where(line => CriticalTableParserSupport.IsAffixLikeLine(line, affixLegendSymbols)).ToList();
|
var rawAffixLines = lines.Where(line => CriticalTableParserSupport.IsAffixLikeLine(line, affixLegendSymbols)).ToList();
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ using System.Text.RegularExpressions;
|
|||||||
using System.Xml;
|
using System.Xml;
|
||||||
using System.Xml.Linq;
|
using System.Xml.Linq;
|
||||||
|
|
||||||
|
using RolemasterDb.App.Domain;
|
||||||
|
|
||||||
namespace RolemasterDb.ImportTool.Parsing;
|
namespace RolemasterDb.ImportTool.Parsing;
|
||||||
|
|
||||||
internal static class CriticalTableParserSupport
|
internal static class CriticalTableParserSupport
|
||||||
@@ -156,7 +158,7 @@ internal static class CriticalTableParserSupport
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool IsAffixLikeLine(string line, ISet<string> affixLegendSymbols)
|
internal static bool IsAffixLikeLine(string line, IReadOnlySet<string> affixLegendSymbols)
|
||||||
{
|
{
|
||||||
var value = line.Trim();
|
var value = line.Trim();
|
||||||
if (value.Length == 0)
|
if (value.Length == 0)
|
||||||
@@ -213,7 +215,7 @@ internal static class CriticalTableParserSupport
|
|||||||
value.Contains(" – ", StringComparison.Ordinal);
|
value.Contains(" – ", StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static int CountLineTypeSegments(IReadOnlyList<string> lines, ISet<string> affixLegendSymbols)
|
internal static int CountLineTypeSegments(IReadOnlyList<string> lines, IReadOnlySet<string> affixLegendSymbols)
|
||||||
{
|
{
|
||||||
var segmentCount = 0;
|
var segmentCount = 0;
|
||||||
bool? previousIsAffix = null;
|
bool? previousIsAffix = null;
|
||||||
@@ -280,11 +282,11 @@ internal static class CriticalTableParserSupport
|
|||||||
.Select(item => (int?)item.Top)
|
.Select(item => (int?)item.Top)
|
||||||
.Min() ?? int.MaxValue;
|
.Min() ?? int.MaxValue;
|
||||||
|
|
||||||
internal static HashSet<string> DetectAffixLegendSymbols(IReadOnlyList<XmlTextFragment> fragments, int keyTop)
|
internal static AffixLegend ParseAffixLegend(IReadOnlyList<XmlTextFragment> fragments, int keyTop)
|
||||||
{
|
{
|
||||||
if (keyTop == int.MaxValue)
|
if (keyTop == int.MaxValue)
|
||||||
{
|
{
|
||||||
return [];
|
return AffixLegend.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
var footerLines = GroupByTop(fragments
|
var footerLines = GroupByTop(fragments
|
||||||
@@ -295,24 +297,34 @@ internal static class CriticalTableParserSupport
|
|||||||
.Select(line => CollapseWhitespace(string.Join(' ', line.OrderBy(item => item.Left).Select(item => item.Text))))
|
.Select(line => CollapseWhitespace(string.Join(' ', line.OrderBy(item => item.Left).Select(item => item.Text))))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var symbols = new HashSet<string>(StringComparer.Ordinal);
|
var footerText = string.Join(' ', footerLines);
|
||||||
|
var symbolEffects = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
|
||||||
foreach (var footerLine in footerLines)
|
AddLegendMatch(symbolEffects, footerText, CriticalEffectCodes.MustParryRounds, @"must parry\s*=\s*(\S)");
|
||||||
{
|
AddLegendMatch(symbolEffects, footerText, CriticalEffectCodes.MustParryRounds, @"(\S)\s*=\s*must parry");
|
||||||
AddLegendMatch(symbols, footerLine, @"must parry\s*=\s*(\S)");
|
AddLegendMatch(symbolEffects, footerText, CriticalEffectCodes.NoParryRounds, @"no parry\s*=\s*(\S)");
|
||||||
AddLegendMatch(symbols, footerLine, @"no parry\s*=\s*(\S)");
|
AddLegendMatch(symbolEffects, footerText, CriticalEffectCodes.NoParryRounds, @"(\S)\s*=\s*no parry");
|
||||||
AddLegendMatch(symbols, footerLine, @"stun(?:ned)?\s*=\s*(\S)");
|
AddLegendMatch(symbolEffects, footerText, CriticalEffectCodes.StunnedRounds, @"stun(?:ned)?\s*=\s*(\S)");
|
||||||
AddLegendMatch(symbols, footerLine, @"bleed\s*=\s*(\S)");
|
AddLegendMatch(symbolEffects, footerText, CriticalEffectCodes.StunnedRounds, @"(\S)\s*=\s*stun(?:ned)?");
|
||||||
AddLegendMatch(symbols, footerLine, @"powerpoint modification.*=\s*(\S)");
|
AddLegendMatch(symbolEffects, footerText, CriticalEffectCodes.BleedPerRound, @"bleed\s*=\s*(\S)");
|
||||||
}
|
AddLegendMatch(symbolEffects, footerText, CriticalEffectCodes.BleedPerRound, @"(\S)\s*=\s*bleed");
|
||||||
|
|
||||||
return symbols;
|
return new AffixLegend(
|
||||||
|
symbolEffects,
|
||||||
|
footerText.Contains("powerpoint modification", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? ["P"]
|
||||||
|
: [],
|
||||||
|
supportsFoePenalty: footerText.Contains("foe has", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
footerText.Contains("penalty", StringComparison.OrdinalIgnoreCase),
|
||||||
|
supportsAttackerBonus: footerText.Contains("attacker gets", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
footerText.Contains("next round", StringComparison.OrdinalIgnoreCase),
|
||||||
|
supportsPowerPointModifier: footerText.Contains("powerpoint modification", StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static List<XmlTextFragment> SplitBoundaryCrossingAffixFragments(
|
internal static List<XmlTextFragment> SplitBoundaryCrossingAffixFragments(
|
||||||
IReadOnlyList<XmlTextFragment> bodyFragments,
|
IReadOnlyList<XmlTextFragment> bodyFragments,
|
||||||
IReadOnlyList<(string Key, double CenterX)> columnCenters,
|
IReadOnlyList<(string Key, double CenterX)> columnCenters,
|
||||||
ISet<string> affixLegendSymbols)
|
IReadOnlySet<string> affixLegendSymbols)
|
||||||
{
|
{
|
||||||
var splitFragments = new List<XmlTextFragment>(bodyFragments.Count);
|
var splitFragments = new List<XmlTextFragment>(bodyFragments.Count);
|
||||||
|
|
||||||
@@ -327,7 +339,7 @@ internal static class CriticalTableParserSupport
|
|||||||
internal static List<(int Top, bool IsAffixLike)> BuildBodyLines(
|
internal static List<(int Top, bool IsAffixLike)> BuildBodyLines(
|
||||||
IReadOnlyList<XmlTextFragment> bodyFragments,
|
IReadOnlyList<XmlTextFragment> bodyFragments,
|
||||||
IReadOnlyList<(string Key, double CenterX)> columnCenters,
|
IReadOnlyList<(string Key, double CenterX)> columnCenters,
|
||||||
ISet<string> affixLegendSymbols)
|
IReadOnlySet<string> affixLegendSymbols)
|
||||||
{
|
{
|
||||||
var bodyLines = new List<(int Top, bool IsAffixLike)>();
|
var bodyLines = new List<(int Top, bool IsAffixLike)>();
|
||||||
|
|
||||||
@@ -391,7 +403,7 @@ internal static class CriticalTableParserSupport
|
|||||||
IReadOnlyList<RowAnchor> rowAnchors,
|
IReadOnlyList<RowAnchor> rowAnchors,
|
||||||
IReadOnlyCollection<XmlTextFragment> excludedFragments,
|
IReadOnlyCollection<XmlTextFragment> excludedFragments,
|
||||||
IReadOnlyList<(string Key, double CenterX)> columnCenters,
|
IReadOnlyList<(string Key, double CenterX)> columnCenters,
|
||||||
ISet<string> affixLegendSymbols)
|
IReadOnlySet<string> affixLegendSymbols)
|
||||||
{
|
{
|
||||||
var bodyFragments = fragments
|
var bodyFragments = fragments
|
||||||
.Where(item =>
|
.Where(item =>
|
||||||
@@ -406,7 +418,7 @@ internal static class CriticalTableParserSupport
|
|||||||
return SplitBoundaryCrossingAffixFragments(bodyFragments, columnCenters, affixLegendSymbols);
|
return SplitBoundaryCrossingAffixFragments(bodyFragments, columnCenters, affixLegendSymbols);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void RepairLeadingAffixLeakage(List<ColumnarCellEntry> cellEntries, ISet<string> affixLegendSymbols)
|
internal static void RepairLeadingAffixLeakage(List<ColumnarCellEntry> cellEntries, IReadOnlySet<string> affixLegendSymbols)
|
||||||
{
|
{
|
||||||
var maxRowIndex = cellEntries.Count == 0 ? -1 : cellEntries.Max(item => item.RowIndex);
|
var maxRowIndex = cellEntries.Count == 0 ? -1 : cellEntries.Max(item => item.RowIndex);
|
||||||
var axes = cellEntries
|
var axes = cellEntries
|
||||||
@@ -471,14 +483,14 @@ internal static class CriticalTableParserSupport
|
|||||||
|
|
||||||
internal static void BuildParsedArtifacts(
|
internal static void BuildParsedArtifacts(
|
||||||
IReadOnlyList<ColumnarCellEntry> cellEntries,
|
IReadOnlyList<ColumnarCellEntry> cellEntries,
|
||||||
ISet<string> affixLegendSymbols,
|
AffixLegend affixLegend,
|
||||||
List<ParsedCriticalCellArtifact> parsedCells,
|
List<ParsedCriticalCellArtifact> parsedCells,
|
||||||
List<ParsedCriticalResult> parsedResults,
|
List<ParsedCriticalResult> parsedResults,
|
||||||
List<string> validationErrors)
|
List<string> validationErrors)
|
||||||
{
|
{
|
||||||
foreach (var cellEntry in cellEntries)
|
foreach (var cellEntry in cellEntries)
|
||||||
{
|
{
|
||||||
var content = CriticalCellTextParser.Parse(cellEntry.Lines, affixLegendSymbols);
|
var content = CriticalCellTextParser.Parse(cellEntry.Lines, affixLegend);
|
||||||
validationErrors.AddRange(content.ValidationErrors.Select(error =>
|
validationErrors.AddRange(content.ValidationErrors.Select(error =>
|
||||||
$"Cell '{BuildCellIdentifier(cellEntry)}': {error}"));
|
$"Cell '{BuildCellIdentifier(cellEntry)}': {error}"));
|
||||||
|
|
||||||
@@ -491,6 +503,7 @@ internal static class CriticalTableParserSupport
|
|||||||
content.RawCellText,
|
content.RawCellText,
|
||||||
content.DescriptionText,
|
content.DescriptionText,
|
||||||
content.RawAffixText,
|
content.RawAffixText,
|
||||||
|
content.Effects,
|
||||||
content.Branches));
|
content.Branches));
|
||||||
|
|
||||||
parsedResults.Add(new ParsedCriticalResult(
|
parsedResults.Add(new ParsedCriticalResult(
|
||||||
@@ -500,6 +513,7 @@ internal static class CriticalTableParserSupport
|
|||||||
content.RawCellText,
|
content.RawCellText,
|
||||||
content.DescriptionText,
|
content.DescriptionText,
|
||||||
content.RawAffixText,
|
content.RawAffixText,
|
||||||
|
content.Effects,
|
||||||
content.Branches));
|
content.Branches));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -549,7 +563,7 @@ internal static class CriticalTableParserSupport
|
|||||||
private static IReadOnlyList<XmlTextFragment> SplitBoundaryCrossingAffixFragment(
|
private static IReadOnlyList<XmlTextFragment> SplitBoundaryCrossingAffixFragment(
|
||||||
XmlTextFragment fragment,
|
XmlTextFragment fragment,
|
||||||
IReadOnlyList<(string Key, double CenterX)> columnCenters,
|
IReadOnlyList<(string Key, double CenterX)> columnCenters,
|
||||||
ISet<string> affixLegendSymbols)
|
IReadOnlySet<string> affixLegendSymbols)
|
||||||
{
|
{
|
||||||
if (!LooksLikeBoundaryCrossingAffixFragment(fragment, columnCenters, affixLegendSymbols))
|
if (!LooksLikeBoundaryCrossingAffixFragment(fragment, columnCenters, affixLegendSymbols))
|
||||||
{
|
{
|
||||||
@@ -604,7 +618,7 @@ internal static class CriticalTableParserSupport
|
|||||||
private static bool LooksLikeBoundaryCrossingAffixFragment(
|
private static bool LooksLikeBoundaryCrossingAffixFragment(
|
||||||
XmlTextFragment fragment,
|
XmlTextFragment fragment,
|
||||||
IReadOnlyList<(string Key, double CenterX)> columnCenters,
|
IReadOnlyList<(string Key, double CenterX)> columnCenters,
|
||||||
ISet<string> affixLegendSymbols)
|
IReadOnlySet<string> affixLegendSymbols)
|
||||||
{
|
{
|
||||||
if (!IsAffixLikeLine(fragment.Text, affixLegendSymbols) ||
|
if (!IsAffixLikeLine(fragment.Text, affixLegendSymbols) ||
|
||||||
!fragment.Text.Contains(" ", StringComparison.Ordinal))
|
!fragment.Text.Contains(" ", StringComparison.Ordinal))
|
||||||
@@ -626,13 +640,21 @@ internal static class CriticalTableParserSupport
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddLegendMatch(HashSet<string> symbols, string value, string pattern)
|
private static void AddLegendMatch(
|
||||||
|
IDictionary<string, string> symbolEffects,
|
||||||
|
string value,
|
||||||
|
string effectCode,
|
||||||
|
string pattern)
|
||||||
{
|
{
|
||||||
foreach (Match match in Regex.Matches(value, pattern, RegexOptions.IgnoreCase))
|
foreach (Match match in Regex.Matches(value, pattern, RegexOptions.IgnoreCase))
|
||||||
{
|
{
|
||||||
if (match.Groups.Count > 1)
|
if (match.Groups.Count > 1)
|
||||||
{
|
{
|
||||||
symbols.Add(match.Groups[1].Value);
|
var symbol = match.Groups[1].Value.Trim();
|
||||||
|
if (symbol.Length == 1)
|
||||||
|
{
|
||||||
|
symbolEffects[symbol] = effectCode;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ public sealed class GroupedVariantCriticalTableParser
|
|||||||
columnHeaders.Max(item => item.Top))
|
columnHeaders.Max(item => item.Top))
|
||||||
+ CriticalTableParserSupport.HeaderToBodyMinimumGap;
|
+ CriticalTableParserSupport.HeaderToBodyMinimumGap;
|
||||||
var keyTop = CriticalTableParserSupport.FindKeyTop(fragments);
|
var keyTop = CriticalTableParserSupport.FindKeyTop(fragments);
|
||||||
var affixLegendSymbols = CriticalTableParserSupport.DetectAffixLegendSymbols(fragments, keyTop);
|
var affixLegend = CriticalTableParserSupport.ParseAffixLegend(fragments, keyTop);
|
||||||
|
var affixLegendSymbols = affixLegend.ClassificationSymbols;
|
||||||
var leftCutoff = columnHeaders.Min(item => item.Left) - 10;
|
var leftCutoff = columnHeaders.Min(item => item.Left) - 10;
|
||||||
var rowLabelFragments = CriticalTableParserSupport.FindRowLabelFragments(
|
var rowLabelFragments = CriticalTableParserSupport.FindRowLabelFragments(
|
||||||
fragments,
|
fragments,
|
||||||
@@ -114,7 +115,7 @@ public sealed class GroupedVariantCriticalTableParser
|
|||||||
|
|
||||||
var parsedCells = new List<ParsedCriticalCellArtifact>();
|
var parsedCells = new List<ParsedCriticalCellArtifact>();
|
||||||
var parsedResults = new List<ParsedCriticalResult>();
|
var parsedResults = new List<ParsedCriticalResult>();
|
||||||
CriticalTableParserSupport.BuildParsedArtifacts(cellEntries, affixLegendSymbols, parsedCells, parsedResults, validationErrors);
|
CriticalTableParserSupport.BuildParsedArtifacts(cellEntries, affixLegend, parsedCells, parsedResults, validationErrors);
|
||||||
|
|
||||||
var expectedCellCount = rowAnchors.Count * ExpectedGroups.Length * ExpectedColumns.Length;
|
var expectedCellCount = rowAnchors.Count * ExpectedGroups.Length * ExpectedColumns.Length;
|
||||||
if (parsedCells.Count != expectedCellCount)
|
if (parsedCells.Count != expectedCellCount)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ public sealed class ParsedCriticalBranch(
|
|||||||
string rawText,
|
string rawText,
|
||||||
string descriptionText,
|
string descriptionText,
|
||||||
string? rawAffixText,
|
string? rawAffixText,
|
||||||
|
IReadOnlyList<ParsedCriticalEffect> effects,
|
||||||
int sortOrder)
|
int sortOrder)
|
||||||
{
|
{
|
||||||
public string BranchKind { get; } = branchKind;
|
public string BranchKind { get; } = branchKind;
|
||||||
@@ -15,5 +16,6 @@ public sealed class ParsedCriticalBranch(
|
|||||||
public string RawText { get; } = rawText;
|
public string RawText { get; } = rawText;
|
||||||
public string DescriptionText { get; } = descriptionText;
|
public string DescriptionText { get; } = descriptionText;
|
||||||
public string? RawAffixText { get; } = rawAffixText;
|
public string? RawAffixText { get; } = rawAffixText;
|
||||||
|
public IReadOnlyList<ParsedCriticalEffect> Effects { get; } = effects;
|
||||||
public int SortOrder { get; } = sortOrder;
|
public int SortOrder { get; } = sortOrder;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ public sealed class ParsedCriticalCellArtifact(
|
|||||||
string rawCellText,
|
string rawCellText,
|
||||||
string descriptionText,
|
string descriptionText,
|
||||||
string? rawAffixText,
|
string? rawAffixText,
|
||||||
|
IReadOnlyList<ParsedCriticalEffect> effects,
|
||||||
IReadOnlyList<ParsedCriticalBranch> branches)
|
IReadOnlyList<ParsedCriticalBranch> branches)
|
||||||
{
|
{
|
||||||
public string? GroupKey { get; } = groupKey;
|
public string? GroupKey { get; } = groupKey;
|
||||||
@@ -19,5 +20,6 @@ public sealed class ParsedCriticalCellArtifact(
|
|||||||
public string RawCellText { get; } = rawCellText;
|
public string RawCellText { get; } = rawCellText;
|
||||||
public string DescriptionText { get; } = descriptionText;
|
public string DescriptionText { get; } = descriptionText;
|
||||||
public string? RawAffixText { get; } = rawAffixText;
|
public string? RawAffixText { get; } = rawAffixText;
|
||||||
|
public IReadOnlyList<ParsedCriticalEffect> Effects { get; } = effects;
|
||||||
public IReadOnlyList<ParsedCriticalBranch> Branches { get; } = branches;
|
public IReadOnlyList<ParsedCriticalBranch> Branches { get; } = branches;
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/RolemasterDb.ImportTool/Parsing/ParsedCriticalEffect.cs
Normal file
27
src/RolemasterDb.ImportTool/Parsing/ParsedCriticalEffect.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
namespace RolemasterDb.ImportTool.Parsing;
|
||||||
|
|
||||||
|
public sealed class ParsedCriticalEffect(
|
||||||
|
string effectCode,
|
||||||
|
string? target,
|
||||||
|
int? valueInteger,
|
||||||
|
string? valueExpression,
|
||||||
|
int? durationRounds,
|
||||||
|
int? perRound,
|
||||||
|
int? modifier,
|
||||||
|
string? bodyPart,
|
||||||
|
bool isPermanent,
|
||||||
|
string sourceType,
|
||||||
|
string sourceText)
|
||||||
|
{
|
||||||
|
public string EffectCode { get; } = effectCode;
|
||||||
|
public string? Target { get; } = target;
|
||||||
|
public int? ValueInteger { get; } = valueInteger;
|
||||||
|
public string? ValueExpression { get; } = valueExpression;
|
||||||
|
public int? DurationRounds { get; } = durationRounds;
|
||||||
|
public int? PerRound { get; } = perRound;
|
||||||
|
public int? Modifier { get; } = modifier;
|
||||||
|
public string? BodyPart { get; } = bodyPart;
|
||||||
|
public bool IsPermanent { get; } = isPermanent;
|
||||||
|
public string SourceType { get; } = sourceType;
|
||||||
|
public string SourceText { get; } = sourceText;
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ public sealed class ParsedCriticalResult(
|
|||||||
string rawCellText,
|
string rawCellText,
|
||||||
string descriptionText,
|
string descriptionText,
|
||||||
string? rawAffixText,
|
string? rawAffixText,
|
||||||
|
IReadOnlyList<ParsedCriticalEffect> effects,
|
||||||
IReadOnlyList<ParsedCriticalBranch> branches)
|
IReadOnlyList<ParsedCriticalBranch> branches)
|
||||||
{
|
{
|
||||||
public string? GroupKey { get; } = groupKey;
|
public string? GroupKey { get; } = groupKey;
|
||||||
@@ -15,5 +16,6 @@ public sealed class ParsedCriticalResult(
|
|||||||
public string RawCellText { get; } = rawCellText;
|
public string RawCellText { get; } = rawCellText;
|
||||||
public string DescriptionText { get; } = descriptionText;
|
public string DescriptionText { get; } = descriptionText;
|
||||||
public string? RawAffixText { get; } = rawAffixText;
|
public string? RawAffixText { get; } = rawAffixText;
|
||||||
|
public IReadOnlyList<ParsedCriticalEffect> Effects { get; } = effects;
|
||||||
public IReadOnlyList<ParsedCriticalBranch> Branches { get; } = branches;
|
public IReadOnlyList<ParsedCriticalBranch> Branches { get; } = branches;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ public sealed class StandardCriticalTableParser
|
|||||||
|
|
||||||
var bodyStartTop = headerFragments.Max(item => item.Top) + CriticalTableParserSupport.HeaderToBodyMinimumGap;
|
var bodyStartTop = headerFragments.Max(item => item.Top) + CriticalTableParserSupport.HeaderToBodyMinimumGap;
|
||||||
var keyTop = CriticalTableParserSupport.FindKeyTop(fragments);
|
var keyTop = CriticalTableParserSupport.FindKeyTop(fragments);
|
||||||
var affixLegendSymbols = CriticalTableParserSupport.DetectAffixLegendSymbols(fragments, keyTop);
|
var affixLegend = CriticalTableParserSupport.ParseAffixLegend(fragments, keyTop);
|
||||||
|
var affixLegendSymbols = affixLegend.ClassificationSymbols;
|
||||||
var leftCutoff = headerFragments.Min(item => item.Left) - 10;
|
var leftCutoff = headerFragments.Min(item => item.Left) - 10;
|
||||||
var rowLabelFragments = CriticalTableParserSupport.FindRowLabelFragments(
|
var rowLabelFragments = CriticalTableParserSupport.FindRowLabelFragments(
|
||||||
fragments,
|
fragments,
|
||||||
@@ -88,7 +89,7 @@ public sealed class StandardCriticalTableParser
|
|||||||
|
|
||||||
var parsedCells = new List<ParsedCriticalCellArtifact>();
|
var parsedCells = new List<ParsedCriticalCellArtifact>();
|
||||||
var parsedResults = new List<ParsedCriticalResult>();
|
var parsedResults = new List<ParsedCriticalResult>();
|
||||||
CriticalTableParserSupport.BuildParsedArtifacts(cellEntries, affixLegendSymbols, parsedCells, parsedResults, validationErrors);
|
CriticalTableParserSupport.BuildParsedArtifacts(cellEntries, affixLegend, parsedCells, parsedResults, validationErrors);
|
||||||
|
|
||||||
if (columnCenters.Count != 5)
|
if (columnCenters.Count != 5)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ public sealed class VariantColumnCriticalTableParser
|
|||||||
|
|
||||||
var bodyStartTop = headerFragments.Max(item => item.Top) + CriticalTableParserSupport.HeaderToBodyMinimumGap;
|
var bodyStartTop = headerFragments.Max(item => item.Top) + CriticalTableParserSupport.HeaderToBodyMinimumGap;
|
||||||
var keyTop = CriticalTableParserSupport.FindKeyTop(fragments);
|
var keyTop = CriticalTableParserSupport.FindKeyTop(fragments);
|
||||||
var affixLegendSymbols = CriticalTableParserSupport.DetectAffixLegendSymbols(fragments, keyTop);
|
var affixLegend = CriticalTableParserSupport.ParseAffixLegend(fragments, keyTop);
|
||||||
|
var affixLegendSymbols = affixLegend.ClassificationSymbols;
|
||||||
var leftCutoff = headerFragments.Min(item => item.Left) - 10;
|
var leftCutoff = headerFragments.Min(item => item.Left) - 10;
|
||||||
var rowLabelFragments = CriticalTableParserSupport.FindRowLabelFragments(
|
var rowLabelFragments = CriticalTableParserSupport.FindRowLabelFragments(
|
||||||
fragments,
|
fragments,
|
||||||
@@ -105,7 +106,7 @@ public sealed class VariantColumnCriticalTableParser
|
|||||||
|
|
||||||
var parsedCells = new List<ParsedCriticalCellArtifact>();
|
var parsedCells = new List<ParsedCriticalCellArtifact>();
|
||||||
var parsedResults = new List<ParsedCriticalResult>();
|
var parsedResults = new List<ParsedCriticalResult>();
|
||||||
CriticalTableParserSupport.BuildParsedArtifacts(cellEntries, affixLegendSymbols, parsedCells, parsedResults, validationErrors);
|
CriticalTableParserSupport.BuildParsedArtifacts(cellEntries, affixLegend, parsedCells, parsedResults, validationErrors);
|
||||||
|
|
||||||
if (columnAnchors.Count != ExpectedColumns.Length)
|
if (columnAnchors.Count != ExpectedColumns.Length)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user