acdream/tests/AcDream.App.Tests/Combat/LiveCombatAttackOperationsTests.cs
Erik 7771c07fb6 refactor(app): compose session and player startup
Move streaming, live-session, hydration, local-player, combat, and teleport construction behind the typed Phase-7 boundary. Add exact-owner runtime bindings and focused spawn-claim classification so partial startup rolls back without retaining old session targets while preserving the accepted construction and frame dependencies.

Co-authored-by: Codex <codex@openai.com>
2026-07-22 18:28:32 +02:00

170 lines
5.3 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 OwnedBindingReleasesOnlyItsExactOwnerAndAllowsRebind()
{
var slot = new CombatAttackOperationsSlot();
var first = new FakeOperations { CanStartValue = false };
var second = new FakeOperations { CanStartValue = true };
IDisposable firstBinding = slot.BindOwned(first);
Assert.Throws<InvalidOperationException>(() => slot.BindOwned(second));
firstBinding.Dispose();
using IDisposable secondBinding = slot.BindOwned(second);
firstBinding.Dispose();
Assert.True(slot.CanStartAttack());
}
[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 CanStartValue { get; init; } = true;
public bool CanStartAttack() => CanStartValue;
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;
}
}