/* ** 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: ToppleUpdate.cpp /////////////////////////////////////////////////////////////////////////// // Author: Graham Smallwood, December 2001 // Desc: Update for something that sits around until hit, then topples and stops /////////////////////////////////////////////////////////////////////////////////////////////////// // INCLUDES /////////////////////////////////////////////////////////////////////////////////////// #include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine #include "Common/ThingTemplate.h" #include "Common/ThingFactory.h" #include "Common/PlayerList.h" #include "Common/Player.h" #include "Common/RandomValue.h" #include "Common/Xfer.h" #include "GameClient/FXList.h" #include "GameLogic/AI.h" #include "GameLogic/AIPathfind.h" #include "GameLogic/Module/ToppleUpdate.h" #include "GameLogic/GameLogic.h" #include "GameLogic/Object.h" #include "GameClient/Drawable.h" #include "GameClient/Module/SwayClientUpdate.h" #include "GameLogic/Module/PhysicsUpdate.h" #include "GameLogic/ScriptEngine.h" #include "GameLogic/Damage.h" //------------------------------------------------------------------------------------------------- // this is our "bounce" limit -- slightly less that 90 degrees, to account for slop. static const Real ANGULAR_LIMIT = PI/2 - PI/64; //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- ToppleUpdateModuleData::ToppleUpdateModuleData() { const Real START_VELOCITY_PERCENT = 0.2f; const Real START_ACCEL_PERCENT = 0.01f; const Real VELOCITY_BOUNCE_PERCENT = 0.3f; // multiply the velocity by this when you bounce m_toppleFX = NULL; m_bounceFX = NULL; m_stumpName.clear(); m_killWhenToppled = true; m_killWhenStartToppled = false; m_killStumpWhenToppled = false; m_toppleLeftOrRightOnly = false; m_reorientToppledRubble = false; m_initialVelocityPercent = START_VELOCITY_PERCENT; m_initialAccelPercent = START_ACCEL_PERCENT; m_bounceVelocityPercent = VELOCITY_BOUNCE_PERCENT; } //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- void ToppleUpdateModuleData::buildFieldParse(MultiIniFieldParse& p) { UpdateModuleData::buildFieldParse(p); static const FieldParse dataFieldParse[] = { { "ToppleFX", INI::parseFXList, NULL, offsetof( ToppleUpdateModuleData, m_toppleFX ) }, { "BounceFX", INI::parseFXList, NULL, offsetof( ToppleUpdateModuleData, m_bounceFX ) }, { "StumpName", INI::parseAsciiString, NULL, offsetof( ToppleUpdateModuleData, m_stumpName ) }, { "KillWhenStartToppling", INI::parseBool, NULL, offsetof( ToppleUpdateModuleData, m_killWhenStartToppled ) }, { "KillWhenFinishedToppling", INI::parseBool, NULL, offsetof( ToppleUpdateModuleData, m_killWhenToppled ) }, { "KillStumpWhenToppled", INI::parseBool, NULL, offsetof( ToppleUpdateModuleData, m_killStumpWhenToppled ) }, { "ToppleLeftOrRightOnly", INI::parseBool, NULL, offsetof( ToppleUpdateModuleData, m_toppleLeftOrRightOnly ) }, { "ReorientToppledRubble", INI::parseBool, NULL, offsetof( ToppleUpdateModuleData, m_reorientToppledRubble ) }, { "InitialVelocityPercent", INI::parsePercentToReal, NULL, offsetof( ToppleUpdateModuleData, m_initialVelocityPercent ) }, { "InitialAccelPercent", INI::parsePercentToReal, NULL, offsetof( ToppleUpdateModuleData, m_initialAccelPercent ) }, { "BounceVelocityPercent", INI::parsePercentToReal, NULL, offsetof( ToppleUpdateModuleData, m_bounceVelocityPercent ) }, { 0, 0, 0, 0 } }; p.add(dataFieldParse); } //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- ToppleUpdate::ToppleUpdate( Thing *thing, const ModuleData* moduleData ) : UpdateModule( thing, moduleData ) { //Added By Sadullah Nader //Initialization(s) inserted m_angleDeltaX = 0.0f; m_doBounceFX = FALSE; m_numAngleDeltaX = 0; // m_angularVelocity = 0; m_angularAccumulation = 0; m_angularAcceleration = 0; m_toppleDirection.x = 0; m_toppleDirection.y = 0; m_toppleDirection.z = 0; m_toppleState = TOPPLE_UPRIGHT; m_options = TOPPLE_OPTIONS_NONE; m_stumpID = INVALID_ID; setWakeFrame(getObject(), UPDATE_SLEEP_FOREVER); } //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- ToppleUpdate::~ToppleUpdate( void ) { } //------------------------------------------------------------------------------------------------- static Real angleClosestTo(Real a1, Real a2, Real desired) { a1 = normalizeAngle(a1); a2 = normalizeAngle(a2); return (fabs(stdAngleDiff(desired, a1)) < fabs(stdAngleDiff(desired, a2))) ? a1 : a2; } //------------------------------------------------------------------------------------------------- ///< Start the toppling process by giving a force vector //------------------------------------------------------------------------------------------------- void ToppleUpdate::applyTopplingForce( const Coord3D* toppleDirection, Real toppleSpeed, UnsignedInt options ) { if (getObject()->isEffectivelyDead()) return; //DEBUG_LOG(("awaking ToppleUpdate %08lx\n",this)); setWakeFrame(getObject(), UPDATE_SLEEP_NONE); const ToppleUpdateModuleData* d = getToppleUpdateModuleData(); if (d->m_killWhenStartToppled) { setWakeFrame(getObject(), UPDATE_SLEEP_FOREVER); getObject()->kill(); return; } m_toppleDirection = *toppleDirection; m_toppleDirection.normalize(); TheScriptEngine->adjustToppleDirection(getObject(), &m_toppleDirection); m_angularVelocity = toppleSpeed * d->m_initialVelocityPercent; m_angularAcceleration = toppleSpeed * d->m_initialAccelPercent; m_toppleState = TOPPLE_FALLING; m_options = options; // tell the drawable to stop swaying Drawable * draw = getObject()->getDrawable(); static NameKeyType nameKeySwayUpdate = NAMEKEY("SwayClientUpdate"); ClientUpdateModule ** clientModules = draw->getClientUpdateModules(); if (clientModules) { while (*clientModules) { if ((*clientModules)->getModuleNameKey() == nameKeySwayUpdate) (*(SwayClientUpdate **)clientModules)->stopSway(); ++clientModules; } } // rotate around the z-axis so that our x-axis is perpendicular to the topple direction. // this is really a trick to ensure that relatively planar things (eg, streetlights) // fall parallel to the ground, so that they don't up sticking thru the ground. // yeah, it assumes the models are constructed appropriately, but is a cheap way // of minimizing the problem. (srj) Real curAngleX = normalizeAngle(getObject()->getOrientation()); Real toppleAngle = normalizeAngle(atan2(m_toppleDirection.y, m_toppleDirection.x)); if (d->m_toppleLeftOrRightOnly) { // it's a fence or such, and can only topple left or right, so pick the closest toppleAngle = angleClosestTo(curAngleX + PI/2, curAngleX - PI/2, toppleAngle); m_toppleDirection.x = Cos(toppleAngle); m_toppleDirection.y = Sin(toppleAngle); // go ahead and remove it from the pathfinder now, rather than waiting for the topple to // finish.... since we might be in a slightly different position when toppled, which can // confuse the pathfinder and not de-obstacle everything correctly TheAI->pathfinder()->removeObjectFromPathfindMap(getObject()); } // desired angle is toppleAngle +/- pi/2, whichever is closer to curangle Real desiredAngleX = angleClosestTo(toppleAngle + PI/2, toppleAngle - PI/2, curAngleX); m_numAngleDeltaX = REAL_TO_INT_FLOOR(ANGULAR_LIMIT / (m_angularVelocity * 2)); if (m_numAngleDeltaX < 1) m_numAngleDeltaX = 1; m_angleDeltaX = (desiredAngleX - curAngleX) / m_numAngleDeltaX; getObject()->getDrawable()->setModelConditionState(MODELCONDITION_TOPPLED); FXList::doFXObj(d->m_toppleFX, getObject()); // if this is a tree, create a stump if (!d->m_stumpName.isEmpty()) { const ThingTemplate* ttn = TheThingFactory->findTemplate(d->m_stumpName); Object *stump = TheThingFactory->newObject( ttn, NULL ); if (stump) { stump->setPosition( getObject()->getPosition() ); stump->setOrientation( getObject()->getOrientation() ); m_stumpID = stump->getID(); // if we are "burned", then we will burn our stump const Drawable* draw = getObject()->getDrawable(); if( draw ) { if( draw->getModelConditionFlags().test( MODELCONDITION_BURNED ) == TRUE ) { Drawable* stumpDraw = stump->getDrawable(); if( stumpDraw ) stumpDraw->setModelConditionState( MODELCONDITION_BURNED ); } } } } } //------------------------------------------------------------------------------------------------- ///< Ask if this module is able to be toppled //------------------------------------------------------------------------------------------------- Bool ToppleUpdate::isAbleToBeToppled() const { return m_toppleState == TOPPLE_UPRIGHT; } //------------------------------------------------------------------------------------------------- static void deathByToppling(Object* obj) { // use a special "topppling" damage type here so that // toppled stuff can have different damage/die modules // for toppled-death vs other-death DamageInfo damageInfo; damageInfo.in.m_damageType = DAMAGE_UNRESISTABLE; damageInfo.in.m_deathType = DEATH_TOPPLED; damageInfo.in.m_sourceID = INVALID_ID; damageInfo.in.m_amount = HUGE_DAMAGE_AMOUNT; obj->attemptDamage(&damageInfo); } //------------------------------------------------------------------------------------------------- ///< Keep track of rotational fall distance, bounce and/or stop when needed. //------------------------------------------------------------------------------------------------- UpdateSleepTime ToppleUpdate::update() { //DEBUG_LOG(("updating ToppleUpdate %08lx\n",this)); DEBUG_ASSERTCRASH(m_toppleState != TOPPLE_UPRIGHT, ("hmm, we should be sleeping here")); if ( (m_toppleState == TOPPLE_UPRIGHT) || (m_toppleState == TOPPLE_DOWN) ) return UPDATE_SLEEP_FOREVER; const ToppleUpdateModuleData* d = getToppleUpdateModuleData(); const Real VELOCITY_BOUNCE_LIMIT = 0.01f; // if the velocity after a bounce will be this or lower, just stop at zero const Real VELOCITY_BOUNCE_SOUND_LIMIT = 0.03f; // and if this low, then skip the bounce sound Object* obj = getObject(); if (m_numAngleDeltaX) { Matrix3D xfrm = *obj->getTransformMatrix(); xfrm.In_Place_Pre_Rotate_Z(m_angleDeltaX); obj->setTransformMatrix(&xfrm); --m_numAngleDeltaX; } Real curVelToUse = m_angularVelocity; if (m_angularAccumulation + curVelToUse > ANGULAR_LIMIT) curVelToUse = ANGULAR_LIMIT - m_angularAccumulation; Matrix3D xfrm = *obj->getTransformMatrix(); xfrm.In_Place_Pre_Rotate_X(-curVelToUse * m_toppleDirection.y); xfrm.In_Place_Pre_Rotate_Y(curVelToUse * m_toppleDirection.x); obj->setTransformMatrix(&xfrm); m_angularAccumulation += curVelToUse; if ((m_angularAccumulation >= ANGULAR_LIMIT) && (m_angularVelocity > 0)) { // Hit so either bounce or stop if too little remaining velocity. m_angularVelocity *= -d->m_bounceVelocityPercent; if( BitTest( m_options, TOPPLE_OPTIONS_NO_BOUNCE ) == TRUE || fabs(m_angularVelocity) < VELOCITY_BOUNCE_LIMIT ) { // too slow, just stop m_angularVelocity = 0; m_toppleState = TOPPLE_DOWN; if (d->m_killWhenToppled) { deathByToppling(obj); if (d->m_reorientToppledRubble) { // we have a separate rubble state that needs to be upright, and centered // on the new "center" pos... Vector3 pos; pos.X = 0; pos.Y = 0; pos.Z = obj->getGeometryInfo().getMaxHeightAbovePosition(); Matrix3D::Transform_Vector(*obj->getTransformMatrix(), pos, &pos); Coord3D tmp; tmp.x = pos.X; tmp.y = pos.Y; tmp.z = pos.Z; obj->setPosition(&tmp); // this relies on the fact that setOrientation always forces us straight up in the Z axis! obj->setOrientation(obj->getOrientation()); } } // if kill when toppled if (d->m_killStumpWhenToppled) { Object* stump = TheGameLogic->findObjectByID(m_stumpID); if (stump) { deathByToppling(stump); } } } else if( fabs(m_angularVelocity) >= VELOCITY_BOUNCE_SOUND_LIMIT ) { // fast enough bounce to warrant the bounce fx if( BitTest( m_options, TOPPLE_OPTIONS_NO_FX ) == FALSE ) FXList::doFXObj(d->m_bounceFX, obj); } } else { m_angularVelocity += m_angularAcceleration; } Drawable *draw=obj->getDrawable(); if (draw) draw->setShadowsEnabled(false); return UPDATE_SLEEP_NONE; } //------------------------------------------------------------------------------------------------- /** Do the collision */ //------------------------------------------------------------------------------------------------- void ToppleUpdate::onCollide( Object *other, const Coord3D *loc, const Coord3D *normal ) { // Note that other == null means "collide with ground" // if (other == NULL) return; //@todo JohnA -- Should you get around to adding trees to avoidance pathfinding, then you'll //want to change this code: //if( other->getCrusherLevel() > getObject()->getCrushableLevel() ) //<----proper tree method if( other->getCrusherLevel() > 1 ) { // Give a vector with direction to thing and my speed. Coord3D toppleVector = *getObject()->getPosition(); toppleVector.x -= other->getPosition()->x; toppleVector.y -= other->getPosition()->y; toppleVector.z = 0; Coord3D vel; PhysicsBehavior* phys = other->getPhysics(); if (phys) vel = *phys->getVelocity(); else vel.zero(); getObject()->topple( &toppleVector, vel.length(), TOPPLE_OPTIONS_NONE ); } } // ------------------------------------------------------------------------------------------------ /** CRC */ // ------------------------------------------------------------------------------------------------ void ToppleUpdate::crc( Xfer *xfer ) { // extend base class UpdateModule::crc( xfer ); } // end crc // ------------------------------------------------------------------------------------------------ /** Xfer method * Version Info: * 1: Initial version */ // ------------------------------------------------------------------------------------------------ void ToppleUpdate::xfer( Xfer *xfer ) { // version XferVersion currentVersion = 1; XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); // extend base class UpdateModule::xfer( xfer ); // angular velocity xfer->xferReal( &m_angularVelocity ); // angular acceleration xfer->xferReal( &m_angularAcceleration ); // topple direction xfer->xferCoord3D( &m_toppleDirection ); // topple state xfer->xferUser( &m_toppleState, sizeof( ToppleState ) ); // angluar accumulation xfer->xferReal( &m_angularAccumulation ); // angle delta X xfer->xferReal( &m_angleDeltaX ); // num angle delta X xfer->xferInt( &m_numAngleDeltaX ); // do bounce FX xfer->xferBool( &m_doBounceFX ); // options xfer->xferUnsignedInt( &m_options ); // stump id xfer->xferObjectID( &m_stumpID ); } // end xfer // ------------------------------------------------------------------------------------------------ /** Load post process */ // ------------------------------------------------------------------------------------------------ void ToppleUpdate::loadPostProcess( void ) { // extend base class UpdateModule::loadPostProcess(); } // end loadPostProcess