acdream/tests/AcDream.Core.Tests/Spells/SpellbookTests.cs
Erik 0eab7497c1 fix(spells): load complete retail DAT catalog
Replace the incomplete 3,956-row production CSV with one immutable projection of all 6,266 records in portal.dat. Preserve retail formula targeting, shared Spellbook/MagicRuntime metadata ownership, and fail startup when the required SpellTable is absent.

Co-authored-by: OpenAI Codex <codex@openai.com>
2026-07-15 21:17:13 +02:00

214 lines
6.8 KiB
C#

using AcDream.Core.Spells;
using Xunit;
namespace AcDream.Core.Tests.Spells;
public sealed class SpellbookTests
{
[Fact]
public void InstallMetadata_InstallsOnceAndRefreshesExistingLearnedState()
{
var book = new Spellbook();
book.OnSpellLearned(1u);
int changes = 0;
book.SpellbookChanged += () => changes++;
SpellTable table = SpellTable.Create([TestSpell(1u)]);
book.InstallMetadata(table);
Assert.Same(table, book.Metadata);
Assert.True(book.TryGetMetadata(1u, out _));
Assert.Equal(1, changes);
book.InstallMetadata(table);
Assert.Throws<InvalidOperationException>(
() => book.InstallMetadata(SpellTable.Empty));
}
[Fact]
public void OnSpellLearned_FiresEvent_Idempotent()
{
var book = new Spellbook();
int events = 0;
book.SpellLearned += _ => events++;
book.OnSpellLearned(0x3E1); // Flame Bolt I
book.OnSpellLearned(0x3E1); // duplicate — no event
Assert.Equal(1, events);
Assert.Equal(1, book.LearnedCount);
Assert.True(book.Knows(0x3E1));
}
[Fact]
public void OnSpellForgotten_FiresEvent()
{
var book = new Spellbook();
book.OnSpellLearned(0x3E1);
uint seen = 0;
book.SpellForgotten += id => seen = id;
book.OnSpellForgotten(0x3E1);
Assert.Equal(0x3E1u, seen);
Assert.False(book.Knows(0x3E1));
}
[Fact]
public void OnEnchantmentAdded_FiresEvent_StoresByLayer()
{
var book = new Spellbook();
ActiveEnchantmentRecord added = default;
book.EnchantmentAdded += r => added = r;
book.OnEnchantmentAdded(spellId: 42, layerId: 7, duration: 300f, casterGuid: 0xBEEF);
Assert.Equal(42u, added.SpellId);
Assert.Equal(7u, added.LayerId);
Assert.Equal(300f, added.Duration, 4);
Assert.Equal(1, book.ActiveCount);
}
[Fact]
public void OnEnchantmentAdded_SameLayer_Replaces()
{
var book = new Spellbook();
book.OnEnchantmentAdded(42, 7, 300f, 0);
book.OnEnchantmentAdded(42, 7, 600f, 0); // refresh
Assert.Equal(1, book.ActiveCount);
foreach (var e in book.ActiveEnchantments)
Assert.Equal(600f, e.Duration, 4);
}
[Fact]
public void OnEnchantmentAdded_SameLayerDifferentSpell_RemainsDistinct()
{
var book = new Spellbook();
book.OnEnchantmentAdded(42, 7, 300f, 0);
book.OnEnchantmentAdded(43, 7, 600f, 0);
Assert.Equal(2, book.ActiveCount);
}
[Fact]
public void LiveEnchantments_NewIdentityInsertsAtRetailListHead()
{
var book = new Spellbook();
book.OnEnchantmentAdded(Effect(1u, layer: 1u, category: 12u));
book.OnEnchantmentAdded(Effect(2u, layer: 2u, category: 12u));
Assert.Equal(2u, Assert.Single(book.EnchantmentsInEffectSnapshot).SpellId);
}
[Fact]
public void LiveEnchantment_ExistingIdentityRefreshKeepsListPosition()
{
var book = new Spellbook();
book.OnEnchantmentAdded(Effect(1u, layer: 1u, category: 12u));
book.OnEnchantmentAdded(Effect(2u, layer: 2u, category: 12u));
book.OnEnchantmentAdded(Effect(1u, layer: 1u, category: 12u));
Assert.Equal(2u, Assert.Single(book.EnchantmentsInEffectSnapshot).SpellId);
}
[Fact]
public void ManifestEnchantments_PreserveReceivedListOrder()
{
var book = new Spellbook();
book.ReplaceManifest(
new Dictionary<uint, float>(),
[Effect(1u, layer: 1u, category: 12u), Effect(2u, layer: 2u, category: 12u)],
[],
[],
0x3FFFu);
Assert.Equal(1u, Assert.Single(book.EnchantmentsInEffectSnapshot).SpellId);
}
[Fact]
public void ReplaceManifest_ReplacesFavoritesFiltersAndDesiredComponents()
{
var book = new Spellbook();
book.OnSpellLearned(999);
book.ReplaceManifest(
new Dictionary<uint, float> { [42] = 123f },
[new ActiveEnchantmentRecord(50, 2, 60, 1)],
[new uint[] { 42, 43 }],
[(100u, 25u)],
0xA5u);
Assert.False(book.Knows(999));
Assert.True(book.Knows(42));
Assert.Equal(new uint[] { 42, 43 }, book.GetFavorites(0));
Assert.Equal(25u, book.DesiredComponents[100]);
Assert.Equal(0xA5u, book.SpellbookFilters);
Assert.Equal(1, book.ActiveCount);
}
[Fact]
public void OnEnchantmentRemoved_FiresEvent()
{
var book = new Spellbook();
book.OnEnchantmentAdded(42, 7, 300f, 0);
uint removedSpell = 0;
book.EnchantmentRemoved += r => removedSpell = r.SpellId;
book.OnEnchantmentRemoved(7, 42);
Assert.Equal(42u, removedSpell);
Assert.Equal(0, book.ActiveCount);
}
[Fact]
public void OnPurgeAll_RemovesOnlyFiniteMultAndAddEnchantments()
{
var book = new Spellbook();
book.OnEnchantmentAdded(new(1, 1, 100, 0, Bucket: 1));
book.OnEnchantmentAdded(new(2, 2, 100, 0, Bucket: 2));
book.OnEnchantmentAdded(new(3, 3, -1, 0, Bucket: 2));
book.OnEnchantmentAdded(new(4, 4, 100, 0, Bucket: 4));
book.OnEnchantmentAdded(new(5, 5, 100, 0, Bucket: 8));
int removals = 0;
book.EnchantmentRemoved += _ => removals++;
book.OnPurgeAll();
Assert.Equal(2, removals);
Assert.Equal(3, book.ActiveCount);
Assert.Equal([3u, 4u, 5u],
book.ActiveEnchantmentSnapshot.Select(record => record.SpellId).Order().ToArray());
}
[Fact]
public void OnPurgeBad_UsesStatModBeneficialFlag_AndPreservesPermanent()
{
const uint Beneficial = 0x02000000u;
var book = new Spellbook();
book.OnEnchantmentAdded(new(1, 1, 100, 0, StatModType: 0, Bucket: 1));
book.OnEnchantmentAdded(new(2, 2, 100, 0, StatModType: Beneficial, Bucket: 2));
book.OnEnchantmentAdded(new(3, 3, -1, 0, StatModType: 0, Bucket: 2));
book.OnEnchantmentAdded(new(4, 4, 100, 0, StatModType: 0, Bucket: 8));
book.OnPurgeBadEnchantments();
Assert.Equal([2u, 3u, 4u],
book.ActiveEnchantmentSnapshot.Select(record => record.SpellId).Order().ToArray());
}
private static ActiveEnchantmentRecord Effect(uint spellId, uint layer, uint category)
=> new(
SpellId: spellId,
LayerId: layer,
Duration: 60d,
CasterGuid: 1u,
Bucket: 1u,
StartTime: 10d,
SpellCategory: category,
PowerLevel: 7u);
private static SpellMetadata TestSpell(uint spellId) => new(
spellId, "Test", "War Magic", 0u, 0u, "", 0f, 0,
false, false, "", 0, 0, 0u, 0, false, false, true,
0f, 0u, 0u, 0u, 0);
}