/*
* This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
*
* 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 2 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 .
*/
#include "MoveSpline.h"
#include "Log.h"
#include "Creature.h"
#include "DB2Stores.h"
#include
namespace Movement{
Location MoveSpline::computePosition(int32 time_point, int32 point_index) const
{
ASSERT(Initialized());
float u = 1.0f;
float seg_time = float(spline.length(point_index, point_index + 1));
if (seg_time > 0)
u = std::min(float(time_point - spline.length(point_index)) / seg_time, 1.0f);
Location c;
c.orientation = initialOrientation;
spline.evaluate_percent(point_index, u, c);
if (splineflags.Animation)
;// MoveSplineFlag::Animation disables falling or parabolic movement
else if (splineflags.Parabolic)
computeParabolicElevation(time_point, c.z);
else if (splineflags.Falling)
computeFallElevation(time_point, c.z);
if (splineflags.Done && facing.type != MONSTER_MOVE_NORMAL)
{
if (facing.type == MONSTER_MOVE_FACING_ANGLE)
c.orientation = facing.angle;
else if (facing.type == MONSTER_MOVE_FACING_SPOT)
c.orientation = std::atan2(facing.f.y - c.y, facing.f.x - c.x);
//nothing to do for MoveSplineFlag::Final_Target flag
}
else if (splineflags.Turning)
{
c.orientation = Position::NormalizeOrientation(turn->StartFacing + float(time_point) / float(IN_MILLISECONDS) * turn->RadsPerSec);
}
else
{
if (!splineflags.HasFlag(MoveSplineFlagEnum::OrientationFixed | MoveSplineFlagEnum::Falling | MoveSplineFlagEnum::JumpOrientationFixed))
{
Vector3 hermite;
spline.evaluate_derivative(point_index, u, hermite);
if (hermite.x != 0.f || hermite.y != 0.f)
c.orientation = std::atan2(hermite.y, hermite.x);
}
if (splineflags.Backward)
c.orientation = c.orientation - float(M_PI);
}
return c;
}
Location MoveSpline::ComputePosition() const
{
return computePosition(time_passed, point_Idx);
}
Location MoveSpline::ComputePosition(int32 time_offset) const
{
int32 time_point = time_passed + time_offset;
if (time_point >= Duration())
return computePosition(Duration(), spline.last() - 1);
if (time_point <= 0)
return computePosition(0, spline.first());
// find point_index where spline.length(point_index) < time_point < spline.length(point_index + 1)
int32 point_index = point_Idx;
while (time_point >= spline.length(point_index + 1))
++point_index;
while (time_point < spline.length(point_index))
--point_index;
return computePosition(time_point, point_index);
}
void MoveSpline::computeParabolicElevation(int32 time_point, float& el) const
{
if (time_point > effect_start_time)
{
float t_passedf = MSToSec(time_point - effect_start_time);
float t_durationf = MSToSec(Duration() - effect_start_time); //client use not modified duration here
if (spell_effect_extra && spell_effect_extra->ParabolicCurveId)
t_passedf *= sDB2Manager.GetCurveValueAt(spell_effect_extra->ParabolicCurveId, float(time_point) / Duration());
// -a*x*x + bx + c:
//(dur * v3->z_acceleration * dt)/2 - (v3->z_acceleration * dt * dt)/2 + Z;
el += (t_durationf - t_passedf) * 0.5f * vertical_acceleration * t_passedf;
}
}
void MoveSpline::computeFallElevation(int32 time_point, float& el) const
{
float z_now = spline.getPoint(spline.first()).z - Movement::computeFallElevation(MSToSec(time_point), false);
float final_z = FinalDestination().z;
el = std::max(z_now, final_z);
}
struct FallInitializer
{
FallInitializer(float _start_elevation) : start_elevation(_start_elevation) { }
float start_elevation;
inline int32 operator()(Spline& s, int32 i)
{
return Movement::computeFallTime(start_elevation - s.getPoint(i+1).z, false) * 1000.f;
}
};
enum{
minimal_duration = 1
};
struct CommonInitializer
{
CommonInitializer(float _velocity) : velocityInv(1000.f/_velocity), time(minimal_duration) { }
float velocityInv;
int32 time;
inline int32 operator()(Spline& s, int32 i)
{
time += (s.SegLength(i) * velocityInv);
return time;
}
};
void MoveSpline::init_spline(MoveSplineInitArgs const& args)
{
SplineBase::EvaluationMode mode = args.flags.isSmooth() ? SplineBase::ModeCatmullrom : SplineBase::ModeLinear;
if (args.flags.Cyclic)
{
uint32 cyclic_point = 0;
if (splineflags.Enter_Cycle)
cyclic_point = 1; // shouldn't be modified, came from client
spline.init_cyclic_spline(args.path.data(), args.path.size(), mode, cyclic_point, args.initialOrientation);
}
else
spline.init_spline(args.path.data(), args.path.size(), mode, args.initialOrientation);
// init spline timestamps
if (splineflags.Falling)
{
FallInitializer init(spline.getPoint(spline.first()).z);
spline.initLengths(init);
}
else
{
CommonInitializer init(args.velocity);
spline.initLengths(init);
}
/// @todo what to do in such cases? problem is in input data (all points are at same coords)
if (spline.length() < minimal_duration)
{
TC_LOG_ERROR("misc", "MoveSpline::init_spline: zero length spline, wrong input data?");
spline.set_length(spline.last(), spline.isCyclic() ? 1000 : 1);
}
if (turn)
{
MySpline::LengthType totalTurnTime = static_cast(turn->TotalTurnRads / turn->RadsPerSec * float(IN_MILLISECONDS));
spline.set_length(spline.last(), std::max(spline.length(), totalTurnTime));
}
point_Idx = spline.first();
}
void MoveSpline::Initialize(MoveSplineInitArgs const& args)
{
splineflags = args.flags;
facing = args.facing;
m_Id = args.splineId;
point_Idx_offset = args.path_Idx_offset;
initialOrientation = args.initialOrientation;
time_passed = 0;
vertical_acceleration = 0.f;
effect_start_time = 0;
spell_effect_extra = args.spellEffectExtra;
turn = args.turnData;
anim_tier = args.animTier;
splineIsFacingOnly = args.path.size() == 2 && args.facing.type != MONSTER_MOVE_NORMAL && ((args.path[1] - args.path[0]).length() < 0.1f);
velocity = args.velocity;
// Check if its a stop spline
if (args.flags.Done)
{
spline.clear();
return;
}
init_spline(args);
// init parabolic / animation
// spline initialized, duration known and i able to compute parabolic acceleration
if (args.flags.HasFlag(MoveSplineFlagEnum::Parabolic | MoveSplineFlagEnum::Animation | MoveSplineFlagEnum::FadeObject))
{
int32 spline_duration = Duration();
effect_start_time = spline_duration * args.effect_start_time_percent + args.effect_start_time.count();
if (effect_start_time > spline_duration)
effect_start_time = spline_duration;
if (args.flags.Parabolic && effect_start_time < spline_duration)
{
if (args.parabolic_amplitude != 0.0f)
{
float f_duration = MSToSec(spline_duration - effect_start_time);
vertical_acceleration = args.parabolic_amplitude * 8.f / (f_duration * f_duration);
}
else if (args.vertical_acceleration != 0.0f)
{
vertical_acceleration = args.vertical_acceleration;
}
}
}
}
MoveSpline::MoveSpline() : m_Id(0), time_passed(0),
vertical_acceleration(0.f), initialOrientation(0.f), effect_start_time(0), point_Idx(0), point_Idx_offset(0), velocity(0.f),
onTransport(false), splineIsFacingOnly(false)
{
splineflags.Done = true;
}
/// ============================================================================================
bool MoveSplineInitArgs::Validate(Unit const* unit)
{
#define CHECK(exp, verbose) \
if (!(exp)) return [&]{ \
TC_LOG_ERROR("misc.movesplineinitargs", "MoveSplineInitArgs::Validate: expression '{}' failed for {}", #exp, unit ? std::string_view(verbose) : "cyclic spline continuation"sv); \
return false; \
}()
CHECK(path.size() > 1, unit->GetDebugInfo());
CHECK(velocity >= 0.01f, unit->GetDebugInfo());
CHECK(effect_start_time_percent >= 0.f && effect_start_time_percent <= 1.f, unit->GetDebugInfo());
CHECK(_checkPathLengths(), unit->GetGUID().ToString());
if (spellEffectExtra)
{
CHECK(!spellEffectExtra->ProgressCurveId || sCurveStore.LookupEntry(spellEffectExtra->ProgressCurveId), unit->GetDebugInfo());
CHECK(!spellEffectExtra->ParabolicCurveId || sCurveStore.LookupEntry(spellEffectExtra->ParabolicCurveId), unit->GetDebugInfo());
}
return true;
#undef CHECK
}
// check path lengths - why are we even starting such short movement?
bool MoveSplineInitArgs::_checkPathLengths()
{
constexpr float MIN_XY_OFFSET = -(1 << 11) / 4.0f;
constexpr float MIN_Z_OFFSET = -(1 << 10) / 4.0f;
// positive values have 1 less bit limit (if the highest bit was set, value would be sign extended into negative when decompressing)
constexpr float MAX_XY_OFFSET = (1 << 10) / 4.0f;
constexpr float MAX_Z_OFFSET = (1 << 9) / 4.0f;
auto isValidPackedXYOffset = [](float coord) -> bool { return coord > MIN_XY_OFFSET && coord < MAX_XY_OFFSET; };
auto isValidPackedZOffset = [](float coord) -> bool { return coord > MIN_Z_OFFSET && coord < MAX_Z_OFFSET; };
if (path.size() > 2)
{
Vector3 middle = (path.front() + path.back()) / 2;
for (uint32 i = 1; i < path.size() - 1; ++i)
{
if ((path[i + 1] - path[i]).length() < 0.1f)
return false;
// when compression is enabled, each point coord is packed into 11 bits (10 for Z)
if (!flags.UncompressedPath)
if (!isValidPackedXYOffset(middle.x - path[i].x)
|| !isValidPackedXYOffset(middle.y - path[i].y)
|| !isValidPackedZOffset(middle.z - path[i].z))
flags.UncompressedPath = true;
}
}
return true;
}
MoveSplineInitArgs::MoveSplineInitArgs() : path_Idx_offset(0), velocity(0.f),
parabolic_amplitude(0.f), vertical_acceleration(0.0f), effect_start_time_percent(0.f), effect_start_time(0ms),
splineId(0), initialOrientation(0.f),
walk(false), HasVelocity(false), TransformForTransport(true)
{
}
MoveSplineInitArgs::MoveSplineInitArgs(MoveSplineInitArgs&& args) noexcept = default;
MoveSplineInitArgs::~MoveSplineInitArgs() = default;
/// ============================================================================================
MoveSpline::UpdateResult MoveSpline::_updateState(int32& ms_time_diff)
{
if (Finalized())
{
ms_time_diff = 0;
return Result_Arrived;
}
UpdateResult result = Result_None;
int32 minimal_diff = std::min(ms_time_diff, segment_time_elapsed());
ASSERT(minimal_diff >= 0);
time_passed += minimal_diff;
ms_time_diff -= minimal_diff;
if (time_passed >= next_timestamp())
{
++point_Idx;
if (point_Idx < spline.last())
{
result = Result_NextSegment;
}
else
{
if (spline.isCyclic())
{
point_Idx = spline.first();
time_passed = time_passed % Duration();
result = Result_NextCycle;
// Remove first point from the path after one full cycle.
// That point was the position of the unit prior to entering the cycle and it shouldn't be repeated with continuous cycles.
if (splineflags.Enter_Cycle)
{
splineflags.Enter_Cycle = false;
reinit_spline_for_next_cycle();
}
}
else
{
_Finalize();
ms_time_diff = 0;
result = Result_Arrived;
}
}
}
return result;
}
void MoveSpline::reinit_spline_for_next_cycle()
{
MoveSplineInitArgs args;
args.path.assign(spline.getPoints().begin() + spline.first() + 1, spline.getPoints().begin() + spline.last());
args.facing = facing;
args.flags = splineflags;
args.path_Idx_offset = point_Idx_offset;
args.splineId = m_Id;
args.initialOrientation = initialOrientation;
args.velocity = 1.0f; // Calculated below
args.HasVelocity = true;
args.TransformForTransport = onTransport;
if (args.Validate(nullptr))
{
// New cycle should preserve previous cycle's duration for some weird reason, even though
// the path is really different now. Blizzard is weird. Or this was just a simple oversight.
// Since our splines precalculate length with velocity in mind, if we want to find the desired
// velocity, we have to make a fake spline, calculate its duration and then compare it to the
// desired duration, thus finding out how much the velocity has to be increased for them to match.
MoveSpline tempSpline;
tempSpline.Initialize(args);
args.velocity = (float)tempSpline.Duration() / Duration();
if (args.Validate(nullptr))
init_spline(args);
}
}
std::string MoveSpline::ToString() const
{
std::stringstream str;
str << "MoveSpline\n";
str << "spline Id: " << GetId() << '\n';
str << "flags: " << splineflags.ToString() << '\n';
if (facing.type == MONSTER_MOVE_FACING_ANGLE)
str << "facing angle: " << facing.angle << '\n';
else if (facing.type == MONSTER_MOVE_FACING_TARGET)
str << "facing target: " << facing.target.ToString() << '\n';
else if (facing.type == MONSTER_MOVE_FACING_SPOT)
str << "facing point: " << facing.f.x << " " << facing.f.y << " " << facing.f.z << '\n';
str << "time passed: " << time_passed << '\n';
str << "total time: " << Duration() << '\n';
str << "spline point Id: " << point_Idx << '\n';
str << "path point Id: " << currentPathIdx() << '\n';
str << spline.ToString();
return std::move(str).str();
}
void MoveSpline::_Finalize()
{
splineflags.Done = true;
point_Idx = spline.last() - 1;
time_passed = Duration();
}
int32 MoveSpline::currentPathIdx() const
{
int32 point = point_Idx_offset + point_Idx - spline.first() + (int)Finalized();
if (isCyclic())
point = point % (spline.last()-spline.first());
return point;
}
}