From 943f6fd6e56efd44a1a3831bd09504bc86e30f40 Mon Sep 17 00:00:00 2001 From: andreasw Date: Tue, 13 Oct 2026 15:53:48 +0200 Subject: [PATCH 1/3] Add LinearFlightPath and KillSelfDelay to DumbProjectileBehavior LinearFlightPath: fly a straight line at constant speed instead of the Bezier curve (height/indent/dynamic-height settings are ignored). KillSelfDelay: after detonation, keep the projectile alive (hidden, frozen at the impact point) for the given time before destroy/kill, so attached particle trails catch up instead of clipping short. Works for the weapon exhaust and for model-condition ParticleSysBone systems via the new DRAWABLE_STATUS_KEEP_PARTICLES_WHEN_HIDDEN flag, which W3DModelDraw honors when starting/stopping its particle systems on hide. While lingering the projectile: - has physics reset and DISABLED_HELD so gravity doesn't move it - reports projectileIsLingering() (new ProjectileUpdateInterface method), which ScatterShotUpdate and PointDefenseLaserUpdate use to ignore it Both settings default off; existing data behaves as before. Xfer version 3 saves the detonated/linger state. Co-Authored-By: Claude Opus 5.5 --- .../GameClient/Drawable/Draw/W3DModelDraw.cpp | 7 +- .../GameEngine/Include/GameClient/Drawable.h | 1 + .../GameEngine/Include/GameClient/Drawable.h | 2 + .../GameLogic/Module/DumbProjectileBehavior.h | 6 + .../Include/GameLogic/Module/UpdateModule.h | 1 + .../GameEngine/Source/GameClient/Drawable.cpp | 17 +++ .../Behavior/DumbProjectileBehavior.cpp | 120 ++++++++++++++++-- .../Object/Update/PointDefenseLaserUpdate.cpp | 18 +++ .../Object/Update/ScatterShotUpdate.cpp | 7 + 9 files changed, 168 insertions(+), 11 deletions(-) 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..2bc23286c5d 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); @@ -104,11 +107,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 +134,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/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/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/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(); From b91549e592bdddcafd024df735a0814c2858fb34 Mon Sep 17 00:00:00 2001 From: andreasw Date: Tue, 13 Oct 2026 17:55:42 +0200 Subject: [PATCH 2/3] Fix lingering DumbProjectile never dying with KillSelfDelay While lingering after detonation the projectile sets DISABLED_HELD to freeze physics, but GameLogic skips update modules on disabled objects unless they list that type in getDisabledTypesToProcess(). The behavior's update stopped running, so the projectile was never destroyed and kept emitting its trail particles at the impact point forever. Process updates while DISABLED_HELD. Co-Authored-By: Claude Opus 5.5 --- .../Include/GameLogic/Module/DumbProjectileBehavior.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DumbProjectileBehavior.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DumbProjectileBehavior.h index 2bc23286c5d..10ee0fd29f8 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DumbProjectileBehavior.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DumbProjectileBehavior.h @@ -91,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 From 0e943e5201a5c95b5237d9a53f787d700aab8b02 Mon Sep 17 00:00:00 2001 From: andreasw Date: Tue, 13 Oct 2026 17:55:42 +0200 Subject: [PATCH 3/3] Add MainTurretsFireInSequence to KodiakUpdate Main turrets are separate objects with their own weapons, so weapon rate of fire can't stagger them. With MainTurretsFireInSequence = Yes they take turns, one shot each: only the turn holder may fire, the others are held every frame, and once the holder fires the turn passes on. MainTurretSequenceDelay sets the minimum time between consecutive shots (0 = next frame). Adds Weapon::holdFiringUntil(), which delays the next shot and also leaves the cached READY_TO_FIRE status (like shared reload time does). Xfer version 3 saves the sequence state. Default off. Co-Authored-By: Claude Opus 5.5 --- .../Include/GameLogic/Module/KodiakUpdate.h | 9 ++ .../GameEngine/Include/GameLogic/Weapon.h | 1 + .../GameLogic/Object/Update/KodiakUpdate.cpp | 97 ++++++++++++++++++- .../Source/GameLogic/Object/Weapon.cpp | 13 +++ 4 files changed, 119 insertions(+), 1 deletion(-) 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/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/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/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 {