/*
** Command & Conquer Generals Zero Hour(tm)
** Copyright 2025 Electronic Arts Inc.
**
** This program 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 3 of the License, or
** (at your option) any later version.
**
** This program 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 this program. If not, see .
*/
////////////////////////////////////////////////////////////////////////////////
// //
// (c) 2001-2003 Electronic Arts Inc. //
// //
////////////////////////////////////////////////////////////////////////////////
// FILE: BuildAssistant.cpp ///////////////////////////////////////////////////////////////////////
// Author: Colin Day, February 2002
// Desc: Singleton class to encapsulate some of the more common functions or rules
// that apply to building structures and units
///////////////////////////////////////////////////////////////////////////////////////////////////
// USER INCLUDES //////////////////////////////////////////////////////////////////////////////////
#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine
#include "Common/BuildAssistant.h"
#include "Common/GlobalData.h"
#include "Common/Player.h"
#include "Common/ThingTemplate.h"
#include "Common/GameAudio.h"
#include "Common/ThingFactory.h"
#include "Common/Team.h"
#include "Common/TerrainTypes.h"
#include "Common/MapObject.h"
#include "GameClient/ControlBar.h"
#include "GameClient/Drawable.h"
#include "GameClient/InGameUI.h"
#include "GameClient/TerrainVisual.h"
#include "GameLogic/AI.h"
#include "GameLogic/PartitionManager.h"
#include "GameLogic/TerrainLogic.h"
#include "GameLogic/AIPathfind.h"
#include "GameLogic/Module/AIUpdate.h"
#include "GameLogic/Module/BehaviorModule.h"
#include "GameLogic/Module/ContainModule.h"
#include "GameLogic/Module/CreateModule.h"
#include "GameLogic/Module/ProductionUpdate.h"
#include "GameLogic/Module/ContainModule.h"
#include "GameLogic/Module/ParkingPlaceBehavior.h"
// PUBLIC DATA ////////////////////////////////////////////////////////////////////////////////////
BuildAssistant *TheBuildAssistant = NULL;
#ifdef _INTERNAL
// for occasional debugging...
//#pragma optimize("", off)
//#pragma MESSAGE("************************************** WARNING, optimization disabled for debugging purposes")
#endif
///////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
ObjectSellInfo::ObjectSellInfo( void )
{
m_id = INVALID_ID;
m_sellFrame = 0;
} // end ObjectSellInfo
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
ObjectSellInfo::~ObjectSellInfo( void )
{
} // end ~ObjectSellInfo
///////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
//-------------------------------------------------------------------------------------------------
/** Is this object a dozer */
//-------------------------------------------------------------------------------------------------
static Bool isDozer( Object *obj )
{
// sanity
if( obj == NULL )
return FALSE;
if( obj->isKindOf(KINDOF_DOZER))
return TRUE;
return FALSE;
} // end isDozer
// PUBLIC /////////////////////////////////////////////////////////////////////////////////////////
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
BuildAssistant::BuildAssistant( void )
{
m_buildPositions = NULL;
m_buildPositionSize = 0;
m_sellList.clear();
} // end BuildAssistant
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
BuildAssistant::~BuildAssistant( void )
{
// delete build position array if we used it
if( m_buildPositions )
{
delete [] m_buildPositions;
m_buildPositions = NULL;
m_buildPositionSize = 0;
} // end if
} // end ~BuildAssistant
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
void BuildAssistant::init( void )
{
//
// allocate our array of positions that we use to assist ourselves when constructing
// a tiled array of locations to build things like walls
//
m_buildPositionSize = TheGlobalData->m_maxLineBuildObjects;
m_buildPositions = NEW Coord3D[ m_buildPositionSize ];
} // end init
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
void BuildAssistant::reset( void )
{
// clear all our data from the sell list
ObjectSellInfo *sellInfo;
ObjectSellListIterator it;
for( it = m_sellList.begin(); it != m_sellList.end(); ++it )
{
// get sell info
sellInfo = (*it);
// delete our data and erase this entry from the list
sellInfo->deleteInstance();
} // end for
// clear the sell list
m_sellList.clear();
} // end reset
static const Real FRAMES_TO_ALLOW_SCAFFOLD = LOGICFRAMES_PER_SECOND * 1.5f;
static const Real TOTAL_FRAMES_TO_SELL_OBJECT = LOGICFRAMES_PER_SECOND * 3.0f;
//-------------------------------------------------------------------------------------------------
/** Update phase for the build assistant */
//-------------------------------------------------------------------------------------------------
void BuildAssistant::update( void )
{
ObjectSellInfo *sellInfo;
Object *obj;
// remove any objects from the sell list who's "sell time" is up
ObjectSellListIterator it, thisIterator;
for( it = m_sellList.begin(); it != m_sellList.end(); /*empty*/ )
{
// get this object info
sellInfo = (*it);
// increment the iterator to the next as we may remove it
thisIterator = it;
++it;
// find the object
obj = TheGameLogic->findObjectByID( sellInfo->m_id );
//
// if object is not found, remove it from the list immediately ... this is valid as
// the object maybe was destroyed by other means during the sell process
//
if( obj == NULL )
{
sellInfo->deleteInstance();
m_sellList.erase( thisIterator );
continue;
} // end if
// decrement the construction percent
if( TheGameLogic->getFrame() - sellInfo->m_sellFrame >= FRAMES_TO_ALLOW_SCAFFOLD )
{
Real previousConstructionPercent = obj->getConstructionPercent();
// do the constructoin
obj->setConstructionPercent( previousConstructionPercent - (100.0f / TOTAL_FRAMES_TO_SELL_OBJECT) );
//
// did we cross the threshold from positive to negative ... if so, we will clear
// visual construction indicators
//
if( previousConstructionPercent > 0.0f && obj->getConstructionPercent() <= 0.0f )
{
obj->clearAndSetModelConditionFlags( MAKE_MODELCONDITION_MASK2( MODELCONDITION_PARTIALLY_CONSTRUCTED,
MODELCONDITION_ACTIVELY_BEING_CONSTRUCTED ),
MAKE_MODELCONDITION_MASK( MODELCONDITION_SOLD ) );
//
// set the animation durations so that the regular build up loop animations can be
// done a bit faster for selling
//
Drawable *draw = obj->getDrawable();
if( draw )
draw->setAnimationLoopDuration( TOTAL_FRAMES_TO_SELL_OBJECT / 2 );
} // end if
} // end if
//
// after we've reached zero ... the object has "sunk" back down into the ground ... but
// we need to keep the object around for a little while longer so that the scaffold
// animation can finish up and sink itself back into the ground
//
if( obj->getConstructionPercent() <= -50.0f )
{
// refund the money to the controlling player
Player *player = obj->getControllingPlayer();
if( player )
{
UnsignedInt sellValue;
// 0 is the init, and means you have no override. We would be marked unsellable if someone wanted us to not sell
if( obj->getTemplate()->getRefundValue() != 0 )
sellValue = obj->getTemplate()->getRefundValue();
else
sellValue = REAL_TO_UNSIGNEDINT( obj->getTemplate()->calcCostToBuild( player ) *
TheGlobalData->m_sellPercentage );
player->getMoney()->deposit( sellValue );
// this money shouldn't be scored since it wasn't really "earned."
// player->getScoreKeeper()->addMoneyEarned( sellValue );
} // end if
// cancel any of the production items and refund to the controlling player
ProductionUpdateInterface *pui = obj->getProductionUpdateInterface();
if( pui )
pui->cancelAndRefundAllProduction();
// for debugging
// UnicodeString msg;
// msg.format( L"'%S' has been sold\n", obj->getTemplate()->getName().str() );
// TheInGameUI->message( msg );
// destroy the object
TheGameLogic->destroyObject( obj );
// remove this object from the sell list
sellInfo->deleteInstance();
m_sellList.erase( thisIterator );
} // end if
} // end for
} // end update
//-------------------------------------------------------------------------------------------------
/** Xfer the sell list. */
//-------------------------------------------------------------------------------------------------
void BuildAssistant::xferTheSellList( Xfer *xfer )
{
ObjectSellInfo *sellInfo;
Int count=0;
ObjectSellListIterator it, thisIterator;
for( it = m_sellList.begin(); it != m_sellList.end(); ++it ) {
count++;
}
xfer->xferInt(&count);
if (xfer->getXferMode() == XFER_LOAD) {
m_sellList.clear();
Int i;
for (i=0; ixferObjectID(&sellInfo->m_id);
xfer->xferUnsignedInt(&sellInfo->m_sellFrame);
m_sellList.push_back( sellInfo );
}
} else {
for( it = m_sellList.begin(); it != m_sellList.end(); ++it ) {
// get this object info
sellInfo = (*it);
xfer->xferObjectID(&sellInfo->m_id);
xfer->xferUnsignedInt(&sellInfo->m_sellFrame);
count--;
}
DEBUG_ASSERTCRASH(count==0, ("Inconsistent list size counts."));
}
} // end xferTheSellList
//-------------------------------------------------------------------------------------------------
/** Nice little method to wrap up creating an object from a build */
//-------------------------------------------------------------------------------------------------
Object *BuildAssistant::buildObjectNow( Object *constructorObject, const ThingTemplate *what,
const Coord3D *pos, Real angle, Player *owningPlayer )
{
// sanity
if( what == NULL || pos == NULL )
return NULL;
if( owningPlayer == NULL )
return NULL;// Invalid pointer. Won't happen.
// sanity
if( constructorObject )
{
DEBUG_ASSERTCRASH( constructorObject->getControllingPlayer() == owningPlayer,
("buildObjectNow: Constructor object player is not the same as the controlling player passed in\n") );
} // end if
// Need to validate that we can make this in case someone fakes their CommandSet
// A NULL constructor Object means a script built building so let it slide.
if( (constructorObject != NULL) && !isPossibleToMakeUnit(constructorObject, what) )
return NULL;
// clear out any objects from the building area that are "auto-clearable" when building
clearRemovableForConstruction( what, pos, angle );
if ( moveObjectsForConstruction( what, pos, angle, owningPlayer ) == FALSE )
{
// totally bogus. We tried to move our units out of the way, but they wouldn't.
// Chode-boys.
if (owningPlayer->getPlayerType()==PLAYER_HUMAN) {
return NULL; // ai gets to cheat. jba.
}
}
// do the build
if( isDozer( constructorObject ) && isLineBuildTemplate( what ) == FALSE )
{
AIUpdateInterface *ai = constructorObject->getAIUpdateInterface();
if( ai )
{
ai->aiIdle(CMD_FROM_AI); // stop any current behavior.
return ai->construct( what, pos, angle, owningPlayer, FALSE );
}
return NULL;
} // end else if
else
{
// create the new object. We need to construct it with UnderConstruction set, since we are going to insta build it,
// but we don't want to send double construction type events like power creation. onStructureConstructionComplete
// is called below, so we need to simulate the proper object creation flow from the start. Be like Dozer.
ObjectStatusMaskType startingStatus;
if( what->isKindOf( KINDOF_STRUCTURE ) )
startingStatus.set( MAKE_OBJECT_STATUS_MASK( OBJECT_STATUS_UNDER_CONSTRUCTION ) );
Object *obj = TheThingFactory->newObject( what, owningPlayer->getDefaultTeam(), startingStatus );
obj->setProducer(constructorObject);
// place on terrain surface
Coord3D groundPos;
groundPos.x = pos->x;
groundPos.y = pos->y;
groundPos.z = TheTerrainLogic->getGroundHeight( groundPos.x, groundPos.y );
obj->setPosition( &groundPos );
obj->setOrientation( angle );
TheAI->pathfinder()->addObjectToPathfindMap( obj );
// notify the player that this thing has come into existence
if( obj->isKindOf( KINDOF_STRUCTURE ) )
{
//
// this is a cheat since this method is currently used by the solo AI to make
// buildings out of thin air
//
owningPlayer->onStructureCreated( constructorObject, obj );
owningPlayer->onStructureConstructionComplete( constructorObject, obj, FALSE );
} // end if
else
{
owningPlayer->onUnitCreated( constructorObject, obj );
//Call the voice created for the 1st object -- because it's possible to create multiple objects like redguards!
AudioEventRTS sound = *obj->getTemplate()->getPerUnitSound( "VoiceCreated" );
sound.setObjectID( obj->getID() );
TheAudio->addAudioEvent( &sound );
}
// Now onCreates were called at the constructor. This magically created
// thing needs to be considered as Built for Game specific stuff.
for (BehaviorModule** m = obj->getBehaviorModules(); *m; ++m)
{
CreateModuleInterface* create = (*m)->getCreate();
if (!create)
continue;
create->onBuildComplete();
}
// Creation is another valid and essential time to call this. This building now Looks.
obj->handlePartitionCellMaintenance();
return obj;
} // end else
return NULL;
} // end buildObjectNow
//-------------------------------------------------------------------------------------------------
/** This method will create a line of objects end to end along the line defined in 3D
* space from start to end. This is especially useful in building walls */
//-------------------------------------------------------------------------------------------------
void BuildAssistant::buildObjectLineNow( Object *constructorObject, const ThingTemplate *what,
const Coord3D *start, const Coord3D *end, Real angle,
Player *owningPlayer )
{
TileBuildInfo *tileBuildInfo;
// sanity
if( what == NULL || start == NULL || end == NULL )
return;
// how big are each of our objects
Real objectSize = what->getTemplateGeometryInfo().getMajorRadius() * 2.0f;
// what is our max tiling length we can make
Int maxObjects = TheGlobalData->m_maxLineBuildObjects;
// build an array of locations that we want to build from start to end
tileBuildInfo = buildTiledLocations( what, angle, start, end,
objectSize, maxObjects, constructorObject );
// create an object at each position
for( Int i = 0; i < tileBuildInfo->tilesUsed; i++ )
buildObjectNow( constructorObject, what, &tileBuildInfo->positions[ i ], angle, owningPlayer );
} // end buildObjectLineNow
//-------------------------------------------------------------------------------------------------
/** This structure is passed along to the checkSampleBuildLocation while iterating the
* footprint of an area in order to determine if the entire location is a legal place to
* build */
//-------------------------------------------------------------------------------------------------
struct SampleBuildData
{
Region3D mapRegion;
Bool terrainRestricted; ///< one of the terrain tiles prevents building
Real hiZ; ///< highest sample point used
Real loZ; ///< lowest sample point used
};
//-------------------------------------------------------------------------------------------------
/** This will check the build conditions at the specified sample location point */
//-------------------------------------------------------------------------------------------------
static void checkSampleBuildLocation( const Coord3D *samplePoint, void *userData )
{
TerrainType *terrain;
SampleBuildData *sampleData = (SampleBuildData *)userData;
// get the terrain tile here
terrain = TheTerrainVisual->getTerrainTile( samplePoint->x, samplePoint->y );
if( terrain )
{
// check for the restricts building flag
if( terrain->getRestrictConstruction() )
sampleData->terrainRestricted = TRUE;
} // end if
Int cellX = REAL_TO_INT_FLOOR( samplePoint->x / PATHFIND_CELL_SIZE );
Int cellY = REAL_TO_INT_FLOOR( samplePoint->y / PATHFIND_CELL_SIZE );
PathfindCell* cell = TheAI->pathfinder()->getCell( LAYER_GROUND, cellX, cellY );
if (!cell) {
sampleData->terrainRestricted = TRUE;
} else {
enum PathfindCell::CellType type = cell->getType();
if ( (type == PathfindCell::CELL_WATER) || (type == PathfindCell::CELL_CLIFF) ||
(type == PathfindCell::CELL_IMPASSABLE)) {
sampleData->terrainRestricted = true;
}
}
//
// record the highest and lowest Z points from all the samples and do not allow
// building when the difference between them is too great
//
if( samplePoint->z < sampleData->loZ )
sampleData->loZ = samplePoint->z;
if( samplePoint->z > sampleData->hiZ )
sampleData->hiZ = samplePoint->z;
// too close to edge of map?
if (TheGlobalData->m_MinDistFromEdgeOfMapForBuild > 0.0f)
{
if (samplePoint->x < sampleData->mapRegion.lo.x + TheGlobalData->m_MinDistFromEdgeOfMapForBuild
|| samplePoint->x > sampleData->mapRegion.hi.x - TheGlobalData->m_MinDistFromEdgeOfMapForBuild
|| samplePoint->y < sampleData->mapRegion.lo.y + TheGlobalData->m_MinDistFromEdgeOfMapForBuild
|| samplePoint->y > sampleData->mapRegion.hi.y - TheGlobalData->m_MinDistFromEdgeOfMapForBuild)
{
sampleData->terrainRestricted = TRUE;
}
}
} // end checkSampleBuildLocation
//-------------------------------------------------------------------------------------------------
/** This function will call the user callback at each "sample point" across the footprint
* of where this object would be in the world. The smaller the sample resolution param is
* the more samples will be taken across the footprint area */
//-------------------------------------------------------------------------------------------------
void BuildAssistant::iterateFootprint( const ThingTemplate *build,
Real buildOrientation,
const Coord3D *worldPos,
Real sampleResolution,
IterateFootprintFunc func,
void *funcUserData )
{
// sanity
if( build == NULL || worldPos == NULL || func == NULL )
return;
//
// create a transform matrix for the object position at the specified angle, we will
// iterate the object footprints in "local object extent space" and transform those
// points using this matrix into the world coords for the object at the real
// location and specified angle
//
Matrix3D transform;
transform.Make_Identity();
transform.Adjust_Translation( Vector3( worldPos->x, worldPos->y, worldPos->z ) );
transform.Rotate_Z( buildOrientation );
// get the bounding footprint rectangle for the geometry we're looking at
Real halfFootprintHeight,
halfFootprintWidth;
if( build->getTemplateGeometryInfo().getGeomType() == GEOMETRY_BOX )
{
halfFootprintHeight = build->getTemplateGeometryInfo().getMinorRadius();
halfFootprintWidth = build->getTemplateGeometryInfo().getMajorRadius();
} // end if
else if( build->getTemplateGeometryInfo().getGeomType() == GEOMETRY_SPHERE ||
build->getTemplateGeometryInfo().getGeomType() == GEOMETRY_CYLINDER )
{
halfFootprintHeight = build->getTemplateGeometryInfo().getBoundingCircleRadius();
halfFootprintWidth = build->getTemplateGeometryInfo().getBoundingCircleRadius();
} // end else if
else
{
DEBUG_ASSERTCRASH( 0, ("iterateFootprint: Undefined geometry '%d' for '%s'\n",
build->getTemplateGeometryInfo().getGeomType(), build->getName().str()) );
return;
} // end else
//
// start at a corner of the extent ... box geometries have a major radius down
// the X axis with the minor radius on the Y axis. Note that we are allowing
// the sample process to go an "extra" sample resolution outside of the
// real extent here. This is so that we can be sure to sample all of the
// footprint area, if our sample point is outside of the actual extent footprint
// area because of this we snap it back to sample on the exact edge
//
Real x, y;
Vector3 v;
for( y = -halfFootprintHeight;
y < halfFootprintHeight + sampleResolution;
y += sampleResolution )
{
// snap it to the actual extent since we can go over by one sample resolution
if( y > halfFootprintHeight )
y = halfFootprintHeight;
for( x = -halfFootprintWidth;
x < halfFootprintWidth + sampleResolution;
x += sampleResolution )
{
// snap it to the actual extent since we can go over by one sample resolution
if( x > halfFootprintWidth )
x = halfFootprintWidth;
// transform to world
v.Set( x, y, TheTerrainLogic->getGroundHeight( x, y ) );
transform.Transform_Vector( transform, v, &v );
// for circular geometries we must actually be within the circle
if( build->getTemplateGeometryInfo().getGeomType() == GEOMETRY_SPHERE ||
build->getTemplateGeometryInfo().getGeomType() == GEOMETRY_CYLINDER )
{
Coord2D vector;
vector.x = v.X - worldPos->x;
vector.y = v.Y - worldPos->y;
if( vector.length() > halfFootprintWidth ) // could be height too, radius is all the same for circles
continue; // ignore this point
} // end if
// call the user callback
Coord3D pos;
pos.x = v.X;
pos.y = v.Y;
pos.z = TheTerrainLogic->getGroundHeight( pos.x, pos.y );
func( &pos, funcUserData );
} // end for x
} // end for y
} // end iterateFootprint
//-------------------------------------------------------------------------------------------------
/** Check for objects preventing building at this location. */
//-------------------------------------------------------------------------------------------------
LegalBuildCode BuildAssistant::isLocationClearOfObjects( const Coord3D *worldPos,
const ThingTemplate *build,
Real angle,
Object *builderObject,
UnsignedInt options,
Player *thePlayer)
{
ObjectIterator *iter =
ThePartitionManager->iteratePotentialCollisions( worldPos,
build->getTemplateGeometryInfo(),
angle );
Object *them;
Bool onlyCheckEnemies = (options == NO_ENEMY_OBJECT_OVERLAP);
MemoryPoolObjectHolder hold(iter);
for( them = iter->first(); them; them = iter->next() )
{
Bool feedbackWithFailure = TRUE;
Relationship rel = builderObject ? builderObject->getRelationship( them ) : NEUTRAL;
//Kris: If the object is stealthed and we can't see it, pretend we can build there.
if( BitTest( options, IGNORE_STEALTHED ) )
{
if( rel != ALLIES )
{
if( them->testStatus( OBJECT_STATUS_STEALTHED ) && !them->testStatus( OBJECT_STATUS_DETECTED ) && !them->testStatus( OBJECT_STATUS_DISGUISED ) )
{
if( BitTest( options, FAIL_STEALTHED_WITHOUT_FEEDBACK ) )
{
feedbackWithFailure = FALSE; //We want to fail now but without feedback
}
else
{
continue;
}
}
}
}
//Kris: Patch 1.01 - November 5, 2003
//Prevent busy units (black lotus hacking from being moved by trying to place a building -- exploit).
if( rel == ALLIES )
{
if( them->testStatus( OBJECT_STATUS_IS_USING_ABILITY ) || them->getAI() && them->getAI()->isBusy() )
{
return LBC_OBJECTS_IN_THE_WAY;
}
}
// ignore any kind of class of objects that we will "remove" for building
if( isRemovableForConstruction( them ) == TRUE )
continue;
// ignore land mines, cluster mines and demo traps, since you can build on them
// doing so will damage them during construction, by the way
if (them->isKindOf( KINDOF_MINE ))
continue;
// same story for inert things: ie, radiation fields, pings, and projectile streams (!)...
if (them->isKindOf(KINDOF_INERT))
continue;
// an immobile object may obstruct our building depending on flags.
if( them->isKindOf( KINDOF_IMMOBILE ) )
{
if (onlyCheckEnemies && builderObject && rel != ENEMIES )
{
continue;
}
if( feedbackWithFailure )
{
TheTerrainVisual->addFactionBib( them, TRUE );
return LBC_OBJECTS_IN_THE_WAY;
}
return LBC_GENERIC_FAILURE;
}
if( them->isDisabled() )
{
//Kris: If object is disabled, it can't move out of the way, thus you can't build there.
if( feedbackWithFailure )
{
TheTerrainVisual->addFactionBib( them, TRUE );
return LBC_OBJECTS_IN_THE_WAY;
}
return LBC_GENERIC_FAILURE;
}
//
// if this is an enemy object of the builder object (and therefore the thing
// that will be constructed) we can't build here
//
if( builderObject && rel == ENEMIES )
{
if( feedbackWithFailure )
{
TheTerrainVisual->addFactionBib( them, TRUE );
return LBC_OBJECTS_IN_THE_WAY;
}
return LBC_GENERIC_FAILURE;
}
} // end for, them
if (onlyCheckEnemies)
{
return LBC_OK;
}
// Check for overlapping exit areas.
Real range = 2*(build->getTemplateGeometryInfo().getMajorRadius()+build->getTemplateGeometryInfo().getMinorRadius());
PartitionFilterAcceptByKindOf f1(MAKE_KINDOF_MASK(KINDOF_STRUCTURE), KINDOFMASK_NONE);
PartitionFilter *filters[] = { &f1, NULL };
ObjectIterator *iter2 = ThePartitionManager->iterateObjectsInRange(worldPos, range, FROM_BOUNDINGSPHERE_2D, filters);
MemoryPoolObjectHolder hold2(iter2);
Real myFactoryExitWidth = build->getFactoryExitWidth();
Real myExtraWidth = build->getFactoryExtraBibWidth();
if (thePlayer && thePlayer->isSkirmishAIPlayer()) {
// Skirmish ai adds a little extra around the edges so it doesn't build itself into a corner.
if (myExtraWidth < 3*PATHFIND_CELL_SIZE_F) {
myExtraWidth = 3*PATHFIND_CELL_SIZE_F;
myFactoryExitWidth -= myExtraWidth;
if (myFactoryExitWidth<0) myFactoryExitWidth = 0;
}
}
Bool checkMyExit = false;
Coord3D myExitPos;
GeometryInfo myBounds = build->getTemplateGeometryInfo();
myBounds.setMajorRadius(myBounds.getMajorRadius()+myExtraWidth);
if (myBounds.getGeomType() != GEOMETRY_BOX) {
myBounds.set(GEOMETRY_BOX, false, 40, myBounds.getMajorRadius(), myBounds.getMajorRadius());
} else {
myBounds.setMinorRadius(myBounds.getMinorRadius()+myExtraWidth);
}
GeometryInfo myGeom = build->getTemplateGeometryInfo();
if (myGeom.getGeomType() != GEOMETRY_BOX) {
myGeom.setMinorRadius(myGeom.getMajorRadius());
}
myGeom.setMajorRadius(myFactoryExitWidth/2.0f);
if (myFactoryExitWidth>0) {
myExitPos = *worldPos;
checkMyExit = true;
Real c = (Real)cos(angle);
Real s = (Real)sin(angle);
Real offset = build->getTemplateGeometryInfo().getMajorRadius() + myFactoryExitWidth/2.0f;
myExitPos.x += c*offset;
myExitPos.y += s*offset;
}
for( them = iter2->first(); them; them = iter2->next() )
{
Relationship rel = builderObject ? builderObject->getRelationship( them ) : NEUTRAL;
//Kris: If the building is stealthed and we can't see it, pretend we can build there.
if( BitTest( options, IGNORE_STEALTHED ) )
{
if( rel != ALLIES )
{
if( them->testStatus( OBJECT_STATUS_STEALTHED ) && !them->testStatus( OBJECT_STATUS_DETECTED ) && !them->testStatus( OBJECT_STATUS_DISGUISED ) )
{
continue;
}
}
}
// ignore any kind of class of objects that we will "remove" for building
if( isRemovableForConstruction( them ) == TRUE )
continue;
Real themFactoryExitWidth = them->getTemplate()->getFactoryExitWidth();
Real hisExtraWidth = them->getTemplate()->getFactoryExtraBibWidth();
Bool checkHisExit = false;
Coord3D hisExitPos;
GeometryInfo hisBounds = them->getGeometryInfo();
hisBounds.setMajorRadius(hisBounds.getMajorRadius()+hisExtraWidth);
if (hisBounds.getGeomType() != GEOMETRY_BOX) {
hisBounds.set(GEOMETRY_BOX, false, 40, hisBounds.getMajorRadius(), hisBounds.getMajorRadius());
} else {
hisBounds.setMinorRadius(hisBounds.getMinorRadius()+myExtraWidth);
}
GeometryInfo hisGeom = them->getGeometryInfo();
hisGeom.setMajorRadius(themFactoryExitWidth/2.0f);
if (hisGeom.getGeomType() != GEOMETRY_BOX) {
hisGeom.setMinorRadius(them->getGeometryInfo().getMajorRadius());
}
if (themFactoryExitWidth>0) {
hisExitPos = *them->getPosition();
checkHisExit = true;
Real c = (Real)cos(them->getOrientation());
Real s = (Real)sin(them->getOrientation());
Real offset = them->getGeometryInfo().getMajorRadius() + themFactoryExitWidth/2.0f;
hisExitPos.x += c*offset;
hisExitPos.y += s*offset;
}
if (ThePartitionManager->geomCollidesWithGeom(them->getPosition(), hisBounds, them->getOrientation(),
worldPos, myBounds, angle)) {
TheTerrainVisual->addFactionBib(them, true);
return LBC_OBJECTS_IN_THE_WAY;
}
if (!checkMyExit && !checkHisExit && !hisExtraWidth && !myExtraWidth)
{
continue; // neither has extra exit space.
}
// an immobile object will obstruct our building no matter what team it's on
if ( them->isKindOf( KINDOF_IMMOBILE ) ) {
/* Check for overlap of my exit rectangle to his geom info. */
if (checkMyExit && ThePartitionManager->geomCollidesWithGeom(them->getPosition(), hisBounds, them->getOrientation(),
&myExitPos, myGeom, angle)) {
TheTerrainVisual->addFactionBib(them, true);
return LBC_OBJECTS_IN_THE_WAY;
}
// Check for overlap of his exit rectangle with my geom info
if (checkHisExit && ThePartitionManager->geomCollidesWithGeom(&hisExitPos, hisGeom, them->getOrientation(),
worldPos, myBounds, angle)) {
TheTerrainVisual->addFactionBib(them, true);
return LBC_OBJECTS_IN_THE_WAY;
}
// Check both exit rectangles together.
if (checkMyExit&&checkHisExit&&ThePartitionManager->geomCollidesWithGeom(&hisExitPos, hisGeom, them->getOrientation(),
&myExitPos, myGeom, angle)) {
TheTerrainVisual->addFactionBib(them, true);
return LBC_OBJECTS_IN_THE_WAY;
}
}
} // end for, them
return LBC_OK;
}
//-------------------------------------------------------------------------------------------------
/** Query if we can build at this location. Note that 'build' may be null and is NOT required
* to be valid to know if a location is legal to build at. 'builderObject' is used
* for queries that require a pathfind check and should be NULL if not required */
//-------------------------------------------------------------------------------------------------
LegalBuildCode BuildAssistant::isLocationLegalToBuild( const Coord3D *worldPos,
const ThingTemplate *build,
Real angle,
UnsignedInt options,
Object *builderObject,
Player *player)
{
/* You just can't never build off the map, regardless of options. jba. */
Region3D mapExtent;
TheTerrainLogic->getMaximumPathfindExtent(&mapExtent);
if (!mapExtent.isInRegionNoZ(worldPos)) {
return LBC_RESTRICTED_TERRAIN;
}
// check shroud level
// This should be the first check, since returning other errors for shrouded areas could be used to game the system
if( BitTest( options, SHROUD_REVEALED ) )
{
{
Int x, y;
ThePartitionManager->worldToCell(worldPos->x, worldPos->y, &x, &y);
Int playerIndex = -1;
if (builderObject && builderObject->getControllingPlayer())
playerIndex = builderObject->getControllingPlayer()->getPlayerIndex();
DEBUG_ASSERTCRASH(playerIndex >= 0, ("isLocationLegalToBuild() needs a builderObject with a team to check for shroud"));
if( ThePartitionManager->getShroudStatusForPlayer(playerIndex, x, y) != CELLSHROUD_CLEAR )
{
return LBC_SHROUD;
}
}
}
//
// if NO_OBJECT_OVERLAP is set, we are not allowed to construct 'build' if it would overlap
// any immobile objects, or an enemy object. Friendly objects should politely
// "move out of the way" when you build something where they're standing
//
if( BitTest( options, NO_OBJECT_OVERLAP ) )
{
LegalBuildCode code = isLocationClearOfObjects(worldPos, build, angle, builderObject, options, player);
if( code != LBC_OK )
{
return code;
}
} // end if
//
// if NO_ENEMY_OBJECT_OVERLAP is set, we are not allowed to construct 'build' if it would overlap
// any enemy objects. Friendly objects are ignored.
//
if( BitTest( options, NO_ENEMY_OBJECT_OVERLAP ) )
{
LegalBuildCode code = isLocationClearOfObjects(worldPos, build, angle, builderObject, options, player);
if( code != LBC_OK )
{
return code;
}
} // end if
if (build->isKindOf(KINDOF_CANNOT_BUILD_NEAR_SUPPLIES) && TheGlobalData->m_SupplyBuildBorder > 0)
{
// special case for supply centers: can't build too close to supply sources
PartitionFilterAcceptByKindOf f1(MAKE_KINDOF_MASK(KINDOF_SUPPLY_SOURCE), KINDOFMASK_NONE);
PartitionFilter *filters[] = { &f1, NULL };
// see if there are any reasonably close by
Real range = build->getTemplateGeometryInfo().getBoundingCircleRadius() + TheGlobalData->m_SupplyBuildBorder*2;
Object* tooClose = ThePartitionManager->getClosestObject(worldPos, range, FROM_BOUNDINGSPHERE_2D, filters);
if (tooClose != NULL)
{
// yep, see if we would collide with an expanded version
GeometryInfo tooCloseGeom = tooClose->getGeometryInfo();
tooCloseGeom.expandFootprint(TheGlobalData->m_SupplyBuildBorder);
if (ThePartitionManager->geomCollidesWithGeom(
worldPos,
build->getTemplateGeometryInfo(),
angle,
tooClose->getPosition(),
tooCloseGeom,
tooClose->getOrientation()))
{
TheTerrainVisual->addFactionBib(tooClose, true, TheGlobalData->m_SupplyBuildBorder);
return LBC_TOO_CLOSE_TO_SUPPLIES;
}
}
}
// if clear path is requested check to see if the builder object can get there (unless it's a structure)
if( BitTest( options, CLEAR_PATH ) && builderObject && !builderObject->isKindOf( KINDOF_IMMOBILE ) )
{
AIUpdateInterface *ai = builderObject->getAIUpdateInterface();
//
// if there is no AI interface for this object, it cannot possible pass a clear path
// check since it will never be able to move there
//
/**todo remove this if we need to change the semantics of this function of the builderObject
// actually being able to get to the destination */
//
if( ai == NULL )
return LBC_NO_CLEAR_PATH;
if( ai->isQuickPathAvailable( worldPos ) == FALSE )
return LBC_NO_CLEAR_PATH;
} // end if
// check basic terrain restrctions
if( BitTest( options, TERRAIN_RESTRICTIONS ) )
{
//
// we will take "samples" at this resolution across the footprint of where we are
// going to build the structure
//
Real sampleResolution = MAP_XY_FACTOR;
// get the terrain extents
Region3D terrainExtent;
TheTerrainLogic->getExtent( &terrainExtent );
if (TheTerrainLogic->getLayerForDestination(worldPos) != LAYER_GROUND) {
// we're on a bridge. This is somewhat restricted.
return LBC_RESTRICTED_TERRAIN;
}
//
// check the footprint of where the structure would go to be clear of any non-buildable
// tiles and to make sure there isn't a restricted tile and to make sure it's "flat" enough
//
SampleBuildData sampleData;
TheTerrainLogic->getExtent( &sampleData.mapRegion );
sampleData.terrainRestricted = FALSE;
sampleData.hiZ = terrainExtent.lo.z; // note we set hi point to lowest point
sampleData.loZ = terrainExtent.hi.z; // note we set lo point to highest point
// quick check at triple res.
iterateFootprint( build, angle, worldPos, 3*sampleResolution,
checkSampleBuildLocation, &sampleData );
if( sampleData.terrainRestricted == TRUE )
return LBC_RESTRICTED_TERRAIN;
// check if the height across the whole footprint area is too varied (not flat enough)
if( sampleData.hiZ - sampleData.loZ > TheGlobalData->m_allowedHeightVariationForBuilding )
return LBC_NOT_FLAT_ENOUGH;
// careful check at full res.
iterateFootprint( build, angle, worldPos, sampleResolution,
checkSampleBuildLocation, &sampleData );
if( sampleData.terrainRestricted == TRUE )
return LBC_RESTRICTED_TERRAIN;
// check if the height across the whole footprint area is too varied (not flat enough)
if( sampleData.hiZ - sampleData.loZ > TheGlobalData->m_allowedHeightVariationForBuilding )
return LBC_NOT_FLAT_ENOUGH;
} // end if
// we passed all the checks
return LBC_OK;
} // end isLocationLegalToBuild
//-------------------------------------------------------------------------------------------------
/** Adds bibs to structures near to worldPos */
//-------------------------------------------------------------------------------------------------
void BuildAssistant::addBibs(const Coord3D *worldPos,
const ThingTemplate *build )
{
Real range = build->friend_calcVisionRange();
range += 3*build->getTemplateGeometryInfo().getMajorRadius();
PartitionFilterAcceptByKindOf f1(MAKE_KINDOF_MASK(KINDOF_STRUCTURE), KINDOFMASK_NONE);
PartitionFilter *filters[] = { &f1, NULL };
ObjectIterator *iter = ThePartitionManager->iterateObjectsInRange(worldPos, range, FROM_BOUNDINGSPHERE_2D, filters);
MemoryPoolObjectHolder hold(iter);
for( Object *them = iter->first(); them; them = iter->next() )
{
// ignore any kind of class of objects that we will "remove" for building
if( isRemovableForConstruction( them ) == TRUE )
continue;
if( them->isKindOf( KINDOF_IMMOBILE ) ) {
TheTerrainVisual->addFactionBib(them, true);
}
} // end for, them
}
//-------------------------------------------------------------------------------------------------
/** Tiling wall object helper function, we can use this to "tile" walls when building.
* This function will fill out the ARRAY world positions named 'positions' starting
* at the 'start' point in the world and going to the 'end' point in the world. No
* more than 'maxTiles' will be considered and if the position of any one of
* the objects in the chain is illegal the chain stops there. The number of objects
* actually used for the tiling is returned along with a pointer to the array
* of positions in the tile build info.
*
* REQUIRES: Note that the array at 'postions' must be large enough to hold 'maxTiles'
* entries of positions
*/
//-------------------------------------------------------------------------------------------------
BuildAssistant::TileBuildInfo *BuildAssistant::buildTiledLocations( const ThingTemplate *thingBeingTiled,
Real angle,
const Coord3D *start,
const Coord3D *end,
Real tilingSize,
Int maxTiles,
Object *builderObject )
{
// sanity
if( start == NULL || end == NULL )
return 0;
//
// we will fill out our own internal array of positions, it better be big enough to
// accomodate max tiles, if it's not lets make it bigger!
//
if( maxTiles > m_buildPositionSize )
{
// delete the old array
delete [] m_buildPositions;
// create a new one
m_buildPositions = NEW Coord3D[ maxTiles ];
m_buildPositionSize = maxTiles;
//
// lets try to at least keep sanity here so that we don't have a completely unbounded
// allocation spot in the code here
//
DEBUG_ASSERTCRASH( m_buildPositionSize < 200, ("Do you really need to tile this many objects!!!") );
} // end if
Coord3D *positions = m_buildPositions;
// compute a vector from the start of the line in the world to the end
Coord3D placementVector;
placementVector.x = end->x - start->x;
placementVector.y = end->y - start->y;
placementVector.z = 0.0f; //end->z - start->z;
//
// get the lengh of the placement vector in the world, we'll use this to see how
// many objects we'll need to fill the entire line
//
Real placementLength = placementVector.length();
//
// first see how many objects we're going to need to go from the placement
// line start to the placement line end, we are guaranteed to have one thing (+1)
//
Int tilesNeeded = REAL_TO_INT(placementLength / tilingSize) + 1;
// we have a max that we are allowed to build to
if( tilesNeeded > maxTiles )
tilesNeeded = maxTiles;
// we're guaranteed to have at least one object so lets take care of that position now
positions[ 0 ] = *start;
//
// march down each object and set the position based on its position in the line.
// note that we're skipping the first one because we've already set it explicitly
// and are guaranteed to have at least one object
//
Int tilesUsed = 1; // actual objects "tiled", we are guaranteed to have at least one
Coord3D pos;
Coord3D v = placementVector;
v.normalize();
for( Int i = 1; i < tilesNeeded; i++ )
{
// compute position of object
pos.x = v.x * (tilingSize * i) + start->x;
pos.y = v.y * (tilingSize * i) + start->y;
pos.z = TheTerrainLogic->getGroundHeight( pos.x, pos.y );
// check for a legal position to be at and stop the tiling process if that becomes broken
if( isLocationLegalToBuild( &pos, thingBeingTiled, angle,
BuildAssistant::USE_QUICK_PATHFIND |
BuildAssistant::TERRAIN_RESTRICTIONS |
BuildAssistant::CLEAR_PATH |
BuildAssistant::NO_OBJECT_OVERLAP |
BuildAssistant::SHROUD_REVEALED,
builderObject,
NULL) != LBC_OK )
break;
// save the position in the output array
positions[ i ] = pos;
// we have now actually used one more "tile"
tilesUsed++;
} // end for i
// return a struct filled out with the actual tiles used and the array of locations
static TileBuildInfo tileInfo;
tileInfo.tilesUsed = tilesUsed;
tileInfo.positions = positions;
return &tileInfo;
} // end buildTiledLocations
//-------------------------------------------------------------------------------------------------
/** Is the template passed in one of those wall type structures that we "build" in
* the world after drawing a line of where we want the object(s) placed. The objects
* are placed "tiled" in a line */
//-------------------------------------------------------------------------------------------------
Bool BuildAssistant::isLineBuildTemplate( const ThingTemplate *tTemplate )
{
// sanity
if( tTemplate == NULL )
return FALSE;
if( tTemplate->isKindOf(KINDOF_LINEBUILD))
return TRUE;
return FALSE; // not a line build object
} // end isLineBuildTemplate
//-------------------------------------------------------------------------------------------------
/** This method will check to make sure it is possible to build the requested unit. The
* builder object MUST be present since all construction comes comes from either a Dozer or
* a production building. The player must have satisfied the prereqs for 'whatToBuild'
* This does NOT check available money (see canMakeUnit) */
//-------------------------------------------------------------------------------------------------
Bool BuildAssistant::isPossibleToMakeUnit( Object *builder, const ThingTemplate *whatToBuild ) const
{
// sanity
if( builder == NULL || whatToBuild == NULL )
return FALSE;
// get the command set for the producer object
const CommandSet *commandSet = TheControlBar->findCommandSet( builder->getCommandSetString() );
// if no command set we cannot build anything
if( commandSet == NULL )
{
DEBUG_ASSERTLOG( 0, ("Can't build a '%s' from the builder '%s' because '%s' doesn't have any command set defined\n",
whatToBuild->getName().str(),
builder->getTemplate()->getName().str(),
builder->getTemplate()->getName().str()) );
return FALSE;
} // end if
//
// scan the command set, we must find whatToBuild as one of the "build" commands available
// in the command set. We want to have all players run this logic on all their machines
// so that nobody can hack one game and cheat to make stuff that they can't usually make
//
const CommandButton *commandButton;
const CommandButton *foundCommand = NULL;
Int i;
for( i = 0; i < MAX_COMMANDS_PER_SET; i++ )
{
// get this button
commandButton = commandSet->getCommandButton(i);
if( commandButton &&
(commandButton->getCommandType() == GUI_COMMAND_UNIT_BUILD ||
commandButton->getCommandType() == GUI_COMMAND_DOZER_CONSTRUCT) &&
commandButton->getThingTemplate()->isEquivalentTo(whatToBuild) )
foundCommand = commandButton;
} // end for i
if( foundCommand == NULL )
return FALSE;
// make sure that the player can actually make this unit by checking prereqs and such
Player *player = builder->getControllingPlayer();
if( player->canBuild( foundCommand->getThingTemplate() ) == FALSE )
return FALSE;
// all is well
return TRUE;
}
//-------------------------------------------------------------------------------------------------
/** This method will check to make sure it is possible to build the requested unit. and
* that the player has enough money for 'whatToBuild' */
//-------------------------------------------------------------------------------------------------
CanMakeType BuildAssistant::canMakeUnit( Object *builder, const ThingTemplate *whatToBuild ) const
{
// sanity
if( builder == NULL || whatToBuild == NULL )
return CANMAKE_NO_PREREQ;
if (builder->testScriptStatusBit(OBJECT_STATUS_SCRIPT_DISABLED) || builder->testScriptStatusBit(OBJECT_STATUS_SCRIPT_UNPOWERED))
return CANMAKE_FACTORY_IS_DISABLED;
ProductionUpdateInterface* pu = builder->getProductionUpdateInterface();
//If our builder is actually constructing an object via a special power, then allow it if the templates match.
//It's possible they won't match because a GLA command center could be in "place sneak attack" mode, and queue
//up a worker in the meantime.
if( pu && pu->getSpecialPowerConstructionCommandButton() && pu->getSpecialPowerConstructionCommandButton()->getThingTemplate() == whatToBuild )
{
return CANMAKE_OK;
}
Player *player = builder->getControllingPlayer();
// make sure we're not maxed out for this type of unit.
// Warning: isPossibleToMakeUnit() now implicitly calls
// canBuildMoreOfType(), so do this check first
if ( player && !player->canBuildMoreOfType( whatToBuild ) )
return CANMAKE_MAXED_OUT_FOR_PLAYER;
if (!isPossibleToMakeUnit(builder, whatToBuild))
return CANMAKE_NO_PREREQ;
if (pu != NULL)
{
CanMakeType cmt = pu->canQueueCreateUnit(whatToBuild);
if (cmt != CANMAKE_OK)
return cmt;
}
// make sure we have enough money to build this
Money *money = player->getMoney();
if( whatToBuild->calcCostToBuild( player ) > money->countMoney() )
return CANMAKE_NO_MONEY;
// get the command set for the producer object
return CANMAKE_OK;
}
// ------------------------------------------------------------------------------------------------
/** Some objects will be automatically removed when something is built on them so they
* aren't considered as obstacles when placing them */
// ------------------------------------------------------------------------------------------------
Bool BuildAssistant::isRemovableForConstruction( Object *obj )
{
// sanity
if( obj == NULL )
return FALSE;
if (obj->isKindOf(KINDOF_INERT))
{
DEBUG_CRASH(("should not have gotten here."));
return FALSE;
}
// all shrubbery can be removed
if( obj->isKindOf( KINDOF_SHRUBBERY ) )
return TRUE;
// anything with a kindof marked as cleared by build can be removed
if( obj->isKindOf( KINDOF_CLEARED_BY_BUILD ) )
return TRUE;
// Rubble, scrap & dead units can be cleared.
if( obj->isEffectivelyDead( ) )
return TRUE;
// not removable
return FALSE;
} // end isRemovableForConstruction
// ------------------------------------------------------------------------------------------------
/** Given that we are about to build 'whatToBuild' remove all the objects that are in the
* 'footprint' of where the build will take place that can be auto-removed for construction */
// ------------------------------------------------------------------------------------------------
void BuildAssistant::clearRemovableForConstruction( const ThingTemplate *whatToBuild,
const Coord3D *pos,
Real angle )
{
ObjectIterator *iter =
ThePartitionManager->iteratePotentialCollisions( pos,
whatToBuild->getTemplateGeometryInfo(),
angle );
MemoryPoolObjectHolder hold( iter );
for( Object *them = iter->first(); them; them = iter->next() )
{
// UI feedback objects (always selectable) never get destroyed by construction, but also should never
// trip any of the other side effects of placing them in isRemovableForConstruction()
if( isRemovableForConstruction( them ) == TRUE && !them->isKindOf( KINDOF_ALWAYS_SELECTABLE ) )
TheGameLogic->destroyObject( them );
} // end for, them
TheTerrainVisual->removeTreesAndPropsForConstruction(pos, whatToBuild->getTemplateGeometryInfo(), angle);
} // end clearRemovableForConstruction
// ------------------------------------------------------------------------------------------------
/** clearRemovable is set up to delete objects that should cease to exist (for instance, trees).
* moveObjects will move objects that are owned by the player. It will also return false if
the association with the object is enemy. (We can move neutral things) */
// ------------------------------------------------------------------------------------------------
Bool BuildAssistant::moveObjectsForConstruction( const ThingTemplate *whatToBuild,
const Coord3D *pos,
Real angle,
Player *playerToBuild )
{
GeometryInfo gi (GEOMETRY_BOX, false, 10, whatToBuild->getTemplateGeometryInfo().getMajorRadius(),
whatToBuild->getTemplateGeometryInfo().getMajorRadius());
if (whatToBuild->getTemplateGeometryInfo().getGeomType()==GEOMETRY_BOX) {
gi = whatToBuild->getTemplateGeometryInfo();
}
ObjectIterator *iter =
ThePartitionManager->iteratePotentialCollisions( pos,
gi,
angle );
Bool anyUnmovables = false;
MemoryPoolObjectHolder hold( iter );
Real radius = sqrt(pow(gi.getMajorRadius(), 2) + pow(gi.getMinorRadius(), 2));
radius *= 1.4f; // Fudge the distance,
for( Object *them = iter->first(); them; them = iter->next() )
{
//ignore land mines, cluster mines and demo traps, since you can build on them
// doing so will damage them during construction, by the way
if (them->isKindOf( KINDOF_MINE ))
continue;
// same story for inert things: ie, radiation fields, pings, and projectile streams (!)...
if (them->isKindOf(KINDOF_INERT))
continue;
// Skip KINDOF_ALWAYS_SELECTABLE and isRemovableForConstruction, because if it is
// RemovableForConstruction, it just got destroyed, and won't actually be gone until
// the end of the frame. jba.
if ( !them->isKindOf( KINDOF_ALWAYS_SELECTABLE ) && !isRemovableForConstruction(them) )
{
Relationship rel = playerToBuild->getRelationship(them->getTeam());
if (rel == NEUTRAL || rel == ALLIES)
{
// Pick a point outside of the construction and tell those fools to move there.
// Pick an arbitrary direction and tell the unit to move there, at a distance greater
// than the object's radius.
AIUpdateInterface *ai = them->getAIUpdateInterface();
if (ai)
{
// Vary the distance to move between one half the diameter of the building (roughly)
// and 1.5 times the diameter of the building
Real variedRadius = GameLogicRandomValueReal(0.5, 1.5) * radius;
Coord3D destPos;
Real dir = GameLogicRandomValueReal(-PI, PI);
Vector3 vec(variedRadius, 0, 0);
vec.Rotate_Z(dir);
destPos.x = pos->x + vec.X;
destPos.y = pos->y + vec.Y;
destPos.z = pos->z + vec.Z;
// note that this is an extra-special case... even if the unit's mood is "sleep"
// it still needs to move here. (units with an ai mood of "sleep" won't respond to
// any AI commands. this is a special case that ignores the "sleep" ai mood.) (srj)
ai->aiMoveToPositionEvenIfSleeping(&destPos, CMD_FROM_AI);
}
else
{
anyUnmovables = true;
}
}
else
{
anyUnmovables = true;
}
}
}
return !anyUnmovables;
}
// ------------------------------------------------------------------------------------------------
/** Start the process for selling the object in question */
// ------------------------------------------------------------------------------------------------
void BuildAssistant::sellObject( Object *obj )
{
// sanity
if( obj == NULL )
return;
// we can only sell structures ... sanity check this
if( obj->isKindOf( KINDOF_STRUCTURE ) == FALSE )
return;
// if object already has an entry in the sell list, we shouldn't try to sell it again
ObjectSellInfo *sellInfo = NULL;
ObjectSellListIterator it;
for( it = m_sellList.begin(); it != m_sellList.end(); ++it )
{
sellInfo = (*it);
if( sellInfo->m_id == obj->getID() )
break;
else
sellInfo = NULL;
} // end for
if( sellInfo != NULL )
return;
// set the construction percent of this object just below 100.0% so we can start counting down
obj->setConstructionPercent( 99.9f );
// add this object to the list of objects being sold
sellInfo = newInstance(ObjectSellInfo);
sellInfo->m_id = obj->getID();
sellInfo->m_sellFrame = TheGameLogic->getFrame();
m_sellList.push_front( sellInfo );
//
// set the model condition in the drawable for this object that will show the buildup
// scaffold and adjust the model height by construction percent
//
obj->setModelConditionFlags( MAKE_MODELCONDITION_MASK2( MODELCONDITION_PARTIALLY_CONSTRUCTED,
MODELCONDITION_ACTIVELY_BEING_CONSTRUCTED) );
//
// set this object as under de-construction (sold). It is still a legal target, since you get the money at
// the completion of sale.
//
obj->setStatus( MAKE_OBJECT_STATUS_MASK2( OBJECT_STATUS_SOLD, OBJECT_STATUS_UNSELECTABLE ) );
// for everybody, unselect them at this time. You can't just deselect a drawable. Selection is a logic property.
TheGameLogic->deselectObject(obj, PLAYERMASK_ALL, TRUE);
//
// set the animation durations so that the regular build up loop animations can be
// done a bit faster for selling
//
Drawable *draw = obj->getDrawable();
if( draw )
draw->setAnimationLoopDuration( TOTAL_FRAMES_TO_SELL_OBJECT / 2 );
// We also need to refund all production for the object at start-of-sell time
ProductionUpdateInterface *production = obj->getProductionUpdateInterface();
if ( production )
{
production->cancelAndRefundAllProduction();
}
// Tell the contain module so it can decide what to do.
ContainModuleInterface* contain = obj->getContain();
if( contain )
{
contain->onSelling();
}
// Tell it to stop attacking or anything else it is doing
if( obj->getAI() )
obj->getAI()->aiIdle(CMD_FROM_AI);
// if it has parking places, kill anything parked there.
for (BehaviorModule** i = obj->getBehaviorModules(); *i; ++i)
{
ParkingPlaceBehaviorInterface* pp = (*i)->getParkingPlaceBehaviorInterface();
if (pp)
{
pp->killAllParkedUnits();
break;
}
}
// destroy any mines that are owned by this structure, right now.
// unfortunately, structures don't keep list of mines they own, so we must do
// this the hard way :-( [fortunately, this doens't happen very often, so this
// is probably an acceptable, if icky, solution.] (srj)
for (Object* mine = TheGameLogic->getFirstObject(); mine; mine = mine->getNextObject())
{
if (mine->isKindOf(KINDOF_MINE))
{
if (mine->getProducerID() == obj->getID())
{
TheGameLogic->destroyObject(mine);
}
}
}
} // end sellObject