mirror of
https://github.com/TES3MP/openmw-tes3mp.git
synced 2025-03-04 11:19:40 +00:00
Merge remote-tracking branch 'dteviot/AiCombatRefactor'
This commit is contained in:
commit
86dac4a698
5 changed files with 141 additions and 104 deletions
|
@ -110,6 +110,14 @@ namespace MWMechanics
|
||||||
mForceNoShortcut(false),
|
mForceNoShortcut(false),
|
||||||
mLastActorPos(0,0,0),
|
mLastActorPos(0,0,0),
|
||||||
mMovement(){}
|
mMovement(){}
|
||||||
|
|
||||||
|
void startCombatMove(bool isNpc, bool isDistantCombat, float distToTarget, float rangeAttack);
|
||||||
|
void updateCombatMove(float duration);
|
||||||
|
void stopCombatMove();
|
||||||
|
void startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController,
|
||||||
|
const ESM::Weapon* weapon, bool distantCombat);
|
||||||
|
void updateAttack(CharacterController& characterController);
|
||||||
|
void stopAttack();
|
||||||
};
|
};
|
||||||
|
|
||||||
AiCombat::AiCombat(const MWWorld::Ptr& actor) :
|
AiCombat::AiCombat(const MWWorld::Ptr& actor) :
|
||||||
|
@ -190,32 +198,11 @@ namespace MWMechanics
|
||||||
|| target.getClass().getCreatureStats(target).isDead())
|
|| target.getClass().getCreatureStats(target).isDead())
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
|
||||||
//Update every frame
|
//Update every frame
|
||||||
bool& combatMove = storage.mCombatMove;
|
storage.updateCombatMove(duration);
|
||||||
float& timerCombatMove = storage.mTimerCombatMove;
|
updateActorsMovement(actor, storage.mMovement);
|
||||||
MWMechanics::Movement& movement = storage.mMovement;
|
storage.updateAttack(characterController);
|
||||||
if(combatMove)
|
storage.mActionCooldown -= duration;
|
||||||
{
|
|
||||||
timerCombatMove -= duration;
|
|
||||||
if( timerCombatMove <= 0)
|
|
||||||
{
|
|
||||||
timerCombatMove = 0;
|
|
||||||
movement.mPosition[1] = movement.mPosition[0] = 0;
|
|
||||||
combatMove = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateActorsMovement(actor, movement);
|
|
||||||
|
|
||||||
bool& attack = storage.mAttack;
|
|
||||||
if (attack && (characterController.getAttackStrength() >= storage.mStrength || characterController.readyToPrepareAttack()))
|
|
||||||
attack = false;
|
|
||||||
|
|
||||||
characterController.setAttackingOrSpell(attack);
|
|
||||||
|
|
||||||
float& actionCooldown = storage.mActionCooldown;
|
|
||||||
actionCooldown -= duration;
|
|
||||||
|
|
||||||
float& timerReact = storage.mTimerReact;
|
float& timerReact = storage.mTimerReact;
|
||||||
if(timerReact < REACTION_INTERVAL)
|
if(timerReact < REACTION_INTERVAL)
|
||||||
|
@ -235,11 +222,9 @@ namespace MWMechanics
|
||||||
{
|
{
|
||||||
MWMechanics::Movement& movement = storage.mMovement;
|
MWMechanics::Movement& movement = storage.mMovement;
|
||||||
|
|
||||||
// Stop attacking if target is not seen
|
if (isTargetMagicallyHidden(target))
|
||||||
if (target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Invisibility).getMagnitude() > 0
|
|
||||||
|| target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude() > 75)
|
|
||||||
{
|
{
|
||||||
movement.mPosition[1] = movement.mPosition[0] = 0;
|
storage.stopAttack();
|
||||||
return false; // TODO: run away instead of doing nothing
|
return false; // TODO: run away instead of doing nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -325,41 +310,9 @@ namespace MWMechanics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
float& strength = storage.mStrength;
|
|
||||||
bool& readyToAttack = storage.mReadyToAttack;
|
bool& readyToAttack = storage.mReadyToAttack;
|
||||||
// start new attack
|
// start new attack
|
||||||
if(readyToAttack && characterController.readyToStartAttack())
|
storage.startAttackIfReady(actor, characterController, weapon, distantCombat);
|
||||||
{
|
|
||||||
if (storage.mAttackCooldown <= 0)
|
|
||||||
{
|
|
||||||
storage.mAttack = true; // attack starts just now
|
|
||||||
characterController.setAttackingOrSpell(true);
|
|
||||||
|
|
||||||
if (!distantCombat)
|
|
||||||
chooseBestAttack(weapon, movement);
|
|
||||||
|
|
||||||
strength = Misc::Rng::rollClosedProbability();
|
|
||||||
|
|
||||||
const MWWorld::ESMStore &store = world->getStore();
|
|
||||||
|
|
||||||
//say a provoking combat phrase
|
|
||||||
if (actor.getClass().isNpc())
|
|
||||||
{
|
|
||||||
int chance = store.get<ESM::GameSetting>().find("iVoiceAttackOdds")->getInt();
|
|
||||||
if (Misc::Rng::roll0to99() < chance)
|
|
||||||
{
|
|
||||||
MWBase::Environment::get().getDialogueManager()->say(actor, "attack");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
float baseDelay = store.get<ESM::GameSetting>().find("fCombatDelayCreature")->getFloat();
|
|
||||||
if (actor.getClass().isNpc())
|
|
||||||
baseDelay = store.get<ESM::GameSetting>().find("fCombatDelayNPC")->getFloat();
|
|
||||||
storage.mAttackCooldown = std::min(baseDelay + 0.01 * Misc::Rng::roll0to99(), baseDelay + 0.9);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
storage.mAttackCooldown -= REACTION_INTERVAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Some notes on meanings of variables:
|
* Some notes on meanings of variables:
|
||||||
|
@ -410,22 +363,18 @@ namespace MWMechanics
|
||||||
bool canMoveByZ = (actorClass.canSwim(actor) && world->isSwimming(actor))
|
bool canMoveByZ = (actorClass.canSwim(actor) && world->isSwimming(actor))
|
||||||
|| world->isFlying(actor);
|
|| world->isFlying(actor);
|
||||||
|
|
||||||
// for distant combat we should know if target is in LOS even if distToTarget < rangeAttack
|
|
||||||
bool inLOS = distantCombat ? world->getLOS(actor, target) : true;
|
|
||||||
|
|
||||||
// can't fight if attacker can't go where target is. E.g. A fish can't attack person on land.
|
// can't fight if attacker can't go where target is. E.g. A fish can't attack person on land.
|
||||||
if (distToTarget >= rangeAttack
|
if (distToTarget >= rangeAttack
|
||||||
&& !actorClass.isNpc() && !MWMechanics::isEnvironmentCompatible(actor, target))
|
&& !actorClass.isNpc() && !MWMechanics::isEnvironmentCompatible(actor, target))
|
||||||
{
|
{
|
||||||
// TODO: start fleeing?
|
// TODO: start fleeing?
|
||||||
movement.mPosition[0] = 0;
|
storage.stopAttack();
|
||||||
movement.mPosition[1] = 0;
|
|
||||||
movement.mPosition[2] = 0;
|
|
||||||
readyToAttack = false;
|
|
||||||
characterController.setAttackingOrSpell(false);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for distant combat we should know if target is in LOS even if distToTarget < rangeAttack
|
||||||
|
bool inLOS = distantCombat ? world->getLOS(actor, target) : true;
|
||||||
|
|
||||||
// (within attack dist) || (not quite attack dist while following)
|
// (within attack dist) || (not quite attack dist while following)
|
||||||
if(inLOS && (distToTarget < rangeAttack || (distToTarget <= rangeFollow && followTarget && !isStuck)))
|
if(inLOS && (distToTarget < rangeAttack || (distToTarget <= rangeFollow && followTarget && !isStuck)))
|
||||||
{
|
{
|
||||||
|
@ -439,7 +388,8 @@ namespace MWMechanics
|
||||||
if (distantCombat)
|
if (distantCombat)
|
||||||
{
|
{
|
||||||
osg::Vec3f& lastTargetPos = storage.mLastTargetPos;
|
osg::Vec3f& lastTargetPos = storage.mLastTargetPos;
|
||||||
osg::Vec3f vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, REACTION_INTERVAL, weaptype, strength);
|
osg::Vec3f vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, REACTION_INTERVAL, weaptype,
|
||||||
|
storage.mStrength);
|
||||||
lastTargetPos = vTargetPos;
|
lastTargetPos = vTargetPos;
|
||||||
movement.mRotation[0] = getXAngleToDir(vAimDir);
|
movement.mRotation[0] = getXAngleToDir(vAimDir);
|
||||||
movement.mRotation[2] = getZAngleToDir(vAimDir);
|
movement.mRotation[2] = getZAngleToDir(vAimDir);
|
||||||
|
@ -454,31 +404,12 @@ namespace MWMechanics
|
||||||
if (followTarget && distToTarget > rangeAttack)
|
if (followTarget && distToTarget > rangeAttack)
|
||||||
{
|
{
|
||||||
//Close-up combat: just run up on target
|
//Close-up combat: just run up on target
|
||||||
|
storage.stopCombatMove();
|
||||||
movement.mPosition[1] = 1;
|
movement.mPosition[1] = 1;
|
||||||
}
|
}
|
||||||
else // (within attack dist)
|
else // (within attack dist)
|
||||||
{
|
{
|
||||||
if(movement.mPosition[0] || movement.mPosition[1])
|
storage.startCombatMove(actorClass.isNpc(), distantCombat, distToTarget, rangeAttack);
|
||||||
{
|
|
||||||
storage.mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability();
|
|
||||||
storage.mCombatMove = true;
|
|
||||||
}
|
|
||||||
// only NPCs are smart enough to use dodge movements
|
|
||||||
else if(actorClass.isNpc() && (!distantCombat || (distantCombat && distToTarget < rangeAttack/2)))
|
|
||||||
{
|
|
||||||
//apply sideway movement (kind of dodging) with some probability
|
|
||||||
if (Misc::Rng::rollClosedProbability() < 0.25)
|
|
||||||
{
|
|
||||||
movement.mPosition[0] = Misc::Rng::rollProbability() < 0.5 ? 1.0f : -1.0f;
|
|
||||||
storage.mTimerCombatMove = 0.05f + 0.15f * Misc::Rng::rollClosedProbability();
|
|
||||||
storage.mCombatMove = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(distantCombat && distToTarget < rangeAttack/4)
|
|
||||||
{
|
|
||||||
movement.mPosition[1] = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
readyToAttack = true;
|
readyToAttack = true;
|
||||||
//only once got in melee combat, actor is allowed to use close-up shortcutting
|
//only once got in melee combat, actor is allowed to use close-up shortcutting
|
||||||
|
@ -551,20 +482,19 @@ namespace MWMechanics
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
movement.mPosition[1] = 1;
|
|
||||||
if (readyToAttack)
|
if (readyToAttack)
|
||||||
{
|
{
|
||||||
// to stop possible sideway moving after target moved out of attack range
|
// to stop possible sideway moving after target moved out of attack range
|
||||||
storage.mCombatMove = true;
|
storage.stopCombatMove();
|
||||||
storage.mTimerCombatMove = 0;
|
|
||||||
}
|
|
||||||
readyToAttack = false;
|
readyToAttack = false;
|
||||||
}
|
}
|
||||||
|
movement.mPosition[1] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AiCombat::UpdateActorsMovement(const MWWorld::Ptr& actor, MWMechanics::Movement& desiredMovement)
|
void AiCombat::updateActorsMovement(const MWWorld::Ptr& actor, MWMechanics::Movement& desiredMovement)
|
||||||
{
|
{
|
||||||
MWMechanics::Movement& actorMovementSettings = actor.getClass().getMovementSettings(actor);
|
MWMechanics::Movement& actorMovementSettings = actor.getClass().getMovementSettings(actor);
|
||||||
if (mPathFinder.isPathConstructed())
|
if (mPathFinder.isPathConstructed())
|
||||||
|
@ -583,12 +513,12 @@ namespace MWMechanics
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
actorMovementSettings = desiredMovement;
|
actorMovementSettings = desiredMovement;
|
||||||
RotateActorOnAxis(actor, 2, actorMovementSettings, desiredMovement);
|
rotateActorOnAxis(actor, 2, actorMovementSettings, desiredMovement);
|
||||||
RotateActorOnAxis(actor, 0, actorMovementSettings, desiredMovement);
|
rotateActorOnAxis(actor, 0, actorMovementSettings, desiredMovement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AiCombat::RotateActorOnAxis(const MWWorld::Ptr& actor, int axis,
|
void AiCombat::rotateActorOnAxis(const MWWorld::Ptr& actor, int axis,
|
||||||
MWMechanics::Movement& actorMovementSettings, MWMechanics::Movement& desiredMovement)
|
MWMechanics::Movement& actorMovementSettings, MWMechanics::Movement& desiredMovement)
|
||||||
{
|
{
|
||||||
actorMovementSettings.mRotation[axis] = 0;
|
actorMovementSettings.mRotation[axis] = 0;
|
||||||
|
@ -663,6 +593,104 @@ namespace MWMechanics
|
||||||
package.mPackage = combat.release();
|
package.mPackage = combat.release();
|
||||||
sequence.mPackages.push_back(package);
|
sequence.mPackages.push_back(package);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AiCombatStorage::startCombatMove(bool isNpc, bool isDistantCombat, float distToTarget, float rangeAttack)
|
||||||
|
{
|
||||||
|
if (mMovement.mPosition[0] || mMovement.mPosition[1])
|
||||||
|
{
|
||||||
|
mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability();
|
||||||
|
mCombatMove = true;
|
||||||
|
}
|
||||||
|
// only NPCs are smart enough to use dodge movements
|
||||||
|
else if (isNpc && (!isDistantCombat || (distToTarget < rangeAttack / 2)))
|
||||||
|
{
|
||||||
|
//apply sideway movement (kind of dodging) with some probability
|
||||||
|
if (Misc::Rng::rollClosedProbability() < 0.25)
|
||||||
|
{
|
||||||
|
mMovement.mPosition[0] = Misc::Rng::rollProbability() < 0.5 ? 1.0f : -1.0f;
|
||||||
|
mTimerCombatMove = 0.05f + 0.15f * Misc::Rng::rollClosedProbability();
|
||||||
|
mCombatMove = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDistantCombat && distToTarget < rangeAttack / 4)
|
||||||
|
{
|
||||||
|
mMovement.mPosition[1] = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiCombatStorage::updateCombatMove(float duration)
|
||||||
|
{
|
||||||
|
if (mCombatMove)
|
||||||
|
{
|
||||||
|
mTimerCombatMove -= duration;
|
||||||
|
if (mTimerCombatMove <= 0)
|
||||||
|
{
|
||||||
|
stopCombatMove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiCombatStorage::stopCombatMove()
|
||||||
|
{
|
||||||
|
mTimerCombatMove = 0;
|
||||||
|
mMovement.mPosition[1] = mMovement.mPosition[0] = 0;
|
||||||
|
mCombatMove = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiCombatStorage::startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController,
|
||||||
|
const ESM::Weapon* weapon, bool distantCombat)
|
||||||
|
{
|
||||||
|
if (mReadyToAttack && characterController.readyToStartAttack())
|
||||||
|
{
|
||||||
|
if (mAttackCooldown <= 0)
|
||||||
|
{
|
||||||
|
mAttack = true; // attack starts just now
|
||||||
|
characterController.setAttackingOrSpell(true);
|
||||||
|
|
||||||
|
if (!distantCombat)
|
||||||
|
chooseBestAttack(weapon, mMovement);
|
||||||
|
|
||||||
|
mStrength = Misc::Rng::rollClosedProbability();
|
||||||
|
|
||||||
|
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
|
||||||
|
|
||||||
|
float baseDelay = store.get<ESM::GameSetting>().find("fCombatDelayCreature")->getFloat();
|
||||||
|
if (actor.getClass().isNpc())
|
||||||
|
{
|
||||||
|
baseDelay = store.get<ESM::GameSetting>().find("fCombatDelayNPC")->getFloat();
|
||||||
|
|
||||||
|
//say a provoking combat phrase
|
||||||
|
int chance = store.get<ESM::GameSetting>().find("iVoiceAttackOdds")->getInt();
|
||||||
|
if (Misc::Rng::roll0to99() < chance)
|
||||||
|
{
|
||||||
|
MWBase::Environment::get().getDialogueManager()->say(actor, "attack");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mAttackCooldown = std::min(baseDelay + 0.01 * Misc::Rng::roll0to99(), baseDelay + 0.9);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
mAttackCooldown -= REACTION_INTERVAL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiCombatStorage::updateAttack(CharacterController& characterController)
|
||||||
|
{
|
||||||
|
if (mAttack && (characterController.getAttackStrength() >= mStrength || characterController.readyToPrepareAttack()))
|
||||||
|
{
|
||||||
|
mAttack = false;
|
||||||
|
}
|
||||||
|
characterController.setAttackingOrSpell(mAttack);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AiCombatStorage::stopAttack()
|
||||||
|
{
|
||||||
|
mMovement.mPosition[0] = 0;
|
||||||
|
mMovement.mPosition[1] = 0;
|
||||||
|
mMovement.mPosition[2] = 0;
|
||||||
|
mReadyToAttack = false;
|
||||||
|
mAttack = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -65,8 +65,8 @@ namespace MWMechanics
|
||||||
AiCombatStorage& storage, MWWorld::Ptr target);
|
AiCombatStorage& storage, MWWorld::Ptr target);
|
||||||
|
|
||||||
/// Transfer desired movement (from AiCombatStorage) to Actor
|
/// Transfer desired movement (from AiCombatStorage) to Actor
|
||||||
void UpdateActorsMovement(const MWWorld::Ptr& actor, MWMechanics::Movement& movement);
|
void updateActorsMovement(const MWWorld::Ptr& actor, MWMechanics::Movement& movement);
|
||||||
void RotateActorOnAxis(const MWWorld::Ptr& actor, int axis,
|
void rotateActorOnAxis(const MWWorld::Ptr& actor, int axis,
|
||||||
MWMechanics::Movement& actorMovementSettings, MWMechanics::Movement& desiredMovement);
|
MWMechanics::Movement& actorMovementSettings, MWMechanics::Movement& desiredMovement);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
#include <components/esm/loadcell.hpp>
|
#include <components/esm/loadcell.hpp>
|
||||||
#include <components/esm/loadland.hpp>
|
#include <components/esm/loadland.hpp>
|
||||||
|
#include <components/esm/loadmgef.hpp>
|
||||||
|
|
||||||
#include "../mwbase/world.hpp"
|
#include "../mwbase/world.hpp"
|
||||||
#include "../mwbase/environment.hpp"
|
#include "../mwbase/environment.hpp"
|
||||||
|
@ -129,3 +130,10 @@ bool MWMechanics::AiPackage::doesPathNeedRecalc(ESM::Pathgrid::Point dest, const
|
||||||
{
|
{
|
||||||
return distance(mPrevDest, dest) > 10;
|
return distance(mPrevDest, dest) > 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MWMechanics::AiPackage::isTargetMagicallyHidden(const MWWorld::Ptr& target)
|
||||||
|
{
|
||||||
|
const MagicEffects& magicEffects(target.getClass().getCreatureStats(target).getMagicEffects());
|
||||||
|
return (magicEffects.get(ESM::MagicEffect::Invisibility).getMagnitude() > 0)
|
||||||
|
|| (magicEffects.get(ESM::MagicEffect::Chameleon).getMagnitude() > 75);
|
||||||
|
}
|
||||||
|
|
|
@ -69,6 +69,8 @@ namespace MWMechanics
|
||||||
/// Simulates the passing of time
|
/// Simulates the passing of time
|
||||||
virtual void fastForward(const MWWorld::Ptr& actor, AiState& state) {}
|
virtual void fastForward(const MWWorld::Ptr& actor, AiState& state) {}
|
||||||
|
|
||||||
|
bool isTargetMagicallyHidden(const MWWorld::Ptr& target);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
/// Causes the actor to attempt to walk to the specified location
|
/// Causes the actor to attempt to walk to the specified location
|
||||||
/** \return If the actor has arrived at his destination **/
|
/** \return If the actor has arrived at his destination **/
|
||||||
|
|
|
@ -43,8 +43,7 @@ bool AiPursue::execute (const MWWorld::Ptr& actor, CharacterController& characte
|
||||||
)
|
)
|
||||||
return true; //Target doesn't exist
|
return true; //Target doesn't exist
|
||||||
|
|
||||||
if (target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Invisibility).getMagnitude() > 0
|
if (isTargetMagicallyHidden(target))
|
||||||
|| target.getClass().getCreatureStats(target).getMagicEffects().get(ESM::MagicEffect::Chameleon).getMagnitude() > 75)
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
if(target.getClass().getCreatureStats(target).isDead())
|
if(target.getClass().getCreatureStats(target).isDead())
|
||||||
|
|
Loading…
Reference in a new issue