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

480 lines
21 KiB
Plaintext

@using System
@using System.Collections.Generic
@using System.Linq
@using RolemasterDb.App.Domain
@using RolemasterDb.App.Features
<div class="critical-editor-backdrop" @onclick="HandleBackdropClicked">
<div class="critical-editor-dialog" @onclick: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 Model="Model" OnSubmit="HandleSubmitAsync" class="critical-editor-form">
<div class="critical-editor-body">
@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>Result Preview</h4>
<p class="muted">This is the card the table browser will show.</p>
</div>
</div>
<div class="result-card critical-editor-preview-card">
<div class="result-stats">
<span class="stat-pill">Severity: @Model.ColumnLabel</span>
<span class="stat-pill">Roll band: @Model.RollBand</span>
@if (!string.IsNullOrWhiteSpace(Model.GroupLabel))
{
<span class="stat-pill">Variant: @Model.GroupLabel</span>
}
</div>
<CompactCriticalCell
Description="@Model.DescriptionText"
Effects="@BuildPreviewEffects(Model.Effects)"
Branches="@BuildPreviewBranches(Model.Branches)" />
</div>
</section>
<section class="critical-editor-section">
<div class="critical-editor-section-header">
<div>
<h4>Raw Text</h4>
<p class="muted">Update the source text, then adjust the visible card fields below.</p>
</div>
</div>
<div class="field-shell">
<label>Raw Cell Text</label>
<InputTextArea class="input-shell critical-editor-textarea tall" @bind-Value="Model.RawCellText" />
</div>
<div class="field-shell">
<label>Result Text Override</label>
<InputTextArea class="input-shell critical-editor-textarea compact" @bind-Value="Model.DescriptionText" />
</div>
</section>
<section class="critical-editor-section">
<div class="critical-editor-section-header">
<div>
<h4>Base Effects</h4>
<p class="muted">These chips appear on the main result.</p>
</div>
<button type="button" class="btn-ritual" @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-chip-list">
@for (var index = 0; index < Model.Effects.Count; index++)
{
var effect = Model.Effects[index];
var effectIndex = index;
<div class="critical-editor-chip-card">
<div class="critical-editor-chip-preview">
<AffixBadgeList Effects="@BuildSinglePreviewEffect(effect)" />
<span class="critical-editor-chip-name">@GetEffectLabel(effect)</span>
</div>
<button type="button" class="btn btn-link" @onclick="() => RemoveBaseEffect(effectIndex)">Remove</button>
</div>
}
</div>
@for (var index = 0; index < Model.Effects.Count; index++)
{
var effect = Model.Effects[index];
<div class="critical-editor-card nested">
<div class="critical-editor-card-header">
<strong>@GetEffectLabel(effect)</strong>
</div>
@EffectFields(effect)
</div>
}
}
</section>
<section class="critical-editor-section">
<div class="critical-editor-section-header">
<div>
<h4>Conditions</h4>
<p class="muted">Use condition cards for alternate outcomes.</p>
</div>
<button type="button" class="btn-ritual" @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 branch = Model.Branches[index];
<div class="critical-editor-card branch-card-editor">
<div class="critical-editor-card-header">
<div>
<strong>@GetBranchTitle(branch, index)</strong>
<p class="muted critical-editor-inline-copy">Shown when this condition applies.</p>
</div>
<button type="button" class="btn btn-link" @onclick="() => RemoveBranch(index)">Remove</button>
</div>
<div class="field-shell">
<label>Condition</label>
<InputText class="input-shell" @bind-Value="branch.ConditionText" />
</div>
<div class="field-shell">
<label>Outcome Text</label>
<InputTextArea class="input-shell critical-editor-textarea compact" @bind-Value="branch.DescriptionText" />
</div>
<div class="critical-editor-subsection">
<div class="critical-editor-section-header">
<div>
<h5>Condition Effects</h5>
<p class="muted">These chips only appear when the condition is met.</p>
</div>
<button type="button" class="btn-ritual" @onclick="() => AddBranchEffect(branch)">Add Effect</button>
</div>
@if (branch.Effects.Count == 0)
{
<p class="muted">No effects on this condition card yet.</p>
}
else
{
<div class="critical-editor-chip-list">
@for (var effectIndex = 0; effectIndex < branch.Effects.Count; effectIndex++)
{
var effect = branch.Effects[effectIndex];
var localEffectIndex = effectIndex;
<div class="critical-editor-chip-card">
<div class="critical-editor-chip-preview">
<AffixBadgeList Effects="@BuildSinglePreviewEffect(effect)" />
<span class="critical-editor-chip-name">@GetEffectLabel(effect)</span>
</div>
<button type="button" class="btn btn-link" @onclick="() => RemoveBranchEffect(branch, localEffectIndex)">Remove</button>
</div>
}
</div>
@for (var effectIndex = 0; effectIndex < branch.Effects.Count; effectIndex++)
{
var effect = branch.Effects[effectIndex];
<div class="critical-editor-card nested">
<div class="critical-editor-card-header">
<strong>@GetEffectLabel(effect)</strong>
</div>
@EffectFields(effect)
</div>
}
}
</div>
</div>
}
}
</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 bool IsLoading { get; set; }
[Parameter]
public bool IsSaving { get; set; }
[Parameter]
public string? LoadErrorMessage { get; set; }
[Parameter]
public string? SaveErrorMessage { get; set; }
[Parameter, EditorRequired]
public EventCallback OnClose { get; set; }
[Parameter, EditorRequired]
public EventCallback OnSave { get; set; }
private async Task HandleBackdropClicked()
{
await OnClose.InvokeAsync();
}
private async Task HandleSubmitAsync(EditContext _)
{
await OnSave.InvokeAsync();
}
private void AddBaseEffect()
{
Model?.Effects.Add(CreateDefaultEffectModel());
}
private void RemoveBaseEffect(int index)
{
if (Model is null || index < 0 || index >= Model.Effects.Count)
{
return;
}
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
});
}
private void RemoveBranch(int index)
{
if (Model is null || index < 0 || index >= Model.Branches.Count)
{
return;
}
Model.Branches.RemoveAt(index);
}
private static void AddBranchEffect(CriticalBranchEditorModel branch)
{
branch.Effects.Add(CreateDefaultEffectModel());
}
private static void RemoveBranchEffect(CriticalBranchEditorModel branch, int index)
{
if (index < 0 || index >= branch.Effects.Count)
{
return;
}
branch.Effects.RemoveAt(index);
}
private static CriticalEffectEditorModel CreateDefaultEffectModel() =>
new()
{
EffectCode = CriticalEffectCodes.DirectHits,
SourceType = "symbol"
};
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";
}
private static IReadOnlyList<CriticalEffectLookupResponse> BuildPreviewEffects(IEnumerable<CriticalEffectEditorModel> effects) =>
effects.Select(CreatePreviewEffect).ToList();
private static IReadOnlyList<CriticalEffectLookupResponse> BuildSinglePreviewEffect(CriticalEffectEditorModel effect) =>
[CreatePreviewEffect(effect)];
private static IReadOnlyList<CriticalBranchLookupResponse> BuildPreviewBranches(IEnumerable<CriticalBranchEditorModel> branches) =>
branches
.OrderBy(branch => branch.SortOrder)
.Select(branch => new CriticalBranchLookupResponse(
branch.BranchKind,
branch.ConditionKey,
branch.ConditionText,
branch.DescriptionText,
branch.RawAffixText,
BuildPreviewEffects(branch.Effects),
branch.RawText,
branch.SortOrder))
.ToList();
private static CriticalEffectLookupResponse CreatePreviewEffect(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);
}
@functions {
private RenderFragment EffectFields(CriticalEffectEditorModel effect) => @<div class="critical-editor-effect-fields">
<div class="form-grid critical-editor-effect-grid">
<div class="field-shell">
<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>
@switch (effect.EffectCode)
{
case CriticalEffectCodes.DirectHits:
<div class="field-shell">
<label>Hits</label>
<InputNumber TValue="int?" class="input-shell" @bind-Value="effect.ValueInteger" />
</div>
break;
case CriticalEffectCodes.StunnedRounds:
case CriticalEffectCodes.MustParryRounds:
case CriticalEffectCodes.NoParryRounds:
<div class="field-shell">
<label>Rounds</label>
<InputNumber TValue="int?" class="input-shell" @bind-Value="effect.DurationRounds" />
</div>
break;
case CriticalEffectCodes.BleedPerRound:
<div class="field-shell">
<label>Bleed / Round</label>
<InputNumber TValue="int?" class="input-shell" @bind-Value="effect.PerRound" />
</div>
break;
case CriticalEffectCodes.FoePenalty:
case CriticalEffectCodes.AttackerBonusNextRound:
<div class="field-shell">
<label>Modifier</label>
<InputNumber TValue="int?" class="input-shell" @bind-Value="effect.Modifier" />
</div>
break;
case CriticalEffectCodes.PowerPointModifier:
<div class="field-shell">
<label>Expression</label>
<InputText class="input-shell" @bind-Value="effect.ValueExpression" />
</div>
break;
default:
<div class="field-shell">
<label>Display Text</label>
<InputText class="input-shell" @bind-Value="effect.SourceText" />
</div>
break;
}
@if (!string.IsNullOrWhiteSpace(effect.BodyPart))
{
<div class="field-shell">
<label>Body Part</label>
<InputText class="input-shell" @bind-Value="effect.BodyPart" />
</div>
}
@if (!string.IsNullOrWhiteSpace(effect.Target))
{
<div class="field-shell">
<label>Target</label>
<InputText class="input-shell" @bind-Value="effect.Target" />
</div>
}
</div>
<label class="critical-editor-checkbox">
<InputCheckbox class="form-check-input" @bind-Value="effect.IsPermanent" />
<span>Permanent</span>
</label>
</div>;
}