1188 lines
32 KiB
C
1188 lines
32 KiB
C
/*
|
|
This file is part of Warzone 2100.
|
|
Copyright (C) 1999-2004 Eidos Interactive
|
|
Copyright (C) 2005-2007 Warzone Resurrection 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 "geometry.h"
|
|
#include "map.h"
|
|
#include "mapgrid.h"
|
|
#include "multiplay.h"
|
|
#include "projectile.h"
|
|
#include "visibility.h"
|
|
|
|
/* Calculates attack priority for a certain target */
|
|
static SDWORD targetAttackWeight(BASE_OBJECT *psTarget, BASE_OBJECT *psAttacker, SDWORD weapon_slot);
|
|
|
|
// alliances
|
|
UBYTE alliances[MAX_PLAYERS][MAX_PLAYERS];
|
|
|
|
/* alliance code for ai. return true if an alliance has formed. */
|
|
BOOL aiCheckAlliances(UDWORD s1,UDWORD s2)
|
|
{
|
|
//features have their player number set to (MAX_PLAYERS + 1)
|
|
if ( s1 == (MAX_PLAYERS + 1) || s2 == (MAX_PLAYERS + 1))
|
|
{
|
|
return false;
|
|
}
|
|
if ((s1 == s2) ||
|
|
(alliances[s1][s2] == ALLIANCE_FORMED))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/* Initialise the AI system */
|
|
BOOL aiInitialise(void)
|
|
{
|
|
SDWORD i,j;
|
|
|
|
for(i=0; i<MAX_PLAYERS; i++)
|
|
{
|
|
for(j=0; j<MAX_PLAYERS; j++)
|
|
{
|
|
alliances[i][j] = ALLIANCE_BROKEN;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/* Shutdown the AI system */
|
|
BOOL aiShutdown(void)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Find the best nearest target for a droid
|
|
// Returns integer representing target priority, -1 if failed
|
|
SDWORD aiBestNearestTarget(DROID *psDroid, BASE_OBJECT **ppsObj, int weapon_slot)
|
|
{
|
|
UDWORD i;
|
|
SDWORD bestMod,newMod,failure=-1;
|
|
BASE_OBJECT *psTarget,*friendlyObj,*bestTarget,*targetInQuestion,*tempTarget;
|
|
BOOL electronic = false;
|
|
STRUCTURE *targetStructure;
|
|
WEAPON_EFFECT weaponEffect;
|
|
|
|
//don't bother looking if empty vtol droid
|
|
if (vtolEmpty(psDroid))
|
|
{
|
|
return failure;
|
|
}
|
|
|
|
/* Return if have no weapons */
|
|
// Watermelon:added a protection against no weapon droid 'numWeaps'
|
|
// The ai orders a non-combat droid to patrol = crash without it...
|
|
if(psDroid->asWeaps[0].nStat == 0 || psDroid->numWeaps == 0)
|
|
{
|
|
return failure;
|
|
}
|
|
|
|
droidGetNaybors(psDroid);
|
|
|
|
//weaponMod = asWeaponModifier[weaponEffect][(asPropulsionStats + ((DROID*)psObj)->asBits[COMP_PROPULSION].nStat)->propulsionType];
|
|
weaponEffect = ((WEAPON_STATS *)(asWeaponStats + psDroid->asWeaps[weapon_slot].nStat))->weaponEffect;
|
|
|
|
//electronic warfare can only be used against structures at present - not any more! AB 6/11/98
|
|
electronic = electronicDroid(psDroid);
|
|
psTarget = NULL;
|
|
bestTarget = NULL;
|
|
bestMod = 0;
|
|
for (i=0; i< numNaybors; i++)
|
|
{
|
|
friendlyObj = NULL;
|
|
targetInQuestion = asDroidNaybors[i].psObj;
|
|
|
|
/* This is a friendly unit, check if we can reuse its target */
|
|
if(targetInQuestion->player == psDroid->player ||
|
|
aiCheckAlliances(targetInQuestion->player,psDroid->player))
|
|
{
|
|
friendlyObj = targetInQuestion;
|
|
targetInQuestion = NULL;
|
|
|
|
/* Can we see what it is doing? */
|
|
if(friendlyObj->visible[psDroid->player])
|
|
{
|
|
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 != DORDER_ATTACK)
|
|
{
|
|
// make sure target is near enough
|
|
if (dirtySqrt(psDroid->pos.x, psDroid->pos.y, tempTarget->pos.x, tempTarget->pos.y)
|
|
< droidSensorRange(psDroid))
|
|
{
|
|
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 != (BASE_OBJECT *)psDroid && //in case friendly unit had me as target
|
|
(targetInQuestion->type == OBJ_DROID ||
|
|
targetInQuestion->type == OBJ_STRUCTURE) &&
|
|
targetInQuestion->visible[psDroid->player] &&
|
|
targetInQuestion->player != psDroid->player &&
|
|
!aiCheckAlliances(targetInQuestion->player,psDroid->player))
|
|
{
|
|
|
|
if ( !validTarget((BASE_OBJECT *)psDroid, targetInQuestion, weapon_slot) )
|
|
{
|
|
continue;
|
|
}
|
|
else 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))
|
|
{
|
|
//only a valid target if NOT a transporter
|
|
psTarget = targetInQuestion;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
psTarget = targetInQuestion;
|
|
}
|
|
}
|
|
else if (targetInQuestion->type == OBJ_STRUCTURE)
|
|
{
|
|
if (electronic)
|
|
{
|
|
/* don't want to target structures with resistance of zero if using electronic warfare */
|
|
if (validStructResistance((STRUCTURE *)targetInQuestion))
|
|
{
|
|
psTarget = targetInQuestion;
|
|
}
|
|
}
|
|
else if (((STRUCTURE *)targetInQuestion)->asWeaps[weapon_slot].nStat > 0)
|
|
{
|
|
// structure with weapons - go for this
|
|
psTarget = targetInQuestion;
|
|
}
|
|
else if ( ( ((STRUCTURE *)targetInQuestion)->pStructureType->type != REF_WALL
|
|
&&((STRUCTURE *)targetInQuestion)->pStructureType->type != REF_WALLCORNER
|
|
)
|
|
|| driveModeActive()
|
|
|| (bMultiPlayer && game.type == SKIRMISH && !isHumanPlayer(psDroid->player))
|
|
)
|
|
{
|
|
psTarget = 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;
|
|
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;
|
|
}
|
|
|
|
/* 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)
|
|
{
|
|
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);
|
|
|
|
/* 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 = 1 - 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_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].nStat)->propulsionType] // Our weapon's effect against target
|
|
- WEIGHT_DIST_TILE_DROID * map_coord(dirtySqrt(psAttacker->pos.x, psAttacker->pos.y, targetDroid->pos.x, targetDroid->pos.y)) // farer droids are less attractive
|
|
+ WEIGHT_HEALTH_DROID * damageRatio // 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 = 1 - 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 * map_coord(dirtySqrt(psAttacker->pos.x, psAttacker->pos.y, targetStructure->pos.x, targetStructure->pos.y)) // farer structs are less attractive
|
|
+ WEIGHT_HEALTH_STRUCT * damageRatio // 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 noTarget;
|
|
}
|
|
|
|
/* We prefer objects we can see and can attack immediately */
|
|
if(!visibleObject((BASE_OBJECT *)psAttacker, psTarget, true))
|
|
{
|
|
attackWeight /= WEIGHT_NOT_VISIBLE_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->psTarget == 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;
|
|
}
|
|
|
|
|
|
// see if a structure has the range to fire on a target
|
|
static BOOL aiStructHasRange(STRUCTURE *psStruct, BASE_OBJECT *psTarget, int weapon_slot)
|
|
{
|
|
WEAPON_STATS *psWStats;
|
|
SDWORD xdiff,ydiff, longRange;
|
|
|
|
if (psStruct->numWeaps == 0 || psStruct->asWeaps[0].nStat == 0)
|
|
{
|
|
// Can't attack without a weapon
|
|
return false;
|
|
}
|
|
|
|
psWStats = psStruct->asWeaps[weapon_slot].nStat + asWeaponStats;
|
|
|
|
xdiff = (SDWORD)psStruct->pos.x - (SDWORD)psTarget->pos.x;
|
|
ydiff = (SDWORD)psStruct->pos.y - (SDWORD)psTarget->pos.y;
|
|
longRange = proj_GetLongRange(psWStats);
|
|
if (xdiff*xdiff + ydiff*ydiff < longRange*longRange)
|
|
{
|
|
// in range
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
// 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)
|
|
{
|
|
UDWORD radSquared;
|
|
BASE_OBJECT *psTarget = NULL;
|
|
SDWORD xdiff,ydiff, distSq, tarDist, minDist;//, longRange;
|
|
BOOL bCBTower;
|
|
STRUCTURE *psCStruct;
|
|
DROID *psCommander;
|
|
BOOL bCommanderBlock;
|
|
UDWORD sensorRange = objSensorRange(psObj);
|
|
SECONDARY_STATE state;
|
|
SDWORD curTargetWeight=-1,newTargetWeight;
|
|
|
|
/* Get the sensor range */
|
|
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)
|
|
{
|
|
// Can't attack without a weapon
|
|
return false;
|
|
}
|
|
radSquared = sensorRange * sensorRange;
|
|
break;
|
|
case OBJ_STRUCTURE:
|
|
if (((STRUCTURE *)psObj)->numWeaps == 0 || ((STRUCTURE *)psObj)->asWeaps[0].nStat == 0)
|
|
{
|
|
// Can't attack without a weapon
|
|
return false;
|
|
}
|
|
|
|
// increase the sensor range for AA sites
|
|
// AA sites are defensive structures that can only shoot in the air
|
|
if ( (((STRUCTURE *)psObj)->pStructureType->type == REF_DEFENSE) &&
|
|
(asWeaponStats[((STRUCTURE *)psObj)->asWeaps[weapon_slot].nStat].surfaceToAir == SHOOT_IN_AIR) )
|
|
{
|
|
sensorRange = 3 * sensorRange / 2;
|
|
}
|
|
|
|
radSquared = sensorRange*sensorRange;
|
|
break;
|
|
default:
|
|
radSquared = 0;
|
|
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 */
|
|
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
|
|
))
|
|
{
|
|
/*check its a valid target*/
|
|
if (validTarget(psObj, psTarget, weapon_slot))
|
|
{
|
|
/* See if in sensor range */
|
|
xdiff = psTarget->pos.x - psObj->pos.x;
|
|
ydiff = psTarget->pos.y - psObj->pos.y;
|
|
if ((xdiff*xdiff + ydiff*ydiff < (SDWORD)radSquared) || //target is within our sensor range
|
|
(secondaryGetState((DROID *)psObj, DSO_HALTTYPE, &state) && //in case we got this target from a friendly unit see if can pursue it
|
|
(state != DSS_HALT_HOLD))) //make sure it's guard or pursue
|
|
{
|
|
ASSERT(!isDead(psTarget), "aiChooseTarget: Droid found a dead target!");
|
|
*ppsTarget = psTarget;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if (psObj->type == OBJ_STRUCTURE)
|
|
{
|
|
WEAPON_STATS *psWStats;
|
|
|
|
ASSERT( ((STRUCTURE *)psObj)->asWeaps[weapon_slot].nStat > 0,
|
|
"aiChooseTarget: no weapons on structure" );
|
|
|
|
psWStats = ((STRUCTURE *)psObj)->asWeaps[weapon_slot].nStat + asWeaponStats;
|
|
|
|
// see if there is a target from the command droids
|
|
psTarget = NULL;
|
|
psCommander = cmdDroidGetDesignator(psObj->player);
|
|
bCommanderBlock = false;
|
|
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;
|
|
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
|
|
psTarget = psCommander->psActionTarget[0];
|
|
}
|
|
else
|
|
{
|
|
// target out of range - release the commander block
|
|
bCommanderBlock = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// indirect fire structures use sensor towers first
|
|
tarDist = SDWORD_MAX;
|
|
minDist = psWStats->minRange * psWStats->minRange;
|
|
bCBTower = false;
|
|
if (psTarget == NULL &&
|
|
!bCommanderBlock &&
|
|
!proj_Direct(psWStats))
|
|
{
|
|
for(psCStruct=apsStructLists[psObj->player]; psCStruct; psCStruct=psCStruct->psNext)
|
|
{
|
|
// skip incomplete structures
|
|
if (psCStruct->status != SS_BUILT)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (!bCBTower
|
|
&& structStandardSensor(psCStruct)
|
|
&& psCStruct->psTarget[0] != NULL
|
|
&& !psCStruct->psTarget[0]->died)
|
|
{
|
|
/*check its a valid target*/
|
|
//Watermelon:Greater than 1 for now
|
|
if ( validTarget(psObj, psCStruct->psTarget[0], 0) &&
|
|
aiStructHasRange((STRUCTURE *)psObj, psCStruct->psTarget[0], weapon_slot))
|
|
{
|
|
xdiff = (SDWORD)psCStruct->psTarget[0]->pos.x - (SDWORD)psObj->pos.x;
|
|
ydiff = (SDWORD)psCStruct->psTarget[0]->pos.y - (SDWORD)psObj->pos.y;
|
|
distSq = xdiff*xdiff + ydiff*ydiff;
|
|
if ((distSq < tarDist) &&
|
|
(distSq > minDist))
|
|
{
|
|
tarDist = distSq;
|
|
psTarget = psCStruct->psTarget[0];
|
|
}
|
|
}
|
|
}
|
|
else if (structCBSensor(psCStruct)
|
|
&& psCStruct->psTarget[0] != NULL
|
|
&& !psCStruct->psTarget[0]->died)
|
|
{
|
|
/*check its a valid target*/
|
|
if ( validTarget(psObj, psCStruct->psTarget[0], 0) &&
|
|
aiStructHasRange((STRUCTURE *)psObj, psCStruct->psTarget[0], weapon_slot))
|
|
{
|
|
xdiff = (SDWORD)psCStruct->psTarget[0]->pos.x - (SDWORD)psObj->pos.x;
|
|
ydiff = (SDWORD)psCStruct->psTarget[0]->pos.y - (SDWORD)psObj->pos.y;
|
|
distSq = xdiff*xdiff + ydiff*ydiff;
|
|
if ((!bCBTower || (distSq < tarDist)) &&
|
|
(distSq > minDist))
|
|
{
|
|
tarDist = distSq;
|
|
psTarget = psCStruct->psTarget[0];
|
|
bCBTower = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ((psTarget == NULL) &&
|
|
!bCommanderBlock)
|
|
{
|
|
BASE_OBJECT *psCurr;
|
|
|
|
gridStartIterate((SDWORD)psObj->pos.x, (SDWORD)psObj->pos.y);
|
|
psCurr = gridIterate();
|
|
while (psCurr != NULL)
|
|
{
|
|
//don't target features
|
|
if (psCurr->type != OBJ_FEATURE && !psCurr->died)
|
|
{
|
|
if (psObj->player != psCurr->player &&
|
|
!aiCheckAlliances(psCurr->player,psObj->player))
|
|
{
|
|
/*check its a valid target*/
|
|
//Watermelon:Greater than 1 for now
|
|
if ( validTarget(psObj, psCurr, weapon_slot) &&
|
|
!aiObjIsWall(psCurr))
|
|
{
|
|
// See if in sensor range and visible
|
|
xdiff = psCurr->pos.x - psObj->pos.x;
|
|
ydiff = psCurr->pos.y - psObj->pos.y;
|
|
distSq = xdiff*xdiff + ydiff*ydiff;
|
|
if (distSq < (SDWORD)radSquared &&
|
|
psCurr->visible[psObj->player] &&
|
|
distSq < tarDist)
|
|
{
|
|
psTarget = psCurr;
|
|
tarDist = distSq;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
psCurr = gridIterate();
|
|
}
|
|
}
|
|
|
|
if (psTarget)
|
|
{
|
|
ASSERT(!psTarget->died, "aiChooseTarget: Structure found a dead target!");
|
|
*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)
|
|
{
|
|
SDWORD sensorRange = objSensorRange(psObj);
|
|
UDWORD radSquared;
|
|
BASE_OBJECT *psCurr,*psTemp = NULL;
|
|
BASE_OBJECT *psTarget = NULL;
|
|
SDWORD xdiff,ydiff, distSq, tarDist;
|
|
|
|
/* Get the sensor range */
|
|
switch (psObj->type)
|
|
{
|
|
case OBJ_DROID:
|
|
if (asSensorStats[((DROID *)psObj)->asBits[COMP_SENSOR].nStat].
|
|
location != LOC_TURRET)
|
|
{
|
|
// to be used for Turret Sensors only
|
|
return false;
|
|
}
|
|
radSquared = sensorRange * sensorRange;
|
|
break;
|
|
case OBJ_STRUCTURE:
|
|
if (!(structStandardSensor((STRUCTURE *)psObj) ||
|
|
structVTOLSensor((STRUCTURE *)psObj)))
|
|
{
|
|
// to be used for Standard and VTOL intercept Turret Sensors only
|
|
return false;
|
|
}
|
|
radSquared = sensorRange * sensorRange;
|
|
break;
|
|
default:
|
|
radSquared = 0;
|
|
break;
|
|
}
|
|
|
|
/* See if there is a something in range */
|
|
if (psObj->type == OBJ_DROID && CAN_UPDATE_NAYBORS( (DROID *)psObj ))
|
|
{
|
|
if (aiBestNearestTarget((DROID *)psObj, &psTarget, 0) >= 0)
|
|
{
|
|
/* See if in sensor range */
|
|
xdiff = psTarget->pos.x - psObj->pos.x;
|
|
ydiff = psTarget->pos.y - psObj->pos.y;
|
|
if (xdiff*xdiff + ydiff*ydiff < (SDWORD)radSquared)
|
|
{
|
|
*ppsTarget = psTarget;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
tarDist = SDWORD_MAX;
|
|
gridStartIterate((SDWORD)psObj->pos.x, (SDWORD)psObj->pos.y);
|
|
psCurr = gridIterate();
|
|
while (psCurr != NULL)
|
|
{
|
|
//don't target features
|
|
if (psCurr->type != OBJ_FEATURE && !psCurr->died)
|
|
{
|
|
if (psObj->player != psCurr->player &&
|
|
!aiCheckAlliances(psCurr->player,psObj->player) &&
|
|
!aiObjIsWall(psCurr))
|
|
{
|
|
// See if in sensor range and visible
|
|
xdiff = psCurr->pos.x - psObj->pos.x;
|
|
ydiff = psCurr->pos.y - psObj->pos.y;
|
|
distSq = xdiff*xdiff + ydiff*ydiff;
|
|
if (distSq < (SDWORD)radSquared &&
|
|
psCurr->visible[psObj->player] &&
|
|
distSq < tarDist)
|
|
{
|
|
psTemp = psCurr;
|
|
tarDist = distSq;
|
|
}
|
|
}
|
|
}
|
|
psCurr = gridIterate();
|
|
}
|
|
|
|
if (psTemp)
|
|
{
|
|
ASSERT(!psTemp->died, "aiChooseSensorTarget gave us a dead target");
|
|
*ppsTarget = psTemp;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/* Do the AI for a droid */
|
|
void aiUpdateDroid(DROID *psDroid)
|
|
{
|
|
BASE_OBJECT *psTarget;
|
|
SECONDARY_STATE state;
|
|
BOOL lookForTarget,updateTarget;
|
|
|
|
ASSERT( psDroid != NULL,
|
|
"updateUnitAI: invalid Unit pointer" );
|
|
|
|
if (!myResponsibility(psDroid->player))
|
|
{
|
|
return; // we should not order this droid around
|
|
}
|
|
|
|
lookForTarget = true;
|
|
updateTarget = true;
|
|
|
|
// don't look for a target if sulking
|
|
if (psDroid->action == DACTION_SULK)
|
|
{
|
|
lookForTarget = false;
|
|
updateTarget = false;
|
|
}
|
|
// don't look for a target if doing something else
|
|
if (!orderState(psDroid, DORDER_NONE) &&
|
|
!orderState(psDroid, DORDER_GUARD))
|
|
{
|
|
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 = 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->psTarget)
|
|
{
|
|
updateTarget = false;
|
|
}
|
|
|
|
// don't look for a target if there are any queued orders
|
|
if (psDroid->listSize > 0)
|
|
{
|
|
lookForTarget = false;
|
|
updateTarget = false;
|
|
}
|
|
|
|
// horrible check to stop droids looking for a target if
|
|
// they would switch to the guard order in the order update loop
|
|
if ((psDroid->order == DORDER_NONE) &&
|
|
(psDroid->player == selectedPlayer) &&
|
|
!isVtolDroid(psDroid) &&
|
|
secondaryGetState(psDroid, DSO_HALTTYPE, &state) &&
|
|
(state == DSS_HALT_GUARD))
|
|
{
|
|
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 choose another target if doing anything while guarding
|
|
if (orderState(psDroid, DORDER_GUARD) &&
|
|
(psDroid->action != DACTION_NONE))
|
|
{
|
|
lookForTarget = false;
|
|
}
|
|
|
|
// do not look for a target if droid is currently under direct control.
|
|
if(driveModeActive() && (psDroid == driveGetDriven())) {
|
|
lookForTarget = false;
|
|
updateTarget = false;
|
|
}
|
|
|
|
// only computer sensor droids in the single player game aquire targets
|
|
if ((psDroid->droidType == DROID_SENSOR && psDroid->player == selectedPlayer)
|
|
&& !bMultiPlayer)
|
|
{
|
|
lookForTarget = false;
|
|
updateTarget = false;
|
|
}
|
|
|
|
// do not attack if the attack level is wrong
|
|
if (secondaryGetState(psDroid, DSO_ATTACK_LEVEL, &state))
|
|
{
|
|
if (state != DSS_ALEV_ALWAYS)
|
|
{
|
|
lookForTarget = false;
|
|
}
|
|
}
|
|
|
|
/* Don't rebuild 'Naybor' list too often */
|
|
if(!CAN_UPDATE_NAYBORS(psDroid))
|
|
{
|
|
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 % TARGET_UPD_SKIP_FRAMES) ==
|
|
(frameGetFrameNumber() % TARGET_UPD_SKIP_FRAMES))
|
|
{
|
|
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)
|
|
{
|
|
//Watermelon:only 1 target for sensor droid
|
|
if ( aiChooseTarget((BASE_OBJECT *)psDroid, &psTarget, 0, true) )
|
|
{
|
|
orderDroidObj(psDroid, DORDER_OBSERVE, psTarget);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (aiChooseTarget((BASE_OBJECT *)psDroid, &psTarget, 0, true))
|
|
{
|
|
orderDroidObj(psDroid, DORDER_ATTACKTARGET, psTarget);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*set of rules which determine whether the weapon associated with the object
|
|
can fire on the propulsion type of the target*/
|
|
//Watermelon:added weapon_slot
|
|
BOOL validTarget(BASE_OBJECT *psObject, BASE_OBJECT *psTarget, int weapon_slot)
|
|
{
|
|
BOOL bTargetInAir, bValidTarget = false;
|
|
UBYTE surfaceToAir;
|
|
|
|
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].nStat].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:
|
|
// Can't attack without a weapon
|
|
//Watermelon:re-enabled if (((DROID *)psObject)->numWeaps != 0) to prevent crash
|
|
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;
|
|
}
|
|
/*
|
|
if (((DROID *)psObject)->asWeaps[0].nStat != 0 && ((DROID *)psObject)->numWeaps > 0)
|
|
{
|
|
surfaceToAir = asWeaponStats[((DROID *)psObject)->asWeaps[0].nStat].surfaceToAir;
|
|
}
|
|
else
|
|
{
|
|
surfaceToAir = 0;
|
|
}
|
|
*/
|
|
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;
|
|
}
|
|
|
|
/* Make droid/structure look for a better target */
|
|
BOOL updateAttackTarget(BASE_OBJECT * psAttacker, SDWORD weapon_slot)
|
|
{
|
|
BASE_OBJECT *psBetterTarget=NULL;
|
|
|
|
if(aiChooseTarget(psAttacker, &psBetterTarget, weapon_slot, true)) //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
|
|
{
|
|
orderDroidObj((DROID *)psAttacker, DORDER_ATTACKTARGET, 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);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|