From 52c260043e7c7f7103135f47d716eb66e81e938d Mon Sep 17 00:00:00 2001 From: Robert MacGregor Date: Wed, 29 Jun 2016 00:49:44 -0400 Subject: [PATCH 1/3] Commanders receive flag grab notifications; bots will dodge grenades but only if they see them; fix bots ignoring cloak but now cloak is OP against bots; fixed some issues with defense bots jumping in place randomly; optimize viewcone implementation; implement flag runner bots; randomize loadout selection based on task; make bots rearm after every spawn; make bots use shield packs in combat if they have any; make bots select the first weapon they have after rearming --- scripts/DXAI/aicommander.cs | 117 ++++++++-------- scripts/DXAI/aiconnection.cs | 154 +++++++++------------ scripts/DXAI/helpers.cs | 110 +++++++++++---- scripts/DXAI/loadouts.cs | 61 +++++++-- scripts/DXAI/main.cs | 254 +++++++++++++++++++++++------------ scripts/DXAI/objectives.cs | 133 +++++++++++------- 6 files changed, 510 insertions(+), 319 deletions(-) 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..96e50b6 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,7 +58,7 @@ function AIConnection::isIdle(%this) { if (!isObject(%this.commander)) return true; - + return %this.commander.idleBotList.isMember(%this); } @@ -69,7 +69,7 @@ function AIConnection::isIdle(%this) function AIConnection::reset(%this) { // AIUnassignClient(%this); - + %this.stop(); // %this.clearTasks(); %this.clearStep(); @@ -80,14 +80,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 +104,13 @@ 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 +139,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 +165,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 +197,20 @@ 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.isMovingToTarget) { if (%this.aimAtLocation) @@ -211,7 +220,7 @@ function AIConnection::updateLegs(%this) } else if (%this.isFollowingTarget) { - + } else { @@ -232,55 +241,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 +305,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) - { + { %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 +370,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 +395,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 +411,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..830dfca 100644 --- a/scripts/DXAI/helpers.cs +++ b/scripts/DXAI/helpers.cs @@ -148,6 +148,71 @@ 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; } + //------------------------------------------------------------------------------------------ // 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 +241,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 +281,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 +301,18 @@ 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); +} + //------------------------------------------------------------------------------------------ // 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..92a0175 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,110 @@ 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 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..22aa367 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; 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,7 +134,7 @@ 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) { @@ -146,7 +149,7 @@ function AIEnhancedScoutLocation::monitor(%task, %client) } else %client.scoutTime += 1024; - + // 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,9 +247,9 @@ function AIEnhancedEngageTarget::monitor(%task, %client) %client.engageTargetLastPosition = ""; return; } - - // %client.engageTargetLastPosition = %client.engageTarget.getWorldBoxCenter(); - // %client.setMoveTarget(getRandomPositionOnTerrain(%client.engageTargetLastPosition, 40)); + + // %client.engageTargetLastPosition = %client.engageTarget.getWorldBoxCenter(); + // %client.setMoveTarget(getRandomPositionOnTerrain(%client.engageTargetLastPosition, 40)); //%client.pressFire(); } else if (%client.engageTargetLastPosition !$= "") @@ -244,21 +275,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 +310,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 +321,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 +350,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 +359,7 @@ function AIEnhancedPathCorrectionTask::weight(%task, %client) } function AIEnhancedPathCorrectionTask::monitor(%task, %client) -{ +{ if (%client.isPathCorrecting) { if (%client.player.getEnergyPercent() >= 1) @@ -336,7 +367,7 @@ function AIEnhancedPathCorrectionTask::monitor(%task, %client) else %client.setMoveTarget(-1); } - + } //------------------------------------------------------------------------------------------ @@ -347,14 +378,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 +397,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 +411,7 @@ function AIEnhancedFlagCaptureTask::monitor(%task, %client) function ObjectiveNameToVoice(%bot) { %objective = %bot.getTaskName(); - + %result = "avo.grunt"; switch$(%objective) { @@ -405,6 +436,6 @@ function ObjectiveNameToVoice(%bot) case "AIEnhancedEscort": %result = "slf.tsk.cover"; } - + return %result; -} \ No newline at end of file +} From 2b6dd24341efb36b65fc9610242bfd6758c18f6d Mon Sep 17 00:00:00 2001 From: Robert MacGregor Date: Wed, 29 Jun 2016 19:29:29 -0400 Subject: [PATCH 2/3] Update README.md Move the description from the wiki and jot down notes on supported & planned features. --- README.md | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a593461..ac89a2e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,71 @@ Tribes 2 AI Enhancement ======= -Please refer to the corresponding wiki page on my website [here.](http://dx.no-ip.org/doku.php?id=projects:gaming:t2:ai) +Status: **Work in Progress** + +This is an attempt to enhance the capabilities of the Tribes 2 artificial intelligence system. It attempts to work +out several quirks in the game's AI implementation: + + * Bots are assigned tasks randomly on a per-spawn basis. + * Bots usually end up ignoring hostiles within their line of sight (LOS), unless they linger. + * Bots ignore nearby audible explosions and projectiles crossing into their LOS altogether. + * If a bot is struck, they instantly know where the attacker is. + +Taking all that, the solution that was devised was to create a commander meta-AI per team that assigned the tasks based on the necessity of a given task to be completed and greatly improve the per-bot logic to have better target engagement, weapon selection & firing logic as well as an LOS that actually works. All of this in theory should create a more challengingcomputer controlled foe that is also a bit more fair in terms of what they know and when. + +Commander Behavior +======= + +This commander would balance out the number of available bots across all known tasks using a dynamic weighting system, similar to how at the bot-level there is weighted tasks to determine what the bot will +do and when. If the base defenses have fallen, then the weight for restoring the defense and defending critical assets will rise and the AI Commander will pull bots performing auxiliary (non-essential) tasks and put them onto defending against the onslaught. + +Planned and implemented Features: + +- [x] Commanders ensure deterministic task assignment to bots, guaranteeing sane assignments. +- [ ] Commanders adjust priorities in response to mission events and adjust bot task allocations accordingly. +- [ ] Commanders decide whether or not to give Human control over a bot, particularly lax with idle bots. +- [ ] Commanders ditch prioritization in emergency situations (like the flag is grabbed and its the last point to win). + +Bot Behavior +======= + +At the bot level, there is going to be several changes in how they operate. The most notable one is the implementation of a +view cone to check anything in their LOS. This LOS check is also used to determine if they can see a projectile headed right +at them. This projectile LOS allows them to dodge incoming projectiles, which the AI has never done previously. Further, +sound stimulation is simulated when things explode nearby, which can cause the bot to danger-step away from the source of +the sound. + +Planned and implemented Features: + +- [x] Bots dodge grenades, but only when seen. +- [x] Bots move away from explosion sound sources when heard. +- [x] Bots pick a loadout that is meaningful for their commander-assigned task. +- [ ] Bots rearm at the nearest available inventory station. +- Bots do rearm, but not necessarily at the closest inventory station. +- [ ] Bots defend critical mission elements. +- Only in CTF: Bots defend the generators and flag. +- [ ] Bots attack critical mission elements. +- Only in CTF: Bots can run the flag. +- [ ] Bots have a view cone and react to visual sightings with a randomized delay and see reasonable distances. +- Mostly implemented; doesn't take fog into account. +- [ ] Bots use missile launchers against vehicles when available. +- [ ] Bots deal with cloakers correctly. +- Bots totally ignore cloakers right now. +- [ ] Bots dodge incoming linear pattern projectiles when seen. +- Partially implemented; only works for LinearProjectile and LinearFlareProjectile types. +- [ ] Bots take routes to avoid dangerous static placements, like turrets. +- [ ] Bots scout meaningful areas of the base. +- Only in CTF. +- [ ] Bots can engage hostile targets. +- [ ] Bots can use packs where applicable. +- Only ShieldPack: Bots use the shieldpack when being fought. +- [ ] Bots react to projectiles crossing into their line of sight. +- [ ] Bots report their current task when queried using VCW (What's your assignment?) +- Partially implemented; only works correctly for some tasks. +- [ ] Bots pick the best weapon in their inventory during engagement using either weapon meta data or data gleamed from the profiler. +- [ ] Bots return to base to rearm when sufficiently out of ammo or low on health. + +Planned and supported gamemodes: +- [ ] CTF +- [ ] Hunters + From 6df5c6c5926e668ed604c10cd79bfdc8379c20a4 Mon Sep 17 00:00:00 2001 From: Robert MacGregor Date: Wed, 29 Jun 2016 21:49:32 -0400 Subject: [PATCH 3/3] Update README.md Add some more unimplemented logic. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index ac89a2e..fc10ad9 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,9 @@ Planned and implemented Features: - Partially implemented; only works correctly for some tasks. - [ ] Bots pick the best weapon in their inventory during engagement using either weapon meta data or data gleamed from the profiler. - [ ] Bots return to base to rearm when sufficiently out of ammo or low on health. +- [ ] Bots coordinate on field. For example, some scouts escorting a heavy that will mortar some stuff. +- [ ] Bots repair destroyed base assets. +- [ ] Bots deploy motion sensors, pulse sensors, spider clamps and spike turrets. Planned and supported gamemodes: - [ ] CTF