/***************************************************************************** * Copyright (c) 2014-2024 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 "StaffHireNewAction.h" #include "../Cheats.h" #include "../Context.h" #include "../GameState.h" #include "../core/MemoryStream.h" #include "../drawing/Drawing.h" #include "../entity/EntityRegistry.h" #include "../entity/Staff.h" #include "../interface/Window.h" #include "../localisation/Localisation.h" #include "../localisation/StringIds.h" #include "../management/Finance.h" #include "../ride/Ride.h" #include "../scenario/Scenario.h" #include "../ui/UiContext.h" #include "../ui/WindowManager.h" #include "../world/Entrance.h" #include "../world/Park.h" #include using namespace OpenRCT2; /* rct2: 0x009929FC */ static constexpr PeepSpriteType spriteTypes[] = { PeepSpriteType::Handyman, PeepSpriteType::Mechanic, PeepSpriteType::Security, PeepSpriteType::EntertainerPanda, }; StaffHireNewAction::StaffHireNewAction( bool autoPosition, StaffType staffType, EntertainerCostume entertainerType, uint32_t staffOrders) : _autoPosition(autoPosition) , _staffType(static_cast(staffType)) , _entertainerType(entertainerType) , _staffOrders(staffOrders) { } void StaffHireNewAction::AcceptParameters(GameActionParameterVisitor& visitor) { visitor.Visit("autoPosition", _autoPosition); visitor.Visit("staffType", _staffType); visitor.Visit("entertainerType", _entertainerType); visitor.Visit("staffOrders", _staffOrders); } uint16_t StaffHireNewAction::GetActionFlags() const { return GameAction::GetActionFlags() | GameActions::Flags::AllowWhilePaused; } void StaffHireNewAction::Serialise(DataSerialiser& stream) { GameAction::Serialise(stream); stream << DS_TAG(_autoPosition) << DS_TAG(_staffType) << DS_TAG(_entertainerType) << DS_TAG(_staffOrders); } GameActions::Result StaffHireNewAction::Query() const { return QueryExecute(false); } GameActions::Result StaffHireNewAction::Execute() const { return QueryExecute(true); } GameActions::Result StaffHireNewAction::QueryExecute(bool execute) const { auto res = GameActions::Result(); res.Expenditure = ExpenditureType::Wages; if (_staffType >= static_cast(StaffType::Count)) { LOG_ERROR("Invalid staff type %u", static_cast(_staffType)); return GameActions::Result(GameActions::Status::InvalidParameters, STR_CANT_HIRE_NEW_STAFF, STR_ERR_VALUE_OUT_OF_RANGE); } if (GetNumFreeEntities() < 400) { return GameActions::Result(GameActions::Status::NoFreeElements, STR_CANT_HIRE_NEW_STAFF, STR_TOO_MANY_PEOPLE_IN_GAME); } if (_staffType == static_cast(StaffType::Entertainer)) { if (static_cast(_entertainerType) >= static_cast(EntertainerCostume::Count)) { LOG_ERROR("Invalid entertainer type %u", static_cast(_entertainerType)); return GameActions::Result( GameActions::Status::InvalidParameters, STR_CANT_HIRE_NEW_STAFF, STR_ERR_VALUE_OUT_OF_RANGE); } uint32_t availableCostumes = StaffGetAvailableEntertainerCostumes(); if (!(availableCostumes & (1 << static_cast(_entertainerType)))) { LOG_ERROR("Unavailable entertainer costume %u", static_cast(_entertainerType)); return GameActions::Result( GameActions::Status::InvalidParameters, STR_CANT_HIRE_NEW_STAFF, STR_ERR_VALUE_OUT_OF_RANGE); } } Staff* newPeep = CreateEntity(); if (newPeep == nullptr) { // Too many peeps exist already. return GameActions::Result(GameActions::Status::NoFreeElements, STR_CANT_HIRE_NEW_STAFF, STR_TOO_MANY_PEOPLE_IN_GAME); } if (execute == false) { // In query we just want to see if we can obtain a sprite slot. EntityRemove(newPeep); res.SetData(StaffHireNewActionResult{ EntityId::GetNull() }); } else { newPeep->WindowInvalidateFlags = 0; newPeep->Action = PeepActionType::Walking; newPeep->SpecialSprite = 0; newPeep->ActionSpriteImageOffset = 0; newPeep->WalkingFrameNum = 0; newPeep->ActionSpriteType = PeepActionSpriteType::None; newPeep->PathCheckOptimisation = 0; newPeep->PeepFlags = 0; newPeep->StaffLawnsMown = 0; newPeep->StaffGardensWatered = 0; newPeep->StaffLitterSwept = 0; newPeep->StaffBinsEmptied = 0; newPeep->StaffOrders = _staffOrders; // We search for the first available Id for a given staff type std::set usedStaffIds; for (auto searchPeep : EntityList()) { if (static_cast(searchPeep->AssignedStaffType) != _staffType) continue; usedStaffIds.insert(searchPeep->PeepId); } uint32_t newStaffId = 1; while (usedStaffIds.find(newStaffId) != usedStaffIds.end()) { newStaffId++; } newPeep->PeepId = newStaffId; newPeep->AssignedStaffType = static_cast(_staffType); PeepSpriteType spriteType = spriteTypes[_staffType]; if (_staffType == static_cast(StaffType::Entertainer)) { spriteType = EntertainerCostumeToSprite(_entertainerType); } newPeep->Name = nullptr; newPeep->SpriteType = spriteType; const SpriteBounds* spriteBounds = &GetSpriteBounds(spriteType); newPeep->SpriteData.Width = spriteBounds->sprite_width; newPeep->SpriteData.HeightMin = spriteBounds->sprite_height_negative; newPeep->SpriteData.HeightMax = spriteBounds->sprite_height_positive; if (_autoPosition) { AutoPositionNewStaff(newPeep); } else { // NOTE: This state is required for the window to act. newPeep->State = PeepState::Picked; // INVESTIGATE: x and y are LOCATION_NULL at this point. newPeep->MoveTo(newPeep->GetLocation()); } // Staff uses this newPeep->As()->SetHireDate(GetDate().GetMonthsElapsed()); newPeep->PathfindGoal.x = 0xFF; newPeep->PathfindGoal.y = 0xFF; newPeep->PathfindGoal.z = 0xFF; newPeep->PathfindGoal.direction = INVALID_DIRECTION; uint8_t colour = StaffGetColour(static_cast(_staffType)); newPeep->TshirtColour = colour; newPeep->TrousersColour = colour; // Staff energy determines their walking speed switch (GetGameState().Cheats.SelectedStaffSpeed) { case StaffSpeedCheat::None: newPeep->Energy = kCheatsStaffNormalSpeed; newPeep->EnergyTarget = kCheatsStaffNormalSpeed; break; case StaffSpeedCheat::Frozen: newPeep->Energy = kCheatsStaffFreezeSpeed; newPeep->EnergyTarget = kCheatsStaffFreezeSpeed; break; case StaffSpeedCheat::Fast: newPeep->Energy = kCheatsStaffFastSpeed; newPeep->EnergyTarget = kCheatsStaffFastSpeed; break; } newPeep->StaffMowingTimeout = 0; newPeep->PatrolInfo = nullptr; res.SetData(StaffHireNewActionResult{ newPeep->Id }); } return res; } void StaffHireNewAction::AutoPositionNewStaff(Peep* newPeep) const { // Find a location to place new staff member newPeep->State = PeepState::Falling; uint32_t count = 0; PathElement* guest_tile = nullptr; // Count number of walking guests { for (auto guest : EntityList()) { if (guest->State == PeepState::Walking) { // Check the walking guest's tile. Only count them if they're on a path tile. guest_tile = MapGetPathElementAt(TileCoordsXYZ{ guest->NextLoc }); if (guest_tile != nullptr) ++count; } } } CoordsXYZ newLocation{}; if (count > 0) { // Place staff at a random guest uint32_t rand = ScenarioRandMax(count); Guest* chosenGuest = nullptr; for (auto guest : EntityList()) { if (guest->State == PeepState::Walking) { guest_tile = MapGetPathElementAt(TileCoordsXYZ{ guest->NextLoc }); if (guest_tile != nullptr) { if (rand == 0) { chosenGuest = guest; break; } --rand; } } } if (chosenGuest != nullptr) { newLocation = chosenGuest->GetLocation(); } else { // User must pick a location newPeep->State = PeepState::Picked; newLocation = newPeep->GetLocation(); } } else { // No walking guests; pick random park entrance const auto& gameState = GetGameState(); if (!gameState.Park.Entrances.empty()) { auto rand = ScenarioRandMax(static_cast(gameState.Park.Entrances.size())); const auto& entrance = gameState.Park.Entrances[rand]; auto dir = entrance.direction; newLocation = entrance; // TODO: Replace with CoordsDirectionDelta newLocation.x += 16 + ((dir & 1) == 0 ? ((dir & 2) ? 32 : -32) : 0); newLocation.y += 16 + ((dir & 1) == 1 ? ((dir & 2) ? -32 : 32) : 0); } else { // User must pick a location newPeep->State = PeepState::Picked; newLocation = newPeep->GetLocation(); } } newPeep->MoveTo(newLocation + CoordsXYZ{ 0, 0, 16 }); }