1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-06-19 14:41:34 +00:00

Merge branch 'lockandload' into 'master'

Make the character controller less miserable, round 6: attack animations, round 1

See merge request OpenMW/openmw!2252
This commit is contained in:
psi29a 2022-08-08 21:16:53 +00:00
commit 0a16d54865
2 changed files with 186 additions and 218 deletions

View file

@ -307,7 +307,7 @@ void CharacterController::resetCurrentHitState()
void CharacterController::resetCurrentWeaponState() void CharacterController::resetCurrentWeaponState()
{ {
clearStateAnimation(mCurrentWeapon); clearStateAnimation(mCurrentWeapon);
mUpperBodyState = UpperCharState_Nothing; mUpperBodyState = UpperBodyState::None;
} }
void CharacterController::resetCurrentDeathState() void CharacterController::resetCurrentDeathState()
@ -401,15 +401,15 @@ void CharacterController::refreshHitRecoilAnims()
{ {
if (!mCurrentWeapon.empty()) if (!mCurrentWeapon.empty())
mAnimation->disable(mCurrentWeapon); mAnimation->disable(mCurrentWeapon);
if (mUpperBodyState > UpperCharState_WeapEquiped) if (mUpperBodyState > UpperBodyState::WeaponEquipped)
{ {
mUpperBodyState = UpperCharState_WeapEquiped; mUpperBodyState = UpperBodyState::WeaponEquipped;
if (mWeaponType > ESM::Weapon::None) if (mWeaponType > ESM::Weapon::None)
mAnimation->showWeapons(true); mAnimation->showWeapons(true);
} }
else if (mUpperBodyState < UpperCharState_WeapEquiped) else if (mUpperBodyState < UpperBodyState::WeaponEquipped)
{ {
mUpperBodyState = UpperCharState_Nothing; mUpperBodyState = UpperBodyState::None;
} }
} }
@ -680,7 +680,7 @@ void CharacterController::refreshIdleAnims(CharacterState idle, bool force)
{ {
// FIXME: if one of the below states is close to their last animation frame (i.e. will be disabled in the coming update), // FIXME: if one of the below states is close to their last animation frame (i.e. will be disabled in the coming update),
// the idle animation should be displayed // the idle animation should be displayed
if (((mUpperBodyState != UpperCharState_Nothing && mUpperBodyState != UpperCharState_WeapEquiped) if (((mUpperBodyState != UpperBodyState::None && mUpperBodyState != UpperBodyState::WeaponEquipped)
|| mMovementState != CharState_None || mHitState != CharState_None) && !mPtr.getClass().isBipedal(mPtr)) || mMovementState != CharState_None || mHitState != CharState_None) && !mPtr.getClass().isBipedal(mPtr))
{ {
resetCurrentIdleState(); resetCurrentIdleState();
@ -855,7 +855,7 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim
getActiveWeapon(mPtr, &mWeaponType); getActiveWeapon(mPtr, &mWeaponType);
if (mWeaponType != ESM::Weapon::None) if (mWeaponType != ESM::Weapon::None)
{ {
mUpperBodyState = UpperCharState_WeapEquiped; mUpperBodyState = UpperBodyState::WeaponEquipped;
mCurrentWeapon = getWeaponAnimation(mWeaponType); mCurrentWeapon = getWeaponAnimation(mWeaponType);
} }
@ -1071,7 +1071,7 @@ void CharacterController::updatePtr(const MWWorld::Ptr &ptr)
void CharacterController::updateIdleStormState(bool inwater) const void CharacterController::updateIdleStormState(bool inwater) const
{ {
if (!mAnimation->hasAnimation("idlestorm") || mUpperBodyState != UpperCharState_Nothing || inwater) if (!mAnimation->hasAnimation("idlestorm") || mUpperBodyState != UpperBodyState::None || inwater)
{ {
mAnimation->disable("idlestorm"); mAnimation->disable("idlestorm");
return; return;
@ -1113,7 +1113,7 @@ bool CharacterController::updateCarriedLeftVisible(const int weaptype) const
return mAnimation->updateCarriedLeftVisible(weaptype); return mAnimation->updateCarriedLeftVisible(weaptype);
} }
bool CharacterController::updateWeaponState(CharacterState idle) bool CharacterController::updateWeaponState()
{ {
const auto world = MWBase::Environment::get().getWorld(); const auto world = MWBase::Environment::get().getWorld();
auto& prng = world->getPrng(); auto& prng = world->getPrng();
@ -1166,20 +1166,18 @@ bool CharacterController::updateWeaponState(CharacterState idle)
bool forcestateupdate = false; bool forcestateupdate = false;
// We should not play equipping animation and sound during weapon->weapon transition // We should not play equipping animation and sound during weapon->weapon transition
const bool isStillWeapon = weaptype != ESM::Weapon::HandToHand && weaptype != ESM::Weapon::Spell && weaptype != ESM::Weapon::None && const bool isStillWeapon = isRealWeapon(mWeaponType) && isRealWeapon(weaptype);
mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell && mWeaponType != ESM::Weapon::None;
// If the current weapon type was changed in the middle of attack (e.g. by Equip console command or when bound spell expires), // If the current weapon type was changed in the middle of attack (e.g. by Equip console command or when bound spell expires),
// we should force actor to the "weapon equipped" state, interrupt attack and update animations. // we should force actor to the "weapon equipped" state, interrupt attack and update animations.
if (isStillWeapon && mWeaponType != weaptype && mUpperBodyState > UpperCharState_WeapEquiped) if (isStillWeapon && mWeaponType != weaptype && mUpperBodyState > UpperBodyState::WeaponEquipped)
{ {
forcestateupdate = true; forcestateupdate = true;
if (!mCurrentWeapon.empty()) if (!mCurrentWeapon.empty())
mAnimation->disable(mCurrentWeapon); mAnimation->disable(mCurrentWeapon);
mUpperBodyState = UpperCharState_WeapEquiped; mUpperBodyState = UpperBodyState::WeaponEquipped;
setAttackingOrSpell(false); setAttackingOrSpell(false);
mAnimation->showWeapons(true); mAnimation->showWeapons(true);
stats.setAttackingOrSpell(false);
} }
if(!isKnockedOut() && !isKnockedDown() && !isRecovery()) if(!isKnockedOut() && !isKnockedDown() && !isRecovery())
@ -1187,7 +1185,7 @@ bool CharacterController::updateWeaponState(CharacterState idle)
std::string weapgroup; std::string weapgroup;
if ((!isWerewolf || mWeaponType != ESM::Weapon::Spell) if ((!isWerewolf || mWeaponType != ESM::Weapon::Spell)
&& weaptype != mWeaponType && weaptype != mWeaponType
&& mUpperBodyState != UpperCharState_UnEquipingWeap && mUpperBodyState != UpperBodyState::Unequipping
&& !isStillWeapon) && !isStillWeapon)
{ {
// We can not play un-equip animation if weapon changed since last update // We can not play un-equip animation if weapon changed since last update
@ -1209,7 +1207,7 @@ bool CharacterController::updateWeaponState(CharacterState idle)
mAnimation->play(weapgroup, priorityWeapon, unequipMask, false, mAnimation->play(weapgroup, priorityWeapon, unequipMask, false,
1.0f, "unequip start", "unequip stop", 0.0f, 0); 1.0f, "unequip start", "unequip stop", 0.0f, 0);
mUpperBodyState = UpperCharState_UnEquipingWeap; mUpperBodyState = UpperBodyState::Unequipping;
mAnimation->detachArrow(); mAnimation->detachArrow();
@ -1247,7 +1245,8 @@ bool CharacterController::updateWeaponState(CharacterState idle)
if (!isStillWeapon) if (!isStillWeapon)
{ {
clearStateAnimation(mCurrentWeapon); if (animPlaying)
mAnimation->disable(mCurrentWeapon);
if (weaptype != ESM::Weapon::None) if (weaptype != ESM::Weapon::None)
{ {
mAnimation->showWeapons(false); mAnimation->showWeapons(false);
@ -1262,15 +1261,18 @@ bool CharacterController::updateWeaponState(CharacterState idle)
mAnimation->play(weapgroup, priorityWeapon, equipMask, true, mAnimation->play(weapgroup, priorityWeapon, equipMask, true,
1.0f, "equip start", "equip stop", 0.0f, 0); 1.0f, "equip start", "equip stop", 0.0f, 0);
mUpperBodyState = UpperCharState_EquipingWeap; mUpperBodyState = UpperBodyState::Equipping;
// If we do not have the "equip attach" key, show weapon manually. // If we do not have the "equip attach" key, show weapon manually.
if (weaptype != ESM::Weapon::Spell) if (weaptype != ESM::Weapon::Spell && mAnimation->getTextKeyTime(weapgroup+": equip attach") < 0)
{ {
if (mAnimation->getTextKeyTime(weapgroup+": equip attach") < 0)
mAnimation->showWeapons(true); mAnimation->showWeapons(true);
} }
} }
if (!upSoundId.empty())
{
sndMgr->playSound3D(mPtr, upSoundId, 1.0f, 1.0f);
}
} }
if(isWerewolf) if(isWerewolf)
@ -1286,18 +1288,13 @@ bool CharacterController::updateWeaponState(CharacterState idle)
mWeaponType = weaptype; mWeaponType = weaptype;
mCurrentWeapon = weapgroup; mCurrentWeapon = weapgroup;
if(!upSoundId.empty() && !isStillWeapon)
{
sndMgr->playSound3D(mPtr, upSoundId, 1.0f, 1.0f);
}
} }
// Make sure that we disabled unequipping animation // Make sure that we disabled unequipping animation
if (mUpperBodyState == UpperCharState_UnEquipingWeap) if (mUpperBodyState == UpperBodyState::Unequipping)
{ {
resetCurrentWeaponState(); resetCurrentWeaponState();
mWeaponType = ESM::Weapon::None; mWeaponType = ESM::Weapon::None;
mCurrentWeapon = getWeaponAnimation(mWeaponType);
} }
} }
} }
@ -1317,29 +1314,43 @@ bool CharacterController::updateWeaponState(CharacterState idle)
sndMgr->stopSound3D(mPtr, "WolfRun"); sndMgr->stopSound3D(mPtr, "WolfRun");
} }
// Cancel attack if we no longer have ammunition
bool ammunition = true; bool ammunition = true;
bool isWeapon = false;
float weapSpeed = 1.f; float weapSpeed = 1.f;
if (cls.hasInventoryStore(mPtr)) if (cls.hasInventoryStore(mPtr))
{ {
MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr); MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr);
MWWorld::ConstContainerStoreIterator weapon = getActiveWeapon(mPtr, &weaptype); if (stats.getDrawState() == DrawState::Weapon && !mWeapon.isEmpty() && mWeapon.getType() == ESM::Weapon::sRecordId)
isWeapon = (weapon != inv.end() && weapon->getType() == ESM::Weapon::sRecordId);
if (isWeapon)
{ {
weapSpeed = weapon->get<ESM::Weapon>()->mBase->mData.mSpeed; weapSpeed = mWeapon.get<ESM::Weapon>()->mBase->mData.mSpeed;
MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition); MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition);
int ammotype = getWeaponType(weapon->get<ESM::Weapon>()->mBase->mData.mType)->mAmmoType; int ammotype = getWeaponType(mWeapon.get<ESM::Weapon>()->mBase->mData.mType)->mAmmoType;
if (ammotype != ESM::Weapon::None && (ammo == inv.end() || ammo->get<ESM::Weapon>()->mBase->mData.mType != ammotype)) if (ammotype != ESM::Weapon::None)
ammunition = false; ammunition = ammo != inv.end() && ammo->get<ESM::Weapon>()->mBase->mData.mType == ammotype;
} }
if (!ammunition && mUpperBodyState > UpperCharState_WeapEquiped) // Cancel attack if we no longer have ammunition
if (!ammunition)
{
if (mUpperBodyState == UpperBodyState::AttackPreWindUp || mUpperBodyState == UpperBodyState::AttackWindUp)
{ {
if (!mCurrentWeapon.empty())
mAnimation->disable(mCurrentWeapon); mAnimation->disable(mCurrentWeapon);
mUpperBodyState = UpperCharState_WeapEquiped; mUpperBodyState = UpperBodyState::WeaponEquipped;
}
setAttackingOrSpell(false);
}
MWWorld::ConstContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft);
if (torch != inv.end() && torch->getType() == ESM::Light::sRecordId && updateCarriedLeftVisible(mWeaponType))
{
if (mAnimation->isPlaying("shield"))
mAnimation->disable("shield");
mAnimation->play("torch", Priority_Torch, MWRender::Animation::BlendMask_LeftArm,
false, 1.0f, "start", "stop", 0.0f, std::numeric_limits<size_t>::max(), true);
}
else if (mAnimation->isPlaying("torch"))
{
mAnimation->disable("torch");
} }
} }
@ -1352,15 +1363,13 @@ bool CharacterController::updateWeaponState(CharacterState idle)
ESM::WeaponType::Class weapclass = getWeaponType(mWeaponType)->mWeaponClass; ESM::WeaponType::Class weapclass = getWeaponType(mWeaponType)->mWeaponClass;
if(getAttackingOrSpell()) if(getAttackingOrSpell())
{ {
bool resetIdle = ammunition; bool resetIdle = true;
if(mUpperBodyState == UpperCharState_WeapEquiped && (mHitState == CharState_None || mHitState == CharState_Block)) if (mUpperBodyState == UpperBodyState::WeaponEquipped && (mHitState == CharState_None || mHitState == CharState_Block))
{ {
mAttackStrength = 0; mAttackStrength = 0;
// Randomize attacks for non-bipedal creatures // Randomize attacks for non-bipedal creatures
if (cls.getType() == ESM::Creature::sRecordId && if (!cls.isBipedal(mPtr) && (!mAnimation->hasAnimation(mCurrentWeapon) || isRandomAttackAnimation(mCurrentWeapon)))
!cls.isBipedal(mPtr) &&
(!mAnimation->hasAnimation(mCurrentWeapon) || isRandomAttackAnimation(mCurrentWeapon)))
{ {
mCurrentWeapon = chooseRandomAttackAnimation(); mCurrentWeapon = chooseRandomAttackAnimation();
} }
@ -1411,7 +1420,7 @@ bool CharacterController::updateWeaponState(CharacterState idle)
resetIdle = false; resetIdle = false;
// Spellcasting animation needs to "play" for at least one frame to reset the aiming factor // Spellcasting animation needs to "play" for at least one frame to reset the aiming factor
animPlaying = true; animPlaying = true;
mUpperBodyState = UpperCharState_CastingSpell; mUpperBodyState = UpperBodyState::Casting;
} }
// Play the spellcasting animation/VFX if the spellcasting was successful or failed due to insufficient magicka. // Play the spellcasting animation/VFX if the spellcasting was successful or failed due to insufficient magicka.
// Used up powers are exempt from this from some reason. // Used up powers are exempt from this from some reason.
@ -1485,65 +1494,53 @@ bool CharacterController::updateWeaponState(CharacterState idle)
MWRender::Animation::BlendMask_All, true, MWRender::Animation::BlendMask_All, true,
1, startKey, stopKey, 1, startKey, stopKey,
0.0f, 0); 0.0f, 0);
mUpperBodyState = UpperCharState_CastingSpell; mUpperBodyState = UpperBodyState::Casting;
} }
else else
{ {
resetIdle = false; resetIdle = false;
} }
} }
else if(mWeaponType == ESM::Weapon::PickProbe) else
{ {
std::string startKey = "start";
std::string stopKey = "stop";
bool autodisable = false;
if (mWeaponType == ESM::Weapon::PickProbe)
{
autodisable = true;
mUpperBodyState = UpperBodyState::AttackEnd;
world->breakInvisibility(mPtr); world->breakInvisibility(mPtr);
MWWorld::ContainerStoreIterator weapon = cls.getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight);
MWWorld::Ptr item = *weapon;
// TODO: this will only work for the player, and needs to be fixed if NPCs should ever use lockpicks/probes. // TODO: this will only work for the player, and needs to be fixed if NPCs should ever use lockpicks/probes.
MWWorld::Ptr target = world->getFacedObject(); MWWorld::Ptr target = world->getFacedObject();
std::string resultMessage, resultSound; std::string resultMessage, resultSound;
if(!target.isEmpty()) if(!target.isEmpty())
{ {
if(item.getType() == ESM::Lockpick::sRecordId) if (mWeapon.getType() == ESM::Lockpick::sRecordId)
Security(mPtr).pickLock(target, item, resultMessage, resultSound); Security(mPtr).pickLock(target, mWeapon, resultMessage, resultSound);
else if(item.getType() == ESM::Probe::sRecordId) else if (mWeapon.getType() == ESM::Probe::sRecordId)
Security(mPtr).probeTrap(target, item, resultMessage, resultSound); Security(mPtr).probeTrap(target, mWeapon, resultMessage, resultSound);
} }
mAnimation->play(mCurrentWeapon, priorityWeapon,
MWRender::Animation::BlendMask_All, true,
1.0f, "start", "stop", 0.0, 0);
mUpperBodyState = UpperCharState_FollowStartToFollowStop;
if (!resultMessage.empty()) if (!resultMessage.empty())
MWBase::Environment::get().getWindowManager()->messageBox(resultMessage); MWBase::Environment::get().getWindowManager()->messageBox(resultMessage);
if (!resultSound.empty()) if (!resultSound.empty())
sndMgr->playSound3D(target, resultSound, 1.0f, 1.0f); sndMgr->playSound3D(target, resultSound, 1.0f, 1.0f);
} }
else if (ammunition) else if (!isRandomAttackAnimation(mCurrentWeapon))
{ {
std::string startKey;
std::string stopKey;
if (weapclass == ESM::WeaponType::Ranged || weapclass == ESM::WeaponType::Thrown) if (weapclass == ESM::WeaponType::Ranged || weapclass == ESM::WeaponType::Thrown)
{
mAttackType = "shoot"; mAttackType = "shoot";
startKey = mAttackType+" start"; else if (mPtr == getPlayer())
stopKey = mAttackType+" min attack";
}
else if (isRandomAttackAnimation(mCurrentWeapon))
{
startKey = "start";
stopKey = "stop";
}
else
{
if(mPtr == getPlayer())
{ {
if (Settings::Manager::getBool("best attack", "Game")) if (Settings::Manager::getBool("best attack", "Game"))
{ {
if (isWeapon) if (!mWeapon.isEmpty() && mWeapon.getType() == ESM::Weapon::sRecordId)
{ {
MWWorld::ConstContainerStoreIterator weapon = cls.getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight); mAttackType = getBestAttack(mWeapon.get<ESM::Weapon>()->mBase);
mAttackType = getBestAttack(weapon->get<ESM::Weapon>()->mBase);
} }
else else
{ {
@ -1562,12 +1559,12 @@ bool CharacterController::updateWeaponState(CharacterState idle)
} }
mAnimation->play(mCurrentWeapon, priorityWeapon, mAnimation->play(mCurrentWeapon, priorityWeapon,
MWRender::Animation::BlendMask_All, false, MWRender::Animation::BlendMask_All, autodisable,
weapSpeed, startKey, stopKey, weapSpeed, startKey, stopKey,
0.0f, 0); 0.0f, 0);
if(mAnimation->getCurrentTime(mCurrentWeapon) != -1.f) if (mWeaponType != ESM::Weapon::PickProbe && mAnimation->getCurrentTime(mCurrentWeapon) != -1.f)
{ {
mUpperBodyState = UpperCharState_StartToMinAttack; mUpperBodyState = UpperBodyState::AttackPreWindUp;
if (isRandomAttackAnimation(mCurrentWeapon)) if (isRandomAttackAnimation(mCurrentWeapon))
{ {
mAttackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability(prng)); mAttackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability(prng));
@ -1578,25 +1575,34 @@ bool CharacterController::updateWeaponState(CharacterState idle)
} }
// We should not break swim and sneak animations // We should not break swim and sneak animations
if (resetIdle && if (resetIdle && mIdleState != CharState_IdleSneak && mIdleState != CharState_IdleSwim)
idle != CharState_IdleSneak && idle != CharState_IdleSwim &&
mIdleState != CharState_IdleSneak && mIdleState != CharState_IdleSwim)
{ {
resetCurrentIdleState(); resetCurrentIdleState();
} }
}
if (!animPlaying) if (!animPlaying)
animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete);
if(mUpperBodyState == UpperCharState_MinAttackToMaxAttack && !isKnockedDown())
mAttackStrength = complete; if (isKnockedDown())
}
else
{ {
animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); if (mUpperBodyState > UpperBodyState::WeaponEquipped)
if(mUpperBodyState == UpperCharState_MinAttackToMaxAttack && !isKnockedDown()) {
mUpperBodyState = UpperBodyState::WeaponEquipped;
if (mWeaponType > ESM::Weapon::None)
mAnimation->showWeapons(true);
}
if (!mCurrentWeapon.empty())
mAnimation->disable(mCurrentWeapon);
}
if (mUpperBodyState == UpperBodyState::AttackWindUp)
{
mAttackStrength = complete;
if (!getAttackingOrSpell())
{ {
world->breakInvisibility(mPtr); world->breakInvisibility(mPtr);
float attackStrength = complete;
float minAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"min attack"); float minAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"min attack");
float maxAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"max attack"); float maxAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"max attack");
if (minAttackTime == maxAttackTime) if (minAttackTime == maxAttackTime)
@ -1604,25 +1610,12 @@ bool CharacterController::updateWeaponState(CharacterState idle)
// most creatures don't actually have an attack wind-up animation, so use a uniform random value // most creatures don't actually have an attack wind-up animation, so use a uniform random value
// (even some creatures that can use weapons don't have a wind-up animation either, e.g. Rieklings) // (even some creatures that can use weapons don't have a wind-up animation either, e.g. Rieklings)
// Note: vanilla MW uses a random value for *all* non-player actors, but we probably don't need to go that far. // Note: vanilla MW uses a random value for *all* non-player actors, but we probably don't need to go that far.
attackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability(prng)); mAttackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability(prng));
} }
if(weapclass != ESM::WeaponType::Ranged && weapclass != ESM::WeaponType::Thrown) playSwishSound(mAttackStrength);
{
if(isWerewolf)
{
const MWWorld::ESMStore &store = world->getStore();
const ESM::Sound *sound = store.get<ESM::Sound>().searchRandom("WolfSwing", prng);
if(sound)
sndMgr->playSound3D(mPtr, sound->mId, 1.0f, 1.0f);
}
else
{
playSwishSound(attackStrength);
}
}
mAttackStrength = attackStrength;
if (animPlaying)
mAnimation->disable(mCurrentWeapon); mAnimation->disable(mCurrentWeapon);
mAnimation->play(mCurrentWeapon, priorityWeapon, mAnimation->play(mCurrentWeapon, priorityWeapon,
MWRender::Animation::BlendMask_All, false, MWRender::Animation::BlendMask_All, false,
@ -1630,18 +1623,7 @@ bool CharacterController::updateWeaponState(CharacterState idle)
1.0f-complete, 0); 1.0f-complete, 0);
complete = 0.f; complete = 0.f;
mUpperBodyState = UpperCharState_MaxAttackToMinHit; mUpperBodyState = UpperBodyState::AttackRelease;
}
else if (isKnockedDown())
{
if (mUpperBodyState > UpperCharState_WeapEquiped)
{
mUpperBodyState = UpperCharState_WeapEquiped;
if (mWeaponType > ESM::Weapon::None)
mAnimation->showWeapons(true);
}
if (!mCurrentWeapon.empty())
mAnimation->disable(mCurrentWeapon);
} }
} }
@ -1650,15 +1632,15 @@ bool CharacterController::updateWeaponState(CharacterState idle)
{ {
switch (mUpperBodyState) switch (mUpperBodyState)
{ {
case UpperCharState_StartToMinAttack: case UpperBodyState::AttackPreWindUp:
mAnimation->setPitchFactor(complete); mAnimation->setPitchFactor(complete);
break; break;
case UpperCharState_MinAttackToMaxAttack: case UpperBodyState::AttackWindUp:
case UpperCharState_MaxAttackToMinHit: case UpperBodyState::AttackRelease:
case UpperCharState_MinHitToHit: case UpperBodyState::AttackHit:
mAnimation->setPitchFactor(1.f); mAnimation->setPitchFactor(1.f);
break; break;
case UpperCharState_FollowStartToFollowStop: case UpperBodyState::AttackEnd:
if (animPlaying) if (animPlaying)
{ {
// technically we do not need a pitch for crossbow reload animation, // technically we do not need a pitch for crossbow reload animation,
@ -1676,39 +1658,39 @@ bool CharacterController::updateWeaponState(CharacterState idle)
if(!animPlaying) if(!animPlaying)
{ {
if(mUpperBodyState == UpperCharState_EquipingWeap || if (mUpperBodyState == UpperBodyState::Equipping ||
mUpperBodyState == UpperCharState_FollowStartToFollowStop || mUpperBodyState == UpperBodyState::AttackEnd ||
mUpperBodyState == UpperCharState_CastingSpell) mUpperBodyState == UpperBodyState::Casting)
{ {
if (ammunition && mWeaponType == ESM::Weapon::MarksmanCrossbow) if (ammunition && mWeaponType == ESM::Weapon::MarksmanCrossbow)
mAnimation->attachArrow(); mAnimation->attachArrow();
// Cancel stagger animation at the end of an attack to avoid abrupt transitions // Cancel stagger animation at the end of an attack to avoid abrupt transitions
// in favor of a different abrupt transition, like Morrowind // in favor of a different abrupt transition, like Morrowind
if (mUpperBodyState != UpperCharState_EquipingWeap && isRecovery()) if (mUpperBodyState != UpperBodyState::Equipping && isRecovery())
mAnimation->disable(mCurrentHit); mAnimation->disable(mCurrentHit);
mUpperBodyState = UpperCharState_WeapEquiped; mUpperBodyState = UpperBodyState::WeaponEquipped;
} }
else if(mUpperBodyState == UpperCharState_UnEquipingWeap) else if (mUpperBodyState == UpperBodyState::Unequipping)
mUpperBodyState = UpperCharState_Nothing; mUpperBodyState = UpperBodyState::None;
} }
else if(complete >= 1.0f && !isRandomAttackAnimation(mCurrentWeapon)) else if(complete >= 1.0f && !isRandomAttackAnimation(mCurrentWeapon))
{ {
std::string start, stop; std::string start, stop;
switch(mUpperBodyState) switch(mUpperBodyState)
{ {
case UpperCharState_MinAttackToMaxAttack: case UpperBodyState::AttackWindUp:
//hack to avoid body pos desync when jumping/sneaking in 'max attack' state //hack to avoid body pos desync when jumping/sneaking in 'max attack' state
if(!mAnimation->isPlaying(mCurrentWeapon)) if(!mAnimation->isPlaying(mCurrentWeapon))
mAnimation->play(mCurrentWeapon, priorityWeapon, mAnimation->play(mCurrentWeapon, priorityWeapon,
MWRender::Animation::BlendMask_All, false, MWRender::Animation::BlendMask_All, false,
0, mAttackType+" min attack", mAttackType+" max attack", 0.999f, 0); 0, mAttackType+" min attack", mAttackType+" max attack", 0.999f, 0);
break; break;
case UpperCharState_StartToMinAttack: case UpperBodyState::AttackPreWindUp:
case UpperCharState_MaxAttackToMinHit: case UpperBodyState::AttackRelease:
{ {
if (mUpperBodyState == UpperCharState_StartToMinAttack) if (mUpperBodyState == UpperBodyState::AttackPreWindUp)
{ {
// If actor is already stopped preparing attack, do not play the "min attack -> max attack" part. // If actor is already stopped preparing attack, do not play the "min attack -> max attack" part.
// Happens if the player did not hold the attack button. // Happens if the player did not hold the attack button.
@ -1719,12 +1701,11 @@ bool CharacterController::updateWeaponState(CharacterState idle)
{ {
start = mAttackType+" min attack"; start = mAttackType+" min attack";
stop = mAttackType+" max attack"; stop = mAttackType+" max attack";
mUpperBodyState = UpperCharState_MinAttackToMaxAttack; mUpperBodyState = UpperBodyState::AttackWindUp;
break; break;
} }
world->breakInvisibility(mPtr); world->breakInvisibility(mPtr);
if(weapclass != ESM::WeaponType::Ranged && weapclass != ESM::WeaponType::Thrown)
playSwishSound(0.0f); playSwishSound(0.0f);
} }
@ -1738,10 +1719,10 @@ bool CharacterController::updateWeaponState(CharacterState idle)
start = mAttackType+" min hit"; start = mAttackType+" min hit";
stop = mAttackType+" hit"; stop = mAttackType+" hit";
} }
mUpperBodyState = UpperCharState_MinHitToHit; mUpperBodyState = UpperBodyState::AttackHit;
break; break;
} }
case UpperCharState_MinHitToHit: case UpperBodyState::AttackHit:
if(mAttackType == "shoot") if(mAttackType == "shoot")
{ {
start = mAttackType+" follow start"; start = mAttackType+" follow start";
@ -1757,57 +1738,26 @@ bool CharacterController::updateWeaponState(CharacterState idle)
: (str < 1.0f) ? " medium follow stop" : (str < 1.0f) ? " medium follow stop"
: " large follow stop"); : " large follow stop");
} }
mUpperBodyState = UpperCharState_FollowStartToFollowStop; mUpperBodyState = UpperBodyState::AttackEnd;
break; break;
default: default:
break; break;
} }
// Note: apply crossbow reload animation only for upper body
// since blending with movement animations can give weird result.
if(!start.empty()) if(!start.empty())
{ {
int mask = MWRender::Animation::BlendMask_All; bool autodisable = mUpperBodyState == UpperBodyState::AttackEnd;
if (mWeaponType == ESM::Weapon::MarksmanCrossbow)
mask = MWRender::Animation::BlendMask_UpperBody;
mAnimation->disable(mCurrentWeapon); mAnimation->disable(mCurrentWeapon);
if (mUpperBodyState == UpperCharState_FollowStartToFollowStop) mAnimation->play(mCurrentWeapon, priorityWeapon, MWRender::Animation::BlendMask_All, autodisable, weapSpeed, start, stop, 0.0f, 0);
mAnimation->play(mCurrentWeapon, priorityWeapon,
mask, true,
weapSpeed, start, stop, 0.0f, 0);
else
mAnimation->play(mCurrentWeapon, priorityWeapon,
mask, false,
weapSpeed, start, stop, 0.0f, 0);
} }
} }
else if(complete >= 1.0f && isRandomAttackAnimation(mCurrentWeapon)) else if(complete >= 1.0f && isRandomAttackAnimation(mCurrentWeapon))
{ {
clearStateAnimation(mCurrentWeapon); clearStateAnimation(mCurrentWeapon);
mUpperBodyState = UpperCharState_WeapEquiped; mUpperBodyState = UpperBodyState::WeaponEquipped;
} }
if (cls.hasInventoryStore(mPtr)) mAnimation->setAccurateAiming(mUpperBodyState > UpperBodyState::WeaponEquipped);
{
const MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr);
MWWorld::ConstContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft);
if(torch != inv.end() && torch->getType() == ESM::Light::sRecordId
&& updateCarriedLeftVisible(mWeaponType))
{
if (mAnimation->isPlaying("shield"))
mAnimation->disable("shield");
mAnimation->play("torch", Priority_Torch, MWRender::Animation::BlendMask_LeftArm,
false, 1.0f, "start", "stop", 0.0f, (~(size_t)0), true);
}
else if (mAnimation->isPlaying("torch"))
{
mAnimation->disable("torch");
}
}
mAnimation->setAccurateAiming(mUpperBodyState > UpperCharState_WeapEquiped);
return forcestateupdate; return forcestateupdate;
} }
@ -2270,7 +2220,7 @@ void CharacterController::update(float duration)
if (!mSkipAnim) if (!mSkipAnim)
{ {
refreshCurrentAnims(idlestate, movestate, jumpstate, updateWeaponState(idlestate)); refreshCurrentAnims(idlestate, movestate, jumpstate, updateWeaponState());
updateIdleStormState(inwater); updateIdleStormState(inwater);
} }
@ -2567,8 +2517,8 @@ void CharacterController::forceStateUpdate()
mCanCast = false; mCanCast = false;
mCastingManualSpell = false; mCastingManualSpell = false;
setAttackingOrSpell(false); setAttackingOrSpell(false);
if (mUpperBodyState != UpperCharState_Nothing) if (mUpperBodyState != UpperBodyState::None)
mUpperBodyState = UpperCharState_WeapEquiped; mUpperBodyState = UpperBodyState::WeaponEquipped;
refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true); refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true);
@ -2687,13 +2637,13 @@ bool CharacterController::isRandomAttackAnimation(std::string_view group)
bool CharacterController::isAttackPreparing() const bool CharacterController::isAttackPreparing() const
{ {
return mUpperBodyState == UpperCharState_StartToMinAttack || return mUpperBodyState == UpperBodyState::AttackPreWindUp ||
mUpperBodyState == UpperCharState_MinAttackToMaxAttack; mUpperBodyState == UpperBodyState::AttackWindUp;
} }
bool CharacterController::isCastingSpell() const bool CharacterController::isCastingSpell() const
{ {
return mCastingManualSpell || mUpperBodyState == UpperCharState_CastingSpell; return mCastingManualSpell || mUpperBodyState == UpperBodyState::Casting;
} }
bool CharacterController::isReadyToBlock() const bool CharacterController::isReadyToBlock() const
@ -2729,8 +2679,7 @@ bool CharacterController::isRecovery() const
bool CharacterController::isAttackingOrSpell() const bool CharacterController::isAttackingOrSpell() const
{ {
return mUpperBodyState != UpperCharState_Nothing && return mUpperBodyState != UpperBodyState::None && mUpperBodyState != UpperBodyState::WeaponEquipped;
mUpperBodyState != UpperCharState_WeapEquiped;
} }
bool CharacterController::isSneaking() const bool CharacterController::isSneaking() const
@ -2786,7 +2735,7 @@ std::string_view CharacterController::getRandomAttackType()
bool CharacterController::readyToPrepareAttack() const bool CharacterController::readyToPrepareAttack() const
{ {
return (mHitState == CharState_None || mHitState == CharState_Block) return (mHitState == CharState_None || mHitState == CharState_Block)
&& mUpperBodyState <= UpperCharState_WeapEquiped; && mUpperBodyState <= UpperBodyState::WeaponEquipped;
} }
bool CharacterController::readyToStartAttack() const bool CharacterController::readyToStartAttack() const
@ -2794,7 +2743,7 @@ bool CharacterController::readyToStartAttack() const
if (mHitState != CharState_None && mHitState != CharState_Block) if (mHitState != CharState_None && mHitState != CharState_Block)
return false; return false;
return mUpperBodyState == UpperCharState_WeapEquiped; return mUpperBodyState == UpperBodyState::WeaponEquipped;
} }
float CharacterController::getAttackStrength() const float CharacterController::getAttackStrength() const
@ -2819,15 +2768,33 @@ void CharacterController::setHeadTrackTarget(const MWWorld::ConstPtr &target)
void CharacterController::playSwishSound(float attackStrength) const void CharacterController::playSwishSound(float attackStrength) const
{ {
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); ESM::WeaponType::Class weapclass = getWeaponType(mWeaponType)->mWeaponClass;
if (weapclass == ESM::WeaponType::Ranged || weapclass == ESM::WeaponType::Thrown)
return;
std::string sound = "Weapon Swish"; std::string soundId;
if(attackStrength < 0.5f) float pitch = 1.f;
sndMgr->playSound3D(mPtr, sound, 1.0f, 0.8f); //Weak attack
else if(attackStrength < 1.0f) const MWWorld::Class &cls = mPtr.getClass();
sndMgr->playSound3D(mPtr, sound, 1.0f, 1.0f); //Medium attack if (cls.isNpc() && cls.getNpcStats(mPtr).isWerewolf())
{
MWBase::World* world = MWBase::Environment::get().getWorld();
const MWWorld::ESMStore &store = world->getStore();
const ESM::Sound *sound = store.get<ESM::Sound>().searchRandom("WolfSwing", world->getPrng());
if (sound)
soundId = sound->mId;
}
else else
sndMgr->playSound3D(mPtr, sound, 1.0f, 1.2f); //Strong attack {
soundId = "Weapon Swish";
if (attackStrength < 0.5f)
pitch = 0.8f; // Weak attack
else if (attackStrength >= 1.f)
pitch = 1.2f; // Strong attack
}
if (!soundId.empty())
MWBase::Environment::get().getSoundManager()->playSound3D(mPtr, soundId, 1.0f, pitch);
} }
void CharacterController::updateHeadTracking(float duration) void CharacterController::updateHeadTracking(float duration)

View file

@ -103,17 +103,18 @@ enum CharacterState {
CharState_Block CharState_Block
}; };
enum UpperBodyCharacterState { enum class UpperBodyState
UpperCharState_Nothing, {
UpperCharState_EquipingWeap, None,
UpperCharState_UnEquipingWeap, Equipping,
UpperCharState_WeapEquiped, Unequipping,
UpperCharState_StartToMinAttack, WeaponEquipped,
UpperCharState_MinAttackToMaxAttack, AttackPreWindUp,
UpperCharState_MaxAttackToMinHit, AttackWindUp,
UpperCharState_MinHitToHit, AttackRelease,
UpperCharState_FollowStartToFollowStop, AttackHit,
UpperCharState_CastingSpell AttackEnd,
Casting
}; };
enum JumpingState { enum JumpingState {
@ -156,7 +157,7 @@ class CharacterController : public MWRender::Animation::TextKeyListener
CharacterState mHitState{CharState_None}; CharacterState mHitState{CharState_None};
std::string mCurrentHit; std::string mCurrentHit;
UpperBodyCharacterState mUpperBodyState{UpperCharState_Nothing}; UpperBodyState mUpperBodyState{UpperBodyState::None};
JumpingState mJumpState{JumpState_None}; JumpingState mJumpState{JumpState_None};
std::string mCurrentJump; std::string mCurrentJump;
@ -203,7 +204,7 @@ class CharacterController : public MWRender::Animation::TextKeyListener
void clearAnimQueue(bool clearPersistAnims = false); void clearAnimQueue(bool clearPersistAnims = false);
bool updateWeaponState(CharacterState idle); bool updateWeaponState();
void updateIdleStormState(bool inwater) const; void updateIdleStormState(bool inwater) const;
std::string chooseRandomAttackAnimation() const; std::string chooseRandomAttackAnimation() const;