1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-12-12 22:43:07 +00:00

Merge branch 'sworc' into 'master'

Make StartCombat a no-op for dead targets and don't always play attack lines

Closes #7769 and #5413

See merge request OpenMW/openmw!3803
This commit is contained in:
jvoisin 2024-02-05 09:48:20 +00:00
commit 838785d5a3
14 changed files with 161 additions and 87 deletions

View file

@ -24,6 +24,7 @@
Bug #5129: Stuttering animation on Centurion Archer Bug #5129: Stuttering animation on Centurion Archer
Bug #5280: Unskinned shapes in skinned equipment are rendered in the wrong place Bug #5280: Unskinned shapes in skinned equipment are rendered in the wrong place
Bug #5371: Keyframe animation tracks are used for any file that begins with an X Bug #5371: Keyframe animation tracks are used for any file that begins with an X
Bug #5413: Enemies do a battlecry everytime the player summons a creature
Bug #5714: Touch spells cast using ExplodeSpell don't always explode Bug #5714: Touch spells cast using ExplodeSpell don't always explode
Bug #5849: Paralysis breaks landing Bug #5849: Paralysis breaks landing
Bug #5870: Disposing of actors who were selected in the console doesn't deselect them like vanilla Bug #5870: Disposing of actors who were selected in the console doesn't deselect them like vanilla
@ -132,6 +133,7 @@
Bug #7758: Water walking is not taken into account to compute path cost on the water Bug #7758: Water walking is not taken into account to compute path cost on the water
Bug #7761: Rain and ambient loop sounds are mutually exclusive Bug #7761: Rain and ambient loop sounds are mutually exclusive
Bug #7765: OpenMW-CS: Touch Record option is broken Bug #7765: OpenMW-CS: Touch Record option is broken
Bug #7769: Sword of the Perithia: Broken NPCs
Bug #7770: Sword of the Perithia: Script execution failure Bug #7770: Sword of the Perithia: Script execution failure
Bug #7780: Non-ASCII texture paths in NIF files don't work Bug #7780: Non-ASCII texture paths in NIF files don't work
Bug #7785: OpenMW-CS initialising Skill and Attribute fields to 0 instead of -1 on non-FortifyStat spells Bug #7785: OpenMW-CS initialising Skill and Attribute fields to 0 instead of -1 on non-FortifyStat spells

View file

@ -110,7 +110,9 @@ namespace MWBase
virtual bool awarenessCheck(const MWWorld::Ptr& ptr, const MWWorld::Ptr& observer) = 0; virtual bool awarenessCheck(const MWWorld::Ptr& ptr, const MWWorld::Ptr& observer) = 0;
/// Makes \a ptr fight \a target. Also shouts a combat taunt. /// Makes \a ptr fight \a target. Also shouts a combat taunt.
virtual void startCombat(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) = 0; virtual void startCombat(
const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, const std::set<MWWorld::Ptr>* targetAllies)
= 0;
/// Removes an actor and its allies from combat with the actor's targets. /// Removes an actor and its allies from combat with the actor's targets.
virtual void stopCombat(const MWWorld::Ptr& ptr) = 0; virtual void stopCombat(const MWWorld::Ptr& ptr) = 0;

View file

@ -577,8 +577,8 @@ namespace MWMechanics
} }
} }
void Actors::engageCombat(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, void Actors::engageCombat(
std::map<const MWWorld::Ptr, const std::set<MWWorld::Ptr>>& cachedAllies, bool againstPlayer) const const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, SidingCache& cachedAllies, bool againstPlayer) const
{ {
// No combat for totally static creatures // No combat for totally static creatures
if (!actor1.getClass().isMobile(actor1)) if (!actor1.getClass().isMobile(actor1))
@ -606,9 +606,7 @@ namespace MWMechanics
// Get actors allied with actor1. Includes those following or escorting actor1, actors following or escorting // 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 // those actors, (recursive) and any actor currently being followed or escorted by actor1
std::set<MWWorld::Ptr> allies1; const std::set<MWWorld::Ptr>& allies1 = cachedAllies.getActorsSidingWith(actor1);
getActorsSidingWith(actor1, allies1, cachedAllies);
const auto mechanicsManager = MWBase::Environment::get().getMechanicsManager(); const auto mechanicsManager = MWBase::Environment::get().getMechanicsManager();
// If an ally of actor1 has been attacked by actor2 or has attacked actor2, start combat between actor1 and // If an ally of actor1 has been attacked by actor2 or has attacked actor2, start combat between actor1 and
@ -620,7 +618,7 @@ namespace MWMechanics
if (creatureStats2.matchesActorId(ally.getClass().getCreatureStats(ally).getHitAttemptActorId())) if (creatureStats2.matchesActorId(ally.getClass().getCreatureStats(ally).getHitAttemptActorId()))
{ {
mechanicsManager->startCombat(actor1, actor2); mechanicsManager->startCombat(actor1, actor2, &cachedAllies.getActorsSidingWith(actor2));
// Also set the same hit attempt actor. Otherwise, if fighting the player, they may stop combat // 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 // if the player gets out of reach, while the ally would continue combat with the player
creatureStats1.setHitAttemptActorId(ally.getClass().getCreatureStats(ally).getHitAttemptActorId()); creatureStats1.setHitAttemptActorId(ally.getClass().getCreatureStats(ally).getHitAttemptActorId());
@ -633,9 +631,8 @@ namespace MWMechanics
aggressive = true; aggressive = true;
} }
std::set<MWWorld::Ptr> playerAllies;
MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::Ptr player = MWMechanics::getPlayer();
getActorsSidingWith(player, playerAllies, cachedAllies); const std::set<MWWorld::Ptr>& playerAllies = cachedAllies.getActorsSidingWith(player);
bool isPlayerFollowerOrEscorter = playerAllies.find(actor1) != playerAllies.end(); bool isPlayerFollowerOrEscorter = playerAllies.find(actor1) != playerAllies.end();
@ -646,20 +643,17 @@ namespace MWMechanics
// Check that actor2 is in combat with actor1 // Check that actor2 is in combat with actor1
if (creatureStats2.getAiSequence().isInCombat(actor1)) if (creatureStats2.getAiSequence().isInCombat(actor1))
{ {
std::set<MWWorld::Ptr> allies2; const std::set<MWWorld::Ptr>& allies2 = cachedAllies.getActorsSidingWith(actor2);
getActorsSidingWith(actor2, allies2, cachedAllies);
// Check that an ally of actor2 is also in combat with actor1 // Check that an ally of actor2 is also in combat with actor1
for (const MWWorld::Ptr& ally2 : allies2) for (const MWWorld::Ptr& ally2 : allies2)
{ {
if (ally2 != actor2 && ally2.getClass().getCreatureStats(ally2).getAiSequence().isInCombat(actor1)) if (ally2 != actor2 && ally2.getClass().getCreatureStats(ally2).getAiSequence().isInCombat(actor1))
{ {
mechanicsManager->startCombat(actor1, actor2); mechanicsManager->startCombat(actor1, actor2, &allies2);
// Also have actor1's allies start combat // Also have actor1's allies start combat
for (const MWWorld::Ptr& ally1 : allies1) for (const MWWorld::Ptr& ally1 : allies1)
if (ally1 != player) if (ally1 != player)
mechanicsManager->startCombat(ally1, actor2); mechanicsManager->startCombat(ally1, actor2, &allies2);
return; return;
} }
} }
@ -747,7 +741,7 @@ namespace MWMechanics
bool LOS = world->getLOS(actor1, actor2) && mechanicsManager->awarenessCheck(actor2, actor1); bool LOS = world->getLOS(actor1, actor2) && mechanicsManager->awarenessCheck(actor2, actor1);
if (LOS) if (LOS)
mechanicsManager->startCombat(actor1, actor2); mechanicsManager->startCombat(actor1, actor2, &cachedAllies.getActorsSidingWith(actor2));
} }
} }
@ -1093,7 +1087,7 @@ namespace MWMechanics
} }
} }
void Actors::updateCrimePursuit(const MWWorld::Ptr& ptr, float duration) const void Actors::updateCrimePursuit(const MWWorld::Ptr& ptr, float duration, SidingCache& cachedAllies) const
{ {
const MWWorld::Ptr player = getPlayer(); const MWWorld::Ptr player = getPlayer();
if (ptr == player) if (ptr == player)
@ -1133,7 +1127,7 @@ namespace MWMechanics
= esmStore.get<ESM::GameSetting>().find("iCrimeThresholdMultiplier")->mValue.getInteger(); = esmStore.get<ESM::GameSetting>().find("iCrimeThresholdMultiplier")->mValue.getInteger();
if (playerStats.getBounty() >= cutoff * iCrimeThresholdMultiplier) if (playerStats.getBounty() >= cutoff * iCrimeThresholdMultiplier)
{ {
mechanicsManager->startCombat(ptr, player); mechanicsManager->startCombat(ptr, player, &cachedAllies.getActorsSidingWith(player));
creatureStats.setHitAttemptActorId( creatureStats.setHitAttemptActorId(
playerClass.getCreatureStats(player) playerClass.getCreatureStats(player)
.getActorId()); // Stops the guard from quitting combat if player is unreachable .getActorId()); // Stops the guard from quitting combat if player is unreachable
@ -1508,8 +1502,7 @@ namespace MWMechanics
/// \todo move update logic to Actor class where appropriate /// \todo move update logic to Actor class where appropriate
std::map<const MWWorld::Ptr, const std::set<MWWorld::Ptr>> SidingCache cachedAllies{ *this, true }; // will be filled as engageCombat iterates
cachedAllies; // will be filled as engageCombat iterates
const bool aiActive = MWBase::Environment::get().getMechanicsManager()->isAIActive(); const bool aiActive = MWBase::Environment::get().getMechanicsManager()->isAIActive();
const int attackedByPlayerId = player.getClass().getCreatureStats(player).getHitAttemptActorId(); const int attackedByPlayerId = player.getClass().getCreatureStats(player).getHitAttemptActorId();
@ -1597,7 +1590,7 @@ namespace MWMechanics
updateHeadTracking(actor.getPtr(), mActors, isPlayer, ctrl); updateHeadTracking(actor.getPtr(), mActors, isPlayer, ctrl);
if (actor.getPtr().getClass().isNpc() && !isPlayer) if (actor.getPtr().getClass().isNpc() && !isPlayer)
updateCrimePursuit(actor.getPtr(), duration); updateCrimePursuit(actor.getPtr(), duration, cachedAllies);
if (!isPlayer) if (!isPlayer)
{ {
@ -2115,7 +2108,7 @@ namespace MWMechanics
for (const auto& package : stats.getAiSequence()) for (const auto& package : stats.getAiSequence())
{ {
if (excludeInfighting && !sameActor && package->getTypeId() == AiPackageTypeId::Combat if (excludeInfighting && !sameActor && package->getTypeId() == AiPackageTypeId::Combat
&& package->getTarget() == actorPtr) && package->targetIs(actorPtr))
break; break;
if (package->sideWithTarget() && !package->getTarget().isEmpty()) if (package->sideWithTarget() && !package->getTarget().isEmpty())
{ {
@ -2135,7 +2128,7 @@ namespace MWMechanics
} }
list.push_back(package->getTarget()); list.push_back(package->getTarget());
} }
else if (package->getTarget() == actorPtr) else if (package->targetIs(actorPtr))
{ {
list.push_back(iteratedActor); list.push_back(iteratedActor);
} }
@ -2154,7 +2147,7 @@ namespace MWMechanics
std::vector<MWWorld::Ptr> list; std::vector<MWWorld::Ptr> list;
forEachFollowingPackage( forEachFollowingPackage(
mActors, actorPtr, getPlayer(), [&](const Actor& actor, const std::shared_ptr<AiPackage>& package) { mActors, actorPtr, getPlayer(), [&](const Actor& actor, const std::shared_ptr<AiPackage>& package) {
if (package->followTargetThroughDoors() && package->getTarget() == actorPtr) if (package->followTargetThroughDoors() && package->targetIs(actorPtr))
list.push_back(actor.getPtr()); list.push_back(actor.getPtr());
else if (package->getTypeId() != AiPackageTypeId::Combat else if (package->getTypeId() != AiPackageTypeId::Combat
&& package->getTypeId() != AiPackageTypeId::Wander) && package->getTypeId() != AiPackageTypeId::Wander)
@ -2181,38 +2174,12 @@ namespace MWMechanics
getActorsSidingWith(follower, out, excludeInfighting); getActorsSidingWith(follower, out, excludeInfighting);
} }
void Actors::getActorsSidingWith(const MWWorld::Ptr& actor, std::set<MWWorld::Ptr>& out,
std::map<const MWWorld::Ptr, const std::set<MWWorld::Ptr>>& cachedAllies) const
{
// If we have already found actor's allies, use the cache
std::map<const MWWorld::Ptr, const std::set<MWWorld::Ptr>>::const_iterator search = cachedAllies.find(actor);
if (search != cachedAllies.end())
out.insert(search->second.begin(), search->second.end());
else
{
for (const MWWorld::Ptr& follower : getActorsSidingWith(actor, true))
if (out.insert(follower).second && follower != actor)
getActorsSidingWith(follower, out, cachedAllies);
// Cache ptrs and their sets of allies
cachedAllies.insert(std::make_pair(actor, out));
for (const MWWorld::Ptr& iter : out)
{
if (iter == actor)
continue;
search = cachedAllies.find(iter);
if (search == cachedAllies.end())
cachedAllies.insert(std::make_pair(iter, out));
}
}
}
std::vector<int> Actors::getActorsFollowingIndices(const MWWorld::Ptr& actor) const std::vector<int> Actors::getActorsFollowingIndices(const MWWorld::Ptr& actor) const
{ {
std::vector<int> list; std::vector<int> list;
forEachFollowingPackage( forEachFollowingPackage(
mActors, actor, getPlayer(), [&](const Actor&, const std::shared_ptr<AiPackage>& package) { mActors, actor, getPlayer(), [&](const Actor&, const std::shared_ptr<AiPackage>& package) {
if (package->followTargetThroughDoors() && package->getTarget() == actor) if (package->followTargetThroughDoors() && package->targetIs(actor))
{ {
list.push_back(static_cast<const AiFollow*>(package.get())->getFollowIndex()); list.push_back(static_cast<const AiFollow*>(package.get())->getFollowIndex());
return false; return false;
@ -2230,7 +2197,7 @@ namespace MWMechanics
std::map<int, MWWorld::Ptr> map; std::map<int, MWWorld::Ptr> map;
forEachFollowingPackage( forEachFollowingPackage(
mActors, actor, getPlayer(), [&](const Actor& otherActor, const std::shared_ptr<AiPackage>& package) { mActors, actor, getPlayer(), [&](const Actor& otherActor, const std::shared_ptr<AiPackage>& package) {
if (package->followTargetThroughDoors() && package->getTarget() == actor) if (package->followTargetThroughDoors() && package->targetIs(actor))
{ {
const int index = static_cast<const AiFollow*>(package.get())->getFollowIndex(); const int index = static_cast<const AiFollow*>(package.get())->getFollowIndex();
map[index] = otherActor.getPtr(); map[index] = otherActor.getPtr();
@ -2405,4 +2372,32 @@ namespace MWMechanics
seq.fastForward(ptr); seq.fastForward(ptr);
} }
} }
const std::set<MWWorld::Ptr>& SidingCache::getActorsSidingWith(const MWWorld::Ptr& actor)
{
// If we have already found actor's allies, use the cache
auto search = mCache.find(actor);
if (search != mCache.end())
return search->second;
std::set<MWWorld::Ptr>& out = mCache[actor];
for (const MWWorld::Ptr& follower : mActors.getActorsSidingWith(actor, mExcludeInfighting))
{
if (out.insert(follower).second && follower != actor)
{
const auto& allies = getActorsSidingWith(follower);
out.insert(allies.begin(), allies.end());
}
}
// Cache ptrs and their sets of allies
for (const MWWorld::Ptr& iter : out)
{
if (iter == actor)
continue;
search = mCache.find(iter);
if (search == mCache.end())
mCache.emplace(iter, out);
}
return out;
}
} }

View file

@ -36,6 +36,7 @@ namespace MWMechanics
class Actor; class Actor;
class CharacterController; class CharacterController;
class CreatureStats; class CreatureStats;
class SidingCache;
class Actors class Actors
{ {
@ -186,7 +187,7 @@ namespace MWMechanics
void calculateRestoration(const MWWorld::Ptr& ptr, float duration) const; void calculateRestoration(const MWWorld::Ptr& ptr, float duration) const;
void updateCrimePursuit(const MWWorld::Ptr& ptr, float duration) const; void updateCrimePursuit(const MWWorld::Ptr& ptr, float duration, SidingCache& cachedAllies) const;
void killDeadActors(); void killDeadActors();
@ -198,13 +199,25 @@ namespace MWMechanics
@Notes: If againstPlayer = true then actor2 should be the Player. @Notes: If againstPlayer = true then actor2 should be the Player.
If one of the combatants is creature it should be actor1. If one of the combatants is creature it should be actor1.
*/ */
void engageCombat(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, void engageCombat(const MWWorld::Ptr& actor1, const MWWorld::Ptr& actor2, SidingCache& cachedAllies,
std::map<const MWWorld::Ptr, const std::set<MWWorld::Ptr>>& cachedAllies, bool againstPlayer) const; bool againstPlayer) const;
};
/// Recursive version of getActorsSidingWith that takes, adds to and returns a cache of class SidingCache
/// actors mapped to their allies. Excludes infighting {
void getActorsSidingWith(const MWWorld::Ptr& actor, std::set<MWWorld::Ptr>& out, const Actors& mActors;
std::map<const MWWorld::Ptr, const std::set<MWWorld::Ptr>>& cachedAllies) const; const bool mExcludeInfighting;
std::map<MWWorld::Ptr, std::set<MWWorld::Ptr>> mCache;
public:
SidingCache(const Actors& actors, bool excludeInfighting)
: mActors(actors)
, mExcludeInfighting(excludeInfighting)
{
}
/// Recursive version of getActorsSidingWith that takes, returns a cached set of allies
const std::set<MWWorld::Ptr>& getActorsSidingWith(const MWWorld::Ptr& actor);
}; };
} }

View file

@ -270,7 +270,15 @@ namespace MWMechanics
{ {
storage.startCombatMove(isRangedCombat, distToTarget, rangeAttack, actor, target); storage.startCombatMove(isRangedCombat, distToTarget, rangeAttack, actor, target);
// start new attack // start new attack
storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat); bool canShout = true;
ESM::RefId spellId = storage.mCurrentAction->getSpell();
if (!spellId.empty())
{
const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get<ESM::Spell>().find(spellId);
if (spell->mEffects.mList.empty() || spell->mEffects.mList[0].mRange != ESM::RT_Target)
canShout = false;
}
storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat, canShout);
} }
// If actor uses custom destination it has to try to rebuild path because environment can change // If actor uses custom destination it has to try to rebuild path because environment can change
@ -635,7 +643,7 @@ namespace MWMechanics
} }
void AiCombatStorage::startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController, void AiCombatStorage::startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController,
const ESM::Weapon* weapon, bool distantCombat) const ESM::Weapon* weapon, bool distantCombat, bool canShout)
{ {
if (mReadyToAttack && characterController.readyToStartAttack()) if (mReadyToAttack && characterController.readyToStartAttack())
{ {
@ -658,12 +666,15 @@ namespace MWMechanics
baseDelay = store.get<ESM::GameSetting>().find("fCombatDelayNPC")->mValue.getFloat(); baseDelay = store.get<ESM::GameSetting>().find("fCombatDelayNPC")->mValue.getFloat();
} }
// Say a provoking combat phrase if (canShout)
const int iVoiceAttackOdds
= store.get<ESM::GameSetting>().find("iVoiceAttackOdds")->mValue.getInteger();
if (Misc::Rng::roll0to99(prng) < iVoiceAttackOdds)
{ {
MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("attack")); // Say a provoking combat phrase
const int iVoiceAttackOdds
= store.get<ESM::GameSetting>().find("iVoiceAttackOdds")->mValue.getInteger();
if (Misc::Rng::roll0to99(prng) < iVoiceAttackOdds)
{
MWBase::Environment::get().getDialogueManager()->say(actor, ESM::RefId::stringRefId("attack"));
}
} }
mAttackCooldown = std::min(baseDelay + 0.01 * Misc::Rng::roll0to99(prng), baseDelay + 0.9); mAttackCooldown = std::min(baseDelay + 0.01 * Misc::Rng::roll0to99(prng), baseDelay + 0.9);
} }

View file

@ -64,7 +64,7 @@ namespace MWMechanics
void updateCombatMove(float duration); void updateCombatMove(float duration);
void stopCombatMove(); void stopCombatMove();
void startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController, void startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController,
const ESM::Weapon* weapon, bool distantCombat); const ESM::Weapon* weapon, bool distantCombat, bool canShout);
void updateAttack(const MWWorld::Ptr& actor, CharacterController& characterController); void updateAttack(const MWWorld::Ptr& actor, CharacterController& characterController);
void stopAttack(); void stopAttack();

View file

@ -16,6 +16,7 @@ namespace MWMechanics
virtual float getCombatRange(bool& isRanged) const = 0; virtual float getCombatRange(bool& isRanged) const = 0;
virtual float getActionCooldown() const { return 0.f; } virtual float getActionCooldown() const { return 0.f; }
virtual const ESM::Weapon* getWeapon() const { return nullptr; } virtual const ESM::Weapon* getWeapon() const { return nullptr; }
virtual ESM::RefId getSpell() const { return {}; }
virtual bool isAttackingOrSpell() const { return true; } virtual bool isAttackingOrSpell() const { return true; }
virtual bool isFleeing() const { return false; } virtual bool isFleeing() const { return false; }
}; };
@ -43,6 +44,7 @@ namespace MWMechanics
void prepare(const MWWorld::Ptr& actor) override; void prepare(const MWWorld::Ptr& actor) override;
float getCombatRange(bool& isRanged) const override; float getCombatRange(bool& isRanged) const override;
ESM::RefId getSpell() const override { return mSpellId; }
}; };
class ActionEnchantedItem : public Action class ActionEnchantedItem : public Action

View file

@ -98,6 +98,26 @@ MWWorld::Ptr MWMechanics::AiPackage::getTarget() const
return mCachedTarget; return mCachedTarget;
} }
bool MWMechanics::AiPackage::targetIs(const MWWorld::Ptr& ptr) const
{
if (mTargetActorId == -2)
return ptr.isEmpty();
else if (mTargetActorId == -1)
{
if (mTargetActorRefId.empty())
{
mTargetActorId = -2;
return ptr.isEmpty();
}
if (!ptr.isEmpty() && ptr.getCellRef().getRefId() == mTargetActorRefId)
return getTarget() == ptr;
return false;
}
if (ptr.isEmpty() || !ptr.getClass().isActor())
return false;
return ptr.getClass().getCreatureStats(ptr).getActorId() == mTargetActorId;
}
void MWMechanics::AiPackage::reset() void MWMechanics::AiPackage::reset()
{ {
// reset all members // reset all members

View file

@ -87,6 +87,8 @@ namespace MWMechanics
/// Get the target actor the AI is targeted at (not applicable to all AI packages, default return empty Ptr) /// Get the target actor the AI is targeted at (not applicable to all AI packages, default return empty Ptr)
virtual MWWorld::Ptr getTarget() const; virtual MWWorld::Ptr getTarget() const;
/// Optimized version of getTarget() == ptr
virtual bool targetIs(const MWWorld::Ptr& ptr) const;
/// Get the destination point of the AI package (not applicable to all AI packages, default return (0, 0, 0)) /// Get the destination point of the AI package (not applicable to all AI packages, default return (0, 0, 0))
virtual osg::Vec3f getDestination(const MWWorld::Ptr& actor) const { return osg::Vec3f(0, 0, 0); } virtual osg::Vec3f getDestination(const MWWorld::Ptr& actor) const { return osg::Vec3f(0, 0, 0); }

View file

@ -176,11 +176,11 @@ namespace MWMechanics
if (!isInCombat()) if (!isInCombat())
return false; return false;
for (auto it = mPackages.begin(); it != mPackages.end(); ++it) for (const auto& package : mPackages)
{ {
if ((*it)->getTypeId() == AiPackageTypeId::Combat) if (package->getTypeId() == AiPackageTypeId::Combat)
{ {
if ((*it)->getTarget() == actor) if (package->targetIs(actor))
return true; return true;
} }
} }

View file

@ -132,9 +132,6 @@ namespace MWMechanics
/// Are we in combat with this particular actor? /// Are we in combat with this particular actor?
bool isInCombat(const MWWorld::Ptr& actor) const; bool isInCombat(const MWWorld::Ptr& actor) const;
bool canAddTarget(const ESM::Position& actorPos, float distToTarget) const;
///< Function assumes that actor can have only 1 target apart player
/// Removes all combat packages until first non-combat or stack empty. /// Removes all combat packages until first non-combat or stack empty.
void stopCombat(); void stopCombat();

View file

@ -30,6 +30,7 @@
#include "../mwbase/world.hpp" #include "../mwbase/world.hpp"
#include "actor.hpp" #include "actor.hpp"
#include "actors.hpp"
#include "actorutil.hpp" #include "actorutil.hpp"
#include "aicombat.hpp" #include "aicombat.hpp"
#include "aipursue.hpp" #include "aipursue.hpp"
@ -1192,9 +1193,9 @@ namespace MWMechanics
&& !victim.getClass().getCreatureStats(victim).getAiSequence().hasPackage(AiPackageTypeId::Pursue)) && !victim.getClass().getCreatureStats(victim).getAiSequence().hasPackage(AiPackageTypeId::Pursue))
reported = reportCrime(player, victim, type, ESM::RefId(), arg); reported = reportCrime(player, victim, type, ESM::RefId(), arg);
// TODO: combat should be started with an "unaware" flag, which makes the victim flee?
if (!reported) if (!reported)
startCombat(victim, startCombat(victim, player, &playerFollowers);
player); // TODO: combat should be started with an "unaware" flag, which makes the victim flee?
} }
return crimeSeen; return crimeSeen;
} }
@ -1435,7 +1436,7 @@ namespace MWMechanics
observerStats.modCrimeDispositionModifier(dispositionModifier); observerStats.modCrimeDispositionModifier(dispositionModifier);
} }
startCombat(actor, player); startCombat(actor, player, &playerFollowers);
// Apply aggression value to the base Fight rating, so that the actor can continue fighting // Apply aggression value to the base Fight rating, so that the actor can continue fighting
// after a Calm spell wears off // after a Calm spell wears off
@ -1486,7 +1487,7 @@ namespace MWMechanics
// 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.
if (!victim.getClass().getCreatureStats(victim).getAiSequence().isInPursuit()) if (!victim.getClass().getCreatureStats(victim).getAiSequence().isInPursuit())
startCombat(victim, player); startCombat(victim, player, &playerFollowers);
// Set the crime ID, which we will use to calm down participants // Set the crime ID, which we will use to calm down participants
// once the bounty has been paid. // once the bounty has been paid.
@ -1531,14 +1532,14 @@ namespace MWMechanics
if (!peaceful) if (!peaceful)
{ {
startCombat(target, attacker); SidingCache cachedAllies{ mActors, false };
const std::set<MWWorld::Ptr>& attackerAllies = cachedAllies.getActorsSidingWith(attacker);
startCombat(target, attacker, &attackerAllies);
// Force friendly actors into combat to prevent infighting between followers // Force friendly actors into combat to prevent infighting between followers
std::set<MWWorld::Ptr> followersTarget; for (const auto& follower : cachedAllies.getActorsSidingWith(target))
getActorsSidingWith(target, followersTarget);
for (const auto& follower : followersTarget)
{ {
if (follower != attacker && follower != player) if (follower != attacker && follower != player)
startCombat(follower, attacker); startCombat(follower, attacker, &attackerAllies);
} }
} }
} }
@ -1687,7 +1688,8 @@ namespace MWMechanics
} }
} }
void MechanicsManager::startCombat(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) void MechanicsManager::startCombat(
const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, const std::set<MWWorld::Ptr>* targetAllies)
{ {
CreatureStats& stats = ptr.getClass().getCreatureStats(ptr); CreatureStats& stats = ptr.getClass().getCreatureStats(ptr);
@ -1704,6 +1706,31 @@ namespace MWMechanics
return; return;
} }
const bool inCombat = stats.getAiSequence().isInCombat();
bool shout = !inCombat;
if (inCombat)
{
const auto isInCombatWithOneOf = [&](const auto& allies) {
for (const MWWorld::Ptr& ally : allies)
{
if (stats.getAiSequence().isInCombat(ally))
return true;
}
return false;
};
if (targetAllies)
shout = !isInCombatWithOneOf(*targetAllies);
else
{
shout = stats.getAiSequence().isInCombat(target);
if (!shout)
{
std::set<MWWorld::Ptr> sidingActors;
getActorsSidingWith(target, sidingActors);
shout = !isInCombatWithOneOf(sidingActors);
}
}
}
stats.getAiSequence().stack(MWMechanics::AiCombat(target), ptr); stats.getAiSequence().stack(MWMechanics::AiCombat(target), ptr);
if (target == getPlayer()) if (target == getPlayer())
{ {
@ -1738,7 +1765,8 @@ namespace MWMechanics
} }
// Must be done after the target is set up, so that CreatureTargetted dialogue filter works properly // Must be done after the target is set up, so that CreatureTargetted dialogue filter works properly
MWBase::Environment::get().getDialogueManager()->say(ptr, ESM::RefId::stringRefId("attack")); if (shout)
MWBase::Environment::get().getDialogueManager()->say(ptr, ESM::RefId::stringRefId("attack"));
} }
void MechanicsManager::stopCombat(const MWWorld::Ptr& actor) void MechanicsManager::stopCombat(const MWWorld::Ptr& actor)

View file

@ -107,7 +107,8 @@ namespace MWMechanics
bool awarenessCheck(const MWWorld::Ptr& ptr, const MWWorld::Ptr& observer) override; bool awarenessCheck(const MWWorld::Ptr& ptr, const MWWorld::Ptr& observer) override;
/// Makes \a ptr fight \a target. Also shouts a combat taunt. /// Makes \a ptr fight \a target. Also shouts a combat taunt.
void startCombat(const MWWorld::Ptr& ptr, const MWWorld::Ptr& target) override; void startCombat(
const MWWorld::Ptr& ptr, const MWWorld::Ptr& target, const std::set<MWWorld::Ptr>* targetAllies) override;
void stopCombat(const MWWorld::Ptr& ptr) override; void stopCombat(const MWWorld::Ptr& ptr) override;

View file

@ -507,8 +507,9 @@ namespace MWScript
runtime.pop(); runtime.pop();
MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(targetID, true, false); MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(targetID, true, false);
if (!target.isEmpty()) if (!target.isEmpty() && !target.getBase()->isDeleted()
MWBase::Environment::get().getMechanicsManager()->startCombat(actor, target); && !target.getClass().getCreatureStats(target).isDead())
MWBase::Environment::get().getMechanicsManager()->startCombat(actor, target, nullptr);
} }
}; };