#include "aiwander.hpp" #include #include #include "../mwbase/world.hpp" #include "../mwbase/environment.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/dialoguemanager.hpp" #include "../mwworld/class.hpp" #include "../mwworld/esmstore.hpp" #include "../mwworld/cellstore.hpp" #include "creaturestats.hpp" #include "steering.hpp" #include "movement.hpp" namespace MWMechanics { static const int COUNT_BEFORE_RESET = 200; // TODO: maybe no longer needed static const float DOOR_CHECK_INTERVAL = 1.5f; AiWander::AiWander(int distance, int duration, int timeOfDay, const std::vector& idle, bool repeat): mDistance(distance), mDuration(duration), mTimeOfDay(timeOfDay), mIdle(idle), mRepeat(repeat) , mCellX(std::numeric_limits::max()) , mCellY(std::numeric_limits::max()) , mXCell(0) , mYCell(0) , mCell(NULL) , mStuckCount(0) // TODO: maybe no longer needed , mDoorCheckDuration(0) , mTrimCurrentNode(false) , mReaction(0) , mGreetDistanceMultiplier(0) , mGreetDistanceReset(0) , mChance(0) , mRotate(false) , mTargetAngle(0) , mSaidGreeting(false) , mHasReturnPosition(false) , mReturnPosition(0,0,0) { for(unsigned short counter = 0; counter < mIdle.size(); counter++) { if(mIdle[counter] >= 127 || mIdle[counter] < 0) mIdle[counter] = 0; } if(mDistance < 0) mDistance = 0; if(mDuration < 0) mDuration = 0; if(mDuration == 0) mTimeOfDay = 0; mStartTime = MWBase::Environment::get().getWorld()->getTimeStamp(); mPlayedIdle = 0; const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); mIdleChanceMultiplier = store.get().find("fIdleChanceMultiplier")->getFloat(); mGreetDistanceMultiplier = store.get().find("iGreetDistanceMultiplier")->getInt(); mGreetDistanceReset = store.get().find("fGreetDistanceReset")->getFloat(); mChance = store.get().find("fVoiceIdleOdds")->getFloat(); mStoredAvailableNodes = false; mChooseAction = true; mIdleNow = false; mMoveNow = false; mWalking = false; } AiPackage * MWMechanics::AiWander::clone() const { return new AiWander(*this); } /* * AiWander high level states (0.29.0). Not entirely accurate in some cases * e.g. non-NPC actors do not greet and some creatures may be moving even in * the IdleNow state. * * [select node, * build path] * +---------->MoveNow----------->Walking * | | * [allowed | | * nodes] | [hello if near] | * start--->ChooseAction----->IdleNow | * ^ ^ | | * | | | | * | +-----------+ | * | | * +----------------------------------+ * * * New high level states. Not exactly as per vanilla (e.g. door stuff) * but the differences are required because our physics does not work like * vanilla and therefore have to compensate/work around. * * [select node, [if stuck evade * build path] or remove nodes if near door] * +---------->MoveNow<---------->Walking * | ^ | | * | |(near door) | | * [allowed | | | | * nodes] | [hello if near] | | * start--->ChooseAction----->IdleNow | | * ^ ^ | ^ | | * | | | | (stuck near | | * | +-----------+ +---------------+ | * | player) | * +----------------------------------+ * * NOTE: non-time critical operations are run once every 250ms or so. * * TODO: It would be great if door opening/closing can be detected and pathgrid * links dynamically updated. Currently (0.29.0) AiWander allows choosing a * destination beyond closed doors which sometimes makes the actors stuck at the * door and impossible for the player to open the door. * * For now detect being stuck at the door and simply delete the nodes from the * allowed set. The issue is when the door opens the allowed set is not * re-calculated. However this would not be an issue in most cases since hostile * actors will enter combat (i.e. no longer wandering) and different pathfinding * will kick in. */ bool AiWander::execute (const MWWorld::Ptr& actor,float duration) { MWMechanics::CreatureStats& cStats = actor.getClass().getCreatureStats(actor); if(cStats.isDead() || cStats.getHealth().getCurrent() <= 0) return true; // Don't bother with dead actors bool cellChange = mCell && (actor.getCell() != mCell); if(!mCell || cellChange) { mCell = actor.getCell(); mStoredAvailableNodes = false; // prob. not needed since mDistance = 0 } const ESM::Cell *cell = mCell->getCell(); cStats.setDrawState(DrawState_Nothing); cStats.setMovementFlag(CreatureStats::Flag_Run, false); ESM::Position pos = actor.getRefData().getPosition(); // Check if an idle actor is too close to a door - if so start walking mDoorCheckDuration += duration; if(mDoorCheckDuration >= DOOR_CHECK_INTERVAL) { mDoorCheckDuration = 0; // restart timer if(mDistance && // actor is not intended to be stationary mIdleNow && // but is in idle !mWalking && // FIXME: some actors are idle while walking proximityToDoor(actor, MIN_DIST_TO_DOOR_SQUARED*1.6*1.6)) // NOTE: checks interior cells only { mIdleNow = false; mMoveNow = true; mTrimCurrentNode = false; // just in case } } if(mWalking) // have not yet reached the destination { // turn towards the next point in mPath zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0], pos.pos[1]))); actor.getClass().getMovementSettings(actor).mPosition[1] = 1; // Returns true if evasive action needs to be taken if(mObstacleCheck.check(actor, duration)) { // first check if we're walking into a door if(proximityToDoor(actor)) // NOTE: checks interior cells only { // remove allowed points then select another random destination mTrimCurrentNode = true; trimAllowedNodes(mAllowedNodes, mPathFinder); mObstacleCheck.clear(); mPathFinder.clearPath(); mWalking = false; mMoveNow = true; } else // probably walking into another NPC { // TODO: diagonal should have same animation as walk forward // but doesn't seem to do that? actor.getClass().getMovementSettings(actor).mPosition[0] = 1; actor.getClass().getMovementSettings(actor).mPosition[1] = 0.1f; // change the angle a bit, too zTurn(actor, Ogre::Degree(mPathFinder.getZAngleToNext(pos.pos[0] + 1, pos.pos[1]))); } mStuckCount++; // TODO: maybe no longer needed } //#if 0 // TODO: maybe no longer needed if(mStuckCount >= COUNT_BEFORE_RESET) // something has gone wrong, reset { //std::cout << "Reset \""<< cls.getName(actor) << "\"" << std::endl; mObstacleCheck.clear(); stopWalking(actor); mMoveNow = false; mWalking = false; mChooseAction = true; } //#endif } if (mRotate) { // Reduce the turning animation glitch by using a *HUGE* value of // epsilon... TODO: a proper fix might be in either the physics or the // animation subsystem if (zTurn(actor, Ogre::Degree(mTargetAngle), Ogre::Degree(5))) mRotate = false; } mReaction += duration; if(mReaction > 0.25f) // FIXME: hard coded constant { mReaction = 0; return false; } // NOTE: everything below get updated every 0.25 seconds MWBase::World *world = MWBase::Environment::get().getWorld(); if(mDuration) { // End package if duration is complete or mid-night hits: MWWorld::TimeStamp currentTime = world->getTimeStamp(); if(currentTime.getHour() >= mStartTime.getHour() + mDuration) { if(!mRepeat) { stopWalking(actor); return true; } else mStartTime = currentTime; } else if(int(currentTime.getHour()) == 0 && currentTime.getDay() != mStartTime.getDay()) { if(!mRepeat) { stopWalking(actor); return true; } else mStartTime = currentTime; } } // Initialization to discover & store allowed node points for this actor. if(!mStoredAvailableNodes) { // infrequently used, therefore no benefit in caching it as a member const ESM::Pathgrid * pathgrid = world->getStore().get().search(*cell); // cache the current cell location mCellX = cell->mData.mX; mCellY = cell->mData.mY; // If there is no path this actor doesn't go anywhere. See: // https://forum.openmw.org/viewtopic.php?t=1556 // http://www.fliggerty.com/phpBB3/viewtopic.php?f=30&t=5833 if(!pathgrid || pathgrid->mPoints.empty()) mDistance = 0; // A distance value passed into the constructor indicates how far the // actor can wander from the spawn position. AiWander assumes that // pathgrid points are available, and uses them to randomly select wander // destinations within the allowed set of pathgrid points (nodes). if(mDistance) { mXCell = 0; mYCell = 0; if(cell->isExterior()) { mXCell = mCellX * ESM::Land::REAL_SIZE; mYCell = mCellY * ESM::Land::REAL_SIZE; } // FIXME: There might be a bug here. The allowed node points are // based on the actor's current position rather than the actor's // spawn point. As a result the allowed nodes for wander can change // between saves, for example. // // convert npcPos to local (i.e. cell) co-ordinates Ogre::Vector3 npcPos(pos.pos); npcPos[0] = npcPos[0] - mXCell; npcPos[1] = npcPos[1] - mYCell; // mAllowedNodes for this actor with pathgrid point indexes based on mDistance // NOTE: mPoints and mAllowedNodes are in local co-ordinates for(unsigned int counter = 0; counter < pathgrid->mPoints.size(); counter++) { Ogre::Vector3 nodePos(pathgrid->mPoints[counter].mX, pathgrid->mPoints[counter].mY, pathgrid->mPoints[counter].mZ); if(npcPos.squaredDistance(nodePos) <= mDistance * mDistance) mAllowedNodes.push_back(pathgrid->mPoints[counter]); } if(!mAllowedNodes.empty()) { Ogre::Vector3 firstNodePos(mAllowedNodes[0].mX, mAllowedNodes[0].mY, mAllowedNodes[0].mZ); float closestNode = npcPos.squaredDistance(firstNodePos); unsigned int index = 0; for(unsigned int counterThree = 1; counterThree < mAllowedNodes.size(); counterThree++) { Ogre::Vector3 nodePos(mAllowedNodes[counterThree].mX, mAllowedNodes[counterThree].mY, mAllowedNodes[counterThree].mZ); float tempDist = npcPos.squaredDistance(nodePos); if(tempDist < closestNode) index = counterThree; } mCurrentNode = mAllowedNodes[index]; mAllowedNodes.erase(mAllowedNodes.begin() + index); mStoredAvailableNodes = true; // set only if successful in finding allowed nodes } } } // Actor becomes stationary - see above URL's for previous research if(mAllowedNodes.empty()) mDistance = 0; // Don't try to move if you are in a new cell (ie: positioncell command called) but still play idles. if(mDistance && cellChange) mDistance = 0; // For stationary NPCs, move back to the starting location if another AiPackage moved us elsewhere if (cellChange) mHasReturnPosition = false; if (mDistance == 0 && mHasReturnPosition && Ogre::Vector3(pos.pos).squaredDistance(mReturnPosition) > 20*20) { mChooseAction = false; mIdleNow = false; if (!mPathFinder.isPathConstructed()) { Ogre::Vector3 destNodePos = mReturnPosition; ESM::Pathgrid::Point dest; dest.mX = destNodePos[0]; dest.mY = destNodePos[1]; dest.mZ = destNodePos[2]; // actor position is already in world co-ordinates ESM::Pathgrid::Point start; start.mX = pos.pos[0]; start.mY = pos.pos[1]; start.mZ = pos.pos[2]; // don't take shortcuts for wandering mPathFinder.buildPath(start, dest, actor.getCell(), false); if(mPathFinder.isPathConstructed()) { mMoveNow = false; mWalking = true; } } } if(mChooseAction) { mPlayedIdle = 0; getRandomIdle(); // NOTE: sets mPlayedIdle with a random selection if(!mPlayedIdle && mDistance) { mChooseAction = false; mMoveNow = true; } else { // Play idle animation and recreate vanilla (broken?) behavior of resetting start time of AIWander: MWWorld::TimeStamp currentTime = world->getTimeStamp(); mStartTime = currentTime; playIdle(actor, mPlayedIdle); mChooseAction = false; mIdleNow = true; // Play idle voiced dialogue entries randomly int hello = cStats.getAiSetting(CreatureStats::AI_Hello).getModified(); if (hello > 0) { int roll = std::rand()/ (static_cast (RAND_MAX) + 1) * 100; // [0, 99] MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); // Don't bother if the player is out of hearing range if (roll < mChance && Ogre::Vector3(player.getRefData().getPosition().pos).squaredDistance(Ogre::Vector3(pos.pos)) < 1500*1500) MWBase::Environment::get().getDialogueManager()->say(actor, "idle"); } } } // Allow interrupting a walking actor to trigger a greeting if(mIdleNow || (mWalking && !mObstacleCheck.isNormalState() && mDistance)) { // Play a random voice greeting if the player gets too close int hello = cStats.getAiSetting(CreatureStats::AI_Hello).getModified(); float helloDistance = hello; helloDistance *= mGreetDistanceMultiplier; MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); Ogre::Vector3 playerPos(player.getRefData().getPosition().pos); Ogre::Vector3 actorPos(actor.getRefData().getPosition().pos); float playerDistSqr = playerPos.squaredDistance(actorPos); if(playerDistSqr <= helloDistance*helloDistance) { if(mWalking) { stopWalking(actor); mMoveNow = false; mWalking = false; mObstacleCheck.clear(); mIdleNow = true; getRandomIdle(); } if(!mRotate) { Ogre::Vector3 dir = playerPos - actorPos; float length = dir.length(); float faceAngle = Ogre::Radian(Ogre::Math::ACos(dir.y / length) * ((Ogre::Math::ASin(dir.x / length).valueRadians()>0)?1.0:-1.0)).valueDegrees(); float actorAngle = actor.getRefData().getBaseNode()->getOrientation().getRoll().valueDegrees(); // an attempt at reducing the turning animation glitch if(abs(abs(faceAngle) - abs(actorAngle)) >= 5) // TODO: is there a better way? { mTargetAngle = faceAngle; mRotate = true; } } } if (!mSaidGreeting) { // TODO: check if actor is aware / has line of sight if (playerDistSqr <= helloDistance*helloDistance // Only play a greeting if the player is not moving && Ogre::Vector3(player.getClass().getMovementSettings(player).mPosition).squaredLength() == 0) { mSaidGreeting = true; MWBase::Environment::get().getDialogueManager()->say(actor, "hello"); } } else { if (playerDistSqr >= mGreetDistanceReset*mGreetDistanceReset * mGreetDistanceMultiplier*mGreetDistanceMultiplier) mSaidGreeting = false; } // Check if idle animation finished // FIXME: don't stay forever if(!checkIdle(actor, mPlayedIdle) && playerDistSqr > helloDistance*helloDistance) { mPlayedIdle = 0; mIdleNow = false; mChooseAction = true; } } if(mMoveNow && mDistance) { // Construct a new path if there isn't one if(!mPathFinder.isPathConstructed()) { assert(mAllowedNodes.size()); unsigned int randNode = (int)(rand() / ((double)RAND_MAX + 1) * mAllowedNodes.size()); // NOTE: initially constructed with local (i.e. cell) co-ordinates Ogre::Vector3 destNodePos(mAllowedNodes[randNode].mX, mAllowedNodes[randNode].mY, mAllowedNodes[randNode].mZ); // convert dest to use world co-ordinates ESM::Pathgrid::Point dest; dest.mX = destNodePos[0] + mXCell; dest.mY = destNodePos[1] + mYCell; dest.mZ = destNodePos[2]; // actor position is already in world co-ordinates ESM::Pathgrid::Point start; start.mX = pos.pos[0]; start.mY = pos.pos[1]; start.mZ = pos.pos[2]; // don't take shortcuts for wandering mPathFinder.buildPath(start, dest, actor.getCell(), false); if(mPathFinder.isPathConstructed()) { // buildPath inserts dest in case it is not a pathgraph point // index which is a duplicate for AiWander. However below code // does not work since getPath() returns a copy of path not a // reference //if(mPathFinder.getPathSize() > 1) //mPathFinder.getPath().pop_back(); // Remove this node as an option and add back the previously used node (stops NPC from picking the same node): ESM::Pathgrid::Point temp = mAllowedNodes[randNode]; mAllowedNodes.erase(mAllowedNodes.begin() + randNode); // check if mCurrentNode was taken out of mAllowedNodes if(mTrimCurrentNode && mAllowedNodes.size() > 1) mTrimCurrentNode = false; else mAllowedNodes.push_back(mCurrentNode); mCurrentNode = temp; mMoveNow = false; mWalking = true; } // Choose a different node and delete this one from possible nodes because it is uncreachable: else mAllowedNodes.erase(mAllowedNodes.begin() + randNode); } } // Are we there yet? if(mWalking && mPathFinder.checkPathCompleted(pos.pos[0], pos.pos[1], pos.pos[2])) { stopWalking(actor); mMoveNow = false; mWalking = false; mChooseAction = true; mHasReturnPosition = false; } return false; // AiWander package not yet completed } void AiWander::trimAllowedNodes(std::vector& nodes, const PathFinder& pathfinder) { // TODO: how to add these back in once the door opens? // Idea: keep a list of detected closed doors (see aicombat.cpp) // Every now and then check whether one of the doors is opened. (maybe // at the end of playing idle?) If the door is opened then re-calculate // allowed nodes starting from the spawn point. std::list paths = pathfinder.getPath(); while(paths.size() >= 2) { ESM::Pathgrid::Point pt = paths.back(); for(unsigned int j = 0; j < nodes.size(); j++) { // FIXME: doesn't hadle a door with the same X/Y // co-ordinates but with a different Z if(nodes[j].mX == pt.mX && nodes[j].mY == pt.mY) { nodes.erase(nodes.begin() + j); break; } } paths.pop_back(); } } int AiWander::getTypeId() const { return TypeIdWander; } void AiWander::stopWalking(const MWWorld::Ptr& actor) { mPathFinder.clearPath(); actor.getClass().getMovementSettings(actor).mPosition[1] = 0; } void AiWander::playIdle(const MWWorld::Ptr& actor, unsigned short idleSelect) { if(idleSelect == 2) MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle2", 0, 1); else if(idleSelect == 3) MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle3", 0, 1); else if(idleSelect == 4) MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle4", 0, 1); else if(idleSelect == 5) MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle5", 0, 1); else if(idleSelect == 6) MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle6", 0, 1); else if(idleSelect == 7) MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle7", 0, 1); else if(idleSelect == 8) MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle8", 0, 1); else if(idleSelect == 9) MWBase::Environment::get().getMechanicsManager()->playAnimationGroup(actor, "idle9", 0, 1); } bool AiWander::checkIdle(const MWWorld::Ptr& actor, unsigned short idleSelect) { if(idleSelect == 2) return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, "idle2"); else if(idleSelect == 3) return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, "idle3"); else if(idleSelect == 4) return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, "idle4"); else if(idleSelect == 5) return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, "idle5"); else if(idleSelect == 6) return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, "idle6"); else if(idleSelect == 7) return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, "idle7"); else if(idleSelect == 8) return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, "idle8"); else if(idleSelect == 9) return MWBase::Environment::get().getMechanicsManager()->checkAnimationPlaying(actor, "idle9"); else return false; } void AiWander::setReturnPosition(const Ogre::Vector3& position) { if (!mHasReturnPosition) { mHasReturnPosition = true; mReturnPosition = position; } } void AiWander::getRandomIdle() { unsigned short idleRoll = 0; for(unsigned int counter = 0; counter < mIdle.size(); counter++) { unsigned short idleChance = mIdleChanceMultiplier * mIdle[counter]; unsigned short randSelect = (int)(rand() / ((double)RAND_MAX + 1) * int(100 / mIdleChanceMultiplier)); if(randSelect < idleChance && randSelect > idleRoll) { mPlayedIdle = counter+2; idleRoll = randSelect; } } } }