/*****************************************************************************
 * Copyright (c) 2014-2026 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 "Staff.h"

#include "../Context.h"
#include "../Diagnostic.h"
#include "../GameState.h"
#include "../actions/StaffSetOrdersAction.h"
#include "../audio/Audio.h"
#include "../core/DataSerialiser.h"
#include "../entity/EntityList.h"
#include "../entity/EntityRegistry.h"
#include "../interface/Viewport.h"
#include "../localisation/StringIds.h"
#include "../object/ObjectManager.h"
#include "../object/PathAdditionEntry.h"
#include "../object/PeepAnimationsObject.h"
#include "../object/SmallSceneryEntry.h"
#include "../paint/tile_element/Paint.TileElement.h"
#include "../peep/GuestPathfinding.h"
#include "../ride/RideData.h"
#include "../ride/Track.h"
#include "../ride/Vehicle.h"
#include "../scenario/Scenario.h"
#include "../util/Util.h"
#include "../windows/Intent.h"
#include "../world/Footpath.h"
#include "../world/Map.h"
#include "../world/Scenery.h"
#include "../world/Wall.h"
#include "../world/tile_element/EntranceElement.h"
#include "../world/tile_element/PathElement.h"
#include "../world/tile_element/Slope.h"
#include "../world/tile_element/SmallSceneryElement.h"
#include "../world/tile_element/SurfaceElement.h"
#include "../world/tile_element/TrackElement.h"
#include "PatrolArea.h"
#include "Peep.h"

#include <cassert>
#include <iterator>

using namespace OpenRCT2;

template<>
bool EntityBase::Is<Staff>() const
{
    return Type == EntityType::staff;
}

/**
 *
 *  rct2: 0x006C0905
 */
bool Staff::IsLocationInPatrol(const CoordsXY& loc) const
{
    // Check if location is in the park
    if (!MapIsLocationOwnedOrHasRights(loc))
        return false;

    // Check if staff has patrol area
    if (!HasPatrolArea())
        return true;

    return IsPatrolAreaSet(loc);
}

// Check whether the location x,y is inside and on the edge of the
// patrol zone for mechanic.
bool Staff::IsLocationOnPatrolEdge(const CoordsXY& loc) const
{
    bool onZoneEdge = false;
    for (uint8_t neighbourDir = 0; !onZoneEdge && neighbourDir <= 7; neighbourDir++)
    {
        auto neighbourPos = loc + CoordsDirectionDelta[neighbourDir];
        onZoneEdge = !IsLocationInPatrol(neighbourPos);
    }
    return onZoneEdge;
}

bool Staff::CanIgnoreWideFlag(const CoordsXYZ& staffPos, TileElement* path) const
{
    /* Wide flags can potentially wall off parts of a staff patrol zone
     * for the heuristic search.
     * This function provide doors through such "walls" by defining
     * the conditions under which staff can ignore the wide path flag. */
    /* Staff can ignore the wide flag on a path on the edge of the patrol
     * zone based on its adjacent tiles that are also in the patrol zone
     * but not on the patrol zone edge:
     * Basic points of interest are:
     * - how many such tiles there are;
     * - whether there are connected paths on those tiles;
     * - whether the connected paths have the wide flag set.
     * If there are no such tiles, the path is a concave corner of
     * the patrol zone and the wide flag can be ignored.
     * If there is one such tile, the path is on a straight side of the
     * patrol zone. If this one tile is either a connected wide path or
     * not a connected path, the wide flag can be ignored.
     * If there are two such tiles, the path is a convex corner of the
     * patrol zone. If at most one of these tiles is a connected path or
     * both of these tiles are connected wide paths, the wide flag can be
     * ignored. */

    if (!IsLocationOnPatrolEdge(staffPos))
    {
        return false;
    }

    /* Check the connected adjacent paths that are also inside the patrol
     * zone but are not on the patrol zone edge have the wide flag set. */
    uint8_t total = 0;
    uint8_t pathcount = 0;
    uint8_t widecount = 0;
    for (Direction adjac_dir : kAllDirections)
    {
        auto adjacPos = staffPos + CoordsXYZ{ CoordsDirectionDelta[adjac_dir].x, CoordsDirectionDelta[adjac_dir].y, 0 };

        /* Ignore adjacent tiles outside the patrol zone. */
        if (!IsLocationInPatrol(adjacPos))
            continue;

        /* Ignore adjacent tiles on the patrol zone edge. */
        if (IsLocationOnPatrolEdge(adjacPos))
            continue;

        /* Adjacent tile is inside the patrol zone but not on the
         * patrol zone edge. */
        total++;

        /* Check if path has an edge in adjac_dir */
        if (!(path->AsPath()->GetEdges() & (1u << adjac_dir)))
        {
            continue;
        }

        if (path->AsPath()->IsSloped())
        {
            if (path->AsPath()->GetSlopeDirection() == adjac_dir)
            {
                adjacPos.z += kPathHeightStep;
            }
        }

        /* Search through all adjacent map elements */
        TileElement* test_element = MapGetFirstElementAt(adjacPos);
        if (test_element == nullptr)
            return false;
        bool pathfound = false;
        bool widefound = false;
        do
        {
            if (test_element->GetType() != TileElementType::Path)
            {
                continue;
            }

            /* test_element is a path */
            const auto* adjacentPathElement = test_element->AsPath();
            if (!FootpathIsZAndDirectionValid(*adjacentPathElement, adjacPos.z / kCoordsZStep, adjac_dir))
                continue;

            /* test_element is a connected path */
            if (!pathfound)
            {
                pathfound = true;
                pathcount++;
            }

            if (adjacentPathElement->IsWide())
            {
                if (!widefound)
                {
                    widefound = true;
                    widecount++;
                }
            }
        } while (!(test_element++)->IsLastForTile());
    }

    switch (total)
    {
        case 0: /* Concave corner */
            return true;
        case 1: /* Straight side */
        case 2: /* Convex corner */
            if (pathcount <= total - 1 || widecount == total)
            {
                return true;
            }
    }

    return false;
}

/**
 *
 *  rct2: 0x006C095B
 *  returns 0xF if not in a valid patrol area
 */
uint8_t Staff::GetValidPatrolDirections(const CoordsXY& loc) const
{
    uint8_t directions = 0;

    if (IsLocationInPatrol({ loc.x - kCoordsXYStep, loc.y }))
    {
        directions |= (1 << 0);
    }

    if (IsLocationInPatrol({ loc.x, loc.y + kCoordsXYStep }))
    {
        directions |= (1 << 1);
    }

    if (IsLocationInPatrol({ loc.x + kCoordsXYStep, loc.y }))
    {
        directions |= (1 << 2);
    }

    if (IsLocationInPatrol({ loc.x, loc.y - kCoordsXYStep }))
    {
        directions |= (1 << 3);
    }

    if (directions == 0)
    {
        directions = 0xF;
    }

    return directions;
}

/**
 *
 *  rct2: 0x006C1955
 */
void Staff::ResetStats()
{
    for (auto peep : EntityList<Staff>())
    {
        peep->SetHireDate(GetDate().GetMonthsElapsed());
        peep->StaffLawnsMown = 0;
        peep->StaffRidesFixed = 0;
        peep->StaffGardensWatered = 0;
        peep->StaffRidesInspected = 0;
        peep->StaffLitterSwept = 0;
        peep->StaffVandalsStopped = 0;
        peep->StaffBinsEmptied = 0;
    }
}

bool Staff::IsPatrolAreaSet(const CoordsXY& coords) const
{
    if (PatrolInfo != nullptr)
    {
        return PatrolInfo->Get(coords);
    }
    return false;
}

void Staff::SetPatrolArea(const CoordsXY& coords, bool value)
{
    if (PatrolInfo == nullptr)
    {
        if (value)
        {
            PatrolInfo = new PatrolArea();
        }
        else
        {
            return;
        }
    }

    PatrolInfo->Set(coords, value);
}

void Staff::SetPatrolArea(const MapRange& range, bool value)
{
    for (int32_t yy = range.GetY1(); yy <= range.GetY2(); yy += kCoordsXYStep)
    {
        for (int32_t xx = range.GetX1(); xx <= range.GetX2(); xx += kCoordsXYStep)
        {
            SetPatrolArea({ xx, yy }, value);
        }
    }
}

void Staff::ClearPatrolArea()
{
    delete PatrolInfo;
    PatrolInfo = nullptr;
}

bool Staff::HasPatrolArea() const
{
    return PatrolInfo == nullptr ? false : !PatrolInfo->IsEmpty();
}

/**
 *
 *  rct2: 0x006BFBE8
 *
 * Returns kInvalidDirection when no nearby litter or unpathable litter
 */
Direction Staff::HandymanDirectionToNearestLitter() const
{
    // Maximum manhattan distance that litter can be for a handyman to seek to it
    constexpr auto kTileRadius = 3;
    constexpr auto kLookupRadius = kCoordsXYStep * kTileRadius;

    auto nearestLitterDist = std::numeric_limits<int32_t>::max();
    Litter* nearestLitter = nullptr;

    for (int32_t tileX = x - kLookupRadius; tileX <= x + kLookupRadius; tileX += kCoordsXYStep)
    {
        for (int32_t tileY = y - kLookupRadius; tileY <= y + kLookupRadius; tileY += kCoordsXYStep)
        {
            for (auto* litter : EntityTileList<Litter>({ tileX, tileY }))
            {
                // Calculate Manhattan distance with z-weighting
                uint16_t distance = std::abs(litter->x - x) + std::abs(litter->y - y) + std::abs(litter->z - z) * 4;

                if (distance < nearestLitterDist)
                {
                    nearestLitterDist = distance;
                    nearestLitter = litter;
                }
            }
        }
    }

    if (nearestLitterDist > kLookupRadius || nearestLitter == nullptr)
    {
        return kInvalidDirection;
    }

    auto litterTile = CoordsXY{ nearestLitter->x, nearestLitter->y }.ToTileStart();

    if (!IsLocationInPatrol(litterTile))
    {
        return kInvalidDirection;
    }

    Direction nextDirection = DirectionFromTo(CoordsXY(x, y), litterTile.ToTileCentre());

    CoordsXY nextTile = litterTile.ToTileStart() - CoordsDirectionDelta[nextDirection];

    int16_t nextZ = ((z + kCoordsZStep) & 0xFFF0) / kCoordsZStep;

    TileElement* tileElement = MapGetFirstElementAt(nextTile);
    if (tileElement == nullptr)
        return kInvalidDirection;
    do
    {
        if (tileElement->BaseHeight != nextZ)
            continue;
        if (tileElement->GetType() == TileElementType::Entrance || tileElement->GetType() == TileElementType::Track)
        {
            return kInvalidDirection;
        }
    } while (!(tileElement++)->IsLastForTile());

    nextTile = CoordsXY(x, y).ToTileStart() + CoordsDirectionDelta[nextDirection];

    tileElement = MapGetFirstElementAt(nextTile);
    if (tileElement == nullptr)
        return kInvalidDirection;

    do
    {
        if (tileElement->BaseHeight != nextZ)
            continue;
        if (tileElement->GetType() == TileElementType::Entrance || tileElement->GetType() == TileElementType::Track)
        {
            return kInvalidDirection;
        }
    } while (!(tileElement++)->IsLastForTile());

    return nextDirection;
}

/**
 *
 *  rct2: 0x006BF931
 */
uint8_t Staff::HandymanDirectionToUncutGrass(uint8_t valid_directions) const
{
    if (!(GetNextIsSurface()))
    {
        auto surfaceElement = MapGetSurfaceElementAt(NextLoc);
        if (surfaceElement == nullptr)
            return kInvalidDirection;

        if (NextLoc.z != surfaceElement->GetBaseZ())
            return kInvalidDirection;

        if (GetNextIsSloped())
        {
            if (surfaceElement->GetSlope() != kPathSlopeToLandSlope[GetNextDirection()])
                return kInvalidDirection;
        }
        else if (surfaceElement->GetSlope() != kTileSlopeFlat)
            return kInvalidDirection;
    }

    uint8_t chosenDirection = ScenarioRand() & 0x3;
    for (uint8_t i = 0; i < 4; ++i, ++chosenDirection)
    {
        chosenDirection &= 0x3;

        if (!(valid_directions & (1 << chosenDirection)))
        {
            continue;
        }

        CoordsXY chosenTile = CoordsXY{ NextLoc } + CoordsDirectionDelta[chosenDirection];

        if (!MapIsLocationValid(chosenTile))
            continue;

        auto surfaceElement = MapGetSurfaceElementAt(chosenTile);
        if (surfaceElement != nullptr)
        {
            if (std::abs(surfaceElement->GetBaseZ() - NextLoc.z) <= 2 * kCoordsZStep)
            {
                if (surfaceElement->CanGrassGrow() && (surfaceElement->GetGrassLength() & 0x7) >= GRASS_LENGTH_CLEAR_1)
                {
                    return chosenDirection;
                }
            }
        }
    }
    return kInvalidDirection;
}

/**
 *
 *  rct2: 0x006BFD9C
 */
Direction Staff::HandymanDirectionRandSurface(uint8_t validDirections) const
{
    Direction newDirection = ScenarioRand() % kNumOrthogonalDirections;
    for (int32_t i = 0; i < kNumOrthogonalDirections; ++i, ++newDirection)
    {
        newDirection %= kNumOrthogonalDirections;
        if (!(validDirections & (1 << newDirection)))
            continue;

        CoordsXY chosenTile = CoordsXY{ NextLoc } + CoordsDirectionDelta[newDirection];

        if (MapSurfaceIsBlocked(chosenTile))
            continue;

        break;
    }
    // If it tries all directions this is required
    // to make it back to the first direction and
    // override validDirections
    newDirection %= kNumOrthogonalDirections;
    return newDirection;
}

/**
 *
 *  rct2: 0x006BFBA8
 */
bool Staff::DoHandymanPathFinding()
{
    StaffMowingTimeout++;

    Direction litterDirection = kInvalidDirection;
    uint8_t validDirections = GetValidPatrolDirections(NextLoc);

    if ((StaffOrders & STAFF_ORDERS_SWEEPING) && ((getGameState().currentTicks + Id.ToUnderlying()) & 0xFFF) > 110)
    {
        litterDirection = HandymanDirectionToNearestLitter();
    }

    Direction newDirection = kInvalidDirection;
    if (litterDirection == kInvalidDirection && (StaffOrders & STAFF_ORDERS_MOWING) && StaffMowingTimeout >= 12)
    {
        newDirection = HandymanDirectionToUncutGrass(validDirections);
    }

    if (newDirection == kInvalidDirection)
    {
        if (GetNextIsSurface())
        {
            newDirection = HandymanDirectionRandSurface(validDirections);
        }
        else
        {
            auto* pathElement = MapGetPathElementAt(TileCoordsXYZ{ NextLoc });

            if (pathElement == nullptr)
                return true;

            uint8_t pathDirections = (pathElement->GetEdges() & validDirections) & 0xF;
            if (pathDirections == 0)
            {
                newDirection = HandymanDirectionRandSurface(validDirections);
            }
            else
            {
                bool chooseRandom = true;
                if (litterDirection != kInvalidDirection && pathDirections & (1 << litterDirection))
                {
                    /// Check whether path is a queue path and connected to a ride
                    bool connectedQueue = (pathElement->IsQueue() && !pathElement->GetRideIndex().IsNull());
                    /// When in a queue path make the probability of following litter much lower (10% instead of 90%)
                    /// as handymen often get stuck when there is litter on a normal path next to a queue they are in
                    uint32_t chooseRandomProbability = connectedQueue ? 0xE666 : 0x1999;
                    if ((ScenarioRand() & 0xFFFF) >= chooseRandomProbability)
                    {
                        chooseRandom = false;
                        newDirection = litterDirection;
                    }
                }
                else
                {
                    pathDirections &= ~(1 << DirectionReverse(PeepDirection));
                    if (pathDirections == 0)
                    {
                        pathDirections |= 1 << DirectionReverse(PeepDirection);
                    }
                }

                if (chooseRandom)
                {
                    do
                    {
                        newDirection = ScenarioRand() & 3;
                    } while ((pathDirections & (1 << newDirection)) == 0);
                }
            }
        }
    }

    // newDirection can only contain a cardinal direction at this point, no diagonals
    assert(DirectionValid(newDirection));

    CoordsXY chosenTile = CoordsXY{ NextLoc } + CoordsDirectionDelta[newDirection];

    while (!MapIsLocationValid(chosenTile))
    {
        newDirection = HandymanDirectionRandSurface(validDirections);
        chosenTile = CoordsXY{ NextLoc } + CoordsDirectionDelta[newDirection];
    }

    PeepDirection = newDirection;
    SetDestination(chosenTile + CoordsXY{ 16, 16 }, 3);
    if (State == PeepState::queuing)
    {
        DestinationTolerance = (ScenarioRand() & 7) + 2;
    }
    return false;
}

Direction Staff::DirectionSurface(Direction initialDirection) const
{
    uint8_t direction = initialDirection;
    for (int32_t i = 0; i < 3; ++i)
    {
        // Looks left and right from initial direction
        switch (i)
        {
            case 1:
                direction++;
                if (ScenarioRand() & 1)
                {
                    direction -= 2;
                }
                break;
            case 2:
                direction -= 2;
                break;
        }

        direction &= 3;

        if (WallInTheWay({ NextLoc, NextLoc.z, NextLoc.z + kPeepClearanceHeight }, direction))
            continue;

        if (WallInTheWay({ NextLoc, NextLoc.z, NextLoc.z + kPeepClearanceHeight }, DirectionReverse(direction)))
            continue;

        CoordsXY chosenTile = CoordsXY{ NextLoc } + CoordsDirectionDelta[direction];

        if (!MapSurfaceIsBlocked(chosenTile))
        {
            return direction;
        }
    }
    return initialDirection;
}

/**
 *
 *  rct2: 0x006BFF45
 */
Direction Staff::MechanicDirectionSurface() const
{
    Direction direction = ScenarioRand() & 3;

    auto ride = GetRide(CurrentRide);
    if (ride != nullptr && (State == PeepState::answering || State == PeepState::headingToInspection) && (ScenarioRand() & 1))
    {
        auto location = ride->getStation(CurrentRideStation).Exit;
        if (location.IsNull())
        {
            location = ride->getStation(CurrentRideStation).Entrance;
        }

        direction = DirectionFromTo(CoordsXY(x, y), location.ToCoordsXY());
    }

    return DirectionSurface(direction);
}

/**
 *
 *  rct2: 0x006C02D1
 */
Direction Staff::MechanicDirectionPathRand(uint8_t pathDirections) const
{
    if (ScenarioRand() & 1)
    {
        if (pathDirections & (1 << PeepDirection))
            return PeepDirection;
    }

    // Modified from original to spam scenario_rand less
    uint8_t direction = ScenarioRand() & 3;
    for (int32_t i = 0; i < 4; ++i, ++direction)
    {
        direction &= 3;
        if (pathDirections & (1 << direction))
            return direction;
    }
    // This will never happen as pathDirections always has a bit set.
    return PeepDirection;
}

/**
 *
 *  rct2: 0x006C0121
 */
Direction Staff::MechanicDirectionPath(uint8_t validDirections, PathElement* pathElement)
{
    uint32_t pathDirections = pathElement->GetEdges();
    pathDirections &= validDirections;

    if (pathDirections == 0)
    {
        return MechanicDirectionSurface();
    }

    // Check if this is dead end - i.e. only way out is the reverse direction.
    pathDirections &= ~(1 << DirectionReverse(PeepDirection));
    if (pathDirections == 0)
    {
        pathDirections |= (1 << DirectionReverse(PeepDirection));
    }

    Direction direction = Numerics::bitScanForward(pathDirections);
    pathDirections &= ~(1 << direction);
    if (pathDirections == 0)
    {
        if (State != PeepState::answering && State != PeepState::headingToInspection)
        {
            return direction;
        }

        if (SubState != 2)
        {
            return direction;
        }
        SubState = 3;
    }

    pathDirections |= (1 << direction);

    // Mechanic is heading to ride (either broken down or for inspection).
    auto ride = GetRide(CurrentRide);
    if (ride != nullptr && (State == PeepState::answering || State == PeepState::headingToInspection))
    {
        /* Find location of the exit for the target ride station
         * or if the ride has no exit, the entrance. */
        TileCoordsXYZD location = ride->getStation(CurrentRideStation).Exit;
        if (location.IsNull())
        {
            location = ride->getStation(CurrentRideStation).Entrance;

            // If no entrance is present either. This is an incorrect state.
            if (location.IsNull())
            {
                return MechanicDirectionPathRand(pathDirections);
            }
        }

        const auto goalPos = TileCoordsXYZ{ location };
        Direction pathfindDirection = PathFinding::ChooseDirection(
            TileCoordsXYZ{ NextLoc }, goalPos, *this, false, RideId::GetNull());
        if (pathfindDirection == kInvalidDirection)
        {
            /* Heuristic search failed for all directions.
             * Reset the PathfindGoal - this means that the PathfindHistory
             * will be reset in the next call to GuestPathfinding::ChooseDirection().
             * This lets the heuristic search "try again" in case the player has
             * edited the path layout or the mechanic was already stuck in the
             * save game (e.g. with a worse version of the pathfinding). */
            ResetPathfindGoal();
            return MechanicDirectionPathRand(pathDirections);
        }

        return pathfindDirection;
    }
    return MechanicDirectionPathRand(pathDirections);
}

/**
 *
 *  rct2: 0x006BFF2C
 */
bool Staff::DoMechanicPathFinding()
{
    uint8_t validDirections = GetValidPatrolDirections(NextLoc);
    Direction newDirection = kInvalidDirection;
    if (GetNextIsSurface())
    {
        newDirection = MechanicDirectionSurface();
    }
    else
    {
        auto* pathElement = MapGetPathElementAt(TileCoordsXYZ{ NextLoc });
        if (pathElement == nullptr)
            return true;

        newDirection = MechanicDirectionPath(validDirections, pathElement);
    }

    // countof(CoordsDirectionDelta)
    assert(DirectionValid(newDirection));

    CoordsXY chosenTile = CoordsXY{ NextLoc } + CoordsDirectionDelta[newDirection];

    while (!MapIsLocationValid(chosenTile))
    {
        newDirection = MechanicDirectionSurface();
        chosenTile = CoordsXY{ NextLoc } + CoordsDirectionDelta[newDirection];
    }

    PeepDirection = newDirection;
    auto tolerance = (ScenarioRand() & 7) + 2;
    SetDestination(chosenTile + CoordsXY{ 16, 16 }, tolerance);

    return false;
}

/**
 *
 *  rct2: 0x006C050B
 */
Direction Staff::DirectionPath(uint8_t validDirections, PathElement* pathElement) const
{
    uint32_t pathDirections = pathElement->GetEdges();
    if (State != PeepState::answering && State != PeepState::headingToInspection)
    {
        pathDirections &= validDirections;
    }

    if (pathDirections == 0)
    {
        return DirectionSurface(ScenarioRand() & 3);
    }

    pathDirections &= ~(1u << DirectionReverse(PeepDirection));
    if (pathDirections == 0)
    {
        pathDirections |= (1u << DirectionReverse(PeepDirection));
    }

    Direction direction = Numerics::bitScanForward(pathDirections);
    // If this is the only direction they can go, then go
    if (pathDirections == (1u << direction))
    {
        return direction;
    }

    direction = ScenarioRand() & 3;
    for (uint8_t i = 0; i < kNumOrthogonalDirections; ++i, direction = DirectionNext(direction))
    {
        if (pathDirections & (1u << direction))
            return direction;
    }

    // This will never happen as pathDirections will always have a bit set
    return direction;
}

/**
 *
 *  rct2: 0x006C0351
 */
bool Staff::DoMiscPathFinding()
{
    uint8_t validDirections = GetValidPatrolDirections(NextLoc);

    Direction newDirection = kInvalidDirection;
    if (GetNextIsSurface())
    {
        newDirection = DirectionSurface(ScenarioRand() & 3);
    }
    else
    {
        auto* pathElement = MapGetPathElementAt(TileCoordsXYZ{ NextLoc });
        if (pathElement == nullptr)
            return true;

        newDirection = DirectionPath(validDirections, pathElement);
    }

    CoordsXY chosenTile = CoordsXY{ NextLoc } + CoordsDirectionDelta[newDirection];

    while (!MapIsLocationValid(chosenTile))
    {
        newDirection = DirectionSurface(ScenarioRand() & 3);
        chosenTile = CoordsXY{ NextLoc } + CoordsDirectionDelta[newDirection];
    }

    PeepDirection = newDirection;
    auto tolerance = (ScenarioRand() & 7) + 2;
    SetDestination(chosenTile + CoordsXY{ 16, 16 }, tolerance);

    return false;
}

bool Staff::IsMechanicHeadingToFixRideBlockingPath()
{
    if (!IsMechanic())
        return false;

    auto tileCoords = TileCoordsXYZ(CoordsXYZ{ GetDestination(), NextLoc.z });
    auto trackElement = MapGetFirstTileElementWithBaseHeightBetween<TrackElement>(
        { tileCoords, tileCoords.z + kPathHeightStep });
    if (trackElement == nullptr)
        return false;

    auto ride = GetRide(trackElement->GetRideIndex());
    if (ride == nullptr)
        return false;

    return ride->id == CurrentRide;
}

/**
 *
 *  rct2: 0x006C086D
 */
void Staff::EntertainerUpdateNearbyPeeps() const
{
    // Iterate over tiles within a 3-tile radius (96 units)
    constexpr auto kTileRadius = 3;
    constexpr auto kLookupRadius = kCoordsXYStep * kTileRadius;

    for (int32_t tileX = x - kLookupRadius; tileX <= x + kLookupRadius; tileX += kCoordsXYStep)
    {
        for (int32_t tileY = y - kLookupRadius; tileY <= y + kLookupRadius; tileY += kCoordsXYStep)
        {
            for (auto* guest : EntityTileList<Guest>({ tileX, tileY }))
            {
                if (guest->x == kLocationNull)
                    continue;

                int16_t z_dist = std::abs(z - guest->z);
                if (z_dist > kTileRadius / 2)
                    continue;

                int16_t x_dist = std::abs(x - guest->x);
                int16_t y_dist = std::abs(y - guest->y);

                if (x_dist > kLookupRadius)
                    continue;

                if (y_dist > kLookupRadius)
                    continue;

                if (guest->State == PeepState::walking)
                {
                    guest->HappinessTarget = std::min(guest->HappinessTarget + 4, kPeepMaxHappiness);
                }
                else if (guest->State == PeepState::queuing)
                {
                    guest->TimeInQueue = std::max(0, guest->TimeInQueue - 200);
                    guest->HappinessTarget = std::min(guest->HappinessTarget + 3, kPeepMaxHappiness);
                }
            }
        }
    }
}

/**
 *
 *  rct2: 0x006C05AE
 */
bool Staff::DoEntertainerPathFinding()
{
    if (((ScenarioRand() & 0xFFFF) <= 0x4000) && IsActionInterruptableSafely())
    {
        Action = (ScenarioRand() & 1) ? PeepActionType::wave2 : PeepActionType::joy;
        AnimationFrameNum = 0;
        AnimationImageIdOffset = 0;

        UpdateCurrentAnimationType();
        EntertainerUpdateNearbyPeeps();
    }

    return DoMiscPathFinding();
}

/**
 *
 *  rct2: 0x006BF926
 */
bool Staff::DoPathFinding()
{
    switch (AssignedStaffType)
    {
        case StaffType::handyman:
            return DoHandymanPathFinding();
        case StaffType::mechanic:
            return DoMechanicPathFinding();
        case StaffType::security:
            return DoMiscPathFinding();
        case StaffType::entertainer:
            return DoEntertainerPathFinding();

        default:
            assert(false);
            return false;
    }
}

void Staff::SetHireDate(int32_t hireDate)
{
    HireDate = hireDate;
}

int32_t Staff::GetHireDate() const
{
    return HireDate;
}

Drawing::Colour StaffGetColour(StaffType staffType)
{
    const auto& park = getGameState().park;
    switch (staffType)
    {
        case StaffType::handyman:
            return park.staffHandymanColour;
        case StaffType::mechanic:
            return park.staffMechanicColour;
        case StaffType::security:
            return park.staffSecurityColour;
        case StaffType::entertainer:
            return Drawing::Colour::black;
        default:
            assert(false);
            return Drawing::Colour::black;
    }
}

GameActions::Result StaffSetColour(StaffType staffType, OpenRCT2::Drawing::Colour value)
{
    auto& park = getGameState().park;
    switch (staffType)
    {
        case StaffType::handyman:
            park.staffHandymanColour = value;
            break;
        case StaffType::mechanic:
            park.staffMechanicColour = value;
            break;
        case StaffType::security:
            park.staffSecurityColour = value;
            break;
        default:
            return GameActions::Result(
                GameActions::Status::invalidParameters, STR_ERR_INVALID_PARAMETER, STR_ERR_ACTION_INVALID_FOR_THAT_STAFF_TYPE);
    }
    return GameActions::Result();
}

/** rct2: 0x009929C8 */
static constexpr CoordsXY kMowingWaypoints[] = {
    { 28, 28 }, { 28, 4 }, { 20, 4 }, { 20, 28 }, { 12, 28 }, { 12, 4 }, { 4, 4 }, { 4, 28 },
};

/**
 *
 *  rct2: 0x006BF567
 */
void Staff::UpdateMowing()
{
    if (!CheckForPath())
        return;

    while (true)
    {
        if (auto loc = UpdateAction(); loc.has_value())
        {
            int16_t checkZ = TileElementHeight(*loc);
            MoveTo({ loc.value(), checkZ });
            return;
        }

        Var37++;

        if (Var37 == 1)
        {
            SwitchToSpecialSprite(2);
        }

        if (Var37 == std::size(kMowingWaypoints))
        {
            StateReset();
            return;
        }

        auto destination = kMowingWaypoints[Var37] + NextLoc;
        SetDestination(destination);

        if (Var37 != 7)
            continue;

        auto surfaceElement = MapGetSurfaceElementAt(NextLoc);
        if (surfaceElement != nullptr && surfaceElement->CanGrassGrow())
        {
            surfaceElement->SetGrassLength(GRASS_LENGTH_MOWED);
            MapInvalidateTileZoom0({ NextLoc, surfaceElement->GetBaseZ(), surfaceElement->GetBaseZ() + 16 });
        }
        StaffLawnsMown = AddClamp(StaffLawnsMown, 1u);
        WindowInvalidateFlags |= PEEP_INVALIDATE_STAFF_STATS;
    }
}

/**
 *
 *  rct2: 0x006BF7E6
 */
void Staff::UpdateWatering()
{
    StaffMowingTimeout = 0;
    if (SubState == 0)
    {
        if (!CheckForPath())
            return;

        const auto [pathingResult, _] = PerformNextAction();
        if (!(pathingResult & PATHING_DESTINATION_REACHED))
            return;

        Orientation = (Var37 & 3) << 3;
        Action = PeepActionType::staffWatering;
        AnimationFrameNum = 0;
        AnimationImageIdOffset = 0;
        UpdateCurrentAnimationType();

        SubState = 1;
    }
    else if (SubState == 1)
    {
        if (!IsActionWalking())
        {
            UpdateAction();
            Invalidate();
            return;
        }

        auto actionLoc = CoordsXY{ NextLoc } + CoordsDirectionDelta[Var37];

        TileElement* tile_element = MapGetFirstElementAt(actionLoc);
        if (tile_element == nullptr)
            return;

        do
        {
            if (tile_element->GetType() != TileElementType::SmallScenery)
                continue;

            if (abs(NextLoc.z - tile_element->GetBaseZ()) > 4 * kCoordsZStep)
                continue;

            const auto* sceneryEntry = tile_element->AsSmallScenery()->GetEntry();

            if (sceneryEntry == nullptr || !sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_CAN_BE_WATERED))
                continue;

            tile_element->AsSmallScenery()->SetAge(0);
            MapInvalidateTileZoom0({ actionLoc, tile_element->GetBaseZ(), tile_element->GetClearanceZ() });
            StaffGardensWatered = AddClamp(StaffGardensWatered, 1u);
            WindowInvalidateFlags |= PEEP_INVALIDATE_STAFF_STATS;
        } while (!(tile_element++)->IsLastForTile());

        StateReset();
    }
}

/**
 *
 *  rct2: 0x006BF6C9
 */
void Staff::UpdateEmptyingBin()
{
    StaffMowingTimeout = 0;

    if (SubState == 0)
    {
        if (!CheckForPath())
            return;

        const auto [pathingResult, _] = PerformNextAction();
        if (!(pathingResult & PATHING_DESTINATION_REACHED))
            return;

        Orientation = (Var37 & 3) << 3;
        Action = PeepActionType::staffEmptyBin;
        AnimationFrameNum = 0;
        AnimationImageIdOffset = 0;
        UpdateCurrentAnimationType();

        SubState = 1;
    }
    else if (SubState == 1)
    {
        if (IsActionWalking())
        {
            StateReset();
            return;
        }

        UpdateAction();
        Invalidate();

        if (AnimationFrameNum != 11)
            return;

        TileElement* tile_element = MapGetFirstElementAt(NextLoc);
        if (tile_element == nullptr)
            return;

        for (;; tile_element++)
        {
            if (tile_element->GetType() == TileElementType::Path)
            {
                if (NextLoc.z == tile_element->GetBaseZ())
                    break;
            }
            if ((tile_element)->IsLastForTile())
            {
                StateReset();
                return;
            }
        }

        if (!tile_element->AsPath()->HasAddition())
        {
            StateReset();
            return;
        }

        auto* pathAddEntry = tile_element->AsPath()->GetAdditionEntry();
        if (!(pathAddEntry->flags & PATH_ADDITION_FLAG_IS_BIN) || tile_element->AsPath()->IsBroken()
            || tile_element->AsPath()->AdditionIsGhost())
        {
            StateReset();
            return;
        }

        uint8_t additionStatus = tile_element->AsPath()->GetAdditionStatus() | ((3 << Var37) << Var37);
        tile_element->AsPath()->SetAdditionStatus(additionStatus);

        MapInvalidateTileZoom0({ NextLoc, tile_element->GetBaseZ(), tile_element->GetClearanceZ() });
        StaffBinsEmptied = AddClamp(StaffBinsEmptied, 1u);
        WindowInvalidateFlags |= PEEP_INVALIDATE_STAFF_STATS;
    }
}

/**
 *
 *  rct2: 0x6BF641
 */
void Staff::UpdateSweeping()
{
    StaffMowingTimeout = 0;
    if (!CheckForPath())
        return;

    if (Action == PeepActionType::staffSweep && AnimationFrameNum == 8)
    {
        // Remove sick at this location
        Litter::RemoveAt(GetLocation());
        StaffLitterSwept = AddClamp(StaffLitterSwept, 1u);
        WindowInvalidateFlags |= PEEP_INVALIDATE_STAFF_STATS;
    }
    if (auto loc = UpdateAction(); loc.has_value())
    {
        int16_t actionZ = GetZOnSlope((*loc).x, (*loc).y);
        MoveTo({ loc.value(), actionZ });
        return;
    }

    Var37++;
    if (Var37 != 2)
    {
        Action = PeepActionType::staffSweep;
        AnimationFrameNum = 0;
        AnimationImageIdOffset = 0;
        UpdateCurrentAnimationType();
        return;
    }
    StateReset();
}

/**
 *
 *  rct2: 0x006C16D7
 */
void Staff::UpdateHeadingToInspect()
{
    auto ride = GetRide(CurrentRide);
    if (ride == nullptr)
    {
        SetState(PeepState::falling);
        return;
    }

    if (ride->getStation(CurrentRideStation).Exit.IsNull())
    {
        ride->lifecycleFlags &= ~RIDE_LIFECYCLE_DUE_INSPECTION;
        SetState(PeepState::falling);
        return;
    }

    if (ride->mechanicStatus != MechanicStatus::heading || !(ride->lifecycleFlags & RIDE_LIFECYCLE_DUE_INSPECTION))
    {
        SetState(PeepState::falling);
        return;
    }

    if (SubState == 0)
    {
        MechanicTimeSinceCall = 0;
        ResetPathfindGoal();
        SubState = 2;
    }

    if (SubState <= 3)
    {
        MechanicTimeSinceCall++;
        if (MechanicTimeSinceCall > 2500)
        {
            if (ride->lifecycleFlags & RIDE_LIFECYCLE_DUE_INSPECTION && ride->mechanicStatus == MechanicStatus::heading)
            {
                ride->mechanicStatus = MechanicStatus::calling;
            }
            SetState(PeepState::falling);
            return;
        }

        if (!CheckForPath())
            return;

        if (ShouldWaitForLevelCrossing() && !IsMechanicHeadingToFixRideBlockingPath())
            return;

        const auto [pathingResult, rideEntranceExitElement] = PerformNextAction();
        if (!(pathingResult & PATHING_RIDE_EXIT) && !(pathingResult & PATHING_RIDE_ENTRANCE))
        {
            return;
        }

        if (CurrentRide != rideEntranceExitElement->AsEntrance()->GetRideIndex())
            return;

        StationIndex exitIndex = rideEntranceExitElement->AsEntrance()->GetStationIndex();
        if (CurrentRideStation != exitIndex)
            return;

        if (pathingResult & PATHING_RIDE_ENTRANCE)
        {
            if (!ride->getStation(exitIndex).Exit.IsNull())
            {
                return;
            }
        }

        PeepDirection = rideEntranceExitElement->GetDirection();

        auto newDestination = CoordsXY{ 16, 16 } + NextLoc + (DirectionOffsets[PeepDirection] * 53);
        SetDestination(newDestination, 2);
        Orientation = PeepDirection << 3;

        z = rideEntranceExitElement->BaseHeight * 4;
        SubState = 4;
        // Falls through into SubState 4
    }

    int16_t delta_y = abs(GetLocation().y - GetDestination().y);
    if (auto loc = UpdateAction(); loc.has_value())
    {
        auto newZ = ride->getStation(CurrentRideStation).GetBaseZ();
        if (delta_y < 20)
        {
            newZ += ride->getRideTypeDescriptor().Heights.PlatformHeight;
        }

        MoveTo({ loc.value(), newZ });
        return;
    }

    SetState(PeepState::inspecting);
    SubState = 0;
}

/**
 *
 *  rct2: 0x006C0CB8
 */
void Staff::UpdateAnswering()
{
    auto ride = GetRide(CurrentRide);
    if (ride == nullptr || ride->mechanicStatus != MechanicStatus::heading)
    {
        SetState(PeepState::falling);
        return;
    }

    if (SubState == 0)
    {
        Action = PeepActionType::staffAnswerCall;
        AnimationFrameNum = 0;
        AnimationImageIdOffset = 0;

        UpdateCurrentAnimationType();

        SubState = 1;
        PeepWindowStateUpdate(this);
        return;
    }
    if (SubState == 1)
    {
        if (IsActionWalking())
        {
            SubState = 2;
            PeepWindowStateUpdate(this);
            MechanicTimeSinceCall = 0;
            ResetPathfindGoal();
            return;
        }
        UpdateAction();
        Invalidate();
        return;
    }
    if (SubState <= 3)
    {
        MechanicTimeSinceCall++;
        if (MechanicTimeSinceCall > 2500)
        {
            ride->mechanicStatus = MechanicStatus::calling;
            ride->windowInvalidateFlags.set(RideInvalidateFlag::maintenance);
            SetState(PeepState::falling);
            return;
        }

        if (!CheckForPath())
            return;

        if (ShouldWaitForLevelCrossing() && !IsMechanicHeadingToFixRideBlockingPath())
            return;

        const auto [pathingResult, rideEntranceExitElement] = PerformNextAction();
        if (!(pathingResult & PATHING_RIDE_EXIT) && !(pathingResult & PATHING_RIDE_ENTRANCE))
        {
            return;
        }

        if (CurrentRide != rideEntranceExitElement->AsEntrance()->GetRideIndex())
            return;

        StationIndex exitIndex = rideEntranceExitElement->AsEntrance()->GetStationIndex();
        if (CurrentRideStation != exitIndex)
            return;

        if (pathingResult & PATHING_RIDE_ENTRANCE)
        {
            if (!ride->getStation(exitIndex).Exit.IsNull())
            {
                return;
            }
        }

        PeepDirection = rideEntranceExitElement->GetDirection();

        int32_t destX = NextLoc.x + 16 + DirectionOffsets[PeepDirection].x * 53;
        int32_t destY = NextLoc.y + 16 + DirectionOffsets[PeepDirection].y * 53;

        SetDestination({ destX, destY }, 2);
        Orientation = PeepDirection << 3;

        z = rideEntranceExitElement->BaseHeight * 4;
        SubState = 4;
        // Falls through into SubState 4
    }

    int16_t delta_y = abs(y - GetDestination().y);
    if (auto loc = UpdateAction(); loc.has_value())
    {
        auto newZ = ride->getStation(CurrentRideStation).GetBaseZ();
        if (delta_y < 20)
        {
            newZ += ride->getRideTypeDescriptor().Heights.PlatformHeight;
        }

        MoveTo({ loc.value(), newZ });
        return;
    }

    SetState(PeepState::fixing);
    SubState = 0;
}

/** rct2: 0x00992A5C */
static constexpr CoordsXY kWateringUseOffsets[] = {
    { 3, 16 }, { 16, 29 }, { 29, 16 }, { 16, 3 }, { 3, 29 }, { 29, 29 }, { 29, 3 }, { 3, 3 },
};

/**
 *
 *  rct2: 0x006BF483
 */
bool Staff::UpdatePatrollingFindWatering()
{
    if (!(StaffOrders & STAFF_ORDERS_WATER_FLOWERS))
        return false;

    uint8_t chosen_position = ScenarioRand() & 7;
    for (int32_t i = 0; i < 8; ++i, ++chosen_position)
    {
        chosen_position &= 7;

        auto chosenLoc = CoordsXY{ NextLoc } + CoordsDirectionDelta[chosen_position];

        TileElement* tile_element = MapGetFirstElementAt(chosenLoc);

        // This seems to happen in some SV4 files.
        if (tile_element == nullptr)
        {
            continue;
        }

        do
        {
            if (tile_element->GetType() != TileElementType::SmallScenery)
            {
                continue;
            }

            auto z_diff = abs(NextLoc.z - tile_element->GetBaseZ());

            if (z_diff >= 4 * kCoordsZStep)
            {
                continue;
            }

            auto* sceneryEntry = tile_element->AsSmallScenery()->GetEntry();

            if (sceneryEntry == nullptr || !sceneryEntry->HasFlag(SMALL_SCENERY_FLAG_CAN_BE_WATERED))
            {
                continue;
            }

            if (tile_element->AsSmallScenery()->GetAge() < kSceneryWitherAgeThreshold2)
            {
                if (chosen_position >= 4)
                {
                    continue;
                }

                if (tile_element->AsSmallScenery()->GetAge() < kSceneryWitherAgeThreshold1)
                {
                    continue;
                }
            }

            SetState(PeepState::watering);
            Var37 = chosen_position;

            SubState = 0;
            auto destination = kWateringUseOffsets[chosen_position] + GetLocation().ToTileStart();
            SetDestination(destination, 3);

            return true;
        } while (!(tile_element++)->IsLastForTile());
    }
    return false;
}

/**
 *
 *  rct2: 0x006BF3A1
 */
bool Staff::UpdatePatrollingFindBin()
{
    if (!(StaffOrders & STAFF_ORDERS_EMPTY_BINS))
        return false;

    if (GetNextIsSurface())
        return false;

    TileElement* tileElement = MapGetFirstElementAt(NextLoc);
    if (tileElement == nullptr)
        return false;

    for (;; tileElement++)
    {
        if (tileElement->GetType() == TileElementType::Path && (tileElement->GetBaseZ() == NextLoc.z))
            break;

        if (tileElement->IsLastForTile())
            return false;
    }

    if (!tileElement->AsPath()->HasAddition())
        return false;
    auto* pathAddEntry = tileElement->AsPath()->GetAdditionEntry();
    if (pathAddEntry == nullptr)
        return false;

    if (!(pathAddEntry->flags & PATH_ADDITION_FLAG_IS_BIN))
        return false;

    if (tileElement->AsPath()->IsBroken())
        return false;

    if (tileElement->AsPath()->AdditionIsGhost())
        return false;

    uint8_t bin_positions = tileElement->AsPath()->GetEdges();
    uint8_t bin_quantity = tileElement->AsPath()->GetAdditionStatus();
    uint8_t chosen_position = 0;

    for (; chosen_position < 4; ++chosen_position)
    {
        if (!(bin_positions & 1) && !(bin_quantity & 3))
            break;
        bin_positions >>= 1;
        bin_quantity >>= 2;
    }

    if (chosen_position == 4)
        return false;

    Var37 = chosen_position;
    SetState(PeepState::emptyingBin);

    SubState = 0;
    auto destination = BinUseOffsets[chosen_position] + GetLocation().ToTileStart();
    SetDestination(destination, 3);
    return true;
}

/**
 *
 *  rct2: 0x006BF322
 */
bool Staff::UpdatePatrollingFindGrass()
{
    if (!(StaffOrders & STAFF_ORDERS_MOWING))
        return false;

    if (StaffMowingTimeout < 12)
        return false;

    if (!(GetNextIsSurface()))
        return false;

    auto surfaceElement = MapGetSurfaceElementAt(NextLoc);
    if (surfaceElement != nullptr && surfaceElement->CanGrassGrow())
    {
        if ((surfaceElement->GetGrassLength() & 0x7) >= GRASS_LENGTH_CLEAR_1)
        {
            SetState(PeepState::mowing);
            Var37 = 0;
            // Original code used .y for both x and y. Changed to .x to make more sense (both x and y are 28)

            auto destination = kMowingWaypoints[0] + NextLoc;
            SetDestination(destination, 3);
            return true;
        }
    }
    return false;
}

/**
 *
 *  rct2: 0x006BF295
 */
bool Staff::UpdatePatrollingFindSweeping()
{
    if (!(StaffOrders & STAFF_ORDERS_SWEEPING))
        return false;
    auto quad = EntityTileList<Litter>({ x, y });
    for (auto litter : quad)
    {
        uint16_t z_diff = abs(z - litter->z);

        if (z_diff >= 16)
            continue;

        SetState(PeepState::sweeping);

        Var37 = 0;
        SetDestination(litter->GetLocation(), 5);
        return true;
    }

    return false;
}

bool Staff::SecurityGuardPathIsCrowded() const
{
    // Iterate over tiles within a 3-tile radius (96 units)
    constexpr auto kTileRadius = 3;
    constexpr auto kLookupRadius = kCoordsXYStep * kTileRadius;
    constexpr auto kSecurityPathCrowdedThreshold = 20;

    int16_t guestCount = 0;

    for (int32_t tileX = x - kLookupRadius; tileX <= x + kLookupRadius; tileX += kCoordsXYStep)
    {
        for (int32_t tileY = y - kLookupRadius; tileY <= y + kLookupRadius; tileY += kCoordsXYStep)
        {
            for (auto* guest : EntityTileList<Guest>({ tileX, tileY }))
            {
                if (guest->x == kLocationNull)
                    continue;

                int16_t zDist = std::abs(z - guest->z);
                if (zDist > kTileRadius / 2)
                    continue;

                int16_t xDist = std::abs(x - guest->x);
                if (xDist > kLookupRadius)
                    continue;

                int16_t yDist = std::abs(y - guest->y);
                if (yDist > kLookupRadius)
                    continue;

                if (!guest->IsActionWalking())
                    continue;

                guestCount++;
                if (guestCount >= kSecurityPathCrowdedThreshold)
                    return true;
            }
        }
    }

    return false;
}

void Staff::Tick128UpdateStaff()
{
    if (AssignedStaffType != StaffType::security)
        return;

    // Alternate between walking animations based on crowd size
    auto newAnimationGroup = PeepAnimationGroup::normal;
    if (State == PeepState::patrolling && SecurityGuardPathIsCrowded())
        newAnimationGroup = PeepAnimationGroup::alternate;

    if (AnimationGroup == newAnimationGroup)
        return;

    AnimationGroup = newAnimationGroup;
    AnimationImageIdOffset = 0;
    WalkingAnimationFrameNum = 0;
    if (Action < PeepActionType::idle)
        Action = PeepActionType::walking;

    auto& objManager = GetContext()->GetObjectManager();
    auto* animObj = objManager.GetLoadedObject<PeepAnimationsObject>(AnimationObjectIndex);

    // NB: security staff have two animations groups: one regular, and one slow-walking
    PeepFlags &= ~PEEP_FLAGS_SLOW_WALK;
    if (animObj->IsSlowWalking(newAnimationGroup))
    {
        PeepFlags |= PEEP_FLAGS_SLOW_WALK;
    }

    AnimationType = PeepAnimationType::invalid;
    UpdateCurrentAnimationType();
}

bool Staff::IsMechanic() const
{
    return AssignedStaffType == StaffType::mechanic;
}

bool Staff::isEntertainer() const
{
    return AssignedStaffType == StaffType::entertainer;
}

void Staff::Update()
{
    if (PeepFlags & PEEP_FLAGS_POSITION_FROZEN)
    {
        if (!(PeepFlags & PEEP_FLAGS_ANIMATION_FROZEN))
        {
            // This is circumventing other logic, so only update every few ticks
            if ((getGameState().currentTicks & 3) == 0)
            {
                if (IsActionWalking())
                    UpdateWalkingAnimation();
                else
                    UpdateActionAnimation();
                Invalidate();
            }
        }
        return;
    }
    else if (PeepFlags & PEEP_FLAGS_ANIMATION_FROZEN)
    {
        // Animation is frozen while position is not. This allows a peep to walk
        // around without its sprite being updated, which looks very glitchy.
        // We'll just remove the flag and continue as normal, in this case.
        PeepFlags &= ~PEEP_FLAGS_ANIMATION_FROZEN;
    }

    // Walking speed logic
    const auto stepsToTake = GetStepsToTake();
    const auto carryCheck = StepProgress + stepsToTake;
    StepProgress = carryCheck;

    if (carryCheck <= 255)
    {
        // No-op: Keep replay working for now, can be eliminate with a replay update.
    }
    else
    {
        // Loc68FD2F
        switch (State)
        {
            case PeepState::falling:
                UpdateFalling();
                break;
            case PeepState::one:
                Update1();
                break;
            case PeepState::onRide:
                // No action
                break;
            case PeepState::picked:
                UpdatePicked();
                break;
            case PeepState::patrolling:
                UpdatePatrolling();
                break;
            case PeepState::mowing:
                UpdateMowing();
                break;
            case PeepState::sweeping:
                UpdateSweeping();
                break;
            case PeepState::answering:
                UpdateAnswering();
                break;
            case PeepState::fixing:
                UpdateFixing(stepsToTake);
                break;
            case PeepState::inspecting:
                UpdateFixing(stepsToTake);
                break;
            case PeepState::emptyingBin:
                UpdateEmptyingBin();
                break;
            case PeepState::watering:
                UpdateWatering();
                break;
            case PeepState::headingToInspection:
                UpdateHeadingToInspect();
                break;
            default:
                // TODO reset to default state
                assert(false);
                break;
        }
    }
}

/**
 *
 *  rct2: 0x006BF1FD
 */
void Staff::UpdatePatrolling()
{
    if (!CheckForPath())
        return;

    if (ShouldWaitForLevelCrossing() && !IsMechanicHeadingToFixRideBlockingPath())
    {
        UpdateWaitingAtCrossing();
        return;
    }

    const auto [pathingResult, _] = PerformNextAction();
    if (!(pathingResult & PATHING_DESTINATION_REACHED))
        return;

    if (GetNextIsSurface())
    {
        auto surfaceElement = MapGetSurfaceElementAt(NextLoc);

        if (surfaceElement != nullptr)
        {
            int32_t water_height = surfaceElement->GetWaterHeight();
            if (water_height > 0)
            {
                MoveTo({ x, y, water_height });
                SetState(PeepState::falling);
                return;
            }
        }
    }

    if (AssignedStaffType != StaffType::handyman)
        return;

    if (UpdatePatrollingFindSweeping())
        return;

    if (UpdatePatrollingFindGrass())
        return;

    if (UpdatePatrollingFindBin())
        return;

    UpdatePatrollingFindWatering();
}

enum
{
    PEEP_FIXING_ENTER_STATION = 0,
    PEEP_FIXING_MOVE_TO_BROKEN_DOWN_VEHICLE = 1,
    PEEP_FIXING_FIX_VEHICLE_CLOSED_RESTRAINTS = 2,
    PEEP_FIXING_FIX_VEHICLE_CLOSED_DOORS = 3,
    PEEP_FIXING_FIX_VEHICLE_OPEN_RESTRAINTS = 4,
    PEEP_FIXING_FIX_VEHICLE_OPEN_DOORS = 5,
    PEEP_FIXING_FIX_VEHICLE_MALFUNCTION = 6,
    PEEP_FIXING_MOVE_TO_STATION_END = 7,
    PEEP_FIXING_FIX_STATION_END = 8,
    PEEP_FIXING_MOVE_TO_STATION_START = 9,
    PEEP_FIXING_FIX_STATION_START = 10,
    PEEP_FIXING_FIX_STATION_BRAKES = 11,
    PEEP_FIXING_MOVE_TO_STATION_EXIT = 12,
    PEEP_FIXING_FINISH_FIX_OR_INSPECT = 13,
    PEEP_FIXING_LEAVE_BY_ENTRANCE_EXIT = 14,
};

/**
 * FixingSubstatesForBreakdown[] defines the applicable peep sub_states for
 * mechanics fixing a ride. The array is indexed by breakdownReason:
 * - indexes 0-7 are the 8 breakdown reasons (see BREAKDOWN_* in Ride.h)
 *   when fixing a broken down ride;
 * - index 8 is for inspecting a ride.
 */
// clang-format off
static constexpr uint32_t FixingSubstatesForBreakdown[9] = {
    ( // BREAKDOWN_SAFETY_CUT_OUT
        (1 << PEEP_FIXING_MOVE_TO_STATION_END) |
        (1 << PEEP_FIXING_FIX_STATION_END) |
        (1 << PEEP_FIXING_MOVE_TO_STATION_START) |
        (1 << PEEP_FIXING_FIX_STATION_START) |
        (1 << PEEP_FIXING_MOVE_TO_STATION_EXIT) |
        (1 << PEEP_FIXING_FINISH_FIX_OR_INSPECT) |
        (1 << PEEP_FIXING_LEAVE_BY_ENTRANCE_EXIT)
    ),
    ( // BREAKDOWN_RESTRAINTS_STUCK_CLOSED
        (1 << PEEP_FIXING_MOVE_TO_BROKEN_DOWN_VEHICLE) |
        (1 << PEEP_FIXING_FIX_VEHICLE_CLOSED_RESTRAINTS) |
        (1 << PEEP_FIXING_MOVE_TO_STATION_EXIT) |
        (1 << PEEP_FIXING_FINISH_FIX_OR_INSPECT) |
        (1 << PEEP_FIXING_LEAVE_BY_ENTRANCE_EXIT)
    ),
    ( // BREAKDOWN_RESTRAINTS_STUCK_OPEN
        (1 << PEEP_FIXING_MOVE_TO_BROKEN_DOWN_VEHICLE) |
        (1 << PEEP_FIXING_FIX_VEHICLE_OPEN_RESTRAINTS) |
        (1 << PEEP_FIXING_MOVE_TO_STATION_EXIT) |
        (1 << PEEP_FIXING_FINISH_FIX_OR_INSPECT) |
        (1 << PEEP_FIXING_LEAVE_BY_ENTRANCE_EXIT)
    ),
    ( // BREAKDOWN_DOORS_STUCK_CLOSED
        (1 << PEEP_FIXING_MOVE_TO_BROKEN_DOWN_VEHICLE) |
        (1 << PEEP_FIXING_FIX_VEHICLE_CLOSED_DOORS) |
        (1 << PEEP_FIXING_MOVE_TO_STATION_EXIT) |
        (1 << PEEP_FIXING_FINISH_FIX_OR_INSPECT) |
        (1 << PEEP_FIXING_LEAVE_BY_ENTRANCE_EXIT)
    ),
    ( // BREAKDOWN_DOORS_STUCK_OPEN
        (1 << PEEP_FIXING_MOVE_TO_BROKEN_DOWN_VEHICLE) |
        (1 << PEEP_FIXING_FIX_VEHICLE_OPEN_DOORS) |
        (1 << PEEP_FIXING_MOVE_TO_STATION_EXIT) |
        (1 << PEEP_FIXING_FINISH_FIX_OR_INSPECT) |
        (1 << PEEP_FIXING_LEAVE_BY_ENTRANCE_EXIT)
    ),
    ( // BREAKDOWN_VEHICLE_MALFUNCTION
        (1 << PEEP_FIXING_MOVE_TO_BROKEN_DOWN_VEHICLE) |
        (1 << PEEP_FIXING_FIX_VEHICLE_MALFUNCTION) |
        (1 << PEEP_FIXING_MOVE_TO_STATION_EXIT) |
        (1 << PEEP_FIXING_FINISH_FIX_OR_INSPECT) |
        (1 << PEEP_FIXING_LEAVE_BY_ENTRANCE_EXIT)
    ),
    ( // BREAKDOWN_BRAKES_FAILURE
        (1 << PEEP_FIXING_MOVE_TO_STATION_START) |
        (1 << PEEP_FIXING_FIX_STATION_BRAKES) |
        (1 << PEEP_FIXING_MOVE_TO_STATION_EXIT) |
        (1 << PEEP_FIXING_FINISH_FIX_OR_INSPECT) |
        (1 << PEEP_FIXING_LEAVE_BY_ENTRANCE_EXIT)
    ),
    ( // BREAKDOWN_CONTROL_FAILURE
        (1 << PEEP_FIXING_MOVE_TO_STATION_END) |
        (1 << PEEP_FIXING_FIX_STATION_END) |
        (1 << PEEP_FIXING_MOVE_TO_STATION_START) |
        (1 << PEEP_FIXING_FIX_STATION_START) |
        (1 << PEEP_FIXING_MOVE_TO_STATION_EXIT) |
        (1 << PEEP_FIXING_FINISH_FIX_OR_INSPECT) |
        (1 << PEEP_FIXING_LEAVE_BY_ENTRANCE_EXIT)
    ),
    ( // INSPECTION
        (1 << PEEP_FIXING_MOVE_TO_STATION_END) |
        (1 << PEEP_FIXING_FIX_STATION_END) |
        (1 << PEEP_FIXING_MOVE_TO_STATION_START) |
        (1 << PEEP_FIXING_FIX_STATION_START) |
        (1 << PEEP_FIXING_MOVE_TO_STATION_EXIT) |
        (1 << PEEP_FIXING_FINISH_FIX_OR_INSPECT) |
        (1 << PEEP_FIXING_LEAVE_BY_ENTRANCE_EXIT)
    ),
};
// clang-format on

/**
 *
 *  rct2: 0x006C0E8B
 * Also used by inspecting.
 */
void Staff::UpdateFixing(int32_t steps)
{
    auto ride = GetRide(CurrentRide);
    if (ride == nullptr)
    {
        SetState(PeepState::falling);
        return;
    }

    bool progressToNextSubstate = true;
    bool firstRun = true;

    if ((State == PeepState::inspecting)
        && (ride->lifecycleFlags & (RIDE_LIFECYCLE_BREAKDOWN_PENDING | RIDE_LIFECYCLE_BROKEN_DOWN)))
    {
        // Ride has broken down since Mechanic was called to inspect it.
        // Mechanic identifies the breakdown and switches to fixing it.
        State = PeepState::fixing;
    }

    while (progressToNextSubstate)
    {
        switch (SubState)
        {
            case PEEP_FIXING_ENTER_STATION:
                NextFlags &= ~PEEP_NEXT_FLAG_IS_SLOPED;
                progressToNextSubstate = UpdateFixingEnterStation(*ride);
                break;

            case PEEP_FIXING_MOVE_TO_BROKEN_DOWN_VEHICLE:
                progressToNextSubstate = UpdateFixingMoveToBrokenDownVehicle(firstRun, *ride);
                break;

            case PEEP_FIXING_FIX_VEHICLE_CLOSED_RESTRAINTS:
            case PEEP_FIXING_FIX_VEHICLE_CLOSED_DOORS:
            case PEEP_FIXING_FIX_VEHICLE_OPEN_RESTRAINTS:
            case PEEP_FIXING_FIX_VEHICLE_OPEN_DOORS:
                progressToNextSubstate = UpdateFixingFixVehicle(firstRun, *ride);
                break;

            case PEEP_FIXING_FIX_VEHICLE_MALFUNCTION:
                progressToNextSubstate = UpdateFixingFixVehicleMalfunction(firstRun, *ride);
                break;

            case PEEP_FIXING_MOVE_TO_STATION_END:
                progressToNextSubstate = UpdateFixingMoveToStationEnd(firstRun, *ride);
                break;

            case PEEP_FIXING_FIX_STATION_END:
                progressToNextSubstate = UpdateFixingFixStationEnd(firstRun);
                break;

            case PEEP_FIXING_MOVE_TO_STATION_START:
                progressToNextSubstate = UpdateFixingMoveToStationStart(firstRun, *ride);
                break;

            case PEEP_FIXING_FIX_STATION_START:
                progressToNextSubstate = UpdateFixingFixStationStart(firstRun, *ride);
                break;

            case PEEP_FIXING_FIX_STATION_BRAKES:
                progressToNextSubstate = UpdateFixingFixStationBrakes(firstRun, *ride);
                break;

            case PEEP_FIXING_MOVE_TO_STATION_EXIT:
                progressToNextSubstate = UpdateFixingMoveToStationExit(firstRun, *ride);
                break;

            case PEEP_FIXING_FINISH_FIX_OR_INSPECT:
                progressToNextSubstate = UpdateFixingFinishFixOrInspect(firstRun, steps, *ride);
                break;

            case PEEP_FIXING_LEAVE_BY_ENTRANCE_EXIT:
                progressToNextSubstate = UpdateFixingLeaveByEntranceExit(firstRun, *ride);
                break;

            default:
                LOG_ERROR("Invalid substate");
                progressToNextSubstate = false;
        }

        firstRun = false;

        if (!progressToNextSubstate)
        {
            break;
        }

        int32_t subState = SubState;
        uint32_t sub_state_sequence_mask = FixingSubstatesForBreakdown[8];

        if (State != PeepState::inspecting)
        {
            sub_state_sequence_mask = FixingSubstatesForBreakdown[ride->breakdownReasonPending];
        }

        do
        {
            subState++;
        } while ((sub_state_sequence_mask & (1 << subState)) == 0);

        SubState = subState & 0xFF;
    }
}

/**
 * rct2: 0x006C0EEC
 * fixing SubState: enter_station - applies to fixing all break down reasons and ride inspections.
 */
bool Staff::UpdateFixingEnterStation(Ride& ride) const
{
    ride.mechanicStatus = MechanicStatus::fixing;
    ride.windowInvalidateFlags.set(RideInvalidateFlag::maintenance);

    return true;
}

/**
 * rct2: 0x006C0F09
 * fixing SubState: move_to_broken_down_vehicle - applies to fixing all vehicle specific breakdown reasons
 * - see FixingSubstatesForBreakdown[]
 */
bool Staff::UpdateFixingMoveToBrokenDownVehicle(bool firstRun, const Ride& ride)
{
    if (!firstRun)
    {
        Vehicle* vehicle = RideGetBrokenVehicle(ride);
        if (vehicle == nullptr)
        {
            return true;
        }

        while (true)
        {
            if (vehicle->IsHead())
            {
                break;
            }

            auto trackType = vehicle->GetTrackType();
            if (TrackTypeIsStation(trackType))
            {
                break;
            }

            vehicle = getGameState().entities.GetEntity<Vehicle>(vehicle->prev_vehicle_on_ride);
            if (vehicle == nullptr)
            {
                return true;
            }
        }

        CoordsXY offset = DirectionOffsets[PeepDirection];
        auto destination = (offset * -12) + vehicle->GetLocation();
        SetDestination(destination, 2);
    }

    if (auto loc = UpdateAction(); loc.has_value())
    {
        MoveTo({ loc.value(), z });
        return false;
    }

    return true;
}

/**
 * rct2: 0x006C0FD3
 * fixing SubState: fix_vehicle - applies to fixing vehicle with:
 * 1. restraints stuck closed,
 * 2. doors stuck closed,
 * 3. restrains stuck open,
 * 4. doors stuck open.
 * - see FixingSubstatesForBreakdown[]
 */
bool Staff::UpdateFixingFixVehicle(bool firstRun, const Ride& ride)
{
    if (!firstRun)
    {
        Orientation = PeepDirection << 3;

        Action = (ScenarioRand() & 1) ? PeepActionType::staffFix2 : PeepActionType::staffFix;
        AnimationImageIdOffset = 0;
        AnimationFrameNum = 0;
        UpdateCurrentAnimationType();
    }

    if (IsActionWalking())
    {
        return true;
    }

    UpdateAction();
    Invalidate();

    uint8_t actionFrame = (Action == PeepActionType::staffFix) ? 0x25 : 0x50;
    if (AnimationFrameNum != actionFrame)
    {
        return false;
    }

    Vehicle* vehicle = RideGetBrokenVehicle(ride);
    if (vehicle == nullptr)
    {
        return true;
    }

    vehicle->ClearFlag(VehicleFlags::CarIsBroken);

    return false;
}

/**
 * rct2: 0x006C107B
 * fixing SubState: fix_vehicle_malfunction - applies fixing to vehicle malfunction.
 * - see FixingSubstatesForBreakdown[]
 */
bool Staff::UpdateFixingFixVehicleMalfunction(bool firstRun, const Ride& ride)
{
    if (!firstRun)
    {
        Orientation = PeepDirection << 3;
        Action = PeepActionType::staffFix3;
        AnimationImageIdOffset = 0;
        AnimationFrameNum = 0;

        UpdateCurrentAnimationType();
    }

    if (IsActionWalking())
    {
        return true;
    }

    UpdateAction();
    Invalidate();

    if (AnimationFrameNum != 0x65)
    {
        return false;
    }

    Vehicle* vehicle = RideGetBrokenVehicle(ride);
    if (vehicle == nullptr)
    {
        return true;
    }

    vehicle->ClearFlag(VehicleFlags::TrainIsBroken);

    return false;
}

/** rct2: 0x00992A3C */
static constexpr CoordsXY kStationFixingOffsets[] = {
    { -12, 0 },
    { 0, 12 },
    { 12, 0 },
    { 0, -12 },
};

/**
 * rct2: 0x006C1114
 * fixing SubState: move_to_station_end - applies to fixing station specific breakdowns: safety cut-out, control failure,
 * inspection.
 * - see FixingSubstatesForBreakdown[]
 */
bool Staff::UpdateFixingMoveToStationEnd(bool firstRun, const Ride& ride)
{
    if (!firstRun)
    {
        if (ride.getRideTypeDescriptor().HasFlag(RtdFlag::hasSinglePieceStation)
            || !ride.getRideTypeDescriptor().HasFlag(RtdFlag::hasTrack))
        {
            return true;
        }

        auto stationPos = ride.getStation(CurrentRideStation).GetStart();
        if (stationPos.IsNull())
        {
            return true;
        }

        auto tileElement = MapGetTrackElementAt(stationPos);
        if (tileElement == nullptr)
        {
            LOG_ERROR("Couldn't find tile_element");
            return false;
        }

        int32_t trackDirection = tileElement->GetDirection();
        CoordsXY offset = kStationFixingOffsets[trackDirection];

        stationPos.x += 16 + offset.x;
        if (offset.x == 0)
        {
            stationPos.x = GetDestination().x;
        }

        stationPos.y += 16 + offset.y;
        if (offset.y == 0)
        {
            stationPos.y = GetDestination().y;
        }

        SetDestination(stationPos, 2);
    }

    if (auto loc = UpdateAction(); loc.has_value())
    {
        MoveTo({ loc.value(), z });
        return false;
    }

    return true;
}

/**
 * rct2: 0x006C11F5
 * fixing SubState: fix_station_end - applies to fixing station specific breakdowns: safety cut-out, control failure,
 * inspection.
 * - see FixingSubstatesForBreakdown[]
 */
bool Staff::UpdateFixingFixStationEnd(bool firstRun)
{
    if (!firstRun)
    {
        Orientation = PeepDirection << 3;
        Action = PeepActionType::staffCheckBoard;
        AnimationFrameNum = 0;
        AnimationImageIdOffset = 0;

        UpdateCurrentAnimationType();
    }

    if (IsActionWalking())
    {
        return true;
    }

    UpdateAction();
    Invalidate();

    return false;
}

/**
 * rct2: 0x006C1239
 * fixing SubState: move_to_station_start
 * 1. applies to fixing station specific breakdowns: safety cut-out, control failure,
 * 2. applies to fixing brake failure,
 * 3. applies to inspection.
 * - see FixingSubstatesForBreakdown[]
 */
bool Staff::UpdateFixingMoveToStationStart(bool firstRun, const Ride& ride)
{
    if (!firstRun)
    {
        if (ride.getRideTypeDescriptor().HasFlag(RtdFlag::hasSinglePieceStation)
            || !ride.getRideTypeDescriptor().HasFlag(RtdFlag::hasTrack))
        {
            return true;
        }

        auto stationPosition = ride.getStation(CurrentRideStation).GetStart();
        if (stationPosition.IsNull())
        {
            return true;
        }

        CoordsXYE input;
        input.x = stationPosition.x;
        input.y = stationPosition.y;
        input.element = MapGetTrackElementAtFromRide({ input.x, input.y, stationPosition.z }, CurrentRide);
        if (input.element == nullptr)
        {
            return true;
        }

        Direction stationDirection = 0;
        TrackBeginEnd trackBeginEnd;
        while (TrackBlockGetPrevious(input, &trackBeginEnd))
        {
            if (trackBeginEnd.begin_element->AsTrack()->IsStation())
            {
                input.x = trackBeginEnd.begin_x;
                input.y = trackBeginEnd.begin_y;
                input.element = trackBeginEnd.begin_element;

                stationDirection = trackBeginEnd.begin_element->GetDirection();
                continue;
            }

            break;
        }

        // Loc6C12ED:
        auto destination = CoordsXY{ input.x + 16, input.y + 16 };
        auto offset = kStationFixingOffsets[stationDirection];

        destination.x -= offset.x;
        if (offset.x == 0)
        {
            destination.x = GetDestination().x;
        }

        destination.y -= offset.y;
        if (offset.y == 0)
        {
            destination.y = GetDestination().y;
        }

        SetDestination(destination, 2);
    }

    if (auto loc = UpdateAction(); loc.has_value())
    {
        MoveTo({ loc.value(), z });
        return false;
    }

    return true;
}

/**
 * rct2: 0x006C1368
 * fixing SubState: fix_station_start
 * 1. applies to fixing station specific breakdowns: safety cut-out, control failure,
 * 2. applies to inspection.
 * - see FixingSubstatesForBreakdown[]
 */
bool Staff::UpdateFixingFixStationStart(bool firstRun, const Ride& ride)
{
    if (!firstRun)
    {
        if (ride.getRideTypeDescriptor().HasFlag(RtdFlag::hasSinglePieceStation)
            || !ride.getRideTypeDescriptor().HasFlag(RtdFlag::hasTrack))
        {
            return true;
        }

        Orientation = PeepDirection << 3;

        Action = PeepActionType::staffFix;
        AnimationFrameNum = 0;
        AnimationImageIdOffset = 0;

        UpdateCurrentAnimationType();
    }

    if (IsActionWalking())
    {
        return true;
    }

    UpdateAction();

    return false;
}

/**
 * rct2: 0x006C13CE
 * fixing SubState: fix_station_brakes - applies to fixing brake failure
 * - see FixingSubstatesForBreakdown[]
 */
bool Staff::UpdateFixingFixStationBrakes(bool firstRun, Ride& ride)
{
    if (!firstRun)
    {
        Orientation = PeepDirection << 3;

        Action = PeepActionType::staffFixGround;
        AnimationFrameNum = 0;
        AnimationImageIdOffset = 0;

        UpdateCurrentAnimationType();
    }

    if (IsActionWalking())
    {
        return true;
    }

    UpdateAction();
    Invalidate();

    if (AnimationFrameNum == 0x28)
    {
        ride.mechanicStatus = MechanicStatus::hasFixedStationBrakes;
        ride.windowInvalidateFlags.set(RideInvalidateFlag::maintenance);
    }

    if (AnimationFrameNum == 0x13 || AnimationFrameNum == 0x19 || AnimationFrameNum == 0x1F || AnimationFrameNum == 0x25
        || AnimationFrameNum == 0x2B)
    {
        Audio::Play3D(Audio::SoundId::mechanicFix, GetLocation());
    }

    return false;
}

/**
 * rct2: 0x006C1474
 * fixing SubState: move_to_station_exit - applies to fixing all failures & inspections
 * - see FixingSubstatesForBreakdown[]
 */
bool Staff::UpdateFixingMoveToStationExit(bool firstRun, const Ride& ride)
{
    if (!firstRun)
    {
        auto stationPosition = ride.getStation(CurrentRideStation).Exit.ToCoordsXY();
        if (stationPosition.IsNull())
        {
            stationPosition = ride.getStation(CurrentRideStation).Entrance.ToCoordsXY();

            if (stationPosition.IsNull())
            {
                return true;
            }
        }

        stationPosition = stationPosition.ToTileCentre();

        CoordsXY stationPlatformDirection = DirectionOffsets[PeepDirection];
        stationPosition.x += stationPlatformDirection.x * 20;
        stationPosition.y += stationPlatformDirection.y * 20;

        SetDestination(stationPosition, 2);
    }

    if (auto loc = UpdateAction(); loc.has_value())
    {
        MoveTo({ loc.value(), z });
        return false;
    }

    return true;
}

/**
 * rct2: 0x006C1504
 * fixing SubState: finish_fix_or_inspect - applies to fixing all failures & inspections
 * - see FixingSubstatesForBreakdown[]
 */
bool Staff::UpdateFixingFinishFixOrInspect(bool firstRun, int32_t steps, Ride& ride)
{
    if (!firstRun)
    {
        if (State == PeepState::inspecting)
        {
            UpdateRideInspected(CurrentRide);

            StaffRidesInspected = AddClamp(StaffRidesInspected, 1u);
            WindowInvalidateFlags |= PEEP_INVALIDATE_STAFF_STATS;
            ride.mechanicStatus = MechanicStatus::undefined;
            return true;
        }

        StaffRidesFixed = AddClamp(StaffRidesFixed, 1u);
        WindowInvalidateFlags |= PEEP_INVALIDATE_STAFF_STATS;

        Orientation = PeepDirection << 3;
        Action = PeepActionType::staffAnswerCall2;
        AnimationFrameNum = 0;
        AnimationImageIdOffset = 0;

        UpdateCurrentAnimationType();
    }

    if (!IsActionWalking())
    {
        UpdateAction();
        Invalidate();
        return false;
    }

    RideFixBreakdown(ride, steps);
    ride.mechanicStatus = MechanicStatus::undefined;
    return true;
}

/**
 * rct2: 0x006C157E
 * fixing SubState: leave_by_entrance_exit - applies to fixing all failures & inspections
 * - see FixingSubstatesForBreakdown[]
 */
bool Staff::UpdateFixingLeaveByEntranceExit(bool firstRun, const Ride& ride)
{
    if (!firstRun)
    {
        auto exitPosition = ride.getStation(CurrentRideStation).Exit.ToCoordsXY();
        if (exitPosition.IsNull())
        {
            exitPosition = ride.getStation(CurrentRideStation).Entrance.ToCoordsXY();

            if (exitPosition.IsNull())
            {
                SetState(PeepState::falling);
                return false;
            }
        }

        exitPosition = exitPosition.ToTileCentre();

        CoordsXY ebx_direction = DirectionOffsets[PeepDirection];
        exitPosition.x -= ebx_direction.x * 19;
        exitPosition.y -= ebx_direction.y * 19;

        SetDestination(exitPosition, 2);
    }

    int16_t xy_distance;
    if (auto loc = UpdateAction(xy_distance); loc.has_value())
    {
        auto stationHeight = ride.getStation(CurrentRideStation).GetBaseZ();
        if (xy_distance >= 16)
        {
            stationHeight += ride.getRideTypeDescriptor().Heights.PlatformHeight;
        }

        MoveTo({ loc.value(), stationHeight });
        return false;
    }
    SetState(PeepState::falling);
    return false;
}

/**
 * rct2: 0x6B7588
 */
void Staff::UpdateRideInspected(RideId rideIndex)
{
    auto ride = GetRide(rideIndex);
    if (ride != nullptr)
    {
        ride->lifecycleFlags &= ~RIDE_LIFECYCLE_DUE_INSPECTION;
        ride->reliability += ((100 - ride->reliabilityPercentage) / 4) * (ScenarioRand() & 0xFF);
        ride->lastInspection = 0;
        ride->windowInvalidateFlags.set(RideInvalidateFlag::maintenance, RideInvalidateFlag::main, RideInvalidateFlag::list);
    }
}

money64 GetStaffWage(StaffType type)
{
    switch (type)
    {
        default:
        case StaffType::handyman:
            return 50.00_GBP;
        case StaffType::mechanic:
            return 80.00_GBP;
        case StaffType::security:
            return 60.00_GBP;
        case StaffType::entertainer:
            return 55.00_GBP;
    }
}

void Staff::Serialise(DataSerialiser& stream)
{
    Peep::Serialise(stream);
    stream << AssignedStaffType;
    stream << MechanicTimeSinceCall;
    stream << HireDate;
    stream << StaffOrders;
    stream << StaffMowingTimeout;
    stream << StaffLawnsMown;
    stream << StaffGardensWatered;
    stream << StaffLitterSwept;
    stream << StaffBinsEmptied;
}
