diff --git a/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl b/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl
index fa614607881..864431e6697 100644
--- a/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl
+++ b/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl
@@ -158,6 +158,8 @@ static PoolSizeRec PoolSizes[] =
{ "ScatterShotUpdate", 128, 64 },
{ "FireWeaponWhenDamagedBehavior", 32, 32 },
{ "FireWeaponWhenDeadBehavior", 128, 64 },
+ { "FireWeaponOnKillBehavior", 128, 64 },
+ { "CreateObjectOnKillBehavior", 128, 64 },
{ "DelayedUpgradeBehavior", 128, 64 },
{ "GenerateMinefieldBehavior", 32, 32 },
{ "HelicopterSlowDeathBehavior", 64, 32 },
diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt
index 55ad63fdeac..3d26b35e464 100644
--- a/GeneralsMD/Code/GameEngine/CMakeLists.txt
+++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt
@@ -333,6 +333,9 @@ set(GAMEENGINE_SRC
Include/GameLogic/Module/FireWeaponAdvancedUpdate.h
Include/GameLogic/Module/FireWeaponWhenDamagedBehavior.h
Include/GameLogic/Module/FireWeaponWhenDeadBehavior.h
+ Include/GameLogic/Module/FireWeaponOnKillBehavior.h
+ Include/GameLogic/Module/CreateObjectOnKillBehavior.h
+ Include/GameLogic/Module/OnKillModule.h
Include/GameLogic/Module/DelayedUpgradeBehavior.h
Include/GameLogic/Module/FlammableUpdate.h
Include/GameLogic/Module/FlightDeckBehavior.h
@@ -898,6 +901,9 @@ set(GAMEENGINE_SRC
Source/GameLogic/Object/Behavior/FreeFallProjectileBehavior.cpp
Source/GameLogic/Object/Behavior/FireWeaponWhenDamagedBehavior.cpp
Source/GameLogic/Object/Behavior/FireWeaponWhenDeadBehavior.cpp
+ Source/GameLogic/Object/Behavior/FireWeaponOnKillBehavior.cpp
+ Source/GameLogic/Object/Behavior/CreateObjectOnKillBehavior.cpp
+ Source/GameLogic/Object/Behavior/OnKillModule.cpp
Source/GameLogic/Object/Behavior/DelayedUpgradeBehavior.cpp
Source/GameLogic/Object/Behavior/FlightDeckBehavior.cpp
Source/GameLogic/Object/Behavior/GenerateMinefieldBehavior.cpp
diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BehaviorModule.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BehaviorModule.h
index cd23dc9700a..8a2ff9c62d4 100644
--- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BehaviorModule.h
+++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/BehaviorModule.h
@@ -44,6 +44,7 @@ class CreateModuleInterface;
class DamageModuleInterface;
class DestroyModuleInterface;
class DieModuleInterface;
+class OnKillModuleInterface;
class SpecialPowerModuleInterface;
class UpdateModuleInterface;
class UpgradeModuleInterface;
@@ -108,6 +109,7 @@ class BehaviorModuleInterface
virtual DamageModuleInterface* getDamage() = 0;
virtual DestroyModuleInterface* getDestroy() = 0;
virtual DieModuleInterface* getDie() = 0;
+ virtual OnKillModuleInterface* getOnKill() = 0;
virtual SpecialPowerModuleInterface* getSpecialPower() = 0;
virtual UpdateModuleInterface* getUpdate() = 0;
virtual UpgradeModuleInterface* getUpgrade() = 0;
@@ -165,6 +167,7 @@ class BehaviorModule : public ObjectModule, public BehaviorModuleInterface
virtual DamageModuleInterface* getDamage() { return nullptr; }
virtual DestroyModuleInterface* getDestroy() { return nullptr; }
virtual DieModuleInterface* getDie() { return nullptr; }
+ virtual OnKillModuleInterface* getOnKill() { return nullptr; }
virtual SpecialPowerModuleInterface* getSpecialPower() { return nullptr; }
virtual UpdateModuleInterface* getUpdate() { return nullptr; }
virtual UpgradeModuleInterface* getUpgrade() { return nullptr; }
diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/CreateObjectOnKillBehavior.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/CreateObjectOnKillBehavior.h
new file mode 100644
index 00000000000..db408bac7dd
--- /dev/null
+++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/CreateObjectOnKillBehavior.h
@@ -0,0 +1,128 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2025 Electronic Arts Inc.
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+// FILE: CreateObjectOnKillBehavior.h /////////////////////////////////////////////////////////////
+// Desc: Spawns an ObjectCreationList at the position of an object this object kills.
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+#pragma once
+
+#include "GameLogic/Module/BehaviorModule.h"
+#include "GameLogic/Module/UpgradeModule.h"
+#include "GameLogic/Module/OnKillModule.h"
+
+class ObjectCreationList;
+
+//-------------------------------------------------------------------------------------------------
+class CreateObjectOnKillBehaviorModuleData : public BehaviorModuleData
+{
+public:
+ UpgradeMuxData m_upgradeMuxData;
+ Bool m_initiallyActive;
+ KillMuxData m_killMuxData;
+ const ObjectCreationList* m_ocl; ///< spawn this OCL at the victim when we kill something
+ Bool m_createAtKillerLocation; ///< spawn at the killer's position instead of the victim's
+ Bool m_createObjectForVictim; ///< created object is owned by the victim instead of the killer
+
+ CreateObjectOnKillBehaviorModuleData()
+ {
+ m_initiallyActive = false;
+ m_ocl = nullptr;
+ m_createAtKillerLocation = false;
+ m_createObjectForVictim = false;
+ }
+
+ static void buildFieldParse(MultiIniFieldParse& p)
+ {
+ static const FieldParse dataFieldParse[] =
+ {
+ { "StartsActive", INI::parseBool, nullptr, offsetof( CreateObjectOnKillBehaviorModuleData, m_initiallyActive ) },
+ { "CreationList", INI::parseObjectCreationList, nullptr, offsetof( CreateObjectOnKillBehaviorModuleData, m_ocl ) },
+ { "CreateAtKillerLocation", INI::parseBool, nullptr, offsetof( CreateObjectOnKillBehaviorModuleData, m_createAtKillerLocation ) },
+ { "CreateObjectForVictim", INI::parseBool, nullptr, offsetof( CreateObjectOnKillBehaviorModuleData, m_createObjectForVictim ) },
+ { 0, 0, 0, 0 }
+ };
+
+ BehaviorModuleData::buildFieldParse(p);
+ p.add(dataFieldParse);
+ p.add(UpgradeMuxData::getFieldParse(), offsetof( CreateObjectOnKillBehaviorModuleData, m_upgradeMuxData ));
+ p.add(KillMuxData::getFieldParse(), offsetof( CreateObjectOnKillBehaviorModuleData, m_killMuxData ));
+ }
+};
+
+//-------------------------------------------------------------------------------------------------
+//-------------------------------------------------------------------------------------------------
+class CreateObjectOnKillBehavior : public BehaviorModule,
+ public UpgradeMux,
+ public OnKillModuleInterface
+{
+
+ MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE( CreateObjectOnKillBehavior, "CreateObjectOnKillBehavior" )
+ MAKE_STANDARD_MODULE_MACRO_WITH_MODULE_DATA( CreateObjectOnKillBehavior, CreateObjectOnKillBehaviorModuleData )
+
+public:
+
+ CreateObjectOnKillBehavior( Thing *thing, const ModuleData* moduleData );
+ // virtual destructor prototype provided by memory pool declaration
+
+ // module methods
+ static Int getInterfaceMask() { return BehaviorModule::getInterfaceMask() | (MODULEINTERFACE_UPGRADE); }
+
+ // BehaviorModule
+ virtual UpgradeModuleInterface* getUpgrade() { return this; }
+ virtual OnKillModuleInterface* getOnKill() { return this; }
+
+ // OnKillModuleInterface
+ virtual void onKilledObject( Object *victim, const DamageInfo *damageInfo );
+
+protected:
+
+ virtual void upgradeImplementation()
+ {
+ // nothing!
+ }
+
+ virtual void getUpgradeActivationMasks(UpgradeMaskType& activation, UpgradeMaskType& conflicting) const
+ {
+ getCreateObjectOnKillBehaviorModuleData()->m_upgradeMuxData.getUpgradeActivationMasks(activation, conflicting);
+ }
+
+ virtual void performUpgradeFX()
+ {
+ getCreateObjectOnKillBehaviorModuleData()->m_upgradeMuxData.performUpgradeFX(getObject());
+ }
+
+ virtual void processUpgradeRemoval()
+ {
+ getCreateObjectOnKillBehaviorModuleData()->m_upgradeMuxData.muxDataProcessUpgradeRemoval(getObject());
+ }
+
+ virtual Bool requiresAllActivationUpgrades() const
+ {
+ return getCreateObjectOnKillBehaviorModuleData()->m_upgradeMuxData.m_requiresAllTriggers;
+ }
+
+ Bool isUpgradeActive() const { return isAlreadyUpgraded(); }
+
+ virtual Bool isSubObjectsUpgrade() { return false; }
+
+private:
+
+ UnsignedInt m_lastTriggerFrame; ///< frame of the last trigger, for TriggerChance/CooldownTime
+
+};
diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/FireWeaponOnKillBehavior.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/FireWeaponOnKillBehavior.h
new file mode 100644
index 00000000000..0f7f1912049
--- /dev/null
+++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/FireWeaponOnKillBehavior.h
@@ -0,0 +1,122 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2025 Electronic Arts Inc.
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+// FILE: FireWeaponOnKillBehavior.h ///////////////////////////////////////////////////////////////
+// Desc: Fires a weapon at the position of an object this object kills.
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+#pragma once
+
+#include "GameLogic/Module/BehaviorModule.h"
+#include "GameLogic/Module/UpgradeModule.h"
+#include "GameLogic/Module/OnKillModule.h"
+
+class WeaponTemplate;
+
+//-------------------------------------------------------------------------------------------------
+class FireWeaponOnKillBehaviorModuleData : public BehaviorModuleData
+{
+public:
+ UpgradeMuxData m_upgradeMuxData;
+ Bool m_initiallyActive;
+ KillMuxData m_killMuxData;
+ const WeaponTemplate* m_killWeapon; ///< fire this weapon at the victim when we kill something
+
+ FireWeaponOnKillBehaviorModuleData()
+ {
+ m_initiallyActive = false;
+ m_killWeapon = nullptr;
+ }
+
+ static void buildFieldParse(MultiIniFieldParse& p)
+ {
+ static const FieldParse dataFieldParse[] =
+ {
+ { "StartsActive", INI::parseBool, nullptr, offsetof( FireWeaponOnKillBehaviorModuleData, m_initiallyActive ) },
+ { "KillWeapon", INI::parseWeaponTemplate, nullptr, offsetof( FireWeaponOnKillBehaviorModuleData, m_killWeapon ) },
+ { 0, 0, 0, 0 }
+ };
+
+ BehaviorModuleData::buildFieldParse(p);
+ p.add(dataFieldParse);
+ p.add(UpgradeMuxData::getFieldParse(), offsetof( FireWeaponOnKillBehaviorModuleData, m_upgradeMuxData ));
+ p.add(KillMuxData::getFieldParse(), offsetof( FireWeaponOnKillBehaviorModuleData, m_killMuxData ));
+ }
+};
+
+//-------------------------------------------------------------------------------------------------
+//-------------------------------------------------------------------------------------------------
+class FireWeaponOnKillBehavior : public BehaviorModule,
+ public UpgradeMux,
+ public OnKillModuleInterface
+{
+
+ MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE( FireWeaponOnKillBehavior, "FireWeaponOnKillBehavior" )
+ MAKE_STANDARD_MODULE_MACRO_WITH_MODULE_DATA( FireWeaponOnKillBehavior, FireWeaponOnKillBehaviorModuleData )
+
+public:
+
+ FireWeaponOnKillBehavior( Thing *thing, const ModuleData* moduleData );
+ // virtual destructor prototype provided by memory pool declaration
+
+ // module methods
+ static Int getInterfaceMask() { return BehaviorModule::getInterfaceMask() | (MODULEINTERFACE_UPGRADE); }
+
+ // BehaviorModule
+ virtual UpgradeModuleInterface* getUpgrade() { return this; }
+ virtual OnKillModuleInterface* getOnKill() { return this; }
+
+ // OnKillModuleInterface
+ virtual void onKilledObject( Object *victim, const DamageInfo *damageInfo );
+
+protected:
+
+ virtual void upgradeImplementation()
+ {
+ // nothing!
+ }
+
+ virtual void getUpgradeActivationMasks(UpgradeMaskType& activation, UpgradeMaskType& conflicting) const
+ {
+ getFireWeaponOnKillBehaviorModuleData()->m_upgradeMuxData.getUpgradeActivationMasks(activation, conflicting);
+ }
+
+ virtual void performUpgradeFX()
+ {
+ getFireWeaponOnKillBehaviorModuleData()->m_upgradeMuxData.performUpgradeFX(getObject());
+ }
+
+ virtual void processUpgradeRemoval()
+ {
+ getFireWeaponOnKillBehaviorModuleData()->m_upgradeMuxData.muxDataProcessUpgradeRemoval(getObject());
+ }
+
+ virtual Bool requiresAllActivationUpgrades() const
+ {
+ return getFireWeaponOnKillBehaviorModuleData()->m_upgradeMuxData.m_requiresAllTriggers;
+ }
+
+ Bool isUpgradeActive() const { return isAlreadyUpgraded(); }
+
+ virtual Bool isSubObjectsUpgrade() { return false; }
+
+private:
+
+ UnsignedInt m_lastTriggerFrame; ///< frame of the last trigger, for TriggerChance/CooldownTime
+
+};
diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/OnKillModule.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/OnKillModule.h
new file mode 100644
index 00000000000..694c9a7bc11
--- /dev/null
+++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/OnKillModule.h
@@ -0,0 +1,72 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2025 Electronic Arts Inc.
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+// FILE: OnKillModule.h ////////////////////////////////////////////////////////////////////////////
+// Desc: Behavior interface + shared filter for modules that react when the owning object kills
+// another object (the "killer" side, as opposed to DieModule which is the victim side).
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+#pragma once
+
+#include "Common/Module.h"
+#include "Common/KindOf.h"
+#include "Common/ObjectStatusTypes.h"
+#include "GameLogic/Damage.h"
+#include "GameLogic/Module/BehaviorModule.h"
+
+class Object;
+struct FieldParse;
+
+//-------------------------------------------------------------------------------------------------
+/** Implemented by modules that want to be notified whenever their owning object kills another. */
+//-------------------------------------------------------------------------------------------------
+class OnKillModuleInterface
+{
+public:
+ virtual void onKilledObject( Object *victim, const DamageInfo *damageInfo ) = 0;
+};
+
+//-------------------------------------------------------------------------------------------------
+/** Shared filter describing which kills a OnKill module should react to. Checks the KILLED object
+ (victim) by KindOf and ObjectStatus, its player relationship to the killer, and (when a DamageInfo
+ is available) the death type. Does NOT inherit from ModuleData (mirrors DieMuxData). */
+//-------------------------------------------------------------------------------------------------
+class KillMuxData
+{
+public:
+ KindOfMaskType m_victimRequiredKindOf; ///< victim must match these KindOf bits (ALL or ANY, per m_requiresAllKindOfs)
+ KindOfMaskType m_victimForbiddenKindOf; ///< victim must have none of these KindOf bits
+ Bool m_requiresAllKindOfs; ///< if true victim needs ALL required KindOfs, else ANY of them
+ ObjectStatusMaskType m_victimRequiredStatus; ///< victim must have all of these status bits
+ ObjectStatusMaskType m_victimForbiddenStatus; ///< victim must have none of these status bits
+ Int m_victimRelationship; ///< bitmask (WEAPON_AFFECTS_ALLIES/ENEMIES/NEUTRALS) of allowed killer->victim relationships
+ DeathTypeFlags m_deathTypes; ///< only these death types trigger (checked only when a DamageInfo is present)
+ DamageTypeFlags m_damageTypes; ///< only these damage types trigger (checked only when a DamageInfo is present)
+ Real m_triggerChance; ///< chance (0..1) that an applicable kill actually triggers the effect
+ UnsignedInt m_cooldownFrames; ///< min frames between triggers (0 = no cooldown)
+
+ KillMuxData();
+ static const FieldParse* getFieldParse();
+
+ Bool isKillApplicable( const Object *killer, const Object *victim, const DamageInfo *damageInfo ) const;
+
+ // Rolls TriggerChance and enforces CooldownTime. lastTriggerFrame is per-module-instance
+ // state (module data is shared per-template, so it cannot live here). On a successful trigger
+ // lastTriggerFrame is updated to the current frame; a failed chance roll does not start the cooldown.
+ Bool passesChanceAndCooldown( UnsignedInt& lastTriggerFrame ) const;
+};
diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h
index ca405d49680..03d601d28e8 100644
--- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h
+++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h
@@ -236,7 +236,7 @@ class Object : public Thing, public Snapshot
void doTempWeaponBonus( WeaponBonusConditionType status, UnsignedInt duration, TintStatus tintStatus = TINT_STATUS_INVALID );///< At this level, we just pass this on to our helper
void applyBuff(const BuffTemplate* buffTemp, UnsignedInt duration, Object* sourceObj);
- void scoreTheKill( const Object *victim ); ///< I just killed this object.
+ void scoreTheKill( const Object *victim, const DamageInfo *damageInfo = nullptr ); ///< I just killed this object.
void onVeterancyLevelChanged( VeterancyLevel oldLevel, VeterancyLevel newLevel, Bool provideFeedback = TRUE ); ///< I just achieved this level right this moment
void createVeterancyLevelFX(VeterancyLevel oldLevel, VeterancyLevel newLevel);
ExperienceTracker* getExperienceTracker() {return m_experienceTracker;}
diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp
index 03534f5358d..dc6a449b2fe 100644
--- a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp
@@ -85,6 +85,8 @@
#include "GameLogic/Module/BunkerBusterBehavior.h"
#include "GameLogic/Module/FireWeaponWhenDamagedBehavior.h"
#include "GameLogic/Module/FireWeaponWhenDeadBehavior.h"
+#include "GameLogic/Module/FireWeaponOnKillBehavior.h"
+#include "GameLogic/Module/CreateObjectOnKillBehavior.h"
#include "GameLogic/Module/DelayedUpgradeBehavior.h"
#include "GameLogic/Module/GenerateMinefieldBehavior.h"
#include "GameLogic/Module/ParkingPlaceBehavior.h"
@@ -397,6 +399,8 @@ void ModuleFactory::init( void )
addModule( BunkerBusterBehavior );
addModule( FireWeaponWhenDamagedBehavior );
addModule( FireWeaponWhenDeadBehavior );
+ addModule( FireWeaponOnKillBehavior );
+ addModule( CreateObjectOnKillBehavior );
addModule( DelayedUpgradeBehavior );
addModule( GenerateMinefieldBehavior );
addModule( ParkingPlaceBehavior );
diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable/Update/ShipEffectsClientUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable/Update/ShipEffectsClientUpdate.cpp
new file mode 100644
index 00000000000..aec95dfd266
--- /dev/null
+++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable/Update/ShipEffectsClientUpdate.cpp
@@ -0,0 +1,133 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2025 Electronic Arts Inc.
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+////////////////////////////////////////////////////////////////////////////////
+// //
+// (c) 2001-2003 Electronic Arts Inc. //
+// //
+////////////////////////////////////////////////////////////////////////////////
+
+// FILE: ShipEffectsClientUpdate.cpp //////////////////////////////////////////////////////////////////
+// Author: Andi W, 02 2026
+// Desc: client update for ship movement particle effects
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+// INCLUDES ///////////////////////////////////////////////////////////////////////////////////////
+#include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine
+
+#include "GameClient/Drawable.h"
+#include "GameClient/Module/ShipEffectsClientUpdate.h"
+#include "Common/Player.h"
+#include "Common/PlayerList.h"
+#include "Common/ThingFactory.h"
+#include "Common/ThingTemplate.h"
+#include "Common/RandomValue.h"
+#include "Common/DrawModule.h"
+#include "Common/PerfTimer.h"
+#include "Common/Xfer.h"
+#include "GameLogic/Object.h"
+#include "GameLogic/ScriptEngine.h"
+
+//-------------------------------------------------------------------------------------------------
+//-------------------------------------------------------------------------------------------------
+ShipEffectsClientUpdate::ShipEffectsClientUpdate( Thing *thing, const ModuleData* moduleData ) :
+ ClientUpdateModule( thing, moduleData )
+{
+ m_life = 0;
+
+}
+
+//-------------------------------------------------------------------------------------------------
+//-------------------------------------------------------------------------------------------------
+ShipEffectsClientUpdate::~ShipEffectsClientUpdate( void )
+{
+
+}
+
+
+//-------------------------------------------------------------------------------------------------
+/** The client update callback. */
+//-------------------------------------------------------------------------------------------------
+void ShipEffectsClientUpdate::clientUpdate( void )
+{
+ //THIS IS HAPPENING CLIENT-SIDE
+ // I CAN DO WHAT I NEED HERE AND NOT HAVE TO BE LOGIC SYNC-SAFE
+
+
+ ++m_life;
+
+ Drawable *draw = getDrawable();
+ if (draw)
+ {
+
+ for (DrawModule** dm = draw->getDrawModules(); *dm; ++dm)
+ {
+ ObjectDrawInterface* di = (*dm)->getObjectDrawInterface();
+ if (di)
+ {
+ if (di->updateBonesForClientParticleSystems())
+ break;
+ }
+ }
+
+
+ }
+
+
+}
+
+// ------------------------------------------------------------------------------------------------
+/** CRC */
+// ------------------------------------------------------------------------------------------------
+void ShipEffectsClientUpdate::crc( Xfer *xfer )
+{
+
+ // extend base class
+ ClientUpdateModule::crc( xfer );
+
+}
+
+// ------------------------------------------------------------------------------------------------
+/** Xfer method
+ * Version Info:
+ * 1: Initial version */
+// ------------------------------------------------------------------------------------------------
+void ShipEffectsClientUpdate::xfer( Xfer *xfer )
+{
+
+ // version
+ XferVersion currentVersion = 1;
+ XferVersion version = currentVersion;
+ xfer->xferVersion( &version, currentVersion );
+
+ // extend base class
+ ClientUpdateModule::xfer( xfer );
+
+
+}
+
+// ------------------------------------------------------------------------------------------------
+/** Load post process */
+// ------------------------------------------------------------------------------------------------
+void ShipEffectsClientUpdate::loadPostProcess( void )
+{
+
+ // extend base class
+ ClientUpdateModule::loadPostProcess();
+
+}
diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/CreateObjectOnKillBehavior.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/CreateObjectOnKillBehavior.cpp
new file mode 100644
index 00000000000..9e3c41e6abe
--- /dev/null
+++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/CreateObjectOnKillBehavior.cpp
@@ -0,0 +1,120 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2025 Electronic Arts Inc.
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+// FILE: CreateObjectOnKillBehavior.cpp ///////////////////////////////////////////////////////////
+// Desc: Spawns an ObjectCreationList at the position of an object this object kills.
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine
+
+#include "Common/Xfer.h"
+#include "GameLogic/GameLogic.h"
+#include "GameLogic/Module/CreateObjectOnKillBehavior.h"
+#include "GameLogic/Object.h"
+#include "GameLogic/ObjectCreationList.h"
+
+//-------------------------------------------------------------------------------------------------
+//-------------------------------------------------------------------------------------------------
+CreateObjectOnKillBehavior::CreateObjectOnKillBehavior( Thing *thing, const ModuleData* moduleData ) :
+ BehaviorModule( thing, moduleData )
+{
+ m_lastTriggerFrame = 0;
+
+ if (getCreateObjectOnKillBehaviorModuleData()->m_initiallyActive)
+ {
+ giveSelfUpgrade();
+ }
+}
+
+//-------------------------------------------------------------------------------------------------
+//-------------------------------------------------------------------------------------------------
+CreateObjectOnKillBehavior::~CreateObjectOnKillBehavior( void )
+{
+}
+
+//-------------------------------------------------------------------------------------------------
+/** The kill callback. */
+//-------------------------------------------------------------------------------------------------
+void CreateObjectOnKillBehavior::onKilledObject( Object *victim, const DamageInfo *damageInfo )
+{
+ const CreateObjectOnKillBehaviorModuleData* d = getCreateObjectOnKillBehaviorModuleData();
+
+ if (!isUpgradeActive())
+ return;
+
+ if (!d->m_killMuxData.isKillApplicable(getObject(), victim, damageInfo))
+ return;
+
+ if (!d->m_killMuxData.passesChanceAndCooldown(m_lastTriggerFrame))
+ return;
+
+ if (d->m_ocl && victim)
+ {
+ const Coord3D* createPos = d->m_createAtKillerLocation ? getObject()->getPosition() : victim->getPosition();
+ const Object* createOwner = d->m_createObjectForVictim ? victim : getObject();
+ ObjectCreationList::create(d->m_ocl, createOwner, createPos, nullptr, false);
+ }
+}
+
+// ------------------------------------------------------------------------------------------------
+/** CRC */
+// ------------------------------------------------------------------------------------------------
+void CreateObjectOnKillBehavior::crc( Xfer *xfer )
+{
+ // extend base class
+ BehaviorModule::crc( xfer );
+
+ // extend upgrade mux
+ UpgradeMux::upgradeMuxCRC( xfer );
+}
+
+// ------------------------------------------------------------------------------------------------
+/** Xfer method
+ * Version Info:
+ * 1: Initial version
+ * 2: Added m_lastTriggerFrame (TriggerChance/CooldownTime) */
+// ------------------------------------------------------------------------------------------------
+void CreateObjectOnKillBehavior::xfer( Xfer *xfer )
+{
+ // version
+ XferVersion currentVersion = 2;
+ XferVersion version = currentVersion;
+ xfer->xferVersion( &version, currentVersion );
+
+ // extend base class
+ BehaviorModule::xfer( xfer );
+
+ // extend upgrade mux
+ UpgradeMux::upgradeMuxXfer( xfer );
+
+ // last trigger frame (v2+)
+ if (version >= 2)
+ xfer->xferUnsignedInt( &m_lastTriggerFrame );
+}
+
+// ------------------------------------------------------------------------------------------------
+/** Load post process */
+// ------------------------------------------------------------------------------------------------
+void CreateObjectOnKillBehavior::loadPostProcess( void )
+{
+ // extend base class
+ BehaviorModule::loadPostProcess();
+
+ // extend upgrade mux
+ UpgradeMux::upgradeMuxLoadPostProcess();
+}
diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/FireWeaponOnKillBehavior.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/FireWeaponOnKillBehavior.cpp
new file mode 100644
index 00000000000..5632b3d3b80
--- /dev/null
+++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/FireWeaponOnKillBehavior.cpp
@@ -0,0 +1,118 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2025 Electronic Arts Inc.
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+// FILE: FireWeaponOnKillBehavior.cpp /////////////////////////////////////////////////////////////
+// Desc: Fires a weapon at the position of an object this object kills.
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine
+
+#include "Common/Xfer.h"
+#include "GameLogic/GameLogic.h"
+#include "GameLogic/Module/FireWeaponOnKillBehavior.h"
+#include "GameLogic/Object.h"
+#include "GameLogic/Weapon.h"
+
+//-------------------------------------------------------------------------------------------------
+//-------------------------------------------------------------------------------------------------
+FireWeaponOnKillBehavior::FireWeaponOnKillBehavior( Thing *thing, const ModuleData* moduleData ) :
+ BehaviorModule( thing, moduleData )
+{
+ m_lastTriggerFrame = 0;
+
+ if (getFireWeaponOnKillBehaviorModuleData()->m_initiallyActive)
+ {
+ giveSelfUpgrade();
+ }
+}
+
+//-------------------------------------------------------------------------------------------------
+//-------------------------------------------------------------------------------------------------
+FireWeaponOnKillBehavior::~FireWeaponOnKillBehavior( void )
+{
+}
+
+//-------------------------------------------------------------------------------------------------
+/** The kill callback. */
+//-------------------------------------------------------------------------------------------------
+void FireWeaponOnKillBehavior::onKilledObject( Object *victim, const DamageInfo *damageInfo )
+{
+ const FireWeaponOnKillBehaviorModuleData* d = getFireWeaponOnKillBehaviorModuleData();
+
+ if (!isUpgradeActive())
+ return;
+
+ if (!d->m_killMuxData.isKillApplicable(getObject(), victim, damageInfo))
+ return;
+
+ if (!d->m_killMuxData.passesChanceAndCooldown(m_lastTriggerFrame))
+ return;
+
+ if (d->m_killWeapon && victim)
+ {
+ TheWeaponStore->createAndFireTempWeapon(d->m_killWeapon, getObject(), victim->getPosition());
+ }
+}
+
+// ------------------------------------------------------------------------------------------------
+/** CRC */
+// ------------------------------------------------------------------------------------------------
+void FireWeaponOnKillBehavior::crc( Xfer *xfer )
+{
+ // extend base class
+ BehaviorModule::crc( xfer );
+
+ // extend upgrade mux
+ UpgradeMux::upgradeMuxCRC( xfer );
+}
+
+// ------------------------------------------------------------------------------------------------
+/** Xfer method
+ * Version Info:
+ * 1: Initial version
+ * 2: Added m_lastTriggerFrame (TriggerChance/CooldownTime) */
+// ------------------------------------------------------------------------------------------------
+void FireWeaponOnKillBehavior::xfer( Xfer *xfer )
+{
+ // version
+ XferVersion currentVersion = 2;
+ XferVersion version = currentVersion;
+ xfer->xferVersion( &version, currentVersion );
+
+ // extend base class
+ BehaviorModule::xfer( xfer );
+
+ // extend upgrade mux
+ UpgradeMux::upgradeMuxXfer( xfer );
+
+ // last trigger frame (v2+)
+ if (version >= 2)
+ xfer->xferUnsignedInt( &m_lastTriggerFrame );
+}
+
+// ------------------------------------------------------------------------------------------------
+/** Load post process */
+// ------------------------------------------------------------------------------------------------
+void FireWeaponOnKillBehavior::loadPostProcess( void )
+{
+ // extend base class
+ BehaviorModule::loadPostProcess();
+
+ // extend upgrade mux
+ UpgradeMux::upgradeMuxLoadPostProcess();
+}
diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/OnKillModule.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/OnKillModule.cpp
new file mode 100644
index 00000000000..71c254947d8
--- /dev/null
+++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Behavior/OnKillModule.cpp
@@ -0,0 +1,132 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2025 Electronic Arts Inc.
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+// FILE: OnKillModule.cpp //////////////////////////////////////////////////////////////////////////
+// Desc: Shared filter (KillMuxData) for OnKill behavior modules.
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+#include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine
+
+#include "Common/INI.h"
+#include "Common/RandomValue.h"
+#include "GameLogic/GameLogic.h"
+#include "GameLogic/Module/OnKillModule.h"
+#include "GameLogic/Object.h"
+#include "GameLogic/Weapon.h" // for TheWeaponAffectsMaskNames and WEAPON_AFFECTS_* bits
+
+//-------------------------------------------------------------------------------------------------
+KillMuxData::KillMuxData()
+{
+ // KindOf/status masks default to empty (no restriction).
+ m_requiresAllKindOfs = true; // default: victim must match ALL required KindOfs
+ m_victimRelationship = WEAPON_AFFECTS_ENEMIES; // by default, only react to killing enemies
+ m_deathTypes = DEATH_TYPE_FLAGS_ALL;
+ m_damageTypes = DAMAGE_TYPE_FLAGS_ALL;
+ m_triggerChance = 1.0f; // always trigger
+ m_cooldownFrames = 0; // no cooldown
+}
+
+//-------------------------------------------------------------------------------------------------
+const FieldParse* KillMuxData::getFieldParse()
+{
+ static const FieldParse dataFieldParse[] =
+ {
+ { "KilledKindOf", KindOfMaskType::parseFromINI, nullptr, offsetof( KillMuxData, m_victimRequiredKindOf ) },
+ { "ForbiddenKilledKindOf", KindOfMaskType::parseFromINI, nullptr, offsetof( KillMuxData, m_victimForbiddenKindOf ) },
+ { "RequiresAllKindOfs", INI::parseBool, nullptr, offsetof( KillMuxData, m_requiresAllKindOfs ) },
+ { "RequiredKilledStatus", ObjectStatusMaskType::parseFromINI, nullptr, offsetof( KillMuxData, m_victimRequiredStatus ) },
+ { "ForbiddenKilledStatus", ObjectStatusMaskType::parseFromINI, nullptr, offsetof( KillMuxData, m_victimForbiddenStatus ) },
+ { "KilledRelationship", INI::parseBitString32, TheWeaponAffectsMaskNames, offsetof( KillMuxData, m_victimRelationship ) },
+ { "DeathTypes", INI::parseDeathTypeFlags, nullptr, offsetof( KillMuxData, m_deathTypes ) },
+ { "DamageTypes", INI::parseDamageTypeFlags, nullptr, offsetof( KillMuxData, m_damageTypes ) },
+ { "TriggerChance", INI::parsePercentToReal, nullptr, offsetof( KillMuxData, m_triggerChance ) },
+ { "CooldownTime", INI::parseDurationUnsignedInt, nullptr, offsetof( KillMuxData, m_cooldownFrames ) },
+ { nullptr, nullptr, nullptr, 0 }
+ };
+ return dataFieldParse;
+}
+
+//-------------------------------------------------------------------------------------------------
+Bool KillMuxData::isKillApplicable( const Object *killer, const Object *victim, const DamageInfo *damageInfo ) const
+{
+ if (killer == nullptr || victim == nullptr)
+ return false;
+
+ // wrong death type? punt (only when we actually have damage context)
+ if (damageInfo != nullptr && !getDeathTypeFlag(m_deathTypes, damageInfo->in.m_deathType))
+ return false;
+
+ // wrong damage type? punt (only when we actually have damage context)
+ if (damageInfo != nullptr && !getDamageTypeFlag(m_damageTypes, damageInfo->in.m_damageType))
+ return false;
+
+ // victim KindOf: must never have a forbidden bit, and must match the required bits either fully
+ // (ALL) or partially (ANY) depending on m_requiresAllKindOfs.
+ if (m_requiresAllKindOfs)
+ {
+ if (!victim->isKindOfMulti(m_victimRequiredKindOf, m_victimForbiddenKindOf))
+ return false;
+ }
+ else
+ {
+ if (m_victimRequiredKindOf.any() && !victim->isAnyKindOf(m_victimRequiredKindOf))
+ return false;
+ if (victim->isAnyKindOf(m_victimForbiddenKindOf))
+ return false;
+ }
+
+ // all 'required' status bits must be set.
+ if (m_victimRequiredStatus.any() && !victim->getStatusBits().testForAll(m_victimRequiredStatus))
+ return false;
+
+ // all 'forbidden' status bits must be clear.
+ if (m_victimForbiddenStatus.any() && victim->getStatusBits().testForAny(m_victimForbiddenStatus))
+ return false;
+
+ // relationship of the victim to the killer must be in the allowed set.
+ Relationship r = killer->getRelationship(victim);
+ Int need;
+ if (r == ALLIES)
+ need = WEAPON_AFFECTS_ALLIES;
+ else if (r == ENEMIES)
+ need = WEAPON_AFFECTS_ENEMIES;
+ else
+ need = WEAPON_AFFECTS_NEUTRALS;
+ if (!(m_victimRelationship & need))
+ return false;
+
+ return true;
+}
+
+//-------------------------------------------------------------------------------------------------
+Bool KillMuxData::passesChanceAndCooldown( UnsignedInt& lastTriggerFrame ) const
+{
+ UnsignedInt now = TheGameLogic->getFrame();
+
+ // still cooling down? punt (don't roll, don't reset the cooldown)
+ if (m_cooldownFrames > 0 && lastTriggerFrame != 0 && now < lastTriggerFrame + m_cooldownFrames)
+ return false;
+
+ // roll the trigger chance. a failed roll does NOT start the cooldown.
+ if (m_triggerChance < 1.0f && GameLogicRandomValueReal(0.0f, 1.0f) > m_triggerChance)
+ return false;
+
+ // triggered: remember when, to enforce the cooldown next time.
+ lastTriggerFrame = now;
+ return true;
+}
diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp
index 36a68421738..22f88f1a2e3 100644
--- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Body/ActiveBody.cpp
@@ -408,7 +408,7 @@ void ActiveBody::attemptDamage( DamageInfo *damageInfo )
{
//Bike is moving, so just blow it up instead.
if (damager)
- damager->scoreTheKill( obj );
+ damager->scoreTheKill( obj, damageInfo );
obj->kill();
}
else
@@ -423,7 +423,7 @@ void ActiveBody::attemptDamage( DamageInfo *damageInfo )
//Kill the rider.
if (damager)
- damager->scoreTheKill(rider);
+ damager->scoreTheKill(rider, damageInfo);
rider->kill();
}
}
@@ -475,7 +475,7 @@ void ActiveBody::attemptDamage( DamageInfo *damageInfo )
if (!thingToKill->isEffectivelyDead() )
{
if (damager)
- damager->scoreTheKill( thingToKill );
+ damager->scoreTheKill( thingToKill, damageInfo );
thingToKill->kill();
++numKilled;
thingToKill->getControllingPlayer()->getAcademyStats()->recordClearedGarrisonedBuilding();
@@ -717,7 +717,7 @@ void ActiveBody::attemptDamage( DamageInfo *damageInfo )
// Give our killer credit for killing us, if there is one.
if( damager )
{
- damager->scoreTheKill( obj );
+ damager->scoreTheKill( obj, damageInfo );
}
obj->onDie( damageInfo );
diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp
index 638be125900..9172f7850c1 100644
--- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp
@@ -77,6 +77,7 @@
#include "GameLogic/Module/DeletionUpdate.h"
#include "GameLogic/Module/DestroyModule.h"
#include "GameLogic/Module/DieModule.h"
+#include "GameLogic/Module/OnKillModule.h"
#include "GameLogic/Module/DozerAIUpdate.h"
#include "GameLogic/Module/LifeTimeUpdate.h"
#include "GameLogic/Module/ObjectDefectionHelper.h"
@@ -3119,8 +3120,18 @@ Bool Object::isMobile() const
}
//-------------------------------------------------------------------------------------------------
-void Object::scoreTheKill( const Object *victim )
+void Object::scoreTheKill( const Object *victim, const DamageInfo *damageInfo )
{
+ // Notify any OnKill modules first, before the score/experience early-outs below. These are FX/reaction
+ // modules that do their own relationship/status/kindof filtering, so they must fire even for kills the
+ // scorekeeper ignores (e.g. allies, neutrals, non-playable sides).
+ for (BehaviorModule** b = getBehaviorModules(); *b; ++b)
+ {
+ OnKillModuleInterface* onKill = (*b)->getOnKill();
+ if (onKill != nullptr)
+ onKill->onKilledObject( const_cast