#include "Engine.h"
#include "Input.h"
#include "Json.h"
#include "MainMenu.h"
#include "TextureCache.h"
#include "LevelEditor.cpp"
#include <iostream>
#include <random>
#include <deque>
#include <chrono>


// ---------------------------------------------
// SceneDemo: existing complex scene demo
// ---------------------------------------------
class SceneDemo : public Engine {
private:
    float time_ = 0.0f;
    std::vector<std::shared_ptr<GameObject>> rotating_cubes_;
    std::vector<std::shared_ptr<GameObject>> floating_spheres_;
    std::shared_ptr<GameObject> central_sphere_;
    std::shared_ptr<GameObject> ground_plane_;
    std::shared_ptr<GameObject> grid_;
    
    std::mt19937 rng_;
    std::uniform_real_distribution<float> color_dist_{0.3f, 1.0f};
    
public:
    SceneDemo()
      : Engine(1920, 1080, "Modern C++ Engine - Complex Scene Demo"),
        rng_(std::random_device{}()) {}
    
    bool OnInitialize() override {
        std::cout << "Creating complex scene with primitives..." << std::endl;
        
        auto grid_primitive = CreateGrid(25, 1.0f, glm::vec3(0.4f));
        grid_ = CreateGameObject(grid_primitive, glm::vec3(0,0,0));
        AddGameObject(grid_);
        
        auto plane_primitive = CreatePlane(30.0f, 30.0f, glm::vec3(0.2f,0.2f,0.3f));
        ground_plane_ = CreateGameObject(plane_primitive, glm::vec3(0,-0.01f,0));
        AddGameObject(ground_plane_);
        
        auto central_sphere_primitive = CreateSphere(64, glm::vec3(0.9f,0.3f,0.1f));
        central_sphere_ = CreateGameObject(central_sphere_primitive, glm::vec3(0,3,0));
        central_sphere_->SetScale(glm::vec3(2.0f));
        AddGameObject(central_sphere_);
        
        for (int i = 0; i < 8; ++i) {
            glm::vec3 color{color_dist_(rng_), color_dist_(rng_), color_dist_(rng_)};
            auto cube = CreateGameObject(CreateCube(color), glm::vec3(0.0f));
            rotating_cubes_.push_back(cube);
            AddGameObject(cube);
        }
        
        for (int i = 0; i < 15; ++i) {
            glm::vec3 color{color_dist_(rng_), color_dist_(rng_), color_dist_(rng_)};
            float x = (rng_() % 20) - 10.0f;
            float z = (rng_() % 20) - 10.0f;
            float y = 1.0f + (rng_() % 5);
            auto sphere = CreateGameObject(CreateSphere(32, color),
                                           glm::vec3(x,y,z));
            sphere->SetScale(glm::vec3(0.5f + (rng_()%10)*0.1f));
            floating_spheres_.push_back(sphere);
            AddGameObject(sphere);
        }
        
        for (int i = 0; i < 5; ++i) {
            glm::vec3 color{1.0f - i*0.15f, 0.2f + i*0.15f, 0.5f};
            auto cube = CreateGameObject(CreateCube(color),
                                         glm::vec3(-8, i*2.1f+1, 8));
            cube->SetScale(glm::vec3(0.8f + i*0.1f));
            AddGameObject(cube);
        }
        
        for (int i = 0; i < 10; ++i) {
            float hue = i/10.0f;
            glm::vec3 color{hue, 1.0f-hue, 0.5f};
            auto sphere = CreateGameObject(CreateSphere(24, color),
                                           glm::vec3(i*2-9,1,-10));
            sphere->SetScale(glm::vec3(0.3f));
            AddGameObject(sphere);
        }
        
        std::cout << "Scene created with " << game_objects_.size() << " objects!" << std::endl;
        return true;
    }
    
    void OnUpdate(float dt) override {
        time_ += dt;
        if (central_sphere_)
            central_sphere_->SetRotation(glm::vec3(0, time_*30, 0));
        
        for (size_t i = 0; i < rotating_cubes_.size(); ++i) {
            float angle = time_*60.0f + i*(360.0f/rotating_cubes_.size());
            float radius=8.0f, height=2.0f+std::sin(time_*2.0f+i)*1.5f;
            glm::vec3 pos{std::cos(glm::radians(angle))*radius,
                          height,
                          std::sin(glm::radians(angle))*radius};
            rotating_cubes_[i]->SetPosition(pos);
            rotating_cubes_[i]->SetRotation(glm::vec3(angle,angle*1.5f,0));
        }
        
        for (size_t i = 0; i < floating_spheres_.size(); ++i) {
            auto sphere = floating_spheres_[i];
            glm::vec3 p = sphere->GetPosition();
            float bob = std::sin(time_*3.0f + i*0.5f)*0.3f;
            sphere->SetPosition(glm::vec3(p.x, p.y+bob, p.z));
            sphere->SetRotation(glm::vec3(0, time_*20.0f + i*30.0f, 0));
        }
        
        float camAng = time_*15.0f;
        SetCameraOrbit(20.0f, camAng, 12.0f);
        SetCameraTarget(glm::vec3(0,2,0));
        
        static float status_timer=0.0f;
        status_timer += dt;
        if (status_timer >= 3.0f) {
            std::cout<<"Scene running: "<<int(time_)<<"s | "
                     <<game_objects_.size()<<" objects | Cam angle: "
                     <<int(camAng)%360<<"°"<<std::endl;
            status_timer=0.0f;
        }
    }
};



// ---------------------------------------------
// StressTest: incremental spawn until quit
// ---------------------------------------------
class StressTest : public Engine {
public:
    StressTest() : Engine(1920, 1080, "Zoom Stresser") {}


    bool OnInitialize() override {
        last_report_ = std::chrono::high_resolution_clock::now();
        
        // Add a ground plane for visual reference
        auto plane = CreateGameObject(CreatePlane(50.0f, 50.0f, glm::vec3(0.2f, 0.3f, 0.5f)),
                                      glm::vec3(0, -1, 0));
        AddGameObject(plane);
        
        return true;
    }


    void OnUpdate(float dt) override {
        time_ += dt;
        
        // Spawn new objects every second
        if (game_objects_.size() < 10000) {
            spawn_timer_ += dt;
            if (spawn_timer_ >= 1.0f) {
                spawn_timer_ = 0.0f;
                AddRow();
            }
        }
        
        // Animate existing objects
        AnimateObjects();
        
        // Orbit camera around the scene
        float camera_angle = time_ * 20.0f; // 20 degrees per second
        float radius = 30.0f + std::sin(time_ * 0.5f) * 10.0f; // Varying radius
        float height = 15.0f + std::cos(time_ * 0.3f) * 5.0f;  // Varying height
        SetCameraOrbit(radius, camera_angle, height);
        SetCameraTarget(glm::vec3(0, 5, 0));
        
        // Report performance every 2 seconds
        auto now = std::chrono::high_resolution_clock::now();
        float elapsed = std::chrono::duration<float>(now - last_report_).count();
        if (elapsed >= 2.0f) {
            float fps = 1.0f/dt;
            std::cout << "Objects: " << game_objects_.size()
                      << " | FPS: " << fps 
                      << " | Cam angle: " << int(camera_angle) % 360 << "°" << std::endl;
            last_report_ = now;
        }
    }


private:
    void AddRow() {
        int row = int(stress_objects_.size()) / columns_;
        for (int i = 0; i < columns_; ++i) {
            // Colorful gradient based on position
            float hue = (i + row * columns_) / 100.0f;
            glm::vec3 color{
                0.5f + 0.5f * std::sin(hue * 6.28f),
                0.5f + 0.5f * std::sin(hue * 6.28f + 2.09f),
                0.5f + 0.5f * std::sin(hue * 6.28f + 4.19f)
            };
            
            glm::vec3 pos{
                i * spacing_ - columns_ * spacing_ / 2.0f,
                0.5f + row * spacing_,
                -20.0f + row * 2.0f  // Spread them out in depth too
            };
            
            // Alternate between cubes and spheres
            std::shared_ptr<GameObject> obj;
            if ((i + row) % 2 == 0) {
                obj = CreateGameObject(CreateCube(color), pos);
            } else {
                obj = CreateGameObject(CreateSphere(16, color), pos);
            }
            
            obj->SetScale(glm::vec3(0.4f + (i % 5) * 0.1f)); // Varying sizes
            stress_objects_.push_back(obj);
            AddGameObject(obj);
        }
    }
    
    void AnimateObjects() {
        for (size_t i = 0; i < stress_objects_.size(); ++i) {
            auto& obj = stress_objects_[i];
            glm::vec3 pos = obj->GetPosition();
            
            // Floating/bobbing motion
            float bob_speed = 2.0f + (i % 10) * 0.2f;
            float bob_height = 0.3f + (i % 7) * 0.1f;
            float bob = std::sin(time_ * bob_speed + i * 0.1f) * bob_height;
            obj->SetPosition(glm::vec3(pos.x, pos.y + bob, pos.z));
            
            // Rotation animation
            float rot_speed = 30.0f + (i % 13) * 10.0f;
            glm::vec3 rotation{
                time_ * rot_speed + i * 10.0f,
                time_ * (rot_speed * 0.7f) + i * 15.0f,
                time_ * (rot_speed * 0.4f) + i * 20.0f
            };
            obj->SetRotation(rotation);
        }
    }


    float time_{0.0f};
    float spawn_timer_{0.0f};
    std::chrono::high_resolution_clock::time_point last_report_;
    std::vector<std::shared_ptr<GameObject>> stress_objects_;
    static constexpr int columns_{500}; // Reduced for better performance
    static constexpr float spacing_{1.0f};
};


// ---------------------------------------------
// SpinningMonkey: Loads OBJ monkey and spins it
// ---------------------------------------------
class SpinningMonkey : public Engine {
private:
    float time_ = 0.0f;
    std::shared_ptr<GameObject> monkey_;
    std::shared_ptr<GameObject> ground_plane_;
    std::shared_ptr<GameObject> grid_;
    
    // Animation parameters
    float spin_speed_ = 45.0f;  // degrees per second
    float bob_height_ = 2.0f;   // vertical bobbing range
    float bob_speed_ = 1.5f;    // bobbing frequency
    float scale_pulse_ = 0.2f;  // scale pulsing amount
    
public:
    SpinningMonkey()
      : Engine(1920, 1080, "Spinning Monkey Demo - OBJ Model Loader") {}
    
    bool OnInitialize() override {
        std::cout << "Loading Suzanne (Blender Monkey) model..." << std::endl;
        
        // Create ground reference
        auto grid_primitive = CreateGrid(15, 1.0f, glm::vec3(0.3f, 0.3f, 0.4f));
        grid_ = CreateGameObject(grid_primitive, glm::vec3(0, 0, 0));
        AddGameObject(grid_);
        
        auto plane_primitive = CreatePlane(20.0f, 20.0f, glm::vec3(0.15f, 0.25f, 0.35f));
        ground_plane_ = CreateGameObject(plane_primitive, glm::vec3(0, -0.01f, 0));
        AddGameObject(ground_plane_);
        
        // Try to load monkey model (with optional texture)
        try {
            // First try with texture
            auto monkey_primitive = CreateOBJModel("monkey.obj", "monkey.jpg");
            if (monkey_primitive) {
                monkey_ = CreateGameObject(monkey_primitive, glm::vec3(0, 3, 0));
                monkey_->SetScale(glm::vec3(2.0f));
                AddGameObject(monkey_);
                std::cout << "✓ Loaded monkey with texture!" << std::endl;
            }
        } catch (...) {
            std::cout << "! Texture loading failed, trying without texture..." << std::endl;
            try {
                // Fallback: load without texture
                auto monkey_primitive = CreateOBJModel("monkey.obj");
                if (monkey_primitive) {
                    monkey_ = CreateGameObject(monkey_primitive, glm::vec3(0, 3, 0));
                    monkey_->SetScale(glm::vec3(2.0f));
                    AddGameObject(monkey_);
                    std::cout << "✓ Loaded monkey without texture!" << std::endl;
                }
            } catch (...) {
                std::cout << "✗ Failed to load monkey.obj, using fallback cube..." << std::endl;
                // Ultimate fallback: colored cube
                auto fallback_primitive = CreateCube(glm::vec3(1.0f, 0.5f, 0.2f));
                monkey_ = CreateGameObject(fallback_primitive, glm::vec3(0, 3, 0));
                monkey_->SetScale(glm::vec3(2.0f));
                AddGameObject(monkey_);
            }
        }
        
        // Set initial camera position
        SetCameraPosition(glm::vec3(8, 6, 8));
        SetCameraTarget(glm::vec3(0, 3, 0));
        
        std::cout << "Scene initialized with " << game_objects_.size() << " objects!" << std::endl;
        std::cout << "Controls:" << std::endl;
        std::cout << "  - Camera orbits automatically" << std::endl;
        std::cout << "  - Monkey spins and bobs up and down" << std::endl;
        std::cout << "  - Scale pulses with time" << std::endl;
        
        return true;
    }
    
    void OnUpdate(float dt) override {
        time_ += dt;
        
        float spin_y = time_ * spin_speed_;

        if (monkey_) {
            // Spinning animation
           
            float spin_x = std::sin(time_ * 0.3f) * 15.0f;  // Gentle x-axis wobble
            float spin_z = std::cos(time_ * 0.7f) * 10.0f;  // Gentle z-axis wobble
            monkey_->SetRotation(glm::vec3(spin_x, spin_y, spin_z));
            
            // Vertical bobbing
            float base_height = 3.0f;
            float bob_offset = std::sin(time_ * bob_speed_) * bob_height_;
            monkey_->SetPosition(glm::vec3(0, base_height + bob_offset, 0));
            
            // Scale pulsing
            float base_scale = 2.0f;
            float scale_offset = std::sin(time_ * 2.0f) * scale_pulse_;
            float current_scale = base_scale + scale_offset;
            monkey_->SetScale(glm::vec3(current_scale));
        }
        
        // Orbiting camera
        float camera_angle = time_ * 25.0f;  // 25 degrees per second
        float camera_radius = 10.0f + std::sin(time_ * 0.8f) * 3.0f;  // Varying distance
        float camera_height = 6.0f + std::cos(time_ * 0.5f) * 2.0f;   // Varying height
        
        SetCameraOrbit(camera_radius, camera_angle, camera_height);
        SetCameraTarget(glm::vec3(0, 3, 0));  // Always look at monkey
        
        // Status updates every 5 seconds
        static float status_timer = 0.0f;
        status_timer += dt;
        if (status_timer >= 5.0f) {
            std::cout << "🐵 Monkey spinning: " << int(time_) << "s | "
                      << "Rotation: " << int(spin_y) % 360 << "° | "
                      << "Camera: " << int(camera_angle) % 360 << "°" << std::endl;
            status_timer = 0.0f;
        }
    }
};

// ---------------------------------------------
// MonkeyGridDemo: 50 jiggling monkeys on grid
// ---------------------------------------------
class MonkeyGridDemo : public Engine {
private:
    float time_ = 0.0f;
    std::vector<std::shared_ptr<GameObject>> monkeys_;
    std::shared_ptr<GameObject> ground_plane_;
    std::shared_ptr<GameObject> grid_;
    
    static constexpr int MONKEY_COUNT = 50;
    static constexpr int GRID_COLS = 7;
    static constexpr int GRID_ROWS = 8;
    static constexpr float SPACING = 4.0f;
    std::mt19937 rng_{ std::random_device{}() };
    std::uniform_real_distribution<float> jiggle_dist_{ -0.1f, 0.1f };
    
public:
    MonkeyGridDemo()
      : Engine(1920, 1080,  "Monkey Grid Demo") {}
    
    bool OnInitialize() override {
        std::cout << "Spawning " << MONKEY_COUNT << " miniature monkeys on grid..." << std::endl;
        
        // Grid and plane for reference
        auto grid_primitive = CreateGrid(15, 1.0f, glm::vec3(0.3f));
        grid_ = CreateGameObject(grid_primitive, glm::vec3(0, 0, 0));
        AddGameObject(grid_);
        
        auto plane_primitive = CreatePlane(60.0f, 60.0f, glm::vec3(0.2f, 0.25f, 0.3f));
        ground_plane_ = CreateGameObject(plane_primitive, glm::vec3(0, -0.01f, 0));
        AddGameObject(ground_plane_);
        
        // Load OBJ monkey primitive once
        std::shared_ptr<Primitive> monkey_primitive;
        try {
            monkey_primitive = CreateOBJModel("monkey.obj", "monkey.bmp");
            if (!monkey_primitive)
                monkey_primitive = CreateOBJModel("monkey.obj");
        } catch (...) {
            monkey_primitive = CreateCube(glm::vec3(1.0f, 0.5f, 0.2f));
        }
        
        // Spawn monkeys on GRID_COLS×GRID_ROWS grid
        int spawned = 0;
        for (int r = 0; r < GRID_ROWS && spawned < MONKEY_COUNT; ++r) {
            for (int c = 0; c < GRID_COLS && spawned < MONKEY_COUNT; ++c) {
                float x = (c - (GRID_COLS - 1) * 0.5f) * SPACING;
                float z = (r - (GRID_ROWS - 1) * 0.5f) * SPACING;
                auto monkey = CreateGameObject(monkey_primitive, glm::vec3(x, 1.0f, z));
                monkey->SetScale(glm::vec3(0.5f));  // shrink to half size
                monkeys_.push_back(monkey);
                AddGameObject(monkey);
                ++spawned;
            }
        }
        
        // Setup camera
        SetCameraOrbit(50.0f, 45.0f, 30.0f);
        SetCameraTarget(glm::vec3(0, 1.0f, 0));
        
        return true;
    }
    
    void OnUpdate(float dt) override {
        time_ += dt;
        
        // Jiggle and slight bob for each monkey
        for (auto& monkey : monkeys_) {
            glm::vec3 basePos = monkey->GetPosition();
            float jigX = jiggle_dist_(rng_);
            float jigZ = jiggle_dist_(rng_);
            float bobY = std::sin(time_ * 3.0f + basePos.x + basePos.z) * 0.2f;
            
            monkey->SetPosition(glm::vec3(
                basePos.x + jigX,
                1.0f + bobY,
                basePos.z + jigZ
            ));
            
            // Optional subtle rotation
            float angle = std::sin(time_ * 2.0f + basePos.x - basePos.z) * 15.0f;
            monkey->SetRotation(glm::vec3(0, angle, 0));
        }
        
        // Slow orbiting camera
        float camAng = time_ * 10.0f;
        SetCameraOrbit(50.0f, camAng, 30.0f);
        SetCameraTarget(glm::vec3(0, 1.0f, 0));
    }
};



// ---------------------------------------------
// LandscapeDemo: large terrain with trees, rocks
// Full PS4/PS5 controller + keyboard camera
// ---------------------------------------------
class LandscapeDemo : public Engine {
private:
    float time_ = 0.0f;

    // Free-fly camera state
    float cam_yaw_   = -90.0f;
    float cam_pitch_ = 15.0f;
    glm::vec3 cam_pos_{0.0f, 20.0f, 40.0f};
    float cam_speed_ = 20.0f;
    float look_sensitivity_ = 80.0f;

    std::mt19937 rng_{42};

public:
    LandscapeDemo()
      : Engine(1920, 1080, "ZoomEngine - Landscape (Gamepad + Keyboard)") {}

    bool OnInitialize() override {
        std::cout << "Generating landscape..." << std::endl;

        // Large terrain
        auto terrain = CreateTerrain(128, 200.0f, 10.0f);
        auto terrainObj = CreateGameObject(terrain, glm::vec3(0, 0, 0));
        AddGameObject(terrainObj);

        // Scatter trees (cylinder trunk + cone foliage)
        auto trunk_prim  = CreateCylinder(0.3f, 3.0f, 8, glm::vec3(0.4f, 0.25f, 0.1f));
        auto leaves_prim = CreateCone(1.5f, 3.0f, 8, glm::vec3(0.15f, 0.45f, 0.1f));
        auto leaves_prim2 = CreateCone(1.2f, 2.5f, 8, glm::vec3(0.1f, 0.55f, 0.12f));

        std::uniform_real_distribution<float> pos_dist(-80.0f, 80.0f);
        std::uniform_real_distribution<float> scale_dist(0.7f, 1.5f);
        for (int i = 0; i < 120; ++i) {
            float x = pos_dist(rng_);
            float z = pos_dist(rng_);
            // Approximate terrain height at this position
            float h = 0.0f;
            h += std::sin(x * 0.1f) * std::cos(z * 0.1f) * 5.0f;
            h += std::sin(x * 0.25f + 1.3f) * std::cos(z * 0.3f + 0.7f) * 2.5f;
            h += std::sin(x * 0.6f + 2.1f) * std::cos(z * 0.5f + 1.5f) * 1.25f;

            float s = scale_dist(rng_);
            // Trunk
            auto t = CreateGameObject(trunk_prim, glm::vec3(x, h + 1.5f * s, z));
            t->SetScale(glm::vec3(s, s, s));
            AddGameObject(t);
            // Lower foliage
            auto f1 = CreateGameObject(leaves_prim, glm::vec3(x, h + 4.0f * s, z));
            f1->SetScale(glm::vec3(s, s, s));
            AddGameObject(f1);
            // Upper foliage
            auto f2 = CreateGameObject(leaves_prim2, glm::vec3(x, h + 5.5f * s, z));
            f2->SetScale(glm::vec3(s * 0.8f, s * 0.8f, s * 0.8f));
            AddGameObject(f2);
        }

        // Scatter rocks (spheres squished flat)
        auto rock_prim = CreateSphere(8, glm::vec3(0.45f, 0.42f, 0.4f));
        std::uniform_real_distribution<float> rock_scale(0.4f, 1.2f);
        for (int i = 0; i < 60; ++i) {
            float x = pos_dist(rng_);
            float z = pos_dist(rng_);
            float h = 0.0f;
            h += std::sin(x * 0.1f) * std::cos(z * 0.1f) * 5.0f;
            h += std::sin(x * 0.25f + 1.3f) * std::cos(z * 0.3f + 0.7f) * 2.5f;
            float rs = rock_scale(rng_);
            auto r = CreateGameObject(rock_prim, glm::vec3(x, h + rs * 0.3f, z));
            r->SetScale(glm::vec3(rs, rs * 0.5f, rs));
            AddGameObject(r);
        }

        std::cout << "Landscape ready: " << game_objects_.size() << " objects" << std::endl;
        std::cout << "Controls:" << std::endl;
        std::cout << "  Gamepad: Left stick = move, Right stick = look" << std::endl;
        std::cout << "           R2 = up, L2 = down, Cross = sprint" << std::endl;
        std::cout << "  Keyboard: WASD = move, Arrow keys = look, Space/Shift = up/down" << std::endl;
        return true;
    }

    void OnUpdate(float dt) override {
        time_ += dt;
        float speed = cam_speed_;

        // --- Gamepad camera ---
        if (Input::IsGamepadConnected()) {
            // Sprint with Cross
            if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross))
                speed *= 3.0f;

            // Left stick: move forward/back + strafe
            glm::vec2 ls = Input::GetLeftStick();
            // Right stick: look
            glm::vec2 rs = Input::GetRightStick();

            cam_yaw_   += rs.x * look_sensitivity_ * dt;
            cam_pitch_ -= rs.y * look_sensitivity_ * dt;
            if (cam_pitch_ >  89.0f) cam_pitch_ =  89.0f;
            if (cam_pitch_ < -89.0f) cam_pitch_ = -89.0f;

            glm::vec3 forward;
            forward.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
            forward.y = std::sin(glm::radians(cam_pitch_));
            forward.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
            forward = glm::normalize(forward);
            glm::vec3 right = glm::normalize(glm::cross(forward, glm::vec3(0, 1, 0)));

            cam_pos_ += forward * (-ls.y) * speed * dt;
            cam_pos_ += right   * ls.x    * speed * dt;

            // Triggers for vertical movement
            cam_pos_.y += Input::GetR2() * speed * dt;
            cam_pos_.y -= Input::GetL2() * speed * dt;
        }

        // --- Keyboard camera ---
        {
            float kspeed = speed;
            if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT)) kspeed *= 3.0f;

            if (Input::IsKeyHeld(GLFW_KEY_LEFT))  cam_yaw_   -= look_sensitivity_ * dt;
            if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) cam_yaw_   += look_sensitivity_ * dt;
            if (Input::IsKeyHeld(GLFW_KEY_UP))    cam_pitch_  += look_sensitivity_ * dt;
            if (Input::IsKeyHeld(GLFW_KEY_DOWN))  cam_pitch_  -= look_sensitivity_ * dt;
            if (cam_pitch_ >  89.0f) cam_pitch_ =  89.0f;
            if (cam_pitch_ < -89.0f) cam_pitch_ = -89.0f;

            glm::vec3 forward;
            forward.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
            forward.y = std::sin(glm::radians(cam_pitch_));
            forward.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
            forward = glm::normalize(forward);
            glm::vec3 right = glm::normalize(glm::cross(forward, glm::vec3(0, 1, 0)));

            if (Input::IsKeyHeld(GLFW_KEY_W)) cam_pos_ += forward * kspeed * dt;
            if (Input::IsKeyHeld(GLFW_KEY_S)) cam_pos_ -= forward * kspeed * dt;
            if (Input::IsKeyHeld(GLFW_KEY_A)) cam_pos_ -= right   * kspeed * dt;
            if (Input::IsKeyHeld(GLFW_KEY_D)) cam_pos_ += right   * kspeed * dt;
            if (Input::IsKeyHeld(GLFW_KEY_SPACE))       cam_pos_.y += kspeed * dt;
            if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL)) cam_pos_.y -= kspeed * dt;
        }

        // Apply camera
        glm::vec3 forward;
        forward.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
        forward.y = std::sin(glm::radians(cam_pitch_));
        forward.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
        SetCameraPosition(cam_pos_);
        SetCameraTarget(cam_pos_ + glm::normalize(forward));
    }
};

// ---------------------------------------------
// WaterfallDemo: natural waterfall over a rocky cliff. Showcases:
//   - Two water surfaces at different elevations (upper stream, lower pool)
//   - Directional flow current carrying debris downstream
//   - Dense physics droplet cascade off the cliff edge
//   - Translucent "curtain" geometry for the body of the waterfall
//   - PBR rock + wet-rock specular near the falls
// ---------------------------------------------
class WaterfallDemo : public Engine {
    float cam_yaw_   = 180.0f;
    float cam_pitch_ = -5.0f;
    float cam_speed_ = 18.0f;
    float look_sensitivity_ = 80.0f;
    glm::vec3 last_cam_pos_{0};
    float drop_timer_ = 0.0f;
    int   drop_counter_ = 0;
    std::deque<std::string> active_drops_;
    static constexpr int kMaxDrops = 200;
    std::mt19937 drop_rng_{77};
    std::mt19937 scene_rng_{19};
    int log_counter_ = 0;
    float log_timer_ = 0.0f;
    std::deque<std::string> active_logs_;
    static constexpr int kMaxLogs = 8;

public:
    WaterfallDemo()
      : Engine(1920, 1080, "ZoomEngine - Waterfall (Flow Physics)") {}

    bool OnInitialize() override {
        auto& rng = scene_rng_;
        auto rnd = [&](float lo, float hi) {
            std::uniform_real_distribution<float> d(lo, hi); return d(rng);
        };
        auto jit = [&](float s) { return rnd(-s, s); };

        // --- Daylight palette, slightly overcast cool tones (misty) ---
        processAgentCommand(json::parse(R"({
            "action":"sun",
            "direction":[-0.30, 0.75, -0.25],
            "color":[1.85, 1.65, 1.40],
            "zenith":[0.28, 0.45, 0.65],
            "horizon":[0.78, 0.84, 0.88],
            "sky":[0.62, 0.72, 0.78],
            "ground":[0.10, 0.12, 0.10],
            "shadow_extent":100,
            "exposure":0.95,
            "bloom_threshold":1.25,
            "bloom_intensity":1.0,
            "fog_density":0.010
        })"));

        // --- Lower pool + downstream river: primary water, flowing west ---
        processAgentCommand(json::parse(R"({
            "action":"water","enabled":true,
            "level":-0.2,"size":300,
            "amplitude":0.20,"wavelength":9,"speed":1.0,
            "shallow":[0.40, 0.58, 0.58],"deep":[0.03, 0.09, 0.14],
            "density":1.0,"resolution":160,
            "flow_dir":[-1, 0, 0],"flow_speed":4.0
        })"));

        // --- Upper stream: secondary water at top of cliff, smaller patch ---
        processAgentCommand(json::parse(R"({
            "action":"water2","enabled":true,
            "center":[38, 0, 0], "size":55, "level":7.8,
            "amplitude":0.08,"wavelength":5,"speed":0.9,
            "shallow":[0.42, 0.58, 0.60],
            "deep":[0.05, 0.15, 0.22],
            "resolution":80
        })"));

        processAgentCommand(json::parse(R"({
            "action":"weather","wind_dir":[-1,0,0],"wind_speed":0.4,
            "storminess":0.10,"current_coef":0.3
        })"));

        GLuint rock_tex  = TextureCache::Get("rock");
        GLuint noise_tex = TextureCache::Get("noise");
        GLuint wood_tex  = TextureCache::Get("wood");
        GLuint grass_tex = TextureCache::Get("grass");

        // --- Two ground strips leaving river channel open ---
        auto north_bank = CreateGameObject(
            CreatePlane(220.0f, 100.0f, glm::vec3(0.85f, 0.90f, 0.82f)),
            glm::vec3(-40.0f, 0.0f, 60.0f));
        north_bank->SetTexture(grass_tex); north_bank->SetNormalMap(noise_tex);
        north_bank->SetUVScale(glm::vec2(0.25f)); north_bank->SetRoughness(0.95f);
        AddGameObject(north_bank);

        auto south_bank = CreateGameObject(
            CreatePlane(220.0f, 100.0f, glm::vec3(0.85f, 0.90f, 0.82f)),
            glm::vec3(-40.0f, 0.0f, -60.0f));
        south_bank->SetTexture(grass_tex); south_bank->SetNormalMap(noise_tex);
        south_bank->SetUVScale(glm::vec2(0.25f)); south_bank->SetRoughness(0.95f);
        AddGameObject(south_bank);

        // Upper (clifftop) plateau
        auto plateau = CreateGameObject(
            CreatePlane(120.0f, 140.0f, glm::vec3(0.75f, 0.82f, 0.68f)),
            glm::vec3(90.0f, 7.5f, 0.0f));
        plateau->SetTexture(grass_tex); plateau->SetNormalMap(noise_tex);
        plateau->SetUVScale(glm::vec2(0.22f)); plateau->SetRoughness(0.95f);
        AddGameObject(plateau);

        // Stream bed at clifftop (slight dirt depression behind the falls)
        auto stream_bed = CreateGameObject(
            CreatePlane(60.0f, 40.0f, glm::vec3(0.45f, 0.42f, 0.35f)),
            glm::vec3(38.0f, 7.2f, 0.0f));
        stream_bed->SetTexture(TextureCache::Get("dirt"));
        stream_bed->SetNormalMap(noise_tex);
        stream_bed->SetUVScale(glm::vec2(0.3f));
        AddGameObject(stream_bed);

        // --- Cliff face: stacked rock cubes forming a vertical wall ---
        auto cliffBlock = [&](glm::vec3 pos, glm::vec3 scale, float rough = 0.85f) {
            auto b = CreateGameObject(
                CreateCube(glm::vec3(0.78f, 0.74f, 0.68f)),
                pos);
            b->SetScale(scale);
            b->SetRotation(glm::vec3(jit(4), jit(10), jit(4)));
            b->SetTexture(rock_tex);
            b->SetNormalMap(noise_tex);
            b->SetRoughness(rough);
            AddGameObject(b);
        };
        // Main cliff wall at x~10, running along Z
        for (int z = -8; z <= 8; ++z) {
            float h = 7.5f + jit(0.4f);
            cliffBlock(glm::vec3(10.0f + jit(0.6f), h * 0.5f, (float)z * 2.8f),
                       glm::vec3(3.0f + jit(0.5f), h, 2.8f + jit(0.3f)));
        }
        // Stepped rocks just in front (splash-worn)
        for (int i = 0; i < 10; ++i) {
            float x = 7.5f + jit(1.2f);
            float z = rnd(-10.0f, 10.0f);
            float s = rnd(1.2f, 2.2f);
            cliffBlock(glm::vec3(x, s * 0.4f, z), glm::vec3(s, s * 0.8f, s * 0.9f), 0.55f);
        }
        // Boulders in the splash pool
        for (int i = 0; i < 14; ++i) {
            float x = rnd(4.0f, 9.0f);
            float z = rnd(-10.0f, 10.0f);
            float s = rnd(0.6f, 1.3f);
            auto r = CreateGameObject(
                CreateSphere(14, glm::vec3(0.88f, 0.85f, 0.80f)),
                glm::vec3(x, s * 0.4f, z));
            r->SetScale(glm::vec3(s, s * 0.65f, s * 0.95f));
            r->SetTexture(rock_tex);
            r->SetNormalMap(noise_tex);
            r->SetRoughness(0.6f);  // wet rocks shine more
            AddGameObject(r);
        }

        // --- Waterfall curtain: a semi-transparent vertical sheet as the body
        // of flowing water. Emissive aqua-blue so it picks up bloom. The
        // droplets stream in front of it give the detail.
        {
            auto sheet = CreateGameObject(
                CreateCube(glm::vec3(0.75f, 0.90f, 0.98f)),
                glm::vec3(9.5f, 4.0f, 0.0f));
            sheet->SetScale(glm::vec3(0.25f, 8.4f, 20.0f));
            sheet->SetMetallic(0.0f);
            sheet->SetRoughness(0.12f);
            sheet->SetEmissive(glm::vec3(0.55f, 0.85f, 1.00f));  // bloom glow
            AddGameObject(sheet);
            // Inner brighter core (taller, thinner — gives depth)
            auto core = CreateGameObject(
                CreateCube(glm::vec3(0.95f, 0.98f, 1.0f)),
                glm::vec3(9.4f, 4.0f, 0.0f));
            core->SetScale(glm::vec3(0.12f, 8.2f, 11.0f));
            core->SetEmissive(glm::vec3(1.3f, 1.6f, 2.0f));
            AddGameObject(core);
        }

        // Mist volume at the base (bright emissive spheres that bloom)
        for (int i = 0; i < 30; ++i) {
            float x = rnd(6.0f, 10.0f);
            float y = rnd(0.2f, 2.0f);
            float z = rnd(-11.0f, 11.0f);
            auto m = CreateGameObject(
                CreateSphere(6, glm::vec3(1.0f)),
                glm::vec3(x, y, z));
            m->SetScale(glm::vec3(rnd(0.35f, 0.75f)));
            m->SetEmissive(glm::vec3(1.5f, 1.8f, 2.0f));
            AddGameObject(m);
        }

        // --- Trees, rocks on banks ---
        auto tree = [&](glm::vec3 at, float s) {
            auto trunk = CreateGameObject(
                CreateCylinder(0.23f * s, 2.6f * s, 10,
                               glm::vec3(0.9f, 0.84f, 0.78f)),
                at + glm::vec3(0, 1.3f * s, 0));
            trunk->SetRotation(glm::vec3(jit(4), jit(360), jit(4)));
            trunk->SetTexture(wood_tex);
            trunk->SetNormalMap(noise_tex);
            trunk->SetRoughness(0.85f);
            AddGameObject(trunk);
            glm::vec3 cc(rnd(0.12f, 0.22f), rnd(0.38f, 0.52f), rnd(0.15f, 0.22f));
            auto cone = CreateGameObject(
                CreateCone(1.2f * s, 2.7f * s, 10, cc),
                at + glm::vec3(jit(0.15f), 3.0f * s, jit(0.15f)));
            cone->SetNormalMap(noise_tex);
            cone->SetRoughness(0.8f);
            AddGameObject(cone);
        };
        // Downstream banks
        for (int i = 0; i < 35; ++i) {
            float x = rnd(-110.0f, 0.0f);
            float z = (i % 2 == 0 ? 1 : -1) * rnd(20.0f, 55.0f);
            tree(glm::vec3(x, 0, z), rnd(0.8f, 1.4f));
        }
        // Upper plateau
        for (int i = 0; i < 22; ++i) {
            float x = rnd(40.0f, 140.0f);
            float z = rnd(-60.0f, 60.0f);
            tree(glm::vec3(x, 7.5f, z), rnd(0.8f, 1.3f));
        }
        // Scattered boulders on banks
        for (int i = 0; i < 22; ++i) {
            float x = rnd(-90.0f, -2.0f);
            float z = (i % 2 == 0 ? 1 : -1) * rnd(12.0f, 38.0f);
            float s = rnd(0.5f, 1.6f);
            auto r = CreateGameObject(
                CreateSphere(12, glm::vec3(0.85f, 0.82f, 0.78f)),
                glm::vec3(x, s * 0.35f, z));
            r->SetScale(glm::vec3(s, s * 0.6f, s * 0.95f));
            r->SetRotation(glm::vec3(jit(12), jit(180), jit(12)));
            r->SetTexture(rock_tex);
            r->SetNormalMap(noise_tex);
            r->SetRoughness(0.88f);
            AddGameObject(r);
        }

        // Distant mountains
        for (int i = 0; i < 10; ++i) {
            float x = -130.0f + i * 26.0f + jit(6.0f);
            float z = -150.0f + jit(12.0f);
            float s = rnd(0.9f, 1.4f);
            float h = 24.0f * s;
            auto m = CreateGameObject(
                CreateCone(12.0f * s, h, 10, glm::vec3(0.26f, 0.28f, 0.32f)),
                glm::vec3(x, h * 0.5f, z));
            AddGameObject(m);
        }

        processAgentCommand(json::parse(R"({"action":"physics","enabled":true})"));

        // Camera: parked downstream looking back at the falls
        glm::vec3 eye(-25.0f, 4.0f, 0.0f);
        glm::vec3 target(10.0f, 4.0f, 0.0f);
        SetCameraPosition(eye);
        SetCameraTarget(target);
        glm::vec3 fwd = glm::normalize(target - eye);
        cam_pitch_ = glm::degrees(std::asin(fwd.y));
        cam_yaw_   = glm::degrees(std::atan2(fwd.z, fwd.x));
        last_cam_pos_ = eye;

        std::cout << "Waterfall demo ready: " << game_objects_.size() << " objects.\n"
                  << "Flow current carries floating debris downstream.\n"
                  << "Camera: WASD/arrows + gamepad, Space/LCtrl up/down, LShift sprint.\n";
        return true;
    }

    void EmitDroplet() {
        if ((int)active_drops_.size() >= kMaxDrops) {
            std::string old = active_drops_.front();
            active_drops_.pop_front();
            json::Object dc;
            dc["action"] = json::Value(std::string("delete"));
            dc["id"]     = json::Value(old);
            processAgentCommand(json::Value(dc));
        }
        std::uniform_real_distribution<float> zz(-9.0f, 9.0f);
        std::uniform_real_distribution<float> yy(-0.2f, 0.1f);
        std::uniform_real_distribution<float> xx(-0.3f, 0.3f);
        std::string id = "drop_" + std::to_string(drop_counter_++);
        glm::vec3 spawn(10.0f + xx(drop_rng_),
                        7.7f + yy(drop_rng_),
                        zz(drop_rng_));
        // Occasional slightly brighter droplet for visual density
        bool bright = (drop_counter_ % 7 == 0);
        glm::vec3 col = bright ? glm::vec3(0.9f, 1.0f, 1.2f)
                               : glm::vec3(0.65f, 0.85f, 0.98f);
        auto d = CreateGameObject(CreateSphere(8, col), spawn);
        d->SetScale(glm::vec3(bright ? 0.22f : 0.18f));
        d->SetMetallic(0.0f);
        d->SetRoughness(0.12f);
        d->SetEmissive(glm::vec3(0.6f, 0.9f, 1.1f) * (bright ? 1.3f : 1.0f));
        AddGameObject(d);
        namedObjects_[id] = d;

        json::Object bc;
        bc["action"] = json::Value(std::string("body"));
        bc["id"] = json::Value(id);
        bc["shape"] = json::Value(std::string("sphere"));
        bc["mass"] = json::Value(0.08);
        bc["restitution"] = json::Value(0.2);
        bc["friction"] = json::Value(0.05);
        processAgentCommand(json::Value(bc));

        // Initial velocity: mostly downward + westward
        std::uniform_real_distribution<float> vx(-3.0f, -1.5f);
        std::uniform_real_distribution<float> vy(-2.0f, -1.0f);
        std::uniform_real_distribution<float> vz(-0.5f, 0.5f);
        json::Object vc;
        vc["action"] = json::Value(std::string("velocity"));
        vc["id"] = json::Value(id);
        vc["linear"] = json::Value(json::Array{
            json::Value((double)vx(drop_rng_)),
            json::Value((double)vy(drop_rng_)),
            json::Value((double)vz(drop_rng_))});
        processAgentCommand(json::Value(vc));
        active_drops_.push_back(id);
    }

    void EmitLog() {
        // Periodically drop a floating log into the upper stream so it flows
        // over the falls and tumbles downstream in the pool — shows flow physics.
        if ((int)active_logs_.size() >= kMaxLogs) {
            std::string old = active_logs_.front();
            active_logs_.pop_front();
            json::Object dc;
            dc["action"] = json::Value(std::string("delete"));
            dc["id"]     = json::Value(old);
            processAgentCommand(json::Value(dc));
        }
        std::uniform_real_distribution<float> zz(-6.0f, 6.0f);
        std::uniform_real_distribution<float> yaw(0.0f, 360.0f);
        std::string id = "log_" + std::to_string(log_counter_++);
        auto log = CreateGameObject(
            CreateCylinder(0.35f, 2.4f, 10, glm::vec3(0.9f, 0.80f, 0.65f)),
            glm::vec3(60.0f, 8.3f, zz(drop_rng_)));
        log->SetRotation(glm::vec3(0, yaw(drop_rng_), 90.0f));
        log->SetTexture(TextureCache::Get("wood"));
        log->SetNormalMap(TextureCache::Get("noise"));
        log->SetRoughness(0.85f);
        AddGameObject(log);
        namedObjects_[id] = log;

        json::Object bc;
        bc["action"] = json::Value(std::string("body"));
        bc["id"] = json::Value(id);
        bc["shape"] = json::Value(std::string("box"));
        bc["mass"] = json::Value(1.2);
        bc["restitution"] = json::Value(0.2);
        bc["friction"] = json::Value(0.35);
        processAgentCommand(json::Value(bc));

        json::Object vc;
        vc["action"] = json::Value(std::string("velocity"));
        vc["id"] = json::Value(id);
        vc["linear"] = json::Value(json::Array{
            json::Value(-2.0),
            json::Value(0.0),
            json::Value(0.0)});
        processAgentCommand(json::Value(vc));
        active_logs_.push_back(id);
    }

    void OnUpdate(float dt) override {
        // Dense droplet emitter: ~30/sec
        drop_timer_ -= dt;
        while (drop_timer_ <= 0.0f) {
            EmitDroplet();
            drop_timer_ += 0.033f;
        }
        // Floating log every ~3 seconds to demo flow current
        log_timer_ -= dt;
        if (log_timer_ <= 0.0f) {
            EmitLog();
            log_timer_ = 3.0f;
        }

        // Free-fly camera (same pattern as other scenes) ---------------------
        glm::vec3 eng_pos = GetCameraPosition();
        if (glm::length(eng_pos - last_cam_pos_) > 0.0001f) {
            glm::vec3 fwd = GetCameraTarget() - eng_pos;
            if (glm::length(fwd) > 0.0001f) {
                fwd = glm::normalize(fwd);
                cam_pitch_ = glm::degrees(std::asin(fwd.y));
                cam_yaw_   = glm::degrees(std::atan2(fwd.z, fwd.x));
            }
        }
        glm::vec2 ls{0}, rs{0};
        float up = 0, down = 0, speed = cam_speed_;
        bool touched = false;
        if (Input::IsGamepadConnected()) {
            ls = Input::GetLeftStick();
            rs = Input::GetRightStick();
            up = Input::GetR2();
            down = Input::GetL2();
            if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross)) speed *= 3.0f;
            if (glm::length(ls) + glm::length(rs) + up + down > 0.01f) touched = true;
        }
        if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT))   speed *= 3.0f;
        if (Input::IsKeyHeld(GLFW_KEY_W))     { ls.y -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_S))     { ls.y += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_A))     { ls.x -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_D))     { ls.x += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_LEFT))  { rs.x -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) { rs.x += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_UP))    { rs.y -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_DOWN))  { rs.y += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_SPACE))        { up   += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL)) { down += 1.0f; touched = true; }
        if (!touched) { last_cam_pos_ = eng_pos; return; }
        cam_yaw_   += rs.x * look_sensitivity_ * dt;
        cam_pitch_ -= rs.y * look_sensitivity_ * dt;
        if (cam_pitch_ >  89.0f) cam_pitch_ =  89.0f;
        if (cam_pitch_ < -89.0f) cam_pitch_ = -89.0f;
        glm::vec3 fwd;
        fwd.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
        fwd.y = std::sin(glm::radians(cam_pitch_));
        fwd.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
        fwd = glm::normalize(fwd);
        glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
        glm::vec3 pos = eng_pos;
        pos += fwd * (-ls.y) * speed * dt;
        pos += right *  ls.x   * speed * dt;
        pos.y += (up - down) * speed * dt;
        SetCameraPosition(pos);
        SetCameraTarget(pos + fwd);
        last_cam_pos_ = pos;
    }
};


// ---------------------------------------------
// DamDemo: concrete dam with reservoir water above, river water below, and
// a stream of physics droplets pouring through the spillway as a waterfall.
// Shows off: two separate Gerstner water surfaces at different heights, mass
// physics with buoyancy, normal-mapped concrete, PBR lighting, bloom mist.
// ---------------------------------------------
class DamDemo : public Engine {
    float cam_yaw_   = 135.0f;
    float cam_pitch_ = 0.0f;
    float cam_speed_ = 22.0f;
    float look_sensitivity_ = 80.0f;
    glm::vec3 last_cam_pos_{0};
    float drop_timer_ = 0.0f;
    int   drop_counter_ = 0;
    std::deque<std::string> active_drops_;
    static constexpr int kMaxDrops = 120;
    std::mt19937 drop_rng_{41};
    std::mt19937 scene_rng_{9};

public:
    DamDemo()
      : Engine(1920, 1080, "ZoomEngine - Dam (Multi-water)") {}

    bool OnInitialize() override {
        auto& rng = scene_rng_;
        auto rnd = [&](float lo, float hi) {
            std::uniform_real_distribution<float> d(lo, hi); return d(rng);
        };
        auto jit = [&](float s) { return rnd(-s, s); };

        // --- Sun + atmosphere: late-afternoon, soft ---
        processAgentCommand(json::parse(R"({
            "action":"sun",
            "direction":[-0.35, 0.55, -0.25],
            "color":[1.7, 1.25, 0.92],
            "zenith":[0.18, 0.32, 0.58],
            "horizon":[0.85, 0.78, 0.68],
            "sky":[0.55, 0.62, 0.70],
            "ground":[0.08, 0.08, 0.09],
            "shadow_extent":100,
            "exposure":0.95,
            "bloom_threshold":1.2,
            "bloom_intensity":1.0,
            "fog_density":0.008
        })"));

        // --- Primary water: the river below the dam (extends far downstream) ---
        processAgentCommand(json::parse(R"({
            "action":"water","enabled":true,
            "level":-0.3,"size":300,
            "amplitude":0.18,"wavelength":10,"speed":0.9,
            "shallow":[0.42, 0.55, 0.55],"deep":[0.05, 0.10, 0.18],
            "density":1.0,"resolution":160
        })"));

        // --- Secondary water: the reservoir above the dam ---
        // Visual-only, at y=8 (higher than dam base), localized to a square.
        processAgentCommand(json::parse(R"({
            "action":"water2","enabled":true,
            "center":[55, 0, 0], "size":90, "level":8.5,
            "amplitude":0.10,"wavelength":7,"speed":0.5,
            "shallow":[0.38, 0.55, 0.60],
            "deep":[0.02, 0.08, 0.16],
            "resolution":96
        })"));

        processAgentCommand(json::parse(R"({
            "action":"weather","wind_dir":[1,0,0.2],"wind_speed":0.8,
            "storminess":0.05,"current_coef":0.5
        })"));

        // --- Banks for the downstream river ---
        // Split ground into two strips leaving a 40-unit-wide river channel
        // between z=-20 and z=20. Primary water (y=-0.3) shows through the gap.
        GLuint grass_tex = TextureCache::Get("grass");
        GLuint dirt_tex  = TextureCache::Get("dirt");

        auto north_bank = CreateGameObject(
            CreatePlane(220.0f, 100.0f, glm::vec3(0.88f, 0.90f, 0.84f)),
            glm::vec3(-100.0f, 0.0f, 70.0f));
        north_bank->SetTexture(grass_tex);
        north_bank->SetNormalMap(TextureCache::Get("noise"));
        north_bank->SetUVScale(glm::vec2(0.25f));
        north_bank->SetRoughness(0.95f);
        AddGameObject(north_bank);

        auto south_bank = CreateGameObject(
            CreatePlane(220.0f, 100.0f, glm::vec3(0.88f, 0.90f, 0.84f)),
            glm::vec3(-100.0f, 0.0f, -70.0f));
        south_bank->SetTexture(grass_tex);
        south_bank->SetNormalMap(TextureCache::Get("noise"));
        south_bank->SetUVScale(glm::vec2(0.25f));
        south_bank->SetRoughness(0.95f);
        AddGameObject(south_bank);

        // Dam abutment ground on the high side, wrapping around the reservoir
        auto upstream_ground = CreateGameObject(
            CreatePlane(200.0f, 200.0f, glm::vec3(0.75f, 0.80f, 0.70f)),
            glm::vec3(100.0f, 7.5f, 0.0f));
        upstream_ground->SetTexture(grass_tex);
        upstream_ground->SetNormalMap(TextureCache::Get("noise"));
        upstream_ground->SetUVScale(glm::vec2(0.2f));
        upstream_ground->SetRoughness(0.95f);
        AddGameObject(upstream_ground);

        // Reservoir floor: slight dip just below the water for shallow read
        auto bed_high = CreateGameObject(
            CreatePlane(96.0f, 96.0f, glm::vec3(0.55f, 0.52f, 0.42f)),
            glm::vec3(55.0f, 7.8f, 0.0f));
        bed_high->SetTexture(dirt_tex);
        bed_high->SetNormalMap(TextureCache::Get("noise"));
        bed_high->SetUVScale(glm::vec2(0.25f));
        bed_high->SetRoughness(0.95f);
        AddGameObject(bed_high);

        // --- The Dam itself ---
        // Big concrete slab with a spillway notch in the middle.
        // Position at x=10, running along z-axis.
        GLuint concrete = TextureCache::Get("concrete");
        GLuint noise_t  = TextureCache::Get("noise");

        auto damBlock = [&](glm::vec3 pos, glm::vec3 scale, glm::vec3 tint = glm::vec3(0.85f, 0.85f, 0.83f)) {
            auto b = CreateGameObject(CreateCube(tint), pos);
            b->SetScale(scale);
            b->SetTexture(concrete);
            b->SetNormalMap(noise_t);
            b->SetMetallic(0.0f);
            b->SetRoughness(0.85f);
            AddGameObject(b);
        };

        // Left wall (z: -30 to -6), full height y=0..13
        damBlock(glm::vec3(10.0f, 6.5f, -18.0f), glm::vec3(3.5f, 13.0f, 24.0f));
        // Right wall (z: 6 to 30)
        damBlock(glm::vec3(10.0f, 6.5f,  18.0f), glm::vec3(3.5f, 13.0f, 24.0f));
        // Spillway weir — lower section between walls, y=0..6 so reservoir can
        // overflow across the top of it.
        damBlock(glm::vec3(10.0f, 3.0f, 0.0f), glm::vec3(3.5f, 6.0f, 12.0f));
        // Angled buttresses at the ends
        damBlock(glm::vec3(13.5f, 3.5f, -30.0f), glm::vec3(4.0f, 7.0f, 4.0f));
        damBlock(glm::vec3(13.5f, 3.5f,  30.0f), glm::vec3(4.0f, 7.0f, 4.0f));

        // Dam top walkway
        damBlock(glm::vec3(10.0f, 13.3f, 0.0f),
                 glm::vec3(4.5f, 0.4f, 64.0f),
                 glm::vec3(0.76f, 0.76f, 0.74f));

        // Railing posts along the top
        for (int i = 0; i < 15; ++i) {
            float z = -28.0f + i * 4.0f;
            auto post = CreateGameObject(
                CreateCylinder(0.12f, 1.2f, 8, glm::vec3(0.78f, 0.78f, 0.78f)),
                glm::vec3(10.0f + jit(1.5f), 13.0f, z));
            post->SetMetallic(1.0f);
            post->SetRoughness(0.4f);
            AddGameObject(post);
        }

        // --- Surrounding banks: stone clusters and trees ---
        GLuint rock_tex = TextureCache::Get("rock");
        GLuint wood_tex = TextureCache::Get("wood");

        auto boulder = [&](glm::vec3 pos, float s) {
            auto r = CreateGameObject(
                CreateSphere(16, glm::vec3(0.9f, 0.88f, 0.84f)),
                pos);
            r->SetScale(glm::vec3(s, s * rnd(0.55f, 0.8f), s * rnd(0.85f, 1.1f)));
            r->SetRotation(glm::vec3(jit(15), jit(180), jit(15)));
            r->SetTexture(rock_tex);
            r->SetNormalMap(noise_t);
            r->SetRoughness(0.9f);
            AddGameObject(r);
        };
        // Rocks lining the downstream riverbank
        for (int i = 0; i < 25; ++i) {
            float x = rnd(-100.0f, 5.0f);
            float z = (i % 2 == 0 ? -1 : 1) * rnd(18.0f, 36.0f);
            boulder(glm::vec3(x, 0.6f, z), rnd(0.8f, 1.8f));
        }
        // Rocks at the base of the dam spillway (splash zone)
        for (int i = 0; i < 12; ++i) {
            float x = rnd(0.0f, 8.0f);
            float z = rnd(-5.0f, 5.0f);
            boulder(glm::vec3(x, 0.3f, z), rnd(0.6f, 1.2f));
        }

        // Trees along both banks
        auto tree = [&](glm::vec3 at, float s) {
            auto trunk = CreateGameObject(
                CreateCylinder(0.25f * s, 2.8f * s, 10,
                               glm::vec3(0.9f, 0.85f, 0.8f)),
                at + glm::vec3(0, 1.4f * s, 0));
            trunk->SetRotation(glm::vec3(jit(3), jit(360), jit(3)));
            trunk->SetTexture(wood_tex);
            trunk->SetNormalMap(noise_t);
            trunk->SetRoughness(0.85f);
            AddGameObject(trunk);
            glm::vec3 cc(rnd(0.12f, 0.25f), rnd(0.40f, 0.55f), rnd(0.14f, 0.25f));
            auto cone = CreateGameObject(
                CreateCone(1.3f * s, 2.8f * s, 10, cc),
                at + glm::vec3(jit(0.15f), 3.2f * s, jit(0.15f)));
            cone->SetNormalMap(noise_t);
            cone->SetRoughness(0.8f);
            AddGameObject(cone);
        };
        for (int i = 0; i < 30; ++i) {
            float x = rnd(-100.0f, 5.0f);
            float z = (i % 2 == 0 ? -1 : 1) * rnd(25.0f, 55.0f);
            tree(glm::vec3(x, 0, z), rnd(0.8f, 1.4f));
        }
        for (int i = 0; i < 20; ++i) {
            // Upstream bank trees at elevated bed
            float x = rnd(20.0f, 100.0f);
            float z = (i % 2 == 0 ? -1 : 1) * rnd(25.0f, 50.0f);
            tree(glm::vec3(x, 7.5f, z), rnd(0.7f, 1.2f));
        }

        // Distant mountains
        for (int i = 0; i < 12; ++i) {
            float x = -120.0f + i * 24.0f + jit(6.0f);
            float z = -150.0f + jit(12.0f);
            float s = rnd(0.9f, 1.4f);
            float h = 20.0f * s + rnd(0, 6);
            float r = 12.0f * s;
            auto m = CreateGameObject(
                CreateCone(r, h, 10, glm::vec3(0.18f, 0.15f, 0.18f)),
                glm::vec3(x, h * 0.5f, z));
            AddGameObject(m);
        }

        // Floating barrels in the downstream river (wind pushes them)
        for (int i = 0; i < 6; ++i) {
            float x = rnd(-60.0f, -10.0f);
            float z = rnd(-30.0f, 30.0f);
            auto barrel = CreateGameObject(
                CreateCylinder(0.7f, 1.3f, 12, glm::vec3(0.9f, 0.78f, 0.55f)),
                glm::vec3(x, 1.2f, z));
            barrel->SetRotation(glm::vec3(0, jit(180), 90.0f));
            barrel->SetTexture(wood_tex);
            barrel->SetNormalMap(noise_t);
            barrel->SetRoughness(0.8f);
            AddGameObject(barrel);
            std::string id = "barrel_" + std::to_string(i);
            namedObjects_[id] = barrel;
            json::Object c;
            c["action"] = json::Value(std::string("body"));
            c["id"] = json::Value(id);
            c["shape"] = json::Value(std::string("box"));
            c["mass"] = json::Value(1.0);
            c["restitution"] = json::Value(0.3);
            c["friction"] = json::Value(0.4);
            processAgentCommand(json::Value(c));
        }

        processAgentCommand(json::parse(R"({"action":"physics","enabled":true})"));

        // Camera: looking toward the dam from downstream, slightly elevated
        glm::vec3 eye(-20.0f, 4.5f, 0.0f);
        glm::vec3 target(10.0f, 6.0f, 0.0f);
        SetCameraPosition(eye);
        SetCameraTarget(target);
        glm::vec3 fwd = glm::normalize(target - eye);
        cam_pitch_ = glm::degrees(std::asin(fwd.y));
        cam_yaw_   = glm::degrees(std::atan2(fwd.z, fwd.x));
        last_cam_pos_ = eye;

        std::cout << "Dam demo ready: " << game_objects_.size() << " objects.\n"
                  << "Waterfall spawns physics droplets through the spillway.\n"
                  << "Camera: WASD/arrows, Space/LCtrl up/down, LShift sprint.\n";
        return true;
    }

    void EmitDroplet() {
        if ((int)active_drops_.size() >= kMaxDrops) {
            std::string old = active_drops_.front();
            active_drops_.pop_front();
            json::Object dc;
            dc["action"] = json::Value(std::string("delete"));
            dc["id"]     = json::Value(old);
            processAgentCommand(json::Value(dc));
        }
        std::uniform_real_distribution<float> zz(-5.5f, 5.5f);
        std::uniform_real_distribution<float> yy(-0.15f, 0.15f);
        std::string id = "drop_" + std::to_string(drop_counter_++);
        glm::vec3 spawn(12.0f,
                        8.2f + yy(drop_rng_),
                        zz(drop_rng_));
        auto d = CreateGameObject(
            CreateSphere(8, glm::vec3(0.6f, 0.8f, 0.95f)),
            spawn);
        d->SetScale(glm::vec3(0.22f));
        d->SetMetallic(0.0f);
        d->SetRoughness(0.15f);
        d->SetEmissive(glm::vec3(0.15f, 0.25f, 0.3f));  // slight glow for bloom haze
        AddGameObject(d);
        namedObjects_[id] = d;

        json::Object bc;
        bc["action"] = json::Value(std::string("body"));
        bc["id"] = json::Value(id);
        bc["shape"] = json::Value(std::string("sphere"));
        bc["mass"] = json::Value(0.1);
        bc["restitution"] = json::Value(0.25);
        bc["friction"] = json::Value(0.05);
        processAgentCommand(json::Value(bc));

        // Initial velocity: slightly downward + outward (away from dam)
        std::uniform_real_distribution<float> vx(-4.0f, -2.0f);
        std::uniform_real_distribution<float> vy(-1.5f, -0.5f);
        std::uniform_real_distribution<float> vz(-0.8f, 0.8f);
        json::Object vc;
        vc["action"] = json::Value(std::string("velocity"));
        vc["id"] = json::Value(id);
        vc["linear"] = json::Value(json::Array{
            json::Value((double)vx(drop_rng_)),
            json::Value((double)vy(drop_rng_)),
            json::Value((double)vz(drop_rng_))});
        processAgentCommand(json::Value(vc));

        active_drops_.push_back(id);
    }

    void OnUpdate(float dt) override {
        // Waterfall emitter: ~12 droplets per second
        drop_timer_ -= dt;
        while (drop_timer_ <= 0.0f) {
            EmitDroplet();
            drop_timer_ += 0.08f;
        }

        // --- Free-fly camera (same pattern as other scenes) ---
        glm::vec3 eng_pos = GetCameraPosition();
        if (glm::length(eng_pos - last_cam_pos_) > 0.0001f) {
            glm::vec3 fwd = GetCameraTarget() - eng_pos;
            if (glm::length(fwd) > 0.0001f) {
                fwd = glm::normalize(fwd);
                cam_pitch_ = glm::degrees(std::asin(fwd.y));
                cam_yaw_   = glm::degrees(std::atan2(fwd.z, fwd.x));
            }
        }
        glm::vec2 ls{0}, rs{0};
        float up = 0, down = 0, speed = cam_speed_;
        bool touched = false;
        if (Input::IsGamepadConnected()) {
            ls = Input::GetLeftStick();
            rs = Input::GetRightStick();
            up = Input::GetR2();
            down = Input::GetL2();
            if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross)) speed *= 3.0f;
            if (glm::length(ls) + glm::length(rs) + up + down > 0.01f) touched = true;
        }
        if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT))   speed *= 3.0f;
        if (Input::IsKeyHeld(GLFW_KEY_W))     { ls.y -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_S))     { ls.y += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_A))     { ls.x -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_D))     { ls.x += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_LEFT))  { rs.x -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) { rs.x += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_UP))    { rs.y -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_DOWN))  { rs.y += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_SPACE))        { up   += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL)) { down += 1.0f; touched = true; }
        if (!touched) { last_cam_pos_ = eng_pos; return; }
        cam_yaw_   += rs.x * look_sensitivity_ * dt;
        cam_pitch_ -= rs.y * look_sensitivity_ * dt;
        if (cam_pitch_ >  89.0f) cam_pitch_ =  89.0f;
        if (cam_pitch_ < -89.0f) cam_pitch_ = -89.0f;
        glm::vec3 fwd;
        fwd.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
        fwd.y = std::sin(glm::radians(cam_pitch_));
        fwd.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
        fwd = glm::normalize(fwd);
        glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
        glm::vec3 pos = eng_pos;
        pos += fwd * (-ls.y) * speed * dt;
        pos += right *  ls.x   * speed * dt;
        pos.y += (up - down) * speed * dt;
        SetCameraPosition(pos);
        SetCameraTarget(pos + fwd);
        last_cam_pos_ = pos;
    }
};


// ---------------------------------------------
// SanctuaryDemo: end-to-end engine showcase.
//   - PBR metals + normal-mapped stone/wood/grass
//   - HDR + bloom + ACES + MSAA + atmospheric fog
//   - Gerstner water with foam + buoyancy + wind current
//   - Cascade physics: a heavy metal sphere rolls off a ramp, knocks over a
//     line of wooden dominoes, which push a crate stack into the lake.
//   - Shadows, emissive brazier, copper orb, stone pillar ring.
// Pure C++ placement; no AI.
// ---------------------------------------------
class SanctuaryDemo : public Engine {
    float cam_yaw_   = 230.0f;
    float cam_pitch_ = -10.0f;
    float cam_speed_ = 22.0f;
    float look_sensitivity_ = 80.0f;
    glm::vec3 last_cam_pos_{0};
    float time_elapsed_ = 0.0f;
    bool  kickoff_fired_ = false;
    float shot_cooldown_ = 0.0f;
    int   shot_counter_ = 0;
    std::deque<std::string> active_shots_;
    static constexpr int kMaxShots = 60;
    std::mt19937 shot_rng_{1337};

public:
    SanctuaryDemo()
      : Engine(1920, 1080, "ZoomEngine - Sanctuary (Full Showcase)") {}

    void FireBall() {
        glm::vec3 cam_pos = GetCameraPosition();
        glm::vec3 cam_tgt = GetCameraTarget();
        glm::vec3 fwd = glm::normalize(cam_tgt - cam_pos);
        glm::vec3 spawn = cam_pos + fwd * 1.5f;

        std::string id = "shot_" + std::to_string(shot_counter_++);

        // Retire oldest shot if we're at cap
        if ((int)active_shots_.size() >= kMaxShots) {
            std::string old = active_shots_.front();
            active_shots_.pop_front();
            json::Object dc;
            dc["action"] = json::Value(std::string("delete"));
            dc["id"]     = json::Value(old);
            processAgentCommand(json::Value(dc));
        }

        // Color palette — HDR pops for bloom
        std::uniform_real_distribution<float> cd(0.6f, 1.6f);
        glm::vec3 col(cd(shot_rng_), cd(shot_rng_), cd(shot_rng_));

        auto ball = CreateGameObject(CreateSphere(20, col), spawn);
        ball->SetScale(glm::vec3(0.35f));
        ball->SetMetallic(1.0f);
        ball->SetRoughness(0.3f);
        AddGameObject(ball);
        namedObjects_[id] = ball;

        json::Object bc;
        bc["action"]      = json::Value(std::string("body"));
        bc["id"]          = json::Value(id);
        bc["shape"]       = json::Value(std::string("sphere"));
        bc["mass"]        = json::Value(1.5);
        bc["restitution"] = json::Value(0.55);
        bc["friction"]    = json::Value(0.3);
        processAgentCommand(json::Value(bc));

        json::Object vc;
        vc["action"] = json::Value(std::string("velocity"));
        vc["id"]     = json::Value(id);
        vc["linear"] = json::Value(json::Array{
            json::Value((double)(fwd.x * 55.0f)),
            json::Value((double)(fwd.y * 55.0f)),
            json::Value((double)(fwd.z * 55.0f)) });
        processAgentCommand(json::Value(vc));

        active_shots_.push_back(id);
    }

    bool OnInitialize() override {
        std::mt19937 rng(17);
        auto rnd = [&](float lo, float hi) {
            std::uniform_real_distribution<float> d(lo, hi); return d(rng);
        };
        auto jit = [&](float s) { return rnd(-s, s); };

        // --- Global lighting / post tuning ---
        processAgentCommand(json::parse(R"({
            "action":"sun",
            "direction":[-0.62, 0.25, -0.18],
            "color":[2.5, 1.45, 0.85],
            "zenith":[0.14, 0.22, 0.48],
            "horizon":[1.05, 0.58, 0.32],
            "sky":[0.70, 0.48, 0.32],
            "ground":[0.06, 0.06, 0.07],
            "shadow_extent":110,
            "exposure":0.95,
            "bloom_threshold":1.15,
            "bloom_intensity":1.2,
            "fog_density":0.011
        })"));

        // --- Water & weather ---
        processAgentCommand(json::parse(R"({
            "action":"water","enabled":true,
            "level":-0.45,"size":420,
            "amplitude":0.18,"wavelength":13,"speed":0.8,
            "shallow":[0.55,0.42,0.30],"deep":[0.03,0.07,0.13],
            "density":1.1,"resolution":180
        })"));
        processAgentCommand(json::parse(R"({
            "action":"weather","wind_dir":[0.9,0,0.4],
            "wind_speed":0.6,"storminess":0.08,"current_coef":0.5
        })"));

        // --- Ground ---
        auto grass = CreateGameObject(
            CreatePlane(420.0f, 420.0f, glm::vec3(0.95f, 0.97f, 0.92f)),
            glm::vec3(0, 0, 0));
        grass->SetTexture(TextureCache::Get("grass"));
        grass->SetNormalMap(TextureCache::Get("noise"));
        grass->SetUVScale(glm::vec2(0.35f));
        grass->SetRoughness(0.95f);
        AddGameObject(grass);

        // --- Textures / cached refs ---
        GLuint wood_tex    = TextureCache::Get("wood");
        GLuint rock_tex    = TextureCache::Get("rock");
        GLuint marble_tex  = TextureCache::Get("marble");
        GLuint brick_tex   = TextureCache::Get("brick");
        GLuint concrete    = TextureCache::Get("concrete");
        GLuint noise_tex   = TextureCache::Get("noise");

        // --- Helper for physics-enabled objects ---
        auto bodyCmd = [&](const std::string& id, const std::string& shape,
                           float mass, float rest, float frict,
                           bool is_static = false) {
            json::Object c;
            c["action"]      = json::Value(std::string("body"));
            c["id"]          = json::Value(id);
            c["shape"]       = json::Value(shape);
            c["mass"]        = json::Value((double)mass);
            c["restitution"] = json::Value((double)rest);
            c["friction"]    = json::Value((double)frict);
            if (is_static) c["static"] = json::Value(true);
            processAgentCommand(json::Value(c));
        };

        auto addBox = [&](const std::string& id, glm::vec3 pos, glm::vec3 scale,
                          glm::vec3 color, GLuint tex, GLuint normal,
                          float metallic, float roughness,
                          float mass, float rest = 0.2f, float frict = 0.65f,
                          glm::vec3 rot = glm::vec3(0)) {
            auto obj = CreateGameObject(CreateCube(color), pos);
            obj->SetScale(scale);
            obj->SetRotation(rot);
            if (tex) obj->SetTexture(tex);
            if (normal) obj->SetNormalMap(normal);
            obj->SetMetallic(metallic);
            obj->SetRoughness(roughness);
            AddGameObject(obj);
            if (!id.empty()) namedObjects_[id] = obj;
            if (mass > 0 || mass == -1.0f)  // -1 = static
                bodyCmd(id, "box", (mass == -1.0f) ? 0 : mass, rest, frict,
                        mass == -1.0f);
            return obj;
        };

        auto addSphere = [&](const std::string& id, glm::vec3 pos, float radius,
                             glm::vec3 color, GLuint tex, GLuint normal,
                             float metallic, float roughness,
                             float mass, float rest = 0.4f, float frict = 0.3f) {
            auto obj = CreateGameObject(CreateSphere(36, color), pos);
            obj->SetScale(glm::vec3(radius));
            if (tex) obj->SetTexture(tex);
            if (normal) obj->SetNormalMap(normal);
            obj->SetMetallic(metallic);
            obj->SetRoughness(roughness);
            AddGameObject(obj);
            if (!id.empty()) namedObjects_[id] = obj;
            if (mass > 0) bodyCmd(id, "sphere", mass, rest, frict);
            return obj;
        };

        // --- Central forge: stone altar + emissive brazier ---
        addBox("altar",
               glm::vec3(0, 0.75f, 0), glm::vec3(3.0f, 1.5f, 3.0f),
               glm::vec3(0.88f, 0.84f, 0.80f),
               marble_tex, noise_tex, 0.0f, 0.55f, 0);
        // Brazier bowl (flattened sphere with copper tint)
        auto bowl = CreateGameObject(
            CreateSphere(32, glm::vec3(0.95f, 0.55f, 0.28f)),
            glm::vec3(0, 1.65f, 0));
        bowl->SetScale(glm::vec3(0.8f, 0.4f, 0.8f));
        bowl->SetMetallic(1.0f);
        bowl->SetRoughness(0.35f);
        AddGameObject(bowl);
        // Flame — two emissive cones
        auto flame_outer = CreateGameObject(
            CreateCone(0.5f, 1.6f, 12, glm::vec3(1.0f)),
            glm::vec3(0, 2.5f, 0));
        flame_outer->SetEmissive(glm::vec3(5.0f, 1.8f, 0.4f));
        AddGameObject(flame_outer);
        auto flame_inner = CreateGameObject(
            CreateCone(0.25f, 1.0f, 12, glm::vec3(1.0f)),
            glm::vec3(0, 2.6f, 0));
        flame_inner->SetEmissive(glm::vec3(8.0f, 3.5f, 1.2f));
        AddGameObject(flame_inner);

        // --- Ring of stone pillars ---
        for (int i = 0; i < 8; ++i) {
            float ang = (float)i / 8 * 2 * M_PI;
            float R = 7.5f;
            float h = 4.0f + jit(0.5f);
            auto pillar = CreateGameObject(
                CreateCylinder(0.5f, h, 12,
                               glm::vec3(0.88f, 0.85f, 0.82f)),
                glm::vec3(std::cos(ang) * R, h * 0.5f, std::sin(ang) * R));
            pillar->SetTexture(marble_tex);
            pillar->SetNormalMap(noise_tex);
            pillar->SetRoughness(0.55f);
            AddGameObject(pillar);
        }

        // --- PBR showcase pieces ---
        // Polished chrome obelisk
        addBox("obelisk",
               glm::vec3(-7.0f, 3.0f, -2.0f), glm::vec3(0.8f, 6.0f, 0.8f),
               glm::vec3(0.95f, 0.95f, 0.95f), 0, 0,
               1.0f, 0.15f, -1.0f,
               0.0f, 0.0f, glm::vec3(0, 15.0f, 0));
        // Copper mirror-ish orb
        addSphere("copper_orb",
                  glm::vec3(8.0f, 1.3f, -2.5f), 1.3f,
                  glm::vec3(0.95f, 0.55f, 0.28f), 0, 0,
                  1.0f, 0.2f,
                  0.0f);  // static visual

        // --- Physics chain-reaction setup ---
        // A launch ramp angled down from east toward origin; metal ball at top.
        // Ramp is a tilted box; static.
        addBox("ramp",
               glm::vec3(22.0f, 1.9f, 0.0f), glm::vec3(7.0f, 0.3f, 3.0f),
               glm::vec3(0.9f, 0.85f, 0.78f),
               wood_tex, noise_tex, 0.0f, 0.7f, -1.0f,
               0.0f, 0.0f, glm::vec3(0, 0, -18.0f));  // tilt down

        // Domino chain — 12 wooden boards standing in a line from origin out
        for (int i = 0; i < 12; ++i) {
            float x = 12.0f - i * 1.6f;
            addBox("dom_" + std::to_string(i),
                   glm::vec3(x, 1.0f, 0), glm::vec3(0.3f, 2.0f, 1.2f),
                   glm::vec3(0.9f, 0.85f, 0.7f),
                   wood_tex, noise_tex, 0.0f, 0.6f,
                   1.0f, 0.08f, 0.55f);
        }

        // Crate stack — pyramid of wooden crates east of dominoes, near shore
        for (int y = 0; y < 3; ++y) {
            int n = 4 - y;
            for (int i = 0; i < n; ++i) {
                float x = -10.0f - (3 - n) * 0.5f + i * 1.1f;
                float yp = 0.5f + y * 1.0f;
                addBox("crate_" + std::to_string(y * 4 + i),
                       glm::vec3(x, yp, -8.0f), glm::vec3(1.0f),
                       glm::vec3(0.85f, 0.75f, 0.55f),
                       wood_tex, noise_tex, 0.0f, 0.7f,
                       0.8f, 0.1f, 0.7f);
            }
        }

        // Impulse ball — heavy metal sphere at top of ramp
        auto ball = addSphere("impulse_ball",
                              glm::vec3(25.0f, 5.5f, 0.0f), 0.9f,
                              glm::vec3(0.75f, 0.78f, 0.82f), 0, 0,
                              1.0f, 0.3f,
                              8.0f, 0.3f, 0.25f);
        (void)ball;

        // --- Bonus physics content: much more to knock around ---

        // Second domino line at an angle, north of the first
        for (int i = 0; i < 14; ++i) {
            float x = 8.0f - i * 1.4f;
            float z = -4.0f - i * 0.4f;
            addBox("dom2_" + std::to_string(i),
                   glm::vec3(x, 1.0f, z), glm::vec3(0.28f, 2.0f, 1.1f),
                   glm::vec3(0.9f, 0.82f, 0.65f),
                   wood_tex, noise_tex, 0.0f, 0.65f,
                   0.9f, 0.08f, 0.55f,
                   glm::vec3(0, 16.0f, 0));
        }

        // Second crate pyramid, east of the first
        for (int y = 0; y < 3; ++y) {
            int n = 4 - y;
            for (int i = 0; i < n; ++i) {
                float x = 4.0f + i * 1.1f + (3 - n) * 0.5f;
                float yp = 0.5f + y * 1.0f;
                float z = -14.0f;
                addBox("crate2_" + std::to_string(y * 4 + i),
                       glm::vec3(x, yp, z), glm::vec3(1.0f),
                       glm::vec3(0.85f, 0.75f, 0.55f),
                       wood_tex, noise_tex, 0.0f, 0.7f,
                       0.8f, 0.1f, 0.7f);
            }
        }

        // Third crate wall (flat stack), on the west side
        for (int i = 0; i < 12; ++i) {
            int col = i % 4, row = i / 4;
            float x = -18.0f + col * 1.1f;
            float y = 0.5f + row * 1.1f;
            addBox("wallc_" + std::to_string(i),
                   glm::vec3(x, y, -6.0f), glm::vec3(1.0f),
                   glm::vec3(0.65f, 0.78f, 0.85f),  // blueish
                   concrete, noise_tex, 0.0f, 0.75f,
                   0.6f, 0.2f, 0.6f);
        }

        // Bowling-pin triangle near the center, 10 pins
        static const float pin_offsets[10][2] = {
            { 0.0f, 0.0f},
            {-0.45f, 0.8f}, { 0.45f, 0.8f},
            {-0.9f, 1.6f}, { 0.0f, 1.6f}, { 0.9f, 1.6f},
            {-1.35f, 2.4f}, {-0.45f, 2.4f}, { 0.45f, 2.4f}, { 1.35f, 2.4f}
        };
        for (int i = 0; i < 10; ++i) {
            float px = 16.0f + pin_offsets[i][1];
            float pz = 3.0f + pin_offsets[i][0];
            auto pin = CreateGameObject(
                CreateCylinder(0.35f, 1.6f, 14, glm::vec3(0.98f, 0.96f, 0.92f)),
                glm::vec3(px, 0.8f, pz));
            pin->SetMetallic(0.0f);
            pin->SetRoughness(0.35f);
            AddGameObject(pin);
            std::string id = "pin_" + std::to_string(i);
            namedObjects_[id] = pin;
            bodyCmd(id, "box", 0.5f, 0.2f, 0.4f);  // box collider is fine for pins
        }

        // Tall cube tower (8 blocks high) inviting to be knocked down
        for (int i = 0; i < 8; ++i) {
            float y = 0.6f + i * 1.2f;
            addBox("tower_" + std::to_string(i),
                   glm::vec3(-15.0f, y, 8.0f), glm::vec3(1.2f),
                   glm::vec3(0.85f, 0.80f, 0.72f),
                   marble_tex, noise_tex, 0.0f, 0.5f,
                   0.9f, 0.12f, 0.55f,
                   glm::vec3(0, i * 7.0f, 0));
        }

        // Scattered loose objects around the forge area for casual physics
        for (int i = 0; i < 20; ++i) {
            float ang = (float)i / 20.0f * 2 * M_PI + rnd(-0.1f, 0.1f);
            float R   = 18.0f + rnd(-3.0f, 3.0f);
            float x   = std::cos(ang) * R;
            float z   = std::sin(ang) * R;
            bool is_sphere = (i % 2) == 0;
            std::string id = "loose_" + std::to_string(i);
            if (is_sphere) {
                addSphere(id,
                          glm::vec3(x, 1.0f, z), rnd(0.4f, 0.8f),
                          glm::vec3(rnd(0.4f, 0.95f), rnd(0.4f, 0.95f), rnd(0.4f, 0.95f)),
                          rock_tex, noise_tex,
                          rnd(0.0f, 0.9f), rnd(0.2f, 0.9f),
                          0.6f, 0.5f, 0.3f);
            } else {
                addBox(id,
                       glm::vec3(x, 1.0f, z), glm::vec3(rnd(0.6f, 1.1f)),
                       glm::vec3(rnd(0.5f, 0.95f), rnd(0.5f, 0.95f), rnd(0.5f, 0.95f)),
                       wood_tex, noise_tex,
                       0.0f, 0.65f,
                       0.7f, 0.15f, 0.55f,
                       glm::vec3(jit(15), jit(180), jit(15)));
            }
        }

        // --- Floating barrels on the lake (buoyancy demo) ---
        for (int i = 0; i < 5; ++i) {
            float x = rnd(-20.0f, 20.0f);
            float z = rnd(-55.0f, -25.0f);
            auto barrel = CreateGameObject(
                CreateCylinder(0.8f, 1.4f, 12,
                               glm::vec3(0.9f, 0.78f, 0.55f)),
                glm::vec3(x, 1.5f, z));
            barrel->SetRotation(glm::vec3(0, jit(180.0f), 90.0f));
            barrel->SetTexture(wood_tex);
            barrel->SetNormalMap(noise_tex);
            barrel->SetRoughness(0.8f);
            AddGameObject(barrel);
            std::string id = "barrel_" + std::to_string(i);
            namedObjects_[id] = barrel;
            bodyCmd(id, "box", 1.0f, 0.3f, 0.4f);  // approximate as box
        }

        // Floating metal orbs
        for (int i = 0; i < 3; ++i) {
            float x = rnd(-12.0f, 12.0f);
            float z = rnd(-50.0f, -30.0f);
            addSphere("orb_float_" + std::to_string(i),
                      glm::vec3(x, 1.2f, z), 0.7f,
                      glm::vec3(0.92f, 0.65f, 0.35f), 0, 0,
                      1.0f, 0.3f,
                      0.7f, 0.4f);
        }

        // --- Distant mountain silhouettes ---
        for (int i = 0; i < 13; ++i) {
            float x = -175.0f + i * 28.0f + jit(6.0f);
            float z = -170.0f + jit(12.0f);
            float s = rnd(0.9f, 1.5f);
            float h = 22.0f * s + rnd(0, 6);
            float r = 14.0f * s;
            auto m = CreateGameObject(
                CreateCone(r, h, 10, glm::vec3(0.16f, 0.13f, 0.16f)),
                glm::vec3(x, h * 0.5f, z));
            m->SetRoughness(0.95f);
            AddGameObject(m);
        }

        // --- Forest (trees around perimeter, avoiding focal area) ---
        auto tree = [&](glm::vec3 at, float s) {
            auto trunk = CreateGameObject(
                CreateCylinder(0.24f * s, 2.7f * s, 10,
                               glm::vec3(0.92f, 0.88f, 0.82f)),
                at + glm::vec3(0, 1.35f * s, 0));
            trunk->SetRotation(glm::vec3(jit(4), jit(360), jit(4)));
            trunk->SetTexture(wood_tex);
            trunk->SetNormalMap(noise_tex);
            trunk->SetRoughness(0.85f);
            AddGameObject(trunk);
            int layers = 1 + (int)(rng() % 3);
            for (int k = 0; k < layers; ++k) {
                float lh = 2.6f * s * (1.0f - k * 0.22f);
                float lr = 1.3f * s * (1.0f - k * 0.13f);
                glm::vec3 cc(rnd(0.10f, 0.22f), rnd(0.36f, 0.52f),
                             rnd(0.12f, 0.22f));
                auto cone = CreateGameObject(
                    CreateCone(lr, lh, 10, cc),
                    at + glm::vec3(jit(0.15f), 3.0f * s + k * 1.0f * s, jit(0.15f)));
                cone->SetNormalMap(noise_tex);
                cone->SetRoughness(0.8f);
                AddGameObject(cone);
            }
        };
        for (int i = 0; i < 40; ++i) {
            // Scatter around a ring, skipping the focal cone
            float x, z;
            do {
                x = rnd(-80.0f, 80.0f);
                z = rnd(-80.0f, 40.0f);
            } while (std::abs(x) < 18.0f && z > -15.0f && z < 25.0f);  // clearing
            tree(glm::vec3(x, 0, z), rnd(0.7f, 1.4f));
        }

        // --- Boulder scatter ---
        for (int i = 0; i < 35; ++i) {
            float x, z;
            do {
                x = rnd(-70.0f, 70.0f);
                z = rnd(-25.0f, 35.0f);
            } while (std::abs(x) < 10.0f && std::abs(z) < 10.0f);  // no clutter on forge
            float s = rnd(0.4f, 1.3f);
            auto rock = CreateGameObject(
                CreateSphere(14, glm::vec3(0.8f, 0.77f, 0.74f)),
                glm::vec3(x, s * 0.35f, z));
            rock->SetScale(glm::vec3(s, s * rnd(0.5f, 0.8f), s * rnd(0.8f, 1.1f)));
            rock->SetRotation(glm::vec3(jit(15), jit(180), jit(15)));
            rock->SetTexture(rock_tex);
            rock->SetNormalMap(noise_tex);
            rock->SetRoughness(0.9f);
            AddGameObject(rock);
        }

        // --- Reeds along shore ---
        for (int i = 0; i < 70; ++i) {
            float x = rnd(-45.0f, 45.0f);
            float z = rnd(-20.0f, -8.0f);
            float h = rnd(0.8f, 1.7f);
            auto reed = CreateGameObject(
                CreateCylinder(0.04f, h, 4,
                               glm::vec3(rnd(0.5f, 0.65f), rnd(0.45f, 0.55f),
                                         rnd(0.2f, 0.3f))),
                glm::vec3(x, h * 0.5f, z));
            reed->SetRotation(glm::vec3(jit(12), 0, jit(12)));
            reed->SetRoughness(0.8f);
            AddGameObject(reed);
        }

        // --- Wildflowers (HDR-saturated for bloom) ---
        for (int i = 0; i < 120; ++i) {
            float x = rnd(-45.0f, 45.0f);
            float z = rnd(-5.0f, 30.0f);
            if (std::abs(x) < 6.0f && std::abs(z) < 6.0f) continue;  // keep forge clear
            glm::vec3 col;
            int pick = (int)(rng() % 5);
            if      (pick == 0) col = glm::vec3(1.6f, 1.2f, 0.3f);
            else if (pick == 1) col = glm::vec3(1.5f, 0.4f, 0.8f);
            else if (pick == 2) col = glm::vec3(1.3f, 1.25f, 1.2f);
            else if (pick == 3) col = glm::vec3(0.8f, 0.5f, 1.5f);
            else                col = glm::vec3(1.6f, 0.6f, 0.2f);
            auto fl = CreateGameObject(CreateSphere(8, col),
                                       glm::vec3(x, 0.2f + jit(0.05f), z));
            fl->SetScale(glm::vec3(0.09f));
            fl->SetEmissive(col * 0.3f);
            AddGameObject(fl);
        }

        // Enable physics
        processAgentCommand(json::parse(R"({"action":"physics","enabled":true})"));

        // --- Camera pose ---
        glm::vec3 eye(18.0f, 5.5f, 18.0f);
        glm::vec3 target(2.0f, 2.0f, -8.0f);
        SetCameraPosition(eye);
        SetCameraTarget(target);
        glm::vec3 fwd = glm::normalize(target - eye);
        cam_pitch_ = glm::degrees(std::asin(fwd.y));
        cam_yaw_   = glm::degrees(std::atan2(fwd.z, fwd.x));
        last_cam_pos_ = eye;

        std::cout << "Sanctuary demo ready: " << game_objects_.size()
                  << " objects. Chain reaction fires at t=2s.\n"
                  << "Camera: WASD/arrows, Space/LCtrl = up/down, LShift = sprint.\n"
                  << "SHOOT: hold F, click LMB, or hold R1 (gamepad) to fire balls.\n";
        return true;
    }

    void OnUpdate(float dt) override {
        time_elapsed_ += dt;

        // Kick off the chain reaction 2 seconds in, once physics is warm
        if (!kickoff_fired_ && time_elapsed_ > 2.0f) {
            kickoff_fired_ = true;
            // Big left-ward nudge so the ball rolls down the ramp into dominoes
            json::Object c;
            c["action"]  = json::Value(std::string("impulse"));
            c["id"]      = json::Value(std::string("impulse_ball"));
            c["impulse"] = json::Value(json::Array{
                json::Value(-120.0),
                json::Value(0.0),
                json::Value(0.0)});
            processAgentCommand(json::Value(c));
        }

        // Shoot balls from camera: F / LMB / R1 — rate-limited
        shot_cooldown_ -= dt;
        if (shot_cooldown_ <= 0.0f) {
            bool fire = false;
            if (Input::IsKeyHeld(GLFW_KEY_F))
                fire = true;
            if (Input::IsMouseButtonPressed(Input::MouseButton::Left))
                fire = true;
            if (Input::IsGamepadButtonDown(Input::GamepadButton::R1))
                fire = true;
            if (fire) {
                FireBall();
                shot_cooldown_ = 0.16f;  // ~6 shots/sec
            }
        }

        // --- Free-fly camera ---
        glm::vec3 eng_pos = GetCameraPosition();
        if (glm::length(eng_pos - last_cam_pos_) > 0.0001f) {
            glm::vec3 fwd = GetCameraTarget() - eng_pos;
            if (glm::length(fwd) > 0.0001f) {
                fwd = glm::normalize(fwd);
                cam_pitch_ = glm::degrees(std::asin(fwd.y));
                cam_yaw_   = glm::degrees(std::atan2(fwd.z, fwd.x));
            }
        }

        glm::vec2 ls{0}, rs{0};
        float up = 0, down = 0, speed = cam_speed_;
        bool touched = false;
        if (Input::IsGamepadConnected()) {
            ls = Input::GetLeftStick();
            rs = Input::GetRightStick();
            up = Input::GetR2();
            down = Input::GetL2();
            if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross)) speed *= 3.0f;
            if (glm::length(ls) + glm::length(rs) + up + down > 0.01f) touched = true;
        }
        if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT))   speed *= 3.0f;
        if (Input::IsKeyHeld(GLFW_KEY_W))     { ls.y -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_S))     { ls.y += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_A))     { ls.x -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_D))     { ls.x += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_LEFT))  { rs.x -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) { rs.x += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_UP))    { rs.y -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_DOWN))  { rs.y += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_SPACE))        { up   += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL)) { down += 1.0f; touched = true; }

        if (!touched) { last_cam_pos_ = eng_pos; return; }

        cam_yaw_   += rs.x * look_sensitivity_ * dt;
        cam_pitch_ -= rs.y * look_sensitivity_ * dt;
        if (cam_pitch_ >  89.0f) cam_pitch_ =  89.0f;
        if (cam_pitch_ < -89.0f) cam_pitch_ = -89.0f;
        glm::vec3 fwd;
        fwd.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
        fwd.y = std::sin(glm::radians(cam_pitch_));
        fwd.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
        fwd = glm::normalize(fwd);
        glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
        glm::vec3 pos = eng_pos;
        pos += fwd * (-ls.y) * speed * dt;
        pos += right *  ls.x   * speed * dt;
        pos.y += (up - down) * speed * dt;
        SetCameraPosition(pos);
        SetCameraTarget(pos + fwd);
        last_cam_pos_ = pos;
    }
};


// ---------------------------------------------
// SunsetForestDemo: a composed, cinematic scene built to show off the whole
// rendering pipeline — HDR/bloom/tone mapping, shadows, Gerstner water,
// atmospheric fog, sun at a specific time of day. Pure C++ placement, no AI.
// ---------------------------------------------
class SunsetForestDemo : public Engine {
    float cam_yaw_   = 225.0f;
    float cam_pitch_ = -12.0f;
    float cam_speed_ = 20.0f;
    float look_sensitivity_ = 80.0f;
    glm::vec3 last_cam_pos_{0};

public:
    SunsetForestDemo()
      : Engine(1920, 1080, "ZoomEngine - Sunset Forest (HDR)") {}

    bool OnInitialize() override {
        std::mt19937 rng(42);
        auto rand = [&](float lo, float hi) {
            std::uniform_real_distribution<float> d(lo, hi);
            return d(rng);
        };
        auto jitter = [&](float scale) { return rand(-scale, scale); };

        // --- Sunset palette + atmosphere + HDR tuning ---
        processAgentCommand(json::parse(R"({
            "action":"sun",
            "direction":[-0.75, 0.18, -0.15],
            "color":[2.6, 1.40, 0.80],
            "zenith":[0.14, 0.20, 0.45],
            "horizon":[1.05, 0.55, 0.28],
            "sky":[0.75, 0.45, 0.28],
            "ground":[0.05, 0.05, 0.07],
            "shadow_extent":120,
            "exposure":0.9,
            "bloom_threshold":1.15,
            "bloom_intensity":1.1,
            "fog_density":0.012
        })"));

        // --- Ground (grass, textured, PBR) ---
        auto grass = CreateGameObject(
            CreatePlane(400.0f, 400.0f, glm::vec3(0.9f, 0.95f, 0.9f)),
            glm::vec3(0, 0, 0));
        grass->SetTexture(TextureCache::Get("grass"));
        grass->SetNormalMap(TextureCache::Get("noise"));  // fake micro-relief
        grass->SetUVScale(glm::vec2(0.3f));
        grass->SetMetallic(0.0f);
        grass->SetRoughness(0.95f);
        AddGameObject(grass);

        // --- Lake + light breeze ---
        processAgentCommand(json::parse(R"({
            "action":"water",
            "enabled":true,
            "level":-0.55,
            "size":420,
            "amplitude":0.14,
            "wavelength":14,
            "speed":0.7,
            "shallow":[0.55, 0.42, 0.30],
            "deep":[0.03, 0.07, 0.13],
            "resolution":180
        })"));
        processAgentCommand(json::parse(R"({
            "action":"weather",
            "wind_dir":[0.8, 0, 0.6],
            "wind_speed":1.0,
            "storminess":0.1
        })"));

        // Distant mountain silhouettes — broad cones along the horizon
        for (int i = 0; i < 11; ++i) {
            float x = -160.0f + i * 32.0f + jitter(6.0f);
            float z = -170.0f + jitter(10.0f);
            float s = rand(0.9f, 1.5f);
            float h = 22.0f * s + rand(0.0f, 6.0f);
            float r = 14.0f * s;
            auto m = CreateGameObject(
                CreateCone(r, h, 10, glm::vec3(0.17f, 0.13f, 0.16f)),
                glm::vec3(x, h * 0.5f, z));
            AddGameObject(m);
        }
        // Second ridge: smaller cones in front of the big ones, lighter tone
        for (int i = 0; i < 14; ++i) {
            float x = -140.0f + i * 20.0f + jitter(4.0f);
            float z = -130.0f + jitter(8.0f);
            float h = 10.0f + jitter(3.0f);
            float r = 7.0f + jitter(1.5f);
            auto m = CreateGameObject(
                CreateCone(r, h, 9, glm::vec3(0.22f, 0.18f, 0.20f)),
                glm::vec3(x, h * 0.5f, z));
            AddGameObject(m);
        }

        GLuint wood_tex = TextureCache::Get("wood");
        GLuint rock_tex = TextureCache::Get("rock");

        GLuint noise_tex = TextureCache::Get("noise");

        // Tree helper: trunk + 1-3 stacked canopy cones
        auto tree = [&](glm::vec3 pos, float s, glm::vec3 canopyCol) {
            auto trunk = CreateGameObject(
                CreateCylinder(0.22f * s, 2.6f * s, 8,
                               glm::vec3(0.9f, 0.85f, 0.8f)),
                pos + glm::vec3(0, 1.3f * s, 0));
            trunk->SetRotation(glm::vec3(jitter(4.0f), rand(0.0f, 360.0f), jitter(4.0f)));
            trunk->SetTexture(wood_tex);
            trunk->SetNormalMap(noise_tex);
            trunk->SetMetallic(0.0f);
            trunk->SetRoughness(0.85f);
            AddGameObject(trunk);
            int layers = 1 + (int)(rng() % 3);
            for (int i = 0; i < layers; ++i) {
                float lh = 2.4f * s * (1.0f - i * 0.22f);
                float lr = 1.25f * s * (1.0f - i * 0.14f);
                auto cone = CreateGameObject(
                    CreateCone(lr, lh, 10, canopyCol * (0.85f + i * 0.07f)),
                    pos + glm::vec3(jitter(0.2f),
                                    2.7f * s + i * 1.1f * s,
                                    jitter(0.2f)));
                cone->SetNormalMap(noise_tex);
                cone->SetRoughness(0.8f);
                AddGameObject(cone);
            }
        };

        // Ridge/foreground trees (near camera, where ground is above lake level)
        for (int i = 0; i < 30; ++i) {
            float x = rand(-70.0f, 70.0f);
            float z = rand(5.0f, 45.0f);
            // Keep clear corridor near the camera focal area
            if (std::abs(x + 8.0f) < 5.0f && z < 18.0f) continue;
            float s = rand(0.9f, 1.6f);
            glm::vec3 col(rand(0.11f, 0.22f), rand(0.34f, 0.50f), rand(0.13f, 0.22f));
            tree(glm::vec3(x, 0, z), s, col);
        }
        // Far shore trees (past the lake)
        for (int i = 0; i < 45; ++i) {
            float x = rand(-120.0f, 120.0f);
            float z = rand(-95.0f, -45.0f);
            float s = rand(0.55f, 1.0f);
            glm::vec3 col(rand(0.07f, 0.14f), rand(0.22f, 0.35f), rand(0.09f, 0.16f));
            tree(glm::vec3(x, 0, z), s, col);
        }

        // Boulders along the shore and scattered across the ridge
        for (int i = 0; i < 45; ++i) {
            float x = rand(-70.0f, 70.0f);
            float z = rand(-12.0f, 32.0f);
            float s = rand(0.4f, 1.4f);
            auto rock = CreateGameObject(
                CreateSphere(16,
                    glm::vec3(rand(0.75f, 0.95f),
                              rand(0.72f, 0.92f),
                              rand(0.70f, 0.90f))),
                glm::vec3(x, s * 0.35f, z));
            rock->SetScale(glm::vec3(s, s * rand(0.5f, 0.8f), s * rand(0.8f, 1.1f)));
            rock->SetRotation(glm::vec3(jitter(15.0f), jitter(180.0f), jitter(15.0f)));
            rock->SetTexture(rock_tex);
            rock->SetNormalMap(noise_tex);
            rock->SetMetallic(0.0f);
            rock->SetRoughness(0.9f);
            AddGameObject(rock);
        }

        // Reeds along the shoreline (near the lake-grass boundary)
        for (int i = 0; i < 80; ++i) {
            float x = rand(-50.0f, 50.0f);
            // Tight zone on the water side of the shore
            float z = rand(-20.0f, -6.0f);
            float h = rand(0.7f, 1.6f);
            auto reed = CreateGameObject(
                CreateCylinder(0.045f, h, 4,
                               glm::vec3(rand(0.45f, 0.60f),
                                         rand(0.40f, 0.55f),
                                         rand(0.18f, 0.28f))),
                glm::vec3(x, h * 0.5f, z));
            reed->SetRotation(glm::vec3(jitter(15.0f), 0, jitter(15.0f)));
            AddGameObject(reed);
        }

        // Wildflowers: tiny emissive-ish spheres — boost colors so they pop in HDR
        for (int i = 0; i < 100; ++i) {
            float x = rand(-50.0f, 50.0f);
            float z = rand(4.0f, 38.0f);
            glm::vec3 col;
            int pick = (int)(rng() % 5);
            if      (pick == 0) col = glm::vec3(1.40f, 1.10f, 0.30f); // yellow
            else if (pick == 1) col = glm::vec3(1.30f, 0.40f, 0.75f); // pink
            else if (pick == 2) col = glm::vec3(1.20f, 1.15f, 1.10f); // white
            else if (pick == 3) col = glm::vec3(0.70f, 0.45f, 1.30f); // purple
            else                col = glm::vec3(1.40f, 0.55f, 0.20f); // orange
            auto fl = CreateGameObject(CreateSphere(6, col),
                                       glm::vec3(x, 0.18f + jitter(0.05f), z));
            fl->SetScale(glm::vec3(0.09f));
            AddGameObject(fl);
        }

        // Fallen log — story beat in foreground
        {
            auto log = CreateGameObject(
                CreateCylinder(0.55f, 6.5f, 10, glm::vec3(0.92f, 0.88f, 0.82f)),
                glm::vec3(6.0f, 0.55f, 13.0f));
            log->SetRotation(glm::vec3(0, 15.0f, 90.0f));
            log->SetTexture(wood_tex);
            log->SetNormalMap(noise_tex);
            log->SetRoughness(0.8f);
            AddGameObject(log);
        }

        // Polished metal obelisk — PBR showcase (high metallic, low roughness)
        {
            auto obelisk = CreateGameObject(
                CreateCube(glm::vec3(0.92f, 0.88f, 0.78f)),
                glm::vec3(-8.0f, 3.0f, -4.0f));
            obelisk->SetScale(glm::vec3(0.7f, 4.5f, 0.7f));
            obelisk->SetRotation(glm::vec3(0, 18.0f, 0));
            obelisk->SetMetallic(1.0f);
            obelisk->SetRoughness(0.25f);
            AddGameObject(obelisk);

            // Brushed copper sphere — another PBR reference
            auto orb = CreateGameObject(
                CreateSphere(64, glm::vec3(0.95f, 0.55f, 0.28f)),
                glm::vec3(10.0f, 1.2f, -1.0f));
            orb->SetScale(glm::vec3(1.2f));
            orb->SetMetallic(1.0f);
            orb->SetRoughness(0.35f);
            AddGameObject(orb);
        }

        // Campfire — bright HDR colors for the flame to bloom
        {
            glm::vec3 fire_pos(0.0f, 0, 12.0f);
            // Two crossed logs
            auto a = CreateGameObject(
                CreateCylinder(0.16f, 1.3f, 6, glm::vec3(0.20f, 0.12f, 0.06f)),
                fire_pos + glm::vec3(0, 0.16f, 0));
            a->SetRotation(glm::vec3(0, 30.0f, 90.0f));
            AddGameObject(a);
            auto b = CreateGameObject(
                CreateCylinder(0.16f, 1.3f, 6, glm::vec3(0.22f, 0.14f, 0.07f)),
                fire_pos + glm::vec3(0, 0.16f, 0));
            b->SetRotation(glm::vec3(0, -30.0f, 90.0f));
            AddGameObject(b);
            // Outer flame (dimmer, larger) — emissive so it glows via bloom
            auto f1 = CreateGameObject(
                CreateCone(0.45f, 1.3f, 10, glm::vec3(1.0f)),
                fire_pos + glm::vec3(0, 0.75f, 0));
            f1->SetEmissive(glm::vec3(4.5f, 1.6f, 0.35f));
            AddGameObject(f1);
            auto f2 = CreateGameObject(
                CreateCone(0.22f, 0.8f, 10, glm::vec3(1.0f)),
                fire_pos + glm::vec3(0, 0.90f, 0));
            f2->SetEmissive(glm::vec3(7.0f, 3.0f, 0.9f));
            AddGameObject(f2);
            // Ring of stones
            for (int i = 0; i < 9; ++i) {
                float a_rad = i * 0.698f;
                glm::vec3 p = fire_pos + glm::vec3(std::cos(a_rad) * 0.85f,
                                                   0.08f,
                                                   std::sin(a_rad) * 0.85f);
                auto r = CreateGameObject(
                    CreateSphere(8, glm::vec3(0.28f, 0.26f, 0.24f)), p);
                r->SetScale(glm::vec3(0.28f, 0.18f, 0.28f));
                AddGameObject(r);
            }
        }

        // Camera: cinematic three-quarter, looking across ridge -> lake -> mountains
        glm::vec3 eye(20.0f, 7.5f, 28.0f);
        glm::vec3 target(-4.0f, 1.0f, -25.0f);
        SetCameraPosition(eye);
        SetCameraTarget(target);
        glm::vec3 fwd0 = glm::normalize(target - eye);
        cam_pitch_ = glm::degrees(std::asin(fwd0.y));
        cam_yaw_   = glm::degrees(std::atan2(fwd0.z, fwd0.x));
        last_cam_pos_ = eye;

        std::cout << "Sunset Forest ready: " << game_objects_.size()
                  << " objects.\n"
                  << "Controls: WASD/arrows or gamepad, Space/R2 up, "
                     "LCtrl/L2 down, LShift/Cross sprint.\n";
        return true;
    }

    void OnUpdate(float dt) override {
        // Free-fly camera (copied from GrassPoolsDemo for standalone mode).
        glm::vec3 eng_pos = GetCameraPosition();
        if (glm::length(eng_pos - last_cam_pos_) > 0.0001f) {
            glm::vec3 fwd = GetCameraTarget() - eng_pos;
            if (glm::length(fwd) > 0.0001f) {
                fwd = glm::normalize(fwd);
                cam_pitch_ = glm::degrees(std::asin(fwd.y));
                cam_yaw_   = glm::degrees(std::atan2(fwd.z, fwd.x));
            }
        }

        glm::vec2 ls{0}, rs{0};
        float up = 0, down = 0, speed = cam_speed_;
        bool touched = false;

        if (Input::IsGamepadConnected()) {
            ls = Input::GetLeftStick();
            rs = Input::GetRightStick();
            up = Input::GetR2();
            down = Input::GetL2();
            if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross)) speed *= 3.0f;
            if (glm::length(ls) + glm::length(rs) + up + down > 0.01f) touched = true;
        }
        if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT))   speed *= 3.0f;
        if (Input::IsKeyHeld(GLFW_KEY_W))     { ls.y -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_S))     { ls.y += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_A))     { ls.x -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_D))     { ls.x += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_LEFT))  { rs.x -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) { rs.x += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_UP))    { rs.y -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_DOWN))  { rs.y += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_SPACE))        { up   += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL)) { down += 1.0f; touched = true; }

        if (!touched) { last_cam_pos_ = eng_pos; return; }

        cam_yaw_   += rs.x * look_sensitivity_ * dt;
        cam_pitch_ -= rs.y * look_sensitivity_ * dt;
        if (cam_pitch_ >  89.0f) cam_pitch_ =  89.0f;
        if (cam_pitch_ < -89.0f) cam_pitch_ = -89.0f;

        glm::vec3 fwd;
        fwd.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
        fwd.y = std::sin(glm::radians(cam_pitch_));
        fwd.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
        fwd = glm::normalize(fwd);
        glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));

        glm::vec3 pos = eng_pos;
        pos += fwd * (-ls.y) * speed * dt;
        pos += right *  ls.x   * speed * dt;
        pos.y += (up - down) * speed * dt;

        SetCameraPosition(pos);
        SetCameraTarget(pos + fwd);
        last_cam_pos_ = pos;
    }
};


// ---------------------------------------------
// GrassPoolsDemo: standalone showcase of shadows + grass ground + water + pools.
// No AI driving — everything set up in C++ from OnInitialize.
// ---------------------------------------------
class GrassPoolsDemo : public Engine {
    float cam_yaw_   = 215.0f;
    float cam_pitch_ = -15.0f;
    float cam_speed_ = 15.0f;
    float look_sensitivity_ = 80.0f;
    glm::vec3 last_cam_pos_{0};

public:
    GrassPoolsDemo()
      : Engine(1920, 1080, "ZoomEngine - Grass / Pools / Shadows") {}

    bool OnInitialize() override {
        std::mt19937 rng(7);
        auto rand = [&](float lo, float hi) {
            std::uniform_real_distribution<float> d(lo, hi);
            return d(rng);
        };

        // Golden-hour sun via the engine's existing state.
        processAgentCommand(json::parse(R"({
            "action":"sun",
            "direction":[-0.55, 0.32, -0.20],
            "color":[1.55, 1.15, 0.78],
            "zenith":[0.20, 0.40, 0.72],
            "horizon":[0.95, 0.70, 0.50],
            "sky":[0.80, 0.62, 0.42],
            "ground":[0.08, 0.07, 0.05],
            "shadow_extent":80
        })"));

        // Grass ground — textured
        auto grass = CreateGameObject(
            CreatePlane(110.0f, 110.0f, glm::vec3(0.95f, 0.95f, 0.9f)),
            glm::vec3(0, 0, 0));
        grass->SetTexture(TextureCache::Get("grass"));
        grass->SetUVScale(glm::vec2(0.25f));
        AddGameObject(grass);

        // Ocean around the grass: mesh is large, level just below the grass
        // so the horizon line reads as a shore.
        processAgentCommand(json::parse(R"({
            "action":"water",
            "enabled":true,
            "level":-0.25,
            "size":260,
            "amplitude":0.35,
            "wavelength":11,
            "speed":1.0,
            "shallow":[0.30, 0.58, 0.62],
            "deep":[0.02, 0.11, 0.20],
            "resolution":160
        })"));

        // A light breeze so the ocean moves but the scene stays calm.
        processAgentCommand(json::parse(R"({
            "action":"weather",
            "wind_dir":[1, 0, 0.4],
            "wind_speed":1.0,
            "storminess":0.1
        })"));

        // Tiny pools — small translucent teal planes sitting on the grass.
        // Slight Y offset prevents z-fighting; size <2m suggests puddles.
        struct Pool { glm::vec3 pos; float size; };
        const std::vector<Pool> pools = {
            { glm::vec3( 6.0f, 0.02f,  3.0f), 2.4f },
            { glm::vec3(-4.0f, 0.02f, -7.0f), 1.6f },
            { glm::vec3( 1.5f, 0.02f, 10.0f), 1.9f },
            { glm::vec3(-9.0f, 0.02f,  5.5f), 1.2f },
        };
        for (const auto& p : pools) {
            // Use a color that reads as "shallow reflective water"
            auto pool = CreateGameObject(
                CreatePlane(p.size, p.size * 0.7f, glm::vec3(0.35f, 0.58f, 0.65f)),
                p.pos);
            // Slight random yaw to stop them looking like a grid
            pool->SetRotation(glm::vec3(0, rand(0.0f, 360.0f), 0));
            AddGameObject(pool);
        }

        // Shadow casters: 3 standing "monoliths" and a scatter of boulders.
        auto monolith = [&](glm::vec3 pos, glm::vec3 scale, glm::vec3 color, float yaw) {
            auto obj = CreateGameObject(CreateCube(color), pos);
            obj->SetScale(scale);
            obj->SetRotation(glm::vec3(0, yaw, 0));
            AddGameObject(obj);
        };
        GLuint rock_tex = TextureCache::Get("rock");
        GLuint wood_tex = TextureCache::Get("wood");
        auto monolith2 = [&](glm::vec3 pos, glm::vec3 scale, float yaw) {
            auto obj = CreateGameObject(CreateCube(glm::vec3(0.9f, 0.88f, 0.85f)), pos);
            obj->SetScale(scale);
            obj->SetRotation(glm::vec3(0, yaw, 0));
            obj->SetTexture(rock_tex);
            AddGameObject(obj);
        };
        monolith2(glm::vec3( 0.0f, 2.5f,  0.0f), glm::vec3(1.2f, 5.0f, 1.2f), 12.0f);
        monolith2(glm::vec3( 4.5f, 1.8f, -6.0f), glm::vec3(0.9f, 3.6f, 0.9f), -22.0f);
        monolith2(glm::vec3(-7.0f, 2.1f, -2.0f), glm::vec3(1.1f, 4.2f, 1.1f),  48.0f);
        monolith2(glm::vec3( 8.5f, 1.4f,  7.5f), glm::vec3(0.8f, 2.8f, 0.8f), -8.0f);
        (void)monolith; (void)wood_tex;

        auto boulder = [&](glm::vec3 pos, float s, glm::vec3 color) {
            (void)color;
            auto obj = CreateGameObject(CreateSphere(24, glm::vec3(0.85f, 0.82f, 0.78f)), pos);
            obj->SetScale(glm::vec3(s, s * 0.75f, s));
            obj->SetTexture(rock_tex);
            AddGameObject(obj);
        };
        boulder(glm::vec3(-3.0f, 0.6f,  4.0f), 1.2f, glm::vec3(0.45f, 0.42f, 0.38f));
        boulder(glm::vec3( 5.0f, 0.45f, -2.0f), 0.9f, glm::vec3(0.42f, 0.40f, 0.36f));
        boulder(glm::vec3( 2.5f, 0.7f, 12.0f), 1.4f, glm::vec3(0.50f, 0.46f, 0.42f));
        boulder(glm::vec3(-10.0f, 0.5f, 9.0f), 1.0f, glm::vec3(0.40f, 0.38f, 0.35f));
        boulder(glm::vec3(12.0f, 0.4f, -4.0f), 0.8f, glm::vec3(0.48f, 0.45f, 0.40f));

        // Some trees: trunk cylinder (wood texture) + cone foliage.
        auto tree = [&](glm::vec3 at, float s) {
            auto trunk = CreateGameObject(
                CreateCylinder(0.25f * s, 2.4f * s, 10, glm::vec3(0.9f, 0.85f, 0.8f)),
                at + glm::vec3(0, 1.2f * s, 0));
            trunk->SetTexture(TextureCache::Get("wood"));
            AddGameObject(trunk);
            auto canopy = CreateGameObject(
                CreateCone(1.3f * s, 2.6f * s, 12, glm::vec3(0.15f, 0.40f, 0.18f)),
                at + glm::vec3(0, 3.6f * s, 0));
            AddGameObject(canopy);
        };
        tree(glm::vec3(-14.0f, 0, -8.0f), 1.3f);
        tree(glm::vec3( 15.0f, 0, -10.0f), 1.1f);
        tree(glm::vec3(-16.0f, 0,  12.0f), 1.4f);
        tree(glm::vec3( 18.0f, 0,  4.0f), 1.0f);
        tree(glm::vec3(  9.0f, 0, -14.0f), 1.2f);

        // Camera setup: three-quarter view that catches the long shadows.
        glm::vec3 eye(18.0f, 7.0f, 22.0f);
        SetCameraPosition(eye);
        SetCameraTarget(glm::vec3(0, 1.5f, 0));
        last_cam_pos_ = eye;

        std::cout << "Grass/Pools/Shadows demo ready.\n"
                  << "Camera: WASD = move, arrows = look, Space/LCtrl = up/down,"
                     " LShift = sprint.\n";
        return true;
    }

    void OnUpdate(float dt) override {
        // Same free-fly camera as GodSandbox.
        glm::vec3 eng_pos = GetCameraPosition();
        if (glm::length(eng_pos - last_cam_pos_) > 0.0001f) {
            glm::vec3 fwd = GetCameraTarget() - eng_pos;
            if (glm::length(fwd) > 0.0001f) {
                fwd = glm::normalize(fwd);
                cam_pitch_ = glm::degrees(std::asin(fwd.y));
                cam_yaw_   = glm::degrees(std::atan2(fwd.z, fwd.x));
            }
        }

        glm::vec2 ls{0}, rs{0};
        float up = 0, down = 0, speed = cam_speed_;
        bool touched = false;

        if (Input::IsGamepadConnected()) {
            ls = Input::GetLeftStick();
            rs = Input::GetRightStick();
            up = Input::GetR2();
            down = Input::GetL2();
            if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross)) speed *= 3.0f;
            if (glm::length(ls) + glm::length(rs) + up + down > 0.01f) touched = true;
        }
        if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT))   speed *= 3.0f;
        if (Input::IsKeyHeld(GLFW_KEY_W))     { ls.y -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_S))     { ls.y += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_A))     { ls.x -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_D))     { ls.x += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_LEFT))  { rs.x -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) { rs.x += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_UP))    { rs.y -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_DOWN))  { rs.y += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_SPACE))        { up   += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL)) { down += 1.0f; touched = true; }

        if (!touched) { last_cam_pos_ = eng_pos; return; }

        cam_yaw_   += rs.x * look_sensitivity_ * dt;
        cam_pitch_ -= rs.y * look_sensitivity_ * dt;
        if (cam_pitch_ >  89.0f) cam_pitch_ =  89.0f;
        if (cam_pitch_ < -89.0f) cam_pitch_ = -89.0f;

        glm::vec3 fwd;
        fwd.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
        fwd.y = std::sin(glm::radians(cam_pitch_));
        fwd.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
        fwd = glm::normalize(fwd);
        glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));

        glm::vec3 pos = eng_pos;
        pos += fwd * (-ls.y) * speed * dt;
        pos += right *  ls.x   * speed * dt;
        pos.y += (up - down) * speed * dt;

        SetCameraPosition(pos);
        SetCameraTarget(pos + fwd);
        last_cam_pos_ = pos;
    }
};


// ---------------------------------------------
// GodSandbox: blank canvas for the AI god to play in, plus free-fly controls
// Ground plane + grid + gentle initial camera. The AI god drives the scene
// via HTTP; the human drives the camera via gamepad or keyboard. Claude's
// camera commands stay authoritative until the user touches an input.
// ---------------------------------------------
class GodSandbox : public Engine {
    float cam_yaw_   = 225.0f;  // looking toward origin from [15,10,15]
    float cam_pitch_ = -25.0f;
    float cam_speed_ = 15.0f;
    float look_sensitivity_ = 80.0f;
    glm::vec3 last_cam_pos_{15, 10, 15};
    std::string load_path_;  // empty = blank sandbox

public:
    GodSandbox()
      : Engine(1920, 1080, "ZoomEngine - God Sandbox (run ai_god.py)") {}

    void SetLoadPath(const std::string& p) { load_path_ = p; }

    bool OnInitialize() override {
        auto grid = CreateGameObject(CreateGrid(30, 1.0f, glm::vec3(0.25f, 0.25f, 0.35f)),
                                     glm::vec3(0, 0, 0));
        AddGameObject(grid);

        auto plane = CreateGameObject(CreatePlane(40.0f, 40.0f, glm::vec3(0.08f, 0.09f, 0.14f)),
                                      glm::vec3(0, -0.01f, 0));
        AddGameObject(plane);

        SetCameraPosition(glm::vec3(15, 10, 15));
        SetCameraTarget(glm::vec3(0, 0, 0));
        last_cam_pos_ = GetCameraPosition();

        if (!load_path_.empty()) {
            std::string result = loadWorldFromFile(load_path_);
            std::cout << "Loaded world from " << load_path_
                      << " -> " << result << std::endl;
        }

        std::cout << "God Sandbox ready. In another terminal, run:" << std::endl;
        std::cout << "  python ai_god.py" << std::endl;
        std::cout << "Agent API listening on http://localhost:9090/api" << std::endl;
        std::cout << "Camera: WASD move, arrows/right-stick look, Space/R2 up,"
                  << " LShift/L2 down, LCtrl/Cross sprint." << std::endl;
        return true;
    }

    void OnUpdate(float dt) override {
        // Resync local yaw/pitch whenever the AI (or any other source) has
        // moved the camera out from under us since the last frame.
        glm::vec3 eng_pos = GetCameraPosition();
        if (glm::length(eng_pos - last_cam_pos_) > 0.0001f) {
            glm::vec3 fwd = GetCameraTarget() - eng_pos;
            if (glm::length(fwd) > 0.0001f) {
                fwd = glm::normalize(fwd);
                cam_pitch_ = glm::degrees(std::asin(fwd.y));
                cam_yaw_   = glm::degrees(std::atan2(fwd.z, fwd.x));
            }
        }

        // Read inputs
        glm::vec2 ls{0.0f}, rs{0.0f};
        float up = 0.0f, down = 0.0f;
        float speed = cam_speed_;
        bool touched = false;

        if (Input::IsGamepadConnected()) {
            ls = Input::GetLeftStick();
            rs = Input::GetRightStick();
            up   = Input::GetR2();
            down = Input::GetL2();
            if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross))
                speed *= 3.0f;
            if (glm::length(ls) + glm::length(rs) + up + down > 0.01f)
                touched = true;
        }

        if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT))   speed *= 3.0f;
        if (Input::IsKeyHeld(GLFW_KEY_W))     { ls.y -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_S))     { ls.y += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_A))     { ls.x -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_D))     { ls.x += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_LEFT))  { rs.x -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) { rs.x += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_UP))    { rs.y -= 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_DOWN))  { rs.y += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_SPACE))          { up   += 1.0f; touched = true; }
        if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL))   { down += 1.0f; touched = true; }

        if (!touched) {
            last_cam_pos_ = eng_pos;
            return;  // leave whatever the AI god (or initial state) set alone
        }

        cam_yaw_   += rs.x * look_sensitivity_ * dt;
        cam_pitch_ -= rs.y * look_sensitivity_ * dt;
        if (cam_pitch_ >  89.0f) cam_pitch_ =  89.0f;
        if (cam_pitch_ < -89.0f) cam_pitch_ = -89.0f;

        glm::vec3 forward;
        forward.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
        forward.y = std::sin(glm::radians(cam_pitch_));
        forward.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
        forward = glm::normalize(forward);
        glm::vec3 right = glm::normalize(glm::cross(forward, glm::vec3(0, 1, 0)));

        glm::vec3 pos = eng_pos;
        pos += forward * (-ls.y) * speed * dt;
        pos += right   *  ls.x   * speed * dt;
        pos.y += (up - down) * speed * dt;

        SetCameraPosition(pos);
        SetCameraTarget(pos + forward);
        last_cam_pos_ = pos;
    }
};


// ---------------------------------------------
// Launch a scene by action string
// ---------------------------------------------
static void runScene(const std::string& mode) {
    if (mode == "stress") {
        std::cout << "Running Stress Test..." << std::endl;
        StressTest demo;
        demo.Run();
    }
    else if (mode == "monkeygrid") {
        std::cout << "Starting Monkey Grid Demo..." << std::endl;
        MonkeyGridDemo demo;
        demo.Run();
    }
    else if (mode == "edit") {
        std::cout << "Starting Zoom Level Editor..." << std::endl;
        LevelEditor edit;
        edit.Run();
    }
    else if (mode == "landscape") {
        std::cout << "Starting Landscape Demo..." << std::endl;
        LandscapeDemo demo;
        demo.Run();
    }
    else if (mode == "monkey") {
        SpinningMonkey demo;
        demo.Run();
    }
    else if (mode == "god") {
        std::cout << "Starting God Sandbox..." << std::endl;
        GodSandbox demo;
        demo.Run();
    }
    else if (mode == "grass") {
        std::cout << "Starting Grass/Pools/Shadows Demo..." << std::endl;
        GrassPoolsDemo demo;
        demo.Run();
    }
    else if (mode == "sunset") {
        std::cout << "Starting Sunset Forest (HDR) Demo..." << std::endl;
        SunsetForestDemo demo;
        demo.Run();
    }
    else if (mode == "sanctuary" || mode == "full") {
        std::cout << "Starting Sanctuary (Full Engine Showcase)..." << std::endl;
        SanctuaryDemo demo;
        demo.Run();
    }
    else if (mode == "dam") {
        std::cout << "Starting Dam (Multi-water) Demo..." << std::endl;
        DamDemo demo;
        demo.Run();
    }
    else if (mode == "waterfall") {
        std::cout << "Starting Waterfall (Flow Physics) Demo..." << std::endl;
        WaterfallDemo demo;
        demo.Run();
    }
    else if (mode.rfind("loadworld:", 0) == 0) {
        std::string path = mode.substr(10);
        std::cout << "Loading world: " << path << std::endl;
        GodSandbox demo;
        demo.SetLoadPath(path);
        demo.Run();
    }
    else {
        std::cout << "Starting Scene Demo..." << std::endl;
        SceneDemo demo;
        demo.Run();
    }
}

// ---------------------------------------------
// main: menu (default) or direct scene by arg
// ---------------------------------------------
int main(int argc, char** argv) {
    try {
        // Direct launch: ./ModernEngine <scene>
        if (argc > 1) {
            std::string mode = argv[1];
            if (mode != "menu") {
                runScene(mode);
                return 0;
            }
        }

        // Menu mode: load JSON config and show interactive menu
        // Try assets/menu.json, then fall back to built-in default
        json::Value menuConfig;
        try {
            menuConfig = json::parseFile("../assets/menu.json");
            std::cout << "Loaded menu from assets/menu.json" << std::endl;
        } catch (...) {
            try {
                menuConfig = json::parseFile("assets/menu.json");
                std::cout << "Loaded menu from assets/menu.json" << std::endl;
            } catch (...) {
                // Built-in fallback
                menuConfig = json::parse(R"({
                    "title": "ZOOMENGINE",
                    "background_color": [0.04, 0.04, 0.1],
                    "title_color": [1.0, 0.85, 0.2],
                    "item_color": [0.65, 0.65, 0.7],
                    "selected_color": [1.0, 1.0, 0.3],
                    "items": [
                        { "label": "Landscape",       "action": "landscape" },
                        { "label": "Scene Demo",      "action": "demo" },
                        { "label": "Stress Test",     "action": "stress" },
                        { "label": "Spinning Monkey", "action": "monkey" },
                        { "label": "Monkey Grid",     "action": "monkeygrid" },
                        { "label": "God Sandbox",     "action": "god" },
                        { "label": "Level Editor",    "action": "edit" },
                        { "label": "Quit",            "action": "quit" }
                    ]
                })");
                std::cout << "Using built-in menu config" << std::endl;
            }
        }

        // Inject one menu entry per worlds/*.json file, before the Quit item.
        {
            namespace fs = std::filesystem;
            std::error_code ec;
            std::vector<std::string> worldFiles;
            for (const char* dir : {"worlds", "../worlds"}) {
                if (!fs::exists(dir, ec)) continue;
                for (auto& e : fs::directory_iterator(dir, ec)) {
                    if (e.path().extension() == ".json")
                        worldFiles.push_back(e.path().string());
                }
                break;
            }
            if (!worldFiles.empty() && menuConfig.has("items")
                && menuConfig["items"].is_array()) {
                json::Array items = menuConfig["items"].as_array();
                json::Array rebuilt;
                bool inserted = false;
                for (auto& it : items) {
                    if (!inserted && it.is_object()
                        && it.get_string("action") == "quit") {
                        for (auto& path : worldFiles) {
                            fs::path p(path);
                            json::Object entry;
                            entry["label"]  = json::Value(
                                std::string("World: ") + p.stem().string());
                            entry["action"] = json::Value(
                                std::string("loadworld:") + path);
                            rebuilt.push_back(json::Value(entry));
                        }
                        inserted = true;
                    }
                    rebuilt.push_back(it);
                }
                if (!inserted) {
                    for (auto& path : worldFiles) {
                        fs::path p(path);
                        json::Object entry;
                        entry["label"]  = json::Value(
                            std::string("World: ") + p.stem().string());
                        entry["action"] = json::Value(
                            std::string("loadworld:") + path);
                        rebuilt.push_back(json::Value(entry));
                    }
                }
                json::Object newCfg = menuConfig.as_object();
                newCfg["items"] = json::Value(rebuilt);
                menuConfig = json::Value(newCfg);
                std::cout << "Loaded " << worldFiles.size()
                          << " saved world(s) into menu" << std::endl;
            }
        }

        // Menu loop: show menu, launch scene, return to menu
        while (true) {
            std::string action;
            {
                MainMenu menu(menuConfig, [&](const std::string& a) { action = a; });
                menu.Run();
                action = menu.getSelectedAction();
            }

            if (action.empty() || action == "quit")
                break;

            // GLFW needs re-init after the menu window closes
            glfwTerminate();

            runScene(action);

            // After scene exits, GLFW terminates — loop back to re-show menu
            glfwTerminate();
        }

        return 0;
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
        return -1;
    }
}
