Merge pull request #1198 from Allofich/combat

Make combat engagement logic more like vanilla
coverity_scan^2
scrawl 8 years ago
commit 2ea0db1d1a

@ -325,22 +325,38 @@ namespace MWClass
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 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. MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr);
if (!attacker.isEmpty() && !ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat(attacker)) // NOTE: 'object' and/or 'attacker' may be empty.
getCreatureStats(ptr).setAttacked(true); if (!attacker.isEmpty() && !stats.getAiSequence().isInCombat(attacker))
stats.setAttacked(true);
// Self defense // Self defense
bool setOnPcHitMe = true; // Note OnPcHitMe is not set for friendly hits. bool setOnPcHitMe = true; // Note OnPcHitMe is not set for friendly hits.
// No retaliation for totally static creatures (they have no movement or attacks anyway) // No retaliation for totally static creatures (they have no movement or attacks anyway)
if (isMobile(ptr) && !attacker.isEmpty()) if (isMobile(ptr) && !attacker.isEmpty())
{
setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker);
// Attacker and target store each other as hitattemptactor if they have no one stored yet
if (!attacker.isEmpty() && !ptr.isEmpty())
{
MWMechanics::CreatureStats& statsAttacker = attacker.getClass().getCreatureStats(attacker);
// First handle the attacked actor
if ((stats.getHitAttemptActorId() == -1)
&& (statsAttacker.getAiSequence().isInCombat(ptr)
|| attacker == MWMechanics::getPlayer()))
stats.setHitAttemptActorId(statsAttacker.getActorId());
// Next handle the attacking actor
if ((statsAttacker.getHitAttemptActorId() == -1)
&& (statsAttacker.getAiSequence().isInCombat(ptr)
|| attacker == MWMechanics::getPlayer()))
statsAttacker.setHitAttemptActorId(stats.getActorId());
} }
if (!object.isEmpty()) if (!object.isEmpty())
getCreatureStats(ptr).setLastHitAttemptObject(object.getCellRef().getRefId()); stats.setLastHitAttemptObject(object.getCellRef().getRefId());
if (setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer()) if (setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer())
{ {
@ -358,7 +374,7 @@ namespace MWClass
} }
if (!object.isEmpty()) if (!object.isEmpty())
getCreatureStats(ptr).setLastHitObject(object.getCellRef().getRefId()); stats.setLastHitObject(object.getCellRef().getRefId());
if (damage > 0.0f && !object.isEmpty()) if (damage > 0.0f && !object.isEmpty())
MWMechanics::resistNormalWeapon(ptr, attacker, object, damage); MWMechanics::resistNormalWeapon(ptr, attacker, object, damage);
@ -371,16 +387,13 @@ namespace MWClass
if (!attacker.isEmpty()) if (!attacker.isEmpty())
{ {
// Check for knockdown // Check for knockdown
float agilityTerm = getCreatureStats(ptr).getAttribute(ESM::Attribute::Agility).getModified() * getGmst().fKnockDownMult->getFloat(); float agilityTerm = stats.getAttribute(ESM::Attribute::Agility).getModified() * getGmst().fKnockDownMult->getFloat();
float knockdownTerm = getCreatureStats(ptr).getAttribute(ESM::Attribute::Agility).getModified() float knockdownTerm = stats.getAttribute(ESM::Attribute::Agility).getModified()
* getGmst().iKnockDownOddsMult->getInt() * 0.01f + getGmst().iKnockDownOddsBase->getInt(); * getGmst().iKnockDownOddsMult->getInt() * 0.01f + getGmst().iKnockDownOddsBase->getInt();
if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99()) if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99())
{ stats.setKnockedDown(true);
getCreatureStats(ptr).setKnockedDown(true);
}
else else
getCreatureStats(ptr).setHitRecovery(true); // Is this supposed to always occur? stats.setHitRecovery(true); // Is this supposed to always occur?
} }
damage = std::max(1.f, damage); damage = std::max(1.f, damage);
@ -395,15 +408,15 @@ namespace MWClass
MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "Health Damage", 1.0f, 1.0f); MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "Health Damage", 1.0f, 1.0f);
MWMechanics::DynamicStat<float> health(getCreatureStats(ptr).getHealth()); MWMechanics::DynamicStat<float> health(stats.getHealth());
health.setCurrent(health.getCurrent() - damage); health.setCurrent(health.getCurrent() - damage);
getCreatureStats(ptr).setHealth(health); stats.setHealth(health);
} }
else else
{ {
MWMechanics::DynamicStat<float> fatigue(getCreatureStats(ptr).getFatigue()); MWMechanics::DynamicStat<float> fatigue(stats.getFatigue());
fatigue.setCurrent(fatigue.getCurrent() - damage, true); fatigue.setCurrent(fatigue.getCurrent() - damage, true);
getCreatureStats(ptr).setFatigue(fatigue); stats.setFatigue(fatigue);
} }
} }
} }

@ -654,22 +654,38 @@ namespace MWClass
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 void Npc::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const
{ {
MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager();
MWMechanics::CreatureStats& stats = ptr.getClass().getCreatureStats(ptr);
// NOTE: 'object' and/or 'attacker' may be empty. bool wasDead = stats.isDead();
bool wasDead = getCreatureStats(ptr).isDead();
// Note OnPcHitMe is not set for friendly hits. // Note OnPcHitMe is not set for friendly hits.
bool setOnPcHitMe = true; bool setOnPcHitMe = true;
if (!attacker.isEmpty() && !ptr.getClass().getCreatureStats(ptr).getAiSequence().isInCombat(attacker))
{
getCreatureStats(ptr).setAttacked(true);
// NOTE: 'object' and/or 'attacker' may be empty.
if (!attacker.isEmpty() && !stats.getAiSequence().isInCombat(attacker))
{
stats.setAttacked(true);
setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker);
} }
// Attacker and target store each other as hitattemptactor if they have no one stored yet
if (!attacker.isEmpty() && !ptr.isEmpty())
{
MWMechanics::CreatureStats& statsAttacker = attacker.getClass().getCreatureStats(attacker);
// First handle the attacked actor
if ((stats.getHitAttemptActorId() == -1)
&& (statsAttacker.getAiSequence().isInCombat(ptr)
|| attacker == MWMechanics::getPlayer()))
stats.setHitAttemptActorId(statsAttacker.getActorId());
// Next handle the attacking actor
if ((statsAttacker.getHitAttemptActorId() == -1)
&& (statsAttacker.getAiSequence().isInCombat(ptr)
|| attacker == MWMechanics::getPlayer()))
statsAttacker.setHitAttemptActorId(stats.getActorId());
}
if (!object.isEmpty()) if (!object.isEmpty())
getCreatureStats(ptr).setLastHitAttemptObject(object.getCellRef().getRefId()); stats.setLastHitAttemptObject(object.getCellRef().getRefId());
if (setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer()) if (setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer())
{ {
@ -687,7 +703,7 @@ namespace MWClass
} }
if (!object.isEmpty()) if (!object.isEmpty())
getCreatureStats(ptr).setLastHitObject(object.getCellRef().getRefId()); stats.setLastHitObject(object.getCellRef().getRefId());
if (damage > 0.0f && !object.isEmpty()) if (damage > 0.0f && !object.isEmpty())
@ -706,21 +722,16 @@ namespace MWClass
int chance = store.get<ESM::GameSetting>().find("iVoiceHitOdds")->getInt(); int chance = store.get<ESM::GameSetting>().find("iVoiceHitOdds")->getInt();
if (Misc::Rng::roll0to99() < chance) if (Misc::Rng::roll0to99() < chance)
{
MWBase::Environment::get().getDialogueManager()->say(ptr, "hit"); MWBase::Environment::get().getDialogueManager()->say(ptr, "hit");
}
// Check for knockdown // Check for knockdown
float agilityTerm = getCreatureStats(ptr).getAttribute(ESM::Attribute::Agility).getModified() * gmst.fKnockDownMult->getFloat(); float agilityTerm = stats.getAttribute(ESM::Attribute::Agility).getModified() * gmst.fKnockDownMult->getFloat();
float knockdownTerm = getCreatureStats(ptr).getAttribute(ESM::Attribute::Agility).getModified() float knockdownTerm = stats.getAttribute(ESM::Attribute::Agility).getModified()
* gmst.iKnockDownOddsMult->getInt() * 0.01f + gmst.iKnockDownOddsBase->getInt(); * gmst.iKnockDownOddsMult->getInt() * 0.01f + gmst.iKnockDownOddsBase->getInt();
if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99()) if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99())
{ stats.setKnockedDown(true);
getCreatureStats(ptr).setKnockedDown(true);
}
else else
getCreatureStats(ptr).setHitRecovery(true); // Is this supposed to always occur? stats.setHitRecovery(true); // Is this supposed to always occur?
if (damage > 0 && ishealth) if (damage > 0 && ishealth)
{ {
@ -799,13 +810,13 @@ namespace MWClass
} }
MWMechanics::DynamicStat<float> health(getCreatureStats(ptr).getHealth()); MWMechanics::DynamicStat<float> health(getCreatureStats(ptr).getHealth());
health.setCurrent(health.getCurrent() - damage); health.setCurrent(health.getCurrent() - damage);
getCreatureStats(ptr).setHealth(health); stats.setHealth(health);
} }
else else
{ {
MWMechanics::DynamicStat<float> fatigue(getCreatureStats(ptr).getFatigue()); MWMechanics::DynamicStat<float> fatigue(getCreatureStats(ptr).getFatigue());
fatigue.setCurrent(fatigue.getCurrent() - damage, true); fatigue.setCurrent(fatigue.getCurrent() - damage, true);
getCreatureStats(ptr).setFatigue(fatigue); stats.setFatigue(fatigue);
} }
if (!wasDead && getCreatureStats(ptr).isDead()) if (!wasDead && getCreatureStats(ptr).isDead())

@ -9,6 +9,8 @@
#include <components/sceneutil/positionattitudetransform.hpp> #include <components/sceneutil/positionattitudetransform.hpp>
#include <components/settings/settings.hpp>
#include "../mwworld/esmstore.hpp" #include "../mwworld/esmstore.hpp"
#include "../mwworld/class.hpp" #include "../mwworld/class.hpp"
#include "../mwworld/inventorystore.hpp" #include "../mwworld/inventorystore.hpp"
@ -280,7 +282,7 @@ namespace MWMechanics
void Actors::engageCombat (const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, bool againstPlayer) void Actors::engageCombat (const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, bool againstPlayer)
{ {
const CreatureStats& creatureStats1 = actor1.getClass().getCreatureStats(actor1); CreatureStats& creatureStats1 = actor1.getClass().getCreatureStats(actor1);
if (creatureStats1.getAiSequence().isInCombat(actor2)) if (creatureStats1.getAiSequence().isInCombat(actor2))
return; return;
@ -298,97 +300,124 @@ namespace MWMechanics
if (!actor1.getClass().isMobile(actor1)) if (!actor1.getClass().isMobile(actor1))
return; return;
// Start combat if target actor is in combat with one of our followers or escorters // If this is set to true, actor1 will start combat with actor2 if the awareness check at the end of the method returns true
const std::list<MWWorld::Ptr>& followersAndEscorters = getActorsSidingWith(actor1); bool aggressive = false;
for (std::list<MWWorld::Ptr>::const_iterator it = followersAndEscorters.begin(); it != followersAndEscorters.end(); ++it)
// Get actors allied with actor1. Includes those following or escorting actor1, actors following or escorting those actors, (recursive)
// and any actor currently being followed or escorted by actor1
std::set<MWWorld::Ptr> allies1;
getActorsSidingWith(actor1, allies1);
// If an ally of actor1 has been attacked by actor2 or has attacked actor2, start combat between actor1 and actor2
for (std::set<MWWorld::Ptr>::const_iterator it = allies1.begin(); it != allies1.end(); ++it)
{ {
// Need to check both ways since player doesn't use AI packages if (creatureStats1.getAiSequence().isInCombat(*it))
if ((creatureStats2.getAiSequence().isInCombat(*it) continue;
|| it->getClass().getCreatureStats(*it).getAiSequence().isInCombat(actor2))
&& !creatureStats1.getAiSequence().isInCombat(*it)) if (creatureStats2.matchesActorId(it->getClass().getCreatureStats(*it).getHitAttemptActorId()))
{ {
MWBase::Environment::get().getMechanicsManager()->startCombat(actor1, actor2); MWBase::Environment::get().getMechanicsManager()->startCombat(actor1, actor2);
return; // Also set the same hit attempt actor. Otherwise, if fighting the player, they may stop combat
// if the player gets out of reach, while the ally would continue combat with the player
creatureStats1.setHitAttemptActorId(it->getClass().getCreatureStats(*it).getHitAttemptActorId());
return;
} }
}
// Start combat if target actor is in combat with someone we are following through a follow package // If there's been no attack attempt yet but an ally of actor1 is in combat with actor2, become aggressive to actor2
for (std::list<MWMechanics::AiPackage*>::const_iterator it = creatureStats1.getAiSequence().begin(); it != creatureStats1.getAiSequence().end(); ++it) if (it->getClass().getCreatureStats(*it).getAiSequence().isInCombat(actor2))
{ aggressive = true;
if (!(*it)->sideWithTarget()) }
continue;
MWWorld::Ptr followTarget = (*it)->getTarget();
if (followTarget.isEmpty()) std::set<MWWorld::Ptr> playerFollowersAndEscorters;
continue; getActorsSidingWith(MWMechanics::getPlayer(), playerFollowersAndEscorters);
if (creatureStats1.getAiSequence().isInCombat(followTarget)) bool isPlayerFollowerOrEscorter = std::find(playerFollowersAndEscorters.begin(), playerFollowersAndEscorters.end(), actor1) != playerFollowersAndEscorters.end();
continue;
// Need to check both ways since player doesn't use AI packages // If actor2 and at least one actor2 are in combat with actor1, actor1 and its allies start combat with them
if (creatureStats2.getAiSequence().isInCombat(followTarget) // Doesn't apply for player followers/escorters
|| followTarget.getClass().getCreatureStats(followTarget).getAiSequence().isInCombat(actor2)) if (!aggressive && !isPlayerFollowerOrEscorter)
{
// Check that actor2 is in combat with actor1
if (actor2.getClass().getCreatureStats(actor2).getAiSequence().isInCombat(actor1))
{ {
MWBase::Environment::get().getMechanicsManager()->startCombat(actor1, actor2); std::set<MWWorld::Ptr> allies2;
return; getActorsSidingWith(actor2, allies2);
// Check that an ally of actor2 is also in combat with actor1
for (std::set<MWWorld::Ptr>::const_iterator it = allies2.begin(); it != allies2.end(); ++it)
{
if ((it)->getClass().getCreatureStats(*it).getAiSequence().isInCombat(actor1))
{
MWBase::Environment::get().getMechanicsManager()->startCombat(actor1, actor2);
// Also have actor1's allies start combat
for (std::set<MWWorld::Ptr>::const_iterator it2 = allies1.begin(); it2 != allies1.end(); ++it2)
MWBase::Environment::get().getMechanicsManager()->startCombat(*it2, actor2);
return;
}
}
} }
} }
// Start combat with the player if we are already in combat with a player follower or escorter // If set in the settings file, player followers and escorters will become aggressive toward enemies in combat with them or the player
const std::list<MWWorld::Ptr>& playerFollowersAndEscorters = getActorsSidingWith(getPlayer()); if (!aggressive && isPlayerFollowerOrEscorter && Settings::Manager::getBool("followers attack on sight", "Game"))
if (againstPlayer)
{ {
for (std::list<MWWorld::Ptr>::const_iterator it = playerFollowersAndEscorters.begin(); it != playerFollowersAndEscorters.end(); ++it) if (actor2.getClass().getCreatureStats(actor2).getAiSequence().isInCombat(actor1))
aggressive = true;
else
{ {
if (creatureStats1.getAiSequence().isInCombat(*it)) for (std::set<MWWorld::Ptr>::const_iterator it = allies1.begin(); it != allies1.end(); ++it)
{ {
MWBase::Environment::get().getMechanicsManager()->startCombat(actor1, actor2); if (actor2.getClass().getCreatureStats(actor2).getAiSequence().isInCombat(*it))
return; {
aggressive = true;
break;
}
} }
} }
} }
// Otherwise, don't initiate combat with an unreachable target // Stop here if target is unreachable
if (!MWMechanics::canFight(actor1,actor2)) if (!MWMechanics::canFight(actor1, actor2))
return; return;
bool aggressive = false; // Do aggression check if actor2 is the player or a player follower or escorter
if (!aggressive)
if (againstPlayer || std::find(playerFollowersAndEscorters.begin(), playerFollowersAndEscorters.end(), actor2) != playerFollowersAndEscorters.end())
{ {
// Player followers and escorters with high fight should not initiate combat here with the player or with if (againstPlayer || std::find(playerFollowersAndEscorters.begin(), playerFollowersAndEscorters.end(), actor2) != playerFollowersAndEscorters.end())
// other player followers or escorters {
if (std::find(playerFollowersAndEscorters.begin(), playerFollowersAndEscorters.end(), actor1) != playerFollowersAndEscorters.end()) // Player followers and escorters with high fight should not initiate combat with the player or with
return; // other player followers or escorters
if (std::find(playerFollowersAndEscorters.begin(), playerFollowersAndEscorters.end(), actor1) == playerFollowersAndEscorters.end())
aggressive = MWBase::Environment::get().getMechanicsManager()->isAggressive(actor1, actor2); aggressive = MWBase::Environment::get().getMechanicsManager()->isAggressive(actor1, actor2);
}
} }
else
// Make guards go aggressive with creatures that are in combat, unless the creature is a follower or escorter
if (actor1.getClass().isClass(actor1, "Guard") && !actor2.getClass().isNpc())
{ {
// Make guards fight aggressive creatures bool followerOrEscorter = false;
if (!actor1.getClass().isNpc() && actor2.getClass().isClass(actor2, "Guard")) for (std::list<MWMechanics::AiPackage*>::const_iterator it = creatureStats2.getAiSequence().begin(); it != creatureStats2.getAiSequence().end(); ++it)
{ {
if (creatureStats1.getAiSequence().isInCombat() && MWBase::Environment::get().getMechanicsManager()->isAggressive(actor1, actor2)) // The follow package must be first or have nothing but combat before it
aggressive = true; if ((*it)->sideWithTarget())
{
followerOrEscorter = true;
break;
}
else if ((*it)->getTypeId() != MWMechanics::AiPackage::TypeIdCombat)
break;
} }
if (!followerOrEscorter && creatureStats2.getAiSequence().isInCombat())
aggressive = true;
} }
// If any of the above conditions turned actor1 aggressive towards actor2, do an awareness check. If it passes, start combat with actor2.
if (aggressive) if (aggressive)
{ {
bool LOS = MWBase::Environment::get().getWorld()->getLOS(actor1, actor2); bool LOS = MWBase::Environment::get().getWorld()->getLOS(actor1, actor2);
LOS &= MWBase::Environment::get().getMechanicsManager()->awarenessCheck(actor2, actor1);
if (againstPlayer || std::find(playerFollowersAndEscorters.begin(), playerFollowersAndEscorters.end(), actor2) != playerFollowersAndEscorters.end())
LOS &= MWBase::Environment::get().getMechanicsManager()->awarenessCheck(actor2, actor1);
if (LOS) if (LOS)
{
MWBase::Environment::get().getMechanicsManager()->startCombat(actor1, actor2); MWBase::Environment::get().getMechanicsManager()->startCombat(actor1, actor2);
if (!againstPlayer) // start combat between each other
{
MWBase::Environment::get().getMechanicsManager()->startCombat(actor2, actor1);
}
}
} }
} }
@ -931,7 +960,10 @@ namespace MWMechanics
{ {
static const int iCrimeThresholdMultiplier = esmStore.get<ESM::GameSetting>().find("iCrimeThresholdMultiplier")->getInt(); static const int iCrimeThresholdMultiplier = esmStore.get<ESM::GameSetting>().find("iCrimeThresholdMultiplier")->getInt();
if (player.getClass().getNpcStats(player).getBounty() >= cutoff * iCrimeThresholdMultiplier) if (player.getClass().getNpcStats(player).getBounty() >= cutoff * iCrimeThresholdMultiplier)
{
MWBase::Environment::get().getMechanicsManager()->startCombat(ptr, player); MWBase::Environment::get().getMechanicsManager()->startCombat(ptr, player);
creatureStats.setHitAttemptActorId(player.getClass().getCreatureStats(player).getActorId()); // Stops the guard from quitting combat if player is unreachable
}
else else
creatureStats.getAiSequence().stack(AiPursue(player), ptr); creatureStats.getAiSequence().stack(AiPursue(player), ptr);
creatureStats.setAlarmed(true); creatureStats.setAlarmed(true);
@ -1054,10 +1086,25 @@ namespace MWMechanics
if (iter->first == player) if (iter->first == player)
iter->second->getCharacterController()->setAttackingOrSpell(MWBase::Environment::get().getWorld()->getPlayer().getAttackingOrSpell()); iter->second->getCharacterController()->setAttackingOrSpell(MWBase::Environment::get().getWorld()->getPlayer().getAttackingOrSpell());
// If dead or no longer in combat, no longer store any actors who attempted to hit us. Also remove for the player.
if (iter->first != player && (iter->first.getClass().getCreatureStats(iter->first).isDead()
|| !iter->first.getClass().getCreatureStats(iter->first).getAiSequence().isInCombat()
|| !inProcessingRange))
{
iter->first.getClass().getCreatureStats(iter->first).setHitAttemptActorId(-1);
if (player.getClass().getCreatureStats(player).getHitAttemptActorId() == iter->first.getClass().getCreatureStats(iter->first).getActorId())
player.getClass().getCreatureStats(player).setHitAttemptActorId(-1);
}
const MWWorld::Ptr playerHitAttemptActor = MWBase::Environment::get().getWorld()->searchPtrViaActorId(player.getClass().getCreatureStats(player).getHitAttemptActorId());
if (!playerHitAttemptActor.isInCell())
player.getClass().getCreatureStats(player).setHitAttemptActorId(-1);
if (!iter->first.getClass().getCreatureStats(iter->first).isDead()) if (!iter->first.getClass().getCreatureStats(iter->first).isDead())
{ {
MWWorld::Ptr actor = iter->first; // make a copy of the map key to avoid it being invalidated when the player teleports
bool cellChanged = MWBase::Environment::get().getWorld()->hasCellChanged(); bool cellChanged = MWBase::Environment::get().getWorld()->hasCellChanged();
MWWorld::Ptr actor = iter->first; // make a copy of the map key to avoid it being invalidated when the player teleports
updateActor(actor, duration); updateActor(actor, duration);
if (!cellChanged && MWBase::Environment::get().getWorld()->hasCellChanged()) if (!cellChanged && MWBase::Environment::get().getWorld()->hasCellChanged())
{ {
@ -1470,18 +1517,36 @@ namespace MWMechanics
for(PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter) for(PtrActorMap::iterator iter(mActors.begin());iter != mActors.end();++iter)
{ {
const MWWorld::Class &cls = iter->first.getClass(); const MWWorld::Class &cls = iter->first.getClass();
CreatureStats &stats = cls.getCreatureStats(iter->first); const CreatureStats &stats = cls.getCreatureStats(iter->first);
if (stats.isDead()) if (stats.isDead())
continue; continue;
// An actor counts as following if AiFollow or AiEscort is the current AiPackage, or there are only Combat packages before the AiFollow/AiEscort package // An actor counts as siding with this actor if Follow or Escort is the current AI package, or there are only Combat packages before the Follow/Escort package
for (std::list<MWMechanics::AiPackage*>::const_iterator it = stats.getAiSequence().begin(); it != stats.getAiSequence().end(); ++it) for (std::list<MWMechanics::AiPackage*>::const_iterator it = stats.getAiSequence().begin(); it != stats.getAiSequence().end(); ++it)
{ {
if ((*it)->sideWithTarget() && (*it)->getTarget() == actor) if ((*it)->sideWithTarget() && (*it)->getTarget() == actor)
{
list.push_back(iter->first); list.push_back(iter->first);
break;
}
else if ((*it)->getTypeId() != MWMechanics::AiPackage::TypeIdCombat) else if ((*it)->getTypeId() != MWMechanics::AiPackage::TypeIdCombat)
break; break;
} }
// Actors that are targeted by this actor's Follow or Escort packages also side with them
if (actor != getPlayer())
{
const CreatureStats &stats2 = actor.getClass().getCreatureStats(actor);
for (std::list<MWMechanics::AiPackage*>::const_iterator it2 = stats2.getAiSequence().begin(); it2 != stats2.getAiSequence().end(); ++it2)
{
if ((*it2)->sideWithTarget() && !(*it2)->getTarget().isEmpty())
{
list.push_back((*it2)->getTarget());
break;
}
else if ((*it2)->getTypeId() != MWMechanics::AiPackage::TypeIdCombat)
break;
}
}
} }
return list; return list;
} }
@ -1551,8 +1616,6 @@ namespace MWMechanics
return list; return list;
} }
std::list<MWWorld::Ptr> Actors::getActorsFighting(const MWWorld::Ptr& actor) { std::list<MWWorld::Ptr> Actors::getActorsFighting(const MWWorld::Ptr& actor) {
std::list<MWWorld::Ptr> list; std::list<MWWorld::Ptr> list;
std::vector<MWWorld::Ptr> neighbors; std::vector<MWWorld::Ptr> neighbors;

@ -20,6 +20,7 @@
#include "combat.hpp" #include "combat.hpp"
#include "coordinateconverter.hpp" #include "coordinateconverter.hpp"
#include "actorutil.hpp" #include "actorutil.hpp"
#include "mechanicsmanagerimp.hpp"
namespace namespace
{ {
@ -233,9 +234,14 @@ namespace MWMechanics
storage.stopAttack(); storage.stopAttack();
characterController.setAttackingOrSpell(false); characterController.setAttackingOrSpell(false);
storage.mActionCooldown = 0.f; storage.mActionCooldown = 0.f;
if (target == MWMechanics::getPlayer()) // Continue combat if target is player or player follower/escorter and an attack has been attempted
const std::list<MWWorld::Ptr>& playerFollowersAndEscorters = MWBase::Environment::get().getMechanicsManager()->getActorsSidingWith(MWMechanics::getPlayer());
bool targetSidesWithPlayer = (std::find(playerFollowersAndEscorters.begin(), playerFollowersAndEscorters.end(), target) != playerFollowersAndEscorters.end());
if ((target == MWMechanics::getPlayer() || targetSidesWithPlayer)
&& ((actor.getClass().getCreatureStats(actor).getHitAttemptActorId() == target.getClass().getCreatureStats(target).getActorId())
|| (target.getClass().getCreatureStats(target).getHitAttemptActorId() == actor.getClass().getCreatureStats(actor).getActorId())))
forceFlee = true; forceFlee = true;
else else // Otherwise end combat
return true; return true;
} }

@ -21,7 +21,7 @@ namespace MWMechanics
mTalkedTo (false), mAlarmed (false), mAttacked (false), mTalkedTo (false), mAlarmed (false), mAttacked (false),
mKnockdown(false), mKnockdownOneFrame(false), mKnockdownOverOneFrame(false), mKnockdown(false), mKnockdownOneFrame(false), mKnockdownOverOneFrame(false),
mHitRecovery(false), mBlock(false), mMovementFlags(0), mHitRecovery(false), mBlock(false), mMovementFlags(0),
mFallHeight(0), mRecalcMagicka(false), mLastRestock(0,0), mGoldPool(0), mActorId(-1), mFallHeight(0), mRecalcMagicka(false), mLastRestock(0,0), mGoldPool(0), mActorId(-1), mHitAttemptActorId(-1),
mDeathAnimation(-1), mTimeOfDeath(), mLevel (0) mDeathAnimation(-1), mTimeOfDeath(), mLevel (0)
{ {
for (int i=0; i<4; ++i) for (int i=0; i<4; ++i)
@ -371,6 +371,16 @@ namespace MWMechanics
return mLastHitAttemptObject; return mLastHitAttemptObject;
} }
void CreatureStats::setHitAttemptActorId(int actorId)
{
mHitAttemptActorId = actorId;
}
int CreatureStats::getHitAttemptActorId() const
{
return mHitAttemptActorId;
}
void CreatureStats::addToFallHeight(float height) void CreatureStats::addToFallHeight(float height)
{ {
mFallHeight += height; mFallHeight += height;
@ -521,6 +531,7 @@ namespace MWMechanics
state.mActorId = mActorId; state.mActorId = mActorId;
state.mDeathAnimation = mDeathAnimation; state.mDeathAnimation = mDeathAnimation;
state.mTimeOfDeath = mTimeOfDeath.toEsm(); state.mTimeOfDeath = mTimeOfDeath.toEsm();
state.mHitAttemptActorId = mHitAttemptActorId;
mSpells.writeState(state.mSpells); mSpells.writeState(state.mSpells);
mActiveSpells.writeState(state.mActiveSpells); mActiveSpells.writeState(state.mActiveSpells);
@ -569,6 +580,7 @@ namespace MWMechanics
mActorId = state.mActorId; mActorId = state.mActorId;
mDeathAnimation = state.mDeathAnimation; mDeathAnimation = state.mDeathAnimation;
mTimeOfDeath = MWWorld::TimeStamp(state.mTimeOfDeath); mTimeOfDeath = MWWorld::TimeStamp(state.mTimeOfDeath);
mHitAttemptActorId = state.mHitAttemptActorId;
mSpells.readState(state.mSpells); mSpells.readState(state.mSpells);
mActiveSpells.readState(state.mActiveSpells); mActiveSpells.readState(state.mActiveSpells);

@ -62,6 +62,8 @@ namespace MWMechanics
int mGoldPool; int mGoldPool;
int mActorId; int mActorId;
int mHitAttemptActorId; // Stores an actor that attacked this actor. Only one is stored at a time,
// and it is not changed if a different actor attacks. It is cleared when combat ends.
// The index of the death animation that was played, or -1 if none played // The index of the death animation that was played, or -1 if none played
signed char mDeathAnimation; signed char mDeathAnimation;
@ -241,9 +243,11 @@ namespace MWMechanics
bool getStance (Stance flag) const; bool getStance (Stance flag) const;
void setLastHitObject(const std::string &objectid); void setLastHitObject(const std::string &objectid);
void setLastHitAttemptObject(const std::string &objectid);
const std::string &getLastHitObject() const; const std::string &getLastHitObject() const;
void setLastHitAttemptObject(const std::string &objectid);
const std::string &getLastHitAttemptObject() const; const std::string &getLastHitAttemptObject() const;
void setHitAttemptActorId(const int actorId);
int getHitAttemptActorId() const;
// Note, this is just a cache to avoid checking the whole container store every frame. We don't need to store it in saves. // Note, this is just a cache to avoid checking the whole container store every frame. We don't need to store it in saves.
// TODO: Put it somewhere else? // TODO: Put it somewhere else?

@ -1214,48 +1214,51 @@ namespace MWMechanics
} }
} }
bool MechanicsManager::actorAttacked(const MWWorld::Ptr &ptr, const MWWorld::Ptr &attacker) bool MechanicsManager::actorAttacked(const MWWorld::Ptr &target, const MWWorld::Ptr &attacker)
{ {
if (ptr == getPlayer()) std::list<MWWorld::Ptr> followersAttacker = getActorsSidingWith(attacker);
std::list<MWWorld::Ptr> followersTarget = getActorsSidingWith(target);
MWMechanics::CreatureStats& statsTarget = target.getClass().getCreatureStats(target);
if (target == getPlayer())
return false; return false;
std::list<MWWorld::Ptr> followers = getActorsSidingWith(attacker); if (std::find(followersAttacker.begin(), followersAttacker.end(), target) != followersAttacker.end())
MWMechanics::CreatureStats& targetStats = ptr.getClass().getCreatureStats(ptr);
if (std::find(followers.begin(), followers.end(), ptr) != followers.end())
{ {
targetStats.friendlyHit(); statsTarget.friendlyHit();
if (targetStats.getFriendlyHits() < 4) if (statsTarget.getFriendlyHits() < 4)
{ {
MWBase::Environment::get().getDialogueManager()->say(ptr, "hit"); MWBase::Environment::get().getDialogueManager()->say(target, "hit");
return false; return false;
} }
} }
// Attacking an NPC that is already in combat with any other NPC is not a crime // Attacking an NPC that is already in combat with any other NPC is not a crime
AiSequence& seq = targetStats.getAiSequence(); AiSequence& seq = statsTarget.getAiSequence();
bool isFightingNpc = false; bool isFightingNpc = false;
for (std::list<AiPackage*>::const_iterator it = seq.begin(); it != seq.end(); ++it) for (std::list<AiPackage*>::const_iterator it = seq.begin(); it != seq.end(); ++it)
{ {
if ((*it)->getTypeId() == AiPackage::TypeIdCombat) if ((*it)->getTypeId() == AiPackage::TypeIdCombat)
{ {
MWWorld::Ptr target = (*it)->getTarget(); MWWorld::Ptr target2 = (*it)->getTarget();
if (!target.isEmpty() && target.getClass().isNpc()) if (!target2.isEmpty() && target2.getClass().isNpc())
isFightingNpc = true; isFightingNpc = true;
} }
} }
if (ptr.getClass().isNpc() && !attacker.isEmpty() && !seq.isInCombat(attacker) if (target.getClass().isNpc() && !attacker.isEmpty() && !seq.isInCombat(attacker)
&& !isAggressive(ptr, attacker) && !isFightingNpc) && !isAggressive(target, attacker) && !isFightingNpc)
commitCrime(attacker, ptr, MWBase::MechanicsManager::OT_Assault); commitCrime(attacker, target, MWBase::MechanicsManager::OT_Assault);
if (!attacker.isEmpty() && (attacker.getClass().getCreatureStats(attacker).getAiSequence().isInCombat(ptr) if (!attacker.isEmpty() && (attacker.getClass().getCreatureStats(attacker).getAiSequence().isInCombat(target)
|| attacker == getPlayer()) || attacker == getPlayer())
&& !seq.isInCombat(attacker)) && !seq.isInCombat(attacker))
{ {
// Attacker is in combat with us, but we are not in combat with the attacker yet. Time to fight back. // Attacker is in combat with us, but we are not in combat with the attacker yet. Time to fight back.
// Note: accidental or collateral damage attacks are ignored. // Note: accidental or collateral damage attacks are ignored.
startCombat(ptr, attacker); startCombat(target, attacker);
} }
return true; return true;
@ -1365,6 +1368,7 @@ namespace MWMechanics
// if guard starts combat with player, guards pursuing player should do the same // if guard starts combat with player, guards pursuing player should do the same
if (ptr.getClass().isClass(ptr, "Guard")) if (ptr.getClass().isClass(ptr, "Guard"))
{ {
ptr.getClass().getCreatureStats(ptr).setHitAttemptActorId(target.getClass().getCreatureStats(target).getActorId()); // Stops guard from ending combat if player is unreachable
for (Actors::PtrActorMap::const_iterator iter = mActors.begin(); iter != mActors.end(); ++iter) for (Actors::PtrActorMap::const_iterator iter = mActors.begin(); iter != mActors.end(); ++iter)
{ {
if (iter->first.getClass().isClass(iter->first, "Guard")) if (iter->first.getClass().isClass(iter->first, "Guard"))
@ -1374,6 +1378,7 @@ namespace MWMechanics
{ {
aiSeq.stopPursuit(); aiSeq.stopPursuit();
aiSeq.stack(MWMechanics::AiCombat(target), ptr); aiSeq.stack(MWMechanics::AiCombat(target), ptr);
iter->first.getClass().getCreatureStats(iter->first).setHitAttemptActorId(target.getClass().getCreatureStats(target).getActorId()); // Stops guard from ending combat if player is unreachable
} }
} }
} }

@ -90,6 +90,9 @@ void ESM::CreatureStats::load (ESMReader &esm)
mActorId = -1; mActorId = -1;
esm.getHNOT (mActorId, "ACID"); esm.getHNOT (mActorId, "ACID");
mHitAttemptActorId = -1;
esm.getHNOT(mHitAttemptActorId, "HAID");
mDeathAnimation = -1; mDeathAnimation = -1;
esm.getHNOT (mDeathAnimation, "DANM"); esm.getHNOT (mDeathAnimation, "DANM");
@ -203,6 +206,9 @@ void ESM::CreatureStats::save (ESMWriter &esm) const
if (mActorId != -1) if (mActorId != -1)
esm.writeHNT ("ACID", mActorId); esm.writeHNT ("ACID", mActorId);
if (mHitAttemptActorId != -1)
esm.writeHNT("HAID", mHitAttemptActorId);
if (mDeathAnimation != -1) if (mDeathAnimation != -1)
esm.writeHNT ("DANM", mDeathAnimation); esm.writeHNT ("DANM", mDeathAnimation);
@ -240,6 +246,7 @@ void ESM::CreatureStats::blank()
mTradeTime.mDay = 0; mTradeTime.mDay = 0;
mGoldPool = 0; mGoldPool = 0;
mActorId = -1; mActorId = -1;
mHitAttemptActorId = -1;
mHasAiSettings = false; mHasAiSettings = false;
mDead = false; mDead = false;
mDeathAnimationFinished = false; mDeathAnimationFinished = false;

@ -38,6 +38,7 @@ namespace ESM
ESM::TimeStamp mTradeTime; ESM::TimeStamp mTradeTime;
int mGoldPool; int mGoldPool;
int mActorId; int mActorId;
int mHitAttemptActorId;
bool mDead; bool mDead;
bool mDeathAnimationFinished; bool mDeathAnimationFinished;

@ -153,6 +153,10 @@ show effect duration = false
# Prevents merchants from equipping items that are sold to them. # Prevents merchants from equipping items that are sold to them.
prevent merchant equipping = false prevent merchant equipping = false
# Makes player followers and escorters start combat with enemies who have started combat with them
# or the player. Otherwise they wait for the enemies or the player to do an attack first.
followers attack on sight = false
[General] [General]
# Anisotropy reduces distortion in textures at low angles (e.g. 0 to 16). # Anisotropy reduces distortion in textures at low angles (e.g. 0 to 16).

Loading…
Cancel
Save