Add rolemaster situational modifier modal
This commit is contained in:
@@ -74,9 +74,9 @@ public partial class CharacterPanel
|
||||
await RollVisibilityChanged.InvokeAsync(selectedVisibility);
|
||||
}
|
||||
|
||||
private async Task RollSkillAsync(Guid skillId)
|
||||
private async Task RollSkillAsync(CharacterSheetSkill skill)
|
||||
{
|
||||
await RollRequested.InvokeAsync(skillId);
|
||||
await RollRequested.InvokeAsync(skill);
|
||||
}
|
||||
|
||||
private Task OnAddSkillRequestedAsync(Guid? skillGroupId)
|
||||
@@ -408,5 +408,5 @@ public partial class CharacterPanel
|
||||
public EventCallback<string> ErrorOccurred { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<Guid> RollRequested { get; set; }
|
||||
public EventCallback<CharacterSheetSkill> RollRequested { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
@if (Visible)
|
||||
{
|
||||
<div class="modal-overlay" role="presentation" @onclick="HandleOverlayClickAsync">
|
||||
<section class="modal-card rolemaster-roll-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Rolemaster situational modifier"
|
||||
tabindex="-1"
|
||||
@onclick:stopPropagation="true"
|
||||
@onkeydown="HandleKeyDownAsync">
|
||||
<h2>Rolemaster skill roll</h2>
|
||||
<p class="muted">Roll <strong>@SkillName</strong> using <code>@Expression</code>.</p>
|
||||
@if (!string.IsNullOrWhiteSpace(ErrorMessage))
|
||||
{
|
||||
<p class="form-error">@ErrorMessage</p>
|
||||
}
|
||||
<form class="form-grid" @onsubmit="SubmitAsync" @onsubmit:preventDefault>
|
||||
<label for="@ModifierInputId">Situational modifier</label>
|
||||
<input id="@ModifierInputId"
|
||||
@ref="ModifierInputElement"
|
||||
value="@CurrentModifierText"
|
||||
@oninput="OnModifierInput"
|
||||
placeholder="Blank = 0"
|
||||
inputmode="numeric"
|
||||
autocomplete="off"/>
|
||||
<p class="field-help">Optional one-shot bonus or penalty. Examples: <code>20</code>, <code>-15</code>, or blank for <code>0</code>.</p>
|
||||
<div class="inline-actions">
|
||||
<button type="submit" disabled="@(IsMutating || IsSubmitting)">Roll</button>
|
||||
<button type="button" class="ghost" disabled="@(IsMutating || IsSubmitting)" @onclick="CancelRequested">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
|
||||
namespace RpgRoller.Components.Pages.HomeControls;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public partial class RolemasterSkillRollModal
|
||||
{
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
CurrentModifierText = ModifierText;
|
||||
if (!Visible || WasVisible)
|
||||
{
|
||||
WasVisible = Visible;
|
||||
return;
|
||||
}
|
||||
|
||||
PendingFocus = true;
|
||||
WasVisible = true;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!Visible || !PendingFocus)
|
||||
return;
|
||||
|
||||
PendingFocus = false;
|
||||
await ModifierInputElement.FocusAsync();
|
||||
}
|
||||
|
||||
private Task OnModifierInput(ChangeEventArgs args)
|
||||
{
|
||||
CurrentModifierText = args.Value?.ToString() ?? string.Empty;
|
||||
return ModifierTextChanged.InvokeAsync(CurrentModifierText);
|
||||
}
|
||||
|
||||
private Task SubmitAsync()
|
||||
{
|
||||
return ConfirmRequested.InvokeAsync(CurrentModifierText);
|
||||
}
|
||||
|
||||
private Task HandleOverlayClickAsync()
|
||||
{
|
||||
if (IsMutating || IsSubmitting)
|
||||
return Task.CompletedTask;
|
||||
|
||||
return CancelRequested.InvokeAsync();
|
||||
}
|
||||
|
||||
private Task HandleKeyDownAsync(KeyboardEventArgs args)
|
||||
{
|
||||
if ((IsMutating || IsSubmitting) || !string.Equals(args.Key, "Escape", StringComparison.Ordinal))
|
||||
return Task.CompletedTask;
|
||||
|
||||
return CancelRequested.InvokeAsync();
|
||||
}
|
||||
|
||||
private bool PendingFocus { get; set; }
|
||||
private bool WasVisible { get; set; }
|
||||
private string CurrentModifierText { get; set; } = string.Empty;
|
||||
private ElementReference ModifierInputElement { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool Visible { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string SkillName { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string Expression { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public string ModifierText { get; set; } = string.Empty;
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<string> ModifierTextChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool IsMutating { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public bool IsSubmitting { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string ModifierInputId { get; set; } = "rolemaster-situational-modifier";
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<string> ConfirmRequested { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback CancelRequested { get; set; }
|
||||
}
|
||||
@@ -52,7 +52,7 @@
|
||||
class="chip-button"
|
||||
title="Roll skill"
|
||||
disabled="@(IsMutating)"
|
||||
@onclick="() => RollSkillRequested.InvokeAsync(skill.Id)">
|
||||
@onclick="() => RollSkillRequested.InvokeAsync(skill)">
|
||||
<span aria-hidden="true" class="emoji">🎲</span>
|
||||
<span class="sr-only">Roll @skill.Name</span>
|
||||
</button>
|
||||
|
||||
@@ -47,7 +47,7 @@ public partial class SkillGroupBlock
|
||||
public EventCallback<CharacterSheetSkill> EditSkillRequested { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<Guid> RollSkillRequested { get; set; }
|
||||
public EventCallback<CharacterSheetSkill> RollSkillRequested { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<Guid> DeleteSkillRequested { get; set; }
|
||||
|
||||
@@ -216,4 +216,16 @@
|
||||
AllowOwnerEdit="State.CanEditCharacterOwner"
|
||||
AvailableUsernames="State.KnownUsernames"
|
||||
CharacterSaved="Campaigns.OnCharacterUpdatedAsync"
|
||||
CancelRequested="Campaigns.CloseCharacterModals"/>
|
||||
CancelRequested="Campaigns.CloseCharacterModals"/>
|
||||
|
||||
<RolemasterSkillRollModal
|
||||
Visible="State.ShowRolemasterSkillRollModal"
|
||||
SkillName="@(State.PendingRolemasterSkillRoll?.Name ?? string.Empty)"
|
||||
Expression="@(State.PendingRolemasterSkillRoll?.DiceRollDefinition ?? string.Empty)"
|
||||
ModifierText="@State.PendingRolemasterSituationalModifier"
|
||||
ModifierTextChanged="@(text => State.PendingRolemasterSituationalModifier = text)"
|
||||
ErrorMessage="@State.PendingRolemasterSkillRollError"
|
||||
IsMutating="State.IsMutating"
|
||||
IsSubmitting="State.IsSubmittingRolemasterSkillRoll"
|
||||
ConfirmRequested="Play.SubmitRolemasterSkillRollAsync"
|
||||
CancelRequested="Play.CancelRolemasterSkillRollAsync"/>
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using RpgRoller.Components.Pages.HomeControls;
|
||||
using RpgRoller.Contracts;
|
||||
|
||||
namespace RpgRoller.Components.Pages;
|
||||
@@ -143,23 +144,71 @@ public sealed class WorkspacePlayCoordinator(WorkspaceState state, WorkspaceFeed
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task RollSkillAsync(Guid skillId)
|
||||
public Task RollSkillAsync(CharacterSheetSkill skill)
|
||||
{
|
||||
if (state.SelectedCampaign is null)
|
||||
{
|
||||
feedback.SetStatus("No campaign selected.", true);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (string.Equals(state.SelectedCampaign.RulesetId, RulesetFormHelpers.RulesetIds.Rolemaster, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
OpenRolemasterSkillRollModal(skill);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return ExecuteSkillRollAsync(skill.Id, 0);
|
||||
}
|
||||
|
||||
public async Task SubmitRolemasterSkillRollAsync(string situationalModifierText)
|
||||
{
|
||||
if (state.PendingRolemasterSkillRoll is null)
|
||||
return;
|
||||
|
||||
if (!TryParseSituationalModifier(situationalModifierText, out var situationalModifier, out var errorMessage))
|
||||
{
|
||||
state.PendingRolemasterSkillRollError = errorMessage;
|
||||
return;
|
||||
}
|
||||
|
||||
state.PendingRolemasterSituationalModifier = situationalModifierText;
|
||||
state.PendingRolemasterSkillRollError = null;
|
||||
state.IsSubmittingRolemasterSkillRoll = true;
|
||||
try
|
||||
{
|
||||
await ExecuteSkillRollAsync(state.PendingRolemasterSkillRoll.Id, situationalModifier, keepModalOpenOnError: true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
state.IsSubmittingRolemasterSkillRoll = false;
|
||||
}
|
||||
}
|
||||
|
||||
public Task CancelRolemasterSkillRollAsync()
|
||||
{
|
||||
if (state.IsSubmittingRolemasterSkillRoll)
|
||||
return Task.CompletedTask;
|
||||
|
||||
CloseRolemasterSkillRollModal();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task ExecuteSkillRollAsync(Guid skillId, int situationalModifier, bool keepModalOpenOnError = false)
|
||||
{
|
||||
state.IsMutating = true;
|
||||
try
|
||||
{
|
||||
var roll = await apiClient.RequestAsync<RollResult>("POST", $"/api/skills/{skillId}/roll", new RollSkillRequest(state.RollVisibility));
|
||||
var roll = await apiClient.RequestAsync<RollResult>("POST", $"/api/skills/{skillId}/roll", new RollSkillRequest(state.RollVisibility, situationalModifier));
|
||||
CloseRolemasterSkillRollModal();
|
||||
await HandleRecordedRollAsync(roll);
|
||||
}
|
||||
catch (ApiRequestException ex)
|
||||
{
|
||||
feedback.SetStatus(ex.Message, true);
|
||||
if (keepModalOpenOnError)
|
||||
state.PendingRolemasterSkillRollError = ex.Message;
|
||||
else
|
||||
feedback.SetStatus(ex.Message, true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -217,6 +266,48 @@ public sealed class WorkspacePlayCoordinator(WorkspaceState state, WorkspaceFeed
|
||||
state.CurrentCampaignState = null;
|
||||
}
|
||||
|
||||
private void OpenRolemasterSkillRollModal(CharacterSheetSkill skill)
|
||||
{
|
||||
state.PendingRolemasterSkillRoll = skill;
|
||||
state.PendingRolemasterSituationalModifier = string.Empty;
|
||||
state.PendingRolemasterSkillRollError = null;
|
||||
state.ShowRolemasterSkillRollModal = true;
|
||||
}
|
||||
|
||||
private void CloseRolemasterSkillRollModal()
|
||||
{
|
||||
state.ShowRolemasterSkillRollModal = false;
|
||||
state.PendingRolemasterSkillRoll = null;
|
||||
state.PendingRolemasterSituationalModifier = string.Empty;
|
||||
state.PendingRolemasterSkillRollError = null;
|
||||
state.IsSubmittingRolemasterSkillRoll = false;
|
||||
}
|
||||
|
||||
private static bool TryParseSituationalModifier(string? text, out int situationalModifier, out string? errorMessage)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
situationalModifier = 0;
|
||||
errorMessage = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!int.TryParse(text.Trim(), out situationalModifier))
|
||||
{
|
||||
errorMessage = "Enter a whole number like 20, -15, or leave blank for 0.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (situationalModifier is < -MaxSituationalModifier or > MaxSituationalModifier)
|
||||
{
|
||||
errorMessage = $"Enter a whole number between {-MaxSituationalModifier} and {MaxSituationalModifier}.";
|
||||
return false;
|
||||
}
|
||||
|
||||
errorMessage = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task EnsureSelectedCharacterActiveCoreAsync()
|
||||
{
|
||||
if (!state.SelectedCharacterId.HasValue || state.SelectedCampaign is null)
|
||||
@@ -312,4 +403,5 @@ public sealed class WorkspacePlayCoordinator(WorkspaceState state, WorkspaceFeed
|
||||
}
|
||||
|
||||
private const int CampaignLogWindowSize = 25;
|
||||
private const int MaxSituationalModifier = 1000;
|
||||
}
|
||||
@@ -67,8 +67,13 @@ public sealed class WorkspaceState
|
||||
|
||||
public bool ShowCreateCharacterModal { get; set; }
|
||||
public bool ShowEditCharacterModal { get; set; }
|
||||
public bool ShowRolemasterSkillRollModal { get; set; }
|
||||
public bool CanEditCharacterOwner { get; set; }
|
||||
public Guid? EditingCharacterId { get; set; }
|
||||
public CharacterSheetSkill? PendingRolemasterSkillRoll { get; set; }
|
||||
public string PendingRolemasterSituationalModifier { get; set; } = string.Empty;
|
||||
public string? PendingRolemasterSkillRollError { get; set; }
|
||||
public bool IsSubmittingRolemasterSkillRoll { get; set; }
|
||||
public CharacterFormModel CreateCharacterInitialModel { get; set; } = new();
|
||||
public CharacterFormModel EditCharacterInitialModel { get; set; } = new();
|
||||
public int CreateCharacterFormVersion { get; set; }
|
||||
|
||||
@@ -1064,6 +1064,10 @@ select:focus-visible {
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.rolemaster-roll-modal {
|
||||
width: min(28rem, 100%);
|
||||
}
|
||||
|
||||
.mobile-bottom-nav {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
|
||||
Reference in New Issue
Block a user