diff --git a/scripts/DXAI/aicommander.cs b/scripts/DXAI/aicommander.cs index 577a66f..dadda0f 100644 --- a/scripts/DXAI/aicommander.cs +++ b/scripts/DXAI/aicommander.cs @@ -5,7 +5,7 @@ // // The AICommander type is a complex beast. They have the following proerties associated // with them: -// * %commander.botList: A SimSet of all bots that are currently associated with the +// * %commander.botList: A SimSet of all bots that are currently associated with the // given commander. // * %commander.idleBotList: A SimSet of all bots that are currently considered be idle. // These bots were not explicitly given anything to do by the commander AI and so they are @@ -13,7 +13,7 @@ // * %commander.botAssignments[%assignmentID]: An associative container that maps // assignment ID's (those desiginated by $DXAI::Priorities::*) to the total number of // bots assigned. -// * %commander.objectiveCycles[%assignmentID]: An associative container that maps assignment +// * %commander.objectiveCycles[%assignmentID]: An associative container that maps assignment // ID's (those desiginated by $DXAI::Priorities::*) to an instance of a CyclicSet which contains // the ID's of AI nav graph placed objective markers to allow for cycling through the objectives // set for the team. @@ -28,24 +28,26 @@ $DXAI::Priorities::DefendGenerator = 0; $DXAI::Priorities::DefendFlag = 1; $DXAI::Priorities::ScoutBase = 2; //----------------------------------------------- -$DXAI::Priorities::CaptureFlag = 4; +$DXAI::Priorities::CaptureFlag = 3; $DXAI::Priorities::CaptureObjective = 5; $DXAI::Priorities::AttackTurret = 6; -$DXAI::Priorities::Count = 3; +$DXAI::Priorities::Count = 4; //------------------------------------------------------------------------------------------ -// Description: These global variables are the default priorities that commanders will -// initialize with for specific tasks that can be distributed to the bots on the team. -// +// Description: These global variables are the default priorities that commanders will +// initialize with for specific tasks that can be distributed to the bots on the team. +// // NOTE: These should be fairly laid back initially and allow for a good count of idle bots. //------------------------------------------------------------------------------------------ $DXAI::Priorities::DefaultPriorityValue[$DXAI::Priorities::DefendGenerator] = 2; $DXAI::Priorities::DefaultPriorityValue[$DXAI::Priorities::DefendFlag] = 3; $DXAI::Priorities::DefaultPriorityValue[$DXAI::Priorities::ScoutBase] = 1; +$DXAI::Priorities::DefaultPriorityValue[$DXAI::Priorities::CaptureFlag] = 2; $DXAI::Priorities::Text[$DXAI::Priorities::DefendGenerator] = "Defending a Generator"; $DXAI::Priorities::Text[$DXAI::Priorities::DefendFlag] = "Defending the Flag"; $DXAI::Priorities::Text[$DXAI::Priorities::ScoutBase] = "Scouting a Location"; +$DXAI::Priorities::Text[$DXAI::Priorities::CaptureFlag] = "Capture the Flag"; //------------------------------------------------------------------------------------------ // Description: Sets up the AI commander by creating the bot list sim sets as well as @@ -53,32 +55,32 @@ $DXAI::Priorities::Text[$DXAI::Priorities::ScoutBase] = "Scouting a Location"; // independent ticks started up such as the visual acuity tick. //------------------------------------------------------------------------------------------ function AICommander::setup(%this) -{ +{ %this.botList = new SimSet(); %this.idleBotList = new SimSet(); - + for (%iteration = 0; %iteration < ClientGroup.getCount(); %iteration++) { %currentClient = ClientGroup.getObject(%iteration); - + if (%currentClient.isAIControlled() && %currentClient.team == %this.team) { %this.botList.add(%currentClient); %this.idleBotList.add(%currentClient); - + %currentClient.commander = %this; - + %currentClient.initialize(); %currentClient.visibleHostiles = new SimSet(); - + // Start our ticks. %currentClient.updateVisualAcuity(); %currentClient.stuckCheck(); } } - + %this.setDefaultPriorities(); - + // Also set the assignment tracker and the cyclers for each objective type for (%iteration = 0; %iteration < $DXAI::Priorities::Count; %iteration++) { @@ -100,7 +102,7 @@ function AICommander::_skimObjectiveGroup(%this, %group) for (%iteration = 0; %iteration < %group.getCount(); %iteration++) { %current = %group.getObject(%iteration); - + // We're getting ballsy here, recursion in TS! if (%current.getClassName() $= "SimGroup") %this._skimObjectiveGroup(%current); @@ -114,13 +116,13 @@ function AICommander::_skimObjectiveGroup(%this, %group) case "AIODefendLocation": // FIXME: Console spam from .targetObjectID not being set? %datablockName = %current.targetObjectID.getDatablock().getName(); - + // Defending the flag? if (%datablockName $= "FLAG") %this.objectiveCycles[$DXAI::Priorities::DefendFlag].add(%current); else if (%datablockName $="GeneratorLarge") - %this.objectiveCycles[$DXAI::Priorities::DefendGenerator].add(%current); - + %this.objectiveCycles[$DXAI::Priorities::DefendGenerator].add(%current); + case "AIORepairObject": case "AIOTouchObject": case "AIODeployEquipment": @@ -139,13 +141,13 @@ function AICommander::loadObjectives(%this) // First we clear the old cyclers for (%iteration = 0; %iteration < $DXAI::Priorities::Count; %iteration++) %this.objectiveCycles[%iteration].clear(); - + %teamGroup = "Team" @ %this.team; %teamGroup = nameToID(%teamGroup); - + if (!isObject(%teamGroup)) return; - + // Search this group for something named "AIObjectives". Each team has one, so we can't reliably just use that name %group = %teamGroup; for (%iteration = 0; %iteration < %group.getCount(); %iteration++) @@ -157,20 +159,20 @@ function AICommander::loadObjectives(%this) break; } } - + if (%group == %teamGroup) return; - + // Now that we have our objective set, skim it for anything usable %this._skimObjectiveGroup(%group); - + // We also need to determine some locations for objectives not involved in the original game, such as the AIEnhancedScout task. - + // Simply create a scout objective on the flag with a distance of 100m %scoutLocationObjective = new ScriptObject() { distance = 100; }; %defendFlagObjective = %this.objectiveCycles[$DXAI::Priorities::DefendFlag].next(); %scoutLocationObjective.location = %defendFlagObjective.location; - + %this.objectiveCycles[$DXAI::Priorities::ScoutBase].add(%scoutLocationObjective); } @@ -193,18 +195,19 @@ function AICommander::assignTasks(%this) %bot.addTask(AIEnhancedEngageTarget); %bot.addTask(AIEnhancedRearmTask); %bot.addTask(AIEnhancedPathCorrectionTask); - + // We only need this task if we're actually playing CTF. if ($CurrentMissionType $= "CTF") { %bot.addTask(AIEnhancedReturnFlagTask); %bot.addTask(AIEnhancedFlagCaptureTask); } - - %bot.targetLoadout = 0; + + // Assign the default loadout + %bot.targetLoadout = $DXAI::DefaultLoadout ; %bot.shouldRearm = true; } - + // Calculate how much priority we have total %totalPriority = 0.0; for (%iteration = 0; %iteration < $DXAI::Priorities::Count; %iteration++) @@ -212,10 +215,10 @@ function AICommander::assignTasks(%this) %totalPriority += %this.priorities[%iteration]; %botAssignments[%iteration] = 0; } - + // We create a priority queue preemptively so we can sort task priorities as we go and save a little bit of time %priorityQueue = PriorityQueue::create(); - + // Now calculate how many bots we need per objective, and count how many we will need in total %lostBots = false; // Used for a small optimization %botCountRequired = 0; @@ -232,7 +235,7 @@ function AICommander::assignTasks(%this) echo(%botAssignments[%iteration] SPC " bots on task " @ $DXAI::Priorities::Text[%iteration]); } } - + // Deassign from objectives we need less bots for now and put them into the idle list // When we lose bots, our %botAssignments[%task] value will be a negative, so we just need // to ditch mAbs(%botAssignments[%task]) bots from that given task. @@ -240,7 +243,7 @@ function AICommander::assignTasks(%this) // Need to ditch some bots if (%botAssignments[%taskIteration] < 0) %this.deassignBots(%taskIteration, mAbs(%botAssignments[%taskIteration])); - + // Do we have enough idle bots to just shunt everyone into something? if (%this.idleBotList.getCount() >= %botCountRequired) { @@ -259,7 +262,7 @@ function AICommander::assignTasks(%this) for (%botIteration = 0; %botIteration < %requiredBots && %this.idleBotList.getCount() != 0; %botIteration++) %this.assignTask(%taskID, %this.idleBotList.getObject(0)); } - + // Regardless, we need to make sure we cleanup the queue // FIXME: Perhaps just create one per commander and reuse it? %priorityQueue.delete(); @@ -281,7 +284,7 @@ function AICommander::deassignBots(%this, %taskID, %count) %count--; } } - + return %count == 0; } @@ -290,18 +293,18 @@ function AICommander::assignTask(%this, %taskID, %bot) // Don't try to assign if the bot is already assigned something if (!%this.idleBotList.isMember(%bot)) return; - + %this.idleBotList.remove(%bot); - + switch (%taskID) { case $DXAI::Priorities::DefendGenerator or $DXAI::Priorities::DefendFlag: %objective = %this.objectiveCycles[%taskID].next(); - + // Set the bot to defend the location %bot.defendTargetLocation = %objective.location; %datablockName = %objective.targetObjectID.getDatablock().getName(); - + switch$(%datablockName) { case "FLAG": @@ -309,18 +312,24 @@ function AICommander::assignTask(%this, %taskID, %bot) case "GeneratorLarge": %bot.defenseDescription = "generator"; } - - %bot.addTask("AIEnhancedDefendLocation"); - + + %bot.primaryTask = "AIEnhancedDefendLocation"; + %bot.addTask(%bot.primaryTask); + case $DXAI::Priorities::ScoutBase: %objective = %this.objectiveCycles[%taskID].next(); - + // Set the bot to defend the location %bot.scoutTargetLocation = %objective.location; %bot.scoutDistance = %objective.distance; - %bot.addTask("AIEnhancedScoutLocation"); + + %bot.primaryTask = "AIEnhancedScoutLocation"; + %bot.addTask(%bot.primaryTask); + + case $DXAI::Priorities::CaptureFlag: + %bot.shouldRunFlag = true; } - + %this.botAssignments[%taskID]++; %bot.assignment = %taskID; } @@ -336,7 +345,7 @@ function AICommander::setDefaultPriorities(%this) } //------------------------------------------------------------------------------------------ -// Description: Performs a deinitialization that should be ran before deleting the +// Description: Performs a deinitialization that should be ran before deleting the // commander object itself. // // NOTE: This is called automatically by .delete so this shouldn't have to be called @@ -350,7 +359,7 @@ function AICommander::cleanUp(%this) cancel(%current.visualAcuityTick); cancel(%current.stuckCheckTick); } - + %this.botList.delete(); %this.idleBotList.delete(); } @@ -379,7 +388,7 @@ function AICommander::removeBot(%this, %bot) { %this.botList.remove(%bot); %this.idleBotList.remove(%bot); - + %bot.commander = -1; } @@ -394,10 +403,10 @@ function AICommander::addBot(%this, %bot) { if (%bot.team != %this.team) return false; - + %this.botList.add(%bot); %this.idleBotList.add(%bot); - + %bot.commander = %this; return true; } @@ -409,7 +418,7 @@ function AICommander::notifyPlayerDeath(%this, %killedClient, %killedByClient) function AICommander::notifyFlagGrab(%this, %grabbedByClient) { %this.priority[$DXAI::Priorities::DefendFlag]++; - + // ...well, balls, someone nipped me flag! Are there any bots sitting around being lazy? // TODO: We should also include nearby scouting bots into this, as well. if (%this.idleBotList.getCount() != 0) @@ -421,4 +430,4 @@ function AICommander::notifyFlagGrab(%this, %grabbedByClient) %idleBot.attackTarget = %grabbedByClient.player; } } -} \ No newline at end of file +} diff --git a/scripts/DXAI/aiconnection.cs b/scripts/DXAI/aiconnection.cs index d3ba74b..ad7ac1b 100644 --- a/scripts/DXAI/aiconnection.cs +++ b/scripts/DXAI/aiconnection.cs @@ -1,11 +1,11 @@ //------------------------------------------------------------------------------------------ // aiconnection.cs // Source file declaring the custom AIConnection methods used by the DXAI experimental -// AI enhancement project. +// AI enhancement project. // https://github.com/Ragora/T2-DXAI.git // // Copyright (c) 2015 Robert MacGregor -// This software is licensed under the MIT license. +// This software is licensed under the MIT license. // Refer to LICENSE.txt for more information. //------------------------------------------------------------------------------------------ @@ -36,7 +36,7 @@ function AIConnection::update(%this) //------------------------------------------------------------------------------------------ // Description: Called by the main system when a hostile projectile impacts near the bot. -// This ideally is supposed to trigger some search logic instead of instantly knowing +// This ideally is supposed to trigger some search logic instead of instantly knowing // where the shooter is like the original AI did. // // NOTE: This is automatically called by the main system and therefore should not be called @@ -58,10 +58,21 @@ function AIConnection::isIdle(%this) { if (!isObject(%this.commander)) return true; - + return %this.commander.idleBotList.isMember(%this); } +function AIConnection::runJets(%this, %timeMS) +{ + %this.shouldJet = true; + %this.schedule(%timeMS, "_cancelJets"); +} + +function AIConnection::_cancelJets(%this) +{ + %this.shouldJet = false; +} + //------------------------------------------------------------------------------------------ // Description: Basically resets the entire state of the given AIConnection. It does not // unassign tasks, but it does reset the bot's current movement state. @@ -69,7 +80,7 @@ function AIConnection::isIdle(%this) function AIConnection::reset(%this) { // AIUnassignClient(%this); - + %this.stop(); // %this.clearTasks(); %this.clearStep(); @@ -80,14 +91,14 @@ function AIConnection::reset(%this) %this.setTargetObject(-1); %this.pilotVehicle = false; %this.defaultTasksAdded = false; - + if (isObject(%this.controlByHuman)) aiReleaseHumanControl(%this.controlByHuman, %this); } //------------------------------------------------------------------------------------------ // Description: Tells the AIConnection to move to a given position. They will automatically -// plot a path and attempt to navigate there. +// plot a path and attempt to navigate there. // Param %position: The target location to move to. If this is simply -1, then all current // moves will be cancelled. // @@ -104,13 +115,14 @@ function AIConnection::setMoveTarget(%this, %position) %this.isFollowingTarget = false; return; } - + %this.moveTarget = %position; %this.isMovingToTarget = true; %this.isFollowingTarget = false; + %this.setPath(%position); %this.stepMove(%position); - + %this.minimumPathDistance = 9999; %this.maximumPathDistance = -9999; } @@ -139,16 +151,16 @@ function AIConnection::setFollowTarget(%this, %target, %minDistance, %maxDistanc %this.isMovingToTarget = false; %this.isFollowingTarget = false; } - + if (!isObject(%target)) return; - + %this.followTarget = %target; %this.isFollowingTarget = true; %this.followMinDistance = %minDistance; %this.followMaxDistance = %maxDistance; %this.followHostile = %hostile; - + %this.stepEscort(%target); } @@ -165,22 +177,22 @@ function AIConnection::stuckCheck(%this) { if (isEventPending(%this.stuckCheckTick)) cancel(%this.stuckCheckTick); - + %targetDistance = %this.pathDistRemaining(9000); if (!%this.isMovingToTarget || !isObject(%this.player) || %this.player.getState() !$= "Move" || %targetDistance <= 5) { %this.stuckCheckTick = %this.schedule(5000, "stuckCheck"); return; } - + if (!%this.isPathCorrecting && %targetDistance >= %this.minimumPathDistance && %this.minimumPathDistance != 9999) - %this.isPathCorrecting = true; - + %this.isPathCorrecting = true; + if (%targetDistance > %this.maximumPathDistance) %this.maximumPathDistance = %targetDistance; if (%targetDistance < %this.minimumPathDistance) %this.minimumPathDistance = %targetDistance; - + %this.stuckCheckTick = %this.schedule(5000, "stuckCheck"); } @@ -197,11 +209,30 @@ function AIConnection::updateLegs(%this) %now = getSimTime(); %delta = %now - %this.lastUpdateLegs; %this.lastUpdateLegs = %now; - + + // Check the grenade set for anything we'll want to avoid (and can see) + for (%iteration = 0; %iteration < AIGrenadeSet.getCount(); %iteration++) + { + %grenade = AIGrenadeSet.getObject(%iteration); + + if (%this.player.canSeeObject(%grenade, 10, %this.fieldOfView)) + %this.dangerObjects.add(%grenade); + } + // Set any danger we may need. for (%iteration = 0; %iteration < %this.dangerObjects.getCount(); %iteration++) %this.setDangerLocation(%this.dangerObjects.getObject(%iteration).getPosition(), 3); - + + if (%this.shouldJet && !%this.player.isJetting()) + { + %this.pressJump(); + + if (!isEventPending(%this.jetSchedule)) + %this.jetSchedule = %this.schedule(128, "pressJet"); + } + else if (%this.shouldJet) + %this.pressJet(); + if (%this.isMovingToTarget) { if (%this.aimAtLocation) @@ -211,7 +242,7 @@ function AIConnection::updateLegs(%this) } else if (%this.isFollowingTarget) { - + } else { @@ -220,6 +251,14 @@ function AIConnection::updateLegs(%this) } } +function AITask::getWeightFreq(%this) { return %this.weightFreq; } + +function AITask::getMonitorFreq(%this) { return %this.monitorFreq; } + +function AITask::getWeightTimeMS(%this) { return %this.getWeightFreq() * 32; } + +function AITask::getMonitorTimeMS(%this) { return %this.getMonitorFreq() * 32; } + //------------------------------------------------------------------------------------------ // Description: A function called by the ::update function of the AIConnection that is // called once every 32ms by the commander AI logic to update the bot's current aiming & @@ -232,55 +271,55 @@ function AIConnection::updateWeapons(%this) { %lockedObject = %this.player; %mount = %this.player.getObjectMount(); - + if (isObject(%mount)) %lockedObject = %mount; - + // FIXME: Toss %this.player.lockedCount grenades, this will toss all of them basically instantly. if (%lockedObject.isLocked() && %this.player.invFlareGrenade != 0) { %this.pressGrenade(); } - + if (isObject(%this.engageTarget)) { %player = %this.player; %targetDistance = vectorDist(%player.getPosition(), %this.engageTarget.getPosition()); - + // Firstly, just aim at them for now %this.aimAt(%this.engageTarget.getPosition()); // What is our current best weapon? Right now we just check target distance and weapon spread. %bestWeapon = 0; - + for (%iteration = 0; %iteration < %player.weaponSlotCount; %iteration++) { %currentWeapon = %player.weaponSlot[%iteration]; %currentWeaponImage = %currentWeapon.image; - + // No ammo? if (%currentWeapon.usesAmmo && %this.player.inv[%currentWeapon.ammoDB] <= 0) continue; - + if (%targetDistance <= %currentWeapon.dryEffectiveRange) %bestWeapon = %iteration; // else if (%currentWeapon.spread < 3 && %targetDistance >= 20) // %bestWeapon = %iteration; // else if (%targetDistance >= 100 && %currentWeapon.projectileType $= "GrenadeProjectile") // %bestWeapon = %iteration; - + // Weapons with a decent bit of spread should be used <= 20m // Arced & precision Weapons should be used at >= 100m - + } - + %player.selectWeaponSlot(%bestWeapon); %this.pressFire(200); } } //------------------------------------------------------------------------------------------ -// Description: A function called randomly on time periods between +// Description: A function called randomly on time periods between // $DXAI::Bot::MinimumVisualAcuityTime and $DXAI::Bot::MaximumVisualAcuityTime which // attempts to simulate Human eyesight using a complex view cone algorithm implemented // entirely in Torque Script. @@ -296,100 +335,64 @@ function AIConnection::updateVisualAcuity(%this) { if (isEventPending(%this.visualAcuityTick)) cancel(%this.visualAcuityTick); - + // If we can't even see or if we're downright dead, don't do anything. if (%this.visibleDistance = 0 || !isObject(%this.player) || %this.player.getState() !$= "Move") { %this.visualAcuityTick = %this.schedule(getRandom($DXAI::Bot::MinimumVisualAcuityTime, $DXAI::Bot::MaximumVisualAcuityTime), "updateVisualAcuity"); return; } - - // The visual debug feature is a system in which we can use waypoints to view the bot's calculated viewcone per tick. - if (%this.enableVisualDebug) - { - if (!isObject(%this.originMarker)) - { - %this.originMarker = new Waypoint(){ datablock = "WaypointMarker"; team = %this.team; name = %this.namebase SPC " Origin"; }; - %this.clockwiseMarker = new Waypoint(){ datablock = "WaypointMarker"; team = %this.team; name = %this.namebase SPC " Clockwise"; }; - %this.counterClockwiseMarker = new Waypoint(){ datablock = "WaypointMarker"; team = %this.team; name = %this.namebase SPC " Counter Clockwise"; }; - %this.upperMarker = new Waypoint(){ datablock = "WaypointMarker"; team = %this.team; name = %this.namebase SPC " Upper"; }; - %this.lowerMarker = new Waypoint(){ datablock = "WaypointMarker"; team = %this.team; name = %this.namebase SPC " Lower"; }; - } - - %viewCone = %this.calculateViewCone(); - %coneOrigin = getWords(%viewCone, 0, 2); - %viewConeClockwiseVector = getWords(%viewCone, 3, 5); - %viewConeCounterClockwiseVector = getWords(%viewCone, 6, 8); - - %viewConeUpperVector = getWords(%viewCone, 9, 11); - %viewConeLowerVector = getWords(%viewCone, 12, 14); - - // Update all the markers - %this.clockwiseMarker.setPosition(%viewConeClockwiseVector); - %this.counterClockwiseMarker.setPosition(%viewConeCounterClockwiseVector); - %this.upperMarker.setPosition(%viewConeUpperVector); - %this.lowerMarker.setPosition(%viewConeLowerVector); - %this.originMarker.setPosition(%coneOrigin); - } - else if (isObject(%this.originMarker)) - { - %this.originMarker.delete(); - %this.clockwiseMarker.delete(); - %this.counterClockwiseMarker.delete(); - %this.upperMarker.delete(); - %this.lowerMarker.delete(); - } - + %now = getSimTime(); %deltaTime = %now - %this.lastVisualAcuityUpdate; %this.lastVisualAcuityUpdate = %now; - + %visibleObjects = %this.getObjectsInViewcone($TypeMasks::ProjectileObjectType | $TypeMasks::PlayerObjectType, %this.viewDistance, true); for (%iteration = 0; %iteration < %visibleObjects.getCount(); %iteration++) { %current = %visibleObjects.getObject(%iteration); - + %this.awarenessTime[%current] += %deltaTime; - + // Did we "notice" the object yet? %noticeTime = getRandom(700, 1200); if (%this.awarenessTime[%current] < %noticeTime) continue; - + // Is it a object we want to avoid? if (AIGrenadeSet.isMember(%current)) %this.dangerObjects.add(%current); - - if (%current.getType() & $TypeMasks::ProjectileObjectType) - { + + if (%current.getType() & $TypeMasks::ProjectileObjectType && %current.sourceObject != %this.player) + { %className = %current.getClassName(); - + // LinearFlareProjectile and LinearProjectile have linear trajectories, so we can easily determine if a dodge is necessary if (%className $= "LinearFlareProjectile" || %className $= "LinearProjectile") { //%this.setDangerLocation(%current.getPosition(), 20); - + // Perform a raycast to determine a hitpoint %currentPosition = %current.getPosition(); - %rayCast = containerRayCast(%currentPosition, vectorAdd(%currentPosition, vectorScale(%current.initialDirection, 200)), -1, 0); + %rayCast = containerRayCast(%currentPosition, vectorAdd(%currentPosition, vectorScale(%current.initialDirection, 200)), -1, 0); %hitObject = getWord(%raycast, 0); - + // We're set for a direct hit on us! if (%hitObject == %this.player) { %this.setDangerLocation(%current.getPosition(), 30); continue; } - + // If there is no radius damage, don't worry about it now if (!%current.getDatablock().hasDamageRadius) continue; - + // How close is the hit loc? %hitLocation = getWords(%rayCast, 1, 3); %hitDistance = vectorDist(%this.player.getPosition(), %hitLocation); - + // Is it within the radius damage of this thing? if (%hitDistance <= %current.getDatablock().damageRadius) %this.setDangerLocation(%current.getPosition(), 30); @@ -397,21 +400,22 @@ function AIConnection::updateVisualAcuity(%this) // A little bit harder to detect. else if (%className $= "GrenadeProjectile") { - + } } // See a player? else if (%current.getType() & $TypeMasks::PlayerObjectType && %current.client.team != %this.team) { %this.visibleHostiles.add(%current); + //%this.clientDetected(%current); // %this.clientDetected(%current.client); - + // ... if the moron is right there in our LOS then we probably should see them // %start = %this.player.getPosition(); // %end = vectorAdd(%start, vectorScale(%this.player.getEyeVector(), %this.viewDistance)); - - // %rayCast = containerRayCast(%start, %end, -1, %this.player); + + // %rayCast = containerRayCast(%start, %end, -1, %this.player); // %hitObject = getWord(%raycast, 0); // if (%hitObject == %current) @@ -421,12 +425,12 @@ function AIConnection::updateVisualAcuity(%this) // } } } - + // Now we run some logic on some things that we no longer can see. for (%iteration = 0; %iteration < %this.visibleHostiles.getCount(); %iteration++) { %current = %this.visibleHostiles.getObject(%iteration); - + if (%this.visibleHostiles.isMember(%current) && !%visibleObjects.isMember(%current)) { %this.awarenessTime[%current] -= %deltaTime; @@ -437,7 +441,7 @@ function AIConnection::updateVisualAcuity(%this) } } } - + %visibleObjects.delete(); %this.visualAcuityTick = %this.schedule(getRandom($DXAI::Bot::MinimumVisualAcuityTime, $DXAI::Bot::MaximumVisualAcuityTime), "updateVisualAcuity"); -} \ No newline at end of file +} diff --git a/scripts/DXAI/helpers.cs b/scripts/DXAI/helpers.cs index 98b4d1a..6b1888d 100644 --- a/scripts/DXAI/helpers.cs +++ b/scripts/DXAI/helpers.cs @@ -148,6 +148,77 @@ function GameConnection::getClosestInventory(%this) return %closestInventory; } +function Player::getFacingAngle(%this) +{ + %vector = vectorNormalize(%this.getMuzzleVector($WeaponSlot)); + return mAtan(getWord(%vector, 1), getWord(%vector, 0)); +} + +function Player::getViewConeIntersection(%this, %target, %maxDistance, %viewAngle) +{ + %myPosition = %this.getPosition(); + %targetPosition = %target.getPosition(); + + if (vectorDist(%myPosition, %targetPosition) > %maxDistance) + return false; + + %offset = vectorNormalize(vectorSub(%targetPosition, %myPosition)); + %enemyAngle = mAtan(getWord(%offset, 1), getWord(%offset, 0)); + %myAngle = %this.getFacingAngle(); + + %angle = %enemyAngle - %myAngle; + + %viewMax = %viewAngle / 2; + %viewMin = -(%viewAngle / 2); + + if (%angle >= %viewMin && %angle <= %viewMax) + return true; + + return false; +} + +function Player::getPackName(%this) +{ + %item = %this.getMountedImage(2).item; + + if (!isObject(%item)) + return ""; + + return %item.getName(); +} + +function Player::canSeeObject(%this, %target, %maxDistance, %viewAngle) +{ + if (%target.isCloaked() || !%this.getViewConeIntersection(%target, %maxDistance, %viewAngle)) + return false; + + %coneOrigin = %this.getPosition(); + + // Try to raycast the object + %rayCast = containerRayCast(%coneOrigin, %target.getWorldBoxCenter(), $TypeMasks::AllObjectType, %this); + %hitObject = getWord(%raycast, 0); + + // Since the engine doesn't do raycasts against projectiles & items correctly, we just check if the bot + // hit -nothing- when doing the raycast rather than checking for a hit against the object + %workaroundTypes = $TypeMasks::ProjectileObjectType | $TypeMasks::ItemObjectType; + + if (%hitObject == %target || (%target.getType() & %workaroundTypes && !isObject(%hitObject))) + return true; + + return false; +} + +function Precipitation::isCloaked(%this) { return false; } +function LinearProjectile::isCloaked(%this) { return false; } +function LinearFlareProjectile::isCloaked(%this) { return false; } +function GrenadeProjectile::isCloaked(%this) { return false; } +function SniperProjectile::isCloaked(%this) { return false; } + +function Player::getBackwardsVector(%this) +{ + return vectorScale(%this.getForwardVector(), -1); +} + //------------------------------------------------------------------------------------------ // Description: Calculates a list of objects that can be seen by the given client using // distance & field of view values passed in for evaluation. @@ -176,43 +247,22 @@ function GameConnection::getObjectsInViewcone(%this, %typeMask, %distance, %perf if (%distance $= "") %distance = %this.viewDistance; - %viewCone = %this.calculateViewCone(%distance); - - // Extract the results: See TODO above ::calculateViewCone implementation - %coneOrigin = getWords(%viewCone, 0, 2); - %viewConeClockwiseVector = getWords(%viewCone, 3, 5); - %viewConeCounterClockwiseVector = getWords(%viewCone, 6, 8); - %viewConeUpperVector = getWords(%viewCone, 9, 11); - %viewConeLowerVector = getWords(%viewCone, 12, 14); - %result = new SimSet(); + %coneOrigin = %this.player.getPosition(); + // Doing a radius search should hopefully be faster than iterating over all objects in MissionCleanup. // Even if the game did that internally it's definitely faster than doing it in TS InitContainerRadiusSearch(%coneOrigin, %distance, %typeMask); while((%currentObject = containerSearchNext()) != 0) { - if (%currentObject == %this || !isObject(%currentObject) || containerSearchCurrRadDamageDist() > %distance) + if (%currentObject == %this || !isObject(%currentObject) || containerSearchCurrRadDamageDist() > %distance || %currentObject.isCloaked()) continue; // Check if the object is within both the horizontal and vertical triangles representing our view cone - if (%currentObject.getType() & %typeMask && pointInTriangle(%currentObject.getPosition(), %viewConeClockwiseVector, %viewConeCounterClockwiseVector, %coneOrigin) && pointInTriangle(%currentObject.getPosition(), %viewConeLowerVector, %viewConeUpperVector, %coneOrigin)) - { - if (!%performLOSTest) + if (%currentObject.getType() & %typeMask && %this.player.getViewConeIntersection(%currentObject, %distance, %this.fieldOfView)) + if (!%performLOSTest || %this.player.canSeeObject(%currentObject, %distance, %this.fieldOfView)) %result.add(%currentObject); - else - { - %rayCast = containerRayCast(%coneOrigin, %currentObject.getWorldBoxCenter(), $TypeMasks::AllObjectType, %this.player); - - %hitObject = getWord(%raycast, 0); - - // Since the engine doesn't do raycasts against projectiles & items correctly, we just check if the bot - // hit -nothing- when doing the raycast rather than checking for a hit against the object - %workaroundTypes = $TypeMasks::ProjectileObjectType | $TypeMasks::ItemObjectType; - if (%hitObject == %currentObject || (%currentObject.getType() & %workaroundTypes && !isObject(%hitObject))) - %result.add(%currentObject); - } - } } return %result; @@ -237,7 +287,7 @@ function getRandomPosition(%position, %distance, %raycast) if (!%raycast) return %result; - %rayCast = containerRayCast(%position, %result, $TypeMasks::AllObjectType, 0); + %rayCast = containerRayCast(%position, %result, $TypeMasks::InteriorObjectType | $TypeMasks::StaticShapeObjectType, 0); %result = getWords(%raycast, 1, 3); return %result; @@ -257,6 +307,23 @@ function getRandomPositionOnTerrain(%position, %distance) return setWord(%result, 2, getTerrainHeight(%result)); } +function getRandomPositionInInterior(%position, %distance) +{ + %firstPass = getRandomPosition(%position, %distance, true); + + %rayCast = containerRayCast(%position, vectorAdd(%position, "0 0 -9000"), $TypeMasks::InteriorObjectType | $TypeMasks::StaticShapeObjectType, 0); + + if (%rayCast == -1) + return %firstPass; + + return getWords(%raycast, 1, 3); +} + +function getRandomFloat(%min, %max) +{ + return %min + (getRandom() * (%max - %min)); +} + //------------------------------------------------------------------------------------------ // Description: Multiplies two vectors together and returns the result. // Param %vec1: The first vector to multiply. diff --git a/scripts/DXAI/loadouts.cs b/scripts/DXAI/loadouts.cs index 685bd91..68740ee 100644 --- a/scripts/DXAI/loadouts.cs +++ b/scripts/DXAI/loadouts.cs @@ -9,23 +9,64 @@ //------------------------------------------------------------------------------------------ $DXAI::Loadouts[0, "Name"] = "Light Scout"; -$DXAI::Loadouts[0, "Weapon", 0] = ChainGun; +$DXAI::Loadouts[0, "Weapon", 0] = Chaingun; $DXAI::Loadouts[0, "Weapon", 1] = Disc; $DXAI::Loadouts[0, "Weapon", 2] = GrenadeLauncher; $DXAI::Loadouts[0, "Pack"] = EnergyPack; $DXAI::Loadouts[0, "WeaponCount"] = 3; $DXAI::Loadouts[0, "Armor"] = "Light"; -$DXAI::Loadouts[1, "Name"] = "Defender"; -$DXAI::Loadouts[1, "Weapon", 0] = ChainGun; +$DXAI::Loadouts[1, "Name"] = "Light Defender"; +$DXAI::Loadouts[1, "Weapon", 0] = Blaster; $DXAI::Loadouts[1, "Weapon", 1] = Disc; $DXAI::Loadouts[1, "Weapon", 2] = GrenadeLauncher; -$DXAI::Loadouts[1, "Weapon", 3] = GrenadeLauncher; -$DXAI::Loadouts[1, "Pack"] = AmmoPack; -$DXAI::Loadouts[1, "WeaponCount"] = 4; -$DXAI::Loadouts[1, "Armor"] = "Medium"; +$DXAI::Loadouts[1, "Pack"] = EnergyPack; +$DXAI::Loadouts[1, "WeaponCount"] = 3; +$DXAI::Loadouts[1, "Armor"] = "Light"; -$DXAI::OptimalLoadouts["AIEnhancedDefendLocation"] = "1"; +$DXAI::Loadouts[2, "Name"] = "Medium Defender"; +$DXAI::Loadouts[2, "Weapon", 0] = ChainGun; +$DXAI::Loadouts[2, "Weapon", 1] = Disc; +$DXAI::Loadouts[2, "Weapon", 2] = GrenadeLauncher; +$DXAI::Loadouts[2, "Weapon", 3] = Plasma; +$DXAI::Loadouts[2, "Pack"] = AmmoPack; +$DXAI::Loadouts[2, "WeaponCount"] = 4; +$DXAI::Loadouts[2, "Armor"] = "Medium"; -$DXAI::Loadouts::Count = 2; -$DXAI::Loadouts::Default = 0; \ No newline at end of file +$DXAI::Loadouts[3, "Name"] = "Heavy Defender"; +$DXAI::Loadouts[3, "Weapon", 0] = ChainGun; +$DXAI::Loadouts[3, "Weapon", 1] = Disc; +$DXAI::Loadouts[3, "Weapon", 2] = GrenadeLauncher; +$DXAI::Loadouts[3, "Weapon", 3] = Mortar; +$DXAI::Loadouts[3, "Weapon", 4] = Plasma; +$DXAI::Loadouts[3, "Pack"] = AmmoPack; +$DXAI::Loadouts[3, "WeaponCount"] = 5; +$DXAI::Loadouts[3, "Armor"] = "Heavy"; + +$DXAI::Loadouts[4, "Name"] = "Hardened Defender"; +$DXAI::Loadouts[4, "Weapon", 0] = ChainGun; +$DXAI::Loadouts[4, "Weapon", 1] = Disc; +$DXAI::Loadouts[4, "Weapon", 2] = GrenadeLauncher; +$DXAI::Loadouts[4, "Weapon", 3] = Mortar; +$DXAI::Loadouts[4, "Weapon", 4] = Plasma; +$DXAI::Loadouts[4, "Pack"] = ShieldPack; +$DXAI::Loadouts[4, "WeaponCount"] = 5; +$DXAI::Loadouts[4, "Armor"] = "Heavy"; + +$DXAI::Loadouts[5, "Name"] = "Cloaked Scout"; +$DXAI::Loadouts[5, "Weapon", 0] = Chaingun; +$DXAI::Loadouts[5, "Weapon", 1] = Disc; +$DXAI::Loadouts[5, "Weapon", 2] = GrenadeLauncher; +$DXAI::Loadouts[5, "Pack"] = CloakingPack; +$DXAI::Loadouts[5, "WeaponCount"] = 3; +$DXAI::Loadouts[5, "Armor"] = "Light"; + +$DXAI::OptimalLoadouts["AIEnhancedFlagCaptureTask"] = "0"; +$DXAI::OptimalLoadouts["AIEnhancedScoutLocation"] = "0 5"; +$DXAI::OptimalLoadouts["AIEnhancedDefendLocation"] = "2 3 4"; + +// A default loadout to use when the bot has no objective. +$DXAI::DefaultLoadout = 0; + +$DXAI::Loadouts::Count = 6; +$DXAI::Loadouts::Default = 0; diff --git a/scripts/DXAI/main.cs b/scripts/DXAI/main.cs index c09018b..80ecbba 100644 --- a/scripts/DXAI/main.cs +++ b/scripts/DXAI/main.cs @@ -14,8 +14,8 @@ exec("scripts/DXAI/aicommander.cs"); exec("scripts/DXAI/aiconnection.cs"); exec("scripts/DXAI/priorityqueue.cs"); exec("scripts/DXAI/cyclicset.cs"); -exec("scripts/DXAI/loadouts.cs"); exec("scripts/DXAI/weaponProfiler.cs"); +exec("scripts/DXAI/loadouts.cs"); //------------------------------------------------------------------------------------------ // Description: This cleanup function is called when the mission ends to clean up all @@ -25,10 +25,10 @@ exec("scripts/DXAI/weaponProfiler.cs"); function DXAI::cleanup() { $DXAI::System::Setup = false; - + for (%iteration = 1; %iteration < $DXAI::ActiveCommanderCount + 1; %iteration++) $DXAI::ActiveCommander[%iteration].delete(); - + $DXAI::ActiveCommanderCount = 0; } @@ -44,37 +44,37 @@ function DXAI::setup(%numTeams) // Mark the environment as invalidated for each new run so that our hooks // can be verified $DXAI::System::InvalidatedEnvironment = true; - + // Set our setup flag so that the execution hooks can behave correctly $DXAI::System::Setup = true; - + // Create the AIGrenadeSet to hold known grenades. new SimSet(AIGrenadeSet); - + for (%iteration = 1; %iteration < %numTeams + 1; %iteration++) { %commander = new ScriptObject() { class = "AICommander"; team = %iteration; }; %commander.setup(); - + $DXAI::ActiveCommander[%iteration] = %commander; %commander.loadObjectives(); %commander.assignTasks(); } - + // And setup the default values for (%iteration = 0; %iteration < ClientGroup.getCount(); %iteration++) { %currentClient = ClientGroup.getObject(%iteration); - + %currentClient.viewDistance = $DXAI::Bot::DefaultViewDistance; %currentClient.fieldOfView = $DXAI::Bot::DefaultFieldOfView; } - + $DXAI::ActiveCommanderCount = %numTeams; } //------------------------------------------------------------------------------------------ -// Why: Due to the way the AI system must hook into some functions and the way game +// Why: Due to the way the AI system must hook into some functions and the way game // modes work, we must generate runtime overrides for some gamemode related functions. We // can't simply hook DefaultGame functions base game modes will declare their own and so // we'll need to hook those functions post-start as the game mode scripts are executed for @@ -83,7 +83,7 @@ function DXAI::setup(%numTeams) // check that the hooks we need are actually active if the system detects that may be a // necessity to do so. A runtime check is initiated at gamemode start and for each exec // call made during runtime as any given exec can overwrite the hooks we required. -// If they were not overwritten, the function will return 11595 and do nothing else if the +// If they were not overwritten, the function will return 11595 and do nothing else if the // appropriate dummy parameters are passed in. // // TODO: Perhaps calculate %numTeams from the game object? @@ -91,29 +91,29 @@ function DXAI::setup(%numTeams) function DXAI::validateEnvironment() { %gameModeName = $CurrentMissionType @ "Game"; - - %payloadTemplate = %payload = "function " @ %gameModeName @ "::() { return DefaultGame::($DXAI::System::RuntimeDummy); } "; + + %boundPayloadTemplate = "function " @ %gameModeName @ "::() { return DefaultGame::($DXAI::System::RuntimeDummy); } "; + %payloadTemplate = "function () { return ($DXAI::System::RuntimeDummy); } "; if (game.AIChooseGameObjective($DXAI::System::RuntimeDummy) != 11595) { error("DXAI: Function 'DefaultGame::AIChooseGameObjective' detected to be overwritten by the current gamemode. Correcting ..."); - - eval(strReplace(%payloadTemplate, "", "AIChooseGameObjective")); - + + eval(strReplace(%boundPayloadTemplate, "", "AIChooseGameObjective")); + // Make sure the patch took if (game.AIChooseGameObjective($DXAI::System::RuntimeDummy) != 11595) error("DXAI: Failed to patch 'DefaultGame::AIChooseGameObjective'! DXAI may not function correctly."); } - - if (game.onAIRespawn($DXAI::System::RuntimeDummy) != 11595) + + if (onAIRespawn($DXAI::System::RuntimeDummy) != 11595) { - error("DXAI: Function 'DefaultGame::onAIRespawn' detected to be overwritten by the current gamemode. Correcting ... "); - + error("DXAI: Function 'onAIRespawn' detected to be overwritten by the current gamemode. Correcting ... "); eval(strReplace(%payloadTemplate, "", "onAIRespawn")); - + if (game.onAIRespawn($DXAI::System::RuntimeDummy) != 11595) - error("DXAI: Failed to patch 'DefaultGame::onAIRespawn'! DXAI may not function correctly."); + error("DXAI: Failed to patch 'onAIRespawn'! DXAI may not function correctly."); } - + $DXAI::System::InvalidatedEnvironment = false; } @@ -129,18 +129,18 @@ function DXAI::update() { if (isEventPending($DXAI::updateHandle)) cancel($DXAI::updateHandle); - + if (!isObject(Game)) return; - + // Check if the bound functions are overwritten by the current gamemode, or if something // may have invalidated our hooks if ($DXAI::System::InvalidatedEnvironment && $DXAI::System::Setup) DXAI::validateEnvironment(); - + for (%iteration = 1; %iteration < $DXAI::ActiveCommanderCount + 1; %iteration++) $DXAI::ActiveCommander[%iteration].update(); - + // Apparently we can't schedule a bound function otherwise $DXAI::updateHandle = schedule(32, 0, "eval", "DXAI::update();"); } @@ -151,6 +151,11 @@ function DXAI::notifyPlayerDeath(%killed, %killedBy) $DXAI::ActiveCommander[%iteration].notifyPlayerDeath(%killed, %killedBy); } +function DXAI::notifyFlagGrab(%grabbedBy, %flagTeam) +{ + $DXAI::ActiveCommander[%flagTeam].notifyFlagGrab(%grabbedBy); +} + //------------------------------------------------------------------------------------------ // Description: There is a series of functions that the AI code can safely hook without // worry of being overwritten implicitly such as the disconnect or exec functions. For @@ -167,10 +172,10 @@ package DXAI_Hooks function DefaultGame::gameOver(%game) { parent::gameOver(%game); - + DXAI::cleanup(); } - + //------------------------------------------------------------------------------------------ // Description: Called when the mission starts. We use this to perform initialization and // to start the update ticks. @@ -178,11 +183,11 @@ package DXAI_Hooks function DefaultGame::startMatch(%game) { parent::startMatch(%game); - + DXAI::setup(%game.numTeams); DXAI::update(); } - + //------------------------------------------------------------------------------------------ // Description: We hook the disconnect function as a step to fix console spam from leaving // a listen server due to the AI code continuing to run post-server shutdown in those @@ -191,10 +196,10 @@ package DXAI_Hooks function disconnect() { parent::disconnect(); - + DXAI::cleanup(); } - + //------------------------------------------------------------------------------------------ // Description: In the game, bots can be made to change teams which means we need to hook // this event so that commander affiliations can be properly updated. @@ -203,12 +208,12 @@ package DXAI_Hooks { // Remove us from the old commander's control first $DXAI::ActiveCommander[%client.team].removeBot(%client); - + parent::AIChangeTeam(%game, %client, %newTeam); - + $DXAI::ActiveCommander[%newTeam].addBot(%client); } - + //------------------------------------------------------------------------------------------ // Description: In the game, bots can be kicked like regular players so we hook this to // ensure that commanders are properly notified of lesser bot counts. @@ -217,25 +222,25 @@ package DXAI_Hooks { if (isObject(%client.commander)) %client.commander.removeBot(%client); - + parent::onAIDrop(%client); } - + // Hooks for AI System notification function DefaultGame::onClientKilled(%game, %clVictim, %clKiller, %damageType, %implement, %damageLocation) { parent::onClientKilled(%game, %clVictim, %clKiller, %damageType, %implement, %damageLocation); - + DXAI::notifyPlayerDeath(%clVictim, %clKiller); } - + function DefaultGame::onAIKilled(%game, %clVictim, %clKiller, %damageType, %implement) { parent::onAIKilled(%game, %clVictim, %clKiller, %damageType, %implement); - + DXAI::notifyPlayerDeath(%clVictim, %clKiller); } - + //------------------------------------------------------------------------------------------ // Description: We hook this function to implement some basic sound simulation for bots. // This means that if something explodes, a bot will hear it and if the sound is close @@ -244,37 +249,37 @@ package DXAI_Hooks function ProjectileData::onExplode(%data, %proj, %pos, %mod) { parent::onExplode(%data, %proj, %pos, %mod); - + // Look for any bots nearby InitContainerRadiusSearch(%pos, 100, $TypeMasks::PlayerObjectType); - + while ((%targetObject = containerSearchNext()) != 0) { %currentDistance = containerSearchCurrRadDamageDist(); - + if (%currentDistance > 100 || !%targetObject.client.isAIControlled()) continue; - + // Get the projectile team %projectileTeam = -1; if (isObject(%proj.sourceObject)) %projectileTeam = %proj.sourceObject.client.team; - + // Determine if we should run based on team & Team damage %shouldRun = false; if (isObject(%proj.sourceObject) && %projectileTeam == %targetObject.client.team && $TeamDamage) %shouldRun = true; else if (isObject(%proj.sourceObject) && %projectileTeam != %targetObject.client.team) %shouldRun = true; - + // Determine if we 'heard' it. The sound distance seems to be roughly 55m or less and we check the maxDistance // IIRC The 55m distance should scale with the min/max distances and volume but I'm not sure how those interact %heardHit = false; %hitDistance = vectorDist(%targetObject.getWorldBoxCenter(), %pos); - + if (%hitDistance <= 20 && %hitDistance <= %data.explosion.soundProfile.description.maxDistance) %heardHit = true; - + // If the thing has any radius damage (and we heard it), run around a little bit if we need to, and look at it for a bit if (%data.indirectDamage != 0 && %heardHit) { @@ -282,26 +287,26 @@ package DXAI_Hooks // TODO: Perhaps attempt to discern the direction of fire? %targetObject.client.aimAt(%pos); } - + // If we should care and it wasn't a teammate projectile, notify if (%shouldRun && %projectileTeam != %targetObject.client.team) %targetObject.client.notifyProjectileImpact(%data, %proj, %pos); } } - + //------------------------------------------------------------------------------------------ - // Description: This function is hooked so that we can try and guarantee that the DXAI + // Description: This function is hooked so that we can try and guarantee that the DXAI // gamemode hooks still exist in the runtime as game mode scripts are executed for each // mission load. //------------------------------------------------------------------------------------------ function CreateServer(%mission, %missionType) - { + { // Perform the default exec's parent::CreateServer(%mission, %missionType); - + // Ensure that the DXAI is active. DXAI::validateEnvironment(); - + // Run our profiler here as well. WeaponProfiler::run(false); } @@ -316,24 +321,34 @@ package DXAI_Hooks { AIGrenadeSet.add(%projectile); } - + // Make this do nothing so the bots don't ever get any objectives by default function DefaultGame::AIChooseGameObjective(%game, %client) { return 11595; } - - function DefaultGame::onAIRespawn(%game, %client) + + function onAIRespawn(%client) { - // Make sure the bot has no objectives - // %client.reset(); - // %client.defaultTasksAdded = true; + if (%client != $DXAI::System::RuntimeDummy) + parent::onAIRespawn(%client); + + // Clear the tasks and assign the default tasks + // FIXME: Assign tasks on a per-gamemode basis correctly + %client.clearTasks(); + %client.addTask(AIEnhancedEngageTarget); + %client.addTask(AIEnhancedRearmTask); + %client.addTask(AIEnhancedPathCorrectionTask); + %client.addTask(AIEnhancedReturnFlagTask); + %client.addTask(AIEnhancedFlagCaptureTask); + + %client.addTask(%client.primaryTask); + + %client.hasFlag = false; %client.shouldRearm = true; - %client.targetLoadout = 1; - %client.engageTargetLastPosition = ""; %client.engageTarget = -1; - + return 11595; } - + // We package hook the exec() and compile() functions to perform execution environment // checking because these can easily load code that overwrites methods that are otherwise // hooked by DXAI. This can happen with gamemode specific events because DXAI only hooks into @@ -343,13 +358,13 @@ package DXAI_Hooks $DXAI::System::InvalidatedEnvironment = true; parent::exec(%file); } - + function compile(%file) { $DXAI::System::InvalidatedEnvironment = true; parent::compile(%file); } - + function AIRespondToEvent(%client, %eventTag, %targetClient) { %clientPos = %client.player.getWorldBoxCenter(); @@ -359,45 +374,122 @@ package DXAI_Hooks schedule(2000, %targetClient, "AIPlayAnimSound", %targetClient, %clientPos, ObjectiveNameToVoice(%targetClient), $AIAnimSalute, $AIAnimSalute, 0); schedule(3700, %targetClient, "AIPlayAnimSound", %targetClient, %clientPos, "vqk.sorry", $AIAnimSalute, $AIAnimSalute, 0); } - + function AISystemEnabled(%enabled) { parent::AISystemEnabled(%enabled); - - echo(%enabled); $DXAI::AISystemEnabled = %enabled; } - + function AIConnection::onAIDrop(%client) { parent::onAIDrop(%client); - + if (isObject(%client.visibleHostiles)) %client.visibleHostiles.delete(); } - + + function CTFGame::flagCap(%game, %player) + { + parent::flagCap(%game, %player); + + %player.client.hasFlag = false; + } + + function CTFGame::playerTouchEnemyFlag(%game, %player, %flag) + { + parent::playerTouchEnemyFlag(%game, %player, %flag); + + // So you grabbed the flag eh? + %client = %player.client; + + if (%client.isAIControlled()) + { + // In case a bot picks up the flag that wasn't trying to run the flag + %client.shouldRunFlag = true; + + // Make sure he knows he has the flag so he can run home + %client.hasFlag = true; + } + + // Notify the AI Commander + DXAI::notifyFlagGrab(%client, %flag.team); + + return 11595; + } + + function AITask::setMonitorFreq(%this, %freq) + { + parent::setMonitorFreq(%this, %freq); + %this.monitorFreq = %freq; + } + + function AITask::setWeightFreq(%this, %freq) + { + parent::setWeightFreq(%this, %freq); + %this.weightFreq = %freq; + } + function Station::stationTriggered(%data, %obj, %isTriggered) { parent::stationTriggered(%data, %obj, %isTriggered); + %triggeringClient = %obj.triggeredBy.client; + + if (!isObject(%triggeringClient) || !%triggeringClient.isAIControlled()) + return; + // TODO: If the bot isn't supposed to be on the station, at least restock ammunition? // FIXME: Can bots trigger dead stations? - if (%isTriggered && %obj.triggeredBy.client.isAIControlled() && %obj.triggeredBy.client.shouldRearm) + if (%isTriggered && %triggeringClient.shouldRearm) { - %bot = %obj.triggeredBy.client; - - %bot.shouldRearm = false; - %bot.player.clearInventory(); - - %bot.player.setArmor($DXAI::Loadouts[%bot.targetLoadout, "Armor"]); - %bot.player.setInventory($DXAI::Loadouts[%bot.targetLoadout, "Pack"], 1, true); - - for (%iteration = 0; %iteration < $DXAI::Loadouts[%bot.targetLoadout, "WeaponCount"]; %iteration++) + %triggeringClient.shouldRearm = false; + %triggeringClient.player.clearInventory(); + + // Decide what the bot should pick + %targetLoadout = $DXAI::DefaultLoadout; + + if ($DXAI::OptimalLoadouts[%triggeringCLient.primaryTask] !$= "") { - %bot.player.setInventory($DXAI::Loadouts[%bot.targetLoadout, "Weapon", %iteration], 1, true); - %bot.player.setInventory($DXAI::Loadouts[%bot.targetLoadout, "Weapon", %iteration].Image.Ammo, 999, true); // TODO: Make it actually top out correctly! + %count = getWordCount($DXAI::OptimalLoadouts[%triggeringCLient.primaryTask]); + %targetLoadout = getWord($DXAI::OptimalLoadouts[%triggeringCLient.primaryTask], getRandom(0, %count - 1)); } + else if (%triggeringClient.primaryTask !$= "") + error("DXAI: Bot " @ %triggeringClient @ " used default loadout because his current task '" @ %triggeringClient.primaryTask @ "' has no recommended loadouts."); + else + error("DXAI: Bot " @ %triggeringClient @ " used default loadout because he no has task."); + + %triggeringClient.player.setArmor($DXAI::Loadouts[%targetLoadout, "Armor"]); + %triggeringClient.player.setInventory($DXAI::Loadouts[%targetLoadout, "Pack"], 1, true); + + for (%iteration = 0; %iteration < $DXAI::Loadouts[%targetLoadout, "WeaponCount"]; %iteration++) + { + %triggeringClient.player.setInventory($DXAI::Loadouts[%targetLoadout, "Weapon", %iteration], 1, true); + %ammoName = $DXAI::Loadouts[%targetLoadout, "Weapon", %iteration].Image.Ammo; + + // Assign the correct amount of ammo + // FIXME: Does this work with ammo packs? + %armor = %triggeringClient.player.getDatablock(); + if (%armor.max[%ammoName] $= "") + { + %maxAmmo = 999; + error("DXAI: Bot " @ %triggeringClient @ " given 999 units of '" @ %ammoName @ "' because the current armor '" @ %armor.getName() @ "' has no maximum set."); + } + else + %maxAmmo = %armor.max[%ammoName]; + + %triggeringClient.player.setInventory(%ammoName, %maxAmmo, true); + } + + %triggeringClient.currentLoadout = %targetLoadout; + + // Always use the first weapon + %triggeringClient.player.use($DXAI::Loadouts[%targetLoadout, "Weapon", 0]); } + + // Regardless, we want the bot to GTFO off the station when they can + // FIXME: This should be part of the rearm routine, pick a nearby random node before adjusting objective weight + %triggeringClient.schedule(2000, "setDangerLocation", %obj.getPosition(), 20); } }; diff --git a/scripts/DXAI/objectives.cs b/scripts/DXAI/objectives.cs index 9db9719..fbaf61c 100644 --- a/scripts/DXAI/objectives.cs +++ b/scripts/DXAI/objectives.cs @@ -1,6 +1,6 @@ //------------------------------------------------------------------------------------------ // main.cs -// Source file for the DXAI enhanced objective implementations. +// Source file for the DXAI enhanced objective implementations. // https://github.com/Ragora/T2-DXAI.git // // Copyright (c) 2015 Robert MacGregor @@ -12,14 +12,14 @@ $DXAI::Task::LowPriority = 100; $DXAI::Task::MediumPriority = 200; $DXAI::Task::HighPriority = 500; $DXAI::Task::VeryHighPriority = 1000; -$DXAI::Task::ReservedPriority = 5000; +$DXAI::Task::ReservedPriority = 5000; //------------------------------------------------------------------------------------------ // +Param %bot.escortTarget: The ID of the object to escort. This can be literally // any object that exists in the game world. // +Description: The AIEnhancedDefendLocation does exactly as the name implies. The // behavior a bot will exhibit with this code is that the bot will attempt to first to -// the location desiginated by %bot.defendLocation. Once the bot is in range, it will +// the location desiginated by %bot.defendLocation. Once the bot is in range, it will // idly step about near the defense location, performing a sort of short range scouting. // If the bot were to be knocked too far away, then this logic will simply start all over // again. @@ -30,20 +30,20 @@ function AIEnhancedEscort::retire(%task, %client) { %client.isMoving = false; %c function AIEnhancedEscort::weight(%task, %client) { %task.setWeight($DXAI::Task::MediumPriority); } function AIEnhancedEscort::monitor(%task, %client) -{ +{ // Is our escort object even a thing? if (!isObject(%client.escortTarget)) return; - + %escortLocation = %client.escortTarget.getPosition(); - + // Pick a location near the target // FIXME: Only update randomly every so often, or perhaps update using the target's move direction & velocity? // TODO: Keep a minimum distance from the escort target, prevents crowding and accidental vehicular death. %client.isMoving = true; %client.manualAim = true; %client.aimLocation = %escortLocation; - + %client.setMoveTarget(getRandomPositionOnTerrain(%escortLocation, 40)); } @@ -52,7 +52,7 @@ function AIEnhancedEscort::monitor(%task, %client) // must attempt to defend. // +Description: The AIEnhancedDefendLocation does exactly as the name implies. The // behavior a bot will exhibit with this code is that the bot will attempt to first to -// the location desiginated by %bot.defendLocation. Once the bot is in range, it will +// the location desiginated by %bot.defendLocation. Once the bot is in range, it will // idly step about near the defense location, performing a sort of short range scouting. // If the bot were to be knocked too far away, then this logic will simply start all over // again. @@ -63,7 +63,7 @@ function AIEnhancedDefendLocation::retire(%task, %client) { %client.isMoving = f function AIEnhancedDefendLocation::weight(%task, %client) { %task.setWeight($DXAI::Task::MediumPriority); } function AIEnhancedDefendLocation::monitor(%task, %client) -{ +{ if (%client.getPathDistance(%client.defendTargetLocation) <= 40) { // Pick a random time to move to a nearby location @@ -72,19 +72,22 @@ function AIEnhancedDefendLocation::monitor(%task, %client) %client.nextDefendRotation = getRandom(5000, 10000); %client.setMoveTarget(-1); } - + // If we're near our random point, just don't move if (%client.getPathDistance(%client.moveLocation) <= 10) %client.setMoveTarget(-1); - - %client.defendTime += 1024; + + %client.defendTime += %task.getMonitorTimeMS(); if (%client.defendTime >= %client.nextDefendRotation) { %client.defendTime = 0; %client.nextDefendRotation = getRandom(5000, 10000); - - // TODO: Replace with something that detects interiors as well - %client.setMoveTarget(getRandomPositionOnTerrain(%client.defendTargetLocation, 40)); + + %nextPosition = NavGraph.nodeLoc(NavGraph.randNode(%client.player.getPosition(), 40, true, true)); + + // If it isn't far enough, just pass on moving. This will help prevent bots jumping up in the air randomly. + if (vectorDist(%client.player.getPosition(), %nextPosition) > 5) + %client.setMoveTarget(%nextPosition); } } else @@ -114,16 +117,16 @@ function AIEnhancedScoutLocation::monitor(%task, %client) { if (%client.engageTarget) return AIEnhancedScoutLocation::monitorEngage(%task, %client); - + // We can't really work without a NavGraph if (!isObject(NavGraph)) return; - + // We just received the task, so find a node within distance of our scout location if (%client.currentNode == -1) { %client.currentNode = NavGraph.randNode(%client.scoutTargetLocation, %client.scoutDistance, true, true); - + if (%client.currentNode != -1) %client.setMoveTarget(NavGraph.nodeLoc(%client.currentNode)); } @@ -131,13 +134,13 @@ function AIEnhancedScoutLocation::monitor(%task, %client) else { %pathDistance = %client.getPathDistance(%client.moveTarget); - + // Don't move if we're close enough to our next node if (%pathDistance <= 40 && %client.isMovingToTarget) { %client.setMoveTarget(-1); %client.nextScoutRotation = getRandom(5000, 10000); - %client.scoutTime += 1024; + %client.scoutTime += %task.getMonitorTimeMS(); } else if(%client.getPathDistance(%client.moveTarget) > 40) { @@ -145,8 +148,8 @@ function AIEnhancedScoutLocation::monitor(%task, %client) %client.scoutTime = 0; } else - %client.scoutTime += 1024; - + %client.scoutTime += %task.getMonitorTimeMS(); + // Wait a little bit at each node if (%client.scoutTime >= %client.nextScoutRotation) { @@ -155,12 +158,12 @@ function AIEnhancedScoutLocation::monitor(%task, %client) // Pick a new node %client.currentNode = NavGraph.randNode(%client.scoutTargetLocation, %client.scoutDistance, true, true); - + // Ensure that we found a node. if (%client.currentNode != -1) %client.setMoveTarget(NavGraph.nodeLoc(%client.currentNode)); } - } + } } function AIEnhancedScoutLocation::monitorEngage(%task, %client) @@ -180,34 +183,62 @@ function AIEnhancedEngageTarget::initFromObjective(%task, %objective, %client) { function AIEnhancedEngageTarget::assume(%task, %client) { %task.setMonitorFreq(1); } function AIEnhancedEngageTarget::retire(%task, %client) { } -function AIEnhancedEngageTarget::weight(%task, %client) +function AIEnhancedEngageTarget::weight(%task, %client) { // Blow through seen targets %chosenTarget = -1; %chosenTargetDistance = 9999; - + %botPosition = %client.player.getPosition(); for (%iteration = 0; %iteration < %client.visibleHostiles.getCount(); %iteration++) { %current = %client.visibleHostiles.getObject(%iteration); - + %targetDistance = vectorDist(%current.getPosition(), %botPosition); - if (%targetDistance < %chosenTargetDistance) + + // FIXME: We immediately forget about the asshole here + if (%targetDistance < %chosenTargetDistance && %client.player.canSeeObject(%current, %client.viewDistance, %client.fieldOfView)) { %chosenTargetDistance = %targetDistance; %chosenTarget = %current; } } - + %client.engageTarget = %chosenTarget; if (!isObject(%client.engageTarget) && %client.engageTargetLastPosition $= "") + { %task.setWeight($DXAI::Task::NoPriority); + + // Make sure we disable the pack + if (%client.player.getPackName() $= "ShieldPack") + { + %client.player.setImageTrigger(2, false); + %client.rechargingEnergy = false; + } + } else + { %task.setWeight($DXAI::Task::VeryHighPriority); + + // If we have a shield pack on, use it + if (%client.player.getPackName() $= "ShieldPack" && %client.player.getEnergyLevel() >= 30 && !%client.rechargingEnergy) + %client.player.setImageTrigger(2, true); + else if (%client.player.getPackName() $= "ShieldPack" && %client.player.getEnergyLevel() >= 70) + { + %client.player.setImageTrigger(2, true); + %client.rechargingEnergy = false; + } + else if (%client.player.getPackName() $= "ShieldPack") + { + // We ran out of energy, let the pack recharge for a bit + %client.player.setImageTrigger(2, false); + %client.rechargingEnergy = true; + } + } } function AIEnhancedEngageTarget::monitor(%task, %client) -{ +{ if (isObject(%client.engageTarget)) { if (%client.engageTarget.getState() !$= "Move") @@ -216,10 +247,61 @@ function AIEnhancedEngageTarget::monitor(%task, %client) %client.engageTargetLastPosition = ""; return; } - - // %client.engageTargetLastPosition = %client.engageTarget.getWorldBoxCenter(); - // %client.setMoveTarget(getRandomPositionOnTerrain(%client.engageTargetLastPosition, 40)); - //%client.pressFire(); + + // Calculate the T between the target and this bot + %normal = vectorNormalize(vectorSub(%client.engageTarget.getWorldBoxCenter(), %client.player.getWorldBoxCenter())); + + %forwardAngle = mAtan(getWord(%normal, 1), getWord(%normal, 0)); + %horizontalMaxAngle = %forwardAngle + mDegToRad(90); + %horizontalMinAngle = %forwardAngle - mDegToRad(90); + + %randomAngle = getRandomFloat(%horizontalMaxAngle, %horizontalMinAngle); + + // FIXME: Maintain weapon distance + %minDist = 20; + %maxDist = 30; + + %distance = getRandom(%minDist, %maxDist); + + // Calculate a final point + %normal = mSin(%randomAngle) SPC mCos(%randomAngle); + + %targetPoint = vectorAdd(%client.engageTarget.getWorldBoxCenter(), vectorScale(%normal, %distance)); + %targetPoint = setWord(%targetPoint, 2, getTerrainHeight(%targetPoint)); + + %targetPoint = vectorAdd(%targetPoint, "0 0 2"); + %client.engageTargetLastPosition = %client.engageTarget.getWorldBoxCenter(); + + if (%client.engageJetTiming $= "") + %client.engageJetTiming = getRandom(500, 10000); + else + %client.engageJetTime += %task.getMonitorTimeMS(); + + if (%client.engageJetTime >= %client.engageJetTiming && %client.player.getEnergyPercent() >= 0.5) + { + %client.combatJetTiming = getRandom(1000, 1500) + %client.engageJetTiming; + %client.runJets(%client.combatJetTiming); + + %client.engageJetTiming = ""; + %client.engageJetTime = 0; + + %client.isCombatJetting = true; + + %client.setMoveTarget(vectorScale(%client.engageTarget.getBackwardsVector(), 20)); + } + + if (%client.isCombatJetting && %client.combatJetTime < %client.combatJetTiming && %client.player.getEnergyPercent() >= 0.2) + { + %client.combatJetTime += %task.getMonitorTimeMS(); + %client.setMoveTarget(vectorScale(%client.engageTarget.getBackwardsVector(), 20)); + } + else + { + %client.setMoveTarget(%targetPoint); + + %client.combatJetTime = 0; + %client.isCombatJetting = false; + } } else if (%client.engageTargetLastPosition !$= "") { @@ -244,21 +326,21 @@ function AIEnhancedRearmTask::initFromObjective(%task, %objective, %client) { } function AIEnhancedRearmTask::assume(%task, %client) { %task.setMonitorFreq(32); } function AIEnhancedRearmTask::retire(%task, %client) { } -function AIEnhancedRearmTask::weight(%task, %client) +function AIEnhancedRearmTask::weight(%task, %client) { if (%client.shouldRearm) %task.setWeight($DXAI::Task::HighPriority); else %task.setWeight($DXAI::Task::NoPriority); - + %task.setMonitorFreq(getRandom(10, 32)); } function AIEnhancedRearmTask::monitor(%task, %client) -{ +{ if (!isObject(%client.rearmTarget)) %client.rearmTarget = %client.getClosestInventory(); - + if (isObject(%client.rearmTarget)) { // Politely wait if someone is already on it. @@ -279,7 +361,7 @@ function AIEnhancedReturnFlagTask::initFromObjective(%task, %objective, %client) function AIEnhancedReturnFlagTask::assume(%task, %client) { %task.setMonitorFreq(32); } function AIEnhancedReturnFlagTask::retire(%task, %client) { } -function AIEnhancedReturnFlagTask::weight(%task, %client) +function AIEnhancedReturnFlagTask::weight(%task, %client) { %flag = nameToID("Team" @ %client.team @ "Flag1"); if (!isObject(%flag) || %flag.isHome) @@ -290,16 +372,16 @@ function AIEnhancedReturnFlagTask::weight(%task, %client) else { // TODO: For now, all the bots go after it! Make this check if the bot is range. - %task.setWeight($DXAI::Task::HighPriority); - %client.returnFlagTarget = %flag; + %task.setWeight($DXAI::Task::HighPriority); + %client.returnFlagTarget = %flag; } } function AIEnhancedReturnFlagTask::monitor(%task, %client) -{ +{ if (!isObject(%client.returnFlagTarget)) return; - + if (isObject(%client.engageTarget) && %client.engageTarget.getState() $= "Move") AIEnhancedReturnFlagTask::monitorEngage(%task, %client); else @@ -319,7 +401,7 @@ function AIEnhancedPathCorrectionTask::initFromObjective(%task, %objective, %cli function AIEnhancedPathCorrectionTask::assume(%task, %client) { %task.setMonitorFreq(2); } function AIEnhancedPathCorrectionTask::retire(%task, %client) { } -function AIEnhancedPathCorrectionTask::weight(%task, %client) +function AIEnhancedPathCorrectionTask::weight(%task, %client) { if (%client.isPathCorrecting) %task.setWeight($DXAI::Task::VeryHighPriority); @@ -328,7 +410,7 @@ function AIEnhancedPathCorrectionTask::weight(%task, %client) } function AIEnhancedPathCorrectionTask::monitor(%task, %client) -{ +{ if (%client.isPathCorrecting) { if (%client.player.getEnergyPercent() >= 1) @@ -336,7 +418,7 @@ function AIEnhancedPathCorrectionTask::monitor(%task, %client) else %client.setMoveTarget(-1); } - + } //------------------------------------------------------------------------------------------ @@ -347,14 +429,14 @@ function AIEnhancedFlagCaptureTask::initFromObjective(%task, %objective, %client function AIEnhancedFlagCaptureTask::assume(%task, %client) { %task.setMonitorFreq(1); } function AIEnhancedFlagCaptureTask::retire(%task, %client) { } -function AIEnhancedFlagCaptureTask::weight(%task, %client) +function AIEnhancedFlagCaptureTask::weight(%task, %client) { if (%client.shouldRunFlag) { // First, is the enemy flag home? %enemyTeam = %client.team == 1 ? 2 : 1; %enemyFlag = nameToID("Team" @ %enemyTeam @ "Flag1"); - + if (isObject(%enemyFlag) && %enemyFlag.isHome) { %client.targetCaptureFlag = %enemyFlag; @@ -366,11 +448,11 @@ function AIEnhancedFlagCaptureTask::weight(%task, %client) } function AIEnhancedFlagCaptureTask::monitor(%task, %client) -{ - if (!isObject(%client.targetCaptureFlag)) +{ + if (!isObject(%client.targetCaptureFlag) && !%client.hasFlag) return; - - if (%client.targetCaptureFlag.getObjectMount() != %client.player) + + if (!%client.hasFlag) %client.setMoveTarget(%client.targetCaptureFlag.getPosition()); else %client.setMoveTarget(nameToID("Team" @ %client.team @ "Flag1").getPosition()); @@ -380,7 +462,7 @@ function AIEnhancedFlagCaptureTask::monitor(%task, %client) function ObjectiveNameToVoice(%bot) { %objective = %bot.getTaskName(); - + %result = "avo.grunt"; switch$(%objective) { @@ -405,6 +487,6 @@ function ObjectiveNameToVoice(%bot) case "AIEnhancedEscort": %result = "slf.tsk.cover"; } - + return %result; -} \ No newline at end of file +}