mirror of
https://github.com/OpenMW/openmw.git
synced 2025-10-15 02:26:33 +00:00
[Lua] Partially dehardcode onHit
This commit is contained in:
parent
2efa76052f
commit
34eb3d485d
59 changed files with 978 additions and 331 deletions
|
@ -82,7 +82,7 @@ message(STATUS "Configuring OpenMW...")
|
|||
set(OPENMW_VERSION_MAJOR 0)
|
||||
set(OPENMW_VERSION_MINOR 50)
|
||||
set(OPENMW_VERSION_RELEASE 0)
|
||||
set(OPENMW_LUA_API_REVISION 84)
|
||||
set(OPENMW_LUA_API_REVISION 85)
|
||||
set(OPENMW_POSTPROCESSING_API_REVISION 3)
|
||||
|
||||
set(OPENMW_VERSION_COMMITHASH "")
|
||||
|
|
|
@ -163,8 +163,6 @@ bool Launcher::SettingsPage::loadSettings()
|
|||
loadSettingInt(Settings::physics().mAsyncNumThreads, *physicsThreadsSpinBox);
|
||||
loadSettingBool(
|
||||
Settings::game().mAllowActorsToFollowOverWaterSurface, *allowNPCToFollowOverWaterSurfaceCheckBox);
|
||||
loadSettingBool(
|
||||
Settings::game().mUnarmedCreatureAttacksDamageArmor, *unarmedCreatureAttacksDamageArmorCheckBox);
|
||||
loadSettingInt(Settings::game().mActorCollisionShapeType, *actorCollisonShapeTypeComboBox);
|
||||
}
|
||||
|
||||
|
@ -373,8 +371,6 @@ void Launcher::SettingsPage::saveSettings()
|
|||
saveSettingInt(*physicsThreadsSpinBox, Settings::physics().mAsyncNumThreads);
|
||||
saveSettingBool(
|
||||
*allowNPCToFollowOverWaterSurfaceCheckBox, Settings::game().mAllowActorsToFollowOverWaterSurface);
|
||||
saveSettingBool(
|
||||
*unarmedCreatureAttacksDamageArmorCheckBox, Settings::game().mUnarmedCreatureAttacksDamageArmor);
|
||||
saveSettingInt(*actorCollisonShapeTypeComboBox, Settings::game().mActorCollisionShapeType);
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="1">
|
||||
<item row="9" column="1">
|
||||
<widget class="QCheckBox" name="normaliseRaceSpeedCheckBox">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Don't use race weight in NPC movement speed calculations.</p></body></html></string>
|
||||
|
@ -63,7 +63,7 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="1">
|
||||
<item row="8" column="1">
|
||||
<widget class="QCheckBox" name="classicCalmSpellsCheckBox">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Stops combat with NPCs affected by Calm spells every frame -- like in Morrowind without the MCP.</p></body></html></string>
|
||||
|
@ -73,7 +73,7 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="1">
|
||||
<item row="11" column="1">
|
||||
<widget class="QCheckBox" name="avoidCollisionsCheckBox">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>If enabled NPCs apply evasion maneuver to avoid collisions with others.</p></body></html></string>
|
||||
|
@ -123,7 +123,7 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<item row="5" column="1">
|
||||
<widget class="QCheckBox" name="requireAppropriateAmmunitionCheckBox">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>If enabled, a magical ammunition is required to bypass normal weapon resistance or weakness. If disabled, a magical ranged weapon or a magical ammunition is required.</p></body></html></string>
|
||||
|
@ -133,7 +133,7 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="13" column="1">
|
||||
<item row="12" column="1">
|
||||
<widget class="QCheckBox" name="graphicHerbalismCheckBox">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>If this setting is true, containers supporting graphic herbalism will do so instead of opening the menu.</p></body></html></string>
|
||||
|
@ -143,7 +143,7 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="11" column="1">
|
||||
<item row="10" column="1">
|
||||
<widget class="QCheckBox" name="swimUpwardCorrectionCheckBox">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Makes player swim a bit upward from the line of sight. Applies only in third person mode. Intended to make simpler swimming without diving.</p></body></html></string>
|
||||
|
@ -153,7 +153,7 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<item row="7" column="1">
|
||||
<widget class="QCheckBox" name="enchantedWeaponsMagicalCheckBox">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Make enchanted weapons without Magical flag bypass normal weapons resistance, like in Morrowind.</p></body></html></string>
|
||||
|
@ -183,7 +183,7 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<item row="6" column="1">
|
||||
<widget class="QCheckBox" name="canLootDuringDeathAnimationCheckBox">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>If this setting is true, the player is allowed to loot actors (e.g. summoned creatures) during death animation, if they are not in combat. In this case we have to increment death counter and run disposed actor's script instantly.</p><p>If this setting is false, player has to wait until end of death animation in all cases. Makes using of summoned creatures exploit (looting summoned Dremoras and Golden Saints for expensive weapons) a lot harder. Conflicts with mannequin mods, which use SkipAnim to prevent end of death animation.</p></body></html></string>
|
||||
|
@ -203,7 +203,7 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<item row="4" column="1">
|
||||
<widget class="QCheckBox" name="classicReflectedAbsorbSpellsCheckBox">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Effects of reflected Absorb spells are not mirrored - like in Morrowind.</p></body></html></string>
|
||||
|
@ -213,16 +213,6 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QCheckBox" name="unarmedCreatureAttacksDamageArmorCheckBox">
|
||||
<property name="toolTip">
|
||||
<string><html><head/><body><p>Makes unarmed creature attacks able to reduce armor condition, just as attacks from NPCs and armed creatures.</p></body></html></string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Unarmed Creature Attacks Damage Armor</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#include <SDL_events.h>
|
||||
|
||||
#include "../mwgui/mode.hpp"
|
||||
#include "../mwmechanics/damagesourcetype.hpp"
|
||||
#include "../mwrender/animationpriority.hpp"
|
||||
#include <components/sdlutil/events.hpp>
|
||||
|
||||
|
@ -39,6 +40,11 @@ namespace LuaUtil
|
|||
}
|
||||
}
|
||||
|
||||
namespace osg
|
||||
{
|
||||
class Vec3f;
|
||||
}
|
||||
|
||||
namespace MWBase
|
||||
{
|
||||
// \brief LuaManager is the central interface through which the engine invokes lua scripts.
|
||||
|
@ -71,6 +77,10 @@ namespace MWBase
|
|||
= 0;
|
||||
virtual void skillLevelUp(const MWWorld::Ptr& actor, ESM::RefId skillId, std::string_view source) = 0;
|
||||
virtual void skillUse(const MWWorld::Ptr& actor, ESM::RefId skillId, int useType, float scale) = 0;
|
||||
virtual void onHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& weapon,
|
||||
const MWWorld::Ptr& ammo, int attackType, float attackStrength, float damage, bool isHealth,
|
||||
const osg::Vec3f& hitPos, bool successful, MWMechanics::DamageSourceType)
|
||||
= 0;
|
||||
virtual void exteriorCreated(MWWorld::CellStore& cell) = 0;
|
||||
virtual void actorDied(const MWWorld::Ptr& actor) = 0;
|
||||
virtual void questUpdated(const ESM::RefId& questId, int stage) = 0;
|
||||
|
|
|
@ -526,9 +526,6 @@ namespace MWBase
|
|||
/// Spawn a random creature from a levelled list next to the player
|
||||
virtual void spawnRandomCreature(const ESM::RefId& creatureList) = 0;
|
||||
|
||||
/// Spawn a blood effect for \a ptr at \a worldPosition
|
||||
virtual void spawnBloodEffect(const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) = 0;
|
||||
|
||||
virtual void spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride,
|
||||
const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true, bool useAmbientLight = true)
|
||||
= 0;
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
#include "../mwbase/environment.hpp"
|
||||
#include "../mwbase/windowmanager.hpp"
|
||||
|
||||
#include "../mwlua/localscripts.hpp"
|
||||
|
||||
#include "../mwworld/actionequip.hpp"
|
||||
#include "../mwworld/cellstore.hpp"
|
||||
#include "../mwworld/containerstore.hpp"
|
||||
|
@ -109,8 +111,23 @@ namespace MWClass
|
|||
return std::make_pair(slots_, false);
|
||||
}
|
||||
|
||||
ESM::RefId Armor::getEquipmentSkill(const MWWorld::ConstPtr& ptr) const
|
||||
ESM::RefId Armor::getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const
|
||||
{
|
||||
// We don't actually need an actor as such. We just need an object that has
|
||||
// lua scripts and the Combat interface.
|
||||
if (useLuaInterfaceIfAvailable)
|
||||
{
|
||||
// In this interface call, both objects are effectively const, so stripping Const from the ConstPtr is fine.
|
||||
MWWorld::Ptr mutablePtr(
|
||||
const_cast<MWWorld::LiveCellRefBase*>(ptr.mRef), const_cast<MWWorld::CellStore*>(ptr.mCell));
|
||||
auto res = MWLua::LocalScripts::callPlayerInterface<std::string>(
|
||||
"Combat", "getArmorSkill", MWLua::LObject(mutablePtr));
|
||||
if (res)
|
||||
return ESM::RefId::deserializeText(res.value());
|
||||
}
|
||||
|
||||
// Fallback to the old engine implementation when actors don't have their scripts attached yet.
|
||||
|
||||
const MWWorld::LiveCellRef<ESM::Armor>* ref = ptr.get<ESM::Armor>();
|
||||
|
||||
std::string_view typeGmst;
|
||||
|
@ -175,7 +192,7 @@ namespace MWClass
|
|||
|
||||
const ESM::RefId& Armor::getUpSoundId(const MWWorld::ConstPtr& ptr) const
|
||||
{
|
||||
const ESM::RefId es = getEquipmentSkill(ptr);
|
||||
const ESM::RefId es = getEquipmentSkill(ptr, false);
|
||||
static const ESM::RefId lightUp = ESM::RefId::stringRefId("Item Armor Light Up");
|
||||
static const ESM::RefId mediumUp = ESM::RefId::stringRefId("Item Armor Medium Up");
|
||||
static const ESM::RefId heavyUp = ESM::RefId::stringRefId("Item Armor Heavy Up");
|
||||
|
@ -190,7 +207,7 @@ namespace MWClass
|
|||
|
||||
const ESM::RefId& Armor::getDownSoundId(const MWWorld::ConstPtr& ptr) const
|
||||
{
|
||||
const ESM::RefId es = getEquipmentSkill(ptr);
|
||||
const ESM::RefId es = getEquipmentSkill(ptr, false);
|
||||
static const ESM::RefId lightDown = ESM::RefId::stringRefId("Item Armor Light Down");
|
||||
static const ESM::RefId mediumDown = ESM::RefId::stringRefId("Item Armor Medium Down");
|
||||
static const ESM::RefId heavyDown = ESM::RefId::stringRefId("Item Armor Heavy Down");
|
||||
|
@ -221,24 +238,29 @@ namespace MWClass
|
|||
std::string text;
|
||||
|
||||
// get armor type string (light/medium/heavy)
|
||||
std::string_view typeText;
|
||||
std::string typeText;
|
||||
if (ref->mBase->mData.mWeight == 0)
|
||||
{
|
||||
// no type
|
||||
}
|
||||
else
|
||||
{
|
||||
const ESM::RefId armorType = getEquipmentSkill(ptr);
|
||||
const ESM::RefId armorType = getEquipmentSkill(ptr, true);
|
||||
if (armorType == ESM::Skill::LightArmor)
|
||||
typeText = "#{sLight}";
|
||||
else if (armorType == ESM::Skill::MediumArmor)
|
||||
typeText = "#{sMedium}";
|
||||
else
|
||||
else if (armorType == ESM::Skill::HeavyArmor)
|
||||
typeText = "#{sHeavy}";
|
||||
// For other skills, just subtitute the skill name
|
||||
// Normally you would never see this case, but modding allows getEquipmentSkill() to return any skill.
|
||||
else
|
||||
typeText = "#{sSkill" + armorType.toString() + "}";
|
||||
}
|
||||
|
||||
text += "\n#{sArmorRating}: "
|
||||
+ MWGui::ToolTips::toString(static_cast<int>(getEffectiveArmorRating(ptr, MWMechanics::getPlayer())));
|
||||
+ MWGui::ToolTips::toString(
|
||||
static_cast<int>(getSkillAdjustedArmorRating(ptr, MWMechanics::getPlayer(), true)));
|
||||
|
||||
int remainingHealth = getItemHealth(ptr);
|
||||
text += "\n#{sCondition}: " + MWGui::ToolTips::toString(remainingHealth) + "/"
|
||||
|
@ -289,11 +311,25 @@ namespace MWClass
|
|||
return record->mId;
|
||||
}
|
||||
|
||||
float Armor::getEffectiveArmorRating(const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& actor) const
|
||||
float Armor::getSkillAdjustedArmorRating(
|
||||
const MWWorld::ConstPtr& ptr, const MWWorld::Ptr& actor, bool useLuaInterfaceIfAvailable) const
|
||||
{
|
||||
if (useLuaInterfaceIfAvailable && actor == MWMechanics::getPlayer())
|
||||
{
|
||||
// In this interface call, both objects are effectively const, so stripping Const from the ConstPtr is fine.
|
||||
MWWorld::Ptr mutablePtr(
|
||||
const_cast<MWWorld::LiveCellRefBase*>(ptr.mRef), const_cast<MWWorld::CellStore*>(ptr.mCell));
|
||||
auto res = MWLua::LocalScripts::callPlayerInterface<float>(
|
||||
"Combat", "getSkillAdjustedArmorRating", MWLua::LObject(mutablePtr), MWLua::LObject(actor));
|
||||
if (res)
|
||||
return res.value();
|
||||
}
|
||||
|
||||
// Fallback to the old engine implementation when actors don't have their scripts attached yet.
|
||||
|
||||
const MWWorld::LiveCellRef<ESM::Armor>* ref = ptr.get<ESM::Armor>();
|
||||
|
||||
const ESM::RefId armorSkillType = getEquipmentSkill(ptr);
|
||||
const ESM::RefId armorSkillType = getEquipmentSkill(ptr, useLuaInterfaceIfAvailable);
|
||||
float armorSkill = actor.getClass().getSkill(actor, armorSkillType);
|
||||
|
||||
int iBaseArmorSkill = MWBase::Environment::get()
|
||||
|
|
|
@ -41,7 +41,7 @@ namespace MWClass
|
|||
///< \return first: Return IDs of the slot this object can be equipped in; second: can object
|
||||
/// stay stacked when equipped?
|
||||
|
||||
ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr) const override;
|
||||
ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const override;
|
||||
|
||||
MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override;
|
||||
///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip.
|
||||
|
@ -81,7 +81,8 @@ namespace MWClass
|
|||
bool canSell(const MWWorld::ConstPtr& item, int npcServices) const override;
|
||||
|
||||
/// Get the effective armor rating, factoring in the actor's skills, for the given armor.
|
||||
float getEffectiveArmorRating(const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor) const override;
|
||||
float getSkillAdjustedArmorRating(
|
||||
const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor, bool useLuaInterfaceIfAvailable) const override;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -98,7 +98,7 @@ namespace MWClass
|
|||
return std::make_pair(slots_, false);
|
||||
}
|
||||
|
||||
ESM::RefId Clothing::getEquipmentSkill(const MWWorld::ConstPtr& ptr) const
|
||||
ESM::RefId Clothing::getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const
|
||||
{
|
||||
const MWWorld::LiveCellRef<ESM::Clothing>* ref = ptr.get<ESM::Clothing>();
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ namespace MWClass
|
|||
///< \return first: Return IDs of the slot this object can be equipped in; second: can object
|
||||
/// stay stacked when equipped?
|
||||
|
||||
ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr) const override;
|
||||
ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const override;
|
||||
|
||||
MWGui::ToolTipInfo getToolTipInfo(const MWWorld::ConstPtr& ptr, int count) const override;
|
||||
///< @return the content of the tool tip to be displayed. raises exception if the object has no tooltip.
|
||||
|
|
|
@ -25,11 +25,14 @@
|
|||
#include "../mwmechanics/setbaseaisetting.hpp"
|
||||
|
||||
#include "../mwbase/environment.hpp"
|
||||
#include "../mwbase/luamanager.hpp"
|
||||
#include "../mwbase/mechanicsmanager.hpp"
|
||||
#include "../mwbase/soundmanager.hpp"
|
||||
#include "../mwbase/windowmanager.hpp"
|
||||
#include "../mwbase/world.hpp"
|
||||
|
||||
#include "../mwlua/localscripts.hpp"
|
||||
|
||||
#include "../mwworld/actionopen.hpp"
|
||||
#include "../mwworld/actiontalk.hpp"
|
||||
#include "../mwworld/cellstore.hpp"
|
||||
|
@ -283,8 +286,8 @@ namespace MWClass
|
|||
|
||||
if (!success)
|
||||
{
|
||||
victim.getClass().onHit(
|
||||
victim, 0.0f, false, MWWorld::Ptr(), ptr, osg::Vec3f(), false, MWMechanics::DamageSourceType::Melee);
|
||||
MWBase::Environment::get().getLuaManager()->onHit(ptr, victim, weapon, MWWorld::Ptr(), type, attackStrength,
|
||||
0.0f, false, hitPosition, false, MWMechanics::DamageSourceType::Melee);
|
||||
MWMechanics::reduceWeaponCondition(0.f, false, weapon, ptr);
|
||||
return;
|
||||
}
|
||||
|
@ -342,12 +345,12 @@ namespace MWClass
|
|||
|
||||
MWMechanics::diseaseContact(victim, ptr);
|
||||
|
||||
victim.getClass().onHit(
|
||||
victim, damage, healthdmg, weapon, ptr, hitPosition, true, MWMechanics::DamageSourceType::Melee);
|
||||
MWBase::Environment::get().getLuaManager()->onHit(ptr, victim, weapon, MWWorld::Ptr(), type, attackStrength,
|
||||
damage, healthdmg, hitPosition, true, 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,
|
||||
void Creature::onHit(const MWWorld::Ptr& ptr, const std::map<std::string, float>& damages,
|
||||
const MWWorld::Ptr& object, const MWWorld::Ptr& attacker, bool successful,
|
||||
const MWMechanics::DamageSourceType sourceType) const
|
||||
{
|
||||
MWMechanics::CreatureStats& stats = getCreatureStats(ptr);
|
||||
|
@ -397,19 +400,44 @@ namespace MWClass
|
|||
if (!successful)
|
||||
{
|
||||
// Missed
|
||||
if (!attacker.isEmpty() && attacker == MWMechanics::getPlayer())
|
||||
MWBase::Environment::get().getSoundManager()->playSound3D(
|
||||
ptr, ESM::RefId::stringRefId("miss"), 1.0f, 1.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!object.isEmpty())
|
||||
stats.setLastHitObject(object.getCellRef().getRefId());
|
||||
|
||||
if (damage < 0.001f)
|
||||
damage = 0;
|
||||
bool hasDamage = false;
|
||||
bool hasHealthDamage = false;
|
||||
float healthDamage = 0.f;
|
||||
for (auto& [stat, damage] : damages)
|
||||
{
|
||||
if (damage < 0.001f)
|
||||
continue;
|
||||
hasDamage = true;
|
||||
|
||||
if (damage > 0.f)
|
||||
if (stat == "health")
|
||||
{
|
||||
hasHealthDamage = true;
|
||||
healthDamage = damage;
|
||||
MWMechanics::DynamicStat<float> health(getCreatureStats(ptr).getHealth());
|
||||
health.setCurrent(health.getCurrent() - damage);
|
||||
stats.setHealth(health);
|
||||
}
|
||||
else if (stat == "fatigue")
|
||||
{
|
||||
MWMechanics::DynamicStat<float> fatigue(getCreatureStats(ptr).getFatigue());
|
||||
fatigue.setCurrent(fatigue.getCurrent() - damage, true);
|
||||
stats.setFatigue(fatigue);
|
||||
}
|
||||
else if (stat == "magicka")
|
||||
{
|
||||
MWMechanics::DynamicStat<float> magicka(getCreatureStats(ptr).getMagicka());
|
||||
magicka.setCurrent(magicka.getCurrent() - damage);
|
||||
stats.setMagicka(magicka);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasDamage)
|
||||
{
|
||||
if (!attacker.isEmpty())
|
||||
{
|
||||
|
@ -420,35 +448,11 @@ 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 (hasHealthDamage && agilityTerm <= healthDamage && knockdownTerm <= Misc::Rng::roll0to99(prng))
|
||||
stats.setKnockedDown(true);
|
||||
else
|
||||
stats.setHitRecovery(true); // Is this supposed to always occur?
|
||||
}
|
||||
|
||||
if (ishealth)
|
||||
{
|
||||
damage *= damage / (damage + getArmorRating(ptr));
|
||||
damage = std::max(1.f, damage);
|
||||
if (!attacker.isEmpty())
|
||||
{
|
||||
damage = scaleDamage(damage, attacker, ptr);
|
||||
MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition);
|
||||
}
|
||||
|
||||
MWBase::Environment::get().getSoundManager()->playSound3D(
|
||||
ptr, ESM::RefId::stringRefId("Health Damage"), 1.0f, 1.0f);
|
||||
|
||||
MWMechanics::DynamicStat<float> health(stats.getHealth());
|
||||
health.setCurrent(health.getCurrent() - damage);
|
||||
stats.setHealth(health);
|
||||
}
|
||||
else
|
||||
{
|
||||
MWMechanics::DynamicStat<float> fatigue(stats.getFatigue());
|
||||
fatigue.setCurrent(fatigue.getCurrent() - damage, true);
|
||||
stats.setFatigue(fatigue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -591,7 +595,7 @@ namespace MWClass
|
|||
return info;
|
||||
}
|
||||
|
||||
float Creature::getArmorRating(const MWWorld::Ptr& ptr) const
|
||||
float Creature::getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable) const
|
||||
{
|
||||
// Equipment armor rating is deliberately ignored.
|
||||
return getCreatureStats(ptr).getMagicEffects().getOrDefault(ESM::MagicEffect::Shield).getMagnitude();
|
||||
|
@ -764,11 +768,6 @@ namespace MWClass
|
|||
}
|
||||
}
|
||||
|
||||
int Creature::getBloodTexture(const MWWorld::ConstPtr& ptr) const
|
||||
{
|
||||
return ptr.get<ESM::Creature>()->mBase->mBloodType;
|
||||
}
|
||||
|
||||
void Creature::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const
|
||||
{
|
||||
if (!state.mHasCustomState)
|
||||
|
|
|
@ -66,8 +66,8 @@ 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& ptr, const std::map<std::string, float>& damages, const MWWorld::Ptr& object,
|
||||
const MWWorld::Ptr& attacker, bool successful,
|
||||
const MWMechanics::DamageSourceType sourceType) const override;
|
||||
|
||||
std::unique_ptr<MWWorld::Action> activate(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor) const override;
|
||||
|
@ -88,7 +88,7 @@ namespace MWClass
|
|||
///< Return total weight that fits into the object. Throws an exception, if the object can't
|
||||
/// hold other objects.
|
||||
|
||||
float getArmorRating(const MWWorld::Ptr& ptr) const override;
|
||||
float getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable) const override;
|
||||
///< @return combined armor rating of this actor
|
||||
|
||||
bool isEssential(const MWWorld::ConstPtr& ptr) const override;
|
||||
|
@ -118,9 +118,6 @@ namespace MWClass
|
|||
|
||||
float getSkill(const MWWorld::Ptr& ptr, ESM::RefId id) const override;
|
||||
|
||||
/// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini)
|
||||
int getBloodTexture(const MWWorld::ConstPtr& ptr) const override;
|
||||
|
||||
void readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override;
|
||||
///< Read additional state from \a state into \a ptr.
|
||||
|
||||
|
|
|
@ -29,6 +29,8 @@
|
|||
#include "../mwbase/windowmanager.hpp"
|
||||
#include "../mwbase/world.hpp"
|
||||
|
||||
#include "../mwlua/localscripts.hpp"
|
||||
|
||||
#include "../mwmechanics/actorutil.hpp"
|
||||
#include "../mwmechanics/aisetting.hpp"
|
||||
#include "../mwmechanics/autocalcspell.hpp"
|
||||
|
@ -620,8 +622,8 @@ namespace MWClass
|
|||
float damage = 0.0f;
|
||||
if (!success)
|
||||
{
|
||||
othercls.onHit(
|
||||
victim, damage, false, weapon, ptr, osg::Vec3f(), false, MWMechanics::DamageSourceType::Melee);
|
||||
MWBase::Environment::get().getLuaManager()->onHit(ptr, victim, weapon, MWWorld::Ptr(), type, attackStrength,
|
||||
damage, false, hitPosition, false, MWMechanics::DamageSourceType::Melee);
|
||||
MWMechanics::reduceWeaponCondition(damage, false, weapon, ptr);
|
||||
MWMechanics::resistNormalWeapon(victim, ptr, weapon, damage);
|
||||
return;
|
||||
|
@ -694,14 +696,13 @@ namespace MWClass
|
|||
|
||||
MWMechanics::diseaseContact(victim, ptr);
|
||||
|
||||
othercls.onHit(victim, damage, healthdmg, weapon, ptr, hitPosition, true, MWMechanics::DamageSourceType::Melee);
|
||||
MWBase::Environment::get().getLuaManager()->onHit(ptr, victim, weapon, MWWorld::Ptr(), type, attackStrength,
|
||||
damage, healthdmg, hitPosition, true, MWMechanics::DamageSourceType::Melee);
|
||||
}
|
||||
|
||||
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,
|
||||
const MWMechanics::DamageSourceType sourceType) const
|
||||
void Npc::onHit(const MWWorld::Ptr& ptr, const std::map<std::string, float>& damages, const MWWorld::Ptr& object,
|
||||
const MWWorld::Ptr& attacker, bool successful, const MWMechanics::DamageSourceType sourceType) const
|
||||
{
|
||||
MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager();
|
||||
MWMechanics::CreatureStats& stats = getCreatureStats(ptr);
|
||||
bool wasDead = stats.isDead();
|
||||
|
||||
|
@ -748,23 +749,47 @@ namespace MWClass
|
|||
if (!successful)
|
||||
{
|
||||
// Missed
|
||||
if (!attacker.isEmpty() && attacker == MWMechanics::getPlayer())
|
||||
sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("miss"), 1.0f, 1.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!object.isEmpty())
|
||||
stats.setLastHitObject(object.getCellRef().getRefId());
|
||||
|
||||
if (damage < 0.001f)
|
||||
damage = 0;
|
||||
if (ptr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState())
|
||||
return;
|
||||
|
||||
bool godmode = ptr == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState();
|
||||
bool hasDamage = false;
|
||||
bool hasHealthDamage = false;
|
||||
float healthDamage = 0.f;
|
||||
for (auto& [stat, damage] : damages)
|
||||
{
|
||||
if (damage < 0.001f)
|
||||
continue;
|
||||
hasDamage = true;
|
||||
|
||||
if (godmode)
|
||||
damage = 0;
|
||||
if (stat == "health")
|
||||
{
|
||||
hasHealthDamage = true;
|
||||
healthDamage = damage;
|
||||
MWMechanics::DynamicStat<float> health(getCreatureStats(ptr).getHealth());
|
||||
health.setCurrent(health.getCurrent() - damage);
|
||||
stats.setHealth(health);
|
||||
}
|
||||
else if (stat == "fatigue")
|
||||
{
|
||||
MWMechanics::DynamicStat<float> fatigue(getCreatureStats(ptr).getFatigue());
|
||||
fatigue.setCurrent(fatigue.getCurrent() - damage, true);
|
||||
stats.setFatigue(fatigue);
|
||||
}
|
||||
else if (stat == "magicka")
|
||||
{
|
||||
MWMechanics::DynamicStat<float> magicka(getCreatureStats(ptr).getMagicka());
|
||||
magicka.setCurrent(magicka.getCurrent() - damage);
|
||||
stats.setMagicka(magicka);
|
||||
}
|
||||
}
|
||||
|
||||
if (damage > 0.0f && !attacker.isEmpty())
|
||||
if (hasDamage && !attacker.isEmpty())
|
||||
{
|
||||
// 'ptr' is losing health. Play a 'hit' voiced dialog entry if not already saying
|
||||
// something, alert the character controller, scripts, etc.
|
||||
|
@ -783,109 +808,16 @@ 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 (hasHealthDamage && agilityTerm <= healthDamage && knockdownTerm <= Misc::Rng::roll0to99(prng))
|
||||
stats.setKnockedDown(true);
|
||||
else
|
||||
stats.setHitRecovery(true); // Is this supposed to always occur?
|
||||
|
||||
if (damage > 0 && ishealth)
|
||||
{
|
||||
// Hit percentages:
|
||||
// cuirass = 30%
|
||||
// shield, helmet, greaves, boots, pauldrons = 10% each
|
||||
// guantlets = 5% each
|
||||
static const int hitslots[20]
|
||||
= { MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass,
|
||||
MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass,
|
||||
MWWorld::InventoryStore::Slot_Cuirass, MWWorld::InventoryStore::Slot_Cuirass,
|
||||
MWWorld::InventoryStore::Slot_CarriedLeft, MWWorld::InventoryStore::Slot_CarriedLeft,
|
||||
MWWorld::InventoryStore::Slot_Helmet, MWWorld::InventoryStore::Slot_Helmet,
|
||||
MWWorld::InventoryStore::Slot_Greaves, MWWorld::InventoryStore::Slot_Greaves,
|
||||
MWWorld::InventoryStore::Slot_Boots, MWWorld::InventoryStore::Slot_Boots,
|
||||
MWWorld::InventoryStore::Slot_LeftPauldron, MWWorld::InventoryStore::Slot_LeftPauldron,
|
||||
MWWorld::InventoryStore::Slot_RightPauldron, MWWorld::InventoryStore::Slot_RightPauldron,
|
||||
MWWorld::InventoryStore::Slot_LeftGauntlet, MWWorld::InventoryStore::Slot_RightGauntlet };
|
||||
int hitslot = hitslots[Misc::Rng::rollDice(20, prng)];
|
||||
|
||||
float unmitigatedDamage = damage;
|
||||
float x = damage / (damage + getArmorRating(ptr));
|
||||
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::ContainerStoreIterator armorslot = inv.getSlot(hitslot);
|
||||
MWWorld::Ptr armor = ((armorslot != inv.end()) ? *armorslot : MWWorld::Ptr());
|
||||
bool hasArmor = !armor.isEmpty() && armor.getType() == ESM::Armor::sRecordId;
|
||||
// If there's no item in the carried left slot or if it is not a shield redistribute the hit.
|
||||
if (!hasArmor && hitslot == MWWorld::InventoryStore::Slot_CarriedLeft)
|
||||
{
|
||||
if (Misc::Rng::rollDice(2, prng) == 0)
|
||||
hitslot = MWWorld::InventoryStore::Slot_Cuirass;
|
||||
else
|
||||
hitslot = MWWorld::InventoryStore::Slot_LeftPauldron;
|
||||
armorslot = inv.getSlot(hitslot);
|
||||
if (armorslot != inv.end())
|
||||
{
|
||||
armor = *armorslot;
|
||||
hasArmor = !armor.isEmpty() && armor.getType() == ESM::Armor::sRecordId;
|
||||
}
|
||||
}
|
||||
if (hasArmor)
|
||||
{
|
||||
// Unarmed creature attacks don't affect armor condition unless it was
|
||||
// explicitly requested.
|
||||
if (!object.isEmpty() || attacker.isEmpty() || attacker.getClass().isNpc()
|
||||
|| Settings::game().mUnarmedCreatureAttacksDamageArmor)
|
||||
{
|
||||
int armorhealth = armor.getClass().getItemHealth(armor);
|
||||
armorhealth -= std::min(damageDiff, armorhealth);
|
||||
armor.getCellRef().setCharge(armorhealth);
|
||||
|
||||
// Armor broken? unequip it
|
||||
if (armorhealth == 0)
|
||||
armor = *inv.unequipItem(armor);
|
||||
}
|
||||
|
||||
ESM::RefId skill = armor.getClass().getEquipmentSkill(armor);
|
||||
if (ptr == 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);
|
||||
else if (skill == ESM::Skill::MediumArmor)
|
||||
sndMgr->playSound3D(ptr, 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);
|
||||
}
|
||||
else if (ptr == MWMechanics::getPlayer())
|
||||
skillUsageSucceeded(ptr, ESM::Skill::Unarmored, ESM::Skill::Armor_HitByOpponent);
|
||||
}
|
||||
}
|
||||
|
||||
if (ishealth)
|
||||
if (hasHealthDamage && healthDamage > 0.0f)
|
||||
{
|
||||
if (!attacker.isEmpty() && !godmode)
|
||||
damage = scaleDamage(damage, attacker, ptr);
|
||||
|
||||
if (damage > 0.0f)
|
||||
{
|
||||
sndMgr->playSound3D(ptr, ESM::RefId::stringRefId("Health Damage"), 1.0f, 1.0f);
|
||||
if (ptr == MWMechanics::getPlayer())
|
||||
MWBase::Environment::get().getWindowManager()->activateHitOverlay();
|
||||
if (!attacker.isEmpty())
|
||||
MWBase::Environment::get().getWorld()->spawnBloodEffect(ptr, hitPosition);
|
||||
}
|
||||
MWMechanics::DynamicStat<float> health(getCreatureStats(ptr).getHealth());
|
||||
health.setCurrent(health.getCurrent() - damage);
|
||||
stats.setHealth(health);
|
||||
}
|
||||
else
|
||||
{
|
||||
MWMechanics::DynamicStat<float> fatigue(getCreatureStats(ptr).getFatigue());
|
||||
fatigue.setCurrent(fatigue.getCurrent() - damage, true);
|
||||
stats.setFatigue(fatigue);
|
||||
if (ptr == MWMechanics::getPlayer())
|
||||
MWBase::Environment::get().getWindowManager()->activateHitOverlay();
|
||||
}
|
||||
|
||||
if (!wasDead && getCreatureStats(ptr).isDead())
|
||||
|
@ -1136,8 +1068,17 @@ namespace MWClass
|
|||
MWBase::Environment::get().getLuaManager()->skillUse(ptr, skill, usageType, extraFactor);
|
||||
}
|
||||
|
||||
float Npc::getArmorRating(const MWWorld::Ptr& ptr) const
|
||||
float Npc::getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable) const
|
||||
{
|
||||
if (useLuaInterfaceIfAvailable && ptr == MWMechanics::getPlayer())
|
||||
{
|
||||
auto res = MWLua::LocalScripts::callPlayerInterface<float>("Combat", "getArmorRating");
|
||||
if (res)
|
||||
return res.value();
|
||||
}
|
||||
|
||||
// Fallback to the old engine implementation when actors don't have their scripts attached yet.
|
||||
|
||||
const MWWorld::Store<ESM::GameSetting>& store
|
||||
= MWBase::Environment::get().getESMStore()->get<ESM::GameSetting>();
|
||||
|
||||
|
@ -1159,7 +1100,7 @@ namespace MWClass
|
|||
}
|
||||
else
|
||||
{
|
||||
ratings[i] = it->getClass().getEffectiveArmorRating(*it, ptr);
|
||||
ratings[i] = it->getClass().getSkillAdjustedArmorRating(*it, ptr);
|
||||
|
||||
// Take in account armor condition
|
||||
const bool hasHealth = it->getClass().hasItemHealth(*it);
|
||||
|
@ -1308,11 +1249,6 @@ namespace MWClass
|
|||
return getNpcStats(ptr).getSkill(id).getModified();
|
||||
}
|
||||
|
||||
int Npc::getBloodTexture(const MWWorld::ConstPtr& ptr) const
|
||||
{
|
||||
return ptr.get<ESM::NPC>()->mBase->mBloodType;
|
||||
}
|
||||
|
||||
void Npc::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const
|
||||
{
|
||||
if (!state.mHasCustomState)
|
||||
|
|
|
@ -81,8 +81,8 @@ 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& ptr, const std::map<std::string, float>& damages, const MWWorld::Ptr& object,
|
||||
const MWWorld::Ptr& attacker, bool successful,
|
||||
const MWMechanics::DamageSourceType sourceType) const override;
|
||||
|
||||
void getModelsToPreload(const MWWorld::ConstPtr& ptr, std::vector<std::string_view>& models) const override;
|
||||
|
@ -112,7 +112,7 @@ namespace MWClass
|
|||
///< Returns total weight of objects inside this object (including modifications from magic
|
||||
/// effects). Throws an exception, if the object can't hold other objects.
|
||||
|
||||
float getArmorRating(const MWWorld::Ptr& ptr) const override;
|
||||
float getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable) const override;
|
||||
///< @return combined armor rating of this actor
|
||||
|
||||
void adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool rendering) const override;
|
||||
|
@ -137,9 +137,6 @@ namespace MWClass
|
|||
|
||||
float getSkill(const MWWorld::Ptr& ptr, ESM::RefId id) const override;
|
||||
|
||||
/// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini)
|
||||
int getBloodTexture(const MWWorld::ConstPtr& ptr) const override;
|
||||
|
||||
bool isNpc() const override { return true; }
|
||||
|
||||
void readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const override;
|
||||
|
|
|
@ -105,7 +105,7 @@ namespace MWClass
|
|||
return std::make_pair(slots_, stack);
|
||||
}
|
||||
|
||||
ESM::RefId Weapon::getEquipmentSkill(const MWWorld::ConstPtr& ptr) const
|
||||
ESM::RefId Weapon::getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const
|
||||
{
|
||||
const MWWorld::LiveCellRef<ESM::Weapon>* ref = ptr.get<ESM::Weapon>();
|
||||
int type = ref->mBase->mData.mType;
|
||||
|
|
|
@ -42,7 +42,7 @@ namespace MWClass
|
|||
///< \return first: Return IDs of the slot this object can be equipped in; second: can object
|
||||
/// stay stacked when equipped?
|
||||
|
||||
ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr) const override;
|
||||
ESM::RefId getEquipmentSkill(const MWWorld::ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const override;
|
||||
|
||||
int getValue(const MWWorld::ConstPtr& ptr) const override;
|
||||
///< Return trade value of the object. Throws an exception, if the object can't be traded.
|
||||
|
|
|
@ -470,11 +470,10 @@ namespace MWGui
|
|||
if (mPtr.isEmpty())
|
||||
return;
|
||||
|
||||
mArmorRating->setCaptionWithReplacing(
|
||||
"#{sArmor}: " + MyGUI::utility::toString(static_cast<int>(mPtr.getClass().getArmorRating(mPtr))));
|
||||
auto rating = MyGUI::utility::toString(static_cast<int>(mPtr.getClass().getArmorRating(mPtr, true)));
|
||||
mArmorRating->setCaptionWithReplacing("#{sArmor}: " + rating);
|
||||
if (mArmorRating->getTextSize().width > mArmorRating->getSize().width)
|
||||
mArmorRating->setCaptionWithReplacing(
|
||||
MyGUI::utility::toString(static_cast<int>(mPtr.getClass().getArmorRating(mPtr))));
|
||||
mArmorRating->setCaptionWithReplacing(rating);
|
||||
}
|
||||
|
||||
void InventoryWindow::updatePreviewSize()
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#include <components/lua/util.hpp>
|
||||
#include <components/misc/strings/algorithm.hpp>
|
||||
#include <components/misc/strings/lower.hpp>
|
||||
#include <components/settings/values.hpp>
|
||||
#include <components/version/version.hpp>
|
||||
|
||||
#include "../mwbase/environment.hpp"
|
||||
|
@ -159,6 +160,8 @@ namespace MWLua
|
|||
};
|
||||
}
|
||||
|
||||
api["getGameDifficulty"] = []() { return Settings::game().mDifficulty.get(); };
|
||||
|
||||
sol::table readOnlyApi = LuaUtil::makeReadOnly(api);
|
||||
return context.setTypePackage(readOnlyApi, "openmw_core");
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include <components/lua/scriptscontainer.hpp>
|
||||
|
||||
#include "../mwbase/luamanager.hpp"
|
||||
#include "../mwmechanics/actorutil.hpp"
|
||||
|
||||
#include "object.hpp"
|
||||
|
||||
|
@ -91,6 +92,19 @@ namespace MWLua
|
|||
|
||||
void applyStatsCache();
|
||||
|
||||
// Calls a lua interface on the player's scripts. This call is only meant for use in updating UI elements.
|
||||
template <typename T, typename... Args>
|
||||
static std::optional<T> callPlayerInterface(
|
||||
std::string_view interfaceName, std::string_view identifier, const Args&... args)
|
||||
{
|
||||
auto player = MWMechanics::getPlayer();
|
||||
auto scripts = player.getRefData().getLuaScripts();
|
||||
if (scripts)
|
||||
return scripts->callInterface<T>(interfaceName, identifier, args...);
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
protected:
|
||||
SelfObject mData;
|
||||
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
#include <MyGUI_InputManager.h>
|
||||
#include <osg/Stats>
|
||||
|
||||
#include "sol/state_view.hpp"
|
||||
|
||||
#include <components/debug/debuglog.hpp>
|
||||
|
||||
#include <components/esm/luascripts.hpp>
|
||||
|
@ -152,6 +150,17 @@ namespace MWLua
|
|||
});
|
||||
}
|
||||
|
||||
void LuaManager::sendLocalEvent(
|
||||
const MWWorld::Ptr& target, const std::string& name, const std::optional<sol::table>& data)
|
||||
{
|
||||
LuaUtil::BinaryData binary = {};
|
||||
if (data)
|
||||
{
|
||||
binary = LuaUtil::serialize(*data, mLocalSerializer.get());
|
||||
}
|
||||
mLuaEvents.addLocalEvent({ getId(target), name, binary });
|
||||
}
|
||||
|
||||
void LuaManager::update()
|
||||
{
|
||||
if (const int steps = Settings::lua().mGcStepsPerFrame; steps > 0)
|
||||
|
@ -482,6 +491,49 @@ namespace MWLua
|
|||
EngineEvents::OnSkillLevelUp{ getId(actor), skillId.serializeText(), std::string(source) });
|
||||
}
|
||||
|
||||
void LuaManager::onHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& weapon,
|
||||
const MWWorld::Ptr& ammo, int attackType, float attackStrength, float damage, bool isHealth,
|
||||
const osg::Vec3f& hitPos, bool successful, MWMechanics::DamageSourceType sourceType)
|
||||
{
|
||||
mLua.protectedCall([&](LuaUtil::LuaView& view) {
|
||||
sol::table damageTable = view.newTable();
|
||||
if (isHealth)
|
||||
damageTable["health"] = damage;
|
||||
else
|
||||
damageTable["fatigue"] = damage;
|
||||
|
||||
sol::table data = view.newTable();
|
||||
if (!attacker.isEmpty())
|
||||
data["attacker"] = LObject(attacker);
|
||||
if (!weapon.isEmpty())
|
||||
data["weapon"] = LObject(weapon);
|
||||
if (!ammo.isEmpty())
|
||||
data["ammo"] = LObject(weapon);
|
||||
data["type"] = attackType;
|
||||
data["strength"] = attackStrength;
|
||||
data["damage"] = damageTable;
|
||||
data["hitPos"] = hitPos;
|
||||
data["successful"] = successful;
|
||||
switch (sourceType)
|
||||
{
|
||||
case MWMechanics::DamageSourceType::Unspecified:
|
||||
data["sourceType"] = "unspecified";
|
||||
break;
|
||||
case MWMechanics::DamageSourceType::Melee:
|
||||
data["sourceType"] = "melee";
|
||||
break;
|
||||
case MWMechanics::DamageSourceType::Ranged:
|
||||
data["sourceType"] = "ranged";
|
||||
break;
|
||||
case MWMechanics::DamageSourceType::Magical:
|
||||
data["sourceType"] = "magic";
|
||||
break;
|
||||
}
|
||||
|
||||
sendLocalEvent(victim, "Hit", data);
|
||||
});
|
||||
}
|
||||
|
||||
void LuaManager::objectAddedToScene(const MWWorld::Ptr& ptr)
|
||||
{
|
||||
mObjectLists.objectAddedToScene(ptr); // assigns generated RefNum if it is not set yet.
|
||||
|
|
|
@ -92,6 +92,9 @@ namespace MWLua
|
|||
bool loopfallback) override;
|
||||
void skillUse(const MWWorld::Ptr& actor, ESM::RefId skillId, int useType, float scale) override;
|
||||
void skillLevelUp(const MWWorld::Ptr& actor, ESM::RefId skillId, std::string_view source) override;
|
||||
void onHit(const MWWorld::Ptr& attacker, const MWWorld::Ptr& victim, const MWWorld::Ptr& weapon,
|
||||
const MWWorld::Ptr& ammo, int attackType, float attackStrength, float damage, bool isHealth,
|
||||
const osg::Vec3f& hitPos, bool successful, MWMechanics::DamageSourceType sourceType) override;
|
||||
void exteriorCreated(MWWorld::CellStore& cell) override
|
||||
{
|
||||
mEngineEvents.addToQueue(EngineEvents::OnNewExterior{ cell });
|
||||
|
@ -166,6 +169,9 @@ namespace MWLua
|
|||
LuaUtil::InputAction::Registry& inputActions() { return mInputActions; }
|
||||
LuaUtil::InputTrigger::Registry& inputTriggers() { return mInputTriggers; }
|
||||
|
||||
void sendLocalEvent(
|
||||
const MWWorld::Ptr& target, const std::string& name, const std::optional<sol::table>& data = std::nullopt);
|
||||
|
||||
private:
|
||||
void initConfiguration();
|
||||
LocalScripts* createLocalScripts(const MWWorld::Ptr& ptr,
|
||||
|
|
|
@ -420,6 +420,42 @@ namespace MWLua
|
|||
return ptr.getClass().getCapacity(ptr);
|
||||
};
|
||||
|
||||
actor["_onHit"] = [context](const SelfObject& self, const sol::table& options) {
|
||||
sol::optional<sol::table> damageLua = options.get<sol::optional<sol::table>>("damage");
|
||||
std::map<std::string, float> damageCpp;
|
||||
if (damageLua)
|
||||
{
|
||||
for (auto& [key, value] : damageLua.value())
|
||||
{
|
||||
damageCpp[key.as<std::string>()] = value.as<float>();
|
||||
}
|
||||
}
|
||||
std::string sourceTypeStr = options.get_or<std::string>("sourceType", "unspecified");
|
||||
MWMechanics::DamageSourceType sourceType = MWMechanics::DamageSourceType::Unspecified;
|
||||
if (sourceTypeStr == "melee")
|
||||
sourceType = MWMechanics::DamageSourceType::Melee;
|
||||
else if (sourceTypeStr == "ranged")
|
||||
sourceType = MWMechanics::DamageSourceType::Ranged;
|
||||
else if (sourceTypeStr == "magic")
|
||||
sourceType = MWMechanics::DamageSourceType::Magical;
|
||||
sol::optional<Object> weapon = options.get<sol::optional<Object>>("weapon");
|
||||
sol::optional<Object> ammo = options.get<sol::optional<Object>>("ammo");
|
||||
|
||||
context.mLuaManager->addAction(
|
||||
[self = self, damages = std::move(damageCpp), attacker = options.get<sol::optional<Object>>("attacker"),
|
||||
weapon = ammo ? ammo : weapon, successful = options.get<bool>("successful"),
|
||||
sourceType = sourceType] {
|
||||
MWWorld::Ptr attackerPtr;
|
||||
MWWorld::Ptr weaponPtr;
|
||||
if (attacker)
|
||||
attackerPtr = attacker->ptr();
|
||||
if (weapon)
|
||||
weaponPtr = weapon->ptr();
|
||||
self.ptr().getClass().onHit(self.ptr(), damages, weaponPtr, attackerPtr, successful, sourceType);
|
||||
},
|
||||
"HitAction");
|
||||
};
|
||||
|
||||
addActorStatsBindings(actor, context);
|
||||
addActorMagicBindings(actor, context);
|
||||
}
|
||||
|
|
|
@ -74,6 +74,7 @@ namespace MWLua
|
|||
[](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Essential; });
|
||||
record["isRespawning"] = sol::readonly_property(
|
||||
[](const ESM::Creature& rec) -> bool { return rec.mFlags & ESM::Creature::Respawn; });
|
||||
record["bloodType"] = sol::readonly_property([](const ESM::Creature& rec) -> int { return rec.mBloodType; });
|
||||
|
||||
addActorServicesBindings<ESM::Creature>(record, context);
|
||||
}
|
||||
|
|
|
@ -102,6 +102,7 @@ namespace MWLua
|
|||
record["isRespawning"]
|
||||
= sol::readonly_property([](const ESM::NPC& rec) -> bool { return rec.mFlags & ESM::NPC::Respawn; });
|
||||
record["baseGold"] = sol::readonly_property([](const ESM::NPC& rec) -> int { return rec.mNpdt.mGold; });
|
||||
record["bloodType"] = sol::readonly_property([](const ESM::NPC& rec) -> int { return rec.mBloodType; });
|
||||
addActorServicesBindings<ESM::NPC>(record, context);
|
||||
|
||||
npc["classes"] = initClassRecordBindings(context);
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
|
||||
#include "../mwbase/dialoguemanager.hpp"
|
||||
#include "../mwbase/environment.hpp"
|
||||
#include "../mwbase/luamanager.hpp"
|
||||
#include "../mwbase/mechanicsmanager.hpp"
|
||||
#include "../mwbase/soundmanager.hpp"
|
||||
#include "../mwbase/windowmanager.hpp"
|
||||
|
@ -240,8 +241,8 @@ 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);
|
||||
MWBase::Environment::get().getLuaManager()->onHit(attacker, victim, weapon, projectile, 0,
|
||||
attackStrength, damage, false, hitPosition, false, MWMechanics::DamageSourceType::Ranged);
|
||||
MWMechanics::reduceWeaponCondition(damage, false, weapon, attacker);
|
||||
return;
|
||||
}
|
||||
|
@ -299,8 +300,8 @@ namespace MWMechanics
|
|||
victim.getClass().getContainerStore(victim).add(projectile, 1);
|
||||
}
|
||||
|
||||
victim.getClass().onHit(
|
||||
victim, damage, true, projectile, attacker, hitPosition, true, MWMechanics::DamageSourceType::Ranged);
|
||||
MWBase::Environment::get().getLuaManager()->onHit(attacker, victim, weapon, projectile, 0, attackStrength,
|
||||
damage, true, hitPosition, true, MWMechanics::DamageSourceType::Ranged);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -359,8 +359,7 @@ 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);
|
||||
target.getClass().onHit(target, {}, MWWorld::Ptr(), caster, true, MWMechanics::DamageSourceType::Magical);
|
||||
// Apply resistances
|
||||
if (!(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Resistances))
|
||||
{
|
||||
|
|
|
@ -119,8 +119,8 @@ 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& ptr, const std::map<std::string, float>& damages, const Ptr& object,
|
||||
const Ptr& attacker, bool successful, const MWMechanics::DamageSourceType sourceType) const
|
||||
{
|
||||
throw std::runtime_error("class cannot be hit");
|
||||
}
|
||||
|
@ -205,7 +205,7 @@ namespace MWWorld
|
|||
return std::make_pair(std::vector<int>(), false);
|
||||
}
|
||||
|
||||
ESM::RefId Class::getEquipmentSkill(const ConstPtr& ptr) const
|
||||
ESM::RefId Class::getEquipmentSkill(const ConstPtr& ptr, bool useLuaInterfaceIfAvailable) const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
@ -235,7 +235,7 @@ namespace MWWorld
|
|||
return false;
|
||||
}
|
||||
|
||||
float Class::getArmorRating(const MWWorld::Ptr& ptr) const
|
||||
float Class::getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable) const
|
||||
{
|
||||
throw std::runtime_error("Class does not support armor rating");
|
||||
}
|
||||
|
@ -452,11 +452,6 @@ namespace MWWorld
|
|||
throw std::runtime_error("class does not support skills");
|
||||
}
|
||||
|
||||
int Class::getBloodTexture(const MWWorld::ConstPtr& ptr) const
|
||||
{
|
||||
throw std::runtime_error("class does not support gore");
|
||||
}
|
||||
|
||||
void Class::readAdditionalState(const MWWorld::Ptr& ptr, const ESM::ObjectState& state) const {}
|
||||
|
||||
void Class::writeAdditionalState(const MWWorld::ConstPtr& ptr, ESM::ObjectState& state) const {}
|
||||
|
@ -514,7 +509,8 @@ namespace MWWorld
|
|||
return -1;
|
||||
}
|
||||
|
||||
float Class::getEffectiveArmorRating(const ConstPtr& armor, const Ptr& actor) const
|
||||
float Class::getSkillAdjustedArmorRating(
|
||||
const ConstPtr& armor, const Ptr& actor, bool useLuaInterfaceIfAvailable) const
|
||||
{
|
||||
throw std::runtime_error("class does not support armor ratings");
|
||||
}
|
||||
|
|
|
@ -147,11 +147,11 @@ 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,
|
||||
virtual void onHit(const MWWorld::Ptr& ptr, const std::map<std::string, float>& damages,
|
||||
const MWWorld::Ptr& object, const MWWorld::Ptr& attacker, bool successful,
|
||||
const MWMechanics::DamageSourceType sourceType) const;
|
||||
///< 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
|
||||
///< Alerts \a ptr that it's being hit for \a damages 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.
|
||||
|
||||
|
@ -212,7 +212,7 @@ namespace MWWorld
|
|||
///
|
||||
/// Default implementation: return (empty vector, false).
|
||||
|
||||
virtual ESM::RefId getEquipmentSkill(const ConstPtr& ptr) const;
|
||||
virtual ESM::RefId getEquipmentSkill(const ConstPtr& ptr, bool useLuaInterfaceIfAvailable = false) const;
|
||||
/// Return the index of the skill this item corresponds to when equipped.
|
||||
/// (default implementation: return empty ref id)
|
||||
|
||||
|
@ -258,7 +258,7 @@ namespace MWWorld
|
|||
virtual ESM::RefId getSoundIdFromSndGen(const Ptr& ptr, std::string_view type) const;
|
||||
///< Returns the sound ID for \a ptr of the given soundgen \a type.
|
||||
|
||||
virtual float getArmorRating(const MWWorld::Ptr& ptr) const;
|
||||
virtual float getArmorRating(const MWWorld::Ptr& ptr, bool useLuaInterfaceIfAvailable = false) const;
|
||||
///< @return combined armor rating of this actor
|
||||
|
||||
virtual const std::string& getInventoryIcon(const MWWorld::ConstPtr& ptr) const;
|
||||
|
@ -313,9 +313,6 @@ namespace MWWorld
|
|||
virtual bool allowTelekinesis(const MWWorld::ConstPtr& ptr) const { return true; }
|
||||
///< Return whether this class of object can be activated with telekinesis
|
||||
|
||||
/// Get a blood texture suitable for \a ptr (see Blood Texture 0-2 in Morrowind.ini)
|
||||
virtual int getBloodTexture(const MWWorld::ConstPtr& ptr) const;
|
||||
|
||||
virtual Ptr copyToCell(const ConstPtr& ptr, CellStore& cell, int count) const;
|
||||
|
||||
// Similar to `copyToCell`, but preserves RefNum and moves LuaScripts.
|
||||
|
@ -378,7 +375,8 @@ namespace MWWorld
|
|||
virtual int getPrimaryFactionRank(const MWWorld::ConstPtr& ptr) const;
|
||||
|
||||
/// Get the effective armor rating, factoring in the actor's skills, for the given armor.
|
||||
virtual float getEffectiveArmorRating(const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor) const;
|
||||
virtual float getSkillAdjustedArmorRating(
|
||||
const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor, bool useLuaInterfaceIfAvailable = false) const;
|
||||
|
||||
virtual osg::Vec4f getEnchantmentColor(const MWWorld::ConstPtr& item) const;
|
||||
|
||||
|
|
|
@ -408,7 +408,7 @@ void MWWorld::InventoryStore::autoEquipArmor(TSlots& slots_)
|
|||
{
|
||||
if (actorIsNpc)
|
||||
{
|
||||
if (testCls.getEffectiveArmorRating(test, actor) <= unarmoredRating)
|
||||
if (testCls.getSkillAdjustedArmorRating(test, actor) <= unarmoredRating)
|
||||
continue;
|
||||
}
|
||||
else
|
||||
|
@ -463,8 +463,8 @@ void MWWorld::InventoryStore::autoEquipArmor(TSlots& slots_)
|
|||
// For NPCs, compare armor rating; for creatures, compare condition
|
||||
if (actorIsNpc)
|
||||
{
|
||||
const float rating = testCls.getEffectiveArmorRating(test, actor);
|
||||
const float oldRating = oldCls.getEffectiveArmorRating(old, actor);
|
||||
const float rating = testCls.getSkillAdjustedArmorRating(test, actor);
|
||||
const float oldRating = oldCls.getSkillAdjustedArmorRating(old, actor);
|
||||
if (rating <= oldRating)
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -105,7 +105,7 @@ namespace MWWorld
|
|||
|
||||
void setLocals(const ESM::Script& script);
|
||||
|
||||
MWLua::LocalScripts* getLuaScripts() { return mLuaScripts.get(); }
|
||||
MWLua::LocalScripts* getLuaScripts() const { return mLuaScripts.get(); }
|
||||
void setLuaScripts(std::shared_ptr<MWLua::LocalScripts>&&);
|
||||
|
||||
/// This flag is only used for content stack loading and will not be stored in the savegame.
|
||||
|
|
|
@ -3706,24 +3706,6 @@ namespace MWWorld
|
|||
}
|
||||
}
|
||||
|
||||
void World::spawnBloodEffect(const Ptr& ptr, const osg::Vec3f& worldPosition)
|
||||
{
|
||||
if (ptr == getPlayerPtr() && Settings::gui().mHitFader)
|
||||
return;
|
||||
|
||||
std::string_view texture
|
||||
= Fallback::Map::getString("Blood_Texture_" + std::to_string(ptr.getClass().getBloodTexture(ptr)));
|
||||
if (texture.empty())
|
||||
texture = Fallback::Map::getString("Blood_Texture_0");
|
||||
|
||||
// [0, 2]
|
||||
const int number = Misc::Rng::rollDice(3);
|
||||
const VFS::Path::Normalized model = Misc::ResourceHelpers::correctMeshPath(
|
||||
VFS::Path::Normalized(Fallback::Map::getString("Blood_Model_" + std::to_string(number))));
|
||||
|
||||
mRendering->spawnEffect(model, texture, worldPosition, 1.0f, false, false);
|
||||
}
|
||||
|
||||
void World::spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride,
|
||||
const osg::Vec3f& worldPos, float scale, bool isMagicVFX, bool useAmbientLight)
|
||||
{
|
||||
|
|
|
@ -608,9 +608,6 @@ namespace MWWorld
|
|||
/// Spawn a random creature from a levelled list next to the player
|
||||
void spawnRandomCreature(const ESM::RefId& creatureList) override;
|
||||
|
||||
/// Spawn a blood effect for \a ptr at \a worldPosition
|
||||
void spawnBloodEffect(const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) override;
|
||||
|
||||
void spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride,
|
||||
const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true,
|
||||
bool useAmbientLight = true) override;
|
||||
|
|
|
@ -165,6 +165,29 @@ namespace LuaUtil
|
|||
virtual bool isActive() const { return false; }
|
||||
|
||||
protected:
|
||||
// Call a function on an interface.
|
||||
template <typename T, typename... Args>
|
||||
std::optional<T> callInterface(std::string_view interfaceName, std::string_view identifier, const Args&... args)
|
||||
{
|
||||
std::optional<T> res = std::nullopt;
|
||||
mLua.protectedCall([&](LuaUtil::LuaView& view) {
|
||||
LoadedData& data = ensureLoaded();
|
||||
auto I = data.mPublicInterfaces.get<sol::optional<sol::table>>(interfaceName);
|
||||
if (I)
|
||||
{
|
||||
auto o = I->get_or<sol::object>(identifier, sol::nil);
|
||||
if (o.is<sol::function>())
|
||||
{
|
||||
sol::object luaRes = o.as<sol::function>().call(args...);
|
||||
if (luaRes.is<T>())
|
||||
res = luaRes.as<T>();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
struct Handler
|
||||
{
|
||||
int mScriptId;
|
||||
|
|
|
@ -71,8 +71,6 @@ namespace Settings
|
|||
SettingValue<osg::Vec3f> mDefaultActorPathfindHalfExtents{ mIndex, "Game",
|
||||
"default actor pathfind half extents", makeMaxStrictSanitizerVec3f(osg::Vec3f(0, 0, 0)) };
|
||||
SettingValue<bool> mDayNightSwitches{ mIndex, "Game", "day night switches" };
|
||||
SettingValue<bool> mUnarmedCreatureAttacksDamageArmor{ mIndex, "Game",
|
||||
"unarmed creature attacks damage armor" };
|
||||
SettingValue<DetourNavigator::CollisionShapeType> mActorCollisionShapeType{ mIndex, "Game",
|
||||
"actor collision shape type" };
|
||||
SettingValue<bool> mPlayerMovementIgnoresAnimation{ mIndex, "Game", "player movement ignores animation" };
|
||||
|
|
|
@ -2,6 +2,7 @@ paths=(
|
|||
openmw_aux/*lua
|
||||
scripts/omw/activationhandlers.lua
|
||||
scripts/omw/ai.lua
|
||||
scripts/omw/combat/local.lua
|
||||
scripts/omw/input/playercontrols.lua
|
||||
scripts/omw/mechanics/animationcontroller.lua
|
||||
scripts/omw/input/gamepadcontrols.lua
|
||||
|
|
|
@ -71,11 +71,70 @@ Calls the corresponding function in openw.core on the target. Will use core.soun
|
|||
.. code-block:: Lua
|
||||
actor:sendEvent('PlaySound3d', {sound = 'Open Lock'})
|
||||
|
||||
|
||||
**BreakInvisibility**
|
||||
|
||||
Forces the actor to lose all active invisibility effects.
|
||||
|
||||
**Unequip**
|
||||
|
||||
Any script can send ``Unequip`` events with the argument ``item`` or ``slot`` to any actor, to make that actor unequip an item.
|
||||
|
||||
The following two examples are equivalent, except the ``item`` variant is guaranteed to only unequip the specified item and won't unequip a different item if the actor's equipment changed during the same frame:
|
||||
|
||||
.. code-block:: Lua
|
||||
|
||||
local item = Actor.getEquipment(actor, Actor.EQUIPMENT_SLOT.CarriedLeft)
|
||||
if item then
|
||||
actor:sendEvent('Unequip', {item = item})
|
||||
end
|
||||
|
||||
.. code-block:: Lua
|
||||
|
||||
actor:sendEvent('Unequip', {slot = Actor.EQUIPMENT_SLOT.CarriedLeft})
|
||||
|
||||
Combat events
|
||||
-------------
|
||||
|
||||
**Hit**
|
||||
|
||||
Any script can send ``Hit`` events with arguments described in the Combat interface to cause a hit to an actor
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: Lua
|
||||
|
||||
-- See Combat#AttackInfo
|
||||
local attack = {
|
||||
attacker = self,
|
||||
weapon = Actor.getEquipment(self, Actor.EQUIPMENT_SLOT.CarriedRight),
|
||||
sourceType = I.Combat.ATTACK_SOURCE_TYPE.Melee,
|
||||
strenght = 1,
|
||||
type = self.ATTACK_TYPE.Chop,
|
||||
damage = {
|
||||
health = 20,
|
||||
fatigue = 10,
|
||||
},
|
||||
successful = true,
|
||||
}
|
||||
victim:sendEvent('Hit', attack)
|
||||
|
||||
Item events
|
||||
-----------
|
||||
|
||||
**ModifyItemCondition**
|
||||
|
||||
Any script can send ``ModifyItemCondition`` events to global to adjust the condition of an item.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: Lua
|
||||
|
||||
local item = Actor.getEquipment(actor, Actor.EQUIPMENT_SLOT.CarriedLeft)
|
||||
if item then
|
||||
-- Reduce condition by 1
|
||||
-- Note that actor should be included, if applicable, to allow forcibly unequipping items whose condition is reduced to 0
|
||||
core:sendGlobalEvent('ModifyItemCondition', {actor = self, item = item, amount: -1})
|
||||
end
|
||||
|
||||
UI events
|
||||
---------
|
||||
|
|
8
docs/source/reference/lua-scripting/interface_combat.rst
Normal file
8
docs/source/reference/lua-scripting/interface_combat.rst
Normal file
|
@ -0,0 +1,8 @@
|
|||
Interface Combat
|
||||
================
|
||||
|
||||
.. include:: version.rst
|
||||
|
||||
.. raw:: html
|
||||
:file: generated_html/scripts_omw_combat_local.html
|
||||
|
|
@ -23,6 +23,9 @@
|
|||
* - :doc:`Crimes </reference/lua-scripting/interface_crimes>`
|
||||
- |bdg-ctx-global|
|
||||
- Commit crimes.
|
||||
* - :doc:`Combat </reference/lua-scripting/interface_combat>`
|
||||
- |bdg-ctx-local|
|
||||
- Control combat of NPCs and creatures
|
||||
* - :doc:`GamepadControls </reference/lua-scripting/interface_gamepadcontrols>`
|
||||
- |bdg-ctx-player|
|
||||
- Allows to alter behavior of the built-in script that handles player gamepad controls.
|
||||
|
|
|
@ -80,7 +80,6 @@ GUI Settings
|
|||
|
||||
|
||||
Enables or disables the red flash overlay when the character takes damage.
|
||||
Disabling causes the player to "bleed" like NPCs.
|
||||
|
||||
.. omw-setting::
|
||||
:title: werewolf overlay
|
||||
|
|
|
@ -442,17 +442,6 @@ Game Settings
|
|||
|
||||
Some mods add models which change visuals based on time of day. When this setting is enabled, supporting models will automatically make use of Day/night state.
|
||||
|
||||
.. omw-setting::
|
||||
:title: unarmed creature attacks damage armor
|
||||
:type: boolean
|
||||
:range: true, false
|
||||
:default: false
|
||||
:location: :bdg-success:`Launcher > Settings > Gameplay`
|
||||
|
||||
If disabled unarmed creature attacks do not reduce armor condition, just as with vanilla engine.
|
||||
|
||||
If enabled unarmed creature attacks reduce armor condition, the same as attacks from NPCs and armed creatures.
|
||||
|
||||
.. omw-setting::
|
||||
:title: actor collision shape type
|
||||
:type: int
|
||||
|
|
|
@ -43,6 +43,9 @@ set(BUILTIN_DATA_FILES
|
|||
l10n/OMWCamera/ru.yaml
|
||||
l10n/OMWCamera/sv.yaml
|
||||
l10n/OMWCamera/fr.yaml
|
||||
l10n/OMWCombat/en.yaml
|
||||
l10n/OMWCombat/ru.yaml
|
||||
l10n/OMWCombat/sv.yaml
|
||||
l10n/OMWControls/de.yaml
|
||||
l10n/OMWControls/en.yaml
|
||||
l10n/OMWControls/fr.yaml
|
||||
|
@ -87,6 +90,10 @@ set(BUILTIN_DATA_FILES
|
|||
scripts/omw/camera/settings.lua
|
||||
scripts/omw/camera/move360.lua
|
||||
scripts/omw/camera/first_person_auto_switch.lua
|
||||
scripts/omw/combat/common.lua
|
||||
scripts/omw/combat/global.lua
|
||||
scripts/omw/combat/local.lua
|
||||
scripts/omw/combat/menu.lua
|
||||
scripts/omw/console/global.lua
|
||||
scripts/omw/console/local.lua
|
||||
scripts/omw/console/player.lua
|
||||
|
|
|
@ -25,6 +25,9 @@ PLAYER: scripts/omw/input/gamepadcontrols.lua
|
|||
NPC,CREATURE: scripts/omw/ai.lua
|
||||
GLOBAL: scripts/omw/mechanics/globalcontroller.lua
|
||||
CREATURE, NPC, PLAYER: scripts/omw/mechanics/actorcontroller.lua
|
||||
GLOBAL: scripts/omw/combat/global.lua
|
||||
MENU: scripts/omw/combat/menu.lua
|
||||
NPC,CREATURE,PLAYER: scripts/omw/combat/local.lua
|
||||
|
||||
# User interface
|
||||
PLAYER: scripts/omw/ui.lua
|
||||
|
|
16
files/data/l10n/OMWCombat/en.yaml
Normal file
16
files/data/l10n/OMWCombat/en.yaml
Normal file
|
@ -0,0 +1,16 @@
|
|||
Combat: "OpenMW Combat"
|
||||
combatSettingsPageDescription: "OpenMW Combat settings"
|
||||
|
||||
combatSettings: "Combat"
|
||||
|
||||
unarmedCreatureAttacksDamageArmor: "Unarmed creature attacks damage armor"
|
||||
unarmedCreatureAttacksDamageArmorDescription: |
|
||||
Unarmed creatures now also damage armor.
|
||||
|
||||
redistributeShieldHitsWhenNotWearingShield: "Redistribute shield hits when not wearing a shield"
|
||||
redistributeShieldHitsWhenNotWearingShieldDescription: |
|
||||
Equivalent to "Shield hit location fix" from Morrowind Code Patch. Redistributes hits to the shield armor slot to left pauldron or cuirass when not wearing a shield.
|
||||
|
||||
spawnBloodEffectsOnPlayer: "Spawn blood effects on player"
|
||||
spawnBloodEffectsOnPlayerDescription: |
|
||||
If enabled blood effects are spawned on the player when hit in combat, same as any other character.
|
16
files/data/l10n/OMWCombat/ru.yaml
Normal file
16
files/data/l10n/OMWCombat/ru.yaml
Normal file
|
@ -0,0 +1,16 @@
|
|||
Combat: "Боевая система OpenMW"
|
||||
combatSettingsPageDescription: "Настройки боевой системы OpenMW"
|
||||
|
||||
combatSettings: "Бой"
|
||||
|
||||
unarmedCreatureAttacksDamageArmor: "Атаки существ повреждают броню"
|
||||
unarmedCreatureAttacksDamageArmorDescription: |
|
||||
Атаки невооруженных существ тоже будут наносить броне урон.
|
||||
|
||||
redistributeShieldHitsWhenNotWearingShield: "Перераспространять удары по щитам"
|
||||
redistributeShieldHitsWhenNotWearingShieldDescription: |
|
||||
Эквивалентно настройке "Shield hit location fix" из Morrowind Code Patch. Когда персонаж не носит щит, удары по его слоту будут проходить по слоту левого наплечника или кирасы.
|
||||
|
||||
spawnBloodEffectsOnPlayer: "Использовать эффекты крови для игрока"
|
||||
spawnBloodEffectsOnPlayerDescription: |
|
||||
Если настройка включена, эффекты крови будут играть при получении ударов в бою персонажем игрока, как для всех остальных персонажей.
|
16
files/data/l10n/OMWCombat/sv.yaml
Normal file
16
files/data/l10n/OMWCombat/sv.yaml
Normal file
|
@ -0,0 +1,16 @@
|
|||
Combat: "OpenMW Strid"
|
||||
combatSettingsPageDescription: "OpenMW Stridsinställningar"
|
||||
|
||||
combatSettings: "Strid"
|
||||
|
||||
unarmedCreatureAttacksDamageArmor: "Obeväpnad attack från varelser skadar rustning"
|
||||
unarmedCreatureAttacksDamageArmorDescription: |
|
||||
Obeväpnade varelser skadar nu även rustning.
|
||||
|
||||
redistributeShieldHitsWhenNotWearingShield: "Omfördela sköldträffar när sköld inte är buren"
|
||||
redistributeShieldHitsWhenNotWearingShieldDescription: |
|
||||
Motsvarar ”Shield hit location fix” från Morrowind Code Patch. Omfördelar träffar på sköldens rustningsplats till vänster axelstycke (pauldron) eller kyrass (cuirass) när du inte bär en sköld.
|
||||
|
||||
spawnBloodEffectsOnPlayer: "Skapa blodeffekter på spelarfiguren"
|
||||
spawnBloodEffectsOnPlayerDescription: |
|
||||
Om blodeffekter är aktiverade visas de på spelarfiguren när denne träffas i strid, precis som på alla andra rollfigurer.
|
41
files/data/scripts/omw/combat/common.lua
Normal file
41
files/data/scripts/omw/combat/common.lua
Normal file
|
@ -0,0 +1,41 @@
|
|||
local async = require('openmw.async')
|
||||
local storage = require('openmw.storage')
|
||||
local I = require('openmw.interfaces')
|
||||
|
||||
local combatGroup = 'SettingsOMWCombat'
|
||||
|
||||
return {
|
||||
registerSettingsPage = function()
|
||||
I.Settings.registerPage({
|
||||
key = 'OMWCombat',
|
||||
l10n = 'OMWCombat',
|
||||
name = 'Combat',
|
||||
description = 'combatSettingsPageDescription',
|
||||
})
|
||||
end,
|
||||
registerSettingsGroup = function()
|
||||
local function boolSetting(key, default)
|
||||
return {
|
||||
key = key,
|
||||
renderer = 'checkbox',
|
||||
name = key,
|
||||
description = key..'Description',
|
||||
default = default,
|
||||
}
|
||||
end
|
||||
|
||||
I.Settings.registerGroup({
|
||||
key = combatGroup,
|
||||
page = 'OMWCombat',
|
||||
l10n = 'OMWCombat',
|
||||
name = 'combatSettings',
|
||||
permanentStorage = false,
|
||||
order = 0,
|
||||
settings = {
|
||||
boolSetting('unarmedCreatureAttacksDamageArmor', false),
|
||||
boolSetting('redistributeShieldHitsWhenNotWearingShield', false),
|
||||
boolSetting('spawnBloodEffectsOnPlayer', false),
|
||||
},
|
||||
})
|
||||
end,
|
||||
}
|
1
files/data/scripts/omw/combat/global.lua
Normal file
1
files/data/scripts/omw/combat/global.lua
Normal file
|
@ -0,0 +1 @@
|
|||
require('scripts.omw.combat.common').registerSettingsGroup()
|
431
files/data/scripts/omw/combat/local.lua
Normal file
431
files/data/scripts/omw/combat/local.lua
Normal file
|
@ -0,0 +1,431 @@
|
|||
local animation = require('openmw.animation')
|
||||
local async = require('openmw.async')
|
||||
local core = require('openmw.core')
|
||||
local I = require('openmw.interfaces')
|
||||
local self = require('openmw.self')
|
||||
local storage = require('openmw.storage')
|
||||
local types = require('openmw.types')
|
||||
local util = require('openmw.util')
|
||||
local Actor = types.Actor
|
||||
local Weapon = types.Weapon
|
||||
local Player = types.Player
|
||||
local Creature = types.Creature
|
||||
local Armor = types.Armor
|
||||
local isPlayer = Player.objectIsInstance(self)
|
||||
|
||||
local godMode = function() return false end
|
||||
if isPlayer then
|
||||
-- openmw.debug is only allowed on player scripts
|
||||
godMode = function() return require('openmw.debug').isGodMode() end
|
||||
end
|
||||
|
||||
local onHitHandlers = {}
|
||||
|
||||
local settings = storage.globalSection('SettingsOMWCombat')
|
||||
|
||||
local function getSkill(actor, skillId)
|
||||
if Creature.objectIsInstance(actor) then
|
||||
local specialization = core.stats.Skill.record(skillId).specialization
|
||||
local creatureRecord = Creature.record(actor)
|
||||
return creatureRecord[specialization..'Skill']
|
||||
else
|
||||
return types.NPC.stats.skills[skillId](actor).modified
|
||||
end
|
||||
end
|
||||
|
||||
local armorTypeGmst = {
|
||||
[Armor.TYPE.Boots] = core.getGMST('iBootsWeight'),
|
||||
[Armor.TYPE.Cuirass] = core.getGMST('iCuirassWeight'),
|
||||
[Armor.TYPE.Greaves] = core.getGMST('iGreavesWeight'),
|
||||
[Armor.TYPE.Helmet] = core.getGMST('iHelmWeight'),
|
||||
[Armor.TYPE.LBracer] = core.getGMST('iGauntletWeight'),
|
||||
[Armor.TYPE.LGauntlet] = core.getGMST('iGauntletWeight'),
|
||||
[Armor.TYPE.LPauldron] = core.getGMST('iPauldronWeight'),
|
||||
[Armor.TYPE.RBracer] = core.getGMST('iGauntletWeight'),
|
||||
[Armor.TYPE.RGauntlet] = core.getGMST('iGauntletWeight'),
|
||||
[Armor.TYPE.RPauldron] = core.getGMST('iPauldronWeight'),
|
||||
[Armor.TYPE.Shield] = core.getGMST('iShieldWeight'),
|
||||
}
|
||||
|
||||
local armorSlots = {
|
||||
Actor.EQUIPMENT_SLOT.Boots,
|
||||
Actor.EQUIPMENT_SLOT.Cuirass,
|
||||
Actor.EQUIPMENT_SLOT.Greaves,
|
||||
Actor.EQUIPMENT_SLOT.Helmet,
|
||||
Actor.EQUIPMENT_SLOT.LeftGauntlet,
|
||||
Actor.EQUIPMENT_SLOT.LeftPauldron,
|
||||
Actor.EQUIPMENT_SLOT.RightGauntlet,
|
||||
Actor.EQUIPMENT_SLOT.RightPauldron,
|
||||
Actor.EQUIPMENT_SLOT.CarriedLeft,
|
||||
}
|
||||
|
||||
local function getArmorSkill(item)
|
||||
if not item or not Armor.objectIsInstance(item) then
|
||||
return 'unarmored'
|
||||
end
|
||||
local record = Armor.record(item)
|
||||
local weightGmst = armorTypeGmst[record.type]
|
||||
local epsilon = 0.0005
|
||||
if record.weight <= weightGmst * core.getGMST('fLightMaxMod') + epsilon then
|
||||
return 'lightarmor'
|
||||
elseif record.weight <= weightGmst * core.getGMST('fMedMaxMod') + epsilon then
|
||||
return 'mediumarmor'
|
||||
else
|
||||
return 'heavyarmor'
|
||||
end
|
||||
end
|
||||
|
||||
local function getSkillAdjustedArmorRating(item, actor)
|
||||
local record = Armor.record(item)
|
||||
local skillid = I.Combat.getArmorSkill(item)
|
||||
local skill = getSkill(actor, skillid)
|
||||
if record.weight == 0 then
|
||||
return record.baseArmor
|
||||
end
|
||||
return record.baseArmor * skill / core.getGMST('iBaseArmorSkill')
|
||||
end
|
||||
|
||||
local function getEffectiveArmorRating(item, actor)
|
||||
local record = Armor.record(item)
|
||||
local rating = getSkillAdjustedArmorRating(item, actor)
|
||||
if record.health and record.health ~= 0 then
|
||||
rating = rating * (types.Item.itemData(item).condition / record.health)
|
||||
end
|
||||
return rating
|
||||
end
|
||||
|
||||
local function getArmorRating(actor)
|
||||
local magicShield = Actor.activeEffects(actor):getEffect(core.magic.EFFECT_TYPE.Shield).magnitude
|
||||
|
||||
if Creature.objectIsInstance(actor) then
|
||||
return magicShield
|
||||
end
|
||||
|
||||
local equipment = Actor.getEquipment(actor)
|
||||
local ratings = {}
|
||||
local unarmored = getSkill(actor, 'unarmored')
|
||||
local fUnarmoredBase1 = core.getGMST('fUnarmoredBase1')
|
||||
local fUnarmoredBase2 = core.getGMST('fUnarmoredBase2')
|
||||
|
||||
for _, v in pairs(armorSlots) do
|
||||
if equipment[v] and Armor.objectIsInstance(equipment[v]) then
|
||||
ratings[v] = I.Combat.getEffectiveArmorRating(equipment[v], actor)
|
||||
else
|
||||
-- Unarmored
|
||||
ratings[v] = (fUnarmoredBase1 * unarmored) * (fUnarmoredBase2 * unarmored)
|
||||
end
|
||||
end
|
||||
|
||||
return ratings[Actor.EQUIPMENT_SLOT.Cuirass] * 0.3
|
||||
+ ratings[Actor.EQUIPMENT_SLOT.CarriedLeft] * 0.1
|
||||
+ ratings[Actor.EQUIPMENT_SLOT.Helmet] * 0.1
|
||||
+ ratings[Actor.EQUIPMENT_SLOT.Greaves] * 0.1
|
||||
+ ratings[Actor.EQUIPMENT_SLOT.Boots] * 0.1
|
||||
+ ratings[Actor.EQUIPMENT_SLOT.LeftPauldron] * 0.1
|
||||
+ ratings[Actor.EQUIPMENT_SLOT.RightPauldron] * 0.1
|
||||
+ ratings[Actor.EQUIPMENT_SLOT.LeftGauntlet] * 0.05
|
||||
+ ratings[Actor.EQUIPMENT_SLOT.RightGauntlet] * 0.05
|
||||
+ magicShield
|
||||
end
|
||||
|
||||
local function adjustDamageForArmor(damage, actor)
|
||||
local armor = I.Combat.getArmorRating(actor)
|
||||
local x = damage / (damage + armor)
|
||||
return damage * math.max(x, core.getGMST('fCombatArmorMinMult'))
|
||||
end
|
||||
|
||||
local function pickRandomArmor(actor)
|
||||
local slot = nil
|
||||
local roll = math.random(0, 99) -- randIntUniform(0, 100)
|
||||
if roll >= 90 then
|
||||
slot = Actor.EQUIPMENT_SLOT.CarriedLeft
|
||||
local item = Actor.getEquipment(actor, slot)
|
||||
local haveShield = item and Armor.objectIsInstance(item)
|
||||
if settings:get('redistributeShieldHitsWhenNotWearingShield') and not haveShield then
|
||||
if roll >= 95 then
|
||||
slot = Actor.EQUIPMENT_SLOT.Cuirass
|
||||
else
|
||||
slot = Actor.EQUIPMENT_SLOT.LeftPauldron
|
||||
end
|
||||
end
|
||||
elseif roll >= 85 then
|
||||
slot = Actor.EQUIPMENT_SLOT.RightGauntlet
|
||||
elseif roll >= 80 then
|
||||
slot = Actor.EQUIPMENT_SLOT.LeftGauntlet
|
||||
elseif roll >= 70 then
|
||||
slot = Actor.EQUIPMENT_SLOT.RightPauldron
|
||||
elseif roll >= 60 then
|
||||
slot = Actor.EQUIPMENT_SLOT.LeftPauldron
|
||||
elseif roll >= 50 then
|
||||
slot = Actor.EQUIPMENT_SLOT.Boots
|
||||
elseif roll >= 40 then
|
||||
slot = Actor.EQUIPMENT_SLOT.Greaves
|
||||
elseif roll >= 30 then
|
||||
slot = Actor.EQUIPMENT_SLOT.Helmet
|
||||
else
|
||||
slot = Actor.EQUIPMENT_SLOT.Cuirass
|
||||
end
|
||||
|
||||
return Actor.getEquipment(actor, slot)
|
||||
end
|
||||
|
||||
local function getDamage(attack, what)
|
||||
if attack.damage then
|
||||
return attack.damage[what] or 0
|
||||
end
|
||||
end
|
||||
|
||||
local function setDamage(attack, what, damage)
|
||||
attack.damage = attack.damage or {}
|
||||
attack.damage[what] = damage
|
||||
end
|
||||
|
||||
local function applyArmor(attack)
|
||||
local healthDamage = getDamage(attack, 'health')
|
||||
if healthDamage > 0 then
|
||||
local healthDamageAdjusted = I.Combat.adjustDamageForArmor(healthDamage)
|
||||
local diff = math.floor(healthDamageAdjusted - healthDamage)
|
||||
setDamage(attack, 'health', math.max(healthDamageAdjusted, 1))
|
||||
local item = I.Combat.pickRandomArmor()
|
||||
local skillid = I.Combat.getArmorSkill(item)
|
||||
if I.SkillProgression then
|
||||
I.SkillProgression.skillUsed(skillid, {useType = I.SkillProgression.SKILL_USE_TYPES.Armor_HitByOpponent})
|
||||
end
|
||||
if item and Armor.objectIsInstance(item) then
|
||||
local attackerIsUnarmedCreature = attack.attacker and not attack.weapon and Creature.objectIsInstance(attack.attacker)
|
||||
if settings:get('unarmedCreatureAttacksDamageArmor') or not attackerIsUnarmedCreature then
|
||||
core.sendGlobalEvent('ModifyItemCondition', { actor = self, item = item, amount = diff })
|
||||
end
|
||||
|
||||
if skillid == 'lightarmor' then
|
||||
core.sound.playSound3d('Light Armor Hit', self)
|
||||
elseif skillid == 'mediumarmor' then
|
||||
core.sound.playSound3d('Medium Armor Hit', self)
|
||||
elseif skillid == 'heavyarmor' then
|
||||
core.sound.playSound3d('Heavy Armor Hit', self)
|
||||
else
|
||||
core.sound.playSound3d('Hand To Hand Hit', self)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function adjustDamageForDifficulty(attack, defendant)
|
||||
local attackerIsPlayer = attack.attacker and Player.objectIsInstance(attack.attacker)
|
||||
-- The interface guarantees defendant is never nil
|
||||
local defendantIsPlayer = Player.objectIsInstance(defendant)
|
||||
-- If both characters are NPCs or both characters are players then
|
||||
-- difficulty settings do not apply
|
||||
if attackerIsPlayer == defendantIsPlayer then return end
|
||||
|
||||
local fDifficultyMult = core.getGMST('fDifficultyMult')
|
||||
local difficultyTerm = core.getGameDifficulty() * 0.01
|
||||
local x = 0
|
||||
|
||||
if defendantIsPlayer then
|
||||
-- Defending actor is a player
|
||||
if difficultyTerm > 0 then
|
||||
x = difficultyTerm * fDifficultyMult
|
||||
else
|
||||
x = difficultyTerm / fDifficultyMult
|
||||
end
|
||||
elseif attackerIsPlayer then
|
||||
-- Attacking actor is a player
|
||||
if difficultyTerm > 0 then
|
||||
x = -difficultyTerm / fDifficultyMult
|
||||
else
|
||||
x = -difficultyTerm * fDifficultyMult
|
||||
end
|
||||
end
|
||||
|
||||
setDamage(attack, 'health', getDamage(attack, 'health') * (1 + x))
|
||||
end
|
||||
|
||||
local function spawnBloodEffect(position)
|
||||
if isPlayer and not settings:get('spawnBloodEffectsOnPlayer') then
|
||||
return
|
||||
end
|
||||
|
||||
local bloodEffectModel = string.format('Blood_Model_%d', math.random(0, 2)) -- randIntUniformClosed(0, 2)
|
||||
|
||||
-- TODO: implement a Misc::correctMeshPath equivalent instead?
|
||||
-- All it ever does it append 'meshes\\' though
|
||||
bloodEffectModel = 'meshes/'..core.getGMST(bloodEffectModel)
|
||||
|
||||
local record = self.object.type.record(self.object)
|
||||
local bloodTexture = string.format('Blood_Texture_%d', record.bloodType)
|
||||
bloodTexture = core.getGMST(bloodTexture)
|
||||
if not bloodTexture or bloodTexture == '' then
|
||||
bloodTexture = core.getGMST('Blood_Texture_0')
|
||||
end
|
||||
core.sendGlobalEvent('SpawnVfx', {
|
||||
model = bloodEffectModel,
|
||||
position = position,
|
||||
options = {
|
||||
mwMagicVfx = false,
|
||||
particleTextureOverride = bloodTexture,
|
||||
useAmbientLight = false,
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
local function onHit(data)
|
||||
for i = #onHitHandlers, 1, -1 do
|
||||
if onHitHandlers[i](data) == false then
|
||||
return -- skip other handlers
|
||||
end
|
||||
end
|
||||
if data.successful and not godMode() then
|
||||
I.Combat.applyArmor(data)
|
||||
I.Combat.adjustDamageForDifficulty(data)
|
||||
if getDamage(data, 'health') > 0 then
|
||||
core.sound.playSound3d('Health Damage', self)
|
||||
if data.hitPos then
|
||||
spawnBloodEffect(data.hitPos)
|
||||
end
|
||||
end
|
||||
elseif data.attacker and Player.objectIsInstance(data.attacker) then
|
||||
core.sound.playSound3d('miss', self)
|
||||
end
|
||||
Actor._onHit(self, data)
|
||||
end
|
||||
|
||||
---
|
||||
-- Table of possible attack source types
|
||||
-- @type AttackSourceType
|
||||
-- @field #string Magic
|
||||
-- @field #string Melee
|
||||
-- @field #string Ranged
|
||||
-- @field #string Unspecified
|
||||
|
||||
---
|
||||
-- @type AttackInfo
|
||||
-- @field [parent=#AttackInfo] #table damage A table mapping stat name (health, fatigue, or magicka) to number. For example, {health = 50, fatigue = 10} will cause 50 damage to health and 10 to fatigue (before adjusting for armor and difficulty). This field is ignored for failed attacks.
|
||||
-- @field [parent=#AttackInfo] #number strength A number between 0 and 1 representing the attack strength. This field is ignored for failed attacks.
|
||||
-- @field [parent=#AttackInfo] #boolean successful Whether the attack was successful or not.
|
||||
-- @field [parent=#AttackInfo] #AttackSourceType sourceType What class of attack this is.
|
||||
-- @field [parent=#AttackInfo] openmw.self#ATTACK_TYPE type (Optional) Attack variant if applicable. For melee attacks this represents chop vs thrust vs slash. For unarmed creatures this implies which of its 3 possible attacks were used. For other attacks this field can be ignored.
|
||||
-- @field [parent=#AttackInfo] openmw.types#Actor attacker (Optional) Attacking actor
|
||||
-- @field [parent=#AttackInfo] openmw.types#Weapon weapon (Optional) Attacking weapon
|
||||
-- @field [parent=#AttackInfo] openmw.types#Weapon ammo (Optional) Ammo
|
||||
-- @field [parent=#AttackInfo] openmw.util#Vector3 hitPos (Optional) Where on the victim the attack is landing. Used to spawn blood effects. Blood effects are skipped if nil.
|
||||
return {
|
||||
--- Basic combat interface
|
||||
-- @module Combat
|
||||
-- @usage require('openmw.interfaces').Combat
|
||||
--
|
||||
--I.Combat.addOnHitHandler(function(attack)
|
||||
-- -- Adds fatigue loss when hit by draining fatigue when taking health damage
|
||||
-- if attack.damage.health and not attack.damage.fatigue then
|
||||
-- local strengthFactor = Actor.stats.attributes.strength(self).modified / 100 * 0.66
|
||||
-- local enduranceFactor = Actor.stats.attributes.endurance(self).modified / 100 * 0.34
|
||||
-- local factor = 1 - math.min(strengthFactor + enduranceFactor, 1)
|
||||
-- if factor > 0 then
|
||||
-- attack.damage.fatigue = attack.damage.health * factor
|
||||
-- end
|
||||
-- end
|
||||
--end)
|
||||
|
||||
interfaceName = 'Combat',
|
||||
interface = {
|
||||
--- Interface version
|
||||
-- @field [parent=#Combat] #number version
|
||||
version = 0,
|
||||
|
||||
--- Add new onHit handler for this actor
|
||||
-- If `handler(attack)` returns false, other handlers for
|
||||
-- the call will be skipped. where attack is the same @{#AttackInfo} passed to #Combat.onHit
|
||||
-- @function [parent=#Combat] addOnHitHandler
|
||||
-- @param #function handler The handler.
|
||||
addOnHitHandler = function(handler)
|
||||
onHitHandlers[#onHitHandlers + 1] = handler
|
||||
end,
|
||||
|
||||
--- Calculates the character's armor rating and adjusts damage accordingly.
|
||||
-- Note that this function only adjusts the number, use #Combat.applyArmor
|
||||
-- to include other side effects.
|
||||
-- @function [parent=#Combat] adjustDamageForArmor
|
||||
-- @param #number Damage The numeric damage to adjust
|
||||
-- @param openmw.core#GameObject actor (Optional) The actor to calculate the armor rating for. Defaults to self.
|
||||
-- @return #number Damage adjusted for armor
|
||||
adjustDamageForArmor = function(damage, actor) return adjustDamageForArmor(damage, actor or self) end,
|
||||
|
||||
--- Calculates a difficulty multiplier based on current difficulty settings
|
||||
-- and adjusts damage accordingly. Has no effect if both this actor and the
|
||||
-- attacker are NPCs, or if both are Players.
|
||||
-- @function [parent=#Combat] adjustDamageForDifficulty
|
||||
-- @param #Attack attack The attack to adjust
|
||||
-- @param openmw.core#GameObject defendant (Optional) The defendant to make the difficulty adjustment for. Defaults to self.
|
||||
adjustDamageForDifficulty = function(attack, defendant) return adjustDamageForDifficulty(attack, defendant or self) end,
|
||||
|
||||
--- Applies this character's armor to the attack. Adjusts damage, reduces item
|
||||
-- condition accordingly, progresses armor skill, and plays the armor appropriate
|
||||
-- hit sound.
|
||||
-- @function [parent=#Combat] applyArmor
|
||||
-- @param #Attack attack
|
||||
applyArmor = applyArmor,
|
||||
|
||||
--- Computes this character's armor rating.
|
||||
-- Note that this interface function is read by the engine to update the UI.
|
||||
-- This function can still be overridden same as any other interface, but must not call any functions or interfaces that modify anything.
|
||||
-- @function [parent=#Combat] getArmorRating
|
||||
-- @param openmw.core#GameObject actor (Optional) The actor to calculate the armor rating for. Defaults to self.
|
||||
-- @return #number
|
||||
getArmorRating = function(actor) return getArmorRating(actor or self) end,
|
||||
|
||||
--- Computes this character's armor rating.
|
||||
-- You can override this to return any skill you wish (including non-armor skills, if you so wish).
|
||||
-- Note that this interface function is read by the engine to update the UI.
|
||||
-- This function can still be overridden same as any other interface, but must not call any functions or interfaces that modify anything.
|
||||
-- @function [parent=#Combat] getArmorSkill
|
||||
-- @param openmw.core#GameObject item The item
|
||||
-- @return #string The armor skill identifier, or unarmored if the item was nil or not an instace of @{openmw.types#Armor}
|
||||
getArmorSkill = getArmorSkill,
|
||||
|
||||
--- Computes the armor rating of a single piece of @{openmw.types#Armor}, adjusted for skill
|
||||
-- Note that this interface function is read by the engine to update the UI.
|
||||
-- This function can still be overridden same as any other interface, but must not call any functions or interfaces that modify anything.
|
||||
-- @function [parent=#Combat] getSkillAdjustedArmorRating
|
||||
-- @param openmw.core#GameObject item The item
|
||||
-- @param openmw.core#GameObject actor (Optional) The actor, defaults to self
|
||||
-- @return #number
|
||||
getSkillAdjustedArmorRating = function(item, actor) return getSkillAdjustedArmorRating(item, actor or self) end,
|
||||
|
||||
--- Computes the effective armor rating of a single piece of @{openmw.types#Armor}, adjusted for skill and item condition
|
||||
-- @function [parent=#Combat] getEffectiveArmorRating
|
||||
-- @param openmw.core#GameObject item The item
|
||||
-- @param openmw.core#GameObject actor (Optional) The actor, defaults to self
|
||||
-- @return #number
|
||||
getEffectiveArmorRating = function(item, actor) return getEffectiveArmorRating(item, actor or self) end,
|
||||
|
||||
--- Spawns a random blood effect at the given position
|
||||
-- @function [parent=#Combat] spawnBloodEffect
|
||||
-- @param openmw.util#Vector3 position
|
||||
spawnBloodEffect = spawnBloodEffect,
|
||||
|
||||
--- Hit this actor. Normally called as Hit event from the attacking actor, with the same parameters.
|
||||
-- @function [parent=#Combat] onHit
|
||||
-- @param #AttackInfo attackInfo
|
||||
onHit = onHit,
|
||||
|
||||
--- Picks a random armor slot and returns the item equipped in that slot.
|
||||
-- Used to pick which armor to damage / skill to increase when hit during combat.
|
||||
-- @function [parent=#Combat] pickRandomArmor
|
||||
-- @param openmw.core#GameObject actor (Optional) The actor to pick armor from, defaults to self
|
||||
-- @return openmw.core#GameObject The armor equipped in the chosen slot. nil if nothing was equipped in that slot.
|
||||
pickRandomArmor = function(actor) return pickRandomArmor(actor or self) end,
|
||||
|
||||
--- @{#AttackSourceType}
|
||||
-- @field [parent=#Combat] #AttackSourceType ATTACK_SOURCE_TYPES Available attack source types
|
||||
ATTACK_SOURCE_TYPES = {
|
||||
Magic = 'magic',
|
||||
Melee = 'melee',
|
||||
Ranged = 'ranged',
|
||||
Unspecified = 'unspecified',
|
||||
},
|
||||
},
|
||||
|
||||
eventHandlers = {
|
||||
Hit = function(data) I.Combat.onHit(data) end,
|
||||
},
|
||||
}
|
1
files/data/scripts/omw/combat/menu.lua
Normal file
1
files/data/scripts/omw/combat/menu.lua
Normal file
|
@ -0,0 +1 @@
|
|||
require('scripts.omw.combat.common').registerSettingsPage()
|
|
@ -19,5 +19,18 @@ return {
|
|||
BreakInvisibility = function(data)
|
||||
Actor.activeEffects(self):remove(core.magic.EFFECT_TYPE.Invisibility)
|
||||
end,
|
||||
Unequip = function(data)
|
||||
local equipment = Actor.getEquipment(self)
|
||||
if data.item then
|
||||
for slot, item in pairs(equipment) do
|
||||
if item == data.item then
|
||||
equipment[slot] = nil
|
||||
end
|
||||
end
|
||||
elseif data.slot then
|
||||
equipment[slot] = nil
|
||||
end
|
||||
Actor.setEquipment(self, equipment)
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -22,6 +22,16 @@ local function onPlaySound3d(data)
|
|||
end
|
||||
end
|
||||
|
||||
local function onModifyItemCondition(data)
|
||||
local itemData = Item.itemData(data.item)
|
||||
itemData.condition = math.min(data.item.type.record(data.item).health, math.max(0, itemData.condition + data.amount))
|
||||
|
||||
-- Force unequip broken items
|
||||
if data.actor and itemData.condition <= 0 then
|
||||
data.actor:sendEvent('Unequip', {item = data.item})
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
eventHandlers = {
|
||||
SpawnVfx = function(data)
|
||||
|
@ -35,5 +45,6 @@ return {
|
|||
Unlock = function(data)
|
||||
Lockable.unlock(data.target)
|
||||
end,
|
||||
ModifyItemCondition = onModifyItemCondition,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -690,10 +690,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
|
|||
<source><html><head/><body><p>If this setting is true, the player is allowed to loot actors (e.g. summoned creatures) during death animation, if they are not in combat. In this case we have to increment death counter and run disposed actor's script instantly.</p><p>If this setting is false, player has to wait until end of death animation in all cases. Makes using of summoned creatures exploit (looting summoned Dremoras and Golden Saints for expensive weapons) a lot harder. Conflicts with mannequin mods, which use SkipAnim to prevent end of death animation.</p></body></html></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><html><head/><body><p>Makes unarmed creature attacks able to reduce armor condition, just as attacks from NPCs and armed creatures.</p></body></html></source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Off</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
@ -1155,10 +1151,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
|
|||
<source>Classic Reflected Absorb Spells Behavior</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Unarmed Creature Attacks Damage Armor</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Affect Werewolves</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
|
|
@ -798,14 +798,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
|
|||
<source>Classic Reflected Absorb Spells Behavior</source>
|
||||
<translation></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><html><head/><body><p>Makes unarmed creature attacks able to reduce armor condition, just as attacks from NPCs and armed creatures.</p></body></html></source>
|
||||
<translation></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Unarmed Creature Attacks Damage Armor</source>
|
||||
<translation></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Factor Strength into Hand-to-Hand Combat</source>
|
||||
<translation></translation>
|
||||
|
|
|
@ -690,10 +690,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
|
|||
<source><html><head/><body><p>If this setting is true, the player is allowed to loot actors (e.g. summoned creatures) during death animation, if they are not in combat. In this case we have to increment death counter and run disposed actor's script instantly.</p><p>If this setting is false, player has to wait until end of death animation in all cases. Makes using of summoned creatures exploit (looting summoned Dremoras and Golden Saints for expensive weapons) a lot harder. Conflicts with mannequin mods, which use SkipAnim to prevent end of death animation.</p></body></html></source>
|
||||
<translation><html><head/><body><p>Lorsque cette option est activée, le joueur est autorisé à piller créatures et PNJ (p. ex. les créatures invoquées) durant leur animation de mort, si elles ne sont pas en combat. Dans ce cas, le jeu incrémente le conteur de mort et lance son script instantanément.</p><p>Lorsque cette option est désactivée, le joueur doit attendre la fin de l'animation de mort. Dans ce cas, l'utilisation de l'exploit des créatures invoquées (piller des créatures invoquées telles que des Drémoras ou des Saintes Dorées afin d'obtenir des armes de grandes valeurs) est rendu beaucoup plus ardu. Cette option entre en confit avec les Mods de mannequin. En effet, ceux-ci utilisent SkipAnim afin d'éviter la fin de l'animation de mort.</p></body></html></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><html><head/><body><p>Makes unarmed creature attacks able to reduce armor condition, just as attacks from NPCs and armed creatures.</p></body></html></source>
|
||||
<translation><html><head/><body><p>L'option donne aux créatures non armées la capacité d'endommager les pièces d'armure, comme le font les PNJ et les créatures armées.</p></body></html></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Off</source>
|
||||
<translation>Inactif</translation>
|
||||
|
@ -1158,10 +1154,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
|
|||
<source>Classic Reflected Absorb Spells Behavior</source>
|
||||
<translation>Comportement traditionnel de la réflexion des sorts d'absorbtion</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Unarmed Creature Attacks Damage Armor</source>
|
||||
<translation>L'attaque des créatures non armées endomage les armures</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Affect Werewolves</source>
|
||||
<translation>S'applique aux loups garoux</translation>
|
||||
|
|
|
@ -736,14 +736,6 @@ to default Morrowind fonts. Check this box if you still prefer original fonts ov
|
|||
<source><html><head/><body><p>Give actors an ability to swim over the water surface when they follow other actor independently from their ability to swim. Has effect only when nav mesh building is enabled.</p></body></html></source>
|
||||
<translation><html><head/><body><p>Позволяет любым персонажам плыть возле поверхности воды, чтобы следовать за другим персонажем, вне зависимости от того, могут они плыть, или нет. Работает только с навигационной сеткой.</p></body></html></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><html><head/><body><p>Makes unarmed creature attacks able to reduce armor condition, just as attacks from NPCs and armed creatures.</p></body></html></source>
|
||||
<translation><html><head/><body><p>Позволяет атакам существ без оружия повреждать броню цели, по аналогии с атаками оружием.</p></body></html></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Unarmed Creature Attacks Damage Armor</source>
|
||||
<translation>Атаки существ повреждают броню</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Off</source>
|
||||
<translation>Отключено</translation>
|
||||
|
|
|
@ -693,10 +693,6 @@ de ordinarie fonterna i Morrowind. Bocka denna ruta om du ändå föredrar ordin
|
|||
<source><html><head/><body><p>If this setting is true, the player is allowed to loot actors (e.g. summoned creatures) during death animation, if they are not in combat. In this case we have to increment death counter and run disposed actor's script instantly.</p><p>If this setting is false, player has to wait until end of death animation in all cases. Makes using of summoned creatures exploit (looting summoned Dremoras and Golden Saints for expensive weapons) a lot harder. Conflicts with mannequin mods, which use SkipAnim to prevent end of death animation.</p></body></html></source>
|
||||
<translation><html><head/><body><p>Om denna inställning är aktiv tillåts spelaren plundra figurer (exempelvis tillkallade varelser) under deras dödsanimation, om de inte är i strid.</p><p>Om inställningen är inaktiv måste spelaren vänta tills dödsanimationen är slut. Detta gör det mycket svårare att exploatera tillkallade varelser (exempelvis plundra Draemoror eller Golden Saints för att få dyrbara vapen). Inställningen är i konflikt med skyltdocks-moddar som använder SkipAnim för att förhindra avslutning av dödsanimationer.</p></body></html></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source><html><head/><body><p>Makes unarmed creature attacks able to reduce armor condition, just as attacks from NPCs and armed creatures.</p></body></html></source>
|
||||
<translation><html><head/><body><p>Gör att obeväpnade varelseattacker kan reducera rustningars skick, precis som attacker från icke-spelbara figurer och beväpnade varelser.</p></body></html></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Off</source>
|
||||
<translation>Av</translation>
|
||||
|
@ -1163,10 +1159,6 @@ de ordinarie fonterna i Morrowind. Bocka denna ruta om du ändå föredrar ordin
|
|||
<source>Classic Reflected Absorb Spells Behavior</source>
|
||||
<translation>Klassiskt beteende för reflekterade "Absorb"-besvärjelser</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Unarmed Creature Attacks Damage Armor</source>
|
||||
<translation>Obeväpnad attack från varelser skadar rustning</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Affect Werewolves</source>
|
||||
<translation>Påverka varulvar</translation>
|
||||
|
|
|
@ -62,6 +62,11 @@
|
|||
-- @param #string setting Setting name
|
||||
-- @return #any
|
||||
|
||||
---
|
||||
-- The game's difficulty setting.
|
||||
-- @function [parent=#core] getGameDifficulty
|
||||
-- @return #number
|
||||
|
||||
---
|
||||
-- Return l10n formatting function for the given context.
|
||||
-- Localisation files (containing the message names and translations) should be stored in
|
||||
|
|
|
@ -15,6 +15,9 @@
|
|||
---
|
||||
-- @field [parent=#interfaces] scripts.omw.camera.camera#scripts.omw.camera.camera Camera
|
||||
|
||||
---
|
||||
-- @field [parent=#interfaces] scripts.omw.combat.local#scripts.omw.combat.local Combat
|
||||
|
||||
---
|
||||
-- @field [parent=#interfaces] scripts.omw.mwui.init#scripts.omw.mwui.init MWUI
|
||||
|
||||
|
|
|
@ -863,6 +863,7 @@
|
|||
-- @field #boolean isBiped whether the creature is a biped
|
||||
-- @field #boolean isEssential whether the creature is essential
|
||||
-- @field #boolean isRespawning whether the creature respawns after death
|
||||
-- @field #number bloodType integer representing the blood type of the Creature. Used to generate the correct blood vfx.
|
||||
|
||||
|
||||
--- @{#NPC} functions
|
||||
|
@ -1137,6 +1138,7 @@
|
|||
-- @field #list<#TravelDestination> travelDestinations A list of @{#TravelDestination}s for this NPC.
|
||||
-- @field #boolean isEssential whether the NPC is essential
|
||||
-- @field #boolean isRespawning whether the NPC respawns after death
|
||||
-- @field #number bloodType integer representing the blood type of the NPC. Used to generate the correct blood vfx.
|
||||
|
||||
---
|
||||
-- @type TravelDestination
|
||||
|
|
|
@ -363,9 +363,6 @@ default actor pathfind half extents = 29.27999496459961 28.479997634887695 66.5
|
|||
# Enables use of day/night switch nodes
|
||||
day night switches = true
|
||||
|
||||
# Enables degradation of NPC's armor from unarmed creature attacks.
|
||||
unarmed creature attacks damage armor = false
|
||||
|
||||
# Collision is used for both physics simulation and navigation mesh generation for pathfinding:
|
||||
# 0 = Axis-aligned bounding box
|
||||
# 1 = Rotating box
|
||||
|
|
Loading…
Reference in a new issue