diff --git a/scripts/DXAI/aicommander.cs b/scripts/DXAI/aicommander.cs index b9fea53..f341f14 100644 --- a/scripts/DXAI/aicommander.cs +++ b/scripts/DXAI/aicommander.cs @@ -34,7 +34,6 @@ function AICommander::setup(%this) for (%iteration = 0; %iteration < ClientGroup.getCount(); %iteration++) { %currentClient = ClientGroup.getObject(%iteration); - %currentClient.updateVisualAcuity(); if (%currentClient.isAIControlled() && %currentClient.team == %this.team) { @@ -42,6 +41,11 @@ function AICommander::setup(%this) %this.idleBotList.add(%currentClient); %currentClient.commander = %this; + + %currentClient.initialize(); + %currentClient.visibleHostiles = new SimSet(); + %currentClient.updateVisualAcuity(); + %currentClient.stuckCheck(); } } @@ -74,9 +78,7 @@ function AICommander::_skimObjectiveGroup(%this, %group) case "AIODefendLocation": // FIXME: Console spam from .targetObjectID not being set? %datablockName = %current.targetObjectID.getDatablock().getName(); - - echo(%datablockName); - + // Defending the flag? if (%datablockName $= "FLAG") %this.objectiveCycles[$DXAI::Priorities::DefendFlag].add(%current); @@ -243,14 +245,24 @@ function AICommander::assignTask(%this, %taskID, %bot) %objective = %this.objectiveCycles[%taskID].next(); // Set the bot to defend the location - %bot.defendLocation = %objective.location; + %bot.defendTargetLocation = %objective.location; + %datablockName = %objective.targetObjectID.getDatablock().getName(); + + switch$(%datablockName) + { + case "FLAG": + %bot.defenseDescription = "flag"; + case "GeneratorLarge": + %bot.defenseDescription = "generator"; + } + %bot.addTask("AIEnhancedDefendLocation"); case $DXAI::Priorities::ScoutBase: %objective = %this.objectiveCycles[%taskID].next(); // Set the bot to defend the location - %bot.scoutLocation = %objective.location; + %bot.scoutTargetLocation = %objective.location; %bot.scoutDistance = %objective.distance; %bot.addTask("AIEnhancedScoutLocation"); } @@ -267,6 +279,13 @@ function AICommander::setDefaultPriorities(%this) function AICommander::cleanUp(%this) { + for (%iteration = 0; %iteration < %this.botList.getCount(); %iteration++) + { + %current = %this.botList.getObject(%iteration); + cancel(%current.visualAcuityTick); + cancel(%current.stuckCheckTick); + } + %this.botList.delete(); %this.idleBotList.delete(); } diff --git a/scripts/DXAI/aiconnection.cs b/scripts/DXAI/aiconnection.cs index 73df8fd..70e4555 100644 --- a/scripts/DXAI/aiconnection.cs +++ b/scripts/DXAI/aiconnection.cs @@ -9,15 +9,10 @@ // Refer to LICENSE.txt for more information. //------------------------------------------------------------------------------------------ -function AIConnection::initialize(%this, %aiClient) +function AIConnection::initialize(%this) { %this.fieldOfView = 3.14 / 2; // 90* View cone %this.viewDistance = 300; - - if (!isObject(%aiClient)) - error("AIPlayer: Attempted to initialize with bad AI client connection!"); - - %this.client = %aiClient; } function AIConnection::update(%this) @@ -77,6 +72,9 @@ function AIConnection::setMoveTarget(%this, %position) %this.isFollowingTarget = false; %this.setPath(%position); %this.stepMove(%position); + + %this.minimumPathDistance = 9999; + %this.maximumPathDistance = -9999; } function AIConnection::setFollowTarget(%this, %target, %minDistance, %maxDistance, %hostile) @@ -97,14 +95,72 @@ function AIConnection::setFollowTarget(%this, %target, %minDistance, %maxDistanc %this.stepEscort(%target); } +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; + + if (%targetDistance > %this.maximumPathDistance) + %this.maximumPathDistance = %targetDistance; + if (%targetDistance < %this.minimumPathDistance) + %this.minimumPathDistance = %targetDistance; + + %this.stuckCheckTick = %this.schedule(5000, "stuckCheck"); +} + function AIConnection::updateLegs(%this) { + %now = getSimTime(); + %delta = %now - %this.lastUpdateLegs; + %this.lastUpdateLegs = %now; + if (%this.isMovingToTarget) { if (%this.aimAtLocation) %this.aimAt(%this.moveTarget); else if(%this.manualAim) %this.aimAt(%this.moveTarget); + + %targetDistance = %this.pathDistRemaining(9000); + + if (%targetDistance > %this.maximumPathDistance) + %this.maximumPathDistance = %targetDistance; + if (%targetDistance < %this.minimumPathDistance) + %this.minimumPathDistance = %targetDistance; + + // Bots follow a set of lines drawn between nodes to slowly decrement the path distance, + // so bots that are stuck usually get their remaining distance stuck in some range of + // arbitrary values, so we monitor the minimum and maximum values over a period of 5 seconds + + // Test... + %pathDistance = %this.getPathDistance(%this.moveTarget); + if(%pathDistance > 10 && %this.moveTravelTime < 10000) + %this.moveTravelTime += %delta; + else if (%pathDistance < 10) + %this.moveTravelTime = 0; + else if (%this.moveTravelTime >= 10000) + { + // We appear to be stuck, so pick a random nearby node and try to run to it + %this.moveTravelTime = 0; + %this.isPathCorrecting = true; + + if (isObject(NavGraph)) + { + %randomNode = NavGraph.randNode(%this.player.getPosition(), 200, true, true); + if (%randomNode != -1) + %this.setMoveTarget(NavGraph.nodeLoc(%randomNode)); + } + } } else if (%this.isFollowingTarget) { @@ -119,7 +175,24 @@ function AIConnection::updateLegs(%this) function AIConnection::updateWeapons(%this) { - + 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.getWorldBoxCenter()); + + // 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++) + { + // Weapons with a decent bit of spread should be used <= 20m + } + + %player.selectWeaponSlot(%bestWeapon); + } } function AIConnection::updateVisualAcuity(%this) @@ -127,7 +200,7 @@ function AIConnection::updateVisualAcuity(%this) if (isEventPending(%this.visualAcuityTick)) cancel(%this.visualAcuityTick); - if (!isObject(%this.player) || %this.player.getState() !$= "Move") + if (%this.visibleDistance = 0 || !isObject(%this.player) || %this.player.getState() !$= "Move") { %this.visualAcuityTick = %this.schedule(getRandom(230, 400), "updateVisualAcuity"); return; @@ -167,24 +240,26 @@ function AIConnection::updateVisualAcuity(%this) %this.upperMarker.delete(); %this.lowerMarker.delete(); } + + %now = getSimTime(); + %deltaTime = %now - %this.lastVisualAcuityUpdate; + %this.lastVisualAcuityUpdate = %now; - %result = %this.getObjectsInViewcone($TypeMasks::ProjectileObjectType | $TypeMasks::PlayerObjectType, %this.viewDistance, true); - - // What can we see? - for (%i = 0; %i < %result.getCount(); %i++) + %visibleObjects = %this.getObjectsInViewcone($TypeMasks::ProjectileObjectType | $TypeMasks::PlayerObjectType, %this.viewDistance, true); + + for (%iteration = 0; %iteration < %visibleObjects.getCount(); %iteration++) { - %current = %result.getObject(%i); - %this.awarenessTicks[%current]++; + %current = %visibleObjects.getObject(%iteration); + + %this.awarenessTime[%current] += %deltaTime; + + // Did we "notice" the object yet? + %noticeTime = getRandom(700, 1200); + if (%this.awarenessTime[%current] < %noticeTime) + continue; if (%current.getType() & $TypeMasks::ProjectileObjectType) - { - // Did we "notice" the object yet? - // We pick a random notice time between 700ms and 1200 ms - // Obviously this timer runs on a 32ms tick, but it should help provide a little unpredictability - %noticeTime = getRandom(700, 1200); - if (%this.awarenessTicks[%current] < (%noticeTime / 32)) - continue; - + { %className = %current.getClassName(); // LinearFlareProjectile and LinearProjectile have linear trajectories, so we can easily determine if a dodge is necessary @@ -223,26 +298,43 @@ function AIConnection::updateVisualAcuity(%this) } } // See a player? - else if (%current.getType() & $TypeMasks::PlayerObjectType) + else if (%current.getType() & $TypeMasks::PlayerObjectType && %current.client.team != %this.team) { - %this.clientDetected(%current); - %this.clientDetected(%current.client); + %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)); + // %start = %this.player.getPosition(); + // %end = vectorAdd(%start, vectorScale(%this.player.getEyeVector(), %this.viewDistance)); - %rayCast = containerRayCast(%start, %end, -1, %this.player); - %hitObject = getWord(%raycast, 0); + // %rayCast = containerRayCast(%start, %end, -1, %this.player); + // %hitObject = getWord(%raycast, 0); - if (%hitObject == %current) - { - %this.clientDetected(%current); - %this.stepEngage(%current); - } + // if (%hitObject == %current) + // { + // %this.clientDetected(%current); + // %this.stepEngage(%current); + // } } } - %result.delete(); - %this.visualAcuityTick = %this.schedule(getRandom(230, 400), "updateVisualAcuity"); + // 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; + if (%this.awarenessTime[%current] < 200) + { + %this.visibleHostiles.remove(%current); + continue; + } + } + } + + %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/config.cs b/scripts/DXAI/config.cs index e19579e..9488ac9 100644 --- a/scripts/DXAI/config.cs +++ b/scripts/DXAI/config.cs @@ -13,3 +13,6 @@ $DXAI::Commander::minimumGeneratorDefense = 1; $DXAI::Bot::DefaultFieldOfView = 3.14159 / 2; // 90* $DXAI::Bot::DefaultViewDistance = 300; + +$DXAI::Bot::MinimumVisualAcuityTime = 200; +$DXAI::Bot::MaximumVisualAcuityTime = 400; diff --git a/scripts/DXAI/helpers.cs b/scripts/DXAI/helpers.cs index 92def14..798aef6 100644 --- a/scripts/DXAI/helpers.cs +++ b/scripts/DXAI/helpers.cs @@ -188,7 +188,7 @@ function GameConnection::getObjectsInViewcone(%this, %typeMask, %distance, %perf %result.add(%currentObject); else { - %rayCast = containerRayCast(%coneOrigin, %currentObject.getWorldBoxCenter(), -1, 0); + %rayCast = containerRayCast(%coneOrigin, %currentObject.getWorldBoxCenter(), $TypeMasks::AllObjectType, %this.player); %hitObject = getWord(%raycast, 0); @@ -224,6 +224,16 @@ function vectorMultiply(%vec1, %vec2) (getWord(%vec1, 2) * getWord(%vec2, 2)); } +function listStuckBots() +{ + for (%iteration = 0; %iteration < ClientGroup.getCount(); %iteration++) + { + %client = ClientGroup.getObject(%iteration); + if (%client.isAIControlled() && %client.isPathCorrecting) + error(%client); + } +} + // If the map editor was instantiated, this will prevent a little bit // of console warnings function Terraformer::getType(%this) { return 0; } diff --git a/scripts/DXAI/main.cs b/scripts/DXAI/main.cs index 8a5926c..7d97859 100644 --- a/scripts/DXAI/main.cs +++ b/scripts/DXAI/main.cs @@ -245,6 +245,9 @@ package DXAI_Hooks %client.shouldRearm = true; %client.targetLoadout = 1; + %client.engageTargetLastPosition = ""; + %client.engageTarget = -1; + return 11595; } @@ -270,10 +273,26 @@ package DXAI_Hooks //switch$ (%eventTag) //{ schedule(250, %targetClient, "AIPlayAnimSound", %targetClient, %clientPos, "cmd.decline", $AIAnimSalute, $AIAnimSalute, 0); - schedule(2000, %targetClient, "AIPlayAnimSound", %targetClient, %clientPos, ObjectiveNameToVoice(%targetClient.getTaskName()), $AIAnimSalute, $AIAnimSalute, 0); + 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 Station::stationTriggered(%data, %obj, %isTriggered) { parent::stationTriggered(%data, %obj, %isTriggered); diff --git a/scripts/DXAI/objectives.cs b/scripts/DXAI/objectives.cs index 8c2d350..93814d3 100644 --- a/scripts/DXAI/objectives.cs +++ b/scripts/DXAI/objectives.cs @@ -20,7 +20,8 @@ $DXAI::Task::NoPriority = 0; $DXAI::Task::LowPriority = 100; $DXAI::Task::MediumPriority = 200; $DXAI::Task::HighPriority = 500; -$DXAI::Task::VeryHighPriority = 1000; +$DXAI::Task::VeryHighPriority = 1000; +$DXAI::Task::ReservedPriority = 5000; //------------------------------------------------------------------------------------------ // +Param %bot.escortTarget: The ID of the object to escort. This can be literally @@ -119,7 +120,10 @@ function AIEnhancedScoutLocation::retire(%task, %client) { } function AIEnhancedScoutLocation::weight(%task, %client) { %task.setWeight($DXAI::Task::MediumPriority); } 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; @@ -127,7 +131,7 @@ function AIEnhancedScoutLocation::monitor(%task, %client) // We just received the task, so find a node within distance of our scout location if (%client.currentNode == -1) { - %client.currentNode = NavGraph.randNode(%client.scoutLocation, %client.scoutDistance, true, true); + %client.currentNode = NavGraph.randNode(%client.scoutTargetLocation, %client.scoutDistance, true, true); if (%client.currentNode != -1) %client.setMoveTarget(NavGraph.nodeLoc(%client.currentNode)); @@ -138,7 +142,7 @@ function AIEnhancedScoutLocation::monitor(%task, %client) %pathDistance = %client.getPathDistance(%client.moveTarget); // Don't move if we're close enough to our next node - if (%pathDistance <= 40 && %client.isMoving) + if (%pathDistance <= 40 && %client.isMovingToTarget) { %client.setMoveTarget(-1); %client.nextScoutRotation = getRandom(5000, 10000); @@ -159,7 +163,7 @@ function AIEnhancedScoutLocation::monitor(%task, %client) %client.nextScoutRotation = getRandom(5000, 10000); // Pick a new node - %client.currentNode = NavGraph.randNode(%client.scoutLocation, %client.scoutDistance, true, true); + %client.currentNode = NavGraph.randNode(%client.scoutTargetLocation, %client.scoutDistance, true, true); // Ensure that we found a node. if (%client.currentNode != -1) @@ -167,6 +171,11 @@ function AIEnhancedScoutLocation::monitor(%task, %client) } } } + +function AIEnhancedScoutLocation::monitorEngage(%task, %client) +{ +} + //------------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------------ @@ -182,40 +191,24 @@ function AIEnhancedEngageTarget::retire(%task, %client) { } function AIEnhancedEngageTarget::weight(%task, %client) { - if (!isObject(%client.engageTarget)) + // Blow through seen targets + %chosenTarget = -1; + %chosenTargetDistance = 9999; + + %botPosition = %client.player.getPosition(); + for (%iteration = 0; %iteration < %client.visibleHostiles.getCount(); %iteration++) { - %visibleObjects = %client.getObjectsInViewcone($TypeMasks::PlayerObjectType, %client.viewDistance, true); + %current = %client.visibleHostiles.getObject(%iteration); - // Choose the closest target - // TODO: Choose on more advanced metrics like HP - %chosenTarget = -1; - %chosenTargetDistance = 9999; - for (%iteration = 0; %iteration < %visibleObjects.getCount(); %iteration++) + %targetDistance = vectorDist(%current.getPosition(), %botPosition); + if (%targetDistance < %chosenTargetDistance) { - %potentialTarget = %visibleObjects.getObject(%iteration); - - %potentialTargetDistance = vectorDist(%potentialTarget.getPosition(), %client.player.getPosition()); - if (%potentialTarget.client.team != %client.team && %potentialTargetDistance < %chosenTargetDistance) - { - %chosenTargetDistance = %potentialTargetDistance; - %chosenTarget = %potentialTarget; - } + %chosenTargetDistance = %targetDistance; + %chosenTarget = %current; } - - %visibleObjects.delete(); - %client.engageTarget = %chosenTarget; } - else - { - // Can we still see them? - %rayCast = containerRayCast(%client.player.getWorldBoxCenter(), %client.engageTarget.getWorldBoxCenter(), -1, %client.player); - %hitObject = getWord(%raycast, 0); - - // TODO: Go to the last known position. - if (%hitObject != %client.engageTarget) - %client.engageTarget = -1; - } - + + %client.engageTarget = %chosenTarget; if (!isObject(%client.engageTarget) && %client.engageTargetLastPosition $= "") %task.setWeight($DXAI::Task::NoPriority); else @@ -225,26 +218,16 @@ function AIEnhancedEngageTarget::weight(%task, %client) function AIEnhancedEngageTarget::monitor(%task, %client) { if (isObject(%client.engageTarget)) - { - %player = %client.player; - %targetDistance = vectorDist(%player.getPosition(), %client.engageTarget.getPosition()); - - // Firstly, just aim at them for now - %client.aimAt(%client.engageTarget.getWorldBoxCenter()); - - // 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++) + { + if (%client.engageTarget.getState() !$= "Move") { - // Weapons with a decent bit of spread should be used <= 20m + %client.engageTarget = -1; + %client.engageTargetLastPosition = ""; + return; } - %player.selectWeaponSlot(%bestWeapon); - %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 !$= "") @@ -258,6 +241,7 @@ function AIEnhancedEngageTarget::monitor(%task, %client) } } } + //------------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------------ @@ -275,6 +259,8 @@ function AIEnhancedRearmTask::weight(%task, %client) %task.setWeight($DXAI::Task::HighPriority); else %task.setWeight($DXAI::Task::NoPriority); + + %task.setMonitorFreq(getRandom(10, 32)); } function AIEnhancedRearmTask::monitor(%task, %client) @@ -322,26 +308,44 @@ function AIEnhancedReturnFlagTask::monitor(%task, %client) { if (!isObject(%client.returnFlagTarget)) return; - - %client.setMoveTarget(%client.returnFlagTarget.getPosition()); + + if (isObject(%client.engageTarget) && %client.engageTarget.getState() $= "Move") + AIEnhancedReturnFlagTask::monitorEngage(%task, %client); + else + %client.setMoveTarget(%client.returnFlagTarget.getPosition()); } + +function AIEnhancedReturnFlagTask::monitorEngage(%task, %client) +{ +} + //------------------------------------------------------------------------------------------ //------------------------------------------------------------------------------------------ // Description: A task that performs path correction. //------------------------------------------------------------------------------------------` function AIEnhancedPathCorrectionTask::initFromObjective(%task, %objective, %client) { } -function AIEnhancedPathCorrectionTask::assume(%task, %client) { %task.setMonitorFreq(32); } +function AIEnhancedPathCorrectionTask::assume(%task, %client) { %task.setMonitorFreq(2); } function AIEnhancedPathCorrectionTask::retire(%task, %client) { } function AIEnhancedPathCorrectionTask::weight(%task, %client) { - %task.setWeight(0); + if (%client.isPathCorrecting) + %task.setWeight($DXAI::Task::VeryHighPriority); + else + %task.setWeight($DXAI::Task::NoPriority); } function AIEnhancedPathCorrectionTask::monitor(%task, %client) -{ - +{ + if (%client.isPathCorrecting) + { + if (%client.player.getEnergyPercent() >= 1) + %client.isPathCorrecting = false; + else + %client.setMoveTarget(-1); + } + } //------------------------------------------------------------------------------------------ @@ -383,8 +387,10 @@ function AIEnhancedFlagCaptureTask::monitor(%task, %client) } //------------------------------------------------------------------------------------------ -function ObjectiveNameToVoice(%objective) +function ObjectiveNameToVoice(%bot) { + %objective = %bot.getTaskName(); + %result = "avo.grunt"; switch$(%objective) { @@ -397,7 +403,15 @@ function ObjectiveNameToVoice(%objective) case "AIEnhancedScoutLocation": %result = "slf.def.defend"; case "AIEnhancedDefendLocation": - %result = "slf.def.defend"; + switch$(%bot.defenseDescription) + { + case "flag": + %result = "slf.def.flag"; + case "generator": + %result = "slf.def.generator"; + default: + %result = "slf.def.defend"; + } case "AIEnhancedEscort": %result = "slf.tsk.cover"; }