$AI_PLAYER_ENABLED = true; //Are bots loaded when markers found on the map $AI_PLAYER_FIREDELAY = 500; //Delay from end of one firing pulse to begin of next shot. $AI_PLAYER_ENHANCED_FOV_TIME =3000; //Length of time bot is given 360deg vision when attacked. $AI_PLAYER_FOV =160; //The bots field of vision 160 means the bot sees -80 to +80 degrees //from it's eye vector. $AI_PLAYER_DETECT_DISTANCE =200; //Sets how close a target has to be before a bot will start reacting //to its presence. $AI_PLAYER_IGNORE_DISTANCE = 150; //A bot will ignore and not shoot at a target beyond this distance. $AI_PLAYER_SCANTIME =500; //This is the minimum time in milliseconds between scans for threats. $AI_PLAYER_CREATION_DELAY =5000; //This delay controls how long a bot will wait to start scanning for //threats upon creation. $AI_PLAYER_TRIGGER_DOWN = 100; //Sets how long the bot holds down the trigger when firing. For single //shot weapons a low value works fine, for continuous fire weapons the //larger the number, the larget the burst of fire will last. $AI_PLAYER_DEFAULTRESPAWN = true; //Sets whether bots respawn or not as a deault - can be overrode by setting //a dynamic variable called respawn in the map editor on a per bot basis. $AI_PLAYER_RESPAWN_DELAY = 20000; //Sets the delay in ms that a bot will wait before respawning. $AI_PLAYER_MAX_ATTENTION = 10; //This number and $AI_PLAYER_SCANTIME are multiplied to set the delay in the //thinking loop. Used to free up processor time on bots out of the mix. //My default values of 500ms min and 10 for an attention level means that a bot will //scan no more often than 500ms and no less than 5 seconds. //The Orc Marker is a simple shape that is placed in the map during design time. //The shape is replaced with an Orc at the shapes position, and the shape is hidden. //DO NOT set the shape to be removed upon level loading. If you do so, when you go in to //edit the map the shapes will have been deleted, and if you save the file your Orc markers will //not be saved and your markers will be lost. exec("~/data/shapes/Orc/Orc.cs"); datablock PlayerData(OrcBody) { renderFirstPerson = false; emap = true; className = Armor; shapeFile = "~/data/shapes/Orc/Orc.dts"; cameraMaxDist = 3; computeCRC = true; canObserve = true; cmdCategory = "Clients"; cameraDefaultFov = 90.0; cameraMinFov = 5.0; cameraMaxFov = 120.0; debrisShapeName = "~/data/shapes/Orc/debris_Orc.dts"; debris = playerDebris; aiAvoidThis = true; minLookAngle = -1.4; maxLookAngle = 1.4; maxFreelookAngle = 3.0; mass = 90; drag = 0.3; maxdrag = 0.4; density = 10; maxDamage = 100; maxEnergy = 60; repairRate = 0.33; energyPerDamagePoint = 75.0; rechargeRate = 0.256; runForce = 48 * 90; runEnergyDrain = 0; minRunEnergy = 0; maxForwardSpeed = 14; maxBackwardSpeed = 13; maxSideSpeed = 13; maxUnderwaterForwardSpeed = 7.4; maxUnderwaterBackwardSpeed = 6.8; maxUnderwaterSideSpeed = 6.8; jumpForce = 8.3 * 90; jumpEnergyDrain = 0; minJumpEnergy = 0; jumpDelay = 15; recoverDelay = 9; recoverRunForceScale = 1.2; minImpactSpeed = 45; speedDamageScale = 0.4; boundingBox = "1.2 1.2 2.3"; pickupRadius = 0.75; // Damage location details boxNormalHeadPercentage = 0.83; boxNormalTorsoPercentage = 0.49; boxHeadLeftPercentage = 0; boxHeadRightPercentage = 1; boxHeadBackPercentage = 0; boxHeadFrontPercentage = 1; // Foot Prints decalData = PlayerFootprint; decalOffset = 0.25; footPuffEmitter = LightPuffEmitter; footPuffNumParts = 10; footPuffRadius = 0.25; dustEmitter = LiftoffDustEmitter; splash = PlayerSplash; splashVelocity = 4.0; splashAngle = 67.0; splashFreqMod = 300.0; splashVelEpsilon = 0.60; bubbleEmitTime = 0.4; splashEmitter[0] = PlayerFoamDropletsEmitter; splashEmitter[1] = PlayerFoamEmitter; splashEmitter[2] = PlayerBubbleEmitter; mediumSplashSoundVelocity = 10.0; hardSplashSoundVelocity = 20.0; exitSplashSoundVelocity = 5.0; // Controls over slope of runnable/jumpable surfaces runSurfaceAngle = 70; jumpSurfaceAngle = 80; minJumpSpeed = 20; maxJumpSpeed = 30; horizMaxSpeed = 68; horizResistSpeed = 33; horizResistFactor = 0.35; upMaxSpeed = 80; upResistSpeed = 25; upResistFactor = 0.3; footstepSplashHeight = 0.35; //NOTE: some sounds commented out until wav's are available // Footstep Sounds FootSoftSound = FootLightSoftSound; FootHardSound = FootLightHardSound; FootMetalSound = FootLightMetalSound; FootSnowSound = FootLightSnowSound; FootShallowSound = FootLightShallowSplashSound; FootWadingSound = FootLightWadingSound; FootUnderwaterSound = FootLightUnderwaterSound; //FootBubblesSound = FootLightBubblesSound; //movingBubblesSound = ArmorMoveBubblesSound; //waterBreathSound = WaterBreathMaleSound; //impactSoftSound = ImpactLightSoftSound; //impactHardSound = ImpactLightHardSound; //impactMetalSound = ImpactLightMetalSound; //impactSnowSound = ImpactLightSnowSound; //impactWaterEasy = ImpactLightWaterEasySound; //impactWaterMedium = ImpactLightWaterMediumSound; //impactWaterHard = ImpactLightWaterHardSound; groundImpactMinSpeed = 10.0; groundImpactShakeFreq = "4.0 4.0 4.0"; groundImpactShakeAmp = "1.0 1.0 1.0"; groundImpactShakeDuration = 0.8; groundImpactShakeFalloff = 10.0; //exitingWater = ExitingWaterLightSound; observeParameters = "0.5 4.5 4.5"; // Allowable Inventory Items maxInv[BulletAmmo] = 20; maxInv[HealthKit] = 1; maxInv[RifleAmmo] = 100; maxInv[CrossbowAmmo] = 50; maxInv[Crossbow] = 1; maxInv[Rifle] = 1; }; datablock ItemData(OrcMarker) { //This shape will be listed under the Shapes in the mission editor category = "AIMarker"; // Basic Item properties //Basic shape file to use as a marker in the editor. shapeFile = "~/data/shapes/markers/octahedron.dts"; mass = 1; friction = 1; elasticity = 0.3; }; datablock PlayerData(Orc : OrcBody) { shootingDelay = 100; }; function Orc::onReachDestination(%this,%obj) { // Moves to the next node on the path. // Override for all player. Normally we'd override this for only // a specific player datablock or class of players. if (%obj.path !$= "") { if (%obj.currentNode == %obj.targetNode) %this.onEndOfPath(%obj,%obj.path); else %obj.moveToNextNode(); } } function Orc::onEndOfPath(%this,%obj,%path) { %obj.nextTask(); } function Orc::onEndSequence(%this,%obj,%slot) { echo("Sequence Done!"); %obj.stopThread(%slot); %obj.nextTask(); } function Orc::OnDamage(%this, %obj, %delta) { %obj.attentionlevel=1; %obj.enhancefov(%obj); if(%obj.getState() $= "Dead" && %obj.respawn == true) { %obj.delaybeforerespawn(%obj.botname, %obj.marker); %this.player=0; } } //The load entities function is called from game.cs //The call takes the form of // Orc::LoadEntities(); //It is placed in the game.cs file right after the part that reads // Start the game timer // if ($Game::Duration) // $Game::Schedule = schedule($Game::Duration * 1000, 0, "onGameDurationEnd" ); // $Game::Running = true; //The call uses a simple brute force method of finding all OrcMarkers //within the given radius of the center of the map. It then inserts an AI player at the //position of each marker, and then hides the marker from view. function Orc::LoadEntities() { //checks to see if the entities are to be processed. if ($AI_PLAYER_ENABLED == true) { echo("Loading Orc entities..."); %position = "0 0 0"; %radius = 100000.0; InitContainerRadiusSearch(%position, %radius, $TypeMasks::ItemObjectType); %i=0; while ((%targetObject = containerSearchNext()) != 0) { if (%targetobject.getDataBlock().getName() $= "OrcMarker") { %i++; %player = Orc::spawnAtMarker("" @ %i, %targetobject); %targetobject.sethidden(true); } } } else { echo("Patrol entities disabled..."); } } //OpenFire - This function is called when the Orc has located a target. function Orc::openfire(%this, %obj, %tgt) { //If the Orc is dead, or the target is dead, then the Orc //will not shoot, and the Orc's target will be cleared. if (%obj.getState() $= "Dead" || %tgt.player.getstate() $="Dead") { %firing = false; %obj.clearaim(); } else { //If the Orc was not firing, it will check the current distance to the target, //if the target is within the Orc's ignore distance it will shoot, otherwise //the Orc will not shoot. if(!%firing) { %rtt=vectorDist(%obj.getposition(), %tgt.player.getposition()); if(%rtt < $AI_PLAYER_IGNORE_DISTANCE) { %firing = true; //The following lines is added to keep the Orc stocked with ammo all the time. //If you want the Orc to run out of ammunition then remove this line. %obj.incinventory("Crossbowammo",100); //This sets the Orcs target to shoot at. It uses a VectorAdd to //help correct for the problem of the Orc shooting at the targets //feet. //I would recommend using a non-ballistic weapon for your Orcs //otherwise you may have to do some additional math to calculate a good //ballistics drop based on the Orcs range to the target. %obj.setAimLocation(VectorAdd(%tgt.player.getPosition(),"0 0 2.0")); //This actually starts the Orc shooting - equivalent to pulling and //holding down the trigger. %obj.setImageTrigger(0,true); } else { %obj.clearaim(); } } } //This schedule is set to call the ceasefire method at the end of the desired time //set for the Orc to hold down the trigger %this.trigger = %this.schedule($AI_PLAYER_TRIGGER_DOWN,"ceasefire", %obj); } //CeaseFire - this makes the Orc lift off the trigger function Orc::ceasefire(%this, %obj) { //Makes the Orc lift off the trigger %obj.setImageTrigger(0,false); //This schedule is created to implement a delay between the Orc's //shots. This can be used to help set how aggressive the bot is. %this.trigger = %this.schedule($AI_PLAYER_FIREDELAY,"delayfire", %obj); } function Orc::delayfire(%this, %obj) { //This clears the Orc's 'firing' flag. //This allows the player to shoot again the next time it //sees and selects a target within range. %firing = false; } //----------------------------------------------------------------------------- // Orc static functions //----------------------------------------------------------------------------- function Orc::spawn(%name, %obj) { // Create the demo player object %player = new Orc() { dataBlock = Orc; //This value is set to true for Orcs, it is referenced in player.cs //to help handle respawning bots properly. isbot=true; //These variables are set for each bot, this is done on a per bot basis //because the fov and attention level for each bot will be adjusted during //the game. //FOV = Bot's field of vision in degrees. (180 deg. means everything in front of the bot) //attentionlevel = The bot's attention level is raised or lowered based on how near targets //are and whether targets are visible or not. fov=$AI_PLAYER_FOV; attentionlevel = $AI_PLAYER_MAX_ATTENTION/2; //The following line allows gives access to the markers dynamic variables and it's spawn position. marker = %obj; //The pathname variable is a dynamic variable set during map editing. //This allows the designer to attach each bot to a seperate path path = %obj.pathname; botname = %name; }; MissionCleanup.add(%player); //The following set of if-then statements trap to see if the marker has a dynamic variable //called respawn. (Set during map editing.) //If it does then the players respawn flag is set accordingly true or false //as stated by the variable. //If there is no respawn variable set, then the flag is set to the default global value if (%obj.respawn $= "" ) { %player.respawn=$AI_PLAYER_DEFAULTRESPAWN; } else { if (%obj.respawn == true) %player.respawn=true; else %player.respawn=false; } //Calls equipbot - to set the Orcs initial inventory %player.EquipBot(%player); // %player.setShapeName(%name); %player.setTransform(%obj.gettransform()); %player.followPath(%obj.pathname,-1); //This schedule is set start the bots thinking cycle. //The creation delay is used so that the Orcs can be set to not start //thinking for a long delay while the rest of the game loads and initializes. %player.schedule($AI_PLAYER_CREATION_DELAY,"doscan", %player); return %player; } //The delay before respawn function is set to wait a specified duration before //respawning an Orc function Orc::DelayBeforeRespawn(%this, %name, %obj) { %this.respawntrigger = %this.schedule($AI_PLAYER_RESPAWN_DELAY,"respawn", %name, %obj); } //Equipbot - this function is called by spawn and respawn and is used to set the bot's //initial inventory. function Orc::EquipBot(%this, %obj) { %obj.mountImage(CrossBowImage,0); %obj.setInventory(CrossbowAmmo,1000); } // EnhanceFOV- This function sets the FOV for the Orc to 360 degrees //This function is called from player.cs when the Orc takes damage. //This has the same effect as having the Orc scan all around him when damaged. //This reduces the need to perform a lot of extra checks to see which vector the damage has //come from, etc... function Orc::EnhanceFOV(%this, %obj) { if (%obj.fov != 360) { %obj.fov = 360; %this.fovtrigger = %this.schedule($AI_PLAYER_ENHANCED_FOV_TIME,"restorefov", %obj); } } //RestoreFOV - this function resets the Orc's FOV back to the default value for the bot function Orc::restoreFOV(%this, %obj) { %obj.fov = $AI_PLAYER_FOV; } //This function is called when a bot is respawned, it is nearly identical to spawn //With the exception of the parameters passed to it. function Orc::respawn(%this, %name, %obj) { // Create the demo player object %player = new Orc() { dataBlock = PatrolPlayer; isbot=true; attentionlevel = $AI_PLAYER_MAX_ATTENTION/2; fov=$AI_PLAYER_FOV; marker = %obj; path = %obj.pathname; botname = %name; }; MissionCleanup.add(%player); %player.setShapeName(%name); %player.EquipBot(%player); %player.setTransform(%obj.gettransform()); %player.followPath(%obj.pathname,-1); if (%obj.respawn $="" ) { %player.respawn=$AI_PLAYER_DEFAULTRESPAWN; } else { if (%obj.respawn == true) %player.respawn=true; else %player.respawn=false; } %player.schedule($AI_PLAYER_CREATION_DELAY,"doscan", %player); return %player; } function Orc::spawnAtMarker(%name,%obj) { // Spawn a player and place him on the first node of the path if (!isObject(%obj)) return; %player = Orc::spawn(%name, %obj); return %player; } //----------------------------------------------------------------------------- // Orc methods //----------------------------------------------------------------------------- function Orc::followPath(%this,%path,%node) { // Start the player following a path %this.stopThread(0); if (!isObject(%path)) { %this.path = ""; return; } if (%node > %path.getCount() - 1) %this.targetNode = %path.getCount() - 1; else %this.targetNode = %node; if (%this.path $= %path) %this.moveToNode(%this.currentNode); else { %this.path = %path; %this.moveToNode(0); } } function Orc::moveToNextNode(%this) { if (%this.targetNode < 0 || %this.currentNode < %this.targetNode) { if (%this.currentNode < %this.path.getCount() - 1) %this.moveToNode(%this.currentNode + 1); else %this.moveToNode(0); } else if (%this.currentNode == 0) %this.moveToNode(%this.path.getCount() - 1); else %this.moveToNode(%this.currentNode - 1); } function Orc::moveToNode(%this,%index) { // Move to the given path node index %this.currentNode = %index; %node = %this.path.getObject(%index); %this.setMoveDestination(%node.getTransform(), %index == %this.targetNode); } //AITargeting // Return the angle of a vector in relation to world origin //This function comes from code taken from several resources on garagegames //The only mods to the code I made are in the end of the code to ensure that variable //returns the angle in degrees, and that the angle is within 0-360 degrees. //I've seen sets of this code with the function returng %degangle rather than //%angle - but in this script %angle is needed. function Orc::getAngleofVector(%this, %vec) { %vector = VectorNormalize(%vec); %vecx = getWord(%vector,0); %vecy = getWord(%vector,1); if(%vecx >= 0 && %vecy >= 0) %quad = 1; else if(%vecx >= 0 && %vecy < 0) %quad = 2; else if(%vecx < 0 && %vecy < 0) %quad = 3; else %quad = 4; %angle = mATan(%vecy/%vecx, -1); %degangle = mRadToDeg(%angle); switch(%quad) { case 1: %angle = %degangle-90; case 2: %angle = %degangle+270; case 3: %angle = %degangle+90; case 4: %angle = %degangle+450; } if (%angle < 0) %angle = %angle + 360; return %angle; } //This is another function taken from code off of garagegames. //The only mods I made to it was to add the extra check to ensure that the //angle is within the 0-360 range. function Orc::check2DAngletoTarget(%this, %obj, %tgt) { %eyeVec = VectorNormalize(%this.getEyeVector()); %eyeangle = %this.getAngleofVector(%eyeVec); %posVec = VectorSub(%tgt.player.getPosition(), %obj.getPosition()); %posangle = %this.getAngleofVector(%posVec); %angle = %posangle - %eyeAngle; %angle = %angle ? %angle : %angle * -1; if (%angle < 0) %angle = %angle + 360; return %angle; } //The DoScan function is the thinking part of the bot. //The following code is commented heavily to help explain the logic behind //the scripting. function Orc::DoScan(%this, %obj) { //Cancels the ailoop schedule cancel(%this.ailoop); //if the %obj object is invalid then exit the DoScan Loop. if (!%obj) { return; } //By cancelling the ailoop, then checking to see if the bot is valid, //This should cancel any errant ailoops left hanging by bots killed or //removed from the game. //The next line calls a function to locate the closest human client that is //both within the bot's range, within it's field of vision, and is not hidden by //objects in the environment. %tgtid = %this.GetClosestHumanInSightandRange(%obj); //Is the %tgtid valid target? if(%tgtid >=0) { //If yes, then get the player object of the client %tgt=ClientGroup.getobject(%tgtid); if (%obj.getaimobject() != %tgt) { //Open fire on the current target %this.openfire(%obj, %tgt); } } else { //Since there is no valid target, then make sure the Orc is //not firing, and clear it's target so it will face in a normal direction, //rather than tracking towards the target %obj.setImageTrigger(0,false); %obj.clearaim(); //The attention level is a value from 1 to MAX. The higher the number the //less attention the bot is paying. A level of 1 means that the bot scans //at it's fastest level. //The attention level is adjusted in GetClosestHumanInSightandRange //The next line ensures that the Orc's attention never goes above the default max if (%this.attentionlevel == $AI_PLAYER_MAX_ATTENTION) %obj.clearaim(); } //Sets the bot's next "DoScan" call - based on the default scan time adjusted by the //bot's attention level. %this.ailoop=%this.schedule($AI_PLAYER_SCANTIME * %this.attentionlevel,"DoScan" , %obj); } //This is another function taken from code found on garagegames. //It checks to see if there are any static objects blocking the view //from the Orc to the target. function Orc::CheckLOS(%this, %obj, %tgt) { %eyeTrans = %obj.getEyeTransform(); %eyeEnd = %tgt.player.getEyeTransform(); %searchResult = containerRayCast(%eyeTrans, %eyeEnd, $TypeMasks::PlayerObjectType | $TypeMasks::TerrainObjectType | $TypeMasks::InteriorObjectType, %obj); %foundObject = getword(%searchResult,0); if(%foundObject.getType() & $TypeMasks::PlayerObjectType) { return true; } else { return false; } } function Orc::GetClosestHumanInSightandRange(%this, %obj) { %index = -1; //sets the initial index value to -1 The index is the id number of the nearest //human target found %botpos = %this.getposition(); //The bots current position %count = ClientGroup.getCount(); //The number of clients to check //The for-next loop cycles through all of the valid clients for(%i=0; %i < %count; %i++) { %client = ClientGroup.getobject(%i); //Get the client info for the client at index %i //If the client is invalid then the function bails out returning a -1 value, for no //target found. if (%client.player $= "" || %client.player ==0) return -1; //The following line just changes the %client to %tgt to make it easier to follow the code below %tgt = %client; %playpos = %client.player.getposition(); //Assigns the player position to a variable %tempdist = vectorDist(%playpos, %botpos); //Determine the distance from the bot to the target //The first test we perform is to see if the target is within the bots range //Is target in range? If not bail out of checking to see if its in view. if (%tempdist <= $AI_PLAYER_DETECT_DISTANCE) { //Lower attentionlevel to increase response time... %this.attentionlevel--; if(%this.attentionlevel < 1) %this.attentionlevel=1; //The second check is to see if the target is within the FOV of the bot. //Is the target within the fov field of vision of the bot? if(%this.Istargetinview(%obj, %tgt, %obj.fov)) { //Lower attentionlevel to increase response time... %this.attentionlevel--; if(%this.attentionlevel < 1) %this.attentionlevel=1; //The third check we run is to see if there is anything blocking the //target from the bot. if(%this.CheckLOS(%obj, %tgt)) { //We lower the bots attention level again, to further increase it's //response time, this effectively means that the bot will respnd faster to //objects that are both in range and in plain sight. %this.attentionlevel--; //Prevent the attention level from dropping below 1 if(%this.attentionlevel < 1) %this.attentionlevel=1; //If there is no current target, then the index to the client/target //and distance to the target are set. The distance is saved temporarily to run //a comparison against the other clients to determine which is closest. if(%i == 0) { %dist = %tempdist; %index = %i; } else { //If there is a current target, then check the distance to the new target as //compared to the current set target. If the new target is closest, then set //the index and tempdistance to the new target. if(%dist > %tempdist) { %dist = %tempdist; %index = %i; } } } } } else { //If there are no targets in view, then the bots attention will slowly lapse and increase //This will slow down how fast the bot thinks and how often it checks for threats. %this.attentionlevel = %this.attentionlevel + 0.5; if(%this.attentionlevel > $AI_PLAYER_MAX_ATTENTION) %this.attentionlevel=$AI_PLAYER_MAX_ATTENTION; } } return %index; } //This function checks to see if the target supplied is within the bots FOV function Orc::IsTargetInView(%this, %obj, %tgt, %fov) { %ang = %this.check2dangletotarget(%obj, %tgt); %visleft = 360 - (%fov/2); %visright = %fov/2; if (%ang > %visleft || %ang < %visright) { return true; } else { return false; } } //----------------------------------------------------------------------------- function Orc::wait(%this,%time) { %this.schedule(%time * 1000,"nextTask"); } function Orc::done(%this,%time) { %this.schedule(0,"delete"); } function Orc::fire(%this,%bool) { if (%bool) { cancel(%this.trigger); %this.singleShot(); } else cancel(%this.trigger); %this.nextTask(); } function Orc::animate(%this,%seq) { //%this.stopThread(0); //%this.playThread(0,%seq); %this.setActionThread(%seq); }