forked from teamnwah/openmw-tes3coop
Resolve conflicts in pull request #55
# Conflicts: # README.md # apps/openmw/mwclass/npc.cpp # apps/openmw/mwmechanics/combat.cpp
This commit is contained in:
commit
3b7693c719
49 changed files with 974 additions and 785 deletions
|
@ -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,97 +213,14 @@ 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))
|
||||
bool isRangedCombat = false;
|
||||
if (currentAction.get())
|
||||
{
|
||||
//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;
|
||||
rangeAttack = currentAction->getCombatRange(isRangedCombat);
|
||||
// Get weapon characteristics
|
||||
weapon = currentAction->getWeapon();
|
||||
}
|
||||
else //is creature
|
||||
{
|
||||
weaptype = actorClass.getCreatureStats(actor).getDrawState() == DrawState_Spell ? WeapType_Spell : WeapType_HandToHand;
|
||||
weapRange = fCombatDistance;
|
||||
}
|
||||
|
||||
bool distantCombat = false;
|
||||
if (weaptype != WeapType_Spell)
|
||||
{
|
||||
// TODO: move to ActionWeapon
|
||||
if (weaptype == WeapType_BowAndArrow || weaptype == WeapType_Crossbow || weaptype == WeapType_Thrown)
|
||||
{
|
||||
rangeAttack = 1000;
|
||||
rangeFollow = 0; // not needed in ranged combat
|
||||
distantCombat = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
rangeAttack = weapRange;
|
||||
rangeFollow = 300;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
distantCombat = (rangeAttack > 500);
|
||||
}
|
||||
|
||||
|
||||
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());
|
||||
|
@ -348,155 +228,52 @@ namespace MWMechanics
|
|||
|
||||
osg::Vec3f vAimDir = MWBase::Environment::get().getWorld()->aimToTarget(actor, target);
|
||||
float distToTarget = MWBase::Environment::get().getWorld()->getHitDistance(actor, target);
|
||||
|
||||
storage.mReadyToAttack = (distToTarget <= rangeAttack);
|
||||
|
||||
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);
|
||||
|
||||
// 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);
|
||||
|
||||
storage.mMovement.mRotation[0] = getXAngleToDir(vAimDir);
|
||||
storage.mMovement.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((vTargetPos-vActorPos)); // using vAimDir results in spastic movements since the head is animated
|
||||
}
|
||||
}
|
||||
else // remote pathfinding
|
||||
{
|
||||
bool preferShortcut = false;
|
||||
if (!distantCombat) inLOS = world->getLOS(actor, target);
|
||||
|
||||
// check if shortcut is available
|
||||
bool& forceNoShortcut = storage.mForceNoShortcut;
|
||||
ESM::Position& shortcutFailPos = storage.mShortcutFailPos;
|
||||
|
||||
if(inLOS && (!isStuck || readyToAttack)
|
||||
&& (!forceNoShortcut || (shortcutFailPos.asVec3() - vActorPos).length() >= PATHFIND_SHORTCUT_RETRY_DIST))
|
||||
{
|
||||
if(speed == 0.0f) speed = actorClass.getSpeed(actor);
|
||||
// maximum dist before pit/obstacle for actor to avoid them depending on his speed
|
||||
float maxAvoidDist = REACTION_INTERVAL * speed + speed / MAX_VEL_ANGULAR_RADIANS * 2; // *2 - for reliability
|
||||
preferShortcut = checkWayIsClear(vActorPos, vTargetPos, osg::Vec3f(vAimDir.x(), vAimDir.y(), 0).length() > maxAvoidDist*1.5? maxAvoidDist : maxAvoidDist/2);
|
||||
}
|
||||
|
||||
// don't use pathgrid when actor can move in 3 dimensions
|
||||
if (canMoveByZ)
|
||||
{
|
||||
preferShortcut = true;
|
||||
movement.mRotation[0] = getXAngleToDir(vAimDir);
|
||||
}
|
||||
|
||||
if(preferShortcut)
|
||||
{
|
||||
movement.mRotation[2] = getZAngleToDir((vTargetPos-vActorPos));
|
||||
forceNoShortcut = false;
|
||||
shortcutFailPos.pos[0] = shortcutFailPos.pos[1] = shortcutFailPos.pos[2] = 0;
|
||||
mPathFinder.clearPath();
|
||||
}
|
||||
else // if shortcut failed stick to path grid
|
||||
{
|
||||
if(!isStuck && shortcutFailPos.pos[0] == 0.0f && shortcutFailPos.pos[1] == 0.0f && shortcutFailPos.pos[2] == 0.0f)
|
||||
{
|
||||
forceNoShortcut = true;
|
||||
shortcutFailPos = pos;
|
||||
}
|
||||
|
||||
followTarget = false;
|
||||
|
||||
buildNewPath(actor, target);
|
||||
|
||||
// should always return a path (even if it's just go straight on target.)
|
||||
assert(mPathFinder.isPathConstructed());
|
||||
}
|
||||
|
||||
if (readyToAttack)
|
||||
{
|
||||
// to stop possible sideway moving after target moved out of attack range
|
||||
storage.stopCombatMove();
|
||||
readyToAttack = false;
|
||||
}
|
||||
movement.mPosition[1] = 1;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void AiCombat::updateActorsMovement(const MWWorld::Ptr& actor, float duration, MWMechanics::Movement& desiredMovement)
|
||||
void AiCombat::updateActorsMovement(const MWWorld::Ptr& actor, float duration, AiCombatStorage& storage)
|
||||
{
|
||||
// apply combat movement
|
||||
MWMechanics::Movement& actorMovementSettings = actor.getClass().getMovementSettings(actor);
|
||||
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))
|
||||
{
|
||||
mMovement.mPosition[0] = Misc::Rng::rollProbability() < 0.5 ? 1.0f : -1.0f;
|
||||
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; // 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
|
||||
{
|
||||
// Already done in AiCombat itself
|
||||
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
|
||||
{
|
||||
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;
|
||||
|
||||
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
|
||||
// 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 (!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);
|
||||
}
|
||||
}
|
||||
else // probably walking into another NPC
|
||||
{
|
||||
mObstacleCheck.takeEvasiveAction(movement);
|
||||
MWBase::Environment::get().getWorld()->activateDoor(door, 1);
|
||||
}
|
||||
}
|
||||
else { //Not stuck, so reset things
|
||||
movement.mPosition[1] = 1; //Just run forward
|
||||
else // any other obstacle (NPC, crate, etc.)
|
||||
{
|
||||
mObstacleCheck.takeEvasiveAction(movement);
|
||||
}
|
||||
}
|
||||
|
||||
bool MWMechanics::AiPackage::doesPathNeedRecalc(ESM::Pathgrid::Point dest, const ESM::Cell *cell)
|
||||
bool MWMechanics::AiPackage::shortcutPath(const ESM::Pathgrid::Point& startPoint, const ESM::Pathgrid::Point& endPoint, const MWWorld::Ptr& actor, bool *destInLOS)
|
||||
{
|
||||
return mPathFinder.getPath().empty() || (distance(mPrevDest, dest) > 10);
|
||||
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)
|
||||
{
|
||||
mShortcutProhibited = false;
|
||||
mShortcutFailPos = ESM::Pathgrid::Point();
|
||||
}
|
||||
}
|
||||
if (!isClear)
|
||||
{
|
||||
if (mShortcutFailPos.mX == 0 && mShortcutFailPos.mY == 0 && mShortcutFailPos.mZ == 0)
|
||||
{
|
||||
mShortcutProhibited = true;
|
||||
mShortcutFailPos = startPoint;
|
||||
}
|
||||
}
|
||||
|
||||
return isClear;
|
||||
}
|
||||
|
||||
bool MWMechanics::AiPackage::doesPathNeedRecalc(const ESM::Pathgrid::Point& newDest, const MWWorld::CellStore* currentCell)
|
||||
{
|
||||
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);
|
||||
|
||||
protected:
|
||||
/// Causes the actor to attempt to walk to the specified location
|
||||
/** \return If the actor has arrived at his destination **/
|
||||
bool pathTo(const MWWorld::Ptr& actor, ESM::Pathgrid::Point dest, float duration);
|
||||
/// 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);
|
||||
|
||||
virtual bool doesPathNeedRecalc(ESM::Pathgrid::Point dest, const ESM::Cell *cell);
|
||||
protected:
|
||||
/// Handles path building and shortcutting with obstacles avoiding
|
||||
/** \return If the actor has arrived at his destination **/
|
||||
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(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);
|
||||
|
||||
switch(effectentry.mRange)
|
||||
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(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"
|
||||
};
|
||||
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;
|
||||
}
|
||||
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);
|
||||
|
||||
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
|
||||
for (std::vector<ESM::ENAMstruct>::const_iterator iter = spell->mEffects.mList.begin();
|
||||
iter != spell->mEffects.mList.end(); ++iter)
|
||||
{
|
||||
const ESM::Static* castStatic;
|
||||
if (!effect->mCasting.empty())
|
||||
castStatic = store.get<ESM::Static>().find (effect->mCasting);
|
||||
const ESM::MagicEffect *effect;
|
||||
effect = store.get<ESM::MagicEffect>().find(iter->mEffectID);
|
||||
|
||||
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");
|
||||
|
||||
animation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex);
|
||||
}
|
||||
|
||||
if (!mCaster.getClass().isActor())
|
||||
animation->addSpellCastGlow(effect);
|
||||
|
||||
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
|
||||
castStatic = store.get<ESM::Static>().find ("VFX_DefaultCast");
|
||||
|
||||
animation->addEffect("meshes\\" + castStatic->mModel, effect->mIndex);
|
||||
sndMgr->playSound3D(mCaster, schools[effect->mData.mSchool]+" cast", 1.0f, 1.0f);
|
||||
}
|
||||
|
||||
if (!mCaster.getClass().isActor())
|
||||
animation->addSpellCastGlow(effect);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
|
|
58
docs/source/openmw-mods/differences.rst
Normal file
58
docs/source/openmw-mods/differences.rst
Normal file
|
@ -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>
|
4
docs/source/openmw-mods/foreword.rst
Normal file
4
docs/source/openmw-mods/foreword.rst
Normal file
|
@ -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!
|
16
docs/source/openmw-mods/index.rst
Normal file
16
docs/source/openmw-mods/index.rst
Normal file
|
@ -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
|
27
docs/source/openmw-mods/mod-install.rst
Normal file
27
docs/source/openmw-mods/mod-install.rst
Normal file
|
@ -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.
|
||||
|
||||
#.
|
53
docs/source/tutorial-style-guide.txt
Normal file
53
docs/source/tutorial-style-guide.txt
Normal file
|
@ -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…
Reference in a new issue