Align Play with table deep links
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
@page "/"
|
||||
@rendermode InteractiveServer
|
||||
@using System
|
||||
@using RolemasterDb.App.Frontend.AppState
|
||||
@inject LookupService LookupService
|
||||
@inject TableContextState TableContextState
|
||||
|
||||
<PageTitle>Lookup Desk</PageTitle>
|
||||
<PageTitle>Play</PageTitle>
|
||||
|
||||
@if (referenceData is null)
|
||||
{
|
||||
@@ -13,210 +15,256 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="dashboard-grid">
|
||||
<section class="panel">
|
||||
<h2 class="panel-title">Attack Lookup</h2>
|
||||
<p class="panel-copy">Choose an attack, armor type, and attack roll. If it produces a critical and you enter the follow-up roll, the app resolves that too.</p>
|
||||
|
||||
<div class="lookup-form">
|
||||
<div class="form-grid">
|
||||
<div class="field-shell">
|
||||
<label for="attack-table">Attack table</label>
|
||||
<select id="attack-table" class="input-shell" value="@attackInput.AttackTable" @onchange="HandleAttackTableChanged">
|
||||
@foreach (var attackTable in referenceData.AttackTables)
|
||||
{
|
||||
<option value="@attackTable.Key">@attackTable.Label</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field-shell">
|
||||
<label for="armor-type">Armor type</label>
|
||||
<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>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<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" @bind:after="HandleAttackInputsChanged" />
|
||||
<button type="button" class="roll-button" @onclick="RollAttack">Roll</button>
|
||||
</div>
|
||||
@if (SelectedAttackTable is { FumbleMinRoll: int attackFumbleMin, FumbleMaxRoll: int attackFumbleMax })
|
||||
{
|
||||
<p class="muted lookup-roll-note">Fumble range: @FormatRange(attackFumbleMin, attackFumbleMax)</p>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(attackRollSummary))
|
||||
{
|
||||
<p class="muted lookup-roll-note">@attackRollSummary</p>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(attackFumbleMessage))
|
||||
{
|
||||
<p class="lookup-roll-note is-warning">@attackFumbleMessage</p>
|
||||
}
|
||||
</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">
|
||||
<input id="critical-roll" class="input-shell" type="number" min="1" @bind="attackInput.CriticalRollText" />
|
||||
<button type="button" class="roll-button" @onclick="RollAttackCriticalAsync">Roll</button>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(attackCriticalRollSummary))
|
||||
{
|
||||
<p class="muted lookup-roll-note">@attackCriticalRollSummary</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-row">
|
||||
<button class="btn-ritual" @onclick="RunAttackLookupAsync">Resolve attack</button>
|
||||
<span class="muted">Leave critical roll blank if you only need the hit result.</span>
|
||||
</div>
|
||||
<div class="play-page">
|
||||
<header class="panel play-hero">
|
||||
<div class="play-hero-copy">
|
||||
<p class="play-eyebrow">Play</p>
|
||||
<h1 class="play-title">Resolve attacks fast. Open the table only when you need context.</h1>
|
||||
<p class="play-summary">Use the attack lane for live results and the direct critical lane when you already know the exact table and severity.</p>
|
||||
</div>
|
||||
<div class="play-hero-meta" aria-label="Play page workflow">
|
||||
<span class="tag">Live lookup</span>
|
||||
<span class="tag">Fast resolution</span>
|
||||
<span class="tag">Table deep links</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(attackError))
|
||||
{
|
||||
<p class="error-text">@attackError</p>
|
||||
}
|
||||
|
||||
@if (attackResult is not null)
|
||||
{
|
||||
<div class="result-shell">
|
||||
<div class="result-card">
|
||||
<h3>@attackResult.AttackTableName vs @attackResult.ArmorTypeLabel</h3>
|
||||
<div class="result-stats">
|
||||
@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))
|
||||
{
|
||||
<span class="stat-pill">@attackResult.CriticalSeverity @attackResult.CriticalType critical</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="stat-pill">No critical</span>
|
||||
}
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(attackResult.Notes))
|
||||
{
|
||||
<p class="muted">@attackResult.Notes</p>
|
||||
}
|
||||
|
||||
@if (attackResult.AutoCritical is not null)
|
||||
{
|
||||
<div class="callout">
|
||||
<h4>Resolved critical</h4>
|
||||
<CriticalLookupResultCard Result="attackResult.AutoCritical" />
|
||||
</div>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(attackResult.CriticalSeverity))
|
||||
{
|
||||
<div class="callout">The attack produced a critical. Add a critical roll to resolve it automatically.</div>
|
||||
}
|
||||
<div class="play-layout">
|
||||
<section class="panel play-panel play-panel-primary">
|
||||
<div class="play-panel-header">
|
||||
<div class="play-panel-copy">
|
||||
<p class="play-eyebrow">Primary lane</p>
|
||||
<h2 class="panel-title">Attack lookup</h2>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2 class="panel-title">Direct Critical Lookup</h2>
|
||||
<p class="panel-copy">Use this when you already know the critical table, severity, roll, and variant if the table uses one.</p>
|
||||
|
||||
<div class="lookup-form">
|
||||
<div class="form-grid">
|
||||
<div class="field-shell">
|
||||
<label for="critical-table">Critical table</label>
|
||||
<select id="critical-table" class="input-shell" value="@criticalInput.CriticalType" @onchange="HandleCriticalTableChanged">
|
||||
@foreach (var criticalTable in referenceData.CriticalTables)
|
||||
{
|
||||
<option value="@criticalTable.Key">@criticalTable.Label</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field-shell">
|
||||
<label for="critical-column">Severity</label>
|
||||
<select id="critical-column" class="input-shell" @bind="criticalInput.Column">
|
||||
@foreach (var column in SelectedCriticalTable?.Columns ?? [])
|
||||
{
|
||||
<option value="@column.Key">@column.Label</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if (SelectedCriticalTable?.Groups.Count > 0)
|
||||
{
|
||||
<div class="lookup-form">
|
||||
<div class="form-grid play-form-grid play-form-grid-primary">
|
||||
<div class="field-shell">
|
||||
<label for="critical-group">Variant</label>
|
||||
<select id="critical-group" class="input-shell" @bind="criticalInput.Group">
|
||||
@foreach (var group in SelectedCriticalTable.Groups)
|
||||
<label for="attack-table">Attack table</label>
|
||||
<select id="attack-table" class="input-shell" value="@attackInput.AttackTable" @onchange="HandleAttackTableChanged">
|
||||
@foreach (var attackTable in referenceData.AttackTables)
|
||||
{
|
||||
<option value="@group.Key">@group.Label</option>
|
||||
<option value="@attackTable.Key">@attackTable.Label</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="field-shell">
|
||||
<label for="critical-roll-direct">Critical roll</label>
|
||||
<div class="roll-input-row">
|
||||
<input id="critical-roll-direct" class="input-shell" type="number" min="1" @bind="criticalInput.Roll" />
|
||||
<button type="button" class="roll-button" @onclick="RollDirectCritical">Roll</button>
|
||||
<div class="field-shell">
|
||||
<label for="armor-type">Armor type</label>
|
||||
<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>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(directCriticalRollSummary))
|
||||
{
|
||||
<p class="muted lookup-roll-note">@directCriticalRollSummary</p>
|
||||
}
|
||||
|
||||
<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" @bind:after="HandleAttackInputsChanged"/>
|
||||
<button type="button" class="roll-button" @onclick="RollAttack">Roll</button>
|
||||
</div>
|
||||
@if (SelectedAttackTable is { FumbleMinRoll: int attackFumbleMin, FumbleMaxRoll: int attackFumbleMax })
|
||||
{
|
||||
<p class="muted lookup-roll-note">Fumble range: @FormatRange(attackFumbleMin, attackFumbleMax)</p>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(attackRollSummary))
|
||||
{
|
||||
<p class="muted lookup-roll-note">@attackRollSummary</p>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(attackFumbleMessage))
|
||||
{
|
||||
<p class="lookup-roll-note is-warning">@attackFumbleMessage</p>
|
||||
}
|
||||
</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">
|
||||
<input id="critical-roll" class="input-shell" type="number" min="1" @bind="attackInput.CriticalRollText"/>
|
||||
<button type="button" class="roll-button" @onclick="RollAttackCriticalAsync">Roll</button>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(attackCriticalRollSummary))
|
||||
{
|
||||
<p class="muted lookup-roll-note">@attackCriticalRollSummary</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-row play-action-row">
|
||||
<button class="btn-ritual" @onclick="RunAttackLookupAsync">Resolve attack</button>
|
||||
<span class="muted play-action-hint">Leave critical roll blank if you only need the hit result.</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-row">
|
||||
<button class="btn-ritual" @onclick="RunCriticalLookupAsync">Resolve critical</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(attackError))
|
||||
{
|
||||
<p class="error-text">@attackError</p>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(criticalError))
|
||||
{
|
||||
<p class="error-text">@criticalError</p>
|
||||
}
|
||||
@if (attackResult is not null)
|
||||
{
|
||||
<div class="result-shell play-result-dock">
|
||||
<div class="result-card play-result-card">
|
||||
<div class="play-result-heading">
|
||||
<div>
|
||||
<p class="play-result-eyebrow">Attack result</p>
|
||||
<h3>@attackResult.AttackTableName vs @attackResult.ArmorTypeLabel</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (criticalResult is not null)
|
||||
{
|
||||
<div class="result-shell">
|
||||
<CriticalLookupResultCard Result="criticalResult" />
|
||||
<div class="result-stats">
|
||||
@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))
|
||||
{
|
||||
<span class="stat-pill">@attackResult.CriticalSeverity @attackResult.CriticalType critical</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="stat-pill">No critical</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(attackResult.Notes))
|
||||
{
|
||||
<p class="muted play-result-note">@attackResult.Notes</p>
|
||||
}
|
||||
|
||||
@if (attackResult.AutoCritical is not null)
|
||||
{
|
||||
<div class="callout play-callout">
|
||||
<div class="play-result-heading">
|
||||
<div>
|
||||
<p class="play-result-eyebrow">Follow-up critical</p>
|
||||
<h4>Resolved critical</h4>
|
||||
</div>
|
||||
<PlayResultActions Href="@BuildTablesHref(attackResult.AutoCritical)"/>
|
||||
</div>
|
||||
<CriticalLookupResultCard Result="attackResult.AutoCritical"/>
|
||||
</div>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(attackResult.CriticalSeverity))
|
||||
{
|
||||
<div class="callout play-callout">
|
||||
The attack produced a critical. Add a critical roll to resolve it automatically.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="panel play-panel play-panel-secondary">
|
||||
<div class="play-panel-header">
|
||||
<div class="play-panel-copy">
|
||||
<p class="play-eyebrow">Quick jump</p>
|
||||
<h2 class="panel-title">Direct critical lookup</h2>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
<div class="lookup-form">
|
||||
<div class="form-grid play-form-grid">
|
||||
<div class="field-shell">
|
||||
<label for="critical-table">Critical table</label>
|
||||
<select id="critical-table" class="input-shell" value="@criticalInput.CriticalType" @onchange="HandleCriticalTableChanged">
|
||||
@foreach (var criticalTable in referenceData.CriticalTables)
|
||||
{
|
||||
<option value="@criticalTable.Key">@criticalTable.Label</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field-shell">
|
||||
<label for="critical-column">Severity</label>
|
||||
<select id="critical-column" class="input-shell" @bind="criticalInput.Column">
|
||||
@foreach (var column in SelectedCriticalTable?.Columns ?? [])
|
||||
{
|
||||
<option value="@column.Key">@column.Label</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if (SelectedCriticalTable?.Groups.Count > 0)
|
||||
{
|
||||
<div class="field-shell">
|
||||
<label for="critical-group">Variant</label>
|
||||
<select id="critical-group" class="input-shell" @bind="criticalInput.Group">
|
||||
@foreach (var group in SelectedCriticalTable.Groups)
|
||||
{
|
||||
<option value="@group.Key">@group.Label</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="field-shell">
|
||||
<label for="critical-roll-direct">Critical roll</label>
|
||||
<div class="roll-input-row">
|
||||
<input id="critical-roll-direct" class="input-shell" type="number" min="1" @bind="criticalInput.Roll"/>
|
||||
<button type="button" class="roll-button" @onclick="RollDirectCritical">Roll</button>
|
||||
</div>
|
||||
@if (!string.IsNullOrWhiteSpace(directCriticalRollSummary))
|
||||
{
|
||||
<p class="muted lookup-roll-note">@directCriticalRollSummary</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-row play-action-row">
|
||||
<button class="btn-ritual" @onclick="RunCriticalLookupAsync">Resolve critical</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(criticalError))
|
||||
{
|
||||
<p class="error-text">@criticalError</p>
|
||||
}
|
||||
|
||||
@if (criticalResult is not null)
|
||||
{
|
||||
<div class="result-shell play-result-dock">
|
||||
<div class="play-result-heading">
|
||||
<div>
|
||||
<p class="play-result-eyebrow">Critical result</p>
|
||||
</div>
|
||||
<PlayResultActions Href="@BuildTablesHref(criticalResult)"/>
|
||||
</div>
|
||||
<CriticalLookupResultCard Result="criticalResult"/>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -271,11 +319,7 @@ else
|
||||
return;
|
||||
}
|
||||
|
||||
var response = await LookupService.LookupAttackAsync(new AttackLookupRequest(
|
||||
attackInput.AttackTable,
|
||||
attackInput.ArmorType,
|
||||
attackResolution.EffectiveTotal,
|
||||
string.IsNullOrWhiteSpace(attackInput.CriticalRollText) ? null : criticalRoll));
|
||||
var response = await LookupService.LookupAttackAsync(new AttackLookupRequest(attackInput.AttackTable, attackInput.ArmorType, attackResolution.EffectiveTotal, string.IsNullOrWhiteSpace(attackInput.CriticalRollText) ? null : criticalRoll));
|
||||
|
||||
if (response is null)
|
||||
{
|
||||
@@ -292,11 +336,7 @@ else
|
||||
criticalError = null;
|
||||
criticalResult = null;
|
||||
|
||||
var response = await LookupService.LookupCriticalAsync(new CriticalLookupRequest(
|
||||
criticalInput.CriticalType,
|
||||
criticalInput.Column,
|
||||
criticalInput.Roll,
|
||||
SelectedCriticalTable?.Groups.Count > 0 ? criticalInput.Group : null));
|
||||
var response = await LookupService.LookupCriticalAsync(new CriticalLookupRequest(criticalInput.CriticalType, criticalInput.Column, criticalInput.Roll, SelectedCriticalTable?.Groups.Count > 0 ? criticalInput.Group : null));
|
||||
|
||||
if (response is null)
|
||||
{
|
||||
@@ -347,16 +387,11 @@ else
|
||||
}
|
||||
|
||||
CriticalTableReference? criticalTable = null;
|
||||
var pendingAttack = await LookupService.LookupAttackAsync(new AttackLookupRequest(
|
||||
attackInput.AttackTable,
|
||||
attackInput.ArmorType,
|
||||
attackResolution.EffectiveTotal,
|
||||
null));
|
||||
var pendingAttack = await LookupService.LookupAttackAsync(new AttackLookupRequest(attackInput.AttackTable, attackInput.ArmorType, attackResolution.EffectiveTotal, null));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(pendingAttack?.CriticalType))
|
||||
{
|
||||
criticalTable = referenceData?.CriticalTables.FirstOrDefault(item =>
|
||||
string.Equals(item.Key, pendingAttack.CriticalType, StringComparison.Ordinal));
|
||||
criticalTable = referenceData?.CriticalTables.FirstOrDefault(item => string.Equals(item.Key, pendingAttack.CriticalType, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
var result = RolemasterRoller.RollCritical(Random.Shared, criticalTable);
|
||||
@@ -394,15 +429,11 @@ else
|
||||
}
|
||||
|
||||
private AttackResolutionSummary BuildAttackResolution() =>
|
||||
AttackResolutionCalculator.Resolve(
|
||||
attackInput.AttackRoll,
|
||||
attackInput.OffensiveBonus,
|
||||
attackInput.DefensiveBonus);
|
||||
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 })
|
||||
if (!AttackResolutionCalculator.IsFumble(resolution, SelectedAttackTable) || SelectedAttackTable is not { FumbleMinRoll: int fumbleMinRoll, FumbleMaxRoll: int fumbleMaxRoll })
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -442,9 +473,7 @@ else
|
||||
return $"{summary} {string.Join(' ', modifiers)} = {resolution.EffectiveTotal}.";
|
||||
}
|
||||
|
||||
private static string BuildCriticalRollSummary(
|
||||
LookupRollResult result,
|
||||
CriticalTableReference? criticalTable)
|
||||
private static string BuildCriticalRollSummary(LookupRollResult result, CriticalTableReference? criticalTable)
|
||||
{
|
||||
var summary = BuildRollSummary(result, "Critical");
|
||||
if (criticalTable is null)
|
||||
@@ -452,14 +481,30 @@ else
|
||||
return $"{summary} Standard 1-100 roll used because no critical table is currently resolved from the attack result.";
|
||||
}
|
||||
|
||||
return RolemasterRoller.AllowsOpenEndedCritical(criticalTable)
|
||||
? $"{summary} {criticalTable.Label} uses open-ended rolls."
|
||||
: $"{summary} {criticalTable.Label} is capped at 1-100.";
|
||||
return RolemasterRoller.AllowsOpenEndedCritical(criticalTable) ? $"{summary} {criticalTable.Label} uses open-ended rolls." : $"{summary} {criticalTable.Label} is capped at 1-100.";
|
||||
}
|
||||
|
||||
private static string FormatRange(int minRoll, int maxRoll) =>
|
||||
$"{minRoll:00}-{maxRoll:00}";
|
||||
|
||||
private string? BuildTablesHref(CriticalLookupResponse? result)
|
||||
{
|
||||
if (referenceData is null || result is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var table = referenceData.CriticalTables.FirstOrDefault(item => string.Equals(item.Key, result.CriticalType, StringComparison.Ordinal));
|
||||
if (table is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var snapshot = new TableContextSnapshot(TableSlug: table.Key, GroupKey: result.Group, ColumnKey: result.Column, RollBand: result.RollBand, Mode: TableContextMode.Reference);
|
||||
|
||||
return TableContextState.BuildUri("/tables", snapshot);
|
||||
}
|
||||
|
||||
private sealed class AttackLookupForm
|
||||
{
|
||||
public string AttackTable { get; set; } = string.Empty;
|
||||
@@ -477,4 +522,5 @@ else
|
||||
public string Group { get; set; } = string.Empty;
|
||||
public int Roll { get; set; } = 72;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -149,7 +149,7 @@
|
||||
await PersistAndSyncTableContextAsync();
|
||||
}
|
||||
|
||||
private async Task LoadTableDetailAsync()
|
||||
private async Task LoadTableDetailAsync(TableContextSnapshot? routeContext = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(selectedTableSlug))
|
||||
{
|
||||
@@ -172,6 +172,7 @@
|
||||
}
|
||||
|
||||
await RecordRecentTableVisitAsync();
|
||||
ApplyRouteContext(routeContext);
|
||||
NormalizeViewStateForCurrentDetail();
|
||||
}
|
||||
catch (Exception exception)
|
||||
@@ -203,12 +204,14 @@
|
||||
if (string.IsNullOrWhiteSpace(selectedTableSlug) || !string.Equals(resolvedTableSlug, selectedTableSlug, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
selectedTableSlug = resolvedTableSlug;
|
||||
await LoadTableDetailAsync();
|
||||
await LoadTableDetailAsync(initialContext);
|
||||
await PersistAndSyncTableContextAsync();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyRouteContext(initialContext);
|
||||
NormalizeViewStateForCurrentDetail();
|
||||
await PersistAndSyncTableContextAsync();
|
||||
}
|
||||
}
|
||||
@@ -373,7 +376,7 @@
|
||||
}
|
||||
|
||||
private RolemasterDb.App.Frontend.AppState.TableContextSnapshot BuildCurrentTableContext() =>
|
||||
new(TableSlug: selectedTableSlug, Mode: RolemasterDb.App.Frontend.AppState.TableContextMode.Reference);
|
||||
new(TableSlug: selectedTableSlug, GroupKey: selectedCell?.GroupKey, ColumnKey: selectedCell?.ColumnKey, RollBand: selectedCell?.RollBand, ResultId: selectedCell?.ResultId, Mode: RolemasterDb.App.Frontend.AppState.TableContextMode.Reference);
|
||||
|
||||
private void SelectCell(TablesCellSelection selection)
|
||||
{
|
||||
@@ -408,6 +411,18 @@
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void ApplyRouteContext(TableContextSnapshot? routeContext)
|
||||
{
|
||||
if (tableDetail is null)
|
||||
{
|
||||
selectedCell = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var resolvedCell = TableContextCellResolver.FindCell(tableDetail, routeContext);
|
||||
selectedCell = resolvedCell is null ? null : new TablesCellSelection(resolvedCell.ResultId, resolvedCell.RollBand, resolvedCell.ColumnKey, resolvedCell.GroupKey);
|
||||
}
|
||||
|
||||
private void NormalizeViewStateForCurrentDetail()
|
||||
{
|
||||
referenceMode = NormalizeMode(referenceMode);
|
||||
|
||||
13
src/RolemasterDb.App/Components/Play/PlayResultActions.razor
Normal file
13
src/RolemasterDb.App/Components/Play/PlayResultActions.razor
Normal file
@@ -0,0 +1,13 @@
|
||||
@if (!string.IsNullOrWhiteSpace(Href))
|
||||
{
|
||||
<div class="play-result-actions">
|
||||
<a class="play-action-link" href="@Href">Open in Tables</a>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public string? Href { get; set; }
|
||||
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
@using RolemasterDb.App.Components
|
||||
@using RolemasterDb.App.Components.Curation
|
||||
@using RolemasterDb.App.Components.Layout
|
||||
@using RolemasterDb.App.Components.Play
|
||||
@using RolemasterDb.App.Components.Primitives
|
||||
@using RolemasterDb.App.Components.Shell
|
||||
@using RolemasterDb.App.Components.Shared
|
||||
|
||||
Reference in New Issue
Block a user