Resolve conflicts in pull request #55

# Conflicts:
#	README.md
#	apps/openmw/mwclass/npc.cpp
#	apps/openmw/mwmechanics/combat.cpp
pull/58/head
David Cernat 8 years ago
commit 3b7693c719

@ -509,6 +509,8 @@ printf "OpenAL-Soft 1.17.2... "
add_cmake_opts -DOPENAL_INCLUDE_DIR="${OPENAL_SDK}/include/AL" \ add_cmake_opts -DOPENAL_INCLUDE_DIR="${OPENAL_SDK}/include/AL" \
-DOPENAL_LIBRARY="${OPENAL_SDK}/libs/Win${BITS}/OpenAL32.lib" -DOPENAL_LIBRARY="${OPENAL_SDK}/libs/Win${BITS}/OpenAL32.lib"
add_runtime_dlls "$(pwd)/openal-soft-1.17.2-bin/bin/WIN${BITS}/soft_oal.dll:OpenAL32.dll"
echo Done. echo Done.
} }
cd $DEPS cd $DEPS
@ -631,7 +633,7 @@ printf "SDL 2.0.4... "
export SDL2DIR="$(real_pwd)/SDL2-2.0.4" export SDL2DIR="$(real_pwd)/SDL2-2.0.4"
add_runtime_dlls "${SDL2DIR}/lib/x${ARCHSUFFIX}/SDL2.dll" add_runtime_dlls "$(pwd)/SDL2-2.0.4/lib/x${ARCHSUFFIX}/SDL2.dll"
echo Done. echo Done.
} }
@ -691,8 +693,16 @@ if [ -z $CI ]; then
echo "- Copying Runtime DLLs..." echo "- Copying Runtime DLLs..."
mkdir -p $BUILD_CONFIG mkdir -p $BUILD_CONFIG
for DLL in $RUNTIME_DLLS; do for DLL in $RUNTIME_DLLS; do
echo " $(basename $DLL)." TARGET="$(basename "$DLL")"
cp "$DLL" $BUILD_CONFIG/ if [[ "$DLL" == *":"* ]]; then
IFS=':'; SPLIT=( ${DLL} ); unset IFS
DLL=${SPLIT[0]}
TARGET=${SPLIT[1]}
fi
echo " ${TARGET}."
cp "$DLL" "$BUILD_CONFIG/$TARGET"
done done
echo echo

@ -464,10 +464,11 @@ if(WIN32)
INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/Plugin_MyGUI_OpenMW_Resources.dll" DESTINATION ".") INSTALL(PROGRAMS "${OpenMW_BINARY_DIR}/Release/Plugin_MyGUI_OpenMW_Resources.dll" DESTINATION ".")
ENDIF(BUILD_MYGUI_PLUGIN) ENDIF(BUILD_MYGUI_PLUGIN)
INSTALL(DIRECTORY IF(DESIRED_QT_VERSION MATCHES 5)
"${OpenMW_BINARY_DIR}/Release/platforms" INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/Release/platforms" DESTINATION ".")
"${OpenMW_BINARY_DIR}/resources" ENDIF()
DESTINATION ".")
INSTALL(DIRECTORY "${OpenMW_BINARY_DIR}/resources" DESTINATION ".")
FILE(GLOB plugin_dir "${OpenMW_BINARY_DIR}/Release/osgPlugins-*") FILE(GLOB plugin_dir "${OpenMW_BINARY_DIR}/Release/osgPlugins-*")
INSTALL(DIRECTORY ${plugin_dir} DESTINATION ".") INSTALL(DIRECTORY ${plugin_dir} DESTINATION ".")

@ -81,7 +81,7 @@ void CSMDoc::Runner::start (bool delayed)
arguments << ("--script-run="+mStartup->fileName());; arguments << ("--script-run="+mStartup->fileName());;
arguments << arguments <<
QString::fromUtf8 (("--data="+mProjectPath.parent_path().string()).c_str()); QString::fromUtf8 (("--data=\""+mProjectPath.parent_path().string()+"\"").c_str());
for (std::vector<std::string>::const_iterator iter (mContentFiles.begin()); for (std::vector<std::string>::const_iterator iter (mContentFiles.begin());
iter!=mContentFiles.end(); ++iter) iter!=mContentFiles.end(); ++iter)

@ -321,6 +321,10 @@ void CSMDoc::WriteCellCollectionStage::perform (int stage, Messages& messages)
{ {
CSMWorld::CellRef refRecord = ref.get(); CSMWorld::CellRef refRecord = ref.get();
// Check for uninitialized content file
if (!refRecord.mRefNum.hasContentFile())
refRecord.mRefNum.mContentFile = 0;
// recalculate the ref's cell location // recalculate the ref's cell location
std::ostringstream stream; std::ostringstream stream;
if (!interior) if (!interior)

@ -64,7 +64,7 @@ int CSMWorld::Data::count (RecordBase::State state, const CollectionBase& collec
CSMWorld::Data::Data (ToUTF8::FromType encoding, const ResourcesManager& resourcesManager, const Fallback::Map* fallback, const boost::filesystem::path& resDir) CSMWorld::Data::Data (ToUTF8::FromType encoding, const ResourcesManager& resourcesManager, const Fallback::Map* fallback, const boost::filesystem::path& resDir)
: mEncoder (encoding), mPathgrids (mCells), mRefs (mCells), : mEncoder (encoding), mPathgrids (mCells), mRefs (mCells),
mResourcesManager (resourcesManager), mFallbackMap(fallback), mResourcesManager (resourcesManager), mFallbackMap(fallback),
mReader (0), mDialogue (0), mReaderIndex(0), mResourceSystem(new Resource::ResourceSystem(resourcesManager.getVFS())) mReader (0), mDialogue (0), mReaderIndex(1), mResourceSystem(new Resource::ResourceSystem(resourcesManager.getVFS()))
{ {
mResourceSystem->getSceneManager()->setShaderPath((resDir / "shaders").string()); mResourceSystem->getSceneManager()->setShaderPath((resDir / "shaders").string());
@ -899,9 +899,11 @@ int CSMWorld::Data::startLoading (const boost::filesystem::path& path, bool base
mReader = new ESM::ESMReader; mReader = new ESM::ESMReader;
mReader->setEncoder (&mEncoder); mReader->setEncoder (&mEncoder);
mReader->setIndex(mReaderIndex++); mReader->setIndex((project || !base) ? 0 : mReaderIndex++);
mReader->open (path.string()); mReader->open (path.string());
mContentFileNames.insert(std::make_pair(path.filename().string(), mReader->getIndex()));
mBase = base; mBase = base;
mProject = project; mProject = project;
@ -914,6 +916,20 @@ int CSMWorld::Data::startLoading (const boost::filesystem::path& path, bool base
mMetaData.setRecord (0, Record<MetaData> (RecordBase::State_ModifiedOnly, 0, &metaData)); mMetaData.setRecord (0, Record<MetaData> (RecordBase::State_ModifiedOnly, 0, &metaData));
} }
// Fix uninitialized master data index
for (std::vector<ESM::Header::MasterData>::const_iterator masterData = mReader->getGameFiles().begin();
masterData != mReader->getGameFiles().end(); ++masterData)
{
std::map<std::string, int>::iterator nameResult = mContentFileNames.find(masterData->name);
if (nameResult != mContentFileNames.end())
{
ESM::Header::MasterData& hackedMasterData = const_cast<ESM::Header::MasterData&>(*masterData);
hackedMasterData.index = nameResult->second;
}
}
return mReader->getRecordCount(); return mReader->getRecordCount();
} }

@ -123,6 +123,8 @@ namespace CSMWorld
std::vector<boost::shared_ptr<ESM::ESMReader> > mReaders; std::vector<boost::shared_ptr<ESM::ESMReader> > mReaders;
std::map<std::string, int> mContentFileNames;
// not implemented // not implemented
Data (const Data&); Data (const Data&);
Data& operator= (const Data&); Data& operator= (const Data&);

@ -92,7 +92,7 @@ osg::Vec3f CSVRender::InstanceMode::getScreenCoords(const osg::Vec3f& pos)
} }
CSVRender::InstanceMode::InstanceMode (WorldspaceWidget *worldspaceWidget, QWidget *parent) CSVRender::InstanceMode::InstanceMode (WorldspaceWidget *worldspaceWidget, QWidget *parent)
: EditMode (worldspaceWidget, QIcon (":placeholder"), Mask_Reference, "Instance editing", : EditMode (worldspaceWidget, QIcon (":placeholder"), Mask_Reference | Mask_Terrain, "Instance editing",
parent), mSubMode (0), mSubModeId ("move"), mSelectionMode (0), mDragMode (DragMode_None), parent), mSubMode (0), mSubModeId ("move"), mSelectionMode (0), mDragMode (DragMode_None),
mDragAxis (-1), mLocked (false), mUnitScaleDist(1) mDragAxis (-1), mLocked (false), mUnitScaleDist(1)
{ {

@ -486,9 +486,8 @@ namespace MWBase
virtual void castSpell (const MWWorld::Ptr& actor) = 0; virtual void castSpell (const MWWorld::Ptr& actor) = 0;
virtual void launchMagicBolt (const std::string& model, const std::string& sound, const std::string& spellId, virtual void launchMagicBolt (const std::string& spellId, bool stack, const ESM::EffectList& effects,
float speed, bool stack, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const std::string& sourceName, const osg::Vec3f& fallbackDirection) = 0;
const MWWorld::Ptr& caster, const std::string& sourceName, const osg::Vec3f& fallbackDirection) = 0;
virtual void launchProjectile (MWWorld::Ptr actor, MWWorld::ConstPtr projectile, virtual void launchProjectile (MWWorld::Ptr actor, MWWorld::ConstPtr projectile,
const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr bow, float speed, float attackStrength) = 0; const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr bow, float speed, float attackStrength) = 0;

@ -264,7 +264,7 @@ namespace MWClass
if(Misc::Rng::roll0to99() >= hitchance) if(Misc::Rng::roll0to99() >= hitchance)
{ {
victim.getClass().onHit(victim, 0.0f, false, MWWorld::Ptr(), ptr, false); victim.getClass().onHit(victim, 0.0f, false, MWWorld::Ptr(), ptr, osg::Vec3f(), false);
MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr);
return; return;
} }
@ -318,15 +318,12 @@ namespace MWClass
if (MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage, attackStrength)) if (MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage, attackStrength))
damage = 0; damage = 0;
if (damage > 0)
MWBase::Environment::get().getWorld()->spawnBloodEffect(victim, hitPosition);
MWMechanics::diseaseContact(victim, ptr); MWMechanics::diseaseContact(victim, ptr);
victim.getClass().onHit(victim, damage, healthdmg, weapon, ptr, true); victim.getClass().onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true);
} }
void Creature::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, bool successful) const void Creature::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const
{ {
// NOTE: 'object' and/or 'attacker' may be empty. // NOTE: 'object' and/or 'attacker' may be empty.
@ -342,10 +339,10 @@ namespace MWClass
setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker);
} }
if(!object.isEmpty()) if (!object.isEmpty())
getCreatureStats(ptr).setLastHitAttemptObject(object.getCellRef().getRefId()); getCreatureStats(ptr).setLastHitAttemptObject(object.getCellRef().getRefId());
if(setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer()) if (setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer())
{ {
const std::string &script = ptr.get<ESM::Creature>()->mBase->mScript; const std::string &script = ptr.get<ESM::Creature>()->mBase->mScript;
/* Set the OnPCHitMe script variable. The script is responsible for clearing it. */ /* Set the OnPCHitMe script variable. The script is responsible for clearing it. */
@ -353,14 +350,14 @@ namespace MWClass
ptr.getRefData().getLocals().setVarByInt(script, "onpchitme", 1); ptr.getRefData().getLocals().setVarByInt(script, "onpchitme", 1);
} }
if(!successful) if (!successful)
{ {
// Missed // Missed
MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "miss", 1.0f, 1.0f); MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "miss", 1.0f, 1.0f);
return; return;
} }
if(!object.isEmpty()) if (!object.isEmpty())
getCreatureStats(ptr).setLastHitObject(object.getCellRef().getRefId()); getCreatureStats(ptr).setLastHitObject(object.getCellRef().getRefId());
if (damage > 0.0f && !object.isEmpty()) if (damage > 0.0f && !object.isEmpty())
@ -391,7 +388,10 @@ namespace MWClass
if(ishealth) if(ishealth)
{ {
if (!attacker.isEmpty()) if (!attacker.isEmpty())
{
damage = scaleDamage(damage, attacker, ptr); damage = scaleDamage(damage, attacker, ptr);
MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition);
}
MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "Health Damage", 1.0f, 1.0f); MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "Health Damage", 1.0f, 1.0f);

@ -58,7 +58,7 @@ namespace MWClass
virtual void hit(const MWWorld::Ptr& ptr, float attackStrength, int type) const; virtual void hit(const MWWorld::Ptr& ptr, float attackStrength, int type) const;
virtual void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, bool successful) const; virtual void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const;
virtual boost::shared_ptr<MWWorld::Action> activate (const MWWorld::Ptr& ptr, virtual boost::shared_ptr<MWWorld::Action> activate (const MWWorld::Ptr& ptr,
const MWWorld::Ptr& actor) const; const MWWorld::Ptr& actor) const;

@ -609,7 +609,7 @@ namespace MWClass
mwmp::Main::get().getLocalPlayer()->SendAttack(0); mwmp::Main::get().getLocalPlayer()->SendAttack(0);
} }
othercls.onHit(victim, 0.0f, false, weapon, ptr, false); othercls.onHit(victim, 0.0f, false, weapon, ptr, osg::Vec3f(), false);
MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr);
return; return;
} }
@ -664,15 +664,12 @@ namespace MWClass
if (MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage, attackStrength)) if (MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage, attackStrength))
damage = 0; damage = 0;
if (healthdmg && damage > 0)
MWBase::Environment::get().getWorld()->spawnBloodEffect(victim, hitPosition);
MWMechanics::diseaseContact(victim, ptr); MWMechanics::diseaseContact(victim, ptr);
othercls.onHit(victim, damage, healthdmg, weapon, ptr, true); othercls.onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true);
} }
void Npc::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, bool successful) const void Npc::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const
{ {
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
@ -689,10 +686,10 @@ namespace MWClass
setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker);
} }
if(!object.isEmpty()) if (!object.isEmpty())
getCreatureStats(ptr).setLastHitAttemptObject(object.getCellRef().getRefId()); getCreatureStats(ptr).setLastHitAttemptObject(object.getCellRef().getRefId());
if(setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer()) if (setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer())
{ {
const std::string &script = ptr.getClass().getScript(ptr); const std::string &script = ptr.getClass().getScript(ptr);
/* Set the OnPCHitMe script variable. The script is responsible for clearing it. */ /* Set the OnPCHitMe script variable. The script is responsible for clearing it. */
@ -700,14 +697,14 @@ namespace MWClass
ptr.getRefData().getLocals().setVarByInt(script, "onpchitme", 1); ptr.getRefData().getLocals().setVarByInt(script, "onpchitme", 1);
} }
if(!successful) if (!successful)
{ {
// Missed // Missed
sndMgr->playSound3D(ptr, "miss", 1.0f, 1.0f); sndMgr->playSound3D(ptr, "miss", 1.0f, 1.0f);
return; return;
} }
if(!object.isEmpty()) if (!object.isEmpty())
getCreatureStats(ptr).setLastHitObject(object.getCellRef().getRefId()); getCreatureStats(ptr).setLastHitObject(object.getCellRef().getRefId());
@ -717,7 +714,7 @@ namespace MWClass
if (damage < 0.001f) if (damage < 0.001f)
damage = 0; damage = 0;
if(damage > 0.0f && !attacker.isEmpty()) if (damage > 0.0f && !attacker.isEmpty())
{ {
// 'ptr' is losing health. Play a 'hit' voiced dialog entry if not already saying // 'ptr' is losing health. Play a 'hit' voiced dialog entry if not already saying
// something, alert the character controller, scripts, etc. // something, alert the character controller, scripts, etc.
@ -748,7 +745,7 @@ namespace MWClass
else else
getCreatureStats(ptr).setHitRecovery(true); // Is this supposed to always occur? getCreatureStats(ptr).setHitRecovery(true); // Is this supposed to always occur?
if(damage > 0 && ishealth) if (damage > 0 && ishealth)
{ {
// Hit percentages: // Hit percentages:
// cuirass = 30% // cuirass = 30%
@ -810,16 +807,18 @@ namespace MWClass
} }
} }
if(ishealth) if (ishealth)
{ {
if (!attacker.isEmpty()) if (!attacker.isEmpty())
damage = scaleDamage(damage, attacker, ptr); damage = scaleDamage(damage, attacker, ptr);
if(damage > 0.0f) if (damage > 0.0f)
{ {
sndMgr->playSound3D(ptr, "Health Damage", 1.0f, 1.0f); sndMgr->playSound3D(ptr, "Health Damage", 1.0f, 1.0f);
if (ptr == MWMechanics::getPlayer()) if (ptr == MWMechanics::getPlayer())
MWBase::Environment::get().getWindowManager()->activateHitOverlay(); MWBase::Environment::get().getWindowManager()->activateHitOverlay();
if (!attacker.isEmpty())
MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition);
} }
MWMechanics::DynamicStat<float> health(getCreatureStats(ptr).getHealth()); MWMechanics::DynamicStat<float> health(getCreatureStats(ptr).getHealth());
health.setCurrent(health.getCurrent() - damage); health.setCurrent(health.getCurrent() - damage);

@ -73,7 +73,7 @@ namespace MWClass
virtual void hit(const MWWorld::Ptr& ptr, float attackStrength, int type) const; virtual void hit(const MWWorld::Ptr& ptr, float attackStrength, int type) const;
virtual void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, bool successful) const; virtual void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const;
virtual void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector<std::string>& models) const; virtual void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector<std::string>& models) const;
///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: list getModel(). ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: list getModel().

@ -23,33 +23,29 @@ namespace MWMechanics
return new AiActivate(*this); return new AiActivate(*this);
} }
bool AiActivate::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) bool AiActivate::execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration)
{ {
ESM::Position pos = actor.getRefData().getPosition(); //position of the actor
const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mObjectId, false); //The target to follow const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mObjectId, false); //The target to follow
actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing); actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing);
if(target == MWWorld::Ptr() || if (target == MWWorld::Ptr() ||
!target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently registered !target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should check whether the target is currently registered
// with the MechanicsManager // with the MechanicsManager
) )
return true; //Target doesn't exist return true; //Target doesn't exist
//Set the target desition from the actor //Set the target destination for the actor
ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos; ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos;
if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) < MWBase::Environment::get().getWorld()->getMaxActivationDistance()) { //Stop when you get in activation range if (pathTo(actor, dest, duration, MWBase::Environment::get().getWorld()->getMaxActivationDistance())) //Stop when you get in activation range
actor.getClass().getMovementSettings(actor).mPosition[1] = 0; {
// activate when reached
MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getPtr(mObjectId,false); MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getPtr(mObjectId,false);
MWBase::Environment::get().getWorld()->activate(target, actor); MWBase::Environment::get().getWorld()->activate(target, actor);
return true; return true;
} }
else {
pathTo(actor, dest, duration); //Go to the destination
}
return false; return false;
} }

@ -23,50 +23,10 @@ namespace
{ {
//chooses an attack depending on probability to avoid uniformity //chooses an attack depending on probability to avoid uniformity
ESM::Weapon::AttackType chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement); std::string chooseBestAttack(const ESM::Weapon* weapon);
osg::Vec3f AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const osg::Vec3f& vLastTargetPos, osg::Vec3f AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const osg::Vec3f& vLastTargetPos,
float duration, int weapType, float strength); float duration, int weapType, float strength);
float getZAngleToDir(const osg::Vec3f& dir)
{
return std::atan2(dir.x(), dir.y());
}
float getXAngleToDir(const osg::Vec3f& dir)
{
return -std::asin(dir.z() / dir.length());
}
const float REACTION_INTERVAL = 0.25f;
const float PATHFIND_Z_REACH = 50.0f;
// distance at which actor pays more attention to decide whether to shortcut or stick to pathgrid
const float PATHFIND_CAUTION_DIST = 500.0f;
// distance after which actor (failed previously to shortcut) will try again
const float PATHFIND_SHORTCUT_RETRY_DIST = 300.0f;
// cast up-down ray with some offset from actor position to check for pits/obstacles on the way to target;
// magnitude of pits/obstacles is defined by PATHFIND_Z_REACH
bool checkWayIsClear(const osg::Vec3f& from, const osg::Vec3f& to, float offsetXY)
{
if((to - from).length() >= PATHFIND_CAUTION_DIST || std::abs(from.z() - to.z()) <= PATHFIND_Z_REACH)
{
osg::Vec3f dir = to - from;
dir.z() = 0;
dir.normalize();
float verticalOffset = 200; // instead of '200' here we want the height of the actor
osg::Vec3f _from = from + dir*offsetXY + osg::Vec3f(0,0,1) * verticalOffset;
// cast up-down ray and find height in world space of hit
float h = _from.z() - MWBase::Environment::get().getWorld()->getDistToNearestRayHit(_from, osg::Vec3f(0,0,-1), verticalOffset + PATHFIND_Z_REACH + 1);
if(std::abs(from.z() - h) <= PATHFIND_Z_REACH)
return true;
}
return false;
}
} }
namespace MWMechanics namespace MWMechanics
@ -80,7 +40,7 @@ namespace MWMechanics
float mTimerCombatMove; float mTimerCombatMove;
bool mReadyToAttack; bool mReadyToAttack;
bool mAttack; bool mAttack;
bool mFollowTarget; float mAttackRange;
bool mCombatMove; bool mCombatMove;
osg::Vec3f mLastTargetPos; osg::Vec3f mLastTargetPos;
const MWWorld::CellStore* mCell; const MWWorld::CellStore* mCell;
@ -89,16 +49,15 @@ namespace MWMechanics
float mStrength; float mStrength;
bool mForceNoShortcut; bool mForceNoShortcut;
ESM::Position mShortcutFailPos; ESM::Position mShortcutFailPos;
osg::Vec3f mLastActorPos;
MWMechanics::Movement mMovement; MWMechanics::Movement mMovement;
AiCombatStorage(): AiCombatStorage():
mAttackCooldown(0), mAttackCooldown(0),
mTimerReact(0), mTimerReact(AI_REACTION_TIME),
mTimerCombatMove(0), mTimerCombatMove(0),
mReadyToAttack(false), mReadyToAttack(false),
mAttack(false), mAttack(false),
mFollowTarget(false), mAttackRange(0),
mCombatMove(false), mCombatMove(false),
mLastTargetPos(0,0,0), mLastTargetPos(0,0,0),
mCell(NULL), mCell(NULL),
@ -107,10 +66,10 @@ namespace MWMechanics
mStrength(), mStrength(),
mForceNoShortcut(false), mForceNoShortcut(false),
mShortcutFailPos(), mShortcutFailPos(),
mLastActorPos(0,0,0), mMovement()
mMovement(){} {}
void startCombatMove(bool isNpc, bool isDistantCombat, float distToTarget, float rangeAttack); void startCombatMove(bool isDistantCombat, float distToTarget, float rangeAttack, const MWWorld::Ptr& actor, const MWWorld::Ptr& target);
void updateCombatMove(float duration); void updateCombatMove(float duration);
void stopCombatMove(); void stopCombatMove();
void startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController, void startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController,
@ -179,6 +138,7 @@ namespace MWMechanics
* Use the Observer Pattern to co-ordinate attacks, provide intelligence on * Use the Observer Pattern to co-ordinate attacks, provide intelligence on
* whether the target was hit, etc. * whether the target was hit, etc.
*/ */
bool AiCombat::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration) bool AiCombat::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration)
{ {
// get or create temporary storage // get or create temporary storage
@ -197,34 +157,38 @@ namespace MWMechanics
|| target.getClass().getCreatureStats(target).isDead()) || target.getClass().getCreatureStats(target).isDead())
return true; return true;
//Update every frame if (storage.mCurrentAction.get()) // need to wait to init action with it's attack range
{
//Update every frame
bool is_target_reached = pathTo(actor, target.getRefData().getPosition().pos, duration, storage.mAttackRange);
if (is_target_reached) storage.mReadyToAttack = true;
}
storage.updateCombatMove(duration); storage.updateCombatMove(duration);
updateActorsMovement(actor, duration, storage.mMovement); if (storage.mReadyToAttack) updateActorsMovement(actor, duration, storage);
storage.updateAttack(characterController); storage.updateAttack(characterController);
storage.mActionCooldown -= duration; storage.mActionCooldown -= duration;
float& timerReact = storage.mTimerReact; float& timerReact = storage.mTimerReact;
if(timerReact < REACTION_INTERVAL) if (timerReact < AI_REACTION_TIME)
{ {
timerReact += duration; timerReact += duration;
return false;
} }
else else
{ {
timerReact = 0; timerReact = 0;
return reactionTimeActions(actor, characterController, storage, target); attack(actor, target, storage, characterController);
} }
return false;
} }
bool AiCombat::reactionTimeActions(const MWWorld::Ptr& actor, CharacterController& characterController, void AiCombat::attack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, AiCombatStorage& storage, CharacterController& characterController)
AiCombatStorage& storage, MWWorld::Ptr target)
{ {
MWMechanics::Movement& movement = storage.mMovement;
if (isTargetMagicallyHidden(target)) if (isTargetMagicallyHidden(target))
{ {
storage.stopAttack(); storage.stopAttack();
return false; // TODO: run away instead of doing nothing return; // TODO: run away instead of doing nothing
} }
const MWWorld::CellStore*& currentCell = storage.mCell; const MWWorld::CellStore*& currentCell = storage.mCell;
@ -239,10 +203,9 @@ namespace MWMechanics
float& actionCooldown = storage.mActionCooldown; float& actionCooldown = storage.mActionCooldown;
if (actionCooldown > 0) if (actionCooldown > 0)
return false; return;
float rangeAttack = 0; float &rangeAttack = storage.mAttackRange;
float rangeFollow = 0;
boost::shared_ptr<Action>& currentAction = storage.mCurrentAction; boost::shared_ptr<Action>& currentAction = storage.mCurrentAction;
if (characterController.readyToPrepareAttack()) if (characterController.readyToPrepareAttack())
{ {
@ -250,253 +213,67 @@ namespace MWMechanics
actionCooldown = currentAction->getActionCooldown(); actionCooldown = currentAction->getActionCooldown();
} }
if (currentAction.get())
currentAction->getCombatRange(rangeAttack, rangeFollow);
// FIXME: consider moving this stuff to ActionWeapon::getCombatRange
const ESM::Weapon *weapon = NULL; const ESM::Weapon *weapon = NULL;
MWMechanics::WeaponType weaptype = WeapType_None; bool isRangedCombat = false;
float weapRange = 1.0f; if (currentAction.get())
// Get weapon characteristics
MWBase::World* world = MWBase::Environment::get().getWorld();
static const float fCombatDistance = world->getStore().get<ESM::GameSetting>().find("fCombatDistance")->getFloat();
if (actorClass.hasInventoryStore(actor))
{
//Get weapon range
MWWorld::ContainerStoreIterator weaponSlot =
MWMechanics::getActiveWeapon(actorClass.getCreatureStats(actor), actorClass.getInventoryStore(actor), &weaptype);
if (weaptype == WeapType_HandToHand)
{
static float fHandToHandReach =
world->getStore().get<ESM::GameSetting>().find("fHandToHandReach")->getFloat();
weapRange = fHandToHandReach;
}
else if (weaptype != WeapType_PickProbe && weaptype != WeapType_Spell && weaptype != WeapType_None)
{
// All other WeapTypes are actually weapons, so get<ESM::Weapon> is safe.
weapon = weaponSlot->get<ESM::Weapon>()->mBase;
weapRange = weapon->mData.mReach;
}
weapRange *= fCombatDistance;
}
else //is creature
{
weaptype = actorClass.getCreatureStats(actor).getDrawState() == DrawState_Spell ? WeapType_Spell : WeapType_HandToHand;
weapRange = fCombatDistance;
}
bool distantCombat = false;
if (weaptype != WeapType_Spell)
{
// TODO: move to ActionWeapon
if (weaptype == WeapType_BowAndArrow || weaptype == WeapType_Crossbow || weaptype == WeapType_Thrown)
{
rangeAttack = 1000;
rangeFollow = 0; // not needed in ranged combat
distantCombat = true;
}
else
{
rangeAttack = weapRange;
rangeFollow = 300;
}
}
else
{ {
distantCombat = (rangeAttack > 500); rangeAttack = currentAction->getCombatRange(isRangedCombat);
// Get weapon characteristics
weapon = currentAction->getWeapon();
} }
bool& readyToAttack = storage.mReadyToAttack;
// start new attack
storage.startAttackIfReady(actor, characterController, weapon, distantCombat);
/*
* Some notes on meanings of variables:
*
* rangeAttack:
*
* - Distance where attack using the actor's weapon is possible:
* longer for ranged weapons (obviously?) vs. melee weapons
* - Determined by weapon's reach parameter; hardcoded value
* for ranged weapon and for creatures
* - Once within this distance mFollowTarget is triggered
*
* rangeFollow:
*
* - Applies to melee weapons or hand to hand only (or creatures without
* weapons)
* - Distance a little further away than the actor's weapon reach
* i.e. rangeFollow > rangeAttack for melee weapons
* - Hardcoded value (0 for ranged weapons)
* - Once the target gets beyond this distance mFollowTarget is cleared
* and a path to the target needs to be found
*
* mFollowTarget:
*
* - Once triggered, the actor follows the target with LOS shortcut
* (the shortcut really only applies to cells where pathgrids are
* available, since the default path without pathgrids is direct to
* target even if LOS is not achieved)
*/
ESM::Position pos = actor.getRefData().getPosition(); ESM::Position pos = actor.getRefData().getPosition();
osg::Vec3f vActorPos(pos.asVec3()); osg::Vec3f vActorPos(pos.asVec3());
osg::Vec3f vTargetPos(target.getRefData().getPosition().asVec3()); osg::Vec3f vTargetPos(target.getRefData().getPosition().asVec3());
osg::Vec3f vAimDir = MWBase::Environment::get().getWorld()->aimToTarget(actor, target); osg::Vec3f vAimDir = MWBase::Environment::get().getWorld()->aimToTarget(actor, target);
float distToTarget = MWBase::Environment::get().getWorld()->getHitDistance(actor, target); float distToTarget = MWBase::Environment::get().getWorld()->getHitDistance(actor, target);
osg::Vec3f& lastActorPos = storage.mLastActorPos;
bool& followTarget = storage.mFollowTarget;
bool isStuck = false;
float speed = 0.0f;
if(movement.mPosition[1] && (lastActorPos - vActorPos).length() < (speed = actorClass.getSpeed(actor)) * REACTION_INTERVAL / 2)
isStuck = true;
lastActorPos = vActorPos;
// check if actor can move along z-axis
bool canMoveByZ = (actorClass.canSwim(actor) && world->isSwimming(actor))
|| world->isFlying(actor);
storage.mReadyToAttack = (distToTarget <= rangeAttack);
// can't fight if attacker can't go where target is. E.g. A fish can't attack person on land. // can't fight if attacker can't go where target is. E.g. A fish can't attack person on land.
if (distToTarget >= rangeAttack if (distToTarget > rangeAttack
&& !actorClass.isNpc() && !MWMechanics::isEnvironmentCompatible(actor, target)) && !actorClass.isNpc() && !MWMechanics::isEnvironmentCompatible(actor, target))
{ {
// TODO: start fleeing? // TODO: start fleeing?
storage.stopAttack(); storage.stopAttack();
return false; return;
} }
// for distant combat we should know if target is in LOS even if distToTarget < rangeAttack if (storage.mReadyToAttack)
bool inLOS = distantCombat ? world->getLOS(actor, target) : true;
// (within attack dist) || (not quite attack dist while following)
if(inLOS && (distToTarget < rangeAttack || (distToTarget <= rangeFollow && followTarget && !isStuck)))
{ {
mPathFinder.clearPath(); storage.startCombatMove(isRangedCombat, distToTarget, rangeAttack, actor, target);
//Melee and Close-up combat // start new attack
storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat);
// getXAngleToDir determines vertical angle to target:
// if actor can move along z-axis it will control movement dir if (isRangedCombat)
// if can't - it will control correct aiming.
// note: in getZAngleToDir if we preserve dir.z then horizontal angle can be inaccurate
if (distantCombat)
{ {
// rotate actor taking into account target movement direction and projectile speed
osg::Vec3f& lastTargetPos = storage.mLastTargetPos; osg::Vec3f& lastTargetPos = storage.mLastTargetPos;
vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, REACTION_INTERVAL, weaptype, vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, AI_REACTION_TIME, (weapon ? weapon->mData.mType : 0), storage.mStrength);
storage.mStrength);
lastTargetPos = vTargetPos; lastTargetPos = vTargetPos;
movement.mRotation[0] = getXAngleToDir(vAimDir);
movement.mRotation[2] = getZAngleToDir(vAimDir);
}
else
{
movement.mRotation[0] = getXAngleToDir(vAimDir);
movement.mRotation[2] = getZAngleToDir((vTargetPos-vActorPos)); // using vAimDir results in spastic movements since the head is animated
}
// (not quite attack dist while following)
if (followTarget && distToTarget > rangeAttack)
{
//Close-up combat: just run up on target
storage.stopCombatMove();
movement.mPosition[1] = 1;
}
else // (within attack dist)
{
storage.startCombatMove(actorClass.isNpc(), distantCombat, distToTarget, rangeAttack);
readyToAttack = true; storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir);
//only once got in melee combat, actor is allowed to use close-up shortcutting storage.mMovement.mRotation[2] = getZAngleToDir(vAimDir);
followTarget = true;
} }
} else
else // remote pathfinding
{
bool preferShortcut = false;
if (!distantCombat) inLOS = world->getLOS(actor, target);
// check if shortcut is available
bool& forceNoShortcut = storage.mForceNoShortcut;
ESM::Position& shortcutFailPos = storage.mShortcutFailPos;
if(inLOS && (!isStuck || readyToAttack)
&& (!forceNoShortcut || (shortcutFailPos.asVec3() - vActorPos).length() >= PATHFIND_SHORTCUT_RETRY_DIST))
{
if(speed == 0.0f) speed = actorClass.getSpeed(actor);
// maximum dist before pit/obstacle for actor to avoid them depending on his speed
float maxAvoidDist = REACTION_INTERVAL * speed + speed / MAX_VEL_ANGULAR_RADIANS * 2; // *2 - for reliability
preferShortcut = checkWayIsClear(vActorPos, vTargetPos, osg::Vec3f(vAimDir.x(), vAimDir.y(), 0).length() > maxAvoidDist*1.5? maxAvoidDist : maxAvoidDist/2);
}
// don't use pathgrid when actor can move in 3 dimensions
if (canMoveByZ)
{
preferShortcut = true;
movement.mRotation[0] = getXAngleToDir(vAimDir);
}
if(preferShortcut)
{
movement.mRotation[2] = getZAngleToDir((vTargetPos-vActorPos));
forceNoShortcut = false;
shortcutFailPos.pos[0] = shortcutFailPos.pos[1] = shortcutFailPos.pos[2] = 0;
mPathFinder.clearPath();
}
else // if shortcut failed stick to path grid
{
if(!isStuck && shortcutFailPos.pos[0] == 0.0f && shortcutFailPos.pos[1] == 0.0f && shortcutFailPos.pos[2] == 0.0f)
{
forceNoShortcut = true;
shortcutFailPos = pos;
}
followTarget = false;
buildNewPath(actor, target);
// should always return a path (even if it's just go straight on target.)
assert(mPathFinder.isPathConstructed());
}
if (readyToAttack)
{ {
// to stop possible sideway moving after target moved out of attack range storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir);
storage.stopCombatMove(); storage.mMovement.mRotation[2] = getZAngleToDir((vTargetPos-vActorPos)); // using vAimDir results in spastic movements since the head is animated
readyToAttack = false;
} }
movement.mPosition[1] = 1;
} }
return false;
} }
void AiCombat::updateActorsMovement(const MWWorld::Ptr& actor, float duration, MWMechanics::Movement& desiredMovement) void AiCombat::updateActorsMovement(const MWWorld::Ptr& actor, float duration, AiCombatStorage& storage)
{ {
// apply combat movement
MWMechanics::Movement& actorMovementSettings = actor.getClass().getMovementSettings(actor); MWMechanics::Movement& actorMovementSettings = actor.getClass().getMovementSettings(actor);
if (mPathFinder.isPathConstructed()) actorMovementSettings.mPosition[0] = storage.mMovement.mPosition[0];
{ actorMovementSettings.mPosition[1] = storage.mMovement.mPosition[1];
const ESM::Position& pos = actor.getRefData().getPosition(); actorMovementSettings.mPosition[2] = storage.mMovement.mPosition[2];
if (mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1]))
{ rotateActorOnAxis(actor, 2, actorMovementSettings, storage.mMovement);
actorMovementSettings.mPosition[1] = 0; rotateActorOnAxis(actor, 0, actorMovementSettings, storage.mMovement);
}
else
{
evadeObstacles(actor, duration, pos);
}
}
else
{
actorMovementSettings = desiredMovement;
rotateActorOnAxis(actor, 2, actorMovementSettings, desiredMovement);
rotateActorOnAxis(actor, 0, actorMovementSettings, desiredMovement);
}
} }
void AiCombat::rotateActorOnAxis(const MWWorld::Ptr& actor, int axis, void AiCombat::rotateActorOnAxis(const MWWorld::Ptr& actor, int axis,
@ -514,35 +291,6 @@ namespace MWMechanics
} }
} }
bool AiCombat::doesPathNeedRecalc(ESM::Pathgrid::Point dest, const ESM::Cell *cell)
{
if (!mPathFinder.getPath().empty())
{
osg::Vec3f currPathTarget(PathFinder::MakeOsgVec3(mPathFinder.getPath().back()));
osg::Vec3f newPathTarget = PathFinder::MakeOsgVec3(dest);
float dist = (newPathTarget - currPathTarget).length();
float targetPosThreshold = (cell->isExterior()) ? 300.0f : 100.0f;
return dist > targetPosThreshold;
}
else
{
// necessarily construct a new path
return true;
}
}
void AiCombat::buildNewPath(const MWWorld::Ptr& actor, const MWWorld::Ptr& target)
{
ESM::Pathgrid::Point newPathTarget = PathFinder::MakePathgridPoint(target.getRefData().getPosition());
//construct new path only if target has moved away more than on [targetPosThreshold]
if (doesPathNeedRecalc(newPathTarget, actor.getCell()->getCell()))
{
ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(actor.getRefData().getPosition()));
mPathFinder.buildSyncedPath(start, newPathTarget, actor.getCell(), false);
}
}
int AiCombat::getTypeId() const int AiCombat::getTypeId() const
{ {
return TypeIdCombat; return TypeIdCombat;
@ -558,7 +306,6 @@ namespace MWMechanics
return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId);
} }
AiCombat *MWMechanics::AiCombat::clone() const AiCombat *MWMechanics::AiCombat::clone() const
{ {
return new AiCombat(*this); return new AiCombat(*this);
@ -575,25 +322,49 @@ namespace MWMechanics
sequence.mPackages.push_back(package); sequence.mPackages.push_back(package);
} }
void AiCombatStorage::startCombatMove(bool isNpc, bool isDistantCombat, float distToTarget, float rangeAttack) void AiCombatStorage::startCombatMove(bool isDistantCombat, float distToTarget, float rangeAttack, const MWWorld::Ptr& actor, const MWWorld::Ptr& target)
{ {
if (mMovement.mPosition[0] || mMovement.mPosition[1]) if (mMovement.mPosition[0] || mMovement.mPosition[1])
{ {
mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(); mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability();
mCombatMove = true; mCombatMove = true;
} }
// only NPCs are smart enough to use dodge movements // dodge movements (for NPCs and bipedal creatures)
else if (isNpc && (!isDistantCombat || (distToTarget < rangeAttack / 2))) else if (actor.getClass().isBipedal(actor))
{ {
//apply sideway movement (kind of dodging) with some probability // get the range of the target's weapon
if (Misc::Rng::rollClosedProbability() < 0.25) float rangeAttackOfTarget = 0.f;
bool isRangedCombat = false;
MWWorld::Ptr targetWeapon = MWWorld::Ptr();
const MWWorld::Class& targetClass = target.getClass();
if (targetClass.hasInventoryStore(target))
{
MWMechanics::WeaponType weapType = WeapType_None;
MWWorld::ContainerStoreIterator weaponSlot =
MWMechanics::getActiveWeapon(targetClass.getCreatureStats(target), targetClass.getInventoryStore(target), &weapType);
if (weapType != WeapType_PickProbe && weapType != WeapType_Spell && weapType != WeapType_None && weapType != WeapType_HandToHand)
targetWeapon = *weaponSlot;
}
boost::shared_ptr<Action> targetWeaponAction (new ActionWeapon(targetWeapon));
if (targetWeaponAction.get())
rangeAttackOfTarget = targetWeaponAction->getCombatRange(isRangedCombat);
// apply sideway movement (kind of dodging) with some probability
// if actor is within range of target's weapon
if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability() < 0.25)
{ {
mMovement.mPosition[0] = Misc::Rng::rollProbability() < 0.5 ? 1.0f : -1.0f; 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.05f + 0.15f * Misc::Rng::rollClosedProbability();
mCombatMove = true; mCombatMove = true;
} }
} }
// Original engine behavior seems to be to back up during ranged combat
// according to fCombatDistance or opponent's weapon range, unless opponent
// is also using a ranged weapon
if (isDistantCombat && distToTarget < rangeAttack / 4) if (isDistantCombat && distToTarget < rangeAttack / 4)
{ {
mMovement.mPosition[1] = -1; mMovement.mPosition[1] = -1;
@ -630,7 +401,7 @@ namespace MWMechanics
characterController.setAttackingOrSpell(true); characterController.setAttackingOrSpell(true);
if (!distantCombat) if (!distantCombat)
chooseBestAttack(weapon, mMovement); characterController.setAIAttackType(chooseBestAttack(weapon));
mStrength = Misc::Rng::rollClosedProbability(); mStrength = Misc::Rng::rollClosedProbability();
@ -651,7 +422,7 @@ namespace MWMechanics
mAttackCooldown = std::min(baseDelay + 0.01 * Misc::Rng::roll0to99(), baseDelay + 0.9); mAttackCooldown = std::min(baseDelay + 0.01 * Misc::Rng::roll0to99(), baseDelay + 0.9);
} }
else else
mAttackCooldown -= REACTION_INTERVAL; mAttackCooldown -= AI_REACTION_TIME;
} }
} }
@ -678,32 +449,11 @@ namespace MWMechanics
namespace namespace
{ {
ESM::Weapon::AttackType chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement) std::string chooseBestAttack(const ESM::Weapon* weapon)
{ {
ESM::Weapon::AttackType attackType; std::string attackType;
if (weapon == NULL) if (weapon != NULL)
{
//hand-to-hand deal equal damage for each type
float roll = Misc::Rng::rollClosedProbability();
if(roll <= 0.333f) //side punch
{
movement.mPosition[0] = (Misc::Rng::rollClosedProbability() < 0.5f) ? 1.0f : -1.0f;
movement.mPosition[1] = 0;
attackType = ESM::Weapon::AT_Slash;
}
else if(roll <= 0.666f) //forward punch
{
movement.mPosition[1] = 1;
attackType = ESM::Weapon::AT_Thrust;
}
else
{
movement.mPosition[1] = movement.mPosition[0] = 0;
attackType = ESM::Weapon::AT_Chop;
}
}
else
{ {
//the more damage attackType deals the more probability it has //the more damage attackType deals the more probability it has
int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2; int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2;
@ -712,21 +462,11 @@ ESM::Weapon::AttackType chooseBestAttack(const ESM::Weapon* weapon, MWMechanics:
float roll = Misc::Rng::rollClosedProbability() * (slash + chop + thrust); float roll = Misc::Rng::rollClosedProbability() * (slash + chop + thrust);
if(roll <= slash) if(roll <= slash)
{ attackType = "slash";
movement.mPosition[0] = (Misc::Rng::rollClosedProbability() < 0.5f) ? 1.0f : -1.0f;
movement.mPosition[1] = 0;
attackType = ESM::Weapon::AT_Slash;
}
else if(roll <= (slash + thrust)) else if(roll <= (slash + thrust))
{ attackType = "thrust";
movement.mPosition[1] = 1;
attackType = ESM::Weapon::AT_Thrust;
}
else else
{ attackType = "chop";
movement.mPosition[1] = movement.mPosition[0] = 0;
attackType = ESM::Weapon::AT_Chop;
}
} }
return attackType; return attackType;

@ -55,19 +55,14 @@ namespace MWMechanics
virtual bool canCancel() const { return false; } virtual bool canCancel() const { return false; }
virtual bool shouldCancelPreviousAi() const { return false; } virtual bool shouldCancelPreviousAi() const { return false; }
protected:
virtual bool doesPathNeedRecalc(ESM::Pathgrid::Point dest, const ESM::Cell *cell);
private: private:
int mTargetActorId; int mTargetActorId;
void buildNewPath(const MWWorld::Ptr& actor, const MWWorld::Ptr& target); void attack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, AiCombatStorage& storage, CharacterController& characterController);
bool reactionTimeActions(const MWWorld::Ptr& actor, CharacterController& characterController,
AiCombatStorage& storage, MWWorld::Ptr target);
/// Transfer desired movement (from AiCombatStorage) to Actor /// Transfer desired movement (from AiCombatStorage) to Actor
void updateActorsMovement(const MWWorld::Ptr& actor, float duration, MWMechanics::Movement& movement); void updateActorsMovement(const MWWorld::Ptr& actor, float duration, AiCombatStorage& storage);
void rotateActorOnAxis(const MWWorld::Ptr& actor, int axis, void rotateActorOnAxis(const MWWorld::Ptr& actor, int axis,
MWMechanics::Movement& actorMovementSettings, MWMechanics::Movement& desiredMovement); MWMechanics::Movement& actorMovementSettings, MWMechanics::Movement& desiredMovement);
}; };

@ -40,23 +40,21 @@ int getRangeTypes (const ESM::EffectList& effects)
return types; return types;
} }
void suggestCombatRange(int rangeTypes, float& rangeAttack, float& rangeFollow) float suggestCombatRange(int rangeTypes)
{ {
if (rangeTypes & Touch) if (rangeTypes & Touch)
{ {
rangeAttack = 100.f; static const float fCombatDistance = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fCombatDistance")->getFloat();
rangeFollow = 300.f; return fCombatDistance;
} }
else if (rangeTypes & Target) else if (rangeTypes & Target)
{ {
rangeAttack = 1000.f; return 1000.f;
rangeFollow = 0.f;
} }
else else
{ {
// For Self spells, distance doesn't matter, so back away slightly to avoid enemy hits // For Self spells, distance doesn't matter, so back away slightly to avoid enemy hits
rangeAttack = 600.f; return 600.f;
rangeFollow = 0.f;
} }
} }
@ -427,11 +425,13 @@ namespace MWMechanics
} }
} }
void ActionSpell::getCombatRange(float& rangeAttack, float& rangeFollow) float ActionSpell::getCombatRange (bool& isRanged) const
{ {
const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().find(mSpellId); const ESM::Spell* spell = MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>().find(mSpellId);
int types = getRangeTypes(spell->mEffects); int types = getRangeTypes(spell->mEffects);
suggestCombatRange(types, rangeAttack, rangeFollow);
isRanged = (types & Target);
return suggestCombatRange(types);
} }
void ActionEnchantedItem::prepare(const MWWorld::Ptr &actor) void ActionEnchantedItem::prepare(const MWWorld::Ptr &actor)
@ -441,18 +441,17 @@ namespace MWMechanics
actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Spell); actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Spell);
} }
void ActionEnchantedItem::getCombatRange(float& rangeAttack, float& rangeFollow) float ActionEnchantedItem::getCombatRange(bool& isRanged) const
{ {
const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get<ESM::Enchantment>().find(mItem->getClass().getEnchantment(*mItem)); const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get<ESM::Enchantment>().find(mItem->getClass().getEnchantment(*mItem));
int types = getRangeTypes(enchantment->mEffects); int types = getRangeTypes(enchantment->mEffects);
suggestCombatRange(types, rangeAttack, rangeFollow); return suggestCombatRange(types);
} }
void ActionPotion::getCombatRange(float& rangeAttack, float& rangeFollow) float ActionPotion::getCombatRange(bool& isRanged) const
{ {
// distance doesn't matter, so back away slightly to avoid enemy hits // distance doesn't matter, so back away slightly to avoid enemy hits
rangeAttack = 600.f; return 600.f;
rangeFollow = 0.f;
} }
void ActionPotion::prepare(const MWWorld::Ptr &actor) void ActionPotion::prepare(const MWWorld::Ptr &actor)
@ -482,9 +481,35 @@ namespace MWMechanics
actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Weapon); actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Weapon);
} }
void ActionWeapon::getCombatRange(float& rangeAttack, float& rangeFollow) float ActionWeapon::getCombatRange(bool& isRanged) const
{
isRanged = false;
static const float fCombatDistance = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fCombatDistance")->getFloat();
if (mWeapon.isEmpty())
{
static float fHandToHandReach =
MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fHandToHandReach")->getFloat();
return fHandToHandReach * fCombatDistance;
}
const ESM::Weapon* weapon = mWeapon.get<ESM::Weapon>()->mBase;
if (weapon->mData.mType >= ESM::Weapon::MarksmanBow)
{
isRanged = true;
return 1000.f;
}
else
return weapon->mData.mReach * fCombatDistance;
}
const ESM::Weapon* ActionWeapon::getWeapon() const
{ {
// Already done in AiCombat itself if (mWeapon.isEmpty())
return NULL;
return mWeapon.get<ESM::Weapon>()->mBase;
} }
boost::shared_ptr<Action> prepareNextAction(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy) boost::shared_ptr<Action> prepareNextAction(const MWWorld::Ptr &actor, const MWWorld::Ptr &enemy)

@ -16,8 +16,9 @@ namespace MWMechanics
public: public:
virtual ~Action() {} virtual ~Action() {}
virtual void prepare(const MWWorld::Ptr& actor) = 0; virtual void prepare(const MWWorld::Ptr& actor) = 0;
virtual void getCombatRange (float& rangeAttack, float& rangeFollow) = 0; virtual float getCombatRange (bool& isRanged) const = 0;
virtual float getActionCooldown() { return 0.f; } virtual float getActionCooldown() { return 0.f; }
virtual const ESM::Weapon* getWeapon() const { return NULL; };
}; };
class ActionSpell : public Action class ActionSpell : public Action
@ -28,7 +29,7 @@ namespace MWMechanics
/// Sets the given spell as selected on the actor's spell list. /// Sets the given spell as selected on the actor's spell list.
virtual void prepare(const MWWorld::Ptr& actor); virtual void prepare(const MWWorld::Ptr& actor);
virtual void getCombatRange (float& rangeAttack, float& rangeFollow); virtual float getCombatRange (bool& isRanged) const;
}; };
class ActionEnchantedItem : public Action class ActionEnchantedItem : public Action
@ -38,7 +39,7 @@ namespace MWMechanics
MWWorld::ContainerStoreIterator mItem; MWWorld::ContainerStoreIterator mItem;
/// Sets the given item as selected enchanted item in the actor's InventoryStore. /// Sets the given item as selected enchanted item in the actor's InventoryStore.
virtual void prepare(const MWWorld::Ptr& actor); virtual void prepare(const MWWorld::Ptr& actor);
virtual void getCombatRange (float& rangeAttack, float& rangeFollow); virtual float getCombatRange (bool& isRanged) const;
/// Since this action has no animation, apply a small cool down for using it /// Since this action has no animation, apply a small cool down for using it
virtual float getActionCooldown() { return 1.f; } virtual float getActionCooldown() { return 1.f; }
@ -51,7 +52,7 @@ namespace MWMechanics
MWWorld::Ptr mPotion; MWWorld::Ptr mPotion;
/// Drinks the given potion. /// Drinks the given potion.
virtual void prepare(const MWWorld::Ptr& actor); virtual void prepare(const MWWorld::Ptr& actor);
virtual void getCombatRange (float& rangeAttack, float& rangeFollow); virtual float getCombatRange (bool& isRanged) const;
/// Since this action has no animation, apply a small cool down for using it /// Since this action has no animation, apply a small cool down for using it
virtual float getActionCooldown() { return 1.f; } virtual float getActionCooldown() { return 1.f; }
@ -69,7 +70,8 @@ namespace MWMechanics
: mAmmunition(ammo), mWeapon(weapon) {} : mAmmunition(ammo), mWeapon(weapon) {}
/// Equips the given weapon. /// Equips the given weapon.
virtual void prepare(const MWWorld::Ptr& actor); virtual void prepare(const MWWorld::Ptr& actor);
virtual void getCombatRange (float& rangeAttack, float& rangeFollow); virtual float getCombatRange (bool& isRanged) const;
virtual const ESM::Weapon* getWeapon() const;
}; };
float rateSpell (const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy); float rateSpell (const ESM::Spell* spell, const MWWorld::Ptr& actor, const MWWorld::Ptr& enemy);

@ -137,35 +137,24 @@ bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characte
//Set the target destination from the actor //Set the target destination from the actor
ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos; ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos;
float dist = distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]); if (!storage.mMoving)
if (storage.mMoving) //Stop when you get close
storage.mMoving = (dist > followDistance);
else
{ {
const float threshold = 10; const float threshold = 10; // to avoid constant switching between moving/stopping
storage.mMoving = (dist > followDistance + threshold); followDistance += threshold;
} }
if(!storage.mMoving) storage.mMoving = !pathTo(actor, dest, duration, followDistance); // Go to the destination
{
actor.getClass().getMovementSettings(actor).mPosition[1] = 0;
// turn towards target anyway if (storage.mMoving)
float directionX = target.getRefData().getPosition().pos[0] - actor.getRefData().getPosition().pos[0];
float directionY = target.getRefData().getPosition().pos[1] - actor.getRefData().getPosition().pos[1];
zTurn(actor, std::atan2(directionX,directionY), osg::DegreesToRadians(5.f));
}
else
{ {
pathTo(actor, dest, duration); //Go to the destination //Check if you're far away
} float dist = distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]);
//Check if you're far away if (dist > 450)
if(dist > 450) actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, true); //Make NPC run
actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, true); //Make NPC run else if (dist < 325) //Have a bit of a dead zone, otherwise npc will constantly flip between running and not when right on the edge of the running threshhold
else if(dist < 325) //Have a bit of a dead zone, otherwise npc will constantly flip between running and not when right on the edge of the running threshhold actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, false); //make NPC walk
actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, false); //make NPC walk }
return false; return false;
} }

@ -19,8 +19,18 @@
#include "actorutil.hpp" #include "actorutil.hpp"
#include "coordinateconverter.hpp" #include "coordinateconverter.hpp"
#include <osg/Quat>
MWMechanics::AiPackage::~AiPackage() {} MWMechanics::AiPackage::~AiPackage() {}
MWMechanics::AiPackage::AiPackage() :
mTimer(AI_REACTION_TIME + 1.0f), // to force initial pathbuild
mRotateOnTheRunChecks(0),
mIsShortcutting(false),
mShortcutProhibited(false), mShortcutFailPos()
{
}
MWWorld::Ptr MWMechanics::AiPackage::getTarget() const MWWorld::Ptr MWMechanics::AiPackage::getTarget() const
{ {
return MWWorld::Ptr(); return MWWorld::Ptr();
@ -51,14 +61,20 @@ bool MWMechanics::AiPackage::getRepeat() const
return false; return false;
} }
MWMechanics::AiPackage::AiPackage() : mTimer(0.26f) { //mTimer starts at .26 to force initial pathbuild void MWMechanics::AiPackage::reset()
{
// reset all members
mTimer = AI_REACTION_TIME + 1.0f;
mIsShortcutting = false;
mShortcutProhibited = false;
mShortcutFailPos = ESM::Pathgrid::Point();
mPathFinder.clearPath();
mObstacleCheck.clear();
} }
bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest, float duration, float destTolerance)
bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, ESM::Pathgrid::Point dest, float duration)
{ {
//Update various Timers
mTimer += duration; //Update timer mTimer += duration; //Update timer
ESM::Position pos = actor.getRefData().getPosition(); //position of the actor ESM::Position pos = actor.getRefData().getPosition(); //position of the actor
@ -73,42 +89,79 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, ESM::Pathgrid::Po
return false; return false;
} }
//*********************** // handle path building and shortcutting
/// Checks if you can't get to the end position at all, adds end position to end of path ESM::Pathgrid::Point start = pos.pos;
/// Rebuilds path every quarter of a second, in case the target has moved
//*********************** float distToTarget = distance(start, dest);
if(mTimer > 0.25) bool isDestReached = (distToTarget <= destTolerance);
if (!isDestReached && mTimer > AI_REACTION_TIME)
{ {
const ESM::Cell *cell = actor.getCell()->getCell(); bool wasShortcutting = mIsShortcutting;
if (doesPathNeedRecalc(dest, cell)) { //Only rebuild path if it's moved bool destInLOS = false;
mPathFinder.buildSyncedPath(pos.pos, dest, actor.getCell(), true); //Rebuild path, in case the target has moved if (getTypeId() != TypeIdWander) // prohibit shortcuts for AiWander
mPrevDest = dest; mIsShortcutting = shortcutPath(start, dest, actor, &destInLOS); // try to shortcut first
}
if(!mPathFinder.getPath().empty()) //Path has points in it if (!mIsShortcutting)
{ {
ESM::Pathgrid::Point lastPos = mPathFinder.getPath().back(); //Get the end of the proposed path if (wasShortcutting || doesPathNeedRecalc(dest, actor.getCell())) // if need to rebuild path
{
mPathFinder.buildSyncedPath(start, dest, actor.getCell());
mRotateOnTheRunChecks = 3;
// give priority to go directly on target if there is minimal opportunity
if (destInLOS && mPathFinder.getPath().size() > 1)
{
// get point just before dest
std::list<ESM::Pathgrid::Point>::const_iterator pPointBeforeDest = mPathFinder.getPath().end();
--pPointBeforeDest;
--pPointBeforeDest;
// if start point is closer to the target then last point of path (excluding target itself) then go straight on the target
if (distance(start, dest) <= distance(dest, *pPointBeforeDest))
{
mPathFinder.clearPath();
mPathFinder.addPointToPath(dest);
}
}
}
if(distance(dest, lastPos) > 100) //End of the path is far from the destination if (!mPathFinder.getPath().empty()) //Path has points in it
mPathFinder.addPointToPath(dest); //Adds the final destination to the path, to try to get to where you want to go {
ESM::Pathgrid::Point lastPos = mPathFinder.getPath().back(); //Get the end of the proposed path
if(distance(dest, lastPos) > 100) //End of the path is far from the destination
mPathFinder.addPointToPath(dest); //Adds the final destination to the path, to try to get to where you want to go
}
} }
mTimer = 0; mTimer = 0;
} }
//************************ if (isDestReached || mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1])) // if path is finished
/// Checks if you aren't moving; attempts to unstick you
//************************
if(mPathFinder.checkPathCompleted(pos.pos[0],pos.pos[1])) //Path finished?
{ {
// Reset mTimer so that path will be built right away when a package is repeated // turn to destination point
mTimer = 0.26f; zTurn(actor, getZAngleToPoint(start, dest));
smoothTurn(actor, getXAngleToPoint(start, dest), 0);
return true; return true;
} }
else else
{ {
if (mRotateOnTheRunChecks == 0
|| isReachableRotatingOnTheRun(actor, *mPathFinder.getPath().begin())) // to prevent circling around a path point
{
actor.getClass().getMovementSettings(actor).mPosition[1] = 1; // move to the target
if (mRotateOnTheRunChecks > 0) mRotateOnTheRunChecks--;
}
// handle obstacles on the way
evadeObstacles(actor, duration, pos); evadeObstacles(actor, duration, pos);
} }
// turn to next path point by X,Z axes
zTurn(actor, mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]));
smoothTurn(actor, mPathFinder.getXAngleToNext(pos.pos[0], pos.pos[1], pos.pos[2]), 0);
return false; return false;
} }
@ -117,30 +170,106 @@ void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor, float dur
zTurn(actor, mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1])); zTurn(actor, mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]));
MWMechanics::Movement& movement = actor.getClass().getMovementSettings(actor); MWMechanics::Movement& movement = actor.getClass().getMovementSettings(actor);
if (mObstacleCheck.check(actor, duration))
// check if stuck due to obstacles
if (!mObstacleCheck.check(actor, duration)) return;
// first check if obstacle is a door
MWWorld::Ptr door = getNearbyDoor(actor); // NOTE: checks interior cells only
if (door != MWWorld::Ptr())
{ {
// first check if we're walking into a door // note: AiWander currently does not open doors
MWWorld::Ptr door = getNearbyDoor(actor); if (getTypeId() != TypeIdWander && !door.getCellRef().getTeleport() && door.getCellRef().getTrap().empty()
if (door != MWWorld::Ptr()) // NOTE: checks interior cells only && door.getCellRef().getLockLevel() <= 0 && door.getClass().getDoorState(door) == 0)
{ {
if (!door.getCellRef().getTeleport() && door.getCellRef().getTrap().empty() MWBase::Environment::get().getWorld()->activateDoor(door, 1);
&& door.getCellRef().getLockLevel() <= 0 && door.getClass().getDoorState(door) == 0) {
MWBase::Environment::get().getWorld()->activateDoor(door, 1);
}
} }
else // probably walking into another NPC }
else // any other obstacle (NPC, crate, etc.)
{
mObstacleCheck.takeEvasiveAction(movement);
}
}
bool MWMechanics::AiPackage::shortcutPath(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor, bool *destInLOS)
{
const MWWorld::Class& actorClass = actor.getClass();
MWBase::World* world = MWBase::Environment::get().getWorld();
// check if actor can move along z-axis
bool actorCanMoveByZ = (actorClass.canSwim(actor) && MWBase::Environment::get().getWorld()->isSwimming(actor))
|| world->isFlying(actor);
// don't use pathgrid when actor can move in 3 dimensions
bool isPathClear = actorCanMoveByZ;
if (!isPathClear
&& (!mShortcutProhibited || (PathFinder::MakeOsgVec3(mShortcutFailPos) - PathFinder::MakeOsgVec3(startPoint)).length() >= PATHFIND_SHORTCUT_RETRY_DIST))
{
// check if target is clearly visible
isPathClear = !MWBase::Environment::get().getWorld()->castRay(
static_cast<float>(startPoint.mX), static_cast<float>(startPoint.mY), static_cast<float>(startPoint.mZ),
static_cast<float>(endPoint.mX), static_cast<float>(endPoint.mY), static_cast<float>(endPoint.mZ));
if (destInLOS != NULL) *destInLOS = isPathClear;
if (!isPathClear)
return false;
// check if an actor can move along the shortcut path
isPathClear = checkWayIsClearForActor(startPoint, endPoint, actor);
}
if (isPathClear) // can shortcut the path
{
mPathFinder.clearPath();
mPathFinder.addPointToPath(endPoint);
return true;
}
return false;
}
bool MWMechanics::AiPackage::checkWayIsClearForActor(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor)
{
bool actorCanMoveByZ = (actor.getClass().canSwim(actor) && MWBase::Environment::get().getWorld()->isSwimming(actor))
|| MWBase::Environment::get().getWorld()->isFlying(actor);
if (actorCanMoveByZ)
return true;
float actorSpeed = actor.getClass().getSpeed(actor);
float maxAvoidDist = AI_REACTION_TIME * actorSpeed + actorSpeed / MAX_VEL_ANGULAR_RADIANS * 2; // *2 - for reliability
osg::Vec3f::value_type distToTarget = osg::Vec3f(static_cast<float>(endPoint.mX), static_cast<float>(endPoint.mY), 0).length();
float offsetXY = distToTarget > maxAvoidDist*1.5? maxAvoidDist : maxAvoidDist/2;
bool isClear = checkWayIsClear(PathFinder::MakeOsgVec3(startPoint), PathFinder::MakeOsgVec3(endPoint), offsetXY);
// update shortcut prohibit state
if (isClear)
{
if (mShortcutProhibited)
{ {
mObstacleCheck.takeEvasiveAction(movement); mShortcutProhibited = false;
mShortcutFailPos = ESM::Pathgrid::Point();
} }
} }
else { //Not stuck, so reset things if (!isClear)
movement.mPosition[1] = 1; //Just run forward {
if (mShortcutFailPos.mX == 0 && mShortcutFailPos.mY == 0 && mShortcutFailPos.mZ == 0)
{
mShortcutProhibited = true;
mShortcutFailPos = startPoint;
}
} }
return isClear;
} }
bool MWMechanics::AiPackage::doesPathNeedRecalc(ESM::Pathgrid::Point dest, const ESM::Cell *cell) bool MWMechanics::AiPackage::doesPathNeedRecalc(const ESM::Pathgrid::Point& newDest, const MWWorld::CellStore* currentCell)
{ {
return mPathFinder.getPath().empty() || (distance(mPrevDest, dest) > 10); return mPathFinder.getPath().empty() || (distance(mPathFinder.getPath().back(), newDest) > 10) || mPathFinder.getPathCell() != currentCell;
} }
bool MWMechanics::AiPackage::isTargetMagicallyHidden(const MWWorld::Ptr& target) bool MWMechanics::AiPackage::isTargetMagicallyHidden(const MWWorld::Ptr& target)
@ -173,3 +302,32 @@ bool MWMechanics::AiPackage::isNearInactiveCell(const ESM::Position& actorPos)
return false; return false;
} }
} }
bool MWMechanics::AiPackage::isReachableRotatingOnTheRun(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest)
{
// get actor's shortest radius for moving in circle
float speed = actor.getClass().getSpeed(actor);
speed += speed * 0.1f; // 10% real speed inaccuracy
float radius = speed / MAX_VEL_ANGULAR_RADIANS;
// get radius direction to the center
const float* rot = actor.getRefData().getPosition().rot;
osg::Quat quatRot(rot[0], -osg::X_AXIS, rot[1], -osg::Y_AXIS, rot[2], -osg::Z_AXIS);
osg::Vec3f dir = quatRot * osg::Y_AXIS; // actor's orientation direction is a tangent to circle
osg::Vec3f radiusDir = dir ^ osg::Z_AXIS; // radius is perpendicular to a tangent
radiusDir.normalize();
radiusDir *= radius;
// pick up the nearest center candidate
osg::Vec3f dest_ = PathFinder::MakeOsgVec3(dest);
osg::Vec3f pos = actor.getRefData().getPosition().asVec3();
osg::Vec3f center1 = pos - radiusDir;
osg::Vec3f center2 = pos + radiusDir;
osg::Vec3f center = (center1 - dest_).length2() < (center2 - dest_).length2() ? center1 : center2;
float distToDest = (center - dest_).length();
// if pathpoint is reachable for the actor rotating on the run:
// no points of actor's circle should be farther from the center than destination point
return (radius <= distToDest);
}

@ -24,6 +24,7 @@ namespace ESM
namespace MWMechanics namespace MWMechanics
{ {
const float AI_REACTION_TIME = 0.25f;
class CharacterController; class CharacterController;
@ -91,14 +92,29 @@ namespace MWMechanics
/// Return true if this package should repeat. Currently only used for Wander packages. /// Return true if this package should repeat. Currently only used for Wander packages.
virtual bool getRepeat() const; virtual bool getRepeat() const;
/// Reset pathfinding state
void reset();
bool isTargetMagicallyHidden(const MWWorld::Ptr& target); bool isTargetMagicallyHidden(const MWWorld::Ptr& target);
/// Return if actor's rotation speed is sufficient to rotate to the destination pathpoint on the run. Otherwise actor should rotate while standing.
static bool isReachableRotatingOnTheRun(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest);
protected: protected:
/// Causes the actor to attempt to walk to the specified location /// Handles path building and shortcutting with obstacles avoiding
/** \return If the actor has arrived at his destination **/ /** \return If the actor has arrived at his destination **/
bool pathTo(const MWWorld::Ptr& actor, ESM::Pathgrid::Point dest, float duration); bool pathTo(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest, float duration, float destTolerance = 0.0f);
/// Check if there aren't any obstacles along the path to make shortcut possible
/// If a shortcut is possible then path will be cleared and filled with the destination point.
/// \param destInLOS If not NULL function will return ray cast check result
/// \return If can shortcut the path
bool shortcutPath(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor, bool *destInLOS);
/// Check if the way to the destination is clear, taking into account actor speed
bool checkWayIsClearForActor(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor);
virtual bool doesPathNeedRecalc(ESM::Pathgrid::Point dest, const ESM::Cell *cell); virtual bool doesPathNeedRecalc(const ESM::Pathgrid::Point& newDest, const MWWorld::CellStore* currentCell);
void evadeObstacles(const MWWorld::Ptr& actor, float duration, const ESM::Position& pos); void evadeObstacles(const MWWorld::Ptr& actor, float duration, const ESM::Position& pos);
@ -108,11 +124,16 @@ namespace MWMechanics
float mTimer; float mTimer;
ESM::Pathgrid::Point mPrevDest; osg::Vec3f mLastActorPos;
short mRotateOnTheRunChecks; // attempts to check rotation to the pathpoint on the run possibility
bool mIsShortcutting; // if shortcutting at the moment
bool mShortcutProhibited; // shortcutting may be prohibited after unsuccessful attempt
ESM::Pathgrid::Point mShortcutFailPos; // position of last shortcut fail
private: private:
bool isNearInactiveCell(const ESM::Position& actorPos); bool isNearInactiveCell(const ESM::Position& actorPos);
}; };
} }

@ -33,7 +33,6 @@ bool AiPursue::execute (const MWWorld::Ptr& actor, CharacterController& characte
if(actor.getClass().getCreatureStats(actor).isDead()) if(actor.getClass().getCreatureStats(actor).isDead())
return true; return true;
ESM::Position pos = actor.getRefData().getPosition(); //position of the actor
const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); //The target to follow const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); //The target to follow
if(target == MWWorld::Ptr() || !target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently registered if(target == MWWorld::Ptr() || !target.getRefData().getCount() || !target.getRefData().isEnabled() // Really we should be checking whether the target is currently registered
@ -52,14 +51,10 @@ bool AiPursue::execute (const MWWorld::Ptr& actor, CharacterController& characte
//Set the target desition from the actor //Set the target desition from the actor
ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos; ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos;
if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) < 100) { //Stop when you get close if (pathTo(actor, dest, duration, 100)) {
actor.getClass().getMovementSettings(actor).mPosition[1] = 0; target.getClass().activate(target,actor).get()->execute(actor); //Arrest player when reached
target.getClass().activate(target,actor).get()->execute(actor); //Arrest player
return true; return true;
} }
else {
pathTo(actor, dest, duration); //Go to the destination
}
actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, true); //Make NPC run actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, true); //Make NPC run

@ -234,6 +234,7 @@ void AiSequence::execute (const MWWorld::Ptr& actor, CharacterController& charac
// Put repeating noncombat AI packages on the end of the stack so they can be used again // Put repeating noncombat AI packages on the end of the stack so they can be used again
if (isActualAiPackage(packageTypeId) && (mRepeat || package->getRepeat())) if (isActualAiPackage(packageTypeId) && (mRepeat || package->getRepeat()))
{ {
package->reset();
mPackages.push_back(package->clone()); mPackages.push_back(package->clone());
} }
// To account for the rare case where AiPackage::execute() queued another AI package // To account for the rare case where AiPackage::execute() queued another AI package

@ -30,15 +30,11 @@ namespace MWMechanics
{ {
AiTravel::AiTravel(float x, float y, float z) AiTravel::AiTravel(float x, float y, float z)
: mX(x),mY(y),mZ(z) : mX(x),mY(y),mZ(z)
, mCellX(std::numeric_limits<int>::max())
, mCellY(std::numeric_limits<int>::max())
{ {
} }
AiTravel::AiTravel(const ESM::AiSequence::AiTravel *travel) AiTravel::AiTravel(const ESM::AiSequence::AiTravel *travel)
: mX(travel->mData.mX), mY(travel->mData.mY), mZ(travel->mData.mZ) : mX(travel->mData.mX), mY(travel->mData.mY), mZ(travel->mData.mZ)
, mCellX(std::numeric_limits<int>::max())
, mCellY(std::numeric_limits<int>::max())
{ {
} }
@ -66,18 +62,6 @@ namespace MWMechanics
return false; return false;
} }
bool AiTravel::doesPathNeedRecalc(ESM::Pathgrid::Point dest, const ESM::Cell *cell)
{
bool cellChange = cell->mData.mX != mCellX || cell->mData.mY != mCellY;
if (!mPathFinder.isPathConstructed() || cellChange)
{
mCellX = cell->mData.mX;
mCellY = cell->mData.mY;
return true;
}
return false;
}
int AiTravel::getTypeId() const int AiTravel::getTypeId() const
{ {
return TypeIdTravel; return TypeIdTravel;

@ -34,17 +34,10 @@ namespace MWMechanics
virtual int getTypeId() const; virtual int getTypeId() const;
protected:
virtual bool doesPathNeedRecalc(ESM::Pathgrid::Point dest, const ESM::Cell *cell);
private: private:
float mX; float mX;
float mY; float mY;
float mZ; float mZ;
int mCellX;
int mCellY;
}; };
} }

@ -29,7 +29,6 @@ namespace MWMechanics
{ {
static const int COUNT_BEFORE_RESET = 10; static const int COUNT_BEFORE_RESET = 10;
static const float DOOR_CHECK_INTERVAL = 1.5f; static const float DOOR_CHECK_INTERVAL = 1.5f;
static const float REACTION_INTERVAL = 0.25f;
static const int GREETING_SHOULD_START = 4; //how many reaction intervals should pass before NPC can greet player static const int GREETING_SHOULD_START = 4; //how many reaction intervals should pass before NPC can greet player
static const int GREETING_SHOULD_END = 10; static const int GREETING_SHOULD_END = 10;
@ -74,8 +73,6 @@ namespace MWMechanics
unsigned short mIdleAnimation; unsigned short mIdleAnimation;
std::vector<unsigned short> mBadIdles; // Idle animations that when called cause errors std::vector<unsigned short> mBadIdles; // Idle animations that when called cause errors
PathFinder mPathFinder;
// do we need to calculate allowed nodes based on mDistance // do we need to calculate allowed nodes based on mDistance
bool mPopulateAvailableNodes; bool mPopulateAvailableNodes;
@ -86,8 +83,6 @@ namespace MWMechanics
ESM::Pathgrid::Point mCurrentNode; ESM::Pathgrid::Point mCurrentNode;
bool mTrimCurrentNode; bool mTrimCurrentNode;
ObstacleCheck mObstacleCheck;
float mDoorCheckDuration; float mDoorCheckDuration;
int mStuckCount; int mStuckCount;
@ -196,7 +191,6 @@ namespace MWMechanics
{ {
// get or create temporary storage // get or create temporary storage
AiWanderStorage& storage = state.get<AiWanderStorage>(); AiWanderStorage& storage = state.get<AiWanderStorage>();
const MWWorld::CellStore*& currentCell = storage.mCell; const MWWorld::CellStore*& currentCell = storage.mCell;
MWMechanics::CreatureStats& cStats = actor.getClass().getCreatureStats(actor); MWMechanics::CreatureStats& cStats = actor.getClass().getCreatureStats(actor);
@ -206,6 +200,7 @@ namespace MWMechanics
bool cellChange = currentCell && (actor.getCell() != currentCell); bool cellChange = currentCell && (actor.getCell() != currentCell);
if(!currentCell || cellChange) if(!currentCell || cellChange)
{ {
stopWalking(actor, storage);
currentCell = actor.getCell(); currentCell = actor.getCell();
storage.mPopulateAvailableNodes = true; storage.mPopulateAvailableNodes = true;
} }
@ -223,7 +218,7 @@ namespace MWMechanics
float& lastReaction = storage.mReaction; float& lastReaction = storage.mReaction;
lastReaction += duration; lastReaction += duration;
if (REACTION_INTERVAL <= lastReaction) if (AI_REACTION_TIME <= lastReaction)
{ {
lastReaction = 0; lastReaction = 0;
return reactionTimeActions(actor, storage, currentCell, cellChange, pos, duration); return reactionTimeActions(actor, storage, currentCell, cellChange, pos, duration);
@ -273,7 +268,7 @@ namespace MWMechanics
} }
// If Wandering manually and hit an obstacle, stop // If Wandering manually and hit an obstacle, stop
if (storage.mIsWanderingManually && storage.mObstacleCheck.check(actor, duration, 2.0f)) { if (storage.mIsWanderingManually && mObstacleCheck.check(actor, duration, 2.0f)) {
completeManualWalking(actor, storage); completeManualWalking(actor, storage);
} }
@ -300,14 +295,14 @@ namespace MWMechanics
if ((wanderState == Wander_MoveNow) && storage.mCanWanderAlongPathGrid) if ((wanderState == Wander_MoveNow) && storage.mCanWanderAlongPathGrid)
{ {
// Construct a new path if there isn't one // Construct a new path if there isn't one
if(!storage.mPathFinder.isPathConstructed()) if(!mPathFinder.isPathConstructed())
{ {
if (!storage.mAllowedNodes.empty()) if (!storage.mAllowedNodes.empty())
{ {
setPathToAnAllowedNode(actor, storage, pos); setPathToAnAllowedNode(actor, storage, pos);
} }
} }
} else if (storage.mIsWanderingManually && storage.mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], DESTINATION_TOLERANCE)) { } else if (storage.mIsWanderingManually && mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], DESTINATION_TOLERANCE)) {
completeManualWalking(actor, storage); completeManualWalking(actor, storage);
} }
@ -337,7 +332,7 @@ namespace MWMechanics
void AiWander::returnToStartLocation(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos) void AiWander::returnToStartLocation(const MWWorld::Ptr& actor, AiWanderStorage& storage, ESM::Position& pos)
{ {
if (!storage.mPathFinder.isPathConstructed()) if (!mPathFinder.isPathConstructed())
{ {
ESM::Pathgrid::Point dest(PathFinder::MakePathgridPoint(mReturnPosition)); ESM::Pathgrid::Point dest(PathFinder::MakePathgridPoint(mReturnPosition));
@ -345,9 +340,9 @@ namespace MWMechanics
ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(pos)); ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(pos));
// don't take shortcuts for wandering // don't take shortcuts for wandering
storage.mPathFinder.buildSyncedPath(start, dest, actor.getCell(), false); mPathFinder.buildSyncedPath(start, dest, actor.getCell());
if (storage.mPathFinder.isPathConstructed()) if (mPathFinder.isPathConstructed())
{ {
storage.setState(Wander_Walking); storage.setState(Wander_Walking);
} }
@ -379,9 +374,13 @@ namespace MWMechanics
// Check if land creature will walk onto water or if water creature will swim onto land // Check if land creature will walk onto water or if water creature will swim onto land
if ((!isWaterCreature && !destinationIsAtWater(actor, destination)) || if ((!isWaterCreature && !destinationIsAtWater(actor, destination)) ||
(isWaterCreature && !destinationThroughGround(currentPositionVec3f, destination))) { (isWaterCreature && !destinationThroughGround(currentPositionVec3f, destination))) {
storage.mPathFinder.buildSyncedPath(currentPosition, destinationPosition, actor.getCell(), true); mPathFinder.buildSyncedPath(currentPosition, destinationPosition, actor.getCell());
storage.mPathFinder.addPointToPath(destinationPosition); mPathFinder.addPointToPath(destinationPosition);
storage.setState(Wander_Walking, true);
if (mPathFinder.isPathConstructed())
{
storage.setState(Wander_Walking, true);
}
return; return;
} }
} while (--attempts); } while (--attempts);
@ -407,7 +406,7 @@ namespace MWMechanics
void AiWander::completeManualWalking(const MWWorld::Ptr &actor, AiWanderStorage &storage) { void AiWander::completeManualWalking(const MWWorld::Ptr &actor, AiWanderStorage &storage) {
stopWalking(actor, storage); stopWalking(actor, storage);
storage.mObstacleCheck.clear(); mObstacleCheck.clear();
storage.setState(Wander_IdleNow); storage.setState(Wander_IdleNow);
} }
@ -475,7 +474,7 @@ namespace MWMechanics
float duration, AiWanderStorage& storage, ESM::Position& pos) float duration, AiWanderStorage& storage, ESM::Position& pos)
{ {
// Are we there yet? // Are we there yet?
if (storage.mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], DESTINATION_TOLERANCE)) if (pathTo(actor, mPathFinder.getPath().back(), duration, DESTINATION_TOLERANCE))
{ {
stopWalking(actor, storage); stopWalking(actor, storage);
storage.setState(Wander_ChooseAction); storage.setState(Wander_ChooseAction);
@ -517,40 +516,27 @@ namespace MWMechanics
void AiWander::evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage, float duration, ESM::Position& pos) void AiWander::evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage, float duration, ESM::Position& pos)
{ {
// turn towards the next point in mPath if (mObstacleCheck.isEvading())
zTurn(actor, storage.mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]));
MWMechanics::Movement& movement = actor.getClass().getMovementSettings(actor);
if (storage.mObstacleCheck.check(actor, duration))
{ {
// first check if we're walking into a door // first check if we're walking into a door
if (proximityToDoor(actor)) // NOTE: checks interior cells only if (proximityToDoor(actor)) // NOTE: checks interior cells only
{ {
// remove allowed points then select another random destination // remove allowed points then select another random destination
storage.mTrimCurrentNode = true; storage.mTrimCurrentNode = true;
trimAllowedNodes(storage.mAllowedNodes, storage.mPathFinder); trimAllowedNodes(storage.mAllowedNodes, mPathFinder);
storage.mObstacleCheck.clear(); mObstacleCheck.clear();
storage.mPathFinder.clearPath(); mPathFinder.clearPath();
storage.setState(Wander_MoveNow); storage.setState(Wander_MoveNow);
} }
else // probably walking into another NPC
{ storage.mStuckCount++; // TODO: maybe no longer needed
// TODO: diagonal should have same animation as walk forward
// but doesn't seem to do that?
storage.mObstacleCheck.takeEvasiveAction(movement);
}
storage.mStuckCount++; // TODO: maybe no longer needed
}
else
{
movement.mPosition[1] = 1;
} }
// if stuck for sufficiently long, act like current location was the destination // if stuck for sufficiently long, act like current location was the destination
if (storage.mStuckCount >= COUNT_BEFORE_RESET) // something has gone wrong, reset if (storage.mStuckCount >= COUNT_BEFORE_RESET) // something has gone wrong, reset
{ {
//std::cout << "Reset \""<< cls.getName(actor) << "\"" << std::endl; //std::cout << "Reset \""<< cls.getName(actor) << "\"" << std::endl;
storage.mObstacleCheck.clear(); mObstacleCheck.clear();
stopWalking(actor, storage); stopWalking(actor, storage);
storage.setState(Wander_ChooseAction); storage.setState(Wander_ChooseAction);
@ -627,7 +613,7 @@ namespace MWMechanics
if (storage.mState == Wander_Walking) if (storage.mState == Wander_Walking)
{ {
stopWalking(actor, storage); stopWalking(actor, storage);
storage.mObstacleCheck.clear(); mObstacleCheck.clear();
storage.setState(Wander_IdleNow); storage.setState(Wander_IdleNow);
} }
@ -667,9 +653,9 @@ namespace MWMechanics
ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(actorPos)); ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(actorPos));
// don't take shortcuts for wandering // don't take shortcuts for wandering
storage.mPathFinder.buildSyncedPath(start, dest, actor.getCell(), false); mPathFinder.buildSyncedPath(start, dest, actor.getCell());
if (storage.mPathFinder.isPathConstructed()) if (mPathFinder.isPathConstructed())
{ {
// Remove this node as an option and add back the previously used node (stops NPC from picking the same node): // Remove this node as an option and add back the previously used node (stops NPC from picking the same node):
ESM::Pathgrid::Point temp = storage.mAllowedNodes[randNode]; ESM::Pathgrid::Point temp = storage.mAllowedNodes[randNode];
@ -726,7 +712,7 @@ namespace MWMechanics
void AiWander::stopWalking(const MWWorld::Ptr& actor, AiWanderStorage& storage) void AiWander::stopWalking(const MWWorld::Ptr& actor, AiWanderStorage& storage)
{ {
storage.mPathFinder.clearPath(); mPathFinder.clearPath();
actor.getClass().getMovementSettings(actor).mPosition[1] = 0; actor.getClass().getMovementSettings(actor).mPosition[1] = 0;
} }

@ -109,7 +109,7 @@ namespace MWMechanics
bool mStoredInitialActorPosition; bool mStoredInitialActorPosition;
void getAllowedNodes(const MWWorld::Ptr& actor, const ESM::Cell* cell, AiWanderStorage& storage); void getAllowedNodes(const MWWorld::Ptr& actor, const ESM::Cell* cell, AiWanderStorage& storage);
void trimAllowedNodes(std::vector<ESM::Pathgrid::Point>& nodes, const PathFinder& pathfinder); void trimAllowedNodes(std::vector<ESM::Pathgrid::Point>& nodes, const PathFinder& pathfinder);
// constants for converting idleSelect values into groupNames // constants for converting idleSelect values into groupNames

@ -1208,7 +1208,6 @@ bool CharacterController::updateWeaponState()
if(mUpperBodyState == UpperCharState_WeapEquiped && (mHitState == CharState_None || mHitState == CharState_Block)) if(mUpperBodyState == UpperCharState_WeapEquiped && (mHitState == CharState_None || mHitState == CharState_Block))
{ {
MWBase::Environment::get().getWorld()->breakInvisibility(mPtr); MWBase::Environment::get().getWorld()->breakInvisibility(mPtr);
mAttackType.clear();
if(mWeaponType == WeapType_Spell) if(mWeaponType == WeapType_Spell)
{ {
// Unset casting flag, otherwise pressing the mouse button down would // Unset casting flag, otherwise pressing the mouse button down would
@ -1237,19 +1236,27 @@ bool CharacterController::updateWeaponState()
cast.playSpellCastingEffects(spellid); cast.playSpellCastingEffects(spellid);
const ESM::Spell *spell = store.get<ESM::Spell>().find(spellid); const ESM::Spell *spell = store.get<ESM::Spell>().find(spellid);
const ESM::ENAMstruct &effectentry = spell->mEffects.mList.at(0);
const ESM::ENAMstruct &lastEffect = spell->mEffects.mList.at(spell->mEffects.mList.size() - 1);
const ESM::MagicEffect *effect; const ESM::MagicEffect *effect;
effect = store.get<ESM::MagicEffect>().find(effectentry.mEffectID);
effect = store.get<ESM::MagicEffect>().find(lastEffect.mEffectID); // use last effect of list for color of VFX_Hands
const ESM::Static* castStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find ("VFX_Hands"); const ESM::Static* castStatic = MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().find ("VFX_Hands");
if (mAnimation->getNode("Bip01 L Hand"))
mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 L Hand", effect->mParticle);
if (mAnimation->getNode("Bip01 R Hand")) for (size_t iter = 0; iter < spell->mEffects.mList.size(); ++iter) // play hands vfx for each effect
mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 R Hand", effect->mParticle); {
if (mAnimation->getNode("Bip01 L Hand"))
mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 L Hand", effect->mParticle);
if (mAnimation->getNode("Bip01 R Hand"))
mAnimation->addEffect("meshes\\" + castStatic->mModel, -1, false, "Bip01 R Hand", effect->mParticle);
}
const ESM::ENAMstruct &firstEffect = spell->mEffects.mList.at(0); // first effect used for casting animation
switch(effectentry.mRange) switch(firstEffect.mRange)
{ {
case 0: mAttackType = "self"; break; case 0: mAttackType = "self"; break;
case 1: mAttackType = "touch"; break; case 1: mAttackType = "touch"; break;
@ -1309,14 +1316,17 @@ bool CharacterController::updateWeaponState()
{ {
if (isWeapon) if (isWeapon)
{ {
if(mPtr == getPlayer() && if(mPtr == getPlayer())
Settings::Manager::getBool("best attack", "Game"))
{ {
MWWorld::ContainerStoreIterator weapon = mPtr.getClass().getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight); if (Settings::Manager::getBool("best attack", "Game"))
mAttackType = getBestAttack(weapon->get<ESM::Weapon>()->mBase); {
MWWorld::ContainerStoreIterator weapon = mPtr.getClass().getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight);
mAttackType = getBestAttack(weapon->get<ESM::Weapon>()->mBase);
}
else
setAttackTypeBasedOnMovement();
} }
else // else if (mPtr != getPlayer()) use mAttackType already set by AiCombat
setAttackTypeBasedOnMovement();
} }
else else
setAttackTypeRandomly(); setAttackTypeRandomly();
@ -1761,7 +1771,7 @@ void CharacterController::update(float duration)
float realHealthLost = static_cast<float>(healthLost * (1.0f - 0.25f * fatigueTerm)); float realHealthLost = static_cast<float>(healthLost * (1.0f - 0.25f * fatigueTerm));
health.setCurrent(health.getCurrent() - realHealthLost); health.setCurrent(health.getCurrent() - realHealthLost);
cls.getCreatureStats(mPtr).setHealth(health); cls.getCreatureStats(mPtr).setHealth(health);
cls.onHit(mPtr, realHealthLost, true, MWWorld::Ptr(), MWWorld::Ptr(), true); cls.onHit(mPtr, realHealthLost, true, MWWorld::Ptr(), MWWorld::Ptr(), osg::Vec3f(), true);
const int acrobaticsSkill = cls.getSkill(mPtr, ESM::Skill::Acrobatics); const int acrobaticsSkill = cls.getSkill(mPtr, ESM::Skill::Acrobatics);
if (healthLost > (acrobaticsSkill * fatigueTerm)) if (healthLost > (acrobaticsSkill * fatigueTerm))
@ -2227,6 +2237,11 @@ void CharacterController::setAttackingOrSpell(bool attackingOrSpell)
mAttackingOrSpell = attackingOrSpell; mAttackingOrSpell = attackingOrSpell;
} }
void CharacterController::setAIAttackType(std::string attackType)
{
mAttackType = attackType;
}
bool CharacterController::readyToPrepareAttack() const bool CharacterController::readyToPrepareAttack() const
{ {
return (mHitState == CharState_None || mHitState == CharState_Block) return (mHitState == CharState_None || mHitState == CharState_Block)

@ -269,6 +269,7 @@ public:
bool isSneaking() const; bool isSneaking() const;
void setAttackingOrSpell(bool attackingOrSpell); void setAttackingOrSpell(bool attackingOrSpell);
void setAIAttackType(std::string attackType); // set and used by AiCombat
bool readyToPrepareAttack() const; bool readyToPrepareAttack() const;
bool readyToStartAttack() const; bool readyToStartAttack() const;

@ -207,9 +207,9 @@ namespace MWMechanics
if (Misc::Rng::roll0to99() >= getHitChance(attacker, victim, skillValue)) if (Misc::Rng::roll0to99() >= getHitChance(attacker, victim, skillValue))
{ {
if(attacker == getPlayer()) if (attacker == getPlayer())
mwmp::Main::get().getLocalPlayer()->GetAttack()->success = false; mwmp::Main::get().getLocalPlayer()->GetAttack()->success = false;
victim.getClass().onHit(victim, 0.0f, false, projectile, attacker, false); victim.getClass().onHit(victim, 0.0f, false, projectile, attacker, osg::Vec3f(), false);
MWMechanics::reduceWeaponCondition(0.f, false, weapon, attacker); MWMechanics::reduceWeaponCondition(0.f, false, weapon, attacker);
return; return;
} }
@ -237,9 +237,6 @@ namespace MWMechanics
if (weapon != projectile) if (weapon != projectile)
appliedEnchantment = applyOnStrikeEnchantment(attacker, victim, projectile, hitPosition); appliedEnchantment = applyOnStrikeEnchantment(attacker, victim, projectile, hitPosition);
if (damage > 0)
MWBase::Environment::get().getWorld()->spawnBloodEffect(victim, hitPosition);
// Non-enchanted arrows shot at enemies have a chance to turn up in their inventory // Non-enchanted arrows shot at enemies have a chance to turn up in their inventory
if (victim != getPlayer() if (victim != getPlayer()
&& !appliedEnchantment) && !appliedEnchantment)
@ -249,7 +246,7 @@ namespace MWMechanics
victim.getClass().getContainerStore(victim).add(projectile, 1, victim); victim.getClass().getContainerStore(victim).add(projectile, 1, victim);
} }
victim.getClass().onHit(victim, damage, true, projectile, attacker, true); victim.getClass().onHit(victim, damage, true, projectile, attacker, hitPosition, true);
} }
float getHitChance(const MWWorld::Ptr &attacker, const MWWorld::Ptr &victim, int skillValue) float getHitChance(const MWWorld::Ptr &attacker, const MWWorld::Ptr &victim, int skillValue)

@ -93,6 +93,11 @@ namespace MWMechanics
return mWalkState == State_Norm; return mWalkState == State_Norm;
} }
bool ObstacleCheck::isEvading() const
{
return mWalkState == State_Evade;
}
/* /*
* input - actor, duration (time since last check) * input - actor, duration (time since last check)
* output - true if evasive action needs to be taken * output - true if evasive action needs to be taken

@ -33,6 +33,7 @@ namespace MWMechanics
void clear(); void clear();
bool isNormalState() const; bool isNormalState() const;
bool isEvading() const;
// Returns true if there is an obstacle and an evasive action // Returns true if there is an obstacle and an evasive action
// should be taken // should be taken

@ -82,6 +82,43 @@ namespace MWMechanics
return sqrt(x * x + y * y + z * z); return sqrt(x * x + y * y + z * z);
} }
float getZAngleToDir(const osg::Vec3f& dir)
{
return std::atan2(dir.x(), dir.y());
}
float getXAngleToDir(const osg::Vec3f& dir)
{
float dirLen = dir.length();
return (dirLen != 0) ? -std::asin(dir.z() / dirLen) : 0;
}
float getZAngleToPoint(const ESM::Pathgrid::Point &origin, const ESM::Pathgrid::Point &dest)
{
osg::Vec3f dir = PathFinder::MakeOsgVec3(dest) - PathFinder::MakeOsgVec3(origin);
return getZAngleToDir(dir);
}
float getXAngleToPoint(const ESM::Pathgrid::Point &origin, const ESM::Pathgrid::Point &dest)
{
osg::Vec3f dir = PathFinder::MakeOsgVec3(dest) - PathFinder::MakeOsgVec3(origin);
return getXAngleToDir(dir);
}
bool checkWayIsClear(const osg::Vec3f& from, const osg::Vec3f& to, float offsetXY)
{
osg::Vec3f dir = to - from;
dir.z() = 0;
dir.normalize();
float verticalOffset = 200; // instead of '200' here we want the height of the actor
osg::Vec3f _from = from + dir*offsetXY + osg::Z_AXIS * verticalOffset;
// cast up-down ray and find height of hit in world space
float h = _from.z() - MWBase::Environment::get().getWorld()->getDistToNearestRayHit(_from, -osg::Z_AXIS, verticalOffset + PATHFIND_Z_REACH + 1);
return (std::abs(from.z() - h) <= PATHFIND_Z_REACH);
}
PathFinder::PathFinder() PathFinder::PathFinder()
: mPathgrid(NULL), : mPathgrid(NULL),
mCell(NULL) mCell(NULL)
@ -132,23 +169,10 @@ namespace MWMechanics
*/ */
void PathFinder::buildPath(const ESM::Pathgrid::Point &startPoint, void PathFinder::buildPath(const ESM::Pathgrid::Point &startPoint,
const ESM::Pathgrid::Point &endPoint, const ESM::Pathgrid::Point &endPoint,
const MWWorld::CellStore* cell, const MWWorld::CellStore* cell)
bool allowShortcuts)
{ {
mPath.clear(); mPath.clear();
if(allowShortcuts)
{
// if there's a ray cast hit, can't take a direct path
if (!MWBase::Environment::get().getWorld()->castRay(
static_cast<float>(startPoint.mX), static_cast<float>(startPoint.mY), static_cast<float>(startPoint.mZ),
static_cast<float>(endPoint.mX), static_cast<float>(endPoint.mY), static_cast<float>(endPoint.mZ)))
{
mPath.push_back(endPoint);
return;
}
}
if(mCell != cell || !mPathgrid) if(mCell != cell || !mPathgrid)
{ {
mCell = cell; mCell = cell;
@ -243,6 +267,19 @@ namespace MWMechanics
return std::atan2(directionX, directionY); return std::atan2(directionX, directionY);
} }
float PathFinder::getXAngleToNext(float x, float y, float z) const
{
// This should never happen (programmers should have an if statement checking
// isPathConstructed that prevents this call if otherwise).
if(mPath.empty())
return 0.;
const ESM::Pathgrid::Point &nextPoint = *mPath.begin();
osg::Vec3f dir = MakeOsgVec3(nextPoint) - osg::Vec3f(x,y,z);
return getXAngleToDir(dir);
}
bool PathFinder::checkPathCompleted(float x, float y, float tolerance) bool PathFinder::checkPathCompleted(float x, float y, float tolerance)
{ {
if(mPath.empty()) if(mPath.empty())
@ -264,19 +301,18 @@ namespace MWMechanics
// see header for the rationale // see header for the rationale
void PathFinder::buildSyncedPath(const ESM::Pathgrid::Point &startPoint, void PathFinder::buildSyncedPath(const ESM::Pathgrid::Point &startPoint,
const ESM::Pathgrid::Point &endPoint, const ESM::Pathgrid::Point &endPoint,
const MWWorld::CellStore* cell, const MWWorld::CellStore* cell)
bool allowShortcuts)
{ {
if (mPath.size() < 2) if (mPath.size() < 2)
{ {
// if path has one point, then it's the destination. // if path has one point, then it's the destination.
// don't need to worry about bad path for this case // don't need to worry about bad path for this case
buildPath(startPoint, endPoint, cell, allowShortcuts); buildPath(startPoint, endPoint, cell);
} }
else else
{ {
const ESM::Pathgrid::Point oldStart(*getPath().begin()); const ESM::Pathgrid::Point oldStart(*getPath().begin());
buildPath(startPoint, endPoint, cell, allowShortcuts); buildPath(startPoint, endPoint, cell);
if (mPath.size() >= 2) if (mPath.size() >= 2)
{ {
// if 2nd waypoint of new path == 1st waypoint of old, // if 2nd waypoint of new path == 1st waypoint of old,
@ -292,4 +328,8 @@ namespace MWMechanics
} }
} }
const MWWorld::CellStore* PathFinder::getPathCell() const
{
return mCell;
}
} }

@ -16,6 +16,20 @@ namespace MWMechanics
{ {
float distance(const ESM::Pathgrid::Point& point, float x, float y, float); float distance(const ESM::Pathgrid::Point& point, float x, float y, float);
float distance(const ESM::Pathgrid::Point& a, const ESM::Pathgrid::Point& b); float distance(const ESM::Pathgrid::Point& a, const ESM::Pathgrid::Point& b);
float getZAngleToDir(const osg::Vec3f& dir);
float getXAngleToDir(const osg::Vec3f& dir);
float getZAngleToPoint(const ESM::Pathgrid::Point &origin, const ESM::Pathgrid::Point &dest);
float getXAngleToPoint(const ESM::Pathgrid::Point &origin, const ESM::Pathgrid::Point &dest);
const float PATHFIND_Z_REACH = 50.0f;
//static const float sMaxSlope = 49.0f; // duplicate as in physicssystem
// distance after which actor (failed previously to shortcut) will try again
const float PATHFIND_SHORTCUT_RETRY_DIST = 300.0f;
// cast up-down ray with some offset from actor position to check for pits/obstacles on the way to target;
// magnitude of pits/obstacles is defined by PATHFIND_Z_REACH
bool checkWayIsClear(const osg::Vec3f& from, const osg::Vec3f& to, float offsetXY);
class PathFinder class PathFinder
{ {
public: public:
@ -39,12 +53,17 @@ namespace MWMechanics
void clearPath(); void clearPath();
void buildPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint,
const MWWorld::CellStore* cell);
bool checkPathCompleted(float x, float y, float tolerance = PathTolerance); bool checkPathCompleted(float x, float y, float tolerance = PathTolerance);
///< \Returns true if we are within \a tolerance units of the last path point. ///< \Returns true if we are within \a tolerance units of the last path point.
/// In radians /// In radians
float getZAngleToNext(float x, float y) const; float getZAngleToNext(float x, float y) const;
float getXAngleToNext(float x, float y, float z) const;
bool isPathConstructed() const bool isPathConstructed() const
{ {
return !mPath.empty(); return !mPath.empty();
@ -60,6 +79,8 @@ namespace MWMechanics
return mPath; return mPath;
} }
const MWWorld::CellStore* getPathCell() const;
/** Synchronize new path with old one to avoid visiting 1 waypoint 2 times /** Synchronize new path with old one to avoid visiting 1 waypoint 2 times
@note @note
BuildPath() takes closest PathGrid point to NPC as first point of path. BuildPath() takes closest PathGrid point to NPC as first point of path.
@ -68,9 +89,9 @@ namespace MWMechanics
Which results in NPC "running in a circle" back to the just passed waypoint. Which results in NPC "running in a circle" back to the just passed waypoint.
*/ */
void buildSyncedPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint, void buildSyncedPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint,
const MWWorld::CellStore* cell, bool allowShortcuts = true); const MWWorld::CellStore* cell);
void addPointToPath(ESM::Pathgrid::Point &point) void addPointToPath(const ESM::Pathgrid::Point &point)
{ {
mPath.push_back(point); mPath.push_back(point);
} }
@ -130,9 +151,6 @@ namespace MWMechanics
} }
private: private:
void buildPath(const ESM::Pathgrid::Point &startPoint, const ESM::Pathgrid::Point &endPoint,
const MWWorld::CellStore* cell, bool allowShortcuts = true);
std::list<ESM::Pathgrid::Point> mPath; std::list<ESM::Pathgrid::Point> mPath;
const ESM::Pathgrid *mPathgrid; const ESM::Pathgrid *mPathgrid;

@ -2,6 +2,7 @@
#include <cfloat> #include <cfloat>
#include <limits> #include <limits>
#include <iomanip>
#include <boost/format.hpp> #include <boost/format.hpp>
@ -29,41 +30,6 @@
#include "npcstats.hpp" #include "npcstats.hpp"
#include "actorutil.hpp" #include "actorutil.hpp"
namespace
{
/// Get projectile properties (model, sound and speed) for a spell with the given effects
/// If \a model is empty, the spell has no ranged effects and should not spawn a projectile.
void getProjectileInfo (const ESM::EffectList& effects, std::string& model, std::string& sound, float& speed)
{
for (std::vector<ESM::ENAMstruct>::const_iterator iter (effects.mList.begin());
iter!=effects.mList.end(); ++iter)
{
if (iter->mRange != ESM::RT_Target)
continue;
const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find (
iter->mEffectID);
model = magicEffect->mBolt;
if (model.empty())
model = "VFX_DefaultBolt";
static const std::string schools[] = {
"alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration"
};
if (!magicEffect->mBoltSound.empty())
sound = magicEffect->mBoltSound;
else
sound = schools[magicEffect->mData.mSchool] + " bolt";
speed = magicEffect->mData.mSpeed;
break;
}
}
}
namespace MWMechanics namespace MWMechanics
{ {
@ -319,6 +285,21 @@ namespace MWMechanics
{ {
} }
void CastSpell::launchMagicBolt (const ESM::EffectList& effects)
{
osg::Vec3f fallbackDirection (0,1,0);
// 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());
MWBase::Environment::get().getWorld()->launchMagicBolt(mId, false, effects,
mCaster, mSourceName, fallbackDirection);
}
void CastSpell::inflict(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, void CastSpell::inflict(const MWWorld::Ptr &target, const MWWorld::Ptr &caster,
const ESM::EffectList &effects, ESM::RangeType range, bool reflected, bool exploded) const ESM::EffectList &effects, ESM::RangeType range, bool reflected, bool exploded)
{ {
@ -358,7 +339,6 @@ namespace MWMechanics
ESM::EffectList reflectedEffects; ESM::EffectList reflectedEffects;
std::vector<ActiveSpells::ActiveEffect> appliedLastingEffects; std::vector<ActiveSpells::ActiveEffect> appliedLastingEffects;
bool firstAppliedEffect = true;
bool anyHarmfulEffect = false; bool anyHarmfulEffect = false;
// HACK: cache target's magic effects here, and add any applied effects to it. Use the cached effects for determining resistance. // HACK: cache target's magic effects here, and add any applied effects to it. Use the cached effects for determining resistance.
@ -547,20 +527,15 @@ namespace MWMechanics
if (target.getClass().isActor() || magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration) if (target.getClass().isActor() || magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)
{ {
// Play sound, only for the first effect static const std::string schools[] = {
if (firstAppliedEffect) "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration"
{ };
static const std::string schools[] = {
"alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
}; if(!magicEffect->mHitSound.empty())
sndMgr->playSound3D(target, magicEffect->mHitSound, 1.0f, 1.0f);
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); else
if(!magicEffect->mHitSound.empty()) sndMgr->playSound3D(target, schools[magicEffect->mData.mSchool]+" hit", 1.0f, 1.0f);
sndMgr->playSound3D(target, magicEffect->mHitSound, 1.0f, 1.0f);
else
sndMgr->playSound3D(target, schools[magicEffect->mData.mSchool]+" hit", 1.0f, 1.0f);
firstAppliedEffect = false;
}
// Add VFX // Add VFX
const ESM::Static* castStatic; const ESM::Static* castStatic;
@ -596,7 +571,7 @@ namespace MWMechanics
// Notify the target actor they've been hit // Notify the target actor they've been hit
if (anyHarmfulEffect && target.getClass().isActor() && target != caster && !caster.isEmpty() && caster.getClass().isActor()) if (anyHarmfulEffect && target.getClass().isActor() && target != caster && !caster.isEmpty() && caster.getClass().isActor())
target.getClass().onHit(target, 0.f, true, MWWorld::Ptr(), caster, true); target.getClass().onHit(target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true);
} }
bool CastSpell::applyInstantEffect(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, const MWMechanics::EffectKey& effect, float magnitude) bool CastSpell::applyInstantEffect(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, const MWMechanics::EffectKey& effect, float magnitude)
@ -793,17 +768,7 @@ namespace MWMechanics
} }
if (launchProjectile) if (launchProjectile)
{ launchMagicBolt(enchantment->mEffects);
std::string projectileModel;
std::string sound;
float speed = 0;
getProjectileInfo(enchantment->mEffects, projectileModel, sound, speed);
if (!projectileModel.empty())
MWBase::Environment::get().getWorld()->launchMagicBolt(projectileModel, sound, mId, speed,
false, enchantment->mEffects, mCaster, mSourceName,
// Not needed, enchantments can only be cast by actors
osg::Vec3f(1,0,0));
}
else if (!mTarget.isEmpty()) else if (!mTarget.isEmpty())
inflict(mTarget, mCaster, enchantment->mEffects, ESM::RT_Target); inflict(mTarget, mCaster, enchantment->mEffects, ESM::RT_Target);
@ -909,28 +874,9 @@ namespace MWMechanics
inflict(mCaster, mCaster, spell->mEffects, ESM::RT_Self); inflict(mCaster, mCaster, spell->mEffects, ESM::RT_Self);
if (!mTarget.isEmpty()) if (!mTarget.isEmpty())
{
inflict(mTarget, mCaster, spell->mEffects, ESM::RT_Touch); inflict(mTarget, mCaster, spell->mEffects, ESM::RT_Touch);
}
std::string projectileModel; launchMagicBolt(spell->mEffects);
std::string sound;
float speed = 0;
getProjectileInfo(spell->mEffects, projectileModel, sound, speed);
if (!projectileModel.empty())
{
osg::Vec3f fallbackDirection (0,1,0);
// 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());
MWBase::Environment::get().getWorld()->launchMagicBolt(projectileModel, sound, mId, speed,
false, spell->mEffects, mCaster, mSourceName, fallbackDirection);
}
return true; return true;
} }
@ -1003,36 +949,39 @@ namespace MWMechanics
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);
const ESM::ENAMstruct &effectentry = spell->mEffects.mList.at(0);
const ESM::MagicEffect *effect; for (std::vector<ESM::ENAMstruct>::const_iterator iter = spell->mEffects.mList.begin();
effect = store.get<ESM::MagicEffect>().find(effectentry.mEffectID); iter != spell->mEffects.mList.end(); ++iter)
{
const ESM::MagicEffect *effect;
effect = store.get<ESM::MagicEffect>().find(iter->mEffectID);
MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(mCaster); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(mCaster);
if (mCaster.getClass().isActor()) // TODO: Non-actors (except for large statics?) should also create a spell cast vfx if (mCaster.getClass().isActor()) // TODO: Non-actors (except for large statics?) should also create a spell cast vfx
{ {
const ESM::Static* castStatic; const ESM::Static* castStatic;
if (!effect->mCasting.empty()) if (!effect->mCasting.empty())
castStatic = store.get<ESM::Static>().find (effect->mCasting); castStatic = store.get<ESM::Static>().find (effect->mCasting);
else else
castStatic = store.get<ESM::Static>().find ("VFX_DefaultCast"); castStatic = store.get<ESM::Static>().find ("VFX_DefaultCast");
animation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex); animation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex);
} }
if (!mCaster.getClass().isActor()) if (!mCaster.getClass().isActor())
animation->addSpellCastGlow(effect); animation->addSpellCastGlow(effect);
static const std::string schools[] = { static const std::string schools[] = {
"alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration"
}; };
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);
else else
sndMgr->playSound3D(mCaster, schools[effect->mData.mSchool]+" cast", 1.0f, 1.0f); sndMgr->playSound3D(mCaster, schools[effect->mData.mSchool]+" cast", 1.0f, 1.0f);
}
} }
int getEffectiveEnchantmentCastCost(float castCost, const MWWorld::Ptr &actor) int getEffectiveEnchantmentCastCost(float castCost, const MWWorld::Ptr &actor)

@ -94,6 +94,9 @@ namespace MWMechanics
void playSpellCastingEffects(const std::string &spellid); void playSpellCastingEffects(const std::string &spellid);
/// Launch a bolt with the given effects.
void launchMagicBolt (const ESM::EffectList& effects);
/// @note \a target can be any type of object, not just actors. /// @note \a target can be any type of object, not just actors.
/// @note \a caster can be any type of object, or even an empty object. /// @note \a caster can be any type of object, or even an empty object.
void inflict (const MWWorld::Ptr& target, const MWWorld::Ptr& caster, void inflict (const MWWorld::Ptr& target, const MWWorld::Ptr& caster,

@ -102,7 +102,7 @@ namespace MWScript
|| ::Misc::StringUtils::ciEqual(item, "gold_025") || ::Misc::StringUtils::ciEqual(item, "gold_025")
|| ::Misc::StringUtils::ciEqual(item, "gold_100")) || ::Misc::StringUtils::ciEqual(item, "gold_100"))
item = "gold_001"; item = "gold_001";
MWWorld::ContainerStore& store = ptr.getClass().getContainerStore (ptr); MWWorld::ContainerStore& store = ptr.getClass().getContainerStore (ptr);
runtime.push (store.count(item)); runtime.push (store.count(item));
@ -136,7 +136,7 @@ namespace MWScript
|| ::Misc::StringUtils::ciEqual(item, "gold_025") || ::Misc::StringUtils::ciEqual(item, "gold_025")
|| ::Misc::StringUtils::ciEqual(item, "gold_100")) || ::Misc::StringUtils::ciEqual(item, "gold_100"))
item = "gold_001"; item = "gold_001";
MWWorld::ContainerStore& store = ptr.getClass().getContainerStore (ptr); MWWorld::ContainerStore& store = ptr.getClass().getContainerStore (ptr);
std::string itemName; std::string itemName;
@ -188,7 +188,11 @@ namespace MWScript
break; break;
} }
if (it == invStore.end()) if (it == invStore.end())
throw std::runtime_error("Item to equip not found"); {
it = ptr.getClass().getContainerStore (ptr).add (item, 1, ptr);
std::cerr << "Implicitly adding one " << item << " to container "
"to fulfil requirements of Equip instruction" << std::endl;
}
if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr()) if (ptr == MWBase::Environment::get().getWorld()->getPlayerPtr())
MWBase::Environment::get().getWindowManager()->useItem(*it); MWBase::Environment::get().getWindowManager()->useItem(*it);

@ -98,7 +98,7 @@ namespace MWWorld
throw std::runtime_error("class cannot block"); throw std::runtime_error("class cannot block");
} }
void Class::onHit(const Ptr& ptr, float damage, bool ishealth, const Ptr& object, const Ptr& attacker, bool successful) const void Class::onHit(const Ptr& ptr, float damage, bool ishealth, const Ptr& object, const Ptr& attacker, const osg::Vec3f& hitPosition, bool successful) const
{ {
throw std::runtime_error("class cannot be hit"); throw std::runtime_error("class cannot be hit");
} }

@ -120,7 +120,7 @@ namespace MWWorld
/// enums. ignored for creature attacks. /// enums. ignored for creature attacks.
/// (default implementation: throw an exception) /// (default implementation: throw an exception)
virtual void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, bool successful) const; virtual void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const;
///< Alerts \a ptr that it's being hit for \a damage points to health if \a ishealth is ///< Alerts \a ptr that it's being hit for \a damage points to health if \a ishealth is
/// true (else fatigue) by \a object (sword, arrow, etc). \a attacker specifies the /// true (else fatigue) by \a object (sword, arrow, etc). \a attacker specifies the
/// actor responsible for the attack, and \a successful specifies if the hit is /// actor responsible for the attack, and \a successful specifies if the hit is

@ -1,5 +1,7 @@
#include "projectilemanager.hpp" #include "projectilemanager.hpp"
#include <iomanip>
#include <osg/PositionAttitudeTransform> #include <osg/PositionAttitudeTransform>
#include <components/esm/esmwriter.hpp> #include <components/esm/esmwriter.hpp>
@ -33,6 +35,56 @@
#include "../mwphysics/physicssystem.hpp" #include "../mwphysics/physicssystem.hpp"
namespace
{
ESM::EffectList getMagicBoltData(std::vector<std::string>& projectileIDs, std::vector<std::string>& sounds, float& speed, const ESM::EffectList& effects)
{
int count = 0;
speed = 0.0f;
ESM::EffectList projectileEffects;
for (std::vector<ESM::ENAMstruct>::const_iterator iter (effects.mList.begin());
iter!=effects.mList.end(); ++iter)
{
const ESM::MagicEffect *magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find (
iter->mEffectID);
// All the projectiles should use the same speed. From observations in the
// original engine, this seems to be the average of the constituent effects.
speed += magicEffect->mData.mSpeed;
count++;
if (iter->mRange != ESM::RT_Target)
continue;
if (magicEffect->mBolt.empty())
projectileIDs.push_back("VFX_DefaultBolt");
else
projectileIDs.push_back(magicEffect->mBolt);
static const std::string schools[] = {
"alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration"
};
if (!magicEffect->mBoltSound.empty())
sounds.push_back(magicEffect->mBoltSound);
else
sounds.push_back(schools[magicEffect->mData.mSchool] + " bolt");
projectileEffects.mList.push_back(*iter);
}
if (count != 0)
speed /= count;
if (projectileEffects.mList.size() > 1) // insert a VFX_Multiple projectile if there are multiple projectile effects
{
std::ostringstream ID;
ID << "VFX_Multiple" << effects.mList.size();
std::vector<std::string>::iterator it;
it = projectileIDs.begin();
it = projectileIDs.insert(it, ID.str());
}
return projectileEffects;
}
}
namespace MWWorld namespace MWWorld
{ {
@ -94,6 +146,18 @@ namespace MWWorld
mResourceSystem->getSceneManager()->getInstance(model, attachTo); mResourceSystem->getSceneManager()->getInstance(model, attachTo);
if (state.mIdMagic.size() > 1)
for (size_t iter = 1; iter != state.mIdMagic.size(); ++iter)
{
std::ostringstream nodeName;
nodeName << "Dummy" << std::setw(2) << std::setfill('0') << iter;
const ESM::Weapon* weapon = MWBase::Environment::get().getWorld()->getStore().get<ESM::Weapon>().find (state.mIdMagic.at(iter));
SceneUtil::FindByNameVisitor findVisitor(nodeName.str());
attachTo->accept(findVisitor);
if (findVisitor.mFoundNode)
mResourceSystem->getSceneManager()->getInstance("meshes\\" + weapon->mModel, findVisitor.mFoundNode);
}
SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor; SceneUtil::DisableFreezeOnCullVisitor disableFreezeOnCullVisitor;
state.mNode->accept(disableFreezeOnCullVisitor); state.mNode->accept(disableFreezeOnCullVisitor);
@ -112,10 +176,8 @@ namespace MWWorld
state.mEffectAnimationTime->addTime(duration); state.mEffectAnimationTime->addTime(duration);
} }
void ProjectileManager::launchMagicBolt(const std::string &model, const std::string &sound, void ProjectileManager::launchMagicBolt(const std::string &spellId, bool stack, const ESM::EffectList &effects, const Ptr &caster,
const std::string &spellId, float speed, bool stack, const std::string &sourceName, const osg::Vec3f& fallbackDirection)
const ESM::EffectList &effects, const Ptr &caster, const std::string &sourceName,
const osg::Vec3f& fallbackDirection)
{ {
osg::Vec3f pos = caster.getRefData().getPosition().asVec3(); osg::Vec3f pos = caster.getRefData().getPosition().asVec3();
if (caster.getClass().isActor()) if (caster.getClass().isActor())
@ -137,33 +199,31 @@ namespace MWWorld
MagicBoltState state; MagicBoltState state;
state.mSourceName = sourceName; state.mSourceName = sourceName;
state.mId = model;
state.mSpellId = spellId; state.mSpellId = spellId;
state.mCasterHandle = caster; state.mCasterHandle = caster;
if (caster.getClass().isActor()) if (caster.getClass().isActor())
state.mActorId = caster.getClass().getCreatureStats(caster).getActorId(); state.mActorId = caster.getClass().getCreatureStats(caster).getActorId();
else else
state.mActorId = -1; state.mActorId = -1;
state.mSpeed = speed;
state.mStack = stack; state.mStack = stack;
state.mSoundId = sound;
// Only interested in "on target" effects state.mEffects = getMagicBoltData(state.mIdMagic, state.mSoundIds, state.mSpeed, effects);
for (std::vector<ESM::ENAMstruct>::const_iterator iter (effects.mList.begin());
iter!=effects.mList.end(); ++iter)
{
if (iter->mRange == ESM::RT_Target)
state.mEffects.mList.push_back(*iter);
}
MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), model); // Non-projectile should have been removed by getMagicBoltData
if (state.mEffects.mList.empty())
return;
MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), state.mIdMagic.at(0));
MWWorld::Ptr ptr = ref.getPtr(); MWWorld::Ptr ptr = ref.getPtr();
createModel(state, ptr.getClass().getModel(ptr), pos, orient, true); createModel(state, ptr.getClass().getModel(ptr), pos, orient, true);
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
state.mSound = sndMgr->playSound3D(pos, sound, 1.0f, 1.0f, MWBase::SoundManager::Play_TypeSfx, MWBase::SoundManager::Play_Loop); for (size_t it = 0; it != state.mSoundIds.size(); it++)
{
state.mSounds.push_back(sndMgr->playSound3D(pos, state.mSoundIds.at(it), 1.0f, 1.0f, MWBase::SoundManager::Play_TypeSfx, MWBase::SoundManager::Play_Loop));
}
mMagicBolts.push_back(state); mMagicBolts.push_back(state);
} }
@ -173,7 +233,7 @@ namespace MWWorld
state.mActorId = actor.getClass().getCreatureStats(actor).getActorId(); state.mActorId = actor.getClass().getCreatureStats(actor).getActorId();
state.mBowId = bow.getCellRef().getRefId(); state.mBowId = bow.getCellRef().getRefId();
state.mVelocity = orient * osg::Vec3f(0,1,0) * speed; state.mVelocity = orient * osg::Vec3f(0,1,0) * speed;
state.mId = projectile.getCellRef().getRefId(); state.mIdArrow = projectile.getCellRef().getRefId();
state.mCasterHandle = actor; state.mCasterHandle = actor;
state.mAttackStrength = attackStrength; state.mAttackStrength = attackStrength;
@ -205,8 +265,10 @@ namespace MWWorld
osg::Vec3f pos(it->mNode->getPosition()); osg::Vec3f pos(it->mNode->getPosition());
osg::Vec3f newPos = pos + direction * duration * speed; osg::Vec3f newPos = pos + direction * duration * speed;
if (it->mSound.get()) for (size_t soundIter = 0; soundIter != it->mSounds.size(); soundIter++)
it->mSound->setPosition(newPos); {
it->mSounds.at(soundIter)->setPosition(newPos);
}
it->mNode->setPosition(newPos); it->mNode->setPosition(newPos);
@ -246,7 +308,11 @@ namespace MWWorld
MWBase::Environment::get().getWorld()->explodeSpell(pos, it->mEffects, caster, result.mHitObject, MWBase::Environment::get().getWorld()->explodeSpell(pos, it->mEffects, caster, result.mHitObject,
ESM::RT_Target, it->mSpellId, it->mSourceName); ESM::RT_Target, it->mSpellId, it->mSourceName);
MWBase::Environment::get().getSoundManager()->stopSound(it->mSound); for (size_t soundIter = 0; soundIter != it->mSounds.size(); soundIter++)
{
MWBase::Environment::get().getSoundManager()->stopSound(it->mSounds.at(soundIter));
}
mParent->removeChild(it->mNode); mParent->removeChild(it->mNode);
it = mMagicBolts.erase(it); it = mMagicBolts.erase(it);
@ -286,7 +352,7 @@ namespace MWWorld
{ {
if (result.mHit) if (result.mHit)
{ {
MWWorld::ManualRef projectileRef(MWBase::Environment::get().getWorld()->getStore(), it->mId); MWWorld::ManualRef projectileRef(MWBase::Environment::get().getWorld()->getStore(), it->mIdArrow);
// Try to get a Ptr to the bow that was used. It might no longer exist. // Try to get a Ptr to the bow that was used. It might no longer exist.
MWWorld::Ptr bow = projectileRef.getPtr(); MWWorld::Ptr bow = projectileRef.getPtr();
@ -326,7 +392,10 @@ namespace MWWorld
for (std::vector<MagicBoltState>::iterator it = mMagicBolts.begin(); it != mMagicBolts.end(); ++it) for (std::vector<MagicBoltState>::iterator it = mMagicBolts.begin(); it != mMagicBolts.end(); ++it)
{ {
mParent->removeChild(it->mNode); mParent->removeChild(it->mNode);
MWBase::Environment::get().getSoundManager()->stopSound(it->mSound); for (size_t soundIter = 0; soundIter != it->mSounds.size(); soundIter++)
{
MWBase::Environment::get().getSoundManager()->stopSound(it->mSounds.at(soundIter));
}
} }
mMagicBolts.clear(); mMagicBolts.clear();
} }
@ -338,7 +407,7 @@ namespace MWWorld
writer.startRecord(ESM::REC_PROJ); writer.startRecord(ESM::REC_PROJ);
ESM::ProjectileState state; ESM::ProjectileState state;
state.mId = it->mId; state.mId = it->mIdArrow;
state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition())); state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition()));
state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude())); state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude()));
state.mActorId = it->mActorId; state.mActorId = it->mActorId;
@ -357,14 +426,14 @@ namespace MWWorld
writer.startRecord(ESM::REC_MPRJ); writer.startRecord(ESM::REC_MPRJ);
ESM::MagicBoltState state; ESM::MagicBoltState state;
state.mId = it->mId; state.mId = it->mIdMagic.at(0);
state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition())); state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition()));
state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude())); state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude()));
state.mActorId = it->mActorId; state.mActorId = it->mActorId;
state.mSpellId = it->mSpellId; state.mSpellId = it->mSpellId;
state.mEffects = it->mEffects; state.mEffects = it->mEffects;
state.mSound = it->mSoundId; state.mSound = it->mSoundIds.at(0);
state.mSourceName = it->mSourceName; state.mSourceName = it->mSourceName;
state.mSpeed = it->mSpeed; state.mSpeed = it->mSpeed;
state.mStack = it->mStack; state.mStack = it->mStack;
@ -386,7 +455,7 @@ namespace MWWorld
state.mActorId = esm.mActorId; state.mActorId = esm.mActorId;
state.mBowId = esm.mBowId; state.mBowId = esm.mBowId;
state.mVelocity = esm.mVelocity; state.mVelocity = esm.mVelocity;
state.mId = esm.mId; state.mIdArrow = esm.mId;
state.mAttackStrength = esm.mAttackStrength; state.mAttackStrength = esm.mAttackStrength;
std::string model; std::string model;
@ -413,17 +482,20 @@ namespace MWWorld
MagicBoltState state; MagicBoltState state;
state.mSourceName = esm.mSourceName; state.mSourceName = esm.mSourceName;
state.mId = esm.mId; state.mIdMagic.push_back(esm.mId);
state.mSpellId = esm.mSpellId; state.mSpellId = esm.mSpellId;
state.mActorId = esm.mActorId; state.mActorId = esm.mActorId;
state.mSpeed = esm.mSpeed;
state.mStack = esm.mStack; state.mStack = esm.mStack;
state.mEffects = esm.mEffects; state.mEffects = getMagicBoltData(state.mIdMagic, state.mSoundIds, state.mSpeed, esm.mEffects);
state.mSpeed = esm.mSpeed; // speed is derived from non-projectile effects as well as
// projectile effects, so we can't calculate it from the save
// file's effect list, which is already trimmed of non-projectile
// effects. We need to use the stored value.
std::string model; std::string model;
try try
{ {
MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), esm.mId); MWWorld::ManualRef ref(MWBase::Environment::get().getWorld()->getStore(), state.mIdMagic.at(0));
MWWorld::Ptr ptr = ref.getPtr(); MWWorld::Ptr ptr = ref.getPtr();
model = ptr.getClass().getModel(ptr); model = ptr.getClass().getModel(ptr);
} }
@ -435,9 +507,12 @@ namespace MWWorld
createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), true); createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), true);
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
state.mSound = sndMgr->playSound3D(esm.mPosition, esm.mSound, 1.0f, 1.0f,
MWBase::SoundManager::Play_TypeSfx, MWBase::SoundManager::Play_Loop); for (size_t soundIter = 0; soundIter != state.mSoundIds.size(); soundIter++)
state.mSoundId = esm.mSound; {
state.mSounds.push_back(sndMgr->playSound3D(esm.mPosition, state.mSoundIds.at(soundIter), 1.0f, 1.0f,
MWBase::SoundManager::Play_TypeSfx, MWBase::SoundManager::Play_Loop));
}
mMagicBolts.push_back(state); mMagicBolts.push_back(state);
return true; return true;

@ -49,9 +49,8 @@ namespace MWWorld
MWRender::RenderingManager* rendering, MWPhysics::PhysicsSystem* physics); MWRender::RenderingManager* rendering, MWPhysics::PhysicsSystem* physics);
/// If caster is an actor, the actor's facing orientation is used. Otherwise fallbackDirection is used. /// If caster is an actor, the actor's facing orientation is used. Otherwise fallbackDirection is used.
void launchMagicBolt (const std::string& model, const std::string &sound, const std::string &spellId, void launchMagicBolt (const std::string &spellId, bool stack, const ESM::EffectList& effects,
float speed, bool stack, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const std::string& sourceName, const osg::Vec3f& fallbackDirection);
const MWWorld::Ptr& caster, const std::string& sourceName, const osg::Vec3f& fallbackDirection);
void launchProjectile (MWWorld::Ptr actor, MWWorld::ConstPtr projectile, void launchProjectile (MWWorld::Ptr actor, MWWorld::ConstPtr projectile,
const osg::Vec3f& pos, const osg::Quat& orient, MWWorld::Ptr bow, float speed, float attackStrength); const osg::Vec3f& pos, const osg::Quat& orient, MWWorld::Ptr bow, float speed, float attackStrength);
@ -84,8 +83,11 @@ namespace MWWorld
MWWorld::Ptr getCaster(); MWWorld::Ptr getCaster();
// MW-id of this projectile // MW-ids of a magic projectile
std::string mId; std::vector<std::string> mIdMagic;
// MW-id of an arrow projectile
std::string mIdArrow;
}; };
struct MagicBoltState : public State struct MagicBoltState : public State
@ -101,8 +103,8 @@ namespace MWWorld
bool mStack; bool mStack;
MWBase::SoundPtr mSound; std::vector<MWBase::SoundPtr> mSounds;
std::string mSoundId; std::vector<std::string> mSoundIds;
}; };
struct ProjectileState : public State struct ProjectileState : public State

@ -2726,11 +2726,10 @@ namespace MWWorld
mProjectileManager->launchProjectile(actor, projectile, worldPos, orient, bow, speed, attackStrength); mProjectileManager->launchProjectile(actor, projectile, worldPos, orient, bow, speed, attackStrength);
} }
void World::launchMagicBolt (const std::string& model, const std::string &sound, const std::string &spellId, void World::launchMagicBolt (const std::string &spellId, bool stack, const ESM::EffectList& effects,
float speed, bool stack, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const std::string& sourceName, const osg::Vec3f& fallbackDirection)
const MWWorld::Ptr& caster, const std::string& sourceName, const osg::Vec3f& fallbackDirection)
{ {
mProjectileManager->launchMagicBolt(model, sound, spellId, speed, stack, effects, caster, sourceName, fallbackDirection); mProjectileManager->launchMagicBolt(spellId, stack, effects, caster, sourceName, fallbackDirection);
} }
const std::vector<std::string>& World::getContentFiles() const const std::vector<std::string>& World::getContentFiles() const
@ -3184,8 +3183,8 @@ namespace MWWorld
{ {
const ESM::MagicEffect* effect = getStore().get<ESM::MagicEffect>().find(effectIt->mEffectID); const ESM::MagicEffect* effect = getStore().get<ESM::MagicEffect>().find(effectIt->mEffectID);
if (effectIt->mArea <= 0) if ((effectIt->mArea <= 0 && !ignore.isEmpty() && ignore.getClass().isActor()) || effectIt->mRange != rangeType)
continue; // Not an area effect continue; // Not right range type, or not area effect and hit an actor
// Spawn the explosion orb effect // Spawn the explosion orb effect
const ESM::Static* areaStatic; const ESM::Static* areaStatic;
@ -3194,7 +3193,13 @@ namespace MWWorld
else else
areaStatic = getStore().get<ESM::Static>().find ("VFX_DefaultArea"); areaStatic = getStore().get<ESM::Static>().find ("VFX_DefaultArea");
mRendering->spawnEffect("meshes\\" + areaStatic->mModel, "", origin, static_cast<float>(effectIt->mArea)); if (effectIt->mArea <= 0)
{
mRendering->spawnEffect("meshes\\" + areaStatic->mModel, "", origin, 1.0f);
continue;
}
else
mRendering->spawnEffect("meshes\\" + areaStatic->mModel, "", origin, static_cast<float>(effectIt->mArea * 2));
// Play explosion sound (make sure to use NoTrack, since we will delete the projectile now) // Play explosion sound (make sure to use NoTrack, since we will delete the projectile now)
static const std::string schools[] = { static const std::string schools[] = {

@ -594,9 +594,8 @@ namespace MWWorld
*/ */
virtual void castSpell (const MWWorld::Ptr& actor); virtual void castSpell (const MWWorld::Ptr& actor);
virtual void launchMagicBolt (const std::string& model, const std::string& sound, const std::string& spellId, virtual void launchMagicBolt (const std::string& spellId, bool stack, const ESM::EffectList& effects,
float speed, bool stack, const ESM::EffectList& effects, const MWWorld::Ptr& caster, const std::string& sourceName, const osg::Vec3f& fallbackDirection);
const MWWorld::Ptr& caster, const std::string& sourceName, const osg::Vec3f& fallbackDirection);
virtual void launchProjectile (MWWorld::Ptr actor, MWWorld::ConstPtr projectile, virtual void launchProjectile (MWWorld::Ptr actor, MWWorld::ConstPtr projectile,
const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr bow, float speed, float attackStrength); const osg::Vec3f& worldPos, const osg::Quat& orient, MWWorld::Ptr bow, float speed, float attackStrength);

@ -390,7 +390,7 @@ std::string Manager::getString(const std::string &setting, const std::string &ca
return it->second; return it->second;
throw std::runtime_error(std::string("Trying to retrieve a non-existing setting: ") + setting throw std::runtime_error(std::string("Trying to retrieve a non-existing setting: ") + setting
+ ".\nMake sure the settings-default.cfg file file was properly installed."); + ".\nMake sure the settings-default.cfg file was properly installed.");
} }
float Manager::getFloat (const std::string& setting, const std::string& category) float Manager::getFloat (const std::string& setting, const std::string& category)

@ -10,6 +10,7 @@ Components
openmw/index openmw/index
openmw-cs/index openmw-cs/index
openmw-mods/index
Indices and tables Indices and tables

@ -0,0 +1,58 @@
Modding OpenMW vs Morrowind
#################################
A brief overview of the differences between the two engines.
============================================================
OpenMW is designed to be able to use all the normal Morrowind mod files such as ESM/ESP plugins, texture replacers, mesh replacers, etc.
.. warning::
All external programs and libraries that depend on ``morrowind.exe`` cannot function with OpenMW. This means you should assume mods dependent on Morrowind Graphics Extender, Morrowind Code Patch, Morrowind Script Extender, etc, will *not* work correctly, nor will the tools themselves.
Multiple Data Folders
---------------------
The largest difference between OpenMW and Morrowind in terms of data structure is OpenMW's support of multiple data folders. This has many advantages, especially when it comes to unistalling mods and preventing unintentional overwrites of files.
.. warning::
Most mods can still be installed into the root OpenMW data folder, but this is not recommended.
To install mods via this new feature:
#. Open ``openmw.cfg`` with your preffered text editor. It is located as described in https://wiki.openmw.org/index.php?title=Paths and *not* in your OpenMW root directory.
#. Find or search for ``data=``. This is located very near the bottom of the file.
#. Add a new line below this line and make a new entry of the format ``data=path/to/your/mod``
#. Make as many of these entries as you need for each mod folder you want to include.
#. Save ``openmw.cfg``
.. note::
All mod folders must adhere to the same file structure as ``~/Morrowind/Data Files/``.
.. TODO create a PATHS ReST file that I can reference instead of the Wiki.
To uninstall these mods simply delete that mod's respective ``data=`` entry.
The mods are loaded in the order of these entries, with the top being overwritten by mods added towards the bottom.
.. note::
Mods that depend on ESM/ESP plugins can be rearranged within the OpenMW Launcher, but mesh/texture replacer mods can only be reordered by moving their ``data=`` entry.
OpenMW Launcher
---------------
The launcher included with OpenMW is similar to the original Morrowind Launcher. Go to the Data Files tab to enable and disable plugins. You can also drag list items to modify the load order. Content lists can be created at the bottom by clicking the New Content List button, creating a list name, then setting up a new modlist. This is helpful for different player profiles and testing out different load orders.
.. TODO use a substitution image for the New Content List button.
Settings.cfg
------------
The ``settings.cfg`` file is essentially the same as the INI files for Morrowind. It is located in the same directory as ``openmw.cfg``. This is where many video, audio, GUI, input, etc. settings can be modified. Some are available in-game, but many are only available in this configuration file. Please see https://wiki.openmw.org/index.php?title=Settings for the complete listing.
.. TODO Create a proper ReST document tree for all the settings rather than Wiki.
Open Source Resources Support
-----------------------------
While OpenMW supports all of the original files that Morrowind supported, we've expanded support to many open source file formats. These are summarized below:
<this will be a table of the type of file, the morrowind supported file, and the OpenMW supported file formats>

@ -0,0 +1,4 @@
Foreword
########
OpenMW is a complete game engine built to be content agnostic. The majority of this guide is applicable to any non-Morrowind project using its engine. That being said, it was designed with the extensive modding community of Morrowind in mind. Therefore, if you are already familiar with modding in Morrowind, you will likely be able to start modding in OpenMW with little to no instruction. We do recommend you at least refer to :doc:`differences` to find out about what's different between OpenMW and the original Morrowind engine. For everyone else, or just a good refresher, read on!

@ -0,0 +1,16 @@
########################
OpenMW Modding Reference
########################
The following document is the complete reference guide to modifying, or modding, your OpenMW setup. It does not cover content creation itself, only how to alter or add to your OpenMW gameplay experience. To learn more about creating new content for OpenMW, please refer to :doc:`../openmw-cs/index`.
.. warning::
OpenMW is still software in development. This manual does not cover any of its shortcomings. It is written as if everything was working as inteded. Please report any software problems as bugs in the software, not errors in the manual.
.. toctree::
:caption: Table of Contents
:maxdepth: 2
foreword
differences
mod-install

@ -0,0 +1,27 @@
How To Install and Use Mods
###########################
The following is a detailed guide on how to install and enable mods in OpenMW using best practices.
Install
-------
#. Your mod probably comes in some kind of archive, such as ``.zip``, ``.rar``, ``.7z``, or something along those lines. Unpack this archive into its own folder.
#. Ensure the structure of this folder is correct.
#. Locate the plugin files, ``.esp`` or ``.omwaddon``. The folder containing the plugin files we will call your *data folder*
#. Check that all resource folders (``Meshes``, ``Textures``, etc.) containing additional resource files (the actual meshes, textures, etc.) are in the *data folder*.
.. note::
There may be multiple levels of folders, but the location of the plugins must be the same as the resource folders.
#. Open your ``openmw.cfg`` file in your preferred plain text editor. It is located as described in https://wiki.openmw.org/index.php?title=Paths and *not* in your OpenMW root directory.
#. Find or search for ``data=``. This is located very near the bottom of the file. If you are using Morrowind, this first entry should already point to your Morrowind data directory, ``Data Files``; otherwise it will point to your game file, ``.omwgame``.
#. Create a new line underneath and type: ``data="path/to/your/data folder"`` Remember, the *data folder* is where your mod's plugin files are. The double quotes around this path name are *required*.
#. Save your ``openmw.cfg`` file.
You have now installed your mod. Any simple replacer mods that only contain resource files such as meshes or textures will now automatically be loaded in the order of their ``data=*`` entry. This is important to note because replacer mods that replace the same resource will overwrite previous ones as you go down the list.
Enable
------
Any mods that have plugin files must be enabled to work.
#.

@ -0,0 +1,53 @@
####################
Tutorial Style Guide
####################
Please contact Ravenwing about any questions relating to this guide.
Foreword
--------
I shall try to be as brief as possible without sacrificing clarity, just as you should be when writing documentation. The SCOPE of this guide is limited to all non-source code documentation.
References
----------
-Syntax: reStructuredText (ReST): http://docutils.sourceforge.net/rst.html
-Parser: Sphinx: http://www.sphinx-doc.org/en/stable/
-British and English Spelling: https://www.spellzone.com/pages/british-american.cfm
Language
--------
British English
Sorry, I'm American and I'll probably have a million relapses, but this seems to have been decided and uniformity is key!
Text Wrapping
-------------
DO NOT manually decide where each line ends! Enable text wrapping in your text editor! (usually under View) Even Notepad allows automatic text wrapping, so use it. If you program, you may be used to keeping your lines under 80 characters wide and manually pressing enter, but don't. This is because we are writing mostly prose. When someone has to come in and edit portions of text that puts all this out of alignment, it takes FOREVER to readjust everything. Plus, indentation is very important in ReST, and you may forget to indent properly.
Indentations
------------
This isn't as important, especially since Sphinx converts tabs to spaces, but I find it much easier to keep large blocks of things aligned if you just tab. If you can edit your tab width, please set it to 4 spaces.
Spaces
------
Use only one space after each sentence. Some people were taught two spaces, but this is a carry-over from typewriters. Even though your text editor is probably using monospaced characters like a typewriter, the formats Sphinx is converting into will make all the adjustments they need to be beautifully legible.
Commas
------
Oxford comma. Use it. I know this goes against using British English, but this is technical writing. You cannot assume our audience will know from context if the last two items in a list are grouped or not.
I also prefer parentheticals to commas around gerund phrases.
Files and Extensions
--------------------
When referring to a filename or filepath use the double back quotes as for inline literals. (e.g. ``morrowind.exe``)
When referring to a file extension by itself, use ``.lowerCaseExtension``. (e.g. ``.esm``)
If referring to a folder or file by a general name, use *emphasis*
If referring to a folder by its actual name, even without the path, use ``inline literal``
Loading…
Cancel
Save