diff --git a/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl b/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl index fa614607881..7641721fff0 100644 --- a/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl +++ b/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl @@ -175,6 +175,7 @@ static PoolSizeRec PoolSizes[] = { "BunkerBusterBehavior", 16, 16 }, { "ObjectTracker", 128, 32 }, { "OCLUpdate", 16, 16 }, + { "OrbitalBeamUpdate", 8, 8 }, { "BodyParticleSystem", 196, 64 }, { "HighlanderBody", 2048, 128 }, { "BridgeTowerBody", 16, 16}, diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt index 55ad63fdeac..10d74b81742 100644 --- a/GeneralsMD/Code/GameEngine/CMakeLists.txt +++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt @@ -390,6 +390,7 @@ set(GAMEENGINE_SRC Include/GameLogic/Module/OCLSpecialPower.h Include/GameLogic/Module/OCLUpdate.h Include/GameLogic/Module/OpenContain.h + Include/GameLogic/Module/OrbitalBeamUpdate.h Include/GameLogic/Module/OverchargeBehavior.h Include/GameLogic/Module/OverlordContain.h Include/GameLogic/Module/ParachuteContain.h @@ -1085,6 +1086,7 @@ set(GAMEENGINE_SRC Source/GameLogic/Object/Update/NeutronMissileSlowDeathUpdate.cpp Source/GameLogic/Object/Update/NeutronMissileUpdate.cpp Source/GameLogic/Object/Update/OCLUpdate.cpp + Source/GameLogic/Object/Update/OrbitalBeamUpdate.cpp Source/GameLogic/Object/Update/ParticleUplinkCannonUpdate.cpp Source/GameLogic/Object/Update/PhysicsUpdate.cpp Source/GameLogic/Object/Update/PilotFindVehicleUpdate.cpp diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/LaserUpdate.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/LaserUpdate.h index b26b5976059..2312dcbbb7b 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/LaserUpdate.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/LaserUpdate.h @@ -111,6 +111,9 @@ class LaserUpdate : public ClientUpdateModule void setDecayFrames( UnsignedInt decayFrames ); + void startFadeOut( UnsignedInt fadeFrames ); ///< begin fading the beam's alpha out now over fadeFrames + UnsignedInt getFadeOutFrames() const { return getLaserUpdateModuleData()->m_fadeOutDurationFrames; } ///< the fade-out duration configured on this laser template + const Coord3D* getStartPos() { return &m_startPos; } const Coord3D* getEndPos() { return &m_endPos; } diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/OrbitalBeamUpdate.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/OrbitalBeamUpdate.h new file mode 100644 index 00000000000..66af379fd5e --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/OrbitalBeamUpdate.h @@ -0,0 +1,169 @@ +/* +** 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: OrbitalBeamUpdate.h ///////////////////////////////////////////////////////////////////// +// Desc: Update module stub for an orbital beam effect. +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +// INCLUDES /////////////////////////////////////////////////////////////////////////////////////// +#include "GameLogic/Module/UpdateModule.h" +#include "Common/AudioEventRTS.h" + +// FORWARD REFERENCES ///////////////////////////////////////////////////////////////////////////// +class FXList; +class WeaponTemplate; +enum ParticleSystemID CPP_11(: Int); + +//------------------------------------------------------------------------------------------------- +enum InterpolationType CPP_11(: Int) +{ + INTERP_LINEAR = 0, ///< constant speed + INTERP_SMOOTH, ///< smoothstep: ease in/out + INTERP_SMOOTHER, ///< smootherstep: silkier ease in/out + INTERP_EASE_IN, ///< slow start, fast finish (quadratic) + INTERP_EASE_OUT, ///< fast start, slow finish (quadratic) + INTERP_EASE_IN_CUBIC, ///< stronger accelerating start + INTERP_EASE_OUT_CUBIC,///< stronger decelerating finish + INTERP_SINE, ///< cosine ease in/out + INTERP_EXPO_IN, ///< near-still then whip in + INTERP_EXPO_OUT, ///< instant burst then long settle + INTERP_CIRCULAR, ///< arc-like, hard finish + INTERP_BACK, ///< overshoots past the center then settles + INTERP_ELASTIC, ///< springy oscillation before settling + INTERP_BOUNCE, ///< bounces before settling + INTERP_STEP_HOLD, ///< holds at the ring, then ramps in near the end + INTERP_PULSE, ///< discrete stepped movement inward +}; + +//------------------------------------------------------------------------------------------------- +class OrbitalBeamUpdateModuleData : public UpdateModuleData +{ +public: + + OrbitalBeamUpdateModuleData(); + + static void buildFieldParse(MultiIniFieldParse& p); + static void parseAppendWeaponTemplate( INI* ini, void* instance, void* store, const void* userData ); ///< appends one weapon to the listable ChargeWeapon field + + UnsignedShort m_numChargeBeams; // < how many charge beams are spawned + Real m_chargeBeamRadius; // < distance from center the charge beams are spawned + Real m_chargeBeamHeight; // < height above the surface the charge/final beams originate from + Bool m_chargeBeamsStartCenter; // < if set, charge beams originate from a fixed apex above the object center instead of directly above each impact + Real m_chargeBeamRotation; // < total angle (radians) each beam sweeps around center while converging; 0 = no rotation + InterpolationType m_chargeBeamInterpolation; // < easing mode for the charge beam convergence animation + UnsignedInt m_initialDelayFrames; // < time until everything starts + UnsignedInt m_delayBetweenChargeBeamsFrames; // < delay between initial charge beams are spawned + UnsignedInt m_delayChargeToAnimFrames; // < delay between all charge beams are spawned and they start to animate + UnsignedInt m_beamAnimationDurationFrames; // < duration how long the animation phase is going + UnsignedInt m_delayAnimationToFinalFrames; // < delay between finished animation and final beam + UnsignedInt m_finalBeamDurationFrames; // < How long the final beam is active + + AsciiString m_chargeBeamLaserNameName; + AsciiString m_finalBeamLaserNameName; + + Bool m_randomizeChargeBeamOrder; // < if set, charge beams spawn in randomized order (same positions) + Bool m_hitWaterSurface; // < if set, beam endpoints rest on the water surface instead of the floor when over water + + const FXList* m_chargeBeamStartFX; // < optional FXList played at each charge beam's impact point when it spawns + const FXList* m_finalFX; // < optional FXList played once at the object center when the final beam appears + const WeaponTemplate* m_finalWeapon; // < optional weapon fired at the object center when the final beam appears + std::vector m_chargeWeapons; // < weapons fired at the object center during the charge beam animation stage + + AudioEventRTS m_initialSound; // < plays during the initial delay and charge beam spawning + AudioEventRTS m_chargeSound; // < plays during the charge beam animation + + std::vector m_chargeBeamParticleSystemNames; // < particle systems on each charge beam's impact point (both terrains) + std::vector m_chargeBeamParticleSystemNamesWater; // < additional systems only while the impact point is over water + std::vector m_chargeBeamParticleSystemNamesLand; // < additional systems only while the impact point is over land + +private: + +}; + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +class OrbitalBeamUpdate : public UpdateModule +{ + + MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE( OrbitalBeamUpdate, "OrbitalBeamUpdate" ) + MAKE_STANDARD_MODULE_MACRO_WITH_MODULE_DATA( OrbitalBeamUpdate, OrbitalBeamUpdateModuleData ) + +public: + + OrbitalBeamUpdate( Thing *thing, const ModuleData* moduleData ); + // virtual destructor prototype provided by memory pool declaration + + virtual UpdateSleepTime update(); + +protected: + + enum + { + STAGE_INITIAL_DELAY = 0, ///< waiting before anything happens + STAGE_SPAWN_CHARGE, ///< spawning the ring of charge beams + STAGE_ANIMATE, ///< charge beams translate inward toward the center + STAGE_FINAL, ///< charge beams gone, single final beam active + STAGE_DONE ///< final beam gone, object destroyed + }; + + Real surfaceZAt( Real x, Real y ) const; ///< beam endpoint z at (x,y): water surface when HitWaterSurface and over water, else ground height + Real interpolate( Real t ) const; ///< remap a 0..1 animation factor by the configured interpolation mode + Coord3D beamSkyPos( const Coord3D* groundPos, const Coord3D* center ) const; ///< beam origin: fixed apex above center (ChargeBeamsStartCenter) or directly above the impact + DrawableID spawnBeam( const AsciiString& tmplName, const Coord3D* startPos, const Coord3D* groundPos ); ///< spawn a start->ground laser drawable, returns its id + void killBeam( DrawableID& id ); ///< destroy a beam drawable immediately and invalidate the id + void beginBeamFadeOut( DrawableID& id ); ///< fade the beam out then destroy it later (or destroy now if no fade), invalidates the id + void updateDyingBeams( UnsignedInt now ); ///< destroy beams whose fade-out has elapsed + + void spawnChargeBeamSystems( const Coord3D* groundPos ); ///< create this beam's particle systems at its impact point + void moveChargeBeamSystems( size_t beamIndex, const Coord3D* groundPos ); ///< move a beam's particle systems to its impact point, swapping terrain sets on shoreline crossing + void destroyChargeBeamSystems(); ///< destroy all charge beam particle systems + void createTerrainSystems( Bool overWater, const Coord3D* pos, std::vector& out ); ///< create the water- or land-specific systems at pos + void createSystemsFromNames( const std::vector& names, const Coord3D* pos, std::vector& out ); ///< resolve names and create particle systems at pos (INVALID id kept for unresolved names) + void destroyParticleSystemList( std::vector& ids ); ///< destroy each valid system id then clear the list + + void startPhaseSound( AudioEventRTS& playing, const AudioEventRTS& tmpl ); ///< start a looping phase sound attached to this object + void stopPhaseSound( AudioEventRTS& playing ); ///< stop a phase sound (safe even if never started) + void fireFinalWeapon( const Coord3D* pos ); ///< fire the final weapon at pos and schedule the next shot from its fire rate + void updateChargeWeapons( const Coord3D* pos, UnsignedInt now ); ///< fire each due charge weapon at pos, rescheduling from its own fire rate + + UnsignedInt m_stage; ///< current stage of the effect + UnsignedInt m_stageStartFrame; ///< frame the current stage was entered + UnsignedInt m_nextChargeSpawnFrame; ///< next frame a charge beam may be spawned + UnsignedShort m_chargeBeamsSpawned; ///< how many charge beams have been spawned so far + Bool m_started; ///< has the effect been seeded on the first update + Bool m_chargeFadeStarted; ///< charge beams have begun fading out (at animation end) + UnsignedInt m_nextFinalFireFrame; ///< frame the next final-weapon shot is due (STAGE_FINAL) + DrawableID m_finalBeamID; ///< the final beam drawable + + std::vector m_nextChargeWeaponFire; ///< per charge weapon, frame its next shot is due (STAGE_ANIMATE) + + std::vector m_chargeBeamIDs; ///< the spawned charge beam drawables + std::vector m_chargeBeamGroundPos; ///< each charge beam's original ground position (ring), used to lerp inward + std::vector m_spawnOrder; ///< ring slot filled at each spawn step (shuffled when randomized) + std::vector m_dyingBeamIDs; ///< beams currently fading out, awaiting destruction + std::vector m_dyingBeamDeathFrame; ///< frame each dying beam should be destroyed + std::vector m_chargeBeamSystemIDs; ///< flat list of each charge beam's generic particle systems (beam i at [i*N .. i*N+N)) + std::vector< std::vector > m_chargeBeamTerrainSystemIDs; ///< per-beam water/land-specific systems (size varies with terrain) + std::vector m_chargeBeamOverWater; ///< per-beam last-known terrain (true = over water), for detecting shoreline crossings + + AudioEventRTS m_initialSoundEvent; ///< currently playing InitialSound (holds its handle) + AudioEventRTS m_chargeSoundEvent; ///< currently playing ChargeSound (holds its handle) + +}; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp index 03534f5358d..51e6227caf3 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp @@ -163,6 +163,7 @@ #include "GameLogic/Module/JumpjetMissileAIUpdate.h" #include "GameLogic/Module/NeutronMissileUpdate.h" #include "GameLogic/Module/OCLUpdate.h" +#include "GameLogic/Module/OrbitalBeamUpdate.h" #include "GameLogic/Module/PhysicsUpdate.h" #ifdef ALLOW_SURRENDER #include "GameLogic/Module/POWTruckAIUpdate.h" @@ -495,6 +496,7 @@ void ModuleFactory::init( void ) addModule( SlavedUpdate ); addModule( MobMemberSlavedUpdate ); addModule( OCLUpdate ); + addModule( OrbitalBeamUpdate ); addModule( SpecialAbilityUpdate ); addModule( MissileLauncherBuildingUpdate ); addModule( SupplyCenterProductionExitUpdate ); diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/ObjectCreationList.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/ObjectCreationList.cpp index b2d0f2c1302..8adb0e9ef52 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/ObjectCreationList.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/ObjectCreationList.cpp @@ -1072,7 +1072,10 @@ class GenericObjectCreationNugget : public ObjectCreationNugget } if (m_experienceSink && sourceObj) { - obj->getExperienceTracker()->setExperienceSink(sourceObj->getID()); + // Chain the sink: if the caller is itself a sink for someone else (e.g. a projectile + // whose sink is the launcher), pledge to that final owner, not the transient caller. + ObjectID sinkID = sourceObj->getExperienceTracker()->getExperienceSink(); + obj->getExperienceTracker()->setExperienceSink(sinkID != INVALID_ID ? sinkID : sourceObj->getID()); } if (m_inheritsWeaponBonus && sourceObj) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/LaserUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/LaserUpdate.cpp index fd6e1613953..1f00b1942cc 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/LaserUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/LaserUpdate.cpp @@ -316,6 +316,17 @@ void LaserUpdate::setDecayFrames( UnsignedInt decayFrames ) } } +//------------------------------------------------------------------------------------------------- +void LaserUpdate::startFadeOut( UnsignedInt fadeFrames ) +{ + if( fadeFrames == 0 ) + return; + + m_fadingOut = true; + m_fadeOutStartFrame = TheGameLogic->getFrame(); + m_fadeOutFinishFrame = m_fadeOutStartFrame + fadeFrames; +} + //------------------------------------------------------------------------------------------------- void LaserUpdate::initLaser( const Object *parent, const Object *target, const Coord3D *startPos, const Coord3D *endPos, AsciiString parentBoneName, Int sizeDeltaFrames ) @@ -566,6 +577,13 @@ void LaserUpdate::updateContinuousLaser(const Object* parent, const Object* targ } } + // Honor an explicit start override only when there is no parent object anchoring the origin. + // Parent-anchored lasers (e.g. weapon beams) keep their bone/parent-based start; a parentless + // caller that supplies an explicit start (e.g. a vertical orbital beam whose origin tracks the + // moving ground point) gets its origin updated, matching initLaser. + if (parent == NULL && startPos) + m_startPos = *startPos; + if (target && !endPos) { // If a target object, use it (unless we override it!) diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/OrbitalBeamUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/OrbitalBeamUpdate.cpp new file mode 100644 index 00000000000..29091bd5827 --- /dev/null +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/OrbitalBeamUpdate.cpp @@ -0,0 +1,961 @@ +/* +** 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: OrbitalBeamUpdate.cpp /////////////////////////////////////////////////////////////////// +// Desc: Update module for an orbital beam effect: a ring of charge beams that converge inward +// and are then replaced by a single final beam, after which the object dies. +/////////////////////////////////////////////////////////////////////////////////////////////////// + +// INCLUDES /////////////////////////////////////////////////////////////////////////////////////// +#include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine + +#include "Common/GameAudio.h" +#include "Common/RandomValue.h" +#include "Common/Xfer.h" +#include "Common/ThingFactory.h" +#include "Common/ThingTemplate.h" +#include "GameClient/Drawable.h" +#include "GameClient/FXList.h" +#include "GameClient/GameClient.h" +#include "GameClient/ParticleSys.h" +#include "GameLogic/GameLogic.h" +#include "GameLogic/Object.h" +#include "GameLogic/TerrainLogic.h" +#include "GameLogic/Weapon.h" +#include "GameLogic/Module/LaserUpdate.h" +#include "GameLogic/Module/OrbitalBeamUpdate.h" + +// How far above the ground a beam starts (in the sky). Matches ParticleUplinkCannonUpdate. +constexpr const Real ORBITAL_BEAM_Z_OFFSET = 3500.0f; + +// Order must match enum InterpolationType. +static const char* TheInterpolationTypeNames[] = +{ + "LINEAR", + "SMOOTH", + "SMOOTHER", + "EASE_IN", + "EASE_OUT", + "EASE_IN_CUBIC", + "EASE_OUT_CUBIC", + "SINE", + "EXPO_IN", + "EXPO_OUT", + "CIRCULAR", + "BACK", + "ELASTIC", + "BOUNCE", + "STEP_HOLD", + "PULSE", + nullptr +}; + +//------------------------------------------------------------------------------------------------- +OrbitalBeamUpdateModuleData::OrbitalBeamUpdateModuleData() +{ + m_numChargeBeams = 0; + m_chargeBeamRadius = 0.0f; + m_chargeBeamHeight = ORBITAL_BEAM_Z_OFFSET; + m_chargeBeamsStartCenter = FALSE; + m_chargeBeamRotation = 0.0f; + m_chargeBeamInterpolation = INTERP_LINEAR; + m_initialDelayFrames = 0; + m_delayBetweenChargeBeamsFrames = 0; + m_delayChargeToAnimFrames = 0; + m_beamAnimationDurationFrames = 0; + m_delayAnimationToFinalFrames = 0; + m_finalBeamDurationFrames = 0; + m_randomizeChargeBeamOrder = FALSE; + m_hitWaterSurface = FALSE; + m_chargeBeamStartFX = nullptr; + m_finalFX = nullptr; + m_finalWeapon = nullptr; +} + +//------------------------------------------------------------------------------------------------- +/*static*/ void OrbitalBeamUpdateModuleData::buildFieldParse(MultiIniFieldParse& p) +{ + UpdateModuleData::buildFieldParse(p); + + static const FieldParse dataFieldParse[] = + { + { "NumChargeBeams", INI::parseUnsignedShort, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_numChargeBeams ) }, + { "ChargeBeamRadius", INI::parseReal, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_chargeBeamRadius ) }, + { "ChargeBeamHeight", INI::parseReal, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_chargeBeamHeight ) }, + { "ChargeBeamsStartCenter", INI::parseBool, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_chargeBeamsStartCenter ) }, + { "ChargeBeamRotation", INI::parseAngleReal, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_chargeBeamRotation ) }, + { "ChargeBeamInterpolation", INI::parseIndexList, TheInterpolationTypeNames, offsetof( OrbitalBeamUpdateModuleData, m_chargeBeamInterpolation ) }, + { "InitialDelay", INI::parseDurationUnsignedInt, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_initialDelayFrames ) }, + { "DelayBetweenChargeBeams", INI::parseDurationUnsignedInt, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_delayBetweenChargeBeamsFrames ) }, + { "DelayChargeToAnim", INI::parseDurationUnsignedInt, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_delayChargeToAnimFrames ) }, + { "BeamAnimationDuration", INI::parseDurationUnsignedInt, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_beamAnimationDurationFrames ) }, + { "DelayAnimationToFinal", INI::parseDurationUnsignedInt, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_delayAnimationToFinalFrames ) }, + { "FinalBeamDuration", INI::parseDurationUnsignedInt, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_finalBeamDurationFrames ) }, + { "ChargeBeamName", INI::parseAsciiString, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_chargeBeamLaserNameName ) }, + { "FinalBeamName", INI::parseAsciiString, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_finalBeamLaserNameName ) }, + { "RandomizeChargeBeamOrder", INI::parseBool, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_randomizeChargeBeamOrder ) }, + { "HitWaterSurface", INI::parseBool, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_hitWaterSurface ) }, + { "ChargeBeamStartFX", INI::parseFXList, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_chargeBeamStartFX ) }, + { "FinalFX", INI::parseFXList, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_finalFX ) }, + { "InitialSound", INI::parseAudioEventRTS, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_initialSound ) }, + { "ChargeSound", INI::parseAudioEventRTS, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_chargeSound ) }, + { "FinalWeapon", INI::parseWeaponTemplate, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_finalWeapon ) }, + { "ChargeWeapon", OrbitalBeamUpdateModuleData::parseAppendWeaponTemplate, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_chargeWeapons ) }, + { "ChargeBeamParticleSystem", INI::parseAsciiStringVectorAppend, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_chargeBeamParticleSystemNames ) }, + { "ChargeBeamParticleSystemWater", INI::parseAsciiStringVectorAppend, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_chargeBeamParticleSystemNamesWater ) }, + { "ChargeBeamParticleSystemLand", INI::parseAsciiStringVectorAppend, nullptr, offsetof( OrbitalBeamUpdateModuleData, m_chargeBeamParticleSystemNamesLand ) }, + { nullptr, nullptr, nullptr, 0 } + }; + p.add(dataFieldParse); +} + +//------------------------------------------------------------------------------------------------- +/** Listable field: each occurrence appends one weapon template to the charge weapons vector. */ +//------------------------------------------------------------------------------------------------- +/*static*/ void OrbitalBeamUpdateModuleData::parseAppendWeaponTemplate( INI* ini, void* instance, void* store, const void* /*userData*/ ) +{ + std::vector* vec = (std::vector*)store; + const WeaponTemplate* w = nullptr; + INI::parseWeaponTemplate( ini, instance, &w, nullptr ); + if( w != nullptr ) + vec->push_back( w ); +} + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +OrbitalBeamUpdate::OrbitalBeamUpdate( Thing *thing, const ModuleData* moduleData ) : UpdateModule( thing, moduleData ) +{ + m_stage = STAGE_INITIAL_DELAY; + m_stageStartFrame = 0; + m_nextChargeSpawnFrame = 0; + m_chargeBeamsSpawned = 0; + m_started = FALSE; + m_chargeFadeStarted = FALSE; + m_nextFinalFireFrame = 0; + m_finalBeamID = INVALID_DRAWABLE_ID; +} + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +OrbitalBeamUpdate::~OrbitalBeamUpdate( void ) +{ + // Clean up any beams still alive if we get destroyed prematurely. + for( std::vector::iterator it = m_chargeBeamIDs.begin(); it != m_chargeBeamIDs.end(); ++it ) + { + killBeam( *it ); + } + for( std::vector::iterator it = m_dyingBeamIDs.begin(); it != m_dyingBeamIDs.end(); ++it ) + { + killBeam( *it ); + } + killBeam( m_finalBeamID ); + destroyChargeBeamSystems(); + stopPhaseSound( m_initialSoundEvent ); + stopPhaseSound( m_chargeSoundEvent ); +} + +//------------------------------------------------------------------------------------------------- +/** Beam endpoint z at (x,y): the water surface when HitWaterSurface is set and the point is over + * water, otherwise the ground height. */ +//------------------------------------------------------------------------------------------------- +Real OrbitalBeamUpdate::surfaceZAt( Real x, Real y ) const +{ + Real waterZ; + if( getOrbitalBeamUpdateModuleData()->m_hitWaterSurface && TheTerrainLogic->isUnderwater( x, y, &waterZ ) ) + return waterZ; + + return TheTerrainLogic->getGroundHeight( x, y ); +} + +//------------------------------------------------------------------------------------------------- +/** Remap a 0..1 animation factor by the configured interpolation mode. */ +//------------------------------------------------------------------------------------------------- +Real OrbitalBeamUpdate::interpolate( Real t ) const +{ + switch( getOrbitalBeamUpdateModuleData()->m_chargeBeamInterpolation ) + { + case INTERP_SMOOTH: + return t * t * (3.0f - 2.0f * t); // smoothstep + + case INTERP_SMOOTHER: + return t * t * t * (t * (t * 6.0f - 15.0f) + 10.0f); // smootherstep + + case INTERP_EASE_IN: + return t * t; + + case INTERP_EASE_OUT: + return t * (2.0f - t); + + case INTERP_EASE_IN_CUBIC: + return t * t * t; + + case INTERP_EASE_OUT_CUBIC: + { + const Real u = 1.0f - t; + return 1.0f - u * u * u; + } + + case INTERP_SINE: + return 0.5f - 0.5f * cos( PI * t ); + + case INTERP_EXPO_IN: + return t <= 0.0f ? 0.0f : (Real)pow( 2.0f, 10.0f * (t - 1.0f) ); + + case INTERP_EXPO_OUT: + return t >= 1.0f ? 1.0f : 1.0f - (Real)pow( 2.0f, -10.0f * t ); + + case INTERP_CIRCULAR: + return 1.0f - sqrt( 1.0f - t * t ); + + case INTERP_BACK: + { + // easeOutBack: overshoots past the center then settles back to it. + const Real c1 = 1.70158f; + const Real c3 = c1 + 1.0f; + const Real u = t - 1.0f; + return 1.0f + c3 * u * u * u + c1 * u * u; + } + + case INTERP_ELASTIC: + { + // easeOutElastic + if( t <= 0.0f ) return 0.0f; + if( t >= 1.0f ) return 1.0f; + const Real c4 = (2.0f * PI) / 3.0f; + return (Real)pow( 2.0f, -10.0f * t ) * sin( (t * 10.0f - 0.75f) * c4 ) + 1.0f; + } + + case INTERP_BOUNCE: + { + // easeOutBounce + const Real n1 = 7.5625f; + const Real d1 = 2.75f; + Real u = t; + if( u < 1.0f / d1 ) + return n1 * u * u; + else if( u < 2.0f / d1 ) + { + u -= 1.5f / d1; + return n1 * u * u + 0.75f; + } + else if( u < 2.5f / d1 ) + { + u -= 2.25f / d1; + return n1 * u * u + 0.9375f; + } + else + { + u -= 2.625f / d1; + return n1 * u * u + 0.984375f; + } + } + + case INTERP_STEP_HOLD: + // Hold at the ring, then ramp in over the last 20% of the animation. + return t < 0.8f ? 0.0f : (t - 0.8f) / 0.2f; + + case INTERP_PULSE: + { + // Discrete stepped movement inward. + const Real steps = 5.0f; + return floor( t * steps ) / steps; + } + + case INTERP_LINEAR: + default: + return t; + } +} + +//------------------------------------------------------------------------------------------------- +/** Beam origin: a fixed apex above the object center when ChargeBeamsStartCenter is set, otherwise + * directly above the impact point. Height above the local surface is ChargeBeamHeight. */ +//------------------------------------------------------------------------------------------------- +Coord3D OrbitalBeamUpdate::beamSkyPos( const Coord3D* groundPos, const Coord3D* center ) const +{ + const OrbitalBeamUpdateModuleData *data = getOrbitalBeamUpdateModuleData(); + + Coord3D sky; + if( data->m_chargeBeamsStartCenter ) + { + sky.x = center->x; + sky.y = center->y; + sky.z = center->z + data->m_chargeBeamHeight; + } + else + { + sky = *groundPos; + sky.z += data->m_chargeBeamHeight; + } + return sky; +} + +//------------------------------------------------------------------------------------------------- +/** Spawn a beam drawable from startPos down to groundPos. */ +//------------------------------------------------------------------------------------------------- +DrawableID OrbitalBeamUpdate::spawnBeam( const AsciiString& tmplName, const Coord3D* startPos, const Coord3D* groundPos ) +{ + if( tmplName.isEmpty() ) + return INVALID_DRAWABLE_ID; + + const ThingTemplate *tmpl = TheThingFactory->findTemplate( tmplName ); + if( tmpl == nullptr ) + return INVALID_DRAWABLE_ID; + + Drawable *beam = TheThingFactory->newDrawable( tmpl ); + if( beam == nullptr ) + return INVALID_DRAWABLE_ID; + + static const NameKeyType nameKeyClientUpdate = NAMEKEY( "LaserUpdate" ); + LaserUpdate *update = (LaserUpdate*)beam->findClientUpdateModule( nameKeyClientUpdate ); + if( update ) + { + update->initLaser( nullptr, nullptr, startPos, groundPos, AsciiString::TheEmptyString ); + } + + return beam->getID(); +} + +//------------------------------------------------------------------------------------------------- +/** Destroy a beam drawable and invalidate its id. */ +//------------------------------------------------------------------------------------------------- +void OrbitalBeamUpdate::killBeam( DrawableID& id ) +{ + if( id != INVALID_DRAWABLE_ID ) + { + Drawable *beam = TheGameClient->findDrawableByID( id ); + if( beam ) + TheGameClient->destroyDrawable( beam ); + id = INVALID_DRAWABLE_ID; + } +} + +//------------------------------------------------------------------------------------------------- +/** Fade the beam's alpha out then destroy it once the fade elapses. If no fade duration is + * configured, destroy it immediately. Either way the passed id is invalidated. */ +//------------------------------------------------------------------------------------------------- +void OrbitalBeamUpdate::beginBeamFadeOut( DrawableID& id ) +{ + if( id == INVALID_DRAWABLE_ID ) + return; + + Drawable *beam = TheGameClient->findDrawableByID( id ); + if( beam ) + { + static const NameKeyType nameKeyClientUpdate = NAMEKEY( "LaserUpdate" ); + LaserUpdate *update = (LaserUpdate*)beam->findClientUpdateModule( nameKeyClientUpdate ); + if( update ) + { + // Use the fade-out duration configured on the beam's own laser template. + const UnsignedInt fadeFrames = update->getFadeOutFrames(); + if( fadeFrames > 0 ) + { + update->startFadeOut( fadeFrames ); + m_dyingBeamIDs.push_back( id ); + m_dyingBeamDeathFrame.push_back( TheGameLogic->getFrame() + fadeFrames ); + id = INVALID_DRAWABLE_ID; + return; + } + } + } + + // No fade configured (or no laser module) -- destroy right away. + killBeam( id ); +} + +//------------------------------------------------------------------------------------------------- +/** Destroy any dying beams whose fade-out has elapsed. */ +//------------------------------------------------------------------------------------------------- +void OrbitalBeamUpdate::updateDyingBeams( UnsignedInt now ) +{ + for( size_t i = 0; i < m_dyingBeamIDs.size(); ) + { + if( now >= m_dyingBeamDeathFrame[ i ] ) + { + killBeam( m_dyingBeamIDs[ i ] ); + // swap-erase to keep it cheap; both lists stay in lockstep + m_dyingBeamIDs[ i ] = m_dyingBeamIDs.back(); + m_dyingBeamDeathFrame[ i ] = m_dyingBeamDeathFrame.back(); + m_dyingBeamIDs.pop_back(); + m_dyingBeamDeathFrame.pop_back(); + } + else + { + ++i; + } + } +} + +//------------------------------------------------------------------------------------------------- +/** Resolve each name to a particle system template and create it at pos. One id is appended per + * name (INVALID_PARTICLE_SYSTEM_ID for names that fail to resolve or create) so callers relying on + * a fixed per-beam stride stay aligned. */ +//------------------------------------------------------------------------------------------------- +void OrbitalBeamUpdate::createSystemsFromNames( const std::vector& names, const Coord3D* pos, std::vector& out ) +{ + for( std::vector::const_iterator it = names.begin(); it != names.end(); ++it ) + { + ParticleSystemID id = INVALID_PARTICLE_SYSTEM_ID; + + const ParticleSystemTemplate *tmpl = TheParticleSystemManager->findTemplate( *it ); + if( tmpl ) + { + ParticleSystem *system = TheParticleSystemManager->createParticleSystem( tmpl ); + if( system ) + { + system->setPosition( pos ); + id = system->getSystemID(); + } + } + + out.push_back( id ); + } +} + +//------------------------------------------------------------------------------------------------- +/** Create the water- or land-specific systems at pos, appending their ids to out. */ +//------------------------------------------------------------------------------------------------- +void OrbitalBeamUpdate::createTerrainSystems( Bool overWater, const Coord3D* pos, std::vector& out ) +{ + const OrbitalBeamUpdateModuleData *data = getOrbitalBeamUpdateModuleData(); + const std::vector& names = + overWater ? data->m_chargeBeamParticleSystemNamesWater : data->m_chargeBeamParticleSystemNamesLand; + + createSystemsFromNames( names, pos, out ); +} + +//------------------------------------------------------------------------------------------------- +/** Destroy each valid system id in the list, then clear it. */ +//------------------------------------------------------------------------------------------------- +void OrbitalBeamUpdate::destroyParticleSystemList( std::vector& ids ) +{ + for( std::vector::iterator it = ids.begin(); it != ids.end(); ++it ) + { + if( *it != INVALID_PARTICLE_SYSTEM_ID ) + TheParticleSystemManager->destroyParticleSystemByID( *it ); + } + ids.clear(); +} + +//------------------------------------------------------------------------------------------------- +/** Start a looping phase sound attached to this object (no-op if the event is unset). */ +//------------------------------------------------------------------------------------------------- +void OrbitalBeamUpdate::startPhaseSound( AudioEventRTS& playing, const AudioEventRTS& tmpl ) +{ + if( tmpl.getEventName().isEmpty() ) + return; + + playing = tmpl; + playing.setObjectID( getObject()->getID() ); + playing.setPlayingHandle( TheAudio->addAudioEvent( &playing ) ); +} + +//------------------------------------------------------------------------------------------------- +/** Stop a phase sound. Safe even if it was never started. */ +//------------------------------------------------------------------------------------------------- +void OrbitalBeamUpdate::stopPhaseSound( AudioEventRTS& playing ) +{ + TheAudio->removeAudioEvent( playing.getPlayingHandle() ); +} + +//------------------------------------------------------------------------------------------------- +/** Fire the final weapon at pos, then schedule the next shot from the weapon's own fire rate. */ +//------------------------------------------------------------------------------------------------- +void OrbitalBeamUpdate::fireFinalWeapon( const Coord3D* pos ) +{ + const OrbitalBeamUpdateModuleData *data = getOrbitalBeamUpdateModuleData(); + if( !data->m_finalWeapon ) + return; + + TheWeaponStore->createAndFireTempWeapon( data->m_finalWeapon, getObject(), pos ); + + WeaponBonus bonus; // defaults to 1.0 for all fields (rate-of-fire divisor = 1) + Int delay = data->m_finalWeapon->getDelayBetweenShots( bonus ); + if( delay < 1 ) + delay = 1; // never stall + + m_nextFinalFireFrame = TheGameLogic->getFrame() + delay; +} + +//------------------------------------------------------------------------------------------------- +/** Fire each charge weapon whose shot is due at pos, then reschedule it from its own fire rate. */ +//------------------------------------------------------------------------------------------------- +void OrbitalBeamUpdate::updateChargeWeapons( const Coord3D* pos, UnsignedInt now ) +{ + const OrbitalBeamUpdateModuleData *data = getOrbitalBeamUpdateModuleData(); + + for( size_t i = 0; i < data->m_chargeWeapons.size() && i < m_nextChargeWeaponFire.size(); ++i ) + { + if( now < m_nextChargeWeaponFire[ i ] ) + continue; + + TheWeaponStore->createAndFireTempWeapon( data->m_chargeWeapons[ i ], getObject(), pos ); + + WeaponBonus bonus; // defaults to 1.0 for all fields (rate-of-fire divisor = 1) + Int delay = data->m_chargeWeapons[ i ]->getDelayBetweenShots( bonus ); + if( delay < 1 ) + delay = 1; // never stall + + m_nextChargeWeaponFire[ i ] = now + delay; + } +} + +//------------------------------------------------------------------------------------------------- +/** Create this beam's configured particle systems at its impact point. The generic systems keep an + * exact per-beam stride (one entry per template even on failure); the terrain systems are picked + * by the impact point's current terrain. */ +//------------------------------------------------------------------------------------------------- +void OrbitalBeamUpdate::spawnChargeBeamSystems( const Coord3D* groundPos ) +{ + const OrbitalBeamUpdateModuleData *data = getOrbitalBeamUpdateModuleData(); + + // Generic systems (kept at an exact per-beam stride = name count). + createSystemsFromNames( data->m_chargeBeamParticleSystemNames, groundPos, m_chargeBeamSystemIDs ); + + // Terrain-specific systems for this beam's current terrain. + const Bool overWater = TheTerrainLogic->isUnderwater( groundPos->x, groundPos->y ); + m_chargeBeamTerrainSystemIDs.push_back( std::vector() ); + createTerrainSystems( overWater, groundPos, m_chargeBeamTerrainSystemIDs.back() ); + m_chargeBeamOverWater.push_back( overWater ); +} + +//------------------------------------------------------------------------------------------------- +/** Move a beam's particle systems to its current impact point, swapping the terrain-specific set + * when the impact point crosses the shoreline. */ +//------------------------------------------------------------------------------------------------- +void OrbitalBeamUpdate::moveChargeBeamSystems( size_t beamIndex, const Coord3D* groundPos ) +{ + // Generic systems (fixed stride). + const size_t numSystems = getOrbitalBeamUpdateModuleData()->m_chargeBeamParticleSystemNames.size(); + const size_t base = beamIndex * numSystems; + for( size_t k = 0; k < numSystems; ++k ) + { + if( base + k >= m_chargeBeamSystemIDs.size() ) + break; + + ParticleSystem *system = TheParticleSystemManager->findParticleSystem( m_chargeBeamSystemIDs[ base + k ] ); + if( system ) + system->setPosition( groundPos ); + } + + if( beamIndex >= m_chargeBeamTerrainSystemIDs.size() ) + return; + + // Swap the terrain-specific set on a shoreline crossing. + const Bool overWater = TheTerrainLogic->isUnderwater( groundPos->x, groundPos->y ); + std::vector& terrainSystems = m_chargeBeamTerrainSystemIDs[ beamIndex ]; + if( overWater != m_chargeBeamOverWater[ beamIndex ] ) + { + destroyParticleSystemList( terrainSystems ); + createTerrainSystems( overWater, groundPos, terrainSystems ); + m_chargeBeamOverWater[ beamIndex ] = overWater; + } + + // Move the terrain systems. + for( std::vector::iterator it = terrainSystems.begin(); it != terrainSystems.end(); ++it ) + { + ParticleSystem *system = TheParticleSystemManager->findParticleSystem( *it ); + if( system ) + system->setPosition( groundPos ); + } +} + +//------------------------------------------------------------------------------------------------- +/** Destroy all charge beam particle systems (generic + terrain). */ +//------------------------------------------------------------------------------------------------- +void OrbitalBeamUpdate::destroyChargeBeamSystems() +{ + destroyParticleSystemList( m_chargeBeamSystemIDs ); + + for( std::vector< std::vector >::iterator it = m_chargeBeamTerrainSystemIDs.begin(); + it != m_chargeBeamTerrainSystemIDs.end(); ++it ) + { + destroyParticleSystemList( *it ); + } + m_chargeBeamTerrainSystemIDs.clear(); + m_chargeBeamOverWater.clear(); +} + +//------------------------------------------------------------------------------------------------- +//------------------------------------------------------------------------------------------------- +UpdateSleepTime OrbitalBeamUpdate::update( void ) +{ + // Orbital beam update has multiple stages + // 0: initial delay, waiting + // 1: spawning charge beams. Charge beams are spawned in a circular radius around the center (Object pos) + // starting at 0 degree, each beam is offset by the same angle + // 2: Animation phase, beams move inward and rotate while doing so (rotaion is future TODO, not yet to implement) + // 3: Final phase, charge beams dissappear, final beam is spawned + // 4: object dies + + const OrbitalBeamUpdateModuleData *data = getOrbitalBeamUpdateModuleData(); + const UnsignedInt now = TheGameLogic->getFrame(); + + // Seed the timing on the very first update. + if( !m_started ) + { + m_started = TRUE; + m_stageStartFrame = now; + + // InitialSound plays through the initial delay and charge beam spawning. + startPhaseSound( m_initialSoundEvent, data->m_initialSound ); + } + + // Destroy any beams whose fade-out has finished, regardless of stage. + updateDyingBeams( now ); + + // The center of the effect is the position of the object we are attached to. + Coord3D center = *getObject()->getPosition(); + center.z = surfaceZAt( center.x, center.y ); + + switch( m_stage ) + { + //--------------------------------------------------------------------------------------------- + case STAGE_INITIAL_DELAY: + { + if( now - m_stageStartFrame >= data->m_initialDelayFrames ) + { + m_stage = STAGE_SPAWN_CHARGE; + m_stageStartFrame = now; + m_nextChargeSpawnFrame = now; + + // Build the ring-slot order for spawning. Positions are identical either way; + // only the order in which slots are filled changes. + m_spawnOrder.resize( data->m_numChargeBeams ); + for( UnsignedShort i = 0; i < data->m_numChargeBeams; ++i ) + m_spawnOrder[ i ] = i; + + if( data->m_randomizeChargeBeamOrder ) + { + // Fisher-Yates shuffle using the synced logic RNG (deterministic across clients). + for( Int i = (Int)data->m_numChargeBeams - 1; i > 0; --i ) + { + Int j = GameLogicRandomValue( 0, i ); + std::swap( m_spawnOrder[ i ], m_spawnOrder[ j ] ); + } + } + } + break; + } + + //--------------------------------------------------------------------------------------------- + case STAGE_SPAWN_CHARGE: + { + // Spawn charge beams one at a time, paced by m_delayBetweenChargeBeamsFrames. + while( m_chargeBeamsSpawned < data->m_numChargeBeams && now >= m_nextChargeSpawnFrame ) + { + const UnsignedShort slot = m_spawnOrder[ m_chargeBeamsSpawned ]; + const Real angle = (Real)slot * TWO_PI / (Real)data->m_numChargeBeams; + + Coord3D groundPos; + groundPos.x = center.x + data->m_chargeBeamRadius * cos( angle ); + groundPos.y = center.y + data->m_chargeBeamRadius * sin( angle ); + groundPos.z = surfaceZAt( groundPos.x, groundPos.y ); + + Coord3D sky = beamSkyPos( &groundPos, ¢er ); + m_chargeBeamGroundPos.push_back( groundPos ); + m_chargeBeamIDs.push_back( spawnBeam( data->m_chargeBeamLaserNameName, &sky, &groundPos ) ); + spawnChargeBeamSystems( &groundPos ); + FXList::doFXPos( data->m_chargeBeamStartFX, &groundPos ); + + ++m_chargeBeamsSpawned; + m_nextChargeSpawnFrame += data->m_delayBetweenChargeBeamsFrames; + + // Count the charge->anim hold from when the LAST beam spawned, not from stage entry. + if( m_chargeBeamsSpawned >= data->m_numChargeBeams ) + m_stageStartFrame = now; + } + + // Once all are spawned, wait the charge->anim delay before animating. + if( m_chargeBeamsSpawned >= data->m_numChargeBeams && + now - m_stageStartFrame >= data->m_delayChargeToAnimFrames ) + { + m_stage = STAGE_ANIMATE; + m_stageStartFrame = now; + + // InitialSound ends, ChargeSound plays during the animation. + stopPhaseSound( m_initialSoundEvent ); + startPhaseSound( m_chargeSoundEvent, data->m_chargeSound ); + + // Arm the charge weapons so they all fire on the first animate frame. + m_nextChargeWeaponFire.assign( data->m_chargeWeapons.size(), now ); + } + break; + } + + //--------------------------------------------------------------------------------------------- + case STAGE_ANIMATE: + { + // Fire the charge weapons at the center throughout the animation stage. + updateChargeWeapons( ¢er, now ); + + // Translate each charge beam's ground endpoint from its ring position toward the center. + Real factor = 1.0f; + if( data->m_beamAnimationDurationFrames > 0 ) + { + factor = (Real)(now - m_stageStartFrame) / (Real)data->m_beamAnimationDurationFrames; + if( factor > 1.0f ) + factor = 1.0f; + } + + if( !m_chargeFadeStarted ) + { + // Eased progress used for the beam positions (timing/completion stay on raw factor). + const Real t = interpolate( factor ); + + // Still animating: translate each charge beam's ground endpoint inward. + for( size_t i = 0; i < m_chargeBeamIDs.size(); ++i ) + { + const Coord3D& start = m_chargeBeamGroundPos[ i ]; + Coord3D groundPos; + if( data->m_chargeBeamRotation != 0.0f ) + { + // Spiral inward: radius shrinks to 0 while the angle sweeps the total rotation. + const Real dx = start.x - center.x; + const Real dy = start.y - center.y; + const Real baseAngle = atan2( dy, dx ); + const Real baseRadius = sqrt( dx * dx + dy * dy ); + const Real curRadius = baseRadius * (1.0f - t); + const Real curAngle = baseAngle + t * data->m_chargeBeamRotation; + groundPos.x = center.x + curRadius * cos( curAngle ); + groundPos.y = center.y + curRadius * sin( curAngle ); + } + else + { + // Straight-in translation. + groundPos.x = start.x + (center.x - start.x) * t; + groundPos.y = start.y + (center.y - start.y) * t; + } + groundPos.z = surfaceZAt( groundPos.x, groundPos.y ); + + // Keep this beam's particle systems on the moving impact point. + moveChargeBeamSystems( i, &groundPos ); + + Drawable *beam = TheGameClient->findDrawableByID( m_chargeBeamIDs[ i ] ); + if( beam == nullptr ) + continue; + + static const NameKeyType nameKeyClientUpdate = NAMEKEY( "LaserUpdate" ); + LaserUpdate *update = (LaserUpdate*)beam->findClientUpdateModule( nameKeyClientUpdate ); + if( update == nullptr ) + continue; + + Coord3D skyPos = beamSkyPos( &groundPos, ¢er ); + + // Move the beam without re-initializing it, so fade-in/widen/fade-out keep running. + // Center-start beams keep a fixed apex; vertical beams track the moving impact. + update->updateContinuousLaser( nullptr, nullptr, &skyPos, &groundPos ); + } + + // Animation just finished: begin fading the charge beams now, so they fade out + // during the anim->final wait rather than at the transition. The impact particle + // systems die together with the start of the fade. + if( factor >= 1.0f ) + { + destroyChargeBeamSystems(); + + for( std::vector::iterator it = m_chargeBeamIDs.begin(); it != m_chargeBeamIDs.end(); ++it ) + { + beginBeamFadeOut( *it ); + } + m_chargeBeamIDs.clear(); + m_chargeBeamGroundPos.clear(); + m_chargeFadeStarted = TRUE; + + // ChargeSound ends when the animation completes. + stopPhaseSound( m_chargeSoundEvent ); + } + } + + // After the anim->final delay, spawn the final beam and advance. + if( m_chargeFadeStarted && + now - m_stageStartFrame >= data->m_beamAnimationDurationFrames + data->m_delayAnimationToFinalFrames ) + { + m_stage = STAGE_FINAL; + m_stageStartFrame = now; + + Coord3D finalSky = beamSkyPos( ¢er, ¢er ); + m_finalBeamID = spawnBeam( data->m_finalBeamLaserNameName, &finalSky, ¢er ); + + // One-shot FX at the center as the final beam appears. + FXList::doFXPos( data->m_finalFX, ¢er ); + + // Fire the final weapon at the center as the final beam appears; schedules repeats. + fireFinalWeapon( ¢er ); + } + break; + } + + //--------------------------------------------------------------------------------------------- + case STAGE_FINAL: + { + // Keep firing the final weapon at its own fire rate while the final beam is alive. + if( data->m_finalWeapon && now >= m_nextFinalFireFrame ) + fireFinalWeapon( ¢er ); + + if( now - m_stageStartFrame >= data->m_finalBeamDurationFrames ) + { + // Fade the final beam out (or kill it now), then move to teardown. + beginBeamFadeOut( m_finalBeamID ); + m_stage = STAGE_DONE; + m_stageStartFrame = now; + } + break; + } + + //--------------------------------------------------------------------------------------------- + case STAGE_DONE: + { + // Wait for any fading beams to finish before we destroy ourselves, otherwise the + // beams (independent drawables) would be orphaned mid-fade. + if( m_dyingBeamIDs.empty() ) + { + TheGameLogic->destroyObject( getObject() ); + return UPDATE_SLEEP_FOREVER; + } + break; + } + } + + return UPDATE_SLEEP_NONE; +} + +// ------------------------------------------------------------------------------------------------ +/** CRC */ +// ------------------------------------------------------------------------------------------------ +void OrbitalBeamUpdate::crc( Xfer *xfer ) +{ + + // extend base class + UpdateModule::crc( xfer ); + +} + +// ------------------------------------------------------------------------------------------------ +/** Xfer method + * Version Info: + * 1: Initial version */ +// ------------------------------------------------------------------------------------------------ +void OrbitalBeamUpdate::xfer( Xfer *xfer ) +{ + + // version + XferVersion currentVersion = 1; + XferVersion version = currentVersion; + xfer->xferVersion( &version, currentVersion ); + + // extend base class + UpdateModule::xfer( xfer ); + + // stage state + xfer->xferUnsignedInt( &m_stage ); + xfer->xferUnsignedInt( &m_stageStartFrame ); + xfer->xferUnsignedInt( &m_nextChargeSpawnFrame ); + xfer->xferUnsignedShort( &m_chargeBeamsSpawned ); + xfer->xferBool( &m_started ); + xfer->xferBool( &m_chargeFadeStarted ); + xfer->xferUnsignedInt( &m_nextFinalFireFrame ); + xfer->xferDrawableID( &m_finalBeamID ); + + // charge beam drawables + UnsignedShort beamCount = (UnsignedShort)m_chargeBeamIDs.size(); + xfer->xferUnsignedShort( &beamCount ); + if( xfer->getXferMode() == XFER_LOAD ) + { + m_chargeBeamIDs.resize( beamCount, INVALID_DRAWABLE_ID ); + m_chargeBeamGroundPos.resize( beamCount ); + } + for( UnsignedShort i = 0; i < beamCount; ++i ) + { + xfer->xferDrawableID( &m_chargeBeamIDs[ i ] ); + xfer->xferCoord3D( &m_chargeBeamGroundPos[ i ] ); + } + + // per charge weapon next-fire schedule + UnsignedShort chargeWeaponCount = (UnsignedShort)m_nextChargeWeaponFire.size(); + xfer->xferUnsignedShort( &chargeWeaponCount ); + if( xfer->getXferMode() == XFER_LOAD ) + m_nextChargeWeaponFire.resize( chargeWeaponCount, 0 ); + for( UnsignedShort i = 0; i < chargeWeaponCount; ++i ) + xfer->xferUnsignedInt( &m_nextChargeWeaponFire[ i ] ); + + // spawn order (must be xfered so randomized order stays in sync) + UnsignedShort orderCount = (UnsignedShort)m_spawnOrder.size(); + xfer->xferUnsignedShort( &orderCount ); + if( xfer->getXferMode() == XFER_LOAD ) + m_spawnOrder.resize( orderCount ); + for( UnsignedShort i = 0; i < orderCount; ++i ) + xfer->xferUnsignedShort( &m_spawnOrder[ i ] ); + + // beams currently fading out, awaiting destruction + UnsignedShort dyingCount = (UnsignedShort)m_dyingBeamIDs.size(); + xfer->xferUnsignedShort( &dyingCount ); + if( xfer->getXferMode() == XFER_LOAD ) + { + m_dyingBeamIDs.resize( dyingCount, INVALID_DRAWABLE_ID ); + m_dyingBeamDeathFrame.resize( dyingCount ); + } + for( UnsignedShort i = 0; i < dyingCount; ++i ) + { + xfer->xferDrawableID( &m_dyingBeamIDs[ i ] ); + xfer->xferUnsignedInt( &m_dyingBeamDeathFrame[ i ] ); + } + + // charge beam generic particle systems (flat, N per beam) + UnsignedShort systemCount = (UnsignedShort)m_chargeBeamSystemIDs.size(); + xfer->xferUnsignedShort( &systemCount ); + if( xfer->getXferMode() == XFER_LOAD ) + m_chargeBeamSystemIDs.resize( systemCount, INVALID_PARTICLE_SYSTEM_ID ); + for( UnsignedShort i = 0; i < systemCount; ++i ) + xfer->xferUser( &m_chargeBeamSystemIDs[ i ], sizeof( ParticleSystemID ) ); + + // charge beam terrain-specific particle systems (per beam, variable size) + terrain state + UnsignedShort terrainBeamCount = (UnsignedShort)m_chargeBeamTerrainSystemIDs.size(); + xfer->xferUnsignedShort( &terrainBeamCount ); + if( xfer->getXferMode() == XFER_LOAD ) + { + m_chargeBeamTerrainSystemIDs.resize( terrainBeamCount ); + m_chargeBeamOverWater.resize( terrainBeamCount, FALSE ); + } + for( UnsignedShort i = 0; i < terrainBeamCount; ++i ) + { + // std::vector is a proxy specialization, so xfer through a temp. + Bool overWater = m_chargeBeamOverWater[ i ]; + xfer->xferBool( &overWater ); + m_chargeBeamOverWater[ i ] = overWater; + + UnsignedShort innerCount = (UnsignedShort)m_chargeBeamTerrainSystemIDs[ i ].size(); + xfer->xferUnsignedShort( &innerCount ); + if( xfer->getXferMode() == XFER_LOAD ) + m_chargeBeamTerrainSystemIDs[ i ].resize( innerCount, INVALID_PARTICLE_SYSTEM_ID ); + for( UnsignedShort k = 0; k < innerCount; ++k ) + xfer->xferUser( &m_chargeBeamTerrainSystemIDs[ i ][ k ], sizeof( ParticleSystemID ) ); + } + +} + +// ------------------------------------------------------------------------------------------------ +/** Load post process */ +// ------------------------------------------------------------------------------------------------ +void OrbitalBeamUpdate::loadPostProcess( void ) +{ + + // extend base class + UpdateModule::loadPostProcess(); + +}