Commit 49d81241 authored by elsid's avatar elsid

Merge branch 'master' into pathfinder_detour

parents 50b6ae3e fc82e680
......@@ -8,6 +8,7 @@
Bug #2256: Landing sound not playing when jumping immediately after landing
Bug #2274: Thin platform clips through player character instead of lifting
Bug #2326: After a bound item expires the last equipped item of that type is not automatically re-equipped
Bug #2446: Restore Attribute/Skill should allow restoring drained attributes
Bug #2455: Creatures attacks degrade armor
Bug #2562: Forcing AI to activate a teleport door sometimes causes a crash
Bug #2626: Resurrecting the player does not resume the game
......@@ -33,6 +34,7 @@
Bug #3788: GetPCInJail and GetPCTraveling do not work as in vanilla
Bug #3836: Script fails to compile when command argument contains "\n"
Bug #3876: Landscape texture painting is misaligned
Bug #3890: Magic light source attenuation is inaccurate
Bug #3897: Have Goodbye give all choices the effects of Goodbye
Bug #3911: [macOS] Typing in the "Content List name" dialog box produces double characters
Bug #3920: RemoveSpellEffects doesn't remove constant effects
......@@ -45,6 +47,7 @@
Bug #4110: Fixed undo / redo menu text losing the assigned shortcuts
Bug #4125: OpenMW logo cropped on bugtracker
Bug #4215: OpenMW shows book text after last EOL tag
Bug #4217: Fixme implementation differs from Morrowind's
Bug #4221: Characters get stuck in V-shaped terrain
Bug #4230: AiTravel package issues break some Tribunal quests
Bug #4231: Infected rats from the "Crimson Plague" quest rendered unconscious by change in Drain Fatigue functionality
......@@ -54,6 +57,7 @@
Bug #4274: Pre-0.43 death animations are not forward-compatible with 0.43+
Bug #4286: Scripted animations can be interrupted
Bug #4291: Non-persistent actors that started the game as dead do not play death animations
Bug #4292: CenterOnCell implementation differs from vanilla
Bug #4293: Faction members are not aware of faction ownerships in barter
Bug #4304: "Follow" not working as a second AI package
Bug #4307: World cleanup should remove dead bodies only if death animation is finished
......@@ -64,7 +68,7 @@
Bug #4368: Settings window ok button doesn't have key focus by default
Bug #4378: On-self absorb spells restore stats
Bug #4393: NPCs walk back to where they were after using ResetActors
Bug #4416: Handle exception if we try to play non-music file
Bug #4416: Non-music files crash the game when they are tried to be played
Bug #4419: MRK NiStringExtraData is handled incorrectly
Bug #4426: RotateWorld behavior is incorrect
Bug #4429: [Windows] Error on build INSTALL.vcxproj project (debug) with cmake 3.7.2
......@@ -80,6 +84,7 @@
Bug #4459: NotCell dialogue condition doesn't support partial matches
Bug #4460: Script function "Equip" doesn't bypass beast restrictions
Bug #4461: "Open" spell from non-player caster isn't a crime
Bug #4463: %g format doesn't return more digits
Bug #4464: OpenMW keeps AiState cached storages even after we cancel AI packages
Bug #4467: Content selector: cyrillic characters are decoded incorrectly in plugin descriptions
Bug #4469: Abot Silt Striders – Model turn 90 degrees on horizontal
......@@ -130,10 +135,12 @@
Bug #4633: Sneaking stance affects speed even if the actor is not able to crouch
Bug #4641: GetPCJumping is handled incorrectly
Bug #4644: %Name should be available for all actors, not just for NPCs
Bug #4646: Weapon force-equipment messes up ongoing attack animations
Bug #4648: Hud thinks that throwing weapons have condition
Bug #4649: Levelup fully restores health
Bug #4653: Length of non-ASCII strings is handled incorrectly in ESM reader
Bug #4654: Editor: UpdateVisitor does not initialize skeletons for animated objects
Bug #4656: Combat AI: back up behaviour is incorrect
Bug #4668: Editor: Light source color is displayed as an integer
Bug #4669: ToggleCollision should trace the player down after collision being enabled
Bug #4671: knownEffect functions should use modified Alchemy skill
......@@ -141,11 +148,16 @@
Bug #4674: Journal can be opened when settings window is open
Bug #4677: Crash in ESM reader when NPC record has DNAM record without DODT one
Bug #4678: Crash in ESP parser when SCVR has no variable names
Bug #4684: Spell Absorption is additive
Bug #4685: Missing sound causes an exception inside Say command
Bug #4689: Default creature soundgen entries are not used
Bug #4691: Loading bar for cell should be moved up when text is still active at bottom of screen
Feature #912: Editor: Add missing icons to UniversalId tables
Feature #1221: Editor: Creature/NPC rendering
Feature #1617: Editor: Enchantment effect record verifier
Feature #1645: Casting effects from objects
Feature #2606: Editor: Implemented (optional) case sensitive global search
Feature #2787: Use the autogenerated collision box, if the creature mesh has no predefined one
Feature #2847: Content selector: allow to copy the path to a file by using the context menu
Feature #3083: Play animation when NPC is casting spell via script
Feature #3103: Provide option for disposition to get increased by successful trade
......@@ -155,6 +167,7 @@
Feature #4012: Editor: Write a log file if OpenCS crashes
Feature #4222: 360° screenshots
Feature #4256: Implement ToggleBorders (TB) console command
Feature #4285: Support soundgen calls for activators
Feature #4324: Add CFBundleIdentifier in Info.plist to allow for macOS function key shortcuts
Feature #4345: Add equivalents for the command line commands to Launcher
Feature #4404: Editor: All EnumDelegate fields should have their items sorted alphabetically
......@@ -174,6 +187,8 @@
Feature #4632: AI priority: utilize vanilla AI GMSTs for priority rating
Feature #4636: Use sTo GMST in spellmaking menu
Feature #4642: Batching potion creation
Feature #4647: Cull actors outside of AI processing range
Feature #4682: Use the collision box from basic creature mesh if the X one have no collisions
Task #2490: Don't open command prompt window on Release-mode builds automatically
Task #4545: Enable is_pod string test
Task #4605: Optimize skinning
......
......@@ -6,5 +6,5 @@ brew outdated cmake || brew upgrade cmake
brew outdated pkgconfig || brew upgrade pkgconfig
brew install qt
curl -fSL -R -J https://downloads.openmw.org/osx/dependencies/openmw-deps-100d2e0.zip -o ~/openmw-deps.zip
curl -fSL -R -J https://downloads.openmw.org/osx/dependencies/openmw-deps-4eec887.zip -o ~/openmw-deps.zip
unzip -o ~/openmw-deps.zip -d /private/tmp/openmw-deps > /dev/null
......@@ -7,6 +7,7 @@
#include <components/debug/debuglog.hpp>
#include <components/fallback/validate.hpp>
#include <components/misc/rng.hpp>
#include <components/nifosg/nifloader.hpp>
#include "model/doc/document.hpp"
......@@ -355,6 +356,8 @@ int CS::Editor::run()
if (mLocal.empty())
return 1;
Misc::Rng::init();
mStartup.show();
QApplication::setQuitOnLastWindowClosed (true);
......
......@@ -233,6 +233,10 @@ namespace MWBase
virtual void castSpell(const MWWorld::Ptr& ptr, const std::string spellId, bool manualSpell) = 0;
virtual void processChangedSettings (const std::set< std::pair<std::string, std::string> >& settings) = 0;
virtual float getActorsProcessingRange() const = 0;
/// Check if the target actor was detected by an observer
/// If the observer is a non-NPC, check all actors in AI processing distance as observers
virtual bool isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) = 0;
......
......@@ -285,6 +285,8 @@ namespace MWBase
virtual void setEnemy (const MWWorld::Ptr& enemy) = 0;
virtual int getMessagesCount() const = 0;
virtual const Translation::Storage& getTranslationDataStorage() const = 0;
/// Warning: do not use MyGUI::InputManager::setKeyFocusWidget directly. Instead use this.
......
......@@ -268,8 +268,8 @@ namespace MWBase
///< Adjust position after load to be on ground. Must be called after model load.
/// @param force do this even if the ptr is flying
virtual void fixPosition (const MWWorld::Ptr& actor) = 0;
///< Attempt to fix position so that the Ptr is no longer inside collision geometry.
virtual void fixPosition () = 0;
///< Attempt to fix position so that the player is not stuck inside the geometry.
/// @note No-op for items in containers. Use ContainerStore::removeItem instead.
virtual void deleteObject (const MWWorld::Ptr& ptr) = 0;
......@@ -303,9 +303,14 @@ namespace MWBase
///< Queues movement for \a ptr (in local space), to be applied in the next call to
/// doPhysics.
virtual bool castRay (float x1, float y1, float z1, float x2, float y2, float z2, bool ignoreDoors=false) = 0;
virtual bool castRay (float x1, float y1, float z1, float x2, float y2, float z2, int mask) = 0;
///< cast a Ray and return true if there is an object in the ray path.
virtual bool castRay (float x1, float y1, float z1, float x2, float y2, float z2) = 0;
virtual void setActorCollisionMode(const MWWorld::Ptr& ptr, bool enabled) = 0;
virtual bool isActorCollisionEnabled(const MWWorld::Ptr& ptr) = 0;
virtual bool toggleCollisionMode() = 0;
///< Toggle collision mode for player. If disabled player object should ignore
/// collisions and gravity.
......
#include "activator.hpp"
#include <components/esm/loadacti.hpp>
#include <components/misc/rng.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp"
......@@ -134,4 +135,74 @@ namespace MWClass
return MWWorld::Ptr(cell.insert(ref), &cell);
}
std::string Activator::getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const
{
const std::string model = getModel(ptr); // Assume it's not empty, since we wouldn't have gotten the soundgen otherwise
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
std::string creatureId;
for (const ESM::Creature &iter : store.get<ESM::Creature>())
{
if (!iter.mModel.empty() && Misc::StringUtils::ciEqual(model, "meshes\\" + iter.mModel))
{
creatureId = !iter.mOriginal.empty() ? iter.mOriginal : iter.mId;
break;
}
}
int type = getSndGenTypeFromName(name);
std::vector<const ESM::SoundGenerator*> fallbacksounds;
if (!creatureId.empty())
{
std::vector<const ESM::SoundGenerator*> sounds;
for (auto sound = store.get<ESM::SoundGenerator>().begin(); sound != store.get<ESM::SoundGenerator>().end(); ++sound)
{
if (type == sound->mType && !sound->mCreature.empty() && (Misc::StringUtils::ciEqual(creatureId, sound->mCreature)))
sounds.push_back(&*sound);
if (type == sound->mType && sound->mCreature.empty())
fallbacksounds.push_back(&*sound);
}
if (!sounds.empty())
return sounds[Misc::Rng::rollDice(sounds.size())]->mSound;
if (!fallbacksounds.empty())
return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size())]->mSound;
}
else
{
// The activator doesn't have a corresponding creature ID, but we can try to use the defaults
for (auto sound = store.get<ESM::SoundGenerator>().begin(); sound != store.get<ESM::SoundGenerator>().end(); ++sound)
if (type == sound->mType && sound->mCreature.empty())
fallbacksounds.push_back(&*sound);
if (!fallbacksounds.empty())
return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size())]->mSound;
}
return std::string();
}
int Activator::getSndGenTypeFromName(const std::string &name)
{
if (name == "left")
return ESM::SoundGenerator::LeftFoot;
if (name == "right")
return ESM::SoundGenerator::RightFoot;
if (name == "swimleft")
return ESM::SoundGenerator::SwimLeft;
if (name == "swimright")
return ESM::SoundGenerator::SwimRight;
if (name == "moan")
return ESM::SoundGenerator::Moan;
if (name == "roar")
return ESM::SoundGenerator::Roar;
if (name == "scream")
return ESM::SoundGenerator::Scream;
if (name == "land")
return ESM::SoundGenerator::Land;
throw std::runtime_error(std::string("Unexpected soundgen type: ")+name);
}
}
......@@ -10,6 +10,8 @@ namespace MWClass
virtual MWWorld::Ptr copyToCellImpl(const MWWorld::ConstPtr &ptr, MWWorld::CellStore &cell) const;
static int getSndGenTypeFromName(const std::string &name);
public:
virtual void insertObjectRendering (const MWWorld::Ptr& ptr, const std::string& model, MWRender::RenderingInterface& renderingInterface) const;
......@@ -44,6 +46,8 @@ namespace MWClass
///< Whether or not to use animated variant of model (default false)
virtual bool isActivator() const;
virtual std::string getSoundIdFromSndGen(const MWWorld::Ptr &ptr, const std::string &name) const;
};
}
......
......@@ -297,7 +297,7 @@ namespace MWClass
{
const MWWorld::InventoryStore& invStore = npc.getClass().getInventoryStore(npc);
if (ptr.getCellRef().getCharge() == 0)
if (getItemHealth(ptr) == 0)
return std::make_pair(0, "#{sInventoryMessage1}");
// slots that this item can be equipped in
......
......@@ -632,25 +632,27 @@ namespace MWClass
if(type >= 0)
{
std::vector<const ESM::SoundGenerator*> sounds;
std::vector<const ESM::SoundGenerator*> fallbacksounds;
MWWorld::LiveCellRef<ESM::Creature>* ref = ptr.get<ESM::Creature>();
const std::string& ourId = (ref->mBase->mOriginal.empty()) ? ptr.getCellRef().getRefId() : ref->mBase->mOriginal;
MWWorld::Store<ESM::SoundGenerator>::iterator sound = store.begin();
while(sound != store.end())
while (sound != store.end())
{
if (type == sound->mType && !sound->mCreature.empty() && (Misc::StringUtils::ciEqual(ourId, sound->mCreature)))
sounds.push_back(&*sound);
if (type == sound->mType && sound->mCreature.empty())
fallbacksounds.push_back(&*sound);
++sound;
}
if(!sounds.empty())
if (!sounds.empty())
return sounds[Misc::Rng::rollDice(sounds.size())]->mSound;
if (!fallbacksounds.empty())
return fallbacksounds[Misc::Rng::rollDice(fallbacksounds.size())]->mSound;
}
if (type == ESM::SoundGenerator::Land)
return "Body Fall Large";
return "";
}
......@@ -688,9 +690,9 @@ namespace MWClass
MWBase::World *world = MWBase::Environment::get().getWorld();
osg::Vec3f pos(ptr.getRefData().getPosition().asVec3());
if(world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr))
return 2;
return ESM::SoundGenerator::SwimLeft;
if(world->isOnGround(ptr))
return 0;
return ESM::SoundGenerator::LeftFoot;
return -1;
}
if(name == "right")
......@@ -698,23 +700,23 @@ namespace MWClass
MWBase::World *world = MWBase::Environment::get().getWorld();
osg::Vec3f pos(ptr.getRefData().getPosition().asVec3());
if(world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr))
return 3;
return ESM::SoundGenerator::SwimRight;
if(world->isOnGround(ptr))
return 1;
return ESM::SoundGenerator::RightFoot;
return -1;
}
if(name == "swimleft")
return 2;
return ESM::SoundGenerator::SwimLeft;
if(name == "swimright")
return 3;
return ESM::SoundGenerator::SwimRight;
if(name == "moan")
return 4;
return ESM::SoundGenerator::Moan;
if(name == "roar")
return 5;
return ESM::SoundGenerator::Roar;
if(name == "scream")
return 6;
return ESM::SoundGenerator::Scream;
if(name == "land")
return 7;
return ESM::SoundGenerator::Land;
throw std::runtime_error(std::string("Unexpected soundgen type: ")+name);
}
......
......@@ -1148,9 +1148,7 @@ namespace MWClass
const bool hasHealth = it->getClass().hasItemHealth(*it);
if (hasHealth)
{
int armorHealth = it->getClass().getItemHealth(*it);
int armorMaxHealth = it->getClass().getItemMaxHealth(*it);
ratings[i] *= (float(armorHealth) / armorMaxHealth);
ratings[i] *= it->getClass().getItemNormalizedHealth(*it);
}
}
}
......@@ -1242,15 +1240,9 @@ namespace MWClass
return "";
}
// Morrowind ignores land soundgen for NPCs
if(name == "land")
{
MWBase::World *world = MWBase::Environment::get().getWorld();
osg::Vec3f pos(ptr.getRefData().getPosition().asVec3());
if (world->isUnderwater(ptr.getCell(), pos) || world->isWalkingOnWater(ptr))
return "DefaultLandWater";
return "DefaultLand";
}
return "";
if(name == "swimleft")
return "Swim Left";
if(name == "swimright")
......
......@@ -383,7 +383,7 @@ namespace MWClass
std::pair<int, std::string> Weapon::canBeEquipped(const MWWorld::ConstPtr &ptr, const MWWorld::Ptr &npc) const
{
if (hasItemHealth(ptr) && ptr.getCellRef().getCharge() == 0)
if (hasItemHealth(ptr) && getItemHealth(ptr) == 0)
return std::make_pair(0, "#{sInventoryMessage1}");
// Do not allow equip weapons from inventory during attack
......
......@@ -96,7 +96,7 @@ namespace MWGui
Log(Debug::Warning) << "Warning: no splash screens found!";
}
void LoadingScreen::setLabel(const std::string &label, bool important)
void LoadingScreen::setLabel(const std::string &label, bool important, bool center)
{
mImportantLabel = important;
......@@ -105,7 +105,11 @@ namespace MWGui
MyGUI::IntSize size(mLoadingText->getTextSize().width+padding, mLoadingBox->getHeight());
size.width = std::max(300, size.width);
mLoadingBox->setSize(size);
mLoadingBox->setPosition(mMainWidget->getWidth()/2 - mLoadingBox->getWidth()/2, mLoadingBox->getTop());
if (center)
mLoadingBox->setPosition(mMainWidget->getWidth()/2 - mLoadingBox->getWidth()/2, mMainWidget->getHeight()/2 - mLoadingBox->getHeight()/2);
else
mLoadingBox->setPosition(mMainWidget->getWidth()/2 - mLoadingBox->getWidth()/2, mMainWidget->getHeight() - mLoadingBox->getHeight() - 8);
}
void LoadingScreen::setVisible(bool visible)
......
......@@ -34,7 +34,7 @@ namespace MWGui
virtual ~LoadingScreen();
/// Overridden from Loading::Listener, see the Loading::Listener documentation for usage details
virtual void setLabel (const std::string& label, bool important);
virtual void setLabel (const std::string& label, bool important, bool center);
virtual void loadingOn(bool visible=true);
virtual void loadingOff();
virtual void setProgressRange (size_t range);
......
......@@ -51,7 +51,7 @@ void MerchantRepair::setPtr(const MWWorld::Ptr &actor)
{
int maxDurability = iter->getClass().getItemMaxHealth(*iter);
int durability = iter->getClass().getItemHealth(*iter);
if (maxDurability == durability)
if (maxDurability == durability || maxDurability == 0)
continue;
int basePrice = iter->getClass().getValue(*iter);
......
......@@ -35,6 +35,11 @@ namespace MWGui
}
}
int MessageBoxManager::getMessagesCount()
{
return mMessageBoxes.size();
}
void MessageBoxManager::clear()
{
if (mInterMessageBoxe)
......
......@@ -28,6 +28,8 @@ namespace MWGui
bool createInteractiveMessageBox (const std::string& message, const std::vector<std::string>& buttons);
bool isInteractiveMessageBox ();
int getMessagesCount();
const InteractiveMessageBox* getInteractiveMessageBox() const { return mInterMessageBoxe; }
/// Remove all message boxes
......
......@@ -20,6 +20,7 @@
#include "../mwbase/world.hpp"
#include "../mwbase/soundmanager.hpp"
#include "../mwbase/inputmanager.hpp"
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/windowmanager.hpp"
#include "confirmationdialog.hpp"
......@@ -437,6 +438,7 @@ namespace MWGui
MWBase::Environment::get().getSoundManager()->processChangedSettings(changed);
MWBase::Environment::get().getWindowManager()->processChangedSettings(changed);
MWBase::Environment::get().getInputManager()->processChangedSettings(changed);
MWBase::Environment::get().getMechanicsManager()->processChangedSettings(changed);
}
void SettingsWindow::onKeyboardSwitchClicked(MyGUI::Widget* _sender)
......
......@@ -91,8 +91,7 @@ namespace
if (ench->mData.mType == ESM::Enchantment::ConstantEffect)
leftChargePercent = 101;
else
leftChargePercent = (left.mBase.getCellRef().getEnchantmentCharge() == -1) ? 100
: static_cast<int>(left.mBase.getCellRef().getEnchantmentCharge() / static_cast<float>(ench->mData.mCharge) * 100);
leftChargePercent = static_cast<int>(left.mBase.getCellRef().getNormalizedEnchantmentCharge(ench->mData.mCharge) * 100);
}
}
......@@ -104,8 +103,7 @@ namespace
if (ench->mData.mType == ESM::Enchantment::ConstantEffect)
rightChargePercent = 101;
else
rightChargePercent = (right.mBase.getCellRef().getEnchantmentCharge() == -1) ? 100
: static_cast<int>(right.mBase.getCellRef().getEnchantmentCharge() / static_cast<float>(ench->mData.mCharge) * 100);
rightChargePercent = static_cast<int>(right.mBase.getCellRef().getNormalizedEnchantmentCharge(ench->mData.mCharge) * 100);
}
}
......
......@@ -35,8 +35,7 @@ namespace
float price = static_cast<float>(item.getClass().getValue(item));
if (item.getClass().hasItemHealth(item))
{
price *= item.getClass().getItemHealth(item);
price /= item.getClass().getItemMaxHealth(item);
price *= item.getClass().getItemNormalizedHealth(item);
}
return static_cast<int>(price * count);
}
......
......@@ -1369,8 +1369,7 @@ namespace MWGui
const ESM::Enchantment* ench = mStore->get<ESM::Enchantment>()
.find(item.getClass().getEnchantment(item));
int chargePercent = (item.getCellRef().getEnchantmentCharge() == -1) ? 100
: static_cast<int>(item.getCellRef().getEnchantmentCharge() / static_cast<float>(ench->mData.mCharge) * 100);
int chargePercent = static_cast<int>(item.getCellRef().getNormalizedEnchantmentCharge(ench->mData.mCharge) * 100);
mHud->setSelectedEnchantItem(item, chargePercent);
mSpellWindow->setTitle(item.getClass().getName(item));
}
......@@ -1386,7 +1385,7 @@ namespace MWGui
int durabilityPercent = 100;
if (item.getClass().hasItemHealth(item))
{
durabilityPercent = static_cast<int>(item.getClass().getItemHealth(item) / static_cast<float>(item.getClass().getItemMaxHealth(item)) * 100);
durabilityPercent = static_cast<int>(item.getClass().getItemNormalizedHealth(item) * 100);
}
mHud->setSelectedWeapon(item, durabilityPercent);
mInventoryWindow->setTitle(item.getClass().getName(item));
......@@ -1671,6 +1670,15 @@ namespace MWGui
mHud->setEnemy(enemy);
}
int WindowManager::getMessagesCount() const
{
int count = 0;
if (mMessageBoxManager)
count = mMessageBoxManager->getMessagesCount();
return count;
}
Loading::Listener* WindowManager::getLoadingScreen()
{
return mLoadingScreen;
......
......@@ -316,6 +316,8 @@ namespace MWGui
virtual void setEnemy (const MWWorld::Ptr& enemy);
virtual int getMessagesCount() const;
virtual const Translation::Storage& getTranslationDataStorage() const;
void onSoulgemDialogButtonPressed (int button);
......
This diff is collapsed.
......@@ -65,6 +65,9 @@ namespace MWMechanics
/// paused we may want to do it manually (after equipping permanent enchantment)
void updateMagicEffects (const MWWorld::Ptr& ptr);
void updateProcessingRange();
float getProcessingRange() const;
void addActor (const MWWorld::Ptr& ptr, bool updateImmediately=false);
///< Register an actor for stats management
///
......@@ -168,6 +171,7 @@ namespace MWMechanics
private:
PtrActorMap mActors;
float mTimerDisposeSummonsCorpses;
float mActorsProcessingRange;
};
}
......
......@@ -4,6 +4,10 @@
#include <components/esm/aisequence.hpp>
#include <components/sceneutil/positionattitudetransform.hpp>
#include "../mwphysics/collisiontype.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp"
......@@ -456,7 +460,48 @@ namespace MWMechanics
mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability();
mCombatMove = true;
}
else if (isDistantCombat)
{
// Backing up behaviour
// Actor backs up slightly further away than opponent's weapon range
// (in vanilla - only as far as oponent's weapon range),
// or not at all if opponent is using a ranged weapon
if (targetUsesRanged || distToTarget > rangeAttackOfTarget*1.5) // Don't back up if the target is wielding ranged weapon
return;
// actor should not back up into water
if (MWBase::Environment::get().getWorld()->isUnderwater(MWWorld::ConstPtr(actor), 0.5f))
return;
int mask = MWPhysics::CollisionType_World | MWPhysics::CollisionType_HeightMap | MWPhysics::CollisionType_Door;
// Actor can not back up if there is no free space behind
// Currently we take the 35% of actor's height from the ground as vector height.
// This approach allows us to detect small obstacles (e.g. crates) and curved walls.
osg::Vec3f halfExtents = MWBase::Environment::get().getWorld()->getHalfExtents(actor);
osg::Vec3f pos = actor.getRefData().getPosition().asVec3();
osg::Vec3f source = pos + osg::Vec3f(0, 0, 0.75f * halfExtents.z());
osg::Vec3f fallbackDirection = actor.getRefData().getBaseNode()->getAttitude() * osg::Vec3f(0,-1,0);
osg::Vec3f destination = source + fallbackDirection * (halfExtents.y() + 16);
bool isObstacleDetected = MWBase::Environment::get().getWorld()->castRay(source.x(), source.y(), source.z(), destination.x(), destination.y(), destination.z(), mask);
if (isObstacleDetected)
return;
// Check if there is nothing behind - probably actor is near cliff.
// A current approach: cast ray 1.5-yard ray down in 1.5 yard behind actor from 35% of actor's height.
// If we did not hit anything, there is a cliff behind actor.
source = pos + osg::Vec3f(0, 0, 0.75f * halfExtents.z()) + fallbackDirection * (halfExtents.y() + 96);
destination = source - osg::Vec3f(0, 0, 0.75f * halfExtents.z() + 96);
bool isCliffDetected = !MWBase::Environment::get().getWorld()->castRay(source.x(), source.y(), source.z(), destination.x(), destination.y(), destination.z(), mask);
if (isCliffDetected)
return;
mMovement.mPosition[1] = -1;
}
// dodge movements (for NPCs and bipedal creatures)
// Note: do not use for ranged combat yet since in couple with back up behaviour can move actor out of cliff
else if (actor.getClass().isBipedal(actor))
{
// apply sideway movement (kind of dodging) with some probability
......@@ -468,20 +513,6 @@ namespace MWMechanics
mCombatMove = true;
}
}
// Backing up behaviour
// Actor backs up slightly further away than opponent's weapon range
// (in vanilla - only as far as oponent's weapon range),
// or not at all if opponent is using a ranged weapon
if (isDistantCombat)
{
// actor should not back up into water
if (MWBase::Environment::get().getWorld()->isUnderwater(MWWorld::ConstPtr(actor), 0.5f))
return;
if (!targetUsesRanged && distToTarget <= rangeAttackOfTarget*1.5) // Don't back up if the target is wielding ranged weapon
mMovement.mPosition[1] = -1;
}
}
void AiCombatStorage::updateCombatMove(float duration)
......
......@@ -110,7 +110,7 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const osg::Vec3f&
}
/// Stops the actor when it gets too close to a unloaded cell
//... At current time, this test is unnecessary. AI shuts down when actor is more than 7168
//... At current time, this test is unnecessary. AI shuts down when actor is more than "actors processing range" setting value
//... units from player, and exterior cells are 8192 units long and wide.
//... But AI processing distance may increase in the future.
if (isNearInactiveCell(position))
......@@ -356,7 +356,7 @@ bool MWMechanics::AiPackage::isNearInactiveCell(osg::Vec3f position)
// currently assumes 3 x 3 grid for exterior cells, with player at center cell.
// ToDo: (Maybe) use "exterior cell load distance" setting to get count of actual active cells
// While AI Process distance is 7168, AI shuts down actors before they reach edges of 3 x 3 grid.
// AI shuts down actors before they reach edges of 3 x 3 grid.
const float distanceFromEdge = 200.0;
float minThreshold = (-1.0f * ESM::Land::REAL_SIZE) + distanceFromEdge;
float maxThreshold = (2.0f * ESM::Land::REAL_SIZE) - distanceFromEdge;
......
......@@ -3,8 +3,9 @@
#include <components/esm/aisequence.hpp>
#include <components/esm/loadcell.hpp>
#include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/world.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/cellstore.hpp"
......@@ -21,7 +22,8 @@ bool isWithinMaxRange(const osg::Vec3f& pos1, const osg::Vec3f& pos2)
// Maximum travel distance for vanilla compatibility.
// Was likely meant to prevent NPCs walking into non-loaded exterior cells, but for some reason is used in interior cells as well.