#include "Engine.h"
#include "Input.h"
#include "Json.h"
#include "MainMenu.h"
#include "TextureCache.h"
#include "Hud.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));
    }
};

// ---------------------------------------------
// OdysseyDemo — large explorable world with verticality and a playable
// character visible in first person (arm + hand), plus a 6-slot item
// hotbar. Each item throws a different kind of energy ball.
// Zones radiate from a central spawn:
//   N: Forest valley (lowest)
//   E: Sakura plateau (+8 elevation, reached via ramp)
//   S: Desert basin (warm, flat)
//   W: Tech cliffs (+14 elevation, glowing spires)
//   Center: lava/volcano pit with an emissive glow column
// ---------------------------------------------
class OdysseyDemo : public Engine {
    // Camera state
    float cam_yaw_   = 180.0f;
    float cam_pitch_ = -10.0f;
    float speed_     = 18.0f;
    float look_sens_ = 90.0f;
    glm::vec3 last_cam_pos_{0};

    // First-person arm
    std::shared_ptr<GameObject> arm_upper_, arm_lower_, hand_;
    float throw_anim_ = 0.0f;   // 0..1 animation progress
    bool  arm_ready_ = false;

    // Hotbar items
    struct Item {
        const char* label;
        glm::vec3   color;
        glm::vec3   emissive;
        float       mass;
        float       velocity;
        float       radius;
    };
    Item items_[6] = {
        {"STEEL",  glm::vec3(0.85f, 0.88f, 0.95f), glm::vec3(0.0f),               2.5f, 55.0f, 0.35f},
        {"FIRE",   glm::vec3(1.40f, 0.35f, 0.10f), glm::vec3(4.5f, 1.2f, 0.3f),   1.2f, 55.0f, 0.38f},
        {"ICE",    glm::vec3(0.65f, 0.92f, 1.10f), glm::vec3(0.4f, 0.9f, 1.5f),   1.8f, 55.0f, 0.35f},
        {"PLASMA", glm::vec3(1.30f, 0.40f, 1.30f), glm::vec3(2.2f, 0.5f, 3.5f),   0.8f, 70.0f, 0.32f},
        {"VOID",   glm::vec3(0.12f, 0.08f, 0.18f), glm::vec3(0.0f),               5.0f, 45.0f, 0.45f},
        {"GOLD",   glm::vec3(1.20f, 0.95f, 0.45f), glm::vec3(0.6f, 0.4f, 0.0f),   3.0f, 60.0f, 0.38f},
    };
    int  selected_ = 0;
    bool q_prev_ = false, e_prev_ = false;
    bool ctrl_cycle_cooldown_ = false;

    // Shooting
    int    shot_counter_ = 0;
    float  shot_cooldown_ = 0.0f;
    std::deque<std::string> active_shots_;
    static constexpr int kMaxShots = 80;
    std::mt19937 rng_{66612};

public:
    OdysseyDemo()
      : Engine(1920, 1080, "ZoomEngine - Odyssey (expansive)") {}

    bool OnInitialize() override {
        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); };

        // Neutral-daylight base; zone tint comes from textures
        processAgentCommand(json::parse(R"({
            "action":"sun",
            "direction":[-0.40, 0.70, -0.25],
            "color":[1.95, 1.80, 1.55],
            "zenith":[0.22, 0.40, 0.68],
            "horizon":[0.85, 0.78, 0.70],
            "sky":[0.62, 0.72, 0.78],
            "ground":[0.08, 0.09, 0.10],
            "shadow_extent":150,
            "exposure":0.95,
            "bloom_threshold":1.15,
            "bloom_intensity":1.2,
            "fog_density":0.0035
        })"));
        processAgentCommand(json::parse(R"({"action":"water","enabled":false})"));

        GLuint grass_tex = TextureCache::Get("grass");
        GLuint rock_tex  = TextureCache::Get("rock");
        GLuint dirt_t    = TextureCache::Get("dirt");
        GLuint marble_t  = TextureCache::Get("marble");
        GLuint concrete  = TextureCache::Get("concrete");
        GLuint wood_tex  = TextureCache::Get("wood");
        GLuint grid_t    = TextureCache::Get("grid");
        GLuint noise_t   = TextureCache::Get("noise");

        // --- Forest valley (N, y≈0, default plane) ---
        auto forest = CreateGameObject(
            CreatePlane(240.0f, 120.0f, glm::vec3(0.72f, 0.85f, 0.68f)),
            glm::vec3(0, 0, 70.0f));
        forest->SetTexture(grass_tex); forest->SetNormalMap(noise_t);
        forest->SetUVScale(glm::vec2(0.2f)); forest->SetRoughness(0.95f);
        AddGameObject(forest);

        // Spawn plaza (center)
        auto spawn = CreateGameObject(
            CreatePlane(40.0f, 40.0f, glm::vec3(0.92f, 0.92f, 0.88f)),
            glm::vec3(0, 0.01f, 0));
        spawn->SetTexture(marble_t); spawn->SetNormalMap(noise_t);
        spawn->SetUVScale(glm::vec2(0.2f)); spawn->SetRoughness(0.5f);
        AddGameObject(spawn);

        // --- Sakura plateau (E, y=8) ---
        // Stepped ramp up from center to plateau
        for (int step = 1; step <= 4; ++step) {
            auto s = CreateGameObject(
                CreateCube(glm::vec3(0.95f, 0.85f, 0.82f)),
                glm::vec3(20.0f + step * 3.5f, step * 1.0f, 0));
            s->SetScale(glm::vec3(4.0f, 2.0f, 18.0f));
            s->SetTexture(marble_t); s->SetNormalMap(noise_t);
            s->SetRoughness(0.55f);
            AddGameObject(s);
        }
        auto sakura_ground = CreateGameObject(
            CreatePlane(100.0f, 140.0f, glm::vec3(1.0f, 0.82f, 0.85f)),
            glm::vec3(90.0f, 8.0f, 0));
        sakura_ground->SetTexture(grass_tex); sakura_ground->SetNormalMap(noise_t);
        sakura_ground->SetUVScale(glm::vec2(0.25f)); sakura_ground->SetRoughness(0.92f);
        AddGameObject(sakura_ground);
        // Pink plateau sides (visible from below)
        for (int i = 0; i < 4; ++i) {
            auto side = CreateGameObject(
                CreateCube(glm::vec3(0.86f, 0.78f, 0.82f)),
                glm::vec3(90.0f + (i - 1.5f) * 30.0f, 4.0f, 70.0f));
            side->SetScale(glm::vec3(30.0f, 8.0f, 4.0f));
            side->SetTexture(concrete); side->SetNormalMap(noise_t);
            side->SetRoughness(0.85f);
            AddGameObject(side);
        }

        // Cherry trees on plateau
        auto cherry = [&](glm::vec3 at, float s) {
            auto trunk = CreateGameObject(
                CreateCylinder(0.24f * s, 2.6f * s, 10,
                               glm::vec3(0.48f, 0.32f, 0.22f)),
                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_t);
            trunk->SetRoughness(0.85f);
            AddGameObject(trunk);
            for (int k = 0; k < 3; ++k) {
                float r = 1.6f * s * (1.0f - k * 0.20f);
                auto canopy = CreateGameObject(
                    CreateSphere(24, glm::vec3(1.25f, 0.55f, 0.82f)),
                    at + glm::vec3(jit(0.25f), 2.8f * s + k * 0.9f * s, jit(0.25f)));
                canopy->SetScale(glm::vec3(r, r * 0.75f, r));
                canopy->SetEmissive(glm::vec3(0.25f, 0.10f, 0.18f));
                canopy->SetRoughness(0.85f);
                AddGameObject(canopy);
            }
        };
        for (int i = 0; i < 35; ++i) {
            float x = 90.0f + rnd(-45.0f, 45.0f);
            float z = rnd(-60.0f, 60.0f);
            cherry(glm::vec3(x, 8.0f, z), rnd(0.9f, 1.4f));
        }
        // Floating petals
        for (int i = 0; i < 120; ++i) {
            float x = 90.0f + rnd(-45.0f, 45.0f);
            float z = rnd(-60.0f, 60.0f);
            float y = 8.0f + rnd(2.0f, 9.0f);
            auto petal = CreateGameObject(
                CreateSphere(6, glm::vec3(1.5f, 0.6f, 0.85f)),
                glm::vec3(x, y, z));
            petal->SetScale(glm::vec3(0.08f, 0.04f, 0.08f));
            petal->SetEmissive(glm::vec3(0.50f, 0.15f, 0.25f));
            AddGameObject(petal);
        }

        // --- Desert basin (S, flat at y=0) ---
        auto desert_g = CreateGameObject(
            CreatePlane(160.0f, 120.0f, glm::vec3(1.05f, 0.88f, 0.60f)),
            glm::vec3(0, 0.0f, -75.0f));
        desert_g->SetTexture(dirt_t); desert_g->SetNormalMap(noise_t);
        desert_g->SetUVScale(glm::vec2(0.3f)); desert_g->SetRoughness(0.98f);
        AddGameObject(desert_g);
        // Step pyramid
        for (int lvl = 0; lvl < 6; ++lvl) {
            float s = 12.0f - lvl * 1.8f;
            auto p = CreateGameObject(
                CreateCube(glm::vec3(0.92f, 0.76f, 0.52f)),
                glm::vec3(-30.0f, 1.1f + lvl * 2.2f, -90.0f));
            p->SetScale(glm::vec3(s, 2.2f, s));
            p->SetTexture(dirt_t); p->SetNormalMap(noise_t);
            p->SetRoughness(0.95f);
            AddGameObject(p);
        }
        // Obelisk
        {
            auto obel = CreateGameObject(
                CreateCube(glm::vec3(0.95f, 0.85f, 0.55f)),
                glm::vec3(30.0f, 6.0f, -90.0f));
            obel->SetScale(glm::vec3(1.8f, 12.0f, 1.8f));
            obel->SetTexture(marble_t); obel->SetNormalMap(noise_t);
            obel->SetRoughness(0.45f);
            obel->SetRotation(glm::vec3(0, 20.0f, 0));
            AddGameObject(obel);
        }
        // Cacti scattered
        for (int i = 0; i < 24; ++i) {
            float x = rnd(-70.0f, 70.0f);
            float z = rnd(-120.0f, -30.0f);
            float h = rnd(2.0f, 3.5f);
            auto c = CreateGameObject(
                CreateCylinder(0.4f, h, 8, glm::vec3(0.30f, 0.55f, 0.32f)),
                glm::vec3(x, h * 0.5f, z));
            c->SetRoughness(0.8f); c->SetNormalMap(noise_t);
            AddGameObject(c);
        }
        // Sand dunes (flattened spheres)
        for (int i = 0; i < 10; ++i) {
            float x = rnd(-70.0f, 70.0f);
            float z = rnd(-120.0f, -30.0f);
            auto d = CreateGameObject(
                CreateSphere(18, glm::vec3(1.0f, 0.82f, 0.55f)),
                glm::vec3(x, 0, z));
            d->SetScale(glm::vec3(rnd(4, 7), rnd(1, 2), rnd(3, 6)));
            d->SetTexture(dirt_t); d->SetNormalMap(noise_t);
            d->SetRoughness(0.95f);
            AddGameObject(d);
        }

        // --- Tech cliffs (W, high at y=14) ---
        // Vertical cliff face approaching from the plaza
        for (int i = 0; i < 6; ++i) {
            auto block = CreateGameObject(
                CreateCube(glm::vec3(0.75f, 0.78f, 0.84f)),
                glm::vec3(-30.0f - i * 3.0f, (i + 1) * 1.2f, 0));
            block->SetScale(glm::vec3(3.0f, 2.4f * (i + 1), 18.0f));
            block->SetTexture(concrete); block->SetNormalMap(noise_t);
            block->SetRoughness(0.75f);
            AddGameObject(block);
        }
        auto tech_top = CreateGameObject(
            CreatePlane(100.0f, 140.0f, glm::vec3(0.30f, 0.34f, 0.40f)),
            glm::vec3(-90.0f, 14.0f, 0));
        tech_top->SetTexture(grid_t); tech_top->SetNormalMap(noise_t);
        tech_top->SetUVScale(glm::vec2(0.5f));
        tech_top->SetMetallic(0.6f);
        tech_top->SetRoughness(0.3f);
        AddGameObject(tech_top);
        // Chrome pylons on top
        for (int i = 0; i < 12; ++i) {
            float a = (float)i / 12.0f * 2 * (float)M_PI;
            float R = 22.0f;
            float x = -90.0f + std::cos(a) * R;
            float z = std::sin(a) * R;
            auto p = CreateGameObject(
                CreateCube(glm::vec3(0.96f)),
                glm::vec3(x, 18.0f, z));
            p->SetScale(glm::vec3(0.8f, 8.0f, 0.8f));
            p->SetMetallic(1.0f); p->SetRoughness(0.15f);
            AddGameObject(p);
        }
        // Giant glowing spire at center of tech
        {
            auto spire = CreateGameObject(
                CreateCube(glm::vec3(0.95f)),
                glm::vec3(-90.0f, 24.0f, 0));
            spire->SetScale(glm::vec3(1.2f, 20.0f, 1.2f));
            spire->SetMetallic(1.0f); spire->SetRoughness(0.1f);
            spire->SetEmissive(glm::vec3(1.5f, 0.3f, 3.5f));
            AddGameObject(spire);
        }
        // Cyan neon bars scattered
        for (int i = 0; i < 16; ++i) {
            float x = -90.0f + rnd(-40.0f, 40.0f);
            float z = rnd(-55.0f, 55.0f);
            auto bar = CreateGameObject(
                CreateCube(glm::vec3(1.0f)),
                glm::vec3(x, 14.2f, z));
            bar->SetScale(glm::vec3(rnd(1.2f, 3.0f), 0.08f, 0.25f));
            bar->SetRotation(glm::vec3(0, jit(180.0f), 0));
            bar->SetEmissive(glm::vec3(0.15f, 2.8f, 3.2f));
            AddGameObject(bar);
        }

        // --- Central volcano/lava pit (height column + glowing ground) ---
        {
            auto mouth = CreateGameObject(
                CreateCone(5.0f, 3.0f, 16, glm::vec3(0.30f, 0.22f, 0.18f)),
                glm::vec3(0, 1.5f, 0));
            mouth->SetTexture(rock_tex); mouth->SetNormalMap(noise_t);
            mouth->SetRoughness(0.9f);
            // Invert so it's a bowl? Easier — just use as volcano base
            AddGameObject(mouth);
            // Lava glow (flat disk inside)
            auto lava = CreateGameObject(
                CreateCylinder(3.0f, 0.15f, 32, glm::vec3(1.0f)),
                glm::vec3(0, 2.2f, 0));
            lava->SetEmissive(glm::vec3(5.0f, 1.5f, 0.2f));
            AddGameObject(lava);
            // Glow column rising
            for (int i = 0; i < 5; ++i) {
                auto smoke = CreateGameObject(
                    CreateSphere(12, glm::vec3(1.0f)),
                    glm::vec3(jit(0.5f), 3.0f + i * 2.0f, jit(0.5f)));
                float s = 1.0f + i * 0.3f;
                smoke->SetScale(glm::vec3(s, s, s));
                smoke->SetEmissive(glm::vec3(2.5f - i * 0.35f, 0.8f, 0.15f));
                AddGameObject(smoke);
            }
        }

        // --- Forest trees ---
        auto tree = [&](glm::vec3 at, float s) {
            auto trunk = CreateGameObject(
                CreateCylinder(0.25f * s, 2.8f * s, 10,
                               glm::vec3(0.85f, 0.78f, 0.65f)),
                at + glm::vec3(0, 1.4f * s, 0));
            trunk->SetRotation(glm::vec3(jit(4), jit(360), jit(4)));
            trunk->SetTexture(wood_tex); trunk->SetNormalMap(noise_t);
            trunk->SetRoughness(0.85f);
            AddGameObject(trunk);
            glm::vec3 cc(rnd(0.10f, 0.22f), rnd(0.38f, 0.55f), rnd(0.12f, 0.22f));
            auto cone = CreateGameObject(
                CreateCone(1.4f * s, 3.2f * 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 < 55; ++i) {
            float x = rnd(-70.0f, 70.0f);
            float z = 30.0f + rnd(10.0f, 120.0f);
            tree(glm::vec3(x, 0, z), rnd(0.8f, 1.4f));
        }

        // --- Central plaza pylons (so you orient from spawn) ---
        for (int i = 0; i < 4; ++i) {
            float a = (float)i / 4.0f * 2 * (float)M_PI + 0.25f;
            float R = 14.0f;
            auto p = CreateGameObject(
                CreateCylinder(0.4f, 6.0f, 12, glm::vec3(0.90f)),
                glm::vec3(std::cos(a) * R, 3.0f, std::sin(a) * R));
            p->SetMetallic(1.0f); p->SetRoughness(0.25f);
            p->SetEmissive(glm::vec3(1.2f, 1.0f, 0.6f));
            AddGameObject(p);
        }

        // --- Outer mountain ring ---
        for (int i = 0; i < 32; ++i) {
            float a = (float)i / 32.0f * 2 * (float)M_PI;
            float R = 220.0f;
            float sc = 1.0f + ((float)((i * 37) % 100) / 100.0f) * 0.5f;
            float h = 32.0f * sc;
            auto m = CreateGameObject(
                CreateCone(15.0f * sc, h, 10, glm::vec3(0.22f, 0.20f, 0.24f)),
                glm::vec3(std::cos(a) * R + jit(5.0f), h * 0.5f, std::sin(a) * R + jit(5.0f)));
            m->SetRoughness(0.92f);
            AddGameObject(m);
        }

        // --- First-person arm ---
        BuildArm();

        // Camera starts facing north across forest
        glm::vec3 eye(0, 3.0f, -3.0f);
        SetCameraPosition(eye);
        SetCameraTarget(glm::vec3(0, 3.0f, 10.0f));
        glm::vec3 fwd = glm::normalize(GetCameraTarget() - 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 << "Odyssey ready: " << game_objects_.size() << " objects.\n"
                  << "  WASD/arrows move+look, Space/LCtrl up/down, LShift sprint.\n"
                  << "  1-6 or Q/E cycle items   F/LMB/R1 to throw.\n";
        return true;
    }

    void BuildArm() {
        // Upper arm (shoulder → elbow)
        arm_upper_ = CreateGameObject(
            CreateCylinder(0.10f, 0.45f, 10, glm::vec3(0.92f, 0.85f, 0.78f)),
            glm::vec3(0));
        arm_upper_->SetRoughness(0.75f);
        AddGameObject(arm_upper_);
        // Forearm (elbow → wrist)
        arm_lower_ = CreateGameObject(
            CreateCylinder(0.085f, 0.45f, 10, glm::vec3(0.92f, 0.85f, 0.78f)),
            glm::vec3(0));
        arm_lower_->SetRoughness(0.75f);
        AddGameObject(arm_lower_);
        // Hand (sphere)
        hand_ = CreateGameObject(
            CreateSphere(18, glm::vec3(0.92f, 0.85f, 0.78f)),
            glm::vec3(0));
        hand_->SetScale(glm::vec3(0.11f));
        hand_->SetRoughness(0.7f);
        AddGameObject(hand_);
        arm_ready_ = true;
    }

    void UpdateArm(float dt) {
        if (!arm_ready_) return;
        throw_anim_ = std::max(0.0f, throw_anim_ - dt * 4.0f);
        // Build camera basis
        glm::vec3 cp = GetCameraPosition();
        glm::vec3 tg = GetCameraTarget();
        glm::vec3 fwd = glm::normalize(tg - cp);
        glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
        glm::vec3 up    = glm::cross(right, fwd);
        // Throw motion: forearm pitches forward then snaps back
        float t = throw_anim_;
        float forward_boost = t * 0.45f;
        // Shoulder offset: to the right and slightly down from the eye
        glm::vec3 shoulder = cp + right * 0.55f + up * -0.40f + fwd * 0.35f;
        // Elbow: slightly forward + down
        glm::vec3 elbow = shoulder + fwd * (0.18f + forward_boost * 0.4f)
                                   + up    * (-0.20f + t * 0.1f);
        // Wrist: more forward, pitches during throw
        glm::vec3 wrist = elbow + fwd * (0.35f + forward_boost)
                                + up    * (-0.05f + t * 0.3f);

        auto midpoint = [](const glm::vec3& a, const glm::vec3& b) {
            return (a + b) * 0.5f;
        };
        auto yawPitch = [&](const glm::vec3& dir) {
            glm::vec3 d = glm::normalize(dir);
            float pitch = glm::degrees(std::asin(d.y));
            float yaw   = glm::degrees(std::atan2(d.x, d.z));
            // Cylinder primitive's axis is +Y. We want to align +Y to 'dir'.
            // Compose rotations: yaw around Y then tilt.
            // Using the engine's Euler order (X, Y, Z degrees), a tilt along
            // the axis of rotation is approximated by pitch-around-X.
            return glm::vec3(-pitch - 90.0f, yaw, 0.0f);
        };

        glm::vec3 upperDir = elbow - shoulder;
        glm::vec3 lowerDir = wrist - elbow;
        arm_upper_->SetPosition(midpoint(shoulder, elbow));
        arm_upper_->SetRotation(yawPitch(upperDir));
        float ul = glm::length(upperDir);
        arm_upper_->SetScale(glm::vec3(1.0f, ul / 0.45f, 1.0f));

        arm_lower_->SetPosition(midpoint(elbow, wrist));
        arm_lower_->SetRotation(yawPitch(lowerDir));
        float ll = glm::length(lowerDir);
        arm_lower_->SetScale(glm::vec3(1.0f, ll / 0.45f, 1.0f));

        hand_->SetPosition(wrist);
        // Color hand tint by currently-selected item so you know what you'll throw
        hand_->SetEmissive(items_[selected_].emissive * 0.3f);
    }

    void FireBall() {
        const Item& it = items_[selected_];
        glm::vec3 cp = GetCameraPosition();
        glm::vec3 tg = GetCameraTarget();
        glm::vec3 fwd = glm::normalize(tg - cp);
        glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
        glm::vec3 up    = glm::cross(right, fwd);
        glm::vec3 spawn = cp + right * 0.55f + up * -0.25f + fwd * 1.2f;

        if ((int)active_shots_.size() >= kMaxShots) {
            auto old = active_shots_.front(); active_shots_.pop_front();
            processAgentCommand(json::parse("{\"action\":\"delete\",\"id\":\"" + old + "\"}"));
        }
        std::string id = "shot_" + std::to_string(shot_counter_++);
        auto b = CreateGameObject(CreateSphere(20, it.color), spawn);
        b->SetScale(glm::vec3(it.radius));
        b->SetMetallic(selected_ == 0 || selected_ == 5 ? 1.0f : 0.0f);
        b->SetRoughness(selected_ == 0 ? 0.3f : 0.5f);
        b->SetEmissive(it.emissive);
        AddGameObject(b);
        namedObjects_[id] = b;

        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((double)it.mass);
        bc["restitution"]=json::Value(0.5);
        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 * it.velocity)),
            json::Value((double)(fwd.y * it.velocity)),
            json::Value((double)(fwd.z * it.velocity))});
        processAgentCommand(json::Value(vc));
        active_shots_.push_back(id);

        throw_anim_ = 1.0f;
        processAgentCommand(json::parse("{\"action\":\"physics\",\"enabled\":true}"));
    }

    void OnUpdate(float dt) override {
        // Hotbar select (1-6, Q/E cycle, gamepad L1/R1-ish — but R1 is shoot,
        // so cycle with shoulder ls-click or D-pad)
        int keys[6] = {GLFW_KEY_1, GLFW_KEY_2, GLFW_KEY_3, GLFW_KEY_4,
                       GLFW_KEY_5, GLFW_KEY_6};
        for (int i = 0; i < 6; ++i) if (Input::IsKeyPressed(keys[i])) selected_ = i;
        bool q = Input::IsKeyHeld(GLFW_KEY_Q);
        bool e = Input::IsKeyHeld(GLFW_KEY_E);
        if (q && !q_prev_) selected_ = (selected_ + 5) % 6;
        if (e && !e_prev_) selected_ = (selected_ + 1) % 6;
        q_prev_ = q; e_prev_ = e;

        // Shoot
        shot_cooldown_ -= dt;
        if (shot_cooldown_ <= 0.0f) {
            bool fire = Input::IsKeyHeld(GLFW_KEY_F)
                     || Input::IsMouseButtonPressed(Input::MouseButton::Left)
                     || Input::IsGamepadButtonDown(Input::GamepadButton::R1);
            if (fire) { FireBall(); shot_cooldown_ = 0.18f; }
        }

        // Free-fly camera (sync from engine, apply pan + look)
        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 = 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) {
            cam_yaw_   += rs.x * look_sens_ * dt;
            cam_pitch_ -= rs.y * look_sens_ * dt;
            cam_pitch_ = std::clamp(cam_pitch_, -89.0f, 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;
        } else {
            last_cam_pos_ = eng_pos;
        }

        UpdateArm(dt);
    }

    void OnRenderOverlay() override {
        Hud hud(font_, width_, height_);

        // --- Hotbar: 6 slots centered at bottom ---
        int N = 6;
        float slot_w = 84.0f, slot_h = 84.0f;
        float pad = 8.0f;
        float total_w = N * slot_w + (N - 1) * pad;
        float start_x = (width_ - total_w) * 0.5f;
        float y = 18.0f;
        for (int i = 0; i < N; ++i) {
            float x = start_x + i * (slot_w + pad);
            const Item& it = items_[i];
            bool sel = (i == selected_);
            glm::vec4 bg(0.03f, 0.04f, 0.07f, 0.8f);
            glm::vec4 br = sel ? glm::vec4(it.color, 1.0f)
                               : glm::vec4(0.8f, 0.8f, 0.8f, 0.35f);
            float bt = sel ? 3.0f : 1.5f;
            hud.beginPanel(x, y, slot_w, slot_h, bg, br, bt);
            hud.drawPanelBackgroundNow();
            // Item icon: colored rect (emissive tint applied via color mix)
            glm::vec3 ic = it.color + it.emissive * 0.2f;
            hud.rect(x + 14, y + 26, slot_w - 28, slot_h - 44,
                     glm::vec4(ic, 1.0f));
            // Number badge (top-left) and label (bottom)
            char num[4]; std::snprintf(num, sizeof(num), "%d", i + 1);
            hud.text(x + 8, y + slot_h - 18, num, 14.0f,
                     glm::vec3(1, 1, 1), true);
            hud.text(x + 10, y + 7, it.label, 12.0f,
                     glm::vec3(1, 1, 1), true);
            hud.endPanel();
        }

        // Controls hint (top-left)
        hud.beginPanel(14, (float)height_ - 104, 440, 88,
                       glm::vec4(0.02f, 0.03f, 0.05f, 0.7f),
                       glm::vec4(0.5f, 0.8f, 0.9f, 0.5f), 2.0f);
        hud.drawPanelBackgroundNow();
        hud.label("ODYSSEY", 18.0f, glm::vec3(0.8f, 0.95f, 1.0f), true);
        hud.separator();
        hud.label("WASD MOVE   SPACE/LCTRL FLY   SHIFT SPRINT", 13.0f,
                  glm::vec3(0.85f, 0.9f, 0.85f));
        hud.label("F OR LMB THROW   1-6 OR Q/E SELECT ITEM", 13.0f,
                  glm::vec3(0.85f, 0.9f, 0.85f));
        hud.endPanel();

        // Crosshair at center
        {
            float cx = width_ * 0.5f, cy = height_ * 0.5f;
            hud.rect(cx - 10, cy - 1, 7, 2, glm::vec4(1, 1, 1, 0.7f));
            hud.rect(cx + 3,  cy - 1, 7, 2, glm::vec4(1, 1, 1, 0.7f));
            hud.rect(cx - 1,  cy - 10, 2, 7, glm::vec4(1, 1, 1, 0.7f));
            hud.rect(cx - 1,  cy + 3,  2, 7, glm::vec4(1, 1, 1, 0.7f));
        }

        hud.flush();
    }
};


// ---------------------------------------------
// BuildersDemo — top-down "business village" foundation.
//   - Camera: fixed-pitch bird's-eye, WASD pans, scroll/+/- zooms
//   - Left-click on ground to place a building at the cursor
//   - Keys 1-4 pick which building TYPE to place next
//   - Each building spawns 3 animated "workers" that wander around it
//   - Each building has a business idea (displayed above it)
//   - Mock worker messages post periodically to a corner log (placeholder
//     for Claude-backed task output in the next round)
//   - Y key: cycle which building's messages show in detail
// ---------------------------------------------
class BuildersDemo : public Engine {
    struct Worker {
        glm::vec3 pos;
        glm::vec3 target;
        float     speed;
        std::shared_ptr<GameObject> obj;
        std::shared_ptr<GameObject> head;  // tiny sphere on top
    };
    struct Building {
        int   type_idx;
        std::string business;
        glm::vec3 pos;
        std::shared_ptr<GameObject> box;
        std::shared_ptr<GameObject> roof;
        std::vector<Worker> workers;
        float next_message_in = 2.0f;
    };

    std::vector<Building> buildings_;
    std::deque<std::string> message_log_;  // last N messages
    static constexpr int kMaxMessages = 12;
    int   active_btype_ = 0;
    bool  mouse_prev_down_ = false;
    float cam_x_ = 0.0f, cam_z_ = 0.0f;
    float cam_height_ = 40.0f;   // zoom via scroll
    float cam_yaw_   = 0.0f;     // 0 = camera facing +z (straight down-forward)
    float cam_pitch_frac_ = 0.35f;  // how far the target is offset forward
    float pan_speed_ = 30.0f;
    float place_cooldown_ = 0.0f;
    float map_radius_ = 90.0f;   // soft boundary; camera clamped to this circle
    bool  gp_sel_prev_[4] = {false,false,false,false};
    bool  gp_place_prev_ = false;
    std::mt19937 rng_{31415};

    // Business templates — 4 types with distinct color + prompt seed
    struct BType {
        const char* label;
        const char* example_business;
        glm::vec3   roof_color;   // emissive
        glm::vec3   wall_color;
    };
    const BType kBTypes[4] = {
        {"Tech Startup",   "SaaS dashboard for mobile analytics",
         glm::vec3(0.4f, 1.6f, 2.4f),  glm::vec3(0.85f, 0.88f, 0.95f)},
        {"Design Studio",  "Brand identity for indie games",
         glm::vec3(2.4f, 0.7f, 1.6f),  glm::vec3(0.95f, 0.82f, 0.85f)},
        {"Research Lab",   "Material science for EV batteries",
         glm::vec3(0.8f, 2.4f, 2.0f),  glm::vec3(0.78f, 0.95f, 0.92f)},
        {"Workshop",       "Custom handcrafted furniture",
         glm::vec3(2.4f, 1.4f, 0.5f),  glm::vec3(0.92f, 0.80f, 0.60f)},
    };

public:
    BuildersDemo()
      : Engine(1920, 1080, "ZoomEngine - Builders (top-down)") {}

    bool OnInitialize() override {
        // Overcast flat lighting — reads top-down well, minimal harsh shadows
        processAgentCommand(json::parse(R"({
            "action":"sun",
            "direction":[-0.25, 0.95, -0.15],
            "color":[1.85, 1.80, 1.70],
            "zenith":[0.45, 0.55, 0.70],
            "horizon":[0.85, 0.85, 0.82],
            "sky":[0.72, 0.76, 0.80],
            "ground":[0.10, 0.11, 0.12],
            "shadow_extent":100,
            "exposure":0.95,
            "bloom_threshold":1.25,
            "bloom_intensity":1.0,
            "fog_density":0.002
        })"));
        processAgentCommand(json::parse(R"({"action":"water","enabled":false})"));

        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); };

        GLuint grass_tex = TextureCache::Get("grass");
        GLuint dirt_t    = TextureCache::Get("dirt");
        GLuint rock_tex  = TextureCache::Get("rock");
        GLuint concrete  = TextureCache::Get("concrete");
        GLuint stripes_t = TextureCache::Get("stripes");
        GLuint marble_t  = TextureCache::Get("marble");
        GLuint wood_tex  = TextureCache::Get("wood");
        GLuint noise_t   = TextureCache::Get("noise");

        // --- Varied terrain: 3x3 tiles (60 units each) with different
        // texture + color palettes so zones read distinctly from above.
        struct Tile { GLuint tex; glm::vec3 col; float uv; float rough; };
        const Tile tiles[9] = {
            { grass_tex, glm::vec3(0.75f, 0.90f, 0.70f), 0.20f, 0.95f }, // NW green
            { dirt_t,    glm::vec3(0.92f, 0.80f, 0.55f), 0.25f, 0.95f }, // N  dune
            { grass_tex, glm::vec3(0.70f, 0.85f, 0.75f), 0.22f, 0.95f }, // NE green-cool
            { rock_tex,  glm::vec3(0.78f, 0.76f, 0.72f), 0.22f, 0.85f }, // W  rocky
            { marble_t,  glm::vec3(0.92f, 0.90f, 0.85f), 0.20f, 0.55f }, // C  plaza
            { grass_tex, glm::vec3(0.80f, 0.88f, 0.78f), 0.20f, 0.92f }, // E  grass
            { stripes_t, glm::vec3(0.78f, 0.85f, 0.75f), 0.15f, 0.90f }, // SW field
            { grass_tex, glm::vec3(0.78f, 0.92f, 0.72f), 0.22f, 0.92f }, // S  grass
            { dirt_t,    glm::vec3(0.85f, 0.76f, 0.55f), 0.25f, 0.95f }, // SE dirt
        };
        const float kTile = 60.0f;
        for (int j = 0; j < 3; ++j) {
            for (int i = 0; i < 3; ++i) {
                int idx = j * 3 + i;
                float x = (i - 1) * kTile;
                float z = (j - 1) * kTile;
                auto t = CreateGameObject(
                    CreatePlane(kTile, kTile, tiles[idx].col),
                    glm::vec3(x, 0.0f, z));
                t->SetTexture(tiles[idx].tex);
                t->SetNormalMap(noise_t);
                t->SetUVScale(glm::vec2(tiles[idx].uv));
                t->SetRoughness(tiles[idx].rough);
                AddGameObject(t);
            }
        }

        // --- Central plaza rings (unchanged visual focus) ---
        for (int i = 1; i <= 3; ++i) {
            auto ring = CreateGameObject(
                CreateCylinder((float)i * 4.0f, 0.06f, 48,
                               glm::vec3(0.94f, 0.92f, 0.88f)),
                glm::vec3(0, 0.015f, 0));
            ring->SetTexture(marble_t); ring->SetNormalMap(noise_t);
            ring->SetRoughness(0.45f);
            AddGameObject(ring);
        }

        // --- Map border: DENSE ring of trees inside the soft cam bound ---
        // Two rows of trees so it reads as a real wall, not a thin fence.
        auto border_tree = [&](float x, float z, float s, float rough = 0.85f) {
            auto trunk = CreateGameObject(
                CreateCylinder(0.30f * s, 2.8f * s, 10,
                               glm::vec3(0.80f, 0.72f, 0.60f)),
                glm::vec3(x, 1.4f * s, z));
            trunk->SetRotation(glm::vec3(jit(3), jit(360), jit(3)));
            trunk->SetTexture(wood_tex); trunk->SetNormalMap(noise_t);
            trunk->SetRoughness(rough);
            AddGameObject(trunk);
            glm::vec3 cc(rnd(0.10f, 0.22f), rnd(0.38f, 0.55f), rnd(0.12f, 0.22f));
            auto cone = CreateGameObject(
                CreateCone(1.8f * s, 4.2f * s, 10, cc),
                glm::vec3(x + jit(0.15f), 3.2f * s, z + jit(0.15f)));
            cone->SetNormalMap(noise_t); cone->SetRoughness(0.8f);
            AddGameObject(cone);
        };
        const float border_r_inner = 94.0f;
        const float border_r_outer = 100.0f;
        for (int i = 0; i < 48; ++i) {
            float a = (float)i / 48.0f * 2 * (float)M_PI + jit(0.02f);
            float s = rnd(1.0f, 1.6f);
            border_tree(std::cos(a) * border_r_inner + jit(2.0f),
                        std::sin(a) * border_r_inner + jit(2.0f),
                        s);
        }
        for (int i = 0; i < 40; ++i) {
            float a = (float)i / 40.0f * 2 * (float)M_PI + 0.065f;
            float s = rnd(1.1f, 1.7f);
            border_tree(std::cos(a) * border_r_outer + jit(2.5f),
                        std::sin(a) * border_r_outer + jit(2.5f),
                        s);
        }

        // --- Outer mountain ring (backdrop + final boundary) ---
        for (int i = 0; i < 28; ++i) {
            float a = (float)i / 28.0f * 2 * (float)M_PI + jit(0.05f);
            float R = 160.0f + rnd(-10.0f, 15.0f);
            float sc = 1.0f + ((float)((i * 47) % 100) / 100.0f) * 0.6f;
            float h = 30.0f * sc;
            auto m = CreateGameObject(
                CreateCone(15.0f * sc, h, 10, glm::vec3(0.22f, 0.21f, 0.24f)),
                glm::vec3(std::cos(a) * R, h * 0.5f, std::sin(a) * R));
            m->SetRoughness(0.9f);
            AddGameObject(m);
        }

        // --- A few scattered trees inside the play area for variety ---
        for (int i = 0; i < 18; ++i) {
            float x = rnd(-80.0f, 80.0f);
            float z = rnd(-80.0f, 80.0f);
            // avoid the central plaza / spawn area
            if (std::abs(x) < 16.0f && std::abs(z) < 16.0f) continue;
            border_tree(x, z, rnd(0.7f, 1.2f));
        }

        // Initial log message
        AddMessage("[System] BuildersDemo ready. Click ground to place a building.");
        AddMessage("[System] Keys 1-4: select building type. F or LMB: place.");

        // Initial camera: top-down with slight pitch so mouse picking works
        cam_x_ = 0.0f; cam_z_ = 0.0f;
        UpdateCamera();
        return true;
    }

    void UpdateCamera() {
        // High-angle view that rotates around the target. Yaw determines the
        // horizontal direction the camera is pushed from, so "forward" for
        // the player is toward +Z in view-space.
        float cy = std::cos(glm::radians(cam_yaw_));
        float sy = std::sin(glm::radians(cam_yaw_));
        float back = cam_height_ * cam_pitch_frac_;
        glm::vec3 eye(cam_x_ + sy * back, cam_height_, cam_z_ + cy * back);
        glm::vec3 tgt(cam_x_, 0.0f, cam_z_);
        SetCameraPosition(eye);
        SetCameraTarget(tgt);
    }

    void AddMessage(const std::string& msg) {
        message_log_.push_back(msg);
        while ((int)message_log_.size() > kMaxMessages) message_log_.pop_front();
        std::cout << msg << "\n";
    }

    void PlaceBuilding(glm::vec3 pos) {
        // Check it's not too close to another building
        for (auto& b : buildings_) {
            if (glm::length(glm::vec2(b.pos.x - pos.x, b.pos.z - pos.z)) < 5.0f) {
                AddMessage("[System] Too close to another building — skipped.");
                return;
            }
        }
        Building b;
        b.type_idx = active_btype_;
        b.business = kBTypes[active_btype_].example_business;
        b.pos = glm::vec3(pos.x, 0, pos.z);

        // Walls
        auto box = CreateGameObject(
            CreateCube(kBTypes[active_btype_].wall_color),
            glm::vec3(pos.x, 2.0f, pos.z));
        box->SetScale(glm::vec3(3.0f, 4.0f, 3.0f));
        box->SetTexture(TextureCache::Get("concrete"));
        box->SetNormalMap(TextureCache::Get("noise"));
        box->SetRoughness(0.75f);
        AddGameObject(box);
        b.box = box;

        // Roof — emissive so it glows with type color
        auto roof = CreateGameObject(
            CreateCube(glm::vec3(1.0f)),
            glm::vec3(pos.x, 4.2f, pos.z));
        roof->SetScale(glm::vec3(3.3f, 0.4f, 3.3f));
        roof->SetMetallic(0.3f);
        roof->SetRoughness(0.35f);
        roof->SetEmissive(kBTypes[active_btype_].roof_color);
        AddGameObject(roof);
        b.roof = roof;

        // 3 workers around the building
        std::uniform_real_distribution<float> ang(0.0f, 2 * M_PI);
        for (int i = 0; i < 3; ++i) {
            Worker w;
            float a = ang(rng_);
            w.pos = glm::vec3(pos.x + std::cos(a) * 2.2f,
                              0.0f,
                              pos.z + std::sin(a) * 2.2f);
            w.target = w.pos;
            w.speed = 1.5f + (rng_() % 10) * 0.1f;
            // Body
            auto body = CreateGameObject(
                CreateCylinder(0.22f, 0.9f, 10,
                               glm::vec3(0.95f, 0.95f, 0.95f)),
                glm::vec3(w.pos.x, 0.45f, w.pos.z));
            body->SetMetallic(0.0f); body->SetRoughness(0.6f);
            AddGameObject(body);
            w.obj = body;
            // Head (emissive to match building color)
            auto head = CreateGameObject(
                CreateSphere(12, glm::vec3(1.0f)),
                glm::vec3(w.pos.x, 1.05f, w.pos.z));
            head->SetScale(glm::vec3(0.25f));
            head->SetEmissive(kBTypes[active_btype_].roof_color * 0.6f);
            AddGameObject(head);
            w.head = head;
            b.workers.push_back(w);
        }
        buildings_.push_back(std::move(b));
        AddMessage(std::string("[") + kBTypes[active_btype_].label + "] Building #"
                   + std::to_string((int)buildings_.size())
                   + " opened. Business: " + kBTypes[active_btype_].example_business);
    }

    void StepWorkers(float dt) {
        std::uniform_real_distribution<float> offs(-2.4f, 2.4f);
        for (auto& b : buildings_) {
            for (auto& w : b.workers) {
                glm::vec3 to = w.target - w.pos;
                float d = glm::length(to);
                if (d < 0.05f) {
                    // Pick new random target within building's zone
                    w.target = b.pos + glm::vec3(offs(rng_), 0, offs(rng_));
                } else {
                    glm::vec3 step = (to / d) * w.speed * dt;
                    if (glm::length(step) > d) step = to;
                    w.pos += step;
                    w.obj->SetPosition(glm::vec3(w.pos.x, 0.45f, w.pos.z));
                    w.head->SetPosition(glm::vec3(w.pos.x, 1.05f, w.pos.z));
                    // Simple bob to suggest walking
                    float bob = 0.04f * std::sin((float)glfwGetTime() * 6.0f
                                                 + w.speed * 2.0f);
                    w.obj->SetPosition(glm::vec3(w.pos.x, 0.45f + bob, w.pos.z));
                    w.head->SetPosition(glm::vec3(w.pos.x, 1.05f + bob, w.pos.z));
                }
            }
        }
    }

    void TickMessages(float dt) {
        // Each building periodically posts a mock "work result" — in V2
        // this is replaced by a real Claude API call driven by a helper.
        static const std::array<std::array<const char*, 5>, 4> templates = {{
            { "drafted a wireframe for the onboarding flow",
              "committed a PR with auth refactors",
              "reviewed metrics, CTR up 3.2%",
              "spiked a new analytics aggregation",
              "shipped a bug fix for chart rendering" },
            { "pitched a new logo direction",
              "uploaded four mood-board options",
              "iterated on the brand color system",
              "finalized the key art for Chapter 2",
              "drafted marketing copy for the press kit" },
            { "ran an overnight electrolyte viscosity test",
              "calibrated the coin-cell jig",
              "drafted a technical note on dendrite growth",
              "annotated the SEM images from batch 7",
              "updated the doping-ratio ablation chart" },
            { "milled a new maple chair leg set",
              "sanded the walnut desk to 600-grit",
              "applied the first oil coat to the bookshelf",
              "sketched a new modular shelving concept",
              "shipped the dining-table order to the client" },
        }};
        for (auto& b : buildings_) {
            b.next_message_in -= dt;
            if (b.next_message_in <= 0.0f) {
                b.next_message_in = 4.0f + (rng_() % 40) / 10.0f;
                int widx = rng_() % (int)b.workers.size();
                const auto& tmpls = templates[b.type_idx];
                int tidx = rng_() % (int)tmpls.size();
                std::ostringstream ss;
                ss << "[" << kBTypes[b.type_idx].label << "#"
                   << (&b - &buildings_[0] + 1)
                   << "] Worker " << (widx + 1) << ": " << tmpls[tidx];
                AddMessage(ss.str());
            }
        }
    }

    void OnUpdate(float dt) override {
        // -------- Gamepad pre-sample --------
        glm::vec2 ls(0), rs(0);
        float gp_in = 0.0f, gp_out = 0.0f;
        bool gp_sprint = false;
        bool gp_place  = false;
        bool gp_pick[4] = {false,false,false,false};
        if (Input::IsGamepadConnected()) {
            ls = Input::GetLeftStick();
            rs = Input::GetRightStick();
            gp_out = Input::GetR2();   // zoom out (raise camera)
            gp_in  = Input::GetL2();   // zoom in
            gp_sprint = Input::IsGamepadButtonDown(Input::GamepadButton::Cross);
            gp_place  = Input::IsGamepadButtonDown(Input::GamepadButton::R1);
            gp_pick[0] = Input::IsGamepadButtonDown(Input::GamepadButton::Square);
            gp_pick[1] = Input::IsGamepadButtonDown(Input::GamepadButton::Triangle);
            gp_pick[2] = Input::IsGamepadButtonDown(Input::GamepadButton::Circle);
            // Cross is used for sprint; use D-pad down / Cross only if others
            // are rarely pressed. Treat D-pad as fallback. For V1 just skip
            // a 4th gamepad type-select slot (fine — keyboard has 1-4).
        }

        // -------- Yaw (camera rotation) --------
        float yaw_rate = 80.0f;  // deg/sec
        if (Input::IsKeyHeld(GLFW_KEY_Q))     cam_yaw_ -= yaw_rate * dt;
        if (Input::IsKeyHeld(GLFW_KEY_E))     cam_yaw_ += yaw_rate * dt;
        if (Input::IsKeyHeld(GLFW_KEY_LEFT))  cam_yaw_ -= yaw_rate * dt;
        if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) cam_yaw_ += yaw_rate * dt;
        cam_yaw_ += rs.x * yaw_rate * dt;  // gamepad right stick X
        while (cam_yaw_ >  360.0f) cam_yaw_ -= 360.0f;
        while (cam_yaw_ < -360.0f) cam_yaw_ += 360.0f;

        // -------- Pan (keyboard + gamepad left stick), relative to yaw --------
        float speed = pan_speed_;
        if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT) || gp_sprint) speed *= 2.5f;
        float ix = 0.0f, iz = 0.0f;
        if (Input::IsKeyHeld(GLFW_KEY_W)) iz -= 1.0f;
        if (Input::IsKeyHeld(GLFW_KEY_S)) iz += 1.0f;
        if (Input::IsKeyHeld(GLFW_KEY_A)) ix -= 1.0f;
        if (Input::IsKeyHeld(GLFW_KEY_D)) ix += 1.0f;
        ix += ls.x; iz += ls.y;
        // Rotate pan vector by yaw so "forward" tracks camera direction
        float cy = std::cos(glm::radians(cam_yaw_));
        float sy = std::sin(glm::radians(cam_yaw_));
        float wx =  ix * cy + iz * sy;
        float wz = -ix * sy + iz * cy;
        cam_x_ += wx * speed * dt;
        cam_z_ += wz * speed * dt;

        // Zoom (keyboard +/-, gamepad R2/L2, up/down arrows)
        if (Input::IsKeyHeld(GLFW_KEY_EQUAL) || gp_in > 0.05f)
            cam_height_ = std::max(12.0f, cam_height_ - (15.0f + gp_in * 25.0f) * dt);
        if (Input::IsKeyHeld(GLFW_KEY_MINUS) || gp_out > 0.05f)
            cam_height_ = std::min(130.0f, cam_height_ + (15.0f + gp_out * 25.0f) * dt);
        if (Input::IsKeyHeld(GLFW_KEY_UP))   cam_height_ = std::max(12.0f, cam_height_ - 18.0f * dt);
        if (Input::IsKeyHeld(GLFW_KEY_DOWN)) cam_height_ = std::min(130.0f, cam_height_ + 18.0f * dt);

        // Soft circular clamp to keep camera from leaving the map
        {
            float r = std::sqrt(cam_x_ * cam_x_ + cam_z_ * cam_z_);
            if (r > map_radius_) {
                float k = map_radius_ / r;
                cam_x_ *= k;
                cam_z_ *= k;
            }
        }
        UpdateCamera();

        // -------- Building type select (keyboard + gamepad face buttons edge) --------
        if (Input::IsKeyPressed(GLFW_KEY_1)) { active_btype_ = 0; AddMessage("[System] Placing: Tech Startup"); }
        if (Input::IsKeyPressed(GLFW_KEY_2)) { active_btype_ = 1; AddMessage("[System] Placing: Design Studio"); }
        if (Input::IsKeyPressed(GLFW_KEY_3)) { active_btype_ = 2; AddMessage("[System] Placing: Research Lab"); }
        if (Input::IsKeyPressed(GLFW_KEY_4)) { active_btype_ = 3; AddMessage("[System] Placing: Workshop"); }
        for (int i = 0; i < 3; ++i) {
            if (gp_pick[i] && !gp_sel_prev_[i]) {
                active_btype_ = i;
                const char* label = (i == 0 ? "Tech Startup" :
                                     i == 1 ? "Design Studio" : "Research Lab");
                AddMessage(std::string("[System] Placing: ") + label);
            }
            gp_sel_prev_[i] = gp_pick[i];
        }

        // -------- Place building (mouse/F/R1, edge-triggered) --------
        place_cooldown_ -= dt;
        bool mouse_now = Input::IsMouseButtonPressed(Input::MouseButton::Left);
        bool place = (mouse_now && !mouse_prev_down_)
                  || Input::IsKeyPressed(GLFW_KEY_F)
                  || (gp_place && !gp_place_prev_);
        mouse_prev_down_ = mouse_now;
        gp_place_prev_ = gp_place;
        if (place && place_cooldown_ <= 0.0f) {
            // With gamepad + no mouse cursor, just drop at the camera target.
            glm::vec3 hit = gp_place
                ? glm::vec3(cam_x_, 0.0f, cam_z_)
                : ScreenToGround();
            if (std::abs(hit.x) + std::abs(hit.z) > 0.001f ||
                (gp_place && (cam_x_ != 0 || cam_z_ != 0))) {
                PlaceBuilding(hit);
                place_cooldown_ = 0.25f;
            }
        }

        StepWorkers(dt);
        TickMessages(dt);
    }

    void OnRenderOverlay() override {
        Hud hud(font_, width_, height_);

        // ====== TOP-LEFT: Build panel ======
        const BType& bt = kBTypes[active_btype_];
        hud.beginPanel(14, (float)height_ - 210, 480, 182,
                       glm::vec4(0.02f, 0.03f, 0.05f, 0.75f),
                       glm::vec4(bt.roof_color, 0.75f),
                       2.5f);
        hud.drawPanelBackgroundNow();
        hud.icon(14, 14, glm::vec4(bt.roof_color, 1.0f));
        hud.label(std::string("PLACING: ") + bt.label, 22.0f,
                  glm::vec3(1, 1, 1), true);
        hud.label(bt.example_business, 16.0f,
                  glm::vec3(0.85f, 0.95f, 0.9f));
        hud.separator();
        hud.label("[1][2][3][4] type   LMB/F place", 14.0f,
                  glm::vec3(0.7f, 0.85f, 0.75f));
        hud.label("WASD pan   +/- zoom", 14.0f,
                  glm::vec3(0.7f, 0.85f, 0.75f));
        hud.endPanel();

        // ====== TOP-RIGHT: Stats panel ======
        hud.beginPanel((float)width_ - 320, (float)height_ - 156, 300, 128,
                       glm::vec4(0.02f, 0.03f, 0.05f, 0.75f),
                       glm::vec4(0.5f, 0.75f, 0.9f, 0.6f),
                       2.5f);
        hud.drawPanelBackgroundNow();
        hud.label("VILLAGE STATS", 19.0f, glm::vec3(0.6f, 0.85f, 1.0f), true);
        hud.separator();
        char buf[128];
        std::snprintf(buf, sizeof(buf), "Buildings:  %d", (int)buildings_.size());
        hud.label(buf, 17.0f, glm::vec3(1, 1, 1));
        std::snprintf(buf, sizeof(buf), "Workers:    %d", (int)(buildings_.size() * 3));
        hud.label(buf, 17.0f, glm::vec3(1, 1, 1));
        hud.endPanel();

        // ====== BOTTOM-LEFT: Message log ======
        float log_h = std::min(360.0f, 48.0f + (float)message_log_.size() * 22.0f);
        hud.beginPanel(14, 14, 720, log_h,
                       glm::vec4(0.02f, 0.03f, 0.05f, 0.75f),
                       glm::vec4(0.9f, 0.9f, 0.9f, 0.35f),
                       2.0f);
        hud.drawPanelBackgroundNow();
        hud.label("AGENT LOG", 16.0f, glm::vec3(0.7f, 0.9f, 1.0f), true);
        hud.separator();
        int count = (int)message_log_.size();
        int shown = std::min(count, 10);
        for (int i = count - 1; i >= std::max(0, count - shown); --i) {
            float a = 0.45f + 0.55f * float(i + 1 - (count - shown)) / float(shown);
            hud.label(message_log_[i], 14.0f,
                      glm::vec3(1.0f, 1.0f, 1.0f) * a);
        }
        hud.endPanel();

        // ====== Per-building floating labels + capacity bars ======
        glm::mat4 view = glm::lookAt(camera_position_, camera_target_, camera_up_);
        glm::mat4 proj = glm::perspective(glm::radians(settings_.fov),
                                          float(width_) / height_, 0.1f, settings_.drawDistance);
        glm::mat4 vp = proj * view;
        for (int i = 0; i < (int)buildings_.size(); ++i) {
            const Building& b = buildings_[i];
            glm::vec4 clip = vp * glm::vec4(b.pos.x, 6.0f, b.pos.z, 1.0f);
            if (clip.w <= 0.01f) continue;
            glm::vec3 ndc = glm::vec3(clip) / clip.w;
            if (std::abs(ndc.x) > 1.2f || std::abs(ndc.y) > 1.2f) continue;
            float sx = (ndc.x * 0.5f + 0.5f) * width_;
            float sy = (ndc.y * 0.5f + 0.5f) * height_;
            const BType& bt2 = kBTypes[b.type_idx];
            hud.rect(sx - 70, sy + 6, 140, 22,
                     glm::vec4(0.0f, 0.0f, 0.0f, 0.6f));
            hud.rect(sx - 70, sy + 28, 140, 2, glm::vec4(bt2.roof_color, 0.9f));
            hud.text(sx - 64, sy + 11,
                     std::string(bt2.label) + " #" + std::to_string(i + 1),
                     12.0f, glm::vec3(1.0f), true);
        }

        hud.flush();
    }
};


// ---------------------------------------------
// FourMapDemo — large four-quadrant themed map (160×160).
//   NW Forest  — mushrooms, conifers, moss rocks, fallen logs
//   NE Sakura  — cherry groves, stone lanterns, torii, blossoms
//   SW Desert  — cacti, dunes, step pyramid, obelisk, sand rocks
//   SE Tech    — chrome pylons, neon bars, crate pyramids, glowing monolith
//   CENTER     — sunken physics pool (buoyancy only inside the 6-unit radius)
//
// Zone atmospheres blend smoothly as the camera moves: sun color, sky,
// bloom threshold/intensity, fog density — each zone has its own recipe.
//
// F / LMB / R1 — shoot HDR metal ball
// ---------------------------------------------
class FourMapDemo : public Engine {
    float cam_yaw_   = 225.0f;
    float cam_pitch_ = -10.0f;
    float look_sensitivity_ = 80.0f;
    glm::vec3 last_cam_pos_{0};

    // Terrain params — heights for sakura (NE) and desert (SW) quadrants
    static constexpr float kSakuraMaxH    = 3.2f;
    static constexpr float kDesertMaxH    = 2.6f;
    static constexpr float kTerrainFalloff = 8.0f;
    static constexpr float kMapHalf       = 78.0f;

    // Walking character
    static constexpr float kEyeH     = 1.72f;
    static constexpr float kWalkSpeed = 6.5f;
    static constexpr float kRunMult  = 2.2f;
    static constexpr float kJumpV    = 7.5f;
    static constexpr float kGravity  = 20.0f;
    static constexpr float kStepUp   = 1.4f;   // max ground-height rise per move allowed

    float vy_        = 0.0f;
    bool  grounded_  = true;
    bool  fly_mode_  = false;
    bool  fly_prev_  = false;
    bool  jump_prev_ = false;

    // First-person arm
    std::shared_ptr<GameObject> arm_upper_, arm_lower_, hand_;
    bool  arm_ready_ = false;
    float throw_anim_ = 0.0f;

    float shot_cooldown_ = 0.0f;
    int   shot_counter_ = 0;
    std::deque<std::string> active_shots_;
    static constexpr int kMaxShots = 80;
    std::mt19937 rng_{2026};

public:
    FourMapDemo()
      : Engine(1920, 1080, "ZoomEngine - Four-Quadrant Map") {}

    // Sample the ground height used by the mesh generator in each quadrant.
    // Returns 0 outside the sakura/desert zones, tapered near quadrant edges.
    static float SampleTerrainH(float wx, float wz) {
        float mh = 0.0f, lx = 0.0f, lz = 0.0f;
        bool inside = false;
        // Sakura NE centered at (40, 40), local 80x80
        if (wx >= 0.0f && wz >= 0.0f && wx <= 80.0f && wz <= 80.0f) {
            mh = kSakuraMaxH; lx = wx - 40.0f; lz = wz - 40.0f; inside = true;
        }
        // Desert SW centered at (-40, -40)
        else if (wx <= 0.0f && wz <= 0.0f && wx >= -80.0f && wz >= -80.0f) {
            mh = kDesertMaxH; lx = wx + 40.0f; lz = wz + 40.0f; inside = true;
        }
        if (!inside) return 0.0f;
        float h = std::sin(lx * 0.1f) * std::cos(lz * 0.1f) * mh * 0.5f;
        h += std::sin(lx * 0.25f + 1.3f) * std::cos(lz * 0.3f + 0.7f) * mh * 0.25f;
        h += std::sin(lx * 0.6f + 2.1f) * std::cos(lz * 0.5f + 1.5f) * mh * 0.125f;
        // Match the edge falloff applied in TerrainPrimitive
        float dx_edge = 40.0f - std::fabs(lx);
        float dz_edge = 40.0f - std::fabs(lz);
        float d = std::min(dx_edge, dz_edge);
        float f = std::clamp(d / kTerrainFalloff, 0.0f, 1.0f);
        f = f * f * (3.0f - 2.0f * f);
        return h * f;
    }

    void 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));
    }

    std::shared_ptr<GameObject>
    addBox(const std::string& id, glm::vec3 pos, glm::vec3 scale,
           glm::vec3 color, GLuint tex, GLuint nmap,
           float metallic, float roughness, float mass,
           float rest = 0.2f, float frict = 0.6f,
           glm::vec3 rot = glm::vec3(0), glm::vec3 emissive = glm::vec3(0)) {
        auto obj = CreateGameObject(CreateCube(color), pos);
        obj->SetScale(scale);
        obj->SetRotation(rot);
        if (tex)  obj->SetTexture(tex);
        if (nmap) obj->SetNormalMap(nmap);
        obj->SetMetallic(metallic);
        obj->SetRoughness(roughness);
        if (glm::length(emissive) > 0) obj->SetEmissive(emissive);
        AddGameObject(obj);
        if (!id.empty()) namedObjects_[id] = obj;
        if (mass > 0)          bodyCmd(id, "box", mass, rest, frict, false);
        else if (mass == -1.0f) bodyCmd(id, "box", 0,   rest, frict, true);
        return obj;
    }

    std::shared_ptr<GameObject>
    addSphere(const std::string& id, glm::vec3 pos, float r,
              glm::vec3 color, GLuint tex, GLuint nmap,
              float metallic, float roughness, float mass,
              float rest = 0.35f, float frict = 0.35f,
              glm::vec3 emissive = glm::vec3(0)) {
        auto obj = CreateGameObject(CreateSphere(28, color), pos);
        obj->SetScale(glm::vec3(r));
        if (tex)  obj->SetTexture(tex);
        if (nmap) obj->SetNormalMap(nmap);
        obj->SetMetallic(metallic);
        obj->SetRoughness(roughness);
        if (glm::length(emissive) > 0) obj->SetEmissive(emissive);
        AddGameObject(obj);
        if (!id.empty()) namedObjects_[id] = obj;
        if (mass > 0) bodyCmd(id, "sphere", mass, rest, frict);
        return obj;
    }

    bool OnInitialize() override {
        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); };

        // --- Base palette: reduced fog, bright daylight. Gets blended per
        // zone each frame in UpdateAtmosphere. Values here are the starting
        // point before the camera picks a zone.
        processAgentCommand(json::parse(R"({
            "action":"sun",
            "direction":[-0.45, 0.60, -0.25],
            "color":[2.05, 1.85, 1.60],
            "zenith":[0.26, 0.42, 0.65],
            "horizon":[0.82, 0.80, 0.74],
            "sky":[0.65, 0.72, 0.78],
            "ground":[0.08, 0.09, 0.10],
            "shadow_extent":120,
            "exposure":0.95,
            "bloom_threshold":1.2,
            "bloom_intensity":1.0,
            "fog_density":0.003
        })"));

        // Physics-enabled pool: primary water mesh restricted to a small
        // central disk via bounds. Objects outside get no buoyancy; objects
        // inside bob in the pool properly. Water mesh size matches bounds
        // diameter so it doesn't visually spill.
        processAgentCommand(json::parse(R"({
            "action":"water","enabled":true,
            "level":0.7,"size":11,
            "amplitude":0.07,"wavelength":2.6,"speed":0.8,
            "shallow":[0.52, 0.68, 0.70],
            "deep":[0.04, 0.18, 0.28],
            "density":2.2,"resolution":96,
            "bounds_center":[0,0,0],"bounds_radius":5.0
        })"));
        processAgentCommand(json::parse(R"({
            "action":"weather","wind_dir":[1,0,0.2],"wind_speed":0.2,
            "storminess":0.0,"current_coef":0.0
        })"));

        GLuint grass_tex = TextureCache::Get("grass");
        GLuint rock_tex  = TextureCache::Get("rock");
        GLuint wood_tex  = TextureCache::Get("wood");
        GLuint dirt_t    = TextureCache::Get("dirt");
        GLuint concrete  = TextureCache::Get("concrete");
        GLuint marble_t  = TextureCache::Get("marble");
        GLuint checker_t = TextureCache::Get("checker");
        GLuint grid_t    = TextureCache::Get("grid");
        GLuint noise_t   = TextureCache::Get("noise");

        // --- Four quadrant ground planes (80×80 each, map total 160×160) ---
        auto forest_ground = CreateGameObject(
            CreatePlane(80.0f, 80.0f, glm::vec3(0.72f, 0.82f, 0.66f)),
            glm::vec3(-40.0f, 0.0f, 40.0f));
        forest_ground->SetTexture(grass_tex); forest_ground->SetNormalMap(noise_t);
        forest_ground->SetUVScale(glm::vec2(0.3f)); forest_ground->SetRoughness(0.95f);
        AddGameObject(forest_ground);

        // Sakura quadrant: deformed heightmap terrain, edges taper to 0
        auto sakura_ground = CreateGameObject(
            CreateTerrain(80, 80.0f, kSakuraMaxH,
                          glm::vec3(1.0f, 0.82f, 0.85f),
                          kTerrainFalloff, true),
            glm::vec3(40.0f, 0.0f, 40.0f));
        sakura_ground->SetTexture(grass_tex); sakura_ground->SetNormalMap(noise_t);
        sakura_ground->SetUVScale(glm::vec2(0.3f)); sakura_ground->SetRoughness(0.92f);
        AddGameObject(sakura_ground);

        // Desert quadrant: variable-height deformed plane (like the dune style)
        auto desert_ground = CreateGameObject(
            CreateTerrain(80, 80.0f, kDesertMaxH,
                          glm::vec3(1.05f, 0.88f, 0.62f),
                          kTerrainFalloff, true),
            glm::vec3(-40.0f, 0.0f, -40.0f));
        desert_ground->SetTexture(dirt_t); desert_ground->SetNormalMap(noise_t);
        desert_ground->SetUVScale(glm::vec2(0.35f)); desert_ground->SetRoughness(0.98f);
        AddGameObject(desert_ground);

        auto tech_ground = CreateGameObject(
            CreatePlane(80.0f, 80.0f, glm::vec3(0.3f, 0.32f, 0.38f)),
            glm::vec3(40.0f, 0.0f, -40.0f));
        tech_ground->SetTexture(grid_t); tech_ground->SetNormalMap(noise_t);
        tech_ground->SetUVScale(glm::vec2(0.5f));
        tech_ground->SetMetallic(0.6f);
        tech_ground->SetRoughness(0.3f);
        AddGameObject(tech_ground);

        // --- Physics ground plane: infinite static plane at y=0 covers everything
        // We register one of the quadrants as the physics body, but the plane
        // collider itself is infinite so it catches objects anywhere.
        namedObjects_["__ground"] = forest_ground;
        processAgentCommand(json::parse(R"({
            "action":"body","id":"__ground","shape":"plane",
            "normal":[0,1,0],"offset":0,"static":true
        })"));

        // --- Perimeter physics borders: static stone walls ringing the
        // 160×160 map (X,Z in [-80,80]) so the player and dynamic props
        // can't leave the play area. Pink-tinted marble blends with the
        // sakura quadrant and reads as neutral stone against the others.
        {
            const float kBL = 162.0f;   // wall length (slight corner overlap)
            const float kBH = 4.0f;     // wall height (above jump apex)
            const float kBT = 1.0f;     // wall thickness
            const glm::vec3 kBC(0.95f, 0.82f, 0.85f);
            // North wall (z = +80)
            addBox("border_n", glm::vec3(0.0f, kBH * 0.5f,  80.5f),
                   glm::vec3(kBL, kBH, kBT), kBC,
                   marble_t, noise_t, 0.0f, 0.75f, -1.0f);
            // South wall (z = -80)
            addBox("border_s", glm::vec3(0.0f, kBH * 0.5f, -80.5f),
                   glm::vec3(kBL, kBH, kBT), kBC,
                   marble_t, noise_t, 0.0f, 0.75f, -1.0f);
            // East wall (x = +80)
            addBox("border_e", glm::vec3( 80.5f, kBH * 0.5f, 0.0f),
                   glm::vec3(kBT, kBH, kBL), kBC,
                   marble_t, noise_t, 0.0f, 0.75f, -1.0f);
            // West wall (x = -80)
            addBox("border_w", glm::vec3(-80.5f, kBH * 0.5f, 0.0f),
                   glm::vec3(kBT, kBH, kBL), kBC,
                   marble_t, noise_t, 0.0f, 0.75f, -1.0f);
        }

        // ============================ FOREST (NW) =====================
        // Trees + mossy boulders + glowing mushrooms + fallen logs
        auto tree = [&](glm::vec3 at, float s) {
            auto trunk = CreateGameObject(
                CreateCylinder(0.22f * s, 2.6f * s, 10,
                               glm::vec3(0.86f, 0.80f, 0.70f)),
                at + glm::vec3(0, 1.3f * 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.08f, 0.16f), rnd(0.38f, 0.55f), rnd(0.10f, 0.20f));
            auto c = CreateGameObject(CreateCone(1.3f * s, 2.8f * s, 10, cc),
                                       at + glm::vec3(0, 3.1f * s, 0));
            c->SetNormalMap(noise_t); c->SetRoughness(0.8f);
            AddGameObject(c);
        };
        for (int i = 0; i < 45; ++i) {
            float x = rnd(-78.0f, -6.0f);
            float z = rnd( 6.0f,  78.0f);
            tree(glm::vec3(x, 0, z), rnd(0.8f, 1.5f));
        }
        // Physics moss rocks (dynamic)
        for (int i = 0; i < 14; ++i) {
            float x = rnd(-76.0f, -8.0f);
            float z = rnd( 8.0f,  76.0f);
            float s = rnd(0.5f, 1.4f);
            addSphere("frock_" + std::to_string(i),
                      glm::vec3(x, s + 0.1f, z), s,
                      glm::vec3(0.45f, 0.55f, 0.38f),
                      rock_tex, noise_t,
                      0.0f, 0.9f,
                      1.2f, 0.3f, 0.4f);
        }
        // Glowing mushrooms
        for (int i = 0; i < 26; ++i) {
            float x = rnd(-77.0f, -7.0f);
            float z = rnd( 7.0f,  77.0f);
            // Stem
            auto stem = CreateGameObject(
                CreateCylinder(0.12f, 0.45f, 8,
                               glm::vec3(0.92f, 0.90f, 0.82f)),
                glm::vec3(x, 0.23f, z));
            stem->SetRoughness(0.75f);
            AddGameObject(stem);
            // Cap (emissive for bloom)
            glm::vec3 cap_col = (i % 3 == 0)
                ? glm::vec3(1.2f, 0.4f, 0.9f)   // pink
                : (i % 3 == 1 ? glm::vec3(0.3f, 1.1f, 1.3f)   // cyan
                               : glm::vec3(1.3f, 0.9f, 0.2f)); // yellow
            auto cap = CreateGameObject(
                CreateCone(0.35f, 0.3f, 10, cap_col),
                glm::vec3(x, 0.55f, z));
            cap->SetEmissive(cap_col * 0.7f);
            AddGameObject(cap);
        }
        // Fallen logs (dynamic — can be rolled)
        for (int i = 0; i < 6; ++i) {
            float x = rnd(-74.0f, -10.0f);
            float z = rnd(10.0f,  74.0f);
            std::string id = "flog_" + std::to_string(i);
            auto log = CreateGameObject(
                CreateCylinder(0.4f, 3.0f, 10, glm::vec3(0.85f, 0.72f, 0.55f)),
                glm::vec3(x, 0.45f, z));
            log->SetRotation(glm::vec3(0, jit(180.0f), 90.0f));
            log->SetTexture(wood_tex); log->SetNormalMap(noise_t);
            log->SetRoughness(0.85f);
            AddGameObject(log);
            namedObjects_[id] = log;
            bodyCmd(id, "box", 2.0f, 0.2f, 0.6f, false);
        }

        // ============================ SAKURA PINK (NE) =================
        // Cherry trees (pink canopy), stone lanterns, blossom petals
        auto cherry = [&](glm::vec3 at, float s) {
            auto trunk = CreateGameObject(
                CreateCylinder(0.22f * s, 2.4f * s, 10,
                               glm::vec3(0.45f, 0.30f, 0.20f)),
                at + glm::vec3(0, 1.2f * s, 0));
            trunk->SetRotation(glm::vec3(jit(4), jit(360), jit(4)));
            trunk->SetTexture(wood_tex); trunk->SetNormalMap(noise_t);
            trunk->SetRoughness(0.85f);
            AddGameObject(trunk);
            // Pink canopy: stacked flattened spheres
            for (int k = 0; k < 3; ++k) {
                float r = 1.6f * s * (1.0f - k * 0.20f);
                auto canopy = CreateGameObject(
                    CreateSphere(24, glm::vec3(1.2f, 0.55f, 0.82f)),
                    at + glm::vec3(jit(0.25f), 2.6f * s + k * 0.9f * s, jit(0.25f)));
                canopy->SetScale(glm::vec3(r, r * 0.75f, r));
                canopy->SetEmissive(glm::vec3(0.25f, 0.10f, 0.18f));
                canopy->SetRoughness(0.85f);
                AddGameObject(canopy);
            }
        };
        for (int i = 0; i < 30; ++i) {
            float x = rnd(  7.0f,  78.0f);
            float z = rnd(  7.0f,  78.0f);
            cherry(glm::vec3(x, 0, z), rnd(0.9f, 1.5f));
        }
        // Stone lanterns (static decorative)
        for (int i = 0; i < 14; ++i) {
            float x = rnd( 10.0f, 75.0f);
            float z = rnd( 10.0f, 75.0f);
            // Base
            auto base = CreateGameObject(
                CreateCube(glm::vec3(0.85f, 0.82f, 0.78f)),
                glm::vec3(x, 0.3f, z));
            base->SetScale(glm::vec3(0.6f, 0.3f, 0.6f));
            base->SetTexture(marble_t); base->SetNormalMap(noise_t);
            base->SetRoughness(0.6f);
            AddGameObject(base);
            // Pillar
            auto pillar = CreateGameObject(
                CreateCylinder(0.18f, 1.2f, 10, glm::vec3(0.88f, 0.85f, 0.80f)),
                glm::vec3(x, 1.05f, z));
            pillar->SetTexture(marble_t); pillar->SetNormalMap(noise_t);
            pillar->SetRoughness(0.55f);
            AddGameObject(pillar);
            // Lamp box (emissive warm)
            auto lamp = CreateGameObject(
                CreateCube(glm::vec3(1.0f)),
                glm::vec3(x, 1.9f, z));
            lamp->SetScale(glm::vec3(0.45f));
            lamp->SetEmissive(glm::vec3(3.0f, 1.9f, 0.7f));
            AddGameObject(lamp);
            // Roof cap
            auto roof = CreateGameObject(
                CreateCone(0.55f, 0.3f, 8, glm::vec3(0.45f, 0.25f, 0.2f)),
                glm::vec3(x, 2.35f, z));
            roof->SetRoughness(0.85f);
            AddGameObject(roof);
        }
        // Dynamic pink rocks and crates to knock around
        for (int i = 0; i < 14; ++i) {
            float x = rnd(  8.0f, 78.0f);
            float z = rnd(  8.0f, 78.0f);
            bool sph = (i % 2 == 0);
            if (sph) {
                addSphere("srk_" + std::to_string(i),
                          glm::vec3(x, 1.0f, z), rnd(0.5f, 0.9f),
                          glm::vec3(0.95f, 0.80f, 0.85f),
                          rock_tex, noise_t,
                          0.0f, 0.8f,
                          1.0f, 0.35f, 0.4f);
            } else {
                addBox("sbox_" + std::to_string(i),
                       glm::vec3(x, 0.6f, z), glm::vec3(rnd(0.9f, 1.3f)),
                       glm::vec3(1.1f, 0.65f, 0.85f),
                       marble_t, noise_t, 0.0f, 0.55f,
                       0.9f, 0.15f, 0.5f,
                       glm::vec3(jit(15), jit(180), jit(15)));
            }
        }
        // Floating pink petals (purely decorative, no physics)
        for (int i = 0; i < 90; ++i) {
            float x = rnd( 6.0f, 78.0f);
            float z = rnd( 6.0f, 78.0f);
            float y = rnd(1.5f, 5.0f);
            auto petal = CreateGameObject(
                CreateSphere(6, glm::vec3(1.4f, 0.6f, 0.85f)),
                glm::vec3(x, y, z));
            petal->SetScale(glm::vec3(0.08f, 0.04f, 0.08f));
            petal->SetEmissive(glm::vec3(0.45f, 0.15f, 0.25f));
            AddGameObject(petal);
        }

        // ============================ DESERT (SW) ======================
        // Cacti + sand boulders + a small pyramid
        for (int i = 0; i < 12; ++i) {
            float x = rnd(-77.0f, -7.0f);
            float z = rnd(-77.0f, -7.0f);
            std::string id = "cactus_" + std::to_string(i);
            float h = rnd(2.0f, 3.4f);
            auto c = CreateGameObject(
                CreateCylinder(0.35f, h, 8, glm::vec3(0.30f, 0.55f, 0.32f)),
                glm::vec3(x, h * 0.5f, z));
            c->SetRoughness(0.8f);
            c->SetNormalMap(noise_t);
            AddGameObject(c);
            namedObjects_[id] = c;
            bodyCmd(id, "box", 1.5f, 0.2f, 0.5f, false);
            // Arm
            if (i % 2 == 0) {
                auto arm = CreateGameObject(
                    CreateCylinder(0.2f, 1.2f, 8, glm::vec3(0.32f, 0.56f, 0.33f)),
                    glm::vec3(x + 0.45f, h * 0.6f, z));
                arm->SetRotation(glm::vec3(0, 0, 60.0f));
                arm->SetRoughness(0.8f);
                AddGameObject(arm);
            }
        }
        // Sand boulders (physics)
        for (int i = 0; i < 16; ++i) {
            float x = rnd(-78.0f, -6.0f);
            float z = rnd(-78.0f, -6.0f);
            float s = rnd(0.5f, 1.3f);
            addSphere("drock_" + std::to_string(i),
                      glm::vec3(x, s, z), s,
                      glm::vec3(0.88f, 0.72f, 0.50f),
                      rock_tex, noise_t,
                      0.0f, 0.85f,
                      1.2f, 0.25f, 0.45f);
        }
        // Step pyramid (larger, static) — signature landmark
        for (int lvl = 0; lvl < 6; ++lvl) {
            float s = 10.0f - lvl * 1.4f;
            addBox("pyr_" + std::to_string(lvl),
                   glm::vec3(-55.0f, 0.9f + lvl * 1.8f, -55.0f),
                   glm::vec3(s, 1.8f, s),
                   glm::vec3(0.92f, 0.76f, 0.55f),
                   dirt_t, noise_t, 0.0f, 0.9f,
                   -1.0f);
        }
        // Obelisk near pyramid
        addBox("desert_obelisk",
               glm::vec3(-45.0f, 5.0f, -58.0f),
               glm::vec3(1.5f, 10.0f, 1.5f),
               glm::vec3(0.95f, 0.82f, 0.65f),
               marble_t, noise_t, 0.0f, 0.5f, -1.0f,
               0.0f, 0.0f, glm::vec3(0, 18.0f, 0));
        // Dune mounds (static)
        for (int i = 0; i < 14; ++i) {
            float x = rnd(-74.0f, -8.0f);
            float z = rnd(-74.0f, -8.0f);
            auto dune = CreateGameObject(
                CreateSphere(20, glm::vec3(1.0f, 0.85f, 0.55f)),
                glm::vec3(x, 0.0f, z));
            dune->SetScale(glm::vec3(4.0f, 1.0f, 3.5f));
            dune->SetTexture(dirt_t); dune->SetNormalMap(noise_t);
            dune->SetRoughness(0.95f);
            AddGameObject(dune);
        }

        // ============================ TECH (SE) =======================
        // Chrome pillars + emissive cyan bars + crate stack
        for (int i = 0; i < 14; ++i) {
            float ang = (float)i / 14.0f * 2 * M_PI;
            float R = 25.0f;
            float x = 40.0f + std::cos(ang) * R;
            float z = -40.0f + std::sin(ang) * R;
            std::string id = "pylon_" + std::to_string(i);
            addBox(id,
                   glm::vec3(x, 3.0f, z), glm::vec3(0.7f, 6.0f, 0.7f),
                   glm::vec3(0.92f),
                   0, 0, 1.0f, 0.15f,
                   -1.0f,
                   0.0f, 0.0f, glm::vec3(0, jit(15.0f), 0));
        }
        // Emissive cyan bars laid horizontally between pylons (decorative)
        for (int i = 0; i < 24; ++i) {
            float x = rnd( 8.0f, 76.0f);
            float z = rnd(-76.0f, -8.0f);
            auto bar = CreateGameObject(
                CreateCube(glm::vec3(1.0f)),
                glm::vec3(x, 0.15f, z));
            bar->SetScale(glm::vec3(rnd(1.2f, 2.4f), 0.05f, 0.2f));
            bar->SetRotation(glm::vec3(0, jit(180.0f), 0));
            bar->SetEmissive(glm::vec3(0.1f, 2.5f, 3.0f));
            AddGameObject(bar);
        }
        // Tech crate pyramid (dynamic)
        for (int y = 0; y < 4; ++y) {
            int n = 4 - y;
            for (int i = 0; i < n; ++i) {
                float x = 18.0f + i * 1.1f + (3 - n) * 0.55f;
                float yp = 0.55f + y * 1.1f;
                addBox("tcrate_" + std::to_string(y * 4 + i),
                       glm::vec3(x, yp, -18.0f), glm::vec3(1.0f),
                       glm::vec3(0.85f, 0.88f, 0.95f),
                       checker_t, noise_t, 0.85f, 0.25f,
                       0.9f, 0.3f, 0.45f);
            }
        }
        // Chrome spheres scattered (dynamic)
        for (int i = 0; i < 6; ++i) {
            float x = rnd( 8.0f, 76.0f);
            float z = rnd(-76.0f, -8.0f);
            std::string col_id = "tsph_" + std::to_string(i);
            addSphere(col_id,
                      glm::vec3(x, 0.7f, z), 0.6f,
                      glm::vec3(0.95f, 0.95f, 0.95f),
                      0, 0, 1.0f, 0.25f,
                      1.3f, 0.55f, 0.2f);
        }
        // Glowing monolith — signature tech landmark
        addBox("tech_obelisk",
               glm::vec3(65.0f, 6.0f, -65.0f), glm::vec3(1.6f, 12.0f, 1.6f),
               glm::vec3(0.92f, 0.94f, 1.0f),
               0, 0, 1.0f, 0.1f, -1.0f,
               0.0f, 0.0f, glm::vec3(0, 30.0f, 0),
               glm::vec3(0.5f, 1.8f, 3.0f));
        // Secondary glowing pillar
        addBox("tech_spire",
               glm::vec3(25.0f, 8.0f, -60.0f), glm::vec3(0.8f, 16.0f, 0.8f),
               glm::vec3(0.95f, 0.96f, 1.0f),
               0, 0, 1.0f, 0.08f, -1.0f,
               0.0f, 0.0f, glm::vec3(0),
               glm::vec3(1.5f, 0.4f, 3.5f));

        // ============================ CENTER: PHYSICS POOL ============
        // Raised basin (6.0 unit radius, wall height ~0.9) built from
        // a ring of static stone blocks. Water mesh is already set via
        // the "water" command with bounds_radius=5.0 so buoyancy only
        // kicks in inside the ring.
        for (int i = 0; i < 24; ++i) {
            float ang = (float)i / 24.0f * 2 * M_PI;
            float R = 5.8f;
            float x = std::cos(ang) * R;
            float z = std::sin(ang) * R;
            std::string id = "poolw_" + std::to_string(i);
            addBox(id,
                   glm::vec3(x, 0.5f, z), glm::vec3(0.9f, 1.0f, 0.9f),
                   glm::vec3(0.82f, 0.78f, 0.74f),
                   marble_t, noise_t, 0.0f, 0.55f, -1.0f,  // static
                   0.0f, 0.0f, glm::vec3(jit(5), ang * 57.3f + jit(10), jit(5)));
        }
        // Stepping-stone paths from pool toward each quadrant
        for (int q = 0; q < 4; ++q) {
            float dx = (q & 1) ? 1.0f : -1.0f;
            float dz = (q & 2) ? -1.0f : 1.0f;
            for (int s = 0; s < 6; ++s) {
                float x = dx * (8.0f + s * 3.5f);
                float z = dz * (8.0f + s * 3.5f);
                auto stone = CreateGameObject(
                    CreateCube(glm::vec3(0.82f, 0.78f, 0.74f)),
                    glm::vec3(x, 0.05f, z));
                stone->SetScale(glm::vec3(1.6f, 0.1f, 1.6f));
                stone->SetTexture(marble_t); stone->SetNormalMap(noise_t);
                stone->SetRoughness(0.55f);
                AddGameObject(stone);
            }
        }

        // --- Level border: dense mountain ring just outside the 160x160 map.
        // 56 cones at R=90 with overlapping footprints form an impassable wall.
        for (int i = 0; i < 56; ++i) {
            float ang = (float)i / 56.0f * 2 * M_PI + jit(0.03f);
            float R = 90.0f + jit(4.0f);
            float x = std::cos(ang) * R;
            float z = std::sin(ang) * R;
            float sc = rnd(1.0f, 1.5f);
            float h = 34.0f * sc;
            auto m = CreateGameObject(
                CreateCone(14.0f * sc, h, 10, glm::vec3(0.22f, 0.20f, 0.25f)),
                glm::vec3(x, h * 0.5f, z));
            m->SetRoughness(0.92f);
            AddGameObject(m);
        }
        // Second, farther mountain ring for depth (behind the border)
        for (int i = 0; i < 24; ++i) {
            float ang = (float)i / 24.0f * 2 * M_PI + jit(0.06f);
            float R = 140.0f + jit(15.0f);
            float x = std::cos(ang) * R;
            float z = std::sin(ang) * R;
            float sc = rnd(1.2f, 1.8f);
            float h = 40.0f * sc;
            auto m = CreateGameObject(
                CreateCone(18.0f * sc, h, 10, glm::vec3(0.18f, 0.17f, 0.22f)),
                glm::vec3(x, h * 0.5f, z));
            AddGameObject(m);
        }

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

        // First-person arm: 3 GameObjects, repositioned from camera basis each frame
        BuildArm();

        // Spawn player on forest-side walkway, feet on ground, looking SE toward pool.
        float spawn_x = -8.0f, spawn_z = 14.0f;
        float foot_y = SampleTerrainH(spawn_x, spawn_z);
        glm::vec3 eye(spawn_x, foot_y + kEyeH, spawn_z);
        glm::vec3 target = eye + glm::vec3(1.0f, -0.2f, -1.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;
        grounded_ = true;

        std::cout << "Four-Map ready: " << game_objects_.size() << " objects.\n"
                  << "  NW Forest | NE Sakura (hills) | SW Desert (dunes) | SE Tech | Center: pool\n"
                  << "  WASD walk, mouse/arrows look, Space jump, LShift sprint. F2 toggles fly.\n"
                  << "  F / LMB / R1 to throw balls. Mountain ring blocks escape.\n";
        return true;
    }

    void BuildArm() {
        arm_upper_ = CreateGameObject(
            CreateCylinder(0.10f, 0.45f, 10, glm::vec3(0.92f, 0.85f, 0.78f)),
            glm::vec3(0));
        arm_upper_->SetRoughness(0.75f);
        AddGameObject(arm_upper_);
        arm_lower_ = CreateGameObject(
            CreateCylinder(0.085f, 0.45f, 10, glm::vec3(0.92f, 0.85f, 0.78f)),
            glm::vec3(0));
        arm_lower_->SetRoughness(0.75f);
        AddGameObject(arm_lower_);
        hand_ = CreateGameObject(
            CreateSphere(18, glm::vec3(0.92f, 0.85f, 0.78f)),
            glm::vec3(0));
        hand_->SetScale(glm::vec3(0.11f));
        hand_->SetRoughness(0.7f);
        AddGameObject(hand_);
        arm_ready_ = true;
    }

    void UpdateArm(float dt) {
        if (!arm_ready_) return;
        throw_anim_ = std::max(0.0f, throw_anim_ - dt * 4.0f);
        glm::vec3 cp = GetCameraPosition();
        glm::vec3 tg = GetCameraTarget();
        glm::vec3 fwd = glm::normalize(tg - cp);
        glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
        glm::vec3 up    = glm::cross(right, fwd);
        float t = throw_anim_;
        float forward_boost = t * 0.45f;
        glm::vec3 shoulder = cp + right * 0.55f + up * -0.40f + fwd * 0.35f;
        glm::vec3 elbow    = shoulder + fwd * (0.18f + forward_boost * 0.4f)
                                      + up    * (-0.20f + t * 0.1f);
        glm::vec3 wrist    = elbow    + fwd * (0.35f + forward_boost)
                                      + up    * (-0.05f + t * 0.3f);
        auto midpoint = [](const glm::vec3& a, const glm::vec3& b) { return (a + b) * 0.5f; };
        auto yawPitch = [&](const glm::vec3& dir) {
            glm::vec3 d = glm::normalize(dir);
            float pitch = glm::degrees(std::asin(d.y));
            float yaw   = glm::degrees(std::atan2(d.x, d.z));
            return glm::vec3(-pitch - 90.0f, yaw, 0.0f);
        };
        glm::vec3 upperDir = elbow - shoulder;
        glm::vec3 lowerDir = wrist - elbow;
        arm_upper_->SetPosition(midpoint(shoulder, elbow));
        arm_upper_->SetRotation(yawPitch(upperDir));
        arm_upper_->SetScale(glm::vec3(1.0f, glm::length(upperDir) / 0.45f, 1.0f));
        arm_lower_->SetPosition(midpoint(elbow, wrist));
        arm_lower_->SetRotation(yawPitch(lowerDir));
        arm_lower_->SetScale(glm::vec3(1.0f, glm::length(lowerDir) / 0.45f, 1.0f));
        hand_->SetPosition(wrist);
    }

    // Per-zone atmosphere recipes — blended each frame based on camera XZ.
    struct Zone {
        glm::vec3 sun, zenith, horizon, sky, ground;
        float fog, bloom_i, bloom_t, expo;
    };
    void UpdateAtmosphere() {
        glm::vec3 cp = GetCameraPosition();
        float tx = glm::clamp((cp.x + 40.0f) / 80.0f, 0.0f, 1.0f);
        float tz = glm::clamp((cp.z + 40.0f) / 80.0f, 0.0f, 1.0f);
        float wF = (1.0f - tx) * tz;         // NW forest
        float wS =        tx  * tz;          // NE sakura
        float wD = (1.0f - tx) * (1.0f - tz); // SW desert
        float wT =        tx  * (1.0f - tz); // SE tech

        Zone Zf{ glm::vec3(2.0f, 1.85f, 1.55f), glm::vec3(0.20f, 0.40f, 0.55f),
                 glm::vec3(0.72f, 0.82f, 0.74f), glm::vec3(0.55f, 0.72f, 0.62f),
                 glm::vec3(0.08f, 0.12f, 0.08f), 0.004f, 1.0f,  1.25f, 0.95f };
        Zone Zs{ glm::vec3(2.20f, 1.75f, 1.85f), glm::vec3(0.36f, 0.45f, 0.66f),
                 glm::vec3(1.15f, 0.74f, 0.86f), glm::vec3(0.92f, 0.70f, 0.80f),
                 glm::vec3(0.10f, 0.06f, 0.10f), 0.003f, 1.6f,  1.05f, 1.00f };
        Zone Zd{ glm::vec3(2.45f, 1.75f, 1.25f), glm::vec3(0.25f, 0.34f, 0.55f),
                 glm::vec3(1.05f, 0.74f, 0.48f), glm::vec3(0.92f, 0.70f, 0.48f),
                 glm::vec3(0.14f, 0.10f, 0.06f), 0.006f, 0.75f, 1.30f, 0.95f };
        Zone Zt{ glm::vec3(1.60f, 1.95f, 2.25f), glm::vec3(0.04f, 0.10f, 0.25f),
                 glm::vec3(0.20f, 0.40f, 0.65f), glm::vec3(0.22f, 0.38f, 0.58f),
                 glm::vec3(0.04f, 0.06f, 0.10f), 0.010f, 2.00f, 0.95f, 0.92f };

        auto blend = [&](auto Zf_v, auto Zs_v, auto Zd_v, auto Zt_v) {
            return wF * Zf_v + wS * Zs_v + wD * Zd_v + wT * Zt_v;
        };
        sun_color_      = blend(Zf.sun,     Zs.sun,     Zd.sun,     Zt.sun);
        zenith_color_   = blend(Zf.zenith,  Zs.zenith,  Zd.zenith,  Zt.zenith);
        horizon_color_  = blend(Zf.horizon, Zs.horizon, Zd.horizon, Zt.horizon);
        sky_color_      = blend(Zf.sky,     Zs.sky,     Zd.sky,     Zt.sky);
        ground_color_   = blend(Zf.ground,  Zs.ground,  Zd.ground,  Zt.ground);
        fog_density_    = blend(Zf.fog,     Zs.fog,     Zd.fog,     Zt.fog);
        bloom_intensity_= blend(Zf.bloom_i, Zs.bloom_i, Zd.bloom_i, Zt.bloom_i);
        bloom_threshold_= blend(Zf.bloom_t, Zs.bloom_t, Zd.bloom_t, Zt.bloom_t);
        exposure_       = blend(Zf.expo,    Zs.expo,    Zd.expo,    Zt.expo);
    }

    void FireBall() {
        glm::vec3 cp = GetCameraPosition();
        glm::vec3 tg = GetCameraTarget();
        glm::vec3 fwd = glm::normalize(tg - cp);
        if ((int)active_shots_.size() >= kMaxShots) {
            auto old = active_shots_.front(); active_shots_.pop_front();
            processAgentCommand(json::parse("{\"action\":\"delete\",\"id\":\"" + old + "\"}"));
        }
        std::uniform_real_distribution<float> cd(0.7f, 1.7f);
        glm::vec3 col(cd(rng_), cd(rng_), cd(rng_));
        std::string id = "shot_" + std::to_string(shot_counter_++);
        auto b = CreateGameObject(CreateSphere(20, col), cp + fwd * 1.4f);
        b->SetScale(glm::vec3(0.32f));
        b->SetMetallic(1.0f);
        b->SetRoughness(0.25f);
        AddGameObject(b);
        namedObjects_[id] = b;
        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.2);
        bc["restitution"]=json::Value(0.6);
        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)),
            json::Value((double)(fwd.y * 55)),
            json::Value((double)(fwd.z * 55))});
        processAgentCommand(json::Value(vc));
        active_shots_.push_back(id);
    }

    void OnUpdate(float dt) override {
        UpdateAtmosphere();  // blend zone palettes each frame

        shot_cooldown_ -= dt;
        if (shot_cooldown_ <= 0.0f) {
            bool fire = Input::IsKeyHeld(GLFW_KEY_F)
                     || Input::IsMouseButtonPressed(Input::MouseButton::Left)
                     || Input::IsGamepadButtonDown(Input::GamepadButton::R1);
            if (fire) { FireBall(); shot_cooldown_ = 0.15f; }
        }

        // --- Input gathering -------------------------------------------------
        glm::vec3 eng_pos = GetCameraPosition();
        glm::vec2 ls{0}, rs{0};
        float up_in = 0, down_in = 0;
        bool sprint = false, jump_now = false, fly_toggle = false;

        if (Input::IsGamepadConnected()) {
            ls = Input::GetLeftStick();
            rs = Input::GetRightStick();
            up_in = Input::GetR2();
            down_in = Input::GetL2();
            if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross)) sprint = true;
            if (Input::IsGamepadButtonDown(Input::GamepadButton::Circle)) jump_now = true;
        }
        if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT)) sprint = true;
        if (Input::IsKeyHeld(GLFW_KEY_W))     ls.y -= 1.0f;
        if (Input::IsKeyHeld(GLFW_KEY_S))     ls.y += 1.0f;
        if (Input::IsKeyHeld(GLFW_KEY_A))     ls.x -= 1.0f;
        if (Input::IsKeyHeld(GLFW_KEY_D))     ls.x += 1.0f;
        if (Input::IsKeyHeld(GLFW_KEY_LEFT))  rs.x -= 1.0f;
        if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) rs.x += 1.0f;
        if (Input::IsKeyHeld(GLFW_KEY_UP))    rs.y -= 1.0f;
        if (Input::IsKeyHeld(GLFW_KEY_DOWN))  rs.y += 1.0f;
        if (Input::IsKeyHeld(GLFW_KEY_SPACE)) jump_now = true;
        if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL)) down_in += 1.0f;

        bool fly_pressed_now = Input::IsKeyHeld(GLFW_KEY_F2);
        if (fly_pressed_now && !fly_prev_) { fly_mode_ = !fly_mode_; vy_ = 0.0f; }
        fly_prev_ = fly_pressed_now;

        // --- Look update -----------------------------------------------------
        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 fwd_h   = glm::normalize(glm::vec3(fwd.x, 0.0f, fwd.z));
        glm::vec3 right_h = glm::normalize(glm::vec3(right.x, 0.0f, right.z));

        // --- Movement --------------------------------------------------------
        glm::vec3 pos = eng_pos;

        if (fly_mode_) {
            // Free-fly for debug / fast traversal
            float speed = 22.0f * (sprint ? 3.0f : 1.0f);
            pos += fwd  * (-ls.y) * speed * dt;
            pos += right *  ls.x   * speed * dt;
            pos.y += (up_in - down_in) * speed * dt;
        } else {
            // Grounded walking
            float speed = kWalkSpeed * (sprint ? kRunMult : 1.0f);
            glm::vec3 horiz = fwd_h * (-ls.y) * speed * dt
                            + right_h *  ls.x * speed * dt;

            // Slope test: project candidate x/z, sample ground height
            glm::vec3 cand = pos + horiz;
            cand.x = glm::clamp(cand.x, -kMapHalf, kMapHalf);
            cand.z = glm::clamp(cand.z, -kMapHalf, kMapHalf);
            float foot_now  = pos.y - kEyeH;
            float foot_cand = SampleTerrainH(cand.x, cand.z);

            if (grounded_ && (foot_cand - foot_now) > kStepUp) {
                // Too steep — block horizontal motion this frame
                cand.x = pos.x;
                cand.z = pos.z;
                foot_cand = SampleTerrainH(cand.x, cand.z);
            }

            // Vertical: jump + gravity
            if (jump_now && !jump_prev_ && grounded_) {
                vy_ = kJumpV;
                grounded_ = false;
            }
            if (!grounded_) {
                vy_ -= kGravity * dt;
                cand.y = pos.y + vy_ * dt;
                if (cand.y <= foot_cand + kEyeH) {
                    cand.y = foot_cand + kEyeH;
                    vy_ = 0.0f;
                    grounded_ = true;
                }
            } else {
                // Stick to ground (walk up/down gentle slopes smoothly)
                cand.y = foot_cand + kEyeH;
                vy_ = 0.0f;
                // Detect walking off a ledge
                if (foot_cand < foot_now - 0.25f) grounded_ = false;
            }
            pos = cand;
        }
        jump_prev_ = jump_now;

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

        // First-person arm
        UpdateArm(dt);
    }
};


// ---------------------------------------------
// PhysicsPlaygroundDemo — expansive showcase scene.
//
// Zones:
//   CENTER — chrome monolith, 10 pulsing emissive bollards, Jenga tower,
//            loose props (HDR flame in the middle)
//   NORTH  — 7×7 PBR material grid (metallic × roughness) on pedestals
//   EAST   — ramp + 40-domino cascade + 10 bowling pins. Chain fires at t=2s.
//   WEST   — catapult frame + target crate stack. Periodic launch every 18s.
//   SOUTH  — cliff, waterfall, lake with flow current + floating barrels
//
// Interactive:
//   F / LMB / R1      → shoot metallic HDR ball from camera
//   T                 → cycle storm state (calm → squall → gale)
//   G                 → chaos: apply random upward impulse to every dynamic body
//
// Time-driven:
//   t=2s              → domino kickoff ball fires
//   every 18s         → catapult hurls a heavy ball at the arena
//   every frame       → waterfall droplet stream; bollards pulse via SetEmissive
// ---------------------------------------------
class PhysicsPlaygroundDemo : public Engine {
    float cam_yaw_   = 200.0f;
    float cam_pitch_ = -8.0f;
    float cam_speed_ = 22.0f;
    float look_sensitivity_ = 80.0f;
    glm::vec3 last_cam_pos_{0};

    float time_           = 0.0f;
    float shot_cooldown_  = 0.0f;
    float drop_timer_     = 0.0f;
    float catapult_timer_ = 12.0f;  // first fire at ~12s
    bool  kickoff_fired_  = false;
    int   storm_state_    = 0;
    float t_down_prev_    = false;
    float g_down_prev_    = false;

    std::mt19937 rng_{2024};
    std::mt19937 drop_rng_{2025};
    int shot_counter_ = 0;
    int drop_counter_ = 0;
    int cata_counter_ = 0;
    std::deque<std::string> active_shots_;
    std::deque<std::string> active_drops_;
    std::deque<std::string> active_cata_;
    std::vector<std::string> bollard_ids_;

    static constexpr int kMaxShots = 80;
    static constexpr int kMaxDrops = 120;
    static constexpr int kMaxCata  = 5;

public:
    PhysicsPlaygroundDemo()
      : Engine(1920, 1080, "ZoomEngine - Physics Playground") {}

    // --- Placement helpers --------------------------------------------------
    void 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));
    }
    std::shared_ptr<GameObject> addBox(const std::string& id, glm::vec3 pos, glm::vec3 scale,
                                       glm::vec3 color, GLuint tex, GLuint nmap,
                                       float metallic, float roughness, float mass,
                                       float rest = 0.2f, float frict = 0.6f,
                                       glm::vec3 rot = glm::vec3(0)) {
        auto obj = CreateGameObject(CreateCube(color), pos);
        obj->SetScale(scale);
        obj->SetRotation(rot);
        if (tex) obj->SetTexture(tex);
        if (nmap) obj->SetNormalMap(nmap);
        obj->SetMetallic(metallic);
        obj->SetRoughness(roughness);
        AddGameObject(obj);
        if (!id.empty()) namedObjects_[id] = obj;
        if (mass > 0)       bodyCmd(id, "box", mass, rest, frict, false);
        else if (mass == -1.0f) bodyCmd(id, "box", 0,    rest, frict, true);
        return obj;
    }
    std::shared_ptr<GameObject> addSphere(const std::string& id, glm::vec3 pos, float r,
                                          glm::vec3 color, GLuint tex, GLuint nmap,
                                          float metallic, float roughness, float mass,
                                          float rest = 0.4f, float frict = 0.3f) {
        auto obj = CreateGameObject(CreateSphere(32, color), pos);
        obj->SetScale(glm::vec3(r));
        if (tex) obj->SetTexture(tex);
        if (nmap) obj->SetNormalMap(nmap);
        obj->SetMetallic(metallic);
        obj->SetRoughness(roughness);
        AddGameObject(obj);
        if (!id.empty()) namedObjects_[id] = obj;
        if (mass > 0) bodyCmd(id, "sphere", mass, rest, frict, false);
        return obj;
    }

    // --- Init ---------------------------------------------------------------
    bool OnInitialize() override {
        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); };

        // --- Sky + HDR tuning (clear-day base; storm toggle can darken later) ---
        processAgentCommand(json::parse(R"({
            "action":"sun",
            "direction":[-0.45, 0.55, -0.30],
            "color":[2.0, 1.55, 1.15],
            "zenith":[0.22, 0.40, 0.70],
            "horizon":[0.85, 0.75, 0.62],
            "sky":[0.60, 0.68, 0.78],
            "ground":[0.08, 0.09, 0.10],
            "shadow_extent":110,
            "exposure":0.95,
            "bloom_threshold":1.15,
            "bloom_intensity":1.1,
            "fog_density":0.007
        })"));

        // Primary water — the lake in the south zone; flows west so floats drift
        processAgentCommand(json::parse(R"({
            "action":"water","enabled":true,
            "level":-0.25,"size":200,
            "amplitude":0.14,"wavelength":10,"speed":0.8,
            "shallow":[0.42, 0.58, 0.58],"deep":[0.03, 0.10, 0.18],
            "density":1.0,"resolution":160,
            "flow_dir":[-1, 0, 0],"flow_speed":1.5
        })"));
        processAgentCommand(json::parse(R"({
            "action":"weather","wind_dir":[-1,0,0.2],
            "wind_speed":0.5,"storminess":0.0,"current_coef":0.4
        })"));

        GLuint grass_tex = TextureCache::Get("grass");
        GLuint rock_tex  = TextureCache::Get("rock");
        GLuint wood_tex  = TextureCache::Get("wood");
        GLuint marble_t  = TextureCache::Get("marble");
        GLuint concrete  = TextureCache::Get("concrete");
        GLuint brick_t   = TextureCache::Get("brick");
        GLuint noise_t   = TextureCache::Get("noise");
        GLuint dirt_t    = TextureCache::Get("dirt");

        // --- Ground (two strips, leaving the lake channel open to south) ---
        auto north_grass = CreateGameObject(
            CreatePlane(220.0f, 120.0f, glm::vec3(0.88f, 0.92f, 0.80f)),
            glm::vec3(0, 0, 45.0f));
        north_grass->SetTexture(grass_tex); north_grass->SetNormalMap(noise_t);
        north_grass->SetUVScale(glm::vec2(0.22f)); north_grass->SetRoughness(0.95f);
        AddGameObject(north_grass);

        auto center_stone = CreateGameObject(
            CreatePlane(80.0f, 40.0f, glm::vec3(0.78f, 0.78f, 0.76f)),
            glm::vec3(0, 0.01f, 0));
        center_stone->SetTexture(marble_t); center_stone->SetNormalMap(noise_t);
        center_stone->SetUVScale(glm::vec2(0.2f)); center_stone->SetRoughness(0.45f);
        AddGameObject(center_stone);

        auto west_dirt = CreateGameObject(
            CreatePlane(80.0f, 40.0f, glm::vec3(0.68f, 0.62f, 0.50f)),
            glm::vec3(-50.0f, 0, 0));
        west_dirt->SetTexture(dirt_t); west_dirt->SetNormalMap(noise_t);
        west_dirt->SetUVScale(glm::vec2(0.25f)); west_dirt->SetRoughness(0.95f);
        AddGameObject(west_dirt);

        auto east_stone = CreateGameObject(
            CreatePlane(80.0f, 40.0f, glm::vec3(0.72f, 0.70f, 0.66f)),
            glm::vec3(50.0f, 0, 0));
        east_stone->SetTexture(concrete); east_stone->SetNormalMap(noise_t);
        east_stone->SetUVScale(glm::vec2(0.22f)); east_stone->SetRoughness(0.75f);
        AddGameObject(east_stone);

        // -------------------- CENTRAL ARENA ------------------------------
        // Chrome monolith centerpiece
        addBox("monolith",
               glm::vec3(0, 6.0f, 0), glm::vec3(1.4f, 12.0f, 1.4f),
               glm::vec3(0.96f), 0, 0, 1.0f, 0.12f, -1.0f);  // static

        // 10 pulsing emissive bollards in a ring around the monolith
        for (int i = 0; i < 10; ++i) {
            float ang = (float)i / 10.0f * 2 * M_PI;
            float R = 6.0f;
            std::string id = "boll_" + std::to_string(i);
            auto boll = CreateGameObject(
                CreateCylinder(0.25f, 1.6f, 10, glm::vec3(0.9f, 0.9f, 0.9f)),
                glm::vec3(std::cos(ang) * R, 0.8f, std::sin(ang) * R));
            boll->SetMetallic(0.9f);
            boll->SetRoughness(0.3f);
            boll->SetEmissive(glm::vec3(1.5f, 0.6f, 0.2f));  // initial; pulses
            AddGameObject(boll);
            namedObjects_[id] = boll;
            bollard_ids_.push_back(id);
        }

        // Jenga tower — 5 levels × 3 boxes, alternating orientation
        for (int lvl = 0; lvl < 5; ++lvl) {
            for (int i = 0; i < 3; ++i) {
                bool along_x = (lvl % 2 == 0);
                glm::vec3 pos, scale;
                if (along_x) {
                    pos = glm::vec3(8.0f + (i - 1) * 0.7f, 0.35f + lvl * 0.7f, 6.0f);
                    scale = glm::vec3(0.6f, 0.6f, 2.0f);
                } else {
                    pos = glm::vec3(8.0f, 0.35f + lvl * 0.7f, 6.0f + (i - 1) * 0.7f);
                    scale = glm::vec3(2.0f, 0.6f, 0.6f);
                }
                addBox("jen_" + std::to_string(lvl * 3 + i),
                       pos, scale, glm::vec3(0.92f, 0.86f, 0.72f),
                       wood_tex, noise_t, 0.0f, 0.65f,
                       0.7f, 0.08f, 0.55f);
            }
        }

        // HDR pyre in the monolith's shadow — signature bloom source
        {
            auto logA = CreateGameObject(
                CreateCylinder(0.18f, 1.3f, 8, glm::vec3(0.25f, 0.16f, 0.08f)),
                glm::vec3(-6.0f, 0.2f, 0.0f));
            logA->SetRotation(glm::vec3(0, 30.0f, 90.0f));
            logA->SetTexture(wood_tex); logA->SetNormalMap(noise_t);
            AddGameObject(logA);
            auto logB = CreateGameObject(
                CreateCylinder(0.18f, 1.3f, 8, glm::vec3(0.27f, 0.18f, 0.1f)),
                glm::vec3(-6.0f, 0.25f, 0.0f));
            logB->SetRotation(glm::vec3(0, -30.0f, 90.0f));
            logB->SetTexture(wood_tex); logB->SetNormalMap(noise_t);
            AddGameObject(logB);
            auto flame1 = CreateGameObject(
                CreateCone(0.45f, 1.4f, 10, glm::vec3(1.0f)),
                glm::vec3(-6.0f, 0.9f, 0.0f));
            flame1->SetEmissive(glm::vec3(5.5f, 2.0f, 0.4f));
            AddGameObject(flame1);
            auto flame2 = CreateGameObject(
                CreateCone(0.22f, 0.8f, 10, glm::vec3(1.0f)),
                glm::vec3(-6.0f, 1.05f, 0.0f));
            flame2->SetEmissive(glm::vec3(9.0f, 4.0f, 1.5f));
            AddGameObject(flame2);
        }

        // -------------------- NORTH: PBR MATERIAL GRID ------------------
        const int N = 7;
        glm::vec3 material_base(0.88f, 0.32f, 0.18f);  // red-orange showpiece
        for (int row = 0; row < N; ++row) {
            for (int col = 0; col < N; ++col) {
                float m = (float)row / (N - 1);
                float r = (float)col / (N - 1) * 0.92f + 0.05f;
                float xx = -9.0f + col * 3.0f;
                float zz = 25.0f + row * 3.5f;
                // Pedestal
                auto ped = CreateGameObject(
                    CreateCube(glm::vec3(0.80f, 0.77f, 0.72f)),
                    glm::vec3(xx, 0.6f, zz));
                ped->SetScale(glm::vec3(1.3f, 1.2f, 1.3f));
                ped->SetTexture(marble_t); ped->SetNormalMap(noise_t);
                ped->SetRoughness(0.45f);
                AddGameObject(ped);
                // Sphere on top
                auto sph = CreateGameObject(
                    CreateSphere(48, material_base),
                    glm::vec3(xx, 1.9f, zz));
                sph->SetScale(glm::vec3(0.95f));
                sph->SetMetallic(m);
                sph->SetRoughness(r);
                AddGameObject(sph);
            }
        }
        // Backdrop wall behind the gallery
        for (int i = 0; i < 10; ++i) {
            float xx = -13.5f + i * 3.0f;
            auto b = CreateGameObject(
                CreateCube(glm::vec3(0.82f, 0.78f, 0.72f)),
                glm::vec3(xx, 3.5f, 50.0f));
            b->SetScale(glm::vec3(3.0f, 7.0f, 1.2f));
            b->SetTexture(brick_t); b->SetNormalMap(noise_t);
            b->SetRoughness(0.82f);
            AddGameObject(b);
        }

        // -------------------- EAST: DOMINOES + BOWLING ------------------
        // Ramp for kickoff ball at far east
        addBox("ramp",
               glm::vec3(45.0f, 2.0f, 0.0f), glm::vec3(7.0f, 0.3f, 3.0f),
               glm::vec3(0.85f, 0.78f, 0.65f),
               wood_tex, noise_t, 0.0f, 0.7f, -1.0f,
               0.0f, 0.0f, glm::vec3(0, 0, -20.0f));

        // 40-domino straight line west from x=38 to x=6
        for (int i = 0; i < 40; ++i) {
            float x = 38.0f - i * 0.82f;
            addBox("dom_" + std::to_string(i),
                   glm::vec3(x, 0.9f, 0.0f), glm::vec3(0.2f, 1.8f, 0.95f),
                   glm::vec3(0.92f, 0.88f, 0.75f),
                   wood_tex, noise_t, 0.0f, 0.6f,
                   0.8f, 0.07f, 0.55f);
        }

        // Bowling pins at west end of domino line
        static const float pin_off[10][2] = {
            { 0.0f, 0.0f},
            {-0.45f, 0.9f}, { 0.45f, 0.9f},
            {-0.9f, 1.8f}, { 0.0f, 1.8f}, { 0.9f, 1.8f},
            {-1.35f, 2.7f}, {-0.45f, 2.7f}, { 0.45f, 2.7f}, { 1.35f, 2.7f}
        };
        for (int i = 0; i < 10; ++i) {
            glm::vec3 p(4.0f - pin_off[i][1], 0.8f, pin_off[i][0]);
            auto pin = CreateGameObject(
                CreateCylinder(0.32f, 1.5f, 14,
                               glm::vec3(0.97f, 0.96f, 0.92f)),
                p);
            pin->SetMetallic(0.0f);
            pin->SetRoughness(0.3f);
            AddGameObject(pin);
            std::string id = "pin_" + std::to_string(i);
            namedObjects_[id] = pin;
            bodyCmd(id, "box", 0.4f, 0.2f, 0.4f, false);
        }

        // Impulse ball on top of the ramp — fires at t=2
        addSphere("kickoff",
                  glm::vec3(48.0f, 5.5f, 0.0f), 0.85f,
                  glm::vec3(0.75f, 0.78f, 0.82f), 0, 0,
                  1.0f, 0.28f,
                  9.0f, 0.3f, 0.25f);

        // -------------------- WEST: CATAPULT FRAME -----------------------
        // Tall wooden A-frame made of stacked boxes (purely decorative static)
        auto postL = CreateGameObject(
            CreateCube(glm::vec3(0.58f, 0.40f, 0.24f)),
            glm::vec3(-45.0f, 4.0f, 3.0f));
        postL->SetScale(glm::vec3(0.6f, 8.0f, 0.6f));
        postL->SetRotation(glm::vec3(0, 0, 12.0f));
        postL->SetTexture(wood_tex); postL->SetNormalMap(noise_t);
        postL->SetRoughness(0.8f);
        AddGameObject(postL);
        auto postR = CreateGameObject(
            CreateCube(glm::vec3(0.58f, 0.40f, 0.24f)),
            glm::vec3(-45.0f, 4.0f, -3.0f));
        postR->SetScale(glm::vec3(0.6f, 8.0f, 0.6f));
        postR->SetRotation(glm::vec3(0, 0, -12.0f));
        postR->SetTexture(wood_tex); postR->SetNormalMap(noise_t);
        postR->SetRoughness(0.8f);
        AddGameObject(postR);
        auto crossbeam = CreateGameObject(
            CreateCube(glm::vec3(0.58f, 0.40f, 0.24f)),
            glm::vec3(-45.0f, 8.3f, 0.0f));
        crossbeam->SetScale(glm::vec3(0.8f, 0.8f, 8.0f));
        crossbeam->SetTexture(wood_tex); crossbeam->SetNormalMap(noise_t);
        crossbeam->SetRoughness(0.8f);
        AddGameObject(crossbeam);

        // Target crate pyramid in the center — catapult aims here
        for (int y = 0; y < 3; ++y) {
            int n = 4 - y;
            for (int i = 0; i < n; ++i) {
                float xx = -30.0f + i * 1.05f + (3 - n) * 0.5f;
                float yp = 0.55f + y * 1.05f;
                addBox("ccrate_" + std::to_string(y * 4 + i),
                       glm::vec3(xx, yp, 0.0f), glm::vec3(1.0f),
                       glm::vec3(0.80f, 0.70f, 0.48f),
                       wood_tex, noise_t, 0.0f, 0.7f,
                       0.7f, 0.12f, 0.65f);
            }
        }

        // -------------------- SOUTH: WATERFALL + LAKE -------------------
        // East-west cliff wall at z=-25, spanning x from -24 to 24 (waterfall
        // source). Height y=0..5.4. Lake is south (z<-25), arena is north.
        for (int i = 0; i < 9; ++i) {
            float xx = -24.0f + i * 6.0f;
            auto b = CreateGameObject(
                CreateCube(glm::vec3(0.80f, 0.76f, 0.70f)),
                glm::vec3(xx, 2.7f, -25.0f));
            b->SetScale(glm::vec3(6.2f, 5.4f + jit(0.3f), 4.0f));
            b->SetRotation(glm::vec3(jit(2), jit(6), jit(2)));
            b->SetTexture(rock_tex); b->SetNormalMap(noise_t);
            b->SetRoughness(0.85f);
            AddGameObject(b);
        }
        // Waterfall curtain sheet — along the south face of the cliff
        {
            auto sheet = CreateGameObject(
                CreateCube(glm::vec3(0.75f, 0.90f, 0.98f)),
                glm::vec3(0.0f, 2.6f, -27.0f));
            sheet->SetScale(glm::vec3(50.0f, 5.2f, 0.2f));
            sheet->SetMetallic(0.0f);
            sheet->SetRoughness(0.12f);
            sheet->SetEmissive(glm::vec3(0.5f, 0.8f, 1.0f));
            AddGameObject(sheet);
        }
        // Splash boulders at the foot of the cliff, on the lake side
        for (int i = 0; i < 12; ++i) {
            float xx = rnd(-22.0f, 22.0f);
            float zz = rnd(-32.0f, -27.0f);
            float s  = rnd(0.5f, 1.2f);
            auto r = CreateGameObject(
                CreateSphere(14, glm::vec3(0.86f, 0.82f, 0.78f)),
                glm::vec3(xx, s * 0.35f, zz));
            r->SetScale(glm::vec3(s, s * 0.6f, s * 0.95f));
            r->SetTexture(rock_tex); r->SetNormalMap(noise_t);
            r->SetRoughness(0.7f);
            AddGameObject(r);
        }
        // Lake-side trees
        auto tree = [&](glm::vec3 at, float s) {
            auto trunk = CreateGameObject(
                CreateCylinder(0.22f * s, 2.5f * s, 10,
                               glm::vec3(0.88f, 0.84f, 0.78f)),
                at + glm::vec3(0, 1.25f * 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.22f), rnd(0.38f, 0.55f), rnd(0.13f, 0.20f));
            auto cone = CreateGameObject(
                CreateCone(1.2f * s, 2.7f * s, 10, cc),
                at + glm::vec3(0, 2.9f * s, 0));
            cone->SetNormalMap(noise_t); cone->SetRoughness(0.8f);
            AddGameObject(cone);
        };

        // Floating barrels in the lake (flow carries them west)
        for (int i = 0; i < 6; ++i) {
            float xx = rnd(-10.0f, 5.0f);
            float zz = rnd(-40.0f, -15.0f);
            auto barrel = CreateGameObject(
                CreateCylinder(0.7f, 1.3f, 12, glm::vec3(0.9f, 0.78f, 0.55f)),
                glm::vec3(xx, 1.2f, zz));
            barrel->SetRotation(glm::vec3(0, jit(180.0f), 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;
            bodyCmd(id, "box", 1.0f, 0.3f, 0.4f, false);
        }

        // -------------------- PERIPHERY: trees, rocks, mountains --------
        // Trees scattered around perimeter (avoiding zones)
        for (int i = 0; i < 40; ++i) {
            float x, z;
            int tries = 0;
            do {
                x = rnd(-90.0f, 90.0f);
                z = rnd(-60.0f, 90.0f);
                tries++;
            } while ((std::abs(x) < 50.0f && std::abs(z) < 30.0f) && tries < 20);  // avoid center zones
            tree(glm::vec3(x, 0, z), rnd(0.7f, 1.4f));
        }

        // Scatter boulders
        for (int i = 0; i < 50; ++i) {
            float x, z;
            do {
                x = rnd(-90.0f, 90.0f);
                z = rnd(-60.0f, 90.0f);
            } while (std::abs(x) < 30.0f && std::abs(z) < 25.0f);
            float s = rnd(0.4f, 1.4f);
            auto r = CreateGameObject(
                CreateSphere(12, glm::vec3(0.84f, 0.81f, 0.76f)),
                glm::vec3(x, s * 0.35f, z));
            r->SetScale(glm::vec3(s, s * rnd(0.5f, 0.8f), s * rnd(0.85f, 1.1f)));
            r->SetRotation(glm::vec3(jit(12), jit(180), jit(12)));
            r->SetTexture(rock_tex); r->SetNormalMap(noise_t);
            r->SetRoughness(0.9f);
            AddGameObject(r);
        }

        // Distant mountains
        for (int i = 0; i < 14; ++i) {
            float x = -160.0f + i * 24.0f + jit(6.0f);
            float z = -140.0f + jit(14.0f);
            float s = rnd(0.9f, 1.4f);
            float h = 22.0f * s;
            auto m = CreateGameObject(
                CreateCone(12.0f * s, h, 10, glm::vec3(0.22f, 0.20f, 0.24f)),
                glm::vec3(x, h * 0.5f, z));
            AddGameObject(m);
        }
        // Wildflowers for bloom accents near the arena
        for (int i = 0; i < 80; ++i) {
            float x, z;
            do {
                x = rnd(-50.0f, 50.0f);
                z = rnd(-15.0f, 45.0f);
            } while (std::abs(x) < 16.0f && std::abs(z) < 10.0f);
            glm::vec3 col;
            int pick = (int)(rng_() % 5);
            if      (pick == 0) col = glm::vec3(1.5f, 1.1f, 0.3f);
            else if (pick == 1) col = glm::vec3(1.4f, 0.4f, 0.8f);
            else if (pick == 2) col = glm::vec3(1.2f, 1.2f, 1.1f);
            else if (pick == 3) col = glm::vec3(0.75f, 0.45f, 1.3f);
            else                col = glm::vec3(1.5f, 0.5f, 0.2f);
            auto fl = CreateGameObject(CreateSphere(6, col),
                                       glm::vec3(x, 0.2f + jit(0.05f), z));
            fl->SetScale(glm::vec3(0.08f));
            fl->SetEmissive(col * 0.25f);
            AddGameObject(fl);
        }

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

        // Camera: overlook from southwest, angled at the arena
        glm::vec3 eye(-22.0f, 9.0f, 18.0f);
        glm::vec3 target(4.0f, 3.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 << "Physics Playground ready: " << game_objects_.size() << " objects.\n"
                  << "  F / LMB / R1 — shoot an HDR metal ball\n"
                  << "  T — cycle storm (calm / squall / gale)\n"
                  << "  G — chaos: launch all dynamic bodies upward\n"
                  << "  Every 18s the catapult hurls a heavy sphere at the arena.\n"
                  << "  Domino cascade fires at t=2s.\n";
        return true;
    }

    void FireBall() {
        glm::vec3 cp = GetCameraPosition();
        glm::vec3 tg = GetCameraTarget();
        glm::vec3 fwd = glm::normalize(tg - cp);
        if ((int)active_shots_.size() >= kMaxShots) {
            auto old = active_shots_.front(); active_shots_.pop_front();
            processAgentCommand(json::parse("{\"action\":\"delete\",\"id\":\"" + old + "\"}"));
        }
        std::uniform_real_distribution<float> cd(0.6f, 1.7f);
        glm::vec3 col(cd(rng_), cd(rng_), cd(rng_));
        std::string id = "shot_" + std::to_string(shot_counter_++);
        auto b = CreateGameObject(CreateSphere(20, col), cp + fwd * 1.4f);
        b->SetScale(glm::vec3(0.35f));
        b->SetMetallic(1.0f);
        b->SetRoughness(0.25f);
        AddGameObject(b);
        namedObjects_[id] = b;
        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 * 60)),
            json::Value((double)(fwd.y * 60)),
            json::Value((double)(fwd.z * 60))});
        processAgentCommand(json::Value(vc));
        active_shots_.push_back(id);
    }

    void CycleStorm() {
        storm_state_ = (storm_state_ + 1) % 3;
        float s = storm_state_ == 0 ? 0.0f : (storm_state_ == 1 ? 0.4f : 0.85f);
        json::Object w;
        w["action"]     = json::Value(std::string("weather"));
        w["storminess"] = json::Value((double)s);
        w["wind_dir"]   = json::Value(json::Array{
            json::Value(-1.0), json::Value(0.0), json::Value(0.3)});
        w["wind_speed"] = json::Value((double)(0.5 + s * 4.0));
        processAgentCommand(json::Value(w));
        std::cout << "[storm] state=" << storm_state_ << " storminess=" << s << "\n";
    }

    void ChaosBurst() {
        std::uniform_real_distribution<float> rx(-4.0f, 4.0f);
        std::uniform_real_distribution<float> ry(10.0f, 22.0f);
        std::uniform_real_distribution<float> rr(-6.0f, 6.0f);
        for (auto& [name, handle] : bodyHandleByName_) {
            phys::Body* b = physics_.Get(handle);
            if (!b || b->inv_mass == 0.0f) continue;
            b->linear_velocity  += glm::vec3(rx(rng_), ry(rng_), rx(rng_));
            b->angular_velocity += glm::vec3(rr(rng_), rr(rng_), rr(rng_));
            b->sleeping = false;
            b->sleep_timer = 0.0f;
        }
        std::cout << "[chaos] launched " << bodyHandleByName_.size() << " bodies\n";
    }

    void FireCatapult() {
        if ((int)active_cata_.size() >= kMaxCata) {
            auto old = active_cata_.front(); active_cata_.pop_front();
            processAgentCommand(json::parse("{\"action\":\"delete\",\"id\":\"" + old + "\"}"));
        }
        std::string id = "cata_" + std::to_string(cata_counter_++);
        auto b = CreateGameObject(
            CreateSphere(32, glm::vec3(0.55f, 0.48f, 0.40f)),
            glm::vec3(-45.0f, 9.2f, 0.0f));
        b->SetScale(glm::vec3(1.2f));
        b->SetMetallic(1.0f);
        b->SetRoughness(0.45f);
        AddGameObject(b);
        namedObjects_[id] = b;
        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(15.0);  // heavy projectile
        bc["restitution"]=json::Value(0.35);
        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);
        // Hurl toward +X and slightly down, aimed at the crate pyramid
        vc["linear"]=json::Value(json::Array{
            json::Value(22.0), json::Value(-2.0), json::Value(0.0)});
        processAgentCommand(json::Value(vc));
        active_cata_.push_back(id);
        std::cout << "[catapult] fire\n";
    }

    void EmitDroplet() {
        if ((int)active_drops_.size() >= kMaxDrops) {
            auto old = active_drops_.front(); active_drops_.pop_front();
            processAgentCommand(json::parse("{\"action\":\"delete\",\"id\":\"" + old + "\"}"));
        }
        // Spawn along the top of the cliff (x in [-24, 24]), z just south of cliff
        std::uniform_real_distribution<float> xx(-22.0f, 22.0f);
        std::uniform_real_distribution<float> zz(-27.5f, -26.0f);
        std::uniform_real_distribution<float> yy(-0.15f, 0.25f);
        std::string id = "drop_" + std::to_string(drop_counter_++);
        glm::vec3 spawn(xx(drop_rng_), 5.5f + yy(drop_rng_), zz(drop_rng_));
        auto d = CreateGameObject(
            CreateSphere(8, glm::vec3(0.7f, 0.88f, 1.0f)),
            spawn);
        d->SetScale(glm::vec3(0.18f));
        d->SetMetallic(0.0f);
        d->SetRoughness(0.15f);
        d->SetEmissive(glm::vec3(0.4f, 0.7f, 0.95f));
        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: southward + slight downward
        std::uniform_real_distribution<float> vz(-2.5f, -1.0f);
        std::uniform_real_distribution<float> vy(-1.5f, -0.5f);
        std::uniform_real_distribution<float> vx(-0.4f, 0.4f);
        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 {
        time_ += dt;

        // Pulse emissive bollards
        {
            float pulse = 0.4f + 0.6f * (0.5f + 0.5f * std::sin(time_ * 2.3f));
            glm::vec3 hot(3.0f * pulse, 1.3f * pulse, 0.4f * pulse);
            for (auto& id : bollard_ids_) {
                auto it = namedObjects_.find(id);
                if (it != namedObjects_.end()) it->second->SetEmissive(hot);
            }
        }

        // Kickoff chain reaction at t=2s
        if (!kickoff_fired_ && time_ > 2.0f) {
            kickoff_fired_ = true;
            json::Object c;
            c["action"]=json::Value(std::string("impulse"));
            c["id"]=json::Value(std::string("kickoff"));
            c["impulse"]=json::Value(json::Array{
                json::Value(-150.0), json::Value(0.0), json::Value(0.0)});
            processAgentCommand(json::Value(c));
        }

        // Periodic catapult
        catapult_timer_ -= dt;
        if (catapult_timer_ <= 0.0f) {
            FireCatapult();
            catapult_timer_ = 18.0f;
        }

        // Waterfall emitter (~20/sec)
        drop_timer_ -= dt;
        while (drop_timer_ <= 0.0f) { EmitDroplet(); drop_timer_ += 0.05f; }

        // Shoot (F / LMB / R1)
        shot_cooldown_ -= dt;
        if (shot_cooldown_ <= 0.0f) {
            bool fire = Input::IsKeyHeld(GLFW_KEY_F)
                     || Input::IsMouseButtonPressed(Input::MouseButton::Left)
                     || Input::IsGamepadButtonDown(Input::GamepadButton::R1);
            if (fire) { FireBall(); shot_cooldown_ = 0.14f; }
        }

        // T toggle storm (edge trigger)
        bool t_now = Input::IsKeyHeld(GLFW_KEY_T)
                  || Input::IsGamepadButtonDown(Input::GamepadButton::Triangle);
        if (t_now && !t_down_prev_) CycleStorm();
        t_down_prev_ = t_now;

        // G chaos (edge trigger)
        bool g_now = Input::IsKeyHeld(GLFW_KEY_G)
                  || Input::IsGamepadButtonDown(Input::GamepadButton::Circle);
        if (g_now && !g_down_prev_) ChaosBurst();
        g_down_prev_ = g_now;

        // --- 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;
    }
};


// ---------------------------------------------
// 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};

    // Captured initial pose+body params for every dynamic object, so R / Triangle
    // can put the scene back to t=0.
    struct InitialState {
        std::string id;
        std::string shape;     // "box" or "sphere"
        glm::vec3 position;
        glm::vec3 rotation;    // Euler degrees
        float mass;
        float restitution;
        float friction;
    };
    std::vector<InitialState> initial_states_;

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

    void ResetScene();

    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: confined to a single central pool (visual only via water2).
        // Main water mesh is disabled so the rest of the map is dry land.
        processAgentCommand(json::parse(R"({"action":"water","enabled":false})"));
        processAgentCommand(json::parse(R"({
            "action":"water2","enabled":true,
            "center":[0, 0, -22], "size":14, "level":0.18,
            "amplitude":0.06,"wavelength":4,"speed":0.7,
            "shallow":[0.45,0.55,0.55],
            "deep":[0.04,0.10,0.16],
            "resolution":96
        })"));
        processAgentCommand(json::parse(R"({
            "action":"weather","wind_dir":[0.9,0,0.4],
            "wind_speed":0.6,"storminess":0.05,"current_coef":0.3
        })"));

        // --- Ground (visual + solid physics floor) ---
        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);
        // Register the grass as the ground physics body. The plane collider is
        // infinite at y=0, so it catches anything anywhere on the map.
        namedObjects_["__ground"] = grass;
        processAgentCommand(json::parse(R"({
            "action":"body","id":"__ground","shape":"plane",
            "normal":[0,1,0],"offset":0,"static":true
        })"));

        // --- 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);
            if (!id.empty() && mass > 0)
                initial_states_.push_back({id, "box", pos, rot, mass, rest, frict});
            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);
            if (!id.empty() && mass > 0)
                initial_states_.push_back({id, "sphere", pos, glm::vec3(0),
                                           mass, rest, frict});
            return obj;
        };

        // --- Map perimeter walls: 4 static stone walls ringing an 80×80 play
        // area (X,Z in [-40,40]) so dynamic props can't bounce out of the map.
        {
            const float kBL = 81.0f;   // wall length (slight corner overlap)
            const float kBH = 6.0f;    // wall height
            const float kBT = 1.0f;    // wall thickness
            const glm::vec3 kBC(0.92f, 0.88f, 0.82f);
            addBox("border_n", glm::vec3(0.0f, kBH * 0.5f,  40.5f),
                   glm::vec3(kBL, kBH, kBT), kBC,
                   marble_tex, noise_tex, 0.0f, 0.75f, -1.0f);
            addBox("border_s", glm::vec3(0.0f, kBH * 0.5f, -40.5f),
                   glm::vec3(kBL, kBH, kBT), kBC,
                   marble_tex, noise_tex, 0.0f, 0.75f, -1.0f);
            addBox("border_e", glm::vec3( 40.5f, kBH * 0.5f, 0.0f),
                   glm::vec3(kBT, kBH, kBL), kBC,
                   marble_tex, noise_tex, 0.0f, 0.75f, -1.0f);
            addBox("border_w", glm::vec3(-40.5f, kBH * 0.5f, 0.0f),
                   glm::vec3(kBT, kBH, kBL), kBC,
                   marble_tex, noise_tex, 0.0f, 0.75f, -1.0f);
        }

        // --- Central pool curb: short stone walls forming a basin around the
        // visible water at center=[0,0,-22], size 14. The curb hides the visual
        // water seam at the edge and stops things rolling through into the pool.
        {
            const float kPC = -22.0f;  // pool center Z
            const float kPS = 14.0f;   // pool size (matches water2)
            const float kPH = 0.6f;    // curb height
            const float kPT = 0.5f;    // curb thickness
            const glm::vec3 kPCol(0.78f, 0.74f, 0.70f);
            float half = kPS * 0.5f;
            addBox("pool_n", glm::vec3(0.0f, kPH * 0.5f, kPC + half + kPT * 0.5f),
                   glm::vec3(kPS + kPT, kPH, kPT), kPCol,
                   marble_tex, noise_tex, 0.0f, 0.55f, -1.0f);
            addBox("pool_s", glm::vec3(0.0f, kPH * 0.5f, kPC - half - kPT * 0.5f),
                   glm::vec3(kPS + kPT, kPH, kPT), kPCol,
                   marble_tex, noise_tex, 0.0f, 0.55f, -1.0f);
            addBox("pool_e", glm::vec3( half + kPT * 0.5f, kPH * 0.5f, kPC),
                   glm::vec3(kPT, kPH, kPS), kPCol,
                   marble_tex, noise_tex, 0.0f, 0.55f, -1.0f);
            addBox("pool_w", glm::vec3(-half - kPT * 0.5f, kPH * 0.5f, kPC),
                   glm::vec3(kPT, kPH, kPS), kPCol,
                   marble_tex, noise_tex, 0.0f, 0.55f, -1.0f);
        }

        // --- 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 PLAYGROUND SECTION — east half of the map (X >= +5).
        //  Self-contained chain reaction + bonus props, kept clear of the
        //  forge (origin) and the southern pool.
        // ============================================================

        // Static launch ramp. Tilted -18° around Z so the +X end (where the
        // ball spawns) is the LOW end and the -X end is the HIGH end. Ball
        // gets a strong -X kick at t=2s, slides off the high end, falls onto
        // the floor and continues into the domino chain.
        addBox("ramp",
               glm::vec3(28.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));

        // Heavy metal impulse ball, sitting near the +X (low) end of the ramp.
        addSphere("impulse_ball",
                  glm::vec3(30.0f, 4.2f, 0.0f), 0.9f,
                  glm::vec3(0.75f, 0.78f, 0.82f), 0, 0,
                  1.0f, 0.3f,
                  8.0f, 0.3f, 0.25f);

        // Primary domino chain — 12 boards along z=0, between ramp and forge.
        // Spacing 1.5 < domino height 2.0, so each falling domino's tip
        // overlaps the next domino's base — the chain propagates reliably.
        for (int i = 0; i < 12; ++i) {
            float x = 22.0f - i * 1.5f;
            addBox("dom_" + std::to_string(i),
                   glm::vec3(x, 1.0f, 0.0f), glm::vec3(0.25f, 2.0f, 1.2f),
                   glm::vec3(0.9f, 0.85f, 0.7f),
                   wood_tex, noise_tex, 0.0f, 0.6f,
                   0.6f, 0.05f, 0.7f);
        }

        // Secondary domino chain, parallel to the first at z = +6.
        for (int i = 0; i < 12; ++i) {
            float x = 22.0f - i * 1.5f;
            addBox("dom2_" + std::to_string(i),
                   glm::vec3(x, 1.0f, 6.0f), glm::vec3(0.25f, 2.0f, 1.1f),
                   glm::vec3(0.9f, 0.82f, 0.65f),
                   wood_tex, noise_tex, 0.0f, 0.65f,
                   0.6f, 0.05f, 0.7f);
        }

        // Crate pyramid, north end of physics section.
        for (int y = 0; y < 3; ++y) {
            int n = 4 - y;
            for (int i = 0; i < n; ++i) {
                float x = 14.0f + i * 1.1f + (3 - n) * 0.5f;
                float yp = 0.5f + y * 1.0f;
                addBox("crate_" + std::to_string(y * 4 + i),
                       glm::vec3(x, yp, 14.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);
            }
        }

        // Second crate pyramid, mid-section.
        for (int y = 0; y < 3; ++y) {
            int n = 4 - y;
            for (int i = 0; i < n; ++i) {
                float x = 26.0f + i * 1.1f + (3 - n) * 0.5f;
                float yp = 0.5f + y * 1.0f;
                addBox("crate2_" + std::to_string(y * 4 + i),
                       glm::vec3(x, yp, 14.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);
            }
        }

        // Concrete crate wall, eastern edge of section.
        for (int i = 0; i < 12; ++i) {
            int col = i % 4, row = i / 4;
            float x = 32.0f + col * 1.1f;
            float y = 0.5f + row * 1.1f;
            addBox("wallc_" + std::to_string(i),
                   glm::vec3(x, y, 22.0f), glm::vec3(1.0f),
                   glm::vec3(0.65f, 0.78f, 0.85f),
                   concrete, noise_tex, 0.0f, 0.75f,
                   0.6f, 0.2f, 0.6f);
        }

        // Bowling-pin triangle, set up to receive whatever falls past the
        // domino chain.
        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 = 14.0f - pin_offsets[i][1];
            float pz = -8.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);
            initial_states_.push_back({id, "box",
                glm::vec3(px, 0.8f, pz), glm::vec3(0), 0.5f, 0.2f, 0.4f});
        }

        // Cube tower, southeast corner of section.
        for (int i = 0; i < 8; ++i) {
            float y = 0.6f + i * 1.2f;
            addBox("tower_" + std::to_string(i),
                   glm::vec3(30.0f, y, -18.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 across the eastern half (avoiding the pool
        // and the forge).
        for (int i = 0; i < 16; ++i) {
            float x = rnd(8.0f, 36.0f);
            float z = rnd(-30.0f, 30.0f);
            // Skip cells that overlap key set-pieces.
            if (z > -2.0f && z < 8.0f && x > 6.0f && x < 24.0f) continue;
            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)));
            }
        }

        // --- 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"
                  << "RESET: press R or Triangle (gamepad) to restart the demo.\n";
        return true;
    }

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

        // Kick off the chain reaction 2 seconds in, once physics is warm.
        // Heavy -X impulse sends the ball off the ramp and into the first
        // domino at x=22.
        if (!kickoff_fired_ && time_elapsed_ > 2.0f) {
            kickoff_fired_ = true;
            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(-260.0),
                json::Value(0.0),
                json::Value(0.0)});
            processAgentCommand(json::Value(c));
        }

        // Reset (R / Triangle) — restore every dynamic prop to its initial
        // pose with zero velocity, clear shot balls, and re-arm the kickoff.
        if (Input::IsKeyPressed(GLFW_KEY_R)
            || Input::IsGamepadButtonPressed(Input::GamepadButton::Triangle)) {
            ResetScene();
        }

        // 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;
    }
};

void SanctuaryDemo::ResetScene() {
    // Restore each captured dynamic body to its initial pose with zero
    // velocity. Re-issuing the "body" command rebuilds the physics body at
    // the GameObject's current position (so we set that first) with linear
    // and angular velocity = 0.
    for (const auto& s : initial_states_) {
        auto it = namedObjects_.find(s.id);
        if (it == namedObjects_.end()) continue;
        it->second->SetPosition(s.position);
        it->second->SetRotation(s.rotation);
        json::Object c;
        c["action"]      = json::Value(std::string("body"));
        c["id"]          = json::Value(s.id);
        c["shape"]       = json::Value(s.shape);
        c["mass"]        = json::Value((double)s.mass);
        c["restitution"] = json::Value((double)s.restitution);
        c["friction"]    = json::Value((double)s.friction);
        processAgentCommand(json::Value(c));
    }
    // Despawn any in-flight player-fired balls.
    for (const auto& id : active_shots_) {
        json::Object dc;
        dc["action"] = json::Value(std::string("delete"));
        dc["id"]     = json::Value(id);
        processAgentCommand(json::Value(dc));
    }
    active_shots_.clear();
    // Re-arm the chain-reaction kickoff.
    time_elapsed_ = 0.0f;
    kickoff_fired_ = false;
}


// ---------------------------------------------
// 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 == "playground") {
        std::cout << "Starting Physics Playground..." << std::endl;
        PhysicsPlaygroundDemo demo;
        demo.Run();
    }
    else if (mode == "fourmap" || mode == "quadrants") {
        std::cout << "Starting Four-Quadrant Map..." << std::endl;
        FourMapDemo demo;
        demo.Run();
    }
    else if (mode == "builders") {
        std::cout << "Starting Builders (top-down)..." << std::endl;
        BuildersDemo demo;
        demo.Run();
    }
    else if (mode == "odyssey") {
        std::cout << "Starting Odyssey (Explore)..." << std::endl;
        OdysseyDemo 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;
    }
}
