forked from teamnwah/openmw-tes3coop
3f28634d1f
Note, I suspect Rng::rollClosedProbability() is not needed. The only difference between it and rollProbability() is that one time in 37k (on Windows), it will give an output of 1.0. On some versions of Linux, the value of 1.0 will occur about 1 time in 4 billion.
496 lines
14 KiB
C++
496 lines
14 KiB
C++
|
|
#include "alchemy.hpp"
|
|
|
|
#include <cassert>
|
|
#include <cstdlib>
|
|
|
|
#include <algorithm>
|
|
#include <stdexcept>
|
|
#include <map>
|
|
|
|
#include <openengine/misc/rng.hpp>
|
|
|
|
#include <components/esm/loadskil.hpp>
|
|
#include <components/esm/loadappa.hpp>
|
|
#include <components/esm/loadgmst.hpp>
|
|
#include <components/esm/loadmgef.hpp>
|
|
|
|
|
|
#include "../mwbase/environment.hpp"
|
|
#include "../mwbase/world.hpp"
|
|
|
|
#include "../mwworld/esmstore.hpp"
|
|
#include "../mwworld/containerstore.hpp"
|
|
#include "../mwworld/class.hpp"
|
|
#include "../mwworld/cellstore.hpp"
|
|
#include "../mwworld/manualref.hpp"
|
|
|
|
#include "magiceffects.hpp"
|
|
#include "creaturestats.hpp"
|
|
#include "npcstats.hpp"
|
|
|
|
MWMechanics::Alchemy::Alchemy()
|
|
: mValue(0)
|
|
{
|
|
}
|
|
|
|
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>();
|
|
|
|
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]);
|
|
|
|
++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::Retort : ESM::Apparatus::Albemic;
|
|
|
|
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")->getFloat();
|
|
|
|
// value
|
|
mValue = static_cast<int> (
|
|
x * MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find ("iAlchemyMod")->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)
|
|
{
|
|
std::ostringstream os;
|
|
os << "invalid base cost for magic effect " << iter->mId;
|
|
throw std::runtime_error (os.str());
|
|
}
|
|
|
|
float fPotionT1MagMul =
|
|
MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find ("fPotionT1MagMult")->getFloat();
|
|
|
|
if (fPotionT1MagMul<=0)
|
|
throw std::runtime_error ("invalid gmst: fPotionT1MagMul");
|
|
|
|
float fPotionT1DurMult =
|
|
MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find ("fPotionT1DurMult")->getFloat();
|
|
|
|
if (fPotionT1DurMult<=0)
|
|
throw std::runtime_error ("invalid gmst: fPotionT1DurMult");
|
|
|
|
float magnitude = magicEffect->mData.mFlags & ESM::MagicEffect::NoMagnitude ?
|
|
1 : (x / fPotionT1MagMul) / magicEffect->mData.mBaseCost;
|
|
float duration = magicEffect->mData.mFlags & ESM::MagicEffect::NoDuration ?
|
|
1 : (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 0;
|
|
}
|
|
|
|
void MWMechanics::Alchemy::removeIngredients()
|
|
{
|
|
bool needsUpdate = false;
|
|
|
|
for (TIngredientsContainer::iterator iter (mIngredients.begin()); iter!=mIngredients.end(); ++iter)
|
|
if (!iter->isEmpty())
|
|
{
|
|
iter->getContainerStore()->remove(*iter, 1, mAlchemist);
|
|
|
|
if (iter->getRefData().getCount()<1)
|
|
{
|
|
needsUpdate = true;
|
|
*iter = MWWorld::Ptr();
|
|
}
|
|
}
|
|
|
|
if (needsUpdate)
|
|
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;
|
|
|
|
int index = OEngine::Misc::Rng::rollDice(6);
|
|
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);
|
|
const NpcStats& npcStats = mAlchemist.getClass().getNpcStats (mAlchemist);
|
|
|
|
return
|
|
(npcStats.getSkill (ESM::Skill::Alchemy).getModified() +
|
|
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;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
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() && Misc::StringUtils::ciEqual(ingredient.getClass().getId(ingredient),
|
|
iter->getClass().getId(*iter)))
|
|
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();
|
|
}
|
|
|
|
MWMechanics::Alchemy::Result MWMechanics::Alchemy::create (const std::string& name)
|
|
{
|
|
if (mTools[ESM::Apparatus::MortarPestle].isEmpty())
|
|
return Result_NoMortarAndPestle;
|
|
|
|
if (countIngredients()<2)
|
|
return Result_LessThanTwoIngredients;
|
|
|
|
if (name.empty())
|
|
return Result_NoName;
|
|
|
|
if (listEffects().empty())
|
|
return Result_NoEffects;
|
|
|
|
if (beginEffects() == endEffects())
|
|
{
|
|
// all effects were nullified due to insufficient skill
|
|
removeIngredients();
|
|
return Result_RandomFailure;
|
|
}
|
|
|
|
if (getAlchemyFactor() < OEngine::Misc::Rng::roll0to99())
|
|
{
|
|
removeIngredients();
|
|
return Result_RandomFailure;
|
|
}
|
|
|
|
addPotion (name);
|
|
|
|
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))->getString();
|
|
}
|