/*
** Command & Conquer Generals(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: MinefieldBehavior.cpp //////////////////////////////////////////////////////////////////
// Author: Steven Johnson, June 2002
// Desc: Minefield behavior
///////////////////////////////////////////////////////////////////////////////////////////////////
// INCLUDES ///////////////////////////////////////////////////////////////////////////////////////
#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine
#define DEFINE_RELATIONSHIP_NAMES
#include "Common/GameState.h"
#include "Common/RandomValue.h"
#include "Common/Xfer.h"
#include "GameClient/Drawable.h"
#include "GameClient/InGameUI.h"
#include "GameLogic/GameLogic.h"
#include "GameLogic/Object.h"
#include "GameLogic/Module/AIUpdate.h"
#include "GameLogic/Module/BodyModule.h"
#include "GameLogic/Module/PhysicsUpdate.h"
#include "GameLogic/Module/MinefieldBehavior.h"
#include "GameLogic/Module/AutoHealBehavior.h"
#include "GameLogic/Weapon.h"
#ifdef _INTERNAL
// for occasional debugging...
//#pragma optimize("", off)
//#pragma MESSAGE("************************************** WARNING, optimization disabled for debugging purposes")
#endif
// detonation never puts our health below this, since we probably auto-regen
const Real MIN_HEALTH = 0.1f;
//-------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
MinefieldBehaviorModuleData::MinefieldBehaviorModuleData()
{
m_detonationWeapon = NULL;
m_detonatedBy = (1 << ENEMIES) | (1 << NEUTRAL);
m_stopsRegenAfterCreatorDies = true;
m_regenerates = false;
m_workersDetonate = false;
m_creatorDeathCheckRate = LOGICFRAMES_PER_SECOND;
m_scootFromStartingPointTime = 0;
m_repeatDetonateMoveThresh = 1.0f;
m_numVirtualMines = 1;
m_healthPercentToDrainPerSecond = 0.0f;
}
//-------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
/*static*/ void MinefieldBehaviorModuleData::buildFieldParse( MultiIniFieldParse &p )
{
UpdateModuleData::buildFieldParse( p );
static const FieldParse dataFieldParse[] =
{
{ "DetonationWeapon", INI::parseWeaponTemplate, NULL, offsetof( MinefieldBehaviorModuleData, m_detonationWeapon ) },
{ "DetonatedBy", INI::parseBitString32, TheRelationshipNames, offsetof( MinefieldBehaviorModuleData, m_detonatedBy ) },
{ "StopsRegenAfterCreatorDies", INI::parseBool, NULL, offsetof( MinefieldBehaviorModuleData, m_stopsRegenAfterCreatorDies ) },
{ "Regenerates", INI::parseBool, NULL, offsetof( MinefieldBehaviorModuleData, m_regenerates ) },
{ "WorkersDetonate", INI::parseBool, NULL, offsetof( MinefieldBehaviorModuleData, m_workersDetonate ) },
{ "CreatorDeathCheckRate", INI::parseDurationUnsignedInt, NULL, offsetof( MinefieldBehaviorModuleData, m_creatorDeathCheckRate ) },
{ "ScootFromStartingPointTime", INI::parseDurationUnsignedInt, NULL, offsetof( MinefieldBehaviorModuleData, m_scootFromStartingPointTime ) },
{ "NumVirtualMines", INI::parseUnsignedInt, NULL, offsetof( MinefieldBehaviorModuleData, m_numVirtualMines ) },
{ "RepeatDetonateMoveThresh", INI::parseReal, NULL, offsetof( MinefieldBehaviorModuleData, m_repeatDetonateMoveThresh ) },
{ "DegenPercentPerSecondAfterCreatorDies", INI::parsePercentToReal, NULL, offsetof( MinefieldBehaviorModuleData, m_healthPercentToDrainPerSecond ) },
{ 0, 0, 0, 0 }
};
p.add( dataFieldParse );
}
///////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////////
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
MinefieldBehavior::MinefieldBehavior( Thing *thing, const ModuleData* moduleData )
: UpdateModule( thing, moduleData )
{
const MinefieldBehaviorModuleData* d = getMinefieldBehaviorModuleData();
m_nextDeathCheckFrame = 0;
m_scootFramesLeft = 0;
m_scootVel.zero();
m_scootAccel.zero();
m_detonators.clear();
m_ignoreDamage = false;
m_regenerates = d->m_regenerates;
m_draining = false;
m_virtualMinesRemaining = d->m_numVirtualMines;
for (Int i = 0; i < MAX_IMMUNITY; ++i)
{
m_immunes[i].id = INVALID_ID;
m_immunes[i].collideTime = 0;
}
// start off awake, and we will calcSleepTime from here on
setWakeFrame( getObject(), UPDATE_SLEEP_NONE );
// mines aren't auto-acquirable
getObject()->setStatus(OBJECT_STATUS_NO_ATTACK_FROM_AI);
}
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
MinefieldBehavior::~MinefieldBehavior()
{
}
// ------------------------------------------------------------------------------------------------
// ------------------------------------------------------------------------------------------------
UpdateSleepTime MinefieldBehavior::calcSleepTime()
{
const MinefieldBehaviorModuleData* d = getMinefieldBehaviorModuleData();
// if we're draining we have to update every frame
if (m_draining)
return UPDATE_SLEEP_NONE;
// if we're scooting we need to update every frame
if( m_scootFramesLeft > 0 )
return UPDATE_SLEEP_NONE;
// if there is anybody in our immulity monitoring we need to update every frame
for( Int i = 0; i < MAX_IMMUNITY; ++i )
if( m_immunes[ i ].id != INVALID_ID )
return UPDATE_SLEEP_NONE;
UnsignedInt sleepTime = FOREVER;
UnsignedInt now = TheGameLogic->getFrame();
//
// sleep until the next death check frame we already have figured outif we care
// about it (that is, when our creator dies)
//
if (m_regenerates && d->m_stopsRegenAfterCreatorDies)
sleepTime = min( sleepTime, m_nextDeathCheckFrame - now );
// if we don't want to sleep forever, prevent 0 frame sleeps
if( sleepTime == 0 )
sleepTime = 1;
// sleep forever
return UPDATE_SLEEP( sleepTime );
}
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
UpdateSleepTime MinefieldBehavior::update()
{
Object* obj = getObject();
const MinefieldBehaviorModuleData* d = getMinefieldBehaviorModuleData();
UnsignedInt now = TheGameLogic->getFrame();
if (m_scootFramesLeft > 0)
{
Coord3D pt = *obj->getPosition();
m_scootVel.x += m_scootAccel.x;
m_scootVel.y += m_scootAccel.y;
m_scootVel.z += m_scootAccel.z;
pt.x += m_scootVel.x;
pt.y += m_scootVel.y;
pt.z += m_scootVel.z;
// srj sez: scooting mines always go on the highest layer.
Coord3D tmp = pt;
tmp.z = 99999.0f;
PathfindLayerEnum newLayer = TheTerrainLogic->getHighestLayerForDestination(&tmp);
obj->setLayer(newLayer);
Real ground = TheTerrainLogic->getLayerHeight( pt.x, pt.y, newLayer );
if (newLayer != LAYER_GROUND)
{
// ensure we are slightly above the bridge, to account for fudge & sloppy art
const Real FUDGE = 1.0f;
ground += FUDGE;
}
if (pt.z < ground || m_scootFramesLeft <= 1)
pt.z = ground;
obj->setPosition(&pt);
--m_scootFramesLeft;
}
// check for expired immunities.
for (Int i = 0; i < MAX_IMMUNITY; ++i)
{
if (m_immunes[i].id == INVALID_ID)
continue;
if (TheGameLogic->findObjectByID(m_immunes[i].id) == NULL ||
now > m_immunes[i].collideTime + 2)
{
//DEBUG_LOG(("expiring an immunity %d\n",m_immunes[i].id));
m_immunes[i].id = INVALID_ID; // he's dead, jim.
m_immunes[i].collideTime = 0;
}
}
if (now >= m_nextDeathCheckFrame)
{
// check to see if there is an enemy building on me... since enemy buildings can be build on top of me
// check to see if the building that made me is gone, and whether therefore, I should go
if (m_regenerates && d->m_stopsRegenAfterCreatorDies)
{
m_nextDeathCheckFrame = now + d->m_creatorDeathCheckRate;
ObjectID producerID = getObject()->getProducerID();
if (producerID != INVALID_ID)
{
Object* producer = TheGameLogic->findObjectByID(producerID);
if (producer == NULL || producer->isEffectivelyDead())
{
m_regenerates = false;
m_draining = true;
static const NameKeyType key_AutoHealBehavior = NAMEKEY("AutoHealBehavior");
AutoHealBehavior* ahb = (AutoHealBehavior*)obj->findUpdateModule( key_AutoHealBehavior );
if (ahb)
ahb->stopHealing();
}
}
}
}
if (m_draining)
{
DamageInfo damageInfo;
damageInfo.in.m_amount = (obj->getBodyModule()->getMaxHealth() * d->m_healthPercentToDrainPerSecond) / LOGICFRAMES_PER_SECOND;
damageInfo.in.m_sourceID = obj->getID();
damageInfo.in.m_damageType = DAMAGE_UNRESISTABLE;
damageInfo.in.m_deathType = DEATH_NORMAL;
obj->attemptDamage( &damageInfo );
}
return calcSleepTime();
}
// ------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
void MinefieldBehavior::detonateOnce(const Coord3D& position)
{
const MinefieldBehaviorModuleData* d = getMinefieldBehaviorModuleData();
if (d->m_detonationWeapon)
{
Object* obj = getObject();
TheWeaponStore->createAndFireTempWeapon(d->m_detonationWeapon, obj, &position);
}
if (m_virtualMinesRemaining > 0)
--m_virtualMinesRemaining;
if (!m_regenerates && m_virtualMinesRemaining == 0)
{
TheGameLogic->destroyObject(getObject());
}
else
{
Real percent = (Real)m_virtualMinesRemaining / (Real)d->m_numVirtualMines;
BodyModuleInterface* body = getObject()->getBodyModule();
Real health = body->getHealth();
Real desired = percent * body->getMaxHealth();
if (desired < MIN_HEALTH)
desired = MIN_HEALTH;
Real amount = health - desired;
if (amount > 0.0f)
{
m_ignoreDamage = true;
//body->internalChangeHealth(desired - health);
//can't use this, AutoHeal won't work unless we go thru normal damage stuff
DamageInfo extraDamageInfo;
extraDamageInfo.in.m_damageType = DAMAGE_UNRESISTABLE;
extraDamageInfo.in.m_deathType = DEATH_NONE;
extraDamageInfo.in.m_sourceID = getObject()->getID();
extraDamageInfo.in.m_amount = amount;
getObject()->attemptDamage(&extraDamageInfo);
m_ignoreDamage = false;
}
}
if (m_virtualMinesRemaining == 0)
{
getObject()->setModelConditionState(MODELCONDITION_RUBBLE);
getObject()->setStatus(OBJECT_STATUS_MASKED);
}
else
{
getObject()->clearModelConditionState(MODELCONDITION_RUBBLE);
getObject()->clearStatus(OBJECT_STATUS_MASKED);
}
}
//-----------------------------------------------------------------------------
static Real calcDistSquared(const Coord3D& a, const Coord3D& b)
{
return sqr(a.x - b.x) + sqr(a.y - b.y) + sqr(a.z - b.z);
}
// ------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
void MinefieldBehavior::onCollide( Object *other, const Coord3D *loc, const Coord3D *normal )
{
if (other == NULL || other->isEffectivelyDead())
return;
if (m_virtualMinesRemaining == 0)
return;
Object* obj = getObject();
const MinefieldBehaviorModuleData* d = getMinefieldBehaviorModuleData();
UnsignedInt now = TheGameLogic->getFrame();
// is this guy in our immune list?
// NOTE NOTE NOTE, must always do this check FIRST so that 'collideTime' is updated...
for (Int i = 0; i < MAX_IMMUNITY; ++i)
{
if (m_immunes[i].id == other->getID())
{
//DEBUG_LOG(("ignoring due to immunity %d\n",m_immunes[i].id));
m_immunes[i].collideTime = now;
return;
}
}
if (!d->m_workersDetonate)
{
// infantry+dozer=worker.
if (other->isKindOf(KINDOF_INFANTRY) && other->isKindOf(KINDOF_DOZER))
return;
}
Int requiredMask = 0;
Relationship r = obj->getRelationship(other);
if (r == ALLIES) requiredMask = (1 << ALLIES);
else if (r == ENEMIES) requiredMask = (1 << ENEMIES);
else if (r == NEUTRAL) requiredMask = (1 << NEUTRAL);
if ((d->m_detonatedBy & requiredMask) == 0)
return;
// are we active?
if (m_scootFramesLeft > 0)
return;
// things that are in the process of clearing mines are immune to mine detonation,
// even if we aren't the specific mine they are trying to clear. (however, they must
// have a real mine they area trying to clear... it's possible they could be trying to
// clear a position where there is no mine, in which case we grant them no immunity, muwahahaha)
AIUpdateInterface* otherAI = other->getAI();
if (otherAI && otherAI->isClearingMines() && otherAI->getGoalObject() != NULL)
{
// mine-clearers are granted immunity to us for as long as they continuously
// collide, even if no longer clearing mines. (this prevents the problem
// of a guy who touches two close-together mines while clearing, then puts up his
// detector and is blown to smithereens by the other one.)
for (Int i = 0; i < MAX_IMMUNITY; ++i)
{
if (m_immunes[i].id == INVALID_ID || m_immunes[i].id == other->getID())
{
//DEBUG_LOG(("add/update immunity %d\n",m_immunes[i].id));
m_immunes[i].id = other->getID();
m_immunes[i].collideTime = now;
// wake up
setWakeFrame( obj, calcSleepTime() );
break;
}
}
return;
}
// if we detonated another one nearby, we have to move a little bit to detonate another one.
Bool found = false;
for (std::vector::iterator it = m_detonators.begin(); it != m_detonators.end(); ++it)
{
if (other->getID() == it->id)
{
found = TRUE;
Real distSqr = calcDistSquared(*other->getPosition(), it->where);
if (distSqr <= sqr(d->m_repeatDetonateMoveThresh))
{
// too close. punt for now.
return;
}
else
{
// far enough. update the loc, then break out and blow up.
it->where = *other->getPosition();
break;
}
}
}
if (!found)
{
// add him to the list.
DetonatorInfo detInfo;
detInfo.id = other->getID();
detInfo.where = *other->getPosition();
m_detonators.push_back(detInfo);
}
Coord3D detPt = *other->getPosition();
obj->getGeometryInfo().clipPointToFootprint(*obj->getPosition(), detPt);
detonateOnce(detPt);
}
// ------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
void MinefieldBehavior::onDamage( DamageInfo *damageInfo )
{
if (m_ignoreDamage)
return;
const MinefieldBehaviorModuleData* d = getMinefieldBehaviorModuleData();
// detonate as many times as neccessary for our virtual mine count to match our health
BodyModuleInterface* body = getObject()->getBodyModule();
for (;;)
{
Real virtualMinesExpectedF = ((Real)d->m_numVirtualMines * body->getHealth() / body->getMaxHealth());
Int virtualMinesExpected =
damageInfo->in.m_damageType == DAMAGE_HEALING ?
REAL_TO_INT_FLOOR(virtualMinesExpectedF) :
REAL_TO_INT_CEIL(virtualMinesExpectedF);
if (virtualMinesExpected > d->m_numVirtualMines)
virtualMinesExpected = d->m_numVirtualMines;
if (m_virtualMinesRemaining < virtualMinesExpected)
{
m_virtualMinesRemaining = virtualMinesExpected;
}
else if (m_virtualMinesRemaining > virtualMinesExpected)
{
if (m_draining &&
damageInfo->in.m_sourceID == getObject()->getID() &&
damageInfo->in.m_damageType == DAMAGE_UNRESISTABLE)
{
// don't detonate.... just ditch a mine
--m_virtualMinesRemaining;
}
else
{
detonateOnce(*getObject()->getPosition());
}
}
else
{
break;
}
}
if (m_virtualMinesRemaining == 0)
{
// oops, if someone did weapon damage they may have nuked our health to zero,
// which would be bad if we regen. prevent this. (srj)
if (m_regenerates && body->getHealth() < MIN_HEALTH)
{
body->internalChangeHealth(MIN_HEALTH - body->getHealth());
}
getObject()->setModelConditionState(MODELCONDITION_RUBBLE);
getObject()->setStatus(OBJECT_STATUS_MASKED);
}
else
{
getObject()->clearModelConditionState(MODELCONDITION_RUBBLE);
getObject()->clearStatus(OBJECT_STATUS_MASKED);
}
}
// ------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
void MinefieldBehavior::onHealing( DamageInfo *damageInfo )
{
onDamage(damageInfo);
}
// ------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
void MinefieldBehavior::onDie( const DamageInfo *damageInfo )
{
TheGameLogic->destroyObject(getObject());
}
// ------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
void MinefieldBehavior::disarm()
{
if (!m_regenerates)
{
TheGameLogic->destroyObject(getObject());
return;
}
// detonation never puts our health below this, since we probably auto-regen
const Real MIN_HEALTH = 0.1f;
BodyModuleInterface* body = getObject()->getBodyModule();
Real desired = MIN_HEALTH;
Real amount = body->getHealth() - desired;
m_ignoreDamage = true;
//body->internalChangeHealth(desired - health);
//can't use this, AutoHeal won't work unless we go thru normal damage stuff
DamageInfo extraDamageInfo;
extraDamageInfo.in.m_damageType = DAMAGE_UNRESISTABLE;
extraDamageInfo.in.m_deathType = DEATH_NONE;
extraDamageInfo.in.m_sourceID = getObject()->getID();
extraDamageInfo.in.m_amount = amount;
getObject()->attemptDamage(&extraDamageInfo);
m_ignoreDamage = false;
m_virtualMinesRemaining = 0;
getObject()->setModelConditionState(MODELCONDITION_RUBBLE);
getObject()->setStatus(OBJECT_STATUS_MASKED);
}
// ------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
void MinefieldBehavior::setScootParms(const Coord3D& start, const Coord3D& end)
{
Object* obj = getObject();
const MinefieldBehaviorModuleData* d = getMinefieldBehaviorModuleData();
UnsignedInt scootFromStartingPointTime = d->m_scootFromStartingPointTime;
Coord3D endOnGround = end;
endOnGround.z = TheTerrainLogic->getGroundHeight( endOnGround.x, endOnGround.y );
if (start.z > endOnGround.z)
{
// figure out how long it will take to fall, and replace scoot time with that
UnsignedInt fallingTime = REAL_TO_INT_CEIL(sqrtf(2.0f * (start.z - endOnGround.z) / fabs(TheGlobalData->m_gravity)));
// we can scoot after we land, but don't want to stop scooting before we land
if (scootFromStartingPointTime < fallingTime)
scootFromStartingPointTime = fallingTime;
}
if (scootFromStartingPointTime == 0)
{
obj->setPosition(&endOnGround);
m_scootFramesLeft = 0;
}
else
{
// x = x0 + vt + 0.5at^2
// thus 2(dx - vt)/t^2 = a
Real dx = endOnGround.x - start.x;
Real dy = endOnGround.y - start.y;
Real dz = endOnGround.z - start.z;
Real dist = sqrt(sqr(dx) + sqr(dy));
if (dist <= 0.1f && fabs(dz) <= 0.1f)
{
obj->setPosition(&endOnGround);
m_scootFramesLeft = 0;
}
else
{
Real t = (Real)scootFromStartingPointTime;
Real scootFromStartingPointSpeed = dist / t;
Real accelMag = fabs(2.0f * (dist - scootFromStartingPointSpeed*t)/sqr(t));
Real dxNorm = (dist <= 0.1f) ? 0.0f : (dx / dist);
Real dyNorm = (dist <= 0.1f) ? 0.0f : (dy / dist);
m_scootVel.x = dxNorm * scootFromStartingPointSpeed;
m_scootVel.y = dyNorm * scootFromStartingPointSpeed;
m_scootAccel.x = -dxNorm * accelMag;
m_scootAccel.y = -dyNorm * accelMag;
m_scootAccel.z = TheGlobalData->m_gravity;
obj->setPosition(&start);
m_scootFramesLeft = scootFromStartingPointTime;
// we need to wake ourselves up because we could be lying here sleeping forever
setWakeFrame( obj, calcSleepTime() );
}
}
}
// ------------------------------------------------------------------------------------------------
/** CRC */
// ------------------------------------------------------------------------------------------------
void MinefieldBehavior::crc( Xfer *xfer )
{
// extend base class
UpdateModule::crc( xfer );
} // end crc
// ------------------------------------------------------------------------------------------------
/** Xfer method
* Version Info:
* 1: Initial version */
// ------------------------------------------------------------------------------------------------
void MinefieldBehavior::xfer( Xfer *xfer )
{
// version
XferVersion currentVersion = 1;
XferVersion version = currentVersion;
xfer->xferVersion( &version, currentVersion );
// extend base class
UpdateModule::xfer( xfer );
// mines remaining
/// @todo srj -- ensure health, appearance, etc are correct for save/reload! post-MP!
xfer->xferUnsignedInt( &m_virtualMinesRemaining );
// next death check frame
xfer->xferUnsignedInt( &m_nextDeathCheckFrame );
// scoot frames left
xfer->xferUnsignedInt( &m_scootFramesLeft );
// scoot velocity
xfer->xferCoord3D( &m_scootVel );
// scoot acceleration
xfer->xferCoord3D( &m_scootAccel );
xfer->xferBool( &m_ignoreDamage );
xfer->xferBool( &m_regenerates );
xfer->xferBool( &m_draining );
// immunities
UnsignedByte maxImmunity = MAX_IMMUNITY;
xfer->xferUnsignedByte( &maxImmunity );
if( maxImmunity != MAX_IMMUNITY )
{
DEBUG_CRASH(( "MinefieldBehavior::xfer - MAX_IMMUNITY has changed size, you must version this code and then you can remove this error message\n" ));
throw SC_INVALID_DATA;
} // end if
for( UnsignedByte i = 0; i < maxImmunity; ++i )
{
// object id
xfer->xferObjectID( &m_immunes[ i ].id );
// collide time
xfer->xferUnsignedInt( &m_immunes[ i ].collideTime );
} // end for, i
if( xfer->getXferMode() == XFER_LOAD )
m_detonators.clear();
} // end xfer
// ------------------------------------------------------------------------------------------------
/** Load post process */
// ------------------------------------------------------------------------------------------------
void MinefieldBehavior::loadPostProcess( void )
{
// extend base class
UpdateModule::loadPostProcess();
} // end loadPostProcess