mirror of
https://github.com/OpenMW/openmw.git
synced 2025-01-20 16:53:55 +00:00
Feature #956: Implement blocking melee attacks
This commit is contained in:
parent
82a07af72c
commit
16f5f5862d
14 changed files with 247 additions and 26 deletions
|
@ -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
|
||||
|
|
|
@ -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<double> (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<double> (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<float> 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<float> 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<MWWorld::Action> activate (const MWWorld::Ptr& ptr,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,7 +92,8 @@ enum CharacterState {
|
|||
CharState_SwimDeath,
|
||||
|
||||
CharState_Hit,
|
||||
CharState_KnockDown
|
||||
CharState_KnockDown,
|
||||
CharState_Block
|
||||
};
|
||||
|
||||
enum WeaponType {
|
||||
|
|
111
apps/openmw/mwmechanics/combat.cpp
Normal file
111
apps/openmw/mwmechanics/combat.cpp
Normal file
|
@ -0,0 +1,111 @@
|
|||
#include "combat.hpp"
|
||||
|
||||
#include <OgreSceneNode.h>
|
||||
|
||||
#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<ESM::GameSetting>& gmst = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();
|
||||
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<double> (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<float> 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;
|
||||
}
|
||||
|
||||
}
|
14
apps/openmw/mwmechanics/combat.hpp
Normal file
14
apps/openmw/mwmechanics/combat.hpp
Normal file
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue