using System; using AcDream.Core.Spells; namespace AcDream.App.Spells; /// /// 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. /// /// /// Port of ClientMagicSystem::CastSpell (0x00568040) and /// FreeHandsAndCastSpell (0x00566EF0). This controller deliberately /// does not consume mana/components or start local effects. /// public sealed class SpellCastingController { private readonly Spellbook _spellbook; private readonly Func _selectedObject; private readonly Func _localPlayerId; private readonly Action _stopCompletely; private readonly Action _sendUntargeted; private readonly Action _sendTargeted; private readonly Action _displayMessage; private readonly Func _targetCompatible; private readonly Func _targetCompatibleSilent; private readonly Func _hasRequiredComponents; private readonly Action _incrementBusy; private readonly Func _canSend; public SpellCastingController( Spellbook spellbook, Func selectedObject, Func localPlayerId, Action stopCompletely, Action sendUntargeted, Action sendTargeted, Action displayMessage, Func? targetCompatible = null, Func? hasRequiredComponents = null, Action? incrementBusy = null, Func? canSend = null, Func? 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, }