/* ----------------------------------------------------------------------
    CRM64Pro GDK - Roberto Prieto
    Copyright (C) 2013-2026 MegaStorm Systems

    This software is provided 'as-is', without any express or implied
    warranty. In no event will the authors be held liable for any damages
    arising from the use of this software.

    Permission is granted to anyone to use this software for any purpose,
    including commercial applications, and to alter it and redistribute it
    freely, subject to the following restrictions:

    1. The origin of this software must not be misrepresented; you must not
       claim that you wrote the original software.
    2. Altered source versions must be plainly marked as such, and must not be
       misrepresented as being the original software.
    3. This notice may not be removed or altered from any source distribution.

------------------------------------------------------------------------
              Tutorial 16 Scene Adventure (Point&Click Basics)
------------------------------------------------------------------------

    Overview:
    This tutorial loads a point-and-click adventure map, moves the player with
    the mouse, picks up a key, and opens a matching door.

    Key concepts:
    - Load a TMX map with SceneMgr.
    - Use TMX object properties for gameplay data.
    - Query an object layer from the mouse position.
    - Move a SceneObject inside a polygon area.
    - Use a Screen render callback to draw the Scene.

    Layer layout:
    - Layer 1: Image world background.
    - Layer 2: Object layer (player, area_player, interactables).

    Controls:
    - Left-Click: Interact / pick up.
    - Right-Click: Move player.
    - S: Toggle smooth scroll.
    - D: Toggle object debug AABB and trigger zones.
    - Q / ESC: Quit.

    Loop order:
    - Poll events and capture mouse button edges.
    - Convert the mouse position to world coordinates.
    - Resolve hover, interaction and movement targets.
    - Run one fixed logic tick via Scene::update().
    - Let Scene update the camera.
    - Render from callback using Scene::render().
------------------------------------------------------------------------ */

#include "CRM64Pro.h"
#include "RunParameter.h"

using namespace CRM64Pro;

#define BASEDIR "Base/"
#ifdef CRM64PRO_PLATFORM_WINDOWS
#define OUTPUTDIR "Win64/"
#elif defined(CRM64PRO_PLATFORM_LINUX)
#define OUTPUTDIR "Linux/"
#elif defined(CRM64PRO_PLATFORM_MACOS)
#define OUTPUTDIR "macOS/"
#endif

#define RESOURCE_BG_CDC_NAME "Tutorial_bg"
#define OUTPUT_CDC OUTPUTDIR"Tutorial.cdc"

// Screen and viewport size.
static const Sint32 iScreenW = 960;
static const Sint32 iScreenH = 540;
static const Sint32 iViewW = 600;
static const Sint32 iViewH = 500;
static const Sint32 iViewX = (iScreenW - iViewW) / 2;
static const Sint32 iViewY = 0;

// Logic timing.
static const Sint32 iLogicRate = 20;
static const float fPlayerSpeed = 80.0f;
static const float fInteractDistance = 20.0f;
static const Uint32 iSpeechDurationMS = 1700;

struct TutorialState
{
    Scene* pScene = nullptr;
    Sint32 idBgImage = -1;
    Sint32 idGuiImage = -1;
    Sint32 idKeyImage = -1;
    Sint32 idBuiltInFont = -1;
    string sSpeech;
    Uint64 iSpeechUntil = 0;
    SceneLayerObject* pObjLayer = nullptr;
    SceneObject* pPlayer = nullptr;
    SceneObject* pAreaPlayer = nullptr;
    float fTargetX = 0.0f;
    float fTargetY = 0.0f;
    bool bMoving = false;
    Sint32 iCollectedKeyDoor = -1;
    bool bHasKeyInInventory = false;
    bool bGameFinished = false;
};

static TutorialState* g_pState = nullptr;

static void updatePlayerMovement(float fDeltaTime);
static void onLayerRenderSpeech(const SceneLayerContext& rContext);

class AdventureRenderObject : public SceneObject
{
public:
    void update(Scene*, SceneLayerContext& rContext) override
    {
        if(!g_pState) return;
        if(g_pState->pPlayer && getID() == g_pState->pPlayer->getID())
        {
            updatePlayerMovement(rContext.fDeltaTime);
        }
    }
};

class TutorialObjectFactory : public ISceneObjectFactory
{
public:
    SceneObject* create(const string&) override
    {
        return new(std::nothrow) AdventureRenderObject();
    }
};

// Show a speech message for the requested duration.
static void setSpeech(const string& sText, Uint32 iDurationMS = iSpeechDurationMS)
{
    g_pState->sSpeech = sText;
    g_pState->iSpeechUntil = Main::instance().timer().getTicks() + static_cast<Uint64>(iDurationMS);
}

// Check whether a hotspot is inside the player area.
static bool isInsidePlayerArea(float fHotX, float fHotY)
{
    if(!g_pState) return true;
    if(!g_pState->pAreaPlayer) return true;
    return g_pState->pAreaPlayer->containsPoint(fHotX, fHotY);
}

// Find the closest point on the player area boundary.
static bool nearestPointOnAreaBoundary(float px, float py, float& rOutX, float& rOutY)
{
    if(!g_pState) return false;
    if(!g_pState->pAreaPlayer) return false;
    const SceneShapeData& shape = g_pState->pAreaPlayer->getShape();
    if(shape.eType != SST_POLYGON || shape.vPoints.size() < 2) return false;

    const float baseX = g_pState->pAreaPlayer->getX();
    const float baseY = g_pState->pAreaPlayer->getY();
    float bestDist2 = 1.0e30f;
    float bestX = px;
    float bestY = py;
    const size_t n = shape.vPoints.size();

    for(size_t i = 0; i < n; ++i)
    {
        const SDL_FPoint& a = shape.vPoints[i];
        const SDL_FPoint& b = shape.vPoints[(i + 1) % n];
        const float ax = baseX + a.x;
        const float ay = baseY + a.y;
        const float bx = baseX + b.x;
        const float by = baseY + b.y;
        const float abx = bx - ax;
        const float aby = by - ay;
        const float apx = px - ax;
        const float apy = py - ay;
        const float abLen2 = (abx * abx) + (aby * aby);

        float t = 0.0f;
        if(abLen2 > 0.00001f)
        {
            t = ((apx * abx) + (apy * aby)) / abLen2;
            if(t < 0.0f) t = 0.0f;
            if(t > 1.0f) t = 1.0f;
        }

        const float qx = ax + (abx * t);
        const float qy = ay + (aby * t);
        const float dx = px - qx;
        const float dy = py - qy;
        const float d2 = (dx * dx) + (dy * dy);
        if(d2 < bestDist2)
        {
            bestDist2 = d2;
            bestX = qx;
            bestY = qy;
        }
    }

    rOutX = bestX;
    rOutY = bestY;
    return true;
}

// Keep a hotspot inside the player area.
static bool constrainHotspotToPlayerArea(float oldHotX, float oldHotY, float newHotX, float newHotY, float& rOutHotX, float& rOutHotY)
{
    if(isInsidePlayerArea(newHotX, newHotY))
    {
        rOutHotX = newHotX;
        rOutHotY = newHotY;
        return true;
    }

    if(isInsidePlayerArea(newHotX, oldHotY))
    {
        rOutHotX = newHotX;
        rOutHotY = oldHotY;
        return true;
    }

    if(isInsidePlayerArea(oldHotX, newHotY))
    {
        rOutHotX = oldHotX;
        rOutHotY = newHotY;
        return true;
    }

    float edgeX = oldHotX;
    float edgeY = oldHotY;
    if(nearestPointOnAreaBoundary(newHotX, newHotY, edgeX, edgeY))
    {
        rOutHotX = edgeX;
        rOutHotY = edgeY;
        return true;
    }

    return false;
}

// Check whether the player is close enough to an object.
static bool isPlayerNearObject(const SceneObject* pObj, float fMaxDistance)
{
    if(!g_pState) return false;
    if(!pObj || !g_pState->pPlayer) return false;

    // Player proximity uses full player box (not a single hotspot point).
    const float pX = g_pState->pPlayer->getX();
    const float pY = g_pState->pPlayer->getY();
    const float pW = g_pState->pPlayer->getWidth();
    const float pH = g_pState->pPlayer->getHeight();
    const float pMinX = pX;
    const float pMinY = pY;
    const float pMaxX = pX + pW;
    const float pMaxY = pY + pH;

    const Physics::AABB& aabb = pObj->getShape().AABB;
    const float oMinX = aabb.minX - fMaxDistance;
    const float oMinY = aabb.minY - fMaxDistance;
    const float oMaxX = aabb.maxX + fMaxDistance;
    const float oMaxY = aabb.maxY + fMaxDistance;

    const bool bOverlapX = (pMaxX >= oMinX) && (pMinX <= oMaxX);
    const bool bOverlapY = (pMaxY >= oMinY) && (pMinY <= oMaxY);
    return bOverlapX && bOverlapY;
}

// Adapt the renderer to the engine callback signature.
static Sint32 renderWrapper(Sint32)
{
    if(!g_pState || !g_pState->pScene) return 0;

    if(g_pState->idBgImage > 0)
    {
        Image* pBG = Main::instance().imageMgr().get(g_pState->idBgImage);
        if(pBG) pBG->render();
    }

    g_pState->pScene->render();

    // UI overlay (screen space): centered in the strip below the viewport.
    Image* pGUI = (g_pState->idGuiImage > 0) ? Main::instance().imageMgr().get(g_pState->idGuiImage) : nullptr;
    if(pGUI)
    {
        const float fGuiW = static_cast<float>(pGUI->getWidth());
        const float fGuiH = static_cast<float>(pGUI->getHeight());
        const float fGuiX = (static_cast<float>(iScreenW) - fGuiW) * 0.5f;
        const float fGuiAreaY = static_cast<float>(iViewY + iViewH);
        const float fGuiAreaH = static_cast<float>(iScreenH - (iViewY + iViewH));
        const float fGuiY = fGuiAreaY + ((fGuiAreaH - fGuiH) * 0.5f);
        SDL_FRect rGuiDst = { fGuiX, fGuiY, fGuiW, fGuiH };
        pGUI->render(0, nullptr, &rGuiDst);

        if(g_pState->bHasKeyInInventory && g_pState->idKeyImage > 0)
        {
            Image* pKey = Main::instance().imageMgr().get(g_pState->idKeyImage);
            if(pKey)
            {
                const float fKeyW = static_cast<float>(pKey->getWidth());
                const float fKeyH = static_cast<float>(pKey->getHeight());
                // Fixed inventory slot over gui.png (left-side slot).
                const float fKeyX = fGuiX + 10.0f;
                const float fKeyY = fGuiY + ((fGuiH - fKeyH) * 0.5f);
                SDL_FRect rKeyDst = { fKeyX, fKeyY, fKeyW, fKeyH };
                pKey->render(0, nullptr, &rKeyDst);
            }
        }
    }

    return 0;
}

// Draw the active speech message on the scene layer.
static void onLayerRenderSpeech(const SceneLayerContext& rContext)
{
    if(rContext.idLayer != 2) return;
    if(g_pState->sSpeech.empty()) return;
    if(Main::instance().timer().getTicks() > g_pState->iSpeechUntil) return;

    Font* pFont = Main::instance().fontMgr().get(g_pState->idBuiltInFont);
    if(!pFont) return;

    float fTextX = static_cast<float>(rContext.rViewport.x + 14);
    float fTextY = static_cast<float>(rContext.rViewport.y + 14);

    if(g_pState->pPlayer)
    {
        SDL_FPoint pScreen = rContext.worldToScreen(
            g_pState->pPlayer->getX() + (g_pState->pPlayer->getWidth() * 0.5f) + 16.0f,
            g_pState->pPlayer->getY() - 20.0f, true);
        fTextX = pScreen.x;
        fTextY = pScreen.y;
    }

    if(fTextX < static_cast<float>(rContext.rViewport.x)) fTextX = static_cast<float>(rContext.rViewport.x);
    if(fTextY < static_cast<float>(rContext.rViewport.y)) fTextY = static_cast<float>(rContext.rViewport.y);

    pFont->setPosition(fTextX, fTextY);
    pFont->render(g_pState->sSpeech.c_str());
}

// Load the map and find the objects used by the tutorial.
static bool loadMapAndResolveObjects()
{
    if(!g_pState) return false;
    Main& mC64 = Main::instance();
    Log* pLog = mC64.logMgr().get();
    static TutorialObjectFactory sTutorialObjectFactory;

    const Sint32 iTE = mC64.sceneMgr().loadFromFile(BASEDIR "tiled/adventure/adventure.tmx", &sTutorialObjectFactory);
    g_pState->pScene = mC64.sceneMgr().get(iTE);
    if(!g_pState->pScene)
    {
        if(pLog) pLog->msg(LL_ERROR, "Failed to load adventure map.\n");
        return false;
    }

    SDL_Rect rView = { iViewX, iViewY, iViewW, iViewH };
    if(!g_pState->pScene->setViewport(&rView) ||
        !g_pState->pScene->setLayerFeatures(1, SLF_UPDATE | SLF_RENDER, true) ||
        !g_pState->pScene->setLayerFeatures(2, SLF_UPDATE | SLF_RENDER, true) ||
        !g_pState->pScene->setLayerFeatures(1, SLF_SMOOTHSCROLL, true) ||
        !g_pState->pScene->setLayerFeatures(2, SLF_SMOOTHSCROLL, true) ||
        !g_pState->pScene->setLayerOnRenderEnd(2, onLayerRenderSpeech))
    {
        if(pLog) pLog->msg(LL_ERROR, "Failed to configure adventure Scene layers.\n");
        return false;
    }

    if(pLog)
    {
        SceneLayerImage* pL1 = g_pState->pScene->accessLayerImage(1);
        SceneLayerObject* pL2 = g_pState->pScene->accessLayerObject(2);
        const float fL1W = (pL1 ? pL1->getTotalWidth() : -1.0f);
        const float fL1H = (pL1 ? pL1->getTotalHeight() : -1.0f);
        const float fL2W = (pL2 ? pL2->getTotalWidth() : -1.0f);
        const float fL2H = (pL2 ? pL2->getTotalHeight() : -1.0f);

        pLog->msg(LL_INFO, "Adventure: layer 1 image size: %.1fx%.1f | layer 2 object size: %.1fx%.1f | Scene size: %dx%d\n",
            fL1W, fL1H, fL2W, fL2H, g_pState->pScene->getWidth(), g_pState->pScene->getHeight());
    }

    g_pState->pObjLayer = g_pState->pScene->accessLayerObject(2);
    if(!g_pState->pObjLayer)
    {
        if(pLog) pLog->msg(LL_ERROR, "Object layer not found at index 2.\n");
        return false;
    }
    g_pState->pObjLayer->setRenderOrderMode(SORO_FEET);

    vector<SceneObject*> vObjects;
    g_pState->pObjLayer->queryRegion(0.0f, 0.0f, 100000.0f, 100000.0f, vObjects);

    for(SceneObject* pObj : vObjects)
    {
        if(pLog)
        {
            pLog->msg(LL_INFO, "Adventure object loaded: id=%d name='%s' type='%s'\n",
                pObj->getID(), pObj->getName().c_str(), pObj->getType().c_str());
        }
        const string& rType = pObj->getType();
        if(rType.empty() ||
            (rType != "player" &&
             rType != "area_player" &&
             rType != "interact"))
        {
            if(pLog)
            {
                pLog->msg(LL_WARNING,
                    "Unsupported adventure object type: id=%d name='%s' type='%s'\n",
                    pObj->getID(), pObj->getName().c_str(), pObj->getType().c_str());
            }
        }

        if((pObj->getType() == "player" || pObj->getName() == "player") && !g_pState->pPlayer)
        {
            g_pState->pPlayer = pObj;
            g_pState->fTargetX = pObj->getX() + (pObj->getWidth() * 0.5f);
            g_pState->fTargetY = pObj->getY() + pObj->getHeight();
        }
        else if((pObj->getType() == "area_player" || pObj->getName() == "area_player") && !g_pState->pAreaPlayer)
        {
            g_pState->pAreaPlayer = pObj;
        }
    }

    if(!g_pState->pPlayer)
    {
        if(pLog) pLog->msg(LL_ERROR, "Player object not found (name/type: player).\n");
        return false;
    }
    if(!g_pState->pAreaPlayer && pLog) pLog->msg(LL_WARNING, "area_player object not found.\n");

    SceneCameraParams mCam;
    mCam.fDamping = 10.0f;
    mCam.bClampToBounds = true;
    mCam.ptTargetAnchor = { 0.5f, 1.0f };
    mCam.ptScreenAnchor = { 0.5f, 0.5f };
    if(!g_pState->pScene->setCameraTarget(2, g_pState->pPlayer) ||
        !g_pState->pScene->setCameraParams(2, mCam) ||
        !g_pState->pScene->setCameraMode(2, SCM_SMOOTH))
    {
        if(pLog) pLog->msg(LL_ERROR, "Failed to configure adventure camera.\n");
        return false;
    }

    return true;
}

// Move the player according to the current input.
static void updatePlayerMovement(float fDeltaTime)
{
    if(!g_pState) return;
    if(!g_pState->bMoving || !g_pState->pPlayer) return;

    const float fHotX = g_pState->pPlayer->getX() + (g_pState->pPlayer->getWidth() * 0.5f);
    const float fHotY = g_pState->pPlayer->getY() + g_pState->pPlayer->getHeight();
    const float fDX = g_pState->fTargetX - fHotX;
    const float fDY = g_pState->fTargetY - fHotY;
    const float fDist = sqrtf(fDX * fDX + fDY * fDY);
    const float fStep = fPlayerSpeed * fDeltaTime;

    if(fDist <= fStep || fDist <= 0.001f)
    {
        float fOutHotX = fHotX;
        float fOutHotY = fHotY;
        if(constrainHotspotToPlayerArea(fHotX, fHotY, g_pState->fTargetX, g_pState->fTargetY, fOutHotX, fOutHotY))
        {
            g_pState->pPlayer->setPosition(fOutHotX - (g_pState->pPlayer->getWidth() * 0.5f), fOutHotY - g_pState->pPlayer->getHeight());
        }
        g_pState->bMoving = false;
        return;
    }

    const float fInvDist = 1.0f / fDist;
    const float fNewHotX = fHotX + (fDX * fInvDist * fStep);
    const float fNewHotY = fHotY + (fDY * fInvDist * fStep);
    float fOutHotX = fHotX;
    float fOutHotY = fHotY;

    if(!constrainHotspotToPlayerArea(fHotX, fHotY, fNewHotX, fNewHotY, fOutHotX, fOutHotY))
    {
        g_pState->bMoving = false;
        return;
    }

    g_pState->pPlayer->setPosition(fOutHotX - (g_pState->pPlayer->getWidth() * 0.5f), fOutHotY - g_pState->pPlayer->getHeight());
}

// Close resources in ownership order, including partially completed setup.
static void closeTutorial()
{
    Main& mC64 = Main::instance();
    mC64.sceneMgr().close(0);
    mC64.fontMgr().close(0);
    mC64.imageMgr().close(0);
    Main::terminate();
}

// Run the tutorial application and release its resources.
int main(int argc, char* argv[])
{
    eConfigRendererDriver eRenderer = CRD_SOFTWARE;
    if(!runParameterReadRender(argc, argv, eRenderer)) return 1;

    CMem::setStatsLevel(CMem::MSL_HIGH);
    atexit(CMem::destroy);

    Main& mC64 = Main::instance();
    TutorialState state;
    g_pState = &state;

    Log* pLog = mC64.logMgr().get();

    pLog->init("Tutorial_16_Scene_Adventure", LL_DEBUG, LM_FILE | LM_STDOUT, OUTPUTDIR"Tutorial_16_Scene_Adventure.log");
    if(!mC64.timer().init() || !mC64.timer().setRate(0, iLogicRate))
    {
        if(pLog) pLog->msg(LL_ERROR, "Failed to initialize tutorial timing.\n");
        Main::terminate();
        return -1;
    }
    mC64.configMgr().setMTFriendly(1);

    Screen* pScreen = mC64.configMgr().get();
    pScreen->setSize(iScreenW, iScreenH);
    pScreen->setDriver(eRenderer);
    pScreen->setTitle("Tutorial 16 Scene - Adventure");
    if(pScreen->show() < 0)
    {
        Main::terminate();
        return -1;
    }
    if(!pScreen->setRenderCallback(renderWrapper))
    {
        if(pLog) pLog->msg(LL_ERROR, "Failed to set the render callback.\n");
        Main::terminate();
        return -1;
    }
    g_pState->idBgImage = mC64.imageMgr().load(OUTPUT_CDC, RESOURCE_BG_CDC_NAME);
    g_pState->idGuiImage = mC64.imageMgr().loadFromFile(BASEDIR "tiled/adventure/gui.png");
    g_pState->idKeyImage = mC64.imageMgr().loadFromFile(BASEDIR "tiled/adventure/key.png", "keyGUI");
    g_pState->idBuiltInFont = mC64.fontMgr().getBuiltin("CourierNew10White");
    if(g_pState->idBgImage <= 0 || g_pState->idGuiImage <= 0 || g_pState->idKeyImage <= 0 ||
        g_pState->idBuiltInFont <= 0 || !mC64.fontMgr().get(g_pState->idBuiltInFont))
    {
        if(pLog) pLog->msg(LL_ERROR, "Failed to load adventure background, UI or font resources.\n");
        closeTutorial();
        return -1;
    }

    if(!loadMapAndResolveObjects())
    {
        closeTutorial();
        return -1;
    }

    if(pLog)
    {
        pLog->msg(LL_INFO, "Adventure tutorial loaded.\n");
        pLog->msg(LL_INFO, "\nCONTROLS:\n [Left-Click] Interact with objects\n [Right-Click] Move\n [S] Toggle smooth scroll\n [D] Toggle object AABB\n [Q/Esc] Quit\n\n");
    }

    SDL_Event ev;
    bool bDone = false;
    bool bSmooth = true;

    while(!bDone)
    {
        while(mC64.update(&ev))
        {
            if(ev.type == SDL_EVENT_QUIT) bDone = true;
            if(ev.type == SDL_EVENT_KEY_DOWN)
            {
                if(ev.key.key == SDLK_ESCAPE || ev.key.key == SDLK_Q) bDone = true;
                if(ev.key.key == SDLK_S && g_pState->pScene)
                {
                    bSmooth = !bSmooth;
                    g_pState->pScene->setLayerFeatures(1, SLF_SMOOTHSCROLL, bSmooth);
                    g_pState->pScene->setLayerFeatures(2, SLF_SMOOTHSCROLL, bSmooth);
                    if(pLog) pLog->msg(LL_INFO, "Smooth scroll: %s\n", bSmooth ? "ON" : "OFF");
                }
                if(ev.key.key == SDLK_D && g_pState->pObjLayer)
                {
                    const bool bEnabled = !g_pState->pScene->isDebugObjectOverlayEnabled(SDO_OBJECT_AABB, SDOT_STANDARD);
                    g_pState->pScene->setDebugObjectOverlay(SDO_OBJECT_AABB, SDOT_STANDARD, bEnabled);
                    g_pState->pScene->setDebugObjectOverlay(SDO_TRIGGER_ZONES, SDOT_STANDARD, bEnabled);
                    if(pLog) pLog->msg(LL_INFO, "Debug AABB: %s\n", bEnabled ? "ON" : "OFF");
                }
            }
        }
        if(bDone) break;

        const Sint32 iMouseButtons = mC64.cursorMgr().getButtons();
        const Sint32 iMouseButtonsPrev = mC64.cursorMgr().getButtonsPrev();
        const bool bLeftPressed =
            ((iMouseButtons & SDL_BUTTON_MASK(SDL_BUTTON_LEFT)) != 0) &&
            ((iMouseButtonsPrev & SDL_BUTTON_MASK(SDL_BUTTON_LEFT)) == 0);
        const bool bRightPressed =
            ((iMouseButtons & SDL_BUTTON_MASK(SDL_BUTTON_RIGHT)) != 0) &&
            ((iMouseButtonsPrev & SDL_BUTTON_MASK(SDL_BUTTON_RIGHT)) == 0);

        float fWX = 0.0f;
        float fWY = 0.0f;
        vector<SceneObject*> vHits;

        const bool bCursorWorldValid = (g_pState->pScene && g_pState->pObjLayer &&
            g_pState->pScene->mouseToWorld(&fWX, &fWY, 2));

        if(bCursorWorldValid)
        {
            vector<SceneObject*> vCandidates;
            g_pState->pObjLayer->queryRegion(fWX, fWY, 1.0f, 1.0f, vCandidates);

            for(SceneObject* pObj : vCandidates)
            {
                if(pObj && pObj->containsPoint(fWX, fWY))
                {
                    vHits.push_back(pObj);
                }
            }
        }

        bool bHoverInteract = false;
        for(SceneObject* pObj : vHits)
        {
            if(pObj->getType() == "interact")
            {
                bHoverInteract = true;
                break;
            }
        }
        mC64.cursorMgr().select(bHoverInteract ? SDL_SYSTEM_CURSOR_POINTER : SDL_SYSTEM_CURSOR_DEFAULT);

        if(bLeftPressed)
        {
            for(SceneObject* pObj : vHits)
            {
                if(!pObj || pObj->getType() != "interact") continue;

                if(!isPlayerNearObject(pObj, fInteractDistance))
                {
                    setSpeech("I need to get closer.");
                    continue;
                }

                const bool bPickup = pObj->getPropertyBool("pickup", false);
                const Sint32 iKeyDoor = pObj->getPropertyInt("keydoor", -1);

                if(bPickup)
                {
                    const Sint32 idObject = pObj->getID();
                    const string sObjectName = pObj->getName();
                    if(!g_pState->pObjLayer->closeObject(idObject))
                    {
                        if(pLog) pLog->msg(LL_ERROR, "Failed to pick up '%s'.\n", sObjectName.c_str());
                        setSpeech("I couldn't pick that up.");
                        continue;
                    }
                    if(iKeyDoor >= 0)
                    {
                        g_pState->iCollectedKeyDoor = iKeyDoor;
                        g_pState->bHasKeyInInventory = true;
                    }
                    if(pLog) pLog->msg(LL_INFO, "Picked up '%s' (keydoor=%d)\n", sObjectName.c_str(), iKeyDoor);
                    setSpeech("You got the key.");
                    continue;
                }

                if(iKeyDoor >= 0)
                {
                    if(g_pState->iCollectedKeyDoor == iKeyDoor)
                    {
                        const Sint32 idObject = pObj->getID();
                        const string sObjectName = pObj->getName();
                        if(!g_pState->pObjLayer->closeObject(idObject))
                        {
                            if(pLog) pLog->msg(LL_ERROR, "Failed to unlock '%s'.\n", sObjectName.c_str());
                            setSpeech("The door would not open.");
                            continue;
                        }
                        if(pLog) pLog->msg(LL_INFO, "Unlocked '%s' with keydoor=%d\n", sObjectName.c_str(), iKeyDoor);
                        g_pState->bHasKeyInInventory = false;
                        if(!g_pState->bGameFinished)
                        {
                            g_pState->bGameFinished = true;
                            g_pState->pScene->pause();
                            mC64.tool().messageBox("Adventure", "You unlocked the door. End of game.", MBB_OK, MBT_INFO);
                            g_pState->pScene->resume();
                            bDone = true;
                            break;
                        }
                    }
                    else
                    {
                        if(pLog) pLog->msg(LL_INFO, "'%s' is locked. Missing keydoor=%d\n", pObj->getName().c_str(), iKeyDoor);
                        setSpeech("It's locked.");
                    }
                }
            }
        }

        if(bRightPressed && g_pState->pPlayer && bCursorWorldValid)
        {
            const float fTX = fWX;
            const float fTY = fWY;
            bool bValidTarget = true;
            float fMoveTX = fTX;
            float fMoveTY = fTY;

            if(g_pState->pAreaPlayer)
            {
                bValidTarget = g_pState->pAreaPlayer->containsPoint(fTX, fTY);
                if(!bValidTarget)
                {
                    bValidTarget = nearestPointOnAreaBoundary(fTX, fTY, fMoveTX, fMoveTY);
                }
            }

            if(bValidTarget)
            {
                g_pState->fTargetX = fMoveTX;
                g_pState->fTargetY = fMoveTY;
                g_pState->bMoving = true;
            }
            else if(pLog) pLog->msg(LL_INFO, "Target out of area_player: (%.1f, %.1f)\n", fTX, fTY);
        }

        if(bDone) break;
        g_pState->pScene->update();
    }
    mC64.sceneMgr().info();
    closeTutorial();
    return 0;
}
