New SpellMetadata + SpellTable. Loads docs/research/data/spells.csv at GameWindow construction (3,956 spells x 11 useful fields including Family for buff stacking which issue #6 needs). The CSV is copied to bin/<config>/net10.0/data/spells.csv via the csproj <None Include> entry; SpellTable.LoadFromCsv resolves relative to AppContext.BaseDirectory. Hand-rolled CSV parser handles RFC 4180 quoted fields with embedded commas (the Description column) + escaped double-quotes ("" -> "). No external CsvHelper dep. Falls back to SpellTable.Empty + console warning if the file is missing (tooling contexts). Spellbook now accepts an optional SpellTable in its constructor + exposes TryGetMetadata(spellId, out SpellMetadata). When the table is absent (legacy `new Spellbook()` calls), TryGetMetadata returns false gracefully so existing tests keep passing. GameWindow: - SpellTable field initialized via LoadSpellTable() helper that handles the missing-file case + emits the spells: loaded N entries log line. - SpellBook field constructor-initialized with the loaded SpellTable so TryGetMetadata works for the live session. 10 new tests (SpellTableTests): - Empty table behavior - Header-only loads to empty - Single row populates all metadata - Quoted Description with embedded commas - Blank lines skipped - Bad-spell-id rows silently skipped (third-party data is messy) - Unknown spell-id lookup returns false - ParseRow primitive: simple comma split, quoted-field with comma, escaped double-quote. Total tests: 818 -> 828. Closes #11. Phase G (issue #6 — fold enchantment buffs into vital max via EnchantmentMath using SpellTable.Family for stacking) unblocked. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
128 lines
4.9 KiB
C#
128 lines
4.9 KiB
C#
using System.IO;
|
|
using AcDream.Core.Spells;
|
|
|
|
namespace AcDream.Core.Tests.Spells;
|
|
|
|
/// <summary>
|
|
/// Tests for the <see cref="SpellTable"/> CSV loader. Uses synthetic
|
|
/// fixture strings rather than a real spells.csv so we don't depend
|
|
/// on docs/research/data/ contents at test time.
|
|
///
|
|
/// Closes ISSUES.md #11 — spell metadata pipeline.
|
|
/// </summary>
|
|
public sealed class SpellTableTests
|
|
{
|
|
// Header used across fixtures — matches the column names from the
|
|
// real docs/research/data/spells.csv. Row format is the same.
|
|
private const string Header =
|
|
"Spell ID,Spell ID [Hex],Name,SortKey,IconId [Hex],Difficulty,Duration,Family,Flags [Hex],Generation,IsDebuff,IsFastWindup,IsFellowship,IsIrresistible,IsOffensive,IsUntargetted,Mana,School,Speed,Spell Words,CasterEffect,TargetEffect,TargetMask [Hex],Type,Description,Unknown1,Unknown2,Unknown3,Unknown4,Unknown5,Unknown6,Unknown7,Unknown8,Unknown9,Unknown10";
|
|
|
|
private static SpellTable LoadFrom(string csv) =>
|
|
SpellTable.LoadFromReader(new StringReader(csv));
|
|
|
|
[Fact]
|
|
public void Empty_TableHasZeroEntries()
|
|
{
|
|
Assert.Equal(0, SpellTable.Empty.Count);
|
|
Assert.False(SpellTable.Empty.TryGet(1u, out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadFromReader_HeaderOnly_EmptyTable()
|
|
{
|
|
var table = LoadFrom(Header);
|
|
Assert.Equal(0, table.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadFromReader_SingleSimpleRow_HasMetadata()
|
|
{
|
|
// Spell ID 1 = Strength Other I, Family 1 (the actual real-data row).
|
|
string csv =
|
|
Header + "\n" +
|
|
"1,0x1,Strength Other I,6450,0x600138C,1,1800,1,0x6,1,False,False,False,True,False,False,10,Creature Enchantment,0,Malar Cazael,0,6,0x10,1,Increases Strength.,5,1,1,1,0,0,0,0,0,0";
|
|
var table = LoadFrom(csv);
|
|
|
|
Assert.Equal(1, table.Count);
|
|
Assert.True(table.TryGet(1u, out var meta));
|
|
Assert.Equal("Strength Other I", meta.Name);
|
|
Assert.Equal("Creature Enchantment", meta.School);
|
|
Assert.Equal(1u, meta.Family);
|
|
Assert.Equal(0x600138Cu, meta.IconId);
|
|
Assert.Equal("Malar Cazael", meta.SpellWords);
|
|
Assert.Equal(1800f, meta.Duration);
|
|
Assert.Equal(10, meta.ManaCost);
|
|
Assert.False(meta.IsDebuff);
|
|
Assert.False(meta.IsFellowship);
|
|
Assert.Equal("Increases Strength.", meta.Description);
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadFromReader_QuotedDescriptionWithCommas_ParsesIntactly()
|
|
{
|
|
// The Description field in the real CSV is double-quoted so the
|
|
// embedded comma doesn't split the row.
|
|
string csv =
|
|
Header + "\n" +
|
|
"2,0x2,Strength Self I,6464,0x600138C,1,1800,1,0x400C,1,False,True,False,True,False,True,15,Creature Enchantment,0.01,Malar Cazael,0,6,0x10,1,\"Increases the caster's Strength by 10 points, lasting 30 minutes.\",0,0,1,2,0,0,0,0,0,0";
|
|
var table = LoadFrom(csv);
|
|
|
|
Assert.True(table.TryGet(2u, out var meta));
|
|
Assert.Equal("Increases the caster's Strength by 10 points, lasting 30 minutes.", meta.Description);
|
|
Assert.Equal("Strength Self I", meta.Name);
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadFromReader_BlankLines_AreSkipped()
|
|
{
|
|
string csv =
|
|
Header + "\n" +
|
|
"1,0x1,Test,0,0x0,1,1,1,0x0,1,False,False,False,False,False,False,1,War Magic,0,Words,0,0,0x0,1,Desc,0,0,0,0,0,0,0,0,0,0\n" +
|
|
"\n" +
|
|
" \n" +
|
|
"2,0x2,Test2,0,0x0,1,1,1,0x0,1,False,False,False,False,False,False,1,War Magic,0,Words,0,0,0x0,1,Desc,0,0,0,0,0,0,0,0,0,0";
|
|
var table = LoadFrom(csv);
|
|
Assert.Equal(2, table.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadFromReader_BadSpellId_RowSkipped()
|
|
{
|
|
string csv =
|
|
Header + "\n" +
|
|
"not_a_uint,0x1,Bad,0,0x0,1,1,1,0x0,1,False,False,False,False,False,False,1,War Magic,0,Words,0,0,0x0,1,Desc,0,0,0,0,0,0,0,0,0,0\n" +
|
|
"5,0x5,Good,0,0x0,1,1,1,0x0,1,False,False,False,False,False,False,1,War Magic,0,Words,0,0,0x0,1,Desc,0,0,0,0,0,0,0,0,0,0";
|
|
var table = LoadFrom(csv);
|
|
Assert.Equal(1, table.Count);
|
|
Assert.True(table.TryGet(5u, out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void TryGet_UnknownSpellId_ReturnsFalse()
|
|
{
|
|
var table = LoadFrom(Header);
|
|
Assert.False(table.TryGet(99999u, out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseRow_SimpleCsv_SplitsOnCommas()
|
|
{
|
|
var fields = SpellTable.ParseRow("a,b,c");
|
|
Assert.Equal(new[] { "a", "b", "c" }, fields);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseRow_QuotedFieldWithComma_KeepsComma()
|
|
{
|
|
var fields = SpellTable.ParseRow("a,\"b,c\",d");
|
|
Assert.Equal(new[] { "a", "b,c", "d" }, fields);
|
|
}
|
|
|
|
[Fact]
|
|
public void ParseRow_EscapedDoubleQuoteInsideQuotedField()
|
|
{
|
|
// RFC 4180: "" inside a quoted field is a literal " character.
|
|
var fields = SpellTable.ParseRow("a,\"b\"\"c\",d");
|
|
Assert.Equal(new[] { "a", "b\"c", "d" }, fields);
|
|
}
|
|
}
|