Merge pull request #1874 from akortunov/combat_anims

Fix some issues with attack animations
pull/540/head
Bret Curtis 6 years ago committed by GitHub
commit 7f3769d5fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -39,6 +39,7 @@
Bug #4230: AiTravel package issues break some Tribunal quests
Bug #4231: Infected rats from the "Crimson Plague" quest rendered unconscious by change in Drain Fatigue functionality
Bug #4251: Stationary NPCs do not return to their position after combat
Bug #4271: Scamp flickers when attacking
Bug #4274: Pre-0.43 death animations are not forward-compatible with 0.43+
Bug #4286: Scripted animations can be interrupted
Bug #4291: Non-persistent actors that started the game as dead do not play death animations
@ -83,6 +84,7 @@
Bug #4503: Cast and ExplodeSpell commands increase alteration skill
Bug #4510: Division by zero in MWMechanics::CreatureStats::setAttribute
Bug #4519: Knockdown does not discard movement in the 1st-person mode
Bug #4531: Movement does not reset idle animations
Bug #4539: Paper Doll is affected by GUI scaling
Bug #4545: Creatures flee from werewolves
Bug #4551: Replace 0 sound range with default range separately
@ -95,6 +97,7 @@
Bug #4574: Player turning animations are twitchy
Bug #4575: Weird result of attack animation blending with movement animations
Bug #4576: Reset of idle animations when attack can not be started
Bug #4591: Attack strength should be 0 if player did not hold the attack button
Feature #1645: Casting effects from objects
Feature #2606: Editor: Implemented (optional) case sensitive global search
Feature #3083: Play animation when NPC is casting spell via script

@ -257,7 +257,7 @@ namespace MWBase
virtual void cleanupSummonedCreature(const MWWorld::Ptr& caster, int creatureActorId) = 0;
virtual void confiscateStolenItemToOwner(const MWWorld::Ptr &player, const MWWorld::Ptr &item, const MWWorld::Ptr& victim, int count) = 0;
virtual bool isAttackPrepairing(const MWWorld::Ptr& ptr) = 0;
virtual bool isAttackPreparing(const MWWorld::Ptr& ptr) = 0;
virtual bool isRunning(const MWWorld::Ptr& ptr) = 0;
virtual bool isSneaking(const MWWorld::Ptr& ptr) = 0;
};

@ -1004,9 +1004,9 @@ namespace MWInput
if (!mControlSwitch["playerfighting"] || !mControlSwitch["playercontrols"])
return;
// We want to interrupt animation only if attack is prepairing, but still is not triggered
// We want to interrupt animation only if attack is preparing, but still is not triggered
// Otherwise we will get a "speedshooting" exploit, when player can skip reload animation by hitting "Toggle Weapon" key twice
if (MWBase::Environment::get().getMechanicsManager()->isAttackPrepairing(mPlayer->getPlayer()))
if (MWBase::Environment::get().getMechanicsManager()->isAttackPreparing(mPlayer->getPlayer()))
mPlayer->setAttackingOrSpell(false);
else if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(mPlayer->getPlayer()))
return;

@ -896,14 +896,14 @@ namespace MWMechanics
}
}
bool Actors::isAttackPrepairing(const MWWorld::Ptr& ptr)
bool Actors::isAttackPreparing(const MWWorld::Ptr& ptr)
{
PtrActorMap::iterator it = mActors.find(ptr);
if (it == mActors.end())
return false;
CharacterController* ctrl = it->second->getCharacterController();
return ctrl->isAttackPrepairing();
return ctrl->isAttackPreparing();
}
bool Actors::isRunning(const MWWorld::Ptr& ptr)

@ -118,7 +118,7 @@ namespace MWMechanics
int countDeaths (const std::string& id) const;
///< Return the number of deaths for actors with the given ID.
bool isAttackPrepairing(const MWWorld::Ptr& ptr);
bool isAttackPreparing(const MWWorld::Ptr& ptr);
bool isRunning(const MWWorld::Ptr& ptr);
bool isSneaking(const MWWorld::Ptr& ptr);

@ -372,21 +372,32 @@ namespace MWMechanics
actorMovementSettings.mPosition[1] = storage.mMovement.mPosition[1];
actorMovementSettings.mPosition[2] = storage.mMovement.mPosition[2];
rotateActorOnAxis(actor, 2, actorMovementSettings, storage.mMovement);
rotateActorOnAxis(actor, 0, actorMovementSettings, storage.mMovement);
rotateActorOnAxis(actor, 2, actorMovementSettings, storage);
rotateActorOnAxis(actor, 0, actorMovementSettings, storage);
}
void AiCombat::rotateActorOnAxis(const MWWorld::Ptr& actor, int axis,
MWMechanics::Movement& actorMovementSettings, MWMechanics::Movement& desiredMovement)
MWMechanics::Movement& actorMovementSettings, AiCombatStorage& storage)
{
actorMovementSettings.mRotation[axis] = 0;
float& targetAngleRadians = desiredMovement.mRotation[axis];
float& targetAngleRadians = storage.mMovement.mRotation[axis];
if (targetAngleRadians != 0)
{
if (smoothTurn(actor, targetAngleRadians, axis))
// Some attack animations contain small amount of movement.
// Since we use cone shapes for melee, we can use a threshold to avoid jittering
std::shared_ptr<Action>& currentAction = storage.mCurrentAction;
bool isRangedCombat = false;
currentAction->getCombatRange(isRangedCombat);
// Check if the actor now facing desired direction, no need to turn any more
if (isRangedCombat)
{
if (smoothTurn(actor, targetAngleRadians, axis))
targetAngleRadians = 0;
}
else
{
// actor now facing desired direction, no need to turn any more
targetAngleRadians = 0;
if (smoothTurn(actor, targetAngleRadians, axis, osg::DegreesToRadians(3.f)))
targetAngleRadians = 0;
}
}
}
@ -453,7 +464,7 @@ namespace MWMechanics
if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability() < 0.25)
{
mMovement.mPosition[0] = Misc::Rng::rollProbability() < 0.5 ? 1.0f : -1.0f; // to the left/right
mTimerCombatMove = 0.05f + 0.15f * Misc::Rng::rollClosedProbability();
mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability();
mCombatMove = true;
}
}

@ -129,7 +129,7 @@ namespace MWMechanics
/// Transfer desired movement (from AiCombatStorage) to Actor
void updateActorsMovement(const MWWorld::Ptr& actor, float duration, AiCombatStorage& storage);
void rotateActorOnAxis(const MWWorld::Ptr& actor, int axis,
MWMechanics::Movement& actorMovementSettings, MWMechanics::Movement& desiredMovement);
MWMechanics::Movement& actorMovementSettings, AiCombatStorage& storage);
};

@ -411,10 +411,6 @@ void CharacterController::refreshMovementAnims(const WeaponInfo* weap, Character
if(force || movement != mMovementState)
{
mMovementState = movement;
if (movement != CharState_None)
mIdleState = CharState_None;
std::string movementAnimName;
MWRender::Animation::BlendMask movemask = MWRender::Animation::BlendMask_All;
const StateInfo *movestate = std::find_if(sMovementList, sMovementListEnd, FindCharState(mMovementState));
@ -531,7 +527,7 @@ void CharacterController::refreshMovementAnims(const WeaponInfo* weap, Character
void CharacterController::refreshIdleAnims(const WeaponInfo* weap, CharacterState idle, bool force)
{
if(force || idle != mIdleState || (!mAnimation->isPlaying(mCurrentIdle) && mAnimQueue.empty()))
if(force || idle != mIdleState || mIdleState == CharState_None || (!mAnimation->isPlaying(mCurrentIdle) && mAnimQueue.empty()))
{
mIdleState = idle;
size_t numLoops = ~0ul;
@ -562,14 +558,24 @@ void CharacterController::refreshIdleAnims(const WeaponInfo* weap, CharacterStat
// play until the Loop Stop key 2 to 5 times, then play until the Stop key
// this replicates original engine behavior for the "Idle1h" 1st-person animation
numLoops = 1 + Misc::Rng::rollDice(4);
}
}
}
mAnimation->disable(mCurrentIdle);
// There is no need to restart anim if the new and old anims are the same.
// Just update a number of loops.
float startPoint = 0;
if (!mCurrentIdle.empty() && mCurrentIdle == idleGroup)
{
mAnimation->getInfo(mCurrentIdle, &startPoint);
}
if(!mCurrentIdle.empty())
mAnimation->disable(mCurrentIdle);
mCurrentIdle = idleGroup;
if(!mCurrentIdle.empty())
mAnimation->play(mCurrentIdle, idlePriority, MWRender::Animation::BlendMask_All, false,
1.0f, "start", "stop", 0.0f, numLoops, true);
1.0f, "start", "stop", startPoint, numLoops, true);
}
}
@ -1385,14 +1391,6 @@ bool CharacterController::updateWeaponState()
MWBase::Environment::get().getWorld()->breakInvisibility(mPtr);
mAttackStrength = 0;
// Randomize attacks for non-bipedal creatures with Weapon flag
if (mPtr.getClass().getTypeName() == typeid(ESM::Creature).name() &&
!mPtr.getClass().isBipedal(mPtr) &&
(!mAnimation->hasAnimation(mCurrentWeapon) || isRandomAttackAnimation(mCurrentWeapon)))
{
mCurrentWeapon = chooseRandomAttackAnimation();
}
if(mWeaponType == WeapType_Spell)
{
// Unset casting flag, otherwise pressing the mouse button down would
@ -1401,15 +1399,10 @@ bool CharacterController::updateWeaponState()
if (mPtr == player)
{
MWBase::Environment::get().getWorld()->getPlayer().setAttackingOrSpell(false);
}
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
// For the player, set the spell we want to cast
// This has to be done at the start of the casting animation,
// *not* when selecting a spell in the GUI (otherwise you could change the spell mid-animation)
if (mPtr == player)
{
// For the player, set the spell we want to cast
// This has to be done at the start of the casting animation,
// *not* when selecting a spell in the GUI (otherwise you could change the spell mid-animation)
std::string selectedSpell = MWBase::Environment::get().getWindowManager()->getSelectedSpell();
stats.getSpells().setSelectedSpell(selectedSpell);
}
@ -1421,6 +1414,7 @@ bool CharacterController::updateWeaponState()
MWMechanics::CastSpell cast(mPtr, NULL, false, mCastingManualSpell);
cast.playSpellCastingEffects(spellid);
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
const ESM::Spell *spell = store.get<ESM::Spell>().find(spellid);
const ESM::ENAMstruct &lastEffect = spell->mEffects.mList.back();
const ESM::MagicEffect *effect;
@ -1520,13 +1514,12 @@ bool CharacterController::updateWeaponState()
startKey = mAttackType+" start";
stopKey = mAttackType+" min attack";
}
if (isRandomAttackAnimation(mCurrentWeapon))
else if (isRandomAttackAnimation(mCurrentWeapon))
{
startKey = "start";
stopKey = "stop";
}
else if (mAttackType != "shoot")
else
{
if(mPtr == getPlayer())
{
@ -1683,11 +1676,6 @@ bool CharacterController::updateWeaponState()
std::string start, stop;
switch(mUpperBodyState)
{
case UpperCharState_StartToMinAttack:
start = mAttackType+" min attack";
stop = mAttackType+" max attack";
mUpperBodyState = UpperCharState_MinAttackToMaxAttack;
break;
case UpperCharState_MinAttackToMaxAttack:
//hack to avoid body pos desync when jumping/sneaking in 'max attack' state
if(!mAnimation->isPlaying(mCurrentWeapon))
@ -1695,6 +1683,23 @@ bool CharacterController::updateWeaponState()
MWRender::Animation::BlendMask_All, false,
0, mAttackType+" min attack", mAttackType+" max attack", 0.999f, 0);
break;
case UpperCharState_StartToMinAttack:
{
// 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.
// Note: if the "min attack"->"max attack" is a stub, "play" it anyway. Attack strength will be 1.
float minAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"min attack");
float maxAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"max attack");
if (mAttackingOrSpell || minAttackTime == maxAttackTime)
{
start = mAttackType+" min attack";
stop = mAttackType+" max attack";
mUpperBodyState = UpperCharState_MinAttackToMaxAttack;
break;
}
playSwishSound(0.0f);
}
// Fall-through
case UpperCharState_MaxAttackToMinHit:
if(mAttackType == "shoot")
{
@ -2094,7 +2099,16 @@ void CharacterController::update(float duration)
if(mAnimQueue.empty() || inwater || sneak)
{
idlestate = (inwater ? CharState_IdleSwim : (sneak && !inJump ? CharState_IdleSneak : CharState_Idle));
// Note: turning animations should not interrupt idle ones.
// Also movement should not stop idle animation for spellcasting stance.
if (inwater)
idlestate = CharState_IdleSwim;
else if (sneak && !inJump)
idlestate = CharState_IdleSneak;
else if (movestate != CharState_None && !isTurning() && mWeaponType != WeapType_Spell)
idlestate = CharState_None;
else
idlestate = CharState_Idle;
}
else
updateAnimQueue();
@ -2493,7 +2507,7 @@ bool CharacterController::isRandomAttackAnimation(const std::string& group) cons
group == "attack3" || group == "swimattack3");
}
bool CharacterController::isAttackPrepairing() const
bool CharacterController::isAttackPreparing() const
{
return mUpperBodyState == UpperCharState_StartToMinAttack ||
mUpperBodyState == UpperCharState_MinAttackToMaxAttack;

@ -281,7 +281,7 @@ public:
void forceStateUpdate();
bool isAttackPrepairing() const;
bool isAttackPreparing() const;
bool isCastingSpell() const;
bool isReadyToBlock() const;
bool isKnockedDown() const;

@ -436,9 +436,9 @@ namespace MWMechanics
return mActors.isActorDetected(actor, observer);
}
bool MechanicsManager::isAttackPrepairing(const MWWorld::Ptr& ptr)
bool MechanicsManager::isAttackPreparing(const MWWorld::Ptr& ptr)
{
return mActors.isAttackPrepairing(ptr);
return mActors.isAttackPreparing(ptr);
}
bool MechanicsManager::isRunning(const MWWorld::Ptr& ptr)

@ -229,7 +229,7 @@ namespace MWMechanics
virtual void confiscateStolenItemToOwner(const MWWorld::Ptr &player, const MWWorld::Ptr &item, const MWWorld::Ptr& victim, int count);
virtual bool isAttackPrepairing(const MWWorld::Ptr& ptr);
virtual bool isAttackPreparing(const MWWorld::Ptr& ptr);
virtual bool isRunning(const MWWorld::Ptr& ptr);
virtual bool isSneaking(const MWWorld::Ptr& ptr);

Loading…
Cancel
Save