From 5775a9af0cd4a0a1a7d795f8e0dd18acea161c86 Mon Sep 17 00:00:00 2001 From: Max Yari Date: Fri, 8 Dec 2023 13:46:11 +0100 Subject: [PATCH] WIP 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 --- apps/openmw/mwbase/luamanager.hpp | 4 + apps/openmw/mwclass/creature.cpp | 73 ++++++++------ apps/openmw/mwclass/creature.hpp | 6 +- apps/openmw/mwclass/npc.cpp | 115 ++++++++++++++--------- apps/openmw/mwclass/npc.hpp | 5 +- apps/openmw/mwlua/engineevents.cpp | 36 +++++++ apps/openmw/mwlua/engineevents.hpp | 12 ++- apps/openmw/mwlua/localscripts.cpp | 2 +- apps/openmw/mwlua/localscripts.hpp | 7 ++ apps/openmw/mwlua/luamanagerimp.cpp | 9 ++ apps/openmw/mwlua/luamanagerimp.hpp | 2 + apps/openmw/mwlua/types/actor.cpp | 69 +++++++++++++- apps/openmw/mwlua/types/actor.hpp | 2 + apps/openmw/mwmechanics/combat.cpp | 20 +++- apps/openmw/mwmechanics/spelleffects.cpp | 13 ++- apps/openmw/mwworld/class.cpp | 6 +- apps/openmw/mwworld/class.hpp | 29 +++++- 17 files changed, 318 insertions(+), 92 deletions(-) diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp index a5d6fe1114..193a900eed 100644 --- a/apps/openmw/mwbase/luamanager.hpp +++ b/apps/openmw/mwbase/luamanager.hpp @@ -9,6 +9,7 @@ #include "../mwgui/mode.hpp" #include "../mwrender/animationpriority.hpp" +#include "../mwworld/class.hpp" #include 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; diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp index 9224f6f0d8..637885956d 100644 --- a/apps/openmw/mwclass/creature.cpp +++ b/apps/openmw/mwclass/creature.cpp @@ -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()->mBase->mScript; + const ESM::RefId& script = target.get()->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 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 fatigue(stats.getFatigue()); fatigue.setCurrent(fatigue.getCurrent() - damage, true); stats.setFatigue(fatigue); diff --git a/apps/openmw/mwclass/creature.hpp b/apps/openmw/mwclass/creature.hpp index b8619128c2..8e223388b7 100644 --- a/apps/openmw/mwclass/creature.hpp +++ b/apps/openmw/mwclass/creature.hpp @@ -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 activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override; ///< Generate action for activation diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 0b61436d11..4b5ae75e8c 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -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().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(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 health(getCreatureStats(ptr).getHealth()); + MWMechanics::DynamicStat health(getCreatureStats(target).getHealth()); health.setCurrent(health.getCurrent() - damage); stats.setHealth(health); } - else + if (damageData.mAffectsFatigue) { - MWMechanics::DynamicStat fatigue(getCreatureStats(ptr).getFatigue()); + // If health was also damaged - damage was scaled using scaleDamage, is that undesirable here? + MWMechanics::DynamicStat 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 Npc::activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const diff --git a/apps/openmw/mwclass/npc.hpp b/apps/openmw/mwclass/npc.hpp index 29ab459242..b23f3d7999 100644 --- a/apps/openmw/mwclass/npc.hpp +++ b/apps/openmw/mwclass/npc.hpp @@ -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& models) const override; diff --git a/apps/openmw/mwlua/engineevents.cpp b/apps/openmw/mwlua/engineevents.cpp index 6c652bccba..ae153d7dad 100644 --- a/apps/openmw/mwlua/engineevents.cpp +++ b/apps/openmw/mwlua/engineevents.cpp @@ -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 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); diff --git a/apps/openmw/mwlua/engineevents.hpp b/apps/openmw/mwlua/engineevents.hpp index fb9183eb7c..240239ad2c 100644 --- a/apps/openmw/mwlua/engineevents.hpp +++ b/apps/openmw/mwlua/engineevents.hpp @@ -1,11 +1,13 @@ #ifndef MWLUA_ENGINEEVENTS_H #define MWLUA_ENGINEEVENTS_H +#include #include #include // 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 mWeapon; + std::optional 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; void clear() { mQueue.clear(); } diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp index 7a3e9ff23a..a466f61356 100644 --- a/apps/openmw/mwlua/localscripts.cpp +++ b/apps/openmw/mwlua/localscripts.cpp @@ -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) diff --git a/apps/openmw/mwlua/localscripts.hpp b/apps/openmw/mwlua/localscripts.hpp index adbf20292d..95aa12ef7c 100644 --- a/apps/openmw/mwlua/localscripts.hpp +++ b/apps/openmw/mwlua/localscripts.hpp @@ -7,6 +7,7 @@ #include #include + #include #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 weapon, + sol::optional 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" }; diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 5fa2d9867c..04b43aab90 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -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(getId(weapon)) : std::nullopt; + auto attackerId = !weapon.isEmpty() ? std::optional(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() diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index 3f2135e9c9..b3356744d9 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -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 { diff --git a/apps/openmw/mwlua/types/actor.cpp b/apps/openmw/mwlua/types/actor.cpp index 413a656e90..b8af586102 100644 --- a/apps/openmw/mwlua/types/actor.cpp +++ b/apps/openmw/mwlua/types/actor.cpp @@ -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 dmgDataT + = context.mLua->sol().new_usertype("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 hitConfigT + = context.mLua->sol().new_usertype("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 weapon, const sol::optional 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(damageDataObj); + sol::table h = LuaUtil::cast(hitConfigObj); + + MWWorld::DamageData dmgData; + if (d["damage"] != sol::nil) + dmgData.mDamage = ((sol::object)d["damage"]).as(); + if (d["affectsHealth"] != sol::nil) + dmgData.mAffectsHealth = ((sol::object)d["affectsHealth"]).as(); + if (d["affectsFatigue"] != sol::nil) + dmgData.mAffectsFatigue = ((sol::object)d["affectsFatigue"]).as(); + if (d["isSuccessful"] != sol::nil) + dmgData.mIsSuccessful = ((sol::object)d["isSuccessful"]).as(); + dmgData.mHitPosition = d.get_or("hitPosition", dmgData.mHitPosition); + if (d["forceMagical"] != sol::nil) + dmgData.mForceMagical = ((sol::object)d["forceMagical"]).as(); + + 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); } - } diff --git a/apps/openmw/mwlua/types/actor.hpp b/apps/openmw/mwlua/types/actor.hpp index 425e44451b..0e0fda47f3 100644 --- a/apps/openmw/mwlua/types/actor.hpp +++ b/apps/openmw/mwlua/types/actor.hpp @@ -80,5 +80,7 @@ namespace MWLua return LuaUtil::makeReadOnly(travelDests); }); } + + void addActorHitBindings(const Context& context); } #endif // MWLUA_ACTOR_H diff --git a/apps/openmw/mwmechanics/combat.cpp b/apps/openmw/mwmechanics/combat.cpp index e7c7342284..b705412862 100644 --- a/apps/openmw/mwmechanics/combat.cpp +++ b/apps/openmw/mwmechanics/combat.cpp @@ -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); } } diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index 7035c7f61c..29572fdfb6 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -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)) { diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp index 105fbca80a..99fde818ce 100644 --- a/apps/openmw/mwworld/class.cpp +++ b/apps/openmw/mwworld/class.cpp @@ -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"); } diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp index d3d75aa935..2717890f17 100644 --- a/apps/openmw/mwworld/class.hpp +++ b/apps/openmw/mwworld/class.hpp @@ -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 activate(const Ptr& ptr, const Ptr& actor) const; ///< Generate action for activation (default implementation: return a null action).