Rework spell effects management

pull/540/head
Andrei Kortunov 7 years ago
parent 8516aee6e0
commit 31f8bea1dd

@ -95,6 +95,7 @@
Bug #4574: Player turning animations are twitchy Bug #4574: Player turning animations are twitchy
Bug #4575: Weird result of attack animation blending with movement animations Bug #4575: Weird result of attack animation blending with movement animations
Bug #4576: Reset of idle animations when attack can not be started Bug #4576: Reset of idle animations when attack can not be started
Feature #1645: Casting effects from objects
Feature #2606: Editor: Implemented (optional) case sensitive global search Feature #2606: Editor: Implemented (optional) case sensitive global search
Feature #3083: Play animation when NPC is casting spell via script Feature #3083: Play animation when NPC is casting spell via script
Feature #3103: Provide option for disposition to get increased by successful trade Feature #3103: Provide option for disposition to get increased by successful trade

@ -536,7 +536,7 @@ namespace MWBase
/// Spawn a blood effect for \a ptr at \a worldPosition /// Spawn a blood effect for \a ptr at \a worldPosition
virtual void spawnBloodEffect (const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) = 0; virtual void spawnBloodEffect (const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) = 0;
virtual void spawnEffect (const std::string& model, const std::string& textureOverride, const osg::Vec3f& worldPos) = 0; virtual void spawnEffect (const std::string& model, const std::string& textureOverride, const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true) = 0;
virtual void explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster, virtual void explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster,
const MWWorld::Ptr& ignore, ESM::RangeType rangeType, const std::string& id, const MWWorld::Ptr& ignore, ESM::RangeType rangeType, const std::string& id,

@ -24,6 +24,7 @@
#include "../mwworld/inventorystore.hpp" #include "../mwworld/inventorystore.hpp"
#include "../mwrender/animation.hpp" #include "../mwrender/animation.hpp"
#include "../mwrender/vismask.hpp"
#include "npcstats.hpp" #include "npcstats.hpp"
#include "actorutil.hpp" #include "actorutil.hpp"
@ -327,16 +328,19 @@ namespace MWMechanics
} }
void CastSpell::launchMagicBolt () void CastSpell::launchMagicBolt ()
{ {
osg::Vec3f fallbackDirection (0,1,0); osg::Vec3f fallbackDirection(0, 1, 0);
osg::Vec3f offset(0, 0, 0);
if (!mTarget.isEmpty() && mTarget.getClass().isActor())
offset.z() = MWBase::Environment::get().getWorld()->getHalfExtents(mTarget).z();
// Fall back to a "caster to target" direction if we have no other means of determining it // Fall back to a "caster to target" direction if we have no other means of determining it
// (e.g. when cast by a non-actor) // (e.g. when cast by a non-actor)
if (!mTarget.isEmpty()) if (!mTarget.isEmpty())
fallbackDirection = fallbackDirection =
osg::Vec3f(mTarget.getRefData().getPosition().asVec3())- (mTarget.getRefData().getPosition().asVec3() + offset) -
osg::Vec3f(mCaster.getRefData().getPosition().asVec3()); (mCaster.getRefData().getPosition().asVec3());
MWBase::Environment::get().getWorld()->launchMagicBolt(mId, mCaster, fallbackDirection); MWBase::Environment::get().getWorld()->launchMagicBolt(mId, mCaster, fallbackDirection);
} }
@ -999,11 +1003,13 @@ namespace MWMechanics
return true; return true;
} }
void CastSpell::playSpellCastingEffects(const std::string &spellid){ void CastSpell::playSpellCastingEffects(const std::string &spellid)
{
const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore();
const ESM::Spell *spell = store.get<ESM::Spell>().find(spellid); const ESM::Spell *spell = store.get<ESM::Spell>().find(spellid);
std::vector<std::string> addedEffects;
for (std::vector<ESM::ENAMstruct>::const_iterator iter = spell->mEffects.mList.begin(); for (std::vector<ESM::ENAMstruct>::const_iterator iter = spell->mEffects.mList.begin();
iter != spell->mEffects.mList.end(); ++iter) iter != spell->mEffects.mList.end(); ++iter)
{ {
@ -1012,18 +1018,56 @@ namespace MWMechanics
MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(mCaster); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(mCaster);
if (animation && mCaster.getClass().isActor()) // TODO: Non-actors should also create a spell cast vfx even if they are disabled (animation == NULL) const ESM::Static* castStatic;
if (!effect->mCasting.empty())
castStatic = store.get<ESM::Static>().find (effect->mCasting);
else
castStatic = store.get<ESM::Static>().find ("VFX_DefaultCast");
// check if the effect was already added
if (std::find(addedEffects.begin(), addedEffects.end(), "meshes\\" + castStatic->mModel) != addedEffects.end())
continue;
std::string texture = effect->mParticle;
float scale = 1.0f;
osg::Vec3f pos (mCaster.getRefData().getPosition().asVec3());
if (animation && mCaster.getClass().isNpc())
{ {
const ESM::Static* castStatic; // For NPC we should take race height as scaling factor
const ESM::NPC *npc = mCaster.get<ESM::NPC>()->mBase;
const MWWorld::ESMStore &esmStore =
MWBase::Environment::get().getWorld()->getStore();
if (!effect->mCasting.empty()) const ESM::Race *race =
castStatic = store.get<ESM::Static>().find (effect->mCasting); esmStore.get<ESM::Race>().find(npc->mRace);
else
castStatic = store.get<ESM::Static>().find ("VFX_DefaultCast");
std::string texture = effect->mParticle; scale = npc->isMale() ? race->mData.mHeight.mMale : race->mData.mHeight.mFemale;
}
else
{
osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(mCaster);
animation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex, false, "", texture); // TODO: take a size of particle or NPC with height and weight = 1.0 as scale = 1.0
float scaleX = halfExtents.x() * 2 / 60.f;
float scaleY = halfExtents.y() * 2 / 60.f;
float scaleZ = halfExtents.z() * 2 / 120.f;
scale = std::max({ scaleX, scaleY, scaleZ });
}
// If the caster has no animation, add the effect directly to the effectManager
if (animation)
{
animation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex, false, "", texture, scale);
}
else
{
// We should set scale for effect manager manually
float meshScale = !mCaster.getClass().isActor() ? mCaster.getCellRef().getScale() : 1.0f;
MWBase::Environment::get().getWorld()->spawnEffect("meshes\\" + castStatic->mModel, effect->mParticle, pos, scale * meshScale);
} }
if (animation && !mCaster.getClass().isActor()) if (animation && !mCaster.getClass().isActor())
@ -1033,6 +1077,8 @@ namespace MWMechanics
"alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration"
}; };
addedEffects.push_back("meshes\\" + castStatic->mModel);
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
if(!effect->mCastSound.empty()) if(!effect->mCastSound.empty())
sndMgr->playSound3D(mCaster, effect->mCastSound, 1.0f, 1.0f); sndMgr->playSound3D(mCaster, effect->mCastSound, 1.0f, 1.0f);

@ -8,6 +8,7 @@
#include <osg/MatrixTransform> #include <osg/MatrixTransform>
#include <osg/BlendFunc> #include <osg/BlendFunc>
#include <osg/Material> #include <osg/Material>
#include <osg/PositionAttitudeTransform>
#include <osgParticle/ParticleSystem> #include <osgParticle/ParticleSystem>
#include <osgParticle/ParticleProcessor> #include <osgParticle/ParticleProcessor>
@ -204,6 +205,110 @@ namespace
std::vector<std::pair<osg::Node*, osg::Group*> > mToRemove; std::vector<std::pair<osg::Node*, osg::Group*> > mToRemove;
}; };
class RemoveFinishedCallbackVisitor : public RemoveVisitor
{
public:
RemoveFinishedCallbackVisitor()
: RemoveVisitor()
, mEffectId(-1)
{
}
RemoveFinishedCallbackVisitor(int effectId)
: RemoveVisitor()
, mEffectId(effectId)
{
}
virtual void apply(osg::Node &node)
{
traverse(node);
}
virtual void apply(osg::Group &group)
{
traverse(group);
osg::Callback* callback = group.getUpdateCallback();
if (callback)
{
// We should remove empty transformation nodes and finished callbacks here
MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast<MWRender::UpdateVfxCallback*>(callback);
bool finished = vfxCallback && vfxCallback->mFinished;
bool toRemove = vfxCallback && mEffectId >= 0 && vfxCallback->mParams.mEffectId == mEffectId;
if (finished || toRemove)
{
mToRemove.push_back(std::make_pair(group.asNode(), group.getParent(0)));
}
}
}
virtual void apply(osg::MatrixTransform &node)
{
traverse(node);
}
virtual void apply(osg::Geometry&)
{
}
private:
int mEffectId;
};
class FindVfxCallbacksVisitor : public osg::NodeVisitor
{
public:
std::vector<MWRender::UpdateVfxCallback*> mCallbacks;
FindVfxCallbacksVisitor()
: osg::NodeVisitor(TRAVERSE_ALL_CHILDREN)
, mEffectId(-1)
{
}
FindVfxCallbacksVisitor(int effectId)
: osg::NodeVisitor(TRAVERSE_ALL_CHILDREN)
, mEffectId(effectId)
{
}
virtual void apply(osg::Node &node)
{
traverse(node);
}
virtual void apply(osg::Group &group)
{
osg::Callback* callback = group.getUpdateCallback();
if (callback)
{
MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast<MWRender::UpdateVfxCallback*>(callback);
if (vfxCallback)
{
if (mEffectId < 0 || vfxCallback->mParams.mEffectId == mEffectId)
{
mCallbacks.push_back(vfxCallback);
}
}
}
traverse(group);
}
virtual void apply(osg::MatrixTransform &node)
{
traverse(node);
}
virtual void apply(osg::Geometry&)
{
}
private:
int mEffectId;
};
// Removes all drawables from a graph. // Removes all drawables from a graph.
class CleanObjectRootVisitor : public RemoveVisitor class CleanObjectRootVisitor : public RemoveVisitor
{ {
@ -287,7 +392,6 @@ namespace
} }
} }
}; };
} }
namespace MWRender namespace MWRender
@ -432,6 +536,42 @@ namespace MWRender
const std::multimap<float, std::string>& getTextKeys() const; const std::multimap<float, std::string>& getTextKeys() const;
}; };
void UpdateVfxCallback::operator()(osg::Node* node, osg::NodeVisitor* nv)
{
traverse(node, nv);
if (mFinished)
return;
double newTime = nv->getFrameStamp()->getSimulationTime();
if (mStartingTime == 0)
{
mStartingTime = newTime;
return;
}
double duration = newTime - mStartingTime;
mStartingTime = newTime;
mParams.mAnimTime->addTime(duration);
if (mParams.mAnimTime->getTime() >= mParams.mMaxControllerLength)
{
if (mParams.mLoop)
{
// Start from the beginning again; carry over the remainder
// Not sure if this is actually needed, the controller function might already handle loops
float remainder = mParams.mAnimTime->getTime() - mParams.mMaxControllerLength;
mParams.mAnimTime->resetTime(remainder);
}
else
{
// Remove effect immediately
mParams.mObjects.reset();
mFinished = true;
}
}
}
class ResetAccumRootCallback : public osg::NodeCallback class ResetAccumRootCallback : public osg::NodeCallback
{ {
public: public:
@ -1436,15 +1576,22 @@ namespace MWRender
useQuadratic, quadraticValue, quadraticRadiusMult, useLinear, linearRadiusMult, linearValue); useQuadratic, quadraticValue, quadraticRadiusMult, useLinear, linearRadiusMult, linearValue);
} }
void Animation::addEffect (const std::string& model, int effectId, bool loop, const std::string& bonename, const std::string& texture) void Animation::addEffect (const std::string& model, int effectId, bool loop, const std::string& bonename, const std::string& texture, float scale)
{ {
if (!mObjectRoot.get()) if (!mObjectRoot.get())
return; return;
// Early out if we already have this effect // Early out if we already have this effect
for (std::vector<EffectParams>::iterator it = mEffects.begin(); it != mEffects.end(); ++it) FindVfxCallbacksVisitor visitor(effectId);
if (it->mLoop && loop && it->mEffectId == effectId && it->mBoneName == bonename) mInsert->accept(visitor);
for (std::vector<UpdateVfxCallback*>::iterator it = visitor.mCallbacks.begin(); it != visitor.mCallbacks.end(); ++it)
{
UpdateVfxCallback* callback = *it;
if (loop && !callback->mFinished && callback->mParams.mLoop && callback->mParams.mBoneName == bonename)
return; return;
}
EffectParams params; EffectParams params;
params.mModelName = model; params.mModelName = model;
@ -1459,11 +1606,13 @@ namespace MWRender
parentNode = found->second; parentNode = found->second;
} }
osg::ref_ptr<osg::Node> node = mResourceSystem->getSceneManager()->getInstance(model, parentNode);
node->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); osg::ref_ptr<osg::PositionAttitudeTransform> trans = new osg::PositionAttitudeTransform;
trans->setScale(osg::Vec3f(scale, scale, scale));
parentNode->addChild(trans);
params.mObjects = PartHolderPtr(new PartHolder(node)); osg::ref_ptr<osg::Node> node = mResourceSystem->getSceneManager()->getInstance(model, trans);
node->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
SceneUtil::FindMaxControllerLengthVisitor findMaxLengthVisitor; SceneUtil::FindMaxControllerLengthVisitor findMaxLengthVisitor;
node->accept(findMaxLengthVisitor); node->accept(findMaxLengthVisitor);
@ -1471,71 +1620,50 @@ namespace MWRender
// FreezeOnCull doesn't work so well with effect particles, that tend to have moving emitters // FreezeOnCull doesn't work so well with effect particles, that tend to have moving emitters
SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor; SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor;
node->accept(disableFreezeOnCullVisitor); node->accept(disableFreezeOnCullVisitor);
params.mMaxControllerLength = findMaxLengthVisitor.getMaxLength();
node->setNodeMask(Mask_Effect); node->setNodeMask(Mask_Effect);
params.mMaxControllerLength = findMaxLengthVisitor.getMaxLength();
params.mLoop = loop; params.mLoop = loop;
params.mEffectId = effectId; params.mEffectId = effectId;
params.mBoneName = bonename; params.mBoneName = bonename;
params.mObjects = PartHolderPtr(new PartHolder(node));
params.mAnimTime = std::shared_ptr<EffectAnimationTime>(new EffectAnimationTime); params.mAnimTime = std::shared_ptr<EffectAnimationTime>(new EffectAnimationTime);
trans->addUpdateCallback(new UpdateVfxCallback(params));
SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::shared_ptr<SceneUtil::ControllerSource>(params.mAnimTime)); SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::shared_ptr<SceneUtil::ControllerSource>(params.mAnimTime));
node->accept(assignVisitor); node->accept(assignVisitor);
overrideFirstRootTexture(texture, mResourceSystem, node); overrideFirstRootTexture(texture, mResourceSystem, node);
// TODO: in vanilla morrowind the effect is scaled based on the host object's bounding box.
mEffects.push_back(params);
} }
void Animation::removeEffect(int effectId) void Animation::removeEffect(int effectId)
{ {
for (std::vector<EffectParams>::iterator it = mEffects.begin(); it != mEffects.end(); ++it) RemoveFinishedCallbackVisitor visitor(effectId);
{ mInsert->accept(visitor);
if (it->mEffectId == effectId) visitor.remove();
{
mEffects.erase(it);
return;
}
}
} }
void Animation::getLoopingEffects(std::vector<int> &out) const void Animation::getLoopingEffects(std::vector<int> &out) const
{ {
for (std::vector<EffectParams>::const_iterator it = mEffects.begin(); it != mEffects.end(); ++it) FindVfxCallbacksVisitor visitor;
mInsert->accept(visitor);
for (std::vector<UpdateVfxCallback*>::iterator it = visitor.mCallbacks.begin(); it != visitor.mCallbacks.end(); ++it)
{ {
if (it->mLoop) UpdateVfxCallback* callback = *it;
out.push_back(it->mEffectId);
if (callback->mParams.mLoop && !callback->mFinished)
out.push_back(callback->mParams.mEffectId);
} }
} }
void Animation::updateEffects(float duration) void Animation::updateEffects(float duration)
{ {
for (std::vector<EffectParams>::iterator it = mEffects.begin(); it != mEffects.end(); ) // TODO: objects without animation still will have
{ // transformation nodes with finished callbacks
it->mAnimTime->addTime(duration); RemoveFinishedCallbackVisitor visitor;
mInsert->accept(visitor);
if (it->mAnimTime->getTime() >= it->mMaxControllerLength) visitor.remove();
{
if (it->mLoop)
{
// Start from the beginning again; carry over the remainder
// Not sure if this is actually needed, the controller function might already handle loops
float remainder = it->mAnimTime->getTime() - it->mMaxControllerLength;
it->mAnimTime->resetTime(remainder);
}
else
{
it = mEffects.erase(it);
continue;
}
}
++it;
}
} }
bool Animation::upperBodyReady() const bool Animation::upperBodyReady() const
@ -1778,5 +1906,4 @@ namespace MWRender
mNode->getParent(0)->removeChild(mNode); mNode->getParent(0)->removeChild(mNode);
} }
} }
} }

@ -71,6 +71,17 @@ private:
}; };
typedef std::shared_ptr<PartHolder> PartHolderPtr; typedef std::shared_ptr<PartHolder> PartHolderPtr;
struct EffectParams
{
std::string mModelName; // Just here so we don't add the same effect twice
PartHolderPtr mObjects;
std::shared_ptr<EffectAnimationTime> mAnimTime;
float mMaxControllerLength;
int mEffectId;
bool mLoop;
std::string mBoneName;
};
class Animation : public osg::Referenced class Animation : public osg::Referenced
{ {
public: public:
@ -247,19 +258,6 @@ protected:
osg::Vec3f mAccumulate; osg::Vec3f mAccumulate;
struct EffectParams
{
std::string mModelName; // Just here so we don't add the same effect twice
PartHolderPtr mObjects;
std::shared_ptr<EffectAnimationTime> mAnimTime;
float mMaxControllerLength;
int mEffectId;
bool mLoop;
std::string mBoneName;
};
std::vector<EffectParams> mEffects;
TextKeyListener* mTextKeyListener; TextKeyListener* mTextKeyListener;
osg::ref_ptr<RotateController> mHeadController; osg::ref_ptr<RotateController> mHeadController;
@ -369,7 +367,7 @@ public:
* @param texture override the texture specified in the model's materials - if empty, do not override * @param texture override the texture specified in the model's materials - if empty, do not override
* @note Will not add an effect twice. * @note Will not add an effect twice.
*/ */
void addEffect (const std::string& model, int effectId, bool loop = false, const std::string& bonename = "", const std::string& texture = ""); void addEffect (const std::string& model, int effectId, bool loop = false, const std::string& bonename = "", const std::string& texture = "", float scale = 1.0f);
void removeEffect (int effectId); void removeEffect (int effectId);
void getLoopingEffects (std::vector<int>& out) const; void getLoopingEffects (std::vector<int>& out) const;
@ -489,5 +487,24 @@ public:
ObjectAnimation(const MWWorld::Ptr& ptr, const std::string &model, Resource::ResourceSystem* resourceSystem, bool animated, bool allowLight); ObjectAnimation(const MWWorld::Ptr& ptr, const std::string &model, Resource::ResourceSystem* resourceSystem, bool animated, bool allowLight);
}; };
class UpdateVfxCallback : public osg::NodeCallback
{
public:
UpdateVfxCallback(EffectParams& params)
: mFinished(false)
, mParams(params)
, mStartingTime(0)
{
}
bool mFinished;
EffectParams mParams;
virtual void operator()(osg::Node* node, osg::NodeVisitor* nv);
private:
double mStartingTime;
};
} }
#endif #endif

@ -1082,6 +1082,7 @@ namespace MWScript
MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getPtr (targetId, false); MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getPtr (targetId, false);
MWMechanics::CastSpell cast(ptr, target, false, true); MWMechanics::CastSpell cast(ptr, target, false, true);
cast.playSpellCastingEffects(spell->mId);
cast.mHitPosition = target.getRefData().getPosition().asVec3(); cast.mHitPosition = target.getRefData().getPosition().asVec3();
cast.mAlwaysSucceed = true; cast.mAlwaysSucceed = true;
cast.cast(spell); cast.cast(spell);

@ -3407,9 +3407,9 @@ namespace MWWorld
mRendering->spawnEffect(model, texture, worldPosition, 1.0f, false); mRendering->spawnEffect(model, texture, worldPosition, 1.0f, false);
} }
void World::spawnEffect(const std::string &model, const std::string &textureOverride, const osg::Vec3f &worldPos) void World::spawnEffect(const std::string &model, const std::string &textureOverride, const osg::Vec3f &worldPos, float scale, bool isMagicVFX)
{ {
mRendering->spawnEffect(model, textureOverride, worldPos); mRendering->spawnEffect(model, textureOverride, worldPos, scale, isMagicVFX);
} }
void World::explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const Ptr& caster, const Ptr& ignore, ESM::RangeType rangeType, void World::explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const Ptr& caster, const Ptr& ignore, ESM::RangeType rangeType,

@ -646,7 +646,7 @@ namespace MWWorld
/// Spawn a blood effect for \a ptr at \a worldPosition /// Spawn a blood effect for \a ptr at \a worldPosition
void spawnBloodEffect (const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) override; void spawnBloodEffect (const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) override;
void spawnEffect (const std::string& model, const std::string& textureOverride, const osg::Vec3f& worldPos) override; void spawnEffect (const std::string& model, const std::string& textureOverride, const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true) override;
void explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const MWWorld::Ptr& ignore, void explodeSpell(const osg::Vec3f& origin, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const MWWorld::Ptr& ignore,
ESM::RangeType rangeType, const std::string& id, const std::string& sourceName, ESM::RangeType rangeType, const std::string& id, const std::string& sourceName,

Loading…
Cancel
Save