warzone2100/src/ai.cpp

1301 lines
39 KiB
C++

/*
This file is part of Warzone 2100.
Copyright (C) 1999-2004 Eidos Interactive
Copyright (C) 2005-2013 Warzone 2100 Project
Warzone 2100 is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
Warzone 2100 is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Warzone 2100; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
/**
* @file ai.c
*
* AI update functions for the different object types.
*
*/
#include "lib/framework/frame.h"
#include "action.h"
#include "cmddroid.h"
#include "combat.h"
#include "drive.h"
#include "mapgrid.h"
#include "map.h"
#include "projectile.h"
/* Weights used for target selection code,
* target distance is used as 'common currency'
*/
#define WEIGHT_DIST_TILE 13 //In points used in weaponmodifier.txt and structuremodifier.txt
#define WEIGHT_DIST_TILE_DROID WEIGHT_DIST_TILE //How much weight a distance of 1 tile (128 world units) has when looking for the best nearest target
#define WEIGHT_DIST_TILE_STRUCT WEIGHT_DIST_TILE
#define WEIGHT_HEALTH_DROID (WEIGHT_DIST_TILE * 10) //How much weight unit damage has (100% of damage is equaly weighted as 10 tiles distance)
//~100% damage should be ~8 tiles (max sensor range)
#define WEIGHT_HEALTH_STRUCT (WEIGHT_DIST_TILE * 7)
#define WEIGHT_NOT_VISIBLE_F 10 //We really don't like objects we can't see
#define WEIGHT_SERVICE_DROIDS (WEIGHT_DIST_TILE_DROID * 5) //We don't want them to be repairing droids or structures while we are after them
#define WEIGHT_WEAPON_DROIDS (WEIGHT_DIST_TILE_DROID * 4) //We prefer to go after anything that has a gun and can hurt us
#define WEIGHT_COMMAND_DROIDS (WEIGHT_DIST_TILE_DROID * 6) //Commanders get a higher priority
#define WEIGHT_MILITARY_STRUCT WEIGHT_DIST_TILE_STRUCT //Droid/cyborg factories, repair facility; shouldn't have too much weight
#define WEIGHT_WEAPON_STRUCT WEIGHT_WEAPON_DROIDS //Same as weapon droids (?)
#define WEIGHT_DERRICK_STRUCT (WEIGHT_MILITARY_STRUCT + WEIGHT_DIST_TILE_STRUCT * 4) //Even if it's 4 tiles further away than defenses we still choose it
#define WEIGHT_STRUCT_NOTBUILT_F 8 //Humans won't fool us anymore!
#define OLD_TARGET_THRESHOLD (WEIGHT_DIST_TILE * 4) //it only makes sense to switch target if new one is 4+ tiles closer
#define EMP_DISABLED_PENALTY_F 10 //EMP shouldn't attack emped targets again
#define EMP_STRUCT_PENALTY_F (EMP_DISABLED_PENALTY_F * 2) //EMP don't attack structures, should be bigger than EMP_DISABLED_PENALTY_F
#define TOO_CLOSE_PENALTY_F 20
#define TARGET_DOOMED_PENALTY_F 10 // Targets that have a lot of damage incoming are less attractive
#define TARGET_DOOMED_SLOW_RELOAD_T 21 // Weapon ROF threshold for above penalty. per minute.
//Some weights for the units attached to a commander
#define WEIGHT_CMD_RANK (WEIGHT_DIST_TILE * 4) //A single rank is as important as 4 tiles distance
#define WEIGHT_CMD_SAME_TARGET WEIGHT_DIST_TILE //Don't want this to be too high, since a commander can have many units assigned
uint8_t alliances[MAX_PLAYER_SLOTS][MAX_PLAYER_SLOTS];
/// A bitfield of vision sharing in alliances, for quick manipulation of vision information
PlayerMask alliancebits[MAX_PLAYER_SLOTS];
/// A bitfield for the satellite uplink
PlayerMask satuplinkbits;
static int aiDroidRange(DROID *psDroid, int weapon_slot)
{
int32_t longRange;
if (psDroid->droidType == DROID_SENSOR)
{
longRange = objSensorRange(psDroid);
}
else if (psDroid->numWeaps == 0 || psDroid->asWeaps[0].nStat == 0)
{
// Can't attack without a weapon
return 0;
}
else
{
WEAPON_STATS *psWStats = psDroid->asWeaps[weapon_slot].nStat + asWeaponStats;
longRange = proj_GetLongRange(psWStats, psDroid->player);
}
return longRange;
}
// see if a structure has the range to fire on a target
static bool aiStructHasRange(STRUCTURE *psStruct, BASE_OBJECT *psTarget, int weapon_slot)
{
if (psStruct->numWeaps == 0 || psStruct->asWeaps[0].nStat == 0)
{
// Can't attack without a weapon
return false;
}
WEAPON_STATS *psWStats = psStruct->asWeaps[weapon_slot].nStat + asWeaponStats;
int longRange = proj_GetLongRange(psWStats, psStruct->player);
return objPosDiffSq(psStruct, psTarget) < longRange*longRange && lineOfFire(psStruct, psTarget, weapon_slot, true);
}
static bool aiDroidHasRange(DROID *psDroid, BASE_OBJECT *psTarget, int weapon_slot)
{
int32_t longRange = aiDroidRange(psDroid, weapon_slot);
return objPosDiffSq(psDroid, psTarget) < longRange*longRange;
}
static bool aiObjHasRange(BASE_OBJECT *psObj, BASE_OBJECT *psTarget, int weapon_slot)
{
if (psObj->type == OBJ_DROID)
{
return aiDroidHasRange((DROID *)psObj, psTarget, weapon_slot);
}
else if (psObj->type == OBJ_STRUCTURE)
{
return aiStructHasRange((STRUCTURE *)psObj, psTarget, weapon_slot);
}
return false;
}
/* Initialise the AI system */
bool aiInitialise(void)
{
SDWORD i,j;
for (i = 0; i < MAX_PLAYER_SLOTS; i++)
{
alliancebits[i] = 0;
for (j = 0; j < MAX_PLAYER_SLOTS; j++)
{
bool valid = (i == j && i < MAX_PLAYERS);
alliances[i][j] = valid ? ALLIANCE_FORMED : ALLIANCE_BROKEN;
alliancebits[i] |= valid << j;
}
}
satuplinkbits = 0;
return true;
}
/* Shutdown the AI system */
bool aiShutdown(void)
{
return true;
}
/** Search the global list of sensors for a possible target for psObj. */
static BASE_OBJECT *aiSearchSensorTargets(BASE_OBJECT *psObj, int weapon_slot, WEAPON_STATS *psWStats, UWORD *targetOrigin)
{
int longRange = proj_GetLongRange(psWStats, psObj->player);
int tarDist = longRange * longRange;
bool foundCB = false;
int minDist = psWStats->upgrade[psObj->player].minRange * psWStats->upgrade[psObj->player].minRange;
BASE_OBJECT *psSensor, *psTarget = NULL;
if (targetOrigin)
{
*targetOrigin = ORIGIN_UNKNOWN;
}
for (psSensor = apsSensorList[0]; psSensor; psSensor = psSensor->psNextFunc)
{
BASE_OBJECT *psTemp = NULL;
bool isCB = false;
bool isRD = false;
if (!aiCheckAlliances(psSensor->player, psObj->player))
{
continue;
}
else if (psSensor->type == OBJ_DROID)
{
DROID *psDroid = (DROID *)psSensor;
ASSERT_OR_RETURN(NULL, psDroid->droidType == DROID_SENSOR, "A non-sensor droid in a sensor list is non-sense");
// Skip non-observing droids.
if (psDroid->action != DACTION_OBSERVE)
{
continue;
}
psTemp = psDroid->psActionTarget[0];
isCB = cbSensorDroid(psDroid);
isRD = objRadarDetector((BASE_OBJECT *)psDroid);
}
else if (psSensor->type == OBJ_STRUCTURE)
{
STRUCTURE *psCStruct = (STRUCTURE *)psSensor;
// skip incomplete structures
if (psCStruct->status != SS_BUILT)
{
continue;
}
psTemp = psCStruct->psTarget[0];
isCB = structCBSensor(psCStruct);
isRD = objRadarDetector((BASE_OBJECT *)psCStruct);
}
if (!psTemp || psTemp->died || !validTarget(psObj, psTemp, 0) || aiCheckAlliances(psTemp->player, psObj->player))
{
continue;
}
int distSq = objPosDiffSq(psTemp->pos, psObj->pos);
// Need to be in range, prefer closer targets or CB targets
if ((isCB > foundCB || (isCB == foundCB && distSq < tarDist)) && distSq > minDist)
{
if (aiObjHasRange(psObj, psTemp, weapon_slot) && visibleObject(psSensor, psTemp, false))
{
tarDist = distSq;
psTarget = psTemp;
if (targetOrigin)
{
*targetOrigin = ORIGIN_SENSOR;
}
if (isCB)
{
if (targetOrigin)
{
*targetOrigin = ORIGIN_CB_SENSOR;
}
foundCB = true; // got CB target, drop everything and shoot!
}
else if (isRD)
{
if (targetOrigin)
{
*targetOrigin = ORIGIN_RADAR_DETECTOR;
}
}
}
}
}
return psTarget;
}
/* Calculates attack priority for a certain target */
static SDWORD targetAttackWeight(BASE_OBJECT *psTarget, BASE_OBJECT *psAttacker, SDWORD weapon_slot)
{
SDWORD targetTypeBonus=0, damageRatio=0, attackWeight=0, noTarget=-1;
UDWORD weaponSlot;
DROID *targetDroid=NULL,*psAttackerDroid=NULL,*psGroupDroid,*psDroid;
STRUCTURE *targetStructure=NULL;
WEAPON_EFFECT weaponEffect;
WEAPON_STATS *attackerWeapon;
bool bEmpWeap=false,bCmdAttached=false,bTargetingCmd=false;
if (psTarget == NULL || psAttacker == NULL || psTarget->died)
{
return noTarget;
}
ASSERT(psTarget != psAttacker, "targetAttackWeight: Wanted to evaluate the worth of attacking ourselves...");
targetTypeBonus = 0; //Sensors/ecm droids, non-military structures get lower priority
/* Get attacker weapon effect */
if(psAttacker->type == OBJ_DROID)
{
psAttackerDroid = (DROID *)psAttacker;
attackerWeapon = (WEAPON_STATS *)(asWeaponStats + psAttackerDroid->asWeaps[weapon_slot].nStat);
//check if this droid is assigned to a commander
bCmdAttached = hasCommander(psAttackerDroid);
//find out if current target is targeting our commander
if(bCmdAttached)
{
if(psTarget->type == OBJ_DROID)
{
psDroid = (DROID *)psTarget;
//go through all enemy weapon slots
for(weaponSlot = 0; !bTargetingCmd &&
weaponSlot < ((DROID *)psTarget)->numWeaps; weaponSlot++)
{
//see if this weapon is targeting our commander
if (psDroid->psActionTarget[weaponSlot] == (BASE_OBJECT *)psAttackerDroid->psGroup->psCommander)
{
bTargetingCmd = true;
}
}
}
else
{
if(psTarget->type == OBJ_STRUCTURE)
{
//go through all enemy weapons
for(weaponSlot = 0; !bTargetingCmd && weaponSlot < ((STRUCTURE *)psTarget)->numWeaps; weaponSlot++)
{
if (((STRUCTURE *)psTarget)->psTarget[weaponSlot] ==
(BASE_OBJECT *)psAttackerDroid->psGroup->psCommander)
{
bTargetingCmd = true;
}
}
}
}
}
}
else if(psAttacker->type == OBJ_STRUCTURE)
{
attackerWeapon = ((WEAPON_STATS *)(asWeaponStats + ((STRUCTURE *)psAttacker)->asWeaps[weapon_slot].nStat));
}
else /* feature */
{
ASSERT(!"invalid attacker object type", "targetAttackWeight: Invalid attacker object type");
return noTarget;
}
//Get weapon effect
weaponEffect = attackerWeapon->weaponEffect;
//See if attacker is using an EMP weapon
bEmpWeap = (attackerWeapon->weaponSubClass == WSC_EMP);
int dist = iHypot(removeZ(psAttacker->pos - psTarget->pos));
bool tooClose = dist <= attackerWeapon->upgrade[psAttacker->player].minRange;
if (tooClose)
{
dist = objSensorRange(psAttacker); // If object is too close to fire at, consider it to be at maximum range.
}
/* Calculate attack weight */
if(psTarget->type == OBJ_DROID)
{
targetDroid = (DROID *)psTarget;
if (targetDroid->died)
{
debug(LOG_NEVER, "Target droid is dead, skipping invalid droid.\n");
return noTarget;
}
/* Calculate damage this target suffered */
if (targetDroid->originalBody == 0) // FIXME Somewhere we get 0HP droids from
{
damageRatio = 0;
debug(LOG_ERROR, "targetAttackWeight: 0HP droid detected!");
debug(LOG_ERROR, " Type: %i Name: \"%s\" Owner: %i \"%s\")",
targetDroid->droidType, targetDroid->aName, targetDroid->player, getPlayerName(targetDroid->player));
}
else
{
damageRatio = 100 - 100*targetDroid->body / targetDroid->originalBody;
}
assert(targetDroid->originalBody != 0); // Assert later so we get the info from above
/* See if this type of a droid should be prioritized */
switch (targetDroid->droidType)
{
case DROID_SENSOR:
case DROID_ECM:
case DROID_PERSON:
case DROID_TRANSPORTER:
case DROID_SUPERTRANSPORTER:
case DROID_DEFAULT:
case DROID_ANY:
break;
case DROID_CYBORG:
case DROID_WEAPON:
case DROID_CYBORG_SUPER:
targetTypeBonus = WEIGHT_WEAPON_DROIDS;
break;
case DROID_COMMAND:
targetTypeBonus = WEIGHT_COMMAND_DROIDS;
break;
case DROID_CONSTRUCT:
case DROID_REPAIR:
case DROID_CYBORG_CONSTRUCT:
case DROID_CYBORG_REPAIR:
targetTypeBonus = WEIGHT_SERVICE_DROIDS;
break;
}
/* Now calculate the overall weight */
attackWeight = asWeaponModifier[weaponEffect][(asPropulsionStats + targetDroid->asBits[COMP_PROPULSION])->propulsionType] // Our weapon's effect against target
+ asWeaponModifierBody[weaponEffect][(asBodyStats + targetDroid->asBits[COMP_BODY])->size]
+ WEIGHT_DIST_TILE_DROID * objSensorRange(psAttacker) / TILE_UNITS
- WEIGHT_DIST_TILE_DROID * dist/TILE_UNITS // farther droids are less attractive
+ WEIGHT_HEALTH_DROID * damageRatio/100 // we prefer damaged droids
+ targetTypeBonus; // some droid types have higher priority
/* If attacking with EMP try to avoid targets that were already "EMPed" */
if(bEmpWeap &&
(targetDroid->lastHitWeapon == WSC_EMP) &&
((gameTime - targetDroid->timeLastHit) < EMP_DISABLE_TIME)) //target still disabled
{
attackWeight /= EMP_DISABLED_PENALTY_F;
}
}
else if(psTarget->type == OBJ_STRUCTURE)
{
targetStructure = (STRUCTURE *)psTarget;
/* Calculate damage this target suffered */
damageRatio = 100 - 100*targetStructure->body / structureBody(targetStructure);
/* See if this type of a structure should be prioritized */
switch(targetStructure->pStructureType->type)
{
case REF_DEFENSE:
targetTypeBonus = WEIGHT_WEAPON_STRUCT;
break;
case REF_RESOURCE_EXTRACTOR:
targetTypeBonus = WEIGHT_DERRICK_STRUCT;
break;
case REF_FACTORY:
case REF_CYBORG_FACTORY:
case REF_REPAIR_FACILITY:
targetTypeBonus = WEIGHT_MILITARY_STRUCT;
break;
default:
break;
}
/* Now calculate the overall weight */
attackWeight = asStructStrengthModifier[weaponEffect][targetStructure->pStructureType->strength] // Our weapon's effect against target
+ WEIGHT_DIST_TILE_STRUCT * objSensorRange(psAttacker) / TILE_UNITS
- WEIGHT_DIST_TILE_STRUCT * dist/TILE_UNITS // farther structs are less attractive
+ WEIGHT_HEALTH_STRUCT * damageRatio/100 // we prefer damaged structures
+ targetTypeBonus; // some structure types have higher priority
/* Go for unfinished structures only if nothing else found (same for non-visible structures) */
if(targetStructure->status != SS_BUILT) //a decoy?
{
attackWeight /= WEIGHT_STRUCT_NOTBUILT_F;
}
/* EMP should only attack structures if no enemy droids are around */
if(bEmpWeap)
{
attackWeight /= EMP_STRUCT_PENALTY_F;
}
}
else //a feature
{
return 1;
}
/* We prefer objects we can see and can attack immediately */
if(!visibleObject((BASE_OBJECT *)psAttacker, psTarget, true))
{
attackWeight /= WEIGHT_NOT_VISIBLE_F;
}
if (tooClose)
{
attackWeight /= TOO_CLOSE_PENALTY_F;
}
/* Penalty for units that are already considered doomed (but the missile might miss!) */
if (aiObjectIsProbablyDoomed(psTarget))
{
/* indirect firing units have slow reload times, so give the target a chance to die,
* and give a different unit a chance to get in range, too. */
if (weaponROF(attackerWeapon, psAttacker->player) < TARGET_DOOMED_SLOW_RELOAD_T) {
debug(LOG_NEVER,"Not killing unit - doomed. My ROF: %i (%s)", weaponROF(attackerWeapon, psAttacker->player), getName(attackerWeapon));
return noTarget;
}
attackWeight /= TARGET_DOOMED_PENALTY_F;
}
/* Commander-related criterias */
if(bCmdAttached) //attached to a commander and don't have a target assigned by some order
{
ASSERT(psAttackerDroid->psGroup->psCommander != NULL, "Commander is NULL");
//if commander is being targeted by our target, try to defend the commander
if(bTargetingCmd)
{
attackWeight += WEIGHT_CMD_RANK * ( 1 + getDroidLevel(psAttackerDroid->psGroup->psCommander));
}
//fire support - go through all droids assigned to the commander
for (psGroupDroid = psAttackerDroid->psGroup->psList; psGroupDroid; psGroupDroid = psGroupDroid->psGrpNext)
{
for(weaponSlot = 0; weaponSlot < psGroupDroid->numWeaps; weaponSlot++)
{
//see if this droid is currently targeting current target
if(psGroupDroid->order.psObj == psTarget ||
psGroupDroid->psActionTarget[weaponSlot] == psTarget)
{
//we prefer targets that are already targeted and hence will be destroyed faster
attackWeight += WEIGHT_CMD_SAME_TARGET;
}
}
}
}
return attackWeight;
}
// Find the best nearest target for a droid.
// If extraRange is higher than zero, then this is the range it accepts for movement to target.
// Returns integer representing target priority, -1 if failed
int aiBestNearestTarget(DROID *psDroid, BASE_OBJECT **ppsObj, int weapon_slot, int extraRange)
{
SDWORD bestMod = 0,newMod, failure = -1;
BASE_OBJECT *psTarget = NULL, *bestTarget = NULL, *tempTarget;
bool electronic = false;
STRUCTURE *targetStructure;
WEAPON_EFFECT weaponEffect;
UWORD tmpOrigin = ORIGIN_UNKNOWN;
//don't bother looking if empty vtol droid
if (vtolEmpty(psDroid))
{
return failure;
}
/* Return if have no weapons */
// The ai orders a non-combat droid to patrol = crash without it...
if ((psDroid->asWeaps[0].nStat == 0 || psDroid->numWeaps == 0) && psDroid->droidType != DROID_SENSOR)
{
return failure;
}
// Check if we have a CB target to begin with
if (!proj_Direct(asWeaponStats + psDroid->asWeaps[weapon_slot].nStat))
{
WEAPON_STATS *psWStats = psDroid->asWeaps[weapon_slot].nStat + asWeaponStats;
bestTarget = aiSearchSensorTargets((BASE_OBJECT *)psDroid, weapon_slot, psWStats, &tmpOrigin);
bestMod = targetAttackWeight(bestTarget, (BASE_OBJECT *)psDroid, weapon_slot);
}
weaponEffect = (asWeaponStats + psDroid->asWeaps[weapon_slot].nStat)->weaponEffect;
electronic = electronicDroid(psDroid);
// Range was previously 9*TILE_UNITS. Increasing this doesn't seem to help much, though. Not sure why.
int droidRange = std::min(aiDroidRange(psDroid, weapon_slot) + extraRange, objSensorRange(psDroid) + 6*TILE_UNITS);
static GridList gridList; // static to avoid allocations.
gridList = gridStartIterate(psDroid->pos.x, psDroid->pos.y, droidRange);
for (GridIterator gi = gridList.begin(); gi != gridList.end(); ++gi)
{
BASE_OBJECT *friendlyObj = NULL;
BASE_OBJECT *targetInQuestion = *gi;
/* This is a friendly unit, check if we can reuse its target */
if(aiCheckAlliances(targetInQuestion->player,psDroid->player))
{
friendlyObj = targetInQuestion;
targetInQuestion = NULL;
/* Can we see what it is doing? */
if(friendlyObj->visible[psDroid->player] == UBYTE_MAX)
{
if(friendlyObj->type == OBJ_DROID)
{
DROID *friendlyDroid = (DROID *)friendlyObj;
/* See if friendly droid has a target */
tempTarget = friendlyDroid->psActionTarget[0];
if(tempTarget && !tempTarget->died)
{
//make sure a weapon droid is targeting it
if(friendlyDroid->numWeaps > 0)
{
// make sure this target wasn't assigned explicitly to this droid
if (friendlyDroid->order.type != DORDER_ATTACK)
{
targetInQuestion = tempTarget; //consider this target
}
}
}
}
else if(friendlyObj->type == OBJ_STRUCTURE)
{
tempTarget = ((STRUCTURE*)friendlyObj)->psTarget[0];
if (tempTarget && !tempTarget->died)
{
targetInQuestion = tempTarget;
}
}
}
}
if (targetInQuestion != NULL
&& targetInQuestion != psDroid // in case friendly unit had me as target
&& (targetInQuestion->type == OBJ_DROID || targetInQuestion->type == OBJ_STRUCTURE || targetInQuestion->type == OBJ_FEATURE)
&& targetInQuestion->visible[psDroid->player] == UBYTE_MAX
&& !aiCheckAlliances(targetInQuestion->player,psDroid->player)
&& validTarget(psDroid, targetInQuestion, weapon_slot)
&& objPosDiffSq(psDroid, targetInQuestion) < droidRange*droidRange)
{
if (targetInQuestion->type == OBJ_DROID)
{
// in multiPlayer - don't attack Transporters with EW
if (bMultiPlayer)
{
// if not electronic then valid target
if (!electronic
|| (electronic
&& (((DROID *)targetInQuestion)->droidType != DROID_TRANSPORTER && ((DROID *)targetInQuestion)->droidType != DROID_SUPERTRANSPORTER)))
{
//only a valid target if NOT a transporter
psTarget = targetInQuestion;
}
}
else
{
psTarget = targetInQuestion;
}
}
else if (targetInQuestion->type == OBJ_STRUCTURE)
{
STRUCTURE *psStruct = (STRUCTURE *)targetInQuestion;
if (electronic)
{
/* don't want to target structures with resistance of zero if using electronic warfare */
if (validStructResistance((STRUCTURE *)targetInQuestion))
{
psTarget = targetInQuestion;
}
}
else if (psStruct->asWeaps[0].nStat > 0)
{
// structure with weapons - go for this
psTarget = targetInQuestion;
}
else if ((psStruct->pStructureType->type != REF_WALL && psStruct->pStructureType->type != REF_WALLCORNER)
|| driveModeActive() || (bMultiPlayer && !isHumanPlayer(psDroid->player)))
{
psTarget = targetInQuestion;
}
}
else if (targetInQuestion->type == OBJ_FEATURE
&& psDroid->lastFrustratedTime > 0
&& gameTime - psDroid->lastFrustratedTime < FRUSTRATED_TIME
&& ((FEATURE *)targetInQuestion)->psStats->damageable
&& psDroid->player != scavengerPlayer()) // hack to avoid scavs blowing up their nice feature walls
{
psTarget = targetInQuestion;
objTrace(psDroid->id, "considering shooting at %s in frustration", objInfo(targetInQuestion));
}
/* Check if our weapon is most effective against this object */
if(psTarget != NULL && psTarget == targetInQuestion) //was assigned?
{
newMod = targetAttackWeight(psTarget, (BASE_OBJECT *)psDroid, weapon_slot);
/* Remember this one if it's our best target so far */
if( newMod >= 0 && (newMod > bestMod || bestTarget == NULL))
{
bestMod = newMod;
tmpOrigin = ORIGIN_ALLY;
bestTarget = psTarget;
}
}
}
}
if (bestTarget)
{
ASSERT(!bestTarget->died, "aiBestNearestTarget: AI gave us a target that is already dead.");
targetStructure = visGetBlockingWall((BASE_OBJECT *)psDroid, bestTarget);
/* See if target is blocked by a wall; only affects direct weapons */
if (proj_Direct(asWeaponStats + psDroid->asWeaps[weapon_slot].nStat)
&& targetStructure)
{
//are we any good against walls?
if(asStructStrengthModifier[weaponEffect][targetStructure->pStructureType->strength] >= 100) //can attack atleast with default strength
{
bestTarget = (BASE_OBJECT *)targetStructure; //attack wall
}
}
*ppsObj = bestTarget;
return bestMod;
}
return failure;
}
// Are there a lot of bullets heading towards the droid?
static bool aiDroidIsProbablyDoomed(DROID *psDroid)
{
return psDroid->expectedDamage > psDroid->body
&& psDroid->expectedDamage - psDroid->body > psDroid->body/5; // Doomed if projectiles will damage 120% of remaining body points.
}
// Are there a lot of bullets heading towards the structure?
static bool aiStructureIsProbablyDoomed(STRUCTURE *psStructure)
{
return psStructure->expectedDamage > psStructure->body
&& psStructure->expectedDamage - psStructure->body > psStructure->body/15; // Doomed if projectiles will damage 106.6666666667% of remaining body points.
}
// Are there a lot of bullets heading towards the object?
bool aiObjectIsProbablyDoomed(BASE_OBJECT *psObject)
{
if (psObject->died)
return true; // Was definitely doomed.
switch (psObject->type)
{
case OBJ_DROID:
return aiDroidIsProbablyDoomed((DROID *)psObject);
case OBJ_STRUCTURE:
return aiStructureIsProbablyDoomed((STRUCTURE *)psObject);
default:
return false;
}
}
// Update the expected damage of the object.
void aiObjectAddExpectedDamage(BASE_OBJECT *psObject, SDWORD damage)
{
if (psObject == NULL)
return; // Hard to destroy the ground.
switch (psObject->type)
{
case OBJ_DROID:
((DROID *)psObject)->expectedDamage += damage;
ASSERT((SDWORD)((DROID *)psObject)->expectedDamage >= 0, "aiObjectAddExpectedDamage: Negative amount of projectiles heading towards droid.");
break;
case OBJ_STRUCTURE:
((STRUCTURE *)psObject)->expectedDamage += damage;
ASSERT((SDWORD)((STRUCTURE *)psObject)->expectedDamage >= 0, "aiObjectAddExpectedDamage: Negative amount of projectiles heading towards droid.");
break;
default:
break;
}
}
// see if an object is a wall
static bool aiObjIsWall(BASE_OBJECT *psObj)
{
if (psObj->type != OBJ_STRUCTURE)
{
return false;
}
if ( ((STRUCTURE *)psObj)->pStructureType->type != REF_WALL &&
((STRUCTURE *)psObj)->pStructureType->type != REF_WALLCORNER )
{
return false;
}
return true;
}
/* See if there is a target in range */
bool aiChooseTarget(BASE_OBJECT *psObj, BASE_OBJECT **ppsTarget, int weapon_slot, bool bUpdateTarget, UWORD *targetOrigin)
{
BASE_OBJECT *psTarget = NULL;
DROID *psCommander;
SDWORD curTargetWeight=-1;
UWORD tmpOrigin = ORIGIN_UNKNOWN;
if (targetOrigin)
{
*targetOrigin = ORIGIN_UNKNOWN;
}
switch (psObj->type)
{
case OBJ_DROID:
if (((DROID *)psObj)->asWeaps[weapon_slot].nStat == 0)
{
return false;
}
if (((DROID *)psObj)->asWeaps[0].nStat == 0 &&
((DROID *)psObj)->droidType != DROID_SENSOR)
{
return false; // Can't target without a weapon or sensor
}
break;
case OBJ_STRUCTURE:
if (((STRUCTURE *)psObj)->numWeaps == 0 || ((STRUCTURE *)psObj)->asWeaps[0].nStat == 0)
{
// Can't attack without a weapon
return false;
}
break;
default:
break;
}
/* See if there is a something in range */
if (psObj->type == OBJ_DROID)
{
BASE_OBJECT *psCurrTarget = ((DROID *)psObj)->psActionTarget[0];
/* find a new target */
int newTargetWeight = aiBestNearestTarget((DROID *)psObj, &psTarget, weapon_slot);
/* Calculate weight of the current target if updating; but take care not to target
* ourselves... */
if (bUpdateTarget && psCurrTarget != psObj)
{
curTargetWeight = targetAttackWeight(psCurrTarget, psObj, weapon_slot);
}
if (newTargetWeight >= 0 // found a new target
&& (!bUpdateTarget // choosing a new target, don't care if current one is better
|| curTargetWeight <= 0 // attacker had no valid target, use new one
|| newTargetWeight > curTargetWeight + OLD_TARGET_THRESHOLD) // updating and new target is better
&& validTarget(psObj, psTarget, weapon_slot)
&& aiDroidHasRange((DROID *)psObj, psTarget, weapon_slot))
{
ASSERT(!isDead(psTarget), "aiChooseTarget: Droid found a dead target!");
*ppsTarget = psTarget;
return true;
}
}
else if (psObj->type == OBJ_STRUCTURE)
{
WEAPON_STATS *psWStats = NULL;
bool bCommanderBlock = false;
ASSERT(((STRUCTURE *)psObj)->asWeaps[weapon_slot].nStat > 0, "no weapons on structure");
psWStats = ((STRUCTURE *)psObj)->asWeaps[weapon_slot].nStat + asWeaponStats;
int longRange = proj_GetLongRange(psWStats, psObj->player);
// see if there is a target from the command droids
psTarget = NULL;
psCommander = cmdDroidGetDesignator(psObj->player);
if (!proj_Direct(psWStats) && (psCommander != NULL) &&
aiStructHasRange((STRUCTURE *)psObj, (BASE_OBJECT *)psCommander, weapon_slot))
{
// there is a commander that can fire designate for this structure
// set bCommanderBlock so that the structure does not fire until the commander
// has a target - (slow firing weapons will not be ready to fire otherwise).
bCommanderBlock = true;
// I do believe this will never happen, check for yourself :-)
debug(LOG_NEVER,"Commander %d is good enough for fire designation", psCommander->id);
if (psCommander->action == DACTION_ATTACK
&& psCommander->psActionTarget[0] != NULL
&& !psCommander->psActionTarget[0]->died)
{
// the commander has a target to fire on
if (aiStructHasRange((STRUCTURE *)psObj, psCommander->psActionTarget[0], weapon_slot))
{
// target in range - fire on it
tmpOrigin = ORIGIN_COMMANDER;
psTarget = psCommander->psActionTarget[0];
}
else
{
// target out of range - release the commander block
bCommanderBlock = false;
}
}
}
// indirect fire structures use sensor towers first
if (psTarget == NULL && !bCommanderBlock && !proj_Direct(psWStats))
{
psTarget = aiSearchSensorTargets(psObj, weapon_slot, psWStats, &tmpOrigin);
}
if (psTarget == NULL && !bCommanderBlock)
{
int targetValue = -1;
int tarDist = INT32_MAX;
int srange = longRange;
if (!proj_Direct(psWStats) && srange > objSensorRange(psObj))
{
// search radius of indirect weapons limited by their sight, unless they use
// external sensors to provide fire designation
srange = objSensorRange(psObj);
}
static GridList gridList; // static to avoid allocations.
gridList = gridStartIterate(psObj->pos.x, psObj->pos.y, srange);
for (GridIterator gi = gridList.begin(); gi != gridList.end(); ++gi)
{
BASE_OBJECT *psCurr = *gi;
/* Check that it is a valid target */
if (psCurr->type != OBJ_FEATURE && !psCurr->died
&& !aiCheckAlliances(psCurr->player, psObj->player)
&& validTarget(psObj, psCurr, weapon_slot) && psCurr->visible[psObj->player] == UBYTE_MAX
&& aiStructHasRange((STRUCTURE *)psObj, psCurr, weapon_slot))
{
int newTargetValue = targetAttackWeight(psCurr, psObj, weapon_slot);
// See if in sensor range and visible
int distSq = objPosDiffSq(psCurr->pos, psObj->pos);
if (newTargetValue < targetValue || (newTargetValue == targetValue && distSq >= tarDist))
{
continue;
}
tmpOrigin = ORIGIN_VISUAL;
psTarget = psCurr;
tarDist = distSq;
targetValue = newTargetValue;
}
}
}
if (psTarget)
{
ASSERT(!psTarget->died, "aiChooseTarget: Structure found a dead target!");
if (targetOrigin)
{
*targetOrigin = tmpOrigin;
}
*ppsTarget = psTarget;
return true;
}
}
return false;
}
/* See if there is a target in range for Sensor objects*/
bool aiChooseSensorTarget(BASE_OBJECT *psObj, BASE_OBJECT **ppsTarget)
{
int sensorRange = objSensorRange(psObj);
unsigned int radSquared = sensorRange * sensorRange;
bool radarDetector = objRadarDetector(psObj);
if (!objActiveRadar(psObj) && !radarDetector)
{
ASSERT(false, "Only to be used for sensor turrets!");
return false;
}
/* See if there is something in range */
if (psObj->type == OBJ_DROID)
{
BASE_OBJECT *psTarget = NULL;
if (aiBestNearestTarget((DROID *)psObj, &psTarget, 0) >= 0)
{
/* See if in sensor range */
const int xdiff = psTarget->pos.x - psObj->pos.x;
const int ydiff = psTarget->pos.y - psObj->pos.y;
const unsigned int distSq = xdiff * xdiff + ydiff * ydiff;
// I do believe this will never happen, check for yourself :-)
debug(LOG_NEVER, "Sensor droid(%d) found possible target(%d)!!!", psObj->id, psTarget->id);
if (distSq < radSquared)
{
*ppsTarget = psTarget;
return true;
}
}
}
else // structure
{
BASE_OBJECT * psTemp = NULL;
int tarDist = SDWORD_MAX;
static GridList gridList; // static to avoid allocations.
gridList = gridStartIterate(psObj->pos.x, psObj->pos.y, objSensorRange(psObj));
for (GridIterator gi = gridList.begin(); gi != gridList.end(); ++gi)
{
BASE_OBJECT *psCurr = *gi;
// Don't target features or doomed/dead objects
if (psCurr->type != OBJ_FEATURE && !psCurr->died)
{
if (!aiCheckAlliances(psCurr->player,psObj->player) && !aiObjIsWall(psCurr))
{
// See if in sensor range and visible
const int xdiff = psCurr->pos.x - psObj->pos.x;
const int ydiff = psCurr->pos.y - psObj->pos.y;
const unsigned int distSq = xdiff * xdiff + ydiff * ydiff;
if (distSq < radSquared && psCurr->visible[psObj->player] == UBYTE_MAX && distSq < tarDist)
{
psTemp = psCurr;
tarDist = distSq;
}
}
}
}
if (psTemp)
{
ASSERT(!psTemp->died, "aiChooseSensorTarget gave us a dead target");
*ppsTarget = psTemp;
return true;
}
}
return false;
}
/* Make droid/structure look for a better target */
static bool updateAttackTarget(BASE_OBJECT * psAttacker, SDWORD weapon_slot)
{
BASE_OBJECT *psBetterTarget=NULL;
UWORD tmpOrigin = ORIGIN_UNKNOWN;
if(aiChooseTarget(psAttacker, &psBetterTarget, weapon_slot, true, &tmpOrigin)) //update target
{
if(psAttacker->type == OBJ_DROID)
{
DROID *psDroid = (DROID *)psAttacker;
if( (orderState(psDroid, DORDER_NONE) ||
orderState(psDroid, DORDER_GUARD) ||
orderState(psDroid, DORDER_ATTACKTARGET)) &&
weapon_slot == 0) //Watermelon:only primary slot(0) updates affect order
{
actionDroid((DROID *)psAttacker, DACTION_ATTACK, psBetterTarget);
}
else //can't override current order
{
setDroidActionTarget(psDroid, psBetterTarget, weapon_slot);
}
}
else if (psAttacker->type == OBJ_STRUCTURE)
{
STRUCTURE *psBuilding = (STRUCTURE *)psAttacker;
setStructureTarget(psBuilding, psBetterTarget, weapon_slot, tmpOrigin);
}
return true;
}
return false;
}
/* Do the AI for a droid */
void aiUpdateDroid(DROID *psDroid)
{
BASE_OBJECT *psTarget;
bool lookForTarget,updateTarget;
ASSERT(psDroid != NULL, "Invalid droid pointer");
if (!psDroid || isDead((BASE_OBJECT *)psDroid))
{
return;
}
lookForTarget = false;
updateTarget = false;
// look for a target if doing nothing
if (orderState(psDroid, DORDER_NONE) ||
orderState(psDroid, DORDER_GUARD) ||
orderState(psDroid, DORDER_HOLD))
{
lookForTarget = true;
}
// but do not choose another target if doing anything while guarding
if (orderState(psDroid, DORDER_GUARD) &&
(psDroid->action != DACTION_NONE))
{
lookForTarget = false;
}
// don't look for a target if sulking
if (psDroid->action == DACTION_SULK)
{
lookForTarget = false;
}
/* Only try to update target if already have some target */
if (psDroid->action == DACTION_ATTACK ||
psDroid->action == DACTION_MOVEFIRE ||
psDroid->action == DACTION_MOVETOATTACK ||
psDroid->action == DACTION_ROTATETOATTACK)
{
updateTarget = true;
}
if ((orderState(psDroid, DORDER_OBSERVE) || orderState(psDroid, DORDER_ATTACKTARGET)) &&
psDroid->order.psObj && psDroid->order.psObj->died)
{
lookForTarget = true;
updateTarget = false;
}
/* Don't update target if we are sent to attack and reached
attack destination (attacking our target) */
if (orderState(psDroid, DORDER_ATTACK) && psDroid->psActionTarget[0] == psDroid->order.psObj)
{
updateTarget = false;
}
// don't look for a target if there are any queued orders
if (psDroid->listSize > 0)
{
lookForTarget = false;
updateTarget = false;
}
// don't allow units to start attacking if they will switch to guarding the commander
if(hasCommander(psDroid))
{
lookForTarget = false;
updateTarget = false;
}
if(bMultiPlayer && isVtolDroid(psDroid) && isHumanPlayer(psDroid->player))
{
lookForTarget = false;
updateTarget = false;
}
// do not look for a target if droid is currently under direct control.
if(driveModeActive() && (psDroid == driveGetDriven())) {
lookForTarget = false;
updateTarget = false;
}
// CB and VTOL CB droids can't autotarget.
if (psDroid->droidType == DROID_SENSOR && !standardSensorDroid(psDroid))
{
lookForTarget = false;
updateTarget = false;
}
// do not attack if the attack level is wrong
if (secondaryGetState(psDroid, DSO_ATTACK_LEVEL) != DSS_ALEV_ALWAYS)
{
lookForTarget = false;
}
/* For commanders and non-assigned non-commanders:
look for a better target once in a while */
if(!lookForTarget && updateTarget)
{
if((psDroid->numWeaps > 0) && !hasCommander(psDroid)) //not assigned to commander
{
if((psDroid->id + gameTime)/TARGET_UPD_SKIP_FRAMES != (psDroid->id + gameTime - deltaGameTime)/TARGET_UPD_SKIP_FRAMES)
{
unsigned int i;
(void)updateAttackTarget((BASE_OBJECT*)psDroid, 0); // this function always has to be called on weapon-slot 0 (even if ->numWeaps == 0)
//updates all targets
for (i = 1; i < psDroid->numWeaps; ++i)
{
(void)updateAttackTarget((BASE_OBJECT*)psDroid, i);
}
}
}
}
/* Null target - see if there is an enemy to attack */
if (lookForTarget && !updateTarget)
{
if (psDroid->droidType == DROID_SENSOR)
{
if (aiChooseSensorTarget((BASE_OBJECT *)psDroid, &psTarget))
{
actionDroid(psDroid, DACTION_OBSERVE, psTarget);
}
}
else
{
if (aiChooseTarget((BASE_OBJECT *)psDroid, &psTarget, 0, true, NULL))
{
actionDroid(psDroid, DACTION_ATTACK, psTarget);
}
}
}
}
/* Check if any of our weapons can hit the target... */
bool checkAnyWeaponsTarget(BASE_OBJECT *psObject, BASE_OBJECT *psTarget)
{
DROID *psDroid = (DROID *) psObject;
for (int i = 0;i < psDroid->numWeaps;i++)
{
if (validTarget(psObject, psTarget, i))
{
return true;
}
}
return false;
}
/* Set of rules which determine whether the weapon associated with the object can fire on the propulsion type of the target. */
bool validTarget(BASE_OBJECT *psObject, BASE_OBJECT *psTarget, int weapon_slot)
{
bool bTargetInAir = false, bValidTarget = false;
UBYTE surfaceToAir = 0;
if (!psTarget)
{
return false;
}
// Need to check propulsion type of target
switch (psTarget->type)
{
case OBJ_DROID:
if (asPropulsionTypes[asPropulsionStats[((DROID *)psTarget)->asBits[COMP_PROPULSION]].propulsionType].travel == AIR)
{
if (((DROID *)psTarget)->sMove.Status != MOVEINACTIVE)
{
bTargetInAir = true;
}
else
{
bTargetInAir = false;
}
}
else
{
bTargetInAir = false;
}
break;
case OBJ_STRUCTURE:
default:
//lets hope so!
bTargetInAir = false;
break;
}
//need what can shoot at
switch (psObject->type)
{
case OBJ_DROID:
if (((DROID *)psObject)->droidType == DROID_SENSOR)
{
return !bTargetInAir; // Sensor droids should not target anything in the air.
}
// Can't attack without a weapon
if (((DROID *)psObject)->numWeaps != 0 && ((DROID *)psObject)->asWeaps[weapon_slot].nStat != 0)
{
surfaceToAir = asWeaponStats[((DROID *)psObject)->asWeaps[weapon_slot].nStat].surfaceToAir;
if (((surfaceToAir & SHOOT_IN_AIR) && bTargetInAir) || ((surfaceToAir & SHOOT_ON_GROUND) && !bTargetInAir))
{
return true;
}
}
else
{
return false;
}
break;
case OBJ_STRUCTURE:
// Can't attack without a weapon
//Watermelon:re-enabled if (((DROID *)psObject)->numWeaps != 0) to prevent crash
if (((STRUCTURE *)psObject)->numWeaps != 0 &&
((STRUCTURE *)psObject)->asWeaps[weapon_slot].nStat != 0)
{
surfaceToAir = asWeaponStats[((STRUCTURE *)psObject)->asWeaps[weapon_slot].nStat].surfaceToAir;
}
else
{
surfaceToAir = 0;
}
if (((surfaceToAir & SHOOT_IN_AIR) && bTargetInAir) || ((surfaceToAir & SHOOT_ON_GROUND) && !bTargetInAir))
{
return true;
}
break;
default:
surfaceToAir = 0;
break;
}
//if target is in the air and you can shoot in the air - OK
if (bTargetInAir && (surfaceToAir & SHOOT_IN_AIR))
{
bValidTarget = true;
}
//if target is on the ground and can shoot at it - OK
if (!bTargetInAir && (surfaceToAir & SHOOT_ON_GROUND))
{
bValidTarget = true;
}
return bValidTarget;
}