Implement critical editor override state
This commit is contained in:
@@ -551,6 +551,17 @@ Acceptance criteria:
|
||||
|
||||
### Phase 3: Generated versus overridden state model
|
||||
|
||||
Status:
|
||||
|
||||
- implemented in the web app on March 15, 2026
|
||||
|
||||
Implemented model:
|
||||
|
||||
- result-level override flags separate generated description and collection state from manual edits
|
||||
- effect rows and condition rows carry explicit origin keys plus override markers
|
||||
- re-parse now merges generated parser output with the current override state instead of replacing the whole editor payload
|
||||
- saved editor state is persisted so later edit sessions keep the same generated-versus-overridden boundaries
|
||||
|
||||
Scope:
|
||||
|
||||
- explicitly track which values are parser-generated and which values were manually overridden
|
||||
|
||||
@@ -65,7 +65,11 @@
|
||||
"descriptionText": "Current curated prose",
|
||||
"rawAffixText": "+8H - 2S",
|
||||
"parseStatus": "verified",
|
||||
"parsedJson": "{}",
|
||||
"parsedJson": "{\"version\":1,\"isDescriptionOverridden\":false,\"isRawAffixTextOverridden\":false,\"areEffectsOverridden\":false,\"areBranchesOverridden\":false,\"effects\":[],\"branches\":[]}",
|
||||
"isDescriptionOverridden": false,
|
||||
"isRawAffixTextOverridden": false,
|
||||
"areEffectsOverridden": false,
|
||||
"areBranchesOverridden": false,
|
||||
"validationMessages": [],
|
||||
"effects": [],
|
||||
"branches": []
|
||||
@@ -77,9 +81,21 @@
|
||||
<h2 class="panel-title">Cell re-parse</h2>
|
||||
<p class="panel-copy"><code>POST /api/tables/critical/{slug}/cells/{resultId}/reparse</code></p>
|
||||
<pre class="code-block">{
|
||||
"rawCellText": "Strike to thigh. +8H\nWith greaves: blow glances aside."
|
||||
"currentState": {
|
||||
"rawCellText": "Strike to thigh. +8H\nWith greaves: blow glances aside.",
|
||||
"descriptionText": "Curated prose",
|
||||
"rawAffixText": "+8H",
|
||||
"parseStatus": "partial",
|
||||
"parsedJson": "{}",
|
||||
"isDescriptionOverridden": true,
|
||||
"isRawAffixTextOverridden": false,
|
||||
"areEffectsOverridden": false,
|
||||
"areBranchesOverridden": false,
|
||||
"effects": [],
|
||||
"branches": []
|
||||
}
|
||||
}</pre>
|
||||
<p class="panel-copy">Re-runs the shared single-cell parser and returns a refreshed editor payload without saving changes.</p>
|
||||
<p class="panel-copy">Re-runs the shared single-cell parser, merges the generated result with the current override state, and returns the refreshed editor payload without saving changes.</p>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
@@ -91,6 +107,10 @@
|
||||
"rawAffixText": "+10H - must parry 2 rnds",
|
||||
"parseStatus": "manually_curated",
|
||||
"parsedJson": "{\"reviewed\":true}",
|
||||
"isDescriptionOverridden": true,
|
||||
"isRawAffixTextOverridden": false,
|
||||
"areEffectsOverridden": false,
|
||||
"areBranchesOverridden": false,
|
||||
"effects": [
|
||||
{
|
||||
"effectCode": "direct_hits",
|
||||
@@ -104,7 +124,9 @@
|
||||
"bodyPart": null,
|
||||
"isPermanent": false,
|
||||
"sourceType": "symbol",
|
||||
"sourceText": "+10H"
|
||||
"sourceText": "+10H",
|
||||
"originKey": "base:direct_hits:1",
|
||||
"isOverridden": true
|
||||
}
|
||||
],
|
||||
"branches": []
|
||||
|
||||
@@ -391,7 +391,7 @@
|
||||
|
||||
try
|
||||
{
|
||||
var response = await LookupService.ReparseCriticalCellAsync(selectedTableSlug, editingResultId.Value, editorModel.RawCellText);
|
||||
var response = await LookupService.ReparseCriticalCellAsync(selectedTableSlug, editingResultId.Value, editorModel.ToRequest());
|
||||
if (response is null)
|
||||
{
|
||||
editorReparseError = "The selected cell could not be re-parsed.";
|
||||
|
||||
@@ -14,6 +14,9 @@ public sealed class CriticalBranchEditorModel
|
||||
public string? RawAffixText { get; set; }
|
||||
public string ParsedJson { get; set; } = "{}";
|
||||
public int SortOrder { get; set; }
|
||||
public string? OriginKey { get; set; }
|
||||
public bool IsOverridden { get; set; }
|
||||
public bool AreEffectsOverridden { get; set; }
|
||||
public List<CriticalEffectEditorModel> Effects { get; set; } = [];
|
||||
|
||||
public static CriticalBranchEditorModel FromItem(CriticalBranchEditorItem item) =>
|
||||
@@ -28,6 +31,9 @@ public sealed class CriticalBranchEditorModel
|
||||
RawAffixText = item.RawAffixText,
|
||||
ParsedJson = item.ParsedJson,
|
||||
SortOrder = item.SortOrder,
|
||||
OriginKey = item.OriginKey,
|
||||
IsOverridden = item.IsOverridden,
|
||||
AreEffectsOverridden = item.AreEffectsOverridden,
|
||||
Effects = item.Effects.Select(CriticalEffectEditorModel.FromItem).ToList()
|
||||
};
|
||||
|
||||
@@ -42,6 +48,9 @@ public sealed class CriticalBranchEditorModel
|
||||
RawAffixText,
|
||||
SerializeParsedEffects(Effects),
|
||||
SortOrder,
|
||||
OriginKey,
|
||||
IsOverridden,
|
||||
AreEffectsOverridden,
|
||||
Effects.Select(effect => effect.ToItem()).ToList());
|
||||
|
||||
private string BuildRawText()
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
}
|
||||
<div class="field-shell">
|
||||
<label>Result Text</label>
|
||||
<InputTextArea class="input-shell critical-editor-textarea compact" @bind-Value="Model.DescriptionText" />
|
||||
<InputTextArea class="input-shell critical-editor-textarea compact" @bind-Value="Model.DescriptionText" @bind-Value:after="MarkDescriptionOverridden" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -134,11 +134,11 @@
|
||||
<div class="critical-editor-branch-line">
|
||||
<div class="field-shell">
|
||||
<label>Condition</label>
|
||||
<InputText class="input-shell" @bind-Value="branch.ConditionText" />
|
||||
<InputText class="input-shell" @bind-Value="branch.ConditionText" @bind-Value:after="() => MarkBranchOverridden(branch)" />
|
||||
</div>
|
||||
<div class="field-shell critical-editor-branch-outcome">
|
||||
<label>Outcome Text</label>
|
||||
<InputText class="input-shell" @bind-Value="branch.DescriptionText" />
|
||||
<InputText class="input-shell" @bind-Value="branch.DescriptionText" @bind-Value:after="() => MarkBranchOverridden(branch)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -321,7 +321,13 @@
|
||||
|
||||
private void AddBaseEffect()
|
||||
{
|
||||
Model?.Effects.Add(CreateDefaultEffectModel());
|
||||
if (Model is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Model.AreEffectsOverridden = true;
|
||||
Model.Effects.Add(CreateDefaultEffectModel());
|
||||
}
|
||||
|
||||
private void RemoveBaseEffect(int index)
|
||||
@@ -331,6 +337,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
Model.AreEffectsOverridden = true;
|
||||
Model.Effects.RemoveAt(index);
|
||||
}
|
||||
|
||||
@@ -344,8 +351,10 @@
|
||||
Model.Branches.Add(new CriticalBranchEditorModel
|
||||
{
|
||||
ConditionText = $"Condition {Model.Branches.Count + 1}",
|
||||
SortOrder = Model.Branches.Count + 1
|
||||
SortOrder = Model.Branches.Count + 1,
|
||||
IsOverridden = true
|
||||
});
|
||||
Model.AreBranchesOverridden = true;
|
||||
}
|
||||
|
||||
private void RemoveBranch(int index)
|
||||
@@ -355,11 +364,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
Model.AreBranchesOverridden = true;
|
||||
Model.Branches.RemoveAt(index);
|
||||
}
|
||||
|
||||
private static void AddBranchEffect(CriticalBranchEditorModel branch)
|
||||
{
|
||||
branch.AreEffectsOverridden = true;
|
||||
branch.Effects.Add(CreateDefaultEffectModel());
|
||||
}
|
||||
|
||||
@@ -370,6 +381,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
branch.AreEffectsOverridden = true;
|
||||
branch.Effects.RemoveAt(index);
|
||||
}
|
||||
|
||||
@@ -377,7 +389,8 @@
|
||||
new()
|
||||
{
|
||||
EffectCode = CriticalEffectCodes.DirectHits,
|
||||
SourceType = "symbol"
|
||||
SourceType = "symbol",
|
||||
IsOverridden = true
|
||||
};
|
||||
|
||||
private static string GetBranchTitle(CriticalBranchEditorModel branch, int index) =>
|
||||
@@ -428,6 +441,25 @@
|
||||
effect.IsPermanent = false;
|
||||
effect.SourceText = null;
|
||||
effect.SourceType = AffixDisplayMap.TryGet(effect.EffectCode, out _) ? "symbol" : "manual";
|
||||
effect.IsOverridden = true;
|
||||
}
|
||||
|
||||
private void MarkDescriptionOverridden()
|
||||
{
|
||||
if (Model is not null)
|
||||
{
|
||||
Model.IsDescriptionOverridden = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static void MarkBranchOverridden(CriticalBranchEditorModel branch)
|
||||
{
|
||||
branch.IsOverridden = true;
|
||||
}
|
||||
|
||||
private static void MarkEffectOverridden(CriticalEffectEditorModel effect)
|
||||
{
|
||||
effect.IsOverridden = true;
|
||||
}
|
||||
|
||||
private static string GetAdvancedSummary(CriticalCellEditorModel model)
|
||||
@@ -531,7 +563,7 @@
|
||||
{
|
||||
<div class="field-shell critical-editor-effect-extra">
|
||||
<label>Body Part</label>
|
||||
<InputText class="input-shell" @bind-Value="effect.BodyPart" />
|
||||
<InputText class="input-shell" @bind-Value="effect.BodyPart" @bind-Value:after="() => MarkEffectOverridden(effect)" />
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -543,30 +575,30 @@
|
||||
{
|
||||
case CriticalEffectCodes.DirectHits:
|
||||
<label>Hits</label>
|
||||
<InputNumber TValue="int?" class="input-shell" @bind-Value="effect.ValueInteger" />
|
||||
<InputNumber TValue="int?" class="input-shell" @bind-Value="effect.ValueInteger" @bind-Value:after="() => MarkEffectOverridden(effect)" />
|
||||
break;
|
||||
case CriticalEffectCodes.StunnedRounds:
|
||||
case CriticalEffectCodes.MustParryRounds:
|
||||
case CriticalEffectCodes.NoParryRounds:
|
||||
<label>Rounds</label>
|
||||
<InputNumber TValue="int?" class="input-shell" @bind-Value="effect.DurationRounds" />
|
||||
<InputNumber TValue="int?" class="input-shell" @bind-Value="effect.DurationRounds" @bind-Value:after="() => MarkEffectOverridden(effect)" />
|
||||
break;
|
||||
case CriticalEffectCodes.BleedPerRound:
|
||||
<label>Bleed / Round</label>
|
||||
<InputNumber TValue="int?" class="input-shell" @bind-Value="effect.PerRound" />
|
||||
<InputNumber TValue="int?" class="input-shell" @bind-Value="effect.PerRound" @bind-Value:after="() => MarkEffectOverridden(effect)" />
|
||||
break;
|
||||
case CriticalEffectCodes.FoePenalty:
|
||||
case CriticalEffectCodes.AttackerBonusNextRound:
|
||||
<label>Modifier</label>
|
||||
<InputNumber TValue="int?" class="input-shell" @bind-Value="effect.Modifier" />
|
||||
<InputNumber TValue="int?" class="input-shell" @bind-Value="effect.Modifier" @bind-Value:after="() => MarkEffectOverridden(effect)" />
|
||||
break;
|
||||
case CriticalEffectCodes.PowerPointModifier:
|
||||
<label>Expression</label>
|
||||
<InputText class="input-shell" @bind-Value="effect.ValueExpression" />
|
||||
<InputText class="input-shell" @bind-Value="effect.ValueExpression" @bind-Value:after="() => MarkEffectOverridden(effect)" />
|
||||
break;
|
||||
default:
|
||||
<label>Display Text</label>
|
||||
<InputText class="input-shell" @bind-Value="effect.SourceText" />
|
||||
<InputText class="input-shell" @bind-Value="effect.SourceText" @bind-Value:after="() => MarkEffectOverridden(effect)" />
|
||||
break;
|
||||
}
|
||||
</div>;
|
||||
|
||||
@@ -20,6 +20,10 @@ public sealed class CriticalCellEditorModel
|
||||
public string? RawAffixText { get; set; }
|
||||
public string ParseStatus { get; set; } = string.Empty;
|
||||
public string ParsedJson { get; set; } = "{}";
|
||||
public bool IsDescriptionOverridden { get; set; }
|
||||
public bool IsRawAffixTextOverridden { get; set; }
|
||||
public bool AreEffectsOverridden { get; set; }
|
||||
public bool AreBranchesOverridden { get; set; }
|
||||
public List<string> ValidationMessages { get; set; } = [];
|
||||
public List<CriticalEffectEditorModel> Effects { get; set; } = [];
|
||||
public List<CriticalBranchEditorModel> Branches { get; set; } = [];
|
||||
@@ -42,18 +46,27 @@ public sealed class CriticalCellEditorModel
|
||||
RawAffixText = response.RawAffixText,
|
||||
ParseStatus = response.ParseStatus,
|
||||
ParsedJson = response.ParsedJson,
|
||||
IsDescriptionOverridden = response.IsDescriptionOverridden,
|
||||
IsRawAffixTextOverridden = response.IsRawAffixTextOverridden,
|
||||
AreEffectsOverridden = response.AreEffectsOverridden,
|
||||
AreBranchesOverridden = response.AreBranchesOverridden,
|
||||
ValidationMessages = response.ValidationMessages.ToList(),
|
||||
Effects = response.Effects.Select(CriticalEffectEditorModel.FromItem).ToList(),
|
||||
Branches = response.Branches.Select(CriticalBranchEditorModel.FromItem).ToList()
|
||||
};
|
||||
|
||||
public CriticalCellUpdateRequest ToRequest() =>
|
||||
new(
|
||||
public CriticalCellUpdateRequest ToRequest()
|
||||
{
|
||||
var request = new CriticalCellUpdateRequest(
|
||||
RawCellText,
|
||||
DescriptionText,
|
||||
RawAffixText,
|
||||
ResolveParseStatus(Effects, Branches),
|
||||
SerializeParsedEffects(Effects),
|
||||
IsDescriptionOverridden,
|
||||
IsRawAffixTextOverridden,
|
||||
AreEffectsOverridden,
|
||||
AreBranchesOverridden,
|
||||
Effects.Select(effect => effect.ToItem()).ToList(),
|
||||
Branches
|
||||
.OrderBy(branch => branch.SortOrder)
|
||||
@@ -64,6 +77,12 @@ public sealed class CriticalCellEditorModel
|
||||
})
|
||||
.ToList());
|
||||
|
||||
return request with
|
||||
{
|
||||
ParsedJson = CriticalCellEditorSnapshot.FromRequest(request).ToJson()
|
||||
};
|
||||
}
|
||||
|
||||
private static string ResolveParseStatus(
|
||||
IReadOnlyList<CriticalEffectEditorModel> effects,
|
||||
IReadOnlyList<CriticalBranchEditorModel> branches) =>
|
||||
|
||||
@@ -16,6 +16,8 @@ public sealed class CriticalEffectEditorModel
|
||||
public bool IsPermanent { get; set; }
|
||||
public string SourceType { get; set; } = "symbol";
|
||||
public string? SourceText { get; set; }
|
||||
public string? OriginKey { get; set; }
|
||||
public bool IsOverridden { get; set; }
|
||||
|
||||
public static CriticalEffectEditorModel FromItem(CriticalEffectEditorItem item) =>
|
||||
new()
|
||||
@@ -31,7 +33,9 @@ public sealed class CriticalEffectEditorModel
|
||||
BodyPart = item.BodyPart,
|
||||
IsPermanent = item.IsPermanent,
|
||||
SourceType = item.SourceType,
|
||||
SourceText = item.SourceText
|
||||
SourceText = item.SourceText,
|
||||
OriginKey = item.OriginKey,
|
||||
IsOverridden = item.IsOverridden
|
||||
};
|
||||
|
||||
public CriticalEffectEditorItem ToItem() =>
|
||||
@@ -47,5 +51,7 @@ public sealed class CriticalEffectEditorModel
|
||||
BodyPart,
|
||||
IsPermanent,
|
||||
SourceType,
|
||||
SourceText);
|
||||
SourceText,
|
||||
OriginKey,
|
||||
IsOverridden);
|
||||
}
|
||||
|
||||
@@ -12,4 +12,7 @@ public sealed record CriticalBranchEditorItem(
|
||||
string? RawAffixText,
|
||||
string ParsedJson,
|
||||
int SortOrder,
|
||||
string? OriginKey,
|
||||
bool IsOverridden,
|
||||
bool AreEffectsOverridden,
|
||||
IReadOnlyList<CriticalEffectEditorItem> Effects);
|
||||
|
||||
@@ -18,6 +18,10 @@ public sealed record CriticalCellEditorResponse(
|
||||
string? RawAffixText,
|
||||
string ParseStatus,
|
||||
string ParsedJson,
|
||||
bool IsDescriptionOverridden,
|
||||
bool IsRawAffixTextOverridden,
|
||||
bool AreEffectsOverridden,
|
||||
bool AreBranchesOverridden,
|
||||
IReadOnlyList<string> ValidationMessages,
|
||||
IReadOnlyList<CriticalEffectEditorItem> Effects,
|
||||
IReadOnlyList<CriticalBranchEditorItem> Branches);
|
||||
|
||||
55
src/RolemasterDb.App/Features/CriticalCellEditorSnapshot.cs
Normal file
55
src/RolemasterDb.App/Features/CriticalCellEditorSnapshot.cs
Normal file
@@ -0,0 +1,55 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace RolemasterDb.App.Features;
|
||||
|
||||
public sealed record CriticalCellEditorSnapshot(
|
||||
int Version,
|
||||
bool IsDescriptionOverridden,
|
||||
bool IsRawAffixTextOverridden,
|
||||
bool AreEffectsOverridden,
|
||||
bool AreBranchesOverridden,
|
||||
IReadOnlyList<CriticalEffectEditorItem> Effects,
|
||||
IReadOnlyList<CriticalBranchEditorItem> Branches)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public static CriticalCellEditorSnapshot FromRequest(CriticalCellUpdateRequest request) =>
|
||||
new(
|
||||
1,
|
||||
request.IsDescriptionOverridden,
|
||||
request.IsRawAffixTextOverridden,
|
||||
request.AreEffectsOverridden,
|
||||
request.AreBranchesOverridden,
|
||||
request.Effects,
|
||||
request.Branches);
|
||||
|
||||
public string ToJson() =>
|
||||
JsonSerializer.Serialize(this, JsonOptions);
|
||||
|
||||
public static bool TryParse(string json, out CriticalCellEditorSnapshot? snapshot)
|
||||
{
|
||||
snapshot = null;
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(json);
|
||||
if (!document.RootElement.TryGetProperty("version", out var versionElement) ||
|
||||
versionElement.ValueKind != JsonValueKind.Number ||
|
||||
versionElement.GetInt32() != 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
snapshot = JsonSerializer.Deserialize<CriticalCellEditorSnapshot>(json, JsonOptions);
|
||||
return snapshot is not null;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace RolemasterDb.App.Features;
|
||||
|
||||
public sealed record CriticalCellReparseRequest(
|
||||
string RawCellText);
|
||||
CriticalCellUpdateRequest CurrentState);
|
||||
|
||||
@@ -8,5 +8,9 @@ public sealed record CriticalCellUpdateRequest(
|
||||
string? RawAffixText,
|
||||
string ParseStatus,
|
||||
string ParsedJson,
|
||||
bool IsDescriptionOverridden,
|
||||
bool IsRawAffixTextOverridden,
|
||||
bool AreEffectsOverridden,
|
||||
bool AreBranchesOverridden,
|
||||
IReadOnlyList<CriticalEffectEditorItem> Effects,
|
||||
IReadOnlyList<CriticalBranchEditorItem> Branches);
|
||||
|
||||
@@ -12,4 +12,6 @@ public sealed record CriticalEffectEditorItem(
|
||||
string? BodyPart,
|
||||
bool IsPermanent,
|
||||
string SourceType,
|
||||
string? SourceText);
|
||||
string? SourceText,
|
||||
string? OriginKey,
|
||||
bool IsOverridden);
|
||||
|
||||
@@ -296,7 +296,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
public async Task<CriticalCellEditorResponse?> ReparseCriticalCellAsync(
|
||||
string slug,
|
||||
int resultId,
|
||||
string rawCellText,
|
||||
CriticalCellUpdateRequest currentState,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await using var dbContext = await dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
@@ -319,8 +319,10 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
}
|
||||
|
||||
var affixLegend = await BuildSharedAffixLegendAsync(dbContext, result.CriticalTableId, cancellationToken);
|
||||
var content = SharedParsing.CriticalCellTextParser.Parse(rawCellText, affixLegend);
|
||||
return CreateCellEditorResponse(result, content);
|
||||
var content = SharedParsing.CriticalCellTextParser.Parse(currentState.RawCellText, affixLegend);
|
||||
var generatedState = CreateGeneratedEditorState(content);
|
||||
var mergedState = MergeGeneratedState(currentState, generatedState);
|
||||
return CreateCellEditorResponse(result, mergedState, content.ValidationErrors);
|
||||
}
|
||||
|
||||
public async Task<CriticalCellEditorResponse?> UpdateCriticalCellAsync(
|
||||
@@ -354,14 +356,14 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
result.DescriptionText = request.DescriptionText.Trim();
|
||||
result.RawAffixText = NormalizeOptionalText(request.RawAffixText);
|
||||
result.ParseStatus = request.ParseStatus.Trim();
|
||||
result.ParsedJson = string.IsNullOrWhiteSpace(request.ParsedJson) ? "{}" : request.ParsedJson.Trim();
|
||||
result.ParsedJson = CriticalCellEditorSnapshot.FromRequest(request).ToJson();
|
||||
|
||||
ReplaceBaseEffects(dbContext, result, request.Effects);
|
||||
ReplaceBranches(dbContext, result, request.Branches);
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return CreateCellEditorResponse(result);
|
||||
return CreateCellEditorResponse(result, request, []);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<CriticalTableLegendEntry> BuildLegend(IReadOnlyList<CriticalTableCellDetail> cells)
|
||||
@@ -422,37 +424,20 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
effect.SourceType,
|
||||
effect.SourceText);
|
||||
|
||||
private static CriticalCellEditorResponse CreateCellEditorResponse(CriticalResult result) =>
|
||||
new(
|
||||
result.Id,
|
||||
result.CriticalTable.Slug,
|
||||
result.CriticalTable.DisplayName,
|
||||
result.CriticalTable.SourceDocument,
|
||||
result.CriticalRollBand.Label,
|
||||
result.CriticalGroup?.GroupKey,
|
||||
result.CriticalGroup?.Label,
|
||||
result.CriticalColumn.ColumnKey,
|
||||
result.CriticalColumn.Label,
|
||||
result.CriticalColumn.Role,
|
||||
result.RawCellText,
|
||||
result.DescriptionText,
|
||||
result.RawAffixText,
|
||||
result.ParseStatus,
|
||||
result.ParsedJson,
|
||||
[],
|
||||
result.Effects
|
||||
.OrderBy(effect => effect.Id)
|
||||
.Select(CreateEffectEditorItem)
|
||||
.ToList(),
|
||||
result.Branches
|
||||
.OrderBy(branch => branch.SortOrder)
|
||||
.Select(CreateBranchEditorItem)
|
||||
.ToList());
|
||||
private static CriticalCellEditorResponse CreateCellEditorResponse(CriticalResult result)
|
||||
{
|
||||
var state = CreateCurrentEditorState(result);
|
||||
return CreateCellEditorResponse(result, state, []);
|
||||
}
|
||||
|
||||
private static CriticalCellEditorResponse CreateCellEditorResponse(
|
||||
CriticalResult result,
|
||||
SharedParsing.CriticalCellParseContent content) =>
|
||||
new(
|
||||
CriticalCellUpdateRequest state,
|
||||
IReadOnlyList<string> validationMessages)
|
||||
{
|
||||
var snapshotJson = CriticalCellEditorSnapshot.FromRequest(state).ToJson();
|
||||
|
||||
return new(
|
||||
result.Id,
|
||||
result.CriticalTable.Slug,
|
||||
result.CriticalTable.DisplayName,
|
||||
@@ -463,19 +448,19 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
result.CriticalColumn.ColumnKey,
|
||||
result.CriticalColumn.Label,
|
||||
result.CriticalColumn.Role,
|
||||
content.RawCellText,
|
||||
content.DescriptionText,
|
||||
content.RawAffixText,
|
||||
ResolveParseStatus(content.Effects, content.Branches),
|
||||
SerializeParsedEffects(content.Effects),
|
||||
content.ValidationErrors.ToList(),
|
||||
content.Effects
|
||||
.Select(CreateEffectEditorItem)
|
||||
.ToList(),
|
||||
content.Branches
|
||||
.OrderBy(branch => branch.SortOrder)
|
||||
.Select(CreateBranchEditorItem)
|
||||
.ToList());
|
||||
state.RawCellText,
|
||||
state.DescriptionText,
|
||||
state.RawAffixText,
|
||||
state.ParseStatus,
|
||||
snapshotJson,
|
||||
state.IsDescriptionOverridden,
|
||||
state.IsRawAffixTextOverridden,
|
||||
state.AreEffectsOverridden,
|
||||
state.AreBranchesOverridden,
|
||||
validationMessages.ToList(),
|
||||
state.Effects.ToList(),
|
||||
state.Branches.ToList());
|
||||
}
|
||||
|
||||
private static CriticalBranchLookupResponse CreateBranchLookupResponse(CriticalBranch branch) =>
|
||||
new(
|
||||
@@ -491,8 +476,11 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
branch.RawText,
|
||||
branch.SortOrder);
|
||||
|
||||
private static CriticalBranchEditorItem CreateBranchEditorItem(CriticalBranch branch) =>
|
||||
new(
|
||||
private static CriticalBranchEditorItem CreateBranchEditorItem(CriticalBranch branch, int branchIndex)
|
||||
{
|
||||
var originKey = CreateBranchOriginKey(branchIndex, branch.BranchKind, branch.ConditionKey, branch.ConditionText);
|
||||
|
||||
return new(
|
||||
branch.BranchKind,
|
||||
branch.ConditionKey,
|
||||
branch.ConditionText,
|
||||
@@ -502,13 +490,20 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
branch.RawAffixText,
|
||||
branch.ParsedJson,
|
||||
branch.SortOrder,
|
||||
originKey,
|
||||
false,
|
||||
false,
|
||||
(branch.Effects ?? Enumerable.Empty<CriticalEffect>())
|
||||
.OrderBy(effect => effect.Id)
|
||||
.Select(CreateEffectEditorItem)
|
||||
.Select((effect, effectIndex) => CreateEffectEditorItem(effect, CreateBranchEffectOriginKey(originKey, effectIndex, effect.EffectCode)))
|
||||
.ToList());
|
||||
}
|
||||
|
||||
private static CriticalBranchEditorItem CreateBranchEditorItem(SharedParsing.ParsedCriticalBranch branch) =>
|
||||
new(
|
||||
private static CriticalBranchEditorItem CreateBranchEditorItem(SharedParsing.ParsedCriticalBranch branch, int branchIndex)
|
||||
{
|
||||
var originKey = CreateBranchOriginKey(branchIndex, branch.BranchKind, branch.ConditionKey, branch.ConditionText);
|
||||
|
||||
return new(
|
||||
branch.BranchKind,
|
||||
branch.ConditionKey,
|
||||
branch.ConditionText,
|
||||
@@ -518,11 +513,15 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
branch.RawAffixText,
|
||||
SerializeParsedEffects(branch.Effects),
|
||||
branch.SortOrder,
|
||||
originKey,
|
||||
false,
|
||||
false,
|
||||
branch.Effects
|
||||
.Select(CreateEffectEditorItem)
|
||||
.Select((effect, effectIndex) => CreateEffectEditorItem(effect, CreateBranchEffectOriginKey(originKey, effectIndex, effect.EffectCode)))
|
||||
.ToList());
|
||||
}
|
||||
|
||||
private static CriticalEffectEditorItem CreateEffectEditorItem(CriticalEffect effect) =>
|
||||
private static CriticalEffectEditorItem CreateEffectEditorItem(CriticalEffect effect, string originKey) =>
|
||||
new(
|
||||
effect.EffectCode,
|
||||
effect.Target,
|
||||
@@ -535,9 +534,11 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
effect.BodyPart,
|
||||
effect.IsPermanent,
|
||||
effect.SourceType,
|
||||
effect.SourceText);
|
||||
effect.SourceText,
|
||||
originKey,
|
||||
false);
|
||||
|
||||
private static CriticalEffectEditorItem CreateEffectEditorItem(SharedParsing.ParsedCriticalEffect effect) =>
|
||||
private static CriticalEffectEditorItem CreateEffectEditorItem(SharedParsing.ParsedCriticalEffect effect, string originKey) =>
|
||||
new(
|
||||
effect.EffectCode,
|
||||
effect.Target,
|
||||
@@ -550,7 +551,192 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
||||
effect.BodyPart,
|
||||
effect.IsPermanent,
|
||||
effect.SourceType,
|
||||
effect.SourceText);
|
||||
effect.SourceText,
|
||||
originKey,
|
||||
false);
|
||||
|
||||
private static CriticalCellUpdateRequest CreateCurrentEditorState(CriticalResult result)
|
||||
{
|
||||
if (CriticalCellEditorSnapshot.TryParse(result.ParsedJson, out var snapshot) && snapshot is not null)
|
||||
{
|
||||
return new CriticalCellUpdateRequest(
|
||||
result.RawCellText,
|
||||
result.DescriptionText,
|
||||
result.RawAffixText,
|
||||
result.ParseStatus,
|
||||
result.ParsedJson,
|
||||
snapshot.IsDescriptionOverridden,
|
||||
snapshot.IsRawAffixTextOverridden,
|
||||
snapshot.AreEffectsOverridden,
|
||||
snapshot.AreBranchesOverridden,
|
||||
snapshot.Effects.ToList(),
|
||||
snapshot.Branches.ToList());
|
||||
}
|
||||
|
||||
var effects = result.Effects
|
||||
.OrderBy(effect => effect.Id)
|
||||
.Select((effect, effectIndex) => CreateEffectEditorItem(effect, CreateBaseEffectOriginKey(effectIndex, effect.EffectCode)))
|
||||
.ToList();
|
||||
var branches = result.Branches
|
||||
.OrderBy(branch => branch.SortOrder)
|
||||
.Select((branch, branchIndex) => CreateBranchEditorItem(branch, branchIndex))
|
||||
.ToList();
|
||||
|
||||
return new CriticalCellUpdateRequest(
|
||||
result.RawCellText,
|
||||
result.DescriptionText,
|
||||
result.RawAffixText,
|
||||
result.ParseStatus,
|
||||
result.ParsedJson,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
effects,
|
||||
branches);
|
||||
}
|
||||
|
||||
private static CriticalCellUpdateRequest CreateGeneratedEditorState(SharedParsing.CriticalCellParseContent content)
|
||||
{
|
||||
var effects = content.Effects
|
||||
.Select((effect, effectIndex) => CreateEffectEditorItem(effect, CreateBaseEffectOriginKey(effectIndex, effect.EffectCode)))
|
||||
.ToList();
|
||||
var branches = content.Branches
|
||||
.OrderBy(branch => branch.SortOrder)
|
||||
.Select((branch, branchIndex) => CreateBranchEditorItem(branch, branchIndex))
|
||||
.ToList();
|
||||
|
||||
return new CriticalCellUpdateRequest(
|
||||
content.RawCellText,
|
||||
content.DescriptionText,
|
||||
content.RawAffixText,
|
||||
ResolveParseStatus(content.Effects, content.Branches),
|
||||
SerializeParsedEffects(content.Effects),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
effects,
|
||||
branches);
|
||||
}
|
||||
|
||||
private static CriticalCellUpdateRequest MergeGeneratedState(
|
||||
CriticalCellUpdateRequest currentState,
|
||||
CriticalCellUpdateRequest generatedState) =>
|
||||
new(
|
||||
currentState.RawCellText,
|
||||
currentState.IsDescriptionOverridden ? currentState.DescriptionText : generatedState.DescriptionText,
|
||||
currentState.IsRawAffixTextOverridden ? currentState.RawAffixText : generatedState.RawAffixText,
|
||||
generatedState.ParseStatus,
|
||||
generatedState.ParsedJson,
|
||||
currentState.IsDescriptionOverridden,
|
||||
currentState.IsRawAffixTextOverridden,
|
||||
currentState.AreEffectsOverridden,
|
||||
currentState.AreBranchesOverridden,
|
||||
currentState.AreEffectsOverridden
|
||||
? currentState.Effects.ToList()
|
||||
: MergeEffectItems(currentState.Effects, generatedState.Effects),
|
||||
currentState.AreBranchesOverridden
|
||||
? currentState.Branches
|
||||
.OrderBy(branch => branch.SortOrder)
|
||||
.ToList()
|
||||
: MergeBranchItems(currentState.Branches, generatedState.Branches));
|
||||
|
||||
private static List<CriticalBranchEditorItem> MergeBranchItems(
|
||||
IReadOnlyList<CriticalBranchEditorItem> currentBranches,
|
||||
IReadOnlyList<CriticalBranchEditorItem> generatedBranches)
|
||||
{
|
||||
var currentByOrigin = currentBranches
|
||||
.Where(branch => !string.IsNullOrWhiteSpace(branch.OriginKey))
|
||||
.ToDictionary(branch => branch.OriginKey!, StringComparer.Ordinal);
|
||||
var merged = new List<CriticalBranchEditorItem>(generatedBranches.Count);
|
||||
var matchedOrigins = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var generatedBranch in generatedBranches)
|
||||
{
|
||||
if (generatedBranch.OriginKey is not null &&
|
||||
currentByOrigin.TryGetValue(generatedBranch.OriginKey, out var currentBranch))
|
||||
{
|
||||
matchedOrigins.Add(generatedBranch.OriginKey);
|
||||
|
||||
if (currentBranch.IsOverridden)
|
||||
{
|
||||
merged.Add(currentBranch);
|
||||
continue;
|
||||
}
|
||||
|
||||
merged.Add(generatedBranch with
|
||||
{
|
||||
AreEffectsOverridden = currentBranch.AreEffectsOverridden,
|
||||
Effects = currentBranch.AreEffectsOverridden
|
||||
? currentBranch.Effects.ToList()
|
||||
: MergeEffectItems(currentBranch.Effects, generatedBranch.Effects)
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
merged.Add(generatedBranch);
|
||||
}
|
||||
|
||||
merged.AddRange(currentBranches.Where(branch =>
|
||||
branch.IsOverridden &&
|
||||
branch.OriginKey is not null &&
|
||||
!matchedOrigins.Contains(branch.OriginKey)));
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
private static List<CriticalEffectEditorItem> MergeEffectItems(
|
||||
IReadOnlyList<CriticalEffectEditorItem> currentEffects,
|
||||
IReadOnlyList<CriticalEffectEditorItem> generatedEffects)
|
||||
{
|
||||
var currentByOrigin = currentEffects
|
||||
.Where(effect => !string.IsNullOrWhiteSpace(effect.OriginKey))
|
||||
.ToDictionary(effect => effect.OriginKey!, StringComparer.Ordinal);
|
||||
var merged = new List<CriticalEffectEditorItem>(generatedEffects.Count);
|
||||
var matchedOrigins = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var generatedEffect in generatedEffects)
|
||||
{
|
||||
if (generatedEffect.OriginKey is not null &&
|
||||
currentByOrigin.TryGetValue(generatedEffect.OriginKey, out var currentEffect))
|
||||
{
|
||||
matchedOrigins.Add(generatedEffect.OriginKey);
|
||||
merged.Add(currentEffect.IsOverridden ? currentEffect : generatedEffect);
|
||||
continue;
|
||||
}
|
||||
|
||||
merged.Add(generatedEffect);
|
||||
}
|
||||
|
||||
merged.AddRange(currentEffects.Where(effect =>
|
||||
effect.IsOverridden &&
|
||||
effect.OriginKey is not null &&
|
||||
!matchedOrigins.Contains(effect.OriginKey)));
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
private static string CreateBaseEffectOriginKey(int effectIndex, string effectCode) =>
|
||||
$"base:{NormalizeOriginSegment(effectCode)}:{effectIndex + 1}";
|
||||
|
||||
private static string CreateBranchOriginKey(int branchIndex, string branchKind, string? conditionKey, string conditionText) =>
|
||||
$"branch:{NormalizeOriginSegment(branchKind)}:{NormalizeOriginSegment(conditionKey ?? conditionText)}:{branchIndex + 1}";
|
||||
|
||||
private static string CreateBranchEffectOriginKey(string branchOriginKey, int effectIndex, string effectCode) =>
|
||||
$"{branchOriginKey}:effect:{NormalizeOriginSegment(effectCode)}:{effectIndex + 1}";
|
||||
|
||||
private static string NormalizeOriginSegment(string value)
|
||||
{
|
||||
var normalized = new string(value
|
||||
.Trim()
|
||||
.ToLowerInvariant()
|
||||
.Select(character => char.IsLetterOrDigit(character) ? character : '_')
|
||||
.ToArray())
|
||||
.Trim('_');
|
||||
|
||||
return string.IsNullOrWhiteSpace(normalized) ? "empty" : normalized;
|
||||
}
|
||||
|
||||
private static void ReplaceBaseEffects(
|
||||
RolemasterDbContext dbContext,
|
||||
|
||||
@@ -42,7 +42,7 @@ api.MapGet("/tables/critical/{slug}/cells/{resultId:int}", async (string slug, i
|
||||
});
|
||||
api.MapPost("/tables/critical/{slug}/cells/{resultId:int}/reparse", async (string slug, int resultId, CriticalCellReparseRequest request, LookupService lookupService, CancellationToken cancellationToken) =>
|
||||
{
|
||||
var result = await lookupService.ReparseCriticalCellAsync(slug, resultId, request.RawCellText, cancellationToken);
|
||||
var result = await lookupService.ReparseCriticalCellAsync(slug, resultId, request.CurrentState, cancellationToken);
|
||||
return result is null ? Results.NotFound() : Results.Ok(result);
|
||||
});
|
||||
api.MapPut("/tables/critical/{slug}/cells/{resultId:int}", async (string slug, int resultId, CriticalCellUpdateRequest request, LookupService lookupService, CancellationToken cancellationToken) =>
|
||||
|
||||
@@ -41,72 +41,15 @@ public sealed class CriticalCellReparseIntegrationTests
|
||||
public async Task Lookup_service_reparse_uses_shared_parser_and_table_legend_data()
|
||||
{
|
||||
var databasePath = Path.Combine(Path.GetTempPath(), $"rolemaster-reparse-{Guid.NewGuid():N}.db");
|
||||
|
||||
await using (var seedContext = CreateDbContext(databasePath))
|
||||
{
|
||||
await seedContext.Database.EnsureCreatedAsync();
|
||||
|
||||
var table = new CriticalTable
|
||||
{
|
||||
Slug = "slash",
|
||||
DisplayName = "Slash Critical Strike Table",
|
||||
Family = "standard",
|
||||
SourceDocument = "Slash.pdf",
|
||||
Notes = null
|
||||
};
|
||||
var column = new CriticalColumn
|
||||
{
|
||||
CriticalTable = table,
|
||||
ColumnKey = "B",
|
||||
Label = "B",
|
||||
Role = "severity",
|
||||
SortOrder = 2
|
||||
};
|
||||
var rollBand = new CriticalRollBand
|
||||
{
|
||||
CriticalTable = table,
|
||||
Label = "36-40",
|
||||
MinRoll = 36,
|
||||
MaxRoll = 40,
|
||||
SortOrder = 8
|
||||
};
|
||||
var result = new CriticalResult
|
||||
{
|
||||
CriticalTable = table,
|
||||
CriticalColumn = column,
|
||||
CriticalRollBand = rollBand,
|
||||
RawCellText = "Old text",
|
||||
DescriptionText = "Old description",
|
||||
ParseStatus = "verified",
|
||||
ParsedJson = "{}"
|
||||
};
|
||||
|
||||
result.Effects.Add(new CriticalEffect
|
||||
{
|
||||
EffectCode = AppCriticalEffectCodes.StunnedRounds,
|
||||
Target = "foe",
|
||||
DurationRounds = 1,
|
||||
IsPermanent = false,
|
||||
SourceType = "symbol",
|
||||
SourceText = "∫"
|
||||
});
|
||||
|
||||
seedContext.CriticalTables.Add(table);
|
||||
seedContext.CriticalResults.Add(result);
|
||||
await seedContext.SaveChangesAsync();
|
||||
}
|
||||
await SeedSlashResultAsync(databasePath);
|
||||
|
||||
var lookupService = new LookupService(CreateDbContextFactory(databasePath));
|
||||
await using var verifyContext = CreateDbContext(databasePath);
|
||||
var resultId = await verifyContext.CriticalResults
|
||||
.Where(item => item.CriticalTable.Slug == "slash")
|
||||
.Select(item => item.Id)
|
||||
.SingleAsync();
|
||||
var resultId = await GetSlashResultIdAsync(databasePath);
|
||||
|
||||
var response = await lookupService.ReparseCriticalCellAsync(
|
||||
"slash",
|
||||
resultId,
|
||||
"Strike to thigh.\n+10H\nWith greaves: glancing blow.\n2∫");
|
||||
CreateEditorRequest("Strike to thigh.\n+10H\nWith greaves: glancing blow.\n2∫", "Old description"));
|
||||
|
||||
Assert.NotNull(response);
|
||||
Assert.Equal("Strike to thigh.", response!.DescriptionText);
|
||||
@@ -118,6 +61,219 @@ public sealed class CriticalCellReparseIntegrationTests
|
||||
Assert.Empty(response.ValidationMessages);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Lookup_service_reparse_preserves_overridden_description_and_effect_value()
|
||||
{
|
||||
var databasePath = Path.Combine(Path.GetTempPath(), $"rolemaster-reparse-overrides-{Guid.NewGuid():N}.db");
|
||||
await SeedSlashResultAsync(databasePath);
|
||||
|
||||
var lookupService = new LookupService(CreateDbContextFactory(databasePath));
|
||||
var resultId = await GetSlashResultIdAsync(databasePath);
|
||||
|
||||
var response = await lookupService.ReparseCriticalCellAsync(
|
||||
"slash",
|
||||
resultId,
|
||||
CreateEditorRequest(
|
||||
"Strike to thigh.\n+10H",
|
||||
"Curated thigh strike.",
|
||||
isDescriptionOverridden: true,
|
||||
effects:
|
||||
[
|
||||
new CriticalEffectEditorItem(
|
||||
AppCriticalEffectCodes.DirectHits,
|
||||
null,
|
||||
12,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
"manual",
|
||||
"+12H",
|
||||
"base:direct_hits:1",
|
||||
true)
|
||||
]));
|
||||
|
||||
Assert.NotNull(response);
|
||||
Assert.Equal("Curated thigh strike.", response!.DescriptionText);
|
||||
Assert.Single(response.Effects);
|
||||
Assert.Equal(12, response.Effects[0].ValueInteger);
|
||||
Assert.True(response.IsDescriptionOverridden);
|
||||
Assert.True(response.Effects[0].IsOverridden);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Lookup_service_reparse_preserves_manual_branch_collection_when_overridden()
|
||||
{
|
||||
var databasePath = Path.Combine(Path.GetTempPath(), $"rolemaster-reparse-branches-{Guid.NewGuid():N}.db");
|
||||
await SeedSlashResultAsync(databasePath);
|
||||
|
||||
var lookupService = new LookupService(CreateDbContextFactory(databasePath));
|
||||
var resultId = await GetSlashResultIdAsync(databasePath);
|
||||
|
||||
var response = await lookupService.ReparseCriticalCellAsync(
|
||||
"slash",
|
||||
resultId,
|
||||
CreateEditorRequest(
|
||||
"Strike to thigh.\n+10H",
|
||||
"Strike to thigh.",
|
||||
areBranchesOverridden: true,
|
||||
branches:
|
||||
[
|
||||
new CriticalBranchEditorItem(
|
||||
"conditional",
|
||||
"with_shield",
|
||||
"With shield",
|
||||
"{}",
|
||||
"With shield: glancing blow.",
|
||||
"Glancing blow.",
|
||||
null,
|
||||
"{}",
|
||||
1,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
[])
|
||||
]));
|
||||
|
||||
Assert.NotNull(response);
|
||||
Assert.Single(response!.Branches);
|
||||
Assert.Equal("with_shield", response.Branches[0].ConditionKey);
|
||||
Assert.True(response.AreBranchesOverridden);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Lookup_service_save_and_load_preserves_override_metadata()
|
||||
{
|
||||
var databasePath = Path.Combine(Path.GetTempPath(), $"rolemaster-save-load-{Guid.NewGuid():N}.db");
|
||||
await SeedSlashResultAsync(databasePath);
|
||||
|
||||
var lookupService = new LookupService(CreateDbContextFactory(databasePath));
|
||||
var resultId = await GetSlashResultIdAsync(databasePath);
|
||||
var request = CreateEditorRequest(
|
||||
"Strike to thigh.\n+10H",
|
||||
"Curated thigh strike.",
|
||||
isDescriptionOverridden: true,
|
||||
effects:
|
||||
[
|
||||
new CriticalEffectEditorItem(
|
||||
AppCriticalEffectCodes.DirectHits,
|
||||
null,
|
||||
12,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
"manual",
|
||||
"+12H",
|
||||
"base:direct_hits:1",
|
||||
true)
|
||||
]);
|
||||
|
||||
var saveResponse = await lookupService.UpdateCriticalCellAsync("slash", resultId, request);
|
||||
var loadResponse = await lookupService.GetCriticalCellEditorAsync("slash", resultId);
|
||||
|
||||
Assert.NotNull(saveResponse);
|
||||
Assert.NotNull(loadResponse);
|
||||
Assert.True(loadResponse!.IsDescriptionOverridden);
|
||||
Assert.Single(loadResponse.Effects);
|
||||
Assert.Equal("base:direct_hits:1", loadResponse.Effects[0].OriginKey);
|
||||
Assert.True(loadResponse.Effects[0].IsOverridden);
|
||||
Assert.Contains("\"version\":1", loadResponse.ParsedJson, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static CriticalCellUpdateRequest CreateEditorRequest(
|
||||
string rawCellText,
|
||||
string descriptionText,
|
||||
bool isDescriptionOverridden = false,
|
||||
bool isRawAffixTextOverridden = false,
|
||||
bool areEffectsOverridden = false,
|
||||
bool areBranchesOverridden = false,
|
||||
string? rawAffixText = null,
|
||||
IReadOnlyList<CriticalEffectEditorItem>? effects = null,
|
||||
IReadOnlyList<CriticalBranchEditorItem>? branches = null) =>
|
||||
new(
|
||||
rawCellText,
|
||||
descriptionText,
|
||||
rawAffixText,
|
||||
"partial",
|
||||
"{}",
|
||||
isDescriptionOverridden,
|
||||
isRawAffixTextOverridden,
|
||||
areEffectsOverridden,
|
||||
areBranchesOverridden,
|
||||
effects ?? [],
|
||||
branches ?? []);
|
||||
|
||||
private static async Task SeedSlashResultAsync(string databasePath)
|
||||
{
|
||||
await using var seedContext = CreateDbContext(databasePath);
|
||||
await seedContext.Database.EnsureCreatedAsync();
|
||||
|
||||
var table = new CriticalTable
|
||||
{
|
||||
Slug = "slash",
|
||||
DisplayName = "Slash Critical Strike Table",
|
||||
Family = "standard",
|
||||
SourceDocument = "Slash.pdf",
|
||||
Notes = null
|
||||
};
|
||||
var column = new CriticalColumn
|
||||
{
|
||||
CriticalTable = table,
|
||||
ColumnKey = "B",
|
||||
Label = "B",
|
||||
Role = "severity",
|
||||
SortOrder = 2
|
||||
};
|
||||
var rollBand = new CriticalRollBand
|
||||
{
|
||||
CriticalTable = table,
|
||||
Label = "36-40",
|
||||
MinRoll = 36,
|
||||
MaxRoll = 40,
|
||||
SortOrder = 8
|
||||
};
|
||||
var result = new CriticalResult
|
||||
{
|
||||
CriticalTable = table,
|
||||
CriticalColumn = column,
|
||||
CriticalRollBand = rollBand,
|
||||
RawCellText = "Old text",
|
||||
DescriptionText = "Old description",
|
||||
ParseStatus = "verified",
|
||||
ParsedJson = "{}"
|
||||
};
|
||||
|
||||
result.Effects.Add(new CriticalEffect
|
||||
{
|
||||
EffectCode = AppCriticalEffectCodes.StunnedRounds,
|
||||
Target = "foe",
|
||||
DurationRounds = 1,
|
||||
IsPermanent = false,
|
||||
SourceType = "symbol",
|
||||
SourceText = "∫"
|
||||
});
|
||||
|
||||
seedContext.CriticalTables.Add(table);
|
||||
seedContext.CriticalResults.Add(result);
|
||||
await seedContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static async Task<int> GetSlashResultIdAsync(string databasePath)
|
||||
{
|
||||
await using var verifyContext = CreateDbContext(databasePath);
|
||||
return await verifyContext.CriticalResults
|
||||
.Where(item => item.CriticalTable.Slug == "slash")
|
||||
.Select(item => item.Id)
|
||||
.SingleAsync();
|
||||
}
|
||||
|
||||
private static RolemasterDbContext CreateDbContext(string databasePath)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<RolemasterDbContext>()
|
||||
|
||||
Reference in New Issue
Block a user