mirror of
https://github.com/TES3MP/openmw-tes3mp.git
synced 2025-03-03 13:49:40 +00:00
Merge remote-tracking branch 'terrorfisch/aistate'
This commit is contained in:
commit
6beee95151
23 changed files with 537 additions and 307 deletions
|
@ -25,6 +25,7 @@
|
||||||
#include "npcstats.hpp"
|
#include "npcstats.hpp"
|
||||||
#include "creaturestats.hpp"
|
#include "creaturestats.hpp"
|
||||||
#include "movement.hpp"
|
#include "movement.hpp"
|
||||||
|
#include "character.hpp"
|
||||||
|
|
||||||
#include "../mwbase/environment.hpp"
|
#include "../mwbase/environment.hpp"
|
||||||
#include "../mwbase/mechanicsmanager.hpp"
|
#include "../mwbase/mechanicsmanager.hpp"
|
||||||
|
@ -1170,7 +1171,7 @@ namespace MWMechanics
|
||||||
updateCrimePersuit(iter->first, duration);
|
updateCrimePersuit(iter->first, duration);
|
||||||
|
|
||||||
if (iter->first != player)
|
if (iter->first != player)
|
||||||
iter->first.getClass().getCreatureStats(iter->first).getAiSequence().execute(iter->first, duration);
|
iter->first.getClass().getCreatureStats(iter->first).getAiSequence().execute(iter->first,iter->second->getAiState(), duration);
|
||||||
|
|
||||||
CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first);
|
CreatureStats &stats = iter->first.getClass().getCreatureStats(iter->first);
|
||||||
if(!stats.isDead())
|
if(!stats.isDead())
|
||||||
|
|
|
@ -21,7 +21,7 @@ MWMechanics::AiActivate *MWMechanics::AiActivate::clone() const
|
||||||
{
|
{
|
||||||
return new AiActivate(*this);
|
return new AiActivate(*this);
|
||||||
}
|
}
|
||||||
bool MWMechanics::AiActivate::execute (const MWWorld::Ptr& actor,float duration)
|
bool MWMechanics::AiActivate::execute (const MWWorld::Ptr& actor, AiState& state, float duration)
|
||||||
{
|
{
|
||||||
ESM::Position pos = actor.getRefData().getPosition(); //position of the actor
|
ESM::Position pos = actor.getRefData().getPosition(); //position of the actor
|
||||||
const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mObjectId, false); //The target to follow
|
const MWWorld::Ptr target = MWBase::Environment::get().getWorld()->searchPtr(mObjectId, false); //The target to follow
|
||||||
|
|
|
@ -28,7 +28,7 @@ namespace MWMechanics
|
||||||
AiActivate(const ESM::AiSequence::AiActivate* activate);
|
AiActivate(const ESM::AiSequence::AiActivate* activate);
|
||||||
|
|
||||||
virtual AiActivate *clone() const;
|
virtual AiActivate *clone() const;
|
||||||
virtual bool execute (const MWWorld::Ptr& actor,float duration);
|
virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration);
|
||||||
virtual int getTypeId() const;
|
virtual int getTypeId() const;
|
||||||
|
|
||||||
virtual void writeState(ESM::AiSequence::AiSequence& sequence) const;
|
virtual void writeState(ESM::AiSequence::AiSequence& sequence) const;
|
||||||
|
|
|
@ -18,7 +18,7 @@ MWMechanics::AiAvoidDoor::AiAvoidDoor(const MWWorld::Ptr& doorPtr)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MWMechanics::AiAvoidDoor::execute (const MWWorld::Ptr& actor,float duration)
|
bool MWMechanics::AiAvoidDoor::execute (const MWWorld::Ptr& actor, AiState& state, float duration)
|
||||||
{
|
{
|
||||||
|
|
||||||
ESM::Position pos = actor.getRefData().getPosition();
|
ESM::Position pos = actor.getRefData().getPosition();
|
||||||
|
|
|
@ -20,7 +20,7 @@ namespace MWMechanics
|
||||||
|
|
||||||
virtual AiAvoidDoor *clone() const;
|
virtual AiAvoidDoor *clone() const;
|
||||||
|
|
||||||
virtual bool execute (const MWWorld::Ptr& actor,float duration);
|
virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration);
|
||||||
|
|
||||||
virtual int getTypeId() const;
|
virtual int getTypeId() const;
|
||||||
|
|
||||||
|
|
|
@ -26,12 +26,6 @@
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
static float sgn(Ogre::Radian a)
|
|
||||||
{
|
|
||||||
if(a.valueDegrees() > 0)
|
|
||||||
return 1.0;
|
|
||||||
return -1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
//chooses an attack depending on probability to avoid uniformity
|
//chooses an attack depending on probability to avoid uniformity
|
||||||
ESM::Weapon::AttackType chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement);
|
ESM::Weapon::AttackType chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement);
|
||||||
|
@ -41,16 +35,15 @@ namespace
|
||||||
Ogre::Vector3 AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const Ogre::Vector3& vLastTargetPos,
|
Ogre::Vector3 AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const Ogre::Vector3& vLastTargetPos,
|
||||||
float duration, int weapType, float strength);
|
float duration, int weapType, float strength);
|
||||||
|
|
||||||
float getZAngleToDir(const Ogre::Vector3& dir, float dirLen = 0.0f)
|
float getZAngleToDir(const Ogre::Vector3& dir)
|
||||||
{
|
{
|
||||||
float len = (dirLen > 0.0f)? dirLen : dir.length();
|
return Ogre::Math::ATan2(dir.x,dir.y).valueDegrees();
|
||||||
return Ogre::Radian( Ogre::Math::ACos(dir.y / len) * sgn(Ogre::Math::ASin(dir.x / len)) ).valueDegrees();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
float getXAngleToDir(const Ogre::Vector3& dir, float dirLen = 0.0f)
|
float getXAngleToDir(const Ogre::Vector3& dir, float dirLen = 0.0f)
|
||||||
{
|
{
|
||||||
float len = (dirLen > 0.0f)? dirLen : dir.length();
|
float len = (dirLen > 0.0f)? dirLen : dir.length();
|
||||||
return Ogre::Radian(-Ogre::Math::ASin(dir.z / len)).valueDegrees();
|
return -Ogre::Math::ASin(dir.z / len).valueDegrees();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -88,40 +81,60 @@ namespace MWMechanics
|
||||||
static const float DOOR_CHECK_INTERVAL = 1.5f; // same as AiWander
|
static const float DOOR_CHECK_INTERVAL = 1.5f; // same as AiWander
|
||||||
// NOTE: MIN_DIST_TO_DOOR_SQUARED is defined in obstacle.hpp
|
// NOTE: MIN_DIST_TO_DOOR_SQUARED is defined in obstacle.hpp
|
||||||
|
|
||||||
|
|
||||||
|
/// \brief This class holds the variables AiCombat needs which are deleted if the package becomes inactive.
|
||||||
|
struct AiCombatStorage : AiTemporaryBase
|
||||||
|
{
|
||||||
|
float mTimerAttack;
|
||||||
|
float mTimerReact;
|
||||||
|
float mTimerCombatMove;
|
||||||
|
bool mReadyToAttack;
|
||||||
|
bool mAttack;
|
||||||
|
bool mFollowTarget;
|
||||||
|
bool mCombatMove;
|
||||||
|
Ogre::Vector3 mLastTargetPos;
|
||||||
|
const MWWorld::CellStore* mCell;
|
||||||
|
boost::shared_ptr<Action> mCurrentAction;
|
||||||
|
float mActionCooldown;
|
||||||
|
float mStrength;
|
||||||
|
float mMinMaxAttackDuration[3][2];
|
||||||
|
bool mMinMaxAttackDurationInitialised;
|
||||||
|
bool mForceNoShortcut;
|
||||||
|
ESM::Position mShortcutFailPos;
|
||||||
|
Ogre::Vector3 mLastActorPos;
|
||||||
|
MWMechanics::Movement mMovement;
|
||||||
|
|
||||||
|
AiCombatStorage():
|
||||||
|
mTimerAttack(0),
|
||||||
|
mTimerReact(0),
|
||||||
|
mTimerCombatMove(0),
|
||||||
|
mAttack(false),
|
||||||
|
mFollowTarget(false),
|
||||||
|
mCombatMove(false),
|
||||||
|
mReadyToAttack(false),
|
||||||
|
mForceNoShortcut(false),
|
||||||
|
mCell(NULL),
|
||||||
|
mCurrentAction(),
|
||||||
|
mActionCooldown(0),
|
||||||
|
mStrength(),
|
||||||
|
mMinMaxAttackDurationInitialised(false),
|
||||||
|
mLastTargetPos(0,0,0),
|
||||||
|
mLastActorPos(0,0,0),
|
||||||
|
mMovement(){}
|
||||||
|
};
|
||||||
|
|
||||||
AiCombat::AiCombat(const MWWorld::Ptr& actor) :
|
AiCombat::AiCombat(const MWWorld::Ptr& actor) :
|
||||||
mTargetActorId(actor.getClass().getCreatureStats(actor).getActorId())
|
mTargetActorId(actor.getClass().getCreatureStats(actor).getActorId())
|
||||||
, mMinMaxAttackDuration()
|
{}
|
||||||
, mMovement()
|
|
||||||
{
|
|
||||||
init();
|
|
||||||
|
|
||||||
mLastTargetPos = Ogre::Vector3(actor.getRefData().getPosition().pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
AiCombat::AiCombat(const ESM::AiSequence::AiCombat *combat)
|
AiCombat::AiCombat(const ESM::AiSequence::AiCombat *combat)
|
||||||
: mMinMaxAttackDuration()
|
|
||||||
, mMovement()
|
|
||||||
{
|
{
|
||||||
mTargetActorId = combat->mTargetActorId;
|
mTargetActorId = combat->mTargetActorId;
|
||||||
|
|
||||||
init();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AiCombat::init()
|
void AiCombat::init()
|
||||||
{
|
{
|
||||||
mActionCooldown = 0;
|
|
||||||
mTimerAttack = 0;
|
|
||||||
mTimerReact = 0;
|
|
||||||
mTimerCombatMove = 0;
|
|
||||||
mFollowTarget = false;
|
|
||||||
mReadyToAttack = false;
|
|
||||||
mAttack = false;
|
|
||||||
mCombatMove = false;
|
|
||||||
mForceNoShortcut = false;
|
|
||||||
mStrength = 0;
|
|
||||||
mCell = NULL;
|
|
||||||
mLastTargetPos = Ogre::Vector3(0,0,0);
|
|
||||||
mMinMaxAttackDurationInitialised = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -170,8 +183,13 @@ namespace MWMechanics
|
||||||
* Use the Observer Pattern to co-ordinate attacks, provide intelligence on
|
* Use the Observer Pattern to co-ordinate attacks, provide intelligence on
|
||||||
* whether the target was hit, etc.
|
* whether the target was hit, etc.
|
||||||
*/
|
*/
|
||||||
bool AiCombat::execute (const MWWorld::Ptr& actor,float duration)
|
bool AiCombat::execute (const MWWorld::Ptr& actor, AiState& state, float duration)
|
||||||
{
|
{
|
||||||
|
// get or create temporary storage
|
||||||
|
AiCombatStorage& storage = state.get<AiCombatStorage>();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//General description
|
//General description
|
||||||
if(actor.getClass().getCreatureStats(actor).isDead())
|
if(actor.getClass().getCreatureStats(actor).isDead())
|
||||||
return true;
|
return true;
|
||||||
|
@ -197,32 +215,39 @@ namespace MWMechanics
|
||||||
{
|
{
|
||||||
actorClass.getCreatureStats(actor).setAttackingOrSpell(false);
|
actorClass.getCreatureStats(actor).setAttackingOrSpell(false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//Update every frame
|
//Update every frame
|
||||||
if(mCombatMove)
|
bool& combatMove = storage.mCombatMove;
|
||||||
|
float& timerCombatMove = storage.mTimerCombatMove;
|
||||||
|
MWMechanics::Movement& movement = storage.mMovement;
|
||||||
|
if(combatMove)
|
||||||
{
|
{
|
||||||
mTimerCombatMove -= duration;
|
timerCombatMove -= duration;
|
||||||
if( mTimerCombatMove <= 0)
|
if( timerCombatMove <= 0)
|
||||||
{
|
{
|
||||||
mTimerCombatMove = 0;
|
timerCombatMove = 0;
|
||||||
mMovement.mPosition[1] = mMovement.mPosition[0] = 0;
|
movement.mPosition[1] = movement.mPosition[0] = 0;
|
||||||
mCombatMove = false;
|
combatMove = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
actorClass.getMovementSettings(actor) = mMovement;
|
actorClass.getMovementSettings(actor) = movement;
|
||||||
actorClass.getMovementSettings(actor).mRotation[0] = 0;
|
actorClass.getMovementSettings(actor).mRotation[0] = 0;
|
||||||
actorClass.getMovementSettings(actor).mRotation[2] = 0;
|
actorClass.getMovementSettings(actor).mRotation[2] = 0;
|
||||||
|
|
||||||
if(mMovement.mRotation[2] != 0)
|
if(movement.mRotation[2] != 0)
|
||||||
{
|
{
|
||||||
if(zTurn(actor, Ogre::Degree(mMovement.mRotation[2]))) mMovement.mRotation[2] = 0;
|
if(zTurn(actor, Ogre::Degree(movement.mRotation[2]))) movement.mRotation[2] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(mMovement.mRotation[0] != 0)
|
if(movement.mRotation[0] != 0)
|
||||||
{
|
{
|
||||||
if(smoothTurn(actor, Ogre::Degree(mMovement.mRotation[0]), 0)) mMovement.mRotation[0] = 0;
|
if(smoothTurn(actor, Ogre::Degree(movement.mRotation[0]), 0)) movement.mRotation[0] = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Some skills affect period of strikes.For berserk-like style period ~ 0.25f
|
//TODO: Some skills affect period of strikes.For berserk-like style period ~ 0.25f
|
||||||
|
@ -230,44 +255,57 @@ namespace MWMechanics
|
||||||
|
|
||||||
ESM::Weapon::AttackType attackType;
|
ESM::Weapon::AttackType attackType;
|
||||||
|
|
||||||
if(mReadyToAttack)
|
|
||||||
|
|
||||||
|
|
||||||
|
bool& attack = storage.mAttack;
|
||||||
|
bool& readyToAttack = storage.mReadyToAttack;
|
||||||
|
float& timerAttack = storage.mTimerAttack;
|
||||||
|
|
||||||
|
bool& minMaxAttackDurationInitialised = storage.mMinMaxAttackDurationInitialised;
|
||||||
|
float (&minMaxAttackDuration)[3][2] = storage.mMinMaxAttackDuration;
|
||||||
|
|
||||||
|
if(readyToAttack)
|
||||||
{
|
{
|
||||||
if (!mMinMaxAttackDurationInitialised)
|
if (!minMaxAttackDurationInitialised)
|
||||||
{
|
{
|
||||||
// TODO: this must be updated when a different weapon is equipped
|
// TODO: this must be updated when a different weapon is equipped
|
||||||
getMinMaxAttackDuration(actor, mMinMaxAttackDuration);
|
getMinMaxAttackDuration(actor, minMaxAttackDuration);
|
||||||
mMinMaxAttackDurationInitialised = true;
|
minMaxAttackDurationInitialised = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mTimerAttack < 0) mAttack = false;
|
if (timerAttack < 0) attack = false;
|
||||||
|
|
||||||
mTimerAttack -= duration;
|
timerAttack -= duration;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
mTimerAttack = -attacksPeriod;
|
timerAttack = -attacksPeriod;
|
||||||
mAttack = false;
|
attack = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
actorClass.getCreatureStats(actor).setAttackingOrSpell(mAttack);
|
actorClass.getCreatureStats(actor).setAttackingOrSpell(attack);
|
||||||
|
|
||||||
mActionCooldown -= duration;
|
|
||||||
|
|
||||||
|
float& actionCooldown = storage.mActionCooldown;
|
||||||
|
actionCooldown -= duration;
|
||||||
|
|
||||||
|
float& timerReact = storage.mTimerReact;
|
||||||
float tReaction = 0.25f;
|
float tReaction = 0.25f;
|
||||||
if(mTimerReact < tReaction)
|
if(timerReact < tReaction)
|
||||||
{
|
{
|
||||||
mTimerReact += duration;
|
timerReact += duration;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Update with period = tReaction
|
//Update with period = tReaction
|
||||||
|
|
||||||
mTimerReact = 0;
|
timerReact = 0;
|
||||||
|
const MWWorld::CellStore*& currentCell = storage.mCell;
|
||||||
bool cellChange = mCell && (actor.getCell() != mCell);
|
bool cellChange = currentCell && (actor.getCell() != currentCell);
|
||||||
if(!mCell || cellChange)
|
if(!currentCell || cellChange)
|
||||||
{
|
{
|
||||||
mCell = actor.getCell();
|
currentCell = actor.getCell();
|
||||||
}
|
}
|
||||||
|
|
||||||
MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(actor);
|
MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(actor);
|
||||||
|
@ -276,18 +314,19 @@ namespace MWMechanics
|
||||||
|
|
||||||
actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true);
|
actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true);
|
||||||
|
|
||||||
if (mActionCooldown > 0)
|
if (actionCooldown > 0)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
float rangeAttack = 0;
|
float rangeAttack = 0;
|
||||||
float rangeFollow = 0;
|
float rangeFollow = 0;
|
||||||
|
boost::shared_ptr<Action>& currentAction = storage.mCurrentAction;
|
||||||
if (anim->upperBodyReady())
|
if (anim->upperBodyReady())
|
||||||
{
|
{
|
||||||
mCurrentAction = prepareNextAction(actor, target);
|
currentAction = prepareNextAction(actor, target);
|
||||||
mActionCooldown = mCurrentAction->getActionCooldown();
|
actionCooldown = currentAction->getActionCooldown();
|
||||||
}
|
}
|
||||||
if (mCurrentAction.get())
|
if (currentAction.get())
|
||||||
mCurrentAction->getCombatRange(rangeAttack, rangeFollow);
|
currentAction->getCombatRange(rangeAttack, rangeFollow);
|
||||||
|
|
||||||
// FIXME: consider moving this stuff to ActionWeapon::getCombatRange
|
// FIXME: consider moving this stuff to ActionWeapon::getCombatRange
|
||||||
const ESM::Weapon *weapon = NULL;
|
const ESM::Weapon *weapon = NULL;
|
||||||
|
@ -346,21 +385,23 @@ namespace MWMechanics
|
||||||
weapRange = 150.f;
|
weapRange = 150.f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
float& strength = storage.mStrength;
|
||||||
// start new attack
|
// start new attack
|
||||||
if(mReadyToAttack)
|
if(readyToAttack)
|
||||||
{
|
{
|
||||||
if(mTimerAttack <= -attacksPeriod)
|
if(timerAttack <= -attacksPeriod)
|
||||||
{
|
{
|
||||||
mAttack = true; // attack starts just now
|
attack = true; // attack starts just now
|
||||||
|
|
||||||
if (!distantCombat) attackType = chooseBestAttack(weapon, mMovement);
|
if (!distantCombat) attackType = chooseBestAttack(weapon, movement);
|
||||||
else attackType = ESM::Weapon::AT_Chop; // cause it's =0
|
else attackType = ESM::Weapon::AT_Chop; // cause it's =0
|
||||||
|
|
||||||
mStrength = static_cast<float>(rand()) / RAND_MAX;
|
strength = static_cast<float>(rand()) / RAND_MAX;
|
||||||
|
|
||||||
// Note: may be 0 for some animations
|
// Note: may be 0 for some animations
|
||||||
mTimerAttack = mMinMaxAttackDuration[attackType][0] +
|
timerAttack = minMaxAttackDuration[attackType][0] +
|
||||||
(mMinMaxAttackDuration[attackType][1] - mMinMaxAttackDuration[attackType][0]) * mStrength;
|
(minMaxAttackDuration[attackType][1] - minMaxAttackDuration[attackType][0]) * strength;
|
||||||
|
|
||||||
//say a provoking combat phrase
|
//say a provoking combat phrase
|
||||||
if (actor.getClass().isNpc())
|
if (actor.getClass().isNpc())
|
||||||
|
@ -411,13 +452,16 @@ namespace MWMechanics
|
||||||
Ogre::Vector3 vTargetPos(target.getRefData().getPosition().pos);
|
Ogre::Vector3 vTargetPos(target.getRefData().getPosition().pos);
|
||||||
Ogre::Vector3 vDirToTarget = vTargetPos - vActorPos;
|
Ogre::Vector3 vDirToTarget = vTargetPos - vActorPos;
|
||||||
float distToTarget = vDirToTarget.length();
|
float distToTarget = vDirToTarget.length();
|
||||||
|
|
||||||
|
Ogre::Vector3& lastActorPos = storage.mLastActorPos;
|
||||||
|
bool& followTarget = storage.mFollowTarget;
|
||||||
|
|
||||||
bool isStuck = false;
|
bool isStuck = false;
|
||||||
float speed = 0.0f;
|
float speed = 0.0f;
|
||||||
if(mMovement.mPosition[1] && (mLastActorPos - vActorPos).length() < (speed = actorClass.getSpeed(actor)) * tReaction / 2)
|
if(movement.mPosition[1] && (lastActorPos - vActorPos).length() < (speed = actorClass.getSpeed(actor)) * tReaction / 2)
|
||||||
isStuck = true;
|
isStuck = true;
|
||||||
|
|
||||||
mLastActorPos = vActorPos;
|
lastActorPos = vActorPos;
|
||||||
|
|
||||||
// check if actor can move along z-axis
|
// check if actor can move along z-axis
|
||||||
bool canMoveByZ = (actorClass.canSwim(actor) && world->isSwimming(actor))
|
bool canMoveByZ = (actorClass.canSwim(actor) && world->isSwimming(actor))
|
||||||
|
@ -427,7 +471,7 @@ namespace MWMechanics
|
||||||
bool inLOS = distantCombat ? world->getLOS(actor, target) : true;
|
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 && mFollowTarget && !isStuck)))
|
if(inLOS && (distToTarget < rangeAttack || (distToTarget <= rangeFollow && followTarget && !isStuck)))
|
||||||
{
|
{
|
||||||
//Melee and Close-up combat
|
//Melee and Close-up combat
|
||||||
|
|
||||||
|
@ -437,29 +481,30 @@ namespace MWMechanics
|
||||||
// note: in getZAngleToDir if we preserve dir.z then horizontal angle can be inaccurate
|
// note: in getZAngleToDir if we preserve dir.z then horizontal angle can be inaccurate
|
||||||
if (distantCombat)
|
if (distantCombat)
|
||||||
{
|
{
|
||||||
Ogre::Vector3 vAimDir = AimDirToMovingTarget(actor, target, mLastTargetPos, tReaction, weaptype, mStrength);
|
Ogre::Vector3& lastTargetPos = storage.mLastTargetPos;
|
||||||
mLastTargetPos = vTargetPos;
|
Ogre::Vector3 vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, tReaction, weaptype, strength);
|
||||||
mMovement.mRotation[0] = getXAngleToDir(vAimDir);
|
lastTargetPos = vTargetPos;
|
||||||
mMovement.mRotation[2] = getZAngleToDir(Ogre::Vector3(vAimDir.x, vAimDir.y, 0));
|
movement.mRotation[0] = getXAngleToDir(vAimDir);
|
||||||
|
movement.mRotation[2] = getZAngleToDir(vAimDir);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
mMovement.mRotation[0] = getXAngleToDir(vDirToTarget, distToTarget);
|
movement.mRotation[0] = getXAngleToDir(vDirToTarget, distToTarget);
|
||||||
mMovement.mRotation[2] = getZAngleToDir(Ogre::Vector3(vDirToTarget.x, vDirToTarget.y, 0));
|
movement.mRotation[2] = getZAngleToDir(vDirToTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
// (not quite attack dist while following)
|
// (not quite attack dist while following)
|
||||||
if (mFollowTarget && distToTarget > rangeAttack)
|
if (followTarget && distToTarget > rangeAttack)
|
||||||
{
|
{
|
||||||
//Close-up combat: just run up on target
|
//Close-up combat: just run up on target
|
||||||
mMovement.mPosition[1] = 1;
|
movement.mPosition[1] = 1;
|
||||||
}
|
}
|
||||||
else // (within attack dist)
|
else // (within attack dist)
|
||||||
{
|
{
|
||||||
if(mMovement.mPosition[0] || mMovement.mPosition[1])
|
if(movement.mPosition[0] || movement.mPosition[1])
|
||||||
{
|
{
|
||||||
mTimerCombatMove = 0.1f + 0.1f * static_cast<float>(rand())/RAND_MAX;
|
timerCombatMove = 0.1f + 0.1f * static_cast<float>(rand())/RAND_MAX;
|
||||||
mCombatMove = true;
|
combatMove = true;
|
||||||
}
|
}
|
||||||
// only NPCs are smart enough to use dodge movements
|
// only NPCs are smart enough to use dodge movements
|
||||||
else if(actorClass.isNpc() && (!distantCombat || (distantCombat && distToTarget < rangeAttack/2)))
|
else if(actorClass.isNpc() && (!distantCombat || (distantCombat && distToTarget < rangeAttack/2)))
|
||||||
|
@ -467,20 +512,20 @@ namespace MWMechanics
|
||||||
//apply sideway movement (kind of dodging) with some probability
|
//apply sideway movement (kind of dodging) with some probability
|
||||||
if(static_cast<float>(rand())/RAND_MAX < 0.25)
|
if(static_cast<float>(rand())/RAND_MAX < 0.25)
|
||||||
{
|
{
|
||||||
mMovement.mPosition[0] = static_cast<float>(rand())/RAND_MAX < 0.5? 1: -1;
|
movement.mPosition[0] = static_cast<float>(rand())/RAND_MAX < 0.5? 1: -1;
|
||||||
mTimerCombatMove = 0.05f + 0.15f * static_cast<float>(rand())/RAND_MAX;
|
timerCombatMove = 0.05f + 0.15f * static_cast<float>(rand())/RAND_MAX;
|
||||||
mCombatMove = true;
|
combatMove = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(distantCombat && distToTarget < rangeAttack/4)
|
if(distantCombat && distToTarget < rangeAttack/4)
|
||||||
{
|
{
|
||||||
mMovement.mPosition[1] = -1;
|
movement.mPosition[1] = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
mReadyToAttack = 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
|
||||||
mFollowTarget = true;
|
followTarget = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else // remote pathfinding
|
else // remote pathfinding
|
||||||
|
@ -489,8 +534,11 @@ namespace MWMechanics
|
||||||
if (!distantCombat) inLOS = world->getLOS(actor, target);
|
if (!distantCombat) inLOS = world->getLOS(actor, target);
|
||||||
|
|
||||||
// check if shortcut is available
|
// check if shortcut is available
|
||||||
if(inLOS && (!isStuck || mReadyToAttack)
|
bool& forceNoShortcut = storage.mForceNoShortcut;
|
||||||
&& (!mForceNoShortcut || (Ogre::Vector3(mShortcutFailPos.pos) - vActorPos).length() >= PATHFIND_SHORTCUT_RETRY_DIST))
|
ESM::Position& shortcutFailPos = storage.mShortcutFailPos;
|
||||||
|
|
||||||
|
if(inLOS && (!isStuck || readyToAttack)
|
||||||
|
&& (!forceNoShortcut || (Ogre::Vector3(shortcutFailPos.pos) - vActorPos).length() >= PATHFIND_SHORTCUT_RETRY_DIST))
|
||||||
{
|
{
|
||||||
if(speed == 0.0f) speed = actorClass.getSpeed(actor);
|
if(speed == 0.0f) speed = actorClass.getSpeed(actor);
|
||||||
// maximum dist before pit/obstacle for actor to avoid them depending on his speed
|
// maximum dist before pit/obstacle for actor to avoid them depending on his speed
|
||||||
|
@ -504,21 +552,21 @@ namespace MWMechanics
|
||||||
if(preferShortcut)
|
if(preferShortcut)
|
||||||
{
|
{
|
||||||
if (canMoveByZ)
|
if (canMoveByZ)
|
||||||
mMovement.mRotation[0] = getXAngleToDir(vDirToTarget, distToTarget);
|
movement.mRotation[0] = getXAngleToDir(vDirToTarget, distToTarget);
|
||||||
mMovement.mRotation[2] = getZAngleToDir(vDirToTarget, distToTarget);
|
movement.mRotation[2] = getZAngleToDir(vDirToTarget);
|
||||||
mForceNoShortcut = false;
|
forceNoShortcut = false;
|
||||||
mShortcutFailPos.pos[0] = mShortcutFailPos.pos[1] = mShortcutFailPos.pos[2] = 0;
|
shortcutFailPos.pos[0] = shortcutFailPos.pos[1] = shortcutFailPos.pos[2] = 0;
|
||||||
mPathFinder.clearPath();
|
mPathFinder.clearPath();
|
||||||
}
|
}
|
||||||
else // if shortcut failed stick to path grid
|
else // if shortcut failed stick to path grid
|
||||||
{
|
{
|
||||||
if(!isStuck && mShortcutFailPos.pos[0] == 0.0f && mShortcutFailPos.pos[1] == 0.0f && mShortcutFailPos.pos[2] == 0.0f)
|
if(!isStuck && shortcutFailPos.pos[0] == 0.0f && shortcutFailPos.pos[1] == 0.0f && shortcutFailPos.pos[2] == 0.0f)
|
||||||
{
|
{
|
||||||
mForceNoShortcut = true;
|
forceNoShortcut = true;
|
||||||
mShortcutFailPos = pos;
|
shortcutFailPos = pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
mFollowTarget = false;
|
followTarget = false;
|
||||||
|
|
||||||
buildNewPath(actor, target); //may fail to build a path, check before use
|
buildNewPath(actor, target); //may fail to build a path, check before use
|
||||||
|
|
||||||
|
@ -536,7 +584,7 @@ namespace MWMechanics
|
||||||
// if current actor pos is closer to target then last point of path (excluding target itself) then go straight on target
|
// if current actor pos is closer to target then last point of path (excluding target itself) then go straight on target
|
||||||
if(distToTarget <= (vTargetPos - vBeforeTarget).length())
|
if(distToTarget <= (vTargetPos - vBeforeTarget).length())
|
||||||
{
|
{
|
||||||
mMovement.mRotation[2] = getZAngleToDir(vDirToTarget, distToTarget);
|
movement.mRotation[2] = getZAngleToDir(vDirToTarget);
|
||||||
preferShortcut = true;
|
preferShortcut = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -545,20 +593,20 @@ namespace MWMechanics
|
||||||
if(!preferShortcut)
|
if(!preferShortcut)
|
||||||
{
|
{
|
||||||
if(!mPathFinder.getPath().empty())
|
if(!mPathFinder.getPath().empty())
|
||||||
mMovement.mRotation[2] = mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]);
|
movement.mRotation[2] = mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]);
|
||||||
else
|
else
|
||||||
mMovement.mRotation[2] = getZAngleToDir(vDirToTarget, distToTarget);
|
movement.mRotation[2] = getZAngleToDir(vDirToTarget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mMovement.mPosition[1] = 1;
|
movement.mPosition[1] = 1;
|
||||||
if (mReadyToAttack)
|
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
|
||||||
mCombatMove = true;
|
combatMove = true;
|
||||||
mTimerCombatMove = 0;
|
timerCombatMove = 0;
|
||||||
}
|
}
|
||||||
mReadyToAttack = false;
|
readyToAttack = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!isStuck && distToTarget > rangeAttack && !distantCombat)
|
if(!isStuck && distToTarget > rangeAttack && !distantCombat)
|
||||||
|
@ -579,16 +627,16 @@ namespace MWMechanics
|
||||||
float t = s1/speed1;
|
float t = s1/speed1;
|
||||||
float s2 = speed2 * t;
|
float s2 = speed2 * t;
|
||||||
float t_swing =
|
float t_swing =
|
||||||
mMinMaxAttackDuration[ESM::Weapon::AT_Thrust][0] +
|
minMaxAttackDuration[ESM::Weapon::AT_Thrust][0] +
|
||||||
(mMinMaxAttackDuration[ESM::Weapon::AT_Thrust][1] - mMinMaxAttackDuration[ESM::Weapon::AT_Thrust][0]) * static_cast<float>(rand()) / RAND_MAX;
|
(minMaxAttackDuration[ESM::Weapon::AT_Thrust][1] - minMaxAttackDuration[ESM::Weapon::AT_Thrust][0]) * static_cast<float>(rand()) / RAND_MAX;
|
||||||
|
|
||||||
if (t + s2/speed1 <= t_swing)
|
if (t + s2/speed1 <= t_swing)
|
||||||
{
|
{
|
||||||
mReadyToAttack = true;
|
readyToAttack = true;
|
||||||
if(mTimerAttack <= -attacksPeriod)
|
if(timerAttack <= -attacksPeriod)
|
||||||
{
|
{
|
||||||
mTimerAttack = t_swing;
|
timerAttack = t_swing;
|
||||||
mAttack = true;
|
attack = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -598,7 +646,7 @@ namespace MWMechanics
|
||||||
// coded at 250ms or 1/4 second
|
// coded at 250ms or 1/4 second
|
||||||
//
|
//
|
||||||
// TODO: Add a parameter to vary DURATION_SAME_SPOT?
|
// TODO: Add a parameter to vary DURATION_SAME_SPOT?
|
||||||
if((distToTarget > rangeAttack || mFollowTarget) &&
|
if((distToTarget > rangeAttack || followTarget) &&
|
||||||
mObstacleCheck.check(actor, tReaction)) // check if evasive action needed
|
mObstacleCheck.check(actor, tReaction)) // check if evasive action needed
|
||||||
{
|
{
|
||||||
// probably walking into another NPC TODO: untested in combat situation
|
// probably walking into another NPC TODO: untested in combat situation
|
||||||
|
@ -610,8 +658,8 @@ namespace MWMechanics
|
||||||
if(mPathFinder.isPathConstructed())
|
if(mPathFinder.isPathConstructed())
|
||||||
zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0] + 1, pos.pos[1])));
|
zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0] + 1, pos.pos[1])));
|
||||||
|
|
||||||
if(mFollowTarget)
|
if(followTarget)
|
||||||
mFollowTarget = false;
|
followTarget = false;
|
||||||
// FIXME: can fool actors to stay behind doors, etc.
|
// FIXME: can fool actors to stay behind doors, etc.
|
||||||
// Related to Bug#1102 and to some degree #1155 as well
|
// Related to Bug#1102 and to some degree #1155 as well
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ namespace MWMechanics
|
||||||
|
|
||||||
virtual AiCombat *clone() const;
|
virtual AiCombat *clone() const;
|
||||||
|
|
||||||
virtual bool execute (const MWWorld::Ptr& actor,float duration);
|
virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration);
|
||||||
|
|
||||||
virtual int getTypeId() const;
|
virtual int getTypeId() const;
|
||||||
|
|
||||||
|
@ -54,40 +54,14 @@ namespace MWMechanics
|
||||||
virtual void writeState(ESM::AiSequence::AiSequence &sequence) const;
|
virtual void writeState(ESM::AiSequence::AiSequence &sequence) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
PathFinder mPathFinder;
|
|
||||||
// controls duration of the actual strike
|
|
||||||
float mTimerAttack;
|
|
||||||
float mTimerReact;
|
|
||||||
// controls duration of the sideway & forward moves
|
|
||||||
// when mCombatMove is true
|
|
||||||
float mTimerCombatMove;
|
|
||||||
|
|
||||||
// AiCombat states
|
|
||||||
bool mReadyToAttack, mAttack;
|
|
||||||
bool mFollowTarget;
|
|
||||||
bool mCombatMove;
|
|
||||||
|
|
||||||
float mStrength; // this is actually make sense only in ranged combat
|
|
||||||
float mMinMaxAttackDuration[3][2]; // slash, thrust, chop has different durations
|
|
||||||
bool mMinMaxAttackDurationInitialised;
|
|
||||||
|
|
||||||
bool mForceNoShortcut;
|
|
||||||
ESM::Position mShortcutFailPos;
|
|
||||||
|
|
||||||
Ogre::Vector3 mLastActorPos;
|
|
||||||
MWMechanics::Movement mMovement;
|
|
||||||
|
|
||||||
int mTargetActorId;
|
int mTargetActorId;
|
||||||
Ogre::Vector3 mLastTargetPos;
|
|
||||||
|
|
||||||
const MWWorld::CellStore* mCell;
|
|
||||||
ObstacleCheck mObstacleCheck;
|
|
||||||
|
|
||||||
boost::shared_ptr<Action> mCurrentAction;
|
|
||||||
float mActionCooldown;
|
|
||||||
|
|
||||||
void buildNewPath(const MWWorld::Ptr& actor, const MWWorld::Ptr& target);
|
void buildNewPath(const MWWorld::Ptr& actor, const MWWorld::Ptr& target);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -64,7 +64,7 @@ namespace MWMechanics
|
||||||
return new AiEscort(*this);
|
return new AiEscort(*this);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AiEscort::execute (const MWWorld::Ptr& actor,float duration)
|
bool AiEscort::execute (const MWWorld::Ptr& actor, AiState& state, float duration)
|
||||||
{
|
{
|
||||||
// If AiEscort has ran for as long or longer then the duration specified
|
// If AiEscort has ran for as long or longer then the duration specified
|
||||||
// and the duration is not infinite, the package is complete.
|
// and the duration is not infinite, the package is complete.
|
||||||
|
|
|
@ -33,7 +33,7 @@ namespace MWMechanics
|
||||||
|
|
||||||
virtual AiEscort *clone() const;
|
virtual AiEscort *clone() const;
|
||||||
|
|
||||||
virtual bool execute (const MWWorld::Ptr& actor,float duration);
|
virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration);
|
||||||
|
|
||||||
virtual int getTypeId() const;
|
virtual int getTypeId() const;
|
||||||
|
|
||||||
|
@ -48,7 +48,6 @@ namespace MWMechanics
|
||||||
float mMaxDist;
|
float mMaxDist;
|
||||||
float mRemainingDuration; // In seconds
|
float mRemainingDuration; // In seconds
|
||||||
|
|
||||||
PathFinder mPathFinder;
|
|
||||||
int mCellX;
|
int mCellX;
|
||||||
int mCellY;
|
int mCellY;
|
||||||
};
|
};
|
||||||
|
|
|
@ -41,7 +41,7 @@ MWMechanics::AiFollow::AiFollow(const ESM::AiSequence::AiFollow *follow)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MWMechanics::AiFollow::execute (const MWWorld::Ptr& actor,float duration)
|
bool MWMechanics::AiFollow::execute (const MWWorld::Ptr& actor, AiState& state, float duration)
|
||||||
{
|
{
|
||||||
MWWorld::Ptr target = getTarget();
|
MWWorld::Ptr target = getTarget();
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ namespace MWMechanics
|
||||||
|
|
||||||
virtual AiFollow *clone() const;
|
virtual AiFollow *clone() const;
|
||||||
|
|
||||||
virtual bool execute (const MWWorld::Ptr& actor,float duration);
|
virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration);
|
||||||
|
|
||||||
virtual int getTypeId() const;
|
virtual int getTypeId() const;
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
#include "../mwbase/world.hpp"
|
#include "../mwbase/world.hpp"
|
||||||
|
|
||||||
#include "obstacle.hpp"
|
#include "obstacle.hpp"
|
||||||
|
#include "aistate.hpp"
|
||||||
|
|
||||||
namespace MWWorld
|
namespace MWWorld
|
||||||
{
|
{
|
||||||
|
@ -20,8 +21,10 @@ namespace ESM
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
namespace MWMechanics
|
namespace MWMechanics
|
||||||
{
|
{
|
||||||
|
|
||||||
/// \brief Base class for AI packages
|
/// \brief Base class for AI packages
|
||||||
class AiPackage
|
class AiPackage
|
||||||
{
|
{
|
||||||
|
@ -50,7 +53,7 @@ namespace MWMechanics
|
||||||
|
|
||||||
/// Updates and runs the package (Should run every frame)
|
/// Updates and runs the package (Should run every frame)
|
||||||
/// \return Package completed?
|
/// \return Package completed?
|
||||||
virtual bool execute (const MWWorld::Ptr& actor,float duration) = 0;
|
virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration) = 0;
|
||||||
|
|
||||||
/// Returns the TypeID of the AiPackage
|
/// Returns the TypeID of the AiPackage
|
||||||
/// \see enum TypeId
|
/// \see enum TypeId
|
||||||
|
|
|
@ -30,7 +30,7 @@ AiPursue *MWMechanics::AiPursue::clone() const
|
||||||
{
|
{
|
||||||
return new AiPursue(*this);
|
return new AiPursue(*this);
|
||||||
}
|
}
|
||||||
bool AiPursue::execute (const MWWorld::Ptr& actor, float duration)
|
bool AiPursue::execute (const MWWorld::Ptr& actor, AiState& state, float duration)
|
||||||
{
|
{
|
||||||
if(actor.getClass().getCreatureStats(actor).isDead())
|
if(actor.getClass().getCreatureStats(actor).isDead())
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -31,7 +31,7 @@ namespace MWMechanics
|
||||||
AiPursue(const ESM::AiSequence::AiPursue* pursue);
|
AiPursue(const ESM::AiSequence::AiPursue* pursue);
|
||||||
|
|
||||||
virtual AiPursue *clone() const;
|
virtual AiPursue *clone() const;
|
||||||
virtual bool execute (const MWWorld::Ptr& actor,float duration);
|
virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration);
|
||||||
virtual int getTypeId() const;
|
virtual int getTypeId() const;
|
||||||
|
|
||||||
MWWorld::Ptr getTarget() const;
|
MWWorld::Ptr getTarget() const;
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
#include "aisequence.hpp"
|
#include "aisequence.hpp"
|
||||||
|
|
||||||
#include "aipackage.hpp"
|
#include "aipackage.hpp"
|
||||||
|
#include "aistate.hpp"
|
||||||
|
|
||||||
#include "aiwander.hpp"
|
#include "aiwander.hpp"
|
||||||
#include "aiescort.hpp"
|
#include "aiescort.hpp"
|
||||||
|
@ -146,7 +147,7 @@ bool AiSequence::isPackageDone() const
|
||||||
return mDone;
|
return mDone;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AiSequence::execute (const MWWorld::Ptr& actor,float duration)
|
void AiSequence::execute (const MWWorld::Ptr& actor, AiState& state,float duration)
|
||||||
{
|
{
|
||||||
if(actor != MWBase::Environment::get().getWorld()->getPlayerPtr()
|
if(actor != MWBase::Environment::get().getWorld()->getPlayerPtr()
|
||||||
&& !actor.getClass().getCreatureStats(actor).getKnockedDown())
|
&& !actor.getClass().getCreatureStats(actor).getKnockedDown())
|
||||||
|
@ -208,7 +209,7 @@ void AiSequence::execute (const MWWorld::Ptr& actor,float duration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (package->execute (actor,duration))
|
if (package->execute (actor,state,duration))
|
||||||
{
|
{
|
||||||
// To account for the rare case where AiPackage::execute() queued another AI package
|
// To account for the rare case where AiPackage::execute() queued another AI package
|
||||||
// (e.g. AiPursue executing a dialogue script that uses startCombat)
|
// (e.g. AiPursue executing a dialogue script that uses startCombat)
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
#include <list>
|
#include <list>
|
||||||
|
|
||||||
#include <components/esm/loadnpc.hpp>
|
#include <components/esm/loadnpc.hpp>
|
||||||
|
//#include "aistate.hpp"
|
||||||
|
|
||||||
namespace MWWorld
|
namespace MWWorld
|
||||||
{
|
{
|
||||||
|
@ -18,9 +19,15 @@ namespace ESM
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
namespace MWMechanics
|
namespace MWMechanics
|
||||||
{
|
{
|
||||||
class AiPackage;
|
class AiPackage;
|
||||||
|
|
||||||
|
template< class Base > class DerivedClassStorage;
|
||||||
|
class AiTemporaryBase;
|
||||||
|
typedef DerivedClassStorage<AiTemporaryBase> AiState;
|
||||||
|
|
||||||
/// \brief Sequence of AI-packages for a single actor
|
/// \brief Sequence of AI-packages for a single actor
|
||||||
/** The top-most AI package is run each frame. When completed, it is removed from the stack. **/
|
/** The top-most AI package is run each frame. When completed, it is removed from the stack. **/
|
||||||
|
@ -88,7 +95,7 @@ namespace MWMechanics
|
||||||
void stopPursuit();
|
void stopPursuit();
|
||||||
|
|
||||||
/// Execute current package, switching if needed.
|
/// Execute current package, switching if needed.
|
||||||
void execute (const MWWorld::Ptr& actor,float duration);
|
void execute (const MWWorld::Ptr& actor, MWMechanics::AiState& state, float duration);
|
||||||
|
|
||||||
/// Remove all packages.
|
/// Remove all packages.
|
||||||
void clear();
|
void clear();
|
||||||
|
|
136
apps/openmw/mwmechanics/aistate.hpp
Normal file
136
apps/openmw/mwmechanics/aistate.hpp
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
#ifndef AISTATE_H
|
||||||
|
#define AISTATE_H
|
||||||
|
|
||||||
|
#include <typeinfo>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
// c++11 replacement
|
||||||
|
#include <boost/static_assert.hpp>
|
||||||
|
#include <boost/type_traits/is_base_of.hpp>
|
||||||
|
|
||||||
|
namespace MWMechanics
|
||||||
|
{
|
||||||
|
|
||||||
|
/** \brief stores one object of any class derived from Base.
|
||||||
|
* Requesting a certain dereived class via get() either returns
|
||||||
|
* the stored object if it has the correct type or otherwise replaces
|
||||||
|
* it with an object of the requested type.
|
||||||
|
*/
|
||||||
|
template< class Base >
|
||||||
|
class DerivedClassStorage
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
Base* mStorage;
|
||||||
|
|
||||||
|
// assert that Derived is derived from Base.
|
||||||
|
template< class Derived >
|
||||||
|
void assert_derived()
|
||||||
|
{
|
||||||
|
// c++11:
|
||||||
|
// static_assert( std::is_base_of<Base,Derived> , "DerivedClassStorage may only store derived classes" );
|
||||||
|
|
||||||
|
// boost:
|
||||||
|
BOOST_STATIC_ASSERT((boost::is_base_of<Base,Derived>::value));//,"DerivedClassStorage may only store derived classes");
|
||||||
|
}
|
||||||
|
|
||||||
|
//if needed you have to provide a clone member function
|
||||||
|
DerivedClassStorage( const DerivedClassStorage& other );
|
||||||
|
DerivedClassStorage& operator=( const DerivedClassStorage& );
|
||||||
|
|
||||||
|
public:
|
||||||
|
/// \brief returns reference to stored object or deletes it and creates a fitting
|
||||||
|
template< class Derived >
|
||||||
|
Derived& get()
|
||||||
|
{
|
||||||
|
assert_derived<Derived>();
|
||||||
|
|
||||||
|
Derived* result = dynamic_cast<Derived*>(mStorage);
|
||||||
|
|
||||||
|
if(!result)
|
||||||
|
{
|
||||||
|
if(mStorage)
|
||||||
|
delete mStorage;
|
||||||
|
mStorage = result = new Derived();
|
||||||
|
}
|
||||||
|
|
||||||
|
//return a reference to the (new allocated) object
|
||||||
|
return *result;
|
||||||
|
}
|
||||||
|
|
||||||
|
template< class Derived >
|
||||||
|
void store( const Derived& payload )
|
||||||
|
{
|
||||||
|
assert_derived<Derived>();
|
||||||
|
if(mStorage)
|
||||||
|
delete mStorage;
|
||||||
|
mStorage = new Derived(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// \brief takes ownership of the passed object
|
||||||
|
template< class Derived >
|
||||||
|
void moveIn( Derived* p )
|
||||||
|
{
|
||||||
|
assert_derived<Derived>();
|
||||||
|
if(mStorage)
|
||||||
|
delete mStorage;
|
||||||
|
mStorage = p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// \brief gives away ownership of object. Throws exception if storage does not contain Derived or is empty.
|
||||||
|
template< class Derived >
|
||||||
|
Derived* moveOut()
|
||||||
|
{
|
||||||
|
assert_derived<Derived>();
|
||||||
|
|
||||||
|
|
||||||
|
if(!mStorage)
|
||||||
|
throw std::runtime_error("Cant move out: empty storage.");
|
||||||
|
|
||||||
|
Derived* result = dynamic_cast<Derived*>(mStorage);
|
||||||
|
|
||||||
|
if(!mStorage)
|
||||||
|
throw std::runtime_error("Cant move out: wrong type requested.");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool empty() const
|
||||||
|
{
|
||||||
|
return mStorage == NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::type_info& getType() const
|
||||||
|
{
|
||||||
|
return typeid(mStorage);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
DerivedClassStorage():mStorage(NULL){};
|
||||||
|
~DerivedClassStorage()
|
||||||
|
{
|
||||||
|
if(mStorage)
|
||||||
|
delete mStorage;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/// \brief base class for the temporary storage of AiPackages.
|
||||||
|
/**
|
||||||
|
* Each AI package with temporary values needs a AiPackageStorage class
|
||||||
|
* which is derived from AiTemporaryBase. The CharacterController holds a container
|
||||||
|
* AiState where one of these storages can be stored at a time.
|
||||||
|
* The execute(...) member function takes this container as an argument.
|
||||||
|
* */
|
||||||
|
struct AiTemporaryBase
|
||||||
|
{
|
||||||
|
virtual ~AiTemporaryBase(){};
|
||||||
|
};
|
||||||
|
|
||||||
|
/// \brief Container for AI package status.
|
||||||
|
typedef DerivedClassStorage<AiTemporaryBase> AiState;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // AISTATE_H
|
|
@ -17,7 +17,7 @@
|
||||||
namespace MWMechanics
|
namespace MWMechanics
|
||||||
{
|
{
|
||||||
AiTravel::AiTravel(float x, float y, float z)
|
AiTravel::AiTravel(float x, float y, float z)
|
||||||
: mX(x),mY(y),mZ(z),mPathFinder()
|
: mX(x),mY(y),mZ(z)
|
||||||
, mCellX(std::numeric_limits<int>::max())
|
, mCellX(std::numeric_limits<int>::max())
|
||||||
, mCellY(std::numeric_limits<int>::max())
|
, mCellY(std::numeric_limits<int>::max())
|
||||||
{
|
{
|
||||||
|
@ -25,7 +25,6 @@ namespace MWMechanics
|
||||||
|
|
||||||
AiTravel::AiTravel(const ESM::AiSequence::AiTravel *travel)
|
AiTravel::AiTravel(const ESM::AiSequence::AiTravel *travel)
|
||||||
: mX(travel->mData.mX), mY(travel->mData.mY), mZ(travel->mData.mZ)
|
: mX(travel->mData.mX), mY(travel->mData.mY), mZ(travel->mData.mZ)
|
||||||
, mPathFinder()
|
|
||||||
, mCellX(std::numeric_limits<int>::max())
|
, mCellX(std::numeric_limits<int>::max())
|
||||||
, mCellY(std::numeric_limits<int>::max())
|
, mCellY(std::numeric_limits<int>::max())
|
||||||
{
|
{
|
||||||
|
@ -37,7 +36,7 @@ namespace MWMechanics
|
||||||
return new AiTravel(*this);
|
return new AiTravel(*this);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AiTravel::execute (const MWWorld::Ptr& actor,float duration)
|
bool AiTravel::execute (const MWWorld::Ptr& actor, AiState& state, float duration)
|
||||||
{
|
{
|
||||||
MWBase::World *world = MWBase::Environment::get().getWorld();
|
MWBase::World *world = MWBase::Environment::get().getWorld();
|
||||||
ESM::Position pos = actor.getRefData().getPosition();
|
ESM::Position pos = actor.getRefData().getPosition();
|
||||||
|
|
|
@ -27,7 +27,7 @@ namespace MWMechanics
|
||||||
|
|
||||||
virtual AiTravel *clone() const;
|
virtual AiTravel *clone() const;
|
||||||
|
|
||||||
virtual bool execute (const MWWorld::Ptr& actor,float duration);
|
virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration);
|
||||||
|
|
||||||
virtual int getTypeId() const;
|
virtual int getTypeId() const;
|
||||||
|
|
||||||
|
@ -39,7 +39,6 @@ namespace MWMechanics
|
||||||
int mCellX;
|
int mCellX;
|
||||||
int mCellY;
|
int mCellY;
|
||||||
|
|
||||||
PathFinder mPathFinder;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@
|
||||||
#include "steering.hpp"
|
#include "steering.hpp"
|
||||||
#include "movement.hpp"
|
#include "movement.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
namespace MWMechanics
|
namespace MWMechanics
|
||||||
{
|
{
|
||||||
static const int COUNT_BEFORE_RESET = 200; // TODO: maybe no longer needed
|
static const int COUNT_BEFORE_RESET = 200; // TODO: maybe no longer needed
|
||||||
|
@ -26,6 +28,55 @@ namespace MWMechanics
|
||||||
static const int GREETING_SHOULD_START = 4; //how many reaction intervals should pass before NPC can greet player
|
static const int GREETING_SHOULD_START = 4; //how many reaction intervals should pass before NPC can greet player
|
||||||
static const int GREETING_SHOULD_END = 10;
|
static const int GREETING_SHOULD_END = 10;
|
||||||
|
|
||||||
|
/// \brief This class holds the variables AiWander needs which are deleted if the package becomes inactive.
|
||||||
|
struct AiWanderStorage : AiTemporaryBase
|
||||||
|
{
|
||||||
|
// the z rotation angle (degrees) we want to reach
|
||||||
|
// used every frame when mRotate is true
|
||||||
|
Ogre::Radian mTargetAngle;
|
||||||
|
bool mRotate;
|
||||||
|
float mReaction; // update some actions infrequently
|
||||||
|
|
||||||
|
|
||||||
|
AiWander::GreetingState mSaidGreeting;
|
||||||
|
int mGreetingTimer;
|
||||||
|
|
||||||
|
// Cached current cell location
|
||||||
|
int mCellX;
|
||||||
|
int mCellY;
|
||||||
|
// Cell location multiplied by ESM::Land::REAL_SIZE
|
||||||
|
float mXCell;
|
||||||
|
float mYCell;
|
||||||
|
|
||||||
|
const MWWorld::CellStore* mCell; // for detecting cell change
|
||||||
|
|
||||||
|
// AiWander states
|
||||||
|
bool mChooseAction;
|
||||||
|
bool mIdleNow;
|
||||||
|
bool mMoveNow;
|
||||||
|
bool mWalking;
|
||||||
|
|
||||||
|
unsigned short mPlayedIdle;
|
||||||
|
|
||||||
|
AiWanderStorage():
|
||||||
|
mTargetAngle(0),
|
||||||
|
mRotate(false),
|
||||||
|
mReaction(0),
|
||||||
|
mSaidGreeting(AiWander::Greet_None),
|
||||||
|
mGreetingTimer(0),
|
||||||
|
mCellX(std::numeric_limits<int>::max()),
|
||||||
|
mCellY(std::numeric_limits<int>::max()),
|
||||||
|
mXCell(0),
|
||||||
|
mYCell(0),
|
||||||
|
mCell(NULL),
|
||||||
|
mChooseAction(true),
|
||||||
|
mIdleNow(false),
|
||||||
|
mMoveNow(false),
|
||||||
|
mWalking(false),
|
||||||
|
mPlayedIdle(0)
|
||||||
|
{};
|
||||||
|
};
|
||||||
|
|
||||||
AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector<unsigned char>& idle, bool repeat):
|
AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector<unsigned char>& idle, bool repeat):
|
||||||
mDistance(distance), mDuration(duration), mTimeOfDay(timeOfDay), mIdle(idle), mRepeat(repeat)
|
mDistance(distance), mDuration(duration), mTimeOfDay(timeOfDay), mIdle(idle), mRepeat(repeat)
|
||||||
{
|
{
|
||||||
|
@ -37,19 +88,11 @@ namespace MWMechanics
|
||||||
{
|
{
|
||||||
// NOTE: mDistance and mDuration must be set already
|
// NOTE: mDistance and mDuration must be set already
|
||||||
|
|
||||||
mCellX = std::numeric_limits<int>::max();
|
|
||||||
mCellY = std::numeric_limits<int>::max();
|
|
||||||
mXCell = 0;
|
|
||||||
mYCell = 0;
|
|
||||||
mCell = NULL;
|
|
||||||
mStuckCount = 0;// TODO: maybe no longer needed
|
mStuckCount = 0;// TODO: maybe no longer needed
|
||||||
mDoorCheckDuration = 0;
|
mDoorCheckDuration = 0;
|
||||||
mTrimCurrentNode = false;
|
mTrimCurrentNode = false;
|
||||||
mReaction = 0;
|
|
||||||
mRotate = false;
|
|
||||||
mTargetAngle = 0;
|
|
||||||
mSaidGreeting = Greet_None;
|
|
||||||
mGreetingTimer = 0;
|
|
||||||
mHasReturnPosition = false;
|
mHasReturnPosition = false;
|
||||||
mReturnPosition = Ogre::Vector3(0,0,0);
|
mReturnPosition = Ogre::Vector3(0,0,0);
|
||||||
|
|
||||||
|
@ -61,13 +104,9 @@ namespace MWMechanics
|
||||||
mTimeOfDay = 0;
|
mTimeOfDay = 0;
|
||||||
|
|
||||||
mStartTime = MWBase::Environment::get().getWorld()->getTimeStamp();
|
mStartTime = MWBase::Environment::get().getWorld()->getTimeStamp();
|
||||||
mPlayedIdle = 0;
|
|
||||||
|
|
||||||
mStoredAvailableNodes = false;
|
mStoredAvailableNodes = false;
|
||||||
mChooseAction = true;
|
|
||||||
mIdleNow = false;
|
|
||||||
mMoveNow = false;
|
|
||||||
mWalking = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AiPackage * MWMechanics::AiWander::clone() const
|
AiPackage * MWMechanics::AiWander::clone() const
|
||||||
|
@ -125,53 +164,65 @@ namespace MWMechanics
|
||||||
* actors will enter combat (i.e. no longer wandering) and different pathfinding
|
* actors will enter combat (i.e. no longer wandering) and different pathfinding
|
||||||
* will kick in.
|
* will kick in.
|
||||||
*/
|
*/
|
||||||
bool AiWander::execute (const MWWorld::Ptr& actor,float duration)
|
bool AiWander::execute (const MWWorld::Ptr& actor, AiState& state, float duration)
|
||||||
{
|
{
|
||||||
|
// get or create temporary storage
|
||||||
|
AiWanderStorage& storage = state.get<AiWanderStorage>();
|
||||||
|
|
||||||
|
|
||||||
|
const MWWorld::CellStore*& currentCell = storage.mCell;
|
||||||
MWMechanics::CreatureStats& cStats = actor.getClass().getCreatureStats(actor);
|
MWMechanics::CreatureStats& cStats = actor.getClass().getCreatureStats(actor);
|
||||||
if(cStats.isDead() || cStats.getHealth().getCurrent() <= 0)
|
if(cStats.isDead() || cStats.getHealth().getCurrent() <= 0)
|
||||||
return true; // Don't bother with dead actors
|
return true; // Don't bother with dead actors
|
||||||
|
|
||||||
bool cellChange = mCell && (actor.getCell() != mCell);
|
bool cellChange = currentCell && (actor.getCell() != currentCell);
|
||||||
if(!mCell || cellChange)
|
if(!currentCell || cellChange)
|
||||||
{
|
{
|
||||||
mCell = actor.getCell();
|
currentCell = actor.getCell();
|
||||||
mStoredAvailableNodes = false; // prob. not needed since mDistance = 0
|
mStoredAvailableNodes = false; // prob. not needed since mDistance = 0
|
||||||
}
|
}
|
||||||
const ESM::Cell *cell = mCell->getCell();
|
const ESM::Cell *cell = currentCell->getCell();
|
||||||
|
|
||||||
cStats.setDrawState(DrawState_Nothing);
|
cStats.setDrawState(DrawState_Nothing);
|
||||||
cStats.setMovementFlag(CreatureStats::Flag_Run, false);
|
cStats.setMovementFlag(CreatureStats::Flag_Run, false);
|
||||||
|
|
||||||
ESM::Position pos = actor.getRefData().getPosition();
|
ESM::Position pos = actor.getRefData().getPosition();
|
||||||
|
|
||||||
|
|
||||||
|
bool& idleNow = storage.mIdleNow;
|
||||||
|
bool& moveNow = storage.mMoveNow;
|
||||||
|
bool& walking = storage.mWalking;
|
||||||
// Check if an idle actor is too close to a door - if so start walking
|
// Check if an idle actor is too close to a door - if so start walking
|
||||||
mDoorCheckDuration += duration;
|
mDoorCheckDuration += duration;
|
||||||
if(mDoorCheckDuration >= DOOR_CHECK_INTERVAL)
|
if(mDoorCheckDuration >= DOOR_CHECK_INTERVAL)
|
||||||
{
|
{
|
||||||
mDoorCheckDuration = 0; // restart timer
|
mDoorCheckDuration = 0; // restart timer
|
||||||
if(mDistance && // actor is not intended to be stationary
|
if(mDistance && // actor is not intended to be stationary
|
||||||
mIdleNow && // but is in idle
|
idleNow && // but is in idle
|
||||||
!mWalking && // FIXME: some actors are idle while walking
|
!walking && // FIXME: some actors are idle while walking
|
||||||
proximityToDoor(actor, MIN_DIST_TO_DOOR_SQUARED*1.6*1.6)) // NOTE: checks interior cells only
|
proximityToDoor(actor, MIN_DIST_TO_DOOR_SQUARED*1.6*1.6)) // NOTE: checks interior cells only
|
||||||
{
|
{
|
||||||
mIdleNow = false;
|
idleNow = false;
|
||||||
mMoveNow = true;
|
moveNow = true;
|
||||||
mTrimCurrentNode = false; // just in case
|
mTrimCurrentNode = false; // just in case
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Are we there yet?
|
// Are we there yet?
|
||||||
if(mWalking &&
|
bool& chooseAction = storage.mChooseAction;
|
||||||
|
if(walking &&
|
||||||
mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2]))
|
mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2]))
|
||||||
{
|
{
|
||||||
stopWalking(actor);
|
stopWalking(actor);
|
||||||
mMoveNow = false;
|
moveNow = false;
|
||||||
mWalking = false;
|
walking = false;
|
||||||
mChooseAction = true;
|
chooseAction = true;
|
||||||
mHasReturnPosition = false;
|
mHasReturnPosition = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(mWalking) // have not yet reached the destination
|
|
||||||
|
|
||||||
|
if(walking) // have not yet reached the destination
|
||||||
{
|
{
|
||||||
// turn towards the next point in mPath
|
// turn towards the next point in mPath
|
||||||
zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1])));
|
zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1])));
|
||||||
|
@ -188,8 +239,8 @@ namespace MWMechanics
|
||||||
trimAllowedNodes(mAllowedNodes, mPathFinder);
|
trimAllowedNodes(mAllowedNodes, mPathFinder);
|
||||||
mObstacleCheck.clear();
|
mObstacleCheck.clear();
|
||||||
mPathFinder.clearPath();
|
mPathFinder.clearPath();
|
||||||
mWalking = false;
|
walking = false;
|
||||||
mMoveNow = true;
|
moveNow = true;
|
||||||
}
|
}
|
||||||
else // probably walking into another NPC
|
else // probably walking into another NPC
|
||||||
{
|
{
|
||||||
|
@ -210,29 +261,33 @@ namespace MWMechanics
|
||||||
mObstacleCheck.clear();
|
mObstacleCheck.clear();
|
||||||
|
|
||||||
stopWalking(actor);
|
stopWalking(actor);
|
||||||
mMoveNow = false;
|
moveNow = false;
|
||||||
mWalking = false;
|
walking = false;
|
||||||
mChooseAction = true;
|
chooseAction = true;
|
||||||
}
|
}
|
||||||
//#endif
|
//#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mRotate)
|
|
||||||
|
Ogre::Radian& targetAngle = storage.mTargetAngle;
|
||||||
|
bool& rotate = storage.mRotate;
|
||||||
|
if (rotate)
|
||||||
{
|
{
|
||||||
// Reduce the turning animation glitch by using a *HUGE* value of
|
// Reduce the turning animation glitch by using a *HUGE* value of
|
||||||
// epsilon... TODO: a proper fix might be in either the physics or the
|
// epsilon... TODO: a proper fix might be in either the physics or the
|
||||||
// animation subsystem
|
// animation subsystem
|
||||||
if (zTurn(actor, Ogre::Degree(mTargetAngle), Ogre::Degree(5)))
|
if (zTurn(actor, targetAngle, Ogre::Degree(5)))
|
||||||
mRotate = false;
|
rotate = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
mReaction += duration;
|
float& lastReaction = storage.mReaction;
|
||||||
if(mReaction < REACTION_INTERVAL)
|
lastReaction += duration;
|
||||||
|
if(lastReaction < REACTION_INTERVAL)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
mReaction = 0;
|
lastReaction = 0;
|
||||||
|
|
||||||
// NOTE: everything below get updated every REACTION_INTERVAL seconds
|
// NOTE: everything below get updated every REACTION_INTERVAL seconds
|
||||||
|
|
||||||
|
@ -263,6 +318,12 @@ namespace MWMechanics
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
int& cachedCellX = storage.mCellX;
|
||||||
|
int& cachedCellY = storage.mCellY;
|
||||||
|
float& cachedCellXposition = storage.mXCell;
|
||||||
|
float& cachedCellYposition = storage.mYCell;
|
||||||
// Initialization to discover & store allowed node points for this actor.
|
// Initialization to discover & store allowed node points for this actor.
|
||||||
if(!mStoredAvailableNodes)
|
if(!mStoredAvailableNodes)
|
||||||
{
|
{
|
||||||
|
@ -271,8 +332,8 @@ namespace MWMechanics
|
||||||
pathgrid = world->getStore().get<ESM::Pathgrid>().search(*cell);
|
pathgrid = world->getStore().get<ESM::Pathgrid>().search(*cell);
|
||||||
|
|
||||||
// cache the current cell location
|
// cache the current cell location
|
||||||
mCellX = cell->mData.mX;
|
cachedCellX = cell->mData.mX;
|
||||||
mCellY = cell->mData.mY;
|
cachedCellY = cell->mData.mY;
|
||||||
|
|
||||||
// If there is no path this actor doesn't go anywhere. See:
|
// If there is no path this actor doesn't go anywhere. See:
|
||||||
// https://forum.openmw.org/viewtopic.php?t=1556
|
// https://forum.openmw.org/viewtopic.php?t=1556
|
||||||
|
@ -286,12 +347,12 @@ namespace MWMechanics
|
||||||
// destinations within the allowed set of pathgrid points (nodes).
|
// destinations within the allowed set of pathgrid points (nodes).
|
||||||
if(mDistance)
|
if(mDistance)
|
||||||
{
|
{
|
||||||
mXCell = 0;
|
cachedCellXposition = 0;
|
||||||
mYCell = 0;
|
cachedCellYposition = 0;
|
||||||
if(cell->isExterior())
|
if(cell->isExterior())
|
||||||
{
|
{
|
||||||
mXCell = mCellX * ESM::Land::REAL_SIZE;
|
cachedCellXposition = cachedCellX * ESM::Land::REAL_SIZE;
|
||||||
mYCell = mCellY * ESM::Land::REAL_SIZE;
|
cachedCellYposition = cachedCellY * ESM::Land::REAL_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: There might be a bug here. The allowed node points are
|
// FIXME: There might be a bug here. The allowed node points are
|
||||||
|
@ -301,8 +362,8 @@ namespace MWMechanics
|
||||||
//
|
//
|
||||||
// convert npcPos to local (i.e. cell) co-ordinates
|
// convert npcPos to local (i.e. cell) co-ordinates
|
||||||
Ogre::Vector3 npcPos(pos.pos);
|
Ogre::Vector3 npcPos(pos.pos);
|
||||||
npcPos[0] = npcPos[0] - mXCell;
|
npcPos[0] = npcPos[0] - cachedCellXposition;
|
||||||
npcPos[1] = npcPos[1] - mYCell;
|
npcPos[1] = npcPos[1] - cachedCellYposition;
|
||||||
|
|
||||||
// mAllowedNodes for this actor with pathgrid point indexes based on mDistance
|
// mAllowedNodes for this actor with pathgrid point indexes based on mDistance
|
||||||
// NOTE: mPoints and mAllowedNodes are in local co-ordinates
|
// NOTE: mPoints and mAllowedNodes are in local co-ordinates
|
||||||
|
@ -347,8 +408,8 @@ namespace MWMechanics
|
||||||
mHasReturnPosition = false;
|
mHasReturnPosition = false;
|
||||||
if (mDistance == 0 && mHasReturnPosition && Ogre::Vector3(pos.pos).squaredDistance(mReturnPosition) > 20*20)
|
if (mDistance == 0 && mHasReturnPosition && Ogre::Vector3(pos.pos).squaredDistance(mReturnPosition) > 20*20)
|
||||||
{
|
{
|
||||||
mChooseAction = false;
|
chooseAction = false;
|
||||||
mIdleNow = false;
|
idleNow = false;
|
||||||
|
|
||||||
if (!mPathFinder.isPathConstructed())
|
if (!mPathFinder.isPathConstructed())
|
||||||
{
|
{
|
||||||
|
@ -370,30 +431,32 @@ namespace MWMechanics
|
||||||
|
|
||||||
if(mPathFinder.isPathConstructed())
|
if(mPathFinder.isPathConstructed())
|
||||||
{
|
{
|
||||||
mMoveNow = false;
|
moveNow = false;
|
||||||
mWalking = true;
|
walking = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(mChooseAction)
|
AiWander::GreetingState& greetingState = storage.mSaidGreeting;
|
||||||
|
short unsigned& playedIdle = storage.mPlayedIdle;
|
||||||
|
if(chooseAction)
|
||||||
{
|
{
|
||||||
mPlayedIdle = 0;
|
playedIdle = 0;
|
||||||
getRandomIdle(); // NOTE: sets mPlayedIdle with a random selection
|
getRandomIdle(playedIdle); // NOTE: sets mPlayedIdle with a random selection
|
||||||
|
|
||||||
if(!mPlayedIdle && mDistance)
|
if(!playedIdle && mDistance)
|
||||||
{
|
{
|
||||||
mChooseAction = false;
|
chooseAction = false;
|
||||||
mMoveNow = true;
|
moveNow = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Play idle animation and recreate vanilla (broken?) behavior of resetting start time of AIWander:
|
// Play idle animation and recreate vanilla (broken?) behavior of resetting start time of AIWander:
|
||||||
MWWorld::TimeStamp currentTime = world->getTimeStamp();
|
MWWorld::TimeStamp currentTime = world->getTimeStamp();
|
||||||
mStartTime = currentTime;
|
mStartTime = currentTime;
|
||||||
playIdle(actor, mPlayedIdle);
|
playIdle(actor, playedIdle);
|
||||||
mChooseAction = false;
|
chooseAction = false;
|
||||||
mIdleNow = true;
|
idleNow = true;
|
||||||
|
|
||||||
// Play idle voiced dialogue entries randomly
|
// Play idle voiced dialogue entries randomly
|
||||||
int hello = cStats.getAiSetting(CreatureStats::AI_Hello).getModified();
|
int hello = cStats.getAiSetting(CreatureStats::AI_Hello).getModified();
|
||||||
|
@ -417,7 +480,7 @@ namespace MWMechanics
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow interrupting a walking actor to trigger a greeting
|
// Allow interrupting a walking actor to trigger a greeting
|
||||||
if(mIdleNow || mWalking)
|
if(idleNow || walking)
|
||||||
{
|
{
|
||||||
// Play a random voice greeting if the player gets too close
|
// Play a random voice greeting if the player gets too close
|
||||||
int hello = cStats.getAiSetting(CreatureStats::AI_Hello).getModified();
|
int hello = cStats.getAiSetting(CreatureStats::AI_Hello).getModified();
|
||||||
|
@ -432,77 +495,76 @@ namespace MWMechanics
|
||||||
Ogre::Vector3 actorPos(actor.getRefData().getPosition().pos);
|
Ogre::Vector3 actorPos(actor.getRefData().getPosition().pos);
|
||||||
float playerDistSqr = playerPos.squaredDistance(actorPos);
|
float playerDistSqr = playerPos.squaredDistance(actorPos);
|
||||||
|
|
||||||
if (mSaidGreeting == Greet_None)
|
int& greetingTimer = storage.mGreetingTimer;
|
||||||
|
if (greetingState == Greet_None)
|
||||||
{
|
{
|
||||||
if ((playerDistSqr <= helloDistance*helloDistance) &&
|
if ((playerDistSqr <= helloDistance*helloDistance) &&
|
||||||
!player.getClass().getCreatureStats(player).isDead() && MWBase::Environment::get().getWorld()->getLOS(player, actor)
|
!player.getClass().getCreatureStats(player).isDead() && MWBase::Environment::get().getWorld()->getLOS(player, actor)
|
||||||
&& MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, actor))
|
&& MWBase::Environment::get().getMechanicsManager()->awarenessCheck(player, actor))
|
||||||
mGreetingTimer++;
|
greetingTimer++;
|
||||||
|
|
||||||
if (mGreetingTimer >= GREETING_SHOULD_START)
|
if (greetingTimer >= GREETING_SHOULD_START)
|
||||||
{
|
{
|
||||||
mSaidGreeting = Greet_InProgress;
|
greetingState = Greet_InProgress;
|
||||||
MWBase::Environment::get().getDialogueManager()->say(actor, "hello");
|
MWBase::Environment::get().getDialogueManager()->say(actor, "hello");
|
||||||
mGreetingTimer = 0;
|
greetingTimer = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(mSaidGreeting == Greet_InProgress)
|
if(greetingState == Greet_InProgress)
|
||||||
{
|
{
|
||||||
mGreetingTimer++;
|
greetingTimer++;
|
||||||
|
|
||||||
if(mWalking)
|
if(walking)
|
||||||
{
|
{
|
||||||
stopWalking(actor);
|
stopWalking(actor);
|
||||||
mMoveNow = false;
|
moveNow = false;
|
||||||
mWalking = false;
|
walking = false;
|
||||||
mObstacleCheck.clear();
|
mObstacleCheck.clear();
|
||||||
mIdleNow = true;
|
idleNow = true;
|
||||||
getRandomIdle();
|
getRandomIdle(playedIdle);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!mRotate)
|
if(!rotate)
|
||||||
{
|
{
|
||||||
Ogre::Vector3 dir = playerPos - actorPos;
|
Ogre::Vector3 dir = playerPos - actorPos;
|
||||||
float length = dir.length();
|
|
||||||
|
|
||||||
float faceAngle = Ogre::Radian(Ogre::Math::ACos(dir.y / length) *
|
Ogre::Radian faceAngle = Ogre::Math::ATan2(dir.x,dir.y);
|
||||||
((Ogre::Math::ASin(dir.x / length).valueRadians()>0)?1.0:-1.0)).valueDegrees();
|
Ogre::Radian actorAngle = actor.getRefData().getBaseNode()->getOrientation().getRoll();
|
||||||
float actorAngle = actor.getRefData().getBaseNode()->getOrientation().getRoll().valueDegrees();
|
|
||||||
// an attempt at reducing the turning animation glitch
|
// an attempt at reducing the turning animation glitch
|
||||||
if(abs(abs(faceAngle) - abs(actorAngle)) >= 5) // TODO: is there a better way?
|
if( Ogre::Math::Abs( faceAngle - actorAngle ) >= Ogre::Degree(5) ) // TODO: is there a better way?
|
||||||
{
|
{
|
||||||
mTargetAngle = faceAngle;
|
targetAngle = faceAngle;
|
||||||
mRotate = true;
|
rotate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mGreetingTimer >= GREETING_SHOULD_END)
|
if (greetingTimer >= GREETING_SHOULD_END)
|
||||||
{
|
{
|
||||||
mSaidGreeting = Greet_Done;
|
greetingState = Greet_Done;
|
||||||
mGreetingTimer = 0;
|
greetingTimer = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mSaidGreeting == MWMechanics::AiWander::Greet_Done)
|
if (greetingState == MWMechanics::AiWander::Greet_Done)
|
||||||
{
|
{
|
||||||
static float fGreetDistanceReset = MWBase::Environment::get().getWorld()->getStore()
|
static float fGreetDistanceReset = MWBase::Environment::get().getWorld()->getStore()
|
||||||
.get<ESM::GameSetting>().find("fGreetDistanceReset")->getFloat();
|
.get<ESM::GameSetting>().find("fGreetDistanceReset")->getFloat();
|
||||||
|
|
||||||
if (playerDistSqr >= fGreetDistanceReset*fGreetDistanceReset)
|
if (playerDistSqr >= fGreetDistanceReset*fGreetDistanceReset)
|
||||||
mSaidGreeting = Greet_None;
|
greetingState = Greet_None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if idle animation finished
|
// Check if idle animation finished
|
||||||
if(!checkIdle(actor, mPlayedIdle) && (playerDistSqr > helloDistance*helloDistance || mSaidGreeting == MWMechanics::AiWander::Greet_Done))
|
if(!checkIdle(actor, playedIdle) && (playerDistSqr > helloDistance*helloDistance || greetingState == MWMechanics::AiWander::Greet_Done))
|
||||||
{
|
{
|
||||||
mPlayedIdle = 0;
|
playedIdle = 0;
|
||||||
mIdleNow = false;
|
idleNow = false;
|
||||||
mChooseAction = true;
|
chooseAction = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(mMoveNow && mDistance)
|
if(moveNow && mDistance)
|
||||||
{
|
{
|
||||||
// Construct a new path if there isn't one
|
// Construct a new path if there isn't one
|
||||||
if(!mPathFinder.isPathConstructed())
|
if(!mPathFinder.isPathConstructed())
|
||||||
|
@ -516,8 +578,8 @@ namespace MWMechanics
|
||||||
|
|
||||||
// convert dest to use world co-ordinates
|
// convert dest to use world co-ordinates
|
||||||
ESM::Pathgrid::Point dest;
|
ESM::Pathgrid::Point dest;
|
||||||
dest.mX = destNodePos[0] + mXCell;
|
dest.mX = destNodePos[0] + cachedCellXposition;
|
||||||
dest.mY = destNodePos[1] + mYCell;
|
dest.mY = destNodePos[1] + cachedCellYposition;
|
||||||
dest.mZ = destNodePos[2];
|
dest.mZ = destNodePos[2];
|
||||||
|
|
||||||
// actor position is already in world co-ordinates
|
// actor position is already in world co-ordinates
|
||||||
|
@ -548,8 +610,8 @@ namespace MWMechanics
|
||||||
mAllowedNodes.push_back(mCurrentNode);
|
mAllowedNodes.push_back(mCurrentNode);
|
||||||
mCurrentNode = temp;
|
mCurrentNode = temp;
|
||||||
|
|
||||||
mMoveNow = false;
|
moveNow = false;
|
||||||
mWalking = true;
|
walking = true;
|
||||||
}
|
}
|
||||||
// Choose a different node and delete this one from possible nodes because it is uncreachable:
|
// Choose a different node and delete this one from possible nodes because it is uncreachable:
|
||||||
else
|
else
|
||||||
|
@ -648,7 +710,7 @@ namespace MWMechanics
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AiWander::getRandomIdle()
|
void AiWander::getRandomIdle(short unsigned& playedIdle)
|
||||||
{
|
{
|
||||||
unsigned short idleRoll = 0;
|
unsigned short idleRoll = 0;
|
||||||
|
|
||||||
|
@ -661,7 +723,7 @@ namespace MWMechanics
|
||||||
unsigned short randSelect = (int)(rand() / ((double)RAND_MAX + 1) * int(100 / fIdleChanceMultiplier));
|
unsigned short randSelect = (int)(rand() / ((double)RAND_MAX + 1) * int(100 / fIdleChanceMultiplier));
|
||||||
if(randSelect < idleChance && randSelect > idleRoll)
|
if(randSelect < idleChance && randSelect > idleRoll)
|
||||||
{
|
{
|
||||||
mPlayedIdle = counter+2;
|
playedIdle = counter+2;
|
||||||
idleRoll = randSelect;
|
idleRoll = randSelect;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,9 @@
|
||||||
|
|
||||||
#include "../mwworld/timestamp.hpp"
|
#include "../mwworld/timestamp.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
#include "aistate.hpp"
|
||||||
|
|
||||||
namespace ESM
|
namespace ESM
|
||||||
{
|
{
|
||||||
namespace AiSequence
|
namespace AiSequence
|
||||||
|
@ -21,7 +24,9 @@ namespace ESM
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace MWMechanics
|
namespace MWMechanics
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
/// \brief Causes the Actor to wander within a specified range
|
/// \brief Causes the Actor to wander within a specified range
|
||||||
class AiWander : public AiPackage
|
class AiWander : public AiPackage
|
||||||
{
|
{
|
||||||
|
@ -36,12 +41,11 @@ namespace MWMechanics
|
||||||
|
|
||||||
AiWander (const ESM::AiSequence::AiWander* wander);
|
AiWander (const ESM::AiSequence::AiWander* wander);
|
||||||
|
|
||||||
// NOTE: mDistance and mDuration must be set already
|
|
||||||
void init();
|
|
||||||
|
|
||||||
virtual AiPackage *clone() const;
|
virtual AiPackage *clone() const;
|
||||||
|
|
||||||
virtual bool execute (const MWWorld::Ptr& actor,float duration);
|
virtual bool execute (const MWWorld::Ptr& actor, AiState& state, float duration);
|
||||||
|
|
||||||
virtual int getTypeId() const;
|
virtual int getTypeId() const;
|
||||||
|
|
||||||
|
@ -51,11 +55,20 @@ namespace MWMechanics
|
||||||
|
|
||||||
virtual void writeState(ESM::AiSequence::AiSequence &sequence) const;
|
virtual void writeState(ESM::AiSequence::AiSequence &sequence) const;
|
||||||
|
|
||||||
|
|
||||||
|
enum GreetingState {
|
||||||
|
Greet_None,
|
||||||
|
Greet_InProgress,
|
||||||
|
Greet_Done
|
||||||
|
};
|
||||||
private:
|
private:
|
||||||
|
// NOTE: mDistance and mDuration must be set already
|
||||||
|
void init();
|
||||||
|
|
||||||
void stopWalking(const MWWorld::Ptr& actor);
|
void stopWalking(const MWWorld::Ptr& actor);
|
||||||
void playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect);
|
void playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect);
|
||||||
bool checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect);
|
bool checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect);
|
||||||
void getRandomIdle();
|
void getRandomIdle(unsigned short& playedIdle);
|
||||||
|
|
||||||
int mDistance; // how far the actor can wander from the spawn point
|
int mDistance; // how far the actor can wander from the spawn point
|
||||||
int mDuration;
|
int mDuration;
|
||||||
|
@ -63,36 +76,21 @@ namespace MWMechanics
|
||||||
std::vector<unsigned char> mIdle;
|
std::vector<unsigned char> mIdle;
|
||||||
bool mRepeat;
|
bool mRepeat;
|
||||||
|
|
||||||
enum GreetingState {
|
|
||||||
Greet_None,
|
|
||||||
Greet_InProgress,
|
|
||||||
Greet_Done
|
|
||||||
};
|
|
||||||
GreetingState mSaidGreeting;
|
|
||||||
int mGreetingTimer;
|
|
||||||
|
|
||||||
bool mHasReturnPosition; // NOTE: Could be removed if mReturnPosition was initialized to actor position,
|
bool mHasReturnPosition; // NOTE: Could be removed if mReturnPosition was initialized to actor position,
|
||||||
// if we had the actor in the AiWander constructor...
|
// if we had the actor in the AiWander constructor...
|
||||||
Ogre::Vector3 mReturnPosition;
|
Ogre::Vector3 mReturnPosition;
|
||||||
|
|
||||||
// Cached current cell location
|
|
||||||
int mCellX;
|
|
||||||
int mCellY;
|
|
||||||
// Cell location multiplied by ESM::Land::REAL_SIZE
|
|
||||||
float mXCell;
|
|
||||||
float mYCell;
|
|
||||||
|
|
||||||
const MWWorld::CellStore* mCell; // for detecting cell change
|
|
||||||
|
|
||||||
|
|
||||||
// if false triggers calculating allowed nodes based on mDistance
|
// if false triggers calculating allowed nodes based on mDistance
|
||||||
bool mStoredAvailableNodes;
|
bool mStoredAvailableNodes;
|
||||||
// AiWander states
|
|
||||||
bool mChooseAction;
|
|
||||||
bool mIdleNow;
|
|
||||||
bool mMoveNow;
|
|
||||||
bool mWalking;
|
|
||||||
|
|
||||||
unsigned short mPlayedIdle;
|
|
||||||
|
|
||||||
|
|
||||||
MWWorld::TimeStamp mStartTime;
|
MWWorld::TimeStamp mStartTime;
|
||||||
|
|
||||||
|
@ -103,18 +101,16 @@ namespace MWMechanics
|
||||||
void trimAllowedNodes(std::vector<ESM::Pathgrid::Point>& nodes,
|
void trimAllowedNodes(std::vector<ESM::Pathgrid::Point>& nodes,
|
||||||
const PathFinder& pathfinder);
|
const PathFinder& pathfinder);
|
||||||
|
|
||||||
PathFinder mPathFinder;
|
// PathFinder mPathFinder;
|
||||||
|
|
||||||
ObstacleCheck mObstacleCheck;
|
// ObstacleCheck mObstacleCheck;
|
||||||
float mDoorCheckDuration;
|
float mDoorCheckDuration;
|
||||||
int mStuckCount;
|
int mStuckCount;
|
||||||
|
|
||||||
// the z rotation angle (degrees) we want to reach
|
|
||||||
// used every frame when mRotate is true
|
|
||||||
float mTargetAngle;
|
|
||||||
bool mRotate;
|
|
||||||
float mReaction; // update some actions infrequently
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
#include <components/esm/loadmgef.hpp>
|
#include <components/esm/loadmgef.hpp>
|
||||||
|
|
||||||
#include "../mwworld/ptr.hpp"
|
#include "../mwworld/ptr.hpp"
|
||||||
|
#include "aistate.hpp"
|
||||||
|
|
||||||
namespace MWWorld
|
namespace MWWorld
|
||||||
{
|
{
|
||||||
|
@ -138,6 +139,9 @@ class CharacterController
|
||||||
{
|
{
|
||||||
MWWorld::Ptr mPtr;
|
MWWorld::Ptr mPtr;
|
||||||
MWRender::Animation *mAnimation;
|
MWRender::Animation *mAnimation;
|
||||||
|
|
||||||
|
//
|
||||||
|
AiState mAiState;
|
||||||
|
|
||||||
typedef std::deque<std::pair<std::string,size_t> > AnimationQueue;
|
typedef std::deque<std::pair<std::string,size_t> > AnimationQueue;
|
||||||
AnimationQueue mAnimQueue;
|
AnimationQueue mAnimQueue;
|
||||||
|
@ -218,6 +222,8 @@ public:
|
||||||
{ return mDeathState != CharState_None; }
|
{ return mDeathState != CharState_None; }
|
||||||
|
|
||||||
void forceStateUpdate();
|
void forceStateUpdate();
|
||||||
|
|
||||||
|
AiState& getAiState() { return mAiState; }
|
||||||
};
|
};
|
||||||
|
|
||||||
void getWeaponGroup(WeaponType weaptype, std::string &group);
|
void getWeaponGroup(WeaponType weaptype, std::string &group);
|
||||||
|
|
|
@ -278,9 +278,8 @@ namespace MWMechanics
|
||||||
const ESM::Pathgrid::Point &nextPoint = *mPath.begin();
|
const ESM::Pathgrid::Point &nextPoint = *mPath.begin();
|
||||||
float directionX = nextPoint.mX - x;
|
float directionX = nextPoint.mX - x;
|
||||||
float directionY = nextPoint.mY - y;
|
float directionY = nextPoint.mY - y;
|
||||||
float directionResult = sqrt(directionX * directionX + directionY * directionY);
|
|
||||||
|
|
||||||
return Ogre::Radian(Ogre::Math::ACos(directionY / directionResult) * sgn(Ogre::Math::ASin(directionX / directionResult))).valueDegrees();
|
return Ogre::Math::ATan2(directionX,directionY).valueDegrees();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool PathFinder::checkWaypoint(float x, float y, float z)
|
bool PathFinder::checkWaypoint(float x, float y, float z)
|
||||||
|
|
Loading…
Reference in a new issue