feat: port retail magic lifecycle and retained spell UI
Complete the retail cast-intent, target, component, enchantment, and busy-state paths; mount the DAT-authored spell bar, spellbook, component book, effects panels, and shared panel lifecycle; and add scoped input plus conformance coverage. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
7b7ffcd278
commit
07be994d97
84 changed files with 17822 additions and 1051 deletions
|
|
@ -0,0 +1,91 @@
|
|||
using AcDream.Core.Spells;
|
||||
|
||||
namespace AcDream.Core.Tests.Spells;
|
||||
|
||||
public sealed class EnchantmentRegistryProjectionTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetEnchantmentsInEffect_ExcludesVitaeAndCooldown()
|
||||
{
|
||||
IReadOnlyList<ActiveEnchantmentRecord> result =
|
||||
EnchantmentRegistryProjection.GetEnchantmentsInEffect(
|
||||
[
|
||||
Enchantment(1u, 1u, category: 1u),
|
||||
Enchantment(2u, 4u, category: 2u),
|
||||
Enchantment(3u, 8u, category: 3u),
|
||||
Enchantment(4u, 2u, category: 4u),
|
||||
]);
|
||||
|
||||
Assert.Equal([1u, 4u], result.Select(record => record.SpellId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEnchantmentsInEffect_CategoryDuel_PrefersHigherPower()
|
||||
{
|
||||
IReadOnlyList<ActiveEnchantmentRecord> result =
|
||||
EnchantmentRegistryProjection.GetEnchantmentsInEffect(
|
||||
[
|
||||
Enchantment(1u, 1u, category: 12u, power: 1u),
|
||||
Enchantment(2u, 1u, category: 12u, power: 7u),
|
||||
]);
|
||||
|
||||
Assert.Equal(2u, Assert.Single(result).SpellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEnchantmentsInEffect_PowerComparisonUsesRetailSignedInt()
|
||||
{
|
||||
IReadOnlyList<ActiveEnchantmentRecord> result =
|
||||
EnchantmentRegistryProjection.GetEnchantmentsInEffect(
|
||||
[
|
||||
Enchantment(1u, 1u, category: 12u, power: 1u),
|
||||
Enchantment(2u, 1u, category: 12u, power: uint.MaxValue),
|
||||
]);
|
||||
|
||||
Assert.Equal(1u, Assert.Single(result).SpellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEnchantmentsInEffect_EqualPower_PrefersLaterStartAndKeepsExactTie()
|
||||
{
|
||||
IReadOnlyList<ActiveEnchantmentRecord> result =
|
||||
EnchantmentRegistryProjection.GetEnchantmentsInEffect(
|
||||
[
|
||||
Enchantment(1u, 1u, category: 12u, power: 7u, start: 10d),
|
||||
Enchantment(2u, 1u, category: 12u, power: 7u, start: 20d),
|
||||
Enchantment(3u, 2u, category: 12u, power: 7u, start: 20d),
|
||||
]);
|
||||
|
||||
Assert.Equal(2u, Assert.Single(result).SpellId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetEnchantmentsInEffect_ReplacingWinner_AppendsInRetailOrder()
|
||||
{
|
||||
IReadOnlyList<ActiveEnchantmentRecord> result =
|
||||
EnchantmentRegistryProjection.GetEnchantmentsInEffect(
|
||||
[
|
||||
Enchantment(1u, 1u, category: 10u, power: 1u),
|
||||
Enchantment(2u, 1u, category: 20u, power: 1u),
|
||||
Enchantment(3u, 2u, category: 10u, power: 2u),
|
||||
]);
|
||||
|
||||
Assert.Equal([2u, 3u], result.Select(record => record.SpellId));
|
||||
}
|
||||
|
||||
private static ActiveEnchantmentRecord Enchantment(
|
||||
uint spellId,
|
||||
uint bucket,
|
||||
uint category,
|
||||
uint power = 1u,
|
||||
double start = 0d)
|
||||
=> new(
|
||||
SpellId: spellId,
|
||||
LayerId: spellId,
|
||||
Duration: 60d,
|
||||
CasterGuid: 1u,
|
||||
Bucket: bucket,
|
||||
StartTime: start,
|
||||
SpellCategory: category,
|
||||
PowerLevel: power);
|
||||
}
|
||||
59
tests/AcDream.Core.Tests/Spells/RetailSpellFormulaTests.cs
Normal file
59
tests/AcDream.Core.Tests/Spells/RetailSpellFormulaTests.cs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
using AcDream.Core.Spells;
|
||||
|
||||
namespace AcDream.Core.Tests.Spells;
|
||||
|
||||
public sealed class RetailSpellFormulaTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(1u, 1)]
|
||||
[InlineData(6u, 6)]
|
||||
[InlineData(0x6Eu, 6)]
|
||||
[InlineData(0x70u, 7)]
|
||||
[InlineData(0xC0u, 7)]
|
||||
[InlineData(0xC1u, 8)]
|
||||
[InlineData(7u, 0)]
|
||||
public void RoughHeuristic_UsesFormulaPowerComponent(uint component, int expected)
|
||||
=> Assert.Equal(expected,
|
||||
RetailSpellFormula.InqSpellLevelByRoughHeuristic([component]));
|
||||
|
||||
[Fact]
|
||||
public void CustomizeForAccount_IsDeterministic_AndOnlyChangesTapers()
|
||||
{
|
||||
uint[] formula = [6u, 63u, 10u, 64u, 20u, 30u, 65u, 40u];
|
||||
|
||||
IReadOnlyList<uint> first = RetailSpellFormula.CustomizeForAccount(
|
||||
formula, 3u, "testaccount");
|
||||
IReadOnlyList<uint> second = RetailSpellFormula.CustomizeForAccount(
|
||||
formula, 3u, "testaccount");
|
||||
|
||||
Assert.Equal(first, second);
|
||||
Assert.Equal(formula[0], first[0]);
|
||||
Assert.Equal(formula[1], first[1]);
|
||||
Assert.Equal(formula[2], first[2]);
|
||||
Assert.InRange(first[3], 63u, 74u);
|
||||
Assert.Equal(formula[4], first[4]);
|
||||
Assert.Equal(formula[5], first[5]);
|
||||
Assert.InRange(first[6], 63u, 74u);
|
||||
Assert.Equal(formula[7], first[7]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(1u, 1)]
|
||||
[InlineData(2u, 2)]
|
||||
[InlineData(3u, 3)]
|
||||
[InlineData(0x6Eu, 3)]
|
||||
[InlineData(0x70u, 4)]
|
||||
[InlineData(0xC1u, 4)]
|
||||
public void ScarabOnlyFormula_KeepsPowerAndAppendsRetailTaperCount(
|
||||
uint powerComponent,
|
||||
int expectedTapers)
|
||||
{
|
||||
IReadOnlyList<uint> result = RetailSpellFormula.InqScarabOnlyFormula(
|
||||
[powerComponent, 63u, 10u, 64u]);
|
||||
|
||||
Assert.Equal(powerComponent, result[0]);
|
||||
Assert.Equal(expectedTapers, result.Count(component => component == 0xBCu));
|
||||
Assert.DoesNotContain(63u, result);
|
||||
Assert.DoesNotContain(10u, result);
|
||||
}
|
||||
}
|
||||
|
|
@ -61,6 +61,72 @@ public sealed class SpellbookTests
|
|||
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()
|
||||
{
|
||||
|
|
@ -76,18 +142,49 @@ public sealed class SpellbookTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void OnPurgeAll_RemovesAllEnchantments_FiresPerRecord()
|
||||
public void OnPurgeAll_RemovesOnlyFiniteMultAndAddEnchantments()
|
||||
{
|
||||
var book = new Spellbook();
|
||||
book.OnEnchantmentAdded(1, 1, 100f, 0);
|
||||
book.OnEnchantmentAdded(2, 2, 100f, 0);
|
||||
book.OnEnchantmentAdded(3, 3, 100f, 0);
|
||||
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(3, removals);
|
||||
Assert.Equal(0, book.ActiveCount);
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue