acdream/docs/research/2026-04-28-combat-animation-planner.md

3.9 KiB

Combat Animation Planner Pseudocode

Sources

  • Retail ClientCombatSystem::ExecuteAttack (0x0056BB70): sends targeted melee or missile attack intent and records pending response state. It does not choose a local swing animation.
  • Retail ClientCombatSystem::HandleCommenceAttackEvent (0x0056AD20): starts/updates power-bar and busy UI state. The event carries no MotionCommand.
  • Retail command-name table around 0x00803F34: combat commands include Twitch1..4, StaggerBackward, StaggerForward, ThrustMed, SlashHigh, Shoot, AttackHigh1, and later offhand/multistrike commands.
  • ACE Player_Melee.DoSwingMotion and GetSwingAnimation: server chooses a swing from CombatManeuverTable.GetMotion(...) and broadcasts the selected MotionCommand with UpdateMotion.
  • ACE CombatManeuverTable.GetMotion: indexes (stance, attack height, attack type) to one or more motion commands; power level chooses between multiple entries.

Retail Rule

Combat GameEvents are state/UI notifications. Motion state is the animation authority.

Pseudocode

PlanForEvent(event):
    return None

PlanFromWireCommand(wireCommand, speed):
    fullCommand = MotionCommandResolver.ReconstructFullCommand(wireCommand)
    return PlanFromFullCommand(fullCommand, speed)

PlanFromFullCommand(fullCommand, speed):
    kind = ClassifyMotionCommand(fullCommand)
    if kind is None:
        return None

    routeKind = AnimationCommandRouter.Classify(fullCommand)
    return Plan(kind, routeKind, fullCommand, speed)

ClassifyMotionCommand(fullCommand):
    if command is a combat stance:
        return CombatStance
    if command is a thrust/slash/backhand/offhand/multistrike motion:
        return MeleeSwing
    if command is Shoot, MissileAttack*, or Reload:
        return MissileAttack
    if command is AttackHigh/Med/Low 1..6:
        return CreatureAttack
    if command is CastSpell, UseMagicStaff, or UseMagicWand:
        return SpellCast
    if command is Twitch*, Stagger*, FallDown, or Sanctuary:
        return HitReaction
    if command is Dead:
        return Death
    return None

Maneuver Selection Pseudocode

SelectMotion(table, stance, attackHeight, attackType, powerLevel,
             isThrustSlashWeapon):
    candidates = []
    for maneuver in table.CombatManeuvers:
        if maneuver.Style == stance
           and maneuver.AttackHeight == attackHeight
           and maneuver.AttackType == attackType:
            candidates.append(maneuver.Motion)

    if candidates is empty:
        return None

    subdivision = isThrustSlashWeapon ? 0.66 : 0.33

    if candidates.Count > 1 and powerLevel < subdivision:
        motion = candidates[1]
    else:
        motion = candidates[0]

    return motion

This matches ACE CombatManeuverTable.GetMotion plus Player_Melee.GetSwingAnimation. The prevMotion parameter is present in ACE's table API but the current ACE implementation does not use it; the power threshold chooses between multiple entries.

Named Retail Motion IDs

DatReaderWriter.Enums.MotionCommand is shifted by three entries starting at AllegianceHometownRecall. Named retail command tables are:

  • command_ids table lines 1017626-1017658: 0x016E..0x0197 -> 0x1000016E..0x10000197.
  • command-name table lines 1068272-1068313: OffhandSlashHigh = 0x10000170, AttackLow6 = 0x1000018B, PunchFastLow = 0x1000018E, etc.

MotionCommandResolver therefore overrides that range after building the DRW reflection table, otherwise offhand and late unarmed attack actions resolve as UI/mappable commands and never reach PlayAction.

Implementation Note

The next table-driven layer can use DatReaderWriter.DBObjs.CombatTable and DatReaderWriter.Types.CombatManeuver directly. acdream already references Chorizite.DatReaderWriter; the missing live-state piece is a named CombatTable data-id on player/creature state.