From 1d4be08f6e4c2dbd89cc0c3408a8231ee4497277 Mon Sep 17 00:00:00 2001 From: mrcheko Date: Sun, 31 May 2015 19:19:14 +0300 Subject: [PATCH] refactor: move combat move and attack functions to AiCombatStorage --- apps/openmw/mwmechanics/aicombat.cpp | 682 ++++++++++++++------------- 1 file changed, 352 insertions(+), 330 deletions(-) diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 2f68087e58..735e1b6dc8 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -27,14 +27,15 @@ #include "aicombataction.hpp" #include "combat.hpp" +// forward declarations namespace { - - //chooses an attack depending on probability to avoid uniformity + /// Choose an attack depending on probability to avoid uniformity ESM::Weapon::AttackType chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement); - + /// Get an attack min/max attack duration for current actor's weapon void getMinMaxAttackDuration(const MWWorld::Ptr& actor, float (*fMinMaxDurations)[2]); + /// Calculate direction to aim at the moving target Ogre::Vector3 AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const Ogre::Vector3& vLastTargetPos, float duration, int weapType, float strength); @@ -49,34 +50,15 @@ namespace return -Ogre::Math::ASin(dir.z / len).valueDegrees(); } - 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 Ogre::Vector3& from, const Ogre::Vector3& to, float offsetXY) - { - if((to - from).length() >= PATHFIND_CAUTION_DIST || std::abs(from.z - to.z) <= PATHFIND_Z_REACH) - { - Ogre::Vector3 dir = to - from; - dir.z = 0; - dir.normalise(); - float verticalOffset = 200; // instead of '200' here we want the height of the actor - Ogre::Vector3 _from = from + dir*offsetXY + Ogre::Vector3::UNIT_Z * verticalOffset; - - // cast up-down ray and find height in world space of hit - float h = _from.z - MWBase::Environment::get().getWorld()->getDistToNearestRayHit(_from, -Ogre::Vector3::UNIT_Z, verticalOffset + PATHFIND_Z_REACH + 1); - - if(std::abs(from.z - h) <= PATHFIND_Z_REACH) - return true; - } - - return false; - } + /// Cast up-down ray to fo pits/obstacles on the way + /// \note magnitude of pits/obstacles is defined by PATHFIND_Z_REACH + bool checkWayIsClear(const Ogre::Vector3& from, const Ogre::Vector3& to, float offsetXY); } namespace MWMechanics @@ -108,24 +90,36 @@ namespace MWMechanics 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(){} + 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() + {} + + void startCombatMove(bool isNpc, bool isDistantCombat, float distToTarget, float rangeAttack); + void updateCombatMove(float duration); + void stopCombatMove(); + + void updateWeaponAttackDurations(const MWWorld::Ptr& actor); + void updateAttack(float duration, float attacksPeriod); + bool startAttack(bool isDistantCombat, const ESM::Weapon* weapon, float attacksPeriod); + + /// Start special run attack taking into account actor-target speeds and weapon characteristics + void startRunAttack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float attacksPeriod, float distToTarget, float weapRange); }; - + AiCombat::AiCombat(const MWWorld::Ptr& actor) : mTargetActorId(actor.getClass().getCreatureStats(actor).getActorId()) {} @@ -136,9 +130,7 @@ namespace MWMechanics } void AiCombat::init() - { - - } + {} /* * Current AiCombat movement states (as of 0.29.0), ignoring the details of the @@ -191,8 +183,6 @@ namespace MWMechanics // get or create temporary storage AiCombatStorage& storage = state.get(); - - //General description if(actor.getClass().getCreatureStats(actor).isDead()) return true; @@ -209,21 +199,10 @@ namespace MWMechanics const MWWorld::Class& actorClass = actor.getClass(); MWBase::World* world = MWBase::Environment::get().getWorld(); - //Update every frame - bool& combatMove = storage.mCombatMove; - float& timerCombatMove = storage.mTimerCombatMove; + storage.updateCombatMove(duration); + MWMechanics::Movement& movement = storage.mMovement; - if(combatMove) - { - timerCombatMove -= duration; - if( timerCombatMove <= 0) - { - timerCombatMove = 0; - movement.mPosition[1] = movement.mPosition[0] = 0; - combatMove = false; - } - } actorClass.getMovementSettings(actor) = movement; actorClass.getMovementSettings(actor).mRotation[0] = 0; @@ -242,39 +221,12 @@ namespace MWMechanics //TODO: Some skills affect period of strikes.For berserk-like style period ~ 0.25f 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 - getMinMaxAttackDuration(actor, minMaxAttackDuration); - minMaxAttackDurationInitialised = true; - } - - if (timerAttack < 0) attack = false; - - timerAttack -= duration; - } - else - { - timerAttack = -attacksPeriod; - attack = false; - } - - actorClass.getCreatureStats(actor).setAttackingOrSpell(attack); - + // update attack state + storage.updateWeaponAttackDurations(actor); + storage.updateAttack(duration, attacksPeriod); + actorClass.getCreatureStats(actor).setAttackingOrSpell(storage.mAttack); float& actionCooldown = storage.mActionCooldown; actionCooldown -= duration; @@ -383,38 +335,20 @@ namespace MWMechanics weapRange = 150.f; } - - float& strength = storage.mStrength; // start new attack - if(readyToAttack) + bool newAttack = storage.startAttack(distantCombat, weapon, attacksPeriod); + + if (newAttack && actor.getClass().isNpc()) { - if(timerAttack <= -attacksPeriod) + //say a provoking combat phrase + const MWWorld::ESMStore &store = world->getStore(); + int chance = store.get().find("iVoiceAttackOdds")->getInt(); + if (OEngine::Misc::Rng::roll0to99() < chance) { - attack = true; // attack starts just now - - if (!distantCombat) attackType = chooseBestAttack(weapon, movement); - else attackType = ESM::Weapon::AT_Chop; // cause it's =0 - - strength = OEngine::Misc::Rng::rollClosedProbability(); - - // Note: may be 0 for some animations - timerAttack = minMaxAttackDuration[attackType][0] + - (minMaxAttackDuration[attackType][1] - minMaxAttackDuration[attackType][0]) * strength; - - //say a provoking combat phrase - if (actor.getClass().isNpc()) - { - const MWWorld::ESMStore &store = world->getStore(); - int chance = store.get().find("iVoiceAttackOdds")->getInt(); - if (OEngine::Misc::Rng::roll0to99() < chance) - { - MWBase::Environment::get().getDialogueManager()->say(actor, "attack"); - } - } + MWBase::Environment::get().getDialogueManager()->say(actor, "attack"); } } - /* * Some notes on meanings of variables: * @@ -492,7 +426,7 @@ namespace MWMechanics if (distantCombat) { Ogre::Vector3& lastTargetPos = storage.mLastTargetPos; - Ogre::Vector3 vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, tReaction, weaptype, strength); + Ogre::Vector3 vAimDir = AimDirToMovingTarget(actor, target, lastTargetPos, tReaction, weaptype, storage.mStrength); lastTargetPos = vTargetPos; movement.mRotation[0] = getXAngleToDir(vAimDir); movement.mRotation[2] = getZAngleToDir(vAimDir); @@ -507,31 +441,12 @@ namespace MWMechanics if (followTarget && distToTarget > rangeAttack) { //Close-up combat: just run up on target + storage.stopCombatMove(); movement.mPosition[1] = 1; } else // (within attack dist) { - if(movement.mPosition[0] || movement.mPosition[1]) - { - timerCombatMove = 0.1f + 0.1f * OEngine::Misc::Rng::rollClosedProbability(); - combatMove = true; - } - // only NPCs are smart enough to use dodge movements - else if(actorClass.isNpc() && (!distantCombat || (distantCombat && distToTarget < rangeAttack/2))) - { - //apply sideway movement (kind of dodging) with some probability - if (OEngine::Misc::Rng::rollClosedProbability() < 0.25) - { - movement.mPosition[0] = OEngine::Misc::Rng::rollProbability() < 0.5 ? 1.0f : -1.0f; - timerCombatMove = 0.05f + 0.15f * OEngine::Misc::Rng::rollClosedProbability(); - combatMove = true; - } - } - - if(distantCombat && distToTarget < rangeAttack/4) - { - movement.mPosition[1] = -1; - } + storage.startCombatMove(actorClass.isNpc(), distantCombat, distToTarget, rangeAttack); readyToAttack = true; //only once got in melee combat, actor is allowed to use close-up shortcutting @@ -609,52 +524,24 @@ namespace MWMechanics } } - movement.mPosition[1] = 1; if (readyToAttack) { // to stop possible sideway moving after target moved out of attack range - combatMove = true; - timerCombatMove = 0; + storage.stopCombatMove(); + readyToAttack = false; } - readyToAttack = false; + movement.mPosition[1] = 1; } - if(!isStuck && distToTarget > rangeAttack && !distantCombat) + if (!isStuck && distToTarget > rangeAttack && !distantCombat) { - //special run attack; it shouldn't affect melee combat tactics - if(actorClass.getMovementSettings(actor).mPosition[1] == 1) + // 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]) * OEngine::Misc::Rng::rollClosedProbability(); - - if (t + s2/speed1 <= t_swing) - { - readyToAttack = true; - if(timerAttack <= -attacksPeriod) - { - timerAttack = t_swing; - attack = true; - } - } + storage.startRunAttack(actor, target, attacksPeriod, distToTarget, weapRange); } } - // NOTE: This section gets updated every tReaction, which is currently hard - // coded at 250ms or 1/4 second - // // TODO: Add a parameter to vary DURATION_SAME_SPOT? if((distToTarget > rangeAttack || followTarget) && mObstacleCheck.check(actor, tReaction)) // check if evasive action needed @@ -749,187 +636,322 @@ namespace MWMechanics package.mPackage = combat.release(); sequence.mPackages.push_back(package); } + + void AiCombatStorage::startCombatMove(bool isNpc, bool isDistantCombat, float distToTarget, float rangeAttack) + { + if (mMovement.mPosition[0] || mMovement.mPosition[1]) + { + mTimerCombatMove = 0.1f + 0.1f * OEngine::Misc::Rng::rollClosedProbability(); + mCombatMove = true; + } + // only NPCs are smart enough to use dodge movements + else if (isNpc && (!isDistantCombat || (isDistantCombat && distToTarget < rangeAttack/2))) + { + //apply sideway movement (kind of dodging) with some probability + if (OEngine::Misc::Rng::rollClosedProbability() < 0.25) + { + mMovement.mPosition[0] = OEngine::Misc::Rng::rollProbability() < 0.5 ? 1.0f : -1.0f; + mTimerCombatMove = 0.05f + 0.15f * OEngine::Misc::Rng::rollClosedProbability(); + mCombatMove = true; + } + } + + if (isDistantCombat && distToTarget < rangeAttack/4) + { + mMovement.mPosition[1] = -1; + } + } + + void AiCombatStorage::updateCombatMove(float duration) + { + if (!mCombatMove) return; + + mTimerCombatMove -= duration; + if (mTimerCombatMove <= 0) + { + stopCombatMove(); + } + } + + void AiCombatStorage::stopCombatMove() + { + mCombatMove = false; + mTimerCombatMove = 0; + mMovement.mPosition[1] = mMovement.mPosition[0] = 0; + } + + void AiCombatStorage::updateWeaponAttackDurations(const MWWorld::Ptr& actor) + { + if (mReadyToAttack && !mMinMaxAttackDurationInitialised) + { + // TODO: this must be updated when a different weapon is equipped + getMinMaxAttackDuration(actor, mMinMaxAttackDuration); + mMinMaxAttackDurationInitialised = true; + } + } + + void AiCombatStorage::updateAttack(float duration, float attacksPeriod) + { + if (mReadyToAttack) + { + if (mTimerAttack < 0) mAttack = false; + mTimerAttack -= duration; + } + else + { + mTimerAttack = -attacksPeriod; + mAttack = false; + } + } + + bool AiCombatStorage::startAttack(bool isDistantCombat, const ESM::Weapon* weapon, float attacksPeriod) + { + if (!mReadyToAttack || mTimerAttack > -attacksPeriod) + return false; + + // start new attack + mAttack = true; + + ESM::Weapon::AttackType attackType; + + if (!isDistantCombat) + attackType = chooseBestAttack(weapon, mMovement); + else + attackType = ESM::Weapon::AT_Chop; // cause it's =0 + + mStrength = OEngine::Misc::Rng::rollClosedProbability(); + + // Note: may be 0 for some animations + mTimerAttack = mMinMaxAttackDuration[attackType][0] + + (mMinMaxAttackDuration[attackType][1] - mMinMaxAttackDuration[attackType][0]) * mStrength; + + return true; + } + + void AiCombatStorage::startRunAttack(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, float attacksPeriod, float distToTarget, float weapRange) + { + float actorSpeed = actor.getClass().getSpeed(actor); + float targetSpeed = target.getClass().getSpeed(target); + + if (target.getClass().getMovementSettings(target).mPosition[0] == 0 + && target.getClass().getMovementSettings(target).mPosition[1] == 0) + targetSpeed = 0; + + float distToOvercome = distToTarget - weapRange; + float t_toOvercome = distToOvercome/actorSpeed; + float distTargetWillOvercome = targetSpeed * t_toOvercome; + float t_weap_swing = mMinMaxAttackDuration[ESM::Weapon::AT_Thrust][0] + + (mMinMaxAttackDuration[ESM::Weapon::AT_Thrust][1] - mMinMaxAttackDuration[ESM::Weapon::AT_Thrust][0]) * OEngine::Misc::Rng::rollClosedProbability(); + + if (t_toOvercome + distTargetWillOvercome/actorSpeed <= t_weap_swing) + { + mReadyToAttack = true; + if (mTimerAttack <= -attacksPeriod) + { + mTimerAttack = t_weap_swing; + mAttack = true; + } + } + } } namespace { - -ESM::Weapon::AttackType chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement) -{ - ESM::Weapon::AttackType attackType; - - if (weapon == NULL) + ESM::Weapon::AttackType chooseBestAttack(const ESM::Weapon* weapon, MWMechanics::Movement &movement) { - //hand-to-hand deal equal damage for each type - float roll = OEngine::Misc::Rng::rollClosedProbability(); - if(roll <= 0.333f) //side punch + ESM::Weapon::AttackType attackType; + + if (weapon == NULL) { - movement.mPosition[0] = OEngine::Misc::Rng::rollClosedProbability() ? 1.0f : -1.0f; - movement.mPosition[1] = 0; - attackType = ESM::Weapon::AT_Slash; - } - else if(roll <= 0.666f) //forward punch - { - movement.mPosition[1] = 1; - attackType = ESM::Weapon::AT_Thrust; + //hand-to-hand deal equal damage for each type + float roll = OEngine::Misc::Rng::rollClosedProbability(); + if(roll <= 0.333f) //side punch + { + movement.mPosition[0] = OEngine::Misc::Rng::rollClosedProbability() ? 1.0f : -1.0f; + movement.mPosition[1] = 0; + attackType = ESM::Weapon::AT_Slash; + } + else if(roll <= 0.666f) //forward punch + { + movement.mPosition[1] = 1; + attackType = ESM::Weapon::AT_Thrust; + } + else + { + movement.mPosition[1] = movement.mPosition[0] = 0; + attackType = ESM::Weapon::AT_Chop; + } } else { - movement.mPosition[1] = movement.mPosition[0] = 0; - attackType = ESM::Weapon::AT_Chop; + //the more damage attackType deals the more probability it has + int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2; + int chop = (weapon->mData.mChop[0] + weapon->mData.mChop[1])/2; + int thrust = (weapon->mData.mThrust[0] + weapon->mData.mThrust[1])/2; + + float total = static_cast(slash + chop + thrust); + + float roll = OEngine::Misc::Rng::rollClosedProbability(); + if(roll <= (slash/total)) + { + movement.mPosition[0] = (OEngine::Misc::Rng::rollClosedProbability() < 0.5f) ? 1.0f : -1.0f; + movement.mPosition[1] = 0; + attackType = ESM::Weapon::AT_Slash; + } + else if(roll <= (slash + (thrust/total))) + { + movement.mPosition[1] = 1; + attackType = ESM::Weapon::AT_Thrust; + } + else + { + movement.mPosition[1] = movement.mPosition[0] = 0; + attackType = ESM::Weapon::AT_Chop; + } } + + return attackType; } - else + + void getMinMaxAttackDuration(const MWWorld::Ptr& actor, float (*fMinMaxDurations)[2]) { - //the more damage attackType deals the more probability it has - int slash = (weapon->mData.mSlash[0] + weapon->mData.mSlash[1])/2; - int chop = (weapon->mData.mChop[0] + weapon->mData.mChop[1])/2; - int thrust = (weapon->mData.mThrust[0] + weapon->mData.mThrust[1])/2; - - float total = static_cast(slash + chop + thrust); - - float roll = OEngine::Misc::Rng::rollClosedProbability(); - if(roll <= (slash/total)) + if (!actor.getClass().hasInventoryStore(actor)) // creatures { - movement.mPosition[0] = (OEngine::Misc::Rng::rollClosedProbability() < 0.5f) ? 1.0f : -1.0f; - movement.mPosition[1] = 0; - attackType = ESM::Weapon::AT_Slash; + 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; } - else if(roll <= (slash + (thrust/total))) + + // 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) { - movement.mPosition[1] = 1; - attackType = ESM::Weapon::AT_Thrust; + 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; + } + + } + + Ogre::Vector3 AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const Ogre::Vector3& vLastTargetPos, + float duration, int weapType, float strength) + { + float projSpeed; + + // get projectile speed (depending on weapon type) + if (weapType == ESM::Weapon::MarksmanThrown) + { + static float fThrownWeaponMinSpeed = + MWBase::Environment::get().getWorld()->getStore().get().find("fThrownWeaponMinSpeed")->getFloat(); + static float fThrownWeaponMaxSpeed = + MWBase::Environment::get().getWorld()->getStore().get().find("fThrownWeaponMaxSpeed")->getFloat(); + + projSpeed = + fThrownWeaponMinSpeed + (fThrownWeaponMaxSpeed - fThrownWeaponMinSpeed) * strength; } else { - movement.mPosition[1] = movement.mPosition[0] = 0; - attackType = ESM::Weapon::AT_Chop; + static float fProjectileMinSpeed = + MWBase::Environment::get().getWorld()->getStore().get().find("fProjectileMinSpeed")->getFloat(); + static float fProjectileMaxSpeed = + MWBase::Environment::get().getWorld()->getStore().get().find("fProjectileMaxSpeed")->getFloat(); + + projSpeed = + fProjectileMinSpeed + (fProjectileMaxSpeed - fProjectileMinSpeed) * strength; } + + // idea: perpendicular to dir to target speed components of target move vector and projectile vector should be the same + + Ogre::Vector3 vActorPos = Ogre::Vector3(actor.getRefData().getPosition().pos); + Ogre::Vector3 vTargetPos = Ogre::Vector3(target.getRefData().getPosition().pos); + Ogre::Vector3 vDirToTarget = vTargetPos - vActorPos; + float distToTarget = vDirToTarget.length(); + + Ogre::Vector3 vTargetMoveDir = vTargetPos - vLastTargetPos; + vTargetMoveDir /= duration; // |vTargetMoveDir| is target real speed in units/sec now + + Ogre::Vector3 vPerpToDir = vDirToTarget.crossProduct(Ogre::Vector3::UNIT_Z); + + float velPerp = vTargetMoveDir.dotProduct(vPerpToDir.normalisedCopy()); + float velDir = vTargetMoveDir.dotProduct(vDirToTarget.normalisedCopy()); + + // time to collision between target and projectile + float t_collision; + + float projVelDirSquared = projSpeed * projSpeed - velPerp * velPerp; + float projDistDiff = vDirToTarget.dotProduct(vTargetMoveDir.normalisedCopy()); + projDistDiff = sqrt(distToTarget * distToTarget - projDistDiff * projDistDiff); + + if (projVelDirSquared > 0) + t_collision = projDistDiff / (sqrt(projVelDirSquared) - velDir); + else t_collision = 0; // speed of projectile is not enough to reach moving target + + return vTargetPos + vTargetMoveDir * t_collision - vActorPos; } - return attackType; -} - -void getMinMaxAttackDuration(const MWWorld::Ptr& actor, float (*fMinMaxDurations)[2]) -{ - if (!actor.getClass().hasInventoryStore(actor)) // creatures + bool checkWayIsClear(const Ogre::Vector3& from, const Ogre::Vector3& to, float offsetXY) { - 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) + if((to - from).length() >= PATHFIND_CAUTION_DIST || std::abs(from.z - to.z) <= PATHFIND_Z_REACH) { - fMinMaxDurations[i][0] = fMinMaxDurations[i][1] = 0.1f; - continue; + Ogre::Vector3 dir = to - from; + dir.z = 0; + dir.normalise(); + float verticalOffset = 200; // instead of '200' here we want the height of the actor + Ogre::Vector3 _from = from + dir*offsetXY + Ogre::Vector3::UNIT_Z * verticalOffset; + + // cast up-down ray and find height in world space of hit + float h = _from.z - MWBase::Environment::get().getWorld()->getDistToNearestRayHit(_from, -Ogre::Vector3::UNIT_Z, verticalOffset + PATHFIND_Z_REACH + 1); + + if(std::abs(from.z - h) <= PATHFIND_Z_REACH) + return true; } - 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; + return false; } - -} - -Ogre::Vector3 AimDirToMovingTarget(const MWWorld::Ptr& actor, const MWWorld::Ptr& target, const Ogre::Vector3& vLastTargetPos, - float duration, int weapType, float strength) -{ - float projSpeed; - - // get projectile speed (depending on weapon type) - if (weapType == ESM::Weapon::MarksmanThrown) - { - static float fThrownWeaponMinSpeed = - MWBase::Environment::get().getWorld()->getStore().get().find("fThrownWeaponMinSpeed")->getFloat(); - static float fThrownWeaponMaxSpeed = - MWBase::Environment::get().getWorld()->getStore().get().find("fThrownWeaponMaxSpeed")->getFloat(); - - projSpeed = - fThrownWeaponMinSpeed + (fThrownWeaponMaxSpeed - fThrownWeaponMinSpeed) * strength; - } - else - { - static float fProjectileMinSpeed = - MWBase::Environment::get().getWorld()->getStore().get().find("fProjectileMinSpeed")->getFloat(); - static float fProjectileMaxSpeed = - MWBase::Environment::get().getWorld()->getStore().get().find("fProjectileMaxSpeed")->getFloat(); - - projSpeed = - fProjectileMinSpeed + (fProjectileMaxSpeed - fProjectileMinSpeed) * strength; - } - - // idea: perpendicular to dir to target speed components of target move vector and projectile vector should be the same - - Ogre::Vector3 vActorPos = Ogre::Vector3(actor.getRefData().getPosition().pos); - Ogre::Vector3 vTargetPos = Ogre::Vector3(target.getRefData().getPosition().pos); - Ogre::Vector3 vDirToTarget = vTargetPos - vActorPos; - float distToTarget = vDirToTarget.length(); - - Ogre::Vector3 vTargetMoveDir = vTargetPos - vLastTargetPos; - vTargetMoveDir /= duration; // |vTargetMoveDir| is target real speed in units/sec now - - Ogre::Vector3 vPerpToDir = vDirToTarget.crossProduct(Ogre::Vector3::UNIT_Z); - - float velPerp = vTargetMoveDir.dotProduct(vPerpToDir.normalisedCopy()); - float velDir = vTargetMoveDir.dotProduct(vDirToTarget.normalisedCopy()); - - // time to collision between target and projectile - float t_collision; - - float projVelDirSquared = projSpeed * projSpeed - velPerp * velPerp; - float projDistDiff = vDirToTarget.dotProduct(vTargetMoveDir.normalisedCopy()); - projDistDiff = sqrt(distToTarget * distToTarget - projDistDiff * projDistDiff); - - if (projVelDirSquared > 0) - t_collision = projDistDiff / (sqrt(projVelDirSquared) - velDir); - else t_collision = 0; // speed of projectile is not enough to reach moving target - - return vTargetPos + vTargetMoveDir * t_collision - vActorPos; -} - }