diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 51c83eee1c..ff4ae61486 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -264,7 +264,7 @@ namespace MWClass if(Misc::Rng::roll0to99() >= hitchance) { - victim.getClass().onHit(victim, 0.0f, false, MWWorld::Ptr(), ptr, false); + victim.getClass().onHit(victim, 0.0f, false, MWWorld::Ptr(), ptr, osg::Vec3f(), false); MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); return; } @@ -318,15 +318,12 @@ namespace MWClass if (MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage, attackStrength)) damage = 0; - if (damage > 0) - MWBase::Environment::get().getWorld()->spawnBloodEffect(victim, hitPosition); - MWMechanics::diseaseContact(victim, ptr); - victim.getClass().onHit(victim, damage, healthdmg, weapon, ptr, true); + victim.getClass().onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true); } - void Creature::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, bool successful) const + void Creature::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const { // NOTE: 'object' and/or 'attacker' may be empty. @@ -342,10 +339,10 @@ namespace MWClass setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); } - if(!object.isEmpty()) + if (!object.isEmpty()) getCreatureStats(ptr).setLastHitAttemptObject(object.getCellRef().getRefId()); - if(setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer()) + if (setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer()) { const std::string &script = ptr.get()->mBase->mScript; /* Set the OnPCHitMe script variable. The script is responsible for clearing it. */ @@ -353,14 +350,14 @@ namespace MWClass ptr.getRefData().getLocals().setVarByInt(script, "onpchitme", 1); } - if(!successful) + if (!successful) { // Missed MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "miss", 1.0f, 1.0f); return; } - if(!object.isEmpty()) + if (!object.isEmpty()) getCreatureStats(ptr).setLastHitObject(object.getCellRef().getRefId()); if (damage > 0.0f && !object.isEmpty()) @@ -391,7 +388,10 @@ namespace MWClass if(ishealth) { if (!attacker.isEmpty()) + { damage = scaleDamage(damage, attacker, ptr); + MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition); + } MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "Health Damage", 1.0f, 1.0f); diff --git a/apps/openmw/mwclass/creature.hpp b/apps/openmw/mwclass/creature.hpp index bea56887a0..654df60b60 100644 --- a/apps/openmw/mwclass/creature.hpp +++ b/apps/openmw/mwclass/creature.hpp @@ -58,7 +58,7 @@ namespace MWClass virtual void hit(const MWWorld::Ptr& ptr, float attackStrength, int type) const; - virtual void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, bool successful) const; + virtual void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const; virtual boost::shared_ptr activate (const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const; diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 98fdd76713..52debfb346 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -591,7 +591,7 @@ namespace MWClass if (Misc::Rng::roll0to99() >= hitchance) { - othercls.onHit(victim, 0.0f, false, weapon, ptr, false); + othercls.onHit(victim, 0.0f, false, weapon, ptr, osg::Vec3f(), false); MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr); return; } @@ -646,15 +646,12 @@ namespace MWClass if (MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage, attackStrength)) damage = 0; - if (healthdmg && damage > 0) - MWBase::Environment::get().getWorld()->spawnBloodEffect(victim, hitPosition); - MWMechanics::diseaseContact(victim, ptr); - othercls.onHit(victim, damage, healthdmg, weapon, ptr, true); + othercls.onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true); } - void Npc::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, bool successful) const + void Npc::onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const { MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); @@ -671,10 +668,10 @@ namespace MWClass setOnPcHitMe = MWBase::Environment::get().getMechanicsManager()->actorAttacked(ptr, attacker); } - if(!object.isEmpty()) + if (!object.isEmpty()) getCreatureStats(ptr).setLastHitAttemptObject(object.getCellRef().getRefId()); - if(setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer()) + if (setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer()) { const std::string &script = ptr.getClass().getScript(ptr); /* Set the OnPCHitMe script variable. The script is responsible for clearing it. */ @@ -682,14 +679,14 @@ namespace MWClass ptr.getRefData().getLocals().setVarByInt(script, "onpchitme", 1); } - if(!successful) + if (!successful) { // Missed sndMgr->playSound3D(ptr, "miss", 1.0f, 1.0f); return; } - if(!object.isEmpty()) + if (!object.isEmpty()) getCreatureStats(ptr).setLastHitObject(object.getCellRef().getRefId()); @@ -699,7 +696,7 @@ namespace MWClass if (damage < 0.001f) damage = 0; - if(damage > 0.0f && !attacker.isEmpty()) + if (damage > 0.0f && !attacker.isEmpty()) { // 'ptr' is losing health. Play a 'hit' voiced dialog entry if not already saying // something, alert the character controller, scripts, etc. @@ -725,7 +722,7 @@ namespace MWClass else getCreatureStats(ptr).setHitRecovery(true); // Is this supposed to always occur? - if(damage > 0 && ishealth) + if (damage > 0 && ishealth) { // Hit percentages: // cuirass = 30% @@ -787,16 +784,18 @@ namespace MWClass } } - if(ishealth) + if (ishealth) { if (!attacker.isEmpty()) damage = scaleDamage(damage, attacker, ptr); - if(damage > 0.0f) + if (damage > 0.0f) { sndMgr->playSound3D(ptr, "Health Damage", 1.0f, 1.0f); if (ptr == MWMechanics::getPlayer()) MWBase::Environment::get().getWindowManager()->activateHitOverlay(); + if (!attacker.isEmpty()) + MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition); } MWMechanics::DynamicStat health(getCreatureStats(ptr).getHealth()); health.setCurrent(health.getCurrent() - damage); diff --git a/apps/openmw/mwclass/npc.hpp b/apps/openmw/mwclass/npc.hpp index 95edbd4089..9416051766 100644 --- a/apps/openmw/mwclass/npc.hpp +++ b/apps/openmw/mwclass/npc.hpp @@ -73,7 +73,7 @@ namespace MWClass virtual void hit(const MWWorld::Ptr& ptr, float attackStrength, int type) const; - virtual void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, bool successful) const; + virtual void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const; virtual void getModelsToPreload(const MWWorld::Ptr& ptr, std::vector& models) const; ///< Get a list of models to preload that this object may use (directly or indirectly). default implementation: list getModel(). diff --git a/apps/openmw/mwmechanics/aicombat.cpp b/apps/openmw/mwmechanics/aicombat.cpp index 379119f601..c821eac0d2 100644 --- a/apps/openmw/mwmechanics/aicombat.cpp +++ b/apps/openmw/mwmechanics/aicombat.cpp @@ -69,7 +69,7 @@ namespace MWMechanics mMovement() {} - void startCombatMove(bool isNpc, bool isDistantCombat, float distToTarget, float rangeAttack); + void startCombatMove(bool isDistantCombat, float distToTarget, float rangeAttack, const MWWorld::Ptr& actor, const MWWorld::Ptr& target); void updateCombatMove(float duration); void stopCombatMove(); void startAttackIfReady(const MWWorld::Ptr& actor, CharacterController& characterController, @@ -242,7 +242,7 @@ namespace MWMechanics if (storage.mReadyToAttack) { - storage.startCombatMove(actorClass.isNpc(), isRangedCombat, distToTarget, rangeAttack); + storage.startCombatMove(isRangedCombat, distToTarget, rangeAttack, actor, target); // start new attack storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat); @@ -306,7 +306,6 @@ namespace MWMechanics return MWBase::Environment::get().getWorld()->searchPtrViaActorId(mTargetActorId); } - AiCombat *MWMechanics::AiCombat::clone() const { return new AiCombat(*this); @@ -323,18 +322,39 @@ namespace MWMechanics sequence.mPackages.push_back(package); } - void AiCombatStorage::startCombatMove(bool isNpc, bool isDistantCombat, float distToTarget, float rangeAttack) + void AiCombatStorage::startCombatMove(bool isDistantCombat, float distToTarget, float rangeAttack, const MWWorld::Ptr& actor, const MWWorld::Ptr& target) { if (mMovement.mPosition[0] || mMovement.mPosition[1]) { mTimerCombatMove = 0.1f + 0.1f * Misc::Rng::rollClosedProbability(); mCombatMove = true; } - // dodge movements (for NPCs only) - else if (isNpc && (!isDistantCombat || (distToTarget < rangeAttack / 2))) + // dodge movements (for NPCs and bipedal creatures) + else if (actor.getClass().isBipedal(actor)) { - //apply sideway movement (kind of dodging) with some probability - if (Misc::Rng::rollClosedProbability() < 0.25) + // get the range of the target's weapon + float rangeAttackOfTarget = 0.f; + bool isRangedCombat = false; + MWWorld::Ptr targetWeapon = MWWorld::Ptr(); + const MWWorld::Class& targetClass = target.getClass(); + + if (targetClass.hasInventoryStore(target)) + { + MWMechanics::WeaponType weapType = WeapType_None; + MWWorld::ContainerStoreIterator weaponSlot = + MWMechanics::getActiveWeapon(targetClass.getCreatureStats(target), targetClass.getInventoryStore(target), &weapType); + if (weapType != WeapType_PickProbe && weapType != WeapType_Spell && weapType != WeapType_None && weapType != WeapType_HandToHand) + targetWeapon = *weaponSlot; + } + + boost::shared_ptr targetWeaponAction (new ActionWeapon(targetWeapon)); + + if (targetWeaponAction.get()) + rangeAttackOfTarget = targetWeaponAction->getCombatRange(isRangedCombat); + + // apply sideway movement (kind of dodging) with some probability + // if actor is within range of target's weapon + if (distToTarget <= rangeAttackOfTarget && Misc::Rng::rollClosedProbability() < 0.25) { mMovement.mPosition[0] = Misc::Rng::rollProbability() < 0.5 ? 1.0f : -1.0f; // to the left/right mTimerCombatMove = 0.05f + 0.15f * Misc::Rng::rollClosedProbability(); @@ -342,6 +362,9 @@ namespace MWMechanics } } + // Original engine behavior seems to be to back up during ranged combat + // according to fCombatDistance or opponent's weapon range, unless opponent + // is also using a ranged weapon if (isDistantCombat && distToTarget < rangeAttack / 4) { mMovement.mPosition[1] = -1; diff --git a/apps/openmw/mwmechanics/aicombataction.cpp b/apps/openmw/mwmechanics/aicombataction.cpp index a704100357..094df1db37 100644 --- a/apps/openmw/mwmechanics/aicombataction.cpp +++ b/apps/openmw/mwmechanics/aicombataction.cpp @@ -462,8 +462,6 @@ namespace MWMechanics void ActionWeapon::prepare(const MWWorld::Ptr &actor) { - mIsNpc = actor.getClass().isNpc(); - if (actor.getClass().hasInventoryStore(actor)) { if (mWeapon.isEmpty()) @@ -491,17 +489,9 @@ namespace MWMechanics if (mWeapon.isEmpty()) { - if (!mIsNpc) - { - return fCombatDistance; - } - else - { - static float fHandToHandReach = - MWBase::Environment::get().getWorld()->getStore().get().find("fHandToHandReach")->getFloat(); - - return fHandToHandReach * fCombatDistance; - } + static float fHandToHandReach = + MWBase::Environment::get().getWorld()->getStore().get().find("fHandToHandReach")->getFloat(); + return fHandToHandReach * fCombatDistance; } const ESM::Weapon* weapon = mWeapon.get()->mBase; diff --git a/apps/openmw/mwmechanics/aicombataction.hpp b/apps/openmw/mwmechanics/aicombataction.hpp index e4ce443464..c280d3c118 100644 --- a/apps/openmw/mwmechanics/aicombataction.hpp +++ b/apps/openmw/mwmechanics/aicombataction.hpp @@ -63,7 +63,6 @@ namespace MWMechanics private: MWWorld::Ptr mAmmunition; MWWorld::Ptr mWeapon; - bool mIsNpc; public: /// \a weapon may be empty for hand-to-hand combat diff --git a/apps/openmw/mwmechanics/aipackage.cpp b/apps/openmw/mwmechanics/aipackage.cpp index 9f6ef2597d..abdc570423 100644 --- a/apps/openmw/mwmechanics/aipackage.cpp +++ b/apps/openmw/mwmechanics/aipackage.cpp @@ -19,10 +19,13 @@ #include "actorutil.hpp" #include "coordinateconverter.hpp" +#include + MWMechanics::AiPackage::~AiPackage() {} MWMechanics::AiPackage::AiPackage() : mTimer(AI_REACTION_TIME + 1.0f), // to force initial pathbuild + mRotateOnTheRunChecks(0), mIsShortcutting(false), mShortcutProhibited(false), mShortcutFailPos() { @@ -104,6 +107,7 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgr if (wasShortcutting || doesPathNeedRecalc(dest, actor.getCell())) // if need to rebuild path { mPathFinder.buildSyncedPath(start, dest, actor.getCell()); + mRotateOnTheRunChecks = 3; // give priority to go directly on target if there is minimal opportunity if (destInLOS && mPathFinder.getPath().size() > 1) @@ -143,7 +147,13 @@ bool MWMechanics::AiPackage::pathTo(const MWWorld::Ptr& actor, const ESM::Pathgr } else { - actor.getClass().getMovementSettings(actor).mPosition[1] = 1; // run to target + if (mRotateOnTheRunChecks == 0 + || isReachableRotatingOnTheRun(actor, *mPathFinder.getPath().begin())) // to prevent circling around a path point + { + actor.getClass().getMovementSettings(actor).mPosition[1] = 1; // move to the target + if (mRotateOnTheRunChecks > 0) mRotateOnTheRunChecks--; + } + // handle obstacles on the way evadeObstacles(actor, duration, pos); } @@ -292,3 +302,32 @@ bool MWMechanics::AiPackage::isNearInactiveCell(const ESM::Position& actorPos) return false; } } + +bool MWMechanics::AiPackage::isReachableRotatingOnTheRun(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest) +{ + // get actor's shortest radius for moving in circle + float speed = actor.getClass().getSpeed(actor); + speed += speed * 0.1f; // 10% real speed inaccuracy + float radius = speed / MAX_VEL_ANGULAR_RADIANS; + + // get radius direction to the center + const float* rot = actor.getRefData().getPosition().rot; + osg::Quat quatRot(rot[0], -osg::X_AXIS, rot[1], -osg::Y_AXIS, rot[2], -osg::Z_AXIS); + osg::Vec3f dir = quatRot * osg::Y_AXIS; // actor's orientation direction is a tangent to circle + osg::Vec3f radiusDir = dir ^ osg::Z_AXIS; // radius is perpendicular to a tangent + radiusDir.normalize(); + radiusDir *= radius; + + // pick up the nearest center candidate + osg::Vec3f dest_ = PathFinder::MakeOsgVec3(dest); + osg::Vec3f pos = actor.getRefData().getPosition().asVec3(); + osg::Vec3f center1 = pos - radiusDir; + osg::Vec3f center2 = pos + radiusDir; + osg::Vec3f center = (center1 - dest_).length2() < (center2 - dest_).length2() ? center1 : center2; + + float distToDest = (center - dest_).length(); + + // if pathpoint is reachable for the actor rotating on the run: + // no points of actor's circle should be farther from the center than destination point + return (radius <= distToDest); +} diff --git a/apps/openmw/mwmechanics/aipackage.hpp b/apps/openmw/mwmechanics/aipackage.hpp index c0df76c327..4960c14c33 100644 --- a/apps/openmw/mwmechanics/aipackage.hpp +++ b/apps/openmw/mwmechanics/aipackage.hpp @@ -97,6 +97,9 @@ namespace MWMechanics bool isTargetMagicallyHidden(const MWWorld::Ptr& target); + /// Return if actor's rotation speed is sufficient to rotate to the destination pathpoint on the run. Otherwise actor should rotate while standing. + static bool isReachableRotatingOnTheRun(const MWWorld::Ptr& actor, const ESM::Pathgrid::Point& dest); + protected: /// Handles path building and shortcutting with obstacles avoiding /** \return If the actor has arrived at his destination **/ @@ -123,6 +126,8 @@ namespace MWMechanics osg::Vec3f mLastActorPos; + short mRotateOnTheRunChecks; // attempts to check rotation to the pathpoint on the run possibility + bool mIsShortcutting; // if shortcutting at the moment bool mShortcutProhibited; // shortcutting may be prohibited after unsuccessful attempt ESM::Pathgrid::Point mShortcutFailPos; // position of last shortcut fail diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index bb8929f8bf..e351229d0e 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -1771,7 +1771,7 @@ void CharacterController::update(float duration) float realHealthLost = static_cast(healthLost * (1.0f - 0.25f * fatigueTerm)); health.setCurrent(health.getCurrent() - realHealthLost); cls.getCreatureStats(mPtr).setHealth(health); - cls.onHit(mPtr, realHealthLost, true, MWWorld::Ptr(), MWWorld::Ptr(), true); + cls.onHit(mPtr, realHealthLost, true, MWWorld::Ptr(), MWWorld::Ptr(), osg::Vec3f(), true); const int acrobaticsSkill = cls.getSkill(mPtr, ESM::Skill::Acrobatics); if (healthLost > (acrobaticsSkill * fatigueTerm)) diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index df2793bcb7..b454f8e3a7 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -190,7 +190,7 @@ namespace MWMechanics if (Misc::Rng::roll0to99() >= getHitChance(attacker, victim, skillValue)) { - victim.getClass().onHit(victim, 0.0f, false, projectile, attacker, false); + victim.getClass().onHit(victim, 0.0f, false, projectile, attacker, osg::Vec3f(), false); MWMechanics::reduceWeaponCondition(0.f, false, weapon, attacker); return; } @@ -218,9 +218,6 @@ namespace MWMechanics if (weapon != projectile) appliedEnchantment = applyOnStrikeEnchantment(attacker, victim, projectile, hitPosition); - if (damage > 0) - MWBase::Environment::get().getWorld()->spawnBloodEffect(victim, hitPosition); - // Non-enchanted arrows shot at enemies have a chance to turn up in their inventory if (victim != getPlayer() && !appliedEnchantment) @@ -230,7 +227,7 @@ namespace MWMechanics victim.getClass().getContainerStore(victim).add(projectile, 1, victim); } - victim.getClass().onHit(victim, damage, true, projectile, attacker, true); + victim.getClass().onHit(victim, damage, true, projectile, attacker, hitPosition, true); } float getHitChance(const MWWorld::Ptr &attacker, const MWWorld::Ptr &victim, int skillValue) diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index 95f2d940fa..5024dfeec4 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -569,7 +569,7 @@ namespace MWMechanics // Notify the target actor they've been hit if (anyHarmfulEffect && target.getClass().isActor() && target != caster && !caster.isEmpty() && caster.getClass().isActor()) - target.getClass().onHit(target, 0.f, true, MWWorld::Ptr(), caster, true); + target.getClass().onHit(target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true); } bool CastSpell::applyInstantEffect(const MWWorld::Ptr &target, const MWWorld::Ptr &caster, const MWMechanics::EffectKey& effect, float magnitude) diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index ecfe7cb7c4..12b69c7cac 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -98,7 +98,7 @@ namespace MWWorld throw std::runtime_error("class cannot block"); } - void Class::onHit(const Ptr& ptr, float damage, bool ishealth, const Ptr& object, const Ptr& attacker, bool successful) const + void Class::onHit(const Ptr& ptr, float damage, bool ishealth, const Ptr& object, const Ptr& attacker, const osg::Vec3f& hitPosition, bool successful) const { throw std::runtime_error("class cannot be hit"); } diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index 23128ea9dd..01d20032bc 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -120,7 +120,7 @@ namespace MWWorld /// enums. ignored for creature attacks. /// (default implementation: throw an exception) - virtual void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, bool successful) const; + virtual void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, const osg::Vec3f &hitPosition, bool successful) const; ///< Alerts \a ptr that it's being hit for \a damage points to health if \a ishealth is /// true (else fatigue) by \a object (sword, arrow, etc). \a attacker specifies the /// actor responsible for the attack, and \a successful specifies if the hit is