1
0
mirror of https://gitlab.com/OpenMW/openmw.git synced 2025-01-30 12:32:36 +00:00
florent.teppe 65cdd489fb create a specific esm reader function for RefID to avoid allocation for string and then again for RefId
Fixed some types

removed useless header

applied clang format

fixed compile tests

fixed clang tidy, and closer to logic before this MR

Removed hardcoded refids

unless there is a returned value we don't use static RefIds
can use == between RefId and hardcoded string

Fix clang format

Fixed a few instances where std::string was used, when only const std::string& was needed

removed unused variable
2022-12-27 19:15:57 +01:00

609 lines
18 KiB
C++

#include "alchemy.hpp"
#include <cassert>
#include <cmath>
#include <algorithm>
#include <map>
#include <stdexcept>
#include <components/misc/rng.hpp>
#include <components/esm3/loadappa.hpp>
#include <components/esm3/loadgmst.hpp>
#include <components/esm3/loadmgef.hpp>
#include <components/esm3/loadskil.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/world.hpp"
#include "../mwworld/class.hpp"
#include "../mwworld/containerstore.hpp"
#include "../mwworld/esmstore.hpp"
#include "creaturestats.hpp"
#include "magiceffects.hpp"
MWMechanics::Alchemy::Alchemy()
: mValue(0)
, mPotionName("")
{
}
std::set<MWMechanics::EffectKey> MWMechanics::Alchemy::listEffects() const
{
std::map<EffectKey, int> effects;
for (TIngredientsIterator iter(mIngredients.begin()); iter != mIngredients.end(); ++iter)
{
if (!iter->isEmpty())
{
const MWWorld::LiveCellRef<ESM::Ingredient>* ingredient = iter->get<ESM::Ingredient>();
std::set<EffectKey> seenEffects;
for (int i = 0; i < 4; ++i)
if (ingredient->mBase->mData.mEffectID[i] != -1)
{
EffectKey key(ingredient->mBase->mData.mEffectID[i],
ingredient->mBase->mData.mSkills[i] != -1 ? ingredient->mBase->mData.mSkills[i]
: ingredient->mBase->mData.mAttributes[i]);
if (seenEffects.insert(key).second)
++effects[key];
}
}
}
std::set<EffectKey> effects2;
for (std::map<EffectKey, int>::const_iterator iter(effects.begin()); iter != effects.end(); ++iter)
if (iter->second > 1)
effects2.insert(iter->first);
return effects2;
}
void MWMechanics::Alchemy::applyTools(int flags, float& value) const
{
bool magnitude = !(flags & ESM::MagicEffect::NoMagnitude);
bool duration = !(flags & ESM::MagicEffect::NoDuration);
bool negative = (flags & ESM::MagicEffect::Harmful) != 0;
int tool = negative ? ESM::Apparatus::Alembic : ESM::Apparatus::Retort;
int setup = 0;
if (!mTools[tool].isEmpty() && !mTools[ESM::Apparatus::Calcinator].isEmpty())
setup = 1;
else if (!mTools[tool].isEmpty())
setup = 2;
else if (!mTools[ESM::Apparatus::Calcinator].isEmpty())
setup = 3;
else
return;
float toolQuality = setup == 1 || setup == 2 ? mTools[tool].get<ESM::Apparatus>()->mBase->mData.mQuality : 0;
float calcinatorQuality = setup == 1 || setup == 3
? mTools[ESM::Apparatus::Calcinator].get<ESM::Apparatus>()->mBase->mData.mQuality
: 0;
float quality = 1;
switch (setup)
{
case 1:
quality = negative ? 2 * toolQuality + 3 * calcinatorQuality
: (magnitude && duration ? 2 * toolQuality + calcinatorQuality
: 2 / 3.0f * (toolQuality + calcinatorQuality) + 0.5f);
break;
case 2:
quality = negative ? 1 + toolQuality : (magnitude && duration ? toolQuality : toolQuality + 0.5f);
break;
case 3:
quality = magnitude && duration ? calcinatorQuality : calcinatorQuality + 0.5f;
break;
}
if (setup == 3 || !negative)
{
value += quality;
}
else
{
if (quality == 0)
throw std::runtime_error("invalid derived alchemy apparatus quality");
value /= quality;
}
}
void MWMechanics::Alchemy::updateEffects()
{
mEffects.clear();
mValue = 0;
if (countIngredients() < 2 || mAlchemist.isEmpty() || mTools[ESM::Apparatus::MortarPestle].isEmpty())
return;
// find effects
std::set<EffectKey> effects(listEffects());
// general alchemy factor
float x = getAlchemyFactor();
x *= mTools[ESM::Apparatus::MortarPestle].get<ESM::Apparatus>()->mBase->mData.mQuality;
x *= MWBase::Environment::get()
.getWorld()
->getStore()
.get<ESM::GameSetting>()
.find("fPotionStrengthMult")
->mValue.getFloat();
// value
mValue = static_cast<int>(x
* MWBase::Environment::get()
.getWorld()
->getStore()
.get<ESM::GameSetting>()
.find("iAlchemyMod")
->mValue.getFloat());
// build quantified effect list
for (std::set<EffectKey>::const_iterator iter(effects.begin()); iter != effects.end(); ++iter)
{
const ESM::MagicEffect* magicEffect
= MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find(iter->mId);
if (magicEffect->mData.mBaseCost <= 0)
{
const std::string os = "invalid base cost for magic effect " + std::to_string(iter->mId);
throw std::runtime_error(os);
}
float fPotionT1MagMul = MWBase::Environment::get()
.getWorld()
->getStore()
.get<ESM::GameSetting>()
.find("fPotionT1MagMult")
->mValue.getFloat();
if (fPotionT1MagMul <= 0)
throw std::runtime_error("invalid gmst: fPotionT1MagMul");
float fPotionT1DurMult = MWBase::Environment::get()
.getWorld()
->getStore()
.get<ESM::GameSetting>()
.find("fPotionT1DurMult")
->mValue.getFloat();
if (fPotionT1DurMult <= 0)
throw std::runtime_error("invalid gmst: fPotionT1DurMult");
float magnitude = (magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude)
? 1.0f
: (x / fPotionT1MagMul) / magicEffect->mData.mBaseCost;
float duration = (magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration)
? 1.0f
: (x / fPotionT1DurMult) / magicEffect->mData.mBaseCost;
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude))
applyTools(magicEffect->mData.mFlags, magnitude);
if (!(magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration))
applyTools(magicEffect->mData.mFlags, duration);
duration = roundf(duration);
magnitude = roundf(magnitude);
if (magnitude > 0 && duration > 0)
{
ESM::ENAMstruct effect;
effect.mEffectID = iter->mId;
effect.mAttribute = -1;
effect.mSkill = -1;
if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill)
effect.mSkill = iter->mArg;
else if (magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute)
effect.mAttribute = iter->mArg;
effect.mRange = 0;
effect.mArea = 0;
effect.mDuration = static_cast<int>(duration);
effect.mMagnMin = effect.mMagnMax = static_cast<int>(magnitude);
mEffects.push_back(effect);
}
}
}
const ESM::Potion* MWMechanics::Alchemy::getRecord(const ESM::Potion& toFind) const
{
const MWWorld::Store<ESM::Potion>& potions = MWBase::Environment::get().getWorld()->getStore().get<ESM::Potion>();
MWWorld::Store<ESM::Potion>::iterator iter = potions.begin();
for (; iter != potions.end(); ++iter)
{
if (iter->mEffects.mList.size() != mEffects.size())
continue;
if (iter->mName != toFind.mName || iter->mScript != toFind.mScript
|| iter->mData.mWeight != toFind.mData.mWeight || iter->mData.mValue != toFind.mData.mValue
|| iter->mData.mAutoCalc != toFind.mData.mAutoCalc)
continue;
// Don't choose an ID that came from the content files, would have unintended side effects
// where alchemy can be used to produce quest-relevant items
if (!potions.isDynamic(iter->mId))
continue;
bool mismatch = false;
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 = 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)
{
mismatch = true;
break;
}
}
if (!mismatch)
return &(*iter);
}
return nullptr;
}
void MWMechanics::Alchemy::removeIngredients()
{
for (TIngredientsContainer::iterator iter(mIngredients.begin()); iter != mIngredients.end(); ++iter)
if (!iter->isEmpty())
{
iter->getContainerStore()->remove(*iter, 1, mAlchemist);
if (iter->getRefData().getCount() < 1)
*iter = MWWorld::Ptr();
}
updateEffects();
}
void MWMechanics::Alchemy::addPotion(const std::string& name)
{
ESM::Potion newRecord;
newRecord.mData.mWeight = 0;
for (TIngredientsIterator iter(beginIngredients()); iter != endIngredients(); ++iter)
if (!iter->isEmpty())
newRecord.mData.mWeight += iter->get<ESM::Ingredient>()->mBase->mData.mWeight;
if (countIngredients() > 0)
newRecord.mData.mWeight /= countIngredients();
newRecord.mData.mValue = mValue;
newRecord.mData.mAutoCalc = 0;
newRecord.mName = name;
auto& prng = MWBase::Environment::get().getWorld()->getPrng();
int index = Misc::Rng::rollDice(6, prng);
assert(index >= 0 && index < 6);
static const char* meshes[] = { "standard", "bargain", "cheap", "fresh", "exclusive", "quality" };
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;
const ESM::Potion* record = getRecord(newRecord);
if (!record)
record = MWBase::Environment::get().getWorld()->createRecord(newRecord);
mAlchemist.getClass().getContainerStore(mAlchemist).add(record->mId, 1, mAlchemist);
}
void MWMechanics::Alchemy::increaseSkill()
{
mAlchemist.getClass().skillUsageSucceeded(mAlchemist, ESM::Skill::Alchemy, 0);
}
float MWMechanics::Alchemy::getAlchemyFactor() const
{
const CreatureStats& creatureStats = mAlchemist.getClass().getCreatureStats(mAlchemist);
return (mAlchemist.getClass().getSkill(mAlchemist, ESM::Skill::Alchemy)
+ 0.1f * creatureStats.getAttribute(ESM::Attribute::Intelligence).getModified()
+ 0.1f * creatureStats.getAttribute(ESM::Attribute::Luck).getModified());
}
int MWMechanics::Alchemy::countIngredients() const
{
int ingredients = 0;
for (TIngredientsIterator iter(beginIngredients()); iter != endIngredients(); ++iter)
if (!iter->isEmpty())
++ingredients;
return ingredients;
}
int MWMechanics::Alchemy::countPotionsToBrew() const
{
Result readyStatus = getReadyStatus();
if (readyStatus != Result_Success)
return 0;
int toBrew = -1;
for (TIngredientsIterator iter(beginIngredients()); iter != endIngredients(); ++iter)
if (!iter->isEmpty())
{
int count = iter->getRefData().getCount();
if ((count > 0 && count < toBrew) || toBrew < 0)
toBrew = count;
}
return toBrew;
}
void MWMechanics::Alchemy::setAlchemist(const MWWorld::Ptr& npc)
{
mAlchemist = npc;
mIngredients.resize(4);
std::fill(mIngredients.begin(), mIngredients.end(), MWWorld::Ptr());
mTools.resize(4);
std::fill(mTools.begin(), mTools.end(), MWWorld::Ptr());
mEffects.clear();
MWWorld::ContainerStore& store = npc.getClass().getContainerStore(npc);
for (MWWorld::ContainerStoreIterator iter(store.begin(MWWorld::ContainerStore::Type_Apparatus));
iter != store.end(); ++iter)
{
MWWorld::LiveCellRef<ESM::Apparatus>* ref = iter->get<ESM::Apparatus>();
int type = ref->mBase->mData.mType;
if (type < 0 || type >= static_cast<int>(mTools.size()))
throw std::runtime_error("invalid apparatus type");
if (!mTools[type].isEmpty())
if (ref->mBase->mData.mQuality <= mTools[type].get<ESM::Apparatus>()->mBase->mData.mQuality)
continue;
mTools[type] = *iter;
}
}
MWMechanics::Alchemy::TToolsIterator MWMechanics::Alchemy::beginTools() const
{
return mTools.begin();
}
MWMechanics::Alchemy::TToolsIterator MWMechanics::Alchemy::endTools() const
{
return mTools.end();
}
MWMechanics::Alchemy::TIngredientsIterator MWMechanics::Alchemy::beginIngredients() const
{
return mIngredients.begin();
}
MWMechanics::Alchemy::TIngredientsIterator MWMechanics::Alchemy::endIngredients() const
{
return mIngredients.end();
}
void MWMechanics::Alchemy::clear()
{
mAlchemist = MWWorld::Ptr();
mTools.clear();
mIngredients.clear();
mEffects.clear();
setPotionName("");
}
void MWMechanics::Alchemy::setPotionName(const std::string& name)
{
mPotionName = name;
}
int MWMechanics::Alchemy::addIngredient(const MWWorld::Ptr& ingredient)
{
// find a free slot
int slot = -1;
for (int i = 0; i < static_cast<int>(mIngredients.size()); ++i)
if (mIngredients[i].isEmpty())
{
slot = i;
break;
}
if (slot == -1)
return -1;
for (TIngredientsIterator iter(mIngredients.begin()); iter != mIngredients.end(); ++iter)
if (!iter->isEmpty() && ingredient.getCellRef().getRefId() == iter->getCellRef().getRefId())
return -1;
mIngredients[slot] = ingredient;
updateEffects();
return slot;
}
void MWMechanics::Alchemy::removeIngredient(int index)
{
if (index >= 0 && index < static_cast<int>(mIngredients.size()))
{
mIngredients[index] = MWWorld::Ptr();
updateEffects();
}
}
MWMechanics::Alchemy::TEffectsIterator MWMechanics::Alchemy::beginEffects() const
{
return mEffects.begin();
}
MWMechanics::Alchemy::TEffectsIterator MWMechanics::Alchemy::endEffects() const
{
return mEffects.end();
}
bool MWMechanics::Alchemy::knownEffect(unsigned int potionEffectIndex, const MWWorld::Ptr& npc)
{
float alchemySkill = npc.getClass().getSkill(npc, ESM::Skill::Alchemy);
static const float fWortChanceValue = MWBase::Environment::get()
.getWorld()
->getStore()
.get<ESM::GameSetting>()
.find("fWortChanceValue")
->mValue.getFloat();
return (potionEffectIndex <= 1 && alchemySkill >= fWortChanceValue)
|| (potionEffectIndex <= 3 && alchemySkill >= fWortChanceValue * 2)
|| (potionEffectIndex <= 5 && alchemySkill >= fWortChanceValue * 3)
|| (potionEffectIndex <= 7 && alchemySkill >= fWortChanceValue * 4);
}
MWMechanics::Alchemy::Result MWMechanics::Alchemy::getReadyStatus() const
{
if (mTools[ESM::Apparatus::MortarPestle].isEmpty())
return Result_NoMortarAndPestle;
if (countIngredients() < 2)
return Result_LessThanTwoIngredients;
if (mPotionName.empty())
return Result_NoName;
if (listEffects().empty())
return Result_NoEffects;
return Result_Success;
}
MWMechanics::Alchemy::Result MWMechanics::Alchemy::create(const std::string& name, int& count)
{
setPotionName(name);
Result readyStatus = getReadyStatus();
if (readyStatus == Result_NoEffects)
removeIngredients();
if (readyStatus != Result_Success)
return readyStatus;
Result result = Result_RandomFailure;
int brewedCount = 0;
for (int i = 0; i < count; ++i)
{
if (createSingle() == Result_Success)
{
result = Result_Success;
brewedCount++;
}
}
count = brewedCount;
return result;
}
MWMechanics::Alchemy::Result MWMechanics::Alchemy::createSingle()
{
if (beginEffects() == endEffects())
{
// all effects were nullified due to insufficient skill
removeIngredients();
return Result_RandomFailure;
}
auto& prng = MWBase::Environment::get().getWorld()->getPrng();
if (getAlchemyFactor() < Misc::Rng::roll0to99(prng))
{
removeIngredients();
return Result_RandomFailure;
}
addPotion(mPotionName);
removeIngredients();
increaseSkill();
return Result_Success;
}
std::string MWMechanics::Alchemy::suggestPotionName()
{
std::set<MWMechanics::EffectKey> effects = listEffects();
if (effects.empty())
return "";
int effectId = effects.begin()->mId;
return MWBase::Environment::get()
.getWorld()
->getStore()
.get<ESM::GameSetting>()
.find(ESM::MagicEffect::effectIdToString(effectId))
->mValue.getString();
}
std::vector<std::string> MWMechanics::Alchemy::effectsDescription(const MWWorld::ConstPtr& ptr, const int alchemySkill)
{
std::vector<std::string> effects;
const auto& item = ptr.get<ESM::Ingredient>()->mBase;
const auto& gmst = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();
const static auto fWortChanceValue = gmst.find("fWortChanceValue")->mValue.getFloat();
const auto& data = item->mData;
for (auto i = 0; i < 4; ++i)
{
const auto effectID = data.mEffectID[i];
const auto skillID = data.mSkills[i];
const auto attributeID = data.mAttributes[i];
if (alchemySkill < fWortChanceValue * (i + 1))
break;
if (effectID != -1)
{
std::string effect = gmst.find(ESM::MagicEffect::effectIdToString(effectID))->mValue.getString();
if (skillID != -1)
effect += " " + gmst.find(ESM::Skill::sSkillNameIds[skillID])->mValue.getString();
else if (attributeID != -1)
effect += " " + gmst.find(ESM::Attribute::sGmstAttributeIds[attributeID])->mValue.getString();
effects.push_back(effect);
}
}
return effects;
}