1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-06-27 04:41:34 +00:00

Merge branch 'master' into combat_anims

This commit is contained in:
Bret Curtis 2018-08-19 10:03:39 +02:00 committed by GitHub
commit 3c827da702
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 519 additions and 184 deletions

View file

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

View file

@ -15,12 +15,14 @@
Bug #2872: Tab completion in console doesn't work with explicit reference Bug #2872: Tab completion in console doesn't work with explicit reference
Bug #2971: Compiler did not reject lines with naked expressions beginning with x.y Bug #2971: Compiler did not reject lines with naked expressions beginning with x.y
Bug #3049: Drain and Fortify effects are not properly applied on health, magicka and fatigue Bug #3049: Drain and Fortify effects are not properly applied on health, magicka and fatigue
Bug #3072: Fatal error on AddItem <item> that has a script containing Equip <item>
Bug #3249: Fixed revert function not updating views properly Bug #3249: Fixed revert function not updating views properly
Bug #3374: Touch spells not hitting kwama foragers Bug #3374: Touch spells not hitting kwama foragers
Bug #3486: [Mod] NPC Commands does not work Bug #3486: [Mod] NPC Commands does not work
Bug #3533: GetSpellEffects should detect effects with zero duration Bug #3533: GetSpellEffects should detect effects with zero duration
Bug #3591: Angled hit distance too low Bug #3591: Angled hit distance too low
Bug #3629: DB assassin attack never triggers creature spawning Bug #3629: DB assassin attack never triggers creature spawning
Bug #3788: GetPCInJail and GetPCTraveling do not work as in vanilla
Bug #3876: Landscape texture painting is misaligned Bug #3876: Landscape texture painting is misaligned
Bug #3897: Have Goodbye give all choices the effects of Goodbye Bug #3897: Have Goodbye give all choices the effects of Goodbye
Bug #3911: [macOS] Typing in the "Content List name" dialog box produces double characters Bug #3911: [macOS] Typing in the "Content List name" dialog box produces double characters
@ -42,6 +44,7 @@
Bug #4286: Scripted animations can be interrupted Bug #4286: Scripted animations can be interrupted
Bug #4291: Non-persistent actors that started the game as dead do not play death animations Bug #4291: Non-persistent actors that started the game as dead do not play death animations
Bug #4293: Faction members are not aware of faction ownerships in barter Bug #4293: Faction members are not aware of faction ownerships in barter
Bug #4304: "Follow" not working as a second AI package
Bug #4307: World cleanup should remove dead bodies only if death animation is finished Bug #4307: World cleanup should remove dead bodies only if death animation is finished
Bug #4311: OpenMW does not handle RootCollisionNode correctly Bug #4311: OpenMW does not handle RootCollisionNode correctly
Bug #4327: Missing animations during spell/weapon stance switching Bug #4327: Missing animations during spell/weapon stance switching
@ -95,6 +98,7 @@
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
Bug #4591: Attack strength should be 0 if player did not hold the attack button 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 #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

View file

@ -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,
@ -566,6 +566,9 @@ namespace MWBase
virtual bool isPlayerInJail() const = 0; virtual bool isPlayerInJail() const = 0;
virtual void setPlayerTraveling(bool traveling) = 0;
virtual bool isPlayerTraveling() const = 0;
virtual void rotateWorldObject (const MWWorld::Ptr& ptr, osg::Quat rotate) = 0; virtual void rotateWorldObject (const MWWorld::Ptr& ptr, osg::Quat rotate) = 0;
/// Return terrain height at \a worldPos position. /// Return terrain height at \a worldPos position.

View file

@ -517,7 +517,7 @@ namespace MWGui
// Give the script a chance to run once before we do anything else // Give the script a chance to run once before we do anything else
// this is important when setting pcskipequip as a reaction to onpcequip being set (bk_treasuryreport does this) // this is important when setting pcskipequip as a reaction to onpcequip being set (bk_treasuryreport does this)
if (!script.empty() && MWBase::Environment::get().getWorld()->getScriptsEnabled()) if (!force && !script.empty() && MWBase::Environment::get().getWorld()->getScriptsEnabled())
{ {
MWScript::InterpreterContext interpreterContext (&ptr.getRefData().getLocals(), ptr); MWScript::InterpreterContext interpreterContext (&ptr.getRefData().getLocals(), ptr);
MWBase::Environment::get().getScriptManager()->run (script, interpreterContext); MWBase::Environment::get().getScriptManager()->run (script, interpreterContext);

View file

@ -154,6 +154,10 @@ namespace MWGui
if (playerGold<price) if (playerGold<price)
return; return;
// Set "traveling" flag, so GetPCTraveling can detect teleportation.
// We will reset this flag during next world update.
MWBase::Environment::get().getWorld()->setPlayerTraveling(true);
if (!mPtr.getCell()->isExterior()) if (!mPtr.getCell()->isExterior())
// Interior cell -> mages guild transport // Interior cell -> mages guild transport
MWBase::Environment::get().getWindowManager()->playSound("mysticism cast"); MWBase::Environment::get().getWindowManager()->playSound("mysticism cast");

View file

@ -2,7 +2,6 @@
#include <typeinfo> #include <typeinfo>
#include <iostream> #include <iostream>
#include <components/esm/esmreader.hpp> #include <components/esm/esmreader.hpp>
#include <components/esm/esmwriter.hpp> #include <components/esm/esmwriter.hpp>
#include <components/esm/loadnpc.hpp> #include <components/esm/loadnpc.hpp>
@ -404,7 +403,7 @@ namespace MWMechanics
std::set<MWWorld::Ptr> playerAllies; std::set<MWWorld::Ptr> playerAllies;
getActorsSidingWith(MWMechanics::getPlayer(), playerAllies, cachedAllies); getActorsSidingWith(MWMechanics::getPlayer(), playerAllies, cachedAllies);
bool isPlayerFollowerOrEscorter = std::find(playerAllies.begin(), playerAllies.end(), actor1) != playerAllies.end(); bool isPlayerFollowerOrEscorter = playerAllies.find(actor1) != playerAllies.end();
// If actor2 and at least one actor2 are in combat with actor1, actor1 and its allies start combat with them // If actor2 and at least one actor2 are in combat with actor1, actor1 and its allies start combat with them
// Doesn't apply for player followers/escorters // Doesn't apply for player followers/escorters
@ -458,7 +457,7 @@ namespace MWMechanics
// Do aggression check if actor2 is the player or a player follower or escorter // Do aggression check if actor2 is the player or a player follower or escorter
if (!aggressive) if (!aggressive)
{ {
if (againstPlayer || std::find(playerAllies.begin(), playerAllies.end(), actor2) != playerAllies.end()) if (againstPlayer || playerAllies.find(actor2) != playerAllies.end())
{ {
// Player followers and escorters with high fight should not initiate combat with the player or with // Player followers and escorters with high fight should not initiate combat with the player or with
// other player followers or escorters // other player followers or escorters
@ -1777,38 +1776,35 @@ namespace MWMechanics
std::list<MWWorld::Ptr> Actors::getActorsSidingWith(const MWWorld::Ptr& actor) std::list<MWWorld::Ptr> Actors::getActorsSidingWith(const MWWorld::Ptr& actor)
{ {
std::list<MWWorld::Ptr> list; std::list<MWWorld::Ptr> list;
for(PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter) for(PtrActorMap::iterator iter = mActors.begin(); iter != mActors.end(); ++iter)
{ {
const MWWorld::Class &cls = iter->first.getClass(); const MWWorld::Ptr &iteratedActor = iter->first;
const CreatureStats &stats = cls.getCreatureStats(iter->first); if (iteratedActor == getPlayer())
continue;
const CreatureStats &stats = iteratedActor.getClass().getCreatureStats(iteratedActor);
if (stats.isDead()) if (stats.isDead())
continue; continue;
// An actor counts as siding with this actor if Follow or Escort is the current AI package, or there are only Combat packages before the Follow/Escort package // An actor counts as siding with this actor if Follow or Escort is the current AI package, or there are only Combat and Wander packages before the Follow/Escort package
for (std::list<MWMechanics::AiPackage*>::const_iterator it = stats.getAiSequence().begin(); it != stats.getAiSequence().end(); ++it)
{
if ((*it)->sideWithTarget() && (*it)->getTarget() == actor)
{
list.push_back(iter->first);
break;
}
else if ((*it)->getTypeId() != MWMechanics::AiPackage::TypeIdCombat)
break;
}
// Actors that are targeted by this actor's Follow or Escort packages also side with them // Actors that are targeted by this actor's Follow or Escort packages also side with them
if (actor != getPlayer()) for (auto package = stats.getAiSequence().begin(); package != stats.getAiSequence().end(); ++package)
{ {
const CreatureStats &stats2 = actor.getClass().getCreatureStats(actor); const MWWorld::Ptr &target = (*package)->getTarget();
for (std::list<MWMechanics::AiPackage*>::const_iterator it2 = stats2.getAiSequence().begin(); it2 != stats2.getAiSequence().end(); ++it2) if ((*package)->sideWithTarget() && !target.isEmpty())
{ {
if ((*it2)->sideWithTarget() && !(*it2)->getTarget().isEmpty()) if (iteratedActor == actor)
{ {
list.push_back((*it2)->getTarget()); list.push_back(target);
break;
} }
else if ((*it2)->getTypeId() != MWMechanics::AiPackage::TypeIdCombat) else if (target == actor)
break; {
list.push_back(iteratedActor);
}
break;
} }
else if ((*package)->getTypeId() != AiPackage::TypeIdCombat && (*package)->getTypeId() != AiPackage::TypeIdWander)
break;
} }
} }
return list; return list;
@ -1819,17 +1815,21 @@ namespace MWMechanics
std::list<MWWorld::Ptr> list; std::list<MWWorld::Ptr> list;
for(PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter) for(PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter)
{ {
const MWWorld::Class &cls = iter->first.getClass(); const MWWorld::Ptr &iteratedActor = iter->first;
CreatureStats &stats = cls.getCreatureStats(iter->first); if (iteratedActor == getPlayer())
continue;
const CreatureStats &stats = iteratedActor.getClass().getCreatureStats(iteratedActor);
if (stats.isDead()) if (stats.isDead())
continue; continue;
// An actor counts as following if AiFollow is the current AiPackage, or there are only Combat packages before the AiFollow package // An actor counts as following if AiFollow is the current AiPackage,
for (std::list<MWMechanics::AiPackage*>::const_iterator it = stats.getAiSequence().begin(); it != stats.getAiSequence().end(); ++it) // or there are only Combat and Wander packages before the AiFollow package
for (auto package = stats.getAiSequence().begin(); package != stats.getAiSequence().end(); ++package)
{ {
if ((*it)->followTargetThroughDoors() && (*it)->getTarget() == actor) if ((*package)->followTargetThroughDoors() && (*package)->getTarget() == actor)
list.push_back(iter->first); list.push_back(iteratedActor);
else if ((*it)->getTypeId() != MWMechanics::AiPackage::TypeIdCombat) else if ((*package)->getTypeId() != AiPackage::TypeIdCombat && (*package)->getTypeId() != AiPackage::TypeIdWander)
break; break;
} }
} }
@ -1878,24 +1878,24 @@ namespace MWMechanics
std::list<int> list; std::list<int> list;
for(PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter) for(PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter)
{ {
const MWWorld::Class &cls = iter->first.getClass(); const MWWorld::Ptr &iteratedActor = iter->first;
CreatureStats &stats = cls.getCreatureStats(iter->first); if (iteratedActor == getPlayer())
continue;
const CreatureStats &stats = iteratedActor.getClass().getCreatureStats(iteratedActor);
if (stats.isDead()) if (stats.isDead())
continue; continue;
// An actor counts as following if AiFollow is the current AiPackage, or there are only Combat packages before the AiFollow package // An actor counts as following if AiFollow is the current AiPackage,
for (std::list<MWMechanics::AiPackage*>::const_iterator it = stats.getAiSequence().begin(); it != stats.getAiSequence().end(); ++it) // or there are only Combat and Wander packages before the AiFollow package
for (auto package = stats.getAiSequence().begin(); package != stats.getAiSequence().end(); ++package)
{ {
if ((*it)->getTypeId() == MWMechanics::AiPackage::TypeIdFollow) if ((*package)->followTargetThroughDoors() && (*package)->getTarget() == actor)
{ {
MWWorld::Ptr followTarget = (*it)->getTarget(); list.push_back(static_cast<AiFollow*>(*package)->getFollowIndex());
if (followTarget.isEmpty())
continue;
if (followTarget == actor)
list.push_back(static_cast<MWMechanics::AiFollow*>(*it)->getFollowIndex());
break; break;
} }
else if ((*it)->getTypeId() != MWMechanics::AiPackage::TypeIdCombat) else if ((*package)->getTypeId() != AiPackage::TypeIdCombat && (*package)->getTypeId() != AiPackage::TypeIdWander)
break; break;
} }
} }
@ -1907,14 +1907,14 @@ namespace MWMechanics
std::vector<MWWorld::Ptr> neighbors; std::vector<MWWorld::Ptr> neighbors;
osg::Vec3f position (actor.getRefData().getPosition().asVec3()); osg::Vec3f position (actor.getRefData().getPosition().asVec3());
getObjectsInRange(position, aiProcessingDistance, neighbors); getObjectsInRange(position, aiProcessingDistance, neighbors);
for(std::vector<MWWorld::Ptr>::const_iterator iter(neighbors.begin());iter != neighbors.end();++iter) for(auto neighbor = neighbors.begin(); neighbor != neighbors.end(); ++neighbor)
{ {
const MWWorld::Class &cls = iter->getClass(); const CreatureStats &stats = neighbor->getClass().getCreatureStats(*neighbor);
const CreatureStats &stats = cls.getCreatureStats(*iter); if (stats.isDead() || *neighbor == actor)
if (stats.isDead() || *iter == actor)
continue; continue;
if (stats.getAiSequence().isInCombat(actor)) if (stats.getAiSequence().isInCombat(actor))
list.push_front(*iter); list.push_front(*neighbor);
} }
return list; return list;
} }
@ -1926,15 +1926,18 @@ namespace MWMechanics
osg::Vec3f position (actor.getRefData().getPosition().asVec3()); osg::Vec3f position (actor.getRefData().getPosition().asVec3());
getObjectsInRange(position, aiProcessingDistance, neighbors); getObjectsInRange(position, aiProcessingDistance, neighbors);
std::list<MWWorld::Ptr> followers = getActorsFollowing(actor); std::set<MWWorld::Ptr> followers;
for(std::vector<MWWorld::Ptr>::const_iterator iter(neighbors.begin());iter != neighbors.end();++iter) getActorsFollowing(actor, followers);
for (auto neighbor = neighbors.begin(); neighbor != neighbors.end(); ++neighbor)
{ {
const CreatureStats &stats = iter->getClass().getCreatureStats(*iter); const CreatureStats &stats = neighbor->getClass().getCreatureStats(*neighbor);
if (stats.isDead() || *iter == actor || iter->getClass().isPureWaterCreature(*iter)) if (stats.isDead() || *neighbor == actor || neighbor->getClass().isPureWaterCreature(*neighbor))
continue; continue;
const bool isFollower = std::find(followers.begin(), followers.end(), *iter) != followers.end();
if (stats.getAiSequence().isInCombat(actor) || (MWBase::Environment::get().getMechanicsManager()->isAggressive(*iter, actor) && !isFollower)) const bool isFollower = followers.find(*neighbor) != followers.end();
list.push_back(*iter);
if (stats.getAiSequence().isInCombat(actor) || (MWBase::Environment::get().getMechanicsManager()->isAggressive(*neighbor, actor) && !isFollower))
list.push_back(*neighbor);
} }
return list; return list;
} }

View file

@ -42,7 +42,19 @@ bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::Charac
return false; 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)); bool turned = smoothTurn(actor, getZAngleToDir(dir), 2, osg::DegreesToRadians(3.f));
turned &= smoothTurn(actor, getXAngleToDir(dir), 0, osg::DegreesToRadians(3.f)); turned &= smoothTurn(actor, getXAngleToDir(dir), 0, osg::DegreesToRadians(3.f));

View file

@ -19,6 +19,7 @@
#include "aicombataction.hpp" #include "aicombataction.hpp"
#include "aipursue.hpp" #include "aipursue.hpp"
#include "actorutil.hpp" #include "actorutil.hpp"
#include "../mwworld/class.hpp"
namespace MWMechanics namespace MWMechanics
{ {
@ -122,6 +123,20 @@ bool AiSequence::isInCombat() const
return false; 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 bool AiSequence::hasPackage(int typeId) const
{ {
for (std::list<AiPackage*>::const_iterator it = mPackages.begin(); it != mPackages.end(); ++it) for (std::list<AiPackage*>::const_iterator it = mPackages.begin(); it != mPackages.end(); ++it)

View file

@ -88,6 +88,9 @@ namespace MWMechanics
/// Is there any combat package? /// Is there any combat package?
bool isInCombat () const; 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? /// Does this AI sequence have the given package type?
bool hasPackage(int typeId) const; bool hasPackage(int typeId) const;

View file

@ -1,6 +1,7 @@
#include "mechanicsmanagerimp.hpp" #include "mechanicsmanagerimp.hpp"
#include <limits.h> #include <limits.h>
#include <set>
#include <components/misc/rng.hpp> #include <components/misc/rng.hpp>
@ -1445,11 +1446,12 @@ namespace MWMechanics
if (target == getPlayer() || !attacker.getClass().isActor()) if (target == getPlayer() || !attacker.getClass().isActor())
return false; return false;
std::list<MWWorld::Ptr> followersAttacker = getActorsSidingWith(attacker); std::set<MWWorld::Ptr> followersAttacker;
getActorsSidingWith(attacker, followersAttacker);
MWMechanics::CreatureStats& statsTarget = target.getClass().getCreatureStats(target); MWMechanics::CreatureStats& statsTarget = target.getClass().getCreatureStats(target);
if (std::find(followersAttacker.begin(), followersAttacker.end(), target) != followersAttacker.end()) if (followersAttacker.find(target) != followersAttacker.end())
{ {
statsTarget.friendlyHit(); statsTarget.friendlyHit();
@ -1460,24 +1462,11 @@ namespace MWMechanics
} }
} }
// Attacking an NPC that is already in combat with any other NPC is not a crime if (canCommitCrimeAgainst(target, attacker))
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))
commitCrime(attacker, target, MWBase::MechanicsManager::OT_Assault); commitCrime(attacker, target, MWBase::MechanicsManager::OT_Assault);
AiSequence& seq = statsTarget.getAiSequence();
if (!attacker.isEmpty() && (attacker.getClass().getCreatureStats(attacker).getAiSequence().isInCombat(target) if (!attacker.isEmpty() && (attacker.getClass().getCreatureStats(attacker).getAiSequence().isInCombat(target)
|| attacker == getPlayer()) || attacker == getPlayer())
&& !seq.isInCombat(attacker)) && !seq.isInCombat(attacker))
@ -1504,6 +1493,14 @@ namespace MWMechanics
return true; 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) void MechanicsManager::actorKilled(const MWWorld::Ptr &victim, const MWWorld::Ptr &attacker)
{ {
if (attacker.isEmpty() || victim.isEmpty()) if (attacker.isEmpty() || victim.isEmpty())
@ -1516,11 +1513,10 @@ namespace MWMechanics
return; // TODO: implement animal rights return; // TODO: implement animal rights
const MWMechanics::NpcStats& victimStats = victim.getClass().getNpcStats(victim); const MWMechanics::NpcStats& victimStats = victim.getClass().getNpcStats(victim);
if (victimStats.getCrimeId() == -1) const MWWorld::Ptr &player = getPlayer();
return; bool canCommit = attacker == player && canCommitCrimeAgainst(victim, attacker);
// For now we report only about crimes of player and player's followers // For now we report only about crimes of player and player's followers
const MWWorld::Ptr &player = getPlayer();
if (attacker != player) if (attacker != player)
{ {
std::set<MWWorld::Ptr> playerFollowers; std::set<MWWorld::Ptr> playerFollowers;
@ -1529,6 +1525,9 @@ namespace MWMechanics
return; return;
} }
if (!canCommit && victimStats.getCrimeId() == -1)
return;
// Simple check for who attacked first: if the player attacked first, a crimeId should be set // 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, // 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. // for bystanders it is not possible to tell who attacked first, anyway.

View file

@ -132,6 +132,12 @@ namespace MWMechanics
/// @note No-op for non-player attackers /// @note No-op for non-player attackers
virtual void actorKilled (const MWWorld::Ptr& victim, const MWWorld::Ptr& attacker); 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 /// 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 /// @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, virtual void itemTaken (const MWWorld::Ptr& ptr, const MWWorld::Ptr& item, const MWWorld::Ptr& container,

View file

@ -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);

View file

@ -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,83 +1606,64 @@ namespace MWRender
parentNode = found->second; parentNode = found->second;
} }
osg::ref_ptr<osg::Node> node = mResourceSystem->getSceneManager()->getInstance(model, parentNode);
osg::ref_ptr<osg::PositionAttitudeTransform> trans = new osg::PositionAttitudeTransform;
trans->setScale(osg::Vec3f(scale, scale, scale));
parentNode->addChild(trans);
osg::ref_ptr<osg::Node> node = mResourceSystem->getSceneManager()->getInstance(model, trans);
node->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF); node->getOrCreateStateSet()->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
params.mObjects = PartHolderPtr(new PartHolder(node));
SceneUtil::FindMaxControllerLengthVisitor findMaxLengthVisitor; SceneUtil::FindMaxControllerLengthVisitor findMaxLengthVisitor;
node->accept(findMaxLengthVisitor); node->accept(findMaxLengthVisitor);
// 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);
} }
} }
} }

View file

@ -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

View file

@ -49,6 +49,7 @@
#include <boost/algorithm/string.hpp> #include <boost/algorithm/string.hpp>
#include "../mwworld/cellstore.hpp" #include "../mwworld/cellstore.hpp"
#include "../mwworld/class.hpp"
#include "../mwgui/loadingscreen.hpp" #include "../mwgui/loadingscreen.hpp"
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
@ -1321,6 +1322,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() void RenderingManager::resetFieldOfView()
{ {
if (mFieldOfViewOverridden == true) if (mFieldOfViewOverridden == true)

View file

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

View file

@ -192,8 +192,9 @@ namespace MWScript
if (it == invStore.end()) if (it == invStore.end())
{ {
it = ptr.getClass().getContainerStore (ptr).add (item, 1, ptr); it = ptr.getClass().getContainerStore (ptr).add (item, 1, ptr);
Log(Debug::Warning) << "Implicitly adding one " << item << " to container " Log(Debug::Warning) << "Implicitly adding one " << item <<
"to fulfil requirements of Equip instruction"; " to the inventory store of " << ptr.getCellRef().getRefId() <<
" to fulfill the requirements of Equip instruction";
} }
if (ptr == MWMechanics::getPlayer()) if (ptr == MWMechanics::getPlayer())

View file

@ -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);
@ -1155,8 +1156,7 @@ namespace MWScript
virtual void execute (Interpreter::Runtime &runtime) virtual void execute (Interpreter::Runtime &runtime)
{ {
/// \todo implement traveling check runtime.push (MWBase::Environment::get().getWorld()->isPlayerTraveling());
runtime.push (0);
} }
}; };

View file

@ -155,7 +155,8 @@ namespace MWWorld
mGodMode(false), mScriptsEnabled(true), mContentFiles (contentFiles), mUserDataPath(userDataPath), mGodMode(false), mScriptsEnabled(true), mContentFiles (contentFiles), mUserDataPath(userDataPath),
mActivationDistanceOverride (activationDistanceOverride), mStartupScript(startupScript), mActivationDistanceOverride (activationDistanceOverride), mStartupScript(startupScript),
mStartCell (startCell), mDistanceToFacedObject(-1), mTeleportEnabled(true), mStartCell (startCell), mDistanceToFacedObject(-1), mTeleportEnabled(true),
mLevitationEnabled(true), mGoToJail(false), mDaysInPrison(0), mSpellPreloadTimer(0.f) mLevitationEnabled(true), mGoToJail(false), mDaysInPrison(0),
mPlayerTraveling(false), mPlayerInJail(false), mSpellPreloadTimer(0.f)
{ {
mPhysics.reset(new MWPhysics::PhysicsSystem(resourceSystem, rootNode)); mPhysics.reset(new MWPhysics::PhysicsSystem(resourceSystem, rootNode));
mRendering.reset(new MWRender::RenderingManager(viewer, rootNode, resourceSystem, workQueue, &mFallback, resourcePath)); mRendering.reset(new MWRender::RenderingManager(viewer, rootNode, resourceSystem, workQueue, &mFallback, resourcePath));
@ -313,6 +314,8 @@ namespace MWWorld
mGoToJail = false; mGoToJail = false;
mTeleportEnabled = true; mTeleportEnabled = true;
mLevitationEnabled = true; mLevitationEnabled = true;
mPlayerTraveling = false;
mPlayerInJail = false;
fillGlobalVariables(); fillGlobalVariables();
} }
@ -1644,6 +1647,15 @@ namespace MWWorld
if (mGoToJail && !paused) if (mGoToJail && !paused)
goToJail(); goToJail();
// Reset "traveling" flag - there was a frame to detect traveling.
mPlayerTraveling = false;
// The same thing for "in jail" flag: reset it if:
// 1. Player was in jail
// 2. Jailing window was closed
if (mPlayerInJail && !mGoToJail && !MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Jail))
mPlayerInJail = false;
updateWeather(duration, paused); updateWeather(duration, paused);
if (!paused) if (!paused)
@ -3283,6 +3295,7 @@ namespace MWWorld
{ {
// Reset bounty and forget the crime now, but don't change cell yet (the player should be able to read the dialog text first) // Reset bounty and forget the crime now, but don't change cell yet (the player should be able to read the dialog text first)
mGoToJail = true; mGoToJail = true;
mPlayerInJail = true;
MWWorld::Ptr player = getPlayerPtr(); MWWorld::Ptr player = getPlayerPtr();
@ -3308,10 +3321,17 @@ namespace MWWorld
bool World::isPlayerInJail() const bool World::isPlayerInJail() const
{ {
if (mGoToJail) return mPlayerInJail;
return true; }
return MWBase::Environment::get().getWindowManager()->containsMode(MWGui::GM_Jail); void World::setPlayerTraveling(bool traveling)
{
mPlayerTraveling = traveling;
}
bool World::isPlayerTraveling() const
{
return mPlayerTraveling;
} }
float World::getTerrainHeightAt(const osg::Vec3f& worldPos) const float World::getTerrainHeightAt(const osg::Vec3f& worldPos) const
@ -3319,12 +3339,16 @@ namespace MWWorld
return mRendering->getTerrainHeightAt(worldPos); 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) if (rendering)
return mPhysics->getRenderingHalfExtents(actor); return mPhysics->getRenderingHalfExtents(object);
else else
return mPhysics->getHalfExtents(actor); return mPhysics->getHalfExtents(object);
} }
std::string World::exportSceneGraph(const Ptr &ptr) std::string World::exportSceneGraph(const Ptr &ptr)
@ -3383,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,

View file

@ -168,6 +168,8 @@ namespace MWWorld
bool mLevitationEnabled; bool mLevitationEnabled;
bool mGoToJail; bool mGoToJail;
int mDaysInPrison; int mDaysInPrison;
bool mPlayerTraveling;
bool mPlayerInJail;
float mSpellPreloadTimer; float mSpellPreloadTimer;
@ -644,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,
@ -672,6 +674,9 @@ namespace MWWorld
bool isPlayerInJail() const override; bool isPlayerInJail() const override;
void setPlayerTraveling(bool traveling) override;
bool isPlayerTraveling() const override;
/// Return terrain height at \a worldPos position. /// Return terrain height at \a worldPos position.
float getTerrainHeightAt(const osg::Vec3f& worldPos) const override; float getTerrainHeightAt(const osg::Vec3f& worldPos) const override;

View file

@ -557,7 +557,7 @@ namespace Compiler
mExplicit.clear(); 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) switch (placeholder)
{ {

View file

@ -83,7 +83,7 @@ namespace Compiler
std::string mArguments; std::string mArguments;
protected: 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) {} virtual void visitedCharacter(char c) {}
public: public:

View file

@ -24,7 +24,7 @@ namespace Interpreter
Runtime& mRuntime; Runtime& mRuntime;
protected: 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; std::ostringstream out;
out.fill(padding); out.fill(padding);
@ -58,8 +58,34 @@ namespace Interpreter
float value = mRuntime[0].mFloat; float value = mRuntime[0].mFloat;
mRuntime.pop(); mRuntime.pop();
out << std::fixed << value; if (notation == FixedNotation)
mFormattedMessage += out.str(); {
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; break;
default: default:

View file

@ -49,11 +49,15 @@ namespace Misc
width = (widthSet) ? width : -1; width = (widthSet) ? width : -1;
if (m[i] == 'S' || m[i] == 's') if (m[i] == 'S' || m[i] == 's')
visitedPlaceholder(StringPlaceholder, pad, width, precision); visitedPlaceholder(StringPlaceholder, pad, width, precision, FixedNotation);
else if (m[i] == 'g' || m[i] == 'G') else if (m[i] == 'd' || m[i] == 'i')
visitedPlaceholder(IntegerPlaceholder, pad, width, precision); visitedPlaceholder(IntegerPlaceholder, pad, width, precision, FixedNotation);
else if (m[i] == 'f' || m[i] == 'F') 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);
} }
} }
} }

View file

@ -15,7 +15,14 @@ namespace Misc
FloatPlaceholder 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; virtual void visitedCharacter(char c) = 0;
public: public:

View file

@ -9,7 +9,7 @@ few chapters to familiarise yourself with the new interface.
.. warning:: .. warning::
OpenMW CS is still software in development. The manual does not cover any of 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 Please report any software problems as bugs in the software, not errors in
the manual. the manual.

View file

@ -121,7 +121,7 @@ existing filters into more complex ones.
Scopes 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: that determines the lifetime of the filter. These are the supported scopes:
``project::`` ``project::``

View file

@ -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. 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). 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. 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 .. 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. "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" 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. on a different column.