diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 3b533b4168..41cc320ad6 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -74,7 +74,7 @@ add_openmw_dir (mwmechanics mechanicsmanagerimp stat character creaturestats magiceffects movement actors objects drawstate spells activespells npcstats aipackage aisequence alchemy aiwander aitravel aifollow aiescort aiactivate aicombat repair enchanting pathfinding security spellsuccess spellcasting - disease pickpocket levelledlist + disease pickpocket levelledlist combat ) add_openmw_dir (mwbase diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 20b37833a9..d0a9137608 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -30,6 +30,7 @@ #include "../mwworld/inventorystore.hpp" #include "../mwmechanics/npcstats.hpp" +#include "../mwmechanics/combat.hpp" namespace { @@ -325,8 +326,11 @@ namespace MWClass } } - // TODO: do not do this if the attack is blocked - MWBase::Environment::get().getWorld()->spawnBloodEffect(victim, hitPosition); + if (!weapon.isEmpty() && MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage)) + damage = 0; + + if (damage > 0) + MWBase::Environment::get().getWorld()->spawnBloodEffect(victim, hitPosition); victim.getClass().onHit(victim, damage, true, MWWorld::Ptr(), ptr, true); } @@ -355,31 +359,57 @@ namespace MWClass ptr.getRefData().getLocals().setVarByInt(script, "onpchitme", 1); } - // Check for knockdown - float agilityTerm = getCreatureStats(ptr).getAttribute(ESM::Attribute::Agility).getModified() * fKnockDownMult->getFloat(); - float knockdownTerm = getCreatureStats(ptr).getAttribute(ESM::Attribute::Agility).getModified() - * iKnockDownOddsMult->getInt() * 0.01 + iKnockDownOddsBase->getInt(); - int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] - if (ishealth && agilityTerm <= damage && knockdownTerm <= roll) + if (damage > 0.f) { - getCreatureStats(ptr).setKnockedDown(true); + // Check for knockdown + float agilityTerm = getCreatureStats(ptr).getAttribute(ESM::Attribute::Agility).getModified() * fKnockDownMult->getFloat(); + float knockdownTerm = getCreatureStats(ptr).getAttribute(ESM::Attribute::Agility).getModified() + * iKnockDownOddsMult->getInt() * 0.01 + iKnockDownOddsBase->getInt(); + int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] + if (ishealth && agilityTerm <= damage && knockdownTerm <= roll) + { + getCreatureStats(ptr).setKnockedDown(true); - } - else - getCreatureStats(ptr).setHitRecovery(true); // Is this supposed to always occur? + } + else + getCreatureStats(ptr).setHitRecovery(true); // Is this supposed to always occur? - if(ishealth) - { - if(damage > 0.0f) + if(ishealth) + { MWBase::Environment::get().getSoundManager()->playSound3D(ptr, "Health Damage", 1.0f, 1.0f); - float health = getCreatureStats(ptr).getHealth().getCurrent() - damage; - setActorHealth(ptr, health, attacker); + float health = getCreatureStats(ptr).getHealth().getCurrent() - damage; + setActorHealth(ptr, health, attacker); + } + else + { + MWMechanics::DynamicStat fatigue(getCreatureStats(ptr).getFatigue()); + fatigue.setCurrent(fatigue.getCurrent() - damage, true); + getCreatureStats(ptr).setFatigue(fatigue); + } } - else + } + + void Creature::block(const MWWorld::Ptr &ptr) const + { + MWWorld::InventoryStore& inv = getInventoryStore(ptr); + MWWorld::ContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); + if (shield == inv.end()) + return; + + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + switch(shield->getClass().getEquipmentSkill(*shield)) { - MWMechanics::DynamicStat fatigue(getCreatureStats(ptr).getFatigue()); - fatigue.setCurrent(fatigue.getCurrent() - damage, true); - getCreatureStats(ptr).setFatigue(fatigue); + case ESM::Skill::LightArmor: + sndMgr->playSound3D(ptr, "Light Armor Hit", 1.0f, 1.0f); + break; + case ESM::Skill::MediumArmor: + sndMgr->playSound3D(ptr, "Medium Armor Hit", 1.0f, 1.0f); + break; + case ESM::Skill::HeavyArmor: + sndMgr->playSound3D(ptr, "Heavy Armor Hit", 1.0f, 1.0f); + break; + default: + return; } } diff --git a/apps/openmw/mwclass/creature.hpp b/apps/openmw/mwclass/creature.hpp index cfa06ed627..ca8abc0405 100644 --- a/apps/openmw/mwclass/creature.hpp +++ b/apps/openmw/mwclass/creature.hpp @@ -56,6 +56,8 @@ namespace MWClass virtual void hit(const MWWorld::Ptr& ptr, int type) const; + virtual void block(const MWWorld::Ptr &ptr) 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 setActorHealth(const MWWorld::Ptr& ptr, float health, const MWWorld::Ptr& attacker) const; diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 96cb4832d1..f4ccd3c5f7 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -20,6 +20,7 @@ #include "../mwmechanics/movement.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/disease.hpp" +#include "../mwmechanics/combat.hpp" #include "../mwworld/ptr.hpp" #include "../mwworld/actiontalk.hpp" @@ -609,8 +610,10 @@ namespace MWClass } } - // TODO: do not do this if the attack is blocked - if (healthdmg) + if (!weapon.isEmpty() && MWMechanics::blockMeleeAttack(ptr, victim, weapon, damage)) + damage = 0; + + if (healthdmg && damage > 0) MWBase::Environment::get().getWorld()->spawnBloodEffect(victim, hitPosition); othercls.onHit(victim, damage, healthdmg, weapon, ptr, true); @@ -751,6 +754,30 @@ namespace MWClass } } + void Npc::block(const MWWorld::Ptr &ptr) const + { + MWWorld::InventoryStore& inv = getInventoryStore(ptr); + MWWorld::ContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); + if (shield == inv.end()) + return; + + MWBase::SoundManager *sndMgr = MWBase::Environment::get().getSoundManager(); + switch(shield->getClass().getEquipmentSkill(*shield)) + { + case ESM::Skill::LightArmor: + sndMgr->playSound3D(ptr, "Light Armor Hit", 1.0f, 1.0f); + break; + case ESM::Skill::MediumArmor: + sndMgr->playSound3D(ptr, "Medium Armor Hit", 1.0f, 1.0f); + break; + case ESM::Skill::HeavyArmor: + sndMgr->playSound3D(ptr, "Heavy Armor Hit", 1.0f, 1.0f); + break; + default: + return; + } + } + void Npc::setActorHealth(const MWWorld::Ptr& ptr, float health, const MWWorld::Ptr& attacker) const { MWMechanics::CreatureStats &crstats = getCreatureStats(ptr); diff --git a/apps/openmw/mwclass/npc.hpp b/apps/openmw/mwclass/npc.hpp index 157e3afd9f..2bd91d1985 100644 --- a/apps/openmw/mwclass/npc.hpp +++ b/apps/openmw/mwclass/npc.hpp @@ -79,6 +79,8 @@ namespace MWClass virtual void onHit(const MWWorld::Ptr &ptr, float damage, bool ishealth, const MWWorld::Ptr &object, const MWWorld::Ptr &attacker, bool successful) const; + virtual void block(const MWWorld::Ptr &ptr) const; + virtual void setActorHealth(const MWWorld::Ptr& ptr, float health, const MWWorld::Ptr& attacker) const; virtual boost::shared_ptr activate (const MWWorld::Ptr& ptr, diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index 3f29cbb7c5..2f5b0ca6c4 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -153,6 +153,7 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat { bool recovery = mPtr.getClass().getCreatureStats(mPtr).getHitRecovery(); bool knockdown = mPtr.getClass().getCreatureStats(mPtr).getKnockedDown(); + bool block = mPtr.getClass().getCreatureStats(mPtr).getBlock(); if(mHitState == CharState_None) { if(knockdown) @@ -167,6 +168,12 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat mCurrentHit = chooseRandomGroup("hit"); mAnimation->play(mCurrentHit, Priority_Hit, MWRender::Animation::Group_All, true, 1, "start", "stop", 0.0f, 0); } + else if (block) + { + mHitState = CharState_Block; + mCurrentHit = "shield"; + mAnimation->play(mCurrentHit, Priority_Hit, MWRender::Animation::Group_All, true, 1, "block start", "block stop", 0.0f, 0); + } } else if(!mAnimation->isPlaying(mCurrentHit)) { @@ -175,6 +182,8 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat mPtr.getClass().getCreatureStats(mPtr).setKnockedDown(false); if (recovery) mPtr.getClass().getCreatureStats(mPtr).setHitRecovery(false); + if (block) + mPtr.getClass().getCreatureStats(mPtr).setBlock(false); mHitState = CharState_None; } } diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index ea12d9b94c..915de93eba 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -92,7 +92,8 @@ enum CharacterState { CharState_SwimDeath, CharState_Hit, - CharState_KnockDown + CharState_KnockDown, + CharState_Block }; enum WeaponType { diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp new file mode 100644 index 0000000000..d547368579 --- /dev/null +++ b/apps/openmw/mwmechanics/combat.cpp @@ -0,0 +1,111 @@ +#include "combat.hpp" + +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/world.hpp" + +#include "../mwmechanics/creaturestats.hpp" +#include "../mwmechanics/movement.hpp" + +#include "../mwworld/class.hpp" +#include "../mwworld/inventorystore.hpp" + +namespace +{ + +Ogre::Radian signedAngle(Ogre::Vector3 v1, Ogre::Vector3 v2, Ogre::Vector3 n) +{ + return Ogre::Math::ATan2( + n.dotProduct( v1.crossProduct(v2) ), + v1.dotProduct(v2) + ); +} + +} + +namespace MWMechanics +{ + + bool blockMeleeAttack(const MWWorld::Ptr &attacker, const MWWorld::Ptr &blocker, const MWWorld::Ptr &weapon, float damage) + { + if (!blocker.getClass().hasInventoryStore(blocker)) + return false; + + if (blocker.getClass().getCreatureStats(blocker).getKnockedDown() + || blocker.getClass().getCreatureStats(blocker).getHitRecovery()) + return false; + + MWWorld::InventoryStore& inv = blocker.getClass().getInventoryStore(blocker); + MWWorld::ContainerStoreIterator shield = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); + if (shield == inv.end() || shield->getTypeName() != typeid(ESM::Armor).name()) + return false; + + Ogre::Degree angle = signedAngle (Ogre::Vector3(attacker.getRefData().getPosition().pos) - Ogre::Vector3(blocker.getRefData().getPosition().pos), + blocker.getRefData().getBaseNode()->getOrientation().yAxis(), Ogre::Vector3(0,0,1)); + + const MWWorld::Store& gmst = MWBase::Environment::get().getWorld()->getStore().get(); + if (angle.valueDegrees() < gmst.find("fCombatBlockLeftAngle")->getFloat()) + return false; + if (angle.valueDegrees() > gmst.find("fCombatBlockRightAngle")->getFloat()) + return false; + + MWMechanics::CreatureStats& blockerStats = blocker.getClass().getCreatureStats(blocker); + if (blockerStats.getDrawState() == DrawState_Spell) + return false; + + MWMechanics::CreatureStats& attackerStats = attacker.getClass().getCreatureStats(attacker); + + float blockTerm = blocker.getClass().getSkill(blocker, ESM::Skill::Block) + 0.2 * blockerStats.getAttribute(ESM::Attribute::Agility).getModified() + + 0.1 * blockerStats.getAttribute(ESM::Attribute::Luck).getModified(); + float enemySwing = attackerStats.getAttackStrength(); + float swingTerm = enemySwing * gmst.find("fSwingBlockMult")->getFloat() + gmst.find("fSwingBlockBase")->getFloat(); + + float blockerTerm = blockTerm * swingTerm; + if (blocker.getClass().getMovementSettings(blocker).mPosition[1] <= 0) + blockerTerm *= gmst.find("fBlockStillBonus")->getFloat(); + blockerTerm *= blockerStats.getFatigueTerm(); + + float attackerSkill = attacker.getClass().getSkill(attacker, weapon.getClass().getEquipmentSkill(weapon)); + float attackerTerm = attackerSkill + 0.2 * attackerStats.getAttribute(ESM::Attribute::Agility).getModified() + + 0.1 * attackerStats.getAttribute(ESM::Attribute::Luck).getModified(); + attackerTerm *= attackerStats.getFatigueTerm(); + + int x = int(blockerTerm - attackerTerm); + int iBlockMaxChance = gmst.find("iBlockMaxChance")->getInt(); + int iBlockMinChance = gmst.find("iBlockMinChance")->getInt(); + x = std::min(iBlockMaxChance, std::max(iBlockMinChance, x)); + + int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] + if (roll < x) + { + // Reduce shield durability by incoming damage + if (shield->getCellRef().mCharge == -1) + shield->getCellRef().mCharge = shield->getClass().getItemMaxHealth(*shield); + shield->getCellRef().mCharge -= std::min(shield->getCellRef().mCharge, int(damage)); + if (!shield->getCellRef().mCharge) + inv.unequipItem(*shield, blocker); + + // Reduce blocker fatigue + const float fFatigueBlockBase = gmst.find("fFatigueBlockBase")->getFloat(); + const float fFatigueBlockMult = gmst.find("fFatigueBlockMult")->getFloat(); + const float fWeaponFatigueBlockMult = gmst.find("fWeaponFatigueBlockMult")->getFloat(); + MWMechanics::DynamicStat fatigue = blockerStats.getFatigue(); + float normalizedEncumbrance = blocker.getClass().getEncumbrance(blocker) / blocker.getClass().getCapacity(blocker); + normalizedEncumbrance = std::min(1.f, normalizedEncumbrance); + float fatigueLoss = fFatigueBlockBase + normalizedEncumbrance * fFatigueBlockMult; + fatigueLoss += weapon.getClass().getWeight(weapon) * attackerStats.getAttackStrength() * fWeaponFatigueBlockMult; + fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss); + blockerStats.setFatigue(fatigue); + + blockerStats.setBlock(true); + + if (blocker.getClass().isNpc()) + blocker.getClass().skillUsageSucceeded(blocker, ESM::Skill::Block, 0); + + return true; + } + return false; + } + +} diff --git a/apps/openmw/mwmechanics/combat.hpp b/apps/openmw/mwmechanics/combat.hpp new file mode 100644 index 0000000000..d666905f26 --- /dev/null +++ b/apps/openmw/mwmechanics/combat.hpp @@ -0,0 +1,14 @@ +#ifndef OPENMW_MECHANICS_COMBAT_H +#define OPENMW_MECHANICS_COMBAT_H + +#include "../mwworld/ptr.hpp" + +namespace MWMechanics +{ + +/// @return can we block the attack? +bool blockMeleeAttack (const MWWorld::Ptr& attacker, const MWWorld::Ptr& blocker, const MWWorld::Ptr& weapon, float damage); + +} + +#endif diff --git a/apps/openmw/mwmechanics/creaturestats.cpp b/apps/openmw/mwmechanics/creaturestats.cpp index 62bae8ca89..8f84a79797 100644 --- a/apps/openmw/mwmechanics/creaturestats.cpp +++ b/apps/openmw/mwmechanics/creaturestats.cpp @@ -15,7 +15,7 @@ namespace MWMechanics mAttacked (false), mHostile (false), mAttackingOrSpell(false), mAttackType(AT_Chop), mIsWerewolf(false), - mFallHeight(0), mRecalcDynamicStats(false), mKnockdown(false), mHitRecovery(false), + mFallHeight(0), mRecalcDynamicStats(false), mKnockdown(false), mHitRecovery(false), mBlock(false), mMovementFlags(0), mDrawState (DrawState_Nothing), mAttackStrength(0.f) { for (int i=0; i<4; ++i) @@ -427,6 +427,16 @@ namespace MWMechanics return mHitRecovery; } + void CreatureStats::setBlock(bool value) + { + mBlock = value; + } + + bool CreatureStats::getBlock() const + { + return mBlock; + } + bool CreatureStats::getMovementFlag (Flag flag) const { return mMovementFlags & flag; diff --git a/apps/openmw/mwmechanics/creaturestats.hpp b/apps/openmw/mwmechanics/creaturestats.hpp index 6893f385ec..0501eb2867 100644 --- a/apps/openmw/mwmechanics/creaturestats.hpp +++ b/apps/openmw/mwmechanics/creaturestats.hpp @@ -39,6 +39,7 @@ namespace MWMechanics bool mAttackingOrSpell; bool mKnockdown; bool mHitRecovery; + bool mBlock; unsigned int mMovementFlags; float mAttackStrength; // Note only some creatures attack with weapons @@ -204,6 +205,8 @@ namespace MWMechanics bool getKnockedDown() const; void setHitRecovery(bool value); bool getHitRecovery() const; + void setBlock(bool value); + bool getBlock() const; enum Flag { diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index a7623efea6..cd2d9a47a0 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -679,6 +679,9 @@ void Animation::handleTextKey(AnimState &state, const std::string &groupname, co else if (groupname == "spellcast" && evt.substr(evt.size()-7, 7) == "release") MWBase::Environment::get().getWorld()->castSpell(mPtr); + + else if (groupname == "shield" && evt.compare(off, len, "block hit") == 0) + mPtr.getClass().block(mPtr); } void Animation::changeGroups(const std::string &groupname, int groups) diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index c7b0d2e1f2..9771ffde3f 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -92,6 +92,11 @@ namespace MWWorld throw std::runtime_error("class cannot hit"); } + void Class::block(const Ptr &ptr) const + { + 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 { throw std::runtime_error("class cannot be hit"); diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index 823538cf80..0dee8b292a 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -128,6 +128,10 @@ namespace MWWorld /// actor responsible for the attack, and \a successful specifies if the hit is /// successful or not. + virtual void block (const Ptr& ptr) const; + ///< Play the appropriate sound for a blocked attack, depending on the currently equipped shield + /// (default implementation: throw an exception) + virtual void setActorHealth(const Ptr& ptr, float health, const Ptr& attacker=Ptr()) const; ///< Sets a new current health value for the actor, optionally specifying the object causing /// the change. Use this instead of using CreatureStats directly as this will make sure the