acdream/tests/AcDream.App.Tests/Combat/CombatAttackControllerTests.cs
Erik 2215c76c7e feat(combat): port retail basic combat bar
Mount authored gmCombatUI, share one press/hold/release request state machine across DAT buttons and keybindings, and recover the exact 1.0s/0.8s power timing from matching retail x86. The same timer fixes jump charge, while ready-stance, response queueing, auto-repeat, layout binding, migration, and conformance coverage keep behavior architectural rather than panel-local.

Co-Authored-By: Codex <codex@openai.com>
2026-07-11 14:03:28 +02:00

147 lines
4.4 KiB
C#

using AcDream.App.Combat;
using AcDream.Core.Combat;
using AcDream.UI.Abstractions.Input;
namespace AcDream.App.Tests.Combat;
public sealed class CombatAttackControllerTests
{
[Fact]
public void NormalStance_ChargesLinearlyOverOneSecond_AndReleaseSendsCurrentPower()
{
double now = 10d;
var sent = new List<(AttackHeight Height, float Power)>();
var combat = new CombatState();
using var controller = Create(combat, () => now, sent);
combat.SetCombatMode(CombatMode.Melee);
controller.PressAttack(AttackHeight.High);
now = 10.5d;
Assert.Equal(0.5f, controller.PowerBarLevel, 3);
controller.ReleaseAttack();
var attack = Assert.Single(sent);
Assert.Equal(AttackHeight.High, attack.Height);
Assert.Equal(0.5f, attack.Power, 3);
}
[Fact]
public void DualWieldStance_UsesRetailPointEightSecondPowerUpTime()
{
double now = 3d;
var combat = new CombatState();
using var controller = Create(
combat,
() => now,
[],
isDualWield: () => true);
combat.SetCombatMode(CombatMode.Melee);
controller.PressAttack(AttackHeight.Medium);
now = 3.4d;
Assert.Equal(0.5f, controller.PowerBarLevel, 3);
}
[Fact]
public void EarlyRelease_CommitsOnUseTimeTickAtReleasedPower()
{
double now = 0d;
var sent = new List<(AttackHeight Height, float Power)>();
var combat = new CombatState();
using var controller = Create(combat, () => now, sent);
combat.SetCombatMode(CombatMode.Missile);
controller.SetDesiredPower(1f);
controller.PressAttack(AttackHeight.Low);
now = 0.25d;
controller.ReleaseAttack();
Assert.Empty(sent);
now = 0.26d;
controller.Tick();
var attack = Assert.Single(sent);
Assert.Equal(AttackHeight.Low, attack.Height);
Assert.Equal(0.25f, attack.Power, 3);
}
[Fact]
public void HoldBinding_UsesPressAndReleaseTransitions()
{
double now = 1d;
var sent = new List<(AttackHeight Height, float Power)>();
var combat = new CombatState();
using var controller = Create(combat, () => now, sent);
combat.SetCombatMode(CombatMode.Melee);
Assert.True(controller.HandleInputAction(
InputAction.CombatLowAttack, ActivationType.Press));
now = 1.5d;
Assert.True(controller.HandleInputAction(
InputAction.CombatLowAttack, ActivationType.Release));
Assert.Equal(AttackHeight.Low, Assert.Single(sent).Height);
}
[Fact]
public void LeavingTargetedCombat_CancelsAnActiveBuild()
{
double now = 0d;
var combat = new CombatState();
using var controller = Create(combat, () => now, []);
combat.SetCombatMode(CombatMode.Melee);
controller.PressAttack(AttackHeight.Medium);
now = 0.4d;
combat.SetCombatMode(CombatMode.NonCombat);
Assert.False(controller.BuildInProgress);
Assert.False(controller.AttackRequestInProgress);
Assert.Equal(0f, controller.PowerBarLevel);
}
[Fact]
public void RequestWaitsUntilPlayerReachesReadyStanceBeforeBuilding()
{
double now = 0d;
bool ready = false;
var combat = new CombatState();
using var controller = new CombatAttackController(
combat,
() => true,
(_, _) => true,
playerReadyForAttack: () => ready,
now: () => now);
combat.SetCombatMode(CombatMode.Missile);
controller.PressAttack(AttackHeight.High);
Assert.True(controller.AttackRequestInProgress);
Assert.False(controller.BuildInProgress);
ready = true;
now = 5d;
controller.Tick();
Assert.True(controller.BuildInProgress);
Assert.Equal(0f, controller.PowerBarLevel);
}
private static CombatAttackController Create(
CombatState combat,
Func<double> now,
List<(AttackHeight Height, float Power)> sent,
Func<bool>? isDualWield = null)
=> new(
combat,
canStartAttack: () => true,
sendAttack: (height, power) =>
{
sent.Add((height, power));
return true;
},
isDualWield: isDualWield,
autoRepeatAttack: () => false,
now: now);
}