From 5e53dd91ef5386adb0cdd887695038cfe0c2530c Mon Sep 17 00:00:00 2001 From: Robert MacGregor Date: Mon, 5 Oct 2015 05:03:45 -0400 Subject: [PATCH] Bots now all have an engage task; bots now all have a return flag task; AI engage has some basic engage functionality; began work on the new AI-player interaction system; Bots will spot players via their view cone now --- scripts/DXAI/aicommander.cs | 17 +++- scripts/DXAI/helpers.cs | 63 ++++++++++++ scripts/DXAI/loadouts.cs | 31 ++++++ scripts/DXAI/main.cs | 42 +++++++- scripts/DXAI/objectives.cs | 195 +++++++++++++++++++++++++++++++++++- 5 files changed, 342 insertions(+), 6 deletions(-) create mode 100644 scripts/DXAI/loadouts.cs diff --git a/scripts/DXAI/aicommander.cs b/scripts/DXAI/aicommander.cs index c5eb231..c746aac 100644 --- a/scripts/DXAI/aicommander.cs +++ b/scripts/DXAI/aicommander.cs @@ -132,7 +132,22 @@ function AICommander::loadObjectives(%this) function AICommander::assignTasks(%this) { - // Calculate how much priority we have total, first + // First, assign objectives that all bots should have + for (%iteration = 0; %iteration < %this.botList.getCount(); %iteration++) + { + %bot = %this.botList.getObject(%iteration); + %bot.addTask(AIEnhancedEngageTarget); + %bot.addTask(AIEnhancedRearmTask); + + // We only need this task if we're actually playing CTF. + if ($CurrentMissionType $= "CTF") + %bot.addTask(AIEnhancedReturnFlagTask); + + %bot.targetLoadout = 0; + %bot.shouldRearm = true; + } + + // Calculate how much priority we have total %totalPriority = 0.0; for (%iteration = 0; %iteration < $DXAI::Priorities::Count; %iteration++) { diff --git a/scripts/DXAI/helpers.cs b/scripts/DXAI/helpers.cs index 975566e..3f97550 100644 --- a/scripts/DXAI/helpers.cs +++ b/scripts/DXAI/helpers.cs @@ -103,6 +103,59 @@ function GameConnection::calculateViewCone(%this, %distance) return %coneOrigin SPC %viewConeClockwisePoint SPC %viewConeCounterClockwisePoint SPC %viewConeUpperPoint SPC %viewConeLowerPoint; } +function SimSet::recurse(%this, %result) +{ + if (!isObject(%result)) + %result = new SimSet(); + + for (%iteration = 0; %iteration < %this.getCount(); %iteration++) + { + %current = %this.getObject(%iteration); + + if (%current.getClassName() $= "SimGroup" || %current.getClassName() $= "SimSet") + %current.recurse(%result); + else + %result.add(%current); + } + + return %result; +} + +// TODO: Use the nav graph to estimate an actual distance? +function GameConnection::getClosestInventory(%this) +{ + if (!isObject(%this.player)) + return -1; + + %group = nameToID("Team" @ %this.team); + if (!isObject(%group)) + return -1; + + %teamObjects = %group.recurse(); + + %closestInventory = -1; + %closestInventoryDistance = 9999; + for (%iteration = 0; %iteration < %teamObjects.getCount(); %iteration++) + { + %current = %teamObjects.getObject(%iteration); + + if (%current.getClassName() $= "StaticShape" && %current.getDatablock().getName() $= "StationInventory") + { + %inventoryDistance = vectorDist(%current.getPosition(), %this.player.getPosition()); + + if (%inventoryDistance < %closestInventoryDistance) + { + %closestInventoryDistance = %inventoryDistance; + %closestInventory = %current; + } + } + } + + %teamObjects.delete(); + + return %closestInventory; +} + // View cone simulation function function GameConnection::getObjectsInViewcone(%this, %typeMask, %distance, %performLOSTest) { @@ -194,3 +247,13 @@ $DXAI::System::RuntimeDummy = new ScriptObject(RuntimeDummy) { class = "RuntimeD function RuntimeDummy::addTask() { } function RuntimeDummy::reset() { } + +$TypeMasks::InteractiveObjectType = $TypeMasks::PlayerObjectType | $TypeMasks::VehicleObjectType | $TypeMasks::WaterObjectType | $TypeMasks::ProjectileObjectType | $TypeMasks::ItemObjectType | $TypeMasks::CorpseObjectType; +$TypeMasks::UnInteractiveObjectType = $TypeMasks::StaticObjectType | $TypeMasks::TerrainObjectType | $TypeMasks::InteriorObjectType | $TypeMasks::StaticTSObjectType | $TypeMasks::StaticRenderedObjectType; +$TypeMasks::BaseAssetObjectType = $TypeMasks::ForceFieldObjectType | $TypeMasks::TurretObjectType | $TypeMasks::SensorObjectType | $TypeMasks::StationObjectType | $TypeMasks::GeneratorObjectType; +$TypeMasks::GameSupportObjectType = $TypeMasks::TriggerObjectType | $TypeMasks::MarkerObjectType | $TypeMasks::CameraObjectType | $TypeMasks::VehicleBlockerObjectType | $TypeMasks::PhysicalZoneObjectType; +$TypeMasks::GameContentObjectType = $TypeMasks::ExplosionObjectType | $TypeMasks::CorpseObjectType | $TypeMasks::DebrisObjectType; +$TypeMasks::DefaultLOSObjectType = $TypeMasks::TerrainObjectType | $TypeMasks::InteriorObjectType | $TypeMasks::StaticObjectType; + +// We declare the AllObjectType like this instead of -1 because it seems -1 can sometimes not work? +$TypeMasks::AllObjectType = $TypeMasks::InteractiveObjectType | $TypeMasks::DefaultLOSObjectType | $TypeMasks::GameContentObjectType | $TypeMasks::GameSupportObjectType | $TypeMasks::BaseAssetObjectType | $TypeMasks::UnInteractiveObjectType; diff --git a/scripts/DXAI/loadouts.cs b/scripts/DXAI/loadouts.cs new file mode 100644 index 0000000..d0c4147 --- /dev/null +++ b/scripts/DXAI/loadouts.cs @@ -0,0 +1,31 @@ +//------------------------------------------------------------------------------------------ +// loadouts.cs +// Source file declaring usable loadouts for the bots and mapping them to their most +// appropriate tasks. +// https://github.com/Ragora/T2-DXAI.git +// +// Copyright (c) 2014 Robert MacGregor +// This software is licensed under the MIT license. Refer to LICENSE.txt for more information. +//------------------------------------------------------------------------------------------ + +$DXAI::Loadouts[0, "Name"] = "Light Scout"; +$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, "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::OptimalLoadouts["AIEnhancedDefendLocation"] = "1"; + +$DXAI::Loadouts::Count = 2; +$DXAI::Loadouts::Default = 0; \ No newline at end of file diff --git a/scripts/DXAI/main.cs b/scripts/DXAI/main.cs index fa1fb3f..8a5926c 100644 --- a/scripts/DXAI/main.cs +++ b/scripts/DXAI/main.cs @@ -14,6 +14,7 @@ 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"); // General DXAI API implementations function DXAI::cleanup() @@ -208,9 +209,13 @@ package DXAI_Hooks 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 + // 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) + { %targetObject.client.schedule(getRandom(250, 400), "setDangerLocation", %pos, 20); + // 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) @@ -237,6 +242,8 @@ package DXAI_Hooks // Make sure the bot has no objectives // %client.reset(); // %client.defaultTasksAdded = true; + %client.shouldRearm = true; + %client.targetLoadout = 1; return 11595; } @@ -256,6 +263,39 @@ package DXAI_Hooks $DXAI::System::InvalidatedEnvironment = true; parent::compile(%file); } + + function AIRespondToEvent(%client, %eventTag, %targetClient) + { + %clientPos = %client.player.getWorldBoxCenter(); + //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(3700, %targetClient, "AIPlayAnimSound", %targetClient, %clientPos, "vqk.sorry", $AIAnimSalute, $AIAnimSalute, 0); + } + + function Station::stationTriggered(%data, %obj, %isTriggered) + { + parent::stationTriggered(%data, %obj, %isTriggered); + + // TODO: If the bot isn't supposed to be on the station, at least restock ammunition? + if (%isTriggered && %obj.triggeredBy.client.isAIControlled() && %obj.triggeredBy.client.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++) + { + %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! + } + } + } }; // Only activate the package if it isn't already active. diff --git a/scripts/DXAI/objectives.cs b/scripts/DXAI/objectives.cs index a997251..b6b49de 100644 --- a/scripts/DXAI/objectives.cs +++ b/scripts/DXAI/objectives.cs @@ -20,7 +20,7 @@ function AIEnhancedEscort::initFromObjective(%task, %objective, %client) { } function AIEnhancedEscort::assume(%task, %client) { %task.setMonitorFreq(1); } function AIEnhancedEscort::retire(%task, %client) { %client.isMoving = false; %client.manualAim = false; } -function AIEnhancedEscort::weight(%task, %client) { %task.setWeight(1000); } +function AIEnhancedEscort::weight(%task, %client) { %task.setWeight(500); } function AIEnhancedEscort::monitor(%task, %client) { @@ -53,7 +53,7 @@ function AIEnhancedEscort::monitor(%task, %client) function AIEnhancedDefendLocation::initFromObjective(%task, %objective, %client) { } function AIEnhancedDefendLocation::assume(%task, %client) { %task.setMonitorFreq(1); } function AIEnhancedDefendLocation::retire(%task, %client) { %client.isMoving = false; } -function AIEnhancedDefendLocation::weight(%task, %client) { %task.setWeight(1000); } +function AIEnhancedDefendLocation::weight(%task, %client) { %task.setWeight(500); } function AIEnhancedDefendLocation::monitor(%task, %client) { @@ -103,7 +103,7 @@ function AIEnhancedDefendLocation::monitor(%task, %client) function AIEnhancedScoutLocation::initFromObjective(%task, %objective, %client) { } function AIEnhancedScoutLocation::assume(%task, %client) { %task.setMonitorFreq(1); %client.currentNode = -1; } function AIEnhancedScoutLocation::retire(%task, %client) { } -function AIEnhancedScoutLocation::weight(%task, %client) { %task.setWeight(1000); } +function AIEnhancedScoutLocation::weight(%task, %client) { %task.setWeight(500); } function AIEnhancedScoutLocation::monitor(%task, %client) { @@ -159,4 +159,191 @@ function AIEnhancedScoutLocation::monitor(%task, %client) } } } -//------------------------------------------------------------------------------------------ \ No newline at end of file +//------------------------------------------------------------------------------------------ + +//------------------------------------------------------------------------------------------ +// +Param %bot.engangeDistance: The maximum distance at which the bot will go out to +// attack a hostile. +// +Param %bot.engageTarget: A manually assigned engage target to go after. +// +Description: The AIEnhancedEngageTarget is a better implementation of the base +// AI engage logic. +//------------------------------------------------------------------------------------------` +function AIEnhancedEngageTarget::initFromObjective(%task, %objective, %client) { } +function AIEnhancedEngageTarget::assume(%task, %client) { %task.setMonitorFreq(1); } +function AIEnhancedEngageTarget::retire(%task, %client) { } + +function AIEnhancedEngageTarget::weight(%task, %client) +{ + if (!isObject(%client.engageTarget)) + { + %visibleObjects = %client.getObjectsInViewcone($TypeMasks::PlayerObjectType, %client.viewDistance, true); + + // Choose the closest target + // TODO: Choose on more advanced metrics like HP + %chosenTarget = -1; + %chosenTargetDistance = 9999; + for (%iteration = 0; %iteration < %visibleObjects.getCount(); %iteration++) + { + %potentialTarget = %visibleObjects.getObject(%iteration); + + %potentialTargetDistance = vectorDist(%potentialTarget.getPosition(), %client.player.getPosition()); + if (%potentialTarget.client.team != %client.team && %potentialTargetDistance < %chosenTargetDistance) + { + %chosenTargetDistance = %potentialTargetDistance; + %chosenTarget = %potentialTarget; + } + } + + %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; + } + + if (!isObject(%client.engageTarget) && %client.engageTargetLastPosition $= "") + %task.setWeight(0); + else + %task.setWeight(1000); +} + +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++) + { + // Weapons with a decent bit of spread should be used <= 20m + } + + %player.selectWeaponSlot(%bestWeapon); + %client.engageTargetLastPosition = %client.engageTarget.getWorldBoxCenter(); + + %client.isMoving = true; + %client.moveLocation = getRandomPositionOnTerrain(%client.engageTargetLastPosition, 40); + + %client.pressFire(); + } + else if (%client.engageTargetLastPosition !$= "") + { + %client.isMoving = true; + %client.moveLocation = %client.engageTargetLastPosition; + + if (vectorDist(%client.player.getPosition(), %client.engageTargetLastPosition) <= 10) + { + %client.engageTargetLastPosition = ""; + %client.isMoving = false; + } + } +} +//------------------------------------------------------------------------------------------ + +//------------------------------------------------------------------------------------------ +// +Param %bot.shouldRearm: A boolean representing whether or not this bot should go +// and rearm. +// +Param %bot.targetInventory: The ID of the inventory station to rearm at. +//------------------------------------------------------------------------------------------` +function AIEnhancedRearmTask::initFromObjective(%task, %objective, %client) { } +function AIEnhancedRearmTask::assume(%task, %client) { %task.setMonitorFreq(1); } +function AIEnhancedRearmTask::retire(%task, %client) { } + +function AIEnhancedRearmTask::weight(%task, %client) +{ + if (%client.shouldRearm) + %task.setWeight(600); + else + %task.setWeight(0); +} + +function AIEnhancedRearmTask::monitor(%task, %client) +{ + if (!isObject(%client.targetInventory)) + %client.targetInventory = %client.getClosestInventory(); + + if (isObject(%client.targetInventory)) + { + // Politely wait if someone is already on it. + if (vectorDist(%client.targetInventory.getPosition(), %client.player.getPosition()) <= 7 && isObject(%client.targetInventory.triggeredBy)) + %client.isMoving = false; + else + { + %client.isMoving = true; + %client.moveLocation = %client.targetInventory.getPosition(); + } + } + else + %client.shouldRearm = false; // No inventories? +} +//------------------------------------------------------------------------------------------ + +//------------------------------------------------------------------------------------------ +// Description: A task that actually makes the bots return a flag that's nearby. +//------------------------------------------------------------------------------------------` +function AIEnhancedReturnFlagTask::initFromObjective(%task, %objective, %client) { } +function AIEnhancedReturnFlagTask::assume(%task, %client) { %task.setMonitorFreq(1); } +function AIEnhancedReturnFlagTask::retire(%task, %client) { } + +function AIEnhancedReturnFlagTask::weight(%task, %client) +{ + %flag = nameToID("Team" @ %client.team @ "Flag1"); + if (!isObject(%flag) || %flag.isHome) + { + %task.setWeight(0); + %client.targetFlag = -1; + %client.isMoving = false; + } + else + { + // TODO: For now, all the bots go after it! Make this check if the bot is range. + %task.setWeight(700); + + %client.targetFlag = %flag; + } +} + +function AIEnhancedReturnFlagTask::monitor(%task, %client) +{ + if (!isObject(%client.targetFlag)) + return; + + // TODO: Make the bot engage the flag runner if its currently held. + %client.isMoving = true; + %client.moveLocation = %client.targetFlag.getPosition(); +} +//------------------------------------------------------------------------------------------ + +function ObjectiveNameToVoice(%objective) +{ + %result = "avo.grunt"; + switch$(%objective) + { + case "AIEnhancedReturnFlagTask": + %result = "slf.def.flag"; + case "AIEnhancedRearmTask": + %result = "avo.grunt"; + case "AIEnhancedEngageTarget": + %result = "slf.att.attack"; + case "AIEnhancedScoutLocation": + %result = "slf.def.defend"; + case "AIEnhancedEscort": + %result = "slf.tsk.cover"; + } + + return %result; +} \ No newline at end of file