#include "Engine.h" #include "Input.h" #include "Json.h" #include "MainMenu.h" #include "TextureCache.h" #include "Hud.h" #include "LevelEditor.cpp" #include #include #include #include // --------------------------------------------- // SceneDemo: existing complex scene demo // --------------------------------------------- class SceneDemo : public Engine { private: float time_ = 0.0f; std::vector> rotating_cubes_; std::vector> floating_spheres_; std::shared_ptr central_sphere_; std::shared_ptr ground_plane_; std::shared_ptr grid_; std::mt19937 rng_; std::uniform_real_distribution 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: "<= 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(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 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> 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 monkey_; std::shared_ptr ground_plane_; std::shared_ptr 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> monkeys_; std::shared_ptr ground_plane_; std::shared_ptr 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 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 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 pos_dist(-80.0f, 80.0f); std::uniform_real_distribution 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 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 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 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 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 obj; std::shared_ptr head; // tiny sphere on top }; struct Building { int type_idx; std::string business; glm::vec3 pos; std::shared_ptr box; std::shared_ptr roof; std::vector workers; float next_message_in = 2.0f; }; std::vector buildings_; std::deque 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 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 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 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, 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 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 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 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 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 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 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 active_shots_; std::deque active_drops_; std::deque active_cata_; std::vector 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 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 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 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 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 rx(-4.0f, 4.0f); std::uniform_real_distribution ry(10.0f, 22.0f); std::uniform_real_distribution 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 xx(-22.0f, 22.0f); std::uniform_real_distribution zz(-27.5f, -26.0f); std::uniform_real_distribution 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 vz(-2.5f, -1.0f); std::uniform_real_distribution vy(-1.5f, -0.5f); std::uniform_real_distribution 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 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 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 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 zz(-9.0f, 9.0f); std::uniform_real_distribution yy(-0.2f, 0.1f); std::uniform_real_distribution 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 vx(-3.0f, -1.5f); std::uniform_real_distribution vy(-2.0f, -1.0f); std::uniform_real_distribution 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 zz(-6.0f, 6.0f); std::uniform_real_distribution 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 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 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 zz(-5.5f, 5.5f); std::uniform_real_distribution 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 vx(-4.0f, -2.0f); std::uniform_real_distribution vy(-1.5f, -0.5f); std::uniform_real_distribution 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 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 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 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 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 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 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 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 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 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; } }