#include "aiwander.hpp" #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) , mSaidGreeting(false) { 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; //mPathgrid = NULL; mIdleChanceMultiplier = MWBase::Environment::get().getWorld()->getStore().get().find("fIdleChanceMultiplier")->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. Note also many of * the actions now have reaction times. * * [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) | * +----------------------------------+ * * TODO: non-time critical operations should be 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 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. Normally this would not be an issue since hostile actors will * enter combat (i.e. no longer wandering) */ 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); 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; } } ESM::Position pos = actor.getRefData().getPosition(); // 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; } #if 0 if(actor.getClass().getName(actor) == "Rat") { std::cout << "rat allowed "<< std::to_string(mAllowedNodes.size()) +" mDist "+std::to_string(mDistance) +" pos "+std::to_string(static_cast(npcPos[0])) +", "+std::to_string(static_cast(npcPos[1])) < idleRoll) { mPlayedIdle = counter+2; idleRoll = randSelect; } } 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) { const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); float chance = store.get().find("fVoiceIdleOdds")->getFloat(); 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 < chance && Ogre::Vector3(player.getRefData().getPosition().pos).distance(Ogre::Vector3(pos.pos)) < 1500) MWBase::Environment::get().getDialogueManager()->say(actor, "idle"); } } } // 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)) // NOTE: checks interior cells only { mIdleNow = false; mMoveNow = true; mTrimCurrentNode = false; // just in case //#if 0 std::cout << "idle door \""+actor.getClass().getName(actor)+"\" "<< std::endl; //#endif } } // Allow interrupting a walking actor to trigger a greeting if(mIdleNow || (mWalking && !mObstacleCheck.isNormalState())) { // Play a random voice greeting if the player gets too close const MWWorld::ESMStore &store = MWBase::Environment::get().getWorld()->getStore(); int hello = cStats.getAiSetting(CreatureStats::AI_Hello).getModified(); float helloDistance = hello; int iGreetDistanceMultiplier = store.get().find("iGreetDistanceMultiplier")->getInt(); helloDistance *= iGreetDistanceMultiplier; MWWorld::Ptr player = MWBase::Environment::get().getWorld()->getPlayerPtr(); float playerDist = Ogre::Vector3(player.getRefData().getPosition().pos).distance( Ogre::Vector3(actor.getRefData().getPosition().pos)); if(mWalking && playerDist <= helloDistance) { stopWalking(actor); mMoveNow = false; mWalking = false; mObstacleCheck.clear(); } if (!mSaidGreeting) { // TODO: check if actor is aware / has line of sight if (playerDist <= 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"); // TODO: turn to face player and interrupt the idle animation? } } else { float fGreetDistanceReset = store.get().find("fGreetDistanceReset")->getFloat(); if (playerDist >= fGreetDistanceReset * iGreetDistanceMultiplier) mSaidGreeting = false; } // Check if idle animation finished if(!checkIdle(actor, mPlayedIdle)) { 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; //#if 0 std::cout << "deleted "<< std::to_string(mCurrentNode.mX) +", "+std::to_string(mCurrentNode.mY) << std::endl; //#endif //#if 0 std::cout << "allowed size "<< std::to_string(mAllowedNodes.size()) << std::endl; //#endif } 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); //#if 0 //std::cout << "actor \""<< actor.getClass().getName(actor) << "\"" << std::endl; if(actor.getClass().getName(actor) == "Rat") std::cout << "erase no path "<< std::to_string(mAllowedNodes[randNode].mX) +", "+std::to_string(mAllowedNodes[randNode].mY) << std::endl; //#endif } } } // 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; } else 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 } 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? std::list paths = pathfinder.getPath(); while(paths.size() >= 2) { ESM::Pathgrid::Point pt = paths.back(); #if 0 std::cout << "looking for "<< "pt "+std::to_string(pt.mX)+", "+std::to_string(pt.mY) <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; } }