mirror of
https://gitlab.com/OpenMW/openmw.git
synced 2025-01-27 03:35:27 +00:00
Merge branch 'yourclonesareveryimpressive' into 'master'
Make the character controller less miserable, round 3: code duplication and knockout interruption See merge request OpenMW/openmw!2008
This commit is contained in:
commit
471c111fa2
@ -161,6 +161,68 @@ std::string deathStateToAnimGroup(MWMechanics::CharacterState state)
|
||||
}
|
||||
}
|
||||
|
||||
// Converts a hit state to its equivalent animation group as long as it is a hit state.
|
||||
std::string hitStateToAnimGroup(MWMechanics::CharacterState state)
|
||||
{
|
||||
using namespace MWMechanics;
|
||||
switch (state)
|
||||
{
|
||||
case CharState_SwimHit: return "swimhit";
|
||||
case CharState_SwimKnockDown: return "swimknockdown";
|
||||
case CharState_SwimKnockOut: return "swimknockout";
|
||||
|
||||
case CharState_Hit: return "hit";
|
||||
case CharState_KnockDown: return "knockdown";
|
||||
case CharState_KnockOut: return "knockout";
|
||||
|
||||
case CharState_Block: return "shield";
|
||||
|
||||
default: return {};
|
||||
}
|
||||
}
|
||||
|
||||
// Converts an idle state to its equivalent animation group.
|
||||
std::string idleStateToAnimGroup(MWMechanics::CharacterState state)
|
||||
{
|
||||
using namespace MWMechanics;
|
||||
switch (state)
|
||||
{
|
||||
case CharState_IdleSwim:
|
||||
return "idleswim";
|
||||
case CharState_IdleSneak:
|
||||
return "idlesneak";
|
||||
case CharState_Idle:
|
||||
case CharState_Idle2:
|
||||
case CharState_Idle3:
|
||||
case CharState_Idle4:
|
||||
case CharState_Idle5:
|
||||
case CharState_Idle6:
|
||||
case CharState_Idle7:
|
||||
case CharState_Idle8:
|
||||
case CharState_Idle9:
|
||||
case CharState_SpecialIdle:
|
||||
return "idle";
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
MWRender::Animation::AnimPriority getIdlePriority(MWMechanics::CharacterState state)
|
||||
{
|
||||
using namespace MWMechanics;
|
||||
MWRender::Animation::AnimPriority priority(Priority_Default);
|
||||
switch (state)
|
||||
{
|
||||
case CharState_IdleSwim:
|
||||
return Priority_SwimIdle;
|
||||
case CharState_IdleSneak:
|
||||
priority[MWRender::Animation::BoneGroup_LowerBody] = Priority_SneakIdleLowerBody;
|
||||
[[fallthrough]];
|
||||
default:
|
||||
return priority;
|
||||
}
|
||||
}
|
||||
|
||||
float getFallDamage(const MWWorld::Ptr& ptr, float fallHeight)
|
||||
{
|
||||
MWBase::World *world = MWBase::Environment::get().getWorld();
|
||||
@ -209,143 +271,166 @@ std::string CharacterController::chooseRandomGroup (const std::string& prefix, i
|
||||
return prefix + std::to_string(roll);
|
||||
}
|
||||
|
||||
|
||||
void CharacterController::clearStateAnimation(std::string &anim) const
|
||||
{
|
||||
if (anim.empty())
|
||||
return;
|
||||
if (mAnimation)
|
||||
mAnimation->disable(anim);
|
||||
anim.clear();
|
||||
}
|
||||
|
||||
void CharacterController::resetCurrentJumpState()
|
||||
{
|
||||
clearStateAnimation(mCurrentJump);
|
||||
mJumpState = JumpState_None;
|
||||
}
|
||||
|
||||
void CharacterController::resetCurrentMovementState()
|
||||
{
|
||||
clearStateAnimation(mCurrentMovement);
|
||||
mMovementState = CharState_None;
|
||||
}
|
||||
|
||||
void CharacterController::resetCurrentIdleState()
|
||||
{
|
||||
clearStateAnimation(mCurrentIdle);
|
||||
mIdleState = CharState_None;
|
||||
}
|
||||
|
||||
void CharacterController::resetCurrentHitState()
|
||||
{
|
||||
clearStateAnimation(mCurrentHit);
|
||||
mHitState = CharState_None;
|
||||
}
|
||||
|
||||
void CharacterController::resetCurrentWeaponState()
|
||||
{
|
||||
clearStateAnimation(mCurrentWeapon);
|
||||
mUpperBodyState = UpperCharState_Nothing;
|
||||
}
|
||||
|
||||
void CharacterController::resetCurrentDeathState()
|
||||
{
|
||||
clearStateAnimation(mCurrentDeath);
|
||||
mDeathState = CharState_None;
|
||||
}
|
||||
|
||||
void CharacterController::refreshHitRecoilAnims(CharacterState& idle)
|
||||
{
|
||||
const auto world = MWBase::Environment::get().getWorld();
|
||||
auto& charClass = mPtr.getClass();
|
||||
if (!charClass.isActor())
|
||||
return;
|
||||
const auto world = MWBase::Environment::get().getWorld();
|
||||
auto& stats = charClass.getCreatureStats(mPtr);
|
||||
bool knockout = stats.getFatigue().getCurrent() < 0 || stats.getFatigue().getBase() == 0;
|
||||
bool recovery = stats.getHitRecovery();
|
||||
bool knockdown = stats.getKnockedDown();
|
||||
bool block = stats.getBlock();
|
||||
bool isSwimming = world->isSwimming(mPtr);
|
||||
auto& prng = world->getPrng();
|
||||
if(mHitState == CharState_None)
|
||||
|
||||
if (mHitState != CharState_None)
|
||||
{
|
||||
if (stats.getFatigue().getCurrent() < 0 || stats.getFatigue().getBase() == 0)
|
||||
if (!mAnimation->isPlaying(mCurrentHit))
|
||||
{
|
||||
mTimeUntilWake = Misc::Rng::rollClosedProbability(prng) * 2 + 1; // Wake up after 1 to 3 seconds
|
||||
if (isSwimming && mAnimation->hasAnimation("swimknockout"))
|
||||
{
|
||||
mHitState = CharState_SwimKnockOut;
|
||||
mCurrentHit = "swimknockout";
|
||||
mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, false, 1, "start", "stop", 0.0f, ~0ul);
|
||||
mHitState = CharState_None;
|
||||
mCurrentHit.clear();
|
||||
stats.setKnockedDown(false);
|
||||
stats.setHitRecovery(false);
|
||||
stats.setBlock(false);
|
||||
}
|
||||
else if (!isSwimming && mAnimation->hasAnimation("knockout"))
|
||||
{
|
||||
mHitState = CharState_KnockOut;
|
||||
mCurrentHit = "knockout";
|
||||
mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, false, 1, "start", "stop", 0.0f, ~0ul);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Knockout animations are missing. Fall back to idle animation, so target actor still can be killed via HtH.
|
||||
mCurrentHit.erase();
|
||||
else if (isKnockedOut())
|
||||
mAnimation->setLoopingEnabled(mCurrentHit, knockout);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!knockout && !knockdown && !recovery && !block)
|
||||
return;
|
||||
|
||||
MWRender::Animation::AnimPriority priority(Priority_Knockdown);
|
||||
std::string startKey = "start";
|
||||
std::string stopKey = "stop";
|
||||
if (knockout)
|
||||
{
|
||||
mHitState = isSwimming ? CharState_SwimKnockOut : CharState_KnockOut;
|
||||
stats.setKnockedDown(true);
|
||||
}
|
||||
else if (knockdown)
|
||||
{
|
||||
if (isSwimming && mAnimation->hasAnimation("swimknockdown"))
|
||||
{
|
||||
mHitState = CharState_SwimKnockDown;
|
||||
mCurrentHit = "swimknockdown";
|
||||
mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0);
|
||||
}
|
||||
else if (!isSwimming && mAnimation->hasAnimation("knockdown"))
|
||||
{
|
||||
mHitState = CharState_KnockDown;
|
||||
mCurrentHit = "knockdown";
|
||||
mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Knockdown animation is missing. Cancel knockdown state.
|
||||
stats.setKnockedDown(false);
|
||||
}
|
||||
mHitState = isSwimming ? CharState_SwimKnockDown : CharState_KnockDown;
|
||||
}
|
||||
else if (recovery)
|
||||
{
|
||||
std::string anim = chooseRandomGroup("swimhit");
|
||||
if (isSwimming && mAnimation->hasAnimation(anim))
|
||||
{
|
||||
mHitState = CharState_SwimHit;
|
||||
mCurrentHit = anim;
|
||||
mAnimation->play(mCurrentHit, Priority_Hit, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0);
|
||||
mHitState = isSwimming ? CharState_SwimHit : CharState_Hit;
|
||||
priority = Priority_Hit;
|
||||
}
|
||||
else
|
||||
{
|
||||
anim = chooseRandomGroup("hit");
|
||||
if (mAnimation->hasAnimation(anim))
|
||||
{
|
||||
mHitState = CharState_Hit;
|
||||
mCurrentHit = anim;
|
||||
mAnimation->play(mCurrentHit, Priority_Hit, MWRender::Animation::BlendMask_All, true, 1, "start", "stop", 0.0f, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (block && mAnimation->hasAnimation("shield"))
|
||||
else if (block)
|
||||
{
|
||||
mHitState = CharState_Block;
|
||||
mCurrentHit = "shield";
|
||||
MWRender::Animation::AnimPriority priorityBlock (Priority_Hit);
|
||||
priorityBlock[MWRender::Animation::BoneGroup_LeftArm] = Priority_Block;
|
||||
priorityBlock[MWRender::Animation::BoneGroup_LowerBody] = Priority_WeaponLowerBody;
|
||||
mAnimation->play(mCurrentHit, priorityBlock, MWRender::Animation::BlendMask_All, true, 1, "block start", "block stop", 0.0f, 0);
|
||||
priority = Priority_Block;
|
||||
priority[MWRender::Animation::BoneGroup_LeftArm] = Priority_Block;
|
||||
priority[MWRender::Animation::BoneGroup_LowerBody] = Priority_WeaponLowerBody;
|
||||
startKey = "block start";
|
||||
stopKey = "block stop";
|
||||
}
|
||||
|
||||
mCurrentHit = hitStateToAnimGroup(mHitState);
|
||||
|
||||
if (isRecovery())
|
||||
{
|
||||
mCurrentHit = chooseRandomGroup(mCurrentHit);
|
||||
if (mHitState == CharState_SwimHit && !mAnimation->hasAnimation(mCurrentHit))
|
||||
mCurrentHit = chooseRandomGroup(hitStateToAnimGroup(CharState_Hit));
|
||||
}
|
||||
|
||||
if (!mAnimation->hasAnimation(mCurrentHit))
|
||||
{
|
||||
// The hit animation is missing. Reset the current hit state and immediately cancel all states as if the animation were instantaneous.
|
||||
mHitState = CharState_None;
|
||||
mCurrentHit.clear();
|
||||
stats.setKnockedDown(false);
|
||||
stats.setHitRecovery(false);
|
||||
stats.setBlock(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel upper body animations
|
||||
if (isKnockedOut() || isKnockedDown())
|
||||
{
|
||||
clearStateAnimation(mCurrentWeapon);
|
||||
if (mUpperBodyState > UpperCharState_WeapEquiped)
|
||||
{
|
||||
mAnimation->disable(mCurrentWeapon);
|
||||
mUpperBodyState = UpperCharState_WeapEquiped;
|
||||
if (mWeaponType > ESM::Weapon::None)
|
||||
mAnimation->showWeapons(true);
|
||||
}
|
||||
else if (mUpperBodyState > UpperCharState_Nothing && mUpperBodyState < UpperCharState_WeapEquiped)
|
||||
else if (mUpperBodyState < UpperCharState_WeapEquiped)
|
||||
{
|
||||
mAnimation->disable(mCurrentWeapon);
|
||||
mUpperBodyState = UpperCharState_Nothing;
|
||||
}
|
||||
}
|
||||
if (mHitState != CharState_None)
|
||||
|
||||
mAnimation->play(mCurrentHit, priority, MWRender::Animation::BlendMask_All, true, 1, startKey, stopKey, 0.0f, ~0ul);
|
||||
|
||||
idle = CharState_None;
|
||||
}
|
||||
else if(!mAnimation->isPlaying(mCurrentHit))
|
||||
{
|
||||
mCurrentHit.erase();
|
||||
if (knockdown)
|
||||
stats.setKnockedDown(false);
|
||||
if (recovery)
|
||||
stats.setHitRecovery(false);
|
||||
if (block)
|
||||
stats.setBlock(false);
|
||||
mHitState = CharState_None;
|
||||
}
|
||||
else if (isKnockedOut() && stats.getFatigue().getCurrent() > 0 && mTimeUntilWake <= 0)
|
||||
{
|
||||
mHitState = isSwimming ? CharState_SwimKnockDown : CharState_KnockDown;
|
||||
mAnimation->disable(mCurrentHit);
|
||||
mAnimation->play(mCurrentHit, Priority_Knockdown, MWRender::Animation::BlendMask_All, true, 1, "loop stop", "stop", 0.0f, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void CharacterController::refreshJumpAnims(const std::string& weapShortGroup, JumpingState jump, CharacterState& idle, bool force)
|
||||
void CharacterController::refreshJumpAnims(JumpingState jump, CharacterState& idle, bool force)
|
||||
{
|
||||
if (!force && jump == mJumpState && idle == CharState_None)
|
||||
return;
|
||||
|
||||
std::string jumpAnimName;
|
||||
if (jump == JumpState_None)
|
||||
{
|
||||
resetCurrentJumpState();
|
||||
return;
|
||||
}
|
||||
|
||||
std::string weapShortGroup = getWeaponShortGroup(mWeaponType);
|
||||
std::string jumpAnimName = "jump" + weapShortGroup;
|
||||
MWRender::Animation::BlendMask jumpmask = MWRender::Animation::BlendMask_All;
|
||||
if (jump != JumpState_None)
|
||||
{
|
||||
jumpAnimName = "jump";
|
||||
if(!weapShortGroup.empty())
|
||||
{
|
||||
jumpAnimName += weapShortGroup;
|
||||
if(!mAnimation->hasAnimation(jumpAnimName))
|
||||
if (!weapShortGroup.empty() && !mAnimation->hasAnimation(jumpAnimName))
|
||||
{
|
||||
jumpAnimName = fallbackShortWeaponGroup("jump", &jumpmask);
|
||||
|
||||
@ -354,39 +439,22 @@ void CharacterController::refreshJumpAnims(const std::string& weapShortGroup, Ju
|
||||
if (jumpmask == MWRender::Animation::BlendMask_LowerBody && idle == CharState_None)
|
||||
idle = CharState_Idle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!force && jump == mJumpState)
|
||||
return;
|
||||
|
||||
bool startAtLoop = (jump == mJumpState);
|
||||
mJumpState = jump;
|
||||
clearStateAnimation(mCurrentJump);
|
||||
|
||||
if (!mCurrentJump.empty())
|
||||
{
|
||||
mAnimation->disable(mCurrentJump);
|
||||
mCurrentJump.clear();
|
||||
}
|
||||
if (!mAnimation->hasAnimation(jumpAnimName))
|
||||
return;
|
||||
|
||||
mCurrentJump = jumpAnimName;
|
||||
if(mJumpState == JumpState_InAir)
|
||||
{
|
||||
if (mAnimation->hasAnimation(jumpAnimName))
|
||||
{
|
||||
mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, false,
|
||||
1.0f, startAtLoop ? "loop start" : "start", "stop", 0.f, ~0ul);
|
||||
mCurrentJump = jumpAnimName;
|
||||
}
|
||||
}
|
||||
mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, false, 1.0f, startAtLoop ? "loop start" : "start", "stop", 0.f, ~0ul);
|
||||
else if (mJumpState == JumpState_Landing)
|
||||
{
|
||||
if (mAnimation->hasAnimation(jumpAnimName))
|
||||
{
|
||||
mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, true,
|
||||
1.0f, "loop stop", "stop", 0.0f, 0);
|
||||
mCurrentJump = jumpAnimName;
|
||||
}
|
||||
}
|
||||
mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, true, 1.0f, "loop stop", "stop", 0.0f, 0);
|
||||
}
|
||||
|
||||
bool CharacterController::onOpen() const
|
||||
@ -449,6 +517,13 @@ std::string CharacterController::getWeaponAnimation(int weaponType) const
|
||||
return weaponGroup;
|
||||
}
|
||||
|
||||
std::string CharacterController::getWeaponShortGroup(int weaponType) const
|
||||
{
|
||||
if (weaponType == ESM::Weapon::HandToHand && !mPtr.getClass().isBipedal(mPtr))
|
||||
return {};
|
||||
return getWeaponType(weaponType)->mShortGroup;
|
||||
}
|
||||
|
||||
std::string CharacterController::fallbackShortWeaponGroup(const std::string& baseGroupName, MWRender::Animation::BlendMask* blendMask) const
|
||||
{
|
||||
bool isRealWeapon = mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell && mWeaponType != ESM::Weapon::None;
|
||||
@ -460,8 +535,8 @@ std::string CharacterController::fallbackShortWeaponGroup(const std::string& bas
|
||||
return baseGroupName;
|
||||
}
|
||||
|
||||
static const std::string oneHandFallback = getWeaponType(ESM::Weapon::LongBladeOneHand)->mShortGroup;
|
||||
static const std::string twoHandFallback = getWeaponType(ESM::Weapon::LongBladeTwoHand)->mShortGroup;
|
||||
static const std::string oneHandFallback = getWeaponShortGroup(ESM::Weapon::LongBladeOneHand);
|
||||
static const std::string twoHandFallback = getWeaponShortGroup(ESM::Weapon::LongBladeTwoHand);
|
||||
|
||||
std::string groupName = baseGroupName;
|
||||
const ESM::WeaponType* weapInfo = getWeaponType(mWeaponType);
|
||||
@ -486,28 +561,37 @@ std::string CharacterController::fallbackShortWeaponGroup(const std::string& bas
|
||||
return groupName;
|
||||
}
|
||||
|
||||
void CharacterController::refreshMovementAnims(const std::string& weapShortGroup, CharacterState movement, CharacterState& idle, bool force)
|
||||
void CharacterController::refreshMovementAnims(CharacterState movement, CharacterState& idle, bool force)
|
||||
{
|
||||
if (movement == mMovementState && idle == mIdleState && !force)
|
||||
return;
|
||||
|
||||
// Reset idle if we actually play movement animations excepts of these cases:
|
||||
// 1. When we play turning animations
|
||||
// 2. When we use a fallback animation for lower body since movement animation for given weapon is missing (e.g. for crossbows and spellcasting)
|
||||
bool resetIdle = (movement != CharState_None && !isTurning());
|
||||
|
||||
std::string movementAnimName = movementStateToAnimGroup(movement);
|
||||
|
||||
if (movementAnimName.empty())
|
||||
{
|
||||
resetCurrentMovementState();
|
||||
return;
|
||||
}
|
||||
|
||||
std::string::size_type swimpos = movementAnimName.find("swim");
|
||||
if (!mAnimation->hasAnimation(movementAnimName))
|
||||
{
|
||||
if (swimpos != std::string::npos)
|
||||
{
|
||||
movementAnimName.erase(swimpos, 4);
|
||||
swimpos = std::string::npos;
|
||||
}
|
||||
}
|
||||
|
||||
MWRender::Animation::BlendMask movemask = MWRender::Animation::BlendMask_All;
|
||||
|
||||
if (!movementAnimName.empty())
|
||||
{
|
||||
if(!weapShortGroup.empty())
|
||||
{
|
||||
std::string::size_type swimpos = movementAnimName.find("swim");
|
||||
if (swimpos == std::string::npos)
|
||||
std::string weapShortGroup = getWeaponShortGroup(mWeaponType);
|
||||
if (swimpos == std::string::npos && !weapShortGroup.empty())
|
||||
{
|
||||
std::string weapMovementAnimName;
|
||||
if (mWeaponType == ESM::Weapon::Spell && (movement == CharState_TurnLeft || movement == CharState_TurnRight)) // Spellcasting stance turning is a special case
|
||||
// Spellcasting stance turning is a special case
|
||||
if (mWeaponType == ESM::Weapon::Spell && (movement == CharState_TurnLeft || movement == CharState_TurnRight))
|
||||
weapMovementAnimName = weapShortGroup + movementAnimName;
|
||||
else
|
||||
weapMovementAnimName = movementAnimName + weapShortGroup;
|
||||
@ -519,56 +603,29 @@ void CharacterController::refreshMovementAnims(const std::string& weapShortGroup
|
||||
// For upper body there will be idle animation.
|
||||
if (movemask == MWRender::Animation::BlendMask_LowerBody && idle == CharState_None)
|
||||
idle = CharState_Idle;
|
||||
|
||||
if (movemask == MWRender::Animation::BlendMask_LowerBody)
|
||||
resetIdle = false;
|
||||
}
|
||||
|
||||
movementAnimName = weapMovementAnimName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(force || movement != mMovementState)
|
||||
{
|
||||
mMovementState = movement;
|
||||
if (!movementAnimName.empty())
|
||||
{
|
||||
if (!force && movement == mMovementState)
|
||||
return;
|
||||
|
||||
if (!mAnimation->hasAnimation(movementAnimName))
|
||||
{
|
||||
std::string::size_type swimpos = movementAnimName.find("swim");
|
||||
if (swimpos != std::string::npos)
|
||||
{
|
||||
movementAnimName.erase(swimpos, 4);
|
||||
if (!weapShortGroup.empty())
|
||||
{
|
||||
std::string weapMovementAnimName = movementAnimName + weapShortGroup;
|
||||
if(mAnimation->hasAnimation(weapMovementAnimName))
|
||||
movementAnimName = weapMovementAnimName;
|
||||
else
|
||||
{
|
||||
movementAnimName = fallbackShortWeaponGroup(movementAnimName, &movemask);
|
||||
if (movemask == MWRender::Animation::BlendMask_LowerBody)
|
||||
resetIdle = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (swimpos == std::string::npos || !mAnimation->hasAnimation(movementAnimName))
|
||||
{
|
||||
std::string::size_type runpos = movementAnimName.find("run");
|
||||
if (runpos != std::string::npos)
|
||||
{
|
||||
movementAnimName.replace(runpos, 3, "walk");
|
||||
|
||||
if (!mAnimation->hasAnimation(movementAnimName))
|
||||
movementAnimName.clear();
|
||||
}
|
||||
else
|
||||
movementAnimName.clear();
|
||||
}
|
||||
{
|
||||
resetCurrentMovementState();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
mMovementState = movement;
|
||||
|
||||
// If we're playing the same animation, start it from the point it ended
|
||||
float startpoint = 0.f;
|
||||
if (!mCurrentMovement.empty() && movementAnimName == mCurrentMovement)
|
||||
@ -576,26 +633,22 @@ void CharacterController::refreshMovementAnims(const std::string& weapShortGroup
|
||||
|
||||
mMovementAnimationControlled = true;
|
||||
|
||||
mAnimation->disable(mCurrentMovement);
|
||||
|
||||
if (!mAnimation->hasAnimation(movementAnimName))
|
||||
movementAnimName.clear();
|
||||
|
||||
clearStateAnimation(mCurrentMovement);
|
||||
mCurrentMovement = movementAnimName;
|
||||
if(!mCurrentMovement.empty())
|
||||
|
||||
// Reset idle if we actually play movement animations excepts of these cases:
|
||||
// 1. When we play turning animations
|
||||
// 2. When we use a fallback animation for lower body since movement animation for given weapon is missing (e.g. for crossbows and spellcasting)
|
||||
if (!isTurning() && movemask == MWRender::Animation::BlendMask_All)
|
||||
{
|
||||
if (resetIdle)
|
||||
{
|
||||
mAnimation->disable(mCurrentIdle);
|
||||
mIdleState = CharState_None;
|
||||
resetCurrentIdleState();
|
||||
idle = CharState_None;
|
||||
}
|
||||
|
||||
// For non-flying creatures, MW uses the Walk animation to calculate the animation velocity
|
||||
// even if we are running. This must be replicated, otherwise the observed speed would differ drastically.
|
||||
mAdjustMovementAnimSpeed = true;
|
||||
if (mPtr.getClass().getType() == ESM::Creature::sRecordId
|
||||
&& !(mPtr.get<ESM::Creature>()->mBase->mFlags & ESM::Creature::Flies))
|
||||
if (mPtr.getClass().getType() == ESM::Creature::sRecordId && !(mPtr.get<ESM::Creature>()->mBase->mFlags & ESM::Creature::Flies))
|
||||
{
|
||||
CharacterState walkState = runStateToWalkState(mMovementState);
|
||||
std::string anim = movementStateToAnimGroup(walkState);
|
||||
@ -628,15 +681,10 @@ void CharacterController::refreshMovementAnims(const std::string& weapShortGroup
|
||||
}
|
||||
}
|
||||
|
||||
mAnimation->play(mCurrentMovement, Priority_Movement, movemask, false,
|
||||
1.f, "start", "stop", startpoint, ~0ul, true);
|
||||
}
|
||||
else
|
||||
mMovementState = CharState_None;
|
||||
}
|
||||
mAnimation->play(mCurrentMovement, Priority_Movement, movemask, false, 1.f, "start", "stop", startpoint, ~0ul, true);
|
||||
}
|
||||
|
||||
void CharacterController::refreshIdleAnims(const std::string& weapShortGroup, CharacterState idle, bool force)
|
||||
void CharacterController::refreshIdleAnims(CharacterState idle, bool force)
|
||||
{
|
||||
// FIXME: if one of the below states is close to their last animation frame (i.e. will be disabled in the coming update),
|
||||
// the idle animation should be displayed
|
||||
@ -646,59 +694,55 @@ void CharacterController::refreshIdleAnims(const std::string& weapShortGroup, Ch
|
||||
&& !mPtr.getClass().isBipedal(mPtr))
|
||||
idle = CharState_None;
|
||||
|
||||
if(force || idle != mIdleState || (!mAnimation->isPlaying(mCurrentIdle) && mAnimQueue.empty()))
|
||||
{
|
||||
if (!force && idle == mIdleState && (mAnimation->isPlaying(mCurrentIdle) || !mAnimQueue.empty()))
|
||||
return;
|
||||
|
||||
mIdleState = idle;
|
||||
size_t numLoops = ~0ul;
|
||||
|
||||
std::string idleGroup;
|
||||
MWRender::Animation::AnimPriority idlePriority (Priority_Default);
|
||||
std::string idleGroup = idleStateToAnimGroup(mIdleState);
|
||||
MWRender::Animation::AnimPriority priority = getIdlePriority(mIdleState);
|
||||
|
||||
// Only play "idleswim" or "idlesneak" if they exist. Otherwise, fallback to
|
||||
// "idle"+weapon or "idle".
|
||||
if(mIdleState == CharState_IdleSwim && mAnimation->hasAnimation("idleswim"))
|
||||
if ((mIdleState == CharState_IdleSwim || mIdleState == CharState_IdleSneak) && !mAnimation->hasAnimation(idleGroup))
|
||||
idleGroup = idleStateToAnimGroup(CharState_Idle);
|
||||
|
||||
if (idleGroup.empty())
|
||||
{
|
||||
idleGroup = "idleswim";
|
||||
idlePriority = Priority_SwimIdle;
|
||||
resetCurrentIdleState();
|
||||
return;
|
||||
}
|
||||
else if(mIdleState == CharState_IdleSneak && mAnimation->hasAnimation("idlesneak"))
|
||||
|
||||
std::string weapShortGroup = getWeaponShortGroup(mWeaponType);
|
||||
if (mIdleState != CharState_IdleSwim && mIdleState != CharState_IdleSneak && mIdleState != CharState_None && !weapShortGroup.empty())
|
||||
{
|
||||
idleGroup = "idlesneak";
|
||||
idlePriority[MWRender::Animation::BoneGroup_LowerBody] = Priority_SneakIdleLowerBody;
|
||||
}
|
||||
else if(mIdleState != CharState_None)
|
||||
{
|
||||
idleGroup = "idle";
|
||||
if(!weapShortGroup.empty())
|
||||
{
|
||||
idleGroup += weapShortGroup;
|
||||
if(!mAnimation->hasAnimation(idleGroup))
|
||||
{
|
||||
idleGroup = fallbackShortWeaponGroup("idle");
|
||||
}
|
||||
std::string weapIdleGroup = idleGroup + weapShortGroup;
|
||||
if (!mAnimation->hasAnimation(weapIdleGroup))
|
||||
weapIdleGroup = fallbackShortWeaponGroup(idleGroup);
|
||||
idleGroup = weapIdleGroup;
|
||||
|
||||
// play until the Loop Stop key 2 to 5 times, then play until the Stop key
|
||||
// this replicates original engine behavior for the "Idle1h" 1st-person animation
|
||||
auto& prng = MWBase::Environment::get().getWorld()->getPrng();
|
||||
numLoops = 1 + Misc::Rng::rollDice(4, prng);
|
||||
}
|
||||
}
|
||||
|
||||
// There is no need to restart anim if the new and old anims are the same.
|
||||
// Just update a number of loops.
|
||||
float startPoint = 0;
|
||||
if (!mCurrentIdle.empty() && mCurrentIdle == idleGroup)
|
||||
if (!mAnimation->hasAnimation(idleGroup))
|
||||
{
|
||||
resetCurrentIdleState();
|
||||
return;
|
||||
}
|
||||
|
||||
float startPoint = 0.f;
|
||||
// There is no need to restart anim if the new and old anims are the same.
|
||||
// Just update the number of loops.
|
||||
if (mCurrentIdle == idleGroup)
|
||||
mAnimation->getInfo(mCurrentIdle, &startPoint);
|
||||
}
|
||||
|
||||
if(!mCurrentIdle.empty())
|
||||
mAnimation->disable(mCurrentIdle);
|
||||
|
||||
clearStateAnimation(mCurrentIdle);
|
||||
mCurrentIdle = idleGroup;
|
||||
if(!mCurrentIdle.empty())
|
||||
mAnimation->play(mCurrentIdle, idlePriority, MWRender::Animation::BlendMask_All, false,
|
||||
1.0f, "start", "stop", startPoint, numLoops, true);
|
||||
}
|
||||
mAnimation->play(mCurrentIdle, priority, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", startPoint, numLoops, true);
|
||||
}
|
||||
|
||||
void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterState movement, JumpingState jump, bool force)
|
||||
@ -707,18 +751,12 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat
|
||||
if (isPersistentAnimPlaying())
|
||||
return;
|
||||
|
||||
if (mPtr.getClass().isActor())
|
||||
refreshHitRecoilAnims(idle);
|
||||
|
||||
std::string weap;
|
||||
if (mWeaponType != ESM::Weapon::HandToHand || mPtr.getClass().isBipedal(mPtr))
|
||||
weap = getWeaponType(mWeaponType)->mShortGroup;
|
||||
|
||||
refreshJumpAnims(weap, jump, idle, force);
|
||||
refreshMovementAnims(weap, movement, idle, force);
|
||||
refreshJumpAnims(jump, idle, force);
|
||||
refreshMovementAnims(movement, idle, force);
|
||||
|
||||
// idle handled last as it can depend on the other states
|
||||
refreshIdleAnims(weap, idle, force);
|
||||
refreshIdleAnims(idle, force);
|
||||
}
|
||||
|
||||
void CharacterController::playDeath(float startpoint, CharacterState death)
|
||||
@ -740,21 +778,11 @@ void CharacterController::playDeath(float startpoint, CharacterState death)
|
||||
// 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.
|
||||
mMovementState = CharState_None;
|
||||
mAnimation->disable(mCurrentMovement);
|
||||
mCurrentMovement.clear();
|
||||
mUpperBodyState = UpperCharState_Nothing;
|
||||
mAnimation->disable(mCurrentWeapon);
|
||||
mCurrentWeapon.clear();
|
||||
mHitState = CharState_None;
|
||||
mAnimation->disable(mCurrentHit);
|
||||
mCurrentHit.clear();
|
||||
mIdleState = CharState_None;
|
||||
mAnimation->disable(mCurrentIdle);
|
||||
mCurrentIdle.clear();
|
||||
mJumpState = JumpState_None;
|
||||
mAnimation->disable(mCurrentJump);
|
||||
mCurrentJump.clear();
|
||||
resetCurrentMovementState();
|
||||
resetCurrentWeaponState();
|
||||
resetCurrentHitState();
|
||||
resetCurrentIdleState();
|
||||
resetCurrentJumpState();
|
||||
mMovementAnimationControlled = true;
|
||||
|
||||
mAnimation->play(mCurrentDeath, Priority_Death, MWRender::Animation::BlendMask_All,
|
||||
@ -897,7 +925,7 @@ void CharacterController::handleTextKey(std::string_view groupname, SceneUtil::T
|
||||
{
|
||||
std::string_view evt = key->second;
|
||||
|
||||
if(evt.compare(0, 7, "sound: ") == 0)
|
||||
if (evt.substr(0, 7) == "sound: ")
|
||||
{
|
||||
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
|
||||
sndMgr->playSound3D(mPtr, evt.substr(7), 1.0f, 1.0f);
|
||||
@ -905,7 +933,7 @@ void CharacterController::handleTextKey(std::string_view groupname, SceneUtil::T
|
||||
}
|
||||
|
||||
auto& charClass = mPtr.getClass();
|
||||
if(evt.compare(0, 10, "soundgen: ") == 0)
|
||||
if (evt.substr(0, 10) == "soundgen: ")
|
||||
{
|
||||
std::string soundgen = std::string(evt.substr(10));
|
||||
|
||||
@ -947,30 +975,34 @@ void CharacterController::handleTextKey(std::string_view groupname, SceneUtil::T
|
||||
return;
|
||||
}
|
||||
|
||||
if(evt.compare(0, groupname.size(), groupname) != 0 ||
|
||||
evt.compare(groupname.size(), 2, ": ") != 0)
|
||||
if (evt.substr(0, groupname.size()) != groupname || evt.substr(groupname.size(), 2) != ": ")
|
||||
{
|
||||
// Not ours, skip it
|
||||
return;
|
||||
}
|
||||
const size_t off = groupname.size()+2;
|
||||
const size_t len = evt.size() - off;
|
||||
|
||||
if(groupname == "shield" && evt.compare(off, len, "equip attach") == 0)
|
||||
std::string_view action = evt.substr(groupname.size() + 2);
|
||||
if (action == "equip attach")
|
||||
{
|
||||
if (groupname == "shield")
|
||||
mAnimation->showCarriedLeft(true);
|
||||
else if(groupname == "shield" && evt.compare(off, len, "unequip detach") == 0)
|
||||
mAnimation->showCarriedLeft(false);
|
||||
else if(evt.compare(off, len, "equip attach") == 0)
|
||||
else
|
||||
mAnimation->showWeapons(true);
|
||||
else if(evt.compare(off, len, "unequip detach") == 0)
|
||||
}
|
||||
else if (action == "unequip detach")
|
||||
{
|
||||
if (groupname == "shield")
|
||||
mAnimation->showCarriedLeft(false);
|
||||
else
|
||||
mAnimation->showWeapons(false);
|
||||
else if(evt.compare(off, len, "chop hit") == 0)
|
||||
}
|
||||
else if (action == "chop hit")
|
||||
charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop);
|
||||
else if(evt.compare(off, len, "slash hit") == 0)
|
||||
else if (action == "slash hit")
|
||||
charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Slash);
|
||||
else if(evt.compare(off, len, "thrust hit") == 0)
|
||||
else if (action == "thrust hit")
|
||||
charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust);
|
||||
else if(evt.compare(off, len, "hit") == 0)
|
||||
else if (action == "hit")
|
||||
{
|
||||
if (groupname == "attack1" || groupname == "swimattack1")
|
||||
charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Chop);
|
||||
@ -981,9 +1013,7 @@ void CharacterController::handleTextKey(std::string_view groupname, SceneUtil::T
|
||||
else
|
||||
charClass.hit(mPtr, mAttackStrength);
|
||||
}
|
||||
else if (!groupname.empty()
|
||||
&& (groupname.compare(0, groupname.size()-1, "attack") == 0 || groupname.compare(0, groupname.size()-1, "swimattack") == 0)
|
||||
&& evt.compare(off, len, "start") == 0)
|
||||
else if (isRandomAttackAnimation(groupname) && action == "start")
|
||||
{
|
||||
std::multimap<float, std::string>::const_iterator hitKey = key;
|
||||
|
||||
@ -1012,25 +1042,22 @@ void CharacterController::handleTextKey(std::string_view groupname, SceneUtil::T
|
||||
charClass.hit(mPtr, mAttackStrength, ESM::Weapon::AT_Thrust);
|
||||
}
|
||||
}
|
||||
else if (evt.compare(off, len, "shoot attach") == 0)
|
||||
else if (action == "shoot attach")
|
||||
mAnimation->attachArrow();
|
||||
else if (evt.compare(off, len, "shoot release") == 0)
|
||||
else if (action == "shoot release")
|
||||
mAnimation->releaseArrow(mAttackStrength);
|
||||
else if (evt.compare(off, len, "shoot follow attach") == 0)
|
||||
else if (action == "shoot follow attach")
|
||||
mAnimation->attachArrow();
|
||||
|
||||
else if (groupname == "spellcast" && evt.substr(evt.size()-7, 7) == "release"
|
||||
// Make sure this key is actually for the RangeType we are casting. The flame atronach has
|
||||
// the same animation for all range types, so there are 3 "release" keys on the same time, one for each range type.
|
||||
&& evt.compare(off, len, mAttackType + " release") == 0)
|
||||
else if (groupname == "spellcast" && action == mAttackType + " release")
|
||||
{
|
||||
MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell);
|
||||
mCastingManualSpell = false;
|
||||
}
|
||||
|
||||
else if (groupname == "shield" && evt.compare(off, len, "block hit") == 0)
|
||||
else if (groupname == "shield" && action == "block hit")
|
||||
charClass.block(mPtr);
|
||||
else if (groupname == "containeropen" && evt.compare(off, len, "loot") == 0)
|
||||
else if (groupname == "containeropen" && action == "loot")
|
||||
MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_Container, mPtr);
|
||||
}
|
||||
|
||||
@ -1144,9 +1171,9 @@ bool CharacterController::updateState(CharacterState idle)
|
||||
if (isStillWeapon && mWeaponType != weaptype && mUpperBodyState > UpperCharState_WeapEquiped)
|
||||
{
|
||||
forcestateupdate = true;
|
||||
clearStateAnimation(mCurrentWeapon);
|
||||
mUpperBodyState = UpperCharState_WeapEquiped;
|
||||
setAttackingOrSpell(false);
|
||||
mAnimation->disable(mCurrentWeapon);
|
||||
mAnimation->showWeapons(true);
|
||||
stats.setAttackingOrSpell(false);
|
||||
}
|
||||
@ -1216,7 +1243,7 @@ bool CharacterController::updateState(CharacterState idle)
|
||||
|
||||
if (!isStillWeapon)
|
||||
{
|
||||
mAnimation->disable(mCurrentWeapon);
|
||||
clearStateAnimation(mCurrentWeapon);
|
||||
if (weaptype != ESM::Weapon::None)
|
||||
{
|
||||
mAnimation->showWeapons(false);
|
||||
@ -1264,8 +1291,7 @@ bool CharacterController::updateState(CharacterState idle)
|
||||
// Make sure that we disabled unequipping animation
|
||||
if (mUpperBodyState == UpperCharState_UnEquipingWeap)
|
||||
{
|
||||
mUpperBodyState = UpperCharState_Nothing;
|
||||
mAnimation->disable(mCurrentWeapon);
|
||||
resetCurrentWeaponState();
|
||||
mWeaponType = ESM::Weapon::None;
|
||||
mCurrentWeapon = getWeaponAnimation(mWeaponType);
|
||||
}
|
||||
@ -1307,7 +1333,7 @@ bool CharacterController::updateState(CharacterState idle)
|
||||
|
||||
if (!ammunition && mUpperBodyState > UpperCharState_WeapEquiped)
|
||||
{
|
||||
mAnimation->disable(mCurrentWeapon);
|
||||
clearStateAnimation(mCurrentWeapon);
|
||||
mUpperBodyState = UpperCharState_WeapEquiped;
|
||||
}
|
||||
}
|
||||
@ -1532,8 +1558,7 @@ bool CharacterController::updateState(CharacterState idle)
|
||||
idle != CharState_IdleSneak && idle != CharState_IdleSwim &&
|
||||
mIdleState != CharState_IdleSneak && mIdleState != CharState_IdleSwim)
|
||||
{
|
||||
mAnimation->disable(mCurrentIdle);
|
||||
mIdleState = CharState_None;
|
||||
resetCurrentIdleState();
|
||||
}
|
||||
|
||||
animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete);
|
||||
@ -1589,7 +1614,7 @@ bool CharacterController::updateState(CharacterState idle)
|
||||
if (mWeaponType > ESM::Weapon::None)
|
||||
mAnimation->showWeapons(true);
|
||||
}
|
||||
mAnimation->disable(mCurrentWeapon);
|
||||
clearStateAnimation(mCurrentWeapon);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1726,7 +1751,7 @@ bool CharacterController::updateState(CharacterState idle)
|
||||
}
|
||||
else if(complete >= 1.0f && isRandomAttackAnimation(mCurrentWeapon))
|
||||
{
|
||||
mAnimation->disable(mCurrentWeapon);
|
||||
clearStateAnimation(mCurrentWeapon);
|
||||
mUpperBodyState = UpperCharState_WeapEquiped;
|
||||
}
|
||||
|
||||
@ -1783,9 +1808,6 @@ void CharacterController::update(float duration)
|
||||
|
||||
updateMagicEffects();
|
||||
|
||||
if (isKnockedOut())
|
||||
mTimeUntilWake -= duration;
|
||||
|
||||
bool isPlayer = mPtr == MWMechanics::getPlayer();
|
||||
bool isFirstPersonPlayer = isPlayer && MWBase::Environment::get().getWorld()->isFirstPerson();
|
||||
bool godmode = isPlayer && MWBase::Environment::get().getWorld()->getGodModeState();
|
||||
@ -2046,7 +2068,7 @@ void CharacterController::update(float duration)
|
||||
vec.z() = 0.0f;
|
||||
|
||||
// We should reset idle animation during landing
|
||||
mAnimation->disable(mCurrentIdle);
|
||||
clearStateAnimation(mCurrentIdle);
|
||||
|
||||
float height = cls.getCreatureStats(mPtr).land(isPlayer);
|
||||
float healthLost = getFallDamage(mPtr, height);
|
||||
@ -2389,8 +2411,7 @@ void CharacterController::unpersistAnimationState()
|
||||
complete = (time - start) / (stop - start);
|
||||
}
|
||||
|
||||
mAnimation->disable(mCurrentIdle);
|
||||
mCurrentIdle.clear();
|
||||
clearStateAnimation(mCurrentIdle);
|
||||
mIdleState = CharState_SpecialIdle;
|
||||
|
||||
bool loopfallback = (mAnimQueue.front().mGroup.compare(0,4,"idle") == 0);
|
||||
@ -2440,8 +2461,7 @@ bool CharacterController::playGroup(const std::string &groupname, int mode, int
|
||||
{
|
||||
clearAnimQueue(persist);
|
||||
|
||||
mAnimation->disable(mCurrentIdle);
|
||||
mCurrentIdle.clear();
|
||||
clearStateAnimation(mCurrentIdle);
|
||||
|
||||
mIdleState = CharState_SpecialIdle;
|
||||
bool loopfallback = (entry.mGroup.compare(0,4,"idle") == 0);
|
||||
@ -2535,11 +2555,7 @@ CharacterController::KillResult CharacterController::kill()
|
||||
if (mDeathState == CharState_None)
|
||||
{
|
||||
playRandomDeath();
|
||||
|
||||
mAnimation->disable(mCurrentIdle);
|
||||
|
||||
mIdleState = CharState_None;
|
||||
mCurrentIdle.clear();
|
||||
resetCurrentIdleState();
|
||||
return Result_DeathAnimStarted;
|
||||
}
|
||||
|
||||
@ -2559,10 +2575,7 @@ void CharacterController::resurrect()
|
||||
if(mDeathState == CharState_None)
|
||||
return;
|
||||
|
||||
if(mAnimation)
|
||||
mAnimation->disable(mCurrentDeath);
|
||||
mCurrentDeath.clear();
|
||||
mDeathState = CharState_None;
|
||||
resetCurrentDeathState();
|
||||
mWeaponType = ESM::Weapon::None;
|
||||
}
|
||||
|
||||
|
@ -190,18 +190,24 @@ class CharacterController : public MWRender::Animation::TextKeyListener
|
||||
|
||||
bool mCastingManualSpell{false};
|
||||
|
||||
float mTimeUntilWake{0.f};
|
||||
|
||||
bool mIsMovingBackward{false};
|
||||
osg::Vec2f mSmoothedSpeed;
|
||||
|
||||
std::string getMovementBasedAttackType() const;
|
||||
|
||||
void clearStateAnimation(std::string &anim) const;
|
||||
void resetCurrentJumpState();
|
||||
void resetCurrentMovementState();
|
||||
void resetCurrentIdleState();
|
||||
void resetCurrentHitState();
|
||||
void resetCurrentWeaponState();
|
||||
void resetCurrentDeathState();
|
||||
|
||||
void refreshCurrentAnims(CharacterState idle, CharacterState movement, JumpingState jump, bool force=false);
|
||||
void refreshHitRecoilAnims(CharacterState& idle);
|
||||
void refreshJumpAnims(const std::string& weapShortGroup, JumpingState jump, CharacterState& idle, bool force=false);
|
||||
void refreshMovementAnims(const std::string& weapShortGroup, CharacterState movement, CharacterState& idle, bool force=false);
|
||||
void refreshIdleAnims(const std::string& weapShortGroup, CharacterState idle, bool force=false);
|
||||
void refreshJumpAnims(JumpingState jump, CharacterState& idle, bool force=false);
|
||||
void refreshMovementAnims(CharacterState movement, CharacterState& idle, bool force=false);
|
||||
void refreshIdleAnims(CharacterState idle, bool force=false);
|
||||
|
||||
void clearAnimQueue(bool clearPersistAnims = false);
|
||||
|
||||
@ -232,6 +238,7 @@ class CharacterController : public MWRender::Animation::TextKeyListener
|
||||
std::string fallbackShortWeaponGroup(const std::string& baseGroupName, MWRender::Animation::BlendMask* blendMask = nullptr) const;
|
||||
|
||||
std::string getWeaponAnimation(int weaponType) const;
|
||||
std::string getWeaponShortGroup(int weaponType) const;
|
||||
|
||||
bool getAttackingOrSpell() const;
|
||||
void setAttackingOrSpell(bool attackingOrSpell) const;
|
||||
|
Loading…
x
Reference in New Issue
Block a user