@using System @using System.Collections.Generic @using System.Linq @using Microsoft.JSInterop @using RolemasterDb.App.Domain @using RolemasterDb.App.Features @implements IAsyncDisposable @inject IJSRuntime JSRuntime
@if (Model is not null) {

Edit Result Card

@Model.TableName · Roll band @Model.RollBand · Severity @Model.ColumnLabel @if (!string.IsNullOrWhiteSpace(Model.GroupLabel)) { · Variant @Model.GroupLabel }

} else {

Edit Result Card

}
@if (IsLoading) {

Loading editor...

} else if (!string.IsNullOrWhiteSpace(LoadErrorMessage)) {

@LoadErrorMessage

} else if (Model is not null) {
@if (!string.IsNullOrWhiteSpace(ReparseErrorMessage)) {

@ReparseErrorMessage

} @if (!string.IsNullOrWhiteSpace(SaveErrorMessage)) {

@SaveErrorMessage

}

Quick Parse Input

First line is the result prose. Later lines are base affixes or condition: ... lines with comma-separated shorthand.

@if (Model.SourcePageNumber is not null) { Page @Model.SourcePageNumber }
@if (!string.IsNullOrWhiteSpace(Model.SourceImageUrl)) { @BuildSourceImageAltText(Model) } else {

No source image is available for this cell yet.

}

Example: Foe brings his guard up, frightened by your display. then +5, 1mp or w/o shield: glancing blow, +15, 3s, 3np.

@foreach (var entry in QuickParseLegendEntries) {
@entry.Token =
}
@if (Model.ValidationMessages.Count > 0) {

@GetParserNoteSummary(Model.ValidationMessages.Count)

} @if (HasComparisonDifferences(Model, ComparisonBaseline)) {

Fresh generation differs from the current edited card. Review Generated Compare before saving if you want to keep the overrides.

}

Base Effects

Edit the badges and values that appear on the main result.

@if (Model.Effects.Count == 0) {

No base effects on this result yet.

} else {
@for (var index = 0; index < Model.Effects.Count; index++) { var i = index; var effect = Model.Effects[i];
@InlineEffectRow(effect, () => RemoveBaseEffect(i))
}
}

Conditions

Keep alternate outcomes compact and easy to scan.

@if (Model.Branches.Count == 0) {

No alternate condition cards on this result yet.

} else { @for (var index = 0; index < Model.Branches.Count; index++) { var i = index; var branch = Model.Branches[i];
@GetBranchTitle(branch, i)

Shown only when this condition applies.

Condition Effects

These only appear when the condition is met.

@if (branch.Effects.Count == 0) {

No effects on this condition yet.

} else {
@for (var effectIndex = 0; effectIndex < branch.Effects.Count; effectIndex++) { var j = effectIndex; var effect = branch.Effects[j];
@InlineEffectRow(effect, () => RemoveBranchEffect(branch, j))
}
}
} }
Advanced Review @GetReviewSummary(Model, ComparisonBaseline)
@if (Model.GeneratedState is not null) {
Generated Compare

Compare the current edited card against the fresh generated result from the quick parse input.

@foreach (var item in GetComparisonSummaryItems(Model, ComparisonBaseline)) { @item }
@if (ComparisonBaseline is not null) { }
@if (Model.GeneratedState.TokenReviewItems.Count > 0) {
Token Review

These tokens were unknown or only partially understood during generation and need manual review.

@foreach (var issue in Model.GeneratedState.TokenReviewItems) {

@issue.ReviewText

}
} }
}
@code { [Parameter, EditorRequired] public CriticalCellEditorModel? Model { get; set; } [Parameter] public CriticalCellEditorModel? ComparisonBaseline { get; set; } [Parameter] public bool IsLoading { get; set; } [Parameter] public bool IsReparsing { get; set; } [Parameter] public bool IsSaving { get; set; } [Parameter] public string? LoadErrorMessage { get; set; } [Parameter] public string? ReparseErrorMessage { get; set; } [Parameter] public string? SaveErrorMessage { get; set; } [Parameter, EditorRequired] public EventCallback OnClose { get; set; } [Parameter, EditorRequired] public EventCallback OnReparse { get; set; } [Parameter, EditorRequired] public EventCallback OnSave { get; set; } private static readonly IReadOnlyList<(string Token, IReadOnlyList Effects)> QuickParseLegendEntries = [ ("+15", [CreateQuickLegendEffect(CriticalEffectCodes.DirectHits, valueInteger: 15)]), ("3s", [CreateQuickLegendEffect(CriticalEffectCodes.StunnedRounds, durationRounds: 3)]), ("1mp", [CreateQuickLegendEffect(CriticalEffectCodes.MustParryRounds, durationRounds: 1)]), ("3np", [CreateQuickLegendEffect(CriticalEffectCodes.NoParryRounds, durationRounds: 3)]), ("1hpr", [CreateQuickLegendEffect(CriticalEffectCodes.BleedPerRound, perRound: 1)]), ("-20", [CreateQuickLegendEffect(CriticalEffectCodes.FoePenalty, modifier: -20)]), ("+20b", [CreateQuickLegendEffect(CriticalEffectCodes.AttackerBonusNextRound, modifier: 20)]), ("+2d10-3pp", [CreateQuickLegendEffect(CriticalEffectCodes.PowerPointModifier, valueExpression: "2d10-3")]) ]; private IJSObjectReference? jsModule; private bool isBackdropPointerDown; protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) { return; } jsModule = await JSRuntime.InvokeAsync( "import", "./Components/Shared/CriticalCellEditorDialog.razor.js"); await jsModule.InvokeVoidAsync("lockBackgroundScroll"); } public async ValueTask DisposeAsync() { if (jsModule is null) { return; } try { await jsModule.InvokeVoidAsync("unlockBackgroundScroll"); await jsModule.DisposeAsync(); } catch (JSDisconnectedException) { } } private void HandleBackdropPointerDown() { isBackdropPointerDown = true; } private async Task HandleBackdropPointerUp() { if (!isBackdropPointerDown) { return; } isBackdropPointerDown = false; await OnClose.InvokeAsync(); } private void HandleBackdropPointerCancel() { isBackdropPointerDown = false; } private void HandleDialogPointerDown() { isBackdropPointerDown = false; } private void HandleDialogPointerUp() { isBackdropPointerDown = false; } private void HandleDialogPointerCancel() { isBackdropPointerDown = false; } private async Task HandleSubmitAsync(EditContext _) { await OnSave.InvokeAsync(); } private async Task HandleReparseClickAsync(MouseEventArgs _) { await OnReparse.InvokeAsync(); } private void HandleQuickParseInputChanged(ChangeEventArgs args) { if (Model is null) { return; } Model.QuickParseInput = args.Value?.ToString() ?? string.Empty; } private void AddBaseEffect() { if (Model is null) { return; } Model.AreEffectsOverridden = true; Model.Effects.Add(CreateDefaultEffectModel()); } private void RemoveBaseEffect(int index) { if (Model is null || index < 0 || index >= Model.Effects.Count) { return; } Model.AreEffectsOverridden = true; Model.Effects.RemoveAt(index); } private void AddBranch() { if (Model is null) { return; } Model.Branches.Add(new CriticalBranchEditorModel { ConditionText = $"Condition {Model.Branches.Count + 1}", SortOrder = Model.Branches.Count + 1, IsOverridden = true }); Model.AreBranchesOverridden = true; } private void RemoveBranch(int index) { if (Model is null || index < 0 || index >= Model.Branches.Count) { return; } Model.AreBranchesOverridden = true; Model.Branches.RemoveAt(index); } private static void AddBranchEffect(CriticalBranchEditorModel branch) { branch.AreEffectsOverridden = true; branch.Effects.Add(CreateDefaultEffectModel()); } private static void RemoveBranchEffect(CriticalBranchEditorModel branch, int index) { if (index < 0 || index >= branch.Effects.Count) { return; } branch.AreEffectsOverridden = true; branch.Effects.RemoveAt(index); } private static CriticalEffectEditorModel CreateDefaultEffectModel() => new() { EffectCode = CriticalEffectCodes.DirectHits, SourceType = "symbol", IsOverridden = true }; private static string GetBranchTitle(CriticalBranchEditorModel branch, int index) => string.IsNullOrWhiteSpace(branch.ConditionText) ? $"Condition {index + 1}" : branch.ConditionText; private static string GetEffectLabel(CriticalEffectEditorModel effect) { if (AffixDisplayMap.TryGet(effect.EffectCode, out var info)) { return info.Label; } return string.IsNullOrWhiteSpace(effect.EffectCode) ? "Custom Effect" : effect.EffectCode; } private static IEnumerable> GetEffectOptions(string? currentCode) { if (!string.IsNullOrWhiteSpace(currentCode) && !AffixDisplayMap.Entries.ContainsKey(currentCode)) { yield return new KeyValuePair(currentCode, currentCode); } foreach (var entry in AffixDisplayMap.Entries.OrderBy(item => item.Value.Label)) { yield return new KeyValuePair(entry.Key, entry.Value.Label); } } private static void HandleEffectCodeChanged(CriticalEffectEditorModel effect, string? newCode) { var normalizedCode = newCode?.Trim() ?? string.Empty; if (string.Equals(effect.EffectCode, normalizedCode, StringComparison.Ordinal)) { return; } effect.EffectCode = normalizedCode; effect.Target = null; effect.ValueInteger = null; effect.ValueDecimal = null; effect.ValueExpression = null; effect.DurationRounds = null; effect.PerRound = null; effect.Modifier = null; effect.BodyPart = null; effect.IsPermanent = false; effect.SourceText = null; effect.SourceType = AffixDisplayMap.TryGet(effect.EffectCode, out _) ? "symbol" : "manual"; effect.IsOverridden = true; } private void MarkDescriptionOverridden() { if (Model is not null) { Model.IsDescriptionOverridden = true; } } private static void MarkBranchOverridden(CriticalBranchEditorModel branch) { branch.IsOverridden = true; } private static void MarkEffectOverridden(CriticalEffectEditorModel effect) { effect.IsOverridden = true; } private static string GetReviewSummary(CriticalCellEditorModel model, CriticalCellEditorModel? comparisonBaseline) { var differenceCount = GetComparisonDifferenceCount(model, comparisonBaseline); var noteCount = model.ValidationMessages.Count; var segments = new List(); 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" : string.Join(" · ", segments); } private static string GetParserNoteSummary(int noteCount) => noteCount == 1 ? "1 parser note is available under Advanced Review." : $"{noteCount} parser notes are available under Advanced Review."; private static CriticalEffectLookupResponse CreateQuickLegendEffect( string effectCode, int? valueInteger = null, string? valueExpression = null, int? durationRounds = null, int? perRound = null, int? modifier = null) => new( effectCode, "foe", valueInteger, valueExpression, durationRounds, perRound, modifier, null, false, "legend", null); private static IReadOnlyList GetComparisonSummaryItems(CriticalCellEditorModel model, CriticalCellEditorModel? comparisonBaseline) { if (model.GeneratedState is null) { return ["Generated comparison is unavailable."]; } var comparisonSourceModel = GetComparisonSourceModel(model, comparisonBaseline); var comparisonSourceEffects = BuildPreviewEffects(comparisonSourceModel); var comparisonSourceBranches = BuildPreviewBranches(comparisonSourceModel); var items = new List(); if (CriticalCellComparisonEvaluator.DescriptionDiffers(comparisonSourceModel.DescriptionText, model.GeneratedState.DescriptionText)) { items.Add("Result text differs"); } if (CriticalCellComparisonEvaluator.EffectsDiffer(comparisonSourceEffects, model.GeneratedState.Effects)) { items.Add("Base effects differ"); } if (CriticalCellComparisonEvaluator.BranchesDiffer(comparisonSourceBranches, model.GeneratedState.Branches)) { items.Add("Conditions differ"); } if (items.Count == 0) { items.Add("Current card matches the fresh generation"); } if (model.GeneratedState.TokenReviewItems.Count > 0) { items.Add($"{model.GeneratedState.TokenReviewItems.Count} token review item{(model.GeneratedState.TokenReviewItems.Count == 1 ? string.Empty : "s")}"); } 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) { if (model.GeneratedState is null) { return 0; } var comparisonSourceModel = GetComparisonSourceModel(model, comparisonBaseline); return CriticalCellComparisonEvaluator.GetDifferenceCount( comparisonSourceModel.DescriptionText, BuildPreviewEffects(comparisonSourceModel), BuildPreviewBranches(comparisonSourceModel), model.GeneratedState); } private static bool HasComparisonDifferences(CriticalCellEditorModel? model, CriticalCellEditorModel? comparisonBaseline) => model is not null && GetComparisonDifferenceCount(model, comparisonBaseline) > 0; 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 string BuildSourceImageAltText(CriticalCellEditorModel model) { var segments = new List { model.TableName, $"roll band {model.RollBand}", $"column {model.ColumnLabel}" }; if (!string.IsNullOrWhiteSpace(model.GroupLabel)) { segments.Add($"variant {model.GroupLabel}"); } return string.Join(", ", segments); } private static IReadOnlyList BuildPreviewEffects(CriticalCellEditorModel model) => model.Effects .Select(CreatePreviewEffect) .ToList(); private static IReadOnlyList BuildPreviewBranches(CriticalCellEditorModel model) => model.Branches .OrderBy(branch => branch.SortOrder) .Select(CreatePreviewBranch) .ToList(); private static IReadOnlyList BuildSingleBadgeEffect(CriticalEffectEditorModel effect) => [CreateBadgeEffect(effect)]; private static CriticalEffectLookupResponse CreateBadgeEffect(CriticalEffectEditorModel 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 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 { private RenderFragment InlineEffectRow(CriticalEffectEditorModel effect, Action onRemove) => @
@InlineValueField(effect) @if (!string.IsNullOrWhiteSpace(effect.BodyPart)) {
}
; private RenderFragment InlineValueField(CriticalEffectEditorModel effect) => @
@switch (effect.EffectCode) { case CriticalEffectCodes.DirectHits: break; case CriticalEffectCodes.StunnedRounds: case CriticalEffectCodes.MustParryRounds: case CriticalEffectCodes.NoParryRounds: break; case CriticalEffectCodes.BleedPerRound: break; case CriticalEffectCodes.FoePenalty: case CriticalEffectCodes.AttackerBonusNextRound: break; case CriticalEffectCodes.PowerPointModifier: break; default: break; }
; }