fix(animation): keep combat weapons in sync

This commit is contained in:
Kelsi
2026-07-16 21:47:40 -07:00
parent e68d7adbb6
commit 71f4fc4036
3 changed files with 66 additions and 3 deletions

View File

@@ -1340,7 +1340,27 @@ void Application::update(float deltaTime) {
updateCheckpoint = "in_game: auto-unsheathe";
if (gameHandler) {
const bool autoAttacking = gameHandler->isAutoAttacking();
if (autoAttacking && !wasAutoAttacking_ && appearanceComposer_ && appearanceComposer_->isWeaponsSheathed()) {
// Keep the attachment state consistent with the ongoing attack, not
// just the initial false -> true transition. Z can be pressed after
// combat has already started, and pre-WotLK servers briefly send
// ATTACKSTOP while the client retains attack intent for a retry.
const bool attackWeaponNeeded = autoAttacking || gameHandler->hasAutoAttackIntent();
const auto& inventory = gameHandler->getInventory();
const auto& mainHand = inventory.getEquipSlot(game::EquipSlot::MAIN_HAND);
const auto& offHand = inventory.getEquipSlot(game::EquipSlot::OFF_HAND);
const auto& ranged = inventory.getEquipSlot(game::EquipSlot::RANGED);
const bool hasOffHandWeapon = !offHand.empty() &&
offHand.item.inventoryType == game::InvType::ONE_HAND;
const bool hasRangedWeapon = !ranged.empty() &&
(ranged.item.inventoryType == game::InvType::RANGED_BOW ||
ranged.item.inventoryType == game::InvType::RANGED_GUN ||
ranged.item.inventoryType == game::InvType::THROWN);
const bool hasDrawableWeapon = !mainHand.empty() || hasOffHandWeapon || hasRangedWeapon;
if (attackWeaponNeeded && hasDrawableWeapon && appearanceComposer_ &&
appearanceComposer_->isWeaponsSheathed()) {
if (renderer && renderer->getAnimationController()) {
renderer->getAnimationController()->playWeaponSheathAnimation(false);
}
appearanceComposer_->setWeaponsSheathed(false);
appearanceComposer_->loadEquippedWeapons();
}

View File

@@ -147,8 +147,14 @@ AnimOutput CharacterAnimator::resolveAnimation() {
combatIn.currentAnimTime = fi.currentAnimTime;
combatIn.currentAnimDuration = fi.currentAnimDuration;
combatIn.haveAnimState = fi.haveAnimState;
combatIn.hasUnsheathe = caps_.resolvedUnsheathe != 0;
combatIn.hasSheathe = caps_.resolvedSheathe != 0;
// A character model supporting draw/sheath animations does not mean the
// character currently has something to draw. Empty-handed combat must go
// directly to READY_UNARMED / ATTACK_UNARMED. Equipped fist weapons still
// have inventoryType ONE_HAND and retain their dedicated fist animation path.
const bool hasDrawableWeapon = loadout_.inventoryType != game::InvType::NON_EQUIP ||
(rangedWeaponActive_ && loadout_.rangedType != RangedWeaponType::NONE);
combatIn.hasUnsheathe = hasDrawableWeapon && caps_.resolvedUnsheathe != 0;
combatIn.hasSheathe = hasDrawableWeapon && caps_.resolvedSheathe != 0;
// ── Combat FSM (highest priority for non-mount) ─────────────────────
auto combatOut = combat_.resolve(combatIn, caps_, loadout_);

View File

@@ -14,6 +14,7 @@ static AnimCapabilitySet makeCombatCaps() {
caps.resolvedMelee1H = anim::ATTACK_1H;
caps.resolvedMelee2H = anim::ATTACK_2H;
caps.resolvedMeleeUnarmed = anim::ATTACK_UNARMED;
caps.resolvedReadyUnarmed = anim::READY_UNARMED;
caps.resolvedStun = anim::STUN;
caps.resolvedUnsheathe = anim::UNSHEATHE;
caps.resolvedSheathe = anim::SHEATHE;
@@ -38,6 +39,42 @@ TEST_CASE("CombatFSM: INACTIVE by default", "[combat]") {
CHECK_FALSE(fsm.isActive());
}
TEST_CASE("CombatFSM: unarmed combat uses fist-ready and fist-swing animations", "[combat][unarmed]") {
CombatFSM fsm;
auto caps = makeCombatCaps();
auto loadout = unarmedLoadout();
auto in = combatInput();
in.hasUnsheathe = false;
auto ready = fsm.resolve(in, caps, loadout);
REQUIRE(ready.valid);
CHECK(fsm.getState() == CombatFSM::State::COMBAT_IDLE);
CHECK(ready.animId == anim::READY_UNARMED);
in.meleeSwingTimer = 0.5f;
auto swing = fsm.resolve(in, caps, loadout);
REQUIRE(swing.valid);
CHECK(fsm.getState() == CombatFSM::State::MELEE_SWING);
CHECK(swing.animId == anim::ATTACK_UNARMED);
}
TEST_CASE("CombatFSM: equipped fist weapons retain fist-weapon animations", "[combat][fist-weapon]") {
CombatFSM fsm;
auto caps = makeCombatCaps();
caps.resolvedReadyFist = anim::READY_FIST;
caps.resolvedMeleeFist = anim::ATTACK_FIST_1H;
WeaponLoadout loadout;
loadout.inventoryType = wowee::game::InvType::ONE_HAND;
loadout.isFist = true;
auto in = combatInput();
fsm.setState(CombatFSM::State::COMBAT_IDLE);
CHECK(fsm.resolve(in, caps, loadout).animId == anim::READY_FIST);
in.meleeSwingTimer = 0.5f;
CHECK(fsm.resolve(in, caps, loadout).animId == anim::ATTACK_FIST_1H);
}
TEST_CASE("CombatFSM: INACTIVE → UNSHEATHE on COMBAT_ENTER event", "[combat]") {
CombatFSM fsm;
fsm.onEvent(AnimEvent::COMBAT_ENTER);