mirror of
https://github.com/OpenRCT2/OpenRCT2
synced 2026-01-16 03:23:15 +01:00
2831 lines
84 KiB
C++
2831 lines
84 KiB
C++
/*****************************************************************************
|
|
* Copyright (c) 2014-2025 OpenRCT2 developers
|
|
*
|
|
* For a complete list of all authors, please refer to contributors.md
|
|
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
|
|
*
|
|
* OpenRCT2 is licensed under the GNU General Public License version 3.
|
|
*****************************************************************************/
|
|
|
|
#include "Peep.h"
|
|
|
|
#include "../Cheats.h"
|
|
#include "../Context.h"
|
|
#include "../Diagnostic.h"
|
|
#include "../Game.h"
|
|
#include "../GameState.h"
|
|
#include "../Input.h"
|
|
#include "../OpenRCT2.h"
|
|
#include "../SpriteIds.h"
|
|
#include "../actions/GameAction.h"
|
|
#include "../audio/Audio.h"
|
|
#include "../audio/AudioChannel.h"
|
|
#include "../audio/AudioMixer.h"
|
|
#include "../config/Config.h"
|
|
#include "../core/EnumUtils.hpp"
|
|
#include "../core/Guard.hpp"
|
|
#include "../core/String.hpp"
|
|
#include "../drawing/LightFX.h"
|
|
#include "../entity/Balloon.h"
|
|
#include "../entity/EntityList.h"
|
|
#include "../entity/EntityRegistry.h"
|
|
#include "../entity/EntityTweener.h"
|
|
#include "../interface/Viewport.h"
|
|
#include "../interface/WindowBase.h"
|
|
#include "../localisation/Formatter.h"
|
|
#include "../localisation/Formatting.h"
|
|
#include "../management/Finance.h"
|
|
#include "../management/Marketing.h"
|
|
#include "../management/NewsItem.h"
|
|
#include "../network/Network.h"
|
|
#include "../object/ObjectManager.h"
|
|
#include "../object/PeepAnimationsObject.h"
|
|
#include "../paint/Paint.h"
|
|
#include "../peep/GuestPathfinding.h"
|
|
#include "../peep/PeepSpriteIds.h"
|
|
#include "../profiling/Profiling.h"
|
|
#include "../ride/Ride.h"
|
|
#include "../ride/RideData.h"
|
|
#include "../ride/ShopItem.h"
|
|
#include "../ride/Station.h"
|
|
#include "../ride/Track.h"
|
|
#include "../scenario/Scenario.h"
|
|
#include "../ui/WindowManager.h"
|
|
#include "../util/Util.h"
|
|
#include "../windows/Intent.h"
|
|
#include "../world/Climate.h"
|
|
#include "../world/ConstructionClearance.h"
|
|
#include "../world/Entrance.h"
|
|
#include "../world/Footpath.h"
|
|
#include "../world/Map.h"
|
|
#include "../world/Park.h"
|
|
#include "../world/Scenery.h"
|
|
#include "../world/tile_element/EntranceElement.h"
|
|
#include "../world/tile_element/PathElement.h"
|
|
#include "../world/tile_element/SurfaceElement.h"
|
|
#include "../world/tile_element/TrackElement.h"
|
|
#include "PatrolArea.h"
|
|
#include "Staff.h"
|
|
|
|
#include <cassert>
|
|
#include <iterator>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <optional>
|
|
|
|
using namespace OpenRCT2;
|
|
using namespace OpenRCT2::Audio;
|
|
using namespace OpenRCT2::Drawing;
|
|
|
|
using OpenRCT2::Drawing::LightFx::LightType;
|
|
|
|
static uint8_t _backupAnimationImageIdOffset;
|
|
static TileElement* _peepRideEntranceExitElement;
|
|
|
|
static std::shared_ptr<IAudioChannel> _crowdSoundChannel = nullptr;
|
|
|
|
static void GuestReleaseBalloon(Guest* peep, int16_t spawn_height);
|
|
|
|
static PeepAnimationType PeepSpecialSpriteToAnimationGroupMap[] = {
|
|
PeepAnimationType::Walking,
|
|
PeepAnimationType::HoldMat,
|
|
PeepAnimationType::StaffMower,
|
|
};
|
|
|
|
static PeepAnimationType PeepActionToAnimationGroupMap[] = {
|
|
PeepAnimationType::CheckTime,
|
|
PeepAnimationType::EatFood,
|
|
PeepAnimationType::ShakeHead,
|
|
PeepAnimationType::EmptyPockets,
|
|
PeepAnimationType::SittingEatFood,
|
|
PeepAnimationType::SittingLookAroundLeft,
|
|
PeepAnimationType::SittingLookAroundRight,
|
|
PeepAnimationType::Wow,
|
|
PeepAnimationType::ThrowUp,
|
|
PeepAnimationType::Jump,
|
|
PeepAnimationType::StaffSweep,
|
|
PeepAnimationType::Drowning,
|
|
PeepAnimationType::StaffAnswerCall,
|
|
PeepAnimationType::StaffAnswerCall2,
|
|
PeepAnimationType::StaffCheckBoard,
|
|
PeepAnimationType::StaffFix,
|
|
PeepAnimationType::StaffFix2,
|
|
PeepAnimationType::StaffFixGround,
|
|
PeepAnimationType::StaffFix3,
|
|
PeepAnimationType::StaffWatering,
|
|
PeepAnimationType::Joy,
|
|
PeepAnimationType::ReadMap,
|
|
PeepAnimationType::Wave,
|
|
PeepAnimationType::StaffEmptyBin,
|
|
PeepAnimationType::Wave2,
|
|
PeepAnimationType::TakePhoto,
|
|
PeepAnimationType::Clap,
|
|
PeepAnimationType::Disgust,
|
|
PeepAnimationType::DrawPicture,
|
|
PeepAnimationType::BeingWatched,
|
|
PeepAnimationType::WithdrawMoney,
|
|
};
|
|
|
|
template<>
|
|
bool EntityBase::Is<Peep>() const
|
|
{
|
|
return Type == EntityType::Guest || Type == EntityType::Staff;
|
|
}
|
|
|
|
uint8_t Peep::GetNextDirection() const
|
|
{
|
|
return NextFlags & PEEP_NEXT_FLAG_DIRECTION_MASK;
|
|
}
|
|
|
|
bool Peep::GetNextIsSloped() const
|
|
{
|
|
return NextFlags & PEEP_NEXT_FLAG_IS_SLOPED;
|
|
}
|
|
|
|
bool Peep::GetNextIsSurface() const
|
|
{
|
|
return NextFlags & PEEP_NEXT_FLAG_IS_SURFACE;
|
|
}
|
|
|
|
void Peep::SetNextFlags(uint8_t next_direction, bool is_sloped, bool is_surface)
|
|
{
|
|
NextFlags = next_direction & PEEP_NEXT_FLAG_DIRECTION_MASK;
|
|
NextFlags |= is_sloped ? PEEP_NEXT_FLAG_IS_SLOPED : 0;
|
|
NextFlags |= is_surface ? PEEP_NEXT_FLAG_IS_SURFACE : 0;
|
|
}
|
|
|
|
bool Peep::CanBePickedUp() const
|
|
{
|
|
switch (State)
|
|
{
|
|
case PeepState::one:
|
|
case PeepState::queuingFront:
|
|
case PeepState::onRide:
|
|
case PeepState::enteringRide:
|
|
case PeepState::leavingRide:
|
|
case PeepState::enteringPark:
|
|
case PeepState::leavingPark:
|
|
case PeepState::fixing:
|
|
case PeepState::buying:
|
|
case PeepState::inspecting:
|
|
return false;
|
|
case PeepState::falling:
|
|
case PeepState::walking:
|
|
case PeepState::queuing:
|
|
case PeepState::sitting:
|
|
case PeepState::picked:
|
|
case PeepState::patrolling:
|
|
case PeepState::mowing:
|
|
case PeepState::sweeping:
|
|
case PeepState::answering:
|
|
case PeepState::watching:
|
|
case PeepState::emptyingBin:
|
|
case PeepState::usingBin:
|
|
case PeepState::watering:
|
|
case PeepState::headingToInspection:
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
int32_t PeepGetStaffCount()
|
|
{
|
|
return getGameState().entities.GetEntityListCount(EntityType::Staff);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0068F0A9
|
|
*/
|
|
void PeepUpdateAll()
|
|
{
|
|
PROFILED_FUNCTION();
|
|
|
|
if (isInEditorMode())
|
|
return;
|
|
|
|
const auto currentTicks = getGameState().currentTicks;
|
|
|
|
constexpr auto kTicks128Mask = 128u - 1u;
|
|
const auto currentTicksMasked = currentTicks & kTicks128Mask;
|
|
|
|
uint32_t index = 0;
|
|
// Warning this loop can delete peeps
|
|
for (auto peep : EntityList<Guest>())
|
|
{
|
|
if ((index & kTicks128Mask) == currentTicksMasked)
|
|
{
|
|
peep->Tick128UpdateGuest(index);
|
|
}
|
|
|
|
// 128 tick can delete so double check its not deleted
|
|
if (peep->Type == EntityType::Guest)
|
|
{
|
|
peep->Update();
|
|
}
|
|
|
|
index++;
|
|
}
|
|
|
|
for (auto staff : EntityList<Staff>())
|
|
{
|
|
if ((index & kTicks128Mask) == currentTicksMasked)
|
|
{
|
|
staff->Tick128UpdateStaff();
|
|
}
|
|
|
|
// 128 tick can delete so double check its not deleted
|
|
if (staff->Type == EntityType::Staff)
|
|
{
|
|
staff->Update();
|
|
}
|
|
|
|
index++;
|
|
}
|
|
}
|
|
|
|
void PeepUpdateAllBoundingBoxes()
|
|
{
|
|
for (auto* peep : EntityList<Guest>())
|
|
{
|
|
peep->UpdateSpriteBoundingBox();
|
|
}
|
|
|
|
for (auto* peep : EntityList<Staff>())
|
|
{
|
|
peep->UpdateSpriteBoundingBox();
|
|
}
|
|
}
|
|
|
|
/*
|
|
* rct2: 0x68F3AE
|
|
* Set peep state to falling if path below has gone missing, return true if current path is valid, false if peep starts falling.
|
|
*/
|
|
bool Peep::CheckForPath()
|
|
{
|
|
PROFILED_FUNCTION();
|
|
|
|
PathCheckOptimisation++;
|
|
if ((PathCheckOptimisation & 0xF) != (Id.ToUnderlying() & 0xF))
|
|
{
|
|
// This condition makes the check happen less often
|
|
// As a side effect peeps hover for a short,
|
|
// random time when a path below them has been deleted
|
|
return true;
|
|
}
|
|
|
|
TileElement* tile_element = MapGetFirstElementAt(NextLoc);
|
|
|
|
auto mapType = TileElementType::Path;
|
|
if (GetNextIsSurface())
|
|
{
|
|
mapType = TileElementType::Surface;
|
|
}
|
|
|
|
do
|
|
{
|
|
if (tile_element == nullptr)
|
|
break;
|
|
if (tile_element->GetType() == mapType)
|
|
{
|
|
if (NextLoc.z == tile_element->GetBaseZ())
|
|
{
|
|
// Found a suitable path or surface
|
|
return true;
|
|
}
|
|
}
|
|
} while (!(tile_element++)->IsLastForTile());
|
|
|
|
// Found no suitable path
|
|
SetState(PeepState::falling);
|
|
return false;
|
|
}
|
|
|
|
bool Peep::ShouldWaitForLevelCrossing() const
|
|
{
|
|
if (IsOnPathBlockedByVehicle())
|
|
{
|
|
// Try to get out of the way
|
|
return false;
|
|
}
|
|
|
|
auto curPos = TileCoordsXYZ(GetLocation());
|
|
auto dstPos = TileCoordsXYZ(CoordsXYZ{ GetDestination(), NextLoc.z });
|
|
if ((curPos.x != dstPos.x || curPos.y != dstPos.y) && FootpathIsBlockedByVehicle(dstPos))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool Peep::IsOnLevelCrossing() const
|
|
{
|
|
auto loc = GetLocation();
|
|
auto pathElement = MapGetFootpathElement(loc);
|
|
if (pathElement != nullptr)
|
|
{
|
|
return pathElement->AsPath()->IsLevelCrossing(loc);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Peep::IsOnPathBlockedByVehicle() const
|
|
{
|
|
auto curPos = TileCoordsXYZ(GetLocation());
|
|
return FootpathIsBlockedByVehicle(curPos);
|
|
}
|
|
|
|
PeepAnimationType Peep::GetAnimationType()
|
|
{
|
|
if (IsActionInterruptable())
|
|
{ // PeepActionType::none1 or PeepActionType::none2
|
|
return PeepSpecialSpriteToAnimationGroupMap[SpecialSprite];
|
|
}
|
|
|
|
if (EnumValue(Action) < std::size(PeepActionToAnimationGroupMap))
|
|
{
|
|
return PeepActionToAnimationGroupMap[EnumValue(Action)];
|
|
}
|
|
|
|
Guard::Assert(
|
|
EnumValue(Action) >= std::size(PeepActionToAnimationGroupMap) && Action < PeepActionType::idle,
|
|
"Invalid peep action %u", EnumValue(Action));
|
|
return PeepAnimationType::Walking;
|
|
}
|
|
|
|
/*
|
|
* rct2: 0x00693B58
|
|
*/
|
|
void Peep::UpdateCurrentAnimationType()
|
|
{
|
|
PeepAnimationType newAnimationType = GetAnimationType();
|
|
if (AnimationType == newAnimationType)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AnimationType = newAnimationType;
|
|
|
|
Invalidate();
|
|
UpdateSpriteBoundingBox();
|
|
Invalidate();
|
|
}
|
|
|
|
void Peep::UpdateSpriteBoundingBox()
|
|
{
|
|
auto& objManager = GetContext()->GetObjectManager();
|
|
auto* animObj = objManager.GetLoadedObject<PeepAnimationsObject>(AnimationObjectIndex);
|
|
|
|
const auto& spriteBounds = animObj->GetSpriteBounds(AnimationGroup, AnimationType);
|
|
SpriteData.Width = spriteBounds.spriteWidth;
|
|
SpriteData.HeightMin = spriteBounds.spriteHeightNegative;
|
|
SpriteData.HeightMax = spriteBounds.spriteHeightPositive;
|
|
}
|
|
|
|
/* rct2: 0x00693BE5 */
|
|
void Peep::SwitchToSpecialSprite(uint8_t special_sprite_id)
|
|
{
|
|
if (special_sprite_id == SpecialSprite)
|
|
return;
|
|
|
|
SpecialSprite = special_sprite_id;
|
|
|
|
if (IsActionInterruptable())
|
|
{
|
|
AnimationImageIdOffset = 0;
|
|
}
|
|
UpdateCurrentAnimationType();
|
|
}
|
|
|
|
void Peep::StateReset()
|
|
{
|
|
SetState(PeepState::one);
|
|
SwitchToSpecialSprite(0);
|
|
}
|
|
|
|
/** rct2: 0x00981D7C, 0x00981D7E */
|
|
static constexpr CoordsXY kWalkingOffsetByDirection[kNumOrthogonalDirections] = {
|
|
{ -2, 0 },
|
|
{ 0, 2 },
|
|
{ 2, 0 },
|
|
{ 0, -2 },
|
|
};
|
|
|
|
std::optional<CoordsXY> Peep::UpdateAction()
|
|
{
|
|
int16_t xy_distance;
|
|
return UpdateAction(xy_distance);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x6939EB
|
|
* Also used to move peeps to the correct position to
|
|
* start an action. Returns true if the correct destination
|
|
* has not yet been reached. xy_distance is how close the
|
|
* peep is to the target.
|
|
*/
|
|
std::optional<CoordsXY> Peep::UpdateAction(int16_t& xy_distance)
|
|
{
|
|
PROFILED_FUNCTION();
|
|
|
|
_backupAnimationImageIdOffset = AnimationImageIdOffset;
|
|
if (Action == PeepActionType::idle)
|
|
{
|
|
Action = PeepActionType::walking;
|
|
}
|
|
|
|
CoordsXY differenceLoc = GetLocation();
|
|
differenceLoc -= GetDestination();
|
|
|
|
int32_t x_delta = abs(differenceLoc.x);
|
|
int32_t y_delta = abs(differenceLoc.y);
|
|
|
|
xy_distance = x_delta + y_delta;
|
|
|
|
// We're taking an easier route if we're just walking
|
|
if (IsActionWalking())
|
|
{
|
|
return UpdateWalkingAction(differenceLoc, xy_distance);
|
|
}
|
|
|
|
if (!UpdateActionAnimation())
|
|
{
|
|
AnimationImageIdOffset = 0;
|
|
Action = PeepActionType::walking;
|
|
UpdateCurrentAnimationType();
|
|
return { { x, y } };
|
|
}
|
|
|
|
// Should we throw up, and are we at the frame where sick appears?
|
|
if (auto* guest = As<Guest>(); guest != nullptr)
|
|
{
|
|
if (Action == PeepActionType::throwUp && AnimationFrameNum == 15)
|
|
{
|
|
guest->ThrowUp();
|
|
}
|
|
}
|
|
|
|
return { { x, y } };
|
|
}
|
|
|
|
bool Peep::UpdateActionAnimation()
|
|
{
|
|
auto& objManager = GetContext()->GetObjectManager();
|
|
auto* animObj = objManager.GetLoadedObject<PeepAnimationsObject>(AnimationObjectIndex);
|
|
|
|
const PeepAnimation& peepAnimation = animObj->GetPeepAnimation(AnimationGroup, AnimationType);
|
|
AnimationFrameNum++;
|
|
|
|
// If last frame of action
|
|
if (AnimationFrameNum >= peepAnimation.frameOffsets.size())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
AnimationImageIdOffset = peepAnimation.frameOffsets[AnimationFrameNum];
|
|
return true;
|
|
}
|
|
|
|
std::optional<CoordsXY> Peep::UpdateWalkingAction(const CoordsXY& differenceLoc, int16_t& xy_distance)
|
|
{
|
|
if (!IsActionWalking())
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
|
|
if (xy_distance <= DestinationTolerance)
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
|
|
int32_t x_delta = abs(differenceLoc.x);
|
|
int32_t y_delta = abs(differenceLoc.y);
|
|
|
|
int32_t nextDirection = 0;
|
|
if (x_delta < y_delta)
|
|
{
|
|
nextDirection = 1;
|
|
if (differenceLoc.y >= 0)
|
|
{
|
|
nextDirection = 3;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
nextDirection = 2;
|
|
if (differenceLoc.x >= 0)
|
|
{
|
|
nextDirection = 0;
|
|
}
|
|
}
|
|
|
|
Orientation = nextDirection * 8;
|
|
|
|
CoordsXY loc = { x, y };
|
|
loc += kWalkingOffsetByDirection[nextDirection];
|
|
|
|
UpdateWalkingAnimation();
|
|
|
|
return loc;
|
|
}
|
|
|
|
void Peep::UpdateWalkingAnimation()
|
|
{
|
|
auto& objManager = GetContext()->GetObjectManager();
|
|
auto* animObj = objManager.GetLoadedObject<PeepAnimationsObject>(AnimationObjectIndex);
|
|
|
|
WalkingAnimationFrameNum++;
|
|
const PeepAnimation& peepAnimation = animObj->GetPeepAnimation(AnimationGroup, AnimationType);
|
|
if (WalkingAnimationFrameNum >= peepAnimation.frameOffsets.size())
|
|
{
|
|
WalkingAnimationFrameNum = 0;
|
|
}
|
|
AnimationImageIdOffset = peepAnimation.frameOffsets[WalkingAnimationFrameNum];
|
|
}
|
|
|
|
/**
|
|
* rct2: 0x0069A409
|
|
* Decreases rider count if on/entering a ride.
|
|
*/
|
|
void PeepDecrementNumRiders(Peep* peep)
|
|
{
|
|
if (peep->State == PeepState::onRide || peep->State == PeepState::enteringRide)
|
|
{
|
|
auto ride = GetRide(peep->CurrentRide);
|
|
if (ride != nullptr)
|
|
{
|
|
ride->numRiders = std::max(0, ride->numRiders - 1);
|
|
ride->windowInvalidateFlags |= RIDE_INVALIDATE_RIDE_MAIN | RIDE_INVALIDATE_RIDE_LIST;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Call after changing a peeps state to insure that all relevant windows update.
|
|
* Note also increase ride count if on/entering a ride.
|
|
* rct2: 0x0069A42F
|
|
*/
|
|
void PeepWindowStateUpdate(Peep* peep)
|
|
{
|
|
auto* windowMgr = Ui::GetWindowManager();
|
|
WindowBase* w = windowMgr->FindByNumber(WindowClass::peep, peep->Id.ToUnderlying());
|
|
if (w != nullptr)
|
|
w->onPrepareDraw();
|
|
|
|
if (peep->Is<Guest>())
|
|
{
|
|
if (peep->State == PeepState::onRide || peep->State == PeepState::enteringRide)
|
|
{
|
|
auto ride = GetRide(peep->CurrentRide);
|
|
if (ride != nullptr)
|
|
{
|
|
ride->numRiders++;
|
|
ride->windowInvalidateFlags |= RIDE_INVALIDATE_RIDE_MAIN | RIDE_INVALIDATE_RIDE_LIST;
|
|
}
|
|
}
|
|
|
|
windowMgr->InvalidateByNumber(WindowClass::peep, peep->Id);
|
|
windowMgr->InvalidateByClass(WindowClass::guestList);
|
|
}
|
|
else
|
|
{
|
|
windowMgr->InvalidateByNumber(WindowClass::peep, peep->Id);
|
|
windowMgr->InvalidateByClass(WindowClass::staffList);
|
|
}
|
|
}
|
|
|
|
void Peep::Pickup()
|
|
{
|
|
if (auto* guest = As<Guest>(); guest != nullptr)
|
|
{
|
|
guest->RemoveFromRide();
|
|
}
|
|
MoveTo({ kLocationNull, y, z });
|
|
SetState(PeepState::picked);
|
|
SubState = 0;
|
|
}
|
|
|
|
void Peep::PickupAbort(int32_t old_x)
|
|
{
|
|
if (State != PeepState::picked)
|
|
return;
|
|
|
|
MoveTo({ old_x, y, z + 8 });
|
|
|
|
if (x != kLocationNull)
|
|
{
|
|
SetState(PeepState::falling);
|
|
Action = PeepActionType::walking;
|
|
SpecialSprite = 0;
|
|
AnimationImageIdOffset = 0;
|
|
AnimationType = PeepAnimationType::Walking;
|
|
PathCheckOptimisation = 0;
|
|
}
|
|
|
|
gPickupPeepImage = ImageId();
|
|
}
|
|
|
|
// Returns GameActions::Status::OK when a peep can be dropped at the given location. When apply is set to true the peep gets
|
|
// dropped.
|
|
GameActions::Result Peep::Place(const TileCoordsXYZ& location, bool apply)
|
|
{
|
|
auto* pathElement = MapGetPathElementAt(location);
|
|
TileElement* tileElement = reinterpret_cast<TileElement*>(pathElement);
|
|
if (pathElement == nullptr)
|
|
{
|
|
tileElement = reinterpret_cast<TileElement*>(MapGetSurfaceElementAt(location));
|
|
}
|
|
if (tileElement == nullptr)
|
|
{
|
|
return GameActions::Result(GameActions::Status::InvalidParameters, STR_ERR_CANT_PLACE_PERSON_HERE, kStringIdNone);
|
|
}
|
|
|
|
// Set the coordinate of destination to be exactly
|
|
// in the middle of a tile.
|
|
CoordsXYZ destination = { location.ToCoordsXY().ToTileCentre(), tileElement->GetBaseZ() + 16 };
|
|
|
|
if (!MapIsLocationOwned(destination))
|
|
{
|
|
return GameActions::Result(GameActions::Status::NotOwned, STR_ERR_CANT_PLACE_PERSON_HERE, STR_LAND_NOT_OWNED_BY_PARK);
|
|
}
|
|
|
|
if (auto res = MapCanConstructAt({ destination, destination.z, destination.z + (1 * 8) }, { 0b1111, 0 });
|
|
res.Error != GameActions::Status::Ok)
|
|
{
|
|
const auto stringId = std::get<StringId>(res.ErrorMessage);
|
|
if (stringId != STR_RAISE_OR_LOWER_LAND_FIRST && stringId != STR_FOOTPATH_IN_THE_WAY)
|
|
{
|
|
return GameActions::Result(
|
|
GameActions::Status::NoClearance, STR_ERR_CANT_PLACE_PERSON_HERE, stringId, res.ErrorMessageArgs.data());
|
|
}
|
|
}
|
|
|
|
if (apply)
|
|
{
|
|
MoveTo(destination);
|
|
SetState(PeepState::falling);
|
|
Action = PeepActionType::walking;
|
|
SpecialSprite = 0;
|
|
AnimationImageIdOffset = 0;
|
|
AnimationType = PeepAnimationType::Walking;
|
|
PathCheckOptimisation = 0;
|
|
EntityTweener::Get().Reset();
|
|
if (auto* guest = As<Guest>(); guest != nullptr)
|
|
{
|
|
AnimationType = PeepAnimationType::Invalid;
|
|
guest->HappinessTarget = std::max(guest->HappinessTarget - 10, 0);
|
|
UpdateCurrentAnimationType();
|
|
}
|
|
}
|
|
|
|
return GameActions::Result();
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069A535
|
|
*/
|
|
void PeepEntityRemove(Peep* peep)
|
|
{
|
|
auto* guest = peep->As<Guest>();
|
|
if (guest != nullptr)
|
|
{
|
|
guest->RemoveFromRide();
|
|
}
|
|
peep->Invalidate();
|
|
|
|
auto* windowMgr = Ui::GetWindowManager();
|
|
windowMgr->CloseByNumber(WindowClass::peep, peep->Id);
|
|
windowMgr->CloseByNumber(WindowClass::firePrompt, EnumValue(peep->Type));
|
|
|
|
auto* staff = peep->As<Staff>();
|
|
// Needed for invalidations after sprite removal
|
|
bool wasGuest = staff == nullptr;
|
|
if (wasGuest)
|
|
{
|
|
News::DisableNewsItems(News::ItemType::peepOnRide, peep->Id.ToUnderlying());
|
|
}
|
|
else
|
|
{
|
|
staff->ClearPatrolArea();
|
|
UpdateConsolidatedPatrolAreas();
|
|
|
|
News::DisableNewsItems(News::ItemType::peep, staff->Id.ToUnderlying());
|
|
}
|
|
getGameState().entities.EntityRemove(peep);
|
|
|
|
auto intent = Intent(wasGuest ? INTENT_ACTION_REFRESH_GUEST_LIST : INTENT_ACTION_REFRESH_STAFF_LIST);
|
|
ContextBroadcastIntent(&intent);
|
|
}
|
|
|
|
/**
|
|
* New function removes peep from park existence. Works with staff.
|
|
*/
|
|
void Peep::Remove()
|
|
{
|
|
auto* guest = As<Guest>();
|
|
if (guest != nullptr)
|
|
{
|
|
if (!guest->OutsideOfPark)
|
|
{
|
|
DecrementGuestsInPark();
|
|
auto intent = Intent(INTENT_ACTION_UPDATE_GUEST_COUNT);
|
|
ContextBroadcastIntent(&intent);
|
|
}
|
|
if (State == PeepState::enteringPark)
|
|
{
|
|
DecrementGuestsHeadingForPark();
|
|
}
|
|
}
|
|
PeepEntityRemove(this);
|
|
}
|
|
|
|
/**
|
|
* Falling and its subset drowning
|
|
* rct2: 0x690028
|
|
*/
|
|
void Peep::UpdateFalling()
|
|
{
|
|
if (Action == PeepActionType::drowning)
|
|
{
|
|
// Check to see if we are ready to drown.
|
|
UpdateAction();
|
|
Invalidate();
|
|
if (Action == PeepActionType::drowning)
|
|
return;
|
|
|
|
if (Config::Get().notifications.GuestDied)
|
|
{
|
|
auto ft = Formatter();
|
|
FormatNameTo(ft);
|
|
News::AddItemToQueue(News::ItemType::blank, STR_NEWS_ITEM_GUEST_DROWNED, x | (y << 16), ft);
|
|
}
|
|
|
|
auto& gameState = getGameState();
|
|
gameState.park.ratingCasualtyPenalty = std::min(gameState.park.ratingCasualtyPenalty + 25, 1000);
|
|
Remove();
|
|
return;
|
|
}
|
|
|
|
// If not drowning then falling. Note: peeps 'fall' after leaving a ride/enter the park.
|
|
TileElement* tile_element = MapGetFirstElementAt(CoordsXY{ x, y });
|
|
TileElement* saved_map = nullptr;
|
|
int32_t saved_height = 0;
|
|
|
|
if (tile_element != nullptr)
|
|
{
|
|
do
|
|
{
|
|
// If a path check if we are on it
|
|
if (tile_element->GetType() == TileElementType::Path)
|
|
{
|
|
int32_t height = MapHeightFromSlope(
|
|
{ x, y }, tile_element->AsPath()->GetSlopeDirection(), tile_element->AsPath()->IsSloped())
|
|
+ tile_element->GetBaseZ();
|
|
|
|
if (height < z - 1 || height > z + 8)
|
|
continue;
|
|
|
|
saved_height = height;
|
|
saved_map = tile_element;
|
|
break;
|
|
} // If a surface get the height and see if we are on it
|
|
else if (tile_element->GetType() == TileElementType::Surface)
|
|
{
|
|
// If the surface is water check to see if we could be drowning
|
|
if (tile_element->AsSurface()->GetWaterHeight() > 0)
|
|
{
|
|
int32_t height = tile_element->AsSurface()->GetWaterHeight();
|
|
|
|
if (height - 4 >= z && height < z + 20)
|
|
{
|
|
// Looks like we are drowning!
|
|
MoveTo({ x, y, height });
|
|
|
|
if (auto* guest = As<Guest>(); guest != nullptr)
|
|
{
|
|
// Drop balloon if held
|
|
GuestReleaseBalloon(guest, height);
|
|
guest->InsertNewThought(PeepThoughtType::Drowning);
|
|
}
|
|
|
|
Action = PeepActionType::drowning;
|
|
AnimationFrameNum = 0;
|
|
AnimationImageIdOffset = 0;
|
|
|
|
UpdateCurrentAnimationType();
|
|
PeepWindowStateUpdate(this);
|
|
return;
|
|
}
|
|
}
|
|
int32_t map_height = TileElementHeight({ x, y });
|
|
if (map_height < z || map_height - 4 > z)
|
|
continue;
|
|
saved_height = map_height;
|
|
saved_map = tile_element;
|
|
} // If not a path or surface go see next element
|
|
else
|
|
continue;
|
|
} while (!(tile_element++)->IsLastForTile());
|
|
}
|
|
|
|
// This will be null if peep is falling
|
|
if (saved_map == nullptr)
|
|
{
|
|
if (z <= 1)
|
|
{
|
|
// Remove peep if it has gone to the void
|
|
Remove();
|
|
return;
|
|
}
|
|
MoveTo({ x, y, z - 2 });
|
|
return;
|
|
}
|
|
|
|
MoveTo({ x, y, saved_height });
|
|
|
|
NextLoc = { CoordsXY{ x, y }.ToTileStart(), saved_map->GetBaseZ() };
|
|
|
|
if (saved_map->GetType() != TileElementType::Path)
|
|
{
|
|
SetNextFlags(0, false, true);
|
|
}
|
|
else
|
|
{
|
|
SetNextFlags(saved_map->AsPath()->GetSlopeDirection(), saved_map->AsPath()->IsSloped(), false);
|
|
}
|
|
SetState(PeepState::one);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x6902A2
|
|
*/
|
|
void Peep::Update1()
|
|
{
|
|
if (!CheckForPath())
|
|
return;
|
|
|
|
if (Is<Guest>())
|
|
{
|
|
SetState(PeepState::walking);
|
|
}
|
|
else
|
|
{
|
|
SetState(PeepState::patrolling);
|
|
}
|
|
|
|
SetDestination(GetLocation(), 10);
|
|
PeepDirection = Orientation >> 3;
|
|
}
|
|
|
|
void Peep::SetState(PeepState new_state)
|
|
{
|
|
PeepDecrementNumRiders(this);
|
|
State = new_state;
|
|
PeepWindowStateUpdate(this);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x690009
|
|
*/
|
|
void Peep::UpdatePicked()
|
|
{
|
|
if (getGameState().currentTicks & 0x1F)
|
|
return;
|
|
SubState++;
|
|
auto* guest = As<Guest>();
|
|
if (SubState == 13 && guest != nullptr)
|
|
{
|
|
guest->InsertNewThought(PeepThoughtType::Help);
|
|
}
|
|
}
|
|
|
|
uint32_t Peep::GetStepsToTake() const
|
|
{
|
|
uint32_t stepsToTake = Energy;
|
|
if (stepsToTake < 95 && State == PeepState::queuing)
|
|
stepsToTake = 95;
|
|
if ((PeepFlags & PEEP_FLAGS_SLOW_WALK) && State != PeepState::queuing)
|
|
stepsToTake /= 2;
|
|
if (IsActionWalking() && GetNextIsSloped())
|
|
{
|
|
stepsToTake /= 2;
|
|
if (State == PeepState::queuing)
|
|
stepsToTake += stepsToTake / 2;
|
|
}
|
|
// Ensure guests make it across a level crossing in time
|
|
constexpr auto minStepsForCrossing = 55;
|
|
if (stepsToTake < minStepsForCrossing && IsOnPathBlockedByVehicle())
|
|
stepsToTake = minStepsForCrossing;
|
|
|
|
return stepsToTake;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069BF41
|
|
*/
|
|
void PeepProblemWarningsUpdate()
|
|
{
|
|
auto& gameState = getGameState();
|
|
|
|
Ride* ride;
|
|
uint32_t hungerCounter = 0, lostCounter = 0, noexitCounter = 0, thirstCounter = 0, litterCounter = 0, disgustCounter = 0,
|
|
toiletCounter = 0, vandalismCounter = 0;
|
|
uint8_t* warningThrottle = gameState.park.peepWarningThrottle;
|
|
|
|
int32_t inQueueCounter = 0;
|
|
int32_t tooLongQueueCounter = 0;
|
|
std::map<RideId, int32_t> queueComplainingGuestsMap;
|
|
|
|
for (auto peep : EntityList<Guest>())
|
|
{
|
|
if (peep->OutsideOfPark)
|
|
continue;
|
|
|
|
if (peep->State == PeepState::queuing || peep->State == PeepState::queuingFront)
|
|
inQueueCounter++;
|
|
|
|
if (peep->Thoughts[0].freshness > 5)
|
|
continue;
|
|
|
|
switch (peep->Thoughts[0].type)
|
|
{
|
|
case PeepThoughtType::Lost: // 0x10
|
|
lostCounter++;
|
|
break;
|
|
|
|
case PeepThoughtType::Hungry: // 0x14
|
|
if (peep->GuestHeadingToRideId.IsNull())
|
|
{
|
|
hungerCounter++;
|
|
break;
|
|
}
|
|
ride = GetRide(peep->GuestHeadingToRideId);
|
|
if (ride != nullptr && !ride->getRideTypeDescriptor().HasFlag(RtdFlag::sellsFood))
|
|
hungerCounter++;
|
|
break;
|
|
|
|
case PeepThoughtType::Thirsty:
|
|
if (peep->GuestHeadingToRideId.IsNull())
|
|
{
|
|
thirstCounter++;
|
|
break;
|
|
}
|
|
ride = GetRide(peep->GuestHeadingToRideId);
|
|
if (ride != nullptr && !ride->getRideTypeDescriptor().HasFlag(RtdFlag::sellsDrinks))
|
|
thirstCounter++;
|
|
break;
|
|
|
|
case PeepThoughtType::Toilet:
|
|
if (peep->GuestHeadingToRideId.IsNull())
|
|
{
|
|
toiletCounter++;
|
|
break;
|
|
}
|
|
ride = GetRide(peep->GuestHeadingToRideId);
|
|
if (ride != nullptr && ride->getRideTypeDescriptor().specialType != RtdSpecialType::toilet)
|
|
toiletCounter++;
|
|
break;
|
|
|
|
case PeepThoughtType::BadLitter: // 0x1a
|
|
litterCounter++;
|
|
break;
|
|
case PeepThoughtType::CantFindExit: // 0x1b
|
|
noexitCounter++;
|
|
break;
|
|
case PeepThoughtType::PathDisgusting: // 0x1f
|
|
disgustCounter++;
|
|
break;
|
|
case PeepThoughtType::Vandalism: // 0x21
|
|
vandalismCounter++;
|
|
break;
|
|
case PeepThoughtType::QueuingAges:
|
|
tooLongQueueCounter++;
|
|
queueComplainingGuestsMap[peep->Thoughts[0].rideId]++;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// could maybe be packed into a loop, would lose a lot of clarity though
|
|
if (warningThrottle[0])
|
|
--warningThrottle[0];
|
|
else if (hungerCounter >= kPeepHungerWarningThreshold && hungerCounter >= gameState.park.numGuestsInPark / 16)
|
|
{
|
|
warningThrottle[0] = 4;
|
|
if (Config::Get().notifications.GuestWarnings)
|
|
{
|
|
constexpr auto thoughtId = static_cast<uint32_t>(PeepThoughtType::Hungry);
|
|
News::AddItemToQueue(News::ItemType::peeps, STR_PEEPS_ARE_HUNGRY, thoughtId, {});
|
|
}
|
|
}
|
|
|
|
if (warningThrottle[1])
|
|
--warningThrottle[1];
|
|
else if (thirstCounter >= kPeepThirstWarningThreshold && thirstCounter >= gameState.park.numGuestsInPark / 16)
|
|
{
|
|
warningThrottle[1] = 4;
|
|
if (Config::Get().notifications.GuestWarnings)
|
|
{
|
|
constexpr auto thoughtId = static_cast<uint32_t>(PeepThoughtType::Thirsty);
|
|
News::AddItemToQueue(News::ItemType::peeps, STR_PEEPS_ARE_THIRSTY, thoughtId, {});
|
|
}
|
|
}
|
|
|
|
if (warningThrottle[2])
|
|
--warningThrottle[2];
|
|
else if (toiletCounter >= kPeepToiletWarningThreshold && toiletCounter >= gameState.park.numGuestsInPark / 16)
|
|
{
|
|
warningThrottle[2] = 4;
|
|
if (Config::Get().notifications.GuestWarnings)
|
|
{
|
|
constexpr auto thoughtId = static_cast<uint32_t>(PeepThoughtType::Toilet);
|
|
News::AddItemToQueue(News::ItemType::peeps, STR_PEEPS_CANT_FIND_TOILET, thoughtId, {});
|
|
}
|
|
}
|
|
|
|
if (warningThrottle[3])
|
|
--warningThrottle[3];
|
|
else if (litterCounter >= kPeepLitterWarningThreshold && litterCounter >= gameState.park.numGuestsInPark / 32)
|
|
{
|
|
warningThrottle[3] = 4;
|
|
if (Config::Get().notifications.GuestWarnings)
|
|
{
|
|
constexpr auto thoughtId = static_cast<uint32_t>(PeepThoughtType::BadLitter);
|
|
News::AddItemToQueue(News::ItemType::peeps, STR_PEEPS_DISLIKE_LITTER, thoughtId, {});
|
|
}
|
|
}
|
|
|
|
if (warningThrottle[4])
|
|
--warningThrottle[4];
|
|
else if (disgustCounter >= kPeepDisgustWarningThreshold && disgustCounter >= gameState.park.numGuestsInPark / 32)
|
|
{
|
|
warningThrottle[4] = 4;
|
|
if (Config::Get().notifications.GuestWarnings)
|
|
{
|
|
constexpr auto thoughtId = static_cast<uint32_t>(PeepThoughtType::PathDisgusting);
|
|
News::AddItemToQueue(News::ItemType::peeps, STR_PEEPS_DISGUSTED_BY_PATHS, thoughtId, {});
|
|
}
|
|
}
|
|
|
|
if (warningThrottle[5])
|
|
--warningThrottle[5];
|
|
else if (vandalismCounter >= kPeepVandalismWarningThreshold && vandalismCounter >= gameState.park.numGuestsInPark / 32)
|
|
{
|
|
warningThrottle[5] = 4;
|
|
if (Config::Get().notifications.GuestWarnings)
|
|
{
|
|
constexpr auto thoughtId = static_cast<uint32_t>(PeepThoughtType::Vandalism);
|
|
News::AddItemToQueue(News::ItemType::peeps, STR_PEEPS_DISLIKE_VANDALISM, thoughtId, {});
|
|
}
|
|
}
|
|
|
|
if (warningThrottle[6])
|
|
--warningThrottle[6];
|
|
else if (noexitCounter >= kPeepNoExitWarningThreshold)
|
|
{
|
|
warningThrottle[6] = 4;
|
|
if (Config::Get().notifications.GuestWarnings)
|
|
{
|
|
constexpr auto thoughtId = static_cast<uint32_t>(PeepThoughtType::CantFindExit);
|
|
News::AddItemToQueue(News::ItemType::peeps, STR_PEEPS_GETTING_LOST_OR_STUCK, thoughtId, {});
|
|
}
|
|
}
|
|
else if (lostCounter >= kPeepLostWarningThreshold)
|
|
{
|
|
warningThrottle[6] = 4;
|
|
if (Config::Get().notifications.GuestWarnings)
|
|
{
|
|
constexpr auto thoughtId = static_cast<uint32_t>(PeepThoughtType::Lost);
|
|
News::AddItemToQueue(News::ItemType::peeps, STR_PEEPS_GETTING_LOST_OR_STUCK, thoughtId, {});
|
|
}
|
|
}
|
|
|
|
if (warningThrottle[7])
|
|
--warningThrottle[7];
|
|
else if (tooLongQueueCounter > kPeepTooLongQueueThreshold && tooLongQueueCounter > inQueueCounter / 20)
|
|
{ // The amount of guests complaining about queue duration is at least 5% of the amount of queuing guests.
|
|
// This includes guests who are no longer queuing.
|
|
warningThrottle[7] = 4;
|
|
if (Config::Get().notifications.GuestWarnings)
|
|
{
|
|
auto rideWithMostQueueComplaints = std::max_element(
|
|
queueComplainingGuestsMap.begin(), queueComplainingGuestsMap.end(),
|
|
[](auto& lhs, auto& rhs) { return lhs.second < rhs.second; });
|
|
auto rideId = rideWithMostQueueComplaints->first.ToUnderlying();
|
|
News::AddItemToQueue(News::ItemType::ride, STR_PEEPS_COMPLAINING_ABOUT_QUEUE_LENGTH_WARNING, rideId, {});
|
|
}
|
|
}
|
|
}
|
|
|
|
void PeepStopCrowdNoise()
|
|
{
|
|
if (_crowdSoundChannel != nullptr)
|
|
{
|
|
_crowdSoundChannel->Stop();
|
|
_crowdSoundChannel = nullptr;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006BD18A
|
|
*/
|
|
void PeepUpdateCrowdNoise()
|
|
{
|
|
PROFILED_FUNCTION();
|
|
|
|
if (OpenRCT2::Audio::gGameSoundsOff)
|
|
return;
|
|
|
|
if (!Config::Get().sound.SoundEnabled)
|
|
return;
|
|
|
|
if (gLegacyScene == LegacyScene::scenarioEditor)
|
|
return;
|
|
|
|
auto viewport = gMusicTrackingViewport;
|
|
if (viewport == nullptr)
|
|
return;
|
|
|
|
// Count the number of peeps visible
|
|
auto visiblePeeps = 0;
|
|
|
|
for (auto peep : EntityList<Guest>())
|
|
{
|
|
if (peep->x == kLocationNull)
|
|
continue;
|
|
if (viewport->viewPos.x > peep->SpriteData.SpriteRect.GetRight())
|
|
continue;
|
|
if (viewport->viewPos.x + viewport->ViewWidth() < peep->SpriteData.SpriteRect.GetLeft())
|
|
continue;
|
|
if (viewport->viewPos.y > peep->SpriteData.SpriteRect.GetBottom())
|
|
continue;
|
|
if (viewport->viewPos.y + viewport->ViewHeight() < peep->SpriteData.SpriteRect.GetTop())
|
|
continue;
|
|
|
|
visiblePeeps += peep->State == PeepState::queuing ? 1 : 2;
|
|
}
|
|
|
|
// This function doesn't account for the fact that the screen might be so big that 100 peeps could potentially be very
|
|
// spread out and therefore not produce any crowd noise. Perhaps a more sophisticated solution would check how many peeps
|
|
// were in close proximity to each other.
|
|
|
|
// Allows queuing peeps to make half as much noise, and at least 6 peeps must be visible for any crowd noise
|
|
visiblePeeps = (visiblePeeps / 2) - 6;
|
|
if (visiblePeeps < 0)
|
|
{
|
|
// Mute crowd noise
|
|
if (_crowdSoundChannel != nullptr)
|
|
{
|
|
_crowdSoundChannel->SetVolume(0);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int32_t volume;
|
|
|
|
// Formula to scale peeps to dB where peeps [0, 120] scales approximately logarithmically to [-3314, -150] dB/100
|
|
// 207360000 maybe related to DSBVOLUME_MIN which is -10,000 (dB/100)
|
|
volume = 120 - std::min(visiblePeeps, 120);
|
|
volume = volume * volume * volume * volume;
|
|
volume = (viewport->zoom.ApplyInversedTo(207360000 - volume) - 207360000) / 65536 - 150;
|
|
|
|
// Load and play crowd noise if needed and set volume
|
|
if (_crowdSoundChannel == nullptr || _crowdSoundChannel->IsDone())
|
|
{
|
|
_crowdSoundChannel = CreateAudioChannel(SoundId::crowdAmbience, true, 0);
|
|
if (_crowdSoundChannel != nullptr)
|
|
{
|
|
_crowdSoundChannel->SetGroup(OpenRCT2::Audio::MixerGroup::Sound);
|
|
}
|
|
}
|
|
if (_crowdSoundChannel != nullptr)
|
|
{
|
|
_crowdSoundChannel->SetVolume(DStoMixerVolume(volume));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069BE9B
|
|
*/
|
|
void PeepApplause()
|
|
{
|
|
for (auto peep : EntityList<Guest>())
|
|
{
|
|
if (peep->OutsideOfPark)
|
|
continue;
|
|
|
|
// Release balloon
|
|
GuestReleaseBalloon(peep, peep->z + 9);
|
|
|
|
// Clap
|
|
if ((peep->State == PeepState::walking || peep->State == PeepState::queuing) && peep->IsActionInterruptableSafely())
|
|
{
|
|
peep->Action = PeepActionType::clap;
|
|
peep->AnimationFrameNum = 0;
|
|
peep->AnimationImageIdOffset = 0;
|
|
peep->UpdateCurrentAnimationType();
|
|
}
|
|
}
|
|
|
|
// Play applause noise
|
|
OpenRCT2::Audio::Play(OpenRCT2::Audio::SoundId::applause, 0, ContextGetWidth() / 2);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069C35E
|
|
*/
|
|
void PeepUpdateDaysInQueue()
|
|
{
|
|
for (auto peep : EntityList<Guest>())
|
|
{
|
|
if (!peep->OutsideOfPark && peep->State == PeepState::queuing)
|
|
{
|
|
if (peep->DaysInQueue < 255)
|
|
{
|
|
peep->DaysInQueue += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void Peep::FormatActionTo(Formatter& ft) const
|
|
{
|
|
switch (State)
|
|
{
|
|
case PeepState::falling:
|
|
ft.Add<StringId>(Action == PeepActionType::drowning ? STR_DROWNING : STR_WALKING);
|
|
break;
|
|
case PeepState::one:
|
|
ft.Add<StringId>(STR_WALKING);
|
|
break;
|
|
case PeepState::onRide:
|
|
case PeepState::leavingRide:
|
|
case PeepState::enteringRide:
|
|
{
|
|
auto ride = GetRide(CurrentRide);
|
|
if (ride != nullptr)
|
|
{
|
|
ft.Add<StringId>(ride->getRideTypeDescriptor().HasFlag(RtdFlag::describeAsInside) ? STR_IN_RIDE : STR_ON_RIDE);
|
|
ride->formatNameTo(ft);
|
|
}
|
|
else
|
|
{
|
|
ft.Add<StringId>(STR_ON_RIDE).Add<StringId>(kStringIdNone);
|
|
}
|
|
break;
|
|
}
|
|
case PeepState::buying:
|
|
{
|
|
ft.Add<StringId>(STR_AT_RIDE);
|
|
auto ride = GetRide(CurrentRide);
|
|
if (ride != nullptr)
|
|
{
|
|
ride->formatNameTo(ft);
|
|
}
|
|
else
|
|
{
|
|
ft.Add<StringId>(kStringIdNone);
|
|
}
|
|
break;
|
|
}
|
|
case PeepState::walking:
|
|
case PeepState::usingBin:
|
|
{
|
|
if (auto* guest = As<Guest>(); guest != nullptr)
|
|
{
|
|
if (!guest->GuestHeadingToRideId.IsNull())
|
|
{
|
|
auto ride = GetRide(guest->GuestHeadingToRideId);
|
|
if (ride != nullptr)
|
|
{
|
|
ft.Add<StringId>(STR_HEADING_FOR);
|
|
ride->formatNameTo(ft);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ft.Add<StringId>((PeepFlags & PEEP_FLAGS_LEAVING_PARK) ? STR_LEAVING_PARK : STR_WALKING);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case PeepState::queuingFront:
|
|
case PeepState::queuing:
|
|
{
|
|
auto ride = GetRide(CurrentRide);
|
|
if (ride != nullptr)
|
|
{
|
|
ft.Add<StringId>(STR_QUEUING_FOR);
|
|
ride->formatNameTo(ft);
|
|
}
|
|
break;
|
|
}
|
|
case PeepState::sitting:
|
|
ft.Add<StringId>(STR_SITTING);
|
|
break;
|
|
case PeepState::watching:
|
|
if (!CurrentRide.IsNull())
|
|
{
|
|
auto ride = GetRide(CurrentRide);
|
|
if (ride != nullptr)
|
|
{
|
|
ft.Add<StringId>((StandingFlags & 0x1) ? STR_WATCHING_CONSTRUCTION_OF : STR_WATCHING_RIDE);
|
|
ride->formatNameTo(ft);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ft.Add<StringId>((StandingFlags & 0x1) ? STR_WATCHING_NEW_RIDE_BEING_CONSTRUCTED : STR_LOOKING_AT_SCENERY);
|
|
}
|
|
break;
|
|
case PeepState::picked:
|
|
ft.Add<StringId>(STR_SELECT_LOCATION);
|
|
break;
|
|
case PeepState::patrolling:
|
|
case PeepState::enteringPark:
|
|
case PeepState::leavingPark:
|
|
ft.Add<StringId>(STR_WALKING);
|
|
break;
|
|
case PeepState::mowing:
|
|
ft.Add<StringId>(STR_MOWING_GRASS);
|
|
break;
|
|
case PeepState::sweeping:
|
|
ft.Add<StringId>(STR_SWEEPING_FOOTPATH);
|
|
break;
|
|
case PeepState::watering:
|
|
ft.Add<StringId>(STR_WATERING_GARDENS);
|
|
break;
|
|
case PeepState::emptyingBin:
|
|
ft.Add<StringId>(STR_EMPTYING_LITTER_BIN);
|
|
break;
|
|
case PeepState::answering:
|
|
if (SubState == 0)
|
|
{
|
|
ft.Add<StringId>(STR_WALKING);
|
|
}
|
|
else if (SubState == 1)
|
|
{
|
|
ft.Add<StringId>(STR_ANSWERING_RADIO_CALL);
|
|
}
|
|
else
|
|
{
|
|
ft.Add<StringId>(STR_RESPONDING_TO_RIDE_BREAKDOWN_CALL);
|
|
auto ride = GetRide(CurrentRide);
|
|
if (ride != nullptr)
|
|
{
|
|
ride->formatNameTo(ft);
|
|
}
|
|
else
|
|
{
|
|
ft.Add<StringId>(kStringIdNone);
|
|
}
|
|
}
|
|
break;
|
|
case PeepState::fixing:
|
|
{
|
|
ft.Add<StringId>(STR_FIXING_RIDE);
|
|
auto ride = GetRide(CurrentRide);
|
|
if (ride != nullptr)
|
|
{
|
|
ride->formatNameTo(ft);
|
|
}
|
|
else
|
|
{
|
|
ft.Add<StringId>(kStringIdNone);
|
|
}
|
|
break;
|
|
}
|
|
case PeepState::headingToInspection:
|
|
{
|
|
ft.Add<StringId>(STR_HEADING_TO_RIDE_FOR_INSPECTION);
|
|
auto ride = GetRide(CurrentRide);
|
|
if (ride != nullptr)
|
|
{
|
|
ride->formatNameTo(ft);
|
|
}
|
|
else
|
|
{
|
|
ft.Add<StringId>(kStringIdNone);
|
|
}
|
|
break;
|
|
}
|
|
case PeepState::inspecting:
|
|
{
|
|
ft.Add<StringId>(STR_INSPECTING_RIDE);
|
|
auto ride = GetRide(CurrentRide);
|
|
if (ride != nullptr)
|
|
{
|
|
ride->formatNameTo(ft);
|
|
}
|
|
else
|
|
{
|
|
ft.Add<StringId>(kStringIdNone);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static constexpr StringId kStaffNames[] = {
|
|
STR_HANDYMAN_X,
|
|
STR_MECHANIC_X,
|
|
STR_SECURITY_GUARD_X,
|
|
STR_ENTERTAINER_X,
|
|
};
|
|
|
|
void Peep::FormatNameTo(Formatter& ft) const
|
|
{
|
|
if (Name == nullptr)
|
|
{
|
|
auto& gameState = getGameState();
|
|
const bool showGuestNames = gameState.park.flags & PARK_FLAGS_SHOW_REAL_GUEST_NAMES;
|
|
const bool showStaffNames = gameState.park.flags & PARK_FLAGS_SHOW_REAL_STAFF_NAMES;
|
|
|
|
auto* staff = As<Staff>();
|
|
const bool isStaff = staff != nullptr;
|
|
|
|
if ((!isStaff && showGuestNames) || (isStaff && showStaffNames))
|
|
{
|
|
auto nameId = PeepId;
|
|
if (isStaff)
|
|
{
|
|
// Prevent staff from getting the same names by offsetting the name table based on staff type.
|
|
nameId *= 256 * EnumValue(staff->AssignedStaffType) + 1;
|
|
}
|
|
|
|
auto realNameStringId = GetRealNameStringIDFromPeepID(nameId);
|
|
ft.Add<StringId>(realNameStringId);
|
|
}
|
|
else if (isStaff)
|
|
{
|
|
auto staffNameIndex = static_cast<uint8_t>(staff->AssignedStaffType);
|
|
if (staffNameIndex >= std::size(kStaffNames))
|
|
{
|
|
staffNameIndex = 0;
|
|
}
|
|
|
|
ft.Add<StringId>(kStaffNames[staffNameIndex]);
|
|
ft.Add<uint32_t>(PeepId);
|
|
}
|
|
else
|
|
{
|
|
ft.Add<StringId>(STR_GUEST_X).Add<uint32_t>(PeepId);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ft.Add<StringId>(STR_STRING).Add<const char*>(Name);
|
|
}
|
|
}
|
|
|
|
std::string Peep::GetName() const
|
|
{
|
|
Formatter ft;
|
|
FormatNameTo(ft);
|
|
return FormatStringIDLegacy(STR_STRINGID, ft.Data());
|
|
}
|
|
|
|
bool Peep::SetName(std::string_view value)
|
|
{
|
|
if (value.empty())
|
|
{
|
|
std::free(Name);
|
|
Name = nullptr;
|
|
return true;
|
|
}
|
|
|
|
auto newNameMemory = static_cast<char*>(std::malloc(value.size() + 1));
|
|
if (newNameMemory != nullptr)
|
|
{
|
|
std::memcpy(newNameMemory, value.data(), value.size());
|
|
newNameMemory[value.size()] = '\0';
|
|
std::free(Name);
|
|
Name = newNameMemory;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool Peep::IsActionWalking() const
|
|
{
|
|
return Action == PeepActionType::walking;
|
|
}
|
|
|
|
bool Peep::IsActionIdle() const
|
|
{
|
|
return Action == PeepActionType::idle;
|
|
}
|
|
|
|
bool Peep::IsActionInterruptable() const
|
|
{
|
|
return IsActionIdle() || IsActionWalking();
|
|
}
|
|
|
|
/**
|
|
* Used to avoid peep action and animation triggers that cause them to stop moving and might put them at risk
|
|
* of getting run over at level crossings, such as guests reading the map and entertainers performing.
|
|
*/
|
|
bool Peep::IsActionInterruptableSafely() const
|
|
{
|
|
return IsActionInterruptable() && !IsOnLevelCrossing();
|
|
}
|
|
|
|
void PeepSetMapTooltip(Peep* peep)
|
|
{
|
|
auto ft = Formatter();
|
|
if (auto* guest = peep->As<Guest>(); guest != nullptr)
|
|
{
|
|
ft.Add<StringId>((peep->PeepFlags & PEEP_FLAGS_TRACKING) ? STR_TRACKED_GUEST_MAP_TIP : STR_GUEST_MAP_TIP);
|
|
ft.Add<uint32_t>(GetPeepFaceSpriteSmall(guest));
|
|
guest->FormatNameTo(ft);
|
|
guest->FormatActionTo(ft);
|
|
}
|
|
else
|
|
{
|
|
ft.Add<StringId>(STR_STAFF_MAP_TIP);
|
|
peep->FormatNameTo(ft);
|
|
peep->FormatActionTo(ft);
|
|
}
|
|
|
|
auto intent = Intent(INTENT_ACTION_SET_MAP_TOOLTIP);
|
|
intent.PutExtra(INTENT_EXTRA_FORMATTER, &ft);
|
|
ContextBroadcastIntent(&intent);
|
|
}
|
|
|
|
/**
|
|
* rct2: 0x00693BAB
|
|
*/
|
|
void Peep::SwitchNextAnimationType()
|
|
{
|
|
// TBD: Add nextAnimationType as function parameter and make peep->NextAnimationType obsolete?
|
|
if (NextAnimationType != AnimationType)
|
|
{
|
|
Invalidate();
|
|
AnimationType = NextAnimationType;
|
|
|
|
auto& objManager = GetContext()->GetObjectManager();
|
|
auto* animObj = objManager.GetLoadedObject<PeepAnimationsObject>(AnimationObjectIndex);
|
|
|
|
const auto& spriteBounds = animObj->GetSpriteBounds(AnimationGroup, NextAnimationType);
|
|
SpriteData.Width = spriteBounds.spriteWidth;
|
|
SpriteData.HeightMin = spriteBounds.spriteHeightNegative;
|
|
SpriteData.HeightMax = spriteBounds.spriteHeightPositive;
|
|
Invalidate();
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00693EF2
|
|
*/
|
|
static void PeepReturnToCentreOfTile(Peep* peep)
|
|
{
|
|
peep->PeepDirection = DirectionReverse(peep->PeepDirection);
|
|
auto destination = peep->GetLocation().ToTileCentre();
|
|
peep->SetDestination(destination, 5);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00693f2C
|
|
*/
|
|
static bool PeepInteractWithEntrance(Peep* peep, const CoordsXYE& coords, uint8_t& pathing_result)
|
|
{
|
|
auto tile_element = coords.element;
|
|
uint8_t entranceType = tile_element->AsEntrance()->GetEntranceType();
|
|
auto rideIndex = tile_element->AsEntrance()->GetRideIndex();
|
|
|
|
if ((entranceType == ENTRANCE_TYPE_RIDE_ENTRANCE) || (entranceType == ENTRANCE_TYPE_RIDE_EXIT))
|
|
{
|
|
// If an entrance or exit that doesn't belong to the ride we are queuing for ignore the entrance/exit
|
|
// This can happen when paths clip through entrance/exits
|
|
if (peep->State == PeepState::queuing && peep->CurrentRide != rideIndex)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
// Store some details to determine when to override the default
|
|
// behaviour (defined below) for when staff attempt to enter a ride
|
|
// to fix/inspect it.
|
|
if (entranceType == ENTRANCE_TYPE_RIDE_EXIT)
|
|
{
|
|
pathing_result |= PATHING_RIDE_EXIT;
|
|
_peepRideEntranceExitElement = tile_element;
|
|
}
|
|
else if (entranceType == ENTRANCE_TYPE_RIDE_ENTRANCE)
|
|
{
|
|
pathing_result |= PATHING_RIDE_ENTRANCE;
|
|
_peepRideEntranceExitElement = tile_element;
|
|
}
|
|
|
|
if (entranceType == ENTRANCE_TYPE_RIDE_EXIT)
|
|
{
|
|
// Default guest/staff behaviour attempting to enter a
|
|
// ride exit is to turn around.
|
|
peep->InteractionRideIndex = RideId::GetNull();
|
|
PeepReturnToCentreOfTile(peep);
|
|
return true;
|
|
}
|
|
|
|
if (entranceType == ENTRANCE_TYPE_RIDE_ENTRANCE)
|
|
{
|
|
auto ride = GetRide(rideIndex);
|
|
if (ride == nullptr)
|
|
return false;
|
|
|
|
auto* guest = peep->As<Guest>();
|
|
if (guest == nullptr)
|
|
{
|
|
// Default staff behaviour attempting to enter a
|
|
// ride entrance is to turn around.
|
|
peep->InteractionRideIndex = RideId::GetNull();
|
|
PeepReturnToCentreOfTile(peep);
|
|
return true;
|
|
}
|
|
|
|
if (guest->State == PeepState::queuing)
|
|
{
|
|
// Guest is in the ride queue.
|
|
guest->RideSubState = PeepRideSubState::atQueueFront;
|
|
guest->AnimationImageIdOffset = _backupAnimationImageIdOffset;
|
|
return true;
|
|
}
|
|
|
|
// Guest is on a normal path, i.e. ride has no queue.
|
|
if (guest->InteractionRideIndex == rideIndex)
|
|
{
|
|
// Peep is retrying the ride entrance without leaving
|
|
// the path tile and without trying any other ride
|
|
// attached to this path tile. i.e. stick with the
|
|
// peeps previous decision not to go on the ride.
|
|
PeepReturnToCentreOfTile(guest);
|
|
return true;
|
|
}
|
|
|
|
guest->TimeLost = 0;
|
|
auto stationNum = tile_element->AsEntrance()->GetStationIndex();
|
|
// Guest walks up to the ride for the first time since entering
|
|
// the path tile or since considering another ride attached to
|
|
// the path tile.
|
|
if (!guest->ShouldGoOnRide(*ride, stationNum, false, false))
|
|
{
|
|
// Peep remembers that this is the last ride they
|
|
// considered while on this path tile.
|
|
guest->InteractionRideIndex = rideIndex;
|
|
PeepReturnToCentreOfTile(guest);
|
|
return true;
|
|
}
|
|
|
|
// Guest has decided to go on the ride.
|
|
guest->AnimationImageIdOffset = _backupAnimationImageIdOffset;
|
|
guest->InteractionRideIndex = rideIndex;
|
|
|
|
auto& station = ride->getStation(stationNum);
|
|
auto previous_last = station.LastPeepInQueue;
|
|
station.LastPeepInQueue = guest->Id;
|
|
guest->GuestNextInQueue = previous_last;
|
|
station.QueueLength++;
|
|
|
|
guest->CurrentRide = rideIndex;
|
|
guest->CurrentRideStation = stationNum;
|
|
guest->DaysInQueue = 0;
|
|
guest->SetState(PeepState::queuing);
|
|
guest->RideSubState = PeepRideSubState::atQueueFront;
|
|
guest->TimeInQueue = 0;
|
|
if (guest->PeepFlags & PEEP_FLAGS_TRACKING)
|
|
{
|
|
auto ft = Formatter();
|
|
guest->FormatNameTo(ft);
|
|
ride->formatNameTo(ft);
|
|
if (Config::Get().notifications.GuestQueuingForRide)
|
|
{
|
|
News::AddItemToQueue(News::ItemType::peepOnRide, STR_PEEP_TRACKING_PEEP_JOINED_QUEUE_FOR_X, guest->Id, ft);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// PARK_ENTRANCE
|
|
auto* guest = peep->As<Guest>();
|
|
if (guest == nullptr)
|
|
{
|
|
// Staff cannot leave the park, so go back.
|
|
PeepReturnToCentreOfTile(peep);
|
|
return true;
|
|
}
|
|
|
|
// If not the centre of the entrance arch
|
|
if (tile_element->AsEntrance()->GetSequenceIndex() != 0)
|
|
{
|
|
PeepReturnToCentreOfTile(guest);
|
|
return true;
|
|
}
|
|
|
|
auto& gameState = getGameState();
|
|
uint8_t entranceDirection = tile_element->GetDirection();
|
|
if (entranceDirection != guest->PeepDirection)
|
|
{
|
|
if (DirectionReverse(entranceDirection) != guest->PeepDirection)
|
|
{
|
|
PeepReturnToCentreOfTile(guest);
|
|
return true;
|
|
}
|
|
|
|
// Peep is leaving the park.
|
|
if (guest->State != PeepState::walking)
|
|
{
|
|
PeepReturnToCentreOfTile(guest);
|
|
return true;
|
|
}
|
|
|
|
if (!(guest->PeepFlags & PEEP_FLAGS_LEAVING_PARK))
|
|
{
|
|
// If the park is open and leaving flag isn't set return to centre
|
|
if (gameState.park.flags & PARK_FLAGS_PARK_OPEN)
|
|
{
|
|
PeepReturnToCentreOfTile(guest);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
auto destination = guest->GetDestination() + CoordsDirectionDelta[guest->PeepDirection];
|
|
guest->SetDestination(destination, 9);
|
|
guest->MoveTo({ coords, guest->z });
|
|
guest->SetState(PeepState::leavingPark);
|
|
|
|
guest->Var37 = 0;
|
|
if (guest->PeepFlags & PEEP_FLAGS_TRACKING)
|
|
{
|
|
auto ft = Formatter();
|
|
guest->FormatNameTo(ft);
|
|
if (Config::Get().notifications.GuestLeftPark)
|
|
{
|
|
News::AddItemToQueue(News::ItemType::peepOnRide, STR_PEEP_TRACKING_LEFT_PARK, guest->Id, ft);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Peep is entering the park.
|
|
|
|
if (guest->State != PeepState::enteringPark)
|
|
{
|
|
PeepReturnToCentreOfTile(guest);
|
|
return true;
|
|
}
|
|
|
|
if (!(gameState.park.flags & PARK_FLAGS_PARK_OPEN))
|
|
{
|
|
guest->State = PeepState::leavingPark;
|
|
guest->Var37 = 1;
|
|
DecrementGuestsHeadingForPark();
|
|
PeepWindowStateUpdate(guest);
|
|
PeepReturnToCentreOfTile(guest);
|
|
return true;
|
|
}
|
|
|
|
bool found = false;
|
|
auto entrance = std::find_if(gameState.park.entrances.begin(), gameState.park.entrances.end(), [coords](const auto& e) {
|
|
return coords.ToTileStart() == e;
|
|
});
|
|
if (entrance != gameState.park.entrances.end())
|
|
{
|
|
int16_t z = entrance->z / 8;
|
|
entranceDirection = entrance->direction;
|
|
auto nextLoc = coords.ToTileStart() + CoordsDirectionDelta[entranceDirection];
|
|
|
|
// Make sure there is a path right behind the entrance, otherwise turn around
|
|
TileElement* nextTileElement = MapGetFirstElementAt(nextLoc);
|
|
do
|
|
{
|
|
if (nextTileElement == nullptr)
|
|
break;
|
|
if (nextTileElement->GetType() != TileElementType::Path)
|
|
continue;
|
|
|
|
if (nextTileElement->AsPath()->IsQueue())
|
|
continue;
|
|
|
|
if (nextTileElement->AsPath()->IsSloped())
|
|
{
|
|
uint8_t slopeDirection = nextTileElement->AsPath()->GetSlopeDirection();
|
|
if (slopeDirection == entranceDirection)
|
|
{
|
|
if (z != nextTileElement->BaseHeight)
|
|
{
|
|
continue;
|
|
}
|
|
found = true;
|
|
break;
|
|
}
|
|
|
|
if (DirectionReverse(slopeDirection) != entranceDirection)
|
|
continue;
|
|
|
|
if (z - 2 != nextTileElement->BaseHeight)
|
|
continue;
|
|
found = true;
|
|
break;
|
|
}
|
|
|
|
if (z != nextTileElement->BaseHeight)
|
|
{
|
|
continue;
|
|
}
|
|
found = true;
|
|
break;
|
|
} while (!(nextTileElement++)->IsLastForTile());
|
|
}
|
|
|
|
if (!found)
|
|
{
|
|
guest->State = PeepState::leavingPark;
|
|
guest->Var37 = 1;
|
|
DecrementGuestsHeadingForPark();
|
|
PeepWindowStateUpdate(guest);
|
|
PeepReturnToCentreOfTile(guest);
|
|
return true;
|
|
}
|
|
|
|
auto entranceFee = Park::GetEntranceFee();
|
|
if (entranceFee != 0)
|
|
{
|
|
if (guest->HasItem(ShopItem::Voucher))
|
|
{
|
|
if (guest->VoucherType == VOUCHER_TYPE_PARK_ENTRY_HALF_PRICE)
|
|
{
|
|
entranceFee /= 2;
|
|
guest->RemoveItem(ShopItem::Voucher);
|
|
guest->WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_INVENTORY;
|
|
}
|
|
else if (guest->VoucherType == VOUCHER_TYPE_PARK_ENTRY_FREE)
|
|
{
|
|
entranceFee = 0;
|
|
guest->RemoveItem(ShopItem::Voucher);
|
|
guest->WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_INVENTORY;
|
|
}
|
|
}
|
|
if (entranceFee > guest->CashInPocket)
|
|
{
|
|
guest->State = PeepState::leavingPark;
|
|
guest->Var37 = 1;
|
|
DecrementGuestsHeadingForPark();
|
|
PeepWindowStateUpdate(guest);
|
|
PeepReturnToCentreOfTile(guest);
|
|
return true;
|
|
}
|
|
|
|
gameState.park.totalIncomeFromAdmissions += entranceFee;
|
|
guest->SpendMoney(guest->PaidToEnter, entranceFee, ExpenditureType::parkEntranceTickets);
|
|
guest->PeepFlags |= PEEP_FLAGS_HAS_PAID_FOR_PARK_ENTRY;
|
|
}
|
|
|
|
getGameState().park.totalAdmissions++;
|
|
|
|
auto* windowMgr = Ui::GetWindowManager();
|
|
windowMgr->InvalidateByNumber(WindowClass::parkInformation, 0);
|
|
|
|
guest->Var37 = 1;
|
|
auto destination = guest->GetDestination();
|
|
destination += CoordsDirectionDelta[guest->PeepDirection];
|
|
guest->SetDestination(destination, 7);
|
|
guest->MoveTo({ coords, guest->z });
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x006946D8
|
|
*/
|
|
static void PeepFootpathMoveForward(Peep* peep, const CoordsXYE& coords, bool vandalism)
|
|
{
|
|
const auto* pathElement = coords.element->AsPath();
|
|
assert(pathElement != nullptr);
|
|
|
|
peep->NextLoc = { coords.ToTileStart(), pathElement->GetBaseZ() };
|
|
peep->SetNextFlags(pathElement->GetSlopeDirection(), pathElement->IsSloped(), false);
|
|
|
|
int16_t z = peep->GetZOnSlope(coords.x, coords.y);
|
|
|
|
auto* guest = peep->As<Guest>();
|
|
if (guest == nullptr)
|
|
{
|
|
peep->MoveTo({ coords, z });
|
|
return;
|
|
}
|
|
|
|
uint8_t vandalThoughtTimeout = (guest->VandalismSeen & 0xC0) >> 6;
|
|
// Advance the vandalised tiles by 1
|
|
uint8_t vandalisedTiles = (guest->VandalismSeen * 2) & 0x3F;
|
|
|
|
if (vandalism)
|
|
{
|
|
// Add one more to the vandalised tiles
|
|
vandalisedTiles |= 1;
|
|
// If there has been 2 vandalised tiles in the last 6
|
|
if (vandalisedTiles & 0x3E && (vandalThoughtTimeout == 0))
|
|
{
|
|
if ((ScenarioRand() & 0xFFFF) <= 10922)
|
|
{
|
|
guest->InsertNewThought(PeepThoughtType::Vandalism);
|
|
guest->HappinessTarget = std::max(0, guest->HappinessTarget - 17);
|
|
}
|
|
vandalThoughtTimeout = 3;
|
|
}
|
|
}
|
|
|
|
if (vandalThoughtTimeout && (ScenarioRand() & 0xFFFF) <= 4369)
|
|
{
|
|
vandalThoughtTimeout--;
|
|
}
|
|
|
|
guest->VandalismSeen = (vandalThoughtTimeout << 6) | vandalisedTiles;
|
|
|
|
constexpr uint16_t kThresholdCrowdCount = 10;
|
|
constexpr uint8_t kThresholdLitterCount = 3;
|
|
constexpr uint8_t kThresholdVomitCount = 3;
|
|
|
|
// Don't allow this loop to become too expensive, lets just have enough to potentially satisfy
|
|
// all the conditions.
|
|
constexpr uint32_t kMaxIterations = kThresholdCrowdCount + kThresholdLitterCount + kThresholdVomitCount;
|
|
|
|
uint16_t crowdCount = 0;
|
|
uint8_t litterCount = 0;
|
|
uint8_t vomitCount = 0;
|
|
uint32_t iterations = 0;
|
|
|
|
auto quad = EntityTileList(coords);
|
|
for (auto* otherEnt : quad)
|
|
{
|
|
if (iterations++ >= kMaxIterations)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (std::abs(otherEnt->z - guest->NextLoc.z) > 16)
|
|
continue;
|
|
|
|
if (const auto* otherPeep = otherEnt->As<Peep>(); otherPeep != nullptr)
|
|
{
|
|
if (otherPeep->State != PeepState::walking)
|
|
continue;
|
|
|
|
crowdCount++;
|
|
continue;
|
|
}
|
|
|
|
if (const auto* litter = otherEnt->As<Litter>(); litter != nullptr)
|
|
{
|
|
if (litter->SubType != Litter::Type::Vomit && litter->SubType != Litter::Type::VomitAlt)
|
|
{
|
|
litterCount++;
|
|
}
|
|
else
|
|
{
|
|
vomitCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (crowdCount >= kThresholdCrowdCount && guest->State == PeepState::walking && (ScenarioRand() & 0xFFFF) <= 21845)
|
|
{
|
|
guest->InsertNewThought(PeepThoughtType::Crowded);
|
|
guest->HappinessTarget = std::max(0, guest->HappinessTarget - 14);
|
|
}
|
|
|
|
litterCount = std::min(kThresholdLitterCount, litterCount);
|
|
vomitCount = std::min(kThresholdVomitCount, vomitCount);
|
|
|
|
uint8_t disgustingTime = guest->DisgustingCount & 0xC0;
|
|
uint8_t disgustingCount = ((guest->DisgustingCount & 0xF) << 2) | vomitCount;
|
|
guest->DisgustingCount = disgustingCount | disgustingTime;
|
|
|
|
if (disgustingTime & 0xC0 && (ScenarioRand() & 0xFFFF) <= 4369)
|
|
{
|
|
// Reduce the disgusting time
|
|
guest->DisgustingCount -= 0x40;
|
|
}
|
|
else
|
|
{
|
|
uint8_t totalDisgustingCount = 0;
|
|
for (uint8_t time = 0; time < 3; time++)
|
|
{
|
|
totalDisgustingCount += (disgustingCount >> (2 * time)) & 0x3;
|
|
}
|
|
|
|
if (totalDisgustingCount >= kThresholdVomitCount && (ScenarioRand() & 0xFFFF) <= 10922)
|
|
{
|
|
guest->InsertNewThought(PeepThoughtType::PathDisgusting);
|
|
guest->HappinessTarget = std::max(0, guest->HappinessTarget - 17);
|
|
// Reset disgusting time
|
|
guest->DisgustingCount |= 0xC0;
|
|
}
|
|
}
|
|
|
|
uint8_t litterTime = guest->LitterCount & 0xC0;
|
|
litterCount = ((guest->LitterCount & 0xF) << 2) | litterCount;
|
|
guest->LitterCount = litterCount | litterTime;
|
|
|
|
if (litterTime & 0xC0 && (ScenarioRand() & 0xFFFF) <= 4369)
|
|
{
|
|
// Reduce the litter time
|
|
guest->LitterCount -= 0x40;
|
|
}
|
|
else
|
|
{
|
|
uint8_t totalLitter = 0;
|
|
for (uint8_t time = 0; time < 3; time++)
|
|
{
|
|
totalLitter += (litterCount >> (2 * time)) & 0x3;
|
|
}
|
|
|
|
if (totalLitter >= kThresholdLitterCount && (ScenarioRand() & 0xFFFF) <= 10922)
|
|
{
|
|
guest->InsertNewThought(PeepThoughtType::BadLitter);
|
|
guest->HappinessTarget = std::max(0, guest->HappinessTarget - 17);
|
|
// Reset litter time
|
|
guest->LitterCount |= 0xC0;
|
|
}
|
|
}
|
|
|
|
guest->MoveTo({ coords, z });
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069455E
|
|
*/
|
|
static void PeepInteractWithPath(Peep* peep, const CoordsXYE& coords)
|
|
{
|
|
// 0x00F1AEE2
|
|
const auto* pathElement = coords.element->AsPath();
|
|
assert(pathElement != nullptr);
|
|
|
|
bool vandalismPresent = false;
|
|
if (pathElement->HasAddition() && pathElement->IsBroken() && (pathElement->GetEdges()) != 0xF)
|
|
{
|
|
vandalismPresent = true;
|
|
}
|
|
|
|
int16_t z = pathElement->GetBaseZ();
|
|
auto* guest = peep->As<Guest>();
|
|
if (MapIsLocationOwned({ coords, z }))
|
|
{
|
|
if (guest != nullptr && guest->OutsideOfPark)
|
|
{
|
|
PeepReturnToCentreOfTile(guest);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (guest == nullptr || !guest->OutsideOfPark)
|
|
{
|
|
PeepReturnToCentreOfTile(peep);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (guest != nullptr && pathElement->IsQueue())
|
|
{
|
|
auto rideIndex = pathElement->GetRideIndex();
|
|
if (guest->State == PeepState::queuing)
|
|
{
|
|
// Check if this queue is connected to the ride the
|
|
// peep is queuing for, i.e. the player hasn't edited
|
|
// the queue, rebuilt the ride, etc.
|
|
if (guest->CurrentRide == rideIndex)
|
|
{
|
|
PeepFootpathMoveForward(guest, coords, vandalismPresent);
|
|
}
|
|
else
|
|
{
|
|
// Queue got disconnected from the original ride.
|
|
guest->InteractionRideIndex = RideId::GetNull();
|
|
guest->RemoveFromQueue();
|
|
guest->SetState(PeepState::one);
|
|
PeepFootpathMoveForward(guest, coords, vandalismPresent);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Peep is not queuing.
|
|
guest->TimeLost = 0;
|
|
auto stationNum = pathElement->GetStationIndex();
|
|
|
|
if (pathElement->HasQueueBanner()
|
|
&& pathElement->GetQueueBannerDirection()
|
|
== DirectionReverse(guest->PeepDirection) // Ride sign is facing the direction the peep is walking
|
|
)
|
|
{
|
|
/* Peep is approaching the entrance of a ride queue.
|
|
* Decide whether to go on the ride. */
|
|
auto* ride = GetRide(rideIndex);
|
|
if (ride != nullptr && guest->ShouldGoOnRide(*ride, stationNum, true, false))
|
|
{
|
|
// Peep has decided to go on the ride at the queue.
|
|
guest->InteractionRideIndex = rideIndex;
|
|
|
|
// Add the peep to the ride queue.
|
|
auto& station = ride->getStation(stationNum);
|
|
auto old_last_peep = station.LastPeepInQueue;
|
|
station.LastPeepInQueue = guest->Id;
|
|
guest->GuestNextInQueue = old_last_peep;
|
|
station.QueueLength++;
|
|
|
|
PeepDecrementNumRiders(guest);
|
|
guest->CurrentRide = rideIndex;
|
|
guest->CurrentRideStation = stationNum;
|
|
guest->State = PeepState::queuing;
|
|
guest->DaysInQueue = 0;
|
|
PeepWindowStateUpdate(guest);
|
|
|
|
guest->RideSubState = PeepRideSubState::inQueue;
|
|
guest->DestinationTolerance = 2;
|
|
guest->TimeInQueue = 0;
|
|
if (guest->PeepFlags & PEEP_FLAGS_TRACKING)
|
|
{
|
|
auto ft = Formatter();
|
|
guest->FormatNameTo(ft);
|
|
ride->formatNameTo(ft);
|
|
if (Config::Get().notifications.GuestQueuingForRide)
|
|
{
|
|
News::AddItemToQueue(
|
|
News::ItemType::peepOnRide, STR_PEEP_TRACKING_PEEP_JOINED_QUEUE_FOR_X, guest->Id, ft);
|
|
}
|
|
}
|
|
|
|
// Force set centre of tile to prevent issues with guests accidentally skipping the queue
|
|
auto queueTileCentre = CoordsXY{ CoordsXY{ guest->NextLoc } + CoordsDirectionDelta[guest->PeepDirection] }
|
|
.ToTileCentre();
|
|
guest->SetDestination(queueTileCentre);
|
|
|
|
PeepFootpathMoveForward(guest, coords, vandalismPresent);
|
|
}
|
|
else
|
|
{
|
|
// Peep has decided not to go on the ride.
|
|
PeepReturnToCentreOfTile(guest);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* Peep is approaching a queue tile without a ride
|
|
* sign facing the peep. */
|
|
PeepFootpathMoveForward(guest, coords, vandalismPresent);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
peep->InteractionRideIndex = RideId::GetNull();
|
|
if (guest != nullptr && peep->State == PeepState::queuing)
|
|
{
|
|
guest->RemoveFromQueue();
|
|
guest->SetState(PeepState::one);
|
|
}
|
|
PeepFootpathMoveForward(peep, coords, vandalismPresent);
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00693F70
|
|
*/
|
|
static bool PeepInteractWithShop(Peep* peep, const CoordsXYE& coords)
|
|
{
|
|
RideId rideIndex = coords.element->AsTrack()->GetRideIndex();
|
|
auto ride = GetRide(rideIndex);
|
|
if (ride == nullptr || !ride->getRideTypeDescriptor().HasFlag(RtdFlag::isShopOrFacility))
|
|
return false;
|
|
|
|
auto* guest = peep->As<Guest>();
|
|
if (guest == nullptr)
|
|
{
|
|
PeepReturnToCentreOfTile(peep);
|
|
return true;
|
|
}
|
|
|
|
// If we are queuing ignore the 'shop'
|
|
// This can happen when paths clip through track
|
|
if (guest->State == PeepState::queuing)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
guest->TimeLost = 0;
|
|
|
|
if (ride->status != RideStatus::open)
|
|
{
|
|
PeepReturnToCentreOfTile(guest);
|
|
return true;
|
|
}
|
|
|
|
if (guest->InteractionRideIndex == rideIndex)
|
|
{
|
|
PeepReturnToCentreOfTile(guest);
|
|
return true;
|
|
}
|
|
|
|
if (guest->PeepFlags & PEEP_FLAGS_LEAVING_PARK)
|
|
{
|
|
PeepReturnToCentreOfTile(guest);
|
|
return true;
|
|
}
|
|
|
|
if (ride->getRideTypeDescriptor().HasFlag(RtdFlag::guestsShouldGoInsideFacility))
|
|
{
|
|
guest->TimeLost = 0;
|
|
if (!guest->ShouldGoOnRide(*ride, StationIndex::FromUnderlying(0), false, false))
|
|
{
|
|
PeepReturnToCentreOfTile(guest);
|
|
return true;
|
|
}
|
|
|
|
auto cost = ride->price[0];
|
|
if (cost != 0 && !(getGameState().park.flags & PARK_FLAGS_NO_MONEY))
|
|
{
|
|
ride->totalProfit = AddClamp(ride->totalProfit, cost);
|
|
ride->windowInvalidateFlags |= RIDE_INVALIDATE_RIDE_INCOME;
|
|
guest->SpendMoney(cost, ExpenditureType::parkRideTickets);
|
|
}
|
|
|
|
auto coordsCentre = coords.ToTileCentre();
|
|
guest->SetDestination(coordsCentre, 3);
|
|
guest->CurrentRide = rideIndex;
|
|
guest->SetState(PeepState::enteringRide);
|
|
guest->RideSubState = PeepRideSubState::approachShop;
|
|
|
|
guest->GuestTimeOnRide = 0;
|
|
ride->curNumCustomers++;
|
|
if (guest->PeepFlags & PEEP_FLAGS_TRACKING)
|
|
{
|
|
auto ft = Formatter();
|
|
guest->FormatNameTo(ft);
|
|
ride->formatNameTo(ft);
|
|
StringId string_id = ride->getRideTypeDescriptor().HasFlag(RtdFlag::describeAsInside)
|
|
? STR_PEEP_TRACKING_PEEP_IS_IN_X
|
|
: STR_PEEP_TRACKING_PEEP_IS_ON_X;
|
|
if (Config::Get().notifications.GuestUsedFacility)
|
|
{
|
|
News::AddItemToQueue(News::ItemType::peepOnRide, string_id, guest->Id, ft);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (guest->GuestHeadingToRideId == rideIndex)
|
|
guest->GuestHeadingToRideId = RideId::GetNull();
|
|
guest->AnimationImageIdOffset = _backupAnimationImageIdOffset;
|
|
guest->SetState(PeepState::buying);
|
|
guest->CurrentRide = rideIndex;
|
|
guest->SubState = 0;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x00693C9E
|
|
*/
|
|
std::pair<uint8_t, TileElement*> Peep::PerformNextAction()
|
|
{
|
|
uint8_t pathingResult = 0;
|
|
TileElement* tileResult = nullptr;
|
|
|
|
PeepActionType previousAction = Action;
|
|
|
|
if (Action == PeepActionType::idle)
|
|
Action = PeepActionType::walking;
|
|
|
|
auto* guest = As<Guest>();
|
|
if (State == PeepState::queuing && guest != nullptr)
|
|
{
|
|
if (guest->UpdateQueuePosition(previousAction))
|
|
{
|
|
return { pathingResult, tileResult };
|
|
}
|
|
}
|
|
|
|
std::optional<CoordsXY> loc;
|
|
if (loc = UpdateAction(); !loc.has_value())
|
|
{
|
|
pathingResult |= PATHING_DESTINATION_REACHED;
|
|
uint8_t result = 0;
|
|
|
|
if (guest != nullptr)
|
|
{
|
|
result = PathFinding::CalculateNextDestination(*guest);
|
|
}
|
|
else
|
|
{
|
|
auto* staff = As<Staff>();
|
|
result = staff->DoPathFinding();
|
|
}
|
|
|
|
if (result != 0)
|
|
return { pathingResult, tileResult };
|
|
|
|
if (loc = UpdateAction(); !loc.has_value())
|
|
return { pathingResult, tileResult };
|
|
}
|
|
|
|
auto newLoc = *loc;
|
|
CoordsXY truncatedNewLoc = newLoc.ToTileStart();
|
|
if (truncatedNewLoc == CoordsXY{ NextLoc })
|
|
{
|
|
int16_t height = GetZOnSlope(newLoc.x, newLoc.y);
|
|
MoveTo({ newLoc.x, newLoc.y, height });
|
|
return { pathingResult, tileResult };
|
|
}
|
|
|
|
if (MapIsEdge(newLoc))
|
|
{
|
|
if (guest != nullptr && guest->OutsideOfPark)
|
|
{
|
|
pathingResult |= PATHING_OUTSIDE_PARK;
|
|
}
|
|
PeepReturnToCentreOfTile(this);
|
|
return { pathingResult, tileResult };
|
|
}
|
|
|
|
TileElement* tileElement = MapGetFirstElementAt(newLoc);
|
|
if (tileElement == nullptr)
|
|
return { pathingResult, tileResult };
|
|
|
|
int16_t base_z = std::max(0, (z / 8) - 2);
|
|
int16_t top_z = (z / 8) + 1;
|
|
|
|
do
|
|
{
|
|
if (base_z > tileElement->BaseHeight)
|
|
continue;
|
|
if (top_z < tileElement->BaseHeight)
|
|
continue;
|
|
if (tileElement->IsGhost())
|
|
continue;
|
|
|
|
if (tileElement->GetType() == TileElementType::Path)
|
|
{
|
|
PeepInteractWithPath(this, { newLoc, tileElement });
|
|
tileResult = tileElement;
|
|
|
|
return { pathingResult, tileResult };
|
|
}
|
|
|
|
if (tileElement->GetType() == TileElementType::Track)
|
|
{
|
|
if (PeepInteractWithShop(this, { newLoc, tileElement }))
|
|
{
|
|
tileResult = tileElement;
|
|
return { pathingResult, tileResult };
|
|
}
|
|
}
|
|
else if (tileElement->GetType() == TileElementType::Entrance)
|
|
{
|
|
if (PeepInteractWithEntrance(this, { newLoc, tileElement }, pathingResult))
|
|
{
|
|
tileResult = tileElement;
|
|
return { pathingResult, tileResult };
|
|
}
|
|
}
|
|
} while (!(tileElement++)->IsLastForTile());
|
|
|
|
if (Is<Staff>() || (GetNextIsSurface()))
|
|
{
|
|
int16_t height = abs(TileElementHeight(newLoc) - z);
|
|
if (height <= 3 || (Is<Staff>() && height <= 32))
|
|
{
|
|
InteractionRideIndex = RideId::GetNull();
|
|
if (guest != nullptr && State == PeepState::queuing)
|
|
{
|
|
guest->RemoveFromQueue();
|
|
SetState(PeepState::one);
|
|
}
|
|
|
|
if (!MapIsLocationInPark(newLoc))
|
|
{
|
|
PeepReturnToCentreOfTile(this);
|
|
return { pathingResult, tileResult };
|
|
}
|
|
|
|
auto surfaceElement = MapGetSurfaceElementAt(newLoc);
|
|
if (surfaceElement == nullptr)
|
|
{
|
|
PeepReturnToCentreOfTile(this);
|
|
return { pathingResult, tileResult };
|
|
}
|
|
|
|
int16_t water_height = surfaceElement->GetWaterHeight();
|
|
if (water_height > 0)
|
|
{
|
|
PeepReturnToCentreOfTile(this);
|
|
return { pathingResult, tileResult };
|
|
}
|
|
|
|
auto* staff = As<Staff>();
|
|
if (staff != nullptr && !GetNextIsSurface())
|
|
{
|
|
// Prevent staff from leaving the path on their own unless they're allowed to mow.
|
|
if (!((staff->StaffOrders & STAFF_ORDERS_MOWING) && staff->StaffMowingTimeout >= 12))
|
|
{
|
|
PeepReturnToCentreOfTile(staff);
|
|
return { pathingResult, tileResult };
|
|
}
|
|
}
|
|
|
|
// The peep is on a surface and not on a path
|
|
NextLoc = { truncatedNewLoc, surfaceElement->GetBaseZ() };
|
|
SetNextFlags(0, false, true);
|
|
|
|
height = GetZOnSlope(newLoc.x, newLoc.y);
|
|
MoveTo({ newLoc.x, newLoc.y, height });
|
|
return { pathingResult, tileResult };
|
|
}
|
|
}
|
|
|
|
PeepReturnToCentreOfTile(this);
|
|
return { pathingResult, tileResult };
|
|
}
|
|
|
|
/**
|
|
* Gets the height including the bit depending on how far up the slope the peep
|
|
* is.
|
|
* rct2: 0x00694921
|
|
*/
|
|
int32_t Peep::GetZOnSlope(int32_t tile_x, int32_t tile_y)
|
|
{
|
|
if (tile_x == kLocationNull)
|
|
return 0;
|
|
|
|
if (GetNextIsSurface())
|
|
{
|
|
return TileElementHeight({ tile_x, tile_y });
|
|
}
|
|
|
|
uint8_t slope = GetNextDirection();
|
|
return NextLoc.z + MapHeightFromSlope({ tile_x, tile_y }, slope, GetNextIsSloped());
|
|
}
|
|
|
|
StringId GetRealNameStringIDFromPeepID(uint32_t id)
|
|
{
|
|
// Generate a name_string_idx from the peep Id using bit twiddling
|
|
uint16_t ax = static_cast<uint16_t>(id + 0xF0B);
|
|
uint16_t dx = 0;
|
|
static constexpr uint16_t twiddlingBitOrder[] = {
|
|
4, 9, 3, 7, 5, 8, 2, 1, 6, 0, 12, 11, 13, 10,
|
|
};
|
|
for (size_t i = 0; i < std::size(twiddlingBitOrder); i++)
|
|
{
|
|
dx |= (ax & (1 << twiddlingBitOrder[i]) ? 1 : 0) << i;
|
|
}
|
|
ax = dx & 0xF;
|
|
dx *= 4;
|
|
ax *= 4096;
|
|
dx += ax;
|
|
if (dx < ax)
|
|
{
|
|
dx += 0x1000;
|
|
}
|
|
dx /= 4;
|
|
dx += kRealNameStart;
|
|
return dx;
|
|
}
|
|
|
|
int32_t PeepCompare(const EntityId sprite_index_a, const EntityId sprite_index_b)
|
|
{
|
|
Peep const* peep_a = getGameState().entities.GetEntity<Peep>(sprite_index_a);
|
|
Peep const* peep_b = getGameState().entities.GetEntity<Peep>(sprite_index_b);
|
|
if (peep_a == nullptr || peep_b == nullptr)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
// Compare types
|
|
if (peep_a->Type != peep_b->Type)
|
|
{
|
|
return static_cast<int32_t>(peep_a->Type) - static_cast<int32_t>(peep_b->Type);
|
|
}
|
|
|
|
if (peep_a->Name == nullptr && peep_b->Name == nullptr)
|
|
{
|
|
if (getGameState().park.flags & PARK_FLAGS_SHOW_REAL_GUEST_NAMES)
|
|
{
|
|
// Potentially could find a more optional way of sorting dynamic real names
|
|
}
|
|
else
|
|
{
|
|
// Simple ID comparison for when both peeps use a number or a generated name
|
|
return peep_a->PeepId - peep_b->PeepId;
|
|
}
|
|
}
|
|
|
|
// Compare their names as strings
|
|
char nameA[256]{};
|
|
Formatter ft;
|
|
peep_a->FormatNameTo(ft);
|
|
OpenRCT2::FormatStringLegacy(nameA, sizeof(nameA), STR_STRINGID, ft.Data());
|
|
|
|
char nameB[256]{};
|
|
ft.Rewind();
|
|
peep_b->FormatNameTo(ft);
|
|
OpenRCT2::FormatStringLegacy(nameB, sizeof(nameB), STR_STRINGID, ft.Data());
|
|
return String::logicalCmp(nameA, nameB);
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069926C
|
|
*/
|
|
void PeepUpdateNames()
|
|
{
|
|
auto& gameState = getGameState();
|
|
auto& config = Config::Get().general;
|
|
|
|
if (config.ShowRealNamesOfGuests)
|
|
gameState.park.flags |= PARK_FLAGS_SHOW_REAL_GUEST_NAMES;
|
|
else
|
|
gameState.park.flags &= ~PARK_FLAGS_SHOW_REAL_GUEST_NAMES;
|
|
|
|
if (config.ShowRealNamesOfStaff)
|
|
gameState.park.flags |= PARK_FLAGS_SHOW_REAL_STAFF_NAMES;
|
|
else
|
|
gameState.park.flags &= ~PARK_FLAGS_SHOW_REAL_STAFF_NAMES;
|
|
|
|
auto intent = Intent(INTENT_ACTION_REFRESH_GUEST_LIST);
|
|
ContextBroadcastIntent(&intent);
|
|
GfxInvalidateScreen();
|
|
}
|
|
|
|
void IncrementGuestsInPark()
|
|
{
|
|
auto& gameState = getGameState();
|
|
if (gameState.park.numGuestsInPark < UINT32_MAX)
|
|
{
|
|
gameState.park.numGuestsInPark++;
|
|
}
|
|
else
|
|
{
|
|
Guard::Fail("Attempt to increment guests in park above max value (65535).");
|
|
}
|
|
}
|
|
|
|
void IncrementGuestsHeadingForPark()
|
|
{
|
|
auto& gameState = getGameState();
|
|
if (gameState.park.numGuestsHeadingForPark < UINT32_MAX)
|
|
{
|
|
gameState.park.numGuestsHeadingForPark++;
|
|
}
|
|
else
|
|
{
|
|
Guard::Fail("Attempt to increment guests heading for park above max value (65535).");
|
|
}
|
|
}
|
|
|
|
void DecrementGuestsInPark()
|
|
{
|
|
auto& gameState = getGameState();
|
|
if (gameState.park.numGuestsInPark > 0)
|
|
{
|
|
gameState.park.numGuestsInPark--;
|
|
}
|
|
else
|
|
{
|
|
LOG_ERROR("Attempt to decrement guests in park below zero.");
|
|
}
|
|
}
|
|
|
|
void DecrementGuestsHeadingForPark()
|
|
{
|
|
auto& gameState = getGameState();
|
|
|
|
if (gameState.park.numGuestsHeadingForPark > 0)
|
|
{
|
|
gameState.park.numGuestsHeadingForPark--;
|
|
}
|
|
else
|
|
{
|
|
LOG_ERROR("Attempt to decrement guests heading for park below zero.");
|
|
}
|
|
}
|
|
|
|
static void GuestReleaseBalloon(Guest* peep, int16_t spawn_height)
|
|
{
|
|
if (peep->HasItem(ShopItem::Balloon))
|
|
{
|
|
peep->RemoveItem(ShopItem::Balloon);
|
|
|
|
if (peep->AnimationGroup == PeepAnimationGroup::Balloon && peep->x != kLocationNull)
|
|
{
|
|
Balloon::Create({ peep->x, peep->y, spawn_height }, peep->BalloonColour, false);
|
|
peep->WindowInvalidateFlags |= PEEP_INVALIDATE_PEEP_INVENTORY;
|
|
peep->UpdateAnimationGroup();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069A512
|
|
*/
|
|
void Peep::RemoveFromRide()
|
|
{
|
|
auto* guest = As<Guest>();
|
|
if (guest != nullptr && State == PeepState::queuing)
|
|
{
|
|
guest->RemoveFromQueue();
|
|
}
|
|
StateReset();
|
|
}
|
|
|
|
void Peep::SetDestination(const CoordsXY& coords)
|
|
{
|
|
DestinationX = static_cast<uint16_t>(coords.x);
|
|
DestinationY = static_cast<uint16_t>(coords.y);
|
|
}
|
|
|
|
void Peep::SetDestination(const CoordsXY& coords, int32_t tolerance)
|
|
{
|
|
SetDestination(coords);
|
|
DestinationTolerance = tolerance;
|
|
}
|
|
|
|
CoordsXY Peep::GetDestination() const
|
|
{
|
|
return CoordsXY{ DestinationX, DestinationY };
|
|
}
|
|
|
|
void Peep::Serialise(DataSerialiser& stream)
|
|
{
|
|
EntityBase::Serialise(stream);
|
|
if (stream.IsLoading())
|
|
{
|
|
Name = nullptr;
|
|
}
|
|
stream << NextLoc;
|
|
stream << NextFlags;
|
|
stream << State;
|
|
stream << SubState;
|
|
stream << AnimationGroup;
|
|
stream << TshirtColour;
|
|
stream << TrousersColour;
|
|
stream << DestinationX;
|
|
stream << DestinationY;
|
|
stream << DestinationTolerance;
|
|
stream << Var37;
|
|
stream << Energy;
|
|
stream << EnergyTarget;
|
|
stream << Mass;
|
|
// stream << base.WindowInvalidateFlags;
|
|
stream << CurrentRide;
|
|
stream << CurrentRideStation;
|
|
stream << CurrentTrain;
|
|
stream << CurrentCar;
|
|
stream << CurrentSeat;
|
|
stream << SpecialSprite;
|
|
stream << AnimationType;
|
|
stream << NextAnimationType;
|
|
stream << AnimationImageIdOffset;
|
|
stream << Action;
|
|
stream << AnimationFrameNum;
|
|
stream << StepProgress;
|
|
stream << PeepDirection;
|
|
stream << InteractionRideIndex;
|
|
stream << PeepId;
|
|
stream << PathCheckOptimisation;
|
|
stream << PathfindGoal;
|
|
stream << PathfindHistory;
|
|
stream << WalkingAnimationFrameNum;
|
|
stream << PeepFlags;
|
|
}
|
|
|
|
void Peep::Paint(PaintSession& session, int32_t imageDirection) const
|
|
{
|
|
PROFILED_FUNCTION();
|
|
|
|
if (LightFx::IsAvailable())
|
|
{
|
|
if (Is<Staff>())
|
|
{
|
|
auto loc = GetLocation();
|
|
switch (Orientation)
|
|
{
|
|
case 0:
|
|
loc.x -= 10;
|
|
break;
|
|
case 8:
|
|
loc.y += 10;
|
|
break;
|
|
case 16:
|
|
loc.x += 10;
|
|
break;
|
|
case 24:
|
|
loc.y -= 10;
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
LightFx::Add3DLight(*this, 0, loc, LightType::Spot1);
|
|
}
|
|
}
|
|
|
|
if (session.DPI.zoom_level > ZoomLevel{ 2 })
|
|
{
|
|
return;
|
|
}
|
|
|
|
PeepAnimationType actionAnimationGroup = AnimationType;
|
|
uint8_t imageOffset = AnimationImageIdOffset;
|
|
|
|
if (Action == PeepActionType::idle)
|
|
{
|
|
actionAnimationGroup = NextAnimationType;
|
|
imageOffset = 0;
|
|
}
|
|
|
|
auto& objManager = GetContext()->GetObjectManager();
|
|
auto* animObj = objManager.GetLoadedObject<PeepAnimationsObject>(AnimationObjectIndex);
|
|
|
|
uint32_t baseImageId = animObj->GetPeepAnimation(AnimationGroup, actionAnimationGroup).baseImage;
|
|
|
|
// Offset frame onto the base image, using rotation except for the 'picked up' state
|
|
if (actionAnimationGroup != PeepAnimationType::Hanging)
|
|
baseImageId += (imageDirection >> 3) + imageOffset * 4;
|
|
else
|
|
baseImageId += imageOffset;
|
|
|
|
auto imageId = ImageId(baseImageId, TshirtColour, TrousersColour);
|
|
|
|
// In the following 4 calls to PaintAddImageAsParent/PaintAddImageAsChild, we add 5 (instead of 3) to the
|
|
// bound_box_offset_z to make sure peeps are drawn on top of railways
|
|
auto bb = BoundBoxXYZ{ { 0, 0, z + 5 }, { 1, 1, 11 } };
|
|
auto offset = CoordsXYZ{ 0, 0, z };
|
|
PaintAddImageAsParent(session, imageId, { 0, 0, z }, bb);
|
|
|
|
auto* guest = As<Guest>();
|
|
if (guest == nullptr)
|
|
return;
|
|
|
|
// Can't display any accessories whilst drowning or clapping
|
|
if (Action == PeepActionType::drowning || Action == PeepActionType::clap)
|
|
return;
|
|
|
|
// There are only 6 walking frames available for each item,
|
|
// as well as 1 sprite for sitting and 1 for standing still.
|
|
auto itemFrame = imageOffset % 6;
|
|
if (actionAnimationGroup == PeepAnimationType::WatchRide)
|
|
itemFrame = 6;
|
|
else if (actionAnimationGroup == PeepAnimationType::SittingIdle)
|
|
itemFrame = 7;
|
|
|
|
if (AnimationGroup == PeepAnimationGroup::Hat)
|
|
{
|
|
auto itemOffset = kPeepSpriteHatItemStart;
|
|
imageId = ImageId(itemOffset + (imageDirection >> 3) + itemFrame * 4, guest->HatColour);
|
|
PaintAddImageAsChild(session, imageId, offset, bb);
|
|
return;
|
|
}
|
|
|
|
if (AnimationGroup == PeepAnimationGroup::Balloon)
|
|
{
|
|
auto itemOffset = kPeepSpriteBalloonItemStart;
|
|
imageId = ImageId(itemOffset + (imageDirection >> 3) + itemFrame * 4, guest->BalloonColour);
|
|
PaintAddImageAsChild(session, imageId, offset, bb);
|
|
return;
|
|
}
|
|
|
|
if (AnimationGroup == PeepAnimationGroup::Umbrella)
|
|
{
|
|
auto itemOffset = kPeepSpriteUmbrellaItemStart;
|
|
imageId = ImageId(itemOffset + (imageDirection >> 3) + itemFrame * 4, guest->UmbrellaColour);
|
|
PaintAddImageAsChild(session, imageId, offset, bb);
|
|
return;
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* rct2: 0x0069A98C
|
|
*/
|
|
void Peep::ResetPathfindGoal()
|
|
{
|
|
PathfindGoal.SetNull();
|
|
PathfindGoal.direction = kInvalidDirection;
|
|
}
|