diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp index a7e55e21587..e19c4e52c40 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp @@ -2097,7 +2097,9 @@ W3DModelDraw::~W3DModelDraw() //------------------------------------------------------------------------------------------------- void W3DModelDraw::doStartOrStopParticleSys() { - Bool hidden = getDrawable()->isDrawableEffectivelyHidden() || m_fullyObscuredByShroud; + const Drawable* draw = getDrawable(); + Bool hidden = (draw->isDrawableEffectivelyHidden() && !draw->testDrawableStatus(DRAWABLE_STATUS_KEEP_PARTICLES_WHEN_HIDDEN)) + || m_fullyObscuredByShroud; for (std::vector::const_iterator it = m_particleSystemIDs.begin(); it != m_particleSystemIDs.end(); ++it) //for (std::vector::const_iterator it = m_particleSystemIDs.begin(); it != m_particleSystemIDs.end(); ++it) @@ -3088,7 +3090,8 @@ void W3DModelDraw::recalcBonesForClientParticleSystems() // important: mark it as do-not-save, since we'll just re-create it when we reload. sys->setSaveable(FALSE); - if (drawable->isDrawableEffectivelyHidden() || m_fullyObscuredByShroud) + if ((drawable->isDrawableEffectivelyHidden() && !drawable->testDrawableStatus(DRAWABLE_STATUS_KEEP_PARTICLES_WHEN_HIDDEN)) + || m_fullyObscuredByShroud) { sys->stop(); // don't start the systems for drawables that are hidden. } diff --git a/Generals/Code/GameEngine/Include/GameClient/Drawable.h b/Generals/Code/GameEngine/Include/GameClient/Drawable.h index bbe206ff429..8d315b1e46b 100644 --- a/Generals/Code/GameEngine/Include/GameClient/Drawable.h +++ b/Generals/Code/GameEngine/Include/GameClient/Drawable.h @@ -232,6 +232,7 @@ enum DrawableStatus CPP_11(: DrawableStatusBits) DRAWABLE_STATUS_NONE = 0x00000000, ///< no status DRAWABLE_STATUS_DRAWS_IN_MIRROR = 0x00000001, ///< drawable can reflect DRAWABLE_STATUS_SHADOWS = 0x00000002, ///< use setShadowsEnabled() access method + DRAWABLE_STATUS_KEEP_PARTICLES_WHEN_HIDDEN = 0x00000004, ///< model-condition particle systems keep emitting while the drawable is hidden DRAWABLE_STATUS_NO_STATE_PARTICLES = 0x00000008, ///< do *not* auto-create particle systems based on model condition DRAWABLE_STATUS_NO_SAVE = 0x00000010, ///< do *not* save this drawable (UI fluff only). ignored (error, actually) if attached to an object diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h index dca8120211e..340c73896a5 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h @@ -234,6 +234,7 @@ enum DrawableStatus CPP_11(: DrawableStatusBits) DRAWABLE_STATUS_NONE = 0x00000000, ///< no status DRAWABLE_STATUS_DRAWS_IN_MIRROR = 0x00000001, ///< drawable can reflect DRAWABLE_STATUS_SHADOWS = 0x00000002, ///< use setShadowsEnabled() access method + DRAWABLE_STATUS_KEEP_PARTICLES_WHEN_HIDDEN = 0x00000004, ///< model-condition particle systems keep emitting while the drawable is hidden DRAWABLE_STATUS_NO_STATE_PARTICLES = 0x00000008, ///< do *not* auto-create particle systems based on model condition DRAWABLE_STATUS_NO_SAVE = 0x00000010, ///< do *not* save this drawable (UI fluff only). ignored (error, actually) if attached to an object @@ -324,6 +325,7 @@ class Drawable : public Thing, DrawableInfo *getDrawableInfo() {return &m_drawableInfo;} void setDrawableHidden( Bool hidden ); ///< hide or unhide drawable + void setKeepParticlesWhenHidden( Bool keep ); ///< let model-condition particle systems keep emitting while hidden // // note that this is not necessarily the 'get' reflection of setDrawableHidden, since drawables // can spontaneously hide via stealth. (srj) diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DumbProjectileBehavior.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DumbProjectileBehavior.h index fc7fb3df12b..10ee0fd29f8 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DumbProjectileBehavior.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DumbProjectileBehavior.h @@ -68,6 +68,9 @@ class DumbProjectileBehaviorModuleData : public UpdateModuleData Real m_dynamicHeightMinScale; Real m_dynamicHeightMinRange; + Bool m_linearFlightPath; ///< fly a straight line at constant speed instead of the Bezier curve + UnsignedInt m_killSelfDelay; ///< frames to linger (hidden, particles still emitting) after detonation before dying + DumbProjectileBehaviorModuleData(); static void buildFieldParse(MultiIniFieldParse& p); @@ -88,6 +91,8 @@ class DumbProjectileBehavior : public UpdateModule, public ProjectileUpdateInter // UpdateModuleInterface virtual UpdateSleepTime update() override; + // we hold ourselves (DISABLED_HELD) while lingering after detonation (KillSelfDelay) and must keep updating to die + virtual DisabledMaskType getDisabledTypesToProcess() const override { return MAKE_DISABLED_MASK( DISABLED_HELD ); } virtual ProjectileUpdateInterface* getProjectileUpdateInterface() override { return this; } // ProjectileUpdateInterface @@ -104,11 +109,13 @@ class DumbProjectileBehavior : public UpdateModule, public ProjectileUpdateInter virtual Object* getTargetObject(); virtual const Coord3D* getTargetPosition(); virtual Bool projectileShouldCollideWithWater() const override; + virtual Bool projectileIsLingering() const override { return m_hasDetonated && getDumbProjectileBehaviorModuleData()->m_killSelfDelay > 0; } protected: void positionForLaunch(const Object *launcher, WeaponSlotType wslot, Int specificBarrelToUse); void detonate(); + void doDetonationDeath(); private: @@ -129,6 +136,7 @@ class DumbProjectileBehavior : public UpdateModule, public ProjectileUpdateInter ParticleSystemID m_exhaustID; Bool m_hasDetonated; ///< + UnsignedInt m_killSelfFrame; ///< while lingering after detonation: frame at which we die (0 = not lingering) Bool calcFlightPath(Bool recalcNumSegments); #if defined(RTS_DEBUG) diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/KodiakUpdate.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/KodiakUpdate.h index 6171852ccb1..87d5afd6945 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/KodiakUpdate.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/KodiakUpdate.h @@ -54,6 +54,9 @@ class KodiakUpdateModuleData : public ModuleData UnsignedInt m_numSideTurrets; UnsignedInt m_numAATurrets; + Bool m_mainTurretsFireInSequence; ///< main turrets take turns firing one shot each + UnsignedInt m_mainTurretSequenceDelay; ///< min frames between consecutive main turret shots in sequence mode + Real m_missileScatterRadius; std::vector m_scatterTargets; ///< targets for missile attack, amount of targets should match clip size of primary weapon @@ -116,6 +119,9 @@ class KodiakUpdate : public SpecialPowerUpdateModule void friend_enableAfterburners(Bool v); + void resetMainTurretSequence(const std::vector& turrets); + void updateMainTurretSequence(const std::vector& turrets); + @@ -129,6 +135,9 @@ class KodiakUpdate : public SpecialPowerUpdateModule UnsignedInt m_orbitEscapeFrame; + UnsignedInt m_seqTurretIndex; ///< main turret that currently has the turn (MainTurretsFireInSequence) + UnsignedInt m_seqHolderShotFrame; ///< turn holder's last shot frame when it got the turn; a change means it fired + RadiusDecal m_attackAreaDecal; RadiusDecal m_targetingReticleDecal; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/UpdateModule.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/UpdateModule.h index 89258ee75dd..e754c85a69a 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/UpdateModule.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/UpdateModule.h @@ -267,6 +267,7 @@ class ProjectileUpdateInterface virtual void setFramesTillCountermeasureDiversionOccurs( UnsignedInt frames ) = 0; ///< Number of frames till missile diverts to countermeasures. virtual void projectileNowJammed() = 0; virtual Bool projectileShouldCollideWithWater() const { return false; }; + virtual Bool projectileIsLingering() const { return false; } ///< true while detonated but kept alive (hidden) so attached particles can catch up virtual Object* getTargetObject() = 0; virtual const Coord3D* getTargetPosition() = 0; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Weapon.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Weapon.h index ce3324b2cc4..61c507e1a64 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Weapon.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Weapon.h @@ -730,6 +730,7 @@ class Weapon : public MemoryPoolObject, // do not ever use this unless you are weaponset.cpp void setPossibleNextShotFrame( UnsignedInt frameNum ) { m_whenWeCanFireAgain = frameNum; } + void holdFiringUntil( UnsignedInt frameNum ); ///< don't fire before frameNum (never shortens an existing delay) void setPreAttackFinishedFrame( UnsignedInt frameNum ) { m_whenPreAttackFinished = frameNum; } void setLastReloadStartedFrame( UnsignedInt frameNum ) { m_whenLastReloadStarted = frameNum; } void setNextPreAttackFXFrame(UnsignedInt frameNum) { m_nextPreAttackFXFrame = frameNum; } diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index effceb2616a..390d1df19c8 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -4947,6 +4947,23 @@ void Drawable::setDrawableHidden( Bool hidden ) } } +//------------------------------------------------------------------------------------------------- +/** Allow model-condition particle systems to keep emitting while the drawable is hidden */ +//------------------------------------------------------------------------------------------------- +void Drawable::setKeepParticlesWhenHidden( Bool keep ) +{ + if (keep == testDrawableStatus(DRAWABLE_STATUS_KEEP_PARTICLES_WHEN_HIDDEN)) + return; + + if (keep) + setDrawableStatus(DRAWABLE_STATUS_KEEP_PARTICLES_WHEN_HIDDEN); + else + clearDrawableStatus(DRAWABLE_STATUS_KEEP_PARTICLES_WHEN_HIDDEN); + + // let the draw modules re-evaluate whether their particle systems run + updateHiddenStatus(); +} + //------------------------------------------------------------------------------------------------- void Drawable::updateDrawableClipStatus( UnsignedInt shotsRemaining, UnsignedInt maxShots, WeaponSlotType slot ) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/DumbProjectileBehavior.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/DumbProjectileBehavior.cpp index 5e4e4df08e3..192b7f9b7b0 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/DumbProjectileBehavior.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/DumbProjectileBehavior.cpp @@ -70,7 +70,9 @@ DumbProjectileBehaviorModuleData::DumbProjectileBehaviorModuleData() : m_flightPathAdjustDistPerFrame(0.0f), m_applyLauncherBonus(FALSE), m_dynamicHeightMinScale(1.0), - m_dynamicHeightMinRange(1.0) + m_dynamicHeightMinRange(1.0), + m_linearFlightPath(FALSE), + m_killSelfDelay(0) { } @@ -103,6 +105,9 @@ void DumbProjectileBehaviorModuleData::buildFieldParse(MultiIniFieldParse& p) { "DynamicHeightMinScale", INI::parseReal, NULL, offsetof(DumbProjectileBehaviorModuleData, m_dynamicHeightMinScale) }, { "DynamicHeightMinRange", INI::parseReal, NULL, offsetof(DumbProjectileBehaviorModuleData, m_dynamicHeightMinRange) }, + { "LinearFlightPath", INI::parseBool, nullptr, offsetof(DumbProjectileBehaviorModuleData, m_linearFlightPath) }, + { "KillSelfDelay", INI::parseDurationUnsignedInt, nullptr, offsetof(DumbProjectileBehaviorModuleData, m_killSelfDelay) }, + { nullptr, nullptr, nullptr, 0 } }; @@ -132,6 +137,7 @@ DumbProjectileBehavior::DumbProjectileBehavior( Thing *thing, const ModuleData* m_exhaustID = INVALID_PARTICLE_SYSTEM_ID; m_hasDetonated = FALSE; + m_killSelfFrame = 0; } //------------------------------------------------------------------------------------------------- @@ -448,6 +454,45 @@ Bool DumbProjectileBehavior::calcFlightPath(Bool recalcNumSegments) return false; } + if (d->m_linearFlightPath) + { + // Straight line at constant speed: evenly spaced points from start to end (inclusive), + // same sampling convention as BezierSegment::getSegmentPoints. + Coord3D delta; + delta.x = controlPoints[3].x - controlPoints[0].x; + delta.y = controlPoints[3].y - controlPoints[0].y; + delta.z = controlPoints[3].z - controlPoints[0].z; + + if (recalcNumSegments) + { + m_flightPathSegments = ceil( delta.length() / m_flightPathSpeed ); + } + + m_flightPath.clear(); + m_flightPath.resize(m_flightPathSegments); + if (m_flightPathSegments == 1) + { + m_flightPath[0] = controlPoints[3]; + } + else + { + for (Int i = 0; i < m_flightPathSegments; ++i) + { + Real t = (Real)i / (Real)(m_flightPathSegments - 1); + m_flightPath[i].x = controlPoints[0].x + delta.x * t; + m_flightPath[i].y = controlPoints[0].y + delta.y * t; + m_flightPath[i].z = controlPoints[0].z + delta.z * t; + } + } + +#if defined(RTS_DEBUG) + if( TheGlobalData->m_debugProjectilePath ) + displayFlightPath(); +#endif + + return true; + } + // X and Y for inner points are along the line between us, so normalize and scale a vector between us, but // only use the x and y of the result Vector3 targetVector;// 0 origin vector between me and him @@ -584,7 +629,51 @@ void DumbProjectileBehavior::detonate() if (m_detonationWeaponTmpl) { TheWeaponStore->handleProjectileDetonation(m_detonationWeaponTmpl, obj, obj->getPosition(), m_extraBonusFlags); + } + + const UnsignedInt killSelfDelay = getDumbProjectileBehaviorModuleData()->m_killSelfDelay; + if (killSelfDelay > 0) + { + // Linger hidden at the impact point for a few frames so attached particle trails + // (weapon exhaust and model-condition particle systems) can catch up before we die. + m_killSelfFrame = TheGameLogic->getFrame() + killSelfDelay; + obj->setStatus( MAKE_OBJECT_STATUS_MASK( OBJECT_STATUS_NO_COLLISIONS ) ); + + // Freeze in place: Physics has been accumulating gravity while our flight path overrode the + // position each frame. Clear it, and hold the object so physics doesn't move it while lingering. + if (PhysicsBehavior* physics = obj->getPhysics()) + physics->resetDynamicPhysics(); + obj->setDisabled( DISABLED_HELD ); + + if (Drawable* draw = obj->getDrawable()) + { + draw->setKeepParticlesWhenHidden(true); + draw->setDrawableHidden(true); + } + } + else + { + doDetonationDeath(); + if (obj->getDrawable()) + obj->getDrawable()->setDrawableHidden(true); + } + + m_hasDetonated = TRUE; + +} + +//------------------------------------------------------------------------------------------------- +void DumbProjectileBehavior::doDetonationDeath() +{ + Object* obj = getObject(); + + // stop any particle systems we kept alive while lingering + if (Drawable* draw = obj->getDrawable()) + draw->setKeepParticlesWhenHidden(false); + + if (m_detonationWeaponTmpl) + { if ( getDumbProjectileBehaviorModuleData()->m_detonateCallsKill ) { // don't call kill(); do it manually, so we can specify DEATH_DETONATED @@ -611,12 +700,6 @@ void DumbProjectileBehavior::detonate() damageInfo.in.m_amount = obj->getBodyModule()->getMaxHealth(); obj->attemptDamage( &damageInfo ); } - - if (obj->getDrawable()) - obj->getDrawable()->setDrawableHidden(true); - - m_hasDetonated = TRUE; - } //------------------------------------------------------------------------------------------------- @@ -627,6 +710,18 @@ UpdateSleepTime DumbProjectileBehavior::update() { const DumbProjectileBehaviorModuleData* d = getDumbProjectileBehaviorModuleData(); + if (m_hasDetonated && d->m_killSelfDelay > 0) + { + // lingering at the impact point after detonation (KillSelfDelay); die once the delay is up. + // (without KillSelfDelay we keep the original behavior below) + if (m_killSelfFrame != 0 && TheGameLogic->getFrame() >= m_killSelfFrame) + { + m_killSelfFrame = 0; + doDetonationDeath(); + } + return UPDATE_SLEEP_NONE; + } + if (m_lifespanFrame != 0 && TheGameLogic->getFrame() >= m_lifespanFrame) { // lifetime demands detonation @@ -816,17 +911,18 @@ void DumbProjectileBehavior::crc( Xfer *xfer ) * Version Info: * 1: Initial version * 2: TheSuperHackers @bugfix Added m_currentFlightPathStep for mid-flight save/load. + * Also added m_launchVeterancy (for veterancy FX/OCL selection) + * 3: Added m_hasDetonated and m_killSelfFrame (KillSelfDelay lingering) */ // ------------------------------------------------------------------------------------------------ void DumbProjectileBehavior::xfer( Xfer *xfer ) { // version - // 2: Added m_launchVeterancy (for veterancy FX/OCL selection) #if RETAIL_COMPATIBLE_XFER_SAVE XferVersion currentVersion = 1; #else - XferVersion currentVersion = 2; + XferVersion currentVersion = 3; #endif XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); @@ -899,6 +995,12 @@ void DumbProjectileBehavior::xfer( Xfer *xfer ) xfer->xferInt( &m_currentFlightPathStep ); } + if( version >= 3 ) + { + xfer->xferBool( &m_hasDetonated ); + xfer->xferUnsignedInt( &m_killSelfFrame ); + } + } // ------------------------------------------------------------------------------------------------ diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/KodiakUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/KodiakUpdate.cpp index cb4fd3f80c5..a5a357c46f2 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/KodiakUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/KodiakUpdate.cpp @@ -73,6 +73,9 @@ KodiakUpdateModuleData::KodiakUpdateModuleData() m_numAATurrets = 0; m_turretRecenterFramesBeforeExit = 0; m_initialAttackDelayFrames = 0; + + m_mainTurretsFireInSequence = FALSE; + m_mainTurretSequenceDelay = 0; } static Real zero = 0.0f; @@ -104,6 +107,8 @@ static Real zero = 0.0f; { "MissileScatterRadius", INI::parseReal, NULL, offsetof(KodiakUpdateModuleData, m_missileScatterRadius) }, { "TurretRecenterTimeBeforeExit", INI::parseDurationUnsignedInt, NULL, offsetof(KodiakUpdateModuleData, m_turretRecenterFramesBeforeExit) }, { "InitialAttackDelay", INI::parseDurationUnsignedInt, NULL, offsetof(KodiakUpdateModuleData, m_initialAttackDelayFrames) }, + { "MainTurretsFireInSequence", INI::parseBool, NULL, offsetof(KodiakUpdateModuleData, m_mainTurretsFireInSequence) }, + { "MainTurretSequenceDelay", INI::parseDurationUnsignedInt, NULL, offsetof(KodiakUpdateModuleData, m_mainTurretSequenceDelay) }, { 0, 0, 0, 0 } @@ -135,6 +140,8 @@ KodiakUpdate::KodiakUpdate( Thing *thing, const ModuleData* moduleData ) : Speci m_attackAreaDecal.clear(); m_targetingReticleDecal.clear(); m_orbitEscapeFrame = 0; + m_seqTurretIndex = 0; + m_seqHolderShotFrame = 0; m_afterburnerSound = *(getObject()->getTemplate()->getPerUnitSound("Afterburner")); #if defined TRACKERS @@ -455,6 +462,7 @@ UpdateSleepTime KodiakUpdate::update() { setLogicalStatus( GUNSHIP_STATUS_ORBITING ); m_orbitEscapeFrame = TheGameLogic->getFrame() + data->m_orbitFrames; + resetMainTurretSequence( turrets ); AIUpdateInterface *shipAI = gunship->getAIUpdateInterface(); if ( shipAI) @@ -517,6 +525,10 @@ UpdateSleepTime KodiakUpdate::update() } else { + if (data->m_mainTurretsFireInSequence) + { + updateMainTurretSequence( turrets ); + } // ONLY EVERY FEW FRAMES DO WE RE_EVALUATE THE TARGET OBJECT if ( TheGameLogic->getFrame() %data->m_mainTargetingRate < ONE ) @@ -841,6 +853,82 @@ Bool KodiakUpdate::isFairDistanceFromShip( Object *target ) } +//------------------------------------------------------------------------------------------------- +// Main turret firing sequence (MainTurretsFireInSequence) +//------------------------------------------------------------------------------------------------- +static UnsignedInt getTurretLastShotFrame(const Object* turret) +{ + UnsignedInt lastShot = 0; + for (Int slot = 0; slot < WEAPONSLOT_COUNT; ++slot) + { + const Weapon* weapon = turret->getWeaponInWeaponSlot((WeaponSlotType)slot); + if (weapon && weapon->getLastShotFrame() > lastShot) + lastShot = weapon->getLastShotFrame(); + } + return lastShot; +} + +//------------------------------------------------------------------------------------------------- +/** Keep all weapons of the turret from firing before untilFrame (same approach as shared reload time). */ +static void holdTurretWeapons(Object* turret, UnsignedInt untilFrame) +{ + for (Int slot = 0; slot < WEAPONSLOT_COUNT; ++slot) + { + Weapon* weapon = turret->getWeaponInWeaponSlot((WeaponSlotType)slot); + if (weapon) + weapon->holdFiringUntil(untilFrame); + } +} + +//------------------------------------------------------------------------------------------------- +void KodiakUpdate::resetMainTurretSequence(const std::vector& turrets) +{ + m_seqTurretIndex = 0; + m_seqHolderShotFrame = turrets.empty() ? 0 : getTurretLastShotFrame(turrets.front()); +} + +//------------------------------------------------------------------------------------------------- +/** Main turrets take turns: only the turn holder may fire; once it fired, the turn passes on. */ +void KodiakUpdate::updateMainTurretSequence(const std::vector& turrets) +{ + const KodiakUpdateModuleData* data = getKodiakUpdateModuleData(); + + UnsignedInt numMain = data->m_numMainTurrets; + if (numMain > turrets.size()) + numMain = turrets.size(); + if (numMain < 2) + return; + + if (m_seqTurretIndex >= numMain) + { + m_seqTurretIndex = 0; + m_seqHolderShotFrame = getTurretLastShotFrame(turrets[0]); + } + + const UnsignedInt now = TheGameLogic->getFrame(); + + // Did the turn holder fire? Then pass the turn to the next main turret. + UnsignedInt holderShot = getTurretLastShotFrame(turrets[m_seqTurretIndex]); + if (holderShot != m_seqHolderShotFrame) + { + m_seqTurretIndex = (m_seqTurretIndex + 1) % numMain; + Object* nextHolder = turrets[m_seqTurretIndex]; + m_seqHolderShotFrame = getTurretLastShotFrame(nextHolder); + + // never in the same frame as the previous shot, and respect the sequence delay + UnsignedInt delay = data->m_mainTurretSequenceDelay > 1 ? data->m_mainTurretSequenceDelay : 1; + holdTurretWeapons(nextHolder, holderShot + delay); + } + + // Everyone else waits. now+2 covers the rest of this frame and the next one regardless of + // whether the turrets update before or after us; we refresh this every frame. + for (UnsignedInt i = 0; i < numMain; ++i) + { + if (i != m_seqTurretIndex) + holdTurretWeapons(turrets[i], now + 2); + } +} + @@ -952,6 +1040,7 @@ void KodiakUpdate::crc( Xfer *xfer ) * Version Info: * 1: Initial version * 2: Can't flat-save decals, that's a hella crash (they aren't saved (no memory) and they have a pointer in them). And half the class wasn't saved. + * 3: Added main turret firing sequence state (m_seqTurretIndex, m_seqHolderShotFrame) */ // ------------------------------------------------------------------------------------------------ void KodiakUpdate::xfer( Xfer *xfer ) @@ -959,7 +1048,7 @@ void KodiakUpdate::xfer( Xfer *xfer ) // const SpectreGunshipUpdateModuleData *data = getSpectreGunshipUpdateModuleData(); // version - XferVersion currentVersion = 2; + XferVersion currentVersion = 3; XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); @@ -995,6 +1084,12 @@ void KodiakUpdate::xfer( Xfer *xfer ) xfer->xferCoord3D( &m_positionToShootAt ); } + if( version >= 3 ) + { + xfer->xferUnsignedInt( &m_seqTurretIndex ); + xfer->xferUnsignedInt( &m_seqHolderShotFrame ); + } + } // end xfer // ------------------------------------------------------------------------------------------------ diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PointDefenseLaserUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PointDefenseLaserUpdate.cpp index 0bbe34ac99b..11df349430b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PointDefenseLaserUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PointDefenseLaserUpdate.cpp @@ -161,6 +161,16 @@ void PointDefenseLaserUpdate::fireWhenReady() //Track our target Object *target = TheGameLogic->findObjectByID( m_bestTargetID ); + if( target && target->isKindOf( KINDOF_PROJECTILE ) ) + { + // Our target detonated and only lingers for its trail (DumbProjectile KillSelfDelay); drop it. + const ProjectileUpdateInterface* pui = target->getProjectileUpdateInterface(); + if( pui && pui->projectileIsLingering() ) + { + target = nullptr; + m_bestTargetID = INVALID_ID; + } + } if( target ) { WeaponBonus bonus; @@ -290,6 +300,14 @@ Object* PointDefenseLaserUpdate::scanClosestTarget() continue; } + if( other->isKindOf( KINDOF_PROJECTILE ) ) + { + // Already detonated, only lingering (hidden) for its trail (DumbProjectile KillSelfDelay). + const ProjectileUpdateInterface* pui = other->getProjectileUpdateInterface(); + if( pui && pui->projectileIsLingering() ) + continue; + } + Real fDist = sqrt( ThePartitionManager->getDistanceSquared( me, other, FROM_CENTER_2D ) ); if( fDist <= fireRange ) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ScatterShotUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ScatterShotUpdate.cpp index 9be31488da2..0dfd421f99c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ScatterShotUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ScatterShotUpdate.cpp @@ -229,6 +229,13 @@ UpdateSleepTime ScatterShotUpdate::update(void) return UPDATE_SLEEP_FOREVER; } + // Our projectile has already detonated and only lingers for its trail (DumbProjectile KillSelfDelay); don't trigger anymore. + if (getObject()->isKindOf(KINDOF_PROJECTILE)) { + const ProjectileUpdateInterface* pui = getObject()->getProjectileUpdateInterface(); + if (pui && pui->projectileIsLingering()) + return UPDATE_SLEEP_FOREVER; + } + // Instant Trigger if (data->m_triggerInstantly) { triggerScatterShot(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp index b61b6c4737a..d3556186f92 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Weapon.cpp @@ -3558,6 +3558,19 @@ Object* Weapon::forceFireWeapon( const Object *source, const Coord3D *pos) return TheGameLogic->findObjectByID( projectileID ); } +//------------------------------------------------------------------------------------------------- +void Weapon::holdFiringUntil( UnsignedInt frameNum ) +{ + if( m_whenWeCanFireAgain >= frameNum ) + return; + + // getStatus() keeps returning a cached READY_TO_FIRE until m_whenWeCanFireAgain, so the status + // must change too (same as shared reload time). Reloading / out of ammo keep their status. + if( getStatus() == READY_TO_FIRE ) + setStatus( BETWEEN_FIRING_SHOTS ); + m_whenWeCanFireAgain = frameNum; +} + //------------------------------------------------------------------------------------------------- WeaponStatus Weapon::getStatus() const {