//----------------------------------------------------------------------------- // V12 Engine // // Copyright (c) 2001 GarageGames.Com // Portions Copyright (c) 2001 by Sierra Online, Inc. //----------------------------------------------------------------------------- #include "core/realComp.h" #include "math/mMatrix.h" #include "console/console.h" #include "game/gameBase.h" #include "ai/aiConnection.h" #include "ai/aiStep.h" #include "ai/aiNavStep.h" #include "ai/aiTask.h" #include "ai/graphMath.h" #include "terrain/waterBlock.h" #include "scenegraph/sceneGraph.h" #include "game/vehicle.h" #include "platform/profiler.h" IMPLEMENT_CONOBJECT(AIConnection); static S32 gAIDetectionOffset = 0; AIConnection::AIConnection() { mAIControlled = true; mMoveMode = mMoveModePending = ModeStop; mMoveLocation.set(0, 0, 0); mNodeLocation.set(0, 0, 0); mAimLocation.set(0, 0, 0); mMoveSpeed = 0.0f; mMoveTolerance = 0.25f; mStep = NULL; mCurrentTask = NULL; mCurrentTaskTime = 0; mPathDest.set(0, 0, 0); mNewPath = false; //clear the triggers for (int i = 0; i < MaxTriggerKeys; i++) mTriggers[i] = false; mPrevNodeLocation.set(0, 0, 0); mInitialLocation.set(0, 0, 0); mEnergyReserve = 0.0f; mEnergyFloat = 0.10f; mEnergyRecharge = false; //init engage vars mEngageState = ChooseWeapon; mProjectileName = StringTable->insert(""); mProjectile = NULL; mEngageMinDistance = 20; mEngageMaxDistance = 75; mDistToTarg2D = -1.0f; mLookAtTargetTimeMS = 0; mFiring = false; mTriggerCounter = 0; mScriptTriggerCounter = -1; mVictimTime = 0; mSkillLevel = 0.5; mChangeWeaponCounter = 0; mTurretMountedId = 0; mMountedImage = NULL; //piloting vars mPilotDestination.set(0, 0, 0); mPilotAimLocation.set(0, 0, 0); mPilotSpeed = 1.0f; mPitchUpMax = -0.25; mPitchDownMax = 0.1f; mPitchIncMax = 0.05f; mCurrentPitch = 0; mPreviousPitch = 0; mDesiredPitch = 0; mPitchIncrement = 0; mTargStillTimeMS = 0; mTargPrevTimeMS = 0; mTargPrevLocation[0].set(0, 0, 0); mTargPrevLocation[1].set(0, 0, 0); mTargPrevLocation[2].set(0, 0, 0); mTargPrevLocation[3].set(0, 0, 0); mPlayerDetectionIndex = 0; mPlayerDetectionCounter = gAIDetectionOffset; gAIDetectionOffset++; if (gAIDetectionOffset >= 3) gAIDetectionOffset = 0; mDetectHiddenPeriod = 6000; mBlindedTimer = 0; //init target object vars mTargetObject = NULL; mObjectMode = DestroyObject; mDistToObject2D = -1.0f; //these are used for both mEngageTarget and mTargetObject mRangeToTarget = 30; mCheckTargetLOSCounter = 0; mTargetInRange = false; mTargetInSight = false; mWeaponEnergy = 0.0f; mWeaponErrorFactor = 1.0f; //init evading vars mEnemyProjectile = NULL; mEvadingCounter = 0; mIsEvading = false; mAvoidingObject = NULL; mStuckInitialized = false; // LH. Just for the heck of it (and as way to familiarize with code) - added in all // these that weren't being constructed. mMoveDestination.set(0, 0, 0); mEvadeLocation.set(0, 0, 0); mImpactLocation.set(0, 0, 0); mCorpseLocation.set(0, 0, 0); mStateCounter = 0; mDelayCounter = 0; mPackCheckCounter = 0; mAimAtLazedTarget = false; mProjectileCounter = 0; mPath.setTeam(getSensorGroup()); mStuckDestination.set(0,0,0); mTargetPlayer = NULL; mLocation.set(0,0,0); mVelocity = mVelocity2D = mRotation = mHeadRotation = mMuzzlePosition = mEyePosition = mTargLocation = mTargVelocity = mTargVelocity2D = mTargRotation = mLocation; mTargEnergy = 1.0; mTargDamage = 0; } AIConnection::~AIConnection() { } void AIConnection::setSkillLevel(F32 level) { mSkillLevel = getMax(0.0f, getMin(1.0f, level)); } void AIConnection::setMoveSpeed(F32 speed) { if (speed <= 0.0f) mMoveSpeed = 0.0f; else mMoveSpeed = getMin(1.0f, speed); } void AIConnection::setMoveMode(S32 mode, bool abortStuckCode) { if (mode < 0 || mode >= ModeCount) mode = 0; //if we're setting mode::express and we're currently stuck, let the stuck code //finish "unsticking" the bot... calling setMoveDestination will abort the stuck code if (mMoveMode == ModeStuck && mode != ModeStop && !abortStuckCode) return; //make sure we're not moving if in the middle of a jet... mMoveModePending = mode; if (!mNavUsingJet) mMoveMode = mode; } void AIConnection::setMoveTolerance(F32 tolerance) { mMoveTolerance = getMax(0.1f, tolerance); } void AIConnection::setMoveDestination(const Point3F &location) { if (mMoveDestination != location && mMoveMode == ModeStuck) { setMoveMode(ModeExpress, true); mStuckLocation.set(0, 0, 0); } mMoveDestination = location; } void AIConnection::setMoveLocation(const Point3F &location) { mMoveLocation = location; } void AIConnection::setAimLocation(const Point3F &location) { mAimLocation = location; } bool AIConnection::setScriptAimLocation(const Point3F &location, S32 duration) { //can't set through scripts if the bots are aiming at either an object, or an engage player if (mTargetPlayer || bool(mTargetObject)) return false; setAimLocation(location); mLookAtTargetTimeMS = getMax(mLookAtTargetTimeMS, (S32)Sim::getCurrentTime() + duration); return true; } void AIConnection::setPilotPitchRange(F32 pitchUpMax, F32 pitchDownMax, F32 pitchIncMax) { mPitchUpMax = pitchUpMax; mPitchDownMax = pitchDownMax; mPitchIncMax = pitchIncMax; } void AIConnection::setPilotDestination(const Point3F &dest, F32 maxSpeed) { //must always aim where you're flying to mPilotDestination = dest; mPilotAimLocation = dest; mPilotSpeed = getMax(0.0f, getMin(1.0f, maxSpeed)); } void AIConnection::setPilotAimLocation(const Point3F &aimLocation) { //if you're aiming, you can't move mPilotAimLocation = aimLocation; mPilotSpeed = 0; } void AIConnection::setWeaponInfo(const char *projectile, S32 minDist, S32 maxDist, S32 triggerCount, F32 energyRequired, F32 errorFactor) { //set the non-pointer params mEngageMinDistance = minDist; mEngageMaxDistance = maxDist; mTriggerCounter = triggerCount; mScriptTriggerCounter = -1; mWeaponEnergy = energyRequired; mWeaponErrorFactor = errorFactor; if ((! projectile) || (! projectile[0]) || (! dStricmp(projectile, "NoAmmo"))) mProjectile = NULL; else { if (! Sim::findObject(projectile, mProjectile)) { Con::printf("setWeaponInfo() failed - unable to find datablock: %s", projectile); mProjectile = NULL; } } if (mProjectile) mProjectileName = StringTable->insert(projectile); else mProjectileName = StringTable->insert(""); } void AIConnection::setEnergyLevels(F32 eReserve, F32 eFloat) { mEnergyReserve = eReserve; mEnergyFloat = eFloat; } void AIConnection::setEngageTarget(GameConnection *target) { //reset the bools if we're aiming at someone new... //note, these extraneous retarded checks are because mTargetObject is a SimObjectPtr... //won't compile just comparing target != mEngageTarget... if (target != NULL && ((! bool(mEngageTarget)) || (target->getId() != mEngageTarget->getId()))) { //reset some engagement vars mTargetInRange = false; mTargetInSight = false; mEngageState = ChooseWeapon; mFiring = false; mTriggerCounter = 0; mScriptTriggerCounter = -1; } mEngageTarget = target; mTargetPlayer = NULL; if ((! bool(mEngageTarget)) || (! mEngageTarget->getControlObject())) mEngageTarget = NULL; else mTargetPlayer = dynamic_cast(mEngageTarget->getControlObject()); //make sure we actually got a target if (! mTargetPlayer) mEngageTarget = NULL; } S32 AIConnection::getEngageTarget() { //see if have someone to shoot at Player *targetPlayer = NULL; if ((! bool(mEngageTarget)) || (! mEngageTarget->getControlObject())) { mTargetPlayer = NULL; mEngageTarget = NULL; return -1; } else targetPlayer = dynamic_cast(mEngageTarget->getControlObject()); //see if the target is dead if (! targetPlayer || ! dStricmp(targetPlayer->getStateName(), "dead")) { mTargetPlayer = NULL; mEngageTarget = NULL; return -1; } //return the id of the target return mEngageTarget->getId(); } void AIConnection::setVictim(GameConnection *victim, Player *corpse) { mVictim = victim; mCorpse = corpse; mVictimTime = Sim::getCurrentTime(); } S32 AIConnection::getVictimCorpse() { if (bool(mCorpse)) return mCorpse->getId(); else return -1; } S32 AIConnection::getVictimTime() { return mVictimTime; } void AIConnection::setTargetObject(ShapeBase *targetObject, F32 range, S32 objectMode) { //reset the bools if we're not aiming at a player, and we have a new target object //note, these extraneous retarded checks are because mTargetObject is a SimObjectPtr... //won't compile just comparing targetObject != mTargetObject... if (! mTargetPlayer && (! targetObject || ! bool(mTargetObject) || targetObject->getId() != mTargetObject->getId())) { mTargetInRange = false; mTargetInSight = false; mEngageState = ChooseWeapon; mFiring = false; mTriggerCounter = 0; mScriptTriggerCounter = -1; } mTargetObject = targetObject; mRangeToTarget = range; mObjectMode = objectMode; } S32 AIConnection::getTargetObject() { if (bool(mTargetObject)) return mTargetObject->getId(); else return -1; } void AIConnection::setPathDest(const Point3F * dest) { if( dest ) mPathDest = * dest; mNewPath = true; } const Point3F * AIConnection::getPathDest() { if( mNewPath ){ mNewPath = false; return & mPathDest; } return NULL; } // Pass down jetting abilities to the path machinery- void AIConnection::setPathCapabilities(Player * ) { PROFILE_START(AI_setPathCapabilities); JetManager::Ability ability; // Tribes player jetting was remove from the player class. #if 0 player->getJetAbility(ability.acc, ability.dur, ability.v0); #else ability.acc = 0; ability.dur = 0; ability.v0 = 0; #endif mPath.setJetAbility(ability); PROFILE_END(); } F32 AIConnection::getPathDistance(const Point3F &destination, const Point3F &source) { //find our current location Point3F sourceLocation = source; if (sourceLocation == Point3F(-1, -1, -1)) { //make sure we have a valid player control object Player *myPlayer = NULL; if (! getControlObject()) return -1; myPlayer = dynamic_cast(getControlObject()); if (! myPlayer) return -1; setPathCapabilities(myPlayer); MatrixF const& tempTransform = myPlayer->getTransform(); tempTransform.getColumn(3, &sourceLocation); //make sure the client can actually get to the destination if (! mPath.canReachLoc(destination)) return -1; } //this is a weird change - want the bots to avoid vertical movement, so the distance function will //exaggerate the z component... Point3F distVec = sourceLocation - destination; return mSqrt(distVec.x * distVec.x + distVec.y * distVec.y + 9 * distVec.z * distVec.z); //this is just a euclidean distance anyways... if it becomes an actual path distance again, use it... //return NavigationGraph::fastDistance(sourceLocation, destination); } F32 AIConnection::getPathDistRemaining(F32 maxDist) { //if the path is current, use the path distRemaining function if (mPath.isPathCurrent()) return mPath.distRemaining(maxDist); //otherwise, return the euclidean dist else return getMin(maxDist, (mLocation - mMoveDestination).len()); } Point3F AIConnection::getLOSLocation(const Point3F &targetPoint, F32 minDistance, F32 maxDistance, const Point3F &nearPoint) { //find our current location Point3F sourceLocation; Player *myPlayer = NULL; if (! getControlObject()) return targetPoint; myPlayer = dynamic_cast(getControlObject()); if (! myPlayer) return targetPoint; MatrixF const& tempTransform = myPlayer->getTransform(); tempTransform.getColumn(3, &sourceLocation); Point3F nearLocation = nearPoint; if (nearLocation == Point3F(-1, -1, -1)) nearLocation = sourceLocation; Point3F graphPoint; if (minDistance < 0) graphPoint = NavigationGraph::findLOSLocation(sourceLocation, targetPoint, 0, SphereF(nearLocation, 1e6), maxDistance); else graphPoint = NavigationGraph::findLOSLocation(sourceLocation, targetPoint, minDistance, SphereF(nearLocation, minDistance / 2.0f), maxDistance); return graphPoint; } Point3F AIConnection::getHideLocation(const Point3F &targetPoint, F32 range, const Point3F &nearPoint, F32 hideLength) { //find our current location Point3F sourceLocation = nearPoint; if (sourceLocation == Point3F(-1, -1, -1)) { Player *myPlayer = NULL; if (! getControlObject()) return targetPoint; myPlayer = dynamic_cast(getControlObject()); if (! myPlayer) return targetPoint; MatrixF const& tempTransform = myPlayer->getTransform(); tempTransform.getColumn(3, &sourceLocation); } if (hideLength <= 0) return NavigationGraph::hideOnSlope(sourceLocation, targetPoint, range, 20); else return NavigationGraph::hideOnDistance(sourceLocation, targetPoint, range, hideLength); } void AIConnection::process(ShapeBase *ctrlObject) { if (!ctrlObject) return; PROFILE_START(AI_process); //update the task queue AITask *highestWeightTask = NULL; S32 i; for (i = 0; i < mTaskList.size(); i++) { AITask *task = mTaskList[i]; task->calcWeight(this); //see if it's the highest so far if ((! highestWeightTask) || (task->getWeight() > highestWeightTask->getWeight())) highestWeightTask = task; } // Path needs team for avoiding threats- mPath.setTeam(getSensorGroup()); //now see if we have a new task if (highestWeightTask && mCurrentTask != highestWeightTask) { if (bool(mCurrentTask)) mCurrentTask->retire(this); highestWeightTask->assume(this); mCurrentTask = highestWeightTask; mCurrentTaskTime = Sim::getCurrentTime(); } //monitor the current task if (mCurrentTask) { PROFILE_START(AI_MonitorTask); mCurrentTask->monitor(this); PROFILE_END(); } //see if our control object is a player Player *myPlayer = NULL; myPlayer = dynamic_cast(ctrlObject); if (myPlayer) { //initialize the process vars initProcessVars(myPlayer); //next, process the current step if (bool(mStep)) mStep->process(this, myPlayer); //process the engagement if (mTurretMountedId <= 0) { PROFILE_START(AI_EngagementOuter); processEngagement(myPlayer); PROFILE_END(); } //finally, process the movement itself if (!myPlayer->isMounted()) processMovement(myPlayer); else processVehicleMovement(myPlayer); } //else see if we're trying to pilot a vehicle else { Vehicle *myVehicle; myVehicle = dynamic_cast(ctrlObject); if (myVehicle) { processPilotVehicle(myVehicle); } } //debug aiDebugStuff(); PROFILE_END(); } // LH- changed this to not normalize when there's a zero length. Dividing by zero // results in interupts. Instead do normalization here checking lengths first. F32 AIConnection::get2DDot(const Point3F &vec1, const Point3F &vec2) { //create the 2D vectors and check for zero length. Point3F vec1_2D(vec1.x, vec1.y, 0); F32 len1 = vec1_2D.len(); if (len1 < __EQUAL_CONST_F) return 1; Point3F vec2_2D(vec2.x, vec2.y, 0); F32 len2 = vec2_2D.len(); if (len2 < __EQUAL_CONST_F) return 1; // Normalize and return the dot- return mDot(vec1_2D /= len1, vec2_2D /= len2); } F32 AIConnection::get2DAngle(const Point3F &endPt, const Point3F &basePt) { Point3F direction2D = endPt - basePt; F32 angularDirection; direction2D.z = 0; if (! isZero(direction2D.y)) angularDirection = mAtan(direction2D.x, direction2D.y); else if (direction2D.x < 0) angularDirection = -M_PI / 2.0f; else angularDirection = M_PI / 2.0f; if (angularDirection < 0) angularDirection += M_2PI; else if (angularDirection >= M_2PI) angularDirection -= M_2PI; //note: return value is always between 0 and (2 * Pi) return angularDirection; } Point3F AIConnection::dopeAimLocation(const Point3F &startLocation, const Point3F &aimLocation) { //find the "horizontal" orthogonal vector Point3F horzOrth; if (! findCrossVector(startLocation - aimLocation, Point3F(0, 0, 1), &horzOrth)) return aimLocation; //now find the "vertical" orthogonal vector Point3F vertOrth; if (! findCrossVector(startLocation - aimLocation, horzOrth, &vertOrth)) return aimLocation; //now we have the horizonal and vertical components of the plane which is perpendicular to //the direction we are firing... //determine the radius factor F32 radiusFactor; if (mSkillLevel == 1.0f) radiusFactor = 0.0f; else radiusFactor = 0.04 + (1.0f - mSkillLevel) * 0.2; //calculate the radius error F32 radiusError = (startLocation - aimLocation).len() * radiusFactor * mWeaponErrorFactor; if (! mProjectile->isBallistic && mTargStillTimeMS > 0 && radiusError > 0) { //begin honing in after 3 seconds, and over the next 3 seconds S32 elapsedTime = Sim::getCurrentTime() - mTargStillTimeMS - 3000; if (elapsedTime > 0) radiusError = radiusError * (F32(3000 - getMin(elapsedTime, 3000)) / 3000.0f); } F32 horzError = gRandGen.randF() * radiusError * (gRandGen.randF() < 0.5f ? -1.0f : 1.0f); F32 vertError = gRandGen.randF() * radiusError * (gRandGen.randF() < 0.5f ? -1.0f : 1.0f); Point3F dopedAimLocation = aimLocation + (horzOrth * horzError) + (vertOrth * vertError); return dopedAimLocation; } F32 AIConnection::getOutdoorRadius(const Point3F &location) { F32 freedomRadius; if (mPath.locationIsOutdoors(location, &freedomRadius)) return freedomRadius; else return -1; } void AIConnection::initProcessVars(Player *player) { PROFILE_START(AI_initProcessVars); //find my current location (global coords), velocity, energy, damage, etc... MatrixF const& tempTransform = player->getTransform(); tempTransform.getColumn(3, &mLocation); mVelocity = player->getVelocity(); mVelocity2D = mVelocity; mVelocity2D.z = 0; mRotation = player->getRotation(); mHeadRotation = player->getHeadRotation(); mEnergy = player->getEnergyValue(); mDamage = player->getDamageValue(); if (mEnergy < mEnergyReserve) mEnergyRecharge = true; else if (mEnergy > mEnergyReserve + mEnergyFloat) mEnergyRecharge = false; if (((mEnergy > mEnergyReserve + mEnergyFloat) || (mEnergy > mEnergyReserve && !mEnergyRecharge)) && (mEnergy > mWeaponEnergy - mEnergyFloat)) mEnergyAvailable = true; else mEnergyAvailable = false; //find the muzzle point //player->getMuzzlePoint(0, &mMuzzlePosition); player->getMuzzlePointAI(0, &mMuzzlePosition); //find the eye transform MatrixF eyeTransform; player->getEyeTransform(&eyeTransform); eyeTransform.getColumn(3, &mEyePosition); //find out far we have to go, and if we're heading in the right direction mDistToNode2D = (Point3F(mNodeLocation.x, mNodeLocation.y, 0) - Point3F(mLocation.x, mLocation.y, 0)).len(); //find out how far off course our velocity is taking us mDotOffCourse = get2DDot(mNodeLocation - mLocation, mVelocity2D); mDotOffCourse = mClampF(mDotOffCourse, -1.0f, 1.0f); //find out the difference between them F32 dummy; bool outdoors = mPath.locationIsOutdoors(mLocation, &dummy); mHeadingDownhill = false; if (mVelocity2D.len() >= player->getMaxForwardVelocity() * 0.85f && outdoors && !mInWater) { //make sure we're heading in approximately the right direction (+- 30 deg or so) if (mDotOffCourse > 0.85) { Point3F myDirection2D = mVelocity2D; if (myDirection2D.len() < 0.001f) myDirection2D.set(0, 1, 0); myDirection2D.normalize(); U32 mask = TerrainObjectType | InteriorObjectType | WaterObjectType; RayInfo ray1Info, ray2Info; Point3F startPt = mLocation; Point3F endPt = mLocation; endPt.z -= 5.0f; //if we're not within 1.0 m of the ground, we can't ski... player->disableCollision(); if (! gServerContainer.castRay(startPt, endPt, mask, &ray1Info)) mHeadingDownhill = true; else { //run another LOS to see if we are heading downhill startPt += myDirection2D; endPt += myDirection2D; //if we don't hit anything, we're (on the edge of a cliff?) heading downhill if (! gServerContainer.castRay(startPt, endPt, mask, &ray2Info)) mHeadingDownhill = true; else if (ray1Info.point.z > ray2Info.point.z) mHeadingDownhill = true; } player->enableCollision(); } } //see if we have a target object if (bool(mTargetObject)) { MatrixF const& objTransform = mTargetObject->getTransform(); objTransform.getColumn(3, &mObjectLocation); mDistToObject2D = (Point3F(mLocation.x, mLocation.y, 0.0f) - Point3F(mObjectLocation.x, mObjectLocation.y, 0.0f)).len(); } //see if have someone to shoot at mTargetPlayer = NULL; if ((! bool(mEngageTarget)) || (! mEngageTarget->getControlObject())) mEngageTarget = NULL; else mTargetPlayer = dynamic_cast(mEngageTarget->getControlObject()); if (mTargetPlayer) { //find the target location (global coords), velocity, energy, damage, etc... MatrixF const& targTransform = mTargetPlayer->getTransform(); targTransform.getColumn(3, &mTargLocation); mTargVelocity = mTargetPlayer->getVelocity(); mTargVelocity2D = mTargVelocity; mTargVelocity2D.z = 0; mTargRotation = mTargetPlayer->getRotation(); mTargEnergy = mTargetPlayer->getEnergyValue(); mTargDamage = mTargetPlayer->getDamageValue(); //see if the target is standing still if (mTargVelocity.len() > 4.0f) mTargStillTimeMS = 0; else if (mTargStillTimeMS == 0) mTargStillTimeMS = Sim::getCurrentTime(); //keep an array of prev locations to simulate a slower response time... (up to 600 ms) if (Sim::getCurrentTime() - mTargPrevTimeMS > 300) { mTargPrevTimeMS = Sim::getCurrentTime(); mTargPrevLocation[3] = mTargPrevLocation[2]; mTargPrevLocation[2] = mTargPrevLocation[1]; mTargPrevLocation[1] = mTargPrevLocation[0]; mTargetPlayer->getWorldBox().getCenter(&mTargPrevLocation[0]); } } else mEngageTarget = NULL; if (mTargetPlayer) { //see if the target is dead if (! dStricmp(mTargetPlayer->getStateName(), "dead")) { mTargetPlayer = NULL; mEngageTarget = NULL; } else { //find the 2D distance between you and the target Point3F dist2DVector = mTargLocation - mLocation; dist2DVector.z = 0; mDistToTarg2D = dist2DVector.len(); } } //see if we're outdoors mOutdoors = (getOutdoorRadius(mLocation) > 0); //see if either we or the targ is in water mInWater = false; mTargInWater = false; mObjectInWater = false; SimpleQueryList sql; gServerSceneGraph->getWaterObjectList(sql); // gServerContainer.findObjects(WaterObjectType, SimpleQueryList::insertionCallback, S32(&sql)); for (U32 i = 0; i < sql.mList.size(); i++) { WaterBlock* pBlock = dynamic_cast(sql.mList[i]); if (pBlock) { if (pBlock->isPointSubmergedSimple(mLocation) || pBlock->isPointSubmergedSimple(mMuzzlePosition)) mInWater = true; if (mTargetPlayer) { if (pBlock->isPointSubmergedSimple(mTargLocation)) mTargInWater = true; } if (bool(mTargetObject)) { if (pBlock->isPointSubmergedSimple(mObjectLocation)) mObjectInWater = true; } } } //now see if we're within range of either the engageTarget, or the targetObject if (--mCheckTargetLOSCounter <= 0) { mCheckTargetLOSCounter = 10; mTargetInSight = false; if (mTargetPlayer || bool(mTargetObject)) { F32 rangeDist; Point3F rangeLocation; if (mTargetPlayer) { rangeDist = mDistToTarg2D; mTargetPlayer->getWorldBox().getCenter(&rangeLocation); } else { rangeDist = mDistToObject2D; mTargetObject->getWorldBox().getCenter(&rangeLocation); } if (rangeDist <= mRangeToTarget) { //see if we have line of site RayInfo rayInfo; Point3F startPt = mMuzzlePosition; Point3F endPt = rangeLocation; U32 mask = TerrainObjectType | InteriorObjectType; if (! gServerContainer.castRay(startPt, endPt, mask, &rayInfo)) mTargetInSight = true; } } } //reset the weaponEnergy level if req'd if (! bool(mTargetPlayer)) mWeaponEnergy = 0.0f; //set the corpse vars if (bool(mCorpse)) { MatrixF const& corpseTransform = mCorpse->getTransform(); corpseTransform.getColumn(3, &mCorpseLocation); } PROFILE_END(); } void AIConnection::updateDetectionTable(Player *player) { //if the player has been blinded, no updates to the table can be made if (Sim::getCurrentTime() < mBlindedTimer) return; //time slice the detection LOS calls... mPlayerDetectionCounter--; if (mPlayerDetectionCounter <= 0) { SimGroup *clientGroup = Sim::getClientGroup(); AssertFatal(clientGroup, "Unable to get the client group"); //make sure we have more than one client if (clientGroup->size() <= 1) return; //find the next client index mPlayerDetectionIndex++; if (mPlayerDetectionIndex >= clientGroup->size()) mPlayerDetectionIndex = 0; //get the client from the group GameConnection *targClient = static_cast((*clientGroup)[mPlayerDetectionIndex]); //make sure it's not me... S32 targClientId = targClient->getId(); if (getId() == targClientId) { mPlayerDetectionIndex++; if (mPlayerDetectionIndex >= clientGroup->size()) mPlayerDetectionIndex = 0; targClient = static_cast((*clientGroup)[mPlayerDetectionIndex]); targClientId = targClient->getId(); } //now find this target in the table PlayerDetectionEntry *targEntry = NULL; for (S32 i = 0; i < mPlayerDetectionTable.size(); i++) { if (mPlayerDetectionTable[i].playerId == targClientId) targEntry = &(mPlayerDetectionTable[i]); } //if the entry wasn't found, create one... (and yes, this table will never shrink) if (!targEntry) { PlayerDetectionEntry tempEntry; tempEntry.playerId = targClientId; tempEntry.playerLOS = false; tempEntry.playerLOSTime = 0; tempEntry.playerLastPosition.set(0, 0, 0); mPlayerDetectionTable.push_front(tempEntry); targEntry = &(mPlayerDetectionTable.first()); } //make sure the client has a player Player *targPlayer = NULL; if (! targClient->getControlObject()) { if (targEntry->playerLOS) { targEntry->playerLOS = false; targEntry->playerLOSTime = Sim::getCurrentTime(); } return; } targPlayer = dynamic_cast(targClient->getControlObject()); if (! targPlayer) { if (targEntry->playerLOS) { targEntry->playerLOS = false; targEntry->playerLOSTime = Sim::getCurrentTime(); } return; } //now cast line of sight, see if we can see the player MatrixF myEyeTransform, targEyeTransform; Point3F myEyePosition, targEyePosition; player->getEyeTransform(&myEyeTransform); myEyeTransform.getColumn(3, &myEyePosition); targPlayer->getEyeTransform(&targEyeTransform); targEyeTransform.getColumn(3, &targEyePosition); //if the target player is cloaked, they can't be detected bool targPlayerIsCloaked = targPlayer->getCloakedState(); Point3F losVector = targEyePosition - myEyePosition; F32 distToTarg = losVector.len(); bool clearLOSToTarg; if (mSkillLevel >= 1.0f) clearLOSToTarg = true; else if (distToTarg < 0.5f) clearLOSToTarg = true; else if (targPlayerIsCloaked) clearLOSToTarg = false; else if (distToTarg > 300.0f) clearLOSToTarg = false; else { //first, see if we're facing the right way... Point3F facingVector = mAimLocation - mLocation; F32 visibleRange = 0.4 - (0.4 * mSkillLevel); if (facingVector.len() < 0.5f) clearLOSToTarg = false; else if (get2DDot(facingVector, losVector) < visibleRange && distToTarg > 1.5f) clearLOSToTarg = false; else { //see if we have line of site if (losVector.len() < 0.001f) losVector.set(0, 1, 0); losVector.normalize(); RayInfo rayInfo; Point3F startPt = myEyePosition; Point3F endPt = myEyePosition + (getMin(300.0f, distToTarg) * losVector); U32 mask = TerrainObjectType | InteriorObjectType; if (! gServerContainer.castRay(startPt, endPt, mask, &rayInfo)) clearLOSToTarg = true; else clearLOSToTarg = false; } } //set the time if the status changed if (clearLOSToTarg != targEntry->playerLOS) targEntry->playerLOSTime = Sim::getCurrentTime(); //set the bool targEntry->playerLOS = clearLOSToTarg; //update the position if required if (clearLOSToTarg) targPlayer->getWorldBox().getCenter(&targEntry->playerLastPosition); //don't forget to set the timeslice counter mPlayerDetectionCounter = 30 / (clientGroup->size() - 1); if (mPlayerDetectionCounter < 3) mPlayerDetectionCounter = 3; } } void AIConnection::setBlinded(S32 duration) { //can't blind the Kidney Bot!!! if (mSkillLevel >= 1.0f) return; //first, set the blinded timer mBlindedTimer = Sim::getCurrentTime() + duration; //now loop through the table, and anyone who he has LOS to, he now doesn't PlayerDetectionEntry *targEntry = NULL; for (S32 i = 0; i < mPlayerDetectionTable.size(); i++) { targEntry = &(mPlayerDetectionTable[i]); if (targEntry->playerLOS) { targEntry->playerLOS = false; targEntry->playerLOSTime = Sim::getCurrentTime(); } } //next, set the evade location to make us react... Point3F dangerLocation = mLocation; dangerLocation.x += -2.0f + (gRandGen.randF() * 4.0f); dangerLocation.y += -2.0f + (gRandGen.randF() * 4.0f); setEvadeLocation(dangerLocation); mEvadingCounter = 30; } void AIConnection::clientDetected(S32 targId) { //first, make sure we're not detecting ourself if (getId() == targId) return; SimGroup *clientGroup = Sim::getClientGroup(); AssertFatal(clientGroup, "Unable to get the client group"); GameConnection *targClient = NULL; for (SimGroup::iterator itr = clientGroup->begin(); itr != clientGroup->end(); itr++) { GameConnection *client = static_cast(*itr); if (client->getId() == targId) { targClient = client; break; } } if (! targClient) return; //now find this target in the table PlayerDetectionEntry *targEntry = NULL; for (S32 i = 0; i < mPlayerDetectionTable.size(); i++) { if (mPlayerDetectionTable[i].playerId == targId) targEntry = &(mPlayerDetectionTable[i]); } //if the entry wasn't found, create one... (and yes, this table will never shrink) if (!targEntry) { PlayerDetectionEntry tempEntry; tempEntry.playerId = targId; tempEntry.playerLOS = false; tempEntry.playerLOSTime = 0; tempEntry.playerLastPosition.set(0, 0, 0); mPlayerDetectionTable.push_front(tempEntry); targEntry = &(mPlayerDetectionTable.first()); } //make sure the client has a player Player *targPlayer = NULL; if (! targClient->getControlObject()) return; targPlayer = dynamic_cast(targClient->getControlObject()); if (! targPlayer) return; //set the time if the status changed if (!targEntry->playerLOS) targEntry->playerLOSTime = Sim::getCurrentTime(); //set the bool targEntry->playerLOS = true; //update the position targPlayer->getWorldBox().getCenter(&targEntry->playerLastPosition); } bool AIConnection::hasLOSToClient(S32 clientId, S32 &losTime, Point3F &lastLocation) { //now find this target in the table PlayerDetectionEntry *targEntry = NULL; for (S32 i = 0; i < mPlayerDetectionTable.size(); i++) { if (mPlayerDetectionTable[i].playerId == clientId) targEntry = &(mPlayerDetectionTable[i]); } if (! targEntry) { losTime = Sim::getCurrentTime(); lastLocation.set(0, 0, 0); return false; } else { losTime = Sim::getCurrentTime() - targEntry->playerLOSTime; lastLocation = targEntry->playerLastPosition; return targEntry->playerLOS; } } // LH- put script calls into separate methods so profiler can measure them. // Then added slicing since it was weighing in on profiles. void AIConnection::scriptProcessEngagement() { if (!gScriptEngageSlicer.ready(mPackCheckCounter, 6)) return; char idStr[32], targetIdStr[32], targetTypeStr[32], projectileStr[64]; dSprintf(idStr, sizeof(idStr), "%d", getId()); if (mTargetPlayer) { dSprintf(targetTypeStr, sizeof(targetTypeStr), "%s", "player"); dSprintf(targetIdStr, sizeof(targetIdStr), "%d", mEngageTarget->getId()); } else if (bool(mTargetObject) && mObjectMode == AIConnection::DestroyObject) { dSprintf(targetTypeStr, sizeof(targetTypeStr), "%s", "object"); dSprintf(targetIdStr, sizeof(targetIdStr), "%d", mTargetObject->getId()); } else { dSprintf(targetTypeStr, sizeof(targetTypeStr), "%s", "none"); dSprintf(targetIdStr, sizeof(targetIdStr), "%d", -1); } if (bool(mEnemyProjectile) && mEvadingCounter > 0 && mEvadingCounter < 45) dSprintf(projectileStr, sizeof(projectileStr), "%d", mEnemyProjectile->getId()); else dSprintf(projectileStr, sizeof(projectileStr), "%d", -1); Con::executef(5, "AIProcessEngagement", idStr, targetIdStr, targetTypeStr, projectileStr); } void AIConnection::scriptChooseEngageWeapon(F32 distToTarg) { char idStr[32], targetIdStr[32], distTotargStr[32]; dSprintf(idStr, sizeof(idStr), "%d", getId()); dSprintf(targetIdStr, sizeof(targetIdStr), "%d", mEngageTarget->getId()); dSprintf(distTotargStr, sizeof(distTotargStr), "%f", distToTarg); const char* canUseEnergyStr = mNavUsingJet ? "false" : "true"; const char* environmentStr = (mInWater || mTargInWater ? "water" : (mOutdoors ? "outdoors" : "indoors")); Con::executef(6, "AIChooseEngageWeapon", idStr, targetIdStr, distTotargStr, canUseEnergyStr, environmentStr); } void AIConnection::scriptChooseObjectWeapon(F32 distToTarg) { char idStr[32], targetIdStr[32], distTotargStr[32]; dSprintf(idStr, sizeof(idStr), "%d", getId()); dSprintf(targetIdStr, sizeof(targetIdStr), "%d", mTargetObject->getId()); dSprintf(distTotargStr, sizeof(distTotargStr), "%f", distToTarg); const char* canUseEnergyStr = mNavUsingJet ? "false" : "true"; const char* environmentStr = (mInWater || mTargInWater ? "water" : (mOutdoors ? "outdoors" : "indoors")); Con::executef(7, "AIChooseObjectWeapon", idStr, targetIdStr, distTotargStr, gTargetObjectMode[mObjectMode], canUseEnergyStr, environmentStr); } void AIConnection::setEvadeLocation(const Point3F &dangerLocation, S32 durationTicks) { //find a new location orthogonal to the danger location to head to Point3F dangerVector = dangerLocation - mLocation; Point3F myVector = mMoveLocation - mLocation; if (dangerVector.len() < 0.001f) dangerVector.set(0, 1, 0); dangerVector.normalize(); if (myVector.len() < 0.001f) myVector.set(0, 1, 0); myVector.normalize(); Point3F orthDanger; mCross(dangerVector, Point3F(0, 0, 1), &orthDanger); if (orthDanger.len() < 0.001f) orthDanger.set(0, 1, 0); orthDanger.normalize(); //if we have a NAN vector, the danger location is us! if (orthDanger.x != orthDanger.x || orthDanger.y != orthDanger.y) { dangerVector = mTargLocation - mLocation; if (dangerVector.len() < 0.001f) dangerVector.set(0, 1, 0); dangerVector.normalize(); mCross(dangerVector, Point3F(0, 0, 1), &orthDanger); orthDanger.normalize(); //if we have *STILL* have a NAN vector, head north! if (orthDanger.x != orthDanger.x || orthDanger.y != orthDanger.y) orthDanger.set(0, 1, 0); } //choose the point closest to the direction we're already moving... F32 myAngle = get2DAngle(mLocation + mVelocity2D, mLocation); F32 tempAngle1 = get2DAngle(mLocation, mTargLocation + orthDanger); F32 tempAngle2 = get2DAngle(mLocation, mTargLocation - orthDanger); F32 angleDiff1, angleDiff2; if (myAngle < tempAngle1) angleDiff1 = getMin(tempAngle1 - myAngle, 256 + myAngle - tempAngle1); else angleDiff1 = getMin(myAngle - tempAngle1, 256 + tempAngle1 - myAngle); if (myAngle < tempAngle2) angleDiff2 = getMin(tempAngle2 - myAngle, 256 + myAngle - tempAngle2); else tempAngle2 = getMin(myAngle - tempAngle2, 256 + tempAngle2 - myAngle); if (angleDiff1 < angleDiff2) mEvadeLocation = mLocation + (orthDanger * 30.0f); else mEvadeLocation = mLocation - (orthDanger * 30.0f); if (durationTicks > 0) mEvadingCounter = durationTicks; } void AIConnection::processEngagement(Player *player) { //updatate the detection table PROFILE_START(AI_updateDetectionTable); updateDetectionTable(player); PROFILE_END(); //call the script function once each frame to handle packs, grenades, health kits, etc... scriptProcessEngagement(); //see if we need to reset the script trigger counter (if we changed weapons...) ShapeBaseImageData* tempImage = player->getMountedImage(0); if (tempImage != mMountedImage) mScriptTriggerCounter = -1; mMountedImage = tempImage; //if the script wants us to fire (using the repair pack to self repair mainly)... if (mScriptTriggerCounter-- >= 0) pressFire(true); //make sure we have someone to shoot at if (! mTargetPlayer && ! bool(mTargetObject)) return; //if we have something or someone to shoot at, let the engage code determine whether to fire pressFire(false); //reset the counter mLookAtTargetTimeMS = Sim::getCurrentTime() + 1500; //see if we're shooting a player, or a static object bool engagingPlayer; Point3F targetLocation; F32 distToTarg; if (mTargetPlayer) { engagingPlayer = true; //if the player has been blinded, use the last known location only if (Sim::getCurrentTime() < mBlindedTimer) { S32 dummyTime; hasLOSToClient(mEngageTarget->getId(), dummyTime, targetLocation); } else { //see if we have LOS to the client S32 losTime; Point3F losLocation; bool hasLOS = hasLOSToClient(mEngageTarget->getId(), losTime, losLocation); //if we have just lost LOS to the target, assume he went around a corner, and fire at the last known location if (!hasLOS && losTime < 2500) targetLocation = losLocation; //this simulates slower response - the bot doesn't quite keep up to the target's current location else if (mSkillLevel >= 0.9f) targetLocation = mTargPrevLocation[0]; else if (mSkillLevel >= 0.7f) targetLocation = mTargPrevLocation[1]; else if (mSkillLevel >= 0.3f) targetLocation = mTargPrevLocation[2]; else targetLocation = mTargPrevLocation[3]; } //now calculate the 2D distance distToTarg = (Point3F(targetLocation.x, targetLocation.y, 0) - Point3F(mLocation.x, mLocation.y, 0)).len(); } else { engagingPlayer = false; //if a repair node has been added to the object, use that, otherwise, use the worldBoxCenter targetLocation = mTargetObject->getAIRepairPoint(); if (targetLocation == Point3F(0, 0, 0)) mTargetObject->getWorldBox().getCenter(&targetLocation); distToTarg = mDistToObject2D; } //detect the projectiles from the target mProjectileCounter--; if (engagingPlayer) { const char *incoming = mEngageTarget->getDataField(StringTable->insert("projectile"), NULL); if (incoming && incoming[0]) { Projectile *projectile; if (Sim::findObject(incoming, projectile)) { //see if it's a new threat, or time to re-evaluate the current one if (projectile != (Projectile*)mEnemyProjectile || mProjectileCounter <= 0) { //reset the projectile counter mProjectileCounter = 15; F32 timeToImpact; ProjectileData* projData = projectile->getDataBlock() != NULL ? dynamic_cast(projectile->getDataBlock()) : NULL; if (projData && projectile->calculateImpact(4.0f, mImpactLocation, timeToImpact)) { //see if the impact location is within range Point3F predictMyLocation = mLocation + mVelocity * timeToImpact; F32 distToDanger = (predictMyLocation - mImpactLocation).len(); if (distToDanger < getMax(projData->damageRadius, 2.0f)) { setEvadeLocation(mImpactLocation); mEnemyProjectile = projectile; //set the evade counter - any value above 45 is simulated response time S32 responseTime = S32(30 * (1.0f - mSkillLevel)); mEvadingCounter = 45 + responseTime; } } } } } } //if we're in the middle of a NavGraph Jet, we can't shoot... Point3F dummyPoint; if (mNavUsingJet && mJetting.shouldAimAt(dummyPoint)) return; //here we only fire at someone if we've seen them recently enough if (bool(mEngageTarget)) { S32 detectLOSTime; Point3F lastLOSLocation; bool hasLOSDetect = hasLOSToClient(mEngageTarget->getId(), detectLOSTime, lastLOSLocation); if (!hasLOSDetect && (detectLOSTime > mDetectHiddenPeriod)) return; } //process the engagement state machine switch (mEngageState) { case ChooseWeapon: { //set the aim location if (! mTargetInRange) setAimLocation(targetLocation); else setAimLocation(Point3F(targetLocation.x, targetLocation.y, mAimLocation.z)); //set the bool mFiring = false; //limit changing the weapon too often for low skill if (--mChangeWeaponCounter > 0) { mEngageState = ReloadWeapon; mStateCounter = 45; //set the delay counter mDelayCounter = S32(20.0f * (1.0f - mSkillLevel)); } //else reset the counter else mChangeWeaponCounter = S32((1.0f - mSkillLevel) / 0.10f); //keep track of the previous weapon StringTableEntry prevWeapon = mProjectileName; //call the script function to set the weapon vars if (engagingPlayer) scriptChooseEngageWeapon(distToTarg); else scriptChooseObjectWeapon(distToTarg); if (distToTarg > mEngageMaxDistance) { mEngageState = OutOfRange; mStateCounter = 30; } else { mEngageState = ReloadWeapon; mStateCounter = 60; //set the delay counter mDelayCounter = S32(20.0f * (1.0f - mSkillLevel)); //see if we've switched weapons if (dStricmp(mProjectileName, prevWeapon)) mDelayCounter *= 3; } break; } case OutOfRange: { //set the aim location if (! mTargetInRange) setAimLocation(targetLocation); else setAimLocation(Point3F(targetLocation.x, targetLocation.y, mAimLocation.z)); if (distToTarg <= mEngageMaxDistance || (--mStateCounter <= 0)) mEngageState = ChooseWeapon; break; } case ReloadWeapon: { //set the aim location if (! mTargetInRange) setAimLocation(targetLocation); else setAimLocation(Point3F(targetLocation.x, targetLocation.y, mAimLocation.z)); //make sure we're not stuck in this state if (--mStateCounter <= 0) { mEngageState = ChooseWeapon; break; } //make sure we're not trying to use an energy weapon while jetting if (mWeaponEnergy > 0 && mNavUsingJet) { mEngageState = ChooseWeapon; break; } //if the weapon is ready, time to aim bool weaponReady = player->isImageReady(0); bool hasEnergy = (mEnergy >= mWeaponEnergy); //add in factor so they're not unloading on you at lower skill settings... F32 skillVelocityFactor = 6.0f + (mSkillLevel * mSkillLevel * mSkillLevel * 300.0f); bool mustSlowDown = (mVelocity.len() > skillVelocityFactor); if (weaponReady && hasEnergy && !mustSlowDown) { mStateCounter = 15; if (--mDelayCounter <= 0) mEngageState = FindTargetPoint; } //if firing is sustained, keep firing if (mFiring && mTriggerCounter > 0) { mTriggerCounter--; pressFire(); } break; } case FindTargetPoint: { //set the aim location if (! mTargetInRange) setAimLocation(targetLocation); else setAimLocation(Point3F(targetLocation.x, targetLocation.y, mAimLocation.z)); //make sure we're not stuck in this state if (--mStateCounter <= 0) { mEngageState = ChooseWeapon; break; } //make sure we're not trying to use an energy weapon while jetting if (mWeaponEnergy > 0 && mNavUsingJet) { mEngageState = ChooseWeapon; break; } //should we go for center mass, or splash damage, or are we shooting at a lazed target Point3F aimAtTargetPoint; mAimAtLazedTarget = false; //only shoot at lazed targets if we're using a ballistic weapon if (mProjectile && mProjectile->isBallistic) { //see if we have a lazed target point... SimSet *targetSet = Sim::getServerTargetSet(); if (targetSet) { SimSet::iterator i; for (i = targetSet->begin(); i != targetSet->end(); i++) { GameBase *target = static_cast(*i); Point3F targetPoint; U32 dummyTeam; //when the sensor network is finished, then the target's //visibility can be properly determined. target->getTarget(&targetPoint, &dummyTeam); //see if the target is within 20m of what we're trying to shoot at... if ((targetPoint - targetLocation).len() < 10.0f) { aimAtTargetPoint = targetPoint; mAimAtLazedTarget = true; } } } } if (! mAimAtLazedTarget) { //see if we should aim for the target's feet if (engagingPlayer && mProjectile && mProjectile->hasDamageRadius && mProjectile->damageRadius > 5.0f) aimAtTargetPoint = mTargLocation; //else aim for the target's center mass else aimAtTargetPoint = targetLocation; } //find the aim vector Point3F aimVectorMin, aimVectorMax; F32 timeMin, timeMax; if (mProjectile) { bool canShoot; if (engagingPlayer) canShoot = mProjectile->calculateAim(aimAtTargetPoint, mTargVelocity, mMuzzlePosition, mVelocity, &aimVectorMin, &timeMin, &aimVectorMax, &timeMax); else { canShoot = false; if (mObjectMode != MissileVehicle || player->getLockedTargetId() == mTargetObject->getId()) canShoot = mProjectile->calculateAim(aimAtTargetPoint, Point3F(0, 0, 0), mMuzzlePosition, mVelocity, &aimVectorMin, &timeMin, &aimVectorMax, &timeMax); } if (canShoot) { //find the aimlocation from the normalized aim vector Point3F aimLocation = mMuzzlePosition + aimVectorMin * (aimAtTargetPoint - mMuzzlePosition).len(); //now add the tone down the accuracy for moving targets bool needToDope = mVelocity.len() > 4.0f || engagingPlayer || (mProjectile->isBallistic && ! mAimAtLazedTarget); if (needToDope) aimLocation = dopeAimLocation(mMuzzlePosition, aimLocation); //set the aim location setAimLocation(aimLocation); mEngageState = AimAtTarget; mStateCounter = 15; //call processEngagement again immediately PROFILE_START(AI_EngagementInner); processEngagement(player); PROFILE_END(); } else { //reset the bool mTargetInRange = false; //if firing is sustained, keep firing if (mFiring && mTriggerCounter > 0) { mTriggerCounter--; pressFire(); } } } //if no projectile, we need to find one... else { mEngageState = ChooseWeapon; mTargetInRange = false; } break; } case AimAtTarget: { //make sure we're not trying to use an energy weapon while jetting if (mWeaponEnergy > 0 && mNavUsingJet) { mEngageState = ChooseWeapon; break; } //the process order is: fire, then move + aim, we need to aim as if we're //shooting next frame if (engagingPlayer) { Point3F nextFrameAimPoint = mAimLocation - mVelocity / 30.0f; setAimLocation(nextFrameAimPoint); } //see if we have line of site bool clearLOSToTarget = false; RayInfo rayInfo; Point3F startPt = mMuzzlePosition + mVelocity / 30.0f; //as if it's next frame already Point3F endPt = mAimLocation; U32 mask = TerrainObjectType | InteriorObjectType | PlayerObjectType | ForceFieldObjectType; //if the player is mounted on a vehicle, mask in vehicle types as well... if (player->isMounted()) mask |= VehicleObjectType; player->disableCollision(); if (! gServerContainer.castRay(startPt, endPt, mask, &rayInfo)) clearLOSToTarget = true; else if (engagingPlayer && bool(rayInfo.object) && bool(mTargetPlayer)) { if (rayInfo.object->getId() == mTargetPlayer->getId()) clearLOSToTarget = true; } else if (!engagingPlayer && bool(rayInfo.object) && bool(mTargetObject)) { if (rayInfo.object->getId() == mTargetObject->getId()) clearLOSToTarget = true; } player->enableCollision(); bool readyToFire = false; //if we have a clear LOS, or if we're firing a ballistic weapon and have enough skill to shoot from behind hills if (clearLOSToTarget || (mProjectile->isBallistic && mSkillLevel >= 0.7f)) readyToFire = true; //else if we're firing a linear weapon with splash damage else if (! clearLOSToTarget && ! mProjectile->isBallistic) { //see if the impact point is within the damage radius of the projectile if (mProjectile->hasDamageRadius && (rayInfo.point - mAimLocation).len() < getMax(10.0f, mProjectile->damageRadius)) { //need to see if the impact point has LOS to the target, otherwise, bot will try to shoot through walls... //readyToFire = true; readyToFire = false; } } if (readyToFire) { mTargetInRange = true; mEngageState = FireWeapon; } else { mTargetInRange = false; mEngageState = FindTargetPoint; } //if firing is sustained, keep firing if (mFiring && mTriggerCounter > 0) { mTriggerCounter--; pressFire(); } break; } case FireWeapon: { //make sure we're not trying to use an energy weapon while jetting if (mWeaponEnergy > 0 && mNavUsingJet) { mEngageState = ChooseWeapon; break; } //if we're firing, we must be in range mTargetInRange = true; //press fire and move to reload pressFire(); mFiring = true; mTriggerCounter--; //see if firing is to be sustained (chaingun, elf gun, etc...) if (mTriggerCounter > 0) { mEngageState = FindTargetPoint; mStateCounter = 15; } else { mEngageState = ChooseWeapon; mStateCounter = 60; } break; } } } F32 AIConnection::angleDifference(F32 angle1, F32 angle2) { F32 tempAngle1 = getMin(angle1, angle2); F32 tempAngle2 = getMax(angle1, angle2); F32 difference = getMin(F32(tempAngle2 - tempAngle1), F32(tempAngle1 + M_2PI - tempAngle2)); return difference; } Point3F AIConnection::correctHeading() { Point3F newLocation = mNodeLocation; //make sure we're heading in approximately the right direction (+- 30 deg or so) //no need to overcorrect if we're not moving very fast if (mVelocity2D.len() > 4) { //if we're heading more than 90 deg in the wrong direction, or we're heading *way* too fast //to hit the node, choose the exact opposite direction of the velocity if (mDotOffCourse <= 0) { //find out our current heading (sub 90 deg since our axis is rotated) F32 heading = mAtan(mVelocity2D.x, mVelocity2D.y) - (M_PI / 2.0f); F32 newHeading = heading + M_PI; newLocation.x = mLocation.x + mDistToNode2D * mCos(newHeading); newLocation.y = mLocation.y - mDistToNode2D * mSin(newHeading); } //else mirror the angle through the desired heading else { //find out our current heading (sub 90 deg since our axis is rotated) F32 heading = mAtan(mVelocity2D.x, mVelocity2D.y) - (M_PI / 2.0f); //find out what angle we're currently off by F32 headingDiff = mAcos(mDotOffCourse); //determine the "overcompensate" factor F32 overCompFactor; if (mDotOffCourse > 0.85) overCompFactor = 3.0f; else overCompFactor = 2.0f; //now see find out which direction F32 angle1 = heading + headingDiff; F32 angle2 = heading - headingDiff; Point3F newVec1(mCos(angle1), -mSin(angle1), 0); Point3F newVec2(mCos(angle2), -mSin(angle2), 0); //the dot with one of these angles should have a dot near 1 - //create the "correcting" angle to overcompensate the headingDiff F32 newAngle; F32 angle1Dot = get2DDot(mNodeLocation - mLocation, newVec1); F32 angle2Dot = get2DDot(mNodeLocation - mLocation, newVec2); if (angle1Dot > angle2Dot) newAngle = heading + overCompFactor * headingDiff; else newAngle = heading - overCompFactor * headingDiff; //find the new point newLocation.x = mLocation.x + mDistToNode2D * mCos(newAngle); newLocation.y = mLocation.y - mDistToNode2D * mSin(newAngle); } } return newLocation; } Point3F AIConnection::avoidPlayers(Player *player, const Point3F &desiredDestination, bool destIsFinal) { F32 avoidObjectAngle = -1; //see if we're near another player Box3F queryBox; queryBox.min = mLocation; queryBox.max = mLocation; queryBox.min -= Point3F(2.0f, 2.0f, 0.5f); queryBox.max += Point3F(2.0f, 2.0f, 2.5f); ShapeBase *closestObject = NULL; F32 closestDist = 32767; Point3F closestLocation; SimpleQueryList result; //U32 mask = PlayerObjectType; U32 mask = ShapeBaseObjectType | StaticTSObjectType; gServerContainer.findObjects(queryBox, mask, SimpleQueryList::insertionCallback, S32(&result)); if (result.mList.size() > 1) { //find out if the closest person is to the left or right... for (S32 i = 0; i < result.mList.size(); i++) { ShapeBase *neighborObject = static_cast(result.mList[i]); //can't bump into yourself if (neighborObject->getId() == player->getId()) continue; //if it's not a static TS object, see if it's a shape base with the aiAvoidThis flag set... if (!(neighborObject->getType() & StaticTSObjectType)) { ShapeBaseData *db = static_cast(neighborObject->getDataBlock()); if (!db || ! db->aiAvoidThis) continue; } //make sure it's not a corpse! if (neighborObject->getType() & CorpseObjectType) continue; //now we see if it's actually in our way, or if it's just the bounding box... Point3F tempVector = neighborObject->getBoxCenter() - player->getBoxCenter(); if (tempVector.len() > 1.0f) { tempVector.normalize(); player->disableCollision(); RayInfo rayInfo; Point3F startPt = player->getBoxCenter(); Point3F endPt = startPt + (tempVector * 2.0f); bool losResult = gServerContainer.castRay(startPt, endPt, neighborObject->getType(), &rayInfo); player->enableCollision(); //we disregard this object if we didn't intersect with it if (!losResult || (rayInfo.object->getId() != neighborObject->getId())) continue; } MatrixF const& tempTransform = neighborObject->getTransform(); Point3F tempLocation; tempTransform.getColumn(3, &tempLocation); F32 tempDist = (tempLocation - mLocation).len(); if (tempDist < closestDist) { closestDist = tempDist; closestObject = neighborObject; closestLocation = tempLocation; } } } //if we didn't find anyone, return the desired dest if (! closestObject) { mAvoidingObject = NULL; return desiredDestination; } //otherwise, we've got a neighbor... else { //update the detection table for this bot Player *closestPlayer = dynamic_cast(closestObject); if (bool(closestPlayer)) { GameConnection *closestClient = closestPlayer->getControllingClient(); if (closestClient) clientDetected(closestClient->getId()); } //find out how close we are to our destination Point3F newLocation; F32 newHeading; F32 distToDest2D = (Point3F(desiredDestination.x, desiredDestination.y, 0.0f) - Point3F(mLocation.x, mLocation.y, 0.0f)).len(); //create and normalize the vectors Point3F directionVector = desiredDestination - mLocation; Point3F neighborVector = closestLocation - mLocation; directionVector.z = 0; //normalize the vector if (directionVector.len() < 0.001f) directionVector.set(0, 1, 0); directionVector.normalize(); //make sure we didn't get any -NAN values... if (directionVector.x != directionVector.x || directionVector.y != directionVector.y) directionVector.set(0, 1, 0); neighborVector.z = 0; //normalize the vector if (neighborVector.len() < 0.001f) neighborVector.set(0, 1, 0); neighborVector.normalize(); //make sure we didn't get any -NAN values... if (neighborVector.x != neighborVector.x || neighborVector.y != neighborVector.y) neighborVector.set(0, 1, 0); //dot the direction vector with the neighbor vector - if directly in our path, move at 90 deg... F32 neighborDot = get2DDot(neighborVector, directionVector); //if the neighbor is behind us, not at all in our way, and we're still moving if (! destIsFinal && neighborDot < 0.5) { mAvoidingObject = NULL; return desiredDestination; } //we've stopped - move at 45 away from target else if (destIsFinal) { F32 neighborHeading = mAtan(neighborVector.x, neighborVector.y) - (M_PI / 2.0f); F32 angle1 = neighborHeading + ((M_PI / 2.0f) + (M_PI / 4.0f)); F32 angle2 = neighborHeading - ((M_PI / 2.0f) + (M_PI / 4.0f)); Point3F newVec1(mCos(angle1), -mSin(angle1), 0); Point3F newVec2(mCos(angle2), -mSin(angle2), 0); //find out which newVec will take us furthest away from the neighbor (smallest dot product) F32 angle1Dot = get2DDot(neighborVector, newVec1); F32 angle2Dot = get2DDot(neighborVector, newVec2); if (angle1Dot < angle2Dot) newHeading = angle1; else newHeading = angle2; //now that we've chosen the new heading, calculate the new destination location newLocation.x = mLocation.x + getMax(6.0f, distToDest2D) * mCos(newHeading); newLocation.y = mLocation.y - getMax(6.0f, distToDest2D) * mSin(newHeading); newLocation.z = desiredDestination.z; //note that since we're already at our final destination, all we need to do is //get bumped out of the way, so no need to save the avoidance vars... mAvoidingObject = NULL; } else if ((GameBase*)mAvoidingObject != closestObject || desiredDestination != mAvoidDestinationPoint) { F32 neighborHeading = mAtan(neighborVector.x, neighborVector.y) - (M_PI / 2.0f); F32 angle1 = neighborHeading + (M_PI / 2.0f); F32 angle2 = neighborHeading - (M_PI / 2.0f); Point3F newVec1(mCos(angle1), -mSin(angle1), 0); Point3F newVec2(mCos(angle2), -mSin(angle2), 0); F32 angle1Dot = get2DDot(directionVector, newVec1); F32 angle2Dot = get2DDot(directionVector, newVec2); if (angle1Dot > angle2Dot) newHeading = angle1; else newHeading = angle2; //now that we've chosen the new heading, calculate the new destination location newLocation.x = mLocation.x + getMax(6.0f, distToDest2D) * mCos(newHeading); newLocation.y = mLocation.y - getMax(6.0f, distToDest2D) * mSin(newHeading); newLocation.z = desiredDestination.z; //now save off the avoidance vars mAvoidingObject = closestObject; mAvoidSourcePoint = mLocation; mAvoidDestinationPoint = desiredDestination; mAvoidMovePoint = newLocation; mAvoidForcedPath = false; } //otherwise we hit this object the last time we were trying to move to our dest else { if ((mAvoidMovePoint - mLocation).len() < 1.5f) { if (! mAvoidForcedPath) { mPath.forceSearch(); mAvoidForcedPath = true; } newLocation = desiredDestination; } else newLocation = mAvoidMovePoint; } return newLocation; } } void AIConnection::processVehicleMovement(Player *player) { //first, kill the nav jetting state machine... if (mNavUsingJet) { mNavUsingJet = false; mJetting.reset(); mPath.forceSearch(); } //now set the aim location if the bot is a passenger. //note - if the passenger is in a fixed mounted position, this will be ignored anyways... if (player->isMounted() && bool(player->getObjectMount())) { Point3F vec; MatrixF vehicleMat; player->getObjectMount()->getMountTransform(player->getMountNode(), &vehicleMat); vehicleMat.getColumn(1,&vec); F32 vehicleRot = mAtan(vec.x,vec.y) - (M_PI / 2.0f); //choose a point so the bot will face the same direction as the vehicle... Point3F aimVector(mCos(vehicleRot), -mSin(vehicleRot), 0); Point3F aimLocation = mLocation + 30.0f * aimVector; aimLocation.z += 2.0f; setScriptAimLocation(aimLocation, 1000); } //finally, the script callback (mainly to handle vehicle firing, etc...) char idStr[32]; dSprintf(idStr, sizeof(idStr), "%d", getId()); Con::executef(2, "AIProcessVehicle", idStr); } void AIConnection::processPilotVehicle(Vehicle *myVehicle) { //eventually, I may move some of the pilot code from ::getMoveList() to here... myVehicle; //kill the nav jetting state machine... if (mNavUsingJet) { mNavUsingJet = false; mJetting.reset(); mPath.forceSearch(); } //finally, the script callback (mainly to handle vehicle firing, etc...) char idStr[32]; dSprintf(idStr, sizeof(idStr), "%d", getId()); Con::executef(2, "AIPilotVehicle", idStr); } void AIConnection::processMovement(Player *player) { player->updateWorkingCollisionSet(); // Inform path machinery about our jetting ability. setPathCapabilities(player); //if we're avoiding danger, override the current destination and move mode... //note, if mEvadingCounter > 45, we use this to slow down reaction time... mEvadingCounter--; if (mEvadingCounter > 0 && mEvadingCounter < 45) { //make sure we don't get stuckin in the nav jet state machine mJetting.reset(); mNavUsingJet = false; mIsEvading = true; setMoveLocation(Point3F(mEvadeLocation.x, mEvadeLocation.y, 0)); setMoveSpeed(1.0f); //jump if we can - should encourage skiing if (player->canJump()) pressJump(); else pressJet(); } //regular navigation... else { // For debugging movement code of single bot (lh)- S32 focusOnBot = Con::getIntVariable("$AIFocusOnBot"); if (focusOnBot) { if (focusOnBot != getId()) return; else { F32 E = mPath.jetWillNeedEnergy(12.0); if (E > 0.0) Con::printf("%d wants %f energy", focusOnBot, E); } } //reset the nav jet stuff if we've just finished evading if (mIsEvading) { mIsEvading = false; mNavUsingJet = false; mPath.forceSearch(); } mPath.setDestMounted(false); if (!mNavUsingJet) mMoveMode = mMoveModePending; switch (mMoveMode) { case ModeStop: { //update the path anyways - mainly to correct aiming probs... mPath.updateLocations(mLocation, mMoveDestination); mNodeLocation = mPath.getSeekLoc(mVelocity); mNavUsingJet = false; //even stopped, we need to move out of the way of teammates Point3F moveLocation = avoidPlayers(player, mLocation, true); if (moveLocation != mLocation) { setMoveSpeed(0.6f); setMoveLocation(moveLocation); } else { setMoveSpeed(0.0f); } break; } case ModeGainHeight: { mNavUsingJet = false; //jump if we can - should encourage skiing if (player->canJump()) pressJump(); else pressJet(); setMoveLocation(mLocation); break; } case ModeMountVehicle: //set the path var mPath.setDestMounted(true); //if we're already mounted, no need to keep moving... if (player->isMounted()) { setMoveSpeed(0.0f); break; } //fall through to mode express... case ModeWalk: case ModeExpress: { //see if we have a corpse to walk to first mPath.updateLocations(mLocation, mMoveDestination); mNodeLocation = mPath.getSeekLoc(mVelocity); //see if we have a brand new node location... if (mNodeLocation != mPrevNodeLocation) { mPrevNodeLocation = mNodeLocation; mInitialLocation = mLocation; } //else see if we've overshot the node and have to recalc the path else { if (!mNavUsingJet) { Point3F vec1 = mInitialLocation - mNodeLocation; Point3F vec2 = mLocation - mNodeLocation; // Moved 2d check here - problems picking up packs with 3d dist check. // Also, vectors don't need to be normalized if dot compare is with 0. vec2.z = vec1.z = 0; if (vec1.lenSquared() > 1.0f && vec2.lenSquared() > 1.0f) if (mDot(vec1, vec2) < 0.0f) { mPrevNodeLocation.set(0, 0, 0); mPath.forceSearch(); } } } if (mPath.userMustJet() && !mInWater) { mNavUsingJet = true; if (mJetting.status() == AIJetWorking) { mJetting.process(this, player); if (mJetting.status() != AIJetWorking) { if (mJetting.status() == AIJetSuccess) mPath.informJetDone(); else { if (mMoveMode == ModeMountVehicle) pressJump(); mPath.forceSearch(); } mJetting.reset(); } else if (mJetting.badTimeToSearch()) mPath.informJetBusy(); } else mJetting.init(mNodeLocation, mPath.intoMount(), mPath.getJetInfo()); } else { if (mNavUsingJet) { //force a path search after jetting is done... mPath.forceSearch(); mNavUsingJet = false; } //jet if we're falling too quickly if (mVelocity.z < -20.0f) pressJet(); //see if we're getting near the end of the path //F32 distToEnd = getMax(mPath.distRemaining(2 * mMoveTolerance), (mLocation - mMoveDestination).len()); bool pathIsCurrent = mPath.isPathCurrent(); F32 distToEnd = (pathIsCurrent ? mPath.distRemaining(2 * mMoveTolerance) : (2 * mMoveTolerance)); F32 tolerance = distToEnd > mMoveTolerance ? 0.25f : mMoveTolerance; F32 jetEnergy = mPath.jetWillNeedEnergy(30); //find out where we're really heading... Point3F moveLocation = mNodeLocation; if (mDistToNode2D > 0.25f) moveLocation = correctHeading(); //calculate the distance to where we're moving F32 distToMove2D = (Point3F(mLocation.x, mLocation.y, 0) - Point3F(moveLocation.x, moveLocation.y, 0)).len(); F32 distToEnd2D = (Point3F(mLocation.x, mLocation.y, 0) - Point3F(mMoveDestination.x, mMoveDestination.y, 0)).len(); distToEnd = getMax(distToEnd, distToEnd2D); //now adjust the heading if we're bumping into other players if (mMoveMode != ModeMountVehicle || distToEnd > 3.0f) moveLocation = avoidPlayers(player, moveLocation, false); //now move in the calculated direction setMoveLocation(moveLocation); //see if we're stuck... if (distToEnd > tolerance && distToEnd > 1.0f) { Point3F checkStuck2D = mLocation - mStuckLocation; checkStuck2D.z = 0.0f; if ((mNodeLocation - mStuckDestination).len() > 1.0f || (checkStuck2D).len() > 1.0f) { mStuckLocation = mLocation; mStuckDestination = mNodeLocation; mStuckTimer = Sim::getCurrentTime(); } else if (Sim::getCurrentTime() - mStuckTimer > 2000) { setMoveMode(ModeStuck); mStuckInitialized = false; mStuckTryJump = true; mStuckJumpInitialized = false; return; } else { // Know we want to be moving- inform Path of how well we're doing- mPath.informProgress(mVelocity); } } //look ahead in the path, and see if the next point is collinear with our current destination Point3F nextNodeLocation; bool isCollinearPath = false; if (mPath.getPathNodeLoc(1, nextNodeLocation)) { F32 tempDot = get2DDot(moveLocation - mLocation, nextNodeLocation - moveLocation); if (tempDot > 0.9f) isCollinearPath = true; } //add jumping and jetting to speed the player up... if (distToMove2D > 30.0f) { //full speed with skiing setMoveSpeed(1.0f); //jump if we can - should encourage skiing if (mHeadingDownhill) { if (player->canJump() && mSkillLevel >= 0.25f) pressJump(); } else { if (mEnergyAvailable && mEnergy > jetEnergy && mMoveMode != ModeWalk) { if (player->canJump()) pressJump(); else pressJet(); } } } else if (distToEnd > 10.0f && isCollinearPath) { //full speed - no skiing setMoveSpeed(1.0f); if (mEnergyAvailable && mEnergy > jetEnergy && mMoveMode != ModeWalk) pressJet(); } else if (distToMove2D > getMax(4.0f, tolerance)) { //full speed - no skiing setMoveSpeed(1.0f); } else if (distToMove2D > tolerance) { //slower speed with no skiing setMoveSpeed(0.4f); } else if (distToEnd < tolerance) { //stopped - we've arrived setMoveMode(ModeStop); } } } break; case ModeStuck: { if (mStuckTryJump) { if (! mStuckJumpInitialized) { mStuckJumpInitialized = true; mStuckJumpTimer = Sim::getCurrentTime(); setMoveSpeed(0.0f); setMoveLocation(mLocation); } else if (Sim::getCurrentTime() - mStuckJumpTimer < 500) { if (mEnergy < 0.25f) mStuckJumpTimer = Sim::getCurrentTime(); } else if (Sim::getCurrentTime() - mStuckJumpTimer < 1000) { pressJump(); pressJet(); } else if (Sim::getCurrentTime() - mStuckJumpTimer < 2000) { setMoveLocation(mStuckDestination); setMoveSpeed(1.0f); pressJet(); } else { //set the bool so it'll try to back up if we're still stuck... mStuckTryJump = false; //see if that unstuck us... if ((mLocation - mStuckLocation).len() > 1.0f) { mPath.forceSearch(); setMoveSpeed(1.0f); setMoveMode(ModeExpress, true); mStuckLocation.set(0, 0, 0); return; } } } //else try backing up, and researching the path else { //I guess we're still stuck... try backing up if (! mStuckInitialized) { mStuckInitialized = true; //find a new direction to move Point3F newLocation; F32 foundLength = 32767; Point3F curHeading = mNodeLocation - mLocation; F32 curAngle = mAtan(curHeading.x, curHeading.y) - (M_PI / 2.0f); for (F32 i = 0.0f; i < 5.0f; i += 1.0f) { //run an LOS to see if the way is clear... F32 testAngle = curAngle + ((i + 2.0) * M_PI / 4.0f); Point3F newVec(mCos(testAngle), -mSin(testAngle), 0.0f); U32 mask = TerrainObjectType | InteriorObjectType; RayInfo rayInfo; Point3F startPt = mLocation; Point3F endPt = mLocation + (4.0f * newVec); startPt.z += 0.3f; endPt.z += 0.3f; if (! gServerContainer.castRay(startPt, endPt, mask, &rayInfo)) { newLocation = endPt; break; } else if ((rayInfo.point - startPt).len() < foundLength) { foundLength = (rayInfo.point - startPt).len(); newLocation = endPt; } } //set the move location, and set the time stamp mStuckTimer = Sim::getCurrentTime(); setMoveLocation(newLocation); setMoveSpeed(1.0f); } //see if our time has run out, or if we're close to our dest if ((mLocation - mMoveLocation).len() < 1.0f || Sim::getCurrentTime() - mStuckTimer > 2000) { mStuckLocation.set(0, 0, 0); mPath.informStuck(mStuckLocation, mStuckDestination); setMoveMode(ModeExpress, true); } } } break; } } //if we're in the middle of a nav jet, aim at the landing place... Point3F jetAimLocation; if (mNavUsingJet && mJetting.shouldAimAt(jetAimLocation)) { setAimLocation(jetAimLocation); } //else set the aiming if we don't have a target to shoot at else if (mMoveMode != ModeStop && mPath.isPathCurrent() && !bool(mTargetPlayer) && !bool(mTargetObject) && (Sim::getCurrentTime() > mLookAtTargetTimeMS)) { //set the player to always look 10m ahead on the path Point3F directionVector; Point3F aimPoint; F32 distToEnd = mPath.distRemaining(10); if (distToEnd >= 10) aimPoint = mPath.getLocOnPath(10); else aimPoint = mNodeLocation; //now extend that point by 30m so we don't look down directionVector = aimPoint - mLocation; directionVector.z = 0; F32 directionLen = directionVector.len(); // LH pulled apart the normalize() to avoid (interupt causing) divides by zero if (directionLen < 0.001) { aimPoint.x = mLocation.x + 30.0f * mCos(mRotation.z); aimPoint.y = mLocation.y - 30.0f * mSin(mRotation.z); aimPoint.z = mMuzzlePosition.z; } else { // LH- here's the normalize: aimPoint.x = mLocation.x + 30.0f * (directionVector.x / directionLen); aimPoint.y = mLocation.y + 30.0f * (directionVector.y / directionLen); aimPoint.z = mMuzzlePosition.z; } setAimLocation(aimPoint); } } void AIConnection::scriptSustainFire(S32 count) { mScriptTriggerCounter = count; } static Point3F extractRotation(const MatrixF & matrix) { const F32 * mat = (const F32*)matrix; Point3F r; r.x = mAsin(mat[MatrixF::idx(2,1)]); if(mCos(r.x) != 0.f) { r.y = mAtan(-mat[MatrixF::idx(2,0)], mat[MatrixF::idx(2,2)]); r.z = mAtan(-mat[MatrixF::idx(0,1)], mat[MatrixF::idx(1,1)]); } else { r.y = 0.f; r.z = mAtan(mat[MatrixF::idx(1,0)], mat[MatrixF::idx(0,0)]); } return(r); } void AIConnection::getMoveList(Move** movePtr,U32* numMoves) { //initialize the move structure and return pointers mMove = NullMove; *movePtr = &mMove; *numMoves = 1; //if the system is not enabled, return having set the movePtr to a NullMove if (!gAISystemEnabled) return; //movement is done in the object's rotation - create a matrix MatrixF moveMatrix; moveMatrix.set(EulerF(0, 0, 0)); moveMatrix.setColumn(3, Point3F(0, 0, 0)); moveMatrix.transpose(); //aiming variables Point3F curLocation; F32 curYaw, curPitch; F32 newYaw, newPitch; Point3F rotation; F32 xDiff, yDiff, zDiff; //make sure we have a valid control object ShapeBase *ctrlObject = getControlObject(); if (! ctrlObject) return; //get the control object Player *myPlayer = NULL; myPlayer = dynamic_cast(ctrlObject); //make sure we're either controlling a player or a vehicle if (myPlayer) { //if we're dead, no need to call any of the script functions... if (! dStricmp(myPlayer->getStateName(), "dead")) return; //process the ai tasks and steps process(myPlayer); F32 vehicleRot = 0; // Find the rotation around the Z axis from the mounted vehicle if (myPlayer->isMounted() && bool(myPlayer->getObjectMount())) { Point3F vec, pos; MatrixF vehicleMat; myPlayer->getObjectMount()->getMountTransform(myPlayer->getMountNode(), &vehicleMat); vehicleMat.getColumn(1,&vec); vehicleMat.getColumn(3,&pos); vehicleRot = -mAtan(-vec.x,vec.y); } //get the current location (global coords) MatrixF const& myTransform = myPlayer->getTransform(); myTransform.getColumn(3, &curLocation); //initialize the aiming variables rotation = myPlayer->getRotation(); Point3F headRotation = myPlayer->getHeadRotation(); curYaw = rotation.z + vehicleRot; curPitch = headRotation.x; xDiff = mAimLocation.x - curLocation.x; yDiff = mAimLocation.y - curLocation.y; //first do Yaw if (! isZero(xDiff) || ! isZero(yDiff)) { //use the cur yaw between -Pi and Pi while (curYaw > M_2PI) curYaw -= M_2PI; while (curYaw < -M_2PI) curYaw += M_2PI; //find the new yaw newYaw = mAtan(xDiff, yDiff); //find the yaw diff F32 yawDiff = newYaw - curYaw; //make it between 0 and 2PI if (yawDiff < 0.0f) yawDiff += M_2PI; else if (yawDiff >= M_2PI) yawDiff -= M_2PI; //now make sure we take the short way around the circle if (yawDiff > M_PI) yawDiff -= M_2PI; else if (yawDiff < -M_PI) yawDiff += M_2PI; mMove.yaw = yawDiff; //set up the movement matrix moveMatrix.set(EulerF(0, 0, newYaw)); } else moveMatrix.set(EulerF(0, 0, curYaw)); //next do pitch // F32 horzDist = Point2F(yDiff, xDiff).len(); // F32 horzDist = Point2F(mAimLocation.x - mMuzzlePosition.x, mAimLocation.y - mMuzzlePosition.y).len(); F32 horzDist = Point2F(mAimLocation.x - mEyePosition.x, mAimLocation.y - mEyePosition.y).len(); if (! isZero(horzDist)) { //we shoot from the gun, not the eye... // F32 vertDist = mAimLocation.z - mMuzzlePosition.z; F32 vertDist = mAimLocation.z - mEyePosition.z; newPitch = mAtan(horzDist, vertDist) - (M_PI / 2.0f); F32 pitchDiff = newPitch - curPitch; mMove.pitch = pitchDiff; } //finally, move towards mMoveLocation xDiff = mMoveLocation.x - curLocation.x; yDiff = mMoveLocation.y - curLocation.y; if (((mFabs(xDiff) > 0) || (mFabs(yDiff) > 0)) && (! isZero(mMoveSpeed))) { if (isZero(xDiff)) mMove.y = (curLocation.y > mMoveLocation.y ? -mMoveSpeed : mMoveSpeed); else if (isZero(yDiff)) mMove.x = (curLocation.x > mMoveLocation.x ? -mMoveSpeed : mMoveSpeed); else if (mFabs(xDiff) > mFabs(yDiff)) { F32 value = mFabs(yDiff / xDiff) * mMoveSpeed; mMove.y = (curLocation.y > mMoveLocation.y ? -value : value); mMove.x = (curLocation.x > mMoveLocation.x ? -mMoveSpeed : mMoveSpeed); } else { F32 value = mFabs(xDiff / yDiff) * mMoveSpeed; mMove.x = (curLocation.x > mMoveLocation.x ? -value : value); mMove.y = (curLocation.y > mMoveLocation.y ? -mMoveSpeed : mMoveSpeed); } //now multiply the move vector by the transpose of the object rotation matrix moveMatrix.transpose(); Point3F newMove; moveMatrix.mulP(Point3F(mMove.x, mMove.y, 0), &newMove); //and sub the result back in the move structure mMove.x = newMove.x; mMove.y = newMove.y; } } else { Vehicle *myVehicle; myVehicle = dynamic_cast(ctrlObject); if (myVehicle) { //process the ai tasks and steps (well, not steps get ignored for piloting vehicles...) process(myVehicle); //get the current location (global coords) MatrixF const& myTransform = myVehicle->getTransform(); myTransform.getColumn(3, &curLocation); //initialize the aiming variables rotation = extractRotation(myTransform); xDiff = mPilotAimLocation.x - curLocation.x; yDiff = mPilotAimLocation.y - curLocation.y; zDiff = mPilotAimLocation.z - curLocation.z; F32 dist2D = Point2F(yDiff, xDiff).len(); //first do Yaw curYaw = rotation.z; F32 yawDiff = 0; Point3F velocity = myVehicle->getVelocity(); Point3F velocity2D(velocity.x, velocity.y, 0.0f); F32 velocityDot = get2DDot(velocity2D, (mPilotDestination - curLocation)); //if ((mFabs(xDiff) > 5.0f || mFabs(yDiff) > 5.0f) && (mPilotSpeed == 0.0f || (velocityDot < 0.8f && velocity2D.len() > 5.0f))) if (mFabs(xDiff) > 5.0f || mFabs(yDiff) > 5.0f) { //use the cur yaw between -Pi and Pi while (curYaw > M_2PI) curYaw -= M_2PI; while (curYaw < -M_2PI) curYaw += M_2PI; //find the new yaw newYaw = mAtan(xDiff, yDiff); //find the yaw diff yawDiff = newYaw - curYaw; //make it between 0 and 2PI if (yawDiff < 0.0f) yawDiff += M_2PI; else if (yawDiff >= M_2PI) yawDiff -= M_2PI; //now make sure we take the short way around the circle if (yawDiff > M_PI) yawDiff -= M_2PI; else if (yawDiff < -M_PI) yawDiff += M_2PI; //set up the movement matrix mMove.yaw = yawDiff; moveMatrix.set(EulerF(0, 0, newYaw)); } else moveMatrix.set(EulerF(0, 0, curYaw)); //both pitch and destination should use the pilotDestination, not the aim location xDiff = mPilotDestination.x - curLocation.x; yDiff = mPilotDestination.y - curLocation.y; zDiff = mPilotDestination.z - curLocation.z; dist2D = Point2F(yDiff, xDiff).len(); //get the yAxis - the vertical slope of which is our current pitch Point3F yAxis; myTransform.mulV(Point3F(0, 1, 0), &yAxis); yAxis.normalize(); curPitch = mAtan(1.0f, yAxis.z) - (M_PI / 2.0f); //if we're below our destination, and don't have enough vertical velocity, pitch up... newPitch = mAtan(dist2D, zDiff) - (M_PI / 2.0f); newPitch = getMax(mPitchUpMax, getMin(mPitchDownMax, newPitch)); //use a pitch delta to prevent from pitching too fast... S32 pitchDir = 0; F32 pitchDelta = curPitch - mPreviousPitch; mPreviousPitch = curPitch; //make sure we've got some horizontal speed if (mPilotSpeed > 0.0f && velocity2D.len() > 5.0f) { //at our current rate of ascent/descent, F32 timeToDest = dist2D / velocity2D.len(); F32 vertDist = velocity.z * timeToDest; //if we're below our dest and our vertical lift is too low, pitch up if (zDiff > 5.0f && (vertDist < zDiff - 2.0f || curPitch > 0.0f)) pitchDir = 1; //else if we're higher than our dest, and our vertical drop is too low, pitch down else if (zDiff < -5.0f && (vertDist > zDiff + 2.0f || curPitch < 0.0f)) pitchDir = -1; //make sure we're within our pitch limits, and that we're not pitching too fast if (pitchDir > 0 && (curPitch < mPitchUpMax || pitchDelta < -mPitchIncMax)) pitchDir = -1; else if (pitchDir < 0 && (curPitch > mPitchDownMax || pitchDelta > mPitchIncMax)) pitchDir = 1; //if we didn't meet the above conditions, our velocity is on track, and we should level out if (pitchDir == 0) { //if we're pitched up, and our change in pitch is also in the up direction if (curPitch < 0.0f && pitchDelta < 0) pitchDir = -1; else if (curPitch > 0.0f && pitchDelta > 0) pitchDir = 1; } //now put the pitch into the pitch move struct if (pitchDir > 0) mMove.pitch = mPitchUpMax; else if (pitchDir < 0) mMove.pitch = mPitchDownMax; } //now calculate the actual movement towards mPilotDestination //first calculate speed - scale by whether we're already heading in the right direction... F32 speed = 0.0f; if ((mFabs(xDiff) > 5.0f) || (mFabs(yDiff) > 5.0f)) { //if we're heading within a 45 deg cone, add some speed if (mFabs(yawDiff < 0.8f)) speed = 1.0f - (mFabs(yawDiff) / M_PI); } //slow our movespeed down according to how far we are from our target... if (speed > 0.0f && dist2D >= 5.0f && dist2D <= 20.0f) speed *= 0.3f * getMin(0.7f, dist2D / 20.0f); //see if we need to reverse thrust... if (dist2D < 6.0f && velocity2D.len() > 5.0f && mFabs(velocityDot) >= 0.78f) speed = getMax(-1.0f, -2.0f * speed); //now factor in our pilot speed speed *= mPilotSpeed; //see if we should add some jetting if (velocity2D.len() < 8.0f && curPitch <= 0.05f && zDiff > 5.0f) mTriggers[JetTrigger] = true; //set some debugging vars mVehicleLocation = curLocation; mCurrentPitch = curPitch; mDesiredPitch = newPitch; mPitchIncrement = mMove.pitch; mPilotDistToDest2D = dist2D; mPilotCurVelocity = myVehicle->getVelocity().len(); mPilotCurThrust = speed; mCurrentYaw = curYaw; //fill in the move struction if (((mFabs(xDiff) > 5.0f) || (mFabs(yDiff) > 5.0f)) && (! isZero(speed))) { if (mFabs(xDiff) <= 5.0f) mMove.y = (curLocation.y > mPilotDestination.y ? -speed : speed); else if (mFabs(yDiff) <= 5.0f) mMove.x = (curLocation.x > mPilotDestination.x ? -speed : speed); else if (mFabs(xDiff) > mFabs(yDiff)) { F32 value = mFabs(yDiff / xDiff) * speed; mMove.y = (curLocation.y > mPilotDestination.y ? -value : value); mMove.x = (curLocation.x > mPilotDestination.x ? -speed : speed); } else { F32 value = mFabs(xDiff / yDiff) * speed; mMove.x = (curLocation.x > mPilotDestination.x ? -value : value); mMove.y = (curLocation.y > mPilotDestination.y ? -speed : speed); } //now multiply the move vector by the transpose of the object rotation matrix moveMatrix.transpose(); Point3F newMove; moveMatrix.mulP(Point3F(mMove.x, mMove.y, 0), &newMove); //and sub the result back in the move structure mMove.x = newMove.x; mMove.y = newMove.y; } } } //copy the triggers into the move for (int i = 0; i < MaxTriggerKeys; i++) { mMove.trigger[i] = mTriggers[i]; mTriggers[i] = false; } } //----------------------------------------------------------------------------- //STEP AND TASK FUNCTIONS void AIConnection::clearStep() { if (mStep) mStep->deleteObject(); mStep = NULL; } void AIConnection::setStep(AIStep *step) { clearStep(); mStep = step; } const char *AIConnection::getStepStatus() { if (! mStep) return "Finished"; else { int status = mStep->getStatus(); switch (status) { case AIStep::InProgress: return "InProgress"; case AIStep::Failed: return "Failed"; case AIStep::Finished: default: return "Finished"; } } } const char *AIConnection::getStepName() { if (! mStep) return "NONE"; else { const char *stepName = mStep->getName(); if (!stepName || !stepName[0]) return "NONE"; else return stepName; } } void AIConnection::clearTasks() { while (mTaskList.size()) { AITask *tempPtr = mTaskList[0]; mTaskList.pop_front(); tempPtr->deleteObject(); } mCurrentTask = NULL; } void AIConnection::addTask(AITask *task) { if (! task) return; mTaskList.push_back(task); } void AIConnection::removeTask(S32 id) { for (S32 i = 0; i < mTaskList.size(); i++) { if (mTaskList[i]->getId() == id) { if (mCurrentTask == mTaskList[i]) { mCurrentTask->retire(this); mCurrentTask = NULL; } mTaskList[i]->deleteObject(); mTaskList.erase(i); break; } } } void AIConnection::listTasks() { for (S32 i = 0; i < mTaskList.size(); i++) { Con::printf("%d: %s", mTaskList[i]->getId(), mTaskList[i]->getName()); } } void AIConnection::missionCycleCleanup() { setMoveMode(ModeStop); clearTasks(); clearStep(); mPath.forceSearch(); mPath.missionCycleCleanup(); }