843 lines
36 KiB
Plaintext
843 lines
36 KiB
Plaintext
@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
|
|
|
|
<div class="critical-editor-backdrop"
|
|
@onpointerdown="HandleBackdropPointerDown"
|
|
@onpointerup="HandleBackdropPointerUp"
|
|
@onpointercancel="HandleBackdropPointerCancel">
|
|
<div class="critical-editor-dialog"
|
|
@onpointerdown="HandleDialogPointerDown"
|
|
@onpointerup="HandleDialogPointerUp"
|
|
@onpointercancel="HandleDialogPointerCancel"
|
|
@onpointerdown:stopPropagation="true"
|
|
@onpointerup:stopPropagation="true"
|
|
@onpointercancel:stopPropagation="true">
|
|
<header class="critical-editor-header">
|
|
<div>
|
|
@if (Model is not null)
|
|
{
|
|
<h3 class="panel-title">Edit Result Card</h3>
|
|
<p class="muted critical-editor-meta">
|
|
<strong>@Model.TableName</strong>
|
|
<span> · Roll band <strong>@Model.RollBand</strong></span>
|
|
<span> · Severity <strong>@Model.ColumnLabel</strong></span>
|
|
@if (!string.IsNullOrWhiteSpace(Model.GroupLabel))
|
|
{
|
|
<span> · Variant <strong>@Model.GroupLabel</strong></span>
|
|
}
|
|
</p>
|
|
}
|
|
else
|
|
{
|
|
<h3 class="panel-title">Edit Result Card</h3>
|
|
}
|
|
</div>
|
|
<button type="button" class="btn btn-link critical-editor-close" @onclick="OnClose">Close</button>
|
|
</header>
|
|
|
|
@if (IsLoading)
|
|
{
|
|
<div class="critical-editor-body">
|
|
<p class="muted">Loading editor...</p>
|
|
</div>
|
|
}
|
|
else if (!string.IsNullOrWhiteSpace(LoadErrorMessage))
|
|
{
|
|
<div class="critical-editor-body">
|
|
<p class="error-text critical-editor-error">@LoadErrorMessage</p>
|
|
</div>
|
|
}
|
|
else if (Model is not null)
|
|
{
|
|
<EditForm @key="Model" Model="Model" OnSubmit="HandleSubmitAsync" class="critical-editor-form">
|
|
<div class="critical-editor-body">
|
|
@if (!string.IsNullOrWhiteSpace(ReparseErrorMessage))
|
|
{
|
|
<p class="error-text critical-editor-error">@ReparseErrorMessage</p>
|
|
}
|
|
|
|
@if (!string.IsNullOrWhiteSpace(SaveErrorMessage))
|
|
{
|
|
<p class="error-text critical-editor-error">@SaveErrorMessage</p>
|
|
}
|
|
|
|
<section class="critical-editor-section">
|
|
<div class="critical-editor-section-header">
|
|
<div>
|
|
<h4>Quick Parse Input</h4>
|
|
<p class="muted">First line is the result prose. Later lines are base affixes or <code>condition: ...</code> lines with comma-separated shorthand.</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="btn-ritual"
|
|
@onclick="HandleReparseClickAsync"
|
|
@onclick:stopPropagation="true"
|
|
@onclick:preventDefault="true"
|
|
disabled="@(IsSaving || IsReparsing)">
|
|
@(IsReparsing ? "Generating..." : "Generate From Quick Input")
|
|
</button>
|
|
</div>
|
|
<div class="critical-editor-quick-parse-grid">
|
|
<div class="field-shell">
|
|
<label>Quick Parse Input</label>
|
|
<textarea
|
|
class="input-shell critical-editor-textarea tall"
|
|
value="@Model.QuickParseInput"
|
|
@oninput="HandleQuickParseInputChanged"></textarea>
|
|
</div>
|
|
|
|
<div class="critical-editor-quick-parse-source">
|
|
<div class="critical-editor-quick-parse-source-header">
|
|
<label>Source Cell</label>
|
|
@if (Model.SourcePageNumber is not null)
|
|
{
|
|
<span class="chip">Page @Model.SourcePageNumber</span>
|
|
}
|
|
</div>
|
|
|
|
<div class="critical-editor-quick-parse-source-frame">
|
|
@if (!string.IsNullOrWhiteSpace(Model.SourceImageUrl))
|
|
{
|
|
<img
|
|
class="critical-editor-source-image is-inline"
|
|
src="@Model.SourceImageUrl"
|
|
alt="@BuildSourceImageAltText(Model)" />
|
|
}
|
|
else
|
|
{
|
|
<p class="muted critical-editor-inline-copy">No source image is available for this cell yet.</p>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p class="muted critical-editor-advanced-hint">Example: <code>Foe brings his guard up, frightened by your display.</code> then <code>+5, 1mp</code> or <code>w/o shield: glancing blow, +15, 3s, 3np</code>.</p>
|
|
<div class="critical-editor-quick-legend">
|
|
@foreach (var entry in QuickParseLegendEntries)
|
|
{
|
|
<div class="critical-editor-quick-legend-item">
|
|
<code>@entry.Token</code>
|
|
<span class="critical-editor-quick-legend-equals">=</span>
|
|
<AffixBadgeList Effects="@entry.Effects" />
|
|
</div>
|
|
}
|
|
</div>
|
|
@if (Model.ValidationMessages.Count > 0)
|
|
{
|
|
<p class="muted critical-editor-advanced-hint">@GetParserNoteSummary(Model.ValidationMessages.Count)</p>
|
|
}
|
|
@if (HasComparisonDifferences(Model, ComparisonBaseline))
|
|
{
|
|
<p class="muted critical-editor-advanced-hint">Fresh generation differs from the current edited card. Review Generated Compare before saving if you want to keep the overrides.</p>
|
|
}
|
|
<div class="field-shell">
|
|
<label>Result Text</label>
|
|
<InputTextArea class="input-shell critical-editor-textarea compact" @bind-Value="Model.DescriptionText" @bind-Value:after="MarkDescriptionOverridden" />
|
|
</div>
|
|
</section>
|
|
|
|
<section class="critical-editor-section">
|
|
<div class="critical-editor-section-header">
|
|
<div>
|
|
<h4>Base Effects</h4>
|
|
<p class="muted">Edit the badges and values that appear on the main result.</p>
|
|
</div>
|
|
<button type="button" class="btn-ritual critical-editor-compact-button" @onclick="AddBaseEffect">Add Effect</button>
|
|
</div>
|
|
@if (Model.Effects.Count == 0)
|
|
{
|
|
<p class="muted">No base effects on this result yet.</p>
|
|
}
|
|
else
|
|
{
|
|
<div class="critical-editor-inline-list">
|
|
@for (var index = 0; index < Model.Effects.Count; index++)
|
|
{
|
|
var i = index;
|
|
var effect = Model.Effects[i];
|
|
<div class="critical-editor-inline-row">
|
|
@InlineEffectRow(effect, () => RemoveBaseEffect(i))
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</section>
|
|
|
|
<section class="critical-editor-section">
|
|
<div class="critical-editor-section-header">
|
|
<div>
|
|
<h4>Conditions</h4>
|
|
<p class="muted">Keep alternate outcomes compact and easy to scan.</p>
|
|
</div>
|
|
<button type="button" class="btn-ritual critical-editor-compact-button" @onclick="AddBranch">Add Condition</button>
|
|
</div>
|
|
@if (Model.Branches.Count == 0)
|
|
{
|
|
<p class="muted">No alternate condition cards on this result yet.</p>
|
|
}
|
|
else
|
|
{
|
|
@for (var index = 0; index < Model.Branches.Count; index++)
|
|
{
|
|
var i = index;
|
|
var branch = Model.Branches[i];
|
|
<div class="critical-editor-card branch-card-editor">
|
|
<div class="critical-editor-card-header">
|
|
<div>
|
|
<strong>@GetBranchTitle(branch, i)</strong>
|
|
<p class="muted critical-editor-inline-copy">Shown only when this condition applies.</p>
|
|
</div>
|
|
<button type="button" class="critical-editor-inline-button" @onclick="() => RemoveBranch(i)">Remove</button>
|
|
</div>
|
|
<div class="critical-editor-branch-line">
|
|
<div class="field-shell">
|
|
<label>Condition</label>
|
|
<InputText class="input-shell" @bind-Value="branch.ConditionText" @bind-Value:after="() => MarkBranchOverridden(branch)" />
|
|
</div>
|
|
<div class="field-shell critical-editor-branch-outcome">
|
|
<label>Outcome Text</label>
|
|
<InputText class="input-shell" @bind-Value="branch.DescriptionText" @bind-Value:after="() => MarkBranchOverridden(branch)" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="critical-editor-subsection">
|
|
<div class="critical-editor-section-header">
|
|
<div>
|
|
<h5>Condition Effects</h5>
|
|
<p class="muted">These only appear when the condition is met.</p>
|
|
</div>
|
|
<button type="button" class="btn-ritual critical-editor-compact-button" @onclick="() => AddBranchEffect(branch)">Add Effect</button>
|
|
</div>
|
|
|
|
@if (branch.Effects.Count == 0)
|
|
{
|
|
<p class="muted">No effects on this condition yet.</p>
|
|
}
|
|
else
|
|
{
|
|
<div class="critical-editor-inline-list">
|
|
@for (var effectIndex = 0; effectIndex < branch.Effects.Count; effectIndex++)
|
|
{
|
|
var j = effectIndex;
|
|
var effect = branch.Effects[j];
|
|
<div class="critical-editor-inline-row">
|
|
@InlineEffectRow(effect, () => RemoveBranchEffect(branch, j))
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
</section>
|
|
|
|
<section class="critical-editor-section">
|
|
<details class="critical-editor-advanced">
|
|
<summary class="critical-editor-advanced-summary">
|
|
<span>Advanced Review</span>
|
|
<span class="critical-editor-advanced-meta">@GetReviewSummary(Model, ComparisonBaseline)</span>
|
|
</summary>
|
|
|
|
<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 generated result from the quick parse input.</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 Quick Input"
|
|
Caption="This is the fresh generated 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>
|
|
|
|
@if (Model.GeneratedState.TokenReviewItems.Count > 0)
|
|
{
|
|
<div class="critical-editor-card nested">
|
|
<div class="critical-editor-card-header">
|
|
<div>
|
|
<strong>Token Review</strong>
|
|
<p class="muted critical-editor-inline-copy">These tokens were unknown or only partially understood during generation and need manual review.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="critical-editor-validation-list">
|
|
@foreach (var issue in Model.GeneratedState.TokenReviewItems)
|
|
{
|
|
<p class="critical-editor-validation-item">@issue.ReviewText</p>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
</div>
|
|
</details>
|
|
</section>
|
|
</div>
|
|
|
|
<footer class="critical-editor-footer">
|
|
<button type="button" class="btn btn-link" @onclick="OnClose" disabled="@(IsSaving)">Cancel</button>
|
|
<button type="submit" class="btn-ritual" disabled="@(IsSaving)">
|
|
@(IsSaving ? "Saving..." : "Save Cell")
|
|
</button>
|
|
</footer>
|
|
</EditForm>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
@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<CriticalEffectLookupResponse> 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<IJSObjectReference>(
|
|
"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<KeyValuePair<string, string>> GetEffectOptions(string? currentCode)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(currentCode) && !AffixDisplayMap.Entries.ContainsKey(currentCode))
|
|
{
|
|
yield return new KeyValuePair<string, string>(currentCode, currentCode);
|
|
}
|
|
|
|
foreach (var entry in AffixDisplayMap.Entries.OrderBy(item => item.Value.Label))
|
|
{
|
|
yield return new KeyValuePair<string, string>(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<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" : 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<string> 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<string>();
|
|
|
|
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<string>
|
|
{
|
|
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<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 IReadOnlyList<CriticalEffectLookupResponse> 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) => @<div class="critical-editor-effect-row">
|
|
<div class="critical-editor-effect-row-main">
|
|
<div class="critical-editor-effect-badge">
|
|
<AffixBadgeList Effects="@BuildSingleBadgeEffect(effect)" />
|
|
</div>
|
|
<div class="field-shell critical-editor-effect-kind">
|
|
<label>Effect</label>
|
|
<select class="input-shell" value="@effect.EffectCode" @onchange="args => HandleEffectCodeChanged(effect, args.Value?.ToString())">
|
|
@foreach (var option in GetEffectOptions(effect.EffectCode))
|
|
{
|
|
<option value="@option.Key">@option.Value</option>
|
|
}
|
|
</select>
|
|
</div>
|
|
@InlineValueField(effect)
|
|
@if (!string.IsNullOrWhiteSpace(effect.BodyPart))
|
|
{
|
|
<div class="field-shell critical-editor-effect-extra">
|
|
<label>Body Part</label>
|
|
<InputText class="input-shell" @bind-Value="effect.BodyPart" @bind-Value:after="() => MarkEffectOverridden(effect)" />
|
|
</div>
|
|
}
|
|
</div>
|
|
<button type="button" class="critical-editor-inline-button" @onclick="onRemove">Remove</button>
|
|
</div>;
|
|
|
|
private RenderFragment InlineValueField(CriticalEffectEditorModel effect) => @<div class="field-shell critical-editor-effect-value">
|
|
@switch (effect.EffectCode)
|
|
{
|
|
case CriticalEffectCodes.DirectHits:
|
|
<label>Hits</label>
|
|
<InputNumber TValue="int?" class="input-shell" @bind-Value="effect.ValueInteger" @bind-Value:after="() => MarkEffectOverridden(effect)" />
|
|
break;
|
|
case CriticalEffectCodes.StunnedRounds:
|
|
case CriticalEffectCodes.MustParryRounds:
|
|
case CriticalEffectCodes.NoParryRounds:
|
|
<label>Rounds</label>
|
|
<InputNumber TValue="int?" class="input-shell" @bind-Value="effect.DurationRounds" @bind-Value:after="() => MarkEffectOverridden(effect)" />
|
|
break;
|
|
case CriticalEffectCodes.BleedPerRound:
|
|
<label>Bleed / Round</label>
|
|
<InputNumber TValue="int?" class="input-shell" @bind-Value="effect.PerRound" @bind-Value:after="() => MarkEffectOverridden(effect)" />
|
|
break;
|
|
case CriticalEffectCodes.FoePenalty:
|
|
case CriticalEffectCodes.AttackerBonusNextRound:
|
|
<label>Modifier</label>
|
|
<InputNumber TValue="int?" class="input-shell" @bind-Value="effect.Modifier" @bind-Value:after="() => MarkEffectOverridden(effect)" />
|
|
break;
|
|
case CriticalEffectCodes.PowerPointModifier:
|
|
<label>Expression</label>
|
|
<InputText class="input-shell" @bind-Value="effect.ValueExpression" @bind-Value:after="() => MarkEffectOverridden(effect)" />
|
|
break;
|
|
default:
|
|
<label>Display Text</label>
|
|
<InputText class="input-shell" @bind-Value="effect.SourceText" @bind-Value:after="() => MarkEffectOverridden(effect)" />
|
|
break;
|
|
}
|
|
</div>;
|
|
}
|