Merge remote-tracking branch 'scrawl/master'

deque
Marc Zinnschlag 11 years ago
commit cae0c4a044

@ -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()
{

@ -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;
}
}

@ -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 && mWeaponType != WeapType_Spell && mWeaponType != WeapType_HandToHand)
if (mWeaponType != WeapType_None)
{
getWeaponGroup(mWeaponType, mCurrentWeapon);
mUpperBodyState = UpperCharState_WeapEquiped;
getWeaponGroup(mWeaponType, mCurrentWeapon);
}
if(mWeaponType != WeapType_None && mWeaponType != WeapType_Spell && mWeaponType != WeapType_HandToHand)
{
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

@ -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…
Cancel
Save