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" \
-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.
}
cd $DEPS
@ -631,7 +633,7 @@ printf "SDL 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.
}
@ -691,8 +693,16 @@ if [ -z $CI ]; then
echo "- Copying Runtime DLLs..."
mkdir -p $BUILD_CONFIG
for DLL in $RUNTIME_DLLS; do
echo " $(basename $DLL)."
cp "$DLL" $BUILD_CONFIG/
TARGET="$(basename "$DLL")"
if [[ "$DLL" == *":"* ]]; then
IFS=':'; SPLIT=( ${DLL} ); unset IFS
DLL=${SPLIT[0]}
TARGET=${SPLIT[1]}
fi
echo " ${TARGET}."
cp "$DLL" "$BUILD_CONFIG/$TARGET"
done
echo

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

@ -81,7 +81,7 @@ void CSMDoc::Runner::start (bool delayed)
arguments << ("--script-run="+mStartup->fileName());;
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());
iter!=mContentFiles.end(); ++iter)

@ -321,6 +321,10 @@ void CSMDoc::WriteCellCollectionStage::perform (int stage, Messages& messages)
{
CSMWorld::CellRef refRecord = ref.get();
// Check for uninitialized content file
if (!refRecord.mRefNum.hasContentFile())
refRecord.mRefNum.mContentFile = 0;
// recalculate the ref's cell location
std::ostringstream stream;
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)
: mEncoder (encoding), mPathgrids (mCells), mRefs (mCells),
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());
@ -899,9 +899,11 @@ int CSMWorld::Data::startLoading (const boost::filesystem::path& path, bool base
mReader = new ESM::ESMReader;
mReader->setEncoder (&mEncoder);
mReader->setIndex(mReaderIndex++);
mReader->setIndex((project || !base) ? 0 : mReaderIndex++);
mReader->open (path.string());
mContentFileNames.insert(std::make_pair(path.filename().string(), mReader->getIndex()));
mBase = base;
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));
}
// 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();
}

@ -123,6 +123,8 @@ namespace CSMWorld
std::vector<boost::shared_ptr<ESM::ESMReader> > mReaders;
std::map<std::string, int> mContentFileNames;
// not implemented
Data (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)
: 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),
mDragAxis (-1), mLocked (false), mUnitScaleDist(1)
{

@ -486,9 +486,8 @@ namespace MWBase
virtual void castSpell (const MWWorld::Ptr& actor) = 0;
virtual void launchMagicBolt (const std::string& model, const std::string& sound, const std::string& spellId,
float speed, bool stack, const ESM::EffectList& effects,
const MWWorld::Ptr& caster, const std::string& sourceName, const osg::Vec3f& fallbackDirection) = 0;
virtual void launchMagicBolt (const std::string& spellId, bool stack, const ESM::EffectList& effects,
const MWWorld::Ptr& caster, const std::string& sourceName, const osg::Vec3f& fallbackDirection) = 0;
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;

@ -264,7 +264,7 @@ namespace MWClass
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);
return;
}
@ -318,15 +318,12 @@ namespace MWClass
if (MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage, attackStrength))
damage = 0;
if (damage > 0)
MWBase::Environment::get().getWorld()->spawnBloodEffect(victim, hitPosition);
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.
@ -342,10 +339,10 @@ namespace MWClass
setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker);
}
if(!object.isEmpty())
if (!object.isEmpty())
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;
/* 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);
}
if(!successful)
if (!successful)
{
// Missed
MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "miss", 1.0f, 1.0f);
return;
}
if(!object.isEmpty())
if (!object.isEmpty())
getCreatureStats(ptr).setLastHitObject(object.getCellRef().getRefId());
if (damage > 0.0f && !object.isEmpty())
@ -391,7 +388,10 @@ namespace MWClass
if(ishealth)
{
if (!attacker.isEmpty())
{
damage = scaleDamage(damage, attacker, ptr);
MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition);
}
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 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,
const MWWorld::Ptr& actor) const;

@ -609,7 +609,7 @@ namespace MWClass
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);
return;
}
@ -664,15 +664,12 @@ namespace MWClass
if (MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage, attackStrength))
damage = 0;
if (healthdmg && damage > 0)
MWBase::Environment::get().getWorld()->spawnBloodEffect(victim, hitPosition);
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();
@ -689,10 +686,10 @@ namespace MWClass
setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker);
}
if(!object.isEmpty())
if (!object.isEmpty())
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);
/* 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);
}
if(!successful)
if (!successful)
{
// Missed
sndMgr->playSound3D(ptr, "miss", 1.0f, 1.0f);
return;
}
if(!object.isEmpty())
if (!object.isEmpty())
getCreatureStats(ptr).setLastHitObject(object.getCellRef().getRefId());
@ -717,7 +714,7 @@ namespace MWClass
if (damage < 0.001f)
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
// something, alert the character controller, scripts, etc.
@ -748,7 +745,7 @@ namespace MWClass
else
getCreatureStats(ptr).setHitRecovery(true); // Is this supposed to always occur?
if(damage > 0 && ishealth)
if (damage > 0 && ishealth)
{
// Hit percentages:
// cuirass = 30%
@ -810,16 +807,18 @@ namespace MWClass
}
}
if(ishealth)
if (ishealth)
{
if (!attacker.isEmpty())
damage = scaleDamage(damage, attacker, ptr);
if(damage > 0.0f)
if (damage > 0.0f)
{
sndMgr->playSound3D(ptr, "Health Damage", 1.0f, 1.0f);
if (ptr == MWMechanics::getPlayer())
MWBase::Environment::get().getWindowManager()->activateHitOverlay();
if (!attacker.isEmpty())
MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition);
}
MWMechanics::DynamicStat<float> health(getCreatureStats(ptr).getHealth());
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 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;
///< 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);
}
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
actor.getClass().getCreatureStats(actor).setDrawState(DrawState_Nothing);
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 check whether the target is currently registered
// 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;
if(distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]) < MWBase::Environment::get().getWorld()->getMaxActivationDistance()) { //Stop when you get in activation range
actor.getClass().getMovementSettings(actor).mPosition[1] = 0;
if (pathTo(actor, dest, duration, MWBase::Environment::get().getWorld()->getMaxActivationDistance())) //Stop when you get in activation range
{
// activate when reached
MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getPtr(mObjectId,false);
MWBase::Environment::get().getWorld()->activate(target, actor);
return true;
}
else {
pathTo(actor, dest, duration); //Go to the destination
}
return false;
}

@ -23,50 +23,10 @@ namespace
{
//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,
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
@ -80,7 +40,7 @@ namespace MWMechanics
float mTimerCombatMove;
bool mReadyToAttack;
bool mAttack;
bool mFollowTarget;
float mAttackRange;
bool mCombatMove;
osg::Vec3f mLastTargetPos;
const MWWorld::CellStore* mCell;
@ -89,16 +49,15 @@ namespace MWMechanics
float mStrength;
bool mForceNoShortcut;
ESM::Position mShortcutFailPos;
osg::Vec3f mLastActorPos;
MWMechanics::Movement mMovement;
AiCombatStorage():
mAttackCooldown(0),
mTimerReact(0),
mTimerReact(AI_REACTION_TIME),
mTimerCombatMove(0),
mReadyToAttack(false),
mAttack(false),
mFollowTarget(false),
mAttackRange(0),
mCombatMove(false),
mLastTargetPos(0,0,0),
mCell(NULL),
@ -107,10 +66,10 @@ namespace MWMechanics
mStrength(),
mForceNoShortcut(false),
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 stopCombatMove();
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
* whether the target was hit, etc.
*/
bool AiCombat::execute (const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state, float duration)
{
// get or create temporary storage
@ -197,34 +157,38 @@ namespace MWMechanics
|| target.getClass().getCreatureStats(target).isDead())
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);
updateActorsMovement(actor, duration, storage.mMovement);
if (storage.mReadyToAttack) updateActorsMovement(actor, duration, storage);
storage.updateAttack(characterController);
storage.mActionCooldown -= duration;
float& timerReact = storage.mTimerReact;
if(timerReact < REACTION_INTERVAL)
if (timerReact < AI_REACTION_TIME)
{
timerReact += duration;
return false;
}
else
{
timerReact = 0;
return reactionTimeActions(actor, characterController, storage, target);
attack(actor, target, storage, characterController);
}
return false;
}
bool AiCombat::reactionTimeActions(const MWWorld::Ptr& actor, CharacterController& characterController,
AiCombatStorage& storage, MWWorld::Ptr target)
void AiCombat::attack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, AiCombatStorage& storage, CharacterController& characterController)
{
MWMechanics::Movement& movement = storage.mMovement;
if (isTargetMagicallyHidden(target))
{
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;
@ -239,10 +203,9 @@ namespace MWMechanics
float& actionCooldown = storage.mActionCooldown;
if (actionCooldown > 0)
return false;
return;
float rangeAttack = 0;
float rangeFollow = 0;
float &rangeAttack = storage.mAttackRange;
boost::shared_ptr<Action>& currentAction = storage.mCurrentAction;
if (characterController.readyToPrepareAttack())
{
@ -250,253 +213,67 @@ namespace MWMechanics
actionCooldown = currentAction->getActionCooldown();
}
if (currentAction.get())
currentAction->getCombatRange(rangeAttack, rangeFollow);
// FIXME: consider moving this stuff to ActionWeapon::getCombatRange
const ESM::Weapon *weapon = NULL;
MWMechanics::WeaponType weaptype = WeapType_None;
float weapRange = 1.0f;
// 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
bool isRangedCombat = false;
if (currentAction.get())
{
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();
osg::Vec3f vActorPos(pos.asVec3());
osg::Vec3f vTargetPos(target.getRefData().getPosition().asVec3());
osg::Vec3f vAimDir = MWBase::Environment::get().getWorld()->aimToTarget(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.
if (distToTarget >= rangeAttack
if (distToTarget > rangeAttack
&& !actorClass.isNpc() && !MWMechanics::isEnvironmentCompatible(actor, target))
{
// TODO: start fleeing?
storage.stopAttack();
return false;
return;
}
// for distant combat we should know if target is in LOS even if distToTarget < rangeAttack
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)))
if (storage.mReadyToAttack)
{
mPathFinder.clearPath();
//Melee and Close-up combat
// getXAngleToDir determines vertical angle to target:
// if actor can move along z-axis it will control movement dir
// if can't - it will control correct aiming.
// note: in getZAngleToDir if we preserve dir.z then horizontal angle can be inaccurate
if (distantCombat)
storage.startCombatMove(isRangedCombat, distToTarget, rangeAttack, actor, target);
// start new attack
storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat);
if (isRangedCombat)
{
// rotate actor taking into account target movement direction and projectile speed
osg::Vec3f& lastTargetPos = storage.mLastTargetPos;
vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, REACTION_INTERVAL, weaptype,
storage.mStrength);
vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, AI_REACTION_TIME, (weapon ? weapon->mData.mType : 0), storage.mStrength);
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;
//only once got in melee combat, actor is allowed to use close-up shortcutting
followTarget = true;
storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir);
storage.mMovement.mRotation[2] = getZAngleToDir(vAimDir);
}
}
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)
else
{
// to stop possible sideway moving after target moved out of attack range
storage.stopCombatMove();
readyToAttack = false;
storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir);
storage.mMovement.mRotation[2] = getZAngleToDir((vTargetPos-vActorPos)); // using vAimDir results in spastic movements since the head is animated
}
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);
if (mPathFinder.isPathConstructed())
{
const ESM::Position& pos = actor.getRefData().getPosition();
if (mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1]))
{
actorMovementSettings.mPosition[1] = 0;
}
else
{
evadeObstacles(actor, duration, pos);
}
}
else
{
actorMovementSettings = desiredMovement;
rotateActorOnAxis(actor, 2, actorMovementSettings, desiredMovement);
rotateActorOnAxis(actor, 0, actorMovementSettings, desiredMovement);
}
actorMovementSettings.mPosition[0] = storage.mMovement.mPosition[0];
actorMovementSettings.mPosition[1] = storage.mMovement.mPosition[1];
actorMovementSettings.mPosition[2] = storage.mMovement.mPosition[2];
rotateActorOnAxis(actor, 2, actorMovementSettings, storage.mMovement);
rotateActorOnAxis(actor, 0, actorMovementSettings, storage.mMovement);
}
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
{
return TypeIdCombat;
@ -558,7 +306,6 @@ namespace MWMechanics
return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId);
}
AiCombat *MWMechanics::AiCombat::clone() const
{
return new AiCombat(*this);
@ -575,25 +322,49 @@ namespace MWMechanics
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])
{
mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability();
mCombatMove = true;
}
// only NPCs are smart enough to use dodge movements
else if (isNpc && (!isDistantCombat || (distToTarget < rangeAttack / 2)))
// dodge movements (for NPCs and bipedal creatures)
else if (actor.getClass().isBipedal(actor))
{
//apply sideway movement (kind of dodging) with some probability
if (Misc::Rng::rollClosedProbability() < 0.25)
// get the range of the target's weapon
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();
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)
{
mMovement.mPosition[1] = -1;
@ -630,7 +401,7 @@ namespace MWMechanics
characterController.setAttackingOrSpell(true);
if (!distantCombat)
chooseBestAttack(weapon, mMovement);
characterController.setAIAttackType(chooseBestAttack(weapon));
mStrength = Misc::Rng::rollClosedProbability();
@ -651,7 +422,7 @@ namespace MWMechanics
mAttackCooldown = std::min(baseDelay + 0.01 * Misc::Rng::roll0to99(), baseDelay + 0.9);
}
else
mAttackCooldown -= REACTION_INTERVAL;
mAttackCooldown -= AI_REACTION_TIME;
}
}
@ -678,32 +449,11 @@ namespace MWMechanics
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)
{
//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
if (weapon != NULL)
{
//the more damage attackType deals the more probability it has
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);
if(roll <= slash)
{
movement.mPosition[0] = (Misc::Rng::rollClosedProbability() < 0.5f) ? 1.0f : -1.0f;
movement.mPosition[1] = 0;
attackType = ESM::Weapon::AT_Slash;
}
attackType = "slash";
else if(roll <= (slash + thrust))
{
movement.mPosition[1] = 1;
attackType = ESM::Weapon::AT_Thrust;
}
attackType = "thrust";
else
{
movement.mPosition[1] = movement.mPosition[0] = 0;
attackType = ESM::Weapon::AT_Chop;
}
attackType = "chop";
}
return attackType;

@ -55,19 +55,14 @@ namespace MWMechanics
virtual bool canCancel() const { return false; }
virtual bool shouldCancelPreviousAi() const { return false; }
protected:
virtual bool doesPathNeedRecalc(ESM::Pathgrid::Point dest, const ESM::Cell *cell);
private:
int mTargetActorId;
void buildNewPath(const MWWorld::Ptr& actor, const MWWorld::Ptr& target);
bool reactionTimeActions(const MWWorld::Ptr& actor, CharacterController& characterController,
AiCombatStorage& storage, MWWorld::Ptr target);
void attack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, AiCombatStorage& storage, CharacterController& characterController);
/// 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,
MWMechanics::Movement& actorMovementSettings, MWMechanics::Movement& desiredMovement);
};

@ -40,23 +40,21 @@ int getRangeTypes (const ESM::EffectList& effects)
return types;
}
void suggestCombatRange(int rangeTypes, float& rangeAttack, float& rangeFollow)
float suggestCombatRange(int rangeTypes)
{
if (rangeTypes & Touch)
{
rangeAttack = 100.f;
rangeFollow = 300.f;
static const float fCombatDistance = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fCombatDistance")->getFloat();
return fCombatDistance;
}
else if (rangeTypes & Target)
{
rangeAttack = 1000.f;
rangeFollow = 0.f;
return 1000.f;
}
else
{
// For Self spells, distance doesn't matter, so back away slightly to avoid enemy hits
rangeAttack = 600.f;
rangeFollow = 0.f;
return 600.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);
int types = getRangeTypes(spell->mEffects);
suggestCombatRange(types, rangeAttack, rangeFollow);
isRanged = (types & Target);
return suggestCombatRange(types);
}
void ActionEnchantedItem::prepare(const MWWorld::Ptr &actor)
@ -441,18 +441,17 @@ namespace MWMechanics
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));
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
rangeAttack = 600.f;
rangeFollow = 0.f;
return 600.f;
}
void ActionPotion::prepare(const MWWorld::Ptr &actor)
@ -482,9 +481,35 @@ namespace MWMechanics
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)

@ -16,8 +16,9 @@ namespace MWMechanics
public:
virtual ~Action() {}
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 const ESM::Weapon* getWeapon() const { return NULL; };
};
class ActionSpell : public Action
@ -28,7 +29,7 @@ namespace MWMechanics
/// Sets the given spell as selected on the actor's spell list.
virtual void prepare(const MWWorld::Ptr& actor);
virtual void getCombatRange (float& rangeAttack, float& rangeFollow);
virtual float getCombatRange (bool& isRanged) const;
};
class ActionEnchantedItem : public Action
@ -38,7 +39,7 @@ namespace MWMechanics
MWWorld::ContainerStoreIterator mItem;
/// Sets the given item as selected enchanted item in the actor's InventoryStore.
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
virtual float getActionCooldown() { return 1.f; }
@ -51,7 +52,7 @@ namespace MWMechanics
MWWorld::Ptr mPotion;
/// Drinks the given potion.
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
virtual float getActionCooldown() { return 1.f; }
@ -69,7 +70,8 @@ namespace MWMechanics
: mAmmunition(ammo), mWeapon(weapon) {}
/// Equips the given weapon.
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);

@ -137,35 +137,24 @@ bool AiFollow::execute (const MWWorld::Ptr& actor, CharacterController& characte
//Set the target destination from the actor
ESM::Pathgrid::Point dest = target.getRefData().getPosition().pos;
float dist = distance(dest, pos.pos[0], pos.pos[1], pos.pos[2]);
if (storage.mMoving) //Stop when you get close
storage.mMoving = (dist > followDistance);
else
if (!storage.mMoving)
{
const float threshold = 10;
storage.mMoving = (dist > followDistance + threshold);
const float threshold = 10; // to avoid constant switching between moving/stopping
followDistance += threshold;
}
if(!storage.mMoving)
{
actor.getClass().getMovementSettings(actor).mPosition[1] = 0;
storage.mMoving = !pathTo(actor, dest, duration, followDistance); // Go to the destination
// turn towards target anyway
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
if (storage.mMoving)
{
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)
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
actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, false); //make NPC walk
if (dist > 450)
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
actor.getClass().getCreatureStats(actor).setMovementFlag(MWMechanics::CreatureStats::Flag_Run, false); //make NPC walk
}
return false;
}

@ -19,8 +19,18 @@
#include "actorutil.hpp"
#include "coordinateconverter.hpp"
#include <osg/Quat>
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
{
return MWWorld::Ptr();
@ -51,14 +61,20 @@ bool MWMechanics::AiPackage::getRepeat() const
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, ESM::Pathgrid::Point dest, float duration)
bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest, float duration, float destTolerance)
{
//Update various Timers
mTimer += duration; //Update timer
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;
}
//***********************
/// Checks if you can't get to the end position at all, adds end position to end of path
/// Rebuilds path every quarter of a second, in case the target has moved
//***********************
if(mTimer > 0.25)
// handle path building and shortcutting
ESM::Pathgrid::Point start = pos.pos;
float distToTarget = distance(start, dest);
bool isDestReached = (distToTarget <= destTolerance);
if (!isDestReached && mTimer > AI_REACTION_TIME)
{
const ESM::Cell *cell = actor.getCell()->getCell();
if (doesPathNeedRecalc(dest, cell)) { //Only rebuild path if it's moved
mPathFinder.buildSyncedPath(pos.pos, dest, actor.getCell(), true); //Rebuild path, in case the target has moved
mPrevDest = dest;
}
bool wasShortcutting = mIsShortcutting;
bool destInLOS = false;
if (getTypeId() != TypeIdWander) // prohibit shortcuts for AiWander
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
mPathFinder.addPointToPath(dest); //Adds the final destination to the path, to try to get to where you want to go
if (!mPathFinder.getPath().empty()) //Path has points in it
{
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;
}
//************************
/// Checks if you aren't moving; attempts to unstick you
//************************
if(mPathFinder.checkPathCompleted(pos.pos[0],pos.pos[1])) //Path finished?
if (isDestReached || mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1])) // if path is finished
{
// Reset mTimer so that path will be built right away when a package is repeated
mTimer = 0.26f;
// turn to destination point
zTurn(actor, getZAngleToPoint(start, dest));
smoothTurn(actor, getXAngleToPoint(start, dest), 0);
return true;
}
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);
}
// 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;
}
@ -117,30 +170,106 @@ void MWMechanics::AiPackage::evadeObstacles(const MWWorld::Ptr& actor, float dur
zTurn(actor, mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]));
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
MWWorld::Ptr door = getNearbyDoor(actor);
if (door != MWWorld::Ptr()) // NOTE: checks interior cells only
// note: AiWander currently does not open doors
if (getTypeId() != TypeIdWander && !door.getCellRef().getTeleport() && door.getCellRef().getTrap().empty()
&& door.getCellRef().getLockLevel() <= 0 && door.getClass().getDoorState(door) == 0)
{
if (!door.getCellRef().getTeleport() && door.getCellRef().getTrap().empty()
&& door.getCellRef().getLockLevel() <= 0 && door.getClass().getDoorState(door) == 0) {
MWBase::Environment::get().getWorld()->activateDoor(door, 1);
}
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
movement.mPosition[1] = 1; //Just run forward
if (!isClear)
{
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)
@ -173,3 +302,32 @@ bool MWMechanics::AiPackage::isNearInactiveCell(const ESM::Position& actorPos)
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
{
const float AI_REACTION_TIME = 0.25f;
class CharacterController;
@ -91,14 +92,29 @@ namespace MWMechanics
/// Return true if this package should repeat. Currently only used for Wander packages.
virtual bool getRepeat() const;
/// Reset pathfinding state
void reset();
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:
/// 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 **/
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);
@ -108,11 +124,16 @@ namespace MWMechanics
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:
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())
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
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
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
actor.getClass().getMovementSettings(actor).mPosition[1] = 0;
target.getClass().activate(target,actor).get()->execute(actor); //Arrest player
if (pathTo(actor, dest, duration, 100)) {
target.getClass().activate(target,actor).get()->execute(actor); //Arrest player when reached
return true;
}
else {
pathTo(actor, dest, duration); //Go to the destination
}
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
if (isActualAiPackage(packageTypeId) && (mRepeat || package->getRepeat()))
{
package->reset();
mPackages.push_back(package->clone());
}
// 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)
: 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)
: 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;
}
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
{
return TypeIdTravel;

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

@ -29,7 +29,6 @@ namespace MWMechanics
{
static const int COUNT_BEFORE_RESET = 10;
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_END = 10;
@ -74,8 +73,6 @@ namespace MWMechanics
unsigned short mIdleAnimation;
std::vector<unsigned short> mBadIdles; // Idle animations that when called cause errors
PathFinder mPathFinder;
// do we need to calculate allowed nodes based on mDistance
bool mPopulateAvailableNodes;
@ -86,8 +83,6 @@ namespace MWMechanics
ESM::Pathgrid::Point mCurrentNode;
bool mTrimCurrentNode;
ObstacleCheck mObstacleCheck;
float mDoorCheckDuration;
int mStuckCount;
@ -196,7 +191,6 @@ namespace MWMechanics
{
// get or create temporary storage
AiWanderStorage& storage = state.get<AiWanderStorage>();
const MWWorld::CellStore*& currentCell = storage.mCell;
MWMechanics::CreatureStats& cStats = actor.getClass().getCreatureStats(actor);
@ -206,6 +200,7 @@ namespace MWMechanics
bool cellChange = currentCell && (actor.getCell() != currentCell);
if(!currentCell || cellChange)
{
stopWalking(actor, storage);
currentCell = actor.getCell();
storage.mPopulateAvailableNodes = true;
}
@ -223,7 +218,7 @@ namespace MWMechanics
float& lastReaction = storage.mReaction;
lastReaction += duration;
if (REACTION_INTERVAL <= lastReaction)
if (AI_REACTION_TIME <= lastReaction)
{
lastReaction = 0;
return reactionTimeActions(actor, storage, currentCell, cellChange, pos, duration);
@ -273,7 +268,7 @@ namespace MWMechanics
}
// 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);
}
@ -300,14 +295,14 @@ namespace MWMechanics
if ((wanderState == Wander_MoveNow) && storage.mCanWanderAlongPathGrid)
{
// Construct a new path if there isn't one
if(!storage.mPathFinder.isPathConstructed())
if(!mPathFinder.isPathConstructed())
{
if (!storage.mAllowedNodes.empty())
{
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);
}
@ -337,7 +332,7 @@ namespace MWMechanics
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));
@ -345,9 +340,9 @@ namespace MWMechanics
ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(pos));
// 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);
}
@ -379,9 +374,13 @@ namespace MWMechanics
// Check if land creature will walk onto water or if water creature will swim onto land
if ((!isWaterCreature && !destinationIsAtWater(actor, destination)) ||
(isWaterCreature && !destinationThroughGround(currentPositionVec3f, destination))) {
storage.mPathFinder.buildSyncedPath(currentPosition, destinationPosition, actor.getCell(), true);
storage.mPathFinder.addPointToPath(destinationPosition);
storage.setState(Wander_Walking, true);
mPathFinder.buildSyncedPath(currentPosition, destinationPosition, actor.getCell());
mPathFinder.addPointToPath(destinationPosition);
if (mPathFinder.isPathConstructed())
{
storage.setState(Wander_Walking, true);
}
return;
}
} while (--attempts);
@ -407,7 +406,7 @@ namespace MWMechanics
void AiWander::completeManualWalking(const MWWorld::Ptr &actor, AiWanderStorage &storage) {
stopWalking(actor, storage);
storage.mObstacleCheck.clear();
mObstacleCheck.clear();
storage.setState(Wander_IdleNow);
}
@ -475,7 +474,7 @@ namespace MWMechanics
float duration, AiWanderStorage& storage, ESM::Position& pos)
{
// 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);
storage.setState(Wander_ChooseAction);
@ -517,40 +516,27 @@ namespace MWMechanics
void AiWander::evadeObstacles(const MWWorld::Ptr& actor, AiWanderStorage& storage, float duration, ESM::Position& pos)
{
// turn towards the next point in mPath
zTurn(actor, storage.mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]));
MWMechanics::Movement& movement = actor.getClass().getMovementSettings(actor);
if (storage.mObstacleCheck.check(actor, duration))
if (mObstacleCheck.isEvading())
{
// first check if we're walking into a door
if (proximityToDoor(actor)) // NOTE: checks interior cells only
{
// remove allowed points then select another random destination
storage.mTrimCurrentNode = true;
trimAllowedNodes(storage.mAllowedNodes, storage.mPathFinder);
storage.mObstacleCheck.clear();
storage.mPathFinder.clearPath();
trimAllowedNodes(storage.mAllowedNodes, mPathFinder);
mObstacleCheck.clear();
mPathFinder.clearPath();
storage.setState(Wander_MoveNow);
}
else // probably walking into another NPC
{
// 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;
storage.mStuckCount++; // TODO: maybe no longer needed
}
// if stuck for sufficiently long, act like current location was the destination
if (storage.mStuckCount >= COUNT_BEFORE_RESET) // something has gone wrong, reset
{
//std::cout << "Reset \""<< cls.getName(actor) << "\"" << std::endl;
storage.mObstacleCheck.clear();
mObstacleCheck.clear();
stopWalking(actor, storage);
storage.setState(Wander_ChooseAction);
@ -627,7 +613,7 @@ namespace MWMechanics
if (storage.mState == Wander_Walking)
{
stopWalking(actor, storage);
storage.mObstacleCheck.clear();
mObstacleCheck.clear();
storage.setState(Wander_IdleNow);
}
@ -667,9 +653,9 @@ namespace MWMechanics
ESM::Pathgrid::Point start(PathFinder::MakePathgridPoint(actorPos));
// 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):
ESM::Pathgrid::Point temp = storage.mAllowedNodes[randNode];
@ -726,7 +712,7 @@ namespace MWMechanics
void AiWander::stopWalking(const MWWorld::Ptr& actor, AiWanderStorage& storage)
{
storage.mPathFinder.clearPath();
mPathFinder.clearPath();
actor.getClass().getMovementSettings(actor).mPosition[1] = 0;
}

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

@ -1208,7 +1208,6 @@ bool CharacterController::updateWeaponState()
if(mUpperBodyState == UpperCharState_WeapEquiped && (mHitState == CharState_None || mHitState == CharState_Block))
{
MWBase::Environment::get().getWorld()->breakInvisibility(mPtr);
mAttackType.clear();
if(mWeaponType == WeapType_Spell)
{
// Unset casting flag, otherwise pressing the mouse button down would
@ -1237,19 +1236,27 @@ bool CharacterController::updateWeaponState()
cast.playSpellCastingEffects(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;
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");
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);
for (size_t iter = 0; iter < spell->mEffects.mList.size(); ++iter) // play hands vfx for each effect
{
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 1: mAttackType = "touch"; break;
@ -1309,14 +1316,17 @@ bool CharacterController::updateWeaponState()
{
if (isWeapon)
{
if(mPtr == getPlayer() &&
Settings::Manager::getBool("best attack", "Game"))
if(mPtr == getPlayer())
{
MWWorld::ContainerStoreIterator weapon = mPtr.getClass().getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight);
mAttackType = getBestAttack(weapon->get<ESM::Weapon>()->mBase);
if (Settings::Manager::getBool("best attack", "Game"))
{
MWWorld::ContainerStoreIterator weapon = mPtr.getClass().getInventoryStore(mPtr).getSlot(MWWorld::InventoryStore::Slot_CarriedRight);
mAttackType = getBestAttack(weapon->get<ESM::Weapon>()->mBase);
}
else
setAttackTypeBasedOnMovement();
}
else
setAttackTypeBasedOnMovement();
// else if (mPtr != getPlayer()) use mAttackType already set by AiCombat
}
else
setAttackTypeRandomly();
@ -1761,7 +1771,7 @@ void CharacterController::update(float duration)
float realHealthLost = static_cast<float>(healthLost * (1.0f - 0.25f * fatigueTerm));
health.setCurrent(health.getCurrent() - realHealthLost);
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);
if (healthLost > (acrobaticsSkill * fatigueTerm))
@ -2227,6 +2237,11 @@ void CharacterController::setAttackingOrSpell(bool attackingOrSpell)
mAttackingOrSpell = attackingOrSpell;
}
void CharacterController::setAIAttackType(std::string attackType)
{
mAttackType = attackType;
}
bool CharacterController::readyToPrepareAttack() const
{
return (mHitState == CharState_None || mHitState == CharState_Block)

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

@ -207,9 +207,9 @@ namespace MWMechanics
if (Misc::Rng::roll0to99() >= getHitChance(attacker, victim, skillValue))
{
if(attacker == getPlayer())
if (attacker == getPlayer())
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);
return;
}
@ -237,9 +237,6 @@ namespace MWMechanics
if (weapon != projectile)
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
if (victim != getPlayer()
&& !appliedEnchantment)
@ -249,7 +246,7 @@ namespace MWMechanics
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)

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

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

@ -82,6 +82,43 @@ namespace MWMechanics
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()
: mPathgrid(NULL),
mCell(NULL)
@ -132,23 +169,10 @@ namespace MWMechanics
*/
void PathFinder::buildPath(const ESM::Pathgrid::Point &startPoint,
const ESM::Pathgrid::Point &endPoint,
const MWWorld::CellStore* cell,
bool allowShortcuts)
const MWWorld::CellStore* cell)
{
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)
{
mCell = cell;
@ -243,6 +267,19 @@ namespace MWMechanics
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)
{
if(mPath.empty())
@ -264,19 +301,18 @@ namespace MWMechanics
// see header for the rationale
void PathFinder::buildSyncedPath(const ESM::Pathgrid::Point &startPoint,
const ESM::Pathgrid::Point &endPoint,
const MWWorld::CellStore* cell,
bool allowShortcuts)
const MWWorld::CellStore* cell)
{
if (mPath.size() < 2)
{
// if path has one point, then it's the destination.
// don't need to worry about bad path for this case
buildPath(startPoint, endPoint, cell, allowShortcuts);
buildPath(startPoint, endPoint, cell);
}
else
{
const ESM::Pathgrid::Point oldStart(*getPath().begin());
buildPath(startPoint, endPoint, cell, allowShortcuts);
buildPath(startPoint, endPoint, cell);
if (mPath.size() >= 2)
{
// 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& 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
{
public:
@ -39,12 +53,17 @@ namespace MWMechanics
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);
///< \Returns true if we are within \a tolerance units of the last path point.
/// In radians
float getZAngleToNext(float x, float y) const;
float getXAngleToNext(float x, float y, float z) const;
bool isPathConstructed() const
{
return !mPath.empty();
@ -60,6 +79,8 @@ namespace MWMechanics
return mPath;
}
const MWWorld::CellStore* getPathCell() const;
/** Synchronize new path with old one to avoid visiting 1 waypoint 2 times
@note
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.
*/
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);
}
@ -130,9 +151,6 @@ namespace MWMechanics
}
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;
const ESM::Pathgrid *mPathgrid;

@ -2,6 +2,7 @@
#include <cfloat>
#include <limits>
#include <iomanip>
#include <boost/format.hpp>
@ -29,41 +30,6 @@
#include "npcstats.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
{
@ -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,
const ESM::EffectList &effects, ESM::RangeType range, bool reflected, bool exploded)
{
@ -358,7 +339,6 @@ namespace MWMechanics
ESM::EffectList reflectedEffects;
std::vector<ActiveSpells::ActiveEffect> appliedLastingEffects;
bool firstAppliedEffect = true;
bool anyHarmfulEffect = false;
// 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)
{
// Play sound, only for the first effect
if (firstAppliedEffect)
{
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);
else
sndMgr->playSound3D(target, schools[magicEffect->mData.mSchool]+" hit", 1.0f, 1.0f);
firstAppliedEffect = false;
}
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);
else
sndMgr->playSound3D(target, schools[magicEffect->mData.mSchool]+" hit", 1.0f, 1.0f);
// Add VFX
const ESM::Static* castStatic;
@ -596,7 +571,7 @@ namespace MWMechanics
// Notify the target actor they've been hit
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)
@ -793,17 +768,7 @@ namespace MWMechanics
}
if (launchProjectile)
{
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));
}
launchMagicBolt(enchantment->mEffects);
else if (!mTarget.isEmpty())
inflict(mTarget, mCaster, enchantment->mEffects, ESM::RT_Target);
@ -909,28 +874,9 @@ namespace MWMechanics
inflict(mCaster, mCaster, spell->mEffects, ESM::RT_Self);
if (!mTarget.isEmpty())
{
inflict(mTarget, mCaster, spell->mEffects, ESM::RT_Touch);
}
std::string projectileModel;
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);
}
launchMagicBolt(spell->mEffects);
return true;
}
@ -1003,36 +949,39 @@ namespace MWMechanics
const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore();
const ESM::Spell *spell = store.get<ESM::Spell>().find(spellid);
const ESM::ENAMstruct &effectentry = spell->mEffects.mList.at(0);
const ESM::MagicEffect *effect;
effect = store.get<ESM::MagicEffect>().find(effectentry.mEffectID);
for (std::vector<ESM::ENAMstruct>::const_iterator iter = spell->mEffects.mList.begin();
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
{
const ESM::Static* castStatic;
if (!effect->mCasting.empty())
castStatic = store.get<ESM::Static>().find (effect->mCasting);
else
castStatic = store.get<ESM::Static>().find ("VFX_DefaultCast");
if (mCaster.getClass().isActor()) // TODO: Non-actors (except for large statics?) should also create a spell cast vfx
{
const ESM::Static* castStatic;
if (!effect->mCasting.empty())
castStatic = store.get<ESM::Static>().find (effect->mCasting);
else
castStatic = store.get<ESM::Static>().find ("VFX_DefaultCast");
animation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex);
}
animation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex);
}
if (!mCaster.getClass().isActor())
animation->addSpellCastGlow(effect);
if (!mCaster.getClass().isActor())
animation->addSpellCastGlow(effect);
static const std::string schools[] = {
"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(!effect->mCastSound.empty())
sndMgr->playSound3D(mCaster, effect->mCastSound, 1.0f, 1.0f);
else
sndMgr->playSound3D(mCaster, schools[effect->mData.mSchool]+" cast", 1.0f, 1.0f);
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
if(!effect->mCastSound.empty())
sndMgr->playSound3D(mCaster, effect->mCastSound, 1.0f, 1.0f);
else
sndMgr->playSound3D(mCaster, schools[effect->mData.mSchool]+" cast", 1.0f, 1.0f);
}
}
int getEffectiveEnchantmentCastCost(float castCost, const MWWorld::Ptr &actor)

@ -94,6 +94,9 @@ namespace MWMechanics
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 caster can be any type of object, or even an empty object.
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_100"))
item = "gold_001";
MWWorld::ContainerStore& store = ptr.getClass().getContainerStore (ptr);
runtime.push (store.count(item));
@ -136,7 +136,7 @@ namespace MWScript
|| ::Misc::StringUtils::ciEqual(item, "gold_025")
|| ::Misc::StringUtils::ciEqual(item, "gold_100"))
item = "gold_001";
MWWorld::ContainerStore& store = ptr.getClass().getContainerStore (ptr);
std::string itemName;
@ -188,7 +188,11 @@ namespace MWScript
break;
}
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())
MWBase::Environment::get().getWindowManager()->useItem(*it);

@ -98,7 +98,7 @@ namespace MWWorld
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");
}

@ -120,7 +120,7 @@ namespace MWWorld
/// enums. ignored for creature attacks.
/// (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
/// 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

@ -1,5 +1,7 @@
#include "projectilemanager.hpp"
#include <iomanip>
#include <osg/PositionAttitudeTransform>
#include <components/esm/esmwriter.hpp>
@ -33,6 +35,56 @@
#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
{
@ -94,6 +146,18 @@ namespace MWWorld
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;
state.mNode->accept(disableFreezeOnCullVisitor);
@ -112,10 +176,8 @@ namespace MWWorld
state.mEffectAnimationTime->addTime(duration);
}
void ProjectileManager::launchMagicBolt(const std::string &model, const std::string &sound,
const std::string &spellId, float speed, bool stack,
const ESM::EffectList &effects, const Ptr &caster, const std::string &sourceName,
const osg::Vec3f& fallbackDirection)
void ProjectileManager::launchMagicBolt(const std::string &spellId, bool stack, const ESM::EffectList &effects, const Ptr &caster,
const std::string &sourceName, const osg::Vec3f& fallbackDirection)
{
osg::Vec3f pos = caster.getRefData().getPosition().asVec3();
if (caster.getClass().isActor())
@ -137,33 +199,31 @@ namespace MWWorld
MagicBoltState state;
state.mSourceName = sourceName;
state.mId = model;
state.mSpellId = spellId;
state.mCasterHandle = caster;
if (caster.getClass().isActor())
state.mActorId = caster.getClass().getCreatureStats(caster).getActorId();
else
state.mActorId = -1;
state.mSpeed = speed;
state.mStack = stack;
state.mSoundId = sound;
// Only interested in "on target" 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);
}
state.mEffects = getMagicBoltData(state.mIdMagic, state.mSoundIds, state.mSpeed, effects);
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();
createModel(state, ptr.getClass().getModel(ptr), pos, orient, true);
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);
}
@ -173,7 +233,7 @@ namespace MWWorld
state.mActorId = actor.getClass().getCreatureStats(actor).getActorId();
state.mBowId = bow.getCellRef().getRefId();
state.mVelocity = orient * osg::Vec3f(0,1,0) * speed;
state.mId = projectile.getCellRef().getRefId();
state.mIdArrow = projectile.getCellRef().getRefId();
state.mCasterHandle = actor;
state.mAttackStrength = attackStrength;
@ -205,8 +265,10 @@ namespace MWWorld
osg::Vec3f pos(it->mNode->getPosition());
osg::Vec3f newPos = pos + direction * duration * speed;
if (it->mSound.get())
it->mSound->setPosition(newPos);
for (size_t soundIter = 0; soundIter != it->mSounds.size(); soundIter++)
{
it->mSounds.at(soundIter)->setPosition(newPos);
}
it->mNode->setPosition(newPos);
@ -246,7 +308,11 @@ namespace MWWorld
MWBase::Environment::get().getWorld()->explodeSpell(pos, it->mEffects, caster, result.mHitObject,
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);
it = mMagicBolts.erase(it);
@ -286,7 +352,7 @@ namespace MWWorld
{
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.
MWWorld::Ptr bow = projectileRef.getPtr();
@ -326,7 +392,10 @@ namespace MWWorld
for (std::vector<MagicBoltState>::iterator it = mMagicBolts.begin(); it != mMagicBolts.end(); ++it)
{
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();
}
@ -338,7 +407,7 @@ namespace MWWorld
writer.startRecord(ESM::REC_PROJ);
ESM::ProjectileState state;
state.mId = it->mId;
state.mId = it->mIdArrow;
state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition()));
state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude()));
state.mActorId = it->mActorId;
@ -357,14 +426,14 @@ namespace MWWorld
writer.startRecord(ESM::REC_MPRJ);
ESM::MagicBoltState state;
state.mId = it->mId;
state.mId = it->mIdMagic.at(0);
state.mPosition = ESM::Vector3(osg::Vec3f(it->mNode->getPosition()));
state.mOrientation = ESM::Quaternion(osg::Quat(it->mNode->getAttitude()));
state.mActorId = it->mActorId;
state.mSpellId = it->mSpellId;
state.mEffects = it->mEffects;
state.mSound = it->mSoundId;
state.mSound = it->mSoundIds.at(0);
state.mSourceName = it->mSourceName;
state.mSpeed = it->mSpeed;
state.mStack = it->mStack;
@ -386,7 +455,7 @@ namespace MWWorld
state.mActorId = esm.mActorId;
state.mBowId = esm.mBowId;
state.mVelocity = esm.mVelocity;
state.mId = esm.mId;
state.mIdArrow = esm.mId;
state.mAttackStrength = esm.mAttackStrength;
std::string model;
@ -413,17 +482,20 @@ namespace MWWorld
MagicBoltState state;
state.mSourceName = esm.mSourceName;
state.mId = esm.mId;
state.mIdMagic.push_back(esm.mId);
state.mSpellId = esm.mSpellId;
state.mActorId = esm.mActorId;
state.mSpeed = esm.mSpeed;
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;
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();
model = ptr.getClass().getModel(ptr);
}
@ -435,9 +507,12 @@ namespace MWWorld
createModel(state, model, osg::Vec3f(esm.mPosition), osg::Quat(esm.mOrientation), true);
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);
state.mSoundId = esm.mSound;
for (size_t soundIter = 0; soundIter != state.mSoundIds.size(); soundIter++)
{
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);
return true;

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

@ -2726,11 +2726,10 @@ namespace MWWorld
mProjectileManager->launchProjectile(actor, projectile, worldPos, orient, bow, speed, attackStrength);
}
void World::launchMagicBolt (const std::string& model, const std::string &sound, const std::string &spellId,
float speed, bool stack, const ESM::EffectList& effects,
const MWWorld::Ptr& caster, const std::string& sourceName, const osg::Vec3f& fallbackDirection)
void World::launchMagicBolt (const std::string &spellId, bool stack, const ESM::EffectList& effects,
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
@ -3184,8 +3183,8 @@ namespace MWWorld
{
const ESM::MagicEffect* effect = getStore().get<ESM::MagicEffect>().find(effectIt->mEffectID);
if (effectIt->mArea <= 0)
continue; // Not an area effect
if ((effectIt->mArea <= 0 && !ignore.isEmpty() && ignore.getClass().isActor()) || effectIt->mRange != rangeType)
continue; // Not right range type, or not area effect and hit an actor
// Spawn the explosion orb effect
const ESM::Static* areaStatic;
@ -3194,7 +3193,13 @@ namespace MWWorld
else
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)
static const std::string schools[] = {

@ -594,9 +594,8 @@ namespace MWWorld
*/
virtual void castSpell (const MWWorld::Ptr& actor);
virtual void launchMagicBolt (const std::string& model, const std::string& sound, const std::string& spellId,
float speed, bool stack, const ESM::EffectList& effects,
const MWWorld::Ptr& caster, const std::string& sourceName, const osg::Vec3f& fallbackDirection);
virtual void launchMagicBolt (const std::string& spellId, bool stack, const ESM::EffectList& effects,
const MWWorld::Ptr& caster, const std::string& sourceName, const osg::Vec3f& fallbackDirection);
virtual void launchProjectile (MWWorld::Ptr actor, MWWorld::ConstPtr projectile,
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;
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)

@ -10,6 +10,7 @@ Components
openmw/index
openmw-cs/index
openmw-mods/index
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