update to combat ai behaviour

actorid
mrcheko 11 years ago
parent f62497dfdc
commit d5f794d4fb

@ -13,6 +13,7 @@
#include "../mwworld/inventorystore.hpp"
#include "../mwworld/manualref.hpp"
#include "../mwworld/actionequip.hpp"
#include "../mwworld/player.hpp"
#include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp"
@ -190,7 +191,7 @@ namespace MWMechanics
&& LOS
)
{
creatureStats.getAiSequence().stack(AiCombat("player"));
creatureStats.getAiSequence().stack(AiCombat(MWBase::Environment::get().getWorld()->getPlayer().getPlayer()));
creatureStats.setHostile(true);
}
}

@ -4,14 +4,19 @@
#include "../mwworld/class.hpp"
#include "../mwworld/timestamp.hpp"
#include "../mwbase/world.hpp"
#include "../mwbase/environment.hpp"
#include "../mwbase/mechanicsmanager.hpp"
#include "../mwbase/dialoguemanager.hpp"
#include "character.hpp"
#include "../mwworld/inventorystore.hpp"
#include "creaturestats.hpp"
#include "npcstats.hpp"
#include <OgreMath.h>
#include <OgreVector3.h>
namespace
{
@ -25,111 +30,309 @@ namespace
namespace MWMechanics
{
AiCombat::AiCombat(const std::string &targetId)
:mTargetId(targetId),mTimer(0),mTimer2(0)
AiCombat::AiCombat(const MWWorld::Ptr& actor) :
mTarget(actor),
mTimerAttack(0),
mTimerReact(0),
mTimerCombatMove(0),
mCloseUp(false),
mReadyToAttack(false),
mStrike(false),
mCombatMove(false),
mMovement()
{
}
bool AiCombat::execute (const MWWorld::Ptr& actor,float duration)
{
if(!MWWorld::Class::get(actor).getCreatureStats(actor).isHostile()) return true;
//General description
if(!actor.getClass().getCreatureStats(actor).isHostile())
return true;
if(actor.getClass().getCreatureStats(actor).getHealth().getCurrent() <= 0)
return true;
const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->getPtr(mTargetId, false);
//update every frame
determineAttackType(actor, mMovement);
if(MWWorld::Class::get(actor).getCreatureStats(actor).getHealth().getCurrent() <= 0) return true;
if(mCombatMove)
{
mTimerCombatMove -= duration;
if( mTimerCombatMove <= 0)
{
mTimerCombatMove = 0;
mMovement.mPosition[1] = mMovement.mPosition[0] = 0;
mCombatMove = false;
}
}
actor.getClass().getMovementSettings(actor) = mMovement;
//actor.getClass().getCreatureStats(actor).setAttackingOrSpell(mReadyToAttack);
mTimerAttack -= duration;
actor.getClass().getCreatureStats(actor).setAttackingOrSpell(mStrike);
float tReaction = 0.25f;
if(mTimerReact < tReaction)
{
mTimerReact += duration;
return false;
}
else
{
mTimerReact = 0;
}
//actual attacking logic
//TODO: Some skills affect period of strikes.For berserk-like style period ~ 0.25f
float attackPeriod = 1.0f;
if(mReadyToAttack)
{
if(mTimerAttack <= -attackPeriod)
{
//TODO: should depend on time between 'start' to 'min attack'
//for better controlling of NPCs' attack strength.
//Also it seems that this time is different for slash/thrust/chop
mTimerAttack = 0.35f * static_cast<float>(rand())/RAND_MAX;
mStrike = true;
//say a provoking combat phrase
if (actor.getClass().isNpc())
{
const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore();
int chance = store.get<ESM::GameSetting>().find("iVoiceAttackOdds")->getInt();
int roll = std::rand()/ (static_cast<double> (RAND_MAX) + 1) * 100; // [0, 99]
if (roll < chance)
{
MWBase::Environment::get().getDialogueManager()->say(actor, "attack");
}
}
}
else if (mTimerAttack <= 0)
mStrike = false;
}
else
{
mTimerAttack = -attackPeriod;
mStrike = false;
}
const MWWorld::Class &cls = actor.getClass();
const ESM::Weapon *weapon = NULL;
MWMechanics::WeaponType weaptype;
float weapRange, weapSpeed = 1.0f;
if(actor.getTypeName() == typeid(ESM::NPC).name())
{
MWWorld::Class::get(actor).
MWWorld::Class::get(actor).setStance(actor, MWWorld::Class::Run,true);
MWMechanics::DrawState_ state = MWWorld::Class::get(actor).getNpcStats(actor).getDrawState();
actor.getClass().setStance(actor, MWWorld::Class::Run,true);
MWMechanics::DrawState_ state = actor.getClass().getNpcStats(actor).getDrawState();
if (state == MWMechanics::DrawState_Spell || state == MWMechanics::DrawState_Nothing)
MWWorld::Class::get(actor).getNpcStats(actor).setDrawState(MWMechanics::DrawState_Weapon);
//MWWorld::Class::get(actor).getCreatureStats(actor).setAttackingOrSpell(true);
actor.getClass().getNpcStats(actor).setDrawState(MWMechanics::DrawState_Weapon);
//Get weapon speed and range
MWWorld::ContainerStoreIterator weaponSlot =
MWMechanics::getActiveWeapon(cls.getNpcStats(actor), cls.getInventoryStore(actor), &weaptype);
if (weaptype == WeapType_HandToHand)
{
const MWWorld::Store<ESM::GameSetting> &gmst =
MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();
weapRange = gmst.find("fHandToHandReach")->getFloat();
}
else
{
weapon = weaponSlot->get<ESM::Weapon>()->mBase;
weapRange = weapon->mData.mReach;
weapSpeed = weapon->mData.mSpeed;
}
weapRange *= 100.0f;
}
ESM::Position pos = actor.getRefData().getPosition();
const ESM::Pathgrid *pathgrid =
MWBase::Environment::get().getWorld()->getStore().get<ESM::Pathgrid>().search(*actor.getCell()->mCell);
float xCell = 0;
float yCell = 0;
//MWWorld::Class::get(actor).getCreatureStats(actor).setAttackingOrSpell(false);
if (actor.getCell()->mCell->isExterior())
ESM::Position pos = actor.getRefData().getPosition();
float zAngle;
float rangeMelee;
float rangeCloseUp;
bool distantCombat = false;
if (weaptype==WeapType_BowAndArrow || weaptype==WeapType_Crossbow || weaptype==WeapType_ThowWeapon) // || WeapType_Spell_OnTarget
{
rangeMelee = 1000; // TODO: should depend on archer skill
rangeCloseUp = 0; //doesn't needed when attacking from distance
distantCombat = true;
}
else
{
xCell = actor.getCell()->mCell->mData.mX * ESM::Land::REAL_SIZE;
yCell = actor.getCell()->mCell->mData.mY * ESM::Land::REAL_SIZE;
rangeMelee = weapRange;
rangeCloseUp = 300;
}
ESM::Pathgrid::Point dest;
dest.mX = target.getRefData().getPosition().pos[0];
dest.mY = target.getRefData().getPosition().pos[1];
dest.mZ = target.getRefData().getPosition().pos[2];
Ogre::Vector3 vStart(pos.pos[0], pos.pos[1], pos.pos[2]);
ESM::Position targetPos = mTarget.getRefData().getPosition();
Ogre::Vector3 vDest(targetPos.pos[0], targetPos.pos[1], targetPos.pos[2]);
Ogre::Vector3 vDir = vDest - vStart;
float distBetween = vDir.length();
if(distBetween < rangeMelee || (distBetween <= rangeCloseUp && mCloseUp) )
{
//Melee and Close-up combat
vDir.z = 0;
float dirLen = vDir.length();
zAngle = Ogre::Radian( Ogre::Math::ACos(vDir.y / dirLen) * sgn(Ogre::Math::ASin(vDir.x / dirLen)) ).valueDegrees();
// TODO: use movement settings instead of rotating directly
MWBase::Environment::get().getWorld()->rotateObject(actor, 0, 0, zAngle, false);
if(mPathFinder.isPathConstructed())
mPathFinder.clearPath();
//MWWorld::Class::get(actor).getMovementSettings(actor).mPosition[1] = 0;
ESM::Pathgrid::Point start;
start.mX = pos.pos[0];
start.mY = pos.pos[1];
start.mZ = pos.pos[2];
//bool LOS = MWBase::Environment::get().getWorld()->getLOS(actor, mTarget);
if (mCloseUp && distBetween > rangeMelee)
{
//Close-up combat: just run up on target
mMovement.mPosition[1] = 1;
}
else
{
//Melee: stop running and attack
mMovement.mPosition[1] = 0;
chooseBestAttack(weapon, mMovement);
if(mMovement.mPosition[0] != 0 || mMovement.mPosition[1])
{
mTimerCombatMove = 0.1f + 0.1f * static_cast<float>(rand())/RAND_MAX;
mCombatMove = true;
}
else if(!distantCombat || (distantCombat && rangeMelee/5))
{
//apply sideway movement (kind of dodging) with some probability
if(static_cast<float>(rand())/RAND_MAX < 0.25)
{
mMovement.mPosition[0] = static_cast<float>(rand())/RAND_MAX < 0.5? 1: -1;
mTimerCombatMove = 0.05f + 0.15f * static_cast<float>(rand())/RAND_MAX;
mCombatMove = true;
}
}
mTimer2 = mTimer2 + duration;
if(distantCombat && distBetween < rangeMelee/4)
{
mMovement.mPosition[1] = -1;
}
if(!mPathFinder.isPathConstructed())
mPathFinder.buildPath(start, dest, pathgrid, xCell, yCell, true);
mReadyToAttack = true;
//only once got in melee combat, actor is allowed to use close-up shortcutting
mCloseUp = true;
}
}
else
{
mPathFinder2.buildPath(start, dest, pathgrid, xCell, yCell, true);
ESM::Pathgrid::Point lastPt = mPathFinder.getPath().back();
if((mTimer2 > 0.25)&&(mPathFinder2.getPathSize() < mPathFinder.getPathSize() ||
(dest.mX - lastPt.mX)*(dest.mX - lastPt.mX)+(dest.mY - lastPt.mY)*(dest.mY - lastPt.mY)+(dest.mZ - lastPt.mZ)*(dest.mZ - lastPt.mZ) > 200*200))
//target is at far distance: build & follow the path
mCloseUp = false;
buildNewPath(actor);
//delete visited path node
mPathFinder.checkPathCompleted(pos.pos[0],pos.pos[1],pos.pos[2]);
zAngle = mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]);
// TODO: use movement settings instead of rotating directly
MWBase::Environment::get().getWorld()->rotateObject(actor, 0, 0, zAngle, false);
//MWWorld::Class::get(actor).getMovementSettings(actor).mPosition[1] = 1;
mMovement.mPosition[1] = 1;
mReadyToAttack = false;
}
if(distBetween > rangeMelee)
{
//special run attack; it shouldn't affect melee combat tactics
if(actor.getClass().getMovementSettings(actor).mPosition[1] == 1)
{
mTimer2 = 0;
mPathFinder = mPathFinder2;
//check if actor can overcome the distance = distToTarget - attackerWeapRange
//less than in time of playing weapon anim from 'start' to 'hit' tags (t_swing)
//then start attacking
float speed1 = cls.getSpeed(actor);
float speed2 = cls.getSpeed(mTarget);
if(actor.getClass().getMovementSettings(mTarget).mPosition[0] == 0
&& actor.getClass().getMovementSettings(mTarget).mPosition[1] == 0)
speed2 = 0;
float s1 = distBetween - weapRange;
float t = s1/speed1;
float s2 = speed2 * t;
float t_swing = 0.17f/weapSpeed;//0.17 should be the time of playing weapon anim from 'start' to 'hit' tags
if (t + s2/speed1 <= t_swing)
{
mReadyToAttack = true;
if(mTimerAttack <= -attackPeriod)
{
mTimerAttack = 0.45f*static_cast<float>(rand())/RAND_MAX;
mStrike = true;
}
}
}
}
mPathFinder.checkPathCompleted(pos.pos[0],pos.pos[1],pos.pos[2]);
return false;
}
float zAngle = mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]);
void AiCombat::buildNewPath(const MWWorld::Ptr& actor)
{
//Construct path to target
ESM::Pathgrid::Point dest;
dest.mX = mTarget.getRefData().getPosition().pos[0];
dest.mY = mTarget.getRefData().getPosition().pos[1];
dest.mZ = mTarget.getRefData().getPosition().pos[2];
Ogre::Vector3 newPathTarget = Ogre::Vector3(dest.mX, dest.mY, dest.mZ);
// TODO: use movement settings instead of rotating directly
MWBase::Environment::get().getWorld()->rotateObject(actor, 0, 0, zAngle, false);
MWWorld::Class::get(actor).getMovementSettings(actor).mPosition[1] = 1;
ESM::Pathgrid::Point lastPt = mPathFinder.getPath().back();
Ogre::Vector3 currPathTarget(lastPt.mX, lastPt.mY, lastPt.mZ);
float dist = Ogre::Math::Abs((newPathTarget - currPathTarget).length());
float targetPosThreshold;
bool isOutside = actor.getCell()->mCell->isExterior();
if (isOutside)
targetPosThreshold = 300;
else
targetPosThreshold = 100;
float range = 100;
MWWorld::Class::get(actor).getCreatureStats(actor).setAttackingOrSpell(false);
if((dest.mX - start.mX)*(dest.mX - start.mX)+(dest.mY - start.mY)*(dest.mY - start.mY)+(dest.mZ - start.mZ)*(dest.mZ - start.mZ)
< range*range)
if(dist > targetPosThreshold)
{
float directionX = dest.mX - start.mX;
float directionY = dest.mY - start.mY;
float directionResult = sqrt(directionX * directionX + directionY * directionY);
//construct new path only if target has moved away more than on <targetPosThreshold>
ESM::Position pos = actor.getRefData().getPosition();
zAngle = Ogre::Radian( Ogre::Math::ACos(directionY / directionResult) * sgn(Ogre::Math::ASin(directionX / directionResult)) ).valueDegrees();
// TODO: use movement settings instead of rotating directly
MWBase::Environment::get().getWorld()->rotateObject(actor, 0, 0, zAngle, false);
ESM::Pathgrid::Point start;
start.mX = pos.pos[0];
start.mY = pos.pos[1];
start.mZ = pos.pos[2];
mPathFinder.clearPath();
const ESM::Pathgrid *pathgrid =
MWBase::Environment::get().getWorld()->getStore().get<ESM::Pathgrid>().search(*actor.getCell()->mCell);
float xCell = 0;
float yCell = 0;
if(mTimer == 0)
{
MWWorld::Class::get(actor).getCreatureStats(actor).setAttackingOrSpell(false);
//mTimer = mTimer + duration;
}
if( mTimer > 1)
if (actor.getCell()->mCell->isExterior())
{
MWWorld::Class::get(actor).getCreatureStats(actor).setAttackingOrSpell(true);
mTimer = 0;
xCell = actor.getCell()->mCell->mData.mX * ESM::Land::REAL_SIZE;
yCell = actor.getCell()->mCell->mData.mY * ESM::Land::REAL_SIZE;
}
if(!mPathFinder.isPathConstructed())
mPathFinder.buildPath(start, dest, pathgrid, xCell, yCell, isOutside);
else
{
mTimer = mTimer + duration;
}
PathFinder newPathFinder;
newPathFinder.buildPath(start, dest, pathgrid, xCell, yCell, isOutside);
MWWorld::Class::get(actor).getMovementSettings(actor).mPosition[1] = 0;
//MWWorld::Class::get(actor).getCreatureStats(actor).setAttackingOrSpell(!MWWorld::Class::get(actor).getCreatureStats(actor).getAttackingOrSpell());
//TO EXPLORE:
//maybe here is a mistake (?): PathFinder::getPathSize() returns number of grid points in the path,
//not the actual path length. Here we should know if the new path is actually more effective.
//if(pathFinder2.getPathSize() < mPathFinder.getPathSize())
mPathFinder = newPathFinder;
}
}
return false;
}
int AiCombat::getTypeId() const
@ -146,5 +349,36 @@ namespace MWMechanics
{
return new AiCombat(*this);
}
static void determineAttackType(const MWWorld::Ptr& actor, MWMechanics::Movement &movement)
{
if (movement.mPosition[0] && !movement.mPosition[1]) //sideway
actor.getClass().getCreatureStats(actor).setAttackType(MWMechanics::CreatureStats::AT_Slash);
else if (movement.mPosition[1]) //forward
actor.getClass().getCreatureStats(actor).setAttackType(MWMechanics::CreatureStats::AT_Thrust);
else
actor.getClass().getCreatureStats(actor).setAttackType(MWMechanics::CreatureStats::AT_Chop);
}
static void chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement)
{
if (weapon == NULL)
return;
int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2;
int chop = (weapon->mData.mChop[0] + weapon->mData.mChop[1])/2;
int thrust = (weapon->mData.mThrust[0] + weapon->mData.mThrust[1])/2;
float total = slash + chop + thrust;
if(static_cast<float>(rand())/RAND_MAX <= static_cast<float>(slash)/total)
{
movement.mPosition[0] = (static_cast<float>(rand())/RAND_MAX < 0.5f)? 1: -1;
movement.mPosition[1] = 0;
}
if (static_cast<float>(rand())/RAND_MAX <= static_cast<float>(thrust)/total)
movement.mPosition[1] = 1;
//else chop
}
}

@ -7,12 +7,14 @@
#include "movement.hpp"
#include "../mwbase/world.hpp"
namespace MWMechanics
{
class AiCombat : public AiPackage
{
public:
AiCombat(const std::string &targetId);
AiCombat(const MWWorld::Ptr& actor);
virtual AiCombat *clone() const;
@ -24,13 +26,27 @@ namespace MWMechanics
virtual unsigned int getPriority() const;
private:
std::string mTargetId;
PathFinder mPathFinder;
PathFinder mPathFinder2;
float mTimer;
float mTimer2;
//controls duration of the actual strike
float mTimerAttack;
float mTimerReact;
//controls duration of the sideway & forward moves
//when mCombatMove is true
float mTimerCombatMove;
bool mReadyToAttack, mStrike;
bool mCloseUp;
bool mCombatMove;
MWMechanics::Movement mMovement;
MWWorld::Ptr mTarget;
void buildNewPath(const MWWorld::Ptr& actor);
};
static void determineAttackType(const MWWorld::Ptr& actor, MWMechanics::Movement &movement);
//chooses an attack depending on probability to avoid uniformity
static void chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement);
}
#endif

@ -315,7 +315,7 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat
}
void CharacterController::getWeaponGroup(WeaponType weaptype, std::string &group)
void getWeaponGroup(WeaponType weaptype, std::string &group)
{
const WeaponInfo *info = std::find_if(sWeaponTypeList, sWeaponTypeListEnd, FindWeaponType(weaptype));
if(info != sWeaponTypeListEnd)
@ -323,7 +323,7 @@ void CharacterController::getWeaponGroup(WeaponType weaptype, std::string &group
}
MWWorld::ContainerStoreIterator CharacterController::getActiveWeapon(NpcStats &stats, MWWorld::InventoryStore &inv, WeaponType *weaptype)
MWWorld::ContainerStoreIterator getActiveWeapon(NpcStats &stats, MWWorld::InventoryStore &inv, WeaponType *weaptype)
{
if(stats.getDrawState() == DrawState_Spell)
{
@ -441,6 +441,7 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim
{
getWeaponGroup(mWeaponType, mCurrentWeapon);
mUpperBodyState = UpperCharState_WeapEquiped;
mAnimation->showWeapons(true);
}
}

@ -168,12 +168,6 @@ class CharacterController
void refreshCurrentAnims(CharacterState idle, CharacterState movement, bool force=false);
static void getWeaponGroup(WeaponType weaptype, std::string &group);
static MWWorld::ContainerStoreIterator getActiveWeapon(NpcStats &stats,
MWWorld::InventoryStore &inv,
WeaponType *weaptype);
void clearAnimQueue();
bool updateNpcState(bool onground, bool inwater, bool isrunning, bool sneak);
@ -205,6 +199,10 @@ public:
void forceStateUpdate();
};
void getWeaponGroup(WeaponType weaptype, std::string &group);
MWWorld::ContainerStoreIterator getActiveWeapon(NpcStats &stats,
MWWorld::InventoryStore &inv,
WeaponType *weaptype);
}
#endif /* GAME_MWMECHANICS_CHARACTER_HPP */

Loading…
Cancel
Save