1
0
mirror of https://gitlab.com/OpenMW/openmw.git synced 2025-03-14 01:19:59 +00:00
Probably a first properly working commit?

Push to get clang compiler feedback

hit engine handler and function binding

pass things by reference

Reverted late-miss behaviour, fail early on miss with 0 damage and all other flags set to false
This commit is contained in:
Max Yari 2023-12-08 13:46:11 +01:00 committed by MaxYari
parent a49a900a7b
commit 5775a9af0c
17 changed files with 318 additions and 92 deletions

View File

@ -9,6 +9,7 @@
#include "../mwgui/mode.hpp"
#include "../mwrender/animationpriority.hpp"
#include "../mwworld/class.hpp"
#include <components/sdlutil/events.hpp>
namespace MWWorld
@ -59,6 +60,9 @@ namespace MWBase
virtual void noGame() = 0;
virtual void objectAddedToScene(const MWWorld::Ptr& ptr) = 0;
virtual void objectRemovedFromScene(const MWWorld::Ptr& ptr) = 0;
virtual void actorHit(const MWWorld::Ptr& target, const MWWorld::Ptr& weapon, const MWWorld::Ptr& attacker,
const MWWorld::DamageData& damageData, const MWWorld::HitConfig& hitConfig)
= 0;
virtual void objectTeleported(const MWWorld::Ptr& ptr) = 0;
virtual void itemConsumed(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) = 0;
virtual void objectActivated(const MWWorld::Ptr& object, const MWWorld::Ptr& actor) = 0;

View File

@ -283,8 +283,14 @@ namespace MWClass
if (!success)
{
victim.getClass().onHit(
victim, 0.0f, false, MWWorld::Ptr(), ptr, osg::Vec3f(), false, MWMechanics::DamageSourceType::Melee);
MWWorld::DamageData dmgData = {
.mDamage = 0.0f,
.mAffectsHealth = false,
.mAffectsFatigue = false,
.mIsSuccessful = false,
.mHitPosition = osg::Vec3f(),
};
victim.getClass().onHit(victim, MWWorld::Ptr(), ptr, dmgData, MWMechanics::DamageSourceType::Melee);
MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr);
return;
}
@ -342,15 +348,20 @@ namespace MWClass
MWMechanics::diseaseContact(victim, ptr);
victim.getClass().onHit(
victim, damage, healthdmg, weapon, ptr, hitPosition, true, MWMechanics::DamageSourceType::Melee);
MWWorld::DamageData dmgData = {
.mDamage = damage,
.mAffectsHealth = healthdmg,
.mAffectsFatigue = !healthdmg,
.mIsSuccessful = true,
.mHitPosition = hitPosition,
};
victim.getClass().onHit(victim, weapon, ptr, dmgData, MWMechanics::DamageSourceType::Melee);
}
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 MWMechanics::DamageSourceType sourceType) const
void Creature::onHit(const MWWorld::Ptr& target, const MWWorld::Ptr& weapon, const MWWorld::Ptr& attacker,
const MWWorld::DamageData& damageData, const MWWorld::HitConfig& hitConfig) const
{
MWMechanics::CreatureStats& stats = getCreatureStats(ptr);
MWMechanics::CreatureStats& stats = getCreatureStats(target);
// Self defense
bool setOnPcHitMe = true;
@ -378,37 +389,39 @@ namespace MWClass
MWMechanics::CreatureStats& statsAttacker = attacker.getClass().getCreatureStats(attacker);
// First handle the attacked actor
if ((stats.getHitAttemptActorId() == -1)
&& (statsAttacker.getAiSequence().isInCombat(ptr) || attacker == MWMechanics::getPlayer()))
&& (statsAttacker.getAiSequence().isInCombat(target) || attacker == MWMechanics::getPlayer()))
stats.setHitAttemptActorId(statsAttacker.getActorId());
// Next handle the attacking actor
if ((statsAttacker.getHitAttemptActorId() == -1)
&& (statsAttacker.getAiSequence().isInCombat(ptr) || attacker == MWMechanics::getPlayer()))
&& (statsAttacker.getAiSequence().isInCombat(target) || attacker == MWMechanics::getPlayer()))
statsAttacker.setHitAttemptActorId(stats.getActorId());
}
if (!object.isEmpty())
stats.setLastHitAttemptObject(object.getCellRef().getRefId());
if (!weapon.isEmpty())
stats.setLastHitAttemptObject(weapon.getCellRef().getRefId());
if (setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer())
{
const ESM::RefId& script = ptr.get<ESM::Creature>()->mBase->mScript;
const ESM::RefId& script = target.get<ESM::Creature>()->mBase->mScript;
/* Set the OnPCHitMe script variable. The script is responsible for clearing it. */
if (!script.empty())
ptr.getRefData().getLocals().setVarByInt(script, "onpchitme", 1);
target.getRefData().getLocals().setVarByInt(script, "onpchitme", 1);
}
if (!successful)
if (!damageData.mIsSuccessful)
{
// Missed
if (!attacker.isEmpty() && attacker == MWMechanics::getPlayer())
if (!attacker.isEmpty() && attacker == MWMechanics::getPlayer() && hitConfig.mPlaySFX)
MWBase::Environment::get().getSoundManager()->playSound3D(
ptr, ESM::RefId::stringRefId("miss"), 1.0f, 1.0f);
target, ESM::RefId::stringRefId("miss"), 1.0f, 1.0f);
return;
}
if (!object.isEmpty())
stats.setLastHitObject(object.getCellRef().getRefId());
if (!weapon.isEmpty())
stats.setLastHitObject(weapon.getCellRef().getRefId());
auto damage = damageData.mDamage
if (damage < 0.001f)
damage = 0;
@ -424,31 +437,35 @@ namespace MWClass
* getGmst().iKnockDownOddsMult->mValue.getInteger() * 0.01f
+ getGmst().iKnockDownOddsBase->mValue.getInteger();
auto& prng = MWBase::Environment::get().getWorld()->getPrng();
if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99(prng))
if (!hitConfig.mAvoidKnockdown && damageData.mAffectsHealth && agilityTerm <= damage
&& knockdownTerm <= Misc::Rng::roll0to99(prng))
stats.setKnockedDown(true);
else
else if (!hitConfig.mAvoidHitReaction)
stats.setHitRecovery(true); // Is this supposed to always occur?
}
if (ishealth)
if (damageData.mAffectsHealth)
{
damage *= damage / (damage + getArmorRating(ptr));
damage *= damage / (damage + getArmorRating(target));
damage = std::max(1.f, damage);
if (!attacker.isEmpty())
{
damage = scaleDamage(damage, attacker, ptr);
MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition);
damage = scaleDamage(damage, attacker, target);
if (hitConfig.mPlayVFX)
MWBase::Environment::get().getWorld()->spawnBloodEffect(target, damageData.mHitPosition);
}
MWBase::Environment::get().getSoundManager()->playSound3D(
ptr, ESM::RefId::stringRefId("Health Damage"), 1.0f, 1.0f);
if (hitConfig.mPlaySFX)
MWBase::Environment::get().getSoundManager()->playSound3D(
target, ESM::RefId::stringRefId("Health Damage"), 1.0f, 1.0f);
MWMechanics::DynamicStat<float> health(stats.getHealth());
health.setCurrent(health.getCurrent() - damage);
stats.setHealth(health);
}
else
if (damageData.mAffectsFatigue)
{
// If health was also damaged - damage was scaled using scaleDamage, is that undesirable here?
MWMechanics::DynamicStat<float> fatigue(stats.getFatigue());
fatigue.setCurrent(fatigue.getCurrent() - damage, true);
stats.setFatigue(fatigue);

View File

@ -66,9 +66,9 @@ namespace MWClass
void hit(const MWWorld::Ptr& ptr, float attackStrength, int type, const MWWorld::Ptr& victim,
const osg::Vec3f& hitPosition, bool success) const override;
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 MWMechanics::DamageSourceType sourceType) const override;
void onHit(const MWWorld::Ptr& target, const MWWorld::Ptr& weapon, const MWWorld::Ptr& attacker,
const MWWorld::DamageData& damageData,
const MWWorld::HitConfig& hitConfig = MWWorld::HitConfig(), const MWMechanics::DamageSourceType sourceType) const override;
std::unique_ptr<MWWorld::Action> activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override;
///< Generate action for activation

View File

@ -620,8 +620,16 @@ namespace MWClass
float damage = 0.0f;
if (!success)
{
othercls.onHit(
victim, damage, false, weapon, ptr, osg::Vec3f(), false, MWMechanics::DamageSourceType::Melee);
MWWorld::DamageData dmgData = {
.mDamage = damage,
.mAffectsHealth = false,
.mAffectsFatigue = false,
.mIsSuccessful = false,
.mHitPosition = hitPosition,
};
othercls.onHit(victim, weapon, ptr, dmgData, MWMechanics::DamageSourceType::Melee);
MWMechanics::reduceWeaponCondition(damage, false, weapon, ptr);
MWMechanics::resistNormalWeapon(victim, ptr, weapon, damage);
return;
@ -694,19 +702,29 @@ namespace MWClass
MWMechanics::diseaseContact(victim, ptr);
othercls.onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true, MWMechanics::DamageSourceType::Melee);
}
MWWorld::DamageData dmgData = {
.mDamage = damage,
.mAffectsHealth = healthdmg,
.mAffectsFatigue = !healthdmg,
.mIsSuccessful = true,
.mHitPosition = hitPosition,
};
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,
othercls.onHit(victim, weapon, ptr, dmgData, MWMechanics::DamageSourceType::Melee);
}
// target = ptr , weapon = object
void Npc::onHit(const MWWorld::Ptr& target, const MWWorld::Ptr& weapon, const MWWorld::Ptr& attacker,
const MWWorld::DamageData& damageData, const MWWorld::HitConfig& hitConfig,
const MWMechanics::DamageSourceType sourceType) const
{
MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager();
MWMechanics::CreatureStats& stats = getCreatureStats(ptr);
MWMechanics::CreatureStats& stats = getCreatureStats(target);
bool wasDead = stats.isDead();
bool setOnPcHitMe = true;
auto damage = damageData.mDamage;
// NOTE: 'object' and/or 'attacker' may be empty.
if (!attacker.isEmpty() && attacker.getClass().isActor() && !stats.getAiSequence().isInCombat(attacker))
{
@ -725,41 +743,42 @@ namespace MWClass
MWMechanics::CreatureStats& statsAttacker = attacker.getClass().getCreatureStats(attacker);
// First handle the attacked actor
if ((stats.getHitAttemptActorId() == -1)
&& (statsAttacker.getAiSequence().isInCombat(ptr) || attacker == MWMechanics::getPlayer()))
&& (statsAttacker.getAiSequence().isInCombat(target) || attacker == MWMechanics::getPlayer()))
stats.setHitAttemptActorId(statsAttacker.getActorId());
// Next handle the attacking actor
if ((statsAttacker.getHitAttemptActorId() == -1)
&& (statsAttacker.getAiSequence().isInCombat(ptr) || attacker == MWMechanics::getPlayer()))
&& (statsAttacker.getAiSequence().isInCombat(target) || attacker == MWMechanics::getPlayer()))
statsAttacker.setHitAttemptActorId(stats.getActorId());
}
if (!object.isEmpty())
stats.setLastHitAttemptObject(object.getCellRef().getRefId());
if (!weapon.isEmpty())
stats.setLastHitAttemptObject(weapon.getCellRef().getRefId());
if (setOnPcHitMe && !attacker.isEmpty() && attacker == MWMechanics::getPlayer())
{
const ESM::RefId& script = getScript(ptr);
const ESM::RefId& script = getScript(target);
/* Set the OnPCHitMe script variable. The script is responsible for clearing it. */
if (!script.empty())
ptr.getRefData().getLocals().setVarByInt(script, "onpchitme", 1);
target.getRefData().getLocals().setVarByInt(script, "onpchitme", 1);
}
if (!successful)
if (!damageData.mIsSuccessful)
{
// Missed
if (!attacker.isEmpty() && attacker == MWMechanics::getPlayer())
sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("miss"), 1.0f, 1.0f);
if (!attacker.isEmpty() && attacker == MWMechanics::getPlayer() && hitConfig.mPlaySFX)
sndMgr->playSound3D(target, ESM::RefId::stringRefId("miss"), 1.0f, 1.0f);
MWBase::Environment::get().getLuaManager()->actorHit(target, weapon, attacker, damageData, hitConfig);
return;
}
if (!object.isEmpty())
stats.setLastHitObject(object.getCellRef().getRefId());
if (!weapon.isEmpty())
stats.setLastHitObject(weapon.getCellRef().getRefId());
if (damage < 0.001f)
damage = 0;
bool godmode = ptr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState();
bool godmode = target == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState();
if (godmode)
damage = 0;
@ -775,7 +794,7 @@ namespace MWClass
int chance = store.get<ESM::GameSetting>().find("iVoiceHitOdds")->mValue.getInteger();
auto& prng = MWBase::Environment::get().getWorld()->getPrng();
if (Misc::Rng::roll0to99(prng) < chance)
MWBase::Environment::get().getDialogueManager()->say(ptr, ESM::RefId::stringRefId("hit"));
MWBase::Environment::get().getDialogueManager()->say(target, ESM::RefId::stringRefId("hit"));
// Check for knockdown
float agilityTerm
@ -783,12 +802,13 @@ namespace MWClass
float knockdownTerm = stats.getAttribute(ESM::Attribute::Agility).getModified()
* gmst.iKnockDownOddsMult->mValue.getInteger() * 0.01f
+ gmst.iKnockDownOddsBase->mValue.getInteger();
if (ishealth && agilityTerm <= damage && knockdownTerm <= Misc::Rng::roll0to99(prng))
if (!hitConfig.mAvoidKnockdown && damageData.mAffectsHealth && agilityTerm <= damage
&& knockdownTerm <= Misc::Rng::roll0to99(prng))
stats.setKnockedDown(true);
else
else if (!hitConfig.mAvoidHitReaction)
stats.setHitRecovery(true); // Is this supposed to always occur?
if (damage > 0 && ishealth)
if (damage > 0 && damageData.mAffectsHealth)
{
// Hit percentages:
// cuirass = 30%
@ -808,13 +828,13 @@ namespace MWClass
int hitslot = hitslots[Misc::Rng::rollDice(20, prng)];
float unmitigatedDamage = damage;
float x = damage / (damage + getArmorRating(ptr));
float x = damage / (damage + getArmorRating(target));
damage *= std::max(gmst.fCombatArmorMinMult->mValue.getFloat(), x);
int damageDiff = static_cast<int>(unmitigatedDamage - damage);
damage = std::max(1.f, damage);
damageDiff = std::max(1, damageDiff);
MWWorld::InventoryStore& inv = getInventoryStore(ptr);
MWWorld::InventoryStore& inv = getInventoryStore(target);
MWWorld::ContainerStoreIterator armorslot = inv.getSlot(hitslot);
MWWorld::Ptr armor = ((armorslot != inv.end()) ? *armorslot : MWWorld::Ptr());
bool hasArmor = !armor.isEmpty() && armor.getType() == ESM::Armor::sRecordId;
@ -836,7 +856,7 @@ namespace MWClass
{
// Unarmed creature attacks don't affect armor condition unless it was
// explicitly requested.
if (!object.isEmpty() || attacker.isEmpty() || attacker.getClass().isNpc()
if (!weapon.isEmpty() || attacker.isEmpty() || attacker.getClass().isNpc()
|| Settings::game().mUnarmedCreatureAttacksDamageArmor)
{
int armorhealth = armor.getClass().getItemHealth(armor);
@ -849,46 +869,48 @@ namespace MWClass
}
ESM::RefId skill = armor.getClass().getEquipmentSkill(armor);
if (ptr == MWMechanics::getPlayer())
if (target == MWMechanics::getPlayer())
skillUsageSucceeded(ptr, skill, ESM::Skill::Armor_HitByOpponent);
if (skill == ESM::Skill::LightArmor)
sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Light Armor Hit"), 1.0f, 1.0f);
sndMgr->playSound3D(target, ESM::RefId::stringRefId("Light Armor Hit"), 1.0f, 1.0f);
else if (skill == ESM::Skill::MediumArmor)
sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Medium Armor Hit"), 1.0f, 1.0f);
sndMgr->playSound3D(target, ESM::RefId::stringRefId("Medium Armor Hit"), 1.0f, 1.0f);
else if (skill == ESM::Skill::HeavyArmor)
sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Heavy Armor Hit"), 1.0f, 1.0f);
sndMgr->playSound3D(target, ESM::RefId::stringRefId("Heavy Armor Hit"), 1.0f, 1.0f);
}
else if (ptr == MWMechanics::getPlayer())
else if (target == MWMechanics::getPlayer())
skillUsageSucceeded(ptr, ESM::Skill::Unarmored, ESM::Skill::Armor_HitByOpponent);
}
}
if (ishealth)
if (damageData.mAffectsHealth)
{
if (!attacker.isEmpty() && !godmode)
damage = scaleDamage(damage, attacker, ptr);
damage = scaleDamage(damage, attacker, target);
if (damage > 0.0f)
{
sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Health Damage"), 1.0f, 1.0f);
if (ptr == MWMechanics::getPlayer())
if (hitConfig.mPlaySFX)
sndMgr->playSound3D(target, ESM::RefId::stringRefId("Health Damage"), 1.0f, 1.0f);
if (target == MWMechanics::getPlayer() && hitConfig.mPlayVFX)
MWBase::Environment::get().getWindowManager()->activateHitOverlay();
if (!attacker.isEmpty())
MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition);
if (!attacker.isEmpty() && hitConfig.mPlayVFX)
MWBase::Environment::get().getWorld()->spawnBloodEffect(target, damageData.mHitPosition);
}
MWMechanics::DynamicStat<float> health(getCreatureStats(ptr).getHealth());
MWMechanics::DynamicStat<float> health(getCreatureStats(target).getHealth());
health.setCurrent(health.getCurrent() - damage);
stats.setHealth(health);
}
else
if (damageData.mAffectsFatigue)
{
MWMechanics::DynamicStat<float> fatigue(getCreatureStats(ptr).getFatigue());
// If health was also damaged - damage was scaled using scaleDamage, is that undesirable here?
MWMechanics::DynamicStat<float> fatigue(getCreatureStats(target).getFatigue());
fatigue.setCurrent(fatigue.getCurrent() - damage, true);
stats.setFatigue(fatigue);
}
if (!wasDead && getCreatureStats(ptr).isDead())
if (!wasDead && getCreatureStats(target).isDead())
{
// NPC was killed
if (!attacker.isEmpty() && attacker.getClass().isNpc()
@ -897,8 +919,17 @@ namespace MWClass
attacker.getClass().getNpcStats(attacker).addWerewolfKill();
}
MWBase::Environment::get().getMechanicsManager()->actorKilled(ptr, attacker);
MWBase::Environment::get().getMechanicsManager()->actorKilled(target, attacker);
}
// Trigger Lua engine handler
//
// TO DO: add this to creatures and others.
// TO DO: if a lua script will call actor's hit function from this engine handler - an infinite loop of hits may
// occure, probably should allow to pass a flag that will disable an event for the lua triggered hit. But at the
// same time mods might want to read hit events triggered by other mods, so maybe some way to uniquely identify
// a hit should be introduced?
MWBase::Environment::get().getLuaManager()->actorHit(target, weapon, attacker, damageData, hitConfig);
}
std::unique_ptr<MWWorld::Action> Npc::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const

View File

@ -81,8 +81,9 @@ namespace MWClass
void hit(const MWWorld::Ptr& ptr, float attackStrength, int type, const MWWorld::Ptr& victim,
const osg::Vec3f& hitPosition, bool success) const override;
void onHit(const MWWorld::Ptr& ptr, float damage, bool ishealth, const MWWorld::Ptr& object,
const MWWorld::Ptr& attacker, const osg::Vec3f& hitPosition, bool successful,
void onHit(const MWWorld::Ptr& target, const MWWorld::Ptr& weapon, const MWWorld::Ptr& attacker,
const MWWorld::DamageData& damageData,
const MWWorld::HitConfig& hitConfig = MWWorld::HitConfig(),
const MWMechanics::DamageSourceType sourceType) const override;
void getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector<std::string_view>& models) const override;

View File

@ -74,6 +74,42 @@ namespace MWLua
mGlobalScripts.onUseItem(GObject(obj), GObject(actor), event.mForce);
}
void operator()(const OnActorHit& event) const
{
MWWorld::Ptr target = getPtr(event.mTarget);
if (target.isEmpty())
return;
MWLua::LObject weaponObj;
if (event.mWeapon.has_value())
{
weaponObj = LObject(getPtr(event.mWeapon.value()));
}
MWWorld::Ptr attacker;
MWLua::LObject attackerObj;
if (event.mAttacker.has_value())
{
attacker = getPtr(event.mAttacker.value());
attackerObj = LObject(attacker);
}
/*sol::optional<const MWLua::LObject> attackerObj = [&] {
}();*/
if (event.mWeapon.has_value() && event.mAttacker.has_value())
{
// sol::optional doesnt work properly with const LObject now, so until then only run this when all of
// the objects are present
if (auto* scripts = getLocalScripts(target))
scripts->onActorHit(LObject(target), weaponObj, attackerObj, event.mDamageData, event.mHitConfig);
if (!attacker.isEmpty())
if (auto* scripts = getLocalScripts(attacker))
scripts->onActorHit(
LObject(target), weaponObj, attackerObj, event.mDamageData, event.mHitConfig);
}
}
void operator()(const OnConsume& event) const
{
MWWorld::Ptr actor = getPtr(event.mActor);

View File

@ -1,11 +1,13 @@
#ifndef MWLUA_ENGINEEVENTS_H
#define MWLUA_ENGINEEVENTS_H
#include <optional>
#include <variant>
#include <components/esm3/cellref.hpp> // defines RefNum that is used as a unique id
#include "../mwworld/cellstore.hpp"
#include "../mwworld/class.hpp"
namespace MWLua
{
@ -42,6 +44,14 @@ namespace MWLua
ESM::RefNum mObject;
bool mForce;
};
struct OnActorHit
{
ESM::RefNum mTarget;
std::optional<ESM::RefNum> mWeapon;
std::optional<ESM::RefNum> mAttacker;
MWWorld::DamageData mDamageData;
MWWorld::HitConfig mHitConfig;
};
struct OnConsume
{
ESM::RefNum mActor;
@ -70,7 +80,7 @@ namespace MWLua
std::string mSkill;
std::string mSource;
};
using Event = std::variant<OnActive, OnInactive, OnConsume, OnActivate, OnUseItem, OnNewExterior, OnTeleported,
using Event = std::variant<OnActive, OnInactive, OnConsume, OnActivate, OnUseItem, OnActorHit, OnNewExterior, OnTeleported,
OnAnimationTextKey, OnSkillUse, OnSkillLevelUp>;
void clear() { mQueue.clear(); }

View File

@ -232,7 +232,7 @@ namespace MWLua
[&](LuaUtil::LuaView& view) { addPackage("openmw.self", sol::make_object(view.sol(), &mData)); });
registerEngineHandlers({ &mOnActiveHandlers, &mOnInactiveHandlers, &mOnConsumeHandlers, &mOnActivatedHandlers,
&mOnTeleportedHandlers, &mOnAnimationTextKeyHandlers, &mOnPlayAnimationHandlers, &mOnSkillUse,
&mOnSkillLevelUp });
&mOnSkillLevelUp, &mOnActorHit });
}
void LocalScripts::setActive(bool active)

View File

@ -7,6 +7,7 @@
#include <utility>
#include <components/lua/luastate.hpp>
#include <components/lua/scriptscontainer.hpp>
#include "../mwbase/luamanager.hpp"
@ -71,6 +72,11 @@ namespace MWLua
bool isActive() const override { return mData.mIsActive; }
void onConsume(const LObject& consumable) { callEngineHandlers(mOnConsumeHandlers, consumable); }
void onActivated(const LObject& actor) { callEngineHandlers(mOnActivatedHandlers, actor); }
void onActorHit(const LObject& target, sol::optional<const MWLua::LObject> weapon,
sol::optional<const MWLua::LObject> attacker, MWWorld::DamageData damageData, MWWorld::HitConfig hitConfig)
{
callEngineHandlers(mOnActorHit, target, weapon, attacker, damageData, hitConfig);
}
void onTeleported() { callEngineHandlers(mOnTeleportedHandlers); }
void onAnimationTextKey(std::string_view groupname, std::string_view key)
{
@ -98,6 +104,7 @@ namespace MWLua
EngineHandlerList mOnActiveHandlers{ "onActive" };
EngineHandlerList mOnInactiveHandlers{ "onInactive" };
EngineHandlerList mOnConsumeHandlers{ "onConsume" };
EngineHandlerList mOnActorHit{ "onActorHit" };
EngineHandlerList mOnActivatedHandlers{ "onActivated" };
EngineHandlerList mOnTeleportedHandlers{ "onTeleported" };
EngineHandlerList mOnAnimationTextKeyHandlers{ "_onAnimationTextKey" };

View File

@ -514,6 +514,15 @@ namespace MWLua
}
}
void LuaManager::actorHit(const MWWorld::Ptr& target, const MWWorld::Ptr& weapon, const MWWorld::Ptr& attacker,
const MWWorld::DamageData& damageData, const MWWorld::HitConfig& hitConfig)
{
auto weaponId = !weapon.isEmpty() ? std::optional<ESM::RefNum>(getId(weapon)) : std::nullopt;
auto attackerId = !weapon.isEmpty() ? std::optional<ESM::RefNum>(getId(attacker)) : std::nullopt;
mEngineEvents.addToQueue(
EngineEvents::OnActorHit{ getId(target), weaponId, attackerId, damageData, hitConfig });
}
void LuaManager::inputEvent(const InputEvent& event)
{
if (!MyGUI::InputManager::getInstance().isModalAny()

View File

@ -75,6 +75,8 @@ namespace MWLua
void noGame() override;
void objectAddedToScene(const MWWorld::Ptr& ptr) override;
void objectRemovedFromScene(const MWWorld::Ptr& ptr) override;
void actorHit(const MWWorld::Ptr& target, const MWWorld::Ptr& weapon, const MWWorld::Ptr& attacker,
const MWWorld::DamageData& damageData, const MWWorld::HitConfig& hitConfig) override;
void inputEvent(const InputEvent& event) override;
void itemConsumed(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) override
{

View File

@ -170,6 +170,34 @@ namespace MWLua
MWBase::Environment::get().getWindowManager()->setSelectedEnchantItem(*it);
}
void addActorHitBindings(const Context& context)
{
// Damage data usertype containing damage properties
sol::usertype<MWWorld::DamageData> dmgDataT
= context.mLua->sol().new_usertype<MWWorld::DamageData>("DamageData");
dmgDataT["damage"] = sol::readonly_property([](const MWWorld::DamageData& d) { return d.mDamage; });
dmgDataT["affectsHealth"]
= sol::readonly_property([](const MWWorld::DamageData& d) { return d.mAffectsHealth; });
dmgDataT["affectsFatigue"]
= sol::readonly_property([](const MWWorld::DamageData& d) { return d.mAffectsFatigue; });
dmgDataT["isSuccessful"] = sol::readonly_property([](const MWWorld::DamageData& d) { return d.mIsSuccessful; });
dmgDataT["hitPosition"] = sol::readonly_property([](const MWWorld::DamageData& d) { return d.mHitPosition; });
dmgDataT["forceMagical"] = sol::readonly_property([](const MWWorld::DamageData& d) { return d.mForceMagical; });
// Damage data usertype containing damage properties
sol::usertype<MWWorld::HitConfig> hitConfigT
= context.mLua->sol().new_usertype<MWWorld::HitConfig>("HitConfig");
hitConfigT["avoidHitReaction"]
= sol::readonly_property([](const MWWorld::HitConfig& h) { return h.mAvoidHitReaction; });
hitConfigT["avoidKnockdown"]
= sol::readonly_property([](const MWWorld::HitConfig& h) { return h.mAvoidKnockdown; });
hitConfigT["playVFX"] = sol::readonly_property([](const MWWorld::HitConfig& h) { return h.mPlayVFX; });
hitConfigT["playSFX"] = sol::readonly_property([](const MWWorld::HitConfig& h) { return h.mPlaySFX; });
hitConfigT["id"] = sol::readonly_property([](const MWWorld::HitConfig& h) { return h.mId; });
}
void addActorBindings(sol::table actor, const Context& context)
{
sol::state_view lua = context.sol();
@ -297,6 +325,45 @@ namespace MWLua
const MWWorld::Class& cls = o.ptr().getClass();
return cls.getCurrentSpeed(o.ptr());
};
actor["hit"]
= [](const Object& target, const sol::optional<Object> weapon, const sol::optional<Object> attacker,
const sol::object& damageDataObj, const sol::object& hitConfigObj) {
const MWWorld::Class& targetCls = target.ptr().getClass();
auto targetPtr = target.ptr();
MWWorld::Ptr weaponPtr;
MWWorld::Ptr attackerPtr;
if (weapon.has_value())
weaponPtr = weapon.value().ptr();
if (attacker.has_value())
attackerPtr = attacker.value().ptr();
sol::table d = LuaUtil::cast<sol::table>(damageDataObj);
sol::table h = LuaUtil::cast<sol::table>(hitConfigObj);
MWWorld::DamageData dmgData;
if (d["damage"] != sol::nil)
dmgData.mDamage = ((sol::object)d["damage"]).as<float>();
if (d["affectsHealth"] != sol::nil)
dmgData.mAffectsHealth = ((sol::object)d["affectsHealth"]).as<bool>();
if (d["affectsFatigue"] != sol::nil)
dmgData.mAffectsFatigue = ((sol::object)d["affectsFatigue"]).as<bool>();
if (d["isSuccessful"] != sol::nil)
dmgData.mIsSuccessful = ((sol::object)d["isSuccessful"]).as<bool>();
dmgData.mHitPosition = d.get_or("hitPosition", dmgData.mHitPosition);
if (d["forceMagical"] != sol::nil)
dmgData.mForceMagical = ((sol::object)d["forceMagical"]).as<bool>();
MWWorld::HitConfig hitConfig;
hitConfig.mAvoidHitReaction = h.get_or("avoidHitReaction", hitConfig.mAvoidHitReaction);
hitConfig.mAvoidKnockdown = h.get_or("avoidKnockdown", hitConfig.mAvoidKnockdown);
hitConfig.mPlayVFX = h.get_or("playVFX", hitConfig.mPlayVFX);
hitConfig.mPlaySFX = h.get_or("playSFX", hitConfig.mPlaySFX);
hitConfig.mId = h.get_or("id", hitConfig.mId);
targetCls.onHit(targetPtr, weaponPtr, attackerPtr, dmgData, hitConfig);
};
// for compatibility; should be removed later
actor["runSpeed"] = actor["getRunSpeed"];
@ -422,6 +489,6 @@ namespace MWLua
addActorStatsBindings(actor, context);
addActorMagicBindings(actor, context);
addActorHitBindings(context);
}
}

View File

@ -80,5 +80,7 @@ namespace MWLua
return LuaUtil::makeReadOnly(travelDests);
});
}
void addActorHitBindings(const Context& context);
}
#endif // MWLUA_ACTOR_H

View File

@ -240,8 +240,14 @@ namespace MWMechanics
if (Misc::Rng::roll0to99(world->getPrng()) >= getHitChance(attacker, victim, skillValue))
{
victim.getClass().onHit(victim, damage, false, projectile, attacker, osg::Vec3f(), false,
MWMechanics::DamageSourceType::Ranged);
MWWorld::DamageData dmgData = {
.mDamage = damage,
.mAffectsHealth = true,
.mAffectsFatigue = false,
.mIsSuccessful = false,
.mHitPosition = osg::Vec3f(),
};
victim.getClass().onHit(victim, projectile, attacker, dmgData, MWMechanics::DamageSourceType::Ranged);
MWMechanics::reduceWeaponCondition(damage, false, weapon, attacker);
return;
}
@ -299,8 +305,14 @@ namespace MWMechanics
victim.getClass().getContainerStore(victim).add(projectile, 1);
}
victim.getClass().onHit(
victim, damage, true, projectile, attacker, hitPosition, true, MWMechanics::DamageSourceType::Ranged);
MWWorld::DamageData dmgData = {
.mDamage = damage,
.mAffectsHealth = true,
.mAffectsFatigue = false,
.mIsSuccessful = true,
.mHitPosition = hitPosition,
};
victim.getClass().onHit(victim, projectile, attacker, dmgData, MWMechanics::DamageSourceType::Ranged);
}
}

View File

@ -357,8 +357,17 @@ namespace
// Notify the target actor they've been hit
bool isHarmful = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful;
if (target.getClass().isActor() && target != caster && !caster.isEmpty() && isHarmful)
target.getClass().onHit(
target, 0.0f, true, MWWorld::Ptr(), caster, osg::Vec3f(), true, MWMechanics::DamageSourceType::Magical);
{
MWWorld::DamageData dmgData = {
.mDamage = 0.0f,
.mAffectsHealth = true,
.mAffectsFatigue = false,
.mIsSuccessful = true,
.mHitPosition = osg::Vec3f(),
};
target.getClass().onHit(target, MWWorld::Ptr(), caster, dmgData, MWMechanics::DamageSourceType::Magical);
}
// Apply resistances
if (!(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Resistances))
{

View File

@ -118,9 +118,9 @@ namespace MWWorld
{
throw std::runtime_error("class cannot hit");
}
void Class::onHit(const Ptr& ptr, float damage, bool ishealth, const Ptr& object, const Ptr& attacker,
const osg::Vec3f& hitPosition, bool successful, const MWMechanics::DamageSourceType sourceType) const
void Class::onHit(const Ptr& target, const Ptr& weapon, const Ptr& attacker, const DamageData& damageData,
const HitConfig& hitConfig, const MWMechanics::DamageSourceType sourceType) const
{
throw std::runtime_error("class cannot be hit");
}

View File

@ -58,6 +58,25 @@ namespace MWWorld
class CellStore;
class Action;
struct DamageData
{
float mDamage;
bool mAffectsHealth = true;
bool mAffectsFatigue = false;
bool mIsSuccessful = true;
osg::Vec3f mHitPosition = osg::Vec3f(0, 0, 0);
bool mForceMagical = false;
};
struct HitConfig
{
bool mAvoidHitReaction = false;
bool mAvoidKnockdown = false;
bool mPlayVFX = true;
bool mPlaySFX = true;
std::string mId;
};
/// \brief Base class for referenceable esm records
class Class
{
@ -144,13 +163,13 @@ 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, const osg::Vec3f& hitPosition, bool successful,
const MWMechanics::DamageSourceType sourceType) const;
virtual void onHit(const Ptr& target, const Ptr& weapon, const Ptr& attacker, const DamageData& damageData,
const HitConfig& hitConfig = HitConfig(),const MWMechanics::DamageSourceType sourceType) const;
/// To do: Update the description
///< 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. \a successful specifies if the hit is
/// successful or not. \a sourceType classifies the damage source.
/// actor responsible for the attack, and \a successful specifies if the hit is
/// successful or not.
virtual std::unique_ptr<Action> activate(const Ptr& ptr, const Ptr& actor) const;
///< Generate action for activation (default implementation: return a null action).