From 9bcc546274a56e0d436c9c0f9d8b0c241e995091 Mon Sep 17 00:00:00 2001 From: pWn3d Date: Sun, 1 Jun 2025 19:00:32 +0200 Subject: [PATCH 1/3] feat(module): Add RiftSlowDeathBehavior Cherry-picked from the rift branch. Slow death module that pulls nearby objects toward a locked-in rift position over delay/rampup/main/fadeout phases, then applies a final outward shockwave push. Pool sizes moved to Core since the original commit, so the pool entry now lives in GameMemoryInitPools_GeneralsMD.inl instead of MemoryInit.cpp. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Ju33QbWNdD5y7pRdHRwTcZ --- .../System/GameMemoryInitPools_GeneralsMD.inl | 1 + GeneralsMD/Code/GameEngine/CMakeLists.txt | 2 + .../GameLogic/Module/RiftSlowDeathUpdate.h | 59 ++++ .../Source/Common/Thing/ModuleFactory.cpp | 3 + .../Object/Update/RiftSlowDeathUpdate.cpp | 315 ++++++++++++++++++ 5 files changed, 380 insertions(+) create mode 100644 GeneralsMD/Code/GameEngine/Include/GameLogic/Module/RiftSlowDeathUpdate.h create mode 100644 GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/RiftSlowDeathUpdate.cpp diff --git a/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl b/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl index 07435a46062..683be7eb63b 100644 --- a/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl +++ b/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl @@ -709,5 +709,6 @@ static PoolSizeRec PoolSizes[] = { "DrawBridgeTowerUpdate", 8, 8 }, { "DrawBridgeUpdate", 4, 4 }, { "CrateApplyUpgrade", 64, 32 }, + { "RiftSlowDeathBehavior", 8, 8 }, { 0, 0, 0 } }; diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index ccf1808268b..06ece6e8613 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -405,6 +405,7 @@ set(GAMEENGINE_SRC Include/GameLogic/Module/RepairDockUpdate.h Include/GameLogic/Module/ReplaceObjectUpgrade.h Include/GameLogic/Module/RiderChangeContain.h + Include/GameLogic/Module/RiftSlowDeathUpdate.h Include/GameLogic/Module/SabotageCommandCenterCrateCollide.h Include/GameLogic/Module/SabotageFakeBuildingCrateCollide.h Include/GameLogic/Module/SabotageInternetCenterCrateCollide.h @@ -1219,6 +1220,7 @@ set(GAMEENGINE_SRC Source/GameLogic/Object/Update/OrbitalBeamUpdate.cpp Source/GameLogic/Object/Update/ProximityCaptureUpdate.cpp Source/GameLogic/Object/Update/RadiusDecalBehavior.cpp + Source/GameLogic/Object/Update/RiftSlowDeathUpdate.cpp Source/GameLogic/Object/Update/ResetSpecialPowerTimerWhileAliveUpdate.cpp Source/GameLogic/Object/Update/ScatterShotUpdate.cpp Source/GameLogic/Object/Update/ShipSlowDeathBehavior.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/RiftSlowDeathUpdate.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/RiftSlowDeathUpdate.h new file mode 100644 index 00000000000..260270128e3 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/RiftSlowDeathUpdate.h @@ -0,0 +1,59 @@ +#pragma once + +#ifndef __RIFTSlowDeathBehavior_H_ +#define __RIFTSlowDeathBehavior_H_ + +// INCLUDES /////////////////////////////////////////////////////////////////////////////////////// +#include "GameLogic/Module/SlowDeathBehavior.h" + +class FXList; + +// ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ +class RiftSlowDeathBehaviorModuleData : public SlowDeathBehaviorModuleData +{ + +public: + + RiftSlowDeathBehaviorModuleData(void); + + static void buildFieldParse(MultiIniFieldParse& p); + + UnsignedInt m_initialDelay; ///< Frames before effect starts + UnsignedInt m_rampupTime; ///< Frames till full effect + UnsignedInt m_mainDuration; ///< Frames with full effect after rampupTime + UnsignedInt m_fadeOutTime; ///< Frames till zero effect after mainDuration + Real m_radius; ///< Radius of the effect + Real m_damage; ///< damage applied per tick + const FXList* m_FXmain; ///< FX played at start of main duration + const FXList* m_FXcharge; ///< FX played at start + const FXList* m_FXfinal; ///< FX played at final push +}; + +// ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ +class RiftSlowDeathBehavior : public SlowDeathBehavior +{ + + MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE(RiftSlowDeathBehavior, "RiftSlowDeathBehavior") + MAKE_STANDARD_MODULE_MACRO_WITH_MODULE_DATA(RiftSlowDeathBehavior, RiftSlowDeathBehaviorModuleData) + +public: + + RiftSlowDeathBehavior(Thing* thing, const ModuleData* moduleData); + // virtual destructor prototype provided by memory pool declaration + + virtual UpdateSleepTime update(void); ///< the update call + +protected: + + void applyPull(float magnitude_multiplier) const; + void doFinalPush() const; + + UnsignedInt m_activationFrame; ///< frame we were activated on + Coord3D m_riftPos; ///< Position where the rift starts, center of mass + bool m_finalPush; ///< final push happened + bool m_playedMainFX; +}; + +#endif // end __RIFTSlowDeathBehavior_H_ \ No newline at end of file diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp index 331377fc23d..aa18c63d5fd 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp @@ -322,6 +322,8 @@ #include "GameClient/Module/DynamicGeometryClientUpdate.h" #include "GameClient/Module/BeaconClientUpdate.h" +#include "GameLogic/Module/RiftSlowDeathUpdate.h" + // PUBLIC DATA //////////////////////////////////////////////////////////////////////////////////// ModuleFactory *TheModuleFactory = nullptr; ///< the module factory singleton @@ -379,6 +381,7 @@ void ModuleFactory::init() addModule( HelicopterSlowDeathBehavior ); addModule( ShipSlowDeathBehavior ); addModule( NeutronMissileSlowDeathBehavior ); + addModule( RiftSlowDeathBehavior ); addModule( CaveContain ); addModule( OpenContain ); addModule( OverchargeBehavior ); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/RiftSlowDeathUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/RiftSlowDeathUpdate.cpp new file mode 100644 index 00000000000..bcc4557821b --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/RiftSlowDeathUpdate.cpp @@ -0,0 +1,315 @@ +#include "GameLogic/Module/RiftSlowDeathUpdate.h" +#include "GameLogic/GameLogic.h" +#include "GameLogic/Object.h" +#include "GameLogic/ObjectIter.h" +#include "GameLogic/PartitionManager.h" +#include "GameLogic/Module/ToppleUpdate.h" +#include "GameLogic/TerrainLogic.h" +#include "GameClient/FXList.h" +#include "GameLogic/Module/PhysicsUpdate.h" + +RiftSlowDeathBehaviorModuleData::RiftSlowDeathBehaviorModuleData(void) +{ + m_initialDelay = 0; + m_rampupTime = 0; + m_mainDuration = 0; + m_fadeOutTime = 0; + m_radius = 0.0f; + m_damage = 0.0f; + m_FXmain = NULL; + m_FXcharge = NULL; + m_FXfinal = NULL; +} // end RiftSlowDeathBehaviorModuleData + +// ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ +/*static*/ void RiftSlowDeathBehaviorModuleData::buildFieldParse(MultiIniFieldParse& p) +{ + + SlowDeathBehaviorModuleData::buildFieldParse(p); + + static const FieldParse dataFieldParse[] = + { + { "InitialDelay", INI::parseDurationUnsignedInt, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_initialDelay) }, + { "RampupTime", INI::parseDurationUnsignedInt, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_rampupTime) }, + { "MainDuration", INI::parseDurationUnsignedInt, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_mainDuration) }, + { "FadeOutTime", INI::parseDurationUnsignedInt, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_fadeOutTime) }, + { "Radius", INI::parseReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_radius) }, + { "Damage", INI::parseReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_damage) }, + { "FXMain", INI::parseFXList, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_FXmain) }, + { "FXCharge", INI::parseFXList, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_FXcharge) }, + { "FXFinal", INI::parseFXList, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_FXfinal) }, + { 0, 0, 0, 0 } + }; + + p.add(dataFieldParse); + +} // end buildFieldParse + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ +RiftSlowDeathBehavior::RiftSlowDeathBehavior(Thing* thing, const ModuleData* moduleData) + : SlowDeathBehavior(thing, moduleData) +{ + m_activationFrame = 0; + m_riftPos.x = 0.0f; + m_riftPos.y = 0.0f; + m_riftPos.z = 0.0f; + m_finalPush = false; + m_playedMainFX = false; +} // end RiftSlowDeathBehavior + +// ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ +RiftSlowDeathBehavior::~RiftSlowDeathBehavior(void) +{ + +} // end ~RiftSlowDeathBehavior + +// ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ +UpdateSleepTime RiftSlowDeathBehavior::update(void) +{ + /// @todo srj use SLEEPY_UPDATE here + // get the module data + const RiftSlowDeathBehaviorModuleData* modData = getRiftSlowDeathBehaviorModuleData(); + + // call the base class cause we're extending functionality + SlowDeathBehavior::update(); + + // get out of here if we're not activated yet + if (isSlowDeathActivated() == FALSE) + return UPDATE_SLEEP_NONE; + + // get the current frame + UnsignedInt currFrame = TheGameLogic->getFrame(); + + // when we become activated we want to do a few things + if (m_activationFrame == 0) + { + const Coord3D* projectilePos = getObject()->getPosition(); + + // Lock in position when activated + m_riftPos.x = projectilePos->x; + m_riftPos.y = projectilePos->y; + m_riftPos.z = TheTerrainLogic->getGroundHeight(m_riftPos.x, m_riftPos.y) + 100.0f; + + // record the frame + m_activationFrame = currFrame; + + FXList::doFXPos(modData->m_FXcharge, &m_riftPos); + //DEBUG_LOG(("Rift Position(%f, %f, %f)\n", m_riftPos.x, m_riftPos.y, m_riftPos.z)); + //DEBUG_LOG(("Rift Frame Start: %d\n", m_activationFrame)); + } // end if + + UnsignedInt frameProgress = currFrame - m_activationFrame; + + if (frameProgress < modData->m_initialDelay) { + // Charging Up + + //DEBUG_LOG(("RIFT Initial: %d\n", frameProgress)); + } + else if (frameProgress < modData->m_initialDelay + modData->m_rampupTime) { + // rampup phase + float rampup = static_cast(frameProgress - modData->m_initialDelay) / static_cast(modData->m_rampupTime); + + if (!m_playedMainFX) { + FXList::doFXPos(modData->m_FXmain, &m_riftPos); + m_playedMainFX = true; + } + + applyPull(rampup); + //DEBUG_LOG(("RIFT Charge: %d\n", frameProgress)); + } + else if (frameProgress < modData->m_initialDelay + modData->m_rampupTime + modData->m_mainDuration) { + // main damage phase + + if (!m_playedMainFX) { + FXList::doFXPos(modData->m_FXmain, &m_riftPos); + m_playedMainFX = true; + } + + applyPull(1.0f); + //DEBUG_LOG(("RIFT Main: %d\n", frameProgress)); + } + else if (frameProgress < modData->m_initialDelay + modData->m_rampupTime + modData->m_mainDuration + modData->m_fadeOutTime) { + // fade out phase + float fadeout = 1.0f - (static_cast(frameProgress - (modData->m_initialDelay + modData->m_rampupTime + modData->m_mainDuration)) / static_cast(modData->m_fadeOutTime)); + + applyPull(fadeout); + //DEBUG_LOG(("RIFT Fade: %d\n", frameProgress)); + } + else { + // do final push, + if (!m_finalPush) { + FXList::doFXPos(modData->m_FXfinal, &m_riftPos); + doFinalPush(); + m_finalPush = true; + } + //DEBUG_LOG(("RIFT FINAL: %d\n", frameProgress)); + } + + + return UPDATE_SLEEP_NONE; + +} // end update + +void RiftSlowDeathBehavior::applyPull(float magnitude_multiplier) const +{ + const RiftSlowDeathBehaviorModuleData* data = getRiftSlowDeathBehaviorModuleData(); + + DamageInfo damageInfo; + damageInfo.in.m_damageType = DAMAGE_EXPLOSION; + damageInfo.in.m_deathType = DEATH_EXPLODED; + damageInfo.in.m_sourceID = getObject()->getID(); + damageInfo.in.m_amount = data->m_damage * magnitude_multiplier; + damageInfo.in.m_shockWaveVector.zero(); + damageInfo.in.m_shockWaveAmount = 0.5f; + damageInfo.in.m_shockWaveRadius = data->m_radius; + damageInfo.in.m_shockWaveTaperOff = 1.0f; + + // scan objects in our region + ObjectIterator* iter = ThePartitionManager->iterateObjectsInRange(&m_riftPos, + data->m_radius, + FROM_CENTER_2D, NULL); + + MemoryPoolObjectHolder hold(iter); + + for (Object* currentObj = iter->first(); currentObj != NULL; currentObj = iter->next()) + { + // get other position + const Coord3D * objPos = currentObj->getPosition(); + + // compute vector from the object to the rift center + Coord3D forceVector; + forceVector.x = m_riftPos.x - objPos->x; + forceVector.y = m_riftPos.y - objPos->y; + forceVector.z = m_riftPos.z - objPos->z; + + /*if (objPos->z > m_riftPos.z) { + // avoid overshooting + forceVector.z = -1.0f; + }*/ + damageInfo.in.m_shockWaveVector = forceVector; + //Real distance = forceVector.length(); + //Real pull_strength = (distance / data->m_radius) * 0.5f + 0.5f; + + if (!currentObj->isKindOf(KINDOF_IMMOBILE) && !currentObj->isKindOf(KINDOF_PROJECTILE) && !currentObj->isKindOf(KINDOF_INERT) && objPos->length() > 3.0f*magnitude_multiplier) { + Coord3D offset = forceVector; + offset.normalize(); + offset.scale(3.0f * magnitude_multiplier); + offset.x += objPos->x; + offset.y += objPos->y; + offset.z += objPos->z; + currentObj->setPosition(&offset); + } + /*PhysicsBehavior* phys = currentObj->getPhysics(); + Real phys_mult = 0.1f; + if (phys != NULL) { + phys_mult = phys->getMass(); + + const Coord3D* current_velocity = phys->getVelocity(); + Coord3D minus_vel; + minus_vel.x = -current_velocity->x; + minus_vel.y = -current_velocity->y; + minus_vel.z = -current_velocity->z; + phys->applyForce(&minus_vel); + }*/ + //pull_strength *= magnitude_multiplier * phys_mult; + //currentObj->topple(&forceVector, pull_strength * 10.0f * magnitude_multiplier, TOPPLE_OPTIONS_NO_BOUNCE | TOPPLE_OPTIONS_NO_FX); + + //damageInfo.in.m_shockWaveAmount = pull_strength * 0.2f; + currentObj->attemptDamage(&damageInfo); + } +} + +void RiftSlowDeathBehavior::doFinalPush() const +{ + const RiftSlowDeathBehaviorModuleData* data = getRiftSlowDeathBehaviorModuleData(); + + DamageInfo damageInfo; + damageInfo.in.m_damageType = DAMAGE_EXPLOSION; + damageInfo.in.m_deathType = DEATH_SPLATTED; + damageInfo.in.m_sourceID = getObject()->getID(); + damageInfo.in.m_amount = 1.0f; + damageInfo.in.m_shockWaveVector.zero(); + damageInfo.in.m_shockWaveAmount = 50.0f; + damageInfo.in.m_shockWaveRadius = data->m_radius; + damageInfo.in.m_shockWaveTaperOff = 0.1f; + + // scan objects in our region + ObjectIterator* iter = ThePartitionManager->iterateObjectsInRange(&m_riftPos, + data->m_radius, + FROM_CENTER_2D, NULL); + + MemoryPoolObjectHolder hold(iter); + + for (Object* currentObj = iter->first(); currentObj != NULL; currentObj = iter->next()) + { + // get other position + const Coord3D* objPos = currentObj->getPosition(); + + // compute vector from the object to the rift center + Coord3D forceVector; + forceVector.x = objPos->x - m_riftPos.x; + forceVector.y = objPos->y - m_riftPos.y; + forceVector.z = 0.0f; + + damageInfo.in.m_shockWaveVector = forceVector; + + currentObj->attemptDamage(&damageInfo); + } +} + +// ------------------------------------------------------------------------------------------------ +/** CRC */ +// ------------------------------------------------------------------------------------------------ +void RiftSlowDeathBehavior::crc(Xfer* xfer) +{ + + // extend base class + SlowDeathBehavior::crc(xfer); + +} // end crc + +// ------------------------------------------------------------------------------------------------ +/** Xfer method + * Version Info: + * 1: Initial version */ + // ------------------------------------------------------------------------------------------------ +void RiftSlowDeathBehavior::xfer(Xfer* xfer) +{ + + // version + XferVersion currentVersion = 1; + XferVersion version = currentVersion; + xfer->xferVersion(&version, currentVersion); + + // extend base class + SlowDeathBehavior::xfer(xfer); + + // activation frame + xfer->xferUnsignedInt(&m_activationFrame); + + xfer->xferCoord3D(&m_riftPos); + + xfer->xferBool(&m_finalPush); + + xfer->xferBool(&m_playedMainFX); + +} // end xfer + +// ------------------------------------------------------------------------------------------------ +/** Load post process */ +// ------------------------------------------------------------------------------------------------ +void RiftSlowDeathBehavior::loadPostProcess(void) +{ + + // extend base class + SlowDeathBehavior::loadPostProcess(); + +} // end loadPostProcess \ No newline at end of file From 3f844ee67056147d1e2f52e93e21b12578ede5be Mon Sep 17 00:00:00 2001 From: pWn3d Date: Sun, 27 Sep 2026 09:46:10 +0200 Subject: [PATCH 2/3] Updated Rift behavior --- .../GameLogic/Module/RiftSlowDeathUpdate.h | 119 ++- .../Object/Update/RiftSlowDeathUpdate.cpp | 926 ++++++++++++++---- 2 files changed, 864 insertions(+), 181 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/RiftSlowDeathUpdate.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/RiftSlowDeathUpdate.h index 260270128e3..0582edda130 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/RiftSlowDeathUpdate.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/RiftSlowDeathUpdate.h @@ -4,9 +4,47 @@ #define __RIFTSlowDeathBehavior_H_ // INCLUDES /////////////////////////////////////////////////////////////////////////////////////// +#include "Common/KindOf.h" +#include "GameLogic/Damage.h" #include "GameLogic/Module/SlowDeathBehavior.h" class FXList; +class ObjectCreationList; +class Object; + +// ------------------------------------------------------------------------------------------------ +/** The rift runs through a fixed sequence of phases once the slow death is activated. Each phase + * yields a "strength" in the 0..1 range that scales suction and damage. */ +// ------------------------------------------------------------------------------------------------ +enum RiftPhaseType +{ + RIFTPHASE_INACTIVE = 0, ///< slow death not begun yet + RIFTPHASE_CHARGE, ///< initial delay; charge FX only, nothing is pulled yet + RIFTPHASE_RAMPUP, ///< strength climbs 0 -> 1 + RIFTPHASE_MAIN, ///< full strength + RIFTPHASE_FADEOUT, ///< strength falls 1 -> 0 + RIFTPHASE_DONE ///< collapsed; waiting for the base class to destroy us +}; + +// ------------------------------------------------------------------------------------------------ +/** State we keep for each object the rift currently holds. We cannot re-derive this from the + * world each frame: the capture flags have to be undone on release, the one-shot setup must not + * re-fire, and objects without a PhysicsBehavior have nowhere else to store their velocity. */ +// ------------------------------------------------------------------------------------------------ +struct RiftVictim +{ + enum + { + RIFTSAVE_WAS_DISABLED_FREEFALL = 0x01, ///< victim was already in freefall before we grabbed it + RIFTSAVE_WAS_IN_FREEFALL = 0x02, ///< physics IS_IN_FREEFALL was already set + RIFTSAVE_USES_LOCOMOTOR = 0x04 ///< flyer we steer via its own locomotor + }; + + ObjectID m_id; + Coord3D m_kinematicVel; ///< only used when the victim has no PhysicsBehavior + Int m_spinSign; ///< +1/-1, drawn once at capture + UnsignedByte m_savedFlags; ///< RIFTSAVE_* bits +}; // ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------ @@ -19,15 +57,45 @@ class RiftSlowDeathBehaviorModuleData : public SlowDeathBehaviorModuleData static void buildFieldParse(MultiIniFieldParse& p); - UnsignedInt m_initialDelay; ///< Frames before effect starts - UnsignedInt m_rampupTime; ///< Frames till full effect - UnsignedInt m_mainDuration; ///< Frames with full effect after rampupTime - UnsignedInt m_fadeOutTime; ///< Frames till zero effect after mainDuration - Real m_radius; ///< Radius of the effect - Real m_damage; ///< damage applied per tick - const FXList* m_FXmain; ///< FX played at start of main duration - const FXList* m_FXcharge; ///< FX played at start - const FXList* m_FXfinal; ///< FX played at final push + UnsignedInt m_initialDelay; ///< Frames before effect starts + UnsignedInt m_rampupTime; ///< Frames till full effect + UnsignedInt m_mainDuration; ///< Frames with full effect after rampupTime + UnsignedInt m_fadeOutTime; ///< Frames till zero effect after mainDuration + + Real m_radius; ///< Radius of the effect + Real m_eventHorizonRadius; ///< Inside this radius victims are consumed; 0 disables + Real m_liftHeight; ///< How far above the ground the rift center sits + + Real m_pullAcceleration; ///< Inward suction, dist per frame squared + Real m_swirlFactor; ///< Bends the pull sideways; 0 falls straight in, ~1 orbits. + ///< Deliberately a direction bias and not a separate thrust: a + ///< tangential force would pump energy in and spiral victims out. + Real m_pullDamping; ///< Fraction of velocity bled off per frame. Without this the + ///< inward speed grows without bound and victims slingshot clear. + Real m_liftAcceleration; ///< Vertical acceleration toward the rift plane + Real m_maxPullSpeed; ///< Speed cap for held victims, dist per frame; 0 is uncapped + Real m_initialLiftVelocity; ///< Upward kick on capture, dist per frame + Bool m_cancelGravity; ///< Counteract world gravity while a victim is held + Real m_spinRate; ///< Tumble budget, rads per frame + Real m_hugeVehiclePullScalar; ///< Suction multiplier for KINDOF_HUGE_VEHICLE + + Real m_damagePerSecond; ///< Damage dealt per second of exposure at full strength + UnsignedInt m_damageInterval; ///< Frames between damage ticks + DamageType m_damageType; ///< Damage type used for the continuous damage + DeathType m_consumeDeathType; ///< Death type used when a victim crosses the event horizon + + Real m_finalPushForce; ///< Shockwave amount of the collapse blast + Real m_finalPushSpeed; ///< Outward velocity given to held victims on collapse + DeathType m_finalPushDeathType; ///< Death type of the collapse blast + + KindOfMaskType m_pullKindOf; ///< If any bit is set, only these kinds are pulled + KindOfMaskType m_noPullKindOf; ///< These kinds are never pulled + + const FXList* m_FXmain; ///< FX played at the start of the rampup + const FXList* m_FXcharge; ///< FX played at activation + const FXList* m_FXfinal; ///< FX played on collapse + const FXList* m_FXconsume; ///< FX played on each victim crossing the event horizon + const ObjectCreationList* m_OCLconsume; ///< OCL fired on each victim crossing the event horizon }; // ------------------------------------------------------------------------------------------------ @@ -47,13 +115,32 @@ class RiftSlowDeathBehavior : public SlowDeathBehavior protected: - void applyPull(float magnitude_multiplier) const; - void doFinalPush() const; + void onActivated(UnsignedInt currFrame); ///< lock in the rift position and phase timings + RiftPhaseType computePhase(UnsignedInt currFrame, Real* strength) const; + void onPhaseEnter(RiftPhaseType phase); ///< one-shot effects for a phase transition + + void doRiftTick(Real strength, UnsignedInt currFrame); ///< one pass over everything in range + void doCollapse(void); ///< release the held victims and blow them outwards + + Bool isPullable(const Object* victim) const; ///< can this object be dragged around at all + Bool usesLocomotorCapture(const Object* victim) const; ///< flyer whose locomotor keeps running while disabled + RiftVictim* findVictim(ObjectID id); ///< NULL if we are not holding it + void captureVictim(Object* victim); ///< one-shot setup when a victim is first grabbed + void steerVictim(RiftVictim& victim, Object* obj, const Coord3D& toRift, Real dist, Real strength); + void releaseVictim(const RiftVictim& victim, Object* obj); ///< undo what capture cannot self-heal + void releaseAllVictims(void); + + UnsignedInt m_activationFrame; ///< frame we were activated on + UnsignedInt m_chargeEndFrame; ///< absolute frame the rampup begins + UnsignedInt m_rampEndFrame; ///< absolute frame the main phase begins + UnsignedInt m_mainEndFrame; ///< absolute frame the fade out begins + UnsignedInt m_fadeEndFrame; ///< absolute frame the rift collapses + UnsignedInt m_nextDamageFrame; ///< absolute frame of the next damage tick + + Coord3D m_riftPos; ///< Position where the rift sits, locked in at activation + Int m_phase; ///< current RiftPhaseType - UnsignedInt m_activationFrame; ///< frame we were activated on - Coord3D m_riftPos; ///< Position where the rift starts, center of mass - bool m_finalPush; ///< final push happened - bool m_playedMainFX; + std::vector m_victims; ///< objects we currently hold }; -#endif // end __RIFTSlowDeathBehavior_H_ \ No newline at end of file +#endif // end __RIFTSlowDeathBehavior_H_ diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/RiftSlowDeathUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/RiftSlowDeathUpdate.cpp index bcc4557821b..84b7b5e406d 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/RiftSlowDeathUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/RiftSlowDeathUpdate.cpp @@ -1,12 +1,23 @@ +#define DEFINE_DEATH_NAMES + #include "GameLogic/Module/RiftSlowDeathUpdate.h" +#include "Common/GlobalData.h" +#include "Common/Xfer.h" #include "GameLogic/GameLogic.h" +#include "GameLogic/Locomotor.h" #include "GameLogic/Object.h" #include "GameLogic/ObjectIter.h" +#include "GameLogic/ObjectCreationList.h" #include "GameLogic/PartitionManager.h" -#include "GameLogic/Module/ToppleUpdate.h" #include "GameLogic/TerrainLogic.h" -#include "GameClient/FXList.h" +#include "GameLogic/Module/AIUpdate.h" #include "GameLogic/Module/PhysicsUpdate.h" +#include "GameClient/FXList.h" + +// A victim that has drifted a little past the rim is not dropped immediately; without this +// hysteresis an object orbiting the edge would capture and release every other frame, re-firing +// the one-shot setup and burning logic random values (which would desync). +static const Real RIFT_RELEASE_SLOP = 1.15f; RiftSlowDeathBehaviorModuleData::RiftSlowDeathBehaviorModuleData(void) { @@ -14,11 +25,44 @@ RiftSlowDeathBehaviorModuleData::RiftSlowDeathBehaviorModuleData(void) m_rampupTime = 0; m_mainDuration = 0; m_fadeOutTime = 0; + m_radius = 0.0f; - m_damage = 0.0f; + m_eventHorizonRadius = 0.0f; + m_liftHeight = 100.0f; + + m_pullAcceleration = 0.0f; + m_swirlFactor = 0.0f; + m_pullDamping = 0.12f; + m_liftAcceleration = 0.0f; + m_maxPullSpeed = 0.0f; + m_initialLiftVelocity = 0.0f; + m_cancelGravity = TRUE; + m_spinRate = 0.0f; + m_hugeVehiclePullScalar = 1.0f; + + m_damagePerSecond = 0.0f; + m_damageInterval = LOGICFRAMES_PER_SECOND; + m_damageType = DAMAGE_EXPLOSION; + m_consumeDeathType = DEATH_EXPLODED; + + m_finalPushForce = 0.0f; + m_finalPushSpeed = 0.0f; + m_finalPushDeathType = DEATH_SPLATTED; + + // things that have no business being dragged around by a gravity well + m_noPullKindOf.set(KINDOF_PROJECTILE); + m_noPullKindOf.set(KINDOF_SMALL_MISSILE); + m_noPullKindOf.set(KINDOF_BALLISTIC_MISSILE); + m_noPullKindOf.set(KINDOF_INERT); + m_noPullKindOf.set(KINDOF_DRAWABLE_ONLY); + m_noPullKindOf.set(KINDOF_OPTIMIZED_TREE); + m_noPullKindOf.set(KINDOF_MINE); + m_FXmain = NULL; m_FXcharge = NULL; m_FXfinal = NULL; + m_FXconsume = NULL; + m_OCLconsume = NULL; } // end RiftSlowDeathBehaviorModuleData // ------------------------------------------------------------------------------------------------ @@ -30,15 +74,43 @@ RiftSlowDeathBehaviorModuleData::RiftSlowDeathBehaviorModuleData(void) static const FieldParse dataFieldParse[] = { - { "InitialDelay", INI::parseDurationUnsignedInt, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_initialDelay) }, - { "RampupTime", INI::parseDurationUnsignedInt, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_rampupTime) }, - { "MainDuration", INI::parseDurationUnsignedInt, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_mainDuration) }, - { "FadeOutTime", INI::parseDurationUnsignedInt, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_fadeOutTime) }, - { "Radius", INI::parseReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_radius) }, - { "Damage", INI::parseReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_damage) }, - { "FXMain", INI::parseFXList, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_FXmain) }, - { "FXCharge", INI::parseFXList, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_FXcharge) }, - { "FXFinal", INI::parseFXList, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_FXfinal) }, + { "InitialDelay", INI::parseDurationUnsignedInt, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_initialDelay) }, + { "RampupTime", INI::parseDurationUnsignedInt, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_rampupTime) }, + { "MainDuration", INI::parseDurationUnsignedInt, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_mainDuration) }, + { "FadeOutTime", INI::parseDurationUnsignedInt, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_fadeOutTime) }, + + { "Radius", INI::parseReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_radius) }, + { "EventHorizonRadius", INI::parseReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_eventHorizonRadius) }, + { "LiftHeight", INI::parseReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_liftHeight) }, + + { "PullAcceleration", INI::parseAccelerationReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_pullAcceleration) }, + { "SwirlFactor", INI::parseReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_swirlFactor) }, + { "PullDamping", INI::parsePercentToReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_pullDamping) }, + { "LiftAcceleration", INI::parseAccelerationReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_liftAcceleration) }, + { "MaxPullSpeed", INI::parseVelocityReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_maxPullSpeed) }, + { "InitialLiftVelocity", INI::parseVelocityReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_initialLiftVelocity) }, + { "CancelGravity", INI::parseBool, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_cancelGravity) }, + { "SpinRate", INI::parseAngularVelocityReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_spinRate) }, + { "HugeVehiclePullScalar",INI::parseReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_hugeVehiclePullScalar) }, + + { "DamagePerSecond", INI::parseReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_damagePerSecond) }, + { "DamageInterval", INI::parseDurationUnsignedInt, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_damageInterval) }, + { "DamageType", INI::parseIndexList, DamageTypeFlags::s_bitNameList, offsetof(RiftSlowDeathBehaviorModuleData, m_damageType) }, + { "ConsumeDeathType", INI::parseIndexList, TheDeathNames, offsetof(RiftSlowDeathBehaviorModuleData, m_consumeDeathType) }, + + { "FinalPushForce", INI::parseReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_finalPushForce) }, + { "FinalPushSpeed", INI::parseVelocityReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_finalPushSpeed) }, + { "FinalPushDeathType", INI::parseIndexList, TheDeathNames, offsetof(RiftSlowDeathBehaviorModuleData, m_finalPushDeathType) }, + + { "PullKindOf", KindOfMaskType::parseFromINI, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_pullKindOf) }, + { "NoPullKindOf", KindOfMaskType::parseFromINI, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_noPullKindOf) }, + + { "FXMain", INI::parseFXList, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_FXmain) }, + { "FXCharge", INI::parseFXList, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_FXcharge) }, + { "FXFinal", INI::parseFXList, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_FXfinal) }, + { "FXConsume", INI::parseFXList, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_FXconsume) }, + { "OCLConsume", INI::parseObjectCreationList, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_OCLconsume) }, + { 0, 0, 0, 0 } }; @@ -56,11 +128,13 @@ RiftSlowDeathBehavior::RiftSlowDeathBehavior(Thing* thing, const ModuleData* mod : SlowDeathBehavior(thing, moduleData) { m_activationFrame = 0; - m_riftPos.x = 0.0f; - m_riftPos.y = 0.0f; - m_riftPos.z = 0.0f; - m_finalPush = false; - m_playedMainFX = false; + m_chargeEndFrame = 0; + m_rampEndFrame = 0; + m_mainEndFrame = 0; + m_fadeEndFrame = 0; + m_nextDamageFrame = 0; + m_riftPos.zero(); + m_phase = RIFTPHASE_INACTIVE; } // end RiftSlowDeathBehavior // ------------------------------------------------------------------------------------------------ @@ -68,16 +142,16 @@ RiftSlowDeathBehavior::RiftSlowDeathBehavior(Thing* thing, const ModuleData* mod RiftSlowDeathBehavior::~RiftSlowDeathBehavior(void) { + // last line of defense: if we are torn down while still holding victims, hand them back + if (TheGameLogic != NULL) + releaseAllVictims(); + } // end ~RiftSlowDeathBehavior // ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------ UpdateSleepTime RiftSlowDeathBehavior::update(void) { - /// @todo srj use SLEEPY_UPDATE here - // get the module data - const RiftSlowDeathBehaviorModuleData* modData = getRiftSlowDeathBehaviorModuleData(); - // call the base class cause we're extending functionality SlowDeathBehavior::update(); @@ -85,185 +159,689 @@ UpdateSleepTime RiftSlowDeathBehavior::update(void) if (isSlowDeathActivated() == FALSE) return UPDATE_SLEEP_NONE; - // get the current frame UnsignedInt currFrame = TheGameLogic->getFrame(); - // when we become activated we want to do a few things - if (m_activationFrame == 0) + if (m_phase == RIFTPHASE_INACTIVE) + onActivated(currFrame); + + Real strength = 0.0f; + RiftPhaseType newPhase = computePhase(currFrame, &strength); + if ((Int)newPhase != m_phase) + { + m_phase = newPhase; + onPhaseEnter(newPhase); + } + + if (m_phase == RIFTPHASE_RAMPUP || m_phase == RIFTPHASE_MAIN || m_phase == RIFTPHASE_FADEOUT) + doRiftTick(strength, currFrame); + + return UPDATE_SLEEP_NONE; + +} // end update + +// ------------------------------------------------------------------------------------------------ +/** Lock in where the rift sits and when each phase ends. Everything downstream works off these + * absolute frames, so the timings never drift and survive a save/load. */ +// ------------------------------------------------------------------------------------------------ +void RiftSlowDeathBehavior::onActivated(UnsignedInt currFrame) +{ + const RiftSlowDeathBehaviorModuleData* d = getRiftSlowDeathBehaviorModuleData(); + + const Coord3D* pos = getObject()->getPosition(); + m_riftPos.x = pos->x; + m_riftPos.y = pos->y; + m_riftPos.z = TheTerrainLogic->getGroundHeight(m_riftPos.x, m_riftPos.y) + d->m_liftHeight; + + m_activationFrame = currFrame; + m_chargeEndFrame = currFrame + d->m_initialDelay; + m_rampEndFrame = m_chargeEndFrame + d->m_rampupTime; + m_mainEndFrame = m_rampEndFrame + d->m_mainDuration; + m_fadeEndFrame = m_mainEndFrame + d->m_fadeOutTime; + m_nextDamageFrame = m_chargeEndFrame; + + m_phase = RIFTPHASE_CHARGE; + onPhaseEnter(RIFTPHASE_CHARGE); + + // the base class destroys us on its own schedule; if that lands early the rift is cut short + DEBUG_ASSERTCRASH(m_fadeEndFrame <= getDestructionFrame(), + ("RiftSlowDeathBehavior: DestructionDelay is shorter than the rift phases; the rift will be cut off")); + +} // end onActivated + +// ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ +RiftPhaseType RiftSlowDeathBehavior::computePhase(UnsignedInt currFrame, Real* strength) const +{ + *strength = 0.0f; + + if (currFrame < m_chargeEndFrame) + return RIFTPHASE_CHARGE; + + if (currFrame < m_rampEndFrame) + { + // guarded: a zero length phase would divide by zero and poison every position with a NaN + UnsignedInt span = m_rampEndFrame - m_chargeEndFrame; + if (span > 0) + *strength = (Real)(currFrame - m_chargeEndFrame) / (Real)span; + return RIFTPHASE_RAMPUP; + } + + if (currFrame < m_mainEndFrame) + { + *strength = 1.0f; + return RIFTPHASE_MAIN; + } + + if (currFrame < m_fadeEndFrame) + { + UnsignedInt span = m_fadeEndFrame - m_mainEndFrame; + if (span > 0) + *strength = 1.0f - ((Real)(currFrame - m_mainEndFrame) / (Real)span); + return RIFTPHASE_FADEOUT; + } + + return RIFTPHASE_DONE; + +} // end computePhase + +// ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ +void RiftSlowDeathBehavior::onPhaseEnter(RiftPhaseType phase) +{ + const RiftSlowDeathBehaviorModuleData* d = getRiftSlowDeathBehaviorModuleData(); + + switch (phase) + { + case RIFTPHASE_CHARGE: + FXList::doFXPos(d->m_FXcharge, &m_riftPos); + break; + + case RIFTPHASE_RAMPUP: + FXList::doFXPos(d->m_FXmain, &m_riftPos); + break; + + case RIFTPHASE_DONE: + doCollapse(); + break; + + default: + break; + } + +} // end onPhaseEnter + +// ------------------------------------------------------------------------------------------------ +/** One pass over everything in range: damage on its own cadence, then suction for whatever we are + * allowed to move. */ +// ------------------------------------------------------------------------------------------------ +void RiftSlowDeathBehavior::doRiftTick(Real strength, UnsignedInt currFrame) +{ + const RiftSlowDeathBehaviorModuleData* d = getRiftSlowDeathBehaviorModuleData(); + Object* self = getObject(); + + Bool damageThisFrame = FALSE; + Real damageAmount = 0.0f; + if (d->m_damagePerSecond > 0.0f && currFrame >= m_nextDamageFrame) + { + UnsignedInt interval = d->m_damageInterval > 0 ? d->m_damageInterval : 1; + damageAmount = d->m_damagePerSecond * ((Real)interval / (Real)LOGICFRAMES_PER_SECOND) * strength; + m_nextDamageFrame = currFrame + interval; + damageThisFrame = TRUE; + } + + DamageInfo damageInfo; + damageInfo.in.m_damageType = d->m_damageType; + damageInfo.in.m_deathType = d->m_consumeDeathType; + damageInfo.in.m_sourceID = self->getID(); + damageInfo.in.m_amount = damageAmount; + // no shockwave here: we move victims through physics directly, and attemptDamage refuses to + // shove anything airborne anyway, which is exactly what we have lifted + damageInfo.in.m_shockWaveAmount = 0.0f; + damageInfo.in.m_shockWaveVector.zero(); + + PartitionFilterAlive filterAlive; + PartitionFilterOnMap filterOnMap; + PartitionFilter* filters[] = { &filterAlive, &filterOnMap, NULL }; + + // 2D range, so Radius keeps meaning "the circle on the ground" that the targeting reticle shows + ObjectIterator* iter = ThePartitionManager->iterateObjectsInRange(&m_riftPos, + d->m_radius, FROM_CENTER_2D, filters); + MemoryPoolObjectHolder hold(iter); + + // consuming kills objects, so collect them and deal with them once the iterator is released + std::vector consumed; + std::vector inRange; + + for (Object* victim = iter->first(); victim != NULL; victim = iter->next()) { - const Coord3D* projectilePos = getObject()->getPosition(); + if (victim == self) + continue; - // Lock in position when activated - m_riftPos.x = projectilePos->x; - m_riftPos.y = projectilePos->y; - m_riftPos.z = TheTerrainLogic->getGroundHeight(m_riftPos.x, m_riftPos.y) + 100.0f; + if (victim->isEffectivelyDead()) + continue; - // record the frame - m_activationFrame = currFrame; + // passengers ride along with whatever is carrying them + if (victim->getContainedBy() != NULL) + continue; - FXList::doFXPos(modData->m_FXcharge, &m_riftPos); - //DEBUG_LOG(("Rift Position(%f, %f, %f)\n", m_riftPos.x, m_riftPos.y, m_riftPos.z)); - //DEBUG_LOG(("Rift Frame Start: %d\n", m_activationFrame)); - } // end if + if (damageThisFrame) + victim->attemptDamage(&damageInfo); - UnsignedInt frameProgress = currFrame - m_activationFrame; + if (!isPullable(victim)) + continue; + + Coord3D toRift; + toRift.x = m_riftPos.x - victim->getPosition()->x; + toRift.y = m_riftPos.y - victim->getPosition()->y; + toRift.z = m_riftPos.z - victim->getPosition()->z; + Real dist = toRift.length(); + + if (d->m_eventHorizonRadius > 0.0f && dist <= d->m_eventHorizonRadius) + { + consumed.push_back(victim->getID()); + continue; + } - if (frameProgress < modData->m_initialDelay) { - // Charging Up + RiftVictim* held = findVictim(victim->getID()); + if (held == NULL) + { + captureVictim(victim); + held = findVictim(victim->getID()); + } - //DEBUG_LOG(("RIFT Initial: %d\n", frameProgress)); + if (held != NULL) + { + inRange.push_back(victim->getID()); + steerVictim(*held, victim, toRift, dist, strength); + } } - else if (frameProgress < modData->m_initialDelay + modData->m_rampupTime) { - // rampup phase - float rampup = static_cast(frameProgress - modData->m_initialDelay) / static_cast(modData->m_rampupTime); - - if (!m_playedMainFX) { - FXList::doFXPos(modData->m_FXmain, &m_riftPos); - m_playedMainFX = true; + + // drop anything we were holding that is gone, dead, or has drifted well clear of the rim + Real releaseRadiusSqr = (d->m_radius * RIFT_RELEASE_SLOP) * (d->m_radius * RIFT_RELEASE_SLOP); + for (Int i = (Int)m_victims.size() - 1; i >= 0; --i) + { + const RiftVictim& v = m_victims[i]; + + Bool keep = FALSE; + for (std::vector::const_iterator it = inRange.begin(); it != inRange.end(); ++it) + { + if (*it == v.m_id) + { + keep = TRUE; + break; + } + } + if (keep) + continue; + + Object* obj = TheGameLogic->findObjectByID(v.m_id); + if (obj != NULL && !obj->isEffectivelyDead()) + { + // still alive: only let go once it is properly clear, so we don't chatter at the rim + Coord3D away; + away.x = obj->getPosition()->x - m_riftPos.x; + away.y = obj->getPosition()->y - m_riftPos.y; + away.z = 0.0f; + if (away.lengthSqr() <= releaseRadiusSqr) + continue; + + releaseVictim(v, obj); } - applyPull(rampup); - //DEBUG_LOG(("RIFT Charge: %d\n", frameProgress)); + m_victims.erase(m_victims.begin() + i); } - else if (frameProgress < modData->m_initialDelay + modData->m_rampupTime + modData->m_mainDuration) { - // main damage phase - - if (!m_playedMainFX) { - FXList::doFXPos(modData->m_FXmain, &m_riftPos); - m_playedMainFX = true; + + // now that the iterator is gone it is safe to destroy things + for (std::vector::const_iterator it = consumed.begin(); it != consumed.end(); ++it) + { + Object* victim = TheGameLogic->findObjectByID(*it); + if (victim == NULL) + continue; + + for (Int i = (Int)m_victims.size() - 1; i >= 0; --i) + { + if (m_victims[i].m_id == *it) + { + m_victims.erase(m_victims.begin() + i); + break; + } } - applyPull(1.0f); - //DEBUG_LOG(("RIFT Main: %d\n", frameProgress)); + FXList::doFXPos(d->m_FXconsume, victim->getPosition()); + ObjectCreationList::create(d->m_OCLconsume, self, victim->getPosition(), NULL, 0.0f); + victim->kill(DAMAGE_UNRESISTABLE, d->m_consumeDeathType); + } + +} // end doRiftTick + +// ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ +Bool RiftSlowDeathBehavior::isPullable(const Object* victim) const +{ + const RiftSlowDeathBehaviorModuleData* d = getRiftSlowDeathBehaviorModuleData(); + + // anything nailed down only ever takes damage + if (victim->isKindOf(KINDOF_IMMOBILE) + || victim->isKindOf(KINDOF_STRUCTURE) + || victim->isKindOf(KINDOF_BRIDGE) + || victim->isKindOf(KINDOF_BRIDGE_TOWER)) + return FALSE; + + if (victim->isAnyKindOf(d->m_noPullKindOf)) + return FALSE; + + if (KINDOFMASK_ANY_SET(d->m_pullKindOf) && !victim->isAnyKindOf(d->m_pullKindOf)) + return FALSE; + + return TRUE; + +} // end isPullable + +// ------------------------------------------------------------------------------------------------ +/** Flyers whose locomotor template is flagged LocomotorWorksWhenDisabled keep steering themselves + * even while disabled, and their handleBehaviorZ rewrites the z position every frame. Fighting + * that is a losing game, so those victims get steered through their own locomotor instead. */ +// ------------------------------------------------------------------------------------------------ +Bool RiftSlowDeathBehavior::usesLocomotorCapture(const Object* victim) const +{ + const AIUpdateInterface* ai = victim->getAIUpdateInterface(); + if (ai == NULL) + return FALSE; + + const Locomotor* loco = ai->getCurLocomotor(); + return loco != NULL && loco->getLocomotorWorksWhenDisabled(); + +} // end usesLocomotorCapture + +// ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ +RiftVictim* RiftSlowDeathBehavior::findVictim(ObjectID id) +{ + for (std::vector::iterator it = m_victims.begin(); it != m_victims.end(); ++it) + { + if (it->m_id == id) + return &(*it); } - else if (frameProgress < modData->m_initialDelay + modData->m_rampupTime + modData->m_mainDuration + modData->m_fadeOutTime) { - // fade out phase - float fadeout = 1.0f - (static_cast(frameProgress - (modData->m_initialDelay + modData->m_rampupTime + modData->m_mainDuration)) / static_cast(modData->m_fadeOutTime)); + return NULL; + +} // end findVictim + +// ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ +void RiftSlowDeathBehavior::captureVictim(Object* victim) +{ + const RiftSlowDeathBehaviorModuleData* d = getRiftSlowDeathBehaviorModuleData(); + + RiftVictim v; + v.m_id = victim->getID(); + v.m_kinematicVel.zero(); + v.m_spinSign = GameLogicRandomValue(0, 1) ? 1 : -1; + v.m_savedFlags = 0; + + PhysicsBehavior* physics = victim->getPhysics(); - applyPull(fadeout); - //DEBUG_LOG(("RIFT Fade: %d\n", frameProgress)); + if (usesLocomotorCapture(victim)) + { + // leave the flyer's AI running; steerVictim drives its preferred height instead + v.m_savedFlags |= RiftVictim::RIFTSAVE_USES_LOCOMOTOR; } - else { - // do final push, - if (!m_finalPush) { - FXList::doFXPos(modData->m_FXfinal, &m_riftPos); - doFinalPush(); - m_finalPush = true; + else + { + // DISABLED_FREEFALL is the only disable that suspends the AI while leaving physics running + // (AIUpdate tolerates DISABLED_HELD only), so it is what hands the object over to us. + if (victim->isDisabledByType(DISABLED_FREEFALL)) + v.m_savedFlags |= RiftVictim::RIFTSAVE_WAS_DISABLED_FREEFALL; + victim->setDisabled(DISABLED_FREEFALL); + + if (physics != NULL) + { + if (physics->getIsInFreeFall()) + v.m_savedFlags |= RiftVictim::RIFTSAVE_WAS_IN_FREEFALL; + + physics->setStickToGround(FALSE); + physics->setAllowToFall(TRUE); // stop physics clamping z to the terrain + physics->setIsInFreeFall(TRUE); // physics then re-asserts the disable for us each frame + physics->setAllowBouncing(FALSE); + physics->setAllowCollideForce(FALSE); + physics->setImmuneToFallingDamage(TRUE); + physics->setStunned(TRUE); + victim->setModelConditionState(MODELCONDITION_STUNNED_FLAILING); + + if (d->m_initialLiftVelocity > 0.0f) + { + Coord3D kick; + kick.x = 0.0f; + kick.y = 0.0f; + kick.z = d->m_initialLiftVelocity; + physics->addVelocityTo(&kick); + } } - //DEBUG_LOG(("RIFT FINAL: %d\n", frameProgress)); } - - return UPDATE_SLEEP_NONE; + if (physics != NULL) + { + // a parked object's physics is asleep and addVelocityTo does not wake it, so poke it with + // a zero force, which does + Coord3D zero; + zero.zero(); + physics->applyForce(&zero); + } -} // end update + m_victims.push_back(v); -void RiftSlowDeathBehavior::applyPull(float magnitude_multiplier) const +} // end captureVictim + +// ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ +void RiftSlowDeathBehavior::steerVictim(RiftVictim& victim, Object* obj, const Coord3D& toRift, + Real dist, Real strength) { - const RiftSlowDeathBehaviorModuleData* data = getRiftSlowDeathBehaviorModuleData(); + const RiftSlowDeathBehaviorModuleData* d = getRiftSlowDeathBehaviorModuleData(); - DamageInfo damageInfo; - damageInfo.in.m_damageType = DAMAGE_EXPLOSION; - damageInfo.in.m_deathType = DEATH_EXPLODED; - damageInfo.in.m_sourceID = getObject()->getID(); - damageInfo.in.m_amount = data->m_damage * magnitude_multiplier; - damageInfo.in.m_shockWaveVector.zero(); - damageInfo.in.m_shockWaveAmount = 0.5f; - damageInfo.in.m_shockWaveRadius = data->m_radius; - damageInfo.in.m_shockWaveTaperOff = 1.0f; + Real falloff = 1.0f; + if (d->m_radius > 0.0f) + { + falloff = 1.0f - (dist / d->m_radius); + if (falloff < 0.0f) + falloff = 0.0f; + else if (falloff > 1.0f) + falloff = 1.0f; + } - // scan objects in our region - ObjectIterator* iter = ThePartitionManager->iterateObjectsInRange(&m_riftPos, - data->m_radius, - FROM_CENTER_2D, NULL); + Real scale = strength * falloff; + if (obj->isKindOf(KINDOF_HUGE_VEHICLE)) + scale *= d->m_hugeVehiclePullScalar; + + // horizontal direction toward the rift, and the tangent that turns the fall into a spiral + Coord3D inward; + inward.x = toRift.x; + inward.y = toRift.y; + inward.z = 0.0f; + Real dist2D = inward.length(); + if (dist2D > 0.001f) + { + inward.x /= dist2D; + inward.y /= dist2D; + } + else + { + inward.zero(); + } - MemoryPoolObjectHolder hold(iter); + // Bend the pull sideways rather than adding a separate tangential thrust. Rotating a + // fixed-length vector cannot inject energy, so victims spiral in; a tangential force would + // raise their orbit every frame and eventually fling them clear of the radius. + // Fixed handedness: one coherent vortex reads as a single object, mixed directions read as noise. + Coord3D dir; + dir.x = inward.x - inward.y * d->m_swirlFactor; + dir.y = inward.y + inward.x * d->m_swirlFactor; + dir.z = 0.0f; + Real dirLen = dir.length(); + if (dirLen > 0.001f) + { + dir.x /= dirLen; + dir.y /= dirLen; + } + + Coord3D accel; + accel.x = dir.x * d->m_pullAcceleration * scale; + accel.y = dir.y * d->m_pullAcceleration * scale; + accel.z = 0.0f; + + // vertical: ease off as the victim reaches the rift plane, and push down on anything above it + Real liftZone = d->m_liftHeight > 0.0f ? d->m_liftHeight : 1.0f; + Real zSpring = toRift.z / liftZone; + if (zSpring < -1.0f) + zSpring = -1.0f; + else if (zSpring > 1.0f) + zSpring = 1.0f; + accel.z = d->m_liftAcceleration * scale * zSpring; + + PhysicsBehavior* physics = obj->getPhysics(); + + if ((victim.m_savedFlags & RiftVictim::RIFTSAVE_USES_LOCOMOTOR) != 0) + { + // hand the altitude to the flyer's own flight model; it will fly itself into the rift + AIUpdateInterface* ai = obj->getAIUpdateInterface(); + Locomotor* loco = (ai != NULL) ? ai->getCurLocomotor() : NULL; + if (loco != NULL) + { + // preferred height is relative to the surface for the locomotor modes flyers use + Real surfaceHeight = TheTerrainLogic->getGroundHeight(obj->getPosition()->x, obj->getPosition()->y); + Real wanted = m_riftPos.z - surfaceHeight; + if (wanted < 0.0f) + wanted = 0.0f; + loco->setPreferredHeight(wanted); + } + // the flyer handles its own z, so only drag it sideways + accel.z = 0.0f; + } + else if (physics != NULL && d->m_cancelGravity) + { + // gravity is added unconditionally every frame, so cancel it or LiftAcceleration has to + // out-muscle it before any of the tuning numbers mean anything + accel.z -= TheGlobalData->m_gravity * scale; + } + + if (physics != NULL) + { + // addVelocityTo can only add, so work out the clamped delta ourselves + Coord3D newVel = *physics->getVelocity(); + + // Viscous drag first. This is what bounds the whole system: terminal speed settles at + // roughly pullAcceleration/pullDamping instead of growing every frame, so a victim cannot + // build up enough momentum to shoot through the middle and escape the far side. + if (d->m_pullDamping > 0.0f) + { + Real drag = d->m_pullDamping * scale; + if (drag > 1.0f) + drag = 1.0f; + newVel.x -= newVel.x * drag; + newVel.y -= newVel.y * drag; + // z needs it too: with gravity cancelled the lift term is otherwise a lossless spring, + // so victims would bob around the rift plane forever instead of settling into it + newVel.z -= newVel.z * drag; + } + + newVel.add(accel); + + if (d->m_maxPullSpeed > 0.0f) + { + Real speed = newVel.length(); + if (speed > d->m_maxPullSpeed) + newVel.scale(d->m_maxPullSpeed / speed); + } - for (Object* currentObj = iter->first(); currentObj != NULL; currentObj = iter->next()) + Coord3D delta; + delta.x = newVel.x - physics->getVelocity()->x; + delta.y = newVel.y - physics->getVelocity()->y; + delta.z = newVel.z - physics->getVelocity()->z; + physics->addVelocityTo(&delta); + + if ((victim.m_savedFlags & RiftVictim::RIFTSAVE_USES_LOCOMOTOR) == 0) + { + // physics clears these when the victim touches down, so keep re-asserting them + physics->setStickToGround(FALSE); + physics->setAllowToFall(TRUE); + physics->setIsInFreeFall(TRUE); + obj->setDisabled(DISABLED_FREEFALL); + + if (d->m_spinRate > 0.0f) + { + Real spin = d->m_spinRate * scale * (Real)victim.m_spinSign; + physics->setYawRate(spin); + physics->setPitchRate(spin * 0.5f); + physics->setRollRate(spin * 0.5f); + } + } + } + else { - // get other position - const Coord3D * objPos = currentObj->getPosition(); - - // compute vector from the object to the rift center - Coord3D forceVector; - forceVector.x = m_riftPos.x - objPos->x; - forceVector.y = m_riftPos.y - objPos->y; - forceVector.z = m_riftPos.z - objPos->z; - - /*if (objPos->z > m_riftPos.z) { - // avoid overshooting - forceVector.z = -1.0f; - }*/ - damageInfo.in.m_shockWaveVector = forceVector; - //Real distance = forceVector.length(); - //Real pull_strength = (distance / data->m_radius) * 0.5f + 0.5f; - - if (!currentObj->isKindOf(KINDOF_IMMOBILE) && !currentObj->isKindOf(KINDOF_PROJECTILE) && !currentObj->isKindOf(KINDOF_INERT) && objPos->length() > 3.0f*magnitude_multiplier) { - Coord3D offset = forceVector; - offset.normalize(); - offset.scale(3.0f * magnitude_multiplier); - offset.x += objPos->x; - offset.y += objPos->y; - offset.z += objPos->z; - currentObj->setPosition(&offset); + // no physics module, so integrate the victim ourselves + if (d->m_pullDamping > 0.0f) + { + Real drag = d->m_pullDamping * scale; + if (drag > 1.0f) + drag = 1.0f; + victim.m_kinematicVel.x -= victim.m_kinematicVel.x * drag; + victim.m_kinematicVel.y -= victim.m_kinematicVel.y * drag; + victim.m_kinematicVel.z -= victim.m_kinematicVel.z * drag; + } + + victim.m_kinematicVel.add(accel); + if (d->m_maxPullSpeed > 0.0f) + { + Real speed = victim.m_kinematicVel.length(); + if (speed > d->m_maxPullSpeed) + victim.m_kinematicVel.scale(d->m_maxPullSpeed / speed); } - /*PhysicsBehavior* phys = currentObj->getPhysics(); - Real phys_mult = 0.1f; - if (phys != NULL) { - phys_mult = phys->getMass(); - - const Coord3D* current_velocity = phys->getVelocity(); - Coord3D minus_vel; - minus_vel.x = -current_velocity->x; - minus_vel.y = -current_velocity->y; - minus_vel.z = -current_velocity->z; - phys->applyForce(&minus_vel); - }*/ - //pull_strength *= magnitude_multiplier * phys_mult; - //currentObj->topple(&forceVector, pull_strength * 10.0f * magnitude_multiplier, TOPPLE_OPTIONS_NO_BOUNCE | TOPPLE_OPTIONS_NO_FX); - - //damageInfo.in.m_shockWaveAmount = pull_strength * 0.2f; - currentObj->attemptDamage(&damageInfo); + + // setPosition re-aligns KINDOF_STICK_TO_TERRAIN_SLOPE objects back onto the ground, so go + // through the transform directly or props would never leave the terrain + Matrix3D mtx = *obj->getTransformMatrix(); + mtx.Adjust_X_Translation(victim.m_kinematicVel.x); + mtx.Adjust_Y_Translation(victim.m_kinematicVel.y); + mtx.Adjust_Z_Translation(victim.m_kinematicVel.z); + if (d->m_spinRate > 0.0f) + mtx.Rotate_Z(d->m_spinRate * scale * (Real)victim.m_spinSign); + obj->setTransformMatrix(&mtx); + } + +} // end steerVictim + +// ------------------------------------------------------------------------------------------------ +/** Undo the parts of the capture that do not clean themselves up. Physics clears IS_IN_FREEFALL, + * ALLOW_TO_FALL and the stun on its own once the victim lands. */ +// ------------------------------------------------------------------------------------------------ +void RiftSlowDeathBehavior::releaseVictim(const RiftVictim& victim, Object* obj) +{ + PhysicsBehavior* physics = obj->getPhysics(); + + if ((victim.m_savedFlags & RiftVictim::RIFTSAVE_USES_LOCOMOTOR) != 0) + { + AIUpdateInterface* ai = obj->getAIUpdateInterface(); + Locomotor* loco = (ai != NULL) ? ai->getCurLocomotor() : NULL; + if (loco != NULL) + loco->restorePreferredHeightFromTemplate(); + return; + } + + if (physics != NULL) + { + physics->setYawRate(0.0f); + physics->setPitchRate(0.0f); + physics->setRollRate(0.0f); + physics->setAllowCollideForce(TRUE); + physics->setAllowBouncing(TRUE); + physics->setImmuneToFallingDamage(FALSE); + physics->setIsInFreeFall((victim.m_savedFlags & RiftVictim::RIFTSAVE_WAS_IN_FREEFALL) != 0); + // leave allowToFall set: it clears itself on landing, and clearing it now would snap the + // victim straight back down to the terrain + physics->setAllowToFall(TRUE); + } + + if ((victim.m_savedFlags & RiftVictim::RIFTSAVE_WAS_DISABLED_FREEFALL) == 0) + obj->clearDisabled(DISABLED_FREEFALL); + + // the victim is still carrying whatever order it had before we grabbed it + AIUpdateInterface* ai = obj->getAIUpdateInterface(); + if (ai != NULL && !ai->isAiInDeadState()) + ai->aiIdle(CMD_FROM_AI); + +} // end releaseVictim + +// ------------------------------------------------------------------------------------------------ +// ------------------------------------------------------------------------------------------------ +void RiftSlowDeathBehavior::releaseAllVictims(void) +{ + for (std::vector::const_iterator it = m_victims.begin(); it != m_victims.end(); ++it) + { + Object* obj = TheGameLogic->findObjectByID(it->m_id); + if (obj != NULL && !obj->isEffectivelyDead()) + releaseVictim(*it, obj); } -} + m_victims.clear(); + +} // end releaseAllVictims -void RiftSlowDeathBehavior::doFinalPush() const +// ------------------------------------------------------------------------------------------------ +/** The singularity collapses: everything we held is let go and thrown clear, and the whole radius + * takes one outward blast. */ +// ------------------------------------------------------------------------------------------------ +void RiftSlowDeathBehavior::doCollapse(void) { - const RiftSlowDeathBehaviorModuleData* data = getRiftSlowDeathBehaviorModuleData(); + const RiftSlowDeathBehaviorModuleData* d = getRiftSlowDeathBehaviorModuleData(); + Object* self = getObject(); + + FXList::doFXPos(d->m_FXfinal, &m_riftPos); + + // throw the victims we were holding before letting go of them; attemptDamage will not shove + // anything airborne, and airborne is exactly what these are + if (d->m_finalPushSpeed > 0.0f) + { + for (std::vector::const_iterator it = m_victims.begin(); it != m_victims.end(); ++it) + { + Object* obj = TheGameLogic->findObjectByID(it->m_id); + if (obj == NULL || obj->isEffectivelyDead()) + continue; + + PhysicsBehavior* physics = obj->getPhysics(); + if (physics == NULL) + continue; + + Coord3D outward; + outward.x = obj->getPosition()->x - m_riftPos.x; + outward.y = obj->getPosition()->y - m_riftPos.y; + outward.z = 0.0f; + if (outward.lengthSqr() < 0.001f) + outward.x = 1.0f; + outward.normalize(); + outward.scale(d->m_finalPushSpeed); + outward.z = d->m_finalPushSpeed * 0.5f; + + physics->addVelocityTo(&outward); + } + } + + releaseAllVictims(); + + if (d->m_finalPushForce <= 0.0f) + return; DamageInfo damageInfo; - damageInfo.in.m_damageType = DAMAGE_EXPLOSION; - damageInfo.in.m_deathType = DEATH_SPLATTED; - damageInfo.in.m_sourceID = getObject()->getID(); + damageInfo.in.m_damageType = d->m_damageType; + damageInfo.in.m_deathType = d->m_finalPushDeathType; + damageInfo.in.m_sourceID = self->getID(); damageInfo.in.m_amount = 1.0f; - damageInfo.in.m_shockWaveVector.zero(); - damageInfo.in.m_shockWaveAmount = 50.0f; - damageInfo.in.m_shockWaveRadius = data->m_radius; + damageInfo.in.m_shockWaveAmount = d->m_finalPushForce; + damageInfo.in.m_shockWaveRadius = d->m_radius; damageInfo.in.m_shockWaveTaperOff = 0.1f; - // scan objects in our region - ObjectIterator* iter = ThePartitionManager->iterateObjectsInRange(&m_riftPos, - data->m_radius, - FROM_CENTER_2D, NULL); + PartitionFilterAlive filterAlive; + PartitionFilterOnMap filterOnMap; + PartitionFilter* filters[] = { &filterAlive, &filterOnMap, NULL }; + ObjectIterator* iter = ThePartitionManager->iterateObjectsInRange(&m_riftPos, + d->m_radius, FROM_CENTER_2D, filters); MemoryPoolObjectHolder hold(iter); - for (Object* currentObj = iter->first(); currentObj != NULL; currentObj = iter->next()) + for (Object* victim = iter->first(); victim != NULL; victim = iter->next()) { - // get other position - const Coord3D* objPos = currentObj->getPosition(); - - // compute vector from the object to the rift center - Coord3D forceVector; - forceVector.x = objPos->x - m_riftPos.x; - forceVector.y = objPos->y - m_riftPos.y; - forceVector.z = 0.0f; - - damageInfo.in.m_shockWaveVector = forceVector; - - currentObj->attemptDamage(&damageInfo); + if (victim == self) + continue; + + // the length of this vector is read as the distance from the blast center, and its z is + // overwritten downstream, so build it flat + Coord3D outward; + outward.x = victim->getPosition()->x - m_riftPos.x; + outward.y = victim->getPosition()->y - m_riftPos.y; + outward.z = 0.0f; + + damageInfo.in.m_shockWaveVector = outward; + victim->attemptDamage(&damageInfo); } -} + +} // end doCollapse // ------------------------------------------------------------------------------------------------ /** CRC */ @@ -279,28 +857,46 @@ void RiftSlowDeathBehavior::crc(Xfer* xfer) // ------------------------------------------------------------------------------------------------ /** Xfer method * Version Info: - * 1: Initial version */ + * 1: Initial version + * 2: Phase machine, damage cadence and held victim list */ // ------------------------------------------------------------------------------------------------ void RiftSlowDeathBehavior::xfer(Xfer* xfer) { // version - XferVersion currentVersion = 1; + XferVersion currentVersion = 2; XferVersion version = currentVersion; xfer->xferVersion(&version, currentVersion); // extend base class SlowDeathBehavior::xfer(xfer); - // activation frame xfer->xferUnsignedInt(&m_activationFrame); - xfer->xferCoord3D(&m_riftPos); - xfer->xferBool(&m_finalPush); + xfer->xferUnsignedInt(&m_chargeEndFrame); + xfer->xferUnsignedInt(&m_rampEndFrame); + xfer->xferUnsignedInt(&m_mainEndFrame); + xfer->xferUnsignedInt(&m_fadeEndFrame); + xfer->xferUnsignedInt(&m_nextDamageFrame); + xfer->xferInt(&m_phase); + + Int victimCount = (Int)m_victims.size(); + xfer->xferInt(&victimCount); + if (xfer->getXferMode() == XFER_LOAD) + { + m_victims.clear(); + m_victims.resize(victimCount); + } + for (Int i = 0; i < victimCount; ++i) + { + RiftVictim& v = m_victims[i]; + xfer->xferObjectID(&v.m_id); + xfer->xferCoord3D(&v.m_kinematicVel); + xfer->xferInt(&v.m_spinSign); + xfer->xferUnsignedByte(&v.m_savedFlags); + } - xfer->xferBool(&m_playedMainFX); - } // end xfer // ------------------------------------------------------------------------------------------------ @@ -312,4 +908,4 @@ void RiftSlowDeathBehavior::loadPostProcess(void) // extend base class SlowDeathBehavior::loadPostProcess(); -} // end loadPostProcess \ No newline at end of file +} // end loadPostProcess From f5fdb13ec750922dcd95ce3121c3344347f1a5bd Mon Sep 17 00:00:00 2001 From: pWn3d Date: Sun, 27 Sep 2026 14:43:10 +0200 Subject: [PATCH 3/3] RiftDeath update --- Core/GameEngine/Include/GameLogic/Damage.h | 3 +- .../Code/GameEngine/Include/Common/KindOf.h | 1 + .../GameLogic/Module/RiftSlowDeathUpdate.h | 16 +- .../Source/Common/System/KindOf.cpp | 1 + .../Object/Update/RiftSlowDeathUpdate.cpp | 237 ++++++++++++++---- 5 files changed, 209 insertions(+), 49 deletions(-) diff --git a/Core/GameEngine/Include/GameLogic/Damage.h b/Core/GameEngine/Include/GameLogic/Damage.h index ec26db1dfbd..33e3ef92352 100644 --- a/Core/GameEngine/Include/GameLogic/Damage.h +++ b/Core/GameEngine/Include/GameLogic/Damage.h @@ -219,6 +219,7 @@ enum DeathType CPP_11(: Int) //New Death Types DEATH_CHRONO, DEATH_CRYO, + DEATH_BLACK_HOLE, DEATH_NUM_TYPES // keep this last }; @@ -250,7 +251,7 @@ static const char *const TheDeathNames[] = //New: "CHRONO", "CRYO", - + "BLACK_HOLE", nullptr }; static_assert(ARRAY_SIZE(TheDeathNames) == DEATH_NUM_TYPES + 1, "Incorrect array size"); diff --git a/GeneralsMD/Code/GameEngine/Include/Common/KindOf.h b/GeneralsMD/Code/GameEngine/Include/Common/KindOf.h index 2c2aac9e1ef..8a9fe144bdd 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/KindOf.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/KindOf.h @@ -221,6 +221,7 @@ enum KindOfType CPP_11(: Int) // TheSuperHackers @info New kinds for Mods KINDOF_NO_ATTACK_WARNING, ///< does not trigger the under attack radar/EVA warning when taking damage + KINDOF_IMMUNE_TO_PULL, ///< never dragged around by pulling effects such as RiftSlowDeathBehavior (still takes their damage) KINDOF_COUNT, // total number of kindofs KINDOF_FIRST = 0, diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/RiftSlowDeathUpdate.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/RiftSlowDeathUpdate.h index 0582edda130..b6325edd2b9 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/RiftSlowDeathUpdate.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/RiftSlowDeathUpdate.h @@ -37,7 +37,9 @@ struct RiftVictim { RIFTSAVE_WAS_DISABLED_FREEFALL = 0x01, ///< victim was already in freefall before we grabbed it RIFTSAVE_WAS_IN_FREEFALL = 0x02, ///< physics IS_IN_FREEFALL was already set - RIFTSAVE_USES_LOCOMOTOR = 0x04 ///< flyer we steer via its own locomotor + RIFTSAVE_USES_LOCOMOTOR = 0x04, ///< flyer we steer via its own locomotor + RIFTSAVE_DYING = 0x08, ///< died near the center; keep pulling it until it is destroyed + RIFTSAVE_DEBRIS = 0x10 ///< structure debris we spawned; destroyed at the event horizon }; ObjectID m_id; @@ -82,7 +84,9 @@ class RiftSlowDeathBehaviorModuleData : public SlowDeathBehaviorModuleData Real m_damagePerSecond; ///< Damage dealt per second of exposure at full strength UnsignedInt m_damageInterval; ///< Frames between damage ticks DamageType m_damageType; ///< Damage type used for the continuous damage - DeathType m_consumeDeathType; ///< Death type used when a victim crosses the event horizon + DeathType m_deathType; ///< Death type of the continuous damage outside ConsumeDeathRadius + DeathType m_consumeDeathType; ///< Death type used on the event horizon and for damage kills within ConsumeDeathRadius + Real m_consumeDeathRadius; ///< Victims killed by damage within this distance of the center use ConsumeDeathType Real m_finalPushForce; ///< Shockwave amount of the collapse blast Real m_finalPushSpeed; ///< Outward velocity given to held victims on collapse @@ -96,6 +100,10 @@ class RiftSlowDeathBehaviorModuleData : public SlowDeathBehaviorModuleData const FXList* m_FXfinal; ///< FX played on collapse const FXList* m_FXconsume; ///< FX played on each victim crossing the event horizon const ObjectCreationList* m_OCLconsume; ///< OCL fired on each victim crossing the event horizon + const ObjectCreationList* m_OCLstructureDebris; ///< OCL fired at a random spot on a damaged structure; its debris is pulled in + Real m_structureDebrisChance; ///< Chance per damaged structure per damage tick to fire m_OCLstructureDebris + Int m_structureDebrisCount; ///< How often the OCL fires per spawn, each at its own spot. Only the + ///< first object an OCL creates can be grabbed, so use Count = 1 there. }; // ------------------------------------------------------------------------------------------------ @@ -122,13 +130,15 @@ class RiftSlowDeathBehavior : public SlowDeathBehavior void doRiftTick(Real strength, UnsignedInt currFrame); ///< one pass over everything in range void doCollapse(void); ///< release the held victims and blow them outwards - Bool isPullable(const Object* victim) const; ///< can this object be dragged around at all + Bool isPullable(const Object* victim, Bool isDebris = FALSE) const; ///< can this object be dragged around at all Bool usesLocomotorCapture(const Object* victim) const; ///< flyer whose locomotor keeps running while disabled RiftVictim* findVictim(ObjectID id); ///< NULL if we are not holding it void captureVictim(Object* victim); ///< one-shot setup when a victim is first grabbed + void beginPhysicsCapture(RiftVictim& v, Object* victim); ///< hand the victim from its AI over to physics void steerVictim(RiftVictim& victim, Object* obj, const Coord3D& toRift, Real dist, Real strength); void releaseVictim(const RiftVictim& victim, Object* obj); ///< undo what capture cannot self-heal void releaseAllVictims(void); + void spawnStructureDebris(Object* structure); ///< rip a chunk off a structure and grab it UnsignedInt m_activationFrame; ///< frame we were activated on UnsignedInt m_chargeEndFrame; ///< absolute frame the rampup begins diff --git a/GeneralsMD/Code/GameEngine/Source/Common/System/KindOf.cpp b/GeneralsMD/Code/GameEngine/Source/Common/System/KindOf.cpp index e1879b2c942..753ac4e7219 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/System/KindOf.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/System/KindOf.cpp @@ -208,6 +208,7 @@ const char* const KindOfMaskType::s_bitNameList[] = "TARGET_DESIGNATOR", "NO_ATTACK_WARNING", + "IMMUNE_TO_PULL", nullptr }; diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/RiftSlowDeathUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/RiftSlowDeathUpdate.cpp index 84b7b5e406d..516a0b8c714 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/RiftSlowDeathUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/RiftSlowDeathUpdate.cpp @@ -11,6 +11,7 @@ #include "GameLogic/PartitionManager.h" #include "GameLogic/TerrainLogic.h" #include "GameLogic/Module/AIUpdate.h" +#include "GameLogic/Module/BodyModule.h" #include "GameLogic/Module/PhysicsUpdate.h" #include "GameClient/FXList.h" @@ -43,7 +44,9 @@ RiftSlowDeathBehaviorModuleData::RiftSlowDeathBehaviorModuleData(void) m_damagePerSecond = 0.0f; m_damageInterval = LOGICFRAMES_PER_SECOND; m_damageType = DAMAGE_EXPLOSION; + m_deathType = DEATH_NORMAL; m_consumeDeathType = DEATH_EXPLODED; + m_consumeDeathRadius = 0.0f; m_finalPushForce = 0.0f; m_finalPushSpeed = 0.0f; @@ -63,6 +66,9 @@ RiftSlowDeathBehaviorModuleData::RiftSlowDeathBehaviorModuleData(void) m_FXfinal = NULL; m_FXconsume = NULL; m_OCLconsume = NULL; + m_OCLstructureDebris = NULL; + m_structureDebrisChance = 1.0f; + m_structureDebrisCount = 1; } // end RiftSlowDeathBehaviorModuleData // ------------------------------------------------------------------------------------------------ @@ -96,7 +102,9 @@ RiftSlowDeathBehaviorModuleData::RiftSlowDeathBehaviorModuleData(void) { "DamagePerSecond", INI::parseReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_damagePerSecond) }, { "DamageInterval", INI::parseDurationUnsignedInt, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_damageInterval) }, { "DamageType", INI::parseIndexList, DamageTypeFlags::s_bitNameList, offsetof(RiftSlowDeathBehaviorModuleData, m_damageType) }, + { "DeathType", INI::parseIndexList, TheDeathNames, offsetof(RiftSlowDeathBehaviorModuleData, m_deathType) }, { "ConsumeDeathType", INI::parseIndexList, TheDeathNames, offsetof(RiftSlowDeathBehaviorModuleData, m_consumeDeathType) }, + { "ConsumeDeathRadius", INI::parseReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_consumeDeathRadius) }, { "FinalPushForce", INI::parseReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_finalPushForce) }, { "FinalPushSpeed", INI::parseVelocityReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_finalPushSpeed) }, @@ -110,6 +118,9 @@ RiftSlowDeathBehaviorModuleData::RiftSlowDeathBehaviorModuleData(void) { "FXFinal", INI::parseFXList, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_FXfinal) }, { "FXConsume", INI::parseFXList, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_FXconsume) }, { "OCLConsume", INI::parseObjectCreationList, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_OCLconsume) }, + { "StructureDebrisOCL", INI::parseObjectCreationList, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_OCLstructureDebris) }, + { "StructureDebrisChance",INI::parsePercentToReal, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_structureDebrisChance) }, + { "StructureDebrisCount", INI::parseInt, NULL, offsetof(RiftSlowDeathBehaviorModuleData, m_structureDebrisCount) }, { 0, 0, 0, 0 } }; @@ -291,7 +302,7 @@ void RiftSlowDeathBehavior::doRiftTick(Real strength, UnsignedInt currFrame) DamageInfo damageInfo; damageInfo.in.m_damageType = d->m_damageType; - damageInfo.in.m_deathType = d->m_consumeDeathType; + damageInfo.in.m_deathType = d->m_deathType; damageInfo.in.m_sourceID = self->getID(); damageInfo.in.m_amount = damageAmount; // no shockwave here: we move victims through physics directly, and attemptDamage refuses to @@ -311,6 +322,7 @@ void RiftSlowDeathBehavior::doRiftTick(Real strength, UnsignedInt currFrame) // consuming kills objects, so collect them and deal with them once the iterator is released std::vector consumed; std::vector inRange; + std::vector debrisSources; for (Object* victim = iter->first(); victim != NULL; victim = iter->next()) { @@ -324,18 +336,33 @@ void RiftSlowDeathBehavior::doRiftTick(Real strength, UnsignedInt currFrame) if (victim->getContainedBy() != NULL) continue; - if (damageThisFrame) - victim->attemptDamage(&damageInfo); - - if (!isPullable(victim)) - continue; - Coord3D toRift; toRift.x = m_riftPos.x - victim->getPosition()->x; toRift.y = m_riftPos.y - victim->getPosition()->y; toRift.z = m_riftPos.z - victim->getPosition()->z; Real dist = toRift.length(); + Bool pullable = isPullable(victim); + + if (damageThisFrame) + { + // only things that can actually fall in may die the consume death + damageInfo.in.m_deathType = (pullable && dist <= d->m_consumeDeathRadius) ? d->m_consumeDeathType : d->m_deathType; + // the same DamageInfo is reused for every victim, so clear what the last one reported + damageInfo.out = DamageInfoOutput(); + victim->attemptDamage(&damageInfo); + + if (d->m_OCLstructureDebris != NULL + && victim->isKindOf(KINDOF_STRUCTURE) + && !damageInfo.out.m_noEffect + && damageInfo.out.m_actualDamageDealt > 0.0f + && GameLogicRandomValueReal(0.0f, 1.0f) < d->m_structureDebrisChance) + debrisSources.push_back(victim->getID()); + } + + if (!pullable) + continue; + if (d->m_eventHorizonRadius > 0.0f && dist <= d->m_eventHorizonRadius) { consumed.push_back(victim->getID()); @@ -360,7 +387,7 @@ void RiftSlowDeathBehavior::doRiftTick(Real strength, UnsignedInt currFrame) Real releaseRadiusSqr = (d->m_radius * RIFT_RELEASE_SLOP) * (d->m_radius * RIFT_RELEASE_SLOP); for (Int i = (Int)m_victims.size() - 1; i >= 0; --i) { - const RiftVictim& v = m_victims[i]; + RiftVictim& v = m_victims[i]; Bool keep = FALSE; for (std::vector::const_iterator it = inRange.begin(); it != inRange.end(); ++it) @@ -382,16 +409,67 @@ void RiftSlowDeathBehavior::doRiftTick(Real strength, UnsignedInt currFrame) away.x = obj->getPosition()->x - m_riftPos.x; away.y = obj->getPosition()->y - m_riftPos.y; away.z = 0.0f; - if (away.lengthSqr() <= releaseRadiusSqr) + // but let go at once of anything that stopped being pullable, e.g. just turned invulnerable + if (away.lengthSqr() <= releaseRadiusSqr && isPullable(obj)) continue; releaseVictim(v, obj); } + else if (obj != NULL) + { + // Dead but not yet destroyed. Anything that died close to the center keeps falling in + // until its own death modules remove it; the alive filter hides it from the scan above, + // so it is steered from here instead. + Coord3D toRift; + toRift.x = m_riftPos.x - obj->getPosition()->x; + toRift.y = m_riftPos.y - obj->getPosition()->y; + toRift.z = m_riftPos.z - obj->getPosition()->z; + Real dist = toRift.length(); + + // our debris has an InactiveBody, which counts as dead from birth, so it always lands here + if ((v.m_savedFlags & RiftVictim::RIFTSAVE_DEBRIS) != 0) + { + if (d->m_eventHorizonRadius > 0.0f && dist <= d->m_eventHorizonRadius) + { + TheGameLogic->destroyObject(obj); + m_victims.erase(m_victims.begin() + i); + continue; + } + + steerVictim(v, obj, toRift, dist, strength); + continue; + } + + if ((v.m_savedFlags & RiftVictim::RIFTSAVE_DYING) != 0 || dist <= d->m_consumeDeathRadius) + { + if ((v.m_savedFlags & RiftVictim::RIFTSAVE_DYING) == 0) + { + v.m_savedFlags |= RiftVictim::RIFTSAVE_DYING; + + // a dead flyer's AI no longer drives its locomotor, so take it over through physics + if ((v.m_savedFlags & RiftVictim::RIFTSAVE_USES_LOCOMOTOR) != 0) + { + v.m_savedFlags &= ~RiftVictim::RIFTSAVE_USES_LOCOMOTOR; + beginPhysicsCapture(v, obj); + } + } + + steerVictim(v, obj, toRift, dist, strength); + continue; + } + } m_victims.erase(m_victims.begin() + i); } - // now that the iterator is gone it is safe to destroy things + // now that the iterator is gone it is safe to create and destroy things + for (std::vector::const_iterator it = debrisSources.begin(); it != debrisSources.end(); ++it) + { + Object* structure = TheGameLogic->findObjectByID(*it); + if (structure != NULL) + spawnStructureDebris(structure); + } + for (std::vector::const_iterator it = consumed.begin(); it != consumed.end(); ++it) { Object* victim = TheGameLogic->findObjectByID(*it); @@ -416,7 +494,7 @@ void RiftSlowDeathBehavior::doRiftTick(Real strength, UnsignedInt currFrame) // ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------ -Bool RiftSlowDeathBehavior::isPullable(const Object* victim) const +Bool RiftSlowDeathBehavior::isPullable(const Object* victim, Bool isDebris) const { const RiftSlowDeathBehaviorModuleData* d = getRiftSlowDeathBehaviorModuleData(); @@ -424,9 +502,29 @@ Bool RiftSlowDeathBehavior::isPullable(const Object* victim) const if (victim->isKindOf(KINDOF_IMMOBILE) || victim->isKindOf(KINDOF_STRUCTURE) || victim->isKindOf(KINDOF_BRIDGE) - || victim->isKindOf(KINDOF_BRIDGE_TOWER)) + || victim->isKindOf(KINDOF_BRIDGE_TOWER) + || victim->isKindOf(KINDOF_IMMUNE_TO_PULL)) return FALSE; + // invulnerable things are left alone. Our own debris is exempt: it usually has an InactiveBody, + // which always reports itself as indestructible. + if (!isDebris) + { + const BodyModuleInterface* body = victim->getBodyModule(); + if ((body != NULL && body->isIndestructible()) || victim->getIsUndetectedDefector()) + return FALSE; + + // these take damage but can never die, so the event horizon could never consume them + static const NameKeyType key_ImmortalBody = NAMEKEY("ImmortalBody"); + static const NameKeyType key_HighlanderBody = NAMEKEY("HighlanderBody"); + for (BehaviorModule** m = victim->getBehaviorModules(); *m; ++m) + { + NameKeyType key = (*m)->getModuleNameKey(); + if (key == key_ImmortalBody || key == key_HighlanderBody) + return FALSE; + } + } + if (victim->isAnyKindOf(d->m_noPullKindOf)) return FALSE; @@ -470,8 +568,6 @@ RiftVictim* RiftSlowDeathBehavior::findVictim(ObjectID id) // ------------------------------------------------------------------------------------------------ void RiftSlowDeathBehavior::captureVictim(Object* victim) { - const RiftSlowDeathBehaviorModuleData* d = getRiftSlowDeathBehaviorModuleData(); - RiftVictim v; v.m_id = victim->getID(); v.m_kinematicVel.zero(); @@ -487,35 +583,7 @@ void RiftSlowDeathBehavior::captureVictim(Object* victim) } else { - // DISABLED_FREEFALL is the only disable that suspends the AI while leaving physics running - // (AIUpdate tolerates DISABLED_HELD only), so it is what hands the object over to us. - if (victim->isDisabledByType(DISABLED_FREEFALL)) - v.m_savedFlags |= RiftVictim::RIFTSAVE_WAS_DISABLED_FREEFALL; - victim->setDisabled(DISABLED_FREEFALL); - - if (physics != NULL) - { - if (physics->getIsInFreeFall()) - v.m_savedFlags |= RiftVictim::RIFTSAVE_WAS_IN_FREEFALL; - - physics->setStickToGround(FALSE); - physics->setAllowToFall(TRUE); // stop physics clamping z to the terrain - physics->setIsInFreeFall(TRUE); // physics then re-asserts the disable for us each frame - physics->setAllowBouncing(FALSE); - physics->setAllowCollideForce(FALSE); - physics->setImmuneToFallingDamage(TRUE); - physics->setStunned(TRUE); - victim->setModelConditionState(MODELCONDITION_STUNNED_FLAILING); - - if (d->m_initialLiftVelocity > 0.0f) - { - Coord3D kick; - kick.x = 0.0f; - kick.y = 0.0f; - kick.z = d->m_initialLiftVelocity; - physics->addVelocityTo(&kick); - } - } + beginPhysicsCapture(v, victim); } if (physics != NULL) @@ -531,6 +599,46 @@ void RiftSlowDeathBehavior::captureVictim(Object* victim) } // end captureVictim +// ------------------------------------------------------------------------------------------------ +/** Take the victim away from its AI and hand it to physics, which steerVictim then drives. */ +// ------------------------------------------------------------------------------------------------ +void RiftSlowDeathBehavior::beginPhysicsCapture(RiftVictim& v, Object* victim) +{ + const RiftSlowDeathBehaviorModuleData* d = getRiftSlowDeathBehaviorModuleData(); + + // DISABLED_FREEFALL is the only disable that suspends the AI while leaving physics running + // (AIUpdate tolerates DISABLED_HELD only), so it is what hands the object over to us. + if (victim->isDisabledByType(DISABLED_FREEFALL)) + v.m_savedFlags |= RiftVictim::RIFTSAVE_WAS_DISABLED_FREEFALL; + victim->setDisabled(DISABLED_FREEFALL); + + PhysicsBehavior* physics = victim->getPhysics(); + if (physics == NULL) + return; + + if (physics->getIsInFreeFall()) + v.m_savedFlags |= RiftVictim::RIFTSAVE_WAS_IN_FREEFALL; + + physics->setStickToGround(FALSE); + physics->setAllowToFall(TRUE); // stop physics clamping z to the terrain + physics->setIsInFreeFall(TRUE); // physics then re-asserts the disable for us each frame + physics->setAllowBouncing(FALSE); + physics->setAllowCollideForce(FALSE); + physics->setImmuneToFallingDamage(TRUE); + physics->setStunned(TRUE); + victim->setModelConditionState(MODELCONDITION_STUNNED_FLAILING); + + if (d->m_initialLiftVelocity > 0.0f) + { + Coord3D kick; + kick.x = 0.0f; + kick.y = 0.0f; + kick.z = d->m_initialLiftVelocity; + physics->addVelocityTo(&kick); + } + +} // end beginPhysicsCapture + // ------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------ void RiftSlowDeathBehavior::steerVictim(RiftVictim& victim, Object* obj, const Coord3D& toRift, @@ -757,13 +865,52 @@ void RiftSlowDeathBehavior::releaseAllVictims(void) for (std::vector::const_iterator it = m_victims.begin(); it != m_victims.end(); ++it) { Object* obj = TheGameLogic->findObjectByID(it->m_id); - if (obj != NULL && !obj->isEffectivelyDead()) + if (obj != NULL && (!obj->isEffectivelyDead() || (it->m_savedFlags & RiftVictim::RIFTSAVE_DEBRIS) != 0)) releaseVictim(*it, obj); } m_victims.clear(); } // end releaseAllVictims +// ------------------------------------------------------------------------------------------------ +/** Fire the debris OCL at a random spot on the structure, so it looks like the rift is tearing + * pieces out of it. */ +// ------------------------------------------------------------------------------------------------ +void RiftSlowDeathBehavior::spawnStructureDebris(Object* structure) +{ + const RiftSlowDeathBehaviorModuleData* d = getRiftSlowDeathBehaviorModuleData(); + const GeometryInfo& geom = structure->getGeometryInfo(); + + Real angle = structure->getOrientation(); + Real c = Cos(angle); + Real s = Sin(angle); + + for (Int n = 0; n < d->m_structureDebrisCount; ++n) + { + // the footprint offset is in the structure's local space + Coord3D offset; + geom.makeRandomOffsetWithinFootprint(offset); + + Coord3D pos = *structure->getPosition(); + pos.x += offset.x * c - offset.y * s; + pos.y += offset.x * s + offset.y * c; + pos.z += GameLogicRandomValueReal(0.0f, geom.getMaxHeightAbovePosition()); + + Object* debris = ObjectCreationList::create(d->m_OCLstructureDebris, structure, &pos, NULL, 0.0f); + + // Debris usually has an InactiveBody and so counts as dead from birth, which hides it from + // the scan in doRiftTick. We therefore grab it here and track it ourselves. + if (debris == NULL || !isPullable(debris, TRUE) || findVictim(debris->getID()) != NULL) + continue; + + captureVictim(debris); + RiftVictim* held = findVictim(debris->getID()); + if (held != NULL) + held->m_savedFlags |= RiftVictim::RIFTSAVE_DEBRIS; + } + +} // end spawnStructureDebris + // ------------------------------------------------------------------------------------------------ /** The singularity collapses: everything we held is let go and thrown clear, and the whole radius * takes one outward blast. */ @@ -782,7 +929,7 @@ void RiftSlowDeathBehavior::doCollapse(void) for (std::vector::const_iterator it = m_victims.begin(); it != m_victims.end(); ++it) { Object* obj = TheGameLogic->findObjectByID(it->m_id); - if (obj == NULL || obj->isEffectivelyDead()) + if (obj == NULL || (obj->isEffectivelyDead() && (it->m_savedFlags & RiftVictim::RIFTSAVE_DEBRIS) == 0)) continue; PhysicsBehavior* physics = obj->getPhysics();