Add critical cell reparse comparison review

This commit is contained in:
2026-03-15 12:07:50 +01:00
parent 203fed6315
commit b002a94523
11 changed files with 451 additions and 18 deletions

View File

@@ -507,6 +507,7 @@ The following groundwork is already implemented in the web app as of March 15, 2
- the critical table browser is framed for play use rather than import administration - the critical table browser is framed for play use rather than import administration
- critical cells support re-parse from shared parser logic - critical cells support re-parse from shared parser logic
- advanced diagnostics have been separated from the primary editing flow - advanced diagnostics have been separated from the primary editing flow
- advanced curation now includes a generated-versus-current comparison view for re-parse review
These changes are real and complete, but they are no longer the active roadmap because the detailed acceptance checklist still has unfinished items. These changes are real and complete, but they are no longer the active roadmap because the detailed acceptance checklist still has unfinished items.
@@ -575,6 +576,17 @@ Acceptance criteria:
### Phase 4: Compare and review workflow ### Phase 4: Compare and review workflow
Status:
- implemented in the web app on March 15, 2026
Implemented model:
- re-parse now returns the freshly generated parser output alongside the merged editor state
- the editor captures the pre-reparse card as a review baseline before applying the merged state
- advanced curation shows side-by-side cards for the prior edit, the fresh generated parse, and the merged post-reparse result when available
- diff summary chips call out result-text, base-effect, and condition changes without requiring raw JSON inspection
Scope: Scope:
- add a clear comparison surface for generated result versus current edited result - add a clear comparison surface for generated result versus current edited result
@@ -641,4 +653,4 @@ Mitigation:
## Recommended Next Step ## Recommended Next Step
Implement the new Phase 3 next. The remaining structural gap is generated-versus-overridden state, so re-parse and future import refreshes can preserve intentional curation instead of treating all edited values the same. Implement Phase 5 next. The remaining cleanup is to tighten the boundary between compact curation tools and engineering diagnostics so ordinary correction workflows stay user-facing while deeper diagnostics remain available behind an explicit advanced surface.

View File

@@ -206,6 +206,7 @@
{ {
<CriticalCellEditorDialog <CriticalCellEditorDialog
Model="editorModel" Model="editorModel"
ComparisonBaseline="editorComparisonBaselineModel"
IsLoading="isEditorLoading" IsLoading="isEditorLoading"
IsReparsing="isEditorReparsing" IsReparsing="isEditorReparsing"
IsSaving="isEditorSaving" IsSaving="isEditorSaving"
@@ -237,6 +238,7 @@
private string? editorSaveError; private string? editorSaveError;
private int? editingResultId; private int? editingResultId;
private CriticalCellEditorModel? editorModel; private CriticalCellEditorModel? editorModel;
private CriticalCellEditorModel? editorComparisonBaselineModel;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@@ -336,6 +338,7 @@
editorReparseError = null; editorReparseError = null;
editorSaveError = null; editorSaveError = null;
editorModel = null; editorModel = null;
editorComparisonBaselineModel = null;
editingResultId = resultId; editingResultId = resultId;
isEditorReparsing = false; isEditorReparsing = false;
isEditorSaving = false; isEditorSaving = false;
@@ -376,6 +379,7 @@
editorSaveError = null; editorSaveError = null;
editingResultId = null; editingResultId = null;
editorModel = null; editorModel = null;
editorComparisonBaselineModel = null;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
@@ -391,6 +395,7 @@
try try
{ {
var comparisonBaseline = editorModel.Clone();
var response = await LookupService.ReparseCriticalCellAsync(selectedTableSlug, editingResultId.Value, editorModel.ToRequest()); var response = await LookupService.ReparseCriticalCellAsync(selectedTableSlug, editingResultId.Value, editorModel.ToRequest());
if (response is null) if (response is null)
{ {
@@ -398,6 +403,7 @@
return; return;
} }
editorComparisonBaselineModel = comparisonBaseline;
editorModel = CriticalCellEditorModel.FromResponse(response); editorModel = CriticalCellEditorModel.FromResponse(response);
} }
catch (Exception exception) catch (Exception exception)

View File

@@ -73,4 +73,22 @@ public sealed class CriticalBranchEditorModel
effects.Count == 0 effects.Count == 0
? "{}" ? "{}"
: JsonSerializer.Serialize(new { effects = effects.Select(effect => effect.ToItem()).ToList() }); : JsonSerializer.Serialize(new { effects = effects.Select(effect => effect.ToItem()).ToList() });
public CriticalBranchEditorModel Clone() =>
new()
{
BranchKind = BranchKind,
ConditionKey = ConditionKey,
ConditionText = ConditionText,
ConditionJson = ConditionJson,
RawText = RawText,
DescriptionText = DescriptionText,
RawAffixText = RawAffixText,
ParsedJson = ParsedJson,
SortOrder = SortOrder,
OriginKey = OriginKey,
IsOverridden = IsOverridden,
AreEffectsOverridden = AreEffectsOverridden,
Effects = Effects.Select(effect => effect.Clone()).ToList()
};
} }

View File

@@ -74,6 +74,10 @@
{ {
<p class="muted critical-editor-advanced-hint">@GetParserNoteSummary(Model.ValidationMessages.Count)</p> <p class="muted critical-editor-advanced-hint">@GetParserNoteSummary(Model.ValidationMessages.Count)</p>
} }
@if (HasComparisonDifferences(Model, ComparisonBaseline))
{
<p class="muted critical-editor-advanced-hint">Fresh parsing differs from the current edited card. Review Generated Compare before saving if you want to keep the overrides.</p>
}
<div class="field-shell"> <div class="field-shell">
<label>Result Text</label> <label>Result Text</label>
<InputTextArea class="input-shell critical-editor-textarea compact" @bind-Value="Model.DescriptionText" @bind-Value:after="MarkDescriptionOverridden" /> <InputTextArea class="input-shell critical-editor-textarea compact" @bind-Value="Model.DescriptionText" @bind-Value:after="MarkDescriptionOverridden" />
@@ -176,11 +180,55 @@
<section class="critical-editor-section critical-editor-diagnostics-section"> <section class="critical-editor-section critical-editor-diagnostics-section">
<details class="critical-editor-advanced"> <details class="critical-editor-advanced">
<summary class="critical-editor-advanced-summary"> <summary class="critical-editor-advanced-summary">
<span>Advanced Diagnostics</span> <span>Advanced Review & Diagnostics</span>
<span class="critical-editor-advanced-meta">@GetAdvancedSummary(Model)</span> <span class="critical-editor-advanced-meta">@GetAdvancedSummary(Model, ComparisonBaseline)</span>
</summary> </summary>
<div class="critical-editor-advanced-body"> <div class="critical-editor-advanced-body">
@if (Model.GeneratedState is not null)
{
<div class="critical-editor-card nested">
<div class="critical-editor-card-header">
<div>
<strong>Generated Compare</strong>
<p class="muted critical-editor-inline-copy">Compare the current edited card against the fresh parser output from the raw text.</p>
</div>
</div>
<div class="chip-row">
@foreach (var item in GetComparisonSummaryItems(Model, ComparisonBaseline))
{
<span class="chip">@item</span>
}
</div>
<div class="critical-editor-compare-grid">
<CriticalResultPreviewCard
Title="Current Edited Card"
Caption="@GetCurrentComparisonCaption(ComparisonBaseline)"
Description="@GetComparisonSourceModel(Model, ComparisonBaseline).DescriptionText"
Effects="@BuildPreviewEffects(GetComparisonSourceModel(Model, ComparisonBaseline))"
Branches="@BuildPreviewBranches(GetComparisonSourceModel(Model, ComparisonBaseline))" />
<CriticalResultPreviewCard
Title="Generated From Raw Text"
Caption="This is the fresh parser output before override preservation."
Description="@Model.GeneratedState.DescriptionText"
Effects="@Model.GeneratedState.Effects"
Branches="@Model.GeneratedState.Branches"
Notes="@Model.GeneratedState.ValidationMessages" />
@if (ComparisonBaseline is not null)
{
<CriticalResultPreviewCard
Title="Current After Re-Parse"
Caption="This is the merged editor state after keeping the existing overrides."
Description="@Model.DescriptionText"
Effects="@BuildPreviewEffects(Model)"
Branches="@BuildPreviewBranches(Model)" />
}
</div>
</div>
}
<div class="critical-editor-card nested"> <div class="critical-editor-card nested">
<div class="critical-editor-card-header"> <div class="critical-editor-card-header">
<div> <div>
@@ -277,6 +325,9 @@
[Parameter, EditorRequired] [Parameter, EditorRequired]
public CriticalCellEditorModel? Model { get; set; } public CriticalCellEditorModel? Model { get; set; }
[Parameter]
public CriticalCellEditorModel? ComparisonBaseline { get; set; }
[Parameter] [Parameter]
public bool IsLoading { get; set; } public bool IsLoading { get; set; }
@@ -462,10 +513,23 @@
effect.IsOverridden = true; effect.IsOverridden = true;
} }
private static string GetAdvancedSummary(CriticalCellEditorModel model) private static string GetAdvancedSummary(CriticalCellEditorModel model, CriticalCellEditorModel? comparisonBaseline)
{ {
var differenceCount = GetComparisonDifferenceCount(model, comparisonBaseline);
var noteCount = model.ValidationMessages.Count; var noteCount = model.ValidationMessages.Count;
return noteCount == 0 ? "Parser metadata and save payload" : $"{noteCount} parser note{(noteCount == 1 ? string.Empty : "s")}"; var segments = new List<string>();
if (differenceCount > 0)
{
segments.Add($"{differenceCount} review change{(differenceCount == 1 ? string.Empty : "s")}");
}
if (noteCount > 0)
{
segments.Add($"{noteCount} parser note{(noteCount == 1 ? string.Empty : "s")}");
}
return segments.Count == 0 ? "Generated compare and diagnostics" : string.Join(" · ", segments);
} }
private static string GetParserNoteSummary(int noteCount) => private static string GetParserNoteSummary(int noteCount) =>
@@ -494,6 +558,135 @@
private static string BuildCurrentSavePayloadJson(CriticalCellEditorModel model) => private static string BuildCurrentSavePayloadJson(CriticalCellEditorModel model) =>
JsonSerializer.Serialize(model.ToRequest(), DiagnosticJsonOptions); JsonSerializer.Serialize(model.ToRequest(), DiagnosticJsonOptions);
private static IReadOnlyList<string> GetComparisonSummaryItems(CriticalCellEditorModel model, CriticalCellEditorModel? comparisonBaseline)
{
if (model.GeneratedState is null)
{
return ["Generated comparison is unavailable."];
}
var items = new List<string>();
if (DescriptionDiffers(model, comparisonBaseline))
{
items.Add("Result text differs");
}
if (EffectsDiffer(model, comparisonBaseline))
{
items.Add("Base effects differ");
}
if (BranchesDiffer(model, comparisonBaseline))
{
items.Add("Conditions differ");
}
if (items.Count == 0)
{
items.Add("Current card matches the fresh parse");
}
if (model.GeneratedState.ValidationMessages.Count > 0)
{
items.Add($"{model.GeneratedState.ValidationMessages.Count} parser note{(model.GeneratedState.ValidationMessages.Count == 1 ? string.Empty : "s")}");
}
return items;
}
private static int GetComparisonDifferenceCount(CriticalCellEditorModel model, CriticalCellEditorModel? comparisonBaseline)
{
var count = 0;
if (DescriptionDiffers(model, comparisonBaseline))
{
count++;
}
if (EffectsDiffer(model, comparisonBaseline))
{
count++;
}
if (BranchesDiffer(model, comparisonBaseline))
{
count++;
}
return count;
}
private static bool HasComparisonDifferences(CriticalCellEditorModel? model, CriticalCellEditorModel? comparisonBaseline) =>
model is not null && GetComparisonDifferenceCount(model, comparisonBaseline) > 0;
private static bool DescriptionDiffers(CriticalCellEditorModel model, CriticalCellEditorModel? comparisonBaseline) =>
model.GeneratedState is not null &&
!string.Equals(
NormalizeDisplayText(GetComparisonSourceModel(model, comparisonBaseline).DescriptionText),
NormalizeDisplayText(model.GeneratedState.DescriptionText),
StringComparison.Ordinal);
private static bool EffectsDiffer(CriticalCellEditorModel model, CriticalCellEditorModel? comparisonBaseline) =>
model.GeneratedState is not null &&
SerializeComparisonValue(BuildPreviewEffects(GetComparisonSourceModel(model, comparisonBaseline)).Select(ProjectEffectForComparison).ToList()) !=
SerializeComparisonValue(model.GeneratedState.Effects.Select(ProjectEffectForComparison).ToList());
private static bool BranchesDiffer(CriticalCellEditorModel model, CriticalCellEditorModel? comparisonBaseline) =>
model.GeneratedState is not null &&
SerializeComparisonValue(BuildPreviewBranches(GetComparisonSourceModel(model, comparisonBaseline)).Select(ProjectBranchForComparison).ToList()) !=
SerializeComparisonValue(model.GeneratedState.Branches.Select(ProjectBranchForComparison).ToList());
private static string NormalizeDisplayText(string? value) =>
value?.Trim() ?? string.Empty;
private static string SerializeComparisonValue<TValue>(TValue value) =>
JsonSerializer.Serialize(value, DiagnosticJsonOptions);
private static object ProjectEffectForComparison(CriticalEffectLookupResponse effect) => new
{
effect.EffectCode,
effect.Target,
effect.ValueInteger,
effect.ValueExpression,
effect.DurationRounds,
effect.PerRound,
effect.Modifier,
effect.BodyPart,
effect.IsPermanent,
SourceText = NormalizeDisplayText(effect.SourceText)
};
private static object ProjectBranchForComparison(CriticalBranchLookupResponse branch) => new
{
Condition = NormalizeDisplayText(branch.ConditionText),
Description = NormalizeDisplayText(branch.Description),
Effects = branch.Effects
.Select(ProjectEffectForComparison)
.ToList()
};
private static CriticalCellEditorModel GetComparisonSourceModel(
CriticalCellEditorModel model,
CriticalCellEditorModel? comparisonBaseline) =>
comparisonBaseline ?? model;
private static string GetCurrentComparisonCaption(CriticalCellEditorModel? comparisonBaseline) =>
comparisonBaseline is null
? "This is the result that will be saved."
: "This is the edited card before the last re-parse.";
private static IReadOnlyList<CriticalEffectLookupResponse> BuildPreviewEffects(CriticalCellEditorModel model) =>
model.Effects
.Select(CreatePreviewEffect)
.ToList();
private static IReadOnlyList<CriticalBranchLookupResponse> BuildPreviewBranches(CriticalCellEditorModel model) =>
model.Branches
.OrderBy(branch => branch.SortOrder)
.Select(CreatePreviewBranch)
.ToList();
private static List<(string Scope, string EffectLabel, string SourceType, string? SourceText)> GetEffectMetadataRows(CriticalCellEditorModel model) private static List<(string Scope, string EffectLabel, string SourceType, string? SourceText)> GetEffectMetadataRows(CriticalCellEditorModel model)
{ {
var rows = new List<(string Scope, string EffectLabel, string SourceType, string? SourceText)>(); var rows = new List<(string Scope, string EffectLabel, string SourceType, string? SourceText)>();
@@ -541,6 +734,22 @@
effect.IsPermanent, effect.IsPermanent,
effect.SourceType, effect.SourceType,
effect.SourceText); effect.SourceText);
private static CriticalEffectLookupResponse CreatePreviewEffect(CriticalEffectEditorModel effect) =>
CreateBadgeEffect(effect);
private static CriticalBranchLookupResponse CreatePreviewBranch(CriticalBranchEditorModel branch) =>
new(
branch.BranchKind,
branch.ConditionKey,
branch.ConditionText,
branch.DescriptionText,
branch.RawAffixText,
branch.Effects
.Select(CreatePreviewEffect)
.ToList(),
branch.RawText,
branch.SortOrder);
} }
@functions { @functions {

View File

@@ -27,6 +27,7 @@ public sealed class CriticalCellEditorModel
public List<string> ValidationMessages { get; set; } = []; public List<string> ValidationMessages { get; set; } = [];
public List<CriticalEffectEditorModel> Effects { get; set; } = []; public List<CriticalEffectEditorModel> Effects { get; set; } = [];
public List<CriticalBranchEditorModel> Branches { get; set; } = []; public List<CriticalBranchEditorModel> Branches { get; set; } = [];
public CriticalCellComparisonState? GeneratedState { get; set; }
public static CriticalCellEditorModel FromResponse(CriticalCellEditorResponse response) => public static CriticalCellEditorModel FromResponse(CriticalCellEditorResponse response) =>
new() new()
@@ -52,7 +53,8 @@ public sealed class CriticalCellEditorModel
AreBranchesOverridden = response.AreBranchesOverridden, AreBranchesOverridden = response.AreBranchesOverridden,
ValidationMessages = response.ValidationMessages.ToList(), ValidationMessages = response.ValidationMessages.ToList(),
Effects = response.Effects.Select(CriticalEffectEditorModel.FromItem).ToList(), Effects = response.Effects.Select(CriticalEffectEditorModel.FromItem).ToList(),
Branches = response.Branches.Select(CriticalBranchEditorModel.FromItem).ToList() Branches = response.Branches.Select(CriticalBranchEditorModel.FromItem).ToList(),
GeneratedState = response.GeneratedState
}; };
public CriticalCellUpdateRequest ToRequest() public CriticalCellUpdateRequest ToRequest()
@@ -83,6 +85,34 @@ public sealed class CriticalCellEditorModel
}; };
} }
public CriticalCellEditorModel Clone() =>
new()
{
ResultId = ResultId,
TableSlug = TableSlug,
TableName = TableName,
SourceDocument = SourceDocument,
RollBand = RollBand,
GroupKey = GroupKey,
GroupLabel = GroupLabel,
ColumnKey = ColumnKey,
ColumnLabel = ColumnLabel,
ColumnRole = ColumnRole,
RawCellText = RawCellText,
DescriptionText = DescriptionText,
RawAffixText = RawAffixText,
ParseStatus = ParseStatus,
ParsedJson = ParsedJson,
IsDescriptionOverridden = IsDescriptionOverridden,
IsRawAffixTextOverridden = IsRawAffixTextOverridden,
AreEffectsOverridden = AreEffectsOverridden,
AreBranchesOverridden = AreBranchesOverridden,
ValidationMessages = ValidationMessages.ToList(),
Effects = Effects.Select(effect => effect.Clone()).ToList(),
Branches = Branches.Select(branch => branch.Clone()).ToList(),
GeneratedState = GeneratedState
};
private static string ResolveParseStatus( private static string ResolveParseStatus(
IReadOnlyList<CriticalEffectEditorModel> effects, IReadOnlyList<CriticalEffectEditorModel> effects,
IReadOnlyList<CriticalBranchEditorModel> branches) => IReadOnlyList<CriticalBranchEditorModel> branches) =>

View File

@@ -54,4 +54,23 @@ public sealed class CriticalEffectEditorModel
SourceText, SourceText,
OriginKey, OriginKey,
IsOverridden); IsOverridden);
public CriticalEffectEditorModel Clone() =>
new()
{
EffectCode = EffectCode,
Target = Target,
ValueInteger = ValueInteger,
ValueDecimal = ValueDecimal,
ValueExpression = ValueExpression,
DurationRounds = DurationRounds,
PerRound = PerRound,
Modifier = Modifier,
BodyPart = BodyPart,
IsPermanent = IsPermanent,
SourceType = SourceType,
SourceText = SourceText,
OriginKey = OriginKey,
IsOverridden = IsOverridden
};
} }

View File

@@ -0,0 +1,57 @@
@using System
@using System.Collections.Generic
@using RolemasterDb.App.Features
<div class="critical-editor-compare-card">
<div class="critical-editor-compare-card-header">
<div>
<strong>@Title</strong>
@if (!string.IsNullOrWhiteSpace(Caption))
{
<p class="muted critical-editor-inline-copy">@Caption</p>
}
</div>
</div>
@if ((Effects?.Count ?? 0) == 0 && (Branches?.Count ?? 0) == 0 && string.IsNullOrWhiteSpace(Description))
{
<p class="muted">No visible result is available for this card.</p>
}
else
{
<CompactCriticalCell
Description="@Description"
Effects="Effects"
Branches="Branches" />
}
@if ((Notes?.Count ?? 0) > 0)
{
<div class="critical-editor-validation-list">
@foreach (var note in Notes!)
{
<p class="critical-editor-validation-item">@note</p>
}
</div>
}
</div>
@code {
[Parameter, EditorRequired]
public string Title { get; set; } = string.Empty;
[Parameter]
public string? Caption { get; set; }
[Parameter, EditorRequired]
public string Description { get; set; } = string.Empty;
[Parameter]
public IReadOnlyList<CriticalEffectLookupResponse>? Effects { get; set; }
[Parameter]
public IReadOnlyList<CriticalBranchLookupResponse>? Branches { get; set; }
[Parameter]
public IReadOnlyList<string>? Notes { get; set; }
}

View File

@@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace RolemasterDb.App.Features;
public sealed record CriticalCellComparisonState(
string DescriptionText,
IReadOnlyList<CriticalEffectLookupResponse> Effects,
IReadOnlyList<CriticalBranchLookupResponse> Branches,
IReadOnlyList<string> ValidationMessages);

View File

@@ -24,4 +24,5 @@ public sealed record CriticalCellEditorResponse(
bool AreBranchesOverridden, bool AreBranchesOverridden,
IReadOnlyList<string> ValidationMessages, IReadOnlyList<string> ValidationMessages,
IReadOnlyList<CriticalEffectEditorItem> Effects, IReadOnlyList<CriticalEffectEditorItem> Effects,
IReadOnlyList<CriticalBranchEditorItem> Branches); IReadOnlyList<CriticalBranchEditorItem> Branches,
CriticalCellComparisonState? GeneratedState);

View File

@@ -290,7 +290,14 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
item => item.Id == resultId && item.CriticalTable.Slug == normalizedSlug, item => item.Id == resultId && item.CriticalTable.Slug == normalizedSlug,
cancellationToken); cancellationToken);
return result is null ? null : CreateCellEditorResponse(result); if (result is null)
{
return null;
}
var currentState = CreateCurrentEditorState(result);
var generatedContent = await ParseCriticalCellContentAsync(dbContext, result.CriticalTableId, currentState.RawCellText, cancellationToken);
return CreateCellEditorResponse(result, currentState, generatedContent.ValidationErrors, CreateComparisonState(generatedContent));
} }
public async Task<CriticalCellEditorResponse?> ReparseCriticalCellAsync( public async Task<CriticalCellEditorResponse?> ReparseCriticalCellAsync(
@@ -322,7 +329,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
var content = SharedParsing.CriticalCellTextParser.Parse(currentState.RawCellText, affixLegend); var content = SharedParsing.CriticalCellTextParser.Parse(currentState.RawCellText, affixLegend);
var generatedState = CreateGeneratedEditorState(content); var generatedState = CreateGeneratedEditorState(content);
var mergedState = MergeGeneratedState(currentState, generatedState); var mergedState = MergeGeneratedState(currentState, generatedState);
return CreateCellEditorResponse(result, mergedState, content.ValidationErrors); return CreateCellEditorResponse(result, mergedState, content.ValidationErrors, CreateComparisonState(content));
} }
public async Task<CriticalCellEditorResponse?> UpdateCriticalCellAsync( public async Task<CriticalCellEditorResponse?> UpdateCriticalCellAsync(
@@ -363,7 +370,8 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
await dbContext.SaveChangesAsync(cancellationToken); await dbContext.SaveChangesAsync(cancellationToken);
return CreateCellEditorResponse(result, request, []); var generatedContent = await ParseCriticalCellContentAsync(dbContext, result.CriticalTableId, request.RawCellText, cancellationToken);
return CreateCellEditorResponse(result, request, generatedContent.ValidationErrors, CreateComparisonState(generatedContent));
} }
private static IReadOnlyList<CriticalTableLegendEntry> BuildLegend(IReadOnlyList<CriticalTableCellDetail> cells) private static IReadOnlyList<CriticalTableLegendEntry> BuildLegend(IReadOnlyList<CriticalTableCellDetail> cells)
@@ -424,16 +432,11 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
effect.SourceType, effect.SourceType,
effect.SourceText); effect.SourceText);
private static CriticalCellEditorResponse CreateCellEditorResponse(CriticalResult result)
{
var state = CreateCurrentEditorState(result);
return CreateCellEditorResponse(result, state, []);
}
private static CriticalCellEditorResponse CreateCellEditorResponse( private static CriticalCellEditorResponse CreateCellEditorResponse(
CriticalResult result, CriticalResult result,
CriticalCellUpdateRequest state, CriticalCellUpdateRequest state,
IReadOnlyList<string> validationMessages) IReadOnlyList<string> validationMessages,
CriticalCellComparisonState? generatedState)
{ {
var snapshotJson = CriticalCellEditorSnapshot.FromRequest(state).ToJson(); var snapshotJson = CriticalCellEditorSnapshot.FromRequest(state).ToJson();
@@ -459,7 +462,8 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
state.AreBranchesOverridden, state.AreBranchesOverridden,
validationMessages.ToList(), validationMessages.ToList(),
state.Effects.ToList(), state.Effects.ToList(),
state.Branches.ToList()); state.Branches.ToList(),
generatedState);
} }
private static CriticalBranchLookupResponse CreateBranchLookupResponse(CriticalBranch branch) => private static CriticalBranchLookupResponse CreateBranchLookupResponse(CriticalBranch branch) =>
@@ -521,6 +525,18 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
.ToList()); .ToList());
} }
private static CriticalCellComparisonState CreateComparisonState(SharedParsing.CriticalCellParseContent content) =>
new(
content.DescriptionText,
content.Effects
.Select(CreateEffectLookupResponse)
.ToList(),
content.Branches
.OrderBy(branch => branch.SortOrder)
.Select(CreateBranchLookupResponse)
.ToList(),
content.ValidationErrors.ToList());
private static CriticalEffectEditorItem CreateEffectEditorItem(CriticalEffect effect, string originKey) => private static CriticalEffectEditorItem CreateEffectEditorItem(CriticalEffect effect, string originKey) =>
new( new(
effect.EffectCode, effect.EffectCode,
@@ -806,6 +822,33 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
SourceText = NormalizeOptionalText(effect.SourceText) SourceText = NormalizeOptionalText(effect.SourceText)
}; };
private static CriticalEffectLookupResponse CreateEffectLookupResponse(SharedParsing.ParsedCriticalEffect effect) =>
new(
effect.EffectCode,
effect.Target,
effect.ValueInteger,
effect.ValueExpression,
effect.DurationRounds,
effect.PerRound,
effect.Modifier,
effect.BodyPart,
effect.IsPermanent,
effect.SourceType,
effect.SourceText);
private static CriticalBranchLookupResponse CreateBranchLookupResponse(SharedParsing.ParsedCriticalBranch branch) =>
new(
branch.BranchKind,
branch.ConditionKey,
branch.ConditionText,
branch.DescriptionText,
branch.RawAffixText,
branch.Effects
.Select(CreateEffectLookupResponse)
.ToList(),
branch.RawText,
branch.SortOrder);
private static string ResolveParseStatus( private static string ResolveParseStatus(
IReadOnlyList<SharedParsing.ParsedCriticalEffect> effects, IReadOnlyList<SharedParsing.ParsedCriticalEffect> effects,
IReadOnlyList<SharedParsing.ParsedCriticalBranch> branches) => IReadOnlyList<SharedParsing.ParsedCriticalBranch> branches) =>
@@ -876,6 +919,16 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
supportsPowerPointModifier); supportsPowerPointModifier);
} }
private static async Task<SharedParsing.CriticalCellParseContent> ParseCriticalCellContentAsync(
RolemasterDbContext dbContext,
int tableId,
string rawCellText,
CancellationToken cancellationToken)
{
var affixLegend = await BuildSharedAffixLegendAsync(dbContext, tableId, cancellationToken);
return SharedParsing.CriticalCellTextParser.Parse(rawCellText, affixLegend);
}
private static bool IsLegendSymbolEffectCode(string effectCode) => private static bool IsLegendSymbolEffectCode(string effectCode) =>
effectCode is CriticalEffectCodes.MustParryRounds effectCode is CriticalEffectCodes.MustParryRounds
or CriticalEffectCodes.NoParryRounds or CriticalEffectCodes.NoParryRounds

View File

@@ -834,6 +834,25 @@ textarea {
margin: 0; margin: 0;
} }
.critical-editor-compare-grid {
display: grid;
gap: 0.85rem;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.critical-editor-compare-card {
display: grid;
gap: 0.75rem;
padding: 0.9rem;
border-radius: 16px;
background: rgba(255, 255, 255, 0.72);
border: 1px solid rgba(127, 96, 55, 0.12);
}
.critical-editor-compare-card .critical-cell {
min-height: 100%;
}
.critical-editor-inline-list { .critical-editor-inline-list {
display: grid; display: grid;
gap: 0.55rem; gap: 0.55rem;