using AcDream.Core.Selection; using AcDream.Core.Spells; using AcDream.Runtime.Gameplay; namespace AcDream.Runtime.Tests.Gameplay; public sealed class RuntimeSpellCastStateTests { [Fact] public void Cast_SelfTargeted_SendsLocalPlayerAsTarget() { Spellbook book = MakeBook(flags: 0x8, untargeted: true, targetMask: 0x10); uint target = 0; var operations = new FakeOperations { LocalPlayerId = 42u, SendTargetedAction = (id, _) => target = id, }; RuntimeSpellCastState state = Create(book, operations, selected: 99u); Assert.Equal(CastRequestResult.Sent, state.Cast(1)); Assert.Equal(42u, target); } [Fact] public void Cast_TargetedWithoutSelection_DoesNotSend() { Spellbook book = MakeBook(flags: 0, untargeted: false, targetMask: 0x10); var operations = new FakeOperations { LocalPlayerId = 42u }; RuntimeSpellCastState state = Create(book, operations); Assert.Equal(CastRequestResult.NoTarget, state.Cast(1)); Assert.Equal(0, operations.TargetedSends); Assert.Contains( "select", operations.LastMessage, StringComparison.OrdinalIgnoreCase); } [Fact] public void Cast_Untargeted_StopsThenSendsThenIncrementsBusy() { Spellbook book = MakeBook(flags: 0, untargeted: true, targetMask: 0); var order = new List(); var operations = new FakeOperations { LocalPlayerId = 42u, StopAction = () => order.Add("stop"), SendUntargetedAction = _ => order.Add("send"), IncrementBusyAction = () => order.Add("busy"), }; RuntimeSpellCastState state = Create(book, operations); Assert.Equal(CastRequestResult.Sent, state.Cast(1)); Assert.Equal(["stop", "send", "busy"], order); } [Fact] public void Cast_MissingComponents_DoesNotIncrementBusyOrSend() { Spellbook book = MakeBook(flags: 0, untargeted: true, targetMask: 0); var operations = new FakeOperations { LocalPlayerId = 42u, HasComponents = false, }; RuntimeSpellCastState state = Create(book, operations); Assert.Equal(CastRequestResult.MissingComponents, state.Cast(1)); Assert.Equal(0, operations.UntargetedSends); Assert.Equal(0, operations.BusyIncrements); } [Fact] public void Cast_Disconnected_DoesNotIncrementBusy() { Spellbook book = MakeBook(flags: 0, untargeted: true, targetMask: 0); var operations = new FakeOperations { LocalPlayerId = 42u, CanSend = false, }; RuntimeSpellCastState state = Create(book, operations); Assert.Equal(CastRequestResult.Unavailable, state.Cast(1)); Assert.Equal(0, operations.BusyIncrements); } [Fact] public void Cast_SendThrows_DoesNotIncrementBusyAndRethrows() { Spellbook book = MakeBook(flags: 0, untargeted: true, targetMask: 0); var operations = new FakeOperations { LocalPlayerId = 42u, SendUntargetedAction = _ => throw new InvalidOperationException("wire"), }; RuntimeSpellCastState state = Create(book, operations); Assert.Throws(() => state.Cast(1)); Assert.Equal(0, operations.BusyIncrements); Assert.Null(state.LastRequestedSpellId); } private static RuntimeSpellCastState Create( Spellbook book, FakeOperations operations, uint? selected = null) { var selection = new SelectionState(); if (selected is uint id) selection.Select(id, SelectionChangeSource.System); return new RuntimeSpellCastState(book, selection, operations); } private static Spellbook MakeBook(uint flags, bool untargeted, uint targetMask) { const string header = "Spell ID,Name,Flags [Hex],IsUntargetted,TargetMask [Hex]"; string row = $"1,Test,0x{flags:X},{untargeted},0x{targetMask:X}"; SpellTable table = SpellTable.LoadFromReader( new StringReader($"{header}\n{row}")); var book = new Spellbook(table); book.OnSpellLearned(1); return book; } private sealed class FakeOperations : IRuntimeSpellCastOperations { public uint LocalPlayerId { get; init; } public bool CanSend { get; init; } = true; public bool HasComponents { get; init; } = true; public bool TargetCompatible { get; init; } = true; public int UntargetedSends { get; private set; } public int TargetedSends { get; private set; } public int BusyIncrements { get; private set; } public string LastMessage { get; private set; } = string.Empty; public Action? StopAction { get; init; } public Action? SendUntargetedAction { get; init; } public Action? SendTargetedAction { get; init; } public Action? IncrementBusyAction { get; init; } public bool HasRequiredComponents(uint spellId) => HasComponents; public bool IsTargetCompatible( uint targetId, SpellMetadata spell, bool showMessage) => TargetCompatible; public void StopCompletely() => StopAction?.Invoke(); public void SendUntargeted(uint spellId) { UntargetedSends++; SendUntargetedAction?.Invoke(spellId); } public void SendTargeted(uint targetId, uint spellId) { TargetedSends++; SendTargetedAction?.Invoke(targetId, spellId); } public void DisplayMessage(string message) => LastMessage = message; public void IncrementBusy() { BusyIncrements++; IncrementBusyAction?.Invoke(); } } }