mirror of
https://gitlab.com/OpenMW/openmw.git
synced 2025-01-26 09:35:28 +00:00
Merge remote-tracking branch 'scrawl/master'
This commit is contained in:
commit
cae0c4a044
@ -12,7 +12,7 @@ set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/)
|
|||||||
message(STATUS "Configuring OpenMW...")
|
message(STATUS "Configuring OpenMW...")
|
||||||
|
|
||||||
set(OPENMW_VERSION_MAJOR 0)
|
set(OPENMW_VERSION_MAJOR 0)
|
||||||
set(OPENMW_VERSION_MINOR 30)
|
set(OPENMW_VERSION_MINOR 31)
|
||||||
set(OPENMW_VERSION_RELEASE 0)
|
set(OPENMW_VERSION_RELEASE 0)
|
||||||
|
|
||||||
set(OPENMW_VERSION_COMMITHASH "")
|
set(OPENMW_VERSION_COMMITHASH "")
|
||||||
|
@ -69,7 +69,7 @@ add_openmw_dir (mwmechanics
|
|||||||
mechanicsmanagerimp stat character creaturestats magiceffects movement actors objects
|
mechanicsmanagerimp stat character creaturestats magiceffects movement actors objects
|
||||||
drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor
|
drawstate spells activespells npcstats aipackage aisequence aipursue alchemy aiwander aitravel aifollow aiavoiddoor
|
||||||
aiescort aiactivate aicombat repair enchanting pathfinding pathgrid security spellsuccess spellcasting
|
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
|
add_openmw_dir (mwstate
|
||||||
|
@ -250,8 +250,11 @@ namespace MWClass
|
|||||||
text += "\n#{sTrapped}";
|
text += "\n#{sTrapped}";
|
||||||
|
|
||||||
if (MWBase::Environment::get().getWindowManager()->getFullHelp())
|
if (MWBase::Environment::get().getWindowManager()->getFullHelp())
|
||||||
|
{
|
||||||
text += MWGui::ToolTips::getMiscString(ref->mBase->mScript, "Script");
|
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;
|
info.text = text;
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
#include "../mwmechanics/spellcasting.hpp"
|
#include "../mwmechanics/spellcasting.hpp"
|
||||||
#include "../mwmechanics/disease.hpp"
|
#include "../mwmechanics/disease.hpp"
|
||||||
#include "../mwmechanics/combat.hpp"
|
#include "../mwmechanics/combat.hpp"
|
||||||
|
#include "../mwmechanics/autocalcspell.hpp"
|
||||||
|
|
||||||
#include "../mwworld/ptr.hpp"
|
#include "../mwworld/ptr.hpp"
|
||||||
#include "../mwworld/actiontalk.hpp"
|
#include "../mwworld/actiontalk.hpp"
|
||||||
@ -53,6 +54,24 @@ namespace
|
|||||||
return new NpcCustomData (*this);
|
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)
|
void autoCalculateAttributes (const ESM::NPC* npc, MWMechanics::CreatureStats& creatureStats)
|
||||||
{
|
{
|
||||||
// race bonus
|
// race bonus
|
||||||
@ -108,8 +127,9 @@ namespace
|
|||||||
}
|
}
|
||||||
modifierSum += add;
|
modifierSum += add;
|
||||||
}
|
}
|
||||||
creatureStats.setAttribute(attribute, std::min(creatureStats.getAttribute(attribute).getBase()
|
creatureStats.setAttribute(attribute, std::min(
|
||||||
+ static_cast<int>((level-1) * modifierSum+0.5), 100) );
|
round_ieee_754(creatureStats.getAttribute(attribute).getBase()
|
||||||
|
+ (level-1) * modifierSum), 100) );
|
||||||
}
|
}
|
||||||
|
|
||||||
// initial health
|
// initial health
|
||||||
@ -193,18 +213,6 @@ namespace
|
|||||||
majorMultiplier = 1.0f;
|
majorMultiplier = 1.0f;
|
||||||
break;
|
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?
|
// is this skill in the same Specialization as the class?
|
||||||
@ -217,12 +225,25 @@ namespace
|
|||||||
|
|
||||||
npcStats.getSkill(skillIndex).setBase(
|
npcStats.getSkill(skillIndex).setBase(
|
||||||
std::min(
|
std::min(
|
||||||
npcStats.getSkill(skillIndex).getBase()
|
round_ieee_754(
|
||||||
|
npcStats.getSkill(skillIndex).getBase()
|
||||||
+ 5
|
+ 5
|
||||||
+ raceBonus
|
+ raceBonus
|
||||||
+ specBonus
|
+ 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);
|
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 exit();
|
||||||
|
|
||||||
|
virtual void resetReference();
|
||||||
|
|
||||||
void open(const MWWorld::Ptr& npc);
|
void open(const MWWorld::Ptr& npc);
|
||||||
void onFrame ();
|
void onFrame ();
|
||||||
|
|
||||||
|
@ -258,6 +258,12 @@ namespace MWGui
|
|||||||
onTakeAllButtonClicked(mTakeButton);
|
onTakeAllButtonClicked(mTakeButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ContainerWindow::resetReference()
|
||||||
|
{
|
||||||
|
ReferenceInterface::resetReference();
|
||||||
|
mItemView->setModel(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
void ContainerWindow::close()
|
void ContainerWindow::close()
|
||||||
{
|
{
|
||||||
WindowBase::close();
|
WindowBase::close();
|
||||||
|
@ -54,6 +54,8 @@ namespace MWGui
|
|||||||
void open(const MWWorld::Ptr& container, bool loot=false);
|
void open(const MWWorld::Ptr& container, bool loot=false);
|
||||||
virtual void close();
|
virtual void close();
|
||||||
|
|
||||||
|
virtual void resetReference();
|
||||||
|
|
||||||
virtual void exit();
|
virtual void exit();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
@ -260,21 +260,28 @@ namespace MWGui
|
|||||||
|
|
||||||
// More hacks! The french game uses several win1252 characters that are not included
|
// 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.
|
// in the cp437 encoding of the font. Fall back to similar available characters.
|
||||||
// Same for U+2013
|
if (mEncoding == ToUTF8::CP437)
|
||||||
std::map<int, int> additional;
|
|
||||||
additional[39] = 0x2019; // apostrophe
|
|
||||||
additional[45] = 0x2013; // dash
|
|
||||||
if (additional.find(i) != additional.end() && mEncoding == ToUTF8::CP437)
|
|
||||||
{
|
{
|
||||||
MyGUI::xml::ElementPtr code = codes->createChild("Code");
|
std::multimap<int, int> additional;
|
||||||
code->addAttribute("index", additional[i]);
|
additional.insert(std::make_pair(39, 0x2019)); // apostrophe
|
||||||
code->addAttribute("coord", MyGUI::utility::toString(x1) + " "
|
additional.insert(std::make_pair(45, 0x2013)); // dash
|
||||||
+ MyGUI::utility::toString(y1) + " "
|
additional.insert(std::make_pair(34, 0x201D)); // right double quotation mark
|
||||||
+ MyGUI::utility::toString(w) + " "
|
additional.insert(std::make_pair(34, 0x201C)); // left double quotation mark
|
||||||
+ MyGUI::utility::toString(h));
|
for (std::multimap<int, int>::iterator it = additional.begin(); it != additional.end(); ++it)
|
||||||
code->addAttribute("advance", data[i].width);
|
{
|
||||||
code->addAttribute("bearing", MyGUI::utility::toString(data[i].kerning) + " "
|
if (it->first != i)
|
||||||
+ MyGUI::utility::toString((fontSize-data[i].ascent)));
|
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
|
// 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)
|
void ItemView::setSize(const MyGUI::IntSize &_value)
|
||||||
{
|
{
|
||||||
|
bool changed = (_value.width != getWidth() || _value.height != getHeight());
|
||||||
Base::setSize(_value);
|
Base::setSize(_value);
|
||||||
update();
|
if (changed)
|
||||||
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ItemView::setSize(int _width, int _height)
|
void ItemView::setSize(int _width, int _height)
|
||||||
{
|
{
|
||||||
Base::setSize(_width, _height);
|
setSize(MyGUI::IntSize(_width, _height));
|
||||||
update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ItemView::setCoord(const MyGUI::IntCoord &_value)
|
void ItemView::setCoord(const MyGUI::IntCoord &_value)
|
||||||
{
|
{
|
||||||
|
bool changed = (_value.width != getWidth() || _value.height != getHeight());
|
||||||
Base::setCoord(_value);
|
Base::setCoord(_value);
|
||||||
update();
|
if (changed)
|
||||||
|
update();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ItemView::setCoord(int _left, int _top, int _width, int _height)
|
void ItemView::setCoord(int _left, int _top, int _width, int _height)
|
||||||
{
|
{
|
||||||
Base::setCoord(_left, _top, _width, _height);
|
setCoord(MyGUI::IntCoord(_left, _top, _width, _height));
|
||||||
update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ItemView::registerComponents()
|
void ItemView::registerComponents()
|
||||||
|
@ -531,4 +531,10 @@ namespace MWGui
|
|||||||
sellerStats.setLastRestockTime(MWBase::Environment::get().getWorld()->getTimeStamp());
|
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 exit();
|
||||||
|
|
||||||
|
virtual void resetReference();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ItemView* mItemView;
|
ItemView* mItemView;
|
||||||
|
@ -707,11 +707,13 @@ namespace MWInput
|
|||||||
}
|
}
|
||||||
|
|
||||||
void InputManager::quickLoad() {
|
void InputManager::quickLoad() {
|
||||||
MWBase::Environment::get().getStateManager()->quickLoad();
|
if (!MyGUI::InputManager::getInstance().isModalAny())
|
||||||
|
MWBase::Environment::get().getStateManager()->quickLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
void InputManager::quickSave() {
|
void InputManager::quickSave() {
|
||||||
MWBase::Environment::get().getStateManager()->quickSave();
|
if (!MyGUI::InputManager::getInstance().isModalAny())
|
||||||
|
MWBase::Environment::get().getStateManager()->quickSave();
|
||||||
}
|
}
|
||||||
void InputManager::toggleSpell()
|
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. */
|
* beginning. */
|
||||||
int mode = ((movement == mCurrentMovement) ? 2 : 1);
|
int mode = ((movement == mCurrentMovement) ? 2 : 1);
|
||||||
|
|
||||||
|
mMovementAnimationControlled = true;
|
||||||
|
|
||||||
mAnimation->disable(mCurrentMovement);
|
mAnimation->disable(mCurrentMovement);
|
||||||
mCurrentMovement = movement;
|
mCurrentMovement = movement;
|
||||||
mMovementAnimVelocity = 0.0f;
|
|
||||||
if(!mCurrentMovement.empty())
|
if(!mCurrentMovement.empty())
|
||||||
{
|
{
|
||||||
float vel, speedmult = 1.0f;
|
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)
|
if(mMovementSpeed > 0.0f && (vel=mAnimation->getVelocity(anim)) > 1.0f)
|
||||||
{
|
{
|
||||||
mMovementAnimVelocity = vel;
|
|
||||||
speedmult = mMovementSpeed / vel;
|
speedmult = mMovementSpeed / vel;
|
||||||
}
|
}
|
||||||
else if (mMovementState == CharState_TurnLeft || mMovementState == CharState_TurnRight)
|
else if (mMovementState == CharState_TurnLeft || mMovementState == CharState_TurnRight)
|
||||||
speedmult = 1.f; // TODO: should get a speed mult depending on the current turning speed
|
speedmult = 1.f; // TODO: should get a speed mult depending on the current turning speed
|
||||||
else if (mMovementSpeed > 0.0f)
|
else if (mMovementSpeed > 0.0f)
|
||||||
|
{
|
||||||
// The first person anims don't have any velocity to calculate a speed multiplier from.
|
// The first person anims don't have any velocity to calculate a speed multiplier from.
|
||||||
// We use the third person velocities instead.
|
// We use the third person velocities instead.
|
||||||
// FIXME: should be pulled from the actual animation, but it is not presently loaded.
|
// FIXME: should be pulled from the actual animation, but it is not presently loaded.
|
||||||
speedmult = mMovementSpeed / (isrunning ? 222.857f : 154.064f);
|
speedmult = mMovementSpeed / (isrunning ? 222.857f : 154.064f);
|
||||||
|
mMovementAnimationControlled = false;
|
||||||
|
}
|
||||||
mAnimation->play(mCurrentMovement, Priority_Movement, movegroup, false,
|
mAnimation->play(mCurrentMovement, Priority_Movement, movegroup, false,
|
||||||
speedmult, ((mode!=2)?"start":"loop start"), "stop", 0.0f, ~0ul);
|
speedmult, ((mode!=2)?"start":"loop start"), "stop", 0.0f, ~0ul);
|
||||||
}
|
}
|
||||||
@ -506,6 +509,7 @@ void CharacterController::playDeath(float startpoint, CharacterState death)
|
|||||||
mJumpState = JumpState_None;
|
mJumpState = JumpState_None;
|
||||||
mAnimation->disable(mCurrentJump);
|
mAnimation->disable(mCurrentJump);
|
||||||
mCurrentJump = "";
|
mCurrentJump = "";
|
||||||
|
mMovementAnimationControlled = true;
|
||||||
|
|
||||||
mAnimation->play(mCurrentDeath, Priority_Death, MWRender::Animation::Group_All,
|
mAnimation->play(mCurrentDeath, Priority_Death, MWRender::Animation::Group_All,
|
||||||
false, 1.0f, "start", "stop", startpoint, 0);
|
false, 1.0f, "start", "stop", startpoint, 0);
|
||||||
@ -547,7 +551,7 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim
|
|||||||
, mIdleState(CharState_None)
|
, mIdleState(CharState_None)
|
||||||
, mMovementState(CharState_None)
|
, mMovementState(CharState_None)
|
||||||
, mMovementSpeed(0.0f)
|
, mMovementSpeed(0.0f)
|
||||||
, mMovementAnimVelocity(0.0f)
|
, mMovementAnimationControlled(true)
|
||||||
, mDeathState(CharState_None)
|
, mDeathState(CharState_None)
|
||||||
, mHitState(CharState_None)
|
, mHitState(CharState_None)
|
||||||
, mUpperBodyState(UpperCharState_Nothing)
|
, mUpperBodyState(UpperCharState_Nothing)
|
||||||
@ -570,10 +574,14 @@ CharacterController::CharacterController(const MWWorld::Ptr &ptr, MWRender::Anim
|
|||||||
if (cls.hasInventoryStore(mPtr))
|
if (cls.hasInventoryStore(mPtr))
|
||||||
{
|
{
|
||||||
getActiveWeapon(cls.getCreatureStats(mPtr), cls.getInventoryStore(mPtr), &mWeaponType);
|
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)
|
if(mWeaponType != WeapType_None && mWeaponType != WeapType_Spell && mWeaponType != WeapType_HandToHand)
|
||||||
{
|
{
|
||||||
getWeaponGroup(mWeaponType, mCurrentWeapon);
|
|
||||||
mUpperBodyState = UpperCharState_WeapEquiped;
|
|
||||||
mAnimation->showWeapons(true);
|
mAnimation->showWeapons(true);
|
||||||
mAnimation->setWeaponGroup(mCurrentWeapon);
|
mAnimation->setWeaponGroup(mCurrentWeapon);
|
||||||
}
|
}
|
||||||
@ -1241,6 +1249,7 @@ void CharacterController::update(float duration)
|
|||||||
if (inwater || flying)
|
if (inwater || flying)
|
||||||
cls.getCreatureStats(mPtr).land();
|
cls.getCreatureStats(mPtr).land();
|
||||||
|
|
||||||
|
bool inJump = true;
|
||||||
if(!onground && !flying && !inwater)
|
if(!onground && !flying && !inwater)
|
||||||
{
|
{
|
||||||
// In the air (either getting up —ascending part of jump— or falling).
|
// In the air (either getting up —ascending part of jump— or falling).
|
||||||
@ -1330,6 +1339,8 @@ void CharacterController::update(float duration)
|
|||||||
mJumpState = JumpState_None;
|
mJumpState = JumpState_None;
|
||||||
vec.z = 0.0f;
|
vec.z = 0.0f;
|
||||||
|
|
||||||
|
inJump = false;
|
||||||
|
|
||||||
if(std::abs(vec.x/2.0f) > std::abs(vec.y))
|
if(std::abs(vec.x/2.0f) > std::abs(vec.y))
|
||||||
{
|
{
|
||||||
if(vec.x > 0.0f)
|
if(vec.x > 0.0f)
|
||||||
@ -1391,6 +1402,8 @@ void CharacterController::update(float duration)
|
|||||||
forcestateupdate = updateCreatureState() || forcestateupdate;
|
forcestateupdate = updateCreatureState() || forcestateupdate;
|
||||||
|
|
||||||
refreshCurrentAnims(idlestate, movestate, forcestateupdate);
|
refreshCurrentAnims(idlestate, movestate, forcestateupdate);
|
||||||
|
if (inJump)
|
||||||
|
mMovementAnimationControlled = false;
|
||||||
|
|
||||||
if (!mSkipAnim)
|
if (!mSkipAnim)
|
||||||
{
|
{
|
||||||
@ -1402,7 +1415,7 @@ void CharacterController::update(float duration)
|
|||||||
else //avoid z-rotating for knockdown
|
else //avoid z-rotating for knockdown
|
||||||
world->rotateObject(mPtr, rot.x, rot.y, 0.0f, true);
|
world->rotateObject(mPtr, rot.x, rot.y, 0.0f, true);
|
||||||
|
|
||||||
if (mMovementAnimVelocity == 0)
|
if (!mMovementAnimationControlled)
|
||||||
world->queueMovement(mPtr, vec);
|
world->queueMovement(mPtr, vec);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -1446,7 +1459,7 @@ void CharacterController::update(float duration)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update movement
|
// Update movement
|
||||||
if(mMovementAnimVelocity > 0)
|
if(mMovementAnimationControlled && mPtr.getClass().isActor())
|
||||||
world->queueMovement(mPtr, moved);
|
world->queueMovement(mPtr, moved);
|
||||||
}
|
}
|
||||||
else if (mAnimation)
|
else if (mAnimation)
|
||||||
|
@ -147,7 +147,7 @@ class CharacterController
|
|||||||
CharacterState mMovementState;
|
CharacterState mMovementState;
|
||||||
std::string mCurrentMovement;
|
std::string mCurrentMovement;
|
||||||
float mMovementSpeed;
|
float mMovementSpeed;
|
||||||
float mMovementAnimVelocity;
|
bool mMovementAnimationControlled;
|
||||||
|
|
||||||
CharacterState mDeathState;
|
CharacterState mDeathState;
|
||||||
std::string mCurrentDeath;
|
std::string mCurrentDeath;
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
#include <OgreSceneNode.h>
|
#include <OgreSceneNode.h>
|
||||||
|
|
||||||
#include "spellcasting.hpp"
|
#include "spellcasting.hpp"
|
||||||
|
#include "autocalcspell.hpp"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@ -155,19 +156,6 @@ namespace MWMechanics
|
|||||||
npcStats.getSkill (index).setBase (
|
npcStats.getSkill (index).setBase (
|
||||||
npcStats.getSkill (index).getBase() + bonus);
|
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
|
// forced update and current value adjustments
|
||||||
mActors.updateActor (ptr, 0);
|
mActors.updateActor (ptr, 0);
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ namespace MWMechanics
|
|||||||
x *= 0.1 * magicEffect->mData.mBaseCost;
|
x *= 0.1 * magicEffect->mData.mBaseCost;
|
||||||
x *= 0.5 * (it->mMagnMin + it->mMagnMax);
|
x *= 0.5 * (it->mMagnMin + it->mMagnMax);
|
||||||
x *= it->mArea * 0.05 * magicEffect->mData.mBaseCost;
|
x *= it->mArea * 0.05 * magicEffect->mData.mBaseCost;
|
||||||
if (magicEffect->mData.mFlags & ESM::MagicEffect::CastTarget)
|
if (it->mRange == ESM::RT_Target)
|
||||||
x *= 1.5;
|
x *= 1.5;
|
||||||
static const float fEffectCostMult = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find(
|
static const float fEffectCostMult = MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>().find(
|
||||||
"fEffectCostMult")->getFloat();
|
"fEffectCostMult")->getFloat();
|
||||||
|
@ -149,6 +149,8 @@ namespace MWRender
|
|||||||
mViewModeToggleQueued = true;
|
mViewModeToggleQueued = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
mViewModeToggleQueued = false;
|
||||||
|
|
||||||
mFirstPersonView = !mFirstPersonView;
|
mFirstPersonView = !mFirstPersonView;
|
||||||
processViewChange();
|
processViewChange();
|
||||||
|
@ -1088,7 +1088,7 @@ public:
|
|||||||
|
|
||||||
void close() { }
|
void close() { }
|
||||||
|
|
||||||
bool update(Ogre::MaterialPtr &mat, Ogre::Rectangle2D *rect, int screen_width, int screen_height)
|
bool update()
|
||||||
{ return false; }
|
{ return false; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -86,16 +86,24 @@ namespace MWScript
|
|||||||
float ay = Ogre::Radian(ptr.getRefData().getPosition().rot[1]).valueDegrees();
|
float ay = Ogre::Radian(ptr.getRefData().getPosition().rot[1]).valueDegrees();
|
||||||
float az = Ogre::Radian(ptr.getRefData().getPosition().rot[2]).valueDegrees();
|
float az = Ogre::Radian(ptr.getRefData().getPosition().rot[2]).valueDegrees();
|
||||||
|
|
||||||
|
MWWorld::LocalRotation localRot = ptr.getRefData().getLocalRotation();
|
||||||
|
|
||||||
if (axis == "x")
|
if (axis == "x")
|
||||||
{
|
{
|
||||||
|
localRot.rot[0] = 0;
|
||||||
|
ptr.getRefData().setLocalRotation(localRot);
|
||||||
MWBase::Environment::get().getWorld()->rotateObject(ptr,angle,ay,az);
|
MWBase::Environment::get().getWorld()->rotateObject(ptr,angle,ay,az);
|
||||||
}
|
}
|
||||||
else if (axis == "y")
|
else if (axis == "y")
|
||||||
{
|
{
|
||||||
|
localRot.rot[1] = 0;
|
||||||
|
ptr.getRefData().setLocalRotation(localRot);
|
||||||
MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,angle,az);
|
MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,angle,az);
|
||||||
}
|
}
|
||||||
else if (axis == "z")
|
else if (axis == "z")
|
||||||
{
|
{
|
||||||
|
localRot.rot[2] = 0;
|
||||||
|
ptr.getRefData().setLocalRotation(localRot);
|
||||||
MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,angle);
|
MWBase::Environment::get().getWorld()->rotateObject(ptr,ax,ay,angle);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -209,8 +209,6 @@ namespace MWWorld
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setUp() {
|
void setUp() {
|
||||||
//std::sort(mStatic.begin(), mStatic.end(), RecordCmp());
|
|
||||||
|
|
||||||
mShared.clear();
|
mShared.clear();
|
||||||
mShared.reserve(mStatic.size());
|
mShared.reserve(mStatic.size());
|
||||||
typename std::map<std::string, T>::iterator it = mStatic.begin();
|
typename std::map<std::string, T>::iterator it = mStatic.begin();
|
||||||
@ -675,18 +673,15 @@ namespace MWWorld
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setUp() {
|
void setUp() {
|
||||||
//typedef std::vector<ESM::Cell>::iterator Iterator;
|
|
||||||
typedef DynamicExt::iterator ExtIterator;
|
typedef DynamicExt::iterator ExtIterator;
|
||||||
typedef std::map<std::string, ESM::Cell>::iterator IntIterator;
|
typedef std::map<std::string, ESM::Cell>::iterator IntIterator;
|
||||||
|
|
||||||
//std::sort(mInt.begin(), mInt.end(), RecordCmp());
|
|
||||||
mSharedInt.clear();
|
mSharedInt.clear();
|
||||||
mSharedInt.reserve(mInt.size());
|
mSharedInt.reserve(mInt.size());
|
||||||
for (IntIterator it = mInt.begin(); it != mInt.end(); ++it) {
|
for (IntIterator it = mInt.begin(); it != mInt.end(); ++it) {
|
||||||
mSharedInt.push_back(&(it->second));
|
mSharedInt.push_back(&(it->second));
|
||||||
}
|
}
|
||||||
|
|
||||||
//std::sort(mExt.begin(), mExt.end(), ExtCmp());
|
|
||||||
mSharedExt.clear();
|
mSharedExt.clear();
|
||||||
mSharedExt.reserve(mExt.size());
|
mSharedExt.reserve(mExt.size());
|
||||||
for (ExtIterator it = mExt.begin(); it != mExt.end(); ++it) {
|
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
|
} //end namespace
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -1123,7 +1123,10 @@ namespace MWWorld
|
|||||||
|
|
||||||
ptr.getRefData().setPosition(pos);
|
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)
|
void World::localRotateObject (const Ptr& ptr, float x, float y, float z)
|
||||||
@ -2649,6 +2652,8 @@ namespace MWWorld
|
|||||||
{
|
{
|
||||||
mGoToJail = false;
|
mGoToJail = false;
|
||||||
|
|
||||||
|
MWBase::Environment::get().getWindowManager()->removeGuiMode(MWGui::GM_Dialogue);
|
||||||
|
|
||||||
MWWorld::Ptr player = getPlayerPtr();
|
MWWorld::Ptr player = getPlayerPtr();
|
||||||
teleportToClosestMarker(player, "prisonmarker");
|
teleportToClosestMarker(player, "prisonmarker");
|
||||||
int bounty = player.getClass().getNpcStats(player).getBounty();
|
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)
|
void NPC::load(ESMReader &esm)
|
||||||
{
|
{
|
||||||
//mNpdt52.mGold = -10;
|
|
||||||
|
|
||||||
mPersistent = esm.getRecordFlags() & 0x0400;
|
mPersistent = esm.getRecordFlags() & 0x0400;
|
||||||
|
|
||||||
mModel = esm.getHNOString("MODL");
|
mModel = esm.getHNOString("MODL");
|
||||||
@ -63,7 +61,6 @@ void NPC::load(ESMReader &esm)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
mAiPackage.load(esm);
|
mAiPackage.load(esm);
|
||||||
esm.skipRecord();
|
|
||||||
}
|
}
|
||||||
void NPC::save(ESMWriter &esm) const
|
void NPC::save(ESMWriter &esm) const
|
||||||
{
|
{
|
||||||
|
@ -27,8 +27,8 @@ struct Spell
|
|||||||
|
|
||||||
enum Flags
|
enum Flags
|
||||||
{
|
{
|
||||||
F_Autocalc = 1,
|
F_Autocalc = 1, // Can be selected by NPC spells auto-calc
|
||||||
F_PCStart = 2,
|
F_PCStart = 2, // Can be selected by player spells auto-calc
|
||||||
F_Always = 4 // Casting always succeeds
|
F_Always = 4 // Casting always succeeds
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12,59 +12,6 @@
|
|||||||
namespace Misc
|
namespace Misc
|
||||||
{
|
{
|
||||||
|
|
||||||
bool begins(const char* str1, const char* str2)
|
std::locale StringUtils::mLocale = std::locale::classic();
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,15 +4,18 @@
|
|||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <locale>
|
||||||
|
|
||||||
namespace Misc
|
namespace Misc
|
||||||
{
|
{
|
||||||
class StringUtils
|
class StringUtils
|
||||||
{
|
{
|
||||||
|
|
||||||
|
static std::locale mLocale;
|
||||||
struct ci
|
struct ci
|
||||||
{
|
{
|
||||||
bool operator()(int x, int y) const {
|
bool operator()(char x, char y) const {
|
||||||
return std::tolower(x) < std::tolower(y);
|
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 xit = x.begin();
|
||||||
std::string::const_iterator yit = y.begin();
|
std::string::const_iterator yit = y.begin();
|
||||||
for (; xit != x.end(); ++xit, ++yit) {
|
for (; xit != x.end(); ++xit, ++yit) {
|
||||||
if (std::tolower(*xit) != std::tolower(*yit)) {
|
if (std::tolower(*xit, mLocale) != std::tolower(*yit, mLocale)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,7 +45,7 @@ public:
|
|||||||
for(;xit != x.end() && yit != y.end() && len > 0;++xit,++yit,--len)
|
for(;xit != x.end() && yit != y.end() && len > 0;++xit,++yit,--len)
|
||||||
{
|
{
|
||||||
int res = *xit - *yit;
|
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;
|
return (res > 0) ? 1 : -1;
|
||||||
}
|
}
|
||||||
if(len > 0)
|
if(len > 0)
|
||||||
@ -57,12 +60,8 @@ public:
|
|||||||
|
|
||||||
/// Transforms input string to lower case w/o copy
|
/// Transforms input string to lower case w/o copy
|
||||||
static std::string &toLower(std::string &inout) {
|
static std::string &toLower(std::string &inout) {
|
||||||
std::transform(
|
for (unsigned int i=0; i<inout.size(); ++i)
|
||||||
inout.begin(),
|
inout[i] = std::tolower(inout[i], mLocale);
|
||||||
inout.end(),
|
|
||||||
inout.begin(),
|
|
||||||
(int (*)(int)) std::tolower
|
|
||||||
);
|
|
||||||
return inout;
|
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
|
#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
|
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.
|
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)
|
License: GPL (see GPL3.txt for more information)
|
||||||
Website: http://www.openmw.org
|
Website: http://www.openmw.org
|
||||||
|
|
||||||
@ -96,6 +96,188 @@ Allowed options:
|
|||||||
|
|
||||||
CHANGELOG
|
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
|
0.30.0
|
||||||
|
|
||||||
Bug #416: Extreme shaking can occur during cell transitions while moving
|
Bug #416: Extreme shaking can occur during cell transitions while moving
|
||||||
|
Loading…
x
Reference in New Issue
Block a user