mirror of
https://github.com/OpenRCT2/OpenRCT2
synced 2026-01-23 06:44:38 +01:00
2281 lines
84 KiB
C++
2281 lines
84 KiB
C++
/*****************************************************************************
|
|
* Copyright (c) 2014-2025 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.
|
|
*****************************************************************************/
|
|
|
|
#include "RideRatings.h"
|
|
|
|
#include "../Cheats.h"
|
|
#include "../Context.h"
|
|
#include "../GameState.h"
|
|
#include "../OpenRCT2.h"
|
|
#include "../core/UnitConversion.h"
|
|
#include "../interface/Window.h"
|
|
#include "../profiling/Profiling.h"
|
|
#include "../scripting/ScriptEngine.h"
|
|
#include "../world/Map.h"
|
|
#include "../world/tile_element/PathElement.h"
|
|
#include "../world/tile_element/SurfaceElement.h"
|
|
#include "../world/tile_element/TileElement.h"
|
|
#include "../world/tile_element/TrackElement.h"
|
|
#include "Ride.h"
|
|
#include "RideData.h"
|
|
#include "RideManager.hpp"
|
|
#include "Station.h"
|
|
#include "Track.h"
|
|
#include "TrackData.h"
|
|
|
|
#include <iterator>
|
|
|
|
using namespace OpenRCT2;
|
|
using namespace OpenRCT2::Scripting;
|
|
using namespace OpenRCT2::TrackMetaData;
|
|
|
|
enum
|
|
{
|
|
RIDE_RATINGS_STATE_FIND_NEXT_RIDE,
|
|
RIDE_RATINGS_STATE_INITIALISE,
|
|
RIDE_RATINGS_STATE_2,
|
|
RIDE_RATINGS_STATE_CALCULATE,
|
|
RIDE_RATINGS_STATE_4,
|
|
RIDE_RATINGS_STATE_5
|
|
};
|
|
|
|
enum
|
|
{
|
|
PROXIMITY_WATER_OVER, // 0x0138B596
|
|
PROXIMITY_WATER_TOUCH, // 0x0138B598
|
|
PROXIMITY_WATER_LOW, // 0x0138B59A
|
|
PROXIMITY_WATER_HIGH, // 0x0138B59C
|
|
PROXIMITY_SURFACE_TOUCH, // 0x0138B59E
|
|
PROXIMITY_QUEUE_PATH_OVER, // 0x0138B5A0
|
|
PROXIMITY_QUEUE_PATH_TOUCH_ABOVE, // 0x0138B5A2
|
|
PROXIMITY_QUEUE_PATH_TOUCH_UNDER, // 0x0138B5A4
|
|
PROXIMITY_PATH_TOUCH_ABOVE, // 0x0138B5A6
|
|
PROXIMITY_PATH_TOUCH_UNDER, // 0x0138B5A8
|
|
PROXIMITY_OWN_TRACK_TOUCH_ABOVE, // 0x0138B5AA
|
|
PROXIMITY_OWN_TRACK_CLOSE_ABOVE, // 0x0138B5AC
|
|
PROXIMITY_FOREIGN_TRACK_ABOVE_OR_BELOW, // 0x0138B5AE
|
|
PROXIMITY_FOREIGN_TRACK_TOUCH_ABOVE, // 0x0138B5B0
|
|
PROXIMITY_FOREIGN_TRACK_CLOSE_ABOVE, // 0x0138B5B2
|
|
PROXIMITY_SCENERY_SIDE_BELOW, // 0x0138B5B4
|
|
PROXIMITY_SCENERY_SIDE_ABOVE, // 0x0138B5B6
|
|
PROXIMITY_OWN_STATION_TOUCH_ABOVE, // 0x0138B5B8
|
|
PROXIMITY_OWN_STATION_CLOSE_ABOVE, // 0x0138B5BA
|
|
PROXIMITY_TRACK_THROUGH_VERTICAL_LOOP, // 0x0138B5BC
|
|
PROXIMITY_PATH_TROUGH_VERTICAL_LOOP, // 0x0138B5BE
|
|
PROXIMITY_INTERSECTING_VERTICAL_LOOP, // 0x0138B5C0
|
|
PROXIMITY_THROUGH_VERTICAL_LOOP, // 0x0138B5C2
|
|
PROXIMITY_PATH_SIDE_CLOSE, // 0x0138B5C4
|
|
PROXIMITY_FOREIGN_TRACK_SIDE_CLOSE, // 0x0138B5C6
|
|
PROXIMITY_SURFACE_SIDE_CLOSE, // 0x0138B5C8
|
|
PROXIMITY_COUNT
|
|
};
|
|
|
|
struct ShelteredEights
|
|
{
|
|
uint8_t TrackShelteredEighths;
|
|
uint8_t TotalShelteredEighths;
|
|
};
|
|
|
|
// Amount of updates allowed per updating state on the current tick.
|
|
// The total amount would be MaxRideRatingSubSteps * RideRatingMaxUpdateStates which
|
|
// would be currently 80, this is the worst case of sub-steps and may break out earlier.
|
|
static constexpr size_t MaxRideRatingUpdateSubSteps = 20;
|
|
|
|
static void ride_ratings_update_state(RideRatingUpdateState& state);
|
|
static void ride_ratings_update_state_0(RideRatingUpdateState& state);
|
|
static void ride_ratings_update_state_1(RideRatingUpdateState& state);
|
|
static void ride_ratings_update_state_2(RideRatingUpdateState& state);
|
|
static void ride_ratings_update_state_3(RideRatingUpdateState& state);
|
|
static void ride_ratings_update_state_4(RideRatingUpdateState& state);
|
|
static void ride_ratings_update_state_5(RideRatingUpdateState& state);
|
|
static void ride_ratings_begin_proximity_loop(RideRatingUpdateState& state);
|
|
static void RideRatingsCalculate(RideRatingUpdateState& state, Ride& ride);
|
|
static void RideRatingsCalculateValue(Ride& ride);
|
|
static void ride_ratings_score_close_proximity(RideRatingUpdateState& state, TileElement* inputTileElement);
|
|
static void RideRatingsAdd(RatingTuple& ratings, int32_t excitement, int32_t intensity, int32_t nausea);
|
|
|
|
static ShelteredEights GetNumOfShelteredEighths(const Ride& ride);
|
|
static money64 RideComputeUpkeep(RideRatingUpdateState& state, const Ride& ride);
|
|
static void SetUnreliabilityFactor(Ride& ride);
|
|
|
|
static void RideRatingsApplyAdjustments(const Ride& ride, RatingTuple& ratings);
|
|
static void RideRatingsApplyIntensityPenalty(RatingTuple& ratings);
|
|
|
|
static void RideRatingsApplyBonusLength(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusSynchronisation(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusTrainLength(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusMaxSpeed(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusAverageSpeed(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusDuration(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusGForces(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusTurns(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusDrops(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusSheltered(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusRotations(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusOperationOption(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusReversedTrains(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusGoKartRace(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusTowerRide(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusRotoDrop(RatingTuple& ratings, const Ride& ride);
|
|
static void RideRatingsApplyBonusMazeSize(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusBoatHireNoCircuit(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusSlideUnlimitedRides(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusMotionSimulatorMode(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonus3DCinemaMode(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusTopSpinMode(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusReversals(
|
|
RatingTuple& ratings, const Ride& ride, RideRatingUpdateState& state, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusHoles(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusNumTrains(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusDownwardLaunch(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusLaunchedFreefallSpecial(
|
|
RatingTuple& ratings, const Ride& ride, RideRatingUpdateState& state, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusProximity(
|
|
RatingTuple& ratings, const Ride& ride, RideRatingUpdateState& state, RatingsModifier modifier);
|
|
static void RideRatingsApplyBonusScenery(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyRequirementLength(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyRequirementDropHeight(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyRequirementMaxSpeed(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyRequirementNumDrops(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyRequirementNegativeGs(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyRequirementLateralGs(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyRequirementInversions(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyRequirementUnsheltered(
|
|
RatingTuple& ratings, const Ride& ride, uint8_t shelteredEighths, RatingsModifier modifier);
|
|
static void RideRatingsApplyRequirementReversals(
|
|
RatingTuple& ratings, const Ride& ride, RideRatingUpdateState& state, RatingsModifier modifier);
|
|
static void RideRatingsApplyRequirementHoles(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyRequirementStations(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyRequirementSplashdown(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
static void RideRatingsApplyPenaltyLateralGs(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier);
|
|
|
|
void RideRatingResetUpdateStates()
|
|
{
|
|
RideRatingUpdateState nullState{};
|
|
nullState.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
|
|
|
|
auto& updateStates = GetGameState().RideRatingUpdateStates;
|
|
std::fill(updateStates.begin(), updateStates.end(), nullState);
|
|
}
|
|
|
|
/**
|
|
* This is a small hack function to keep calling the ride rating processor until
|
|
* the given ride's ratings have been calculated. Whatever is currently being
|
|
* processed will be overwritten.
|
|
* Only purpose of this function currently is for testing.
|
|
*/
|
|
void RideRatingsUpdateRide(const Ride& ride)
|
|
{
|
|
RideRatingUpdateState state;
|
|
if (ride.status != RideStatus::Closed)
|
|
{
|
|
state.CurrentRide = ride.id;
|
|
state.State = RIDE_RATINGS_STATE_INITIALISE;
|
|
while (state.State != RIDE_RATINGS_STATE_FIND_NEXT_RIDE)
|
|
{
|
|
ride_ratings_update_state(state);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006B5A2A
|
|
*/
|
|
void RideRatingsUpdateAll()
|
|
{
|
|
PROFILED_FUNCTION();
|
|
|
|
if (gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR)
|
|
return;
|
|
|
|
for (auto& updateState : GetGameState().RideRatingUpdateStates)
|
|
{
|
|
for (size_t i = 0; i < MaxRideRatingUpdateSubSteps; ++i)
|
|
{
|
|
ride_ratings_update_state(updateState);
|
|
|
|
// We need to abort the loop if the state machine requested to find the next ride.
|
|
if (updateState.State == RIDE_RATINGS_STATE_FIND_NEXT_RIDE)
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void ride_ratings_update_state(RideRatingUpdateState& state)
|
|
{
|
|
switch (state.State)
|
|
{
|
|
case RIDE_RATINGS_STATE_FIND_NEXT_RIDE:
|
|
ride_ratings_update_state_0(state);
|
|
break;
|
|
case RIDE_RATINGS_STATE_INITIALISE:
|
|
ride_ratings_update_state_1(state);
|
|
break;
|
|
case RIDE_RATINGS_STATE_2:
|
|
ride_ratings_update_state_2(state);
|
|
break;
|
|
case RIDE_RATINGS_STATE_CALCULATE:
|
|
ride_ratings_update_state_3(state);
|
|
break;
|
|
case RIDE_RATINGS_STATE_4:
|
|
ride_ratings_update_state_4(state);
|
|
break;
|
|
case RIDE_RATINGS_STATE_5:
|
|
ride_ratings_update_state_5(state);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static bool RideRatingIsUpdatingRide(RideId id)
|
|
{
|
|
const auto& updateStates = GetGameState().RideRatingUpdateStates;
|
|
return std::any_of(updateStates.begin(), updateStates.end(), [id](auto& state) {
|
|
return state.CurrentRide == id && state.State != RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
|
|
});
|
|
}
|
|
|
|
static bool ShouldSkipRatingCalculation(const Ride& ride)
|
|
{
|
|
// Skip rides that are closed.
|
|
if (ride.status == RideStatus::Closed)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Skip anything that is already updating.
|
|
if (RideRatingIsUpdatingRide(ride.id))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Skip rides that have a fixed rating.
|
|
if (ride.lifecycle_flags & RIDE_LIFECYCLE_FIXED_RATINGS)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
static RideId GetNextRideToUpdate(RideId currentRide)
|
|
{
|
|
auto rm = GetRideManager();
|
|
if (rm.size() == 0)
|
|
{
|
|
return RideId::GetNull();
|
|
}
|
|
|
|
auto it = rm.get(currentRide);
|
|
if (it == rm.end())
|
|
{
|
|
// Start at the beginning, ride is missing.
|
|
it = rm.begin();
|
|
}
|
|
else
|
|
{
|
|
it = std::next(it);
|
|
}
|
|
|
|
// Filter out rides to avoid wasting a tick to find the next ride.
|
|
while (it != rm.end() && ShouldSkipRatingCalculation(*it))
|
|
{
|
|
it++;
|
|
}
|
|
|
|
// If we reached the end of the list we start over,
|
|
// in case the next ride doesn't pass the filter function it will
|
|
// look for the next matching ride in the next tick.
|
|
if (it == rm.end())
|
|
it = rm.begin();
|
|
|
|
return (*it).id;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006B5A5C
|
|
*/
|
|
static void ride_ratings_update_state_0(RideRatingUpdateState& state)
|
|
{
|
|
// It is possible that the current ride being calculated has
|
|
// been removed or due to import invalid. For both, reset
|
|
// ratings and start check at the start
|
|
if (GetRide(state.CurrentRide) == nullptr)
|
|
{
|
|
state.CurrentRide = {};
|
|
}
|
|
|
|
const auto nextRideId = GetNextRideToUpdate(state.CurrentRide);
|
|
const auto* nextRide = GetRide(nextRideId);
|
|
if (nextRide != nullptr && !ShouldSkipRatingCalculation(*nextRide))
|
|
{
|
|
Guard::Assert(!RideRatingIsUpdatingRide(nextRideId));
|
|
state.State = RIDE_RATINGS_STATE_INITIALISE;
|
|
}
|
|
state.CurrentRide = nextRideId;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006B5A94
|
|
*/
|
|
static void ride_ratings_update_state_1(RideRatingUpdateState& state)
|
|
{
|
|
state.ProximityTotal = 0;
|
|
for (int32_t i = 0; i < PROXIMITY_COUNT; i++)
|
|
{
|
|
state.ProximityScores[i] = 0;
|
|
}
|
|
state.AmountOfBrakes = 0;
|
|
state.AmountOfReversers = 0;
|
|
state.State = RIDE_RATINGS_STATE_2;
|
|
state.StationFlags = 0;
|
|
ride_ratings_begin_proximity_loop(state);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006B5C66
|
|
*/
|
|
static void ride_ratings_update_state_2(RideRatingUpdateState& state)
|
|
{
|
|
const RideId rideIndex = state.CurrentRide;
|
|
auto ride = GetRide(rideIndex);
|
|
if (ride == nullptr || ride->status == RideStatus::Closed || ride->type >= RIDE_TYPE_COUNT)
|
|
{
|
|
state.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
|
|
return;
|
|
}
|
|
|
|
auto loc = state.Proximity;
|
|
OpenRCT2::TrackElemType trackType = state.ProximityTrackType;
|
|
|
|
TileElement* tileElement = MapGetFirstElementAt(loc);
|
|
if (tileElement == nullptr)
|
|
{
|
|
state.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
|
|
return;
|
|
}
|
|
do
|
|
{
|
|
if (tileElement->IsGhost())
|
|
continue;
|
|
if (tileElement->GetType() != TileElementType::Track)
|
|
continue;
|
|
if (tileElement->GetBaseZ() != loc.z)
|
|
continue;
|
|
if (tileElement->AsTrack()->GetRideIndex() != ride->id)
|
|
{
|
|
// Only check that the track belongs to the same ride if ride does not have buildable track
|
|
if (!ride->GetRideTypeDescriptor().HasFlag(RtdFlag::hasTrack))
|
|
continue;
|
|
}
|
|
|
|
if (trackType == TrackElemType::None
|
|
|| (tileElement->AsTrack()->GetSequenceIndex() == 0 && trackType == tileElement->AsTrack()->GetTrackType()))
|
|
{
|
|
if (trackType == TrackElemType::EndStation)
|
|
{
|
|
auto entranceIndex = tileElement->AsTrack()->GetStationIndex();
|
|
state.StationFlags &= ~RIDE_RATING_STATION_FLAG_NO_ENTRANCE;
|
|
if (ride->GetStation(entranceIndex).Entrance.IsNull())
|
|
{
|
|
state.StationFlags |= RIDE_RATING_STATION_FLAG_NO_ENTRANCE;
|
|
}
|
|
}
|
|
|
|
ride_ratings_score_close_proximity(state, tileElement);
|
|
|
|
CoordsXYE trackElement = { state.Proximity, tileElement };
|
|
CoordsXYE nextTrackElement;
|
|
if (!TrackBlockGetNext(&trackElement, &nextTrackElement, nullptr, nullptr))
|
|
{
|
|
state.State = RIDE_RATINGS_STATE_4;
|
|
return;
|
|
}
|
|
|
|
loc = { nextTrackElement, nextTrackElement.element->GetBaseZ() };
|
|
tileElement = nextTrackElement.element;
|
|
if (loc == state.ProximityStart)
|
|
{
|
|
state.State = RIDE_RATINGS_STATE_CALCULATE;
|
|
return;
|
|
}
|
|
state.Proximity = loc;
|
|
state.ProximityTrackType = tileElement->AsTrack()->GetTrackType();
|
|
return;
|
|
}
|
|
} while (!(tileElement++)->IsLastForTile());
|
|
|
|
state.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006B5E4D
|
|
*/
|
|
static void ride_ratings_update_state_3(RideRatingUpdateState& state)
|
|
{
|
|
auto ride = GetRide(state.CurrentRide);
|
|
if (ride == nullptr || ride->status == RideStatus::Closed)
|
|
{
|
|
state.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
|
|
return;
|
|
}
|
|
|
|
RideRatingsCalculate(state, *ride);
|
|
RideRatingsCalculateValue(*ride);
|
|
|
|
WindowInvalidateByNumber(WindowClass::Ride, state.CurrentRide.ToUnderlying());
|
|
state.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006B5BAB
|
|
*/
|
|
static void ride_ratings_update_state_4(RideRatingUpdateState& state)
|
|
{
|
|
state.State = RIDE_RATINGS_STATE_5;
|
|
ride_ratings_begin_proximity_loop(state);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006B5D72
|
|
*/
|
|
static void ride_ratings_update_state_5(RideRatingUpdateState& state)
|
|
{
|
|
auto ride = GetRide(state.CurrentRide);
|
|
if (ride == nullptr || ride->status == RideStatus::Closed)
|
|
{
|
|
state.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
|
|
return;
|
|
}
|
|
|
|
auto loc = state.Proximity;
|
|
OpenRCT2::TrackElemType trackType = state.ProximityTrackType;
|
|
|
|
TileElement* tileElement = MapGetFirstElementAt(loc);
|
|
if (tileElement == nullptr)
|
|
{
|
|
state.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
|
|
return;
|
|
}
|
|
do
|
|
{
|
|
if (tileElement->IsGhost())
|
|
continue;
|
|
if (tileElement->GetType() != TileElementType::Track)
|
|
continue;
|
|
if (tileElement->GetBaseZ() != loc.z)
|
|
continue;
|
|
if (tileElement->AsTrack()->GetRideIndex() != ride->id)
|
|
{
|
|
// Only check that the track belongs to the same ride if ride does not have buildable track
|
|
if (!ride->GetRideTypeDescriptor().HasFlag(RtdFlag::hasTrack))
|
|
continue;
|
|
}
|
|
|
|
if (trackType == TrackElemType::None || trackType == tileElement->AsTrack()->GetTrackType())
|
|
{
|
|
ride_ratings_score_close_proximity(state, tileElement);
|
|
|
|
TrackBeginEnd trackBeginEnd;
|
|
if (!TrackBlockGetPrevious({ state.Proximity, tileElement }, &trackBeginEnd))
|
|
{
|
|
state.State = RIDE_RATINGS_STATE_CALCULATE;
|
|
return;
|
|
}
|
|
|
|
loc.x = trackBeginEnd.begin_x;
|
|
loc.y = trackBeginEnd.begin_y;
|
|
loc.z = trackBeginEnd.begin_z;
|
|
if (loc == state.ProximityStart)
|
|
{
|
|
state.State = RIDE_RATINGS_STATE_CALCULATE;
|
|
return;
|
|
}
|
|
state.Proximity = loc;
|
|
state.ProximityTrackType = trackBeginEnd.begin_element->AsTrack()->GetTrackType();
|
|
return;
|
|
}
|
|
} while (!(tileElement++)->IsLastForTile());
|
|
|
|
state.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006B5BB2
|
|
*/
|
|
static void ride_ratings_begin_proximity_loop(RideRatingUpdateState& state)
|
|
{
|
|
auto ride = GetRide(state.CurrentRide);
|
|
if (ride == nullptr || ride->status == RideStatus::Closed)
|
|
{
|
|
state.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
|
|
return;
|
|
}
|
|
|
|
const auto& rtd = ride->GetRideTypeDescriptor();
|
|
if (rtd.specialType == RtdSpecialType::maze)
|
|
{
|
|
state.State = RIDE_RATINGS_STATE_CALCULATE;
|
|
return;
|
|
}
|
|
|
|
for (auto& station : ride->GetStations())
|
|
{
|
|
if (!station.Start.IsNull())
|
|
{
|
|
state.StationFlags &= ~RIDE_RATING_STATION_FLAG_NO_ENTRANCE;
|
|
if (station.Entrance.IsNull())
|
|
{
|
|
state.StationFlags |= RIDE_RATING_STATION_FLAG_NO_ENTRANCE;
|
|
}
|
|
|
|
auto location = station.GetStart();
|
|
state.Proximity = location;
|
|
state.ProximityTrackType = TrackElemType::None;
|
|
state.ProximityStart = location;
|
|
return;
|
|
}
|
|
}
|
|
|
|
state.State = RIDE_RATINGS_STATE_FIND_NEXT_RIDE;
|
|
}
|
|
|
|
static void proximity_score_increment(RideRatingUpdateState& state, int32_t type)
|
|
{
|
|
state.ProximityScores[type]++;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006B6207
|
|
*/
|
|
static void ride_ratings_score_close_proximity_in_direction(
|
|
RideRatingUpdateState& state, TileElement* inputTileElement, int32_t direction)
|
|
{
|
|
auto scorePos = CoordsXY{ CoordsXY{ state.Proximity } + CoordsDirectionDelta[direction] };
|
|
if (!MapIsLocationValid(scorePos))
|
|
return;
|
|
|
|
TileElement* tileElement = MapGetFirstElementAt(scorePos);
|
|
if (tileElement == nullptr)
|
|
return;
|
|
do
|
|
{
|
|
if (tileElement->IsGhost())
|
|
continue;
|
|
|
|
switch (tileElement->GetType())
|
|
{
|
|
case TileElementType::Surface:
|
|
if (state.ProximityBaseHeight <= inputTileElement->BaseHeight)
|
|
{
|
|
if (inputTileElement->ClearanceHeight <= tileElement->BaseHeight)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_SURFACE_SIDE_CLOSE);
|
|
}
|
|
}
|
|
break;
|
|
case TileElementType::Path:
|
|
if (abs(inputTileElement->GetBaseZ() - tileElement->GetBaseZ()) <= 2 * kCoordsZStep)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_PATH_SIDE_CLOSE);
|
|
}
|
|
break;
|
|
case TileElementType::Track:
|
|
if (inputTileElement->AsTrack()->GetRideIndex() != tileElement->AsTrack()->GetRideIndex())
|
|
{
|
|
if (abs(inputTileElement->GetBaseZ() - tileElement->GetBaseZ()) <= 2 * kCoordsZStep)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_FOREIGN_TRACK_SIDE_CLOSE);
|
|
}
|
|
}
|
|
break;
|
|
case TileElementType::SmallScenery:
|
|
case TileElementType::LargeScenery:
|
|
if (tileElement->GetBaseZ() < inputTileElement->GetClearanceZ())
|
|
{
|
|
if (inputTileElement->GetBaseZ() > tileElement->GetClearanceZ())
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_SCENERY_SIDE_ABOVE);
|
|
}
|
|
else
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_SCENERY_SIDE_BELOW);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
} while (!(tileElement++)->IsLastForTile());
|
|
}
|
|
|
|
static void ride_ratings_score_close_proximity_loops_helper(RideRatingUpdateState& state, const CoordsXYE& coordsElement)
|
|
{
|
|
TileElement* tileElement = MapGetFirstElementAt(coordsElement);
|
|
if (tileElement == nullptr)
|
|
return;
|
|
do
|
|
{
|
|
if (tileElement->IsGhost())
|
|
continue;
|
|
|
|
auto type = tileElement->GetType();
|
|
if (type == TileElementType::Path)
|
|
{
|
|
int32_t zDiff = static_cast<int32_t>(tileElement->BaseHeight)
|
|
- static_cast<int32_t>(coordsElement.element->BaseHeight);
|
|
if (zDiff >= 0 && zDiff <= 16)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_PATH_TROUGH_VERTICAL_LOOP);
|
|
}
|
|
}
|
|
else if (type == TileElementType::Track)
|
|
{
|
|
bool elementsAreAt90DegAngle = ((tileElement->GetDirection() ^ coordsElement.element->GetDirection()) & 1) != 0;
|
|
if (elementsAreAt90DegAngle)
|
|
{
|
|
int32_t zDiff = static_cast<int32_t>(tileElement->BaseHeight)
|
|
- static_cast<int32_t>(coordsElement.element->BaseHeight);
|
|
if (zDiff >= 0 && zDiff <= 16)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_TRACK_THROUGH_VERTICAL_LOOP);
|
|
if (tileElement->AsTrack()->GetTrackType() == TrackElemType::LeftVerticalLoop
|
|
|| tileElement->AsTrack()->GetTrackType() == TrackElemType::RightVerticalLoop)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_INTERSECTING_VERTICAL_LOOP);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} while (!(tileElement++)->IsLastForTile());
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006B62DA
|
|
*/
|
|
static void ride_ratings_score_close_proximity_loops(RideRatingUpdateState& state, TileElement* inputTileElement)
|
|
{
|
|
auto trackType = inputTileElement->AsTrack()->GetTrackType();
|
|
if (trackType == TrackElemType::LeftVerticalLoop || trackType == TrackElemType::RightVerticalLoop)
|
|
{
|
|
ride_ratings_score_close_proximity_loops_helper(state, { state.Proximity, inputTileElement });
|
|
|
|
int32_t direction = inputTileElement->GetDirection();
|
|
ride_ratings_score_close_proximity_loops_helper(
|
|
state, { CoordsXY{ state.Proximity } + CoordsDirectionDelta[direction], inputTileElement });
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006B5F9D
|
|
*/
|
|
static void ride_ratings_score_close_proximity(RideRatingUpdateState& state, TileElement* inputTileElement)
|
|
{
|
|
if (state.StationFlags & RIDE_RATING_STATION_FLAG_NO_ENTRANCE)
|
|
{
|
|
return;
|
|
}
|
|
|
|
state.ProximityTotal++;
|
|
TileElement* tileElement = MapGetFirstElementAt(state.Proximity);
|
|
if (tileElement == nullptr)
|
|
return;
|
|
do
|
|
{
|
|
if (tileElement->IsGhost())
|
|
continue;
|
|
|
|
int32_t waterHeight;
|
|
switch (tileElement->GetType())
|
|
{
|
|
case TileElementType::Surface:
|
|
state.ProximityBaseHeight = tileElement->BaseHeight;
|
|
if (tileElement->GetBaseZ() == state.Proximity.z)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_SURFACE_TOUCH);
|
|
}
|
|
waterHeight = tileElement->AsSurface()->GetWaterHeight();
|
|
if (waterHeight != 0)
|
|
{
|
|
auto z = waterHeight;
|
|
if (z <= state.Proximity.z)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_WATER_OVER);
|
|
if (z == state.Proximity.z)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_WATER_TOUCH);
|
|
}
|
|
z += 16;
|
|
if (z == state.Proximity.z)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_WATER_LOW);
|
|
}
|
|
z += 112;
|
|
if (z <= state.Proximity.z)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_WATER_HIGH);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case TileElementType::Path:
|
|
if (!tileElement->AsPath()->IsQueue())
|
|
{
|
|
if (tileElement->GetClearanceZ() == inputTileElement->GetBaseZ())
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_PATH_TOUCH_ABOVE);
|
|
}
|
|
if (tileElement->GetBaseZ() == inputTileElement->GetClearanceZ())
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_PATH_TOUCH_UNDER);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (tileElement->GetClearanceZ() <= inputTileElement->GetBaseZ())
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_QUEUE_PATH_OVER);
|
|
}
|
|
if (tileElement->GetClearanceZ() == inputTileElement->GetBaseZ())
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_QUEUE_PATH_TOUCH_ABOVE);
|
|
}
|
|
if (tileElement->GetBaseZ() == inputTileElement->GetClearanceZ())
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_QUEUE_PATH_TOUCH_UNDER);
|
|
}
|
|
}
|
|
break;
|
|
case TileElementType::Track:
|
|
{
|
|
auto trackType = tileElement->AsTrack()->GetTrackType();
|
|
if (trackType == TrackElemType::LeftVerticalLoop || trackType == TrackElemType::RightVerticalLoop)
|
|
{
|
|
int32_t sequence = tileElement->AsTrack()->GetSequenceIndex();
|
|
if (sequence == 3 || sequence == 6)
|
|
{
|
|
if (tileElement->BaseHeight - inputTileElement->ClearanceHeight <= 10)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_THROUGH_VERTICAL_LOOP);
|
|
}
|
|
}
|
|
}
|
|
if (inputTileElement->AsTrack()->GetRideIndex() != tileElement->AsTrack()->GetRideIndex())
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_FOREIGN_TRACK_ABOVE_OR_BELOW);
|
|
if (tileElement->GetClearanceZ() == inputTileElement->GetBaseZ())
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_FOREIGN_TRACK_TOUCH_ABOVE);
|
|
}
|
|
if (tileElement->ClearanceHeight + 2 <= inputTileElement->BaseHeight)
|
|
{
|
|
if (tileElement->ClearanceHeight + 10 >= inputTileElement->BaseHeight)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_FOREIGN_TRACK_CLOSE_ABOVE);
|
|
}
|
|
}
|
|
if (inputTileElement->ClearanceHeight == tileElement->BaseHeight)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_FOREIGN_TRACK_TOUCH_ABOVE);
|
|
}
|
|
if (inputTileElement->ClearanceHeight + 2 == tileElement->BaseHeight)
|
|
{
|
|
if (static_cast<uint8_t>(inputTileElement->ClearanceHeight + 10) >= tileElement->BaseHeight)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_FOREIGN_TRACK_CLOSE_ABOVE);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bool isStation = tileElement->AsTrack()->IsStation();
|
|
if (tileElement->ClearanceHeight == inputTileElement->BaseHeight)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_OWN_TRACK_TOUCH_ABOVE);
|
|
if (isStation)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_OWN_STATION_TOUCH_ABOVE);
|
|
}
|
|
}
|
|
if (tileElement->ClearanceHeight + 2 <= inputTileElement->BaseHeight)
|
|
{
|
|
if (tileElement->ClearanceHeight + 10 >= inputTileElement->BaseHeight)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_OWN_TRACK_CLOSE_ABOVE);
|
|
if (isStation)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_OWN_STATION_CLOSE_ABOVE);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (inputTileElement->GetClearanceZ() == tileElement->GetBaseZ())
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_OWN_TRACK_TOUCH_ABOVE);
|
|
if (isStation)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_OWN_STATION_TOUCH_ABOVE);
|
|
}
|
|
}
|
|
if (inputTileElement->ClearanceHeight + 2 <= tileElement->BaseHeight)
|
|
{
|
|
if (inputTileElement->ClearanceHeight + 10 >= tileElement->BaseHeight)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_OWN_TRACK_CLOSE_ABOVE);
|
|
if (isStation)
|
|
{
|
|
proximity_score_increment(state, PROXIMITY_OWN_STATION_CLOSE_ABOVE);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
} // switch tileElement->GetType
|
|
} while (!(tileElement++)->IsLastForTile());
|
|
|
|
uint8_t direction = inputTileElement->GetDirection();
|
|
ride_ratings_score_close_proximity_in_direction(state, inputTileElement, (direction + 1) & 3);
|
|
ride_ratings_score_close_proximity_in_direction(state, inputTileElement, (direction - 1) & 3);
|
|
ride_ratings_score_close_proximity_loops(state, inputTileElement);
|
|
|
|
switch (state.ProximityTrackType)
|
|
{
|
|
case TrackElemType::Brakes:
|
|
state.AmountOfBrakes++;
|
|
break;
|
|
case TrackElemType::LeftReverser:
|
|
case TrackElemType::RightReverser:
|
|
state.AmountOfReversers++;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void RideRatingsCalculate(RideRatingUpdateState& state, Ride& ride)
|
|
{
|
|
const auto& rrd = ride.GetRideTypeDescriptor().RatingsData;
|
|
|
|
switch (rrd.Type)
|
|
{
|
|
case RatingsCalculationType::Normal:
|
|
if (!(ride.lifecycle_flags & RIDE_LIFECYCLE_TESTED))
|
|
return;
|
|
break;
|
|
case RatingsCalculationType::FlatRide:
|
|
ride.lifecycle_flags |= RIDE_LIFECYCLE_TESTED;
|
|
ride.lifecycle_flags |= RIDE_LIFECYCLE_NO_RAW_STATS;
|
|
break;
|
|
case RatingsCalculationType::Stall:
|
|
ride.upkeep_cost = RideComputeUpkeep(state, ride);
|
|
ride.window_invalidate_flags |= RIDE_INVALIDATE_RIDE_INCOME;
|
|
// Exit ratings
|
|
return;
|
|
}
|
|
|
|
ride.unreliability_factor = rrd.Unreliability;
|
|
SetUnreliabilityFactor(ride);
|
|
|
|
const auto shelteredEighths = GetNumOfShelteredEighths(ride);
|
|
ride.sheltered_eighths = (rrd.RideShelter == -1) ? shelteredEighths.TotalShelteredEighths : rrd.RideShelter;
|
|
|
|
RatingTuple ratings = rrd.BaseRatings;
|
|
// Apply Modifiers
|
|
for (const auto& modifier : rrd.Modifiers)
|
|
{
|
|
switch (modifier.type)
|
|
{
|
|
case RatingsModifierType::BonusLength:
|
|
RideRatingsApplyBonusLength(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusSynchronisation:
|
|
RideRatingsApplyBonusSynchronisation(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusTrainLength:
|
|
RideRatingsApplyBonusTrainLength(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusMaxSpeed:
|
|
RideRatingsApplyBonusMaxSpeed(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusAverageSpeed:
|
|
RideRatingsApplyBonusAverageSpeed(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusDuration:
|
|
RideRatingsApplyBonusDuration(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusGForces:
|
|
RideRatingsApplyBonusGForces(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusTurns:
|
|
RideRatingsApplyBonusTurns(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusDrops:
|
|
RideRatingsApplyBonusDrops(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusSheltered:
|
|
RideRatingsApplyBonusSheltered(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusProximity:
|
|
RideRatingsApplyBonusProximity(ratings, ride, state, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusScenery:
|
|
RideRatingsApplyBonusScenery(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusRotations:
|
|
RideRatingsApplyBonusRotations(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusOperationOption:
|
|
RideRatingsApplyBonusOperationOption(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusReversedTrains:
|
|
RideRatingsApplyBonusReversedTrains(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusGoKartRace:
|
|
RideRatingsApplyBonusGoKartRace(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusTowerRide:
|
|
RideRatingsApplyBonusTowerRide(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusRotoDrop:
|
|
RideRatingsApplyBonusRotoDrop(ratings, ride);
|
|
break;
|
|
case RatingsModifierType::BonusMazeSize:
|
|
RideRatingsApplyBonusMazeSize(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusBoatHireNoCircuit:
|
|
RideRatingsApplyBonusBoatHireNoCircuit(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusSlideUnlimitedRides:
|
|
RideRatingsApplyBonusSlideUnlimitedRides(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusMotionSimulatorMode:
|
|
RideRatingsApplyBonusMotionSimulatorMode(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::Bonus3DCinemaMode:
|
|
RideRatingsApplyBonus3DCinemaMode(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusTopSpinMode:
|
|
RideRatingsApplyBonusTopSpinMode(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusReversals:
|
|
RideRatingsApplyBonusReversals(ratings, ride, state, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusHoles:
|
|
RideRatingsApplyBonusHoles(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusNumTrains:
|
|
RideRatingsApplyBonusNumTrains(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusDownwardLaunch:
|
|
RideRatingsApplyBonusDownwardLaunch(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::BonusLaunchedFreefallSpecial:
|
|
RideRatingsApplyBonusLaunchedFreefallSpecial(ratings, ride, state, modifier);
|
|
break;
|
|
case RatingsModifierType::RequirementLength:
|
|
RideRatingsApplyRequirementLength(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::RequirementMaxSpeed:
|
|
RideRatingsApplyRequirementMaxSpeed(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::RequirementLateralGs:
|
|
RideRatingsApplyRequirementLateralGs(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::RequirementInversions:
|
|
RideRatingsApplyRequirementInversions(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::RequirementUnsheltered:
|
|
RideRatingsApplyRequirementUnsheltered(ratings, ride, shelteredEighths.TrackShelteredEighths, modifier);
|
|
break;
|
|
case RatingsModifierType::RequirementReversals:
|
|
RideRatingsApplyRequirementReversals(ratings, ride, state, modifier);
|
|
break;
|
|
case RatingsModifierType::RequirementHoles:
|
|
RideRatingsApplyRequirementHoles(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::RequirementStations:
|
|
RideRatingsApplyRequirementStations(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::RequirementSplashdown:
|
|
RideRatingsApplyRequirementSplashdown(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::PenaltyLateralGs:
|
|
RideRatingsApplyPenaltyLateralGs(ratings, ride, modifier);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// Requirements that may be ignored if the ride has inversions
|
|
if (ride.inversions == 0 || !rrd.RelaxRequirementsIfInversions)
|
|
{
|
|
switch (modifier.type)
|
|
{
|
|
case RatingsModifierType::RequirementDropHeight:
|
|
RideRatingsApplyRequirementDropHeight(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::RequirementNumDrops:
|
|
RideRatingsApplyRequirementNumDrops(ratings, ride, modifier);
|
|
break;
|
|
case RatingsModifierType::RequirementNegativeGs:
|
|
RideRatingsApplyRequirementNegativeGs(ratings, ride, modifier);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Universl ratings adjustments
|
|
RideRatingsApplyIntensityPenalty(ratings);
|
|
RideRatingsApplyAdjustments(ride, ratings);
|
|
ride.ratings = ratings;
|
|
|
|
ride.upkeep_cost = RideComputeUpkeep(state, ride);
|
|
ride.window_invalidate_flags |= RIDE_INVALIDATE_RIDE_INCOME;
|
|
|
|
#ifdef ORIGINAL_RATINGS
|
|
if (!ride.ratings.isNull())
|
|
{
|
|
// Address underflows allowed by original RCT2 code
|
|
ride.ratings.excitement = std::max<uint16_t>(0, ride.ratings.excitement);
|
|
ride.ratings.intensity = std::max<uint16_t>(0, ride.ratings.intensity);
|
|
ride.ratings.nausea = std::max<uint16_t>(0, ride.ratings.nausea);
|
|
}
|
|
#endif
|
|
|
|
#ifdef ENABLE_SCRIPTING
|
|
// Only call the 'ride.ratings.calculate' API hook if testing of the ride is complete
|
|
if (ride.lifecycle_flags & RIDE_LIFECYCLE_TESTED)
|
|
{
|
|
auto& hookEngine = GetContext()->GetScriptEngine().GetHookEngine();
|
|
if (hookEngine.HasSubscriptions(HOOK_TYPE::RIDE_RATINGS_CALCULATE))
|
|
{
|
|
auto ctx = GetContext()->GetScriptEngine().GetContext();
|
|
auto originalRatings = ride.ratings;
|
|
|
|
// Create event args object
|
|
auto obj = DukObject(ctx);
|
|
obj.Set("rideId", ride.id.ToUnderlying());
|
|
obj.Set("excitement", originalRatings.excitement);
|
|
obj.Set("intensity", originalRatings.intensity);
|
|
obj.Set("nausea", originalRatings.nausea);
|
|
|
|
// Call the subscriptions
|
|
auto e = obj.Take();
|
|
hookEngine.Call(HOOK_TYPE::RIDE_RATINGS_CALCULATE, e, true);
|
|
|
|
auto scriptExcitement = AsOrDefault(e["excitement"], static_cast<int32_t>(originalRatings.excitement));
|
|
auto scriptIntensity = AsOrDefault(e["intensity"], static_cast<int32_t>(originalRatings.intensity));
|
|
auto scriptNausea = AsOrDefault(e["nausea"], static_cast<int32_t>(originalRatings.nausea));
|
|
|
|
ride.ratings.excitement = std::clamp<int32_t>(scriptExcitement, 0, INT16_MAX);
|
|
ride.ratings.intensity = std::clamp<int32_t>(scriptIntensity, 0, INT16_MAX);
|
|
ride.ratings.nausea = std::clamp<int32_t>(scriptNausea, 0, INT16_MAX);
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static void RideRatingsCalculateValue(Ride& ride)
|
|
{
|
|
struct Row
|
|
{
|
|
int32_t months, multiplier, divisor, summand;
|
|
};
|
|
static const Row ageTableNew[] = {
|
|
{ 5, 3, 2, 0 }, // 1.5x
|
|
{ 13, 6, 5, 0 }, // 1.2x
|
|
{ 40, 1, 1, 0 }, // 1x
|
|
{ 64, 3, 4, 0 }, // 0.75x
|
|
{ 88, 9, 16, 0 }, // 0.56x
|
|
{ 104, 27, 64, 0 }, // 0.42x
|
|
{ 120, 81, 256, 0 }, // 0.32x
|
|
{ 128, 81, 512, 0 }, // 0.16x
|
|
{ 200, 81, 1024, 0 }, // 0.08x
|
|
{ 200, 9, 16, 0 }, // 0.56x "easter egg"
|
|
};
|
|
|
|
#ifdef ORIGINAL_RATINGS
|
|
static const Row ageTableOld[] = {
|
|
{ 5, 1, 1, 30 }, // +30
|
|
{ 13, 1, 1, 10 }, // +10
|
|
{ 40, 1, 1, 0 }, // 1x
|
|
{ 64, 3, 4, 0 }, // 0.75x
|
|
{ 88, 9, 16, 0 }, // 0.56x
|
|
{ 104, 27, 64, 0 }, // 0.42x
|
|
{ 120, 81, 256, 0 }, // 0.32x
|
|
{ 128, 81, 512, 0 }, // 0.16x
|
|
{ 200, 81, 1024, 0 }, // 0.08x
|
|
{ 200, 9, 16, 0 }, // 0.56x "easter egg"
|
|
};
|
|
#endif
|
|
|
|
if (!RideHasRatings(ride))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Start with the base ratings, multiplied by the ride type specific weights for excitement, intensity and nausea.
|
|
const auto& ratingsMultipliers = ride.GetRideTypeDescriptor().RatingsMultipliers;
|
|
money64 value = (((ride.ratings.excitement * ratingsMultipliers.excitement) * 32) >> 15)
|
|
+ (((ride.ratings.intensity * ratingsMultipliers.intensity) * 32) >> 15)
|
|
+ (((ride.ratings.nausea * ratingsMultipliers.nausea) * 32) >> 15);
|
|
|
|
int32_t monthsOld = 0;
|
|
if (!GetGameState().Cheats.disableRideValueAging)
|
|
{
|
|
monthsOld = ride.GetAge();
|
|
}
|
|
|
|
const Row* ageTable = ageTableNew;
|
|
size_t tableSize = std::size(ageTableNew);
|
|
|
|
#ifdef ORIGINAL_RATINGS
|
|
ageTable = ageTableOld;
|
|
tableSize = std::size(ageTableOld);
|
|
#endif
|
|
|
|
Row lastRow = ageTable[tableSize - 1];
|
|
|
|
// Ride is older than oldest age in the table?
|
|
if (monthsOld >= lastRow.months)
|
|
{
|
|
value = (value * lastRow.multiplier) / lastRow.divisor + lastRow.summand;
|
|
}
|
|
else
|
|
{
|
|
// Find the first hit in the table that matches this ride's age
|
|
for (size_t it = 0; it < tableSize; it++)
|
|
{
|
|
Row curr = ageTable[it];
|
|
|
|
if (monthsOld < curr.months)
|
|
{
|
|
value = (value * curr.multiplier) / curr.divisor + curr.summand;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Other ride of same type penalty
|
|
const auto& rideManager = GetRideManager();
|
|
auto rideType = ride.type;
|
|
auto otherRidesOfSameType = std::count_if(rideManager.begin(), rideManager.end(), [rideType](const Ride& r) {
|
|
return r.status == RideStatus::Open && r.type == rideType;
|
|
});
|
|
if (otherRidesOfSameType > 1)
|
|
value -= value / 4;
|
|
|
|
ride.value = std::max(0.00_GBP, value);
|
|
}
|
|
|
|
/**
|
|
* I think this function computes ride upkeep? Though it is weird that the
|
|
* rct2: Sub65E621
|
|
* inputs
|
|
* - edi: ride ptr
|
|
*/
|
|
static money64 RideComputeUpkeep(RideRatingUpdateState& state, const Ride& ride)
|
|
{
|
|
// data stored at 0x0057E3A8, incrementing 18 bytes at a time
|
|
auto upkeep = ride.GetRideTypeDescriptor().UpkeepCosts.BaseCost;
|
|
|
|
auto trackCost = ride.GetRideTypeDescriptor().UpkeepCosts.CostPerTrackPiece;
|
|
upkeep += trackCost * ride.getNumPoweredLifts();
|
|
|
|
uint32_t totalLength = ToHumanReadableRideLength(ride.GetTotalLength());
|
|
|
|
// The data originally here was 20's and 0's. The 20's all represented
|
|
// rides that had tracks. The 0's were fixed rides like crooked house or
|
|
// dodgems.
|
|
// Data source is 0x0097E3AC
|
|
totalLength *= ride.GetRideTypeDescriptor().UpkeepCosts.TrackLengthMultiplier;
|
|
upkeep += static_cast<uint16_t>(totalLength >> 10);
|
|
|
|
if (ride.lifecycle_flags & RIDE_LIFECYCLE_ON_RIDE_PHOTO)
|
|
{
|
|
// The original code read from a table starting at 0x0097E3AE and
|
|
// incrementing by 0x12 bytes between values. However, all of these
|
|
// values were 40. I have replaced the table lookup with the constant
|
|
// 40 in this case.
|
|
upkeep += 40;
|
|
}
|
|
|
|
// Add maintenance cost for reverser track pieces
|
|
upkeep += 10 * state.AmountOfReversers;
|
|
|
|
// Add maintenance cost for brake track pieces
|
|
upkeep += 20 * state.AmountOfBrakes;
|
|
|
|
// these seem to be adhoc adjustments to a ride's upkeep/cost, times
|
|
// various variables set on the ride itself.
|
|
|
|
// https://gist.github.com/kevinburke/e19b803cd2769d96c540
|
|
upkeep += ride.GetRideTypeDescriptor().UpkeepCosts.CostPerTrain * ride.NumTrains;
|
|
upkeep += ride.GetRideTypeDescriptor().UpkeepCosts.CostPerCar * ride.num_cars_per_train;
|
|
|
|
// slight upkeep boosts for some rides - 5 for mini railway, 10 for log
|
|
// flume/rapids, 10 for roller coaster, 28 for giga coaster
|
|
upkeep += ride.GetRideTypeDescriptor().UpkeepCosts.CostPerStation * ride.num_stations;
|
|
|
|
if (ride.mode == RideMode::ReverseInclineLaunchedShuttle)
|
|
{
|
|
upkeep += 30;
|
|
}
|
|
else if (ride.mode == RideMode::PoweredLaunchPasstrough)
|
|
{
|
|
upkeep += 160;
|
|
}
|
|
else if (ride.mode == RideMode::LimPoweredLaunch)
|
|
{
|
|
upkeep += 320;
|
|
}
|
|
else if (ride.mode == RideMode::PoweredLaunch || ride.mode == RideMode::PoweredLaunchBlockSectioned)
|
|
{
|
|
upkeep += 220;
|
|
}
|
|
|
|
// multiply by 5/8
|
|
upkeep *= 10;
|
|
upkeep >>= 4;
|
|
return upkeep;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0065E7FB
|
|
*
|
|
* inputs
|
|
* - bx: excitement
|
|
* - cx: intensity
|
|
* - bp: nausea
|
|
* - edi: ride ptr
|
|
*/
|
|
static void RideRatingsApplyAdjustments(const Ride& ride, RatingTuple& ratings)
|
|
{
|
|
const auto* rideEntry = GetRideEntryByIndex(ride.subtype);
|
|
|
|
if (rideEntry == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Apply ride entry multipliers
|
|
RideRatingsAdd(
|
|
ratings, ((static_cast<int32_t>(ratings.excitement) * rideEntry->excitement_multiplier) >> 7),
|
|
((static_cast<int32_t>(ratings.intensity) * rideEntry->intensity_multiplier) >> 7),
|
|
((static_cast<int32_t>(ratings.nausea) * rideEntry->nausea_multiplier) >> 7));
|
|
|
|
// Apply total air time
|
|
#ifdef ORIGINAL_RATINGS
|
|
if (ride.GetRideTypeDescriptor().HasFlag(RtdFlag::hasAirTime))
|
|
{
|
|
uint16_t totalAirTime = ride.totalAirTime;
|
|
if (rideEntry->flags & RIDE_ENTRY_FLAG_LIMIT_AIRTIME_BONUS)
|
|
{
|
|
if (totalAirTime >= 96)
|
|
{
|
|
totalAirTime -= 96;
|
|
ratings.excitement -= totalAirTime / 8;
|
|
ratings.nausea += totalAirTime / 16;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ratings.excitement += totalAirTime / 8;
|
|
ratings.nausea += totalAirTime / 16;
|
|
}
|
|
}
|
|
#else
|
|
if (ride.GetRideTypeDescriptor().HasFlag(RtdFlag::hasAirTime))
|
|
{
|
|
int32_t excitementModifier;
|
|
if (rideEntry->flags & RIDE_ENTRY_FLAG_LIMIT_AIRTIME_BONUS)
|
|
{
|
|
// Limit airtime bonus for heartline twister coaster (see issues #2031 and #2064)
|
|
excitementModifier = std::min<uint16_t>(ride.totalAirTime, 96) / 8;
|
|
}
|
|
else
|
|
{
|
|
excitementModifier = ride.totalAirTime / 8;
|
|
}
|
|
int32_t nauseaModifier = ride.totalAirTime / 16;
|
|
|
|
RideRatingsAdd(ratings, excitementModifier, 0, nauseaModifier);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* Lowers excitement, the higher the intensity.
|
|
* rct2: 0x0065E7A3
|
|
*/
|
|
static void RideRatingsApplyIntensityPenalty(RatingTuple& ratings)
|
|
{
|
|
static constexpr ride_rating intensityBounds[] = { 1000, 1100, 1200, 1320, 1450 };
|
|
ride_rating excitement = ratings.excitement;
|
|
for (auto intensityBound : intensityBounds)
|
|
{
|
|
if (ratings.intensity >= intensityBound)
|
|
{
|
|
excitement -= excitement / 4;
|
|
}
|
|
}
|
|
ratings.excitement = excitement;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00655FD6
|
|
*/
|
|
static void SetUnreliabilityFactor(Ride& ride)
|
|
{
|
|
// Special unreliability for a few ride types
|
|
if (ride.type == RIDE_TYPE_COMPACT_INVERTED_COASTER && ride.mode == RideMode::ReverseInclineLaunchedShuttle)
|
|
{
|
|
ride.unreliability_factor += 10;
|
|
}
|
|
else if (ride.type == RIDE_TYPE_LOOPING_ROLLER_COASTER && ride.IsPoweredLaunched())
|
|
{
|
|
ride.unreliability_factor += 5;
|
|
}
|
|
else if (ride.type == RIDE_TYPE_CHAIRLIFT)
|
|
{
|
|
ride.unreliability_factor += (ride.speed * 2);
|
|
}
|
|
// The bigger the difference in lift speed and minimum the higher the unreliability
|
|
uint8_t minLiftSpeed = ride.GetRideTypeDescriptor().LiftData.minimum_speed;
|
|
ride.unreliability_factor += (ride.lift_hill_speed - minLiftSpeed) * 2;
|
|
}
|
|
|
|
static uint32_t get_proximity_score_helper_1(uint16_t x, uint16_t max, uint32_t multiplier)
|
|
{
|
|
return (std::min(x, max) * multiplier) >> 16;
|
|
}
|
|
|
|
static uint32_t get_proximity_score_helper_2(uint16_t x, uint16_t additionIfNotZero, uint16_t max, uint32_t multiplier)
|
|
{
|
|
uint32_t result = x;
|
|
if (result != 0)
|
|
result += additionIfNotZero;
|
|
return (std::min<int32_t>(result, max) * multiplier) >> 16;
|
|
}
|
|
|
|
static uint32_t get_proximity_score_helper_3(uint16_t x, uint16_t resultIfNotZero)
|
|
{
|
|
return x == 0 ? 0 : resultIfNotZero;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0065E277
|
|
*/
|
|
static uint32_t ride_ratings_get_proximity_score(RideRatingUpdateState& state)
|
|
{
|
|
const uint16_t* scores = state.ProximityScores;
|
|
|
|
uint32_t result = 0;
|
|
result += get_proximity_score_helper_1(scores[PROXIMITY_WATER_OVER], 60, 0x00AAAA);
|
|
result += get_proximity_score_helper_1(scores[PROXIMITY_WATER_TOUCH], 22, 0x0245D1);
|
|
result += get_proximity_score_helper_1(scores[PROXIMITY_WATER_LOW], 10, 0x020000);
|
|
result += get_proximity_score_helper_1(scores[PROXIMITY_WATER_HIGH], 40, 0x00A000);
|
|
result += get_proximity_score_helper_1(scores[PROXIMITY_SURFACE_TOUCH], 70, 0x01B6DB);
|
|
result += get_proximity_score_helper_1(scores[PROXIMITY_QUEUE_PATH_OVER] + 8, 12, 0x064000);
|
|
result += get_proximity_score_helper_3(scores[PROXIMITY_QUEUE_PATH_TOUCH_ABOVE], 40);
|
|
result += get_proximity_score_helper_3(scores[PROXIMITY_QUEUE_PATH_TOUCH_UNDER], 45);
|
|
result += get_proximity_score_helper_2(scores[PROXIMITY_PATH_TOUCH_ABOVE], 10, 20, 0x03C000);
|
|
result += get_proximity_score_helper_2(scores[PROXIMITY_PATH_TOUCH_UNDER], 10, 20, 0x044000);
|
|
result += get_proximity_score_helper_2(scores[PROXIMITY_OWN_TRACK_TOUCH_ABOVE], 10, 15, 0x035555);
|
|
result += get_proximity_score_helper_1(scores[PROXIMITY_OWN_TRACK_CLOSE_ABOVE], 5, 0x060000);
|
|
result += get_proximity_score_helper_2(scores[PROXIMITY_FOREIGN_TRACK_ABOVE_OR_BELOW], 10, 15, 0x02AAAA);
|
|
result += get_proximity_score_helper_2(scores[PROXIMITY_FOREIGN_TRACK_TOUCH_ABOVE], 10, 15, 0x04AAAA);
|
|
result += get_proximity_score_helper_1(scores[PROXIMITY_FOREIGN_TRACK_CLOSE_ABOVE], 5, 0x090000);
|
|
result += get_proximity_score_helper_1(scores[PROXIMITY_SCENERY_SIDE_BELOW], 35, 0x016DB6);
|
|
result += get_proximity_score_helper_1(scores[PROXIMITY_SCENERY_SIDE_ABOVE], 35, 0x00DB6D);
|
|
result += get_proximity_score_helper_3(scores[PROXIMITY_OWN_STATION_TOUCH_ABOVE], 55);
|
|
result += get_proximity_score_helper_3(scores[PROXIMITY_OWN_STATION_CLOSE_ABOVE], 25);
|
|
result += get_proximity_score_helper_2(scores[PROXIMITY_TRACK_THROUGH_VERTICAL_LOOP], 4, 6, 0x140000);
|
|
result += get_proximity_score_helper_2(scores[PROXIMITY_PATH_TROUGH_VERTICAL_LOOP], 4, 6, 0x0F0000);
|
|
result += get_proximity_score_helper_3(scores[PROXIMITY_INTERSECTING_VERTICAL_LOOP], 100);
|
|
result += get_proximity_score_helper_2(scores[PROXIMITY_THROUGH_VERTICAL_LOOP], 4, 6, 0x0A0000);
|
|
result += get_proximity_score_helper_2(scores[PROXIMITY_PATH_SIDE_CLOSE], 10, 20, 0x01C000);
|
|
result += get_proximity_score_helper_2(scores[PROXIMITY_FOREIGN_TRACK_SIDE_CLOSE], 10, 20, 0x024000);
|
|
result += get_proximity_score_helper_2(scores[PROXIMITY_SURFACE_SIDE_CLOSE], 10, 20, 0x028000);
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Calculates how much of the track is sheltered in eighths.
|
|
* rct2: 0x0065E72D
|
|
*/
|
|
static ShelteredEights GetNumOfShelteredEighths(const Ride& ride)
|
|
{
|
|
int32_t totalLength = ride.GetTotalLength();
|
|
int32_t shelteredLength = ride.sheltered_length;
|
|
int32_t lengthEighth = totalLength / 8;
|
|
int32_t lengthCounter = lengthEighth;
|
|
uint8_t numShelteredEighths = 0;
|
|
for (int32_t i = 0; i < 7; i++)
|
|
{
|
|
if (shelteredLength >= lengthCounter)
|
|
{
|
|
lengthCounter += lengthEighth;
|
|
numShelteredEighths++;
|
|
}
|
|
}
|
|
|
|
uint8_t trackShelteredEighths = numShelteredEighths;
|
|
const auto* rideType = GetRideEntryByIndex(ride.subtype);
|
|
if (rideType == nullptr)
|
|
{
|
|
return { 0, 0 };
|
|
}
|
|
if (rideType->flags & RIDE_ENTRY_FLAG_COVERED_RIDE)
|
|
numShelteredEighths = 7;
|
|
|
|
return { trackShelteredEighths, numShelteredEighths };
|
|
}
|
|
|
|
static RatingTuple get_flat_turns_rating(const Ride& ride)
|
|
{
|
|
int32_t num3PlusTurns = GetTurnCount3Elements(ride, 0);
|
|
int32_t num2Turns = GetTurnCount2Elements(ride, 0);
|
|
int32_t num1Turns = GetTurnCount1Element(ride, 0);
|
|
|
|
RatingTuple rating;
|
|
rating.excitement = (num3PlusTurns * 0x28000) >> 16;
|
|
rating.excitement += (num2Turns * 0x30000) >> 16;
|
|
rating.excitement += (num1Turns * 63421) >> 16;
|
|
|
|
rating.intensity = (num3PlusTurns * 81920) >> 16;
|
|
rating.intensity += (num2Turns * 49152) >> 16;
|
|
rating.intensity += (num1Turns * 21140) >> 16;
|
|
|
|
rating.nausea = (num3PlusTurns * 0x50000) >> 16;
|
|
rating.nausea += (num2Turns * 0x32000) >> 16;
|
|
rating.nausea += (num1Turns * 42281) >> 16;
|
|
|
|
return rating;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0065DF72
|
|
*/
|
|
static RatingTuple get_banked_turns_rating(const Ride& ride)
|
|
{
|
|
int32_t num3PlusTurns = GetTurnCount3Elements(ride, 1);
|
|
int32_t num2Turns = GetTurnCount2Elements(ride, 1);
|
|
int32_t num1Turns = GetTurnCount1Element(ride, 1);
|
|
|
|
RatingTuple rating;
|
|
rating.excitement = (num3PlusTurns * 0x3C000) >> 16;
|
|
rating.excitement += (num2Turns * 0x3C000) >> 16;
|
|
rating.excitement += (num1Turns * 73992) >> 16;
|
|
|
|
rating.intensity = (num3PlusTurns * 0x14000) >> 16;
|
|
rating.intensity += (num2Turns * 49152) >> 16;
|
|
rating.intensity += (num1Turns * 21140) >> 16;
|
|
|
|
rating.nausea = (num3PlusTurns * 0x50000) >> 16;
|
|
rating.nausea += (num2Turns * 0x32000) >> 16;
|
|
rating.nausea += (num1Turns * 48623) >> 16;
|
|
|
|
return rating;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0065E047
|
|
*/
|
|
static RatingTuple get_sloped_turns_rating(const Ride& ride)
|
|
{
|
|
RatingTuple rating;
|
|
|
|
int32_t num4PlusTurns = GetTurnCount4PlusElements(ride, 2);
|
|
int32_t num3Turns = GetTurnCount3Elements(ride, 2);
|
|
int32_t num2Turns = GetTurnCount2Elements(ride, 2);
|
|
int32_t num1Turns = GetTurnCount1Element(ride, 2);
|
|
|
|
rating.excitement = (std::min(num4PlusTurns, 4) * 0x78000) >> 16;
|
|
rating.excitement += (std::min(num3Turns, 6) * 273066) >> 16;
|
|
rating.excitement += (std::min(num2Turns, 6) * 0x3AAAA) >> 16;
|
|
rating.excitement += (std::min(num1Turns, 7) * 187245) >> 16;
|
|
rating.intensity = 0;
|
|
rating.nausea = (std::min(num4PlusTurns, 8) * 0x78000) >> 16;
|
|
|
|
return rating;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0065E0F2
|
|
*/
|
|
static RatingTuple get_inversions_ratings(uint16_t inversions)
|
|
{
|
|
RatingTuple rating;
|
|
|
|
rating.excitement = (std::min<int32_t>(inversions, 6) * 0x1AAAAA) >> 16;
|
|
rating.intensity = (inversions * 0x320000) >> 16;
|
|
rating.nausea = (inversions * 0x15AAAA) >> 16;
|
|
|
|
return rating;
|
|
}
|
|
|
|
void SpecialTrackElementRatingsAjustment_Default(const Ride& ride, int32_t& excitement, int32_t& intensity, int32_t& nausea)
|
|
{
|
|
if (ride.HasWaterSplash())
|
|
{
|
|
excitement += 50;
|
|
intensity += 30;
|
|
nausea += 20;
|
|
}
|
|
if (ride.HasWaterfall())
|
|
{
|
|
excitement += 55;
|
|
intensity += 30;
|
|
}
|
|
if (ride.HasWhirlpool())
|
|
{
|
|
excitement += 35;
|
|
intensity += 20;
|
|
nausea += 23;
|
|
}
|
|
}
|
|
|
|
void SpecialTrackElementRatingsAjustment_GhostTrain(const Ride& ride, int32_t& excitement, int32_t& intensity, int32_t& nausea)
|
|
{
|
|
if (ride.HasSpinningTunnel())
|
|
{
|
|
excitement += 40;
|
|
intensity += 25;
|
|
nausea += 55;
|
|
}
|
|
}
|
|
|
|
void SpecialTrackElementRatingsAjustment_LogFlume(const Ride& ride, int32_t& excitement, int32_t& intensity, int32_t& nausea)
|
|
{
|
|
if (ride.HasLogReverser())
|
|
{
|
|
excitement += 48;
|
|
intensity += 55;
|
|
nausea += 65;
|
|
}
|
|
}
|
|
|
|
static RatingTuple GetSpecialTrackElementsRating(uint8_t type, const Ride& ride)
|
|
{
|
|
int32_t excitement = 0, intensity = 0, nausea = 0;
|
|
const auto& rtd = ride.GetRideTypeDescriptor();
|
|
rtd.SpecialElementRatingAdjustment(ride, excitement, intensity, nausea);
|
|
|
|
uint8_t helixSections = RideGetHelixSections(ride);
|
|
|
|
int32_t helixesUpTo9 = std::min<int32_t>(helixSections, 9);
|
|
excitement += (helixesUpTo9 * 254862) >> 16;
|
|
|
|
int32_t helixesUpTo11 = std::min<int32_t>(helixSections, 11);
|
|
intensity += (helixesUpTo11 * 148945) >> 16;
|
|
|
|
int32_t helixesOver5UpTo10 = std::clamp<int32_t>(helixSections - 5, 0, 10);
|
|
nausea += (helixesOver5UpTo10 * 0x140000) >> 16;
|
|
|
|
RatingTuple rating = { static_cast<ride_rating>(excitement), static_cast<ride_rating>(intensity),
|
|
static_cast<ride_rating>(nausea) };
|
|
return rating;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0065DDD1
|
|
*/
|
|
static RatingTuple ride_ratings_get_turns_ratings(const Ride& ride)
|
|
{
|
|
int32_t excitement = 0, intensity = 0, nausea = 0;
|
|
|
|
RatingTuple specialTrackElementsRating = GetSpecialTrackElementsRating(ride.type, ride);
|
|
excitement += specialTrackElementsRating.excitement;
|
|
intensity += specialTrackElementsRating.intensity;
|
|
nausea += specialTrackElementsRating.nausea;
|
|
|
|
RatingTuple flatTurnsRating = get_flat_turns_rating(ride);
|
|
excitement += flatTurnsRating.excitement;
|
|
intensity += flatTurnsRating.intensity;
|
|
nausea += flatTurnsRating.nausea;
|
|
|
|
RatingTuple bankedTurnsRating = get_banked_turns_rating(ride);
|
|
excitement += bankedTurnsRating.excitement;
|
|
intensity += bankedTurnsRating.intensity;
|
|
nausea += bankedTurnsRating.nausea;
|
|
|
|
RatingTuple slopedTurnsRating = get_sloped_turns_rating(ride);
|
|
excitement += slopedTurnsRating.excitement;
|
|
intensity += slopedTurnsRating.intensity;
|
|
nausea += slopedTurnsRating.nausea;
|
|
|
|
auto inversions = ride.GetRideTypeDescriptor().specialType == RtdSpecialType::miniGolf ? ride.holes : ride.inversions;
|
|
RatingTuple inversionsRating = get_inversions_ratings(inversions);
|
|
excitement += inversionsRating.excitement;
|
|
intensity += inversionsRating.intensity;
|
|
nausea += inversionsRating.nausea;
|
|
|
|
RatingTuple rating = { static_cast<ride_rating>(excitement), static_cast<ride_rating>(intensity),
|
|
static_cast<ride_rating>(nausea) };
|
|
return rating;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0065E1C2
|
|
*/
|
|
static RatingTuple ride_ratings_get_sheltered_ratings(const Ride& ride)
|
|
{
|
|
int32_t shelteredLengthShifted = (ride.sheltered_length) >> 16;
|
|
|
|
uint32_t shelteredLengthUpTo1000 = std::min(shelteredLengthShifted, 1000);
|
|
uint32_t shelteredLengthUpTo2000 = std::min(shelteredLengthShifted, 2000);
|
|
|
|
int32_t excitement = (shelteredLengthUpTo1000 * 9175) >> 16;
|
|
int32_t intensity = (shelteredLengthUpTo2000 * 0x2666) >> 16;
|
|
int32_t nausea = (shelteredLengthUpTo1000 * 0x4000) >> 16;
|
|
|
|
/*eax = (ride.var_11C * 30340) >> 16;*/
|
|
/*nausea += eax;*/
|
|
|
|
if (ride.num_sheltered_sections & ShelteredSectionsBits::BankingWhileSheltered)
|
|
{
|
|
excitement += 20;
|
|
nausea += 15;
|
|
}
|
|
|
|
if (ride.num_sheltered_sections & ShelteredSectionsBits::RotatingWhileSheltered)
|
|
{
|
|
excitement += 20;
|
|
nausea += 15;
|
|
}
|
|
|
|
uint8_t lowerVal = ride.GetNumShelteredSections();
|
|
lowerVal = std::min<uint8_t>(lowerVal, 11);
|
|
excitement += (lowerVal * 774516) >> 16;
|
|
|
|
RatingTuple rating = { static_cast<ride_rating>(excitement), static_cast<ride_rating>(intensity),
|
|
static_cast<ride_rating>(nausea) };
|
|
return rating;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0065DCDC
|
|
*/
|
|
static RatingTuple ride_ratings_get_gforce_ratings(const Ride& ride)
|
|
{
|
|
RatingTuple result = {
|
|
.excitement = 0,
|
|
.intensity = 0,
|
|
.nausea = 0,
|
|
};
|
|
|
|
// Apply maximum positive G force factor
|
|
result.excitement += (ride.max_positive_vertical_g * 5242) >> 16;
|
|
result.intensity += (ride.max_positive_vertical_g * 52428) >> 16;
|
|
result.nausea += (ride.max_positive_vertical_g * 17039) >> 16;
|
|
|
|
// Apply maximum negative G force factor
|
|
fixed16_2dp gforce = ride.max_negative_vertical_g;
|
|
result.excitement += (std::clamp<fixed16_2dp>(gforce, -FIXED_2DP(2, 50), FIXED_2DP(0, 00)) * -15728) >> 16;
|
|
result.intensity += ((gforce - FIXED_2DP(1, 00)) * -52428) >> 16;
|
|
result.nausea += ((gforce - FIXED_2DP(1, 00)) * -14563) >> 16;
|
|
|
|
// Apply lateral G force factor
|
|
result.excitement += (std::min<fixed16_2dp>(FIXED_2DP(1, 50), ride.max_lateral_g) * 26214) >> 16;
|
|
result.intensity += ride.max_lateral_g;
|
|
result.nausea += (ride.max_lateral_g * 21845) >> 16;
|
|
|
|
// Very high lateral G force penalty
|
|
#ifdef ORIGINAL_RATINGS
|
|
if (ride.max_lateral_g > FIXED_2DP(2, 80))
|
|
{
|
|
result.intensity += FIXED_2DP(3, 75);
|
|
result.nausea += FIXED_2DP(2, 00);
|
|
}
|
|
if (ride.max_lateral_g > FIXED_2DP(3, 10))
|
|
{
|
|
result.excitement /= 2;
|
|
result.intensity += FIXED_2DP(8, 50);
|
|
result.nausea += FIXED_2DP(4, 00);
|
|
}
|
|
#endif
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0065E139
|
|
*/
|
|
static RatingTuple ride_ratings_get_drop_ratings(const Ride& ride)
|
|
{
|
|
RatingTuple result = {
|
|
/* .excitement = */ 0,
|
|
/* .intensity = */ 0,
|
|
/* .nausea = */ 0,
|
|
};
|
|
|
|
// Apply number of drops factor
|
|
int32_t drops = ride.getNumDrops();
|
|
result.excitement += (std::min(9, drops) * 728177) >> 16;
|
|
result.intensity += (drops * 928426) >> 16;
|
|
result.nausea += (drops * 655360) >> 16;
|
|
|
|
// Apply highest drop factor
|
|
RideRatingsAdd(
|
|
result, ((ride.highest_drop_height * 2) * 16000) >> 16, ((ride.highest_drop_height * 2) * 32000) >> 16,
|
|
((ride.highest_drop_height * 2) * 10240) >> 16);
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Calculates a score based on the surrounding scenery.
|
|
* rct2: 0x0065E557
|
|
*/
|
|
static int32_t ride_ratings_get_scenery_score(const Ride& ride)
|
|
{
|
|
auto stationIndex = RideGetFirstValidStationStart(ride);
|
|
CoordsXY location;
|
|
|
|
if (stationIndex.IsNull())
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
const auto& rtd = ride.GetRideTypeDescriptor();
|
|
if (rtd.specialType == RtdSpecialType::maze)
|
|
{
|
|
location = ride.GetStation().Entrance.ToCoordsXY();
|
|
}
|
|
else
|
|
{
|
|
location = ride.GetStation(stationIndex).Start;
|
|
}
|
|
|
|
int32_t z = TileElementHeight(location);
|
|
|
|
// Check if station is underground, returns a fixed mediocre score since you can't have scenery underground
|
|
if (z > ride.GetStation(stationIndex).GetBaseZ())
|
|
{
|
|
return 40;
|
|
}
|
|
|
|
// Count surrounding scenery items
|
|
int32_t numSceneryItems = 0;
|
|
auto tileLocation = TileCoordsXY(location);
|
|
auto& gameState = GetGameState();
|
|
for (int32_t yy = std::max(tileLocation.y - 5, 0); yy <= std::min(tileLocation.y + 5, gameState.MapSize.y - 1); yy++)
|
|
{
|
|
for (int32_t xx = std::max(tileLocation.x - 5, 0); xx <= std::min(tileLocation.x + 5, gameState.MapSize.x - 1); xx++)
|
|
{
|
|
// Count scenery items on this tile
|
|
TileElement* tileElement = MapGetFirstElementAt(TileCoordsXY{ xx, yy });
|
|
if (tileElement == nullptr)
|
|
continue;
|
|
do
|
|
{
|
|
if (tileElement->IsGhost())
|
|
continue;
|
|
|
|
const auto type = tileElement->GetType();
|
|
if (type == TileElementType::SmallScenery || type == TileElementType::LargeScenery)
|
|
numSceneryItems++;
|
|
} while (!(tileElement++)->IsLastForTile());
|
|
}
|
|
}
|
|
|
|
return std::min(numSceneryItems, 47) * 5;
|
|
}
|
|
|
|
#pragma region Ride rating calculation helpers
|
|
|
|
static void RideRatingsSet(RatingTuple& ratings, int32_t excitement, int32_t intensity, int32_t nausea)
|
|
{
|
|
ratings.excitement = 0;
|
|
ratings.intensity = 0;
|
|
ratings.nausea = 0;
|
|
RideRatingsAdd(ratings, excitement, intensity, nausea);
|
|
}
|
|
|
|
/**
|
|
* Add to a ride rating with overflow protection.
|
|
*/
|
|
static void RideRatingsAdd(RatingTuple& ratings, int32_t excitement, int32_t intensity, int32_t nausea)
|
|
{
|
|
int32_t newExcitement = ratings.excitement + excitement;
|
|
int32_t newIntensity = ratings.intensity + intensity;
|
|
int32_t newNausea = ratings.nausea + nausea;
|
|
ratings.excitement = std::clamp<int32_t>(newExcitement, 0, INT16_MAX);
|
|
ratings.intensity = std::clamp<int32_t>(newIntensity, 0, INT16_MAX);
|
|
ratings.nausea = std::clamp<int32_t>(newNausea, 0, INT16_MAX);
|
|
}
|
|
|
|
static void RideRatingsApplyBonusLength(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
RideRatingsAdd(
|
|
ratings, (std::min(ToHumanReadableRideLength(ride.GetTotalLength()), modifier.threshold) * modifier.excitement) >> 16,
|
|
0, 0);
|
|
}
|
|
|
|
static void RideRatingsApplyBonusSynchronisation(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
if ((ride.depart_flags & RIDE_DEPART_SYNCHRONISE_WITH_ADJACENT_STATIONS) && RideHasAdjacentStation(ride))
|
|
{
|
|
RideRatingsAdd(ratings, modifier.excitement, modifier.intensity, modifier.nausea);
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyBonusTrainLength(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
RideRatingsAdd(ratings, ((ride.num_cars_per_train - 1) * modifier.excitement) >> 16, 0, 0);
|
|
}
|
|
|
|
static void RideRatingsApplyBonusMaxSpeed(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
int32_t maxSpeedMod = ride.max_speed >> 16;
|
|
RideRatingsAdd(
|
|
ratings, (maxSpeedMod * modifier.excitement) >> 16, (maxSpeedMod * modifier.intensity) >> 16,
|
|
(maxSpeedMod * modifier.nausea) >> 16);
|
|
}
|
|
|
|
static void RideRatingsApplyBonusAverageSpeed(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
int32_t avgSpeedMod = ride.average_speed >> 16;
|
|
RideRatingsAdd(ratings, (avgSpeedMod * modifier.excitement) >> 16, (avgSpeedMod * modifier.intensity) >> 16, 0);
|
|
}
|
|
|
|
static void RideRatingsApplyBonusDuration(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
RideRatingsAdd(ratings, (std::min(ride.GetTotalTime(), modifier.threshold) * modifier.excitement) >> 16, 0, 0);
|
|
}
|
|
|
|
static void RideRatingsApplyBonusGForces(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
RatingTuple subRating = ride_ratings_get_gforce_ratings(ride);
|
|
RideRatingsAdd(
|
|
ratings, (subRating.excitement * modifier.excitement) >> 16, (subRating.intensity * modifier.intensity) >> 16,
|
|
(subRating.nausea * modifier.nausea) >> 16);
|
|
}
|
|
|
|
static void RideRatingsApplyBonusTurns(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
RatingTuple subRating = ride_ratings_get_turns_ratings(ride);
|
|
RideRatingsAdd(
|
|
ratings, (subRating.excitement * modifier.excitement) >> 16, (subRating.intensity * modifier.intensity) >> 16,
|
|
(subRating.nausea * modifier.nausea) >> 16);
|
|
}
|
|
|
|
static void RideRatingsApplyBonusDrops(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
RatingTuple subRating = ride_ratings_get_drop_ratings(ride);
|
|
RideRatingsAdd(
|
|
ratings, (subRating.excitement * modifier.excitement) >> 16, (subRating.intensity * modifier.intensity) >> 16,
|
|
(subRating.nausea * modifier.nausea) >> 16);
|
|
}
|
|
|
|
static void RideRatingsApplyBonusSheltered(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
RatingTuple subRating = ride_ratings_get_sheltered_ratings(ride);
|
|
RideRatingsAdd(
|
|
ratings, (subRating.excitement * modifier.excitement) >> 16, (subRating.intensity * modifier.intensity) >> 16,
|
|
(subRating.nausea * modifier.nausea) >> 16);
|
|
}
|
|
|
|
static void RideRatingsApplyBonusRotations(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
RideRatingsAdd(
|
|
ratings, ride.rotations * modifier.excitement, ride.rotations * modifier.intensity, ride.rotations * modifier.nausea);
|
|
}
|
|
|
|
static void RideRatingsApplyBonusOperationOption(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
int32_t intensity = (modifier.intensity >= 0) ? (ride.operation_option * modifier.intensity)
|
|
: (ride.operation_option / std::abs(modifier.intensity));
|
|
RideRatingsAdd(ratings, ride.operation_option * modifier.excitement, intensity, ride.operation_option * modifier.nausea);
|
|
}
|
|
|
|
static void RideRatingsApplyBonusReversedTrains(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
if (ride.HasLifecycleFlag(RIDE_LIFECYCLE_REVERSED_TRAINS))
|
|
{
|
|
RideRatingsAdd(
|
|
ratings, ((ratings.excitement * modifier.excitement) >> 7), (ratings.intensity * modifier.intensity) >> 7,
|
|
(ratings.nausea * modifier.nausea) >> 7);
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyBonusGoKartRace(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
if (ride.mode == RideMode::Race && ride.NumTrains >= modifier.threshold)
|
|
{
|
|
RideRatingsAdd(ratings, modifier.excitement, modifier.intensity, modifier.nausea);
|
|
|
|
int32_t lapsFactor = (ride.NumLaps - 1) * 30;
|
|
RideRatingsAdd(ratings, lapsFactor, lapsFactor / 2, 0);
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyBonusTowerRide(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
int32_t lengthFactor = ToHumanReadableRideLength(ride.GetTotalLength());
|
|
RideRatingsAdd(
|
|
ratings, (lengthFactor * modifier.excitement) >> 16, (lengthFactor * modifier.intensity) >> 16,
|
|
(lengthFactor * modifier.nausea) >> 16);
|
|
}
|
|
|
|
static void RideRatingsApplyBonusRotoDrop(RatingTuple& ratings, const Ride& ride)
|
|
{
|
|
int32_t lengthFactor = (ToHumanReadableRideLength(ride.GetTotalLength()) * 209715) >> 16;
|
|
RideRatingsAdd(ratings, lengthFactor, lengthFactor * 2, lengthFactor * 2);
|
|
}
|
|
|
|
static void RideRatingsApplyBonusMazeSize(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
int32_t size = std::min<uint16_t>(ride.maze_tiles, modifier.threshold);
|
|
RideRatingsAdd(ratings, size * modifier.excitement, size * modifier.intensity, size * modifier.nausea);
|
|
}
|
|
|
|
static void RideRatingsApplyBonusBoatHireNoCircuit(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
// Most likely checking if the ride has does not have a circuit
|
|
if (!(ride.lifecycle_flags & RIDE_LIFECYCLE_TESTED))
|
|
{
|
|
RideRatingsAdd(ratings, modifier.excitement, modifier.intensity, modifier.nausea);
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyBonusSlideUnlimitedRides(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
if (ride.mode == RideMode::UnlimitedRidesPerAdmission)
|
|
{
|
|
RideRatingsAdd(ratings, modifier.excitement, modifier.intensity, modifier.nausea);
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyBonusMotionSimulatorMode(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
// Hardcoded until ride mode refactor
|
|
if (ride.mode == RideMode::FilmThrillRiders)
|
|
{
|
|
RideRatingsSet(ratings, RIDE_RATING(3, 25), RIDE_RATING(4, 10), RIDE_RATING(3, 30));
|
|
}
|
|
else
|
|
{
|
|
RideRatingsSet(ratings, RIDE_RATING(2, 90), RIDE_RATING(3, 50), RIDE_RATING(3, 00));
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyBonus3DCinemaMode(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
// Hardcoded until ride mode refactor
|
|
switch (ride.mode)
|
|
{
|
|
default:
|
|
case RideMode::MouseTails3DFilm:
|
|
RideRatingsSet(ratings, RIDE_RATING(3, 50), RIDE_RATING(2, 40), RIDE_RATING(1, 40));
|
|
break;
|
|
case RideMode::StormChasers3DFilm:
|
|
RideRatingsSet(ratings, RIDE_RATING(4, 00), RIDE_RATING(2, 65), RIDE_RATING(1, 55));
|
|
break;
|
|
case RideMode::SpaceRaiders3DFilm:
|
|
RideRatingsSet(ratings, RIDE_RATING(4, 20), RIDE_RATING(2, 60), RIDE_RATING(1, 48));
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyBonusTopSpinMode(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
// Hardcoded until ride mode refactor
|
|
switch (ride.mode)
|
|
{
|
|
default:
|
|
case RideMode::Beginners:
|
|
RideRatingsSet(ratings, RIDE_RATING(2, 00), RIDE_RATING(4, 80), RIDE_RATING(5, 74));
|
|
break;
|
|
case RideMode::Intense:
|
|
RideRatingsSet(ratings, RIDE_RATING(3, 00), RIDE_RATING(5, 75), RIDE_RATING(6, 64));
|
|
break;
|
|
case RideMode::Berserk:
|
|
RideRatingsSet(ratings, RIDE_RATING(3, 20), RIDE_RATING(6, 80), RIDE_RATING(7, 94));
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyBonusReversals(
|
|
RatingTuple& ratings, const Ride& ride, RideRatingUpdateState& state, RatingsModifier modifier)
|
|
{
|
|
int32_t numReversers = std::min<uint16_t>(state.AmountOfReversers, modifier.threshold);
|
|
RideRatingsAdd(
|
|
ratings, numReversers * modifier.excitement, numReversers * modifier.intensity, numReversers * modifier.nausea);
|
|
}
|
|
|
|
static void RideRatingsApplyBonusHoles(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
RideRatingsAdd(
|
|
ratings, (ride.holes) * modifier.excitement, (ride.holes) * modifier.intensity, (ride.holes) * modifier.nausea);
|
|
}
|
|
|
|
static void RideRatingsApplyBonusNumTrains(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
// For some reason the original code ran this twice, before and after the operation option bonus
|
|
// Has been changed to call once with double value
|
|
if (ride.NumTrains >= modifier.threshold)
|
|
{
|
|
RideRatingsAdd(ratings, modifier.excitement, modifier.intensity, modifier.nausea);
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyBonusDownwardLaunch(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
if (ride.mode == RideMode::DownwardLaunch)
|
|
{
|
|
RideRatingsAdd(ratings, modifier.excitement, modifier.intensity, modifier.nausea);
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyBonusOperationOptionFreefall(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
RideRatingsAdd(
|
|
ratings, (ride.operation_option * modifier.excitement) >> 16, (ride.operation_option * modifier.intensity) >> 16,
|
|
(ride.operation_option * modifier.nausea) >> 16);
|
|
}
|
|
|
|
static void RideRatingsApplyBonusLaunchedFreefallSpecial(
|
|
RatingTuple& ratings, const Ride& ride, RideRatingUpdateState& state, RatingsModifier modifier)
|
|
{
|
|
int32_t excitement = (ToHumanReadableRideLength(ride.GetTotalLength()) * 32768) >> 16;
|
|
RideRatingsAdd(ratings, excitement, 0, 0);
|
|
|
|
#ifdef ORIGINAL_RATINGS
|
|
RideRatingsApplyBonusOperationOptionFreefall(ratings, ride, modifier);
|
|
#else
|
|
// Only apply "launch speed" effects when the setting can be modified
|
|
if (ride.mode == RideMode::UpwardLaunch)
|
|
{
|
|
RideRatingsApplyBonusOperationOptionFreefall(ratings, ride, modifier);
|
|
}
|
|
else
|
|
{
|
|
// Fix #3282: When the ride mode is in downward launch mode, the intensity and
|
|
// nausea were fixed regardless of how high the ride is. The following
|
|
// calculation is based on roto-drop which is a similar mechanic.
|
|
int32_t lengthFactor = (ToHumanReadableRideLength(ride.GetTotalLength()) * 209715) >> 16;
|
|
RideRatingsAdd(ratings, lengthFactor, lengthFactor * 2, lengthFactor * 2);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static void RideRatingsApplyBonusProximity(
|
|
RatingTuple& ratings, const Ride& ride, RideRatingUpdateState& state, RatingsModifier modifier)
|
|
{
|
|
RideRatingsAdd(ratings, (ride_ratings_get_proximity_score(state) * modifier.excitement) >> 16, 0, 0);
|
|
}
|
|
|
|
static void RideRatingsApplyBonusScenery(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
RideRatingsAdd(ratings, (ride_ratings_get_scenery_score(ride) * modifier.excitement) >> 16, 0, 0);
|
|
}
|
|
|
|
static void RideRatingsApplyRequirementLength(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
if (ride.GetStation().SegmentLength < modifier.threshold)
|
|
{
|
|
ratings.excitement /= modifier.excitement;
|
|
ratings.intensity /= modifier.intensity;
|
|
ratings.nausea /= modifier.nausea;
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyRequirementDropHeight(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
if (ride.highest_drop_height < modifier.threshold)
|
|
{
|
|
ratings.excitement /= modifier.excitement;
|
|
ratings.intensity /= modifier.intensity;
|
|
ratings.nausea /= modifier.nausea;
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyRequirementMaxSpeed(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
if (ride.max_speed < modifier.threshold)
|
|
{
|
|
ratings.excitement /= modifier.excitement;
|
|
ratings.intensity /= modifier.intensity;
|
|
ratings.nausea /= modifier.nausea;
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyRequirementNumDrops(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
if (ride.getNumDrops() < modifier.threshold)
|
|
{
|
|
ratings.excitement /= modifier.excitement;
|
|
ratings.intensity /= modifier.intensity;
|
|
ratings.nausea /= modifier.nausea;
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyRequirementNegativeGs(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
if (ride.max_negative_vertical_g >= modifier.threshold)
|
|
{
|
|
ratings.excitement /= modifier.excitement;
|
|
ratings.intensity /= modifier.intensity;
|
|
ratings.nausea /= modifier.nausea;
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyRequirementLateralGs(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
if (ride.max_lateral_g < modifier.threshold)
|
|
{
|
|
ratings.excitement /= modifier.excitement;
|
|
ratings.intensity /= modifier.intensity;
|
|
ratings.nausea /= modifier.nausea;
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyRequirementInversions(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
if (ride.inversions < modifier.threshold)
|
|
{
|
|
ratings.excitement /= modifier.excitement;
|
|
ratings.intensity /= modifier.intensity;
|
|
ratings.nausea /= modifier.nausea;
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyRequirementUnsheltered(
|
|
RatingTuple& ratings, const Ride& ride, uint8_t shelteredEighths, RatingsModifier modifier)
|
|
{
|
|
if (shelteredEighths >= modifier.threshold)
|
|
{
|
|
ratings.excitement /= modifier.excitement;
|
|
ratings.intensity /= modifier.intensity;
|
|
ratings.nausea /= modifier.nausea;
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyRequirementReversals(
|
|
RatingTuple& ratings, const Ride& ride, RideRatingUpdateState& state, RatingsModifier modifier)
|
|
{
|
|
if (state.AmountOfReversers < modifier.threshold)
|
|
{
|
|
ratings.excitement /= modifier.excitement;
|
|
ratings.intensity /= modifier.intensity;
|
|
ratings.nausea /= modifier.nausea;
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyRequirementHoles(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
if (ride.holes < modifier.threshold)
|
|
{
|
|
ratings.excitement /= modifier.excitement;
|
|
ratings.intensity /= modifier.intensity;
|
|
ratings.nausea /= modifier.nausea;
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyRequirementStations(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
if (ride.num_stations <= modifier.threshold)
|
|
{
|
|
// Excitement is set to 0 in original code - this could be changed for consistency
|
|
ratings.excitement = 0;
|
|
ratings.intensity /= modifier.intensity;
|
|
ratings.nausea /= modifier.nausea;
|
|
}
|
|
}
|
|
|
|
static void RideRatingsApplyRequirementSplashdown(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
if (!(ride.special_track_elements & RIDE_ELEMENT_TUNNEL_SPLASH_OR_RAPIDS))
|
|
{
|
|
ratings.excitement /= modifier.excitement;
|
|
ratings.intensity /= modifier.intensity;
|
|
ratings.nausea /= modifier.nausea;
|
|
}
|
|
}
|
|
|
|
#ifndef ORIGINAL_RATINGS
|
|
static RatingTuple ride_ratings_get_excessive_lateral_g_penalty(const Ride& ride)
|
|
{
|
|
RatingTuple result{};
|
|
if (ride.max_lateral_g > FIXED_2DP(2, 80))
|
|
{
|
|
result.intensity = FIXED_2DP(3, 75);
|
|
result.nausea = FIXED_2DP(2, 00);
|
|
}
|
|
|
|
if (ride.max_lateral_g > FIXED_2DP(3, 10))
|
|
{
|
|
// Remove half of the ride_ratings_get_gforce_ratings
|
|
result.excitement = (ride.max_positive_vertical_g * 5242) >> 16;
|
|
|
|
// Apply maximum negative G force factor
|
|
fixed16_2dp gforce = ride.max_negative_vertical_g;
|
|
result.excitement += (std::clamp<fixed16_2dp>(gforce, -FIXED_2DP(2, 50), FIXED_2DP(0, 00)) * -15728) >> 16;
|
|
|
|
// Apply lateral G force factor
|
|
result.excitement += (std::min<fixed16_2dp>(FIXED_2DP(1, 50), ride.max_lateral_g) * 26214) >> 16;
|
|
|
|
// Remove half of the ride_ratings_get_gforce_ratings
|
|
result.excitement /= 2;
|
|
result.excitement *= -1;
|
|
result.intensity = FIXED_2DP(12, 25);
|
|
result.nausea = FIXED_2DP(6, 00);
|
|
}
|
|
return result;
|
|
}
|
|
#endif
|
|
|
|
static void RideRatingsApplyPenaltyLateralGs(RatingTuple& ratings, const Ride& ride, RatingsModifier modifier)
|
|
{
|
|
#ifndef ORIGINAL_RATINGS
|
|
RatingTuple subRating = ride_ratings_get_excessive_lateral_g_penalty(ride);
|
|
RideRatingsAdd(
|
|
ratings, (subRating.excitement * modifier.excitement) >> 16, (subRating.intensity * modifier.intensity) >> 16,
|
|
(subRating.nausea * modifier.nausea) >> 16);
|
|
#endif
|
|
}
|
|
|
|
#pragma endregion
|
|
|
|
bool RatingTuple::isNull() const
|
|
{
|
|
return excitement == kRideRatingUndefined;
|
|
}
|
|
|
|
void RatingTuple::setNull()
|
|
{
|
|
excitement = kRideRatingUndefined;
|
|
}
|