Merge remote-tracking branch 'refs/remotes/upstream/master'

pull/541/head
AnyOldName3 6 years ago
commit 25e92481f4

@ -173,6 +173,7 @@ Programmers
vocollapse
Yohaulticetl
zelurker
James Carty (MrTopCat)
Documentation
-------------
@ -181,9 +182,11 @@ Documentation
Alejandro Sanchez (HiPhish)
Bodillium
Bret Curtis (psi29a)
David Walley (Loriel)
Cramal
Ryan Tucker (Ravenwing)
sir_herrbatka
Diego Crespo
Packagers
---------

@ -39,6 +39,7 @@
Bug #4230: AiTravel package issues break some Tribunal quests
Bug #4231: Infected rats from the "Crimson Plague" quest rendered unconscious by change in Drain Fatigue functionality
Bug #4251: Stationary NPCs do not return to their position after combat
Bug #4271: Scamp flickers when attacking
Bug #4274: Pre-0.43 death animations are not forward-compatible with 0.43+
Bug #4286: Scripted animations can be interrupted
Bug #4291: Non-persistent actors that started the game as dead do not play death animations
@ -83,6 +84,7 @@
Bug #4503: Cast and ExplodeSpell commands increase alteration skill
Bug #4510: Division by zero in MWMechanics::CreatureStats::setAttribute
Bug #4519: Knockdown does not discard movement in the 1st-person mode
Bug #4531: Movement does not reset idle animations
Bug #4539: Paper Doll is affected by GUI scaling
Bug #4545: Creatures flee from werewolves
Bug #4551: Replace 0 sound range with default range separately
@ -95,6 +97,8 @@
Bug #4574: Player turning animations are twitchy
Bug #4575: Weird result of attack animation blending with movement animations
Bug #4576: Reset of idle animations when attack can not be started
Bug #4591: Attack strength should be 0 if player did not hold the attack button
Feature #1645: Casting effects from objects
Feature #2606: Editor: Implemented (optional) case sensitive global search
Feature #3083: Play animation when NPC is casting spell via script
Feature #3103: Provide option for disposition to get increased by successful trade

@ -257,7 +257,7 @@ namespace MWBase
virtual void cleanupSummonedCreature(const MWWorld::Ptr& caster, int creatureActorId) = 0;
virtual void confiscateStolenItemToOwner(const MWWorld::Ptr &player, const MWWorld::Ptr &item, const MWWorld::Ptr& victim, int count) = 0;
virtual bool isAttackPrepairing(const MWWorld::Ptr& ptr) = 0;
virtual bool isAttackPreparing(const MWWorld::Ptr& ptr) = 0;
virtual bool isRunning(const MWWorld::Ptr& ptr) = 0;
virtual bool isSneaking(const MWWorld::Ptr& ptr) = 0;
};

@ -536,7 +536,7 @@ namespace MWBase
/// Spawn a blood effect for \a ptr at \a worldPosition
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,
const MWWorld::Ptr& ignore, ESM::RangeType rangeType, const std::string& id,

@ -1004,9 +1004,9 @@ namespace MWInput
if (!mControlSwitch["playerfighting"] || !mControlSwitch["playercontrols"])
return;
// We want to interrupt animation only if attack is prepairing, but still is not triggered
// We want to interrupt animation only if attack is preparing, but still is not triggered
// Otherwise we will get a "speedshooting" exploit, when player can skip reload animation by hitting "Toggle Weapon" key twice
if (MWBase::Environment::get().getMechanicsManager()->isAttackPrepairing(mPlayer->getPlayer()))
if (MWBase::Environment::get().getMechanicsManager()->isAttackPreparing(mPlayer->getPlayer()))
mPlayer->setAttackingOrSpell(false);
else if (MWBase::Environment::get().getMechanicsManager()->isAttackingOrSpell(mPlayer->getPlayer()))
return;

@ -896,14 +896,14 @@ namespace MWMechanics
}
}
bool Actors::isAttackPrepairing(const MWWorld::Ptr& ptr)
bool Actors::isAttackPreparing(const MWWorld::Ptr& ptr)
{
PtrActorMap::iterator it = mActors.find(ptr);
if (it == mActors.end())
return false;
CharacterController* ctrl = it->second->getCharacterController();
return ctrl->isAttackPrepairing();
return ctrl->isAttackPreparing();
}
bool Actors::isRunning(const MWWorld::Ptr& ptr)

@ -118,7 +118,7 @@ namespace MWMechanics
int countDeaths (const std::string& id) const;
///< Return the number of deaths for actors with the given ID.
bool isAttackPrepairing(const MWWorld::Ptr& ptr);
bool isAttackPreparing(const MWWorld::Ptr& ptr);
bool isRunning(const MWWorld::Ptr& ptr);
bool isSneaking(const MWWorld::Ptr& ptr);

@ -42,7 +42,19 @@ bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::Charac
return false;
}
osg::Vec3f dir = target.getRefData().getPosition().asVec3() - actor.getRefData().getPosition().asVec3();
osg::Vec3f targetPos = target.getRefData().getPosition().asVec3();
if (target.getClass().isActor())
{
osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(target);
targetPos.z() += halfExtents.z() * 2 * 0.75f;
}
osg::Vec3f actorPos = actor.getRefData().getPosition().asVec3();
osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor);
actorPos.z() += halfExtents.z() * 2 * 0.75f;
osg::Vec3f dir = targetPos - actorPos;
bool turned = smoothTurn(actor, getZAngleToDir(dir), 2, osg::DegreesToRadians(3.f));
turned &= smoothTurn(actor, getXAngleToDir(dir), 0, osg::DegreesToRadians(3.f));

@ -372,21 +372,32 @@ namespace MWMechanics
actorMovementSettings.mPosition[1] = storage.mMovement.mPosition[1];
actorMovementSettings.mPosition[2] = storage.mMovement.mPosition[2];
rotateActorOnAxis(actor, 2, actorMovementSettings, storage.mMovement);
rotateActorOnAxis(actor, 0, actorMovementSettings, storage.mMovement);
rotateActorOnAxis(actor, 2, actorMovementSettings, storage);
rotateActorOnAxis(actor, 0, actorMovementSettings, storage);
}
void AiCombat::rotateActorOnAxis(const MWWorld::Ptr& actor, int axis,
MWMechanics::Movement& actorMovementSettings, MWMechanics::Movement& desiredMovement)
MWMechanics::Movement& actorMovementSettings, AiCombatStorage& storage)
{
actorMovementSettings.mRotation[axis] = 0;
float& targetAngleRadians = desiredMovement.mRotation[axis];
float& targetAngleRadians = storage.mMovement.mRotation[axis];
if (targetAngleRadians != 0)
{
if (smoothTurn(actor, targetAngleRadians, axis))
// Some attack animations contain small amount of movement.
// Since we use cone shapes for melee, we can use a threshold to avoid jittering
std::shared_ptr<Action>& currentAction = storage.mCurrentAction;
bool isRangedCombat = false;
currentAction->getCombatRange(isRangedCombat);
// Check if the actor now facing desired direction, no need to turn any more
if (isRangedCombat)
{
if (smoothTurn(actor, targetAngleRadians, axis))
targetAngleRadians = 0;
}
else
{
// actor now facing desired direction, no need to turn any more
targetAngleRadians = 0;
if (smoothTurn(actor, targetAngleRadians, axis, osg::DegreesToRadians(3.f)))
targetAngleRadians = 0;
}
}
}
@ -453,7 +464,7 @@ namespace MWMechanics
if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability() < 0.25)
{
mMovement.mPosition[0] = Misc::Rng::rollProbability() < 0.5 ? 1.0f : -1.0f; // to the left/right
mTimerCombatMove = 0.05f + 0.15f * Misc::Rng::rollClosedProbability();
mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability();
mCombatMove = true;
}
}

@ -129,7 +129,7 @@ namespace MWMechanics
/// Transfer desired movement (from AiCombatStorage) to Actor
void updateActorsMovement(const MWWorld::Ptr& actor, float duration, AiCombatStorage& storage);
void rotateActorOnAxis(const MWWorld::Ptr& actor, int axis,
MWMechanics::Movement& actorMovementSettings, MWMechanics::Movement& desiredMovement);
MWMechanics::Movement& actorMovementSettings, AiCombatStorage& storage);
};

@ -19,6 +19,7 @@
#include "aicombataction.hpp"
#include "aipursue.hpp"
#include "actorutil.hpp"
#include "../mwworld/class.hpp"
namespace MWMechanics
{
@ -122,6 +123,20 @@ bool AiSequence::isInCombat() const
return false;
}
bool AiSequence::isEngagedWithActor() const
{
for (std::list<AiPackage *>::const_iterator it = mPackages.begin(); it != mPackages.end(); ++it)
{
if ((*it)->getTypeId() == AiPackage::TypeIdCombat)
{
MWWorld::Ptr target2 = (*it)->getTarget();
if (!target2.isEmpty() && target2.getClass().isNpc())
return true;
}
}
return false;
}
bool AiSequence::hasPackage(int typeId) const
{
for (std::list<AiPackage*>::const_iterator it = mPackages.begin(); it != mPackages.end(); ++it)

@ -88,6 +88,9 @@ namespace MWMechanics
/// Is there any combat package?
bool isInCombat () const;
/// Are we in combat with any other actor, who's also engaging us?
bool isEngagedWithActor () const;
/// Does this AI sequence have the given package type?
bool hasPackage(int typeId) const;

@ -411,10 +411,6 @@ void CharacterController::refreshMovementAnims(const WeaponInfo* weap, Character
if(force || movement != mMovementState)
{
mMovementState = movement;
if (movement != CharState_None)
mIdleState = CharState_None;
std::string movementAnimName;
MWRender::Animation::BlendMask movemask = MWRender::Animation::BlendMask_All;
const StateInfo *movestate = std::find_if(sMovementList, sMovementListEnd, FindCharState(mMovementState));
@ -531,7 +527,7 @@ void CharacterController::refreshMovementAnims(const WeaponInfo* weap, Character
void CharacterController::refreshIdleAnims(const WeaponInfo* weap, CharacterState idle, bool force)
{
if(force || idle != mIdleState || (!mAnimation->isPlaying(mCurrentIdle) && mAnimQueue.empty()))
if(force || idle != mIdleState || mIdleState == CharState_None || (!mAnimation->isPlaying(mCurrentIdle) && mAnimQueue.empty()))
{
mIdleState = idle;
size_t numLoops = ~0ul;
@ -562,14 +558,24 @@ void CharacterController::refreshIdleAnims(const WeaponInfo* weap, CharacterStat
// 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
numLoops = 1 + Misc::Rng::rollDice(4);
}
}
}
mAnimation->disable(mCurrentIdle);
// 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)
{
mAnimation->getInfo(mCurrentIdle, &startPoint);
}
if(!mCurrentIdle.empty())
mAnimation->disable(mCurrentIdle);
mCurrentIdle = idleGroup;
if(!mCurrentIdle.empty())
mAnimation->play(mCurrentIdle, idlePriority, MWRender::Animation::BlendMask_All, false,
1.0f, "start", "stop", 0.0f, numLoops, true);
1.0f, "start", "stop", startPoint, numLoops, true);
}
}
@ -1385,14 +1391,6 @@ bool CharacterController::updateWeaponState()
MWBase::Environment::get().getWorld()->breakInvisibility(mPtr);
mAttackStrength = 0;
// Randomize attacks for non-bipedal creatures with Weapon flag
if (mPtr.getClass().getTypeName() == typeid(ESM::Creature).name() &&
!mPtr.getClass().isBipedal(mPtr) &&
(!mAnimation->hasAnimation(mCurrentWeapon) || isRandomAttackAnimation(mCurrentWeapon)))
{
mCurrentWeapon = chooseRandomAttackAnimation();
}
if(mWeaponType == WeapType_Spell)
{
// Unset casting flag, otherwise pressing the mouse button down would
@ -1401,15 +1399,10 @@ bool CharacterController::updateWeaponState()
if (mPtr == player)
{
MWBase::Environment::get().getWorld()->getPlayer().setAttackingOrSpell(false);
}
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
// For the player, set the spell we want to cast
// This has to be done at the start of the casting animation,
// *not* when selecting a spell in the GUI (otherwise you could change the spell mid-animation)
if (mPtr == player)
{
// For the player, set the spell we want to cast
// This has to be done at the start of the casting animation,
// *not* when selecting a spell in the GUI (otherwise you could change the spell mid-animation)
std::string selectedSpell = MWBase::Environment::get().getWindowManager()->getSelectedSpell();
stats.getSpells().setSelectedSpell(selectedSpell);
}
@ -1421,6 +1414,7 @@ bool CharacterController::updateWeaponState()
MWMechanics::CastSpell cast(mPtr, NULL, false, mCastingManualSpell);
cast.playSpellCastingEffects(spellid);
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
const ESM::Spell *spell = store.get<ESM::Spell>().find(spellid);
const ESM::ENAMstruct &lastEffect = spell->mEffects.mList.back();
const ESM::MagicEffect *effect;
@ -1520,13 +1514,12 @@ bool CharacterController::updateWeaponState()
startKey = mAttackType+" start";
stopKey = mAttackType+" min attack";
}
if (isRandomAttackAnimation(mCurrentWeapon))
else if (isRandomAttackAnimation(mCurrentWeapon))
{
startKey = "start";
stopKey = "stop";
}
else if (mAttackType != "shoot")
else
{
if(mPtr == getPlayer())
{
@ -1683,11 +1676,6 @@ bool CharacterController::updateWeaponState()
std::string start, stop;
switch(mUpperBodyState)
{
case UpperCharState_StartToMinAttack:
start = mAttackType+" min attack";
stop = mAttackType+" max attack";
mUpperBodyState = UpperCharState_MinAttackToMaxAttack;
break;
case UpperCharState_MinAttackToMaxAttack:
//hack to avoid body pos desync when jumping/sneaking in 'max attack' state
if(!mAnimation->isPlaying(mCurrentWeapon))
@ -1695,6 +1683,23 @@ bool CharacterController::updateWeaponState()
MWRender::Animation::BlendMask_All, false,
0, mAttackType+" min attack", mAttackType+" max attack", 0.999f, 0);
break;
case UpperCharState_StartToMinAttack:
{
// If actor is already stopped preparing attack, do not play the "min attack -> max attack" part.
// Happens if the player did not hold the attack button.
// Note: if the "min attack"->"max attack" is a stub, "play" it anyway. Attack strength will be 1.
float minAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"min attack");
float maxAttackTime = mAnimation->getTextKeyTime(mCurrentWeapon+": "+mAttackType+" "+"max attack");
if (mAttackingOrSpell || minAttackTime == maxAttackTime)
{
start = mAttackType+" min attack";
stop = mAttackType+" max attack";
mUpperBodyState = UpperCharState_MinAttackToMaxAttack;
break;
}
playSwishSound(0.0f);
}
// Fall-through
case UpperCharState_MaxAttackToMinHit:
if(mAttackType == "shoot")
{
@ -2094,7 +2099,16 @@ void CharacterController::update(float duration)
if(mAnimQueue.empty() || inwater || sneak)
{
idlestate = (inwater ? CharState_IdleSwim : (sneak && !inJump ? CharState_IdleSneak : CharState_Idle));
// Note: turning animations should not interrupt idle ones.
// Also movement should not stop idle animation for spellcasting stance.
if (inwater)
idlestate = CharState_IdleSwim;
else if (sneak && !inJump)
idlestate = CharState_IdleSneak;
else if (movestate != CharState_None && !isTurning() && mWeaponType != WeapType_Spell)
idlestate = CharState_None;
else
idlestate = CharState_Idle;
}
else
updateAnimQueue();
@ -2493,7 +2507,7 @@ bool CharacterController::isRandomAttackAnimation(const std::string& group) cons
group == "attack3" || group == "swimattack3");
}
bool CharacterController::isAttackPrepairing() const
bool CharacterController::isAttackPreparing() const
{
return mUpperBodyState == UpperCharState_StartToMinAttack ||
mUpperBodyState == UpperCharState_MinAttackToMaxAttack;

@ -281,7 +281,7 @@ public:
void forceStateUpdate();
bool isAttackPrepairing() const;
bool isAttackPreparing() const;
bool isCastingSpell() const;
bool isReadyToBlock() const;
bool isKnockedDown() const;

@ -436,9 +436,9 @@ namespace MWMechanics
return mActors.isActorDetected(actor, observer);
}
bool MechanicsManager::isAttackPrepairing(const MWWorld::Ptr& ptr)
bool MechanicsManager::isAttackPreparing(const MWWorld::Ptr& ptr)
{
return mActors.isAttackPrepairing(ptr);
return mActors.isAttackPreparing(ptr);
}
bool MechanicsManager::isRunning(const MWWorld::Ptr& ptr)
@ -1462,24 +1462,11 @@ namespace MWMechanics
}
}
// Attacking an NPC that is already in combat with any other NPC is not a crime
AiSequence& seq = statsTarget.getAiSequence();
bool isFightingNpc = false;
for (std::list<AiPackage*>::const_iterator it = seq.begin(); it != seq.end(); ++it)
{
if ((*it)->getTypeId() == AiPackage::TypeIdCombat)
{
MWWorld::Ptr target2 = (*it)->getTarget();
if (!target2.isEmpty() && target2.getClass().isNpc())
isFightingNpc = true;
}
}
if (target.getClass().isNpc() && !attacker.isEmpty() && !seq.isInCombat(attacker)
&& !isAggressive(target, attacker) && !isFightingNpc
&& !target.getClass().getCreatureStats(target).getAiSequence().hasPackage(AiPackage::TypeIdPursue))
if (canCommitCrimeAgainst(target, attacker))
commitCrime(attacker, target, MWBase::MechanicsManager::OT_Assault);
AiSequence& seq = statsTarget.getAiSequence();
if (!attacker.isEmpty() && (attacker.getClass().getCreatureStats(attacker).getAiSequence().isInCombat(target)
|| attacker == getPlayer())
&& !seq.isInCombat(attacker))
@ -1506,6 +1493,14 @@ namespace MWMechanics
return true;
}
bool MechanicsManager::canCommitCrimeAgainst(const MWWorld::Ptr &target, const MWWorld::Ptr &attacker)
{
MWMechanics::AiSequence seq = target.getClass().getCreatureStats(target).getAiSequence();
return target.getClass().isNpc() && !attacker.isEmpty() && !seq.isInCombat(attacker)
&& !isAggressive(target, attacker) && !seq.isEngagedWithActor()
&& !target.getClass().getCreatureStats(target).getAiSequence().hasPackage(AiPackage::TypeIdPursue);
}
void MechanicsManager::actorKilled(const MWWorld::Ptr &victim, const MWWorld::Ptr &attacker)
{
if (attacker.isEmpty() || victim.isEmpty())
@ -1518,11 +1513,10 @@ namespace MWMechanics
return; // TODO: implement animal rights
const MWMechanics::NpcStats& victimStats = victim.getClass().getNpcStats(victim);
if (victimStats.getCrimeId() == -1)
return;
const MWWorld::Ptr &player = getPlayer();
bool canCommit = attacker == player && canCommitCrimeAgainst(victim, attacker);
// For now we report only about crimes of player and player's followers
const MWWorld::Ptr &player = getPlayer();
if (attacker != player)
{
std::set<MWWorld::Ptr> playerFollowers;
@ -1531,6 +1525,9 @@ namespace MWMechanics
return;
}
if (!canCommit && victimStats.getCrimeId() == -1)
return;
// Simple check for who attacked first: if the player attacked first, a crimeId should be set
// Doesn't handle possible edge case where no one reported the assault, but in such a case,
// for bystanders it is not possible to tell who attacked first, anyway.

@ -132,6 +132,12 @@ namespace MWMechanics
/// @note No-op for non-player attackers
virtual void actorKilled (const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker);
/// Checks if commiting a crime is currently valid
/// @param victim The actor being attacked
/// @param attacker The actor commiting the crime
/// @return true if the victim is a valid target for crime
virtual bool canCommitCrimeAgainst(const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker);
/// Utility to check if taking this item is illegal and calling commitCrime if so
/// @param container The container the item is in; may be empty for an item in the world
virtual void itemTaken (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, const MWWorld::Ptr& container,
@ -223,7 +229,7 @@ namespace MWMechanics
virtual void confiscateStolenItemToOwner(const MWWorld::Ptr &player, const MWWorld::Ptr &item, const MWWorld::Ptr& victim, int count);
virtual bool isAttackPrepairing(const MWWorld::Ptr& ptr);
virtual bool isAttackPreparing(const MWWorld::Ptr& ptr);
virtual bool isRunning(const MWWorld::Ptr& ptr);
virtual bool isSneaking(const MWWorld::Ptr& ptr);

@ -24,6 +24,7 @@
#include "../mwworld/inventorystore.hpp"
#include "../mwrender/animation.hpp"
#include "../mwrender/vismask.hpp"
#include "npcstats.hpp"
#include "actorutil.hpp"
@ -327,16 +328,19 @@ namespace MWMechanics
}
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
// (e.g. when cast by a non-actor)
if (!mTarget.isEmpty())
fallbackDirection =
osg::Vec3f(mTarget.getRefData().getPosition().asVec3())-
osg::Vec3f(mCaster.getRefData().getPosition().asVec3());
(mTarget.getRefData().getPosition().asVec3() + offset) -
(mCaster.getRefData().getPosition().asVec3());
MWBase::Environment::get().getWorld()->launchMagicBolt(mId, mCaster, fallbackDirection);
}
@ -999,11 +1003,13 @@ namespace MWMechanics
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 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();
iter != spell->mEffects.mList.end(); ++iter)
{
@ -1012,18 +1018,56 @@ namespace MWMechanics
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())
castStatic = store.get<ESM::Static>().find (effect->mCasting);
else
castStatic = store.get<ESM::Static>().find ("VFX_DefaultCast");
const ESM::Race *race =
esmStore.get<ESM::Race>().find(npc->mRace);
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())
@ -1033,6 +1077,8 @@ namespace MWMechanics
"alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration"
};
addedEffects.push_back("meshes\\" + castStatic->mModel);
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
if(!effect->mCastSound.empty())
sndMgr->playSound3D(mCaster, effect->mCastSound, 1.0f, 1.0f);

@ -108,11 +108,19 @@ namespace MWMechanics
}
}
int skill = item.getClass().getEquipmentSkill(item);
if (skill != -1)
if (actor.getClass().isNpc())
{
int value = actor.getClass().getSkill(actor, skill);
rating *= MWMechanics::getHitChance(actor, enemy, value) / 100.f;
int skill = item.getClass().getEquipmentSkill(item);
if (skill != -1)
{
int value = actor.getClass().getSkill(actor, skill);
rating *= MWMechanics::getHitChance(actor, enemy, value) / 100.f;
}
}
else
{
MWWorld::LiveCellRef<ESM::Creature> *ref = actor.get<ESM::Creature>();
rating *= MWMechanics::getHitChance(actor, enemy, ref->mBase->mData.mCombat) / 100.f;
}
return rating * rangedMult;

@ -8,6 +8,7 @@
#include <osg/MatrixTransform>
#include <osg/BlendFunc>
#include <osg/Material>
#include <osg/PositionAttitudeTransform>
#include <osgParticle/ParticleSystem>
#include <osgParticle/ParticleProcessor>
@ -204,6 +205,110 @@ namespace
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.
class CleanObjectRootVisitor : public RemoveVisitor
{
@ -287,7 +392,6 @@ namespace
}
}
};
}
namespace MWRender
@ -432,6 +536,42 @@ namespace MWRender
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
{
public:
@ -1436,15 +1576,22 @@ namespace MWRender
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())
return;
// Early out if we already have this effect
for (std::vector<EffectParams>::iterator it = mEffects.begin(); it != mEffects.end(); ++it)
if (it->mLoop && loop && it->mEffectId == effectId && it->mBoneName == bonename)
FindVfxCallbacksVisitor visitor(effectId);
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;
}
EffectParams params;
params.mModelName = model;
@ -1459,11 +1606,13 @@ namespace MWRender
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;
node->accept(findMaxLengthVisitor);
@ -1471,71 +1620,50 @@ namespace MWRender
// FreezeOnCull doesn't work so well with effect particles, that tend to have moving emitters
SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor;
node->accept(disableFreezeOnCullVisitor);
params.mMaxControllerLength = findMaxLengthVisitor.getMaxLength();
node->setNodeMask(Mask_Effect);
params.mMaxControllerLength = findMaxLengthVisitor.getMaxLength();
params.mLoop = loop;
params.mEffectId = effectId;
params.mBoneName = bonename;
params.mObjects = PartHolderPtr(new PartHolder(node));
params.mAnimTime = std::shared_ptr<EffectAnimationTime>(new EffectAnimationTime);
trans->addUpdateCallback(new UpdateVfxCallback(params));
SceneUtil::AssignControllerSourcesVisitor assignVisitor(std::shared_ptr<SceneUtil::ControllerSource>(params.mAnimTime));
node->accept(assignVisitor);
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)
{
for (std::vector<EffectParams>::iterator it = mEffects.begin(); it != mEffects.end(); ++it)
{
if (it->mEffectId == effectId)
{
mEffects.erase(it);
return;
}
}
RemoveFinishedCallbackVisitor visitor(effectId);
mInsert->accept(visitor);
visitor.remove();
}
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)
out.push_back(it->mEffectId);
UpdateVfxCallback* callback = *it;
if (callback->mParams.mLoop && !callback->mFinished)
out.push_back(callback->mParams.mEffectId);
}
}
void Animation::updateEffects(float duration)
{
for (std::vector<EffectParams>::iterator it = mEffects.begin(); it != mEffects.end(); )
{
it->mAnimTime->addTime(duration);
if (it->mAnimTime->getTime() >= it->mMaxControllerLength)
{
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;
}
// TODO: objects without animation still will have
// transformation nodes with finished callbacks
RemoveFinishedCallbackVisitor visitor;
mInsert->accept(visitor);
visitor.remove();
}
bool Animation::upperBodyReady() const
@ -1778,5 +1906,4 @@ namespace MWRender
mNode->getParent(0)->removeChild(mNode);
}
}
}

@ -71,6 +71,17 @@ private:
};
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
{
public:
@ -247,19 +258,6 @@ protected:
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;
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
* @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 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);
};
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

@ -50,6 +50,7 @@
#include <boost/algorithm/string.hpp>
#include "../mwworld/cellstore.hpp"
#include "../mwworld/class.hpp"
#include "../mwgui/loadingscreen.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp"
@ -1347,6 +1348,29 @@ namespace MWRender
}
}
osg::Vec3f RenderingManager::getHalfExtents(const MWWorld::ConstPtr& object) const
{
osg::Vec3f halfExtents(0, 0, 0);
std::string modelName = object.getClass().getModel(object);
if (modelName.empty())
return halfExtents;
osg::ref_ptr<const osg::Node> node = mResourceSystem->getSceneManager()->getTemplate(modelName);
osg::ComputeBoundsVisitor computeBoundsVisitor;
computeBoundsVisitor.setTraversalMask(~(MWRender::Mask_ParticleSystem|MWRender::Mask_Effect));
const_cast<osg::Node*>(node.get())->accept(computeBoundsVisitor);
osg::BoundingBox bounds = computeBoundsVisitor.getBoundingBox();
if (bounds.valid())
{
halfExtents[0] = std::abs(bounds.xMax() - bounds.xMin()) / 2.f;
halfExtents[1] = std::abs(bounds.yMax() - bounds.yMin()) / 2.f;
halfExtents[2] = std::abs(bounds.zMax() - bounds.zMin()) / 2.f;
}
return halfExtents;
}
void RenderingManager::resetFieldOfView()
{
if (mFieldOfViewOverridden == true)

@ -204,6 +204,8 @@ namespace MWRender
/// reset a previous overrideFieldOfView() call, i.e. revert to field of view specified in the settings file.
void resetFieldOfView();
osg::Vec3f getHalfExtents(const MWWorld::ConstPtr& object) const;
void exportSceneGraph(const MWWorld::Ptr& ptr, const std::string& filename, const std::string& format);
LandManager* getLandManager() const;

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

@ -3339,12 +3339,16 @@ namespace MWWorld
return mRendering->getTerrainHeightAt(worldPos);
}
osg::Vec3f World::getHalfExtents(const ConstPtr& actor, bool rendering) const
osg::Vec3f World::getHalfExtents(const ConstPtr& object, bool rendering) const
{
if (!object.getClass().isActor())
return mRendering->getHalfExtents(object);
// Handle actors separately because of bodyparts
if (rendering)
return mPhysics->getRenderingHalfExtents(actor);
return mPhysics->getRenderingHalfExtents(object);
else
return mPhysics->getHalfExtents(actor);
return mPhysics->getHalfExtents(object);
}
std::string World::exportSceneGraph(const Ptr &ptr)
@ -3403,9 +3407,9 @@ namespace MWWorld
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,

@ -646,7 +646,7 @@ namespace MWWorld
/// Spawn a blood effect for \a ptr at \a worldPosition
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,
ESM::RangeType rangeType, const std::string& id, const std::string& sourceName,

@ -557,7 +557,7 @@ namespace Compiler
mExplicit.clear();
}
void GetArgumentsFromMessageFormat::visitedPlaceholder(Placeholder placeholder, char /*padding*/, int /*width*/, int /*precision*/)
void GetArgumentsFromMessageFormat::visitedPlaceholder(Placeholder placeholder, char /*padding*/, int /*width*/, int /*precision*/, Notation /*notation*/)
{
switch (placeholder)
{

@ -83,7 +83,7 @@ namespace Compiler
std::string mArguments;
protected:
virtual void visitedPlaceholder(Placeholder placeholder, char padding, int width, int precision);
virtual void visitedPlaceholder(Placeholder placeholder, char padding, int width, int precision, Notation notation);
virtual void visitedCharacter(char c) {}
public:

@ -24,7 +24,7 @@ namespace Interpreter
Runtime& mRuntime;
protected:
virtual void visitedPlaceholder(Placeholder placeholder, char padding, int width, int precision)
virtual void visitedPlaceholder(Placeholder placeholder, char padding, int width, int precision, Notation notation)
{
std::ostringstream out;
out.fill(padding);
@ -58,8 +58,34 @@ namespace Interpreter
float value = mRuntime[0].mFloat;
mRuntime.pop();
out << std::fixed << value;
mFormattedMessage += out.str();
if (notation == FixedNotation)
{
out << std::fixed << value;
mFormattedMessage += out.str();
}
else if (notation == ShortestNotation)
{
std::string scientific;
std::string fixed;
out << std::scientific << value;
scientific = out.str();
out.str(std::string());
out.clear();
out << std::fixed << value;
fixed = out.str();
mFormattedMessage += fixed.length() < scientific.length() ? fixed : scientific;
}
else
{
out << std::scientific << value;
mFormattedMessage += out.str();
}
}
break;
default:

@ -49,11 +49,15 @@ namespace Misc
width = (widthSet) ? width : -1;
if (m[i] == 'S' || m[i] == 's')
visitedPlaceholder(StringPlaceholder, pad, width, precision);
else if (m[i] == 'g' || m[i] == 'G')
visitedPlaceholder(IntegerPlaceholder, pad, width, precision);
visitedPlaceholder(StringPlaceholder, pad, width, precision, FixedNotation);
else if (m[i] == 'd' || m[i] == 'i')
visitedPlaceholder(IntegerPlaceholder, pad, width, precision, FixedNotation);
else if (m[i] == 'f' || m[i] == 'F')
visitedPlaceholder(FloatPlaceholder, pad, width, precision);
visitedPlaceholder(FloatPlaceholder, pad, width, precision, FixedNotation);
else if (m[i] == 'e' || m[i] == 'E')
visitedPlaceholder(FloatPlaceholder, pad, width, precision, ScientificNotation);
else if (m[i] == 'g' || m[i] == 'G')
visitedPlaceholder(FloatPlaceholder, pad, width, precision, ShortestNotation);
}
}
}

@ -15,7 +15,14 @@ namespace Misc
FloatPlaceholder
};
virtual void visitedPlaceholder(Placeholder placeholder, char padding, int width, int precision) = 0;
enum Notation
{
FixedNotation,
ScientificNotation,
ShortestNotation
};
virtual void visitedPlaceholder(Placeholder placeholder, char padding, int width, int precision, Notation notation) = 0;
virtual void visitedCharacter(char c) = 0;
public:

@ -9,7 +9,7 @@ few chapters to familiarise yourself with the new interface.
.. warning::
OpenMW CS is still software in development. The manual does not cover any of
its shortcomings, it is written as if everything was working as inteded.
its shortcomings, it is written as if everything was working as intended.
Please report any software problems as bugs in the software, not errors in
the manual.

@ -121,7 +121,7 @@ existing filters into more complex ones.
Scopes
======
Every default filter has the prefix ``project``. This is a *scpoe*, a mechanism
Every default filter has the prefix ``project``. This is a *scope*, a mechanism
that determines the lifetime of the filter. These are the supported scopes:
``project::``

@ -236,7 +236,7 @@ a negative number indicating that he will restock again to maintain that level.
However, it's an attractive item, so he will probably wear it rather than sell it.
So set his stock level too high for him to wear them all (3 works, 2 might do).
Another possibilty, again in Seyda Neen making it easy to access, would be for
Another possibility, again in Seyda Neen making it easy to access, would be for
Fargoth to give it to the player in exchange for his healing ring.
.. figure:: https://gitlab.com/OpenMW/openmw-docs/raw/master/docs/source/manuals/openmw-cs/_static/images/chapter-1/Ring_to_Fargoth_1.png
@ -360,7 +360,7 @@ the base game.
"Modified" status will cover items from the base game which have been modified in this addon.
Click on the top of the column to toggle between ascending and descending order - thus between "Added"
and "Modified" at the top. Or put your desired modified status into a filter then sort alpabetically
and "Modified" at the top. Or put your desired modified status into a filter then sort alphabetically
on a different column.

Loading…
Cancel
Save