diff --git a/src/RolemasterDb.App/Components/Pages/Api.razor b/src/RolemasterDb.App/Components/Pages/Api.razor index a3aa3a4..b77616a 100644 --- a/src/RolemasterDb.App/Components/Pages/Api.razor +++ b/src/RolemasterDb.App/Components/Pages/Api.razor @@ -15,6 +15,15 @@
{
   "attackTables": [
     { "key": "broadsword", "label": "Broadsword" }
+  ],
+  "criticalTables": [
+    {
+      "key": "mana",
+      "label": "Mana Critical Strike Table",
+      "family": "standard",
+      "sourceDocument": "Mana.pdf",
+      "notes": "Imported from PDF XML extraction."
+    }
   ]
 }
@@ -34,10 +43,11 @@

Critical lookup

POST /api/lookup/critical

{
-  "criticalType": "slash",
-  "column": "B",
-  "roll": 72,
+  "criticalType": "mana",
+  "column": "E",
+  "roll": 100,
   "group": null
 }
+

Response now includes table metadata, roll-band bounds, raw imported cell text, parse status, and parsed JSON alongside the gameplay description.

diff --git a/src/RolemasterDb.App/Components/Pages/Home.razor b/src/RolemasterDb.App/Components/Pages/Home.razor index 824b7be..25bccd5 100644 --- a/src/RolemasterDb.App/Components/Pages/Home.razor +++ b/src/RolemasterDb.App/Components/Pages/Home.razor @@ -17,8 +17,8 @@ else Rolemaster Lookup Desk

Resolve the attack roll, then the critical, from one place.

- This starter app seeds a small SQLite dataset and exposes the same lookup flow through Blazor and minimal APIs. - The current data is intentionally limited to a first pass so the import pipeline can grow from a working base. + Attack tables still come from the starter dataset, while critical lookups now read the importer-managed tables loaded into the same SQLite file. + The page surfaces both the gameplay result and the import metadata behind each critical entry.

@referenceData.AttackTables.Count attack tables @@ -104,16 +104,7 @@ else {

Automatic critical resolution

-
- @attackResult.AutoCritical.CriticalTableName - Column: @attackResult.AutoCritical.Column - Band: @attackResult.AutoCritical.RollBand -
-

@attackResult.AutoCritical.Description

- @if (!string.IsNullOrWhiteSpace(attackResult.AutoCritical.AffixText)) - { -

@attackResult.AutoCritical.AffixText

- } +
} else if (!string.IsNullOrWhiteSpace(attackResult.CriticalSeverity)) @@ -127,7 +118,7 @@ else

Direct Critical Lookup

-

Use this when you already know the critical table, column, and roll.

+

Use this when you already know the critical table, column, roll, and group if the selected table has variants.

@@ -146,11 +137,24 @@ else
+ @if (SelectedCriticalTable?.Groups.Count > 0) + { +
+ + +
+ } +
@@ -170,26 +174,14 @@ else @if (criticalResult is not null) {
-
-

@criticalResult.CriticalTableName

-
- Column: @criticalResult.Column - Band: @criticalResult.RollBand - Roll: @criticalResult.Roll -
-

@criticalResult.Description

- @if (!string.IsNullOrWhiteSpace(criticalResult.AffixText)) - { -

@criticalResult.AffixText

- } -
+
}
-

Seeded Reference Data

-

The schema supports much more than what is seeded today. These are the initial tables standing up the flow.

+

Loaded Reference Data

+

Attack tables remain starter content. Critical tables below are whatever importer-managed entries are currently loaded into the app database.

@foreach (var attackTable in referenceData.AttackTables) @@ -202,10 +194,7 @@ else @foreach (var criticalTable in referenceData.CriticalTables) { -
- @criticalTable.Label - Critical key: @criticalTable.Key, columns: @string.Join(", ", criticalTable.Columns.Select(column => column.Label)) -
+ }
@@ -234,6 +223,7 @@ else var initialCriticalTable = referenceData.CriticalTables.FirstOrDefault(); criticalInput.CriticalType = initialCriticalTable?.Key ?? string.Empty; criticalInput.Column = initialCriticalTable?.Columns.FirstOrDefault()?.Key ?? string.Empty; + criticalInput.Group = initialCriticalTable?.Groups.FirstOrDefault()?.Key ?? string.Empty; } private async Task RunAttackLookupAsync() @@ -271,11 +261,11 @@ else criticalInput.CriticalType, criticalInput.Column, criticalInput.Roll, - null)); + SelectedCriticalTable?.Groups.Count > 0 ? criticalInput.Group : null)); if (response is null) { - criticalError = "No seeded critical result matched that table, column, and roll."; + criticalError = "No loaded critical result matched that table, group, column, and roll."; return; } @@ -288,6 +278,7 @@ else var table = referenceData?.CriticalTables.FirstOrDefault(item => item.Key == criticalInput.CriticalType); criticalInput.Column = table?.Columns.FirstOrDefault()?.Key ?? string.Empty; + criticalInput.Group = table?.Groups.FirstOrDefault()?.Key ?? string.Empty; criticalResult = null; criticalError = null; } @@ -304,6 +295,7 @@ else { public string CriticalType { get; set; } = string.Empty; public string Column { get; set; } = string.Empty; + public string Group { get; set; } = string.Empty; public int Roll { get; set; } = 72; } } diff --git a/src/RolemasterDb.App/Components/Shared/CriticalLookupResultCard.razor b/src/RolemasterDb.App/Components/Shared/CriticalLookupResultCard.razor new file mode 100644 index 0000000..14f4d6f --- /dev/null +++ b/src/RolemasterDb.App/Components/Shared/CriticalLookupResultCard.razor @@ -0,0 +1,94 @@ +@using System.Text.Json + +
+

@Result.CriticalTableName

+
+ Table: @Result.CriticalType + Column: @Result.ColumnLabel + Role: @Result.ColumnRole + Band: @Result.RollBand + Roll: @Result.Roll + Status: @Result.ParseStatus +
+ +
+
+ Family + @Result.CriticalFamily +
+
+ Source + @Result.SourceDocument +
+
+ Column Key + @Result.Column +
+
+ Roll Range + @FormatRollRange(Result.RollBandMinRoll, Result.RollBandMaxRoll) +
+ @if (!string.IsNullOrWhiteSpace(Result.Group)) + { +
+ Group + @(string.IsNullOrWhiteSpace(Result.GroupLabel) ? Result.Group : $"{Result.GroupLabel} ({Result.Group})") +
+ } +
+ + @if (!string.IsNullOrWhiteSpace(Result.TableNotes)) + { +

@Result.TableNotes

+ } + +

@Result.Description

+ + @if (!string.IsNullOrWhiteSpace(Result.AffixText)) + { +
+

Affix Text

+

@Result.AffixText

+
+ } + +
+ Raw Imported Cell +
@Result.RawCellText
+
+ +
+ Parsed JSON +
@FormatJson(Result.ParsedJson)
+
+
+ +@code { + [Parameter, EditorRequired] + public CriticalLookupResponse Result { get; set; } = null!; + + private static string FormatRollRange(int minRoll, int? maxRoll) => + maxRoll is null + ? $"{minRoll}+" + : minRoll == maxRoll + ? minRoll.ToString() + : $"{minRoll}-{maxRoll}"; + + private static string FormatJson(string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return "{}"; + } + + try + { + using var document = JsonDocument.Parse(value); + return JsonSerializer.Serialize(document.RootElement, new JsonSerializerOptions { WriteIndented = true }); + } + catch (JsonException) + { + return value; + } + } +} diff --git a/src/RolemasterDb.App/Components/Shared/CriticalTableReferenceCard.razor b/src/RolemasterDb.App/Components/Shared/CriticalTableReferenceCard.razor new file mode 100644 index 0000000..0765313 --- /dev/null +++ b/src/RolemasterDb.App/Components/Shared/CriticalTableReferenceCard.razor @@ -0,0 +1,73 @@ +
+ @Table.Label +
+
+ Key + @Table.Key +
+
+ Family + @Table.Family +
+
+ Source + @Table.SourceDocument +
+
+ Columns + @Table.Columns.Count +
+
+ Groups + @Table.Groups.Count +
+
+ Roll Bands + @Table.RollBands.Count +
+
+ + @if (!string.IsNullOrWhiteSpace(Table.Notes)) + { +

@Table.Notes

+ } + +
+ @foreach (var column in Table.Columns) + { + @column.Label (@column.Key, @column.Role, #@column.SortOrder) + } +
+ + @if (Table.Groups.Count > 0) + { +
+ @foreach (var group in Table.Groups) + { + @group.Label (@group.Key, #@group.SortOrder) + } +
+ } + +
+ Roll Bands (@Table.RollBands.Count) +
+ @foreach (var rollBand in Table.RollBands) + { + @rollBand.Label (@FormatRollRange(rollBand.MinRoll, rollBand.MaxRoll), #@rollBand.SortOrder) + } +
+
+
+ +@code { + [Parameter, EditorRequired] + public CriticalTableReference Table { get; set; } = null!; + + private static string FormatRollRange(int minRoll, int? maxRoll) => + maxRoll is null + ? $"{minRoll}+" + : minRoll == maxRoll + ? minRoll.ToString() + : $"{minRoll}-{maxRoll}"; +} diff --git a/src/RolemasterDb.App/Components/_Imports.razor b/src/RolemasterDb.App/Components/_Imports.razor index 017e577..e516b63 100644 --- a/src/RolemasterDb.App/Components/_Imports.razor +++ b/src/RolemasterDb.App/Components/_Imports.razor @@ -11,3 +11,4 @@ @using RolemasterDb.App @using RolemasterDb.App.Components @using RolemasterDb.App.Components.Layout +@using RolemasterDb.App.Components.Shared diff --git a/src/RolemasterDb.App/Features/LookupContracts.cs b/src/RolemasterDb.App/Features/LookupContracts.cs index 16310c5..51c7e14 100644 --- a/src/RolemasterDb.App/Features/LookupContracts.cs +++ b/src/RolemasterDb.App/Features/LookupContracts.cs @@ -2,11 +2,32 @@ namespace RolemasterDb.App.Features; public sealed record LookupOption(string Key, string Label); +public sealed record CriticalColumnReference( + string Key, + string Label, + string Role, + int SortOrder); + +public sealed record CriticalGroupReference( + string Key, + string Label, + int SortOrder); + +public sealed record CriticalRollBandReference( + string Label, + int MinRoll, + int? MaxRoll, + int SortOrder); + public sealed record CriticalTableReference( string Key, string Label, - IReadOnlyList Columns, - IReadOnlyList Groups); + string Family, + string SourceDocument, + string? Notes, + IReadOnlyList Columns, + IReadOnlyList Groups, + IReadOnlyList RollBands); public sealed record LookupReferenceData( IReadOnlyList AttackTables, @@ -28,12 +49,23 @@ public sealed record CriticalLookupRequest( public sealed record CriticalLookupResponse( string CriticalType, string CriticalTableName, + string CriticalFamily, + string SourceDocument, + string? TableNotes, string? Group, + string? GroupLabel, string Column, + string ColumnLabel, + string ColumnRole, int Roll, string RollBand, + int RollBandMinRoll, + int? RollBandMaxRoll, + string RawCellText, string Description, - string? AffixText); + string? AffixText, + string ParseStatus, + string ParsedJson); public sealed record AttackLookupResponse( string AttackTable, diff --git a/src/RolemasterDb.App/Features/LookupService.cs b/src/RolemasterDb.App/Features/LookupService.cs index 54405dc..a2f380d 100644 --- a/src/RolemasterDb.App/Features/LookupService.cs +++ b/src/RolemasterDb.App/Features/LookupService.cs @@ -26,6 +26,7 @@ public sealed class LookupService(IDbContextFactory dbConte .AsSplitQuery() .Include(item => item.Columns) .Include(item => item.Groups) + .Include(item => item.RollBands) .OrderBy(item => item.DisplayName) .ToListAsync(cancellationToken); @@ -35,8 +36,21 @@ public sealed class LookupService(IDbContextFactory dbConte criticalTables.Select(item => new CriticalTableReference( item.Slug, item.DisplayName, - item.Columns.OrderBy(column => column.SortOrder).Select(column => new LookupOption(column.ColumnKey, column.Label)).ToList(), - item.Groups.OrderBy(group => group.SortOrder).Select(group => new LookupOption(group.GroupKey, group.Label)).ToList())) + item.Family, + item.SourceDocument, + item.Notes, + item.Columns + .OrderBy(column => column.SortOrder) + .Select(column => new CriticalColumnReference(column.ColumnKey, column.Label, column.Role, column.SortOrder)) + .ToList(), + item.Groups + .OrderBy(group => group.SortOrder) + .Select(group => new CriticalGroupReference(group.GroupKey, group.Label, group.SortOrder)) + .ToList(), + item.RollBands + .OrderBy(rollBand => rollBand.SortOrder) + .Select(rollBand => new CriticalRollBandReference(rollBand.Label, rollBand.MinRoll, rollBand.MaxRoll, rollBand.SortOrder)) + .ToList())) .ToList()); } @@ -102,12 +116,23 @@ public sealed class LookupService(IDbContextFactory dbConte .Select(item => new CriticalLookupResponse( item.CriticalTable.Slug, item.CriticalTable.DisplayName, + item.CriticalTable.Family, + item.CriticalTable.SourceDocument, + item.CriticalTable.Notes, item.CriticalGroup != null ? item.CriticalGroup.GroupKey : null, + item.CriticalGroup != null ? item.CriticalGroup.Label : null, item.CriticalColumn.ColumnKey, + item.CriticalColumn.Label, + item.CriticalColumn.Role, request.Roll, item.CriticalRollBand.Label, + item.CriticalRollBand.MinRoll, + item.CriticalRollBand.MaxRoll, + item.RawCellText, item.DescriptionText, - item.RawAffixText)) + item.RawAffixText, + item.ParseStatus, + item.ParsedJson)) .SingleOrDefaultAsync(cancellationToken); } diff --git a/src/RolemasterDb.App/rolemaster.db b/src/RolemasterDb.App/rolemaster.db index e1a6feb..8992895 100644 Binary files a/src/RolemasterDb.App/rolemaster.db and b/src/RolemasterDb.App/rolemaster.db differ diff --git a/src/RolemasterDb.App/wwwroot/app.css b/src/RolemasterDb.App/wwwroot/app.css index 3e3d526..69a8c87 100644 --- a/src/RolemasterDb.App/wwwroot/app.css +++ b/src/RolemasterDb.App/wwwroot/app.css @@ -182,6 +182,29 @@ textarea { margin-bottom: 0.45rem; } +.detail-grid { + display: grid; + gap: 0.7rem; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + margin-bottom: 0.9rem; +} + +.detail-item { + display: grid; + gap: 0.15rem; + padding: 0.65rem 0.75rem; + border-radius: 14px; + background: rgba(255, 250, 240, 0.72); + border: 1px solid rgba(127, 96, 55, 0.12); +} + +.detail-label { + color: #75562f; + font-size: 0.76rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + .result-stats { display: flex; gap: 0.7rem; @@ -224,6 +247,43 @@ textarea { .table-list-item strong { display: block; + margin-bottom: 0.6rem; +} + +.chip-row { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; + margin-top: 0.8rem; +} + +.chip { + border-radius: 999px; + padding: 0.38rem 0.68rem; + background: rgba(238, 223, 193, 0.5); + border: 1px solid rgba(127, 96, 55, 0.14); + color: #5b4327; + font-size: 0.82rem; +} + +.chip small { + color: var(--ink-soft); +} + +.details-block { + margin-top: 0.85rem; +} + +.details-block summary { + cursor: pointer; + color: var(--accent); + font-weight: 600; + margin-bottom: 0.6rem; +} + +.stacked-copy { + margin: 0; + white-space: pre-wrap; } .code-block {