acdream/src/AcDream.Core/Spells/EnchantmentRegistryProjection.cs
Erik 07be994d97 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>
2026-07-15 10:55:22 +02:00

67 lines
2.6 KiB
C#

using System;
using System.Collections.Generic;
namespace AcDream.Core.Spells;
/// <summary>
/// Pure port of retail's ordinary in-effect registry projection:
/// <c>CEnchantmentRegistry::GetEnchantmentsInEffect @ 0x00594540</c>,
/// <c>GetEnchantmentsInEffectFromList @ 0x00594430</c>,
/// <c>Duel @ 0x005942B0</c>, and <c>Enchantment::Duel @ 0x005CACE0</c>.
/// </summary>
public static class EnchantmentRegistryProjection
{
/// <summary>
/// Merges multiplicative then additive records and retains one winner per
/// spell category. Higher power wins; equal power is replaced only by a
/// later start time. Vitae and cooldown registries do not participate.
/// </summary>
public static IReadOnlyList<ActiveEnchantmentRecord> GetEnchantmentsInEffect(
IEnumerable<ActiveEnchantmentRecord> enchantments)
{
ArgumentNullException.ThrowIfNull(enchantments);
ActiveEnchantmentRecord[] source = enchantments as ActiveEnchantmentRecord[]
?? [.. enchantments];
var result = new List<ActiveEnchantmentRecord>();
DuelList(source, bucket: 1u, result);
DuelList(source, bucket: 2u, result);
return result;
}
private static void DuelList(
IReadOnlyList<ActiveEnchantmentRecord> source,
uint bucket,
List<ActiveEnchantmentRecord> result)
{
for (int sourceIndex = 0; sourceIndex < source.Count; sourceIndex++)
{
ActiveEnchantmentRecord candidate = source[sourceIndex];
if (candidate.Bucket != bucket) continue;
int incumbentIndex = result.FindIndex(existing =>
existing.SpellCategory == candidate.SpellCategory);
if (incumbentIndex >= 0)
{
ActiveEnchantmentRecord incumbent = result[incumbentIndex];
if (!CandidateWins(incumbent, candidate))
continue;
// Retail removes the losing node, then appends the winner.
result.RemoveAt(incumbentIndex);
}
result.Add(candidate);
}
}
private static bool CandidateWins(
ActiveEnchantmentRecord incumbent,
ActiveEnchantmentRecord candidate)
{
// Retail Enchantment::_power_level is signed int even though the wire
// field is read as four raw bytes.
int incumbentPower = unchecked((int)incumbent.PowerLevel);
int candidatePower = unchecked((int)candidate.PowerLevel);
return candidatePower > incumbentPower
|| (candidatePower == incumbentPower
&& candidate.StartTime > incumbent.StartTime);
}
}