feat(anim): Phase L.1c select combat maneuvers

This commit is contained in:
Erik 2026-04-28 11:44:17 +02:00
parent 831392a7b2
commit 646246ba84
6 changed files with 385 additions and 33 deletions

View file

@ -9,17 +9,20 @@ public sealed class CombatAnimationPlannerTests
[Theory]
[InlineData(0x10000058u, CombatAnimationKind.MeleeSwing)] // ThrustMed
[InlineData(0x1000005Bu, CombatAnimationKind.MeleeSwing)] // SlashHigh
[InlineData(0x10000180u, CombatAnimationKind.MeleeSwing)] // OffhandDoubleThrustMed
[InlineData(0x1000017Du, CombatAnimationKind.MeleeSwing)] // OffhandDoubleThrustMed
[InlineData(0x1000018Eu, CombatAnimationKind.MeleeSwing)] // PunchFastLow
[InlineData(0x10000061u, CombatAnimationKind.MissileAttack)] // Shoot
[InlineData(0x100000D4u, CombatAnimationKind.MissileAttack)] // Reload
[InlineData(0x10000062u, CombatAnimationKind.CreatureAttack)] // AttackHigh1
[InlineData(0x1000018Eu, CombatAnimationKind.CreatureAttack)] // AttackLow6
[InlineData(0x1000018Bu, CombatAnimationKind.CreatureAttack)] // AttackLow6
[InlineData(0x400000D3u, CombatAnimationKind.SpellCast)] // CastSpell
[InlineData(0x400000E0u, CombatAnimationKind.SpellCast)] // UseMagicStaff
[InlineData(0x10000051u, CombatAnimationKind.HitReaction)] // Twitch1
[InlineData(0x10000055u, CombatAnimationKind.HitReaction)] // StaggerBackward
[InlineData(0x40000011u, CombatAnimationKind.Death)] // Dead
[InlineData(0x8000003Eu, CombatAnimationKind.CombatStance)] // SwordCombat
[InlineData(0x80000043u, CombatAnimationKind.CombatStance)] // SlingCombat
[InlineData(0x80000044u, CombatAnimationKind.CombatStance)] // 2HandedSwordCombat
public void ClassifyMotionCommand_RecognisesRetailCombatCommands(
uint command,
CombatAnimationKind expected)
@ -27,6 +30,18 @@ public sealed class CombatAnimationPlannerTests
Assert.Equal(expected, CombatAnimationPlanner.ClassifyMotionCommand(command));
}
[Theory]
[InlineData(0x0170, 0x10000170u)] // OffhandSlashHigh
[InlineData(0x017D, 0x1000017Du)] // OffhandDoubleThrustMed
[InlineData(0x018B, 0x1000018Bu)] // AttackLow6
[InlineData(0x018E, 0x1000018Eu)] // PunchFastLow
public void MotionCommandResolver_UsesNamedRetailLateCombatCommands(
ushort wireCommand,
uint expectedFullCommand)
{
Assert.Equal(expectedFullCommand, MotionCommandResolver.ReconstructFullCommand(wireCommand));
}
[Fact]
public void PlanFromWireCommand_Swing_IsActionOverlay()
{

View file

@ -0,0 +1,155 @@
using AcDream.Core.Combat;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Types;
using DatAttackHeight = DatReaderWriter.Enums.AttackHeight;
using DatAttackType = DatReaderWriter.Enums.AttackType;
using DatMotionCommand = DatReaderWriter.Enums.MotionCommand;
using DatMotionStance = DatReaderWriter.Enums.MotionStance;
using Xunit;
namespace AcDream.Core.Tests.Combat;
public sealed class CombatManeuverSelectorTests
{
[Fact]
public void SelectMotion_UsesFirstEntryAtOrAboveSubdivision()
{
var table = MakeTable(
Entry(DatMotionStance.SwordCombat, DatAttackHeight.Medium,
DatAttackType.Slash, DatMotionCommand.SlashMed),
Entry(DatMotionStance.SwordCombat, DatAttackHeight.Medium,
DatAttackType.Slash, DatMotionCommand.BackhandMed));
var atThreshold = CombatManeuverSelector.SelectMotion(
table,
DatMotionStance.SwordCombat,
DatAttackHeight.Medium,
DatAttackType.Slash,
powerLevel: CombatManeuverSelector.DefaultSubdivision);
var highPower = CombatManeuverSelector.SelectMotion(
table,
DatMotionStance.SwordCombat,
DatAttackHeight.Medium,
DatAttackType.Slash,
powerLevel: 1f);
Assert.Equal(DatMotionCommand.SlashMed, atThreshold.Motion);
Assert.Equal(DatMotionCommand.SlashMed, highPower.Motion);
}
[Fact]
public void SelectMotion_UsesSecondEntryBelowSubdivision()
{
var table = MakeTable(
Entry(DatMotionStance.SwordCombat, DatAttackHeight.Medium,
DatAttackType.Slash, DatMotionCommand.SlashMed),
Entry(DatMotionStance.SwordCombat, DatAttackHeight.Medium,
DatAttackType.Slash, DatMotionCommand.BackhandMed));
var selection = CombatManeuverSelector.SelectMotion(
table,
DatMotionStance.SwordCombat,
DatAttackHeight.Medium,
DatAttackType.Slash,
powerLevel: 0.2f);
Assert.True(selection.Found);
Assert.Equal(DatMotionCommand.BackhandMed, selection.Motion);
Assert.Equal(DatAttackType.Slash, selection.EffectiveAttackType);
Assert.Equal(2, selection.Candidates.Count);
}
[Fact]
public void SelectMotion_ThrustSlashWeaponUsesTwoThirdsSubdivision()
{
var table = MakeTable(
Entry(DatMotionStance.SwordCombat, DatAttackHeight.High,
DatAttackType.Slash, DatMotionCommand.SlashHigh),
Entry(DatMotionStance.SwordCombat, DatAttackHeight.High,
DatAttackType.Slash, DatMotionCommand.BackhandHigh));
var normal = CombatManeuverSelector.SelectMotion(
table,
DatMotionStance.SwordCombat,
DatAttackHeight.High,
DatAttackType.Slash,
powerLevel: 0.5f);
var thrustSlash = CombatManeuverSelector.SelectMotion(
table,
DatMotionStance.SwordCombat,
DatAttackHeight.High,
DatAttackType.Slash,
powerLevel: 0.5f,
isThrustSlashWeapon: true);
Assert.Equal(DatMotionCommand.SlashHigh, normal.Motion);
Assert.Equal(DatMotionCommand.BackhandHigh, thrustSlash.Motion);
Assert.Equal(CombatManeuverSelector.ThrustSlashSubdivision, thrustSlash.Subdivision);
}
[Fact]
public void SelectMotion_MissingLookupReturnsNone()
{
var table = MakeTable(
Entry(DatMotionStance.BowCombat, DatAttackHeight.High,
DatAttackType.Punch, DatMotionCommand.Shoot));
var selection = CombatManeuverSelector.SelectMotion(
table,
DatMotionStance.SwordCombat,
DatAttackHeight.High,
DatAttackType.Punch,
powerLevel: 0.5f);
Assert.Equal(CombatManeuverSelection.None, selection);
}
[Fact]
public void FindMotions_PreservesRetailTableOrder()
{
var table = MakeTable(
Entry(DatMotionStance.HandCombat, DatAttackHeight.Low,
DatAttackType.Kick, DatMotionCommand.AttackLow1),
Entry(DatMotionStance.HandCombat, DatAttackHeight.Low,
DatAttackType.Kick, (DatMotionCommand)0x1000018Eu),
Entry(DatMotionStance.HandCombat, DatAttackHeight.Low,
DatAttackType.Punch, DatMotionCommand.AttackLow2));
var motions = CombatManeuverSelector.FindMotions(
table,
DatMotionStance.HandCombat,
DatAttackHeight.Low,
DatAttackType.Kick);
Assert.Equal(new[]
{
DatMotionCommand.AttackLow1,
(DatMotionCommand)0x1000018Eu,
}, motions);
}
private static CombatTable MakeTable(params CombatManeuver[] maneuvers)
{
var table = new CombatTable();
table.CombatManeuvers.AddRange(maneuvers);
return table;
}
private static CombatManeuver Entry(
DatMotionStance stance,
DatAttackHeight height,
DatAttackType type,
DatMotionCommand motion)
{
return new CombatManeuver
{
Style = stance,
AttackHeight = height,
AttackType = type,
MinSkillLevel = 0,
Motion = motion,
};
}
}