Merge branch 'active-spells-rework' into 'master'

Lua: active spell params and active spell effects

See merge request OpenMW/openmw!3179
macos_ci_fix
Petr Mikheev 1 year ago
commit abde92e207

@ -1,9 +1,15 @@
#include "magicbindings.hpp"
#include <components/esm3/activespells.hpp>
#include <components/esm3/loadalch.hpp>
#include <components/esm3/loadarmo.hpp>
#include <components/esm3/loadbook.hpp>
#include <components/esm3/loadclot.hpp>
#include <components/esm3/loadench.hpp>
#include <components/esm3/loadingr.hpp>
#include <components/esm3/loadmgef.hpp>
#include <components/esm3/loadspel.hpp>
#include <components/esm3/loadweap.hpp>
#include <components/lua/luastate.hpp>
#include <components/misc/color.hpp>
#include <components/misc/resourcehelpers.hpp>
@ -111,6 +117,12 @@ namespace MWLua
MWMechanics::EffectKey key;
MWMechanics::EffectParam param;
};
struct ActiveSpell
{
ObjectVariant mActor;
MWMechanics::ActiveSpells::ActiveSpellParams mParams;
};
// class returned via 'types.Actor.spells(obj)' in Lua
using ActorSpells = ActorStore<MWMechanics::Spells>;
// class returned via 'types.Actor.activeEffects(obj)' in Lua
@ -141,6 +153,10 @@ namespace sol
struct is_automagical<MWLua::ActorStore<T>> : std::false_type
{
};
template <>
struct is_automagical<MWLua::ActiveSpell> : std::false_type
{
};
}
namespace MWLua
@ -152,6 +168,27 @@ namespace MWLua
else
return ESM::RefId::deserializeText(LuaUtil::cast<std::string_view>(spellOrId));
}
static ESM::RefId toRecordId(const sol::object& recordOrId)
{
if (recordOrId.is<ESM::Spell>())
return recordOrId.as<const ESM::Spell*>()->mId;
else if (recordOrId.is<ESM::Potion>())
return recordOrId.as<const ESM::Potion*>()->mId;
else if (recordOrId.is<ESM::Ingredient>())
return recordOrId.as<const ESM::Ingredient*>()->mId;
else if (recordOrId.is<ESM::Enchantment>())
return recordOrId.as<const ESM::Enchantment*>()->mId;
else if (recordOrId.is<ESM::Armor>())
return recordOrId.as<const ESM::Armor*>()->mId;
else if (recordOrId.is<ESM::Book>())
return recordOrId.as<const ESM::Book*>()->mId;
else if (recordOrId.is<ESM::Clothing>())
return recordOrId.as<const ESM::Clothing*>()->mId;
else if (recordOrId.is<ESM::Weapon>())
return recordOrId.as<const ESM::Weapon*>()->mId;
else
return ESM::RefId::deserializeText(LuaUtil::cast<std::string_view>(recordOrId));
}
sol::table initCoreMagicBindings(const Context& context)
{
@ -372,6 +409,122 @@ namespace MWLua
// magicEffectT["projectileSpeed"]
// = sol::readonly_property([](const ESM::MagicEffect& rec) -> float { return rec.mData.mSpeed; });
auto activeSpellEffectT = context.mLua->sol().new_usertype<ESM::ActiveEffect>("ActiveSpellEffect");
activeSpellEffectT[sol::meta_function::to_string] = [](const ESM::ActiveEffect& effect) {
return "ActiveSpellEffect[" + ESM::MagicEffect::indexToGmstString(effect.mEffectId) + "]";
};
activeSpellEffectT["id"] = sol::readonly_property([](const ESM::ActiveEffect& effect) -> std::string {
auto name = ESM::MagicEffect::indexToName(effect.mEffectId);
return Misc::StringUtils::lowerCase(name);
});
activeSpellEffectT["name"] = sol::readonly_property([](const ESM::ActiveEffect& effect) -> std::string {
return MWMechanics::EffectKey(effect.mEffectId, effect.mArg).toString();
});
activeSpellEffectT["affectedSkill"]
= sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional<std::string> {
auto* rec = magicEffectStore->find(effect.mEffectId);
if ((rec->mData.mFlags & ESM::MagicEffect::TargetSkill) && effect.mArg >= 0
&& effect.mArg < ESM::Skill::Length)
return ESM::Skill::indexToRefId(effect.mArg).serializeText();
else
return sol::nullopt;
});
activeSpellEffectT["affectedAttribute"]
= sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional<std::string> {
auto* rec = magicEffectStore->find(effect.mEffectId);
if ((rec->mData.mFlags & ESM::MagicEffect::TargetAttribute) && effect.mArg >= 0
&& effect.mArg < ESM::Attribute::Length)
return Misc::StringUtils::lowerCase(ESM::Attribute::sAttributeNames[effect.mArg]);
else
return sol::nullopt;
});
activeSpellEffectT["magnitudeThisFrame"]
= sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional<float> {
auto* rec = magicEffectStore->find(effect.mEffectId);
if (rec->mData.mFlags & ESM::MagicEffect::Flags::NoMagnitude)
return sol::nullopt;
return effect.mMagnitude;
});
activeSpellEffectT["minMagnitude"]
= sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional<float> {
auto* rec = magicEffectStore->find(effect.mEffectId);
if (rec->mData.mFlags & ESM::MagicEffect::Flags::NoMagnitude)
return sol::nullopt;
return effect.mMinMagnitude;
});
activeSpellEffectT["maxMagnitude"]
= sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional<float> {
auto* rec = magicEffectStore->find(effect.mEffectId);
if (rec->mData.mFlags & ESM::MagicEffect::Flags::NoMagnitude)
return sol::nullopt;
return effect.mMaxMagnitude;
});
activeSpellEffectT["durationLeft"]
= sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional<float> {
// Permanent/constant effects, abilities, etc. will have a negative duration
if (effect.mDuration < 0)
return sol::nullopt;
auto* rec = magicEffectStore->find(effect.mEffectId);
if (rec->mData.mFlags & ESM::MagicEffect::Flags::NoDuration)
return sol::nullopt;
return effect.mTimeLeft;
});
activeSpellEffectT["duration"]
= sol::readonly_property([magicEffectStore](const ESM::ActiveEffect& effect) -> sol::optional<float> {
// Permanent/constant effects, abilities, etc. will have a negative duration
if (effect.mDuration < 0)
return sol::nullopt;
auto* rec = magicEffectStore->find(effect.mEffectId);
if (rec->mData.mFlags & ESM::MagicEffect::Flags::NoDuration)
return sol::nullopt;
return effect.mDuration;
});
auto activeSpellT = context.mLua->sol().new_usertype<ActiveSpell>("ActiveSpellParams");
activeSpellT[sol::meta_function::to_string] = [](const ActiveSpell& activeSpell) {
return "ActiveSpellParams[" + activeSpell.mParams.getId().serializeText() + "]";
};
activeSpellT["name"] = sol::readonly_property(
[](const ActiveSpell& activeSpell) -> std::string_view { return activeSpell.mParams.getDisplayName(); });
activeSpellT["id"] = sol::readonly_property(
[](const ActiveSpell& activeSpell) -> std::string { return activeSpell.mParams.getId().serializeText(); });
activeSpellT["item"] = sol::readonly_property([&lua](const ActiveSpell& activeSpell) -> sol::object {
auto item = activeSpell.mParams.getItem();
if (!item.isSet())
return sol::nil;
auto itemPtr = MWBase::Environment::get().getWorldModel()->getPtr(item);
if (itemPtr.isEmpty())
return sol::nil;
if (activeSpell.mActor.isGObject())
return sol::make_object(lua, GObject(itemPtr));
else
return sol::make_object(lua, LObject(itemPtr));
});
activeSpellT["caster"] = sol::readonly_property([&lua](const ActiveSpell& activeSpell) -> sol::object {
auto caster
= MWBase::Environment::get().getWorld()->searchPtrViaActorId(activeSpell.mParams.getCasterActorId());
if (caster.isEmpty())
return sol::nil;
else
{
if (activeSpell.mActor.isGObject())
return sol::make_object(lua, GObject(getId(caster)));
else
return sol::make_object(lua, LObject(getId(caster)));
}
});
activeSpellT["effects"] = sol::readonly_property([&lua](const ActiveSpell& activeSpell) -> sol::table {
sol::table res(lua, sol::create);
size_t tableIndex = 0;
for (const ESM::ActiveEffect& effect : activeSpell.mParams.getEffects())
{
if (!(effect.mFlags & ESM::ActiveEffect::Flag_Applied))
continue;
res[++tableIndex] = effect; // ESM::ActiveEffect (effect params)
}
return res;
});
auto activeEffectT = context.mLua->sol().new_usertype<ActiveEffect>("ActiveEffect");
activeEffectT[sol::meta_function::to_string] = [](const ActiveEffect& effect) {
@ -547,18 +700,16 @@ namespace MWLua
};
// pairs(types.Actor.activeSpells(o))
// Note that the indexes are fake, and only for consistency with other lua pairs interfaces. You can't use them
// for anything.
activeSpellsT["__pairs"] = [](sol::this_state ts, ActorActiveSpells& self) {
sol::state_view lua(ts);
self.reset();
return sol::as_function([lua, &self]() mutable -> std::pair<sol::object, sol::object> {
return sol::as_function([lua, self]() mutable -> std::pair<sol::object, sol::object> {
if (!self.isEnd())
{
auto result = sol::make_object(lua, self.mIterator->getId());
auto index = sol::make_object(lua, self.mIndex + 1);
auto id = sol::make_object(lua, self.mIterator->getId().serializeText());
auto params = sol::make_object(lua, ActiveSpell{ self.mActor, *self.mIterator });
self.advance();
return { index, result };
return { params, params };
}
else
{
@ -568,10 +719,13 @@ namespace MWLua
};
// types.Actor.activeSpells(o):isSpellActive(id)
activeSpellsT["isSpellActive"] = [](const ActorActiveSpells& spells, const sol::object& spellOrId) -> bool {
auto id = toSpellId(spellOrId);
if (auto* store = spells.getStore())
return store->isSpellActive(id);
activeSpellsT["isSpellActive"]
= [](const ActorActiveSpells& activeSpells, const sol::object& recordOrId) -> bool {
if (auto* store = activeSpells.getStore())
{
auto id = toRecordId(recordOrId);
return store->isSpellActive(id) || store->isEnchantmentActive(id);
}
return false;
};
@ -593,7 +747,7 @@ namespace MWLua
activeEffectsT["__pairs"] = [](sol::this_state ts, ActorActiveEffects& self) {
sol::state_view lua(ts);
self.reset();
return sol::as_function([lua, &self]() mutable -> std::pair<sol::object, sol::object> {
return sol::as_function([lua, self]() mutable -> std::pair<sol::object, sol::object> {
if (!self.isEnd())
{
ActiveEffect effect = ActiveEffect{ self.mIterator->first, self.mIterator->second };

@ -28,6 +28,7 @@
#include "../mwworld/class.hpp"
#include "../mwworld/esmstore.hpp"
#include "../mwworld/inventorystore.hpp"
#include "../mwworld/manualref.hpp"
namespace
{
@ -423,6 +424,30 @@ namespace MWMechanics
!= mSpells.end();
}
bool ActiveSpells::isEnchantmentActive(const ESM::RefId& id) const
{
const auto& store = MWBase::Environment::get().getESMStore();
if (store->get<ESM::Enchantment>().search(id) == nullptr)
return false;
// Enchantment id is not stored directly. Instead the enchanted item is stored.
return std::find_if(mSpells.begin(), mSpells.end(), [&](const auto& spell) {
switch (store->find(spell.mId))
{
case ESM::REC_ARMO:
return store->get<ESM::Armor>().find(spell.mId)->mEnchant == id;
case ESM::REC_BOOK:
return store->get<ESM::Book>().find(spell.mId)->mEnchant == id;
case ESM::REC_CLOT:
return store->get<ESM::Clothing>().find(spell.mId)->mEnchant == id;
case ESM::REC_WEAP:
return store->get<ESM::Weapon>().find(spell.mId)->mEnchant == id;
default:
return false;
}
}) != mSpells.end();
}
void ActiveSpells::addSpell(const ActiveSpellParams& params)
{
mQueue.emplace_back(params);

@ -72,6 +72,8 @@ namespace MWMechanics
const std::string& getDisplayName() const { return mDisplayName; }
ESM::RefNum getItem() const { return mItem; }
// Increments worsenings count and sets the next timestamp
void worsen();
@ -143,8 +145,12 @@ namespace MWMechanics
/// Remove all spells
void clear(const MWWorld::Ptr& ptr);
/// True if a spell associated with this id is active
/// \note For enchantments, this is the id of the enchanted item, not the enchantment itself
bool isSpellActive(const ESM::RefId& id) const;
///< case insensitive
/// True if the enchantment is active
bool isEnchantmentActive(const ESM::RefId& id) const;
void skipWorsenings(double hours);

@ -316,6 +316,26 @@
-- local all = cell:getAll()
-- local weapons = cell:getAll(types.Weapon)
---
-- @type ActiveSpell
-- @field #string name The spell or item display name
-- @field #string id Record id of the spell or item used to cast the spell
-- @field openmw.core#GameObject item The enchanted item used to cast the spell, or nil if the spell was not cast from an enchanted item. Note that if the spell was cast for a single-use enchantment such as a scroll, this will be nil.
-- @field openmw.core#GameObject caster The caster object, or nil if the spell has no defined caster
-- @field #list<#ActiveSpellEffect> effects The active effects (@{#ActiveSpellEffect}) of this spell.
---
-- @type ActiveSpellEffect
-- @field #string affectedSkill @{#SKILL} or nil
-- @field #string affectedAttribute @{#ATTRIBUTE} or nil
-- @field #string id Magic effect id
-- @field #string name Localized name of the effect
-- @field #number magnitudeThisFrame The magnitude of the effect in the current frame. This will be a new random number between minMagnitude and maxMagnitude every frame. Or nil if the effect has no magnitude.
-- @field #number minMagnitude The minimum magnitude of this effect, or nil if the effect has no magnitude.
-- @field #number maxMagnitude The maximum magnitude of this effect, or nil if the effect has no magnitude.
-- @field #number duration Total duration in seconds of this spell effect, should not be confused with remaining duration. Or nil if the effect is not temporary.
-- @field #number durationLeft Remaining duration in seconds of this spell effect, or nil if the effect is not temporary.
--- Possible @{#EnchantmentType} values
-- @field [parent=#Magic] #EnchantmentType ENCHANTMENT_TYPE
@ -699,8 +719,8 @@
---
-- @type MagicEffectWithParams
-- @field #MagicEffect effect @{#MagicEffect}
-- @field #any affectedSkill @{#SKILL} or nil
-- @field #any affectedAttribute @{#ATTRIBUTE} or nil
-- @field #string affectedSkill @{#SKILL} or nil
-- @field #string affectedAttribute @{#ATTRIBUTE} or nil
-- @field #number range
-- @field #number area
-- @field #number magnitudeMin
@ -709,8 +729,8 @@
---
-- @type ActiveEffect
-- @field #any affectedSkill @{#SKILL} or nil
-- @field #any affectedAttribute @{#ATTRIBUTE} or nil
-- @field #string affectedSkill @{#SKILL} or nil
-- @field #string affectedAttribute @{#ATTRIBUTE} or nil
-- @field #string id Effect id string
-- @field #string name Localized name of the effect
-- @field #number magnitude

@ -239,7 +239,7 @@
-- @param openmw.core#GameObject actor
-- @return #ActorActiveSpells
--- Read-only list of spells currently affecting the actor.
--- Read-only list of spells currently affecting the actor. Can be iterated over for a list of @{openmw.core#ActiveSpell}
-- @type ActorActiveSpells
-- @usage -- print active spells
-- for _, spell in pairs(Actor.activeSpells(self)) do
@ -251,12 +251,33 @@
-- else
-- print('Player does not have bound longbow')
-- end
-- @usage -- Print all information about active spells
-- for id, params in pairs(Actor.activeSpells(self)) do
-- print('active spell '..tostring(id)..':')
-- print(' name: '..tostring(params.name))
-- print(' id: '..tostring(params.id))
-- print(' item: '..tostring(params.item))
-- print(' caster: '..tostring(params.caster))
-- print(' effects: '..tostring(params.effects))
-- for _, effect in pairs(params.effects) do
-- print(' -> effects['..tostring(effect)..']:')
-- print(' id: '..tostring(effect.id))
-- print(' name: '..tostring(effect.name))
-- print(' affectedSkill: '..tostring(effect.affectedSkill))
-- print(' affectedAttribute: '..tostring(effect.affectedAttribute))
-- print(' magnitudeThisFrame: '..tostring(effect.magnitudeThisFrame))
-- print(' minMagnitude: '..tostring(effect.minMagnitude))
-- print(' maxMagnitude: '..tostring(effect.maxMagnitude))
-- print(' duration: '..tostring(effect.duration))
-- print(' durationLeft: '..tostring(effect.durationLeft))
-- end
-- end
---
-- Get whether a specific spell is active on the actor.
-- @function [parent=#ActorActiveSpells] isSpellActive
-- @param self
-- @param #any spellOrId @{openmw.core#Spell} or string spell id
-- @param #any recordOrId record or string record ID of the active spell's source. valid records are @{openmw.core#Spell}, @{openmw.core#Enchantment}, #IngredientRecord, or #PotionRecord
-- @return true if spell is active, false otherwise
---

Loading…
Cancel
Save