diff --git a/distribution/changelog.txt b/distribution/changelog.txt
index d2f454e624..7dda3462c3 100644
--- a/distribution/changelog.txt
+++ b/distribution/changelog.txt
@@ -1,5 +1,6 @@
0.4.4 (in development)
------------------------------------------------------------------------
+- Feature: [#11269] Add properties for speed and length of vehicle animations.
- Feature: [#15849] Objectives can now be set for up to 50000 guests.
- Feature: [#18537] Add shift/control modifiers to window close buttons, closing all but the given window or all windows of the same type, respectively.
- Feature: [#18732] [Plugin] API to get the guests thoughts.
diff --git a/src/openrct2/entity/Yaw.hpp b/src/openrct2/entity/Yaw.hpp
index 0e2ec6d707..aac3bf5537 100644
--- a/src/openrct2/entity/Yaw.hpp
+++ b/src/openrct2/entity/Yaw.hpp
@@ -56,6 +56,10 @@ namespace OpenRCT2::Entity::Yaw
{
return yaw;
}
+ [[nodiscard]] constexpr int32_t YawTo64(int32_t yaw)
+ {
+ return yaw << 1;
+ }
[[nodiscard]] constexpr int32_t YawToPrecision(int32_t yaw, SpritePrecision precision)
{
diff --git a/src/openrct2/libopenrct2.vcxproj b/src/openrct2/libopenrct2.vcxproj
index b86b45e203..478e94b52b 100644
--- a/src/openrct2/libopenrct2.vcxproj
+++ b/src/openrct2/libopenrct2.vcxproj
@@ -276,6 +276,7 @@
+
@@ -1028,4 +1029,4 @@
-
+
\ No newline at end of file
diff --git a/src/openrct2/math/Trigonometry.hpp b/src/openrct2/math/Trigonometry.hpp
new file mode 100644
index 0000000000..98e73f5c8c
--- /dev/null
+++ b/src/openrct2/math/Trigonometry.hpp
@@ -0,0 +1,121 @@
+/*****************************************************************************
+ * Copyright (c) 2014-2023 OpenRCT2 developers
+ *
+ * For a complete list of all authors, please refer to contributors.md
+ * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
+ *
+ * OpenRCT2 is licensed under the GNU General Public License version 3.
+ *****************************************************************************/
+
+#pragma once
+#include "../entity/Yaw.hpp"
+#include "../ride/Vehicle.h"
+#include "../world/Location.hpp"
+
+namespace OpenRCT2::Math::Trigonometry
+{
+ /**
+ * The cos and sin of sprite direction
+ * ROUND(COS((32/64+(L1/64))*(2*PI()))*256,0), ROUND(SIN(((L1/64))*(2*PI())) * 256,0)
+ * Where L1 represents an incrementing column 0 - 63
+ * Note: Must be at least 32bit to ensure all users do not overflow
+ */
+ static constexpr CoordsXY YawToDirectionVector[64] = {
+ { -256, 0 }, { -255, 25 }, { -251, 50 }, { -245, 74 }, { -237, 98 }, { -226, 121 }, { -213, 142 },
+ { -198, 162 }, { -181, 181 }, { -162, 198 }, { -142, 213 }, { -121, 226 }, { -98, 237 }, { -74, 245 },
+ { -50, 251 }, { -25, 255 }, { 0, 256 }, { 25, 255 }, { 50, 251 }, { 74, 245 }, { 98, 237 },
+ { 121, 226 }, { 142, 213 }, { 162, 198 }, { 181, 181 }, { 198, 162 }, { 213, 142 }, { 226, 121 },
+ { 237, 98 }, { 245, 74 }, { 251, 50 }, { 255, 25 }, { 256, 0 }, { 255, -25 }, { 251, -50 },
+ { 245, -74 }, { 237, -98 }, { 226, -121 }, { 213, -142 }, { 198, -162 }, { 181, -181 }, { 162, -198 },
+ { 142, -213 }, { 121, -226 }, { 98, -237 }, { 74, -245 }, { 50, -251 }, { 25, -255 }, { 0, -256 },
+ { -25, -255 }, { -50, -251 }, { -74, -245 }, { -98, -237 }, { -121, -226 }, { -142, -213 }, { -162, -198 },
+ { -181, -181 }, { -198, -162 }, { -213, -142 }, { -226, -121 }, { -237, -98 }, { -245, -74 }, { -251, -50 },
+ { -255, -25 },
+ };
+ // Currently OpenRCT2::Entity::Yaw::BaseSpritePrecision is 32, but one day it will be 64.
+ static_assert(std::size(YawToDirectionVector) == 64);
+
+ /**
+ * The cos and sin of vehicle pitch based on vehicle sprite angles
+ * COS((Y1/360)*2*PI())*256,-SIN((Y1/360)*2*PI())*256
+ * Where Y1 represents the angle of pitch in degrees
+ */
+ constexpr CoordsXY PitchToDirectionVectorFromGeometry[] = {
+ { 256, 0 }, // flat
+ { 251, 49 }, // slopes up
+ { 236, 97 }, // slopes up
+ { 195, 165 }, // slopes up
+ { 134, 217 }, // slopes up
+ { 251, -49 }, // slopes down
+ { 236, -97 }, // slopes down
+ { 195, -165 }, // slopes down
+ { 135, -217 }, // slopes down
+ { 70, 246 }, // slopes vertical up
+ { 0, 256 }, // slopes vertical up
+ { -66, 247 }, // slopes looping up
+ { -128, 221 }, // slopes looping up
+ { -181, 181 }, // slopes looping up
+ { -221, 128 }, // slopes looping up
+ { -247, 66 }, // slopes looping up
+ { -256, 0 }, // inverted
+ { 70, -246 }, // slopes vertical down
+ { 0, -256 }, // slopes vertical down
+ { -66, -247 }, // slopes looping down
+ { -128, -221 }, // slopes looping down
+ { -181, -181 }, // slopes looping down
+ { -221, -128 }, // slopes looping down
+ { -247, -66 }, // slopes looping down
+ { 221, 128 }, // corkscrew up left
+ { 128, 221 }, // corkscrew up left
+ { 0, 256 }, // corkscrew up left
+ { -128, 221 }, // corkscrew up left
+ { -221, 128 }, // corkscrew up left
+ { -221, -128 }, // corkscrew down left
+ { -128, -221 }, // corkscrew down left
+ { 0, -256 }, // corkscrew down left
+ { 128, -221 }, // corkscrew down left
+ { 221, -128 }, // corkscrew down left
+ { 221, 128 }, // corkscrew up right
+ { 128, 221 }, // corkscrew up right
+ { 0, 256 }, // corkscrew up right
+ { -128, 221 }, // corkscrew up right
+ { -221, 128 }, // corkscrew up right
+ { -221, -128 }, // corkscrew down right
+ { -128, -221 }, // corkscrew down right
+ { 0, -256 }, // corkscrew down right
+ { 128, -221 }, // corkscrew down right
+ { 221, 128 }, // corkscrew down right
+ { 256, 0 }, // half helixes
+ { 256, 0 }, // half helixes
+ { 256, 0 }, // half helixes
+ { 256, 0 }, // half helixes
+ { 256, 0 }, // quarter helixes
+ { 256, 0 }, // quarter helixes
+ { 252, 42 }, // diagonal slopes up
+ { 241, 83 }, // diagonal slopes up
+ { 168, 193 }, // diagonal slopes up
+ { 252, -42 }, // diagonal slopes down
+ { 241, -83 }, // diagonal slopes down
+ { 168, -193 }, // diagonal slopes down
+ { 236, -97 }, // inverting transition slopes down
+ { 195, -165 }, // inverting transition slopes down
+ { 134, -217 }, // inverting transition slopes down
+ { 252, 44 }, // spiral lift hill up
+ };
+ static_assert(std::size(PitchToDirectionVectorFromGeometry) == NumVehiclePitches);
+
+ constexpr int32_t ComputeHorizontalMagnitude(int32_t length, uint8_t pitch)
+ {
+ return (-PitchToDirectionVectorFromGeometry[static_cast(pitch)].y * length) / 256;
+ }
+
+ constexpr CoordsXY ComputeXYVector(int32_t magnitude, uint8_t yaw)
+ {
+ return (static_cast(YawToDirectionVector[yaw]) * magnitude) / 256;
+ }
+
+ constexpr CoordsXY ComputeXYVector(int32_t length, uint8_t pitch, uint8_t yaw)
+ {
+ return ComputeXYVector(ComputeHorizontalMagnitude(length, pitch), yaw);
+ }
+} // namespace OpenRCT2::Math::Trigonometry
diff --git a/src/openrct2/network/NetworkBase.cpp b/src/openrct2/network/NetworkBase.cpp
index 4a685f42f2..35a13838b1 100644
--- a/src/openrct2/network/NetworkBase.cpp
+++ b/src/openrct2/network/NetworkBase.cpp
@@ -43,7 +43,7 @@
// It is used for making sure only compatible builds get connected, even within
// single OpenRCT2 version.
-#define NETWORK_STREAM_VERSION "11"
+#define NETWORK_STREAM_VERSION "12"
#define NETWORK_STREAM_ID OPENRCT2_VERSION "-" NETWORK_STREAM_VERSION
diff --git a/src/openrct2/object/RideObject.cpp b/src/openrct2/object/RideObject.cpp
index 0f45555a73..305e64fa5b 100644
--- a/src/openrct2/object/RideObject.cpp
+++ b/src/openrct2/object/RideObject.cpp
@@ -43,6 +43,56 @@ static const uint8_t SpriteGroupMultiplier[EnumValue(SpriteGroupType::Count)] =
1, 2, 2, 2, 2, 2, 2, 10, 1, 2, 2, 2, 2, 2, 2, 2, 6, 4, 4, 4, 4, 4, 4, 4, 12, 4, 4, 4, 4, 4, 20, 3, 1,
};
+constexpr const uint8_t DefaultSteamSpawnPosition[] = { 11, 22 };
+
+static const EnumMap AnimationNameLookup{
+ { "none", CarEntryAnimation::None },
+ { "simpleVehicle", CarEntryAnimation::SimpleVehicle },
+ { "steamLocomotive", CarEntryAnimation::SteamLocomotive },
+ { "swanBoat", CarEntryAnimation::SwanBoat },
+ { "monorailCycle", CarEntryAnimation::MonorailCycle },
+ { "MultiDimension", CarEntryAnimation::MultiDimension },
+ { "observationTower", CarEntryAnimation::ObservationTower },
+ { "animalFlying", CarEntryAnimation::AnimalFlying },
+};
+
+constexpr const auto NumLegacyAnimationTypes = 11;
+
+struct LegacyAnimationParameters
+{
+ uint16_t Speed;
+ uint8_t NumFrames;
+ CarEntryAnimation Alias;
+};
+
+constexpr const LegacyAnimationParameters VehicleEntryDefaultAnimation[] = {
+ { 0, 1, CarEntryAnimation::None }, // None
+ { 1 << 12, 4, CarEntryAnimation::SteamLocomotive }, // Miniature Railway Locomotive
+ { 1 << 10, 2, CarEntryAnimation::SwanBoat }, // Swan Boat
+ { 1 << 11, 6, CarEntryAnimation::SimpleVehicle }, // Canoe
+ { 1 << 11, 7, CarEntryAnimation::SimpleVehicle }, // Rowboat
+ { 1 << 10, 2, CarEntryAnimation::SimpleVehicle }, // Water Tricycle
+ { 0x3333, 8, CarEntryAnimation::ObservationTower }, // Observation Tower
+ { 1 << 10, 4, CarEntryAnimation::SimpleVehicle }, // Mini Helicopter
+ { 1 << 11, 4, CarEntryAnimation::MonorailCycle }, // Monorail Cycle
+ { 0x3333, 8, CarEntryAnimation::MultiDimension }, // Multi Dimension Coaster
+ { 24, 4, CarEntryAnimation::AnimalFlying }, // Animal Flying
+};
+static_assert(std::size(VehicleEntryDefaultAnimation) == NumLegacyAnimationTypes);
+
+static CarEntryAnimation GetAnimationTypeFromString(const std::string& s)
+{
+ auto result = AnimationNameLookup.find(s);
+ return (result != AnimationNameLookup.end()) ? result->second : CarEntryAnimation::None;
+}
+
+static LegacyAnimationParameters GetDefaultAnimationParameters(uint8_t legacyAnimationType)
+{
+ if (legacyAnimationType >= NumLegacyAnimationTypes)
+ return VehicleEntryDefaultAnimation[0];
+ return VehicleEntryDefaultAnimation[legacyAnimationType];
+}
+
static constexpr SpritePrecision PrecisionFromNumFrames(uint8_t numRotationFrames)
{
if (numRotationFrames == 0)
@@ -240,7 +290,7 @@ void RideObject::Load()
carEntry.NumCarImages = imageIndex - currentCarImagesOffset;
- // Move the offset over this car’s images. Including peeps
+ // Move the offset over this car's images. Including peeps
currentCarImagesOffset = imageIndex + carEntry.no_seating_rows * carEntry.NumCarImages;
// 0x6DEB0D
@@ -350,7 +400,7 @@ void RideObject::ReadLegacyCar([[maybe_unused]] IReadObjectContext* context, ISt
car->sprite_width = stream->ReadValue();
car->sprite_height_negative = stream->ReadValue();
car->sprite_height_positive = stream->ReadValue();
- car->animation = stream->ReadValue();
+ auto legacyAnimation = stream->ReadValue();
car->flags = stream->ReadValue();
car->base_num_frames = stream->ReadValue();
stream->Seek(15 * 4, STREAM_SEEK_CURRENT);
@@ -368,6 +418,14 @@ void RideObject::ReadLegacyCar([[maybe_unused]] IReadObjectContext* context, ISt
car->draw_order = stream->ReadValue();
car->num_vertical_frames_override = stream->ReadValue();
stream->Seek(4, STREAM_SEEK_CURRENT);
+
+ // OpenRCT2-specific features below
+ auto animationProperties = GetDefaultAnimationParameters(legacyAnimation);
+ car->animation = animationProperties.Alias;
+ car->AnimationSpeed = animationProperties.Speed;
+ car->AnimationFrames = animationProperties.NumFrames;
+ car->SteamEffect.Longitudinal = DefaultSteamSpawnPosition[0];
+ car->SteamEffect.Vertical = DefaultSteamSpawnPosition[1];
ReadLegacySpriteGroups(car, spriteGroups);
}
@@ -383,8 +441,8 @@ uint8_t RideObject::CalculateNumVerticalFrames(const CarEntry& carEntry)
{
if (!(carEntry.flags & CAR_ENTRY_FLAG_SPINNING_ADDITIONAL_FRAMES))
{
- if (carEntry.flags & CAR_ENTRY_FLAG_VEHICLE_ANIMATION
- && carEntry.animation != CAR_ENTRY_ANIMATION_OBSERVATION_TOWER)
+ if ((carEntry.flags & CAR_ENTRY_FLAG_VEHICLE_ANIMATION)
+ && carEntry.animation != CarEntryAnimation::ObservationTower)
{
if (!(carEntry.flags & CAR_ENTRY_FLAG_DODGEM_INUSE_LIGHTS))
{
@@ -647,7 +705,6 @@ CarEntry RideObject::ReadJsonCar([[maybe_unused]] IReadObjectContext* context, j
car.sprite_width = Json::GetNumber(jCar["spriteWidth"]);
car.sprite_height_negative = Json::GetNumber(jCar["spriteHeightNegative"]);
car.sprite_height_positive = Json::GetNumber(jCar["spriteHeightPositive"]);
- car.animation = Json::GetNumber(jCar["animation"]);
car.base_num_frames = Json::GetNumber(jCar["baseNumFrames"]);
car.NumCarImages = Json::GetNumber(jCar["numImages"]);
car.no_seating_rows = Json::GetNumber(jCar["numSeatRows"]);
@@ -664,6 +721,38 @@ CarEntry RideObject::ReadJsonCar([[maybe_unused]] IReadObjectContext* context, j
car.draw_order = Json::GetNumber(jCar["drawOrder"]);
car.num_vertical_frames_override = Json::GetNumber(jCar["numVerticalFramesOverride"]);
+ auto jAnimation = jCar["animation"];
+ if (jAnimation.is_object())
+ {
+ car.animation = GetAnimationTypeFromString(Json::GetString(jAnimation["animationType"]));
+ car.AnimationSpeed = Json::GetNumber(jAnimation["animationSpeed"]);
+ car.AnimationFrames = Json::GetNumber(jAnimation["animationFrames"]);
+ }
+ else
+ {
+ auto animationProperties = GetDefaultAnimationParameters(Json::GetNumber(jAnimation));
+ car.animation = animationProperties.Alias;
+ car.AnimationSpeed = animationProperties.Speed;
+ car.AnimationFrames = animationProperties.NumFrames;
+
+ if (!jCar["animationSpeed"].is_null())
+ car.AnimationSpeed = Json::GetNumber(jCar["animationSpeed"]);
+ if (!jCar["animationFrames"].is_null())
+ car.AnimationFrames = Json::GetNumber(jCar["animationFrames"]);
+ }
+
+ auto jSteamTranslation = jCar["steamPosition"];
+ if (jSteamTranslation.is_object())
+ {
+ car.SteamEffect.Longitudinal = Json::GetNumber(jSteamTranslation["longitudinal"], DefaultSteamSpawnPosition[0]);
+ car.SteamEffect.Vertical = Json::GetNumber(jSteamTranslation["vertical"], DefaultSteamSpawnPosition[1]);
+ }
+ else
+ {
+ car.SteamEffect.Longitudinal = DefaultSteamSpawnPosition[0];
+ car.SteamEffect.Vertical = DefaultSteamSpawnPosition[1];
+ }
+
auto jLoadingPositions = jCar["loadingPositions"];
if (jLoadingPositions.is_array())
{
diff --git a/src/openrct2/ride/CarEntry.h b/src/openrct2/ride/CarEntry.h
index c8b31a31bc..eda82c121f 100644
--- a/src/openrct2/ride/CarEntry.h
+++ b/src/openrct2/ride/CarEntry.h
@@ -23,6 +23,19 @@ namespace OpenRCT2::Audio
enum class SoundId : uint8_t;
}
+enum class CarEntryAnimation : uint8_t
+{
+ None = 0,
+ SimpleVehicle,
+ SteamLocomotive,
+ SwanBoat,
+ MonorailCycle,
+ MultiDimension,
+ ObservationTower,
+ AnimalFlying,
+ Count,
+};
+
enum : uint32_t
{
CAR_ENTRY_FLAG_POWERED_RIDE_UNRESTRICTED_GRAVITY = 1
@@ -166,7 +179,7 @@ struct CarEntry
uint8_t sprite_width;
uint8_t sprite_height_negative;
uint8_t sprite_height_positive;
- uint8_t animation;
+ CarEntryAnimation animation;
uint32_t flags;
uint16_t base_num_frames; // The number of sprites of animation or swinging per rotation frame
uint32_t base_image_id;
@@ -187,6 +200,13 @@ struct CarEntry
uint8_t num_vertical_frames_override; // A custom number that can be used rather than letting RCT2 determine it.
// Needs the CAR_ENTRY_FLAG_OVERRIDE_NUM_VERTICAL_FRAMES flag to be set.
uint8_t peep_loading_waypoint_segments;
+ uint16_t AnimationSpeed;
+ uint8_t AnimationFrames;
+ struct
+ {
+ int8_t Longitudinal;
+ int8_t Vertical;
+ } SteamEffect;
std::vector> peep_loading_waypoints = {};
std::vector peep_loading_positions = {};
diff --git a/src/openrct2/ride/RideData.cpp b/src/openrct2/ride/RideData.cpp
index 266ec56172..1a330e901e 100644
--- a/src/openrct2/ride/RideData.cpp
+++ b/src/openrct2/ride/RideData.cpp
@@ -133,7 +133,7 @@ const CarEntry CableLiftVehicle = {
/* .sprite_width = */ 0,
/* .sprite_height_negative = */ 0,
/* .sprite_height_positive = */ 0,
- /* .animation = */ 0,
+ /* .animation = */ CarEntryAnimation::None,
/* .flags = */ 0,
/* .base_num_frames = */ 1,
/* .base_image_id = */ 29110,
@@ -184,7 +184,11 @@ const CarEntry CableLiftVehicle = {
/* .effect_visual = */ 1,
/* .draw_order = */ 14,
/* .num_vertical_frames_override = */ 0,
- /* .peep_loading_positions = */ 0
+ /* .peep_loading_positions = */ 0,
+ /* .AnimationExponent = */ 0,
+ /* .AnimationFrames = */ 0,
+ /* .SteamEffectType.longitudinal = */ 0,
+ /* .SteamEffectType.vertical = */ 0
};
/* rct2: 0x009A0AA0 */
diff --git a/src/openrct2/ride/Vehicle.cpp b/src/openrct2/ride/Vehicle.cpp
index 54d408539d..1dd7cf3606 100644
--- a/src/openrct2/ride/Vehicle.cpp
+++ b/src/openrct2/ride/Vehicle.cpp
@@ -26,6 +26,7 @@
#include "../localisation/Formatter.h"
#include "../localisation/Localisation.h"
#include "../management/NewsItem.h"
+#include "../math/Trigonometry.hpp"
#include "../object/SmallSceneryEntry.h"
#include "../platform/Platform.h"
#include "../profiling/Profiling.h"
@@ -56,6 +57,7 @@
using namespace OpenRCT2::Audio;
using namespace OpenRCT2::TrackMetaData;
+using namespace OpenRCT2::Math::Trigonometry;
static bool vehicle_boat_is_location_accessible(const CoordsXYZ& location);
constexpr int16_t VEHICLE_MAX_SPIN_SPEED = 1536;
@@ -435,66 +437,6 @@ static constexpr const OpenRCT2::Audio::SoundId DoorCloseSoundIds[] = {
OpenRCT2::Audio::SoundId::Portcullis,
};
-static const struct
-{
- int8_t x, y, z;
-} SteamParticleOffsets[][16] = {
- {
- { -11, 0, 22 },
- { -10, 4, 22 },
- { -8, 8, 22 },
- { -4, 10, 22 },
- { 0, 11, 22 },
- { 4, 10, 22 },
- { 8, 8, 22 },
- { 10, 4, 22 },
- { 11, 0, 22 },
- { 10, -4, 22 },
- { 8, -8, 22 },
- { 4, -10, 22 },
- { 0, -11, 22 },
- { -4, -10, 22 },
- { -8, -8, 22 },
- { -10, -4, 22 },
- },
- {
- { -9, 0, 27 },
- { -8, 4, 27 },
- { -6, 6, 27 },
- { -4, 8, 27 },
- { 0, 9, 27 },
- { 4, 8, 27 },
- { 6, 6, 27 },
- { 8, 4, 27 },
- { 9, 0, 27 },
- { 8, -4, 27 },
- { 6, -6, 27 },
- { 4, -8, 27 },
- { 0, -9, 27 },
- { -4, -8, 27 },
- { -6, -6, 27 },
- { -8, -4, 27 },
- },
- {
- { -13, 0, 18 },
- { -12, 4, 17 },
- { -9, 9, 17 },
- { -4, 8, 17 },
- { 0, 13, 18 },
- { 4, 8, 17 },
- { 6, 6, 17 },
- { 8, 4, 17 },
- { 13, 0, 18 },
- { 8, -4, 17 },
- { 6, -6, 17 },
- { 4, -8, 17 },
- { 0, -13, 18 },
- { -4, -8, 17 },
- { -6, -6, 17 },
- { -8, -4, 17 },
- },
-};
-
template<> bool EntityBase::Is() const
{
return Type == EntityType::Vehicle;
@@ -1350,23 +1292,23 @@ bool Vehicle::OpenRestraints()
continue;
}
}
- if (carEntry.animation == CAR_ENTRY_ANIMATION_OBSERVATION_TOWER && vehicle->animation_frame != 0)
+ if (carEntry.animation == CarEntryAnimation::ObservationTower && vehicle->animation_frame != 0)
{
if (vehicle->animationState <= 0xCCCC)
{
- vehicle->animationState += 0x3333;
+ vehicle->animationState += carEntry.AnimationSpeed;
}
else
{
vehicle->animationState = 0;
vehicle->animation_frame++;
- vehicle->animation_frame &= 7;
+ vehicle->animation_frame %= carEntry.AnimationFrames;
vehicle->Invalidate();
}
restraintsOpen = false;
continue;
}
- if (carEntry.animation == CAR_ENTRY_ANIMATION_ANIMAL_FLYING
+ if (carEntry.animation == CarEntryAnimation::AnimalFlying
&& (vehicle->animation_frame != 0 || vehicle->animationState > 0))
{
vehicle->UpdateAnimationAnimalFlying();
@@ -6637,164 +6579,211 @@ void Vehicle::UpdateAnimationAnimalFlying()
animationState = frameWaitTimes[animation_frame];
}
+/**
+ * Get the frame of animation for the current animationState based on animation speed and animation frames
+ */
+static uint8_t GetTargetFrame(const CarEntry& carEntry, uint32_t animationState)
+{
+ if (carEntry.AnimationSpeed == 0)
+ return 0;
+ auto targetFrame = animationState / (carEntry.AnimationSpeed << 2);
+ // mask of 0xFF
+ targetFrame &= std::numeric_limits::max();
+ // multiply by number of frames. After the bitshift 8, the range will be 0 to AnimationFrames - 1
+ targetFrame *= carEntry.AnimationFrames;
+ return targetFrame >> std::numeric_limits::digits;
+}
+
+/**
+ * Compute the position that steam should be spawned
+ */
+static constexpr CoordsXYZ ComputeSteamOffset(int32_t height, int32_t length, uint8_t pitch, uint8_t yaw)
+{
+ uint8_t trueYaw = OpenRCT2::Entity::Yaw::YawTo64(yaw);
+ auto offsets = PitchToDirectionVectorFromGeometry[pitch];
+ int32_t projectedRun = (offsets.x * length - offsets.y * height) / 256;
+ int32_t projectedHeight = (offsets.x * height + offsets.y * length) / 256;
+ return { ComputeXYVector(projectedRun, trueYaw), projectedHeight };
+}
+
+/**
+ * Decide based on current frame and number of frames if a steam particle should be generated on this frame
+ */
+static bool ShouldMakeSteam(uint8_t targetFrame, uint8_t animationFrames)
+{
+ if (animationFrames < 1)
+ return false;
+ // steam is produced twice per wheel revolution
+ return targetFrame == 0 || targetFrame == animationFrames / 2;
+}
+
+/**
+ * Dummy function
+ */
+static void AnimateNone(Vehicle& vehicle, const CarEntry& carEntry)
+{
+ return;
+}
+
+/**
+ * Animate the vehicle based on its speed
+ */
+static void AnimateSimpleVehicle(Vehicle& vehicle, const CarEntry& carEntry)
+{
+ vehicle.animationState += _vehicleVelocityF64E08;
+ uint8_t targetFrame = GetTargetFrame(carEntry, vehicle.animationState);
+ if (vehicle.animation_frame != targetFrame)
+ {
+ vehicle.animation_frame = targetFrame;
+ vehicle.Invalidate();
+ }
+}
+
+/**
+ * Animate the vehicle based on its speed plus add steam particles
+ */
+static void AnimateSteamLocomotive(Vehicle& vehicle, const CarEntry& carEntry)
+{
+ vehicle.animationState += _vehicleVelocityF64E08;
+ uint8_t targetFrame = GetTargetFrame(carEntry, vehicle.animationState);
+ if (vehicle.animation_frame != targetFrame)
+ {
+ vehicle.animation_frame = targetFrame;
+ if (ShouldMakeSteam(targetFrame, carEntry.AnimationFrames))
+ {
+ auto curRide = vehicle.GetRide();
+ if (curRide != nullptr)
+ {
+ if (!RideHasStationShelter(*curRide)
+ || (vehicle.status != Vehicle::Status::MovingToEndOfStation && vehicle.status != Vehicle::Status::Arriving))
+ {
+ CoordsXYZ steamOffset = ComputeSteamOffset(
+ carEntry.SteamEffect.Vertical, carEntry.SteamEffect.Longitudinal, vehicle.Pitch,
+ vehicle.sprite_direction);
+ SteamParticle::Create(CoordsXYZ(vehicle.x, vehicle.y, vehicle.z) + steamOffset);
+ }
+ }
+ }
+ vehicle.Invalidate();
+ }
+}
+
+/**
+ * Animate the vehicle based on its speed. Specialized animation with exactly 2 frames due to how peep animation works.
+ */
+static void AnimateSwanBoat(Vehicle& vehicle, const CarEntry& carEntry)
+{
+ // The animation of swan boats places frames at 0 and 2 instead of 0 and 1 like Water Tricycles due to the second
+ // pair of peeps. The animation technically uses 4 frames, but ignores frames 1 and 3.
+ vehicle.animationState += _vehicleVelocityF64E08;
+ uint8_t targetFrame = GetTargetFrame(carEntry, vehicle.animationState) * 2;
+ if (vehicle.animation_frame != targetFrame)
+ {
+ vehicle.animation_frame = targetFrame;
+ vehicle.Invalidate();
+ }
+}
+
+/**
+ * Monorail Cycle animation only animates when a peep is present
+ */
+static void AnimateMonorailCycle(Vehicle& vehicle, const CarEntry& carEntry)
+{
+ if (vehicle.num_peeps != 0)
+ {
+ AnimateSimpleVehicle(vehicle, carEntry);
+ }
+}
+
+/**
+ * Observation tower animates at a constant speed continuously
+ */
+static void AnimateObservationTower(Vehicle& vehicle, const CarEntry& carEntry)
+{
+ if (vehicle.animationState <= 0xCCCC)
+ {
+ vehicle.animationState += carEntry.AnimationSpeed;
+ }
+ else
+ {
+ vehicle.animationState = 0;
+ vehicle.animation_frame += 1;
+ vehicle.animation_frame %= carEntry.AnimationFrames;
+ vehicle.Invalidate();
+ }
+}
+/**
+ * seatRotation value of 4 translates to animationFrame value of 0. This function makes that true for any number of animation
+ * frames
+ */
+static int16_t MultiDimensionTargetAngle(int16_t seatRotation, int16_t animationFrames)
+{
+ return ((seatRotation - 4) % animationFrames + animationFrames) % animationFrames;
+}
+
+/**
+ * Multidimension targets a specific animation frame based on track
+ */
+static void AnimateMultiDimension(Vehicle& vehicle, const CarEntry& carEntry)
+{
+ if (vehicle.seat_rotation != vehicle.target_seat_rotation)
+ {
+ if (vehicle.animationState <= 0xCCCC)
+ {
+ vehicle.animationState += carEntry.AnimationSpeed;
+ }
+ else
+ {
+ vehicle.animationState = 0;
+
+ if (vehicle.seat_rotation >= vehicle.target_seat_rotation)
+ vehicle.seat_rotation--;
+ else
+ vehicle.seat_rotation++;
+
+ int16_t targetSeatRotation = MultiDimensionTargetAngle(vehicle.seat_rotation, carEntry.AnimationFrames);
+ if (targetSeatRotation != vehicle.animation_frame)
+ {
+ vehicle.animation_frame = targetSeatRotation;
+ vehicle.Invalidate();
+ }
+ }
+ }
+}
+
+/**
+ * Animal Flying animates only on chainlift and in an unusual way. Made by Spacek531
+ */
+static void AnimateAnimalFlying(Vehicle& vehicle, const CarEntry& carEntry)
+{
+ vehicle.UpdateAnimationAnimalFlying();
+ // makes animation play faster with vehicle speed
+ uint8_t targetFrame = abs(_vehicleVelocityF64E08) >> carEntry.AnimationSpeed;
+ vehicle.animationState = std::max(vehicle.animationState - targetFrame, 0u);
+}
+
+using AnimateFunction = void (*)(Vehicle& vehicle, const CarEntry& carEntry);
+
+constexpr static const AnimateFunction AnimationFunctions[]{
+ AnimateNone, AnimateSimpleVehicle, AnimateSteamLocomotive, AnimateSwanBoat,
+ AnimateMonorailCycle, AnimateMultiDimension, AnimateObservationTower, AnimateAnimalFlying,
+};
+static_assert(std::size(AnimationFunctions) == EnumValue(CarEntryAnimation::Count));
+
/**
*
* rct2: 0x006D63D4
*/
void Vehicle::UpdateAdditionalAnimation()
{
- uint8_t targetFrame{};
- uint8_t curFrame{};
- uint32_t eax{};
-
auto carEntry = Entry();
if (carEntry == nullptr)
{
return;
}
- switch (carEntry->animation)
- {
- case CAR_ENTRY_ANIMATION_MINITURE_RAILWAY_LOCOMOTIVE: // Loc6D652B
- animationState += _vehicleVelocityF64E08;
- targetFrame = (animationState >> 20) & 3;
- if (animation_frame != targetFrame)
- {
- curFrame = animation_frame;
- animation_frame = targetFrame;
- targetFrame &= 0x02;
- curFrame &= 0x02;
- if (targetFrame != curFrame)
- {
- auto curRide = GetRide();
- if (curRide != nullptr)
- {
- if (!RideHasStationShelter(*curRide)
- || (status != Vehicle::Status::MovingToEndOfStation && status != Vehicle::Status::Arriving))
- {
- int32_t typeIndex = [&] {
- switch (Pitch)
- {
- case 2:
- // uphill
- return 1;
- case 6:
- // downhill
- return 2;
- default:
- return 0;
- }
- }();
- int32_t directionIndex = sprite_direction >> 1;
- auto offset = SteamParticleOffsets[typeIndex][directionIndex];
- SteamParticle::Create({ x + offset.x, y + offset.y, z + offset.z });
- }
- }
- }
- Invalidate();
- }
- break;
- case CAR_ENTRY_ANIMATION_SWAN: // Loc6D6424
- animationState += _vehicleVelocityF64E08;
- targetFrame = (animationState >> 18) & 2;
- if (animation_frame != targetFrame)
- {
- animation_frame = targetFrame;
- Invalidate();
- }
- break;
- case CAR_ENTRY_ANIMATION_CANOES: // Loc6D6482
- animationState += _vehicleVelocityF64E08;
- eax = ((animationState >> 13) & 0xFF) * 6;
- targetFrame = (eax >> 8) & 0xFF;
- if (animation_frame != targetFrame)
- {
- animation_frame = targetFrame;
- Invalidate();
- }
- break;
- case CAR_ENTRY_ANIMATION_ROW_BOATS: // Loc6D64F7
- animationState += _vehicleVelocityF64E08;
- eax = ((animationState >> 13) & 0xFF) * 7;
- targetFrame = (eax >> 8) & 0xFF;
- if (animation_frame != targetFrame)
- {
- animation_frame = targetFrame;
- Invalidate();
- }
- break;
- case CAR_ENTRY_ANIMATION_WATER_TRICYCLES: // Loc6D6453
- animationState += _vehicleVelocityF64E08;
- targetFrame = (animationState >> 19) & 1;
- if (animation_frame != targetFrame)
- {
- animation_frame = targetFrame;
- Invalidate();
- }
- break;
- case CAR_ENTRY_ANIMATION_OBSERVATION_TOWER: // Loc6D65C3
- if (animationState <= 0xCCCC)
- {
- animationState += 0x3333;
- }
- else
- {
- animationState = 0;
- animation_frame += 1;
- animation_frame &= 7;
- Invalidate();
- }
- break;
- case CAR_ENTRY_ANIMATION_HELICARS: // Loc6D63F5
- animationState += _vehicleVelocityF64E08;
- targetFrame = (animationState >> 18) & 3;
- if (animation_frame != targetFrame)
- {
- animation_frame = targetFrame;
- Invalidate();
- }
- break;
- case CAR_ENTRY_ANIMATION_MONORAIL_CYCLES: // Loc6D64B6
- if (num_peeps != 0)
- {
- animationState += _vehicleVelocityF64E08;
- eax = ((animationState >> 13) & 0xFF) << 2;
- targetFrame = (eax >> 8) & 0xFF;
- if (animation_frame != targetFrame)
- {
- animation_frame = targetFrame;
- Invalidate();
- }
- }
- break;
- case CAR_ENTRY_ANIMATION_MULTI_DIM_COASTER: // Loc6D65E1
- if (seat_rotation != target_seat_rotation)
- {
- if (animationState <= 0xCCCC)
- {
- animationState += 0x3333;
- }
- else
- {
- animationState = 0;
-
- if (seat_rotation >= target_seat_rotation)
- seat_rotation--;
-
- else
- seat_rotation++;
-
- animation_frame = (seat_rotation - 4) & 7;
- Invalidate();
- }
- }
- break;
- case CAR_ENTRY_ANIMATION_ANIMAL_FLYING:
- UpdateAnimationAnimalFlying();
- // makes animation play faster with vehicle speed
- targetFrame = abs(_vehicleVelocityF64E08) >> 24;
- animationState = std::max(animationState - targetFrame, 0u);
- break;
- }
+ if (carEntry->AnimationFrames == 0 || carEntry->animation >= CarEntryAnimation::Count)
+ return;
+ AnimationFunctions[EnumValue(carEntry->animation)](*this, *carEntry);
}
/**
diff --git a/src/openrct2/ride/Vehicle.h b/src/openrct2/ride/Vehicle.h
index d46a8deb76..55fc833ecf 100644
--- a/src/openrct2/ride/Vehicle.h
+++ b/src/openrct2/ride/Vehicle.h
@@ -38,6 +38,9 @@ struct GForces
int32_t LateralG{};
};
+// How many valid pitch values are currently in the game. Eventually pitch will be enumerated.
+constexpr const uint8_t NumVehiclePitches = 60;
+
// Size: 0x09
struct VehicleInfo
{
@@ -230,6 +233,7 @@ struct Vehicle : EntityBase
Ride* GetRide() const;
Vehicle* TrainHead() const;
Vehicle* TrainTail() const;
+ void UpdateAnimationAnimalFlying();
void EnableCollisionsForTrain();
/**
* Instantly moves the specific car forward or backwards along the track.
@@ -328,7 +332,6 @@ private:
void UpdateCrashSetup();
void UpdateCollisionSetup();
int32_t UpdateMotionDodgems();
- void UpdateAnimationAnimalFlying();
void UpdateAdditionalAnimation();
void CheckIfMissing();
bool CurrentTowerElementIsTop();
@@ -422,21 +425,6 @@ enum class MiniGolfAnimation : uint8_t
Putt,
};
-enum
-{
- CAR_ENTRY_ANIMATION_NONE,
- CAR_ENTRY_ANIMATION_MINITURE_RAILWAY_LOCOMOTIVE,
- CAR_ENTRY_ANIMATION_SWAN,
- CAR_ENTRY_ANIMATION_CANOES,
- CAR_ENTRY_ANIMATION_ROW_BOATS,
- CAR_ENTRY_ANIMATION_WATER_TRICYCLES,
- CAR_ENTRY_ANIMATION_OBSERVATION_TOWER,
- CAR_ENTRY_ANIMATION_HELICARS,
- CAR_ENTRY_ANIMATION_MONORAIL_CYCLES,
- CAR_ENTRY_ANIMATION_MULTI_DIM_COASTER,
- CAR_ENTRY_ANIMATION_ANIMAL_FLYING // OpenRCT2-specific feature
-};
-
namespace VehicleFlags
{
constexpr uint32_t OnLiftHill = (1 << 0);
diff --git a/src/openrct2/scripting/bindings/object/ScObject.hpp b/src/openrct2/scripting/bindings/object/ScObject.hpp
index 20a50d1250..fdfbb991de 100644
--- a/src/openrct2/scripting/bindings/object/ScObject.hpp
+++ b/src/openrct2/scripting/bindings/object/ScObject.hpp
@@ -274,7 +274,7 @@ namespace OpenRCT2::Scripting
auto entry = GetEntry();
if (entry != nullptr)
{
- return entry->animation;
+ return EnumValue(entry->animation);
}
return 0;
}