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:
Erik 2026-07-15 10:55:22 +02:00
parent 7b7ffcd278
commit 07be994d97
84 changed files with 17822 additions and 1051 deletions

View file

@ -0,0 +1,45 @@
using AcDream.Core.Items;
using AcDream.Core.Spells;
namespace AcDream.App.Spells;
public readonly record struct SpellTargetPolicyResult(bool Allowed, string? Message)
{
public static SpellTargetPolicyResult Accept { get; } = new(true, null);
}
/// <summary>
/// Pure projection of ClientMagicSystem::ObjectCompatibleWithSpellTargetType
/// (0x00567230). The caller owns target lookup and message presentation.
/// </summary>
public static class RetailSpellTargetPolicy
{
private const uint SpecialTargetMask = 0x00008107u;
public static SpellTargetPolicyResult Evaluate(
uint localPlayerId,
ClientObject target,
SpellMetadata spell)
{
uint mask = spell.TargetMask;
uint special = mask & SpecialTargetMask;
if (target.ObjectId == localPlayerId && special == 0u)
return new(false, "You cannot cast this spell upon yourself.");
if (target.StackSize > 1)
return new(false, "Cannot cast spell on a stack of items.");
if (((uint)target.Type & mask) == 0u && special == 0u)
return new(false, $"This spell cannot be cast on {target.Name}.");
// Retail joins the ordinary type match and the 0x8107 special-mask
// bypass before these two gates. IsPlayer is PWD bit 3; otherwise the
// object must carry BF_ATTACKABLE. Pets are never legal spell targets.
var flags = (PublicWeenieFlags)target.PublicWeenieBitfield.GetValueOrDefault();
bool playerOrAttackable = (flags
& (PublicWeenieFlags.Player | PublicWeenieFlags.Attackable)) != 0;
if (!playerOrAttackable || target.PetOwnerId != 0u)
return new(false, $"This spell cannot be cast on {target.Name}.");
return SpellTargetPolicyResult.Accept;
}
}