mirror of https://github.com/OpenRCT2/OpenRCT2.git
7486 lines
218 KiB
C++
7486 lines
218 KiB
C++
/*****************************************************************************
|
|
* Copyright (c) 2014-2020 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 "Guest.h"
|
|
|
|
#include "../Context.h"
|
|
#include "../Game.h"
|
|
#include "../OpenRCT2.h"
|
|
#include "../audio/audio.h"
|
|
#include "../config/Config.h"
|
|
#include "../core/Guard.hpp"
|
|
#include "../core/Numerics.hpp"
|
|
#include "../interface/Window_internal.h"
|
|
#include "../localisation/Localisation.h"
|
|
#include "../management/Finance.h"
|
|
#include "../management/Marketing.h"
|
|
#include "../management/NewsItem.h"
|
|
#include "../network/network.h"
|
|
#include "../rct2/RCT2.h"
|
|
#include "../ride/Ride.h"
|
|
#include "../ride/RideData.h"
|
|
#include "../ride/ShopItem.h"
|
|
#include "../ride/Station.h"
|
|
#include "../ride/Track.h"
|
|
#include "../ride/Vehicle.h"
|
|
#include "../scenario/Scenario.h"
|
|
#include "../scripting/HookEngine.h"
|
|
#include "../scripting/ScriptEngine.h"
|
|
#include "../util/Math.hpp"
|
|
#include "../windows/Intent.h"
|
|
#include "../world/Balloon.h"
|
|
#include "../world/Climate.h"
|
|
#include "../world/Footpath.h"
|
|
#include "../world/LargeScenery.h"
|
|
#include "../world/Map.h"
|
|
#include "../world/MoneyEffect.h"
|
|
#include "../world/Park.h"
|
|
#include "../world/Particle.h"
|
|
#include "../world/Scenery.h"
|
|
#include "../world/Sprite.h"
|
|
#include "../world/Surface.h"
|
|
#include "../world/TileElementsView.h"
|
|
#include "GuestPathfinding.h"
|
|
#include "Peep.h"
|
|
#include "RideUseSystem.h"
|
|
#include "Staff.h"
|
|
|
|
#include <algorithm>
|
|
#include <functional>
|
|
#include <iterator>
|
|
|
|
using namespace OpenRCT2;
|
|
|
|
// Locations of the spiral slide platform that a peep walks from the entrance of the ride to the
|
|
// entrance of the slide. Up to 4 waypoints for each 4 sides that an ride entrance can be located
|
|
// and 4 different rotations of the ride. 4 * 4 * 4 = 64 locations.
|
|
// clang-format off
|
|
static constexpr const CoordsXY SpiralSlideWalkingPath[64] = {
|
|
{ 56, 8 },
|
|
{ 8, 8 },
|
|
{ 8, 32 },
|
|
{ 32, 32 },
|
|
{ 8, 8 },
|
|
{ 8, 8 },
|
|
{ 8, 32 },
|
|
{ 32, 32 },
|
|
{ 8, 32 },
|
|
{ 8, 32 },
|
|
{ 8, 32 },
|
|
{ 32, 32 },
|
|
{ 8, 56 },
|
|
{ 8, 32 },
|
|
{ 8, 32 },
|
|
{ 32, 32 },
|
|
{ 56, 24 },
|
|
{ 32, 24 },
|
|
{ 32, 24 },
|
|
{ 32, 0 },
|
|
{ 56, -24 },
|
|
{ 56, 24 },
|
|
{ 32, 24 },
|
|
{ 32, 0 },
|
|
{ 8, 24 },
|
|
{ 32, 24 },
|
|
{ 32, 24 },
|
|
{ 32, 0 },
|
|
{ 32, 24 },
|
|
{ 32, 24 },
|
|
{ 32, 24 },
|
|
{ 32, 0 },
|
|
{ 24, 0 },
|
|
{ 24, 0 },
|
|
{ 24, 0 },
|
|
{ 0, 0 },
|
|
{ 24, -24 },
|
|
{ 24, 0 },
|
|
{ 24, 0 },
|
|
{ 0, 0 },
|
|
{ -24, -24 },
|
|
{ 24, -24 },
|
|
{ 24, 0 },
|
|
{ 0, 0 },
|
|
{ 24, 24 },
|
|
{ 24, 0 },
|
|
{ 24, 0 },
|
|
{ 0, 0 },
|
|
{ 24, 8 },
|
|
{ 0, 8 },
|
|
{ 0, 8 },
|
|
{ 0, 32 },
|
|
{ 0, 8 },
|
|
{ 0, 8 },
|
|
{ 0, 8 },
|
|
{ 0, 32 },
|
|
{ -24, 8 },
|
|
{ 0, 8 },
|
|
{ 0, 8 },
|
|
{ 0, 32 },
|
|
{ -24, 56 },
|
|
{ -24, 8 },
|
|
{ 0, 8 },
|
|
{ 0, 32 },
|
|
};
|
|
|
|
/** rct2: 0x00981F4C, 0x00981F4E */
|
|
static constexpr const CoordsXY _WatchingPositionOffsets[] = {
|
|
{ 7, 5 },
|
|
{ 5, 25 },
|
|
{ 25, 5 },
|
|
{ 5, 7 },
|
|
{ 7, 9 },
|
|
{ 9, 25 },
|
|
{ 25, 9 },
|
|
{ 9, 7 },
|
|
{ 7, 23 },
|
|
{ 23, 25 },
|
|
{ 25, 23 },
|
|
{ 23, 7 },
|
|
{ 7, 27 },
|
|
{ 27, 25 },
|
|
{ 25, 27 },
|
|
{ 27, 7 },
|
|
{ 7, 0 },
|
|
{ 0, 25 },
|
|
{ 25, 0 },
|
|
{ 0, 7 },
|
|
{ 7, 0 },
|
|
{ 0, 25 },
|
|
{ 25, 0 },
|
|
{ 0, 7 },
|
|
{ 7, 0 },
|
|
{ 0, 25 },
|
|
{ 25, 0 },
|
|
{ 0, 7 },
|
|
{ 7, 0 },
|
|
{ 0, 25 },
|
|
{ 25, 0 },
|
|
{ 0, 7 },
|
|
};
|
|
|
|
static constexpr const ride_rating NauseaMaximumThresholds[] = {
|
|
300,
|
|
600,
|
|
800,
|
|
1000,
|
|
};
|
|
|
|
/** rct2: 009823AC */
|
|
static constexpr const PeepThoughtType crowded_thoughts[] = {
|
|
PeepThoughtType::Lost,
|
|
PeepThoughtType::Tired,
|
|
PeepThoughtType::BadLitter,
|
|
PeepThoughtType::Hungry,
|
|
PeepThoughtType::Thirsty,
|
|
PeepThoughtType::VeryClean,
|
|
PeepThoughtType::Crowded,
|
|
PeepThoughtType::Scenery,
|
|
PeepThoughtType::VeryClean,
|
|
PeepThoughtType::Music,
|
|
PeepThoughtType::Watched,
|
|
PeepThoughtType::NotHungry,
|
|
PeepThoughtType::NotThirsty,
|
|
PeepThoughtType::Toilet,
|
|
PeepThoughtType::None,
|
|
PeepThoughtType::None,
|
|
};
|
|
|
|
static constexpr const char *gPeepEasterEggNames[] = {
|
|
"MICHAEL SCHUMACHER",
|
|
"JACQUES VILLENEUVE",
|
|
"DAMON HILL",
|
|
"MR BEAN",
|
|
"CHRIS SAWYER",
|
|
"KATIE BRAYSHAW",
|
|
"MELANIE WARN",
|
|
"SIMON FOSTER",
|
|
"JOHN WARDLEY",
|
|
"LISA STIRLING",
|
|
"DONALD MACRAE",
|
|
"KATHERINE MCGOWAN",
|
|
"FRANCES MCGOWAN",
|
|
"CORINA MASSOURA",
|
|
"CAROL YOUNG",
|
|
"MIA SHERIDAN",
|
|
"KATIE RODGER",
|
|
"EMMA GARRELL",
|
|
"JOANNE BARTON",
|
|
"FELICITY ANDERSON",
|
|
"KATIE SMITH",
|
|
"EILIDH BELL",
|
|
"NANCY STILLWAGON",
|
|
"DAVID ELLIS",
|
|
};
|
|
// clang-format on
|
|
|
|
// Flags used by PeepThoughtToActionMap
|
|
enum PeepThoughtToActionFlag : uint8_t
|
|
{
|
|
PEEP_THOUGHT_ACTION_NO_FLAGS = 0,
|
|
PEEP_THOUGHT_ACTION_FLAG_RIDE = (1 << 0),
|
|
PEEP_THOUGHT_ACTION_FLAG_SHOP_ITEM_SINGULAR = (1 << 1),
|
|
PEEP_THOUGHT_ACTION_FLAG_SHOP_ITEM_INDEFINITE = (1 << 2),
|
|
};
|
|
|
|
/** rct2: 0x00981DB0 */
|
|
static struct
|
|
{
|
|
PeepActionType action;
|
|
PeepThoughtToActionFlag flags;
|
|
} PeepThoughtToActionMap[] = {
|
|
{ PeepActionType::ShakeHead, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::EmptyPockets, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Wow, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_SHOP_ITEM_SINGULAR },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_SHOP_ITEM_INDEFINITE },
|
|
{ PeepActionType::ShakeHead, PEEP_THOUGHT_ACTION_FLAG_SHOP_ITEM_INDEFINITE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Wave, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Joy, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::CheckTime, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Wave, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Wave, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Disgust, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::BeingWatched, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::ShakeHead, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Joy, PEEP_THOUGHT_ACTION_NO_FLAGS },
|
|
{ PeepActionType::Walking, PEEP_THOUGHT_ACTION_FLAG_RIDE },
|
|
};
|
|
|
|
// These arrays contain the base minimum and maximum nausea ratings for peeps, based on their nausea tolerance level.
|
|
static constexpr const ride_rating NauseaMinimumThresholds[] = {
|
|
0,
|
|
0,
|
|
200,
|
|
400,
|
|
};
|
|
|
|
static bool peep_has_voucher_for_free_ride(Guest* peep, Ride* ride);
|
|
static void peep_ride_is_too_intense(Guest* peep, Ride* ride, bool peepAtRide);
|
|
static void peep_reset_ride_heading(Guest* peep);
|
|
static void peep_tried_to_enter_full_queue(Guest* peep, Ride* ride);
|
|
static int16_t peep_calculate_ride_satisfaction(Guest* peep, Ride* ride);
|
|
static void peep_update_favourite_ride(Guest* peep, Ride* ride);
|
|
static int16_t peep_calculate_ride_value_satisfaction(Guest* peep, Ride* ride);
|
|
static int16_t peep_calculate_ride_intensity_nausea_satisfaction(Guest* peep, Ride* ride);
|
|
static void peep_update_ride_nausea_growth(Guest* peep, Ride* ride);
|
|
static bool peep_should_go_on_ride_again(Guest* peep, Ride* ride);
|
|
static bool peep_should_preferred_intensity_increase(Guest* peep);
|
|
static bool peep_really_liked_ride(Guest* peep, Ride* ride);
|
|
static PeepThoughtType peep_assess_surroundings(int16_t centre_x, int16_t centre_y, int16_t centre_z);
|
|
static void peep_update_hunger(Guest* peep);
|
|
static void peep_decide_whether_to_leave_park(Guest* peep);
|
|
static void peep_leave_park(Guest* peep);
|
|
static void peep_head_for_nearest_ride_type(Guest* peep, int32_t rideType);
|
|
static void peep_head_for_nearest_ride_with_flags(Guest* peep, int32_t rideTypeFlags);
|
|
bool loc_690FD0(Peep* peep, ride_id_t* rideToView, uint8_t* rideSeatToView, TileElement* tileElement);
|
|
|
|
template<> bool EntityBase::Is<Guest>() const
|
|
{
|
|
return Type == EntityType::Guest;
|
|
}
|
|
|
|
static bool IsValidLocation(const CoordsXYZ& coords)
|
|
{
|
|
if (coords.x != LOCATION_NULL)
|
|
{
|
|
if (map_is_location_valid(coords))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
template<void (Guest::*EasterEggFunc)(Guest*), bool applyToSelf> static void ApplyEasterEggToNearbyGuests(Guest* guest)
|
|
{
|
|
const auto guestLoc = guest->GetLocation();
|
|
if (!IsValidLocation(guestLoc))
|
|
return;
|
|
|
|
for (auto* otherGuest : EntityTileList<Guest>(guestLoc))
|
|
{
|
|
if constexpr (!applyToSelf)
|
|
{
|
|
if (otherGuest == guest)
|
|
{
|
|
// Can not apply effect on self.
|
|
continue;
|
|
}
|
|
}
|
|
auto zDiff = std::abs(otherGuest->z - guestLoc.z);
|
|
if (zDiff <= 32)
|
|
{
|
|
std::invoke(EasterEggFunc, *guest, otherGuest);
|
|
}
|
|
}
|
|
}
|
|
|
|
void Guest::GivePassingPeepsPurpleClothes(Guest* passingPeep)
|
|
{
|
|
passingPeep->TshirtColour = COLOUR_BRIGHT_PURPLE;
|
|
passingPeep->TrousersColour = COLOUR_BRIGHT_PURPLE;
|
|
passingPeep->Invalidate();
|
|
}
|
|
|
|
void Guest::GivePassingPeepsPizza(Guest* passingPeep)
|
|
{
|
|
if (passingPeep->HasItem(ShopItem::Pizza))
|
|
return;
|
|
|
|
passingPeep->GiveItem(ShopItem::Pizza);
|
|
|
|
int32_t peepDirection = (sprite_direction >> 3) ^ 2;
|
|
int32_t otherPeepOppositeDirection = passingPeep->sprite_direction >> 3;
|
|
if (peepDirection == otherPeepOppositeDirection)
|
|
{
|
|
if (passingPeep->IsActionInterruptable())
|
|
{
|
|
passingPeep->Action = PeepActionType::Wave2;
|
|
passingPeep->ActionFrame = 0;
|
|
passingPeep->ActionSpriteImageOffset = 0;
|
|
passingPeep->UpdateCurrentActionSpriteType();
|
|
}
|
|
}
|
|
}
|
|
|
|
void Guest::MakePassingPeepsSick(Guest* passingPeep)
|
|
{
|
|
if (passingPeep->State != PeepState::Walking)
|
|
return;
|
|
|
|
if (passingPeep->IsActionInterruptable())
|
|
{
|
|
passingPeep->Action = PeepActionType::ThrowUp;
|
|
passingPeep->ActionFrame = 0;
|
|
passingPeep->ActionSpriteImageOffset = 0;
|
|
passingPeep->UpdateCurrentActionSpriteType();
|
|
}
|
|
}
|
|
|
|
void Guest::GivePassingPeepsIceCream(Guest* passingPeep)
|
|
{
|
|
if (passingPeep->HasItem(ShopItem::IceCream))
|
|
return;
|
|
|
|
passingPeep->GiveItem(ShopItem::IceCream);
|
|
passingPeep->UpdateSpriteType();
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0068FD3A
|
|
*/
|
|
void Guest::UpdateEasterEggInteractions()
|
|
{
|
|
if (PeepFlags & PEEP_FLAGS_PURPLE)
|
|
{
|
|
ApplyEasterEggToNearbyGuests<&Guest::GivePassingPeepsPurpleClothes, true>(this);
|
|
}
|
|
|
|
if (PeepFlags & PEEP_FLAGS_PIZZA)
|
|
{
|
|
ApplyEasterEggToNearbyGuests<&Guest::GivePassingPeepsPizza, true>(this);
|
|
}
|
|
|
|
if (PeepFlags & PEEP_FLAGS_CONTAGIOUS)
|
|
{
|
|
ApplyEasterEggToNearbyGuests<&Guest::MakePassingPeepsSick, false>(this);
|
|
}
|
|
|
|
if (PeepFlags & PEEP_FLAGS_ICE_CREAM)
|
|
{
|
|
ApplyEasterEggToNearbyGuests<&Guest::GivePassingPeepsIceCream, false>(this);
|
|
}
|
|
|
|
if (PeepFlags & PEEP_FLAGS_JOY)
|
|
{
|
|
if ((scenario_rand() & 0xFFFF) <= 1456)
|
|
{
|
|
if (IsActionInterruptable())
|
|
{
|
|
Action = PeepActionType::Joy;
|
|
ActionFrame = 0;
|
|
ActionSpriteImageOffset = 0;
|
|
UpdateCurrentActionSpriteType();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int32_t Guest::GetEasterEggNameId() const
|
|
{
|
|
char buffer[256]{};
|
|
|
|
Formatter ft;
|
|
FormatNameTo(ft);
|
|
format_string(buffer, sizeof(buffer), STR_STRINGID, ft.Data());
|
|
|
|
for (uint32_t i = 0; i < std::size(gPeepEasterEggNames); i++)
|
|
if (_stricmp(buffer, gPeepEasterEggNames[i]) == 0)
|
|
return static_cast<int32_t>(i);
|
|
|
|
return -1;
|
|
}
|
|
|
|
void Guest::HandleEasterEggName()
|
|
{
|
|
PeepFlags &= ~PEEP_FLAGS_WAVING;
|
|
if (CheckEasterEggName(EASTEREGG_PEEP_NAME_KATIE_BRAYSHAW))
|
|
{
|
|
PeepFlags |= PEEP_FLAGS_WAVING;
|
|
}
|
|
|
|
PeepFlags &= ~PEEP_FLAGS_PHOTO;
|
|
if (CheckEasterEggName(EASTEREGG_PEEP_NAME_CHRIS_SAWYER))
|
|
{
|
|
PeepFlags |= PEEP_FLAGS_PHOTO;
|
|
}
|
|
|
|
PeepFlags &= ~PEEP_FLAGS_PAINTING;
|
|
if (CheckEasterEggName(EASTEREGG_PEEP_NAME_SIMON_FOSTER))
|
|
{
|
|
PeepFlags |= PEEP_FLAGS_PAINTING;
|
|
}
|
|
|
|
PeepFlags &= ~PEEP_FLAGS_WOW;
|
|
if (CheckEasterEggName(EASTEREGG_PEEP_NAME_JOHN_WARDLEY))
|
|
{
|
|
PeepFlags |= PEEP_FLAGS_WOW;
|
|
}
|
|
|
|
if (CheckEasterEggName(EASTEREGG_PEEP_NAME_MELANIE_WARN))
|
|
{
|
|
Happiness = 250;
|
|
HappinessTarget = 250;
|
|
Energy = 127;
|
|
EnergyTarget = 127;
|
|
Nausea = 0;
|
|
NauseaTarget = 0;
|
|
}
|
|
|
|
PeepFlags &= ~PEEP_FLAGS_LITTER;
|
|
if (CheckEasterEggName(EASTEREGG_PEEP_NAME_LISA_STIRLING))
|
|
{
|
|
PeepFlags |= PEEP_FLAGS_LITTER;
|
|
}
|
|
|
|
PeepFlags &= ~PEEP_FLAGS_LOST;
|
|
if (CheckEasterEggName(EASTEREGG_PEEP_NAME_DONALD_MACRAE))
|
|
{
|
|
PeepFlags |= PEEP_FLAGS_LOST;
|
|
}
|
|
|
|
PeepFlags &= ~PEEP_FLAGS_HUNGER;
|
|
if (CheckEasterEggName(EASTEREGG_PEEP_NAME_KATHERINE_MCGOWAN))
|
|
{
|
|
PeepFlags |= PEEP_FLAGS_HUNGER;
|
|
}
|
|
|
|
PeepFlags &= ~PEEP_FLAGS_TOILET;
|
|
if (CheckEasterEggName(EASTEREGG_PEEP_NAME_FRANCES_MCGOWAN))
|
|
{
|
|
PeepFlags |= PEEP_FLAGS_TOILET;
|
|
}
|
|
|
|
PeepFlags &= ~PEEP_FLAGS_CROWDED;
|
|
if (CheckEasterEggName(EASTEREGG_PEEP_NAME_CORINA_MASSOURA))
|
|
{
|
|
PeepFlags |= PEEP_FLAGS_CROWDED;
|
|
}
|
|
|
|
PeepFlags &= ~PEEP_FLAGS_HAPPINESS;
|
|
if (CheckEasterEggName(EASTEREGG_PEEP_NAME_CAROL_YOUNG))
|
|
{
|
|
PeepFlags |= PEEP_FLAGS_HAPPINESS;
|
|
}
|
|
|
|
PeepFlags &= ~PEEP_FLAGS_NAUSEA;
|
|
if (CheckEasterEggName(EASTEREGG_PEEP_NAME_MIA_SHERIDAN))
|
|
{
|
|
PeepFlags |= PEEP_FLAGS_NAUSEA;
|
|
}
|
|
|
|
if (CheckEasterEggName(EASTEREGG_PEEP_NAME_KATIE_RODGER))
|
|
{
|
|
PeepFlags |= PEEP_FLAGS_LEAVING_PARK;
|
|
PeepFlags &= ~PEEP_FLAGS_PARK_ENTRANCE_CHOSEN;
|
|
}
|
|
|
|
PeepFlags &= ~PEEP_FLAGS_PURPLE;
|
|
if (CheckEasterEggName(EASTEREGG_PEEP_NAME_EMMA_GARRELL))
|
|
{
|
|
PeepFlags |= PEEP_FLAGS_PURPLE;
|
|
}
|
|
|
|
PeepFlags &= ~PEEP_FLAGS_PIZZA;
|
|
if (CheckEasterEggName(EASTEREGG_PEEP_NAME_JOANNE_BARTON))
|
|
{
|
|
PeepFlags |= PEEP_FLAGS_PIZZA;
|
|
}
|
|
|
|
PeepFlags &= ~PEEP_FLAGS_CONTAGIOUS;
|
|
if (CheckEasterEggName(EASTEREGG_PEEP_NAME_FELICITY_ANDERSON))
|
|
{
|
|
PeepFlags |= PEEP_FLAGS_CONTAGIOUS;
|
|
}
|
|
|
|
PeepFlags &= ~PEEP_FLAGS_JOY;
|
|
if (CheckEasterEggName(EASTEREGG_PEEP_NAME_KATIE_SMITH))
|
|
{
|
|
PeepFlags |= PEEP_FLAGS_JOY;
|
|
}
|
|
|
|
PeepFlags &= ~PEEP_FLAGS_ANGRY;
|
|
if (CheckEasterEggName(EASTEREGG_PEEP_NAME_EILIDH_BELL))
|
|
{
|
|
PeepFlags |= PEEP_FLAGS_ANGRY;
|
|
}
|
|
|
|
PeepFlags &= ~PEEP_FLAGS_ICE_CREAM;
|
|
if (CheckEasterEggName(EASTEREGG_PEEP_NAME_NANCY_STILLWAGON))
|
|
{
|
|
PeepFlags |= PEEP_FLAGS_ICE_CREAM;
|
|
}
|
|
|
|
PeepFlags &= ~PEEP_FLAGS_HERE_WE_ARE;
|
|
if (CheckEasterEggName(EASTEREGG_PEEP_NAME_DAVID_ELLIS))
|
|
{
|
|
PeepFlags |= PEEP_FLAGS_HERE_WE_ARE;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069A5A0
|
|
* tests if a peep's name matches a cheat code, normally returns using a register flag
|
|
*/
|
|
int32_t Guest::CheckEasterEggName(int32_t index) const
|
|
{
|
|
char buffer[256]{};
|
|
|
|
Formatter ft;
|
|
FormatNameTo(ft);
|
|
format_string(buffer, sizeof(buffer), STR_STRINGID, ft.Data());
|
|
|
|
return _stricmp(buffer, gPeepEasterEggNames[index]) == 0;
|
|
}
|
|
|
|
void Guest::loc_68F9F3()
|
|
{
|
|
// Idle peep happiness tends towards 127 (50%).
|
|
if (HappinessTarget >= 128)
|
|
HappinessTarget--;
|
|
else
|
|
HappinessTarget++;
|
|
|
|
NauseaTarget = std::max(NauseaTarget - 2, 0);
|
|
|
|
if (Energy <= 50)
|
|
{
|
|
Energy = std::max(Energy - 2, 0);
|
|
}
|
|
|
|
if (Hunger < 10)
|
|
{
|
|
Hunger = std::max(Hunger - 1, 0);
|
|
}
|
|
|
|
if (Thirst < 10)
|
|
{
|
|
Thirst = std::max(Thirst - 1, 0);
|
|
}
|
|
|
|
if (Toilet >= 195)
|
|
{
|
|
Toilet--;
|
|
}
|
|
|
|
if (State == PeepState::Walking && NauseaTarget >= 128)
|
|
{
|
|
if ((scenario_rand() & 0xFF) <= static_cast<uint8_t>((Nausea - 128) / 2))
|
|
{
|
|
if (IsActionInterruptable())
|
|
{
|
|
Action = PeepActionType::ThrowUp;
|
|
ActionFrame = 0;
|
|
ActionSpriteImageOffset = 0;
|
|
UpdateCurrentActionSpriteType();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Guest::loc_68FA89()
|
|
{
|
|
// 68FA89
|
|
if (TimeToConsume == 0 && HasFoodOrDrink())
|
|
{
|
|
TimeToConsume += 3;
|
|
}
|
|
|
|
if (TimeToConsume != 0 && State != PeepState::OnRide)
|
|
{
|
|
TimeToConsume = std::max(TimeToConsume - 3, 0);
|
|
|
|
if (HasDrink())
|
|
{
|
|
Thirst = std::min(Thirst + 7, 255);
|
|
}
|
|
else
|
|
{
|
|
Hunger = std::min(Hunger + 7, 255);
|
|
Thirst = std::max(Thirst - 3, 0);
|
|
Toilet = std::min(Toilet + 2, 255);
|
|
}
|
|
|
|
if (TimeToConsume == 0)
|
|
{
|
|
int32_t chosen_food = bitscanforward(GetFoodOrDrinkFlags());
|
|
if (chosen_food != -1)
|
|
{
|
|
ShopItem food = ShopItem(chosen_food);
|
|
RemoveItem(food);
|
|
|
|
auto discardContainer = GetShopItemDescriptor(food).DiscardContainer;
|
|
if (discardContainer != ShopItem::None)
|
|
{
|
|
GiveItem(discardContainer);
|
|
}
|
|
|
|
WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_INVENTORY;
|
|
UpdateSpriteType();
|
|
}
|
|
}
|
|
}
|
|
|
|
uint8_t newEnergy = Energy;
|
|
uint8_t newTargetEnergy = EnergyTarget;
|
|
if (newEnergy >= newTargetEnergy)
|
|
{
|
|
newEnergy -= 2;
|
|
if (newEnergy < newTargetEnergy)
|
|
newEnergy = newTargetEnergy;
|
|
}
|
|
else
|
|
{
|
|
newEnergy = std::min(PEEP_MAX_ENERGY_TARGET, newEnergy + 4);
|
|
if (newEnergy > newTargetEnergy)
|
|
newEnergy = newTargetEnergy;
|
|
}
|
|
|
|
if (newEnergy < PEEP_MIN_ENERGY)
|
|
newEnergy = PEEP_MIN_ENERGY;
|
|
|
|
/* Previous code here suggested maximum energy is 128. */
|
|
newEnergy = std::min(static_cast<uint8_t>(PEEP_MAX_ENERGY), newEnergy);
|
|
|
|
if (newEnergy != Energy)
|
|
{
|
|
Energy = newEnergy;
|
|
WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_2;
|
|
}
|
|
|
|
uint8_t newHappiness = Happiness;
|
|
uint8_t newHappinessGrowth = HappinessTarget;
|
|
if (newHappiness >= newHappinessGrowth)
|
|
{
|
|
newHappiness = std::max(newHappiness - 4, 0);
|
|
if (newHappiness < newHappinessGrowth)
|
|
newHappiness = newHappinessGrowth;
|
|
}
|
|
else
|
|
{
|
|
newHappiness = std::min(255, newHappiness + 4);
|
|
if (newHappiness > newHappinessGrowth)
|
|
newHappiness = newHappinessGrowth;
|
|
}
|
|
|
|
if (newHappiness != Happiness)
|
|
{
|
|
Happiness = newHappiness;
|
|
WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_2;
|
|
}
|
|
|
|
uint8_t newNausea = Nausea;
|
|
uint8_t newNauseaGrowth = NauseaTarget;
|
|
if (newNausea >= newNauseaGrowth)
|
|
{
|
|
newNausea = std::max(newNausea - 4, 0);
|
|
if (newNausea < newNauseaGrowth)
|
|
newNausea = newNauseaGrowth;
|
|
}
|
|
else
|
|
{
|
|
newNausea = std::min(255, newNausea + 4);
|
|
if (newNausea > newNauseaGrowth)
|
|
newNausea = newNauseaGrowth;
|
|
}
|
|
|
|
if (newNausea != Nausea)
|
|
{
|
|
Nausea = newNausea;
|
|
WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_2;
|
|
}
|
|
}
|
|
|
|
void Guest::Tick128UpdateGuest(int32_t index)
|
|
{
|
|
if (static_cast<uint32_t>(index & 0x1FF) == (gCurrentTicks & 0x1FF))
|
|
{
|
|
/* Effect of masking with 0x1FF here vs mask 0x7F,
|
|
* which is the condition for calling this function, is
|
|
* to reduce how often the content in this conditional
|
|
* is executed to once every four calls. */
|
|
if (PeepFlags & PEEP_FLAGS_CROWDED)
|
|
{
|
|
PeepThoughtType thought_type = crowded_thoughts[scenario_rand() & 0xF];
|
|
if (thought_type != PeepThoughtType::None)
|
|
{
|
|
InsertNewThought(thought_type);
|
|
}
|
|
}
|
|
|
|
if (PeepFlags & PEEP_FLAGS_EXPLODE && x != LOCATION_NULL)
|
|
{
|
|
if (State == PeepState::Walking || State == PeepState::Sitting)
|
|
{
|
|
OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::Crash, GetLocation());
|
|
|
|
ExplosionCloud::Create({ x, y, z + 16 });
|
|
ExplosionFlare::Create({ x, y, z + 16 });
|
|
|
|
Remove();
|
|
return;
|
|
}
|
|
|
|
PeepFlags &= ~PEEP_FLAGS_EXPLODE;
|
|
}
|
|
|
|
if (PeepFlags & PEEP_FLAGS_HUNGER)
|
|
{
|
|
if (Hunger >= 15)
|
|
Hunger -= 15;
|
|
}
|
|
|
|
if (PeepFlags & PEEP_FLAGS_TOILET)
|
|
{
|
|
if (Toilet <= 180)
|
|
Toilet += 50;
|
|
}
|
|
|
|
if (PeepFlags & PEEP_FLAGS_HAPPINESS)
|
|
{
|
|
HappinessTarget = 5;
|
|
}
|
|
|
|
if (PeepFlags & PEEP_FLAGS_NAUSEA)
|
|
{
|
|
NauseaTarget = 200;
|
|
if (Nausea <= 130)
|
|
Nausea = 130;
|
|
}
|
|
|
|
if (Angriness != 0)
|
|
Angriness--;
|
|
|
|
if (State == PeepState::Walking || State == PeepState::Sitting)
|
|
{
|
|
SurroundingsThoughtTimeout++;
|
|
if (SurroundingsThoughtTimeout >= 18)
|
|
{
|
|
SurroundingsThoughtTimeout = 0;
|
|
if (x != LOCATION_NULL)
|
|
{
|
|
PeepThoughtType thought_type = peep_assess_surroundings(x & 0xFFE0, y & 0xFFE0, z);
|
|
|
|
if (thought_type != PeepThoughtType::None)
|
|
{
|
|
InsertNewThought(thought_type);
|
|
HappinessTarget = std::min(PEEP_MAX_HAPPINESS, HappinessTarget + 45);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
UpdateSpriteType();
|
|
|
|
if (State == PeepState::OnRide || State == PeepState::EnteringRide)
|
|
{
|
|
GuestTimeOnRide = std::min(255, GuestTimeOnRide + 1);
|
|
|
|
if (PeepFlags & PEEP_FLAGS_WOW)
|
|
{
|
|
InsertNewThought(PeepThoughtType::Wow2);
|
|
}
|
|
|
|
if (GuestTimeOnRide > 15)
|
|
{
|
|
HappinessTarget = std::max(0, HappinessTarget - 5);
|
|
|
|
if (GuestTimeOnRide > 22)
|
|
{
|
|
auto ride = get_ride(CurrentRide);
|
|
if (ride != nullptr)
|
|
{
|
|
PeepThoughtType thought_type = ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_IN_RIDE)
|
|
? PeepThoughtType::GetOut
|
|
: PeepThoughtType::GetOff;
|
|
|
|
InsertNewThought(thought_type, CurrentRide);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (State == PeepState::Walking && !OutsideOfPark && !(PeepFlags & PEEP_FLAGS_LEAVING_PARK) && GuestNumRides == 0
|
|
&& GuestHeadingToRideId == RIDE_ID_NULL)
|
|
{
|
|
uint32_t time_duration = gCurrentTicks - ParkEntryTime;
|
|
time_duration /= 2048;
|
|
|
|
if (time_duration >= 5)
|
|
{
|
|
PickRideToGoOn();
|
|
|
|
if (GuestHeadingToRideId == RIDE_ID_NULL)
|
|
{
|
|
HappinessTarget = std::max(HappinessTarget - 128, 0);
|
|
peep_leave_park(this);
|
|
peep_update_hunger(this);
|
|
loc_68F9F3();
|
|
loc_68FA89();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ((scenario_rand() & 0xFFFF) <= ((HasItem(ShopItem::Map)) ? 8192U : 2184U))
|
|
{
|
|
PickRideToGoOn();
|
|
}
|
|
|
|
if (static_cast<uint32_t>(index & 0x3FF) == (gCurrentTicks & 0x3FF))
|
|
{
|
|
/* Effect of masking with 0x3FF here vs mask 0x1FF,
|
|
* which is used in the encompassing conditional, is
|
|
* to reduce how often the content in this conditional
|
|
* is executed to once every second time the encompassing
|
|
* conditional executes. */
|
|
|
|
if (!OutsideOfPark && (State == PeepState::Walking || State == PeepState::Sitting))
|
|
{
|
|
uint8_t num_thoughts = 0;
|
|
PeepThoughtType possible_thoughts[5];
|
|
|
|
if (PeepFlags & PEEP_FLAGS_LEAVING_PARK)
|
|
{
|
|
possible_thoughts[num_thoughts++] = PeepThoughtType::GoHome;
|
|
}
|
|
else
|
|
{
|
|
if (Energy <= 70 && Happiness < 128)
|
|
{
|
|
possible_thoughts[num_thoughts++] = PeepThoughtType::Tired;
|
|
}
|
|
|
|
if (Hunger <= 10 && !HasFoodOrDrink())
|
|
{
|
|
possible_thoughts[num_thoughts++] = PeepThoughtType::Hungry;
|
|
}
|
|
|
|
if (Thirst <= 25 && !HasFoodOrDrink())
|
|
{
|
|
possible_thoughts[num_thoughts++] = PeepThoughtType::Thirsty;
|
|
}
|
|
|
|
if (Toilet >= 160)
|
|
{
|
|
possible_thoughts[num_thoughts++] = PeepThoughtType::Toilet;
|
|
}
|
|
|
|
if (!(gParkFlags & PARK_FLAGS_NO_MONEY) && CashInPocket <= MONEY(9, 00) && Happiness >= 105 && Energy >= 70)
|
|
{
|
|
/* The energy check was originally a second check on happiness.
|
|
* This was superfluous so should probably check something else.
|
|
* Guessed that this should really be checking energy, since
|
|
* the addresses for happiness and energy are quite close,
|
|
* 70 is also the threshold for tired thoughts (see above) and
|
|
* it makes sense that a tired peep might not think about getting
|
|
* more money. */
|
|
possible_thoughts[num_thoughts++] = PeepThoughtType::RunningOut;
|
|
}
|
|
}
|
|
|
|
if (num_thoughts != 0)
|
|
{
|
|
PeepThoughtType chosen_thought = possible_thoughts[scenario_rand() % num_thoughts];
|
|
|
|
InsertNewThought(chosen_thought);
|
|
|
|
switch (chosen_thought)
|
|
{
|
|
case PeepThoughtType::Hungry:
|
|
peep_head_for_nearest_ride_with_flags(this, RIDE_TYPE_FLAG_SELLS_FOOD);
|
|
break;
|
|
case PeepThoughtType::Thirsty:
|
|
peep_head_for_nearest_ride_with_flags(this, RIDE_TYPE_FLAG_SELLS_DRINKS);
|
|
break;
|
|
case PeepThoughtType::Toilet:
|
|
peep_head_for_nearest_ride_with_flags(this, RIDE_TYPE_FLAG_IS_TOILET);
|
|
break;
|
|
case PeepThoughtType::RunningOut:
|
|
peep_head_for_nearest_ride_type(this, RIDE_TYPE_CASH_MACHINE);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* This branch of the conditional is executed on the
|
|
* remaining times the encompassing conditional is
|
|
* executed (which is also every second time, but
|
|
* the alternate time to the true branch). */
|
|
if (Nausea >= 140)
|
|
{
|
|
PeepThoughtType thought_type = PeepThoughtType::Sick;
|
|
if (Nausea >= 200)
|
|
{
|
|
thought_type = PeepThoughtType::VerySick;
|
|
peep_head_for_nearest_ride_type(this, RIDE_TYPE_FIRST_AID);
|
|
}
|
|
InsertNewThought(thought_type);
|
|
}
|
|
}
|
|
|
|
switch (State)
|
|
{
|
|
case PeepState::Walking:
|
|
case PeepState::LeavingPark:
|
|
case PeepState::EnteringPark:
|
|
peep_decide_whether_to_leave_park(this);
|
|
peep_update_hunger(this);
|
|
break;
|
|
|
|
case PeepState::Sitting:
|
|
if (EnergyTarget <= 135)
|
|
EnergyTarget += 5;
|
|
|
|
if (Thirst >= 5)
|
|
{
|
|
Thirst -= 4;
|
|
Toilet = std::min(255, Toilet + 3);
|
|
}
|
|
|
|
if (NauseaTarget >= 50)
|
|
NauseaTarget -= 6;
|
|
|
|
// In the original this branched differently
|
|
// but it would mean setting the peep happiness from
|
|
// a thought type entry which i think is incorrect.
|
|
peep_update_hunger(this);
|
|
break;
|
|
|
|
case PeepState::Queuing:
|
|
if (TimeInQueue >= 2000)
|
|
{
|
|
/* Peep happiness is affected once the peep has been waiting
|
|
* too long in a queue. */
|
|
bool found = false;
|
|
for (auto* pathElement : TileElementsView<PathElement>(NextLoc))
|
|
{
|
|
if (pathElement->GetBaseZ() != NextLoc.z)
|
|
continue;
|
|
|
|
// Check if the footpath has a queue line TV monitor on it
|
|
if (pathElement->HasAddition() && !pathElement->AdditionIsGhost())
|
|
{
|
|
auto* pathAddEntry = pathElement->GetAdditionEntry();
|
|
if (pathAddEntry != nullptr && (pathAddEntry->flags & PATH_BIT_FLAG_IS_QUEUE_SCREEN))
|
|
{
|
|
found = true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (found)
|
|
{
|
|
/* Queue line TV monitors make the peeps waiting in the queue
|
|
* slowly happier, up to a certain level. */
|
|
/* Why don't queue line TV monitors start affecting the peeps
|
|
* as soon as they join the queue?? */
|
|
if (HappinessTarget < 90)
|
|
HappinessTarget = 90;
|
|
|
|
if (HappinessTarget < 165)
|
|
HappinessTarget += 2;
|
|
}
|
|
else
|
|
{
|
|
/* Without a queue line TV monitor peeps waiting too long
|
|
* in a queue get less happy. */
|
|
HappinessTarget = std::max(HappinessTarget - 4, 0);
|
|
}
|
|
}
|
|
peep_update_hunger(this);
|
|
break;
|
|
case PeepState::EnteringRide:
|
|
if (SubState == 17 || SubState == 15)
|
|
{
|
|
peep_decide_whether_to_leave_park(this);
|
|
}
|
|
peep_update_hunger(this);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
loc_68F9F3();
|
|
}
|
|
|
|
loc_68FA89();
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00691677
|
|
*/
|
|
void Guest::TryGetUpFromSitting()
|
|
{
|
|
// Eats all food first
|
|
if (HasFoodOrDrink())
|
|
return;
|
|
|
|
TimeToSitdown--;
|
|
if (TimeToSitdown)
|
|
return;
|
|
|
|
SetState(PeepState::Walking);
|
|
|
|
// Set destination to the centre of the tile.
|
|
auto destination = GetLocation().ToTileCentre();
|
|
SetDestination(destination, 5);
|
|
UpdateCurrentActionSpriteType();
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069152B
|
|
*/
|
|
void Guest::UpdateSitting()
|
|
{
|
|
if (SittingSubState == PeepSittingSubState::TryingToSit)
|
|
{
|
|
if (!CheckForPath())
|
|
return;
|
|
// 691541
|
|
|
|
uint8_t pathingResult;
|
|
PerformNextAction(pathingResult);
|
|
if (!(pathingResult & PATHING_DESTINATION_REACHED))
|
|
return;
|
|
|
|
auto loc = GetLocation().ToTileStart() + CoordsXYZ{ BenchUseOffsets[Var37 & 0x7], 0 };
|
|
|
|
MoveTo(loc);
|
|
|
|
sprite_direction = ((Var37 + 2) & 3) * 8;
|
|
Action = PeepActionType::Idle;
|
|
NextActionSpriteType = PeepActionSpriteType::SittingIdle;
|
|
SwitchNextActionSpriteType();
|
|
|
|
SittingSubState = PeepSittingSubState::SatDown;
|
|
|
|
// Sets time to sit on seat
|
|
TimeToSitdown = (129 - Energy) * 16 + 50;
|
|
}
|
|
else if (SittingSubState == PeepSittingSubState::SatDown)
|
|
{
|
|
if (!IsActionInterruptable())
|
|
{
|
|
UpdateAction();
|
|
if (!IsActionWalking())
|
|
return;
|
|
|
|
Action = PeepActionType::Idle;
|
|
TryGetUpFromSitting();
|
|
return;
|
|
}
|
|
|
|
if ((PeepFlags & PEEP_FLAGS_LEAVING_PARK))
|
|
{
|
|
SetState(PeepState::Walking);
|
|
|
|
// Set destination to the centre of the tile
|
|
auto destination = GetLocation().ToTileCentre();
|
|
SetDestination(destination, 5);
|
|
UpdateCurrentActionSpriteType();
|
|
return;
|
|
}
|
|
|
|
if (SpriteType == PeepSpriteType::Umbrella)
|
|
{
|
|
TryGetUpFromSitting();
|
|
return;
|
|
}
|
|
|
|
if (HasFoodOrDrink())
|
|
{
|
|
if ((scenario_rand() & 0xFFFF) > 1310)
|
|
{
|
|
TryGetUpFromSitting();
|
|
return;
|
|
}
|
|
Action = PeepActionType::SittingEatFood;
|
|
ActionFrame = 0;
|
|
ActionSpriteImageOffset = 0;
|
|
UpdateCurrentActionSpriteType();
|
|
return;
|
|
}
|
|
|
|
int32_t rand = scenario_rand();
|
|
if ((rand & 0xFFFF) > 131)
|
|
{
|
|
TryGetUpFromSitting();
|
|
return;
|
|
}
|
|
if (SpriteType == PeepSpriteType::Balloon || SpriteType == PeepSpriteType::Hat)
|
|
{
|
|
TryGetUpFromSitting();
|
|
return;
|
|
}
|
|
|
|
Action = PeepActionType::SittingLookAroundLeft;
|
|
if (rand & 0x80000000)
|
|
{
|
|
Action = PeepActionType::SittingLookAroundRight;
|
|
}
|
|
|
|
if (rand & 0x40000000)
|
|
{
|
|
Action = PeepActionType::SittingCheckWatch;
|
|
}
|
|
ActionFrame = 0;
|
|
ActionSpriteImageOffset = 0;
|
|
UpdateCurrentActionSpriteType();
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* To simplify check of 0x36BA3E0 and 0x11FF78
|
|
* returns false on no food.
|
|
*/
|
|
int64_t Guest::GetFoodOrDrinkFlags() const
|
|
{
|
|
return GetItemFlags() & (ShopItemsGetAllFoods() | ShopItemsGetAllDrinks());
|
|
}
|
|
|
|
int64_t Guest::GetEmptyContainerFlags() const
|
|
{
|
|
return GetItemFlags() & ShopItemsGetAllContainers();
|
|
}
|
|
|
|
bool Guest::HasFoodOrDrink() const
|
|
{
|
|
return GetFoodOrDrinkFlags() != 0;
|
|
}
|
|
|
|
/**
|
|
* To simplify check of NOT(0x12BA3C0 and 0x118F48)
|
|
* returns 0 on no food.
|
|
*/
|
|
bool Guest::HasDrink() const
|
|
{
|
|
return GetItemFlags() & ShopItemsGetAllDrinks();
|
|
}
|
|
|
|
bool Guest::HasEmptyContainer() const
|
|
{
|
|
return GetEmptyContainerFlags() != 0;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x69C308
|
|
* Check if lost.
|
|
*/
|
|
void Guest::CheckIfLost()
|
|
{
|
|
if (!(PeepFlags & PEEP_FLAGS_LOST))
|
|
{
|
|
if (ride_get_count() < 2)
|
|
return;
|
|
PeepFlags ^= PEEP_FLAGS_21;
|
|
|
|
if (!(PeepFlags & PEEP_FLAGS_21))
|
|
return;
|
|
|
|
TimeLost++;
|
|
if (TimeLost != 254)
|
|
return;
|
|
TimeLost = 230;
|
|
}
|
|
InsertNewThought(PeepThoughtType::Lost);
|
|
|
|
HappinessTarget = std::max(HappinessTarget - 30, 0);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x69C26B
|
|
* Check if can't find ride.
|
|
*/
|
|
void Guest::CheckCantFindRide()
|
|
{
|
|
if (GuestHeadingToRideId == RIDE_ID_NULL)
|
|
return;
|
|
|
|
// Peeps will think "I can't find ride X" twice before giving up completely.
|
|
if (GuestIsLostCountdown == 30 || GuestIsLostCountdown == 60)
|
|
{
|
|
InsertNewThought(PeepThoughtType::CantFind, GuestHeadingToRideId);
|
|
HappinessTarget = std::max(HappinessTarget - 30, 0);
|
|
}
|
|
|
|
GuestIsLostCountdown--;
|
|
if (GuestIsLostCountdown != 0)
|
|
return;
|
|
|
|
GuestHeadingToRideId = RIDE_ID_NULL;
|
|
rct_window* w = window_find_by_number(WC_PEEP, sprite_index);
|
|
|
|
if (w != nullptr)
|
|
{
|
|
window_event_invalidate_call(w);
|
|
}
|
|
|
|
window_invalidate_by_number(WC_PEEP, sprite_index);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x69C2D0
|
|
* Check if can't find exit.
|
|
*/
|
|
void Guest::CheckCantFindExit()
|
|
{
|
|
if (!(PeepFlags & PEEP_FLAGS_LEAVING_PARK))
|
|
return;
|
|
|
|
// Peeps who can't find the park exit will continue to get less happy until they find it.
|
|
if (GuestIsLostCountdown == 1)
|
|
{
|
|
InsertNewThought(PeepThoughtType::CantFindExit);
|
|
HappinessTarget = std::max(HappinessTarget - 30, 0);
|
|
}
|
|
|
|
if (--GuestIsLostCountdown == 0)
|
|
GuestIsLostCountdown = 90;
|
|
}
|
|
|
|
/** Main logic to decide whether a peep should buy an item in question
|
|
*
|
|
* Also handles the purchase as well, so once it returns, the peep will have the
|
|
* item and the money will have been deducted.
|
|
*
|
|
* eax: shopItem | (rideIndex << 8)
|
|
* ecx: price
|
|
* esi: *peep
|
|
*
|
|
* Returns 0 or 1 depending on if the peep decided to buy the item
|
|
*
|
|
* rct2: 0x0069AF1E
|
|
*/
|
|
bool Guest::DecideAndBuyItem(Ride* ride, ShopItem shopItem, money32 price)
|
|
{
|
|
money32 itemValue;
|
|
|
|
bool hasVoucher = false;
|
|
|
|
bool isRainingAndUmbrella = shopItem == ShopItem::Umbrella && climate_is_raining();
|
|
|
|
if ((HasItem(ShopItem::Voucher)) && (VoucherType == VOUCHER_TYPE_FOOD_OR_DRINK_FREE) && (VoucherShopItem == shopItem))
|
|
{
|
|
hasVoucher = true;
|
|
}
|
|
|
|
if (HasItem(shopItem))
|
|
{
|
|
InsertNewThought(PeepThoughtType::AlreadyGot, shopItem);
|
|
return false;
|
|
}
|
|
|
|
if (GetShopItemDescriptor(shopItem).IsFoodOrDrink())
|
|
{
|
|
int32_t food = bitscanforward(GetFoodOrDrinkFlags());
|
|
if (food != -1)
|
|
{
|
|
InsertNewThought(PeepThoughtType::HaventFinished, static_cast<ShopItem>(food));
|
|
return false;
|
|
}
|
|
|
|
if (Nausea >= 145)
|
|
return false;
|
|
}
|
|
|
|
if ((shopItem == ShopItem::Balloon || shopItem == ShopItem::IceCream || shopItem == ShopItem::Candyfloss
|
|
|| shopItem == ShopItem::Sunglasses)
|
|
&& climate_is_raining())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ((shopItem == ShopItem::Sunglasses || shopItem == ShopItem::IceCream) && gClimateCurrent.Temperature < 12)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (GetShopItemDescriptor(shopItem).IsFood() && (Hunger > 75))
|
|
{
|
|
InsertNewThought(PeepThoughtType::NotHungry);
|
|
return false;
|
|
}
|
|
|
|
if (GetShopItemDescriptor(shopItem).IsDrink() && (Thirst > 75))
|
|
{
|
|
InsertNewThought(PeepThoughtType::NotThirsty);
|
|
return false;
|
|
}
|
|
|
|
if (!isRainingAndUmbrella && (shopItem != ShopItem::Map) && GetShopItemDescriptor(shopItem).IsSouvenir() && !hasVoucher)
|
|
{
|
|
if (((scenario_rand() & 0x7F) + 0x73) > Happiness || GuestNumRides < 3)
|
|
return false;
|
|
}
|
|
|
|
if (!hasVoucher)
|
|
{
|
|
if (price != 0 && !(gParkFlags & PARK_FLAGS_NO_MONEY))
|
|
{
|
|
if (CashInPocket == 0)
|
|
{
|
|
InsertNewThought(PeepThoughtType::SpentMoney);
|
|
return false;
|
|
}
|
|
if (price > CashInPocket)
|
|
{
|
|
InsertNewThought(PeepThoughtType::CantAffordItem, shopItem);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (gClimateCurrent.Temperature >= 21)
|
|
itemValue = GetShopItemDescriptor(shopItem).HotValue;
|
|
else if (gClimateCurrent.Temperature <= 11)
|
|
itemValue = GetShopItemDescriptor(shopItem).ColdValue;
|
|
else
|
|
itemValue = GetShopItemDescriptor(shopItem).BaseValue;
|
|
|
|
if (itemValue < price)
|
|
{
|
|
itemValue -= price;
|
|
|
|
if (!isRainingAndUmbrella)
|
|
{
|
|
itemValue = -itemValue;
|
|
if (Happiness >= 128)
|
|
{
|
|
itemValue /= 2;
|
|
if (Happiness >= 180)
|
|
itemValue /= 2;
|
|
}
|
|
if (itemValue > (static_cast<money16>(scenario_rand() & 0x07)))
|
|
{
|
|
// "I'm not paying that much for x"
|
|
InsertNewThought(GetShopItemDescriptor(shopItem).TooMuchThought, ride->id);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
itemValue -= price;
|
|
itemValue = std::max(8, itemValue);
|
|
|
|
if (!(gParkFlags & PARK_FLAGS_NO_MONEY))
|
|
{
|
|
if (itemValue >= static_cast<money32>(scenario_rand() & 0x07))
|
|
{
|
|
// "This x is a really good value"
|
|
InsertNewThought(GetShopItemDescriptor(shopItem).GoodValueThought, ride->id);
|
|
}
|
|
}
|
|
|
|
int32_t happinessGrowth = itemValue * 4;
|
|
HappinessTarget = std::min((HappinessTarget + happinessGrowth), PEEP_MAX_HAPPINESS);
|
|
Happiness = std::min((Happiness + happinessGrowth), PEEP_MAX_HAPPINESS);
|
|
}
|
|
|
|
// reset itemValue for satisfaction calculation
|
|
if (gClimateCurrent.Temperature >= 21)
|
|
itemValue = GetShopItemDescriptor(shopItem).HotValue;
|
|
else if (gClimateCurrent.Temperature <= 11)
|
|
itemValue = GetShopItemDescriptor(shopItem).ColdValue;
|
|
else
|
|
itemValue = GetShopItemDescriptor(shopItem).BaseValue;
|
|
itemValue -= price;
|
|
uint8_t satisfaction = 0;
|
|
if (itemValue > -8)
|
|
{
|
|
satisfaction++;
|
|
if (itemValue > -3)
|
|
{
|
|
satisfaction++;
|
|
if (itemValue > 3)
|
|
satisfaction++;
|
|
}
|
|
}
|
|
ride_update_satisfaction(ride, satisfaction);
|
|
}
|
|
|
|
// The peep has now decided to buy the item (or, specifically, has not been
|
|
// dissuaded so far).
|
|
GiveItem(shopItem);
|
|
|
|
if (shopItem == ShopItem::TShirt)
|
|
TshirtColour = ride->track_colour[0].main;
|
|
|
|
if (shopItem == ShopItem::Hat)
|
|
HatColour = ride->track_colour[0].main;
|
|
|
|
if (shopItem == ShopItem::Balloon)
|
|
BalloonColour = ride->track_colour[0].main;
|
|
|
|
if (shopItem == ShopItem::Umbrella)
|
|
UmbrellaColour = ride->track_colour[0].main;
|
|
|
|
if (shopItem == ShopItem::Map)
|
|
ResetPathfindGoal();
|
|
|
|
uint16_t consumptionTime = GetShopItemDescriptor(shopItem).ConsumptionTime;
|
|
TimeToConsume = std::min((TimeToConsume + consumptionTime), 255);
|
|
|
|
if (shopItem == ShopItem::Photo)
|
|
Photo1RideRef = ride->id;
|
|
|
|
if (shopItem == ShopItem::Photo2)
|
|
Photo2RideRef = ride->id;
|
|
|
|
if (shopItem == ShopItem::Photo3)
|
|
Photo3RideRef = ride->id;
|
|
|
|
if (shopItem == ShopItem::Photo4)
|
|
Photo4RideRef = ride->id;
|
|
|
|
WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_INVENTORY;
|
|
UpdateSpriteType();
|
|
if (PeepFlags & PEEP_FLAGS_TRACKING)
|
|
{
|
|
auto ft = Formatter();
|
|
FormatNameTo(ft);
|
|
ft.Add<rct_string_id>(GetShopItemDescriptor(shopItem).Naming.Indefinite);
|
|
if (gConfigNotifications.guest_bought_item)
|
|
{
|
|
News::AddItemToQueue(News::ItemType::PeepOnRide, STR_PEEP_TRACKING_NOTIFICATION_BOUGHT_X, sprite_index, ft);
|
|
}
|
|
}
|
|
|
|
if (GetShopItemDescriptor(shopItem).IsFood())
|
|
AmountOfFood++;
|
|
|
|
if (GetShopItemDescriptor(shopItem).IsDrink())
|
|
AmountOfDrinks++;
|
|
|
|
if (GetShopItemDescriptor(shopItem).IsSouvenir())
|
|
AmountOfSouvenirs++;
|
|
|
|
money16* expend_type = &PaidOnSouvenirs;
|
|
ExpenditureType expenditure = ExpenditureType::ShopStock;
|
|
|
|
if (GetShopItemDescriptor(shopItem).IsFood())
|
|
{
|
|
expend_type = &PaidOnFood;
|
|
expenditure = ExpenditureType::FoodDrinkStock;
|
|
}
|
|
|
|
if (GetShopItemDescriptor(shopItem).IsDrink())
|
|
{
|
|
expend_type = &PaidOnDrink;
|
|
expenditure = ExpenditureType::FoodDrinkStock;
|
|
}
|
|
|
|
if (!(gParkFlags & PARK_FLAGS_NO_MONEY))
|
|
finance_payment(GetShopItemDescriptor(shopItem).Cost, expenditure);
|
|
|
|
// Sets the expenditure type to *_FOODDRINK_SALES or *_SHOP_SALES appropriately.
|
|
expenditure = static_cast<ExpenditureType>(static_cast<int32_t>(expenditure) - 1);
|
|
if (hasVoucher)
|
|
{
|
|
RemoveItem(ShopItem::Voucher);
|
|
WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_INVENTORY;
|
|
}
|
|
else if (!(gParkFlags & PARK_FLAGS_NO_MONEY))
|
|
{
|
|
SpendMoney(*expend_type, price, expenditure);
|
|
}
|
|
ride->total_profit += (price - GetShopItemDescriptor(shopItem).Cost);
|
|
ride->window_invalidate_flags |= RIDE_INVALIDATE_RIDE_INCOME;
|
|
ride->cur_num_customers++;
|
|
ride->total_customers++;
|
|
ride->window_invalidate_flags |= RIDE_INVALIDATE_RIDE_CUSTOMER;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Updates various peep stats upon entering a ride, as well as updating the
|
|
* ride's satisfaction value.
|
|
* rct2: 0x0069545B
|
|
*/
|
|
void Guest::OnEnterRide(Ride* ride)
|
|
{
|
|
if (ride == nullptr)
|
|
return;
|
|
|
|
// Calculate how satisfying the ride is for the peep. Can range from -140 to +105.
|
|
int16_t satisfaction = peep_calculate_ride_satisfaction(this, ride);
|
|
|
|
// Update the satisfaction stat of the ride.
|
|
uint8_t rideSatisfaction = 0;
|
|
if (satisfaction >= 40)
|
|
rideSatisfaction = 3;
|
|
else if (satisfaction >= 20)
|
|
rideSatisfaction = 2;
|
|
else if (satisfaction >= 0)
|
|
rideSatisfaction = 1;
|
|
|
|
ride_update_satisfaction(ride, rideSatisfaction);
|
|
|
|
// Update various peep stats.
|
|
if (GuestNumRides < 255)
|
|
GuestNumRides++;
|
|
|
|
SetHasRidden(ride);
|
|
peep_update_favourite_ride(this, ride);
|
|
HappinessTarget = std::clamp(HappinessTarget + satisfaction, 0, PEEP_MAX_HAPPINESS);
|
|
peep_update_ride_nausea_growth(this, ride);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069576E
|
|
*/
|
|
void Guest::OnExitRide(Ride* ride)
|
|
{
|
|
if (PeepFlags & PEEP_FLAGS_RIDE_SHOULD_BE_MARKED_AS_FAVOURITE)
|
|
{
|
|
PeepFlags &= ~PEEP_FLAGS_RIDE_SHOULD_BE_MARKED_AS_FAVOURITE;
|
|
FavouriteRide = ride->id;
|
|
// TODO fix this flag name or add another one
|
|
WindowInvalidateFlags |= PEEP_INVALIDATE_STAFF_STATS;
|
|
}
|
|
Happiness = HappinessTarget;
|
|
Nausea = NauseaTarget;
|
|
WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_STATS;
|
|
|
|
if (PeepFlags & PEEP_FLAGS_LEAVING_PARK)
|
|
PeepFlags &= ~(PEEP_FLAGS_PARK_ENTRANCE_CHOSEN);
|
|
|
|
if (ride != nullptr && peep_should_go_on_ride_again(this, ride))
|
|
{
|
|
GuestHeadingToRideId = ride->id;
|
|
GuestIsLostCountdown = 200;
|
|
ResetPathfindGoal();
|
|
WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_ACTION;
|
|
}
|
|
|
|
if (peep_should_preferred_intensity_increase(this))
|
|
{
|
|
if (Intensity.GetMaximum() < 15)
|
|
{
|
|
Intensity = Intensity.WithMaximum(Intensity.GetMaximum() + 1);
|
|
}
|
|
}
|
|
|
|
if (ride != nullptr && peep_really_liked_ride(this, ride))
|
|
{
|
|
InsertNewThought(PeepThoughtType::WasGreat, ride->id);
|
|
|
|
static constexpr OpenRCT2::Audio::SoundId laughs[3] = {
|
|
OpenRCT2::Audio::SoundId::Laugh1,
|
|
OpenRCT2::Audio::SoundId::Laugh2,
|
|
OpenRCT2::Audio::SoundId::Laugh3,
|
|
};
|
|
int32_t laughType = scenario_rand() & 7;
|
|
if (laughType < 3)
|
|
{
|
|
OpenRCT2::Audio::Play3D(laughs[laughType], GetLocation());
|
|
}
|
|
}
|
|
|
|
if (ride != nullptr)
|
|
{
|
|
ride->total_customers++;
|
|
ride->window_invalidate_flags |= RIDE_INVALIDATE_RIDE_CUSTOMER;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00695DD2
|
|
*/
|
|
void Guest::PickRideToGoOn()
|
|
{
|
|
if (State != PeepState::Walking)
|
|
return;
|
|
if (GuestHeadingToRideId != RIDE_ID_NULL)
|
|
return;
|
|
if (PeepFlags & PEEP_FLAGS_LEAVING_PARK)
|
|
return;
|
|
if (HasFoodOrDrink())
|
|
return;
|
|
if (x == LOCATION_NULL)
|
|
return;
|
|
|
|
auto ride = FindBestRideToGoOn();
|
|
if (ride != nullptr)
|
|
{
|
|
// Head to that ride
|
|
GuestHeadingToRideId = ride->id;
|
|
GuestIsLostCountdown = 200;
|
|
ResetPathfindGoal();
|
|
WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_ACTION;
|
|
|
|
// Make peep look at their map if they have one
|
|
if (HasItem(ShopItem::Map))
|
|
{
|
|
ReadMap();
|
|
}
|
|
}
|
|
}
|
|
|
|
Ride* Guest::FindBestRideToGoOn()
|
|
{
|
|
// Pick the most exciting ride
|
|
auto rideConsideration = FindRidesToGoOn();
|
|
Ride* mostExcitingRide = nullptr;
|
|
for (auto& ride : GetRideManager())
|
|
{
|
|
const auto rideIndex = EnumValue(ride.id);
|
|
if (rideConsideration.size() > rideIndex && rideConsideration[rideIndex])
|
|
{
|
|
if (!(ride.lifecycle_flags & RIDE_LIFECYCLE_QUEUE_FULL))
|
|
{
|
|
if (ShouldGoOnRide(&ride, 0, false, true) && ride_has_ratings(&ride))
|
|
{
|
|
if (mostExcitingRide == nullptr || ride.excitement > mostExcitingRide->excitement)
|
|
{
|
|
mostExcitingRide = &ride;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return mostExcitingRide;
|
|
}
|
|
|
|
std::bitset<MAX_RIDES> Guest::FindRidesToGoOn()
|
|
{
|
|
std::bitset<MAX_RIDES> rideConsideration;
|
|
|
|
// FIX Originally checked for a toy, likely a mistake and should be a map,
|
|
// but then again this seems to only allow the peep to go on
|
|
// rides they haven't been on before.
|
|
if (HasItem(ShopItem::Map))
|
|
{
|
|
// Consider rides that peep hasn't been on yet
|
|
for (auto& ride : GetRideManager())
|
|
{
|
|
if (!HasRidden(&ride))
|
|
{
|
|
rideConsideration[EnumValue(ride.id)] = true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Take nearby rides into consideration
|
|
constexpr auto radius = 10 * 32;
|
|
int32_t cx = floor2(x, 32);
|
|
int32_t cy = floor2(y, 32);
|
|
for (int32_t tileX = cx - radius; tileX <= cx + radius; tileX += COORDS_XY_STEP)
|
|
{
|
|
for (int32_t tileY = cy - radius; tileY <= cy + radius; tileY += COORDS_XY_STEP)
|
|
{
|
|
auto location = CoordsXY{ tileX, tileY };
|
|
if (!map_is_location_valid(location))
|
|
continue;
|
|
|
|
for (auto* trackElement : TileElementsView<TrackElement>(location))
|
|
{
|
|
auto rideIndex = trackElement->GetRideIndex();
|
|
if (rideIndex != RIDE_ID_NULL)
|
|
{
|
|
rideConsideration[EnumValue(rideIndex)] = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Always take the tall rides into consideration (realistic as you can usually see them from anywhere in the park)
|
|
for (auto& ride : GetRideManager())
|
|
{
|
|
if (ride.highest_drop_height > 66 || ride.excitement >= RIDE_RATING(8, 00))
|
|
{
|
|
rideConsideration[EnumValue(ride.id)] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return rideConsideration;
|
|
}
|
|
|
|
/**
|
|
* This function is called whenever a peep is deciding whether or not they want
|
|
* to go on a ride or visit a shop. They may be physically present at the
|
|
* ride/shop, or they may just be thinking about it.
|
|
* rct2: 0x006960AB
|
|
*/
|
|
bool Guest::ShouldGoOnRide(Ride* ride, int32_t entranceNum, bool atQueue, bool thinking)
|
|
{
|
|
// Indicates whether a peep is physically at the ride, or is just thinking about going on the ride.
|
|
bool peepAtRide = !thinking;
|
|
|
|
if (ride->status == RideStatus::Open && !(ride->lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN))
|
|
{
|
|
// Peeps that are leaving the park will refuse to go on any rides, with the exception of free transport rides.
|
|
assert(ride->type < std::size(RideTypeDescriptors));
|
|
if (!ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_TRANSPORT_RIDE) || ride->value == RIDE_VALUE_UNDEFINED
|
|
|| ride_get_price(ride) != 0)
|
|
{
|
|
if (PeepFlags & PEEP_FLAGS_LEAVING_PARK)
|
|
{
|
|
ChoseNotToGoOnRide(ride, peepAtRide, false);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_IS_SHOP))
|
|
{
|
|
return ShouldGoToShop(ride, peepAtRide);
|
|
}
|
|
|
|
// This used to check !(flags & 2), but the function is only ever called with flags = 0, 1 or 6.
|
|
// This means we can use the existing !(flags & 4) check.
|
|
if (peepAtRide)
|
|
{
|
|
// Peeps won't join a queue that has 1000 peeps already in it.
|
|
if (ride->stations[entranceNum].QueueLength >= 1000)
|
|
{
|
|
peep_tried_to_enter_full_queue(this, ride);
|
|
return false;
|
|
}
|
|
|
|
// Rides without queues can only have one peep waiting at a time.
|
|
if (!atQueue)
|
|
{
|
|
if (ride->stations[entranceNum].LastPeepInQueue != SPRITE_INDEX_NULL)
|
|
{
|
|
peep_tried_to_enter_full_queue(this, ride);
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Check if there's room in the queue for the peep to enter.
|
|
Guest* lastPeepInQueue = GetEntity<Guest>(ride->stations[entranceNum].LastPeepInQueue);
|
|
if (lastPeepInQueue != nullptr && (abs(lastPeepInQueue->z - z) <= 6))
|
|
{
|
|
int32_t dx = abs(lastPeepInQueue->x - x);
|
|
int32_t dy = abs(lastPeepInQueue->y - y);
|
|
int32_t maxD = std::max(dx, dy);
|
|
|
|
// Unlike normal paths, peeps cannot overlap when queueing for a ride.
|
|
// This check enforces a minimum distance between peeps entering the queue.
|
|
if (maxD < 8)
|
|
{
|
|
peep_tried_to_enter_full_queue(this, ride);
|
|
return false;
|
|
}
|
|
|
|
// This checks if there's a peep standing still at the very end of the queue.
|
|
if (maxD <= 13 && lastPeepInQueue->TimeInQueue > 10)
|
|
{
|
|
peep_tried_to_enter_full_queue(this, ride);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Assuming the queue conditions are met, peeps will always go on free transport rides.
|
|
// Ride ratings, recent crashes and weather will all be ignored.
|
|
money16 ridePrice = ride_get_price(ride);
|
|
if (!ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_TRANSPORT_RIDE) || ride->value == RIDE_VALUE_UNDEFINED
|
|
|| ridePrice != 0)
|
|
{
|
|
if (PreviousRide == ride->id)
|
|
{
|
|
ChoseNotToGoOnRide(ride, peepAtRide, false);
|
|
return false;
|
|
}
|
|
|
|
// Basic price checks
|
|
if (ridePrice != 0 && !peep_has_voucher_for_free_ride(this, ride) && !(gParkFlags & PARK_FLAGS_NO_MONEY))
|
|
{
|
|
if (ridePrice > CashInPocket)
|
|
{
|
|
if (peepAtRide)
|
|
{
|
|
if (CashInPocket <= 0)
|
|
{
|
|
InsertNewThought(PeepThoughtType::SpentMoney);
|
|
}
|
|
else
|
|
{
|
|
InsertNewThought(PeepThoughtType::CantAffordRide, ride->id);
|
|
}
|
|
}
|
|
ChoseNotToGoOnRide(ride, peepAtRide, true);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// If happy enough, peeps will ignore the fact that a ride has recently crashed.
|
|
if (ride->last_crash_type != RIDE_CRASH_TYPE_NONE && Happiness < 225)
|
|
{
|
|
if (peepAtRide)
|
|
{
|
|
InsertNewThought(PeepThoughtType::NotSafe, ride->id);
|
|
if (HappinessTarget >= 64)
|
|
{
|
|
HappinessTarget -= 8;
|
|
}
|
|
ride_update_popularity(ride, 0);
|
|
}
|
|
ChoseNotToGoOnRide(ride, peepAtRide, true);
|
|
return false;
|
|
}
|
|
|
|
if (ride_has_ratings(ride))
|
|
{
|
|
// If a peep has already decided that they're going to go on a ride, they'll skip the weather and
|
|
// excitement check and will only do a basic intensity check when they arrive at the ride itself.
|
|
if (ride->id == GuestHeadingToRideId)
|
|
{
|
|
if (ride->intensity > RIDE_RATING(10, 00) && !gCheatsIgnoreRideIntensity)
|
|
{
|
|
peep_ride_is_too_intense(this, ride, peepAtRide);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Peeps won't go on rides that aren't sufficiently undercover while it's raining.
|
|
// The threshold is fairly low and only requires about 10-15% of the ride to be undercover.
|
|
if (climate_is_raining() && (ride->sheltered_eighths) < 3)
|
|
{
|
|
if (peepAtRide)
|
|
{
|
|
InsertNewThought(PeepThoughtType::NotWhileRaining, ride->id);
|
|
if (HappinessTarget >= 64)
|
|
{
|
|
HappinessTarget -= 8;
|
|
}
|
|
ride_update_popularity(ride, 0);
|
|
}
|
|
ChoseNotToGoOnRide(ride, peepAtRide, true);
|
|
return false;
|
|
}
|
|
|
|
if (!gCheatsIgnoreRideIntensity)
|
|
{
|
|
// Intensity calculations. Even though the max intensity can go up to 15, it's capped
|
|
// at 10.0 (before happiness calculations). A full happiness bar will increase the max
|
|
// intensity and decrease the min intensity by about 2.5.
|
|
ride_rating maxIntensity = std::min(Intensity.GetMaximum() * 100, 1000) + Happiness;
|
|
ride_rating minIntensity = (Intensity.GetMinimum() * 100) - Happiness;
|
|
if (ride->intensity < minIntensity)
|
|
{
|
|
if (peepAtRide)
|
|
{
|
|
InsertNewThought(PeepThoughtType::MoreThrilling, ride->id);
|
|
if (HappinessTarget >= 64)
|
|
{
|
|
HappinessTarget -= 8;
|
|
}
|
|
ride_update_popularity(ride, 0);
|
|
}
|
|
ChoseNotToGoOnRide(ride, peepAtRide, true);
|
|
return false;
|
|
}
|
|
if (ride->intensity > maxIntensity)
|
|
{
|
|
peep_ride_is_too_intense(this, ride, peepAtRide);
|
|
return false;
|
|
}
|
|
|
|
// Nausea calculations.
|
|
ride_rating maxNausea = NauseaMaximumThresholds[(EnumValue(NauseaTolerance) & 3)] + Happiness;
|
|
|
|
if (ride->nausea > maxNausea)
|
|
{
|
|
if (peepAtRide)
|
|
{
|
|
InsertNewThought(PeepThoughtType::Sickening, ride->id);
|
|
if (HappinessTarget >= 64)
|
|
{
|
|
HappinessTarget -= 8;
|
|
}
|
|
ride_update_popularity(ride, 0);
|
|
}
|
|
ChoseNotToGoOnRide(ride, peepAtRide, true);
|
|
return false;
|
|
}
|
|
|
|
// Very nauseous peeps will only go on very gentle rides.
|
|
if (ride->nausea >= FIXED_2DP(1, 40) && Nausea > 160)
|
|
{
|
|
ChoseNotToGoOnRide(ride, peepAtRide, false);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the ride has not yet been rated and is capable of having g-forces,
|
|
// there's a 90% chance that the peep will ignore it.
|
|
if (!ride_has_ratings(ride) && ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_PEEP_CHECK_GFORCES))
|
|
{
|
|
if ((scenario_rand() & 0xFFFF) > 0x1999U)
|
|
{
|
|
ChoseNotToGoOnRide(ride, peepAtRide, false);
|
|
return false;
|
|
}
|
|
|
|
if (!gCheatsIgnoreRideIntensity)
|
|
{
|
|
if (ride->max_positive_vertical_g > FIXED_2DP(5, 00) || ride->max_negative_vertical_g < FIXED_2DP(-4, 00)
|
|
|| ride->max_lateral_g > FIXED_2DP(4, 00))
|
|
{
|
|
ChoseNotToGoOnRide(ride, peepAtRide, false);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
uint32_t value = ride->value;
|
|
|
|
// If the value of the ride hasn't yet been calculated, peeps will be willing to pay any amount for the ride.
|
|
if (value != 0xFFFF && !peep_has_voucher_for_free_ride(this, ride) && !(gParkFlags & PARK_FLAGS_NO_MONEY))
|
|
{
|
|
// The amount peeps are willing to pay is decreased by 75% if they had to pay to enter the park.
|
|
if (PeepFlags & PEEP_FLAGS_HAS_PAID_FOR_PARK_ENTRY)
|
|
value /= 4;
|
|
|
|
// Peeps won't pay more than twice the value of the ride.
|
|
ridePrice = ride_get_price(ride);
|
|
if (ridePrice > static_cast<money16>(value * 2))
|
|
{
|
|
if (peepAtRide)
|
|
{
|
|
InsertNewThought(PeepThoughtType::BadValue, ride->id);
|
|
if (HappinessTarget >= 60)
|
|
{
|
|
HappinessTarget -= 16;
|
|
}
|
|
ride_update_popularity(ride, 0);
|
|
}
|
|
ChoseNotToGoOnRide(ride, peepAtRide, true);
|
|
return false;
|
|
}
|
|
|
|
// A ride is good value if the price is 50% or less of the ride value and the peep didn't pay to enter the park.
|
|
if (ridePrice <= static_cast<money16>(value / 2) && peepAtRide)
|
|
{
|
|
if (!(gParkFlags & PARK_FLAGS_NO_MONEY))
|
|
{
|
|
if (!(PeepFlags & PEEP_FLAGS_HAS_PAID_FOR_PARK_ENTRY))
|
|
{
|
|
InsertNewThought(PeepThoughtType::GoodValue, ride->id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// At this point, the peep has decided to go on the ride.
|
|
if (peepAtRide)
|
|
{
|
|
ride_update_popularity(ride, 1);
|
|
}
|
|
|
|
if (ride->id == GuestHeadingToRideId)
|
|
{
|
|
peep_reset_ride_heading(this);
|
|
}
|
|
|
|
ride->lifecycle_flags &= ~RIDE_LIFECYCLE_QUEUE_FULL;
|
|
return true;
|
|
}
|
|
|
|
ChoseNotToGoOnRide(ride, peepAtRide, false);
|
|
return false;
|
|
}
|
|
|
|
bool Guest::ShouldGoToShop(Ride* ride, bool peepAtShop)
|
|
{
|
|
// Peeps won't go to the same shop twice in a row.
|
|
if (ride->id == PreviousRide)
|
|
{
|
|
ChoseNotToGoOnRide(ride, peepAtShop, true);
|
|
return false;
|
|
}
|
|
|
|
if (ride->type == RIDE_TYPE_TOILETS)
|
|
{
|
|
if (Toilet < 70)
|
|
{
|
|
ChoseNotToGoOnRide(ride, peepAtShop, true);
|
|
return false;
|
|
}
|
|
|
|
// The amount that peeps are willing to pay to use the Toilets scales with their toilet stat.
|
|
// It effectively has a minimum of $0.10 (due to the check above) and a maximum of $0.60.
|
|
if (ride_get_price(ride) * 40 > Toilet)
|
|
{
|
|
if (peepAtShop)
|
|
{
|
|
InsertNewThought(PeepThoughtType::NotPaying, ride->id);
|
|
if (HappinessTarget >= 60)
|
|
{
|
|
HappinessTarget -= 16;
|
|
}
|
|
ride_update_popularity(ride, 0);
|
|
}
|
|
ChoseNotToGoOnRide(ride, peepAtShop, true);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (ride->type == RIDE_TYPE_FIRST_AID)
|
|
{
|
|
if (Nausea < 128)
|
|
{
|
|
ChoseNotToGoOnRide(ride, peepAtShop, true);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Basic price checks
|
|
auto ridePrice = ride_get_price(ride);
|
|
if (ridePrice != 0 && ridePrice > CashInPocket)
|
|
{
|
|
if (peepAtShop)
|
|
{
|
|
if (CashInPocket <= 0)
|
|
{
|
|
InsertNewThought(PeepThoughtType::SpentMoney);
|
|
}
|
|
else
|
|
{
|
|
InsertNewThought(PeepThoughtType::CantAffordRide, ride->id);
|
|
}
|
|
}
|
|
ChoseNotToGoOnRide(ride, peepAtShop, true);
|
|
return false;
|
|
}
|
|
|
|
if (peepAtShop)
|
|
{
|
|
ride_update_popularity(ride, 1);
|
|
if (ride->id == GuestHeadingToRideId)
|
|
{
|
|
peep_reset_ride_heading(this);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Used when no logging to an expend type required
|
|
void Guest::SpendMoney(money32 amount, ExpenditureType expenditure)
|
|
{
|
|
money16 unused;
|
|
SpendMoney(unused, amount, expenditure);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069926C
|
|
* Expend type was previously an offset saved in 0x00F1AEC0
|
|
*/
|
|
void Guest::SpendMoney(money16& peep_expend_type, money32 amount, ExpenditureType expenditure)
|
|
{
|
|
assert(!(gParkFlags & PARK_FLAGS_NO_MONEY));
|
|
|
|
CashInPocket = std::max(0, CashInPocket - amount);
|
|
CashSpent += amount;
|
|
|
|
peep_expend_type += static_cast<money16>(amount);
|
|
|
|
window_invalidate_by_number(WC_PEEP, sprite_index);
|
|
|
|
finance_payment(-amount, expenditure);
|
|
|
|
if (gConfigGeneral.show_guest_purchases && !(gScreenFlags & SCREEN_FLAGS_TITLE_DEMO))
|
|
{
|
|
// HACK Currently disabled for multiplayer due to limitation of all sprites
|
|
// needing to be synchronised
|
|
if (network_get_mode() == NETWORK_MODE_NONE && !gOpenRCT2Headless)
|
|
{
|
|
MoneyEffect::CreateAt(amount, GetLocation(), true);
|
|
}
|
|
}
|
|
|
|
OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::Purchase, GetLocation());
|
|
}
|
|
|
|
void Guest::SetHasRidden(const Ride* ride)
|
|
{
|
|
OpenRCT2::RideUse::GetHistory().Add(sprite_index, ride->id);
|
|
|
|
SetHasRiddenRideType(ride->type);
|
|
}
|
|
|
|
bool Guest::HasRidden(const Ride* ride) const
|
|
{
|
|
return OpenRCT2::RideUse::GetHistory().Contains(sprite_index, ride->id);
|
|
}
|
|
|
|
void Guest::SetHasRiddenRideType(int32_t rideType)
|
|
{
|
|
OpenRCT2::RideUse::GetTypeHistory().Add(sprite_index, rideType);
|
|
}
|
|
|
|
bool Guest::HasRiddenRideType(int32_t rideType) const
|
|
{
|
|
return OpenRCT2::RideUse::GetTypeHistory().Contains(sprite_index, rideType);
|
|
}
|
|
|
|
void Guest::SetParkEntryTime(int32_t entryTime)
|
|
{
|
|
ParkEntryTime = entryTime;
|
|
}
|
|
|
|
int32_t Guest::GetParkEntryTime() const
|
|
{
|
|
return ParkEntryTime;
|
|
}
|
|
|
|
void Guest::ChoseNotToGoOnRide(Ride* ride, bool peepAtRide, bool updateLastRide)
|
|
{
|
|
if (peepAtRide && updateLastRide)
|
|
{
|
|
PreviousRide = ride->id;
|
|
PreviousRideTimeOut = 0;
|
|
}
|
|
|
|
if (ride->id == GuestHeadingToRideId)
|
|
{
|
|
peep_reset_ride_heading(this);
|
|
}
|
|
}
|
|
|
|
void Guest::ReadMap()
|
|
{
|
|
if (IsActionInterruptable())
|
|
{
|
|
Action = PeepActionType::ReadMap;
|
|
ActionFrame = 0;
|
|
ActionSpriteImageOffset = 0;
|
|
UpdateCurrentActionSpriteType();
|
|
}
|
|
}
|
|
|
|
static bool peep_has_voucher_for_free_ride(Guest* peep, Ride* ride)
|
|
{
|
|
return peep->HasItem(ShopItem::Voucher) && peep->VoucherType == VOUCHER_TYPE_RIDE_FREE && peep->VoucherRideId == ride->id;
|
|
}
|
|
|
|
/**
|
|
* When the queue is full, peeps will ignore the ride when thinking about what to go on next.
|
|
* Does not effect peeps that walk up to the queue entrance.
|
|
* This flag is reset the next time a peep successfully joins the queue.
|
|
*/
|
|
static void peep_tried_to_enter_full_queue(Guest* peep, Ride* ride)
|
|
{
|
|
ride->lifecycle_flags |= RIDE_LIFECYCLE_QUEUE_FULL;
|
|
peep->PreviousRide = ride->id;
|
|
peep->PreviousRideTimeOut = 0;
|
|
// Change status "Heading to" to "Walking" if queue is full
|
|
if (ride->id == peep->GuestHeadingToRideId)
|
|
{
|
|
peep_reset_ride_heading(peep);
|
|
}
|
|
}
|
|
|
|
static void peep_reset_ride_heading(Guest* peep)
|
|
{
|
|
peep->GuestHeadingToRideId = RIDE_ID_NULL;
|
|
peep->WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_ACTION;
|
|
}
|
|
|
|
static void peep_ride_is_too_intense(Guest* peep, Ride* ride, bool peepAtRide)
|
|
{
|
|
if (peepAtRide)
|
|
{
|
|
peep->InsertNewThought(PeepThoughtType::Intense, ride->id);
|
|
if (peep->HappinessTarget >= 64)
|
|
{
|
|
peep->HappinessTarget -= 8;
|
|
}
|
|
ride_update_popularity(ride, 0);
|
|
}
|
|
peep->ChoseNotToGoOnRide(ride, peepAtRide, true);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00691C6E
|
|
*/
|
|
static Vehicle* peep_choose_car_from_ride(Peep* peep, Ride* ride, std::vector<uint8_t>& car_array)
|
|
{
|
|
uint8_t chosen_car = scenario_rand();
|
|
if (ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_HAS_G_FORCES) && ((chosen_car & 0xC) != 0xC))
|
|
{
|
|
chosen_car = (scenario_rand() & 1) ? 0 : static_cast<uint8_t>(car_array.size()) - 1;
|
|
}
|
|
else
|
|
{
|
|
chosen_car = (chosen_car * static_cast<uint16_t>(car_array.size())) >> 8;
|
|
}
|
|
|
|
peep->CurrentCar = car_array[chosen_car];
|
|
|
|
Vehicle* vehicle = GetEntity<Vehicle>(ride->vehicles[peep->CurrentTrain]);
|
|
if (vehicle == nullptr)
|
|
{
|
|
return nullptr;
|
|
}
|
|
return vehicle->GetCar(peep->CurrentCar);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00691CD1
|
|
*/
|
|
static void peep_choose_seat_from_car(Peep* peep, Ride* ride, Vehicle* vehicle)
|
|
{
|
|
if (vehicle == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
uint8_t chosen_seat = vehicle->next_free_seat;
|
|
|
|
if (ride->mode == RideMode::ForwardRotation || ride->mode == RideMode::BackwardRotation)
|
|
{
|
|
chosen_seat = (((~vehicle->Pitch + 1) >> 3) & 0xF) * 2;
|
|
if (vehicle->next_free_seat & 1)
|
|
{
|
|
chosen_seat++;
|
|
}
|
|
}
|
|
peep->CurrentSeat = chosen_seat;
|
|
vehicle->next_free_seat++;
|
|
|
|
vehicle->peep[peep->CurrentSeat] = peep->sprite_index;
|
|
vehicle->peep_tshirt_colours[peep->CurrentSeat] = peep->TshirtColour;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00691D27
|
|
*/
|
|
void Guest::GoToRideEntrance(Ride* ride)
|
|
{
|
|
TileCoordsXYZD tileLocation = ride_get_entrance_location(ride, CurrentRideStation);
|
|
if (tileLocation.IsNull())
|
|
{
|
|
RemoveFromQueue();
|
|
return;
|
|
}
|
|
|
|
auto location = tileLocation.ToCoordsXYZD().ToTileCentre();
|
|
int16_t x_shift = DirectionOffsets[location.direction].x;
|
|
int16_t y_shift = DirectionOffsets[location.direction].y;
|
|
|
|
uint8_t shift_multiplier = 21;
|
|
rct_ride_entry* rideEntry = get_ride_entry(ride->subtype);
|
|
if (rideEntry != nullptr)
|
|
{
|
|
if (rideEntry->vehicles[rideEntry->default_vehicle].flags & VEHICLE_ENTRY_FLAG_MINI_GOLF
|
|
|| rideEntry->vehicles[rideEntry->default_vehicle].flags
|
|
& (VEHICLE_ENTRY_FLAG_CHAIRLIFT | VEHICLE_ENTRY_FLAG_GO_KART))
|
|
{
|
|
shift_multiplier = 32;
|
|
}
|
|
}
|
|
|
|
x_shift *= shift_multiplier;
|
|
y_shift *= shift_multiplier;
|
|
|
|
location.x += x_shift;
|
|
location.y += y_shift;
|
|
|
|
SetDestination(location, 2);
|
|
SetState(PeepState::EnteringRide);
|
|
RideSubState = PeepRideSubState::InEntrance;
|
|
|
|
RejoinQueueTimeout = 0;
|
|
GuestTimeOnRide = 0;
|
|
|
|
RemoveFromQueue();
|
|
}
|
|
|
|
bool Guest::FindVehicleToEnter(Ride* ride, std::vector<uint8_t>& car_array)
|
|
{
|
|
uint8_t chosen_train = RideStation::NO_TRAIN;
|
|
|
|
if (ride->mode == RideMode::Dodgems || ride->mode == RideMode::Race)
|
|
{
|
|
if (ride->lifecycle_flags & RIDE_LIFECYCLE_PASS_STATION_NO_STOPPING)
|
|
return false;
|
|
|
|
for (int32_t i = 0; i < ride->num_vehicles; ++i)
|
|
{
|
|
Vehicle* vehicle = GetEntity<Vehicle>(ride->vehicles[i]);
|
|
if (vehicle == nullptr)
|
|
continue;
|
|
|
|
if (vehicle->next_free_seat >= vehicle->num_seats)
|
|
continue;
|
|
|
|
if (vehicle->status != Vehicle::Status::WaitingForPassengers)
|
|
continue;
|
|
chosen_train = i;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
chosen_train = ride->stations[CurrentRideStation].TrainAtStation;
|
|
}
|
|
if (chosen_train >= MAX_VEHICLES_PER_RIDE)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
CurrentTrain = chosen_train;
|
|
|
|
int32_t i = 0;
|
|
|
|
uint16_t vehicle_id = ride->vehicles[chosen_train];
|
|
for (Vehicle* vehicle = GetEntity<Vehicle>(vehicle_id); vehicle != nullptr;
|
|
vehicle = GetEntity<Vehicle>(vehicle->next_vehicle_on_train), ++i)
|
|
{
|
|
uint8_t num_seats = vehicle->num_seats;
|
|
if (vehicle->IsUsedInPairs())
|
|
{
|
|
if (vehicle->next_free_seat & 1)
|
|
{
|
|
car_array.clear();
|
|
car_array.push_back(i);
|
|
return true;
|
|
}
|
|
num_seats &= VEHICLE_SEAT_NUM_MASK;
|
|
}
|
|
if (num_seats == vehicle->next_free_seat)
|
|
continue;
|
|
|
|
if (ride->mode == RideMode::ForwardRotation || ride->mode == RideMode::BackwardRotation)
|
|
{
|
|
uint8_t position = (((~vehicle->Pitch + 1) >> 3) & 0xF) * 2;
|
|
if (vehicle->peep[position] != SPRITE_INDEX_NULL)
|
|
continue;
|
|
}
|
|
car_array.push_back(i);
|
|
}
|
|
|
|
return !car_array.empty();
|
|
}
|
|
|
|
static void peep_update_ride_at_entrance_try_leave(Guest* peep)
|
|
{
|
|
// Destination Tolerance is zero when peep has completely
|
|
// entered entrance
|
|
if (peep->DestinationTolerance == 0)
|
|
{
|
|
peep->RemoveFromQueue();
|
|
peep->SetState(PeepState::Falling);
|
|
}
|
|
}
|
|
|
|
static bool peep_check_ride_price_at_entrance(Guest* peep, Ride* ride, money32 ridePrice)
|
|
{
|
|
if ((peep->HasItem(ShopItem::Voucher)) && peep->VoucherType == VOUCHER_TYPE_RIDE_FREE
|
|
&& peep->VoucherRideId == peep->CurrentRide)
|
|
return true;
|
|
|
|
if (peep->CashInPocket <= 0 && !(gParkFlags & PARK_FLAGS_NO_MONEY))
|
|
{
|
|
peep->InsertNewThought(PeepThoughtType::SpentMoney);
|
|
peep_update_ride_at_entrance_try_leave(peep);
|
|
return false;
|
|
}
|
|
|
|
if (ridePrice > peep->CashInPocket)
|
|
{
|
|
peep->InsertNewThought(PeepThoughtType::CantAffordRide, peep->CurrentRide);
|
|
peep_update_ride_at_entrance_try_leave(peep);
|
|
return false;
|
|
}
|
|
|
|
uint16_t value = ride->value;
|
|
if (value != RIDE_VALUE_UNDEFINED)
|
|
{
|
|
if (value * 2 < ridePrice)
|
|
{
|
|
peep->InsertNewThought(PeepThoughtType::BadValue, peep->CurrentRide);
|
|
peep_update_ride_at_entrance_try_leave(peep);
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* The satisfaction values calculated here are used to determine how happy the peep is with the ride,
|
|
* and also affects the satisfaction stat of the ride itself. The factors that affect satisfaction include:
|
|
* - The price of the ride compared to the ride's value
|
|
* - How closely the intensity and nausea of the ride matches the peep's preferences
|
|
* - How long the peep was waiting in the queue
|
|
* - If the peep has been on the ride before, or on another ride of the same type
|
|
*/
|
|
static int16_t peep_calculate_ride_satisfaction(Guest* peep, Ride* ride)
|
|
{
|
|
int16_t satisfaction = peep_calculate_ride_value_satisfaction(peep, ride);
|
|
satisfaction += peep_calculate_ride_intensity_nausea_satisfaction(peep, ride);
|
|
|
|
// Calculate satisfaction based on how long the peep has been in the queue for.
|
|
// (For comparison: peeps start thinking "I've been queueing for a long time" at 3500 and
|
|
// start leaving the queue at 4300.)
|
|
if (peep->TimeInQueue >= 4500)
|
|
satisfaction -= 35;
|
|
else if (peep->TimeInQueue >= 2250)
|
|
satisfaction -= 10;
|
|
else if (peep->TimeInQueue <= 750)
|
|
satisfaction += 10;
|
|
|
|
// Peeps get a small boost in satisfaction if they've been on a ride of the same type before,
|
|
// and this boost is doubled if they've already been on this particular ride.
|
|
if (peep->HasRiddenRideType(ride->type))
|
|
satisfaction += 10;
|
|
|
|
if (peep->HasRidden(get_ride(peep->CurrentRide)))
|
|
satisfaction += 10;
|
|
|
|
return satisfaction;
|
|
}
|
|
|
|
/**
|
|
* Check to see if the specified ride should become the peep's favourite.
|
|
* For this, a "ride rating" is calculated based on the excitement of the ride and the peep's current happiness.
|
|
* As this value cannot exceed 255, the happier the peep is, the more irrelevant the ride's excitement becomes.
|
|
* Due to the minimum happiness requirement, an excitement rating of more than 3.8 has no further effect.
|
|
*
|
|
* If the ride rating is higher than any ride the peep has already been on and the happiness criteria is met,
|
|
* the ride becomes the peep's favourite. (This doesn't happen right away, but will be updated once the peep
|
|
* exits the ride.)
|
|
*/
|
|
static void peep_update_favourite_ride(Guest* peep, Ride* ride)
|
|
{
|
|
peep->PeepFlags &= ~PEEP_FLAGS_RIDE_SHOULD_BE_MARKED_AS_FAVOURITE;
|
|
uint8_t peepRideRating = std::clamp((ride->excitement / 4) + peep->Happiness, 0, PEEP_MAX_HAPPINESS);
|
|
if (peepRideRating >= peep->FavouriteRideRating)
|
|
{
|
|
if (peep->Happiness >= 160 && peep->HappinessTarget >= 160)
|
|
{
|
|
peep->FavouriteRideRating = peepRideRating;
|
|
peep->PeepFlags |= PEEP_FLAGS_RIDE_SHOULD_BE_MARKED_AS_FAVOURITE;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* rct2: 0x00695555 */
|
|
static int16_t peep_calculate_ride_value_satisfaction(Guest* peep, Ride* ride)
|
|
{
|
|
if (gParkFlags & PARK_FLAGS_NO_MONEY)
|
|
{
|
|
return -30;
|
|
}
|
|
|
|
if (ride->value == RIDE_VALUE_UNDEFINED)
|
|
{
|
|
return -30;
|
|
}
|
|
|
|
money16 ridePrice = ride_get_price(ride);
|
|
if (ride->value >= ridePrice)
|
|
{
|
|
return -5;
|
|
}
|
|
|
|
if ((ride->value + ((ride->value * peep->Happiness) / 256)) >= ridePrice)
|
|
{
|
|
return -30;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Calculate satisfaction based on the intensity and nausea of the ride.
|
|
* The best possible score from this section is achieved by having the intensity and nausea
|
|
* of the ride fall exactly within the peep's preferences, but lower scores can still be achieved
|
|
* if the peep's happiness is enough to offset it.
|
|
*/
|
|
static int16_t peep_calculate_ride_intensity_nausea_satisfaction(Guest* peep, Ride* ride)
|
|
{
|
|
if (!ride_has_ratings(ride))
|
|
{
|
|
return 70;
|
|
}
|
|
|
|
uint8_t intensitySatisfaction = 3;
|
|
uint8_t nauseaSatisfaction = 3;
|
|
ride_rating maxIntensity = peep->Intensity.GetMaximum() * 100;
|
|
ride_rating minIntensity = peep->Intensity.GetMinimum() * 100;
|
|
if (minIntensity <= ride->intensity && maxIntensity >= ride->intensity)
|
|
{
|
|
intensitySatisfaction--;
|
|
}
|
|
minIntensity -= peep->Happiness * 2;
|
|
maxIntensity += peep->Happiness;
|
|
if (minIntensity <= ride->intensity && maxIntensity >= ride->intensity)
|
|
{
|
|
intensitySatisfaction--;
|
|
}
|
|
minIntensity -= peep->Happiness * 2;
|
|
maxIntensity += peep->Happiness;
|
|
if (minIntensity <= ride->intensity && maxIntensity >= ride->intensity)
|
|
{
|
|
intensitySatisfaction--;
|
|
}
|
|
|
|
// Although it's not shown in the interface, a peep with Average or High nausea tolerance
|
|
// has a minimum preferred nausea value. (For peeps with None or Low, this is set to zero.)
|
|
ride_rating minNausea = NauseaMinimumThresholds[(EnumValue(peep->NauseaTolerance) & 3)];
|
|
ride_rating maxNausea = NauseaMaximumThresholds[(EnumValue(peep->NauseaTolerance) & 3)];
|
|
if (minNausea <= ride->nausea && maxNausea >= ride->nausea)
|
|
{
|
|
nauseaSatisfaction--;
|
|
}
|
|
minNausea -= peep->Happiness * 2;
|
|
maxNausea += peep->Happiness;
|
|
if (minNausea <= ride->nausea && maxNausea >= ride->nausea)
|
|
{
|
|
nauseaSatisfaction--;
|
|
}
|
|
minNausea -= peep->Happiness * 2;
|
|
maxNausea += peep->Happiness;
|
|
if (minNausea <= ride->nausea && maxNausea >= ride->nausea)
|
|
{
|
|
nauseaSatisfaction--;
|
|
}
|
|
|
|
uint8_t highestSatisfaction = std::max(intensitySatisfaction, nauseaSatisfaction);
|
|
uint8_t lowestSatisfaction = std::min(intensitySatisfaction, nauseaSatisfaction);
|
|
|
|
switch (highestSatisfaction)
|
|
{
|
|
default:
|
|
case 0:
|
|
return 70;
|
|
case 1:
|
|
switch (lowestSatisfaction)
|
|
{
|
|
default:
|
|
case 0:
|
|
return 50;
|
|
case 1:
|
|
return 35;
|
|
}
|
|
case 2:
|
|
switch (lowestSatisfaction)
|
|
{
|
|
default:
|
|
case 0:
|
|
return 35;
|
|
case 1:
|
|
return 20;
|
|
case 2:
|
|
return 10;
|
|
}
|
|
case 3:
|
|
switch (lowestSatisfaction)
|
|
{
|
|
default:
|
|
case 0:
|
|
return -35;
|
|
case 1:
|
|
return -50;
|
|
case 2:
|
|
return -60;
|
|
case 3:
|
|
return -60;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the nausea growth of the peep based on a ride. This is calculated based on:
|
|
* - The nausea rating of the ride
|
|
* - Their new happiness growth rate (the higher, the less nauseous)
|
|
* - How hungry the peep is (+0% nausea at 50% hunger up to +100% nausea at 100% hunger)
|
|
* - The peep's nausea tolerance (Final modifier: none: 100%, low: 50%, average: 25%, high: 12.5%)
|
|
*/
|
|
static void peep_update_ride_nausea_growth(Guest* peep, Ride* ride)
|
|
{
|
|
uint32_t nauseaMultiplier = std::clamp(256 - peep->HappinessTarget, 64, 200);
|
|
uint32_t nauseaGrowthRateChange = (ride->nausea * nauseaMultiplier) / 512;
|
|
nauseaGrowthRateChange *= std::max(static_cast<uint8_t>(128), peep->Hunger) / 64;
|
|
nauseaGrowthRateChange >>= (EnumValue(peep->NauseaTolerance) & 3);
|
|
peep->NauseaTarget = static_cast<uint8_t>(std::min(peep->NauseaTarget + nauseaGrowthRateChange, 255u));
|
|
}
|
|
|
|
static bool peep_should_go_on_ride_again(Guest* peep, Ride* ride)
|
|
{
|
|
if (!ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_PEEP_WILL_RIDE_AGAIN))
|
|
return false;
|
|
if (!ride_has_ratings(ride))
|
|
return false;
|
|
if (ride->intensity > RIDE_RATING(10, 00) && !gCheatsIgnoreRideIntensity)
|
|
return false;
|
|
if (peep->Happiness < 180)
|
|
return false;
|
|
if (peep->Energy < 100)
|
|
return false;
|
|
if (peep->Nausea > 160)
|
|
return false;
|
|
if (peep->Hunger < 30)
|
|
return false;
|
|
if (peep->Thirst < 20)
|
|
return false;
|
|
if (peep->Toilet > 170)
|
|
return false;
|
|
|
|
uint8_t r = (scenario_rand() & 0xFF);
|
|
if (r <= 128)
|
|
{
|
|
if (peep->GuestNumRides > 7)
|
|
return false;
|
|
if (r > 64)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static bool peep_should_preferred_intensity_increase(Guest* peep)
|
|
{
|
|
if (gParkFlags & PARK_FLAGS_PREF_LESS_INTENSE_RIDES)
|
|
return false;
|
|
if (peep->Happiness < 200)
|
|
return false;
|
|
|
|
return (scenario_rand() & 0xFF) >= static_cast<uint8_t>(peep->Intensity);
|
|
}
|
|
|
|
static bool peep_really_liked_ride(Guest* peep, Ride* ride)
|
|
{
|
|
if (peep->Happiness < 215)
|
|
return false;
|
|
if (peep->Nausea > 120)
|
|
return false;
|
|
if (!ride_has_ratings(ride))
|
|
return false;
|
|
if (ride->intensity > RIDE_RATING(10, 00) && !gCheatsIgnoreRideIntensity)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069BC9A
|
|
*/
|
|
static PeepThoughtType peep_assess_surroundings(int16_t centre_x, int16_t centre_y, int16_t centre_z)
|
|
{
|
|
if ((tile_element_height({ centre_x, centre_y })) > centre_z)
|
|
return PeepThoughtType::None;
|
|
|
|
uint16_t num_scenery = 0;
|
|
uint16_t num_fountains = 0;
|
|
uint16_t nearby_music = 0;
|
|
uint16_t num_rubbish = 0;
|
|
|
|
int16_t initial_x = std::max(centre_x - 160, 0);
|
|
int16_t initial_y = std::max(centre_y - 160, 0);
|
|
int16_t final_x = std::min(centre_x + 160, MAXIMUM_MAP_SIZE_BIG);
|
|
int16_t final_y = std::min(centre_y + 160, MAXIMUM_MAP_SIZE_BIG);
|
|
|
|
for (int16_t x = initial_x; x < final_x; x += COORDS_XY_STEP)
|
|
{
|
|
for (int16_t y = initial_y; y < final_y; y += COORDS_XY_STEP)
|
|
{
|
|
for (auto* tileElement : TileElementsView({ x, y }))
|
|
{
|
|
Ride* ride;
|
|
|
|
switch (tileElement->GetType())
|
|
{
|
|
case TILE_ELEMENT_TYPE_PATH:
|
|
{
|
|
if (!tileElement->AsPath()->HasAddition())
|
|
break;
|
|
|
|
auto* pathAddEntry = tileElement->AsPath()->GetAdditionEntry();
|
|
if (pathAddEntry == nullptr)
|
|
{
|
|
return PeepThoughtType::None;
|
|
}
|
|
if (tileElement->AsPath()->AdditionIsGhost())
|
|
break;
|
|
|
|
if (pathAddEntry->flags & (PATH_BIT_FLAG_JUMPING_FOUNTAIN_WATER | PATH_BIT_FLAG_JUMPING_FOUNTAIN_SNOW))
|
|
{
|
|
num_fountains++;
|
|
break;
|
|
}
|
|
if (tileElement->AsPath()->IsBroken())
|
|
{
|
|
num_rubbish++;
|
|
}
|
|
break;
|
|
}
|
|
case TILE_ELEMENT_TYPE_LARGE_SCENERY:
|
|
case TILE_ELEMENT_TYPE_SMALL_SCENERY:
|
|
num_scenery++;
|
|
break;
|
|
case TILE_ELEMENT_TYPE_TRACK:
|
|
ride = get_ride(tileElement->AsTrack()->GetRideIndex());
|
|
if (ride != nullptr)
|
|
{
|
|
if (ride->lifecycle_flags & RIDE_LIFECYCLE_MUSIC && ride->status != RideStatus::Closed
|
|
&& !(ride->lifecycle_flags & (RIDE_LIFECYCLE_BROKEN_DOWN | RIDE_LIFECYCLE_CRASHED)))
|
|
{
|
|
if (ride->type == RIDE_TYPE_MERRY_GO_ROUND)
|
|
{
|
|
nearby_music |= 1;
|
|
break;
|
|
}
|
|
|
|
if (ride->music == MUSIC_STYLE_ORGAN)
|
|
{
|
|
nearby_music |= 1;
|
|
break;
|
|
}
|
|
|
|
if (ride->type == RIDE_TYPE_DODGEMS)
|
|
{
|
|
// Dodgems drown out music?
|
|
nearby_music |= 2;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (auto litter : EntityList<Litter>())
|
|
{
|
|
int16_t dist_x = abs(litter->x - centre_x);
|
|
int16_t dist_y = abs(litter->y - centre_y);
|
|
if (std::max(dist_x, dist_y) <= 160)
|
|
{
|
|
num_rubbish++;
|
|
}
|
|
}
|
|
|
|
if (num_fountains >= 5 && num_rubbish < 20)
|
|
return PeepThoughtType::Fountains;
|
|
|
|
if (num_scenery >= 40 && num_rubbish < 8)
|
|
return PeepThoughtType::Scenery;
|
|
|
|
if (nearby_music == 1 && num_rubbish < 20)
|
|
return PeepThoughtType::Music;
|
|
|
|
if (num_rubbish < 2 && !gCheatsDisableLittering)
|
|
// if disable littering cheat is enabled, peeps will not have the "clean and tidy park" thought
|
|
return PeepThoughtType::VeryClean;
|
|
|
|
return PeepThoughtType::None;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0068F9A9
|
|
*/
|
|
static void peep_update_hunger(Guest* peep)
|
|
{
|
|
if (peep->Hunger >= 3)
|
|
{
|
|
peep->Hunger -= 2;
|
|
|
|
peep->EnergyTarget = std::min(peep->EnergyTarget + 2, PEEP_MAX_ENERGY_TARGET);
|
|
peep->Toilet = std::min(peep->Toilet + 1, 255);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Main purpose is to decide when peeps leave the park due to
|
|
* low happiness, low energy and (if appropriate) low money.
|
|
*
|
|
* rct2: 0x0068F8CD
|
|
*/
|
|
static void peep_decide_whether_to_leave_park(Guest* peep)
|
|
{
|
|
if (peep->EnergyTarget >= 33)
|
|
{
|
|
peep->EnergyTarget -= 2;
|
|
}
|
|
|
|
if (gClimateCurrent.Temperature >= 21 && peep->Thirst >= 5)
|
|
{
|
|
peep->Thirst--;
|
|
}
|
|
|
|
if (peep->OutsideOfPark)
|
|
{
|
|
return;
|
|
}
|
|
|
|
/* Peeps that are happy enough, have enough energy and
|
|
* (if appropriate) have enough money will always stay
|
|
* in the park. */
|
|
if (!(peep->PeepFlags & PEEP_FLAGS_LEAVING_PARK))
|
|
{
|
|
if (gParkFlags & PARK_FLAGS_NO_MONEY)
|
|
{
|
|
if (peep->Energy >= 70 && peep->Happiness >= 60)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (peep->Energy >= 55 && peep->Happiness >= 45 && peep->CashInPocket >= MONEY(5, 00))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Approx 95% chance of staying in the park
|
|
if ((scenario_rand() & 0xFFFF) > 3276)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// In the remaining 5% chance the peep leaves the park.
|
|
peep_leave_park(peep);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0068F93E
|
|
*/
|
|
static void peep_leave_park(Guest* peep)
|
|
{
|
|
peep->GuestHeadingToRideId = RIDE_ID_NULL;
|
|
if (peep->PeepFlags & PEEP_FLAGS_LEAVING_PARK)
|
|
{
|
|
if (peep->GuestIsLostCountdown < 60)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
peep->GuestIsLostCountdown = 254;
|
|
peep->PeepFlags |= PEEP_FLAGS_LEAVING_PARK;
|
|
peep->PeepFlags &= ~PEEP_FLAGS_PARK_ENTRANCE_CHOSEN;
|
|
}
|
|
|
|
peep->InsertNewThought(PeepThoughtType::GoHome);
|
|
|
|
rct_window* w = window_find_by_number(WC_PEEP, peep->sprite_index);
|
|
if (w != nullptr)
|
|
window_event_invalidate_call(w);
|
|
window_invalidate_by_number(WC_PEEP, peep->sprite_index);
|
|
}
|
|
|
|
template<typename T> static void peep_head_for_nearest_ride(Guest* peep, bool considerOnlyCloseRides, T predicate)
|
|
{
|
|
if (peep->State != PeepState::Sitting && peep->State != PeepState::Watching && peep->State != PeepState::Walking)
|
|
{
|
|
return;
|
|
}
|
|
if (peep->PeepFlags & PEEP_FLAGS_LEAVING_PARK)
|
|
return;
|
|
if (peep->x == LOCATION_NULL)
|
|
return;
|
|
if (peep->GuestHeadingToRideId != RIDE_ID_NULL)
|
|
{
|
|
auto ride = get_ride(peep->GuestHeadingToRideId);
|
|
if (ride != nullptr && predicate(*ride))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
std::bitset<MAX_RIDES> rideConsideration;
|
|
if (!considerOnlyCloseRides && (peep->HasItem(ShopItem::Map)))
|
|
{
|
|
// Consider all rides in the park
|
|
for (const auto& ride : GetRideManager())
|
|
{
|
|
if (predicate(ride))
|
|
{
|
|
rideConsideration[EnumValue(ride.id)] = true;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Take nearby rides into consideration
|
|
constexpr auto searchRadius = 10 * 32;
|
|
int32_t cx = floor2(peep->x, 32);
|
|
int32_t cy = floor2(peep->y, 32);
|
|
for (auto x = cx - searchRadius; x <= cx + searchRadius; x += COORDS_XY_STEP)
|
|
{
|
|
for (auto y = cy - searchRadius; y <= cy + searchRadius; y += COORDS_XY_STEP)
|
|
{
|
|
auto location = CoordsXY{ x, y };
|
|
if (!map_is_location_valid(location))
|
|
continue;
|
|
|
|
for (auto* trackElement : TileElementsView<TrackElement>(location))
|
|
{
|
|
auto rideIndex = trackElement->GetRideIndex();
|
|
auto ride = get_ride(rideIndex);
|
|
if (ride == nullptr)
|
|
continue;
|
|
|
|
if (!predicate(*ride))
|
|
continue;
|
|
|
|
rideConsideration[EnumValue(ride->id)] = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Filter the considered rides
|
|
ride_id_t potentialRides[MAX_RIDES];
|
|
size_t numPotentialRides = 0;
|
|
for (auto& ride : GetRideManager())
|
|
{
|
|
if (rideConsideration[EnumValue(ride.id)])
|
|
{
|
|
if (!(ride.lifecycle_flags & RIDE_LIFECYCLE_QUEUE_FULL))
|
|
{
|
|
if (peep->ShouldGoOnRide(&ride, 0, false, true))
|
|
{
|
|
potentialRides[numPotentialRides++] = ride.id;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pick the closest ride
|
|
Ride* closestRide{};
|
|
auto closestRideDistance = std::numeric_limits<int32_t>::max();
|
|
for (size_t i = 0; i < numPotentialRides; i++)
|
|
{
|
|
auto ride = get_ride(potentialRides[i]);
|
|
if (ride != nullptr)
|
|
{
|
|
auto rideLocation = ride->stations[0].Start;
|
|
int32_t distance = abs(rideLocation.x - peep->x) + abs(rideLocation.y - peep->y);
|
|
if (distance < closestRideDistance)
|
|
{
|
|
closestRide = ride;
|
|
closestRideDistance = distance;
|
|
}
|
|
}
|
|
}
|
|
if (closestRide != nullptr)
|
|
{
|
|
// Head to that ride
|
|
peep->GuestHeadingToRideId = closestRide->id;
|
|
peep->GuestIsLostCountdown = 200;
|
|
peep->ResetPathfindGoal();
|
|
peep->WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_ACTION;
|
|
peep->TimeLost = 0;
|
|
}
|
|
}
|
|
|
|
static void peep_head_for_nearest_ride_type(Guest* peep, int32_t rideType)
|
|
{
|
|
auto considerOnlyCloseRides = rideType == RIDE_TYPE_FIRST_AID;
|
|
return peep_head_for_nearest_ride(
|
|
peep, considerOnlyCloseRides, [rideType](const Ride& ride) { return ride.type == rideType; });
|
|
}
|
|
|
|
static void peep_head_for_nearest_ride_with_flags(Guest* peep, int32_t rideTypeFlags)
|
|
{
|
|
if ((rideTypeFlags & RIDE_TYPE_FLAG_IS_TOILET) && peep->HasFoodOrDrink())
|
|
{
|
|
return;
|
|
}
|
|
peep_head_for_nearest_ride(
|
|
peep, false, [rideTypeFlags](const Ride& ride) { return ride.GetRideTypeDescriptor().HasFlag(rideTypeFlags); });
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00699FE3
|
|
* Stops peeps that are having thoughts
|
|
* such as "I'm hungry" after visiting a food shop.
|
|
* Works for Thirst/Hungry/Low Money/Toilet
|
|
*/
|
|
void Guest::StopPurchaseThought(uint8_t ride_type)
|
|
{
|
|
auto thoughtType = PeepThoughtType::Hungry;
|
|
|
|
if (!GetRideTypeDescriptor(ride_type).HasFlag(RIDE_TYPE_FLAG_SELLS_FOOD))
|
|
{
|
|
thoughtType = PeepThoughtType::Thirsty;
|
|
if (!GetRideTypeDescriptor(ride_type).HasFlag(RIDE_TYPE_FLAG_SELLS_DRINKS))
|
|
{
|
|
thoughtType = PeepThoughtType::RunningOut;
|
|
if (ride_type != RIDE_TYPE_CASH_MACHINE)
|
|
{
|
|
thoughtType = PeepThoughtType::Toilet;
|
|
if (!GetRideTypeDescriptor(ride_type).HasFlag(RIDE_TYPE_FLAG_IS_TOILET))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove the related thought
|
|
for (int32_t i = 0; i < PEEP_MAX_THOUGHTS; ++i)
|
|
{
|
|
PeepThought* thought = &Thoughts[i];
|
|
|
|
if (thought->type == PeepThoughtType::None)
|
|
break;
|
|
|
|
if (thought->type != thoughtType)
|
|
continue;
|
|
|
|
if (i < PEEP_MAX_THOUGHTS - 1)
|
|
{
|
|
memmove(thought, thought + 1, sizeof(PeepThought) * (PEEP_MAX_THOUGHTS - i - 1));
|
|
}
|
|
|
|
Thoughts[PEEP_MAX_THOUGHTS - 1].type = PeepThoughtType::None;
|
|
|
|
WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_THOUGHTS;
|
|
i--;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069AEB7
|
|
*/
|
|
static bool peep_should_use_cash_machine(Guest* peep, ride_id_t rideIndex)
|
|
{
|
|
if (gParkFlags & PARK_FLAGS_NO_MONEY)
|
|
return false;
|
|
if (peep->PeepFlags & PEEP_FLAGS_LEAVING_PARK)
|
|
return false;
|
|
if (peep->CashInPocket > MONEY(20, 00))
|
|
return false;
|
|
if (115 + (scenario_rand() % 128) > peep->Happiness)
|
|
return false;
|
|
if (peep->Energy < 80)
|
|
return false;
|
|
|
|
auto ride = get_ride(rideIndex);
|
|
if (ride != nullptr)
|
|
{
|
|
ride_update_satisfaction(ride, peep->Happiness >> 6);
|
|
ride->cur_num_customers++;
|
|
ride->total_customers++;
|
|
ride->window_invalidate_flags |= RIDE_INVALIDATE_RIDE_CUSTOMER;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006912A3
|
|
*/
|
|
void Guest::UpdateBuying()
|
|
{
|
|
if (!CheckForPath())
|
|
return;
|
|
|
|
auto ride = get_ride(CurrentRide);
|
|
if (ride == nullptr || ride->status != RideStatus::Open)
|
|
{
|
|
SetState(PeepState::Falling);
|
|
return;
|
|
}
|
|
|
|
if (SubState == 1)
|
|
{
|
|
if (!IsActionWalking())
|
|
{
|
|
UpdateAction();
|
|
Invalidate();
|
|
return;
|
|
}
|
|
|
|
if (ride->type == RIDE_TYPE_CASH_MACHINE)
|
|
{
|
|
if (CurrentRide != PreviousRide)
|
|
{
|
|
CashInPocket += MONEY(50, 00);
|
|
}
|
|
window_invalidate_by_number(WC_PEEP, sprite_index);
|
|
}
|
|
sprite_direction ^= 0x10;
|
|
|
|
auto destination = CoordsXY{ 16, 16 } + NextLoc;
|
|
SetDestination(destination);
|
|
PeepDirection = direction_reverse(PeepDirection);
|
|
|
|
SetState(PeepState::Walking);
|
|
return;
|
|
}
|
|
|
|
bool item_bought = false;
|
|
|
|
if (CurrentRide != PreviousRide)
|
|
{
|
|
if (ride->type == RIDE_TYPE_CASH_MACHINE)
|
|
{
|
|
item_bought = peep_should_use_cash_machine(this, CurrentRide);
|
|
if (!item_bought)
|
|
{
|
|
PreviousRide = CurrentRide;
|
|
PreviousRideTimeOut = 0;
|
|
}
|
|
else
|
|
{
|
|
Action = PeepActionType::WithdrawMoney;
|
|
ActionFrame = 0;
|
|
ActionSpriteImageOffset = 0;
|
|
|
|
UpdateCurrentActionSpriteType();
|
|
|
|
ride->no_primary_items_sold++;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
rct_ride_entry* ride_type = get_ride_entry(ride->subtype);
|
|
if (ride_type == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
if (ride_type->shop_item[1] != ShopItem::None)
|
|
{
|
|
money16 price = ride->price[1];
|
|
|
|
item_bought = DecideAndBuyItem(ride, ride_type->shop_item[1], price);
|
|
if (item_bought)
|
|
{
|
|
ride->no_secondary_items_sold++;
|
|
}
|
|
}
|
|
|
|
if (!item_bought && ride_type->shop_item[0] != ShopItem::None)
|
|
{
|
|
money16 price = ride->price[0];
|
|
|
|
item_bought = DecideAndBuyItem(ride, ride_type->shop_item[0], price);
|
|
if (item_bought)
|
|
{
|
|
ride->no_primary_items_sold++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (item_bought)
|
|
{
|
|
ride_update_popularity(ride, 1);
|
|
|
|
StopPurchaseThought(ride->type);
|
|
}
|
|
else
|
|
{
|
|
ride_update_popularity(ride, 0);
|
|
}
|
|
SubState = 1;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00691A3B
|
|
*/
|
|
void Guest::UpdateRideAtEntrance()
|
|
{
|
|
auto ride = get_ride(CurrentRide);
|
|
if (ride == nullptr)
|
|
return;
|
|
|
|
// The peep will keep advancing in the entranceway
|
|
// whilst in this state. When it has reached the very
|
|
// front of the queue destination tolerance is set to
|
|
// zero to indicate it is final decision time (try_leave will pass).
|
|
// When a peep has to return to the queue without getting on a ride
|
|
// this is the state it will return to.
|
|
if (DestinationTolerance != 0)
|
|
{
|
|
int16_t xy_distance;
|
|
if (auto loc = UpdateAction(xy_distance); loc.has_value())
|
|
{
|
|
int16_t actionZ = z;
|
|
if (xy_distance < 16)
|
|
{
|
|
auto entrance = ride_get_entrance_location(ride, CurrentRideStation).ToCoordsXYZ();
|
|
actionZ = entrance.z + 2;
|
|
}
|
|
MoveTo({ loc.value(), actionZ });
|
|
}
|
|
else
|
|
{
|
|
DestinationTolerance = 0;
|
|
sprite_direction ^= (1 << 4);
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
std::vector<uint8_t> carArray;
|
|
|
|
if (ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_NO_VEHICLES))
|
|
{
|
|
if (ride->num_riders >= ride->operation_option)
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
if (!FindVehicleToEnter(ride, carArray))
|
|
return;
|
|
}
|
|
|
|
if (ride->status != RideStatus::Open || ride->vehicle_change_timeout != 0)
|
|
{
|
|
peep_update_ride_at_entrance_try_leave(this);
|
|
return;
|
|
}
|
|
|
|
if (ride->lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN)
|
|
return;
|
|
|
|
money16 ridePrice = ride_get_price(ride);
|
|
if (ridePrice != 0)
|
|
{
|
|
if (!peep_check_ride_price_at_entrance(this, ride, ridePrice))
|
|
return;
|
|
}
|
|
|
|
if (!ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_NO_VEHICLES))
|
|
{
|
|
Vehicle* vehicle = peep_choose_car_from_ride(this, ride, carArray);
|
|
peep_choose_seat_from_car(this, ride, vehicle);
|
|
}
|
|
GoToRideEntrance(ride);
|
|
}
|
|
|
|
/** rct2: 0x00981FD4, 0x00981FD6 */
|
|
static constexpr const CoordsXY _MazeEntranceStart[] = {
|
|
{ 8, 8 },
|
|
{ 8, 24 },
|
|
{ 24, 24 },
|
|
{ 24, 8 },
|
|
};
|
|
|
|
static void peep_update_ride_leave_entrance_maze(Guest* peep, Ride* ride, CoordsXYZD& entrance_loc)
|
|
{
|
|
peep->MazeLastEdge = entrance_loc.direction + 1;
|
|
|
|
entrance_loc.x += CoordsDirectionDelta[entrance_loc.direction].x;
|
|
entrance_loc.y += CoordsDirectionDelta[entrance_loc.direction].y;
|
|
|
|
uint8_t direction = entrance_loc.direction * 4 + 11;
|
|
if (scenario_rand() & 0x40)
|
|
{
|
|
direction += 4;
|
|
peep->MazeLastEdge += 2;
|
|
}
|
|
|
|
direction &= 0xF;
|
|
// Direction is 11, 15, 3, or 7
|
|
peep->Var37 = direction;
|
|
peep->MazeLastEdge &= 3;
|
|
|
|
entrance_loc.x += _MazeEntranceStart[direction / 4].x;
|
|
entrance_loc.y += _MazeEntranceStart[direction / 4].y;
|
|
|
|
peep->SetDestination(entrance_loc, 3);
|
|
|
|
ride->cur_num_customers++;
|
|
peep->OnEnterRide(ride);
|
|
peep->RideSubState = PeepRideSubState::MazePathfinding;
|
|
}
|
|
|
|
static void peep_update_ride_leave_entrance_spiral_slide(Guest* peep, Ride* ride, CoordsXYZD& entrance_loc)
|
|
{
|
|
entrance_loc = { ride->stations[peep->CurrentRideStation].GetStart(), entrance_loc.direction };
|
|
|
|
TileElement* tile_element = ride_get_station_start_track_element(ride, peep->CurrentRideStation);
|
|
|
|
uint8_t direction_track = (tile_element == nullptr ? 0 : tile_element->GetDirection());
|
|
|
|
peep->Var37 = (entrance_loc.direction << 2) | (direction_track << 4);
|
|
|
|
entrance_loc += SpiralSlideWalkingPath[peep->Var37];
|
|
|
|
peep->SetDestination(entrance_loc);
|
|
peep->CurrentCar = 0;
|
|
|
|
ride->cur_num_customers++;
|
|
peep->OnEnterRide(ride);
|
|
peep->RideSubState = PeepRideSubState::ApproachSpiralSlide;
|
|
}
|
|
|
|
uint8_t Guest::GetWaypointedSeatLocation(const Ride& ride, rct_ride_entry_vehicle* vehicle_type, uint8_t track_direction) const
|
|
{
|
|
// The seatlocation can be split into segments around the ride base
|
|
// to decide the segment first split off the segmentable seat location
|
|
// from the fixed section
|
|
uint8_t seatLocationSegment = CurrentSeat & 0x7;
|
|
uint8_t seatLocationFixed = CurrentSeat & 0xF8;
|
|
|
|
// Enterprise has more segments (8) compared to the normal (4)
|
|
if (ride.type != RIDE_TYPE_ENTERPRISE)
|
|
track_direction *= 2;
|
|
|
|
// Type 1 loading doesn't do segments and all peeps go to the same
|
|
// location on the ride
|
|
if (vehicle_type->peep_loading_waypoint_segments == 0)
|
|
{
|
|
track_direction /= 2;
|
|
seatLocationSegment = 0;
|
|
seatLocationFixed = 0;
|
|
}
|
|
seatLocationSegment += track_direction;
|
|
seatLocationSegment &= 0x7;
|
|
return seatLocationSegment + seatLocationFixed;
|
|
}
|
|
|
|
void Guest::UpdateRideLeaveEntranceWaypoints(const Ride& ride)
|
|
{
|
|
TileCoordsXYZD entranceLocation = ride_get_entrance_location(&ride, CurrentRideStation);
|
|
Guard::Assert(!entranceLocation.IsNull());
|
|
uint8_t direction_entrance = entranceLocation.direction;
|
|
|
|
CoordsXY waypoint = ride.stations[CurrentRideStation].Start.ToTileCentre();
|
|
|
|
TileElement* tile_element = ride_get_station_start_track_element(&ride, CurrentRideStation);
|
|
|
|
uint8_t direction_track = (tile_element == nullptr ? 0 : tile_element->GetDirection());
|
|
|
|
auto vehicle = GetEntity<Vehicle>(ride.vehicles[CurrentTrain]);
|
|
if (vehicle == nullptr)
|
|
{
|
|
// TODO: Goto ride exit on failure.
|
|
return;
|
|
}
|
|
auto ride_entry = vehicle->GetRideEntry();
|
|
auto vehicle_type = &ride_entry->vehicles[vehicle->vehicle_type];
|
|
|
|
Var37 = (direction_entrance | GetWaypointedSeatLocation(ride, vehicle_type, direction_track) * 4) * 4;
|
|
|
|
if (ride.type == RIDE_TYPE_ENTERPRISE)
|
|
{
|
|
waypoint.x = vehicle->x;
|
|
waypoint.y = vehicle->y;
|
|
}
|
|
|
|
const auto waypointIndex = Var37 / 4;
|
|
Guard::Assert(vehicle_type->peep_loading_waypoints.size() >= static_cast<size_t>(waypointIndex));
|
|
waypoint.x += vehicle_type->peep_loading_waypoints[waypointIndex][0].x;
|
|
waypoint.y += vehicle_type->peep_loading_waypoints[waypointIndex][0].y;
|
|
|
|
SetDestination(waypoint);
|
|
RideSubState = PeepRideSubState::ApproachVehicleWaypoints;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006921D3
|
|
*/
|
|
void Guest::UpdateRideAdvanceThroughEntrance()
|
|
{
|
|
auto ride = get_ride(CurrentRide);
|
|
if (ride == nullptr)
|
|
return;
|
|
|
|
int16_t actionZ, xy_distance;
|
|
|
|
auto ride_entry = ride->GetRideEntry();
|
|
|
|
if (auto loc = UpdateAction(xy_distance); loc.has_value())
|
|
{
|
|
uint16_t distanceThreshold = 16;
|
|
if (ride_entry != nullptr)
|
|
{
|
|
uint8_t vehicle = ride_entry->default_vehicle;
|
|
if (ride_entry->vehicles[vehicle].flags & VEHICLE_ENTRY_FLAG_MINI_GOLF
|
|
|| ride_entry->vehicles[vehicle].flags & (VEHICLE_ENTRY_FLAG_CHAIRLIFT | VEHICLE_ENTRY_FLAG_GO_KART))
|
|
{
|
|
distanceThreshold = 28;
|
|
}
|
|
}
|
|
|
|
if (RideSubState == PeepRideSubState::InEntrance && xy_distance < distanceThreshold)
|
|
{
|
|
RideSubState = PeepRideSubState::FreeVehicleCheck;
|
|
}
|
|
|
|
actionZ = ride->stations[CurrentRideStation].GetBaseZ();
|
|
|
|
distanceThreshold += 4;
|
|
if (xy_distance < distanceThreshold)
|
|
{
|
|
actionZ += ride->GetRideTypeDescriptor().Heights.PlatformHeight;
|
|
}
|
|
|
|
MoveTo({ loc.value(), actionZ });
|
|
return;
|
|
}
|
|
|
|
Guard::Assert(RideSubState == PeepRideSubState::LeaveEntrance, "Peep ridesubstate should be LeaveEntrance");
|
|
if (ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_NO_VEHICLES))
|
|
{
|
|
auto entranceLocation = ride_get_entrance_location(ride, CurrentRideStation).ToCoordsXYZD();
|
|
Guard::Assert(!entranceLocation.IsNull());
|
|
|
|
if (ride->type == RIDE_TYPE_MAZE)
|
|
{
|
|
peep_update_ride_leave_entrance_maze(this, ride, entranceLocation);
|
|
return;
|
|
}
|
|
if (ride->type == RIDE_TYPE_SPIRAL_SLIDE)
|
|
{
|
|
peep_update_ride_leave_entrance_spiral_slide(this, ride, entranceLocation);
|
|
return;
|
|
}
|
|
|
|
// If the ride type was changed guests will become stuck.
|
|
// Inform the player about this if its a new issue or hasn't been addressed within 120 seconds.
|
|
if ((ride->current_issues & RIDE_ISSUE_GUESTS_STUCK) == 0 || gCurrentTicks - ride->last_issue_time > 3000)
|
|
{
|
|
ride->current_issues |= RIDE_ISSUE_GUESTS_STUCK;
|
|
ride->last_issue_time = gCurrentTicks;
|
|
|
|
auto ft = Formatter();
|
|
ride->FormatNameTo(ft);
|
|
if (gConfigNotifications.ride_warnings)
|
|
{
|
|
News::AddItemToQueue(News::ItemType::Ride, STR_GUESTS_GETTING_STUCK_ON_RIDE, EnumValue(CurrentRide), ft);
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
Vehicle* vehicle = GetEntity<Vehicle>(ride->vehicles[CurrentTrain]);
|
|
if (vehicle == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
vehicle = vehicle->GetCar(CurrentCar);
|
|
if (vehicle == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ride_entry = vehicle->GetRideEntry();
|
|
if (ride_entry == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
rct_ride_entry_vehicle* vehicle_type = &ride_entry->vehicles[vehicle->vehicle_type];
|
|
|
|
if (vehicle_type->flags & VEHICLE_ENTRY_FLAG_LOADING_WAYPOINTS)
|
|
{
|
|
UpdateRideLeaveEntranceWaypoints(*ride);
|
|
return;
|
|
}
|
|
|
|
if (vehicle_type->flags & VEHICLE_ENTRY_FLAG_DODGEM_CAR_PLACEMENT)
|
|
{
|
|
SetDestination(vehicle->GetLocation(), 15);
|
|
RideSubState = PeepRideSubState::ApproachVehicle;
|
|
return;
|
|
}
|
|
|
|
int8_t load_position = 0;
|
|
// Safe, in case current seat > number of loading positions
|
|
uint16_t numSeatPositions = static_cast<uint16_t>(vehicle_type->peep_loading_positions.size());
|
|
if (numSeatPositions != 0)
|
|
{
|
|
size_t loadPositionIndex = numSeatPositions - 1;
|
|
if (CurrentSeat < numSeatPositions)
|
|
{
|
|
loadPositionIndex = CurrentSeat;
|
|
}
|
|
load_position = vehicle_type->peep_loading_positions[loadPositionIndex];
|
|
}
|
|
|
|
auto destination = GetDestination();
|
|
switch (vehicle->sprite_direction / 8)
|
|
{
|
|
case 0:
|
|
destination.x = vehicle->x - load_position;
|
|
break;
|
|
case 1:
|
|
destination.y = vehicle->y + load_position;
|
|
break;
|
|
case 2:
|
|
destination.x = vehicle->x + load_position;
|
|
break;
|
|
case 3:
|
|
destination.y = vehicle->y - load_position;
|
|
break;
|
|
}
|
|
SetDestination(destination);
|
|
|
|
RideSubState = PeepRideSubState::ApproachVehicle;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069321D
|
|
*/
|
|
static void peep_go_to_ride_exit(Peep* peep, Ride* ride, int16_t x, int16_t y, int16_t z, uint8_t exit_direction)
|
|
{
|
|
z += ride->GetRideTypeDescriptor().Heights.PlatformHeight;
|
|
|
|
peep->MoveTo({ x, y, z });
|
|
|
|
Guard::Assert(peep->CurrentRideStation < MAX_STATIONS);
|
|
auto exit = ride_get_exit_location(ride, peep->CurrentRideStation);
|
|
Guard::Assert(!exit.IsNull());
|
|
x = exit.x;
|
|
y = exit.y;
|
|
x *= 32;
|
|
y *= 32;
|
|
x += 16;
|
|
y += 16;
|
|
|
|
int16_t x_shift = DirectionOffsets[exit_direction].x;
|
|
int16_t y_shift = DirectionOffsets[exit_direction].y;
|
|
|
|
int16_t shift_multiplier = 20;
|
|
|
|
rct_ride_entry* rideEntry = get_ride_entry(ride->subtype);
|
|
if (rideEntry != nullptr)
|
|
{
|
|
rct_ride_entry_vehicle* vehicle_entry = &rideEntry->vehicles[rideEntry->default_vehicle];
|
|
if (vehicle_entry->flags & VEHICLE_ENTRY_FLAG_MINI_GOLF
|
|
|| vehicle_entry->flags & (VEHICLE_ENTRY_FLAG_CHAIRLIFT | VEHICLE_ENTRY_FLAG_GO_KART))
|
|
{
|
|
shift_multiplier = 32;
|
|
}
|
|
}
|
|
|
|
x_shift *= shift_multiplier;
|
|
y_shift *= shift_multiplier;
|
|
|
|
x -= x_shift;
|
|
y -= y_shift;
|
|
|
|
peep->SetDestination({ x, y }, 2);
|
|
|
|
peep->sprite_direction = exit_direction * 8;
|
|
peep->RideSubState = PeepRideSubState::ApproachExit;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006920B4
|
|
*/
|
|
void Guest::UpdateRideFreeVehicleEnterRide(Ride* ride)
|
|
{
|
|
money16 ridePrice = ride_get_price(ride);
|
|
if (ridePrice != 0)
|
|
{
|
|
if ((HasItem(ShopItem::Voucher)) && (VoucherType == VOUCHER_TYPE_RIDE_FREE) && (VoucherRideId == CurrentRide))
|
|
{
|
|
RemoveItem(ShopItem::Voucher);
|
|
WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_INVENTORY;
|
|
}
|
|
else
|
|
{
|
|
ride->total_profit += ridePrice;
|
|
ride->window_invalidate_flags |= RIDE_INVALIDATE_RIDE_INCOME;
|
|
SpendMoney(PaidOnRides, ridePrice, ExpenditureType::ParkRideTickets);
|
|
}
|
|
}
|
|
|
|
RideSubState = PeepRideSubState::LeaveEntrance;
|
|
uint8_t queueTime = DaysInQueue;
|
|
if (queueTime < 253)
|
|
queueTime += 3;
|
|
|
|
queueTime /= 2;
|
|
if (queueTime != ride->stations[CurrentRideStation].QueueTime)
|
|
{
|
|
ride->stations[CurrentRideStation].QueueTime = queueTime;
|
|
window_invalidate_by_number(WC_RIDE, EnumValue(CurrentRide));
|
|
}
|
|
|
|
if (PeepFlags & PEEP_FLAGS_TRACKING)
|
|
{
|
|
auto ft = Formatter();
|
|
FormatNameTo(ft);
|
|
ride->FormatNameTo(ft);
|
|
|
|
rct_string_id msg_string;
|
|
if (ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_IN_RIDE))
|
|
msg_string = STR_PEEP_TRACKING_PEEP_IS_IN_X;
|
|
else
|
|
msg_string = STR_PEEP_TRACKING_PEEP_IS_ON_X;
|
|
|
|
if (gConfigNotifications.guest_on_ride)
|
|
{
|
|
News::AddItemToQueue(News::ItemType::PeepOnRide, msg_string, sprite_index, ft);
|
|
}
|
|
}
|
|
|
|
if (ride->type == RIDE_TYPE_SPIRAL_SLIDE)
|
|
{
|
|
SwitchToSpecialSprite(1);
|
|
}
|
|
|
|
UpdateRideAdvanceThroughEntrance();
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00691FD4
|
|
*/
|
|
static void peep_update_ride_no_free_vehicle_rejoin_queue(Guest* peep, Ride* ride)
|
|
{
|
|
TileCoordsXYZD entranceLocation = ride_get_entrance_location(ride, peep->CurrentRideStation);
|
|
|
|
int32_t x = entranceLocation.x * 32;
|
|
int32_t y = entranceLocation.y * 32;
|
|
x += 16 - DirectionOffsets[entranceLocation.direction].x * 20;
|
|
y += 16 - DirectionOffsets[entranceLocation.direction].y * 20;
|
|
|
|
peep->SetDestination({ x, y }, 2);
|
|
peep->SetState(PeepState::QueuingFront);
|
|
peep->RideSubState = PeepRideSubState::AtEntrance;
|
|
|
|
ride->QueueInsertGuestAtFront(peep->CurrentRideStation, peep);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00691E42
|
|
* Note: Before this was the entry
|
|
* point for sub state 1 and 3. The
|
|
* check has been removed that would
|
|
* branch it out to 1 and 3. Now uses
|
|
* separate functions.
|
|
*/
|
|
void Guest::UpdateRideFreeVehicleCheck()
|
|
{
|
|
auto ride = get_ride(CurrentRide);
|
|
if (ride == nullptr)
|
|
return;
|
|
|
|
if (ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_NO_VEHICLES))
|
|
{
|
|
if (ride->status != RideStatus::Open || ride->vehicle_change_timeout != 0 || (++RejoinQueueTimeout) == 0)
|
|
{
|
|
peep_update_ride_no_free_vehicle_rejoin_queue(this, ride);
|
|
return;
|
|
}
|
|
|
|
UpdateRideFreeVehicleEnterRide(ride);
|
|
return;
|
|
}
|
|
|
|
Vehicle* vehicle = GetEntity<Vehicle>(ride->vehicles[CurrentTrain]);
|
|
if (vehicle == nullptr)
|
|
{
|
|
// TODO: Leave ride on failure goes for all returns on nullptr in this function
|
|
return;
|
|
}
|
|
vehicle = vehicle->GetCar(CurrentCar);
|
|
if (vehicle == nullptr)
|
|
return;
|
|
|
|
rct_ride_entry* ride_entry = vehicle->GetRideEntry();
|
|
if (ride_entry == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ride_entry->vehicles[0].flags & VEHICLE_ENTRY_FLAG_MINI_GOLF)
|
|
{
|
|
vehicle->mini_golf_flags &= ~MiniGolfFlag::Flag5;
|
|
|
|
for (size_t i = 0; i < ride->num_vehicles; ++i)
|
|
{
|
|
Vehicle* train = GetEntity<Vehicle>(ride->vehicles[i]);
|
|
if (train == nullptr)
|
|
continue;
|
|
|
|
Vehicle* second_vehicle = GetEntity<Vehicle>(train->next_vehicle_on_train);
|
|
if (second_vehicle == nullptr)
|
|
continue;
|
|
|
|
if (second_vehicle->num_peeps == 0)
|
|
continue;
|
|
|
|
if (second_vehicle->mini_golf_flags & MiniGolfFlag::Flag5)
|
|
continue;
|
|
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!vehicle->IsUsedInPairs())
|
|
{
|
|
UpdateRideFreeVehicleEnterRide(ride);
|
|
return;
|
|
}
|
|
|
|
if (ride->mode == RideMode::ForwardRotation || ride->mode == RideMode::BackwardRotation)
|
|
{
|
|
if (CurrentSeat & 1 || !(vehicle->next_free_seat & 1))
|
|
{
|
|
UpdateRideFreeVehicleEnterRide(ride);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
uint8_t seat = CurrentSeat | 1;
|
|
if (seat < vehicle->next_free_seat)
|
|
{
|
|
UpdateRideFreeVehicleEnterRide(ride);
|
|
return;
|
|
}
|
|
}
|
|
|
|
Vehicle* currentTrain = GetEntity<Vehicle>(ride->vehicles[CurrentTrain]);
|
|
if (currentTrain == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
if (ride->status == RideStatus::Open && ++RejoinQueueTimeout != 0
|
|
&& !currentTrain->HasUpdateFlag(VEHICLE_UPDATE_FLAG_TRAIN_READY_DEPART))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ride->mode != RideMode::ForwardRotation && ride->mode != RideMode::BackwardRotation)
|
|
{
|
|
if (vehicle->next_free_seat - 1 != CurrentSeat)
|
|
return;
|
|
}
|
|
|
|
vehicle->next_free_seat--;
|
|
vehicle->peep[CurrentSeat] = SPRITE_INDEX_NULL;
|
|
|
|
peep_update_ride_no_free_vehicle_rejoin_queue(this, ride);
|
|
}
|
|
|
|
void Guest::UpdateRideApproachVehicle()
|
|
{
|
|
if (auto loc = UpdateAction(); loc.has_value())
|
|
{
|
|
MoveTo({ loc.value(), z });
|
|
return;
|
|
}
|
|
RideSubState = PeepRideSubState::EnterVehicle;
|
|
}
|
|
|
|
void Guest::UpdateRideEnterVehicle()
|
|
{
|
|
auto* ride = get_ride(CurrentRide);
|
|
if (ride != nullptr)
|
|
{
|
|
auto* vehicle = GetEntity<Vehicle>(ride->vehicles[CurrentTrain]);
|
|
if (vehicle != nullptr)
|
|
{
|
|
vehicle = vehicle->GetCar(CurrentCar);
|
|
if (vehicle == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ride->mode != RideMode::ForwardRotation && ride->mode != RideMode::BackwardRotation)
|
|
{
|
|
if (CurrentSeat != vehicle->num_peeps)
|
|
return;
|
|
}
|
|
|
|
if (vehicle->IsUsedInPairs())
|
|
{
|
|
auto* seatedGuest = GetEntity<Guest>(vehicle->peep[CurrentSeat ^ 1]);
|
|
if (seatedGuest != nullptr)
|
|
{
|
|
if (seatedGuest->RideSubState != PeepRideSubState::EnterVehicle)
|
|
return;
|
|
|
|
vehicle->num_peeps++;
|
|
ride->cur_num_customers++;
|
|
|
|
vehicle->ApplyMass(seatedGuest->Mass);
|
|
seatedGuest->MoveTo({ LOCATION_NULL, 0, 0 });
|
|
seatedGuest->SetState(PeepState::OnRide);
|
|
seatedGuest->GuestTimeOnRide = 0;
|
|
seatedGuest->RideSubState = PeepRideSubState::OnRide;
|
|
seatedGuest->OnEnterRide(ride);
|
|
}
|
|
}
|
|
|
|
vehicle->num_peeps++;
|
|
ride->cur_num_customers++;
|
|
|
|
vehicle->ApplyMass(Mass);
|
|
vehicle->Invalidate();
|
|
|
|
MoveTo({ LOCATION_NULL, 0, 0 });
|
|
|
|
SetState(PeepState::OnRide);
|
|
|
|
GuestTimeOnRide = 0;
|
|
RideSubState = PeepRideSubState::OnRide;
|
|
OnEnterRide(ride);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00693028
|
|
*/
|
|
void Guest::UpdateRideLeaveVehicle()
|
|
{
|
|
auto ride = get_ride(CurrentRide);
|
|
if (ride == nullptr)
|
|
return;
|
|
|
|
Vehicle* vehicle = GetEntity<Vehicle>(ride->vehicles[CurrentTrain]);
|
|
if (vehicle == nullptr)
|
|
return;
|
|
|
|
uint8_t ride_station = vehicle->current_station;
|
|
vehicle = vehicle->GetCar(CurrentCar);
|
|
if (vehicle == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Check if ride is NOT Ferris Wheel.
|
|
if (ride->mode != RideMode::ForwardRotation && ride->mode != RideMode::BackwardRotation)
|
|
{
|
|
if (vehicle->num_peeps - 1 != CurrentSeat)
|
|
return;
|
|
}
|
|
|
|
ActionSpriteImageOffset++;
|
|
if (ActionSpriteImageOffset & 3)
|
|
return;
|
|
|
|
ActionSpriteImageOffset = 0;
|
|
|
|
vehicle->num_peeps--;
|
|
vehicle->ApplyMass(-Mass);
|
|
vehicle->Invalidate();
|
|
|
|
if (ride_station >= MAX_STATIONS)
|
|
{
|
|
// HACK #5658: Some parks have hacked rides which end up in this state
|
|
auto bestStationIndex = ride_get_first_valid_station_exit(ride);
|
|
if (bestStationIndex == STATION_INDEX_NULL)
|
|
{
|
|
bestStationIndex = 0;
|
|
}
|
|
ride_station = bestStationIndex;
|
|
}
|
|
CurrentRideStation = ride_station;
|
|
rct_ride_entry* rideEntry = vehicle->GetRideEntry();
|
|
if (rideEntry == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
rct_ride_entry_vehicle* vehicle_entry = &rideEntry->vehicles[vehicle->vehicle_type];
|
|
|
|
if (!(vehicle_entry->flags & VEHICLE_ENTRY_FLAG_LOADING_WAYPOINTS))
|
|
{
|
|
assert(CurrentRideStation < MAX_STATIONS);
|
|
TileCoordsXYZD exitLocation = ride_get_exit_location(ride, CurrentRideStation);
|
|
CoordsXYZD platformLocation;
|
|
platformLocation.z = ride->stations[CurrentRideStation].GetBaseZ();
|
|
|
|
platformLocation.direction = direction_reverse(exitLocation.direction);
|
|
|
|
if (!ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_VEHICLE_IS_INTEGRAL))
|
|
{
|
|
for (; vehicle != nullptr && !vehicle->IsHead(); vehicle = GetEntity<Vehicle>(vehicle->prev_vehicle_on_ride))
|
|
{
|
|
auto trackType = vehicle->GetTrackType();
|
|
if (trackType == TrackElemType::Flat || trackType > TrackElemType::MiddleStation)
|
|
continue;
|
|
|
|
bool foundStation = false;
|
|
for (auto* trackElement : TileElementsView<TrackElement>(vehicle->TrackLocation))
|
|
{
|
|
if (trackElement->GetBaseZ() != vehicle->TrackLocation.z)
|
|
continue;
|
|
|
|
if (trackElement->GetStationIndex() != CurrentRideStation)
|
|
continue;
|
|
|
|
foundStation = true;
|
|
break;
|
|
}
|
|
|
|
if (foundStation)
|
|
break;
|
|
}
|
|
|
|
if (vehicle == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
uint8_t shiftMultiplier = 12;
|
|
uint8_t specialDirection = platformLocation.direction;
|
|
|
|
rideEntry = get_ride_entry(ride->subtype);
|
|
|
|
if (rideEntry != nullptr)
|
|
{
|
|
vehicle_entry = &rideEntry->vehicles[rideEntry->default_vehicle];
|
|
|
|
if (vehicle_entry->flags & VEHICLE_ENTRY_FLAG_GO_KART)
|
|
{
|
|
shiftMultiplier = 9;
|
|
}
|
|
|
|
if (vehicle_entry->flags & (VEHICLE_ENTRY_FLAG_CHAIRLIFT | VEHICLE_ENTRY_FLAG_GO_KART))
|
|
{
|
|
specialDirection = ((vehicle->sprite_direction + 3) / 8) + 1;
|
|
specialDirection &= 3;
|
|
|
|
if (vehicle->TrackSubposition == VehicleTrackSubposition::GoKartsRightLane)
|
|
specialDirection = direction_reverse(specialDirection);
|
|
}
|
|
}
|
|
|
|
int16_t xShift = DirectionOffsets[specialDirection].x;
|
|
int16_t yShift = DirectionOffsets[specialDirection].y;
|
|
|
|
platformLocation.x = vehicle->x + xShift * shiftMultiplier;
|
|
platformLocation.y = vehicle->y + yShift * shiftMultiplier;
|
|
|
|
peep_go_to_ride_exit(
|
|
this, ride, platformLocation.x, platformLocation.y, platformLocation.z, platformLocation.direction);
|
|
return;
|
|
}
|
|
|
|
platformLocation.x = vehicle->x + DirectionOffsets[platformLocation.direction].x * 12;
|
|
platformLocation.y = vehicle->y + DirectionOffsets[platformLocation.direction].y * 12;
|
|
|
|
// This can evaluate to false with buggy custom rides.
|
|
if (CurrentSeat < vehicle_entry->peep_loading_positions.size())
|
|
{
|
|
int8_t loadPosition = vehicle_entry->peep_loading_positions[CurrentSeat];
|
|
|
|
switch (vehicle->sprite_direction / 8)
|
|
{
|
|
case 0:
|
|
platformLocation.x -= loadPosition;
|
|
break;
|
|
case 1:
|
|
platformLocation.y += loadPosition;
|
|
break;
|
|
case 2:
|
|
platformLocation.x += loadPosition;
|
|
break;
|
|
case 3:
|
|
platformLocation.y -= loadPosition;
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
log_verbose(
|
|
"CurrentSeat %d is too large! (Vehicle entry has room for %d.)", CurrentSeat,
|
|
vehicle_entry->peep_loading_positions.size());
|
|
}
|
|
|
|
platformLocation.z = ride->stations[CurrentRideStation].GetBaseZ();
|
|
|
|
peep_go_to_ride_exit(
|
|
this, ride, platformLocation.x, platformLocation.y, platformLocation.z, platformLocation.direction);
|
|
return;
|
|
}
|
|
|
|
auto exitLocation = ride_get_exit_location(ride, CurrentRideStation).ToCoordsXYZD();
|
|
Guard::Assert(!exitLocation.IsNull());
|
|
|
|
auto waypointLoc = CoordsXYZ{ ride->stations[CurrentRideStation].Start.ToTileCentre(),
|
|
exitLocation.z + ride->GetRideTypeDescriptor().Heights.PlatformHeight };
|
|
|
|
TileElement* trackElement = ride_get_station_start_track_element(ride, CurrentRideStation);
|
|
|
|
Direction station_direction = (trackElement == nullptr ? 0 : trackElement->GetDirection());
|
|
|
|
vehicle = GetEntity<Vehicle>(ride->vehicles[CurrentTrain]);
|
|
if (vehicle == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
rideEntry = vehicle->GetRideEntry();
|
|
rct_ride_entry_vehicle* vehicleEntry = &rideEntry->vehicles[vehicle->vehicle_type];
|
|
if (vehicleEntry == nullptr)
|
|
return;
|
|
|
|
Var37 = ((exitLocation.direction | GetWaypointedSeatLocation(*ride, vehicleEntry, station_direction) * 4) * 4) | 1;
|
|
|
|
if (ride->type == RIDE_TYPE_ENTERPRISE)
|
|
{
|
|
waypointLoc.x = vehicle->x;
|
|
waypointLoc.y = vehicle->y;
|
|
}
|
|
|
|
Guard::Assert(vehicleEntry->peep_loading_waypoints.size() >= static_cast<size_t>(Var37 / 4));
|
|
CoordsXYZ exitWaypointLoc = waypointLoc;
|
|
|
|
exitWaypointLoc.x += vehicleEntry->peep_loading_waypoints[Var37 / 4][2].x;
|
|
exitWaypointLoc.y += vehicleEntry->peep_loading_waypoints[Var37 / 4][2].y;
|
|
|
|
if (ride->type == RIDE_TYPE_MOTION_SIMULATOR)
|
|
exitWaypointLoc.z += 15;
|
|
|
|
MoveTo(exitWaypointLoc);
|
|
|
|
waypointLoc.x += vehicleEntry->peep_loading_waypoints[Var37 / 4][1].x;
|
|
waypointLoc.y += vehicleEntry->peep_loading_waypoints[Var37 / 4][1].y;
|
|
|
|
SetDestination(waypointLoc, 2);
|
|
RideSubState = PeepRideSubState::ApproachExitWaypoints;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069376A
|
|
*/
|
|
void Guest::UpdateRidePrepareForExit()
|
|
{
|
|
auto ride = get_ride(CurrentRide);
|
|
if (ride == nullptr || CurrentRideStation >= std::size(ride->stations))
|
|
return;
|
|
|
|
auto exit = ride_get_exit_location(ride, CurrentRideStation);
|
|
|
|
auto newDestination = exit.ToCoordsXY().ToTileCentre();
|
|
|
|
auto xShift = DirectionOffsets[exit.direction].x;
|
|
auto yShift = DirectionOffsets[exit.direction].y;
|
|
|
|
int16_t shiftMultiplier = 20;
|
|
|
|
rct_ride_entry* rideEntry = ride->GetRideEntry();
|
|
if (rideEntry != nullptr)
|
|
{
|
|
rct_ride_entry_vehicle* vehicleEntry = &rideEntry->vehicles[rideEntry->default_vehicle];
|
|
if (vehicleEntry->flags & (VEHICLE_ENTRY_FLAG_CHAIRLIFT | VEHICLE_ENTRY_FLAG_GO_KART))
|
|
{
|
|
shiftMultiplier = 32;
|
|
}
|
|
}
|
|
|
|
xShift *= shiftMultiplier;
|
|
yShift *= shiftMultiplier;
|
|
|
|
newDestination.x -= xShift;
|
|
newDestination.y -= yShift;
|
|
|
|
SetDestination(newDestination, 2);
|
|
RideSubState = PeepRideSubState::InExit;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069374F
|
|
*/
|
|
void Guest::UpdateRideApproachExit()
|
|
{
|
|
if (auto loc = UpdateAction(); loc.has_value())
|
|
{
|
|
MoveTo({ loc.value(), z });
|
|
return;
|
|
}
|
|
|
|
UpdateRidePrepareForExit();
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069382E
|
|
*/
|
|
void Guest::UpdateRideInExit()
|
|
{
|
|
auto ride = get_ride(CurrentRide);
|
|
if (ride == nullptr)
|
|
return;
|
|
|
|
int16_t xy_distance;
|
|
|
|
if (auto loc = UpdateAction(xy_distance); loc.has_value())
|
|
{
|
|
if (xy_distance >= 16)
|
|
{
|
|
int16_t actionZ = ride->stations[CurrentRideStation].GetBaseZ();
|
|
|
|
actionZ += ride->GetRideTypeDescriptor().Heights.PlatformHeight;
|
|
MoveTo({ loc.value(), actionZ });
|
|
return;
|
|
}
|
|
|
|
SwitchToSpecialSprite(0);
|
|
MoveTo({ loc.value(), z });
|
|
}
|
|
|
|
if (ride->lifecycle_flags & RIDE_LIFECYCLE_ON_RIDE_PHOTO)
|
|
{
|
|
ShopItem secondaryItem = ride->GetRideTypeDescriptor().PhotoItem;
|
|
if (DecideAndBuyItem(ride, secondaryItem, ride->price[1]))
|
|
{
|
|
ride->no_secondary_items_sold++;
|
|
}
|
|
}
|
|
RideSubState = PeepRideSubState::LeaveExit;
|
|
}
|
|
#pragma warning(default : 6011)
|
|
/**
|
|
*
|
|
* rct2: 0x006926AD
|
|
*/
|
|
void Guest::UpdateRideApproachVehicleWaypoints()
|
|
{
|
|
auto ride = get_ride(CurrentRide);
|
|
if (ride == nullptr)
|
|
return;
|
|
|
|
int16_t xy_distance;
|
|
uint8_t waypoint = Var37 & 3;
|
|
|
|
if (auto loc = UpdateAction(xy_distance); loc.has_value())
|
|
{
|
|
int16_t actionZ;
|
|
// Motion simulators have steps this moves the peeps up the steps
|
|
if (ride->type == RIDE_TYPE_MOTION_SIMULATOR)
|
|
{
|
|
actionZ = ride->stations[CurrentRideStation].GetBaseZ() + 2;
|
|
|
|
if (waypoint == 2)
|
|
{
|
|
xy_distance -= 12;
|
|
if (xy_distance < 0)
|
|
xy_distance = 0;
|
|
|
|
if (xy_distance <= 15)
|
|
{
|
|
actionZ += 15 - xy_distance;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
actionZ = z;
|
|
}
|
|
MoveTo({ loc.value(), actionZ });
|
|
return;
|
|
}
|
|
|
|
if (waypoint == 2)
|
|
{
|
|
RideSubState = PeepRideSubState::EnterVehicle;
|
|
return;
|
|
}
|
|
|
|
waypoint++;
|
|
// This is incrementing the actual peep waypoint
|
|
Var37++;
|
|
|
|
Vehicle* vehicle = GetEntity<Vehicle>(ride->vehicles[CurrentTrain]);
|
|
if (vehicle == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
CoordsXY targetLoc = ride->stations[CurrentRideStation].Start.ToTileCentre();
|
|
|
|
if (ride->type == RIDE_TYPE_ENTERPRISE)
|
|
{
|
|
targetLoc.x = vehicle->x;
|
|
targetLoc.y = vehicle->y;
|
|
}
|
|
|
|
rct_ride_entry* ride_entry = vehicle->GetRideEntry();
|
|
if (ride_entry == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
rct_ride_entry_vehicle* vehicle_type = &ride_entry->vehicles[vehicle->vehicle_type];
|
|
Guard::Assert(waypoint < 3);
|
|
targetLoc.x += vehicle_type->peep_loading_waypoints[Var37 / 4][waypoint].x;
|
|
targetLoc.y += vehicle_type->peep_loading_waypoints[Var37 / 4][waypoint].y;
|
|
|
|
SetDestination(targetLoc);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069357D
|
|
*/
|
|
void Guest::UpdateRideApproachExitWaypoints()
|
|
{
|
|
auto ride = get_ride(CurrentRide);
|
|
if (ride == nullptr)
|
|
return;
|
|
|
|
int16_t xy_distance;
|
|
|
|
if (auto loc = UpdateAction(xy_distance); loc.has_value())
|
|
{
|
|
int16_t actionZ;
|
|
if (ride->type == RIDE_TYPE_MOTION_SIMULATOR)
|
|
{
|
|
actionZ = ride->stations[CurrentRideStation].GetBaseZ() + 2;
|
|
|
|
if ((Var37 & 3) == 1)
|
|
{
|
|
if (xy_distance > 15)
|
|
xy_distance = 15;
|
|
|
|
actionZ += xy_distance;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
actionZ = z;
|
|
}
|
|
MoveTo({ loc.value(), actionZ });
|
|
return;
|
|
}
|
|
|
|
if ((Var37 & 3) != 0)
|
|
{
|
|
if ((Var37 & 3) == 3)
|
|
{
|
|
UpdateRidePrepareForExit();
|
|
return;
|
|
}
|
|
|
|
Var37--;
|
|
Vehicle* vehicle = GetEntity<Vehicle>(ride->vehicles[CurrentTrain]);
|
|
if (vehicle == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
CoordsXY targetLoc = ride->stations[CurrentRideStation].Start.ToTileCentre();
|
|
|
|
if (ride->type == RIDE_TYPE_ENTERPRISE)
|
|
{
|
|
targetLoc.x = vehicle->x;
|
|
targetLoc.y = vehicle->y;
|
|
}
|
|
|
|
rct_ride_entry* rideEntry = vehicle->GetRideEntry();
|
|
rct_ride_entry_vehicle* vehicleEntry = &rideEntry->vehicles[vehicle->vehicle_type];
|
|
|
|
Guard::Assert((Var37 & 3) < 3);
|
|
targetLoc.x += vehicleEntry->peep_loading_waypoints[Var37 / 4][Var37 & 3].x;
|
|
targetLoc.y += vehicleEntry->peep_loading_waypoints[Var37 / 4][Var37 & 3].y;
|
|
|
|
SetDestination(targetLoc);
|
|
return;
|
|
}
|
|
|
|
Var37 |= 3;
|
|
|
|
auto targetLoc = ride_get_exit_location(ride, CurrentRideStation).ToCoordsXYZD().ToTileCentre();
|
|
uint8_t exit_direction = direction_reverse(targetLoc.direction);
|
|
|
|
int16_t x_shift = DirectionOffsets[exit_direction].x;
|
|
int16_t y_shift = DirectionOffsets[exit_direction].y;
|
|
|
|
int16_t shift_multiplier = 20;
|
|
|
|
auto rideEntry = get_ride_entry(ride->subtype);
|
|
if (rideEntry != nullptr)
|
|
{
|
|
auto vehicleEntry = &rideEntry->vehicles[rideEntry->default_vehicle];
|
|
if (vehicleEntry->flags & (VEHICLE_ENTRY_FLAG_CHAIRLIFT | VEHICLE_ENTRY_FLAG_GO_KART))
|
|
{
|
|
shift_multiplier = 32;
|
|
}
|
|
}
|
|
|
|
x_shift *= shift_multiplier;
|
|
y_shift *= shift_multiplier;
|
|
|
|
targetLoc.x -= x_shift;
|
|
targetLoc.y -= y_shift;
|
|
|
|
SetDestination(targetLoc);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006927B3
|
|
*/
|
|
void Guest::UpdateRideApproachSpiralSlide()
|
|
{
|
|
auto ride = get_ride(CurrentRide);
|
|
if (ride == nullptr)
|
|
return;
|
|
|
|
if (auto loc = UpdateAction(); loc.has_value())
|
|
{
|
|
MoveTo({ loc.value(), z });
|
|
return;
|
|
}
|
|
|
|
uint8_t waypoint = Var37 & 3;
|
|
|
|
if (waypoint == 3)
|
|
{
|
|
SubState = 15;
|
|
SetDestination({ 0, 0 });
|
|
Var37 = (Var37 / 4) & 0xC;
|
|
MoveTo({ LOCATION_NULL, y, z });
|
|
return;
|
|
}
|
|
|
|
if (waypoint == 2)
|
|
{
|
|
bool lastRide = false;
|
|
if (ride->status != RideStatus::Open)
|
|
lastRide = true;
|
|
else if (CurrentCar++ != 0)
|
|
{
|
|
if (ride->mode == RideMode::SingleRidePerAdmission)
|
|
lastRide = true;
|
|
if (static_cast<uint8_t>(CurrentCar - 1) > (scenario_rand() & 0xF))
|
|
lastRide = true;
|
|
}
|
|
|
|
if (lastRide)
|
|
{
|
|
auto exit = ride_get_exit_location(ride, CurrentRideStation);
|
|
waypoint = 1;
|
|
Var37 = (exit.direction * 4) | (Var37 & 0x30) | waypoint;
|
|
CoordsXY targetLoc = ride->stations[CurrentRideStation].Start;
|
|
|
|
assert(ride->type == RIDE_TYPE_SPIRAL_SLIDE);
|
|
targetLoc += SpiralSlideWalkingPath[Var37];
|
|
|
|
SetDestination(targetLoc);
|
|
RideSubState = PeepRideSubState::LeaveSpiralSlide;
|
|
return;
|
|
}
|
|
}
|
|
waypoint++;
|
|
// Actually increment the real peep waypoint
|
|
Var37++;
|
|
|
|
CoordsXY targetLoc = ride->stations[CurrentRideStation].Start;
|
|
|
|
assert(ride->type == RIDE_TYPE_SPIRAL_SLIDE);
|
|
targetLoc += SpiralSlideWalkingPath[Var37];
|
|
|
|
SetDestination(targetLoc);
|
|
}
|
|
|
|
/** rct2: 0x00981F0C, 0x00981F0E */
|
|
static constexpr const CoordsXY _SpiralSlideEnd[] = {
|
|
{ 25, 56 },
|
|
{ 56, 7 },
|
|
{ 7, -24 },
|
|
{ -24, 25 },
|
|
};
|
|
|
|
/** rct2: 0x00981F1C, 0x00981F1E */
|
|
static constexpr const CoordsXY _SpiralSlideEndWaypoint[] = {
|
|
{ 8, 56 },
|
|
{ 56, 24 },
|
|
{ 24, -24 },
|
|
{ -24, 8 },
|
|
};
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00692D83
|
|
*/
|
|
void Guest::UpdateRideOnSpiralSlide()
|
|
{
|
|
auto ride = get_ride(CurrentRide);
|
|
if (ride == nullptr || ride->type != RIDE_TYPE_SPIRAL_SLIDE)
|
|
return;
|
|
|
|
auto destination = GetDestination();
|
|
if ((Var37 & 3) == 0)
|
|
{
|
|
switch (destination.x)
|
|
{
|
|
case 0:
|
|
destination.y++;
|
|
if (destination.y >= 30)
|
|
destination.x++;
|
|
|
|
SetDestination(destination);
|
|
return;
|
|
case 1:
|
|
if (ride->slide_in_use != 0)
|
|
return;
|
|
|
|
ride->slide_in_use++;
|
|
ride->slide_peep = sprite_index;
|
|
ride->slide_peep_t_shirt_colour = TshirtColour;
|
|
ride->spiral_slide_progress = 0;
|
|
destination.x++;
|
|
|
|
SetDestination(destination);
|
|
return;
|
|
case 2:
|
|
return;
|
|
case 3:
|
|
{
|
|
auto newLocation = ride->stations[CurrentRideStation].Start;
|
|
uint8_t dir = (Var37 / 4) & 3;
|
|
|
|
// Set the location that the peep walks to go on slide again
|
|
destination = newLocation + _SpiralSlideEndWaypoint[dir];
|
|
SetDestination(destination);
|
|
|
|
// Move the peep sprite to just at the end of the slide
|
|
newLocation.x += _SpiralSlideEnd[dir].x;
|
|
newLocation.y += _SpiralSlideEnd[dir].y;
|
|
|
|
MoveTo({ newLocation, z });
|
|
|
|
sprite_direction = (Var37 & 0xC) * 2;
|
|
|
|
Var37++;
|
|
return;
|
|
}
|
|
default:
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (auto loc = UpdateAction(); loc.has_value())
|
|
{
|
|
MoveTo({ loc.value(), z });
|
|
return;
|
|
}
|
|
|
|
uint8_t waypoint = 2;
|
|
Var37 = (Var37 * 4 & 0x30) + waypoint;
|
|
|
|
CoordsXY targetLoc = ride->stations[CurrentRideStation].Start;
|
|
|
|
assert(ride->type == RIDE_TYPE_SPIRAL_SLIDE);
|
|
targetLoc += SpiralSlideWalkingPath[Var37];
|
|
|
|
SetDestination(targetLoc);
|
|
RideSubState = PeepRideSubState::ApproachSpiralSlide;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00692C6B
|
|
*/
|
|
void Guest::UpdateRideLeaveSpiralSlide()
|
|
{
|
|
// Iterates through the spiral slide waypoints until it reaches
|
|
// waypoint 0. Then it readies to leave the ride by the entrance.
|
|
if (auto loc = UpdateAction(); loc.has_value())
|
|
{
|
|
MoveTo({ loc.value(), z });
|
|
return;
|
|
}
|
|
|
|
auto ride = get_ride(CurrentRide);
|
|
if (ride == nullptr)
|
|
return;
|
|
|
|
uint8_t waypoint = Var37 & 3;
|
|
|
|
if (waypoint != 0)
|
|
{
|
|
if (waypoint == 3)
|
|
{
|
|
UpdateRidePrepareForExit();
|
|
return;
|
|
}
|
|
|
|
waypoint--;
|
|
// Actually decrement the peep waypoint
|
|
Var37--;
|
|
CoordsXY targetLoc = ride->stations[CurrentRideStation].Start;
|
|
|
|
assert(ride->type == RIDE_TYPE_SPIRAL_SLIDE);
|
|
targetLoc += SpiralSlideWalkingPath[Var37];
|
|
|
|
SetDestination(targetLoc);
|
|
return;
|
|
}
|
|
|
|
// Actually force the final waypoint
|
|
Var37 |= 3;
|
|
|
|
auto targetLoc = ride_get_exit_location(ride, CurrentRideStation).ToCoordsXYZD().ToTileCentre();
|
|
|
|
int16_t xShift = DirectionOffsets[direction_reverse(targetLoc.direction)].x;
|
|
int16_t yShift = DirectionOffsets[direction_reverse(targetLoc.direction)].y;
|
|
|
|
int16_t shiftMultiplier = 20;
|
|
|
|
xShift *= shiftMultiplier;
|
|
yShift *= shiftMultiplier;
|
|
|
|
targetLoc.x -= xShift;
|
|
targetLoc.y -= yShift;
|
|
|
|
SetDestination(targetLoc);
|
|
}
|
|
|
|
/** rct2: 0x00981FE4 */
|
|
static constexpr const uint8_t _MazeGetNewDirectionFromEdge[][4] = {
|
|
{ 15, 7, 15, 7 },
|
|
{ 11, 3, 11, 3 },
|
|
{ 7, 15, 7, 15 },
|
|
{ 3, 11, 3, 11 },
|
|
};
|
|
|
|
/** rct2: 0x00981FF4 */
|
|
static constexpr const uint8_t _MazeCurrentDirectionToOpenHedge[][4] = {
|
|
{ 1, 2, 14, 0 },
|
|
{ 4, 5, 6, 2 },
|
|
{ 6, 8, 9, 10 },
|
|
{ 14, 10, 12, 13 },
|
|
};
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00692A83
|
|
*/
|
|
void Guest::UpdateRideMazePathfinding()
|
|
{
|
|
if (auto loc = UpdateAction(); loc.has_value())
|
|
{
|
|
MoveTo({ loc.value(), z });
|
|
return;
|
|
}
|
|
|
|
auto ride = get_ride(CurrentRide);
|
|
if (ride == nullptr)
|
|
return;
|
|
|
|
if (Var37 == 16)
|
|
{
|
|
UpdateRidePrepareForExit();
|
|
return;
|
|
}
|
|
|
|
if (IsActionInterruptable())
|
|
{
|
|
if (Energy > 64 && (scenario_rand() & 0xFFFF) <= 2427)
|
|
{
|
|
Action = PeepActionType::Jump;
|
|
ActionFrame = 0;
|
|
ActionSpriteImageOffset = 0;
|
|
UpdateCurrentActionSpriteType();
|
|
}
|
|
}
|
|
|
|
auto targetLoc = GetDestination().ToTileStart();
|
|
|
|
int16_t stationBaseZ = ride->stations[0].GetBaseZ();
|
|
|
|
// Find the station track element
|
|
auto trackElement = map_get_track_element_at({ targetLoc, stationBaseZ });
|
|
if (trackElement == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
uint16_t mazeEntry = trackElement->GetMazeEntry();
|
|
// Var37 is 3, 7, 11 or 15
|
|
uint8_t hedges[4]{ 0xFF, 0xFF, 0xFF, 0xFF };
|
|
uint8_t openCount = 0;
|
|
uint8_t mazeReverseLastEdge = direction_reverse(MazeLastEdge);
|
|
for (uint8_t i = 0; i < 4; ++i)
|
|
{
|
|
if (!(mazeEntry & (1 << _MazeCurrentDirectionToOpenHedge[Var37 / 4][i])) && i != mazeReverseLastEdge)
|
|
{
|
|
hedges[openCount++] = i;
|
|
}
|
|
}
|
|
|
|
if (openCount == 0)
|
|
{
|
|
if ((mazeEntry & (1 << _MazeCurrentDirectionToOpenHedge[Var37 / 4][mazeReverseLastEdge])))
|
|
{
|
|
return;
|
|
}
|
|
hedges[openCount++] = mazeReverseLastEdge;
|
|
}
|
|
|
|
uint8_t chosenEdge = hedges[scenario_rand() % openCount];
|
|
assert(chosenEdge != 0xFF);
|
|
|
|
targetLoc = GetDestination() + CoordsDirectionDelta[chosenEdge] / 2;
|
|
|
|
enum class maze_type
|
|
{
|
|
invalid,
|
|
hedge,
|
|
entrance_or_exit
|
|
};
|
|
maze_type mazeType = maze_type::invalid;
|
|
|
|
auto tileElement = map_get_first_element_at(targetLoc);
|
|
if (tileElement == nullptr)
|
|
return;
|
|
do
|
|
{
|
|
if (stationBaseZ != tileElement->GetBaseZ())
|
|
continue;
|
|
|
|
if (tileElement->GetType() == TILE_ELEMENT_TYPE_TRACK)
|
|
{
|
|
mazeType = maze_type::hedge;
|
|
break;
|
|
}
|
|
|
|
if (tileElement->GetType() == TILE_ELEMENT_TYPE_ENTRANCE
|
|
&& tileElement->AsEntrance()->GetEntranceType() == ENTRANCE_TYPE_RIDE_EXIT)
|
|
{
|
|
mazeType = maze_type::entrance_or_exit;
|
|
break;
|
|
}
|
|
} while (!(tileElement++)->IsLastForTile());
|
|
|
|
switch (mazeType)
|
|
{
|
|
case maze_type::invalid:
|
|
MazeLastEdge++;
|
|
MazeLastEdge &= 3;
|
|
return;
|
|
case maze_type::hedge:
|
|
SetDestination(targetLoc);
|
|
Var37 = _MazeGetNewDirectionFromEdge[Var37 / 4][chosenEdge];
|
|
MazeLastEdge = chosenEdge;
|
|
break;
|
|
case maze_type::entrance_or_exit:
|
|
targetLoc = GetDestination();
|
|
if (chosenEdge & 1)
|
|
{
|
|
targetLoc.x = targetLoc.ToTileCentre().x;
|
|
}
|
|
else
|
|
{
|
|
targetLoc.y = targetLoc.ToTileCentre().y;
|
|
}
|
|
SetDestination(targetLoc);
|
|
Var37 = 16;
|
|
MazeLastEdge = chosenEdge;
|
|
break;
|
|
}
|
|
|
|
if (auto loc = UpdateAction(); loc.has_value())
|
|
{
|
|
MoveTo({ loc.value(), z });
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006938D2
|
|
*/
|
|
void Guest::UpdateRideLeaveExit()
|
|
{
|
|
auto ride = get_ride(CurrentRide);
|
|
|
|
if (auto loc = UpdateAction(); loc.has_value())
|
|
{
|
|
if (ride != nullptr)
|
|
{
|
|
MoveTo({ loc.value(), ride->stations[CurrentRideStation].GetBaseZ() });
|
|
}
|
|
return;
|
|
}
|
|
|
|
OnExitRide(ride);
|
|
|
|
if (ride != nullptr && (PeepFlags & PEEP_FLAGS_TRACKING))
|
|
{
|
|
auto ft = Formatter();
|
|
FormatNameTo(ft);
|
|
ride->FormatNameTo(ft);
|
|
|
|
if (gConfigNotifications.guest_left_ride)
|
|
{
|
|
News::AddItemToQueue(News::ItemType::PeepOnRide, STR_PEEP_TRACKING_LEFT_RIDE_X, sprite_index, ft);
|
|
}
|
|
}
|
|
|
|
InteractionRideIndex = RIDE_ID_NULL;
|
|
SetState(PeepState::Falling);
|
|
|
|
CoordsXY targetLoc = { x, y };
|
|
|
|
// Find the station track element
|
|
for (auto* pathElement : TileElementsView<PathElement>(targetLoc))
|
|
{
|
|
int16_t height = map_height_from_slope(targetLoc, pathElement->GetSlopeDirection(), pathElement->IsSloped());
|
|
height += pathElement->GetBaseZ();
|
|
|
|
int16_t z_diff = z - height;
|
|
if (z_diff > 0 || z_diff < -16)
|
|
continue;
|
|
|
|
MoveTo({ x, y, height });
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069299C
|
|
*/
|
|
void Guest::UpdateRideShopApproach()
|
|
{
|
|
if (auto loc = UpdateAction(); loc.has_value())
|
|
{
|
|
MoveTo({ loc.value(), z });
|
|
return;
|
|
}
|
|
|
|
RideSubState = PeepRideSubState::InteractShop;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006929BB
|
|
*/
|
|
void Guest::UpdateRideShopInteract()
|
|
{
|
|
auto ride = get_ride(CurrentRide);
|
|
if (ride == nullptr)
|
|
return;
|
|
|
|
const int16_t tileCentreX = NextLoc.x + 16;
|
|
const int16_t tileCentreY = NextLoc.y + 16;
|
|
if (ride->type == RIDE_TYPE_FIRST_AID)
|
|
{
|
|
if (Nausea <= 35)
|
|
{
|
|
RideSubState = PeepRideSubState::LeaveShop;
|
|
|
|
SetDestination({ tileCentreX, tileCentreY }, 3);
|
|
HappinessTarget = std::min(HappinessTarget + 30, PEEP_MAX_HAPPINESS);
|
|
Happiness = HappinessTarget;
|
|
}
|
|
else
|
|
{
|
|
Nausea--;
|
|
NauseaTarget = Nausea;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (Toilet != 0)
|
|
{
|
|
Toilet--;
|
|
return;
|
|
}
|
|
|
|
// Do not play toilet flush sound on title screen as it's considered loud and annoying
|
|
if (!(gScreenFlags & SCREEN_FLAGS_TITLE_DEMO))
|
|
{
|
|
OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::ToiletFlush, GetLocation());
|
|
}
|
|
|
|
RideSubState = PeepRideSubState::LeaveShop;
|
|
|
|
SetDestination({ tileCentreX, tileCentreY }, 3);
|
|
|
|
HappinessTarget = std::min(HappinessTarget + 30, PEEP_MAX_HAPPINESS);
|
|
Happiness = HappinessTarget;
|
|
StopPurchaseThought(ride->type);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00692935
|
|
*/
|
|
void Guest::UpdateRideShopLeave()
|
|
{
|
|
if (auto loc = UpdateAction(); loc.has_value())
|
|
{
|
|
const auto curLoc = GetLocation();
|
|
MoveTo({ loc.value(), curLoc.z });
|
|
|
|
const auto newLoc = GetLocation().ToTileStart();
|
|
if (newLoc.x != NextLoc.x)
|
|
return;
|
|
if (newLoc.y != NextLoc.y)
|
|
return;
|
|
}
|
|
|
|
//#11758 Previously SetState(PeepState::Walking) caused Peeps to double-back to exit point of shop
|
|
SetState(PeepState::Falling);
|
|
|
|
auto ride = get_ride(CurrentRide);
|
|
if (ride != nullptr)
|
|
{
|
|
ride->total_customers++;
|
|
ride->window_invalidate_flags |= RIDE_INVALIDATE_RIDE_CUSTOMER;
|
|
ride_update_satisfaction(ride, Happiness / 64);
|
|
}
|
|
}
|
|
|
|
void Guest::UpdateGuest()
|
|
{
|
|
switch (State)
|
|
{
|
|
case PeepState::QueuingFront:
|
|
UpdateRide();
|
|
break;
|
|
case PeepState::LeavingRide:
|
|
UpdateRide();
|
|
break;
|
|
case PeepState::Walking:
|
|
UpdateWalking();
|
|
break;
|
|
case PeepState::Queuing:
|
|
UpdateQueuing();
|
|
break;
|
|
case PeepState::EnteringRide:
|
|
UpdateRide();
|
|
break;
|
|
case PeepState::Sitting:
|
|
UpdateSitting();
|
|
break;
|
|
case PeepState::EnteringPark:
|
|
UpdateEnteringPark();
|
|
break;
|
|
case PeepState::LeavingPark:
|
|
UpdateLeavingPark();
|
|
break;
|
|
case PeepState::Buying:
|
|
UpdateBuying();
|
|
break;
|
|
case PeepState::Watching:
|
|
UpdateWatching();
|
|
break;
|
|
case PeepState::UsingBin:
|
|
UpdateUsingBin();
|
|
break;
|
|
default:
|
|
// TODO reset to default state
|
|
assert(false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x691A30
|
|
* Used by entering_ride and queueing_front */
|
|
void Guest::UpdateRide()
|
|
{
|
|
NextFlags &= ~PEEP_NEXT_FLAG_IS_SLOPED;
|
|
|
|
switch (RideSubState)
|
|
{
|
|
case PeepRideSubState::AtEntrance:
|
|
UpdateRideAtEntrance();
|
|
break;
|
|
case PeepRideSubState::InEntrance:
|
|
UpdateRideAdvanceThroughEntrance();
|
|
break;
|
|
case PeepRideSubState::FreeVehicleCheck:
|
|
UpdateRideFreeVehicleCheck();
|
|
break;
|
|
case PeepRideSubState::LeaveEntrance:
|
|
UpdateRideAdvanceThroughEntrance();
|
|
break;
|
|
case PeepRideSubState::ApproachVehicle:
|
|
UpdateRideApproachVehicle();
|
|
break;
|
|
case PeepRideSubState::EnterVehicle:
|
|
UpdateRideEnterVehicle();
|
|
break;
|
|
case PeepRideSubState::OnRide:
|
|
// No action, on ride.
|
|
break;
|
|
case PeepRideSubState::LeaveVehicle:
|
|
UpdateRideLeaveVehicle();
|
|
break;
|
|
case PeepRideSubState::ApproachExit:
|
|
UpdateRideApproachExit();
|
|
break;
|
|
case PeepRideSubState::InExit:
|
|
UpdateRideInExit();
|
|
break;
|
|
case PeepRideSubState::ApproachVehicleWaypoints:
|
|
UpdateRideApproachVehicleWaypoints();
|
|
break;
|
|
case PeepRideSubState::ApproachExitWaypoints:
|
|
UpdateRideApproachExitWaypoints();
|
|
break;
|
|
case PeepRideSubState::ApproachSpiralSlide:
|
|
UpdateRideApproachSpiralSlide();
|
|
break;
|
|
case PeepRideSubState::OnSpiralSlide:
|
|
UpdateRideOnSpiralSlide();
|
|
break;
|
|
case PeepRideSubState::LeaveSpiralSlide:
|
|
UpdateRideLeaveSpiralSlide();
|
|
break;
|
|
case PeepRideSubState::MazePathfinding:
|
|
UpdateRideMazePathfinding();
|
|
break;
|
|
case PeepRideSubState::LeaveExit:
|
|
UpdateRideLeaveExit();
|
|
break;
|
|
case PeepRideSubState::ApproachShop:
|
|
UpdateRideShopApproach();
|
|
break;
|
|
case PeepRideSubState::InteractShop:
|
|
UpdateRideShopInteract();
|
|
break;
|
|
case PeepRideSubState::LeaveShop:
|
|
UpdateRideShopLeave();
|
|
break;
|
|
default:
|
|
// Invalid peep sub-state
|
|
assert(false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void peep_update_walking_break_scenery(Guest* peep);
|
|
static bool peep_find_ride_to_look_at(Peep* peep, uint8_t edge, ride_id_t* rideToView, uint8_t* rideSeatToView);
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069030A
|
|
*/
|
|
void Guest::UpdateWalking()
|
|
{
|
|
if (!CheckForPath())
|
|
return;
|
|
|
|
if (PeepFlags & PEEP_FLAGS_WAVING)
|
|
{
|
|
if (IsActionInterruptable())
|
|
{
|
|
if ((0xFFFF & scenario_rand()) < 936)
|
|
{
|
|
Action = PeepActionType::Wave2;
|
|
ActionFrame = 0;
|
|
ActionSpriteImageOffset = 0;
|
|
|
|
UpdateCurrentActionSpriteType();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (PeepFlags & PEEP_FLAGS_PHOTO)
|
|
{
|
|
if (IsActionInterruptable())
|
|
{
|
|
if ((0xFFFF & scenario_rand()) < 936)
|
|
{
|
|
Action = PeepActionType::TakePhoto;
|
|
ActionFrame = 0;
|
|
ActionSpriteImageOffset = 0;
|
|
|
|
UpdateCurrentActionSpriteType();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (PeepFlags & PEEP_FLAGS_PAINTING)
|
|
{
|
|
if (IsActionInterruptable())
|
|
{
|
|
if ((0xFFFF & scenario_rand()) < 936)
|
|
{
|
|
Action = PeepActionType::DrawPicture;
|
|
ActionFrame = 0;
|
|
ActionSpriteImageOffset = 0;
|
|
|
|
UpdateCurrentActionSpriteType();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (PeepFlags & PEEP_FLAGS_LITTER)
|
|
{
|
|
if (!GetNextIsSurface())
|
|
{
|
|
if ((0xFFFF & scenario_rand()) <= 4096)
|
|
{
|
|
static constexpr const Litter::Type litter_types[] = {
|
|
Litter::Type::EmptyCan,
|
|
Litter::Type::Rubbish,
|
|
Litter::Type::BurgerBox,
|
|
Litter::Type::EmptyCup,
|
|
};
|
|
auto litterType = litter_types[scenario_rand() & 0x3];
|
|
const auto loc = GetLocation();
|
|
int32_t litterX = loc.x + (scenario_rand() & 0x7) - 3;
|
|
int32_t litterY = loc.y + (scenario_rand() & 0x7) - 3;
|
|
Direction litterDirection = (scenario_rand() & 0x3);
|
|
|
|
Litter::Create({ litterX, litterY, loc.z, litterDirection }, litterType);
|
|
}
|
|
}
|
|
}
|
|
else if (HasEmptyContainer())
|
|
{
|
|
if ((!GetNextIsSurface()) && (static_cast<uint32_t>(sprite_index & 0x1FF) == (gCurrentTicks & 0x1FF))
|
|
&& ((0xFFFF & scenario_rand()) <= 4096))
|
|
{
|
|
int32_t container = bitscanforward(GetEmptyContainerFlags());
|
|
auto litterType = Litter::Type::Vomit;
|
|
|
|
if (container != -1)
|
|
{
|
|
auto item = static_cast<ShopItem>(container);
|
|
RemoveItem(item);
|
|
litterType = Litter::Type(GetShopItemDescriptor(item).Type);
|
|
}
|
|
|
|
WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_INVENTORY;
|
|
UpdateSpriteType();
|
|
|
|
const auto loc = GetLocation();
|
|
int32_t litterX = loc.x + (scenario_rand() & 0x7) - 3;
|
|
int32_t litterY = loc.y + (scenario_rand() & 0x7) - 3;
|
|
Direction litterDirection = (scenario_rand() & 0x3);
|
|
|
|
Litter::Create({ litterX, litterY, loc.z, litterDirection }, litterType);
|
|
}
|
|
}
|
|
|
|
// Check if vehicle is blocking the destination tile
|
|
auto curPos = TileCoordsXYZ(GetLocation());
|
|
auto dstPos = TileCoordsXYZ(CoordsXYZ{ GetDestination(), NextLoc.z });
|
|
if (curPos.x != dstPos.x || curPos.y != dstPos.y)
|
|
{
|
|
if (footpath_is_blocked_by_vehicle(dstPos))
|
|
{
|
|
// Wait for vehicle to pass
|
|
return;
|
|
}
|
|
}
|
|
|
|
uint8_t pathingResult;
|
|
PerformNextAction(pathingResult);
|
|
if (!(pathingResult & PATHING_DESTINATION_REACHED))
|
|
return;
|
|
|
|
if (GetNextIsSurface())
|
|
{
|
|
auto surfaceElement = map_get_surface_element_at(NextLoc);
|
|
|
|
if (surfaceElement != nullptr)
|
|
{
|
|
int32_t water_height = surfaceElement->GetWaterHeight();
|
|
if (water_height > 0)
|
|
{
|
|
MoveTo({ x, y, water_height });
|
|
SetState(PeepState::Falling);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
CheckIfLost();
|
|
CheckCantFindRide();
|
|
CheckCantFindExit();
|
|
|
|
if (UpdateWalkingFindBench())
|
|
return;
|
|
|
|
if (UpdateWalkingFindBin())
|
|
return;
|
|
|
|
peep_update_walking_break_scenery(this);
|
|
|
|
if (State != PeepState::Walking)
|
|
return;
|
|
|
|
if (PeepFlags & PEEP_FLAGS_LEAVING_PARK)
|
|
return;
|
|
|
|
if (Nausea > 140)
|
|
return;
|
|
|
|
if (Happiness < 120)
|
|
return;
|
|
|
|
if (Toilet > 140)
|
|
return;
|
|
|
|
uint16_t chance = HasFoodOrDrink() ? 13107 : 2849;
|
|
|
|
if ((scenario_rand() & 0xFFFF) > chance)
|
|
return;
|
|
|
|
if (GetNextIsSurface() || GetNextIsSloped())
|
|
return;
|
|
|
|
TileElement* tileElement = map_get_first_element_at(NextLoc);
|
|
if (tileElement == nullptr)
|
|
return;
|
|
|
|
for (;; tileElement++)
|
|
{
|
|
if (tileElement->GetType() == TILE_ELEMENT_TYPE_PATH)
|
|
{
|
|
if (NextLoc.z == tileElement->GetBaseZ())
|
|
break;
|
|
}
|
|
if (tileElement->IsLastForTile())
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
int32_t positions_free = 15;
|
|
|
|
if (tileElement->AsPath()->HasAddition())
|
|
{
|
|
if (!tileElement->AsPath()->AdditionIsGhost())
|
|
{
|
|
auto* pathAddEntry = tileElement->AsPath()->GetAdditionEntry();
|
|
if (pathAddEntry == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!(pathAddEntry->flags & PATH_BIT_FLAG_IS_BENCH))
|
|
positions_free = 9;
|
|
}
|
|
}
|
|
|
|
int32_t edges = (tileElement->AsPath()->GetEdges()) ^ 0xF;
|
|
if (edges == 0)
|
|
return;
|
|
|
|
uint8_t chosen_edge = scenario_rand() & 0x3;
|
|
|
|
for (; !(edges & (1 << chosen_edge));)
|
|
chosen_edge = (chosen_edge + 1) & 3;
|
|
|
|
ride_id_t ride_to_view;
|
|
uint8_t ride_seat_to_view;
|
|
if (!peep_find_ride_to_look_at(this, chosen_edge, &ride_to_view, &ride_seat_to_view))
|
|
return;
|
|
|
|
// Check if there is a peep watching (and if there is place for us)
|
|
for (auto peep : EntityTileList<Peep>({ x, y }))
|
|
{
|
|
if (peep->State != PeepState::Watching)
|
|
continue;
|
|
|
|
if (z != peep->z)
|
|
continue;
|
|
|
|
if ((peep->Var37 & 0x3) != chosen_edge)
|
|
continue;
|
|
|
|
positions_free &= ~(1 << ((peep->Var37 & 0x1C) >> 2));
|
|
}
|
|
|
|
if (!positions_free)
|
|
return;
|
|
|
|
uint8_t chosen_position = scenario_rand() & 0x3;
|
|
|
|
for (; !(positions_free & (1 << chosen_position));)
|
|
chosen_position = (chosen_position + 1) & 3;
|
|
|
|
CurrentRide = ride_to_view;
|
|
CurrentSeat = ride_seat_to_view;
|
|
Var37 = chosen_edge | (chosen_position << 2);
|
|
|
|
SetState(PeepState::Watching);
|
|
SubState = 0;
|
|
|
|
int32_t destX = (x & 0xFFE0) + _WatchingPositionOffsets[Var37 & 0x1F].x;
|
|
int32_t destY = (y & 0xFFE0) + _WatchingPositionOffsets[Var37 & 0x1F].y;
|
|
|
|
SetDestination({ destX, destY }, 3);
|
|
|
|
if (CurrentSeat & 1)
|
|
{
|
|
InsertNewThought(PeepThoughtType::NewRide);
|
|
}
|
|
if (CurrentRide == RIDE_ID_NULL)
|
|
{
|
|
InsertNewThought(PeepThoughtType::Scenery);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x69185D
|
|
*/
|
|
void Guest::UpdateQueuing()
|
|
{
|
|
if (!CheckForPath())
|
|
{
|
|
RemoveFromQueue();
|
|
return;
|
|
}
|
|
auto ride = get_ride(CurrentRide);
|
|
if (ride == nullptr || ride->status != RideStatus::Open)
|
|
{
|
|
RemoveFromQueue();
|
|
SetState(PeepState::One);
|
|
return;
|
|
}
|
|
|
|
// If not in the queue then at front of queue
|
|
if (RideSubState != PeepRideSubState::InQueue)
|
|
{
|
|
bool is_front = true;
|
|
// Fix #4819: Occasionally the peep->GuestNextInQueue is incorrectly set
|
|
// to prevent this from causing the peeps to enter a loop
|
|
// first check if the next in queue is actually nearby
|
|
// if they are not then it's safe to assume that this is
|
|
// the front of the queue.
|
|
Peep* nextGuest = GetEntity<Guest>(GuestNextInQueue);
|
|
if (nextGuest != nullptr)
|
|
{
|
|
if (abs(nextGuest->x - x) < 32 && abs(nextGuest->y - y) < 32)
|
|
{
|
|
is_front = false;
|
|
}
|
|
}
|
|
|
|
if (is_front)
|
|
{
|
|
// Happens every time peep goes onto ride.
|
|
DestinationTolerance = 0;
|
|
SetState(PeepState::QueuingFront);
|
|
RideSubState = PeepRideSubState::AtEntrance;
|
|
return;
|
|
}
|
|
|
|
// Give up queueing for the ride
|
|
sprite_direction ^= (1 << 4);
|
|
Invalidate();
|
|
RemoveFromQueue();
|
|
SetState(PeepState::One);
|
|
return;
|
|
}
|
|
|
|
uint8_t pathingResult;
|
|
PerformNextAction(pathingResult);
|
|
if (!IsActionInterruptable())
|
|
return;
|
|
if (SpriteType == PeepSpriteType::Normal)
|
|
{
|
|
if (TimeInQueue >= 2000 && (0xFFFF & scenario_rand()) <= 119)
|
|
{
|
|
// Eat Food/Look at watch
|
|
Action = PeepActionType::EatFood;
|
|
ActionFrame = 0;
|
|
ActionSpriteImageOffset = 0;
|
|
UpdateCurrentActionSpriteType();
|
|
}
|
|
if (TimeInQueue >= 3500 && (0xFFFF & scenario_rand()) <= 93)
|
|
{
|
|
// Create the I have been waiting in line ages thought
|
|
InsertNewThought(PeepThoughtType::QueuingAges, CurrentRide);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!(TimeInQueue & 0x3F) && IsActionIdle() && NextActionSpriteType == PeepActionSpriteType::WatchRide)
|
|
{
|
|
switch (SpriteType)
|
|
{
|
|
case PeepSpriteType::IceCream:
|
|
case PeepSpriteType::Chips:
|
|
case PeepSpriteType::Burger:
|
|
case PeepSpriteType::Drink:
|
|
case PeepSpriteType::Candyfloss:
|
|
case PeepSpriteType::Pizza:
|
|
case PeepSpriteType::Popcorn:
|
|
case PeepSpriteType::HotDog:
|
|
case PeepSpriteType::Tentacle:
|
|
case PeepSpriteType::ToffeeApple:
|
|
case PeepSpriteType::Doughnut:
|
|
case PeepSpriteType::Coffee:
|
|
case PeepSpriteType::Chicken:
|
|
case PeepSpriteType::Lemonade:
|
|
case PeepSpriteType::Pretzel:
|
|
case PeepSpriteType::SuJongkwa:
|
|
case PeepSpriteType::Juice:
|
|
case PeepSpriteType::FunnelCake:
|
|
case PeepSpriteType::Noodles:
|
|
case PeepSpriteType::Sausage:
|
|
case PeepSpriteType::Soup:
|
|
case PeepSpriteType::Sandwich:
|
|
// Eat food
|
|
Action = PeepActionType::EatFood;
|
|
ActionFrame = 0;
|
|
ActionSpriteImageOffset = 0;
|
|
UpdateCurrentActionSpriteType();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (TimeInQueue < 4300)
|
|
return;
|
|
|
|
if (Happiness <= 65 && (0xFFFF & scenario_rand()) < 2184)
|
|
{
|
|
// Give up queueing for the ride
|
|
sprite_direction ^= (1 << 4);
|
|
Invalidate();
|
|
RemoveFromQueue();
|
|
SetState(PeepState::One);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* rct2: 0x691451
|
|
*/
|
|
void Guest::UpdateEnteringPark()
|
|
{
|
|
if (Var37 != 1)
|
|
{
|
|
uint8_t pathingResult;
|
|
PerformNextAction(pathingResult);
|
|
if ((pathingResult & PATHING_OUTSIDE_PARK))
|
|
{
|
|
decrement_guests_heading_for_park();
|
|
peep_sprite_remove(this);
|
|
}
|
|
return;
|
|
}
|
|
if (auto loc = UpdateAction(); loc.has_value())
|
|
{
|
|
MoveTo({ loc.value(), z });
|
|
return;
|
|
}
|
|
SetState(PeepState::Falling);
|
|
|
|
OutsideOfPark = false;
|
|
ParkEntryTime = gCurrentTicks;
|
|
increment_guests_in_park();
|
|
decrement_guests_heading_for_park();
|
|
auto intent = Intent(INTENT_ACTION_UPDATE_GUEST_COUNT);
|
|
context_broadcast_intent(&intent);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x6914CD
|
|
*/
|
|
void Guest::UpdateLeavingPark()
|
|
{
|
|
if (Var37 != 0)
|
|
{
|
|
uint8_t pathingResult;
|
|
PerformNextAction(pathingResult);
|
|
if (!(pathingResult & PATHING_OUTSIDE_PARK))
|
|
return;
|
|
peep_sprite_remove(this);
|
|
return;
|
|
}
|
|
|
|
if (auto loc = UpdateAction(); loc.has_value())
|
|
{
|
|
MoveTo({ loc.value(), z });
|
|
return;
|
|
}
|
|
|
|
OutsideOfPark = true;
|
|
DestinationTolerance = 5;
|
|
decrement_guests_in_park();
|
|
auto intent = Intent(INTENT_ACTION_UPDATE_GUEST_COUNT);
|
|
context_broadcast_intent(&intent);
|
|
Var37 = 1;
|
|
|
|
window_invalidate_by_class(WC_GUEST_LIST);
|
|
uint8_t pathingResult;
|
|
PerformNextAction(pathingResult);
|
|
if (!(pathingResult & PATHING_OUTSIDE_PARK))
|
|
return;
|
|
Remove();
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x6916D6
|
|
*/
|
|
void Guest::UpdateWatching()
|
|
{
|
|
if (SubState == 0)
|
|
{
|
|
if (!CheckForPath())
|
|
return;
|
|
uint8_t pathingResult;
|
|
PerformNextAction(pathingResult);
|
|
if (!(pathingResult & PATHING_DESTINATION_REACHED))
|
|
return;
|
|
|
|
SetDestination(GetLocation());
|
|
|
|
sprite_direction = (Var37 & 3) * 8;
|
|
|
|
Action = PeepActionType::Idle;
|
|
NextActionSpriteType = PeepActionSpriteType::WatchRide;
|
|
|
|
SwitchNextActionSpriteType();
|
|
|
|
SubState++;
|
|
|
|
TimeToStand = std::clamp(((129 - Energy) * 16 + 50) / 2, 0, 255);
|
|
UpdateSpriteType();
|
|
}
|
|
else if (SubState == 1)
|
|
{
|
|
if (!IsActionInterruptable())
|
|
{
|
|
// 6917F6
|
|
UpdateAction();
|
|
Invalidate();
|
|
if (!IsActionWalking())
|
|
return;
|
|
Action = PeepActionType::Idle;
|
|
}
|
|
else
|
|
{
|
|
if (HasFoodOrDrink())
|
|
{
|
|
if ((scenario_rand() & 0xFFFF) <= 1310)
|
|
{
|
|
Action = PeepActionType::EatFood;
|
|
ActionFrame = 0;
|
|
ActionSpriteImageOffset = 0;
|
|
UpdateCurrentActionSpriteType();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if ((scenario_rand() & 0xFFFF) <= 655)
|
|
{
|
|
Action = PeepActionType::TakePhoto;
|
|
ActionFrame = 0;
|
|
ActionSpriteImageOffset = 0;
|
|
UpdateCurrentActionSpriteType();
|
|
return;
|
|
}
|
|
|
|
if ((StandingFlags & 1))
|
|
{
|
|
if ((scenario_rand() & 0xFFFF) <= 655)
|
|
{
|
|
Action = PeepActionType::Wave;
|
|
ActionFrame = 0;
|
|
ActionSpriteImageOffset = 0;
|
|
UpdateCurrentActionSpriteType();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
StandingFlags ^= (1 << 7);
|
|
if (!(StandingFlags & (1 << 7)))
|
|
return;
|
|
|
|
TimeToStand--;
|
|
if (TimeToStand != 0)
|
|
return;
|
|
|
|
SetState(PeepState::Walking);
|
|
UpdateSpriteType();
|
|
// Send peep to the centre of current tile.
|
|
|
|
auto destination = GetLocation().ToTileCentre();
|
|
SetDestination(destination, 5);
|
|
UpdateCurrentActionSpriteType();
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00691089
|
|
*/
|
|
void Guest::UpdateUsingBin()
|
|
{
|
|
switch (UsingBinSubState)
|
|
{
|
|
case PeepUsingBinSubState::WalkingToBin:
|
|
{
|
|
if (!CheckForPath())
|
|
return;
|
|
|
|
uint8_t pathingResult;
|
|
PerformNextAction(pathingResult);
|
|
if (pathingResult & PATHING_DESTINATION_REACHED)
|
|
{
|
|
UsingBinSubState = PeepUsingBinSubState::GoingBack;
|
|
}
|
|
break;
|
|
}
|
|
case PeepUsingBinSubState::GoingBack:
|
|
{
|
|
if (!IsActionWalking())
|
|
{
|
|
UpdateAction();
|
|
Invalidate();
|
|
return;
|
|
}
|
|
|
|
PathElement* foundElement = nullptr;
|
|
for (auto* pathElement : TileElementsView<PathElement>(NextLoc))
|
|
{
|
|
if (pathElement->GetBaseZ() != NextLoc.z)
|
|
continue;
|
|
|
|
if (!pathElement->HasAddition())
|
|
break;
|
|
|
|
auto* pathAddEntry = pathElement->GetAdditionEntry();
|
|
if (!(pathAddEntry->flags & PATH_BIT_FLAG_IS_BIN))
|
|
break;
|
|
|
|
if (pathElement->IsBroken())
|
|
break;
|
|
|
|
if (pathElement->AdditionIsGhost())
|
|
break;
|
|
|
|
foundElement = pathElement;
|
|
break;
|
|
}
|
|
|
|
if (foundElement == nullptr)
|
|
{
|
|
StateReset();
|
|
return;
|
|
}
|
|
|
|
// Bin selection is one of 4 corners
|
|
uint8_t selectedBin = Var37 * 2;
|
|
|
|
// This counts down 2 = No rubbish, 0 = full
|
|
uint8_t spaceLeftInBin = 0x3 & (foundElement->GetAdditionStatus() >> selectedBin);
|
|
uint64_t emptyContainers = GetEmptyContainerFlags();
|
|
|
|
for (uint8_t curContainer = 0; curContainer < 64; curContainer++)
|
|
{
|
|
if (!(emptyContainers & (1ULL << curContainer)))
|
|
continue;
|
|
|
|
auto item = ShopItem(curContainer);
|
|
if (spaceLeftInBin != 0)
|
|
{
|
|
// OpenRCT2 modification: This previously used
|
|
// the tick count as a simple random function
|
|
// switched to scenario_rand as it is more reliable
|
|
if ((scenario_rand() & 7) == 0)
|
|
spaceLeftInBin--;
|
|
RemoveItem(item);
|
|
WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_INVENTORY;
|
|
UpdateSpriteType();
|
|
continue;
|
|
}
|
|
|
|
auto litterType = Litter::Type(GetShopItemDescriptor(item).Type);
|
|
|
|
int32_t litterX = x + (scenario_rand() & 7) - 3;
|
|
int32_t litterY = y + (scenario_rand() & 7) - 3;
|
|
|
|
Litter::Create({ litterX, litterY, z, static_cast<Direction>(scenario_rand() & 3) }, litterType);
|
|
RemoveItem(item);
|
|
WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_INVENTORY;
|
|
|
|
UpdateSpriteType();
|
|
}
|
|
|
|
uint8_t additionStatus = foundElement->GetAdditionStatus();
|
|
// Place new amount in bin by first clearing the value
|
|
additionStatus &= ~(3 << selectedBin);
|
|
// Then placing the new value.
|
|
additionStatus |= spaceLeftInBin << selectedBin;
|
|
foundElement->SetAdditionStatus(additionStatus);
|
|
|
|
map_invalidate_tile_zoom0({ NextLoc, foundElement->GetBaseZ(), foundElement->GetClearanceZ() });
|
|
StateReset();
|
|
break;
|
|
}
|
|
default:
|
|
Guard::Assert(false, "Invalid sub state");
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* Simplifies 0x690582. Returns true if should find bench*/
|
|
bool Guest::ShouldFindBench()
|
|
{
|
|
if (PeepFlags & PEEP_FLAGS_LEAVING_PARK)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (HasFoodOrDrink())
|
|
{
|
|
if (Hunger < 128 || Happiness < 128)
|
|
{
|
|
if (!GetNextIsSurface() && !GetNextIsSloped())
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Nausea <= 170 && Energy > 50)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return !GetNextIsSurface() && !GetNextIsSloped();
|
|
}
|
|
|
|
static PathElement* FindBench(const CoordsXYZ& loc)
|
|
{
|
|
for (auto* pathElement : TileElementsView<PathElement>(loc))
|
|
{
|
|
if (pathElement->GetBaseZ() != loc.z)
|
|
continue;
|
|
|
|
if (!pathElement->HasAddition())
|
|
continue;
|
|
|
|
auto* pathAddEntry = pathElement->GetAdditionEntry();
|
|
if (pathAddEntry == nullptr || !(pathAddEntry->flags & PATH_BIT_FLAG_IS_BENCH))
|
|
continue;
|
|
|
|
if (pathElement->IsBroken())
|
|
continue;
|
|
|
|
if (pathElement->AdditionIsGhost())
|
|
continue;
|
|
|
|
return pathElement;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00690582
|
|
* Returns true when the guest wants to sit down and has found a bench to sit on
|
|
*/
|
|
bool Guest::UpdateWalkingFindBench()
|
|
{
|
|
if (!ShouldFindBench())
|
|
return false;
|
|
|
|
auto* pathElement = FindBench(NextLoc);
|
|
if (pathElement == nullptr)
|
|
return false;
|
|
|
|
int32_t edges = pathElement->GetEdges() ^ 0xF;
|
|
if (edges == 0)
|
|
return false;
|
|
uint8_t chosen_edge = scenario_rand() & 0x3;
|
|
|
|
for (; !(edges & (1 << chosen_edge));)
|
|
chosen_edge = (chosen_edge + 1) & 0x3;
|
|
|
|
uint8_t free_edge = 3;
|
|
|
|
// Check if there is no peep sitting in chosen_edge
|
|
for (auto peep : EntityTileList<Peep>({ x, y }))
|
|
{
|
|
if (peep->State != PeepState::Sitting)
|
|
continue;
|
|
|
|
if (z != peep->z)
|
|
continue;
|
|
|
|
if ((peep->Var37 & 0x3) != chosen_edge)
|
|
continue;
|
|
|
|
free_edge &= ~(1 << ((peep->Var37 & 0x4) >> 2));
|
|
}
|
|
|
|
if (!free_edge)
|
|
return false;
|
|
|
|
free_edge ^= 0x3;
|
|
if (!free_edge)
|
|
{
|
|
if (scenario_rand() & 0x8000000)
|
|
free_edge = 1;
|
|
}
|
|
|
|
Var37 = ((free_edge & 1) << 2) | chosen_edge;
|
|
|
|
SetState(PeepState::Sitting);
|
|
|
|
SittingSubState = PeepSittingSubState::TryingToSit;
|
|
|
|
int32_t benchX = (x & 0xFFE0) + BenchUseOffsets[Var37 & 0x7].x;
|
|
int32_t benchY = (y & 0xFFE0) + BenchUseOffsets[Var37 & 0x7].y;
|
|
|
|
SetDestination({ benchX, benchY }, 3);
|
|
|
|
return true;
|
|
}
|
|
|
|
static PathElement* FindBin(const CoordsXYZ& loc)
|
|
{
|
|
for (auto* pathElement : TileElementsView<PathElement>(loc))
|
|
{
|
|
if (pathElement->GetBaseZ() != loc.z)
|
|
continue;
|
|
|
|
if (!pathElement->HasAddition())
|
|
continue;
|
|
|
|
auto* pathAddEntry = pathElement->GetAdditionEntry();
|
|
if (pathAddEntry == nullptr || !(pathAddEntry->flags & PATH_BIT_FLAG_IS_BIN))
|
|
continue;
|
|
|
|
if (pathElement->IsBroken())
|
|
continue;
|
|
|
|
if (pathElement->AdditionIsGhost())
|
|
continue;
|
|
|
|
return pathElement;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
bool Guest::UpdateWalkingFindBin()
|
|
{
|
|
auto peep = this;
|
|
if (!peep->HasEmptyContainer())
|
|
return false;
|
|
|
|
if (peep->GetNextIsSurface())
|
|
return false;
|
|
|
|
auto* pathElement = FindBin(peep->NextLoc);
|
|
if (pathElement == nullptr)
|
|
return false;
|
|
|
|
int32_t edges = (pathElement->GetEdges()) ^ 0xF;
|
|
if (edges == 0)
|
|
return false;
|
|
|
|
uint8_t chosen_edge = scenario_rand() & 0x3;
|
|
|
|
// Note: Bin quantity is inverted 0 = full, 3 = empty
|
|
uint8_t bin_quantities = pathElement->GetAdditionStatus();
|
|
|
|
// Rotate the bin to the correct edge. Makes it easier for next calc.
|
|
bin_quantities = Numerics::ror8(Numerics::ror8(bin_quantities, chosen_edge), chosen_edge);
|
|
|
|
for (uint8_t free_edge = 4; free_edge != 0; free_edge--)
|
|
{
|
|
// If not full
|
|
if (bin_quantities & 0x3)
|
|
{
|
|
if (edges & (1 << chosen_edge))
|
|
break;
|
|
}
|
|
chosen_edge = (chosen_edge + 1) & 0x3;
|
|
bin_quantities = Numerics::ror8(bin_quantities, 2);
|
|
if ((free_edge - 1) == 0)
|
|
return 0;
|
|
}
|
|
|
|
peep->Var37 = chosen_edge;
|
|
|
|
peep->SetState(PeepState::UsingBin);
|
|
peep->UsingBinSubState = PeepUsingBinSubState::WalkingToBin;
|
|
|
|
int32_t binX = (peep->x & 0xFFE0) + BinUseOffsets[peep->Var37 & 0x3].x;
|
|
int32_t binY = (peep->y & 0xFFE0) + BinUseOffsets[peep->Var37 & 0x3].y;
|
|
|
|
peep->SetDestination({ binX, binY }, 3);
|
|
|
|
return true;
|
|
}
|
|
|
|
static PathElement* FindBreakableElement(const CoordsXYZ& loc)
|
|
{
|
|
for (auto* pathElement : TileElementsView<PathElement>(loc))
|
|
{
|
|
if (pathElement->GetBaseZ() != loc.z)
|
|
continue;
|
|
|
|
if (!pathElement->HasAddition())
|
|
continue;
|
|
|
|
auto* pathAddEntry = pathElement->GetAdditionEntry();
|
|
if (pathAddEntry == nullptr || !(pathAddEntry->flags & PATH_BIT_FLAG_BREAKABLE))
|
|
continue;
|
|
|
|
if (pathElement->IsBroken())
|
|
continue;
|
|
|
|
if (pathElement->AdditionIsGhost())
|
|
continue;
|
|
|
|
return pathElement;
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00690848
|
|
*/
|
|
static void peep_update_walking_break_scenery(Guest* peep)
|
|
{
|
|
if (gCheatsDisableVandalism)
|
|
return;
|
|
|
|
if (!(peep->PeepFlags & PEEP_FLAGS_ANGRY))
|
|
{
|
|
if (peep->Happiness >= 48)
|
|
return;
|
|
if (peep->Energy < 85)
|
|
return;
|
|
if (peep->State != PeepState::Walking)
|
|
return;
|
|
|
|
if ((peep->LitterCount & 0xC0) != 0xC0 && (peep->DisgustingCount & 0xC0) != 0xC0)
|
|
return;
|
|
|
|
if ((scenario_rand() & 0xFFFF) > 3276)
|
|
return;
|
|
}
|
|
|
|
if (peep->GetNextIsSurface())
|
|
return;
|
|
|
|
auto* tileElement = FindBreakableElement(peep->NextLoc);
|
|
if (tileElement == nullptr)
|
|
return;
|
|
|
|
int32_t edges = tileElement->GetEdges();
|
|
if (edges == 0xF)
|
|
return;
|
|
|
|
// Check if a peep is already sitting on the bench. If so, do not vandalise it.
|
|
for (auto peep2 : EntityTileList<Peep>({ peep->x, peep->y }))
|
|
{
|
|
if ((peep2->State != PeepState::Sitting) || (peep->z != peep2->z))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
for (auto inner_peep : EntityList<Staff>())
|
|
{
|
|
if (inner_peep->AssignedStaffType != StaffType::Security)
|
|
continue;
|
|
|
|
if (inner_peep->x == LOCATION_NULL)
|
|
continue;
|
|
|
|
int32_t x_diff = abs(inner_peep->x - peep->x);
|
|
int32_t y_diff = abs(inner_peep->y - peep->y);
|
|
|
|
if (std::max(x_diff, y_diff) < 224)
|
|
{
|
|
inner_peep->StaffVandalsStopped++;
|
|
return;
|
|
}
|
|
}
|
|
|
|
tileElement->SetIsBroken(true);
|
|
|
|
map_invalidate_tile_zoom1({ peep->NextLoc, tileElement->GetBaseZ(), tileElement->GetBaseZ() + 32 });
|
|
|
|
peep->Angriness = 16;
|
|
}
|
|
|
|
/**
|
|
* rct2: 0x0069101A
|
|
*
|
|
* @return (CF)
|
|
*/
|
|
static bool peep_should_watch_ride(TileElement* tileElement)
|
|
{
|
|
// Ghosts are purely this-client-side and should not cause any interaction,
|
|
// as that may lead to a desync.
|
|
if (network_get_mode() != NETWORK_MODE_NONE)
|
|
{
|
|
if (tileElement->IsGhost())
|
|
return false;
|
|
}
|
|
|
|
auto ride = get_ride(tileElement->AsTrack()->GetRideIndex());
|
|
if (ride == nullptr || !ride->IsRide())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// This is most likely to have peeps watch new rides
|
|
if (ride->excitement == RIDE_RATING_UNDEFINED)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (ride->excitement >= RIDE_RATING(4, 70))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (ride->intensity >= RIDE_RATING(4, 50))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_INTERESTING_TO_LOOK_AT))
|
|
{
|
|
if ((scenario_rand() & 0xFFFF) > 0x3333)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else if (ride->GetRideTypeDescriptor().HasFlag(RIDE_TYPE_FLAG_SLIGHTLY_INTERESTING_TO_LOOK_AT))
|
|
{
|
|
if ((scenario_rand() & 0xFFFF) > 0x1000)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool loc_690FD0(Peep* peep, ride_id_t* rideToView, uint8_t* rideSeatToView, TileElement* tileElement)
|
|
{
|
|
auto ride = get_ride(tileElement->AsTrack()->GetRideIndex());
|
|
if (ride == nullptr)
|
|
return false;
|
|
|
|
*rideToView = ride->id;
|
|
if (ride->excitement == RIDE_RATING_UNDEFINED)
|
|
{
|
|
*rideSeatToView = 1;
|
|
if (ride->status != RideStatus::Open)
|
|
{
|
|
if (tileElement->GetClearanceZ() > peep->NextLoc.z + (8 * COORDS_Z_STEP))
|
|
{
|
|
*rideSeatToView |= (1 << 1);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
*rideSeatToView = 0;
|
|
if (ride->status == RideStatus::Open && !(ride->lifecycle_flags & RIDE_LIFECYCLE_BROKEN_DOWN))
|
|
{
|
|
if (tileElement->GetClearanceZ() > peep->NextLoc.z + (8 * COORDS_Z_STEP))
|
|
{
|
|
*rideSeatToView = 0x02;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00690B99
|
|
*
|
|
* @param edge (eax)
|
|
* @param peep (esi)
|
|
* @param[out] rideToView (cl)
|
|
* @param[out] rideSeatToView (ch)
|
|
* @return !CF
|
|
*/
|
|
static bool peep_find_ride_to_look_at(Peep* peep, uint8_t edge, ride_id_t* rideToView, uint8_t* rideSeatToView)
|
|
{
|
|
TileElement* tileElement;
|
|
|
|
auto surfaceElement = map_get_surface_element_at(peep->NextLoc);
|
|
|
|
tileElement = reinterpret_cast<TileElement*>(surfaceElement);
|
|
if (tileElement == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
do
|
|
{
|
|
// Ghosts are purely this-client-side and should not cause any interaction,
|
|
// as that may lead to a desync.
|
|
if (network_get_mode() != NETWORK_MODE_NONE)
|
|
{
|
|
if (tileElement->IsGhost())
|
|
continue;
|
|
}
|
|
if (tileElement->GetType() != TILE_ELEMENT_TYPE_WALL)
|
|
continue;
|
|
if (tileElement->GetDirection() != edge)
|
|
continue;
|
|
auto wallEntry = tileElement->AsWall()->GetEntry();
|
|
if (wallEntry == nullptr || (wallEntry->flags2 & WALL_SCENERY_2_IS_OPAQUE))
|
|
continue;
|
|
if (peep->NextLoc.z + (4 * COORDS_Z_STEP) <= tileElement->GetBaseZ())
|
|
continue;
|
|
if (peep->NextLoc.z + (1 * COORDS_Z_STEP) >= tileElement->GetClearanceZ())
|
|
continue;
|
|
|
|
return false;
|
|
} while (!(tileElement++)->IsLastForTile());
|
|
|
|
uint16_t x = peep->NextLoc.x + CoordsDirectionDelta[edge].x;
|
|
uint16_t y = peep->NextLoc.y + CoordsDirectionDelta[edge].y;
|
|
if (!map_is_location_valid({ x, y }))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
surfaceElement = map_get_surface_element_at(CoordsXY{ x, y });
|
|
|
|
tileElement = reinterpret_cast<TileElement*>(surfaceElement);
|
|
if (tileElement == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
do
|
|
{
|
|
// Ghosts are purely this-client-side and should not cause any interaction,
|
|
// as that may lead to a desync.
|
|
if (network_get_mode() != NETWORK_MODE_NONE)
|
|
{
|
|
if (tileElement->IsGhost())
|
|
continue;
|
|
}
|
|
if (tileElement->GetType() != TILE_ELEMENT_TYPE_WALL)
|
|
continue;
|
|
if (direction_reverse(tileElement->GetDirection()) != edge)
|
|
continue;
|
|
auto wallEntry = tileElement->AsWall()->GetEntry();
|
|
if (wallEntry == nullptr || (wallEntry->flags2 & WALL_SCENERY_2_IS_OPAQUE))
|
|
continue;
|
|
// TODO: Check whether this shouldn't be <=, as the other loops use. If so, also extract as loop A.
|
|
if (peep->NextLoc.z + (4 * COORDS_Z_STEP) >= tileElement->GetBaseZ())
|
|
continue;
|
|
if (peep->NextLoc.z + (1 * COORDS_Z_STEP) >= tileElement->GetClearanceZ())
|
|
continue;
|
|
|
|
return false;
|
|
} while (!(tileElement++)->IsLastForTile());
|
|
|
|
// TODO: Extract loop B
|
|
tileElement = reinterpret_cast<TileElement*>(surfaceElement);
|
|
do
|
|
{
|
|
// Ghosts are purely this-client-side and should not cause any interaction,
|
|
// as that may lead to a desync.
|
|
if (network_get_mode() != NETWORK_MODE_NONE)
|
|
{
|
|
if (tileElement->IsGhost())
|
|
continue;
|
|
}
|
|
|
|
if (tileElement->GetClearanceZ() + (1 * COORDS_Z_STEP) < peep->NextLoc.z)
|
|
continue;
|
|
if (peep->NextLoc.z + (6 * COORDS_Z_STEP) < tileElement->GetBaseZ())
|
|
continue;
|
|
|
|
if (tileElement->GetType() == TILE_ELEMENT_TYPE_TRACK)
|
|
{
|
|
if (peep_should_watch_ride(tileElement))
|
|
{
|
|
return loc_690FD0(peep, rideToView, rideSeatToView, tileElement);
|
|
}
|
|
}
|
|
|
|
if (tileElement->GetType() == TILE_ELEMENT_TYPE_LARGE_SCENERY)
|
|
{
|
|
const auto* sceneryEntry = tileElement->AsLargeScenery()->GetEntry();
|
|
if (sceneryEntry == nullptr || !(sceneryEntry->flags & LARGE_SCENERY_FLAG_PHOTOGENIC))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
*rideSeatToView = 0;
|
|
if (tileElement->GetClearanceZ() >= peep->NextLoc.z + (8 * COORDS_Z_STEP))
|
|
{
|
|
*rideSeatToView = 0x02;
|
|
}
|
|
|
|
*rideToView = RIDE_ID_NULL;
|
|
|
|
return true;
|
|
}
|
|
} while (!(tileElement++)->IsLastForTile());
|
|
|
|
// TODO: Extract loop C
|
|
tileElement = reinterpret_cast<TileElement*>(surfaceElement);
|
|
do
|
|
{
|
|
// Ghosts are purely this-client-side and should not cause any interaction,
|
|
// as that may lead to a desync.
|
|
if (network_get_mode() != NETWORK_MODE_NONE)
|
|
{
|
|
if (tileElement->IsGhost())
|
|
continue;
|
|
}
|
|
if (tileElement->GetClearanceZ() + (1 * COORDS_Z_STEP) < peep->NextLoc.z)
|
|
continue;
|
|
if (peep->NextLoc.z + (6 * COORDS_Z_STEP) < tileElement->GetBaseZ())
|
|
continue;
|
|
if (tileElement->GetType() == TILE_ELEMENT_TYPE_SURFACE)
|
|
continue;
|
|
if (tileElement->GetType() == TILE_ELEMENT_TYPE_PATH)
|
|
continue;
|
|
|
|
if (tileElement->GetType() == TILE_ELEMENT_TYPE_WALL)
|
|
{
|
|
auto wallEntry = tileElement->AsWall()->GetEntry();
|
|
if (wallEntry == nullptr || (wallEntry->flags2 & WALL_SCENERY_2_IS_OPAQUE))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
} while (!(tileElement++)->IsLastForTile());
|
|
|
|
x += CoordsDirectionDelta[edge].x;
|
|
y += CoordsDirectionDelta[edge].y;
|
|
if (!map_is_location_valid({ x, y }))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
surfaceElement = map_get_surface_element_at(CoordsXY{ x, y });
|
|
|
|
// TODO: extract loop A
|
|
tileElement = reinterpret_cast<TileElement*>(surfaceElement);
|
|
|
|
if (tileElement == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
do
|
|
{
|
|
// Ghosts are purely this-client-side and should not cause any interaction,
|
|
// as that may lead to a desync.
|
|
if (network_get_mode() != NETWORK_MODE_NONE)
|
|
{
|
|
if (tileElement->IsGhost())
|
|
continue;
|
|
}
|
|
if (tileElement->GetType() != TILE_ELEMENT_TYPE_WALL)
|
|
continue;
|
|
if (direction_reverse(tileElement->GetDirection()) != edge)
|
|
continue;
|
|
auto wallEntry = tileElement->AsWall()->GetEntry();
|
|
if (wallEntry == nullptr || (wallEntry->flags2 & WALL_SCENERY_2_IS_OPAQUE))
|
|
continue;
|
|
if (peep->NextLoc.z + (6 * COORDS_Z_STEP) <= tileElement->GetBaseZ())
|
|
continue;
|
|
if (peep->NextLoc.z >= tileElement->GetClearanceZ())
|
|
continue;
|
|
|
|
return false;
|
|
} while (!(tileElement++)->IsLastForTile());
|
|
|
|
// TODO: Extract loop B
|
|
tileElement = reinterpret_cast<TileElement*>(surfaceElement);
|
|
do
|
|
{
|
|
// Ghosts are purely this-client-side and should not cause any interaction,
|
|
// as that may lead to a desync.
|
|
if (network_get_mode() != NETWORK_MODE_NONE)
|
|
{
|
|
if (tileElement->IsGhost())
|
|
continue;
|
|
}
|
|
if (tileElement->GetClearanceZ() + (1 * COORDS_Z_STEP) < peep->NextLoc.z)
|
|
continue;
|
|
if (peep->NextLoc.z + (8 * COORDS_Z_STEP) < tileElement->GetBaseZ())
|
|
continue;
|
|
|
|
if (tileElement->GetType() == TILE_ELEMENT_TYPE_TRACK)
|
|
{
|
|
if (peep_should_watch_ride(tileElement))
|
|
{
|
|
return loc_690FD0(peep, rideToView, rideSeatToView, tileElement);
|
|
}
|
|
}
|
|
|
|
if (tileElement->GetType() == TILE_ELEMENT_TYPE_LARGE_SCENERY)
|
|
{
|
|
auto* sceneryEntry = tileElement->AsLargeScenery()->GetEntry();
|
|
if (!(sceneryEntry == nullptr || sceneryEntry->flags & LARGE_SCENERY_FLAG_PHOTOGENIC))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
*rideSeatToView = 0;
|
|
if (tileElement->GetClearanceZ() >= peep->NextLoc.z + (8 * COORDS_Z_STEP))
|
|
{
|
|
*rideSeatToView = 0x02;
|
|
}
|
|
|
|
*rideToView = RIDE_ID_NULL;
|
|
|
|
return true;
|
|
}
|
|
} while (!(tileElement++)->IsLastForTile());
|
|
|
|
// TODO: Extract loop C
|
|
tileElement = reinterpret_cast<TileElement*>(surfaceElement);
|
|
do
|
|
{
|
|
// Ghosts are purely this-client-side and should not cause any interaction,
|
|
// as that may lead to a desync.
|
|
if (network_get_mode() != NETWORK_MODE_NONE)
|
|
{
|
|
if (tileElement->IsGhost())
|
|
continue;
|
|
}
|
|
if (tileElement->GetClearanceZ() + (1 * COORDS_Z_STEP) < peep->NextLoc.z)
|
|
continue;
|
|
if (peep->NextLoc.z + (8 * COORDS_Z_STEP) < tileElement->GetBaseZ())
|
|
continue;
|
|
if (tileElement->GetType() == TILE_ELEMENT_TYPE_SURFACE)
|
|
continue;
|
|
if (tileElement->GetType() == TILE_ELEMENT_TYPE_PATH)
|
|
continue;
|
|
|
|
if (tileElement->GetType() == TILE_ELEMENT_TYPE_WALL)
|
|
{
|
|
auto wallEntry = tileElement->AsWall()->GetEntry();
|
|
if (wallEntry == nullptr || (wallEntry->flags2 & WALL_SCENERY_2_IS_OPAQUE))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
} while (!(tileElement++)->IsLastForTile());
|
|
|
|
x += CoordsDirectionDelta[edge].x;
|
|
y += CoordsDirectionDelta[edge].y;
|
|
if (!map_is_location_valid({ x, y }))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
surfaceElement = map_get_surface_element_at(CoordsXY{ x, y });
|
|
|
|
// TODO: extract loop A
|
|
tileElement = reinterpret_cast<TileElement*>(surfaceElement);
|
|
if (tileElement == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
do
|
|
{
|
|
// Ghosts are purely this-client-side and should not cause any interaction,
|
|
// as that may lead to a desync.
|
|
if (network_get_mode() != NETWORK_MODE_NONE)
|
|
{
|
|
if (tileElement->IsGhost())
|
|
continue;
|
|
}
|
|
if (tileElement->GetType() != TILE_ELEMENT_TYPE_WALL)
|
|
continue;
|
|
if (direction_reverse(tileElement->GetDirection()) != edge)
|
|
continue;
|
|
auto wallEntry = tileElement->AsWall()->GetEntry();
|
|
if (wallEntry == nullptr || (wallEntry->flags2 & WALL_SCENERY_2_IS_OPAQUE))
|
|
continue;
|
|
if (peep->NextLoc.z + (8 * COORDS_Z_STEP) <= tileElement->GetBaseZ())
|
|
continue;
|
|
if (peep->NextLoc.z >= tileElement->GetClearanceZ())
|
|
continue;
|
|
|
|
return false;
|
|
} while (!(tileElement++)->IsLastForTile());
|
|
|
|
// TODO: Extract loop B
|
|
tileElement = reinterpret_cast<TileElement*>(surfaceElement);
|
|
do
|
|
{
|
|
// Ghosts are purely this-client-side and should not cause any interaction,
|
|
// as that may lead to a desync.
|
|
if (network_get_mode() != NETWORK_MODE_NONE)
|
|
{
|
|
if (tileElement->IsGhost())
|
|
continue;
|
|
}
|
|
if (tileElement->GetClearanceZ() + (1 * COORDS_Z_STEP) < peep->NextLoc.z)
|
|
continue;
|
|
if (peep->NextLoc.z + (10 * COORDS_Z_STEP) < tileElement->GetBaseZ())
|
|
continue;
|
|
|
|
if (tileElement->GetType() == TILE_ELEMENT_TYPE_TRACK)
|
|
{
|
|
if (peep_should_watch_ride(tileElement))
|
|
{
|
|
return loc_690FD0(peep, rideToView, rideSeatToView, tileElement);
|
|
}
|
|
}
|
|
|
|
if (tileElement->GetType() == TILE_ELEMENT_TYPE_LARGE_SCENERY)
|
|
{
|
|
const auto* sceneryEntry = tileElement->AsLargeScenery()->GetEntry();
|
|
if (sceneryEntry == nullptr || !(sceneryEntry->flags & LARGE_SCENERY_FLAG_PHOTOGENIC))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
*rideSeatToView = 0;
|
|
if (tileElement->GetClearanceZ() >= peep->NextLoc.z + (8 * COORDS_Z_STEP))
|
|
{
|
|
*rideSeatToView = 0x02;
|
|
}
|
|
|
|
*rideToView = RIDE_ID_NULL;
|
|
|
|
return true;
|
|
}
|
|
} while (!(tileElement++)->IsLastForTile());
|
|
|
|
return false;
|
|
}
|
|
|
|
/* Part of 0x0069B8CC rct2: 0x0069BC31 */
|
|
void Guest::SetSpriteType(PeepSpriteType new_sprite_type)
|
|
{
|
|
if (SpriteType == new_sprite_type)
|
|
return;
|
|
|
|
SpriteType = new_sprite_type;
|
|
ActionSpriteImageOffset = 0;
|
|
WalkingFrameNum = 0;
|
|
|
|
if (IsActionInterruptable())
|
|
Action = PeepActionType::Walking;
|
|
|
|
PeepFlags &= ~PEEP_FLAGS_SLOW_WALK;
|
|
Guard::Assert(EnumValue(new_sprite_type) < std::size(gSpriteTypeToSlowWalkMap));
|
|
if (gSpriteTypeToSlowWalkMap[EnumValue(new_sprite_type)])
|
|
{
|
|
PeepFlags |= PEEP_FLAGS_SLOW_WALK;
|
|
}
|
|
|
|
ActionSpriteType = PeepActionSpriteType::Invalid;
|
|
UpdateCurrentActionSpriteType();
|
|
|
|
if (State == PeepState::Sitting)
|
|
{
|
|
Action = PeepActionType::Idle;
|
|
NextActionSpriteType = PeepActionSpriteType::SittingIdle;
|
|
SwitchNextActionSpriteType();
|
|
}
|
|
if (State == PeepState::Watching)
|
|
{
|
|
Action = PeepActionType::Idle;
|
|
NextActionSpriteType = PeepActionSpriteType::WatchRide;
|
|
SwitchNextActionSpriteType();
|
|
}
|
|
}
|
|
|
|
struct item_pref_t
|
|
{
|
|
ShopItem item;
|
|
PeepSpriteType sprite_type;
|
|
};
|
|
|
|
// clang-format off
|
|
static item_pref_t item_order_preference[] = {
|
|
{ ShopItem::IceCream, PeepSpriteType::IceCream },
|
|
{ ShopItem::Chips, PeepSpriteType::Chips },
|
|
{ ShopItem::Pizza, PeepSpriteType::Pizza },
|
|
{ ShopItem::Burger, PeepSpriteType::Burger },
|
|
{ ShopItem::Drink, PeepSpriteType::Drink },
|
|
{ ShopItem::Coffee, PeepSpriteType::Coffee },
|
|
{ ShopItem::Chicken, PeepSpriteType::Chicken },
|
|
{ ShopItem::Lemonade, PeepSpriteType::Lemonade },
|
|
{ ShopItem::Candyfloss, PeepSpriteType::Candyfloss },
|
|
{ ShopItem::Popcorn, PeepSpriteType::Popcorn },
|
|
{ ShopItem::HotDog, PeepSpriteType::HotDog },
|
|
{ ShopItem::Tentacle, PeepSpriteType::Tentacle },
|
|
{ ShopItem::ToffeeApple, PeepSpriteType::ToffeeApple },
|
|
{ ShopItem::Doughnut, PeepSpriteType::Doughnut },
|
|
{ ShopItem::Pretzel, PeepSpriteType::Pretzel },
|
|
{ ShopItem::Cookie, PeepSpriteType::Pretzel },
|
|
{ ShopItem::Chocolate, PeepSpriteType::Coffee },
|
|
{ ShopItem::IcedTea, PeepSpriteType::Coffee },
|
|
{ ShopItem::FunnelCake, PeepSpriteType::FunnelCake },
|
|
{ ShopItem::BeefNoodles, PeepSpriteType::Noodles },
|
|
{ ShopItem::FriedRiceNoodles, PeepSpriteType::Noodles },
|
|
{ ShopItem::WontonSoup, PeepSpriteType::Soup },
|
|
{ ShopItem::MeatballSoup, PeepSpriteType::Soup },
|
|
{ ShopItem::FruitJuice, PeepSpriteType::Juice },
|
|
{ ShopItem::SoybeanMilk, PeepSpriteType::SuJongkwa },
|
|
{ ShopItem::Sujeonggwa, PeepSpriteType::SuJongkwa },
|
|
{ ShopItem::SubSandwich, PeepSpriteType::Sandwich },
|
|
{ ShopItem::RoastSausage, PeepSpriteType::Sausage },
|
|
{ ShopItem::Balloon, PeepSpriteType::Balloon },
|
|
{ ShopItem::Hat, PeepSpriteType::Hat },
|
|
{ ShopItem::Sunglasses, PeepSpriteType::Sunglasses },
|
|
};
|
|
// clang-format on
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069B8CC
|
|
*/
|
|
void Guest::UpdateSpriteType()
|
|
{
|
|
if (SpriteType == PeepSpriteType::Balloon && (scenario_rand() & 0xFFFF) <= 327)
|
|
{
|
|
bool isBalloonPopped = false;
|
|
if (x != LOCATION_NULL)
|
|
{
|
|
if ((scenario_rand() & 0xFFFF) <= 13107)
|
|
{
|
|
isBalloonPopped = true;
|
|
OpenRCT2::Audio::Play3D(OpenRCT2::Audio::SoundId::BalloonPop, { x, y, z });
|
|
}
|
|
Balloon::Create({ x, y, z + 9 }, BalloonColour, isBalloonPopped);
|
|
}
|
|
RemoveItem(ShopItem::Balloon);
|
|
WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_INVENTORY;
|
|
}
|
|
|
|
if (climate_is_raining() && (HasItem(ShopItem::Umbrella)) && x != LOCATION_NULL)
|
|
{
|
|
CoordsXY loc = { x, y };
|
|
if (map_is_location_valid(loc.ToTileStart()))
|
|
{
|
|
TileElement* tileElement = map_get_first_element_at(loc);
|
|
while (true)
|
|
{
|
|
if (tileElement == nullptr)
|
|
break;
|
|
if (z < tileElement->GetBaseZ())
|
|
break;
|
|
|
|
if (tileElement->IsLastForTile())
|
|
{
|
|
SetSpriteType(PeepSpriteType::Umbrella);
|
|
return;
|
|
}
|
|
tileElement++;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (auto& itemPref : item_order_preference)
|
|
{
|
|
if (HasItem(itemPref.item))
|
|
{
|
|
SetSpriteType(itemPref.sprite_type);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (State == PeepState::Watching && StandingFlags & (1 << 1))
|
|
{
|
|
SetSpriteType(PeepSpriteType::Watching);
|
|
return;
|
|
}
|
|
|
|
if (Nausea > 170)
|
|
{
|
|
SetSpriteType(PeepSpriteType::VeryNauseous);
|
|
return;
|
|
}
|
|
|
|
if (Nausea > 140)
|
|
{
|
|
SetSpriteType(PeepSpriteType::Nauseous);
|
|
return;
|
|
}
|
|
|
|
if (Energy <= 64 && Happiness < 128)
|
|
{
|
|
SetSpriteType(PeepSpriteType::HeadDown);
|
|
return;
|
|
}
|
|
|
|
if (Energy <= 80 && Happiness < 128)
|
|
{
|
|
SetSpriteType(PeepSpriteType::ArmsCrossed);
|
|
return;
|
|
}
|
|
|
|
if (Toilet > 220)
|
|
{
|
|
SetSpriteType(PeepSpriteType::RequireToilet);
|
|
return;
|
|
}
|
|
|
|
SetSpriteType(PeepSpriteType::Normal);
|
|
}
|
|
|
|
bool Guest::HeadingForRideOrParkExit() const
|
|
{
|
|
return (PeepFlags & PEEP_FLAGS_LEAVING_PARK) || (GuestHeadingToRideId != RIDE_ID_NULL);
|
|
}
|
|
|
|
/**
|
|
* rct2: 0x00698342
|
|
* thought.item (eax)
|
|
* thought.type (ebx)
|
|
* argument_1 (esi & ebx)
|
|
* argument_2 (esi+2)
|
|
*/
|
|
void peep_thought_set_format_args(const PeepThought* thought, Formatter& ft)
|
|
{
|
|
ft.Add<rct_string_id>(PeepThoughts[EnumValue(thought->type)]);
|
|
|
|
PeepThoughtToActionFlag flags = PeepThoughtToActionMap[EnumValue(thought->type)].flags;
|
|
if (flags & PEEP_THOUGHT_ACTION_FLAG_RIDE)
|
|
{
|
|
auto ride = get_ride(thought->rideId);
|
|
if (ride != nullptr)
|
|
{
|
|
ride->FormatNameTo(ft);
|
|
}
|
|
else
|
|
{
|
|
ft.Add<rct_string_id>(STR_NONE);
|
|
}
|
|
}
|
|
else if (flags & PEEP_THOUGHT_ACTION_FLAG_SHOP_ITEM_SINGULAR)
|
|
{
|
|
ft.Add<rct_string_id>(GetShopItemDescriptor(thought->shopItem).Naming.Singular);
|
|
}
|
|
else if (flags & PEEP_THOUGHT_ACTION_FLAG_SHOP_ITEM_INDEFINITE)
|
|
{
|
|
ft.Add<rct_string_id>(GetShopItemDescriptor(thought->shopItem).Naming.Indefinite);
|
|
}
|
|
}
|
|
|
|
void Guest::InsertNewThought(PeepThoughtType thought_type)
|
|
{
|
|
InsertNewThought(thought_type, PeepThoughtItemNone);
|
|
}
|
|
|
|
void Guest::InsertNewThought(PeepThoughtType thought_type, ShopItem shopItem)
|
|
{
|
|
InsertNewThought(thought_type, static_cast<uint16_t>(shopItem));
|
|
}
|
|
|
|
void Guest::InsertNewThought(PeepThoughtType thought_type, ride_id_t rideId)
|
|
{
|
|
InsertNewThought(thought_type, static_cast<uint16_t>(rideId));
|
|
}
|
|
/**
|
|
*
|
|
* rct2: 0x699F5A
|
|
* al:thoughtType
|
|
* ah:thoughtArguments
|
|
* esi: peep
|
|
*/
|
|
void Guest::InsertNewThought(PeepThoughtType thoughtType, uint16_t thoughtArguments)
|
|
{
|
|
PeepActionType newAction = PeepThoughtToActionMap[EnumValue(thoughtType)].action;
|
|
if (newAction != PeepActionType::Walking && IsActionInterruptable())
|
|
{
|
|
Action = newAction;
|
|
ActionFrame = 0;
|
|
ActionSpriteImageOffset = 0;
|
|
UpdateCurrentActionSpriteType();
|
|
}
|
|
|
|
for (int32_t i = 0; i < PEEP_MAX_THOUGHTS; ++i)
|
|
{
|
|
PeepThought* thought = &Thoughts[i];
|
|
// Remove the oldest thought by setting it to NONE.
|
|
if (thought->type == PeepThoughtType::None)
|
|
break;
|
|
|
|
if (thought->type == thoughtType && thought->item == thoughtArguments)
|
|
{
|
|
// If the thought type has not changed then we need to move
|
|
// it to the top of the thought list. This is done by first removing the
|
|
// existing thought and placing it at the top.
|
|
if (i < PEEP_MAX_THOUGHTS - 2)
|
|
{
|
|
memmove(thought, thought + 1, sizeof(PeepThought) * (PEEP_MAX_THOUGHTS - i - 1));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
memmove(&Thoughts[1], &Thoughts[0], sizeof(PeepThought) * (PEEP_MAX_THOUGHTS - 1));
|
|
|
|
Thoughts[0].type = thoughtType;
|
|
Thoughts[0].item = thoughtArguments;
|
|
Thoughts[0].freshness = 0;
|
|
Thoughts[0].fresh_timeout = 0;
|
|
|
|
WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_THOUGHTS;
|
|
}
|
|
|
|
// clang-format off
|
|
/** rct2: 0x009823A0 */
|
|
static constexpr const PeepNauseaTolerance nausea_tolerance_distribution[] = {
|
|
PeepNauseaTolerance::None,
|
|
PeepNauseaTolerance::Low, PeepNauseaTolerance::Low,
|
|
PeepNauseaTolerance::Average, PeepNauseaTolerance::Average, PeepNauseaTolerance::Average,
|
|
PeepNauseaTolerance::High, PeepNauseaTolerance::High, PeepNauseaTolerance::High, PeepNauseaTolerance::High, PeepNauseaTolerance::High, PeepNauseaTolerance::High,
|
|
};
|
|
|
|
/** rct2: 0x009823BC */
|
|
static constexpr const uint8_t trouser_colours[] = {
|
|
COLOUR_BLACK,
|
|
COLOUR_GREY,
|
|
COLOUR_LIGHT_BROWN,
|
|
COLOUR_SATURATED_BROWN,
|
|
COLOUR_DARK_BROWN,
|
|
COLOUR_SALMON_PINK,
|
|
COLOUR_BLACK,
|
|
COLOUR_GREY,
|
|
COLOUR_LIGHT_BROWN,
|
|
COLOUR_SATURATED_BROWN,
|
|
COLOUR_DARK_BROWN,
|
|
COLOUR_SALMON_PINK,
|
|
COLOUR_BLACK,
|
|
COLOUR_GREY,
|
|
COLOUR_LIGHT_BROWN,
|
|
COLOUR_SATURATED_BROWN,
|
|
COLOUR_DARK_BROWN,
|
|
COLOUR_SALMON_PINK,
|
|
COLOUR_DARK_PURPLE,
|
|
COLOUR_LIGHT_PURPLE,
|
|
COLOUR_DARK_BLUE,
|
|
COLOUR_SATURATED_GREEN,
|
|
COLOUR_SATURATED_RED,
|
|
COLOUR_DARK_ORANGE,
|
|
COLOUR_BORDEAUX_RED,
|
|
};
|
|
|
|
/** rct2: 0x009823D5 */
|
|
static constexpr const uint8_t tshirt_colours[] = {
|
|
COLOUR_BLACK,
|
|
COLOUR_GREY,
|
|
COLOUR_LIGHT_BROWN,
|
|
COLOUR_SATURATED_BROWN,
|
|
COLOUR_DARK_BROWN,
|
|
COLOUR_SALMON_PINK,
|
|
COLOUR_BLACK,
|
|
COLOUR_GREY,
|
|
COLOUR_LIGHT_BROWN,
|
|
COLOUR_SATURATED_BROWN,
|
|
COLOUR_DARK_BROWN,
|
|
COLOUR_SALMON_PINK,
|
|
COLOUR_DARK_PURPLE,
|
|
COLOUR_LIGHT_PURPLE,
|
|
COLOUR_DARK_BLUE,
|
|
COLOUR_SATURATED_GREEN,
|
|
COLOUR_SATURATED_RED,
|
|
COLOUR_DARK_ORANGE,
|
|
COLOUR_BORDEAUX_RED,
|
|
COLOUR_WHITE,
|
|
COLOUR_BRIGHT_PURPLE,
|
|
COLOUR_LIGHT_BLUE,
|
|
COLOUR_TEAL,
|
|
COLOUR_DARK_GREEN,
|
|
COLOUR_MOSS_GREEN,
|
|
COLOUR_BRIGHT_GREEN,
|
|
COLOUR_OLIVE_GREEN,
|
|
COLOUR_DARK_OLIVE_GREEN,
|
|
COLOUR_YELLOW,
|
|
COLOUR_LIGHT_ORANGE,
|
|
COLOUR_BRIGHT_RED,
|
|
COLOUR_DARK_PINK,
|
|
COLOUR_BRIGHT_PINK,
|
|
};
|
|
// clang-format on
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069A05D
|
|
*/
|
|
Guest* Guest::Generate(const CoordsXYZ& coords)
|
|
{
|
|
if (GetNumFreeEntities() < 400)
|
|
return nullptr;
|
|
|
|
Guest* peep = CreateEntity<Guest>();
|
|
peep->SpriteType = PeepSpriteType::Normal;
|
|
peep->OutsideOfPark = true;
|
|
peep->State = PeepState::Falling;
|
|
peep->Action = PeepActionType::Walking;
|
|
peep->SpecialSprite = 0;
|
|
peep->ActionSpriteImageOffset = 0;
|
|
peep->WalkingFrameNum = 0;
|
|
peep->ActionSpriteType = PeepActionSpriteType::None;
|
|
peep->PeepFlags = 0;
|
|
peep->FavouriteRide = RIDE_ID_NULL;
|
|
peep->FavouriteRideRating = 0;
|
|
|
|
const rct_sprite_bounds* spriteBounds = &GetSpriteBounds(peep->SpriteType, peep->ActionSpriteType);
|
|
peep->sprite_width = spriteBounds->sprite_width;
|
|
peep->sprite_height_negative = spriteBounds->sprite_height_negative;
|
|
peep->sprite_height_positive = spriteBounds->sprite_height_positive;
|
|
|
|
peep->MoveTo(coords);
|
|
peep->sprite_direction = 0;
|
|
peep->Mass = (scenario_rand() & 0x1F) + 45;
|
|
peep->PathCheckOptimisation = 0;
|
|
peep->InteractionRideIndex = RIDE_ID_NULL;
|
|
peep->PreviousRide = RIDE_ID_NULL;
|
|
peep->Thoughts[0].type = PeepThoughtType::None;
|
|
peep->WindowInvalidateFlags = 0;
|
|
|
|
uint8_t intensityHighest = (scenario_rand() & 0x7) + 3;
|
|
uint8_t intensityLowest = std::min(intensityHighest, static_cast<uint8_t>(7)) - 3;
|
|
|
|
if (intensityHighest >= 7)
|
|
intensityHighest = 15;
|
|
|
|
/* Check which intensity boxes are enabled
|
|
* and apply the appropriate intensity settings. */
|
|
if (gParkFlags & PARK_FLAGS_PREF_LESS_INTENSE_RIDES)
|
|
{
|
|
if (gParkFlags & PARK_FLAGS_PREF_MORE_INTENSE_RIDES)
|
|
{
|
|
intensityLowest = 0;
|
|
intensityHighest = 15;
|
|
}
|
|
else
|
|
{
|
|
intensityLowest = 0;
|
|
intensityHighest = 4;
|
|
}
|
|
}
|
|
else if (gParkFlags & PARK_FLAGS_PREF_MORE_INTENSE_RIDES)
|
|
{
|
|
intensityLowest = 9;
|
|
intensityHighest = 15;
|
|
}
|
|
|
|
peep->Intensity = IntensityRange(intensityLowest, intensityHighest);
|
|
|
|
uint8_t nauseaTolerance = scenario_rand() & 0x7;
|
|
if (gParkFlags & PARK_FLAGS_PREF_MORE_INTENSE_RIDES)
|
|
{
|
|
nauseaTolerance += 4;
|
|
}
|
|
|
|
peep->NauseaTolerance = nausea_tolerance_distribution[nauseaTolerance];
|
|
|
|
/* Scenario editor limits initial guest happiness to between 37..253.
|
|
* To be on the safe side, assume the value could have been hacked
|
|
* to any value 0..255. */
|
|
peep->Happiness = gGuestInitialHappiness;
|
|
/* Assume a default initial happiness of 0 is wrong and set
|
|
* to 128 (50%) instead. */
|
|
if (gGuestInitialHappiness == 0)
|
|
peep->Happiness = 128;
|
|
/* Initial value will vary by -15..16 */
|
|
int8_t happinessDelta = (scenario_rand() & 0x1F) - 15;
|
|
/* Adjust by the delta, clamping at min=0 and max=255. */
|
|
peep->Happiness = std::clamp(peep->Happiness + happinessDelta, 0, PEEP_MAX_HAPPINESS);
|
|
peep->HappinessTarget = peep->Happiness;
|
|
peep->Nausea = 0;
|
|
peep->NauseaTarget = 0;
|
|
|
|
/* Scenario editor limits initial guest hunger to between 37..253.
|
|
* To be on the safe side, assume the value could have been hacked
|
|
* to any value 0..255. */
|
|
peep->Hunger = gGuestInitialHunger;
|
|
/* Initial value will vary by -15..16 */
|
|
int8_t hungerDelta = (scenario_rand() & 0x1F) - 15;
|
|
/* Adjust by the delta, clamping at min=0 and max=255. */
|
|
peep->Hunger = std::clamp(peep->Hunger + hungerDelta, 0, PEEP_MAX_HUNGER);
|
|
|
|
/* Scenario editor limits initial guest thirst to between 37..253.
|
|
* To be on the safe side, assume the value could have been hacked
|
|
* to any value 0..255. */
|
|
peep->Thirst = gGuestInitialThirst;
|
|
/* Initial value will vary by -15..16 */
|
|
int8_t thirstDelta = (scenario_rand() & 0x1F) - 15;
|
|
/* Adjust by the delta, clamping at min=0 and max=255. */
|
|
peep->Thirst = std::clamp(peep->Thirst + thirstDelta, 0, PEEP_MAX_THIRST);
|
|
|
|
peep->Toilet = 0;
|
|
peep->TimeToConsume = 0;
|
|
|
|
peep->GuestNumRides = 0;
|
|
peep->Id = gNextGuestNumber++;
|
|
peep->Name = nullptr;
|
|
|
|
money32 cash = (scenario_rand() & 0x3) * 100 - 100 + gGuestInitialCash;
|
|
if (cash < 0)
|
|
cash = 0;
|
|
|
|
if (gGuestInitialCash == 0)
|
|
{
|
|
cash = 500;
|
|
}
|
|
|
|
if (gParkFlags & PARK_FLAGS_NO_MONEY)
|
|
{
|
|
cash = 0;
|
|
}
|
|
|
|
if (gGuestInitialCash == MONEY16_UNDEFINED)
|
|
{
|
|
cash = 0;
|
|
}
|
|
|
|
peep->CashInPocket = cash;
|
|
peep->CashSpent = 0;
|
|
peep->ParkEntryTime = -1;
|
|
peep->ResetPathfindGoal();
|
|
peep->RemoveAllItems();
|
|
peep->GuestHeadingToRideId = RIDE_ID_NULL;
|
|
peep->LitterCount = 0;
|
|
peep->DisgustingCount = 0;
|
|
peep->VandalismSeen = 0;
|
|
peep->PaidToEnter = 0;
|
|
peep->PaidOnRides = 0;
|
|
peep->PaidOnFood = 0;
|
|
peep->PaidOnDrink = 0;
|
|
peep->PaidOnSouvenirs = 0;
|
|
peep->AmountOfFood = 0;
|
|
peep->AmountOfDrinks = 0;
|
|
peep->AmountOfSouvenirs = 0;
|
|
peep->SurroundingsThoughtTimeout = 0;
|
|
peep->Angriness = 0;
|
|
peep->TimeLost = 0;
|
|
|
|
uint8_t tshirtColour = static_cast<uint8_t>(scenario_rand() % std::size(tshirt_colours));
|
|
peep->TshirtColour = tshirt_colours[tshirtColour];
|
|
|
|
uint8_t trousersColour = static_cast<uint8_t>(scenario_rand() % std::size(trouser_colours));
|
|
peep->TrousersColour = trouser_colours[trousersColour];
|
|
|
|
/* Minimum energy is capped at 32 and maximum at 128, so this initialises
|
|
* a peep with approx 34%-100% energy. (65 - 32) / (128 - 32) ≈ 34% */
|
|
uint8_t energy = (scenario_rand() % 64) + 65;
|
|
peep->Energy = energy;
|
|
peep->EnergyTarget = energy;
|
|
|
|
increment_guests_heading_for_park();
|
|
|
|
#ifdef ENABLE_SCRIPTING
|
|
auto& hookEngine = OpenRCT2::GetContext()->GetScriptEngine().GetHookEngine();
|
|
if (hookEngine.HasSubscriptions(OpenRCT2::Scripting::HOOK_TYPE::GUEST_GENERATION))
|
|
{
|
|
auto ctx = OpenRCT2::GetContext()->GetScriptEngine().GetContext();
|
|
|
|
// Create event args object
|
|
auto obj = OpenRCT2::Scripting::DukObject(ctx);
|
|
obj.Set("id", peep->sprite_index);
|
|
|
|
// Call the subscriptions
|
|
auto e = obj.Take();
|
|
hookEngine.Call(OpenRCT2::Scripting::HOOK_TYPE::GUEST_GENERATION, e, true);
|
|
}
|
|
#endif
|
|
|
|
return peep;
|
|
}
|
|
|
|
enum
|
|
{
|
|
PEEP_FACE_OFFSET_ANGRY = 0,
|
|
PEEP_FACE_OFFSET_VERY_VERY_SICK,
|
|
PEEP_FACE_OFFSET_VERY_SICK,
|
|
PEEP_FACE_OFFSET_SICK,
|
|
PEEP_FACE_OFFSET_VERY_TIRED,
|
|
PEEP_FACE_OFFSET_TIRED,
|
|
PEEP_FACE_OFFSET_VERY_VERY_UNHAPPY,
|
|
PEEP_FACE_OFFSET_VERY_UNHAPPY,
|
|
PEEP_FACE_OFFSET_UNHAPPY,
|
|
PEEP_FACE_OFFSET_NORMAL,
|
|
PEEP_FACE_OFFSET_HAPPY,
|
|
PEEP_FACE_OFFSET_VERY_HAPPY,
|
|
PEEP_FACE_OFFSET_VERY_VERY_HAPPY,
|
|
};
|
|
|
|
static constexpr const int32_t face_sprite_small[] = {
|
|
SPR_PEEP_SMALL_FACE_ANGRY,
|
|
SPR_PEEP_SMALL_FACE_VERY_VERY_SICK,
|
|
SPR_PEEP_SMALL_FACE_VERY_SICK,
|
|
SPR_PEEP_SMALL_FACE_SICK,
|
|
SPR_PEEP_SMALL_FACE_VERY_TIRED,
|
|
SPR_PEEP_SMALL_FACE_TIRED,
|
|
SPR_PEEP_SMALL_FACE_VERY_VERY_UNHAPPY,
|
|
SPR_PEEP_SMALL_FACE_VERY_UNHAPPY,
|
|
SPR_PEEP_SMALL_FACE_UNHAPPY,
|
|
SPR_PEEP_SMALL_FACE_NORMAL,
|
|
SPR_PEEP_SMALL_FACE_HAPPY,
|
|
SPR_PEEP_SMALL_FACE_VERY_HAPPY,
|
|
SPR_PEEP_SMALL_FACE_VERY_VERY_HAPPY,
|
|
};
|
|
|
|
static constexpr const int32_t face_sprite_large[] = {
|
|
SPR_PEEP_LARGE_FACE_ANGRY_0,
|
|
SPR_PEEP_LARGE_FACE_VERY_VERY_SICK_0,
|
|
SPR_PEEP_LARGE_FACE_VERY_SICK_0,
|
|
SPR_PEEP_LARGE_FACE_SICK,
|
|
SPR_PEEP_LARGE_FACE_VERY_TIRED,
|
|
SPR_PEEP_LARGE_FACE_TIRED,
|
|
SPR_PEEP_LARGE_FACE_VERY_VERY_UNHAPPY,
|
|
SPR_PEEP_LARGE_FACE_VERY_UNHAPPY,
|
|
SPR_PEEP_LARGE_FACE_UNHAPPY,
|
|
SPR_PEEP_LARGE_FACE_NORMAL,
|
|
SPR_PEEP_LARGE_FACE_HAPPY,
|
|
SPR_PEEP_LARGE_FACE_VERY_HAPPY,
|
|
SPR_PEEP_LARGE_FACE_VERY_VERY_HAPPY,
|
|
};
|
|
|
|
static int32_t get_face_sprite_offset(Guest* peep)
|
|
{
|
|
// ANGRY
|
|
if (peep->Angriness > 0)
|
|
return PEEP_FACE_OFFSET_ANGRY;
|
|
|
|
// VERY_VERY_SICK
|
|
if (peep->Nausea > 200)
|
|
return PEEP_FACE_OFFSET_VERY_VERY_SICK;
|
|
|
|
// VERY_SICK
|
|
if (peep->Nausea > 170)
|
|
return PEEP_FACE_OFFSET_VERY_SICK;
|
|
|
|
// SICK
|
|
if (peep->Nausea > 140)
|
|
return PEEP_FACE_OFFSET_SICK;
|
|
|
|
// VERY_TIRED
|
|
if (peep->Energy < 46)
|
|
return PEEP_FACE_OFFSET_VERY_TIRED;
|
|
|
|
// TIRED
|
|
if (peep->Energy < 70)
|
|
return PEEP_FACE_OFFSET_TIRED;
|
|
|
|
int32_t offset = PEEP_FACE_OFFSET_VERY_VERY_UNHAPPY;
|
|
// There are 7 different happiness based faces
|
|
for (int32_t i = 37; peep->Happiness >= i; i += 37)
|
|
{
|
|
offset++;
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
/**
|
|
* Function split into large and small sprite
|
|
* rct2: 0x00698721
|
|
*/
|
|
int32_t get_peep_face_sprite_small(Guest* peep)
|
|
{
|
|
return face_sprite_small[get_face_sprite_offset(peep)];
|
|
}
|
|
|
|
/**
|
|
* Function split into large and small sprite
|
|
* rct2: 0x00698721
|
|
*/
|
|
int32_t get_peep_face_sprite_large(Guest* peep)
|
|
{
|
|
return face_sprite_large[get_face_sprite_offset(peep)];
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00693CBB
|
|
*/
|
|
bool Guest::UpdateQueuePosition(PeepActionType previous_action)
|
|
{
|
|
TimeInQueue++;
|
|
|
|
auto* guestNext = GetEntity<Guest>(GuestNextInQueue);
|
|
if (guestNext == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int16_t x_diff = abs(guestNext->x - x);
|
|
int16_t y_diff = abs(guestNext->y - y);
|
|
int16_t z_diff = abs(guestNext->z - z);
|
|
|
|
if (z_diff > 10)
|
|
return false;
|
|
|
|
if (x_diff < y_diff)
|
|
{
|
|
int16_t temp_x = x_diff;
|
|
x_diff = y_diff;
|
|
y_diff = temp_x;
|
|
}
|
|
|
|
x_diff += y_diff / 2;
|
|
if (x_diff > 7)
|
|
{
|
|
if (x_diff > 13)
|
|
{
|
|
if ((x & 0xFFE0) != (guestNext->x & 0xFFE0) || (y & 0xFFE0) != (guestNext->y & 0xFFE0))
|
|
return false;
|
|
}
|
|
|
|
if (sprite_direction != guestNext->sprite_direction)
|
|
return false;
|
|
|
|
switch (guestNext->sprite_direction / 8)
|
|
{
|
|
case 0:
|
|
if (x >= guestNext->x)
|
|
return false;
|
|
break;
|
|
case 1:
|
|
if (y <= guestNext->y)
|
|
return false;
|
|
break;
|
|
case 2:
|
|
if (x <= guestNext->x)
|
|
return false;
|
|
break;
|
|
case 3:
|
|
if (y >= guestNext->y)
|
|
return false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!IsActionInterruptable())
|
|
UpdateAction();
|
|
|
|
if (!IsActionWalking())
|
|
return true;
|
|
|
|
Action = PeepActionType::Idle;
|
|
NextActionSpriteType = PeepActionSpriteType::WatchRide;
|
|
if (previous_action != PeepActionType::Idle)
|
|
Invalidate();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006966A9
|
|
*/
|
|
void Guest::RemoveFromQueue()
|
|
{
|
|
auto ride = get_ride(CurrentRide);
|
|
if (ride == nullptr)
|
|
return;
|
|
|
|
auto& station = ride->stations[CurrentRideStation];
|
|
// Make sure we don't underflow, building while paused might reset it to 0 where peeps have
|
|
// not yet left the queue.
|
|
if (station.QueueLength > 0)
|
|
{
|
|
station.QueueLength--;
|
|
}
|
|
|
|
if (sprite_index == station.LastPeepInQueue)
|
|
{
|
|
station.LastPeepInQueue = GuestNextInQueue;
|
|
return;
|
|
}
|
|
|
|
auto* otherGuest = GetEntity<Guest>(station.LastPeepInQueue);
|
|
if (otherGuest == nullptr)
|
|
{
|
|
log_error("Invalid Guest Queue list!");
|
|
return;
|
|
}
|
|
for (; otherGuest != nullptr; otherGuest = GetEntity<Guest>(otherGuest->GuestNextInQueue))
|
|
{
|
|
if (sprite_index == otherGuest->GuestNextInQueue)
|
|
{
|
|
otherGuest->GuestNextInQueue = GuestNextInQueue;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
uint64_t Guest::GetItemFlags() const
|
|
{
|
|
return ItemFlags;
|
|
}
|
|
|
|
void Guest::SetItemFlags(uint64_t itemFlags)
|
|
{
|
|
ItemFlags = itemFlags;
|
|
}
|
|
|
|
void Guest::RemoveAllItems()
|
|
{
|
|
ItemFlags = 0;
|
|
}
|
|
|
|
void Guest::RemoveItem(ShopItem item)
|
|
{
|
|
ItemFlags &= ~EnumToFlag(item);
|
|
}
|
|
|
|
void Guest::GiveItem(ShopItem item)
|
|
{
|
|
ItemFlags |= EnumToFlag(item);
|
|
}
|
|
|
|
bool Guest::HasItem(ShopItem peepItem) const
|
|
{
|
|
return GetItemFlags() & EnumToFlag(peepItem);
|
|
}
|
|
|
|
static bool IsThoughtShopItemRelated(const PeepThoughtType type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case PeepThoughtType::AlreadyGot:
|
|
case PeepThoughtType::HaventFinished:
|
|
case PeepThoughtType::CantAffordItem:
|
|
return true;
|
|
default:
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Guest::RemoveRideFromMemory(ride_id_t rideId)
|
|
{
|
|
if (State == PeepState::Watching)
|
|
{
|
|
if (CurrentRide == rideId)
|
|
{
|
|
CurrentRide = RIDE_ID_NULL;
|
|
if (TimeToStand >= 50)
|
|
{
|
|
// make peep stop watching the ride
|
|
TimeToStand = 50;
|
|
}
|
|
}
|
|
}
|
|
|
|
// remove any free voucher for this ride from peep
|
|
if (HasItem(ShopItem::Voucher))
|
|
{
|
|
if (VoucherType == VOUCHER_TYPE_RIDE_FREE && VoucherRideId == rideId)
|
|
{
|
|
RemoveItem(ShopItem::Voucher);
|
|
}
|
|
}
|
|
|
|
// remove any photos of this ride from peep
|
|
if (HasItem(ShopItem::Photo))
|
|
{
|
|
if (Photo1RideRef == rideId)
|
|
{
|
|
RemoveItem(ShopItem::Photo);
|
|
}
|
|
}
|
|
if (HasItem(ShopItem::Photo2))
|
|
{
|
|
if (Photo2RideRef == rideId)
|
|
{
|
|
RemoveItem(ShopItem::Photo2);
|
|
}
|
|
}
|
|
if (HasItem(ShopItem::Photo3))
|
|
{
|
|
if (Photo3RideRef == rideId)
|
|
{
|
|
RemoveItem(ShopItem::Photo3);
|
|
}
|
|
}
|
|
if (HasItem(ShopItem::Photo4))
|
|
{
|
|
if (Photo4RideRef == rideId)
|
|
{
|
|
RemoveItem(ShopItem::Photo4);
|
|
}
|
|
}
|
|
|
|
if (GuestHeadingToRideId == rideId)
|
|
{
|
|
GuestHeadingToRideId = RIDE_ID_NULL;
|
|
}
|
|
if (FavouriteRide == rideId)
|
|
{
|
|
FavouriteRide = RIDE_ID_NULL;
|
|
}
|
|
|
|
// Erase all thoughts that contain the ride.
|
|
for (auto it = std::begin(Thoughts); it != std::end(Thoughts);)
|
|
{
|
|
const auto& entry = *it;
|
|
if (entry.type == PeepThoughtType::None)
|
|
break;
|
|
|
|
// Ride ids and shop item ids might have the same value, look only for ride thoughts.
|
|
if (IsThoughtShopItemRelated(entry.type) || entry.rideId != rideId)
|
|
{
|
|
it++;
|
|
continue;
|
|
}
|
|
|
|
if (auto itNext = std::next(it); itNext != std::end(Thoughts))
|
|
{
|
|
// Overwrite this entry by shifting all entries that follow.
|
|
std::rotate(it, itNext, std::end(Thoughts));
|
|
}
|
|
|
|
// Last slot is now free.
|
|
auto& lastEntry = Thoughts.back();
|
|
lastEntry.type = PeepThoughtType::None;
|
|
lastEntry.item = PeepThoughtItemNone;
|
|
}
|
|
}
|