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
67
src/AcDream.Core/Spells/EnchantmentRegistryProjection.cs
Normal file
67
src/AcDream.Core/Spells/EnchantmentRegistryProjection.cs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue