From 520a832a9a7072b8bf306d94bc9b1d1666e615d2 Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 2 Jul 2015 22:24:23 +0200 Subject: [PATCH 1/7] Remove an already resolved todo comment --- apps/openmw/mwmechanics/aicombat.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 35d1f5376..88d4167ab 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -333,9 +333,6 @@ namespace MWMechanics // Get weapon characteristics 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 MWWorld::ContainerStoreIterator weaponSlot = MWMechanics::getActiveWeapon(actorClass.getCreatureStats(actor), actorClass.getInventoryStore(actor), &weaptype); From d07b176b368ac0fd7df95c3240e27b97b6a13813 Mon Sep 17 00:00:00 2001 From: scrawl Date: Thu, 2 Jul 2015 22:25:19 +0200 Subject: [PATCH 2/7] Take into account hit recovery, knockdown and other animations for AiCombat attack timing --- apps/openmw/mwmechanics/aicombat.cpp | 10 +--------- apps/openmw/mwmechanics/character.cpp | 5 +++++ apps/openmw/mwmechanics/character.hpp | 2 ++ 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 88d4167ab..d8b2dc9ac 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -300,10 +300,6 @@ namespace MWMechanics 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); if (actionCooldown > 0) @@ -312,11 +308,7 @@ namespace MWMechanics float rangeAttack = 0; float rangeFollow = 0; boost::shared_ptr& currentAction = storage.mCurrentAction; - // TODO: upperBodyReady() works fine for checking if we can start an attack, - // 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()) + if (characterController.readyToPrepareAttack()) { currentAction = prepareNextAction(actor, target); actionCooldown = currentAction->getActionCooldown(); diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index a3d69c059..ec1e0e81b 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -2047,6 +2047,11 @@ void CharacterController::setAttackingOrSpell(bool attackingOrSpell) mAttackingOrSpell = attackingOrSpell; } +bool CharacterController::readyToPrepareAttack() const +{ + return mHitState == CharState_None && mUpperBodyState <= UpperCharState_WeapEquiped; +} + void CharacterController::setActive(bool active) { mAnimation->setActive(active); diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index 0a8771fb4..6a5e0593d 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -240,6 +240,8 @@ public: void setAttackingOrSpell(bool attackingOrSpell); + bool readyToPrepareAttack() const; + /// @see Animation::setActive void setActive(bool active); From 82740c164501a629ec08213cad2c49b52a919b33 Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 3 Jul 2015 02:29:42 +0200 Subject: [PATCH 3/7] Remove completely broken code --- apps/openmw/mwmechanics/aicombat.cpp | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index d8b2dc9ac..85a88e546 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -640,24 +640,6 @@ namespace MWMechanics } } - // 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; } From 741e5db862d23f845dd93b96605093055b31f236 Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 3 Jul 2015 05:37:18 +0200 Subject: [PATCH 4/7] Fix for non-bipedal creatures that use weapons, e.g. rieklings --- apps/openmw/mwmechanics/character.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index ec1e0e81b..cf963e8aa 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -1789,7 +1789,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; else forcestateupdate = updateCreatureState() || forcestateupdate; From a7bd050928b746386d5ba35425eb0d1c040a4896 Mon Sep 17 00:00:00 2001 From: scrawl Date: Fri, 3 Jul 2015 05:58:12 +0200 Subject: [PATCH 5/7] Accurate attack timings in AiCombat --- apps/openmw/mwmechanics/aicombat.cpp | 169 +++----------------------- apps/openmw/mwmechanics/character.cpp | 18 +++ apps/openmw/mwmechanics/character.hpp | 4 +- 3 files changed, 36 insertions(+), 155 deletions(-) diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 85a88e546..c0d2bc90d 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -31,8 +31,6 @@ namespace //chooses an attack depending on probability to avoid uniformity 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, float duration, int weapType, float strength); @@ -83,7 +81,6 @@ namespace MWMechanics /// \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; @@ -95,15 +92,12 @@ namespace MWMechanics boost::shared_ptr mCurrentAction; float mActionCooldown; float mStrength; - float mMinMaxAttackDuration[3][2]; - bool mMinMaxAttackDurationInitialised; bool mForceNoShortcut; ESM::Position mShortcutFailPos; osg::Vec3f mLastActorPos; MWMechanics::Movement mMovement; AiCombatStorage(): - mTimerAttack(0), mTimerReact(0), mTimerCombatMove(0), mReadyToAttack(false), @@ -115,7 +109,6 @@ namespace MWMechanics mCurrentAction(), mActionCooldown(0), mStrength(), - mMinMaxAttackDurationInitialised(false), mForceNoShortcut(false), mLastActorPos(0,0,0), mMovement(){} @@ -233,41 +226,12 @@ namespace MWMechanics { 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& 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; - - timerAttack -= duration; - } - else - { - timerAttack = -attacksPeriod; + if (attack && (characterController.getAttackStrength() >= storage.mStrength || characterController.readyToPrepareAttack())) attack = false; - } characterController.setAttackingOrSpell(attack); @@ -372,32 +336,26 @@ namespace MWMechanics } - float& strength = storage.mStrength; + float& strength = storage.mStrength; // start new attack - if(readyToAttack) + if(readyToAttack && characterController.readyToStartAttack()) { - if(timerAttack <= -attacksPeriod) - { - attack = true; // attack starts just now - - if (!distantCombat) attackType = chooseBestAttack(weapon, movement); - else attackType = ESM::Weapon::AT_Chop; // cause it's =0 + attack = true; // attack starts just now + characterController.setAttackingOrSpell(attack); - strength = Misc::Rng::rollClosedProbability(); + if (!distantCombat) + chooseBestAttack(weapon, movement); - // Note: may be 0 for some animations - timerAttack = minMaxAttackDuration[attackType][0] + - (minMaxAttackDuration[attackType][1] - minMaxAttackDuration[attackType][0]) * strength; + strength = Misc::Rng::rollClosedProbability(); - //say a provoking combat phrase - if (actor.getClass().isNpc()) + //say a provoking combat phrase + if (actor.getClass().isNpc()) + { + const MWWorld::ESMStore &store = world->getStore(); + int chance = store.get().find("iVoiceAttackOdds")->getInt(); + if (Misc::Rng::roll0to99() < chance) { - const MWWorld::ESMStore &store = world->getStore(); - int chance = store.get().find("iVoiceAttackOdds")->getInt(); - if (Misc::Rng::roll0to99() < chance) - { - MWBase::Environment::get().getDialogueManager()->say(actor, "attack"); - } + MWBase::Environment::get().getDialogueManager()->say(actor, "attack"); } } } @@ -607,39 +565,6 @@ namespace MWMechanics 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; - } - } - } - } - return false; } @@ -765,70 +690,6 @@ ESM::Weapon::AttackType chooseBestAttack(const ESM::Weapon* weapon, MWMechanics: 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()->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, float duration, int weapType, float strength) { diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index cf963e8aa..4b2ce9f4c 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -1259,6 +1259,8 @@ bool CharacterController::updateWeaponState() } animPlaying = mAnimation->getInfo(mCurrentWeapon, &complete); + if(mUpperBodyState == UpperCharState_MinAttackToMaxAttack && mHitState != CharState_KnockDown) + mAttackStrength = complete; } else { @@ -2053,6 +2055,22 @@ 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) { mAnimation->setActive(active); diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index 6a5e0593d..b239b4a92 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -241,6 +241,9 @@ public: void setAttackingOrSpell(bool attackingOrSpell); bool readyToPrepareAttack() const; + bool readyToStartAttack() const; + + float getAttackStrength() const; /// @see Animation::setActive void setActive(bool active); @@ -249,7 +252,6 @@ public: void setHeadTrackTarget(const MWWorld::Ptr& target); }; - void getWeaponGroup(WeaponType weaptype, std::string &group); MWWorld::ContainerStoreIterator getActiveWeapon(CreatureStats &stats, MWWorld::InventoryStore &inv, WeaponType *weaptype); } From 6bcea21e144467b0e0cc2709341e66917aab49f9 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sat, 4 Jul 2015 05:19:05 +0200 Subject: [PATCH 6/7] Use fCombatDelayCreature, fCombatDelayNpc for random delays between aicombat attacks (Bug #1876) --- apps/openmw/mwmechanics/aicombat.cpp | 34 +++++++++++++++++++--------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index c0d2bc90d..4eeea6f1f 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -81,6 +81,7 @@ namespace MWMechanics /// \brief This class holds the variables AiCombat needs which are deleted if the package becomes inactive. struct AiCombatStorage : AiTemporaryBase { + float mAttackCooldown; float mTimerReact; float mTimerCombatMove; bool mReadyToAttack; @@ -98,6 +99,7 @@ namespace MWMechanics MWMechanics::Movement mMovement; AiCombatStorage(): + mAttackCooldown(0), mTimerReact(0), mTimerCombatMove(0), mReadyToAttack(false), @@ -340,24 +342,34 @@ namespace MWMechanics // start new attack if(readyToAttack && characterController.readyToStartAttack()) { - attack = true; // attack starts just now - characterController.setAttackingOrSpell(attack); + if (storage.mAttackCooldown <= 0) + { + attack = true; // attack starts just now + characterController.setAttackingOrSpell(attack); - if (!distantCombat) - chooseBestAttack(weapon, movement); + if (!distantCombat) + chooseBestAttack(weapon, movement); - strength = Misc::Rng::rollClosedProbability(); + strength = Misc::Rng::rollClosedProbability(); - //say a provoking combat phrase - if (actor.getClass().isNpc()) - { const MWWorld::ESMStore &store = world->getStore(); - int chance = store.get().find("iVoiceAttackOdds")->getInt(); - if (Misc::Rng::roll0to99() < chance) + + //say a provoking combat phrase + if (actor.getClass().isNpc()) { - MWBase::Environment::get().getDialogueManager()->say(actor, "attack"); + int chance = store.get().find("iVoiceAttackOdds")->getInt(); + if (Misc::Rng::roll0to99() < chance) + { + MWBase::Environment::get().getDialogueManager()->say(actor, "attack"); + } } + float baseDelay = store.get().find("fCombatDelayCreature")->getFloat(); + if (actor.getClass().isNpc()) + baseDelay = store.get().find("fCombatDelayNPC")->getFloat(); + storage.mAttackCooldown = std::min(baseDelay + 0.01 * Misc::Rng::roll0to99(), baseDelay + 0.9); } + else + storage.mAttackCooldown -= tReaction; } From c90841d64952cc245c1e8f10d7e61fbef78d01b1 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sat, 4 Jul 2015 21:00:01 +0200 Subject: [PATCH 7/7] Fix head tracking for NPCs --- apps/openmw/mwrender/npcanimation.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index becfd8f6e..6252d392b 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -850,7 +850,6 @@ void NpcAnimation::addControllers() Animation::addControllers(); mFirstPersonNeckController = NULL; - mHeadController = NULL; WeaponAnimation::deleteControllers(); if (mViewMode == VM_FirstPerson)