@ -27,46 +27,6 @@ namespace
osg : : Vec3f AimDirToMovingTarget ( const MWWorld : : Ptr & actor , const MWWorld : : Ptr & target , const osg : : Vec3f & vLastTargetPos ,
float duration , int weapType , float strength ) ;
float getZAngleToDir ( const osg : : Vec3f & dir )
{
return std : : atan2 ( dir . x ( ) , dir . y ( ) ) ;
}
float getXAngleToDir ( const osg : : Vec3f & dir )
{
return - std : : asin ( dir . z ( ) / dir . length ( ) ) ;
}
const float REACTION_INTERVAL = 0.25f ;
const float PATHFIND_Z_REACH = 50.0f ;
// distance at which actor pays more attention to decide whether to shortcut or stick to pathgrid
const float PATHFIND_CAUTION_DIST = 500.0f ;
// distance after which actor (failed previously to shortcut) will try again
const float PATHFIND_SHORTCUT_RETRY_DIST = 300.0f ;
// cast up-down ray with some offset from actor position to check for pits/obstacles on the way to target;
// magnitude of pits/obstacles is defined by PATHFIND_Z_REACH
bool checkWayIsClear ( const osg : : Vec3f & from , const osg : : Vec3f & to , float offsetXY )
{
if ( ( to - from ) . length ( ) > = PATHFIND_CAUTION_DIST | | std : : abs ( from . z ( ) - to . z ( ) ) < = PATHFIND_Z_REACH )
{
osg : : Vec3f dir = to - from ;
dir . z ( ) = 0 ;
dir . normalize ( ) ;
float verticalOffset = 200 ; // instead of '200' here we want the height of the actor
osg : : Vec3f _from = from + dir * offsetXY + osg : : Vec3f ( 0 , 0 , 1 ) * verticalOffset ;
// cast up-down ray and find height in world space of hit
float h = _from . z ( ) - MWBase : : Environment : : get ( ) . getWorld ( ) - > getDistToNearestRayHit ( _from , osg : : Vec3f ( 0 , 0 , - 1 ) , verticalOffset + PATHFIND_Z_REACH + 1 ) ;
if ( std : : abs ( from . z ( ) - h ) < = PATHFIND_Z_REACH )
return true ;
}
return false ;
}
}
namespace MWMechanics
@ -80,7 +40,7 @@ namespace MWMechanics
float mTimerCombatMove ;
bool mReadyToAttack ;
bool mAttack ;
bool mFollowTarget ;
float mAttackRange ;
bool mCombatMove ;
osg : : Vec3f mLastTargetPos ;
const MWWorld : : CellStore * mCell ;
@ -89,16 +49,15 @@ namespace MWMechanics
float mStrength ;
bool mForceNoShortcut ;
ESM : : Position mShortcutFailPos ;
osg : : Vec3f mLastActorPos ;
MWMechanics : : Movement mMovement ;
AiCombatStorage ( ) :
mAttackCooldown ( 0 ) ,
mTimerReact ( 0 ) ,
mTimerReact ( AI_REACTION_TIME ) ,
mTimerCombatMove ( 0 ) ,
mReadyToAttack ( false ) ,
mAttack ( false ) ,
m FollowTarget( false ) ,
m AttackRange( 0 ) ,
mCombatMove ( false ) ,
mLastTargetPos ( 0 , 0 , 0 ) ,
mCell ( NULL ) ,
@ -107,8 +66,8 @@ namespace MWMechanics
mStrength ( ) ,
mForceNoShortcut ( false ) ,
mShortcutFailPos ( ) ,
m LastActorPos( 0 , 0 , 0 ) ,
mMovement ( ) { }
m Movement( )
{ }
void startCombatMove ( bool isNpc , bool isDistantCombat , float distToTarget , float rangeAttack ) ;
void updateCombatMove ( float duration ) ;
@ -179,6 +138,7 @@ namespace MWMechanics
* Use the Observer Pattern to co - ordinate attacks , provide intelligence on
* whether the target was hit , etc .
*/
bool AiCombat : : execute ( const MWWorld : : Ptr & actor , CharacterController & characterController , AiState & state , float duration )
{
// get or create temporary storage
@ -197,34 +157,38 @@ namespace MWMechanics
| | target . getClass ( ) . getCreatureStats ( target ) . isDead ( ) )
return true ;
//Update every frame
if ( storage . mCurrentAction . get ( ) ) // need to wait to init action with it's attack range
{
//Update every frame
bool is_target_reached = pathTo ( actor , target . getRefData ( ) . getPosition ( ) . pos , duration , storage . mAttackRange ) ;
if ( is_target_reached ) storage . mReadyToAttack = true ;
}
storage . updateCombatMove ( duration ) ;
updateActorsMovement ( actor , duration , storage . mMovement ) ;
if ( storage . mReadyToAttack ) updateActorsMovement ( actor , duration , storage ) ;
storage . updateAttack ( characterController ) ;
storage . mActionCooldown - = duration ;
float & timerReact = storage . mTimerReact ;
if ( timerReact < REACTION_IN TERVAL )
if ( timerReact < AI_ REACTION_TIM E)
{
timerReact + = duration ;
return false ;
}
else
{
timerReact = 0 ;
return reactionTimeActions ( actor , characterController , storage , target ) ;
attack ( actor , target , storage , characterController ) ;
}
return false ;
}
bool AiCombat : : reactionTimeActions ( const MWWorld : : Ptr & actor , CharacterController & characterController ,
AiCombatStorage & storage , MWWorld : : Ptr target )
void AiCombat : : attack ( const MWWorld : : Ptr & actor , const MWWorld : : Ptr & target , AiCombatStorage & storage , CharacterController & characterController )
{
MWMechanics : : Movement & movement = storage . mMovement ;
if ( isTargetMagicallyHidden ( target ) )
{
storage . stopAttack ( ) ;
return false ; // TODO: run away instead of doing nothing
return ; // TODO: run away instead of doing nothing
}
const MWWorld : : CellStore * & currentCell = storage . mCell ;
@ -239,10 +203,9 @@ namespace MWMechanics
float & actionCooldown = storage . mActionCooldown ;
if ( actionCooldown > 0 )
return false ;
return ;
float rangeAttack = 0 ;
float rangeFollow = 0 ;
float & rangeAttack = storage . mAttackRange ;
boost : : shared_ptr < Action > & currentAction = storage . mCurrentAction ;
if ( characterController . readyToPrepareAttack ( ) )
{
@ -250,253 +213,67 @@ namespace MWMechanics
actionCooldown = currentAction - > getActionCooldown ( ) ;
}
if ( currentAction . get ( ) )
currentAction - > getCombatRange ( rangeAttack , rangeFollow ) ;
// FIXME: consider moving this stuff to ActionWeapon::getCombatRange
const ESM : : Weapon * weapon = NULL ;
MWMechanics : : WeaponType weaptype = WeapType_None ;
float weapRange = 1.0f ;
// Get weapon characteristics
MWBase : : World * world = MWBase : : Environment : : get ( ) . getWorld ( ) ;
static const float fCombatDistance = world - > getStore ( ) . get < ESM : : GameSetting > ( ) . find ( " fCombatDistance " ) - > getFloat ( ) ;
if ( actorClass . hasInventoryStore ( actor ) )
{
//Get weapon range
MWWorld : : ContainerStoreIterator weaponSlot =
MWMechanics : : getActiveWeapon ( actorClass . getCreatureStats ( actor ) , actorClass . getInventoryStore ( actor ) , & weaptype ) ;
if ( weaptype = = WeapType_HandToHand )
{
static float fHandToHandReach =
world - > getStore ( ) . get < ESM : : GameSetting > ( ) . find ( " fHandToHandReach " ) - > getFloat ( ) ;
weapRange = fHandToHandReach ;
}
else if ( weaptype ! = WeapType_PickProbe & & weaptype ! = WeapType_Spell & & weaptype ! = WeapType_None )
{
// All other WeapTypes are actually weapons, so get<ESM::Weapon> is safe.
weapon = weaponSlot - > get < ESM : : Weapon > ( ) - > mBase ;
weapRange = weapon - > mData . mReach ;
}
weapRange * = fCombatDistance ;
}
else //is creature
{
weaptype = actorClass . getCreatureStats ( actor ) . getDrawState ( ) = = DrawState_Spell ? WeapType_Spell : WeapType_HandToHand ;
weapRange = fCombatDistance ;
}
bool distantCombat = false ;
if ( weaptype ! = WeapType_Spell )
{
// TODO: move to ActionWeapon
if ( weaptype = = WeapType_BowAndArrow | | weaptype = = WeapType_Crossbow | | weaptype = = WeapType_Thrown )
{
rangeAttack = 1000 ;
rangeFollow = 0 ; // not needed in ranged combat
distantCombat = true ;
}
else
{
rangeAttack = weapRange ;
rangeFollow = 300 ;
}
}
else
bool isRangedCombat = false ;
if ( currentAction . get ( ) )
{
distantCombat = ( rangeAttack > 500 ) ;
rangeAttack = currentAction - > getCombatRange ( isRangedCombat ) ;
// Get weapon characteristics
weapon = currentAction - > getWeapon ( ) ;
}
bool & readyToAttack = storage . mReadyToAttack ;
// start new attack
storage . startAttackIfReady ( actor , characterController , weapon , distantCombat ) ;
/*
* Some notes on meanings of variables :
*
* rangeAttack :
*
* - Distance where attack using the actor ' s weapon is possible :
* longer for ranged weapons ( obviously ? ) vs . melee weapons
* - Determined by weapon ' s reach parameter ; hardcoded value
* for ranged weapon and for creatures
* - Once within this distance mFollowTarget is triggered
*
* rangeFollow :
*
* - Applies to melee weapons or hand to hand only ( or creatures without
* weapons )
* - Distance a little further away than the actor ' s weapon reach
* i . e . rangeFollow > rangeAttack for melee weapons
* - Hardcoded value ( 0 for ranged weapons )
* - Once the target gets beyond this distance mFollowTarget is cleared
* and a path to the target needs to be found
*
* mFollowTarget :
*
* - Once triggered , the actor follows the target with LOS shortcut
* ( the shortcut really only applies to cells where pathgrids are
* available , since the default path without pathgrids is direct to
* target even if LOS is not achieved )
*/
ESM : : Position pos = actor . getRefData ( ) . getPosition ( ) ;
osg : : Vec3f vActorPos ( pos . asVec3 ( ) ) ;
osg : : Vec3f vTargetPos ( target . getRefData ( ) . getPosition ( ) . asVec3 ( ) ) ;
osg : : Vec3f vAimDir = MWBase : : Environment : : get ( ) . getWorld ( ) - > aimToTarget ( actor , target ) ;
float distToTarget = MWBase : : Environment : : get ( ) . getWorld ( ) - > getHitDistance ( actor , target ) ;
osg : : Vec3f & lastActorPos = storage . mLastActorPos ;
bool & followTarget = storage . mFollowTarget ;
bool isStuck = false ;
float speed = 0.0f ;
if ( movement . mPosition [ 1 ] & & ( lastActorPos - vActorPos ) . length ( ) < ( speed = actorClass . getSpeed ( actor ) ) * REACTION_INTERVAL / 2 )
isStuck = true ;
lastActorPos = vActorPos ;
// check if actor can move along z-axis
bool canMoveByZ = ( actorClass . canSwim ( actor ) & & world - > isSwimming ( actor ) )
| | world - > isFlying ( actor ) ;
storage . mReadyToAttack = ( distToTarget < = rangeAttack ) ;
// can't fight if attacker can't go where target is. E.g. A fish can't attack person on land.
if ( distToTarget > = rangeAttack
if ( distToTarget > rangeAttack
& & ! actorClass . isNpc ( ) & & ! MWMechanics : : isEnvironmentCompatible ( actor , target ) )
{
// TODO: start fleeing?
storage . stopAttack ( ) ;
return false ;
return ;
}
// for distant combat we should know if target is in LOS even if distToTarget < rangeAttack
bool inLOS = distantCombat ? world - > getLOS ( actor , target ) : true ;
// (within attack dist) || (not quite attack dist while following)
if ( inLOS & & ( distToTarget < rangeAttack | | ( distToTarget < = rangeFollow & & followTarget & & ! isStuck ) ) )
if ( storage . mReadyToAttack )
{
mPathFinder . clearPath ( ) ;
//Melee and Close-up combat
// getXAngleToDir determines vertical angle to target:
// if actor can move along z-axis it will control movement dir
// if can't - it will control correct aiming.
// note: in getZAngleToDir if we preserve dir.z then horizontal angle can be inaccurate
if ( distantCombat )
storage . startCombatMove ( actorClass . isNpc ( ) , isRangedCombat , distToTarget , rangeAttack ) ;
// start new attack
storage . startAttackIfReady ( actor , characterController , weapon , isRangedCombat ) ;
if ( isRangedCombat )
{
// rotate actor taking into account target movement direction and projectile speed
osg : : Vec3f & lastTargetPos = storage . mLastTargetPos ;
vAimDir = AimDirToMovingTarget ( actor , target , lastTargetPos , REACTION_INTERVAL , weaptype ,
storage . mStrength ) ;
vAimDir = AimDirToMovingTarget ( actor , target , lastTargetPos , AI_REACTION_TIME , ( weapon ? weapon - > mData . mType : 0 ) , storage . mStrength ) ;
lastTargetPos = vTargetPos ;
movement . mRotation [ 0 ] = getXAngleToDir ( vAimDir ) ;
movement . mRotation [ 2 ] = getZAngleToDir ( vAimDir ) ;
}
else
{
movement . mRotation [ 0 ] = getXAngleToDir ( vAimDir ) ;
movement . mRotation [ 2 ] = getZAngleToDir ( ( vTargetPos - vActorPos ) ) ; // using vAimDir results in spastic movements since the head is animated
}
// (not quite attack dist while following)
if ( followTarget & & distToTarget > rangeAttack )
{
//Close-up combat: just run up on target
storage . stopCombatMove ( ) ;
movement . mPosition [ 1 ] = 1 ;
storage . mMovement . mRotation [ 0 ] = getXAngleToDir ( vAimDir ) ;
storage . mMovement . mRotation [ 2 ] = getZAngleToDir ( vAimDir ) ;
}
else // (within attack dist)
{
storage . startCombatMove ( actorClass . isNpc ( ) , distantCombat , distToTarget , rangeAttack ) ;
readyToAttack = true ;
//only once got in melee combat, actor is allowed to use close-up shortcutting
followTarget = true ;
}
}
else // remote pathfinding
{
bool preferShortcut = false ;
if ( ! distantCombat ) inLOS = world - > getLOS ( actor , target ) ;
// check if shortcut is available
bool & forceNoShortcut = storage . mForceNoShortcut ;
ESM : : Position & shortcutFailPos = storage . mShortcutFailPos ;
if ( inLOS & & ( ! isStuck | | readyToAttack )
& & ( ! forceNoShortcut | | ( shortcutFailPos . asVec3 ( ) - vActorPos ) . length ( ) > = PATHFIND_SHORTCUT_RETRY_DIST ) )
{
if ( speed = = 0.0f ) speed = actorClass . getSpeed ( actor ) ;
// maximum dist before pit/obstacle for actor to avoid them depending on his speed
float maxAvoidDist = REACTION_INTERVAL * speed + speed / MAX_VEL_ANGULAR_RADIANS * 2 ; // *2 - for reliability
preferShortcut = checkWayIsClear ( vActorPos , vTargetPos , osg : : Vec3f ( vAimDir . x ( ) , vAimDir . y ( ) , 0 ) . length ( ) > maxAvoidDist * 1.5 ? maxAvoidDist : maxAvoidDist / 2 ) ;
}
// don't use pathgrid when actor can move in 3 dimensions
if ( canMoveByZ )
{
preferShortcut = true ;
movement . mRotation [ 0 ] = getXAngleToDir ( vAimDir ) ;
}
if ( preferShortcut )
{
movement . mRotation [ 2 ] = getZAngleToDir ( ( vTargetPos - vActorPos ) ) ;
forceNoShortcut = false ;
shortcutFailPos . pos [ 0 ] = shortcutFailPos . pos [ 1 ] = shortcutFailPos . pos [ 2 ] = 0 ;
mPathFinder . clearPath ( ) ;
}
else // if shortcut failed stick to path grid
{
if ( ! isStuck & & shortcutFailPos . pos [ 0 ] = = 0.0f & & shortcutFailPos . pos [ 1 ] = = 0.0f & & shortcutFailPos . pos [ 2 ] = = 0.0f )
{
forceNoShortcut = true ;
shortcutFailPos = pos ;
}
followTarget = false ;
buildNewPath ( actor , target ) ;
// should always return a path (even if it's just go straight on target.)
assert ( mPathFinder . isPathConstructed ( ) ) ;
}
if ( readyToAttack )
else
{
// to stop possible sideway moving after target moved out of attack range
storage . stopCombatMove ( ) ;
readyToAttack = false ;
storage . mMovement . mRotation [ 0 ] = getXAngleToDir ( vAimDir ) ;
storage . mMovement . mRotation [ 2 ] = getZAngleToDir ( ( vTargetPos - vActorPos ) ) ; // using vAimDir results in spastic movements since the head is animated
}
movement . mPosition [ 1 ] = 1 ;
}
return false ;
}
void AiCombat : : updateActorsMovement ( const MWWorld : : Ptr & actor , float duration , MWMechanics: : Movement & desiredMovement )
void AiCombat : : updateActorsMovement ( const MWWorld : : Ptr & actor , float duration , AiCombatStorage & storage )
{
// apply combat movement
MWMechanics : : Movement & actorMovementSettings = actor . getClass ( ) . getMovementSettings ( actor ) ;
if ( mPathFinder . isPathConstructed ( ) )
{
const ESM : : Position & pos = actor . getRefData ( ) . getPosition ( ) ;
if ( mPathFinder . checkPathCompleted ( pos . pos [ 0 ] , pos . pos [ 1 ] ) )
{
actorMovementSettings . mPosition [ 1 ] = 0 ;
}
else
{
evadeObstacles ( actor , duration , pos ) ;
}
}
else
{
actorMovementSettings = desiredMovement ;
rotateActorOnAxis ( actor , 2 , actorMovementSettings , desiredMovement ) ;
rotateActorOnAxis ( actor , 0 , actorMovementSettings , desiredMovement ) ;
}
actorMovementSettings . mPosition [ 0 ] = storage . mMovement . mPosition [ 0 ] ;
actorMovementSettings . mPosition [ 1 ] = storage . mMovement . mPosition [ 1 ] ;
actorMovementSettings . mPosition [ 2 ] = storage . mMovement . mPosition [ 2 ] ;
rotateActorOnAxis ( actor , 2 , actorMovementSettings , storage . mMovement ) ;
rotateActorOnAxis ( actor , 0 , actorMovementSettings , storage . mMovement ) ;
}
void AiCombat : : rotateActorOnAxis ( const MWWorld : : Ptr & actor , int axis ,
@ -514,35 +291,6 @@ namespace MWMechanics
}
}
bool AiCombat : : doesPathNeedRecalc ( ESM : : Pathgrid : : Point dest , const ESM : : Cell * cell )
{
if ( ! mPathFinder . getPath ( ) . empty ( ) )
{
osg : : Vec3f currPathTarget ( PathFinder : : MakeOsgVec3 ( mPathFinder . getPath ( ) . back ( ) ) ) ;
osg : : Vec3f newPathTarget = PathFinder : : MakeOsgVec3 ( dest ) ;
float dist = ( newPathTarget - currPathTarget ) . length ( ) ;
float targetPosThreshold = ( cell - > isExterior ( ) ) ? 300.0f : 100.0f ;
return dist > targetPosThreshold ;
}
else
{
// necessarily construct a new path
return true ;
}
}
void AiCombat : : buildNewPath ( const MWWorld : : Ptr & actor , const MWWorld : : Ptr & target )
{
ESM : : Pathgrid : : Point newPathTarget = PathFinder : : MakePathgridPoint ( target . getRefData ( ) . getPosition ( ) ) ;
//construct new path only if target has moved away more than on [targetPosThreshold]
if ( doesPathNeedRecalc ( newPathTarget , actor . getCell ( ) - > getCell ( ) ) )
{
ESM : : Pathgrid : : Point start ( PathFinder : : MakePathgridPoint ( actor . getRefData ( ) . getPosition ( ) ) ) ;
mPathFinder . buildSyncedPath ( start , newPathTarget , actor . getCell ( ) , false ) ;
}
}
int AiCombat : : getTypeId ( ) const
{
return TypeIdCombat ;
@ -582,13 +330,13 @@ namespace MWMechanics
mTimerCombatMove = 0.1f + 0.1f * Misc : : Rng : : rollClosedProbability ( ) ;
mCombatMove = true ;
}
// only NPCs are smart enough to use dodge movements
// dodge movements (for NPCs only)
else if ( isNpc & & ( ! isDistantCombat | | ( distToTarget < rangeAttack / 2 ) ) )
{
//apply sideway movement (kind of dodging) with some probability
if ( Misc : : Rng : : rollClosedProbability ( ) < 0.25 )
{
mMovement . mPosition [ 0 ] = Misc : : Rng : : rollProbability ( ) < 0.5 ? 1.0f : - 1.0f ;
mMovement . mPosition [ 0 ] = Misc : : Rng : : rollProbability ( ) < 0.5 ? 1.0f : - 1.0f ; // to the left/right
mTimerCombatMove = 0.05f + 0.15f * Misc : : Rng : : rollClosedProbability ( ) ;
mCombatMove = true ;
}
@ -651,7 +399,7 @@ namespace MWMechanics
mAttackCooldown = std : : min ( baseDelay + 0.01 * Misc : : Rng : : roll0to99 ( ) , baseDelay + 0.9 ) ;
}
else
mAttackCooldown - = REACTION_IN TERVAL ;
mAttackCooldown - = AI_ REACTION_TIM E;
}
}