Add OB and DB to attack lookup
This commit is contained in:
@@ -32,7 +32,7 @@ else
|
||||
|
||||
<div class="field-shell">
|
||||
<label for="armor-type">Armor type</label>
|
||||
<select id="armor-type" class="input-shell" @bind="attackInput.ArmorType">
|
||||
<select id="armor-type" class="input-shell" @bind="attackInput.ArmorType" @bind:after="HandleAttackInputsChanged">
|
||||
@foreach (var armorType in referenceData.ArmorTypes)
|
||||
{
|
||||
<option value="@armorType.Key">@armorType.Label</option>
|
||||
@@ -43,7 +43,7 @@ else
|
||||
<div class="field-shell">
|
||||
<label for="attack-roll">Attack roll</label>
|
||||
<div class="roll-input-row">
|
||||
<input id="attack-roll" class="input-shell" type="number" min="1" @bind="attackInput.AttackRoll" />
|
||||
<input id="attack-roll" class="input-shell" type="number" min="1" @bind="attackInput.AttackRoll" @bind:after="HandleAttackInputsChanged" />
|
||||
<button type="button" class="roll-button" @onclick="RollAttack">Roll</button>
|
||||
</div>
|
||||
@if (SelectedAttackTable is { FumbleMinRoll: int attackFumbleMin, FumbleMaxRoll: int attackFumbleMax })
|
||||
@@ -60,6 +60,16 @@ else
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="field-shell">
|
||||
<label for="attack-ob">OB</label>
|
||||
<input id="attack-ob" class="input-shell" type="number" @bind="attackInput.OffensiveBonus" @bind:after="HandleAttackInputsChanged" />
|
||||
</div>
|
||||
|
||||
<div class="field-shell">
|
||||
<label for="attack-db">DB</label>
|
||||
<input id="attack-db" class="input-shell" type="number" @bind="attackInput.DefensiveBonus" @bind:after="HandleAttackInputsChanged" />
|
||||
</div>
|
||||
|
||||
<div class="field-shell">
|
||||
<label for="critical-roll">Critical roll</label>
|
||||
<div class="roll-input-row">
|
||||
@@ -90,7 +100,23 @@ else
|
||||
<div class="result-card">
|
||||
<h3>@attackResult.AttackTableName vs @attackResult.ArmorTypeLabel</h3>
|
||||
<div class="result-stats">
|
||||
<span class="stat-pill">Attack roll: @attackResult.Roll</span>
|
||||
@if (lastAttackResolution is not null)
|
||||
{
|
||||
<span class="stat-pill">Rolled: @lastAttackResolution.RawRoll</span>
|
||||
@if (lastAttackResolution.OffensiveBonus != 0)
|
||||
{
|
||||
<span class="stat-pill">OB: +@lastAttackResolution.OffensiveBonus</span>
|
||||
}
|
||||
@if (lastAttackResolution.DefensiveBonus != 0)
|
||||
{
|
||||
<span class="stat-pill">DB: -@lastAttackResolution.DefensiveBonus</span>
|
||||
}
|
||||
<span class="stat-pill">Total used: @lastAttackResolution.EffectiveTotal</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="stat-pill">Attack total: @attackResult.Roll</span>
|
||||
}
|
||||
<span class="stat-pill">Hits: @attackResult.Hits</span>
|
||||
@if (!string.IsNullOrWhiteSpace(attackResult.CriticalSeverity))
|
||||
{
|
||||
@@ -206,6 +232,7 @@ else
|
||||
private string? attackCriticalRollSummary;
|
||||
private string? directCriticalRollSummary;
|
||||
private string? attackFumbleMessage;
|
||||
private AttackResolutionSummary? lastAttackResolution;
|
||||
|
||||
private AttackTableReference? SelectedAttackTable =>
|
||||
referenceData?.AttackTables.FirstOrDefault(table => table.Key == attackInput.AttackTable);
|
||||
@@ -230,7 +257,8 @@ else
|
||||
{
|
||||
attackError = null;
|
||||
attackResult = null;
|
||||
attackFumbleMessage = BuildAttackFumbleMessage(attackInput.AttackRoll);
|
||||
var attackResolution = BuildAttackResolution();
|
||||
attackFumbleMessage = BuildAttackFumbleMessage(attackResolution);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(attackFumbleMessage))
|
||||
{
|
||||
@@ -246,7 +274,7 @@ else
|
||||
var response = await LookupService.LookupAttackAsync(new AttackLookupRequest(
|
||||
attackInput.AttackTable,
|
||||
attackInput.ArmorType,
|
||||
attackInput.AttackRoll,
|
||||
attackResolution.EffectiveTotal,
|
||||
string.IsNullOrWhiteSpace(attackInput.CriticalRollText) ? null : criticalRoll));
|
||||
|
||||
if (response is null)
|
||||
@@ -256,6 +284,7 @@ else
|
||||
}
|
||||
|
||||
attackResult = response;
|
||||
lastAttackResolution = attackResolution;
|
||||
}
|
||||
|
||||
private async Task RunCriticalLookupAsync()
|
||||
@@ -293,28 +322,23 @@ else
|
||||
private void HandleAttackTableChanged(ChangeEventArgs args)
|
||||
{
|
||||
attackInput.AttackTable = args.Value?.ToString() ?? string.Empty;
|
||||
attackResult = null;
|
||||
attackError = null;
|
||||
attackCriticalRollSummary = null;
|
||||
RefreshAttackRollState();
|
||||
HandleAttackInputsChanged();
|
||||
}
|
||||
|
||||
private void RollAttack()
|
||||
{
|
||||
var result = RolemasterRoller.RollAttack(Random.Shared);
|
||||
attackInput.AttackRoll = result.Total;
|
||||
attackRollSummary = BuildRollSummary(result, "Attack");
|
||||
attackCriticalRollSummary = null;
|
||||
attackResult = null;
|
||||
attackError = null;
|
||||
RefreshAttackRollState();
|
||||
attackRollSummary = BuildAttackRollSummary(result, BuildAttackResolution());
|
||||
HandleAttackInputsChanged(clearRollSummary: false);
|
||||
}
|
||||
|
||||
private async Task RollAttackCriticalAsync()
|
||||
{
|
||||
attackError = null;
|
||||
attackCriticalRollSummary = null;
|
||||
attackFumbleMessage = BuildAttackFumbleMessage(attackInput.AttackRoll);
|
||||
var attackResolution = BuildAttackResolution();
|
||||
attackFumbleMessage = BuildAttackFumbleMessage(attackResolution);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(attackFumbleMessage))
|
||||
{
|
||||
@@ -326,7 +350,7 @@ else
|
||||
var pendingAttack = await LookupService.LookupAttackAsync(new AttackLookupRequest(
|
||||
attackInput.AttackTable,
|
||||
attackInput.ArmorType,
|
||||
attackInput.AttackRoll,
|
||||
attackResolution.EffectiveTotal,
|
||||
null));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(pendingAttack?.CriticalType))
|
||||
@@ -350,21 +374,40 @@ else
|
||||
criticalError = null;
|
||||
}
|
||||
|
||||
private void RefreshAttackRollState()
|
||||
private void HandleAttackInputsChanged()
|
||||
{
|
||||
attackFumbleMessage = BuildAttackFumbleMessage(attackInput.AttackRoll);
|
||||
HandleAttackInputsChanged(clearRollSummary: true);
|
||||
}
|
||||
|
||||
private string? BuildAttackFumbleMessage(int attackRoll)
|
||||
private void HandleAttackInputsChanged(bool clearRollSummary)
|
||||
{
|
||||
if (SelectedAttackTable is not { FumbleMinRoll: int fumbleMinRoll, FumbleMaxRoll: int fumbleMaxRoll } ||
|
||||
attackRoll < fumbleMinRoll ||
|
||||
attackRoll > fumbleMaxRoll)
|
||||
if (clearRollSummary)
|
||||
{
|
||||
attackRollSummary = null;
|
||||
}
|
||||
|
||||
attackResult = null;
|
||||
attackError = null;
|
||||
attackCriticalRollSummary = null;
|
||||
lastAttackResolution = null;
|
||||
attackFumbleMessage = BuildAttackFumbleMessage(BuildAttackResolution());
|
||||
}
|
||||
|
||||
private AttackResolutionSummary BuildAttackResolution() =>
|
||||
AttackResolutionCalculator.Resolve(
|
||||
attackInput.AttackRoll,
|
||||
attackInput.OffensiveBonus,
|
||||
attackInput.DefensiveBonus);
|
||||
|
||||
private string? BuildAttackFumbleMessage(AttackResolutionSummary resolution)
|
||||
{
|
||||
if (!AttackResolutionCalculator.IsFumble(resolution, SelectedAttackTable) ||
|
||||
SelectedAttackTable is not { FumbleMinRoll: int fumbleMinRoll, FumbleMaxRoll: int fumbleMaxRoll })
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return $"{SelectedAttackTable.Label} fumble on {FormatRange(fumbleMinRoll, fumbleMaxRoll)}. Do not run a normal attack lookup.";
|
||||
return $"{SelectedAttackTable.Label} fumble on {FormatRange(fumbleMinRoll, fumbleMaxRoll)}. Raw roll {resolution.RawRoll:00} triggers the fumble before OB or DB is applied.";
|
||||
}
|
||||
|
||||
private static string BuildRollSummary(LookupRollResult result, string label)
|
||||
@@ -377,6 +420,28 @@ else
|
||||
return $"{label} roll: {string.Join(" + ", result.Rolls)} = {result.Total}.";
|
||||
}
|
||||
|
||||
private static string BuildAttackRollSummary(LookupRollResult result, AttackResolutionSummary resolution)
|
||||
{
|
||||
var summary = BuildRollSummary(result, "Attack");
|
||||
if (resolution.OffensiveBonus == 0 && resolution.DefensiveBonus == 0)
|
||||
{
|
||||
return summary;
|
||||
}
|
||||
|
||||
var modifiers = new List<string>();
|
||||
if (resolution.OffensiveBonus != 0)
|
||||
{
|
||||
modifiers.Add($"+{resolution.OffensiveBonus} OB");
|
||||
}
|
||||
|
||||
if (resolution.DefensiveBonus != 0)
|
||||
{
|
||||
modifiers.Add($"-{resolution.DefensiveBonus} DB");
|
||||
}
|
||||
|
||||
return $"{summary} {string.Join(' ', modifiers)} = {resolution.EffectiveTotal}.";
|
||||
}
|
||||
|
||||
private static string BuildCriticalRollSummary(
|
||||
LookupRollResult result,
|
||||
CriticalTableReference? criticalTable)
|
||||
@@ -400,6 +465,8 @@ else
|
||||
public string AttackTable { get; set; } = string.Empty;
|
||||
public string ArmorType { get; set; } = string.Empty;
|
||||
public int AttackRoll { get; set; } = 66;
|
||||
public int OffensiveBonus { get; set; }
|
||||
public int DefensiveBonus { get; set; }
|
||||
public string? CriticalRollText { get; set; } = "72";
|
||||
}
|
||||
|
||||
|
||||
12
src/RolemasterDb.App/Features/AttackResolutionCalculator.cs
Normal file
12
src/RolemasterDb.App/Features/AttackResolutionCalculator.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace RolemasterDb.App.Features;
|
||||
|
||||
public static class AttackResolutionCalculator
|
||||
{
|
||||
public static AttackResolutionSummary Resolve(int rawRoll, int offensiveBonus, int defensiveBonus) =>
|
||||
new(rawRoll, offensiveBonus, defensiveBonus, rawRoll + offensiveBonus - defensiveBonus);
|
||||
|
||||
public static bool IsFumble(AttackResolutionSummary resolution, AttackTableReference? table) =>
|
||||
table is { FumbleMinRoll: int fumbleMinRoll, FumbleMaxRoll: int fumbleMaxRoll } &&
|
||||
resolution.RawRoll >= fumbleMinRoll &&
|
||||
resolution.RawRoll <= fumbleMaxRoll;
|
||||
}
|
||||
7
src/RolemasterDb.App/Features/AttackResolutionSummary.cs
Normal file
7
src/RolemasterDb.App/Features/AttackResolutionSummary.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace RolemasterDb.App.Features;
|
||||
|
||||
public sealed record AttackResolutionSummary(
|
||||
int RawRoll,
|
||||
int OffensiveBonus,
|
||||
int DefensiveBonus,
|
||||
int EffectiveTotal);
|
||||
Reference in New Issue
Block a user