mirror of
				https://github.com/TES3MP/openmw-tes3mp.git
				synced 2025-10-31 20:56:42 +00:00 
			
		
		
		
	Merge remote-tracking branch 'scrawl/master'
This commit is contained in:
		
						commit
						cae0c4a044
					
				
					 30 changed files with 708 additions and 218 deletions
				
			
		|  | @ -12,7 +12,7 @@ set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/) | |||
| message(STATUS "Configuring OpenMW...") | ||||
| 
 | ||||
| set(OPENMW_VERSION_MAJOR 0) | ||||
| set(OPENMW_VERSION_MINOR 30) | ||||
| set(OPENMW_VERSION_MINOR 31) | ||||
| set(OPENMW_VERSION_RELEASE 0) | ||||
| 
 | ||||
| set(OPENMW_VERSION_COMMITHASH "") | ||||
|  |  | |||
|  | @ -69,7 +69,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 | ||||
|     disease pickpocket levelledlist combat steering obstacle autocalcspell | ||||
|     ) | ||||
| 
 | ||||
| add_openmw_dir (mwstate | ||||
|  |  | |||
|  | @ -250,8 +250,11 @@ namespace MWClass | |||
|             text += "\n#{sTrapped}"; | ||||
| 
 | ||||
|         if (MWBase::Environment::get().getWindowManager()->getFullHelp()) | ||||
|         { | ||||
|             text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script"); | ||||
| 
 | ||||
|             text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getOwner(), "Owner"); | ||||
|             text += MWGui::ToolTips::getMiscString(ptr.getCellRef().getFaction(), "Faction"); | ||||
|         } | ||||
|         info.text = text; | ||||
| 
 | ||||
|         return info; | ||||
|  |  | |||
|  | @ -22,6 +22,7 @@ | |||
| #include "../mwmechanics/spellcasting.hpp" | ||||
| #include "../mwmechanics/disease.hpp" | ||||
| #include "../mwmechanics/combat.hpp" | ||||
| #include "../mwmechanics/autocalcspell.hpp" | ||||
| 
 | ||||
| #include "../mwworld/ptr.hpp" | ||||
| #include "../mwworld/actiontalk.hpp" | ||||
|  | @ -53,6 +54,24 @@ 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 i; | ||||
|         if(d > 0.5) | ||||
|             return i + 1.0; | ||||
|         if(is_even(i)) | ||||
|             return i; | ||||
|         return i + 1.0; | ||||
|     } | ||||
| 
 | ||||
|     void autoCalculateAttributes (const ESM::NPC* npc, MWMechanics::CreatureStats& creatureStats) | ||||
|     { | ||||
|         // race bonus
 | ||||
|  | @ -108,8 +127,9 @@ namespace | |||
|                 } | ||||
|                 modifierSum += add; | ||||
|             } | ||||
|             creatureStats.setAttribute(attribute, std::min(creatureStats.getAttribute(attribute).getBase() | ||||
|                 + static_cast<int>((level-1) * modifierSum+0.5), 100) ); | ||||
|             creatureStats.setAttribute(attribute, std::min( | ||||
|                                            round_ieee_754(creatureStats.getAttribute(attribute).getBase() | ||||
|                 + (level-1) * modifierSum), 100) ); | ||||
|         } | ||||
| 
 | ||||
|         // initial health
 | ||||
|  | @ -193,18 +213,6 @@ namespace | |||
|                     majorMultiplier = 1.0f; | ||||
|                     break; | ||||
|                 } | ||||
|                 if (class_->mData.mSkills[k][1] == skillIndex) | ||||
|                 { | ||||
|                     // Major skill -> add starting spells for this skill if existing
 | ||||
|                     const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); | ||||
|                     MWWorld::Store<ESM::Spell>::iterator it = store.get<ESM::Spell>().begin(); | ||||
|                     for (; it != store.get<ESM::Spell>().end(); ++it) | ||||
|                     { | ||||
|                         if (it->mData.mFlags & ESM::Spell::F_Autocalc | ||||
|                                 && MWMechanics::spellSchoolToSkill(MWMechanics::getSpellSchool(&*it, ptr)) == skillIndex) | ||||
|                             npcStats.getSpells().add(it->mId); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // is this skill in the same Specialization as the class?
 | ||||
|  | @ -217,12 +225,25 @@ namespace | |||
| 
 | ||||
|             npcStats.getSkill(skillIndex).setBase( | ||||
|                   std::min( | ||||
|                     npcStats.getSkill(skillIndex).getBase() | ||||
|                     round_ieee_754( | ||||
|                             npcStats.getSkill(skillIndex).getBase() | ||||
|                     + 5 | ||||
|                     + raceBonus | ||||
|                     + specBonus | ||||
|                     + static_cast<int>((level-1) * (majorMultiplier + specMultiplier)), 100)); | ||||
|                     +(int(level)-1) * (majorMultiplier + specMultiplier)), 100)); // Must gracefully handle level 0
 | ||||
|         } | ||||
| 
 | ||||
|         int skills[ESM::Skill::Length]; | ||||
|         for (int i=0; i<ESM::Skill::Length; ++i) | ||||
|             skills[i] = npcStats.getSkill(i).getBase(); | ||||
| 
 | ||||
|         int attributes[ESM::Attribute::Length]; | ||||
|         for (int i=0; i<ESM::Attribute::Length; ++i) | ||||
|             attributes[i] = npcStats.getAttribute(i).getBase(); | ||||
| 
 | ||||
|         std::vector<std::string> spells = MWMechanics::autoCalcNpcSpells(skills, attributes, race); | ||||
|         for (std::vector<std::string>::iterator it = spells.begin(); it != spells.end(); ++it) | ||||
|             npcStats.getSpells().add(*it); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -153,6 +153,11 @@ void CompanionWindow::onReferenceUnavailable() | |||
|     MWBase::Environment::get().getWindowManager()->removeGuiMode(GM_Companion); | ||||
| } | ||||
| 
 | ||||
| void CompanionWindow::resetReference() | ||||
| { | ||||
|     ReferenceInterface::resetReference(); | ||||
|     mItemView->setModel(NULL); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -20,6 +20,8 @@ namespace MWGui | |||
| 
 | ||||
|         virtual void exit(); | ||||
| 
 | ||||
|         virtual void resetReference(); | ||||
| 
 | ||||
|         void open(const MWWorld::Ptr& npc); | ||||
|         void onFrame (); | ||||
| 
 | ||||
|  |  | |||
|  | @ -258,6 +258,12 @@ namespace MWGui | |||
|             onTakeAllButtonClicked(mTakeButton); | ||||
|     } | ||||
| 
 | ||||
|     void ContainerWindow::resetReference() | ||||
|     { | ||||
|         ReferenceInterface::resetReference(); | ||||
|         mItemView->setModel(NULL); | ||||
|     } | ||||
| 
 | ||||
|     void ContainerWindow::close() | ||||
|     { | ||||
|         WindowBase::close(); | ||||
|  |  | |||
|  | @ -54,6 +54,8 @@ namespace MWGui | |||
|         void open(const MWWorld::Ptr& container, bool loot=false); | ||||
|         virtual void close(); | ||||
| 
 | ||||
|         virtual void resetReference(); | ||||
| 
 | ||||
|         virtual void exit(); | ||||
| 
 | ||||
|     private: | ||||
|  |  | |||
|  | @ -260,21 +260,28 @@ namespace MWGui | |||
| 
 | ||||
|             // More hacks! The french game uses several win1252 characters that are not included
 | ||||
|             // in the cp437 encoding of the font. Fall back to similar available characters.
 | ||||
|             // Same for U+2013
 | ||||
|             std::map<int, int> additional; | ||||
|             additional[39] = 0x2019; // apostrophe
 | ||||
|             additional[45] = 0x2013; // dash
 | ||||
|             if (additional.find(i) != additional.end() && mEncoding == ToUTF8::CP437) | ||||
|             if (mEncoding == ToUTF8::CP437) | ||||
|             { | ||||
|                 MyGUI::xml::ElementPtr code = codes->createChild("Code"); | ||||
|                 code->addAttribute("index", additional[i]); | ||||
|                 code->addAttribute("coord", MyGUI::utility::toString(x1) + " " | ||||
|                                             + MyGUI::utility::toString(y1) + " " | ||||
|                                             + MyGUI::utility::toString(w) + " " | ||||
|                                             + MyGUI::utility::toString(h)); | ||||
|                 code->addAttribute("advance", data[i].width); | ||||
|                 code->addAttribute("bearing", MyGUI::utility::toString(data[i].kerning) + " " | ||||
|                                    + MyGUI::utility::toString((fontSize-data[i].ascent))); | ||||
|                 std::multimap<int, int> additional; | ||||
|                 additional.insert(std::make_pair(39, 0x2019)); // apostrophe
 | ||||
|                 additional.insert(std::make_pair(45, 0x2013)); // dash
 | ||||
|                 additional.insert(std::make_pair(34, 0x201D)); // right double quotation mark
 | ||||
|                 additional.insert(std::make_pair(34, 0x201C)); // left double quotation mark
 | ||||
|                 for (std::multimap<int, int>::iterator it = additional.begin(); it != additional.end(); ++it) | ||||
|                 { | ||||
|                     if (it->first != i) | ||||
|                         continue; | ||||
| 
 | ||||
|                     MyGUI::xml::ElementPtr code = codes->createChild("Code"); | ||||
|                     code->addAttribute("index", it->second); | ||||
|                     code->addAttribute("coord", MyGUI::utility::toString(x1) + " " | ||||
|                                                 + MyGUI::utility::toString(y1) + " " | ||||
|                                                 + MyGUI::utility::toString(w) + " " | ||||
|                                                 + MyGUI::utility::toString(h)); | ||||
|                     code->addAttribute("advance", data[i].width); | ||||
|                     code->addAttribute("bearing", MyGUI::utility::toString(data[i].kerning) + " " | ||||
|                                        + MyGUI::utility::toString((fontSize-data[i].ascent))); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // ASCII vertical bar, use this as text input cursor
 | ||||
|  |  | |||
|  | @ -140,26 +140,28 @@ void ItemView::onMouseWheel(MyGUI::Widget *_sender, int _rel) | |||
| 
 | ||||
| void ItemView::setSize(const MyGUI::IntSize &_value) | ||||
| { | ||||
|     bool changed = (_value.width != getWidth() || _value.height != getHeight()); | ||||
|     Base::setSize(_value); | ||||
|     update(); | ||||
|     if (changed) | ||||
|         update(); | ||||
| } | ||||
| 
 | ||||
| void ItemView::setSize(int _width, int _height) | ||||
| { | ||||
|     Base::setSize(_width, _height); | ||||
|     update(); | ||||
|     setSize(MyGUI::IntSize(_width, _height)); | ||||
| } | ||||
| 
 | ||||
| void ItemView::setCoord(const MyGUI::IntCoord &_value) | ||||
| { | ||||
|     bool changed = (_value.width != getWidth() || _value.height != getHeight()); | ||||
|     Base::setCoord(_value); | ||||
|     update(); | ||||
|     if (changed) | ||||
|         update(); | ||||
| } | ||||
| 
 | ||||
| void ItemView::setCoord(int _left, int _top, int _width, int _height) | ||||
| { | ||||
|     Base::setCoord(_left, _top, _width, _height); | ||||
|     update(); | ||||
|     setCoord(MyGUI::IntCoord(_left, _top, _width, _height)); | ||||
| } | ||||
| 
 | ||||
| void ItemView::registerComponents() | ||||
|  |  | |||
|  | @ -531,4 +531,10 @@ namespace MWGui | |||
|             sellerStats.setLastRestockTime(MWBase::Environment::get().getWorld()->getTimeStamp()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     void TradeWindow::resetReference() | ||||
|     { | ||||
|         ReferenceInterface::resetReference(); | ||||
|         mItemView->setModel(NULL); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -37,6 +37,7 @@ namespace MWGui | |||
| 
 | ||||
|             virtual void exit(); | ||||
| 
 | ||||
|             virtual void resetReference(); | ||||
| 
 | ||||
|         private: | ||||
|             ItemView* mItemView; | ||||
|  |  | |||
|  | @ -707,11 +707,13 @@ namespace MWInput | |||
|     } | ||||
| 
 | ||||
|     void InputManager::quickLoad() { | ||||
|         MWBase::Environment::get().getStateManager()->quickLoad(); | ||||
|         if (!MyGUI::InputManager::getInstance().isModalAny()) | ||||
|             MWBase::Environment::get().getStateManager()->quickLoad(); | ||||
|     } | ||||
| 
 | ||||
|     void InputManager::quickSave() { | ||||
|         MWBase::Environment::get().getStateManager()->quickSave(); | ||||
|         if (!MyGUI::InputManager::getInstance().isModalAny()) | ||||
|             MWBase::Environment::get().getStateManager()->quickSave(); | ||||
|     } | ||||
|     void InputManager::toggleSpell() | ||||
|     { | ||||
|  |  | |||
							
								
								
									
										232
									
								
								apps/openmw/mwmechanics/autocalcspell.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								apps/openmw/mwmechanics/autocalcspell.cpp
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,232 @@ | |||
| #include "autocalcspell.hpp" | ||||
| 
 | ||||
| #include <climits> | ||||
| 
 | ||||
| #include "../mwworld/esmstore.hpp" | ||||
| 
 | ||||
| #include "../mwbase/world.hpp" | ||||
| #include "../mwbase/environment.hpp" | ||||
| 
 | ||||
| 
 | ||||
| namespace MWMechanics | ||||
| { | ||||
| 
 | ||||
|     struct SchoolCaps | ||||
|     { | ||||
|         int mCount; | ||||
|         int mLimit; | ||||
|         bool mReachedLimit; | ||||
|         int mMinCost; | ||||
|         std::string mWeakestSpell; | ||||
|     }; | ||||
| 
 | ||||
|     std::vector<std::string> autoCalcNpcSpells(const int *actorSkills, const int *actorAttributes, const ESM::Race* race) | ||||
|     { | ||||
|         const MWWorld::Store<ESM::GameSetting>& gmst = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>(); | ||||
|         static const float fNPCbaseMagickaMult = gmst.find("fNPCbaseMagickaMult")->getFloat(); | ||||
|         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] = gmst.find(gmstName)->getInt(); | ||||
|             } | ||||
|             init = true; | ||||
|         } | ||||
| 
 | ||||
|         std::map<int, SchoolCaps> 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<std::string> selectedSpells; | ||||
| 
 | ||||
|         const MWWorld::Store<ESM::Spell> &spells = | ||||
|             MWBase::Environment::get().getWorld()->getStore().get<ESM::Spell>(); | ||||
| 
 | ||||
|         // 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 (MWWorld::Store<ESM::Spell>::iterator iter = spells.begin(); iter != spells.end(); ++iter) | ||||
|         { | ||||
|             const ESM::Spell* spell = &*iter; | ||||
| 
 | ||||
|             if (spell->mData.mType != ESM::Spell::ST_Spell) | ||||
|                 continue; | ||||
|             if (!(spell->mData.mFlags & ESM::Spell::F_Autocalc)) | ||||
|                 continue; | ||||
|             static const int iAutoSpellTimesCanCast = gmst.find("iAutoSpellTimesCanCast")->getInt(); | ||||
|             if (baseMagicka < iAutoSpellTimesCanCast * spell->mData.mCost) | ||||
|                 continue; | ||||
| 
 | ||||
|             if (race && std::find(race->mPowers.mList.begin(), race->mPowers.mList.end(), spell->mId) != race->mPowers.mList.end()) | ||||
|                 continue; | ||||
| 
 | ||||
|             if (!attrSkillCheck(spell, actorSkills, actorAttributes)) | ||||
|                 continue; | ||||
| 
 | ||||
|             int school; | ||||
|             float skillTerm; | ||||
|             calcWeakestSchool(spell, actorSkills, school, skillTerm); | ||||
|             assert(school >= 0 && school < 6); | ||||
|             SchoolCaps& cap = schoolCaps[school]; | ||||
| 
 | ||||
|             if (cap.mReachedLimit && spell->mData.mCost <= cap.mMinCost) | ||||
|                 continue; | ||||
| 
 | ||||
|             static const float fAutoSpellChance = gmst.find("fAutoSpellChance")->getFloat(); | ||||
|             if (calcAutoCastChance(spell, actorSkills, actorAttributes, school) < fAutoSpellChance) | ||||
|                 continue; | ||||
| 
 | ||||
|             selectedSpells.push_back(spell->mId); | ||||
| 
 | ||||
|             if (cap.mReachedLimit) | ||||
|             { | ||||
|                 std::vector<std::string>::iterator found = std::find(selectedSpells.begin(), selectedSpells.end(), cap.mWeakestSpell); | ||||
|                 if (found != selectedSpells.end()) | ||||
|                     selectedSpells.erase(found); | ||||
| 
 | ||||
|                 cap.mMinCost = INT_MAX; | ||||
|                 for (std::vector<std::string>::iterator weakIt = selectedSpells.begin(); weakIt != selectedSpells.end(); ++weakIt) | ||||
|                 { | ||||
|                     const ESM::Spell* testSpell = spells.find(*weakIt); | ||||
| 
 | ||||
|                     //int testSchool;
 | ||||
|                     //float dummySkillTerm;
 | ||||
|                     //calcWeakestSchool(testSpell, actorSkills, testSchool, dummySkillTerm);
 | ||||
| 
 | ||||
|                     // Note: if there are multiple spells with the same cost, we pick the first one we found.
 | ||||
|                     // So the algorithm depends on the iteration order of the outer loop.
 | ||||
|                     if ( | ||||
|                             // There is a huge bug here. It is not checked that weakestSpell is of the correct school.
 | ||||
|                             // As result multiple SchoolCaps could have the same mWeakestSpell. Erasing the weakest spell would then fail if another school
 | ||||
|                             // already erased it, and so the number of spells would often exceed the sum of limits.
 | ||||
|                             // This bug cannot be fixed without significantly changing the results of the spell autocalc, which will not have been playtested.
 | ||||
|                             //testSchool == school &&
 | ||||
|                             testSpell->mData.mCost < cap.mMinCost) | ||||
|                     { | ||||
|                         cap.mMinCost = testSpell->mData.mCost; | ||||
|                         cap.mWeakestSpell = testSpell->mId; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 cap.mCount += 1; | ||||
|                 if (cap.mCount == cap.mLimit) | ||||
|                     cap.mReachedLimit = true; | ||||
| 
 | ||||
|                 if (spell->mData.mCost < cap.mMinCost) | ||||
|                 { | ||||
|                     cap.mWeakestSpell = spell->mId; | ||||
|                     cap.mMinCost = spell->mData.mCost; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return selectedSpells; | ||||
|     } | ||||
| 
 | ||||
|     bool attrSkillCheck (const ESM::Spell* spell, const int* actorSkills, const int* actorAttributes) | ||||
|     { | ||||
|         const std::vector<ESM::ENAMstruct>& effects = spell->mEffects.mList; | ||||
|         for (std::vector<ESM::ENAMstruct>::const_iterator effectIt = effects.begin(); effectIt != effects.end(); ++effectIt) | ||||
|         { | ||||
|             const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find(effectIt->mEffectID); | ||||
|             static const int iAutoSpellAttSkillMin = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("iAutoSpellAttSkillMin")->getInt(); | ||||
| 
 | ||||
|             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<int, ESM::Skill::SkillEnum> 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) | ||||
|     { | ||||
|         float minChance = FLT_MAX; | ||||
| 
 | ||||
|         const ESM::EffectList& effects = spell->mEffects; | ||||
|         for (std::vector<ESM::ENAMstruct>::const_iterator it = effects.mList.begin(); it != effects.mList.end(); ++it) | ||||
|         { | ||||
|             const ESM::ENAMstruct& effect = *it; | ||||
|             float x = effect.mDuration; | ||||
| 
 | ||||
|             const ESM::MagicEffect* magicEffect = MWBase::Environment::get().getWorld()->getStore().get<ESM::MagicEffect>().find(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 = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find("fEffectCostMult")->getFloat(); | ||||
|             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) | ||||
|     { | ||||
|         if (spell->mData.mType != ESM::Spell::ST_Spell) | ||||
|             return 100.f; | ||||
| 
 | ||||
|         if (spell->mData.mFlags & ESM::Spell::F_Always) | ||||
|             return 100.f; | ||||
| 
 | ||||
|         float skillTerm; | ||||
|         if (effectiveSchool != -1) | ||||
|             skillTerm = 2.f * actorSkills[mapSchoolToSkill(effectiveSchool)]; | ||||
|         else | ||||
|             calcWeakestSchool(spell, actorSkills, effectiveSchool, skillTerm); // 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; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										31
									
								
								apps/openmw/mwmechanics/autocalcspell.hpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								apps/openmw/mwmechanics/autocalcspell.hpp
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | |||
| #ifndef OPENMW_AUTOCALCSPELL_H | ||||
| #define OPENMW_AUTOCALCSPELL_H | ||||
| 
 | ||||
| #include <cfloat> | ||||
| #include <set> | ||||
| 
 | ||||
| #include <components/esm/loadspel.hpp> | ||||
| #include <components/esm/loadskil.hpp> | ||||
| #include <components/esm/loadrace.hpp> | ||||
| 
 | ||||
| namespace MWMechanics | ||||
| { | ||||
| 
 | ||||
| /// Contains algorithm for calculating an NPC's spells based on stats
 | ||||
| /// @note We might want to move this code to a component later, so the editor can use it for preview purposes
 | ||||
| 
 | ||||
| std::vector<std::string> autoCalcNpcSpells(const int* actorSkills, const int* actorAttributes, const ESM::Race* race); | ||||
| 
 | ||||
| // Helpers
 | ||||
| 
 | ||||
| bool attrSkillCheck (const ESM::Spell* spell, const int* actorSkills, const int* actorAttributes); | ||||
| 
 | ||||
| ESM::Skill::SkillEnum mapSchoolToSkill(int school); | ||||
| 
 | ||||
| void calcWeakestSchool(const ESM::Spell* spell, const int* actorSkills, int& effectiveSchool, float& skillTerm); | ||||
| 
 | ||||
| float calcAutoCastChance(const ESM::Spell* spell, const int* actorSkills, const int* actorAttributes, int effectiveSchool); | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| #endif | ||||
|  | @ -361,9 +361,10 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat | |||
|          * beginning. */ | ||||
|         int mode = ((movement == mCurrentMovement) ? 2 : 1); | ||||
| 
 | ||||
|         mMovementAnimationControlled = true; | ||||
| 
 | ||||
|         mAnimation->disable(mCurrentMovement); | ||||
|         mCurrentMovement = movement; | ||||
|         mMovementAnimVelocity = 0.0f; | ||||
|         if(!mCurrentMovement.empty()) | ||||
|         { | ||||
|             float vel, speedmult = 1.0f; | ||||
|  | @ -383,16 +384,18 @@ void CharacterController::refreshCurrentAnims(CharacterState idle, CharacterStat | |||
| 
 | ||||
|             if(mMovementSpeed > 0.0f && (vel=mAnimation->getVelocity(anim)) > 1.0f) | ||||
|             { | ||||
|                 mMovementAnimVelocity = vel; | ||||
|                 speedmult = mMovementSpeed / vel; | ||||
|             } | ||||
|             else if (mMovementState == CharState_TurnLeft || mMovementState == CharState_TurnRight) | ||||
|                 speedmult = 1.f; // TODO: should get a speed mult depending on the current turning speed
 | ||||
|             else if (mMovementSpeed > 0.0f) | ||||
|             { | ||||
|                 // The first person anims don't have any velocity to calculate a speed multiplier from.
 | ||||
|                 // We use the third person velocities instead.
 | ||||
|                 // FIXME: should be pulled from the actual animation, but it is not presently loaded.
 | ||||
|                 speedmult = mMovementSpeed / (isrunning ? 222.857f : 154.064f); | ||||
|                 mMovementAnimationControlled = false; | ||||
|             } | ||||
|             mAnimation->play(mCurrentMovement, Priority_Movement, movegroup, false, | ||||
|                              speedmult, ((mode!=2)?"start":"loop start"), "stop", 0.0f, ~0ul); | ||||
|         } | ||||
|  | @ -506,6 +509,7 @@ void CharacterController::playDeath(float startpoint, CharacterState death) | |||
|     mJumpState = JumpState_None; | ||||
|     mAnimation->disable(mCurrentJump); | ||||
|     mCurrentJump = ""; | ||||
|     mMovementAnimationControlled = true; | ||||
| 
 | ||||
|     mAnimation->play(mCurrentDeath, Priority_Death, MWRender::Animation::Group_All, | ||||
|                     false, 1.0f, "start", "stop", startpoint, 0); | ||||
|  | @ -547,7 +551,7 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim | |||
|     , mIdleState(CharState_None) | ||||
|     , mMovementState(CharState_None) | ||||
|     , mMovementSpeed(0.0f) | ||||
|     , mMovementAnimVelocity(0.0f) | ||||
|     , mMovementAnimationControlled(true) | ||||
|     , mDeathState(CharState_None) | ||||
|     , mHitState(CharState_None) | ||||
|     , mUpperBodyState(UpperCharState_Nothing) | ||||
|  | @ -570,10 +574,14 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim | |||
|         if (cls.hasInventoryStore(mPtr)) | ||||
|         { | ||||
|             getActiveWeapon(cls.getCreatureStats(mPtr), cls.getInventoryStore(mPtr), &mWeaponType); | ||||
|             if (mWeaponType != WeapType_None) | ||||
|             { | ||||
|                 mUpperBodyState = UpperCharState_WeapEquiped; | ||||
|                 getWeaponGroup(mWeaponType, mCurrentWeapon); | ||||
|             } | ||||
| 
 | ||||
|             if(mWeaponType != WeapType_None && mWeaponType != WeapType_Spell && mWeaponType != WeapType_HandToHand) | ||||
|             { | ||||
|                 getWeaponGroup(mWeaponType, mCurrentWeapon); | ||||
|                 mUpperBodyState = UpperCharState_WeapEquiped; | ||||
|                 mAnimation->showWeapons(true); | ||||
|                 mAnimation->setWeaponGroup(mCurrentWeapon); | ||||
|             } | ||||
|  | @ -1241,6 +1249,7 @@ void CharacterController::update(float duration) | |||
|         if (inwater || flying) | ||||
|             cls.getCreatureStats(mPtr).land(); | ||||
| 
 | ||||
|         bool inJump = true; | ||||
|         if(!onground && !flying && !inwater) | ||||
|         { | ||||
|             // In the air (either getting up —ascending part of jump— or falling).
 | ||||
|  | @ -1330,6 +1339,8 @@ void CharacterController::update(float duration) | |||
|                 mJumpState = JumpState_None; | ||||
|             vec.z = 0.0f; | ||||
| 
 | ||||
|             inJump = false; | ||||
| 
 | ||||
|             if(std::abs(vec.x/2.0f) > std::abs(vec.y)) | ||||
|             { | ||||
|                 if(vec.x > 0.0f) | ||||
|  | @ -1391,6 +1402,8 @@ void CharacterController::update(float duration) | |||
|             forcestateupdate = updateCreatureState() || forcestateupdate; | ||||
| 
 | ||||
|         refreshCurrentAnims(idlestate, movestate, forcestateupdate); | ||||
|         if (inJump) | ||||
|             mMovementAnimationControlled = false; | ||||
| 
 | ||||
|         if (!mSkipAnim) | ||||
|         { | ||||
|  | @ -1402,7 +1415,7 @@ void CharacterController::update(float duration) | |||
|             else //avoid z-rotating for knockdown
 | ||||
|                 world->rotateObject(mPtr, rot.x, rot.y, 0.0f, true); | ||||
| 
 | ||||
|             if (mMovementAnimVelocity == 0) | ||||
|             if (!mMovementAnimationControlled) | ||||
|                 world->queueMovement(mPtr, vec); | ||||
|         } | ||||
|         else | ||||
|  | @ -1446,7 +1459,7 @@ void CharacterController::update(float duration) | |||
|         } | ||||
| 
 | ||||
|         // Update movement
 | ||||
|         if(mMovementAnimVelocity > 0) | ||||
|         if(mMovementAnimationControlled && mPtr.getClass().isActor()) | ||||
|             world->queueMovement(mPtr, moved); | ||||
|     } | ||||
|     else if (mAnimation) | ||||
|  |  | |||
|  | @ -147,7 +147,7 @@ class CharacterController | |||
|     CharacterState mMovementState; | ||||
|     std::string mCurrentMovement; | ||||
|     float mMovementSpeed; | ||||
|     float mMovementAnimVelocity; | ||||
|     bool mMovementAnimationControlled; | ||||
| 
 | ||||
|     CharacterState mDeathState; | ||||
|     std::string mCurrentDeath; | ||||
|  |  | |||
|  | @ -19,6 +19,7 @@ | |||
| #include <OgreSceneNode.h> | ||||
| 
 | ||||
| #include "spellcasting.hpp" | ||||
| #include "autocalcspell.hpp" | ||||
| 
 | ||||
| namespace | ||||
| { | ||||
|  | @ -155,19 +156,6 @@ namespace MWMechanics | |||
|                         npcStats.getSkill (index).setBase ( | ||||
|                             npcStats.getSkill (index).getBase() + bonus); | ||||
|                     } | ||||
| 
 | ||||
|                     if (i==1) | ||||
|                     { | ||||
|                         // Major skill - add starting spells for this skill if existing
 | ||||
|                         const MWWorld::ESMStore& store = MWBase::Environment::get().getWorld()->getStore(); | ||||
|                         MWWorld::Store<ESM::Spell>::iterator it = store.get<ESM::Spell>().begin(); | ||||
|                         for (; it != store.get<ESM::Spell>().end(); ++it) | ||||
|                         { | ||||
|                             if (it->mData.mFlags & ESM::Spell::F_PCStart | ||||
|                                     && spellSchoolToSkill(getSpellSchool(&*it, ptr)) == index) | ||||
|                                 creatureStats.getSpells().add(it->mId); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|  | @ -190,6 +178,87 @@ namespace MWMechanics | |||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // F_PCStart spells
 | ||||
|         static const float fPCbaseMagickaMult = esmStore.get<ESM::GameSetting>().find("fPCbaseMagickaMult")->getFloat(); | ||||
| 
 | ||||
|         float baseMagicka = fPCbaseMagickaMult * creatureStats.getAttribute(ESM::Attribute::Intelligence).getBase(); | ||||
|         bool reachedLimit = false; | ||||
|         const ESM::Spell* weakestSpell = NULL; | ||||
|         int minCost = INT_MAX; | ||||
| 
 | ||||
|         std::vector<std::string> selectedSpells; | ||||
| 
 | ||||
|         const ESM::Race* race = NULL; | ||||
|         if (mRaceSelected) | ||||
|             race = esmStore.get<ESM::Race>().find(player->mRace); | ||||
| 
 | ||||
|         int skills[ESM::Skill::Length]; | ||||
|         for (int i=0; i<ESM::Skill::Length; ++i) | ||||
|             skills[i] = npcStats.getSkill(i).getBase(); | ||||
| 
 | ||||
|         int attributes[ESM::Attribute::Length]; | ||||
|         for (int i=0; i<ESM::Attribute::Length; ++i) | ||||
|             attributes[i] = npcStats.getAttribute(i).getBase(); | ||||
| 
 | ||||
|         const MWWorld::Store<ESM::Spell> &spells = | ||||
|             esmStore.get<ESM::Spell>(); | ||||
|         for (MWWorld::Store<ESM::Spell>::iterator iter = spells.begin(); iter != spells.end(); ++iter) | ||||
|         { | ||||
|             const ESM::Spell* spell = &*iter; | ||||
| 
 | ||||
|             if (spell->mData.mType != ESM::Spell::ST_Spell) | ||||
|                 continue; | ||||
|             if (!(spell->mData.mFlags & ESM::Spell::F_PCStart)) | ||||
|                 continue; | ||||
|             if (reachedLimit && spell->mData.mCost <= minCost) | ||||
|                 continue; | ||||
|             if (race && std::find(race->mPowers.mList.begin(), race->mPowers.mList.end(), spell->mId) != race->mPowers.mList.end()) | ||||
|                 continue; | ||||
|             if (baseMagicka < spell->mData.mCost) | ||||
|                 continue; | ||||
| 
 | ||||
|             static const float fAutoPCSpellChance = esmStore.get<ESM::GameSetting>().find("fAutoPCSpellChance")->getFloat(); | ||||
|             if (calcAutoCastChance(spell, skills, attributes, -1) < fAutoPCSpellChance) | ||||
|                 continue; | ||||
| 
 | ||||
|             if (!attrSkillCheck(spell, skills, attributes)) | ||||
|                 continue; | ||||
| 
 | ||||
|             selectedSpells.push_back(spell->mId); | ||||
| 
 | ||||
|             if (reachedLimit) | ||||
|             { | ||||
|                 std::vector<std::string>::iterator it = std::find(selectedSpells.begin(), selectedSpells.end(), weakestSpell->mId); | ||||
|                 if (it != selectedSpells.end()) | ||||
|                     selectedSpells.erase(it); | ||||
| 
 | ||||
|                 minCost = INT_MAX; | ||||
|                 for (std::vector<std::string>::iterator weakIt = selectedSpells.begin(); weakIt != selectedSpells.end(); ++weakIt) | ||||
|                 { | ||||
|                     const ESM::Spell* testSpell = esmStore.get<ESM::Spell>().find(*weakIt); | ||||
|                     if (testSpell->mData.mCost < minCost) | ||||
|                     { | ||||
|                         minCost = testSpell->mData.mCost; | ||||
|                         weakestSpell = testSpell; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 if (spell->mData.mCost < minCost) | ||||
|                 { | ||||
|                     weakestSpell = spell; | ||||
|                     minCost = weakestSpell->mData.mCost; | ||||
|                 } | ||||
|                 static const unsigned int iAutoPCSpellMax = esmStore.get<ESM::GameSetting>().find("iAutoPCSpellMax")->getInt(); | ||||
|                 if (selectedSpells.size() == iAutoPCSpellMax) | ||||
|                     reachedLimit = true; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         for (std::vector<std::string>::iterator it = selectedSpells.begin(); it != selectedSpells.end(); ++it) | ||||
|             creatureStats.getSpells().add(*it); | ||||
| 
 | ||||
|         // forced update and current value adjustments
 | ||||
|         mActors.updateActor (ptr, 0); | ||||
| 
 | ||||
|  |  | |||
|  | @ -92,7 +92,7 @@ namespace MWMechanics | |||
|             x *= 0.1 * magicEffect->mData.mBaseCost; | ||||
|             x *= 0.5 * (it->mMagnMin + it->mMagnMax); | ||||
|             x *= it->mArea * 0.05 * magicEffect->mData.mBaseCost; | ||||
|             if (magicEffect->mData.mFlags & ESM::MagicEffect::CastTarget) | ||||
|             if (it->mRange == ESM::RT_Target) | ||||
|                 x *= 1.5; | ||||
|             static const float fEffectCostMult = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find( | ||||
|                         "fEffectCostMult")->getFloat(); | ||||
|  |  | |||
|  | @ -149,6 +149,8 @@ namespace MWRender | |||
|             mViewModeToggleQueued = true; | ||||
|             return; | ||||
|         } | ||||
|         else | ||||
|             mViewModeToggleQueued = false; | ||||
| 
 | ||||
|         mFirstPersonView = !mFirstPersonView; | ||||
|         processViewChange(); | ||||
|  |  | |||
|  | @ -1088,7 +1088,7 @@ public: | |||
| 
 | ||||
|     void close() { } | ||||
| 
 | ||||
|     bool update(Ogre::MaterialPtr &mat, Ogre::Rectangle2D *rect, int screen_width, int screen_height) | ||||
|     bool update() | ||||
|     { return false; } | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -86,16 +86,24 @@ namespace MWScript | |||
|                     float ay = Ogre::Radian(ptr.getRefData().getPosition().rot[1]).valueDegrees(); | ||||
|                     float az = Ogre::Radian(ptr.getRefData().getPosition().rot[2]).valueDegrees(); | ||||
| 
 | ||||
|                     MWWorld::LocalRotation localRot = ptr.getRefData().getLocalRotation(); | ||||
| 
 | ||||
|                     if (axis == "x") | ||||
|                     { | ||||
|                         localRot.rot[0] = 0; | ||||
|                         ptr.getRefData().setLocalRotation(localRot); | ||||
|                         MWBase::Environment::get().getWorld()->rotateObject(ptr,angle,ay,az); | ||||
|                     } | ||||
|                     else if (axis == "y") | ||||
|                     { | ||||
|                         localRot.rot[1] = 0; | ||||
|                         ptr.getRefData().setLocalRotation(localRot); | ||||
|                         MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,angle,az); | ||||
|                     } | ||||
|                     else if (axis == "z") | ||||
|                     { | ||||
|                         localRot.rot[2] = 0; | ||||
|                         ptr.getRefData().setLocalRotation(localRot); | ||||
|                         MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,angle); | ||||
|                     } | ||||
|                     else | ||||
|  |  | |||
|  | @ -209,8 +209,6 @@ namespace MWWorld | |||
|         } | ||||
| 
 | ||||
|         void setUp() { | ||||
|             //std::sort(mStatic.begin(), mStatic.end(), RecordCmp());
 | ||||
| 
 | ||||
|             mShared.clear(); | ||||
|             mShared.reserve(mStatic.size()); | ||||
|             typename std::map<std::string, T>::iterator it = mStatic.begin(); | ||||
|  | @ -675,18 +673,15 @@ namespace MWWorld | |||
|         } | ||||
| 
 | ||||
|         void setUp() { | ||||
|             //typedef std::vector<ESM::Cell>::iterator Iterator;
 | ||||
|             typedef DynamicExt::iterator ExtIterator; | ||||
|             typedef std::map<std::string, ESM::Cell>::iterator IntIterator; | ||||
| 
 | ||||
|             //std::sort(mInt.begin(), mInt.end(), RecordCmp());
 | ||||
|             mSharedInt.clear(); | ||||
|             mSharedInt.reserve(mInt.size()); | ||||
|             for (IntIterator it = mInt.begin(); it != mInt.end(); ++it) { | ||||
|                 mSharedInt.push_back(&(it->second)); | ||||
|             } | ||||
| 
 | ||||
|             //std::sort(mExt.begin(), mExt.end(), ExtCmp());
 | ||||
|             mSharedExt.clear(); | ||||
|             mSharedExt.reserve(mExt.size()); | ||||
|             for (ExtIterator it = mExt.begin(); it != mExt.end(); ++it) { | ||||
|  | @ -1147,6 +1142,37 @@ namespace MWWorld | |||
|         } | ||||
|     }; | ||||
| 
 | ||||
| 
 | ||||
|     // Specialisation for ESM::Spell to preserve record order as it was in the content files.
 | ||||
|     // The NPC spell autocalc code heavily depends on this order.
 | ||||
|     // We could also do this in the base class, but it's usually not a good idea to depend on record order.
 | ||||
|     template<> | ||||
|     inline void Store<ESM::Spell>::clearDynamic() | ||||
|     { | ||||
|         // remove the dynamic part of mShared
 | ||||
|         mShared.erase(mShared.begin() + mStatic.size(), mShared.end()); | ||||
|         mDynamic.clear(); | ||||
|     } | ||||
| 
 | ||||
|     template<> | ||||
|     inline void Store<ESM::Spell>::load(ESM::ESMReader &esm, const std::string &id) { | ||||
|         std::string idLower = Misc::StringUtils::lowerCase(id); | ||||
| 
 | ||||
|         std::pair<Static::iterator, bool> inserted = mStatic.insert(std::make_pair(idLower, ESM::Spell())); | ||||
|         if (inserted.second) | ||||
|             mShared.push_back(&mStatic[idLower]); | ||||
| 
 | ||||
|         inserted.first->second.mId = idLower; | ||||
|         inserted.first->second.load(esm); | ||||
|     } | ||||
| 
 | ||||
|     template<> | ||||
|     inline void Store<ESM::Spell>::setUp() | ||||
|     { | ||||
|         // remove the dynamic part of mShared
 | ||||
|         mShared.erase(mShared.begin() + mStatic.size(), mShared.end()); | ||||
|     } | ||||
| 
 | ||||
| } //end namespace
 | ||||
| 
 | ||||
| #endif | ||||
|  |  | |||
|  | @ -1123,7 +1123,10 @@ namespace MWWorld | |||
| 
 | ||||
|         ptr.getRefData().setPosition(pos); | ||||
| 
 | ||||
|         mWorldScene->updateObjectRotation(ptr); | ||||
|         if (ptr.getClass().isActor()) | ||||
|             mWorldScene->updateObjectRotation(ptr); | ||||
|         else | ||||
|             mWorldScene->updateObjectLocalRotation(ptr); | ||||
|     } | ||||
| 
 | ||||
|     void World::localRotateObject (const Ptr& ptr, float x, float y, float z) | ||||
|  | @ -2649,6 +2652,8 @@ namespace MWWorld | |||
|         { | ||||
|             mGoToJail = false; | ||||
| 
 | ||||
|             MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Dialogue); | ||||
| 
 | ||||
|             MWWorld::Ptr player = getPlayerPtr(); | ||||
|             teleportToClosestMarker(player, "prisonmarker"); | ||||
|             int bounty = player.getClass().getNpcStats(player).getBounty(); | ||||
|  |  | |||
|  | @ -12,68 +12,3 @@ struct StringOpsTest : public ::testing::Test | |||
|     { | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| TEST_F(StringOpsTest, begins_matching) | ||||
| { | ||||
|   ASSERT_TRUE(Misc::begins("abc", "a")); | ||||
|   ASSERT_TRUE(Misc::begins("abc", "ab")); | ||||
|   ASSERT_TRUE(Misc::begins("abc", "abc")); | ||||
|   ASSERT_TRUE(Misc::begins("abcd", "abc")); | ||||
| } | ||||
| 
 | ||||
| TEST_F(StringOpsTest, begins_not_matching) | ||||
| { | ||||
|   ASSERT_FALSE(Misc::begins("abc", "b")); | ||||
|   ASSERT_FALSE(Misc::begins("abc", "bc")); | ||||
|   ASSERT_FALSE(Misc::begins("abc", "bcd")); | ||||
|   ASSERT_FALSE(Misc::begins("abc", "abcd")); | ||||
| } | ||||
| 
 | ||||
| TEST_F(StringOpsTest, ibegins_matching) | ||||
| { | ||||
|   ASSERT_TRUE(Misc::ibegins("Abc", "a")); | ||||
|   ASSERT_TRUE(Misc::ibegins("aBc", "ab")); | ||||
|   ASSERT_TRUE(Misc::ibegins("abC", "abc")); | ||||
|   ASSERT_TRUE(Misc::ibegins("abcD", "abc")); | ||||
| } | ||||
| 
 | ||||
| TEST_F(StringOpsTest, ibegins_not_matching) | ||||
| { | ||||
|   ASSERT_FALSE(Misc::ibegins("abc", "b")); | ||||
|   ASSERT_FALSE(Misc::ibegins("abc", "bc")); | ||||
|   ASSERT_FALSE(Misc::ibegins("abc", "bcd")); | ||||
|   ASSERT_FALSE(Misc::ibegins("abc", "abcd")); | ||||
| } | ||||
| 
 | ||||
| TEST_F(StringOpsTest, ends_matching) | ||||
| { | ||||
|   ASSERT_TRUE(Misc::ends("abc", "c")); | ||||
|   ASSERT_TRUE(Misc::ends("abc", "bc")); | ||||
|   ASSERT_TRUE(Misc::ends("abc", "abc")); | ||||
|   ASSERT_TRUE(Misc::ends("abcd", "abcd")); | ||||
| } | ||||
| 
 | ||||
| TEST_F(StringOpsTest, ends_not_matching) | ||||
| { | ||||
|   ASSERT_FALSE(Misc::ends("abc", "b")); | ||||
|   ASSERT_FALSE(Misc::ends("abc", "ab")); | ||||
|   ASSERT_FALSE(Misc::ends("abc", "bcd")); | ||||
|   ASSERT_FALSE(Misc::ends("abc", "abcd")); | ||||
| } | ||||
| 
 | ||||
| TEST_F(StringOpsTest, iends_matching) | ||||
| { | ||||
|   ASSERT_TRUE(Misc::iends("Abc", "c")); | ||||
|   ASSERT_TRUE(Misc::iends("aBc", "bc")); | ||||
|   ASSERT_TRUE(Misc::iends("abC", "abc")); | ||||
|   ASSERT_TRUE(Misc::iends("abcD", "abcd")); | ||||
| } | ||||
| 
 | ||||
| TEST_F(StringOpsTest, iends_not_matching) | ||||
| { | ||||
|   ASSERT_FALSE(Misc::iends("abc", "b")); | ||||
|   ASSERT_FALSE(Misc::iends("abc", "ab")); | ||||
|   ASSERT_FALSE(Misc::iends("abc", "bcd")); | ||||
|   ASSERT_FALSE(Misc::iends("abc", "abcd")); | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,8 +10,6 @@ namespace ESM | |||
| 
 | ||||
| void NPC::load(ESMReader &esm) | ||||
| { | ||||
|     //mNpdt52.mGold = -10;
 | ||||
| 
 | ||||
|     mPersistent = esm.getRecordFlags() & 0x0400; | ||||
| 
 | ||||
|     mModel = esm.getHNOString("MODL"); | ||||
|  | @ -63,7 +61,6 @@ void NPC::load(ESMReader &esm) | |||
|         } | ||||
|     } | ||||
|     mAiPackage.load(esm); | ||||
|     esm.skipRecord(); | ||||
| } | ||||
| void NPC::save(ESMWriter &esm) const | ||||
| { | ||||
|  |  | |||
|  | @ -27,8 +27,8 @@ struct Spell | |||
| 
 | ||||
|     enum Flags | ||||
|     { | ||||
|         F_Autocalc = 1, | ||||
|         F_PCStart = 2, | ||||
|         F_Autocalc = 1, // Can be selected by NPC spells auto-calc
 | ||||
|         F_PCStart = 2, // Can be selected by player spells auto-calc
 | ||||
|         F_Always = 4 // Casting always succeeds
 | ||||
|     }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -12,59 +12,6 @@ | |||
| namespace Misc | ||||
| { | ||||
| 
 | ||||
| bool begins(const char* str1, const char* str2) | ||||
| { | ||||
|   while(*str2) | ||||
|     { | ||||
|       if(*str1 == 0 || *str1 != *str2) return false; | ||||
| 
 | ||||
|       str1++; | ||||
|       str2++; | ||||
|     } | ||||
|   return true; | ||||
| } | ||||
| 
 | ||||
| bool ends(const char* str1, const char* str2) | ||||
| { | ||||
|   int len1 = strlen(str1); | ||||
|   int len2 = strlen(str2); | ||||
| 
 | ||||
|   if(len1 < len2) return false; | ||||
| 
 | ||||
|   return strcmp(str2, str1+len1-len2) == 0; | ||||
| } | ||||
| 
 | ||||
| // True if the given chars match, case insensitive
 | ||||
| static bool icmp(char a, char b) | ||||
| { | ||||
|   if(a >= 'A' && a <= 'Z') | ||||
|     a += 'a' - 'A'; | ||||
|   if(b >= 'A' && b <= 'Z') | ||||
|     b += 'a' - 'A'; | ||||
| 
 | ||||
|   return a == b; | ||||
| } | ||||
| 
 | ||||
| bool ibegins(const char* str1, const char* str2) | ||||
| { | ||||
|   while(*str2) | ||||
|     { | ||||
|       if(*str1 == 0 || !icmp(*str1,*str2)) return false; | ||||
| 
 | ||||
|       str1++; | ||||
|       str2++; | ||||
|     } | ||||
|   return true; | ||||
| } | ||||
| 
 | ||||
| bool iends(const char* str1, const char* str2) | ||||
| { | ||||
|   int len1 = strlen(str1); | ||||
|   int len2 = strlen(str2); | ||||
| 
 | ||||
|   if(len1 < len2) return false; | ||||
| 
 | ||||
|   return strcasecmp(str2, str1+len1-len2) == 0; | ||||
| } | ||||
| std::locale StringUtils::mLocale = std::locale::classic(); | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -4,15 +4,18 @@ | |||
| #include <cctype> | ||||
| #include <string> | ||||
| #include <algorithm> | ||||
| #include <locale> | ||||
| 
 | ||||
| namespace Misc | ||||
| { | ||||
| class StringUtils | ||||
| { | ||||
| 
 | ||||
|     static std::locale mLocale; | ||||
|     struct ci | ||||
|     { | ||||
|         bool operator()(int x, int y) const { | ||||
|             return std::tolower(x) < std::tolower(y); | ||||
|         bool operator()(char x, char y) const { | ||||
|             return std::tolower(x, StringUtils::mLocale) < std::tolower(y, StringUtils::mLocale); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|  | @ -28,7 +31,7 @@ public: | |||
|         std::string::const_iterator xit = x.begin(); | ||||
|         std::string::const_iterator yit = y.begin(); | ||||
|         for (; xit != x.end(); ++xit, ++yit) { | ||||
|             if (std::tolower(*xit) != std::tolower(*yit)) { | ||||
|             if (std::tolower(*xit, mLocale) != std::tolower(*yit, mLocale)) { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|  | @ -42,7 +45,7 @@ public: | |||
|         for(;xit != x.end() && yit != y.end() && len > 0;++xit,++yit,--len) | ||||
|         { | ||||
|             int res = *xit - *yit; | ||||
|             if(res != 0 && std::tolower(*xit) != std::tolower(*yit)) | ||||
|             if(res != 0 && std::tolower(*xit, mLocale) != std::tolower(*yit, mLocale)) | ||||
|                 return (res > 0) ? 1 : -1; | ||||
|         } | ||||
|         if(len > 0) | ||||
|  | @ -57,12 +60,8 @@ public: | |||
| 
 | ||||
|     /// Transforms input string to lower case w/o copy
 | ||||
|     static std::string &toLower(std::string &inout) { | ||||
|         std::transform( | ||||
|             inout.begin(), | ||||
|             inout.end(), | ||||
|             inout.begin(), | ||||
|             (int (*)(int)) std::tolower | ||||
|         ); | ||||
|         for (unsigned int i=0; i<inout.size(); ++i) | ||||
|             inout[i] = std::tolower(inout[i], mLocale); | ||||
|         return inout; | ||||
|     } | ||||
| 
 | ||||
|  | @ -74,19 +73,6 @@ public: | |||
|     } | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| /// Returns true if str1 begins with substring str2
 | ||||
| bool begins(const char* str1, const char* str2); | ||||
| 
 | ||||
| /// Returns true if str1 ends with substring str2
 | ||||
| bool ends(const char* str1, const char* str2); | ||||
| 
 | ||||
| /// Case insensitive, returns true if str1 begins with substring str2
 | ||||
| bool ibegins(const char* str1, const char* str2); | ||||
| 
 | ||||
| /// Case insensitive, returns true if str1 ends with substring str2
 | ||||
| bool iends(const char* str1, const char* str2); | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| #endif | ||||
|  |  | |||
							
								
								
									
										184
									
								
								readme.txt
									
									
									
									
									
								
							
							
						
						
									
										184
									
								
								readme.txt
									
									
									
									
									
								
							|  | @ -3,7 +3,7 @@ OpenMW: A reimplementation of The Elder Scrolls III: Morrowind | |||
| OpenMW is an attempt at recreating the engine for the popular role-playing game | ||||
| Morrowind by Bethesda Softworks. You need to own and install the original game for OpenMW to work. | ||||
| 
 | ||||
| Version: 0.30.0 | ||||
| Version: 0.31.0 | ||||
| License: GPL (see GPL3.txt for more information) | ||||
| Website: http://www.openmw.org | ||||
| 
 | ||||
|  | @ -96,6 +96,188 @@ Allowed options: | |||
| 
 | ||||
| CHANGELOG | ||||
| 
 | ||||
| 0.31.0 | ||||
| 
 | ||||
| Bug #245: Cloud direction and weather systems differ from Morrowind | ||||
| Bug #275: Local Map does not always show objects that span multiple cells | ||||
| Bug #538: Update CenterOnCell (COC) function behavior | ||||
| Bug #618: Local and World Map Textures are sometimes Black | ||||
| Bug #640: Water behaviour at night | ||||
| Bug #668: OpenMW doesn't support non-latin paths on Windows | ||||
| Bug #746: OpenMW doesn't check if the background music was already played | ||||
| Bug #747: Door is stuck if cell is left before animation finishes | ||||
| Bug #772: Disabled statics are visible on map | ||||
| Bug #829: OpenMW uses up all available vram, when playing for extended time | ||||
| Bug #869: Dead bodies don't collide with anything | ||||
| Bug #894: Various character creation issues | ||||
| Bug #897/#1369: opencs Segmentation Fault after "new" or "load" | ||||
| Bug #899: Various jumping issues | ||||
| Bug #952: Reflection effects are one frame delayed | ||||
| Bug #993: Able to interact with world during Wait/Rest dialog | ||||
| Bug #995: Dropped items can be placed inside the wall | ||||
| Bug #1008: Corpses always face up upon reentering the cell | ||||
| Bug #1035: Random colour patterns appearing in automap | ||||
| Bug #1037: Footstep volume issues | ||||
| Bug #1047: Creation of wrong links in dialogue window | ||||
| Bug #1129: Summoned creature time life duration seems infinite | ||||
| Bug #1134: Crimes can be committed against hostile NPCs | ||||
| Bug #1136: Creature run speed formula is incorrect | ||||
| Bug #1150: Weakness to Fire doesn't apply to Fire Damage in the same spell | ||||
| Bug #1155: NPCs killing each other | ||||
| Bug #1166: Bittercup script still does not work | ||||
| Bug #1178: .bsa file names are case sensitive. | ||||
| Bug #1179: Crash after trying to load game after being killed | ||||
| Bug #1180: Changing footstep sound location | ||||
| Bug #1196: Jumping not disabled when showing messageboxes | ||||
| Bug #1202: "strange" keys are not shown in binding menu, and are not saved either, but works | ||||
| Bug #1217: Container content changes based on the current position of the mouse | ||||
| Bug #1234: Loading/saving issues with dynamic records | ||||
| Bug #1277: Text pasted into the console appears twice | ||||
| Bug #1284: Crash on New Game | ||||
| Bug #1303: It's possible to skip the chargen | ||||
| Bug #1304: Slaughterfish should not detect the player unless the player is in the water | ||||
| Bug #1311: Editor: deleting Record Filter line does not reset the filter | ||||
| Bug #1324: ERROR: ESM Error: String table overflow when loading Animated Morrowind.esp | ||||
| Bug #1328: Editor: Bogus Filter created when dragging multiple records to filter bar of non-applicable table | ||||
| Bug #1331: Walking/running sound persist after killing NPC`s that are walking/running. | ||||
| Bug #1334: Previously equipped items not shown as unequipped after attempting to sell them. | ||||
| Bug #1335: Actors ignore vertical axis when deciding to attack | ||||
| Bug #1338: Unknown toggle option for shadows | ||||
| Bug #1339: "Ashlands Region" is visible when beginning new game during "Loading Area" process | ||||
| Bug #1340: Guards prompt Player with punishment options after resisting arrest with another guard. | ||||
| Bug #1348: Regression: Bug #1098 has returned with a vengeance | ||||
| Bug #1349: [TR] TR_Data mesh tr_ex_imp_gatejamb01 cannot be activated | ||||
| Bug #1352: Disabling an ESX file does not disable dependent ESX files | ||||
| Bug #1355: CppCat Checks OpenMW | ||||
| Bug #1356: Incorrect voice type filtering for sleep interrupts | ||||
| Bug #1357: Restarting the game clears saves | ||||
| Bug #1360: Seyda Neen silk rider dialog problem | ||||
| Bug #1361: Some lights don't work | ||||
| Bug #1364: It is difficult to bind "Mouse 1" to an action in the options menu | ||||
| Bug #1370: Animation compilation mod does not work properly | ||||
| Bug #1371: SL_Pick01.nif from third party fails to load in openmw, but works in Vanilla | ||||
| Bug #1373: When stealing in front of Sellus Gravius cannot exit the dialog | ||||
| Bug #1378: Installs to /usr/local are not working | ||||
| Bug #1380: Loading a save file fail if one of the content files is disabled | ||||
| Bug #1382: "getHExact() size mismatch" crash on loading official plugin "Siege at Firemoth.esp" | ||||
| Bug #1386: Arkngthand door will not open | ||||
| Bug #1388: Segfault when modifying View Distance in Menu options | ||||
| Bug #1389: Crash when loading a save after dying | ||||
| Bug #1390: Apostrophe characters not displayed [French version] | ||||
| Bug #1391: Custom made icon background texture for magical weapons and stuff isn't scaled properly on GUI. | ||||
| Bug #1393: Coin icon during the level up dialogue are off of the background | ||||
| Bug #1394: Alt+F4 doesn't work on Win version | ||||
| Bug #1395: Changing rings switches only the last one put on | ||||
| Bug #1396: Pauldron parts aren't showing when the robe is equipped | ||||
| Bug #1402: Dialogue of some shrines have wrong button orientation | ||||
| Bug #1403: Items are floating in the air when they're dropped onto dead bodies. | ||||
| Bug #1404: Forearms are not rendered on Argonian females | ||||
| Bug #1407: Alchemy allows making potions from two of the same item | ||||
| Bug #1408: "Max sale" button gives you all the items AND all the trader's gold | ||||
| Bug #1409: Rest "Until Healed" broken for characters with stunted magicka. | ||||
| Bug #1412: Empty travel window opens while playing through start game | ||||
| Bug #1413: Save game ignores missing writing permission | ||||
| Bug #1414: The Underground 2 ESM Error | ||||
| Bug #1416: Not all splash screens in the Splash directory are used | ||||
| Bug #1417: Loading saved game does not terminate | ||||
| Bug #1419: Skyrim: Home of the Nords error | ||||
| Bug #1422: ClearInfoActor | ||||
| Bug #1423: ForceGreeting closes existing dialogue windows | ||||
| Bug #1425: Cannot load save game | ||||
| Bug #1426: Read skill books aren't stored in savegame | ||||
| Bug #1427: Useless items can be set under hotkeys | ||||
| Bug #1429: Text variables in journal | ||||
| Bug #1432: When attacking friendly NPC, the crime is reported and bounty is raised after each swing | ||||
| Bug #1435: Stealing priceless items is without punishment | ||||
| Bug #1437: Door marker at Jobasha's Rare Books is spawning PC in the air | ||||
| Bug #1440: Topic selection menu should be wider | ||||
| Bug #1441: Dropping items on the rug makes them inaccessible | ||||
| Bug #1442: When dropping and taking some looted items, bystanders consider that as a crime | ||||
| Bug #1444: Arrows and bolts are not dropped where the cursor points | ||||
| Bug #1445: Security trainers offering acrobatics instead | ||||
| Bug #1447: Character dash not displayed, French edition | ||||
| Bug #1448: When the player is killed by the guard while having a bounty on his head, the guard dialogue opens over and over instead of loading dialogue | ||||
| Bug #1454: Script error in SkipTutorial | ||||
| Bug #1456: Bad lighting when using certain Morrowind.ini generated by MGE | ||||
| Bug #1457: Heart of Lorkan comes after you when attacking it | ||||
| Bug #1458: Modified Keybindings are not remembered | ||||
| Bug #1459: Dura Gra-Bol doesn't respond to PC attack | ||||
| Bug #1462: Interior cells not loaded with Morrowind Patch active | ||||
| Bug #1469: Item tooltip should show the base value, not real value | ||||
| Bug #1477: Death count is not stored in savegame | ||||
| Bug #1478: AiActivate does not trigger activate scripts | ||||
| Bug #1481: Weapon not rendered when partially submerged in water | ||||
| Bug #1483: Enemies are attacking even while dying | ||||
| Bug #1486: ESM Error: Don't know what to do with INFO | ||||
| Bug #1490: Arrows shot at PC can end up in inventory | ||||
| Bug #1492: Monsters respawn on top of one another | ||||
| Bug #1493: Dialogue box opens with follower NPC even if NPC is dead | ||||
| Bug #1494: Paralysed cliffracers remain airbourne | ||||
| Bug #1495: Dialogue box opens with follower NPC even the game is paused | ||||
| Bug #1496: GUI messages are not cleared when loading another saved game | ||||
| Bug #1499: Underwater sound sometimes plays when transitioning from interior. | ||||
| Bug #1500: Targetted spells and water. | ||||
| Bug #1502: Console error message on info refusal | ||||
| Bug #1507: Bloodmoon MQ The Ritual of Beasts: Can't remove the arrow | ||||
| Bug #1508: Bloodmoon: Fort Frostmoth, cant talk with Carnius Magius | ||||
| Bug #1516: PositionCell doesn't move actors to current cell | ||||
| Bug #1518: ForceGreeting broken for explicit references | ||||
| Bug #1522: Crash after attempting to play non-music file | ||||
| Bug #1523: World map empty after loading interior save | ||||
| Bug #1524: Arrows in waiting/resting dialog act like minimum and maximum buttons | ||||
| Bug #1525: Werewolf: Killed NPC's don't fill werewolfs hunger for blood | ||||
| Bug #1527: Werewolf: Detect life detects wrong type of actor | ||||
| Bug #1529: OpenMW crash during "the shrine of the dead" mission (tribunal) | ||||
| Bug #1530: Selected text in the console has the same color as the background | ||||
| Bug #1539: Barilzar's Mazed Band: Tribunal | ||||
| Bug #1542: Looping taunts from NPC`s after death: Tribunal | ||||
| Bug #1543: OpenCS crash when using drag&drop in script editor | ||||
| Bug #1547: Bamz-Amschend: Centurion Archers combat problem | ||||
| Bug #1548: The Missing Hand: Tribunal | ||||
| Bug #1549: The Mad God: Tribunal, Dome of Serlyn | ||||
| Bug #1557: A bounty is calculated from actual item cost | ||||
| Bug #1562: Invisible terrain on top of Red Mountain | ||||
| Bug #1564: Cave of the hidden music: Bloodmoon | ||||
| Bug #1567: Editor: Deleting of referenceables does not work | ||||
| Bug #1568: Picking up a stack of items and holding the enter key and moving your mouse around paints a bunch of garbage on screen. | ||||
| Bug #1574: Solstheim: Drauger cant inflict damage on player | ||||
| Bug #1578: Solstheim: Bonewolf running animation not working | ||||
| Bug #1585: Particle effects on PC are stopped when paralyzed | ||||
| Bug #1589: Tribunal: Crimson Plague quest does not update when Gedna Relvel is killed | ||||
| Bug #1590: Failed to save game: compile error | ||||
| Bug #1598: Segfault when making Drain/Fortify Skill spells | ||||
| Bug #1599: Unable to switch to fullscreen | ||||
| Bug #1613: Morrowind Rebirth duplicate objects / vanilla objects not removed | ||||
| Feature #32: Periodic Cleanup/Refill | ||||
| Feature #41: Precipitation and weather particles | ||||
| Feature #568: Editor: Configuration setup | ||||
| Feature #649: Editor: Threaded loading | ||||
| Feature #930: Editor: Cell record saving | ||||
| Feature #934: Editor: Body part table | ||||
| Feature #935: Editor: Enchantment effect table | ||||
| Feature #1162: Dialogue merging | ||||
| Feature #1174: Saved Game: add missing creature state | ||||
| Feature #1177: Saved Game: fog of war state | ||||
| Feature #1312: Editor: Combat/Magic/Stealth values for creatures are not displayed | ||||
| Feature #1314: Make NPCs and creatures fight each other | ||||
| Feature #1315: Crime: Murder | ||||
| Feature #1321: Sneak skill enhancements | ||||
| Feature #1323: Handle restocking items | ||||
| Feature #1332: Saved Game: levelled creatures | ||||
| Feature #1347: modFactionReaction script instruction | ||||
| Feature #1362: Animated main menu support | ||||
| Feature #1433: Store walk/run toggle | ||||
| Feature #1449: Use names instead of numbers for saved game files and folders | ||||
| Feature #1453: Adding Delete button to the load menu | ||||
| Feature #1460: Enable Journal screen while in dialogue | ||||
| Feature #1480: Play Battle music when in combat | ||||
| Feature #1501: Followers unable to fast travel with you | ||||
| Feature #1520: Disposition and distance-based aggression/ShouldAttack | ||||
| Feature #1595: Editor: Object rendering in cells | ||||
| Task #940: Move license to locations where applicable | ||||
| Task #1333: Remove cmake git tag reading | ||||
| Task #1566: Editor: Object rendering refactoring | ||||
| 
 | ||||
| 0.30.0 | ||||
| 
 | ||||
| Bug #416: Extreme shaking can occur during cell transitions while moving | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue