Support signed power point affixes
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
@using System.Diagnostics.CodeAnalysis
|
@using System.Diagnostics.CodeAnalysis
|
||||||
@using RolemasterDb.App.Domain
|
@using RolemasterDb.App.Domain
|
||||||
@using RolemasterDb.App.Features
|
@using RolemasterDb.App.Features
|
||||||
|
@using PowerPointModifierNotation = RolemasterDb.CriticalParsing.PowerPointModifierNotation
|
||||||
|
|
||||||
@if (EffectiveEffects.Count > 0)
|
@if (EffectiveEffects.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -61,7 +62,7 @@
|
|||||||
CriticalEffectCodes.DirectHits => $"+{effect.ValueInteger?.ToString() ?? string.Empty}",
|
CriticalEffectCodes.DirectHits => $"+{effect.ValueInteger?.ToString() ?? string.Empty}",
|
||||||
CriticalEffectCodes.FoePenalty
|
CriticalEffectCodes.FoePenalty
|
||||||
or CriticalEffectCodes.AttackerBonusNextRound => effect.Modifier?.ToString() ?? string.Empty,
|
or CriticalEffectCodes.AttackerBonusNextRound => effect.Modifier?.ToString() ?? string.Empty,
|
||||||
CriticalEffectCodes.PowerPointModifier => effect.ValueExpression ?? string.Empty,
|
CriticalEffectCodes.PowerPointModifier => PowerPointModifierNotation.FormatSignedExpression(effect.ValueExpression) ?? string.Empty,
|
||||||
_ => effect.ValueInteger?.ToString()
|
_ => effect.ValueInteger?.ToString()
|
||||||
?? effect.Modifier?.ToString()
|
?? effect.Modifier?.ToString()
|
||||||
?? effect.ValueExpression
|
?? effect.ValueExpression
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using RolemasterDb.CriticalParsing;
|
||||||
|
|
||||||
namespace RolemasterDb.App.Features;
|
namespace RolemasterDb.App.Features;
|
||||||
|
|
||||||
@@ -72,25 +73,10 @@ public static class CriticalQuickNotationFormatter
|
|||||||
|
|
||||||
private static string? FormatPowerPointModifier(string? valueExpression, string? sourceText)
|
private static string? FormatPowerPointModifier(string? valueExpression, string? sourceText)
|
||||||
{
|
{
|
||||||
var expression = CollapseWhitespace(valueExpression);
|
var expression = PowerPointModifierNotation.FormatSignedExpression(valueExpression);
|
||||||
if (string.IsNullOrWhiteSpace(expression))
|
return string.IsNullOrWhiteSpace(expression)
|
||||||
{
|
? sourceText
|
||||||
return sourceText;
|
: $"{expression}pp";
|
||||||
}
|
|
||||||
|
|
||||||
expression = expression.Trim();
|
|
||||||
if (expression.StartsWith('+'))
|
|
||||||
{
|
|
||||||
expression = expression[1..];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expression.StartsWith('(') && expression.EndsWith(')') && expression.Length > 2)
|
|
||||||
{
|
|
||||||
expression = expression[1..^1].Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
expression = Regex.Replace(expression, @"\s+", string.Empty);
|
|
||||||
return $"+{expression}pp";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string CollapseWhitespace(string? value) =>
|
private static string CollapseWhitespace(string? value) =>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ public static class AffixEffectParser
|
|||||||
private const string FoeTarget = "foe";
|
private const string FoeTarget = "foe";
|
||||||
|
|
||||||
private static readonly Regex DirectHitsRegex = new(@"[+-]\s*\d+\s*H\b", RegexOptions.Compiled);
|
private static readonly Regex DirectHitsRegex = new(@"[+-]\s*\d+\s*H\b", RegexOptions.Compiled);
|
||||||
private static readonly Regex PowerPointModifierRegex = new(@"\+\s*\((?<expression>[^)]+)\)\s*P\b", RegexOptions.Compiled);
|
private static readonly Regex PowerPointModifierRegex = new(@"(?<sign>[+-])\s*\((?<expression>[^)]+)\)\s*P\b", RegexOptions.Compiled);
|
||||||
private static readonly Regex ModifierRegex = new(@"\((?<noise>[^0-9+\-)]*)(?<sign>[+-])\s*(?<value>\d+)\)", RegexOptions.Compiled);
|
private static readonly Regex ModifierRegex = new(@"\((?<noise>[^0-9+\-)]*)(?<sign>[+-])\s*(?<value>\d+)\)", RegexOptions.Compiled);
|
||||||
private static readonly Regex QuickDirectHitsRegex = new(@"^\+(?<value>\d+)(?:H)?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private static readonly Regex QuickDirectHitsRegex = new(@"^\+(?<value>\d+)(?:H)?$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
private static readonly Regex QuickStunnedRegex = new(@"^(?<value>\d+)s$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private static readonly Regex QuickStunnedRegex = new(@"^(?<value>\d+)s$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
@@ -16,7 +16,7 @@ public static class AffixEffectParser
|
|||||||
private static readonly Regex QuickBleedRegex = new(@"^(?<value>\d+)hpr$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private static readonly Regex QuickBleedRegex = new(@"^(?<value>\d+)hpr$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
private static readonly Regex QuickFoePenaltyRegex = new(@"^-(?<value>\d+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private static readonly Regex QuickFoePenaltyRegex = new(@"^-(?<value>\d+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
private static readonly Regex QuickAttackerBonusRegex = new(@"^\+(?<value>\d+)b$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private static readonly Regex QuickAttackerBonusRegex = new(@"^\+(?<value>\d+)b$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
private static readonly Regex QuickPowerPointRegex = new(@"^\+(?<expression>.+)pp$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
private static readonly Regex QuickPowerPointRegex = new(@"^(?<sign>[+-])(?<expression>.+)pp$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||||
|
|
||||||
public static IReadOnlyList<ParsedCriticalEffect> Parse(string? rawAffixText, AffixLegend affixLegend)
|
public static IReadOnlyList<ParsedCriticalEffect> Parse(string? rawAffixText, AffixLegend affixLegend)
|
||||||
{
|
{
|
||||||
@@ -101,7 +101,9 @@ public static class AffixEffectParser
|
|||||||
CriticalEffectCodes.PowerPointModifier,
|
CriticalEffectCodes.PowerPointModifier,
|
||||||
FoeTarget,
|
FoeTarget,
|
||||||
null,
|
null,
|
||||||
CriticalCellParserSupport.CollapseWhitespace(match.Groups["expression"].Value),
|
PowerPointModifierNotation.NormalizeExpression(
|
||||||
|
match.Groups["expression"].Value,
|
||||||
|
match.Groups["sign"].Value[0]),
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
@@ -161,6 +163,13 @@ public static class AffixEffectParser
|
|||||||
var symbolClusterRegex = CreateSymbolClusterRegex(affixLegend.EffectSymbols);
|
var symbolClusterRegex = CreateSymbolClusterRegex(affixLegend.EffectSymbols);
|
||||||
if (symbolClusterRegex is null)
|
if (symbolClusterRegex is null)
|
||||||
{
|
{
|
||||||
|
if (matchedEffects.Count > 0)
|
||||||
|
{
|
||||||
|
effects.AddRange(matchedEffects
|
||||||
|
.OrderBy(item => item.Index)
|
||||||
|
.Select(item => item.Effect));
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,18 +332,24 @@ public static class AffixEffectParser
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (TryMatchSingle(QuickPowerPointRegex, token, match =>
|
if (TryMatchSingle(QuickPowerPointRegex, token, match =>
|
||||||
new ParsedCriticalEffect(
|
{
|
||||||
|
var normalizedExpression = PowerPointModifierNotation.NormalizeExpression(
|
||||||
|
match.Groups["expression"].Value,
|
||||||
|
match.Groups["sign"].Value[0])!;
|
||||||
|
|
||||||
|
return new ParsedCriticalEffect(
|
||||||
CriticalEffectCodes.PowerPointModifier,
|
CriticalEffectCodes.PowerPointModifier,
|
||||||
FoeTarget,
|
FoeTarget,
|
||||||
null,
|
null,
|
||||||
NormalizePowerPointExpression(match.Groups["expression"].Value),
|
normalizedExpression,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
"quick",
|
"quick",
|
||||||
$"+{NormalizePowerPointExpression(match.Groups["expression"].Value)}pp"), out effects, out canonicalToken))
|
$"{normalizedExpression}pp");
|
||||||
|
}, out effects, out canonicalToken))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -477,22 +492,4 @@ public static class AffixEffectParser
|
|||||||
.Replace(" +", "+", StringComparison.Ordinal)
|
.Replace(" +", "+", StringComparison.Ordinal)
|
||||||
.Replace("( ", "(", StringComparison.Ordinal)
|
.Replace("( ", "(", StringComparison.Ordinal)
|
||||||
.Replace(" )", ")", StringComparison.Ordinal);
|
.Replace(" )", ")", StringComparison.Ordinal);
|
||||||
|
|
||||||
private static string NormalizePowerPointExpression(string value)
|
|
||||||
{
|
|
||||||
var normalized = CriticalCellParserSupport.CollapseWhitespace(value)
|
|
||||||
.Replace(" ", string.Empty, StringComparison.Ordinal);
|
|
||||||
|
|
||||||
if (normalized.StartsWith('+'))
|
|
||||||
{
|
|
||||||
normalized = normalized[1..];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normalized.StartsWith('(') && normalized.EndsWith(')') && normalized.Length > 2)
|
|
||||||
{
|
|
||||||
normalized = normalized[1..^1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
namespace RolemasterDb.CriticalParsing;
|
||||||
|
|
||||||
|
public static class PowerPointModifierNotation
|
||||||
|
{
|
||||||
|
public static string? NormalizeExpression(string? expression, char? forcedSign = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(expression))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = CriticalCellParserSupport.CollapseWhitespace(expression)
|
||||||
|
.Replace(" ", string.Empty, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
if (normalized.StartsWith('(') && normalized.EndsWith(')') && normalized.Length > 2)
|
||||||
|
{
|
||||||
|
normalized = normalized[1..^1];
|
||||||
|
}
|
||||||
|
|
||||||
|
char? sign = forcedSign;
|
||||||
|
if (normalized.Length > 0 && (normalized[0] == '+' || normalized[0] == '-'))
|
||||||
|
{
|
||||||
|
sign ??= normalized[0];
|
||||||
|
normalized = normalized[1..];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(normalized))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sign.HasValue
|
||||||
|
? $"{sign}{normalized}"
|
||||||
|
: normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? FormatSignedExpression(string? expression)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeExpression(expression);
|
||||||
|
if (string.IsNullOrWhiteSpace(normalized))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized[0] is '+' or '-'
|
||||||
|
? normalized
|
||||||
|
: $"+{normalized}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,7 +38,7 @@ public sealed class CriticalCellReparseIntegrationTests
|
|||||||
Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.BleedPerRound && effect.PerRound == 1);
|
Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.BleedPerRound && effect.PerRound == 1);
|
||||||
Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.FoePenalty && effect.Modifier == -20);
|
Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.FoePenalty && effect.Modifier == -20);
|
||||||
Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.AttackerBonusNextRound && effect.Modifier == 20);
|
Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.AttackerBonusNextRound && effect.Modifier == 20);
|
||||||
Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.PowerPointModifier && effect.ValueExpression == "2d10-3");
|
Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.PowerPointModifier && effect.ValueExpression == "+2d10-3");
|
||||||
Assert.Single(content.Branches);
|
Assert.Single(content.Branches);
|
||||||
Assert.Equal("glancing blow", content.Branches[0].DescriptionText);
|
Assert.Equal("glancing blow", content.Branches[0].DescriptionText);
|
||||||
Assert.Contains(content.Branches[0].Effects, effect => effect.EffectCode == AppCriticalEffectCodes.DirectHits && effect.ValueInteger == 5);
|
Assert.Contains(content.Branches[0].Effects, effect => effect.EffectCode == AppCriticalEffectCodes.DirectHits && effect.ValueInteger == 5);
|
||||||
@@ -46,6 +46,84 @@ public sealed class CriticalCellReparseIntegrationTests
|
|||||||
Assert.Empty(content.ValidationErrors);
|
Assert.Empty(content.ValidationErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Shared_quick_notation_parser_supports_negative_power_point_shorthand()
|
||||||
|
{
|
||||||
|
var legend = new AffixLegend(
|
||||||
|
new Dictionary<string, string>(StringComparer.Ordinal),
|
||||||
|
["P"],
|
||||||
|
supportsFoePenalty: false,
|
||||||
|
supportsAttackerBonus: false,
|
||||||
|
supportsPowerPointModifier: true);
|
||||||
|
|
||||||
|
var content = CriticalQuickNotationParser.Parse(
|
||||||
|
"Void drains the foe.\r\n-2d10+4pp",
|
||||||
|
legend);
|
||||||
|
|
||||||
|
Assert.Contains(content.Effects, effect => effect.EffectCode == AppCriticalEffectCodes.PowerPointModifier && effect.ValueExpression == "-2d10+4");
|
||||||
|
Assert.Equal("-2d10+4pp", content.RawAffixText);
|
||||||
|
Assert.Empty(content.ValidationErrors);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Shared_symbol_affix_parser_supports_negative_power_point_notation()
|
||||||
|
{
|
||||||
|
var legend = new AffixLegend(
|
||||||
|
new Dictionary<string, string>(StringComparer.Ordinal),
|
||||||
|
["P"],
|
||||||
|
supportsFoePenalty: false,
|
||||||
|
supportsAttackerBonus: false,
|
||||||
|
supportsPowerPointModifier: true);
|
||||||
|
|
||||||
|
var effects = AffixEffectParser.Parse("-(2d10+4)P", legend);
|
||||||
|
|
||||||
|
Assert.Contains(effects, effect => effect.EffectCode == AppCriticalEffectCodes.PowerPointModifier && effect.ValueExpression == "-2d10+4");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Quick_notation_formatter_keeps_power_point_modifier_signs()
|
||||||
|
{
|
||||||
|
var formatted = CriticalQuickNotationFormatter.Format(
|
||||||
|
"Arcane force twists around the foe.",
|
||||||
|
[
|
||||||
|
new CriticalEffectEditorItem(
|
||||||
|
AppCriticalEffectCodes.PowerPointModifier,
|
||||||
|
"foe",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"2d10-3",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
"import",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false),
|
||||||
|
new CriticalEffectEditorItem(
|
||||||
|
AppCriticalEffectCodes.PowerPointModifier,
|
||||||
|
"foe",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
"-1d10+4",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
"import",
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false)
|
||||||
|
],
|
||||||
|
[]);
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
"Arcane force twists around the foe.\n+2d10-3pp, -1d10+4pp",
|
||||||
|
formatted.Replace("\r\n", "\n", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Shared_cell_parser_extracts_base_effects_and_condition_branches()
|
public void Shared_cell_parser_extracts_base_effects_and_condition_branches()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -546,7 +546,7 @@ public sealed class StandardCriticalTableParserIntegrationTests
|
|||||||
effect =>
|
effect =>
|
||||||
{
|
{
|
||||||
Assert.Equal(CriticalEffectCodes.PowerPointModifier, effect.EffectCode);
|
Assert.Equal(CriticalEffectCodes.PowerPointModifier, effect.EffectCode);
|
||||||
Assert.Equal("2d10-18", effect.ValueExpression);
|
Assert.Equal("+2d10-18", effect.ValueExpression);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -627,7 +627,7 @@ public sealed class StandardCriticalTableParserIntegrationTests
|
|||||||
Assert.NotNull(manaResponse);
|
Assert.NotNull(manaResponse);
|
||||||
Assert.Contains(slashResponse!.Branches, branch => branch.ConditionKey == "with_leg_greaves" && branch.Effects.Any(effect => effect.EffectCode == CriticalEffectCodes.MustParryRounds));
|
Assert.Contains(slashResponse!.Branches, branch => branch.ConditionKey == "with_leg_greaves" && branch.Effects.Any(effect => effect.EffectCode == CriticalEffectCodes.MustParryRounds));
|
||||||
Assert.Contains(slashResponse.Branches, branch => branch.ConditionKey == "without_leg_greaves" && branch.Effects.Any(effect => effect.EffectCode == CriticalEffectCodes.BleedPerRound));
|
Assert.Contains(slashResponse.Branches, branch => branch.ConditionKey == "without_leg_greaves" && branch.Effects.Any(effect => effect.EffectCode == CriticalEffectCodes.BleedPerRound));
|
||||||
Assert.Contains(manaResponse!.Effects, effect => effect.EffectCode == CriticalEffectCodes.PowerPointModifier && effect.ValueExpression == "2d10-18");
|
Assert.Contains(manaResponse!.Effects, effect => effect.EffectCode == CriticalEffectCodes.PowerPointModifier && effect.ValueExpression == "+2d10-18");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<CriticalTableParseResult> LoadParseResultAsync(CriticalImportManifestEntry entry)
|
private static async Task<CriticalTableParseResult> LoadParseResultAsync(CriticalImportManifestEntry entry)
|
||||||
|
|||||||
Reference in New Issue
Block a user