From c637785e6a59fb4b2d764b68f517b2f282c2ed36 Mon Sep 17 00:00:00 2001 From: pWn3d Date: Sat, 11 Jul 2026 19:28:58 +0200 Subject: [PATCH] add chronosphere update --- .../System/GameMemoryInitPools_GeneralsMD.inl | 1 + .../GameClient/Module/W3DModelDraw.h | 1 + .../W3DDevice/GameClient/W3DTerrainTracks.h | 1 + .../GameClient/Drawable/Draw/W3DModelDraw.cpp | 10 + .../W3DDevice/GameClient/W3DTerrainTracks.cpp | 22 ++ .../GameEngine/Include/Common/DrawModule.h | 2 + GeneralsMD/Code/GameEngine/CMakeLists.txt | 2 + .../GameEngine/Include/Common/DrawModule.h | 2 + .../GameEngine/Include/Common/MessageStream.h | 1 + .../Include/GameClient/ControlBar.h | 10 + .../GameEngine/Include/GameClient/Drawable.h | 1 + .../GameEngine/Include/GameClient/InGameUI.h | 12 + .../Code/GameEngine/Include/GameLogic/AI.h | 1 + .../Module/ChronoSphereUpdateModule.h | 110 ++++++ .../GameEngine/Include/GameLogic/Object.h | 1 + .../Source/Common/MessageStream.cpp | 1 + .../Source/Common/Thing/ModuleFactory.cpp | 2 + .../GameEngine/Source/GameClient/Drawable.cpp | 11 + .../GameClient/GUI/ControlBar/ControlBar.cpp | 7 + .../GameEngine/Source/GameClient/InGameUI.cpp | 60 +++- .../GameClient/MessageStream/CommandXlat.cpp | 45 ++- .../Source/GameLogic/AI/AIGroup.cpp | 36 ++ .../Source/GameLogic/Object/Object.cpp | 30 ++ .../Update/ChronoSphereUpdateModule.cpp | 330 ++++++++++++++++++ .../GameLogic/System/GameLogicDispatch.cpp | 42 +++ 25 files changed, 736 insertions(+), 5 deletions(-) create mode 100644 GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ChronoSphereUpdateModule.h create mode 100644 GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ChronoSphereUpdateModule.cpp diff --git a/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl b/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl index 63edb33ffbc..3ec0d5cdae0 100644 --- a/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl +++ b/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl @@ -195,6 +195,7 @@ static PoolSizeRec PoolSizes[] = { "PilotFindVehicleUpdate", 256, 32 }, { "DemoTrapUpdate", 32, 32 }, { "ParticleUplinkCannonUpdate", 16, 16 }, + { "ChronoSphereUpdateModule", 8, 8 }, { "SpectreGunshipUpdate", 8, 8 }, { "SpectreGunshipDeploymentUpdate", 8, 8 }, { "KodiakUpdate", 8, 8 }, diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/Module/W3DModelDraw.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/Module/W3DModelDraw.h index 9eafd33e86c..cf98ce3f1d2 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/Module/W3DModelDraw.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/Module/W3DModelDraw.h @@ -392,6 +392,7 @@ class W3DModelDraw : public DrawModule, public ObjectDrawInterface virtual Bool isVisible() const; virtual void reactToTransformChange(const Matrix3D* oldMtx, const Coord3D* oldPos, Real oldAngle); + virtual void reactToTeleport(); virtual void reactToGeometryChange() { } // this method must ONLY be called from the client, NEVER From the logic, not even indirectly. diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DTerrainTracks.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DTerrainTracks.h index f1477731bdb..58107169a20 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DTerrainTracks.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/W3DTerrainTracks.h @@ -69,6 +69,7 @@ class TerrainTracksRenderObjClass : public W3DMPO, public RenderObjClass void addEdgeToTrack(Real x, Real y); ///< add a new segment to the track void addCapEdgeToTrack(Real x, Real y); ///< cap the existing segment so we can resume at an unconnected position. void setAirborne(void) {m_airborne = true; } ///< Starts a new section of track, generally after going airborne. + void breakTrack(void); ///< end current strip (last edge feathered) so the next edge starts a fresh, unconnected anchor (teleport - keeps existing edges, no bridging quad) void setOwnerDrawable(const Drawable *owner) {m_ownerDrawable = owner;} protected: diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp index 53c910bc94f..916862f2a8c 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp @@ -4168,6 +4168,16 @@ Int W3DModelDraw::getCurrentBonePositions( return posCount; } +//------------------------------------------------------------------------------------------------- +void W3DModelDraw::reactToTeleport() +{ + // The object was instantly relocated. Break the tread-mark strip so the next edge starts a fresh, + // unconnected anchor at the destination - no stretched bridging quad. Existing edges (the tracks + // laid before the teleport) are kept. + if (m_trackRenderObject) + m_trackRenderObject->breakTrack(); +} + //------------------------------------------------------------------------------------------------- void W3DModelDraw::reactToTransformChange( const Matrix3D* oldMtx, const Coord3D* oldPos, diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainTracks.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainTracks.cpp index d49ead1e582..dd30cf393be 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainTracks.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/W3DTerrainTracks.cpp @@ -294,6 +294,28 @@ void TerrainTracksRenderObjClass::addCapEdgeToTrack(Real x, Real y) m_haveAnchor=false; } +//============================================================================= +// TerrainTracksRenderObjClass::breakTrack +//============================================================================= +/** End the current strip so the next edge starts a fresh, unconnected anchor (used on teleport). +* The edge buffer is one continuous ring, so the existing edges are kept but the last one is +* feathered to transparent - that way the quad joining it to the first edge of the new strip is +* alpha0->alpha0 (invisible) and no line bridges the teleport distance. */ +//============================================================================= +void TerrainTracksRenderObjClass::breakTrack(void) +{ + if (m_activeEdgeCount > 0) + { + Int maxEdgeCount = TheTerrainTracksRenderObjClassSystem->m_maxTankTrackEdges; + Int lastAddedEdge = m_topIndex - 1; + if (lastAddedEdge < 0) + lastAddedEdge = maxEdgeCount - 1; + m_edges[lastAddedEdge].alpha = 0.0f; //feather the tip of the old strip + } + m_haveAnchor = false; //next edge starts a new anchor + m_haveCap = TRUE; +} + //============================================================================= // TerrainTracksRenderObjClass::addEdgeToTrack //============================================================================= diff --git a/Generals/Code/GameEngine/Include/Common/DrawModule.h b/Generals/Code/GameEngine/Include/Common/DrawModule.h index a68ffba6b1d..f15e4bd63a4 100644 --- a/Generals/Code/GameEngine/Include/Common/DrawModule.h +++ b/Generals/Code/GameEngine/Include/Common/DrawModule.h @@ -83,6 +83,8 @@ class DrawModule : public DrawableModule virtual void setTerrainDecalSize(Real x, Real y) {}; virtual void setTerrainDecalOpacity(Real o) {}; + virtual void reactToTeleport() {}; ///< object was instantly relocated (e.g. chronosphere) - break tread marks etc. + virtual void setFullyObscuredByShroud(Bool fullyObscured) = 0; virtual Bool isVisible() const { return true; } ///< for limiting tree sway, etc to visible objects diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index e3e5ffdb75c..405ec6b6811 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -281,6 +281,7 @@ set(GAMEENGINE_SRC Include/GameLogic/Module/CaveContain.h Include/GameLogic/Module/CheckpointUpdate.h Include/GameLogic/Module/ChinookAIUpdate.h + Include/GameLogic/Module/ChronoSphereUpdateModule.h Include/GameLogic/Module/CleanupAreaPower.h Include/GameLogic/Module/CleanupHazardUpdate.h Include/GameLogic/Module/CollideModule.h @@ -1058,6 +1059,7 @@ set(GAMEENGINE_SRC Source/GameLogic/Object/Update/BattlePlanUpdate.cpp Source/GameLogic/Object/Update/BoneFXUpdate.cpp Source/GameLogic/Object/Update/CheckpointUpdate.cpp + Source/GameLogic/Object/Update/ChronoSphereUpdateModule.cpp Source/GameLogic/Object/Update/CleanupHazardUpdate.cpp Source/GameLogic/Object/Update/CommandButtonHuntUpdate.cpp Source/GameLogic/Object/Update/DeletionUpdate.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/Common/DrawModule.h b/GeneralsMD/Code/GameEngine/Include/Common/DrawModule.h index 42a56fab1cb..595d7ce5c6b 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/DrawModule.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/DrawModule.h @@ -84,6 +84,8 @@ class DrawModule : public DrawableModule virtual void setTerrainDecalSize(Real x, Real y) {}; virtual void setTerrainDecalOpacity(Real o) {}; + virtual void reactToTeleport() {}; ///< object was instantly relocated (e.g. chronosphere) - break tread marks etc. + virtual void setFullyObscuredByShroud(Bool fullyObscured) = 0; virtual Bool isVisible() const { return true; } ///< for limiting tree sway, etc to visible objects diff --git a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h index fbdb8596731..48a78b88aec 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/MessageStream.h @@ -601,6 +601,7 @@ class GameMessage : public MemoryPoolObject MSG_LOGIC_CRC, ///< CRC from the logic passed around in a network game :) MSG_SET_MINE_CLEARING_DETAIL, ///< CRC from the logic passed around in a network game :) MSG_ENABLE_RETALIATION_MODE, ///< Turn retaliation mode on or off for the specified player. + MSG_DO_SPECIAL_POWER_AT_TWO_LOCATIONS, ///< chrono-style: (spID, srcLoc, destLoc, options, sourceID) - both points committed in one message MSG_BEGIN_DEBUG_NETWORK_MESSAGES = 1900, ///< network messages that exist only in debug/internal builds. all grouped separately. diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/ControlBar.h b/GeneralsMD/Code/GameEngine/Include/GameClient/ControlBar.h index ad97421c322..0b425c8ba7c 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/ControlBar.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/ControlBar.h @@ -44,6 +44,7 @@ class Object; class ThingTemplate; class WeaponTemplate; class SpecialPowerTemplate; +class FXList; class WindowVideoManager; class WindowVideoManager; class AnimateWindowManager; @@ -100,6 +101,7 @@ enum CommandOption CPP_11(: Int) 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. + NEED_TWO_TARGET_POS = 0x02000000, // command needs the user to select two target positions; only the 2nd click commits (chronosphere). }; #ifdef DEFINE_COMMAND_OPTION_NAMES @@ -134,6 +136,7 @@ static const char *const TheCommandOptionNames[] = "CAN_USE_WAYPOINTS", "MUST_BE_STOPPED", "FORMATION_LAUNCH", + "NEED_TWO_TARGET_POS", nullptr }; @@ -145,6 +148,7 @@ const UnsignedInt COMMAND_OPTION_NEED_TARGET = NEED_TARGET_NEUTRAL_OBJECT | NEED_TARGET_ALLY_OBJECT | NEED_TARGET_POS | + NEED_TWO_TARGET_POS | CONTEXTMODE_COMMAND; const UnsignedInt COMMAND_OPTION_NEED_OBJECT_TARGET = @@ -323,6 +327,7 @@ class CommandButton : public Overridable const AsciiString& getName() const { return m_name; } const AsciiString& getCursorName() const { return m_cursorName; } + const AsciiString& getSecondCursorName() const { return m_secondCursorName; } const AsciiString& getInvalidCursorName() const { return m_invalidCursorName; } const AsciiString& getTextLabel() const { return m_textLabel; } const AsciiString& getDescriptionLabel() const { return m_descriptionLabel; } @@ -333,6 +338,8 @@ class CommandButton : public Overridable GUICommandType getCommandType() const { return m_command; } UnsignedInt getOptions() const { return m_options; } OVERRIDE getThingTemplate() const { return m_thingTemplate; } + const ThingTemplate* getMarkerObject() const { return m_markerTemplate; } + const FXList* getMarkerFX() const { return m_markerFX; } const UpgradeTemplate* getUpgradeTemplate() const { return m_upgradeTemplate; } const SpecialPowerTemplate* getSpecialPowerTemplate() const { return m_specialPower; } RadiusCursorType getRadiusCursorType() const { return m_radiusCursor; } @@ -371,10 +378,13 @@ class CommandButton : public Overridable CommandButton* m_next; UnsignedInt m_options; ///< command options (see CommandOption enum) const ThingTemplate* m_thingTemplate; ///< for commands that use thing templates in command data + const ThingTemplate* m_markerTemplate; ///< client-only marker model shown at the 1st pick of a two-point (chronosphere) power + const FXList* m_markerFX; ///< one-shot client FX played at the 1st pick of a two-point power const UpgradeTemplate* m_upgradeTemplate; ///< for commands that use upgrade templates in command data const SpecialPowerTemplate* m_specialPower; ///< actual special power template RadiusCursorType m_radiusCursor; ///< radius cursor, if any AsciiString m_cursorName; ///< cursor name for placement (NEED_TARGET_POS) or valid version (CONTEXTMODE_COMMAND) + AsciiString m_secondCursorName; ///< cursor name for the 2nd pick of a two-point (NEED_TWO_TARGET_POS) power; falls back to m_cursorName if empty AsciiString m_invalidCursorName; ///< cursor name for invalid version // bleah. shouldn't be mutable, but is. sue me. (Kris) -snork! diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h index 5267e8f6f55..51a91f9394e 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h @@ -351,6 +351,7 @@ class Drawable : public Thing, // Override. void setPosition( const Coord3D *pos ); void reactToGeometryChange(); + void reactToTeleport(); ///< object was instantly relocated - break interpolated visuals (tread marks) const GeometryInfo& getDrawableGeometryInfo() const; diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h b/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h index c6d1f6348ba..edaecce3df2 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/InGameUI.h @@ -433,6 +433,13 @@ friend class Drawable; // for selection/deselection transactions virtual void setGUICommand( const CommandButton *command ); ///< the command has been clicked in the UI and needs additional data virtual const CommandButton *getGUICommand( void ) const; ///< get the pending gui command + // two-point (chronosphere) special power selection: the first click is captured client-side + // and committed nothing; only the second click dispatches. Right-click cancels (via setGUICommand). + virtual void setPendingSpecialPowerFirstLocation( const Coord3D *loc ); ///< store the first-click source location + virtual const Coord3D *getPendingSpecialPowerFirstLocation( void ) const { return &m_pendingSpecialPowerFirstLocation; } + virtual Bool hasPendingSpecialPowerFirstLocation( void ) const { return m_hasPendingSpecialPowerFirstLocation; } + virtual void clearPendingSpecialPowerFirstLocation( void ) { m_hasPendingSpecialPowerFirstLocation = FALSE; destroySpecialPowerSourceMarker(); } + // build interface virtual void placeBuildAvailable( const ThingTemplate *build, Drawable *buildDrawable ); ///< built thing being placed virtual const ThingTemplate *getPendingPlaceType( void ); ///< get item we're trying to place @@ -628,6 +635,8 @@ friend class Drawable; // for selection/deselection transactions protected: + void destroySpecialPowerSourceMarker( void ); ///< remove the two-point special power source marker drawable if present + // ---------------------------------------------------------------------------------------------- // Protected Types ------------------------------------------------------------------------------ // ---------------------------------------------------------------------------------------------- @@ -739,6 +748,9 @@ friend class Drawable; // for selection/deselection transactions MoveHintStruct m_moveHint[ MAX_MOVE_HINTS ]; Int m_nextMoveHint; const CommandButton * m_pendingGUICommand; ///< GUI command that needs additional interaction from the user + Coord3D m_pendingSpecialPowerFirstLocation; ///< first-click source for a two-point special power + Bool m_hasPendingSpecialPowerFirstLocation; ///< TRUE once the first click of a two-point special power is captured + Drawable * m_specialPowerSourceMarker; ///< client-only marker drawable shown at the first-click source (nullptr if none) BuildProgress m_buildProgress[ MAX_BUILD_PROGRESS ]; ///< progress for building units const ThingTemplate * m_pendingPlaceType; ///< type of built thing we're trying to place ObjectID m_pendingPlaceSourceObjectID; ///< source object of the thing constructing the item diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/AI.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/AI.h index 0118458a1aa..48060219140 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/AI.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/AI.h @@ -950,6 +950,7 @@ class AIGroup : public MemoryPoolObject, public Snapshot void groupDoSpecialPower( UnsignedInt specialPowerID, UnsignedInt commandOptions ); void groupDoSpecialPowerAtObject( UnsignedInt specialPowerID, Object *object, UnsignedInt commandOptions ); void groupDoSpecialPowerAtLocation( UnsignedInt specialPowerID, const Coord3D *location, Real angle, const Object *object, UnsignedInt commandOptions ); + void groupDoSpecialPowerAtTwoLocations( UnsignedInt specialPowerID, const Coord3D *source, const Coord3D *dest, UnsignedInt commandOptions ); #ifdef ALLOW_SURRENDER void groupSurrender( const Object *objWeSurrenderedTo, Bool surrender, CommandSourceType cmdSource ); #endif diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ChronoSphereUpdateModule.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ChronoSphereUpdateModule.h new file mode 100644 index 00000000000..c4b798eea5e --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ChronoSphereUpdateModule.h @@ -0,0 +1,110 @@ +/* +** 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: ChronoSphereUpdateModule.h ///////////////////////////////////////////////////////////////// +// Desc: Special power update module for a Red Alert 2 style chronosphere. The player selects two +// locations (source and destination); the second selection can be canceled. This first +// increment only captures the two points and wires the module - the teleport effect is TODO. +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +// INCLUDES /////////////////////////////////////////////////////////////////////////////////////// +#include "Common/KindOf.h" +#include "Common/Science.h" +#include "GameLogic/Module/UpdateModule.h" +#include "GameLogic/Module/SpecialPowerUpdateModule.h" + +// FORWARD REFERENCES ///////////////////////////////////////////////////////////////////////////// +class SpecialPowerModuleInterface; +class FXList; +class ObjectCreationList; + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +class ChronoSphereUpdateModuleData : public ModuleData +{ +public: + SpecialPowerTemplate *m_specialPowerTemplate; + + UnsignedInt m_teleportDelayFrames; ///< delay before the teleport happens + KindOfMaskType m_requiredKindOf; ///< whitelist: units must match to be affected + KindOfMaskType m_forbiddenKindOf; ///< blacklist: units matching are never affected + Real m_radius; ///< radius of the affected area at each point + + FXList *m_sourceFX; ///< FX at the source area + FXList *m_targetFX; ///< FX at the destination area + FXList *m_unitSourceFX; ///< FX on each teleported unit at the source + FXList *m_unitTargetFX; ///< FX on each teleported unit at the destination + + const ObjectCreationList *m_sourceOCL; ///< OCL fired at the source when the power activates (instant, ignores TeleportDelay) + const ObjectCreationList *m_targetOCL; ///< OCL fired at the destination when the power activates (instant) + + ChronoSphereUpdateModuleData(); + static void buildFieldParse(MultiIniFieldParse& p); + +private: + +}; + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +class ChronoSphereUpdateModule : public SpecialPowerUpdateModule +{ + + MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE( ChronoSphereUpdateModule, "ChronoSphereUpdateModule" ) + MAKE_STANDARD_MODULE_MACRO_WITH_MODULE_DATA( ChronoSphereUpdateModule, ChronoSphereUpdateModuleData ); + +public: + + ChronoSphereUpdateModule( Thing *thing, const ModuleData* moduleData ); + // virtual destructor prototype provided by memory pool declaration + + // SpecialPowerUpdateInterface + virtual Bool initiateIntentToDoSpecialPower(const SpecialPowerTemplate *specialPowerTemplate, const Object *targetObj, const Coord3D *targetPos, const Waypoint *way, UnsignedInt commandOptions ); + virtual Bool isSpecialAbility() const { return false; } + virtual Bool isSpecialPower() const { return true; } + virtual Bool isActive() const { return m_active; } + virtual SpecialPowerUpdateInterface* getSpecialPowerUpdateInterface() { return this; } + virtual CommandOption getCommandOption() const { return (CommandOption)0; } + virtual Bool isPowerCurrentlyInUse( const CommandButton *command = nullptr ) const { return m_active; } + virtual ScienceType getExtraRequiredScience() const { return SCIENCE_INVALID; } + + // The chronosphere delivers its destination (the 2nd click) through the existing + // overridable-destination channel - see Object::doSpecialPowerAtTwoLocations. + virtual Bool doesSpecialPowerHaveOverridableDestinationActive() const { return m_active; } + virtual Bool doesSpecialPowerHaveOverridableDestination() const { return true; } + virtual void setSpecialPowerOverridableDestination( const Coord3D *loc ); + + virtual void onObjectCreated(); + virtual UpdateSleepTime update(); + + // termination conditions + virtual DisabledMaskType getDisabledTypesToProcess() const { return MAKE_DISABLED_MASK4( DISABLED_SUBDUED, DISABLED_UNDERPOWERED, DISABLED_EMP, DISABLED_HACKED ); } + +protected: + + void doChronoTeleport(); ///< relocate all matching objects from source to destination, playing FX + + SpecialPowerModuleInterface* m_specialPowerModule; ///< cached paired power module (recharge/cost/timer) + + Coord3D m_sourceLocation; ///< first click - where things teleport FROM + Coord3D m_destLocation; ///< second click - where things teleport TO + UnsignedInt m_teleportFrame; ///< logic frame at which the teleport fires (activation + TeleportDelay) + Bool m_active; ///< TRUE while a teleport is pending (armed, not yet fired) +}; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h index 03d601d28e8..6f75bc8632a 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Object.h @@ -463,6 +463,7 @@ class Object : public Thing, public Snapshot void doSpecialPower( const SpecialPowerTemplate *specialPowerTemplate, UnsignedInt commandOptions, Bool forced = false ); ///< execute power void doSpecialPowerAtObject( const SpecialPowerTemplate *specialPowerTemplate, Object *obj, UnsignedInt commandOptions, Bool forced = false ); ///< execute power void doSpecialPowerAtLocation( const SpecialPowerTemplate *specialPowerTemplate, const Coord3D *loc, Real angle, UnsignedInt commandOptions, Bool forced = false ); ///< execute power + void doSpecialPowerAtTwoLocations( const SpecialPowerTemplate *specialPowerTemplate, const Coord3D *source, const Coord3D *dest, UnsignedInt commandOptions, Bool forced = false ); ///< execute chrono-style power (source + destination) void doSpecialPowerUsingWaypoints( const SpecialPowerTemplate *specialPowerTemplate, const Waypoint *way, UnsignedInt commandOptions, Bool forced = false ); ///< execute power void doCommandButton( const CommandButton *commandButton, CommandSourceType cmdSource ); diff --git a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp index 809f59f5ffa..82ffabb464a 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/MessageStream.cpp @@ -695,6 +695,7 @@ const char *GameMessage::getCommandTypeAsString(GameMessage::Type t) CASE_LABEL(MSG_OBJECT_JOINED_TEAM) CASE_LABEL(MSG_SET_MINE_CLEARING_DETAIL) CASE_LABEL(MSG_ENABLE_RETALIATION_MODE) + CASE_LABEL(MSG_DO_SPECIAL_POWER_AT_TWO_LOCATIONS) } #undef CASE_LABEL diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp index b94baca64c6..e5f5c36a8bb 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp @@ -150,6 +150,7 @@ #include "GameLogic/Module/PilotFindVehicleUpdate.h" #include "GameLogic/Module/DemoTrapUpdate.h" #include "GameLogic/Module/ParticleUplinkCannonUpdate.h" +#include "GameLogic/Module/ChronoSphereUpdateModule.h" #include "GameLogic/Module/SpectreGunshipUpdate.h" #include "GameLogic/Module/SpectreGunshipDeploymentUpdate.h" #include "GameLogic/Module/KodiakUpdate.h" @@ -481,6 +482,7 @@ void ModuleFactory::init( void ) addModule( PilotFindVehicleUpdate ); addModule( DemoTrapUpdate ); addModule( ParticleUplinkCannonUpdate ); + addModule( ChronoSphereUpdateModule ); addModule( SpectreGunshipUpdate ); addModule( SpectreGunshipDeploymentUpdate ); addModule( KodiakUpdate ); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index 225ef7818d5..791d7b2e040 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -669,6 +669,17 @@ Bool Drawable::isVisible() return FALSE; } +//------------------------------------------------------------------------------------------------- +/** Notify draw modules that the object was instantly relocated (teleport), so visual effects that + * interpolate between frames (e.g. terrain tread marks) don't stretch across the jump. */ +void Drawable::reactToTeleport() +{ + for (DrawModule** dm = getDrawModules(); *dm; ++dm) + { + (*dm)->reactToTeleport(); + } +} + //------------------------------------------------------------------------------------------------- Bool Drawable::getShouldAnimate( Bool considerPower ) const { diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp index 4152649ddc3..3fa68814c5c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/GUI/ControlBar/ControlBar.cpp @@ -65,6 +65,7 @@ #include "GameClient/ControlBar.h" #include "GameClient/ControlBarScheme.h" #include "GameClient/Drawable.h" +#include "GameClient/FXList.h" #include "GameClient/Display.h" #include "GameClient/DisplayStringManager.h" #include "GameClient/GameClient.h" @@ -114,7 +115,10 @@ const FieldParse CommandButton::s_commandButtonFieldParseTable[] = { "PurchasedLabel", INI::parseAsciiString, nullptr, offsetof( CommandButton, m_purchasedLabel ) }, { "ConflictingLabel", INI::parseAsciiString, nullptr, offsetof( CommandButton, m_conflictingLabel ) }, { "ButtonImage", INI::parseAsciiString, nullptr, offsetof( CommandButton, m_buttonImageName ) }, + { "MarkerObject", INI::parseThingTemplate, nullptr, offsetof( CommandButton, m_markerTemplate ) }, + { "MarkerFX", INI::parseFXList, nullptr, offsetof( CommandButton, m_markerFX ) }, { "CursorName", INI::parseAsciiString, nullptr, offsetof( CommandButton, m_cursorName ) }, + { "SecondCursorName", INI::parseAsciiString, nullptr, offsetof( CommandButton, m_secondCursorName ) }, { "InvalidCursorName", INI::parseAsciiString, nullptr, offsetof( CommandButton, m_invalidCursorName ) }, { "ButtonBorderType", INI::parseLookupList, CommandButtonMappedBorderTypeNames, offsetof( CommandButton, m_commandButtonBorder ) }, { "RadiusCursorType", INI::parseIndexList, TheRadiusCursorNames, offsetof( CommandButton, m_radiusCursor ) }, @@ -559,6 +563,8 @@ CommandButton::CommandButton( void ) m_command = GUI_COMMAND_NONE; m_thingTemplate = nullptr; + m_markerTemplate = nullptr; + m_markerFX = nullptr; m_upgradeTemplate = nullptr; m_weaponSlot = PRIMARY_WEAPON; m_maxShotsToFire = 0x7fffffff; // huge number @@ -574,6 +580,7 @@ CommandButton::CommandButton( void ) m_flashCount = 0; m_conflictingLabel.clear(); m_cursorName.clear(); + m_secondCursorName.clear(); m_descriptionLabel.clear(); m_invalidCursorName.clear(); m_name.clear(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp index 2ab2c519e8a..c0db27703a1 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/InGameUI.cpp @@ -57,6 +57,7 @@ #include "GameClient/GameText.h" #include "GameClient/GameWindowManager.h" #include "GameClient/Drawable.h" +#include "GameClient/FXList.h" #include "GameClient/GadgetPushButton.h" #include "GameClient/GameClient.h" #include "GameClient/GameWindowGlobal.h" @@ -1125,6 +1126,9 @@ InGameUI::InGameUI() } m_pendingGUICommand = nullptr; + m_pendingSpecialPowerFirstLocation.zero(); + m_hasPendingSpecialPowerFirstLocation = FALSE; + m_specialPowerSourceMarker = nullptr; // allocate an array for the placement icons m_placeIcon = NEW Drawable* [ TheGlobalData->m_maxLineBuildObjects ]; @@ -3087,7 +3091,12 @@ void InGameUI::createCommandHint( const GameMessage *msg ) switch( t ) { case GameMessage::MSG_VALID_GUICOMMAND_HINT: - cursorName = m_pendingGUICommand->getCursorName(); + // For a two-point (chronosphere) power, show a distinct cursor once the first + // point is captured so the player knows they are picking the destination. + if( m_hasPendingSpecialPowerFirstLocation && !m_pendingGUICommand->getSecondCursorName().isEmpty() ) + cursorName = m_pendingGUICommand->getSecondCursorName(); + else + cursorName = m_pendingGUICommand->getCursorName(); break; case GameMessage::MSG_INVALID_GUICOMMAND_HINT: default: @@ -3223,6 +3232,11 @@ void InGameUI::setGUICommand( const CommandButton *command ) if (TheRecorder->getMode() == RECORDERMODETYPE_PLAYBACK) return; + // Any change (or cancel) of the pending command drops a half-finished two-point selection. + // This is also the right-click cancel path (SelectionXlat calls setGUICommand(nullptr)). + m_hasPendingSpecialPowerFirstLocation = FALSE; + destroySpecialPowerSourceMarker(); + // sanity if( command ) { @@ -3295,6 +3309,47 @@ const CommandButton *InGameUI::getGUICommand( void ) const } +//------------------------------------------------------------------------------------------------- +/** Store the first-click source location of a two-point (chronosphere) special power. Nothing is + * committed to game logic here - the second click sends the committing message. */ +//------------------------------------------------------------------------------------------------- +void InGameUI::setPendingSpecialPowerFirstLocation( const Coord3D *loc ) +{ + if( loc == nullptr ) + return; + + m_pendingSpecialPowerFirstLocation = *loc; + m_hasPendingSpecialPowerFirstLocation = TRUE; + + // Optional client-only marker at the source point, configured on the pending command button. + // A model persists until the marker is destroyed (2nd click / cancel); an FXList is a one-shot. + destroySpecialPowerSourceMarker(); + if( m_pendingGUICommand ) + { + const ThingTemplate *markerTmpl = m_pendingGUICommand->getMarkerObject(); + if( markerTmpl ) + { + m_specialPowerSourceMarker = TheThingFactory->newDrawable( markerTmpl, (DrawableStatusBits)DRAWABLE_STATUS_NO_SAVE ); + if( m_specialPowerSourceMarker ) + m_specialPowerSourceMarker->setPosition( loc ); + } + + FXList::doFXPos( m_pendingGUICommand->getMarkerFX(), loc ); + } +} + +//------------------------------------------------------------------------------------------------- +/** Remove the two-point special power source marker drawable, if any. */ +//------------------------------------------------------------------------------------------------- +void InGameUI::destroySpecialPowerSourceMarker( void ) +{ + if( m_specialPowerSourceMarker ) + { + TheGameClient->destroyDrawable( m_specialPowerSourceMarker ); + m_specialPowerSourceMarker = nullptr; + } +} + //------------------------------------------------------------------------------------------------- /** Destroy any drawables we have in our placement icon array and set to null */ //------------------------------------------------------------------------------------------------- @@ -4857,7 +4912,8 @@ Bool InGameUI::canSelectedObjectsDoSpecialPower( const CommandButton *command, c //1) NO TARGET OR POS //2) COMMAND_OPTION_NEED_OBJECT_TARGET //3) NEED_TARGET_POS - Bool doAtPosition = BitIsSet( command->getOptions(), NEED_TARGET_POS ); + // A two-point (chronosphere) power validates each click as a location, just like NEED_TARGET_POS. + Bool doAtPosition = BitIsSet( command->getOptions(), NEED_TARGET_POS ) || BitIsSet( command->getOptions(), NEED_TWO_TARGET_POS ); Bool doAtObject = BitIsSet( command->getOptions(), COMMAND_OPTION_NEED_OBJECT_TARGET ); //Sanity checks diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp index fdccee91d6d..e059bace92b 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/MessageStream/CommandXlat.cpp @@ -1232,6 +1232,38 @@ GameMessage::Type CommandTranslator::issueSpecialPowerCommand( const CommandButt } } + else if( BitIsSet( command->getOptions(), NEED_TWO_TARGET_POS ) ) + { + //TWO LOCATION BASED SPECIAL (chronosphere: source + destination) + msgType = GameMessage::MSG_DO_SPECIAL_POWER_AT_TWO_LOCATIONS; + if( commandType == DO_COMMAND ) + { + if( !TheInGameUI->hasPendingSpecialPowerFirstLocation() ) + { + //First click: capture the source point client-side. Commit nothing to game logic + //and stay pending so the second click (or a right-click cancel) can follow. + TheInGameUI->setPendingSpecialPowerFirstLocation( pos ); + msgType = GameMessage::MSG_INVALID; + } + else + { + //Second click: emit ONE deterministic message carrying both points. + Coord3D source = *TheInGameUI->getPendingSpecialPowerFirstLocation(); + GameMessage *msg = TheMessageStream->appendMessage( msgType ); + msg->appendIntegerArgument( command->getSpecialPowerTemplate()->getID() ); + msg->appendLocationArgument( source ); + msg->appendLocationArgument( *pos ); + msg->appendIntegerArgument( command->getOptions() ); + msg->appendObjectIDArgument( specificSource ); + TheInGameUI->clearPendingSpecialPowerFirstLocation(); + + PickAndPlayInfo info; + info.m_drawTarget = target; + info.m_specialPowerType = command->getSpecialPowerTemplate()->getSpecialPowerType(); + pickAndPlayUnitVoiceResponse( TheInGameUI->getAllSelectedDrawables(), msgType, &info ); + } + } + } else if( BitIsSet( command->getOptions(), NEED_TARGET_POS ) ) { //LOCATION BASED SPECIAL @@ -1278,7 +1310,12 @@ GameMessage::Type CommandTranslator::issueSpecialPowerCommand( const CommandButt } } - if( command->getCommandType() == GUI_COMMAND_SPECIAL_POWER_FROM_SHORTCUT && commandType == DO_COMMAND ) + // Two-point (chronosphere) powers commit atomically on the 2nd click and don't use the + // fire-then-steer overridable-destination model, so skip the shortcut auto-select/steer block - + // it would deselect/select the firing object, which clears the pending first-point state and + // makes the 2nd click impossible. + if( command->getCommandType() == GUI_COMMAND_SPECIAL_POWER_FROM_SHORTCUT && commandType == DO_COMMAND + && !BitIsSet( command->getOptions(), NEED_TWO_TARGET_POS ) ) { Object *obj = sourceDraw->getObject(); SpecialPowerUpdateInterface *spUpdate = obj->findSpecialPowerWithOverridableDestination(); @@ -1793,8 +1830,10 @@ GameMessage::Type CommandTranslator::evaluateContextCommand( Drawable *draw, break; } - // null out the GUI command if we're actually doing something - if( type == DO_COMMAND ) + // null out the GUI command if we're actually doing something. + // Exception: a two-point (chronosphere) power keeps the command pending after the + // first click so the second click (or a right-click cancel) can still be handled. + if( type == DO_COMMAND && !TheInGameUI->hasPendingSpecialPowerFirstLocation() ) { TheInGameUI->setGUICommand( nullptr ); } diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIGroup.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIGroup.cpp index 201cf472c43..9abffda7dd8 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIGroup.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/AI/AIGroup.cpp @@ -2804,6 +2804,42 @@ void AIGroup::groupDoSpecialPowerAtLocation( UnsignedInt specialPowerID, const C } } +//------------------------------------------------------------------------------------------------- +// Chrono-style special power: the player picked a source and a destination. Both points arrive +// together; validity/range is checked against the source point. +//------------------------------------------------------------------------------------------------- +void AIGroup::groupDoSpecialPowerAtTwoLocations( UnsignedInt specialPowerID, const Coord3D *source, const Coord3D *dest, UnsignedInt commandOptions ) +{ + std::list::iterator i; + for( i = m_memberList.begin(); i != m_memberList.end(); ) + { + Object *object = (*i); + + ++i; // just in case the act of specialpowering changes this list + + const SpecialPowerTemplate *spTemplate = TheSpecialPowerStore->findSpecialPowerTemplateByID( specialPowerID ); + if( spTemplate ) + { + // Have to justify the execution in case someone changed their button + if( spTemplate->getRequiredScience() != SCIENCE_INVALID ) + { + if( !object->getControllingPlayer()->hasScience(spTemplate->getRequiredScience()) ) + continue;// Nice try, smacktard. + } + + SpecialPowerModuleInterface *mod = object->getSpecialPowerModule( spTemplate ); + if( mod ) + { + if( TheActionManager->canDoSpecialPowerAtLocation( object, source, CMD_FROM_PLAYER, spTemplate, nullptr, commandOptions ) ) + { + object->doSpecialPowerAtTwoLocations( spTemplate, source, dest, commandOptions ); + object->friend_setUndetectedDefector( FALSE );// My secret is out + } + } + } + } +} + /** * The unit(s)/structure will perform it's special power -- special powers triggered by buildings * don't use AIUpdateInterfaces!!! No special power uses an AIUpdateInterface immediately, but special diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp index 9172f7850c1..45a7045f715 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Object.cpp @@ -5740,6 +5740,36 @@ void Object::doSpecialPowerAtLocation( const SpecialPowerTemplate *specialPowerT } +//------------------------------------------------------------------------------------------------- +/** Execute a chrono-style special power that needs two points. The source is delivered like a + * normal location special; the destination is handed to the update module through the existing + * overridable-destination channel so we don't need a new interface method. */ +//------------------------------------------------------------------------------------------------- +void Object::doSpecialPowerAtTwoLocations( const SpecialPowerTemplate *specialPowerTemplate, + const Coord3D *source, const Coord3D *dest, UnsignedInt commandOptions, Bool forced ) +{ + + if (isDisabled()) + return; + + // sanity + if( !forced && TheSpecialPowerStore->canUseSpecialPower( this, specialPowerTemplate ) == FALSE ) + return; + + // get the module and execute at the source point + SpecialPowerModuleInterface *mod = getSpecialPowerModule( specialPowerTemplate ); + if( mod ) + { + mod->doSpecialPowerAtLocation( source, INVALID_ANGLE, commandOptions ); + + // hand the destination (second click) to the update module + SpecialPowerUpdateInterface *spu = findSpecialPowerWithOverridableDestination( specialPowerTemplate->getSpecialPowerType() ); + if( spu ) + spu->setSpecialPowerOverridableDestination( dest ); + } + +} + //------------------------------------------------------------------------------------------------- /** Execute special power */ //------------------------------------------------------------------------------------------------- diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ChronoSphereUpdateModule.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ChronoSphereUpdateModule.cpp new file mode 100644 index 00000000000..846dcebcf8a --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ChronoSphereUpdateModule.cpp @@ -0,0 +1,330 @@ +/* +** 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: ChronoSphereUpdateModule.cpp /////////////////////////////////////////////////////////////// +// Desc: Special power update module for a Red Alert 2 style chronosphere. The player selects two +// locations (source and destination); this increment captures both points and wires the +// module - the teleport effect is TODO. +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine + +// INCLUDES /////////////////////////////////////////////////////////////////////////////////////// +#include "Common/GameCommon.h" +#include "Common/ThingTemplate.h" +#include "Common/Xfer.h" + +#include "GameClient/Drawable.h" +#include "GameClient/FXList.h" + +#include "GameLogic/AI.h" +#include "GameLogic/AIPathfind.h" +#include "GameLogic/GameLogic.h" +#include "GameLogic/Object.h" +#include "GameLogic/ObjectCreationList.h" +#include "GameLogic/ObjectIter.h" +#include "GameLogic/PartitionManager.h" +#include "GameLogic/TerrainLogic.h" +#include "GameLogic/Module/AIUpdate.h" +#include "GameLogic/Module/SpecialPowerModule.h" +#include "GameLogic/Module/ChronoSphereUpdateModule.h" + +#include + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +ChronoSphereUpdateModuleData::ChronoSphereUpdateModuleData() +{ + m_specialPowerTemplate = nullptr; + m_teleportDelayFrames = 0; + m_radius = 0.0f; + m_sourceFX = nullptr; + m_targetFX = nullptr; + m_unitSourceFX = nullptr; + m_unitTargetFX = nullptr; + m_sourceOCL = nullptr; + m_targetOCL = nullptr; + // m_requiredKindOf / m_forbiddenKindOf default-construct empty +} + +//------------------------------------------------------------------------------------------------- +/*static*/ void ChronoSphereUpdateModuleData::buildFieldParse(MultiIniFieldParse& p) +{ + ModuleData::buildFieldParse(p); + + static const FieldParse dataFieldParse[] = + { + { "SpecialPowerTemplate", INI::parseSpecialPowerTemplate, nullptr, offsetof( ChronoSphereUpdateModuleData, m_specialPowerTemplate ) }, + { "TeleportDelay", INI::parseDurationUnsignedInt, nullptr, offsetof( ChronoSphereUpdateModuleData, m_teleportDelayFrames ) }, + { "RequiredKindOf", KindOfMaskType::parseFromINI, nullptr, offsetof( ChronoSphereUpdateModuleData, m_requiredKindOf ) }, + { "ForbiddenKindOf", KindOfMaskType::parseFromINI, nullptr, offsetof( ChronoSphereUpdateModuleData, m_forbiddenKindOf ) }, + { "Radius", INI::parseReal, nullptr, offsetof( ChronoSphereUpdateModuleData, m_radius ) }, + { "SourceFX", INI::parseFXList, nullptr, offsetof( ChronoSphereUpdateModuleData, m_sourceFX ) }, + { "TargetFX", INI::parseFXList, nullptr, offsetof( ChronoSphereUpdateModuleData, m_targetFX ) }, + { "UnitSourceFX", INI::parseFXList, nullptr, offsetof( ChronoSphereUpdateModuleData, m_unitSourceFX ) }, + { "UnitTargetFX", INI::parseFXList, nullptr, offsetof( ChronoSphereUpdateModuleData, m_unitTargetFX ) }, + { "SourceOCL", INI::parseObjectCreationList, nullptr, offsetof( ChronoSphereUpdateModuleData, m_sourceOCL ) }, + { "TargetOCL", INI::parseObjectCreationList, nullptr, offsetof( ChronoSphereUpdateModuleData, m_targetOCL ) }, + { nullptr, nullptr, nullptr, 0 } + }; + p.add(dataFieldParse); +} + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +ChronoSphereUpdateModule::ChronoSphereUpdateModule( Thing *thing, const ModuleData* moduleData ) : SpecialPowerUpdateModule( thing, moduleData ) +{ + m_specialPowerModule = nullptr; + m_sourceLocation.zero(); + m_destLocation.zero(); + m_teleportFrame = 0; + m_active = FALSE; +} + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +ChronoSphereUpdateModule::~ChronoSphereUpdateModule( void ) +{ +} + +//------------------------------------------------------------------------------------------------- +// Cache the paired SpecialPowerModule (recharge/cost/timer) so we can trigger it later. +//------------------------------------------------------------------------------------------------- +void ChronoSphereUpdateModule::onObjectCreated() +{ + const ChronoSphereUpdateModuleData *data = getChronoSphereUpdateModuleData(); + Object *obj = getObject(); + + if( !data->m_specialPowerTemplate ) + { + DEBUG_CRASH( ("%s object's ChronoSphereUpdateModule lacks access to the SpecialPowerTemplate. Needs to be specified in ini.", obj->getTemplate()->getName().str() ) ); + return; + } + + m_specialPowerModule = obj->getSpecialPowerModule( data->m_specialPowerTemplate ); +} + +//------------------------------------------------------------------------------------------------- +// First click: the source point arrives here as targetPos. The destination (second click) is +// delivered separately via setSpecialPowerOverridableDestination(). +//------------------------------------------------------------------------------------------------- +Bool ChronoSphereUpdateModule::initiateIntentToDoSpecialPower(const SpecialPowerTemplate *specialPowerTemplate, const Object *targetObj, const Coord3D *targetPos, const Waypoint *way, UnsignedInt commandOptions ) +{ + if( m_specialPowerModule == nullptr || m_specialPowerModule->getSpecialPowerTemplate() != specialPowerTemplate ) + { + // Make sure our modules are connected. + return FALSE; + } + + if( targetPos ) + { + m_sourceLocation.set( targetPos ); + } + else if( targetObj ) + { + m_sourceLocation.set( targetObj->getPosition() ); + } + + m_active = TRUE; + + DEBUG_LOG(( "ChronoSphereUpdateModule: source selected at (%.1f, %.1f, %.1f)", + m_sourceLocation.x, m_sourceLocation.y, m_sourceLocation.z )); + + return TRUE; +} + +//------------------------------------------------------------------------------------------------- +// Second click: the destination point arrives through the overridable-destination channel. +//------------------------------------------------------------------------------------------------- +void ChronoSphereUpdateModule::setSpecialPowerOverridableDestination( const Coord3D *loc ) +{ + if( loc == nullptr ) + return; + + m_destLocation.set( loc ); + + DEBUG_LOG(( "ChronoSphereUpdateModule: destination selected at (%.1f, %.1f, %.1f)", + m_destLocation.x, m_destLocation.y, m_destLocation.z )); + + // Both points are now chosen (second click). Trigger the paired SpecialPowerModule so it + // charges the cost, plays the initiate sound/EVA and starts its recharge cooldown. Because + // the SpecialAbility uses UpdateModuleStartsAttack = Yes, nothing fired until this call. + if( m_active && m_specialPowerModule ) + { + m_specialPowerModule->markSpecialPowerTriggered( &m_sourceLocation ); + } + + const ChronoSphereUpdateModuleData *data = getChronoSphereUpdateModuleData(); + + // Fire the source/destination OCLs instantly on activation (they ignore TeleportDelay). + // (Bool) disambiguates the createOwner overload from the angle overload. + ObjectCreationList::create( data->m_sourceOCL, getObject(), &m_sourceLocation, nullptr, (Bool)FALSE ); + ObjectCreationList::create( data->m_targetOCL, getObject(), &m_destLocation, nullptr, (Bool)FALSE ); + + // Arm the teleport: it fires TeleportDelay frames from now. m_active stays TRUE (pending) until + // update() performs the teleport. We must arm the wake here (outside update()); setWakeFrame is + // ignored if called from within update(). + m_teleportFrame = TheGameLogic->getFrame() + data->m_teleportDelayFrames; + setWakeFrame( getObject(), data->m_teleportDelayFrames > 0 ? UPDATE_SLEEP( data->m_teleportDelayFrames ) : UPDATE_SLEEP_NONE ); +} + +//------------------------------------------------------------------------------------------------- +/** The update callback. Fires the teleport once the armed TeleportDelay has elapsed. */ +//------------------------------------------------------------------------------------------------- +UpdateSleepTime ChronoSphereUpdateModule::update() +{ + if( !m_active || TheGameLogic->getFrame() < m_teleportFrame ) + { + // Not armed (or awoken early for some other reason) - go back to sleep. + return UPDATE_SLEEP_FOREVER; + } + + doChronoTeleport(); + + m_active = FALSE; + return UPDATE_SLEEP_FOREVER; +} + +//------------------------------------------------------------------------------------------------- +/** Relocate every object matching the KindOf filter within Radius of the source point to the + * destination point, preserving each object's offset from the area center. Plays the area FX at + * source/destination and the per-unit FX at each object's source and destination position. */ +//------------------------------------------------------------------------------------------------- +void ChronoSphereUpdateModule::doChronoTeleport() +{ + const ChronoSphereUpdateModuleData *data = getChronoSphereUpdateModuleData(); + + // Area FX at the two points (static form is null-safe). + FXList::doFXPos( data->m_sourceFX, &m_sourceLocation ); + FXList::doFXPos( data->m_targetFX, &m_destLocation ); + + // Gather matching objects first, then relocate them (avoid mutating the partition while iterating). + std::vector affected; + { + PartitionFilterAcceptByKindOf kindFilter( data->m_requiredKindOf, data->m_forbiddenKindOf ); + PartitionFilterAlive aliveFilter; + PartitionFilter *filters[] = { &kindFilter, &aliveFilter, nullptr }; + + SimpleObjectIterator *iter = ThePartitionManager->iterateObjectsInRange( + &m_sourceLocation, data->m_radius, FROM_CENTER_2D, filters ); + MemoryPoolObjectHolder hold( iter ); + + for( Object *o = iter->first(); o; o = iter->next() ) + { + if( o != getObject() ) // never teleport the caster itself + affected.push_back( o ); + } + } + + Pathfinder *pathfinder = TheAI->pathfinder(); + + for( std::vector::iterator it = affected.begin(); it != affected.end(); ++it ) + { + Object *obj = *it; + + // Destination keeps the object's offset from the source center; snap Z to the ground there. + const Coord3D *objPos = obj->getPosition(); + Coord3D dest; + dest.x = m_destLocation.x + (objPos->x - m_sourceLocation.x); + dest.y = m_destLocation.y + (objPos->y - m_sourceLocation.y); + dest.z = TheTerrainLogic->getGroundHeight( dest.x, dest.y ); + + // Per-unit FX at the source (before the move). + FXList::doFXObj( data->m_unitSourceFX, obj ); + + // Break interpolated visuals (tread marks) BEFORE the move: setPosition() adds a tread edge + // synchronously (reactToTransformChange), so breaking the strip afterwards is too late - the + // stretched bridge edge would already be in the buffer. Breaking first makes that edge start + // a fresh anchor at the destination, and the pre-teleport tracks are kept. + Drawable *draw = obj->getDrawable(); + if( draw ) + draw->reactToTeleport(); + + if( obj->isKindOf( KINDOF_STRUCTURE ) ) + { + // Structures are baked into the pathfind map as obstacles - bracket the move. + pathfinder->removeObjectFromPathfindMap( obj ); + obj->setPosition( &dest ); + pathfinder->addObjectToPathfindMap( obj ); + } + else + { + // Mobile units: stop current path/goal, move, then re-register footprint cells. + AIUpdateInterface *ai = obj->getAIUpdateInterface(); + if( ai ) + ai->aiIdle( CMD_FROM_AI ); + obj->setPosition( &dest ); + pathfinder->updatePos( obj, &dest ); + } + + // Per-unit FX at the destination (after the move). + FXList::doFXObj( data->m_unitTargetFX, obj ); + } +} + +// ------------------------------------------------------------------------------------------------ +/** CRC */ +// ------------------------------------------------------------------------------------------------ +void ChronoSphereUpdateModule::crc( Xfer *xfer ) +{ + // extend base class + SpecialPowerUpdateModule::crc( xfer ); +} + +// ------------------------------------------------------------------------------------------------ +/** Xfer method + * Version Info: + * 1: Initial version + * 2: Added m_teleportFrame (delayed teleport) */ +// ------------------------------------------------------------------------------------------------ +void ChronoSphereUpdateModule::xfer( Xfer *xfer ) +{ + // version + const XferVersion currentVersion = 2; + XferVersion version = currentVersion; + xfer->xferVersion( &version, currentVersion ); + + // extend base class + SpecialPowerUpdateModule::xfer( xfer ); + + // we do not need to tie up the special power module pointer, it is done on object creation + // SpecialPowerModuleInterface *m_specialPowerModule; + + xfer->xferCoord3D( &m_sourceLocation ); + xfer->xferCoord3D( &m_destLocation ); + if( version >= 2 ) + xfer->xferUnsignedInt( &m_teleportFrame ); + xfer->xferBool( &m_active ); +} + +// ------------------------------------------------------------------------------------------------ +/** Load post process */ +// ------------------------------------------------------------------------------------------------ +void ChronoSphereUpdateModule::loadPostProcess( void ) +{ + // extend base class + SpecialPowerUpdateModule::loadPostProcess(); + + // If a teleport was pending when the game was saved, re-arm the wake so it still fires. + if( m_active ) + { + UnsignedInt now = TheGameLogic->getFrame(); + UnsignedInt delay = (m_teleportFrame > now) ? (m_teleportFrame - now) : 0; + setWakeFrame( getObject(), delay > 0 ? UPDATE_SLEEP( delay ) : UPDATE_SLEEP_NONE ); + } +} diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp index 9bc0e3f9b43..234ed6fc90c 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/System/GameLogicDispatch.cpp @@ -761,6 +761,48 @@ void GameLogic::logicMessageDispatcher( GameMessage *msg, void *userData ) } + //--------------------------------------------------------------------------------------------- + // Chrono-style special power: source + destination committed together in one message. + case GameMessage::MSG_DO_SPECIAL_POWER_AT_TWO_LOCATIONS: + { + // first argument is the special power ID + UnsignedInt specialPowerID = msg->getArgument( 0 )->integer; + + // argument 1 is the source location (first click) + Coord3D sourceCoord = msg->getArgument(1)->location; + + // argument 2 is the destination location (second click) + Coord3D destCoord = msg->getArgument(2)->location; + + // Command button options -- special power may care about variance options + UnsignedInt options = msg->getArgument( 3 )->integer; + + // check for possible specific source, ignoring selection. + ObjectID sourceID = msg->getArgument(4)->objectID; + Object* source = findObjectByID(sourceID); + if (source != nullptr) + { + AIGroupPtr theGroup = TheAI->createGroup(); + theGroup->add(source); + theGroup->groupDoSpecialPowerAtTwoLocations( specialPowerID, &sourceCoord, &destCoord, options ); +#if RETAIL_COMPATIBLE_AIGROUP + TheAI->destroyGroup(theGroup); +#else + theGroup->removeAll(); +#endif + } + else + { + //Use the selected group! + if( currentlySelectedGroup ) + { + currentlySelectedGroup->groupDoSpecialPowerAtTwoLocations( specialPowerID, &sourceCoord, &destCoord, options ); + } + } + break; + + } + //--------------------------------------------------------------------------------------------- case GameMessage::MSG_DO_SPECIAL_POWER_AT_OBJECT: {