You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
801 lines
31 KiB
C++
801 lines
31 KiB
C++
#include "projectilemanager.hpp"
|
|
|
|
#include <iomanip>
|
|
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <osg/PositionAttitudeTransform>
|
|
|
|
#include <components/debug/debuglog.hpp>
|
|
|
|
#include <components/esm/esmwriter.hpp>
|
|
#include <components/esm/projectilestate.hpp>
|
|
|
|
#include <components/misc/constants.hpp>
|
|
#include <components/misc/convert.hpp>
|
|
|
|
#include <components/resource/resourcesystem.hpp>
|
|
#include <components/resource/scenemanager.hpp>
|
|
|
|
#include <components/sceneutil/controller.hpp>
|
|
#include <components/sceneutil/visitor.hpp>
|
|
#include <components/sceneutil/lightmanager.hpp>
|
|
|
|
#include "../mwworld/manualref.hpp"
|
|
#include "../mwworld/class.hpp"
|
|
#include "../mwworld/esmstore.hpp"
|
|
#include "../mwworld/inventorystore.hpp"
|
|
|
|
#include "../mwbase/soundmanager.hpp"
|
|
#include "../mwbase/world.hpp"
|
|
#include "../mwbase/environment.hpp"
|
|
|
|
#include "../mwmechanics/combat.hpp"
|
|
#include "../mwmechanics/creaturestats.hpp"
|
|
#include "../mwmechanics/spellcasting.hpp"
|
|
#include "../mwmechanics/actorutil.hpp"
|
|
#include "../mwmechanics/aipackage.hpp"
|
|
#include "../mwmechanics/weapontype.hpp"
|
|
|
|
#include "../mwrender/animation.hpp"
|
|
#include "../mwrender/vismask.hpp"
|
|
#include "../mwrender/renderingmanager.hpp"
|
|
#include "../mwrender/util.hpp"
|
|
|
|
#include "../mwsound/sound.hpp"
|
|
|
|
#include "../mwphysics/physicssystem.hpp"
|
|
#include "../mwphysics/projectile.hpp"
|
|
|
|
#ifdef USE_OPENXR
|
|
#include "../mwvr/vrenvironment.hpp"
|
|
#include "../mwvr/vranimation.hpp"
|
|
#endif
|
|
|
|
/*
|
|
Start of tes3mp addition
|
|
|
|
Include additional headers for multiplayer purposes
|
|
*/
|
|
#include "../mwmp/MechanicsHelper.hpp"
|
|
/*
|
|
End of tes3mp addition
|
|
*/
|
|
|
|
namespace
|
|
{
|
|
ESM::EffectList getMagicBoltData(std::vector<std::string>& projectileIDs, std::set<std::string>& sounds, float& speed, std::string& texture, std::string& sourceName, const std::string& id)
|
|
{
|
|
const MWWorld::ESMStore& esmStore = MWBase::Environment::get().getWorld()->getStore();
|
|
const ESM::EffectList* effects;
|
|
if (const ESM::Spell* spell = esmStore.get<ESM::Spell>().search(id)) // check if it's a spell
|
|
{
|
|
sourceName = spell->mName;
|
|
effects = &spell->mEffects;
|
|
}
|
|
else // check if it's an enchanted item
|
|
{
|
|
MWWorld::ManualRef ref(esmStore, id);
|
|
MWWorld::Ptr ptr = ref.getPtr();
|
|
const ESM::Enchantment* ench = esmStore.get<ESM::Enchantment>().find(ptr.getClass().getEnchantment(ptr));
|
|
sourceName = ptr.getClass().getName(ptr);
|
|
effects = &ench->mEffects;
|
|
}
|
|
|
|
int count = 0;
|
|
speed = 0.0f;
|
|
ESM::EffectList projectileEffects;
|
|
for (std::vector<ESM::ENAMstruct>::const_iterator iter (effects->mList.begin());
|
|
iter!=effects->mList.end(); ++iter)
|
|
{
|
|
const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find (
|
|
iter->mEffectID);
|
|
|
|
// Speed of multi-effect projectiles should be the average of the constituent effects,
|
|
// based on observation of the original engine.
|
|
speed += magicEffect->mData.mSpeed;
|
|
count++;
|
|
|
|
if (iter->mRange != ESM::RT_Target)
|
|
continue;
|
|
|
|
if (magicEffect->mBolt.empty())
|
|
projectileIDs.emplace_back("VFX_DefaultBolt");
|
|
else
|
|
projectileIDs.push_back(magicEffect->mBolt);
|
|
|
|
static const std::string schools[] = {
|
|
"alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration"
|
|
};
|
|
if (!magicEffect->mBoltSound.empty())
|
|
sounds.emplace(magicEffect->mBoltSound);
|
|
else
|
|
sounds.emplace(schools[magicEffect->mData.mSchool] + " bolt");
|
|
projectileEffects.mList.push_back(*iter);
|
|
}
|
|
|
|
if (count != 0)
|
|
speed /= count;
|
|
|
|
// the particle texture is only used if there is only one projectile
|
|
if (projectileEffects.mList.size() == 1)
|
|
{
|
|
const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find (
|
|
effects->mList.begin()->mEffectID);
|
|
texture = magicEffect->mParticle;
|
|
}
|
|
|
|
if (projectileEffects.mList.size() > 1) // insert a VFX_Multiple projectile if there are multiple projectile effects
|
|
{
|
|
const std::string ID = "VFX_Multiple" + std::to_string(effects->mList.size());
|
|
std::vector<std::string>::iterator it;
|
|
it = projectileIDs.begin();
|
|
it = projectileIDs.insert(it, ID);
|
|
}
|
|
return projectileEffects;
|
|
}
|
|
|
|
osg::Vec4 getMagicBoltLightDiffuseColor(const ESM::EffectList& effects)
|
|
{
|
|
// Calculate combined light diffuse color from magical effects
|
|
osg::Vec4 lightDiffuseColor;
|
|
float lightDiffuseRed = 0.0f;
|
|
float lightDiffuseGreen = 0.0f;
|
|
float lightDiffuseBlue = 0.0f;
|
|
for (std::vector<ESM::ENAMstruct>::const_iterator iter(effects.mList.begin());
|
|
iter != effects.mList.end(); ++iter)
|
|
{
|
|
const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find(
|
|
iter->mEffectID);
|
|
lightDiffuseRed += (static_cast<float>(magicEffect->mData.mRed) / 255.f);
|
|
lightDiffuseGreen += (static_cast<float>(magicEffect->mData.mGreen) / 255.f);
|
|
lightDiffuseBlue += (static_cast<float>(magicEffect->mData.mBlue) / 255.f);
|
|
}
|
|
int numberOfEffects = effects.mList.size();
|
|
lightDiffuseColor = osg::Vec4(lightDiffuseRed / numberOfEffects
|
|
, lightDiffuseGreen / numberOfEffects
|
|
, lightDiffuseBlue / numberOfEffects
|
|
, 1.0f);
|
|
|
|
return lightDiffuseColor;
|
|
}
|
|
}
|
|
|
|
namespace MWWorld
|
|
{
|
|
|
|
ProjectileManager::ProjectileManager(osg::Group* parent, Resource::ResourceSystem* resourceSystem,
|
|
MWRender::RenderingManager* rendering, MWPhysics::PhysicsSystem* physics)
|
|
: mParent(parent)
|
|
, mResourceSystem(resourceSystem)
|
|
, mRendering(rendering)
|
|
, mPhysics(physics)
|
|
, mCleanupTimer(0.0f)
|
|
{
|
|
|
|
}
|
|
|
|
/// Rotates an osg::PositionAttitudeTransform over time.
|
|
class RotateCallback : public osg::NodeCallback
|
|
{
|
|
public:
|
|
RotateCallback(const osg::Vec3f& axis = osg::Vec3f(0,-1,0), float rotateSpeed = osg::PI*2)
|
|
: mAxis(axis)
|
|
, mRotateSpeed(rotateSpeed)
|
|
{
|
|
}
|
|
|
|
void operator()(osg::Node* node, osg::NodeVisitor* nv) override
|
|
{
|
|
osg::PositionAttitudeTransform* transform = static_cast<osg::PositionAttitudeTransform*>(node);
|
|
|
|
double time = nv->getFrameStamp()->getSimulationTime();
|
|
|
|
osg::Quat orient = osg::Quat(time * mRotateSpeed, mAxis);
|
|
transform->setAttitude(orient);
|
|
|
|
traverse(node, nv);
|
|
}
|
|
|
|
private:
|
|
osg::Vec3f mAxis;
|
|
float mRotateSpeed;
|
|
};
|
|
|
|
|
|
void ProjectileManager::createModel(State &state, const std::string &model, const osg::Vec3f& pos, const osg::Quat& orient,
|
|
bool rotate, bool createLight, osg::Vec4 lightDiffuseColor, std::string texture)
|
|
{
|
|
state.mNode = new osg::PositionAttitudeTransform;
|
|
state.mNode->setNodeMask(MWRender::Mask_Effect);
|
|
state.mNode->setPosition(pos);
|
|
state.mNode->setAttitude(orient);
|
|
|
|
osg::Group* attachTo = state.mNode;
|
|
|
|
if (rotate)
|
|
{
|
|
osg::ref_ptr<osg::PositionAttitudeTransform> rotateNode (new osg::PositionAttitudeTransform);
|
|
rotateNode->addUpdateCallback(new RotateCallback());
|
|
state.mNode->addChild(rotateNode);
|
|
attachTo = rotateNode;
|
|
}
|
|
|
|
osg::ref_ptr<osg::Node> projectile = mResourceSystem->getSceneManager()->getInstance(model, attachTo);
|
|
|
|
if (state.mIdMagic.size() > 1)
|
|
for (size_t iter = 1; iter != state.mIdMagic.size(); ++iter)
|
|
{
|
|
std::ostringstream nodeName;
|
|
nodeName << "Dummy" << std::setw(2) << std::setfill('0') << iter;
|
|
const ESM::Weapon* weapon = MWBase::Environment::get().getWorld()->getStore().get<ESM::Weapon>().find (state.mIdMagic.at(iter));
|
|
SceneUtil::FindByNameVisitor findVisitor(nodeName.str());
|
|
attachTo->accept(findVisitor);
|
|
if (findVisitor.mFoundNode)
|
|
mResourceSystem->getSceneManager()->getInstance("meshes\\" + weapon->mModel, findVisitor.mFoundNode);
|
|
}
|
|
|
|
if (createLight)
|
|
{
|
|
osg::ref_ptr<osg::Light> projectileLight(new osg::Light);
|
|
projectileLight->setAmbient(osg::Vec4(1.0f, 1.0f, 1.0f, 1.0f));
|
|
projectileLight->setDiffuse(lightDiffuseColor);
|
|
projectileLight->setSpecular(osg::Vec4(0.0f, 0.0f, 0.0f, 0.0f));
|
|
projectileLight->setConstantAttenuation(0.f);
|
|
projectileLight->setLinearAttenuation(0.1f);
|
|
projectileLight->setQuadraticAttenuation(0.f);
|
|
projectileLight->setPosition(osg::Vec4(pos, 1.0));
|
|
|
|
SceneUtil::LightSource* projectileLightSource = new SceneUtil::LightSource;
|
|
projectileLightSource->setNodeMask(MWRender::Mask_Lighting);
|
|
projectileLightSource->setRadius(66.f);
|
|
|
|
state.mNode->addChild(projectileLightSource);
|
|
projectileLightSource->setLight(projectileLight);
|
|
}
|
|
|
|
SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor;
|
|
state.mNode->accept(disableFreezeOnCullVisitor);
|
|
|
|
state.mNode->addCullCallback(new SceneUtil::LightListCallback);
|
|
|
|
mParent->addChild(state.mNode);
|
|
|
|
state.mEffectAnimationTime.reset(new MWRender::EffectAnimationTime);
|
|
|
|
SceneUtil::AssignControllerSourcesVisitor assignVisitor (state.mEffectAnimationTime);
|
|
state.mNode->accept(assignVisitor);
|
|
|
|
MWRender::overrideFirstRootTexture(texture, mResourceSystem, projectile);
|
|
}
|
|
|
|
void ProjectileManager::update(State& state, float duration)
|
|
{
|
|
state.mEffectAnimationTime->addTime(duration);
|
|
}
|
|
|
|
void ProjectileManager::launchMagicBolt(const std::string &spellId, const Ptr &caster, const osg::Vec3f& fallbackDirection)
|
|
{
|
|
osg::Vec3f pos = caster.getRefData().getPosition().asVec3();
|
|
if (caster.getClass().isActor())
|
|
{
|
|
// Note: we ignore the collision box offset, this is required to make some flying creatures work as intended.
|
|
pos.z() += mPhysics->getRenderingHalfExtents(caster).z() * 2 * Constants::TorsoHeight;
|
|
}
|
|
|
|
if (MWBase::Environment::get().getWorld()->isUnderwater(caster.getCell(), pos)) // Underwater casting not possible
|
|
return;
|
|
|
|
osg::Quat orient;
|
|
#ifdef USE_OPENXR
|
|
if (caster == MWBase::Environment::get().getWorld()->getPlayerPtr())
|
|
{
|
|
osg::Matrix worldMatrix = MWVR::Environment::get().getPlayerAnimation()->getWeaponTransformMatrix();
|
|
orient = worldMatrix.getRotate();
|
|
pos = worldMatrix.getTrans();
|
|
}
|
|
else
|
|
#endif
|
|
if (caster.getClass().isActor())
|
|
orient = osg::Quat(caster.getRefData().getPosition().rot[0], osg::Vec3f(-1,0,0))
|
|
* osg::Quat(caster.getRefData().getPosition().rot[2], osg::Vec3f(0,0,-1));
|
|
else
|
|
orient.makeRotate(osg::Vec3f(0,1,0), osg::Vec3f(fallbackDirection));
|
|
|
|
/*
|
|
Start of tes3mp addition
|
|
|
|
If the actor casting this is a LocalPlayer or LocalActor, track their projectile origin so it can be sent
|
|
in the next PlayerCast or ActorCast packet
|
|
|
|
Otherwise, set the projectileOrigin for a DedicatedPlayer or DedicatedActor
|
|
*/
|
|
mwmp::Cast* localCast = MechanicsHelper::getLocalCast(caster);
|
|
|
|
if (localCast)
|
|
{
|
|
localCast->hasProjectile = true;
|
|
localCast->projectileOrigin.origin[0] = pos.x();
|
|
localCast->projectileOrigin.origin[1] = pos.y();
|
|
localCast->projectileOrigin.origin[2] = pos.z();
|
|
localCast->projectileOrigin.orientation[0] = orient.x();
|
|
localCast->projectileOrigin.orientation[1] = orient.y();
|
|
localCast->projectileOrigin.orientation[2] = orient.z();
|
|
localCast->projectileOrigin.orientation[3] = orient.w();
|
|
}
|
|
else
|
|
{
|
|
mwmp::Cast* dedicatedCast = MechanicsHelper::getDedicatedCast(caster);
|
|
|
|
if (dedicatedCast)
|
|
{
|
|
pos = osg::Vec3f(dedicatedCast->projectileOrigin.origin[0], dedicatedCast->projectileOrigin.origin[1], dedicatedCast->projectileOrigin.origin[2]);
|
|
orient = osg::Quat(dedicatedCast->projectileOrigin.orientation[0], dedicatedCast->projectileOrigin.orientation[1], dedicatedCast->projectileOrigin.orientation[2],
|
|
dedicatedCast->projectileOrigin.orientation[3]);
|
|
}
|
|
}
|
|
/*
|
|
End of tes3mp addition
|
|
*/
|
|
|
|
MagicBoltState state;
|
|
state.mSpellId = spellId;
|
|
state.mCasterHandle = caster;
|
|
if (caster.getClass().isActor())
|
|
state.mActorId = caster.getClass().getCreatureStats(caster).getActorId();
|
|
else
|
|
state.mActorId = -1;
|
|
|
|
std::string texture;
|
|
|
|
state.mEffects = getMagicBoltData(state.mIdMagic, state.mSoundIds, state.mSpeed, texture, state.mSourceName, state.mSpellId);
|
|
|
|
// Non-projectile should have been removed by getMagicBoltData
|
|
if (state.mEffects.mList.empty())
|
|
return;
|
|
|
|
if (!caster.getClass().isActor() && fallbackDirection.length2() <= 0)
|
|
{
|
|
Log(Debug::Warning) << "Unable to launch magic bolt (direction to target is empty)";
|
|
return;
|
|
}
|
|
|
|
MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), state.mIdMagic.at(0));
|
|
MWWorld::Ptr ptr = ref.getPtr();
|
|
|
|
osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(state.mEffects);
|
|
|
|
auto model = ptr.getClass().getModel(ptr);
|
|
createModel(state, model, pos, orient, true, true, lightDiffuseColor, texture);
|
|
|
|
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
|
|
for (const std::string &soundid : state.mSoundIds)
|
|
{
|
|
MWBase::Sound *sound = sndMgr->playSound3D(pos, soundid, 1.0f, 1.0f,
|
|
MWSound::Type::Sfx, MWSound::PlayMode::Loop);
|
|
if (sound)
|
|
state.mSounds.push_back(sound);
|
|
}
|
|
|
|
// in case there are multiple effects, the model is a dummy without geometry. Use the second effect for physics shape
|
|
if (state.mIdMagic.size() > 1)
|
|
model = "meshes\\" + MWBase::Environment::get().getWorld()->getStore().get<ESM::Weapon>().find(state.mIdMagic.at(1))->mModel;
|
|
state.mProjectileId = mPhysics->addProjectile(caster, pos, model, true);
|
|
state.mToDelete = false;
|
|
mMagicBolts.push_back(state);
|
|
}
|
|
|
|
void ProjectileManager::launchProjectile(Ptr actor, ConstPtr projectile, const osg::Vec3f &pos, const osg::Quat &orient, Ptr bow, float speed, float attackStrength)
|
|
{
|
|
ProjectileState state;
|
|
state.mActorId = actor.getClass().getCreatureStats(actor).getActorId();
|
|
state.mBowId = bow.getCellRef().getRefId();
|
|
state.mVelocity = orient * osg::Vec3f(0,1,0) * speed;
|
|
state.mIdArrow = projectile.getCellRef().getRefId();
|
|
state.mCasterHandle = actor;
|
|
state.mAttackStrength = attackStrength;
|
|
int type = projectile.get<ESM::Weapon>()->mBase->mData.mType;
|
|
state.mThrown = MWMechanics::getWeaponType(type)->mWeaponClass == ESM::WeaponType::Thrown;
|
|
|
|
MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), projectile.getCellRef().getRefId());
|
|
MWWorld::Ptr ptr = ref.getPtr();
|
|
|
|
const auto model = ptr.getClass().getModel(ptr);
|
|
createModel(state, model, pos, orient, false, false, osg::Vec4(0,0,0,0));
|
|
if (!ptr.getClass().getEnchantment(ptr).empty())
|
|
SceneUtil::addEnchantedGlow(state.mNode, mResourceSystem, ptr.getClass().getEnchantmentColor(ptr));
|
|
|
|
state.mProjectileId = mPhysics->addProjectile(actor, pos, model, false);
|
|
state.mToDelete = false;
|
|
mProjectiles.push_back(state);
|
|
}
|
|
|
|
void ProjectileManager::updateCasters()
|
|
{
|
|
for (auto& state : mProjectiles)
|
|
mPhysics->setCaster(state.mProjectileId, state.getCaster());
|
|
|
|
for (auto& state : mMagicBolts)
|
|
{
|
|
// casters are identified by actor id in the savegame. objects doesn't have one so they can't be identified back.
|
|
// TODO: should object-type caster be restored from savegame?
|
|
if (state.mActorId == -1)
|
|
continue;
|
|
|
|
auto caster = state.getCaster();
|
|
if (caster.isEmpty())
|
|
{
|
|
Log(Debug::Error) << "Couldn't find caster with ID " << state.mActorId;
|
|
cleanupMagicBolt(state);
|
|
continue;
|
|
}
|
|
mPhysics->setCaster(state.mProjectileId, caster);
|
|
}
|
|
}
|
|
|
|
void ProjectileManager::update(float dt)
|
|
{
|
|
periodicCleanup(dt);
|
|
moveProjectiles(dt);
|
|
moveMagicBolts(dt);
|
|
}
|
|
|
|
void ProjectileManager::periodicCleanup(float dt)
|
|
{
|
|
mCleanupTimer -= dt;
|
|
if (mCleanupTimer <= 0.0f)
|
|
{
|
|
mCleanupTimer = 2.0f;
|
|
|
|
auto isCleanable = [](const ProjectileManager::State& state) -> bool
|
|
{
|
|
const float farawayThreshold = 72000.0f;
|
|
osg::Vec3 playerPos = MWMechanics::getPlayer().getRefData().getPosition().asVec3();
|
|
return (state.mNode->getPosition() - playerPos).length2() >= farawayThreshold*farawayThreshold;
|
|
};
|
|
|
|
for (auto& projectileState : mProjectiles)
|
|
{
|
|
if (isCleanable(projectileState))
|
|
cleanupProjectile(projectileState);
|
|
}
|
|
|
|
for (auto& magicBoltState : mMagicBolts)
|
|
{
|
|
if (isCleanable(magicBoltState))
|
|
cleanupMagicBolt(magicBoltState);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ProjectileManager::moveMagicBolts(float duration)
|
|
{
|
|
for (auto& magicBoltState : mMagicBolts)
|
|
{
|
|
if (magicBoltState.mToDelete)
|
|
continue;
|
|
|
|
auto* projectile = mPhysics->getProjectile(magicBoltState.mProjectileId);
|
|
if (!projectile->isActive())
|
|
continue;
|
|
// If the actor caster is gone, the magic bolt needs to be removed from the scene during the next frame.
|
|
MWWorld::Ptr caster = magicBoltState.getCaster();
|
|
if (!caster.isEmpty() && caster.getClass().isActor())
|
|
{
|
|
if (caster.getRefData().getCount() <= 0 || caster.getClass().getCreatureStats(caster).isDead())
|
|
{
|
|
cleanupMagicBolt(magicBoltState);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
osg::Quat orient = magicBoltState.mNode->getAttitude();
|
|
static float fTargetSpellMaxSpeed = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>()
|
|
.find("fTargetSpellMaxSpeed")->mValue.getFloat();
|
|
float speed = fTargetSpellMaxSpeed * magicBoltState.mSpeed;
|
|
osg::Vec3f direction = orient * osg::Vec3f(0,1,0);
|
|
direction.normalize();
|
|
osg::Vec3f newPos = projectile->getPosition() + direction * duration * speed;
|
|
|
|
update(magicBoltState, duration);
|
|
|
|
// For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result.
|
|
std::vector<MWWorld::Ptr> targetActors;
|
|
if (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer())
|
|
caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors);
|
|
projectile->setValidTargets(targetActors);
|
|
|
|
mPhysics->updateProjectile(magicBoltState.mProjectileId, newPos);
|
|
}
|
|
}
|
|
|
|
void ProjectileManager::moveProjectiles(float duration)
|
|
{
|
|
for (auto& projectileState : mProjectiles)
|
|
{
|
|
if (projectileState.mToDelete)
|
|
continue;
|
|
|
|
auto* projectile = mPhysics->getProjectile(projectileState.mProjectileId);
|
|
if (!projectile->isActive())
|
|
continue;
|
|
// gravity constant - must be way lower than the gravity affecting actors, since we're not
|
|
// simulating aerodynamics at all
|
|
projectileState.mVelocity -= osg::Vec3f(0, 0, Constants::GravityConst * Constants::UnitsPerMeter * 0.1f) * duration;
|
|
|
|
osg::Vec3f newPos = projectile->getPosition() + projectileState.mVelocity * duration;
|
|
|
|
// rotation does not work well for throwing projectiles - their roll angle will depend on shooting direction.
|
|
if (!projectileState.mThrown)
|
|
{
|
|
osg::Quat orient;
|
|
orient.makeRotate(osg::Vec3f(0,1,0), projectileState.mVelocity);
|
|
projectileState.mNode->setAttitude(orient);
|
|
}
|
|
|
|
update(projectileState, duration);
|
|
|
|
MWWorld::Ptr caster = projectileState.getCaster();
|
|
|
|
// For AI actors, get combat targets to use in the ray cast. Only those targets will return a positive hit result.
|
|
std::vector<MWWorld::Ptr> targetActors;
|
|
if (!caster.isEmpty() && caster.getClass().isActor() && caster != MWMechanics::getPlayer())
|
|
caster.getClass().getCreatureStats(caster).getAiSequence().getCombatTargets(targetActors);
|
|
projectile->setValidTargets(targetActors);
|
|
|
|
mPhysics->updateProjectile(projectileState.mProjectileId, newPos);
|
|
}
|
|
}
|
|
|
|
void ProjectileManager::processHits()
|
|
{
|
|
for (auto& projectileState : mProjectiles)
|
|
{
|
|
if (projectileState.mToDelete)
|
|
continue;
|
|
|
|
auto* projectile = mPhysics->getProjectile(projectileState.mProjectileId);
|
|
|
|
const auto pos = projectile->getPosition();
|
|
projectileState.mNode->setPosition(pos);
|
|
|
|
if (projectile->isActive())
|
|
continue;
|
|
|
|
const auto target = projectile->getTarget();
|
|
auto caster = projectileState.getCaster();
|
|
assert(target != caster);
|
|
|
|
if (caster.isEmpty())
|
|
caster = target;
|
|
|
|
// Try to get a Ptr to the bow that was used. It might no longer exist.
|
|
MWWorld::ManualRef projectileRef(MWBase::Environment::get().getWorld()->getStore(), projectileState.mIdArrow);
|
|
MWWorld::Ptr bow = projectileRef.getPtr();
|
|
if (!caster.isEmpty() && projectileState.mIdArrow != projectileState.mBowId)
|
|
{
|
|
MWWorld::InventoryStore& inv = caster.getClass().getInventoryStore(caster);
|
|
MWWorld::ContainerStoreIterator invIt = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedRight);
|
|
if (invIt != inv.end() && Misc::StringUtils::ciEqual(invIt->getCellRef().getRefId(), projectileState.mBowId))
|
|
bow = *invIt;
|
|
}
|
|
if (projectile->getHitWater())
|
|
mRendering->emitWaterRipple(pos);
|
|
|
|
MWMechanics::projectileHit(caster, target, bow, projectileRef.getPtr(), pos, projectileState.mAttackStrength);
|
|
cleanupProjectile(projectileState);
|
|
}
|
|
for (auto& magicBoltState : mMagicBolts)
|
|
{
|
|
if (magicBoltState.mToDelete)
|
|
continue;
|
|
|
|
auto* projectile = mPhysics->getProjectile(magicBoltState.mProjectileId);
|
|
|
|
const auto pos = projectile->getPosition();
|
|
magicBoltState.mNode->setPosition(pos);
|
|
for (const auto& sound : magicBoltState.mSounds)
|
|
sound->setPosition(pos);
|
|
|
|
if (projectile->isActive())
|
|
continue;
|
|
|
|
const auto target = projectile->getTarget();
|
|
const auto caster = magicBoltState.getCaster();
|
|
assert(target != caster);
|
|
|
|
MWMechanics::CastSpell cast(caster, target);
|
|
cast.mHitPosition = pos;
|
|
cast.mId = magicBoltState.mSpellId;
|
|
cast.mSourceName = magicBoltState.mSourceName;
|
|
cast.mStack = false;
|
|
cast.inflict(target, caster, magicBoltState.mEffects, ESM::RT_Target, false, true);
|
|
|
|
MWBase::Environment::get().getWorld()->explodeSpell(pos, magicBoltState.mEffects, caster, target, ESM::RT_Target, magicBoltState.mSpellId, magicBoltState.mSourceName);
|
|
cleanupMagicBolt(magicBoltState);
|
|
}
|
|
mProjectiles.erase(std::remove_if(mProjectiles.begin(), mProjectiles.end(), [](const State& state) { return state.mToDelete; }),
|
|
mProjectiles.end());
|
|
mMagicBolts.erase(std::remove_if(mMagicBolts.begin(), mMagicBolts.end(), [](const State& state) { return state.mToDelete; }),
|
|
mMagicBolts.end());
|
|
}
|
|
|
|
void ProjectileManager::cleanupProjectile(ProjectileManager::ProjectileState& state)
|
|
{
|
|
mParent->removeChild(state.mNode);
|
|
mPhysics->removeProjectile(state.mProjectileId);
|
|
state.mToDelete = true;
|
|
}
|
|
|
|
void ProjectileManager::cleanupMagicBolt(ProjectileManager::MagicBoltState& state)
|
|
{
|
|
mParent->removeChild(state.mNode);
|
|
mPhysics->removeProjectile(state.mProjectileId);
|
|
state.mToDelete = true;
|
|
for (size_t soundIter = 0; soundIter != state.mSounds.size(); soundIter++)
|
|
{
|
|
MWBase::Environment::get().getSoundManager()->stopSound(state.mSounds.at(soundIter));
|
|
}
|
|
}
|
|
|
|
void ProjectileManager::clear()
|
|
{
|
|
for (auto& mProjectile : mProjectiles)
|
|
cleanupProjectile(mProjectile);
|
|
mProjectiles.clear();
|
|
|
|
for (auto& mMagicBolt : mMagicBolts)
|
|
cleanupMagicBolt(mMagicBolt);
|
|
mMagicBolts.clear();
|
|
}
|
|
|
|
void ProjectileManager::write(ESM::ESMWriter &writer, Loading::Listener &progress) const
|
|
{
|
|
for (std::vector<ProjectileState>::const_iterator it = mProjectiles.begin(); it != mProjectiles.end(); ++it)
|
|
{
|
|
writer.startRecord(ESM::REC_PROJ);
|
|
|
|
ESM::ProjectileState state;
|
|
state.mId = it->mIdArrow;
|
|
state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition()));
|
|
state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude()));
|
|
state.mActorId = it->mActorId;
|
|
|
|
state.mBowId = it->mBowId;
|
|
state.mVelocity = it->mVelocity;
|
|
state.mAttackStrength = it->mAttackStrength;
|
|
|
|
state.save(writer);
|
|
|
|
writer.endRecord(ESM::REC_PROJ);
|
|
}
|
|
|
|
for (std::vector<MagicBoltState>::const_iterator it = mMagicBolts.begin(); it != mMagicBolts.end(); ++it)
|
|
{
|
|
writer.startRecord(ESM::REC_MPRJ);
|
|
|
|
ESM::MagicBoltState state;
|
|
state.mId = it->mIdMagic.at(0);
|
|
state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition()));
|
|
state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude()));
|
|
state.mActorId = it->mActorId;
|
|
|
|
state.mSpellId = it->mSpellId;
|
|
state.mSpeed = it->mSpeed;
|
|
|
|
state.save(writer);
|
|
|
|
writer.endRecord(ESM::REC_MPRJ);
|
|
}
|
|
}
|
|
|
|
bool ProjectileManager::readRecord(ESM::ESMReader &reader, uint32_t type)
|
|
{
|
|
if (type == ESM::REC_PROJ)
|
|
{
|
|
ESM::ProjectileState esm;
|
|
esm.load(reader);
|
|
|
|
ProjectileState state;
|
|
state.mActorId = esm.mActorId;
|
|
state.mBowId = esm.mBowId;
|
|
state.mVelocity = esm.mVelocity;
|
|
state.mIdArrow = esm.mId;
|
|
state.mAttackStrength = esm.mAttackStrength;
|
|
state.mToDelete = false;
|
|
|
|
std::string model;
|
|
try
|
|
{
|
|
MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), esm.mId);
|
|
MWWorld::Ptr ptr = ref.getPtr();
|
|
model = ptr.getClass().getModel(ptr);
|
|
int weaponType = ptr.get<ESM::Weapon>()->mBase->mData.mType;
|
|
state.mThrown = MWMechanics::getWeaponType(weaponType)->mWeaponClass == ESM::WeaponType::Thrown;
|
|
|
|
state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, false);
|
|
}
|
|
catch(...)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), false, false, osg::Vec4(0,0,0,0));
|
|
|
|
mProjectiles.push_back(state);
|
|
return true;
|
|
}
|
|
if (type == ESM::REC_MPRJ)
|
|
{
|
|
ESM::MagicBoltState esm;
|
|
esm.load(reader);
|
|
|
|
MagicBoltState state;
|
|
state.mIdMagic.push_back(esm.mId);
|
|
state.mSpellId = esm.mSpellId;
|
|
state.mActorId = esm.mActorId;
|
|
state.mToDelete = false;
|
|
std::string texture;
|
|
|
|
try
|
|
{
|
|
state.mEffects = getMagicBoltData(state.mIdMagic, state.mSoundIds, state.mSpeed, texture, state.mSourceName, state.mSpellId);
|
|
}
|
|
catch(...)
|
|
{
|
|
Log(Debug::Warning) << "Warning: Failed to recreate magic projectile from saved data (id \"" << state.mSpellId << "\" no longer exists?)";
|
|
return true;
|
|
}
|
|
|
|
state.mSpeed = esm.mSpeed; // speed is derived from non-projectile effects as well as
|
|
// projectile effects, so we can't calculate it from the save
|
|
// file's effect list, which is already trimmed of non-projectile
|
|
// effects. We need to use the stored value.
|
|
|
|
std::string model;
|
|
try
|
|
{
|
|
MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), state.mIdMagic.at(0));
|
|
MWWorld::Ptr ptr = ref.getPtr();
|
|
model = ptr.getClass().getModel(ptr);
|
|
}
|
|
catch(...)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
osg::Vec4 lightDiffuseColor = getMagicBoltLightDiffuseColor(state.mEffects);
|
|
createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), true, true, lightDiffuseColor, texture);
|
|
state.mProjectileId = mPhysics->addProjectile(state.getCaster(), osg::Vec3f(esm.mPosition), model, true);
|
|
|
|
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
|
|
for (const std::string &soundid : state.mSoundIds)
|
|
{
|
|
MWBase::Sound *sound = sndMgr->playSound3D(esm.mPosition, soundid, 1.0f, 1.0f,
|
|
MWSound::Type::Sfx, MWSound::PlayMode::Loop);
|
|
if (sound)
|
|
state.mSounds.push_back(sound);
|
|
}
|
|
|
|
mMagicBolts.push_back(state);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
int ProjectileManager::countSavedGameRecords() const
|
|
{
|
|
return mMagicBolts.size() + mProjectiles.size();
|
|
}
|
|
|
|
MWWorld::Ptr ProjectileManager::State::getCaster()
|
|
{
|
|
if (!mCasterHandle.isEmpty())
|
|
return mCasterHandle;
|
|
|
|
return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mActorId);
|
|
}
|
|
|
|
}
|