113 lines
3.9 KiB
Markdown
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.
|