/*****************************************************************************
 * 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.
 *****************************************************************************/

#ifdef ENABLE_SCRIPTING

    #include "ScGuest.hpp"

    #include "../../../Context.h"
    #include "../../../GameState.h"
    #include "../../../entity/Guest.h"
    #include "../../../localisation/Formatter.h"
    #include "../../../localisation/Formatting.h"
    #include "../../../management/Marketing.h"
    #include "../../../object/ObjectManager.h"
    #include "../../../object/PeepAnimationsObject.h"
    #include "../../../peep/PeepAnimations.h"

namespace OpenRCT2::Scripting
{
    static const EnumMap<PeepThoughtType> ThoughtTypeMap(
        {
            { "cant_afford_ride", PeepThoughtType::cantAffordRide },
            { "spent_money", PeepThoughtType::spentMoney },
            { "sick", PeepThoughtType::sick },
            { "very_sick", PeepThoughtType::verySick },
            { "more_thrilling", PeepThoughtType::moreThrilling },
            { "intense", PeepThoughtType::intense },
            { "havent_finished", PeepThoughtType::haventFinished },
            { "sickening", PeepThoughtType::sickening },
            { "bad_value", PeepThoughtType::badValue },
            { "go_home", PeepThoughtType::goHome },
            { "good_value", PeepThoughtType::goodValue },
            { "already_got", PeepThoughtType::alreadyGot },
            { "cant_afford_item", PeepThoughtType::cantAffordItem },
            { "not_hungry", PeepThoughtType::notHungry },
            { "not_thirsty", PeepThoughtType::notThirsty },
            { "drowning", PeepThoughtType::drowning },
            { "lost", PeepThoughtType::lost },
            { "was_great", PeepThoughtType::wasGreat },
            { "queuing_ages", PeepThoughtType::queuingAges },
            { "tired", PeepThoughtType::tired },
            { "hungry", PeepThoughtType::hungry },
            { "thirsty", PeepThoughtType::thirsty },
            { "toilet", PeepThoughtType::toilet },
            { "cant_find", PeepThoughtType::cantFind },
            { "not_paying", PeepThoughtType::notPaying },
            { "not_while_raining", PeepThoughtType::notWhileRaining },
            { "bad_litter", PeepThoughtType::badLitter },
            { "cant_find_exit", PeepThoughtType::cantFindExit },
            { "get_off", PeepThoughtType::getOff },
            { "get_out", PeepThoughtType::getOut },
            { "not_safe", PeepThoughtType::notSafe },
            { "path_disgusting", PeepThoughtType::pathDisgusting },
            { "crowded", PeepThoughtType::crowded },
            { "vandalism", PeepThoughtType::vandalism },
            { "scenery", PeepThoughtType::scenery },
            { "very_clean", PeepThoughtType::veryClean },
            { "fountains", PeepThoughtType::fountains },
            { "music", PeepThoughtType::music },
            { "balloon", PeepThoughtType::balloon },
            { "toy", PeepThoughtType::toy },
            { "map", PeepThoughtType::map },
            { "photo", PeepThoughtType::photo },
            { "umbrella", PeepThoughtType::umbrella },
            { "drink", PeepThoughtType::drink },
            { "burger", PeepThoughtType::burger },
            { "chips", PeepThoughtType::chips },
            { "ice_cream", PeepThoughtType::iceCream },
            { "candyfloss", PeepThoughtType::candyfloss },
            { "pizza", PeepThoughtType::pizza },
            { "popcorn", PeepThoughtType::popcorn },
            { "hot_dog", PeepThoughtType::hotDog },
            { "tentacle", PeepThoughtType::tentacle },
            { "hat", PeepThoughtType::hat },
            { "toffee_apple", PeepThoughtType::toffeeApple },
            { "tshirt", PeepThoughtType::tshirt },
            { "doughnut", PeepThoughtType::doughnut },
            { "coffee", PeepThoughtType::coffee },
            { "chicken", PeepThoughtType::chicken },
            { "lemonade", PeepThoughtType::lemonade },
            { "wow", PeepThoughtType::wow },
            { "wow2", PeepThoughtType::wow2 },
            { "watched", PeepThoughtType::watched },
            { "balloon_much", PeepThoughtType::balloonMuch },
            { "toy_much", PeepThoughtType::toyMuch },
            { "map_much", PeepThoughtType::mapMuch },
            { "photo_much", PeepThoughtType::photoMuch },
            { "umbrella_much", PeepThoughtType::umbrellaMuch },
            { "drink_much", PeepThoughtType::drinkMuch },
            { "burger_much", PeepThoughtType::burgerMuch },
            { "chips_much", PeepThoughtType::chipsMuch },
            { "ice_cream_much", PeepThoughtType::iceCreamMuch },
            { "candyfloss_much", PeepThoughtType::candyflossMuch },
            { "pizza_much", PeepThoughtType::pizzaMuch },
            { "popcorn_much", PeepThoughtType::popcornMuch },
            { "hot_dog_much", PeepThoughtType::hotDogMuch },
            { "tentacle_much", PeepThoughtType::tentacleMuch },
            { "hat_much", PeepThoughtType::hatMuch },
            { "toffee_apple_much", PeepThoughtType::toffeeAppleMuch },
            { "tshirt_much", PeepThoughtType::tshirtMuch },
            { "doughnut_much", PeepThoughtType::doughnutMuch },
            { "coffee_much", PeepThoughtType::coffeeMuch },
            { "chicken_much", PeepThoughtType::chickenMuch },
            { "lemonade_much", PeepThoughtType::lemonadeMuch },
            { "photo2", PeepThoughtType::photo2 },
            { "photo3", PeepThoughtType::photo3 },
            { "photo4", PeepThoughtType::photo4 },
            { "pretzel", PeepThoughtType::pretzel },
            { "hot_chocolate", PeepThoughtType::hotChocolate },
            { "iced_tea", PeepThoughtType::icedTea },
            { "funnel_cake", PeepThoughtType::funnelCake },
            { "sunglasses", PeepThoughtType::sunglasses },
            { "beef_noodles", PeepThoughtType::beefNoodles },
            { "fried_rice_noodles", PeepThoughtType::friedRiceNoodles },
            { "wonton_soup", PeepThoughtType::wontonSoup },
            { "meatball_soup", PeepThoughtType::meatballSoup },
            { "fruit_juice", PeepThoughtType::fruitJuice },
            { "soybean_milk", PeepThoughtType::soybeanMilk },
            { "sujongkwa", PeepThoughtType::sujongkwa },
            { "sub_sandwich", PeepThoughtType::subSandwich },
            { "cookie", PeepThoughtType::cookie },
            { "roast_sausage", PeepThoughtType::roastSausage },
            { "photo2_much", PeepThoughtType::photo2Much },
            { "photo3_much", PeepThoughtType::photo3Much },
            { "photo4_much", PeepThoughtType::photo4Much },
            { "pretzel_much", PeepThoughtType::pretzelMuch },
            { "hot_chocolate_much", PeepThoughtType::hotChocolateMuch },
            { "iced_tea_much", PeepThoughtType::icedTeaMuch },
            { "funnel_cake_much", PeepThoughtType::funnelCakeMuch },
            { "sunglasses_much", PeepThoughtType::sunglassesMuch },
            { "beef_noodles_much", PeepThoughtType::beefNoodlesMuch },
            { "fried_rice_noodles_much", PeepThoughtType::friedRiceNoodlesMuch },
            { "wonton_soup_much", PeepThoughtType::wontonSoupMuch },
            { "meatball_soup_much", PeepThoughtType::meatballSoupMuch },
            { "fruit_juice_much", PeepThoughtType::fruitJuiceMuch },
            { "soybean_milk_much", PeepThoughtType::soybeanMilkMuch },
            { "sujongkwa_much", PeepThoughtType::sujongkwaMuch },
            { "sub_sandwich_much", PeepThoughtType::subSandwichMuch },
            { "cookie_much", PeepThoughtType::cookieMuch },
            { "roast_sausage_much", PeepThoughtType::roastSausageMuch },
            { "help", PeepThoughtType::help },
            { "running_out", PeepThoughtType::runningOut },
            { "new_ride", PeepThoughtType::newRide },
            { "nice_ride_deprecated", PeepThoughtType::niceRideDeprecated },
            { "excited_deprecated", PeepThoughtType::excitedDeprecated },
            { "here_we_are", PeepThoughtType::hereWeAre },
        });

    ScGuest gScGuest;

    JSValue ScGuest::New(JSContext* ctx, EntityId entityId)
    {
        return gScEntity.NewDerivedInstance(ctx, entityId, gScGuest.GetProto());
    }

    void ScGuest::Register(JSContext* ctx)
    {
        static constexpr JSCFunctionListEntry funcs[] = {
            JS_CGETSET_DEF("tshirtColour", &ScGuest::tshirtColour_get, &ScGuest::tshirtColour_set),
            JS_CGETSET_DEF("trousersColour", &ScGuest::trousersColour_get, &ScGuest::trousersColour_set),
            JS_CGETSET_DEF("balloonColour", &ScGuest::balloonColour_get, &ScGuest::balloonColour_set),
            JS_CGETSET_DEF("hatColour", &ScGuest::hatColour_get, &ScGuest::hatColour_set),
            JS_CGETSET_DEF("umbrellaColour", &ScGuest::umbrellaColour_get, &ScGuest::umbrellaColour_set),
            JS_CGETSET_DEF("happiness", &ScGuest::happiness_get, &ScGuest::happiness_set),
            JS_CGETSET_DEF("happinessTarget", &ScGuest::happinessTarget_get, &ScGuest::happinessTarget_set),
            JS_CGETSET_DEF("nausea", &ScGuest::nausea_get, &ScGuest::nausea_set),
            JS_CGETSET_DEF("nauseaTarget", &ScGuest::nauseaTarget_get, &ScGuest::nauseaTarget_set),
            JS_CGETSET_DEF("hunger", &ScGuest::hunger_get, &ScGuest::hunger_set),
            JS_CGETSET_DEF("thirst", &ScGuest::thirst_get, &ScGuest::thirst_set),
            JS_CGETSET_DEF("toilet", &ScGuest::toilet_get, &ScGuest::toilet_set),
            JS_CGETSET_DEF("mass", &ScGuest::mass_get, &ScGuest::mass_set),
            JS_CGETSET_DEF("minIntensity", &ScGuest::minIntensity_get, &ScGuest::minIntensity_set),
            JS_CGETSET_DEF("maxIntensity", &ScGuest::maxIntensity_get, &ScGuest::maxIntensity_set),
            JS_CGETSET_DEF("nauseaTolerance", &ScGuest::nauseaTolerance_get, &ScGuest::nauseaTolerance_set),
            JS_CGETSET_DEF("cash", &ScGuest::cash_get, &ScGuest::cash_set),
            JS_CGETSET_DEF("isInPark", &ScGuest::isInPark_get, nullptr),
            JS_CGETSET_DEF("isLost", &ScGuest::isLost_get, nullptr),
            JS_CGETSET_DEF("lostCountdown", &ScGuest::lostCountdown_get, &ScGuest::lostCountdown_set),
            JS_CGETSET_DEF("favouriteRide", &ScGuest::favouriteRide_get, &ScGuest::favouriteRide_set),
            JS_CGETSET_DEF("thoughts", &ScGuest::thoughts_get, nullptr),
            JS_CGETSET_DEF("items", &ScGuest::items_get, nullptr),
            JS_CGETSET_DEF("availableAnimations", &ScGuest::availableAnimations_get, nullptr),
            JS_CGETSET_DEF("animation", &ScGuest::animation_get, &ScGuest::animation_set),
            JS_CGETSET_DEF("animationOffset", &ScGuest::animationOffset_get, &ScGuest::animationOffset_set),
            JS_CGETSET_DEF("animationLength", &ScGuest::animationLength_get, nullptr),
            JS_CFUNC_DEF("getAnimationSpriteIds", 2, &ScGuest::getAnimationSpriteIds),
            JS_CFUNC_DEF("hasItem", 1, &ScGuest::has_item),
            JS_CFUNC_DEF("giveItem", 1, &ScGuest::give_item),
            JS_CFUNC_DEF("removeItem", 1, &ScGuest::remove_item),
            JS_CFUNC_DEF("removeAllItems", 0, &ScGuest::remove_all_items),
        };
        gScGuest.RegisterDerived(ctx, gScPeep, funcs);
    }

    Guest* ScGuest::GetGuest(JSValue thisVal)
    {
        auto id = GetEntityId(thisVal);
        return getGameState().entities.getEntity<Guest>(id);
    }

    JSValue ScGuest::tshirtColour_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        return JS_NewUint32(ctx, peep != nullptr ? EnumValue(peep->tShirtColour) : 0);
    }
    JSValue ScGuest::tshirtColour_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            peep->tShirtColour = static_cast<Drawing::Colour>(value);
            peep->invalidate();
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::trousersColour_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        return JS_NewUint32(ctx, peep != nullptr ? EnumValue(peep->trousersColour) : 0);
    }
    JSValue ScGuest::trousersColour_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            peep->trousersColour = static_cast<Drawing::Colour>(value);
            peep->invalidate();
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::balloonColour_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        return JS_NewUint32(ctx, peep != nullptr ? EnumValue(peep->balloonColour) : 0);
    }
    JSValue ScGuest::balloonColour_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            peep->balloonColour = static_cast<Drawing::Colour>(value);
            peep->invalidate();
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::hatColour_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        return JS_NewUint32(ctx, peep != nullptr ? EnumValue(peep->hatColour) : 0);
    }
    JSValue ScGuest::hatColour_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            peep->hatColour = static_cast<Drawing::Colour>(value);
            peep->invalidate();
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::umbrellaColour_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        return JS_NewUint32(ctx, peep != nullptr ? EnumValue(peep->umbrellaColour) : 0);
    }
    JSValue ScGuest::umbrellaColour_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            peep->umbrellaColour = static_cast<Drawing::Colour>(value);
            peep->invalidate();
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::happiness_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        return JS_NewUint32(ctx, peep != nullptr ? peep->happiness : 0);
    }
    JSValue ScGuest::happiness_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            peep->happiness = value;
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::happinessTarget_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        return JS_NewUint32(ctx, peep != nullptr ? peep->happinessTarget : 0);
    }
    JSValue ScGuest::happinessTarget_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            peep->happinessTarget = value;
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::nausea_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        return JS_NewUint32(ctx, peep != nullptr ? peep->nausea : 0);
    }
    JSValue ScGuest::nausea_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            peep->nausea = value;
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::nauseaTarget_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        return JS_NewUint32(ctx, peep != nullptr ? peep->nauseaTarget : 0);
    }
    JSValue ScGuest::nauseaTarget_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            peep->nauseaTarget = value;
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::hunger_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        return JS_NewUint32(ctx, peep != nullptr ? peep->hunger : 0);
    }
    JSValue ScGuest::hunger_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            peep->hunger = value;
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::thirst_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        return JS_NewUint32(ctx, peep != nullptr ? peep->thirst : 0);
    }
    JSValue ScGuest::thirst_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            peep->thirst = value;
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::toilet_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        return JS_NewUint32(ctx, peep != nullptr ? peep->toilet : 0);
    }
    JSValue ScGuest::toilet_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            peep->toilet = value;
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::mass_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        return JS_NewUint32(ctx, peep != nullptr ? peep->mass : 0);
    }
    JSValue ScGuest::mass_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            peep->mass = value;
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::minIntensity_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        return JS_NewUint32(ctx, peep != nullptr ? peep->intensity.getMinimum() : 0);
    }
    JSValue ScGuest::minIntensity_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            peep->intensity = peep->intensity.withMinimum(value);
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::maxIntensity_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        return JS_NewUint32(ctx, peep != nullptr ? peep->intensity.getMaximum() : 0);
    }
    JSValue ScGuest::maxIntensity_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            peep->intensity = peep->intensity.withMaximum(value);
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::nauseaTolerance_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        return JS_NewUint32(ctx, peep != nullptr ? EnumValue(peep->nauseaTolerance) : 0);
    }
    JSValue ScGuest::nauseaTolerance_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            peep->nauseaTolerance = static_cast<PeepNauseaTolerance>(std::min<uint8_t>(value, 3));
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::cash_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        return JS_NewInt32(ctx, peep != nullptr ? peep->cashInPocket : 0);
    }
    JSValue ScGuest::cash_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_INT32(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            peep->cashInPocket = std::max(0, value);
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::isInPark_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        return JS_NewBool(ctx, peep != nullptr && !peep->outsideOfPark);
    }

    JSValue ScGuest::isLost_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        return JS_NewBool(ctx, peep != nullptr && peep->guestIsLostCountdown < 90);
    }

    JSValue ScGuest::lostCountdown_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        return JS_NewUint32(ctx, peep != nullptr ? peep->guestIsLostCountdown : 0);
    }
    JSValue ScGuest::lostCountdown_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(value, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            peep->guestIsLostCountdown = value;
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::favouriteRide_get(JSContext* ctx, JSValue thisVal)
    {
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            if (peep->favouriteRide != RideId::GetNull())
            {
                return JS_NewUint32(ctx, peep->favouriteRide.ToUnderlying());
            }
        }
        return JS_NULL;
    }

    JSValue ScGuest::favouriteRide_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            auto& gameState = getGameState();
            if (JS_IsNull(jsValue))
            {
                peep->favouriteRide = RideId::GetNull();
            }
            else if (JS_IsNumber(jsValue))
            {
                JS_UNPACK_UINT32(rideId, ctx, jsValue);
                if (rideId < gameState.rides.size() && gameState.rides[rideId].type != kRideTypeNull)
                {
                    peep->favouriteRide = RideId::FromUnderlying(rideId);
                }
            }
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::thoughts_get(JSContext* ctx, JSValue thisVal)
    {
        auto array = JS_NewArray(ctx);

        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            auto index = 0;
            for (const auto& thought : peep->thoughts)
            {
                if (thought.type == PeepThoughtType::none)
                    break;
                if (thought.freshness == 0)
                    continue;
                auto scThought = gScThought.New(ctx, thought);
                JS_SetPropertyInt64(ctx, array, index++, scThought);
            }
        }

        return array;
    }

    JSValue ScGuest::items_get(JSContext* ctx, JSValue thisVal)
    {
        auto array = JS_NewArray(ctx);

        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            auto index = 0;
            for (const auto& itemEnumPair : ShopItemMap)
            {
                auto shopItem = itemEnumPair.second;
                if (!peep->hasItem(shopItem))
                {
                    continue;
                }

                // GuestItem
                auto obj = JS_NewObject(ctx);
                JS_SetPropertyStr(ctx, obj, "type", JSFromStdString(ctx, itemEnumPair.first));

                if (shopItem == ShopItem::voucher)
                {
                    // Voucher
                    auto voucherType = VoucherTypeMap[peep->voucherType];
                    if (!voucherType.empty())
                    {
                        JS_SetPropertyStr(ctx, obj, "voucherType", JSFromStdString(ctx, voucherType));
                        if (peep->voucherType == VOUCHER_TYPE_RIDE_FREE)
                        {
                            // RideVoucher
                            JS_SetPropertyStr(ctx, obj, "rideId", JS_NewUint32(ctx, peep->voucherRideId.ToUnderlying()));
                        }
                        else if (peep->voucherType == VOUCHER_TYPE_FOOD_OR_DRINK_FREE)
                        {
                            // FoodDrinkVoucher
                            auto voucherItem = ShopItemMap[peep->voucherShopItem];
                            if (!voucherItem.empty())
                            {
                                JS_SetPropertyStr(ctx, obj, "item", JSFromStdString(ctx, voucherItem));
                            }
                        }
                    }
                }
                else if (GetShopItemDescriptor(shopItem).IsPhoto())
                {
                    // GuestPhoto
                    RideId rideId;
                    switch (shopItem)
                    {
                        case ShopItem::photo:
                            rideId = peep->photo1RideRef;
                            break;
                        case ShopItem::photo2:
                            rideId = peep->photo2RideRef;
                            break;
                        case ShopItem::photo3:
                            rideId = peep->photo3RideRef;
                            break;
                        case ShopItem::photo4:
                            rideId = peep->photo4RideRef;
                            break;
                        default:
                            // This should not be possible
                            JS_ThrowPlainError(ctx, "Item is photo without a ride ref.");
                            return JS_EXCEPTION;
                    }

                    JS_SetPropertyStr(ctx, obj, "rideId", JS_NewUint32(ctx, rideId.ToUnderlying()));
                }

                JS_SetPropertyInt64(ctx, array, index++, obj);
            }
        }

        return array;
    }

    bool ScGuest::has_item(JSContext* ctx, JSValue thisVal, JSValue item)
    {
        auto peep = GetGuest(thisVal);
        if (peep == nullptr)
        {
            return false;
        }

        auto type = JSToOptionalStdString(ctx, item, "type");
        if (!type.has_value())
        {
            return false;
        }

        // GuestItem
        auto shopItem = ShopItemMap.TryGet(type.value());
        if (!shopItem || !peep->hasItem(*shopItem))
        {
            return false;
        }

        if (*shopItem == ShopItem::voucher)
        {
            auto voucherType = JSToOptionalStdString(ctx, item, "voucherType");
            if (voucherType.has_value())
            {
                // Voucher
                auto voucher = VoucherTypeMap.TryGet(voucherType.value());
                if (!voucher || *voucher != peep->voucherType)
                {
                    return false;
                }

                if (*voucher == VOUCHER_TYPE_RIDE_FREE)
                {
                    auto rideId = JSToOptionalUint(ctx, item, "rideId");
                    if (rideId.has_value())
                    {
                        // RideVoucher
                        if (rideId.value() != peep->voucherRideId.ToUnderlying())
                        {
                            return false;
                        }
                    }
                }
                else if (*voucher == VOUCHER_TYPE_FOOD_OR_DRINK_FREE)
                {
                    auto foodItem = JSToOptionalStdString(ctx, item, "item");
                    if (foodItem.has_value())
                    {
                        // FoodDrinkVoucher
                        auto voucherItem = ShopItemMap.TryGet(foodItem.value());
                        if (!voucherItem || *voucherItem != peep->voucherShopItem)
                        {
                            return false;
                        }
                    }
                }
            }
        }
        else if (GetShopItemDescriptor(*shopItem).IsPhoto())
        {
            // GuestPhoto
            auto rideId = JSToOptionalUint(ctx, item, "rideId");
            if (rideId.has_value())
            {
                switch (*shopItem)
                {
                    case ShopItem::photo:
                        if (rideId.value() != peep->photo1RideRef.ToUnderlying())
                        {
                            return false;
                        }
                        break;
                    case ShopItem::photo2:
                        if (rideId.value() != peep->photo2RideRef.ToUnderlying())
                        {
                            return false;
                        }
                        break;
                    case ShopItem::photo3:
                        if (rideId.value() != peep->photo3RideRef.ToUnderlying())
                        {
                            return false;
                        }
                        break;
                    case ShopItem::photo4:
                        if (rideId.value() != peep->photo4RideRef.ToUnderlying())
                        {
                            return false;
                        }
                        break;
                    default:
                        return false;
                }
            }
        }
        return true;
    }

    JSValue ScGuest::has_item(JSContext* ctx, JSValue thisVal, int argc, JSValue* argv)
    {
        JS_UNPACK_OBJECT(item, ctx, argv[0]);
        auto result = has_item(ctx, thisVal, item);
        return JS_NewBool(ctx, result);
    }

    JSValue ScGuest::give_item(JSContext* ctx, JSValue thisVal, int argc, JSValue* argv)
    {
        JS_UNPACK_OBJECT(item, ctx, argv[0]);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();

        auto peep = GetGuest(thisVal);
        if (peep == nullptr)
        {
            return JS_UNDEFINED;
        }

        // GuestItem
        auto type = JSToOptionalStdString(ctx, item, "type");
        if (!type.has_value())
        {
            JS_ThrowPlainError(ctx, "Invalid 'type'.");
            return JS_EXCEPTION;
        }

        auto shopItem = ShopItemMap.TryGet(type.value());
        if (!shopItem)
        {
            JS_ThrowPlainError(ctx, "Invalid 'type'.");
            return JS_EXCEPTION;
        }

        if (*shopItem == ShopItem::voucher)
        {
            // Voucher
            auto voucherTypeName = JSToOptionalStdString(ctx, item, "voucherType");
            if (!voucherTypeName.has_value())
            {
                JS_ThrowPlainError(ctx, "Invalid 'voucherType'.");
                return JS_EXCEPTION;
            }

            auto voucherType = VoucherTypeMap.TryGet(voucherTypeName.value());
            if (!voucherType)
            {
                JS_ThrowPlainError(ctx, "Invalid 'voucherType'.");
                return JS_EXCEPTION;
            }

            if (*voucherType == VOUCHER_TYPE_RIDE_FREE)
            {
                // RideVoucher
                auto rideId = JSToOptionalUint(ctx, item, "rideId");
                if (!rideId.has_value())
                {
                    JS_ThrowPlainError(ctx, "Invalid 'rideId'.");
                    return JS_EXCEPTION;
                }

                peep->voucherRideId = RideId::FromUnderlying(rideId.value());
            }
            else if (*voucherType == VOUCHER_TYPE_FOOD_OR_DRINK_FREE)
            {
                // FoodDrinkVoucher
                auto itemName = JSToOptionalStdString(ctx, item, "item");
                if (!itemName.has_value())
                {
                    JS_ThrowPlainError(ctx, "Invalid 'item' (for food/drink voucher).");
                    return JS_EXCEPTION;
                }

                auto voucherItem = ShopItemMap.TryGet(itemName.value());
                if (!voucherItem)
                {
                    JS_ThrowPlainError(ctx, "Invalid 'item' (for food/drink voucher).");
                    return JS_EXCEPTION;
                }

                peep->voucherShopItem = *voucherItem;
            }

            peep->voucherType = *voucherType;
        }
        else if (GetShopItemDescriptor(*shopItem).IsPhoto())
        {
            // GuestPhoto
            auto rideId = JSToOptionalUint(ctx, item, "rideId");
            if (!rideId.has_value())
            {
                JS_ThrowPlainError(ctx, "Invalid 'rideId'.");
                return JS_EXCEPTION;
            }

            switch (*shopItem)
            {
                case ShopItem::photo:
                    peep->photo1RideRef = RideId::FromUnderlying(rideId.value());
                    break;
                case ShopItem::photo2:
                    peep->photo2RideRef = RideId::FromUnderlying(rideId.value());
                    break;
                case ShopItem::photo3:
                    peep->photo3RideRef = RideId::FromUnderlying(rideId.value());
                    break;
                case ShopItem::photo4:
                    peep->photo4RideRef = RideId::FromUnderlying(rideId.value());
                    break;
                default:
                    return JS_UNDEFINED;
            }
        }

        peep->giveItem(*shopItem);
        peep->updateAnimationGroup();
        return JS_UNDEFINED;
    }

    JSValue ScGuest::remove_item(JSContext* ctx, JSValue thisVal, int argc, JSValue* argv)
    {
        JS_UNPACK_OBJECT(item, ctx, argv[0]);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        if (has_item(ctx, thisVal, item))
        {
            auto type = JSToStdString(ctx, item, "type");

            // Since guests can only have one item of a type and this item matches, remove it.
            auto peep = GetGuest(thisVal);
            peep->removeItem(ShopItemMap[type]);
            peep->updateAnimationGroup();
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::remove_all_items(JSContext* ctx, JSValue thisVal, int argc, JSValue* argv)
    {
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();
        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            peep->removeAllItems();
            peep->updateAnimationGroup();
        }
        return JS_UNDEFINED;
    }

    JSValue ScGuest::availableAnimations_get(JSContext* ctx, JSValue thisVal)
    {
        auto availableAnimations = JS_NewArray(ctx);
        auto index = 0;
        for (auto& animation : getAnimationsByPeepType(AnimationPeepType::guest))
        {
            JS_SetPropertyInt64(ctx, availableAnimations, index++, JSFromStdString(ctx, animation.first));
        }
        return availableAnimations;
    }

    JSValue ScGuest::getAnimationSpriteIds(JSContext* ctx, JSValue thisVal, int argc, JSValue* argv)
    {
        JS_UNPACK_STR(groupKey, ctx, argv[0]);
        JS_UNPACK_UINT32(rotation, ctx, argv[1]);
        JSValue spriteIds = JS_NewArray(ctx);

        auto& availableGuestAnimations = getAnimationsByPeepType(AnimationPeepType::guest);
        auto animationType = availableGuestAnimations.TryGet(groupKey);
        if (animationType == std::nullopt)
        {
            return spriteIds;
        }

        auto peep = GetGuest(thisVal);
        if (peep != nullptr)
        {
            auto& objManager = GetContext()->GetObjectManager();
            auto* animObj = objManager.GetLoadedObject<PeepAnimationsObject>(peep->animationObjectIndex);

            const auto& animationGroup = animObj->GetPeepAnimation(peep->animationGroup, *animationType);
            auto idx = 0;
            for (auto frameOffset : animationGroup.frameOffsets)
            {
                auto imageId = animationGroup.baseImage;
                if (animationType != PeepAnimationType::hanging)
                    imageId += rotation + frameOffset * 4;
                else
                    imageId += frameOffset;

                JS_SetPropertyInt64(ctx, spriteIds, idx++, JS_NewUint32(ctx, imageId));
            }
        }
        return spriteIds;
    }

    JSValue ScGuest::animation_get(JSContext* ctx, JSValue thisVal)
    {
        auto* peep = GetGuest(thisVal);
        if (peep == nullptr)
        {
            return JSFromStdString(ctx, "");
        }

        auto& availableGuestAnimations = getAnimationsByPeepType(AnimationPeepType::guest);
        std::string_view action = availableGuestAnimations[peep->animationType];

        // Special consideration for sitting peeps
        // TODO: something funky going on in the state machine
        if (peep->animationType == PeepAnimationType::walking && peep->state == PeepState::sitting)
            action = availableGuestAnimations[PeepAnimationType::sittingIdle];

        return JSFromStdString(ctx, action);
    }

    JSValue ScGuest::animation_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_STR(groupKey, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();

        auto& availableGuestAnimations = getAnimationsByPeepType(AnimationPeepType::guest);
        auto newType = availableGuestAnimations.TryGet(groupKey);
        if (newType == std::nullopt)
        {
            JS_ThrowPlainError(ctx, "Invalid animation for this guest (%s)", groupKey.data());
            return JS_EXCEPTION;
        }

        auto* peep = GetGuest(thisVal);
        peep->animationType = peep->nextAnimationType = *newType;

        auto offset = 0;
        if (peep->isActionWalking())
            peep->walkingAnimationFrameNum = offset;
        else
            peep->animationFrameNum = offset;

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

        const auto& animationGroup = animObj->GetPeepAnimation(peep->animationGroup, peep->animationType);
        peep->animationImageIdOffset = animationGroup.frameOffsets[offset];
        peep->invalidate();
        peep->updateSpriteBoundingBox();
        peep->invalidate();
        return JS_UNDEFINED;
    }

    JSValue ScGuest::animationOffset_get(JSContext* ctx, JSValue thisVal)
    {
        auto* peep = GetGuest(thisVal);
        if (peep == nullptr)
        {
            return JS_NewUint32(ctx, 0);
        }

        auto frame = peep->isActionWalking() ? peep->walkingAnimationFrameNum : peep->animationFrameNum;
        return JS_NewUint32(ctx, frame);
    }

    JSValue ScGuest::animationOffset_set(JSContext* ctx, JSValue thisVal, JSValue jsValue)
    {
        JS_UNPACK_UINT32(offset, ctx, jsValue);
        JS_THROW_IF_GAME_STATE_NOT_MUTABLE();

        auto* peep = GetGuest(thisVal);

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

        const auto& animationGroup = animObj->GetPeepAnimation(peep->animationGroup, peep->animationType);
        auto length = animationGroup.frameOffsets.size();
        offset %= length;

        if (peep->isActionWalking())
            peep->walkingAnimationFrameNum = offset;
        else
            peep->animationFrameNum = offset;

        peep->animationImageIdOffset = animationGroup.frameOffsets[offset];
        peep->updateSpriteBoundingBox();
        return JS_UNDEFINED;
    }

    JSValue ScGuest::animationLength_get(JSContext* ctx, JSValue thisVal)
    {
        auto* peep = GetGuest(thisVal);
        if (peep == nullptr)
        {
            return JS_NewUint32(ctx, 0);
        }

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

        const auto& animationGroup = animObj->GetPeepAnimation(peep->animationGroup, peep->animationType);
        return JS_NewUint32(ctx, static_cast<uint32_t>(animationGroup.frameOffsets.size()));
    }

    using OpaqueThoughtData = struct
    {
        PeepThought thought;
    };

    JSValue ScThought::New(JSContext* ctx, PeepThought thought)
    {
        return MakeWithOpaque(ctx, new OpaqueThoughtData{ thought });
    }

    void ScThought::Register(JSContext* ctx)
    {
        static constexpr JSCFunctionListEntry funcs[] = {
            JS_CGETSET_DEF("type", &ScThought::type_get, nullptr),
            JS_CGETSET_DEF("item", &ScThought::item_get, nullptr),
            JS_CGETSET_DEF("freshness", &ScThought::freshness_get, nullptr),
            JS_CGETSET_DEF("freshTimeout", &ScThought::freshTimeout_get, nullptr),
            JS_CFUNC_DEF("toString", 0, &ScThought::toString),
        };
        RegisterBase(ctx, "Thought", Finalize, funcs);
    }

    void ScThought::Finalize(JSRuntime* rt, JSValue thisVal)
    {
        OpaqueThoughtData* data = gScThought.GetOpaque<OpaqueThoughtData*>(thisVal);
        if (data)
            delete data;
    }

    PeepThought ScThought::GetThought(JSValue thisVal)
    {
        OpaqueThoughtData* data = gScThought.GetOpaque<OpaqueThoughtData*>(thisVal);
        return data->thought;
    }

    JSValue ScThought::type_get(JSContext* ctx, JSValue thisVal)
    {
        auto thought = GetThought(thisVal);
        return JSFromStdString(ctx, ThoughtTypeMap[thought.type]);
    }

    JSValue ScThought::item_get(JSContext* ctx, JSValue thisVal)
    {
        auto thought = GetThought(thisVal);
        return JS_NewUint32(ctx, thought.item);
    }

    JSValue ScThought::freshness_get(JSContext* ctx, JSValue thisVal)
    {
        auto thought = GetThought(thisVal);
        return JS_NewUint32(ctx, thought.freshness);
    }

    JSValue ScThought::freshTimeout_get(JSContext* ctx, JSValue thisVal)
    {
        auto thought = GetThought(thisVal);
        return JS_NewUint32(ctx, thought.fresh_timeout);
    }

    JSValue ScThought::toString(JSContext* ctx, JSValue thisVal, int argc, JSValue* argv)
    {
        auto thought = GetThought(thisVal);
        // format string with arguments
        auto ft = Formatter();
        PeepThoughtSetFormatArgs(&thought, ft);
        auto result = FormatStringIDLegacy(STR_STRINGID, ft.Data());
        return JSFromStdString(ctx, result);
    }

} // namespace OpenRCT2::Scripting

#endif
