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

@ -0,0 +1,89 @@
using DatReaderWriter.DBObjs;
using DatMotionCommand = DatReaderWriter.Enums.MotionCommand;
using DatMotionStance = DatReaderWriter.Enums.MotionStance;
using DatAttackHeight = DatReaderWriter.Enums.AttackHeight;
using DatAttackType = DatReaderWriter.Enums.AttackType;
namespace AcDream.Core.Combat;
/// <summary>
/// Selects combat swing motions from the retail <c>CombatTable</c> DBObj.
///
/// Retail evidence:
/// - <c>CombatManeuverTable::Get</c> (0x0056AB60) loads DB type
/// <c>0x1000000D</c> for a 0x30xxxxxx combat table id.
/// - ACE <c>CombatManeuverTable.GetMotion</c> indexes maneuvers by
/// stance, attack height, and attack type, returning all matching motions.
/// - ACE <c>Player_Melee.GetSwingAnimation</c> then chooses
/// <c>motions[1]</c> when more than one motion exists and power is below
/// the subdivision threshold; otherwise it uses <c>motions[0]</c>.
/// </summary>
public static class CombatManeuverSelector
{
public const float DefaultSubdivision = 0.33f;
public const float ThrustSlashSubdivision = 0.66f;
public static CombatManeuverSelection SelectMotion(
CombatTable table,
DatMotionStance stance,
DatAttackHeight attackHeight,
DatAttackType attackType,
float powerLevel,
bool isThrustSlashWeapon = false)
{
var motions = FindMotions(table, stance, attackHeight, attackType);
if (motions.Count == 0)
return CombatManeuverSelection.None;
float subdivision = isThrustSlashWeapon
? ThrustSlashSubdivision
: DefaultSubdivision;
var motion = motions.Count > 1 && powerLevel < subdivision
? motions[1]
: motions[0];
return new CombatManeuverSelection(
Found: true,
Motion: motion,
Candidates: motions,
EffectiveAttackType: attackType,
Subdivision: subdivision);
}
public static IReadOnlyList<DatMotionCommand> FindMotions(
CombatTable table,
DatMotionStance stance,
DatAttackHeight attackHeight,
DatAttackType attackType)
{
var result = new List<DatMotionCommand>();
foreach (var maneuver in table.CombatManeuvers)
{
if (maneuver.Style == stance
&& maneuver.AttackHeight == attackHeight
&& maneuver.AttackType == attackType)
{
result.Add(maneuver.Motion);
}
}
return result;
}
}
public readonly record struct CombatManeuverSelection(
bool Found,
DatMotionCommand Motion,
IReadOnlyList<DatMotionCommand> Candidates,
DatAttackType EffectiveAttackType,
float Subdivision)
{
public static CombatManeuverSelection None { get; } = new(
Found: false,
Motion: DatMotionCommand.Invalid,
Candidates: Array.Empty<DatMotionCommand>(),
EffectiveAttackType: DatAttackType.Undef,
Subdivision: 0f);
}