/* ----------------------------------------------------------------------
    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 Shooter
------------------------------------------------------------------------

    Overview:
    This tutorial loads a vertical shooter map, creates enemies and a player
    from TMX objects, fires projectiles, and scrolls the background.

    Key concepts:
    - Load a TMX map with a Scene object factory.
    - Create gameplay objects from TMX object types.
    - Use sprites from SceneObject subclasses.
    - Spawn and remove projectiles.
    - Query object collisions from an object layer.
    - Scroll Scene layers with camera autoscroll.
    - Draw the HUD from the Screen render callback.

    Layer layout:
    - Layer 1-2: Autoscrolling world/background layers.
    - Layer 2 object layer: player, enemies, projectiles, effects.

    Controls:
    - Cursor Keys: Move ship.
    - Space: Shoot.
    - S: Toggle smooth scrolling.
    - D: Toggle debug AABB visuals.
    - Q / ESC: Quit.

    Loop order:
    - Poll events and capture toggles.
    - Poll real-time movement/shoot input.
    - Run one fixed logic tick via Scene::update().
    - Evaluate win/lose state after logic update.
    - Render from callback using Scene::render().

    Graphic assets:
    - https://ansimuz.itch.io/spaceship-shooter-environment
------------------------------------------------------------------------ */

#include "CRM64Pro.h"
#include "RunParameter.h"


using namespace CRM64Pro;

// Paths used by the tutorial.
#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 = 480;
static const Sint32 iViewH = 480;
static const Sint32 iViewX = (iScreenW - iViewW) / 2;
static const Sint32 iViewY = (iScreenH - iViewH) / 2;

// Logic timing.
static const Sint32 iLogicRate = 20;

static const float fPlayerSpeed = 150.0f;
static const float fEnemySpeed = 80.0f;
static const float fEntrySpeed = 50.0f;
static const float fAutoScrollX = 0.0f;
static const float fAutoScrollY = -100.0f;
static const Sint32 iShotCooldownTicks = ((250 * iLogicRate) + 999) / 1000; // 250ms => ticks
static const float fPlayerHitShakeAmplitudePx = 10.0f;
static const Uint32 iPlayerHitShakeDurationMs = 500;
static const float fPlayerHitShakeFalloff = 4.5f;

// State shared by callbacks and Scene objects.
struct TutorialState
{
    Scene* pScene = nullptr;
    Sint32 idBgImage = -1;
    Sint32 idHUDFont = -1;
    bool bDebug = false;
    Sint32 idPlayer = -1;
    Sint32 idEnemy1 = -1;
    Sint32 idEnemy2 = -1;
};

static TutorialState* g_pState = nullptr;

// -----------------------------------------------------------------------------------
// Custom game objects.
// -----------------------------------------------------------------------------------

// Base class for all shooter entities.
// Scene object layers manage these objects and include them in spatial queries.
class GameEntity : public SceneObject
{
public:
    Sint32 iIDSprite = -1;
    Sint32 iLife = 1;
    bool   bIsDead = false;

    static Sint32 spawnSpriteChild(Sint32 iBaseSprite)
    {
        if(iBaseSprite < 0) return -1;
        return Main::instance().spriteMgr().child(iBaseSprite);
    }

    virtual ~GameEntity()
    {
        if(iIDSprite >= 0)
        {
            Main::instance().spriteMgr().close(iIDSprite);
            iIDSprite = -1;
        }
    }

    // Scene calls initialize() after creating the object.
    // Load gameplay values from TMX properties here.
    void initialize() override
    {
        SceneObject::initialize();
        iLife = getPropertyInt("life", iLife);
        bIsDead = false;
    }

    virtual void die(Scene* pScene, SceneLayerContext& rContext);

    // Scene calls update() during fixed logic steps.
    void update(Scene* pScene, SceneLayerContext& rContext) override
    {
        if(bIsDead)
        {
            Sprite* pS = Main::instance().spriteMgr().get(iIDSprite);
            // If the death animation (explosion) is finished, remove the object
            if(pS && pS->status() == GS_END)
            {
                SceneLayerObject* pLayer = pScene->accessLayerObject(rContext.idLayer);
                if(pLayer) pLayer->closeObject(getID());
            }
            return;
        }

        // Fixed autoscroll compensation for this tutorial.
        setPosition(getX() + fAutoScrollX * rContext.fDeltaTime, getY() + fAutoScrollY * rContext.fDeltaTime);
    }

    // render: draws the object. 
    // Interpolated world/screen data is provided by Scene in rContext.data.Object.
    void render(Scene* pScene, SceneLayerContext& rContext) override
    {
        if(iIDSprite < 0) return;
        Sprite* pSpr = Main::instance().spriteMgr().get(iIDSprite);
        if(!pSpr) return;

        SDL_FPoint pScreen = rContext.worldToScreen(getX(), getY(), true);
        float fScreenX = rContext.data.Object.bRenderStateValid ? rContext.data.Object.fScreenX : pScreen.x;
        float fScreenY = rContext.data.Object.bRenderStateValid ? rContext.data.Object.fScreenY : pScreen.y;

        pSpr->setPosition(fScreenX, fScreenY);
        pSpr->render();

    }
};

// Explosion: a one-shot visual effect.
class Explosion : public GameEntity
{
public:
    static Sint32 iIDSpriteBase;

    void initialize() override
    {
        GameEntity::initialize();
        iIDSprite = spawnSpriteChild(iIDSpriteBase);
        if(iIDSprite >= 0)
        {
            Sprite* pS = Main::instance().spriteMgr().get(iIDSprite);
            if(pS) pS->selectAnimIndex(0);
        }
    }

    void update(Scene* pScene, SceneLayerContext& rContext) override
    {
        GameEntity::update(pScene, rContext);
        Sprite* pS = Main::instance().spriteMgr().get(iIDSprite);
        if(pS && pS->status() == GS_END)
        {
            SceneLayerObject* pLayer = pScene->accessLayerObject(rContext.idLayer);
            if(pLayer) pLayer->closeObject(getID());
        }
    }
};
Sint32 Explosion::iIDSpriteBase = -1;

// Remove the entity after it is destroyed.
void GameEntity::die(Scene* pScene, SceneLayerContext& rContext)
{
    if(bIsDead) return;
    bIsDead = true;

    // Switch to a per-entity explosion child sprite
    if(iIDSprite >= 0)
    {
        Main::instance().spriteMgr().close(iIDSprite);
        iIDSprite = -1;
    }
    iIDSprite = spawnSpriteChild(Explosion::iIDSpriteBase);
    Sprite* pS = Main::instance().spriteMgr().get(iIDSprite);
    if(!pS)
    {
        SceneLayerObject* pLayer = pScene ? pScene->accessLayerObject(rContext.idLayer) : nullptr;
        if(pLayer) pLayer->closeObject(getID());
        return;
    }
    pS->selectAnimIndex(0);
    pS->resume();
}

// Laser: projectile with collision detection.
class Laser : public GameEntity
{
public:
    static Sint32 iIDLaserBase;
    static Sint32 iIDLaserEnemyBase;
    bool  bIsEnemy = false;
    float fSpeedX = 0.0f;
    float fSpeedY = -500.0f;

    void initialize() override
    {
        if(iIDSprite >= 0) return;
        GameEntity::initialize();
        iIDSprite = spawnSpriteChild(bIsEnemy ? iIDLaserEnemyBase : iIDLaserBase);
        Sprite* pS = Main::instance().spriteMgr().get(iIDSprite);
        if(pS)
        {
            pS->selectAnimIndex(0);
            Sint32 iW, iH;
            pS->getOffset(&iW, &iH);
            setSize((float)iW, (float)iH);
        }
        else setSize(16.0f, 16.0f);
    }

    void update(Scene* pScene, SceneLayerContext& rContext) override
    {
        GameEntity::update(pScene, rContext);
        SceneLayerObject* pLayer = pScene->accessLayerObject(rContext.idLayer);
        if(!pLayer) return;

        // Apply internal movement logic
        setPosition(getX() + fSpeedX * rContext.fDeltaTime, getY() + fSpeedY * rContext.fDeltaTime);

        // Cleanup: remove projectile if it leaves the viewport
        if(fSpeedY < 0)  // Moving UP
        {
            if(getY() + getHeight() < rContext.fScrollY) 
            {
                pLayer->closeObject(getID()); return;
            }
        }
        else { // Moving DOWN
            if(getY() > rContext.fScrollY + rContext.rViewport.h) 
            {
                pLayer->closeObject(getID()); return;
            }
        }

        // HEGA spatial query:
        // Checking collision for a region (in this case, the object layer size).
        vector<SceneObject*> vNeighbors;
        pLayer->queryRegion(getX(), getY(), getWidth(), getHeight(), vNeighbors);

        for(auto* pObj : vNeighbors)
        {
            if(pObj == this) continue;

            bool bHit = false;
            if(!bIsEnemy && pObj->getType() == "enemy") bHit = true;
            else if(bIsEnemy && pObj->getType() == "player") bHit = true;

            SDL_FRect rOverlap;
            if(bHit && overlapsObject(pObj, &rOverlap))
            {
                GameEntity* pTarget = dynamic_cast<GameEntity*>(pObj);
                if(pTarget && !pTarget->bIsDead)
                {
                    // Narrow phase: Pixel-perfect collision check (Optimized with precomputed overlap)
                    Sprite* pS1 = Main::instance().spriteMgr().get(iIDSprite);
                    Sprite* pS2 = Main::instance().spriteMgr().get(pTarget->iIDSprite);

                    if(pS1 && pS2)
                    {
                        if(!Main::instance().physics().overlapsSprite(pS1, getX(), getY(), pS2, pTarget->getX(), pTarget->getY(), &rOverlap))
                        {
                            continue; // No pixel hit, keep checking neighbors
                        }
                    }

                    pTarget->iLife -= 20;

                    // Small camera shake feedback when player gets hit.
                    if(bIsEnemy && pTarget->getType() == "player")
                    {
                        pScene->setCameraShake(1, fPlayerHitShakeAmplitudePx, iPlayerHitShakeDurationMs, fPlayerHitShakeFalloff);
                    }

                    if(pTarget->iLife <= 0) pTarget->die(pScene, rContext);
                }
                pLayer->closeObject(getID());
                return;
            }
        }
    }
};
Sint32 Laser::iIDLaserBase = -1;
Sint32 Laser::iIDLaserEnemyBase = -1;

// Player: controlled ship.
class Player : public GameEntity
{
    Sint32 iShotCooldown = 0;

public:
    static Sint32 iIDSpriteBase;
    float fMoveX = 0.0f;
    float fMoveY = 0.0f;
    bool  bIsEntering = true;
    float fEntryDistance = 0.0f;

    void initialize() override
    {
        GameEntity::initialize();
        iIDSprite = spawnSpriteChild(iIDSpriteBase);
        Sprite* pS = Main::instance().spriteMgr().get(iIDSprite);
        if(pS)
        {
            pS->selectAnimIndex(0);
            Sint32 iW, iH;
            pS->getOffset(&iW, &iH);
            setSize((float)iW, (float)iH);
        }
        else setSize(32.0f, 32.0f);

        bIsEntering = true;
        fEntryDistance = 0.0f;
        if(g_pState) g_pState->idPlayer = getID();
    }

    void update(Scene* pScene, SceneLayerContext& rContext) override
    {
        GameEntity::update(pScene, rContext);
        if(bIsDead) return;
        if(iShotCooldown > 0) iShotCooldown--;

        float fDx = 0, fDy = 0;

        if(bIsEntering)
        {
            // Scripted entry: move into screen
            float fStep = fEntrySpeed * rContext.fDeltaTime;
            fDy = -fStep;
            fEntryDistance += fStep;
            if(fEntryDistance >= 40.0f) bIsEntering = false;
        }
        else
        {
            // User movement
            float fStep = fPlayerSpeed * rContext.fDeltaTime;
            fDx = fMoveX * fStep;
            fDy = fMoveY * fStep;
        }

        // Visual banking based on lateral velocity
        Sprite* pS = Main::instance().spriteMgr().get(iIDSprite);
        if(pS)
        {
            if(fMoveX < 0) {
                if(pS->getAnimCurrentIndex() != 1) pS->selectAnimIndex(1);
            } // Left
            else if(fMoveX > 0) {
                if(pS->getAnimCurrentIndex() != 2) pS->selectAnimIndex(2);
            } // Right
            else if(pS->getAnimCurrentIndex() != 0) pS->selectAnimIndex(0); // Normal
        }

        if(fDx != 0 || fDy != 0)
        {
            float fNewX = getX() + fDx;
            float fNewY = getY() + fDy;

            // Keep the player inside the moving viewport.
            if(fNewX < rContext.fScrollX) fNewX = rContext.fScrollX;
            if(fNewX + getWidth() > rContext.fScrollX + iViewW) fNewX = rContext.fScrollX + iViewW - getWidth();
            if(fNewY < rContext.fScrollY) fNewY = rContext.fScrollY;
            if(fNewY + getHeight() > rContext.fScrollY + iViewH) fNewY = rContext.fScrollY + iViewH - getHeight();

            setPosition(fNewX, fNewY);
        }
    }

    void shoot()
    {
        if(!g_pState || !g_pState->pScene || bIsEntering) return;
        if(iShotCooldown <= 0)
        {
            Laser* pL = new(std::nothrow) Laser();
            if(pL)
            {
                pL->initialize();
                if(pL->iIDSprite < 0)
                {
                    delete pL;
                    return;
                }
                float fLaserX = getX() + (getWidth() / 2.0f) - (pL->getWidth() / 2.0f);
                pL->setPosition(fLaserX, getY() - pL->getHeight());

                SceneLayerObject* pLayer = g_pState->pScene->accessLayerObject(2);
                if(!pLayer || pLayer->addObject(pL) < 0)
                {
                    delete pL;
                    return;
                }
                iShotCooldown = iShotCooldownTicks;
            }
        }
    }
};
Sint32 Player::iIDSpriteBase = -1;

// Enemy: ship with basic AI movement.
class Enemy : public GameEntity
{
public:
    enum EnemyAI { AI_HOVER, AI_SINE };

private:
    EnemyAI m_eAIState = AI_HOVER;
    Sint32  m_iShotCooldown = 0;
    Sint32  m_iNextFlank = 0;
    float   m_fOscillation = 0.0f;
    float   m_fXOffset = 0.0f;
    float   m_fOscSpeed = 2.0f;
    float   m_fOscAmplitude = 30.0f;
    float   m_fSpeedScale = 1.0f;

public:
    static Sint32 iIDSpriteBase;

    void initialize() override
    {
        GameEntity::initialize();
        iIDSprite = spawnSpriteChild(iIDSpriteBase);
        Sprite* pS = Main::instance().spriteMgr().get(iIDSprite);
        if(pS)
        {
            pS->selectAnimIndex(0);
            Sint32 iW, iH;
            pS->getOffset(&iW, &iH);
            setSize((float)iW, (float)iH);
        }
        else setSize(32.0f, 32.0f);

        Tool& mTool = Main::instance().tool();
        m_iShotCooldown = static_cast<Sint32>(mTool.randMWC() % (iLogicRate * 2)); // up to ~2s
        m_iNextFlank = iLogicRate * 3 + static_cast<Sint32>(mTool.randMWC() % (iLogicRate * 3)); // 3..6s

        m_fXOffset = (float)(40 + (mTool.randMWC() % 80));
        if(mTool.randMWC() % 2 == 0) m_fXOffset *= -1.0f;

        m_fOscSpeed = 1.5f + (float)(mTool.randMWC() % 100) / 50.0f;
        m_fOscAmplitude = 10.0f + (float)(mTool.randMWC() % 40);
        m_fSpeedScale = 0.8f + (float)(mTool.randMWC() % 40) / 100.0f;
        m_fOscillation = (float)(mTool.randMWC() % 360);

        static Sint32 iAICounter = 0;
        m_eAIState = (iAICounter++ % 2 == 0) ? AI_HOVER : AI_SINE;
    }

    void update(Scene* pScene, SceneLayerContext& rContext) override
    {
        GameEntity::update(pScene, rContext);
        if(bIsDead) return;

        const float fDt = rContext.fDeltaTime;
        Tool& mTool = Main::instance().tool();
        if(m_iShotCooldown > 0) m_iShotCooldown--;
        if(m_iNextFlank > 0) m_iNextFlank--;

        float fNextX = getX();
        float fNextY = getY();

        SceneLayerObject* pLayer = pScene->accessLayerObject(rContext.idLayer);
        Player* pTargetPlayer = nullptr;
        if(pLayer) pTargetPlayer = dynamic_cast<Player*>(pLayer->getObjectByID(g_pState->idPlayer));

        if(m_eAIState == AI_HOVER)
        {
            if(m_iNextFlank <= 0)
            {
                m_fXOffset *= -1.0f;
                m_iNextFlank = iLogicRate * 3 + static_cast<Sint32>(mTool.randMWC() % (iLogicRate * 4)); // 3..7s
            }

            if(pTargetPlayer)
            {
                float fTargetX = pTargetPlayer->getX() + (pTargetPlayer->getWidth() - getWidth()) / 2.0f + m_fXOffset;
                float fDir = (fTargetX > getX()) ? 1.0f : -1.0f;
                if(abs(fTargetX - getX()) > 5.0f) fNextX += fDir * fEnemySpeed * m_fSpeedScale * 1.5f * fDt;
            }
            m_fOscillation += m_fOscSpeed * fDt;
            fNextY += ((rContext.fScrollY + 50.0f + (sinf(m_fOscillation) * m_fOscAmplitude)) - getY()) * 2.0f * fDt;
        }
        else // AI_SINE
        {
            fNextX += (fEnemySpeed * 1.2f * m_fSpeedScale) * fDt * (m_fXOffset > 0 ? 1.0f : -1.0f);
            if(fNextX < rContext.fScrollX) 
            {
                fNextX = rContext.fScrollX; m_fXOffset = 1.0f;
            }
            if(fNextX > rContext.fScrollX + rContext.rViewport.w - getWidth()) 
            {
                fNextX = rContext.fScrollX + rContext.rViewport.w - getWidth(); m_fXOffset = -1.0f;
            }

            m_fOscillation += m_fOscSpeed * 2.0f * fDt;
            fNextY = rContext.fScrollY + 80.0f + (sinf(m_fOscillation) * 60.0f);
        }

        if(fNextX < rContext.fScrollX) fNextX = rContext.fScrollX;
        if(fNextX > rContext.fScrollX + rContext.rViewport.w - getWidth()) fNextX = rContext.fScrollX + rContext.rViewport.w - getWidth();

        setPosition(fNextX, fNextY);

        // Enemy shooting.
        if(m_iShotCooldown <= 0)
        {
            Laser* pL = new(std::nothrow) Laser();
            if(pL)
            {
                pL->bIsEnemy = true;
                pL->initialize();
                if(pL->iIDSprite < 0)
                {
                    delete pL;
                }
                else
                {
                    float fStartX = getX() + (getWidth() - pL->getWidth()) / 2.0f;
                    float fStartY = getY() + getHeight();
                    pL->setPosition(fStartX, fStartY);

                    if(pTargetPlayer)
                    {
                        float fTx = pTargetPlayer->getX() + pTargetPlayer->getWidth() / 2.0f;
                        float fTy = pTargetPlayer->getY() + pTargetPlayer->getHeight() / 2.0f;
                        float fDx = fTx - fStartX;
                        float fDy = fTy - fStartY;
                        float fDist = sqrtf(fDx * fDx + fDy * fDy);
                        if(fDist > 0)
                        {
                            float fShotSpeed = 300.0f;
                            pL->fSpeedX = (fDx / fDist) * fShotSpeed;
                            pL->fSpeedY = (fDy / fDist) * fShotSpeed;
                        }
                        else pL->fSpeedY = 300.0f;
                    }
                    else pL->fSpeedY = 300.0f;

                    if(!pLayer || pLayer->addObject(pL) < 0) delete pL;
                }
            }
            m_iShotCooldown = (iLogicRate * 8) / 10 + static_cast<Sint32>(mTool.randMWC() % ((iLogicRate * 7) / 10 + 1)); // ~0.8..1.5s
        }
    }
};
Sint32 Enemy::iIDSpriteBase = -1;

// -----------------------------------------------------------------------------------
// Infrastructure (factory & main loop)
// -----------------------------------------------------------------------------------

// TutorialObjectFactory: links Tiled object types to C++ classes.
class TutorialObjectFactory : public ISceneObjectFactory
{
public:
    SceneObject* create(const string& rType) override
    {
        if(rType == "player") return new(std::nothrow) Player();
        if(rType == "enemy")  return new(std::nothrow) Enemy();
        return nullptr;
    }
};

// renderWrapper: global rendering callback.
Sint32 renderWrapper(Sint32)
{
    if(!g_pState || !g_pState->pScene) return 0;
    Main& mC64 = Main::instance();

    // 1. Background image
    if(g_pState->idBgImage > 0)
    {
        Image* pBG = mC64.imageMgr().get(g_pState->idBgImage);
        if(pBG) pBG->render();
    }

    // 2. Scene (handles all layers and entities)
    g_pState->pScene->render();

    // 3. HUD rendering
    Font* pFont = mC64.fontMgr().get(g_pState->idHUDFont);
    if(pFont)
    {
        char szBuf[64];
        Sint32 iTextW = 0;

        SceneLayerObject* pLayer = g_pState->pScene->accessLayerObject(2);
        Player* pP = nullptr;
        if(pLayer) pP = dynamic_cast<Player*>(pLayer->getObjectByID(g_pState->idPlayer));

        if(pP)
        {
            iTextW = pFont->getWidth("Player");
            pFont->setPosition(static_cast<float>(40 - iTextW / 2), 20);
            pFont->render("Player");

            snprintf(szBuf, 64, "%d", pP->iLife);
            iTextW = pFont->getWidth(szBuf);
            pFont->setPosition(static_cast<float>(40 - iTextW / 2), 40);
            pFont->render(szBuf);
        }

        if(pLayer)
        {
            Sint32 aEnemyIDs[2] = { g_pState->idEnemy1, g_pState->idEnemy2 };
            for(Sint32 i = 0; i < 2; i++)
            {
                if(aEnemyIDs[i] == -1) continue;

                Enemy* pE = dynamic_cast<Enemy*>(pLayer->getObjectByID(aEnemyIDs[i]));
                if(pE)
                {
                    snprintf(szBuf, 64, "Enemy %d", i + 1);
                    iTextW = pFont->getWidth(szBuf);
                    float fBaseX = static_cast<float>(iScreenW - 40 - iTextW / 2);
                    float fBaseY = static_cast<float>(20 + (i * 60));
                    pFont->setPosition(fBaseX, fBaseY);
                    pFont->render(szBuf);

                    snprintf(szBuf, 64, "%d", pE->iLife);
                    iTextW = pFont->getWidth(szBuf);
                    pFont->setPosition(static_cast<float>(iScreenW - 40 - iTextW / 2), fBaseY + 20);
                    pFont->render(szBuf);
                }
            }
        }
    }

    return 0;
}

// Close resources in ownership order, including partially completed setup.
static void closeTutorial()
{
    Main& mC64 = Main::instance();
    mC64.sceneMgr().close(0);
    mC64.spriteMgr().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;

    // Memory management
    CMem::setStatsLevel(CMem::MSL_HIGH);
    atexit(CMem::destroy);

    Main& mC64 = Main::instance();
    TutorialState state;
    g_pState = &state;

    mC64.logMgr().get()->init("Tutorial_16_Scene_Shooter", LL_DEBUG, LM_STDOUT | LM_FILE, OUTPUTDIR"Tutorial_16_Scene_Shooter.log");

    // Core initialization
    if(!mC64.timer().init() || !mC64.timer().setRate(0, iLogicRate))
    {
        mC64.logMgr().get()->msg(LL_ERROR, "Failed to initialize tutorial timing.\n");
        Main::terminate();
        return -1;
    }
    mC64.configMgr().setMTFriendly(1);

    Screen* pScreen = mC64.configMgr().get();
    pScreen->setDriver(eRenderer);
    pScreen->setSize(iScreenW, iScreenH);
    pScreen->setTitle("Tutorial 16 Scene - Shooter");
    if(pScreen->show() < 0)
    {
        Main::terminate();
        return -1;
    }
    if(!pScreen->setRenderCallback(renderWrapper))
    {
        mC64.logMgr().get()->msg(LL_ERROR, "Failed to set the render callback.\n");
        Main::terminate();
        return -1;
    }

    // Load game resources
    g_pState->idBgImage = mC64.imageMgr().load(OUTPUT_CDC, RESOURCE_BG_CDC_NAME);
    g_pState->idHUDFont = mC64.fontMgr().getBuiltin("CourierNew10White");
    if(g_pState->idBgImage <= 0 || g_pState->idHUDFont < 0)
    {
        mC64.logMgr().get()->msg(LL_ERROR, "Failed to load the tutorial interface resources.\n");
        closeTutorial();
        return -1;
    }

    Sint32 idCDC = mC64.archiveMgr().load(BASEDIR "tiled/ship/ship.cdc");
    if(idCDC < 0)
    {
        mC64.logMgr().get()->msg(LL_CRITICAL, "ERROR: resource archive missing: %s\n", BASEDIR "tiled/ship/ship.cdc");
        closeTutorial();
        return -1;
    }

    Player::iIDSpriteBase = mC64.spriteMgr().load(idCDC, "ship");
    Enemy::iIDSpriteBase = mC64.spriteMgr().load(idCDC, "enemy");
    Explosion::iIDSpriteBase = mC64.spriteMgr().load(idCDC, "explosion");
    Laser::iIDLaserBase = mC64.spriteMgr().load(idCDC, "laser");
    Laser::iIDLaserEnemyBase = mC64.spriteMgr().load(idCDC, "laser_enemy");
    mC64.archiveMgr().close(idCDC);
    if(Player::iIDSpriteBase < 0 || Enemy::iIDSpriteBase < 0 || Explosion::iIDSpriteBase < 0 ||
       Laser::iIDLaserBase < 0 || Laser::iIDLaserEnemyBase < 0)
    {
        mC64.logMgr().get()->msg(LL_CRITICAL, "Failed to load one or more shooter sprites.\n");
        closeTutorial();
        return -1;
    }

    // Map loading
    static TutorialObjectFactory mFactory;
    Sint32 idScene = mC64.sceneMgr().loadFromFile(BASEDIR "tiled/ship/shooter.tmx", &mFactory);
    if(idScene < 0)
    {
        mC64.logMgr().get()->msg(LL_CRITICAL, "ERROR: map file missing: %s\n", BASEDIR "tiled/ship/shooter.tmx");
        closeTutorial();
        return -1;
    }

    g_pState->pScene = mC64.sceneMgr().get(idScene);
    if(!g_pState->pScene)
    {
        closeTutorial();
        return -1;
    }

    // Viewport & scrolling configuration
    SDL_Rect rView = { iViewX, iViewY, iViewW, iViewH };
    if(!g_pState->pScene->setViewport(&rView))
    {
        mC64.logMgr().get()->msg(LL_ERROR, "Failed to configure the Scene viewport.\n");
        closeTutorial();
        return -1;
    }

    for(Sint32 i = 1; i <= 2; i++)
    {
        SceneCameraParams mCam;
        if(!g_pState->pScene->setLayerFeatures(i, SLF_REPEATY, true) ||
           !g_pState->pScene->getCameraParams(i, &mCam))
        {
            mC64.logMgr().get()->msg(LL_ERROR, "Failed to configure Scene layer %d.\n", i);
            closeTutorial();
            return -1;
        }
        mCam.ptAutoScrollSpeed = { fAutoScrollX, fAutoScrollY };
        if(!g_pState->pScene->setCameraParams(i, mCam) ||
           !g_pState->pScene->setCameraMode(i, SCM_AUTOSCROLL))
        {
            mC64.logMgr().get()->msg(LL_ERROR, "Failed to configure Scene layer %d.\n", i);
            closeTutorial();
            return -1;
        }
    }

    // Initialization: find entities created from TMX
    Player* pPlayer = nullptr;
    SceneLayerObject* pObjLayer = g_pState->pScene->accessLayerObject(2);
    if(!pObjLayer)
    {
        mC64.logMgr().get()->msg(LL_CRITICAL, "Layer 2 must be an object layer.\n");
        closeTutorial();
        return -1;
    }

    pPlayer = dynamic_cast<Player*>(pObjLayer->findFirstByType("player"));
    vector<SceneObject*> vEnemies;
    pObjLayer->findAllByType("enemy", vEnemies);
    Enemy* pEnemy1 = vEnemies.size() > 0 ? dynamic_cast<Enemy*>(vEnemies[0]) : nullptr;
    Enemy* pEnemy2 = vEnemies.size() > 1 ? dynamic_cast<Enemy*>(vEnemies[1]) : nullptr;
    if(!pPlayer || vEnemies.size() != 2 || !pEnemy1 || !pEnemy2)
    {
        mC64.logMgr().get()->msg(LL_CRITICAL, "Map must contain one player and exactly two enemies created by the object factory.\n");
        closeTutorial();
        return -1;
    }
    g_pState->idPlayer = pPlayer->getID();
    g_pState->idEnemy1 = pEnemy1->getID();
    g_pState->idEnemy2 = pEnemy2->getID();
    g_pState->pScene->setDebugObjectOverlay(SDO_OBJECT_AABB, SDOT_STANDARD, false);
    g_pState->pScene->setDebugOverlayColor(SDO_OBJECT_AABB, 0x00FF00FF);

    mC64.logMgr().get()->msg(LL_INFO, "\nCONTROLS:\n [Cursors] Move\n [Space] Shoot\n [S] Smooth scroll\n [D] Debug boxes\n [Q/Esc] Quit\n\n");

    // Main game loop
    SDL_Event ev;
    bool bDone = false;
    bool bSmooth = false;
    while(!bDone)
    {
        // 1. Event handling (Toggle modes and window events)
        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)
                {
                    bSmooth = !bSmooth;
                    for(Sint32 i = 1; i <= g_pState->pScene->getLayerCount(); i++) g_pState->pScene->setLayerFeatures(i, SLF_SMOOTHSCROLL, bSmooth);
                }
                if(ev.key.key == SDLK_D)
                {
                    g_pState->bDebug = !g_pState->bDebug;
                    g_pState->pScene->setDebugObjectOverlay(SDO_OBJECT_AABB, SDOT_STANDARD, g_pState->bDebug);
                }
            }
        }
        if(bDone) break;

        SceneLayerObject* pLayer = g_pState->pScene->accessLayerObject(2);
        pPlayer = pLayer ? dynamic_cast<Player*>(pLayer->getObjectByID(g_pState->idPlayer)) : nullptr;

        // 2. Real-time input polling (Movement and action)
        if(pPlayer && !pPlayer->bIsDead && !pPlayer->bIsEntering)
        {
            float fDX = 0.0f;
            float fDY = 0.0f;

            if(mC64.getKeyState(SDLK_UP))    fDY -= 1.0f;
            if(mC64.getKeyState(SDLK_DOWN))  fDY += 1.0f;
            if(mC64.getKeyState(SDLK_LEFT))  fDX -= 1.0f;
            if(mC64.getKeyState(SDLK_RIGHT)) fDX += 1.0f;

            if(fDX != 0.0f && fDY != 0.0f)
            {
                fDX *= 0.7071f;
                fDY *= 0.7071f;
            }

            pPlayer->fMoveX = fDX;
            pPlayer->fMoveY = fDY;

            if(mC64.getKeyState(SDLK_SPACE)) pPlayer->shoot();
        }

        // 3. Engine logic update (fixed rate by iLogicRate)
        g_pState->pScene->update();

        // 4. Victory / defeat checks after logic update
        pLayer = g_pState->pScene->accessLayerObject(2);
        pPlayer = pLayer ? dynamic_cast<Player*>(pLayer->getObjectByID(g_pState->idPlayer)) : nullptr;
        if(pLayer)
        {
            bool bEnemyAlive = (pLayer->getObjectByID(g_pState->idEnemy1) != nullptr) || (pLayer->getObjectByID(g_pState->idEnemy2) != nullptr);
            bool bPlayerAlive = (pPlayer != nullptr);

            if(!bEnemyAlive && bPlayerAlive)
            {
                g_pState->pScene->pause();
                mC64.tool().messageBox("Victory!", "All enemies destroyed!", MBB_OK, MBT_INFO);
                g_pState->pScene->resume();
                bDone = true;
            }
            else if(!bPlayerAlive)
            {
                g_pState->pScene->pause();
                mC64.tool().messageBox("Game Over", "Your ship was destroyed.", MBB_OK, MBT_ERROR);
                g_pState->pScene->resume();
                bDone = true;
            }
        }
    }

    // Resource cleanup
    mC64.sceneMgr().info();
    closeTutorial();

    return 0;
}
