Files
source/Strategic/ASD.cpp
T
00fb898600 Delete builddefines.h
Nothing was left in it but the include of profiler.h, and 132 translation units
were including it for that alone. Fifteen files were leaning on profiler.h to
drag in <set>, <vector> and <ostream> for them; those now include what they use.

This is the commit that moves line numbers. Removing an include line shifts
__LINE__ by one for everything below it, and __LINE__ is an immediate operand in
every Assert() and DebugMsg() call, so the four game executables differ from
their predecessors by roughly a thousand 32-bit constants each. Every one of
those is accounted for: each is a single immediate that moved by -1 where the
builddefines.h include went away, or +1 where a <set>/<vector> include was
added. Nothing else in .text, .rdata or .data moves, no object file's section
sizes change, and symbolize_crash and Ja2Export stay bit-identical.

The one non-immediate difference is that the 24 Editor translation units of the
non-editor apps stop emitting __Avx2WmemEnabledWeakValue, a 4-byte weak COMDAT
they only ever instantiated through profiler.h's <vector>. It is a UCRT weak
default that other translation units still provide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 01:07:26 -03:00

1542 lines
47 KiB
C++

/**
* @file
* @author Flugente (bears-pit.com)
*/
#include <set>
#include <math.h>
#include "ASD.h"
#include "strategic.h"
#include "Text.h"
#include "random.h"
#include "Interface.h"
#include "Overhead.h"
#include <Font Control.h>
#include "Game Clock.h"
#include "SaveLoadGame.h"
#include "message.h"
#include "Game Event Hook.h"
#include "Town Militia.h"
#include "Campaign.h"
#include "Strategic AI.h"
#include "Strategic Mines.h"
#include "Reinforcement.h"
#include "Player Command.h"
#include "Queen Command.h"
#include "Map Screen Helicopter.h"
#include "Points.h"
#include "Dialogue Control.h"
#include "Sound Control.h"
#include "renderworld.h"
#include "Isometric Utils.h"
#include "Rebel Command.h"
extern BOOLEAN gfTacticalDoHeliRun;
// Arulco special division decision code
// ASD savegame variables
// resources
INT32 gASDResource[ASD_RESOURCE_MAX];
// remember which resources we've already bought but haven't received yet
INT32 gASDResource_Ordered[ASD_RESOURCE_MAX];
UINT32 gASD_Flags = 0;
// enemy heli constants
#define ENEMYHELI_DESTROYED 0x00000001
#define ENEMYHELI_ORDER_GOTODESTINATION 0x00000002
#define ENEMYHELI_KNOWNBYINTEL 0x00000004 // the player knows about this heli from intel (it will be displayed on the map)
#define ENEMYHELI_KNOWNTOPLAYER 0x00000008 // the player knows about this heli (it will be displayed on the map)
#define ENEMYHELI_ORDER_DROPTROOPS 0x00000010
#define ENEMYHELI_ORDER_PICKUPTROOPS 0x00000020
#define ENEMYHELI_REPAIRING 0x00000040 // we are currently repairing
#define ENEMYHELI_REFUELING 0x00000080
#define ENEMYHELI_SHOTDOWN 0x00000100 // heli was destroyed by a SAM (instead of being bombed on the ground etc.)
#define ENEMYHELI_ORDERS (ENEMYHELI_ORDER_GOTODESTINATION|ENEMYHELI_ORDER_DROPTROOPS|ENEMYHELI_ORDER_PICKUPTROOPS|ENEMYHELI_REPAIRING|ENEMYHELI_REFUELING)
#define ENEMYHELI_MAX 2
// for now, this stays fixed
UINT32 gEnemyHeliTravelTimePerSector = 10;
UINT8 gEnemyHeliMaxTroops = 6;
std::vector<ENEMY_HELI> gEnemyHeliVector;
void InitASD( )
{
gASDResource[ASD_MONEY] = 200000;
gASDResource[ASD_FUEL] = 0;
gASDResource[ASD_HELI] = 0;
gASDResource[ASD_JEEP] = 2;
gASDResource[ASD_TANK] = 2;
gASDResource[ASD_ROBOT] = 8;
gASDResource_Ordered[ASD_MONEY] = 0;
gASDResource_Ordered[ASD_FUEL] = 0;
gASDResource_Ordered[ASD_HELI] = 0;
gASDResource_Ordered[ASD_JEEP] = 0;
gASDResource_Ordered[ASD_TANK] = 0;
gASDResource_Ordered[ASD_ROBOT] = 0;
gASD_Flags = 0;
EnemyHeliInit( );
}
BOOLEAN SaveASDData( HWFILE hwFile )
{
UINT32 uiNumBytesWritten = 0;
// ASD data
for ( int i = ASD_MONEY; i < ASD_RESOURCE_MAX; ++i )
{
if ( !FileWrite( hwFile, &gASDResource[i], sizeof(INT32), &uiNumBytesWritten ) ) return(FALSE);
}
for ( int i = ASD_MONEY; i < ASD_RESOURCE_MAX; ++i )
{
if ( !FileWrite( hwFile, &gASDResource_Ordered[i], sizeof(INT32), &uiNumBytesWritten ) ) return(FALSE);
}
if ( !FileWrite( hwFile, &gASD_Flags, sizeof(UINT32), &uiNumBytesWritten ) ) return(FALSE);
// in order to save a vector, we first save its size and then its content
UINT32 size = gEnemyHeliVector.size( );
if ( !FileWrite( hwFile, &size, sizeof(UINT32), &uiNumBytesWritten ) )
return(FALSE);
for ( UINT32 i = 0; i < size; ++i )
{
if ( !FileWrite( hwFile, &gEnemyHeliVector[i], sizeof(ENEMY_HELI), &uiNumBytesWritten ) )
return(FALSE);
}
return TRUE;
}
BOOLEAN LoadASDData( HWFILE hwFile )
{
UINT32 numBytesRead = 0;
// ASD data
for ( int i = ASD_MONEY; i < ASD_RESOURCE_MAX; ++i )
{
numBytesRead = ReadFieldByField( hwFile, &gASDResource[i], sizeof(gASDResource[i]), sizeof(INT32), numBytesRead );
}
for ( int i = ASD_MONEY; i < ASD_RESOURCE_MAX; ++i )
{
numBytesRead = ReadFieldByField( hwFile, &gASDResource_Ordered[i], sizeof(gASDResource_Ordered[i]), sizeof(INT32), numBytesRead );
}
numBytesRead = ReadFieldByField( hwFile, &gASD_Flags, sizeof(gASD_Flags), sizeof(UINT32), numBytesRead );
// clear any artefacts prior to loading
EnemyHeliInit( );
UINT32 size = 0;
numBytesRead = ReadFieldByField( hwFile, &size, sizeof(size), sizeof(UINT32), numBytesRead );
for ( UINT32 i = 0; i < size; ++i )
{
ENEMY_HELI data;
numBytesRead = ReadFieldByField( hwFile, &data, sizeof(data), sizeof(ENEMY_HELI), 0 );
gEnemyHeliVector.push_back( data );
}
return TRUE;
}
// when called, evaluate the current situation, and perform actions to harm the player if necessary
void UpdateASD( )
{
// if feature is not active, nothing will happen
if ( !gGameExternalOptions.fASDActive )
return;
if ( DidGameJustStart( ) )
return;
// enemy helis are unlocked either by a cutscene OR if a minimum progress threshold has been reached
if ( !(gASD_Flags & ASDFACT_HELI_UNLOCKED) && HighestPlayerProgressPercentage( ) >= gGameExternalOptions.usEnemyHeliMinimumProgress )
SetASDFlag( ASDFACT_HELI_UNLOCKED );
if ( !(gASD_Flags & ASDFACT_JEEP_UNLOCKED) && HighestPlayerProgressPercentage( ) >= gGameExternalOptions.usJeepMinimumProgress )
SetASDFlag( ASDFACT_JEEP_UNLOCKED );
if ( !(gASD_Flags & ASDFACT_TANK_UNLOCKED) && HighestPlayerProgressPercentage( ) >= gGameExternalOptions.usTankMinimumProgress )
SetASDFlag( ASDFACT_TANK_UNLOCKED );
if ( !(gASD_Flags & ASDFACT_ROBOT_UNLOCKED) && HighestPlayerProgressPercentage( ) >= gGameExternalOptions.usRobotMinimumProgress )
SetASDFlag( ASDFACT_ROBOT_UNLOCKED );
// determine whether we need to buy new toys
ASDDecideOnPurchases();
// decide how to use helicopters
ASDDecideHeliOperations();
}
void ASDDecideOnPurchases()
{
INT32 needed_fuel = 0;
INT32 needed_heli = 0;
INT32 needed_tank = 0;
INT32 needed_jeep = 0;
INT32 needed_robot = 0;
UINT16 highestplayerprogress = HighestPlayerProgressPercentage( );
// do we need new helis?
if ( gASD_Flags & ASDFACT_HELI_UNLOCKED && gGameExternalOptions.fEnemyHeliActive )
{
// determine number of SAMs under AI control
UINT16 numenemysams = 0;
for ( UINT16 x = 0; x < MAX_NUMBER_OF_SAMS; ++x )
{
StrategicMapElement *pSAMStrategicMap = &(StrategicMap[SECTOR_INFO_TO_STRATEGIC_INDEX( pSamList[x] )]);
if ( pSamList[x] && pSAMStrategicMap && pSAMStrategicMap->fEnemyControlled )
++numenemysams;
}
// only acquire/replace helis if there is still airspace the AI controls
if ( numenemysams )
{
UINT32 activehelis = 0;
for ( std::vector<ENEMY_HELI>::iterator it = gEnemyHeliVector.begin( ); it != gEnemyHeliVector.end( ); ++it )
{
if ( !((*it).flagmask & ENEMYHELI_DESTROYED) )
++activehelis;
}
needed_heli = ENEMYHELI_MAX - activehelis;
// for any heli, we need fuel
needed_fuel += (activehelis + needed_heli) * gGameExternalOptions.gEnemyHeliMaxFuel;
}
}
// do we need new jeeps?
if ( gASD_Flags & ASDFACT_JEEP_UNLOCKED )
{
// how many would we like to have? Note that this refers to the ones in our pool, the vehicles awarded to enemy groups are no longer accounted for
needed_jeep = min( 10, 2 + highestplayerprogress / 10 );
// if we already unlocked tanks, we no longer want jeeps - they are replaced by tanks
if ( (gASD_Flags & ASDFACT_TANK_UNLOCKED) && gGameExternalOptions.fASDAssignsTanks )
{
needed_jeep = 0;
}
// how many are needed?
needed_jeep = max( 0, needed_jeep - gASDResource[ASD_JEEP] );
needed_fuel += needed_jeep * ASDResourceCostFuel( ASD_JEEP );
}
// do we need new tanks?
if ( gASD_Flags & ASDFACT_TANK_UNLOCKED )
{
// how many would we like to have? Note that this refers to the ones in our pool, the vehicles awarded to enemy groups are no longer accounted for
needed_tank = min( 5, highestplayerprogress / 20 );
// how many are needed?
needed_tank = max( 0, needed_tank - gASDResource[ASD_TANK] );
needed_fuel += needed_tank * ASDResourceCostFuel( ASD_TANK );
}
// do we need new robots?
if ( gASD_Flags & ASDFACT_ROBOT_UNLOCKED )
{
// how many would we like to have? Note that this refers to the ones in our pool, the vehicles awarded to enemy groups are no longer accounted for
needed_robot = min( 10, 2 + highestplayerprogress / 10 );
// how many are needed?
needed_robot = max( 0, needed_robot - gASDResource[ASD_ROBOT] );
needed_fuel += needed_robot * ASDResourceCostFuel( ASD_ROBOT );
}
// how much new fuel do we need?
needed_fuel = max( 0, needed_fuel - gASDResource[ASD_FUEL] );
// reduce order by what we'e already ordered
needed_fuel = max( 0, needed_fuel - gASDResource_Ordered[ASD_FUEL] );
needed_jeep = max( 0, needed_jeep - gASDResource_Ordered[ASD_JEEP] );
needed_tank = max( 0, needed_tank - gASDResource_Ordered[ASD_TANK] );
needed_heli = max( 0, needed_heli - gASDResource_Ordered[ASD_HELI] );
needed_robot = max( 0, needed_robot - gASDResource_Ordered[ASD_ROBOT] );
// check whether we can afford all new purchases. If we can't, lower your expectations
while ( needed_fuel * ASDResourceCostMoney( ASD_FUEL ) +
needed_jeep * ASDResourceCostMoney( ASD_JEEP ) +
needed_tank * ASDResourceCostMoney( ASD_TANK ) +
needed_heli * ASDResourceCostMoney( ASD_HELI ) +
needed_robot * ASDResourceCostMoney( ASD_ROBOT ) > gASDResource[ASD_MONEY] )
{
if ( needed_tank ) --needed_tank;
else if ( needed_heli ) --needed_heli;
else if ( needed_jeep ) --needed_jeep;
else if ( needed_robot ) --needed_robot;
else needed_fuel = max( 0, needed_fuel - 10 );
}
// purchase things
if ( needed_fuel )
{
AddStrategicAIResources( ASD_MONEY, -needed_fuel * ASDResourceCostMoney( ASD_FUEL ) );
gASDResource_Ordered[ASD_FUEL] += needed_fuel;
AddStrategicEvent( EVENT_ASD_PURCHASE_FUEL, GetWorldTotalMin( ) + ASDResourceDeliveryTime( ASD_FUEL ), needed_fuel );
}
if ( needed_jeep )
{
AddStrategicAIResources( ASD_MONEY, -needed_jeep * ASDResourceCostMoney( ASD_JEEP ) );
AddStrategicAIResources( ASD_FUEL, -needed_jeep * ASDResourceCostFuel( ASD_JEEP ) );
gASDResource_Ordered[ASD_JEEP] += needed_jeep;
AddStrategicEvent( EVENT_ASD_PURCHASE_JEEP, GetWorldTotalMin( ) + ASDResourceDeliveryTime( ASD_JEEP ), needed_jeep );
}
if ( needed_tank )
{
AddStrategicAIResources( ASD_MONEY, -needed_tank * ASDResourceCostMoney( ASD_TANK ) );
AddStrategicAIResources( ASD_FUEL, -needed_tank * ASDResourceCostFuel( ASD_TANK ) );
gASDResource_Ordered[ASD_TANK] += needed_tank;
AddStrategicEvent( EVENT_ASD_PURCHASE_TANK, GetWorldTotalMin( ) + ASDResourceDeliveryTime( ASD_TANK ), needed_tank );
}
if ( needed_heli )
{
AddStrategicAIResources( ASD_MONEY, -needed_heli * ASDResourceCostMoney( ASD_HELI ) );
gASDResource_Ordered[ASD_HELI] += needed_heli;
AddStrategicEvent( EVENT_ASD_PURCHASE_HELI, GetWorldTotalMin( ) + ASDResourceDeliveryTime( ASD_HELI ), needed_heli );
}
if ( needed_robot )
{
AddStrategicAIResources( ASD_MONEY, -needed_robot * ASDResourceCostMoney( ASD_ROBOT ) );
gASDResource_Ordered[ASD_ROBOT] += needed_robot;
AddStrategicEvent( EVENT_ASD_PURCHASE_ROBOT, GetWorldTotalMin( ) + ASDResourceDeliveryTime( ASD_ROBOT ), needed_robot );
}
}
void ASDDecideHeliOperations()
{
if ( !gGameExternalOptions.fEnemyHeliActive )
return;
INT16 id = -1;
// loop over all helis and, if they are idle, give them orders
std::vector<ENEMY_HELI>::iterator itend = gEnemyHeliVector.end( );
for ( std::vector<ENEMY_HELI>::iterator it = gEnemyHeliVector.begin( ); it != itend; ++it )
{
++id;
if ( (*it).flagmask & ENEMYHELI_DESTROYED )
continue;
if ( (*it).flagmask & ENEMYHELI_ORDERS )
continue;
// safety: make sure heli has fuel and is repaired (player might have sabotaged us...)
UpdateEnemyHeli( id );
if ( (*it).flagmask & ENEMYHELI_ORDERS )
continue;
// if we dont have enough soldiers left to staff a helicopter, don't send one out
if ( giReinforcementPool < gEnemyHeliMaxTroops )
continue;
ENEMY_HELI& heli = (*it);
UINT32 totalrating = 0;
std::vector< std::pair<UINT8, INT16> > ratedhelioperations;
// build a list of all possible locations that require us to drop troops in
for ( UINT8 sX = 1; sX < MAP_WORLD_X - 1; ++sX )
{
for ( UINT8 sY = 1; sY < MAP_WORLD_Y - 1; ++sY )
{
// not if the enemy controls this sector
if ( StrategicMap[CALCULATE_STRATEGIC_INDEX( sX, sY )].fEnemyControlled )
continue;
UINT8 sector = SECTOR( sX, sY );
// if the player has never been here (or if the grace period is not over), this is no valid target
if ( !OkayForEnemyToMoveThroughSector( sector ) )
continue;
// if this is already the target of another helicopter, ignore it
BOOLEAN fOtherHeliAlreadyAssigned = FALSE;
std::vector<ENEMY_HELI>::iterator itend = gEnemyHeliVector.end( );
for ( std::vector<ENEMY_HELI>::iterator it = gEnemyHeliVector.begin( ); it != itend; ++it )
{
if ( (*it).flagmask & ENEMYHELI_DESTROYED )
continue;
if ( (*it).flagmask & (ENEMYHELI_ORDER_GOTODESTINATION) && (*it).sector_destination == sector )
{
fOtherHeliAlreadyAssigned = TRUE;
break;
}
}
if ( fOtherHeliAlreadyAssigned )
continue;
SECTORINFO *pSectorInfo = &(SectorInfo[sector]);
if ( !pSectorInfo )
continue;
// if we would run out of fuel while doing this, don't
if ( 2 * (abs( SECTORX( heli.sector_current ) - SECTORX( sector ) ) + abs( SECTORY( heli.sector_current ) - SECTORY( sector ) )) > heli.fuel )
continue;
// if number of SAM contacts is too high, don't consider this sector
if ( 2 * heli.SetHeliFlightPath( SECTOR( sX, sY ) ) > gGameExternalOptions.gEnemyHeliAllowedSAMContacts )
continue;
INT16 rating = 0; // how important we deem attacking this sector
// on a road, we could set up a roadblock.
if ( IsSectorRoad( sX, sY ) )
rating += 2;
// if volunteer feature is on, farm sectors are a valid target, as they increase the rate with which the player gains volunteers
if ( gGameExternalOptions.fMilitiaVolunteerPool && IsSectorFarm( sX, sY ) )
rating += 5;
// town sectors are worth more
if ( StrategicMap[CALCULATE_STRATEGIC_INDEX( sX, sY )].bNameId != BLANK_SECTOR )
rating += 10;
// if we take a mine, we cut the player's income - very juicy
if ( IsThereAMineInThisSector( sX, sY ) )
rating += 20;
// if this is a player-owned SAM, it is very important that we take this. Should the SAM be operational, we will likely have ignore this tdue to the earlier check for SAM contacts
for ( UINT16 x = 0; x < MAX_NUMBER_OF_SAMS; ++x )
{
if ( pSamList[x] == sector )
{
rating += 30;
break;
}
}
// lower a sector's rating for the amount of militia and mercs here - no sense in dropping a squad into a killing field
// for now, the AI simply knows the player's amount of troops - later on, try to rely on espionage
UINT16 num_militia = CountAllMilitiaInFiveSectors( sX, sY );
UINT16 num_mercs = NumPlayerTeamMembersInSector( sX, sY, 0);
if ( num_militia + num_mercs )
{
// mercs are much more dangerous than militia, so increase their weight
if ( num_militia + 2.5f * num_mercs > 2 * gEnemyHeliMaxTroops )
rating = 0;
else if ( num_militia + 2.5f * num_mercs > gEnemyHeliMaxTroops )
rating /= 2;
// only a very small garrison - we can wipe them out with no risk and add losses to the player. We should totally do that!
else if ( num_militia + 2.5f * num_mercs < gEnemyHeliMaxTroops )
rating *= 1.5f;
}
if ( rating > 0 )
{
totalrating += rating;
ratedhelioperations.push_back( std::make_pair( sector, rating ) );
}
}
}
// for safety, delete set flight data
heli.sector_destination = 0;
heli.sector_waypoint = 0;
heli.flagmask &= ~ENEMYHELI_ORDER_GOTODESTINATION;
// if there are viable operations, pick one at random (weighted by rating) and start that operation
if ( totalrating && !ratedhelioperations.empty( ) )
{
UINT32 pickedrating = Random( totalrating );
UINT32 ratingsofar = 0;
for ( std::vector< std::pair<UINT8, INT16> >::iterator it = ratedhelioperations.begin( ); it != ratedhelioperations.end( ); ++it )
{
if ( pickedrating <= ratingsofar + (*it).second )
{
// attack this sector!
heli.SetHeliFlightPath( (*it).first );
heli.troopcount = gEnemyHeliMaxTroops;
// sevenfm: don't change reinforcement pool when unlimited reinforcements enabled
if (!gfUnlimitedTroops)
giReinforcementPool -= gEnemyHeliMaxTroops;
heli.flagmask |= ENEMYHELI_ORDER_DROPTROOPS;
AddStrategicEvent( EVENT_ENEMY_HELI_UPDATE, GetWorldTotalMin( ) + gEnemyHeliTravelTimePerSector, id );
break;
}
ratingsofar += (*it).second;
}
}
}
}
void SetASDFlag( UINT32 aFlag )
{
gASD_Flags |= aFlag;
}
UINT32 ASDResourceDeliveryTime( UINT8 aType )
{
return gGameExternalOptions.gASDResource_BuyTime[aType];
}
UINT32 ASDResourceCostMoney( UINT8 aType )
{
return gGameExternalOptions.gASDResource_Cost[aType];
}
INT32 GetStrategicAIResourceCount( UINT8 aType )
{
if (aType < 0 || aType >= ASD_RESOURCE_MAX)
return 0;
return gASDResource[aType];
}
// add resources to the AIs resource pool
void AddStrategicAIResources( UINT8 aType, INT32 aAmount )
{
if (aType == ASD_MONEY)
{
aAmount *= RebelCommand::GetASDIncomeModifier();
}
gASDResource[aType] = max( 0, gASDResource[aType] + aAmount );
if ( aType == ASD_HELI )
AddNewHeli( );
}
void ASDReduceOrderedStrategicResources( UINT8 aType, INT32 aAmount )
{
gASDResource_Ordered[aType] = max( 0, gASDResource_Ordered[aType] - aAmount );
}
//////////////////////////////////////////// enemy helis /////////////////////////////////////////////////////////
// determine best heli flight path (minimising contact to hostile SAM sectors) and return number of possible SAM contacts
UINT8 ENEMY_HELI::SetHeliFlightPath( UINT8 aDest )
{
sector_destination = aDest;
flagmask |= ENEMYHELI_ORDER_GOTODESTINATION;
// determine best waypoint
// standard way of pathing is to minimise the max deviation of x and y
// we are allowed to set one intermediate waypoint
// For now, we try 3 methods:
// 1. no waypoint (waypoint equals destination)
// 2. waypoint is x_start, y_dest
// 3. waypoint is x_dest, y_start
// we pick the waypoint whose path has the least amount of hostile SAM sectors.
UINT8 best_samcontacts = 0;
UINT8 tmpsect = sector_current;
UINT8 tmpcontacts = 0;
// 1. no waypoint (waypoint equals destination)
best_samcontacts = NumPlayerAirSpaceOnHeliPath( sector_current, sector_destination );
sector_waypoint = sector_destination;
// 2. waypoint is x_start, y_dest
tmpsect = SECTOR( SECTORX( sector_current ), SECTORY( sector_destination ) );
tmpcontacts = NumPlayerAirSpaceOnHeliPath( sector_current, tmpsect ) + NumPlayerAirSpaceOnHeliPath( tmpsect, sector_destination );
if ( tmpcontacts < best_samcontacts )
{
best_samcontacts = tmpcontacts;
sector_waypoint = tmpsect;
}
// 3. waypoint is x_dest, y_start
tmpsect = SECTOR( SECTORX( sector_destination ), SECTORY( sector_current ) );
tmpcontacts = NumPlayerAirSpaceOnHeliPath( sector_current, tmpsect ) + NumPlayerAirSpaceOnHeliPath( tmpsect, sector_destination );
if ( tmpcontacts < best_samcontacts )
{
best_samcontacts = tmpcontacts;
sector_waypoint = tmpsect;
}
return best_samcontacts;
}
void ENEMY_HELI::Destroy( )
{
hp = 0;
flagmask |= ENEMYHELI_DESTROYED;
// if the player knows about this heli, play a sound and leave a message
if ( flagmask & (ENEMYHELI_KNOWNBYINTEL|ENEMYHELI_KNOWNTOPLAYER) )
{
CHAR16 pStr2[128];
GetSectorIDString( SECTORX( sector_current ), SECTORY( sector_current ), 0, pStr2, FALSE );
MapScreenMessage( FONT_MCOLOR_LTRED, MSG_INTERFACE, szEnemyHeliText[0], pStr2 );
// if heli was shot down by a SAM, play a crashing sound
if ( flagmask & ENEMYHELI_SHOTDOWN )
{
PlayJA2StreamingSampleFromFile( "stsounds\\blah2.wav", RATE_11025, HIGHVOLUME, 1, MIDDLEPAN, NULL );
}
}
// if a helicopter was shot down, there might be survivors or wreckage
if ( flagmask & ENEMYHELI_SHOTDOWN )
{
// remember that a heli crashed here. When entering the sector the next time, we might spawn a downed pilot or wreckage
SectorInfo[sector_current].usSectorInfoFlag |= SECTORINFO_ENEMYHELI_SHOTDOWN;
// if the heli had soldiers aboard, they might have survived the crash. Add them to the sector in that case
// don't do this in sectors that are impassable
if ( troopcount && !SectorIsImpassable( sector_current ) )
{
// it is possible that we shoot down a helicopter while in a fight. In this case, adding soldiers to a sector would either cause a second battle or a state where a fight should start but doesn't
// both is bad, so we simply don't add soldiers in that case. The player will hardly notice, as there is only a chance that soldiers survive anyway
if ( !gTacticalStatus.fEnemyInSector && !(gTacticalStatus.uiFlags & INCOMBAT) )
{
// chance that they survived at all
if ( Chance( 75 ) )
{
// not all might have made it
SectorInfo[sector_current].ubNumElites = min( 255, SectorInfo[sector_current].ubNumElites + 1 + Random(troopcount) );
// if no militia or mercs are here, enemy takes this sector
if ( NumPlayerTeamMembersInSector( SECTORX( sector_current ), SECTORY( sector_current ), 0 ) + NumNonPlayerTeamMembersInSector( SECTORX( sector_current ), SECTORY( sector_current ), MILITIA_TEAM ) <= 0 )
{
SetThisSectorAsEnemyControlled( SECTORX( sector_current ), SECTORY( sector_current ), 0, TRUE );
}
CheckCombatInSectorDueToUnusualEnemyArrival( ENEMY_TEAM, SECTORX( sector_current ), SECTORY( sector_current ), 0 );
}
}
}
}
// reduce these at the end, as we might do sth based on them
fuel = 0;
troopcount = 0;
}
void EnemyHeliInit( )
{
gEnemyHeliVector.clear( );
}
void AddNewHeli( )
{
while ( gASDResource[ASD_HELI] > 0 )
{
--gASDResource[ASD_HELI];
BOOLEAN fHeliReplaced = FALSE;
INT16 id = -1;
// if a heli was destroyed, replace it
std::vector<ENEMY_HELI>::iterator itend = gEnemyHeliVector.end( );
for ( std::vector<ENEMY_HELI>::iterator it = gEnemyHeliVector.begin( ); it != itend; ++it )
{
++id;
if ( (*it).flagmask & ENEMYHELI_DESTROYED )
{
(*it).flagmask = 0;
(*it).sector_home = SECTOR( gModSettings.usEnemyHeliBaseSectorX, gModSettings.usEnemyHeliBaseSectorY );
(*it).sector_destination = 0;
(*it).sector_waypoint = 0;
(*it).sector_current = (*it).sector_home;
(*it).troopcount = 0;
(*it).hp = gGameExternalOptions.gEnemyHeliMaxHP;
(*it).fuel = 0;
UpdateEnemyHeli( id );
fHeliReplaced = TRUE;
break;
}
}
// otherwise, create a new one
if ( !fHeliReplaced )
{
ENEMY_HELI data;
data.flagmask = 0;
data.sector_home = SECTOR( gModSettings.usEnemyHeliBaseSectorX, gModSettings.usEnemyHeliBaseSectorY );
data.sector_destination = 0;
data.sector_waypoint = 0;
data.sector_current = data.sector_home;
data.troopcount = 0;
data.hp = gGameExternalOptions.gEnemyHeliMaxHP;
data.fuel = 0;
gEnemyHeliVector.push_back( data );
++id;
UpdateEnemyHeli( id );
}
}
}
// all enemy helis carrying troops in this sector drop these into combat, and get the order to head home
void EnemyHeliTroopDrop( UINT8 aSector )
{
INT16 id = -1;
std::vector<ENEMY_HELI>::iterator itend = gEnemyHeliVector.end( );
for ( std::vector<ENEMY_HELI>::iterator it = gEnemyHeliVector.begin( ); it != itend; ++it )
{
++id;
if ( (*it).sector_current != aSector )
continue;
if ( (*it).flagmask & ENEMYHELI_DESTROYED )
continue;
if ( !(*it).troopcount )
continue;
SectorInfo[aSector].ubNumElites = min( 255, SectorInfo[aSector].ubNumElites + (*it).troopcount );
(*it).troopcount = 0;
(*it).flagmask &= ~ENEMYHELI_ORDER_DROPTROOPS;
UpdateEnemyHeli( id );
}
}
std::set<UINT8> GetEnemyHeliSectors( BOOLEAN afKnownToPlayer )
{
std::set<UINT8> set;
std::vector<ENEMY_HELI>::iterator itend = gEnemyHeliVector.end( );
for ( std::vector<ENEMY_HELI>::iterator it = gEnemyHeliVector.begin( ); it != itend; ++it )
{
if ( (*it).flagmask & ENEMYHELI_DESTROYED )
continue;
// if the player doesn't know about this heli, don't add this
if ( afKnownToPlayer && !((*it).flagmask & (ENEMYHELI_KNOWNBYINTEL|ENEMYHELI_KNOWNTOPLAYER)) )
continue;
set.insert( (*it).sector_current );
}
return set;
}
std::vector<INT16> GetEnemyHeliIDKnowledgeStatus()
{
INT16 id = -1;
std::vector<INT16> vec;
std::vector<ENEMY_HELI>::iterator itend = gEnemyHeliVector.end();
for ( std::vector<ENEMY_HELI>::iterator it = gEnemyHeliVector.begin(); it != itend; ++it )
{
++id;
if ( ( ( *it ).flagmask & ( ENEMYHELI_DESTROYED | ENEMYHELI_KNOWNBYINTEL | ENEMYHELI_KNOWNTOPLAYER ) ) )
vec.push_back( -1 );
else
vec.push_back( id );
}
return vec;
}
void BuyHeliInfoWithIntel( INT16 id )
{
if ( id >= 0 && id < gEnemyHeliVector.size() )
{
ENEMY_HELI& heli = gEnemyHeliVector[id];
// if the heli is already destroyed, nothing to do here
if ( heli.flagmask & ENEMYHELI_DESTROYED )
return;
heli.flagmask |= ENEMYHELI_KNOWNBYINTEL;
}
}
void UpdateEnemyHeliRepair( INT16 id )
{
if ( id >= 0 && id < gEnemyHeliVector.size( ) )
{
ENEMY_HELI& heli = gEnemyHeliVector[id];
// if the heli is already destroyed, nothing to do here
if ( heli.flagmask & ENEMYHELI_DESTROYED )
return;
// update
if ( heli.flagmask & ENEMYHELI_REPAIRING )
{
heli.flagmask &= ~ENEMYHELI_REPAIRING;
if ( heli.sector_current == heli.sector_home )
{
INT32 cost = (gGameExternalOptions.gEnemyHeliMaxHP - heli.hp) * gGameExternalOptions.gEnemyHeliCostPerRepair1HP;
AddStrategicAIResources( ASD_MONEY, -cost );
heli.hp = gGameExternalOptions.gEnemyHeliMaxHP;
}
}
UpdateEnemyHeli( id );
}
}
void UpdateEnemyHeliRefuel( INT16 id )
{
if ( id >= 0 && id < gEnemyHeliVector.size( ) )
{
ENEMY_HELI& heli = gEnemyHeliVector[id];
// if the heli is already destroyed, nothing to do here
if ( heli.flagmask & ENEMYHELI_DESTROYED )
return;
// update
if ( heli.flagmask & ENEMYHELI_REFUELING )
{
heli.flagmask &= ~ENEMYHELI_REFUELING;
if ( heli.sector_current == heli.sector_home )
{
UINT8 addedfuel = min( gASDResource[ASD_FUEL], gGameExternalOptions.gEnemyHeliMaxFuel - heli.fuel );
AddStrategicAIResources( ASD_FUEL, -addedfuel );
heli.fuel += addedfuel;
// if heli isn't full, try again
if ( heli.fuel < gGameExternalOptions.gEnemyHeliMaxFuel )
{
heli.flagmask |= ENEMYHELI_REFUELING;
AddStrategicEvent( EVENT_ENEMY_HELI_REFUEL, GetWorldTotalMin( ) + (gGameExternalOptions.gEnemyHeliMaxFuel - heli.fuel) * gGameExternalOptions.gEnemyHeliTimePerFuelRefuel, id );
return;
}
}
}
UpdateEnemyHeli( id );
}
}
void UpdateEnemyHeli( INT16 id )
{
if ( id >= 0 && id < gEnemyHeliVector.size( ) )
{
ENEMY_HELI& heli = gEnemyHeliVector[id];
// if the heli is already destroyed, nothing to do here
if ( heli.flagmask & ENEMYHELI_DESTROYED )
return;
if ( heli.flagmask & ENEMYHELI_ORDER_GOTODESTINATION )
{
if ( heli.sector_current == heli.sector_waypoint )
heli.sector_waypoint = heli.sector_destination;
heli.sector_current = GetNextEnemyHeliSector( heli.sector_current, heli.sector_waypoint );
if ( heli.fuel )
--heli.fuel;
else
{
// if the heli is in the air without fuel, it will crash!
heli.flagmask |= ENEMYHELI_SHOTDOWN;
heli.Destroy();
return;
}
// whenever we arrive at a new sector, SAM batteries get a chance to shoot at a heli
EnemyHeliCheckPlayerKnowledge( id );
EnemyHeliSAMCheck( id );
if ( heli.sector_current == heli.sector_destination )
{
heli.sector_waypoint = 0;
heli.flagmask &= ~ENEMYHELI_ORDER_GOTODESTINATION;
}
}
// if the heli is already destroyed, nothing to do here
if ( heli.flagmask & ENEMYHELI_DESTROYED )
return;
// each time we update a heli, check whether we need repairs or refueling
BOOLEAN needrepair = FALSE;
BOOLEAN needrefuel = FALSE;
if ( heli.hp < gGameExternalOptions.gEnemyHeliMaxHP )
{
needrepair = TRUE;
if ( !heli.hp )
{
heli.Destroy( );
return;
}
}
if ( heli.fuel < gGameExternalOptions.gEnemyHeliMaxFuel )
{
needrefuel = TRUE;
}
// if we need repair or refueling and we are at the base, do that
if ( needrepair || needrefuel )
{
if ( heli.sector_current == heli.sector_home )
{
if ( needrepair )
{
// determine how long this will take
INT32 time = (gGameExternalOptions.gEnemyHeliMaxHP - heli.hp) * gGameExternalOptions.gEnemyHeliTimePerHPRepair;
// are we able to fund this?
if ( gGameExternalOptions.gEnemyHeliMaxHP - heli.hp * gGameExternalOptions.gEnemyHeliCostPerRepair1HP <= gASDResource[ASD_MONEY] )
{
heli.flagmask |= ENEMYHELI_REPAIRING;
AddStrategicEvent( EVENT_ENEMY_HELI_REPAIR, GetWorldTotalMin( ) + time, id );
return;
}
}
if ( needrefuel )
{
// determine how long this will take
INT32 time = (gGameExternalOptions.gEnemyHeliMaxFuel - heli.fuel) * gGameExternalOptions.gEnemyHeliTimePerFuelRefuel;
// are we able to fund this?
if ( gGameExternalOptions.gEnemyHeliMaxFuel - heli.fuel <= gASDResource[ASD_FUEL] )
{
heli.flagmask |= ENEMYHELI_REFUELING;
AddStrategicEvent( EVENT_ENEMY_HELI_REFUEL, GetWorldTotalMin( ) + time, id );
return;
}
}
}
else
{
// if we are not at the base and are not ordered to drop or pick up troops, head home
if ( !(heli.flagmask & (ENEMYHELI_ORDER_GOTODESTINATION | ENEMYHELI_ORDER_DROPTROOPS | ENEMYHELI_ORDER_PICKUPTROOPS)) && heli.sector_current != heli.sector_home )
{
// we don't care about number of SAM contacts here - just get us home!
heli.SetHeliFlightPath( heli.sector_home );
}
}
}
// if we still have to move, well, do so
if ( heli.flagmask & (ENEMYHELI_ORDER_GOTODESTINATION) )
{
AddStrategicEvent( EVENT_ENEMY_HELI_UPDATE, GetWorldTotalMin( ) + gEnemyHeliTravelTimePerSector, id );
return;
}
// if we have to drop troops, do so
if ( heli.flagmask & ENEMYHELI_ORDER_DROPTROOPS && heli.sector_current == heli.sector_destination )
{
// for now, we park if there is an ongoing battle here
if ( gTacticalStatus.fEnemyInSector || (gTacticalStatus.uiFlags & INCOMBAT) )
{
AddStrategicEvent( EVENT_ENEMY_HELI_UPDATE, GetWorldTotalMin( ) + gEnemyHeliTravelTimePerSector, id );
return;
}
SectorInfo[heli.sector_current].ubNumElites = min( 255, SectorInfo[heli.sector_current].ubNumElites + heli.troopcount );
heli.troopcount = 0;
heli.flagmask &= ~ENEMYHELI_ORDER_DROPTROOPS;
// if no militia or mercs are here, enemy takes this sector
if ( NumPlayerTeamMembersInSector( SECTORX( heli.sector_current ), SECTORY( heli.sector_current ), 0 ) + NumNonPlayerTeamMembersInSector( SECTORX( heli.sector_current ), SECTORY( heli.sector_current ), MILITIA_TEAM ) <= 0 )
{
SetThisSectorAsEnemyControlled( SECTORX( heli.sector_current ), SECTORY( heli.sector_current ), 0, TRUE );
}
CheckCombatInSectorDueToUnusualEnemyArrival( ENEMY_TEAM, SECTORX( heli.sector_current ), SECTORY( heli.sector_current ), 0 );
// get new orders
return UpdateEnemyHeli( id );
}
// if we have to pick up troops, do so
if ( heli.flagmask & ENEMYHELI_ORDER_PICKUPTROOPS && heli.sector_current == heli.sector_destination )
{
// for now, not if the player is here
if ( !NumPlayerTeamMembersInSector( SECTORX( heli.sector_current ), SECTORY( heli.sector_current ), 0 ) )
{
if ( heli.troopcount < gEnemyHeliMaxTroops )
{
UINT16 possibletroops = SectorInfo[heli.sector_current].ubNumElites;
UINT8 picked_up = min( possibletroops, gEnemyHeliMaxTroops - heli.troopcount );
heli.troopcount += picked_up;
SectorInfo[heli.sector_current].ubNumElites -= picked_up;
heli.flagmask &= ~ENEMYHELI_ORDER_PICKUPTROOPS;
// get new orders
return UpdateEnemyHeli( id );
}
}
}
}
}
void EnemyHeliSAMCheck( INT16 id )
{
if ( id >= 0 && id < gEnemyHeliVector.size( ) )
{
ENEMY_HELI& heli = gEnemyHeliVector[id];
// if the heli is already destroyed, nothing to do here
if ( heli.flagmask & ENEMYHELI_DESTROYED )
return;
INT8 heli_x = SECTORX( heli.sector_current );
INT8 heli_y = SECTORY( heli.sector_current );
// is the airspace in our current sector player-controlled and operational?
if ( StrategicMap[CALCULATE_STRATEGIC_INDEX( heli_x, heli_y )].usAirType & AIRSPACE_PLAYER_ACTIVE )
{
for ( int i = 0; i < NUMBER_OF_SAMS; ++i )
{
BOOLEAN samworking = FALSE;
if ( DoesSamCoverSector( i, heli.sector_current, &samworking ) && samworking )
{
StrategicMapElement *pSAMStrategicMap = &(StrategicMap[SECTOR_INFO_TO_STRATEGIC_INDEX( pSamList[i] )]);
// also check whether this sector is not enemy-controlled - we can't have the AI shooting at its own helis
if ( pSAMStrategicMap && !pSAMStrategicMap->fEnemyControlled )
{
SoldierID ubBestSAMOperatorID = NOBODY;
FLOAT samcth = GetBestSAMOperatorCTH_Player( gpSamSectorX[i], gpSamSectorY[i], 0, &ubBestSAMOperatorID );
// cth is reduced if SAM is damaged, even if it can still operate
samcth = (samcth * pSAMStrategicMap->bSAMCondition) / 100.0f;
// determine distance from SAM to heli
FLOAT distance = sqrt( FLOAT( abs( heli_x - gpSamSectorX[i] ) * abs( heli_x - gpSamSectorX[i] ) + abs( heli_y - gpSamSectorY[i] ) * abs( heli_y - gpSamSectorY[i] ) ) );
// distance penalty
samcth = (samcth * (100.0f - 8 * distance)) / 100.0f;
// if cth is too low, we don't fire
if ( samcth > 0.01 )
{
CHAR16 pStrSectorName[128];
GetSectorIDString( heli_x, heli_y, 0, pStrSectorName, FALSE );
CHAR16 pStrSectorName_SAM[128];
GetSectorIDString( gpSamSectorX[i], gpSamSectorY[i], 0, pStrSectorName_SAM, FALSE );
MapScreenMessage( FONT_MCOLOR_LTRED, MSG_INTERFACE, szEnemyHeliText[8], pStrSectorName_SAM, pStrSectorName );
// if we hit, damage the heli
BOOLEAN fHit = FALSE;
BOOLEAN fDestroyed = FALSE;
if ( Chance( (UINT32)(samcth) ) )
{
fHit = TRUE;
UINT8 damage = gGameExternalOptions.gEnemyHeliSAMDamage_Base + Random( gGameExternalOptions.gEnemyHeliSAMDamage_Var );
MapScreenMessage( FONT_MCOLOR_LTRED, MSG_INTERFACE, szEnemyHeliText[6], pStrSectorName );
heli.hp = max( 0, heli.hp - damage );
if ( !heli.hp )
{
fDestroyed = TRUE;
heli.flagmask |= ENEMYHELI_SHOTDOWN;
heli.Destroy( );
}
}
// depending on our result, award experience to the operator
if ( ubBestSAMOperatorID != NOBODY )
{
if ( fHit )
StatChange( ubBestSAMOperatorID, EXPERAMT, fDestroyed ? 30 : 10, TRUE );
StatChange( ubBestSAMOperatorID, MECHANAMT, 5, TRUE );
}
}
}
}
}
}
EnemyHeliMANPADSCheck( id );
}
}
void EnemyHeliMANPADSCheck( INT16 id )
{
if ( id >= 0 && id < gEnemyHeliVector.size( ) )
{
ENEMY_HELI& heli = gEnemyHeliVector[id];
// if the heli is already destroyed, nothing to do here
if ( heli.flagmask & ENEMYHELI_DESTROYED )
return;
// if the player doesn't know about this heli, he can't shoot it down
if ( !(heli.flagmask & ( ENEMYHELI_KNOWNBYINTEL|ENEMYHELI_KNOWNTOPLAYER)) )
return;
INT8 heli_x = SECTORX( heli.sector_current );
INT8 heli_y = SECTORY( heli.sector_current );
CHAR16 pStrSectorName[128];
GetSectorIDString( heli_x, heli_y, 0, pStrSectorName, FALSE );
// each merc can fire one missile at the heli (break if heli is shot down)
// this requires an armed SAM in the first place. The mercs have to be in the heli's sector or adjacent to it
// sleeping or travelling mercs are excluded
UINT16 uiCnt = 0;
SOLDIERTYPE* pSoldier = NULL;
for ( uiCnt = 0, pSoldier = MercPtrs[uiCnt]; uiCnt <= gTacticalStatus.Team[gbPlayerNum].bLastID; ++uiCnt, ++pSoldier )
{
if ( pSoldier && pSoldier->bActive && pSoldier->stats.bLife >= OKLIFE && !pSoldier->bSectorZ &&
(pSoldier->sSectorX == heli_x && pSoldier->sSectorY == heli_y) ||
(pSoldier->sSectorX == heli_x + 1 && pSoldier->sSectorY == heli_y) ||
(pSoldier->sSectorX == heli_x - 1 && pSoldier->sSectorY == heli_y) ||
(pSoldier->sSectorX == heli_x && pSoldier->sSectorY == heli_y + 1) ||
(pSoldier->sSectorX == heli_x && pSoldier->sSectorY == heli_y - 1)
)
{
// look for a SAM in our inventory
OBJECTTYPE* pObj = pSoldier->GetObjectWithFlag( MANPAD );
if ( pObj )
{
// abort if this is a launcher without ammo
if (ItemIsRocketLauncher(pObj->usItem) && !ItemIsSingleShotRocketLauncher(pObj->usItem))
{
OBJECTTYPE* pAttachment = FindAttachmentByClass( pObj, IC_GRENADE );
if ( !pAttachment->exists( ) )
pAttachment = FindAttachmentByClass( pObj, IC_BOMB );
if ( !pAttachment->exists( ) )
continue;
}
// determine cth
INT16 samcth = pSoldier->stats.bMarksmanship;
samcth = (FLOAT)((samcth * (100.0f + pSoldier->GetTraitCTHModifier( pObj->usItem, 2, NO_PROFILE ))) / 100.0f);
samcth = (samcth * (100.0f + pSoldier->GetBackgroundValue( BG_PERC_SAM_CTH ))) / 100.0f;
// cth is reduced if SAM is damaged, even if it can still operate
samcth = (samcth * (*pObj)[0]->data.objectStatus) / 100.0f;
// determine distance from SAM to heli
FLOAT distance = sqrt(FLOAT(abs( heli_x - pSoldier->sSectorX ) * abs( heli_x - pSoldier->sSectorX ) + abs( heli_y - pSoldier->sSectorY ) * abs( heli_y - pSoldier->sSectorY ) ));
// distance penalty
samcth = (samcth * (100.0f - 20 * distance)) / 100.0f;
MapScreenMessage( FONT_MCOLOR_LTRED, MSG_INTERFACE, szEnemyHeliText[7], pSoldier->GetName( ), Item[pObj->usItem].szItemName, pStrSectorName );
// 'fire' (remove shot)
if (ItemIsSingleShotRocketLauncher(pObj->usItem))
{
CreateItem( Item[pObj->usItem].discardedlauncheritem, (*pObj)[0]->data.objectStatus, pObj );
DirtyMercPanelInterface( pSoldier, DIRTYLEVEL2 );
}
else
{
DeductAmmo( pSoldier, pObj );
}
// if we hit, damage the heli
BOOLEAN fHit = FALSE;
BOOLEAN fDestroyed = FALSE;
if ( Chance( (UINT32)(samcth) ) )
{
fHit = TRUE;
UINT8 damage = gGameExternalOptions.gEnemyHeliMANPADSDamage_Base + Random( gGameExternalOptions.gEnemyHeliMANPADSDamage_Var );
MapScreenMessage( FONT_MCOLOR_LTRED, MSG_INTERFACE, szEnemyHeliText[6], pStrSectorName );
heli.hp = max( 0, heli.hp - damage );
if ( !heli.hp )
{
fDestroyed = TRUE;
heli.flagmask |= ENEMYHELI_SHOTDOWN;
heli.Destroy( );
}
}
// depending on our result, award experience to the operator
if ( fHit )
StatChange( pSoldier, EXPERAMT, fDestroyed ? 30 : 10, TRUE );
StatChange( pSoldier, MARKAMT, 5, TRUE );
}
}
}
}
}
void EnemyHeliCheckPlayerKnowledge( INT16 id )
{
if ( id >= 0 && id < gEnemyHeliVector.size( ) )
{
ENEMY_HELI& heli = gEnemyHeliVector[id];
// if we already know about this from intel, nothing to do here
if ( heli.flagmask & ENEMYHELI_KNOWNBYINTEL )
return;
INT8 current_x = SECTORX( heli.sector_current );
INT8 current_y = SECTORY( heli.sector_current );
// if there are mercs in this sector, they spot the heli and let us know
if ( NumPlayerTeamMembersInSector( current_x, current_y, 0 ) )
{
// if we previously did not know of the heli, our mercs will alert us
if ( !(heli.flagmask & ENEMYHELI_KNOWNTOPLAYER) )
{
SayQuoteFromAnyBodyInThisSector( current_x, current_y, 0, QUOTE_AIR_RAID );
}
heli.flagmask |= ENEMYHELI_KNOWNTOPLAYER;
return;
}
// scouts our militia might be able to detect a heli from nearby sectors
if ( CanSomeoneNearbyScoutThisSector( current_x, current_y, TRUE ) ) // scouting trait check
{
if ( gSkillTraitValues.fSCCanDetectEnemyPresenseAround )
{
heli.flagmask |= ENEMYHELI_KNOWNTOPLAYER;
return;
}
}
// is the airspace in our current sector player-controlled and operational?
if ( StrategicMap[CALCULATE_STRATEGIC_INDEX( current_x, current_y )].usAirType & AIRSPACE_PLAYER_ACTIVE )
{
for ( int i = 0; i < NUMBER_OF_SAMS; ++i )
{
if ( DoesSamCoverSector( i, heli.sector_current ) )
{
// if mercs man the SAM, they will let us know of the new SAM
if ( NumPlayerTeamMembersInSector( gpSamSectorX[i], gpSamSectorY[i], 0 ) )
{
// if we previously did not know of the heli, our mercs will alert us
if ( !(heli.flagmask & ENEMYHELI_KNOWNTOPLAYER) )
{
SayQuoteFromAnyBodyInThisSector( gpSamSectorX[i], gpSamSectorY[i], 0, QUOTE_AIR_RAID );
break;
}
}
}
}
heli.flagmask |= ENEMYHELI_KNOWNTOPLAYER;
return;
}
// if we haven't exited this function already, nobody notices the heli - delete knowledge
heli.flagmask &= ~ENEMYHELI_KNOWNTOPLAYER;
}
}
UINT8 NumPlayerAirSpaceOnHeliPath( UINT8 aStart, UINT8 aEnd )
{
UINT8 samcontacts = 0;
UINT8 tmpsect = aStart;
while ( tmpsect != aEnd )
{
tmpsect = GetNextEnemyHeliSector( tmpsect, aEnd );
if ( StrategicMap[SECTOR_INFO_TO_STRATEGIC_INDEX( tmpsect )].usAirType & AIRSPACE_PLAYER_ACTIVE )
++samcontacts;
}
return samcontacts;
}
// get the next sector in an enemy heli flight path.
UINT8 GetNextEnemyHeliSector( UINT8 aStart, UINT8 aDest )
{
INT8 start_x = SECTORX( aStart );
INT8 start_y = SECTORY( aStart );
INT8 dest_x = SECTORX( aDest );
INT8 dest_y = SECTORY( aDest );
if ( abs( start_x - dest_x ) > abs( start_y - dest_y ) )
{
if ( start_x > dest_x )
return SECTOR( start_x - 1, start_y );
else
return SECTOR( start_x + 1, start_y );
}
else
{
if ( start_y > dest_y )
return SECTOR( start_x, start_y - 1 );
else
return SECTOR( start_x, start_y + 1 );
}
return aStart;
}
void HandleEnemyHelicopterOnGroundGraphic( )
{
// no worries if underground
if ( gbWorldSectorZ )
return;
UINT8 currentsector = SECTOR( gWorldSectorX, gWorldSectorY );
// only relevant if this is the enemies' heli hub
if ( currentsector != SECTOR( gModSettings.usEnemyHeliBaseSectorX, gModSettings.usEnemyHeliBaseSectorY ) )
return;
INT16 id = -1;
std::vector<ENEMY_HELI>::iterator itend = gEnemyHeliVector.end( );
for ( std::vector<ENEMY_HELI>::iterator it = gEnemyHeliVector.begin( ); it != itend; ++it )
{
++id;
// we can only provide graphics for ENEMYHELI_MAX helis
if ( id >= ENEMYHELI_MAX )
break;
AddEnemyHelicopterToMaps( ((*it).sector_current == currentsector), ((*it).flagmask & ENEMYHELI_DESTROYED), gModSettings.sEnemyHeliBaseParkGridno[id], gModSettings.sEnemyHeliBaseParkTileIndex );
}
// Invalidate rendering (if we have any helis at all)
if ( id )
InvalidateWorldRedundency( );
}
void UpdateAndDamageEnemyHeliIfFound( INT16 sSectorX, INT16 sSectorY, INT16 sSectorZ, INT32 sGridNo, UINT8 ubDamage, BOOLEAN aDestroyed )
{
// no worries if underground
if ( gbWorldSectorZ )
return;
UINT8 currentsector = SECTOR( gWorldSectorX, gWorldSectorY );
// only relevant if this is the enemies' heli hub
if ( currentsector != SECTOR( gModSettings.usEnemyHeliBaseSectorX, gModSettings.usEnemyHeliBaseSectorY ) )
return;
// helicopter tiles are multi-tiled. Search for the base gridno
STRUCTURE *pCurrent, *pBaseStructure;
INT32 sBaseGridNo = NOWHERE;
pCurrent = gpWorldLevelData[sGridNo].pStructureHead;
while ( pCurrent != NULL )
{
pBaseStructure = FindBaseStructure( pCurrent );
if ( pBaseStructure && pBaseStructure->ubHitPoints )
{
sBaseGridNo = pBaseStructure->sGridNo;
break;
}
pCurrent = pCurrent->pNext;
}
if ( sBaseGridNo == NOWHERE )
return;
INT16 id = -1;
std::vector<ENEMY_HELI>::iterator itend = gEnemyHeliVector.end( );
for ( std::vector<ENEMY_HELI>::iterator it = gEnemyHeliVector.begin( ); it != itend; ++it )
{
++id;
// we can only provide graphics for ENEMYHELI_MAX helis
if ( id >= ENEMYHELI_MAX )
continue;
// if the heli is already destroyed, nothing to do here
if ( (*it).flagmask & ENEMYHELI_DESTROYED )
continue;
if ( (*it).sector_current == currentsector )
{
if ( gModSettings.sEnemyHeliBaseParkGridno[id] == sBaseGridNo )
{
if ( aDestroyed )
{
(*it).Destroy( );
}
// if the structure is not destroyed, the heli stays
else
{
(*it).hp = max( 1, (*it).hp - ubDamage );
}
}
}
}
}
//////////////////////////////////////////// enemy tanks /////////////////////////////////////////////////////////
UINT32 ASDResourceCostFuel( UINT8 aType )
{
switch ( aType )
{
case ASD_TANK:
return gGameExternalOptions.gASDResource_Fuel_Tank;
break;
case ASD_JEEP:
return gGameExternalOptions.gASDResource_Fuel_Jeep;
break;
case ASD_ROBOT:
return gGameExternalOptions.gASDResource_Fuel_Robot;
break;
}
return 0;
}
// if ASD has tanks, it can allow the queen to upgrade soldiers to tanks
BOOLEAN ASDSoldierUpgradeToTank( )
{
if ( gGameExternalOptions.fASDActive && gGameExternalOptions.fASDAssignsTanks && RebelCommand::GetASDCanDeployUnits() )
{
if ( gASD_Flags & ASDFACT_TANK_UNLOCKED )
{
if ( gASDResource[ASD_TANK] )
{
gASDResource[ASD_TANK] = max( 0, gASDResource[ASD_TANK] - 1 );
return TRUE;
}
}
}
return FALSE;
}
BOOLEAN ASDSoldierUpgradeToJeep( )
{
if ( gGameExternalOptions.fASDActive && gGameExternalOptions.fASDAssignsJeeps && RebelCommand::GetASDCanDeployUnits() )
{
if ( gASD_Flags & ASDFACT_JEEP_UNLOCKED )
{
if ( gASDResource[ASD_JEEP] )
{
gASDResource[ASD_JEEP] = max( 0, gASDResource[ASD_JEEP] - 1 );
return TRUE;
}
}
}
return FALSE;
}
BOOLEAN ASDSoldierUpgradeToRobot( )
{
if ( gGameExternalOptions.fASDActive && gGameExternalOptions.fASDAssignsRobots && RebelCommand::GetASDCanDeployUnits() )
{
if ( gASD_Flags & ASDFACT_ROBOT_UNLOCKED )
{
if ( gASDResource[ASD_ROBOT] )
{
gASDResource[ASD_ROBOT] = max( 0, gASDResource[ASD_ROBOT] - 1 );
return TRUE;
}
}
}
return FALSE;
}