From 6b00d4ad9155d67ddf3c221b9d1b810e6ff30a86 Mon Sep 17 00:00:00 2001 From: cc9cii Date: Wed, 24 Jun 2015 21:05:59 +1000 Subject: [PATCH] Move NPC autocalc to a separate component so that it can be shared between OpenMW and OpenCS. - Vanilla behaviour mimic'd where possible, except copying over autocalc spells to the npc's spell list when the status changes --- apps/opencs/CMakeLists.txt | 2 +- apps/opencs/model/world/columns.cpp | 4 + apps/opencs/model/world/columns.hpp | 4 + apps/opencs/model/world/data.cpp | 504 +++++++++++++- apps/opencs/model/world/data.hpp | 31 +- apps/opencs/model/world/idtree.cpp | 5 + apps/opencs/model/world/idtree.hpp | 12 +- apps/opencs/model/world/refidadapterimp.cpp | 638 ++++++++---------- apps/opencs/model/world/refidadapterimp.hpp | 119 +--- apps/opencs/model/world/refidcollection.cpp | 21 +- apps/opencs/view/world/dialoguesubview.cpp | 32 + apps/opencs/view/world/dialoguesubview.hpp | 2 + apps/openmw/CMakeLists.txt | 4 +- apps/openmw/mwclass/npc.cpp | 199 +----- apps/openmw/mwgui/levelupdialog.cpp | 4 +- .../mwmechanics/mechanicsmanagerimp.cpp | 10 +- apps/openmw/mwworld/esmstore.cpp | 4 +- apps/openmw/mwworld/esmstore.hpp | 6 +- apps/openmw/mwworld/store.hpp | 96 +-- components/CMakeLists.txt | 24 +- components/gameplay/autocalc.cpp | 207 ++++++ components/gameplay/autocalc.hpp | 47 ++ components/gameplay/autocalcspell.cpp | 240 +++++++ components/gameplay/autocalcspell.hpp | 38 ++ components/gameplay/store.hpp | 138 ++++ 25 files changed, 1631 insertions(+), 760 deletions(-) create mode 100644 components/gameplay/autocalc.cpp create mode 100644 components/gameplay/autocalc.hpp create mode 100644 components/gameplay/autocalcspell.cpp create mode 100644 components/gameplay/autocalcspell.hpp create mode 100644 components/gameplay/store.hpp diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index d495415907..4e7c4123a6 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -26,7 +26,7 @@ opencs_units_noqt (model/world universalid record commands columnbase scriptcontext cell refidcollection refidadapter refiddata refidadapterimp ref collectionbase refcollection columns infocollection tablemimedata cellcoordinates cellselection resources resourcesmanager scope pathgrid landtexture land nestedtablewrapper nestedcollection nestedcoladapterimp nestedinfocollection - idcompletionmanager + idcompletionmanager npcstats ) opencs_hdrs_noqt (model/world diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp index 9491c32469..f3f78d2c69 100644 --- a/apps/opencs/model/world/columns.cpp +++ b/apps/opencs/model/world/columns.cpp @@ -311,6 +311,10 @@ namespace CSMWorld { ColumnId_WaterLevel, "Water Level" }, { ColumnId_MapColor, "Map Color" }, + { ColumnId_SpellSrc, "From Race" }, + { ColumnId_SpellCost, "Cast Cost" }, + { ColumnId_SpellChance, "Cast Chance" }, + { ColumnId_UseValue1, "Use value 1" }, { ColumnId_UseValue2, "Use value 2" }, { ColumnId_UseValue3, "Use value 3" }, diff --git a/apps/opencs/model/world/columns.hpp b/apps/opencs/model/world/columns.hpp index 191bbdea8c..456de27ece 100644 --- a/apps/opencs/model/world/columns.hpp +++ b/apps/opencs/model/world/columns.hpp @@ -302,6 +302,10 @@ namespace CSMWorld ColumnId_WaterLevel = 273, ColumnId_MapColor = 274, + ColumnId_SpellSrc = 275, + ColumnId_SpellCost = 276, + ColumnId_SpellChance = 277, + // Allocated to a separate value range, so we don't get a collision should we ever need // to extend the number of use values. ColumnId_UseValue1 = 0x10000, diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index ffa1ff3c3b..4e393fbc3d 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -11,6 +11,10 @@ #include #include +#include +#include +#include + #include "idtable.hpp" #include "idtree.hpp" #include "columnimp.hpp" @@ -19,6 +23,117 @@ #include "resourcesmanager.hpp" #include "resourcetable.hpp" #include "nestedcoladapterimp.hpp" +#include "npcstats.hpp" + +namespace +{ + class SpellStore : public GamePlay::CommonStore + { + const CSMWorld::NestedIdCollection& mSpells; + std::vector mLocal; + + public: + + SpellStore(const CSMWorld::NestedIdCollection& spells) + : mSpells(spells) + { + // prepare data in a format used by OpenMW store + for (int index = 0; index < mSpells.getSize(); ++index) + { + ESM::Spell *spell = const_cast(&mSpells.getRecord(index).get()); + mLocal.push_back(spell); + } + } + + ~SpellStore() {} + + typedef GamePlay::SharedIterator iterator; + + virtual iterator begin() const + { + return mLocal.begin(); + } + + virtual iterator end() const + { + return mLocal.end(); + } + + virtual const ESM::Spell *find(const std::string &id) const + { + return &mSpells.getRecord(id).get(); + } + + virtual size_t getSize() const + { + return mSpells.getSize(); + } + + private: + // not used in OpenCS + virtual void load(ESM::ESMReader &esm, const std::string &id) + { + } + }; + + class CSStore : public GamePlay::StoreWrap + { + const CSMWorld::IdCollection& mGmstTable; + const CSMWorld::IdCollection& mSkillTable; + const CSMWorld::IdCollection& mMagicEffectTable; + const SpellStore mSpellStore; + + public: + + CSStore(const CSMWorld::IdCollection& gmst, + const CSMWorld::IdCollection& skills, + const CSMWorld::IdCollection& magicEffects, + const CSMWorld::NestedIdCollection& spells) + : mGmstTable(gmst), mSkillTable(skills), mMagicEffectTable(magicEffects), mSpellStore(spells) + { } + ~CSStore() {} + + virtual int findGmstInt(const std::string& name) const + { + return mGmstTable.getRecord(name).get().getInt(); + } + + virtual float findGmstFloat(const std::string& name) const + { + return mGmstTable.getRecord(name).get().getFloat(); + } + + virtual const ESM::Skill *findSkill(int index) const + { + // if the skill does not exist, throws std::runtime_error ("invalid ID: " + id) + return &mSkillTable.getRecord(ESM::Skill::indexToId(index)).get(); + } + + virtual const ESM::MagicEffect* findMagicEffect(int id) const + { + // if the magic effect does not exist, throws std::runtime_error ("invalid ID: " + id) + return &mMagicEffectTable.getRecord(ESM::MagicEffect::indexToId((short)id)).get(); + } + + virtual const GamePlay::CommonStore& getSpells() const + { + return mSpellStore; + } + }; + + unsigned short autoCalculateMana(GamePlay::StatsBase& stats) + { + return stats.getBaseAttribute(ESM::Attribute::Intelligence) * 2; + } + + unsigned short autoCalculateFatigue(GamePlay::StatsBase& stats) + { + return stats.getBaseAttribute(ESM::Attribute::Strength) + + stats.getBaseAttribute(ESM::Attribute::Willpower) + + stats.getBaseAttribute(ESM::Attribute::Agility) + + stats.getBaseAttribute(ESM::Attribute::Endurance); + } +} void CSMWorld::Data::addModel (QAbstractItemModel *model, UniversalId::Type type, bool update) { @@ -203,7 +318,7 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, const ResourcesManager& resourc mSpells.addColumn (new RecordStateColumn); mSpells.addColumn (new FixedRecordTypeColumn (UniversalId::Type_Spell)); mSpells.addColumn (new NameColumn); - mSpells.addColumn (new SpellTypeColumn); + mSpells.addColumn (new SpellTypeColumn); // ColumnId_SpellType mSpells.addColumn (new CostColumn); mSpells.addColumn (new FlagColumn (Columns::ColumnId_AutoCalc, 0x1)); mSpells.addColumn (new FlagColumn (Columns::ColumnId_StarterSpell, 0x2)); @@ -517,11 +632,40 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, const ResourcesManager& resourc addModel (new ResourceTable (&mResourcesManager.get (UniversalId::Type_Videos)), UniversalId::Type_Video); + // for autocalc updates when gmst/race/class/skils tables change + CSMWorld::IdTable *gmsts = + static_cast(getTableModel(UniversalId::Type_Gmst)); + CSMWorld::IdTable *skills = + static_cast(getTableModel(UniversalId::Type_Skill)); + CSMWorld::IdTable *classes = + static_cast(getTableModel(UniversalId::Type_Class)); + CSMWorld::IdTree *races = + static_cast(getTableModel(UniversalId::Type_Race)); + CSMWorld::IdTree *objects = + static_cast(getTableModel(UniversalId::Type_Referenceable)); + + connect (gmsts, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (gmstDataChanged (const QModelIndex&, const QModelIndex&))); + connect (skills, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (skillDataChanged (const QModelIndex&, const QModelIndex&))); + connect (classes, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (classDataChanged (const QModelIndex&, const QModelIndex&))); + connect (races, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (raceDataChanged (const QModelIndex&, const QModelIndex&))); + connect (objects, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (npcDataChanged (const QModelIndex&, const QModelIndex&))); + connect (this, SIGNAL (updateNpcAutocalc (int, const std::string&)), + objects, SLOT (updateNpcAutocalc (int, const std::string&))); + connect (this, SIGNAL (cacheNpcStats (const std::string&, NpcStats*)), + this, SLOT (cacheNpcStatsEvent (const std::string&, NpcStats*))); + mRefLoadCache.clear(); // clear here rather than startLoading() and continueLoading() for multiple content files } CSMWorld::Data::~Data() { + clearNpcStatsCache(); + for (std::vector::iterator iter (mModels.begin()); iter!=mModels.end(); ++iter) delete *iter; @@ -1165,3 +1309,361 @@ const CSMWorld::Data& CSMWorld::Data::self () { return *this; } + +void CSMWorld::Data::skillDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +{ + // mData.mAttribute (affects attributes skill bonus autocalc) + // mData.mSpecialization (affects skills autocalc) + CSMWorld::IdTable *skillModel = + static_cast(getTableModel(CSMWorld::UniversalId::Type_Skill)); + + int attributeColumn = skillModel->findColumnIndex(CSMWorld::Columns::ColumnId_Attribute); + int specialisationColumn = skillModel->findColumnIndex(CSMWorld::Columns::ColumnId_Specialisation); + + if ((topLeft.column() <= attributeColumn && attributeColumn <= bottomRight.column()) + || (topLeft.column() <= specialisationColumn && specialisationColumn <= bottomRight.column())) + { + clearNpcStatsCache(); + + std::string empty; + emit updateNpcAutocalc(0/*all*/, empty); + } +} + +void CSMWorld::Data::classDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +{ + // update autocalculated attributes/skills of every NPC with matching class + // - mData.mAttribute[2] + // - mData.mSkills[5][2] + // - mData.mSpecialization + CSMWorld::IdTable *classModel = + static_cast(getTableModel(CSMWorld::UniversalId::Type_Class)); + + int attribute1Column = classModel->findColumnIndex(CSMWorld::Columns::ColumnId_Attribute1); // +1 + int majorSkill1Column = classModel->findColumnIndex(CSMWorld::Columns::ColumnId_MajorSkill1); // +4 + int minorSkill1Column = classModel->findColumnIndex(CSMWorld::Columns::ColumnId_MinorSkill1); // +4 + int specialisationColumn = classModel->findColumnIndex(CSMWorld::Columns::ColumnId_Specialisation); + + if ((topLeft.column() > attribute1Column+1 || attribute1Column > bottomRight.column()) + && (topLeft.column() > majorSkill1Column+4 || majorSkill1Column > bottomRight.column()) + && (topLeft.column() > minorSkill1Column+4 || minorSkill1Column > bottomRight.column()) + && (topLeft.column() > specialisationColumn || specialisationColumn > bottomRight.column())) + { + return; + } + + // get the affected class + int idColumn = classModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id); + for (int classRow = topLeft.row(); classRow <= bottomRight.row(); ++classRow) + { + clearNpcStatsCache(); + + std::string classId = + classModel->data(classModel->index(classRow, idColumn)).toString().toUtf8().constData(); + emit updateNpcAutocalc(1/*class*/, classId); + } +} + +void CSMWorld::Data::raceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +{ + // affects racial bonus attributes & skills + // - mData.mAttributeValues[] + // - mData.mBonus[].mBonus + CSMWorld::IdTree *raceModel = + static_cast(getTableModel(CSMWorld::UniversalId::Type_Race)); + + int attrColumn = raceModel->findColumnIndex(CSMWorld::Columns::ColumnId_RaceAttributes); + int bonusColumn = raceModel->findColumnIndex(CSMWorld::Columns::ColumnId_RaceSkillBonus); + + bool match = false; + if (topLeft.parent().isValid() && bottomRight.parent().isValid()) + { + if ((topLeft.parent().column() <= attrColumn && attrColumn <= bottomRight.parent().column()) + || (topLeft.parent().column() <= bonusColumn && bonusColumn <= bottomRight.parent().column())) + { + match = true; // TODO: check for specific nested column? + } + } + else + { + if ((topLeft.column() <= attrColumn && attrColumn <= bottomRight.column()) + || (topLeft.column() <= bonusColumn && bonusColumn <= bottomRight.column())) + { + match = true; // maybe the whole table changed + } + } + + if (!match) + return; + + // update autocalculated attributes/skills of every NPC with matching race + int idColumn = raceModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id); + for (int raceRow = topLeft.parent().row(); raceRow <= bottomRight.parent().row(); ++raceRow) + { + clearNpcStatsCache(); + + std::string raceId = + raceModel->data(raceModel->index(raceRow, idColumn)).toString().toUtf8().constData(); + emit updateNpcAutocalc(2/*race*/, raceId); + } +} + +// FIXME: currently ignoring level changes +void CSMWorld::Data::npcDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +{ + clearNpcStatsCache(); + + // Either autoCalc flag changed or NPC level changed + CSMWorld::IdTree *objectModel = + static_cast(getTableModel(CSMWorld::UniversalId::Type_Referenceable)); + + int autoCalcColumn = objectModel->findColumnIndex(CSMWorld::Columns::ColumnId_AutoCalc); + int miscColumn = objectModel->findColumnIndex(CSMWorld::Columns::ColumnId_NpcMisc); + + // first check for level + bool levelChanged = false; + if (topLeft.parent().isValid() && bottomRight.parent().isValid()) + { + if (topLeft.parent().column() <= miscColumn && miscColumn <= bottomRight.parent().column()) + { + for (int col = topLeft.column(); col <= bottomRight.column(); ++col) + { + int role = objectModel->nestedHeaderData(topLeft.parent().column(), + col, Qt::Horizontal, CSMWorld::ColumnBase::Role_ColumnId).toInt(); + if (role == CSMWorld::Columns::ColumnId_NpcLevel) + { + levelChanged = true; + break; + } + } + } + } + + // next check for autocalc + bool autoCalcChanged = false; + if (!topLeft.parent().isValid() && !bottomRight.parent().isValid()) + { + if ((topLeft.column() <= autoCalcColumn && autoCalcColumn <= bottomRight.column()) + || (topLeft.column() <= miscColumn && miscColumn <= bottomRight.column())) + { + autoCalcChanged = true; + } + } + + if (!levelChanged && !autoCalcChanged) + return; + + int row = 0; + int end = 0; + if (topLeft.parent().isValid()) + row = topLeft.parent().row(); + else + row = topLeft.row(); + + if (bottomRight.parent().isValid()) + end = bottomRight.parent().row(); + else + end = bottomRight.row(); + + for (; row <= end; ++row) + { + Record record = + static_cast&>(mReferenceables.getRecord(row)); + ESM::NPC &npc = record.get(); + + // If going from autocalc to non-autocalc, save the autocalc values + if (autoCalcChanged) + { + if (npc.mNpdtType != ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) + saveAutoCalcValues(npc); // update attributes and skills + else + npc.mNpdt12.mLevel = npc.mNpdt52.mLevel; // for NPC's loaded as non-autocalc + + record.setModified(npc); + mReferenceables.replace(row, record); + } + } +} + +void CSMWorld::Data::gmstDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +{ + static const QStringList gmsts(QStringList()<< "fNPCbaseMagickaMult" << "fAutoSpellChance" + << "fEffectCostMult" << "iAutoSpellAlterationMax" << "iAutoSpellConjurationMax" + << "iAutoSpellDestructionMax" << "iAutoSpellIllusionMax" << "iAutoSpellMysticismMax" + << "iAutoSpellRestorationMax" << "iAutoSpellTimesCanCast" << "iAutoSpellAttSkillMin"); + + bool match = false; + for (int row = topLeft.row(); row <= bottomRight.row(); ++row) + { + if (gmsts.contains(mGmsts.getRecord(row).get().mId.c_str())) + { + match = true; + break; + } + } + + if (!match) + return; + + clearNpcStatsCache(); + + std::string empty; + emit updateNpcAutocalc(0/*all*/, empty); +} + +// FIXME: how to undo? +void CSMWorld::Data::saveAutoCalcValues(ESM::NPC& npc) +{ + CSMWorld::NpcStats * cachedStats = getCachedNpcData (npc.mId); + if (!cachedStats) + return; // silently fail + + CSMWorld::NpcStats* stats = npcAutoCalculate(npc); + + // update npc + npc.mNpdt52.mLevel = npc.mNpdt12.mLevel; + + npc.mNpdt52.mStrength = stats->getBaseAttribute(ESM::Attribute::Strength); + npc.mNpdt52.mIntelligence = stats->getBaseAttribute(ESM::Attribute::Intelligence); + npc.mNpdt52.mWillpower = stats->getBaseAttribute(ESM::Attribute::Willpower); + npc.mNpdt52.mAgility = stats->getBaseAttribute(ESM::Attribute::Agility); + npc.mNpdt52.mSpeed = stats->getBaseAttribute(ESM::Attribute::Speed); + npc.mNpdt52.mEndurance = stats->getBaseAttribute(ESM::Attribute::Endurance); + npc.mNpdt52.mPersonality = stats->getBaseAttribute(ESM::Attribute::Personality); + npc.mNpdt52.mLuck = stats->getBaseAttribute(ESM::Attribute::Luck); + + for (int i = 0; i < ESM::Skill::Length; ++i) + { + npc.mNpdt52.mSkills[i] = stats->getBaseSkill(i); + } + + npc.mNpdt52.mHealth = stats->getHealth(); + npc.mNpdt52.mMana = stats->getMana(); + npc.mNpdt52.mFatigue = stats->getFatigue(); + npc.mNpdt52.mDisposition = npc.mNpdt12.mDisposition; + npc.mNpdt52.mReputation = npc.mNpdt12.mReputation; + npc.mNpdt52.mRank = npc.mNpdt12.mRank; + npc.mNpdt52.mGold = npc.mNpdt12.mGold; + + // TODO: add spells from autogenerated list like vanilla (but excluding any race powers or abilities) +} + +void CSMWorld::Data::clearNpcStatsCache () +{ + for (std::map::iterator it (mNpcStatCache.begin()); + it != mNpcStatCache.end(); ++it) + delete it->second; + + mNpcStatCache.clear(); +} + +CSMWorld::NpcStats* CSMWorld::Data::npcAutoCalculate(const ESM::NPC& npc) const +{ + CSMWorld::NpcStats * cachedStats = getCachedNpcData (npc.mId); + if (cachedStats) + return cachedStats; + + const ESM::Race *race = &mRaces.getRecord(npc.mRace).get(); + const ESM::Class *class_ = &mClasses.getRecord(npc.mClass).get(); + + bool autoCalc = npc.mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS; + short level = npc.mNpdt52.mLevel; + if (autoCalc) + level = npc.mNpdt12.mLevel; + + CSMWorld::NpcStats *stats = new CSMWorld::NpcStats(); + + CSStore store(mGmsts, mSkills, mMagicEffects, mSpells); + + if (autoCalc) + { + GamePlay::autoCalcAttributesImpl (&npc, race, class_, level, *stats, &store); + + stats->setHealth(autoCalculateHealth(level, class_, *stats)); + stats->setMana(autoCalculateMana(*stats)); + stats->setFatigue(autoCalculateFatigue(*stats)); + + GamePlay::autoCalcSkillsImpl(&npc, race, class_, level, *stats, &store); + + GamePlay::autoCalculateSpells(race, *stats, &store); + } + else + { + for (std::vector::const_iterator it = npc.mSpells.mList.begin(); + it != npc.mSpells.mList.end(); ++it) + { + stats->addSpells(*it); + } + } + + // update spell info + const std::vector &racePowers = race->mPowers.mList; + for (unsigned int i = 0; i < racePowers.size(); ++i) + { + int type = -1; + int spellIndex = mSpells.searchId(racePowers[i]); + if (spellIndex != -1) + type = mSpells.getRecord(spellIndex).get().mData.mType; + stats->addPowers(racePowers[i], type); + } + // cost/chance + int skills[ESM::Skill::Length]; + if (autoCalc) + for (int i = 0; i< ESM::Skill::Length; ++i) + skills[i] = stats->getBaseSkill(i); + else + for (int i = 0; i< ESM::Skill::Length; ++i) + skills[i] = npc.mNpdt52.mSkills[i]; + + int attributes[ESM::Attribute::Length]; + if (autoCalc) + for (int i = 0; i< ESM::Attribute::Length; ++i) + attributes[i] = stats->getBaseAttribute(i); + else + { + attributes[ESM::Attribute::Strength] = npc.mNpdt52.mStrength; + attributes[ESM::Attribute::Willpower] = npc.mNpdt52.mWillpower; + attributes[ESM::Attribute::Agility] = npc.mNpdt52.mAgility; + attributes[ESM::Attribute::Speed] = npc.mNpdt52.mSpeed; + attributes[ESM::Attribute::Endurance] = npc.mNpdt52.mEndurance; + attributes[ESM::Attribute::Personality] = npc.mNpdt52.mPersonality; + attributes[ESM::Attribute::Luck] = npc.mNpdt52.mLuck; + } + + const std::vector& spells = stats->spells(); + for (std::vector::const_iterator it = spells.begin(); it != spells.end(); ++it) + { + int cost = -1; + int spellIndex = mSpells.searchId((*it).mName); + const ESM::Spell* spell = 0; + if (spellIndex != -1) + { + spell = &mSpells.getRecord(spellIndex).get(); + cost = spell->mData.mCost; + + int school; + float skillTerm; + GamePlay::calcWeakestSchool(spell, skills, school, skillTerm, &store); + float chance = calcAutoCastChance(spell, skills, attributes, school, &store); + + stats->addCostAndChance((*it).mName, cost, (int)ceil(chance)); // percent + } + } + + emit cacheNpcStats (npc.mId, stats); + return stats; +} + +void CSMWorld::Data::cacheNpcStatsEvent (const std::string& id, CSMWorld::NpcStats *stats) +{ + mNpcStatCache[id] = stats; +} + +CSMWorld::NpcStats* CSMWorld::Data::getCachedNpcData (const std::string& id) const +{ + std::map::const_iterator it = mNpcStatCache.find(id); + if (it != mNpcStatCache.end()) + return it->second; + else + return 0; +} diff --git a/apps/opencs/model/world/data.hpp b/apps/opencs/model/world/data.hpp index f83f9de227..fdc76fd739 100644 --- a/apps/opencs/model/world/data.hpp +++ b/apps/opencs/model/world/data.hpp @@ -60,6 +60,7 @@ namespace CSMWorld { class ResourcesManager; class Resources; + class NpcStats; class Data : public QObject { @@ -108,6 +109,8 @@ namespace CSMWorld std::vector > mReaders; + std::map mNpcStatCache; + // not implemented Data (const Data&); Data& operator= (const Data&); @@ -121,7 +124,11 @@ namespace CSMWorld static int count (RecordBase::State state, const CollectionBase& collection); - const CSMWorld::Data& self (); + const Data& self (); + + void saveAutoCalcValues(ESM::NPC& npc); + + void clearNpcStatsCache (); public: @@ -277,15 +284,37 @@ namespace CSMWorld std::string getAuthor() const; + NpcStats* npcAutoCalculate (const ESM::NPC& npc) const; + + NpcStats* getCachedNpcData (const std::string& id) const; + signals: void idListChanged(); + // refresh NPC dialogue subviews via object table model + void updateNpcAutocalc (int type, const std::string& id); + + void cacheNpcStats (const std::string& id, NpcStats *stats) const; + private slots: void dataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); void rowsChanged (const QModelIndex& parent, int start, int end); + + // for autocalc updates when gmst/race/class/skils tables change + void gmstDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + void raceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + void classDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + void skillDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + void npcDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + void cacheNpcStatsEvent (const std::string& id, NpcStats *stats); }; } diff --git a/apps/opencs/model/world/idtree.cpp b/apps/opencs/model/world/idtree.cpp index 1e81d6ac2f..fba6721086 100644 --- a/apps/opencs/model/world/idtree.cpp +++ b/apps/opencs/model/world/idtree.cpp @@ -261,3 +261,8 @@ CSMWorld::NestedTableWrapperBase* CSMWorld::IdTree::nestedTable(const QModelInde return mNestedCollection->nestedTable(index.row(), index.column()); } + +void CSMWorld::IdTree::updateNpcAutocalc (int type, const std::string& id) +{ + emit refreshNpcDialogue (type, id); +} diff --git a/apps/opencs/model/world/idtree.hpp b/apps/opencs/model/world/idtree.hpp index 5337ed82b8..b29a3ae93d 100644 --- a/apps/opencs/model/world/idtree.hpp +++ b/apps/opencs/model/world/idtree.hpp @@ -73,11 +73,17 @@ namespace CSMWorld virtual bool hasChildren (const QModelIndex& index) const; - signals: + signals: - void resetStart(const QString& id); + void resetStart(const QString& id); - void resetEnd(const QString& id); + void resetEnd(const QString& id); + + void refreshNpcDialogue (int type, const std::string& id); + + public slots: + + void updateNpcAutocalc (int type, const std::string& id); }; } diff --git a/apps/opencs/model/world/refidadapterimp.cpp b/apps/opencs/model/world/refidadapterimp.cpp index 86f0461af1..085c77ca37 100644 --- a/apps/opencs/model/world/refidadapterimp.cpp +++ b/apps/opencs/model/world/refidadapterimp.cpp @@ -9,191 +9,8 @@ #include "nestedtablewrapper.hpp" #include "usertype.hpp" - -namespace -{ - -int is_even(double d) -{ - double int_part; - - modf(d / 2.0, &int_part); - return 2.0 * int_part == d; -} - -int round_ieee_754(double d) -{ - double i = floor(d); - d -= i; - - if(d < 0.5) - return static_cast(i); - if(d > 0.5) - return static_cast(i) + 1; - if(is_even(i)) - return static_cast(i); - return static_cast(i) + 1; -} - -std::vector autoCalculateAttributes (const ESM::NPC &npc, - const ESM::Race& race, const ESM::Class& class_, const CSMWorld::IdCollection& skillTable) -{ - // race bonus - bool male = (npc.mFlags & ESM::NPC::Female) == 0; - - if (npc.mNpdtType != ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) - return std::vector(); - - short level = npc.mNpdt12.mLevel; - - int attr[ESM::Attribute::Length]; - - for (int i = 0; i < ESM::Attribute::Length; ++i) - { - const ESM::Race::MaleFemale& attribute = race.mData.mAttributeValues[i]; - attr[i] = male ? attribute.mMale : attribute.mFemale; - } - - // class bonus - for (int i = 0; i < 2; ++i) - { - int attribute = class_.mData.mAttribute[i]; - if (attribute >= 0 && attribute < ESM::Attribute::Length) - { - attr[attribute] = attr[attribute] + 10; - } - // else log an error - } - - std::vector result(ESM::Attribute::Length); - // skill bonus - for (int attribute = 0; attribute < ESM::Attribute::Length; ++attribute) - { - float modifierSum = 0; - - for (int j = 0; j < ESM::Skill::Length; ++j) - { - // if the skill does not exist, throws std::runtime_error ("invalid ID: " + id) - const ESM::Skill& skill = skillTable.getRecord(ESM::Skill::indexToId(j)).get(); - - if (skill.mData.mAttribute != attribute) - continue; - - // is this a minor or major skill? - float add = 0.2f; - for (int k = 0; k < 5; ++k) - { - if (class_.mData.mSkills[k][0] == j) - add = 0.5; - } - for (int k = 0; k < 5; ++k) - { - if (class_.mData.mSkills[k][1] == j) - add = 1.0; - } - modifierSum += add; - } - result[attribute] = std::min(round_ieee_754(attr[attribute] + (level-1) * modifierSum), 100); - } - - return result; -} - -std::vector autoCalculateSkills (const ESM::NPC &npc, - const ESM::Race& race, const ESM::Class& class_, const CSMWorld::IdCollection& skillTable) -{ - unsigned char skills[ESM::Skill::Length]; - for (int i = 0; i < ESM::Skill::Length; ++i) - skills[i] = 0; - - for (int i = 0; i < 2; ++i) - { - int bonus = (i == 0) ? 10 : 25; - - for (int i2 = 0; i2 < 5; ++i2) - { - int index = class_.mData.mSkills[i2][i]; - if (index >= 0 && index < ESM::Skill::Length) - { - skills[index] = bonus; - } - } - } - - std::vector result(ESM::Skill::Length); - for (int skillIndex = 0; skillIndex < ESM::Skill::Length; ++skillIndex) - { - float majorMultiplier = 0.1f; - float specMultiplier = 0.0f; - - int raceBonus = 0; - int specBonus = 0; - - for (int raceSkillIndex = 0; raceSkillIndex < 7; ++raceSkillIndex) - { - if (race.mData.mBonus[raceSkillIndex].mSkill == skillIndex) - { - raceBonus = race.mData.mBonus[raceSkillIndex].mBonus; - break; - } - } - - for (int k = 0; k < 5; ++k) - { - // is this a minor or major skill? - if ((class_.mData.mSkills[k][0] == skillIndex) || (class_.mData.mSkills[k][1] == skillIndex)) - { - majorMultiplier = 1.0f; - break; - } - } - - // is this skill in the same Specialization as the class? - const ESM::Skill& skill = skillTable.getRecord(ESM::Skill::indexToId(skillIndex)).get(); - if (skill.mData.mSpecialization == class_.mData.mSpecialization) - { - specMultiplier = 0.5f; - specBonus = 5; - } - - // Must gracefully handle level 0 - result[skillIndex] = std::min(round_ieee_754(skills[skillIndex] + 5 + raceBonus + specBonus + - (npc.mNpdt12.mLevel-1) * (majorMultiplier + specMultiplier)), 100); - } - - return result; -} - -unsigned short autoCalculateHealth(const ESM::NPC &npc, - const ESM::Class& class_, const std::vector& attr) -{ - int multiplier = 3; - - if (class_.mData.mSpecialization == ESM::Class::Combat) - multiplier += 2; - else if (class_.mData.mSpecialization == ESM::Class::Stealth) - multiplier += 1; - - if (class_.mData.mAttribute[0] == ESM::Attribute::Endurance - || class_.mData.mAttribute[1] == ESM::Attribute::Endurance) - multiplier += 1; - - return floor(0.5f * (attr[ESM::Attribute::Strength]+ attr[ESM::Attribute::Endurance])) - + multiplier * (npc.mNpdt12.mLevel-1); -} - -unsigned short autoCalculateMana(const std::vector& attr) -{ - return attr[ESM::Attribute::Intelligence] * 2; -} - -unsigned short autoCalculateFatigue(const std::vector& attr) -{ - return attr[ESM::Attribute::Strength] + attr[ESM::Attribute::Willpower] - + attr[ESM::Attribute::Agility] + attr[ESM::Attribute::Endurance]; -} - -} +#include "idtree.hpp" +#include "npcstats.hpp" CSMWorld::PotionColumns::PotionColumns (const InventoryColumns& columns) : InventoryColumns (columns) {} @@ -741,12 +558,8 @@ CSMWorld::NpcColumns::NpcColumns (const ActorColumns& actorColumns) mMisc(NULL) {} -CSMWorld::NpcRefIdAdapter::NpcRefIdAdapter (const NpcColumns& columns, - const CSMWorld::IdCollection& races, - const CSMWorld::IdCollection& classes, - const CSMWorld::IdCollection& skills) -: ActorRefIdAdapter (UniversalId::Type_Npc, columns), mColumns (columns), - mRaceTable(races), mClassTable(classes), mSkillTable(skills) +CSMWorld::NpcRefIdAdapter::NpcRefIdAdapter (const NpcColumns& columns) +: ActorRefIdAdapter (UniversalId::Type_Npc, columns), mColumns (columns) {} QVariant CSMWorld::NpcRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) @@ -821,46 +634,8 @@ void CSMWorld::NpcRefIdAdapter::setData (const RefIdColumn *column, RefIdData& d npc.mFlags &= ~iter->second; if (iter->second == ESM::NPC::Autocalc) - { - if(value.toInt() != 0) - { - npc.mNpdtType = ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS; - - // if the race/class does not exist, throws std::runtime_error ("invalid ID: " + id) - const ESM::Race& race = mRaceTable.getRecord(npc.mRace).get(); - const ESM::Class& class_ = mClassTable.getRecord(npc.mClass).get(); - std::vector attr = autoCalculateAttributes(npc, race, class_, mSkillTable); - if (attr.empty()) - return; - - std::vector skills = autoCalculateSkills(npc, race, class_, mSkillTable); - - ESM::NPC::NPDTstruct52& npcStruct = npc.mNpdt52; - - npcStruct.mLevel = npc.mNpdt12.mLevel; - npcStruct.mStrength = attr[ESM::Attribute::Strength]; - npcStruct.mIntelligence = attr[ESM::Attribute::Intelligence]; - npcStruct.mWillpower = attr[ESM::Attribute::Willpower]; - npcStruct.mAgility = attr[ESM::Attribute::Agility]; - npcStruct.mSpeed = attr[ESM::Attribute::Speed]; - npcStruct.mEndurance = attr[ESM::Attribute::Endurance]; - npcStruct.mPersonality = attr[ESM::Attribute::Personality]; - npcStruct.mLuck = attr[ESM::Attribute::Luck]; - for (int i = 0; i < ESM::Skill::Length; ++i) - { - npcStruct.mSkills[i] = skills[i]; - } - npcStruct.mHealth = autoCalculateHealth(npc, class_, attr); - npcStruct.mMana = autoCalculateMana(attr); - npcStruct.mFatigue = autoCalculateFatigue(attr); - npcStruct.mDisposition = npc.mNpdt12.mDisposition; - npcStruct.mReputation = npc.mNpdt12.mReputation; - npcStruct.mRank = npc.mNpdt12.mRank; - npcStruct.mGold = npc.mNpdt12.mGold; - } - else - npc.mNpdtType = ESM::NPC::NPC_DEFAULT; - } + npc.mNpdtType = (value.toInt() != 0) ? ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS + : ESM::NPC::NPC_DEFAULT; } else { @@ -873,9 +648,7 @@ void CSMWorld::NpcRefIdAdapter::setData (const RefIdColumn *column, RefIdData& d record.setModified (npc); } -CSMWorld::NpcAttributesRefIdAdapter::NpcAttributesRefIdAdapter(const CSMWorld::IdCollection& races, - const CSMWorld::IdCollection& classes, const CSMWorld::IdCollection& skills) - : mRaceTable(races), mClassTable(classes), mSkillTable(skills) +CSMWorld::NpcAttributesRefIdAdapter::NpcAttributesRefIdAdapter(const CSMWorld::Data& data) : mData(data) {} void CSMWorld::NpcAttributesRefIdAdapter::addNestedRow (const RefIdColumn *column, @@ -940,27 +713,20 @@ QVariant CSMWorld::NpcAttributesRefIdAdapter::getNestedData (const RefIdColumn * default: return QVariant(); // throw an exception here? } else if (subColIndex == 1) - // It may be possible to have mNpdt52 values different to autocalculated ones when - // first loaded, so re-calculate if (npc.mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) { - const ESM::Race& race = mRaceTable.getRecord(npc.mRace).get(); - const ESM::Class& class_ = mClassTable.getRecord(npc.mClass).get(); - std::vector attr = autoCalculateAttributes(npc, race, class_, mSkillTable); - - if (attr.empty()) - return QVariant(); + CSMWorld::NpcStats *stats = mData.npcAutoCalculate(npc); switch (subRowIndex) { - case 0: return static_cast(attr[ESM::Attribute::Strength]); - case 1: return static_cast(attr[ESM::Attribute::Intelligence]); - case 2: return static_cast(attr[ESM::Attribute::Willpower]); - case 3: return static_cast(attr[ESM::Attribute::Agility]); - case 4: return static_cast(attr[ESM::Attribute::Speed]); - case 5: return static_cast(attr[ESM::Attribute::Endurance]); - case 6: return static_cast(attr[ESM::Attribute::Personality]); - case 7: return static_cast(attr[ESM::Attribute::Luck]); + case 0: return static_cast(stats->getBaseAttribute(ESM::Attribute::Strength)); + case 1: return static_cast(stats->getBaseAttribute(ESM::Attribute::Intelligence)); + case 2: return static_cast(stats->getBaseAttribute(ESM::Attribute::Willpower)); + case 3: return static_cast(stats->getBaseAttribute(ESM::Attribute::Agility)); + case 4: return static_cast(stats->getBaseAttribute(ESM::Attribute::Speed)); + case 5: return static_cast(stats->getBaseAttribute(ESM::Attribute::Endurance)); + case 6: return static_cast(stats->getBaseAttribute(ESM::Attribute::Personality)); + case 7: return static_cast(stats->getBaseAttribute(ESM::Attribute::Luck)); default: return QVariant(); // throw an exception here? } } @@ -1019,10 +785,8 @@ int CSMWorld::NpcAttributesRefIdAdapter::getNestedRowsCount(const RefIdColumn *c return 8; } -CSMWorld::NpcSkillsRefIdAdapter::NpcSkillsRefIdAdapter(const CSMWorld::IdCollection& races, - const CSMWorld::IdCollection& classes, - const CSMWorld::IdCollection& skills) - : mRaceTable(races), mClassTable(classes), mSkillTable(skills) +CSMWorld::NpcSkillsRefIdAdapter::NpcSkillsRefIdAdapter(const CSMWorld::Data& data) + : mData(data) {} void CSMWorld::NpcSkillsRefIdAdapter::addNestedRow (const RefIdColumn *column, @@ -1079,18 +843,10 @@ QVariant CSMWorld::NpcSkillsRefIdAdapter::getNestedData (const RefIdColumn *colu return QString(ESM::Skill::sSkillNames[subRowIndex].c_str()); else if (subColIndex == 1) { - // It may be possible to have mNpdt52 values different to autocalculated ones when - // first loaded, so re-calculate if (npc.mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) { - // if the race/class does not exist, throws std::runtime_error ("invalid ID: " + id) - const ESM::Race& race = mRaceTable.getRecord(npc.mRace).get(); - const ESM::Class& class_ = mClassTable.getRecord(npc.mClass).get(); - std::vector skills = autoCalculateSkills(npc, race, class_, mSkillTable); - - int value = static_cast(skills[subRowIndex]); - - return static_cast(skills[subRowIndex]); + CSMWorld::NpcStats *stats = mData.npcAutoCalculate(npc); + return static_cast(stats->getBaseSkill(subRowIndex)); } else { @@ -1132,10 +888,7 @@ int CSMWorld::NpcSkillsRefIdAdapter::getNestedRowsCount(const RefIdColumn *colum return ESM::Skill::Length; } -CSMWorld::NpcMiscRefIdAdapter::NpcMiscRefIdAdapter(const CSMWorld::IdCollection& races, - const CSMWorld::IdCollection& classes, - const CSMWorld::IdCollection& skills) - : mRaceTable(races), mClassTable(classes), mSkillTable(skills) +CSMWorld::NpcMiscRefIdAdapter::NpcMiscRefIdAdapter(const CSMWorld::Data& data) : mData(data) {} CSMWorld::NpcMiscRefIdAdapter::~NpcMiscRefIdAdapter() @@ -1175,17 +928,9 @@ QVariant CSMWorld::NpcMiscRefIdAdapter::getNestedData (const RefIdColumn *column bool autoCalc = (npc.mFlags & ESM::NPC::Autocalc) != 0; - // It may be possible to have mNpdt52 values different to autocalculated ones when - // first loaded, so re-calculate if (autoCalc) { - // if the race/class does not exist, throws std::runtime_error ("invalid ID: " + id) - const ESM::Race& race = mRaceTable.getRecord(npc.mRace).get(); - const ESM::Class& class_ = mClassTable.getRecord(npc.mClass).get(); - std::vector attr = autoCalculateAttributes(npc, race, class_, mSkillTable); - - if (attr.empty()) - return QVariant(); + CSMWorld::NpcStats *stats = mData.npcAutoCalculate(npc); switch (subColIndex) { @@ -1197,17 +942,17 @@ QVariant CSMWorld::NpcMiscRefIdAdapter::getNestedData (const RefIdColumn *column } case 2: { - UserInt i(autoCalculateHealth(npc, class_, attr)); + UserInt i(stats->getHealth()); return QVariant(QVariant::fromValue(i)); } case 3: { - UserInt i(autoCalculateMana(attr)); + UserInt i(stats->getMana()); return QVariant(QVariant::fromValue(i)); } case 4: { - UserInt i(autoCalculateFatigue(attr)); + UserInt i(stats->getFatigue()); return QVariant(QVariant::fromValue(i)); } case 5: return static_cast(record.get().mNpdt12.mDisposition); @@ -1247,108 +992,31 @@ void CSMWorld::NpcMiscRefIdAdapter::setNestedData (const RefIdColumn *column, if (autoCalc) switch(subColIndex) { - case 0: - { - npc.mNpdt12.mLevel = static_cast(value.toInt()); break; - - const ESM::Race& race = mRaceTable.getRecord(npc.mRace).get(); - const ESM::Class& class_ = mClassTable.getRecord(npc.mClass).get(); - std::vector attr = autoCalculateAttributes(npc, race, class_, mSkillTable); - if (attr.empty()) - return; - - ESM::NPC::NPDTstruct52& npcStruct = npc.mNpdt52; - - std::vector skills = autoCalculateSkills(npc, race, class_, mSkillTable); - - npcStruct.mLevel = npc.mNpdt12.mLevel; - npcStruct.mStrength = attr[ESM::Attribute::Strength]; - npcStruct.mIntelligence = attr[ESM::Attribute::Intelligence]; - npcStruct.mWillpower = attr[ESM::Attribute::Willpower]; - npcStruct.mAgility = attr[ESM::Attribute::Agility]; - npcStruct.mSpeed = attr[ESM::Attribute::Speed]; - npcStruct.mEndurance = attr[ESM::Attribute::Endurance]; - npcStruct.mPersonality = attr[ESM::Attribute::Personality]; - npcStruct.mLuck = attr[ESM::Attribute::Luck]; - for (int i = 0; i < ESM::Skill::Length; ++i) - { - npcStruct.mSkills[i] = skills[i]; - } - npcStruct.mHealth = autoCalculateHealth(npc, class_, attr); - npcStruct.mMana = autoCalculateMana(attr); - npcStruct.mFatigue = autoCalculateFatigue(attr); - - break; - } + case 0: npc.mNpdt12.mLevel = static_cast(value.toInt()); break; case 1: return; case 2: return; case 3: return; case 4: return; - case 5: - { - npc.mNpdt12.mDisposition = static_cast(value.toInt()); - npc.mNpdt52.mDisposition = npc.mNpdt12.mDisposition; - break; - } - case 6: - { - npc.mNpdt12.mReputation = static_cast(value.toInt()); - npc.mNpdt52.mReputation = npc.mNpdt12.mReputation; - break; - } - case 7: - { - npc.mNpdt12.mRank = static_cast(value.toInt()); - npc.mNpdt52.mRank = npc.mNpdt12.mRank; - break; - } - case 8: - { - npc.mNpdt12.mGold = value.toInt(); - npc.mNpdt52.mGold = npc.mNpdt12.mGold; - break; - } - case 9: npc.mPersistent = value.toBool(); break; + case 5: npc.mNpdt12.mDisposition = static_cast(value.toInt()); break; + case 6: npc.mNpdt12.mReputation = static_cast(value.toInt()); break; + case 7: npc.mNpdt12.mRank = static_cast(value.toInt()); break; + case 8: npc.mNpdt12.mGold = value.toInt(); break; + case 9: npc.mPersistent = value.toBool(); break; default: return; // throw an exception here? } else switch(subColIndex) { - case 0: - { - npc.mNpdt52.mLevel = static_cast(value.toInt()); - npc.mNpdt12.mLevel = npc.mNpdt52.mLevel; - break; - } - case 1: npc.mNpdt52.mFactionID = static_cast(value.toInt()); break; - case 2: npc.mNpdt52.mHealth = static_cast(value.toInt()); break; - case 3: npc.mNpdt52.mMana = static_cast(value.toInt()); break; - case 4: npc.mNpdt52.mFatigue = static_cast(value.toInt()); break; - case 5: - { - npc.mNpdt52.mDisposition = static_cast(value.toInt()); - npc.mNpdt12.mDisposition = npc.mNpdt52.mDisposition; - break; - } - case 6: - { - npc.mNpdt52.mReputation = static_cast(value.toInt()); - npc.mNpdt12.mReputation = npc.mNpdt52.mReputation; - break; - } - case 7: - { - npc.mNpdt52.mRank = static_cast(value.toInt()); - npc.mNpdt12.mRank = npc.mNpdt52.mRank; - break; - } - case 8: - { - npc.mNpdt52.mGold = value.toInt(); - npc.mNpdt12.mGold = npc.mNpdt52.mGold; - break; - } - case 9: npc.mPersistent = value.toBool(); break; + case 0: npc.mNpdt52.mLevel = static_cast(value.toInt()); break; + case 1: npc.mNpdt52.mFactionID = static_cast(value.toInt()); break; + case 2: npc.mNpdt52.mHealth = static_cast(value.toInt()); break; + case 3: npc.mNpdt52.mMana = static_cast(value.toInt()); break; + case 4: npc.mNpdt52.mFatigue = static_cast(value.toInt()); break; + case 5: npc.mNpdt52.mDisposition = static_cast(value.toInt()); break; + case 6: npc.mNpdt52.mReputation = static_cast(value.toInt()); break; + case 7: npc.mNpdt52.mRank = static_cast(value.toInt()); break; + case 8: npc.mNpdt52.mGold = value.toInt(); break; + case 9: npc.mPersistent = value.toBool(); break; default: return; // throw an exception here? } @@ -1453,3 +1121,227 @@ void CSMWorld::WeaponRefIdAdapter::setData (const RefIdColumn *column, RefIdData EnchantableRefIdAdapter::setData (column, data, index, value); } } + +void CSMWorld::NestedSpellRefIdAdapter::addNestedRow (const RefIdColumn *column, + RefIdData& data, int index, int position) const +{ + Record& record = + static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + + if (record.get().mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) + return; // can't edit autocalculated spells + + ESM::NPC caster = record.get(); + + std::vector& list = caster.mSpells.mList; + + std::string newString; + + if (position >= (int)list.size()) + list.push_back(newString); + else + list.insert(list.begin()+position, newString); + + record.setModified (caster); +} + +void CSMWorld::NestedSpellRefIdAdapter::removeNestedRow (const RefIdColumn *column, + RefIdData& data, int index, int rowToRemove) const +{ + Record& record = + static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + + if (record.get().mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) + return; // can't edit autocalculated spells + + ESM::NPC caster = record.get(); + + std::vector& list = caster.mSpells.mList; + + // avoid race power rows + int size = 0; + int raceIndex = mData.getRaces().searchId(caster.mRace); + if (raceIndex != -1) + size = mData.getRaces().getRecord(raceIndex).get().mPowers.mList.size(); + + if (rowToRemove < 0 || rowToRemove >= static_cast (list.size() + size)) + throw std::runtime_error ("index out of range"); + + if (rowToRemove >= static_cast(list.size()) && rowToRemove < static_cast(list.size() + size)) + return; // hack, assumes the race powers are added at the end + + list.erase (list.begin () + rowToRemove); + + record.setModified (caster); +} + +void CSMWorld::NestedSpellRefIdAdapter::setNestedData (const RefIdColumn *column, + RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const +{ + Record& record = + static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); + + if (record.get().mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) + return; // can't edit autocalculated spells + + ESM::NPC caster = record.get(); + std::vector& list = caster.mSpells.mList; + + // avoid race power rows + int size = 0; + int raceIndex = mData.getRaces().searchId(caster.mRace); + if (raceIndex != -1) + size = mData.getRaces().getRecord(raceIndex).get().mPowers.mList.size(); + + if (subRowIndex < 0 || subRowIndex >= static_cast (list.size() + size)) + throw std::runtime_error ("index out of range"); + + if (subRowIndex >= static_cast(list.size()) && subRowIndex < static_cast(list.size() + size)) + return; // hack, assumes the race powers are added at the end + + if (subColIndex == 0) + list.at(subRowIndex) = std::string(value.toString().toUtf8()); + else + throw std::runtime_error("Trying to access non-existing column in the nested table!"); + + record.setModified (caster); +} + +QVariant CSMWorld::NestedSpellRefIdAdapter::getNestedData (const RefIdColumn *column, + const RefIdData& data, int index, int subRowIndex, int subColIndex) const +{ + const Record& record = + static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + + const std::vector& spells = mData.npcAutoCalculate(record.get())->spells(); + + if (subRowIndex < 0 || subRowIndex >= static_cast (spells.size())) + throw std::runtime_error ("index out of range"); + + switch (subColIndex) + { + case 0: return QString::fromUtf8(spells[subRowIndex].mName.c_str()); + case 1: return spells[subRowIndex].mType; + case 2: return spells[subRowIndex].mFromRace; + case 3: return spells[subRowIndex].mCost; + case 4: return spells[subRowIndex].mChance; + default: + throw std::runtime_error("Trying to access non-existing column in the nested table!"); + } +} + +int CSMWorld::NestedSpellRefIdAdapter::getNestedColumnsCount(const RefIdColumn *column, + const RefIdData& data) const +{ + return 5; +} + +int CSMWorld::NestedSpellRefIdAdapter::getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const +{ + const Record& record = + static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + + const std::vector spells = mData.npcAutoCalculate(record.get())->spells(); + return static_cast(spells.size()); +} + +template <> +void CSMWorld::NestedSpellRefIdAdapter::addNestedRow (const RefIdColumn *column, + RefIdData& data, int index, int position) const +{ + Record& record = + static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + ESM::Creature caster = record.get(); + + std::vector& list = caster.mSpells.mList; + + std::string newString; + + if (position >= (int)list.size()) + list.push_back(newString); + else + list.insert(list.begin()+position, newString); + + record.setModified (caster); +} + +template <> +void CSMWorld::NestedSpellRefIdAdapter::removeNestedRow (const RefIdColumn *column, + RefIdData& data, int index, int rowToRemove) const +{ + Record& record = + static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + ESM::Creature caster = record.get(); + + std::vector& list = caster.mSpells.mList; + + if (rowToRemove < 0 || rowToRemove >= static_cast (list.size())) + throw std::runtime_error ("index out of range"); + + list.erase (list.begin () + rowToRemove); + + record.setModified (caster); +} + +template <> +void CSMWorld::NestedSpellRefIdAdapter::setNestedData (const RefIdColumn *column, + RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const +{ + Record& record = + static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); + ESM::Creature caster = record.get(); + std::vector& list = caster.mSpells.mList; + + if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) + throw std::runtime_error ("index out of range"); + + if (subColIndex == 0) + list.at(subRowIndex) = std::string(value.toString().toUtf8()); + else + throw std::runtime_error("Trying to access non-existing column in the nested table!"); + + record.setModified (caster); +} + +template<> +QVariant CSMWorld::NestedSpellRefIdAdapter::getNestedData (const RefIdColumn *column, + const RefIdData& data, int index, int subRowIndex, int subColIndex) const +{ + const Record& record = + static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + + const std::vector& list = record.get().mSpells.mList; + + if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) + throw std::runtime_error ("index out of range"); + + const std::string& content = list.at(subRowIndex); + + int type = -1; + int spellIndex = mData.getSpells().searchId(content); + if (spellIndex != -1) + type = mData.getSpells().getRecord(spellIndex).get().mData.mType; + + if (subColIndex == 0) + return QString::fromUtf8(content.c_str()); + else if (subColIndex == 1) + return type; + else + throw std::runtime_error("Trying to access non-existing column in the nested table!"); +} + +template <> +int CSMWorld::NestedSpellRefIdAdapter::getNestedColumnsCount(const RefIdColumn *column, + const RefIdData& data) const +{ + return 2; +} + +template <> +int CSMWorld::NestedSpellRefIdAdapter::getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const +{ + const Record& record = + static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); + + return static_cast(record.get().mSpells.mList.size()); +} diff --git a/apps/opencs/model/world/refidadapterimp.hpp b/apps/opencs/model/world/refidadapterimp.hpp index b12f84d5e5..acdc124ebf 100644 --- a/apps/opencs/model/world/refidadapterimp.hpp +++ b/apps/opencs/model/world/refidadapterimp.hpp @@ -21,6 +21,7 @@ #include "refidadapter.hpp" #include "nestedtablewrapper.hpp" #include "idcollection.hpp" +#include "data.hpp" namespace CSMWorld { @@ -806,16 +807,10 @@ namespace CSMWorld class NpcRefIdAdapter : public ActorRefIdAdapter { NpcColumns mColumns; - const IdCollection& mRaceTable; - const IdCollection& mClassTable; - const IdCollection& mSkillTable; public: - NpcRefIdAdapter (const NpcColumns& columns, - const IdCollection& races, - const IdCollection& classes, - const IdCollection& skills); + NpcRefIdAdapter (const NpcColumns& columns); virtual QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const; @@ -860,15 +855,11 @@ namespace CSMWorld class NpcAttributesRefIdAdapter : public NestedRefIdAdapterBase { - const IdCollection& mRaceTable; - const IdCollection& mClassTable; - const IdCollection& mSkillTable; + const Data& mData; public: - NpcAttributesRefIdAdapter (const IdCollection& races, - const IdCollection& classes, - const IdCollection& skills); + NpcAttributesRefIdAdapter (const Data& data); virtual void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const; @@ -895,15 +886,11 @@ namespace CSMWorld class NpcSkillsRefIdAdapter : public NestedRefIdAdapterBase { - const IdCollection& mRaceTable; - const IdCollection& mClassTable; - const IdCollection& mSkillTable; + const Data& mData; public: - NpcSkillsRefIdAdapter (const IdCollection& races, - const IdCollection& classes, - const IdCollection& skills); + NpcSkillsRefIdAdapter (const Data& data); virtual void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const; @@ -930,18 +917,14 @@ namespace CSMWorld class NpcMiscRefIdAdapter : public NestedRefIdAdapterBase { - const IdCollection& mRaceTable; - const IdCollection& mClassTable; - const IdCollection& mSkillTable; + const Data& mData; NpcMiscRefIdAdapter (const NpcMiscRefIdAdapter&); NpcMiscRefIdAdapter& operator= (const NpcMiscRefIdAdapter&); public: - NpcMiscRefIdAdapter (const IdCollection& races, - const IdCollection& classes, - const IdCollection& skills); + NpcMiscRefIdAdapter (const Data& data); virtual ~NpcMiscRefIdAdapter(); virtual void addNestedRow (const RefIdColumn *column, @@ -1189,6 +1172,7 @@ namespace CSMWorld class NestedSpellRefIdAdapter : public NestedRefIdAdapterBase { UniversalId::Type mType; + const Data& mData; // not implemented NestedSpellRefIdAdapter (const NestedSpellRefIdAdapter&); @@ -1196,45 +1180,15 @@ namespace CSMWorld public: - NestedSpellRefIdAdapter(UniversalId::Type type) :mType(type) {} + NestedSpellRefIdAdapter(UniversalId::Type type, const Data& data) :mType(type), mData(data) {} virtual ~NestedSpellRefIdAdapter() {} virtual void addNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int position) const - { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); - ESXRecordT caster = record.get(); - - std::vector& list = caster.mSpells.mList; - - std::string newString; - - if (position >= (int)list.size()) - list.push_back(newString); - else - list.insert(list.begin()+position, newString); - - record.setModified (caster); - } + RefIdData& data, int index, int position) const; virtual void removeNestedRow (const RefIdColumn *column, - RefIdData& data, int index, int rowToRemove) const - { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); - ESXRecordT caster = record.get(); - - std::vector& list = caster.mSpells.mList; - - if (rowToRemove < 0 || rowToRemove >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); - - list.erase (list.begin () + rowToRemove); - - record.setModified (caster); - } + RefIdData& data, int index, int rowToRemove) const; virtual void setNestedTable (const RefIdColumn* column, RefIdData& data, int index, const NestedTableWrapperBase& nestedTable) const @@ -1260,55 +1214,14 @@ namespace CSMWorld } virtual QVariant getNestedData (const RefIdColumn *column, - const RefIdData& data, int index, int subRowIndex, int subColIndex) const - { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); - - const std::vector& list = record.get().mSpells.mList; - - if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); - - const std::string& content = list.at(subRowIndex); - - if (subColIndex == 0) - return QString::fromUtf8(content.c_str()); - else - throw std::runtime_error("Trying to access non-existing column in the nested table!"); - } + const RefIdData& data, int index, int subRowIndex, int subColIndex) const; virtual void setNestedData (const RefIdColumn *column, - RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const - { - Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (row, mType))); - ESXRecordT caster = record.get(); - std::vector& list = caster.mSpells.mList; + RefIdData& data, int row, const QVariant& value, int subRowIndex, int subColIndex) const; - if (subRowIndex < 0 || subRowIndex >= static_cast (list.size())) - throw std::runtime_error ("index out of range"); + virtual int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const; - if (subColIndex == 0) - list.at(subRowIndex) = std::string(value.toString().toUtf8()); - else - throw std::runtime_error("Trying to access non-existing column in the nested table!"); - - record.setModified (caster); - } - - virtual int getNestedColumnsCount(const RefIdColumn *column, const RefIdData& data) const - { - return 1; - } - - virtual int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const - { - const Record& record = - static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); - - return static_cast(record.get().mSpells.mList.size()); - } + virtual int getNestedRowsCount(const RefIdColumn *column, const RefIdData& data, int index) const; }; template diff --git a/apps/opencs/model/world/refidcollection.cpp b/apps/opencs/model/world/refidcollection.cpp index 739e9cec49..d5232db63f 100644 --- a/apps/opencs/model/world/refidcollection.cpp +++ b/apps/opencs/model/world/refidcollection.cpp @@ -146,12 +146,21 @@ CSMWorld::RefIdCollection::RefIdCollection(const CSMWorld::Data& data) actorsColumns.mSpells = &mColumns.back(); std::map spellsMap; spellsMap.insert(std::make_pair(UniversalId::Type_Npc, - new NestedSpellRefIdAdapter (UniversalId::Type_Npc))); + new NestedSpellRefIdAdapter (UniversalId::Type_Npc, data))); spellsMap.insert(std::make_pair(UniversalId::Type_Creature, - new NestedSpellRefIdAdapter (UniversalId::Type_Creature))); + new NestedSpellRefIdAdapter (UniversalId::Type_Creature, data))); mNestedAdapters.push_back (std::make_pair(&mColumns.back(), spellsMap)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_SpellId, CSMWorld::ColumnBase::Display_Spell)); + mColumns.back().addColumn( + new RefIdColumn (Columns::ColumnId_SpellType, CSMWorld::ColumnBase::Display_SpellType, false/*editable*/, false/*user editable*/)); + // creatures do not have below columns + mColumns.back().addColumn( + new RefIdColumn (Columns::ColumnId_SpellSrc, CSMWorld::ColumnBase::Display_YesNo, false, false)); // from race + mColumns.back().addColumn( + new RefIdColumn (Columns::ColumnId_SpellCost, CSMWorld::ColumnBase::Display_Integer, false, false)); + mColumns.back().addColumn( + new RefIdColumn (Columns::ColumnId_SpellChance, CSMWorld::ColumnBase::Display_Integer/*Percent*/, false, false)); // Nested table mColumns.push_back(RefIdColumn (Columns::ColumnId_NpcDestinations, @@ -438,7 +447,7 @@ CSMWorld::RefIdCollection::RefIdCollection(const CSMWorld::Data& data) ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue)); npcColumns.mAttributes = &mColumns.back(); std::map attrMap; - attrMap.insert(std::make_pair(UniversalId::Type_Npc, new NpcAttributesRefIdAdapter(data.getRaces(), data.getClasses(), data.getSkills()))); + attrMap.insert(std::make_pair(UniversalId::Type_Npc, new NpcAttributesRefIdAdapter(data))); mNestedAdapters.push_back (std::make_pair(&mColumns.back(), attrMap)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_NpcAttributes, CSMWorld::ColumnBase::Display_String, false, false)); @@ -450,7 +459,7 @@ CSMWorld::RefIdCollection::RefIdCollection(const CSMWorld::Data& data) ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue)); npcColumns.mSkills = &mColumns.back(); std::map skillsMap; - skillsMap.insert(std::make_pair(UniversalId::Type_Npc, new NpcSkillsRefIdAdapter(data.getRaces(), data.getClasses(), data.getSkills()))); + skillsMap.insert(std::make_pair(UniversalId::Type_Npc, new NpcSkillsRefIdAdapter(data))); mNestedAdapters.push_back (std::make_pair(&mColumns.back(), skillsMap)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_NpcSkills, CSMWorld::ColumnBase::Display_String, false, false)); @@ -462,7 +471,7 @@ CSMWorld::RefIdCollection::RefIdCollection(const CSMWorld::Data& data) ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_List)); npcColumns.mMisc = &mColumns.back(); std::map miscMap; - miscMap.insert(std::make_pair(UniversalId::Type_Npc, new NpcMiscRefIdAdapter(data.getRaces(), data.getClasses(), data.getSkills()))); + miscMap.insert(std::make_pair(UniversalId::Type_Npc, new NpcMiscRefIdAdapter(data))); mNestedAdapters.push_back (std::make_pair(&mColumns.back(), miscMap)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_NpcLevel, CSMWorld::ColumnBase::Display_Integer, @@ -611,7 +620,7 @@ CSMWorld::RefIdCollection::RefIdCollection(const CSMWorld::Data& data) mAdapters.insert (std::make_pair (UniversalId::Type_Miscellaneous, new MiscRefIdAdapter (inventoryColumns, key))); mAdapters.insert (std::make_pair (UniversalId::Type_Npc, - new NpcRefIdAdapter (npcColumns, data.getRaces(), data.getClasses(), data.getSkills()))); + new NpcRefIdAdapter (npcColumns))); mAdapters.insert (std::make_pair (UniversalId::Type_Probe, new ToolRefIdAdapter (UniversalId::Type_Probe, toolsColumns))); mAdapters.insert (std::make_pair (UniversalId::Type_Repair, diff --git a/apps/opencs/view/world/dialoguesubview.cpp b/apps/opencs/view/world/dialoguesubview.cpp index f11079ac41..34e310a49b 100644 --- a/apps/opencs/view/world/dialoguesubview.cpp +++ b/apps/opencs/view/world/dialoguesubview.cpp @@ -678,6 +678,14 @@ CSVWorld::DialogueSubView::DialogueSubView (const CSMWorld::UniversalId& id, CSM connect(mEditWidget, SIGNAL(tableMimeDataDropped(QWidget*, const QModelIndex&, const CSMWorld::UniversalId&, const CSMDoc::Document*)), this, SLOT(tableMimeDataDropped(QWidget*, const QModelIndex&, const CSMWorld::UniversalId&, const CSMDoc::Document*))); + if (id.getType() == CSMWorld::UniversalId::Type_Referenceable) + { + CSMWorld::IdTree *objectTable = static_cast(mTable); + + connect (objectTable, SIGNAL (refreshNpcDialogue (int, const std::string&)), + this, SLOT (refreshNpcDialogue (int, const std::string&))); + } + mMainLayout->addWidget(mEditWidget); mEditWidget->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); @@ -897,3 +905,27 @@ void CSVWorld::DialogueSubView::changeCurrentId (const std::string& newId) selection.push_back(mCurrentId); mCommandDispatcher.setSelection(selection); } + +void CSVWorld::DialogueSubView::refreshNpcDialogue (int type, const std::string& id) +{ + int typeColumn = mTable->findColumnIndex (CSMWorld::Columns::ColumnId_RecordType); + if (CSMWorld::UniversalId::Type_Npc + != mTable->data(mTable->getModelIndex(mCurrentId, typeColumn), Qt::DisplayRole).toInt()) + { + return; + } + + int raceColumn = mTable->findColumnIndex (CSMWorld::Columns::ColumnId_Race); + int classColumn = mTable->findColumnIndex (CSMWorld::Columns::ColumnId_Class); + + if ((type == 0/*FIXME*/ && id == "") // skill or gmst changed + || (id == mTable->data(mTable->getModelIndex(mCurrentId, raceColumn), + Qt::DisplayRole).toString().toUtf8().constData()) // race + || (id == mTable->data(mTable->getModelIndex(mCurrentId, classColumn), + Qt::DisplayRole).toString().toUtf8().constData())) // class + { + int y = mEditWidget->verticalScrollBar()->value(); + mEditWidget->remake (mTable->getModelIndex(mCurrentId, 0).row()); + mEditWidget->verticalScrollBar()->setValue(y); + } +} diff --git a/apps/opencs/view/world/dialoguesubview.hpp b/apps/opencs/view/world/dialoguesubview.hpp index 6cbd8ad778..28b2671557 100644 --- a/apps/opencs/view/world/dialoguesubview.hpp +++ b/apps/opencs/view/world/dialoguesubview.hpp @@ -237,6 +237,8 @@ namespace CSVWorld void requestFocus (const std::string& id); void rowsAboutToBeRemoved(const QModelIndex &parent, int start, int end); + + void refreshNpcDialogue (int type, const std::string& id); }; } diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index c7c701d20e..d541521006 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -66,7 +66,7 @@ add_openmw_dir (mwworld cells localscripts customdata weather inventorystore ptr actionopen actionread actionequip timestamp actionalchemy cellstore actionapply actioneat esmstore store recordcmp fallback actionrepair actionsoulgem livecellref actiondoor - contentloader esmloader actiontrap cellreflist projectilemanager cellref + contentloader esmloader actiontrap cellreflist projectilemanager cellref mwstore ) add_openmw_dir (mwclass @@ -78,7 +78,7 @@ add_openmw_dir (mwmechanics mechanicsmanagerimp stat character creaturestats magiceffects movement actors objects drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor aiescort aiactivate aicombat repair enchanting pathfinding pathgrid security spellsuccess spellcasting - disease pickpocket levelledlist combat steering obstacle autocalcspell difficultyscaling aicombataction actor summoning + disease pickpocket levelledlist combat steering obstacle difficultyscaling aicombataction actor summoning ) add_openmw_dir (mwstate diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp index 3ca57aca88..4f419c6359 100644 --- a/apps/openmw/mwclass/npc.cpp +++ b/apps/openmw/mwclass/npc.cpp @@ -11,6 +11,10 @@ #include #include +#include +#include +#include + #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" #include "../mwbase/mechanicsmanager.hpp" @@ -24,7 +28,6 @@ #include "../mwmechanics/spellcasting.hpp" #include "../mwmechanics/disease.hpp" #include "../mwmechanics/combat.hpp" -#include "../mwmechanics/autocalcspell.hpp" #include "../mwmechanics/difficultyscaling.hpp" #include "../mwmechanics/character.hpp" @@ -36,6 +39,7 @@ #include "../mwworld/customdata.hpp" #include "../mwworld/physicssystem.hpp" #include "../mwworld/cellstore.hpp" +#include "../mwworld/mwstore.hpp" #include "../mwrender/actors.hpp" #include "../mwrender/renderinginterface.hpp" @@ -58,116 +62,47 @@ namespace return new NpcCustomData (*this); } - int is_even(double d) { - double int_part; - modf(d / 2.0, &int_part); - return 2.0 * int_part == d; - } + class Stats : public GamePlay::StatsBase + { + MWMechanics::CreatureStats& mCreatureStats; + MWMechanics::NpcStats& mNpcStats; - int round_ieee_754(double d) { - double i = floor(d); - d -= i; - if(d < 0.5) - return static_cast(i); - if(d > 0.5) - return static_cast(i) + 1; - if(is_even(i)) - return static_cast(i); - return static_cast(i) + 1; - } + public: + + Stats(MWMechanics::CreatureStats& creatureStats, MWMechanics::NpcStats& npcStats) + : mCreatureStats(creatureStats), mNpcStats(npcStats) {} + + virtual unsigned char getBaseAttribute(int index) const { return mCreatureStats.getAttribute(index).getBase(); } + + virtual void setAttribute(int index, unsigned char value) { mCreatureStats.setAttribute(index, value); } + + virtual void addSpells(std::string id) { mCreatureStats.getSpells().add(id); } + + virtual unsigned char getBaseSkill(int index) const { return mNpcStats.getSkill(index).getBase(); } + + virtual void setBaseSkill(int index, unsigned char value) { mNpcStats.getSkill(index).setBase(value); } + }; void autoCalculateAttributes (const ESM::NPC* npc, MWMechanics::CreatureStats& creatureStats) { - // race bonus const ESM::Race *race = MWBase::Environment::get().getWorld()->getStore().get().find(npc->mRace); - bool male = (npc->mFlags & ESM::NPC::Female) == 0; - - int level = creatureStats.getLevel(); - for (int i=0; imData.mAttributeValues[i]; - creatureStats.setAttribute(i, male ? attribute.mMale : attribute.mFemale); - } - - // class bonus const ESM::Class *class_ = MWBase::Environment::get().getWorld()->getStore().get().find(npc->mClass); - for (int i=0; i<2; ++i) - { - int attribute = class_->mData.mAttribute[i]; - if (attribute>=0 && attribute<8) - { - creatureStats.setAttribute(attribute, - creatureStats.getAttribute(attribute).getBase() + 10); - } - } + int level = creatureStats.getLevel(); - // skill bonus - for (int attribute=0; attribute < ESM::Attribute::Length; ++attribute) - { - float modifierSum = 0; + MWMechanics::NpcStats dummy; // npc stats are needed for skills, which is not calculated here + Stats stats(creatureStats, dummy); - for (int j=0; jgetStore().get().find(j); + MWWorld::MWStore store; - if (skill->mData.mAttribute != attribute) - continue; + GamePlay::autoCalcAttributesImpl (npc, race, class_, level, stats, &store); - // is this a minor or major skill? - float add=0.2f; - for (int k=0; k<5; ++k) - { - if (class_->mData.mSkills[k][0] == j) - add=0.5; - } - for (int k=0; k<5; ++k) - { - if (class_->mData.mSkills[k][1] == j) - add=1.0; - } - modifierSum += add; - } - creatureStats.setAttribute(attribute, std::min( - round_ieee_754(creatureStats.getAttribute(attribute).getBase() - + (level-1) * modifierSum), 100) ); - } - - // initial health - int strength = creatureStats.getAttribute(ESM::Attribute::Strength).getBase(); - int endurance = creatureStats.getAttribute(ESM::Attribute::Endurance).getBase(); - - int multiplier = 3; - - if (class_->mData.mSpecialization == ESM::Class::Combat) - multiplier += 2; - else if (class_->mData.mSpecialization == ESM::Class::Stealth) - multiplier += 1; - - if (class_->mData.mAttribute[0] == ESM::Attribute::Endurance - || class_->mData.mAttribute[1] == ESM::Attribute::Endurance) - multiplier += 1; - - creatureStats.setHealth(floor(0.5f * (strength + endurance)) + multiplier * (creatureStats.getLevel() - 1)); + creatureStats.setHealth(GamePlay::autoCalculateHealth(level, class_, stats)); } - /** - * @brief autoCalculateSkills - * - * Skills are calculated with following formulae ( http://www.uesp.net/wiki/Morrowind:NPCs#Skills ): - * - * Skills: (Level - 1) × (Majority Multiplier + Specialization Multiplier) - * - * The Majority Multiplier is 1.0 for a Major or Minor Skill, or 0.1 for a Miscellaneous Skill. - * - * The Specialization Multiplier is 0.5 for a Skill in the same Specialization as the class, - * zero for other Skills. - * - * and by adding class, race, specialization bonus. - */ void autoCalculateSkills(const ESM::NPC* npc, MWMechanics::NpcStats& npcStats, const MWWorld::Ptr& ptr) { const ESM::Class *class_ = @@ -177,77 +112,13 @@ namespace const ESM::Race *race = MWBase::Environment::get().getWorld()->getStore().get().find(npc->mRace); + Stats stats(npcStats, npcStats); - for (int i = 0; i < 2; ++i) - { - int bonus = (i==0) ? 10 : 25; + MWWorld::MWStore store; - for (int i2 = 0; i2 < 5; ++i2) - { - int index = class_->mData.mSkills[i2][i]; - if (index >= 0 && index < ESM::Skill::Length) - { - npcStats.getSkill(index).setBase (npcStats.getSkill(index).getBase() + bonus); - } - } - } + GamePlay::autoCalcSkillsImpl(npc, race, class_, level, stats, &store); - for (int skillIndex = 0; skillIndex < ESM::Skill::Length; ++skillIndex) - { - float majorMultiplier = 0.1f; - float specMultiplier = 0.0f; - - int raceBonus = 0; - int specBonus = 0; - - for (int raceSkillIndex = 0; raceSkillIndex < 7; ++raceSkillIndex) - { - if (race->mData.mBonus[raceSkillIndex].mSkill == skillIndex) - { - raceBonus = race->mData.mBonus[raceSkillIndex].mBonus; - break; - } - } - - for (int k = 0; k < 5; ++k) - { - // is this a minor or major skill? - if ((class_->mData.mSkills[k][0] == skillIndex) || (class_->mData.mSkills[k][1] == skillIndex)) - { - majorMultiplier = 1.0f; - break; - } - } - - // is this skill in the same Specialization as the class? - const ESM::Skill* skill = MWBase::Environment::get().getWorld()->getStore().get().find(skillIndex); - if (skill->mData.mSpecialization == class_->mData.mSpecialization) - { - specMultiplier = 0.5f; - specBonus = 5; - } - - npcStats.getSkill(skillIndex).setBase( - std::min( - round_ieee_754( - npcStats.getSkill(skillIndex).getBase() - + 5 - + raceBonus - + specBonus - +(int(level)-1) * (majorMultiplier + specMultiplier)), 100)); // Must gracefully handle level 0 - } - - int skills[ESM::Skill::Length]; - for (int i=0; i spells = MWMechanics::autoCalcNpcSpells(skills, attributes, race); - for (std::vector::iterator it = spells.begin(); it != spells.end(); ++it) - npcStats.getSpells().add(*it); + GamePlay::autoCalculateSpells(race, stats, &store); } } @@ -392,7 +263,7 @@ namespace MWClass // store ptr.getRefData().setCustomData (data.release()); - getInventoryStore(ptr).autoEquip(ptr); + getInventoryStore(ptr).autoEquip(ptr); } } diff --git a/apps/openmw/mwgui/levelupdialog.cpp b/apps/openmw/mwgui/levelupdialog.cpp index c392372ff3..c6954d91d7 100644 --- a/apps/openmw/mwgui/levelupdialog.cpp +++ b/apps/openmw/mwgui/levelupdialog.cpp @@ -4,6 +4,8 @@ #include #include +#include + #include "../mwbase/windowmanager.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -140,7 +142,7 @@ namespace MWGui if(world->getStore().get().isDynamic(cls->mId)) { // Choosing Stealth specialization and Speed/Agility as attributes, if possible. Otherwise fall back to first class found. - MWWorld::SharedIterator it = world->getStore().get().begin(); + GamePlay::SharedIterator it = world->getStore().get().begin(); for(; it != world->getStore().get().end(); ++it) { if(it->mData.mIsPlayable && it->mData.mSpecialization == 2 && it->mData.mAttribute[0] == 4 && it->mData.mAttribute[1] == 3) diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index c829154e27..5d61bc1d29 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -6,8 +6,11 @@ #include +#include + #include "../mwworld/esmstore.hpp" #include "../mwworld/inventorystore.hpp" +#include "../mwworld/mwstore.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/world.hpp" @@ -23,7 +26,6 @@ #include #include "spellcasting.hpp" -#include "autocalcspell.hpp" #include @@ -252,10 +254,12 @@ namespace MWMechanics continue; static const float fAutoPCSpellChance = esmStore.get().find("fAutoPCSpellChance")->getFloat(); - if (calcAutoCastChance(spell, skills, attributes, -1) < fAutoPCSpellChance) + MWWorld::MWStore store; + + if (GamePlay::calcAutoCastChance(spell, skills, attributes, -1, &store) < fAutoPCSpellChance) continue; - if (!attrSkillCheck(spell, skills, attributes)) + if (!GamePlay::attrSkillCheck(spell, skills, attributes, &store)) continue; selectedSpells.push_back(spell->mId); diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp index 5d9beecb65..61518548bc 100644 --- a/apps/openmw/mwworld/esmstore.cpp +++ b/apps/openmw/mwworld/esmstore.cpp @@ -66,7 +66,7 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) esm.getRecHeader(); // Look up the record type. - std::map::iterator it = mStores.find(n.val); + std::map::iterator it = mStores.find(n.val); if (it == mStores.end()) { if (n.val == ESM::REC_INFO) { @@ -130,7 +130,7 @@ void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) void ESMStore::setUp() { - std::map::iterator it = mStores.begin(); + std::map::iterator it = mStores.begin(); for (; it != mStores.end(); ++it) { it->second->setUp(); } diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp index 05b6339566..61d361b3e5 100644 --- a/apps/openmw/mwworld/esmstore.hpp +++ b/apps/openmw/mwworld/esmstore.hpp @@ -67,7 +67,7 @@ namespace MWWorld // Lookup of all IDs. Makes looking up references faster. Just // maps the id name to the record type. std::map mIds; - std::map mStores; + std::map mStores; ESM::NPC mPlayerTemplate; @@ -75,7 +75,7 @@ namespace MWWorld public: /// \todo replace with SharedIterator - typedef std::map::const_iterator iterator; + typedef std::map::const_iterator iterator; iterator begin() const { return mStores.begin(); @@ -144,7 +144,7 @@ namespace MWWorld void clearDynamic () { - for (std::map::iterator it = mStores.begin(); it != mStores.end(); ++it) + for (std::map::iterator it = mStores.begin(); it != mStores.end(); ++it) it->second->clearDynamic(); mNpcs.insert(mPlayerTemplate); diff --git a/apps/openmw/mwworld/store.hpp b/apps/openmw/mwworld/store.hpp index ab09782b14..852092a9a9 100644 --- a/apps/openmw/mwworld/store.hpp +++ b/apps/openmw/mwworld/store.hpp @@ -10,6 +10,7 @@ #include #include +#include #include @@ -17,89 +18,10 @@ namespace MWWorld { - struct StoreBase - { - virtual ~StoreBase() {} - - virtual void setUp() {} - virtual void listIdentifier(std::vector &list) const {} - - virtual size_t getSize() const = 0; - virtual int getDynamicSize() const { return 0; } - virtual void load(ESM::ESMReader &esm, const std::string &id) = 0; - - virtual bool eraseStatic(const std::string &id) {return false;} - virtual void clearDynamic() {} - - virtual void write (ESM::ESMWriter& writer, Loading::Listener& progress) const {} - - virtual void read (ESM::ESMReader& reader, const std::string& id) {} - ///< Read into dynamic storage - }; - - template - class SharedIterator - { - typedef typename std::vector::const_iterator Iter; - - Iter mIter; - - public: - SharedIterator() {} - - SharedIterator(const SharedIterator &orig) - : mIter(orig.mIter) - {} - - SharedIterator(const Iter &iter) - : mIter(iter) - {} - - SharedIterator &operator++() { - ++mIter; - return *this; - } - - SharedIterator operator++(int) { - SharedIterator iter = *this; - ++mIter; - - return iter; - } - - SharedIterator &operator--() { - --mIter; - return *this; - } - - SharedIterator operator--(int) { - SharedIterator iter = *this; - --mIter; - - return iter; - } - - bool operator==(const SharedIterator &x) const { - return mIter == x.mIter; - } - - bool operator!=(const SharedIterator &x) const { - return !(*this == x); - } - - const T &operator*() const { - return **mIter; - } - - const T *operator->() const { - return &(**mIter); - } - }; - class ESMStore; template - class Store : public StoreBase + class Store : public GamePlay::CommonStore { std::map mStatic; std::vector mShared; // Preserves the record order as it came from the content files (this @@ -137,7 +59,7 @@ namespace MWWorld : mStatic(orig.mData) {} - typedef SharedIterator iterator; + typedef GamePlay::SharedIterator iterator; // setUp needs to be called again after virtual void clearDynamic() @@ -380,7 +302,7 @@ namespace MWWorld } template <> - class Store : public StoreBase + class Store : public GamePlay::StoreBase { // For multiple ESM/ESP files we need one list per file. typedef std::vector LandTextureList; @@ -457,7 +379,7 @@ namespace MWWorld }; template <> - class Store : public StoreBase + class Store : public GamePlay::StoreBase { std::vector mStatic; @@ -472,7 +394,7 @@ namespace MWWorld }; public: - typedef SharedIterator iterator; + typedef GamePlay::SharedIterator iterator; virtual ~Store() { @@ -546,7 +468,7 @@ namespace MWWorld }; template <> - class Store : public StoreBase + class Store : public GamePlay::StoreBase { struct DynamicExtCmp { @@ -586,7 +508,7 @@ namespace MWWorld void handleMovedCellRefs(ESM::ESMReader& esm, ESM::Cell* cell); public: - typedef SharedIterator iterator; + typedef GamePlay::SharedIterator iterator; const ESM::Cell *search(const std::string &id) const { ESM::Cell cell; @@ -834,7 +756,7 @@ namespace MWWorld }; template <> - class Store : public StoreBase + class Store : public GamePlay::StoreBase { private: typedef std::map Interior; diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 1b33b10f6d..8c37c8ef3a 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -6,16 +6,16 @@ set (VERSION_HPP ${CMAKE_CURRENT_SOURCE_DIR}/version/version.hpp) if (GIT_CHECKOUT) add_custom_target (git-version COMMAND ${CMAKE_COMMAND} - -DGIT_EXECUTABLE=${GIT_EXECUTABLE} + -DGIT_EXECUTABLE=${GIT_EXECUTABLE} -DPROJECT_SOURCE_DIR=${PROJECT_SOURCE_DIR} - -DVERSION_HPP_IN=${VERSION_HPP_IN} - -DVERSION_HPP=${VERSION_HPP} - -DOPENMW_VERSION_MAJOR=${OPENMW_VERSION_MAJOR} - -DOPENMW_VERSION_MINOR=${OPENMW_VERSION_MINOR} - -DOPENMW_VERSION_RELEASE=${OPENMW_VERSION_RELEASE} - -DOPENMW_VERSION=${OPENMW_VERSION} + -DVERSION_HPP_IN=${VERSION_HPP_IN} + -DVERSION_HPP=${VERSION_HPP} + -DOPENMW_VERSION_MAJOR=${OPENMW_VERSION_MAJOR} + -DOPENMW_VERSION_MINOR=${OPENMW_VERSION_MINOR} + -DOPENMW_VERSION_RELEASE=${OPENMW_VERSION_RELEASE} + -DOPENMW_VERSION=${OPENMW_VERSION} -P ${CMAKE_CURRENT_SOURCE_DIR}/../cmake/GitVersion.cmake - VERBATIM) + VERBATIM) else (GIT_CHECKOUT) configure_file(${VERSION_HPP_IN} ${VERSION_HPP}) endif (GIT_CHECKOUT) @@ -119,6 +119,10 @@ add_component_dir (fontloader fontloader ) +add_component_dir (gameplay + autocalc autocalcspell + ) + add_component_dir (version version ) @@ -161,14 +165,14 @@ include_directories(${BULLET_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR}) add_library(components STATIC ${COMPONENT_FILES} ${MOC_SRCS} ${ESM_UI_HDR}) -target_link_libraries(components +target_link_libraries(components ${Boost_SYSTEM_LIBRARY} ${Boost_FILESYSTEM_LIBRARY} ${Boost_THREAD_LIBRARY} ${Boost_PROGRAM_OPTIONS_LIBRARY} ${Boost_WAVE_LIBRARY} ${OGRE_LIBRARIES} - ${OENGINE_LIBRARY} + ${OENGINE_LIBRARY} ${BULLET_LIBRARIES} ) diff --git a/components/gameplay/autocalc.cpp b/components/gameplay/autocalc.cpp new file mode 100644 index 0000000000..a1888c434f --- /dev/null +++ b/components/gameplay/autocalc.cpp @@ -0,0 +1,207 @@ +#include "autocalc.hpp" + +#include +#include +#include +#include +#include +#include + +#include "autocalcspell.hpp" + +// Most of the code in this file was moved from apps/openmw/mwclass/npc.cpp +namespace +{ + int is_even(double d) + { + double int_part; + + modf(d / 2.0, &int_part); + return 2.0 * int_part == d; + } + + int round_ieee_754(double d) + { + double i = floor(d); + d -= i; + + if(d < 0.5) + return static_cast(i); + if(d > 0.5) + return static_cast(i) + 1; + if(is_even(i)) + return static_cast(i); + return static_cast(i) + 1; + } +} + +namespace GamePlay +{ + void autoCalcAttributesImpl (const ESM::NPC* npc, + const ESM::Race *race, const ESM::Class *class_, int level, StatsBase& stats, StoreWrap *store) + { + // race bonus + bool male = (npc->mFlags & ESM::NPC::Female) == 0; + + for (int i=0; imData.mAttributeValues[i]; + stats.setAttribute(i, male ? attribute.mMale : attribute.mFemale); + } + + // class bonus + for (int i=0; i<2; ++i) + { + int attribute = class_->mData.mAttribute[i]; + if (attribute>=0 && attributefindSkill(j); + + if (skill->mData.mAttribute != attribute) + continue; + + // is this a minor or major skill? + float add=0.2f; + for (int k=0; k<5; ++k) + { + if (class_->mData.mSkills[k][0] == j) + add=0.5; + } + for (int k=0; k<5; ++k) + { + if (class_->mData.mSkills[k][1] == j) + add=1.0; + } + modifierSum += add; + } + stats.setAttribute(attribute, + std::min(round_ieee_754(stats.getBaseAttribute(attribute) + (level-1) * modifierSum), 100) ); + } + } + + /** + * @brief autoCalculateSkills + * + * Skills are calculated with following formulae ( http://www.uesp.net/wiki/Morrowind:NPCs#Skills ): + * + * Skills: (Level - 1) × (Majority Multiplier + Specialization Multiplier) + * + * The Majority Multiplier is 1.0 for a Major or Minor Skill, or 0.1 for a Miscellaneous Skill. + * + * The Specialization Multiplier is 0.5 for a Skill in the same Specialization as the class, + * zero for other Skills. + * + * and by adding class, race, specialization bonus. + */ + void autoCalcSkillsImpl (const ESM::NPC* npc, + const ESM::Race *race, const ESM::Class *class_, int level, StatsBase& stats, StoreWrap *store) + { + for (int i = 0; i < 2; ++i) + { + int bonus = (i==0) ? 10 : 25; + + for (int i2 = 0; i2 < 5; ++i2) + { + int index = class_->mData.mSkills[i2][i]; + if (index >= 0 && index < ESM::Skill::Length) + { + stats.setBaseSkill (index, stats.getBaseSkill(index) + bonus); + } + } + } + + for (int skillIndex = 0; skillIndex < ESM::Skill::Length; ++skillIndex) + { + float majorMultiplier = 0.1f; + float specMultiplier = 0.0f; + + int raceBonus = 0; + int specBonus = 0; + + for (int raceSkillIndex = 0; raceSkillIndex < 7; ++raceSkillIndex) + { + if (race->mData.mBonus[raceSkillIndex].mSkill == skillIndex) + { + raceBonus = race->mData.mBonus[raceSkillIndex].mBonus; + break; + } + } + + for (int k = 0; k < 5; ++k) + { + // is this a minor or major skill? + if ((class_->mData.mSkills[k][0] == skillIndex) || (class_->mData.mSkills[k][1] == skillIndex)) + { + majorMultiplier = 1.0f; + break; + } + } + + // is this skill in the same Specialization as the class? + const ESM::Skill* skill = store->findSkill(skillIndex); + if (skill->mData.mSpecialization == class_->mData.mSpecialization) + { + specMultiplier = 0.5f; + specBonus = 5; + } + + stats.setBaseSkill(skillIndex, + std::min( + round_ieee_754( + stats.getBaseSkill(skillIndex) + + 5 + + raceBonus + + specBonus + +(int(level)-1) * (majorMultiplier + specMultiplier)), 100)); // Must gracefully handle level 0 + } + } + + unsigned short autoCalculateHealth(int level, const ESM::Class *class_, StatsBase& stats) + { + // initial health + int strength = stats.getBaseAttribute(ESM::Attribute::Strength); + int endurance = stats.getBaseAttribute(ESM::Attribute::Endurance); + + int multiplier = 3; + + if (class_->mData.mSpecialization == ESM::Class::Combat) + multiplier += 2; + else if (class_->mData.mSpecialization == ESM::Class::Stealth) + multiplier += 1; + + if (class_->mData.mAttribute[0] == ESM::Attribute::Endurance + || class_->mData.mAttribute[1] == ESM::Attribute::Endurance) + multiplier += 1; + + return static_cast(floor(0.5f * (strength + endurance)) + multiplier * (level-1)); + } + + void autoCalculateSpells(const ESM::Race *race, StatsBase& stats, StoreWrap *store) + { + int skills[ESM::Skill::Length]; + for (int i=0; i spells = autoCalcNpcSpells(skills, attributes, race, store); + for (std::vector::iterator it = spells.begin(); it != spells.end(); ++it) + stats.addSpells(*it); + } + + StatsBase::StatsBase() {} + + StatsBase::~StatsBase() {} +} diff --git a/components/gameplay/autocalc.hpp b/components/gameplay/autocalc.hpp new file mode 100644 index 0000000000..522070413e --- /dev/null +++ b/components/gameplay/autocalc.hpp @@ -0,0 +1,47 @@ +#ifndef COMPONENTS_GAMEPLAY_AUTOCALC_H +#define COMPONENTS_GAMEPLAY_AUTOCALC_H + +#include + +#include "store.hpp" + +namespace ESM +{ + struct NPC; + struct Race; + struct Class; +} + +namespace GamePlay +{ + // wrapper class for sharing the autocalc code between OpenMW and OpenCS + class StatsBase + { + + public: + + StatsBase(); + virtual ~StatsBase(); + + virtual unsigned char getBaseAttribute(int index) const = 0; + + virtual void setAttribute(int index, unsigned char value) = 0; + + virtual void addSpells(std::string id) = 0; + + virtual unsigned char getBaseSkill(int index) const = 0; + + virtual void setBaseSkill(int index, unsigned char value) = 0; + }; + + void autoCalcAttributesImpl (const ESM::NPC* npc, + const ESM::Race *race, const ESM::Class *class_, int level, StatsBase& stats, StoreWrap *store); + + void autoCalcSkillsImpl (const ESM::NPC* npc, + const ESM::Race *race, const ESM::Class *class_, int level, StatsBase& stats, StoreWrap *store); + + unsigned short autoCalculateHealth(int level, const ESM::Class *class_, StatsBase& stats); + + void autoCalculateSpells(const ESM::Race *race, StatsBase& stats, StoreWrap *store); +} +#endif // COMPONENTS_GAMEPLAY_AUTOCALC_H diff --git a/components/gameplay/autocalcspell.cpp b/components/gameplay/autocalcspell.cpp new file mode 100644 index 0000000000..8f28433676 --- /dev/null +++ b/components/gameplay/autocalcspell.cpp @@ -0,0 +1,240 @@ +#include "autocalcspell.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "autocalc.hpp" + +// Most of the code in this file was moved from apps/openmw/mwmechanics/autocalcspell.cpp +namespace GamePlay +{ + + struct SchoolCaps + { + int mCount; + int mLimit; + bool mReachedLimit; + int mMinCost; + std::string mWeakestSpell; + }; + + std::vector autoCalcNpcSpells(const int *actorSkills, + const int *actorAttributes, const ESM::Race* race, StoreWrap *store) + { + static const float fNPCbaseMagickaMult = store->findGmstFloat("fNPCbaseMagickaMult"); + float baseMagicka = fNPCbaseMagickaMult * actorAttributes[ESM::Attribute::Intelligence]; + + static const std::string schools[] = { + "alteration", "conjuration", "destruction", "illusion", "mysticism", "restoration" + }; + static int iAutoSpellSchoolMax[6]; + static bool init = false; + if (!init) + { + for (int i=0; i<6; ++i) + { + const std::string& gmstName = "iAutoSpell" + schools[i] + "Max"; + iAutoSpellSchoolMax[i] = store->findGmstInt(gmstName); + } + init = true; + } + + std::map schoolCaps; + for (int i=0; i<6; ++i) + { + SchoolCaps caps; + caps.mCount = 0; + caps.mLimit = iAutoSpellSchoolMax[i]; + caps.mReachedLimit = iAutoSpellSchoolMax[i] <= 0; + caps.mMinCost = INT_MAX; + caps.mWeakestSpell.clear(); + schoolCaps[i] = caps; + } + + std::vector selectedSpells; + + const CommonStore &spells = store->getSpells(); + + // Note: the algorithm heavily depends on the traversal order of the spells. For vanilla-compatible results the + // Store must preserve the record ordering as it was in the content files. + for (CommonStore::iterator iter = spells.begin(); iter != spells.end(); ++iter) + { + const ESM::Spell* spell = &*iter; + + if (spell->mData.mType != ESM::Spell::ST_Spell) + continue; + if (!(spell->mData.mFlags & ESM::Spell::F_Autocalc)) + continue; + static const int iAutoSpellTimesCanCast = store->findGmstInt("iAutoSpellTimesCanCast"); + if (baseMagicka < iAutoSpellTimesCanCast * spell->mData.mCost) + continue; + + if (race && race->mPowers.exists(spell->mId)) + continue; + + if (!attrSkillCheck(spell, actorSkills, actorAttributes, store)) + continue; + + int school; + float skillTerm; + calcWeakestSchool(spell, actorSkills, school, skillTerm, store); + assert(school >= 0 && school < 6); + SchoolCaps& cap = schoolCaps[school]; + + if (cap.mReachedLimit && spell->mData.mCost <= cap.mMinCost) + continue; + + static const float fAutoSpellChance = store->findGmstFloat("fAutoSpellChance"); + if (calcAutoCastChance(spell, actorSkills, actorAttributes, school, store) < fAutoSpellChance) + continue; + + selectedSpells.push_back(spell->mId); + + if (cap.mReachedLimit) + { + std::vector::iterator found = std::find(selectedSpells.begin(), selectedSpells.end(), cap.mWeakestSpell); + if (found != selectedSpells.end()) + selectedSpells.erase(found); + + cap.mMinCost = INT_MAX; + for (std::vector::iterator weakIt = selectedSpells.begin(); weakIt != selectedSpells.end(); ++weakIt) + { + const ESM::Spell* testSpell = spells.find(*weakIt); + + //int testSchool; + //float dummySkillTerm; + //calcWeakestSchool(testSpell, actorSkills, testSchool, dummySkillTerm); + + // Note: if there are multiple spells with the same cost, we pick the first one we found. + // So the algorithm depends on the iteration order of the outer loop. + if ( + // There is a huge bug here. It is not checked that weakestSpell is of the correct school. + // As result multiple SchoolCaps could have the same mWeakestSpell. Erasing the weakest spell would then fail if another school + // already erased it, and so the number of spells would often exceed the sum of limits. + // This bug cannot be fixed without significantly changing the results of the spell autocalc, which will not have been playtested. + //testSchool == school && + testSpell->mData.mCost < cap.mMinCost) + { + cap.mMinCost = testSpell->mData.mCost; + cap.mWeakestSpell = testSpell->mId; + } + } + } + else + { + cap.mCount += 1; + if (cap.mCount == cap.mLimit) + cap.mReachedLimit = true; + + if (spell->mData.mCost < cap.mMinCost) + { + cap.mWeakestSpell = spell->mId; + cap.mMinCost = spell->mData.mCost; + } + } + } + + return selectedSpells; + } + + bool attrSkillCheck (const ESM::Spell* spell, + const int* actorSkills, const int* actorAttributes, StoreWrap *store) + { + const std::vector& effects = spell->mEffects.mList; + for (std::vector::const_iterator effectIt = effects.begin(); effectIt != effects.end(); ++effectIt) + { + const ESM::MagicEffect* magicEffect = store->findMagicEffect(effectIt->mEffectID); + static const int iAutoSpellAttSkillMin = store->findGmstInt("iAutoSpellAttSkillMin"); + + if ((magicEffect->mData.mFlags & ESM::MagicEffect::TargetSkill)) + { + assert (effectIt->mSkill >= 0 && effectIt->mSkill < ESM::Skill::Length); + if (actorSkills[effectIt->mSkill] < iAutoSpellAttSkillMin) + return false; + } + + if ((magicEffect->mData.mFlags & ESM::MagicEffect::TargetAttribute)) + { + assert (effectIt->mAttribute >= 0 && effectIt->mAttribute < ESM::Attribute::Length); + if (actorAttributes[effectIt->mAttribute] < iAutoSpellAttSkillMin) + return false; + } + } + + return true; + } + + ESM::Skill::SkillEnum mapSchoolToSkill(int school) + { + std::map schoolSkillMap; // maps spell school to skill id + schoolSkillMap[0] = ESM::Skill::Alteration; + schoolSkillMap[1] = ESM::Skill::Conjuration; + schoolSkillMap[3] = ESM::Skill::Illusion; + schoolSkillMap[2] = ESM::Skill::Destruction; + schoolSkillMap[4] = ESM::Skill::Mysticism; + schoolSkillMap[5] = ESM::Skill::Restoration; + assert(schoolSkillMap.find(school) != schoolSkillMap.end()); + return schoolSkillMap[school]; + } + + void calcWeakestSchool (const ESM::Spell* spell, + const int* actorSkills, int& effectiveSchool, float& skillTerm, StoreWrap *store) + { + float minChance = FLT_MAX; + + const ESM::EffectList& effects = spell->mEffects; + for (std::vector::const_iterator it = effects.mList.begin(); it != effects.mList.end(); ++it) + { + const ESM::ENAMstruct& effect = *it; + float x = static_cast(effect.mDuration); + + const ESM::MagicEffect* magicEffect = store->findMagicEffect(effect.mEffectID); + if (!(magicEffect->mData.mFlags & ESM::MagicEffect::UncappedDamage)) + 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 *= 1.5f; + + static const float fEffectCostMult = store->findGmstFloat("fEffectCostMult"); + x *= fEffectCostMult; + + float s = 2.f * actorSkills[mapSchoolToSkill(magicEffect->mData.mSchool)]; + if (s - x < minChance) + { + minChance = s - x; + effectiveSchool = magicEffect->mData.mSchool; + skillTerm = s; + } + } + } + + float calcAutoCastChance(const ESM::Spell *spell, + const int *actorSkills, const int *actorAttributes, int effectiveSchool, StoreWrap *store) + { + if (spell->mData.mType != ESM::Spell::ST_Spell) + return 100.f; + + if (spell->mData.mFlags & ESM::Spell::F_Always) + return 100.f; + + float skillTerm = 0; + if (effectiveSchool != -1) + skillTerm = 2.f * actorSkills[mapSchoolToSkill(effectiveSchool)]; + else + calcWeakestSchool(spell, actorSkills, effectiveSchool, skillTerm, store); // Note effectiveSchool is unused after this + + float castChance = skillTerm - spell->mData.mCost + 0.2f * actorAttributes[ESM::Attribute::Willpower] + 0.1f * actorAttributes[ESM::Attribute::Luck]; + return castChance; + } +} diff --git a/components/gameplay/autocalcspell.hpp b/components/gameplay/autocalcspell.hpp new file mode 100644 index 0000000000..ceb9f197b9 --- /dev/null +++ b/components/gameplay/autocalcspell.hpp @@ -0,0 +1,38 @@ +#ifndef COMPONENTS_GAMEPLAY_AUTOCALCSPELL_H +#define COMPONENTS_GAMEPLAY_AUTOCALCSPELL_H + +#include + +#include + +namespace ESM +{ + struct Spell; + struct Race; +} + +namespace GamePlay +{ + +class StoreWrap; + +/// Contains algorithm for calculating an NPC's spells based on stats + +std::vector autoCalcNpcSpells(const int* actorSkills, + const int* actorAttributes, const ESM::Race* race, StoreWrap *store); + +// Helpers + +bool attrSkillCheck (const ESM::Spell* spell, const int* actorSkills, const int* actorAttributes, StoreWrap *store); + +ESM::Skill::SkillEnum mapSchoolToSkill(int school); + +void calcWeakestSchool(const ESM::Spell* spell, + const int* actorSkills, int& effectiveSchool, float& skillTerm, StoreWrap *store); + +float calcAutoCastChance(const ESM::Spell* spell, + const int* actorSkills, const int* actorAttributes, int effectiveSchool, StoreWrap *store); + +} + +#endif diff --git a/components/gameplay/store.hpp b/components/gameplay/store.hpp new file mode 100644 index 0000000000..c4d7cba179 --- /dev/null +++ b/components/gameplay/store.hpp @@ -0,0 +1,138 @@ +#ifndef COMPONENTS_GAMEPLAY_STORE_H +#define COMPONENTS_GAMEPLAY_STORE_H + +#include +#include + +namespace Loading +{ + class Listener; +} + +namespace ESM +{ + class ESMWriter; + class ESMReader; + struct Spell; + struct Skill; + struct MagicEffect; +} + +namespace GamePlay +{ + // moved from apps/openmw/mwworld/store.hpp + struct StoreBase + { + virtual ~StoreBase() {} + + virtual void setUp() {} + virtual void listIdentifier(std::vector &list) const {} + + virtual size_t getSize() const = 0; + virtual int getDynamicSize() const { return 0; } + virtual void load(ESM::ESMReader &esm, const std::string &id) = 0; + + virtual bool eraseStatic(const std::string &id) {return false;} + virtual void clearDynamic() {} + + virtual void write (ESM::ESMWriter& writer, Loading::Listener& progress) const {} + + virtual void read (ESM::ESMReader& reader, const std::string& id) {} + ///< Read into dynamic storage + }; + + // moved from apps/openmw/mwworld/store.hpp + template + class SharedIterator + { + typedef typename std::vector::const_iterator Iter; + + Iter mIter; + + public: + SharedIterator() {} + + SharedIterator(const SharedIterator &orig) + : mIter(orig.mIter) + {} + + SharedIterator(const Iter &iter) + : mIter(iter) + {} + + SharedIterator &operator++() { + ++mIter; + return *this; + } + + SharedIterator operator++(int) { + SharedIterator iter = *this; + ++mIter; + + return iter; + } + + SharedIterator &operator--() { + --mIter; + return *this; + } + + SharedIterator operator--(int) { + SharedIterator iter = *this; + --mIter; + + return iter; + } + + bool operator==(const SharedIterator &x) const { + return mIter == x.mIter; + } + + bool operator!=(const SharedIterator &x) const { + return !(*this == x); + } + + const T &operator*() const { + return **mIter; + } + + const T *operator->() const { + return &(**mIter); + } + }; + + // interface class for sharing the autocalc component between OpenMW and OpenCS + template + class CommonStore : public StoreBase + { + + public: + typedef SharedIterator iterator; + + virtual iterator begin() const = 0; + + virtual iterator end() const = 0; + + virtual const T *find(const std::string &id) const = 0; + }; + + // interface class for sharing the autocalc component between OpenMW and OpenCS + class StoreWrap + { + + public: + StoreWrap() {} + virtual ~StoreWrap() {} + + virtual int findGmstInt(const std::string& gmst) const = 0; + + virtual float findGmstFloat(const std::string& gmst) const = 0; + + virtual const ESM::Skill *findSkill(int index) const = 0; + + virtual const ESM::MagicEffect* findMagicEffect(int id) const = 0; + + virtual const CommonStore& getSpells() const = 0; + }; +} +#endif // COMPONENTS_GAMEPLAY_STORE_H