From 072dc6d4383638421fbb271fa5b8fafffd36fc13 Mon Sep 17 00:00:00 2001 From: scrawl Date: Sat, 8 Mar 2014 05:51:47 +0100 Subject: [PATCH] Feature #50: Implement marksman mechanics. --- apps/openmw/mwclass/creature.cpp | 13 +-- apps/openmw/mwclass/npc.cpp | 16 +--- apps/openmw/mwmechanics/combat.cpp | 113 +++++++++++++++++++++++++- apps/openmw/mwmechanics/combat.hpp | 8 ++ apps/openmw/mwrender/npcanimation.cpp | 31 ++++++- apps/openmw/mwworld/worldimp.cpp | 25 +++--- apps/openmw/mwworld/worldimp.hpp | 2 +- 7 files changed, 171 insertions(+), 37 deletions(-) diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 2939a5431..33c4390a0 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -7,6 +7,7 @@ #include "../mwmechanics/creaturestats.hpp" #include "../mwmechanics/magiceffects.hpp" #include "../mwmechanics/movement.hpp" +#include "../mwmechanics/disease.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwbase/environment.hpp" @@ -243,15 +244,7 @@ namespace MWClass Ogre::Vector3 hitPosition = result.second; - MWMechanics::CreatureStats &otherstats = victim.getClass().getCreatureStats(victim); - const MWMechanics::MagicEffects &mageffects = stats.getMagicEffects(); - float hitchance = ref->mBase->mData.mCombat + - (stats.getAttribute(ESM::Attribute::Agility).getModified() / 5.0f) + - (stats.getAttribute(ESM::Attribute::Luck).getModified() / 10.0f); - hitchance *= stats.getFatigueTerm(); - hitchance += mageffects.get(ESM::MagicEffect::FortifyAttack).mMagnitude - - mageffects.get(ESM::MagicEffect::Blind).mMagnitude; - hitchance -= otherstats.getEvasion(); + float hitchance = MWMechanics::getHitChance(ptr, victim, ref->mBase->mData.mCombat); if((::rand()/(RAND_MAX+1.0)) > hitchance/100.0f) { @@ -334,6 +327,8 @@ namespace MWClass if (damage > 0) MWBase::Environment::get().getWorld()->spawnBloodEffect(victim, hitPosition); + MWMechanics::diseaseContact(victim, ptr); + victim.getClass().onHit(victim, damage, true, weapon, ptr, true); } diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 20c7d9a0e..50e002113 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -498,15 +498,7 @@ namespace MWClass if(!weapon.isEmpty()) weapskill = get(weapon).getEquipmentSkill(weapon); - MWMechanics::NpcStats &stats = getNpcStats(ptr); - const MWMechanics::MagicEffects &mageffects = stats.getMagicEffects(); - float hitchance = stats.getSkill(weapskill).getModified() + - (stats.getAttribute(ESM::Attribute::Agility).getModified() / 5.0f) + - (stats.getAttribute(ESM::Attribute::Luck).getModified() / 10.0f); - hitchance *= stats.getFatigueTerm(); - hitchance += mageffects.get(ESM::MagicEffect::FortifyAttack).mMagnitude - - mageffects.get(ESM::MagicEffect::Blind).mMagnitude; - hitchance -= otherstats.getEvasion(); + float hitchance = MWMechanics::getHitChance(ptr, victim, ptr.getClass().getSkill(ptr, weapskill)); if((::rand()/(RAND_MAX+1.0)) > hitchance/100.0f) { @@ -516,6 +508,7 @@ namespace MWClass bool healthdmg; float damage = 0.0f; + MWMechanics::NpcStats &stats = getNpcStats(ptr); if(!weapon.isEmpty()) { const bool weaphashealth = get(weapon).hasItemHealth(weapon); @@ -615,6 +608,8 @@ namespace MWClass if (healthdmg && damage > 0) MWBase::Environment::get().getWorld()->spawnBloodEffect(victim, hitPosition); + MWMechanics::diseaseContact(victim, ptr); + othercls.onHit(victim, damage, healthdmg, weapon, ptr, true); } @@ -648,9 +643,6 @@ namespace MWClass ptr.getRefData().getLocals().setVarByInt(script, "onpchitme", 1); } - if (!attacker.isEmpty()) - MWMechanics::diseaseContact(ptr, attacker); - if (damage > 0.0f && !object.isEmpty()) MWMechanics::resistNormalWeapon(ptr, attacker, object, damage); diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index 6aeb90dad..cdc12e210 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -4,9 +4,12 @@ #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" +#include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/soundmanager.hpp" #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/movement.hpp" +#include "../mwmechanics/spellcasting.hpp" #include "../mwworld/class.hpp" #include "../mwworld/inventorystore.hpp" @@ -17,14 +20,30 @@ namespace { -Ogre::Radian signedAngle(Ogre::Vector3 v1, Ogre::Vector3 v2, Ogre::Vector3 n) +Ogre::Radian signedAngle(Ogre::Vector3 v1, Ogre::Vector3 v2, Ogre::Vector3 normal) { return Ogre::Math::ATan2( - n.dotProduct( v1.crossProduct(v2) ), + normal.dotProduct( v1.crossProduct(v2) ), v1.dotProduct(v2) ); } +void applyEnchantment (const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& object, const Ogre::Vector3& hitPosition) +{ + std::string enchantmentName = !object.isEmpty() ? object.getClass().getEnchantment(object) : ""; + if (!enchantmentName.empty()) + { + const ESM::Enchantment* enchantment = MWBase::Environment::get().getWorld()->getStore().get().find( + enchantmentName); + if (enchantment->mData.mType == ESM::Enchantment::WhenStrikes) + { + MWMechanics::CastSpell cast(attacker, victim); + cast.mHitPosition = hitPosition; + cast.cast(object); + } + } +} + } namespace MWMechanics @@ -135,4 +154,94 @@ namespace MWMechanics MWBase::Environment::get().getWindowManager()->messageBox("#{sMagicTargetResistsWeapons}"); } + void projectileHit(const MWWorld::Ptr &attacker, const MWWorld::Ptr &victim, MWWorld::Ptr weapon, const MWWorld::Ptr &projectile, + const Ogre::Vector3& hitPosition) + { + MWBase::World *world = MWBase::Environment::get().getWorld(); + const MWWorld::Store &gmst = world->getStore().get(); + + MWMechanics::CreatureStats& attackerStats = attacker.getClass().getCreatureStats(attacker); + + const MWWorld::Class &othercls = victim.getClass(); + if(!othercls.isActor()) // Can't hit non-actors + return; + MWMechanics::CreatureStats &otherstats = victim.getClass().getCreatureStats(victim); + if(otherstats.isDead()) // Can't hit dead actors + return; + + if(attacker.getRefData().getHandle() == "player") + MWBase::Environment::get().getWindowManager()->setEnemy(victim); + + int weapskill = ESM::Skill::Marksman; + if(!weapon.isEmpty()) + weapskill = weapon.getClass().getEquipmentSkill(weapon); + + float skillValue = attacker.getClass().getSkill(attacker, + weapon.getClass().getEquipmentSkill(weapon)); + + if((::rand()/(RAND_MAX+1.0)) > getHitChance(attacker, victim, skillValue)/100.0f) + { + victim.getClass().onHit(victim, 0.0f, false, projectile, attacker, false); + return; + } + + float damage = 0.0f; + + float fDamageStrengthBase = gmst.find("fDamageStrengthBase")->getFloat(); + float fDamageStrengthMult = gmst.find("fDamageStrengthMult")->getFloat(); + + const unsigned char* attack = weapon.get()->mBase->mData.mChop; + damage = attack[0] + ((attack[1]-attack[0])*attackerStats.getAttackStrength()); // Bow/crossbow damage + if (weapon != projectile) + { + // Arrow/bolt damage + attack = projectile.get()->mBase->mData.mChop; + damage += attack[0] + ((attack[1]-attack[0])*attackerStats.getAttackStrength()); + } + + damage *= fDamageStrengthBase + + (attackerStats.getAttribute(ESM::Attribute::Strength).getModified() * fDamageStrengthMult * 0.1); + + if(attacker.getRefData().getHandle() == "player") + attacker.getClass().skillUsageSucceeded(attacker, weapskill, 0); + + bool detected = MWBase::Environment::get().getMechanicsManager()->awarenessCheck(attacker, victim); + if(!detected) + { + damage *= gmst.find("fCombatCriticalStrikeMult")->getFloat(); + MWBase::Environment::get().getWindowManager()->messageBox("#{sTargetCriticalStrike}"); + MWBase::Environment::get().getSoundManager()->playSound3D(victim, "critical damage", 1.0f, 1.0f); + } + if (victim.getClass().getCreatureStats(victim).getKnockedDown()) + damage *= gmst.find("fCombatKODamageMult")->getFloat(); + + // Apply "On hit" effect of the weapon + applyEnchantment(attacker, victim, weapon, hitPosition); + if (weapon != projectile) + applyEnchantment(attacker, victim, projectile, hitPosition); + + if (damage > 0) + MWBase::Environment::get().getWorld()->spawnBloodEffect(victim, hitPosition); + + float fProjectileThrownStoreChance = gmst.find("fProjectileThrownStoreChance")->getFloat(); + if ((::rand()/(RAND_MAX+1.0)) < fProjectileThrownStoreChance/100.f) + victim.getClass().getContainerStore(victim).add(projectile, 1, victim); + + victim.getClass().onHit(victim, damage, true, projectile, attacker, true); + } + + float getHitChance(const MWWorld::Ptr &attacker, const MWWorld::Ptr &victim, int skillValue) + { + MWMechanics::CreatureStats &stats = attacker.getClass().getCreatureStats(attacker); + const MWMechanics::MagicEffects &mageffects = stats.getMagicEffects(); + float hitchance = skillValue + + (stats.getAttribute(ESM::Attribute::Agility).getModified() / 5.0f) + + (stats.getAttribute(ESM::Attribute::Luck).getModified() / 10.0f); + hitchance *= stats.getFatigueTerm(); + hitchance += mageffects.get(ESM::MagicEffect::FortifyAttack).mMagnitude - + mageffects.get(ESM::MagicEffect::Blind).mMagnitude; + hitchance -= victim.getClass().getCreatureStats(victim).getEvasion(); + return hitchance; + } + } diff --git a/apps/openmw/mwmechanics/combat.hpp b/apps/openmw/mwmechanics/combat.hpp index 7f2415697..bc58227bf 100644 --- a/apps/openmw/mwmechanics/combat.hpp +++ b/apps/openmw/mwmechanics/combat.hpp @@ -2,6 +2,7 @@ #define OPENMW_MECHANICS_COMBAT_H #include "../mwworld/ptr.hpp" +#include namespace MWMechanics { @@ -11,6 +12,13 @@ bool blockMeleeAttack (const MWWorld::Ptr& attacker, const MWWorld::Ptr& blocker void resistNormalWeapon (const MWWorld::Ptr& actor, const MWWorld::Ptr& attacker, const MWWorld::Ptr& weapon, float& damage); +/// @note for a thrown weapon, \a weapon == \a projectile, for bows/crossbows, \a projectile is the arrow/bolt +void projectileHit (const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, MWWorld::Ptr weapon, const MWWorld::Ptr& projectile, + const Ogre::Vector3& hitPosition); + +/// Get the chance (in percent) for \a attacker to successfully hit \a victim with a given weapon skill value +float getHitChance (const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, int skillValue); + } #endif diff --git a/apps/openmw/mwrender/npcanimation.cpp b/apps/openmw/mwrender/npcanimation.cpp index 6169b6ddb..8f8e65cd0 100644 --- a/apps/openmw/mwrender/npcanimation.cpp +++ b/apps/openmw/mwrender/npcanimation.cpp @@ -779,6 +779,23 @@ void NpcAnimation::releaseArrow() Ogre::Quaternion orient = Ogre::Quaternion(Ogre::Radian(mPtr.getRefData().getPosition().rot[2]), Ogre::Vector3::NEGATIVE_UNIT_Z) * Ogre::Quaternion(Ogre::Radian(mPtr.getRefData().getPosition().rot[0]), Ogre::Vector3::NEGATIVE_UNIT_X); + const MWWorld::Store &gmst = + MWBase::Environment::get().getWorld()->getStore().get(); + + // Reduce fatigue + // somewhat of a guess, but using the weapon weight makes sense + const float fFatigueAttackBase = gmst.find("fFatigueAttackBase")->getFloat(); + const float fFatigueAttackMult = gmst.find("fFatigueAttackMult")->getFloat(); + const float fWeaponFatigueMult = gmst.find("fWeaponFatigueMult")->getFloat(); + MWMechanics::CreatureStats& attackerStats = mPtr.getClass().getCreatureStats(mPtr); + MWMechanics::DynamicStat fatigue = attackerStats.getFatigue(); + const float normalizedEncumbrance = mPtr.getClass().getEncumbrance(mPtr) / mPtr.getClass().getCapacity(mPtr); + float fatigueLoss = fFatigueAttackBase + normalizedEncumbrance * fFatigueAttackMult; + if (!weapon->isEmpty()) + fatigueLoss += weapon->getClass().getWeight(*weapon) * attackerStats.getAttackStrength() * fWeaponFatigueMult; + fatigue.setCurrent(fatigue.getCurrent() - fatigueLoss); + attackerStats.setFatigue(fatigue); + if (weapon->get()->mBase->mData.mType == ESM::Weapon::MarksmanThrown) { // Thrown weapons get detached now @@ -795,7 +812,12 @@ void NpcAnimation::releaseArrow() launchPos = objects->mEntities[0]->getParentNode()->_getDerivedPosition(); } - MWBase::Environment::get().getWorld()->launchProjectile(mPtr, *weapon, launchPos, orient, *weapon, 400); + float fThrownWeaponMinSpeed = gmst.find("fThrownWeaponMinSpeed")->getFloat(); + float fThrownWeaponMaxSpeed = gmst.find("fThrownWeaponMaxSpeed")->getFloat(); + float speed = fThrownWeaponMinSpeed + (fThrownWeaponMaxSpeed - fThrownWeaponMinSpeed) * + mPtr.getClass().getCreatureStats(mPtr).getAttackStrength(); + + MWBase::Environment::get().getWorld()->launchProjectile(mPtr, *weapon, launchPos, orient, *weapon, speed); showWeapons(false); @@ -819,8 +841,11 @@ void NpcAnimation::releaseArrow() launchPos = mAmmunition->mEntities[0]->getParentNode()->_getDerivedPosition(); } - /// \todo speed - MWBase::Environment::get().getWorld()->launchProjectile(mPtr, *ammo, launchPos, orient, *weapon, 400); + float fProjectileMinSpeed = gmst.find("fProjectileMinSpeed")->getFloat(); + float fProjectileMaxSpeed = gmst.find("fProjectileMaxSpeed")->getFloat(); + float speed = fProjectileMinSpeed + (fProjectileMaxSpeed - fProjectileMinSpeed) * mPtr.getClass().getCreatureStats(mPtr).getAttackStrength(); + + MWBase::Environment::get().getWorld()->launchProjectile(mPtr, *ammo, launchPos, orient, *weapon, speed); inv.remove(*ammo, 1, mPtr); mAmmunition.setNull(); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 0ef50c582..62ef09ae4 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -28,7 +28,7 @@ #include "../mwmechanics/npcstats.hpp" #include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/levelledlist.hpp" - +#include "../mwmechanics/combat.hpp" #include "../mwrender/sky.hpp" #include "../mwrender/animation.hpp" @@ -2141,7 +2141,7 @@ namespace MWWorld ProjectileState state; state.mActorHandle = actor.getRefData().getHandle(); state.mBow = bow; - state.mSpeed = speed; + state.mVelocity = orient.yAxis() * speed; MWWorld::ManualRef ref(getStore(), projectile.getCellRef().mRefID); @@ -2258,14 +2258,19 @@ namespace MWWorld MWWorld::Ptr ptr = it->first; - Ogre::Quaternion orient = ptr.getRefData().getBaseNode()->getOrientation(); + // gravity constant - must be way lower than the gravity affecting actors, since we're not + // simulating aerodynamics at all + it->second.mVelocity -= Ogre::Vector3(0, 0, 627.2f * 0.1f) * duration; - float speed = it->second.mSpeed; - - Ogre::Vector3 direction = orient.yAxis(); - direction.normalise(); Ogre::Vector3 pos(ptr.getRefData().getPosition().pos); - Ogre::Vector3 newPos = pos + direction * duration * speed; + Ogre::Vector3 newPos = pos + it->second.mVelocity * duration; + + Ogre::Quaternion orient = Ogre::Vector3::UNIT_Y.getRotationTo(it->second.mVelocity); + Ogre::Matrix3 mat; + orient.ToRotationMatrix(mat); + Ogre::Radian xr,yr,zr; + mat.ToEulerAnglesXYZ(xr, yr, zr); + rotateObject(ptr, -xr.valueDegrees(), -yr.valueDegrees(), -zr.valueDegrees()); // Check for impact btVector3 from(pos.x, pos.y, pos.z); @@ -2300,13 +2305,13 @@ namespace MWWorld } else if (obstacle.getClass().isActor()) { - // Fargoth - obstacle.getClass().getCreatureStats(obstacle).setHealth(0); + MWMechanics::projectileHit(caster, obstacle, it->second.mBow, ptr, pos + (newPos - pos) * cIt->first); } hit = true; } if (hit) { + deleteObject(ptr); mProjectiles.erase(it++); continue; } diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 40fe4c96a..b694e00f7 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -115,7 +115,7 @@ namespace MWWorld MWWorld::Ptr mBow; // bow or crossbow the projectile was fired from - float mSpeed; + Ogre::Vector3 mVelocity; }; std::map mMagicBolts;