mirror of
https://github.com/OpenMW/openmw.git
synced 2025-06-21 11:41:33 +00:00
Spellcasting timing fixes (bug #4227)
Play spellcasting animation and VFX (but not hand VFX) if spellcasting failed due to insufficient magicka Apply spellcasting fatigue loss when the spellcasting starts instead of when the spell is applied
This commit is contained in:
parent
93eb470024
commit
a914d7a9b0
8 changed files with 77 additions and 45 deletions
|
@ -14,6 +14,7 @@
|
||||||
Bug #3867: All followers attack player when one follower enters combat with player
|
Bug #3867: All followers attack player when one follower enters combat with player
|
||||||
Bug #3905: Great House Dagoth issues
|
Bug #3905: Great House Dagoth issues
|
||||||
Bug #4203: Resurrecting an actor doesn't close the loot GUI
|
Bug #4203: Resurrecting an actor doesn't close the loot GUI
|
||||||
|
Bug #4227: Spellcasting restrictions are checked before spellcasting animations are played
|
||||||
Bug #4376: Moved actors don't respawn in their original cells
|
Bug #4376: Moved actors don't respawn in their original cells
|
||||||
Bug #4389: NPC's lips do not move if his head model has the NiBSAnimationNode root node
|
Bug #4389: NPC's lips do not move if his head model has the NiBSAnimationNode root node
|
||||||
Bug #4602: Robert's Bodies: crash inside createInstance()
|
Bug #4602: Robert's Bodies: crash inside createInstance()
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
#include "../mwworld/ptr.hpp"
|
#include "../mwworld/ptr.hpp"
|
||||||
#include "../mwworld/doorstate.hpp"
|
#include "../mwworld/doorstate.hpp"
|
||||||
|
#include "../mwworld/spellcaststate.hpp"
|
||||||
|
|
||||||
#include "../mwrender/rendermode.hpp"
|
#include "../mwrender/rendermode.hpp"
|
||||||
|
|
||||||
|
@ -541,9 +542,9 @@ namespace MWBase
|
||||||
/**
|
/**
|
||||||
* @brief startSpellCast attempt to start casting a spell. Might fail immediately if conditions are not met.
|
* @brief startSpellCast attempt to start casting a spell. Might fail immediately if conditions are not met.
|
||||||
* @param actor
|
* @param actor
|
||||||
* @return true if the spell can be casted (i.e. the animation should start)
|
* @return Success or the failure condition.
|
||||||
*/
|
*/
|
||||||
virtual bool startSpellCast (const MWWorld::Ptr& actor) = 0;
|
virtual MWWorld::SpellCastState startSpellCast (const MWWorld::Ptr& actor) = 0;
|
||||||
|
|
||||||
virtual void castSpell (const MWWorld::Ptr& actor, bool manualSpell=false) = 0;
|
virtual void castSpell (const MWWorld::Ptr& actor, bool manualSpell=false) = 0;
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@
|
||||||
#include "../mwworld/inventorystore.hpp"
|
#include "../mwworld/inventorystore.hpp"
|
||||||
#include "../mwworld/esmstore.hpp"
|
#include "../mwworld/esmstore.hpp"
|
||||||
#include "../mwworld/player.hpp"
|
#include "../mwworld/player.hpp"
|
||||||
|
#include "../mwworld/spellcaststate.hpp"
|
||||||
|
|
||||||
#include "aicombataction.hpp"
|
#include "aicombataction.hpp"
|
||||||
#include "movement.hpp"
|
#include "movement.hpp"
|
||||||
|
@ -1052,8 +1053,10 @@ void CharacterController::handleTextKey(std::string_view groupname, SceneUtil::T
|
||||||
// the same animation for all range types, so there are 3 "release" keys on the same time, one for each range type.
|
// the same animation for all range types, so there are 3 "release" keys on the same time, one for each range type.
|
||||||
else if (groupname == "spellcast" && action == mAttackType + " release")
|
else if (groupname == "spellcast" && action == mAttackType + " release")
|
||||||
{
|
{
|
||||||
MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell);
|
if (mCanCast)
|
||||||
|
MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell);
|
||||||
mCastingManualSpell = false;
|
mCastingManualSpell = false;
|
||||||
|
mCanCast = false;
|
||||||
}
|
}
|
||||||
else if (groupname == "shield" && action == "block hit")
|
else if (groupname == "shield" && action == "block hit")
|
||||||
charClass.block(mPtr);
|
charClass.block(mPtr);
|
||||||
|
@ -1377,7 +1380,13 @@ bool CharacterController::updateState(CharacterState idle)
|
||||||
}
|
}
|
||||||
std::string spellid = stats.getSpells().getSelectedSpell();
|
std::string spellid = stats.getSpells().getSelectedSpell();
|
||||||
bool isMagicItem = false;
|
bool isMagicItem = false;
|
||||||
bool canCast = mCastingManualSpell || world->startSpellCast(mPtr);
|
|
||||||
|
// Play hand VFX and allow castSpell use (assuming an animation is going to be played) if spellcasting is successful.
|
||||||
|
// Manual spellcasting bypasses restrictions.
|
||||||
|
MWWorld::SpellCastState spellCastResult = MWWorld::SpellCastState::Success;
|
||||||
|
if (!mCastingManualSpell)
|
||||||
|
spellCastResult = world->startSpellCast(mPtr);
|
||||||
|
mCanCast = spellCastResult == MWWorld::SpellCastState::Success;
|
||||||
|
|
||||||
if (spellid.empty())
|
if (spellid.empty())
|
||||||
{
|
{
|
||||||
|
@ -1402,7 +1411,9 @@ bool CharacterController::updateState(CharacterState idle)
|
||||||
resetIdle = false;
|
resetIdle = false;
|
||||||
mUpperBodyState = UpperCharState_CastingSpell;
|
mUpperBodyState = UpperCharState_CastingSpell;
|
||||||
}
|
}
|
||||||
else if(!spellid.empty() && canCast)
|
// 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);
|
world->breakInvisibility(mPtr);
|
||||||
MWMechanics::CastSpell cast(mPtr, nullptr, false, mCastingManualSpell);
|
MWMechanics::CastSpell cast(mPtr, nullptr, false, mCastingManualSpell);
|
||||||
|
@ -1420,24 +1431,26 @@ bool CharacterController::updateState(CharacterState idle)
|
||||||
const ESM::Spell *spell = store.get<ESM::Spell>().find(spellid);
|
const ESM::Spell *spell = store.get<ESM::Spell>().find(spellid);
|
||||||
effects = spell->mEffects.mList;
|
effects = spell->mEffects.mList;
|
||||||
}
|
}
|
||||||
|
if (mCanCast)
|
||||||
const ESM::MagicEffect *effect = store.get<ESM::MagicEffect>().find(effects.back().mEffectID); // use last effect of list for color of VFX_Hands
|
|
||||||
|
|
||||||
const ESM::Static* castStatic = world->getStore().get<ESM::Static>().find ("VFX_Hands");
|
|
||||||
|
|
||||||
const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS();
|
|
||||||
|
|
||||||
for (size_t iter = 0; iter < effects.size(); ++iter) // play hands vfx for each effect
|
|
||||||
{
|
{
|
||||||
if (mAnimation->getNode("Bip01 L Hand"))
|
const ESM::MagicEffect *effect = store.get<ESM::MagicEffect>().find(effects.back().mEffectID); // use last effect of list for color of VFX_Hands
|
||||||
mAnimation->addEffect(
|
|
||||||
Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs),
|
|
||||||
-1, false, "Bip01 L Hand", effect->mParticle);
|
|
||||||
|
|
||||||
if (mAnimation->getNode("Bip01 R Hand"))
|
const ESM::Static* castStatic = world->getStore().get<ESM::Static>().find ("VFX_Hands");
|
||||||
mAnimation->addEffect(
|
|
||||||
Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs),
|
const VFS::Manager* const vfs = MWBase::Environment::get().getResourceSystem()->getVFS();
|
||||||
-1, false, "Bip01 R Hand", effect->mParticle);
|
|
||||||
|
for (size_t iter = 0; iter < effects.size(); ++iter) // play hands vfx for each effect
|
||||||
|
{
|
||||||
|
if (mAnimation->getNode("Bip01 L Hand"))
|
||||||
|
mAnimation->addEffect(
|
||||||
|
Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs),
|
||||||
|
-1, false, "Bip01 L Hand", effect->mParticle);
|
||||||
|
|
||||||
|
if (mAnimation->getNode("Bip01 R Hand"))
|
||||||
|
mAnimation->addEffect(
|
||||||
|
Misc::ResourceHelpers::correctMeshPath(castStatic->mModel, vfs),
|
||||||
|
-1, false, "Bip01 R Hand", effect->mParticle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ESM::ENAMstruct &firstEffect = effects.at(0); // first effect used for casting animation
|
const ESM::ENAMstruct &firstEffect = effects.at(0); // first effect used for casting animation
|
||||||
|
@ -1448,8 +1461,10 @@ bool CharacterController::updateState(CharacterState idle)
|
||||||
{
|
{
|
||||||
startKey = "start";
|
startKey = "start";
|
||||||
stopKey = "stop";
|
stopKey = "stop";
|
||||||
world->castSpell(mPtr, mCastingManualSpell); // No "release" text key to use, so cast immediately
|
if (mCanCast)
|
||||||
|
world->castSpell(mPtr, mCastingManualSpell); // No "release" text key to use, so cast immediately
|
||||||
mCastingManualSpell = false;
|
mCastingManualSpell = false;
|
||||||
|
mCanCast = false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -2547,6 +2562,7 @@ void CharacterController::forceStateUpdate()
|
||||||
|
|
||||||
// Make sure we canceled the current attack or spellcasting,
|
// Make sure we canceled the current attack or spellcasting,
|
||||||
// because we disabled attack animations anyway.
|
// because we disabled attack animations anyway.
|
||||||
|
mCanCast = false;
|
||||||
mCastingManualSpell = false;
|
mCastingManualSpell = false;
|
||||||
setAttackingOrSpell(false);
|
setAttackingOrSpell(false);
|
||||||
if (mUpperBodyState != UpperCharState_Nothing)
|
if (mUpperBodyState != UpperCharState_Nothing)
|
||||||
|
|
|
@ -178,6 +178,8 @@ class CharacterController : public MWRender::Animation::TextKeyListener
|
||||||
|
|
||||||
std::string mAttackType; // slash, chop or thrust
|
std::string mAttackType; // slash, chop or thrust
|
||||||
|
|
||||||
|
bool mCanCast{false};
|
||||||
|
|
||||||
bool mCastingManualSpell{false};
|
bool mCastingManualSpell{false};
|
||||||
|
|
||||||
bool mIsMovingBackward{false};
|
bool mIsMovingBackward{false};
|
||||||
|
|
|
@ -313,8 +313,6 @@ namespace MWMechanics
|
||||||
mSourceName = spell->mName;
|
mSourceName = spell->mName;
|
||||||
mId = spell->mId;
|
mId = spell->mId;
|
||||||
|
|
||||||
const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore();
|
|
||||||
|
|
||||||
int school = 0;
|
int school = 0;
|
||||||
|
|
||||||
bool godmode = mCaster == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState();
|
bool godmode = mCaster == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState();
|
||||||
|
@ -327,16 +325,6 @@ namespace MWMechanics
|
||||||
|
|
||||||
if (!godmode)
|
if (!godmode)
|
||||||
{
|
{
|
||||||
// Reduce fatigue (note that in the vanilla game, both GMSTs are 0, and there's no fatigue loss)
|
|
||||||
static const float fFatigueSpellBase = store.get<ESM::GameSetting>().find("fFatigueSpellBase")->mValue.getFloat();
|
|
||||||
static const float fFatigueSpellMult = store.get<ESM::GameSetting>().find("fFatigueSpellMult")->mValue.getFloat();
|
|
||||||
DynamicStat<float> fatigue = stats.getFatigue();
|
|
||||||
const float normalizedEncumbrance = mCaster.getClass().getNormalizedEncumbrance(mCaster);
|
|
||||||
|
|
||||||
float fatigueLoss = MWMechanics::calcSpellCost(*spell) * (fFatigueSpellBase + normalizedEncumbrance * fFatigueSpellMult);
|
|
||||||
fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss);
|
|
||||||
stats.setFatigue(fatigue);
|
|
||||||
|
|
||||||
bool fail = false;
|
bool fail = false;
|
||||||
|
|
||||||
// Check success
|
// Check success
|
||||||
|
|
14
apps/openmw/mwworld/spellcaststate.hpp
Normal file
14
apps/openmw/mwworld/spellcaststate.hpp
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
#ifndef GAME_MWWORLD_SPELLCASTSTATE_H
|
||||||
|
#define GAME_MWWORLD_SPELLCASTSTATE_H
|
||||||
|
|
||||||
|
namespace MWWorld
|
||||||
|
{
|
||||||
|
enum class SpellCastState
|
||||||
|
{
|
||||||
|
Success = 0,
|
||||||
|
InsufficientMagicka = 1,
|
||||||
|
PowerAlreadyUsed = 2
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -2969,12 +2969,12 @@ namespace MWWorld
|
||||||
mGroundcoverStore.init(mStore.get<ESM::Static>(), fileCollections, groundcoverFiles, encoder, listener);
|
mGroundcoverStore.init(mStore.get<ESM::Static>(), fileCollections, groundcoverFiles, encoder, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool World::startSpellCast(const Ptr &actor)
|
MWWorld::SpellCastState World::startSpellCast(const Ptr &actor)
|
||||||
{
|
{
|
||||||
MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor);
|
MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor);
|
||||||
|
|
||||||
std::string message;
|
std::string message;
|
||||||
bool fail = false;
|
MWWorld::SpellCastState result = MWWorld::SpellCastState::Success;
|
||||||
bool isPlayer = (actor == getPlayerPtr());
|
bool isPlayer = (actor == getPlayerPtr());
|
||||||
|
|
||||||
std::string selectedSpell = stats.getSpells().getSelectedSpell();
|
std::string selectedSpell = stats.getSpells().getSelectedSpell();
|
||||||
|
@ -2990,28 +2990,38 @@ namespace MWWorld
|
||||||
if (spellCost > 0 && magicka.getCurrent() < spellCost && !godmode)
|
if (spellCost > 0 && magicka.getCurrent() < spellCost && !godmode)
|
||||||
{
|
{
|
||||||
message = "#{sMagicInsufficientSP}";
|
message = "#{sMagicInsufficientSP}";
|
||||||
fail = true;
|
result = MWWorld::SpellCastState::InsufficientMagicka;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If this is a power, check if it was already used in the last 24h
|
// If this is a power, check if it was already used in the last 24h
|
||||||
if (!fail && spell->mData.mType == ESM::Spell::ST_Power && !stats.getSpells().canUsePower(spell))
|
if (result == MWWorld::SpellCastState::Success && spell->mData.mType == ESM::Spell::ST_Power && !stats.getSpells().canUsePower(spell))
|
||||||
{
|
{
|
||||||
message = "#{sPowerAlreadyUsed}";
|
message = "#{sPowerAlreadyUsed}";
|
||||||
fail = true;
|
result = MWWorld::SpellCastState::PowerAlreadyUsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reduce mana
|
if (result == MWWorld::SpellCastState::Success && !godmode)
|
||||||
if (!fail && !godmode)
|
|
||||||
{
|
{
|
||||||
|
// Reduce mana
|
||||||
magicka.setCurrent(magicka.getCurrent() - spellCost);
|
magicka.setCurrent(magicka.getCurrent() - spellCost);
|
||||||
stats.setMagicka(magicka);
|
stats.setMagicka(magicka);
|
||||||
|
|
||||||
|
// Reduce fatigue (note that in the vanilla game, both GMSTs are 0, and there's no fatigue loss)
|
||||||
|
static const float fFatigueSpellBase = mStore.get<ESM::GameSetting>().find("fFatigueSpellBase")->mValue.getFloat();
|
||||||
|
static const float fFatigueSpellMult = mStore.get<ESM::GameSetting>().find("fFatigueSpellMult")->mValue.getFloat();
|
||||||
|
MWMechanics::DynamicStat<float> fatigue = stats.getFatigue();
|
||||||
|
const float normalizedEncumbrance = actor.getClass().getNormalizedEncumbrance(actor);
|
||||||
|
|
||||||
|
float fatigueLoss = spellCost * (fFatigueSpellBase + normalizedEncumbrance * fFatigueSpellMult);
|
||||||
|
fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss);
|
||||||
|
stats.setFatigue(fatigue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPlayer && fail)
|
if (isPlayer && result != MWWorld::SpellCastState::Success)
|
||||||
MWBase::Environment::get().getWindowManager()->messageBox(message);
|
MWBase::Environment::get().getWindowManager()->messageBox(message);
|
||||||
|
|
||||||
return !fail;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void World::castSpell(const Ptr &actor, bool manualSpell)
|
void World::castSpell(const Ptr &actor, bool manualSpell)
|
||||||
|
|
|
@ -639,9 +639,9 @@ namespace MWWorld
|
||||||
/**
|
/**
|
||||||
* @brief startSpellCast attempt to start casting a spell. Might fail immediately if conditions are not met.
|
* @brief startSpellCast attempt to start casting a spell. Might fail immediately if conditions are not met.
|
||||||
* @param actor
|
* @param actor
|
||||||
* @return true if the spell can be casted (i.e. the animation should start)
|
* @return Success or the failure condition.
|
||||||
*/
|
*/
|
||||||
bool startSpellCast (const MWWorld::Ptr& actor) override;
|
MWWorld::SpellCastState startSpellCast (const MWWorld::Ptr& actor) override;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Cast the actual spell, should be called mid-animation
|
* @brief Cast the actual spell, should be called mid-animation
|
||||||
|
|
Loading…
Reference in a new issue