1
0
mirror of https://gitlab.com/OpenMW/openmw.git synced 2025-01-30 12:32:36 +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.

3215 lines
132 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 <array>
2023-12-05 14:13:35 +00:00
#include <unordered_set>
#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>
2023-03-18 09:30:48 +00:00
#include <components/misc/strings/conversion.hpp>
2023-06-27 23:41:06 +02:00
#include <components/settings/values.hpp>
2015-01-31 23:27:34 +01:00
#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"
2024-01-26 21:39:33 +00:00
#include "../mwbase/luamanager.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:
2024-01-26 21:39:33 +00:00
priority[MWRender::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));
2023-05-23 19:06:08 +02:00
const float jumpSpellBonus = ptr.getClass()
.getCreatureStats(ptr)
.getMagicEffects()
.getOrDefault(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;
mMovementAnimationHasMovement = false;
2022-06-13 14:50:59 +03:00
}
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
{
if (isKnockedOut() && mCurrentHit.empty() && knockout)
return;
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;
2024-01-26 21:39:33 +00:00
priority[MWRender::BoneGroup_LeftArm] = Priority_Block;
priority[MWRender::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
2022-06-13 12:59:18 +03:00
// 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
}
}
if (!mAnimation->hasAnimation(mCurrentHit))
{
mCurrentHit.clear();
return;
}
2022-06-13 12:59:18 +03:00
playBlendedAnimation(mCurrentHit, priority, MWRender::BlendMask_All, true, 1, startKey, stopKey, 0.0f,
std::numeric_limits<uint32_t>::max());
}
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;
2024-01-26 21:39:33 +00:00
MWRender::Animation::BlendMask jumpmask = MWRender::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)
2024-01-26 21:39:33 +00:00
playBlendedAnimation(jumpAnimName, Priority_Jump, jumpmask, false, 1.0f,
startAtLoop ? "loop start" : "start", "stop", 0.f, std::numeric_limits<uint32_t>::max());
else if (mJumpState == JumpState_Landing)
2024-01-26 21:39:33 +00:00
playBlendedAnimation(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;
2024-01-26 21:39:33 +00:00
mAnimation->play(
"containeropen", Priority_Scripted, MWRender::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
2024-01-26 21:39:33 +00:00
mAnimation->play("containerclose", Priority_Scripted, MWRender::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)
2024-01-26 21:39:33 +00:00
*blendMask = MWRender::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
2023-05-09 20:07:08 -04:00
// Special case for crossbows - we should apply 1h animations a fallback only for lower body
2019-08-09 12:10:28 +04:00
if (mWeaponType == ESM::Weapon::MarksmanCrossbow && blendMask != nullptr)
2024-01-26 21:39:33 +00:00
*blendMask = MWRender::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)
2024-01-26 21:39:33 +00:00
*blendMask = MWRender::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
}
}
2024-01-26 21:39:33 +00:00
MWRender::Animation::BlendMask movemask = MWRender::BlendMask_All;
2022-06-13 11:34:17 +03:00
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
2024-01-24 20:39:04 +04:00
movementAnimName = std::move(weapMovementAnimName);
2022-06-13 11:34:17 +03:00
}
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);
mMovementAnimationHasMovement = true;
2018-08-20 22:04:02 +04:00
2022-06-13 15:41:56 +03:00
clearStateAnimation(mCurrentMovement);
2024-02-02 09:28:19 +04:00
mCurrentMovement = std::move(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));
mMovementAnimationHasMovement = false;
2022-09-22 21:26:05 +03:00
}
}
2022-06-13 15:41:56 +03:00
playBlendedAnimation(mCurrentMovement, Priority_Movement, movemask, false, 1.f, "start", "stop", startpoint,
std::numeric_limits<uint32_t>::max(), 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 || !mCurrentHit.empty())
&& !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<uint32_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);
2024-01-24 20:39:04 +04:00
idleGroup = std::move(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);
2024-01-24 20:39:04 +04:00
mCurrentIdle = std::move(idleGroup);
2024-01-26 21:39:33 +00:00
playBlendedAnimation(
mCurrentIdle, priority, MWRender::BlendMask_All, false, 1.0f, "start", "stop", startPoint, numLoops, true);
}
void CharacterController::refreshCurrentAnims(
CharacterState idle, CharacterState movement, JumpingState jump, bool force)
{
2023-10-25 21:05:07 +02:00
// If the current animation is scripted, do not touch it
if (isScriptedAnimPlaying())
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();
2022-09-22 21:26:05 +03:00
2024-01-26 21:39:33 +00:00
playBlendedAnimation(
mCurrentDeath, Priority_Death, MWRender::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 (isScriptedAnimPlaying())
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
2024-01-26 21:39:33 +00:00
MWBase::Environment::get().getLuaManager()->animationTextKey(mPtr, key->second);
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
2023-03-18 09:30:48 +00:00
float volume = 1.0f;
float pitch = 1.0f;
if (soundgen.find(' ') != std::string::npos)
{
std::vector<std::string_view> tokens;
Misc::StringUtils::split(soundgen, tokens);
soundgen = tokens[0];
2023-03-18 09:30:48 +00:00
2015-05-22 00:55:43 +02:00
if (tokens.size() >= 2)
2022-09-22 21:26:05 +03:00
{
2023-03-18 09:30:48 +00:00
volume = Misc::StringUtils::toNumeric<float>(tokens[1], volume);
2022-09-22 21:26:05 +03:00
}
2023-03-18 09:30:48 +00:00
2015-05-22 00:55:43 +02:00
if (tokens.size() >= 3)
2022-09-22 21:26:05 +03:00
{
2023-03-18 09:30:48 +00:00
pitch = Misc::StringUtils::toNumeric<float>(tokens[2], pitch);
2022-09-22 21:26:05 +03:00
}
}
2022-09-22 21:26:05 +03:00
const ESM::RefId 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 (mUpperBodyState == UpperBodyState::Equipping)
{
if (groupname == "shield")
mAnimation->showCarriedLeft(true);
else
mAnimation->showWeapons(true);
}
}
else if (action == "unequip detach")
{
if (mUpperBodyState == UpperBodyState::Unequipping)
{
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)
2022-09-22 21:26:05 +03:00
{
// State update doesn't expect the start key to be the hit key,
// so we have to do this early.
prepareHit();
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);
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, mCastingScriptedSpell);
mCastingScriptedSpell = 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
{
if (!mAnimation->hasAnimation("idlestorm"))
return;
bool animPlaying = mAnimation->isPlaying("idlestorm");
if (mUpperBodyState != UpperBodyState::None || inwater)
{
if (animPlaying)
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 (!animPlaying)
2022-09-22 21:26:05 +03:00
{
2024-01-26 21:39:33 +00:00
int mask = MWRender::BlendMask_Torso | MWRender::BlendMask_RightArm;
playBlendedAnimation("idlestorm", Priority_Storm, mask, true, 1.0f, "start", "stop", 0.0f,
std::numeric_limits<uint32_t>::max(), true);
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 (animPlaying)
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
}
void CharacterController::prepareHit()
{
if (mAttackStrength != -1.f)
return;
auto& prng = MWBase::Environment::get().getWorld()->getPrng();
mAttackStrength = calculateWindUp();
if (mAttackStrength == -1.f)
mAttackStrength = std::min(1.f, 0.1f + Misc::Rng::rollClosedProbability(prng));
ESM::WeaponType::Class weapclass = getWeaponType(mWeaponType)->mWeaponClass;
if (weapclass != ESM::WeaponType::Ranged && weapclass != ESM::WeaponType::Thrown)
{
mAttackSuccess = mPtr.getClass().evaluateHit(mPtr, mAttackVictim, mAttackHitPos);
if (!mAttackSuccess)
mAttackStrength = 0.f;
playSwishSound();
}
}
bool CharacterController::updateWeaponState()
2022-09-22 21:26:05 +03:00
{
2023-12-05 14:13:35 +00:00
// If the current animation is scripted, we can't do anything here.
if (isScriptedAnimPlaying())
return false;
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");
2024-01-26 21:39:33 +00:00
playBlendedAnimation("torch", Priority_Torch, MWRender::BlendMask_LeftArm, false, 1.0f, "start", "stop",
0.0f, std::numeric_limits<uint32_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))
2024-01-26 21:39:33 +00:00
priorityWeapon[MWRender::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::AttackWindUp && 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);
2024-01-26 21:39:33 +00:00
int unequipMask = MWRender::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
{
2024-01-26 21:39:33 +00:00
unequipMask = unequipMask | ~MWRender::BlendMask_LeftArm;
playBlendedAnimation("shield", Priority_Block, MWRender::BlendMask_LeftArm, true, 1.0f,
"unequip start", "unequip stop", 0.0f, 0);
}
else if (mWeaponType == ESM::Weapon::HandToHand)
mAnimation->showCarriedLeft(false);
2024-01-26 21:39:33 +00:00
playBlendedAnimation(
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 && mUpperBodyState <= UpperBodyState::WeaponEquipped)
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);
2024-01-26 21:39:33 +00:00
int equipMask = MWRender::BlendMask_All;
if (useShieldAnims && weaptype != ESM::Weapon::Spell)
2022-09-22 21:26:05 +03:00
{
2024-01-26 21:39:33 +00:00
equipMask = equipMask | ~MWRender::BlendMask_LeftArm;
playBlendedAnimation("shield", Priority_Block, MWRender::BlendMask_LeftArm, true, 1.0f,
"equip start", "equip stop", 0.0f, 0);
}
2024-01-26 21:39:33 +00:00
playBlendedAnimation(
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);
}
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);
}
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. Scripted spellcasting bypasses restrictions.
MWWorld::SpellCastState spellCastResult = MWWorld::SpellCastState::Success;
if (!mCastingScriptedSpell)
spellCastResult = world->startSpellCast(mPtr);
2022-09-04 15:42:40 +02:00
mCanCast = spellCastResult == MWWorld::SpellCastState::Success;
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();
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
2023-06-27 23:41:06 +02:00
if (isMagicItem && !Settings::game().mUseMagicItemAnimations)
{
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.
else if (!spellid.empty() && spellCastResult != MWWorld::SpellCastState::PowerAlreadyUsed)
{
world->breakInvisibility(mPtr);
MWMechanics::CastSpell cast(mPtr, {}, false, mCastingScriptedSpell);
2022-09-22 21:26:05 +03:00
const std::vector<ESM::IndexedENAMstruct>* effects{ nullptr };
const MWWorld::ESMStore& store = world->getStore();
if (isMagicItem)
2022-09-22 21:26:05 +03: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
{
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 (!effects->empty())
2022-09-22 21:26:05 +03:00
{
if (mCanCast)
{
const ESM::MagicEffect* effect = store.get<ESM::MagicEffect>().find(
effects->back().mData.mEffectID); // use last effect of list for color of VFX_Hands
const ESM::Static* castStatic
= world->getStore().get<ESM::Static>().find(ESM::RefId::stringRefId("VFX_Hands"));
2022-09-22 21:26:05 +03:00
if (mAnimation->getNode("Bip01 L Hand"))
mAnimation->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel),
2024-01-26 21:39:33 +00:00
"", false, "Bip01 L Hand", effect->mParticle);
if (mAnimation->getNode("Bip01 R Hand"))
mAnimation->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel),
2024-01-26 21:39:33 +00:00
"", false, "Bip01 R Hand", effect->mParticle);
}
// first effect used for casting animation
const ESM::ENAMstruct& firstEffect = effects->front().mData;
std::string startKey;
std::string stopKey;
if (isRandomAttackAnimation(mCurrentWeapon))
2022-09-22 21:26:05 +03:00
{
startKey = "start";
stopKey = "stop";
if (mCanCast)
world->castSpell(mPtr,
mCastingScriptedSpell); // No "release" text key to use, so cast immediately
mCastingScriptedSpell = false;
mCanCast = false;
}
else
{
switch (firstEffect.mRange)
{
case 0:
mAttackType = "self";
break;
case 1:
mAttackType = "touch";
break;
case 2:
mAttackType = "target";
break;
}
startKey = mAttackType + " start";
stopKey = mAttackType + " stop";
}
2024-01-26 21:39:33 +00:00
playBlendedAnimation(mCurrentWeapon, priorityWeapon, MWRender::BlendMask_All, false, 1,
startKey, stopKey, 0.0f, 0);
mUpperBodyState = UpperBodyState::Casting;
2022-09-22 21:26:05 +03:00
}
}
else
{
resetIdle = false;
}
}
else
{
std::string startKey = "start";
std::string stopKey = "stop";
2024-05-15 12:41:45 -05:00
MWBase::LuaManager::ActorControls* actorControls
= MWBase::Environment::get().getLuaManager()->getActorControls(mPtr);
2024-05-15 11:56:58 -05:00
const bool aiInactive
= actorControls->mDisableAI || !MWBase::Environment::get().getMechanicsManager()->isAIActive();
if (mWeaponType != ESM::Weapon::PickProbe && !isRandomAttackAnimation(mCurrentWeapon))
{
if (weapclass == ESM::WeaponType::Ranged || weapclass == ESM::WeaponType::Thrown)
mAttackType = "shoot";
2024-05-15 14:09:33 -05:00
else if (mPtr == getPlayer())
{
2023-06-27 23:41:06 +02:00
if (Settings::game().mBestAttack)
{
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();
}
}
2024-05-15 14:09:33 -05:00
else if (aiInactive)
{
mAttackType = getDesiredAttackType();
if (mAttackType == "")
mAttackType = getRandomAttackType();
}
// else if (mPtr != getPlayer()) use mAttackType set by AiCombat
2022-08-09 14:10:46 +03:00
startKey = mAttackType + ' ' + startKey;
stopKey = mAttackType + " max attack";
}
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();
2024-01-26 21:39:33 +00:00
playBlendedAnimation(mCurrentWeapon, priorityWeapon, MWRender::BlendMask_All, false, weapSpeed,
startKey, stopKey, 0.0f, 0);
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
}
}
// Evaluate the attack results and play the swish sound.
// Attack animations with no hit key do this earlier.
2022-09-22 21:26:05 +03:00
else
{
prepareHit();
}
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);
2024-01-26 21:39:33 +00:00
playBlendedAnimation(mCurrentWeapon, priorityWeapon, MWRender::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;
2024-01-26 21:39:33 +00:00
playBlendedAnimation(mCurrentWeapon, priorityFollow, MWRender::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.empty())
return;
if (!mAnimation->isPlaying(mAnimQueue.front().mGroup))
2023-10-25 18:59:05 +02:00
{
2023-12-05 14:13:35 +00:00
// Playing animations through mwscript is weird. If an animation is
// a looping animation (idle or other cyclical animations), then they
// will end as expected. However, if they are non-looping animations, they
// will stick around forever or until another animation appears in the queue.
bool shouldPlayOrRestart = mAnimQueue.size() > 1;
if (shouldPlayOrRestart || !mAnimQueue.front().mScripted
|| (mAnimQueue.front().mLoopCount == 0 && mAnimQueue.front().mLooping))
2022-09-22 21:26:05 +03:00
{
2023-12-05 14:13:35 +00:00
mAnimation->setPlayScriptedOnly(false);
mAnimation->disable(mAnimQueue.front().mGroup);
mAnimQueue.pop_front();
2023-12-05 14:13:35 +00:00
shouldPlayOrRestart = true;
2022-09-22 21:26:05 +03:00
}
2023-12-05 14:13:35 +00:00
else
// A non-looping animation will stick around forever, so only restart if the animation
// actually was removed for some reason.
shouldPlayOrRestart = !mAnimation->getInfo(mAnimQueue.front().mGroup)
&& mAnimation->hasAnimation(mAnimQueue.front().mGroup);
2023-10-29 15:33:07 +01:00
2023-12-05 14:13:35 +00:00
if (shouldPlayOrRestart)
{
2023-10-29 15:33:07 +01:00
// Move on to the remaining items of the queue
2023-12-05 14:13:35 +00:00
playAnimQueue();
}
2023-10-25 18:59:05 +02:00
}
else
{
2023-12-05 14:13:35 +00:00
float complete;
size_t loopcount;
mAnimation->getInfo(mAnimQueue.front().mGroup, &complete, nullptr, &loopcount);
mAnimQueue.front().mLoopCount = loopcount;
mAnimQueue.front().mTime = complete;
}
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
2023-12-05 14:13:35 +00:00
void CharacterController::playAnimQueue(bool loopStart)
{
if (!mAnimQueue.empty())
{
clearStateAnimation(mCurrentIdle);
mIdleState = CharState_SpecialIdle;
auto priority = mAnimQueue.front().mScripted ? Priority_Scripted : Priority_Default;
mAnimation->setPlayScriptedOnly(mAnimQueue.front().mScripted);
2024-01-26 21:39:33 +00:00
if (mAnimQueue.front().mScripted)
mAnimation->play(mAnimQueue.front().mGroup, priority, MWRender::BlendMask_All, false,
mAnimQueue.front().mSpeed, (loopStart ? "loop start" : mAnimQueue.front().mStartKey),
mAnimQueue.front().mStopKey, mAnimQueue.front().mTime, mAnimQueue.front().mLoopCount,
mAnimQueue.front().mLooping);
else
playBlendedAnimation(mAnimQueue.front().mGroup, priority, MWRender::BlendMask_All, false,
mAnimQueue.front().mSpeed, (loopStart ? "loop start" : mAnimQueue.front().mStartKey),
mAnimQueue.front().mStopKey, mAnimQueue.front().mTime, mAnimQueue.front().mLoopCount,
mAnimQueue.front().mLooping);
2023-12-05 14:13:35 +00:00
}
}
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();
2023-06-27 23:41:06 +02:00
if (!Settings::game().mNormaliseRaceSpeed && cls.isNpc())
2016-08-22 23:02:57 +02:00
{
const ESM::NPC* npc = mPtr.get<ESM::NPC>()->mBase;
const ESM::Race* race = world->getStore().get<ESM::Race>().find(npc->mRace);
2023-12-17 13:00:14 +01:00
float weight = npc->isMale() ? race->mData.mMaleWeight : race->mData.mFemaleWeight;
2016-08-22 23:02:57 +02:00
scale *= weight;
}
if (cls.isActor() && cls.getCreatureStats(mPtr).wasTeleported())
{
mSmoothedSpeed = osg::Vec2f();
cls.getCreatureStats(mPtr).setTeleported(false);
}
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();
2023-06-27 23:41:06 +02:00
const bool smoothMovement = Settings::game().mSmoothMovement;
if (smoothMovement)
2022-09-22 21:26:05 +03:00
{
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).
2023-06-27 23:41:06 +02:00
maxDelta = duration * (isPlayer ? 1.0 / Settings::game().mSmoothMovementPlayerTurningDelay : 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;
2023-06-27 23:41:06 +02:00
const bool turnToMovementDirection = Settings::game().mTurnToMovementDirection;
const bool isBiped = mPtr.getClass().isBipedal(mPtr);
if (!isBiped || !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() || isScriptedAnimPlaying())
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)
{
2024-01-14 20:33:23 +01:00
cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, ESM::Skill::Athletics_SwimOneSecond);
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)
{
2024-01-14 20:33:23 +01:00
cls.skillUsageSucceeded(mPtr, ESM::Skill::Athletics, ESM::Skill::Athletics_RunOneSecond);
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;
const float jumpHeight = cls.getJump(mPtr);
if (jumpHeight <= 0.f || sneak || inwater || flying || !solid)
{
vec.z() = 0.f;
// Following code might assign some vertical movement regardless, need to reset this manually
// This is used for jumping detection
movementSettings.mPosition[2] = 0;
}
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)
{
mInJump = true;
if (vec.x() == 0 && vec.y() == 0)
vec.z() = jumpHeight;
else
2022-09-22 21:26:05 +03:00
{
osg::Vec3f lat(vec.x(), vec.y(), 0.0f);
lat.normalize();
vec = osg::Vec3f(lat.x(), lat.y(), 1.0f) * jumpHeight * 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)
2024-01-14 20:33:23 +01:00
cls.skillUsageSucceeded(mPtr, ESM::Skill::Acrobatics, ESM::Skill::Acrobatics_Fall);
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.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 && isBiped)
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
}
}
}
if (turnToMovementDirection && !isFirstPersonPlayer && isBiped
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
2023-06-27 23:41:06 +02:00
if (inwater && isPlayer && !isFirstPersonPlayer && Settings::game().mSwimUpwardCorrection)
2013-08-18 23:42:56 -07:00
{
2023-06-27 23:41:06 +02:00
const float swimUpwardCoef = Settings::game().mSwimUpwardCoef;
2020-09-04 15:03:33 +02:00
vec.z() = std::abs(vec.y()) * swimUpwardCoef;
2023-06-27 23:41:06 +02:00
vec.y() *= std::sqrt(1.0f - swimUpwardCoef * swimUpwardCoef);
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
{
if (isBiped)
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;
}
updateAnimQueue();
2023-11-28 21:29:05 +01:00
if (!mAnimQueue.empty())
idlestate = CharState_SpecialIdle;
2023-11-28 21:29:05 +01:00
else if (sneak && !mInJump)
idlestate = CharState_IdleSneak;
else
idlestate = CharState_Idle;
if (inwater)
idlestate = CharState_IdleSwim;
2020-12-24 03:00:09 +01:00
2020-08-07 20:17:44 +00:00
if (!mSkipAnim)
{
refreshCurrentAnims(idlestate, movestate, jumpstate, updateWeaponState());
updateIdleStormState(inwater);
}
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
if (isMovementAnimationControlled())
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
updateHeadTracking(duration);
}
movement = vec;
movementSettings.mPosition[0] = movementSettings.mPosition[1] = 0;
// Can't reset jump state (mPosition[2]) here in full; we don't know for sure whether the PhysicsSystem will
// actually handle it in this frame due to the fixed minimum timestep used for the physics update. It will
// be reset in PhysicsSystem::move once the jump is handled.
if (movement.z() == 0.f)
movementSettings.mPosition[2] = 0;
}
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
}
}
2023-11-04 14:41:08 +01:00
osg::Vec3f movementFromAnimation
= mAnimation->runAnimation(mSkipAnim && !isScriptedAnimPlaying() ? 0.f : duration);
if (mPtr.getClass().isActor() && !isScriptedAnimPlaying())
{
if (isMovementAnimationControlled())
{
if (duration != 0.f && movementFromAnimation != osg::Vec3f())
{
movementFromAnimation /= duration;
// Ensure we're moving in the right general direction.
// In vanilla, all horizontal movement is taken from animations, even when moving diagonally (which
// doesn't have a corresponding animation). So to achieve diagonal movement, we have to rotate the
// movement taken from the animation to the intended direction.
//
// Note that while a complete movement animation cycle will have a well defined direction, no
// individual frame will, and therefore we have to determine the direction based on the currently
// playing cycle instead.
if (speed > 0.f)
{
float animMovementAngle = getAnimationMovementDirection();
float targetMovementAngle = std::atan2(-movement.x(), movement.y());
float diff = targetMovementAngle - animMovementAngle;
movementFromAnimation = osg::Quat(diff, osg::Vec3f(0, 0, 1)) * movementFromAnimation;
}
movement = movementFromAnimation;
}
else
{
movement = osg::Vec3f();
}
}
else if (mSkipAnim)
{
movement = osg::Vec3f();
}
if (mFloatToSurface)
2014-01-04 17:55:09 +02:00
{
if (cls.getCreatureStats(mPtr).isDead()
|| (!godmode
&& cls.getCreatureStats(mPtr)
.getMagicEffects()
.getOrDefault(ESM::MagicEffect::Paralyze)
.getModifier()
> 0))
{
2023-11-04 14:00:13 +01:00
movement.z() = 1.0;
}
}
movement.x() *= scale;
movement.y() *= scale;
world->queueMovement(mPtr, movement);
}
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)
{
2024-01-26 21:39:33 +00:00
// TODO: Probably want to presist lua animations too
if (!iter->mScripted)
continue;
ESM::AnimationState::ScriptedAnimation anim;
anim.mGroup = iter->mGroup;
if (iter == mAnimQueue.begin())
2022-09-22 21:26:05 +03:00
{
float complete;
2023-12-05 14:13:35 +00:00
size_t loopcount;
mAnimation->getInfo(anim.mGroup, &complete, nullptr, &loopcount);
anim.mTime = complete;
2023-12-05 14:13:35 +00:00
anim.mLoopCount = loopcount;
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;
2024-01-28 16:24:15 +01:00
entry.mLoopCount
= static_cast<uint32_t>(std::min<uint64_t>(iter->mLoopCount, std::numeric_limits<uint32_t>::max()));
2024-01-26 21:39:33 +00:00
entry.mLooping = mAnimation->isLoopingAnimation(entry.mGroup);
2024-02-01 20:00:10 +01:00
entry.mScripted = true;
2024-01-26 21:39:33 +00:00
entry.mStartKey = "start";
entry.mStopKey = "stop";
entry.mSpeed = 1.f;
2023-12-05 14:13:35 +00:00
entry.mTime = iter->mTime;
if (iter->mAbsolute)
{
float start = mAnimation->getTextKeyTime(iter->mGroup + ": start");
float stop = mAnimation->getTextKeyTime(iter->mGroup + ": stop");
float time = std::clamp(iter->mTime, start, stop);
entry.mTime = (time - start) / (stop - start);
}
mAnimQueue.push_back(entry);
2022-09-22 21:26:05 +03:00
}
2023-12-05 14:13:35 +00:00
playAnimQueue();
2022-09-22 21:26:05 +03:00
}
}
2024-01-26 21:39:33 +00:00
void CharacterController::playBlendedAnimation(const std::string& groupname, const MWRender::AnimPriority& priority,
int blendMask, bool autodisable, float speedmult, std::string_view start, std::string_view stop,
2024-01-28 16:34:44 +01:00
float startpoint, uint32_t loops, bool loopfallback) const
2024-01-26 21:39:33 +00:00
{
if (mLuaAnimations)
MWBase::Environment::get().getLuaManager()->playAnimation(mPtr, groupname, priority, blendMask, autodisable,
speedmult, start, stop, startpoint, loops, loopfallback);
else
mAnimation->play(
groupname, priority, blendMask, autodisable, speedmult, start, stop, startpoint, loops, loopfallback);
}
bool CharacterController::playGroup(std::string_view groupname, int mode, uint32_t count, bool scripted)
{
if (!mAnimation || !mAnimation->hasAnimation(groupname))
return false;
2023-10-25 21:05:07 +02:00
// We should not interrupt scripted animations with non-scripted ones
if (isScriptedAnimPlaying() && !scripted)
return true;
2024-01-26 21:39:33 +00:00
bool looping = mAnimation->isLoopingAnimation(groupname);
2023-12-05 14:13:35 +00:00
// If this animation is a looped animation 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.
2023-12-05 14:13:35 +00:00
if (!mAnimQueue.empty() && mAnimQueue.front().mGroup == groupname && looping
&& 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;
}
}
2023-12-05 14:13:35 +00:00
// The loop count in vanilla is weird.
// if played with a count of 0, all objects play exactly once from start to stop.
// But if the count is x > 0, actors and non-actors behave differently. actors will loop
// exactly x times, while non-actors will loop x+1 instead.
if (mPtr.getClass().isActor() && count > 0)
2023-12-05 14:13:35 +00:00
count--;
AnimationQueueEntry entry;
2017-09-18 01:21:18 -07:00
entry.mGroup = groupname;
2023-12-05 14:13:35 +00:00
entry.mLoopCount = count;
entry.mTime = 0.f;
2024-01-26 21:39:33 +00:00
// "PlayGroup idle" is a special case, used to remove to stop scripted animations playing
entry.mScripted = (scripted && groupname != "idle");
2023-12-05 14:13:35 +00:00
entry.mLooping = looping;
2024-01-26 21:39:33 +00:00
entry.mSpeed = 1.f;
entry.mStartKey = ((mode == 2) ? "loop start" : "start");
entry.mStopKey = "stop";
2023-12-05 14:13:35 +00:00
bool playImmediately = false;
if (mode != 0 || mAnimQueue.empty() || !isAnimPlaying(mAnimQueue.front().mGroup))
{
clearAnimQueue(scripted);
2023-12-05 14:13:35 +00:00
playImmediately = true;
2022-09-22 21:26:05 +03:00
}
else
{
mAnimQueue.resize(1);
}
mAnimQueue.push_back(entry);
2022-09-22 21:26:05 +03:00
2023-12-05 14:13:35 +00:00
if (playImmediately)
playAnimQueue(mode == 2);
return true;
}
2024-01-26 21:39:33 +00:00
bool CharacterController::playGroupLua(std::string_view groupname, float speed, std::string_view startKey,
std::string_view stopKey, uint32_t loops, bool forceLoop)
2024-01-26 21:39:33 +00:00
{
// Note: In mwscript, "idle" is a special case used to clear the anim queue.
// In lua we offer an explicit clear method instead so this method does not treat "idle" special.
if (!mAnimation || !mAnimation->hasAnimation(groupname))
return false;
AnimationQueueEntry entry;
entry.mGroup = groupname;
// Note: MWScript gives one less loop to actors than non-actors.
// But this is the Lua version. We don't need to reproduce this weirdness here.
2024-01-28 16:24:15 +01:00
entry.mLoopCount = loops;
2024-01-26 21:39:33 +00:00
entry.mStartKey = startKey;
entry.mStopKey = stopKey;
entry.mLooping = mAnimation->isLoopingAnimation(groupname) || forceLoop;
entry.mScripted = true;
entry.mSpeed = speed;
entry.mTime = 0;
if (mAnimQueue.size() > 1)
mAnimQueue.resize(1);
mAnimQueue.push_back(entry);
if (mAnimQueue.size() == 1)
playAnimQueue();
return true;
}
void CharacterController::enableLuaAnimations(bool enable)
{
mLuaAnimations = enable;
}
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
}
bool CharacterController::isScriptedAnimPlaying() const
{
2023-12-05 14:13:35 +00:00
// If the front of the anim queue is scripted, morrowind treats it as if it's
// still playing even if it's actually done.
if (!mAnimQueue.empty())
2023-12-05 14:13:35 +00:00
return mAnimQueue.front().mScripted;
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
bool CharacterController::isMovementAnimationControlled() const
{
if (mHitState != CharState_None)
return true;
if (Settings::game().mPlayerMovementIgnoresAnimation && mPtr == getPlayer())
return false;
if (mInJump)
return false;
2023-10-14 13:56:44 +02:00
bool movementAnimationControlled = mIdleState != CharState_None;
if (mMovementState != CharState_None)
movementAnimationControlled = mMovementAnimationHasMovement;
return movementAnimationControlled;
}
2023-10-25 21:05:07 +02:00
void CharacterController::clearAnimQueue(bool clearScriptedAnims)
{
2017-09-18 01:21:18 -07:00
// Do not interrupt scripted animations, if we want to keep them
2023-10-25 21:05:07 +02:00
if ((!isScriptedAnimPlaying() || clearScriptedAnims) && !mAnimQueue.empty())
2017-09-18 01:21:18 -07:00
mAnimation->disable(mAnimQueue.front().mGroup);
2023-10-25 21:05:07 +02:00
if (clearScriptedAnims)
2022-09-22 21:26:05 +03:00
{
2023-12-05 14:13:35 +00:00
mAnimation->setPlayScriptedOnly(false);
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->mScripted)
2017-09-18 01:21:18 -07:00
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;
mCastingScriptedSpell = 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
}
2023-12-05 14:13:35 +00:00
updateAnimQueue();
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
2024-01-26 21:39:33 +00:00
std::vector<std::string_view> effects = mAnimation->getLoopingEffects();
for (std::string_view effectId : effects)
{
auto index = ESM::MagicEffect::indexNameToIndex(effectId);
if (index >= 0
&& (mPtr.getClass().getCreatureStats(mPtr).isDeathAnimationFinished()
|| mPtr.getClass()
.getCreatureStats(mPtr)
.getMagicEffects()
.getOrDefault(MWMechanics::EffectKey(index))
.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;
2023-05-23 19:06:08 +02:00
float light = mPtr.getClass()
.getCreatureStats(mPtr)
.getMagicEffects()
.getOrDefault(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
2023-05-23 19:06:08 +02:00
bool vampire = mPtr.getClass()
.getCreatureStats(mPtr)
.getMagicEffects()
.getOrDefault(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()
2023-05-23 19:06:08 +02:00
.getOrDefault(ESM::MagicEffect::Invisibility)
2018-06-11 17:18:51 +04:00
.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()
2023-05-23 19:06:08 +02:00
.getOrDefault(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 mCastingScriptedSpell || 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);
}
void CharacterController::castSpell(const ESM::RefId& spellId, bool scriptedSpell)
{
setAttackingOrSpell(true);
mCastingScriptedSpell = scriptedSpell;
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();
}
std::string_view CharacterController::getDesiredAttackType() const
{
return mPtr.getClass().getCreatureStats(mPtr).getAttackType();
}
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
2023-11-04 16:18:36 +01:00
float CharacterController::getAnimationMovementDirection() const
{
switch (mMovementState)
{
case CharState_RunLeft:
case CharState_SneakLeft:
case CharState_SwimWalkLeft:
case CharState_SwimRunLeft:
case CharState_WalkLeft:
return osg::PI_2f;
case CharState_RunRight:
case CharState_SneakRight:
case CharState_SwimWalkRight:
case CharState_SwimRunRight:
case CharState_WalkRight:
return -osg::PI_2f;
case CharState_RunForward:
case CharState_SneakForward:
case CharState_SwimRunForward:
case CharState_SwimWalkForward:
case CharState_WalkForward:
return mAnimation->getLegsYawRadians();
case CharState_RunBack:
case CharState_SneakBack:
case CharState_SwimWalkBack:
case CharState_SwimRunBack:
case CharState_WalkBack:
return mAnimation->getLegsYawRadians() - osg::PIf;
2023-11-04 21:01:06 +01:00
default:
return 0.0f;
}
}
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);
}
MWWorld::MovementDirectionFlags CharacterController::getSupportedMovementDirections() const
{
using namespace std::string_view_literals;
// There are fallbacks in the CharacterController::refreshMovementAnims for certain animations. Arrays below
// represent them.
constexpr std::array all = { ""sv };
constexpr std::array walk = { "walk"sv };
constexpr std::array swimWalk = { "swimwalk"sv, "walk"sv };
constexpr std::array sneak = { "sneak"sv };
constexpr std::array run = { "run"sv, "walk"sv };
constexpr std::array swimRun = { "swimrun"sv, "run"sv, "walk"sv };
constexpr std::array swim = { "swim"sv };
switch (mMovementState)
{
case CharState_None:
case CharState_SpecialIdle:
case CharState_Idle:
case CharState_IdleSwim:
case CharState_IdleSneak:
return mAnimation->getSupportedMovementDirections(all);
case CharState_WalkForward:
case CharState_WalkBack:
case CharState_WalkLeft:
case CharState_WalkRight:
return mAnimation->getSupportedMovementDirections(walk);
case CharState_SwimWalkForward:
case CharState_SwimWalkBack:
case CharState_SwimWalkLeft:
case CharState_SwimWalkRight:
return mAnimation->getSupportedMovementDirections(swimWalk);
case CharState_RunForward:
case CharState_RunBack:
case CharState_RunLeft:
case CharState_RunRight:
return mAnimation->getSupportedMovementDirections(run);
case CharState_SwimRunForward:
case CharState_SwimRunBack:
case CharState_SwimRunLeft:
case CharState_SwimRunRight:
return mAnimation->getSupportedMovementDirections(swimRun);
case CharState_SneakForward:
case CharState_SneakBack:
case CharState_SneakLeft:
case CharState_SneakRight:
return mAnimation->getSupportedMovementDirections(sneak);
case CharState_TurnLeft:
case CharState_TurnRight:
return mAnimation->getSupportedMovementDirections(all);
case CharState_SwimTurnLeft:
case CharState_SwimTurnRight:
return mAnimation->getSupportedMovementDirections(swim);
case CharState_Death1:
case CharState_Death2:
case CharState_Death3:
case CharState_Death4:
case CharState_Death5:
case CharState_SwimDeath:
case CharState_SwimDeathKnockDown:
case CharState_SwimDeathKnockOut:
case CharState_DeathKnockDown:
case CharState_DeathKnockOut:
case CharState_Hit:
case CharState_SwimHit:
case CharState_KnockDown:
case CharState_KnockOut:
case CharState_SwimKnockDown:
case CharState_SwimKnockOut:
case CharState_Block:
return mAnimation->getSupportedMovementDirections(all);
}
return 0;
}
}