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(victim), damageInfo ); + } + // Do stuff that has nothing to do with experience points here, like tell our Player we killed something /// @todo Multiplayer score hook location?