diff --git a/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl b/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl index 5d736a6b92e..baa31f2de83 100644 --- a/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl +++ b/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl @@ -135,6 +135,7 @@ static PoolSizeRec PoolSizes[] = { "MissileAIUpdate", 512, 32 }, { "DumbProjectileBehavior", 64, 32 }, { "FreeFallProjectileBehavior", 32, 32 }, + { "JumpjetMissileAIUpdate", 16, 16 }, { "DestroyDie", 1024, 32 }, { "UpgradeDie", 128, 32 }, { "KeepObjectDie", 128, 32 }, @@ -278,6 +279,7 @@ static PoolSizeRec PoolSizes[] = { "TransitionDamageFX", 384, 128 }, { "TransportAIUpdate", 64, 32 }, { "TransportContain", 128, 32 }, + { "JumpjetContain", 16, 16 }, { "RiderChangeContain", 128, 32 }, { "InternetHackContain", 16, 16 }, { "TunnelContain", 8, 8 }, diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index 9055ccecbe2..39d410a8303 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -371,6 +371,7 @@ set(GAMEENGINE_SRC Include/GameLogic/Module/MaxHealthUpgrade.h Include/GameLogic/Module/MinefieldBehavior.h Include/GameLogic/Module/MissileAIUpdate.h + Include/GameLogic/Module/JumpjetMissileAIUpdate.h Include/GameLogic/Module/MissileLauncherBuildingUpdate.h Include/GameLogic/Module/MobMemberSlavedUpdate.h Include/GameLogic/Module/MobNexusContain.h @@ -483,6 +484,7 @@ set(GAMEENGINE_SRC Include/GameLogic/Module/TransportAIUpdate.h Include/GameLogic/Module/TransportContain.h Include/GameLogic/Module/TunnelContain.h + Include/GameLogic/Module/JumpjetContain.h Include/GameLogic/Module/UndeadBody.h Include/GameLogic/Module/UnitCrateCollide.h Include/GameLogic/Module/UnpauseSpecialPowerUpgrade.h @@ -960,6 +962,7 @@ set(GAMEENGINE_SRC Source/GameLogic/Object/Contain/RiderChangeContain.cpp Source/GameLogic/Object/Contain/TransportContain.cpp Source/GameLogic/Object/Contain/TunnelContain.cpp + Source/GameLogic/Object/Contain/JumpjetContain.cpp Source/GameLogic/Object/Contain/DroneCarrierContain.cpp Source/GameLogic/Object/Create/CreateModule.cpp Source/GameLogic/Object/Create/GrantUpgradeCreate.cpp @@ -1026,6 +1029,7 @@ set(GAMEENGINE_SRC Source/GameLogic/Object/Update/AIUpdate/HackInternetAIUpdate.cpp Source/GameLogic/Object/Update/AIUpdate/JetAIUpdate.cpp Source/GameLogic/Object/Update/AIUpdate/MissileAIUpdate.cpp + Source/GameLogic/Object/Update/AIUpdate/JumpjetMissileAIUpdate.cpp Source/GameLogic/Object/Update/AIUpdate/POWTruckAIUpdate.cpp Source/GameLogic/Object/Update/AIUpdate/RailedTransportAIUpdate.cpp Source/GameLogic/Object/Update/AIUpdate/RailroadGuideAIUpdate.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/Common/SpecialPowerType.h b/GeneralsMD/Code/GameEngine/Include/Common/SpecialPowerType.h index 5f968b4fbf8..eb8e184a515 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/SpecialPowerType.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/SpecialPowerType.h @@ -222,6 +222,8 @@ enum SpecialPowerType CPP_11(: Int) SPECIAL_TOGGLE_DRAWBRIDGE, + SPECIAL_JUMPJET, + SPECIALPOWER_COUNT, // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // NEW CONSTANTS NEED A BEHAVIORTYPE DEFINED IN THE SPECIALPOWER OR return one in getFallbackBehaviorType in ActionManager.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/ControlBar.h b/GeneralsMD/Code/GameEngine/Include/GameClient/ControlBar.h index f2da56d89dd..ad97421c322 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/ControlBar.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/ControlBar.h @@ -99,6 +99,7 @@ enum CommandOption CPP_11(: Int) USES_MINE_CLEARING_WEAPONSET= 0x00200000, // uses the special mine-clearing weaponset, even if not current CAN_USE_WAYPOINTS = 0x00400000, // button has option to use a waypoint path MUST_BE_STOPPED = 0x00800000, // Unit must be stopped in order to be able to use button. + FORMATION_LAUNCH = 0x01000000, // code-only: jumpjet group launch, keep formation offset instead of random scatter. }; #ifdef DEFINE_COMMAND_OPTION_NAMES @@ -132,6 +133,7 @@ static const char *const TheCommandOptionNames[] = "USES_MINE_CLEARING_WEAPONSET", "CAN_USE_WAYPOINTS", "MUST_BE_STOPPED", + "FORMATION_LAUNCH", nullptr }; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Locomotor.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Locomotor.h index a9857c972ef..2a648ec51f3 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Locomotor.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Locomotor.h @@ -264,6 +264,7 @@ class Locomotor : public MemoryPoolObject, public Snapshot AsciiString getTemplateName() const { return m_template->m_name;} Real getMinSpeed() const { return m_template->m_minSpeed;} + Real getMinTurnSpeed() const { return m_template->m_minTurnSpeed;} ///< must be going >= this speed to turn (0 = can turn in place) Real getAccelPitchLimit() const { return m_template->m_accelPitchLimit;} ///< Maximum amount we will pitch up or down under acceleration (including recoil.) Real getDecelPitchLimit() const { return m_template->m_decelPitchLimit;} ///< Maximum amount we will pitch down under deceleration (including recoil.) Real getBounceKick() const { return m_template->m_bounceKick;} ///< How much simulating rough terrain "bounces" a wheel up. diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/JumpjetContain.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/JumpjetContain.h new file mode 100644 index 00000000000..a1fca9f5782 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/JumpjetContain.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 . +*/ + +//////////////////////////////////////////////////////////////////////////////// +// // +// (c) 2001-2003 Electronic Arts Inc. // +// // +//////////////////////////////////////////////////////////////////////////////// + +// FILE: ParachuteContain.h //////////////////////////////////////////////////////////////////////// +// Author: Steven Johnson, March 2002 +// Desc: Contain module for transport units. +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#ifndef __JumpjetContain_H_ +#define __JumpjetContain_H_ + +// USER INCLUDES ////////////////////////////////////////////////////////////////////////////////// +#include "GameLogic/Module/OpenContain.h" + +enum ModelConditionFlagType; + +//------------------------------------------------------------------------------------------------- +class JumpjetContainModuleData : public OpenContainModuleData +{ +public: + //Real m_pitchRateMax; + //Real m_rollRateMax; + //Real m_lowAltitudeDamping; + //Real m_paraOpenDist; ///< deploy the parachute when we have traveled this far + //Real m_freeFallDamagePercent; + ModelConditionFlagType m_conditionFlagFlying; ///< ModelConditionState to use when flying + ModelConditionFlagType m_conditionFlagLanding; ///< ModelConditionState to use before landing + Real m_landingDistance; ///< Distance from target to initiate landing state + Bool m_killWhenLandingInWater; ///< Kill the Rider when landing on water + Bool m_killWhenLandingOnCliff; ///< Kill the Rider when landing on cliffs + Bool m_killWhenLandingOnImpassable; ///< Kill the Rider when landing on impassable terrain + Real m_killWhenLandingInWaterSlop; ///< Water Surface offset when killing the rider + //AudioEventRTS m_parachuteOpenSound; + + JumpjetContainModuleData(); + static void buildFieldParse(MultiIniFieldParse& p); +}; + +//------------------------------------------------------------------------------------------------- +class JumpjetContain : public OpenContain +{ + + MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE(JumpjetContain, "JumpjetContain") + MAKE_STANDARD_MODULE_MACRO_WITH_MODULE_DATA(JumpjetContain, JumpjetContainModuleData) + +public: + + JumpjetContain(Thing* thing, const ModuleData* moduleData); + // virtual destructor prototype provided by memory pool declaration + + //virtual void onDrawableBoundToObject(); + + virtual Bool isValidContainerFor(const Object* obj, bool checkCapacity) const; + virtual Bool isEnclosingContainerFor(const Object* obj) const { return FALSE; } ///< Does this type of Contain Visibly enclose its contents? + virtual Bool isSpecialZeroSlotContainer() const { return true; } + + virtual void onContaining(Object* obj, Bool wasSelected); ///< object now contains 'obj' + virtual void onRemoving(Object* obj); ///< object no longer contains 'obj' + + virtual UpdateSleepTime update(); ///< called once per frame + + // virtual void containReactToTransformChange(); + + virtual void onCollide(Object* other, const Coord3D* loc, const Coord3D* normal); + virtual void onDie(const DamageInfo* damageInfo); + + // virtual void setOverrideDestination( const Coord3D *override ); ///< Instead of falling peacefully towards a clear spot, I will now aim here + +protected: + + virtual Bool isFullyEnclosingContainer() const { return false; } + virtual void positionContainedObjectsRelativeToContainer(); + +private: + + void positionRider(Object* obj); + /*void updateBonePositions(); + void updateOffsetsFromBones(); + void calcSwayMtx(const Coord3D* offset, Matrix3D* mtx); + void setSwayMatrices(); + + Real m_pitch; + Real m_roll; + Real m_pitchRate; + Real m_rollRate; + Real m_startZ; + Bool m_isLandingOverrideSet; + Coord3D m_landingOverride; + Coord3D m_riderAttachBone; + Coord3D m_riderSwayBone; + Coord3D m_paraAttachBone; + Coord3D m_paraSwayBone; + Coord3D m_riderAttachOffset; + Coord3D m_riderSwayOffset; + Coord3D m_paraAttachOffset; + Coord3D m_paraSwayOffset; + Bool m_needToUpdateRiderBones; + Bool m_needToUpdateParaBones; + Bool m_opened;*/ + + Bool m_landing; +}; + +#endif // __JumpjetContain_H_ + diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/JumpjetMissileAIUpdate.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/JumpjetMissileAIUpdate.h new file mode 100644 index 00000000000..7c0a4f83b60 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/JumpjetMissileAIUpdate.h @@ -0,0 +1,126 @@ +/* +** 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: MissileAIUpdate.h +// Author: Michael S. Booth, December 2001 +// Desc: Missile behavior + +#pragma once + +#ifndef _JUMPJET_MISSILE_AI_UPDATE_H_ +#define _JUMPJET_MISSILE_AI_UPDATE_H_ + +#include "Common/GameType.h" +#include "Common/GlobalData.h" +#include "GameLogic/Module/MissileAIUpdate.h" +//#include "GameLogic/WeaponBonusConditionFlags.h" +#include "Common/INI.h" +//#include "WWMath/Matrix3D.h" + +enum ParticleSystemID; +//class FXList; + + +//------------------------------------------------------------------------------------------------- +class JumpjetMissileAIUpdateModuleData : public MissileAIUpdateModuleData +{ +public: + Real m_initialPitchAngle; ///< initial pitch angle the missile will be launched at. + Real m_scatterRadius; ///< max random offset from target + Real m_maxSearchRadius; ///< max radius to search for possible target location + /*Bool m_avoidImpassable; ///< try to avoid impassable cells when finding target + Bool m_avoidWater; ///< try to avoid water when finding target + Bool m_avoidObjects; ///< try to objects when finding target*/ + JumpjetMissileAIUpdateModuleData(); + + static void buildFieldParse(MultiIniFieldParse& p); + +}; + +//------------------------------------------------------------------------------------------------- +class JumpjetMissileAIUpdate : public MissileAIUpdate +{ + MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE(JumpjetMissileAIUpdate, "JumpjetMissileAIUpdate") + MAKE_STANDARD_MODULE_MACRO_WITH_MODULE_DATA(JumpjetMissileAIUpdate, JumpjetMissileAIUpdateModuleData); + +public: + JumpjetMissileAIUpdate(Thing* thing, const ModuleData* moduleData); + + virtual void projectileFireAtObjectOrPosition(const Object* victim, const Coord3D* victimPos, const WeaponTemplate* detWeap, const ParticleSystemTemplate* exhaustSysOverride); + virtual Bool projectileHandleCollision(Object* other); + // virtual Bool processCollision(PhysicsBehavior *physics, Object *other); ///< Returns true if the physics collide should apply the force. Normally not. jba. + + virtual Bool canLaunchToPosition(const Coord3D* targetPos, Coord3D* newPos, Bool keepFormation = false); + + // virtual UpdateSleepTime update(); + virtual void onDelete(void); + + // Bool isLanding( void ); + Real getGoalDistance(void); + +protected: + + void detonate(); + +private: + + /*MissileStateType m_state; ///< the behavior state of the missile + UnsignedInt m_stateTimestamp; ///< time of state change + UnsignedInt m_nextTargetTrackTime; ///< if nonzero, how often we update our target pos + ObjectID m_launcherID; ///< ID of object that launched us (INVALID_ID if not yet launched) + ObjectID m_victimID; ///< ID of object that I am rocketing towards (INVALID_ID if not yet launched) + UnsignedInt m_fuelExpirationDate; ///< how long 'til we run out of fuel + Real m_noTurnDistLeft; ///< when zero, ok to start turning + Real m_maxAccel; + Coord3D m_originalTargetPos; ///< When firing uphill, we aim high to clear the brow of the hill. jba. + Coord3D m_prevPos; + WeaponBonusConditionFlags m_extraBonusFlags; + const WeaponTemplate* m_detonationWeaponTmpl; ///< weapon to fire at end (or null) + const ParticleSystemTemplate* m_exhaustSysTmpl; + ParticleSystemID m_exhaustID; ///< our exhaust particle system (if any) + UnsignedInt m_framesTillDecoyed; ///< Number of frames before missile will get distracted by decoy countermeasures. + Bool m_isTrackingTarget; ///< Was I originally shot at a moving object? + Bool m_isArmed; ///< if true, missile will explode on contact + Bool m_noDamage; ///< if true, missile will not cause damage when it detonates. (Used for flares). + Bool m_isJammed; ///< No target, just shooting at a scattered position + + void doPrelaunchState(); + void doLaunchState(); + void doIgnitionState(); + void doAttackState(Bool turnOK); + void doKillState(); + void doKillSelfState(); + void doDeadState(); + + void airborneTargetGone(); ///< My airborne target has died, so I have to do something cool to make up for that + + void tossExhaust(); + void switchToState(MissileStateType s);*/ + + Bool findOffsetPosition(const Coord3D* targetPos, Coord3D* newPos, Real minRadius, Real maxRadius); + +}; + +#endif // _JUMPJET_MISSILE_AI_UPDATE_H_ + diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/MissileAIUpdate.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/MissileAIUpdate.h index 6f6375fd7a1..490468cda89 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/MissileAIUpdate.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/MissileAIUpdate.h @@ -118,10 +118,13 @@ class MissileAIUpdate : public AIUpdateInterface, public ProjectileUpdateInterfa virtual UpdateSleepTime update(); virtual void onDelete( void ); + virtual void switchToState(MissileStateType s); + + virtual MissileStateType getMissileState() { return m_state; } protected: - void detonate(); + virtual void detonate(); private: @@ -157,7 +160,5 @@ class MissileAIUpdate : public AIUpdateInterface, public ProjectileUpdateInterfa void airborneTargetGone(); ///< My airborne target has died, so I have to do something cool to make up for that void tossExhaust(); - void switchToState(MissileStateType s); - }; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/SpecialAbilityUpdate.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/SpecialAbilityUpdate.h index 2d0d11bc6ec..d58e027d9d5 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/SpecialAbilityUpdate.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/SpecialAbilityUpdate.h @@ -76,6 +76,8 @@ class SpecialAbilityUpdateModuleData : public UpdateModuleData Bool m_approachRequiresLOS; Bool m_needToFaceTarget; Bool m_persistenceRequiresRecharge; + Bool m_requiresMoveToTurn; ///< if set, orient by moving toward target (for locomotors that can't turn in place) instead of facing in place + Real m_facingAngleTolerance; ///< heading delta (radians) considered "facing target" when m_requiresMoveToTurn const ParticleSystemTemplate *m_disableFXParticleSystem; AudioEventRTS m_packSound; @@ -113,6 +115,8 @@ class SpecialAbilityUpdateModuleData : public UpdateModuleData m_preTriggerUnstealthFrames = 0; m_needToFaceTarget = TRUE; m_persistenceRequiresRecharge = FALSE; + m_requiresMoveToTurn = FALSE; + m_facingAngleTolerance = 0.1f; // ~5.7 degrees } static void buildFieldParse(MultiIniFieldParse& p) @@ -159,6 +163,8 @@ class SpecialAbilityUpdateModuleData : public UpdateModuleData { "ApproachRequiresLOS", INI::parseBool, nullptr, offsetof( SpecialAbilityUpdateModuleData, m_approachRequiresLOS ) }, { "NeedToFaceTarget", INI::parseBool, nullptr, offsetof( SpecialAbilityUpdateModuleData, m_needToFaceTarget ) }, { "PersistenceRequiresRecharge",INI::parseBool, nullptr, offsetof( SpecialAbilityUpdateModuleData, m_persistenceRequiresRecharge ) }, + { "RequiresMoveToTurn", INI::parseBool, nullptr, offsetof( SpecialAbilityUpdateModuleData, m_requiresMoveToTurn ) }, + { "FacingAngleTolerance", INI::parseAngleReal, nullptr, offsetof( SpecialAbilityUpdateModuleData, m_facingAngleTolerance ) }, { 0, 0, 0, 0 } }; p.add(dataFieldParse); @@ -270,6 +276,7 @@ class SpecialAbilityUpdate : public SpecialPowerUpdateModule UnsignedInt m_animFrames; //Used for packing/unpacking unit before or after using ability. ObjectID m_targetID; Coord3D m_targetPos; + UnsignedInt m_commandOptions; //Command option flags this ability was triggered with (e.g. FORMATION_LAUNCH). Int m_locationCount; std::list m_specialObjectIDList; //The list of special objects UnsignedInt m_specialObjectEntries; //The size of the list of member Objects diff --git a/GeneralsMD/Code/GameEngine/Source/Common/RTS/ActionManager.cpp b/GeneralsMD/Code/GameEngine/Source/Common/RTS/ActionManager.cpp index 4fa79b6bc4b..166d9e2fcec 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/RTS/ActionManager.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/RTS/ActionManager.cpp @@ -1638,6 +1638,13 @@ Bool ActionManager::canDoSpecialPowerAtLocation( const Object *obj, const Coord3 if( TheTerrainLogic->isUnderwater( loc->x, loc->y ) ) return FALSE; } + case SPECIAL_JUMPJET: + { + if (TheTerrainLogic->isUnderwater(loc->x, loc->y) + || TheTerrainLogic->isCliffCell(loc->x, loc->y)) { + return FALSE; + } + } } // Last check is shroudedness, if it is cared about @@ -1688,6 +1695,7 @@ Bool ActionManager::canDoSpecialPowerAtLocation( const Object *obj, const Coord3 case SPECIAL_CLEANUP_AREA: case SPECIAL_SNEAK_ATTACK: case SPECIAL_BATTLESHIP_BOMBARDMENT: + case SPECIAL_JUMPJET: //Don't allow "damaging" special powers in shrouded areas, but Fogged are okay. return ThePartitionManager->getShroudStatusForPlayer( obj->getControllingPlayer()->getPlayerIndex(), loc ) != CELLSHROUD_SHROUDED; @@ -1952,6 +1960,7 @@ Bool ActionManager::canDoSpecialPowerAtObject( const Object *obj, const Object * case SPECIAL_LAUNCH_BAIKONUR_ROCKET: case SPECIAL_SNEAK_ATTACK: case SPECIAL_TOGGLE_DRAWBRIDGE: + case SPECIAL_JUMPJET: return false; case SPECIAL_REMOTE_CHARGES: diff --git a/GeneralsMD/Code/GameEngine/Source/Common/RTS/SpecialPower.cpp b/GeneralsMD/Code/GameEngine/Source/Common/RTS/SpecialPower.cpp index 8f1fd1456f9..2690e574d67 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/RTS/SpecialPower.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/RTS/SpecialPower.cpp @@ -221,6 +221,8 @@ const char* const SpecialPowerMaskType::s_bitNameList[] = "SPECIAL_TOGGLE_DRAWBRIDGE", + "SPECIAL_JUMPJET", + nullptr }; static_assert(ARRAY_SIZE(SpecialPowerMaskType::s_bitNameList) == SpecialPowerMaskType::NumBits + 1, "Incorrect array size"); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp index 75106e257ed..3d28ceae6c9 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp @@ -69,6 +69,7 @@ #include "GameLogic/Module/RailedTransportContain.h" #include "GameLogic/Module/RiderChangeContain.h" #include "GameLogic/Module/TransportContain.h" +#include "GameLogic/Module/JumpjetContain.h" #include "GameLogic/Module/MobNexusContain.h" #include "GameLogic/Module/TunnelContain.h" #include "GameLogic/Module/OverlordContain.h" @@ -159,6 +160,7 @@ #include "GameLogic/Module/SpecialPowerDesignatorUpdate.h" #include "GameLogic/Module/AutoDepositUpdate.h" #include "GameLogic/Module/MissileAIUpdate.h" +#include "GameLogic/Module/JumpjetMissileAIUpdate.h" #include "GameLogic/Module/NeutronMissileUpdate.h" #include "GameLogic/Module/OCLUpdate.h" #include "GameLogic/Module/PhysicsUpdate.h" @@ -376,6 +378,7 @@ void ModuleFactory::init( void ) addModule( GarrisonContain ); addModule( InternetHackContain ); addModule( TransportContain ); + addModule( JumpjetContain ); addModule( RiderChangeContain ); addModule( RailedTransportContain ); addModule( MobNexusContain ); @@ -447,6 +450,7 @@ void ModuleFactory::init( void ) addModule( BuffUpdate ); addModule( ArmorDamageScalarUpdate ); addModule( MissileAIUpdate ); + addModule( JumpjetMissileAIUpdate ); addModule( NeutronMissileUpdate ); addModule( FireSpreadUpdate ); addModule( FireWeaponUpdate ); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIGroup.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIGroup.cpp index 303b3056d0d..201cf472c43 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIGroup.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIGroup.cpp @@ -2748,6 +2748,12 @@ void AIGroup::groupDoSpecialPowerAtLocation( UnsignedInt specialPowerID, const C //This one requires a position + // Precompute the group center/formation state once so SPECIAL_JUMPJET members can each + // launch to their own formation-relative target instead of all piling on the click point. + Coord2D fMin, fMax; + Coord3D fCenter; + Bool isFormation = getMinMaxAndCenter( &fMin, &fMax, &fCenter ); + std::list::iterator i; for( i = m_memberList.begin(); i != m_memberList.end(); ) { @@ -2775,9 +2781,20 @@ void AIGroup::groupDoSpecialPowerAtLocation( UnsignedInt specialPowerID, const C SpecialPowerModuleInterface *mod = object->getSpecialPowerModule( spTemplate ); if( mod ) { + // Validity/range is still checked against the shared click point. if( TheActionManager->canDoSpecialPowerAtLocation( object, location, CMD_FROM_PLAYER, spTemplate, objectInWay, commandOptions ) ) { - mod->doSpecialPowerAtLocation( location, angle, commandOptions ); + // For jumpjet group launches, give each member its own formation-relative target + // so the group keeps its formation at the destination instead of stacking up. + Coord3D unitLoc = *location; + UnsignedInt opts = commandOptions; + if( spTemplate->getSpecialPowerType() == SPECIAL_JUMPJET ) + { + computeIndividualDestination( &unitLoc, location, object, &fCenter, isFormation ); + opts |= FORMATION_LAUNCH; + } + + mod->doSpecialPowerAtLocation( &unitLoc, angle, opts ); object->friend_setUndetectedDefector( FALSE );// My secret is out } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/JumpjetContain.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/JumpjetContain.cpp new file mode 100644 index 00000000000..5a1e72b5efe --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Contain/JumpjetContain.cpp @@ -0,0 +1,476 @@ +/* +** 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: ParachuteContain.cpp ////////////////////////////////////////////////////////////////////// +// Author: Steven Johnson, March 2002 +// Desc: Contain module for transport units. +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// USER INCLUDES ////////////////////////////////////////////////////////////////////////////////// +#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine + +#include "Common/CRCDebug.h" +#include "Common/Player.h" +#include "Common/RandomValue.h" +#include "Common/ThingTemplate.h" +#include "Common/Xfer.h" + +#include "GameLogic/AIPathfind.h" +#include "GameLogic/Locomotor.h" +#include "GameLogic/Module/AIUpdate.h" +#include "GameLogic/Module/JumpjetContain.h" +#include "GameLogic/Module/JumpjetMissileAIUpdate.h" +#include "GameLogic/Module/PhysicsUpdate.h" +#include "GameLogic/Object.h" +#include "GameLogic/PartitionManager.h" + +#include "GameClient/Drawable.h" + + +const Real NO_START_Z = 1e10; + + +#ifdef _INTERNAL +// for occasional debugging... +//#pragma optimize("", off) +//#pragma MESSAGE("************************************** WARNING, optimization disabled for debugging purposes") +#endif + + +// PRIVATE //////////////////////////////////////////////////////////////////////////////////////// + +//------------------------------------------------------------------------------------------------- +JumpjetContainModuleData::JumpjetContainModuleData() : + //m_pitchRateMax(0), + //m_rollRateMax(0), + //m_lowAltitudeDamping(0.2f), + //m_paraOpenDist(0.0f), + //m_freeFallDamagePercent(0.5f), + m_conditionFlagFlying(MODELCONDITION_FREEFALL), + m_conditionFlagLanding(MODELCONDITION_PARACHUTING), + m_landingDistance(0.0f), + m_killWhenLandingInWater(FALSE), + m_killWhenLandingOnCliff(FALSE), + m_killWhenLandingOnImpassable(FALSE), + m_killWhenLandingInWaterSlop(10.0f) +{ +} + +//------------------------------------------------------------------------------------------------- +void JumpjetContainModuleData::buildFieldParse(MultiIniFieldParse& p) +{ + OpenContainModuleData::buildFieldParse(p); + + static const FieldParse dataFieldParse[] = + { + { "FlyingConditionFlag", ModelConditionFlags::parseSingleBitFromINI, NULL, offsetof(JumpjetContainModuleData, m_conditionFlagFlying) }, + { "LandingConditionFlag", ModelConditionFlags::parseSingleBitFromINI, NULL, offsetof(JumpjetContainModuleData, m_conditionFlagLanding) }, + { "LandingDistance", INI::parseReal, NULL, offsetof(JumpjetContainModuleData, m_landingDistance) }, + { "KillWhenLandingInWater", INI::parseBool, NULL, offsetof(JumpjetContainModuleData, m_killWhenLandingInWater) }, + { "KillWhenLandingOnCliff", INI::parseBool, NULL, offsetof(JumpjetContainModuleData, m_killWhenLandingOnCliff) }, + { "KillWhenLandingOnImpassable", INI::parseBool, NULL, offsetof(JumpjetContainModuleData, m_killWhenLandingOnImpassable) }, + { "KillWhenLandingInWaterSlop", INI::parseReal, NULL, offsetof(JumpjetContainModuleData, m_killWhenLandingInWaterSlop) }, + { 0, 0, 0, 0 } + }; + p.add(dataFieldParse); +} + +// PUBLIC ///////////////////////////////////////////////////////////////////////////////////////// +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +JumpjetContain::JumpjetContain(Thing* thing, const ModuleData* moduleData) : + OpenContain(thing, moduleData) +{ + m_landing = false; + + //getObject()->setStatus( MAKE_OBJECT_STATUS_MASK( OBJECT_STATUS_PARACHUTING ) ); +} + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +JumpjetContain::~JumpjetContain(void) +{ +} + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +/** + can this container contain this kind of object? + and, if checkCapacity is TRUE, does this container have enough space left to hold the given unit? +*/ +Bool JumpjetContain::isValidContainerFor(const Object* rider, Bool checkCapacity) const +{ + if (!rider) + return false; + + // extend functionality + if (OpenContain::isValidContainerFor(rider, checkCapacity) == false) + return false; + + //Int transportSlotCount = rider->getTransportSlotCount(); + + //// if 0, this object isn't transportable. + //// (exception: infantry are always transportable by parachutes, regardless + //// of this.... this allows us to paradrop pilots, but not transport them + //// by other means) + //if (transportSlotCount == 0 && !rider->isKindOf(KINDOF_INFANTRY) && !rider->isKindOf(KINDOF_PARACHUTABLE)) + // return false; + + // we can only "hold" one item at a time. + if (getContainCount() > 0) + return false; + + return true; +} + + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +UpdateSleepTime JumpjetContain::update(void) +{ + OpenContain::update(); + + Object* parachute = getObject(); + if (parachute->isDisabledByType(DISABLED_HELD)) + { + return UPDATE_SLEEP_NONE; // my, that was easy + } + + Object* rider = (getContainCount() > 0) ? getContainList().front() : NULL; + + + // allow us to land on bridges! + const Coord3D* paraPos = getObject()->getPosition(); + PathfindLayerEnum newLayer = TheTerrainLogic->getHighestLayerForDestination(paraPos); + getObject()->setLayer(newLayer); + if (rider) + rider->setLayer(newLayer); + + // If we have lost our passenger for whatever reason, die early. Otherwise we just sit around forever. + if (getContainCount() == 0) + getObject()->kill(); + + + // the collide system doesn't always collide us with the ground if we fall into water. + // so force the issue. + // TODO: + //Real waterZ; + //if (!getObject()->isEffectivelyDead() + // && getObject()->getLayer() == LAYER_GROUND + // && TheTerrainLogic->isUnderwater(paraPos->x, paraPos->y, &waterZ) + // && (paraPos->z - waterZ) < d->m_killWhenLandingInWaterSlop) + //{ + // getObject()->kill(); + //} + + + // Check if MissileAI is in Attack state -> set landing conditionflag + if (!m_landing && (rider)) { + const JumpjetContainModuleData* d = getJumpjetContainModuleData(); + + //Note: Would it make more sense to check target distance here, and not search for the module every frame? + static NameKeyType key_JumpjetMissileAIUpdate = NAMEKEY("JumpjetMissileAIUpdate"); + JumpjetMissileAIUpdate* missileUpdate = (JumpjetMissileAIUpdate*)getObject()->findUpdateModule(key_JumpjetMissileAIUpdate); + + // if ((missileUpdate) && missileUpdate->isLanding()) { + if (missileUpdate) { + + Real goalDistance = missileUpdate->getGoalDistance(); + // DEBUG_LOG((">>>JJC: goalDistance = %f; m_landingDistance = %f\n", goalDistance, d->m_landingDistance)); + if (goalDistance > 0.0f && (d->m_landingDistance < 0 || goalDistance < d->m_landingDistance)) { + + if (d->m_conditionFlagFlying != MODELCONDITION_INVALID) { + rider->clearModelConditionState(d->m_conditionFlagFlying); + } + if (d->m_conditionFlagLanding != MODELCONDITION_INVALID) { + rider->setModelConditionState(d->m_conditionFlagLanding); + } + + m_landing = true; + } + } + } + + return UPDATE_SLEEP_NONE; +} + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +void JumpjetContain::onContaining(Object* rider, Bool wasSelected) +{ + OpenContain::onContaining(rider, wasSelected); + + // objects inside a transport are held + rider->setDisabled(DISABLED_HELD); + + // Do we need this status? + //rider->setStatus( MAKE_OBJECT_STATUS_MASK( OBJECT_STATUS_PARACHUTING ) ); + + const JumpjetContainModuleData* d = getJumpjetContainModuleData(); + if (!m_landing && d->m_conditionFlagFlying != MODELCONDITION_INVALID) { + rider->setModelConditionState(d->m_conditionFlagFlying); + } + + // position him correctly. + positionRider(rider); +} + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +void JumpjetContain::onRemoving(Object* rider) +{ + OpenContain::onRemoving(rider); + + const JumpjetContainModuleData* d = getJumpjetContainModuleData(); + + // object is no longer held inside a transport + rider->clearDisabled(DISABLED_HELD); + //rider->clearStatus( MAKE_OBJECT_STATUS_MASK( OBJECT_STATUS_PARACHUTING ) ); + + // mark parachute as "no-collisions"... it is just ephemeral at this point, + // and having the chute collide with the soldier (and both bounce apart) is + // just dumb-lookin'... + getObject()->setStatus(MAKE_OBJECT_STATUS_MASK(OBJECT_STATUS_NO_COLLISIONS)); + + // position him correctly. + positionRider(rider); + + + // Clear condition flags + if (d->m_conditionFlagLanding != MODELCONDITION_INVALID) { + rider->clearModelConditionState(d->m_conditionFlagLanding); + } + else if (d->m_conditionFlagFlying != MODELCONDITION_INVALID) { + rider->clearModelConditionState(d->m_conditionFlagFlying); + } + + // temporarily mark the guy as being allowed to fall + // (overriding his locomotor's stick-to-ground attribute). + // this will be reset (by PhysicsBehavior) when he touches the ground. + PhysicsBehavior* physics = rider->getPhysics(); + if (physics) + { + physics->setAllowToFall(true); + + Coord3D force; + force.zero(); + physics->applyForce(&force); // force its physics to wake up... should be done when DISABLED_HELD is cleared, but it not, and scared to do it now. + } + + + AIUpdateInterface* riderAI = rider->getAIUpdateInterface(); + if (riderAI) + { + Player* controller = rider->getControllingPlayer(); + if (controller && controller->isSkirmishAIPlayer()) + { + riderAI->aiHunt(CMD_FROM_AI); // hunt, as per Dustin's request. + } + else + { + bool hasRallyPoint = false; + // Get the transport of the rider + Object* transport = TheGameLogic->findObjectByID(rider->getProducerID()); + if (transport) + { + // Get the building that produced the transport + Object* transportProducer = TheGameLogic->findObjectByID(transport->getProducerID()); + if (transportProducer) + { + // See if we need to set a rally point for the object being parachuted + ExitInterface* exitInterface = transportProducer->getObjectExitInterface(); + if (exitInterface && exitInterface->useSpawnRallyPoint()) + { + exitInterface->exitObjectViaDoor(rider, DOOR_1); + hasRallyPoint = true; + } + } + } + + if (!hasRallyPoint) + riderAI->aiIdle(CMD_FROM_AI); // become idle. + } + } + + + // if we land in the water, we die. alas. + if (d->m_killWhenLandingInWater) { + const Coord3D* riderPos = rider->getPosition(); + Real waterZ, terrainZ; + if (TheTerrainLogic->isUnderwater(riderPos->x, riderPos->y, &waterZ, &terrainZ) + && riderPos->z <= waterZ + d->m_killWhenLandingInWaterSlop + && rider->getLayer() == LAYER_GROUND) + { + // don't call kill(); do it manually, so we can specify DEATH_FLOODED + DamageInfo damageInfo; + damageInfo.in.m_damageType = DAMAGE_WATER; // use this instead of UNRESISTABLE so we don't get a dusty damage effect + damageInfo.in.m_deathType = DEATH_FLOODED; + damageInfo.in.m_sourceID = INVALID_ID; + damageInfo.in.m_amount = HUGE_DAMAGE_AMOUNT; + rider->attemptDamage(&damageInfo); + } + } + + // Kill if we landed on impassable ground + Int cellX = REAL_TO_INT(rider->getPosition()->x / PATHFIND_CELL_SIZE); + Int cellY = REAL_TO_INT(rider->getPosition()->y / PATHFIND_CELL_SIZE); + + PathfindCell* cell = TheAI->pathfinder()->getCell(rider->getLayer(), cellX, cellY); + PathfindCell::CellType cellType = cell ? cell->getType() : PathfindCell::CELL_IMPASSABLE; + + // If we land outside the map from a faulty parachute, we die too. + // Otherwise we exist outside the PartitionManger like a cheater. + if (rider->isOffMap() + || ((cellType == PathfindCell::CELL_CLIFF) && d->m_killWhenLandingOnCliff) + || ((cellType == PathfindCell::CELL_WATER) && d->m_killWhenLandingInWater) + || ((cellType == PathfindCell::CELL_IMPASSABLE) && d->m_killWhenLandingOnImpassable) + ) + { + // The Paradrop command was legal, the parachute destination was legal, but the parachute + // can still fail to adjust back on the map. SO this is the place to cap the cheater. + rider->kill(); + } + + // Note: The SpecialPower enum determines which locations are allowed, so if we want to actually target + // water or cliffs, we need to allow it in the ActionManager + +} + +//------------------------------------------------------------------------------------------------- +void JumpjetContain::positionRider(Object* rider) +{ + rider->setOrientation(getObject()->getOrientation()); +} + +//------------------------------------------------------------------------------------------------- +void JumpjetContain::positionContainedObjectsRelativeToContainer() +{ + for (ContainedItemsList::const_iterator it = getContainList().begin(); it != getContainList().end(); ++it) + { + positionRider(*it); + } +} + +//------------------------------------------------------------------------------------------------- +void JumpjetContain::onDie(const DamageInfo* damageInfo) +{ + // if we are airborne when killed, the guy falls screaming to his death... + if (getObject()->isSignificantlyAboveTerrain()) + { + Object* rider = (getContainCount() > 0) ? getContainList().front() : NULL; + if (rider) + { + removeAllContained(); + //const JumpjetContainModuleData* d = getJumpjetContainnModuleData(); + //if (d->m_freeFallDamagePercent > 0.0f) + //{ + // // do some damage just for losing your parachute. + // // not very realistic, but practical to help ensure that + // // you really do die from going "splat" on the ground. + // DamageInfo extraDamageInfo; + // extraDamageInfo.in.m_damageType = DAMAGE_FALLING; + // extraDamageInfo.in.m_deathType = DEATH_SPLATTED; + // extraDamageInfo.in.m_sourceID = damageInfo->in.m_sourceID; + // extraDamageInfo.in.m_amount = rider->getBodyModule()->getMaxHealth() * d->m_freeFallDamagePercent; + // rider->attemptDamage(&extraDamageInfo); + //} + PhysicsBehavior* physics = rider->getPhysics(); + if (physics) + { + physics->setAllowToFall(true); + physics->setIsInFreeFall(true); // bwah ha ha + + Coord3D force; + force.zero(); + physics->applyForce(&force); // force its physics to wake up... should be done when DISABLED_HELD is cleared, but it not, and scared to do it now. + } + } + } + + + OpenContain::onDie(damageInfo); +} + +//------------------------------------------------------------------------------------------------- +void JumpjetContain::onCollide(Object* other, const Coord3D* loc, const Coord3D* normal) +{ + // Note that other == null means "collide with ground" + if (other == NULL) + { + // DEBUG_LOG((">>>JC - onCollide -> kill")); + + // if we're in a container (eg, a transport plane), just ignore this... + if (getObject()->getContainedBy() != NULL) + return; + + removeAllContained(); + + // TheGameLogic->destroyObject(obj); + // kill it, so that the chute's SlowDeath will trigger! + getObject()->kill(); + } +} + +// ------------------------------------------------------------------------------------------------ +/** CRC */ +// ------------------------------------------------------------------------------------------------ +void JumpjetContain::crc(Xfer* xfer) +{ + + // extend base class + OpenContain::crc(xfer); + +} // end crc + +// ------------------------------------------------------------------------------------------------ +/** Xfer method + * Version Info: + * 1: Initial version */ + // ------------------------------------------------------------------------------------------------ +void JumpjetContain::xfer(Xfer* xfer) +{ + + // version + XferVersion currentVersion = 1; + XferVersion version = currentVersion; + xfer->xferVersion(&version, currentVersion); + + // extend base class + OpenContain::xfer(xfer); + + //// landing + xfer->xferBool(&m_landing); + +} // end xfer + +// ------------------------------------------------------------------------------------------------ +/** Load post process */ +// ------------------------------------------------------------------------------------------------ +void JumpjetContain::loadPostProcess(void) +{ + + // extend base class + OpenContain::loadPostProcess(); + +} // end loadPostProcess diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/JumpjetMissileAIUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/JumpjetMissileAIUpdate.cpp new file mode 100644 index 00000000000..8c050ac9cb8 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/AIUpdate/JumpjetMissileAIUpdate.cpp @@ -0,0 +1,298 @@ +/* +** 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: MissileAIUpdate.cpp +// Author: Michael S. Booth, December 2001 +// Desc: Implementation of missile behavior + +#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine + +#include "Common/Thing.h" +#include "Common/ThingTemplate.h" +#include "Common/RandomValue.h" +#include "Common/BitFlagsIO.h" + +#include "GameLogic/AIPathfind.h" +#include "GameLogic/ExperienceTracker.h" +#include "GameLogic/GameLogic.h" +#include "GameLogic/Locomotor.h" +#include "GameLogic/Module/JumpjetMissileAIUpdate.h" +#include "GameLogic/Module/MissileAIUpdate.h" +#include "GameLogic/Module/ContainModule.h" +#include "GameLogic/Module/PhysicsUpdate.h" +#include "GameLogic/Object.h" +#include "GameLogic/PartitionManager.h" +#include "GameLogic/TerrainLogic.h" +#include "GameLogic/Weapon.h" + +#include "GameClient/Drawable.h" +#include "GameClient/FXList.h" +#include "GameClient/ParticleSys.h" + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +JumpjetMissileAIUpdateModuleData::JumpjetMissileAIUpdateModuleData() +{ + m_initialPitchAngle = 0.0f; + m_scatterRadius = 0.0f; + m_maxSearchRadius = 100.0f; + //m_avoidImpassable = TRUE; + //m_avoidObjects = TRUE; + //m_avoidWater = TRUE; +} + +//----------------------------------------------------------------------------- +void JumpjetMissileAIUpdateModuleData::buildFieldParse(MultiIniFieldParse& p) +{ + MissileAIUpdateModuleData::buildFieldParse(p); + + static const FieldParse dataFieldParse[] = + { + { "ScatterRadius", INI::parseReal, NULL, offsetof(JumpjetMissileAIUpdateModuleData, m_scatterRadius) }, + { "MaxSearchRadius", INI::parseReal, NULL, offsetof(JumpjetMissileAIUpdateModuleData, m_maxSearchRadius) }, + { "InitialPitchAngle", INI::parseAngleReal, NULL, offsetof(JumpjetMissileAIUpdateModuleData, m_initialPitchAngle) }, + //{ "TryToAvoidImpassable", INI::parseBool, NULL, offsetof(JumpjetMissileAIUpdateModuleData, m_avoidImpassable) }, + //{ "TryToAvoidWater", INI::parseBool, NULL, offsetof(JumpjetMissileAIUpdateModuleData, m_avoidWater) }, + //{ "TryToAvoidObjects", INI::parseBool, NULL, offsetof(JumpjetMissileAIUpdateModuleData, m_avoidObjects) }, + { 0, 0, 0, 0 } + }; + + p.add(dataFieldParse); +} + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- + +//------------------------------------------------------------------------------------------------- +JumpjetMissileAIUpdate::JumpjetMissileAIUpdate(Thing* thing, const ModuleData* moduleData) : MissileAIUpdate(thing, moduleData) +{ + // const JumpjetMissileAIUpdateModuleData* d = getJumpjetMissileAIUpdateModuleData(); +} + +//------------------------------------------------------------------------------------------------- +JumpjetMissileAIUpdate::~JumpjetMissileAIUpdate() +{ + +} + +//------------------------------------------------------------------------------------------------- +void JumpjetMissileAIUpdate::onDelete(void) +{ + MissileAIUpdate::onDelete(); +} + +//------------------------------------------------------------------------------------------------- +// The actual firing of the missile once setup. +//------------------------------------------------------------------------------------------------- +void JumpjetMissileAIUpdate::projectileFireAtObjectOrPosition(const Object* victim, const Coord3D* victimPos, const WeaponTemplate* detWeap, const ParticleSystemTemplate* exhaustSysOverride) +{ + const JumpjetMissileAIUpdateModuleData* d = getJumpjetMissileAIUpdateModuleData(); + + if (d->m_initialPitchAngle != 0.0f) { + Matrix3D mtx = *getObject()->getTransformMatrix(); + mtx.Rotate_Y(-(d->m_initialPitchAngle)); + getObject()->setTransformMatrix(&mtx); + } + + MissileAIUpdate::projectileFireAtObjectOrPosition(victim, victimPos, detWeap, exhaustSysOverride); +} + +//------------------------------------------------------------------------------------------------- +Bool JumpjetMissileAIUpdate::canLaunchToPosition(const Coord3D* targetPos, Coord3D* newTargetPos, Bool keepFormation) { + + const JumpjetMissileAIUpdateModuleData* d = getJumpjetMissileAIUpdateModuleData(); + //Coord3D newTargetPos = Coord3D(); + Bool positionFound = false; + + if (keepFormation) { + // Formation group launch: the target is already this unit's formation slot, so don't + // random-scatter. Start at the exact position and expand only enough to dodge occupied cells. + positionFound = findOffsetPosition(targetPos, newTargetPos, 0.0f, d->m_scatterRadius); + } + else { + // Look for suitable position (with scatter radius) + // if (d->m_scatterRadius > 0.0f) { + positionFound = findOffsetPosition(targetPos, newTargetPos, d->m_scatterRadius * 0.5f, d->m_scatterRadius); + //} + } + + // If we found none, increase search radius + if (!positionFound) { + positionFound = findOffsetPosition(targetPos, newTargetPos, d->m_scatterRadius, d->m_maxSearchRadius); + } + + // If we still haven't found one, abort. + if (!positionFound) { + DEBUG_LOG((">>>JumpjetMissileAIUpdate: No valid position found. Abort Launch!\n")); + return false; + } + + return true; +} + + +//------------------------------------------------------------------------------------------------- +Bool JumpjetMissileAIUpdate::findOffsetPosition(const Coord3D* targetPos, Coord3D* newPos, Real minRadius, Real maxRadius) +{ + FindPositionOptions fpOptions; + fpOptions.minRadius = GameLogicRandomValueReal(0, minRadius); + fpOptions.maxRadius = maxRadius; + fpOptions.flags = FPF_USE_HIGHEST_LAYER; + + + // const JumpjetMissileAIUpdateModuleData* d = getJumpjetMissileAIUpdateModuleData(); + //if (!d->m_avoidWater) fpOptions.flags |= FPF_IGNORE_WATER; + //if (!d->m_avoidObjects) fpOptions.flags |= FPF_IGNORE_ALL_OBJECTS; + + if (ThePartitionManager->findPositionAround(targetPos, &fpOptions, newPos)) { + return TheAI->pathfinder()->adjustToLandingDestination(getObject(), newPos); + } + + return false; +} + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +Bool JumpjetMissileAIUpdate::projectileHandleCollision(Object* other) +{ + Object* obj = getObject(); + // const JumpjetMissileAIUpdateModuleData* d = getJumpjetMissileAIUpdateModuleData(); + + if (other == NULL) { + // we hit the ground. Check to see if we hit something unexpected. + Coord3D goal = *getGoalPosition(); + Coord3D pos = *obj->getPosition(); + Coord3D delta; + delta.x = pos.x - goal.x; + delta.y = pos.y - goal.y; + delta.z = pos.z - goal.z; + if (delta.z > PATHFIND_CELL_SIZE_F) { + // we're above our target goal. + if (delta.length() > 3 * PATHFIND_CELL_SIZE_F) { + // we're somewhere else. + return true; + } + } + // DEBUG_LOG((">>>JJMAU - projectileHandleCollision - We hit the ground.\n")); + } + + if (other != NULL) + { + // We do not want to collide with other objects + // TODO: review this + + return true; + } + // collided with something... blow'd up! + // DEBUG_LOG((">>>JJMAU - detonate (projectileHandleCollission).\n")); + detonate(); + + // mark ourself as "no collisions" (since we might still exist in slow death mode) + obj->setStatus(MAKE_OBJECT_STATUS_MASK(OBJECT_STATUS_NO_COLLISIONS)); + return true; +} + +//------------------------------------------------------------------------------------------------- +void JumpjetMissileAIUpdate::detonate() +{ + // DEBUG_LOG((">>>JJMAU - detonate.\n")); + + Object* obj = getObject(); + + if (obj->getDrawable()) + obj->getDrawable()->setDrawableHidden(true); + // Delay destroying the object two frames to let the contrail catch up. jba. + + switchToState(KILL_SELF); + + obj->setStatus(MAKE_OBJECT_STATUS_MASK(OBJECT_STATUS_MISSILE_KILLING_SELF)); + +} + +// ------------------------------------------------------------------------------------------------ +//Bool JumpjetMissileAIUpdate::isLanding() +//{ +// return getMissileState() == ATTACK; +//} + +// ---------------- +Real JumpjetMissileAIUpdate::getGoalDistance() +{ + // TODO: We could move this to the JumpjetContain module instead. + // i.e. when the missile is launched, pass the GoalPosition to the JumpjetContain + // and check for proximity there + MissileStateType state = getMissileState(); + //DEBUG_LOG((">>>JJMAU: getGoalDistance - state = %d, ", state)); + if (state >= ATTACK) { + Coord3D goal = *getGoalPosition(); + Coord3D pos = *getObject()->getPosition(); + Coord3D delta; + delta.x = pos.x - goal.x; + delta.y = pos.y - goal.y; + delta.z = pos.z - goal.z; + // DEBUG_LOG(("delta.length = %f\n", delta.length())); + return delta.length(); + } + return -1.0f; +} + + +// ------------------------------------------------------------------------------------------------ +/** CRC */ +// ------------------------------------------------------------------------------------------------ +void JumpjetMissileAIUpdate::crc(Xfer* xfer) +{ + // extend base class + MissileAIUpdate::crc(xfer); +} // end crc + +// ------------------------------------------------------------------------------------------------ +/** Xfer method + * Version Info: + * 1: Initial version */ + // ------------------------------------------------------------------------------------------------ +void JumpjetMissileAIUpdate::xfer(Xfer* xfer) +{ + // version + const XferVersion currentVersion = 6; + XferVersion version = currentVersion; + xfer->xferVersion(&version, currentVersion); + + // extend base class + MissileAIUpdate::xfer(xfer); + +} // end xfer + +// ------------------------------------------------------------------------------------------------ +/** Load post process */ +// ------------------------------------------------------------------------------------------------ +void JumpjetMissileAIUpdate::loadPostProcess(void) +{ + // extend base class + MissileAIUpdate::loadPostProcess(); +} // end loadPostProcess diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/SpecialAbilityUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/SpecialAbilityUpdate.cpp index e521ffa50f0..edfc2072fd3 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/SpecialAbilityUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/SpecialAbilityUpdate.cpp @@ -50,6 +50,7 @@ #include "GameLogic/AIPathfind.h" #include "GameLogic/GameLogic.h" +#include "GameLogic/Locomotor.h" #include "GameLogic/Object.h" #include "GameLogic/PartitionManager.h" #include "GameLogic/Weapon.h" @@ -63,6 +64,7 @@ #include "GameLogic/Module/StickyBombUpdate.h" #include "GameLogic/Module/StealthUpdate.h" #include "GameLogic/Module/ContainModule.h" +#include "GameLogic/Module/JumpjetMissileAIUpdate.h" @@ -75,6 +77,7 @@ SpecialAbilityUpdate::SpecialAbilityUpdate( Thing *thing, const ModuleData* modu m_animFrames = 0; m_targetID = INVALID_ID; m_targetPos.zero(); + m_commandOptions = 0; m_locationCount = 0; m_specialObjectEntries = 0; m_noTargetCommand = false; @@ -404,12 +407,24 @@ UpdateSleepTime SpecialAbilityUpdate::update( void ) else if( isWithinStartAbilityRange() ) { m_withinStartAbilityRange = true; - if( !isFacing() && needToFace() ) + bool is_facing = isFacing(); + bool need_to_face = needToFace(); + + if (!is_facing && need_to_face) { startFacing(); return calcSleepTime(); } + // Do we need to wait for facing to complete? + switch (data->m_specialPowerTemplate->getSpecialPowerType()) + { + case SPECIAL_JUMPJET: + if (need_to_face && is_facing) return calcSleepTime(); + break; + } + + if( needToUnpack() ) { //STEP 2 -- UNPACK @@ -486,6 +501,7 @@ Bool SpecialAbilityUpdate::initiateIntentToDoSpecialPower( const SpecialPowerTem //Clear target values m_targetID = INVALID_ID; m_targetPos.zero(); + m_commandOptions = commandOptions; m_locationCount = 0; m_prepFrames = 0; m_animFrames = 0; @@ -1605,6 +1621,47 @@ void SpecialAbilityUpdate::triggerAbilityEffect() break; } + + case SPECIAL_JUMPJET: + { + Object* jumpjet = createSpecialObject(); + ContainModuleInterface* contain = jumpjet->getContain(); + + if (contain != NULL && contain->isValidContainerFor(getObject(), true)) + { + /*ProjectileUpdateInterface* pui = NULL; + for (BehaviorModule** u = jumpjet->getBehaviorModules(); *u; ++u) + { + if ((pui = (*u)->getProjectileUpdateInterface()) != NULL) + break; + }*/ + + static NameKeyType key_JumpjetMissileAIUpdate = NAMEKEY("JumpjetMissileAIUpdate"); + JumpjetMissileAIUpdate* update = (JumpjetMissileAIUpdate*)jumpjet->findUpdateModule(key_JumpjetMissileAIUpdate); + + if (update) { + Coord3D newTargetPos; + // When launched as part of a formation group, keep the per-unit target instead of scattering. + Bool keepFormation = (m_commandOptions & FORMATION_LAUNCH) != 0; + // DEBUG_LOG((">>> SAU - Try to Launch to pos (%f, %f, %f)\n", m_targetPos.x, m_targetPos.y, m_targetPos.z)); + Bool ok = update->canLaunchToPosition(&m_targetPos, &newTargetPos, keepFormation); + // DEBUG_LOG((">>> SAU - newPos (%f, %f, %f), OK = %d\n", newTargetPos.x, newTargetPos.y, newTargetPos.z, ok)); + + if (ok) { + contain->addToContain(getObject()); + + update->projectileFireAtObjectOrPosition( + NULL, + &newTargetPos, + NULL, + NULL + ); + } + // else { // We could not find a suitable target; abort the ability activation (not yet implemented) + //} + } + } + } } if( data->m_loseStealthOnTrigger && okToLoseStealth) @@ -1847,6 +1904,23 @@ Bool SpecialAbilityUpdate::isFacing() { if( !m_facingComplete && m_facingInitiated) { + const SpecialAbilityUpdateModuleData* data = getSpecialAbilityUpdateModuleData(); + Locomotor *loco = ai->getCurLocomotor(); + if( data->m_requiresMoveToTurn && loco && loco->getMinTurnSpeed() > 0.0f ) + { + //This locomotor can't turn in place (e.g. wings); we're moving toward the + //target to turn. Consider facing complete once our heading is within tolerance. + Real relAngle = ThePartitionManager->getRelativeAngle2D( getObject(), &m_targetPos ); + if( fabs( relAngle ) <= data->m_facingAngleTolerance ) + { + m_facingComplete = true; + ai->aiIdle( CMD_FROM_AI ); //stop the short move; ready to launch + return false; + } + //Still turning (while moving). + return true; + } + if( ai->isIdle() ) { //We finished facing the target @@ -1913,7 +1987,26 @@ void SpecialAbilityUpdate::startFacing() } else if( m_targetPos.x || m_targetPos.y || m_targetPos.z ) //It's zero if not used... { - ai->aiFacePosition( &m_targetPos, CMD_FROM_AI ); + const SpecialAbilityUpdateModuleData* data = getSpecialAbilityUpdateModuleData(); + Locomotor *loco = ai->getCurLocomotor(); + if( data->m_requiresMoveToTurn && loco && loco->getMinTurnSpeed() > 0.0f ) + { + //This locomotor can't turn in place (e.g. wings); facing in place does nothing. + if( fabs( ThePartitionManager->getRelativeAngle2D( getObject(), &m_targetPos ) ) <= data->m_facingAngleTolerance ) + { + //Already pointed at the target -- no need to move. + m_facingComplete = true; + } + else + { + //Move toward the target so the locomotor turns us; isFacing() stops us once aligned. + ai->aiMoveToPosition( &m_targetPos, CMD_FROM_AI ); + } + } + else + { + ai->aiFacePosition( &m_targetPos, CMD_FROM_AI ); + } } } @@ -1971,6 +2064,7 @@ void SpecialAbilityUpdate::endPreparation() case SPECIAL_REMOTE_CHARGES: case SPECIAL_DISGUISE_AS_VEHICLE: case SPECIAL_HELIX_NAPALM_BOMB: + case SPECIAL_JUMPJET: // No, don't delete placed charges. // -OR- Not applicable (doesn't use special objects). break; @@ -2003,13 +2097,14 @@ void SpecialAbilityUpdate::crc( Xfer *xfer ) // ------------------------------------------------------------------------------------------------ /** Xfer method * Version Info: - * 1: Initial version */ + * 1: Initial version + * 2: Added m_commandOptions */ // ------------------------------------------------------------------------------------------------ void SpecialAbilityUpdate::xfer( Xfer *xfer ) { // version - XferVersion currentVersion = 1; + XferVersion currentVersion = 2; XferVersion version = currentVersion; xfer->xferVersion( &version, currentVersion ); @@ -2031,6 +2126,12 @@ void SpecialAbilityUpdate::xfer( Xfer *xfer ) // target position xfer->xferCoord3D( &m_targetPos ); + // command options (v2+) + if( version >= 2 ) + { + xfer->xferUnsignedInt( &m_commandOptions ); + } + // location count xfer->xferInt( &m_locationCount );