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

113 lines
3.9 KiB
Markdown

# 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
```text
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
```text
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.