mirror of
https://github.com/OpenMW/openmw.git
synced 2025-12-24 13:23:08 +00:00
Merge branch 'unrestrictedfailure' into 'master'
Spellcasting timing fixes (bug #4227) Closes #4227 See merge request OpenMW/openmw!2201
This commit is contained in:
commit
5cd4dbd9a9
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::updateWeaponState(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::updateWeaponState(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::updateWeaponState(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::updateWeaponState(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