1
0
mirror of https://gitlab.com/OpenMW/openmw.git synced 2025-01-31 06:32:39 +00:00
OpenMW/apps/openmw/mwmechanics/character.cpp

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

2922 lines
120 KiB
C++
Raw Normal View History

/*
* 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 <sstream>
2015-06-14 23:13:26 +02:00
#include <components/esm/records.hpp>
#include <components/misc/mathutil.hpp>
#include <components/misc/resourcehelpers.hpp>
2015-04-25 01:20:07 +02:00
#include <components/misc/rng.hpp>
#include <components/misc/strings/algorithm.hpp>
2015-01-31 23:27:34 +01:00
#include <components/settings/settings.hpp>
#include <components/sceneutil/positionattitudetransform.hpp>
2013-01-16 10:45:18 -08:00
#include "../mwrender/animation.hpp"
2013-01-16 15:52:03 -08:00
#include "../mwbase/environment.hpp"
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/soundmanager.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwbase/world.hpp"
2013-01-16 15:52:03 -08:00
#include "../mwworld/class.hpp"
2014-02-23 20:11:05 +01:00
#include "../mwworld/esmstore.hpp"
2013-05-01 10:19:16 -07:00
#include "../mwworld/inventorystore.hpp"
#include "../mwworld/player.hpp"
#include "../mwworld/spellcaststate.hpp"
2016-06-17 23:07:16 +09:00
#include "actorutil.hpp"
#include "aicombataction.hpp"
2016-06-17 23:07:16 +09:00
#include "creaturestats.hpp"
#include "movement.hpp"
#include "npcstats.hpp"
#include "security.hpp"
#include "spellcasting.hpp"
#include "weapontype.hpp"
2016-06-17 23:07:16 +09:00
namespace
{
std::string_view getBestAttack(const ESM::Weapon* weapon)
{
2022-02-11 19:26:13 +01:00
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";
2022-09-22 21:26:05 +03:00
else
return "chop";
2022-09-22 21:26:05 +03:00
}
2022-06-12 02:52:49 +03:00
// Converts a movement Run state to its equivalent Walk state, if there is one.
MWMechanics::CharacterState runStateToWalkState(MWMechanics::CharacterState state)
2022-09-22 21:26:05 +03:00
{
using namespace MWMechanics;
switch (state)
2022-09-22 21:26:05 +03:00
{
2022-06-12 02:52:49 +03:00
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;
2022-09-22 21:26:05 +03:00
default:
2022-06-12 02:52:49 +03:00
return state;
2022-09-22 21:26:05 +03:00
}
}
// Converts a Hit state to its equivalent Death state.
MWMechanics::CharacterState hitStateToDeathState(MWMechanics::CharacterState state)
2022-09-22 21:26:05 +03:00
{
using namespace MWMechanics;
switch (state)
2022-09-22 21:26:05 +03:00
{
case CharState_SwimKnockDown:
return CharState_SwimDeathKnockDown;
case CharState_SwimKnockOut:
return CharState_SwimDeathKnockOut;
case CharState_KnockDown:
return CharState_DeathKnockDown;
2022-07-06 10:48:22 +00:00
case CharState_KnockOut:
return CharState_DeathKnockOut;
default:
return CharState_None;
}
2022-09-22 21:26:05 +03:00
}
// Converts a movement state to its equivalent base animation group as long as it is a movement state.
std::string_view 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";
2022-09-22 21:26:05 +03:00
case CharState_SwimWalkForward:
return "swimwalkforward";
case CharState_SwimWalkBack:
return "swimwalkback";
case CharState_SwimWalkLeft:
return "swimwalkleft";
case CharState_SwimWalkRight:
return "swimwalkright";
2022-09-22 21:26:05 +03:00
case CharState_RunForward:
return "runforward";
case CharState_RunBack:
return "runback";
case CharState_RunLeft:
return "runleft";
case CharState_RunRight:
return "runright";
2022-09-22 21:26:05 +03:00
case CharState_SwimRunForward:
return "swimrunforward";
case CharState_SwimRunBack:
return "swimrunback";
case CharState_SwimRunLeft:
return "swimrunleft";
2022-06-12 02:52:49 +03:00
case CharState_SwimRunRight:
return "swimrunright";
2022-09-22 21:26:05 +03:00
2022-06-12 02:52:49 +03:00
case CharState_SneakForward:
return "sneakforward";
case CharState_SneakBack:
2022-06-13 15:41:56 +03:00
return "sneakback";
2022-06-12 02:52:49 +03:00
case CharState_SneakLeft:
2022-06-13 15:41:56 +03:00
return "sneakleft";
2022-06-12 02:52:49 +03:00
case CharState_SneakRight:
return "sneakright";
2022-09-22 21:26:05 +03:00
case CharState_TurnLeft:
return "turnleft";
case CharState_TurnRight:
return "turnright";
case CharState_SwimTurnLeft:
return "swimturnleft";
case CharState_SwimTurnRight:
return "swimturnright";
2022-09-22 21:26:05 +03:00
default:
2022-06-13 14:50:59 +03:00
return {};
2022-09-22 21:26:05 +03:00
}
}
// Converts a death state to its equivalent animation group as long as it is a death state.
std::string_view deathStateToAnimGroup(MWMechanics::CharacterState state)
2022-09-22 21:26:05 +03:00
{
2022-06-13 15:41:56 +03:00
using namespace MWMechanics;
switch (state)
2022-09-22 21:26:05 +03:00
{
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";
2022-09-22 21:26:05 +03:00
default:
2022-06-13 15:41:56 +03:00
return {};
2022-09-22 21:26:05 +03:00
}
}
2022-06-13 14:50:59 +03:00
// Converts a hit state to its equivalent animation group as long as it is a hit state.
std::string hitStateToAnimGroup(MWMechanics::CharacterState state)
2022-09-22 21:26:05 +03:00
{
2022-06-13 15:41:56 +03:00
using namespace MWMechanics;
switch (state)
2022-09-22 21:26:05 +03:00
{
case CharState_SwimHit:
return "swimhit";
2022-06-13 14:50:59 +03:00
case CharState_SwimKnockDown:
return "swimknockdown";
case CharState_SwimKnockOut:
return "swimknockout";
2022-09-22 21:26:05 +03:00
2022-06-13 14:50:59 +03:00
case CharState_Hit:
return "hit";
case CharState_KnockDown:
return "knockdown";
case CharState_KnockOut:
2022-06-13 14:50:59 +03:00
return "knockout";
2022-09-22 21:26:05 +03:00
2022-06-13 14:50:59 +03:00
case CharState_Block:
return "shield";
2022-09-22 21:26:05 +03:00
default:
return {};
2022-09-22 21:26:05 +03:00
}
}
2022-06-13 15:41:56 +03:00
// Converts an idle state to its equivalent animation group.
std::string idleStateToAnimGroup(MWMechanics::CharacterState state)
2022-09-22 21:26:05 +03:00
{
2022-06-13 15:41:56 +03:00
using namespace MWMechanics;
switch (state)
2022-09-22 21:26:05 +03:00
{
case CharState_IdleSwim:
2022-06-13 15:41:56 +03:00
return "idleswim";
case CharState_IdleSneak:
return "idlesneak";
case CharState_Idle:
case CharState_SpecialIdle:
2022-06-13 15:41:56 +03:00
return "idle";
default:
return {};
}
2022-09-22 21:26:05 +03:00
}
MWRender::Animation::AnimPriority getIdlePriority(MWMechanics::CharacterState state)
{
using namespace MWMechanics;
2022-06-13 15:41:56 +03:00
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;
2022-09-22 21:26:05 +03:00
}
}
2022-06-13 14:50:59 +03:00
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)
2022-09-22 21:26:05 +03:00
{
2022-06-13 14:50:59 +03:00
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();
2018-08-29 18:38:12 +03:00
const float fallAcroBase = store.find("fFallAcroBase")->mValue.getFloat();
const float fallAcroMult = store.find("fFallAcroMult")->mValue.getFloat();
2022-06-13 14:50:59 +03:00
const float fallDistanceBase = store.find("fFallDistanceBase")->mValue.getFloat();
2018-08-29 18:38:12 +03:00
const float fallDistanceMult = store.find("fFallDistanceMult")->mValue.getFloat();
2022-06-13 14:50:59 +03:00
float x = fallHeight - fallDistanceMin;
x -= (1.5f * acrobaticsSkill) + jumpSpellBonus;
x = std::max(0.0f, x);
2022-06-13 14:50:59 +03:00
2022-06-13 15:41:56 +03:00
float a = fallAcroBase + fallAcroMult * (100 - acrobaticsSkill);
x = fallDistanceBase + fallDistanceMult * x;
x *= a;
return x;
2022-09-22 21:26:05 +03:00
}
2022-06-13 15:41:56 +03:00
return 0.f;
}
2018-08-29 18:38:12 +03:00
bool isRealWeapon(int weaponType)
{
return weaponType != ESM::Weapon::HandToHand && weaponType != ESM::Weapon::Spell
&& weaponType != ESM::Weapon::None;
}
}
namespace MWMechanics
{
2016-05-19 22:30:14 +02:00
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]
2022-09-22 21:26:05 +03:00
if (num)
*num = roll;
return prefix + std::to_string(roll);
2022-09-22 21:26:05 +03:00
}
2017-09-22 15:26:35 +04:00
2022-06-13 14:50:59 +03:00
void CharacterController::clearStateAnimation(std::string& anim) const
{
if (anim.empty())
return;
if (mAnimation)
mAnimation->disable(anim);
2022-06-13 14:50:59 +03:00
anim.clear();
}
2022-06-13 14:50:59 +03:00
void CharacterController::resetCurrentJumpState()
{
clearStateAnimation(mCurrentJump);
mJumpState = JumpState_None;
2013-12-31 13:24:20 +02:00
}
2022-09-22 21:26:05 +03:00
2022-06-13 14:50:59 +03:00
void CharacterController::resetCurrentMovementState()
{
2022-06-13 14:50:59 +03:00
clearStateAnimation(mCurrentMovement);
mMovementState = CharState_None;
}
2022-09-22 21:26:05 +03:00
2022-06-13 14:50:59 +03:00
void CharacterController::resetCurrentIdleState()
{
clearStateAnimation(mCurrentIdle);
mIdleState = CharState_None;
}
2022-09-22 21:26:05 +03:00
2022-06-13 14:50:59 +03:00
void CharacterController::resetCurrentHitState()
{
clearStateAnimation(mCurrentHit);
2022-06-13 14:50:59 +03:00
mHitState = CharState_None;
}
void CharacterController::resetCurrentWeaponState()
{
clearStateAnimation(mCurrentWeapon);
mUpperBodyState = UpperBodyState::None;
}
void CharacterController::resetCurrentDeathState()
{
clearStateAnimation(mCurrentDeath);
mDeathState = CharState_None;
}
2022-06-13 14:50:59 +03:00
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() && !knockout && !recovery && !knockdown;
bool isSwimming = world->isSwimming(mPtr);
2022-09-22 21:26:05 +03:00
stats.setBlock(false);
if (mPtr == getPlayer() && mHitState == CharState_Block && block)
{
mHitState = CharState_None;
resetCurrentIdleState();
}
if (mHitState != CharState_None)
2022-06-13 14:50:59 +03:00
{
2022-06-13 15:41:56 +03:00
if (!mAnimation->isPlaying(mCurrentHit))
2022-09-22 21:26:05 +03:00
{
mHitState = CharState_None;
mCurrentHit.clear();
stats.setKnockedDown(false);
stats.setHitRecovery(false);
2022-06-13 14:50:59 +03:00
resetCurrentIdleState();
2022-09-22 21:26:05 +03:00
}
2022-06-13 14:50:59 +03:00
else if (isKnockedOut())
mAnimation->setLoopingEnabled(mCurrentHit, knockout);
2022-09-22 21:26:05 +03:00
return;
2022-06-13 14:50:59 +03:00
}
2022-09-22 21:26:05 +03:00
if (!knockout && !knockdown && !recovery && !block)
2022-09-22 21:26:05 +03:00
return;
MWRender::Animation::AnimPriority priority(Priority_Knockdown);
std::string_view startKey = "start";
std::string_view stopKey = "stop";
2022-06-13 14:50:59 +03:00
if (knockout)
{
mHitState = isSwimming ? CharState_SwimKnockOut : CharState_KnockOut;
2022-06-13 14:50:59 +03:00
stats.setKnockedDown(true);
2022-09-22 21:26:05 +03:00
}
2022-06-13 14:50:59 +03:00
else if (knockdown)
2022-09-22 21:26:05 +03:00
{
2013-08-18 23:42:56 -07:00
mHitState = isSwimming ? CharState_SwimKnockDown : CharState_KnockDown;
2022-09-22 21:26:05 +03:00
}
2022-06-13 14:50:59 +03:00
else if (recovery)
2022-09-22 21:26:05 +03:00
{
2013-08-18 23:42:56 -07:00
mHitState = isSwimming ? CharState_SwimHit : CharState_Hit;
2022-07-05 18:29:21 +00:00
priority = Priority_Hit;
2022-09-22 21:26:05 +03:00
}
2022-06-13 14:50:59 +03:00
else if (block)
2022-09-22 21:26:05 +03:00
{
mHitState = CharState_Block;
2022-07-05 18:29:21 +00:00
priority = Priority_Hit;
priority[MWRender::Animation::BoneGroup_LeftArm] = Priority_Block;
priority[MWRender::Animation::BoneGroup_LowerBody] = Priority_WeaponLowerBody;
2022-06-13 14:50:59 +03:00
startKey = "block start";
stopKey = "block stop";
}
mCurrentHit = hitStateToAnimGroup(mHitState);
2013-12-31 13:24:20 +02:00
if (isRecovery())
2022-09-22 21:26:05 +03:00
{
2022-06-13 14:50:59 +03:00
mCurrentHit = chooseRandomGroup(mCurrentHit);
if (mHitState == CharState_SwimHit && !mAnimation->hasAnimation(mCurrentHit))
mCurrentHit = chooseRandomGroup(hitStateToAnimGroup(CharState_Hit));
2022-09-22 21:26:05 +03:00
}
2018-08-20 22:04:02 +04:00
if (!mAnimation->hasAnimation(mCurrentHit))
2022-09-22 21:26:05 +03:00
{
2022-06-13 14:50:59 +03:00
// 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();
2022-06-13 14:50:59 +03:00
stats.setKnockedDown(false);
stats.setHitRecovery(false);
resetCurrentIdleState();
2022-06-13 12:59:18 +03:00
return;
}
// Cancel upper body animations
if (isKnockedOut() || isKnockedDown())
2022-09-22 21:26:05 +03:00
{
2022-06-13 12:59:18 +03:00
if (!mCurrentWeapon.empty())
mAnimation->disable(mCurrentWeapon);
if (mUpperBodyState > UpperBodyState::WeaponEquipped)
2022-09-22 21:26:05 +03:00
{
2022-06-13 12:59:18 +03:00
mUpperBodyState = UpperBodyState::WeaponEquipped;
if (mWeaponType > ESM::Weapon::None)
2022-06-13 12:59:18 +03:00
mAnimation->showWeapons(true);
2022-09-22 21:26:05 +03:00
}
else if (mUpperBodyState < UpperBodyState::WeaponEquipped)
2022-09-22 21:26:05 +03:00
{
mUpperBodyState = UpperBodyState::None;
2022-09-22 21:26:05 +03:00
}
}
2022-06-13 12:59:18 +03:00
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)
2022-09-22 21:26:05 +03:00
{
if (!mCurrentJump.empty())
resetCurrentIdleState();
resetCurrentJumpState();
return;
}
std::string_view weapShortGroup = getWeaponShortGroup(mWeaponType);
std::string jumpAnimName = "jump";
jumpAnimName += weapShortGroup;
MWRender::Animation::BlendMask jumpmask = MWRender::Animation::BlendMask_All;
if (!weapShortGroup.empty() && !mAnimation->hasAnimation(jumpAnimName))
2022-06-13 11:34:17 +03:00
jumpAnimName = fallbackShortWeaponGroup("jump", &jumpmask);
if (!mAnimation->hasAnimation(jumpAnimName))
2022-09-22 21:26:05 +03:00
{
if (!mCurrentJump.empty())
resetCurrentIdleState();
resetCurrentJumpState();
return;
2022-09-22 21:26:05 +03:00
}
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",
2022-06-13 12:59:18 +03:00
"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
2019-08-09 12:58:20 +04:00
{
if (mPtr.getType() == ESM::Container::sRecordId)
2022-09-22 21:26:05 +03:00
{
if (!mAnimation->hasAnimation("containeropen"))
return true;
2019-08-09 12:58:20 +04:00
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"))
2019-08-09 12:10:28 +04:00
return false;
2022-09-22 21:26:05 +03:00
}
2019-08-09 12:10:28 +04:00
return true;
}
void CharacterController::onClose() const
2022-09-22 21:26:05 +03:00
{
if (mPtr.getType() == ESM::Container::sRecordId)
2022-09-22 21:26:05 +03:00
{
if (!mAnimation->hasAnimation("containerclose"))
2019-08-09 12:10:28 +04:00
return;
float complete, startPoint = 0.f;
bool animPlaying = mAnimation->getInfo("containeropen", &complete);
2021-01-09 13:52:01 +04:00
if (animPlaying)
startPoint = 1.f - complete;
2019-08-09 12:10:28 +04:00
mAnimation->play("containerclose", Priority_Persistent, MWRender::Animation::BlendMask_All, false, 1.0f,
"start", "stop", startPoint, 0);
2022-09-22 21:26:05 +03:00
}
}
2019-08-09 12:10:28 +04:00
std::string_view CharacterController::getWeaponAnimation(int weaponType) const
{
std::string_view weaponGroup = getWeaponType(weaponType)->mLongGroup;
if (isRealWeapon(weaponType) && !mAnimation->hasAnimation(weaponGroup))
2022-09-22 21:26:05 +03:00
{
2019-08-09 12:10:28 +04:00
static const std::string_view oneHandFallback = getWeaponType(ESM::Weapon::LongBladeOneHand)->mLongGroup;
static const std::string_view 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)
2019-08-09 12:58:20 +04:00
weaponGroup = twoHandFallback;
2022-09-22 21:26:05 +03:00
else
2019-08-09 12:58:20 +04:00
weaponGroup = oneHandFallback;
2022-09-22 21:26:05 +03:00
}
else if (weaponType == ESM::Weapon::HandToHand && !mPtr.getClass().isBipedal(mPtr))
return "attack1";
return weaponGroup;
2022-09-22 21:26:05 +03:00
}
std::string_view CharacterController::getWeaponShortGroup(int weaponType) const
{
if (weaponType == ESM::Weapon::HandToHand && !mPtr.getClass().isBipedal(mPtr))
2022-06-13 11:34:17 +03:00
return {};
return getWeaponType(weaponType)->mShortGroup;
2022-06-13 11:34:17 +03:00
}
2022-06-13 11:34:17 +03:00
std::string CharacterController::fallbackShortWeaponGroup(
const std::string& baseGroupName, MWRender::Animation::BlendMask* blendMask) const
{
if (!isRealWeapon(mWeaponType))
{
2019-08-09 12:10:28 +04:00
if (blendMask != nullptr)
2022-06-13 11:34:17 +03:00
*blendMask = MWRender::Animation::BlendMask_LowerBody;
2022-06-13 11:34:17 +03:00
return baseGroupName;
2022-09-22 21:26:05 +03:00
}
2022-06-13 11:34:17 +03:00
static const std::string_view oneHandFallback = getWeaponShortGroup(ESM::Weapon::LongBladeOneHand);
static const std::string_view twoHandFallback = getWeaponShortGroup(ESM::Weapon::LongBladeTwoHand);
2022-07-24 20:43:05 +02:00
std::string groupName = baseGroupName;
const ESM::WeaponType* weapInfo = getWeaponType(mWeaponType);
2022-06-13 11:34:17 +03:00
// 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;
2022-06-13 11:34:17 +03:00
else
2019-08-09 12:10:28 +04:00
groupName += oneHandFallback;
2022-09-22 21:26:05 +03:00
2019-08-09 12:10:28 +04:00
// 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;
2022-09-22 21:26:05 +03:00
if (!mAnimation->hasAnimation(groupName))
{
groupName = baseGroupName;
2019-08-09 12:10:28 +04:00
if (blendMask != nullptr)
*blendMask = MWRender::Animation::BlendMask_LowerBody;
}
2022-06-13 11:34:17 +03:00
return groupName;
2018-08-20 22:04:02 +04:00
}
2022-06-13 11:34:17 +03:00
void CharacterController::refreshMovementAnims(CharacterState movement, bool force)
2018-08-20 22:04:02 +04:00
{
2022-06-13 11:34:17 +03:00
if (movement == mMovementState && !force)
return;
std::string_view movementAnimGroup = movementStateToAnimGroup(movement);
2022-09-22 21:26:05 +03:00
2022-06-13 11:34:17 +03:00
if (movementAnimGroup.empty())
2018-08-20 22:04:02 +04:00
{
if (!mCurrentMovement.empty())
resetCurrentIdleState();
resetCurrentMovementState();
2022-06-13 11:34:17 +03:00
return;
}
2022-06-13 11:34:17 +03:00
std::string movementAnimName{ movementAnimGroup };
2022-06-13 11:34:17 +03:00
mMovementState = movement;
std::string::size_type swimpos = movementAnimName.find("swim");
if (!mAnimation->hasAnimation(movementAnimName))
2022-09-22 21:26:05 +03:00
{
2022-06-13 11:34:17 +03:00
if (swimpos != std::string::npos)
2022-09-22 21:26:05 +03:00
{
2022-06-13 11:34:17 +03:00
movementAnimName.erase(swimpos, 4);
swimpos = std::string::npos;
2022-09-22 21:26:05 +03:00
}
}
2022-06-13 11:34:17 +03:00
MWRender::Animation::BlendMask movemask = MWRender::Animation::BlendMask_All;
std::string_view weapShortGroup = getWeaponShortGroup(mWeaponType);
2022-06-13 11:34:17 +03:00
// Non-biped creatures don't use spellcasting-specific movement animations.
if (!isRealWeapon(mWeaponType) && !mPtr.getClass().isBipedal(mPtr))
weapShortGroup = {};
2022-06-13 11:34:17 +03:00
if (swimpos == std::string::npos && !weapShortGroup.empty())
{
std::string weapMovementAnimName;
// Spellcasting stance turning is a special case
if (mWeaponType == ESM::Weapon::Spell && isTurning())
2022-09-22 21:26:05 +03:00
{
2022-06-13 11:34:17 +03:00
weapMovementAnimName = weapShortGroup;
weapMovementAnimName += movementAnimName;
2022-09-22 21:26:05 +03:00
}
2022-06-13 11:34:17 +03:00
else
2022-09-22 21:26:05 +03:00
{
2022-06-13 11:34:17 +03:00
weapMovementAnimName = movementAnimName;
weapMovementAnimName += weapShortGroup;
2022-09-22 21:26:05 +03:00
}
2022-06-13 11:34:17 +03:00
if (!mAnimation->hasAnimation(weapMovementAnimName))
weapMovementAnimName = fallbackShortWeaponGroup(movementAnimName, &movemask);
2022-09-22 21:26:05 +03:00
2022-06-13 11:34:17 +03:00
movementAnimName = weapMovementAnimName;
}
2022-06-13 11:34:17 +03:00
if (!mAnimation->hasAnimation(movementAnimName))
{
std::string::size_type runpos = movementAnimName.find("run");
if (runpos != std::string::npos)
movementAnimName.replace(runpos, 3, "walk");
2022-09-22 21:26:05 +03:00
2022-06-13 11:34:17 +03:00
if (!mAnimation->hasAnimation(movementAnimName))
2022-09-22 21:26:05 +03:00
{
if (!mCurrentMovement.empty())
2022-06-13 11:34:17 +03:00
resetCurrentIdleState();
resetCurrentMovementState();
2022-09-22 21:26:05 +03:00
return;
}
}
2022-06-13 11:34:17 +03:00
// 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;
2018-08-20 22:04:02 +04:00
2022-06-13 15:41:56 +03:00
clearStateAnimation(mCurrentMovement);
mCurrentMovement = movementAnimName;
// For non-flying creatures, MW uses the Walk animation to calculate the animation velocity
2022-06-13 11:34:17 +03:00
// even if we are running. This must be replicated, otherwise the observed speed would differ drastically.
2022-06-13 15:41:56 +03:00
mAdjustMovementAnimSpeed = true;
if (mPtr.getClass().getType() == ESM::Creature::sRecordId
2022-06-13 11:34:17 +03:00
&& !(mPtr.get<ESM::Creature>()->mBase->mFlags & ESM::Creature::Flies))
2022-09-22 21:26:05 +03:00
{
2022-06-13 15:41:56 +03:00
CharacterState walkState = runStateToWalkState(mMovementState);
std::string_view anim = movementStateToAnimGroup(walkState);
2022-06-13 15:41:56 +03:00
mMovementAnimSpeed = mAnimation->getVelocity(anim);
if (mMovementAnimSpeed <= 1.0f)
2022-09-22 21:26:05 +03:00
{
2022-06-13 15:41:56 +03:00
// 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;
2022-09-22 21:26:05 +03:00
}
}
else
{
mMovementAnimSpeed = mAnimation->getVelocity(mCurrentMovement);
if (mMovementAnimSpeed <= 1.0f)
2022-09-22 21:26:05 +03:00
{
// 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;
2022-09-22 21:26:05 +03:00
}
}
2022-06-13 15:41:56 +03:00
mAnimation->play(
mCurrentMovement, Priority_Movement, movemask, false, 1.f, "start", "stop", startpoint, ~0ul, true);
2022-06-13 15:41:56 +03:00
}
void CharacterController::refreshIdleAnims(CharacterState idle, bool force)
2022-06-13 15:41:56 +03:00
{
// 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 != UpperBodyState::None && mUpperBodyState != UpperBodyState::WeaponEquipped)
|| mMovementState != CharState_None || mHitState != CharState_None)
&& !mPtr.getClass().isBipedal(mPtr))
{
resetCurrentIdleState();
return;
2022-09-22 21:26:05 +03:00
}
if (!force && idle == mIdleState && (mAnimation->isPlaying(mCurrentIdle) || !mAnimQueue.empty()))
return;
2022-09-22 21:26:05 +03:00
mIdleState = idle;
2022-09-22 21:26:05 +03:00
std::string idleGroup = idleStateToAnimGroup(mIdleState);
if (idleGroup.empty())
2022-09-22 21:26:05 +03:00
{
resetCurrentIdleState();
return;
}
2022-06-13 15:41:56 +03:00
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)
2022-09-22 21:26:05 +03:00
{
priority = getIdlePriority(CharState_Idle);
idleGroup = idleStateToAnimGroup(CharState_Idle);
2022-09-22 21:26:05 +03:00
}
2022-06-13 15:41:56 +03:00
if (fallback || mIdleState == CharState_Idle || mIdleState == CharState_SpecialIdle)
2022-09-22 21:26:05 +03:00
{
2022-06-13 15:41:56 +03:00
std::string_view weapShortGroup = getWeaponShortGroup(mWeaponType);
if (!weapShortGroup.empty())
2022-09-22 21:26:05 +03:00
{
2022-06-13 15:41:56 +03:00
std::string weapIdleGroup = idleGroup;
weapIdleGroup += weapShortGroup;
2022-06-13 15:41:56 +03:00
if (!mAnimation->hasAnimation(weapIdleGroup))
weapIdleGroup = fallbackShortWeaponGroup(idleGroup);
idleGroup = weapIdleGroup;
2022-09-22 21:26:05 +03:00
2022-06-13 15:41:56 +03:00
// 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);
}
2022-09-22 21:26:05 +03:00
}
if (!mAnimation->hasAnimation(idleGroup))
2022-09-22 21:26:05 +03:00
{
resetCurrentIdleState();
return;
2022-09-22 21:26:05 +03:00
}
2018-06-11 17:18:51 +04:00
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;
2016-05-19 22:30:14 +02:00
refreshHitRecoilAnims();
refreshJumpAnims(jump, force);
refreshMovementAnims(movement, force);
2013-12-31 13:24:20 +02:00
// idle handled last as it can depend on the other states
refreshIdleAnims(idle, force);
2022-09-22 21:26:05 +03:00
}
2013-12-31 13:24:20 +02:00
void CharacterController::playDeath(float startpoint, CharacterState death)
2014-06-18 15:33:09 +02:00
{
mDeathState = death;
mCurrentDeath = deathStateToAnimGroup(mDeathState);
2014-06-18 15:33:09 +02:00
mPtr.getClass().getCreatureStats(mPtr).setDeathAnimation(mDeathState - CharState_Death1);
2022-09-22 21:26:05 +03:00
2014-06-18 15:33:09 +02:00
// 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();
2014-06-18 15:33:09 +02:00
resetCurrentJumpState();
mMovementAnimationControlled = true;
2022-09-22 21:26:05 +03:00
2014-06-18 15:33:09 +02:00
mAnimation->play(mCurrentDeath, Priority_Death, MWRender::Animation::BlendMask_All, false, 1.0f, "start",
"stop", startpoint, 0);
2014-06-18 15:33:09 +02:00
}
CharacterState CharacterController::chooseRandomDeathState() const
2022-09-22 21:26:05 +03:00
{
int selected = 0;
chooseRandomGroup("death", &selected);
return static_cast<CharacterState>(CharState_Death1 + (selected - 1));
2022-09-22 21:26:05 +03:00
}
void CharacterController::playRandomDeath(float startpoint)
2022-09-22 21:26:05 +03:00
{
if (mPtr == getPlayer())
2022-09-22 21:26:05 +03:00
{
// 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();
2022-09-22 21:26:05 +03:00
}
2018-06-12 09:55:43 +04:00
mDeathState = hitStateToDeathState(mHitState);
if (mDeathState == CharState_None && MWBase::Environment::get().getWorld()->isSwimming(mPtr))
mDeathState = CharState_SwimDeath;
2018-06-12 09:55:43 +04:00
if (mDeathState == CharState_None || !mAnimation->hasAnimation(deathStateToAnimGroup(mDeathState)))
mDeathState = chooseRandomDeathState();
// Do not interrupt scripted animation by death
if (isPersistentAnimPlaying())
return;
playDeath(startpoint, mDeathState);
2022-09-22 21:26:05 +03:00
}
std::string CharacterController::chooseRandomAttackAnimation() const
2022-09-22 21:26:05 +03:00
{
std::string result;
bool isSwimming = MWBase::Environment::get().getWorld()->isSwimming(mPtr);
if (isSwimming)
result = chooseRandomGroup("swimattack");
if (!isSwimming || !mAnimation->hasAnimation(result))
result = chooseRandomGroup("attack");
2013-01-16 10:45:18 -08:00
2015-05-22 00:55:43 +02:00
return result;
2022-09-22 21:26:05 +03:00
}
2015-05-22 00:55:43 +02:00
CharacterController::CharacterController(const MWWorld::Ptr& ptr, MWRender::Animation* anim)
: mPtr(ptr)
, mAnimation(anim)
{
2015-04-25 01:20:07 +02:00
if (!mAnimation)
return;
mAnimation->setTextKeyListener(this);
2022-09-22 21:26:05 +03:00
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));
2022-09-22 21:26:05 +03:00
if (cls.hasInventoryStore(mPtr))
{
getActiveWeapon(mPtr, &mWeaponType);
if (mWeaponType != ESM::Weapon::None)
2022-09-22 21:26:05 +03:00
{
mUpperBodyState = UpperBodyState::WeaponEquipped;
2019-08-09 12:58:20 +04:00
mCurrentWeapon = getWeaponAnimation(mWeaponType);
2022-09-22 21:26:05 +03:00
}
if (mWeaponType != ESM::Weapon::None && mWeaponType != ESM::Weapon::Spell
&& mWeaponType != ESM::Weapon::HandToHand)
2022-09-22 21:26:05 +03:00
{
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);
2022-09-22 21:26:05 +03:00
}
mAnimation->showCarriedLeft(updateCarriedLeftVisible(mWeaponType));
}
if (!cls.getCreatureStats(mPtr).isDead())
{
mIdleState = CharState_Idle;
if (cls.getCreatureStats(mPtr).getFallHeight() > 0)
mJumpState = JumpState_InAir;
}
2022-09-22 21:26:05 +03:00
else
{
const MWMechanics::CreatureStats& cStats = mPtr.getClass().getCreatureStats(mPtr);
if (cStats.isDeathAnimationFinished())
2022-09-22 21:26:05 +03:00
{
// 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();
2022-09-22 21:26:05 +03:00
else
mDeathState = static_cast<CharacterState>(CharState_Death1 + deathanim);
mFloatToSurface = cStats.getHealth().getBase() != 0;
2022-09-22 21:26:05 +03:00
}
// else: nothing to do, will detect death in the next frame and start playing death animation
}
}
2013-07-18 00:35:03 -07:00
else
{
/* Don't accumulate with non-actors. */
mAnimation->setAccumulation(osg::Vec3f(0.f, 0.f, 0.f));
mIdleState = CharState_Idle;
}
// Do not update animation status for dead actors
if (mDeathState == CharState_None && (!cls.isActor() || !cls.getCreatureStats(mPtr).isDead()))
refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true);
mAnimation->runAnimation(0.f);
2013-01-16 10:45:18 -08:00
unpersistAnimationState();
}
2015-05-22 00:55:43 +02:00
CharacterController::~CharacterController()
2015-05-22 00:55:43 +02:00
{
if (mAnimation)
2022-09-22 21:26:05 +03:00
{
2015-05-22 00:55:43 +02:00
persistAnimationState();
mAnimation->setTextKeyListener(nullptr);
2022-09-22 21:26:05 +03:00
}
2015-05-22 00:55:43 +02:00
}
void CharacterController::handleTextKey(
std::string_view groupname, SceneUtil::TextKeyMap::ConstIterator key, const SceneUtil::TextKeyMap& map)
2015-05-22 00:55:43 +02:00
{
std::string_view evt = key->second;
2015-05-22 00:55:43 +02:00
if (evt.substr(0, 7) == "sound: ")
2015-05-22 00:55:43 +02:00
{
2021-08-22 05:46:46 +03:00
MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager();
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
sndMgr->playSound3D(mPtr, ESM::RefId::stringRefId(evt.substr(7)), 1.0f, 1.0f);
return;
2015-05-22 00:55:43 +02:00
}
auto& charClass = mPtr.getClass();
if (evt.substr(0, 10) == "soundgen: ")
2015-05-22 00:55:43 +02:00
{
std::string_view soundgen = evt.substr(10);
2022-09-22 21:26:05 +03:00
2015-05-22 00:55:43 +02:00
// 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_view> tokens;
Misc::StringUtils::split(soundgen, tokens);
soundgen = tokens[0];
2015-05-22 00:55:43 +02:00
if (tokens.size() >= 2)
2022-09-22 21:26:05 +03:00
{
std::stringstream stream;
stream << tokens[1];
stream >> volume;
2022-09-22 21:26:05 +03:00
}
2015-05-22 00:55:43 +02:00
if (tokens.size() >= 3)
2022-09-22 21:26:05 +03:00
{
std::stringstream stream;
stream << tokens[2];
stream >> pitch;
2022-09-22 21:26:05 +03:00
}
}
2022-09-22 21:26:05 +03:00
const auto& sound = charClass.getSoundIdFromSndGen(mPtr, soundgen);
if (!sound.empty())
{
MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager();
if (soundgen == "left" || soundgen == "right")
2022-09-22 21:26:05 +03:00
{
2015-05-22 00:55:43 +02:00
sndMgr->playSound3D(
mPtr, sound, volume, pitch, MWSound::Type::Foot, MWSound::PlayMode::NoPlayerLocal);
2022-09-22 21:26:05 +03:00
}
else
{
sndMgr->playSound3D(mPtr, sound, volume, pitch);
2022-09-22 21:26:05 +03:00
}
}
2022-09-22 21:26:05 +03:00
return;
2015-05-22 00:55:43 +02:00
}
if (evt.substr(0, groupname.size()) != groupname || evt.substr(groupname.size(), 2) != ": ")
2015-05-22 00:55:43 +02:00
{
// 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" || action == "slash hit" || action == "thrust hit" || action == "hit")
2022-09-22 21:26:05 +03:00
{
int attackType = -1;
if (action == "hit")
{
if (groupname == "attack1" || groupname == "swimattack1")
attackType = ESM::Weapon::AT_Chop;
else if (groupname == "attack2" || groupname == "swimattack2")
attackType = ESM::Weapon::AT_Slash;
else if (groupname == "attack3" || groupname == "swimattack3")
attackType = ESM::Weapon::AT_Thrust;
}
else if (action == "chop hit")
attackType = ESM::Weapon::AT_Chop;
else if (action == "slash hit")
attackType = ESM::Weapon::AT_Slash;
else if (action == "thrust hit")
attackType = ESM::Weapon::AT_Thrust;
// We want to avoid hit keys that come out of nowhere (e.g. in the follow animation)
// and processing multiple hit keys for a single attack
if (mAttackStrength != -1.f)
{
charClass.hit(mPtr, mAttackStrength, attackType, mAttackVictim, mAttackHitPos, mAttackSuccess);
mAttackStrength = -1.f;
}
2015-05-22 00:55:43 +02:00
}
else if (isRandomAttackAnimation(groupname) && action == "start")
2015-05-22 00:55:43 +02:00
{
std::multimap<float, std::string>::const_iterator hitKey = key;
2022-09-22 21:26:05 +03:00
// 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())
2015-05-22 00:55:43 +02:00
{
if (hitKey->second.starts_with(groupname))
{
std::string_view suffix = std::string_view(hitKey->second).substr(groupname.size());
if (suffix == ": hit")
2022-09-22 21:26:05 +03:00
{
hasHitKey = true;
break;
2022-09-22 21:26:05 +03:00
}
if (suffix == ": stop")
2022-09-22 21:26:05 +03:00
break;
}
2015-05-22 00:55:43 +02:00
++hitKey;
2022-09-22 21:26:05 +03:00
}
if (!hasHitKey && mAttackStrength != -1.f)
2022-09-22 21:26:05 +03:00
{
if (groupname == "attack1" || groupname == "swimattack1")
charClass.hit(
mPtr, mAttackStrength, ESM::Weapon::AT_Chop, mAttackVictim, mAttackHitPos, mAttackSuccess);
else if (groupname == "attack2" || groupname == "swimattack2")
charClass.hit(
mPtr, mAttackStrength, ESM::Weapon::AT_Slash, mAttackVictim, mAttackHitPos, mAttackSuccess);
else if (groupname == "attack3" || groupname == "swimattack3")
charClass.hit(
mPtr, mAttackStrength, ESM::Weapon::AT_Thrust, mAttackVictim, mAttackHitPos, mAttackSuccess);
mAttackStrength = -1.f;
2015-05-22 00:55:43 +02:00
}
}
else if (action == "shoot attach")
2015-05-22 00:55:43 +02:00
mAnimation->attachArrow();
else if (action == "shoot release")
{
// See notes for melee release above
if (mAttackStrength != -1.f)
{
mAnimation->releaseArrow(mAttackStrength);
mAttackStrength = -1.f;
}
}
else if (action == "shoot follow attach")
2015-05-22 00:55:43 +02:00
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
2022-09-22 21:26:05 +03:00
// type.
else if (groupname == "spellcast" && action == mAttackType + " release")
2015-05-22 00:55:43 +02:00
{
if (mCanCast)
MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell);
mCastingManualSpell = false;
mCanCast = false;
2015-05-22 00:55:43 +02:00
}
else if (groupname == "containeropen" && action == "loot")
MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Container, mPtr);
2015-05-22 00:55:43 +02:00
}
void CharacterController::updatePtr(const MWWorld::Ptr& ptr)
{
mPtr = ptr;
}
void CharacterController::updateIdleStormState(bool inwater) const
{
2015-06-01 01:57:15 +02:00
if (!mAnimation->hasAnimation("idlestorm") || mUpperBodyState != UpperBodyState::None || inwater)
{
mAnimation->disable("idlestorm");
2022-09-22 21:26:05 +03:00
return;
}
const auto world = MWBase::Environment::get().getWorld();
if (world->isInStorm())
2022-09-22 21:26:05 +03:00
{
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"))
2022-09-22 21:26:05 +03:00
{
int mask = MWRender::Animation::BlendMask_Torso | MWRender::Animation::BlendMask_RightArm;
2022-06-13 12:59:18 +03:00
mAnimation->play("idlestorm", Priority_Storm, mask, true, 1.0f, "start", "stop", 0.0f, ~0ul);
2022-09-22 21:26:05 +03:00
}
else
{
mAnimation->setLoopingEnabled("idlestorm", true);
2022-09-22 21:26:05 +03:00
}
return;
}
2022-09-22 21:26:05 +03:00
}
if (mAnimation->isPlaying("idlestorm"))
2022-09-22 21:26:05 +03:00
{
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);
}
float CharacterController::calculateWindUp() const
{
if (mCurrentWeapon.empty() || mWeaponType == ESM::Weapon::PickProbe || isRandomAttackAnimation(mCurrentWeapon))
return -1.f;
float minAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon + ": " + mAttackType + " min attack");
float maxAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon + ": " + mAttackType + " max attack");
if (minAttackTime == -1.f || minAttackTime >= maxAttackTime)
return -1.f;
return std::clamp(
(mAnimation->getCurrentTime(mCurrentWeapon) - minAttackTime) / (maxAttackTime - minAttackTime), 0.f, 1.f);
2022-09-22 21:26:05 +03:00
}
bool CharacterController::updateWeaponState()
2022-09-22 21:26:05 +03:00
{
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();
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
const ESM::RefId* downSoundId = nullptr;
bool weaponChanged = false;
bool ammunition = true;
float weapSpeed = 1.f;
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);
MWWorld::Ptr newWeapon;
if (weapon != inv.end())
2022-09-22 21:26:05 +03:00
{
newWeapon = *weapon;
if (isRealWeapon(mWeaponType))
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
downSoundId = &newWeapon.getClass().getDownSoundId(newWeapon);
2022-09-22 21:26:05 +03:00
}
// weapon->HtH switch: weapon is empty already, so we need to take sound from previous weapon
else if (!mWeapon.isEmpty() && weaptype == ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell)
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
downSoundId = &mWeapon.getClass().getDownSoundId(mWeapon);
if (mWeapon != newWeapon)
2022-09-22 21:26:05 +03:00
{
mWeapon = newWeapon;
weaponChanged = true;
2022-09-22 21:26:05 +03:00
}
if (stats.getDrawState() == DrawState::Weapon && !mWeapon.isEmpty()
&& mWeapon.getType() == ESM::Weapon::sRecordId)
{
weapSpeed = mWeapon.get<ESM::Weapon>()->mBase->mData.mSpeed;
MWWorld::ConstContainerStoreIterator ammo = inv.getSlot(MWWorld::InventoryStore::Slot_Ammunition);
int ammotype = getWeaponType(mWeapon.get<ESM::Weapon>()->mBase->mData.mType)->mAmmoType;
if (ammotype != ESM::Weapon::None)
ammunition = ammo != inv.end() && ammo->get<ESM::Weapon>()->mBase->mData.mType == ammotype;
// Cancel attack if we no longer have ammunition
if (!ammunition)
{
if (mUpperBodyState == UpperBodyState::AttackWindUp)
2022-09-22 21:26:05 +03:00
{
mAnimation->disable(mCurrentWeapon);
mUpperBodyState = UpperBodyState::WeaponEquipped;
2022-09-22 21:26:05 +03:00
}
setAttackingOrSpell(false);
}
}
MWWorld::ConstContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft);
if (torch != inv.end() && torch->getType() == ESM::Light::sRecordId
&& updateCarriedLeftVisible(mWeaponType))
{
if (mAnimation->isPlaying("shield"))
mAnimation->disable("shield");
mAnimation->play("torch", Priority_Torch, MWRender::Animation::BlendMask_LeftArm, false, 1.0f, "start",
"stop", 0.0f, std::numeric_limits<size_t>::max(), true);
}
else if (mAnimation->isPlaying("torch"))
{
mAnimation->disable("torch");
2022-09-22 21:26:05 +03:00
}
}
// 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
2022-08-08 20:17:02 +03:00
const bool isStillWeapon = isRealWeapon(mWeaponType) && isRealWeapon(weaptype);
// 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 > UpperBodyState::WeaponEquipped)
{
forcestateupdate = true;
if (!mCurrentWeapon.empty())
mAnimation->disable(mCurrentWeapon);
mUpperBodyState = UpperBodyState::WeaponEquipped;
setAttackingOrSpell(false);
mAnimation->showWeapons(true);
}
if (!isKnockedOut() && !isKnockedDown() && !isRecovery())
{
std::string weapgroup;
if ((!isWerewolf || mWeaponType != ESM::Weapon::Spell) && weaptype != mWeaponType
&& mUpperBodyState != UpperBodyState::Unequipping && !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
2019-08-09 12:58:20 +04:00
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))
2022-09-22 21:26:05 +03:00
{
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 = UpperBodyState::Unequipping;
mAnimation->detachArrow();
// If we do not have the "unequip detach" key, hide weapon manually.
if (mAnimation->getTextKeyTime(weapgroup + ": unequip detach") < 0)
mAnimation->showWeapons(false);
}
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
if (downSoundId && !downSoundId->empty())
{
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
sndMgr->playSound3D(mPtr, *downSoundId, 1.0f, 1.0f);
2022-09-22 21:26:05 +03:00
}
}
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)
2022-09-22 21:26:05 +03:00
{
forcestateupdate = true;
bool useShieldAnims = mAnimation->useShieldAnimations();
if (!useShieldAnims)
mAnimation->showCarriedLeft(updateCarriedLeftVisible(weaptype));
2019-08-09 12:58:20 +04:00
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);
2014-02-23 20:11:05 +01:00
if (!isStillWeapon)
{
2022-08-08 20:17:02 +03:00
if (animPlaying)
mAnimation->disable(mCurrentWeapon);
if (weaptype != ESM::Weapon::None)
{
2022-08-08 20:17:02 +03:00
mAnimation->showWeapons(false);
int equipMask = MWRender::Animation::BlendMask_All;
if (useShieldAnims && weaptype != ESM::Weapon::Spell)
2022-09-22 21:26:05 +03:00
{
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 = UpperBodyState::Equipping;
// If we do not have the "equip attach" key, show weapon manually.
2022-08-08 20:17:02 +03:00
if (weaptype != ESM::Weapon::Spell
&& mAnimation->getTextKeyTime(weapgroup + ": equip attach") < 0)
{
2022-08-08 20:17:02 +03:00
mAnimation->showWeapons(true);
2022-09-22 21:26:05 +03:00
}
if (!mWeapon.isEmpty() && mWeaponType != ESM::Weapon::HandToHand && isRealWeapon(weaptype))
2022-09-22 21:26:05 +03:00
{
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
const ESM::RefId& upSoundId = mWeapon.getClass().getUpSoundId(mWeapon);
if (!upSoundId.empty())
sndMgr->playSound3D(mPtr, upSoundId, 1.0f, 1.0f);
2022-09-22 21:26:05 +03:00
}
}
2022-09-22 21:26:05 +03:00
}
if (isWerewolf)
2022-09-22 21:26:05 +03:00
{
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);
}
2022-08-08 20:17:02 +03:00
}
2022-09-22 21:26:05 +03:00
mWeaponType = weaptype;
mCurrentWeapon = weapgroup;
}
// Make sure that we disabled unequipping animation
if (mUpperBodyState == UpperBodyState::Unequipping)
{
resetCurrentWeaponState();
mWeaponType = ESM::Weapon::None;
}
}
}
if (isWerewolf)
{
const ESM::RefId wolfRun = ESM::RefId::stringRefId("WolfRun");
if (isRunning() && !world->isSwimming(mPtr) && mWeaponType == ESM::Weapon::None)
2022-09-22 21:26:05 +03:00
{
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
if (!sndMgr->getSoundPlaying(mPtr, wolfRun))
sndMgr->playSound3D(mPtr, wolfRun, 1.0f, 1.0f, MWSound::Type::Sfx, MWSound::PlayMode::Loop);
2022-09-22 21:26:05 +03:00
}
else
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
sndMgr->stopSound3D(mPtr, wolfRun);
}
// Combat for actors with persistent animations obviously will be buggy
if (isPersistentAnimPlaying())
return forcestateupdate;
2022-07-31 14:43:57 +03:00
float complete = 0.f;
bool animPlaying = false;
ESM::WeaponType::Class weapclass = getWeaponType(mWeaponType)->mWeaponClass;
if (getAttackingOrSpell())
{
bool resetIdle = true;
2022-08-08 20:17:02 +03:00
if (mUpperBodyState == UpperBodyState::WeaponEquipped
&& (mHitState == CharState_None || mHitState == CharState_Block))
{
mAttackStrength = -1.f;
// Randomize attacks for non-bipedal creatures
if (!cls.isBipedal(mPtr)
&& (!mAnimation->hasAnimation(mCurrentWeapon) || isRandomAttackAnimation(mCurrentWeapon)))
{
2022-08-27 13:07:59 +02:00
mCurrentWeapon = chooseRandomAttackAnimation();
}
if (mWeaponType == ESM::Weapon::Spell)
{
2022-08-09 14:10:46 +03:00
// Unset casting flag, otherwise pressing the mouse button down would
// continue casting every frame if there is no animation
setAttackingOrSpell(false);
if (mPtr == getPlayer())
{
2022-08-09 14:10:46 +03:00
// 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)
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
const ESM::RefId& selectedSpell
2022-08-09 14:10:46 +03:00
= MWBase::Environment::get().getWindowManager()->getSelectedSpell();
stats.getSpells().setSelectedSpell(selectedSpell);
}
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
const ESM::RefId* spellid = &stats.getSpells().getSelectedSpell();
bool isMagicItem = false;
// Play hand VFX and allow castSpell use (assuming an animation is going to be played) if
// spellcasting is successful. Manual spellcasting bypasses restrictions.
MWWorld::SpellCastState spellCastResult = MWWorld::SpellCastState::Success;
if (!mCastingManualSpell)
spellCastResult = world->startSpellCast(mPtr);
2022-09-04 15:42:40 +02:00
mCanCast = spellCastResult == MWWorld::SpellCastState::Success;
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
if (spellid->empty() && cls.hasInventoryStore(mPtr))
{
MWWorld::InventoryStore& inv = cls.getInventoryStore(mPtr);
if (inv.getSelectedEnchantItem() != inv.end())
2022-09-22 21:26:05 +03:00
{
const MWWorld::Ptr& enchantItem = *inv.getSelectedEnchantItem();
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
spellid = &enchantItem.getClass().getEnchantment(enchantItem);
2022-09-04 15:42:40 +02:00
isMagicItem = true;
2022-09-22 21:26:05 +03:00
}
}
2022-09-22 21:26:05 +03:00
static const bool useCastingAnimations
= Settings::Manager::getBool("use magic item animations", "Game");
if (isMagicItem && !useCastingAnimations)
{
world->breakInvisibility(mPtr);
// Enchanted items by default do not use casting animations
world->castSpell(mPtr);
resetIdle = false;
2022-09-04 15:42:40 +02:00
// Spellcasting animation needs to "play" for at least one frame to reset the aiming factor
animPlaying = true;
2022-09-04 15:42:40 +02:00
mUpperBodyState = UpperBodyState::Casting;
}
// Play the spellcasting animation/VFX if the spellcasting was successful or failed due to
// insufficient magicka. Used up powers are exempt from this from some reason.
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
else if (!spellid->empty() && spellCastResult != MWWorld::SpellCastState::PowerAlreadyUsed)
{
world->breakInvisibility(mPtr);
MWMechanics::CastSpell cast(mPtr, {}, false, mCastingManualSpell);
2022-09-22 21:26:05 +03:00
2022-09-04 15:42:40 +02:00
const std::vector<ESM::ENAMstruct>* effects{ nullptr };
const MWWorld::ESMStore& store = world->getStore();
if (isMagicItem)
2022-09-22 21:26:05 +03:00
{
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
const ESM::Enchantment* enchantment = store.get<ESM::Enchantment>().find(*spellid);
2022-09-04 15:42:40 +02:00
effects = &enchantment->mEffects.mList;
cast.playSpellCastingEffects(enchantment);
2022-09-22 21:26:05 +03:00
}
else
{
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
const ESM::Spell* spell = store.get<ESM::Spell>().find(*spellid);
2022-09-04 15:42:40 +02:00
effects = &spell->mEffects.mList;
cast.playSpellCastingEffects(spell);
2022-09-22 21:26:05 +03:00
}
if (mCanCast)
2022-09-22 21:26:05 +03:00
{
2022-09-04 15:42:40 +02:00
const ESM::MagicEffect* effect = store.get<ESM::MagicEffect>().find(
effects->back().mEffectID); // use last effect of list for color of VFX_Hands
2022-09-22 21:26:05 +03:00
const ESM::Static* castStatic
= world->getStore().get<ESM::Static>().find(ESM::RefId::stringRefId("VFX_Hands"));
const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS();
2022-09-22 21:26:05 +03:00
if (!effects->empty())
2022-09-22 21:26:05 +03:00
{
if (mAnimation->getNode("Bip01 L Hand"))
mAnimation->addEffect(
Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs), -1, false,
"Bip01 L Hand", effect->mParticle);
2022-09-22 21:26:05 +03:00
if (mAnimation->getNode("Bip01 R Hand"))
mAnimation->addEffect(
Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs), -1, false,
"Bip01 R Hand", effect->mParticle);
2022-09-22 21:26:05 +03:00
}
}
const ESM::ENAMstruct& firstEffect = effects->at(0); // first effect used for casting animation
std::string startKey;
std::string stopKey;
2022-09-04 15:42:40 +02:00
if (isRandomAttackAnimation(mCurrentWeapon))
{
startKey = "start";
stopKey = "stop";
if (mCanCast)
world->castSpell(
mPtr, mCastingManualSpell); // No "release" text key to use, so cast immediately
mCastingManualSpell = false;
mCanCast = false;
}
2022-09-22 21:26:05 +03:00
else
{
switch (firstEffect.mRange)
2022-09-22 21:26:05 +03:00
{
case 0:
mAttackType = "self";
2022-09-22 21:26:05 +03:00
break;
case 1:
mAttackType = "touch";
2022-09-22 21:26:05 +03:00
break;
case 2:
mAttackType = "target";
2022-09-22 21:26:05 +03:00
break;
}
2022-09-04 15:42:40 +02:00
startKey = mAttackType + " start";
stopKey = mAttackType + " stop";
2022-09-22 21:26:05 +03:00
}
mAnimation->play(mCurrentWeapon, priorityWeapon, MWRender::Animation::BlendMask_All, false, 1,
startKey, stopKey, 0.0f, 0);
mUpperBodyState = UpperBodyState::Casting;
}
else
{
resetIdle = false;
}
}
else
{
std::string startKey = "start";
std::string stopKey = "stop";
if (mWeaponType != ESM::Weapon::PickProbe && !isRandomAttackAnimation(mCurrentWeapon))
{
if (weapclass == ESM::WeaponType::Ranged || weapclass == ESM::WeaponType::Thrown)
mAttackType = "shoot";
else if (mPtr == getPlayer())
{
2022-08-08 20:17:02 +03:00
if (Settings::Manager::getBool("best attack", "Game"))
{
2022-08-08 20:17:02 +03:00
if (!mWeapon.isEmpty() && mWeapon.getType() == ESM::Weapon::sRecordId)
2022-09-22 21:26:05 +03:00
{
2022-08-08 20:17:02 +03:00
mAttackType = getBestAttack(mWeapon.get<ESM::Weapon>()->mBase);
2022-09-22 21:26:05 +03:00
}
else
{
// There is no "best attack" for Hand-to-Hand
mAttackType = getRandomAttackType();
2022-09-22 21:26:05 +03:00
}
}
else
{
mAttackType = getMovementBasedAttackType();
}
}
// else if (mPtr != getPlayer()) use mAttackType set by AiCombat
2022-08-09 14:10:46 +03:00
startKey = mAttackType + ' ' + startKey;
stopKey = mAttackType + " max attack";
}
mAnimation->play(mCurrentWeapon, priorityWeapon, MWRender::Animation::BlendMask_All, false,
weapSpeed, startKey, stopKey, 0.0f, 0);
mUpperBodyState = UpperBodyState::AttackWindUp;
// Reset the attack results when the attack starts.
// Strictly speaking this should probably be done when the attack ends,
// but the attack animation might be cancelled in a myriad different ways.
mAttackSuccess = false;
mAttackVictim = MWWorld::Ptr();
mAttackHitPos = osg::Vec3f();
2022-09-22 21:26:05 +03:00
}
}
2014-01-06 22:00:01 +02:00
// We should not break swim and sneak animations
2022-08-08 20:17:02 +03:00
if (resetIdle && mIdleState != CharState_IdleSneak && mIdleState != CharState_IdleSwim)
{
resetCurrentIdleState();
2022-09-22 21:26:05 +03:00
}
}
// Random attack and pick/probe animations never have wind up and are played to their end.
// Other animations must be released when the attack state is unset.
if (mUpperBodyState == UpperBodyState::AttackWindUp
&& (mWeaponType == ESM::Weapon::PickProbe || isRandomAttackAnimation(mCurrentWeapon)
|| !getAttackingOrSpell()))
{
mUpperBodyState = UpperBodyState::AttackRelease;
world->breakInvisibility(mPtr);
if (mWeaponType == ESM::Weapon::PickProbe)
2022-09-22 21:26:05 +03:00
{
// 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();
if (!target.isEmpty())
2022-09-22 21:26:05 +03:00
{
std::string_view resultMessage, resultSound;
if (mWeapon.getType() == ESM::Lockpick::sRecordId)
Security(mPtr).pickLock(target, mWeapon, resultMessage, resultSound);
else if (mWeapon.getType() == ESM::Probe::sRecordId)
Security(mPtr).probeTrap(target, mWeapon, resultMessage, resultSound);
if (!resultMessage.empty())
MWBase::Environment::get().getWindowManager()->messageBox(resultMessage);
if (!resultSound.empty())
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
sndMgr->playSound3D(target, ESM::RefId::stringRefId(resultSound), 1.0f, 1.0f);
2022-09-22 21:26:05 +03:00
}
}
else
{
mAttackStrength = calculateWindUp();
if (mAttackStrength == -1.f)
mAttackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability(prng));
if (weapclass != ESM::WeaponType::Ranged && weapclass != ESM::WeaponType::Thrown)
2022-09-22 21:26:05 +03:00
{
mAttackSuccess = cls.evaluateHit(mPtr, mAttackVictim, mAttackHitPos);
if (!mAttackSuccess)
mAttackStrength = 0.f;
playSwishSound();
2022-09-22 21:26:05 +03:00
}
}
2022-09-22 21:26:05 +03:00
if (mWeaponType == ESM::Weapon::PickProbe || isRandomAttackAnimation(mCurrentWeapon))
mUpperBodyState = UpperBodyState::AttackEnd;
}
2022-09-22 21:26:05 +03:00
if (mUpperBodyState == UpperBodyState::AttackRelease)
{
// The release state might have been reached before reaching the wind-up section. We'll play the new section
// only when the wind-up section is reached.
float currentTime = mAnimation->getCurrentTime(mCurrentWeapon);
float minAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon + ": " + mAttackType + " min attack");
float maxAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon + ": " + mAttackType + " max attack");
if (minAttackTime <= currentTime && currentTime <= maxAttackTime)
2022-09-22 21:26:05 +03:00
{
std::string hit = mAttackType != "shoot" ? "hit" : "release";
2022-09-22 21:26:05 +03:00
float startPoint = 0.f;
2022-09-22 21:26:05 +03:00
// Skip a bit of the pre-hit section based on the attack strength
if (minAttackTime != -1.f && minAttackTime < maxAttackTime)
2022-09-22 21:26:05 +03:00
{
startPoint = 1.f - mAttackStrength;
float minHitTime = mAnimation->getTextKeyTime(mCurrentWeapon + ": " + mAttackType + " min hit");
float hitTime = mAnimation->getTextKeyTime(mCurrentWeapon + ": " + mAttackType + ' ' + hit);
if (maxAttackTime <= minHitTime && minHitTime < hitTime)
startPoint *= (minHitTime - maxAttackTime) / (hitTime - maxAttackTime);
2022-09-22 21:26:05 +03:00
}
mAnimation->disable(mCurrentWeapon);
mAnimation->play(mCurrentWeapon, priorityWeapon, MWRender::Animation::BlendMask_All, false, weapSpeed,
mAttackType + " max attack", mAttackType + ' ' + hit, startPoint, 0);
2022-09-22 21:26:05 +03:00
}
animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete);
2022-09-22 21:26:05 +03:00
// Try playing the "follow" section if the attack animation ended naturally or didn't play at all.
if (!animPlaying || (currentTime >= maxAttackTime && complete >= 1.f))
{
std::string start = "follow start";
std::string stop = "follow stop";
2022-09-22 21:26:05 +03:00
if (mAttackType != "shoot")
2022-09-22 21:26:05 +03:00
{
std::string strength = mAttackStrength < 0.33f ? "small"
: mAttackStrength < 0.66f ? "medium"
: "large";
start = strength + ' ' + start;
stop = strength + ' ' + stop;
2022-09-22 21:26:05 +03:00
}
// Reset attack strength to make extra sure hits that come out of nowhere aren't processed
mAttackStrength = -1.f;
if (animPlaying)
mAnimation->disable(mCurrentWeapon);
MWRender::Animation::AnimPriority priorityFollow(priorityWeapon);
// Follow animations have lower priority than movement for non-biped creatures, logic be damned
if (!cls.isBipedal(mPtr))
priorityFollow = Priority_Default;
mAnimation->play(mCurrentWeapon, priorityFollow, MWRender::Animation::BlendMask_All, false, weapSpeed,
mAttackType + ' ' + start, mAttackType + ' ' + stop, 0.0f, 0);
mUpperBodyState = UpperBodyState::AttackEnd;
2022-09-22 21:26:05 +03:00
animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete);
}
}
if (!animPlaying)
animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete);
if (!animPlaying || complete >= 1.f)
{
if (mUpperBodyState == UpperBodyState::Equipping || mUpperBodyState == UpperBodyState::AttackEnd
|| mUpperBodyState == UpperBodyState::Casting)
2022-09-22 21:26:05 +03:00
{
if (ammunition && mWeaponType == ESM::Weapon::MarksmanCrossbow)
mAnimation->attachArrow();
// Cancel stagger animation at the end of an attack to avoid abrupt transitions
// in favor of a different abrupt transition, like Morrowind
if (mUpperBodyState != UpperBodyState::Equipping && isRecovery())
mAnimation->disable(mCurrentHit);
if (animPlaying)
mAnimation->disable(mCurrentWeapon);
2022-09-22 21:26:05 +03:00
mUpperBodyState = UpperBodyState::WeaponEquipped;
2022-09-22 21:26:05 +03:00
}
else if (mUpperBodyState == UpperBodyState::Unequipping)
{
if (animPlaying)
mAnimation->disable(mCurrentWeapon);
mUpperBodyState = UpperBodyState::None;
}
}
mAnimation->setPitchFactor(0.f);
if (mUpperBodyState > UpperBodyState::WeaponEquipped
&& (weapclass == ESM::WeaponType::Ranged || weapclass == ESM::WeaponType::Thrown))
{
mAnimation->setPitchFactor(1.f);
// A smooth transition can be provided if a pre-wind-up section is defined. Random attack animations never
2022-08-09 14:43:24 +03:00
// have one.
if (mUpperBodyState == UpperBodyState::AttackWindUp && !isRandomAttackAnimation(mCurrentWeapon))
2022-09-22 21:26:05 +03:00
{
float currentTime = mAnimation->getCurrentTime(mCurrentWeapon);
float minAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon + ": " + mAttackType + " min attack");
float startTime = mAnimation->getTextKeyTime(mCurrentWeapon + ": " + mAttackType + " start");
if (startTime <= currentTime && currentTime < minAttackTime)
mAnimation->setPitchFactor((currentTime - startTime) / (minAttackTime - startTime));
2022-09-22 21:26:05 +03:00
}
2022-08-09 14:43:24 +03:00
else if (mUpperBodyState == UpperBodyState::AttackEnd)
{
// 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));
2022-09-22 21:26:05 +03:00
else
mAnimation->setPitchFactor(1.f - complete);
}
2022-09-22 21:26:05 +03:00
}
2014-01-08 16:05:14 +02:00
mAnimation->setAccurateAiming(mUpperBodyState > UpperBodyState::WeaponEquipped);
return forcestateupdate;
}
void CharacterController::updateAnimQueue()
{
if (mAnimQueue.size() > 1)
{
if (mAnimation->isPlaying(mAnimQueue.front().mGroup) == false)
2022-09-22 21:26:05 +03:00
{
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);
2022-09-22 21:26:05 +03:00
}
}
2022-09-22 21:26:05 +03:00
if (!mAnimQueue.empty())
mAnimation->setLoopingEnabled(mAnimQueue.front().mGroup, mAnimQueue.size() <= 1);
}
2022-08-09 14:43:24 +03:00
void CharacterController::update(float duration)
{
MWBase::World* world = MWBase::Environment::get().getWorld();
MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager();
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();
2016-08-22 23:02:57 +02:00
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())
2022-09-22 21:26:05 +03:00
{
bool onground = world->isOnGround(mPtr);
2013-02-18 06:29:16 -08:00
bool inwater = world->isSwimming(mPtr);
2013-08-18 05:59:06 -07:00
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;
2014-08-02 22:42:40 -07:00
bool isrunning = cls.getCreatureStats(mPtr).getStance(MWMechanics::CreatureStats::Stance_Run) && !flying;
2015-06-03 19:41:19 +02:00
CreatureStats& stats = cls.getCreatureStats(mPtr);
2020-09-04 15:03:33 +02:00
Movement& movementSettings = cls.getMovementSettings(mPtr);
2022-09-22 21:26:05 +03:00
// Force Jump Logic
2022-09-22 21:26:05 +03:00
bool isMoving
= (std::abs(movementSettings.mPosition[0]) > .5 || std::abs(movementSettings.mPosition[1]) > .5);
if (!inwater && !flying)
2022-09-22 21:26:05 +03:00
{
2020-09-04 15:03:33 +02:00
// 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;
}
2022-09-22 21:26:05 +03:00
2015-05-31 18:04:14 +02:00
osg::Vec3f rot = cls.getRotationVector(mPtr);
2020-06-22 02:03:38 +02:00
osg::Vec3f vec(movementSettings.asVec3());
2020-08-31 23:16:10 +02:00
movementSettings.mSpeedFactor = std::min(vec.length(), 1.f);
2020-09-04 15:03:33 +02:00
vec.normalize();
static const bool smoothMovement = Settings::Manager::getBool("smooth movement", "Game");
if (smoothMovement)
2022-09-22 21:26:05 +03:00
{
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];
2020-09-04 15:03:33 +02:00
osg::Vec2f targetSpeed
= Misc::rotateVec2f(osg::Vec2f(vec.x(), vec.y()), -angle) * movementSettings.mSpeedFactor;
2020-09-04 15:03:33 +02:00
osg::Vec2f delta = targetSpeed - mSmoothedSpeed;
float speedDelta = movementSettings.mSpeedFactor - mSmoothedSpeed.length();
float deltaLen = delta.length();
2022-09-22 21:26:05 +03:00
2020-09-04 15:03:33 +02:00
float maxDelta;
if (isFirstPersonPlayer)
2020-09-04 15:03:33 +02:00
maxDelta = 1;
else if (std::abs(speedDelta) < deltaLen / 2)
2020-09-04 15:03:33 +02:00
// Turning is smooth for player and less smooth for NPCs (otherwise NPC can miss a path point).
maxDelta = duration * (isPlayer ? playerTurningCoef : 6.f);
2020-09-04 15:03:33 +02:00
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.
2020-09-04 15:03:33 +02:00
maxDelta = 1;
else // In all other cases speeding up and stopping are smooth.
2020-09-04 15:03:33 +02:00
maxDelta = duration * 3.f;
2022-09-22 21:26:05 +03:00
if (deltaLen > maxDelta)
2020-09-04 15:03:33 +02:00
delta *= maxDelta / deltaLen;
mSmoothedSpeed += delta;
2022-09-22 21:26:05 +03:00
osg::Vec2f newSpeed = Misc::rotateVec2f(mSmoothedSpeed, angle);
2020-09-04 15:03:33 +02:00
movementSettings.mSpeedFactor = newSpeed.normalize();
vec.x() = newSpeed.x();
vec.y() = newSpeed.y();
2022-09-22 21:26:05 +03:00
const float eps = 0.001f;
2020-08-31 23:16:10 +02:00
if (movementSettings.mSpeedFactor < eps)
2022-09-22 21:26:05 +03:00
{
movementSettings.mSpeedFactor = 0;
2020-09-04 15:03:33 +02:00
vec.x() = 0;
vec.y() = 1;
2022-09-22 21:26:05 +03:00
}
else if ((vec.y() < 0) != mIsMovingBackward)
2022-09-22 21:26:05 +03:00
{
2020-06-22 02:03:38 +02:00
if (targetSpeed.length() < eps || (movementSettings.mPosition[1] < 0) == mIsMovingBackward)
vec.y() = mIsMovingBackward ? -eps : eps;
2022-09-22 21:26:05 +03:00
}
2020-09-04 15:03:33 +02:00
vec.normalize();
2022-09-22 21:26:05 +03:00
}
2020-06-22 02:03:38 +02:00
float effectiveRotation = rot.z();
2020-09-01 00:37:37 +02:00
bool canMove = cls.getMaxSpeed(mPtr) > 0;
2020-06-22 02:03:38 +02:00
static const bool turnToMovementDirection
= Settings::Manager::getBool("turn to movement direction", "Game");
2020-09-01 00:37:37 +02:00
if (!turnToMovementDirection || isFirstPersonPlayer)
2022-09-22 21:26:05 +03:00
{
2020-09-01 00:37:37 +02:00
movementSettings.mIsStrafing = std::abs(vec.x()) > std::abs(vec.y()) * 2;
stats.setSideMovementAngle(0);
2022-09-22 21:26:05 +03:00
}
2020-09-01 00:37:37 +02:00
else if (canMove)
2022-09-22 21:26:05 +03:00
{
2020-06-22 02:03:38 +02:00
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)
2020-06-22 02:03:38 +02:00
&& std::abs(targetMovementAngle) > osg::DegreesToRadians(60.0f);
if (movementSettings.mIsStrafing)
targetMovementAngle = 0;
float delta = targetMovementAngle - stats.getSideMovementAngle();
float cosDelta = cosf(delta);
2022-09-22 21:26:05 +03:00
2020-09-04 15:03:33 +02:00
if ((vec.y() < 0) == mIsMovingBackward)
movementSettings.mSpeedFactor
*= std::min(std::max(cosDelta, 0.f) + 0.3f, 1.f); // slow down when turn
2020-06-22 02:03:38 +02:00
if (std::abs(delta) < osg::DegreesToRadians(20.0f))
2020-09-04 15:03:33 +02:00
mIsMovingBackward = vec.y() < 0;
2022-09-22 21:26:05 +03:00
float maxDelta = osg::PI * duration * (2.5f - cosDelta);
2021-11-06 07:30:28 +03:00
delta = std::clamp(delta, -maxDelta, maxDelta);
2020-06-22 02:03:38 +02:00
stats.setSideMovementAngle(stats.getSideMovementAngle() + delta);
effectiveRotation += delta;
}
mAnimation->setLegsYawRadians(stats.getSideMovementAngle());
if (stats.getDrawState() == MWMechanics::DrawState::Nothing || inwater)
2020-06-22 02:03:38 +02:00
mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 2);
else
mAnimation->setUpperBodyYawRadians(stats.getSideMovementAngle() / 4);
if (smoothMovement && !isPlayer && !inwater)
mAnimation->setUpperBodyYawRadians(mAnimation->getUpperBodyYawRadians() + mAnimation->getHeadYaw() / 2);
2020-06-22 02:03:38 +02:00
speed = cls.getCurrentSpeed(mPtr);
vec.x() *= speed;
vec.y() *= speed;
2013-04-28 07:53:04 +02:00
if (isKnockedOut() || isKnockedDown() || isRecovery())
2020-06-22 02:03:38 +02:00
vec = osg::Vec3f();
2013-08-18 05:59:06 -07:00
CharacterState movestate = CharState_None;
CharacterState idlestate = CharState_None;
JumpingState jumpstate = JumpState_None;
const MWWorld::Store<ESM::GameSetting>& gmst = world->getStore().get<ESM::GameSetting>();
if (vec.x() != 0.f || vec.y() != 0.f)
{
// advance athletics
if (isPlayer)
{
if (inwater)
2022-09-22 21:26:05 +03:00
{
mSecondsOfSwimming += duration;
while (mSecondsOfSwimming > 1)
{
cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, 1);
mSecondsOfSwimming -= 1;
}
2022-09-22 21:26:05 +03:00
}
else if (isrunning && !sneak)
2022-09-22 21:26:05 +03:00
{
mSecondsOfRunning += duration;
while (mSecondsOfRunning > 1)
{
cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, 0);
mSecondsOfRunning -= 1;
}
2022-09-22 21:26:05 +03:00
}
}
if (!godmode)
{
// reduce fatigue
float fatigueLoss = 0.f;
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))
2022-09-22 21:26:05 +03:00
{
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;
}
2022-09-22 21:26:05 +03:00
}
fatigueLoss *= duration;
fatigueLoss *= movementSettings.mSpeedFactor;
DynamicStat<float> fatigue = cls.getCreatureStats(mPtr).getFatigue();
fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss, fatigue.getCurrent() < 0);
cls.getCreatureStats(mPtr).setFatigue(fatigue);
}
}
bool wasInJump = mInJump;
mInJump = false;
if (!inwater && !flying && solid)
{
// In the air (either getting up —ascending part of jump— or falling).
if (!onground)
{
mInJump = true;
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;
}
// Started a jump.
else if (mJumpState != JumpState_InAir && vec.z() > 0.f && !sneak)
{
float z = cls.getJump(mPtr);
if (z > 0.f)
2022-09-22 21:26:05 +03:00
{
mInJump = true;
if (vec.x() == 0 && vec.y() == 0)
vec.z() = 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;
}
2022-09-22 21:26:05 +03:00
}
}
}
if (!mInJump)
{
if (mJumpState == JumpState_InAir && !flying && solid && wasInJump)
2017-03-25 22:40:11 +04:00
{
float height = cls.getCreatureStats(mPtr).land(isPlayer);
float healthLost = 0.f;
if (!inwater)
healthLost = getFallDamage(mPtr, height);
if (healthLost > 0.0f)
{
2022-07-24 17:43:02 +03:00
const float fatigueTerm = cls.getCreatureStats(mPtr).getFatigueTerm();
// inflict fall damages
if (!godmode)
2022-09-22 21:26:05 +03:00
{
2022-07-24 17:43:02 +03:00
DynamicStat<float> health = cls.getCreatureStats(mPtr).getHealth();
float realHealthLost = healthLost * (1.0f - 0.25f * fatigueTerm);
health.setCurrent(health.getCurrent() - realHealthLost);
cls.getCreatureStats(mPtr).setHealth(health);
sndMgr->playSound3D(mPtr, ESM::RefId::stringRefId("Health Damage"), 1.0f, 1.0f);
if (isPlayer)
2022-07-24 17:43:02 +03:00
MWBase::Environment::get().getWindowManager()->activateHitOverlay();
2022-09-22 21:26:05 +03:00
}
const float acrobaticsSkill = cls.getSkill(mPtr, ESM::Skill::Acrobatics);
if (healthLost > (acrobaticsSkill * fatigueTerm))
2022-09-22 21:26:05 +03:00
{
if (!godmode)
cls.getCreatureStats(mPtr).setKnockedDown(true);
}
else
{
// report acrobatics progression
if (isPlayer)
cls.skillUsageSucceeded(mPtr, ESM::Skill::Acrobatics, 1);
2022-09-22 21:26:05 +03:00
}
}
2022-09-22 21:26:05 +03:00
if (mPtr.getClass().isNpc())
{
std::string_view 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, ESM::RefId::stringRefId(sound), 1.f, 1.f, MWSound::Type::Foot,
MWSound::PlayMode::NoPlayerLocal);
}
}
if (mAnimation->isPlaying(mCurrentJump))
jumpstate = JumpState_Landing;
vec.x() *= scale;
vec.y() *= scale;
vec.z() = 0.0f;
2017-10-31 14:22:24 +01:00
2020-06-22 02:03:38 +02:00
if (movementSettings.mIsStrafing)
2022-09-22 21:26:05 +03:00
{
2020-06-22 02:03:38 +02:00
if (vec.x() > 0.0f)
2013-08-18 23:42:56 -07:00
movestate = (inwater ? (isrunning ? CharState_SwimRunRight : CharState_SwimWalkRight)
: (sneak ? CharState_SneakRight
: (isrunning ? CharState_RunRight : CharState_WalkRight)));
else if (vec.x() < 0.0f)
2013-08-18 23:42:56 -07:00
movestate = (inwater
? (isrunning ? CharState_SwimRunLeft : CharState_SwimWalkLeft)
: (sneak ? CharState_SneakLeft : (isrunning ? CharState_RunLeft : CharState_WalkLeft)));
2022-09-22 21:26:05 +03:00
}
2020-06-22 02:03:38 +02:00
else if (vec.length2() > 0.0f)
2022-09-22 21:26:05 +03:00
{
2020-06-22 02:03:38 +02:00
if (vec.y() >= 0.0f)
2013-08-18 23:42:56 -07:00
movestate = (inwater ? (isrunning ? CharState_SwimRunForward : CharState_SwimWalkForward)
: (sneak ? CharState_SneakForward
: (isrunning ? CharState_RunForward : CharState_WalkForward)));
2022-09-22 21:26:05 +03:00
else
2013-08-18 23:42:56 -07:00
movestate = (inwater
? (isrunning ? CharState_SwimRunBack : CharState_SwimWalkBack)
: (sneak ? CharState_SneakBack : (isrunning ? CharState_RunBack : CharState_WalkBack)));
2022-09-22 21:26:05 +03:00
}
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;
2022-09-22 21:26:05 +03:00
2018-08-20 22:04:02 +04:00
// It seems only bipedal actors use turning animations.
// Also do not use turning animations in the first-person view and when sneaking.
if (!sneak && !isFirstPersonPlayer && mPtr.getClass().isBipedal(mPtr))
2022-09-22 21:26:05 +03:00
{
2020-06-22 02:03:38 +02:00
if (effectiveRotation > rotationThreshold)
2018-08-20 22:04:02 +04:00
movestate = inwater ? CharState_SwimTurnRight : CharState_TurnRight;
2020-06-22 02:03:38 +02:00
else if (effectiveRotation < -rotationThreshold)
2018-08-20 22:04:02 +04:00
movestate = inwater ? CharState_SwimTurnLeft : CharState_TurnLeft;
2022-09-22 21:26:05 +03:00
}
}
}
2020-06-22 02:03:38 +02:00
if (turnToMovementDirection && !isFirstPersonPlayer
2020-12-24 03:00:09 +01:00
&& (movestate == CharState_SwimRunForward || movestate == CharState_SwimWalkForward
2020-06-22 02:03:38 +02:00
|| movestate == CharState_SwimRunBack || movestate == CharState_SwimWalkBack))
2013-08-18 23:42:56 -07:00
{
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);
}
2020-09-04 15:03:33 +02:00
else
2020-12-24 03:00:09 +01:00
mAnimation->setBodyPitchRadians(0);
2022-09-22 21:26:05 +03:00
2020-08-07 20:17:44 +00:00
static const bool swimUpwardCorrection = Settings::Manager::getBool("swim upward correction", "Game");
if (inwater && isPlayer && !isFirstPersonPlayer && swimUpwardCorrection)
2013-08-18 23:42:56 -07:00
{
static const float swimUpwardCoef = Settings::Manager::getFloat("swim upward coef", "Game");
2020-09-04 15:03:33 +02:00
static const float swimForwardCoef = sqrtf(1.0f - swimUpwardCoef * swimUpwardCoef);
vec.z() = std::abs(vec.y()) * swimUpwardCoef;
vec.y() *= swimForwardCoef;
2022-09-22 21:26:05 +03:00
}
2018-08-20 22:04:02 +04:00
// Player can not use smooth turning as NPCs, so we play turning animation a bit to avoid jittering
if (isPlayer)
2022-09-22 21:26:05 +03:00
{
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())
2018-08-20 22:04:02 +04:00
{
2020-06-22 02:03:38 +02:00
if (animPlaying && complete < threshold)
2018-08-20 22:04:02 +04:00
movestate = mMovementState;
}
2013-08-18 23:42:56 -07:00
}
2022-09-22 21:26:05 +03:00
else
{
2018-08-20 22:04:02 +04:00
if (mPtr.getClass().isBipedal(mPtr))
2022-09-22 21:26:05 +03:00
{
2018-08-20 22:04:02 +04:00
if (mTurnAnimationThreshold > 0)
mTurnAnimationThreshold -= duration;
if (movestate == CharState_TurnRight || movestate == CharState_TurnLeft
2018-08-20 22:04:02 +04:00
|| movestate == CharState_SwimTurnRight || movestate == CharState_SwimTurnLeft)
2022-09-22 21:26:05 +03:00
{
mTurnAnimationThreshold = 0.05f;
2022-09-22 21:26:05 +03:00
}
else if (movestate == CharState_None && isTurning() && mTurnAnimationThreshold > 0)
2022-09-22 21:26:05 +03:00
{
movestate = mMovementState;
2022-09-22 21:26:05 +03:00
}
}
}
if (movestate != CharState_None)
2022-09-22 21:26:05 +03:00
{
clearAnimQueue();
jumpstate = JumpState_None;
}
2020-12-24 03:00:09 +01:00
if (mAnimQueue.empty() || inwater || (sneak && mIdleState != CharState_SpecialIdle))
2022-09-22 21:26:05 +03:00
{
2020-12-24 03:00:09 +01:00
if (inwater)
idlestate = CharState_IdleSwim;
else if (sneak && !mInJump)
2020-12-24 03:00:09 +01:00
idlestate = CharState_IdleSneak;
2022-09-22 21:26:05 +03:00
else
2020-12-24 03:00:09 +01:00
idlestate = CharState_Idle;
}
2020-12-24 03:00:09 +01:00
else
updateAnimQueue();
2020-08-07 20:17:44 +00:00
if (!mSkipAnim)
{
refreshCurrentAnims(idlestate, movestate, jumpstate, updateWeaponState());
updateIdleStormState(inwater);
}
if (mInJump)
mMovementAnimationControlled = false;
2022-09-22 21:26:05 +03:00
if (isTurning())
{
// Adjust animation speed from 1.0 to 1.5 multiplier
if (duration > 0)
2022-09-22 21:26:05 +03:00
{
float turnSpeed = std::min(1.5f, std::abs(rot.z()) / duration / static_cast<float>(osg::PI));
mAnimation->adjustSpeedMult(mCurrentMovement, std::max(turnSpeed, 1.0f));
2022-09-22 21:26:05 +03:00
}
}
2018-08-20 22:04:02 +04:00
else if (mMovementState != CharState_None && mAdjustMovementAnimSpeed)
{
2018-08-20 22:04:02 +04:00
// Vanilla caps the played animation speed.
2020-05-11 15:11:32 +03:00
const float maxSpeedMult = 10.f;
const float speedMult = speed / mMovementAnimSpeed;
2018-08-20 22:04:02 +04:00
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);
2022-09-22 21:26:05 +03:00
}
2018-08-20 22:04:02 +04:00
if (!mSkipAnim)
2022-09-22 21:26:05 +03:00
{
2018-08-20 22:04:02 +04:00
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)
2022-09-22 21:26:05 +03:00
{
2018-08-20 22:04:02 +04:00
rot.z() = 0.0f;
world->rotateObject(mPtr, rot, true);
2022-09-22 21:26:05 +03:00
}
2018-08-20 22:04:02 +04:00
}
2022-09-22 21:26:05 +03:00
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())
{
2022-08-08 20:17:02 +03:00
// 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())
2022-09-22 21:26:05 +03:00
{
// 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);
2022-09-22 21:26:05 +03:00
}
}
bool isPersist = isPersistentAnimPlaying();
osg::Vec3f moved = mAnimation->runAnimation(mSkipAnim && !isPersist ? 0.f : duration);
if (duration > 0.0f)
moved /= duration;
else
2015-04-25 01:20:07 +02:00
moved = osg::Vec3f(0.f, 0.f, 0.f);
2022-09-22 21:26:05 +03:00
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);
}
}
2014-01-03 17:06:05 +01:00
if (mFloatToSurface && cls.isActor())
2014-01-02 21:54:41 +02:00
{
2017-09-22 15:26:35 +04:00
if (cls.getCreatureStats(mPtr).isDead()
|| (!godmode
&& cls.getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Paralyze).getModifier() > 0))
2014-01-04 17:55:09 +02:00
{
moved.z() = 1.0;
}
2014-01-02 21:54:41 +02:00
}
// Update movement
if (mMovementAnimationControlled && mPtr.getClass().isActor())
world->queueMovement(mPtr, moved);
2022-09-22 21:26:05 +03:00
mSkipAnim = false;
mAnimation->enableHeadAnimation(cls.isActor() && !cls.getCreatureStats(mPtr).isDead());
}
2022-09-22 21:26:05 +03:00
void CharacterController::persistAnimationState() const
2013-06-27 14:11:20 -07:00
{
ESM::AnimationState& state = mPtr.getRefData().getAnimationState();
2022-09-22 21:26:05 +03:00
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())
2022-09-22 21:26:05 +03:00
{
anim.mLoopCount = mAnimation->getCurrentLoopCount(anim.mGroup);
float complete;
2018-10-09 10:21:12 +04:00
mAnimation->getInfo(anim.mGroup, &complete, nullptr);
anim.mTime = complete;
2022-09-22 21:26:05 +03:00
}
else
{
anim.mLoopCount = iter->mLoopCount;
anim.mTime = 0.f;
2022-09-22 21:26:05 +03:00
}
state.mScriptedAnims.push_back(anim);
}
}
void CharacterController::unpersistAnimationState()
2020-12-22 06:19:18 +03:00
{
const ESM::AnimationState& state = mPtr.getRefData().getAnimationState();
2022-09-22 21:26:05 +03:00
if (!state.mScriptedAnims.empty())
2020-12-22 06:19:18 +03:00
{
clearAnimQueue();
2020-12-22 06:19:18 +03:00
for (ESM::AnimationState::ScriptedAnimations::const_iterator iter = state.mScriptedAnims.begin();
iter != state.mScriptedAnims.end(); ++iter)
2022-09-22 21:26:05 +03:00
{
2017-09-18 01:21:18 -07:00
AnimationQueueEntry entry;
entry.mGroup = iter->mGroup;
entry.mLoopCount = iter->mLoopCount;
entry.mPersist = true;
mAnimQueue.push_back(entry);
2022-09-22 21:26:05 +03:00
}
const ESM::AnimationState::ScriptedAnimation& anim = state.mScriptedAnims.front();
float complete = anim.mTime;
if (anim.mAbsolute)
2022-09-22 21:26:05 +03:00
{
float start = mAnimation->getTextKeyTime(anim.mGroup + ": start");
float stop = mAnimation->getTextKeyTime(anim.mGroup + ": stop");
2021-11-06 07:30:28 +03:00
float time = std::clamp(anim.mTime, start, stop);
complete = (time - start) / (stop - start);
2022-09-22 21:26:05 +03:00
}
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);
2022-09-22 21:26:05 +03:00
}
}
bool CharacterController::playGroup(std::string_view 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;
2017-09-18 01:21:18 -07:00
// 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
2017-09-18 01:21:18 -07:00
// and remove any other animations that were queued.
// This emulates observed behavior from the original allows the script "OutsideBanner" to animate banners
2017-09-18 01:21:18 -07:00
// correctly.
if (!mAnimQueue.empty() && mAnimQueue.front().mGroup == groupname
2017-09-18 01:21:18 -07:00
&& mAnimation->getTextKeyTime(mAnimQueue.front().mGroup + ": loop start") >= 0
&& mAnimation->isPlaying(groupname))
{
float endOfLoop = mAnimation->getTextKeyTime(mAnimQueue.front().mGroup + ": loop stop");
2022-09-22 21:26:05 +03:00
if (endOfLoop < 0) // if no Loop Stop key was found, use the Stop key
2017-09-18 01:21:18 -07:00
endOfLoop = mAnimation->getTextKeyTime(mAnimQueue.front().mGroup + ": stop");
2022-09-22 21:26:05 +03:00
if (endOfLoop > 0 && (mAnimation->getCurrentTime(mAnimQueue.front().mGroup) < endOfLoop))
2022-09-22 21:26:05 +03:00
{
2018-10-09 10:21:12 +04:00
mAnimQueue.resize(1);
return true;
}
}
count = std::max(count, 1);
AnimationQueueEntry entry;
2017-09-18 01:21:18 -07:00
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);
2022-09-22 21:26:05 +03:00
}
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);
2022-09-22 21:26:05 +03:00
return true;
}
2022-08-23 18:25:25 +02:00
void CharacterController::skipAnim()
2022-09-22 21:26:05 +03:00
{
2022-08-23 18:25:25 +02:00
mSkipAnim = true;
2022-09-22 21:26:05 +03:00
}
2022-08-23 18:25:25 +02:00
bool CharacterController::isPersistentAnimPlaying() const
{
if (!mAnimQueue.empty())
2022-09-22 21:26:05 +03:00
{
const AnimationQueueEntry& first = mAnimQueue.front();
return first.mPersist && isAnimPlaying(first.mGroup);
2022-09-22 21:26:05 +03:00
}
return false;
2022-09-22 21:26:05 +03:00
}
2017-09-18 01:21:18 -07:00
bool CharacterController::isAnimPlaying(std::string_view groupName) const
2022-09-22 21:26:05 +03:00
{
if (mAnimation == nullptr)
return false;
return mAnimation->isPlaying(groupName);
2022-09-22 21:26:05 +03:00
}
2018-06-11 17:18:51 +04:00
2017-09-18 01:21:18 -07:00
void CharacterController::clearAnimQueue(bool clearPersistAnims)
{
2017-09-18 01:21:18 -07:00
// Do not interrupt scripted animations, if we want to keep them
if ((!isPersistentAnimPlaying() || clearPersistAnims) && !mAnimQueue.empty())
mAnimation->disable(mAnimQueue.front().mGroup);
2017-09-18 01:21:18 -07:00
if (clearPersistAnims)
2022-09-22 21:26:05 +03:00
{
2017-09-18 01:21:18 -07:00
mAnimQueue.clear();
return;
2022-09-22 21:26:05 +03:00
}
2017-09-18 01:21:18 -07:00
for (AnimationQueue::iterator it = mAnimQueue.begin(); it != mAnimQueue.end();)
{
if (!it->mPersist)
it = mAnimQueue.erase(it);
2022-09-22 21:26:05 +03:00
else
2017-09-18 01:21:18 -07:00
++it;
}
2017-09-18 01:21:18 -07:00
}
2017-09-18 01:21:18 -07:00
void CharacterController::forceStateUpdate()
2022-09-22 21:26:05 +03:00
{
2017-09-18 01:21:18 -07:00
if (!mAnimation)
2022-09-22 21:26:05 +03:00
return;
2017-09-18 01:21:18 -07:00
clearAnimQueue();
2017-09-18 01:21:18 -07:00
// Make sure we canceled the current attack or spellcasting,
// because we disabled attack animations anyway.
mCanCast = false;
mCastingManualSpell = false;
setAttackingOrSpell(false);
2017-09-18 01:21:18 -07:00
if (mUpperBodyState != UpperBodyState::None)
mUpperBodyState = UpperBodyState::WeaponEquipped;
2017-09-18 01:21:18 -07:00
refreshCurrentAnims(mIdleState, mMovementState, mJumpState, true);
2013-05-12 05:08:01 -07:00
if (mDeathState != CharState_None)
2022-09-22 21:26:05 +03:00
{
playRandomDeath();
2022-09-22 21:26:05 +03:00
}
mAnimation->runAnimation(0.f);
2017-09-18 01:21:18 -07:00
}
2022-09-22 21:26:05 +03:00
CharacterController::KillResult CharacterController::kill()
2017-09-18 01:21:18 -07:00
{
if (mDeathState == CharState_None)
2022-09-22 21:26:05 +03:00
{
playRandomDeath();
2017-09-18 01:21:18 -07:00
resetCurrentIdleState();
return Result_DeathAnimStarted;
2022-09-22 21:26:05 +03:00
}
2017-09-18 01:21:18 -07:00
MWMechanics::CreatureStats& cStats = mPtr.getClass().getCreatureStats(mPtr);
if (isAnimPlaying(mCurrentDeath))
return Result_DeathAnimPlaying;
if (!cStats.isDeathAnimationFinished())
2022-09-22 21:26:05 +03:00
{
2017-09-18 01:21:18 -07:00
cStats.setDeathAnimationFinished(true);
return Result_DeathAnimJustFinished;
2022-09-22 21:26:05 +03:00
}
return Result_DeathAnimFinished;
}
void CharacterController::resurrect()
2022-09-22 21:26:05 +03:00
{
if (mDeathState == CharState_None)
return;
resetCurrentDeathState();
mWeaponType = ESM::Weapon::None;
2022-09-22 21:26:05 +03:00
}
void CharacterController::updateContinuousVfx() const
2022-09-22 21:26:05 +03:00
{
// 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)
2022-09-22 21:26:05 +03:00
{
if (mPtr.getClass().getCreatureStats(mPtr).isDeathAnimationFinished()
|| mPtr.getClass()
.getCreatureStats(mPtr)
.getMagicEffects()
.get(MWMechanics::EffectKey(effectId))
.getMagnitude()
<= 0)
mAnimation->removeEffect(effectId);
2022-09-22 21:26:05 +03:00
}
}
void CharacterController::updateMagicEffects() const
2022-09-22 21:26:05 +03:00
{
if (!mPtr.getClass().isActor())
return;
float light
2018-10-09 10:21:12 +04:00
= 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;
2018-06-11 17:18:51 +04:00
bool vampire
2022-06-11 03:24:01 +03:00
= mPtr.getClass().getCreatureStats(mPtr).getMagicEffects().get(ESM::MagicEffect::Vampirism).getMagnitude()
2022-09-22 21:26:05 +03:00
> 0.0f;
2022-06-11 03:24:01 +03:00
mAnimation->setVampire(vampire);
}
2018-06-11 17:18:51 +04:00
void CharacterController::setVisibility(float visibility) const
{
2022-06-11 03:24:01 +03:00
// We should take actor's invisibility in account
if (mPtr.getClass().isActor())
2022-09-22 21:26:05 +03:00
{
2018-06-11 17:18:51 +04:00
float alpha = 1.f;
if (mPtr.getClass()
.getCreatureStats(mPtr)
.getMagicEffects()
2018-06-11 17:18:51 +04:00
.get(ESM::MagicEffect::Invisibility)
.getModifier()) // Ignore base magnitude (see bug #3555).
2022-09-22 21:26:05 +03:00
{
if (mPtr == getPlayer())
alpha = 0.25f;
2018-06-11 17:18:51 +04:00
else
alpha = 0.05f;
}
float chameleon = mPtr.getClass()
.getCreatureStats(mPtr)
.getMagicEffects()
.get(ESM::MagicEffect::Chameleon)
.getMagnitude();
if (chameleon)
2022-09-22 21:26:05 +03:00
{
alpha *= std::clamp(1.f - chameleon / 100.f, 0.25f, 0.75f);
2022-09-22 21:26:05 +03:00
}
visibility = std::min(visibility, alpha);
2022-09-22 21:26:05 +03:00
}
// TODO: implement a dithering shader rather than just change object transparency.
mAnimation->setAlpha(visibility);
2022-09-22 21:26:05 +03:00
}
std::string_view CharacterController::getMovementBasedAttackType() const
{
2014-01-02 21:54:41 +02:00
float* move = mPtr.getClass().getMovementSettings(mPtr).mPosition;
if (std::abs(move[1]) > std::abs(move[0]) + 0.2f) // forward-backward
2014-01-02 21:54:41 +02:00
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 == UpperBodyState::AttackWindUp;
}
bool CharacterController::isCastingSpell() const
{
return mCastingManualSpell || mUpperBodyState == UpperBodyState::Casting;
}
bool CharacterController::isReadyToBlock() const
{
return updateCarriedLeftVisible(mWeaponType);
}
bool CharacterController::isKnockedDown() const
{
return mHitState == CharState_KnockDown || mHitState == CharState_SwimKnockDown;
}
2017-09-22 15:26:35 +04:00
bool CharacterController::isKnockedOut() const
{
2017-09-22 15:26:35 +04:00
return mHitState == CharState_KnockOut || mHitState == CharState_SwimKnockOut;
}
bool CharacterController::isTurning() const
{
return mMovementState == CharState_TurnLeft || mMovementState == CharState_TurnRight
|| mMovementState == CharState_SwimTurnLeft || mMovementState == CharState_SwimTurnRight;
}
2017-09-22 15:49:42 +04:00
bool CharacterController::isRecovery() const
{
return mHitState == CharState_Hit || mHitState == CharState_SwimHit;
}
bool CharacterController::isAttackingOrSpell() const
{
return mUpperBodyState != UpperBodyState::None && mUpperBodyState != UpperBodyState::WeaponEquipped;
}
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;
}
2022-06-12 03:02:23 +03:00
void CharacterController::setAttackingOrSpell(bool attackingOrSpell) const
{
mPtr.getClass().getCreatureStats(mPtr).setAttackingOrSpell(attackingOrSpell);
}
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
void CharacterController::castSpell(const ESM::RefId& spellId, bool manualSpell)
{
setAttackingOrSpell(true);
mCastingManualSpell = manualSpell;
ActionSpell action = ActionSpell(spellId);
action.prepare(mPtr);
}
void CharacterController::setAIAttackType(std::string_view attackType)
{
mAttackType = attackType;
}
std::string_view 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 <= UpperBodyState::WeaponEquipped;
}
2015-07-03 05:58:12 +02:00
bool CharacterController::readyToStartAttack() const
{
if (mHitState != CharState_None && mHitState != CharState_Block)
return false;
return mUpperBodyState == UpperBodyState::WeaponEquipped;
}
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);
2022-09-22 21:26:05 +03:00
}
void CharacterController::setHeadTrackTarget(const MWWorld::ConstPtr& target)
{
mHeadTrackTarget = target;
}
void CharacterController::playSwishSound() const
2022-09-22 21:26:05 +03:00
{
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
static ESM::RefId weaponSwish = ESM::RefId::stringRefId("Weapon Swish");
const ESM::RefId* soundId = &weaponSwish;
float volume = 0.98f + mAttackStrength * 0.02f;
float pitch = 0.75f + mAttackStrength * 0.4f;
const MWWorld::Class& cls = mPtr.getClass();
if (cls.isNpc() && cls.getNpcStats(mPtr).isWerewolf())
2022-09-22 21:26:05 +03:00
{
MWBase::World* world = MWBase::Environment::get().getWorld();
2015-05-31 18:04:14 +02:00
const MWWorld::ESMStore& store = world->getStore();
const ESM::Sound* sound = store.get<ESM::Sound>().searchRandom("WolfSwing", world->getPrng());
if (sound)
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
soundId = &sound->mId;
2022-09-22 21:26:05 +03:00
}
2015-05-31 18:04:14 +02:00
Initial commit: In ESM structures, replace the string members that are RefIds to other records, to a new strong type The strong type is actually just a string underneath, but this will help in the future to have a distinction so it's easier to search and replace when we use an integer ID Slowly going through all the changes to make, still hundreds of errors a lot of functions/structures use std::string or stringview to designate an ID. So it takes time Continues slowly replacing ids. There are technically more and more compilation errors I have good hope that there is a point where the amount of errors will dramatically go down as all the main functions use the ESM::RefId type Continue moving forward, changes to the stores slowly moving along Starting to see the fruit of those changes. still many many error, but more and more Irun into a situation where a function is sandwiched between two functions that use the RefId type. More replacements. Things are starting to get easier I can see more and more often the issue is that the function is awaiting a RefId, but is given a string there is less need to go down functions and to fix a long list of them. Still moving forward, and for the first time error count is going down! Good pace, not sure about topics though, mId and mName are actually the same thing and are used interchangeably Cells are back to using string for the name, haven't fixed everything yet. Many other changes Under the bar of 400 compilation errors. more good progress <100 compile errors! More progress Game settings store can use string for find, it was a bit absurd how every use of it required to create refId from string some more progress on other fronts Mostly game settings clean one error opened a lot of other errors. Down to 18, but more will prbably appear only link errors left?? Fixed link errors OpenMW compiles, and launches, with some issues, but still!
2022-09-25 13:17:09 +02:00
if (!soundId->empty())
MWBase::Environment::get().getSoundManager()->playSound3D(mPtr, *soundId, volume, pitch);
2022-09-22 21:26:05 +03:00
}
2015-05-31 18:04:14 +02:00
void CharacterController::updateHeadTracking(float duration)
{
const osg::Node* head = mAnimation->getNode("Bip01 Head");
if (!head)
2015-05-31 18:04:14 +02:00
return;
double zAngleRadians = 0.f;
double xAngleRadians = 0.f;
2022-09-22 21:26:05 +03:00
if (!mHeadTrackTarget.isEmpty())
{
2015-05-31 18:04:14 +02:00
osg::NodePathList nodepaths = head->getParentalNodePaths();
2018-10-09 10:21:12 +04:00
if (nodepaths.empty())
2022-09-22 21:26:05 +03:00
return;
2015-05-31 18:04:14 +02:00
osg::Matrixf mat = osg::computeLocalToWorld(nodepaths[0]);
osg::Vec3f headPos = mat.getTrans();
2022-09-22 21:26:05 +03:00
osg::Vec3f direction;
2015-05-31 18:04:14 +02:00
if (const MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(mHeadTrackTarget))
{
const osg::Node* node = anim->getNode("Head");
2018-10-09 10:21:12 +04:00
if (node == nullptr)
node = anim->getNode("Bip01 Head");
2018-10-09 10:21:12 +04:00
if (node != nullptr)
2022-09-22 21:26:05 +03:00
{
2016-10-02 17:48:54 +09:00
nodepaths = node->getParentalNodePaths();
2016-02-22 19:06:12 +01:00
if (!nodepaths.empty())
direction = osg::computeLocalToWorld(nodepaths[0]).getTrans() - headPos;
2022-09-22 21:26:05 +03:00
}
else
// no head node to look at, fall back to look at center of collision box
2021-04-07 12:07:03 +04:00
direction = MWBase::Environment::get().getWorld()->aimToTarget(mPtr, mHeadTrackTarget, false);
2015-05-31 18:04:14 +02:00
}
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());
2021-03-13 21:51:48 +01:00
zAngleRadians = Misc::normalizeAngle(zAngleRadians - mAnimation->getHeadYaw()) + mAnimation->getHeadYaw();
zAngleRadians *= (1 - direction.z() * direction.z());
xAngleRadians = std::asin(direction.z());
}
2015-05-31 18:04:14 +02:00
const double xLimit = osg::DegreesToRadians(40.0);
const double zLimit = osg::DegreesToRadians(30.0);
double zLimitOffset = mAnimation->getUpperBodyYawRadians();
2021-11-06 07:30:28 +03:00
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;
2015-05-31 18:04:14 +02:00
mAnimation->setHeadPitch(xAngleRadians);
mAnimation->setHeadYaw(zAngleRadians);
}
}