diff --git a/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl b/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl
index 95f714cfcc5..94187718a63 100644
--- a/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl
+++ b/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl
@@ -160,6 +160,7 @@ static PoolSizeRec PoolSizes[] =
{ "DelayedUpgradeBehavior", 128, 64 },
{ "GenerateMinefieldBehavior", 32, 32 },
{ "HelicopterSlowDeathBehavior", 64, 32 },
+ { "ShipSlowDeathBehavior", 64, 32 },
{ "ParkingPlaceBehavior", 32, 32 },
{ "FlightDeckBehavior", 8, 8 },
#ifdef ALLOW_SURRENDER
diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt
index b3ee009a069..80b193315a2 100644
--- a/GeneralsMD/Code/GameEngine/CMakeLists.txt
+++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt
@@ -350,6 +350,7 @@ set(GAMEENGINE_SRC
Include/GameLogic/Module/HealCrateCollide.h
Include/GameLogic/Module/HeightDieUpdate.h
Include/GameLogic/Module/HelicopterSlowDeathUpdate.h
+ Include/GameLogic/Module/ShipSlowDeathBehavior.h
Include/GameLogic/Module/HelixContain.h
Include/GameLogic/Module/MultiAddOnContain.h
Include/GameLogic/Module/HighlanderBody.h
@@ -1064,6 +1065,7 @@ set(GAMEENGINE_SRC
Source/GameLogic/Object/Update/FloatUpdate.cpp
Source/GameLogic/Object/Update/HeightDieUpdate.cpp
Source/GameLogic/Object/Update/HelicopterSlowDeathUpdate.cpp
+ Source/GameLogic/Object/Update/ShipSlowDeathBehavior.cpp
Source/GameLogic/Object/Update/HijackerUpdate.cpp
Source/GameLogic/Object/Update/HordeUpdate.cpp
Source/GameLogic/Object/Update/LaserUpdate.cpp
diff --git a/GeneralsMD/Code/GameEngine/Include/Common/Thing.h b/GeneralsMD/Code/GameEngine/Include/Common/Thing.h
index c180ccd0372..68b318f6fda 100644
--- a/GeneralsMD/Code/GameEngine/Include/Common/Thing.h
+++ b/GeneralsMD/Code/GameEngine/Include/Common/Thing.h
@@ -128,7 +128,9 @@ class Thing : public MemoryPoolObject
Real getHeightAboveTerrain() const;
Real getHeightAboveTerrainOrWater() const;
- Bool isOverWater() const;
+ Bool isOverWater() const; ///< Checks if the object is above or below a water surface
+ Bool isAboveWater() const; ///< Checks if the object is above (but not below) a water surface
+ Bool isBelowWater() const; ///< Checks if the object is below (but not above) a water surface
Bool isAboveTerrain() const { return getHeightAboveTerrain() > 0.0f; }
Bool isAboveTerrainOrWater() const { return getHeightAboveTerrainOrWater() > 0.0f; }
diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/PhysicsUpdate.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/PhysicsUpdate.h
index 73a36e9342d..d3dea8c241b 100644
--- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/PhysicsUpdate.h
+++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/PhysicsUpdate.h
@@ -36,6 +36,8 @@
enum ObjectID CPP_11(: Int);
+class FXList;
+
enum PhysicsTurningType CPP_11(: Int)
{
TURN_NEGATIVE = -1,
@@ -65,6 +67,11 @@ class PhysicsBehaviorModuleData : public UpdateModuleData
Real m_pitchRollYawFactor;
Bool m_vehicleCrashAllowAirborne;
Real m_bounceFactor;
+
+ Bool m_doWaterPhysics; //< We only do water collission checks if this is set!
+ Real m_waterExtraFriction;
+ const FXList* m_waterImpactFX;
+
const WeaponTemplate* m_vehicleCrashesIntoBuildingWeaponTemplate;
const WeaponTemplate* m_vehicleCrashesIntoNonBuildingWeaponTemplate;
@@ -193,7 +200,10 @@ class PhysicsBehavior : public UpdateModule,
void setExtraFriction(Real b) { m_extraFriction = b; }
void setBounceSound(const AudioEventRTS* bounceSound);
+ void setWaterImpactSound(const AudioEventRTS* waterImpactSound);
+ void setWaterImpactFX(const FXList* waterImpactFX);
const AudioEventRTS* getBounceSound() { return m_bounceSound ? &m_bounceSound->m_event : TheAudio->getValidSilentAudioEvent(); }
+ const AudioEventRTS* getWaterImpactSound() { return m_waterImpactSound ? &m_waterImpactSound->m_event : TheAudio->getValidSilentAudioEvent(); }
/**
Reset all values (vel, accel, etc) to starting values.
@@ -238,6 +248,8 @@ class PhysicsBehavior : public UpdateModule,
void testStunnedUnitForDestruction(void);
+ Real getExtraFriction() const;
+
private:
enum PhysicsFlagsType
@@ -255,6 +267,7 @@ class PhysicsBehavior : public UpdateModule,
IS_IN_FREEFALL = 0x0200,
IS_IN_UPDATE = 0x0400,
IS_STUNNED = 0x0800,
+ WAS_ABOVE_WATER_LAST_FRAME = 0x1000,
};
/*
@@ -266,6 +279,7 @@ class PhysicsBehavior : public UpdateModule,
Real m_rollRate; ///< rate of rotation around forward vector
Real m_pitchRate; ///< rate or rotation around side vector
DynamicAudioEventRTS* m_bounceSound; ///< The sound for when this thing bounces, or NULL
+ DynamicAudioEventRTS* m_waterImpactSound; ///< The sound for when this thing hits the water surface, or NULL
Coord3D m_accel; ///< current acceleration
Coord3D m_prevAccel; ///< last frame's acceleration
Coord3D m_vel; ///< current velocity
@@ -284,6 +298,8 @@ class PhysicsBehavior : public UpdateModule,
Bool m_originalAllowBounce; ///< orignal state of allow bounce
+ const FXList* m_waterImpactFX;
+
void setFlag(PhysicsFlagsType f, Bool set) { if (set) m_flags |= f; else m_flags &= ~f; }
Bool getFlag(PhysicsFlagsType f) const { return (m_flags & f) != 0; }
diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ShipSlowDeathBehavior.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ShipSlowDeathBehavior.h
new file mode 100644
index 00000000000..6433a9a5798
--- /dev/null
+++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/ShipSlowDeathBehavior.h
@@ -0,0 +1,179 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2025 Electronic Arts Inc.
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+////////////////////////////////////////////////////////////////////////////////
+// //
+// (c) 2001-2003 Electronic Arts Inc. //
+// //
+////////////////////////////////////////////////////////////////////////////////
+
+// FILE: ShipSlowDeathBehavior.h //////////////////////////////////////////////////////////////
+// Author: Andi W, 01 2026
+// Desc: ship slow deaths
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+#pragma once
+
+// USER INCLUDES //////////////////////////////////////////////////////////////////////////////////
+#include "Common/AudioEventRTS.h"
+#include "GameLogic/Module/SlowDeathBehavior.h"
+
+// FORWARD DECLARATIONS ///////////////////////////////////////////////////////////////////////////
+class ParticleSystemTemplate;
+enum ModelConditionFlagType CPP_11(: Int);
+
+//-------------------------------------------------------------------------------------------------
+//-------------------------------------------------------------------------------------------------
+enum ShipToppleType CPP_11(: Int)
+{
+ TOPPLE_NONE = 0,
+ TOPPLE_FRONT,
+ TOPPLE_BACK,
+ TOPPLE_SIDE_LEFT,
+ TOPPLE_SIDE_RIGHT,
+ TOPPLE_SIDE,
+ TOPPLE_RANDOM,
+
+ TOPPLE_COUNT
+};
+
+#ifdef DEFINE_SHIP_TOPPLE_NAMES
+static const char* const TheShipToppleNames[] =
+{
+ "NONE",
+ "FRONT",
+ "BACK",
+ "LEFT",
+ "RIGHT",
+ "SIDE",
+ "RANDOM",
+ NULL
+};
+static_assert(ARRAY_SIZE(TheShipToppleNames) == TOPPLE_COUNT + 1, "Incorrect array size");
+#endif
+
+//-------------------------------------------------------------------------------------------------
+class ShipSlowDeathBehaviorModuleData : public SlowDeathBehaviorModuleData
+{
+
+public:
+
+ ShipSlowDeathBehaviorModuleData( void );
+
+ static void buildFieldParse(MultiIniFieldParse &p );
+
+ UnsignedInt m_initialDelay;
+ UnsignedInt m_initialDelayVariance;
+ ShipToppleType m_toppleType;
+
+ Real m_wobbleMaxPitch;
+ Real m_wobbleMaxYaw;
+ Real m_wobbleMaxRoll;
+ UnsignedInt m_wobbleInterval;
+
+ Real m_toppleFrontMinPitch;
+ Real m_toppleFrontMaxPitch;
+ Real m_toppleBackMinPitch;
+ Real m_toppleBackMaxPitch;
+ Real m_toppleSideMinRoll;
+ Real m_toppleSideMaxRoll;
+ Real m_toppleHeightMinOffset;
+ Real m_toppleHeightMaxOffset;
+ //Real m_toppleDamping;
+ Real m_toppleAngleCorrectionRate;
+ UnsignedInt m_toppleDuration;
+ UnsignedInt m_toppleDurationVariance;
+
+ Real m_toppleMinPushForce;
+ Real m_toppleMaxPushForce;
+ Real m_toppleMinPushForceSide;
+ Real m_toppleMaxPushForceSide;
+
+ //Real m_sinkWobbleMaxPitch;
+ //Real m_sinkWobbleMaxYaw;
+ //Real m_sinkWobbleMaxRoll;
+ //UnsignedInt m_sinkWobbleInterval;
+
+ Real m_sinkHowFast;
+
+ AudioEventRTS m_deathSound; ///< Sound played during death sequence.
+ const ObjectCreationList* m_oclEjectPilot; ///< OCL for ejecting pilot
+
+ const FXList* m_fxHitGround; ///< fx for hitting the ground
+ const ObjectCreationList* m_oclHitGround; ///< OCL at hit ground event
+
+ const FXList* m_fxStartTopple; ///< fx after init phase
+ const ObjectCreationList* m_oclStartTopple; ///< OCL after init phase
+
+ const FXList* m_fxStartSink; ///< fx after topple / before sinking phase
+ const ObjectCreationList* m_oclStartSink; ///< OCL after topple / before sinking phase
+
+ const ParticleSystemTemplate* m_attachParticleSystem; ///< particle system to attach while sinking
+
+ std::vector m_attachParticleBoneNames;
+
+
+ ModelConditionFlagType m_conditionFlagInit;
+ ModelConditionFlagType m_conditionFlagTopple;
+ ModelConditionFlagType m_conditionFlagSink;
+};
+
+//-------------------------------------------------------------------------------------------------
+//-------------------------------------------------------------------------------------------------
+class ShipSlowDeathBehavior : public SlowDeathBehavior
+{
+
+ MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE( ShipSlowDeathBehavior, "ShipSlowDeathBehavior" )
+ MAKE_STANDARD_MODULE_MACRO_WITH_MODULE_DATA( ShipSlowDeathBehavior, ShipSlowDeathBehaviorModuleData )
+
+public:
+
+ ShipSlowDeathBehavior( Thing *thing, const ModuleData* moduleData );
+ // virtual destructor prototype provided by memory pool declaration
+
+ virtual void beginSlowDeath( const DamageInfo *damageInfo ); ///< begin the slow death cycle
+ virtual UpdateSleepTime update();
+
+protected:
+
+ void doInitPhase();
+ void doTopplePhase();
+ void doSinkPhase();
+
+ void doWobble();
+
+ void beginInitPhase();
+ void beginTopplePhase();
+ void beginSinkPhase();
+
+ // @todo propagate this up to SlowDeathBehavior. I don't wanna do it today, cause its 4/3. jkmcd
+ AudioEventRTS m_deathSound; ///< Sound played during death sequence.
+
+ UnsignedInt m_initStartFrame;
+ UnsignedInt m_toppleStartFrame;
+ UnsignedInt m_sinkStartFrame;
+ Bool m_shipSinkStarted;
+ Int m_chosenToppleType;
+
+ Real m_toppleVarianceRoll;
+
+ Real m_curPitch;
+ Real m_curYaw;
+ Real m_curRoll;
+ Real m_curZ;
+};
diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp
index e2be6907b99..a5f0f724220 100644
--- a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp
@@ -58,6 +58,7 @@
#include "GameLogic/Module/ChronoDeathBehavior.h"
#include "GameLogic/Module/SlowDeathBehavior.h"
#include "GameLogic/Module/HelicopterSlowDeathUpdate.h"
+#include "GameLogic/Module/ShipSlowDeathBehavior.h"
#include "GameLogic/Module/NeutronMissileSlowDeathUpdate.h"
#include "GameLogic/Module/CaveContain.h"
#include "GameLogic/Module/OpenContain.h"
@@ -362,6 +363,7 @@ void ModuleFactory::init( void )
addModule( ChronoDeathBehavior );
addModule( SlowDeathBehavior );
addModule( HelicopterSlowDeathBehavior );
+ addModule( ShipSlowDeathBehavior );
addModule( NeutronMissileSlowDeathBehavior );
addModule( CaveContain );
addModule( OpenContain );
diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Thing/Thing.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Thing/Thing.cpp
index 0490ae3fc1f..7d96a949fd3 100644
--- a/GeneralsMD/Code/GameEngine/Source/Common/Thing/Thing.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/Common/Thing/Thing.cpp
@@ -343,6 +343,26 @@ Bool Thing::isOverWater() const
return m_cachedIsOverWater;
}
+// ------------------------------------------------------------------------------
+Bool Thing::isBelowWater() const
+{
+ if (!(m_cacheFlags & VALID_ALTITUDE_SEALEVEL))
+ {
+ getHeightAboveTerrainOrWater();
+ }
+ return m_cachedIsOverWater && m_cachedAltitudeAboveTerrainOrWater <= 0;
+}
+
+// ------------------------------------------------------------------------------
+Bool Thing::isAboveWater() const
+{
+ if (!(m_cacheFlags & VALID_ALTITUDE_SEALEVEL))
+ {
+ getHeightAboveTerrainOrWater();
+ }
+ return m_cachedIsOverWater && m_cachedAltitudeAboveTerrainOrWater > 0;
+}
+
//=============================================================================
/** If we treat this as airborne, then they slide down slopes. This checks whether
diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/ObjectCreationList.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/ObjectCreationList.cpp
index 28bc686d7b9..6dba65c1edd 100644
--- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/ObjectCreationList.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/ObjectCreationList.cpp
@@ -731,6 +731,7 @@ class GenericObjectCreationNugget : public ObjectCreationNugget
GenericObjectCreationNugget() :
m_requiresLivePlayer(FALSE),
+ m_debrisTemplateName(AsciiString::TheEmptyString),
m_debrisToGenerate(1),
m_mass(0),
m_extraBounciness(0),
@@ -887,6 +888,7 @@ class GenericObjectCreationNugget : public ObjectCreationNugget
{
static const FieldParse myFieldParse[] =
{
+ { "DebrisObject", INI::parseAsciiString, NULL, offsetof(GenericObjectCreationNugget, m_debrisTemplateName) },
{ "ModelNames", parseDebrisObjectNames, NULL, 0 },
{ "Mass", INI::parsePositiveNonZeroReal, NULL, offsetof( GenericObjectCreationNugget, m_mass ) },
{ "AnimationSet", parseAnimSet, NULL, offsetof( GenericObjectCreationNugget, m_animSets) },
@@ -895,6 +897,8 @@ class GenericObjectCreationNugget : public ObjectCreationNugget
{ "MinLODRequired", INI::parseStaticGameLODLevel, NULL, offsetof(GenericObjectCreationNugget, m_minLODRequired) },
{ "Shadow", INI::parseBitString32, TheShadowNames, offsetof( GenericObjectCreationNugget, m_shadowType ) },
{ "BounceSound", INI::parseAudioEventRTS, NULL, offsetof( GenericObjectCreationNugget, m_bounceSound) },
+ { "WaterImpactSound", INI::parseAudioEventRTS, NULL, offsetof( GenericObjectCreationNugget, m_waterImpactSound) },
+ { "WaterImpactFX", INI::parseFXList, NULL, offsetof( GenericObjectCreationNugget, m_waterImpactFX) },
{ 0, 0, 0, 0 }
};
@@ -1148,6 +1152,8 @@ class GenericObjectCreationNugget : public ObjectCreationNugget
objUp->setExtraFriction(m_extraFriction);
objUp->setAllowBouncing(true);
objUp->setBounceSound(&m_bounceSound);
+ objUp->setWaterImpactSound(&m_waterImpactSound);
+ objUp->setWaterImpactFX(m_waterImpactFX);
DUMPREAL(m_extraBounciness);
DUMPREAL(m_extraFriction);
@@ -1316,7 +1322,11 @@ class GenericObjectCreationNugget : public ObjectCreationNugget
Object* reallyCreate(const Coord3D *pos, const Matrix3D *mtx, Real orientation, const Object *sourceObj, UnsignedInt lifetimeFrames ) const
{
- static const ThingTemplate* debrisTemplate = TheThingFactory->findTemplate("GenericDebris");
+ const ThingTemplate* debrisTemplate;
+ if (!m_debrisTemplateName.isEmpty())
+ debrisTemplate = TheThingFactory->findTemplate(m_debrisTemplateName);
+ else
+ debrisTemplate = TheThingFactory->findTemplate("GenericDebris");
if (m_names.empty())
return NULL;
@@ -1363,6 +1373,7 @@ class GenericObjectCreationNugget : public ObjectCreationNugget
continue;
tmpl = debrisTemplate;
+ DEBUG_LOG((">>> CREATE DEBRIS TEMPLATE: %s", debrisTemplate->getName().str()));
}
DEBUG_ASSERTCRASH(tmpl, ("Object %s not found",m_names[pick].str()));
if (!tmpl)
@@ -1458,6 +1469,7 @@ class GenericObjectCreationNugget : public ObjectCreationNugget
AsciiString m_animFinal;
};
std::vector m_names;
+ AsciiString m_debrisTemplateName;
AsciiString m_putInContainer;
std::vector m_animSets;
const FXList* m_fxFinal;
@@ -1488,6 +1500,8 @@ class GenericObjectCreationNugget : public ObjectCreationNugget
Real m_maxDistanceFormation;
Int m_objectCount; // how many objects will there be?
AudioEventRTS m_bounceSound;
+ AudioEventRTS m_waterImpactSound;
+ const FXList* m_waterImpactFX;
Bool m_requiresLivePlayer;
Bool m_experienceSink;
Bool m_inheritsWeaponBonus;
diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PhysicsUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PhysicsUpdate.cpp
index c5f836a7737..c9877b56159 100644
--- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PhysicsUpdate.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/PhysicsUpdate.cpp
@@ -45,6 +45,7 @@
#include "GameLogic/TerrainLogic.h"
#include "GameLogic/Weapon.h"
#include "GameLogic/LogicRandomValue.h"
+#include "GameClient/FXList.h"
const Real DEFAULT_MASS = 1.0f;
@@ -137,6 +138,8 @@ PhysicsBehaviorModuleData::PhysicsBehaviorModuleData()
m_vehicleCrashesIntoNonBuildingWeaponTemplate = TheWeaponStore->findWeaponTemplate("VehicleCrashesIntoNonBuildingWeapon");
m_vehicleCrashAllowAirborne = FALSE;
m_bounceFactor = 1.0f;
+
+ m_waterImpactFX = NULL;
}
//-------------------------------------------------------------------------------------------------
@@ -189,6 +192,10 @@ static void parseFrictionPerSec( INI* ini, void * /*instance*/, void *store, con
{ "VehicleCrashesIntoBuildingWeaponTemplate", INI::parseWeaponTemplate, NULL, offsetof(PhysicsBehaviorModuleData, m_vehicleCrashesIntoBuildingWeaponTemplate) },
{ "VehicleCrashesIntoNonBuildingWeaponTemplate", INI::parseWeaponTemplate, NULL, offsetof(PhysicsBehaviorModuleData, m_vehicleCrashesIntoNonBuildingWeaponTemplate) },
{ "VehicleCrashWeaponAllowAirborne", INI::parseBool, NULL, offsetof(PhysicsBehaviorModuleData, m_vehicleCrashAllowAirborne) },
+
+ { "DoWaterPhysics", INI::parseBool, NULL, offsetof(PhysicsBehaviorModuleData, m_doWaterPhysics) },
+ { "WaterExtraFriction", INI::parseReal, NULL, offsetof(PhysicsBehaviorModuleData, m_waterExtraFriction) },
+ { "WaterImpactFX", INI::parseFXList, NULL, offsetof(PhysicsBehaviorModuleData, m_waterImpactFX) },
{ 0, 0, 0, 0 }
};
p.add(dataFieldParse);
@@ -228,6 +235,9 @@ PhysicsBehavior::PhysicsBehavior( Thing *thing, const ModuleData* moduleData ) :
m_pui = NULL;
m_bounceSound = NULL;
+ m_waterImpactSound = NULL;
+
+ m_waterImpactFX = getPhysicsBehaviorModuleData()->m_waterImpactFX;
#ifdef SLEEPY_PHYSICS
setWakeFrame(getObject(), UPDATE_SLEEP_NONE);
@@ -275,9 +285,18 @@ Bool PhysicsBehavior::isIgnoringCollisionsWith(ObjectID id) const
}
//-------------------------------------------------------------------------------------------------
+Real PhysicsBehavior::getExtraFriction() const
+{
+ if (getPhysicsBehaviorModuleData()->m_doWaterPhysics && getObject()->isBelowWater())
+ return m_extraFriction + getPhysicsBehaviorModuleData()->m_waterExtraFriction;
+
+ return m_extraFriction;
+}
+//-------------------------------------------------------------------------------------------------
+
Real PhysicsBehavior::getAerodynamicFriction() const
{
- Real f = getPhysicsBehaviorModuleData()->m_aerodynamicFriction + m_extraFriction;
+ Real f = getPhysicsBehaviorModuleData()->m_aerodynamicFriction + getExtraFriction();
if (f < MIN_AERO_FRICTION) f = MIN_AERO_FRICTION;
if (f > MAX_FRICTION) f = MAX_FRICTION;
return f;
@@ -286,7 +305,7 @@ Real PhysicsBehavior::getAerodynamicFriction() const
//-------------------------------------------------------------------------------------------------
Real PhysicsBehavior::getForwardFriction() const
{
- Real f = getPhysicsBehaviorModuleData()->m_forwardFriction + m_extraFriction;
+ Real f = getPhysicsBehaviorModuleData()->m_forwardFriction + getExtraFriction();
if (f < MIN_NON_AERO_FRICTION) f = MIN_NON_AERO_FRICTION;
if (f > MAX_FRICTION) f = MAX_FRICTION;
return f;
@@ -295,7 +314,7 @@ Real PhysicsBehavior::getForwardFriction() const
//-------------------------------------------------------------------------------------------------
Real PhysicsBehavior::getLateralFriction() const
{
- Real f = getPhysicsBehaviorModuleData()->m_lateralFriction + m_extraFriction;
+ Real f = getPhysicsBehaviorModuleData()->m_lateralFriction + getExtraFriction();
if (f < MIN_NON_AERO_FRICTION) f = MIN_NON_AERO_FRICTION;
if (f > MAX_FRICTION) f = MAX_FRICTION;
return f;
@@ -304,7 +323,7 @@ Real PhysicsBehavior::getLateralFriction() const
//-------------------------------------------------------------------------------------------------
Real PhysicsBehavior::getZFriction() const
{
- Real f = getPhysicsBehaviorModuleData()->m_ZFriction + m_extraFriction;
+ Real f = getPhysicsBehaviorModuleData()->m_ZFriction + getExtraFriction();
if (f < MIN_NON_AERO_FRICTION) f = MIN_NON_AERO_FRICTION;
if (f > MAX_FRICTION) f = MAX_FRICTION;
return f;
@@ -616,6 +635,29 @@ void PhysicsBehavior::setBounceSound(const AudioEventRTS* bounceSound)
}
}
+//-------------------------------------------------------------------------------------------------
+void PhysicsBehavior::setWaterImpactSound(const AudioEventRTS* waterImpactSound)
+{
+ if (waterImpactSound)
+ {
+ if (m_waterImpactSound == NULL)
+ m_waterImpactSound = newInstance(DynamicAudioEventRTS);
+
+ m_waterImpactSound->m_event = *waterImpactSound;
+ }
+ else
+ {
+ deleteInstance(m_waterImpactSound);
+ m_waterImpactSound = NULL;
+ }
+}
+
+//-------------------------------------------------------------------------------------------------
+void PhysicsBehavior::setWaterImpactFX(const FXList* waterImpactFX)
+{
+ m_waterImpactFX = waterImpactFX;
+}
+
//-------------------------------------------------------------------------------------------------
/**
* Basic rigid body physics using an Euler integrator.
@@ -836,18 +878,21 @@ UpdateSleepTime PhysicsBehavior::update()
applyForce(&bounceForce);
}
- Bool isWaterCollision{ false };
+ // Check our height state:
+
+ Bool isUnderWater{ false };
Bool airborneAtEnd{ false };
if (m_pui != nullptr && m_pui->projectileShouldCollideWithWater()) {
+ // For projectiles that collide with water, we consider water as not airborne
airborneAtEnd = obj->isAboveTerrainOrWater();
-
- if (!airborneAtEnd) {
- const Coord3D* pos = obj->getPosition();
- isWaterCollision = TheTerrainLogic->isUnderwater(pos->x, pos->y);
- }
+ isUnderWater = obj->isBelowWater();
}
else {
airborneAtEnd = obj->isAboveTerrain();
+ if (d->m_doWaterPhysics) {
+ isUnderWater = obj->isBelowWater();
+ DEBUG_LOG((">>> WATER CHECK: %d", isUnderWater));
+ }
}
// it's not good enough to check for airborne being different between
@@ -927,8 +972,22 @@ UpdateSleepTime PhysicsBehavior::update()
}
}
+ if (d->m_doWaterPhysics && getFlag(WAS_ABOVE_WATER_LAST_FRAME) && isUnderWater)
+ {
+ DEBUG_LOG((">>> WATER IMPACT NOW!"));
+ // do water splash sound
+ if (m_waterImpactSound) {
+ AudioEventRTS collisionSound = m_waterImpactSound->m_event;
+ collisionSound.setObjectID(getObject()->getID());
+ TheAudio->addAudioEvent(&collisionSound);
+ }
+
+ FXList::doFXObj(m_waterImpactFX, getObject());
+ }
+
setFlag(UPDATE_EVER_RUN, true);
setFlag(WAS_AIRBORNE_LAST_FRAME, airborneAtEnd);
+ setFlag(WAS_ABOVE_WATER_LAST_FRAME, !isUnderWater);
setFlag(IS_IN_UPDATE, false);
return calcSleepTime();
diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ShipSlowDeathBehavior.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ShipSlowDeathBehavior.cpp
new file mode 100644
index 00000000000..54032160d38
--- /dev/null
+++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/ShipSlowDeathBehavior.cpp
@@ -0,0 +1,703 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2025 Electronic Arts Inc.
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+////////////////////////////////////////////////////////////////////////////////
+// //
+// (c) 2001-2003 Electronic Arts Inc. //
+// //
+////////////////////////////////////////////////////////////////////////////////
+
+// FILE: ShipSlowDeathBehavior.cpp ////////////////////////////////////////////////////////////
+// Author: Andi W, 01 2026
+// Desc: ship slow deaths
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+// USER INCLUDES //////////////////////////////////////////////////////////////////////////////////
+#include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine
+#define DEFINE_SHIP_TOPPLE_NAMES
+#include "Common/GameAudio.h"
+#include "Common/GlobalData.h"
+#include "Common/RandomValue.h"
+#include "Common/ThingFactory.h"
+#include "Common/ThingTemplate.h"
+#include "Common/ModelState.h"
+#include "Common/Xfer.h"
+#include "GameClient/Drawable.h"
+#include "GameClient/FXList.h"
+#include "GameClient/InGameUI.h"
+#include "GameClient/ParticleSys.h"
+#include "GameLogic/GameLogic.h"
+#include "GameLogic/Locomotor.h"
+#include "GameLogic/Object.h"
+#include "GameLogic/ObjectCreationList.h"
+#include "GameLogic/Module/AIUpdate.h"
+#include "GameLogic/Module/BodyModule.h"
+#include "GameLogic/Module/EjectPilotDie.h"
+#include "GameLogic/Module/ShipSlowDeathBehavior.h"
+#include "GameLogic/Module/PhysicsUpdate.h"
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Helicopter slow death update module data ///////////////////////////////////////////////////////
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+
+
+
+//-------------------------------------------------------------------------------------------------
+//-------------------------------------------------------------------------------------------------
+ShipSlowDeathBehaviorModuleData::ShipSlowDeathBehaviorModuleData( void )
+{
+ m_attachParticleBoneNames.clear();
+ m_attachParticleSystem = NULL;
+ m_oclEjectPilot = NULL;
+ m_fxHitGround = NULL;
+ m_oclHitGround = NULL;
+ m_fxStartTopple = NULL;
+ m_oclStartTopple = NULL;
+ m_fxStartSink = NULL;
+ m_oclStartSink = NULL;
+ //m_toppleDamping = 1.0;
+
+ m_conditionFlagInit = MODELCONDITION_RUBBLE;
+ m_conditionFlagTopple = MODELCONDITION_RUBBLE;
+ m_conditionFlagSink = MODELCONDITION_RUBBLE;
+}
+
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+/*static*/ void ShipSlowDeathBehaviorModuleData::buildFieldParse( MultiIniFieldParse &p )
+{
+ SlowDeathBehaviorModuleData::buildFieldParse( p );
+
+ static const FieldParse dataFieldParse[] =
+ {
+ { "InitialDelay", INI::parseDurationUnsignedInt, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_initialDelay) },
+ { "InitialDelayVariance", INI::parseDurationUnsignedInt, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_initialDelayVariance) },
+ { "InitialConditionFlag", ModelConditionFlags::parseSingleBitFromINI, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_conditionFlagInit) },
+
+ { "WobbleMaxAnglePitch", INI::parseAngleReal, NULL, offsetof( ShipSlowDeathBehaviorModuleData, m_wobbleMaxPitch) },
+ { "WobbleMaxAngleYaw", INI::parseAngleReal, NULL, offsetof( ShipSlowDeathBehaviorModuleData, m_wobbleMaxYaw) },
+ { "WobbleMaxAngleRoll", INI::parseAngleReal, NULL, offsetof( ShipSlowDeathBehaviorModuleData, m_wobbleMaxRoll) },
+ { "WobbleInterval", INI::parseDurationUnsignedInt, NULL, offsetof( ShipSlowDeathBehaviorModuleData, m_wobbleInterval) },
+
+ { "ToppleStyle", INI::parseIndexList, TheShipToppleNames, offsetof(ShipSlowDeathBehaviorModuleData, m_toppleType) },
+ { "ToppleFrontMinPitch", INI::parseAngleReal, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_toppleFrontMinPitch) },
+ { "ToppleFrontMaxPitch", INI::parseAngleReal, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_toppleFrontMaxPitch) },
+ { "ToppleBackMinPitch", INI::parseAngleReal, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_toppleBackMinPitch) },
+ { "ToppleBackMaxPitch", INI::parseAngleReal, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_toppleBackMaxPitch) },
+ { "ToppleSideMinRoll", INI::parseAngleReal, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_toppleSideMinRoll) },
+ { "ToppleSideMaxRoll", INI::parseAngleReal, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_toppleSideMaxRoll) },
+ { "ToppleMinHeightOffset", INI::parseReal, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_toppleHeightMinOffset) },
+ { "ToppleMaxHeightOffset", INI::parseReal, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_toppleHeightMaxOffset) },
+ { "ToppleDuration", INI::parseDurationUnsignedInt, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_toppleDuration) },
+ { "ToppleDurationVariance", INI::parseDurationUnsignedInt, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_toppleDurationVariance) },
+
+ { "ToppleMinPushForce", INI::parseReal, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_toppleMinPushForce) },
+ { "ToppleMaxPushForce", INI::parseReal, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_toppleMaxPushForce) },
+ { "ToppleMinPushForceSide", INI::parseReal, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_toppleMinPushForceSide) },
+ { "ToppleMaxPushForceSide", INI::parseReal, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_toppleMaxPushForceSide) },
+ { "ToppleConditionFlag", ModelConditionFlags::parseSingleBitFromINI, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_conditionFlagTopple) },
+
+
+ { "FXStartTopple", INI::parseFXList, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_fxStartTopple) },
+ { "OCLStartTopple", INI::parseObjectCreationList, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_oclStartTopple) },
+
+ //{ "ToppleDampingDuration", INI::parseDurationUnsignedInt, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_toppleDampingDuration) },
+ //{ "ToppleDamping", INI::parseReal, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_toppleDamping) },
+ { "ToppleAngleCorrectionRate", INI::parseAngularVelocityReal, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_toppleAngleCorrectionRate) },
+
+ //{ "SinkWobbleMaxAnglePitch", INI::parseAngleReal, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_sinkWobbleMaxPitch) },
+ //{ "SinkWobbleMaxAngleYaw", INI::parseAngleReal, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_sinkWobbleMaxYaw) },
+ //{ "SinkWobbleMaxAngleRoll", INI::parseAngleReal, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_sinkWobbleMaxRoll) },
+ //{ "SinkWobbleInterval", INI::parseDurationUnsignedInt, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_sinkWobbleInterval) },
+ { "SinkHowFast", INI::parsePercentToReal, NULL, offsetof( ShipSlowDeathBehaviorModuleData, m_sinkHowFast) },
+ { "FXStartSink", INI::parseFXList, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_fxStartSink) },
+ { "OCLStartSink", INI::parseObjectCreationList, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_oclStartSink) },
+ { "SinkConditionFlag", ModelConditionFlags::parseSingleBitFromINI, NULL, offsetof(ShipSlowDeathBehaviorModuleData, m_conditionFlagSink) },
+
+ { "SinkAttachParticle", INI::parseParticleSystemTemplate, NULL, offsetof( ShipSlowDeathBehaviorModuleData, m_attachParticleSystem ) },
+ { "SinkAttachParticleBones", INI::parseAsciiStringVector, NULL, offsetof( ShipSlowDeathBehaviorModuleData, m_attachParticleBoneNames ) },
+
+ { "OCLEjectPilot", INI::parseObjectCreationList, NULL, offsetof( ShipSlowDeathBehaviorModuleData, m_oclEjectPilot ) },
+
+ { "FXHitGround", INI::parseFXList, NULL, offsetof( ShipSlowDeathBehaviorModuleData, m_fxHitGround ) },
+ { "OCLHitGround", INI::parseObjectCreationList, NULL, offsetof( ShipSlowDeathBehaviorModuleData, m_oclHitGround ) },
+
+ { "SoundSinkLoop", INI::parseAudioEventRTS, NULL, offsetof( ShipSlowDeathBehaviorModuleData, m_deathSound) },
+
+ { 0, 0, 0, 0 }
+
+ };
+
+ p.add(dataFieldParse);
+
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Ship slow death update ///////////////////////////////////////////////////////////////////
+/////////////////////////////////////////////////////////////////////////////////////////////////
+
+//-------------------------------------------------------------------------------------------------
+//-------------------------------------------------------------------------------------------------
+ShipSlowDeathBehavior::ShipSlowDeathBehavior( Thing *thing, const ModuleData *moduleData )
+ : SlowDeathBehavior( thing, moduleData )
+{
+
+}
+
+//-------------------------------------------------------------------------------------------------
+//-------------------------------------------------------------------------------------------------
+ShipSlowDeathBehavior::~ShipSlowDeathBehavior( void )
+{
+
+}
+
+//-------------------------------------------------------------------------------------------------
+//-------------------------------------------------------------------------------------------------
+void ShipSlowDeathBehavior::beginSlowDeath( const DamageInfo *damageInfo )
+{
+ // extending functionality
+ SlowDeathBehavior::beginSlowDeath( damageInfo );
+
+ // get the module data
+ const ShipSlowDeathBehaviorModuleData* d = getShipSlowDeathBehaviorModuleData();
+
+ UnsignedInt now = TheGameLogic->getFrame();
+ m_initStartFrame = now;
+ m_toppleStartFrame = now + (d->m_initialDelay + GameLogicRandomValue(0, d->m_initialDelayVariance));
+
+ switch (d->m_toppleType)
+ {
+ case TOPPLE_RANDOM:
+ m_chosenToppleType = GameLogicRandomValue(TOPPLE_FRONT, TOPPLE_SIDE_RIGHT);
+ break;
+ case TOPPLE_SIDE:
+ m_chosenToppleType = GameLogicRandomValue(TOPPLE_SIDE_LEFT, TOPPLE_SIDE_RIGHT);
+ break;
+ default:
+ m_chosenToppleType = d->m_toppleType;
+ break;
+ }
+
+ m_toppleVarianceRoll = GameLogicRandomValueReal(0.0f, 1.0f);
+
+ beginInitPhase();
+
+}
+
+//-------------------------------------------------------------------------------------------------
+//-------------------------------------------------------------------------------------------------
+UpdateSleepTime ShipSlowDeathBehavior::update( void )
+{
+/// @todo srj use SLEEPY_UPDATE here
+ // call the base class cause we're extending functionality
+ SlowDeathBehavior::update();
+
+ // get out of here if we're not activated yet
+ if( isSlowDeathActivated() == FALSE )
+ return UPDATE_SLEEP_NONE;
+
+ // get the module data
+ const ShipSlowDeathBehaviorModuleData *d = getShipSlowDeathBehaviorModuleData();
+
+
+ UnsignedInt now = TheGameLogic->getFrame();
+ if (now < m_toppleStartFrame) {
+ doInitPhase();
+ doWobble();
+ return UPDATE_SLEEP_NONE;
+ }
+ else if (m_sinkStartFrame <= 0) {
+ if (d->m_toppleDuration > 0) {
+ m_sinkStartFrame = now + d->m_toppleDuration;
+ beginTopplePhase();
+ }
+ else {
+ m_sinkStartFrame = now;
+ }
+ }
+ if (now < m_sinkStartFrame) {
+ doTopplePhase();
+ doWobble();
+ return UPDATE_SLEEP_NONE;
+ }
+ else {
+ if (!m_shipSinkStarted)
+ beginSinkPhase();
+
+ doSinkPhase();
+ doWobble();
+ //return UPDATE_SLEEP_NONE;
+ }
+
+ // Check for crashing onto ground
+
+ Object* obj = getObject();
+ if (obj->getHeightAboveTerrain() <= 1.0) {
+
+ // make effect
+ FXList::doFXObj(d->m_fxHitGround, obj);
+ ObjectCreationList::create(d->m_oclHitGround, obj, NULL);
+
+ // Stop the sound from playing.
+ TheAudio->removeAudioEvent(m_deathSound.getPlayingHandle());
+
+ // destroy the ship finally
+ TheGameLogic->destroyObject(obj);
+ }
+
+
+ return UPDATE_SLEEP_NONE;
+
+}
+
+// -------------
+// Utils
+inline Real smoothStep(Real x)
+{
+ x = WWMath::Clamp(x, 0.0f, 1.0f);
+ return x * x * (3.0f - 2.0f * x);
+}
+
+inline Real smoothPlateau(Real x, Real e)
+{
+ x = std::clamp(x, 0.0f, 1.0f);
+ e = std::clamp(e, 0.0f, 0.5f);
+
+ if (e == 0.0f)
+ return (x > 0.0f && x < 1.0f) ? 1.0f : 0.0f;
+
+ // Ease-in region
+ if (x < e)
+ {
+ Real t = x / e; // map to 0..1
+ return smoothStep(t); // smoothstep
+ }
+
+ // Flat top
+ if (x <= 1.0f - e)
+ {
+ return 1.0f;
+ }
+
+ // Ease-out region
+ Real t = (x - (1.0f - e)) / e; // map to 0..1
+ Real s = smoothStep(t); // smoothstep
+ return 1.0f - s;
+}
+
+inline Real fastSin01(Real x)
+{
+ x = x * 2.0f * PI - PI;
+
+ // Optional: wrap input to [-pi, pi] for stability
+ x = std::fmod(x + PI, 2.0f * PI);
+ if (x < 0.0f) x += 2.0f * PI;
+ x -= PI;
+
+ // Parabolic approximation
+ const Real B = 4.0f / PI;
+ const Real C = -4.0f / (PI * PI);
+
+ Real y = B * x + C * x * std::fabs(x);
+
+ // Extra precision correction (improves peaks)
+ const Real P = 0.225f;
+ y = P * (y * std::fabs(y) - y) + y;
+
+ return y;
+}
+
+
+// ------------------------------------------------------------------------------------------------
+void ShipSlowDeathBehavior::beginInitPhase()
+{
+ //DEBUG_LOG((">>> BEGIN INIT PHASE"));
+
+ const ShipSlowDeathBehaviorModuleData* d = getShipSlowDeathBehaviorModuleData();
+ Drawable* draw = getObject()->getDrawable();
+ if (draw) {
+ if (d->m_conditionFlagInit != MODELCONDITION_INVALID)
+ draw->clearAndSetModelConditionState(MODELCONDITION_RUBBLE, d->m_conditionFlagInit);
+ }
+}
+// ------------------------------------------------------------------------------------------------
+void ShipSlowDeathBehavior::beginTopplePhase()
+{
+ //DEBUG_LOG((">>> BEGIN TOPPLE PHASE"));
+
+ const ShipSlowDeathBehaviorModuleData* d = getShipSlowDeathBehaviorModuleData();
+ Object* obj = getObject();
+ // make effect
+ FXList::doFXObj(d->m_fxStartTopple, obj);
+ ObjectCreationList::create(d->m_oclStartTopple, obj, NULL);
+
+ Drawable* draw = getObject()->getDrawable();
+ if (draw) {
+
+ ModelConditionFlags flagsToClear;
+ ModelConditionFlags flagsToSet;
+
+ if (d->m_conditionFlagInit != MODELCONDITION_INVALID)
+ flagsToClear.set(d->m_conditionFlagInit);
+
+ if (d->m_conditionFlagTopple != MODELCONDITION_INVALID) {
+ flagsToSet.set(d->m_conditionFlagTopple);
+
+ draw->clearAndSetModelConditionFlags(flagsToClear, flagsToSet);
+ }
+ }
+}
+// ------------------------------------------------------------------------------------------------
+void ShipSlowDeathBehavior::beginSinkPhase()
+{
+ //DEBUG_LOG((">>> BEGIN SINK PHASE"));
+ m_shipSinkStarted = TRUE;
+
+ const ShipSlowDeathBehaviorModuleData* d = getShipSlowDeathBehaviorModuleData();
+ Object* obj = getObject();
+ // make effect
+ //FXList::doFXObj(d->m_fxStartTopple, obj);
+ //ObjectCreationList::create(d->m_oclStartTopple, obj, NULL);
+
+ m_deathSound = d->m_deathSound;
+
+ if (m_deathSound.getEventName().isEmpty() == false)
+ {
+ m_deathSound.setObjectID(getObject()->getID());
+ m_deathSound.setPlayingHandle(TheAudio->addAudioEvent(&m_deathSound));
+ }
+
+ if (obj->getAIUpdateInterface()) {
+ Locomotor* locomotor = obj->getAIUpdateInterface()->getCurLocomotor();
+ if (locomotor)
+ locomotor->setMaxLift(-TheGlobalData->m_gravity * (1.0f - d->m_sinkHowFast));
+ }
+
+ Drawable* draw = obj->getDrawable();
+ if (draw) {
+
+ ModelConditionFlags flagsToClear;
+ ModelConditionFlags flagsToSet;
+
+ if (d->m_conditionFlagTopple != MODELCONDITION_INVALID)
+ flagsToClear.set(d->m_conditionFlagInit);
+
+ if (d->m_conditionFlagSink != MODELCONDITION_INVALID) {
+ flagsToSet.set(d->m_conditionFlagSink);
+
+ draw->clearAndSetModelConditionFlags(flagsToClear, flagsToSet);
+ }
+
+ if (d->m_attachParticleSystem)
+ {
+ // If no bones are specified, create one instance
+ if (d->m_attachParticleBoneNames.empty()) {
+ ParticleSystem* pSys = TheParticleSystemManager->createParticleSystem(d->m_attachParticleSystem);
+ if (pSys)
+ pSys->attachToObject(obj);
+ }
+ else { // one for each bone
+ Coord3D pos;
+ for (AsciiString boneName : d->m_attachParticleBoneNames) {
+ ParticleSystem* pSys = TheParticleSystemManager->createParticleSystem(d->m_attachParticleSystem);
+ if (pSys) {
+ if (draw->getPristineBonePositions(boneName.str(), 0, &pos, NULL, 1))
+ pSys->setPosition(&pos);
+ pSys->attachToObject(obj);
+ }
+ }
+ }
+ }
+ }
+}
+// ------------------------------------------------------------------------------------------------
+// ------------------------------------------------------------------------------------------------
+void ShipSlowDeathBehavior::doInitPhase()
+{
+ //DEBUG_LOG((">>> SSD doInitPhase 0"));
+
+ //const ShipSlowDeathBehaviorModuleData* d = getShipSlowDeathBehaviorModuleData();
+
+
+ /*const Matrix3D* instMatrix = obj->getDrawable()->getInstanceMatrix();
+ Matrix3D newInstMatrix;
+ newInstMatrix = *instMatrix;
+ newInstMatrix.Set_Translation(shudder);*/
+
+}
+// ------------------------------------------------------------------------------------------------
+void ShipSlowDeathBehavior::doTopplePhase()
+{
+ //DEBUG_LOG((">>> SSD doTopplePhase 0"));
+
+ const ShipSlowDeathBehaviorModuleData* d = getShipSlowDeathBehaviorModuleData();
+
+ if (d->m_toppleDuration <= 0)
+ return;
+
+ Object* obj = getObject();
+
+ UnsignedInt now = TheGameLogic->getFrame();
+ UnsignedInt timePassed = now - m_toppleStartFrame;
+ Real p = INT_TO_REAL(timePassed) / INT_TO_REAL(d->m_toppleDuration);
+
+ Real progressAngle = smoothStep(p); //TODO: apply sinus curve
+ Real progressHeight = smoothStep(p);
+ Real progressPush = smoothPlateau(p, 0.5);
+
+ Matrix3D mtx = *obj->getTransformMatrix();
+
+ Real targetPitch = 0.0f;
+ Real targetYaw = 0.0f;
+ Real targetRoll = 0.0f;
+ Real targetZ = 0.0f;
+
+ const Coord3D* dir = obj->getUnitDirectionVector2D();
+ Coord3D force;
+
+ Real accelForce;
+ //force.x = accelForce * dir->x;
+ //force.y = accelForce * dir->y;
+ force.z = 0.0f;
+
+ // Topple the boat
+ switch (m_chosenToppleType)
+ {
+ case TOPPLE_FRONT:
+ targetPitch = progressAngle * WWMath::Lerp(d->m_toppleFrontMinPitch, d->m_toppleFrontMaxPitch, m_toppleVarianceRoll);
+ accelForce = progressPush * WWMath::Lerp(d->m_toppleMinPushForce, d->m_toppleMaxPushForce, m_toppleVarianceRoll);
+ force.x = accelForce * -dir->x;
+ force.y = accelForce * -dir->y;
+ break;
+ case TOPPLE_BACK:
+ targetPitch = -progressAngle * WWMath::Lerp(d->m_toppleBackMinPitch, d->m_toppleBackMaxPitch, m_toppleVarianceRoll);
+ accelForce = progressPush * WWMath::Lerp(d->m_toppleMinPushForce, d->m_toppleMaxPushForce, m_toppleVarianceRoll);
+ force.x = accelForce * dir->x;
+ force.y = accelForce * dir->y;
+ break;
+ case TOPPLE_SIDE_LEFT:
+ targetRoll = progressAngle * WWMath::Lerp(d->m_toppleSideMinRoll, d->m_toppleSideMaxRoll, m_toppleVarianceRoll);
+ accelForce = progressPush * WWMath::Lerp(d->m_toppleMinPushForceSide, d->m_toppleMaxPushForceSide, m_toppleVarianceRoll);
+ force.x = accelForce * -dir->y;
+ force.y = accelForce * dir->x;
+ break;
+ case TOPPLE_SIDE_RIGHT:
+ targetRoll = -progressAngle * WWMath::Lerp(d->m_toppleSideMinRoll, d->m_toppleSideMaxRoll, m_toppleVarianceRoll);
+ accelForce = progressPush * WWMath::Lerp(d->m_toppleMinPushForceSide, d->m_toppleMaxPushForceSide, m_toppleVarianceRoll);
+ force.x = accelForce * dir->y;
+ force.y = accelForce * -dir->x;
+ break;
+ case TOPPLE_NONE:
+ return;
+ default:
+ DEBUG_CRASH(("Invalid topple type in ShipSlowDeathBehavior"));
+ }
+
+ PhysicsBehavior* physics = obj->getPhysics();
+ DEBUG_ASSERTCRASH(physics, ("ShipSlowDeathBehavior: object '%s' does not have a physics module",
+ obj->getTemplate()->getName().str()));
+ physics->applyMotiveForce(&force);
+
+ targetZ = progressHeight * WWMath::Lerp(d->m_toppleHeightMinOffset, d->m_toppleHeightMaxOffset, m_toppleVarianceRoll);
+
+ // const Coord3D* pos = obj->getPosition();
+
+ Real deltaPitch = targetPitch - m_curPitch;
+ Real deltaYaw = targetYaw - m_curYaw;
+ Real deltaRoll = targetRoll - m_curRoll;
+ Real deltaZ = targetZ - m_curZ;
+
+ //Real R2D = 180.0 / PI;
+ //DEBUG_LOG((">>> SSD TopplePhase: target(P/Y/R/z) = (%f, %f, %f, %f), delta(P/Y/R/Z) = (%f, %f, %f, %f)",
+ // targetPitch*R2D, targetYaw*R2D, targetRoll*R2D, targetZ, deltaPitch*R2D, deltaYaw*R2D, deltaRoll*R2D, deltaZ));
+
+
+ mtx.Rotate_X(deltaRoll);
+ mtx.Rotate_Y(deltaPitch);
+ mtx.Rotate_Z(deltaYaw);
+
+ //mtx.Adjust_Z_Translation(deltaZ);
+
+ obj->setTransformMatrix(&mtx);
+ // obj->setPositionZ(pos->z + deltaZ);
+
+ m_curPitch += deltaPitch;
+ m_curYaw += deltaYaw;
+ m_curRoll += deltaRoll;
+ m_curZ += deltaZ;
+
+ if (obj->getAIUpdateInterface())
+ {
+ Locomotor* locomotor = obj->getAIUpdateInterface()->getCurLocomotor();
+ if (locomotor) {
+ locomotor->setPreferredHeight(locomotor->getPreferredHeight() + deltaZ);
+ }
+ }
+
+}
+
+inline Real getDelta(Real angle, Real delta) {
+ if (angle == 0 || delta == 0)
+ return 0.0f;
+ if (angle < 0)
+ return MIN(-angle, delta);
+ else
+ return -MIN(angle, delta);
+}
+
+// ------------------------------------------------------------------------------------------------
+void ShipSlowDeathBehavior::doSinkPhase()
+{
+ //DEBUG_LOG((">>> SSD doSinkPhase 0"));
+
+ const ShipSlowDeathBehaviorModuleData* d = getShipSlowDeathBehaviorModuleData();
+
+ Object* obj = getObject();
+
+ //if (d->m_toppleDamping < 1.0) {
+ {
+ Matrix3D mtx = *obj->getTransformMatrix();
+
+ //Real targetPitch = m_curPitch * d->m_toppleDamping;
+ //Real targetYaw = m_curYaw * d->m_toppleDamping;
+ //Real targetRoll = m_curRoll * d->m_toppleDamping;
+
+ //Real deltaPitch = targetPitch - m_curPitch;
+ //Real deltaYaw = targetYaw - m_curYaw;
+ //Real deltaRoll = targetRoll - m_curRoll;
+
+ Real angleRate = d->m_toppleAngleCorrectionRate;
+
+ Real deltaPitch = getDelta(m_curPitch, angleRate);
+ Real deltaYaw = getDelta(m_curYaw, angleRate);
+ Real deltaRoll = getDelta(m_curRoll, angleRate);
+
+ //Real R2D = 180.0 / PI;
+ //DEBUG_LOG((">>> SINK TOPPLE CORRECTION, curAngle(P/Y/R) = (%f, %f, %f), delta(P/Y/R) = (%f, %f, %f)",
+ // m_curPitch*R2D, m_curYaw*R2D, m_curRoll*R2D,
+ // deltaPitch*R2D, deltaYaw*R2D, deltaRoll*R2D
+ // ));
+
+ mtx.Rotate_X(deltaRoll);
+ mtx.Rotate_Y(deltaPitch);
+ mtx.Rotate_Z(deltaYaw);
+
+ obj->setTransformMatrix(&mtx);
+
+ m_curPitch += deltaPitch;
+ m_curYaw += deltaYaw;
+ m_curRoll += deltaRoll;
+
+ }
+}
+// ------------------------------------------------------------------------------------------------
+void ShipSlowDeathBehavior::doWobble()
+{
+
+ const ShipSlowDeathBehaviorModuleData* d = getShipSlowDeathBehaviorModuleData();
+
+ if (d->m_wobbleInterval <= 0)
+ return;
+
+ // do the wobble
+ UnsignedInt now = TheGameLogic->getFrame();
+ UnsignedInt timePassed = now - m_initStartFrame;
+ Real pInterval = INT_TO_REAL(timePassed % d->m_wobbleInterval) / INT_TO_REAL(d->m_wobbleInterval);
+
+ // short ease in
+ Real pTotal = INT_TO_REAL(timePassed) / 30.0; // 1sec
+ Real progressTotal = smoothStep(pTotal);
+
+
+ Drawable* draw = getObject()->getDrawable();
+ if (draw) {
+ Matrix3D mx = *draw->getInstanceMatrix();
+
+ //Real pitch = progressInterval * progressTotal * d->m_initWobbleMaxPitch;
+ //Real yaw = progressInterval * progressTotal * d->m_initWobbleMaxYaw;
+ //Real roll = progressInterval * progressTotal * d->m_initWobbleMaxRoll;
+
+ Real pitch = sin(pInterval * TWO_PI) * progressTotal * d->m_wobbleMaxPitch;
+ Real yaw = cos(pInterval * TWO_PI) * progressTotal * d->m_wobbleMaxYaw;
+ Real roll = -sin(pInterval * TWO_PI * 2) * progressTotal * d->m_wobbleMaxRoll;
+
+ mx.Make_Identity();
+ mx.Rotate_Z(roll);
+ mx.Rotate_Y(yaw);
+ mx.Rotate_X(pitch);
+
+ draw->setInstanceMatrix(&mx);
+ }
+}
+
+// ------------------------------------------------------------------------------------------------
+
+
+// ------------------------------------------------------------------------------------------------
+/** CRC */
+// ------------------------------------------------------------------------------------------------
+void ShipSlowDeathBehavior::crc( Xfer *xfer )
+{
+
+ // extend base class
+ SlowDeathBehavior::crc( xfer );
+
+}
+
+// ------------------------------------------------------------------------------------------------
+/** Xfer method
+ * Version Info:
+ * 1: Initial version */
+// ------------------------------------------------------------------------------------------------
+void ShipSlowDeathBehavior::xfer( Xfer *xfer )
+{
+
+ // version
+ XferVersion currentVersion = 1;
+ XferVersion version = currentVersion;
+ xfer->xferVersion( &version, currentVersion );
+
+ // extend base class
+ SlowDeathBehavior::xfer( xfer );
+
+ xfer->xferUnsignedInt( &m_initStartFrame);
+ xfer->xferUnsignedInt( &m_toppleStartFrame);
+ xfer->xferUnsignedInt( &m_sinkStartFrame);
+ xfer->xferBool( &m_shipSinkStarted);
+ xfer->xferInt( &m_chosenToppleType);
+
+ xfer->xferReal( &m_toppleVarianceRoll);
+
+ xfer->xferReal( &m_curPitch);
+ xfer->xferReal( &m_curYaw);
+ xfer->xferReal( &m_curRoll);
+ xfer->xferReal( &m_curZ);
+
+}
+
+// ------------------------------------------------------------------------------------------------
+/** Load post process */
+// ------------------------------------------------------------------------------------------------
+void ShipSlowDeathBehavior::loadPostProcess( void )
+{
+
+ // extend base class
+ SlowDeathBehavior::loadPostProcess();
+
+}