Files
RolemasterDB/src/RolemasterDb.App/Components/Shared/CriticalCellEditorDialog.razor

800 lines
34 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="field-shell">
<label>Quick Parse Input</label>
<textarea
class="input-shell critical-editor-textarea tall"
value="@Model.QuickParseInput"
@oninput="HandleQuickParseInputChanged"></textarea>
</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 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>;
}