Move attack build/repeat state, combat-mode policy, authoritative auto-target transitions, and spell-cast intent beneath RuntimeActionState. Keep App as the input, world-query, DAT-policy, transport, and presentation adapter while preserving retail request and busy ordering. Add direct/graphical parity, reset, failure, and instance-isolation coverage. Co-authored-by: Codex <noreply@openai.com>
173 lines
5.8 KiB
C#
173 lines
5.8 KiB
C#
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<string>();
|
|
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<InvalidOperationException>(() => 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<uint>? SendUntargetedAction { get; init; }
|
|
public Action<uint, uint>? 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();
|
|
}
|
|
}
|
|
}
|