Move fly/chase publication, combat target tracking, and local player projection behind typed runtime owners. Preserve the inbound-created projection/reconcile barrier while removing GameWindow callbacks and duplicate shadow helpers.
152 lines
4.6 KiB
C#
152 lines
4.6 KiB
C#
using AcDream.App.Combat;
|
|
using AcDream.App.Input;
|
|
using AcDream.App.Net;
|
|
using AcDream.Core.Combat;
|
|
using AcDream.Core.Net;
|
|
|
|
namespace AcDream.App.Tests.Combat;
|
|
|
|
public sealed class LiveCombatAttackOperationsTests
|
|
{
|
|
[Fact]
|
|
public void UnboundSlotFailsClosedAndRejectsASecondOwner()
|
|
{
|
|
var slot = new CombatAttackOperationsSlot();
|
|
var first = new FakeOperations();
|
|
var second = new FakeOperations();
|
|
|
|
Assert.False(slot.CanStartAttack());
|
|
Assert.False(slot.SendAttack(AttackHeight.Medium, 0.5f));
|
|
Assert.False(slot.PlayerReadyForAttack);
|
|
|
|
slot.Bind(first);
|
|
slot.Bind(first);
|
|
Assert.Throws<InvalidOperationException>(() => slot.Bind(second));
|
|
}
|
|
|
|
[Fact]
|
|
public void CanStartRequiresLiveWorldBeforeTargetResolution()
|
|
{
|
|
Harness harness = CreateHarness();
|
|
harness.Targets.Acquired = 0x1234u;
|
|
|
|
Assert.False(harness.Owner.CanStartAttack());
|
|
|
|
Assert.Equal(0, harness.Targets.ResolveCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void UnsupportedCombatModeUsesTypedFeedbackSink()
|
|
{
|
|
Harness harness = CreateHarness(inWorld: true);
|
|
|
|
Assert.False(harness.Owner.CanStartAttack());
|
|
|
|
Assert.Equal(["Enter melee or missile combat first"], harness.Feedback.Messages);
|
|
Assert.Equal(0, harness.Targets.ResolveCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void SupportedModeResolvesTargetUsingLiveAutoTargetSetting()
|
|
{
|
|
Harness harness = CreateHarness(inWorld: true);
|
|
harness.Combat.SetCombatMode(CombatMode.Melee);
|
|
harness.Settings.AutoTarget = true;
|
|
harness.Targets.Acquired = 0x1234u;
|
|
harness.Targets.SelectedObjectId = 0x1234u;
|
|
|
|
Assert.True(harness.Owner.CanStartAttack());
|
|
|
|
Assert.True(harness.Targets.LastAutoTarget);
|
|
Assert.Equal(1, harness.Targets.ResolveCount);
|
|
Assert.Empty(harness.Feedback.Messages);
|
|
}
|
|
|
|
[Fact]
|
|
public void MissingTargetPreservesRetailUserFeedback()
|
|
{
|
|
Harness harness = CreateHarness(inWorld: true);
|
|
harness.Combat.SetCombatMode(CombatMode.Missile);
|
|
|
|
Assert.False(harness.Owner.CanStartAttack());
|
|
|
|
Assert.Equal(["No monster target"], harness.Feedback.Messages);
|
|
}
|
|
|
|
private static Harness CreateHarness(bool inWorld = false)
|
|
{
|
|
var combat = new CombatState();
|
|
var targets = new FakeTargets();
|
|
var settings = new FakeSettings();
|
|
var player = new LocalPlayerControllerSlot();
|
|
var live = new FakeLiveSource { IsInWorld = inWorld };
|
|
var feedback = new FakeFeedback();
|
|
var outbound = new LocalPlayerOutboundController(
|
|
static (_, _, _, _, _, _) => { });
|
|
var owner = new LiveCombatAttackOperations(
|
|
combat,
|
|
targets,
|
|
settings,
|
|
player,
|
|
outbound,
|
|
live,
|
|
live,
|
|
feedback);
|
|
return new Harness(owner, combat, targets, settings, feedback);
|
|
}
|
|
|
|
private sealed record Harness(
|
|
LiveCombatAttackOperations Owner,
|
|
CombatState Combat,
|
|
FakeTargets Targets,
|
|
FakeSettings Settings,
|
|
FakeFeedback Feedback);
|
|
|
|
private sealed class FakeTargets : ICombatAttackTargetSource
|
|
{
|
|
public uint? SelectedObjectId { get; set; }
|
|
public uint? Acquired { get; set; }
|
|
public int ResolveCount { get; private set; }
|
|
public bool LastAutoTarget { get; private set; }
|
|
public uint? GetSelectedOrClosestCombatTarget(bool autoTarget)
|
|
{
|
|
ResolveCount++;
|
|
LastAutoTarget = autoTarget;
|
|
return Acquired;
|
|
}
|
|
}
|
|
|
|
private sealed class FakeSettings : ICombatGameplaySettingsSource
|
|
{
|
|
public bool AutoTarget { get; set; }
|
|
public bool AutoRepeatAttack { get; set; }
|
|
public bool ViewCombatTarget { get; set; }
|
|
}
|
|
|
|
private sealed class FakeLiveSource : ILiveInWorldSource, ILiveWorldSessionSource
|
|
{
|
|
public bool IsInWorld { get; set; }
|
|
public WorldSession? CurrentSession => null;
|
|
}
|
|
|
|
private sealed class FakeFeedback : ICombatFeedbackSink
|
|
{
|
|
public List<string> Messages { get; } = [];
|
|
public void Show(string message) => Messages.Add(message);
|
|
}
|
|
|
|
private sealed class FakeOperations : ICombatAttackOperations
|
|
{
|
|
public bool CanStartAttack() => true;
|
|
public void PrepareAttackRequest()
|
|
{
|
|
}
|
|
public bool SendAttack(AttackHeight height, float power) => true;
|
|
public void SendCancelAttack()
|
|
{
|
|
}
|
|
public bool IsDualWield => true;
|
|
public bool PlayerReadyForAttack => true;
|
|
public bool AutoRepeatAttack => true;
|
|
}
|
|
}
|