Merge branch 'spellcast-refactor' into 'master'

Spellcast related Lua API + spellcasting/activespell refactor

See merge request OpenMW/openmw!3922
fix-osga-rotate-wildly
psi29a 9 months ago
commit 012d10703f

@ -180,22 +180,23 @@ namespace
void printEffectList(const ESM::EffectList& effects)
{
int i = 0;
for (const ESM::ENAMstruct& effect : effects.mList)
for (const ESM::IndexedENAMstruct& effect : effects.mList)
{
std::cout << " Effect[" << i << "]: " << magicEffectLabel(effect.mEffectID) << " (" << effect.mEffectID
<< ")" << std::endl;
if (effect.mSkill != -1)
std::cout << " Skill: " << skillLabel(effect.mSkill) << " (" << (int)effect.mSkill << ")"
std::cout << " Effect[" << i << "]: " << magicEffectLabel(effect.mData.mEffectID) << " ("
<< effect.mData.mEffectID << ")" << std::endl;
if (effect.mData.mSkill != -1)
std::cout << " Skill: " << skillLabel(effect.mData.mSkill) << " (" << (int)effect.mData.mSkill << ")"
<< std::endl;
if (effect.mData.mAttribute != -1)
std::cout << " Attribute: " << attributeLabel(effect.mData.mAttribute) << " ("
<< (int)effect.mData.mAttribute << ")" << std::endl;
std::cout << " Range: " << rangeTypeLabel(effect.mData.mRange) << " (" << effect.mData.mRange << ")"
<< std::endl;
if (effect.mAttribute != -1)
std::cout << " Attribute: " << attributeLabel(effect.mAttribute) << " (" << (int)effect.mAttribute
<< ")" << std::endl;
std::cout << " Range: " << rangeTypeLabel(effect.mRange) << " (" << effect.mRange << ")" << std::endl;
// Area is always zero if range type is "Self"
if (effect.mRange != ESM::RT_Self)
std::cout << " Area: " << effect.mArea << std::endl;
std::cout << " Duration: " << effect.mDuration << std::endl;
std::cout << " Magnitude: " << effect.mMagnMin << "-" << effect.mMagnMax << std::endl;
if (effect.mData.mRange != ESM::RT_Self)
std::cout << " Area: " << effect.mData.mArea << std::endl;
std::cout << " Duration: " << effect.mData.mDuration << std::endl;
std::cout << " Magnitude: " << effect.mData.mMagnMin << "-" << effect.mData.mMagnMax << std::endl;
i++;
}
}

@ -60,38 +60,38 @@ void CSMTools::EnchantmentCheckStage::perform(int stage, CSMDoc::Messages& messa
}
else
{
std::vector<ESM::ENAMstruct>::const_iterator effect = enchantment.mEffects.mList.begin();
std::vector<ESM::IndexedENAMstruct>::const_iterator effect = enchantment.mEffects.mList.begin();
for (size_t i = 1; i <= enchantment.mEffects.mList.size(); i++)
{
const std::string number = std::to_string(i);
// At the time of writing this effects, attributes and skills are hardcoded
if (effect->mEffectID < 0 || effect->mEffectID > 142)
if (effect->mData.mEffectID < 0 || effect->mData.mEffectID > 142)
{
messages.add(id, "Effect #" + number + " is invalid", "", CSMDoc::Message::Severity_Error);
++effect;
continue;
}
if (effect->mSkill < -1 || effect->mSkill > 26)
if (effect->mData.mSkill < -1 || effect->mData.mSkill > 26)
messages.add(
id, "Effect #" + number + " affected skill is invalid", "", CSMDoc::Message::Severity_Error);
if (effect->mAttribute < -1 || effect->mAttribute > 7)
if (effect->mData.mAttribute < -1 || effect->mData.mAttribute > 7)
messages.add(
id, "Effect #" + number + " affected attribute is invalid", "", CSMDoc::Message::Severity_Error);
if (effect->mRange < 0 || effect->mRange > 2)
if (effect->mData.mRange < 0 || effect->mData.mRange > 2)
messages.add(id, "Effect #" + number + " range is invalid", "", CSMDoc::Message::Severity_Error);
if (effect->mArea < 0)
if (effect->mData.mArea < 0)
messages.add(id, "Effect #" + number + " area is negative", "", CSMDoc::Message::Severity_Error);
if (effect->mDuration < 0)
if (effect->mData.mDuration < 0)
messages.add(id, "Effect #" + number + " duration is negative", "", CSMDoc::Message::Severity_Error);
if (effect->mMagnMin < 0)
if (effect->mData.mMagnMin < 0)
messages.add(
id, "Effect #" + number + " minimum magnitude is negative", "", CSMDoc::Message::Severity_Error);
if (effect->mMagnMax < 0)
if (effect->mData.mMagnMax < 0)
messages.add(
id, "Effect #" + number + " maximum magnitude is negative", "", CSMDoc::Message::Severity_Error);
if (effect->mMagnMin > effect->mMagnMax)
if (effect->mData.mMagnMin > effect->mData.mMagnMax)
messages.add(id, "Effect #" + number + " minimum magnitude is higher than maximum magnitude", "",
CSMDoc::Message::Severity_Error);
++effect;

@ -255,20 +255,22 @@ namespace CSMWorld
{
ESXRecordT magic = record.get();
std::vector<ESM::ENAMstruct>& effectsList = magic.mEffects.mList;
std::vector<ESM::IndexedENAMstruct>& effectsList = magic.mEffects.mList;
// blank row
ESM::ENAMstruct effect;
effect.mEffectID = 0;
effect.mSkill = -1;
effect.mAttribute = -1;
effect.mRange = 0;
effect.mArea = 0;
effect.mDuration = 0;
effect.mMagnMin = 0;
effect.mMagnMax = 0;
ESM::IndexedENAMstruct effect;
effect.mIndex = position;
effect.mData.mEffectID = 0;
effect.mData.mSkill = -1;
effect.mData.mAttribute = -1;
effect.mData.mRange = 0;
effect.mData.mArea = 0;
effect.mData.mDuration = 0;
effect.mData.mMagnMin = 0;
effect.mData.mMagnMax = 0;
effectsList.insert(effectsList.begin() + position, effect);
magic.mEffects.updateIndexes();
record.setModified(magic);
}
@ -277,12 +279,13 @@ namespace CSMWorld
{
ESXRecordT magic = record.get();
std::vector<ESM::ENAMstruct>& effectsList = magic.mEffects.mList;
std::vector<ESM::IndexedENAMstruct>& effectsList = magic.mEffects.mList;
if (rowToRemove < 0 || rowToRemove >= static_cast<int>(effectsList.size()))
throw std::runtime_error("index out of range");
effectsList.erase(effectsList.begin() + rowToRemove);
magic.mEffects.updateIndexes();
record.setModified(magic);
}
@ -292,7 +295,7 @@ namespace CSMWorld
ESXRecordT magic = record.get();
magic.mEffects.mList
= static_cast<const NestedTableWrapper<std::vector<ESM::ENAMstruct>>&>(nestedTable).mNestedTable;
= static_cast<const NestedTableWrapper<std::vector<ESM::IndexedENAMstruct>>&>(nestedTable).mNestedTable;
record.setModified(magic);
}
@ -300,19 +303,19 @@ namespace CSMWorld
NestedTableWrapperBase* table(const Record<ESXRecordT>& record) const override
{
// deleted by dtor of NestedTableStoring
return new NestedTableWrapper<std::vector<ESM::ENAMstruct>>(record.get().mEffects.mList);
return new NestedTableWrapper<std::vector<ESM::IndexedENAMstruct>>(record.get().mEffects.mList);
}
QVariant getData(const Record<ESXRecordT>& record, int subRowIndex, int subColIndex) const override
{
ESXRecordT magic = record.get();
std::vector<ESM::ENAMstruct>& effectsList = magic.mEffects.mList;
std::vector<ESM::IndexedENAMstruct>& effectsList = magic.mEffects.mList;
if (subRowIndex < 0 || subRowIndex >= static_cast<int>(effectsList.size()))
throw std::runtime_error("index out of range");
ESM::ENAMstruct effect = effectsList[subRowIndex];
ESM::ENAMstruct effect = effectsList[subRowIndex].mData;
switch (subColIndex)
{
case 0:
@ -374,12 +377,12 @@ namespace CSMWorld
{
ESXRecordT magic = record.get();
std::vector<ESM::ENAMstruct>& effectsList = magic.mEffects.mList;
std::vector<ESM::IndexedENAMstruct>& effectsList = magic.mEffects.mList;
if (subRowIndex < 0 || subRowIndex >= static_cast<int>(effectsList.size()))
throw std::runtime_error("index out of range");
ESM::ENAMstruct effect = effectsList[subRowIndex];
ESM::ENAMstruct effect = effectsList[subRowIndex].mData;
switch (subColIndex)
{
case 0:
@ -438,7 +441,7 @@ namespace CSMWorld
throw std::runtime_error("Magic Effects subcolumn index out of range");
}
magic.mEffects.mList[subRowIndex] = effect;
magic.mEffects.mList[subRowIndex].mData = effect;
record.setModified(magic);
}

@ -265,7 +265,7 @@ namespace MWBase
virtual bool isReadyToBlock(const MWWorld::Ptr& ptr) const = 0;
virtual bool isAttackingOrSpell(const MWWorld::Ptr& ptr) const = 0;
virtual void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool manualSpell) = 0;
virtual void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell) = 0;
virtual void processChangedSettings(const std::set<std::pair<std::string, std::string>>& settings) = 0;

@ -461,7 +461,7 @@ namespace MWBase
*/
virtual MWWorld::SpellCastState startSpellCast(const MWWorld::Ptr& actor) = 0;
virtual void castSpell(const MWWorld::Ptr& actor, bool manualSpell = false) = 0;
virtual void castSpell(const MWWorld::Ptr& actor, bool scriptedSpell = false) = 0;
virtual void launchMagicBolt(const ESM::RefId& spellId, const MWWorld::Ptr& caster,
const osg::Vec3f& fallbackDirection, ESM::RefNum item)

@ -136,7 +136,7 @@ namespace MWClass
const ESM::MagicEffect* effect = store.get<ESM::MagicEffect>().find(ESM::MagicEffect::Telekinesis);
animation->addSpellCastGlow(
effect, 1); // 1 second glow to match the time taken for a door opening or closing
effect->getColor(), 1); // 1 second glow to match the time taken for a door opening or closing
}
}

@ -273,7 +273,7 @@ namespace MWGui
void EnchantingDialog::notifyEffectsChanged()
{
mEffectList.mList = mEffects;
mEffectList.populate(mEffects);
mEnchanting.setEffect(mEffectList);
updateLabels();
}

@ -427,7 +427,7 @@ namespace MWGui
{
// use the icon of the first effect
const ESM::MagicEffect* effect = MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(
spell->mEffects.mList.front().mEffectID);
spell->mEffects.mList.front().mData.mEffectID);
std::string icon = effect->mIcon;
std::replace(icon.begin(), icon.end(), '/', '\\');
size_t slashPos = icon.rfind('\\');

@ -299,7 +299,8 @@ namespace MWGui
mSelected->button->setUserString("Spell", spellId.serialize());
// use the icon of the first effect
const ESM::MagicEffect* effect = esmStore.get<ESM::MagicEffect>().find(spell->mEffects.mList.front().mEffectID);
const ESM::MagicEffect* effect
= esmStore.get<ESM::MagicEffect>().find(spell->mEffects.mList.front().mData.mEffectID);
std::string path = effect->mIcon;
std::replace(path.begin(), path.end(), '/', '\\');

@ -470,9 +470,7 @@ namespace MWGui
y *= 1.5;
}
ESM::EffectList effectList;
effectList.mList = mEffects;
mSpell.mEffects = std::move(effectList);
mSpell.mEffects.populate(mEffects);
mSpell.mData.mCost = int(y);
mSpell.mData.mType = ESM::Spell::ST_Spell;
mSpell.mData.mFlags = 0;
@ -528,10 +526,11 @@ namespace MWGui
if (spell->mData.mType != ESM::Spell::ST_Spell)
continue;
for (const ESM::ENAMstruct& effectInfo : spell->mEffects.mList)
for (const ESM::IndexedENAMstruct& effectInfo : spell->mEffects.mList)
{
int16_t effectId = effectInfo.mData.mEffectID;
const ESM::MagicEffect* effect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(effectInfo.mEffectID);
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(effectId);
// skip effects that do not allow spellmaking/enchanting
int requiredFlags
@ -539,8 +538,8 @@ namespace MWGui
if (!(effect->mData.mFlags & requiredFlags))
continue;
if (std::find(knownEffects.begin(), knownEffects.end(), effectInfo.mEffectID) == knownEffects.end())
knownEffects.push_back(effectInfo.mEffectID);
if (std::find(knownEffects.begin(), knownEffects.end(), effectId) == knownEffects.end())
knownEffects.push_back(effectId);
}
}

@ -48,14 +48,14 @@ namespace MWGui
for (const auto& effect : effects.mList)
{
short effectId = effect.mEffectID;
short effectId = effect.mData.mEffectID;
if (effectId != -1)
{
const ESM::MagicEffect* magicEffect = store.get<ESM::MagicEffect>().find(effectId);
const ESM::Attribute* attribute
= store.get<ESM::Attribute>().search(ESM::Attribute::indexToRefId(effect.mAttribute));
const ESM::Skill* skill = store.get<ESM::Skill>().search(ESM::Skill::indexToRefId(effect.mSkill));
= store.get<ESM::Attribute>().search(ESM::Attribute::indexToRefId(effect.mData.mAttribute));
const ESM::Skill* skill = store.get<ESM::Skill>().search(ESM::Skill::indexToRefId(effect.mData.mSkill));
std::string fullEffectName = MWMechanics::getMagicEffectString(*magicEffect, attribute, skill);
std::string convert = Utf8Stream::lowerCaseUtf8(fullEffectName);

@ -222,17 +222,17 @@ namespace MWGui
= store->get<ESM::Spell>().find(ESM::RefId::deserialize(focus->getUserString("Spell")));
info.caption = spell->mName;
Widgets::SpellEffectList effects;
for (const ESM::ENAMstruct& spellEffect : spell->mEffects.mList)
for (const ESM::IndexedENAMstruct& spellEffect : spell->mEffects.mList)
{
Widgets::SpellEffectParams params;
params.mEffectID = spellEffect.mEffectID;
params.mSkill = ESM::Skill::indexToRefId(spellEffect.mSkill);
params.mAttribute = ESM::Attribute::indexToRefId(spellEffect.mAttribute);
params.mDuration = spellEffect.mDuration;
params.mMagnMin = spellEffect.mMagnMin;
params.mMagnMax = spellEffect.mMagnMax;
params.mRange = spellEffect.mRange;
params.mArea = spellEffect.mArea;
params.mEffectID = spellEffect.mData.mEffectID;
params.mSkill = ESM::Skill::indexToRefId(spellEffect.mData.mSkill);
params.mAttribute = ESM::Attribute::indexToRefId(spellEffect.mData.mAttribute);
params.mDuration = spellEffect.mData.mDuration;
params.mMagnMin = spellEffect.mData.mMagnMin;
params.mMagnMax = spellEffect.mData.mMagnMax;
params.mRange = spellEffect.mData.mRange;
params.mArea = spellEffect.mData.mArea;
params.mIsConstant = (spell->mData.mType == ESM::Spell::ST_Ability);
params.mNoTarget = false;
effects.push_back(params);

@ -195,18 +195,18 @@ namespace MWGui::Widgets
const ESM::Spell* spell = store.get<ESM::Spell>().search(mId);
MYGUI_ASSERT(spell, "spell with id '" << mId << "' not found");
for (const ESM::ENAMstruct& effectInfo : spell->mEffects.mList)
for (const ESM::IndexedENAMstruct& effectInfo : spell->mEffects.mList)
{
MWSpellEffectPtr effect
= creator->createWidget<MWSpellEffect>("MW_EffectImage", coord, MyGUI::Align::Default);
SpellEffectParams params;
params.mEffectID = effectInfo.mEffectID;
params.mSkill = ESM::Skill::indexToRefId(effectInfo.mSkill);
params.mAttribute = ESM::Attribute::indexToRefId(effectInfo.mAttribute);
params.mDuration = effectInfo.mDuration;
params.mMagnMin = effectInfo.mMagnMin;
params.mMagnMax = effectInfo.mMagnMax;
params.mRange = effectInfo.mRange;
params.mEffectID = effectInfo.mData.mEffectID;
params.mSkill = ESM::Skill::indexToRefId(effectInfo.mData.mSkill);
params.mAttribute = ESM::Attribute::indexToRefId(effectInfo.mData.mAttribute);
params.mDuration = effectInfo.mData.mDuration;
params.mMagnMin = effectInfo.mData.mMagnMin;
params.mMagnMax = effectInfo.mData.mMagnMax;
params.mRange = effectInfo.mData.mRange;
params.mIsConstant = (flags & MWEffectList::EF_Constant) != 0;
params.mNoTarget = (flags & MWEffectList::EF_NoTarget);
params.mNoMagnitude = (flags & MWEffectList::EF_NoMagnitude);
@ -308,17 +308,17 @@ namespace MWGui::Widgets
SpellEffectList MWEffectList::effectListFromESM(const ESM::EffectList* effects)
{
SpellEffectList result;
for (const ESM::ENAMstruct& effectInfo : effects->mList)
for (const ESM::IndexedENAMstruct& effectInfo : effects->mList)
{
SpellEffectParams params;
params.mEffectID = effectInfo.mEffectID;
params.mSkill = ESM::Skill::indexToRefId(effectInfo.mSkill);
params.mAttribute = ESM::Attribute::indexToRefId(effectInfo.mAttribute);
params.mDuration = effectInfo.mDuration;
params.mMagnMin = effectInfo.mMagnMin;
params.mMagnMax = effectInfo.mMagnMax;
params.mRange = effectInfo.mRange;
params.mArea = effectInfo.mArea;
params.mEffectID = effectInfo.mData.mEffectID;
params.mSkill = ESM::Skill::indexToRefId(effectInfo.mData.mSkill);
params.mAttribute = ESM::Attribute::indexToRefId(effectInfo.mData.mAttribute);
params.mDuration = effectInfo.mData.mDuration;
params.mMagnMin = effectInfo.mData.mMagnMin;
params.mMagnMax = effectInfo.mData.mMagnMax;
params.mRange = effectInfo.mData.mRange;
params.mArea = effectInfo.mData.mArea;
result.push_back(params);
}
return result;

@ -13,12 +13,14 @@
#include <components/lua/luastate.hpp>
#include <components/misc/color.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/misc/strings/format.hpp>
#include <components/resource/resourcesystem.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp"
#include "../mwbase/world.hpp"
#include "../mwmechanics/activespells.hpp"
#include "../mwmechanics/actorutil.hpp"
#include "../mwmechanics/creaturestats.hpp"
#include "../mwmechanics/magiceffects.hpp"
#include "../mwmechanics/spellutil.hpp"
@ -141,7 +143,7 @@ namespace sol
{
};
template <>
struct is_automagical<ESM::ENAMstruct> : std::false_type
struct is_automagical<ESM::IndexedENAMstruct> : std::false_type
{
};
template <>
@ -189,6 +191,26 @@ namespace MWLua
return ESM::RefId::deserializeText(LuaUtil::cast<std::string_view>(recordOrId));
}
static const ESM::Spell* toSpell(const sol::object& spellOrId)
{
if (spellOrId.is<ESM::Spell>())
return spellOrId.as<const ESM::Spell*>();
else
{
auto& store = MWBase::Environment::get().getWorld()->getStore();
auto refId = ESM::RefId::deserializeText(LuaUtil::cast<std::string_view>(spellOrId));
return store.get<ESM::Spell>().find(refId);
}
}
static sol::table effectParamsListToTable(sol::state_view& lua, const std::vector<ESM::IndexedENAMstruct>& effects)
{
sol::table res(lua, sol::create);
for (size_t i = 0; i < effects.size(); ++i)
res[i + 1] = effects[i]; // ESM::IndexedENAMstruct (effect params)
return res;
}
sol::table initCoreMagicBindings(const Context& context)
{
sol::state_view& lua = context.mLua->sol();
@ -281,12 +303,12 @@ namespace MWLua
spellT["name"] = sol::readonly_property([](const ESM::Spell& rec) -> std::string_view { return rec.mName; });
spellT["type"] = sol::readonly_property([](const ESM::Spell& rec) -> int { return rec.mData.mType; });
spellT["cost"] = sol::readonly_property([](const ESM::Spell& rec) -> int { return rec.mData.mCost; });
spellT["effects"] = sol::readonly_property([&lua](const ESM::Spell& rec) -> sol::table {
sol::table res(lua, sol::create);
for (size_t i = 0; i < rec.mEffects.mList.size(); ++i)
res[i + 1] = rec.mEffects.mList[i]; // ESM::ENAMstruct (effect params)
return res;
});
spellT["alwaysSucceedFlag"] = sol::readonly_property(
[](const ESM::Spell& rec) -> bool { return !!(rec.mData.mFlags & ESM::Spell::F_Always); });
spellT["autocalcFlag"] = sol::readonly_property(
[](const ESM::Spell& rec) -> bool { return !!(rec.mData.mFlags & ESM::Spell::F_Autocalc); });
spellT["effects"] = sol::readonly_property(
[&lua](const ESM::Spell& rec) -> sol::table { return effectParamsListToTable(lua, rec.mEffects.mList); });
// Enchantment record
auto enchantT = lua.new_usertype<ESM::Enchantment>("ESM3_Enchantment");
@ -301,46 +323,49 @@ namespace MWLua
enchantT["charge"]
= sol::readonly_property([](const ESM::Enchantment& rec) -> int { return rec.mData.mCharge; });
enchantT["effects"] = sol::readonly_property([&lua](const ESM::Enchantment& rec) -> sol::table {
sol::table res(lua, sol::create);
for (size_t i = 0; i < rec.mEffects.mList.size(); ++i)
res[i + 1] = rec.mEffects.mList[i]; // ESM::ENAMstruct (effect params)
return res;
return effectParamsListToTable(lua, rec.mEffects.mList);
});
// Effect params
auto effectParamsT = lua.new_usertype<ESM::ENAMstruct>("ESM3_EffectParams");
effectParamsT[sol::meta_function::to_string] = [magicEffectStore](const ESM::ENAMstruct& params) {
const ESM::MagicEffect* const rec = magicEffectStore->find(params.mEffectID);
auto effectParamsT = lua.new_usertype<ESM::IndexedENAMstruct>("ESM3_EffectParams");
effectParamsT[sol::meta_function::to_string] = [magicEffectStore](const ESM::IndexedENAMstruct& params) {
const ESM::MagicEffect* const rec = magicEffectStore->find(params.mData.mEffectID);
return "ESM3_EffectParams[" + ESM::MagicEffect::indexToGmstString(rec->mIndex) + "]";
};
effectParamsT["effect"]
= sol::readonly_property([magicEffectStore](const ESM::ENAMstruct& params) -> const ESM::MagicEffect* {
return magicEffectStore->find(params.mEffectID);
effectParamsT["effect"] = sol::readonly_property(
[magicEffectStore](const ESM::IndexedENAMstruct& params) -> const ESM::MagicEffect* {
return magicEffectStore->find(params.mData.mEffectID);
});
effectParamsT["id"] = sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> std::string {
auto name = ESM::MagicEffect::indexToName(params.mData.mEffectID);
return Misc::StringUtils::lowerCase(name);
});
effectParamsT["affectedSkill"]
= sol::readonly_property([](const ESM::ENAMstruct& params) -> sol::optional<std::string> {
ESM::RefId id = ESM::Skill::indexToRefId(params.mSkill);
= sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> sol::optional<std::string> {
ESM::RefId id = ESM::Skill::indexToRefId(params.mData.mSkill);
if (!id.empty())
return id.serializeText();
return sol::nullopt;
});
effectParamsT["affectedAttribute"]
= sol::readonly_property([](const ESM::ENAMstruct& params) -> sol::optional<std::string> {
ESM::RefId id = ESM::Attribute::indexToRefId(params.mAttribute);
= sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> sol::optional<std::string> {
ESM::RefId id = ESM::Attribute::indexToRefId(params.mData.mAttribute);
if (!id.empty())
return id.serializeText();
return sol::nullopt;
});
effectParamsT["range"]
= sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mRange; });
= sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mRange; });
effectParamsT["area"]
= sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mArea; });
= sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mArea; });
effectParamsT["magnitudeMin"]
= sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mMagnMin; });
= sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mMagnMin; });
effectParamsT["magnitudeMax"]
= sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mMagnMax; });
effectParamsT["duration"]
= sol::readonly_property([](const ESM::ENAMstruct& params) -> int { return params.mDuration; });
= sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mMagnMax; });
effectParamsT["duration"] = sol::readonly_property(
[](const ESM::IndexedENAMstruct& params) -> int { return params.mData.mDuration; });
effectParamsT["index"]
= sol::readonly_property([](const ESM::IndexedENAMstruct& params) -> int { return params.mIndex; });
// MagicEffect record
auto magicEffectT = context.mLua->sol().new_usertype<ESM::MagicEffect>("ESM3_MagicEffect");
@ -361,12 +386,22 @@ namespace MWLua
magicEffectT["continuousVfx"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> bool {
return (rec.mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0;
});
magicEffectT["castingStatic"] = sol::readonly_property(
magicEffectT["areaSound"] = sol::readonly_property(
[](const ESM::MagicEffect& rec) -> std::string { return rec.mAreaSound.serializeText(); });
magicEffectT["boltSound"] = sol::readonly_property(
[](const ESM::MagicEffect& rec) -> std::string { return rec.mBoltSound.serializeText(); });
magicEffectT["castSound"] = sol::readonly_property(
[](const ESM::MagicEffect& rec) -> std::string { return rec.mCastSound.serializeText(); });
magicEffectT["hitSound"] = sol::readonly_property(
[](const ESM::MagicEffect& rec) -> std::string { return rec.mHitSound.serializeText(); });
magicEffectT["areaStatic"] = sol::readonly_property(
[](const ESM::MagicEffect& rec) -> std::string { return rec.mArea.serializeText(); });
magicEffectT["boltStatic"] = sol::readonly_property(
[](const ESM::MagicEffect& rec) -> std::string { return rec.mBolt.serializeText(); });
magicEffectT["castStatic"] = sol::readonly_property(
[](const ESM::MagicEffect& rec) -> std::string { return rec.mCasting.serializeText(); });
magicEffectT["hitStatic"] = sol::readonly_property(
[](const ESM::MagicEffect& rec) -> std::string { return rec.mHit.serializeText(); });
magicEffectT["areaStatic"] = sol::readonly_property(
[](const ESM::MagicEffect& rec) -> std::string { return rec.mArea.serializeText(); });
magicEffectT["name"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> std::string_view {
return MWBase::Environment::get()
.getWorld()
@ -382,8 +417,20 @@ namespace MWLua
magicEffectT["color"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> Misc::Color {
return Misc::Color(rec.mData.mRed / 255.f, rec.mData.mGreen / 255.f, rec.mData.mBlue / 255.f, 1.f);
});
magicEffectT["hasDuration"] = sol::readonly_property(
[](const ESM::MagicEffect& rec) -> bool { return !(rec.mData.mFlags & ESM::MagicEffect::NoDuration); });
magicEffectT["hasMagnitude"] = sol::readonly_property(
[](const ESM::MagicEffect& rec) -> bool { return !(rec.mData.mFlags & ESM::MagicEffect::NoMagnitude); });
// TODO: Not self-explanatory. Needs either a better name or documentation. The description in
// loadmgef.hpp is uninformative.
magicEffectT["isAppliedOnce"] = sol::readonly_property(
[](const ESM::MagicEffect& rec) -> bool { return rec.mData.mFlags & ESM::MagicEffect::AppliedOnce; });
magicEffectT["harmful"] = sol::readonly_property(
[](const ESM::MagicEffect& rec) -> bool { return rec.mData.mFlags & ESM::MagicEffect::Harmful; });
magicEffectT["casterLinked"] = sol::readonly_property(
[](const ESM::MagicEffect& rec) -> bool { return rec.mData.mFlags & ESM::MagicEffect::CasterLinked; });
magicEffectT["nonRecastable"] = sol::readonly_property(
[](const ESM::MagicEffect& rec) -> bool { return rec.mData.mFlags & ESM::MagicEffect::NonRecastable; });
// TODO: Should we expose it? What happens if a spell has several effects with different projectileSpeed?
// magicEffectT["projectileSpeed"]
@ -397,6 +444,8 @@ namespace MWLua
auto name = ESM::MagicEffect::indexToName(effect.mEffectId);
return Misc::StringUtils::lowerCase(name);
});
activeSpellEffectT["index"]
= sol::readonly_property([](const ESM::ActiveEffect& effect) -> int { return effect.mEffectIndex; });
activeSpellEffectT["name"] = sol::readonly_property([](const ESM::ActiveEffect& effect) -> std::string {
return MWMechanics::EffectKey(effect.mEffectId, effect.getSkillOrAttribute()).toString();
});
@ -460,12 +509,13 @@ namespace MWLua
auto activeSpellT = context.mLua->sol().new_usertype<ActiveSpell>("ActiveSpellParams");
activeSpellT[sol::meta_function::to_string] = [](const ActiveSpell& activeSpell) {
return "ActiveSpellParams[" + activeSpell.mParams.getId().serializeText() + "]";
return "ActiveSpellParams[" + activeSpell.mParams.getSourceSpellId().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["id"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> std::string {
return activeSpell.mParams.getSourceSpellId().serializeText();
});
activeSpellT["item"] = sol::readonly_property([&lua](const ActiveSpell& activeSpell) -> sol::object {
auto item = activeSpell.mParams.getItem();
if (!item.isSet())
@ -502,6 +552,21 @@ namespace MWLua
}
return res;
});
activeSpellT["fromEquipment"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> bool {
return activeSpell.mParams.hasFlag(ESM::ActiveSpells::Flag_Equipment);
});
activeSpellT["temporary"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> bool {
return activeSpell.mParams.hasFlag(ESM::ActiveSpells::Flag_Temporary);
});
activeSpellT["affectsBaseValues"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> bool {
return activeSpell.mParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues);
});
activeSpellT["stackable"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> bool {
return activeSpell.mParams.hasFlag(ESM::ActiveSpells::Flag_Stackable);
});
activeSpellT["activeSpellId"] = sol::readonly_property([](const ActiveSpell& activeSpell) -> std::string {
return activeSpell.mParams.getActiveSpellId().serializeText();
});
auto activeEffectT = context.mLua->sol().new_usertype<ActiveEffect>("ActiveEffect");
@ -540,6 +605,78 @@ namespace MWLua
return LuaUtil::makeReadOnly(magicApi);
}
static std::pair<std::string_view, std::vector<ESM::IndexedENAMstruct>> getNameAndMagicEffects(
const MWWorld::Ptr& actor, ESM::RefId id, const sol::table& effects, bool quiet)
{
std::vector<int32_t> effectIndexes;
for (const auto& entry : effects)
{
if (entry.second.is<int32_t>())
effectIndexes.push_back(entry.second.as<int32_t>());
else if (entry.second.is<ESM::IndexedENAMstruct>())
throw std::runtime_error("Error: Adding effects as enam structs is not implemented, use indexes.");
else
throw std::runtime_error("Unexpected entry in 'effects' table while trying to add to active effects");
}
const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore();
auto getEffectsFromIndexes = [&](const ESM::EffectList& effects) {
std::vector<ESM::IndexedENAMstruct> enams;
for (auto index : effectIndexes)
enams.push_back(effects.mList.at(index));
return enams;
};
auto getNameAndEffects = [&](auto* record) {
return std::pair<std::string_view, std::vector<ESM::IndexedENAMstruct>>(
record->mName, getEffectsFromIndexes(record->mEffects));
};
auto getNameAndEffectsEnch = [&](auto* record) {
auto* enchantment = esmStore.get<ESM::Enchantment>().find(record->mEnchant);
return std::pair<std::string_view, std::vector<ESM::IndexedENAMstruct>>(
record->mName, getEffectsFromIndexes(enchantment->mEffects));
};
switch (esmStore.find(id))
{
case ESM::REC_ALCH:
return getNameAndEffects(esmStore.get<ESM::Potion>().find(id));
case ESM::REC_INGR:
{
// Ingredients are a special case as their effect list is calculated on consumption.
const ESM::Ingredient* ingredient = esmStore.get<ESM::Ingredient>().find(id);
std::vector<ESM::IndexedENAMstruct> enams;
quiet = quiet || actor != MWMechanics::getPlayer();
for (uint32_t i = 0; i < effectIndexes.size(); i++)
{
if (auto effect = MWMechanics::rollIngredientEffect(actor, ingredient, effectIndexes[i]))
enams.push_back(effect->mList[0]);
}
if (enams.empty() && !quiet)
{
// "X has no effect on you"
std::string message = esmStore.get<ESM::GameSetting>().find("sNotifyMessage50")->mValue.getString();
message = Misc::StringUtils::format(message, ingredient->mName);
MWBase::Environment::get().getWindowManager()->messageBox(message);
}
return { ingredient->mName, std::move(enams) };
}
case ESM::REC_ARMO:
return getNameAndEffectsEnch(esmStore.get<ESM::Armor>().find(id));
case ESM::REC_BOOK:
return getNameAndEffectsEnch(esmStore.get<ESM::Book>().find(id));
case ESM::REC_CLOT:
return getNameAndEffectsEnch(esmStore.get<ESM::Clothing>().find(id));
case ESM::REC_WEAP:
return getNameAndEffectsEnch(esmStore.get<ESM::Weapon>().find(id));
default:
// esmStore.find doesn't find REC_SPELs
case ESM::REC_SPEL:
return getNameAndEffects(esmStore.get<ESM::Spell>().find(id));
}
}
void addActorMagicBindings(sol::table& actor, const Context& context)
{
const MWWorld::Store<ESM::Spell>* spellStore
@ -698,6 +835,16 @@ namespace MWLua
});
};
// types.Actor.spells(o):canUsePower()
spellsT["canUsePower"] = [](const ActorSpells& spells, const sol::object& spellOrId) -> bool {
if (spells.mActor.isLObject())
throw std::runtime_error("Local scripts can modify only spells of the actor they are attached to.");
auto* spell = toSpell(spellOrId);
if (auto* store = spells.getStore())
return store->canUsePower(spell);
return false;
};
// pairs(types.Actor.activeSpells(o))
activeSpellsT["__pairs"] = [](sol::this_state ts, ActorActiveSpells& self) {
sol::state_view lua(ts);
@ -705,7 +852,7 @@ namespace MWLua
return sol::as_function([lua, self]() mutable -> std::pair<sol::object, sol::object> {
if (!self.isEnd())
{
auto id = sol::make_object(lua, self.mIterator->getId().serializeText());
auto id = sol::make_object(lua, self.mIterator->getSourceSpellId().serializeText());
auto params = sol::make_object(lua, ActiveSpell{ self.mActor, *self.mIterator });
self.advance();
return { id, params };
@ -729,14 +876,97 @@ namespace MWLua
};
// types.Actor.activeSpells(o):remove(id)
activeSpellsT["remove"] = [](const ActorActiveSpells& spells, const sol::object& spellOrId) {
activeSpellsT["remove"] = [context](const ActorActiveSpells& spells, std::string_view idStr) {
if (spells.isLObject())
throw std::runtime_error("Local scripts can modify effect only on the actor they are attached to.");
auto id = toSpellId(spellOrId);
context.mLuaManager->addAction([spells = spells, id = ESM::RefId::deserializeText(idStr)]() {
if (auto* store = spells.getStore())
{
store->removeEffects(spells.mActor.ptr(), id);
auto it = store->getActiveSpellById(id);
if (it != store->end())
{
if (it->hasFlag(ESM::ActiveSpells::Flag_Temporary))
store->removeEffectsByActiveSpellId(spells.mActor.ptr(), id);
else
throw std::runtime_error("Can only remove temporary effects.");
}
}
});
};
// types.Actor.activeSpells(o):add(id, spellid, effects, options)
activeSpellsT["add"] = [](const ActorActiveSpells& spells, const sol::table& options) {
if (spells.isLObject())
throw std::runtime_error("Local scripts can modify effect only on the actor they are attached to.");
if (auto* store = spells.getStore())
{
ESM::RefId id = ESM::RefId::deserializeText(options.get<std::string_view>("id"));
sol::optional<Object> item = options.get<sol::optional<Object>>("item");
ESM::RefNum itemId;
if (item)
itemId = item->id();
sol::optional<Object> caster = options.get<sol::optional<Object>>("caster");
bool stackable = options.get_or("stackable", false);
bool ignoreReflect = options.get_or("ignoreReflect", false);
bool ignoreSpellAbsorption = options.get_or("ignoreSpellAbsorption", false);
bool ignoreResistances = options.get_or("ignoreResistances", false);
sol::table effects = options.get<sol::table>("effects");
bool quiet = options.get_or("quiet", false);
if (effects.empty())
throw std::runtime_error("Error: Parameter 'effects': cannot be an empty list/table");
const MWWorld::ESMStore& esmStore = *MWBase::Environment::get().getESMStore();
auto [name, enams] = getNameAndMagicEffects(spells.mActor.ptr(), id, effects, quiet);
name = options.get_or<std::string_view>("name", name);
MWWorld::Ptr casterPtr;
if (caster)
casterPtr = caster->ptrOrEmpty();
bool affectsHealth = false;
MWMechanics::ActiveSpells::ActiveSpellParams params(casterPtr, id, name, itemId);
params.setFlag(ESM::ActiveSpells::Flag_Lua);
params.setFlag(ESM::ActiveSpells::Flag_Temporary);
if (stackable)
params.setFlag(ESM::ActiveSpells::Flag_Stackable);
for (auto enam : enams)
{
const ESM::MagicEffect* mgef = esmStore.get<ESM::MagicEffect>().find(enam.mData.mEffectID);
MWMechanics::ActiveSpells::ActiveEffect effect;
effect.mEffectId = enam.mData.mEffectID;
effect.mArg = MWMechanics::EffectKey(enam.mData).mArg;
effect.mMagnitude = 0.f;
effect.mMinMagnitude = enam.mData.mMagnMin;
effect.mMaxMagnitude = enam.mData.mMagnMax;
effect.mEffectIndex = enam.mIndex;
effect.mFlags = ESM::ActiveEffect::Flag_None;
if (ignoreReflect)
effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Reflect;
if (ignoreSpellAbsorption)
effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_SpellAbsorption;
if (ignoreResistances)
effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Resistances;
bool hasDuration = !(mgef->mData.mFlags & ESM::MagicEffect::NoDuration);
effect.mDuration = hasDuration ? static_cast<float>(enam.mData.mDuration) : 1.f;
bool appliedOnce = mgef->mData.mFlags & ESM::MagicEffect::AppliedOnce;
if (!appliedOnce)
effect.mDuration = std::max(1.f, effect.mDuration);
effect.mTimeLeft = effect.mDuration;
params.getEffects().emplace_back(effect);
affectsHealth = affectsHealth || mgef->mData.mFlags & ESM::MagicEffect::Harmful
|| effect.mEffectId == ESM::MagicEffect::RestoreHealth;
}
store->addSpell(params);
if (affectsHealth && casterPtr == MWMechanics::getPlayer())
// If player is attempting to cast a harmful spell on or is healing a living target, show the
// target's HP bar.
// TODO: This should be moved to Lua once the HUD has been dehardcoded
MWBase::Environment::get().getWindowManager()->setEnemy(spells.mActor.ptr());
}
};

@ -47,15 +47,16 @@ namespace MWLua
{
if (rec.mData.mEffectID[i] < 0)
continue;
ESM::ENAMstruct effect;
effect.mEffectID = rec.mData.mEffectID[i];
effect.mSkill = rec.mData.mSkills[i];
effect.mAttribute = rec.mData.mAttributes[i];
effect.mRange = ESM::RT_Self;
effect.mArea = 0;
effect.mDuration = 0;
effect.mMagnMin = 0;
effect.mMagnMax = 0;
ESM::IndexedENAMstruct effect;
effect.mData.mEffectID = rec.mData.mEffectID[i];
effect.mData.mSkill = rec.mData.mSkills[i];
effect.mData.mAttribute = rec.mData.mAttributes[i];
effect.mData.mRange = ESM::RT_Self;
effect.mData.mArea = 0;
effect.mData.mDuration = 0;
effect.mData.mMagnMin = 0;
effect.mData.mMagnMax = 0;
effect.mIndex = i;
res[i + 1] = effect;
}
return res;

@ -46,7 +46,10 @@ namespace
size_t numEffects = effectsTable.size();
potion.mEffects.mList.resize(numEffects);
for (size_t i = 0; i < numEffects; ++i)
potion.mEffects.mList[i] = LuaUtil::cast<ESM::ENAMstruct>(effectsTable[i + 1]);
{
potion.mEffects.mList[i] = LuaUtil::cast<ESM::IndexedENAMstruct>(effectsTable[i + 1]);
}
potion.mEffects.updateIndexes();
}
return potion;
}
@ -83,7 +86,7 @@ namespace MWLua
record["effects"] = sol::readonly_property([context](const ESM::Potion& rec) -> sol::table {
sol::table res(context.mLua->sol(), sol::create);
for (size_t i = 0; i < rec.mEffects.mList.size(); ++i)
res[i + 1] = rec.mEffects.mList[i]; // ESM::ENAMstruct (effect params)
res[i + 1] = rec.mEffects.mList[i]; // ESM::IndexedENAMstruct (effect params)
return res;
});
}

@ -8,6 +8,7 @@
#include <components/misc/strings/algorithm.hpp>
#include <components/esm/generatedrefid.hpp>
#include <components/esm3/loadench.hpp>
#include <components/esm3/loadmgef.hpp>
#include <components/esm3/loadstat.hpp>
@ -49,16 +50,15 @@ namespace
void addEffects(
std::vector<ESM::ActiveEffect>& effects, const ESM::EffectList& list, bool ignoreResistances = false)
{
int currentEffectIndex = 0;
for (const auto& enam : list.mList)
{
ESM::ActiveEffect effect;
effect.mEffectId = enam.mEffectID;
effect.mArg = MWMechanics::EffectKey(enam).mArg;
effect.mEffectId = enam.mData.mEffectID;
effect.mArg = MWMechanics::EffectKey(enam.mData).mArg;
effect.mMagnitude = 0.f;
effect.mMinMagnitude = enam.mMagnMin;
effect.mMaxMagnitude = enam.mMagnMax;
effect.mEffectIndex = currentEffectIndex++;
effect.mMinMagnitude = enam.mData.mMagnMin;
effect.mMaxMagnitude = enam.mData.mMagnMax;
effect.mEffectIndex = enam.mIndex;
effect.mFlags = ESM::ActiveEffect::Flag_None;
if (ignoreResistances)
effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Resistances;
@ -82,12 +82,13 @@ namespace MWMechanics
mActiveSpells.mIterating = false;
}
ActiveSpells::ActiveSpellParams::ActiveSpellParams(const CastSpell& cast, const MWWorld::Ptr& caster)
: mId(cast.mId)
, mDisplayName(cast.mSourceName)
ActiveSpells::ActiveSpellParams::ActiveSpellParams(
const MWWorld::Ptr& caster, const ESM::RefId& id, std::string_view sourceName, ESM::RefNum item)
: mSourceSpellId(id)
, mDisplayName(sourceName)
, mCasterActorId(-1)
, mItem(cast.mItem)
, mType(cast.mType)
, mItem(item)
, mFlags()
, mWorsenings(-1)
{
if (!caster.isEmpty() && caster.getClass().isActor())
@ -96,48 +97,52 @@ namespace MWMechanics
ActiveSpells::ActiveSpellParams::ActiveSpellParams(
const ESM::Spell* spell, const MWWorld::Ptr& actor, bool ignoreResistances)
: mId(spell->mId)
: mSourceSpellId(spell->mId)
, mDisplayName(spell->mName)
, mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId())
, mType(spell->mData.mType == ESM::Spell::ST_Ability ? ESM::ActiveSpells::Type_Ability
: ESM::ActiveSpells::Type_Permanent)
, mFlags()
, mWorsenings(-1)
{
assert(spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power);
setFlag(ESM::ActiveSpells::Flag_SpellStore);
if (spell->mData.mType == ESM::Spell::ST_Ability)
setFlag(ESM::ActiveSpells::Flag_AffectsBaseValues);
addEffects(mEffects, spell->mEffects, ignoreResistances);
}
ActiveSpells::ActiveSpellParams::ActiveSpellParams(
const MWWorld::ConstPtr& item, const ESM::Enchantment* enchantment, const MWWorld::Ptr& actor)
: mId(item.getCellRef().getRefId())
: mSourceSpellId(item.getCellRef().getRefId())
, mDisplayName(item.getClass().getName(item))
, mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId())
, mItem(item.getCellRef().getRefNum())
, mType(ESM::ActiveSpells::Type_Enchantment)
, mFlags()
, mWorsenings(-1)
{
assert(enchantment->mData.mType == ESM::Enchantment::ConstantEffect);
addEffects(mEffects, enchantment->mEffects);
setFlag(ESM::ActiveSpells::Flag_Equipment);
}
ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ESM::ActiveSpells::ActiveSpellParams& params)
: mId(params.mId)
: mActiveSpellId(params.mActiveSpellId)
, mSourceSpellId(params.mSourceSpellId)
, mEffects(params.mEffects)
, mDisplayName(params.mDisplayName)
, mCasterActorId(params.mCasterActorId)
, mItem(params.mItem)
, mType(params.mType)
, mFlags(params.mFlags)
, mWorsenings(params.mWorsenings)
, mNextWorsening({ params.mNextWorsening })
{
}
ActiveSpells::ActiveSpellParams::ActiveSpellParams(const ActiveSpellParams& params, const MWWorld::Ptr& actor)
: mId(params.mId)
: mSourceSpellId(params.mSourceSpellId)
, mDisplayName(params.mDisplayName)
, mCasterActorId(actor.getClass().getCreatureStats(actor).getActorId())
, mItem(params.mItem)
, mType(params.mType)
, mFlags(params.mFlags)
, mWorsenings(-1)
{
}
@ -145,17 +150,23 @@ namespace MWMechanics
ESM::ActiveSpells::ActiveSpellParams ActiveSpells::ActiveSpellParams::toEsm() const
{
ESM::ActiveSpells::ActiveSpellParams params;
params.mId = mId;
params.mActiveSpellId = mActiveSpellId;
params.mSourceSpellId = mSourceSpellId;
params.mEffects = mEffects;
params.mDisplayName = mDisplayName;
params.mCasterActorId = mCasterActorId;
params.mItem = mItem;
params.mType = mType;
params.mFlags = mFlags;
params.mWorsenings = mWorsenings;
params.mNextWorsening = mNextWorsening.toEsm();
return params;
}
void ActiveSpells::ActiveSpellParams::setFlag(ESM::ActiveSpells::Flags flag)
{
mFlags = static_cast<ESM::ActiveSpells::Flags>(mFlags | flag);
}
void ActiveSpells::ActiveSpellParams::worsen()
{
++mWorsenings;
@ -178,21 +189,31 @@ namespace MWMechanics
{
// Enchantment id is not stored directly. Instead the enchanted item is stored.
const auto& store = MWBase::Environment::get().getESMStore();
switch (store->find(mId))
switch (store->find(mSourceSpellId))
{
case ESM::REC_ARMO:
return store->get<ESM::Armor>().find(mId)->mEnchant;
return store->get<ESM::Armor>().find(mSourceSpellId)->mEnchant;
case ESM::REC_BOOK:
return store->get<ESM::Book>().find(mId)->mEnchant;
return store->get<ESM::Book>().find(mSourceSpellId)->mEnchant;
case ESM::REC_CLOT:
return store->get<ESM::Clothing>().find(mId)->mEnchant;
return store->get<ESM::Clothing>().find(mSourceSpellId)->mEnchant;
case ESM::REC_WEAP:
return store->get<ESM::Weapon>().find(mId)->mEnchant;
return store->get<ESM::Weapon>().find(mSourceSpellId)->mEnchant;
default:
return {};
}
}
const ESM::Spell* ActiveSpells::ActiveSpellParams::getSpell() const
{
return MWBase::Environment::get().getESMStore()->get<ESM::Spell>().search(getSourceSpellId());
}
bool ActiveSpells::ActiveSpellParams::hasFlag(ESM::ActiveSpells::Flags flags) const
{
return static_cast<ESM::ActiveSpells::Flags>(mFlags & flags) == flags;
}
void ActiveSpells::update(const MWWorld::Ptr& ptr, float duration)
{
if (mIterating)
@ -203,8 +224,7 @@ namespace MWMechanics
// Erase no longer active spells and effects
for (auto spellIt = mSpells.begin(); spellIt != mSpells.end();)
{
if (spellIt->mType != ESM::ActiveSpells::Type_Temporary
&& spellIt->mType != ESM::ActiveSpells::Type_Consumable)
if (!spellIt->hasFlag(ESM::ActiveSpells::Flag_Temporary))
{
++spellIt;
continue;
@ -244,7 +264,10 @@ namespace MWMechanics
{
if (spell->mData.mType != ESM::Spell::ST_Spell && spell->mData.mType != ESM::Spell::ST_Power
&& !isSpellActive(spell->mId))
{
mSpells.emplace_back(ActiveSpellParams{ spell, ptr });
mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId());
}
}
bool updateSpellWindow = false;
@ -270,8 +293,8 @@ namespace MWMechanics
if (std::find_if(mSpells.begin(), mSpells.end(),
[&](const ActiveSpellParams& params) {
return params.mItem == slot->getCellRef().getRefNum()
&& params.mType == ESM::ActiveSpells::Type_Enchantment
&& params.mId == slot->getCellRef().getRefId();
&& params.hasFlag(ESM::ActiveSpells::Flag_Equipment)
&& params.mSourceSpellId == slot->getCellRef().getRefId();
})
!= mSpells.end())
continue;
@ -279,8 +302,8 @@ namespace MWMechanics
// invisibility manually
purgeEffect(ptr, ESM::MagicEffect::Invisibility);
applyPurges(ptr);
const ActiveSpellParams& params
= mSpells.emplace_back(ActiveSpellParams{ *slot, enchantment, ptr });
ActiveSpellParams& params = mSpells.emplace_back(ActiveSpellParams{ *slot, enchantment, ptr });
params.setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId());
for (const auto& effect : params.mEffects)
MWMechanics::playEffects(
ptr, *world->getStore().get<ESM::MagicEffect>().find(effect.mEffectId), playNonLooping);
@ -350,12 +373,11 @@ namespace MWMechanics
continue;
bool remove = false;
if (spellIt->mType == ESM::ActiveSpells::Type_Ability
|| spellIt->mType == ESM::ActiveSpells::Type_Permanent)
if (spellIt->hasFlag(ESM::ActiveSpells::Flag_SpellStore))
{
try
{
remove = !spells.hasSpell(spellIt->mId);
remove = !spells.hasSpell(spellIt->mSourceSpellId);
}
catch (const std::runtime_error& e)
{
@ -363,9 +385,9 @@ namespace MWMechanics
Log(Debug::Error) << "Removing active effect: " << e.what();
}
}
else if (spellIt->mType == ESM::ActiveSpells::Type_Enchantment)
else if (spellIt->hasFlag(ESM::ActiveSpells::Flag_Equipment))
{
// Remove constant effect enchantments that have been unequipped
// Remove effects tied to equipment that has been unequipped
const auto& store = ptr.getClass().getInventoryStore(ptr);
remove = true;
for (int slotIndex = 0; slotIndex < MWWorld::InventoryStore::Slots; slotIndex++)
@ -411,11 +433,11 @@ namespace MWMechanics
void ActiveSpells::addToSpells(const MWWorld::Ptr& ptr, const ActiveSpellParams& spell)
{
if (spell.mType != ESM::ActiveSpells::Type_Consumable)
if (!spell.hasFlag(ESM::ActiveSpells::Flag_Stackable))
{
auto found = std::find_if(mSpells.begin(), mSpells.end(), [&](const auto& existing) {
return spell.mId == existing.mId && spell.mCasterActorId == existing.mCasterActorId
&& spell.mItem == existing.mItem;
return spell.mSourceSpellId == existing.mSourceSpellId
&& spell.mCasterActorId == existing.mCasterActorId && spell.mItem == existing.mItem;
});
if (found != mSpells.end())
{
@ -428,6 +450,7 @@ namespace MWMechanics
}
}
mSpells.emplace_back(spell);
mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId());
}
ActiveSpells::ActiveSpells()
@ -445,10 +468,19 @@ namespace MWMechanics
return mSpells.end();
}
ActiveSpells::TIterator ActiveSpells::getActiveSpellById(const ESM::RefId& id)
{
for (TIterator it = begin(); it != end(); it++)
if (it->getActiveSpellId() == id)
return it;
return end();
}
bool ActiveSpells::isSpellActive(const ESM::RefId& id) const
{
return std::find_if(mSpells.begin(), mSpells.end(), [&](const auto& spell) { return spell.mId == id; })
!= mSpells.end();
return std::find_if(mSpells.begin(), mSpells.end(), [&](const auto& spell) {
return spell.mSourceSpellId == id;
}) != mSpells.end();
}
bool ActiveSpells::isEnchantmentActive(const ESM::RefId& id) const
@ -557,9 +589,14 @@ namespace MWMechanics
return removedCurrentSpell;
}
void ActiveSpells::removeEffects(const MWWorld::Ptr& ptr, const ESM::RefId& id)
void ActiveSpells::removeEffectsBySourceSpellId(const MWWorld::Ptr& ptr, const ESM::RefId& id)
{
purge([=](const ActiveSpellParams& params) { return params.mSourceSpellId == id; }, ptr);
}
void ActiveSpells::removeEffectsByActiveSpellId(const MWWorld::Ptr& ptr, const ESM::RefId& id)
{
purge([=](const ActiveSpellParams& params) { return params.mId == id; }, ptr);
purge([=](const ActiveSpellParams& params) { return params.mActiveSpellId == id; }, ptr);
}
void ActiveSpells::purgeEffect(const MWWorld::Ptr& ptr, int effectId, ESM::RefId effectArg)
@ -604,19 +641,19 @@ namespace MWMechanics
void ActiveSpells::readState(const ESM::ActiveSpells& state)
{
for (const ESM::ActiveSpells::ActiveSpellParams& spell : state.mSpells)
{
mSpells.emplace_back(ActiveSpellParams{ spell });
// Generate ID for older saves that didn't have any.
if (mSpells.back().getActiveSpellId().empty())
mSpells.back().setActiveSpellId(MWBase::Environment::get().getESMStore()->generateId());
}
for (const ESM::ActiveSpells::ActiveSpellParams& spell : state.mQueue)
mQueue.emplace_back(ActiveSpellParams{ spell });
}
void ActiveSpells::unloadActor(const MWWorld::Ptr& ptr)
{
purge(
[](const auto& spell) {
return spell.getType() == ESM::ActiveSpells::Type_Consumable
|| spell.getType() == ESM::ActiveSpells::Type_Temporary;
},
ptr);
purge([](const auto& spell) { return spell.hasFlag(ESM::ActiveSpells::Flag_Temporary); }, ptr);
mQueue.clear();
}
}

@ -33,12 +33,13 @@ namespace MWMechanics
using ActiveEffect = ESM::ActiveEffect;
class ActiveSpellParams
{
ESM::RefId mId;
ESM::RefId mActiveSpellId;
ESM::RefId mSourceSpellId;
std::vector<ActiveEffect> mEffects;
std::string mDisplayName;
int mCasterActorId;
ESM::RefNum mItem;
ESM::ActiveSpells::EffectType mType;
ESM::ActiveSpells::Flags mFlags;
int mWorsenings;
MWWorld::TimeStamp mNextWorsening;
MWWorld::Ptr mSource;
@ -57,15 +58,17 @@ namespace MWMechanics
friend class ActiveSpells;
public:
ActiveSpellParams(const CastSpell& cast, const MWWorld::Ptr& caster);
ActiveSpellParams(
const MWWorld::Ptr& caster, const ESM::RefId& id, std::string_view sourceName, ESM::RefNum item);
ESM::RefId getActiveSpellId() const { return mActiveSpellId; }
void setActiveSpellId(ESM::RefId id) { mActiveSpellId = id; }
const ESM::RefId& getId() const { return mId; }
const ESM::RefId& getSourceSpellId() const { return mSourceSpellId; }
const std::vector<ActiveEffect>& getEffects() const { return mEffects; }
std::vector<ActiveEffect>& getEffects() { return mEffects; }
ESM::ActiveSpells::EffectType getType() const { return mType; }
int getCasterActorId() const { return mCasterActorId; }
int getWorsenings() const { return mWorsenings; }
@ -75,6 +78,10 @@ namespace MWMechanics
ESM::RefNum getItem() const { return mItem; }
ESM::RefId getEnchantment() const;
const ESM::Spell* getSpell() const;
bool hasFlag(ESM::ActiveSpells::Flags flags) const;
void setFlag(ESM::ActiveSpells::Flags flags);
// Increments worsenings count and sets the next timestamp
void worsen();
@ -93,6 +100,8 @@ namespace MWMechanics
TIterator end() const;
TIterator getActiveSpellById(const ESM::RefId& id);
void update(const MWWorld::Ptr& ptr, float duration);
private:
@ -132,7 +141,9 @@ namespace MWMechanics
void addSpell(const ESM::Spell* spell, const MWWorld::Ptr& actor);
/// Removes the active effects from this spell/potion/.. with \a id
void removeEffects(const MWWorld::Ptr& ptr, const ESM::RefId& id);
void removeEffectsBySourceSpellId(const MWWorld::Ptr& ptr, const ESM::RefId& id);
/// Removes the active effects of a specific active spell
void removeEffectsByActiveSpellId(const MWWorld::Ptr& ptr, const ESM::RefId& id);
/// Remove all active effects with this effect id
void purgeEffect(const MWWorld::Ptr& ptr, int effectId, ESM::RefId effectArg = {});

@ -1228,11 +1228,11 @@ namespace MWMechanics
}
}
void Actors::castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool manualSpell) const
void Actors::castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell) const
{
const auto iter = mIndex.find(ptr.mRef);
if (iter != mIndex.end())
iter->second->getCharacterController().castSpell(spellId, manualSpell);
iter->second->getCharacterController().castSpell(spellId, scriptedSpell);
}
bool Actors::isActorDetected(const MWWorld::Ptr& actor, const MWWorld::Ptr& observer) const

@ -67,7 +67,7 @@ namespace MWMechanics
void resurrect(const MWWorld::Ptr& ptr) const;
void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool manualSpell = false) const;
void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell = false) const;
void updateActor(const MWWorld::Ptr& old, const MWWorld::Ptr& ptr) const;
///< Updates an actor with a new Ptr

@ -25,11 +25,11 @@ namespace MWMechanics
}
}
MWMechanics::AiCast::AiCast(const ESM::RefId& targetId, const ESM::RefId& spellId, bool manualSpell)
MWMechanics::AiCast::AiCast(const ESM::RefId& targetId, const ESM::RefId& spellId, bool scriptedSpell)
: mTargetId(targetId)
, mSpellId(spellId)
, mCasting(false)
, mManual(manualSpell)
, mScripted(scriptedSpell)
, mDistance(getInitialDistance(spellId))
{
}
@ -49,7 +49,7 @@ bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::Charac
if (target.isEmpty())
return true;
if (!mManual
if (!mScripted
&& !pathTo(actor, target.getRefData().getPosition().asVec3(), duration,
characterController.getSupportedMovementDirections(), mDistance))
{
@ -85,7 +85,7 @@ bool MWMechanics::AiCast::execute(const MWWorld::Ptr& actor, MWMechanics::Charac
if (!mCasting)
{
MWBase::Environment::get().getMechanicsManager()->castSpell(actor, mSpellId, mManual);
MWBase::Environment::get().getMechanicsManager()->castSpell(actor, mSpellId, mScripted);
mCasting = true;
return false;
}

@ -15,7 +15,7 @@ namespace MWMechanics
class AiCast final : public TypedAiPackage<AiCast>
{
public:
AiCast(const ESM::RefId& targetId, const ESM::RefId& spellId, bool manualSpell = false);
AiCast(const ESM::RefId& targetId, const ESM::RefId& spellId, bool scriptedSpell = false);
bool execute(const MWWorld::Ptr& actor, CharacterController& characterController, AiState& state,
float duration) override;
@ -37,7 +37,7 @@ namespace MWMechanics
const ESM::RefId mTargetId;
const ESM::RefId mSpellId;
bool mCasting;
const bool mManual;
const bool mScripted;
const float mDistance;
};
}

@ -275,7 +275,7 @@ namespace MWMechanics
if (!spellId.empty())
{
const ESM::Spell* spell = MWBase::Environment::get().getESMStore()->get<ESM::Spell>().find(spellId);
if (spell->mEffects.mList.empty() || spell->mEffects.mList[0].mRange != ESM::RT_Target)
if (spell->mEffects.mList.empty() || spell->mEffects.mList[0].mData.mRange != ESM::RT_Target)
canShout = false;
}
storage.startAttackIfReady(actor, characterController, weapon, isRangedCombat, canShout);

@ -355,14 +355,14 @@ namespace MWMechanics
{
const ESM::Spell* spell
= MWBase::Environment::get().getESMStore()->get<ESM::Spell>().find(selectedSpellId);
for (std::vector<ESM::ENAMstruct>::const_iterator effectIt = spell->mEffects.mList.begin();
for (std::vector<ESM::IndexedENAMstruct>::const_iterator effectIt = spell->mEffects.mList.begin();
effectIt != spell->mEffects.mList.end(); ++effectIt)
{
if (effectIt->mRange == ESM::RT_Target)
if (effectIt->mData.mRange == ESM::RT_Target)
{
const ESM::MagicEffect* effect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(
effectIt->mEffectID);
effectIt->mData.mEffectID);
dist = effect->mData.mSpeed;
break;
}
@ -375,14 +375,14 @@ namespace MWMechanics
{
const ESM::Enchantment* ench
= MWBase::Environment::get().getESMStore()->get<ESM::Enchantment>().find(enchId);
for (std::vector<ESM::ENAMstruct>::const_iterator effectIt = ench->mEffects.mList.begin();
for (std::vector<ESM::IndexedENAMstruct>::const_iterator effectIt = ench->mEffects.mList.begin();
effectIt != ench->mEffects.mList.end(); ++effectIt)
{
if (effectIt->mRange == ESM::RT_Target)
if (effectIt->mData.mRange == ESM::RT_Target)
{
const ESM::MagicEffect* effect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(
effectIt->mEffectID);
effectIt->mData.mEffectID);
dist = effect->mData.mSpeed;
break;
}

@ -262,13 +262,13 @@ const ESM::Potion* MWMechanics::Alchemy::getRecord(const ESM::Potion& toFind) co
for (size_t i = 0; i < iter->mEffects.mList.size(); ++i)
{
const ESM::ENAMstruct& first = iter->mEffects.mList[i];
const ESM::IndexedENAMstruct& first = iter->mEffects.mList[i];
const ESM::ENAMstruct& second = mEffects[i];
if (first.mEffectID != second.mEffectID || first.mArea != second.mArea || first.mRange != second.mRange
|| first.mSkill != second.mSkill || first.mAttribute != second.mAttribute
|| first.mMagnMin != second.mMagnMin || first.mMagnMax != second.mMagnMax
|| first.mDuration != second.mDuration)
if (first.mData.mEffectID != second.mEffectID || first.mData.mArea != second.mArea
|| first.mData.mRange != second.mRange || first.mData.mSkill != second.mSkill
|| first.mData.mAttribute != second.mAttribute || first.mData.mMagnMin != second.mMagnMin
|| first.mData.mMagnMax != second.mMagnMax || first.mData.mDuration != second.mDuration)
{
mismatch = true;
break;
@ -324,7 +324,7 @@ void MWMechanics::Alchemy::addPotion(const std::string& name)
newRecord.mModel = "m\\misc_potion_" + std::string(meshes[index]) + "_01.nif";
newRecord.mIcon = "m\\tx_potion_" + std::string(meshes[index]) + "_01.dds";
newRecord.mEffects.mList = mEffects;
newRecord.mEffects.populate(mEffects);
const ESM::Potion* record = getRecord(newRecord);
if (!record)

@ -221,7 +221,7 @@ namespace MWMechanics
for (const auto& spellEffect : spell->mEffects.mList)
{
const ESM::MagicEffect* magicEffect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(spellEffect.mEffectID);
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(spellEffect.mData.mEffectID);
static const int iAutoSpellAttSkillMin = MWBase::Environment::get()
.getESMStore()
->get<ESM::GameSetting>()
@ -230,7 +230,7 @@ namespace MWMechanics
if ((magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill))
{
ESM::RefId skill = ESM::Skill::indexToRefId(spellEffect.mSkill);
ESM::RefId skill = ESM::Skill::indexToRefId(spellEffect.mData.mSkill);
auto found = actorSkills.find(skill);
if (found == actorSkills.end() || found->second.getBase() < iAutoSpellAttSkillMin)
return false;
@ -238,7 +238,7 @@ namespace MWMechanics
if ((magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute))
{
ESM::RefId attribute = ESM::Attribute::indexToRefId(spellEffect.mAttribute);
ESM::RefId attribute = ESM::Attribute::indexToRefId(spellEffect.mData.mAttribute);
auto found = actorAttributes.find(attribute);
if (found == actorAttributes.end() || found->second.getBase() < iAutoSpellAttSkillMin)
return false;
@ -253,22 +253,22 @@ namespace MWMechanics
{
// Morrowind for some reason uses a formula slightly different from magicka cost calculation
float minChance = std::numeric_limits<float>::max();
for (const ESM::ENAMstruct& effect : spell->mEffects.mList)
for (const ESM::IndexedENAMstruct& effect : spell->mEffects.mList)
{
const ESM::MagicEffect* magicEffect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(effect.mEffectID);
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(effect.mData.mEffectID);
int minMagn = 1;
int maxMagn = 1;
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude))
{
minMagn = effect.mMagnMin;
maxMagn = effect.mMagnMax;
minMagn = effect.mData.mMagnMin;
maxMagn = effect.mData.mMagnMax;
}
int duration = 0;
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration))
duration = effect.mDuration;
duration = effect.mData.mDuration;
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce))
duration = std::max(1, duration);
@ -281,10 +281,10 @@ namespace MWMechanics
float x = 0.5 * (std::max(1, minMagn) + std::max(1, maxMagn));
x *= 0.1 * magicEffect->mData.mBaseCost;
x *= 1 + duration;
x += 0.05 * std::max(1, effect.mArea) * magicEffect->mData.mBaseCost;
x += 0.05 * std::max(1, effect.mData.mArea) * magicEffect->mData.mBaseCost;
x *= fEffectCostMult;
if (effect.mRange == ESM::RT_Target)
if (effect.mData.mRange == ESM::RT_Target)
x *= 1.5f;
float s = 0.f;

@ -1155,8 +1155,8 @@ namespace MWMechanics
else if (groupname == "spellcast" && action == mAttackType + " release")
{
if (mCanCast)
MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingManualSpell);
mCastingManualSpell = false;
MWBase::Environment::get().getWorld()->castSpell(mPtr, mCastingScriptedSpell);
mCastingScriptedSpell = false;
mCanCast = false;
}
else if (groupname == "containeropen" && action == "loot")
@ -1526,9 +1526,9 @@ namespace MWMechanics
bool isMagicItem = false;
// Play hand VFX and allow castSpell use (assuming an animation is going to be played) if
// spellcasting is successful. Manual spellcasting bypasses restrictions.
// spellcasting is successful. Scripted spellcasting bypasses restrictions.
MWWorld::SpellCastState spellCastResult = MWWorld::SpellCastState::Success;
if (!mCastingManualSpell)
if (!mCastingScriptedSpell)
spellCastResult = world->startSpellCast(mPtr);
mCanCast = spellCastResult == MWWorld::SpellCastState::Success;
@ -1558,9 +1558,9 @@ namespace MWMechanics
else if (!spellid.empty() && spellCastResult != MWWorld::SpellCastState::PowerAlreadyUsed)
{
world->breakInvisibility(mPtr);
MWMechanics::CastSpell cast(mPtr, {}, false, mCastingManualSpell);
MWMechanics::CastSpell cast(mPtr, {}, false, mCastingScriptedSpell);
const std::vector<ESM::ENAMstruct>* effects{ nullptr };
const std::vector<ESM::IndexedENAMstruct>* effects{ nullptr };
const MWWorld::ESMStore& store = world->getStore();
if (isMagicItem)
{
@ -1579,7 +1579,7 @@ namespace MWMechanics
if (mCanCast)
{
const ESM::MagicEffect* effect = store.get<ESM::MagicEffect>().find(
effects->back().mEffectID); // use last effect of list for color of VFX_Hands
effects->back().mData.mEffectID); // use last effect of list for color of VFX_Hands
const ESM::Static* castStatic
= world->getStore().get<ESM::Static>().find(ESM::RefId::stringRefId("VFX_Hands"));
@ -1593,7 +1593,7 @@ namespace MWMechanics
"", false, "Bip01 R Hand", effect->mParticle);
}
// first effect used for casting animation
const ESM::ENAMstruct& firstEffect = effects->front();
const ESM::ENAMstruct& firstEffect = effects->front().mData;
std::string startKey;
std::string stopKey;
@ -1602,9 +1602,9 @@ namespace MWMechanics
startKey = "start";
stopKey = "stop";
if (mCanCast)
world->castSpell(
mPtr, mCastingManualSpell); // No "release" text key to use, so cast immediately
mCastingManualSpell = false;
world->castSpell(mPtr,
mCastingScriptedSpell); // No "release" text key to use, so cast immediately
mCastingScriptedSpell = false;
mCanCast = false;
}
else
@ -2735,7 +2735,7 @@ namespace MWMechanics
// Make sure we canceled the current attack or spellcasting,
// because we disabled attack animations anyway.
mCanCast = false;
mCastingManualSpell = false;
mCastingScriptedSpell = false;
setAttackingOrSpell(false);
if (mUpperBodyState != UpperBodyState::None)
mUpperBodyState = UpperBodyState::WeaponEquipped;
@ -2887,7 +2887,7 @@ namespace MWMechanics
bool CharacterController::isCastingSpell() const
{
return mCastingManualSpell || mUpperBodyState == UpperBodyState::Casting;
return mCastingScriptedSpell || mUpperBodyState == UpperBodyState::Casting;
}
bool CharacterController::isReadyToBlock() const
@ -2941,10 +2941,10 @@ namespace MWMechanics
mPtr.getClass().getCreatureStats(mPtr).setAttackingOrSpell(attackingOrSpell);
}
void CharacterController::castSpell(const ESM::RefId& spellId, bool manualSpell)
void CharacterController::castSpell(const ESM::RefId& spellId, bool scriptedSpell)
{
setAttackingOrSpell(true);
mCastingManualSpell = manualSpell;
mCastingScriptedSpell = scriptedSpell;
ActionSpell action = ActionSpell(spellId);
action.prepare(mPtr);
}

@ -192,7 +192,7 @@ namespace MWMechanics
bool mCanCast{ false };
bool mCastingManualSpell{ false };
bool mCastingScriptedSpell{ false };
bool mIsMovingBackward{ false };
osg::Vec2f mSmoothedSpeed;
@ -312,7 +312,7 @@ namespace MWMechanics
bool isAttackingOrSpell() const;
void setVisibility(float visibility) const;
void castSpell(const ESM::RefId& spellId, bool manualSpell = false);
void castSpell(const ESM::RefId& spellId, bool scriptedSpell = false);
void setAIAttackType(std::string_view attackType);
static std::string_view getRandomAttackType();

@ -198,13 +198,13 @@ namespace MWMechanics
float enchantmentCost = 0.f;
float cost = 0.f;
for (const ESM::ENAMstruct& effect : mEffectList.mList)
for (const ESM::IndexedENAMstruct& effect : mEffectList.mList)
{
float baseCost = (store.get<ESM::MagicEffect>().find(effect.mEffectID))->mData.mBaseCost;
int magMin = std::max(1, effect.mMagnMin);
int magMax = std::max(1, effect.mMagnMax);
int area = std::max(1, effect.mArea);
float duration = static_cast<float>(effect.mDuration);
float baseCost = (store.get<ESM::MagicEffect>().find(effect.mData.mEffectID))->mData.mBaseCost;
int magMin = std::max(1, effect.mData.mMagnMin);
int magMax = std::max(1, effect.mData.mMagnMax);
int area = std::max(1, effect.mData.mArea);
float duration = static_cast<float>(effect.mData.mDuration);
if (mCastStyle == ESM::Enchantment::ConstantEffect)
duration = fEnchantmentConstantDurationMult;
@ -212,7 +212,7 @@ namespace MWMechanics
cost = std::max(1.f, cost);
if (effect.mRange == ESM::RT_Target)
if (effect.mData.mRange == ESM::RT_Target)
cost *= 1.5f;
enchantmentCost += precise ? cost : std::floor(cost);
@ -244,13 +244,7 @@ namespace MWMechanics
for (int i = 0; i < static_cast<int>(iter->mEffects.mList.size()); ++i)
{
const ESM::ENAMstruct& first = iter->mEffects.mList[i];
const ESM::ENAMstruct& second = toFind.mEffects.mList[i];
if (first.mEffectID != second.mEffectID || first.mArea != second.mArea || first.mRange != second.mRange
|| first.mSkill != second.mSkill || first.mAttribute != second.mAttribute
|| first.mMagnMin != second.mMagnMin || first.mMagnMax != second.mMagnMax
|| first.mDuration != second.mDuration)
if (iter->mEffects.mList[i] != toFind.mEffects.mList[i])
{
mismatch = true;
break;

@ -261,10 +261,10 @@ namespace MWMechanics
mObjects.addObject(ptr);
}
void MechanicsManager::castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool manualSpell)
void MechanicsManager::castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell)
{
if (ptr.getClass().isActor())
mActors.castSpell(ptr, spellId, manualSpell);
mActors.castSpell(ptr, spellId, scriptedSpell);
}
void MechanicsManager::remove(const MWWorld::Ptr& ptr, bool keepActive)
@ -1978,11 +1978,7 @@ namespace MWMechanics
// Transforming removes all temporary effects
actor.getClass().getCreatureStats(actor).getActiveSpells().purge(
[](const auto& params) {
return params.getType() == ESM::ActiveSpells::Type_Consumable
|| params.getType() == ESM::ActiveSpells::Type_Temporary;
},
actor);
[](const auto& params) { return params.hasFlag(ESM::ActiveSpells::Flag_Temporary); }, actor);
mActors.updateActor(actor, 0.f);
if (werewolf)

@ -202,7 +202,7 @@ namespace MWMechanics
/// Is \a ptr casting spell or using weapon now?
bool isAttackingOrSpell(const MWWorld::Ptr& ptr) const override;
void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool manualSpell = false) override;
void castSpell(const MWWorld::Ptr& ptr, const ESM::RefId& spellId, bool scriptedSpell = false) override;
void processChangedSettings(const Settings::CategorySettingVector& settings) override;

@ -29,11 +29,11 @@
namespace MWMechanics
{
CastSpell::CastSpell(
const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile, const bool manualSpell)
const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile, const bool scriptedSpell)
: mCaster(caster)
, mTarget(target)
, mFromProjectile(fromProjectile)
, mManualSpell(manualSpell)
, mScriptedSpell(scriptedSpell)
{
}
@ -41,21 +41,21 @@ namespace MWMechanics
const ESM::EffectList& effects, const MWWorld::Ptr& ignore, ESM::RangeType rangeType) const
{
const auto world = MWBase::Environment::get().getWorld();
std::map<MWWorld::Ptr, std::vector<ESM::ENAMstruct>> toApply;
std::map<MWWorld::Ptr, std::vector<ESM::IndexedENAMstruct>> toApply;
int index = -1;
for (const ESM::ENAMstruct& effectInfo : effects.mList)
for (const ESM::IndexedENAMstruct& effectInfo : effects.mList)
{
++index;
const ESM::MagicEffect* effect = world->getStore().get<ESM::MagicEffect>().find(effectInfo.mEffectID);
const ESM::MagicEffect* effect = world->getStore().get<ESM::MagicEffect>().find(effectInfo.mData.mEffectID);
if (effectInfo.mRange != rangeType
|| (effectInfo.mArea <= 0 && !ignore.isEmpty() && ignore.getClass().isActor()))
if (effectInfo.mData.mRange != rangeType
|| (effectInfo.mData.mArea <= 0 && !ignore.isEmpty() && ignore.getClass().isActor()))
continue; // Not right range type, or not area effect and hit an actor
if (mFromProjectile && effectInfo.mArea <= 0)
if (mFromProjectile && effectInfo.mData.mArea <= 0)
continue; // Don't play explosion for projectiles with 0-area effects
if (!mFromProjectile && effectInfo.mRange == ESM::RT_Touch && !ignore.isEmpty()
if (!mFromProjectile && effectInfo.mData.mRange == ESM::RT_Touch && !ignore.isEmpty()
&& !ignore.getClass().isActor() && !ignore.getClass().hasToolTip(ignore)
&& (mCaster.isEmpty() || mCaster.getClass().isActor()))
continue; // Don't play explosion for touch spells on non-activatable objects except when spell is from
@ -70,16 +70,16 @@ namespace MWMechanics
const std::string& texture = effect->mParticle;
if (effectInfo.mArea <= 0)
if (effectInfo.mData.mArea <= 0)
{
if (effectInfo.mRange == ESM::RT_Target)
if (effectInfo.mData.mRange == ESM::RT_Target)
world->spawnEffect(
Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel), texture, mHitPosition, 1.0f);
continue;
}
else
world->spawnEffect(Misc::ResourceHelpers::correctMeshPath(areaStatic->mModel), texture, mHitPosition,
static_cast<float>(effectInfo.mArea * 2));
static_cast<float>(effectInfo.mData.mArea * 2));
// Play explosion sound (make sure to use NoTrack, since we will delete the projectile now)
{
@ -95,7 +95,7 @@ namespace MWMechanics
std::vector<MWWorld::Ptr> objects;
static const int unitsPerFoot = ceil(Constants::UnitsPerFoot);
MWBase::Environment::get().getMechanicsManager()->getObjectsInRange(
mHitPosition, static_cast<float>(effectInfo.mArea * unitsPerFoot), objects);
mHitPosition, static_cast<float>(effectInfo.mData.mArea * unitsPerFoot), objects);
for (const MWWorld::Ptr& affected : objects)
{
// Ignore actors without collisions here, otherwise it will be possible to hit actors outside processing
@ -104,13 +104,6 @@ namespace MWMechanics
continue;
auto& list = toApply[affected];
while (list.size() < static_cast<std::size_t>(index))
{
// Insert dummy effects to preserve indices
auto& dummy = list.emplace_back(effectInfo);
dummy.mRange = ESM::RT_Self;
assert(dummy.mRange != rangeType);
}
list.push_back(effectInfo);
}
}
@ -151,45 +144,34 @@ namespace MWMechanics
void CastSpell::inflict(
const MWWorld::Ptr& target, const ESM::EffectList& effects, ESM::RangeType range, bool exploded) const
{
bool targetIsDeadActor = false;
const bool targetIsActor = !target.isEmpty() && target.getClass().isActor();
if (targetIsActor)
{
// Early-out for characters that have departed.
const auto& stats = target.getClass().getCreatureStats(target);
if (stats.isDead() && stats.isDeathAnimationFinished())
return;
targetIsDeadActor = true;
}
// If none of the effects need to apply, we can early-out
bool found = false;
bool containsRecastable = false;
std::vector<const ESM::MagicEffect*> magicEffects;
magicEffects.reserve(effects.mList.size());
const auto& store = MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>();
for (const ESM::ENAMstruct& effect : effects.mList)
for (const ESM::IndexedENAMstruct& effect : effects.mList)
{
if (effect.mRange == range)
if (effect.mData.mRange == range)
{
found = true;
const ESM::MagicEffect* magicEffect = store.find(effect.mEffectID);
// caster needs to be an actor for linked effects (e.g. Absorb)
if (magicEffect->mData.mFlags & ESM::MagicEffect::CasterLinked
&& (mCaster.isEmpty() || !mCaster.getClass().isActor()))
{
magicEffects.push_back(nullptr);
continue;
}
const ESM::MagicEffect* magicEffect = store.find(effect.mData.mEffectID);
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NonRecastable))
containsRecastable = true;
magicEffects.push_back(magicEffect);
}
else
magicEffects.push_back(nullptr);
}
if (!found)
return;
ActiveSpells::ActiveSpellParams params(*this, mCaster);
ActiveSpells::ActiveSpellParams params(mCaster, mId, mSourceName, mItem);
params.setFlag(mFlags);
bool castByPlayer = (!mCaster.isEmpty() && mCaster == getPlayer());
const ActiveSpells* targetSpells = nullptr;
@ -204,31 +186,32 @@ namespace MWMechanics
return;
}
for (size_t currentEffectIndex = 0; !target.isEmpty() && currentEffectIndex < effects.mList.size();
++currentEffectIndex)
for (auto& enam : effects.mList)
{
const ESM::ENAMstruct& enam = effects.mList[currentEffectIndex];
if (enam.mRange != range)
if (enam.mData.mRange != range)
continue;
const ESM::MagicEffect* magicEffect = magicEffects[currentEffectIndex];
const ESM::MagicEffect* magicEffect = store.find(enam.mData.mEffectID);
if (!magicEffect)
continue;
// caster needs to be an actor for linked effects (e.g. Absorb)
if (magicEffect->mData.mFlags & ESM::MagicEffect::CasterLinked
&& (mCaster.isEmpty() || !mCaster.getClass().isActor()))
continue;
ActiveSpells::ActiveEffect effect;
effect.mEffectId = enam.mEffectID;
effect.mArg = MWMechanics::EffectKey(enam).mArg;
effect.mEffectId = enam.mData.mEffectID;
effect.mArg = MWMechanics::EffectKey(enam.mData).mArg;
effect.mMagnitude = 0.f;
effect.mMinMagnitude = enam.mMagnMin;
effect.mMaxMagnitude = enam.mMagnMax;
effect.mMinMagnitude = enam.mData.mMagnMin;
effect.mMaxMagnitude = enam.mData.mMagnMax;
effect.mTimeLeft = 0.f;
effect.mEffectIndex = static_cast<int>(currentEffectIndex);
effect.mEffectIndex = enam.mIndex;
effect.mFlags = ESM::ActiveEffect::Flag_None;
if (mManualSpell)
if (mScriptedSpell)
effect.mFlags |= ESM::ActiveEffect::Flag_Ignore_Reflect;
bool hasDuration = !(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration);
effect.mDuration = hasDuration ? static_cast<float>(enam.mDuration) : 1.f;
effect.mDuration = hasDuration ? static_cast<float>(enam.mData.mDuration) : 1.f;
bool appliedOnce = magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce;
if (!appliedOnce)
@ -240,8 +223,8 @@ namespace MWMechanics
params.getEffects().emplace_back(effect);
bool effectAffectsHealth = magicEffect->mData.mFlags & ESM::MagicEffect::Harmful
|| enam.mEffectID == ESM::MagicEffect::RestoreHealth;
if (castByPlayer && target != mCaster && targetIsActor && effectAffectsHealth)
|| enam.mData.mEffectID == ESM::MagicEffect::RestoreHealth;
if (castByPlayer && target != mCaster && targetIsActor && !targetIsDeadActor && effectAffectsHealth)
{
// If player is attempting to cast a harmful spell on or is healing a living target, show the target's
// HP bar.
@ -262,7 +245,10 @@ namespace MWMechanics
if (!params.getEffects().empty())
{
if (targetIsActor)
{
if (!targetIsDeadActor)
target.getClass().getCreatureStats(target).getActiveSpells().addSpell(params);
}
else
{
// Apply effects instantly. We can ignore effect deletion since the entire params object gets
@ -336,7 +322,7 @@ namespace MWMechanics
ESM::RefId school = ESM::Skill::Alteration;
if (!enchantment->mEffects.mList.empty())
{
short effectId = enchantment->mEffects.mList.front().mEffectID;
short effectId = enchantment->mEffects.mList.front().mData.mEffectID;
const ESM::MagicEffect* magicEffect = store->get<ESM::MagicEffect>().find(effectId);
school = magicEffect->mData.mSchool;
}
@ -387,7 +373,8 @@ namespace MWMechanics
{
mSourceName = potion->mName;
mId = potion->mId;
mType = ESM::ActiveSpells::Type_Consumable;
mFlags = static_cast<ESM::ActiveSpells::Flags>(
ESM::ActiveSpells::Flag_Temporary | ESM::ActiveSpells::Flag_Stackable);
inflict(mCaster, potion->mEffects, ESM::RT_Self);
@ -403,7 +390,7 @@ namespace MWMechanics
bool godmode = mCaster == MWMechanics::getPlayer() && MWBase::Environment::get().getWorld()->getGodModeState();
if (mCaster.getClass().isActor() && !mAlwaysSucceed && !mManualSpell)
if (mCaster.getClass().isActor() && !mAlwaysSucceed && !mScriptedSpell)
{
school = getSpellSchool(spell, mCaster);
@ -438,7 +425,7 @@ namespace MWMechanics
stats.getSpells().usePower(spell);
}
if (!mManualSpell && mCaster == getPlayer() && spellIncreasesSkill(spell))
if (!mScriptedSpell && mCaster == getPlayer() && spellIncreasesSkill(spell))
mCaster.getClass().skillUsageSucceeded(mCaster, school, ESM::Skill::Spellcast_Success);
// A non-actor doesn't play its spell cast effects from a character controller, so play them here
@ -458,62 +445,27 @@ namespace MWMechanics
bool CastSpell::cast(const ESM::Ingredient* ingredient)
{
mId = ingredient->mId;
mType = ESM::ActiveSpells::Type_Consumable;
mFlags = static_cast<ESM::ActiveSpells::Flags>(
ESM::ActiveSpells::Flag_Temporary | ESM::ActiveSpells::Flag_Stackable);
mSourceName = ingredient->mName;
ESM::ENAMstruct effect;
effect.mEffectID = ingredient->mData.mEffectID[0];
effect.mSkill = ingredient->mData.mSkills[0];
effect.mAttribute = ingredient->mData.mAttributes[0];
effect.mRange = ESM::RT_Self;
effect.mArea = 0;
const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore();
const auto magicEffect = store.get<ESM::MagicEffect>().find(effect.mEffectID);
const MWMechanics::CreatureStats& creatureStats = mCaster.getClass().getCreatureStats(mCaster);
float x = (mCaster.getClass().getSkill(mCaster, ESM::Skill::Alchemy)
+ 0.2f * creatureStats.getAttribute(ESM::Attribute::Intelligence).getModified()
+ 0.1f * creatureStats.getAttribute(ESM::Attribute::Luck).getModified())
* creatureStats.getFatigueTerm();
auto effect = rollIngredientEffect(mCaster, ingredient, mCaster != getPlayer());
auto& prng = MWBase::Environment::get().getWorld()->getPrng();
int roll = Misc::Rng::roll0to99(prng);
if (roll > x)
if (effect)
inflict(mCaster, *effect, ESM::RT_Self);
else
{
// "X has no effect on you"
std::string message = store.get<ESM::GameSetting>().find("sNotifyMessage50")->mValue.getString();
std::string message = MWBase::Environment::get()
.getESMStore()
->get<ESM::GameSetting>()
.find("sNotifyMessage50")
->mValue.getString();
message = Misc::StringUtils::format(message, ingredient->mName);
MWBase::Environment::get().getWindowManager()->messageBox(message);
return false;
}
float magnitude = 0;
float y = roll / std::min(x, 100.f);
y *= 0.25f * x;
if (magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)
effect.mDuration = 1;
else
effect.mDuration = static_cast<int>(y);
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude))
{
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration))
magnitude = floor((0.05f * y) / (0.1f * magicEffect->mData.mBaseCost));
else
magnitude = floor(y / (0.1f * magicEffect->mData.mBaseCost));
magnitude = std::max(1.f, magnitude);
}
else
magnitude = 1;
effect.mMagnMax = static_cast<int>(magnitude);
effect.mMagnMin = static_cast<int>(magnitude);
ESM::EffectList effects;
effects.mList.push_back(effect);
inflict(mCaster, effects, ESM::RT_Self);
return true;
}
@ -527,14 +479,14 @@ namespace MWMechanics
playSpellCastingEffects(spell->mEffects.mList);
}
void CastSpell::playSpellCastingEffects(const std::vector<ESM::ENAMstruct>& effects) const
void CastSpell::playSpellCastingEffects(const std::vector<ESM::IndexedENAMstruct>& effects) const
{
const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore();
std::vector<std::string> addedEffects;
for (const ESM::ENAMstruct& effectData : effects)
for (const ESM::IndexedENAMstruct& effectData : effects)
{
const auto effect = store.get<ESM::MagicEffect>().find(effectData.mEffectID);
const auto effect = store.get<ESM::MagicEffect>().find(effectData.mData.mEffectID);
const ESM::Static* castStatic;
@ -587,7 +539,7 @@ namespace MWMechanics
}
if (animation && !mCaster.getClass().isActor())
animation->addSpellCastGlow(effect);
animation->addSpellCastGlow(effect->getColor());
addedEffects.push_back(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel));

@ -26,7 +26,7 @@ namespace MWMechanics
MWWorld::Ptr mCaster; // May be empty
MWWorld::Ptr mTarget; // May be empty
void playSpellCastingEffects(const std::vector<ESM::ENAMstruct>& effects) const;
void playSpellCastingEffects(const std::vector<ESM::IndexedENAMstruct>& effects) const;
void explodeSpell(const ESM::EffectList& effects, const MWWorld::Ptr& ignore, ESM::RangeType rangeType) const;
@ -41,13 +41,13 @@ namespace MWMechanics
false
}; // Always succeed spells casted by NPCs/creatures regardless of their chance (default: false)
bool mFromProjectile; // True if spell is cast by enchantment of some projectile (arrow, bolt or thrown weapon)
bool mManualSpell; // True if spell is casted from script and ignores some checks (mana level, success chance,
bool mScriptedSpell; // True if spell is casted from script and ignores some checks (mana level, success chance,
// etc.)
ESM::RefNum mItem;
ESM::ActiveSpells::EffectType mType{ ESM::ActiveSpells::Type_Temporary };
ESM::ActiveSpells::Flags mFlags{ ESM::ActiveSpells::Flag_Temporary };
CastSpell(const MWWorld::Ptr& caster, const MWWorld::Ptr& target, const bool fromProjectile = false,
const bool manualSpell = false);
const bool scriptedSpell = false);
bool cast(const ESM::Spell* spell);

@ -289,7 +289,7 @@ namespace
animation->addEffect(Misc::ResourceHelpers::correctMeshPath(absorbStatic->mModel),
ESM::MagicEffect::indexToName(ESM::MagicEffect::SpellAbsorption), false);
int spellCost = 0;
if (const ESM::Spell* spell = esmStore.get<ESM::Spell>().search(spellParams.getId()))
if (const ESM::Spell* spell = esmStore.get<ESM::Spell>().search(spellParams.getSourceSpellId()))
{
spellCost = MWMechanics::calcSpellCost(*spell);
}
@ -314,8 +314,7 @@ namespace
auto& stats = target.getClass().getCreatureStats(target);
auto& magnitudes = stats.getMagicEffects();
// Apply reflect and spell absorption
if (target != caster && spellParams.getType() != ESM::ActiveSpells::Type_Enchantment
&& spellParams.getType() != ESM::ActiveSpells::Type_Permanent)
if (target != caster && spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary))
{
bool canReflect = !(magicEffect->mData.mFlags & ESM::MagicEffect::Unreflectable)
&& !(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Reflect)
@ -358,9 +357,8 @@ namespace
// Apply resistances
if (!(effect.mFlags & ESM::ActiveEffect::Flag_Ignore_Resistances))
{
const ESM::Spell* spell = nullptr;
if (spellParams.getType() == ESM::ActiveSpells::Type_Temporary)
spell = MWBase::Environment::get().getESMStore()->get<ESM::Spell>().search(spellParams.getId());
const ESM::Spell* spell
= spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary) ? spellParams.getSpell() : nullptr;
float magnitudeMult
= MWMechanics::getEffectMultiplier(effect.mEffectId, target, caster, spell, &magnitudes);
if (magnitudeMult == 0)
@ -429,10 +427,9 @@ namespace MWMechanics
// Dispel removes entire spells at once
target.getClass().getCreatureStats(target).getActiveSpells().purge(
[magnitude = effect.mMagnitude](const ActiveSpells::ActiveSpellParams& params) {
if (params.getType() == ESM::ActiveSpells::Type_Temporary)
if (params.hasFlag(ESM::ActiveSpells::Flag_Temporary))
{
const ESM::Spell* spell
= MWBase::Environment::get().getESMStore()->get<ESM::Spell>().search(params.getId());
const ESM::Spell* spell = params.getSpell();
if (spell && spell->mData.mType == ESM::Spell::ST_Spell)
{
auto& prng = MWBase::Environment::get().getWorld()->getPrng();
@ -645,7 +642,7 @@ namespace MWMechanics
else if (effect.mEffectId == ESM::MagicEffect::DamageFatigue)
index = 2;
// Damage "Dynamic" abilities reduce the base value
if (spellParams.getType() == ESM::ActiveSpells::Type_Ability)
if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues))
modDynamicStat(target, index, -effect.mMagnitude);
else
{
@ -666,7 +663,7 @@ namespace MWMechanics
else if (!godmode)
{
// Damage Skill abilities reduce base skill :todd:
if (spellParams.getType() == ESM::ActiveSpells::Type_Ability)
if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues))
{
auto& npcStats = target.getClass().getNpcStats(target);
SkillValue& skill = npcStats.getSkill(effect.getSkillOrAttribute());
@ -725,7 +722,7 @@ namespace MWMechanics
case ESM::MagicEffect::FortifyHealth:
case ESM::MagicEffect::FortifyMagicka:
case ESM::MagicEffect::FortifyFatigue:
if (spellParams.getType() == ESM::ActiveSpells::Type_Ability)
if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues))
modDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, effect.mMagnitude);
else
adjustDynamicStat(
@ -737,7 +734,7 @@ namespace MWMechanics
break;
case ESM::MagicEffect::FortifyAttribute:
// Abilities affect base stats, but not for drain
if (spellParams.getType() == ESM::ActiveSpells::Type_Ability)
if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues))
{
auto& creatureStats = target.getClass().getCreatureStats(target);
auto attribute = effect.getSkillOrAttribute();
@ -757,7 +754,7 @@ namespace MWMechanics
case ESM::MagicEffect::FortifySkill:
if (!target.getClass().isNpc())
invalid = true;
else if (spellParams.getType() == ESM::ActiveSpells::Type_Ability)
else if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues))
{
// Abilities affect base stats, but not for drain
auto& npcStats = target.getClass().getNpcStats(target);
@ -922,7 +919,7 @@ namespace MWMechanics
{
MWRender::Animation* animation = world->getAnimation(target);
if (animation)
animation->addSpellCastGlow(magicEffect);
animation->addSpellCastGlow(magicEffect->getColor());
int magnitude = static_cast<int>(roll(effect));
if (target.getCellRef().getLockLevel()
< magnitude) // If the door is not already locked to a higher value, lock it to spell magnitude
@ -947,7 +944,7 @@ namespace MWMechanics
MWRender::Animation* animation = world->getAnimation(target);
if (animation)
animation->addSpellCastGlow(magicEffect);
animation->addSpellCastGlow(magicEffect->getColor());
int magnitude = static_cast<int>(roll(effect));
if (target.getCellRef().getLockLevel() <= magnitude)
{
@ -985,7 +982,7 @@ namespace MWMechanics
return { MagicApplicationResult::Type::APPLIED, receivedMagicDamage, affectedHealth };
auto& stats = target.getClass().getCreatureStats(target);
auto& magnitudes = stats.getMagicEffects();
if (spellParams.getType() != ESM::ActiveSpells::Type_Ability
if (!spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues)
&& !(effect.mFlags & ESM::ActiveEffect::Flag_Applied))
{
MagicApplicationResult::Type result
@ -998,10 +995,9 @@ namespace MWMechanics
oldMagnitude = effect.mMagnitude;
else
{
if (spellParams.getType() != ESM::ActiveSpells::Type_Enchantment)
playEffects(target, *magicEffect,
spellParams.getType() == ESM::ActiveSpells::Type_Consumable
|| spellParams.getType() == ESM::ActiveSpells::Type_Temporary);
if (!spellParams.hasFlag(ESM::ActiveSpells::Flag_Equipment)
&& !spellParams.hasFlag(ESM::ActiveSpells::Flag_Lua))
playEffects(target, *magicEffect, spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary));
if (effect.mEffectId == ESM::MagicEffect::Soultrap && !target.getClass().isNpc()
&& target.getType() == ESM::Creature::sRecordId
&& target.get<ESM::Creature>()->mBase->mData.mSoul == 0 && caster == getPlayer())
@ -1016,8 +1012,7 @@ namespace MWMechanics
if (effect.mDuration != 0)
{
float mult = dt;
if (spellParams.getType() == ESM::ActiveSpells::Type_Consumable
|| spellParams.getType() == ESM::ActiveSpells::Type_Temporary)
if (spellParams.hasFlag(ESM::ActiveSpells::Flag_Temporary))
mult = std::min(effect.mTimeLeft, dt);
effect.mMagnitude *= mult;
}
@ -1195,7 +1190,7 @@ namespace MWMechanics
case ESM::MagicEffect::FortifyHealth:
case ESM::MagicEffect::FortifyMagicka:
case ESM::MagicEffect::FortifyFatigue:
if (spellParams.getType() == ESM::ActiveSpells::Type_Ability)
if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues))
modDynamicStat(target, effect.mEffectId - ESM::MagicEffect::FortifyHealth, -effect.mMagnitude);
else
adjustDynamicStat(
@ -1206,7 +1201,7 @@ namespace MWMechanics
break;
case ESM::MagicEffect::FortifyAttribute:
// Abilities affect base stats, but not for drain
if (spellParams.getType() == ESM::ActiveSpells::Type_Ability)
if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues))
{
auto& creatureStats = target.getClass().getCreatureStats(target);
auto attribute = effect.getSkillOrAttribute();
@ -1222,7 +1217,7 @@ namespace MWMechanics
break;
case ESM::MagicEffect::FortifySkill:
// Abilities affect base stats, but not for drain
if (spellParams.getType() == ESM::ActiveSpells::Type_Ability)
if (spellParams.hasFlag(ESM::ActiveSpells::Flag_AffectsBaseValues))
{
auto& npcStats = target.getClass().getNpcStats(target);
auto& skill = npcStats.getSkill(effect.getSkillOrAttribute());

@ -34,7 +34,7 @@ namespace
if (effectFilter == -1)
{
const ESM::Spell* spell
= MWBase::Environment::get().getESMStore()->get<ESM::Spell>().search(it->getId());
= MWBase::Environment::get().getESMStore()->get<ESM::Spell>().search(it->getSourceSpellId());
if (!spell || spell->mData.mType != ESM::Spell::ST_Spell)
continue;
}
@ -67,7 +67,7 @@ namespace
const MWMechanics::ActiveSpells& activeSpells = actor.getClass().getCreatureStats(actor).getActiveSpells();
for (MWMechanics::ActiveSpells::TIterator it = activeSpells.begin(); it != activeSpells.end(); ++it)
{
if (it->getId() != spellId)
if (it->getSourceSpellId() != spellId)
continue;
const MWMechanics::ActiveSpells::ActiveSpellParams& params = *it;
@ -85,7 +85,7 @@ namespace
int actorId = caster.getClass().getCreatureStats(caster).getActorId();
const auto& active = target.getClass().getCreatureStats(target).getActiveSpells();
return std::find_if(active.begin(), active.end(), [&](const auto& spell) {
return spell.getCasterActorId() == actorId && spell.getId() == id;
return spell.getCasterActorId() == actorId && spell.getSourceSpellId() == id;
}) != active.end();
}
@ -110,13 +110,13 @@ namespace MWMechanics
int getRangeTypes(const ESM::EffectList& effects)
{
int types = 0;
for (std::vector<ESM::ENAMstruct>::const_iterator it = effects.mList.begin(); it != effects.mList.end(); ++it)
for (const ESM::IndexedENAMstruct& effect : effects.mList)
{
if (it->mRange == ESM::RT_Self)
if (effect.mData.mRange == ESM::RT_Self)
types |= RangeTypes::Self;
else if (it->mRange == ESM::RT_Touch)
else if (effect.mData.mRange == ESM::RT_Touch)
types |= RangeTypes::Touch;
else if (it->mRange == ESM::RT_Target)
else if (effect.mData.mRange == ESM::RT_Target)
types |= RangeTypes::Target;
}
return types;
@ -735,12 +735,12 @@ namespace MWMechanics
static const float fAIMagicSpellMult = gmst.find("fAIMagicSpellMult")->mValue.getFloat();
static const float fAIRangeMagicSpellMult = gmst.find("fAIRangeMagicSpellMult")->mValue.getFloat();
for (const ESM::ENAMstruct& effect : list.mList)
for (const ESM::IndexedENAMstruct& effect : list.mList)
{
float effectRating = rateEffect(effect, actor, enemy);
float effectRating = rateEffect(effect.mData, actor, enemy);
if (useSpellMult)
{
if (effect.mRange == ESM::RT_Target)
if (effect.mData.mRange == ESM::RT_Target)
effectRating *= fAIRangeMagicSpellMult;
else
effectRating *= fAIMagicSpellMult;
@ -760,10 +760,10 @@ namespace MWMechanics
float mult = fAIMagicSpellMult;
for (std::vector<ESM::ENAMstruct>::const_iterator effectIt = spell->mEffects.mList.begin();
for (std::vector<ESM::IndexedENAMstruct>::const_iterator effectIt = spell->mEffects.mList.begin();
effectIt != spell->mEffects.mList.end(); ++effectIt)
{
if (effectIt->mRange == ESM::RT_Target)
if (effectIt->mData.mRange == ESM::RT_Target)
{
if (!MWBase::Environment::get().getWorld()->isSwimming(enemy))
mult = fAIRangeMagicSpellMult;

@ -174,7 +174,7 @@ namespace MWMechanics
{
for (const auto& effectIt : spell->mEffects.mList)
{
if (effectIt.mEffectID == ESM::MagicEffect::Corprus)
if (effectIt.mData.mEffectID == ESM::MagicEffect::Corprus)
{
return true;
}

@ -4,6 +4,7 @@
#include <components/esm3/loadalch.hpp>
#include <components/esm3/loadench.hpp>
#include <components/esm3/loadingr.hpp>
#include <components/esm3/loadmgef.hpp>
#include "../mwbase/environment.hpp"
@ -23,13 +24,13 @@ namespace MWMechanics
{
float cost = 0;
for (const ESM::ENAMstruct& effect : list.mList)
for (const ESM::IndexedENAMstruct& effect : list.mList)
{
float effectCost = std::max(0.f, MWMechanics::calcEffectCost(effect, nullptr, method));
float effectCost = std::max(0.f, MWMechanics::calcEffectCost(effect.mData, nullptr, method));
// This is applied to the whole spell cost for each effect when
// creating spells, but is only applied on the effect itself in TES:CS.
if (effect.mRange == ESM::RT_Target)
if (effect.mData.mRange == ESM::RT_Target)
effectCost *= 1.5;
cost += effectCost;
@ -158,25 +159,83 @@ namespace MWMechanics
return potion.mData.mValue;
}
std::optional<ESM::EffectList> rollIngredientEffect(
MWWorld::Ptr caster, const ESM::Ingredient* ingredient, uint32_t index)
{
if (index >= 4)
throw std::range_error("Index out of range");
ESM::ENAMstruct effect;
effect.mEffectID = ingredient->mData.mEffectID[index];
effect.mSkill = ingredient->mData.mSkills[index];
effect.mAttribute = ingredient->mData.mAttributes[index];
effect.mRange = ESM::RT_Self;
effect.mArea = 0;
if (effect.mEffectID < 0)
return std::nullopt;
const MWWorld::ESMStore& store = *MWBase::Environment::get().getESMStore();
const auto magicEffect = store.get<ESM::MagicEffect>().find(effect.mEffectID);
const MWMechanics::CreatureStats& creatureStats = caster.getClass().getCreatureStats(caster);
float x = (caster.getClass().getSkill(caster, ESM::Skill::Alchemy)
+ 0.2f * creatureStats.getAttribute(ESM::Attribute::Intelligence).getModified()
+ 0.1f * creatureStats.getAttribute(ESM::Attribute::Luck).getModified())
* creatureStats.getFatigueTerm();
auto& prng = MWBase::Environment::get().getWorld()->getPrng();
int roll = Misc::Rng::roll0to99(prng);
if (roll > x)
{
return std::nullopt;
}
float magnitude = 0;
float y = roll / std::min(x, 100.f);
y *= 0.25f * x;
if (magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)
effect.mDuration = 1;
else
effect.mDuration = static_cast<int>(y);
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude))
{
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration))
magnitude = floor((0.05f * y) / (0.1f * magicEffect->mData.mBaseCost));
else
magnitude = floor(y / (0.1f * magicEffect->mData.mBaseCost));
magnitude = std::max(1.f, magnitude);
}
else
magnitude = 1;
effect.mMagnMax = static_cast<int>(magnitude);
effect.mMagnMin = static_cast<int>(magnitude);
ESM::EffectList effects;
effects.mList.push_back({ effect, index });
return effects;
}
float calcSpellBaseSuccessChance(const ESM::Spell* spell, const MWWorld::Ptr& actor, ESM::RefId* effectiveSchool)
{
// Morrowind for some reason uses a formula slightly different from magicka cost calculation
float y = std::numeric_limits<float>::max();
float lowestSkill = 0;
for (const ESM::ENAMstruct& effect : spell->mEffects.mList)
for (const ESM::IndexedENAMstruct& effect : spell->mEffects.mList)
{
float x = static_cast<float>(effect.mDuration);
float x = static_cast<float>(effect.mData.mDuration);
const auto magicEffect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(effect.mEffectID);
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(effect.mData.mEffectID);
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::AppliedOnce))
x = std::max(1.f, x);
x *= 0.1f * magicEffect->mData.mBaseCost;
x *= 0.5f * (effect.mMagnMin + effect.mMagnMax);
x += effect.mArea * 0.05f * magicEffect->mData.mBaseCost;
if (effect.mRange == ESM::RT_Target)
x *= 0.5f * (effect.mData.mMagnMin + effect.mData.mMagnMax);
x += effect.mData.mArea * 0.05f * magicEffect->mData.mBaseCost;
if (effect.mData.mRange == ESM::RT_Target)
x *= 1.5f;
static const float fEffectCostMult = MWBase::Environment::get()
.getESMStore()

@ -3,10 +3,14 @@
#include <components/esm3/loadskil.hpp>
#include <optional>
namespace ESM
{
struct EffectList;
struct ENAMstruct;
struct Enchantment;
struct Ingredient;
struct MagicEffect;
struct Potion;
struct Spell;
@ -36,6 +40,8 @@ namespace MWMechanics
int getEnchantmentCharge(const ESM::Enchantment& enchantment);
int getPotionValue(const ESM::Potion& potion);
std::optional<ESM::EffectList> rollIngredientEffect(
MWWorld::Ptr caster, const ESM::Ingredient* ingredient, uint32_t index = 0);
/**
* @param spell spell to cast

@ -1511,7 +1511,7 @@ namespace MWRender
return mObjectRoot.get();
}
void Animation::addSpellCastGlow(const ESM::MagicEffect* effect, float glowDuration)
void Animation::addSpellCastGlow(const osg::Vec4f& color, float glowDuration)
{
if (!mGlowUpdater || (mGlowUpdater->isDone() || (mGlowUpdater->isPermanentGlowUpdater() == true)))
{
@ -1520,12 +1520,11 @@ namespace MWRender
if (mGlowUpdater && mGlowUpdater->isPermanentGlowUpdater())
{
mGlowUpdater->setColor(effect->getColor());
mGlowUpdater->setColor(color);
mGlowUpdater->setDuration(glowDuration);
}
else
mGlowUpdater
= SceneUtil::addEnchantedGlow(mObjectRoot, mResourceSystem, effect->getColor(), glowDuration);
mGlowUpdater = SceneUtil::addEnchantedGlow(mObjectRoot, mResourceSystem, color, glowDuration);
}
}

@ -345,7 +345,7 @@ namespace MWRender
// Add a spell casting glow to an object. From measuring video taken from the original engine,
// the glow seems to be about 1.5 seconds except for telekinesis, which is 1 second.
void addSpellCastGlow(const ESM::MagicEffect* effect, float glowDuration = 1.5);
void addSpellCastGlow(const osg::Vec4f& color, float glowDuration = 1.5);
virtual void updatePtr(const MWWorld::Ptr& ptr);

@ -560,7 +560,7 @@ namespace MWScript
runtime.pop();
if (ptr.getClass().isActor())
ptr.getClass().getCreatureStats(ptr).getActiveSpells().removeEffects(ptr, spellid);
ptr.getClass().getCreatureStats(ptr).getActiveSpells().removeEffectsBySourceSpellId(ptr, spellid);
}
};

@ -532,7 +532,7 @@ namespace MWWorld
return result;
const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().search(
enchantment->mEffects.mList.front().mEffectID);
enchantment->mEffects.mList.front().mData.mEffectID);
if (!magicEffect)
return result;

@ -171,31 +171,31 @@ namespace
auto iter = spell.mEffects.mList.begin();
while (iter != spell.mEffects.mList.end())
{
const ESM::MagicEffect* mgef = magicEffects.search(iter->mEffectID);
const ESM::MagicEffect* mgef = magicEffects.search(iter->mData.mEffectID);
if (!mgef)
{
Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId
<< ": dropping invalid effect (index " << iter->mEffectID << ")";
<< ": dropping invalid effect (index " << iter->mData.mEffectID << ")";
iter = spell.mEffects.mList.erase(iter);
changed = true;
continue;
}
if (!(mgef->mData.mFlags & ESM::MagicEffect::TargetAttribute) && iter->mAttribute != -1)
if (!(mgef->mData.mFlags & ESM::MagicEffect::TargetAttribute) && iter->mData.mAttribute != -1)
{
iter->mAttribute = -1;
iter->mData.mAttribute = -1;
Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId
<< ": dropping unexpected attribute argument of "
<< ESM::MagicEffect::indexToGmstString(iter->mEffectID) << " effect";
<< ESM::MagicEffect::indexToGmstString(iter->mData.mEffectID) << " effect";
changed = true;
}
if (!(mgef->mData.mFlags & ESM::MagicEffect::TargetSkill) && iter->mSkill != -1)
if (!(mgef->mData.mFlags & ESM::MagicEffect::TargetSkill) && iter->mData.mSkill != -1)
{
iter->mSkill = -1;
iter->mData.mSkill = -1;
Log(Debug::Verbose) << RecordType::getRecordType() << " " << spell.mId
<< ": dropping unexpected skill argument of "
<< ESM::MagicEffect::indexToGmstString(iter->mEffectID) << " effect";
<< ESM::MagicEffect::indexToGmstString(iter->mData.mEffectID) << " effect";
changed = true;
}
@ -742,7 +742,16 @@ namespace MWWorld
case ESM::REC_DYNA:
reader.getSubNameIs("COUN");
if (reader.getFormatVersion() <= ESM::MaxActiveSpellTypeVersion)
{
uint32_t dynamicCount32 = 0;
reader.getHT(dynamicCount32);
mDynamicCount = dynamicCount32;
}
else
{
reader.getHT(mDynamicCount);
}
return true;
default:

@ -162,7 +162,7 @@ namespace MWWorld
std::vector<StoreBase*> mStores;
std::vector<DynamicStore*> mDynamicStores;
unsigned int mDynamicCount;
uint64_t mDynamicCount;
mutable std::unordered_map<ESM::RefId, std::weak_ptr<MWMechanics::SpellList>> mSpellListCache;
@ -209,6 +209,7 @@ namespace MWWorld
void clearDynamic();
void rebuildIdsIndex();
ESM::RefId generateId() { return ESM::RefId::generated(mDynamicCount++); }
void movePlayerRecord();
@ -229,7 +230,7 @@ namespace MWWorld
template <class T>
const T* insert(const T& x)
{
const ESM::RefId id = ESM::RefId::generated(mDynamicCount++);
const ESM::RefId id = generateId();
Store<T>& store = getWritable<T>();
if (store.search(id) != nullptr)

@ -46,8 +46,8 @@ namespace MWWorld
for (auto& effect : spell->mEffects.mList)
{
if (effect.mEffectID == ESM::MagicEffect::DrainAttribute)
stats.mWorsenings[effect.mAttribute] = oldStats.mWorsenings;
if (effect.mData.mEffectID == ESM::MagicEffect::DrainAttribute)
stats.mWorsenings[effect.mData.mAttribute] = oldStats.mWorsenings;
}
creatureStats.mCorprusSpells[id] = stats;
}
@ -58,30 +58,30 @@ namespace MWWorld
if (!spell || spell->mData.mType == ESM::Spell::ST_Spell || spell->mData.mType == ESM::Spell::ST_Power)
continue;
ESM::ActiveSpells::ActiveSpellParams params;
params.mId = id;
params.mSourceSpellId = id;
params.mDisplayName = spell->mName;
params.mCasterActorId = creatureStats.mActorId;
if (spell->mData.mType == ESM::Spell::ST_Ability)
params.mType = ESM::ActiveSpells::Type_Ability;
params.mFlags = ESM::Compatibility::ActiveSpells::Type_Ability_Flags;
else
params.mType = ESM::ActiveSpells::Type_Permanent;
params.mFlags = ESM::Compatibility::ActiveSpells::Type_Permanent_Flags;
params.mWorsenings = -1;
params.mNextWorsening = ESM::TimeStamp();
int effectIndex = 0;
for (const auto& enam : spell->mEffects.mList)
{
if (oldParams.mPurgedEffects.find(effectIndex) == oldParams.mPurgedEffects.end())
if (oldParams.mPurgedEffects.find(enam.mIndex) == oldParams.mPurgedEffects.end())
{
ESM::ActiveEffect effect;
effect.mEffectId = enam.mEffectID;
effect.mArg = MWMechanics::EffectKey(enam).mArg;
effect.mEffectId = enam.mData.mEffectID;
effect.mArg = MWMechanics::EffectKey(enam.mData).mArg;
effect.mDuration = -1;
effect.mTimeLeft = -1;
effect.mEffectIndex = effectIndex;
auto rand = oldParams.mEffectRands.find(effectIndex);
effect.mEffectIndex = enam.mIndex;
auto rand = oldParams.mEffectRands.find(enam.mIndex);
if (rand != oldParams.mEffectRands.end())
{
float magnitude = (enam.mMagnMax - enam.mMagnMin) * rand->second + enam.mMagnMin;
float magnitude
= (enam.mData.mMagnMax - enam.mData.mMagnMin) * rand->second + enam.mData.mMagnMin;
effect.mMagnitude = magnitude;
effect.mMinMagnitude = magnitude;
effect.mMaxMagnitude = magnitude;
@ -92,13 +92,12 @@ namespace MWWorld
else
{
effect.mMagnitude = 0.f;
effect.mMinMagnitude = enam.mMagnMin;
effect.mMaxMagnitude = enam.mMagnMax;
effect.mMinMagnitude = enam.mData.mMagnMin;
effect.mMaxMagnitude = enam.mData.mMagnMax;
effect.mFlags = ESM::ActiveEffect::Flag_None;
}
params.mEffects.emplace_back(effect);
}
effectIndex++;
}
creatureStats.mActiveSpells.mSpells.emplace_back(params);
}
@ -132,30 +131,28 @@ namespace MWWorld
if (!enchantment)
continue;
ESM::ActiveSpells::ActiveSpellParams params;
params.mId = id;
params.mSourceSpellId = id;
params.mDisplayName = std::move(name);
params.mCasterActorId = creatureStats.mActorId;
params.mType = ESM::ActiveSpells::Type_Enchantment;
params.mFlags = ESM::Compatibility::ActiveSpells::Type_Enchantment_Flags;
params.mWorsenings = -1;
params.mNextWorsening = ESM::TimeStamp();
for (std::size_t effectIndex = 0;
effectIndex < oldMagnitudes.size() && effectIndex < enchantment->mEffects.mList.size(); ++effectIndex)
for (const auto& enam : enchantment->mEffects.mList)
{
const auto& enam = enchantment->mEffects.mList[effectIndex];
auto [random, multiplier] = oldMagnitudes[effectIndex];
float magnitude = (enam.mMagnMax - enam.mMagnMin) * random + enam.mMagnMin;
auto [random, multiplier] = oldMagnitudes[enam.mIndex];
float magnitude = (enam.mData.mMagnMax - enam.mData.mMagnMin) * random + enam.mData.mMagnMin;
magnitude *= multiplier;
if (magnitude <= 0)
continue;
ESM::ActiveEffect effect;
effect.mEffectId = enam.mEffectID;
effect.mEffectId = enam.mData.mEffectID;
effect.mMagnitude = magnitude;
effect.mMinMagnitude = magnitude;
effect.mMaxMagnitude = magnitude;
effect.mArg = MWMechanics::EffectKey(enam).mArg;
effect.mArg = MWMechanics::EffectKey(enam.mData).mArg;
effect.mDuration = -1;
effect.mTimeLeft = -1;
effect.mEffectIndex = static_cast<int>(effectIndex);
effect.mEffectIndex = enam.mIndex;
// Prevent recalculation of resistances and don't reflect or absorb the effect
effect.mFlags = ESM::ActiveEffect::Flag_Ignore_Resistances | ESM::ActiveEffect::Flag_Ignore_Reflect
| ESM::ActiveEffect::Flag_Ignore_SpellAbsorption;
@ -172,7 +169,7 @@ namespace MWWorld
{
auto it
= std::find_if(creatureStats.mActiveSpells.mSpells.begin(), creatureStats.mActiveSpells.mSpells.end(),
[&](const auto& params) { return params.mId == spell.first; });
[&](const auto& params) { return params.mSourceSpellId == spell.first; });
if (it != creatureStats.mActiveSpells.mSpells.end())
{
it->mNextWorsening = spell.second.mNextWorsening;
@ -188,7 +185,7 @@ namespace MWWorld
continue;
for (auto& params : creatureStats.mActiveSpells.mSpells)
{
if (params.mId == key.mSourceId)
if (params.mSourceSpellId == key.mSourceId)
{
bool found = false;
for (auto& effect : params.mEffects)

@ -79,18 +79,17 @@ namespace
int count = 0;
speed = 0.0f;
ESM::EffectList projectileEffects;
for (std::vector<ESM::ENAMstruct>::const_iterator iter(effects->mList.begin()); iter != effects->mList.end();
++iter)
for (const ESM::IndexedENAMstruct& effect : effects->mList)
{
const ESM::MagicEffect* magicEffect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(iter->mEffectID);
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(effect.mData.mEffectID);
// Speed of multi-effect projectiles should be the average of the constituent effects,
// based on observation of the original engine.
speed += magicEffect->mData.mSpeed;
count++;
if (iter->mRange != ESM::RT_Target)
if (effect.mData.mRange != ESM::RT_Target)
continue;
if (magicEffect->mBolt.empty())
@ -106,7 +105,7 @@ namespace
->get<ESM::Skill>()
.find(magicEffect->mData.mSchool)
->mSchool->mBoltSound);
projectileEffects.mList.push_back(*iter);
projectileEffects.mList.push_back(effect);
}
if (count != 0)
@ -117,7 +116,7 @@ namespace
{
const ESM::MagicEffect* magicEffect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(
effects->mList.begin()->mEffectID);
effects->mList.begin()->mData.mEffectID);
texture = magicEffect->mParticle;
}
@ -136,10 +135,10 @@ namespace
{
// Calculate combined light diffuse color from magical effects
osg::Vec4 lightDiffuseColor;
for (const ESM::ENAMstruct& enam : effects.mList)
for (const ESM::IndexedENAMstruct& enam : effects.mList)
{
const ESM::MagicEffect* magicEffect
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(enam.mEffectID);
= MWBase::Environment::get().getESMStore()->get<ESM::MagicEffect>().find(enam.mData.mEffectID);
lightDiffuseColor += magicEffect->getColor();
}
int numberOfEffects = effects.mList.size();

@ -2940,14 +2940,14 @@ namespace MWWorld
return result;
}
void World::castSpell(const Ptr& actor, bool manualSpell)
void World::castSpell(const Ptr& actor, bool scriptedSpell)
{
MWMechanics::CreatureStats& stats = actor.getClass().getCreatureStats(actor);
const bool casterIsPlayer = actor == MWMechanics::getPlayer();
MWWorld::Ptr target;
// For scripted spells we should not use hit contact
if (manualSpell)
if (scriptedSpell)
{
if (!casterIsPlayer)
{
@ -3015,7 +3015,7 @@ namespace MWWorld
const ESM::RefId& selectedSpell = stats.getSpells().getSelectedSpell();
MWMechanics::CastSpell cast(actor, target, false, manualSpell);
MWMechanics::CastSpell cast(actor, target, false, scriptedSpell);
cast.mHitPosition = hitPosition;
if (!selectedSpell.empty())
@ -3703,22 +3703,22 @@ namespace MWWorld
void World::preloadEffects(const ESM::EffectList* effectList)
{
for (const ESM::ENAMstruct& effectInfo : effectList->mList)
for (const ESM::IndexedENAMstruct& effectInfo : effectList->mList)
{
const ESM::MagicEffect* effect = mStore.get<ESM::MagicEffect>().find(effectInfo.mEffectID);
const ESM::MagicEffect* effect = mStore.get<ESM::MagicEffect>().find(effectInfo.mData.mEffectID);
if (MWMechanics::isSummoningEffect(effectInfo.mEffectID))
if (MWMechanics::isSummoningEffect(effectInfo.mData.mEffectID))
{
preload(mWorldScene.get(), mStore, ESM::RefId::stringRefId("VFX_Summon_Start"));
preload(mWorldScene.get(), mStore, MWMechanics::getSummonedCreature(effectInfo.mEffectID));
preload(mWorldScene.get(), mStore, MWMechanics::getSummonedCreature(effectInfo.mData.mEffectID));
}
preload(mWorldScene.get(), mStore, effect->mCasting);
preload(mWorldScene.get(), mStore, effect->mHit);
if (effectInfo.mArea > 0)
if (effectInfo.mData.mArea > 0)
preload(mWorldScene.get(), mStore, effect->mArea);
if (effectInfo.mRange == ESM::RT_Target)
if (effectInfo.mData.mRange == ESM::RT_Target)
preload(mWorldScene.get(), mStore, effect->mBolt);
}
}

@ -530,7 +530,7 @@ namespace ESM
TEST_P(Esm3SaveLoadRecordTest, enamShouldNotChange)
{
EffectList record;
record.mList.emplace_back(ENAMstruct{
record.mList.emplace_back(IndexedENAMstruct{ {
.mEffectID = 1,
.mSkill = 2,
.mAttribute = 3,
@ -539,20 +539,21 @@ namespace ESM
.mDuration = 6,
.mMagnMin = 7,
.mMagnMax = 8,
});
},
0 });
EffectList result;
saveAndLoadRecord(record, GetParam(), result);
EXPECT_EQ(result.mList.size(), record.mList.size());
EXPECT_EQ(result.mList[0].mEffectID, record.mList[0].mEffectID);
EXPECT_EQ(result.mList[0].mSkill, record.mList[0].mSkill);
EXPECT_EQ(result.mList[0].mAttribute, record.mList[0].mAttribute);
EXPECT_EQ(result.mList[0].mRange, record.mList[0].mRange);
EXPECT_EQ(result.mList[0].mArea, record.mList[0].mArea);
EXPECT_EQ(result.mList[0].mDuration, record.mList[0].mDuration);
EXPECT_EQ(result.mList[0].mMagnMin, record.mList[0].mMagnMin);
EXPECT_EQ(result.mList[0].mMagnMax, record.mList[0].mMagnMax);
EXPECT_EQ(result.mList[0].mData.mEffectID, record.mList[0].mData.mEffectID);
EXPECT_EQ(result.mList[0].mData.mSkill, record.mList[0].mData.mSkill);
EXPECT_EQ(result.mList[0].mData.mAttribute, record.mList[0].mData.mAttribute);
EXPECT_EQ(result.mList[0].mData.mRange, record.mList[0].mData.mRange);
EXPECT_EQ(result.mList[0].mData.mArea, record.mList[0].mData.mArea);
EXPECT_EQ(result.mList[0].mData.mDuration, record.mList[0].mData.mDuration);
EXPECT_EQ(result.mList[0].mData.mMagnMin, record.mList[0].mData.mMagnMin);
EXPECT_EQ(result.mList[0].mData.mMagnMax, record.mList[0].mData.mMagnMax);
}
TEST_P(Esm3SaveLoadRecordTest, weaponShouldNotChange)

@ -93,11 +93,12 @@ namespace ESM
{
for (const auto& params : spells)
{
esm.writeHNRefId(tag, params.mId);
esm.writeHNRefId(tag, params.mSourceSpellId);
esm.writeHNRefId("SPID", params.mActiveSpellId);
esm.writeHNT("CAST", params.mCasterActorId);
esm.writeHNString("DISP", params.mDisplayName);
esm.writeHNT("TYPE", params.mType);
esm.writeHNT("FLAG", params.mFlags);
if (params.mItem.isSet())
esm.writeFormId(params.mItem, true, "ITEM");
if (params.mWorsenings >= 0)
@ -130,14 +131,42 @@ namespace ESM
while (esm.isNextSub(tag))
{
ActiveSpells::ActiveSpellParams params;
params.mId = esm.getRefId();
params.mSourceSpellId = esm.getRefId();
if (format > MaxActiveSpellTypeVersion)
params.mActiveSpellId = esm.getHNRefId("SPID");
esm.getHNT(params.mCasterActorId, "CAST");
params.mDisplayName = esm.getHNString("DISP");
if (format <= MaxClearModifiersFormatVersion)
params.mType = ActiveSpells::Type_Temporary;
params.mFlags = Compatibility::ActiveSpells::Type_Temporary_Flags;
else
{
esm.getHNT(params.mType, "TYPE");
if (format <= MaxActiveSpellTypeVersion)
{
Compatibility::ActiveSpells::EffectType type;
esm.getHNT(type, "TYPE");
switch (type)
{
case Compatibility::ActiveSpells::Type_Ability:
params.mFlags = Compatibility::ActiveSpells::Type_Ability_Flags;
break;
case Compatibility::ActiveSpells::Type_Consumable:
params.mFlags = Compatibility::ActiveSpells::Type_Consumable_Flags;
break;
case Compatibility::ActiveSpells::Type_Enchantment:
params.mFlags = Compatibility::ActiveSpells::Type_Enchantment_Flags;
break;
case Compatibility::ActiveSpells::Type_Permanent:
params.mFlags = Compatibility::ActiveSpells::Type_Permanent_Flags;
break;
case Compatibility::ActiveSpells::Type_Temporary:
params.mFlags = Compatibility::ActiveSpells::Type_Temporary_Flags;
break;
}
}
else
{
esm.getHNT(params.mFlags, "FLAG");
}
if (esm.peekNextSub("ITEM"))
{
if (format <= MaxActiveSpellSlotIndexFormatVersion)

@ -46,23 +46,28 @@ namespace ESM
// format 0, saved games only
struct ActiveSpells
{
enum EffectType
enum Flags : uint32_t
{
Type_Temporary,
Type_Ability,
Type_Enchantment,
Type_Permanent,
Type_Consumable
Flag_Temporary = 1 << 0, //!< Effect will end automatically once its duration ends.
Flag_Equipment = 1 << 1, //!< Effect will end automatically if item is unequipped.
Flag_SpellStore = 1 << 2, //!< Effect will end automatically if removed from the actor's spell store.
Flag_AffectsBaseValues = 1 << 3, //!< Effects will affect base values instead of current values.
Flag_Stackable
= 1 << 4, //!< Effect can stack. If this flag is not set, spells from the same caster and item cannot stack.
Flag_Lua
= 1 << 5, //!< Effect was added via Lua. Should not do any vfx/sound as this is handled by Lua scripts.
};
struct ActiveSpellParams
{
RefId mId;
RefId mActiveSpellId;
RefId mSourceSpellId;
std::vector<ActiveEffect> mEffects;
std::string mDisplayName;
int32_t mCasterActorId;
RefNum mItem;
EffectType mType;
Flags mFlags;
bool mStackable;
int32_t mWorsenings;
TimeStamp mNextWorsening;
};
@ -73,6 +78,29 @@ namespace ESM
void load(ESMReader& esm);
void save(ESMWriter& esm) const;
};
namespace Compatibility
{
namespace ActiveSpells
{
enum EffectType
{
Type_Temporary,
Type_Ability,
Type_Enchantment,
Type_Permanent,
Type_Consumable,
};
using Flags = ESM::ActiveSpells::Flags;
constexpr Flags Type_Temporary_Flags = Flags::Flag_Temporary;
constexpr Flags Type_Consumable_Flags = static_cast<Flags>(Flags::Flag_Temporary | Flags::Flag_Stackable);
constexpr Flags Type_Permanent_Flags = Flags::Flag_SpellStore;
constexpr Flags Type_Ability_Flags
= static_cast<Flags>(Flags::Flag_SpellStore | Flags::Flag_AffectsBaseValues);
constexpr Flags Type_Enchantment_Flags = Flags::Flag_Equipment;
}
}
}
#endif

@ -22,19 +22,40 @@ namespace ESM
}
}
void EffectList::populate(const std::vector<ENAMstruct>& effects)
{
mList.clear();
for (size_t i = 0; i < effects.size(); i++)
mList.push_back({ effects[i], static_cast<uint32_t>(i) });
}
void EffectList::updateIndexes()
{
for (size_t i = 0; i < mList.size(); i++)
mList[i].mIndex = i;
}
void EffectList::add(ESMReader& esm)
{
ENAMstruct s;
esm.getSubComposite(s);
mList.push_back(s);
mList.push_back({ s, static_cast<uint32_t>(mList.size()) });
}
void EffectList::save(ESMWriter& esm) const
{
for (const ENAMstruct& enam : mList)
for (const IndexedENAMstruct& enam : mList)
{
esm.writeNamedComposite("ENAM", enam);
esm.writeNamedComposite("ENAM", enam.mData);
}
}
bool IndexedENAMstruct::operator!=(const IndexedENAMstruct& rhs) const
{
return mData.mEffectID != rhs.mData.mEffectID || mData.mArea != rhs.mData.mArea
|| mData.mRange != rhs.mData.mRange || mData.mSkill != rhs.mData.mSkill
|| mData.mAttribute != rhs.mData.mAttribute || mData.mMagnMin != rhs.mData.mMagnMin
|| mData.mMagnMax != rhs.mData.mMagnMax || mData.mDuration != rhs.mData.mDuration;
}
} // end namespace

@ -26,10 +26,21 @@ namespace ESM
int32_t mArea, mDuration, mMagnMin, mMagnMax;
};
struct IndexedENAMstruct
{
bool operator!=(const IndexedENAMstruct& rhs) const;
bool operator==(const IndexedENAMstruct& rhs) const { return !(this->operator!=(rhs)); }
ENAMstruct mData;
uint32_t mIndex;
};
/// EffectList, ENAM subrecord
struct EffectList
{
std::vector<ENAMstruct> mList;
std::vector<IndexedENAMstruct> mList;
void populate(const std::vector<ENAMstruct>& effects);
void updateIndexes();
/// Load one effect, assumes subrecord name was already read
void add(ESMReader& esm);

@ -26,7 +26,8 @@ namespace ESM
inline constexpr FormatVersion MaxUseEsmCellIdFormatVersion = 26;
inline constexpr FormatVersion MaxActiveSpellSlotIndexFormatVersion = 27;
inline constexpr FormatVersion MaxOldCountFormatVersion = 30;
inline constexpr FormatVersion CurrentSaveGameFormatVersion = 31;
inline constexpr FormatVersion MaxActiveSpellTypeVersion = 31;
inline constexpr FormatVersion CurrentSaveGameFormatVersion = 32;
inline constexpr FormatVersion MinSupportedSaveGameFormatVersion = 5;
inline constexpr FormatVersion OpenMW0_48SaveGameFormatVersion = 21;

@ -339,6 +339,11 @@
-- @field #string id Record id of the spell or item used to cast the spell
-- @field #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 #GameObject caster The caster object, or nil if the spell has no defined caster
-- @field #boolean fromEquipment If set, this spell is tied to an equipped item and can only be ended by unequipping the item.
-- @field #boolean temporary If set, this spell effect is temporary and should end on its own. Either after a single application or after its duration has run out.
-- @field #boolean affectsBaseValues If set, this spell affects the base values of affected stats, rather than modifying current values.
-- @field #boolean stackable If set, this spell can be applied multiple times. If not set, the same spell can only be applied once from the same source (where source is determined by caster + item). In vanilla rules, consumables are stackable while spells and enchantments are not.
-- @field #number activeSpellId A number uniquely identifying this active spell within the affected actor's list of active spells.
-- @field #list<#ActiveSpellEffect> effects The active effects (@{#ActiveSpellEffect}) of this spell.
---
@ -369,7 +374,7 @@
-- @type Enchantment
-- @field #string id Enchantment id
-- @field #number type @{#EnchantmentType}
-- @field #number autocalcFlag If set, the casting cost should be computer rather than reading the cost field
-- @field #boolean autocalcFlag If set, the casting cost should be computed based on the effect list rather than read from the cost field
-- @field #number cost
-- @field #number charge Charge capacity. Should not be confused with current charge.
-- @field #list<#MagicEffectWithParams> effects The effects (@{#MagicEffectWithParams}) of the enchantment
@ -671,6 +676,8 @@
-- @field #number type @{#SpellType}
-- @field #number cost
-- @field #list<#MagicEffectWithParams> effects The effects (@{#MagicEffectWithParams}) of the spell
-- @field #boolean alwaysSucceedFlag If set, the spell should ignore skill checks and always succeed.
-- @field #boolean autocalcFlag If set, the casting cost should be computed based on the effect list rather than read from the cost field
---
-- @type MagicEffect
@ -680,16 +687,28 @@
-- @field #string school Skill ID that is this effect's school
-- @field #number baseCost
-- @field openmw.util#Color color
-- @field #boolean harmful
-- @field #boolean harmful If set, the effect is considered harmful and should elicit a hostile reaction from affected NPCs.
-- @field #boolean continuousVfx Whether the magic effect's vfx should loop or not
-- @field #boolean hasDuration If set, the magic effect has a duration. As an example, divine intervention has no duration while fire damage does.
-- @field #boolean hasMagnitude If set, the magic effect depends on a magnitude. As an example, cure common disease has no magnitude while chameleon does.
-- @field #boolean isAppliedOnce If set, the magic effect is applied fully on cast, rather than being continuously applied over the effect's duration. For example, chameleon is applied once, while fire damage is continuously applied for the duration.
-- @field #boolean casterLinked If set, it is implied the magic effect links back to the caster in some way and should end immediately or never be applied if the caster dies or is not an actor.
-- @field #boolean nonRecastable If set, this effect cannot be re-applied until it has ended. This is used by bound equipment spells.
-- @field #string particle Identifier of the particle texture
-- @field #string castingStatic Identifier of the vfx static used for casting
-- @field #string castStatic Identifier of the vfx static used for casting
-- @field #string hitStatic Identifier of the vfx static used on hit
-- @field #string areaStatic Identifier of the vfx static used for AOE spells
-- @field #string boltStatic Identifier of the projectile vfx static used for ranged spells
-- @field #string castSound Identifier of the sound used for casting
-- @field #string hitSound Identifier of the sound used on hit
-- @field #string areaSound Identifier of the sound used for AOE spells
-- @field #string boltSound Identifier of the projectile sound used for ranged spells
---
-- @type MagicEffectWithParams
-- @field #MagicEffect effect @{#MagicEffect}
-- @field #string id ID of the associated @{#MagicEffect}
-- @field #string affectedSkill Optional skill ID
-- @field #string affectedAttribute Optional attribute ID
-- @field #number range
@ -697,6 +716,7 @@
-- @field #number magnitudeMin
-- @field #number magnitudeMax
-- @field #number duration
-- @field #number index Index of this effect within the original list of @{#MagicEffectWithParams} of the spell/enchantment/potion this effect came from.
---
-- @type ActiveEffect
@ -708,6 +728,7 @@
-- @field #number magnitude current magnitude of the effect. Will be set to 0 when effect is removed or expires.
-- @field #number magnitudeBase
-- @field #number magnitudeModifier
-- @field #number index Index of this effect within the original list of @{#MagicEffectWithParams} of the spell/enchantment/potion this effect came from.
--- @{#Sound}: Sounds and Speech
-- @field [parent=#core] #Sound sound

@ -302,17 +302,59 @@
-- end
---
-- Get whether a specific spell is active on the actor.
-- Get whether any instance of the specific spell is active on the actor.
-- @function [parent=#ActorActiveSpells] isSpellActive
-- @param self
-- @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
-- @param #any recordOrId A record or string record ID. Valid records are @{openmw.core#Spell}, enchanted @{#Item}, @{#IngredientRecord}, or @{#PotionRecord}.
-- @return true if spell is active, false otherwise
---
-- Remove the given spell and all its effects from the given actor's active spells.
-- If true, the actor has not used this power in the last 24h. Will return true for powers the actor does not have.
-- @function [parent=#ActorActiveSpells] canUsePower
-- @param self
-- @param #any spellOrId A @{openmw.core#Spell} or string record id.
---
-- Remove an active spell based on active spell ID (see @{openmw.core#ActiveSpell.activeSpellId}). Can only be used in global scripts or on self. Can only be used to remove spells with the temporary flag set (see @{openmw.core#ActiveSpell.temporary}).
-- @function [parent=#ActorActiveSpells] remove
-- @param self
-- @param #any spellOrId @{openmw.core#Spell} or string spell id
-- @param #any id Active spell ID.
---
-- Adds a new spell to the list of active spells (only in global scripts or on self).
-- Note that this does not play any related VFX or sounds.
-- @function [parent=#ActorActiveSpells] add
-- @param self
-- @param #table options A table of parameters. Must contain the following required parameters:
--
-- * `id` - A string record ID. Valid records are @{openmw.core#Spell}, enchanted @{#Item}, @{#IngredientRecord}, or @{#PotionRecord}.
-- * `effects` - A list of indexes of the effects to be applied. These indexes must be in range of the record's list of @{openmw.core#MagicEffectWithParams}. Note that for Ingredients, normal ingredient consumption rules will be applied to effects.
--
-- And may contain the following optional parameters:
--
-- * `name` - The name to show in the list of active effects in the UI. Default: Name of the record identified by the id.
-- * `ignoreResistances` - If true, resistances will be ignored. Default: false
-- * `ignoreSpellAbsorption` - If true, spell absorption will not be applied. Default: false.
-- * `ignoreReflect` - If true, reflects will not be applied. Default: false.
-- * `caster` - A game object that identifies the caster. Default: nil
-- * `item` - A game object that identifies the specific enchanted item instance used to cast the spell. Default: nil
-- * `stackable` - If true, the spell will be able to stack. If false, existing instances of spells with the same id from the same source (where source is caster + item)
-- * `quiet` - If true, no messages will be printed if the spell is an Ingredient and it had no effect. Always true if the target is not the player.
-- @usage
-- -- Adds the effect of the chameleon spell to the character
-- Actor.activeSpells(self):add({id = 'chameleon', effects = { 0 }})
-- @usage
-- -- Adds the effect of a standard potion of intelligence, without consuming any potions from the character's inventory.
-- -- Note that stackable = true to let the effect stack like a potion should.
-- Actor.activeSpells(self):add({id = 'p_fortify_intelligence_s', effects = { 0 }, stackable = true})
-- @usage
-- -- Adds the negative effect of Greef twice over, and renames it to Good Greef.
-- Actor.activeSpells(self):add({id = 'potion_comberry_brandy_01', effects = { 1, 1 }, stackable = true, name = 'Good Greef'})
-- @usage
-- -- Has the same effect as if the actor ate a chokeweed. With the same variable effect based on skill / random chance.
-- Actor.activeSpells(self):add({id = 'ingred_chokeweed_01', effects = { 0 }, stackable = true, name = 'Chokeweed'})
-- -- Same as above, but uses a different index. Note that if multiple indexes are used, the randomicity is applied separately for each effect.
-- Actor.activeSpells(self):add({id = 'ingred_chokeweed_01', effects = { 1 }, stackable = true, name = 'Chokeweed'})
---
-- Return the spells (@{#ActorSpells}) of the given actor.

Loading…
Cancel
Save