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>
157 lines
5.6 KiB
C#
157 lines
5.6 KiB
C#
using System;
|
|
using AcDream.Core.Spells;
|
|
|
|
namespace AcDream.App.Spells;
|
|
|
|
/// <summary>
|
|
/// App-layer owner for retail cast intent. It validates the client-owned
|
|
/// selection rules, stops movement, and emits exactly one targeted or
|
|
/// untargeted request. ACE owns every gameplay result after that boundary.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Port of <c>ClientMagicSystem::CastSpell</c> (0x00568040) and
|
|
/// <c>FreeHandsAndCastSpell</c> (0x00566EF0). This controller deliberately
|
|
/// does not consume mana/components or start local effects.
|
|
/// </remarks>
|
|
public sealed class SpellCastingController
|
|
{
|
|
private readonly Spellbook _spellbook;
|
|
private readonly Func<uint?> _selectedObject;
|
|
private readonly Func<uint> _localPlayerId;
|
|
private readonly Action _stopCompletely;
|
|
private readonly Action<uint> _sendUntargeted;
|
|
private readonly Action<uint, uint> _sendTargeted;
|
|
private readonly Action<string> _displayMessage;
|
|
private readonly Func<uint, SpellMetadata, bool> _targetCompatible;
|
|
private readonly Func<uint, SpellMetadata, bool> _targetCompatibleSilent;
|
|
private readonly Func<uint, bool> _hasRequiredComponents;
|
|
private readonly Action _incrementBusy;
|
|
private readonly Func<bool> _canSend;
|
|
|
|
public SpellCastingController(
|
|
Spellbook spellbook,
|
|
Func<uint?> selectedObject,
|
|
Func<uint> localPlayerId,
|
|
Action stopCompletely,
|
|
Action<uint> sendUntargeted,
|
|
Action<uint, uint> sendTargeted,
|
|
Action<string> displayMessage,
|
|
Func<uint, SpellMetadata, bool>? targetCompatible = null,
|
|
Func<uint, bool>? hasRequiredComponents = null,
|
|
Action? incrementBusy = null,
|
|
Func<bool>? canSend = null,
|
|
Func<uint, SpellMetadata, bool>? targetCompatibleSilent = null)
|
|
{
|
|
_spellbook = spellbook ?? throw new ArgumentNullException(nameof(spellbook));
|
|
_selectedObject = selectedObject ?? throw new ArgumentNullException(nameof(selectedObject));
|
|
_localPlayerId = localPlayerId ?? throw new ArgumentNullException(nameof(localPlayerId));
|
|
_stopCompletely = stopCompletely ?? throw new ArgumentNullException(nameof(stopCompletely));
|
|
_sendUntargeted = sendUntargeted ?? throw new ArgumentNullException(nameof(sendUntargeted));
|
|
_sendTargeted = sendTargeted ?? throw new ArgumentNullException(nameof(sendTargeted));
|
|
_displayMessage = displayMessage ?? throw new ArgumentNullException(nameof(displayMessage));
|
|
_targetCompatible = targetCompatible ?? ((_, _) => true);
|
|
_targetCompatibleSilent = targetCompatibleSilent ?? _targetCompatible;
|
|
_hasRequiredComponents = hasRequiredComponents ?? (_ => true);
|
|
_incrementBusy = incrementBusy ?? (() => { });
|
|
_canSend = canSend ?? (() => true);
|
|
}
|
|
|
|
public uint? LastRequestedSpellId { get; private set; }
|
|
public uint? LastRequestedTargetId { get; private set; }
|
|
|
|
public bool IsTargetReady(uint spellId)
|
|
{
|
|
if (!_spellbook.Knows(spellId)
|
|
|| !_spellbook.TryGetMetadata(spellId, out SpellMetadata spell))
|
|
return false;
|
|
if (spell.IsSelfTargeted || spell.IsUntargeted || spell.TargetMask == 0u)
|
|
return true;
|
|
return _selectedObject() is uint target and not 0u
|
|
&& _targetCompatibleSilent(target, spell);
|
|
}
|
|
|
|
public CastRequestResult Cast(uint spellId)
|
|
{
|
|
if (!_spellbook.Knows(spellId) || !_spellbook.TryGetMetadata(spellId, out SpellMetadata spell))
|
|
{
|
|
_displayMessage("You do not know that spell.");
|
|
return CastRequestResult.UnknownSpell;
|
|
}
|
|
|
|
if (!_hasRequiredComponents(spellId))
|
|
{
|
|
_displayMessage("You do not have all of this spell's components.");
|
|
return CastRequestResult.MissingComponents;
|
|
}
|
|
|
|
uint? target;
|
|
bool untargeted;
|
|
if (spell.IsSelfTargeted)
|
|
{
|
|
uint playerId = _localPlayerId();
|
|
target = playerId == 0 ? null : playerId;
|
|
untargeted = target is null;
|
|
}
|
|
else if (spell.IsUntargeted || spell.TargetMask == 0)
|
|
{
|
|
target = null;
|
|
untargeted = true;
|
|
}
|
|
else
|
|
{
|
|
target = _selectedObject();
|
|
untargeted = false;
|
|
if (target is null or 0)
|
|
{
|
|
_displayMessage("You must select a suitable target.");
|
|
return CastRequestResult.NoTarget;
|
|
}
|
|
if (!_targetCompatible(target.Value, spell))
|
|
return CastRequestResult.IncompatibleTarget;
|
|
}
|
|
|
|
if (!_canSend())
|
|
{
|
|
_displayMessage("You cannot cast a spell right now.");
|
|
return CastRequestResult.Unavailable;
|
|
}
|
|
|
|
try
|
|
{
|
|
_stopCompletely();
|
|
LastRequestedSpellId = spellId;
|
|
LastRequestedTargetId = target;
|
|
if (untargeted)
|
|
_sendUntargeted(spellId);
|
|
else
|
|
_sendTargeted(target!.Value, spellId);
|
|
// FreeHandsAndCastSpell @ 0x00566EF0 increments the shared UI
|
|
// busy reference only after Event_Cast has been emitted. The
|
|
// matching server UseDone event decrements it.
|
|
_incrementBusy();
|
|
}
|
|
catch
|
|
{
|
|
LastRequestedSpellId = null;
|
|
LastRequestedTargetId = null;
|
|
throw;
|
|
}
|
|
return CastRequestResult.Sent;
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
LastRequestedSpellId = null;
|
|
LastRequestedTargetId = null;
|
|
}
|
|
}
|
|
|
|
public enum CastRequestResult
|
|
{
|
|
Sent,
|
|
UnknownSpell,
|
|
NoTarget,
|
|
IncompatibleTarget,
|
|
MissingComponents,
|
|
Unavailable,
|
|
}
|