/* ** 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: NeutronMissileUpdate.cpp // Author: Michael S. Booth, December 2001 // Desc: Implementation of missile behavior #include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine #include "Common/GameState.h" #include "Common/Thing.h" #include "Common/ThingTemplate.h" #include "Common/RandomValue.h" #include "Common/Xfer.h" #include "GameLogic/ExperienceTracker.h" #include "GameLogic/GameLogic.h" #include "GameLogic/TerrainLogic.h" #include "GameLogic/Object.h" #include "GameLogic/PartitionManager.h" #include "GameLogic/Module/NeutronMissileUpdate.h" #include "GameLogic/Module/AIUpdate.h" #include "GameLogic/Module/PhysicsUpdate.h" #include "GameClient/Drawable.h" #include "GameClient/FXList.h" #include "GameClient/InGameUI.h" #include "GameClient/ParticleSys.h" static const Real STRAIGHT_DOWN_SLOW_FACTOR = 0.5f; //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- //----------------------------------------------------------------------------- NeutronMissileUpdateModuleData::NeutronMissileUpdateModuleData() { m_initialDist = 0.0f; m_maxTurnRate = 999.0f; m_forwardDamping = 0; m_relativeSpeed = 1.0f; m_targetFromDirectlyAbove = 0.0f; m_ignitionFX = NULL; m_launchFX = NULL; m_specialAccelFactor = 1.0f; m_specialSpeedTime = 0; m_specialSpeedHeight = 0.0f; m_specialJitterDistance = 0.0f; m_deliveryDecalRadius = 0; } //----------------------------------------------------------------------------- void NeutronMissileUpdateModuleData::buildFieldParse(MultiIniFieldParse& p) { UpdateModuleData::buildFieldParse(p); static const FieldParse dataFieldParse[] = { { "DistanceToTravelBeforeTurning", INI::parseReal, NULL, offsetof( NeutronMissileUpdateModuleData, m_initialDist ) }, { "MaxTurnRate", INI::parseAngularVelocityReal, NULL, offsetof( NeutronMissileUpdateModuleData, m_maxTurnRate ) }, { "ForwardDamping", INI::parseReal, NULL, offsetof( NeutronMissileUpdateModuleData, m_forwardDamping ) }, { "RelativeSpeed", INI::parseReal, NULL, offsetof( NeutronMissileUpdateModuleData, m_relativeSpeed ) }, { "TargetFromDirectlyAbove", INI::parseReal, NULL, offsetof( NeutronMissileUpdateModuleData, m_targetFromDirectlyAbove ) }, { "LaunchFX", INI::parseFXList, NULL, offsetof( NeutronMissileUpdateModuleData, m_launchFX ) }, { "SpecialSpeedTime", INI::parseDurationUnsignedInt, NULL, offsetof( NeutronMissileUpdateModuleData, m_specialSpeedTime ) }, { "SpecialSpeedHeight", INI::parseReal, NULL, offsetof( NeutronMissileUpdateModuleData, m_specialSpeedHeight ) }, { "SpecialAccelFactor", INI::parseReal, NULL, offsetof( NeutronMissileUpdateModuleData, m_specialAccelFactor ) }, { "SpecialJitterDistance", INI::parseReal, NULL, offsetof( NeutronMissileUpdateModuleData, m_specialJitterDistance ) }, { "IgnitionFX", INI::parseFXList, NULL, offsetof( NeutronMissileUpdateModuleData, m_ignitionFX ) }, { "DeliveryDecal", RadiusDecalTemplate::parseRadiusDecalTemplate, NULL, offsetof( NeutronMissileUpdateModuleData, m_deliveryDecalTemplate ) }, { "DeliveryDecalRadius", INI::parseReal, NULL, offsetof( NeutronMissileUpdateModuleData, m_deliveryDecalRadius ) }, { 0, 0, 0, 0 } }; p.add(dataFieldParse); } //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- NeutronMissileUpdate::NeutronMissileUpdate( Thing *thing, const ModuleData* moduleData ) : UpdateModule( thing, moduleData ) { const NeutronMissileUpdateModuleData* d = getNeutronMissileUpdateModuleData(); m_noTurnDistLeft = d->m_initialDist; m_reachedIntermediatePos = true; m_targetPos.zero(); m_intermedPos.zero(); m_accel.zero(); m_vel.zero(); m_state = PRELAUNCH; m_stateTimestamp = TheGameLogic->getFrame(); m_isArmed = false; m_isLaunched = false; m_launcherID = INVALID_ID; m_attach_wslot = PRIMARY_WEAPON; m_attach_specificBarrelToUse = 0; m_heightAtLaunch = 0; m_frameAtLaunch = 0; m_exhaustSysTmpl = NULL; } //------------------------------------------------------------------------------------------------- NeutronMissileUpdate::~NeutronMissileUpdate( void ) { m_deliveryDecal.clear(); } //------------------------------------------------------------------------------------------------- void NeutronMissileUpdate::onDelete( void ) { } //------------------------------------------------------------------------------------------------- // Prepares the missile for launch via proper weapon-system channels. //------------------------------------------------------------------------------------------------- void NeutronMissileUpdate::projectileLaunchAtObjectOrPosition(const Object *victim, const Coord3D* victimPos, const Object *launcher, WeaponSlotType wslot, Int specificBarrelToUse, const WeaponTemplate* detWeap, const ParticleSystemTemplate* exhaustSysOverride) { DEBUG_ASSERTCRASH(specificBarrelToUse>=0, ("specificBarrelToUse must now be explicit")); m_launcherID = launcher ? launcher->getID() : INVALID_ID; m_attach_wslot = wslot; m_attach_specificBarrelToUse = specificBarrelToUse; m_vel.zero(); if (launcher) { const PhysicsBehavior *phys = launcher->getPhysics(); if (phys) m_vel = *phys->getVelocity(); } projectileFireAtObjectOrPosition( victim, victimPos, detWeap, exhaustSysOverride ); } //------------------------------------------------------------------------------------------------- // The actual firing of the missile once setup. //------------------------------------------------------------------------------------------------- void NeutronMissileUpdate::projectileFireAtObjectOrPosition( const Object *victim, const Coord3D *victimPos, const WeaponTemplate *detWeap, const ParticleSystemTemplate* exhaustSysOverride ) { m_exhaustSysTmpl = exhaustSysOverride; m_state = LAUNCH; m_stateTimestamp = TheGameLogic->getFrame(); if( victim ) { // CalcTarget will add half the target's height. But in this case, we are aiming at the ground // and need to stay aiming at the ground. m_targetPos = *victim->getPosition(); m_intermedPos = m_targetPos; m_intermedPos.z += getNeutronMissileUpdateModuleData()->m_targetFromDirectlyAbove; } else { // Otherwise, we are just a Coord shot. m_targetPos = *victimPos; m_intermedPos = m_targetPos; m_intermedPos.z += getNeutronMissileUpdateModuleData()->m_targetFromDirectlyAbove; } m_deliveryDecal.clear(); getNeutronMissileUpdateModuleData()->m_deliveryDecalTemplate.createRadiusDecal(m_targetPos, getNeutronMissileUpdateModuleData()->m_deliveryDecalRadius, getObject()->getControllingPlayer(), m_deliveryDecal); } //------------------------------------------------------------------------------------------------- /** * Implement LAUNCH state */ void NeutronMissileUpdate::doLaunch( void ) { if (!m_isLaunched) { Object *launcher = TheGameLogic->findObjectByID( m_launcherID ); // if our launch vehicle is gone, destroy ourselves if (launcher == NULL) { m_launcherID = INVALID_ID; TheGameLogic->destroyObject( getObject() ); return; } Matrix3D attachTransform; if (!launcher->getDrawable() || !launcher->getDrawable()->getProjectileLaunchOffset(m_attach_wslot, m_attach_specificBarrelToUse, &attachTransform, TURRET_INVALID, NULL)) { DEBUG_CRASH(("ProjectileLaunchPos %d %d not found!\n",m_attach_wslot, m_attach_specificBarrelToUse)); attachTransform.Make_Identity(); } Matrix3D worldTransform; launcher->convertBonePosToWorldPos(NULL, &attachTransform, NULL, &worldTransform); Vector3 tmp = worldTransform.Get_Translation(); Coord3D worldPos; worldPos.x = tmp.X; worldPos.y = tmp.Y; worldPos.z = tmp.Z; // // the missile on the raising up launch platform is actually 45 degrees from the missile // that is flying around the world ... we want to rotate it "on end and in place" so // that we don't see any decals on the side of the missile 'pop' to the new angle /// @todo, this should not be a hard coded value ... I love demos!!! // worldTransform.Rotate_X( (PI / 2.0f) ); getObject()->getDrawable()->setDrawableHidden(false); getObject()->setTransformMatrix(&worldTransform); getObject()->setPosition(&worldPos); getObject()->getExperienceTracker()->setExperienceSink( m_launcherID ); m_isLaunched = true; if (getNeutronMissileUpdateModuleData()->m_targetFromDirectlyAbove) m_reachedIntermediatePos = false; FXList::doFXObj(getNeutronMissileUpdateModuleData()->m_launchFX, getObject()); m_heightAtLaunch = getObject()->getPosition()->z; m_frameAtLaunch = TheGameLogic->getFrame(); } // fall Coord3D pos = *getObject()->getPosition(); pos.x += m_vel.x; pos.y += m_vel.y; pos.z += m_vel.z; getObject()->setPosition( &pos ); FXList::doFXObj(getNeutronMissileUpdateModuleData()->m_ignitionFX, getObject()); if (m_exhaustSysTmpl != NULL) TheParticleSystemManager->createAttachedParticleSystemID(m_exhaustSysTmpl, getObject()); m_state = ATTACK; m_stateTimestamp = TheGameLogic->getFrame(); // arm the missile's "warhead" m_isArmed = true; } //----------------------------------------------------------------------------- // return the angle (in 3-space) we actually turned. static Real calcTransform(const Object* obj, const Coord3D *pos, Real maxTurnRate, Matrix3D* newTransform ) { // convert to Vector3, to use all its handy stuff Vector3 objPos(obj->getPosition()->x, obj->getPosition()->y, obj->getPosition()->z); Vector3 otherPos(pos->x, pos->y, pos->z); Vector3 objDir = obj->getTransformMatrix()->Rotate_Vector(Vector3(1.0f, 0.0f, 0.0f)); Vector3 otherDir = otherPos - objPos; otherDir.Normalize(); // dot of two unit vectors is cos of angle between them Real c = Vector3::Dot_Product(objDir, otherDir); // bound it in case of numerical error if (c < -1.0) c = -1.0; else if (c > 1.0) c = 1.0; Real angle = (Real)ACos( c ); Vector3 newDir; if (fabs(angle) < maxTurnRate) { // close enough -- point exactly in the right dir newDir = otherDir; } else { // nah, turn as much as we can... this is icky, 'cuz // we need to rotate around the axis perpendicular to these two vecs angle = maxTurnRate; // cross of two vectors is the perpendicular axis #ifdef ALLOW_TEMPORARIES Vector3 objCrossOther = Vector3::Cross_Product(objDir, otherDir); objCrossOther.Normalize(); #else Vector3 objCrossOther; Vector3::Normalized_Cross_Product(objDir, otherDir, &objCrossOther); #endif Matrix3D rotMtx(objCrossOther, angle); newDir = rotMtx.Rotate_Vector(objDir); } newTransform->buildTransformMatrix( objPos, newDir ); return angle; } //------------------------------------------------------------------------------------------------- /** * Implement ATTACK state */ void NeutronMissileUpdate::doAttack( void ) { Matrix3D mx; Real speed = getNeutronMissileUpdateModuleData()->m_relativeSpeed; if (getNeutronMissileUpdateModuleData()->m_targetFromDirectlyAbove && m_reachedIntermediatePos) speed *= STRAIGHT_DOWN_SLOW_FACTOR; // if we're still in the no-turning-time, OR if we're out of fuel if (m_noTurnDistLeft > 0.0f) { mx = *getObject()->getTransformMatrix(); } else { { // // Orient toward destination // Real relAngle = calcTransform(getObject(), m_reachedIntermediatePos ? &m_targetPos : &m_intermedPos, getNeutronMissileUpdateModuleData()->m_maxTurnRate, &mx); // // Modulate speed according to turning. The more we have to turn, the slower we go // Real angleCoeff = (Real)fabs( relAngle ) / (PI / 2.0f); if (angleCoeff > 1.0f) angleCoeff = 1.0; } } // get true forward direction of missile Vector3 trueDir = mx.Get_X_Vector(); trueDir.Normalize(); // // Move forward along forward direction // Real damping = getNeutronMissileUpdateModuleData()->m_forwardDamping; m_accel.x = speed * trueDir.X - damping * m_vel.x; m_accel.y = speed * trueDir.Y - damping * m_vel.y; m_accel.z = speed * trueDir.Z - damping * m_vel.z; m_vel.x += m_accel.x; m_vel.y += m_accel.y; m_vel.z += m_accel.z; Coord3D pos = *getObject()->getPosition(); const NeutronMissileUpdateModuleData* d = getNeutronMissileUpdateModuleData(); UnsignedInt now = TheGameLogic->getFrame(); if (d->m_specialSpeedTime > 0 && now <= m_frameAtLaunch + d->m_specialSpeedTime) { getObject()->getDrawable()->setInstanceMatrix(NULL); UnsignedInt elapsed = now - m_frameAtLaunch; if (elapsed < d->m_specialSpeedTime) { Real timeFrac = (Real)elapsed / (Real)d->m_specialSpeedTime; Real accelFactor = d->m_specialAccelFactor; if (accelFactor < 0.01f) accelFactor = 0.01f; Coord3D newPos = pos; newPos.z = m_heightAtLaunch + (sqr(accelFactor * timeFrac) / accelFactor) * d->m_specialSpeedHeight; m_vel.x = newPos.x - pos.x; m_vel.y = newPos.y - pos.y; m_vel.z = newPos.z - pos.z; if (d->m_specialJitterDistance > 0.0f) { Real amplitude = (1.0f - timeFrac) * d->m_specialJitterDistance; Vector3 vectmp; vectmp.X = 0; // MDC: moving to GameLogicRandomValue. This does not need to be synced, but having it so makes searches *so* much nicer. vectmp.Y = GameLogicRandomValueReal(-1.0, 1.0) * amplitude; vectmp.Z = GameLogicRandomValueReal(-1.0, 1.0) * amplitude; vectmp = mx.Rotate_Vector(vectmp); Matrix3D mtxtmp(1); mtxtmp.Translate(vectmp); getObject()->getDrawable()->setInstanceMatrix( &mtxtmp ); } } } pos.x += m_vel.x; pos.y += m_vel.y; pos.z += m_vel.z; //DEBUG_LOG(("vel %f accel %f z %f\n",m_vel.length(),m_accel.length(), pos.z)); //Real vm = sqrt(m_vel.x*m_vel.x+m_vel.y*m_vel.y+m_vel.z*m_vel.z); //DEBUG_LOG(("vel is %f %f %f (%f)\n",m_vel.x,m_vel.y,m_vel.z,vm)); getObject()->setTransformMatrix( &mx ); getObject()->setPosition( &pos ); } //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- Bool NeutronMissileUpdate::projectileHandleCollision( Object *other ) { Object* obj = getObject(); // check if our warhead is "armed" - if not, we are inert if (projectileIsArmed() == false) return true; // Don't hit your own launcher, ever. if (other != NULL && projectileGetLauncherID() == other->getID()) return true; // collided with something... blow'd up! detonate(); // mark ourself as "no collisions" (since we might still exist in slow death mode) obj->setStatus( MAKE_OBJECT_STATUS_MASK( OBJECT_STATUS_NO_COLLISIONS ) ); return true; } //------------------------------------------------------------------------------------------------- void NeutronMissileUpdate::onDie( const DamageInfo *damageInfo ) { m_deliveryDecal.clear(); } //------------------------------------------------------------------------------------------------- void NeutronMissileUpdate::detonate() { m_deliveryDecal.clear(); Object* obj = getObject(); // kill it (vs destroying it) so that its Die modules are called obj->kill(); m_state = DEAD; if (obj->getDrawable()) obj->getDrawable()->setDrawableHidden(true); } //------------------------------------------------------------------------------------------------- /** * Simulate one frame of a missile's behavior */ UpdateSleepTime NeutronMissileUpdate::update( void ) { m_deliveryDecal.update(); if (!m_reachedIntermediatePos) { Real distSqr = ThePartitionManager->getDistanceSquared(getObject(), &m_intermedPos, FROM_CENTER_3D); Real boundSqr = sqr(getObject()->getGeometryInfo().getBoundingSphereRadius()); if (distSqr <= boundSqr) { m_reachedIntermediatePos = true; getObject()->setPosition(&m_intermedPos); Real vel = m_vel.length(); m_vel.x = 0; m_vel.y = 0; m_vel.z = -vel * STRAIGHT_DOWN_SLOW_FACTOR; } } Coord3D oldPos = *getObject()->getPosition(); Bool oldPosValid = (m_state == ATTACK); // not valid till *after* we've launched switch( m_state ) { case PRELAUNCH: // nothing... just ignore it. break; case LAUNCH: doLaunch(); break; case ATTACK: doAttack(); break; case DEAD: // do nothing break; } if (m_noTurnDistLeft > 0.0f && oldPosValid) { Coord3D newPos = *getObject()->getPosition(); Real distThisTurn = sqrt(sqr(newPos.x-oldPos.x) + sqr(newPos.y-oldPos.y) + sqr(newPos.z-oldPos.z)); //DEBUG_LOG(("noTurnDist goes from %f to %f\n",m_noTurnDistLeft,m_noTurnDistLeft-distThisTurn)); m_noTurnDistLeft -= distThisTurn; } if (m_state != PRELAUNCH && m_state != DEAD && !getObject()->isAboveTerrain()) { // the normal always points straight down, though we could // get the alignWithTerrain() normal if it proves interesting Coord3D normal; normal.x = normal.y = 0.0f; normal.z = -1.0f; getObject()->onCollide(NULL, getObject()->getPosition(), &normal); } return UPDATE_SLEEP_NONE; } // ------------------------------------------------------------------------------------------------ /** CRC */ // ------------------------------------------------------------------------------------------------ void NeutronMissileUpdate::crc( Xfer *xfer ) { // extend base class UpdateModule::crc( xfer ); } // end crc // ------------------------------------------------------------------------------------------------ /** Xfer method * Version Info: * 1: Initial version */ // ------------------------------------------------------------------------------------------------ void NeutronMissileUpdate::xfer( Xfer *xfer ) { // version XferVersion currentVersion = 1; XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); // extend base class UpdateModule::xfer( xfer ); // state xfer->xferUser( &m_state, sizeof( MissileStateType ) ); // target pos xfer->xferCoord3D( &m_targetPos ); // intermed pos xfer->xferCoord3D( &m_intermedPos ); // launcher ID xfer->xferObjectID( &m_launcherID ); // attach weapon slot xfer->xferUser( &m_attach_wslot, sizeof( WeaponSlotType ) ); // attach specific barrel xfer->xferInt( &m_attach_specificBarrelToUse ); // accel xfer->xferCoord3D( &m_accel ); // vel xfer->xferCoord3D( &m_vel ); // state timestamp xfer->xferUnsignedInt( &m_stateTimestamp ); // is launched xfer->xferBool( &m_isLaunched ); // is armed xfer->xferBool( &m_isArmed ); // no turn dist left xfer->xferReal( &m_noTurnDistLeft ); // raeched intermediate pos xfer->xferBool( &m_reachedIntermediatePos ); // farme at launch xfer->xferUnsignedInt( &m_frameAtLaunch ); // height at launch xfer->xferReal( &m_heightAtLaunch ); // decal, if any m_deliveryDecal.xferRadiusDecal(xfer); // particle system template AsciiString name = m_exhaustSysTmpl ? m_exhaustSysTmpl->getName() : AsciiString::TheEmptyString; xfer->xferAsciiString( &name ); if( xfer->getXferMode() == XFER_LOAD ) { // make system template NULL to be safe m_exhaustSysTmpl = NULL; if( name.isEmpty() == FALSE ) { m_exhaustSysTmpl = TheParticleSystemManager->findTemplate( name ); if( m_exhaustSysTmpl == NULL ) { DEBUG_CRASH(( "NeutronMissileUpdate::xfer - Unable to find particle system '%s'\n", name.str() )); throw SC_INVALID_DATA; } // end if } // end if } // end if } // end xfer // ------------------------------------------------------------------------------------------------ /** Load post process */ // ------------------------------------------------------------------------------------------------ void NeutronMissileUpdate::loadPostProcess( void ) { // extend base class UpdateModule::loadPostProcess(); } // end loadPostProcess