mirror of
https://gitlab.com/OpenMW/openmw.git
synced 2025-03-09 22:13:31 +00:00
Remove WindowManager wrappers. It's not safe to use WindowManager in all places and it's not required. Environment stores resource system providing VFS required to call these functions. In the case of ObjectPaging it's available from the member variable. Also ObjectPaging::createChunk may access WindowManager when it's already destructed when exiting the game because it's destructed before CellPreloader finishes all background jobs. Engine::mResourceSystem is destructed after all other systems so it's safe to use it.
2842 lines
109 KiB
C++
2842 lines
109 KiB
C++
/*
|
|
* OpenMW - The completely unofficial reimplementation of Morrowind
|
|
*
|
|
* This file (character.cpp) is part of the OpenMW package.
|
|
*
|
|
* OpenMW is distributed as free software: you can redistribute it
|
|
* and/or modify it under the terms of the GNU General Public License
|
|
* version 3, as published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* version 3 along with this program. If not, see
|
|
* https://www.gnu.org/licenses/ .
|
|
*/
|
|
|
|
#include "character.hpp"
|
|
|
|
#include <iostream>
|
|
|
|
#include <components/misc/mathutil.hpp>
|
|
#include <components/misc/rng.hpp>
|
|
#include <components/misc/stringops.hpp>
|
|
#include <components/misc/resourcehelpers.hpp>
|
|
|
|
#include <components/settings/settings.hpp>
|
|
|
|
#include <components/sceneutil/positionattitudetransform.hpp>
|
|
|
|
#include "../mwrender/animation.hpp"
|
|
|
|
#include "../mwbase/environment.hpp"
|
|
#include "../mwbase/mechanicsmanager.hpp"
|
|
#include "../mwbase/world.hpp"
|
|
#include "../mwbase/soundmanager.hpp"
|
|
#include "../mwbase/windowmanager.hpp"
|
|
|
|
#include "../mwworld/class.hpp"
|
|
#include "../mwworld/inventorystore.hpp"
|
|
#include "../mwworld/esmstore.hpp"
|
|
#include "../mwworld/player.hpp"
|
|
|
|
#include "aicombataction.hpp"
|
|
#include "movement.hpp"
|
|
#include "npcstats.hpp"
|
|
#include "creaturestats.hpp"
|
|
#include "security.hpp"
|
|
#include "actorutil.hpp"
|
|
#include "spellcasting.hpp"
|
|
|
|
namespace
|
|
{
|
|
|
|
std::string getBestAttack (const ESM::Weapon* weapon)
|
|
{
|
|
int slash = weapon->mData.mSlash[0] + weapon->mData.mSlash[1];
|
|
int chop = weapon->mData.mChop[0] + weapon->mData.mChop[1];
|
|
int thrust = weapon->mData.mThrust[0] + weapon->mData.mThrust[1];
|
|
if (slash == chop && slash == thrust)
|
|
return "slash";
|
|
else if (thrust >= chop && thrust >= slash)
|
|
return "thrust";
|
|
else if (slash >= chop && slash >= thrust)
|
|
return "slash";
|
|
else
|
|
return "chop";
|
|
}
|
|
|
|
// Converts a movement Run state to its equivalent Walk state, if there is one.
|
|
MWMechanics::CharacterState runStateToWalkState (MWMechanics::CharacterState state)
|
|
{
|
|
using namespace MWMechanics;
|
|
switch (state)
|
|
{
|
|
case CharState_RunForward: return CharState_WalkForward;
|
|
case CharState_RunBack: return CharState_WalkBack;
|
|
case CharState_RunLeft: return CharState_WalkLeft;
|
|
case CharState_RunRight: return CharState_WalkRight;
|
|
case CharState_SwimRunForward: return CharState_SwimWalkForward;
|
|
case CharState_SwimRunBack: return CharState_SwimWalkBack;
|
|
case CharState_SwimRunLeft: return CharState_SwimWalkLeft;
|
|
case CharState_SwimRunRight: return CharState_SwimWalkRight;
|
|
default: return state;
|
|
}
|
|
}
|
|
|
|
// Converts a Hit state to its equivalent Death state.
|
|
MWMechanics::CharacterState hitStateToDeathState (MWMechanics::CharacterState state)
|
|
{
|
|
using namespace MWMechanics;
|
|
switch (state)
|
|
{
|
|
case CharState_SwimKnockDown: return CharState_SwimDeathKnockDown;
|
|
case CharState_SwimKnockOut: return CharState_SwimDeathKnockOut;
|
|
case CharState_KnockDown: return CharState_DeathKnockDown;
|
|
case CharState_DeathKnockOut: return CharState_DeathKnockOut;
|
|
default: return CharState_None;
|
|
}
|
|
}
|
|
|
|
// Converts a movement state to its equivalent base animation group as long as it is a movement state.
|
|
std::string movementStateToAnimGroup(MWMechanics::CharacterState state)
|
|
{
|
|
using namespace MWMechanics;
|
|
switch (state)
|
|
{
|
|
case CharState_WalkForward: return "walkforward";
|
|
case CharState_WalkBack: return "walkback";
|
|
case CharState_WalkLeft: return "walkleft";
|
|
case CharState_WalkRight: return "walkright";
|
|
|
|
case CharState_SwimWalkForward: return "swimwalkforward";
|
|
case CharState_SwimWalkBack: return "swimwalkback";
|
|
case CharState_SwimWalkLeft: return "swimwalkleft";
|
|
case CharState_SwimWalkRight: return "swimwalkright";
|
|
|
|
case CharState_RunForward: return "runforward";
|
|
case CharState_RunBack: return "runback";
|
|
case CharState_RunLeft: return "runleft";
|
|
case CharState_RunRight: return "runright";
|
|
|
|
case CharState_SwimRunForward: return "swimrunforward";
|
|
case CharState_SwimRunBack: return "swimrunback";
|
|
case CharState_SwimRunLeft: return "swimrunleft";
|
|
case CharState_SwimRunRight: return "swimrunright";
|
|
|
|
case CharState_SneakForward: return "sneakforward";
|
|
case CharState_SneakBack: return "sneakback";
|
|
case CharState_SneakLeft: return "sneakleft";
|
|
case CharState_SneakRight: return "sneakright";
|
|
|
|
case CharState_TurnLeft: return "turnleft";
|
|
case CharState_TurnRight: return "turnright";
|
|
case CharState_SwimTurnLeft: return "swimturnleft";
|
|
case CharState_SwimTurnRight: return "swimturnright";
|
|
default: return {};
|
|
}
|
|
}
|
|
|
|
// Converts a death state to its equivalent animation group as long as it is a death state.
|
|
std::string deathStateToAnimGroup(MWMechanics::CharacterState state)
|
|
{
|
|
using namespace MWMechanics;
|
|
switch (state)
|
|
{
|
|
case CharState_SwimDeath: return "swimdeath";
|
|
case CharState_SwimDeathKnockDown: return "swimdeathknockdown";
|
|
case CharState_SwimDeathKnockOut: return "swimdeathknockout";
|
|
case CharState_DeathKnockDown: return "deathknockdown";
|
|
case CharState_DeathKnockOut: return "deathknockout";
|
|
case CharState_Death1: return "death1";
|
|
case CharState_Death2: return "death2";
|
|
case CharState_Death3: return "death3";
|
|
case CharState_Death4: return "death4";
|
|
case CharState_Death5: return "death5";
|
|
default: return {};
|
|
}
|
|
}
|
|
|
|
// Converts a hit state to its equivalent animation group as long as it is a hit state.
|
|
std::string hitStateToAnimGroup(MWMechanics::CharacterState state)
|
|
{
|
|
using namespace MWMechanics;
|
|
switch (state)
|
|
{
|
|
case CharState_SwimHit: return "swimhit";
|
|
case CharState_SwimKnockDown: return "swimknockdown";
|
|
case CharState_SwimKnockOut: return "swimknockout";
|
|
|
|
case CharState_Hit: return "hit";
|
|
case CharState_KnockDown: return "knockdown";
|
|
case CharState_KnockOut: return "knockout";
|
|
|
|
case CharState_Block: return "shield";
|
|
|
|
default: return {};
|
|
}
|
|
}
|
|
|
|
// Converts an idle state to its equivalent animation group.
|
|
std::string idleStateToAnimGroup(MWMechanics::CharacterState state)
|
|
{
|
|
using namespace MWMechanics;
|
|
switch (state)
|
|
{
|
|
case CharState_IdleSwim:
|
|
return "idleswim";
|
|
case CharState_IdleSneak:
|
|
return "idlesneak";
|
|
case CharState_Idle:
|
|
case CharState_SpecialIdle:
|
|
return "idle";
|
|
default:
|
|
return {};
|
|
}
|
|
}
|
|
|
|
MWRender::Animation::AnimPriority getIdlePriority(MWMechanics::CharacterState state)
|
|
{
|
|
using namespace MWMechanics;
|
|
MWRender::Animation::AnimPriority priority(Priority_Default);
|
|
switch (state)
|
|
{
|
|
case CharState_IdleSwim:
|
|
return Priority_SwimIdle;
|
|
case CharState_IdleSneak:
|
|
priority[MWRender::Animation::BoneGroup_LowerBody] = Priority_SneakIdleLowerBody;
|
|
[[fallthrough]];
|
|
default:
|
|
return priority;
|
|
}
|
|
}
|
|
|
|
float getFallDamage(const MWWorld::Ptr& ptr, float fallHeight)
|
|
{
|
|
MWBase::World *world = MWBase::Environment::get().getWorld();
|
|
const MWWorld::Store<ESM::GameSetting> &store = world->getStore().get<ESM::GameSetting>();
|
|
|
|
const float fallDistanceMin = store.find("fFallDamageDistanceMin")->mValue.getFloat();
|
|
|
|
if (fallHeight >= fallDistanceMin)
|
|
{
|
|
const float acrobaticsSkill = static_cast<float>(ptr.getClass().getSkill(ptr, ESM::Skill::Acrobatics));
|
|
const float jumpSpellBonus = ptr.getClass().getCreatureStats(ptr).getMagicEffects().get(ESM::MagicEffect::Jump).getMagnitude();
|
|
const float fallAcroBase = store.find("fFallAcroBase")->mValue.getFloat();
|
|
const float fallAcroMult = store.find("fFallAcroMult")->mValue.getFloat();
|
|
const float fallDistanceBase = store.find("fFallDistanceBase")->mValue.getFloat();
|
|
const float fallDistanceMult = store.find("fFallDistanceMult")->mValue.getFloat();
|
|
|
|
float x = fallHeight - fallDistanceMin;
|
|
x -= (1.5f * acrobaticsSkill) + jumpSpellBonus;
|
|
x = std::max(0.0f, x);
|
|
|
|
float a = fallAcroBase + fallAcroMult * (100 - acrobaticsSkill);
|
|
x = fallDistanceBase + fallDistanceMult * x;
|
|
x *= a;
|
|
|
|
return x;
|
|
}
|
|
return 0.f;
|
|
}
|
|
|
|
}
|
|
|
|
namespace MWMechanics
|
|
{
|
|
|
|
std::string CharacterController::chooseRandomGroup (const std::string& prefix, int* num) const
|
|
{
|
|
auto& prng = MWBase::Environment::get().getWorld()->getPrng();
|
|
|
|
int numAnims=0;
|
|
while (mAnimation->hasAnimation(prefix + std::to_string(numAnims+1)))
|
|
++numAnims;
|
|
|
|
int roll = Misc::Rng::rollDice(numAnims, prng) + 1; // [1, numAnims]
|
|
if (num)
|
|
*num = roll;
|
|
return prefix + std::to_string(roll);
|
|
}
|
|
|
|
|
|
void CharacterController::clearStateAnimation(std::string &anim) const
|
|
{
|
|
if (anim.empty())
|
|
return;
|
|
if (mAnimation)
|
|
mAnimation->disable(anim);
|
|
anim.clear();
|
|
}
|
|
|
|
void CharacterController::resetCurrentJumpState()
|
|
{
|
|
clearStateAnimation(mCurrentJump);
|
|
mJumpState = JumpState_None;
|
|
}
|
|
|
|
void CharacterController::resetCurrentMovementState()
|
|
{
|
|
clearStateAnimation(mCurrentMovement);
|
|
mMovementState = CharState_None;
|
|
}
|
|
|
|
void CharacterController::resetCurrentIdleState()
|
|
{
|
|
clearStateAnimation(mCurrentIdle);
|
|
mIdleState = CharState_None;
|
|
}
|
|
|
|
void CharacterController::resetCurrentHitState()
|
|
{
|
|
clearStateAnimation(mCurrentHit);
|
|
mHitState = CharState_None;
|
|
}
|
|
|
|
void CharacterController::resetCurrentWeaponState()
|
|
{
|
|
clearStateAnimation(mCurrentWeapon);
|
|
mUpperBodyState = UpperCharState_Nothing;
|
|
}
|
|
|
|
void CharacterController::resetCurrentDeathState()
|
|
{
|
|
clearStateAnimation(mCurrentDeath);
|
|
mDeathState = CharState_None;
|
|
}
|
|
|
|
void CharacterController::refreshHitRecoilAnims()
|
|
{
|
|
auto& charClass = mPtr.getClass();
|
|
if (!charClass.isActor())
|
|
return;
|
|
const auto world = MWBase::Environment::get().getWorld();
|
|
auto& stats = charClass.getCreatureStats(mPtr);
|
|
bool knockout = stats.getFatigue().getCurrent() < 0 || stats.getFatigue().getBase() == 0;
|
|
bool recovery = stats.getHitRecovery();
|
|
bool knockdown = stats.getKnockedDown();
|
|
bool block = stats.getBlock();
|
|
bool isSwimming = world->isSwimming(mPtr);
|
|
|
|
if (mHitState != CharState_None)
|
|
{
|
|
if (!mAnimation->isPlaying(mCurrentHit))
|
|
{
|
|
mHitState = CharState_None;
|
|
mCurrentHit.clear();
|
|
stats.setKnockedDown(false);
|
|
stats.setHitRecovery(false);
|
|
stats.setBlock(false);
|
|
resetCurrentIdleState();
|
|
}
|
|
else if (isKnockedOut())
|
|
mAnimation->setLoopingEnabled(mCurrentHit, knockout);
|
|
return;
|
|
}
|
|
|
|
if (!knockout && !knockdown && !recovery && !block)
|
|
return;
|
|
|
|
MWRender::Animation::AnimPriority priority(Priority_Knockdown);
|
|
std::string startKey = "start";
|
|
std::string stopKey = "stop";
|
|
if (knockout)
|
|
{
|
|
mHitState = isSwimming ? CharState_SwimKnockOut : CharState_KnockOut;
|
|
stats.setKnockedDown(true);
|
|
}
|
|
else if (knockdown)
|
|
{
|
|
mHitState = isSwimming ? CharState_SwimKnockDown : CharState_KnockDown;
|
|
}
|
|
else if (recovery)
|
|
{
|
|
mHitState = isSwimming ? CharState_SwimHit : CharState_Hit;
|
|
priority = Priority_Hit;
|
|
}
|
|
else if (block)
|
|
{
|
|
mHitState = CharState_Block;
|
|
priority = Priority_Block;
|
|
priority[MWRender::Animation::BoneGroup_LeftArm] = Priority_Block;
|
|
priority[MWRender::Animation::BoneGroup_LowerBody] = Priority_WeaponLowerBody;
|
|
startKey = "block start";
|
|
stopKey = "block stop";
|
|
}
|
|
|
|
mCurrentHit = hitStateToAnimGroup(mHitState);
|
|
|
|
if (isRecovery())
|
|
{
|
|
mCurrentHit = chooseRandomGroup(mCurrentHit);
|
|
if (mHitState == CharState_SwimHit && !mAnimation->hasAnimation(mCurrentHit))
|
|
mCurrentHit = chooseRandomGroup(hitStateToAnimGroup(CharState_Hit));
|
|
}
|
|
|
|
if (!mAnimation->hasAnimation(mCurrentHit))
|
|
{
|
|
// The hit animation is missing. Reset the current hit state and immediately cancel all states as if the animation were instantaneous.
|
|
mHitState = CharState_None;
|
|
mCurrentHit.clear();
|
|
stats.setKnockedDown(false);
|
|
stats.setHitRecovery(false);
|
|
stats.setBlock(false);
|
|
resetCurrentIdleState();
|
|
return;
|
|
}
|
|
|
|
// Cancel upper body animations
|
|
if (isKnockedOut() || isKnockedDown())
|
|
{
|
|
if (!mCurrentWeapon.empty())
|
|
mAnimation->disable(mCurrentWeapon);
|
|
if (mUpperBodyState > UpperCharState_WeapEquiped)
|
|
{
|
|
mUpperBodyState = UpperCharState_WeapEquiped;
|
|
if (mWeaponType > ESM::Weapon::None)
|
|
mAnimation->showWeapons(true);
|
|
}
|
|
else if (mUpperBodyState < UpperCharState_WeapEquiped)
|
|
{
|
|
mUpperBodyState = UpperCharState_Nothing;
|
|
}
|
|
}
|
|
|
|
mAnimation->play(mCurrentHit, priority, MWRender::Animation::BlendMask_All, true, 1, startKey, stopKey, 0.0f, ~0ul);
|
|
}
|
|
|
|
void CharacterController::refreshJumpAnims(JumpingState jump, bool force)
|
|
{
|
|
if (!force && jump == mJumpState)
|
|
return;
|
|
|
|
if (jump == JumpState_None)
|
|
{
|
|
if (!mCurrentJump.empty())
|
|
resetCurrentIdleState();
|
|
resetCurrentJumpState();
|
|
return;
|
|
}
|
|
|
|
std::string weapShortGroup = getWeaponShortGroup(mWeaponType);
|
|
std::string jumpAnimName = "jump" + weapShortGroup;
|
|
MWRender::Animation::BlendMask jumpmask = MWRender::Animation::BlendMask_All;
|
|
if (!weapShortGroup.empty() && !mAnimation->hasAnimation(jumpAnimName))
|
|
jumpAnimName = fallbackShortWeaponGroup("jump", &jumpmask);
|
|
|
|
if (!mAnimation->hasAnimation(jumpAnimName))
|
|
{
|
|
if (!mCurrentJump.empty())
|
|
resetCurrentIdleState();
|
|
resetCurrentJumpState();
|
|
return;
|
|
}
|
|
|
|
bool startAtLoop = (jump == mJumpState);
|
|
mJumpState = jump;
|
|
clearStateAnimation(mCurrentJump);
|
|
|
|
mCurrentJump = jumpAnimName;
|
|
if(mJumpState == JumpState_InAir)
|
|
mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, false, 1.0f, startAtLoop ? "loop start" : "start", "stop", 0.f, ~0ul);
|
|
else if (mJumpState == JumpState_Landing)
|
|
mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, true, 1.0f, "loop stop", "stop", 0.0f, 0);
|
|
}
|
|
|
|
bool CharacterController::onOpen() const
|
|
{
|
|
if (mPtr.getType() == ESM::Container::sRecordId)
|
|
{
|
|
if (!mAnimation->hasAnimation("containeropen"))
|
|
return true;
|
|
|
|
if (mAnimation->isPlaying("containeropen"))
|
|
return false;
|
|
|
|
if (mAnimation->isPlaying("containerclose"))
|
|
return false;
|
|
|
|
mAnimation->play("containeropen", Priority_Persistent, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.f, 0);
|
|
if (mAnimation->isPlaying("containeropen"))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void CharacterController::onClose() const
|
|
{
|
|
if (mPtr.getType() == ESM::Container::sRecordId)
|
|
{
|
|
if (!mAnimation->hasAnimation("containerclose"))
|
|
return;
|
|
|
|
float complete, startPoint = 0.f;
|
|
bool animPlaying = mAnimation->getInfo("containeropen", &complete);
|
|
if (animPlaying)
|
|
startPoint = 1.f - complete;
|
|
|
|
mAnimation->play("containerclose", Priority_Persistent, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", startPoint, 0);
|
|
}
|
|
}
|
|
|
|
std::string CharacterController::getWeaponAnimation(int weaponType) const
|
|
{
|
|
std::string weaponGroup = getWeaponType(weaponType)->mLongGroup;
|
|
bool isRealWeapon = weaponType != ESM::Weapon::HandToHand && weaponType != ESM::Weapon::Spell && weaponType != ESM::Weapon::None;
|
|
if (isRealWeapon && !mAnimation->hasAnimation(weaponGroup))
|
|
{
|
|
static const std::string oneHandFallback = getWeaponType(ESM::Weapon::LongBladeOneHand)->mLongGroup;
|
|
static const std::string twoHandFallback = getWeaponType(ESM::Weapon::LongBladeTwoHand)->mLongGroup;
|
|
|
|
const ESM::WeaponType* weapInfo = getWeaponType(weaponType);
|
|
|
|
// For real two-handed melee weapons use 2h swords animations as fallback, otherwise use the 1h ones
|
|
if (weapInfo->mFlags & ESM::WeaponType::TwoHanded && weapInfo->mWeaponClass == ESM::WeaponType::Melee)
|
|
weaponGroup = twoHandFallback;
|
|
else if (isRealWeapon)
|
|
weaponGroup = oneHandFallback;
|
|
}
|
|
else if (weaponType == ESM::Weapon::HandToHand && !mPtr.getClass().isBipedal(mPtr))
|
|
return "attack1";
|
|
|
|
return weaponGroup;
|
|
}
|
|
|
|
std::string CharacterController::getWeaponShortGroup(int weaponType) const
|
|
{
|
|
if (weaponType == ESM::Weapon::HandToHand && !mPtr.getClass().isBipedal(mPtr))
|
|
return {};
|
|
return getWeaponType(weaponType)->mShortGroup;
|
|
}
|
|
|
|
std::string CharacterController::fallbackShortWeaponGroup(const std::string& baseGroupName, MWRender::Animation::BlendMask* blendMask) const
|
|
{
|
|
bool isRealWeapon = mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell && mWeaponType != ESM::Weapon::None;
|
|
if (!isRealWeapon)
|
|
{
|
|
if (blendMask != nullptr)
|
|
*blendMask = MWRender::Animation::BlendMask_LowerBody;
|
|
|
|
return baseGroupName;
|
|
}
|
|
|
|
static const std::string oneHandFallback = getWeaponShortGroup(ESM::Weapon::LongBladeOneHand);
|
|
static const std::string twoHandFallback = getWeaponShortGroup(ESM::Weapon::LongBladeTwoHand);
|
|
|
|
std::string groupName = baseGroupName;
|
|
const ESM::WeaponType* weapInfo = getWeaponType(mWeaponType);
|
|
|
|
// For real two-handed melee weapons use 2h swords animations as fallback, otherwise use the 1h ones
|
|
if (weapInfo->mFlags & ESM::WeaponType::TwoHanded && weapInfo->mWeaponClass == ESM::WeaponType::Melee)
|
|
groupName += twoHandFallback;
|
|
else
|
|
groupName += oneHandFallback;
|
|
|
|
// Special case for crossbows - we shouls apply 1h animations a fallback only for lower body
|
|
if (mWeaponType == ESM::Weapon::MarksmanCrossbow && blendMask != nullptr)
|
|
*blendMask = MWRender::Animation::BlendMask_LowerBody;
|
|
|
|
if (!mAnimation->hasAnimation(groupName))
|
|
{
|
|
groupName = baseGroupName;
|
|
if (blendMask != nullptr)
|
|
*blendMask = MWRender::Animation::BlendMask_LowerBody;
|
|
}
|
|
|
|
return groupName;
|
|
}
|
|
|
|
void CharacterController::refreshMovementAnims(CharacterState movement, bool force)
|
|
{
|
|
if (movement == mMovementState && !force)
|
|
return;
|
|
|
|
std::string movementAnimName = movementStateToAnimGroup(movement);
|
|
|
|
if (movementAnimName.empty())
|
|
{
|
|
if (!mCurrentMovement.empty())
|
|
resetCurrentIdleState();
|
|
resetCurrentMovementState();
|
|
return;
|
|
}
|
|
|
|
mMovementState = movement;
|
|
std::string::size_type swimpos = movementAnimName.find("swim");
|
|
if (!mAnimation->hasAnimation(movementAnimName))
|
|
{
|
|
if (swimpos != std::string::npos)
|
|
{
|
|
movementAnimName.erase(swimpos, 4);
|
|
swimpos = std::string::npos;
|
|
}
|
|
}
|
|
|
|
MWRender::Animation::BlendMask movemask = MWRender::Animation::BlendMask_All;
|
|
|
|
std::string weapShortGroup = getWeaponShortGroup(mWeaponType);
|
|
if (swimpos == std::string::npos && !weapShortGroup.empty())
|
|
{
|
|
std::string weapMovementAnimName;
|
|
// Spellcasting stance turning is a special case
|
|
if (mWeaponType == ESM::Weapon::Spell && isTurning())
|
|
weapMovementAnimName = weapShortGroup + movementAnimName;
|
|
else
|
|
weapMovementAnimName = movementAnimName + weapShortGroup;
|
|
|
|
if (!mAnimation->hasAnimation(weapMovementAnimName))
|
|
weapMovementAnimName = fallbackShortWeaponGroup(movementAnimName, &movemask);
|
|
|
|
movementAnimName = weapMovementAnimName;
|
|
}
|
|
|
|
if (!mAnimation->hasAnimation(movementAnimName))
|
|
{
|
|
std::string::size_type runpos = movementAnimName.find("run");
|
|
if (runpos != std::string::npos)
|
|
movementAnimName.replace(runpos, 3, "walk");
|
|
|
|
if (!mAnimation->hasAnimation(movementAnimName))
|
|
{
|
|
if (!mCurrentMovement.empty())
|
|
resetCurrentIdleState();
|
|
resetCurrentMovementState();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// If we're playing the same animation, start it from the point it ended
|
|
float startpoint = 0.f;
|
|
if (!mCurrentMovement.empty() && movementAnimName == mCurrentMovement)
|
|
mAnimation->getInfo(mCurrentMovement, &startpoint);
|
|
|
|
mMovementAnimationControlled = true;
|
|
|
|
clearStateAnimation(mCurrentMovement);
|
|
mCurrentMovement = movementAnimName;
|
|
|
|
// For non-flying creatures, MW uses the Walk animation to calculate the animation velocity
|
|
// even if we are running. This must be replicated, otherwise the observed speed would differ drastically.
|
|
mAdjustMovementAnimSpeed = true;
|
|
if (mPtr.getClass().getType() == ESM::Creature::sRecordId && !(mPtr.get<ESM::Creature>()->mBase->mFlags & ESM::Creature::Flies))
|
|
{
|
|
CharacterState walkState = runStateToWalkState(mMovementState);
|
|
std::string anim = movementStateToAnimGroup(walkState);
|
|
|
|
mMovementAnimSpeed = mAnimation->getVelocity(anim);
|
|
if (mMovementAnimSpeed <= 1.0f)
|
|
{
|
|
// Another bug: when using a fallback animation (e.g. RunForward as fallback to SwimRunForward),
|
|
// then the equivalent Walk animation will not use a fallback, and if that animation doesn't exist
|
|
// we will play without any scaling.
|
|
// Makes the speed attribute of most water creatures totally useless.
|
|
// And again, this can not be fixed without patching game data.
|
|
mAdjustMovementAnimSpeed = false;
|
|
mMovementAnimSpeed = 1.f;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mMovementAnimSpeed = mAnimation->getVelocity(mCurrentMovement);
|
|
|
|
if (mMovementAnimSpeed <= 1.0f)
|
|
{
|
|
// The first person anims don't have any velocity to calculate a speed multiplier from.
|
|
// We use the third person velocities instead.
|
|
// FIXME: should be pulled from the actual animation, but it is not presently loaded.
|
|
bool sneaking = mMovementState == CharState_SneakForward || mMovementState == CharState_SneakBack
|
|
|| mMovementState == CharState_SneakLeft || mMovementState == CharState_SneakRight;
|
|
mMovementAnimSpeed = (sneaking ? 33.5452f : (isRunning() ? 222.857f : 154.064f));
|
|
mMovementAnimationControlled = false;
|
|
}
|
|
}
|
|
|
|
mAnimation->play(mCurrentMovement, Priority_Movement, movemask, false, 1.f, "start", "stop", startpoint, ~0ul, true);
|
|
}
|
|
|
|
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),
|
|
// the idle animation should be displayed
|
|
if (((mUpperBodyState != UpperCharState_Nothing && mUpperBodyState != UpperCharState_WeapEquiped)
|
|
|| mMovementState != CharState_None || mHitState != CharState_None) && !mPtr.getClass().isBipedal(mPtr))
|
|
{
|
|
resetCurrentIdleState();
|
|
return;
|
|
}
|
|
|
|
if (!force && idle == mIdleState && (mAnimation->isPlaying(mCurrentIdle) || !mAnimQueue.empty()))
|
|
return;
|
|
|
|
mIdleState = idle;
|
|
|
|
std::string idleGroup = idleStateToAnimGroup(mIdleState);
|
|
if (idleGroup.empty())
|
|
{
|
|
resetCurrentIdleState();
|
|
return;
|
|
}
|
|
|
|
MWRender::Animation::AnimPriority priority = getIdlePriority(mIdleState);
|
|
size_t numLoops = std::numeric_limits<size_t>::max();
|
|
|
|
// Only play "idleswim" or "idlesneak" if they exist. Otherwise, fallback to
|
|
// "idle"+weapon or "idle".
|
|
bool fallback = mIdleState != CharState_Idle && !mAnimation->hasAnimation(idleGroup);
|
|
if (fallback)
|
|
{
|
|
priority = getIdlePriority(CharState_Idle);
|
|
idleGroup = idleStateToAnimGroup(CharState_Idle);
|
|
}
|
|
|
|
if (fallback || mIdleState == CharState_Idle || mIdleState == CharState_SpecialIdle)
|
|
{
|
|
std::string weapShortGroup = getWeaponShortGroup(mWeaponType);
|
|
if (!weapShortGroup.empty())
|
|
{
|
|
std::string weapIdleGroup = idleGroup + weapShortGroup;
|
|
if (!mAnimation->hasAnimation(weapIdleGroup))
|
|
weapIdleGroup = fallbackShortWeaponGroup(idleGroup);
|
|
idleGroup = weapIdleGroup;
|
|
|
|
// 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
|
|
auto& prng = MWBase::Environment::get().getWorld()->getPrng();
|
|
numLoops = 1 + Misc::Rng::rollDice(4, prng);
|
|
}
|
|
}
|
|
|
|
if (!mAnimation->hasAnimation(idleGroup))
|
|
{
|
|
resetCurrentIdleState();
|
|
return;
|
|
}
|
|
|
|
float startPoint = 0.f;
|
|
// There is no need to restart anim if the new and old anims are the same.
|
|
// Just update the number of loops.
|
|
if (mCurrentIdle == idleGroup)
|
|
mAnimation->getInfo(mCurrentIdle, &startPoint);
|
|
|
|
clearStateAnimation(mCurrentIdle);
|
|
mCurrentIdle = idleGroup;
|
|
mAnimation->play(mCurrentIdle, priority, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", startPoint, numLoops, true);
|
|
}
|
|
|
|
void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterState movement, JumpingState jump, bool force)
|
|
{
|
|
// If the current animation is persistent, do not touch it
|
|
if (isPersistentAnimPlaying())
|
|
return;
|
|
|
|
refreshHitRecoilAnims();
|
|
refreshJumpAnims(jump, force);
|
|
refreshMovementAnims(movement, force);
|
|
|
|
// idle handled last as it can depend on the other states
|
|
refreshIdleAnims(idle, force);
|
|
}
|
|
|
|
void CharacterController::playDeath(float startpoint, CharacterState death)
|
|
{
|
|
mDeathState = death;
|
|
mCurrentDeath = deathStateToAnimGroup(mDeathState);
|
|
|
|
// Make sure the character was swimming upon death for forward-compatibility
|
|
if (!MWBase::Environment::get().getWorld()->isSwimming(mPtr))
|
|
{
|
|
if (mDeathState == CharState_SwimDeathKnockDown)
|
|
mCurrentDeath = "deathknockdown";
|
|
else if (mDeathState == CharState_SwimDeathKnockOut)
|
|
mCurrentDeath = "deathknockout";
|
|
}
|
|
|
|
mPtr.getClass().getCreatureStats(mPtr).setDeathAnimation(mDeathState - CharState_Death1);
|
|
|
|
// For dead actors, refreshCurrentAnims is no longer called, so we need to disable the movement state manually.
|
|
// Note that these animations wouldn't actually be visible (due to the Death animation's priority being higher).
|
|
// However, they could still trigger text keys, such as Hit events, or sounds.
|
|
resetCurrentMovementState();
|
|
resetCurrentWeaponState();
|
|
resetCurrentHitState();
|
|
resetCurrentIdleState();
|
|
resetCurrentJumpState();
|
|
mMovementAnimationControlled = true;
|
|
|
|
mAnimation->play(mCurrentDeath, Priority_Death, MWRender::Animation::BlendMask_All,
|
|
false, 1.0f, "start", "stop", startpoint, 0);
|
|
}
|
|
|
|
CharacterState CharacterController::chooseRandomDeathState() const
|
|
{
|
|
int selected=0;
|
|
chooseRandomGroup("death", &selected);
|
|
return static_cast<CharacterState>(CharState_Death1 + (selected-1));
|
|
}
|
|
|
|
void CharacterController::playRandomDeath(float startpoint)
|
|
{
|
|
if (mPtr == getPlayer())
|
|
{
|
|
// The first-person animations do not include death, so we need to
|
|
// force-switch to third person before playing the death animation.
|
|
MWBase::Environment::get().getWorld()->useDeathCamera();
|
|
}
|
|
|
|
mDeathState = hitStateToDeathState(mHitState);
|
|
if (mDeathState == CharState_None && MWBase::Environment::get().getWorld()->isSwimming(mPtr))
|
|
mDeathState = CharState_SwimDeath;
|
|
|
|
if (mDeathState == CharState_None || !mAnimation->hasAnimation(deathStateToAnimGroup(mDeathState)))
|
|
mDeathState = chooseRandomDeathState();
|
|
|
|
// Do not interrupt scripted animation by death
|
|
if (isPersistentAnimPlaying())
|
|
return;
|
|
|
|
playDeath(startpoint, mDeathState);
|
|
}
|
|
|
|
std::string CharacterController::chooseRandomAttackAnimation() const
|
|
{
|
|
std::string result;
|
|
bool isSwimming = MWBase::Environment::get().getWorld()->isSwimming(mPtr);
|
|
|
|
if (isSwimming)
|
|
result = chooseRandomGroup("swimattack");
|
|
|
|
if (!isSwimming || !mAnimation->hasAnimation(result))
|
|
result = chooseRandomGroup("attack");
|
|
|
|
return result;
|
|
}
|
|
|
|
CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Animation *anim)
|
|
: mPtr(ptr)
|
|
, mAnimation(anim)
|
|
{
|
|
if(!mAnimation)
|
|
return;
|
|
|
|
mAnimation->setTextKeyListener(this);
|
|
|
|
const MWWorld::Class &cls = mPtr.getClass();
|
|
if(cls.isActor())
|
|
{
|
|
/* Accumulate along X/Y only for now, until we can figure out how we should
|
|
* handle knockout and death which moves the character down. */
|
|
mAnimation->setAccumulation(osg::Vec3f(1.0f, 1.0f, 0.0f));
|
|
|
|
if (cls.hasInventoryStore(mPtr))
|
|
{
|
|
getActiveWeapon(mPtr, &mWeaponType);
|
|
if (mWeaponType != ESM::Weapon::None)
|
|
{
|
|
mUpperBodyState = UpperCharState_WeapEquiped;
|
|
mCurrentWeapon = getWeaponAnimation(mWeaponType);
|
|
}
|
|
|
|
if(mWeaponType != ESM::Weapon::None && mWeaponType != ESM::Weapon::Spell && mWeaponType != ESM::Weapon::HandToHand)
|
|
{
|
|
mAnimation->showWeapons(true);
|
|
// Note: controllers for ranged weapon should use time for beginning of animation to play shooting properly,
|
|
// for other weapons they should use absolute time. Some mods rely on this behaviour (to rotate throwing projectiles, for example)
|
|
ESM::WeaponType::Class weaponClass = getWeaponType(mWeaponType)->mWeaponClass;
|
|
bool useRelativeDuration = weaponClass == ESM::WeaponType::Ranged;
|
|
mAnimation->setWeaponGroup(mCurrentWeapon, useRelativeDuration);
|
|
}
|
|
|
|
mAnimation->showCarriedLeft(updateCarriedLeftVisible(mWeaponType));
|
|
}
|
|
|
|
if(!cls.getCreatureStats(mPtr).isDead())
|
|
{
|
|
mIdleState = CharState_Idle;
|
|
if (cls.getCreatureStats(mPtr).getFallHeight() > 0)
|
|
mJumpState = JumpState_InAir;
|
|
}
|
|
else
|
|
{
|
|
const MWMechanics::CreatureStats& cStats = mPtr.getClass().getCreatureStats(mPtr);
|
|
if (cStats.isDeathAnimationFinished())
|
|
{
|
|
// Set the death state, but don't play it yet
|
|
// We will play it in the first frame, but only if no script set the skipAnim flag
|
|
signed char deathanim = cStats.getDeathAnimation();
|
|
if (deathanim == -1)
|
|
mDeathState = chooseRandomDeathState();
|
|
else
|
|
mDeathState = static_cast<CharacterState>(CharState_Death1 + deathanim);
|
|
|
|
mFloatToSurface = false;
|
|
}
|
|
// else: nothing to do, will detect death in the next frame and start playing death animation
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* Don't accumulate with non-actors. */
|
|
mAnimation->setAccumulation(osg::Vec3f(0.f, 0.f, 0.f));
|
|
}
|
|
|
|
// Update animation status for living actors
|
|
if (mDeathState == CharState_None && cls.isActor() && !cls.getCreatureStats(mPtr).isDead())
|
|
refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true);
|
|
|
|
mAnimation->runAnimation(0.f);
|
|
|
|
unpersistAnimationState();
|
|
}
|
|
|
|
CharacterController::~CharacterController()
|
|
{
|
|
if (mAnimation)
|
|
{
|
|
persistAnimationState();
|
|
mAnimation->setTextKeyListener(nullptr);
|
|
}
|
|
}
|
|
|
|
void CharacterController::handleTextKey(std::string_view groupname, SceneUtil::TextKeyMap::ConstIterator key, const SceneUtil::TextKeyMap& map)
|
|
{
|
|
std::string_view evt = key->second;
|
|
|
|
if (evt.substr(0, 7) == "sound: ")
|
|
{
|
|
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
|
|
sndMgr->playSound3D(mPtr, evt.substr(7), 1.0f, 1.0f);
|
|
return;
|
|
}
|
|
|
|
auto& charClass = mPtr.getClass();
|
|
if (evt.substr(0, 10) == "soundgen: ")
|
|
{
|
|
std::string soundgen = std::string(evt.substr(10));
|
|
|
|
// The event can optionally contain volume and pitch modifiers
|
|
float volume=1.f, pitch=1.f;
|
|
if (soundgen.find(' ') != std::string::npos)
|
|
{
|
|
std::vector<std::string> tokens;
|
|
Misc::StringUtils::split(soundgen, tokens);
|
|
soundgen = tokens[0];
|
|
if (tokens.size() >= 2)
|
|
{
|
|
std::stringstream stream;
|
|
stream << tokens[1];
|
|
stream >> volume;
|
|
}
|
|
if (tokens.size() >= 3)
|
|
{
|
|
std::stringstream stream;
|
|
stream << tokens[2];
|
|
stream >> pitch;
|
|
}
|
|
}
|
|
|
|
std::string sound = charClass.getSoundIdFromSndGen(mPtr, soundgen);
|
|
if(!sound.empty())
|
|
{
|
|
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
|
|
if (soundgen == "left" || soundgen == "right")
|
|
{
|
|
sndMgr->playSound3D(mPtr, sound, volume, pitch, MWSound::Type::Foot,
|
|
MWSound::PlayMode::NoPlayerLocal);
|
|
}
|
|
else
|
|
{
|
|
sndMgr->playSound3D(mPtr, sound, volume, pitch);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (evt.substr(0, groupname.size()) != groupname || evt.substr(groupname.size(), 2) != ": ")
|
|
{
|
|
// Not ours, skip it
|
|
return;
|
|
}
|
|
|
|
std::string_view action = evt.substr(groupname.size() + 2);
|
|
if (action == "equip attach")
|
|
{
|
|
if (groupname == "shield")
|
|
mAnimation->showCarriedLeft(true);
|
|
else
|
|
mAnimation->showWeapons(true);
|
|
}
|
|
else if (action == "unequip detach")
|
|
{
|
|
if (groupname == "shield")
|
|
mAnimation->showCarriedLeft(false);
|
|
else
|
|
mAnimation->showWeapons(false);
|
|
}
|
|
else if (action == "chop hit")
|
|
charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop);
|
|
else if (action == "slash hit")
|
|
charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash);
|
|
else if (action == "thrust hit")
|
|
charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust);
|
|
else if (action == "hit")
|
|
{
|
|
if (groupname == "attack1" || groupname == "swimattack1")
|
|
charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop);
|
|
else if (groupname == "attack2" || groupname == "swimattack2")
|
|
charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash);
|
|
else if (groupname == "attack3" || groupname == "swimattack3")
|
|
charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust);
|
|
else
|
|
charClass.hit(mPtr, mAttackStrength);
|
|
}
|
|
else if (isRandomAttackAnimation(groupname) && action == "start")
|
|
{
|
|
std::multimap<float, std::string>::const_iterator hitKey = key;
|
|
|
|
std::string hitKeyName = std::string(groupname) + ": hit";
|
|
std::string stopKeyName = std::string(groupname) + ": stop";
|
|
// Not all animations have a hit key defined. If there is none, the hit happens with the start key.
|
|
bool hasHitKey = false;
|
|
while (hitKey != map.end())
|
|
{
|
|
if (hitKey->second == hitKeyName)
|
|
{
|
|
hasHitKey = true;
|
|
break;
|
|
}
|
|
if (hitKey->second == stopKeyName)
|
|
break;
|
|
++hitKey;
|
|
}
|
|
if (!hasHitKey)
|
|
{
|
|
if (groupname == "attack1" || groupname == "swimattack1")
|
|
charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop);
|
|
else if (groupname == "attack2" || groupname == "swimattack2")
|
|
charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash);
|
|
else if (groupname == "attack3" || groupname == "swimattack3")
|
|
charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust);
|
|
}
|
|
}
|
|
else if (action == "shoot attach")
|
|
mAnimation->attachArrow();
|
|
else if (action == "shoot release")
|
|
mAnimation->releaseArrow(mAttackStrength);
|
|
else if (action == "shoot follow attach")
|
|
mAnimation->attachArrow();
|
|
// Make sure this key is actually for the RangeType we are casting. The flame atronach has
|
|
// the same animation for all range types, so there are 3 "release" keys on the same time, one for each range type.
|
|
else if (groupname == "spellcast" && action == mAttackType + " release")
|
|
{
|
|
MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell);
|
|
mCastingManualSpell = false;
|
|
}
|
|
else if (groupname == "shield" && action == "block hit")
|
|
charClass.block(mPtr);
|
|
else if (groupname == "containeropen" && action == "loot")
|
|
MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Container, mPtr);
|
|
}
|
|
|
|
void CharacterController::updatePtr(const MWWorld::Ptr &ptr)
|
|
{
|
|
mPtr = ptr;
|
|
}
|
|
|
|
void CharacterController::updateIdleStormState(bool inwater) const
|
|
{
|
|
if (!mAnimation->hasAnimation("idlestorm") || mUpperBodyState != UpperCharState_Nothing || inwater)
|
|
{
|
|
mAnimation->disable("idlestorm");
|
|
return;
|
|
}
|
|
|
|
const auto world = MWBase::Environment::get().getWorld();
|
|
if (world->isInStorm())
|
|
{
|
|
osg::Vec3f stormDirection = world->getStormDirection();
|
|
osg::Vec3f characterDirection = mPtr.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0);
|
|
stormDirection.normalize();
|
|
characterDirection.normalize();
|
|
if (stormDirection * characterDirection < -0.5f)
|
|
{
|
|
if (!mAnimation->isPlaying("idlestorm"))
|
|
{
|
|
int mask = MWRender::Animation::BlendMask_Torso | MWRender::Animation::BlendMask_RightArm;
|
|
mAnimation->play("idlestorm", Priority_Storm, mask, true, 1.0f, "start", "stop", 0.0f, ~0ul);
|
|
}
|
|
else
|
|
{
|
|
mAnimation->setLoopingEnabled("idlestorm", true);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (mAnimation->isPlaying("idlestorm"))
|
|
{
|
|
mAnimation->setLoopingEnabled("idlestorm", false);
|
|
}
|
|
}
|
|
|
|
bool CharacterController::updateCarriedLeftVisible(const int weaptype) const
|
|
{
|
|
// Shields/torches shouldn't be visible during any operation involving two hands
|
|
// There seems to be no text keys for this purpose, except maybe for "[un]equip start/stop",
|
|
// but they are also present in weapon drawing animation.
|
|
return mAnimation->updateCarriedLeftVisible(weaptype);
|
|
}
|
|
|
|
bool CharacterController::updateState(CharacterState idle)
|
|
{
|
|
const auto world = MWBase::Environment::get().getWorld();
|
|
auto& prng = world->getPrng();
|
|
MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager();
|
|
|
|
const MWWorld::Class &cls = mPtr.getClass();
|
|
CreatureStats &stats = cls.getCreatureStats(mPtr);
|
|
int weaptype = ESM::Weapon::None;
|
|
if(stats.getDrawState() == DrawState_Weapon)
|
|
weaptype = ESM::Weapon::HandToHand;
|
|
else if (stats.getDrawState() == DrawState_Spell)
|
|
weaptype = ESM::Weapon::Spell;
|
|
|
|
const bool isWerewolf = cls.isNpc() && cls.getNpcStats(mPtr).isWerewolf();
|
|
|
|
std::string upSoundId;
|
|
std::string downSoundId;
|
|
bool weaponChanged = false;
|
|
if (cls.hasInventoryStore(mPtr))
|
|
{
|
|
MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr);
|
|
MWWorld::ContainerStoreIterator weapon = getActiveWeapon(mPtr, &weaptype);
|
|
if(stats.getDrawState() == DrawState_Spell)
|
|
weapon = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight);
|
|
|
|
if(weapon != inv.end() && mWeaponType != ESM::Weapon::HandToHand && weaptype != ESM::Weapon::HandToHand && weaptype != ESM::Weapon::Spell && weaptype != ESM::Weapon::None)
|
|
upSoundId = weapon->getClass().getUpSoundId(*weapon);
|
|
|
|
if(weapon != inv.end() && mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell && mWeaponType != ESM::Weapon::None)
|
|
downSoundId = weapon->getClass().getDownSoundId(*weapon);
|
|
|
|
// weapon->HtH switch: weapon is empty already, so we need to take sound from previous weapon
|
|
if(weapon == inv.end() && !mWeapon.isEmpty() && weaptype == ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell)
|
|
downSoundId = mWeapon.getClass().getDownSoundId(mWeapon);
|
|
|
|
MWWorld::Ptr newWeapon = weapon != inv.end() ? *weapon : MWWorld::Ptr();
|
|
|
|
if (mWeapon != newWeapon)
|
|
{
|
|
mWeapon = newWeapon;
|
|
weaponChanged = true;
|
|
}
|
|
}
|
|
|
|
// For biped actors, blend weapon animations with lower body animations with higher priority
|
|
MWRender::Animation::AnimPriority priorityWeapon(Priority_Weapon);
|
|
if (cls.isBipedal(mPtr))
|
|
priorityWeapon[MWRender::Animation::BoneGroup_LowerBody] = Priority_WeaponLowerBody;
|
|
|
|
bool forcestateupdate = false;
|
|
|
|
// 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 &&
|
|
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),
|
|
// we should force actor to the "weapon equipped" state, interrupt attack and update animations.
|
|
if (isStillWeapon && mWeaponType != weaptype && mUpperBodyState > UpperCharState_WeapEquiped)
|
|
{
|
|
forcestateupdate = true;
|
|
if (!mCurrentWeapon.empty())
|
|
mAnimation->disable(mCurrentWeapon);
|
|
mUpperBodyState = UpperCharState_WeapEquiped;
|
|
setAttackingOrSpell(false);
|
|
mAnimation->showWeapons(true);
|
|
stats.setAttackingOrSpell(false);
|
|
}
|
|
|
|
if(!isKnockedOut() && !isKnockedDown() && !isRecovery())
|
|
{
|
|
std::string weapgroup;
|
|
if ((!isWerewolf || mWeaponType != ESM::Weapon::Spell)
|
|
&& weaptype != mWeaponType
|
|
&& mUpperBodyState != UpperCharState_UnEquipingWeap
|
|
&& !isStillWeapon)
|
|
{
|
|
// We can not play un-equip animation if weapon changed since last update
|
|
if (!weaponChanged)
|
|
{
|
|
// Note: we do not disable unequipping animation automatically to avoid body desync
|
|
weapgroup = getWeaponAnimation(mWeaponType);
|
|
int unequipMask = MWRender::Animation::BlendMask_All;
|
|
bool useShieldAnims = mAnimation->useShieldAnimations();
|
|
if (useShieldAnims && mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell && !(mWeaponType == ESM::Weapon::None && weaptype == ESM::Weapon::Spell))
|
|
{
|
|
unequipMask = unequipMask |~MWRender::Animation::BlendMask_LeftArm;
|
|
mAnimation->play("shield", Priority_Block,
|
|
MWRender::Animation::BlendMask_LeftArm, true,
|
|
1.0f, "unequip start", "unequip stop", 0.0f, 0);
|
|
}
|
|
else if (mWeaponType == ESM::Weapon::HandToHand)
|
|
mAnimation->showCarriedLeft(false);
|
|
|
|
mAnimation->play(weapgroup, priorityWeapon, unequipMask, false,
|
|
1.0f, "unequip start", "unequip stop", 0.0f, 0);
|
|
mUpperBodyState = UpperCharState_UnEquipingWeap;
|
|
|
|
mAnimation->detachArrow();
|
|
|
|
// If we do not have the "unequip detach" key, hide weapon manually.
|
|
if (mAnimation->getTextKeyTime(weapgroup+": unequip detach") < 0)
|
|
mAnimation->showWeapons(false);
|
|
}
|
|
|
|
if(!downSoundId.empty())
|
|
{
|
|
sndMgr->playSound3D(mPtr, downSoundId, 1.0f, 1.0f);
|
|
}
|
|
}
|
|
|
|
float complete;
|
|
bool animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete);
|
|
if (!animPlaying || complete >= 1.0f)
|
|
{
|
|
// Weapon is changed, no current animation (e.g. unequipping or attack).
|
|
// Start equipping animation now.
|
|
if (weaptype != mWeaponType)
|
|
{
|
|
forcestateupdate = true;
|
|
bool useShieldAnims = mAnimation->useShieldAnimations();
|
|
if (!useShieldAnims)
|
|
mAnimation->showCarriedLeft(updateCarriedLeftVisible(weaptype));
|
|
|
|
weapgroup = getWeaponAnimation(weaptype);
|
|
|
|
// Note: controllers for ranged weapon should use time for beginning of animation to play shooting properly,
|
|
// for other weapons they should use absolute time. Some mods rely on this behaviour (to rotate throwing projectiles, for example)
|
|
ESM::WeaponType::Class weaponClass = getWeaponType(weaptype)->mWeaponClass;
|
|
bool useRelativeDuration = weaponClass == ESM::WeaponType::Ranged;
|
|
mAnimation->setWeaponGroup(weapgroup, useRelativeDuration);
|
|
|
|
if (!isStillWeapon)
|
|
{
|
|
clearStateAnimation(mCurrentWeapon);
|
|
if (weaptype != ESM::Weapon::None)
|
|
{
|
|
mAnimation->showWeapons(false);
|
|
int equipMask = MWRender::Animation::BlendMask_All;
|
|
if (useShieldAnims && weaptype != ESM::Weapon::Spell)
|
|
{
|
|
equipMask = equipMask |~MWRender::Animation::BlendMask_LeftArm;
|
|
mAnimation->play("shield", Priority_Block,
|
|
MWRender::Animation::BlendMask_LeftArm, true,
|
|
1.0f, "equip start", "equip stop", 0.0f, 0);
|
|
}
|
|
|
|
mAnimation->play(weapgroup, priorityWeapon, equipMask, true,
|
|
1.0f, "equip start", "equip stop", 0.0f, 0);
|
|
mUpperBodyState = UpperCharState_EquipingWeap;
|
|
|
|
// If we do not have the "equip attach" key, show weapon manually.
|
|
if (weaptype != ESM::Weapon::Spell)
|
|
{
|
|
if (mAnimation->getTextKeyTime(weapgroup+": equip attach") < 0)
|
|
mAnimation->showWeapons(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(isWerewolf)
|
|
{
|
|
const MWWorld::ESMStore &store = world->getStore();
|
|
const ESM::Sound *sound = store.get<ESM::Sound>().searchRandom("WolfEquip", prng);
|
|
if(sound)
|
|
{
|
|
sndMgr->playSound3D(mPtr, sound->mId, 1.0f, 1.0f);
|
|
}
|
|
}
|
|
|
|
mWeaponType = weaptype;
|
|
mCurrentWeapon = weapgroup;
|
|
|
|
if(!upSoundId.empty() && !isStillWeapon)
|
|
{
|
|
sndMgr->playSound3D(mPtr, upSoundId, 1.0f, 1.0f);
|
|
}
|
|
}
|
|
|
|
// Make sure that we disabled unequipping animation
|
|
if (mUpperBodyState == UpperCharState_UnEquipingWeap)
|
|
{
|
|
resetCurrentWeaponState();
|
|
mWeaponType = ESM::Weapon::None;
|
|
mCurrentWeapon = getWeaponAnimation(mWeaponType);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(isWerewolf)
|
|
{
|
|
if(stats.getStance(MWMechanics::CreatureStats::Stance_Run)
|
|
&& mHasMovedInXY
|
|
&& !world->isSwimming(mPtr)
|
|
&& mWeaponType == ESM::Weapon::None)
|
|
{
|
|
if(!sndMgr->getSoundPlaying(mPtr, "WolfRun"))
|
|
sndMgr->playSound3D(mPtr, "WolfRun", 1.0f, 1.0f, MWSound::Type::Sfx,
|
|
MWSound::PlayMode::Loop);
|
|
}
|
|
else
|
|
sndMgr->stopSound3D(mPtr, "WolfRun");
|
|
}
|
|
|
|
// Cancel attack if we no longer have ammunition
|
|
bool ammunition = true;
|
|
bool isWeapon = false;
|
|
float weapSpeed = 1.f;
|
|
if (cls.hasInventoryStore(mPtr))
|
|
{
|
|
MWWorld::InventoryStore &inv = cls.getInventoryStore(mPtr);
|
|
MWWorld::ConstContainerStoreIterator weapon = getActiveWeapon(mPtr, &weaptype);
|
|
isWeapon = (weapon != inv.end() && weapon->getType() == ESM::Weapon::sRecordId);
|
|
if (isWeapon)
|
|
{
|
|
weapSpeed = weapon->get<ESM::Weapon>()->mBase->mData.mSpeed;
|
|
MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition);
|
|
int ammotype = getWeaponType(weapon->get<ESM::Weapon>()->mBase->mData.mType)->mAmmoType;
|
|
if (ammotype != ESM::Weapon::None && (ammo == inv.end() || ammo->get<ESM::Weapon>()->mBase->mData.mType != ammotype))
|
|
ammunition = false;
|
|
}
|
|
|
|
if (!ammunition && mUpperBodyState > UpperCharState_WeapEquiped)
|
|
{
|
|
if (!mCurrentWeapon.empty())
|
|
mAnimation->disable(mCurrentWeapon);
|
|
mUpperBodyState = UpperCharState_WeapEquiped;
|
|
}
|
|
}
|
|
|
|
// Combat for actors with persistent animations obviously will be buggy
|
|
if (isPersistentAnimPlaying())
|
|
return forcestateupdate;
|
|
|
|
float complete;
|
|
bool animPlaying;
|
|
ESM::WeaponType::Class weapclass = getWeaponType(mWeaponType)->mWeaponClass;
|
|
if(getAttackingOrSpell())
|
|
{
|
|
bool resetIdle = ammunition;
|
|
if(mUpperBodyState == UpperCharState_WeapEquiped && (mHitState == CharState_None || mHitState == CharState_Block))
|
|
{
|
|
world->breakInvisibility(mPtr);
|
|
mAttackStrength = 0;
|
|
|
|
// Randomize attacks for non-bipedal creatures
|
|
if (cls.getType() == ESM::Creature::sRecordId &&
|
|
!cls.isBipedal(mPtr) &&
|
|
(!mAnimation->hasAnimation(mCurrentWeapon) || isRandomAttackAnimation(mCurrentWeapon)))
|
|
{
|
|
mCurrentWeapon = chooseRandomAttackAnimation();
|
|
}
|
|
|
|
if(mWeaponType == ESM::Weapon::Spell)
|
|
{
|
|
// Unset casting flag, otherwise pressing the mouse button down would
|
|
// continue casting every frame if there is no animation
|
|
setAttackingOrSpell(false);
|
|
if (mPtr == getPlayer())
|
|
{
|
|
// 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);
|
|
}
|
|
std::string spellid = stats.getSpells().getSelectedSpell();
|
|
bool isMagicItem = false;
|
|
bool canCast = mCastingManualSpell || world->startSpellCast(mPtr);
|
|
|
|
if (spellid.empty())
|
|
{
|
|
if (cls.hasInventoryStore(mPtr))
|
|
{
|
|
MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr);
|
|
if (inv.getSelectedEnchantItem() != inv.end())
|
|
{
|
|
const MWWorld::Ptr& enchantItem = *inv.getSelectedEnchantItem();
|
|
spellid = enchantItem.getClass().getEnchantment(enchantItem);
|
|
isMagicItem = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
static const bool useCastingAnimations = Settings::Manager::getBool("use magic item animations", "Game");
|
|
if (isMagicItem && !useCastingAnimations)
|
|
{
|
|
// Enchanted items by default do not use casting animations
|
|
world->castSpell(mPtr);
|
|
resetIdle = false;
|
|
}
|
|
else if(!spellid.empty() && canCast)
|
|
{
|
|
MWMechanics::CastSpell cast(mPtr, nullptr, false, mCastingManualSpell);
|
|
cast.playSpellCastingEffects(spellid, isMagicItem);
|
|
|
|
std::vector<ESM::ENAMstruct> effects;
|
|
const MWWorld::ESMStore &store = world->getStore();
|
|
if (isMagicItem)
|
|
{
|
|
const ESM::Enchantment *enchantment = store.get<ESM::Enchantment>().find(spellid);
|
|
effects = enchantment->mEffects.mList;
|
|
}
|
|
else
|
|
{
|
|
const ESM::Spell *spell = store.get<ESM::Spell>().find(spellid);
|
|
effects = spell->mEffects.mList;
|
|
}
|
|
|
|
const ESM::MagicEffect *effect = store.get<ESM::MagicEffect>().find(effects.back().mEffectID); // use last effect of list for color of VFX_Hands
|
|
|
|
const ESM::Static* castStatic = world->getStore().get<ESM::Static>().find ("VFX_Hands");
|
|
|
|
const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS();
|
|
|
|
for (size_t iter = 0; iter < effects.size(); ++iter) // play hands vfx for each effect
|
|
{
|
|
if (mAnimation->getNode("Bip01 L Hand"))
|
|
mAnimation->addEffect(
|
|
Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs),
|
|
-1, false, "Bip01 L Hand", effect->mParticle);
|
|
|
|
if (mAnimation->getNode("Bip01 R Hand"))
|
|
mAnimation->addEffect(
|
|
Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs),
|
|
-1, false, "Bip01 R Hand", effect->mParticle);
|
|
}
|
|
|
|
const ESM::ENAMstruct &firstEffect = effects.at(0); // first effect used for casting animation
|
|
|
|
std::string startKey;
|
|
std::string stopKey;
|
|
if (isRandomAttackAnimation(mCurrentWeapon))
|
|
{
|
|
startKey = "start";
|
|
stopKey = "stop";
|
|
world->castSpell(mPtr, mCastingManualSpell); // No "release" text key to use, so cast immediately
|
|
mCastingManualSpell = false;
|
|
}
|
|
else
|
|
{
|
|
switch(firstEffect.mRange)
|
|
{
|
|
case 0: mAttackType = "self"; break;
|
|
case 1: mAttackType = "touch"; break;
|
|
case 2: mAttackType = "target"; break;
|
|
}
|
|
|
|
startKey = mAttackType+" start";
|
|
stopKey = mAttackType+" stop";
|
|
}
|
|
|
|
mAnimation->play(mCurrentWeapon, priorityWeapon,
|
|
MWRender::Animation::BlendMask_All, true,
|
|
1, startKey, stopKey,
|
|
0.0f, 0);
|
|
mUpperBodyState = UpperCharState_CastingSpell;
|
|
}
|
|
else
|
|
{
|
|
resetIdle = false;
|
|
}
|
|
}
|
|
else if(mWeaponType == ESM::Weapon::PickProbe)
|
|
{
|
|
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.
|
|
MWWorld::Ptr target = world->getFacedObject();
|
|
std::string resultMessage, resultSound;
|
|
|
|
if(!target.isEmpty())
|
|
{
|
|
if(item.getType() == ESM::Lockpick::sRecordId)
|
|
Security(mPtr).pickLock(target, item, resultMessage, resultSound);
|
|
else if(item.getType() == ESM::Probe::sRecordId)
|
|
Security(mPtr).probeTrap(target, item, resultMessage, resultSound);
|
|
}
|
|
mAnimation->play(mCurrentWeapon, priorityWeapon,
|
|
MWRender::Animation::BlendMask_All, true,
|
|
1.0f, "start", "stop", 0.0, 0);
|
|
mUpperBodyState = UpperCharState_FollowStartToFollowStop;
|
|
|
|
if(!resultMessage.empty())
|
|
MWBase::Environment::get().getWindowManager()->messageBox(resultMessage);
|
|
if(!resultSound.empty())
|
|
sndMgr->playSound3D(target, resultSound, 1.0f, 1.0f);
|
|
}
|
|
else if (ammunition)
|
|
{
|
|
std::string startKey;
|
|
std::string stopKey;
|
|
|
|
if(weapclass == ESM::WeaponType::Ranged || weapclass == ESM::WeaponType::Thrown)
|
|
{
|
|
mAttackType = "shoot";
|
|
startKey = mAttackType+" start";
|
|
stopKey = mAttackType+" min attack";
|
|
}
|
|
else if (isRandomAttackAnimation(mCurrentWeapon))
|
|
{
|
|
startKey = "start";
|
|
stopKey = "stop";
|
|
}
|
|
else
|
|
{
|
|
if(mPtr == getPlayer())
|
|
{
|
|
if (Settings::Manager::getBool("best attack", "Game"))
|
|
{
|
|
if (isWeapon)
|
|
{
|
|
MWWorld::ConstContainerStoreIterator weapon = cls.getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight);
|
|
mAttackType = getBestAttack(weapon->get<ESM::Weapon>()->mBase);
|
|
}
|
|
else
|
|
{
|
|
// There is no "best attack" for Hand-to-Hand
|
|
mAttackType = getRandomAttackType();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
mAttackType = getMovementBasedAttackType();
|
|
}
|
|
}
|
|
// else if (mPtr != getPlayer()) use mAttackType set by AiCombat
|
|
startKey = mAttackType+" start";
|
|
stopKey = mAttackType+" min attack";
|
|
}
|
|
|
|
mAnimation->play(mCurrentWeapon, priorityWeapon,
|
|
MWRender::Animation::BlendMask_All, false,
|
|
weapSpeed, startKey, stopKey,
|
|
0.0f, 0);
|
|
if(mAnimation->getCurrentTime(mCurrentWeapon) != -1.f)
|
|
{
|
|
mUpperBodyState = UpperCharState_StartToMinAttack;
|
|
if (isRandomAttackAnimation(mCurrentWeapon))
|
|
{
|
|
mAttackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability(prng));
|
|
playSwishSound(0.0f);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// We should not break swim and sneak animations
|
|
if (resetIdle &&
|
|
idle != CharState_IdleSneak && idle != CharState_IdleSwim &&
|
|
mIdleState != CharState_IdleSneak && mIdleState != CharState_IdleSwim)
|
|
{
|
|
resetCurrentIdleState();
|
|
}
|
|
|
|
animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete);
|
|
if(mUpperBodyState == UpperCharState_MinAttackToMaxAttack && !isKnockedDown())
|
|
mAttackStrength = complete;
|
|
}
|
|
else
|
|
{
|
|
animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete);
|
|
if(mUpperBodyState == UpperCharState_MinAttackToMaxAttack && !isKnockedDown())
|
|
{
|
|
float attackStrength = complete;
|
|
float minAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"min attack");
|
|
float maxAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"max attack");
|
|
if (minAttackTime == maxAttackTime)
|
|
{
|
|
// 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)
|
|
// 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));
|
|
}
|
|
|
|
if(weapclass != ESM::WeaponType::Ranged && weapclass != ESM::WeaponType::Thrown)
|
|
{
|
|
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;
|
|
|
|
mAnimation->disable(mCurrentWeapon);
|
|
mAnimation->play(mCurrentWeapon, priorityWeapon,
|
|
MWRender::Animation::BlendMask_All, false,
|
|
weapSpeed, mAttackType+" max attack", mAttackType+" min hit",
|
|
1.0f-complete, 0);
|
|
|
|
complete = 0.f;
|
|
mUpperBodyState = UpperCharState_MaxAttackToMinHit;
|
|
}
|
|
else if (isKnockedDown())
|
|
{
|
|
if (mUpperBodyState > UpperCharState_WeapEquiped)
|
|
{
|
|
mUpperBodyState = UpperCharState_WeapEquiped;
|
|
if (mWeaponType > ESM::Weapon::None)
|
|
mAnimation->showWeapons(true);
|
|
}
|
|
if (!mCurrentWeapon.empty())
|
|
mAnimation->disable(mCurrentWeapon);
|
|
}
|
|
}
|
|
|
|
mAnimation->setPitchFactor(0.f);
|
|
if (weapclass == ESM::WeaponType::Ranged || weapclass == ESM::WeaponType::Thrown)
|
|
{
|
|
switch (mUpperBodyState)
|
|
{
|
|
case UpperCharState_StartToMinAttack:
|
|
mAnimation->setPitchFactor(complete);
|
|
break;
|
|
case UpperCharState_MinAttackToMaxAttack:
|
|
case UpperCharState_MaxAttackToMinHit:
|
|
case UpperCharState_MinHitToHit:
|
|
mAnimation->setPitchFactor(1.f);
|
|
break;
|
|
case UpperCharState_FollowStartToFollowStop:
|
|
if (animPlaying)
|
|
{
|
|
// technically we do not need a pitch for crossbow reload animation,
|
|
// but we should avoid abrupt repositioning
|
|
if (mWeaponType == ESM::Weapon::MarksmanCrossbow)
|
|
mAnimation->setPitchFactor(std::max(0.f, 1.f-complete*10.f));
|
|
else
|
|
mAnimation->setPitchFactor(1.f-complete);
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(!animPlaying)
|
|
{
|
|
if(mUpperBodyState == UpperCharState_EquipingWeap ||
|
|
mUpperBodyState == UpperCharState_FollowStartToFollowStop ||
|
|
mUpperBodyState == UpperCharState_CastingSpell)
|
|
{
|
|
if (ammunition && mWeaponType == ESM::Weapon::MarksmanCrossbow)
|
|
mAnimation->attachArrow();
|
|
|
|
mUpperBodyState = UpperCharState_WeapEquiped;
|
|
}
|
|
else if(mUpperBodyState == UpperCharState_UnEquipingWeap)
|
|
mUpperBodyState = UpperCharState_Nothing;
|
|
}
|
|
else if(complete >= 1.0f && !isRandomAttackAnimation(mCurrentWeapon))
|
|
{
|
|
std::string start, stop;
|
|
switch(mUpperBodyState)
|
|
{
|
|
case UpperCharState_MinAttackToMaxAttack:
|
|
//hack to avoid body pos desync when jumping/sneaking in 'max attack' state
|
|
if(!mAnimation->isPlaying(mCurrentWeapon))
|
|
mAnimation->play(mCurrentWeapon, priorityWeapon,
|
|
MWRender::Animation::BlendMask_All, false,
|
|
0, mAttackType+" min attack", mAttackType+" max attack", 0.999f, 0);
|
|
break;
|
|
case UpperCharState_StartToMinAttack:
|
|
case UpperCharState_MaxAttackToMinHit:
|
|
{
|
|
if (mUpperBodyState == 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 random.
|
|
float minAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"min attack");
|
|
float maxAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"max attack");
|
|
if (getAttackingOrSpell() || minAttackTime == maxAttackTime)
|
|
{
|
|
start = mAttackType+" min attack";
|
|
stop = mAttackType+" max attack";
|
|
mUpperBodyState = UpperCharState_MinAttackToMaxAttack;
|
|
break;
|
|
}
|
|
|
|
if(weapclass != ESM::WeaponType::Ranged && weapclass != ESM::WeaponType::Thrown)
|
|
playSwishSound(0.0f);
|
|
}
|
|
|
|
if(mAttackType == "shoot")
|
|
{
|
|
start = mAttackType+" min hit";
|
|
stop = mAttackType+" release";
|
|
}
|
|
else
|
|
{
|
|
start = mAttackType+" min hit";
|
|
stop = mAttackType+" hit";
|
|
}
|
|
mUpperBodyState = UpperCharState_MinHitToHit;
|
|
break;
|
|
}
|
|
case UpperCharState_MinHitToHit:
|
|
if(mAttackType == "shoot")
|
|
{
|
|
start = mAttackType+" follow start";
|
|
stop = mAttackType+" follow stop";
|
|
}
|
|
else
|
|
{
|
|
float str = mAttackStrength;
|
|
start = mAttackType+((str < 0.5f) ? " small follow start"
|
|
: (str < 1.0f) ? " medium follow start"
|
|
: " large follow start");
|
|
stop = mAttackType+((str < 0.5f) ? " small follow stop"
|
|
: (str < 1.0f) ? " medium follow stop"
|
|
: " large follow stop");
|
|
}
|
|
mUpperBodyState = UpperCharState_FollowStartToFollowStop;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Note: apply crossbow reload animation only for upper body
|
|
// since blending with movement animations can give weird result.
|
|
if(!start.empty())
|
|
{
|
|
int mask = MWRender::Animation::BlendMask_All;
|
|
if (mWeaponType == ESM::Weapon::MarksmanCrossbow)
|
|
mask = MWRender::Animation::BlendMask_UpperBody;
|
|
|
|
mAnimation->disable(mCurrentWeapon);
|
|
if (mUpperBodyState == UpperCharState_FollowStartToFollowStop)
|
|
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))
|
|
{
|
|
clearStateAnimation(mCurrentWeapon);
|
|
mUpperBodyState = UpperCharState_WeapEquiped;
|
|
}
|
|
|
|
if (cls.hasInventoryStore(mPtr))
|
|
{
|
|
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;
|
|
}
|
|
|
|
void CharacterController::updateAnimQueue()
|
|
{
|
|
if(mAnimQueue.size() > 1)
|
|
{
|
|
if(mAnimation->isPlaying(mAnimQueue.front().mGroup) == false)
|
|
{
|
|
mAnimation->disable(mAnimQueue.front().mGroup);
|
|
mAnimQueue.pop_front();
|
|
|
|
bool loopfallback = (mAnimQueue.front().mGroup.compare(0,4,"idle") == 0);
|
|
mAnimation->play(mAnimQueue.front().mGroup, Priority_Default,
|
|
MWRender::Animation::BlendMask_All, false,
|
|
1.0f, "start", "stop", 0.0f, mAnimQueue.front().mLoopCount, loopfallback);
|
|
}
|
|
}
|
|
|
|
if(!mAnimQueue.empty())
|
|
mAnimation->setLoopingEnabled(mAnimQueue.front().mGroup, mAnimQueue.size() <= 1);
|
|
}
|
|
|
|
void CharacterController::update(float duration)
|
|
{
|
|
MWBase::World *world = MWBase::Environment::get().getWorld();
|
|
const MWWorld::Class &cls = mPtr.getClass();
|
|
osg::Vec3f movement(0.f, 0.f, 0.f);
|
|
float speed = 0.f;
|
|
|
|
updateMagicEffects();
|
|
|
|
bool isPlayer = mPtr == MWMechanics::getPlayer();
|
|
bool isFirstPersonPlayer = isPlayer && MWBase::Environment::get().getWorld()->isFirstPerson();
|
|
bool godmode = isPlayer && MWBase::Environment::get().getWorld()->getGodModeState();
|
|
|
|
float scale = mPtr.getCellRef().getScale();
|
|
|
|
static const bool normalizeSpeed = Settings::Manager::getBool("normalise race speed", "Game");
|
|
if (!normalizeSpeed && mPtr.getClass().isNpc())
|
|
{
|
|
const ESM::NPC* npc = mPtr.get<ESM::NPC>()->mBase;
|
|
const ESM::Race* race = world->getStore().get<ESM::Race>().find(npc->mRace);
|
|
float weight = npc->isMale() ? race->mData.mWeight.mMale : race->mData.mWeight.mFemale;
|
|
scale *= weight;
|
|
}
|
|
|
|
if(!cls.isActor())
|
|
updateAnimQueue();
|
|
else if(!cls.getCreatureStats(mPtr).isDead())
|
|
{
|
|
bool onground = world->isOnGround(mPtr);
|
|
bool incapacitated = ((!godmode && cls.getCreatureStats(mPtr).isParalyzed()) || cls.getCreatureStats(mPtr).getKnockedDown());
|
|
bool inwater = world->isSwimming(mPtr);
|
|
bool flying = world->isFlying(mPtr);
|
|
bool solid = world->isActorCollisionEnabled(mPtr);
|
|
// Can't run and sneak while flying (see speed formula in Npc/Creature::getSpeed)
|
|
bool sneak = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Sneak) && !flying && !inwater;
|
|
bool isrunning = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) && !flying;
|
|
CreatureStats &stats = cls.getCreatureStats(mPtr);
|
|
Movement& movementSettings = cls.getMovementSettings(mPtr);
|
|
|
|
//Force Jump Logic
|
|
|
|
bool isMoving = (std::abs(movementSettings.mPosition[0]) > .5 || std::abs(movementSettings.mPosition[1]) > .5);
|
|
if(!inwater && !flying && solid)
|
|
{
|
|
//Force Jump
|
|
if(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceJump))
|
|
movementSettings.mPosition[2] = onground ? 1 : 0;
|
|
//Force Move Jump, only jump if they're otherwise moving
|
|
if(stats.getMovementFlag(MWMechanics::CreatureStats::Flag_ForceMoveJump) && isMoving)
|
|
movementSettings.mPosition[2] = onground ? 1 : 0;
|
|
}
|
|
|
|
osg::Vec3f rot = cls.getRotationVector(mPtr);
|
|
osg::Vec3f vec(movementSettings.asVec3());
|
|
movementSettings.mSpeedFactor = std::min(vec.length(), 1.f);
|
|
vec.normalize();
|
|
|
|
// TODO: Move this check to mwinput.
|
|
// Joystick analogue movement.
|
|
// Due to the half way split between walking/running, we multiply speed by 2 while walking, unless a keyboard was used.
|
|
if (isPlayer && !isrunning && !sneak && !flying && movementSettings.mSpeedFactor <= 0.5f)
|
|
movementSettings.mSpeedFactor *= 2.f;
|
|
|
|
static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game");
|
|
if (smoothMovement)
|
|
{
|
|
static const float playerTurningCoef = 1.0 / std::max(0.01f, Settings::Manager::getFloat("smooth movement player turning delay", "Game"));
|
|
float angle = mPtr.getRefData().getPosition().rot[2];
|
|
osg::Vec2f targetSpeed = Misc::rotateVec2f(osg::Vec2f(vec.x(), vec.y()), -angle) * movementSettings.mSpeedFactor;
|
|
osg::Vec2f delta = targetSpeed - mSmoothedSpeed;
|
|
float speedDelta = movementSettings.mSpeedFactor - mSmoothedSpeed.length();
|
|
float deltaLen = delta.length();
|
|
|
|
float maxDelta;
|
|
if (isFirstPersonPlayer)
|
|
maxDelta = 1;
|
|
else if (std::abs(speedDelta) < deltaLen / 2)
|
|
// Turning is smooth for player and less smooth for NPCs (otherwise NPC can miss a path point).
|
|
maxDelta = duration * (isPlayer ? playerTurningCoef : 6.f);
|
|
else if (isPlayer && speedDelta < -deltaLen / 2)
|
|
// As soon as controls are released, mwinput switches player from running to walking.
|
|
// So stopping should be instant for player, otherwise it causes a small twitch.
|
|
maxDelta = 1;
|
|
else // In all other cases speeding up and stopping are smooth.
|
|
maxDelta = duration * 3.f;
|
|
|
|
if (deltaLen > maxDelta)
|
|
delta *= maxDelta / deltaLen;
|
|
mSmoothedSpeed += delta;
|
|
|
|
osg::Vec2f newSpeed = Misc::rotateVec2f(mSmoothedSpeed, angle);
|
|
movementSettings.mSpeedFactor = newSpeed.normalize();
|
|
vec.x() = newSpeed.x();
|
|
vec.y() = newSpeed.y();
|
|
|
|
const float eps = 0.001f;
|
|
if (movementSettings.mSpeedFactor < eps)
|
|
{
|
|
movementSettings.mSpeedFactor = 0;
|
|
vec.x() = 0;
|
|
vec.y() = 1;
|
|
}
|
|
else if ((vec.y() < 0) != mIsMovingBackward)
|
|
{
|
|
if (targetSpeed.length() < eps || (movementSettings.mPosition[1] < 0) == mIsMovingBackward)
|
|
vec.y() = mIsMovingBackward ? -eps : eps;
|
|
}
|
|
vec.normalize();
|
|
}
|
|
|
|
float effectiveRotation = rot.z();
|
|
bool canMove = cls.getMaxSpeed(mPtr) > 0;
|
|
static const bool turnToMovementDirection = Settings::Manager::getBool("turn to movement direction", "Game");
|
|
if (!turnToMovementDirection || isFirstPersonPlayer)
|
|
{
|
|
movementSettings.mIsStrafing = std::abs(vec.x()) > std::abs(vec.y()) * 2;
|
|
stats.setSideMovementAngle(0);
|
|
}
|
|
else if (canMove)
|
|
{
|
|
float targetMovementAngle = vec.y() >= 0 ? std::atan2(-vec.x(), vec.y()) : std::atan2(vec.x(), -vec.y());
|
|
movementSettings.mIsStrafing = (stats.getDrawState() != MWMechanics::DrawState_Nothing || inwater)
|
|
&& std::abs(targetMovementAngle) > osg::DegreesToRadians(60.0f);
|
|
if (movementSettings.mIsStrafing)
|
|
targetMovementAngle = 0;
|
|
float delta = targetMovementAngle - stats.getSideMovementAngle();
|
|
float cosDelta = cosf(delta);
|
|
|
|
if ((vec.y() < 0) == mIsMovingBackward)
|
|
movementSettings.mSpeedFactor *= std::min(std::max(cosDelta, 0.f) + 0.3f, 1.f); // slow down when turn
|
|
if (std::abs(delta) < osg::DegreesToRadians(20.0f))
|
|
mIsMovingBackward = vec.y() < 0;
|
|
|
|
float maxDelta = osg::PI * duration * (2.5f - cosDelta);
|
|
delta = std::clamp(delta, -maxDelta, maxDelta);
|
|
stats.setSideMovementAngle(stats.getSideMovementAngle() + delta);
|
|
effectiveRotation += delta;
|
|
}
|
|
|
|
mAnimation->setLegsYawRadians(stats.getSideMovementAngle());
|
|
if (stats.getDrawState() == MWMechanics::DrawState_Nothing || inwater)
|
|
mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 2);
|
|
else
|
|
mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 4);
|
|
if (smoothMovement && !isPlayer && !inwater)
|
|
mAnimation->setUpperBodyYawRadians(mAnimation->getUpperBodyYawRadians() + mAnimation->getHeadYaw() / 2);
|
|
|
|
speed = cls.getCurrentSpeed(mPtr);
|
|
vec.x() *= speed;
|
|
vec.y() *= speed;
|
|
|
|
if(mHitState != CharState_None && mHitState != CharState_Block && mJumpState == JumpState_None)
|
|
vec = osg::Vec3f();
|
|
|
|
CharacterState movestate = CharState_None;
|
|
CharacterState idlestate = CharState_None;
|
|
JumpingState jumpstate = JumpState_None;
|
|
|
|
mHasMovedInXY = std::abs(vec.x())+std::abs(vec.y()) > 0.0f;
|
|
isrunning = isrunning && mHasMovedInXY;
|
|
|
|
// advance athletics
|
|
if(mHasMovedInXY && isPlayer)
|
|
{
|
|
if(inwater)
|
|
{
|
|
mSecondsOfSwimming += duration;
|
|
while(mSecondsOfSwimming > 1)
|
|
{
|
|
cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, 1);
|
|
mSecondsOfSwimming -= 1;
|
|
}
|
|
}
|
|
else if(isrunning && !sneak)
|
|
{
|
|
mSecondsOfRunning += duration;
|
|
while(mSecondsOfRunning > 1)
|
|
{
|
|
cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, 0);
|
|
mSecondsOfRunning -= 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
// reduce fatigue
|
|
const MWWorld::Store<ESM::GameSetting> &gmst = world->getStore().get<ESM::GameSetting>();
|
|
float fatigueLoss = 0;
|
|
static const float fFatigueRunBase = gmst.find("fFatigueRunBase")->mValue.getFloat();
|
|
static const float fFatigueRunMult = gmst.find("fFatigueRunMult")->mValue.getFloat();
|
|
static const float fFatigueSwimWalkBase = gmst.find("fFatigueSwimWalkBase")->mValue.getFloat();
|
|
static const float fFatigueSwimRunBase = gmst.find("fFatigueSwimRunBase")->mValue.getFloat();
|
|
static const float fFatigueSwimWalkMult = gmst.find("fFatigueSwimWalkMult")->mValue.getFloat();
|
|
static const float fFatigueSwimRunMult = gmst.find("fFatigueSwimRunMult")->mValue.getFloat();
|
|
static const float fFatigueSneakBase = gmst.find("fFatigueSneakBase")->mValue.getFloat();
|
|
static const float fFatigueSneakMult = gmst.find("fFatigueSneakMult")->mValue.getFloat();
|
|
|
|
if (cls.getEncumbrance(mPtr) <= cls.getCapacity(mPtr))
|
|
{
|
|
const float encumbrance = cls.getNormalizedEncumbrance(mPtr);
|
|
if (sneak)
|
|
fatigueLoss = fFatigueSneakBase + encumbrance * fFatigueSneakMult;
|
|
else
|
|
{
|
|
if (inwater)
|
|
{
|
|
if (!isrunning)
|
|
fatigueLoss = fFatigueSwimWalkBase + encumbrance * fFatigueSwimWalkMult;
|
|
else
|
|
fatigueLoss = fFatigueSwimRunBase + encumbrance * fFatigueSwimRunMult;
|
|
}
|
|
else if (isrunning)
|
|
fatigueLoss = fFatigueRunBase + encumbrance * fFatigueRunMult;
|
|
}
|
|
}
|
|
fatigueLoss *= duration;
|
|
fatigueLoss *= movementSettings.mSpeedFactor;
|
|
DynamicStat<float> fatigue = cls.getCreatureStats(mPtr).getFatigue();
|
|
|
|
if (!godmode)
|
|
{
|
|
fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss, fatigue.getCurrent() < 0);
|
|
cls.getCreatureStats(mPtr).setFatigue(fatigue);
|
|
}
|
|
|
|
float z = cls.getJump(mPtr);
|
|
if(sneak || inwater || flying || incapacitated || !solid || z <= 0)
|
|
vec.z() = 0.0f;
|
|
|
|
bool inJump = true;
|
|
bool playLandingSound = false;
|
|
if(!onground && !flying && !inwater && solid)
|
|
{
|
|
// In the air (either getting up —ascending part of jump— or falling).
|
|
jumpstate = JumpState_InAir;
|
|
|
|
static const float fJumpMoveBase = gmst.find("fJumpMoveBase")->mValue.getFloat();
|
|
static const float fJumpMoveMult = gmst.find("fJumpMoveMult")->mValue.getFloat();
|
|
float factor = fJumpMoveBase + fJumpMoveMult * mPtr.getClass().getSkill(mPtr, ESM::Skill::Acrobatics)/100.f;
|
|
factor = std::min(1.f, factor);
|
|
vec.x() *= factor;
|
|
vec.y() *= factor;
|
|
vec.z() = 0.0f;
|
|
}
|
|
else if(vec.z() > 0.0f && mJumpState != JumpState_InAir)
|
|
{
|
|
// Started a jump.
|
|
if (z > 0)
|
|
{
|
|
if(vec.x() == 0 && vec.y() == 0)
|
|
vec = osg::Vec3f(0.0f, 0.0f, z);
|
|
else
|
|
{
|
|
osg::Vec3f lat (vec.x(), vec.y(), 0.0f);
|
|
lat.normalize();
|
|
vec = osg::Vec3f(lat.x(), lat.y(), 1.0f) * z * 0.707f;
|
|
}
|
|
}
|
|
}
|
|
else if(mJumpState == JumpState_InAir && !inwater && !flying && solid)
|
|
{
|
|
jumpstate = JumpState_Landing;
|
|
vec.z() = 0.0f;
|
|
|
|
float height = cls.getCreatureStats(mPtr).land(isPlayer);
|
|
float healthLost = getFallDamage(mPtr, height);
|
|
|
|
if (healthLost > 0.0f)
|
|
{
|
|
const float fatigueTerm = cls.getCreatureStats(mPtr).getFatigueTerm();
|
|
|
|
// inflict fall damages
|
|
if (!godmode)
|
|
{
|
|
float realHealthLost = static_cast<float>(healthLost * (1.0f - 0.25f * fatigueTerm));
|
|
cls.onHit(mPtr, realHealthLost, true, MWWorld::Ptr(), MWWorld::Ptr(), osg::Vec3f(), true);
|
|
}
|
|
|
|
const float acrobaticsSkill = cls.getSkill(mPtr, ESM::Skill::Acrobatics);
|
|
if (healthLost > (acrobaticsSkill * fatigueTerm))
|
|
{
|
|
if (!godmode)
|
|
cls.getCreatureStats(mPtr).setKnockedDown(true);
|
|
}
|
|
else
|
|
{
|
|
// report acrobatics progression
|
|
if (isPlayer)
|
|
cls.skillUsageSucceeded(mPtr, ESM::Skill::Acrobatics, 1);
|
|
}
|
|
}
|
|
|
|
if (mPtr.getClass().isNpc())
|
|
playLandingSound = true;
|
|
}
|
|
else
|
|
{
|
|
if(mPtr.getClass().isNpc() && mJumpState == JumpState_InAir && !flying && solid)
|
|
playLandingSound = true;
|
|
|
|
jumpstate = mAnimation->isPlaying(mCurrentJump) ? JumpState_Landing : JumpState_None;
|
|
|
|
vec.x() *= scale;
|
|
vec.y() *= scale;
|
|
vec.z() = 0.0f;
|
|
|
|
inJump = false;
|
|
|
|
if (movementSettings.mIsStrafing)
|
|
{
|
|
if(vec.x() > 0.0f)
|
|
movestate = (inwater ? (isrunning ? CharState_SwimRunRight : CharState_SwimWalkRight)
|
|
: (sneak ? CharState_SneakRight
|
|
: (isrunning ? CharState_RunRight : CharState_WalkRight)));
|
|
else if(vec.x() < 0.0f)
|
|
movestate = (inwater ? (isrunning ? CharState_SwimRunLeft : CharState_SwimWalkLeft)
|
|
: (sneak ? CharState_SneakLeft
|
|
: (isrunning ? CharState_RunLeft : CharState_WalkLeft)));
|
|
}
|
|
else if (vec.length2() > 0.0f)
|
|
{
|
|
if (vec.y() >= 0.0f)
|
|
movestate = (inwater ? (isrunning ? CharState_SwimRunForward : CharState_SwimWalkForward)
|
|
: (sneak ? CharState_SneakForward
|
|
: (isrunning ? CharState_RunForward : CharState_WalkForward)));
|
|
else
|
|
movestate = (inwater ? (isrunning ? CharState_SwimRunBack : CharState_SwimWalkBack)
|
|
: (sneak ? CharState_SneakBack
|
|
: (isrunning ? CharState_RunBack : CharState_WalkBack)));
|
|
}
|
|
else
|
|
{
|
|
// Do not play turning animation for player if rotation speed is very slow.
|
|
// Actual threshold should take framerate in account.
|
|
float rotationThreshold = (isPlayer ? 0.015f : 0.001f) * 60 * duration;
|
|
|
|
// It seems only bipedal actors use turning animations.
|
|
// Also do not use turning animations in the first-person view and when sneaking.
|
|
if (!sneak && jumpstate == JumpState_None && !isFirstPersonPlayer && mPtr.getClass().isBipedal(mPtr))
|
|
{
|
|
if(effectiveRotation > rotationThreshold)
|
|
movestate = inwater ? CharState_SwimTurnRight : CharState_TurnRight;
|
|
else if(effectiveRotation < -rotationThreshold)
|
|
movestate = inwater ? CharState_SwimTurnLeft : CharState_TurnLeft;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (playLandingSound)
|
|
{
|
|
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
|
|
std::string sound;
|
|
osg::Vec3f pos(mPtr.getRefData().getPosition().asVec3());
|
|
if (world->isUnderwater(mPtr.getCell(), pos) || world->isWalkingOnWater(mPtr))
|
|
sound = "DefaultLandWater";
|
|
else if (onground)
|
|
sound = "DefaultLand";
|
|
|
|
if (!sound.empty())
|
|
sndMgr->playSound3D(mPtr, sound, 1.f, 1.f, MWSound::Type::Foot, MWSound::PlayMode::NoPlayerLocal);
|
|
}
|
|
|
|
if (turnToMovementDirection && !isFirstPersonPlayer &&
|
|
(movestate == CharState_SwimRunForward || movestate == CharState_SwimWalkForward ||
|
|
movestate == CharState_SwimRunBack || movestate == CharState_SwimWalkBack))
|
|
{
|
|
float swimmingPitch = mAnimation->getBodyPitchRadians();
|
|
float targetSwimmingPitch = -mPtr.getRefData().getPosition().rot[0];
|
|
float maxSwimPitchDelta = 3.0f * duration;
|
|
swimmingPitch += std::clamp(targetSwimmingPitch - swimmingPitch, -maxSwimPitchDelta, maxSwimPitchDelta);
|
|
mAnimation->setBodyPitchRadians(swimmingPitch);
|
|
}
|
|
else
|
|
mAnimation->setBodyPitchRadians(0);
|
|
|
|
static const bool swimUpwardCorrection = Settings::Manager::getBool("swim upward correction", "Game");
|
|
if (inwater && isPlayer && !isFirstPersonPlayer && swimUpwardCorrection)
|
|
{
|
|
static const float swimUpwardCoef = Settings::Manager::getFloat("swim upward coef", "Game");
|
|
static const float swimForwardCoef = sqrtf(1.0f - swimUpwardCoef * swimUpwardCoef);
|
|
vec.z() = std::abs(vec.y()) * swimUpwardCoef;
|
|
vec.y() *= swimForwardCoef;
|
|
}
|
|
|
|
// Player can not use smooth turning as NPCs, so we play turning animation a bit to avoid jittering
|
|
if (isPlayer)
|
|
{
|
|
float threshold = mCurrentMovement.find("swim") == std::string::npos ? 0.4f : 0.8f;
|
|
float complete;
|
|
bool animPlaying = mAnimation->getInfo(mCurrentMovement, &complete);
|
|
if (movestate == CharState_None && jumpstate == JumpState_None && isTurning())
|
|
{
|
|
if (animPlaying && complete < threshold)
|
|
movestate = mMovementState;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (mPtr.getClass().isBipedal(mPtr))
|
|
{
|
|
if (mTurnAnimationThreshold > 0)
|
|
mTurnAnimationThreshold -= duration;
|
|
|
|
if (movestate == CharState_TurnRight || movestate == CharState_TurnLeft ||
|
|
movestate == CharState_SwimTurnRight || movestate == CharState_SwimTurnLeft)
|
|
{
|
|
mTurnAnimationThreshold = 0.05f;
|
|
}
|
|
else if (movestate == CharState_None && isTurning()
|
|
&& mTurnAnimationThreshold > 0)
|
|
{
|
|
movestate = mMovementState;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (movestate != CharState_None)
|
|
{
|
|
clearAnimQueue();
|
|
jumpstate = JumpState_None;
|
|
}
|
|
|
|
if(mAnimQueue.empty() || inwater || (sneak && mIdleState != CharState_SpecialIdle))
|
|
{
|
|
if (inwater)
|
|
idlestate = CharState_IdleSwim;
|
|
else if (sneak && !inJump)
|
|
idlestate = CharState_IdleSneak;
|
|
else
|
|
idlestate = CharState_Idle;
|
|
}
|
|
else
|
|
updateAnimQueue();
|
|
|
|
if (!mSkipAnim)
|
|
{
|
|
refreshCurrentAnims(idlestate, movestate, jumpstate, updateState(idlestate));
|
|
updateIdleStormState(inwater);
|
|
}
|
|
|
|
if (inJump)
|
|
mMovementAnimationControlled = false;
|
|
|
|
if (isTurning())
|
|
{
|
|
// Adjust animation speed from 1.0 to 1.5 multiplier
|
|
if (duration > 0)
|
|
{
|
|
float turnSpeed = std::min(1.5f, std::abs(rot.z()) / duration / static_cast<float>(osg::PI));
|
|
mAnimation->adjustSpeedMult(mCurrentMovement, std::max(turnSpeed, 1.0f));
|
|
}
|
|
}
|
|
else if (mMovementState != CharState_None && mAdjustMovementAnimSpeed)
|
|
{
|
|
// Vanilla caps the played animation speed.
|
|
const float maxSpeedMult = 10.f;
|
|
const float speedMult = speed / mMovementAnimSpeed;
|
|
mAnimation->adjustSpeedMult(mCurrentMovement, std::min(maxSpeedMult, speedMult));
|
|
// Make sure the actual speed is the "expected" speed even though the animation is slower
|
|
scale *= std::max(1.f, speedMult / maxSpeedMult);
|
|
}
|
|
|
|
if (!mSkipAnim)
|
|
{
|
|
if(!isKnockedDown() && !isKnockedOut())
|
|
{
|
|
if (rot != osg::Vec3f())
|
|
world->rotateObject(mPtr, rot, true);
|
|
}
|
|
else //avoid z-rotating for knockdown
|
|
{
|
|
if (rot.x() != 0 && rot.y() != 0)
|
|
{
|
|
rot.z() = 0.0f;
|
|
world->rotateObject(mPtr, rot, true);
|
|
}
|
|
}
|
|
|
|
if (!mMovementAnimationControlled)
|
|
world->queueMovement(mPtr, vec);
|
|
}
|
|
|
|
movement = vec;
|
|
movementSettings.mPosition[0] = movementSettings.mPosition[1] = 0;
|
|
if (movement.z() == 0.f)
|
|
movementSettings.mPosition[2] = 0;
|
|
// Can't reset jump state (mPosition[2]) here in full; we don't know for sure whether the PhysicSystem will actually handle it in this frame
|
|
// due to the fixed minimum timestep used for the physics update. It will be reset in PhysicSystem::move once the jump is handled.
|
|
|
|
if (!mSkipAnim)
|
|
updateHeadTracking(duration);
|
|
}
|
|
else if(cls.getCreatureStats(mPtr).isDead())
|
|
{
|
|
// initial start of death animation for actors that started the game as dead
|
|
// not done in constructor since we need to give scripts a chance to set the mSkipAnim flag
|
|
if (!mSkipAnim && mDeathState != CharState_None && mCurrentDeath.empty())
|
|
{
|
|
// Fast-forward death animation to end for persisting corpses or corpses after end of death animation
|
|
if (cls.isPersistent(mPtr) || cls.getCreatureStats(mPtr).isDeathAnimationFinished())
|
|
playDeath(1.f, mDeathState);
|
|
}
|
|
}
|
|
|
|
bool isPersist = isPersistentAnimPlaying();
|
|
osg::Vec3f moved = mAnimation->runAnimation(mSkipAnim && !isPersist ? 0.f : duration);
|
|
if(duration > 0.0f)
|
|
moved /= duration;
|
|
else
|
|
moved = osg::Vec3f(0.f, 0.f, 0.f);
|
|
|
|
moved.x() *= scale;
|
|
moved.y() *= scale;
|
|
|
|
// Ensure we're moving in generally the right direction...
|
|
if (speed > 0.f && moved != osg::Vec3f())
|
|
{
|
|
float l = moved.length();
|
|
if (std::abs(movement.x() - moved.x()) > std::abs(moved.x()) / 2 ||
|
|
std::abs(movement.y() - moved.y()) > std::abs(moved.y()) / 2 ||
|
|
std::abs(movement.z() - moved.z()) > std::abs(moved.z()) / 2)
|
|
{
|
|
moved = movement;
|
|
// For some creatures getSpeed doesn't work, so we adjust speed to the animation.
|
|
// TODO: Fix Creature::getSpeed.
|
|
float newLength = moved.length();
|
|
if (newLength > 0 && !cls.isNpc())
|
|
moved *= (l / newLength);
|
|
}
|
|
}
|
|
|
|
if (mFloatToSurface && cls.isActor())
|
|
{
|
|
if (cls.getCreatureStats(mPtr).isDead()
|
|
|| (!godmode && cls.getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Paralyze).getModifier() > 0))
|
|
{
|
|
moved.z() = 1.0;
|
|
}
|
|
}
|
|
|
|
// Update movement
|
|
if(mMovementAnimationControlled && mPtr.getClass().isActor())
|
|
world->queueMovement(mPtr, moved);
|
|
|
|
mSkipAnim = false;
|
|
|
|
mAnimation->enableHeadAnimation(cls.isActor() && !cls.getCreatureStats(mPtr).isDead());
|
|
}
|
|
|
|
void CharacterController::persistAnimationState() const
|
|
{
|
|
ESM::AnimationState& state = mPtr.getRefData().getAnimationState();
|
|
|
|
state.mScriptedAnims.clear();
|
|
for (AnimationQueue::const_iterator iter = mAnimQueue.begin(); iter != mAnimQueue.end(); ++iter)
|
|
{
|
|
if (!iter->mPersist)
|
|
continue;
|
|
|
|
ESM::AnimationState::ScriptedAnimation anim;
|
|
anim.mGroup = iter->mGroup;
|
|
|
|
if (iter == mAnimQueue.begin())
|
|
{
|
|
anim.mLoopCount = mAnimation->getCurrentLoopCount(anim.mGroup);
|
|
float complete;
|
|
mAnimation->getInfo(anim.mGroup, &complete, nullptr);
|
|
anim.mTime = complete;
|
|
}
|
|
else
|
|
{
|
|
anim.mLoopCount = iter->mLoopCount;
|
|
anim.mTime = 0.f;
|
|
}
|
|
|
|
state.mScriptedAnims.push_back(anim);
|
|
}
|
|
}
|
|
|
|
void CharacterController::unpersistAnimationState()
|
|
{
|
|
const ESM::AnimationState& state = mPtr.getRefData().getAnimationState();
|
|
|
|
if (!state.mScriptedAnims.empty())
|
|
{
|
|
clearAnimQueue();
|
|
for (ESM::AnimationState::ScriptedAnimations::const_iterator iter = state.mScriptedAnims.begin(); iter != state.mScriptedAnims.end(); ++iter)
|
|
{
|
|
AnimationQueueEntry entry;
|
|
entry.mGroup = iter->mGroup;
|
|
entry.mLoopCount = iter->mLoopCount;
|
|
entry.mPersist = true;
|
|
|
|
mAnimQueue.push_back(entry);
|
|
}
|
|
|
|
const ESM::AnimationState::ScriptedAnimation& anim = state.mScriptedAnims.front();
|
|
float complete = anim.mTime;
|
|
if (anim.mAbsolute)
|
|
{
|
|
float start = mAnimation->getTextKeyTime(anim.mGroup+": start");
|
|
float stop = mAnimation->getTextKeyTime(anim.mGroup+": stop");
|
|
float time = std::clamp(anim.mTime, start, stop);
|
|
complete = (time - start) / (stop - start);
|
|
}
|
|
|
|
clearStateAnimation(mCurrentIdle);
|
|
mIdleState = CharState_SpecialIdle;
|
|
|
|
bool loopfallback = (mAnimQueue.front().mGroup.compare(0,4,"idle") == 0);
|
|
mAnimation->play(anim.mGroup,
|
|
Priority_Persistent, MWRender::Animation::BlendMask_All, false, 1.0f,
|
|
"start", "stop", complete, anim.mLoopCount, loopfallback);
|
|
}
|
|
}
|
|
|
|
bool CharacterController::playGroup(const std::string &groupname, int mode, int count, bool persist)
|
|
{
|
|
if(!mAnimation || !mAnimation->hasAnimation(groupname))
|
|
return false;
|
|
|
|
// We should not interrupt persistent animations by non-persistent ones
|
|
if (isPersistentAnimPlaying() && !persist)
|
|
return true;
|
|
|
|
// If this animation is a looped animation (has a "loop start" key) that is already playing
|
|
// and has not yet reached the end of the loop, allow it to continue animating with its existing loop count
|
|
// and remove any other animations that were queued.
|
|
// This emulates observed behavior from the original allows the script "OutsideBanner" to animate banners correctly.
|
|
if (!mAnimQueue.empty() && mAnimQueue.front().mGroup == groupname &&
|
|
mAnimation->getTextKeyTime(mAnimQueue.front().mGroup + ": loop start") >= 0 &&
|
|
mAnimation->isPlaying(groupname))
|
|
{
|
|
float endOfLoop = mAnimation->getTextKeyTime(mAnimQueue.front().mGroup+": loop stop");
|
|
|
|
if (endOfLoop < 0) // if no Loop Stop key was found, use the Stop key
|
|
endOfLoop = mAnimation->getTextKeyTime(mAnimQueue.front().mGroup+": stop");
|
|
|
|
if (endOfLoop > 0 && (mAnimation->getCurrentTime(mAnimQueue.front().mGroup) < endOfLoop))
|
|
{
|
|
mAnimQueue.resize(1);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
count = std::max(count, 1);
|
|
|
|
AnimationQueueEntry entry;
|
|
entry.mGroup = groupname;
|
|
entry.mLoopCount = count-1;
|
|
entry.mPersist = persist;
|
|
|
|
if(mode != 0 || mAnimQueue.empty() || !isAnimPlaying(mAnimQueue.front().mGroup))
|
|
{
|
|
clearAnimQueue(persist);
|
|
|
|
clearStateAnimation(mCurrentIdle);
|
|
|
|
mIdleState = CharState_SpecialIdle;
|
|
bool loopfallback = (entry.mGroup.compare(0,4,"idle") == 0);
|
|
mAnimation->play(groupname, persist && groupname != "idle" ? Priority_Persistent : Priority_Default,
|
|
MWRender::Animation::BlendMask_All, false, 1.0f,
|
|
((mode==2) ? "loop start" : "start"), "stop", 0.0f, count-1, loopfallback);
|
|
}
|
|
else
|
|
{
|
|
mAnimQueue.resize(1);
|
|
}
|
|
|
|
// "PlayGroup idle" is a special case, used to remove to stop scripted animations playing
|
|
if (groupname == "idle")
|
|
entry.mPersist = false;
|
|
|
|
mAnimQueue.push_back(entry);
|
|
|
|
return true;
|
|
}
|
|
|
|
void CharacterController::skipAnim()
|
|
{
|
|
mSkipAnim = true;
|
|
}
|
|
|
|
bool CharacterController::isPersistentAnimPlaying() const
|
|
{
|
|
if (!mAnimQueue.empty())
|
|
{
|
|
const AnimationQueueEntry& first = mAnimQueue.front();
|
|
return first.mPersist && isAnimPlaying(first.mGroup);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool CharacterController::isAnimPlaying(const std::string &groupName) const
|
|
{
|
|
if(mAnimation == nullptr)
|
|
return false;
|
|
return mAnimation->isPlaying(groupName);
|
|
}
|
|
|
|
void CharacterController::clearAnimQueue(bool clearPersistAnims)
|
|
{
|
|
// Do not interrupt scripted animations, if we want to keep them
|
|
if ((!isPersistentAnimPlaying() || clearPersistAnims) && !mAnimQueue.empty())
|
|
mAnimation->disable(mAnimQueue.front().mGroup);
|
|
|
|
if (clearPersistAnims)
|
|
{
|
|
mAnimQueue.clear();
|
|
return;
|
|
}
|
|
|
|
for (AnimationQueue::iterator it = mAnimQueue.begin(); it != mAnimQueue.end();)
|
|
{
|
|
if (!it->mPersist)
|
|
it = mAnimQueue.erase(it);
|
|
else
|
|
++it;
|
|
}
|
|
}
|
|
|
|
void CharacterController::forceStateUpdate()
|
|
{
|
|
if(!mAnimation)
|
|
return;
|
|
clearAnimQueue();
|
|
|
|
// Make sure we canceled the current attack or spellcasting,
|
|
// because we disabled attack animations anyway.
|
|
mCastingManualSpell = false;
|
|
setAttackingOrSpell(false);
|
|
if (mUpperBodyState != UpperCharState_Nothing)
|
|
mUpperBodyState = UpperCharState_WeapEquiped;
|
|
|
|
refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true);
|
|
|
|
if(mDeathState != CharState_None)
|
|
{
|
|
playRandomDeath();
|
|
}
|
|
|
|
mAnimation->runAnimation(0.f);
|
|
}
|
|
|
|
CharacterController::KillResult CharacterController::kill()
|
|
{
|
|
if (mDeathState == CharState_None)
|
|
{
|
|
playRandomDeath();
|
|
resetCurrentIdleState();
|
|
return Result_DeathAnimStarted;
|
|
}
|
|
|
|
MWMechanics::CreatureStats& cStats = mPtr.getClass().getCreatureStats(mPtr);
|
|
if (isAnimPlaying(mCurrentDeath))
|
|
return Result_DeathAnimPlaying;
|
|
if (!cStats.isDeathAnimationFinished())
|
|
{
|
|
cStats.setDeathAnimationFinished(true);
|
|
return Result_DeathAnimJustFinished;
|
|
}
|
|
return Result_DeathAnimFinished;
|
|
}
|
|
|
|
void CharacterController::resurrect()
|
|
{
|
|
if(mDeathState == CharState_None)
|
|
return;
|
|
|
|
resetCurrentDeathState();
|
|
mWeaponType = ESM::Weapon::None;
|
|
}
|
|
|
|
void CharacterController::updateContinuousVfx() const
|
|
{
|
|
// Keeping track of when to stop a continuous VFX seems to be very difficult to do inside the spells code,
|
|
// as it's extremely spread out (ActiveSpells, Spells, InventoryStore effects, etc...) so we do it here.
|
|
|
|
// Stop any effects that are no longer active
|
|
std::vector<int> effects;
|
|
mAnimation->getLoopingEffects(effects);
|
|
|
|
for (int effectId : effects)
|
|
{
|
|
if (mPtr.getClass().getCreatureStats(mPtr).isDeathAnimationFinished()
|
|
|| mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(MWMechanics::EffectKey(effectId)).getMagnitude() <= 0)
|
|
mAnimation->removeEffect(effectId);
|
|
}
|
|
}
|
|
|
|
void CharacterController::updateMagicEffects() const
|
|
{
|
|
if (!mPtr.getClass().isActor())
|
|
return;
|
|
|
|
float light = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Light).getMagnitude();
|
|
mAnimation->setLightEffect(light);
|
|
|
|
// If you're dead you don't care about whether you've started/stopped being a vampire or not
|
|
if (mPtr.getClass().getCreatureStats(mPtr).isDead())
|
|
return;
|
|
|
|
bool vampire = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude() > 0.0f;
|
|
mAnimation->setVampire(vampire);
|
|
}
|
|
|
|
void CharacterController::setVisibility(float visibility) const
|
|
{
|
|
// We should take actor's invisibility in account
|
|
if (mPtr.getClass().isActor())
|
|
{
|
|
float alpha = 1.f;
|
|
if (mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Invisibility).getModifier()) // Ignore base magnitude (see bug #3555).
|
|
{
|
|
if (mPtr == getPlayer())
|
|
alpha = 0.25f;
|
|
else
|
|
alpha = 0.05f;
|
|
}
|
|
float chameleon = mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude();
|
|
if (chameleon)
|
|
{
|
|
alpha *= std::clamp(1.f - chameleon / 100.f, 0.25f, 0.75f);
|
|
}
|
|
|
|
visibility = std::min(visibility, alpha);
|
|
}
|
|
|
|
// TODO: implement a dithering shader rather than just change object transparency.
|
|
mAnimation->setAlpha(visibility);
|
|
}
|
|
|
|
std::string CharacterController::getMovementBasedAttackType() const
|
|
{
|
|
float *move = mPtr.getClass().getMovementSettings(mPtr).mPosition;
|
|
if (std::abs(move[1]) > std::abs(move[0]) + 0.2f) // forward-backward
|
|
return "thrust";
|
|
if (std::abs(move[0]) > std::abs(move[1]) + 0.2f) // sideway
|
|
return "slash";
|
|
return "chop";
|
|
}
|
|
|
|
bool CharacterController::isRandomAttackAnimation(std::string_view group)
|
|
{
|
|
return (group == "attack1" || group == "swimattack1" ||
|
|
group == "attack2" || group == "swimattack2" ||
|
|
group == "attack3" || group == "swimattack3");
|
|
}
|
|
|
|
bool CharacterController::isAttackPreparing() const
|
|
{
|
|
return mUpperBodyState == UpperCharState_StartToMinAttack ||
|
|
mUpperBodyState == UpperCharState_MinAttackToMaxAttack;
|
|
}
|
|
|
|
bool CharacterController::isCastingSpell() const
|
|
{
|
|
return mCastingManualSpell || mUpperBodyState == UpperCharState_CastingSpell;
|
|
}
|
|
|
|
bool CharacterController::isReadyToBlock() const
|
|
{
|
|
return updateCarriedLeftVisible(mWeaponType);
|
|
}
|
|
|
|
bool CharacterController::isKnockedDown() const
|
|
{
|
|
return mHitState == CharState_KnockDown ||
|
|
mHitState == CharState_SwimKnockDown;
|
|
}
|
|
|
|
bool CharacterController::isKnockedOut() const
|
|
{
|
|
return mHitState == CharState_KnockOut ||
|
|
mHitState == CharState_SwimKnockOut;
|
|
}
|
|
|
|
bool CharacterController::isTurning() const
|
|
{
|
|
return mMovementState == CharState_TurnLeft ||
|
|
mMovementState == CharState_TurnRight ||
|
|
mMovementState == CharState_SwimTurnLeft ||
|
|
mMovementState == CharState_SwimTurnRight;
|
|
}
|
|
|
|
bool CharacterController::isRecovery() const
|
|
{
|
|
return mHitState == CharState_Hit ||
|
|
mHitState == CharState_SwimHit;
|
|
}
|
|
|
|
bool CharacterController::isAttackingOrSpell() const
|
|
{
|
|
return mUpperBodyState != UpperCharState_Nothing &&
|
|
mUpperBodyState != UpperCharState_WeapEquiped;
|
|
}
|
|
|
|
bool CharacterController::isSneaking() const
|
|
{
|
|
return mIdleState == CharState_IdleSneak ||
|
|
mMovementState == CharState_SneakForward ||
|
|
mMovementState == CharState_SneakBack ||
|
|
mMovementState == CharState_SneakLeft ||
|
|
mMovementState == CharState_SneakRight;
|
|
}
|
|
|
|
bool CharacterController::isRunning() const
|
|
{
|
|
return mMovementState == CharState_RunForward ||
|
|
mMovementState == CharState_RunBack ||
|
|
mMovementState == CharState_RunLeft ||
|
|
mMovementState == CharState_RunRight ||
|
|
mMovementState == CharState_SwimRunForward ||
|
|
mMovementState == CharState_SwimRunBack ||
|
|
mMovementState == CharState_SwimRunLeft ||
|
|
mMovementState == CharState_SwimRunRight;
|
|
}
|
|
|
|
void CharacterController::setAttackingOrSpell(bool attackingOrSpell) const
|
|
{
|
|
mPtr.getClass().getCreatureStats(mPtr).setAttackingOrSpell(attackingOrSpell);
|
|
}
|
|
|
|
void CharacterController::castSpell(const std::string& spellId, bool manualSpell)
|
|
{
|
|
setAttackingOrSpell(true);
|
|
mCastingManualSpell = manualSpell;
|
|
ActionSpell action = ActionSpell(spellId);
|
|
action.prepare(mPtr);
|
|
}
|
|
|
|
void CharacterController::setAIAttackType(const std::string& attackType)
|
|
{
|
|
mAttackType = attackType;
|
|
}
|
|
|
|
std::string CharacterController::getRandomAttackType()
|
|
{
|
|
MWBase::World* world = MWBase::Environment::get().getWorld();
|
|
float random = Misc::Rng::rollProbability(world->getPrng());
|
|
if (random >= 2/3.f)
|
|
return "thrust";
|
|
if (random >= 1/3.f)
|
|
return "slash";
|
|
return "chop";
|
|
}
|
|
|
|
bool CharacterController::readyToPrepareAttack() const
|
|
{
|
|
return (mHitState == CharState_None || mHitState == CharState_Block)
|
|
&& mUpperBodyState <= UpperCharState_WeapEquiped;
|
|
}
|
|
|
|
bool CharacterController::readyToStartAttack() const
|
|
{
|
|
if (mHitState != CharState_None && mHitState != CharState_Block)
|
|
return false;
|
|
|
|
return mUpperBodyState == UpperCharState_WeapEquiped;
|
|
}
|
|
|
|
float CharacterController::getAttackStrength() const
|
|
{
|
|
return mAttackStrength;
|
|
}
|
|
|
|
bool CharacterController::getAttackingOrSpell() const
|
|
{
|
|
return mPtr.getClass().getCreatureStats(mPtr).getAttackingOrSpell();
|
|
}
|
|
|
|
void CharacterController::setActive(int active) const
|
|
{
|
|
mAnimation->setActive(active);
|
|
}
|
|
|
|
void CharacterController::setHeadTrackTarget(const MWWorld::ConstPtr &target)
|
|
{
|
|
mHeadTrackTarget = target;
|
|
}
|
|
|
|
void CharacterController::playSwishSound(float attackStrength) const
|
|
{
|
|
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
|
|
|
|
std::string sound = "Weapon Swish";
|
|
if(attackStrength < 0.5f)
|
|
sndMgr->playSound3D(mPtr, sound, 1.0f, 0.8f); //Weak attack
|
|
else if(attackStrength < 1.0f)
|
|
sndMgr->playSound3D(mPtr, sound, 1.0f, 1.0f); //Medium attack
|
|
else
|
|
sndMgr->playSound3D(mPtr, sound, 1.0f, 1.2f); //Strong attack
|
|
}
|
|
|
|
void CharacterController::updateHeadTracking(float duration)
|
|
{
|
|
const osg::Node* head = mAnimation->getNode("Bip01 Head");
|
|
if (!head)
|
|
return;
|
|
|
|
double zAngleRadians = 0.f;
|
|
double xAngleRadians = 0.f;
|
|
|
|
if (!mHeadTrackTarget.isEmpty())
|
|
{
|
|
osg::NodePathList nodepaths = head->getParentalNodePaths();
|
|
if (nodepaths.empty())
|
|
return;
|
|
osg::Matrixf mat = osg::computeLocalToWorld(nodepaths[0]);
|
|
osg::Vec3f headPos = mat.getTrans();
|
|
|
|
osg::Vec3f direction;
|
|
if (const MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mHeadTrackTarget))
|
|
{
|
|
const osg::Node* node = anim->getNode("Head");
|
|
if (node == nullptr)
|
|
node = anim->getNode("Bip01 Head");
|
|
if (node != nullptr)
|
|
{
|
|
nodepaths = node->getParentalNodePaths();
|
|
if (!nodepaths.empty())
|
|
direction = osg::computeLocalToWorld(nodepaths[0]).getTrans() - headPos;
|
|
}
|
|
else
|
|
// no head node to look at, fall back to look at center of collision box
|
|
direction = MWBase::Environment::get().getWorld()->aimToTarget(mPtr, mHeadTrackTarget, false);
|
|
}
|
|
direction.normalize();
|
|
|
|
if (!mPtr.getRefData().getBaseNode())
|
|
return;
|
|
const osg::Vec3f actorDirection = mPtr.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,1,0);
|
|
|
|
zAngleRadians = std::atan2(actorDirection.x(), actorDirection.y()) - std::atan2(direction.x(), direction.y());
|
|
zAngleRadians = Misc::normalizeAngle(zAngleRadians - mAnimation->getHeadYaw()) + mAnimation->getHeadYaw();
|
|
zAngleRadians *= (1 - direction.z() * direction.z());
|
|
xAngleRadians = std::asin(direction.z());
|
|
}
|
|
|
|
const double xLimit = osg::DegreesToRadians(40.0);
|
|
const double zLimit = osg::DegreesToRadians(30.0);
|
|
double zLimitOffset = mAnimation->getUpperBodyYawRadians();
|
|
xAngleRadians = std::clamp(xAngleRadians, -xLimit, xLimit);
|
|
zAngleRadians = std::clamp(zAngleRadians, -zLimit + zLimitOffset, zLimit + zLimitOffset);
|
|
|
|
float factor = duration*5;
|
|
factor = std::min(factor, 1.f);
|
|
xAngleRadians = (1.f-factor) * mAnimation->getHeadPitch() + factor * xAngleRadians;
|
|
zAngleRadians = (1.f-factor) * mAnimation->getHeadYaw() + factor * zAngleRadians;
|
|
|
|
mAnimation->setHeadPitch(xAngleRadians);
|
|
mAnimation->setHeadYaw(zAngleRadians);
|
|
}
|
|
|
|
}
|