Files
RpgRoller/RpgRoller/Components/Pages/HomeControls/CampaignLogPanel.razor.cs
2026-04-05 01:32:52 +02:00

229 lines
8.2 KiB
C#

using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using RpgRoller.Contracts;
namespace RpgRoller.Components.Pages.HomeControls;
[ExcludeFromCodeCoverage]
public partial class CampaignLogPanel
{
private sealed record EventBadgeView(string Label, string Tone);
protected override async Task OnAfterRenderAsync(bool firstRender)
{
var currentLastRollId = CampaignLog.LastOrDefault()?.RollId;
if (IsCampaignDataLoading || CampaignLog.Count == 0)
{
LastRenderedLogCount = CampaignLog.Count;
LastRenderedLogRollId = currentLastRollId;
return;
}
if (firstRender || CampaignLog.Count > LastRenderedLogCount || currentLastRollId != LastRenderedLogRollId)
{
try
{
await JS.InvokeVoidAsync("rpgRollerApi.scrollElementToBottom", LogFeedRef);
}
catch (JSDisconnectedException)
{
}
catch (InvalidOperationException ex) when (ex.Message.Contains("statically rendered", StringComparison.OrdinalIgnoreCase))
{
}
}
LastRenderedLogCount = CampaignLog.Count;
LastRenderedLogRollId = currentLastRollId;
}
private async Task SubmitCustomRollAsync()
{
CustomRollState.ResetValidation();
var expression = CustomRollState.Model.Expression.Trim();
if (string.IsNullOrWhiteSpace(expression))
{
SetCustomRollError("Enter a roll expression first.");
return;
}
if (!SelectedCharacterId.HasValue)
{
SetCustomRollError("Select a character to make a custom roll.");
return;
}
IsSubmittingCustomRoll = true;
try
{
var roll = await ApiClient.RequestAsync<RollResult>("POST", $"/api/characters/{SelectedCharacterId.Value}/custom-rolls", new
{
expression,
visibility = NormalizedRollVisibility
});
CustomRollState.Model.Expression = string.Empty;
CustomRollState.ResetValidation();
CustomRollInputVersion += 1;
await CustomRollCreated.InvokeAsync(roll);
await JS.InvokeVoidAsync("rpgRollerApi.clearInputValue", CustomRollInputRef);
await InvokeAsync(StateHasChanged);
}
catch (ApiRequestException ex) when (string.Equals(ex.ErrorCode, "invalid_expression", StringComparison.Ordinal))
{
SetCustomRollError(ex.Message);
await InvokeAsync(StateHasChanged);
}
catch (ApiRequestException ex)
{
await ErrorOccurred.InvokeAsync(ex.Message);
}
finally
{
IsSubmittingCustomRoll = false;
}
}
private void SetCustomRollError(string message)
{
CustomRollState.Errors["expression"] = message;
}
private static IReadOnlyList<EventBadgeView> GetEventBadges(CampaignLogListEntry entry)
{
return (entry.EventBadges ?? []).Select(ToEventBadgeView).Where(badge => badge is not null).Cast<EventBadgeView>().ToArray();
}
private static bool HasSummary(CampaignLogListEntry entry)
{
return (entry.EventBadges?.Length ?? 0) > 0 || !string.IsNullOrWhiteSpace(entry.SummaryText);
}
private static EventBadgeView? ToEventBadgeView(string code)
{
return code switch
{
"w6" => new("Wild 6", "positive"),
"w1" => new("Wild 1", "danger"),
"n20" => new("Nat 20", "positive"),
"n1" => new("Nat 1", "danger"),
"rf" => new("Fumble", "danger"),
"r100" => new("100", "rare"),
"r66" => new("66", "rare"),
_ => null
};
}
private static string LogEntryCssClass(CampaignLogListEntry entry, bool isExpanded, bool isFresh)
{
var classes = new List<string> { entry.VisibilityStyle };
if (isExpanded)
classes.Add("expanded");
if (isFresh)
classes.Add("fresh");
return string.Join(" ", classes);
}
[Inject]
private IJSRuntime JS { get; set; } = null!;
[Inject]
private RpgRollerApiClient ApiClient { get; set; } = null!;
private ElementReference LogPanelRef { get; set; }
private ElementReference LogFeedRef { get; set; }
private ElementReference CustomRollInputRef { get; set; }
private int LastRenderedLogCount { get; set; }
private Guid? LastRenderedLogRollId { get; set; }
private int CustomRollInputVersion { get; set; }
private FormState<CustomRollFormModel> CustomRollState { get; } = new();
private bool IsSubmittingCustomRoll { get; set; }
[Parameter]
public bool IsCampaignDataLoading { get; set; }
[Parameter]
public IReadOnlyList<CampaignLogListEntry> CampaignLog { get; set; } = [];
[Parameter]
public Guid? ExpandedRollId { get; set; }
[Parameter]
public Guid? FreshRollId { get; set; }
[Parameter]
public EventCallback<Guid> ToggleRollDetailRequested { get; set; }
[Parameter]
public Func<Guid, CampaignRollDetail?> ResolveRollDetail { get; set; } = _ => null;
[Parameter]
public Func<Guid, bool> IsRollDetailLoading { get; set; } = _ => false;
[Parameter]
public Func<Guid, string?> GetRollDetailError { get; set; } = _ => null;
[Parameter]
public Guid? SelectedCharacterId { get; set; }
[Parameter]
public string? SelectedCharacterName { get; set; }
[Parameter]
public string SelectedCampaignRulesetId { get; set; } = string.Empty;
[Parameter]
public string RollVisibility { get; set; } = "public";
[Parameter]
public bool IsMutating { get; set; }
[Parameter]
public EventCallback<RollResult> CustomRollCreated { get; set; }
[Parameter]
public EventCallback<string> ErrorOccurred { get; set; }
private bool HasCustomRollError => CustomRollState.Errors.ContainsKey("expression");
private string? CustomRollErrorMessage => CustomRollState.Errors.GetValueOrDefault("expression");
private bool IsCustomRollDisabled => IsCampaignDataLoading || IsMutating || IsSubmittingCustomRoll || !SelectedCharacterId.HasValue;
private string CustomRollInputCssClass => HasCustomRollError ? "custom-roll-input error" : "custom-roll-input";
private string? CustomRollInputTitle => HasCustomRollError ? CustomRollErrorMessage : null;
private string CustomRollErrorElementId => "custom-roll-expression-error";
private string? CustomRollInputDescribedBy => HasCustomRollError ? CustomRollErrorElementId : null;
private string CustomRollPlaceholder => SelectedCampaignRulesetId.ToLowerInvariant() switch
{
RulesetFormHelpers.RulesetIds.D6 => "e.g. 5D+4",
RulesetFormHelpers.RulesetIds.Dnd5e => "e.g. 2d12+2",
RulesetFormHelpers.RulesetIds.Rolemaster => "e.g. d10, 15d10, d100!+85",
_ => "Enter a roll expression"
};
private string CustomRollStatusText => SelectedCharacterId.HasValue && !string.IsNullOrWhiteSpace(SelectedCharacterName) ? $"For {SelectedCharacterName} • {RollVisibilityLabel}" : "Select a character to enable";
private string CustomRollHelpText => SelectedCampaignRulesetId.ToLowerInvariant() switch
{
RulesetFormHelpers.RulesetIds.D6 => "Uses the campaign ruleset. D6 custom rolls use one wild die and allow fumbles.",
RulesetFormHelpers.RulesetIds.Rolemaster => $"{RulesetFormHelpers.RolemasterExampleText()}. Logged for the selected character.",
_ => "Uses the selected campaign ruleset and current visibility."
};
private string RollVisibilityLabel => string.Equals(NormalizedRollVisibility, "private", StringComparison.OrdinalIgnoreCase) ? "Private" : "Public";
private string NormalizedRollVisibility => string.Equals(RollVisibility, "private", StringComparison.OrdinalIgnoreCase) ? "private" : "public";
private string CustomRollExpression
{
get => CustomRollState.Model.Expression;
set
{
CustomRollState.Model.Expression = value;
if (HasCustomRollError)
CustomRollState.ResetValidation();
}
}
}