Merge remote-tracking branch 'scrawl/aicombat'

c++11
Marc Zinnschlag 10 years ago
commit 432384d280

@ -31,8 +31,6 @@ namespace
//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);
void getMinMaxAttackDuration(const MWWorld::Ptr& actor, float (*fMinMaxDurations)[2]);
osg::Vec3f AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const osg::Vec3f& vLastTargetPos, osg::Vec3f AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const osg::Vec3f& vLastTargetPos,
float duration, int weapType, float strength); float duration, int weapType, float strength);
@ -83,7 +81,7 @@ namespace MWMechanics
/// \brief This class holds the variables AiCombat needs which are deleted if the package becomes inactive. /// \brief This class holds the variables AiCombat needs which are deleted if the package becomes inactive.
struct AiCombatStorage : AiTemporaryBase struct AiCombatStorage : AiTemporaryBase
{ {
float mTimerAttack; float mAttackCooldown;
float mTimerReact; float mTimerReact;
float mTimerCombatMove; float mTimerCombatMove;
bool mReadyToAttack; bool mReadyToAttack;
@ -95,15 +93,13 @@ namespace MWMechanics
boost::shared_ptr<Action> mCurrentAction; boost::shared_ptr<Action> mCurrentAction;
float mActionCooldown; float mActionCooldown;
float mStrength; float mStrength;
float mMinMaxAttackDuration[3][2];
bool mMinMaxAttackDurationInitialised;
bool mForceNoShortcut; bool mForceNoShortcut;
ESM::Position mShortcutFailPos; ESM::Position mShortcutFailPos;
osg::Vec3f mLastActorPos; osg::Vec3f mLastActorPos;
MWMechanics::Movement mMovement; MWMechanics::Movement mMovement;
AiCombatStorage(): AiCombatStorage():
mTimerAttack(0), mAttackCooldown(0),
mTimerReact(0), mTimerReact(0),
mTimerCombatMove(0), mTimerCombatMove(0),
mReadyToAttack(false), mReadyToAttack(false),
@ -115,7 +111,6 @@ namespace MWMechanics
mCurrentAction(), mCurrentAction(),
mActionCooldown(0), mActionCooldown(0),
mStrength(), mStrength(),
mMinMaxAttackDurationInitialised(false),
mForceNoShortcut(false), mForceNoShortcut(false),
mLastActorPos(0,0,0), mLastActorPos(0,0,0),
mMovement(){} mMovement(){}
@ -233,41 +228,12 @@ namespace MWMechanics
{ {
if(smoothTurn(actor, osg::DegreesToRadians(movement.mRotation[0]), 0)) movement.mRotation[0] = 0; if(smoothTurn(actor, osg::DegreesToRadians(movement.mRotation[0]), 0)) movement.mRotation[0] = 0;
} }
float attacksPeriod = 1.0f;
ESM::Weapon::AttackType attackType;
bool& attack = storage.mAttack; bool& attack = storage.mAttack;
bool& readyToAttack = storage.mReadyToAttack; bool& readyToAttack = storage.mReadyToAttack;
float& timerAttack = storage.mTimerAttack;
bool& minMaxAttackDurationInitialised = storage.mMinMaxAttackDurationInitialised;
float (&minMaxAttackDuration)[3][2] = storage.mMinMaxAttackDuration;
if(readyToAttack)
{
if (!minMaxAttackDurationInitialised)
{
// TODO: this must be updated when a different weapon is equipped
// TODO: it would be much easier to ask the CharacterController about the current % completion of the weapon wind-up animation
getMinMaxAttackDuration(actor, minMaxAttackDuration);
minMaxAttackDurationInitialised = true;
}
if (timerAttack < 0) attack = false; if (attack && (characterController.getAttackStrength() >= storage.mStrength || characterController.readyToPrepareAttack()))
timerAttack -= duration;
}
else
{
timerAttack = -attacksPeriod;
attack = false; attack = false;
}
characterController.setAttackingOrSpell(attack); characterController.setAttackingOrSpell(attack);
@ -300,10 +266,6 @@ namespace MWMechanics
currentCell = actor.getCell(); currentCell = actor.getCell();
} }
MWRender::Animation* anim = MWBase::Environment::get().getWorld()->getAnimation(actor);
if (!anim) // shouldn't happen
return false;
actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true); actorClass.getCreatureStats(actor).setMovementFlag(CreatureStats::Flag_Run, true);
if (actionCooldown > 0) if (actionCooldown > 0)
@ -312,11 +274,7 @@ namespace MWMechanics
float rangeAttack = 0; float rangeAttack = 0;
float rangeFollow = 0; float rangeFollow = 0;
boost::shared_ptr<Action>& currentAction = storage.mCurrentAction; boost::shared_ptr<Action>& currentAction = storage.mCurrentAction;
// TODO: upperBodyReady() works fine for checking if we can start an attack, if (characterController.readyToPrepareAttack())
// but doesn't work properly for checking if the attack is finished (as things like hit recovery or knockdown also play on the upper body)
// Only a minor problem, but can mess with the actionCooldown timer.
// To fix this the AI needs to be brought closer to the CharacterController, so we can simply check if a weapon animation is playing.
if (anim->upperBodyReady())
{ {
currentAction = prepareNextAction(actor, target); currentAction = prepareNextAction(actor, target);
actionCooldown = currentAction->getActionCooldown(); actionCooldown = currentAction->getActionCooldown();
@ -333,9 +291,6 @@ namespace MWMechanics
// Get weapon characteristics // Get weapon characteristics
if (actorClass.hasInventoryStore(actor)) if (actorClass.hasInventoryStore(actor))
{ {
// TODO: Check equipped weapon and equip a different one if we can't attack with it
// (e.g. no ammunition, or wrong type of ammunition equipped, etc. autoEquip is not very smart in this regard))
//Get weapon speed and range //Get weapon speed and range
MWWorld::ContainerStoreIterator weaponSlot = MWWorld::ContainerStoreIterator weaponSlot =
MWMechanics::getActiveWeapon(actorClass.getCreatureStats(actor), actorClass.getInventoryStore(actor), &weaptype); MWMechanics::getActiveWeapon(actorClass.getCreatureStats(actor), actorClass.getInventoryStore(actor), &weaptype);
@ -383,34 +338,38 @@ namespace MWMechanics
} }
float& strength = storage.mStrength; float& strength = storage.mStrength;
// start new attack // start new attack
if(readyToAttack) if(readyToAttack && characterController.readyToStartAttack())
{ {
if(timerAttack <= -attacksPeriod) if (storage.mAttackCooldown <= 0)
{ {
attack = true; // attack starts just now attack = true; // attack starts just now
characterController.setAttackingOrSpell(attack);
if (!distantCombat) attackType = chooseBestAttack(weapon, movement); if (!distantCombat)
else attackType = ESM::Weapon::AT_Chop; // cause it's =0 chooseBestAttack(weapon, movement);
strength = Misc::Rng::rollClosedProbability(); strength = Misc::Rng::rollClosedProbability();
// Note: may be 0 for some animations const MWWorld::ESMStore &store = world->getStore();
timerAttack = minMaxAttackDuration[attackType][0] +
(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())
{ {
const MWWorld::ESMStore &store = world->getStore();
int chance = store.get<ESM::GameSetting>().find("iVoiceAttackOdds")->getInt(); int chance = store.get<ESM::GameSetting>().find("iVoiceAttackOdds")->getInt();
if (Misc::Rng::roll0to99() < chance) if (Misc::Rng::roll0to99() < chance)
{ {
MWBase::Environment::get().getDialogueManager()->say(actor, "attack"); MWBase::Environment::get().getDialogueManager()->say(actor, "attack");
} }
} }
float baseDelay = store.get<ESM::GameSetting>().find("fCombatDelayCreature")->getFloat();
if (actor.getClass().isNpc())
baseDelay = store.get<ESM::GameSetting>().find("fCombatDelayNPC")->getFloat();
storage.mAttackCooldown = std::min(baseDelay + 0.01 * Misc::Rng::roll0to99(), baseDelay + 0.9);
} }
else
storage.mAttackCooldown -= tReaction;
} }
@ -618,57 +577,6 @@ namespace MWMechanics
readyToAttack = false; readyToAttack = false;
} }
if(!isStuck && distToTarget > rangeAttack && !distantCombat)
{
//special run attack; it shouldn't affect melee combat tactics
if(actorClass.getMovementSettings(actor).mPosition[1] == 1)
{
/* check if actor can overcome the distance = distToTarget - attackerWeapRange
less than in time of swinging with weapon (t_swing), then start attacking
*/
float speed1 = actorClass.getSpeed(actor);
float speed2 = target.getClass().getSpeed(target);
if(target.getClass().getMovementSettings(target).mPosition[0] == 0
&& target.getClass().getMovementSettings(target).mPosition[1] == 0)
speed2 = 0;
float s1 = distToTarget - weapRange;
float t = s1/speed1;
float s2 = speed2 * t;
float t_swing =
minMaxAttackDuration[ESM::Weapon::AT_Thrust][0] +
(minMaxAttackDuration[ESM::Weapon::AT_Thrust][1] - minMaxAttackDuration[ESM::Weapon::AT_Thrust][0]) * Misc::Rng::rollClosedProbability();
if (t + s2/speed1 <= t_swing)
{
readyToAttack = true;
if(timerAttack <= -attacksPeriod)
{
timerAttack = t_swing;
attack = true;
}
}
}
}
// TODO: Add a parameter to vary DURATION_SAME_SPOT?
if((distToTarget > rangeAttack || followTarget) &&
mObstacleCheck.check(actor, tReaction)) // check if evasive action needed
{
// probably walking into another NPC TODO: untested in combat situation
// TODO: diagonal should have same animation as walk forward
// but doesn't seem to do that?
actor.getClass().getMovementSettings(actor).mPosition[0] = 1;
actor.getClass().getMovementSettings(actor).mPosition[1] = 0.1f;
// change the angle a bit, too
if(mPathFinder.isPathConstructed())
zTurn(actor, osg::DegreesToRadians(mPathFinder.getZAngleToNext(pos.pos[0] + 1, pos.pos[1])));
if(followTarget)
followTarget = false;
// FIXME: can fool actors to stay behind doors, etc.
// Related to Bug#1102 and to some degree #1155 as well
}
return false; return false;
} }
@ -794,70 +702,6 @@ ESM::Weapon::AttackType chooseBestAttack(const ESM::Weapon* weapon, MWMechanics:
return attackType; return attackType;
} }
void getMinMaxAttackDuration(const MWWorld::Ptr& actor, float (*fMinMaxDurations)[2])
{
if (!actor.getClass().hasInventoryStore(actor)) // creatures
{
fMinMaxDurations[0][0] = fMinMaxDurations[0][1] = 0.1f;
fMinMaxDurations[1][0] = fMinMaxDurations[1][1] = 0.1f;
fMinMaxDurations[2][0] = fMinMaxDurations[2][1] = 0.1f;
return;
}
// get weapon information: type and speed
const ESM::Weapon *weapon = NULL;
MWMechanics::WeaponType weaptype = MWMechanics::WeapType_None;
MWWorld::ContainerStoreIterator weaponSlot =
MWMechanics::getActiveWeapon(actor.getClass().getCreatureStats(actor), actor.getClass().getInventoryStore(actor), &weaptype);
float weapSpeed;
if (weaptype != MWMechanics::WeapType_HandToHand
&& weaptype != MWMechanics::WeapType_Spell
&& weaptype != MWMechanics::WeapType_None)
{
weapon = weaponSlot->get<ESM::Weapon>()->mBase;
weapSpeed = weapon->mData.mSpeed;
}
else weapSpeed = 1.0f;
MWRender::Animation *anim = MWBase::Environment::get().getWorld()->getAnimation(actor);
std::string weapGroup;
MWMechanics::getWeaponGroup(weaptype, weapGroup);
weapGroup = weapGroup + ": ";
bool bRangedWeap = (weaptype >= MWMechanics::WeapType_BowAndArrow && weaptype <= MWMechanics::WeapType_Thrown);
const char *attackType[] = {"chop ", "slash ", "thrust ", "shoot "};
std::string textKey = "start";
std::string textKey2;
// get durations for each attack type
for (int i = 0; i < (bRangedWeap ? 1 : 3); i++)
{
float start1 = anim->getTextKeyTime(weapGroup + (bRangedWeap ? attackType[3] : attackType[i]) + textKey);
if (start1 < 0)
{
fMinMaxDurations[i][0] = fMinMaxDurations[i][1] = 0.1f;
continue;
}
textKey2 = "min attack";
float start2 = anim->getTextKeyTime(weapGroup + (bRangedWeap ? attackType[3] : attackType[i]) + textKey2);
fMinMaxDurations[i][0] = (start2 - start1) / weapSpeed;
textKey2 = "max attack";
start1 = anim->getTextKeyTime(weapGroup + (bRangedWeap ? attackType[3] : attackType[i]) + textKey2);
fMinMaxDurations[i][1] = fMinMaxDurations[i][0] + (start1 - start2) / weapSpeed;
}
}
osg::Vec3f AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const osg::Vec3f& vLastTargetPos, osg::Vec3f AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const osg::Vec3f& vLastTargetPos,
float duration, int weapType, float strength) float duration, int weapType, float strength)
{ {

@ -1259,6 +1259,8 @@ bool CharacterController::updateWeaponState()
} }
animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete);
if(mUpperBodyState == UpperCharState_MinAttackToMaxAttack && mHitState != CharState_KnockDown)
mAttackStrength = complete;
} }
else else
{ {
@ -1789,7 +1791,8 @@ void CharacterController::update(float duration)
} }
} }
if(cls.isBipedal(mPtr)) // bipedal means hand-to-hand could be used (which is handled in updateWeaponState). an existing InventoryStore means an actual weapon could be used.
if(cls.isBipedal(mPtr) || cls.hasInventoryStore(mPtr))
forcestateupdate = updateWeaponState() || forcestateupdate; forcestateupdate = updateWeaponState() || forcestateupdate;
else else
forcestateupdate = updateCreatureState() || forcestateupdate; forcestateupdate = updateCreatureState() || forcestateupdate;
@ -2047,6 +2050,27 @@ void CharacterController::setAttackingOrSpell(bool attackingOrSpell)
mAttackingOrSpell = attackingOrSpell; mAttackingOrSpell = attackingOrSpell;
} }
bool CharacterController::readyToPrepareAttack() const
{
return mHitState == CharState_None && mUpperBodyState <= UpperCharState_WeapEquiped;
}
bool CharacterController::readyToStartAttack() const
{
if (mHitState != CharState_None)
return false;
if (mPtr.getClass().hasInventoryStore(mPtr) || mPtr.getClass().isBipedal(mPtr))
return mUpperBodyState == UpperCharState_WeapEquiped;
else
return mUpperBodyState == UpperCharState_Nothing;
}
float CharacterController::getAttackStrength() const
{
return mAttackStrength;
}
void CharacterController::setActive(bool active) void CharacterController::setActive(bool active)
{ {
mAnimation->setActive(active); mAnimation->setActive(active);

@ -240,6 +240,11 @@ public:
void setAttackingOrSpell(bool attackingOrSpell); void setAttackingOrSpell(bool attackingOrSpell);
bool readyToPrepareAttack() const;
bool readyToStartAttack() const;
float getAttackStrength() const;
/// @see Animation::setActive /// @see Animation::setActive
void setActive(bool active); void setActive(bool active);
@ -247,7 +252,6 @@ public:
void setHeadTrackTarget(const MWWorld::Ptr& target); void setHeadTrackTarget(const MWWorld::Ptr& target);
}; };
void getWeaponGroup(WeaponType weaptype, std::string &group);
MWWorld::ContainerStoreIterator getActiveWeapon(CreatureStats &stats, MWWorld::InventoryStore &inv, WeaponType *weaptype); MWWorld::ContainerStoreIterator getActiveWeapon(CreatureStats &stats, MWWorld::InventoryStore &inv, WeaponType *weaptype);
} }

@ -850,7 +850,6 @@ void NpcAnimation::addControllers()
Animation::addControllers(); Animation::addControllers();
mFirstPersonNeckController = NULL; mFirstPersonNeckController = NULL;
mHeadController = NULL;
WeaponAnimation::deleteControllers(); WeaponAnimation::deleteControllers();
if (mViewMode == VM_FirstPerson) if (mViewMode == VM_FirstPerson)

Loading…
Cancel
Save