mirror of
https://github.com/OpenMW/openmw.git
synced 2025-01-21 07:53:53 +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