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 noMotionCommand. - Retail command-name table around
0x00803F34: combat commands includeTwitch1..4,StaggerBackward,StaggerForward,ThrustMed,SlashHigh,Shoot,AttackHigh1, and later offhand/multistrike commands. - ACE
Player_Melee.DoSwingMotionandGetSwingAnimation: server chooses a swing fromCombatManeuverTable.GetMotion(...)and broadcasts the selectedMotionCommandwithUpdateMotion. - 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_idstable 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.