diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 3399b9ddd..f922a9112 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 metadata + idcompletionmanager npcstats metadata ) opencs_hdrs_noqt (model/world diff --git a/apps/opencs/model/world/columns.cpp b/apps/opencs/model/world/columns.cpp index d6e27caeb..6871d7c95 100644 --- a/apps/opencs/model/world/columns.cpp +++ b/apps/opencs/model/world/columns.cpp @@ -315,6 +315,10 @@ namespace CSMWorld { ColumnId_FileDescription, "File Description" }, { ColumnId_Author, "Author" }, + { 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 d699c67b7..e2c5712d6 100644 --- a/apps/opencs/model/world/columns.hpp +++ b/apps/opencs/model/world/columns.hpp @@ -306,6 +306,10 @@ namespace CSMWorld ColumnId_FileDescription = 276, ColumnId_Author = 277, + ColumnId_SpellSrc = 278, + ColumnId_SpellCost = 279, + ColumnId_SpellChance = 280, + // 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 b38607748..27e57143f 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,71 @@ #include "resourcesmanager.hpp" #include "resourcetable.hpp" #include "nestedcoladapterimp.hpp" +#include "npcstats.hpp" + +namespace +{ + class CSStore : public AutoCalc::StoreCommon + { + const CSMWorld::IdCollection& mGmstTable; + const CSMWorld::IdCollection& mSkillTable; + const CSMWorld::IdCollection& mMagicEffectTable; + const CSMWorld::NestedIdCollection& mSpells; + + public: + + CSStore(const CSMWorld::IdCollection& gmst, + const CSMWorld::IdCollection& skills, + const CSMWorld::IdCollection& magicEffects, + const CSMWorld::NestedIdCollection& spells) + : mGmstTable(gmst), mSkillTable(skills), mMagicEffectTable(magicEffects), mSpells(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 void getSpells(std::vector& spells) + { + // prepare data in a format used by OpenMW store + for (int index = 0; index < mSpells.getSize(); ++index) + spells.push_back(const_cast(&mSpells.getRecord(index).get())); + } + }; + + unsigned short autoCalculateMana(AutoCalc::StatsBase& stats) + { + return stats.getBaseAttribute(ESM::Attribute::Intelligence) * 2; + } + + unsigned short autoCalculateFatigue(AutoCalc::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) { @@ -61,7 +130,7 @@ int CSMWorld::Data::count (RecordBase::State state, const CollectionBase& collec } CSMWorld::Data::Data (ToUTF8::FromType encoding, const ResourcesManager& resourcesManager) -: mEncoder (encoding), mPathgrids (mCells), mRefs (mCells), +: mEncoder (encoding), mPathgrids (mCells), mReferenceables(self()), mRefs (mCells), mResourcesManager (resourcesManager), mReader (0), mDialogue (0), mReaderIndex(0) { int index = 0; @@ -202,7 +271,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)); @@ -525,11 +594,40 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, const ResourcesManager& resourc UniversalId::Type_Video); addModel (new IdTable (&mMetaData), UniversalId::Type_MetaData); + // 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; @@ -1161,3 +1259,270 @@ void CSMWorld::Data::rowsChanged (const QModelIndex& parent, int start, int end) { emit idListChanged(); } + +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 + // - mPowers.mList[] + 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); + int powersColumn = raceModel->findColumnIndex(CSMWorld::Columns::ColumnId_PowerList); + + bool match = false; + int raceRow = topLeft.row(); + int raceEnd = bottomRight.row(); + if (topLeft.parent().isValid() && bottomRight.parent().isValid()) + { + if ((topLeft.parent().column() <= attrColumn && attrColumn <= bottomRight.parent().column()) + || (topLeft.parent().column() <= bonusColumn && bonusColumn <= bottomRight.parent().column()) + || (topLeft.parent().column() <= powersColumn && powersColumn <= bottomRight.parent().column())) + { + match = true; // TODO: check for specific nested column? + raceRow = topLeft.parent().row(); + raceEnd = bottomRight.parent().row(); + } + } + else + { + if ((topLeft.column() <= attrColumn && attrColumn <= bottomRight.column()) + || (topLeft.column() <= bonusColumn && bonusColumn <= bottomRight.column()) + || (topLeft.column() <= powersColumn && powersColumn <= 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 (; raceRow <= raceEnd; ++raceRow) + { + clearNpcStatsCache(); + + std::string raceId = + raceModel->data(raceModel->index(raceRow, idColumn)).toString().toUtf8().constData(); + emit updateNpcAutocalc(2/*race*/, raceId); + } +} + +void CSMWorld::Data::npcDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +{ + // TODO: for now always recalculate + clearNpcStatsCache(); +} + +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); +} + +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; + + std::auto_ptr stats (new CSMWorld::NpcStats()); + + CSStore store(mGmsts, mSkills, mMagicEffects, mSpells); + + if (autoCalc) + { + AutoCalc::autoCalcAttributesImpl (&npc, race, class_, level, *stats, &store); + + stats->setHealth(autoCalculateHealth(level, class_, *stats)); + stats->setMana(autoCalculateMana(*stats)); + stats->setFatigue(autoCalculateFatigue(*stats)); + + AutoCalc::autoCalcSkillsImpl(&npc, race, class_, level, *stats, &store); + + AutoCalc::autoCalculateSpells(race, *stats, &store); + } + else + { + for (std::vector::const_iterator it = npc.mSpells.mList.begin(); + it != npc.mSpells.mList.end(); ++it) + { + stats->addSpell(*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; + AutoCalc::calcWeakestSchool(spell, skills, school, skillTerm, &store); + float chance = calcAutoCastChance(spell, skills, attributes, school, &store); + + stats->addCostAndChance((*it).mName, cost, (int)ceil(chance)); // percent + } + } + + if (stats.get() == 0) + return 0; + + CSMWorld::NpcStats *result = stats.release(); + emit cacheNpcStats (npc.mId, result); + return result; +} + +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 15e39b9ba..1aa9db0b1 100644 --- a/apps/opencs/model/world/data.hpp +++ b/apps/opencs/model/world/data.hpp @@ -61,6 +61,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,6 +124,10 @@ namespace CSMWorld static int count (RecordBase::State state, const CollectionBase& collection); + const Data& self (); + + void clearNpcStatsCache (); + public: Data (ToUTF8::FromType encoding, const ResourcesManager& resourcesManager); @@ -269,15 +276,37 @@ namespace CSMWorld int count (RecordBase::State state) const; ///< Return number of top-level records with the given \a state. + 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 1e81d6ac2..fba672108 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 5337ed82b..b29a3ae93 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/nestedcoladapterimp.cpp b/apps/opencs/model/world/nestedcoladapterimp.cpp index b7d09777d..14a436c83 100644 --- a/apps/opencs/model/world/nestedcoladapterimp.cpp +++ b/apps/opencs/model/world/nestedcoladapterimp.cpp @@ -6,6 +6,7 @@ #include "idcollection.hpp" #include "pathgrid.hpp" #include "info.hpp" +#include "usertype.hpp" namespace CSMWorld { @@ -1069,23 +1070,66 @@ namespace CSMWorld switch (subColIndex) { case 0: return isInterior; - case 1: return (isInterior && !behaveLikeExterior) ? - cell.mAmbi.mAmbient : QVariant(QVariant::UserType); - case 2: return (isInterior && !behaveLikeExterior) ? - cell.mAmbi.mSunlight : QVariant(QVariant::UserType); - case 3: return (isInterior && !behaveLikeExterior) ? - cell.mAmbi.mFog : QVariant(QVariant::UserType); - case 4: return (isInterior && !behaveLikeExterior) ? - cell.mAmbi.mFogDensity : QVariant(QVariant::UserType); + case 1: + { + if (isInterior && !behaveLikeExterior) + return cell.mAmbi.mAmbient; + else + { + UserInt i(cell.mAmbi.mAmbient); + return QVariant(QVariant::fromValue(i)); + } + } + case 2: + { + if (isInterior && !behaveLikeExterior) + return cell.mAmbi.mSunlight; + else + { + UserInt i(cell.mAmbi.mSunlight); + return QVariant(QVariant::fromValue(i)); + } + } + case 3: + { + if (isInterior && !behaveLikeExterior) + return cell.mAmbi.mFog; + else + { + UserInt i(cell.mAmbi.mFog); + return QVariant(QVariant::fromValue(i)); + } + } + case 4: + { + if (isInterior && !behaveLikeExterior) + return cell.mAmbi.mFogDensity; + else + { + UserFloat f(cell.mAmbi.mFogDensity); + return QVariant(QVariant::fromValue(f)); + } + } case 5: { if (isInterior && !behaveLikeExterior && interiorWater) return cell.mWater; else - return QVariant(QVariant::UserType); + { + UserFloat f(cell.mWater); + return QVariant(QVariant::fromValue(f)); + } + } + case 6: + { + if (isInterior) + { + UserInt i(cell.mMapColor); + return QVariant(QVariant::fromValue(i)); + } + else + return cell.mMapColor; // TODO: how to select? } - case 6: return isInterior ? - QVariant(QVariant::UserType) : cell.mMapColor; // TODO: how to select? //case 7: return isInterior ? //behaveLikeExterior : QVariant(QVariant::UserType); default: throw std::runtime_error("Cell subcolumn index out of range"); diff --git a/apps/opencs/model/world/npcstats.cpp b/apps/opencs/model/world/npcstats.cpp new file mode 100644 index 000000000..5b8484934 --- /dev/null +++ b/apps/opencs/model/world/npcstats.cpp @@ -0,0 +1,138 @@ +#include "npcstats.hpp" + +#include +#include + +namespace CSMWorld +{ + NpcStats::NpcStats() : mHealth(0), mMana(0), mFatigue(0) + { + for (int i = 0; i < ESM::Skill::Length; ++i) + mSkill[i] = 0; + } + + NpcStats::NpcStats(const NpcStats &other) + { + for (int i = 0; i < ESM::Attribute::Length; ++i) + mAttr[i] = other.mAttr[i]; + + mSpells = other.mSpells; + + for (int i = 0; i < ESM::Skill::Length; ++i) + mSkill[i] = 0; + + mHealth = other.mHealth; + mMana = other.mMana; + mFatigue = other.mFatigue; + } + + NpcStats::~NpcStats() + {} + + unsigned char NpcStats::getBaseAttribute(int index) const + { + if (index < 0 || index >= ESM::Attribute::Length) + throw std::runtime_error("attrib index out of bounds"); + + return mAttr[index]; + } + + void NpcStats::setAttribute(int index, unsigned char value) + { + if (index < 0 || index >= ESM::Attribute::Length) + throw std::runtime_error("attrib index out of bounds"); + + mAttr[index] = value; + } + + void NpcStats::addSpell(const std::string& id) + { + struct SpellInfo info; + info.mName = id; + info.mType = ESM::Spell::ST_Spell; // default type from autocalc + info.mFromRace = false; + info.mCost = 0; + info.mChance = 0; + + mSpells.insert(mSpells.begin(), info); + } + + void NpcStats::addPowers(const std::string& id, int type) + { + struct SpellInfo info; + info.mName = id; + info.mType = type; + info.mFromRace = true; + info.mCost = 0; + info.mChance = 0; + + mSpells.push_back(info); + } + + void NpcStats::addCostAndChance(const std::string& id, int cost, int chance) + { + // usually only a few spells, so simply iterate through rather than keeping a separate + // lookup index or map + for (std::vector::iterator it = mSpells.begin(); it != mSpells.end(); ++it) + { + if ((*it).mName == id) + { + (*it).mCost = cost; + (*it).mChance = chance; + return; + } + } + } + + const std::vector& NpcStats::spells() const + { + return mSpells; + } + + unsigned char NpcStats::getBaseSkill(int index) const + { + if (index < 0 || index >= ESM::Skill::Length) + throw std::runtime_error("skill index out of bounds"); + + return mSkill[index]; + } + + void NpcStats::setBaseSkill(int index, unsigned char value) + { + if (index < 0 || index >= ESM::Skill::Length) + throw std::runtime_error("skill index out of bounds"); + + mSkill[index] = value; + } + + unsigned short NpcStats::getHealth() + { + return mHealth; + } + + void NpcStats::setHealth(unsigned short health) + { + mHealth = health; + } + + unsigned short NpcStats::getMana() + { + return mMana; + } + + void NpcStats::setMana(unsigned short mana) + { + mMana = mana; + } + + unsigned short NpcStats::getFatigue() + { + return mFatigue; + } + + void NpcStats::setFatigue(unsigned short fatigue) + { + mFatigue = fatigue; + } +} + diff --git a/apps/opencs/model/world/npcstats.hpp b/apps/opencs/model/world/npcstats.hpp new file mode 100644 index 000000000..8cefe586f --- /dev/null +++ b/apps/opencs/model/world/npcstats.hpp @@ -0,0 +1,74 @@ +#ifndef CSM_WORLD_NPCSTATS_H +#define CSM_WORLD_NPCSTATS_H + +#include + +#include + +#include +#include +#include + +namespace CSMWorld +{ + struct SpellInfo + { + std::string mName; + int mType; + bool mFromRace; + int mCost; + int mChance; + }; + + class NpcStats : public AutoCalc::StatsBase + { + + int mAttr[ESM::Attribute::Length]; + std::vector mSpells; + int mSkill[ESM::Skill::Length]; + + unsigned short mHealth; + unsigned short mMana; + unsigned short mFatigue; + + public: + + NpcStats(); + + NpcStats(const NpcStats &other); + + ~NpcStats(); + + virtual unsigned char getBaseAttribute(int index) const; + + virtual void setAttribute(int index, unsigned char value); + + virtual void addSpell(const std::string& id); + + void addPowers(const std::string& id, int type); + + void addCostAndChance(const std::string& id, int cost, int chance); + + const std::vector& spells() const; + + virtual unsigned char getBaseSkill(int index) const; + + virtual void setBaseSkill(int index, unsigned char value); + + unsigned short getHealth(); + + void setHealth(unsigned short health); + + unsigned short getMana(); + + void setMana(unsigned short mana); + + unsigned short getFatigue(); + + void setFatigue(unsigned short fatigue); + }; +} + +Q_DECLARE_METATYPE(CSMWorld::NpcStats*) + +#endif // CSM_WORLD_NPCSTATS_H diff --git a/apps/opencs/model/world/refidadapterimp.cpp b/apps/opencs/model/world/refidadapterimp.cpp index 4c369ef24..2bb980bfe 100644 --- a/apps/opencs/model/world/refidadapterimp.cpp +++ b/apps/opencs/model/world/refidadapterimp.cpp @@ -5,7 +5,12 @@ #include #include +#include + #include "nestedtablewrapper.hpp" +#include "usertype.hpp" +#include "idtree.hpp" +#include "npcstats.hpp" CSMWorld::PotionColumns::PotionColumns (const InventoryColumns& columns) : InventoryColumns (columns) {} @@ -553,8 +558,8 @@ CSMWorld::NpcColumns::NpcColumns (const ActorColumns& actorColumns) mMisc(NULL) {} -CSMWorld::NpcRefIdAdapter::NpcRefIdAdapter (const NpcColumns& columns) -: ActorRefIdAdapter (UniversalId::Type_Npc, columns), mColumns (columns) +CSMWorld::NpcRefIdAdapter::NpcRefIdAdapter (const NpcColumns& columns, const CSMWorld::Data& data) +: ActorRefIdAdapter (UniversalId::Type_Npc, columns), mColumns (columns), mData(data) {} QVariant CSMWorld::NpcRefIdAdapter::getData (const RefIdColumn *column, const RefIdData& data, int index) @@ -629,8 +634,47 @@ void CSMWorld::NpcRefIdAdapter::setData (const RefIdColumn *column, RefIdData& d npc.mFlags &= ~iter->second; if (iter->second == ESM::NPC::Autocalc) - npc.mNpdtType = (value.toInt() != 0) ? ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS - : ESM::NPC::NPC_DEFAULT; + { + if (npc.mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) + { + CSMWorld::NpcStats *stats = mData.npcAutoCalculate(npc); + + // update npc + npc.mNpdtType = ESM::NPC::NPC_DEFAULT; + 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) + } + else + { + npc.mNpdtType = ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS; + npc.mNpdt12.mLevel = npc.mNpdt52.mLevel; // for NPC's loaded as non-autocalc + mData.npcAutoCalculate(npc); + } + } } else { @@ -643,7 +687,7 @@ void CSMWorld::NpcRefIdAdapter::setData (const RefIdColumn *column, RefIdData& d record.setModified (npc); } -CSMWorld::NpcAttributesRefIdAdapter::NpcAttributesRefIdAdapter () +CSMWorld::NpcAttributesRefIdAdapter::NpcAttributesRefIdAdapter(const CSMWorld::Data& data) : mData(data) {} void CSMWorld::NpcAttributesRefIdAdapter::addNestedRow (const RefIdColumn *column, @@ -691,7 +735,8 @@ QVariant CSMWorld::NpcAttributesRefIdAdapter::getNestedData (const RefIdColumn * const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); - const ESM::NPC::NPDTstruct52& npcStruct = record.get().mNpdt52; + const ESM::NPC npc = record.get(); + const ESM::NPC::NPDTstruct52& npcStruct = npc.mNpdt52; if (subColIndex == 0) switch (subRowIndex) @@ -707,18 +752,36 @@ QVariant CSMWorld::NpcAttributesRefIdAdapter::getNestedData (const RefIdColumn * default: return QVariant(); // throw an exception here? } else if (subColIndex == 1) - switch (subRowIndex) + if (npc.mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) { - case 0: return static_cast(npcStruct.mStrength); - case 1: return static_cast(npcStruct.mIntelligence); - case 2: return static_cast(npcStruct.mWillpower); - case 3: return static_cast(npcStruct.mAgility); - case 4: return static_cast(npcStruct.mSpeed); - case 5: return static_cast(npcStruct.mEndurance); - case 6: return static_cast(npcStruct.mPersonality); - case 7: return static_cast(npcStruct.mLuck); - default: return QVariant(); // throw an exception here? + CSMWorld::NpcStats *stats = mData.npcAutoCalculate(npc); + + switch (subRowIndex) + { + 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? + } } + else + switch (subRowIndex) + { + case 0: return static_cast(npcStruct.mStrength); + case 1: return static_cast(npcStruct.mIntelligence); + case 2: return static_cast(npcStruct.mWillpower); + case 3: return static_cast(npcStruct.mAgility); + case 4: return static_cast(npcStruct.mSpeed); + case 5: return static_cast(npcStruct.mEndurance); + case 6: return static_cast(npcStruct.mPersonality); + case 7: return static_cast(npcStruct.mLuck); + default: return QVariant(); // throw an exception here? + } else return QVariant(); // throw an exception here? } @@ -761,7 +824,8 @@ int CSMWorld::NpcAttributesRefIdAdapter::getNestedRowsCount(const RefIdColumn *c return 8; } -CSMWorld::NpcSkillsRefIdAdapter::NpcSkillsRefIdAdapter () +CSMWorld::NpcSkillsRefIdAdapter::NpcSkillsRefIdAdapter(const CSMWorld::Data& data) + : mData(data) {} void CSMWorld::NpcSkillsRefIdAdapter::addNestedRow (const RefIdColumn *column, @@ -809,7 +873,7 @@ QVariant CSMWorld::NpcSkillsRefIdAdapter::getNestedData (const RefIdColumn *colu const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); - const ESM::NPC::NPDTstruct52& npcStruct = record.get().mNpdt52; + const ESM::NPC npc = record.get(); if (subRowIndex < 0 || subRowIndex >= ESM::Skill::Length) throw std::runtime_error ("index out of range"); @@ -817,7 +881,18 @@ QVariant CSMWorld::NpcSkillsRefIdAdapter::getNestedData (const RefIdColumn *colu if (subColIndex == 0) return QString(ESM::Skill::sSkillNames[subRowIndex].c_str()); else if (subColIndex == 1) - return static_cast(npcStruct.mSkills[subRowIndex]); + { + if (npc.mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) + { + CSMWorld::NpcStats *stats = mData.npcAutoCalculate(npc); + return static_cast(stats->getBaseSkill(subRowIndex)); + } + else + { + const ESM::NPC::NPDTstruct52& npcStruct = npc.mNpdt52; + return static_cast(npcStruct.mSkills[subRowIndex]); + } + } else return QVariant(); // throw an exception here? } @@ -852,7 +927,7 @@ int CSMWorld::NpcSkillsRefIdAdapter::getNestedRowsCount(const RefIdColumn *colum return ESM::Skill::Length; } -CSMWorld::NpcMiscRefIdAdapter::NpcMiscRefIdAdapter () +CSMWorld::NpcMiscRefIdAdapter::NpcMiscRefIdAdapter(const CSMWorld::Data& data) : mData(data) {} CSMWorld::NpcMiscRefIdAdapter::~NpcMiscRefIdAdapter() @@ -888,16 +963,37 @@ QVariant CSMWorld::NpcMiscRefIdAdapter::getNestedData (const RefIdColumn *column const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, UniversalId::Type_Npc))); - bool autoCalc = (record.get().mFlags & ESM::NPC::Autocalc) != 0; + const ESM::NPC npc = record.get(); + + bool autoCalc = (npc.mFlags & ESM::NPC::Autocalc) != 0; if (autoCalc) + { + CSMWorld::NpcStats *stats = mData.npcAutoCalculate(npc); + switch (subColIndex) { - case 0: return static_cast(record.get().mNpdt12.mLevel); - case 1: return QVariant(QVariant::UserType); - case 2: return QVariant(QVariant::UserType); - case 3: return QVariant(QVariant::UserType); - case 4: return QVariant(QVariant::UserType); + case 0: return static_cast(npc.mNpdt12.mLevel); + case 1: + { + UserInt i(0); // unknown + return QVariant(QVariant::fromValue(i)); + } + case 2: + { + UserInt i(stats->getHealth()); + return QVariant(QVariant::fromValue(i)); + } + case 3: + { + UserInt i(stats->getMana()); + return QVariant(QVariant::fromValue(i)); + } + case 4: + { + UserInt i(stats->getFatigue()); + return QVariant(QVariant::fromValue(i)); + } case 5: return static_cast(record.get().mNpdt12.mDisposition); case 6: return static_cast(record.get().mNpdt12.mReputation); case 7: return static_cast(record.get().mNpdt12.mRank); @@ -905,6 +1001,7 @@ QVariant CSMWorld::NpcMiscRefIdAdapter::getNestedData (const RefIdColumn *column case 9: return record.get().mPersistent == true; default: return QVariant(); // throw an exception here? } + } else switch (subColIndex) { @@ -934,31 +1031,31 @@ void CSMWorld::NpcMiscRefIdAdapter::setNestedData (const RefIdColumn *column, if (autoCalc) switch(subColIndex) { - case 0: npc.mNpdt12.mLevel = static_cast(value.toInt()); 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()); 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; + 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()); 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 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; + 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? } @@ -1063,3 +1160,238 @@ void CSMWorld::WeaponRefIdAdapter::setData (const RefIdColumn *column, RefIdData EnchantableRefIdAdapter::setData (column, data, index, value); } } + +namespace CSMWorld +{ + +template <> +void 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); +} + +template <> +void 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); +} + +template <> +void 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); +} + +template <> +QVariant 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!"); + } +} + +template <> +int NestedSpellRefIdAdapter::getNestedColumnsCount(const RefIdColumn *column, + const RefIdData& data) const +{ + return 5; +} + +template <> +int 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 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 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 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 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 NestedSpellRefIdAdapter::getNestedColumnsCount(const RefIdColumn *column, + const RefIdData& data) const +{ + return 2; +} + +template <> +int 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 869996da5..4a8288fa4 100644 --- a/apps/opencs/model/world/refidadapterimp.hpp +++ b/apps/opencs/model/world/refidadapterimp.hpp @@ -11,12 +11,17 @@ #include #include #include +#include +#include +#include #include "record.hpp" #include "refiddata.hpp" #include "universalid.hpp" #include "refidadapter.hpp" #include "nestedtablewrapper.hpp" +#include "idcollection.hpp" +#include "data.hpp" namespace CSMWorld { @@ -802,10 +807,11 @@ namespace CSMWorld class NpcRefIdAdapter : public ActorRefIdAdapter { NpcColumns mColumns; + const Data& mData; public: - NpcRefIdAdapter (const NpcColumns& columns); + NpcRefIdAdapter (const NpcColumns& columns, const Data& data); virtual QVariant getData (const RefIdColumn *column, const RefIdData& data, int index) const; @@ -850,9 +856,11 @@ namespace CSMWorld class NpcAttributesRefIdAdapter : public NestedRefIdAdapterBase { + const Data& mData; + public: - NpcAttributesRefIdAdapter (); + NpcAttributesRefIdAdapter (const Data& data); virtual void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const; @@ -879,9 +887,11 @@ namespace CSMWorld class NpcSkillsRefIdAdapter : public NestedRefIdAdapterBase { + const Data& mData; + public: - NpcSkillsRefIdAdapter (); + NpcSkillsRefIdAdapter (const Data& data); virtual void addNestedRow (const RefIdColumn *column, RefIdData& data, int index, int position) const; @@ -908,12 +918,14 @@ namespace CSMWorld class NpcMiscRefIdAdapter : public NestedRefIdAdapterBase { + const Data& mData; + NpcMiscRefIdAdapter (const NpcMiscRefIdAdapter&); NpcMiscRefIdAdapter& operator= (const NpcMiscRefIdAdapter&); public: - NpcMiscRefIdAdapter (); + NpcMiscRefIdAdapter (const Data& data); virtual ~NpcMiscRefIdAdapter(); virtual void addNestedRow (const RefIdColumn *column, @@ -1161,6 +1173,7 @@ namespace CSMWorld class NestedSpellRefIdAdapter : public NestedRefIdAdapterBase { UniversalId::Type mType; + const Data& mData; // not implemented NestedSpellRefIdAdapter (const NestedSpellRefIdAdapter&); @@ -1168,45 +1181,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 @@ -1232,55 +1215,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 5495926b4..fc52fe388 100644 --- a/apps/opencs/model/world/refidcollection.cpp +++ b/apps/opencs/model/world/refidcollection.cpp @@ -10,6 +10,7 @@ #include "columns.hpp" #include "nestedtablewrapper.hpp" #include "nestedcoladapterimp.hpp" +#include "data.hpp" CSMWorld::RefIdColumn::RefIdColumn (int columnId, Display displayType, int flag, bool editable, bool userEditable) @@ -36,7 +37,7 @@ const CSMWorld::RefIdAdapter& CSMWorld::RefIdCollection::findAdapter (UniversalI return *iter->second; } -CSMWorld::RefIdCollection::RefIdCollection() +CSMWorld::RefIdCollection::RefIdCollection(const CSMWorld::Data& data) { BaseColumns baseColumns; @@ -145,12 +146,21 @@ CSMWorld::RefIdCollection::RefIdCollection() 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, @@ -437,7 +447,7 @@ CSMWorld::RefIdCollection::RefIdCollection() ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue)); npcColumns.mAttributes = &mColumns.back(); std::map attrMap; - attrMap.insert(std::make_pair(UniversalId::Type_Npc, new NpcAttributesRefIdAdapter())); + 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)); @@ -449,7 +459,7 @@ CSMWorld::RefIdCollection::RefIdCollection() ColumnBase::Display_NestedHeader, ColumnBase::Flag_Dialogue)); npcColumns.mSkills = &mColumns.back(); std::map skillsMap; - skillsMap.insert(std::make_pair(UniversalId::Type_Npc, new NpcSkillsRefIdAdapter())); + 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)); @@ -461,10 +471,11 @@ CSMWorld::RefIdCollection::RefIdCollection() 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())); + 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)); + new RefIdColumn (Columns::ColumnId_NpcLevel, CSMWorld::ColumnBase::Display_Integer, + ColumnBase::Flag_Dialogue | ColumnBase::Flag_Dialogue_Refresh)); mColumns.back().addColumn( new RefIdColumn (Columns::ColumnId_NpcFactionID, CSMWorld::ColumnBase::Display_Integer)); mColumns.back().addColumn( @@ -609,7 +620,7 @@ CSMWorld::RefIdCollection::RefIdCollection() mAdapters.insert (std::make_pair (UniversalId::Type_Miscellaneous, new MiscRefIdAdapter (inventoryColumns, key))); mAdapters.insert (std::make_pair (UniversalId::Type_Npc, - new NpcRefIdAdapter (npcColumns))); + new NpcRefIdAdapter (npcColumns, data))); 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/model/world/refidcollection.hpp b/apps/opencs/model/world/refidcollection.hpp index 4d511d12d..e8e633663 100644 --- a/apps/opencs/model/world/refidcollection.hpp +++ b/apps/opencs/model/world/refidcollection.hpp @@ -20,6 +20,7 @@ namespace CSMWorld class RefIdAdapter; struct NestedTableWrapperBase; class NestedRefIdAdapterBase; + class Data; class RefIdColumn : public NestableColumn { @@ -56,7 +57,8 @@ namespace CSMWorld public: - RefIdCollection(); + // race, classes and skills required for NPC autocalc + RefIdCollection(const Data& data); virtual ~RefIdCollection(); diff --git a/apps/opencs/model/world/usertype.hpp b/apps/opencs/model/world/usertype.hpp new file mode 100644 index 000000000..23b5c90a4 --- /dev/null +++ b/apps/opencs/model/world/usertype.hpp @@ -0,0 +1,44 @@ +#ifndef CSM_WORLD_USERTYPE_H +#define CSM_WORLD_USERTYPE_H + +#include +#include + +namespace CSMWorld +{ + // Idea from ksimons @stackoverflow + class UserInt + { + public: + + UserInt() : mValue(0) { } + UserInt(int value) : mValue(value) { } + UserInt(const UserInt &other) { mValue = other.mValue; } + ~UserInt() { } + int value() const { return mValue; } + + private: + + int mValue; + }; + + class UserFloat + { + public: + + UserFloat() : mValue(0) { } + UserFloat(float value) : mValue(value) { } + UserFloat(const UserFloat &other) { mValue = other.mValue; } + ~UserFloat() { } + float value() const { return mValue; } + + private: + + float mValue; + }; +} + +Q_DECLARE_METATYPE(CSMWorld::UserInt) +Q_DECLARE_METATYPE(CSMWorld::UserFloat) + +#endif // CSM_WORLD_USERTYPE_H diff --git a/apps/opencs/view/world/dialoguesubview.cpp b/apps/opencs/view/world/dialoguesubview.cpp index 99650834e..a366f53ab 100644 --- a/apps/opencs/view/world/dialoguesubview.cpp +++ b/apps/opencs/view/world/dialoguesubview.cpp @@ -66,7 +66,7 @@ void CSVWorld::NotEditableSubDelegate::setEditorData (QWidget* editor, const QMo CSMWorld::Columns::ColumnId columnId = static_cast ( mTable->getColumnId (index.column())); - + if (QVariant::String == v.type()) { label->setText(v.toString()); @@ -75,7 +75,7 @@ void CSVWorld::NotEditableSubDelegate::setEditorData (QWidget* editor, const QMo { int data = v.toInt(); std::vector enumNames (CSMWorld::Columns::getEnums (columnId)); - + label->setText(QString::fromUtf8(enumNames.at(data).c_str())); } else @@ -430,16 +430,18 @@ void CSVWorld::EditWidget::remake(int row) static_cast (mTable->data (mTable->index (row, typeColumn)).toInt()), mTable->data (mTable->index (row, idColumn)).toString().toUtf8().constData()); - NestedTable* table = new NestedTable(mDocument, id, mNestedModels.back(), this); - table->resizeColumnsToContents(); - - if(mTable->index(row, i).data().type() == QVariant::UserType) + bool editable = mTable->index(row, i).data().type() != QVariant::UserType; + NestedTable* table = new NestedTable(mDocument, id, mNestedModels.back(), this, editable); + if (!editable) { table->setEditTriggers(QAbstractItemView::NoEditTriggers); - table->setEnabled(false); + table->setSelectionMode(QAbstractItemView::NoSelection); + table->setStyleSheet("QTableView { color: gray; }"); + table->horizontalHeader()->setStyleSheet("QHeaderView { color: gray; }"); } else table->setEditTriggers(QAbstractItemView::SelectedClicked | QAbstractItemView::CurrentChanged); + table->resizeColumnsToContents(); int rows = mTable->rowCount(mTable->index(row, i)); int rowHeight = (rows == 0) ? table->horizontalHeader()->height() : table->rowHeight(0); @@ -610,6 +612,14 @@ CSVWorld::SimpleDialogueSubView::SimpleDialogueSubView (const CSMWorld::Universa mEditWidget = new EditWidget(mainWidget, mTable->getModelIndex(mCurrentId, 0).row(), mTable, mCommandDispatcher, document, false); + 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); @@ -699,6 +709,29 @@ void CSVWorld::SimpleDialogueSubView::changeCurrentId (const std::string& newId) mCommandDispatcher.setSelection(selection); } +void CSVWorld::SimpleDialogueSubView::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); + } +} CSVWorld::DialogueSubView::DialogueSubView (const CSMWorld::UniversalId& id, CSMDoc::Document& document, const CreatorFactoryBase& creatorFactory, bool sorting) @@ -774,7 +807,7 @@ CSVWorld::DialogueSubView::DialogueSubView (const CSMWorld::UniversalId& id, deleteButton->setDisabled (true); } - getMainLayout().addLayout (buttonsLayout); + getMainLayout().addLayout (buttonsLayout); } void CSVWorld::DialogueSubView::cloneRequest() @@ -855,7 +888,6 @@ void CSVWorld::DialogueSubView::nextId () } } - void CSVWorld::DialogueSubView::showPreview () { QModelIndex currentIndex (getTable().getModelIndex (getCurrentId(), 0)); diff --git a/apps/opencs/view/world/dialoguesubview.hpp b/apps/opencs/view/world/dialoguesubview.hpp index 5d6915d42..ecfb8df73 100644 --- a/apps/opencs/view/world/dialoguesubview.hpp +++ b/apps/opencs/view/world/dialoguesubview.hpp @@ -216,6 +216,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); }; class DialogueSubView : public SimpleDialogueSubView diff --git a/apps/opencs/view/world/nestedtable.cpp b/apps/opencs/view/world/nestedtable.cpp index 92df59a5f..e4447397d 100644 --- a/apps/opencs/view/world/nestedtable.cpp +++ b/apps/opencs/view/world/nestedtable.cpp @@ -13,12 +13,13 @@ CSVWorld::NestedTable::NestedTable(CSMDoc::Document& document, CSMWorld::UniversalId id, CSMWorld::NestedTableProxyModel* model, - QWidget* parent) + QWidget* parent, + bool editable) : DragRecordTable(document, parent), + mAddNewRowAction(0), + mRemoveRowAction(0), mModel(model) { - mDispatcher = new CSMWorld::CommandDispatcher (document, id, this); - setSelectionBehavior (QAbstractItemView::SelectRows); setSelectionMode (QAbstractItemView::ExtendedSelection); @@ -31,30 +32,36 @@ CSVWorld::NestedTable::NestedTable(CSMDoc::Document& document, int columns = model->columnCount(QModelIndex()); - for(int i = 0 ; i < columns; ++i) - { - CSMWorld::ColumnBase::Display display = static_cast ( - model->headerData (i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); - - CommandDelegate *delegate = CommandDelegateFactoryCollection::get().makeDelegate(display, - mDispatcher, - document, - this); - - setItemDelegateForColumn(i, delegate); - } - setModel(model); - mAddNewRowAction = new QAction (tr ("Add new row"), this); + setAcceptDrops(true); - connect(mAddNewRowAction, SIGNAL(triggered()), - this, SLOT(addNewRowActionTriggered())); + if (editable) + { + mDispatcher = new CSMWorld::CommandDispatcher (document, id, this); + for(int i = 0 ; i < columns; ++i) + { + CSMWorld::ColumnBase::Display display = static_cast ( + model->headerData (i, Qt::Horizontal, CSMWorld::ColumnBase::Role_Display).toInt()); - mRemoveRowAction = new QAction (tr ("Remove row"), this); + CommandDelegate *delegate = CommandDelegateFactoryCollection::get().makeDelegate(display, + mDispatcher, + document, + this); - connect(mRemoveRowAction, SIGNAL(triggered()), - this, SLOT(removeRowActionTriggered())); + setItemDelegateForColumn(i, delegate); + } + + mAddNewRowAction = new QAction (tr ("Add new row"), this); + + connect(mAddNewRowAction, SIGNAL(triggered()), + this, SLOT(addNewRowActionTriggered())); + + mRemoveRowAction = new QAction (tr ("Remove row"), this); + + connect(mRemoveRowAction, SIGNAL(triggered()), + this, SLOT(removeRowActionTriggered())); + } } std::vector CSVWorld::NestedTable::getDraggedRecords() const @@ -65,6 +72,9 @@ std::vector CSVWorld::NestedTable::getDraggedRecords() co void CSVWorld::NestedTable::contextMenuEvent (QContextMenuEvent *event) { + if (!mRemoveRowAction || !mAddNewRowAction) + return; + QModelIndexList selectedRows = selectionModel()->selectedRows(); QMenu menu(this); diff --git a/apps/opencs/view/world/nestedtable.hpp b/apps/opencs/view/world/nestedtable.hpp index 112920401..70008fa97 100644 --- a/apps/opencs/view/world/nestedtable.hpp +++ b/apps/opencs/view/world/nestedtable.hpp @@ -35,7 +35,8 @@ namespace CSVWorld NestedTable(CSMDoc::Document& document, CSMWorld::UniversalId id, CSMWorld::NestedTableProxyModel* model, - QWidget* parent = NULL); + QWidget* parent = NULL, + bool editable = true); virtual std::vector getDraggedRecords() const; diff --git a/apps/opencs/view/world/util.cpp b/apps/opencs/view/world/util.cpp index f21658581..8d83ab567 100644 --- a/apps/opencs/view/world/util.cpp +++ b/apps/opencs/view/world/util.cpp @@ -19,6 +19,7 @@ #include "../../model/world/commanddispatcher.hpp" #include "../widget/coloreditor.hpp" +#include "../../model/world/usertype.hpp" #include "../widget/droplineedit.hpp" #include "dialoguespinbox.hpp" @@ -136,8 +137,8 @@ void CSVWorld::CommandDelegate::setModelDataImp (QWidget *editor, QAbstractItemM } else { - NastyTableModelHack hack (*model); - QStyledItemDelegate::setModelData (editor, &hack, index); + NastyTableModelHack hack (*model); + QStyledItemDelegate::setModelData (editor, &hack, index); new_ = hack.getData(); } @@ -166,7 +167,7 @@ QWidget *CSVWorld::CommandDelegate::createEditor (QWidget *parent, const QStyleO const QModelIndex& index) const { CSMWorld::ColumnBase::Display display = getDisplayTypeFromIndex(index); - + // This createEditor() method is called implicitly from tables. // For boolean values in tables use the default editor (combobox). // Checkboxes is looking ugly in the table view. @@ -324,8 +325,15 @@ void CSVWorld::CommandDelegate::setEditorData (QWidget *editor, const QModelInde if (!n.isEmpty()) { if (!v.isValid()) - v = QVariant(editor->property(n).userType(), (const void *)0); - editor->setProperty(n, v); + editor->setProperty(n, QVariant(editor->property(n).userType(), (const void *)0)); + else if (v.type() == QVariant::UserType + && QString(v.typeName()) == "CSMWorld::UserFloat" && v.canConvert()) + editor->setProperty(n, QVariant(v.value().value())); + else if (v.type() == QVariant::UserType + && QString(v.typeName()) == "CSMWorld::UserInt" && v.canConvert()) + editor->setProperty(n, QVariant(v.value().value())); + else + editor->setProperty(n, v); } } diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index c7c701d20..d54152100 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 3ca57aca8..3b0234a25 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,44 @@ 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; - } - - 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; - } - - void autoCalculateAttributes (const ESM::NPC* npc, MWMechanics::CreatureStats& creatureStats) + class Stats : public AutoCalc::StatsBase + { + MWMechanics::NpcStats& mNpcStats; + + public: + + Stats(MWMechanics::NpcStats& npcStats) : mNpcStats(npcStats) {} + + virtual unsigned char getBaseAttribute(int index) const { return mNpcStats.getAttribute(index).getBase(); } + + virtual void setAttribute(int index, unsigned char value) { mNpcStats.setAttribute(index, value); } + + virtual void addSpell(const std::string& id) { mNpcStats.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::NpcStats& npcStats) { - // 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 = npcStats.getLevel(); - // skill bonus - for (int attribute=0; attribute < ESM::Attribute::Length; ++attribute) - { - float modifierSum = 0; + Stats stats(npcStats); - for (int j=0; jgetStore().get().find(j); + MWWorld::MWStore store; - if (skill->mData.mAttribute != attribute) - continue; + AutoCalc::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)); + npcStats.setHealth(AutoCalc::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 +109,13 @@ namespace const ESM::Race *race = MWBase::Environment::get().getWorld()->getStore().get().find(npc->mRace); + Stats stats(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); - } - } - } + AutoCalc::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); + AutoCalc::autoCalculateSpells(race, stats, &store); } } @@ -392,7 +260,7 @@ namespace MWClass // store ptr.getRefData().setCustomData (data.release()); - getInventoryStore(ptr).autoEquip(ptr); + getInventoryStore(ptr).autoEquip(ptr); } } diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index c829154e2..dc388555e 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 (AutoCalc::calcAutoCastChance(spell, skills, attributes, -1, &store) < fAutoPCSpellChance) continue; - if (!attrSkillCheck(spell, skills, attributes)) + if (!AutoCalc::attrSkillCheck(spell, skills, attributes, &store)) continue; selectedSpells.push_back(spell->mId); diff --git a/apps/openmw/mwworld/mwstore.cpp b/apps/openmw/mwworld/mwstore.cpp new file mode 100644 index 000000000..bdc61033e --- /dev/null +++ b/apps/openmw/mwworld/mwstore.cpp @@ -0,0 +1,37 @@ +#include "mwstore.hpp" + +#include "../mwbase/world.hpp" +#include "../mwbase/environment.hpp" + +#include "esmstore.hpp" + +namespace MWWorld +{ + MWStore::MWStore() + : mGmst(MWBase::Environment::get().getWorld()->getStore().get()), + mSpells(MWBase::Environment::get().getWorld()->getStore().get()) + { } + + MWStore::~MWStore() + { } + + int MWStore::findGmstInt(const std::string& name) const { return mGmst.find(name)->getInt(); } + + float MWStore::findGmstFloat(const std::string& name) const { return mGmst.find(name)->getFloat(); } + + const ESM::Skill *MWStore::findSkill(int index) const + { + return MWBase::Environment::get().getWorld()->getStore().get().find(index); + } + + const ESM::MagicEffect* MWStore::findMagicEffect(int id) const + { + return MWBase::Environment::get().getWorld()->getStore().get().find(id); + } + + void MWStore::getSpells(std::vector& spells) + { + for (Store::iterator iter = mSpells.begin(); iter != mSpells.end(); ++iter) + spells.push_back(const_cast(&*iter)); + } +} diff --git a/apps/openmw/mwworld/mwstore.hpp b/apps/openmw/mwworld/mwstore.hpp new file mode 100644 index 000000000..b833f1503 --- /dev/null +++ b/apps/openmw/mwworld/mwstore.hpp @@ -0,0 +1,34 @@ +#ifndef GAME_MWWORLD_MWSTORE_H +#define GAME_MWWORLD_MWSTORE_H + +#include + +#include + +#include "store.hpp" + +namespace MWWorld +{ + class MWStore : public AutoCalc::StoreCommon + { + const MWWorld::Store& mGmst; + const MWWorld::Store &mSpells; + + public: + + MWStore(); + ~MWStore(); + + virtual int findGmstInt(const std::string& name) const; + + virtual float findGmstFloat(const std::string& name) const; + + virtual const ESM::Skill *findSkill(int index) const; + + virtual const ESM::MagicEffect* findMagicEffect(int id) const; + + virtual void getSpells(std::vector& spells); + }; +} + +#endif // GAME_MWWORLD_MWSTORE_H diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 1b33b10f6..2938dcd81 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -119,6 +119,10 @@ add_component_dir (fontloader fontloader ) +add_component_dir (autocalc + autocalc autocalcspell + ) + add_component_dir (version version ) diff --git a/components/autocalc/autocalc.cpp b/components/autocalc/autocalc.cpp new file mode 100644 index 000000000..122d19763 --- /dev/null +++ b/components/autocalc/autocalc.cpp @@ -0,0 +1,208 @@ +#include "autocalc.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +#include "autocalcspell.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; + } +} + +namespace AutoCalc +{ + void autoCalcAttributesImpl (const ESM::NPC* npc, + const ESM::Race *race, const ESM::Class *class_, int level, StatsBase& stats, StoreCommon *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, StoreCommon *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, 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_, const 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, StoreCommon *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.addSpell(*it); + } + + StatsBase::StatsBase() {} + + StatsBase::~StatsBase() {} +} diff --git a/components/autocalc/autocalc.hpp b/components/autocalc/autocalc.hpp new file mode 100644 index 000000000..5cfe06b42 --- /dev/null +++ b/components/autocalc/autocalc.hpp @@ -0,0 +1,47 @@ +#ifndef COMPONENTS_AUTOCALC_AUTOCALC_H +#define COMPONENTS_AUTOCALC_AUTOCALC_H + +#include + +#include "store.hpp" + +namespace ESM +{ + struct NPC; + struct Race; + struct Class; +} + +namespace AutoCalc +{ + // 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 addSpell(const 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, StoreCommon *store); + + void autoCalcSkillsImpl (const ESM::NPC* npc, + const ESM::Race *race, const ESM::Class *class_, int level, StatsBase& stats, StoreCommon *store); + + unsigned short autoCalculateHealth(int level, const ESM::Class *class_, const StatsBase& stats); + + void autoCalculateSpells(const ESM::Race *race, StatsBase& stats, StoreCommon *store); +} +#endif // COMPONENTS_AUTOCALC_AUTOCALC_H diff --git a/components/autocalc/autocalcspell.cpp b/components/autocalc/autocalcspell.cpp new file mode 100644 index 000000000..78499a092 --- /dev/null +++ b/components/autocalc/autocalcspell.cpp @@ -0,0 +1,249 @@ +#include "autocalcspell.hpp" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "autocalc.hpp" + +namespace AutoCalc +{ + + 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, StoreCommon *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; + std::vector spells; + store->getSpells(spells); + + // 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 (std::vector::const_iterator iter = spells.begin(); iter != spells.end(); ++iter) + { + 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) + { + std::vector::const_iterator it = spells.begin(); + for (; it != spells.end(); ++it) + { + if ((*it)->mId == *weakIt) + break; + } + + if (it == spells.end()) + continue; + + const ESM::Spell* testSpell = *it; + + //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, StoreCommon *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, StoreCommon *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, StoreCommon *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/autocalc/autocalcspell.hpp b/components/autocalc/autocalcspell.hpp new file mode 100644 index 000000000..769e60248 --- /dev/null +++ b/components/autocalc/autocalcspell.hpp @@ -0,0 +1,39 @@ +#ifndef COMPONENTS_AUTOCALC_AUTOCALCSPELL_H +#define COMPONENTS_AUTOCALC_AUTOCALCSPELL_H + +#include + +#include + +namespace ESM +{ + struct Spell; + struct Race; +} + +namespace AutoCalc +{ + +class StoreCommon; + +/// Contains algorithm for calculating an NPC's spells based on stats + +std::vector autoCalcNpcSpells(const int* actorSkills, + const int* actorAttributes, const ESM::Race* race, StoreCommon *store); + +// Helpers + +bool attrSkillCheck (const ESM::Spell* spell, + const int* actorSkills, const int* actorAttributes, StoreCommon *store); + +ESM::Skill::SkillEnum mapSchoolToSkill(int school); + +void calcWeakestSchool(const ESM::Spell* spell, + const int* actorSkills, int& effectiveSchool, float& skillTerm, StoreCommon *store); + +float calcAutoCastChance(const ESM::Spell* spell, + const int* actorSkills, const int* actorAttributes, int effectiveSchool, StoreCommon *store); + +} + +#endif // COMPONENTS_AUTOCALC_AUTOCALCSPELL_H diff --git a/components/autocalc/store.hpp b/components/autocalc/store.hpp new file mode 100644 index 000000000..9a798a5ce --- /dev/null +++ b/components/autocalc/store.hpp @@ -0,0 +1,35 @@ +#ifndef COMPONENTS_AUTOCALC_STORE_H +#define COMPONENTS_AUTOCALC_STORE_H + +#include +#include + +namespace ESM +{ + struct Spell; + struct Skill; + struct MagicEffect; +} + +namespace AutoCalc +{ + // interface class for sharing the autocalc component between OpenMW and OpenCS + class StoreCommon + { + + public: + StoreCommon() {} + virtual ~StoreCommon() {} + + 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 void getSpells(std::vector& spells) = 0; + }; +} +#endif // COMPONENTS_AUTOCALC_STORE_H