2025-04-10 21:24:32 -07:00
|
|
|
/* Copyright (C) 2025 Wildfire Games.
|
2023-12-02 16:30:12 -08:00
|
|
|
* This file is part of 0 A.D.
|
2010-08-01 10:38:01 -07:00
|
|
|
*
|
2023-12-02 16:30:12 -08:00
|
|
|
* 0 A.D. is free software: you can redistribute it and/or modify
|
2010-08-01 10:38:01 -07:00
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation, either version 2 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
2023-12-02 16:30:12 -08:00
|
|
|
* 0 A.D. is distributed in the hope that it will be useful,
|
2010-08-01 10:38:01 -07:00
|
|
|
* 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
|
2023-12-02 16:30:12 -08:00
|
|
|
* along with 0 A.D. If not, see <http://www.gnu.org/licenses/>.
|
2010-08-01 10:38:01 -07:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include "precompiled.h"
|
|
|
|
|
|
|
|
|
|
#include "ICmpDecay.h"
|
|
|
|
|
|
2025-08-02 12:24:35 -07:00
|
|
|
#include "lib/debug.h"
|
|
|
|
|
#include "maths/BoundingBoxAligned.h"
|
|
|
|
|
#include "maths/Fixed.h"
|
|
|
|
|
#include "maths/FixedVector3D.h"
|
|
|
|
|
#include "maths/Vector3D.h"
|
|
|
|
|
#include "ps/Profile.h"
|
2010-08-01 10:38:01 -07:00
|
|
|
#include "simulation2/MessageTypes.h"
|
2025-08-02 12:24:35 -07:00
|
|
|
#include "simulation2/components/ICmpPosition.h"
|
|
|
|
|
#include "simulation2/components/ICmpTerrain.h"
|
|
|
|
|
#include "simulation2/components/ICmpVisual.h"
|
|
|
|
|
#include "simulation2/helpers/Position.h"
|
|
|
|
|
#include "simulation2/system/Component.h"
|
|
|
|
|
#include "simulation2/system/Entity.h"
|
|
|
|
|
#include "simulation2/system/Message.h"
|
2010-08-01 10:38:01 -07:00
|
|
|
|
2025-08-02 12:24:35 -07:00
|
|
|
#include <algorithm>
|
|
|
|
|
#include <cmath>
|
2025-09-14 10:01:37 -07:00
|
|
|
#include <optional>
|
|
|
|
|
#include <random>
|
2025-08-02 12:24:35 -07:00
|
|
|
#include <string>
|
2016-06-21 03:33:11 -07:00
|
|
|
|
2010-08-01 10:38:01 -07:00
|
|
|
/**
|
|
|
|
|
* Fairly basic decay implementation, for units and buildings etc.
|
|
|
|
|
* The decaying entity remains stationary for some time period, then falls downwards
|
|
|
|
|
* with some initial speed and some acceleration, until it has fully sunk.
|
|
|
|
|
* The sinking depth is determined from the actor's bounding box and the terrain.
|
|
|
|
|
*
|
|
|
|
|
* This currently doesn't work with entities whose ICmpPosition has an initial Y offset.
|
|
|
|
|
*
|
|
|
|
|
* This isn't very efficient (we'll store data and iterate every frame for every entity,
|
|
|
|
|
* not just for corpse entities) - it could be designed more optimally if that's a real problem.
|
|
|
|
|
*
|
|
|
|
|
* Eventually we might want to adjust the decay rate based on user configuration (low-spec
|
|
|
|
|
* machines could have fewer corpses), number of corpses, etc.
|
|
|
|
|
*
|
|
|
|
|
* Must not be used on network-synchronised entities, unless \<Inactive\> is present.
|
|
|
|
|
*/
|
2022-03-03 14:42:26 -08:00
|
|
|
class CCmpDecay final : public ICmpDecay
|
2010-08-01 10:38:01 -07:00
|
|
|
{
|
|
|
|
|
public:
|
2025-06-03 23:10:15 -07:00
|
|
|
static void ClassInit(CComponentManager&)
|
2010-08-01 10:38:01 -07:00
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DEFAULT_COMPONENT_ALLOCATOR(Decay)
|
|
|
|
|
|
|
|
|
|
bool m_Active;
|
2013-06-14 12:19:13 -07:00
|
|
|
bool m_ShipSink;
|
2025-09-14 10:01:37 -07:00
|
|
|
bool m_Stochastic;
|
|
|
|
|
|
|
|
|
|
std::optional<std::minstd_rand> m_Generator;
|
|
|
|
|
std::negative_binomial_distribution<> m_Distribution{ 6, 0.032 };
|
|
|
|
|
std::geometric_distribution<>::result_type m_NextSink;
|
|
|
|
|
|
|
|
|
|
float m_SinkProb;
|
2010-08-01 10:38:01 -07:00
|
|
|
float m_DelayTime;
|
|
|
|
|
float m_SinkRate;
|
|
|
|
|
float m_SinkAccel;
|
|
|
|
|
|
2013-06-14 12:19:13 -07:00
|
|
|
entity_pos_t m_InitialXRotation;
|
|
|
|
|
entity_pos_t m_InitialZRotation;
|
|
|
|
|
|
2013-07-22 03:17:00 -07:00
|
|
|
// Used to randomize ship-like sinking
|
|
|
|
|
float m_SinkingAngleX;
|
|
|
|
|
float m_SinkingAngleZ;
|
|
|
|
|
|
2010-08-01 10:38:01 -07:00
|
|
|
float m_CurrentTime;
|
|
|
|
|
float m_TotalSinkDepth; // distance we need to sink (derived from bounding box), or -1 if undetermined
|
|
|
|
|
|
|
|
|
|
static std::string GetSchema()
|
|
|
|
|
{
|
|
|
|
|
return
|
2015-01-21 13:45:05 -08:00
|
|
|
"<element name='Active' a:help='If false, the entity will not do any decaying'>"
|
|
|
|
|
"<data type='boolean'/>"
|
|
|
|
|
"</element>"
|
|
|
|
|
"<element name='SinkingAnim' a:help='If true, the entity will decay in a ship-like manner'>"
|
|
|
|
|
"<data type='boolean'/>"
|
|
|
|
|
"</element>"
|
2025-09-14 10:01:37 -07:00
|
|
|
"<element name='SinkProb' a:help='The entity decays according to the supplied probability each frame.'>"
|
|
|
|
|
"<ref name='nonNegativeDecimal'/>"
|
|
|
|
|
"</element>"
|
2010-08-01 10:38:01 -07:00
|
|
|
"<element name='DelayTime' a:help='Time to wait before starting to sink, in seconds'>"
|
|
|
|
|
"<ref name='nonNegativeDecimal'/>"
|
|
|
|
|
"</element>"
|
2025-04-10 21:24:32 -07:00
|
|
|
"<element name='SinkRate' a:help='Initial rate of sinking, in meters per second'>"
|
2010-08-01 10:38:01 -07:00
|
|
|
"<ref name='nonNegativeDecimal'/>"
|
|
|
|
|
"</element>"
|
2025-04-10 21:24:32 -07:00
|
|
|
"<element name='SinkAccel' a:help='Acceleration rate of sinking, in meters per second per second'>"
|
2010-08-01 10:38:01 -07:00
|
|
|
"<ref name='nonNegativeDecimal'/>"
|
2015-01-21 13:45:05 -08:00
|
|
|
"</element>";
|
2010-08-01 10:38:01 -07:00
|
|
|
}
|
|
|
|
|
|
2022-03-07 15:04:11 -08:00
|
|
|
void Init(const CParamNode& paramNode) override
|
2010-08-01 10:38:01 -07:00
|
|
|
{
|
2015-01-21 13:45:05 -08:00
|
|
|
m_Active = paramNode.GetChild("Active").ToBool();
|
|
|
|
|
m_ShipSink = paramNode.GetChild("SinkingAnim").ToBool();
|
2025-09-14 10:01:37 -07:00
|
|
|
m_SinkProb = paramNode.GetChild("SinkProb").ToFixed().ToFloat();
|
2010-08-01 10:38:01 -07:00
|
|
|
m_DelayTime = paramNode.GetChild("DelayTime").ToFixed().ToFloat();
|
|
|
|
|
m_SinkRate = paramNode.GetChild("SinkRate").ToFixed().ToFloat();
|
|
|
|
|
m_SinkAccel = paramNode.GetChild("SinkAccel").ToFixed().ToFloat();
|
|
|
|
|
|
|
|
|
|
m_CurrentTime = 0.f;
|
|
|
|
|
m_TotalSinkDepth = -1.f;
|
|
|
|
|
|
2025-09-30 19:43:15 -07:00
|
|
|
m_Stochastic = m_SinkProb < 1.0f;
|
2025-09-14 10:01:37 -07:00
|
|
|
|
2025-09-30 19:43:15 -07:00
|
|
|
std::negative_binomial_distribution<int>::param_type new_params(
|
|
|
|
|
6, Clamp(m_SinkProb, 1e-3f, 1.0f));
|
2025-09-14 10:01:37 -07:00
|
|
|
m_Distribution.param(new_params);
|
|
|
|
|
|
2010-08-01 10:38:01 -07:00
|
|
|
// Detect unsafe misconfiguration
|
|
|
|
|
if (m_Active && !ENTITY_IS_LOCAL(GetEntityId()))
|
|
|
|
|
{
|
|
|
|
|
debug_warn(L"CCmpDecay must not be used on non-local (network-synchronised) entities");
|
|
|
|
|
m_Active = false;
|
|
|
|
|
}
|
2014-06-19 16:20:12 -07:00
|
|
|
|
|
|
|
|
if (m_Active)
|
|
|
|
|
GetSimContext().GetComponentManager().DynamicSubscriptionNonsync(MT_Interpolate, this, true);
|
2010-08-01 10:38:01 -07:00
|
|
|
}
|
|
|
|
|
|
2022-03-07 15:04:11 -08:00
|
|
|
void Deinit() override
|
2010-08-01 10:38:01 -07:00
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 23:10:15 -07:00
|
|
|
void Serialize(ISerializer&) override
|
2010-08-01 10:38:01 -07:00
|
|
|
{
|
|
|
|
|
// This component isn't network-synchronised, so don't serialize anything
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-03 23:10:15 -07:00
|
|
|
void Deserialize(const CParamNode& paramNode, IDeserializer&) override
|
2010-08-01 10:38:01 -07:00
|
|
|
{
|
2011-01-16 06:08:38 -08:00
|
|
|
Init(paramNode);
|
2010-08-01 10:38:01 -07:00
|
|
|
}
|
|
|
|
|
|
2025-06-03 05:13:41 -07:00
|
|
|
void HandleMessage(const CMessage& msg, bool /*global*/) override
|
2010-08-01 10:38:01 -07:00
|
|
|
{
|
|
|
|
|
switch (msg.GetType())
|
|
|
|
|
{
|
|
|
|
|
case MT_Interpolate:
|
|
|
|
|
{
|
2016-06-25 06:12:35 -07:00
|
|
|
PROFILE("Decay::Interpolate");
|
2014-06-19 16:20:12 -07:00
|
|
|
|
2010-08-01 10:38:01 -07:00
|
|
|
if (!m_Active)
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
const CMessageInterpolate& msgData = static_cast<const CMessageInterpolate&> (msg);
|
|
|
|
|
|
2013-09-11 13:41:53 -07:00
|
|
|
CmpPtr<ICmpPosition> cmpPosition(GetEntityHandle());
|
2012-02-07 18:46:15 -08:00
|
|
|
if (!cmpPosition || !cmpPosition->IsInWorld())
|
2010-08-01 10:38:01 -07:00
|
|
|
{
|
|
|
|
|
// If there's no position (this usually shouldn't happen), destroy the unit immediately
|
|
|
|
|
GetSimContext().GetComponentManager().DestroyComponentsSoon(GetEntityId());
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compute the depth the first time this is called
|
|
|
|
|
// (This is a bit of an ugly place to do it but at least we'll be sure
|
|
|
|
|
// the actor component was loaded already)
|
|
|
|
|
if (m_TotalSinkDepth < 0.f)
|
|
|
|
|
{
|
|
|
|
|
m_TotalSinkDepth = 1.f; // minimum so we always sink at least a little
|
|
|
|
|
|
2013-09-11 13:41:53 -07:00
|
|
|
CmpPtr<ICmpVisual> cmpVisual(GetEntityHandle());
|
2012-02-07 18:46:15 -08:00
|
|
|
if (cmpVisual)
|
2010-08-01 10:38:01 -07:00
|
|
|
{
|
2011-11-24 22:36:13 -08:00
|
|
|
CBoundingBoxAligned bound = cmpVisual->GetBounds();
|
2010-08-01 10:38:01 -07:00
|
|
|
m_TotalSinkDepth = std::max(m_TotalSinkDepth, bound[1].Y - bound[0].Y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If this is a floating unit, we want it to sink all the way under the terrain,
|
|
|
|
|
// so find the difference between its current position and the terrain
|
|
|
|
|
|
|
|
|
|
CFixedVector3D pos = cmpPosition->GetPosition();
|
|
|
|
|
|
2025-09-14 10:01:37 -07:00
|
|
|
//Use the entities position upon decay start to vary the pseudorandom seed.
|
|
|
|
|
if (!m_Generator.has_value())
|
|
|
|
|
{
|
|
|
|
|
m_Generator.emplace(pos.X.ToInt_RoundToNearest() * pos.Z.ToInt_RoundToNearest());
|
|
|
|
|
m_NextSink = m_Distribution(m_Generator.value());
|
|
|
|
|
}
|
|
|
|
|
|
2013-09-11 13:41:53 -07:00
|
|
|
CmpPtr<ICmpTerrain> cmpTerrain(GetSystemEntity());
|
2012-02-07 18:46:15 -08:00
|
|
|
if (cmpTerrain)
|
2010-08-01 10:38:01 -07:00
|
|
|
{
|
|
|
|
|
fixed ground = cmpTerrain->GetGroundLevel(pos.X, pos.Z);
|
|
|
|
|
m_TotalSinkDepth += std::max(0.f, (pos.Y - ground).ToFloat());
|
|
|
|
|
}
|
2013-06-14 12:19:13 -07:00
|
|
|
|
|
|
|
|
// Sink it further down if it sinks like a ship, as it will rotate.
|
|
|
|
|
if (m_ShipSink)
|
2013-07-22 03:17:00 -07:00
|
|
|
{
|
|
|
|
|
// lacking randomness we'll trick
|
|
|
|
|
m_SinkingAngleX = (pos.X.ToInt_RoundToNearest() % 30 - 15) / 15.0;
|
|
|
|
|
m_SinkingAngleZ = (pos.Z.ToInt_RoundToNearest() % 30) / 40.0;
|
2013-06-14 17:42:07 -07:00
|
|
|
m_TotalSinkDepth += 10.f;
|
2013-07-22 03:17:00 -07:00
|
|
|
}
|
2013-06-14 12:19:13 -07:00
|
|
|
// probably 0 in both cases but we'll remember it anyway.
|
|
|
|
|
m_InitialXRotation = cmpPosition->GetRotation().X;
|
|
|
|
|
m_InitialZRotation = cmpPosition->GetRotation().Z;
|
2010-08-01 10:38:01 -07:00
|
|
|
}
|
|
|
|
|
|
2012-06-06 12:37:03 -07:00
|
|
|
m_CurrentTime += msgData.deltaSimTime;
|
2010-08-01 10:38:01 -07:00
|
|
|
|
2013-06-14 12:19:13 -07:00
|
|
|
if (m_CurrentTime >= m_DelayTime)
|
2010-08-01 10:38:01 -07:00
|
|
|
{
|
2025-09-14 10:01:37 -07:00
|
|
|
//If the entity should sink stochastically, use the negative binomial distribution to see if it should sink each frame.
|
|
|
|
|
if ((m_Stochastic && (m_NextSink-- == 0)) || !m_Stochastic)
|
2013-06-14 12:19:13 -07:00
|
|
|
{
|
2025-09-14 10:01:37 -07:00
|
|
|
m_NextSink = m_Distribution(m_Generator.value());
|
|
|
|
|
float t = m_CurrentTime - m_DelayTime;
|
|
|
|
|
float depth = (m_SinkRate * t) + (m_SinkAccel * t * t);
|
|
|
|
|
|
|
|
|
|
if (m_ShipSink)
|
|
|
|
|
{
|
|
|
|
|
// exponential sinking with tilting
|
|
|
|
|
float tilt_time = t > 5.f ? 5.f : t;
|
|
|
|
|
float tiltSink = tilt_time * tilt_time / 5.f;
|
|
|
|
|
entity_pos_t RotX = entity_pos_t::FromFloat(((m_InitialXRotation.ToFloat() * (5.f - tiltSink)) + (m_SinkingAngleX * tiltSink)) / 5.f);
|
|
|
|
|
entity_pos_t RotZ = entity_pos_t::FromFloat(((m_InitialZRotation.ToFloat() * (3.f - tilt_time)) + (m_SinkingAngleZ * tilt_time)) / 3.f);
|
|
|
|
|
cmpPosition->SetXZRotation(RotX,RotZ);
|
|
|
|
|
|
|
|
|
|
depth = m_SinkRate * (exp(t - 1.f) - 0.54881163609f) + (m_SinkAccel * exp(t - 4.f) - 0.01831563888f);
|
|
|
|
|
if (depth < 0.f)
|
|
|
|
|
depth = 0.f;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cmpPosition->SetHeightOffset(entity_pos_t::FromFloat(-depth));
|
|
|
|
|
|
|
|
|
|
if (depth > m_TotalSinkDepth)
|
|
|
|
|
GetSimContext().GetComponentManager().DestroyComponentsSoon(GetEntityId());
|
2013-06-14 12:19:13 -07:00
|
|
|
}
|
2010-08-01 10:38:01 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
REGISTER_COMPONENT_TYPE(Decay)
|