diff --git a/converters/npc.cs b/converters/npc.cs new file mode 100644 index 0000000..d8334bc --- /dev/null +++ b/converters/npc.cs @@ -0,0 +1,728 @@ +using System; +using System.Collections.Generic; +using System.IO; + +namespace finder2e_foundry_converter.Converters +{ + public struct Result + { + public T Value { get; } + public string? Error { get; } + public bool IsSuccess => Error == null; + + private Result(T value, string? error) + { + Value = value; + Error = error; + } + + public static Result Success(T value) => new Result(value, null); + public static Result Failure(string error) => new Result(default!, error); + } + + public static class NpcConverter + { + private static Dictionary ParseToml(string content) + { + var root = new Dictionary(); + var currentTable = root; + + var lines = content.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + foreach (var rawLine in lines) + { + var line = rawLine.Trim(); + if (string.IsNullOrEmpty(line) || line.StartsWith("#")) + continue; + + if (line.StartsWith("[") && line.EndsWith("]")) + { + var section = line.Substring(1, line.Length - 2).Trim(); + var parts = section.Split('.'); + + currentTable = root; + foreach (var p in parts) + { + var part = p.Trim().Trim('"'); + if (!currentTable.TryGetValue(part, out var next) || !(next is Dictionary)) + { + var newTable = new Dictionary(); + currentTable[part] = newTable; + currentTable = newTable; + } + else + { + currentTable = (Dictionary)next; + } + } + } + else if (line.Contains("=")) + { + var idx = line.IndexOf('='); + var key = line.Substring(0, idx).Trim().Trim('"'); + var valStr = line.Substring(idx + 1).Trim(); + + if (string.IsNullOrEmpty(key)) continue; + + currentTable[key] = ParseTomlValue(valStr); + } + } + + return root; + } + + private static object ParseTomlValue(string valStr) + { + if (valStr.StartsWith("{") && valStr.EndsWith("}")) + { + var inlineTable = new Dictionary(); + var content = valStr.Substring(1, valStr.Length - 2).Trim(); + if (!string.IsNullOrEmpty(content)) + { + var pairs = content.Split(','); + foreach (var pair in pairs) + { + var idx = pair.IndexOf('='); + if (idx >= 0) + { + var k = pair.Substring(0, idx).Trim().Trim('"'); + var vStr = pair.Substring(idx + 1).Trim(); + inlineTable[k] = ParseTomlValue(vStr); + } + } + } + return inlineTable; + } + + if (valStr.StartsWith("\"") && valStr.EndsWith("\"")) + { + return valStr.Substring(1, valStr.Length - 2); + } + + if (long.TryParse(valStr, out var num)) + { + return num; + } + + return valStr; + } + + private static Result> LoadTableDocument(string normalizedSystem) + { + string path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config", $"{normalizedSystem}_tables.toml"); + try + { + if (!File.Exists(path)) + { + // Fallback attempt: sometimes config might be in working directory during dev + var alternativePath = Path.Combine(Directory.GetCurrentDirectory(), "config", $"{normalizedSystem}_tables.toml"); + if (File.Exists(alternativePath)) + { + path = alternativePath; + } + } + + string rawContents = File.ReadAllText(path); + try + { + var document = ParseToml(rawContents); + return Result>.Success(document); + } + catch (Exception ex) + { + return Result>.Failure($"failed to parse {path}: {ex.Message}"); + } + } + catch (Exception ex) + { + return Result>.Failure($"failed to read {path}: {ex.Message}"); + } + } + + private static object? Navigate(Dictionary doc, params string[] path) + { + object current = doc; + foreach (var key in path) + { + if (current is Dictionary dict) + { + if (!dict.TryGetValue(key, out var next)) + return null; + current = next; + } + else + { + return null; + } + } + return current; + } + + public static Result AbilityScoreModifier(int level, string modifierTier, string system) + { + string normalizedSystem = system.Trim().ToLowerInvariant(); + string normalizedTier = modifierTier.Trim().ToLowerInvariant(); + + if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") + { + return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); + } + + if (normalizedTier != "extreme" && normalizedTier != "high" && normalizedTier != "moderate" && normalizedTier != "low") + { + return Result.Failure($"unsupported ability modifier tier `{normalizedTier}`; expected `extreme`, `high`, `moderate`, or `low`"); + } + + var docResult = LoadTableDocument(normalizedSystem); + if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); + + var value = Navigate(docResult.Value!, "ability_modifiers", "levels", level.ToString(), normalizedTier); + if (value == null) + { + return Result.Failure($"no ability modifier entry found for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + if (value is long number) + { + if (number < int.MinValue || number > int.MaxValue) + { + return Result.Failure($"ability modifier value `{number}` is out of range for i32"); + } + return Result.Success((int)number); + } + if (value is string text && text == "n/a") + { + return Result.Failure($"ability modifier tier `{normalizedTier}` is unavailable for level `{level}`"); + } + + return Result.Failure($"invalid ability modifier value for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + public static Result PerceptionModifier(int level, string modifierTier, string system) + { + string normalizedSystem = system.Trim().ToLowerInvariant(); + string normalizedTier = modifierTier.Trim().ToLowerInvariant(); + + if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") + { + return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); + } + + if (normalizedTier != "extreme" && normalizedTier != "high" && normalizedTier != "moderate" && normalizedTier != "low" && normalizedTier != "terrible") + { + return Result.Failure($"unsupported perception tier `{normalizedTier}`; expected `extreme`, `high`, `moderate`, `low`, or `terrible`"); + } + + var docResult = LoadTableDocument(normalizedSystem); + if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); + + var value = Navigate(docResult.Value!, "perception", "levels", level.ToString(), normalizedTier); + if (value == null) + { + return Result.Failure($"no perception entry found for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + if (value is long number) + { + if (number < int.MinValue || number > int.MaxValue) + { + return Result.Failure($"perception value `{number}` is out of range for i32"); + } + return Result.Success((int)number); + } + + return Result.Failure($"invalid perception value for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + public static Result SkillModifier(int level, string modifierTier, string system) + { + string normalizedSystem = system.Trim().ToLowerInvariant(); + string normalizedTier = modifierTier.Trim().ToLowerInvariant(); + + if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") + { + return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); + } + + if (normalizedTier != "extreme" && normalizedTier != "high" && normalizedTier != "moderate" && normalizedTier != "low") + { + return Result.Failure($"unsupported skill tier `{normalizedTier}`; expected `extreme`, `high`, `moderate`, or `low`"); + } + + var docResult = LoadTableDocument(normalizedSystem); + if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); + + var value = Navigate(docResult.Value!, "skills", "levels", level.ToString(), normalizedTier); + if (value == null) + { + return Result.Failure($"no skill entry found for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + if (value is long number) + { + if (number < int.MinValue || number > int.MaxValue) + { + return Result.Failure($"skill value `{number}` is out of range for i32"); + } + return Result.Success((int)number); + } + + if (value is Dictionary range && normalizedTier == "low") + { + if (!range.TryGetValue("minimum", out var minObj) || minObj is not long minLong) + { + return Result.Failure($"missing skill minimum for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + if (!range.TryGetValue("maximum", out var maxObj) || maxObj is not long maxLong) + { + return Result.Failure($"missing skill maximum for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + if (minLong < int.MinValue || minLong > int.MaxValue) + { + return Result.Failure($"skill minimum `{minLong}` is out of range for i32"); + } + if (maxLong < int.MinValue || maxLong > int.MaxValue) + { + return Result.Failure($"skill maximum `{maxLong}` is out of range for i32"); + } + + int minimum = (int)minLong; + int maximum = (int)maxLong; + + if (minimum > maximum) + { + return Result.Failure($"invalid skill range for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + return Result.Success(Random.Shared.Next(minimum, maximum + 1)); + } + + return Result.Failure($"invalid skill value for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + public static Result Ac(int level, string modifierTier, string system) + { + string normalizedSystem = system.Trim().ToLowerInvariant(); + string normalizedTier = modifierTier.Trim().ToLowerInvariant(); + + if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") + { + return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); + } + + if (normalizedTier != "extreme" && normalizedTier != "high" && normalizedTier != "moderate" && normalizedTier != "low") + { + return Result.Failure($"unsupported armor class tier `{normalizedTier}`; expected `extreme`, `high`, `moderate`, or `low`"); + } + + var docResult = LoadTableDocument(normalizedSystem); + if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); + + var value = Navigate(docResult.Value!, "armor_class", "levels", level.ToString(), normalizedTier); + if (value == null) + { + return Result.Failure($"no armor class entry found for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + if (value is long number) + { + if (number < int.MinValue || number > int.MaxValue) + { + return Result.Failure($"armor class value `{number}` is out of range for i32"); + } + return Result.Success((int)number); + } + + return Result.Failure($"invalid armor class value for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + public static Result SavingThrow(int level, string modifierTier, string system) + { + string normalizedSystem = system.Trim().ToLowerInvariant(); + string normalizedTier = modifierTier.Trim().ToLowerInvariant(); + + if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") + { + return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); + } + + if (normalizedTier != "extreme" && normalizedTier != "high" && normalizedTier != "moderate" && normalizedTier != "low" && normalizedTier != "terrible") + { + return Result.Failure($"unsupported saving throw tier `{normalizedTier}`; expected `extreme`, `high`, `moderate`, `low`, or `terrible`"); + } + + var docResult = LoadTableDocument(normalizedSystem); + if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); + + var value = Navigate(docResult.Value!, "saving_throws", "levels", level.ToString(), normalizedTier); + if (value == null) + { + return Result.Failure($"no saving throw entry found for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + if (value is long number) + { + if (number < int.MinValue || number > int.MaxValue) + { + return Result.Failure($"saving throw value `{number}` is out of range for i32"); + } + return Result.Success((int)number); + } + + return Result.Failure($"invalid saving throw value for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + public static Result Hp(int level, string modifierTier, string system) + { + string normalizedSystem = system.Trim().ToLowerInvariant(); + string normalizedTier = modifierTier.Trim().ToLowerInvariant(); + + if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") + { + return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); + } + + if (normalizedTier != "high" && normalizedTier != "moderate" && normalizedTier != "low") + { + return Result.Failure($"unsupported hit point tier `{normalizedTier}`; expected `high`, `moderate`, or `low`"); + } + + var docResult = LoadTableDocument(normalizedSystem); + if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); + + var value = Navigate(docResult.Value!, "hit_points", "levels", level.ToString(), normalizedTier); + if (value == null) + { + return Result.Failure($"no hit point entry found for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + if (value is Dictionary range) + { + if (!range.TryGetValue("minimum", out var minObj) || minObj is not long minLong) + { + return Result.Failure($"missing hit point minimum for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + if (!range.TryGetValue("maximum", out var maxObj) || maxObj is not long maxLong) + { + return Result.Failure($"missing hit point maximum for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + if (minLong < int.MinValue || minLong > int.MaxValue) + { + return Result.Failure($"hit point minimum `{minLong}` is out of range for i32"); + } + if (maxLong < int.MinValue || maxLong > int.MaxValue) + { + return Result.Failure($"hit point maximum `{maxLong}` is out of range for i32"); + } + + int minimum = (int)minLong; + int maximum = (int)maxLong; + + if (minimum > maximum) + { + return Result.Failure($"invalid hit point range for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + return Result.Success(Random.Shared.Next(minimum, maximum + 1)); + } + + return Result.Failure($"invalid hit point value for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + public static Result ResistanceOrWeakness(int level, string system) + { + string normalizedSystem = system.Trim().ToLowerInvariant(); + + if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") + { + return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); + } + + var docResult = LoadTableDocument(normalizedSystem); + if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); + + var levelTable = Navigate(docResult.Value!, "resistances_and_weaknesses", "levels", level.ToString()); + if (levelTable == null || levelTable is not Dictionary range) + { + return Result.Failure($"no resistance/weakness entry found for system `{normalizedSystem}`, level `{level}`"); + } + + if (!range.TryGetValue("minimum", out var minObj) || minObj is not long minLong) + { + return Result.Failure($"missing resistance/weakness minimum for system `{normalizedSystem}`, level `{level}`"); + } + if (!range.TryGetValue("maximum", out var maxObj) || maxObj is not long maxLong) + { + return Result.Failure($"missing resistance/weakness maximum for system `{normalizedSystem}`, level `{level}`"); + } + + if (minLong < int.MinValue || minLong > int.MaxValue) + { + return Result.Failure($"resistance/weakness minimum `{minLong}` is out of range for i32"); + } + if (maxLong < int.MinValue || maxLong > int.MaxValue) + { + return Result.Failure($"resistance/weakness maximum `{maxLong}` is out of range for i32"); + } + + int minimum = (int)minLong; + int maximum = (int)maxLong; + + if (minimum > maximum) + { + return Result.Failure($"invalid resistance/weakness range for system `{normalizedSystem}`, level `{level}`"); + } + + return Result.Success(Random.Shared.Next(minimum, maximum + 1)); + } + + public static Result StrikeAttackBonus(int level, string modifierTier, string system) + { + string normalizedSystem = system.Trim().ToLowerInvariant(); + string normalizedTier = modifierTier.Trim().ToLowerInvariant(); + + if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") + { + return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); + } + + if (normalizedTier != "extreme" && normalizedTier != "high" && normalizedTier != "moderate" && normalizedTier != "low") + { + return Result.Failure($"unsupported strike attack bonus tier `{normalizedTier}`; expected `extreme`, `high`, `moderate`, or `low`"); + } + + var docResult = LoadTableDocument(normalizedSystem); + if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); + + var value = Navigate(docResult.Value!, "strike_attack_bonus", "levels", level.ToString(), normalizedTier); + if (value == null) + { + return Result.Failure($"no strike attack bonus entry found for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + if (value is long number) + { + if (number < int.MinValue || number > int.MaxValue) + { + return Result.Failure($"strike attack bonus value `{number}` is out of range for i32"); + } + return Result.Success((int)number); + } + + return Result.Failure($"invalid strike attack bonus value for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + public static Result StrikeDamageRoll(int level, string modifierTier, string system) + { + string normalizedSystem = system.Trim().ToLowerInvariant(); + string normalizedTier = modifierTier.Trim().ToLowerInvariant(); + + if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") + { + return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); + } + + if (normalizedTier != "extreme" && normalizedTier != "high" && normalizedTier != "moderate" && normalizedTier != "low") + { + return Result.Failure($"unsupported strike damage tier `{normalizedTier}`; expected `extreme`, `high`, `moderate`, or `low`"); + } + + var docResult = LoadTableDocument(normalizedSystem); + if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); + + var value = Navigate(docResult.Value!, "strike_damage", "levels", level.ToString(), normalizedTier); + if (value == null || value is not Dictionary damageTable) + { + return Result.Failure($"no strike damage entry found for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + if (damageTable.TryGetValue("dice", out var diceObj) && diceObj is string dice) + { + return Result.Success(dice); + } + + return Result.Failure($"invalid strike damage roll for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + public static Result StrikeDamageAverage(int level, string modifierTier, string system) + { + string normalizedSystem = system.Trim().ToLowerInvariant(); + string normalizedTier = modifierTier.Trim().ToLowerInvariant(); + + if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") + { + return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); + } + + if (normalizedTier != "extreme" && normalizedTier != "high" && normalizedTier != "moderate" && normalizedTier != "low") + { + return Result.Failure($"unsupported strike damage tier `{normalizedTier}`; expected `extreme`, `high`, `moderate`, or `low`"); + } + + var docResult = LoadTableDocument(normalizedSystem); + if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); + + var value = Navigate(docResult.Value!, "strike_damage", "levels", level.ToString(), normalizedTier); + if (value == null || value is not Dictionary damageTable) + { + return Result.Failure($"no strike damage entry found for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + if (damageTable.TryGetValue("average", out var avgObj) && avgObj is long average) + { + if (average < int.MinValue || average > int.MaxValue) + { + return Result.Failure($"strike damage average `{average}` is out of range for i32"); + } + return Result.Success((int)average); + } + + return Result.Failure($"invalid strike damage average for system `{normalizedSystem}`, level `{level}`, tier `{normalizedTier}`"); + } + + public static Result AreaDamageRoll(int level, bool unlimited, string system) + { + string normalizedSystem = system.Trim().ToLowerInvariant(); + string column = unlimited ? "unlimited" : "limited"; + + if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") + { + return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); + } + + var docResult = LoadTableDocument(normalizedSystem); + if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); + + var value = Navigate(docResult.Value!, "area_damage", "levels", level.ToString(), column); + if (value == null || value is not Dictionary damageTable) + { + return Result.Failure($"no area damage entry found for system `{normalizedSystem}`, level `{level}`, column `{column}`"); + } + + if (damageTable.TryGetValue("dice", out var diceObj) && diceObj is string dice) + { + return Result.Success(dice); + } + + return Result.Failure($"invalid area damage roll for system `{normalizedSystem}`, level `{level}`, column `{column}`"); + } + + public static Result AreaDamageAverage(int level, bool unlimited, string system) + { + string normalizedSystem = system.Trim().ToLowerInvariant(); + string column = unlimited ? "unlimited" : "limited"; + + if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") + { + return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); + } + + var docResult = LoadTableDocument(normalizedSystem); + if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); + + var value = Navigate(docResult.Value!, "area_damage", "levels", level.ToString(), column); + if (value == null || value is not Dictionary damageTable) + { + return Result.Failure($"no area damage entry found for system `{normalizedSystem}`, level `{level}`, column `{column}`"); + } + + if (damageTable.TryGetValue("average", out var avgObj) && avgObj is long average) + { + if (average < int.MinValue || average > int.MaxValue) + { + return Result.Failure($"area damage average `{average}` is out of range for i32"); + } + return Result.Success((int)average); + } + + return Result.Failure($"invalid area damage average for system `{normalizedSystem}`, level `{level}`, column `{column}`"); + } + + public static Result SpellDc(int level, string modifierTier, string system) + { + string normalizedSystem = system.Trim().ToLowerInvariant(); + string normalizedTier = modifierTier.Trim().ToLowerInvariant(); + string column = normalizedTier switch + { + "extreme" => "extreme_dc", + "high" => "high_dc", + "moderate" => "moderate_dc", + _ => null! + }; + + if (column == null) + { + return Result.Failure($"unsupported spell dc tier `{normalizedTier}`; expected `extreme`, `high`, or `moderate`"); + } + + if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") + { + return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); + } + + var docResult = LoadTableDocument(normalizedSystem); + if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); + + var value = Navigate(docResult.Value!, "spellcasting", "levels", level.ToString(), column); + if (value == null) + { + return Result.Failure($"no spell dc entry found for system `{normalizedSystem}`, level `{level}`, column `{column}`"); + } + + if (value is long number) + { + if (number < int.MinValue || number > int.MaxValue) + { + return Result.Failure($"spell dc value `{number}` is out of range for i32"); + } + return Result.Success((int)number); + } + + return Result.Failure($"invalid spell dc value for system `{normalizedSystem}`, level `{level}`, column `{column}`"); + } + + public static Result SpellAttackBonus(int level, string modifierTier, string system) + { + string normalizedSystem = system.Trim().ToLowerInvariant(); + string normalizedTier = modifierTier.Trim().ToLowerInvariant(); + string column = normalizedTier switch + { + "extreme" => "extreme_spell_attack", + "high" => "high_spell_attack", + "moderate" => "moderate_spell_attack", + _ => null! + }; + + if (column == null) + { + return Result.Failure($"unsupported spell attack bonus tier `{normalizedTier}`; expected `extreme`, `high`, or `moderate`"); + } + + if (normalizedSystem != "pf2e" && normalizedSystem != "sf2e") + { + return Result.Failure($"unsupported game system `{normalizedSystem}`; expected `pf2e` or `sf2e`"); + } + + var docResult = LoadTableDocument(normalizedSystem); + if (!docResult.IsSuccess) return Result.Failure(docResult.Error!); + + var value = Navigate(docResult.Value!, "spellcasting", "levels", level.ToString(), column); + if (value == null) + { + return Result.Failure($"no spell attack bonus entry found for system `{normalizedSystem}`, level `{level}`, column `{column}`"); + } + + if (value is long number) + { + if (number < int.MinValue || number > int.MaxValue) + { + return Result.Failure($"spell attack bonus value `{number}` is out of range for i32"); + } + return Result.Success((int)number); + } + + return Result.Failure($"invalid spell attack bonus value for system `{normalizedSystem}`, level `{level}`, column `{column}`"); + } + } +}