Files
zoom/src/main.cpp
2026-04-27 22:18:55 +01:00

5889 lines
251 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "Engine.h"
#include "Input.h"
#include "Json.h"
#include "MainMenu.h"
#include "TextureCache.h"
#include "Hud.h"
#include "LevelEditor.cpp"
#include <iostream>
#include <random>
#include <deque>
#include <chrono>
// ---------------------------------------------
// SceneDemo: existing complex scene demo
// ---------------------------------------------
class SceneDemo : public Engine {
private:
float time_ = 0.0f;
std::vector<std::shared_ptr<GameObject>> rotating_cubes_;
std::vector<std::shared_ptr<GameObject>> floating_spheres_;
std::shared_ptr<GameObject> central_sphere_;
std::shared_ptr<GameObject> ground_plane_;
std::shared_ptr<GameObject> grid_;
std::mt19937 rng_;
std::uniform_real_distribution<float> color_dist_{0.3f, 1.0f};
public:
SceneDemo()
: Engine(1920, 1080, "Modern C++ Engine - Complex Scene Demo"),
rng_(std::random_device{}()) {}
bool OnInitialize() override {
std::cout << "Creating complex scene with primitives..." << std::endl;
auto grid_primitive = CreateGrid(25, 1.0f, glm::vec3(0.4f));
grid_ = CreateGameObject(grid_primitive, glm::vec3(0,0,0));
AddGameObject(grid_);
auto plane_primitive = CreatePlane(30.0f, 30.0f, glm::vec3(0.2f,0.2f,0.3f));
ground_plane_ = CreateGameObject(plane_primitive, glm::vec3(0,-0.01f,0));
AddGameObject(ground_plane_);
auto central_sphere_primitive = CreateSphere(64, glm::vec3(0.9f,0.3f,0.1f));
central_sphere_ = CreateGameObject(central_sphere_primitive, glm::vec3(0,3,0));
central_sphere_->SetScale(glm::vec3(2.0f));
AddGameObject(central_sphere_);
for (int i = 0; i < 8; ++i) {
glm::vec3 color{color_dist_(rng_), color_dist_(rng_), color_dist_(rng_)};
auto cube = CreateGameObject(CreateCube(color), glm::vec3(0.0f));
rotating_cubes_.push_back(cube);
AddGameObject(cube);
}
for (int i = 0; i < 15; ++i) {
glm::vec3 color{color_dist_(rng_), color_dist_(rng_), color_dist_(rng_)};
float x = (rng_() % 20) - 10.0f;
float z = (rng_() % 20) - 10.0f;
float y = 1.0f + (rng_() % 5);
auto sphere = CreateGameObject(CreateSphere(32, color),
glm::vec3(x,y,z));
sphere->SetScale(glm::vec3(0.5f + (rng_()%10)*0.1f));
floating_spheres_.push_back(sphere);
AddGameObject(sphere);
}
for (int i = 0; i < 5; ++i) {
glm::vec3 color{1.0f - i*0.15f, 0.2f + i*0.15f, 0.5f};
auto cube = CreateGameObject(CreateCube(color),
glm::vec3(-8, i*2.1f+1, 8));
cube->SetScale(glm::vec3(0.8f + i*0.1f));
AddGameObject(cube);
}
for (int i = 0; i < 10; ++i) {
float hue = i/10.0f;
glm::vec3 color{hue, 1.0f-hue, 0.5f};
auto sphere = CreateGameObject(CreateSphere(24, color),
glm::vec3(i*2-9,1,-10));
sphere->SetScale(glm::vec3(0.3f));
AddGameObject(sphere);
}
std::cout << "Scene created with " << game_objects_.size() << " objects!" << std::endl;
return true;
}
void OnUpdate(float dt) override {
time_ += dt;
if (central_sphere_)
central_sphere_->SetRotation(glm::vec3(0, time_*30, 0));
for (size_t i = 0; i < rotating_cubes_.size(); ++i) {
float angle = time_*60.0f + i*(360.0f/rotating_cubes_.size());
float radius=8.0f, height=2.0f+std::sin(time_*2.0f+i)*1.5f;
glm::vec3 pos{std::cos(glm::radians(angle))*radius,
height,
std::sin(glm::radians(angle))*radius};
rotating_cubes_[i]->SetPosition(pos);
rotating_cubes_[i]->SetRotation(glm::vec3(angle,angle*1.5f,0));
}
for (size_t i = 0; i < floating_spheres_.size(); ++i) {
auto sphere = floating_spheres_[i];
glm::vec3 p = sphere->GetPosition();
float bob = std::sin(time_*3.0f + i*0.5f)*0.3f;
sphere->SetPosition(glm::vec3(p.x, p.y+bob, p.z));
sphere->SetRotation(glm::vec3(0, time_*20.0f + i*30.0f, 0));
}
float camAng = time_*15.0f;
SetCameraOrbit(20.0f, camAng, 12.0f);
SetCameraTarget(glm::vec3(0,2,0));
static float status_timer=0.0f;
status_timer += dt;
if (status_timer >= 3.0f) {
std::cout<<"Scene running: "<<int(time_)<<"s | "
<<game_objects_.size()<<" objects | Cam angle: "
<<int(camAng)%360<<"°"<<std::endl;
status_timer=0.0f;
}
}
};
// ---------------------------------------------
// StressTest: incremental spawn until quit
// ---------------------------------------------
class StressTest : public Engine {
public:
StressTest() : Engine(1920, 1080, "Zoom Stresser") {}
bool OnInitialize() override {
last_report_ = std::chrono::high_resolution_clock::now();
// Add a ground plane for visual reference
auto plane = CreateGameObject(CreatePlane(50.0f, 50.0f, glm::vec3(0.2f, 0.3f, 0.5f)),
glm::vec3(0, -1, 0));
AddGameObject(plane);
return true;
}
void OnUpdate(float dt) override {
time_ += dt;
// Spawn new objects every second
if (game_objects_.size() < 10000) {
spawn_timer_ += dt;
if (spawn_timer_ >= 1.0f) {
spawn_timer_ = 0.0f;
AddRow();
}
}
// Animate existing objects
AnimateObjects();
// Orbit camera around the scene
float camera_angle = time_ * 20.0f; // 20 degrees per second
float radius = 30.0f + std::sin(time_ * 0.5f) * 10.0f; // Varying radius
float height = 15.0f + std::cos(time_ * 0.3f) * 5.0f; // Varying height
SetCameraOrbit(radius, camera_angle, height);
SetCameraTarget(glm::vec3(0, 5, 0));
// Report performance every 2 seconds
auto now = std::chrono::high_resolution_clock::now();
float elapsed = std::chrono::duration<float>(now - last_report_).count();
if (elapsed >= 2.0f) {
float fps = 1.0f/dt;
std::cout << "Objects: " << game_objects_.size()
<< " | FPS: " << fps
<< " | Cam angle: " << int(camera_angle) % 360 << "°" << std::endl;
last_report_ = now;
}
}
private:
void AddRow() {
int row = int(stress_objects_.size()) / columns_;
for (int i = 0; i < columns_; ++i) {
// Colorful gradient based on position
float hue = (i + row * columns_) / 100.0f;
glm::vec3 color{
0.5f + 0.5f * std::sin(hue * 6.28f),
0.5f + 0.5f * std::sin(hue * 6.28f + 2.09f),
0.5f + 0.5f * std::sin(hue * 6.28f + 4.19f)
};
glm::vec3 pos{
i * spacing_ - columns_ * spacing_ / 2.0f,
0.5f + row * spacing_,
-20.0f + row * 2.0f // Spread them out in depth too
};
// Alternate between cubes and spheres
std::shared_ptr<GameObject> obj;
if ((i + row) % 2 == 0) {
obj = CreateGameObject(CreateCube(color), pos);
} else {
obj = CreateGameObject(CreateSphere(16, color), pos);
}
obj->SetScale(glm::vec3(0.4f + (i % 5) * 0.1f)); // Varying sizes
stress_objects_.push_back(obj);
AddGameObject(obj);
}
}
void AnimateObjects() {
for (size_t i = 0; i < stress_objects_.size(); ++i) {
auto& obj = stress_objects_[i];
glm::vec3 pos = obj->GetPosition();
// Floating/bobbing motion
float bob_speed = 2.0f + (i % 10) * 0.2f;
float bob_height = 0.3f + (i % 7) * 0.1f;
float bob = std::sin(time_ * bob_speed + i * 0.1f) * bob_height;
obj->SetPosition(glm::vec3(pos.x, pos.y + bob, pos.z));
// Rotation animation
float rot_speed = 30.0f + (i % 13) * 10.0f;
glm::vec3 rotation{
time_ * rot_speed + i * 10.0f,
time_ * (rot_speed * 0.7f) + i * 15.0f,
time_ * (rot_speed * 0.4f) + i * 20.0f
};
obj->SetRotation(rotation);
}
}
float time_{0.0f};
float spawn_timer_{0.0f};
std::chrono::high_resolution_clock::time_point last_report_;
std::vector<std::shared_ptr<GameObject>> stress_objects_;
static constexpr int columns_{500}; // Reduced for better performance
static constexpr float spacing_{1.0f};
};
// ---------------------------------------------
// SpinningMonkey: Loads OBJ monkey and spins it
// ---------------------------------------------
class SpinningMonkey : public Engine {
private:
float time_ = 0.0f;
std::shared_ptr<GameObject> monkey_;
std::shared_ptr<GameObject> ground_plane_;
std::shared_ptr<GameObject> grid_;
// Animation parameters
float spin_speed_ = 45.0f; // degrees per second
float bob_height_ = 2.0f; // vertical bobbing range
float bob_speed_ = 1.5f; // bobbing frequency
float scale_pulse_ = 0.2f; // scale pulsing amount
public:
SpinningMonkey()
: Engine(1920, 1080, "Spinning Monkey Demo - OBJ Model Loader") {}
bool OnInitialize() override {
std::cout << "Loading Suzanne (Blender Monkey) model..." << std::endl;
// Create ground reference
auto grid_primitive = CreateGrid(15, 1.0f, glm::vec3(0.3f, 0.3f, 0.4f));
grid_ = CreateGameObject(grid_primitive, glm::vec3(0, 0, 0));
AddGameObject(grid_);
auto plane_primitive = CreatePlane(20.0f, 20.0f, glm::vec3(0.15f, 0.25f, 0.35f));
ground_plane_ = CreateGameObject(plane_primitive, glm::vec3(0, -0.01f, 0));
AddGameObject(ground_plane_);
// Try to load monkey model (with optional texture)
try {
// First try with texture
auto monkey_primitive = CreateOBJModel("monkey.obj", "monkey.jpg");
if (monkey_primitive) {
monkey_ = CreateGameObject(monkey_primitive, glm::vec3(0, 3, 0));
monkey_->SetScale(glm::vec3(2.0f));
AddGameObject(monkey_);
std::cout << "✓ Loaded monkey with texture!" << std::endl;
}
} catch (...) {
std::cout << "! Texture loading failed, trying without texture..." << std::endl;
try {
// Fallback: load without texture
auto monkey_primitive = CreateOBJModel("monkey.obj");
if (monkey_primitive) {
monkey_ = CreateGameObject(monkey_primitive, glm::vec3(0, 3, 0));
monkey_->SetScale(glm::vec3(2.0f));
AddGameObject(monkey_);
std::cout << "✓ Loaded monkey without texture!" << std::endl;
}
} catch (...) {
std::cout << "✗ Failed to load monkey.obj, using fallback cube..." << std::endl;
// Ultimate fallback: colored cube
auto fallback_primitive = CreateCube(glm::vec3(1.0f, 0.5f, 0.2f));
monkey_ = CreateGameObject(fallback_primitive, glm::vec3(0, 3, 0));
monkey_->SetScale(glm::vec3(2.0f));
AddGameObject(monkey_);
}
}
// Set initial camera position
SetCameraPosition(glm::vec3(8, 6, 8));
SetCameraTarget(glm::vec3(0, 3, 0));
std::cout << "Scene initialized with " << game_objects_.size() << " objects!" << std::endl;
std::cout << "Controls:" << std::endl;
std::cout << " - Camera orbits automatically" << std::endl;
std::cout << " - Monkey spins and bobs up and down" << std::endl;
std::cout << " - Scale pulses with time" << std::endl;
return true;
}
void OnUpdate(float dt) override {
time_ += dt;
float spin_y = time_ * spin_speed_;
if (monkey_) {
// Spinning animation
float spin_x = std::sin(time_ * 0.3f) * 15.0f; // Gentle x-axis wobble
float spin_z = std::cos(time_ * 0.7f) * 10.0f; // Gentle z-axis wobble
monkey_->SetRotation(glm::vec3(spin_x, spin_y, spin_z));
// Vertical bobbing
float base_height = 3.0f;
float bob_offset = std::sin(time_ * bob_speed_) * bob_height_;
monkey_->SetPosition(glm::vec3(0, base_height + bob_offset, 0));
// Scale pulsing
float base_scale = 2.0f;
float scale_offset = std::sin(time_ * 2.0f) * scale_pulse_;
float current_scale = base_scale + scale_offset;
monkey_->SetScale(glm::vec3(current_scale));
}
// Orbiting camera
float camera_angle = time_ * 25.0f; // 25 degrees per second
float camera_radius = 10.0f + std::sin(time_ * 0.8f) * 3.0f; // Varying distance
float camera_height = 6.0f + std::cos(time_ * 0.5f) * 2.0f; // Varying height
SetCameraOrbit(camera_radius, camera_angle, camera_height);
SetCameraTarget(glm::vec3(0, 3, 0)); // Always look at monkey
// Status updates every 5 seconds
static float status_timer = 0.0f;
status_timer += dt;
if (status_timer >= 5.0f) {
std::cout << "🐵 Monkey spinning: " << int(time_) << "s | "
<< "Rotation: " << int(spin_y) % 360 << "° | "
<< "Camera: " << int(camera_angle) % 360 << "°" << std::endl;
status_timer = 0.0f;
}
}
};
// ---------------------------------------------
// MonkeyGridDemo: 50 jiggling monkeys on grid
// ---------------------------------------------
class MonkeyGridDemo : public Engine {
private:
float time_ = 0.0f;
std::vector<std::shared_ptr<GameObject>> monkeys_;
std::shared_ptr<GameObject> ground_plane_;
std::shared_ptr<GameObject> grid_;
static constexpr int MONKEY_COUNT = 50;
static constexpr int GRID_COLS = 7;
static constexpr int GRID_ROWS = 8;
static constexpr float SPACING = 4.0f;
std::mt19937 rng_{ std::random_device{}() };
std::uniform_real_distribution<float> jiggle_dist_{ -0.1f, 0.1f };
public:
MonkeyGridDemo()
: Engine(1920, 1080, "Monkey Grid Demo") {}
bool OnInitialize() override {
std::cout << "Spawning " << MONKEY_COUNT << " miniature monkeys on grid..." << std::endl;
// Grid and plane for reference
auto grid_primitive = CreateGrid(15, 1.0f, glm::vec3(0.3f));
grid_ = CreateGameObject(grid_primitive, glm::vec3(0, 0, 0));
AddGameObject(grid_);
auto plane_primitive = CreatePlane(60.0f, 60.0f, glm::vec3(0.2f, 0.25f, 0.3f));
ground_plane_ = CreateGameObject(plane_primitive, glm::vec3(0, -0.01f, 0));
AddGameObject(ground_plane_);
// Load OBJ monkey primitive once
std::shared_ptr<Primitive> monkey_primitive;
try {
monkey_primitive = CreateOBJModel("monkey.obj", "monkey.bmp");
if (!monkey_primitive)
monkey_primitive = CreateOBJModel("monkey.obj");
} catch (...) {
monkey_primitive = CreateCube(glm::vec3(1.0f, 0.5f, 0.2f));
}
// Spawn monkeys on GRID_COLS×GRID_ROWS grid
int spawned = 0;
for (int r = 0; r < GRID_ROWS && spawned < MONKEY_COUNT; ++r) {
for (int c = 0; c < GRID_COLS && spawned < MONKEY_COUNT; ++c) {
float x = (c - (GRID_COLS - 1) * 0.5f) * SPACING;
float z = (r - (GRID_ROWS - 1) * 0.5f) * SPACING;
auto monkey = CreateGameObject(monkey_primitive, glm::vec3(x, 1.0f, z));
monkey->SetScale(glm::vec3(0.5f)); // shrink to half size
monkeys_.push_back(monkey);
AddGameObject(monkey);
++spawned;
}
}
// Setup camera
SetCameraOrbit(50.0f, 45.0f, 30.0f);
SetCameraTarget(glm::vec3(0, 1.0f, 0));
return true;
}
void OnUpdate(float dt) override {
time_ += dt;
// Jiggle and slight bob for each monkey
for (auto& monkey : monkeys_) {
glm::vec3 basePos = monkey->GetPosition();
float jigX = jiggle_dist_(rng_);
float jigZ = jiggle_dist_(rng_);
float bobY = std::sin(time_ * 3.0f + basePos.x + basePos.z) * 0.2f;
monkey->SetPosition(glm::vec3(
basePos.x + jigX,
1.0f + bobY,
basePos.z + jigZ
));
// Optional subtle rotation
float angle = std::sin(time_ * 2.0f + basePos.x - basePos.z) * 15.0f;
monkey->SetRotation(glm::vec3(0, angle, 0));
}
// Slow orbiting camera
float camAng = time_ * 10.0f;
SetCameraOrbit(50.0f, camAng, 30.0f);
SetCameraTarget(glm::vec3(0, 1.0f, 0));
}
};
// ---------------------------------------------
// LandscapeDemo: large terrain with trees, rocks
// Full PS4/PS5 controller + keyboard camera
// ---------------------------------------------
class LandscapeDemo : public Engine {
private:
float time_ = 0.0f;
// Free-fly camera state
float cam_yaw_ = -90.0f;
float cam_pitch_ = 15.0f;
glm::vec3 cam_pos_{0.0f, 20.0f, 40.0f};
float cam_speed_ = 20.0f;
float look_sensitivity_ = 80.0f;
std::mt19937 rng_{42};
public:
LandscapeDemo()
: Engine(1920, 1080, "ZoomEngine - Landscape (Gamepad + Keyboard)") {}
bool OnInitialize() override {
std::cout << "Generating landscape..." << std::endl;
// Large terrain
auto terrain = CreateTerrain(128, 200.0f, 10.0f);
auto terrainObj = CreateGameObject(terrain, glm::vec3(0, 0, 0));
AddGameObject(terrainObj);
// Scatter trees (cylinder trunk + cone foliage)
auto trunk_prim = CreateCylinder(0.3f, 3.0f, 8, glm::vec3(0.4f, 0.25f, 0.1f));
auto leaves_prim = CreateCone(1.5f, 3.0f, 8, glm::vec3(0.15f, 0.45f, 0.1f));
auto leaves_prim2 = CreateCone(1.2f, 2.5f, 8, glm::vec3(0.1f, 0.55f, 0.12f));
std::uniform_real_distribution<float> pos_dist(-80.0f, 80.0f);
std::uniform_real_distribution<float> scale_dist(0.7f, 1.5f);
for (int i = 0; i < 120; ++i) {
float x = pos_dist(rng_);
float z = pos_dist(rng_);
// Approximate terrain height at this position
float h = 0.0f;
h += std::sin(x * 0.1f) * std::cos(z * 0.1f) * 5.0f;
h += std::sin(x * 0.25f + 1.3f) * std::cos(z * 0.3f + 0.7f) * 2.5f;
h += std::sin(x * 0.6f + 2.1f) * std::cos(z * 0.5f + 1.5f) * 1.25f;
float s = scale_dist(rng_);
// Trunk
auto t = CreateGameObject(trunk_prim, glm::vec3(x, h + 1.5f * s, z));
t->SetScale(glm::vec3(s, s, s));
AddGameObject(t);
// Lower foliage
auto f1 = CreateGameObject(leaves_prim, glm::vec3(x, h + 4.0f * s, z));
f1->SetScale(glm::vec3(s, s, s));
AddGameObject(f1);
// Upper foliage
auto f2 = CreateGameObject(leaves_prim2, glm::vec3(x, h + 5.5f * s, z));
f2->SetScale(glm::vec3(s * 0.8f, s * 0.8f, s * 0.8f));
AddGameObject(f2);
}
// Scatter rocks (spheres squished flat)
auto rock_prim = CreateSphere(8, glm::vec3(0.45f, 0.42f, 0.4f));
std::uniform_real_distribution<float> rock_scale(0.4f, 1.2f);
for (int i = 0; i < 60; ++i) {
float x = pos_dist(rng_);
float z = pos_dist(rng_);
float h = 0.0f;
h += std::sin(x * 0.1f) * std::cos(z * 0.1f) * 5.0f;
h += std::sin(x * 0.25f + 1.3f) * std::cos(z * 0.3f + 0.7f) * 2.5f;
float rs = rock_scale(rng_);
auto r = CreateGameObject(rock_prim, glm::vec3(x, h + rs * 0.3f, z));
r->SetScale(glm::vec3(rs, rs * 0.5f, rs));
AddGameObject(r);
}
std::cout << "Landscape ready: " << game_objects_.size() << " objects" << std::endl;
std::cout << "Controls:" << std::endl;
std::cout << " Gamepad: Left stick = move, Right stick = look" << std::endl;
std::cout << " R2 = up, L2 = down, Cross = sprint" << std::endl;
std::cout << " Keyboard: WASD = move, Arrow keys = look, Space/Shift = up/down" << std::endl;
return true;
}
void OnUpdate(float dt) override {
time_ += dt;
float speed = cam_speed_;
// --- Gamepad camera ---
if (Input::IsGamepadConnected()) {
// Sprint with Cross
if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross))
speed *= 3.0f;
// Left stick: move forward/back + strafe
glm::vec2 ls = Input::GetLeftStick();
// Right stick: look
glm::vec2 rs = Input::GetRightStick();
cam_yaw_ += rs.x * look_sensitivity_ * dt;
cam_pitch_ -= rs.y * look_sensitivity_ * dt;
if (cam_pitch_ > 89.0f) cam_pitch_ = 89.0f;
if (cam_pitch_ < -89.0f) cam_pitch_ = -89.0f;
glm::vec3 forward;
forward.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
forward.y = std::sin(glm::radians(cam_pitch_));
forward.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
forward = glm::normalize(forward);
glm::vec3 right = glm::normalize(glm::cross(forward, glm::vec3(0, 1, 0)));
cam_pos_ += forward * (-ls.y) * speed * dt;
cam_pos_ += right * ls.x * speed * dt;
// Triggers for vertical movement
cam_pos_.y += Input::GetR2() * speed * dt;
cam_pos_.y -= Input::GetL2() * speed * dt;
}
// --- Keyboard camera ---
{
float kspeed = speed;
if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT)) kspeed *= 3.0f;
if (Input::IsKeyHeld(GLFW_KEY_LEFT)) cam_yaw_ -= look_sensitivity_ * dt;
if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) cam_yaw_ += look_sensitivity_ * dt;
if (Input::IsKeyHeld(GLFW_KEY_UP)) cam_pitch_ += look_sensitivity_ * dt;
if (Input::IsKeyHeld(GLFW_KEY_DOWN)) cam_pitch_ -= look_sensitivity_ * dt;
if (cam_pitch_ > 89.0f) cam_pitch_ = 89.0f;
if (cam_pitch_ < -89.0f) cam_pitch_ = -89.0f;
glm::vec3 forward;
forward.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
forward.y = std::sin(glm::radians(cam_pitch_));
forward.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
forward = glm::normalize(forward);
glm::vec3 right = glm::normalize(glm::cross(forward, glm::vec3(0, 1, 0)));
if (Input::IsKeyHeld(GLFW_KEY_W)) cam_pos_ += forward * kspeed * dt;
if (Input::IsKeyHeld(GLFW_KEY_S)) cam_pos_ -= forward * kspeed * dt;
if (Input::IsKeyHeld(GLFW_KEY_A)) cam_pos_ -= right * kspeed * dt;
if (Input::IsKeyHeld(GLFW_KEY_D)) cam_pos_ += right * kspeed * dt;
if (Input::IsKeyHeld(GLFW_KEY_SPACE)) cam_pos_.y += kspeed * dt;
if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL)) cam_pos_.y -= kspeed * dt;
}
// Apply camera
glm::vec3 forward;
forward.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
forward.y = std::sin(glm::radians(cam_pitch_));
forward.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
SetCameraPosition(cam_pos_);
SetCameraTarget(cam_pos_ + glm::normalize(forward));
}
};
// ---------------------------------------------
// OdysseyDemo — large explorable world with verticality and a playable
// character visible in first person (arm + hand), plus a 6-slot item
// hotbar. Each item throws a different kind of energy ball.
// Zones radiate from a central spawn:
// N: Forest valley (lowest)
// E: Sakura plateau (+8 elevation, reached via ramp)
// S: Desert basin (warm, flat)
// W: Tech cliffs (+14 elevation, glowing spires)
// Center: lava/volcano pit with an emissive glow column
// ---------------------------------------------
class OdysseyDemo : public Engine {
// Camera state
float cam_yaw_ = 180.0f;
float cam_pitch_ = -10.0f;
float speed_ = 18.0f;
float look_sens_ = 90.0f;
glm::vec3 last_cam_pos_{0};
// First-person arm
std::shared_ptr<GameObject> arm_upper_, arm_lower_, hand_;
float throw_anim_ = 0.0f; // 0..1 animation progress
bool arm_ready_ = false;
// Hotbar items
struct Item {
const char* label;
glm::vec3 color;
glm::vec3 emissive;
float mass;
float velocity;
float radius;
};
Item items_[6] = {
{"STEEL", glm::vec3(0.85f, 0.88f, 0.95f), glm::vec3(0.0f), 2.5f, 55.0f, 0.35f},
{"FIRE", glm::vec3(1.40f, 0.35f, 0.10f), glm::vec3(4.5f, 1.2f, 0.3f), 1.2f, 55.0f, 0.38f},
{"ICE", glm::vec3(0.65f, 0.92f, 1.10f), glm::vec3(0.4f, 0.9f, 1.5f), 1.8f, 55.0f, 0.35f},
{"PLASMA", glm::vec3(1.30f, 0.40f, 1.30f), glm::vec3(2.2f, 0.5f, 3.5f), 0.8f, 70.0f, 0.32f},
{"VOID", glm::vec3(0.12f, 0.08f, 0.18f), glm::vec3(0.0f), 5.0f, 45.0f, 0.45f},
{"GOLD", glm::vec3(1.20f, 0.95f, 0.45f), glm::vec3(0.6f, 0.4f, 0.0f), 3.0f, 60.0f, 0.38f},
};
int selected_ = 0;
bool q_prev_ = false, e_prev_ = false;
bool ctrl_cycle_cooldown_ = false;
// Shooting
int shot_counter_ = 0;
float shot_cooldown_ = 0.0f;
std::deque<std::string> active_shots_;
static constexpr int kMaxShots = 80;
std::mt19937 rng_{66612};
public:
OdysseyDemo()
: Engine(1920, 1080, "ZoomEngine - Odyssey (expansive)") {}
bool OnInitialize() override {
auto rnd = [&](float lo, float hi) {
std::uniform_real_distribution<float> d(lo, hi); return d(rng_);
};
auto jit = [&](float s) { return rnd(-s, s); };
// Neutral-daylight base; zone tint comes from textures
processAgentCommand(json::parse(R"({
"action":"sun",
"direction":[-0.40, 0.70, -0.25],
"color":[1.95, 1.80, 1.55],
"zenith":[0.22, 0.40, 0.68],
"horizon":[0.85, 0.78, 0.70],
"sky":[0.62, 0.72, 0.78],
"ground":[0.08, 0.09, 0.10],
"shadow_extent":150,
"exposure":0.95,
"bloom_threshold":1.15,
"bloom_intensity":1.2,
"fog_density":0.0035
})"));
processAgentCommand(json::parse(R"({"action":"water","enabled":false})"));
GLuint grass_tex = TextureCache::Get("grass");
GLuint rock_tex = TextureCache::Get("rock");
GLuint dirt_t = TextureCache::Get("dirt");
GLuint marble_t = TextureCache::Get("marble");
GLuint concrete = TextureCache::Get("concrete");
GLuint wood_tex = TextureCache::Get("wood");
GLuint grid_t = TextureCache::Get("grid");
GLuint noise_t = TextureCache::Get("noise");
// --- Forest valley (N, y≈0, default plane) ---
auto forest = CreateGameObject(
CreatePlane(240.0f, 120.0f, glm::vec3(0.72f, 0.85f, 0.68f)),
glm::vec3(0, 0, 70.0f));
forest->SetTexture(grass_tex); forest->SetNormalMap(noise_t);
forest->SetUVScale(glm::vec2(0.2f)); forest->SetRoughness(0.95f);
AddGameObject(forest);
// Spawn plaza (center)
auto spawn = CreateGameObject(
CreatePlane(40.0f, 40.0f, glm::vec3(0.92f, 0.92f, 0.88f)),
glm::vec3(0, 0.01f, 0));
spawn->SetTexture(marble_t); spawn->SetNormalMap(noise_t);
spawn->SetUVScale(glm::vec2(0.2f)); spawn->SetRoughness(0.5f);
AddGameObject(spawn);
// --- Sakura plateau (E, y=8) ---
// Stepped ramp up from center to plateau
for (int step = 1; step <= 4; ++step) {
auto s = CreateGameObject(
CreateCube(glm::vec3(0.95f, 0.85f, 0.82f)),
glm::vec3(20.0f + step * 3.5f, step * 1.0f, 0));
s->SetScale(glm::vec3(4.0f, 2.0f, 18.0f));
s->SetTexture(marble_t); s->SetNormalMap(noise_t);
s->SetRoughness(0.55f);
AddGameObject(s);
}
auto sakura_ground = CreateGameObject(
CreatePlane(100.0f, 140.0f, glm::vec3(1.0f, 0.82f, 0.85f)),
glm::vec3(90.0f, 8.0f, 0));
sakura_ground->SetTexture(grass_tex); sakura_ground->SetNormalMap(noise_t);
sakura_ground->SetUVScale(glm::vec2(0.25f)); sakura_ground->SetRoughness(0.92f);
AddGameObject(sakura_ground);
// Pink plateau sides (visible from below)
for (int i = 0; i < 4; ++i) {
auto side = CreateGameObject(
CreateCube(glm::vec3(0.86f, 0.78f, 0.82f)),
glm::vec3(90.0f + (i - 1.5f) * 30.0f, 4.0f, 70.0f));
side->SetScale(glm::vec3(30.0f, 8.0f, 4.0f));
side->SetTexture(concrete); side->SetNormalMap(noise_t);
side->SetRoughness(0.85f);
AddGameObject(side);
}
// Cherry trees on plateau
auto cherry = [&](glm::vec3 at, float s) {
auto trunk = CreateGameObject(
CreateCylinder(0.24f * s, 2.6f * s, 10,
glm::vec3(0.48f, 0.32f, 0.22f)),
at + glm::vec3(0, 1.3f * s, 0));
trunk->SetRotation(glm::vec3(jit(4), jit(360), jit(4)));
trunk->SetTexture(wood_tex); trunk->SetNormalMap(noise_t);
trunk->SetRoughness(0.85f);
AddGameObject(trunk);
for (int k = 0; k < 3; ++k) {
float r = 1.6f * s * (1.0f - k * 0.20f);
auto canopy = CreateGameObject(
CreateSphere(24, glm::vec3(1.25f, 0.55f, 0.82f)),
at + glm::vec3(jit(0.25f), 2.8f * s + k * 0.9f * s, jit(0.25f)));
canopy->SetScale(glm::vec3(r, r * 0.75f, r));
canopy->SetEmissive(glm::vec3(0.25f, 0.10f, 0.18f));
canopy->SetRoughness(0.85f);
AddGameObject(canopy);
}
};
for (int i = 0; i < 35; ++i) {
float x = 90.0f + rnd(-45.0f, 45.0f);
float z = rnd(-60.0f, 60.0f);
cherry(glm::vec3(x, 8.0f, z), rnd(0.9f, 1.4f));
}
// Floating petals
for (int i = 0; i < 120; ++i) {
float x = 90.0f + rnd(-45.0f, 45.0f);
float z = rnd(-60.0f, 60.0f);
float y = 8.0f + rnd(2.0f, 9.0f);
auto petal = CreateGameObject(
CreateSphere(6, glm::vec3(1.5f, 0.6f, 0.85f)),
glm::vec3(x, y, z));
petal->SetScale(glm::vec3(0.08f, 0.04f, 0.08f));
petal->SetEmissive(glm::vec3(0.50f, 0.15f, 0.25f));
AddGameObject(petal);
}
// --- Desert basin (S, flat at y=0) ---
auto desert_g = CreateGameObject(
CreatePlane(160.0f, 120.0f, glm::vec3(1.05f, 0.88f, 0.60f)),
glm::vec3(0, 0.0f, -75.0f));
desert_g->SetTexture(dirt_t); desert_g->SetNormalMap(noise_t);
desert_g->SetUVScale(glm::vec2(0.3f)); desert_g->SetRoughness(0.98f);
AddGameObject(desert_g);
// Step pyramid
for (int lvl = 0; lvl < 6; ++lvl) {
float s = 12.0f - lvl * 1.8f;
auto p = CreateGameObject(
CreateCube(glm::vec3(0.92f, 0.76f, 0.52f)),
glm::vec3(-30.0f, 1.1f + lvl * 2.2f, -90.0f));
p->SetScale(glm::vec3(s, 2.2f, s));
p->SetTexture(dirt_t); p->SetNormalMap(noise_t);
p->SetRoughness(0.95f);
AddGameObject(p);
}
// Obelisk
{
auto obel = CreateGameObject(
CreateCube(glm::vec3(0.95f, 0.85f, 0.55f)),
glm::vec3(30.0f, 6.0f, -90.0f));
obel->SetScale(glm::vec3(1.8f, 12.0f, 1.8f));
obel->SetTexture(marble_t); obel->SetNormalMap(noise_t);
obel->SetRoughness(0.45f);
obel->SetRotation(glm::vec3(0, 20.0f, 0));
AddGameObject(obel);
}
// Cacti scattered
for (int i = 0; i < 24; ++i) {
float x = rnd(-70.0f, 70.0f);
float z = rnd(-120.0f, -30.0f);
float h = rnd(2.0f, 3.5f);
auto c = CreateGameObject(
CreateCylinder(0.4f, h, 8, glm::vec3(0.30f, 0.55f, 0.32f)),
glm::vec3(x, h * 0.5f, z));
c->SetRoughness(0.8f); c->SetNormalMap(noise_t);
AddGameObject(c);
}
// Sand dunes (flattened spheres)
for (int i = 0; i < 10; ++i) {
float x = rnd(-70.0f, 70.0f);
float z = rnd(-120.0f, -30.0f);
auto d = CreateGameObject(
CreateSphere(18, glm::vec3(1.0f, 0.82f, 0.55f)),
glm::vec3(x, 0, z));
d->SetScale(glm::vec3(rnd(4, 7), rnd(1, 2), rnd(3, 6)));
d->SetTexture(dirt_t); d->SetNormalMap(noise_t);
d->SetRoughness(0.95f);
AddGameObject(d);
}
// --- Tech cliffs (W, high at y=14) ---
// Vertical cliff face approaching from the plaza
for (int i = 0; i < 6; ++i) {
auto block = CreateGameObject(
CreateCube(glm::vec3(0.75f, 0.78f, 0.84f)),
glm::vec3(-30.0f - i * 3.0f, (i + 1) * 1.2f, 0));
block->SetScale(glm::vec3(3.0f, 2.4f * (i + 1), 18.0f));
block->SetTexture(concrete); block->SetNormalMap(noise_t);
block->SetRoughness(0.75f);
AddGameObject(block);
}
auto tech_top = CreateGameObject(
CreatePlane(100.0f, 140.0f, glm::vec3(0.30f, 0.34f, 0.40f)),
glm::vec3(-90.0f, 14.0f, 0));
tech_top->SetTexture(grid_t); tech_top->SetNormalMap(noise_t);
tech_top->SetUVScale(glm::vec2(0.5f));
tech_top->SetMetallic(0.6f);
tech_top->SetRoughness(0.3f);
AddGameObject(tech_top);
// Chrome pylons on top
for (int i = 0; i < 12; ++i) {
float a = (float)i / 12.0f * 2 * (float)M_PI;
float R = 22.0f;
float x = -90.0f + std::cos(a) * R;
float z = std::sin(a) * R;
auto p = CreateGameObject(
CreateCube(glm::vec3(0.96f)),
glm::vec3(x, 18.0f, z));
p->SetScale(glm::vec3(0.8f, 8.0f, 0.8f));
p->SetMetallic(1.0f); p->SetRoughness(0.15f);
AddGameObject(p);
}
// Giant glowing spire at center of tech
{
auto spire = CreateGameObject(
CreateCube(glm::vec3(0.95f)),
glm::vec3(-90.0f, 24.0f, 0));
spire->SetScale(glm::vec3(1.2f, 20.0f, 1.2f));
spire->SetMetallic(1.0f); spire->SetRoughness(0.1f);
spire->SetEmissive(glm::vec3(1.5f, 0.3f, 3.5f));
AddGameObject(spire);
}
// Cyan neon bars scattered
for (int i = 0; i < 16; ++i) {
float x = -90.0f + rnd(-40.0f, 40.0f);
float z = rnd(-55.0f, 55.0f);
auto bar = CreateGameObject(
CreateCube(glm::vec3(1.0f)),
glm::vec3(x, 14.2f, z));
bar->SetScale(glm::vec3(rnd(1.2f, 3.0f), 0.08f, 0.25f));
bar->SetRotation(glm::vec3(0, jit(180.0f), 0));
bar->SetEmissive(glm::vec3(0.15f, 2.8f, 3.2f));
AddGameObject(bar);
}
// --- Central volcano/lava pit (height column + glowing ground) ---
{
auto mouth = CreateGameObject(
CreateCone(5.0f, 3.0f, 16, glm::vec3(0.30f, 0.22f, 0.18f)),
glm::vec3(0, 1.5f, 0));
mouth->SetTexture(rock_tex); mouth->SetNormalMap(noise_t);
mouth->SetRoughness(0.9f);
// Invert so it's a bowl? Easier — just use as volcano base
AddGameObject(mouth);
// Lava glow (flat disk inside)
auto lava = CreateGameObject(
CreateCylinder(3.0f, 0.15f, 32, glm::vec3(1.0f)),
glm::vec3(0, 2.2f, 0));
lava->SetEmissive(glm::vec3(5.0f, 1.5f, 0.2f));
AddGameObject(lava);
// Glow column rising
for (int i = 0; i < 5; ++i) {
auto smoke = CreateGameObject(
CreateSphere(12, glm::vec3(1.0f)),
glm::vec3(jit(0.5f), 3.0f + i * 2.0f, jit(0.5f)));
float s = 1.0f + i * 0.3f;
smoke->SetScale(glm::vec3(s, s, s));
smoke->SetEmissive(glm::vec3(2.5f - i * 0.35f, 0.8f, 0.15f));
AddGameObject(smoke);
}
}
// --- Forest trees ---
auto tree = [&](glm::vec3 at, float s) {
auto trunk = CreateGameObject(
CreateCylinder(0.25f * s, 2.8f * s, 10,
glm::vec3(0.85f, 0.78f, 0.65f)),
at + glm::vec3(0, 1.4f * s, 0));
trunk->SetRotation(glm::vec3(jit(4), jit(360), jit(4)));
trunk->SetTexture(wood_tex); trunk->SetNormalMap(noise_t);
trunk->SetRoughness(0.85f);
AddGameObject(trunk);
glm::vec3 cc(rnd(0.10f, 0.22f), rnd(0.38f, 0.55f), rnd(0.12f, 0.22f));
auto cone = CreateGameObject(
CreateCone(1.4f * s, 3.2f * s, 10, cc),
at + glm::vec3(jit(0.15f), 3.2f * s, jit(0.15f)));
cone->SetNormalMap(noise_t); cone->SetRoughness(0.8f);
AddGameObject(cone);
};
for (int i = 0; i < 55; ++i) {
float x = rnd(-70.0f, 70.0f);
float z = 30.0f + rnd(10.0f, 120.0f);
tree(glm::vec3(x, 0, z), rnd(0.8f, 1.4f));
}
// --- Central plaza pylons (so you orient from spawn) ---
for (int i = 0; i < 4; ++i) {
float a = (float)i / 4.0f * 2 * (float)M_PI + 0.25f;
float R = 14.0f;
auto p = CreateGameObject(
CreateCylinder(0.4f, 6.0f, 12, glm::vec3(0.90f)),
glm::vec3(std::cos(a) * R, 3.0f, std::sin(a) * R));
p->SetMetallic(1.0f); p->SetRoughness(0.25f);
p->SetEmissive(glm::vec3(1.2f, 1.0f, 0.6f));
AddGameObject(p);
}
// --- Outer mountain ring ---
for (int i = 0; i < 32; ++i) {
float a = (float)i / 32.0f * 2 * (float)M_PI;
float R = 220.0f;
float sc = 1.0f + ((float)((i * 37) % 100) / 100.0f) * 0.5f;
float h = 32.0f * sc;
auto m = CreateGameObject(
CreateCone(15.0f * sc, h, 10, glm::vec3(0.22f, 0.20f, 0.24f)),
glm::vec3(std::cos(a) * R + jit(5.0f), h * 0.5f, std::sin(a) * R + jit(5.0f)));
m->SetRoughness(0.92f);
AddGameObject(m);
}
// --- First-person arm ---
BuildArm();
// Camera starts facing north across forest
glm::vec3 eye(0, 3.0f, -3.0f);
SetCameraPosition(eye);
SetCameraTarget(glm::vec3(0, 3.0f, 10.0f));
glm::vec3 fwd = glm::normalize(GetCameraTarget() - eye);
cam_pitch_ = glm::degrees(std::asin(fwd.y));
cam_yaw_ = glm::degrees(std::atan2(fwd.z, fwd.x));
last_cam_pos_ = eye;
std::cout << "Odyssey ready: " << game_objects_.size() << " objects.\n"
<< " WASD/arrows move+look, Space/LCtrl up/down, LShift sprint.\n"
<< " 1-6 or Q/E cycle items F/LMB/R1 to throw.\n";
return true;
}
void BuildArm() {
// Upper arm (shoulder → elbow)
arm_upper_ = CreateGameObject(
CreateCylinder(0.10f, 0.45f, 10, glm::vec3(0.92f, 0.85f, 0.78f)),
glm::vec3(0));
arm_upper_->SetRoughness(0.75f);
AddGameObject(arm_upper_);
// Forearm (elbow → wrist)
arm_lower_ = CreateGameObject(
CreateCylinder(0.085f, 0.45f, 10, glm::vec3(0.92f, 0.85f, 0.78f)),
glm::vec3(0));
arm_lower_->SetRoughness(0.75f);
AddGameObject(arm_lower_);
// Hand (sphere)
hand_ = CreateGameObject(
CreateSphere(18, glm::vec3(0.92f, 0.85f, 0.78f)),
glm::vec3(0));
hand_->SetScale(glm::vec3(0.11f));
hand_->SetRoughness(0.7f);
AddGameObject(hand_);
arm_ready_ = true;
}
void UpdateArm(float dt) {
if (!arm_ready_) return;
throw_anim_ = std::max(0.0f, throw_anim_ - dt * 4.0f);
// Build camera basis
glm::vec3 cp = GetCameraPosition();
glm::vec3 tg = GetCameraTarget();
glm::vec3 fwd = glm::normalize(tg - cp);
glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
glm::vec3 up = glm::cross(right, fwd);
// Throw motion: forearm pitches forward then snaps back
float t = throw_anim_;
float forward_boost = t * 0.45f;
// Shoulder offset: to the right and slightly down from the eye
glm::vec3 shoulder = cp + right * 0.55f + up * -0.40f + fwd * 0.35f;
// Elbow: slightly forward + down
glm::vec3 elbow = shoulder + fwd * (0.18f + forward_boost * 0.4f)
+ up * (-0.20f + t * 0.1f);
// Wrist: more forward, pitches during throw
glm::vec3 wrist = elbow + fwd * (0.35f + forward_boost)
+ up * (-0.05f + t * 0.3f);
auto midpoint = [](const glm::vec3& a, const glm::vec3& b) {
return (a + b) * 0.5f;
};
auto yawPitch = [&](const glm::vec3& dir) {
glm::vec3 d = glm::normalize(dir);
float pitch = glm::degrees(std::asin(d.y));
float yaw = glm::degrees(std::atan2(d.x, d.z));
// Cylinder primitive's axis is +Y. We want to align +Y to 'dir'.
// Compose rotations: yaw around Y then tilt.
// Using the engine's Euler order (X, Y, Z degrees), a tilt along
// the axis of rotation is approximated by pitch-around-X.
return glm::vec3(-pitch - 90.0f, yaw, 0.0f);
};
glm::vec3 upperDir = elbow - shoulder;
glm::vec3 lowerDir = wrist - elbow;
arm_upper_->SetPosition(midpoint(shoulder, elbow));
arm_upper_->SetRotation(yawPitch(upperDir));
float ul = glm::length(upperDir);
arm_upper_->SetScale(glm::vec3(1.0f, ul / 0.45f, 1.0f));
arm_lower_->SetPosition(midpoint(elbow, wrist));
arm_lower_->SetRotation(yawPitch(lowerDir));
float ll = glm::length(lowerDir);
arm_lower_->SetScale(glm::vec3(1.0f, ll / 0.45f, 1.0f));
hand_->SetPosition(wrist);
// Color hand tint by currently-selected item so you know what you'll throw
hand_->SetEmissive(items_[selected_].emissive * 0.3f);
}
void FireBall() {
const Item& it = items_[selected_];
glm::vec3 cp = GetCameraPosition();
glm::vec3 tg = GetCameraTarget();
glm::vec3 fwd = glm::normalize(tg - cp);
glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
glm::vec3 up = glm::cross(right, fwd);
glm::vec3 spawn = cp + right * 0.55f + up * -0.25f + fwd * 1.2f;
if ((int)active_shots_.size() >= kMaxShots) {
auto old = active_shots_.front(); active_shots_.pop_front();
processAgentCommand(json::parse("{\"action\":\"delete\",\"id\":\"" + old + "\"}"));
}
std::string id = "shot_" + std::to_string(shot_counter_++);
auto b = CreateGameObject(CreateSphere(20, it.color), spawn);
b->SetScale(glm::vec3(it.radius));
b->SetMetallic(selected_ == 0 || selected_ == 5 ? 1.0f : 0.0f);
b->SetRoughness(selected_ == 0 ? 0.3f : 0.5f);
b->SetEmissive(it.emissive);
AddGameObject(b);
namedObjects_[id] = b;
json::Object bc;
bc["action"]=json::Value(std::string("body"));
bc["id"]=json::Value(id);
bc["shape"]=json::Value(std::string("sphere"));
bc["mass"]=json::Value((double)it.mass);
bc["restitution"]=json::Value(0.5);
bc["friction"]=json::Value(0.3);
processAgentCommand(json::Value(bc));
json::Object vc;
vc["action"]=json::Value(std::string("velocity"));
vc["id"]=json::Value(id);
vc["linear"]=json::Value(json::Array{
json::Value((double)(fwd.x * it.velocity)),
json::Value((double)(fwd.y * it.velocity)),
json::Value((double)(fwd.z * it.velocity))});
processAgentCommand(json::Value(vc));
active_shots_.push_back(id);
throw_anim_ = 1.0f;
processAgentCommand(json::parse("{\"action\":\"physics\",\"enabled\":true}"));
}
void OnUpdate(float dt) override {
// Hotbar select (1-6, Q/E cycle, gamepad L1/R1-ish — but R1 is shoot,
// so cycle with shoulder ls-click or D-pad)
int keys[6] = {GLFW_KEY_1, GLFW_KEY_2, GLFW_KEY_3, GLFW_KEY_4,
GLFW_KEY_5, GLFW_KEY_6};
for (int i = 0; i < 6; ++i) if (Input::IsKeyPressed(keys[i])) selected_ = i;
bool q = Input::IsKeyHeld(GLFW_KEY_Q);
bool e = Input::IsKeyHeld(GLFW_KEY_E);
if (q && !q_prev_) selected_ = (selected_ + 5) % 6;
if (e && !e_prev_) selected_ = (selected_ + 1) % 6;
q_prev_ = q; e_prev_ = e;
// Shoot
shot_cooldown_ -= dt;
if (shot_cooldown_ <= 0.0f) {
bool fire = Input::IsKeyHeld(GLFW_KEY_F)
|| Input::IsMouseButtonPressed(Input::MouseButton::Left)
|| Input::IsGamepadButtonDown(Input::GamepadButton::R1);
if (fire) { FireBall(); shot_cooldown_ = 0.18f; }
}
// Free-fly camera (sync from engine, apply pan + look)
glm::vec3 eng_pos = GetCameraPosition();
if (glm::length(eng_pos - last_cam_pos_) > 0.0001f) {
glm::vec3 fwd = GetCameraTarget() - eng_pos;
if (glm::length(fwd) > 0.0001f) {
fwd = glm::normalize(fwd);
cam_pitch_ = glm::degrees(std::asin(fwd.y));
cam_yaw_ = glm::degrees(std::atan2(fwd.z, fwd.x));
}
}
glm::vec2 ls{0}, rs{0};
float up = 0, down = 0, speed = speed_;
bool touched = false;
if (Input::IsGamepadConnected()) {
ls = Input::GetLeftStick();
rs = Input::GetRightStick();
up = Input::GetR2();
down = Input::GetL2();
if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross)) speed *= 3.0f;
if (glm::length(ls) + glm::length(rs) + up + down > 0.01f) touched = true;
}
if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT)) speed *= 3.0f;
if (Input::IsKeyHeld(GLFW_KEY_W)) { ls.y -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_S)) { ls.y += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_A)) { ls.x -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_D)) { ls.x += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_LEFT)) { rs.x -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) { rs.x += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_UP)) { rs.y -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_DOWN)) { rs.y += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_SPACE)) { up += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL)) { down += 1.0f; touched = true; }
if (touched) {
cam_yaw_ += rs.x * look_sens_ * dt;
cam_pitch_ -= rs.y * look_sens_ * dt;
cam_pitch_ = std::clamp(cam_pitch_, -89.0f, 89.0f);
glm::vec3 fwd;
fwd.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
fwd.y = std::sin(glm::radians(cam_pitch_));
fwd.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
fwd = glm::normalize(fwd);
glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
glm::vec3 pos = eng_pos;
pos += fwd * (-ls.y) * speed * dt;
pos += right * ls.x * speed * dt;
pos.y += (up - down) * speed * dt;
SetCameraPosition(pos);
SetCameraTarget(pos + fwd);
last_cam_pos_ = pos;
} else {
last_cam_pos_ = eng_pos;
}
UpdateArm(dt);
}
void OnRenderOverlay() override {
Hud hud(font_, width_, height_);
// --- Hotbar: 6 slots centered at bottom ---
int N = 6;
float slot_w = 84.0f, slot_h = 84.0f;
float pad = 8.0f;
float total_w = N * slot_w + (N - 1) * pad;
float start_x = (width_ - total_w) * 0.5f;
float y = 18.0f;
for (int i = 0; i < N; ++i) {
float x = start_x + i * (slot_w + pad);
const Item& it = items_[i];
bool sel = (i == selected_);
glm::vec4 bg(0.03f, 0.04f, 0.07f, 0.8f);
glm::vec4 br = sel ? glm::vec4(it.color, 1.0f)
: glm::vec4(0.8f, 0.8f, 0.8f, 0.35f);
float bt = sel ? 3.0f : 1.5f;
hud.beginPanel(x, y, slot_w, slot_h, bg, br, bt);
hud.drawPanelBackgroundNow();
// Item icon: colored rect (emissive tint applied via color mix)
glm::vec3 ic = it.color + it.emissive * 0.2f;
hud.rect(x + 14, y + 26, slot_w - 28, slot_h - 44,
glm::vec4(ic, 1.0f));
// Number badge (top-left) and label (bottom)
char num[4]; std::snprintf(num, sizeof(num), "%d", i + 1);
hud.text(x + 8, y + slot_h - 18, num, 14.0f,
glm::vec3(1, 1, 1), true);
hud.text(x + 10, y + 7, it.label, 12.0f,
glm::vec3(1, 1, 1), true);
hud.endPanel();
}
// Controls hint (top-left)
hud.beginPanel(14, (float)height_ - 104, 440, 88,
glm::vec4(0.02f, 0.03f, 0.05f, 0.7f),
glm::vec4(0.5f, 0.8f, 0.9f, 0.5f), 2.0f);
hud.drawPanelBackgroundNow();
hud.label("ODYSSEY", 18.0f, glm::vec3(0.8f, 0.95f, 1.0f), true);
hud.separator();
hud.label("WASD MOVE SPACE/LCTRL FLY SHIFT SPRINT", 13.0f,
glm::vec3(0.85f, 0.9f, 0.85f));
hud.label("F OR LMB THROW 1-6 OR Q/E SELECT ITEM", 13.0f,
glm::vec3(0.85f, 0.9f, 0.85f));
hud.endPanel();
// Crosshair at center
{
float cx = width_ * 0.5f, cy = height_ * 0.5f;
hud.rect(cx - 10, cy - 1, 7, 2, glm::vec4(1, 1, 1, 0.7f));
hud.rect(cx + 3, cy - 1, 7, 2, glm::vec4(1, 1, 1, 0.7f));
hud.rect(cx - 1, cy - 10, 2, 7, glm::vec4(1, 1, 1, 0.7f));
hud.rect(cx - 1, cy + 3, 2, 7, glm::vec4(1, 1, 1, 0.7f));
}
hud.flush();
}
};
// ---------------------------------------------
// BuildersDemo — top-down "business village" foundation.
// - Camera: fixed-pitch bird's-eye, WASD pans, scroll/+/- zooms
// - Left-click on ground to place a building at the cursor
// - Keys 1-4 pick which building TYPE to place next
// - Each building spawns 3 animated "workers" that wander around it
// - Each building has a business idea (displayed above it)
// - Mock worker messages post periodically to a corner log (placeholder
// for Claude-backed task output in the next round)
// - Y key: cycle which building's messages show in detail
// ---------------------------------------------
class BuildersDemo : public Engine {
struct Worker {
glm::vec3 pos;
glm::vec3 target;
float speed;
std::shared_ptr<GameObject> obj;
std::shared_ptr<GameObject> head; // tiny sphere on top
};
struct Building {
int type_idx;
std::string business;
glm::vec3 pos;
std::shared_ptr<GameObject> box;
std::shared_ptr<GameObject> roof;
std::vector<Worker> workers;
float next_message_in = 2.0f;
};
std::vector<Building> buildings_;
std::deque<std::string> message_log_; // last N messages
static constexpr int kMaxMessages = 12;
int active_btype_ = 0;
bool mouse_prev_down_ = false;
float cam_x_ = 0.0f, cam_z_ = 0.0f;
float cam_height_ = 40.0f; // zoom via scroll
float cam_yaw_ = 0.0f; // 0 = camera facing +z (straight down-forward)
float cam_pitch_frac_ = 0.35f; // how far the target is offset forward
float pan_speed_ = 30.0f;
float place_cooldown_ = 0.0f;
float map_radius_ = 90.0f; // soft boundary; camera clamped to this circle
bool gp_sel_prev_[4] = {false,false,false,false};
bool gp_place_prev_ = false;
std::mt19937 rng_{31415};
// Business templates — 4 types with distinct color + prompt seed
struct BType {
const char* label;
const char* example_business;
glm::vec3 roof_color; // emissive
glm::vec3 wall_color;
};
const BType kBTypes[4] = {
{"Tech Startup", "SaaS dashboard for mobile analytics",
glm::vec3(0.4f, 1.6f, 2.4f), glm::vec3(0.85f, 0.88f, 0.95f)},
{"Design Studio", "Brand identity for indie games",
glm::vec3(2.4f, 0.7f, 1.6f), glm::vec3(0.95f, 0.82f, 0.85f)},
{"Research Lab", "Material science for EV batteries",
glm::vec3(0.8f, 2.4f, 2.0f), glm::vec3(0.78f, 0.95f, 0.92f)},
{"Workshop", "Custom handcrafted furniture",
glm::vec3(2.4f, 1.4f, 0.5f), glm::vec3(0.92f, 0.80f, 0.60f)},
};
public:
BuildersDemo()
: Engine(1920, 1080, "ZoomEngine - Builders (top-down)") {}
bool OnInitialize() override {
// Overcast flat lighting — reads top-down well, minimal harsh shadows
processAgentCommand(json::parse(R"({
"action":"sun",
"direction":[-0.25, 0.95, -0.15],
"color":[1.85, 1.80, 1.70],
"zenith":[0.45, 0.55, 0.70],
"horizon":[0.85, 0.85, 0.82],
"sky":[0.72, 0.76, 0.80],
"ground":[0.10, 0.11, 0.12],
"shadow_extent":100,
"exposure":0.95,
"bloom_threshold":1.25,
"bloom_intensity":1.0,
"fog_density":0.002
})"));
processAgentCommand(json::parse(R"({"action":"water","enabled":false})"));
auto rnd = [&](float lo, float hi) {
std::uniform_real_distribution<float> d(lo, hi); return d(rng_);
};
auto jit = [&](float s) { return rnd(-s, s); };
GLuint grass_tex = TextureCache::Get("grass");
GLuint dirt_t = TextureCache::Get("dirt");
GLuint rock_tex = TextureCache::Get("rock");
GLuint concrete = TextureCache::Get("concrete");
GLuint stripes_t = TextureCache::Get("stripes");
GLuint marble_t = TextureCache::Get("marble");
GLuint wood_tex = TextureCache::Get("wood");
GLuint noise_t = TextureCache::Get("noise");
// --- Varied terrain: 3x3 tiles (60 units each) with different
// texture + color palettes so zones read distinctly from above.
struct Tile { GLuint tex; glm::vec3 col; float uv; float rough; };
const Tile tiles[9] = {
{ grass_tex, glm::vec3(0.75f, 0.90f, 0.70f), 0.20f, 0.95f }, // NW green
{ dirt_t, glm::vec3(0.92f, 0.80f, 0.55f), 0.25f, 0.95f }, // N dune
{ grass_tex, glm::vec3(0.70f, 0.85f, 0.75f), 0.22f, 0.95f }, // NE green-cool
{ rock_tex, glm::vec3(0.78f, 0.76f, 0.72f), 0.22f, 0.85f }, // W rocky
{ marble_t, glm::vec3(0.92f, 0.90f, 0.85f), 0.20f, 0.55f }, // C plaza
{ grass_tex, glm::vec3(0.80f, 0.88f, 0.78f), 0.20f, 0.92f }, // E grass
{ stripes_t, glm::vec3(0.78f, 0.85f, 0.75f), 0.15f, 0.90f }, // SW field
{ grass_tex, glm::vec3(0.78f, 0.92f, 0.72f), 0.22f, 0.92f }, // S grass
{ dirt_t, glm::vec3(0.85f, 0.76f, 0.55f), 0.25f, 0.95f }, // SE dirt
};
const float kTile = 60.0f;
for (int j = 0; j < 3; ++j) {
for (int i = 0; i < 3; ++i) {
int idx = j * 3 + i;
float x = (i - 1) * kTile;
float z = (j - 1) * kTile;
auto t = CreateGameObject(
CreatePlane(kTile, kTile, tiles[idx].col),
glm::vec3(x, 0.0f, z));
t->SetTexture(tiles[idx].tex);
t->SetNormalMap(noise_t);
t->SetUVScale(glm::vec2(tiles[idx].uv));
t->SetRoughness(tiles[idx].rough);
AddGameObject(t);
}
}
// --- Central plaza rings (unchanged visual focus) ---
for (int i = 1; i <= 3; ++i) {
auto ring = CreateGameObject(
CreateCylinder((float)i * 4.0f, 0.06f, 48,
glm::vec3(0.94f, 0.92f, 0.88f)),
glm::vec3(0, 0.015f, 0));
ring->SetTexture(marble_t); ring->SetNormalMap(noise_t);
ring->SetRoughness(0.45f);
AddGameObject(ring);
}
// --- Map border: DENSE ring of trees inside the soft cam bound ---
// Two rows of trees so it reads as a real wall, not a thin fence.
auto border_tree = [&](float x, float z, float s, float rough = 0.85f) {
auto trunk = CreateGameObject(
CreateCylinder(0.30f * s, 2.8f * s, 10,
glm::vec3(0.80f, 0.72f, 0.60f)),
glm::vec3(x, 1.4f * s, z));
trunk->SetRotation(glm::vec3(jit(3), jit(360), jit(3)));
trunk->SetTexture(wood_tex); trunk->SetNormalMap(noise_t);
trunk->SetRoughness(rough);
AddGameObject(trunk);
glm::vec3 cc(rnd(0.10f, 0.22f), rnd(0.38f, 0.55f), rnd(0.12f, 0.22f));
auto cone = CreateGameObject(
CreateCone(1.8f * s, 4.2f * s, 10, cc),
glm::vec3(x + jit(0.15f), 3.2f * s, z + jit(0.15f)));
cone->SetNormalMap(noise_t); cone->SetRoughness(0.8f);
AddGameObject(cone);
};
const float border_r_inner = 94.0f;
const float border_r_outer = 100.0f;
for (int i = 0; i < 48; ++i) {
float a = (float)i / 48.0f * 2 * (float)M_PI + jit(0.02f);
float s = rnd(1.0f, 1.6f);
border_tree(std::cos(a) * border_r_inner + jit(2.0f),
std::sin(a) * border_r_inner + jit(2.0f),
s);
}
for (int i = 0; i < 40; ++i) {
float a = (float)i / 40.0f * 2 * (float)M_PI + 0.065f;
float s = rnd(1.1f, 1.7f);
border_tree(std::cos(a) * border_r_outer + jit(2.5f),
std::sin(a) * border_r_outer + jit(2.5f),
s);
}
// --- Outer mountain ring (backdrop + final boundary) ---
for (int i = 0; i < 28; ++i) {
float a = (float)i / 28.0f * 2 * (float)M_PI + jit(0.05f);
float R = 160.0f + rnd(-10.0f, 15.0f);
float sc = 1.0f + ((float)((i * 47) % 100) / 100.0f) * 0.6f;
float h = 30.0f * sc;
auto m = CreateGameObject(
CreateCone(15.0f * sc, h, 10, glm::vec3(0.22f, 0.21f, 0.24f)),
glm::vec3(std::cos(a) * R, h * 0.5f, std::sin(a) * R));
m->SetRoughness(0.9f);
AddGameObject(m);
}
// --- A few scattered trees inside the play area for variety ---
for (int i = 0; i < 18; ++i) {
float x = rnd(-80.0f, 80.0f);
float z = rnd(-80.0f, 80.0f);
// avoid the central plaza / spawn area
if (std::abs(x) < 16.0f && std::abs(z) < 16.0f) continue;
border_tree(x, z, rnd(0.7f, 1.2f));
}
// Initial log message
AddMessage("[System] BuildersDemo ready. Click ground to place a building.");
AddMessage("[System] Keys 1-4: select building type. F or LMB: place.");
// Initial camera: top-down with slight pitch so mouse picking works
cam_x_ = 0.0f; cam_z_ = 0.0f;
UpdateCamera();
return true;
}
void UpdateCamera() {
// High-angle view that rotates around the target. Yaw determines the
// horizontal direction the camera is pushed from, so "forward" for
// the player is toward +Z in view-space.
float cy = std::cos(glm::radians(cam_yaw_));
float sy = std::sin(glm::radians(cam_yaw_));
float back = cam_height_ * cam_pitch_frac_;
glm::vec3 eye(cam_x_ + sy * back, cam_height_, cam_z_ + cy * back);
glm::vec3 tgt(cam_x_, 0.0f, cam_z_);
SetCameraPosition(eye);
SetCameraTarget(tgt);
}
void AddMessage(const std::string& msg) {
message_log_.push_back(msg);
while ((int)message_log_.size() > kMaxMessages) message_log_.pop_front();
std::cout << msg << "\n";
}
void PlaceBuilding(glm::vec3 pos) {
// Check it's not too close to another building
for (auto& b : buildings_) {
if (glm::length(glm::vec2(b.pos.x - pos.x, b.pos.z - pos.z)) < 5.0f) {
AddMessage("[System] Too close to another building — skipped.");
return;
}
}
Building b;
b.type_idx = active_btype_;
b.business = kBTypes[active_btype_].example_business;
b.pos = glm::vec3(pos.x, 0, pos.z);
// Walls
auto box = CreateGameObject(
CreateCube(kBTypes[active_btype_].wall_color),
glm::vec3(pos.x, 2.0f, pos.z));
box->SetScale(glm::vec3(3.0f, 4.0f, 3.0f));
box->SetTexture(TextureCache::Get("concrete"));
box->SetNormalMap(TextureCache::Get("noise"));
box->SetRoughness(0.75f);
AddGameObject(box);
b.box = box;
// Roof — emissive so it glows with type color
auto roof = CreateGameObject(
CreateCube(glm::vec3(1.0f)),
glm::vec3(pos.x, 4.2f, pos.z));
roof->SetScale(glm::vec3(3.3f, 0.4f, 3.3f));
roof->SetMetallic(0.3f);
roof->SetRoughness(0.35f);
roof->SetEmissive(kBTypes[active_btype_].roof_color);
AddGameObject(roof);
b.roof = roof;
// 3 workers around the building
std::uniform_real_distribution<float> ang(0.0f, 2 * M_PI);
for (int i = 0; i < 3; ++i) {
Worker w;
float a = ang(rng_);
w.pos = glm::vec3(pos.x + std::cos(a) * 2.2f,
0.0f,
pos.z + std::sin(a) * 2.2f);
w.target = w.pos;
w.speed = 1.5f + (rng_() % 10) * 0.1f;
// Body
auto body = CreateGameObject(
CreateCylinder(0.22f, 0.9f, 10,
glm::vec3(0.95f, 0.95f, 0.95f)),
glm::vec3(w.pos.x, 0.45f, w.pos.z));
body->SetMetallic(0.0f); body->SetRoughness(0.6f);
AddGameObject(body);
w.obj = body;
// Head (emissive to match building color)
auto head = CreateGameObject(
CreateSphere(12, glm::vec3(1.0f)),
glm::vec3(w.pos.x, 1.05f, w.pos.z));
head->SetScale(glm::vec3(0.25f));
head->SetEmissive(kBTypes[active_btype_].roof_color * 0.6f);
AddGameObject(head);
w.head = head;
b.workers.push_back(w);
}
buildings_.push_back(std::move(b));
AddMessage(std::string("[") + kBTypes[active_btype_].label + "] Building #"
+ std::to_string((int)buildings_.size())
+ " opened. Business: " + kBTypes[active_btype_].example_business);
}
void StepWorkers(float dt) {
std::uniform_real_distribution<float> offs(-2.4f, 2.4f);
for (auto& b : buildings_) {
for (auto& w : b.workers) {
glm::vec3 to = w.target - w.pos;
float d = glm::length(to);
if (d < 0.05f) {
// Pick new random target within building's zone
w.target = b.pos + glm::vec3(offs(rng_), 0, offs(rng_));
} else {
glm::vec3 step = (to / d) * w.speed * dt;
if (glm::length(step) > d) step = to;
w.pos += step;
w.obj->SetPosition(glm::vec3(w.pos.x, 0.45f, w.pos.z));
w.head->SetPosition(glm::vec3(w.pos.x, 1.05f, w.pos.z));
// Simple bob to suggest walking
float bob = 0.04f * std::sin((float)glfwGetTime() * 6.0f
+ w.speed * 2.0f);
w.obj->SetPosition(glm::vec3(w.pos.x, 0.45f + bob, w.pos.z));
w.head->SetPosition(glm::vec3(w.pos.x, 1.05f + bob, w.pos.z));
}
}
}
}
void TickMessages(float dt) {
// Each building periodically posts a mock "work result" — in V2
// this is replaced by a real Claude API call driven by a helper.
static const std::array<std::array<const char*, 5>, 4> templates = {{
{ "drafted a wireframe for the onboarding flow",
"committed a PR with auth refactors",
"reviewed metrics, CTR up 3.2%",
"spiked a new analytics aggregation",
"shipped a bug fix for chart rendering" },
{ "pitched a new logo direction",
"uploaded four mood-board options",
"iterated on the brand color system",
"finalized the key art for Chapter 2",
"drafted marketing copy for the press kit" },
{ "ran an overnight electrolyte viscosity test",
"calibrated the coin-cell jig",
"drafted a technical note on dendrite growth",
"annotated the SEM images from batch 7",
"updated the doping-ratio ablation chart" },
{ "milled a new maple chair leg set",
"sanded the walnut desk to 600-grit",
"applied the first oil coat to the bookshelf",
"sketched a new modular shelving concept",
"shipped the dining-table order to the client" },
}};
for (auto& b : buildings_) {
b.next_message_in -= dt;
if (b.next_message_in <= 0.0f) {
b.next_message_in = 4.0f + (rng_() % 40) / 10.0f;
int widx = rng_() % (int)b.workers.size();
const auto& tmpls = templates[b.type_idx];
int tidx = rng_() % (int)tmpls.size();
std::ostringstream ss;
ss << "[" << kBTypes[b.type_idx].label << "#"
<< (&b - &buildings_[0] + 1)
<< "] Worker " << (widx + 1) << ": " << tmpls[tidx];
AddMessage(ss.str());
}
}
}
void OnUpdate(float dt) override {
// -------- Gamepad pre-sample --------
glm::vec2 ls(0), rs(0);
float gp_in = 0.0f, gp_out = 0.0f;
bool gp_sprint = false;
bool gp_place = false;
bool gp_pick[4] = {false,false,false,false};
if (Input::IsGamepadConnected()) {
ls = Input::GetLeftStick();
rs = Input::GetRightStick();
gp_out = Input::GetR2(); // zoom out (raise camera)
gp_in = Input::GetL2(); // zoom in
gp_sprint = Input::IsGamepadButtonDown(Input::GamepadButton::Cross);
gp_place = Input::IsGamepadButtonDown(Input::GamepadButton::R1);
gp_pick[0] = Input::IsGamepadButtonDown(Input::GamepadButton::Square);
gp_pick[1] = Input::IsGamepadButtonDown(Input::GamepadButton::Triangle);
gp_pick[2] = Input::IsGamepadButtonDown(Input::GamepadButton::Circle);
// Cross is used for sprint; use D-pad down / Cross only if others
// are rarely pressed. Treat D-pad as fallback. For V1 just skip
// a 4th gamepad type-select slot (fine — keyboard has 1-4).
}
// -------- Yaw (camera rotation) --------
float yaw_rate = 80.0f; // deg/sec
if (Input::IsKeyHeld(GLFW_KEY_Q)) cam_yaw_ -= yaw_rate * dt;
if (Input::IsKeyHeld(GLFW_KEY_E)) cam_yaw_ += yaw_rate * dt;
if (Input::IsKeyHeld(GLFW_KEY_LEFT)) cam_yaw_ -= yaw_rate * dt;
if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) cam_yaw_ += yaw_rate * dt;
cam_yaw_ += rs.x * yaw_rate * dt; // gamepad right stick X
while (cam_yaw_ > 360.0f) cam_yaw_ -= 360.0f;
while (cam_yaw_ < -360.0f) cam_yaw_ += 360.0f;
// -------- Pan (keyboard + gamepad left stick), relative to yaw --------
float speed = pan_speed_;
if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT) || gp_sprint) speed *= 2.5f;
float ix = 0.0f, iz = 0.0f;
if (Input::IsKeyHeld(GLFW_KEY_W)) iz -= 1.0f;
if (Input::IsKeyHeld(GLFW_KEY_S)) iz += 1.0f;
if (Input::IsKeyHeld(GLFW_KEY_A)) ix -= 1.0f;
if (Input::IsKeyHeld(GLFW_KEY_D)) ix += 1.0f;
ix += ls.x; iz += ls.y;
// Rotate pan vector by yaw so "forward" tracks camera direction
float cy = std::cos(glm::radians(cam_yaw_));
float sy = std::sin(glm::radians(cam_yaw_));
float wx = ix * cy + iz * sy;
float wz = -ix * sy + iz * cy;
cam_x_ += wx * speed * dt;
cam_z_ += wz * speed * dt;
// Zoom (keyboard +/-, gamepad R2/L2, up/down arrows)
if (Input::IsKeyHeld(GLFW_KEY_EQUAL) || gp_in > 0.05f)
cam_height_ = std::max(12.0f, cam_height_ - (15.0f + gp_in * 25.0f) * dt);
if (Input::IsKeyHeld(GLFW_KEY_MINUS) || gp_out > 0.05f)
cam_height_ = std::min(130.0f, cam_height_ + (15.0f + gp_out * 25.0f) * dt);
if (Input::IsKeyHeld(GLFW_KEY_UP)) cam_height_ = std::max(12.0f, cam_height_ - 18.0f * dt);
if (Input::IsKeyHeld(GLFW_KEY_DOWN)) cam_height_ = std::min(130.0f, cam_height_ + 18.0f * dt);
// Soft circular clamp to keep camera from leaving the map
{
float r = std::sqrt(cam_x_ * cam_x_ + cam_z_ * cam_z_);
if (r > map_radius_) {
float k = map_radius_ / r;
cam_x_ *= k;
cam_z_ *= k;
}
}
UpdateCamera();
// -------- Building type select (keyboard + gamepad face buttons edge) --------
if (Input::IsKeyPressed(GLFW_KEY_1)) { active_btype_ = 0; AddMessage("[System] Placing: Tech Startup"); }
if (Input::IsKeyPressed(GLFW_KEY_2)) { active_btype_ = 1; AddMessage("[System] Placing: Design Studio"); }
if (Input::IsKeyPressed(GLFW_KEY_3)) { active_btype_ = 2; AddMessage("[System] Placing: Research Lab"); }
if (Input::IsKeyPressed(GLFW_KEY_4)) { active_btype_ = 3; AddMessage("[System] Placing: Workshop"); }
for (int i = 0; i < 3; ++i) {
if (gp_pick[i] && !gp_sel_prev_[i]) {
active_btype_ = i;
const char* label = (i == 0 ? "Tech Startup" :
i == 1 ? "Design Studio" : "Research Lab");
AddMessage(std::string("[System] Placing: ") + label);
}
gp_sel_prev_[i] = gp_pick[i];
}
// -------- Place building (mouse/F/R1, edge-triggered) --------
place_cooldown_ -= dt;
bool mouse_now = Input::IsMouseButtonPressed(Input::MouseButton::Left);
bool place = (mouse_now && !mouse_prev_down_)
|| Input::IsKeyPressed(GLFW_KEY_F)
|| (gp_place && !gp_place_prev_);
mouse_prev_down_ = mouse_now;
gp_place_prev_ = gp_place;
if (place && place_cooldown_ <= 0.0f) {
// With gamepad + no mouse cursor, just drop at the camera target.
glm::vec3 hit = gp_place
? glm::vec3(cam_x_, 0.0f, cam_z_)
: ScreenToGround();
if (std::abs(hit.x) + std::abs(hit.z) > 0.001f ||
(gp_place && (cam_x_ != 0 || cam_z_ != 0))) {
PlaceBuilding(hit);
place_cooldown_ = 0.25f;
}
}
StepWorkers(dt);
TickMessages(dt);
}
void OnRenderOverlay() override {
Hud hud(font_, width_, height_);
// ====== TOP-LEFT: Build panel ======
const BType& bt = kBTypes[active_btype_];
hud.beginPanel(14, (float)height_ - 210, 480, 182,
glm::vec4(0.02f, 0.03f, 0.05f, 0.75f),
glm::vec4(bt.roof_color, 0.75f),
2.5f);
hud.drawPanelBackgroundNow();
hud.icon(14, 14, glm::vec4(bt.roof_color, 1.0f));
hud.label(std::string("PLACING: ") + bt.label, 22.0f,
glm::vec3(1, 1, 1), true);
hud.label(bt.example_business, 16.0f,
glm::vec3(0.85f, 0.95f, 0.9f));
hud.separator();
hud.label("[1][2][3][4] type LMB/F place", 14.0f,
glm::vec3(0.7f, 0.85f, 0.75f));
hud.label("WASD pan +/- zoom", 14.0f,
glm::vec3(0.7f, 0.85f, 0.75f));
hud.endPanel();
// ====== TOP-RIGHT: Stats panel ======
hud.beginPanel((float)width_ - 320, (float)height_ - 156, 300, 128,
glm::vec4(0.02f, 0.03f, 0.05f, 0.75f),
glm::vec4(0.5f, 0.75f, 0.9f, 0.6f),
2.5f);
hud.drawPanelBackgroundNow();
hud.label("VILLAGE STATS", 19.0f, glm::vec3(0.6f, 0.85f, 1.0f), true);
hud.separator();
char buf[128];
std::snprintf(buf, sizeof(buf), "Buildings: %d", (int)buildings_.size());
hud.label(buf, 17.0f, glm::vec3(1, 1, 1));
std::snprintf(buf, sizeof(buf), "Workers: %d", (int)(buildings_.size() * 3));
hud.label(buf, 17.0f, glm::vec3(1, 1, 1));
hud.endPanel();
// ====== BOTTOM-LEFT: Message log ======
float log_h = std::min(360.0f, 48.0f + (float)message_log_.size() * 22.0f);
hud.beginPanel(14, 14, 720, log_h,
glm::vec4(0.02f, 0.03f, 0.05f, 0.75f),
glm::vec4(0.9f, 0.9f, 0.9f, 0.35f),
2.0f);
hud.drawPanelBackgroundNow();
hud.label("AGENT LOG", 16.0f, glm::vec3(0.7f, 0.9f, 1.0f), true);
hud.separator();
int count = (int)message_log_.size();
int shown = std::min(count, 10);
for (int i = count - 1; i >= std::max(0, count - shown); --i) {
float a = 0.45f + 0.55f * float(i + 1 - (count - shown)) / float(shown);
hud.label(message_log_[i], 14.0f,
glm::vec3(1.0f, 1.0f, 1.0f) * a);
}
hud.endPanel();
// ====== Per-building floating labels + capacity bars ======
glm::mat4 view = glm::lookAt(camera_position_, camera_target_, camera_up_);
glm::mat4 proj = glm::perspective(glm::radians(settings_.fov),
float(width_) / height_, 0.1f, settings_.drawDistance);
glm::mat4 vp = proj * view;
for (int i = 0; i < (int)buildings_.size(); ++i) {
const Building& b = buildings_[i];
glm::vec4 clip = vp * glm::vec4(b.pos.x, 6.0f, b.pos.z, 1.0f);
if (clip.w <= 0.01f) continue;
glm::vec3 ndc = glm::vec3(clip) / clip.w;
if (std::abs(ndc.x) > 1.2f || std::abs(ndc.y) > 1.2f) continue;
float sx = (ndc.x * 0.5f + 0.5f) * width_;
float sy = (ndc.y * 0.5f + 0.5f) * height_;
const BType& bt2 = kBTypes[b.type_idx];
hud.rect(sx - 70, sy + 6, 140, 22,
glm::vec4(0.0f, 0.0f, 0.0f, 0.6f));
hud.rect(sx - 70, sy + 28, 140, 2, glm::vec4(bt2.roof_color, 0.9f));
hud.text(sx - 64, sy + 11,
std::string(bt2.label) + " #" + std::to_string(i + 1),
12.0f, glm::vec3(1.0f), true);
}
hud.flush();
}
};
// ---------------------------------------------
// FourMapDemo — large four-quadrant themed map (160×160).
// NW Forest — mushrooms, conifers, moss rocks, fallen logs
// NE Sakura — cherry groves, stone lanterns, torii, blossoms
// SW Desert — cacti, dunes, step pyramid, obelisk, sand rocks
// SE Tech — chrome pylons, neon bars, crate pyramids, glowing monolith
// CENTER — sunken physics pool (buoyancy only inside the 6-unit radius)
//
// Zone atmospheres blend smoothly as the camera moves: sun color, sky,
// bloom threshold/intensity, fog density — each zone has its own recipe.
//
// F / LMB / R1 — shoot HDR metal ball
// ---------------------------------------------
class FourMapDemo : public Engine {
float cam_yaw_ = 225.0f;
float cam_pitch_ = -10.0f;
float look_sensitivity_ = 80.0f;
glm::vec3 last_cam_pos_{0};
// Terrain params — heights for sakura (NE) and desert (SW) quadrants
static constexpr float kSakuraMaxH = 3.2f;
static constexpr float kDesertMaxH = 2.6f;
static constexpr float kTerrainFalloff = 8.0f;
static constexpr float kMapHalf = 78.0f;
// Walking character
static constexpr float kEyeH = 1.72f;
static constexpr float kWalkSpeed = 6.5f;
static constexpr float kRunMult = 2.2f;
static constexpr float kJumpV = 7.5f;
static constexpr float kGravity = 20.0f;
static constexpr float kStepUp = 1.4f; // max ground-height rise per move allowed
float vy_ = 0.0f;
bool grounded_ = true;
bool fly_mode_ = false;
bool fly_prev_ = false;
bool jump_prev_ = false;
// First-person arm
std::shared_ptr<GameObject> arm_upper_, arm_lower_, hand_;
bool arm_ready_ = false;
float throw_anim_ = 0.0f;
float shot_cooldown_ = 0.0f;
int shot_counter_ = 0;
std::deque<std::string> active_shots_;
static constexpr int kMaxShots = 80;
std::mt19937 rng_{2026};
public:
FourMapDemo()
: Engine(1920, 1080, "ZoomEngine - Four-Quadrant Map") {}
// Sample the ground height used by the mesh generator in each quadrant.
// Returns 0 outside the sakura/desert zones, tapered near quadrant edges.
static float SampleTerrainH(float wx, float wz) {
float mh = 0.0f, lx = 0.0f, lz = 0.0f;
bool inside = false;
// Sakura NE centered at (40, 40), local 80x80
if (wx >= 0.0f && wz >= 0.0f && wx <= 80.0f && wz <= 80.0f) {
mh = kSakuraMaxH; lx = wx - 40.0f; lz = wz - 40.0f; inside = true;
}
// Desert SW centered at (-40, -40)
else if (wx <= 0.0f && wz <= 0.0f && wx >= -80.0f && wz >= -80.0f) {
mh = kDesertMaxH; lx = wx + 40.0f; lz = wz + 40.0f; inside = true;
}
if (!inside) return 0.0f;
float h = std::sin(lx * 0.1f) * std::cos(lz * 0.1f) * mh * 0.5f;
h += std::sin(lx * 0.25f + 1.3f) * std::cos(lz * 0.3f + 0.7f) * mh * 0.25f;
h += std::sin(lx * 0.6f + 2.1f) * std::cos(lz * 0.5f + 1.5f) * mh * 0.125f;
// Match the edge falloff applied in TerrainPrimitive
float dx_edge = 40.0f - std::fabs(lx);
float dz_edge = 40.0f - std::fabs(lz);
float d = std::min(dx_edge, dz_edge);
float f = std::clamp(d / kTerrainFalloff, 0.0f, 1.0f);
f = f * f * (3.0f - 2.0f * f);
return h * f;
}
void bodyCmd(const std::string& id, const std::string& shape,
float mass, float rest, float frict, bool is_static = false) {
json::Object c;
c["action"]=json::Value(std::string("body"));
c["id"]=json::Value(id);
c["shape"]=json::Value(shape);
c["mass"]=json::Value((double)mass);
c["restitution"]=json::Value((double)rest);
c["friction"]=json::Value((double)frict);
if (is_static) c["static"]=json::Value(true);
processAgentCommand(json::Value(c));
}
std::shared_ptr<GameObject>
addBox(const std::string& id, glm::vec3 pos, glm::vec3 scale,
glm::vec3 color, GLuint tex, GLuint nmap,
float metallic, float roughness, float mass,
float rest = 0.2f, float frict = 0.6f,
glm::vec3 rot = glm::vec3(0), glm::vec3 emissive = glm::vec3(0)) {
auto obj = CreateGameObject(CreateCube(color), pos);
obj->SetScale(scale);
obj->SetRotation(rot);
if (tex) obj->SetTexture(tex);
if (nmap) obj->SetNormalMap(nmap);
obj->SetMetallic(metallic);
obj->SetRoughness(roughness);
if (glm::length(emissive) > 0) obj->SetEmissive(emissive);
AddGameObject(obj);
if (!id.empty()) namedObjects_[id] = obj;
if (mass > 0) bodyCmd(id, "box", mass, rest, frict, false);
else if (mass == -1.0f) bodyCmd(id, "box", 0, rest, frict, true);
return obj;
}
std::shared_ptr<GameObject>
addSphere(const std::string& id, glm::vec3 pos, float r,
glm::vec3 color, GLuint tex, GLuint nmap,
float metallic, float roughness, float mass,
float rest = 0.35f, float frict = 0.35f,
glm::vec3 emissive = glm::vec3(0)) {
auto obj = CreateGameObject(CreateSphere(28, color), pos);
obj->SetScale(glm::vec3(r));
if (tex) obj->SetTexture(tex);
if (nmap) obj->SetNormalMap(nmap);
obj->SetMetallic(metallic);
obj->SetRoughness(roughness);
if (glm::length(emissive) > 0) obj->SetEmissive(emissive);
AddGameObject(obj);
if (!id.empty()) namedObjects_[id] = obj;
if (mass > 0) bodyCmd(id, "sphere", mass, rest, frict);
return obj;
}
bool OnInitialize() override {
auto rnd = [&](float lo, float hi) {
std::uniform_real_distribution<float> d(lo, hi); return d(rng_);
};
auto jit = [&](float s) { return rnd(-s, s); };
// --- Base palette: reduced fog, bright daylight. Gets blended per
// zone each frame in UpdateAtmosphere. Values here are the starting
// point before the camera picks a zone.
processAgentCommand(json::parse(R"({
"action":"sun",
"direction":[-0.45, 0.60, -0.25],
"color":[2.05, 1.85, 1.60],
"zenith":[0.26, 0.42, 0.65],
"horizon":[0.82, 0.80, 0.74],
"sky":[0.65, 0.72, 0.78],
"ground":[0.08, 0.09, 0.10],
"shadow_extent":120,
"exposure":0.95,
"bloom_threshold":1.2,
"bloom_intensity":1.0,
"fog_density":0.003
})"));
// Physics-enabled pool: primary water mesh restricted to a small
// central disk via bounds. Objects outside get no buoyancy; objects
// inside bob in the pool properly. Water mesh size matches bounds
// diameter so it doesn't visually spill.
processAgentCommand(json::parse(R"({
"action":"water","enabled":true,
"level":0.7,"size":11,
"amplitude":0.07,"wavelength":2.6,"speed":0.8,
"shallow":[0.52, 0.68, 0.70],
"deep":[0.04, 0.18, 0.28],
"density":2.2,"resolution":96,
"bounds_center":[0,0,0],"bounds_radius":5.0
})"));
processAgentCommand(json::parse(R"({
"action":"weather","wind_dir":[1,0,0.2],"wind_speed":0.2,
"storminess":0.0,"current_coef":0.0
})"));
GLuint grass_tex = TextureCache::Get("grass");
GLuint rock_tex = TextureCache::Get("rock");
GLuint wood_tex = TextureCache::Get("wood");
GLuint dirt_t = TextureCache::Get("dirt");
GLuint concrete = TextureCache::Get("concrete");
GLuint marble_t = TextureCache::Get("marble");
GLuint checker_t = TextureCache::Get("checker");
GLuint grid_t = TextureCache::Get("grid");
GLuint noise_t = TextureCache::Get("noise");
// --- Four quadrant ground planes (80×80 each, map total 160×160) ---
auto forest_ground = CreateGameObject(
CreatePlane(80.0f, 80.0f, glm::vec3(0.72f, 0.82f, 0.66f)),
glm::vec3(-40.0f, 0.0f, 40.0f));
forest_ground->SetTexture(grass_tex); forest_ground->SetNormalMap(noise_t);
forest_ground->SetUVScale(glm::vec2(0.3f)); forest_ground->SetRoughness(0.95f);
AddGameObject(forest_ground);
// Sakura quadrant: deformed heightmap terrain, edges taper to 0
auto sakura_ground = CreateGameObject(
CreateTerrain(80, 80.0f, kSakuraMaxH,
glm::vec3(1.0f, 0.82f, 0.85f),
kTerrainFalloff, true),
glm::vec3(40.0f, 0.0f, 40.0f));
sakura_ground->SetTexture(grass_tex); sakura_ground->SetNormalMap(noise_t);
sakura_ground->SetUVScale(glm::vec2(0.3f)); sakura_ground->SetRoughness(0.92f);
AddGameObject(sakura_ground);
// Desert quadrant: variable-height deformed plane (like the dune style)
auto desert_ground = CreateGameObject(
CreateTerrain(80, 80.0f, kDesertMaxH,
glm::vec3(1.05f, 0.88f, 0.62f),
kTerrainFalloff, true),
glm::vec3(-40.0f, 0.0f, -40.0f));
desert_ground->SetTexture(dirt_t); desert_ground->SetNormalMap(noise_t);
desert_ground->SetUVScale(glm::vec2(0.35f)); desert_ground->SetRoughness(0.98f);
AddGameObject(desert_ground);
auto tech_ground = CreateGameObject(
CreatePlane(80.0f, 80.0f, glm::vec3(0.3f, 0.32f, 0.38f)),
glm::vec3(40.0f, 0.0f, -40.0f));
tech_ground->SetTexture(grid_t); tech_ground->SetNormalMap(noise_t);
tech_ground->SetUVScale(glm::vec2(0.5f));
tech_ground->SetMetallic(0.6f);
tech_ground->SetRoughness(0.3f);
AddGameObject(tech_ground);
// --- Physics ground plane: infinite static plane at y=0 covers everything
// We register one of the quadrants as the physics body, but the plane
// collider itself is infinite so it catches objects anywhere.
namedObjects_["__ground"] = forest_ground;
processAgentCommand(json::parse(R"({
"action":"body","id":"__ground","shape":"plane",
"normal":[0,1,0],"offset":0,"static":true
})"));
// --- Perimeter physics borders: static stone walls ringing the
// 160×160 map (X,Z in [-80,80]) so the player and dynamic props
// can't leave the play area. Pink-tinted marble blends with the
// sakura quadrant and reads as neutral stone against the others.
{
const float kBL = 162.0f; // wall length (slight corner overlap)
const float kBH = 4.0f; // wall height (above jump apex)
const float kBT = 1.0f; // wall thickness
const glm::vec3 kBC(0.95f, 0.82f, 0.85f);
// North wall (z = +80)
addBox("border_n", glm::vec3(0.0f, kBH * 0.5f, 80.5f),
glm::vec3(kBL, kBH, kBT), kBC,
marble_t, noise_t, 0.0f, 0.75f, -1.0f);
// South wall (z = -80)
addBox("border_s", glm::vec3(0.0f, kBH * 0.5f, -80.5f),
glm::vec3(kBL, kBH, kBT), kBC,
marble_t, noise_t, 0.0f, 0.75f, -1.0f);
// East wall (x = +80)
addBox("border_e", glm::vec3( 80.5f, kBH * 0.5f, 0.0f),
glm::vec3(kBT, kBH, kBL), kBC,
marble_t, noise_t, 0.0f, 0.75f, -1.0f);
// West wall (x = -80)
addBox("border_w", glm::vec3(-80.5f, kBH * 0.5f, 0.0f),
glm::vec3(kBT, kBH, kBL), kBC,
marble_t, noise_t, 0.0f, 0.75f, -1.0f);
}
// ============================ FOREST (NW) =====================
// Trees + mossy boulders + glowing mushrooms + fallen logs
auto tree = [&](glm::vec3 at, float s) {
auto trunk = CreateGameObject(
CreateCylinder(0.22f * s, 2.6f * s, 10,
glm::vec3(0.86f, 0.80f, 0.70f)),
at + glm::vec3(0, 1.3f * s, 0));
trunk->SetRotation(glm::vec3(jit(3), jit(360), jit(3)));
trunk->SetTexture(wood_tex); trunk->SetNormalMap(noise_t);
trunk->SetRoughness(0.85f);
AddGameObject(trunk);
glm::vec3 cc(rnd(0.08f, 0.16f), rnd(0.38f, 0.55f), rnd(0.10f, 0.20f));
auto c = CreateGameObject(CreateCone(1.3f * s, 2.8f * s, 10, cc),
at + glm::vec3(0, 3.1f * s, 0));
c->SetNormalMap(noise_t); c->SetRoughness(0.8f);
AddGameObject(c);
};
for (int i = 0; i < 45; ++i) {
float x = rnd(-78.0f, -6.0f);
float z = rnd( 6.0f, 78.0f);
tree(glm::vec3(x, 0, z), rnd(0.8f, 1.5f));
}
// Physics moss rocks (dynamic)
for (int i = 0; i < 14; ++i) {
float x = rnd(-76.0f, -8.0f);
float z = rnd( 8.0f, 76.0f);
float s = rnd(0.5f, 1.4f);
addSphere("frock_" + std::to_string(i),
glm::vec3(x, s + 0.1f, z), s,
glm::vec3(0.45f, 0.55f, 0.38f),
rock_tex, noise_t,
0.0f, 0.9f,
1.2f, 0.3f, 0.4f);
}
// Glowing mushrooms
for (int i = 0; i < 26; ++i) {
float x = rnd(-77.0f, -7.0f);
float z = rnd( 7.0f, 77.0f);
// Stem
auto stem = CreateGameObject(
CreateCylinder(0.12f, 0.45f, 8,
glm::vec3(0.92f, 0.90f, 0.82f)),
glm::vec3(x, 0.23f, z));
stem->SetRoughness(0.75f);
AddGameObject(stem);
// Cap (emissive for bloom)
glm::vec3 cap_col = (i % 3 == 0)
? glm::vec3(1.2f, 0.4f, 0.9f) // pink
: (i % 3 == 1 ? glm::vec3(0.3f, 1.1f, 1.3f) // cyan
: glm::vec3(1.3f, 0.9f, 0.2f)); // yellow
auto cap = CreateGameObject(
CreateCone(0.35f, 0.3f, 10, cap_col),
glm::vec3(x, 0.55f, z));
cap->SetEmissive(cap_col * 0.7f);
AddGameObject(cap);
}
// Fallen logs (dynamic — can be rolled)
for (int i = 0; i < 6; ++i) {
float x = rnd(-74.0f, -10.0f);
float z = rnd(10.0f, 74.0f);
std::string id = "flog_" + std::to_string(i);
auto log = CreateGameObject(
CreateCylinder(0.4f, 3.0f, 10, glm::vec3(0.85f, 0.72f, 0.55f)),
glm::vec3(x, 0.45f, z));
log->SetRotation(glm::vec3(0, jit(180.0f), 90.0f));
log->SetTexture(wood_tex); log->SetNormalMap(noise_t);
log->SetRoughness(0.85f);
AddGameObject(log);
namedObjects_[id] = log;
bodyCmd(id, "box", 2.0f, 0.2f, 0.6f, false);
}
// ============================ SAKURA PINK (NE) =================
// Cherry trees (pink canopy), stone lanterns, blossom petals
auto cherry = [&](glm::vec3 at, float s) {
auto trunk = CreateGameObject(
CreateCylinder(0.22f * s, 2.4f * s, 10,
glm::vec3(0.45f, 0.30f, 0.20f)),
at + glm::vec3(0, 1.2f * s, 0));
trunk->SetRotation(glm::vec3(jit(4), jit(360), jit(4)));
trunk->SetTexture(wood_tex); trunk->SetNormalMap(noise_t);
trunk->SetRoughness(0.85f);
AddGameObject(trunk);
// Pink canopy: stacked flattened spheres
for (int k = 0; k < 3; ++k) {
float r = 1.6f * s * (1.0f - k * 0.20f);
auto canopy = CreateGameObject(
CreateSphere(24, glm::vec3(1.2f, 0.55f, 0.82f)),
at + glm::vec3(jit(0.25f), 2.6f * s + k * 0.9f * s, jit(0.25f)));
canopy->SetScale(glm::vec3(r, r * 0.75f, r));
canopy->SetEmissive(glm::vec3(0.25f, 0.10f, 0.18f));
canopy->SetRoughness(0.85f);
AddGameObject(canopy);
}
};
for (int i = 0; i < 30; ++i) {
float x = rnd( 7.0f, 78.0f);
float z = rnd( 7.0f, 78.0f);
cherry(glm::vec3(x, 0, z), rnd(0.9f, 1.5f));
}
// Stone lanterns (static decorative)
for (int i = 0; i < 14; ++i) {
float x = rnd( 10.0f, 75.0f);
float z = rnd( 10.0f, 75.0f);
// Base
auto base = CreateGameObject(
CreateCube(glm::vec3(0.85f, 0.82f, 0.78f)),
glm::vec3(x, 0.3f, z));
base->SetScale(glm::vec3(0.6f, 0.3f, 0.6f));
base->SetTexture(marble_t); base->SetNormalMap(noise_t);
base->SetRoughness(0.6f);
AddGameObject(base);
// Pillar
auto pillar = CreateGameObject(
CreateCylinder(0.18f, 1.2f, 10, glm::vec3(0.88f, 0.85f, 0.80f)),
glm::vec3(x, 1.05f, z));
pillar->SetTexture(marble_t); pillar->SetNormalMap(noise_t);
pillar->SetRoughness(0.55f);
AddGameObject(pillar);
// Lamp box (emissive warm)
auto lamp = CreateGameObject(
CreateCube(glm::vec3(1.0f)),
glm::vec3(x, 1.9f, z));
lamp->SetScale(glm::vec3(0.45f));
lamp->SetEmissive(glm::vec3(3.0f, 1.9f, 0.7f));
AddGameObject(lamp);
// Roof cap
auto roof = CreateGameObject(
CreateCone(0.55f, 0.3f, 8, glm::vec3(0.45f, 0.25f, 0.2f)),
glm::vec3(x, 2.35f, z));
roof->SetRoughness(0.85f);
AddGameObject(roof);
}
// Dynamic pink rocks and crates to knock around
for (int i = 0; i < 14; ++i) {
float x = rnd( 8.0f, 78.0f);
float z = rnd( 8.0f, 78.0f);
bool sph = (i % 2 == 0);
if (sph) {
addSphere("srk_" + std::to_string(i),
glm::vec3(x, 1.0f, z), rnd(0.5f, 0.9f),
glm::vec3(0.95f, 0.80f, 0.85f),
rock_tex, noise_t,
0.0f, 0.8f,
1.0f, 0.35f, 0.4f);
} else {
addBox("sbox_" + std::to_string(i),
glm::vec3(x, 0.6f, z), glm::vec3(rnd(0.9f, 1.3f)),
glm::vec3(1.1f, 0.65f, 0.85f),
marble_t, noise_t, 0.0f, 0.55f,
0.9f, 0.15f, 0.5f,
glm::vec3(jit(15), jit(180), jit(15)));
}
}
// Floating pink petals (purely decorative, no physics)
for (int i = 0; i < 90; ++i) {
float x = rnd( 6.0f, 78.0f);
float z = rnd( 6.0f, 78.0f);
float y = rnd(1.5f, 5.0f);
auto petal = CreateGameObject(
CreateSphere(6, glm::vec3(1.4f, 0.6f, 0.85f)),
glm::vec3(x, y, z));
petal->SetScale(glm::vec3(0.08f, 0.04f, 0.08f));
petal->SetEmissive(glm::vec3(0.45f, 0.15f, 0.25f));
AddGameObject(petal);
}
// ============================ DESERT (SW) ======================
// Cacti + sand boulders + a small pyramid
for (int i = 0; i < 12; ++i) {
float x = rnd(-77.0f, -7.0f);
float z = rnd(-77.0f, -7.0f);
std::string id = "cactus_" + std::to_string(i);
float h = rnd(2.0f, 3.4f);
auto c = CreateGameObject(
CreateCylinder(0.35f, h, 8, glm::vec3(0.30f, 0.55f, 0.32f)),
glm::vec3(x, h * 0.5f, z));
c->SetRoughness(0.8f);
c->SetNormalMap(noise_t);
AddGameObject(c);
namedObjects_[id] = c;
bodyCmd(id, "box", 1.5f, 0.2f, 0.5f, false);
// Arm
if (i % 2 == 0) {
auto arm = CreateGameObject(
CreateCylinder(0.2f, 1.2f, 8, glm::vec3(0.32f, 0.56f, 0.33f)),
glm::vec3(x + 0.45f, h * 0.6f, z));
arm->SetRotation(glm::vec3(0, 0, 60.0f));
arm->SetRoughness(0.8f);
AddGameObject(arm);
}
}
// Sand boulders (physics)
for (int i = 0; i < 16; ++i) {
float x = rnd(-78.0f, -6.0f);
float z = rnd(-78.0f, -6.0f);
float s = rnd(0.5f, 1.3f);
addSphere("drock_" + std::to_string(i),
glm::vec3(x, s, z), s,
glm::vec3(0.88f, 0.72f, 0.50f),
rock_tex, noise_t,
0.0f, 0.85f,
1.2f, 0.25f, 0.45f);
}
// Step pyramid (larger, static) — signature landmark
for (int lvl = 0; lvl < 6; ++lvl) {
float s = 10.0f - lvl * 1.4f;
addBox("pyr_" + std::to_string(lvl),
glm::vec3(-55.0f, 0.9f + lvl * 1.8f, -55.0f),
glm::vec3(s, 1.8f, s),
glm::vec3(0.92f, 0.76f, 0.55f),
dirt_t, noise_t, 0.0f, 0.9f,
-1.0f);
}
// Obelisk near pyramid
addBox("desert_obelisk",
glm::vec3(-45.0f, 5.0f, -58.0f),
glm::vec3(1.5f, 10.0f, 1.5f),
glm::vec3(0.95f, 0.82f, 0.65f),
marble_t, noise_t, 0.0f, 0.5f, -1.0f,
0.0f, 0.0f, glm::vec3(0, 18.0f, 0));
// Dune mounds (static)
for (int i = 0; i < 14; ++i) {
float x = rnd(-74.0f, -8.0f);
float z = rnd(-74.0f, -8.0f);
auto dune = CreateGameObject(
CreateSphere(20, glm::vec3(1.0f, 0.85f, 0.55f)),
glm::vec3(x, 0.0f, z));
dune->SetScale(glm::vec3(4.0f, 1.0f, 3.5f));
dune->SetTexture(dirt_t); dune->SetNormalMap(noise_t);
dune->SetRoughness(0.95f);
AddGameObject(dune);
}
// ============================ TECH (SE) =======================
// Chrome pillars + emissive cyan bars + crate stack
for (int i = 0; i < 14; ++i) {
float ang = (float)i / 14.0f * 2 * M_PI;
float R = 25.0f;
float x = 40.0f + std::cos(ang) * R;
float z = -40.0f + std::sin(ang) * R;
std::string id = "pylon_" + std::to_string(i);
addBox(id,
glm::vec3(x, 3.0f, z), glm::vec3(0.7f, 6.0f, 0.7f),
glm::vec3(0.92f),
0, 0, 1.0f, 0.15f,
-1.0f,
0.0f, 0.0f, glm::vec3(0, jit(15.0f), 0));
}
// Emissive cyan bars laid horizontally between pylons (decorative)
for (int i = 0; i < 24; ++i) {
float x = rnd( 8.0f, 76.0f);
float z = rnd(-76.0f, -8.0f);
auto bar = CreateGameObject(
CreateCube(glm::vec3(1.0f)),
glm::vec3(x, 0.15f, z));
bar->SetScale(glm::vec3(rnd(1.2f, 2.4f), 0.05f, 0.2f));
bar->SetRotation(glm::vec3(0, jit(180.0f), 0));
bar->SetEmissive(glm::vec3(0.1f, 2.5f, 3.0f));
AddGameObject(bar);
}
// Tech crate pyramid (dynamic)
for (int y = 0; y < 4; ++y) {
int n = 4 - y;
for (int i = 0; i < n; ++i) {
float x = 18.0f + i * 1.1f + (3 - n) * 0.55f;
float yp = 0.55f + y * 1.1f;
addBox("tcrate_" + std::to_string(y * 4 + i),
glm::vec3(x, yp, -18.0f), glm::vec3(1.0f),
glm::vec3(0.85f, 0.88f, 0.95f),
checker_t, noise_t, 0.85f, 0.25f,
0.9f, 0.3f, 0.45f);
}
}
// Chrome spheres scattered (dynamic)
for (int i = 0; i < 6; ++i) {
float x = rnd( 8.0f, 76.0f);
float z = rnd(-76.0f, -8.0f);
std::string col_id = "tsph_" + std::to_string(i);
addSphere(col_id,
glm::vec3(x, 0.7f, z), 0.6f,
glm::vec3(0.95f, 0.95f, 0.95f),
0, 0, 1.0f, 0.25f,
1.3f, 0.55f, 0.2f);
}
// Glowing monolith — signature tech landmark
addBox("tech_obelisk",
glm::vec3(65.0f, 6.0f, -65.0f), glm::vec3(1.6f, 12.0f, 1.6f),
glm::vec3(0.92f, 0.94f, 1.0f),
0, 0, 1.0f, 0.1f, -1.0f,
0.0f, 0.0f, glm::vec3(0, 30.0f, 0),
glm::vec3(0.5f, 1.8f, 3.0f));
// Secondary glowing pillar
addBox("tech_spire",
glm::vec3(25.0f, 8.0f, -60.0f), glm::vec3(0.8f, 16.0f, 0.8f),
glm::vec3(0.95f, 0.96f, 1.0f),
0, 0, 1.0f, 0.08f, -1.0f,
0.0f, 0.0f, glm::vec3(0),
glm::vec3(1.5f, 0.4f, 3.5f));
// ============================ CENTER: PHYSICS POOL ============
// Raised basin (6.0 unit radius, wall height ~0.9) built from
// a ring of static stone blocks. Water mesh is already set via
// the "water" command with bounds_radius=5.0 so buoyancy only
// kicks in inside the ring.
for (int i = 0; i < 24; ++i) {
float ang = (float)i / 24.0f * 2 * M_PI;
float R = 5.8f;
float x = std::cos(ang) * R;
float z = std::sin(ang) * R;
std::string id = "poolw_" + std::to_string(i);
addBox(id,
glm::vec3(x, 0.5f, z), glm::vec3(0.9f, 1.0f, 0.9f),
glm::vec3(0.82f, 0.78f, 0.74f),
marble_t, noise_t, 0.0f, 0.55f, -1.0f, // static
0.0f, 0.0f, glm::vec3(jit(5), ang * 57.3f + jit(10), jit(5)));
}
// Stepping-stone paths from pool toward each quadrant
for (int q = 0; q < 4; ++q) {
float dx = (q & 1) ? 1.0f : -1.0f;
float dz = (q & 2) ? -1.0f : 1.0f;
for (int s = 0; s < 6; ++s) {
float x = dx * (8.0f + s * 3.5f);
float z = dz * (8.0f + s * 3.5f);
auto stone = CreateGameObject(
CreateCube(glm::vec3(0.82f, 0.78f, 0.74f)),
glm::vec3(x, 0.05f, z));
stone->SetScale(glm::vec3(1.6f, 0.1f, 1.6f));
stone->SetTexture(marble_t); stone->SetNormalMap(noise_t);
stone->SetRoughness(0.55f);
AddGameObject(stone);
}
}
// --- Level border: dense mountain ring just outside the 160x160 map.
// 56 cones at R=90 with overlapping footprints form an impassable wall.
for (int i = 0; i < 56; ++i) {
float ang = (float)i / 56.0f * 2 * M_PI + jit(0.03f);
float R = 90.0f + jit(4.0f);
float x = std::cos(ang) * R;
float z = std::sin(ang) * R;
float sc = rnd(1.0f, 1.5f);
float h = 34.0f * sc;
auto m = CreateGameObject(
CreateCone(14.0f * sc, h, 10, glm::vec3(0.22f, 0.20f, 0.25f)),
glm::vec3(x, h * 0.5f, z));
m->SetRoughness(0.92f);
AddGameObject(m);
}
// Second, farther mountain ring for depth (behind the border)
for (int i = 0; i < 24; ++i) {
float ang = (float)i / 24.0f * 2 * M_PI + jit(0.06f);
float R = 140.0f + jit(15.0f);
float x = std::cos(ang) * R;
float z = std::sin(ang) * R;
float sc = rnd(1.2f, 1.8f);
float h = 40.0f * sc;
auto m = CreateGameObject(
CreateCone(18.0f * sc, h, 10, glm::vec3(0.18f, 0.17f, 0.22f)),
glm::vec3(x, h * 0.5f, z));
AddGameObject(m);
}
processAgentCommand(json::parse(R"({"action":"physics","enabled":true})"));
// First-person arm: 3 GameObjects, repositioned from camera basis each frame
BuildArm();
// Spawn player on forest-side walkway, feet on ground, looking SE toward pool.
float spawn_x = -8.0f, spawn_z = 14.0f;
float foot_y = SampleTerrainH(spawn_x, spawn_z);
glm::vec3 eye(spawn_x, foot_y + kEyeH, spawn_z);
glm::vec3 target = eye + glm::vec3(1.0f, -0.2f, -1.0f);
SetCameraPosition(eye);
SetCameraTarget(target);
glm::vec3 fwd = glm::normalize(target - eye);
cam_pitch_ = glm::degrees(std::asin(fwd.y));
cam_yaw_ = glm::degrees(std::atan2(fwd.z, fwd.x));
last_cam_pos_ = eye;
grounded_ = true;
std::cout << "Four-Map ready: " << game_objects_.size() << " objects.\n"
<< " NW Forest | NE Sakura (hills) | SW Desert (dunes) | SE Tech | Center: pool\n"
<< " WASD walk, mouse/arrows look, Space jump, LShift sprint. F2 toggles fly.\n"
<< " F / LMB / R1 to throw balls. Mountain ring blocks escape.\n";
return true;
}
void BuildArm() {
arm_upper_ = CreateGameObject(
CreateCylinder(0.10f, 0.45f, 10, glm::vec3(0.92f, 0.85f, 0.78f)),
glm::vec3(0));
arm_upper_->SetRoughness(0.75f);
AddGameObject(arm_upper_);
arm_lower_ = CreateGameObject(
CreateCylinder(0.085f, 0.45f, 10, glm::vec3(0.92f, 0.85f, 0.78f)),
glm::vec3(0));
arm_lower_->SetRoughness(0.75f);
AddGameObject(arm_lower_);
hand_ = CreateGameObject(
CreateSphere(18, glm::vec3(0.92f, 0.85f, 0.78f)),
glm::vec3(0));
hand_->SetScale(glm::vec3(0.11f));
hand_->SetRoughness(0.7f);
AddGameObject(hand_);
arm_ready_ = true;
}
void UpdateArm(float dt) {
if (!arm_ready_) return;
throw_anim_ = std::max(0.0f, throw_anim_ - dt * 4.0f);
glm::vec3 cp = GetCameraPosition();
glm::vec3 tg = GetCameraTarget();
glm::vec3 fwd = glm::normalize(tg - cp);
glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
glm::vec3 up = glm::cross(right, fwd);
float t = throw_anim_;
float forward_boost = t * 0.45f;
glm::vec3 shoulder = cp + right * 0.55f + up * -0.40f + fwd * 0.35f;
glm::vec3 elbow = shoulder + fwd * (0.18f + forward_boost * 0.4f)
+ up * (-0.20f + t * 0.1f);
glm::vec3 wrist = elbow + fwd * (0.35f + forward_boost)
+ up * (-0.05f + t * 0.3f);
auto midpoint = [](const glm::vec3& a, const glm::vec3& b) { return (a + b) * 0.5f; };
auto yawPitch = [&](const glm::vec3& dir) {
glm::vec3 d = glm::normalize(dir);
float pitch = glm::degrees(std::asin(d.y));
float yaw = glm::degrees(std::atan2(d.x, d.z));
return glm::vec3(-pitch - 90.0f, yaw, 0.0f);
};
glm::vec3 upperDir = elbow - shoulder;
glm::vec3 lowerDir = wrist - elbow;
arm_upper_->SetPosition(midpoint(shoulder, elbow));
arm_upper_->SetRotation(yawPitch(upperDir));
arm_upper_->SetScale(glm::vec3(1.0f, glm::length(upperDir) / 0.45f, 1.0f));
arm_lower_->SetPosition(midpoint(elbow, wrist));
arm_lower_->SetRotation(yawPitch(lowerDir));
arm_lower_->SetScale(glm::vec3(1.0f, glm::length(lowerDir) / 0.45f, 1.0f));
hand_->SetPosition(wrist);
}
// Per-zone atmosphere recipes — blended each frame based on camera XZ.
struct Zone {
glm::vec3 sun, zenith, horizon, sky, ground;
float fog, bloom_i, bloom_t, expo;
};
void UpdateAtmosphere() {
glm::vec3 cp = GetCameraPosition();
float tx = glm::clamp((cp.x + 40.0f) / 80.0f, 0.0f, 1.0f);
float tz = glm::clamp((cp.z + 40.0f) / 80.0f, 0.0f, 1.0f);
float wF = (1.0f - tx) * tz; // NW forest
float wS = tx * tz; // NE sakura
float wD = (1.0f - tx) * (1.0f - tz); // SW desert
float wT = tx * (1.0f - tz); // SE tech
Zone Zf{ glm::vec3(2.0f, 1.85f, 1.55f), glm::vec3(0.20f, 0.40f, 0.55f),
glm::vec3(0.72f, 0.82f, 0.74f), glm::vec3(0.55f, 0.72f, 0.62f),
glm::vec3(0.08f, 0.12f, 0.08f), 0.004f, 1.0f, 1.25f, 0.95f };
Zone Zs{ glm::vec3(2.20f, 1.75f, 1.85f), glm::vec3(0.36f, 0.45f, 0.66f),
glm::vec3(1.15f, 0.74f, 0.86f), glm::vec3(0.92f, 0.70f, 0.80f),
glm::vec3(0.10f, 0.06f, 0.10f), 0.003f, 1.6f, 1.05f, 1.00f };
Zone Zd{ glm::vec3(2.45f, 1.75f, 1.25f), glm::vec3(0.25f, 0.34f, 0.55f),
glm::vec3(1.05f, 0.74f, 0.48f), glm::vec3(0.92f, 0.70f, 0.48f),
glm::vec3(0.14f, 0.10f, 0.06f), 0.006f, 0.75f, 1.30f, 0.95f };
Zone Zt{ glm::vec3(1.60f, 1.95f, 2.25f), glm::vec3(0.04f, 0.10f, 0.25f),
glm::vec3(0.20f, 0.40f, 0.65f), glm::vec3(0.22f, 0.38f, 0.58f),
glm::vec3(0.04f, 0.06f, 0.10f), 0.010f, 2.00f, 0.95f, 0.92f };
auto blend = [&](auto Zf_v, auto Zs_v, auto Zd_v, auto Zt_v) {
return wF * Zf_v + wS * Zs_v + wD * Zd_v + wT * Zt_v;
};
sun_color_ = blend(Zf.sun, Zs.sun, Zd.sun, Zt.sun);
zenith_color_ = blend(Zf.zenith, Zs.zenith, Zd.zenith, Zt.zenith);
horizon_color_ = blend(Zf.horizon, Zs.horizon, Zd.horizon, Zt.horizon);
sky_color_ = blend(Zf.sky, Zs.sky, Zd.sky, Zt.sky);
ground_color_ = blend(Zf.ground, Zs.ground, Zd.ground, Zt.ground);
fog_density_ = blend(Zf.fog, Zs.fog, Zd.fog, Zt.fog);
bloom_intensity_= blend(Zf.bloom_i, Zs.bloom_i, Zd.bloom_i, Zt.bloom_i);
bloom_threshold_= blend(Zf.bloom_t, Zs.bloom_t, Zd.bloom_t, Zt.bloom_t);
exposure_ = blend(Zf.expo, Zs.expo, Zd.expo, Zt.expo);
}
void FireBall() {
glm::vec3 cp = GetCameraPosition();
glm::vec3 tg = GetCameraTarget();
glm::vec3 fwd = glm::normalize(tg - cp);
if ((int)active_shots_.size() >= kMaxShots) {
auto old = active_shots_.front(); active_shots_.pop_front();
processAgentCommand(json::parse("{\"action\":\"delete\",\"id\":\"" + old + "\"}"));
}
std::uniform_real_distribution<float> cd(0.7f, 1.7f);
glm::vec3 col(cd(rng_), cd(rng_), cd(rng_));
std::string id = "shot_" + std::to_string(shot_counter_++);
auto b = CreateGameObject(CreateSphere(20, col), cp + fwd * 1.4f);
b->SetScale(glm::vec3(0.32f));
b->SetMetallic(1.0f);
b->SetRoughness(0.25f);
AddGameObject(b);
namedObjects_[id] = b;
json::Object bc;
bc["action"]=json::Value(std::string("body"));
bc["id"]=json::Value(id);
bc["shape"]=json::Value(std::string("sphere"));
bc["mass"]=json::Value(1.2);
bc["restitution"]=json::Value(0.6);
bc["friction"]=json::Value(0.3);
processAgentCommand(json::Value(bc));
json::Object vc;
vc["action"]=json::Value(std::string("velocity"));
vc["id"]=json::Value(id);
vc["linear"]=json::Value(json::Array{
json::Value((double)(fwd.x * 55)),
json::Value((double)(fwd.y * 55)),
json::Value((double)(fwd.z * 55))});
processAgentCommand(json::Value(vc));
active_shots_.push_back(id);
}
void OnUpdate(float dt) override {
UpdateAtmosphere(); // blend zone palettes each frame
shot_cooldown_ -= dt;
if (shot_cooldown_ <= 0.0f) {
bool fire = Input::IsKeyHeld(GLFW_KEY_F)
|| Input::IsMouseButtonPressed(Input::MouseButton::Left)
|| Input::IsGamepadButtonDown(Input::GamepadButton::R1);
if (fire) { FireBall(); shot_cooldown_ = 0.15f; }
}
// --- Input gathering -------------------------------------------------
glm::vec3 eng_pos = GetCameraPosition();
glm::vec2 ls{0}, rs{0};
float up_in = 0, down_in = 0;
bool sprint = false, jump_now = false, fly_toggle = false;
if (Input::IsGamepadConnected()) {
ls = Input::GetLeftStick();
rs = Input::GetRightStick();
up_in = Input::GetR2();
down_in = Input::GetL2();
if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross)) sprint = true;
if (Input::IsGamepadButtonDown(Input::GamepadButton::Circle)) jump_now = true;
}
if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT)) sprint = true;
if (Input::IsKeyHeld(GLFW_KEY_W)) ls.y -= 1.0f;
if (Input::IsKeyHeld(GLFW_KEY_S)) ls.y += 1.0f;
if (Input::IsKeyHeld(GLFW_KEY_A)) ls.x -= 1.0f;
if (Input::IsKeyHeld(GLFW_KEY_D)) ls.x += 1.0f;
if (Input::IsKeyHeld(GLFW_KEY_LEFT)) rs.x -= 1.0f;
if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) rs.x += 1.0f;
if (Input::IsKeyHeld(GLFW_KEY_UP)) rs.y -= 1.0f;
if (Input::IsKeyHeld(GLFW_KEY_DOWN)) rs.y += 1.0f;
if (Input::IsKeyHeld(GLFW_KEY_SPACE)) jump_now = true;
if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL)) down_in += 1.0f;
bool fly_pressed_now = Input::IsKeyHeld(GLFW_KEY_F2);
if (fly_pressed_now && !fly_prev_) { fly_mode_ = !fly_mode_; vy_ = 0.0f; }
fly_prev_ = fly_pressed_now;
// --- Look update -----------------------------------------------------
cam_yaw_ += rs.x * look_sensitivity_ * dt;
cam_pitch_ -= rs.y * look_sensitivity_ * dt;
if (cam_pitch_ > 89.0f) cam_pitch_ = 89.0f;
if (cam_pitch_ < -89.0f) cam_pitch_ = -89.0f;
glm::vec3 fwd;
fwd.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
fwd.y = std::sin(glm::radians(cam_pitch_));
fwd.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
fwd = glm::normalize(fwd);
glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
glm::vec3 fwd_h = glm::normalize(glm::vec3(fwd.x, 0.0f, fwd.z));
glm::vec3 right_h = glm::normalize(glm::vec3(right.x, 0.0f, right.z));
// --- Movement --------------------------------------------------------
glm::vec3 pos = eng_pos;
if (fly_mode_) {
// Free-fly for debug / fast traversal
float speed = 22.0f * (sprint ? 3.0f : 1.0f);
pos += fwd * (-ls.y) * speed * dt;
pos += right * ls.x * speed * dt;
pos.y += (up_in - down_in) * speed * dt;
} else {
// Grounded walking
float speed = kWalkSpeed * (sprint ? kRunMult : 1.0f);
glm::vec3 horiz = fwd_h * (-ls.y) * speed * dt
+ right_h * ls.x * speed * dt;
// Slope test: project candidate x/z, sample ground height
glm::vec3 cand = pos + horiz;
cand.x = glm::clamp(cand.x, -kMapHalf, kMapHalf);
cand.z = glm::clamp(cand.z, -kMapHalf, kMapHalf);
float foot_now = pos.y - kEyeH;
float foot_cand = SampleTerrainH(cand.x, cand.z);
if (grounded_ && (foot_cand - foot_now) > kStepUp) {
// Too steep — block horizontal motion this frame
cand.x = pos.x;
cand.z = pos.z;
foot_cand = SampleTerrainH(cand.x, cand.z);
}
// Vertical: jump + gravity
if (jump_now && !jump_prev_ && grounded_) {
vy_ = kJumpV;
grounded_ = false;
}
if (!grounded_) {
vy_ -= kGravity * dt;
cand.y = pos.y + vy_ * dt;
if (cand.y <= foot_cand + kEyeH) {
cand.y = foot_cand + kEyeH;
vy_ = 0.0f;
grounded_ = true;
}
} else {
// Stick to ground (walk up/down gentle slopes smoothly)
cand.y = foot_cand + kEyeH;
vy_ = 0.0f;
// Detect walking off a ledge
if (foot_cand < foot_now - 0.25f) grounded_ = false;
}
pos = cand;
}
jump_prev_ = jump_now;
SetCameraPosition(pos);
SetCameraTarget(pos + fwd);
last_cam_pos_ = pos;
// First-person arm
UpdateArm(dt);
}
};
// ---------------------------------------------
// PhysicsPlaygroundDemo — expansive showcase scene.
//
// Zones:
// CENTER — chrome monolith, 10 pulsing emissive bollards, Jenga tower,
// loose props (HDR flame in the middle)
// NORTH — 7×7 PBR material grid (metallic × roughness) on pedestals
// EAST — ramp + 40-domino cascade + 10 bowling pins. Chain fires at t=2s.
// WEST — catapult frame + target crate stack. Periodic launch every 18s.
// SOUTH — cliff, waterfall, lake with flow current + floating barrels
//
// Interactive:
// F / LMB / R1 → shoot metallic HDR ball from camera
// T → cycle storm state (calm → squall → gale)
// G → chaos: apply random upward impulse to every dynamic body
//
// Time-driven:
// t=2s → domino kickoff ball fires
// every 18s → catapult hurls a heavy ball at the arena
// every frame → waterfall droplet stream; bollards pulse via SetEmissive
// ---------------------------------------------
class PhysicsPlaygroundDemo : public Engine {
float cam_yaw_ = 200.0f;
float cam_pitch_ = -8.0f;
float cam_speed_ = 22.0f;
float look_sensitivity_ = 80.0f;
glm::vec3 last_cam_pos_{0};
float time_ = 0.0f;
float shot_cooldown_ = 0.0f;
float drop_timer_ = 0.0f;
float catapult_timer_ = 12.0f; // first fire at ~12s
bool kickoff_fired_ = false;
int storm_state_ = 0;
float t_down_prev_ = false;
float g_down_prev_ = false;
std::mt19937 rng_{2024};
std::mt19937 drop_rng_{2025};
int shot_counter_ = 0;
int drop_counter_ = 0;
int cata_counter_ = 0;
std::deque<std::string> active_shots_;
std::deque<std::string> active_drops_;
std::deque<std::string> active_cata_;
std::vector<std::string> bollard_ids_;
static constexpr int kMaxShots = 80;
static constexpr int kMaxDrops = 120;
static constexpr int kMaxCata = 5;
public:
PhysicsPlaygroundDemo()
: Engine(1920, 1080, "ZoomEngine - Physics Playground") {}
// --- Placement helpers --------------------------------------------------
void bodyCmd(const std::string& id, const std::string& shape,
float mass, float rest, float frict, bool is_static = false) {
json::Object c;
c["action"] = json::Value(std::string("body"));
c["id"] = json::Value(id);
c["shape"] = json::Value(shape);
c["mass"] = json::Value((double)mass);
c["restitution"] = json::Value((double)rest);
c["friction"] = json::Value((double)frict);
if (is_static) c["static"] = json::Value(true);
processAgentCommand(json::Value(c));
}
std::shared_ptr<GameObject> addBox(const std::string& id, glm::vec3 pos, glm::vec3 scale,
glm::vec3 color, GLuint tex, GLuint nmap,
float metallic, float roughness, float mass,
float rest = 0.2f, float frict = 0.6f,
glm::vec3 rot = glm::vec3(0)) {
auto obj = CreateGameObject(CreateCube(color), pos);
obj->SetScale(scale);
obj->SetRotation(rot);
if (tex) obj->SetTexture(tex);
if (nmap) obj->SetNormalMap(nmap);
obj->SetMetallic(metallic);
obj->SetRoughness(roughness);
AddGameObject(obj);
if (!id.empty()) namedObjects_[id] = obj;
if (mass > 0) bodyCmd(id, "box", mass, rest, frict, false);
else if (mass == -1.0f) bodyCmd(id, "box", 0, rest, frict, true);
return obj;
}
std::shared_ptr<GameObject> addSphere(const std::string& id, glm::vec3 pos, float r,
glm::vec3 color, GLuint tex, GLuint nmap,
float metallic, float roughness, float mass,
float rest = 0.4f, float frict = 0.3f) {
auto obj = CreateGameObject(CreateSphere(32, color), pos);
obj->SetScale(glm::vec3(r));
if (tex) obj->SetTexture(tex);
if (nmap) obj->SetNormalMap(nmap);
obj->SetMetallic(metallic);
obj->SetRoughness(roughness);
AddGameObject(obj);
if (!id.empty()) namedObjects_[id] = obj;
if (mass > 0) bodyCmd(id, "sphere", mass, rest, frict, false);
return obj;
}
// --- Init ---------------------------------------------------------------
bool OnInitialize() override {
auto rnd = [&](float lo, float hi) {
std::uniform_real_distribution<float> d(lo, hi); return d(rng_);
};
auto jit = [&](float s) { return rnd(-s, s); };
// --- Sky + HDR tuning (clear-day base; storm toggle can darken later) ---
processAgentCommand(json::parse(R"({
"action":"sun",
"direction":[-0.45, 0.55, -0.30],
"color":[2.0, 1.55, 1.15],
"zenith":[0.22, 0.40, 0.70],
"horizon":[0.85, 0.75, 0.62],
"sky":[0.60, 0.68, 0.78],
"ground":[0.08, 0.09, 0.10],
"shadow_extent":110,
"exposure":0.95,
"bloom_threshold":1.15,
"bloom_intensity":1.1,
"fog_density":0.007
})"));
// Primary water — the lake in the south zone; flows west so floats drift
processAgentCommand(json::parse(R"({
"action":"water","enabled":true,
"level":-0.25,"size":200,
"amplitude":0.14,"wavelength":10,"speed":0.8,
"shallow":[0.42, 0.58, 0.58],"deep":[0.03, 0.10, 0.18],
"density":1.0,"resolution":160,
"flow_dir":[-1, 0, 0],"flow_speed":1.5
})"));
processAgentCommand(json::parse(R"({
"action":"weather","wind_dir":[-1,0,0.2],
"wind_speed":0.5,"storminess":0.0,"current_coef":0.4
})"));
GLuint grass_tex = TextureCache::Get("grass");
GLuint rock_tex = TextureCache::Get("rock");
GLuint wood_tex = TextureCache::Get("wood");
GLuint marble_t = TextureCache::Get("marble");
GLuint concrete = TextureCache::Get("concrete");
GLuint brick_t = TextureCache::Get("brick");
GLuint noise_t = TextureCache::Get("noise");
GLuint dirt_t = TextureCache::Get("dirt");
// --- Ground (two strips, leaving the lake channel open to south) ---
auto north_grass = CreateGameObject(
CreatePlane(220.0f, 120.0f, glm::vec3(0.88f, 0.92f, 0.80f)),
glm::vec3(0, 0, 45.0f));
north_grass->SetTexture(grass_tex); north_grass->SetNormalMap(noise_t);
north_grass->SetUVScale(glm::vec2(0.22f)); north_grass->SetRoughness(0.95f);
AddGameObject(north_grass);
auto center_stone = CreateGameObject(
CreatePlane(80.0f, 40.0f, glm::vec3(0.78f, 0.78f, 0.76f)),
glm::vec3(0, 0.01f, 0));
center_stone->SetTexture(marble_t); center_stone->SetNormalMap(noise_t);
center_stone->SetUVScale(glm::vec2(0.2f)); center_stone->SetRoughness(0.45f);
AddGameObject(center_stone);
auto west_dirt = CreateGameObject(
CreatePlane(80.0f, 40.0f, glm::vec3(0.68f, 0.62f, 0.50f)),
glm::vec3(-50.0f, 0, 0));
west_dirt->SetTexture(dirt_t); west_dirt->SetNormalMap(noise_t);
west_dirt->SetUVScale(glm::vec2(0.25f)); west_dirt->SetRoughness(0.95f);
AddGameObject(west_dirt);
auto east_stone = CreateGameObject(
CreatePlane(80.0f, 40.0f, glm::vec3(0.72f, 0.70f, 0.66f)),
glm::vec3(50.0f, 0, 0));
east_stone->SetTexture(concrete); east_stone->SetNormalMap(noise_t);
east_stone->SetUVScale(glm::vec2(0.22f)); east_stone->SetRoughness(0.75f);
AddGameObject(east_stone);
// -------------------- CENTRAL ARENA ------------------------------
// Chrome monolith centerpiece
addBox("monolith",
glm::vec3(0, 6.0f, 0), glm::vec3(1.4f, 12.0f, 1.4f),
glm::vec3(0.96f), 0, 0, 1.0f, 0.12f, -1.0f); // static
// 10 pulsing emissive bollards in a ring around the monolith
for (int i = 0; i < 10; ++i) {
float ang = (float)i / 10.0f * 2 * M_PI;
float R = 6.0f;
std::string id = "boll_" + std::to_string(i);
auto boll = CreateGameObject(
CreateCylinder(0.25f, 1.6f, 10, glm::vec3(0.9f, 0.9f, 0.9f)),
glm::vec3(std::cos(ang) * R, 0.8f, std::sin(ang) * R));
boll->SetMetallic(0.9f);
boll->SetRoughness(0.3f);
boll->SetEmissive(glm::vec3(1.5f, 0.6f, 0.2f)); // initial; pulses
AddGameObject(boll);
namedObjects_[id] = boll;
bollard_ids_.push_back(id);
}
// Jenga tower — 5 levels × 3 boxes, alternating orientation
for (int lvl = 0; lvl < 5; ++lvl) {
for (int i = 0; i < 3; ++i) {
bool along_x = (lvl % 2 == 0);
glm::vec3 pos, scale;
if (along_x) {
pos = glm::vec3(8.0f + (i - 1) * 0.7f, 0.35f + lvl * 0.7f, 6.0f);
scale = glm::vec3(0.6f, 0.6f, 2.0f);
} else {
pos = glm::vec3(8.0f, 0.35f + lvl * 0.7f, 6.0f + (i - 1) * 0.7f);
scale = glm::vec3(2.0f, 0.6f, 0.6f);
}
addBox("jen_" + std::to_string(lvl * 3 + i),
pos, scale, glm::vec3(0.92f, 0.86f, 0.72f),
wood_tex, noise_t, 0.0f, 0.65f,
0.7f, 0.08f, 0.55f);
}
}
// HDR pyre in the monolith's shadow — signature bloom source
{
auto logA = CreateGameObject(
CreateCylinder(0.18f, 1.3f, 8, glm::vec3(0.25f, 0.16f, 0.08f)),
glm::vec3(-6.0f, 0.2f, 0.0f));
logA->SetRotation(glm::vec3(0, 30.0f, 90.0f));
logA->SetTexture(wood_tex); logA->SetNormalMap(noise_t);
AddGameObject(logA);
auto logB = CreateGameObject(
CreateCylinder(0.18f, 1.3f, 8, glm::vec3(0.27f, 0.18f, 0.1f)),
glm::vec3(-6.0f, 0.25f, 0.0f));
logB->SetRotation(glm::vec3(0, -30.0f, 90.0f));
logB->SetTexture(wood_tex); logB->SetNormalMap(noise_t);
AddGameObject(logB);
auto flame1 = CreateGameObject(
CreateCone(0.45f, 1.4f, 10, glm::vec3(1.0f)),
glm::vec3(-6.0f, 0.9f, 0.0f));
flame1->SetEmissive(glm::vec3(5.5f, 2.0f, 0.4f));
AddGameObject(flame1);
auto flame2 = CreateGameObject(
CreateCone(0.22f, 0.8f, 10, glm::vec3(1.0f)),
glm::vec3(-6.0f, 1.05f, 0.0f));
flame2->SetEmissive(glm::vec3(9.0f, 4.0f, 1.5f));
AddGameObject(flame2);
}
// -------------------- NORTH: PBR MATERIAL GRID ------------------
const int N = 7;
glm::vec3 material_base(0.88f, 0.32f, 0.18f); // red-orange showpiece
for (int row = 0; row < N; ++row) {
for (int col = 0; col < N; ++col) {
float m = (float)row / (N - 1);
float r = (float)col / (N - 1) * 0.92f + 0.05f;
float xx = -9.0f + col * 3.0f;
float zz = 25.0f + row * 3.5f;
// Pedestal
auto ped = CreateGameObject(
CreateCube(glm::vec3(0.80f, 0.77f, 0.72f)),
glm::vec3(xx, 0.6f, zz));
ped->SetScale(glm::vec3(1.3f, 1.2f, 1.3f));
ped->SetTexture(marble_t); ped->SetNormalMap(noise_t);
ped->SetRoughness(0.45f);
AddGameObject(ped);
// Sphere on top
auto sph = CreateGameObject(
CreateSphere(48, material_base),
glm::vec3(xx, 1.9f, zz));
sph->SetScale(glm::vec3(0.95f));
sph->SetMetallic(m);
sph->SetRoughness(r);
AddGameObject(sph);
}
}
// Backdrop wall behind the gallery
for (int i = 0; i < 10; ++i) {
float xx = -13.5f + i * 3.0f;
auto b = CreateGameObject(
CreateCube(glm::vec3(0.82f, 0.78f, 0.72f)),
glm::vec3(xx, 3.5f, 50.0f));
b->SetScale(glm::vec3(3.0f, 7.0f, 1.2f));
b->SetTexture(brick_t); b->SetNormalMap(noise_t);
b->SetRoughness(0.82f);
AddGameObject(b);
}
// -------------------- EAST: DOMINOES + BOWLING ------------------
// Ramp for kickoff ball at far east
addBox("ramp",
glm::vec3(45.0f, 2.0f, 0.0f), glm::vec3(7.0f, 0.3f, 3.0f),
glm::vec3(0.85f, 0.78f, 0.65f),
wood_tex, noise_t, 0.0f, 0.7f, -1.0f,
0.0f, 0.0f, glm::vec3(0, 0, -20.0f));
// 40-domino straight line west from x=38 to x=6
for (int i = 0; i < 40; ++i) {
float x = 38.0f - i * 0.82f;
addBox("dom_" + std::to_string(i),
glm::vec3(x, 0.9f, 0.0f), glm::vec3(0.2f, 1.8f, 0.95f),
glm::vec3(0.92f, 0.88f, 0.75f),
wood_tex, noise_t, 0.0f, 0.6f,
0.8f, 0.07f, 0.55f);
}
// Bowling pins at west end of domino line
static const float pin_off[10][2] = {
{ 0.0f, 0.0f},
{-0.45f, 0.9f}, { 0.45f, 0.9f},
{-0.9f, 1.8f}, { 0.0f, 1.8f}, { 0.9f, 1.8f},
{-1.35f, 2.7f}, {-0.45f, 2.7f}, { 0.45f, 2.7f}, { 1.35f, 2.7f}
};
for (int i = 0; i < 10; ++i) {
glm::vec3 p(4.0f - pin_off[i][1], 0.8f, pin_off[i][0]);
auto pin = CreateGameObject(
CreateCylinder(0.32f, 1.5f, 14,
glm::vec3(0.97f, 0.96f, 0.92f)),
p);
pin->SetMetallic(0.0f);
pin->SetRoughness(0.3f);
AddGameObject(pin);
std::string id = "pin_" + std::to_string(i);
namedObjects_[id] = pin;
bodyCmd(id, "box", 0.4f, 0.2f, 0.4f, false);
}
// Impulse ball on top of the ramp — fires at t=2
addSphere("kickoff",
glm::vec3(48.0f, 5.5f, 0.0f), 0.85f,
glm::vec3(0.75f, 0.78f, 0.82f), 0, 0,
1.0f, 0.28f,
9.0f, 0.3f, 0.25f);
// -------------------- WEST: CATAPULT FRAME -----------------------
// Tall wooden A-frame made of stacked boxes (purely decorative static)
auto postL = CreateGameObject(
CreateCube(glm::vec3(0.58f, 0.40f, 0.24f)),
glm::vec3(-45.0f, 4.0f, 3.0f));
postL->SetScale(glm::vec3(0.6f, 8.0f, 0.6f));
postL->SetRotation(glm::vec3(0, 0, 12.0f));
postL->SetTexture(wood_tex); postL->SetNormalMap(noise_t);
postL->SetRoughness(0.8f);
AddGameObject(postL);
auto postR = CreateGameObject(
CreateCube(glm::vec3(0.58f, 0.40f, 0.24f)),
glm::vec3(-45.0f, 4.0f, -3.0f));
postR->SetScale(glm::vec3(0.6f, 8.0f, 0.6f));
postR->SetRotation(glm::vec3(0, 0, -12.0f));
postR->SetTexture(wood_tex); postR->SetNormalMap(noise_t);
postR->SetRoughness(0.8f);
AddGameObject(postR);
auto crossbeam = CreateGameObject(
CreateCube(glm::vec3(0.58f, 0.40f, 0.24f)),
glm::vec3(-45.0f, 8.3f, 0.0f));
crossbeam->SetScale(glm::vec3(0.8f, 0.8f, 8.0f));
crossbeam->SetTexture(wood_tex); crossbeam->SetNormalMap(noise_t);
crossbeam->SetRoughness(0.8f);
AddGameObject(crossbeam);
// Target crate pyramid in the center — catapult aims here
for (int y = 0; y < 3; ++y) {
int n = 4 - y;
for (int i = 0; i < n; ++i) {
float xx = -30.0f + i * 1.05f + (3 - n) * 0.5f;
float yp = 0.55f + y * 1.05f;
addBox("ccrate_" + std::to_string(y * 4 + i),
glm::vec3(xx, yp, 0.0f), glm::vec3(1.0f),
glm::vec3(0.80f, 0.70f, 0.48f),
wood_tex, noise_t, 0.0f, 0.7f,
0.7f, 0.12f, 0.65f);
}
}
// -------------------- SOUTH: WATERFALL + LAKE -------------------
// East-west cliff wall at z=-25, spanning x from -24 to 24 (waterfall
// source). Height y=0..5.4. Lake is south (z<-25), arena is north.
for (int i = 0; i < 9; ++i) {
float xx = -24.0f + i * 6.0f;
auto b = CreateGameObject(
CreateCube(glm::vec3(0.80f, 0.76f, 0.70f)),
glm::vec3(xx, 2.7f, -25.0f));
b->SetScale(glm::vec3(6.2f, 5.4f + jit(0.3f), 4.0f));
b->SetRotation(glm::vec3(jit(2), jit(6), jit(2)));
b->SetTexture(rock_tex); b->SetNormalMap(noise_t);
b->SetRoughness(0.85f);
AddGameObject(b);
}
// Waterfall curtain sheet — along the south face of the cliff
{
auto sheet = CreateGameObject(
CreateCube(glm::vec3(0.75f, 0.90f, 0.98f)),
glm::vec3(0.0f, 2.6f, -27.0f));
sheet->SetScale(glm::vec3(50.0f, 5.2f, 0.2f));
sheet->SetMetallic(0.0f);
sheet->SetRoughness(0.12f);
sheet->SetEmissive(glm::vec3(0.5f, 0.8f, 1.0f));
AddGameObject(sheet);
}
// Splash boulders at the foot of the cliff, on the lake side
for (int i = 0; i < 12; ++i) {
float xx = rnd(-22.0f, 22.0f);
float zz = rnd(-32.0f, -27.0f);
float s = rnd(0.5f, 1.2f);
auto r = CreateGameObject(
CreateSphere(14, glm::vec3(0.86f, 0.82f, 0.78f)),
glm::vec3(xx, s * 0.35f, zz));
r->SetScale(glm::vec3(s, s * 0.6f, s * 0.95f));
r->SetTexture(rock_tex); r->SetNormalMap(noise_t);
r->SetRoughness(0.7f);
AddGameObject(r);
}
// Lake-side trees
auto tree = [&](glm::vec3 at, float s) {
auto trunk = CreateGameObject(
CreateCylinder(0.22f * s, 2.5f * s, 10,
glm::vec3(0.88f, 0.84f, 0.78f)),
at + glm::vec3(0, 1.25f * s, 0));
trunk->SetRotation(glm::vec3(jit(3), jit(360), jit(3)));
trunk->SetTexture(wood_tex); trunk->SetNormalMap(noise_t);
trunk->SetRoughness(0.85f);
AddGameObject(trunk);
glm::vec3 cc(rnd(0.12f, 0.22f), rnd(0.38f, 0.55f), rnd(0.13f, 0.20f));
auto cone = CreateGameObject(
CreateCone(1.2f * s, 2.7f * s, 10, cc),
at + glm::vec3(0, 2.9f * s, 0));
cone->SetNormalMap(noise_t); cone->SetRoughness(0.8f);
AddGameObject(cone);
};
// Floating barrels in the lake (flow carries them west)
for (int i = 0; i < 6; ++i) {
float xx = rnd(-10.0f, 5.0f);
float zz = rnd(-40.0f, -15.0f);
auto barrel = CreateGameObject(
CreateCylinder(0.7f, 1.3f, 12, glm::vec3(0.9f, 0.78f, 0.55f)),
glm::vec3(xx, 1.2f, zz));
barrel->SetRotation(glm::vec3(0, jit(180.0f), 90.0f));
barrel->SetTexture(wood_tex); barrel->SetNormalMap(noise_t);
barrel->SetRoughness(0.8f);
AddGameObject(barrel);
std::string id = "barrel_" + std::to_string(i);
namedObjects_[id] = barrel;
bodyCmd(id, "box", 1.0f, 0.3f, 0.4f, false);
}
// -------------------- PERIPHERY: trees, rocks, mountains --------
// Trees scattered around perimeter (avoiding zones)
for (int i = 0; i < 40; ++i) {
float x, z;
int tries = 0;
do {
x = rnd(-90.0f, 90.0f);
z = rnd(-60.0f, 90.0f);
tries++;
} while ((std::abs(x) < 50.0f && std::abs(z) < 30.0f) && tries < 20); // avoid center zones
tree(glm::vec3(x, 0, z), rnd(0.7f, 1.4f));
}
// Scatter boulders
for (int i = 0; i < 50; ++i) {
float x, z;
do {
x = rnd(-90.0f, 90.0f);
z = rnd(-60.0f, 90.0f);
} while (std::abs(x) < 30.0f && std::abs(z) < 25.0f);
float s = rnd(0.4f, 1.4f);
auto r = CreateGameObject(
CreateSphere(12, glm::vec3(0.84f, 0.81f, 0.76f)),
glm::vec3(x, s * 0.35f, z));
r->SetScale(glm::vec3(s, s * rnd(0.5f, 0.8f), s * rnd(0.85f, 1.1f)));
r->SetRotation(glm::vec3(jit(12), jit(180), jit(12)));
r->SetTexture(rock_tex); r->SetNormalMap(noise_t);
r->SetRoughness(0.9f);
AddGameObject(r);
}
// Distant mountains
for (int i = 0; i < 14; ++i) {
float x = -160.0f + i * 24.0f + jit(6.0f);
float z = -140.0f + jit(14.0f);
float s = rnd(0.9f, 1.4f);
float h = 22.0f * s;
auto m = CreateGameObject(
CreateCone(12.0f * s, h, 10, glm::vec3(0.22f, 0.20f, 0.24f)),
glm::vec3(x, h * 0.5f, z));
AddGameObject(m);
}
// Wildflowers for bloom accents near the arena
for (int i = 0; i < 80; ++i) {
float x, z;
do {
x = rnd(-50.0f, 50.0f);
z = rnd(-15.0f, 45.0f);
} while (std::abs(x) < 16.0f && std::abs(z) < 10.0f);
glm::vec3 col;
int pick = (int)(rng_() % 5);
if (pick == 0) col = glm::vec3(1.5f, 1.1f, 0.3f);
else if (pick == 1) col = glm::vec3(1.4f, 0.4f, 0.8f);
else if (pick == 2) col = glm::vec3(1.2f, 1.2f, 1.1f);
else if (pick == 3) col = glm::vec3(0.75f, 0.45f, 1.3f);
else col = glm::vec3(1.5f, 0.5f, 0.2f);
auto fl = CreateGameObject(CreateSphere(6, col),
glm::vec3(x, 0.2f + jit(0.05f), z));
fl->SetScale(glm::vec3(0.08f));
fl->SetEmissive(col * 0.25f);
AddGameObject(fl);
}
processAgentCommand(json::parse(R"({"action":"physics","enabled":true})"));
// Camera: overlook from southwest, angled at the arena
glm::vec3 eye(-22.0f, 9.0f, 18.0f);
glm::vec3 target(4.0f, 3.0f, 0.0f);
SetCameraPosition(eye);
SetCameraTarget(target);
glm::vec3 fwd = glm::normalize(target - eye);
cam_pitch_ = glm::degrees(std::asin(fwd.y));
cam_yaw_ = glm::degrees(std::atan2(fwd.z, fwd.x));
last_cam_pos_ = eye;
std::cout << "Physics Playground ready: " << game_objects_.size() << " objects.\n"
<< " F / LMB / R1 — shoot an HDR metal ball\n"
<< " T — cycle storm (calm / squall / gale)\n"
<< " G — chaos: launch all dynamic bodies upward\n"
<< " Every 18s the catapult hurls a heavy sphere at the arena.\n"
<< " Domino cascade fires at t=2s.\n";
return true;
}
void FireBall() {
glm::vec3 cp = GetCameraPosition();
glm::vec3 tg = GetCameraTarget();
glm::vec3 fwd = glm::normalize(tg - cp);
if ((int)active_shots_.size() >= kMaxShots) {
auto old = active_shots_.front(); active_shots_.pop_front();
processAgentCommand(json::parse("{\"action\":\"delete\",\"id\":\"" + old + "\"}"));
}
std::uniform_real_distribution<float> cd(0.6f, 1.7f);
glm::vec3 col(cd(rng_), cd(rng_), cd(rng_));
std::string id = "shot_" + std::to_string(shot_counter_++);
auto b = CreateGameObject(CreateSphere(20, col), cp + fwd * 1.4f);
b->SetScale(glm::vec3(0.35f));
b->SetMetallic(1.0f);
b->SetRoughness(0.25f);
AddGameObject(b);
namedObjects_[id] = b;
json::Object bc;
bc["action"]=json::Value(std::string("body"));
bc["id"]=json::Value(id);
bc["shape"]=json::Value(std::string("sphere"));
bc["mass"]=json::Value(1.5);
bc["restitution"]=json::Value(0.55);
bc["friction"]=json::Value(0.3);
processAgentCommand(json::Value(bc));
json::Object vc;
vc["action"]=json::Value(std::string("velocity"));
vc["id"]=json::Value(id);
vc["linear"]=json::Value(json::Array{
json::Value((double)(fwd.x * 60)),
json::Value((double)(fwd.y * 60)),
json::Value((double)(fwd.z * 60))});
processAgentCommand(json::Value(vc));
active_shots_.push_back(id);
}
void CycleStorm() {
storm_state_ = (storm_state_ + 1) % 3;
float s = storm_state_ == 0 ? 0.0f : (storm_state_ == 1 ? 0.4f : 0.85f);
json::Object w;
w["action"] = json::Value(std::string("weather"));
w["storminess"] = json::Value((double)s);
w["wind_dir"] = json::Value(json::Array{
json::Value(-1.0), json::Value(0.0), json::Value(0.3)});
w["wind_speed"] = json::Value((double)(0.5 + s * 4.0));
processAgentCommand(json::Value(w));
std::cout << "[storm] state=" << storm_state_ << " storminess=" << s << "\n";
}
void ChaosBurst() {
std::uniform_real_distribution<float> rx(-4.0f, 4.0f);
std::uniform_real_distribution<float> ry(10.0f, 22.0f);
std::uniform_real_distribution<float> rr(-6.0f, 6.0f);
for (auto& [name, handle] : bodyHandleByName_) {
phys::Body* b = physics_.Get(handle);
if (!b || b->inv_mass == 0.0f) continue;
b->linear_velocity += glm::vec3(rx(rng_), ry(rng_), rx(rng_));
b->angular_velocity += glm::vec3(rr(rng_), rr(rng_), rr(rng_));
b->sleeping = false;
b->sleep_timer = 0.0f;
}
std::cout << "[chaos] launched " << bodyHandleByName_.size() << " bodies\n";
}
void FireCatapult() {
if ((int)active_cata_.size() >= kMaxCata) {
auto old = active_cata_.front(); active_cata_.pop_front();
processAgentCommand(json::parse("{\"action\":\"delete\",\"id\":\"" + old + "\"}"));
}
std::string id = "cata_" + std::to_string(cata_counter_++);
auto b = CreateGameObject(
CreateSphere(32, glm::vec3(0.55f, 0.48f, 0.40f)),
glm::vec3(-45.0f, 9.2f, 0.0f));
b->SetScale(glm::vec3(1.2f));
b->SetMetallic(1.0f);
b->SetRoughness(0.45f);
AddGameObject(b);
namedObjects_[id] = b;
json::Object bc;
bc["action"]=json::Value(std::string("body"));
bc["id"]=json::Value(id);
bc["shape"]=json::Value(std::string("sphere"));
bc["mass"]=json::Value(15.0); // heavy projectile
bc["restitution"]=json::Value(0.35);
bc["friction"]=json::Value(0.3);
processAgentCommand(json::Value(bc));
json::Object vc;
vc["action"]=json::Value(std::string("velocity"));
vc["id"]=json::Value(id);
// Hurl toward +X and slightly down, aimed at the crate pyramid
vc["linear"]=json::Value(json::Array{
json::Value(22.0), json::Value(-2.0), json::Value(0.0)});
processAgentCommand(json::Value(vc));
active_cata_.push_back(id);
std::cout << "[catapult] fire\n";
}
void EmitDroplet() {
if ((int)active_drops_.size() >= kMaxDrops) {
auto old = active_drops_.front(); active_drops_.pop_front();
processAgentCommand(json::parse("{\"action\":\"delete\",\"id\":\"" + old + "\"}"));
}
// Spawn along the top of the cliff (x in [-24, 24]), z just south of cliff
std::uniform_real_distribution<float> xx(-22.0f, 22.0f);
std::uniform_real_distribution<float> zz(-27.5f, -26.0f);
std::uniform_real_distribution<float> yy(-0.15f, 0.25f);
std::string id = "drop_" + std::to_string(drop_counter_++);
glm::vec3 spawn(xx(drop_rng_), 5.5f + yy(drop_rng_), zz(drop_rng_));
auto d = CreateGameObject(
CreateSphere(8, glm::vec3(0.7f, 0.88f, 1.0f)),
spawn);
d->SetScale(glm::vec3(0.18f));
d->SetMetallic(0.0f);
d->SetRoughness(0.15f);
d->SetEmissive(glm::vec3(0.4f, 0.7f, 0.95f));
AddGameObject(d);
namedObjects_[id] = d;
json::Object bc;
bc["action"]=json::Value(std::string("body"));
bc["id"]=json::Value(id);
bc["shape"]=json::Value(std::string("sphere"));
bc["mass"]=json::Value(0.08);
bc["restitution"]=json::Value(0.2);
bc["friction"]=json::Value(0.05);
processAgentCommand(json::Value(bc));
// Initial velocity: southward + slight downward
std::uniform_real_distribution<float> vz(-2.5f, -1.0f);
std::uniform_real_distribution<float> vy(-1.5f, -0.5f);
std::uniform_real_distribution<float> vx(-0.4f, 0.4f);
json::Object vc;
vc["action"]=json::Value(std::string("velocity"));
vc["id"]=json::Value(id);
vc["linear"]=json::Value(json::Array{
json::Value((double)vx(drop_rng_)),
json::Value((double)vy(drop_rng_)),
json::Value((double)vz(drop_rng_))});
processAgentCommand(json::Value(vc));
active_drops_.push_back(id);
}
void OnUpdate(float dt) override {
time_ += dt;
// Pulse emissive bollards
{
float pulse = 0.4f + 0.6f * (0.5f + 0.5f * std::sin(time_ * 2.3f));
glm::vec3 hot(3.0f * pulse, 1.3f * pulse, 0.4f * pulse);
for (auto& id : bollard_ids_) {
auto it = namedObjects_.find(id);
if (it != namedObjects_.end()) it->second->SetEmissive(hot);
}
}
// Kickoff chain reaction at t=2s
if (!kickoff_fired_ && time_ > 2.0f) {
kickoff_fired_ = true;
json::Object c;
c["action"]=json::Value(std::string("impulse"));
c["id"]=json::Value(std::string("kickoff"));
c["impulse"]=json::Value(json::Array{
json::Value(-150.0), json::Value(0.0), json::Value(0.0)});
processAgentCommand(json::Value(c));
}
// Periodic catapult
catapult_timer_ -= dt;
if (catapult_timer_ <= 0.0f) {
FireCatapult();
catapult_timer_ = 18.0f;
}
// Waterfall emitter (~20/sec)
drop_timer_ -= dt;
while (drop_timer_ <= 0.0f) { EmitDroplet(); drop_timer_ += 0.05f; }
// Shoot (F / LMB / R1)
shot_cooldown_ -= dt;
if (shot_cooldown_ <= 0.0f) {
bool fire = Input::IsKeyHeld(GLFW_KEY_F)
|| Input::IsMouseButtonPressed(Input::MouseButton::Left)
|| Input::IsGamepadButtonDown(Input::GamepadButton::R1);
if (fire) { FireBall(); shot_cooldown_ = 0.14f; }
}
// T toggle storm (edge trigger)
bool t_now = Input::IsKeyHeld(GLFW_KEY_T)
|| Input::IsGamepadButtonDown(Input::GamepadButton::Triangle);
if (t_now && !t_down_prev_) CycleStorm();
t_down_prev_ = t_now;
// G chaos (edge trigger)
bool g_now = Input::IsKeyHeld(GLFW_KEY_G)
|| Input::IsGamepadButtonDown(Input::GamepadButton::Circle);
if (g_now && !g_down_prev_) ChaosBurst();
g_down_prev_ = g_now;
// --- Free-fly camera ---
glm::vec3 eng_pos = GetCameraPosition();
if (glm::length(eng_pos - last_cam_pos_) > 0.0001f) {
glm::vec3 fwd = GetCameraTarget() - eng_pos;
if (glm::length(fwd) > 0.0001f) {
fwd = glm::normalize(fwd);
cam_pitch_ = glm::degrees(std::asin(fwd.y));
cam_yaw_ = glm::degrees(std::atan2(fwd.z, fwd.x));
}
}
glm::vec2 ls{0}, rs{0};
float up = 0, down = 0, speed = cam_speed_;
bool touched = false;
if (Input::IsGamepadConnected()) {
ls = Input::GetLeftStick();
rs = Input::GetRightStick();
up = Input::GetR2();
down = Input::GetL2();
if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross)) speed *= 3.0f;
if (glm::length(ls) + glm::length(rs) + up + down > 0.01f) touched = true;
}
if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT)) speed *= 3.0f;
if (Input::IsKeyHeld(GLFW_KEY_W)) { ls.y -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_S)) { ls.y += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_A)) { ls.x -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_D)) { ls.x += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_LEFT)) { rs.x -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) { rs.x += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_UP)) { rs.y -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_DOWN)) { rs.y += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_SPACE)) { up += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL)) { down += 1.0f; touched = true; }
if (!touched) { last_cam_pos_ = eng_pos; return; }
cam_yaw_ += rs.x * look_sensitivity_ * dt;
cam_pitch_ -= rs.y * look_sensitivity_ * dt;
if (cam_pitch_ > 89.0f) cam_pitch_ = 89.0f;
if (cam_pitch_ < -89.0f) cam_pitch_ = -89.0f;
glm::vec3 fwd;
fwd.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
fwd.y = std::sin(glm::radians(cam_pitch_));
fwd.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
fwd = glm::normalize(fwd);
glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
glm::vec3 pos = eng_pos;
pos += fwd * (-ls.y) * speed * dt;
pos += right * ls.x * speed * dt;
pos.y += (up - down) * speed * dt;
SetCameraPosition(pos);
SetCameraTarget(pos + fwd);
last_cam_pos_ = pos;
}
};
// ---------------------------------------------
// WaterfallDemo: natural waterfall over a rocky cliff. Showcases:
// - Two water surfaces at different elevations (upper stream, lower pool)
// - Directional flow current carrying debris downstream
// - Dense physics droplet cascade off the cliff edge
// - Translucent "curtain" geometry for the body of the waterfall
// - PBR rock + wet-rock specular near the falls
// ---------------------------------------------
class WaterfallDemo : public Engine {
float cam_yaw_ = 180.0f;
float cam_pitch_ = -5.0f;
float cam_speed_ = 18.0f;
float look_sensitivity_ = 80.0f;
glm::vec3 last_cam_pos_{0};
float drop_timer_ = 0.0f;
int drop_counter_ = 0;
std::deque<std::string> active_drops_;
static constexpr int kMaxDrops = 200;
std::mt19937 drop_rng_{77};
std::mt19937 scene_rng_{19};
int log_counter_ = 0;
float log_timer_ = 0.0f;
std::deque<std::string> active_logs_;
static constexpr int kMaxLogs = 8;
public:
WaterfallDemo()
: Engine(1920, 1080, "ZoomEngine - Waterfall (Flow Physics)") {}
bool OnInitialize() override {
auto& rng = scene_rng_;
auto rnd = [&](float lo, float hi) {
std::uniform_real_distribution<float> d(lo, hi); return d(rng);
};
auto jit = [&](float s) { return rnd(-s, s); };
// --- Daylight palette, slightly overcast cool tones (misty) ---
processAgentCommand(json::parse(R"({
"action":"sun",
"direction":[-0.30, 0.75, -0.25],
"color":[1.85, 1.65, 1.40],
"zenith":[0.28, 0.45, 0.65],
"horizon":[0.78, 0.84, 0.88],
"sky":[0.62, 0.72, 0.78],
"ground":[0.10, 0.12, 0.10],
"shadow_extent":100,
"exposure":0.95,
"bloom_threshold":1.25,
"bloom_intensity":1.0,
"fog_density":0.010
})"));
// --- Lower pool + downstream river: primary water, flowing west ---
processAgentCommand(json::parse(R"({
"action":"water","enabled":true,
"level":-0.2,"size":300,
"amplitude":0.20,"wavelength":9,"speed":1.0,
"shallow":[0.40, 0.58, 0.58],"deep":[0.03, 0.09, 0.14],
"density":1.0,"resolution":160,
"flow_dir":[-1, 0, 0],"flow_speed":4.0
})"));
// --- Upper stream: secondary water at top of cliff, smaller patch ---
processAgentCommand(json::parse(R"({
"action":"water2","enabled":true,
"center":[38, 0, 0], "size":55, "level":7.8,
"amplitude":0.08,"wavelength":5,"speed":0.9,
"shallow":[0.42, 0.58, 0.60],
"deep":[0.05, 0.15, 0.22],
"resolution":80
})"));
processAgentCommand(json::parse(R"({
"action":"weather","wind_dir":[-1,0,0],"wind_speed":0.4,
"storminess":0.10,"current_coef":0.3
})"));
GLuint rock_tex = TextureCache::Get("rock");
GLuint noise_tex = TextureCache::Get("noise");
GLuint wood_tex = TextureCache::Get("wood");
GLuint grass_tex = TextureCache::Get("grass");
// --- Two ground strips leaving river channel open ---
auto north_bank = CreateGameObject(
CreatePlane(220.0f, 100.0f, glm::vec3(0.85f, 0.90f, 0.82f)),
glm::vec3(-40.0f, 0.0f, 60.0f));
north_bank->SetTexture(grass_tex); north_bank->SetNormalMap(noise_tex);
north_bank->SetUVScale(glm::vec2(0.25f)); north_bank->SetRoughness(0.95f);
AddGameObject(north_bank);
auto south_bank = CreateGameObject(
CreatePlane(220.0f, 100.0f, glm::vec3(0.85f, 0.90f, 0.82f)),
glm::vec3(-40.0f, 0.0f, -60.0f));
south_bank->SetTexture(grass_tex); south_bank->SetNormalMap(noise_tex);
south_bank->SetUVScale(glm::vec2(0.25f)); south_bank->SetRoughness(0.95f);
AddGameObject(south_bank);
// Upper (clifftop) plateau
auto plateau = CreateGameObject(
CreatePlane(120.0f, 140.0f, glm::vec3(0.75f, 0.82f, 0.68f)),
glm::vec3(90.0f, 7.5f, 0.0f));
plateau->SetTexture(grass_tex); plateau->SetNormalMap(noise_tex);
plateau->SetUVScale(glm::vec2(0.22f)); plateau->SetRoughness(0.95f);
AddGameObject(plateau);
// Stream bed at clifftop (slight dirt depression behind the falls)
auto stream_bed = CreateGameObject(
CreatePlane(60.0f, 40.0f, glm::vec3(0.45f, 0.42f, 0.35f)),
glm::vec3(38.0f, 7.2f, 0.0f));
stream_bed->SetTexture(TextureCache::Get("dirt"));
stream_bed->SetNormalMap(noise_tex);
stream_bed->SetUVScale(glm::vec2(0.3f));
AddGameObject(stream_bed);
// --- Cliff face: stacked rock cubes forming a vertical wall ---
auto cliffBlock = [&](glm::vec3 pos, glm::vec3 scale, float rough = 0.85f) {
auto b = CreateGameObject(
CreateCube(glm::vec3(0.78f, 0.74f, 0.68f)),
pos);
b->SetScale(scale);
b->SetRotation(glm::vec3(jit(4), jit(10), jit(4)));
b->SetTexture(rock_tex);
b->SetNormalMap(noise_tex);
b->SetRoughness(rough);
AddGameObject(b);
};
// Main cliff wall at x~10, running along Z
for (int z = -8; z <= 8; ++z) {
float h = 7.5f + jit(0.4f);
cliffBlock(glm::vec3(10.0f + jit(0.6f), h * 0.5f, (float)z * 2.8f),
glm::vec3(3.0f + jit(0.5f), h, 2.8f + jit(0.3f)));
}
// Stepped rocks just in front (splash-worn)
for (int i = 0; i < 10; ++i) {
float x = 7.5f + jit(1.2f);
float z = rnd(-10.0f, 10.0f);
float s = rnd(1.2f, 2.2f);
cliffBlock(glm::vec3(x, s * 0.4f, z), glm::vec3(s, s * 0.8f, s * 0.9f), 0.55f);
}
// Boulders in the splash pool
for (int i = 0; i < 14; ++i) {
float x = rnd(4.0f, 9.0f);
float z = rnd(-10.0f, 10.0f);
float s = rnd(0.6f, 1.3f);
auto r = CreateGameObject(
CreateSphere(14, glm::vec3(0.88f, 0.85f, 0.80f)),
glm::vec3(x, s * 0.4f, z));
r->SetScale(glm::vec3(s, s * 0.65f, s * 0.95f));
r->SetTexture(rock_tex);
r->SetNormalMap(noise_tex);
r->SetRoughness(0.6f); // wet rocks shine more
AddGameObject(r);
}
// --- Waterfall curtain: a semi-transparent vertical sheet as the body
// of flowing water. Emissive aqua-blue so it picks up bloom. The
// droplets stream in front of it give the detail.
{
auto sheet = CreateGameObject(
CreateCube(glm::vec3(0.75f, 0.90f, 0.98f)),
glm::vec3(9.5f, 4.0f, 0.0f));
sheet->SetScale(glm::vec3(0.25f, 8.4f, 20.0f));
sheet->SetMetallic(0.0f);
sheet->SetRoughness(0.12f);
sheet->SetEmissive(glm::vec3(0.55f, 0.85f, 1.00f)); // bloom glow
AddGameObject(sheet);
// Inner brighter core (taller, thinner — gives depth)
auto core = CreateGameObject(
CreateCube(glm::vec3(0.95f, 0.98f, 1.0f)),
glm::vec3(9.4f, 4.0f, 0.0f));
core->SetScale(glm::vec3(0.12f, 8.2f, 11.0f));
core->SetEmissive(glm::vec3(1.3f, 1.6f, 2.0f));
AddGameObject(core);
}
// Mist volume at the base (bright emissive spheres that bloom)
for (int i = 0; i < 30; ++i) {
float x = rnd(6.0f, 10.0f);
float y = rnd(0.2f, 2.0f);
float z = rnd(-11.0f, 11.0f);
auto m = CreateGameObject(
CreateSphere(6, glm::vec3(1.0f)),
glm::vec3(x, y, z));
m->SetScale(glm::vec3(rnd(0.35f, 0.75f)));
m->SetEmissive(glm::vec3(1.5f, 1.8f, 2.0f));
AddGameObject(m);
}
// --- Trees, rocks on banks ---
auto tree = [&](glm::vec3 at, float s) {
auto trunk = CreateGameObject(
CreateCylinder(0.23f * s, 2.6f * s, 10,
glm::vec3(0.9f, 0.84f, 0.78f)),
at + glm::vec3(0, 1.3f * s, 0));
trunk->SetRotation(glm::vec3(jit(4), jit(360), jit(4)));
trunk->SetTexture(wood_tex);
trunk->SetNormalMap(noise_tex);
trunk->SetRoughness(0.85f);
AddGameObject(trunk);
glm::vec3 cc(rnd(0.12f, 0.22f), rnd(0.38f, 0.52f), rnd(0.15f, 0.22f));
auto cone = CreateGameObject(
CreateCone(1.2f * s, 2.7f * s, 10, cc),
at + glm::vec3(jit(0.15f), 3.0f * s, jit(0.15f)));
cone->SetNormalMap(noise_tex);
cone->SetRoughness(0.8f);
AddGameObject(cone);
};
// Downstream banks
for (int i = 0; i < 35; ++i) {
float x = rnd(-110.0f, 0.0f);
float z = (i % 2 == 0 ? 1 : -1) * rnd(20.0f, 55.0f);
tree(glm::vec3(x, 0, z), rnd(0.8f, 1.4f));
}
// Upper plateau
for (int i = 0; i < 22; ++i) {
float x = rnd(40.0f, 140.0f);
float z = rnd(-60.0f, 60.0f);
tree(glm::vec3(x, 7.5f, z), rnd(0.8f, 1.3f));
}
// Scattered boulders on banks
for (int i = 0; i < 22; ++i) {
float x = rnd(-90.0f, -2.0f);
float z = (i % 2 == 0 ? 1 : -1) * rnd(12.0f, 38.0f);
float s = rnd(0.5f, 1.6f);
auto r = CreateGameObject(
CreateSphere(12, glm::vec3(0.85f, 0.82f, 0.78f)),
glm::vec3(x, s * 0.35f, z));
r->SetScale(glm::vec3(s, s * 0.6f, s * 0.95f));
r->SetRotation(glm::vec3(jit(12), jit(180), jit(12)));
r->SetTexture(rock_tex);
r->SetNormalMap(noise_tex);
r->SetRoughness(0.88f);
AddGameObject(r);
}
// Distant mountains
for (int i = 0; i < 10; ++i) {
float x = -130.0f + i * 26.0f + jit(6.0f);
float z = -150.0f + jit(12.0f);
float s = rnd(0.9f, 1.4f);
float h = 24.0f * s;
auto m = CreateGameObject(
CreateCone(12.0f * s, h, 10, glm::vec3(0.26f, 0.28f, 0.32f)),
glm::vec3(x, h * 0.5f, z));
AddGameObject(m);
}
processAgentCommand(json::parse(R"({"action":"physics","enabled":true})"));
// Camera: parked downstream looking back at the falls
glm::vec3 eye(-25.0f, 4.0f, 0.0f);
glm::vec3 target(10.0f, 4.0f, 0.0f);
SetCameraPosition(eye);
SetCameraTarget(target);
glm::vec3 fwd = glm::normalize(target - eye);
cam_pitch_ = glm::degrees(std::asin(fwd.y));
cam_yaw_ = glm::degrees(std::atan2(fwd.z, fwd.x));
last_cam_pos_ = eye;
std::cout << "Waterfall demo ready: " << game_objects_.size() << " objects.\n"
<< "Flow current carries floating debris downstream.\n"
<< "Camera: WASD/arrows + gamepad, Space/LCtrl up/down, LShift sprint.\n";
return true;
}
void EmitDroplet() {
if ((int)active_drops_.size() >= kMaxDrops) {
std::string old = active_drops_.front();
active_drops_.pop_front();
json::Object dc;
dc["action"] = json::Value(std::string("delete"));
dc["id"] = json::Value(old);
processAgentCommand(json::Value(dc));
}
std::uniform_real_distribution<float> zz(-9.0f, 9.0f);
std::uniform_real_distribution<float> yy(-0.2f, 0.1f);
std::uniform_real_distribution<float> xx(-0.3f, 0.3f);
std::string id = "drop_" + std::to_string(drop_counter_++);
glm::vec3 spawn(10.0f + xx(drop_rng_),
7.7f + yy(drop_rng_),
zz(drop_rng_));
// Occasional slightly brighter droplet for visual density
bool bright = (drop_counter_ % 7 == 0);
glm::vec3 col = bright ? glm::vec3(0.9f, 1.0f, 1.2f)
: glm::vec3(0.65f, 0.85f, 0.98f);
auto d = CreateGameObject(CreateSphere(8, col), spawn);
d->SetScale(glm::vec3(bright ? 0.22f : 0.18f));
d->SetMetallic(0.0f);
d->SetRoughness(0.12f);
d->SetEmissive(glm::vec3(0.6f, 0.9f, 1.1f) * (bright ? 1.3f : 1.0f));
AddGameObject(d);
namedObjects_[id] = d;
json::Object bc;
bc["action"] = json::Value(std::string("body"));
bc["id"] = json::Value(id);
bc["shape"] = json::Value(std::string("sphere"));
bc["mass"] = json::Value(0.08);
bc["restitution"] = json::Value(0.2);
bc["friction"] = json::Value(0.05);
processAgentCommand(json::Value(bc));
// Initial velocity: mostly downward + westward
std::uniform_real_distribution<float> vx(-3.0f, -1.5f);
std::uniform_real_distribution<float> vy(-2.0f, -1.0f);
std::uniform_real_distribution<float> vz(-0.5f, 0.5f);
json::Object vc;
vc["action"] = json::Value(std::string("velocity"));
vc["id"] = json::Value(id);
vc["linear"] = json::Value(json::Array{
json::Value((double)vx(drop_rng_)),
json::Value((double)vy(drop_rng_)),
json::Value((double)vz(drop_rng_))});
processAgentCommand(json::Value(vc));
active_drops_.push_back(id);
}
void EmitLog() {
// Periodically drop a floating log into the upper stream so it flows
// over the falls and tumbles downstream in the pool — shows flow physics.
if ((int)active_logs_.size() >= kMaxLogs) {
std::string old = active_logs_.front();
active_logs_.pop_front();
json::Object dc;
dc["action"] = json::Value(std::string("delete"));
dc["id"] = json::Value(old);
processAgentCommand(json::Value(dc));
}
std::uniform_real_distribution<float> zz(-6.0f, 6.0f);
std::uniform_real_distribution<float> yaw(0.0f, 360.0f);
std::string id = "log_" + std::to_string(log_counter_++);
auto log = CreateGameObject(
CreateCylinder(0.35f, 2.4f, 10, glm::vec3(0.9f, 0.80f, 0.65f)),
glm::vec3(60.0f, 8.3f, zz(drop_rng_)));
log->SetRotation(glm::vec3(0, yaw(drop_rng_), 90.0f));
log->SetTexture(TextureCache::Get("wood"));
log->SetNormalMap(TextureCache::Get("noise"));
log->SetRoughness(0.85f);
AddGameObject(log);
namedObjects_[id] = log;
json::Object bc;
bc["action"] = json::Value(std::string("body"));
bc["id"] = json::Value(id);
bc["shape"] = json::Value(std::string("box"));
bc["mass"] = json::Value(1.2);
bc["restitution"] = json::Value(0.2);
bc["friction"] = json::Value(0.35);
processAgentCommand(json::Value(bc));
json::Object vc;
vc["action"] = json::Value(std::string("velocity"));
vc["id"] = json::Value(id);
vc["linear"] = json::Value(json::Array{
json::Value(-2.0),
json::Value(0.0),
json::Value(0.0)});
processAgentCommand(json::Value(vc));
active_logs_.push_back(id);
}
void OnUpdate(float dt) override {
// Dense droplet emitter: ~30/sec
drop_timer_ -= dt;
while (drop_timer_ <= 0.0f) {
EmitDroplet();
drop_timer_ += 0.033f;
}
// Floating log every ~3 seconds to demo flow current
log_timer_ -= dt;
if (log_timer_ <= 0.0f) {
EmitLog();
log_timer_ = 3.0f;
}
// Free-fly camera (same pattern as other scenes) ---------------------
glm::vec3 eng_pos = GetCameraPosition();
if (glm::length(eng_pos - last_cam_pos_) > 0.0001f) {
glm::vec3 fwd = GetCameraTarget() - eng_pos;
if (glm::length(fwd) > 0.0001f) {
fwd = glm::normalize(fwd);
cam_pitch_ = glm::degrees(std::asin(fwd.y));
cam_yaw_ = glm::degrees(std::atan2(fwd.z, fwd.x));
}
}
glm::vec2 ls{0}, rs{0};
float up = 0, down = 0, speed = cam_speed_;
bool touched = false;
if (Input::IsGamepadConnected()) {
ls = Input::GetLeftStick();
rs = Input::GetRightStick();
up = Input::GetR2();
down = Input::GetL2();
if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross)) speed *= 3.0f;
if (glm::length(ls) + glm::length(rs) + up + down > 0.01f) touched = true;
}
if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT)) speed *= 3.0f;
if (Input::IsKeyHeld(GLFW_KEY_W)) { ls.y -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_S)) { ls.y += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_A)) { ls.x -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_D)) { ls.x += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_LEFT)) { rs.x -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) { rs.x += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_UP)) { rs.y -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_DOWN)) { rs.y += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_SPACE)) { up += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL)) { down += 1.0f; touched = true; }
if (!touched) { last_cam_pos_ = eng_pos; return; }
cam_yaw_ += rs.x * look_sensitivity_ * dt;
cam_pitch_ -= rs.y * look_sensitivity_ * dt;
if (cam_pitch_ > 89.0f) cam_pitch_ = 89.0f;
if (cam_pitch_ < -89.0f) cam_pitch_ = -89.0f;
glm::vec3 fwd;
fwd.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
fwd.y = std::sin(glm::radians(cam_pitch_));
fwd.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
fwd = glm::normalize(fwd);
glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
glm::vec3 pos = eng_pos;
pos += fwd * (-ls.y) * speed * dt;
pos += right * ls.x * speed * dt;
pos.y += (up - down) * speed * dt;
SetCameraPosition(pos);
SetCameraTarget(pos + fwd);
last_cam_pos_ = pos;
}
};
// ---------------------------------------------
// DamDemo: concrete dam with reservoir water above, river water below, and
// a stream of physics droplets pouring through the spillway as a waterfall.
// Shows off: two separate Gerstner water surfaces at different heights, mass
// physics with buoyancy, normal-mapped concrete, PBR lighting, bloom mist.
// ---------------------------------------------
class DamDemo : public Engine {
float cam_yaw_ = 135.0f;
float cam_pitch_ = 0.0f;
float cam_speed_ = 22.0f;
float look_sensitivity_ = 80.0f;
glm::vec3 last_cam_pos_{0};
float drop_timer_ = 0.0f;
int drop_counter_ = 0;
std::deque<std::string> active_drops_;
static constexpr int kMaxDrops = 120;
std::mt19937 drop_rng_{41};
std::mt19937 scene_rng_{9};
public:
DamDemo()
: Engine(1920, 1080, "ZoomEngine - Dam (Multi-water)") {}
bool OnInitialize() override {
auto& rng = scene_rng_;
auto rnd = [&](float lo, float hi) {
std::uniform_real_distribution<float> d(lo, hi); return d(rng);
};
auto jit = [&](float s) { return rnd(-s, s); };
// --- Sun + atmosphere: late-afternoon, soft ---
processAgentCommand(json::parse(R"({
"action":"sun",
"direction":[-0.35, 0.55, -0.25],
"color":[1.7, 1.25, 0.92],
"zenith":[0.18, 0.32, 0.58],
"horizon":[0.85, 0.78, 0.68],
"sky":[0.55, 0.62, 0.70],
"ground":[0.08, 0.08, 0.09],
"shadow_extent":100,
"exposure":0.95,
"bloom_threshold":1.2,
"bloom_intensity":1.0,
"fog_density":0.008
})"));
// --- Primary water: the river below the dam (extends far downstream) ---
processAgentCommand(json::parse(R"({
"action":"water","enabled":true,
"level":-0.3,"size":300,
"amplitude":0.18,"wavelength":10,"speed":0.9,
"shallow":[0.42, 0.55, 0.55],"deep":[0.05, 0.10, 0.18],
"density":1.0,"resolution":160
})"));
// --- Secondary water: the reservoir above the dam ---
// Visual-only, at y=8 (higher than dam base), localized to a square.
processAgentCommand(json::parse(R"({
"action":"water2","enabled":true,
"center":[55, 0, 0], "size":90, "level":8.5,
"amplitude":0.10,"wavelength":7,"speed":0.5,
"shallow":[0.38, 0.55, 0.60],
"deep":[0.02, 0.08, 0.16],
"resolution":96
})"));
processAgentCommand(json::parse(R"({
"action":"weather","wind_dir":[1,0,0.2],"wind_speed":0.8,
"storminess":0.05,"current_coef":0.5
})"));
// --- Banks for the downstream river ---
// Split ground into two strips leaving a 40-unit-wide river channel
// between z=-20 and z=20. Primary water (y=-0.3) shows through the gap.
GLuint grass_tex = TextureCache::Get("grass");
GLuint dirt_tex = TextureCache::Get("dirt");
auto north_bank = CreateGameObject(
CreatePlane(220.0f, 100.0f, glm::vec3(0.88f, 0.90f, 0.84f)),
glm::vec3(-100.0f, 0.0f, 70.0f));
north_bank->SetTexture(grass_tex);
north_bank->SetNormalMap(TextureCache::Get("noise"));
north_bank->SetUVScale(glm::vec2(0.25f));
north_bank->SetRoughness(0.95f);
AddGameObject(north_bank);
auto south_bank = CreateGameObject(
CreatePlane(220.0f, 100.0f, glm::vec3(0.88f, 0.90f, 0.84f)),
glm::vec3(-100.0f, 0.0f, -70.0f));
south_bank->SetTexture(grass_tex);
south_bank->SetNormalMap(TextureCache::Get("noise"));
south_bank->SetUVScale(glm::vec2(0.25f));
south_bank->SetRoughness(0.95f);
AddGameObject(south_bank);
// Dam abutment ground on the high side, wrapping around the reservoir
auto upstream_ground = CreateGameObject(
CreatePlane(200.0f, 200.0f, glm::vec3(0.75f, 0.80f, 0.70f)),
glm::vec3(100.0f, 7.5f, 0.0f));
upstream_ground->SetTexture(grass_tex);
upstream_ground->SetNormalMap(TextureCache::Get("noise"));
upstream_ground->SetUVScale(glm::vec2(0.2f));
upstream_ground->SetRoughness(0.95f);
AddGameObject(upstream_ground);
// Reservoir floor: slight dip just below the water for shallow read
auto bed_high = CreateGameObject(
CreatePlane(96.0f, 96.0f, glm::vec3(0.55f, 0.52f, 0.42f)),
glm::vec3(55.0f, 7.8f, 0.0f));
bed_high->SetTexture(dirt_tex);
bed_high->SetNormalMap(TextureCache::Get("noise"));
bed_high->SetUVScale(glm::vec2(0.25f));
bed_high->SetRoughness(0.95f);
AddGameObject(bed_high);
// --- The Dam itself ---
// Big concrete slab with a spillway notch in the middle.
// Position at x=10, running along z-axis.
GLuint concrete = TextureCache::Get("concrete");
GLuint noise_t = TextureCache::Get("noise");
auto damBlock = [&](glm::vec3 pos, glm::vec3 scale, glm::vec3 tint = glm::vec3(0.85f, 0.85f, 0.83f)) {
auto b = CreateGameObject(CreateCube(tint), pos);
b->SetScale(scale);
b->SetTexture(concrete);
b->SetNormalMap(noise_t);
b->SetMetallic(0.0f);
b->SetRoughness(0.85f);
AddGameObject(b);
};
// Left wall (z: -30 to -6), full height y=0..13
damBlock(glm::vec3(10.0f, 6.5f, -18.0f), glm::vec3(3.5f, 13.0f, 24.0f));
// Right wall (z: 6 to 30)
damBlock(glm::vec3(10.0f, 6.5f, 18.0f), glm::vec3(3.5f, 13.0f, 24.0f));
// Spillway weir — lower section between walls, y=0..6 so reservoir can
// overflow across the top of it.
damBlock(glm::vec3(10.0f, 3.0f, 0.0f), glm::vec3(3.5f, 6.0f, 12.0f));
// Angled buttresses at the ends
damBlock(glm::vec3(13.5f, 3.5f, -30.0f), glm::vec3(4.0f, 7.0f, 4.0f));
damBlock(glm::vec3(13.5f, 3.5f, 30.0f), glm::vec3(4.0f, 7.0f, 4.0f));
// Dam top walkway
damBlock(glm::vec3(10.0f, 13.3f, 0.0f),
glm::vec3(4.5f, 0.4f, 64.0f),
glm::vec3(0.76f, 0.76f, 0.74f));
// Railing posts along the top
for (int i = 0; i < 15; ++i) {
float z = -28.0f + i * 4.0f;
auto post = CreateGameObject(
CreateCylinder(0.12f, 1.2f, 8, glm::vec3(0.78f, 0.78f, 0.78f)),
glm::vec3(10.0f + jit(1.5f), 13.0f, z));
post->SetMetallic(1.0f);
post->SetRoughness(0.4f);
AddGameObject(post);
}
// --- Surrounding banks: stone clusters and trees ---
GLuint rock_tex = TextureCache::Get("rock");
GLuint wood_tex = TextureCache::Get("wood");
auto boulder = [&](glm::vec3 pos, float s) {
auto r = CreateGameObject(
CreateSphere(16, glm::vec3(0.9f, 0.88f, 0.84f)),
pos);
r->SetScale(glm::vec3(s, s * rnd(0.55f, 0.8f), s * rnd(0.85f, 1.1f)));
r->SetRotation(glm::vec3(jit(15), jit(180), jit(15)));
r->SetTexture(rock_tex);
r->SetNormalMap(noise_t);
r->SetRoughness(0.9f);
AddGameObject(r);
};
// Rocks lining the downstream riverbank
for (int i = 0; i < 25; ++i) {
float x = rnd(-100.0f, 5.0f);
float z = (i % 2 == 0 ? -1 : 1) * rnd(18.0f, 36.0f);
boulder(glm::vec3(x, 0.6f, z), rnd(0.8f, 1.8f));
}
// Rocks at the base of the dam spillway (splash zone)
for (int i = 0; i < 12; ++i) {
float x = rnd(0.0f, 8.0f);
float z = rnd(-5.0f, 5.0f);
boulder(glm::vec3(x, 0.3f, z), rnd(0.6f, 1.2f));
}
// Trees along both banks
auto tree = [&](glm::vec3 at, float s) {
auto trunk = CreateGameObject(
CreateCylinder(0.25f * s, 2.8f * s, 10,
glm::vec3(0.9f, 0.85f, 0.8f)),
at + glm::vec3(0, 1.4f * s, 0));
trunk->SetRotation(glm::vec3(jit(3), jit(360), jit(3)));
trunk->SetTexture(wood_tex);
trunk->SetNormalMap(noise_t);
trunk->SetRoughness(0.85f);
AddGameObject(trunk);
glm::vec3 cc(rnd(0.12f, 0.25f), rnd(0.40f, 0.55f), rnd(0.14f, 0.25f));
auto cone = CreateGameObject(
CreateCone(1.3f * s, 2.8f * s, 10, cc),
at + glm::vec3(jit(0.15f), 3.2f * s, jit(0.15f)));
cone->SetNormalMap(noise_t);
cone->SetRoughness(0.8f);
AddGameObject(cone);
};
for (int i = 0; i < 30; ++i) {
float x = rnd(-100.0f, 5.0f);
float z = (i % 2 == 0 ? -1 : 1) * rnd(25.0f, 55.0f);
tree(glm::vec3(x, 0, z), rnd(0.8f, 1.4f));
}
for (int i = 0; i < 20; ++i) {
// Upstream bank trees at elevated bed
float x = rnd(20.0f, 100.0f);
float z = (i % 2 == 0 ? -1 : 1) * rnd(25.0f, 50.0f);
tree(glm::vec3(x, 7.5f, z), rnd(0.7f, 1.2f));
}
// Distant mountains
for (int i = 0; i < 12; ++i) {
float x = -120.0f + i * 24.0f + jit(6.0f);
float z = -150.0f + jit(12.0f);
float s = rnd(0.9f, 1.4f);
float h = 20.0f * s + rnd(0, 6);
float r = 12.0f * s;
auto m = CreateGameObject(
CreateCone(r, h, 10, glm::vec3(0.18f, 0.15f, 0.18f)),
glm::vec3(x, h * 0.5f, z));
AddGameObject(m);
}
// Floating barrels in the downstream river (wind pushes them)
for (int i = 0; i < 6; ++i) {
float x = rnd(-60.0f, -10.0f);
float z = rnd(-30.0f, 30.0f);
auto barrel = CreateGameObject(
CreateCylinder(0.7f, 1.3f, 12, glm::vec3(0.9f, 0.78f, 0.55f)),
glm::vec3(x, 1.2f, z));
barrel->SetRotation(glm::vec3(0, jit(180), 90.0f));
barrel->SetTexture(wood_tex);
barrel->SetNormalMap(noise_t);
barrel->SetRoughness(0.8f);
AddGameObject(barrel);
std::string id = "barrel_" + std::to_string(i);
namedObjects_[id] = barrel;
json::Object c;
c["action"] = json::Value(std::string("body"));
c["id"] = json::Value(id);
c["shape"] = json::Value(std::string("box"));
c["mass"] = json::Value(1.0);
c["restitution"] = json::Value(0.3);
c["friction"] = json::Value(0.4);
processAgentCommand(json::Value(c));
}
processAgentCommand(json::parse(R"({"action":"physics","enabled":true})"));
// Camera: looking toward the dam from downstream, slightly elevated
glm::vec3 eye(-20.0f, 4.5f, 0.0f);
glm::vec3 target(10.0f, 6.0f, 0.0f);
SetCameraPosition(eye);
SetCameraTarget(target);
glm::vec3 fwd = glm::normalize(target - eye);
cam_pitch_ = glm::degrees(std::asin(fwd.y));
cam_yaw_ = glm::degrees(std::atan2(fwd.z, fwd.x));
last_cam_pos_ = eye;
std::cout << "Dam demo ready: " << game_objects_.size() << " objects.\n"
<< "Waterfall spawns physics droplets through the spillway.\n"
<< "Camera: WASD/arrows, Space/LCtrl up/down, LShift sprint.\n";
return true;
}
void EmitDroplet() {
if ((int)active_drops_.size() >= kMaxDrops) {
std::string old = active_drops_.front();
active_drops_.pop_front();
json::Object dc;
dc["action"] = json::Value(std::string("delete"));
dc["id"] = json::Value(old);
processAgentCommand(json::Value(dc));
}
std::uniform_real_distribution<float> zz(-5.5f, 5.5f);
std::uniform_real_distribution<float> yy(-0.15f, 0.15f);
std::string id = "drop_" + std::to_string(drop_counter_++);
glm::vec3 spawn(12.0f,
8.2f + yy(drop_rng_),
zz(drop_rng_));
auto d = CreateGameObject(
CreateSphere(8, glm::vec3(0.6f, 0.8f, 0.95f)),
spawn);
d->SetScale(glm::vec3(0.22f));
d->SetMetallic(0.0f);
d->SetRoughness(0.15f);
d->SetEmissive(glm::vec3(0.15f, 0.25f, 0.3f)); // slight glow for bloom haze
AddGameObject(d);
namedObjects_[id] = d;
json::Object bc;
bc["action"] = json::Value(std::string("body"));
bc["id"] = json::Value(id);
bc["shape"] = json::Value(std::string("sphere"));
bc["mass"] = json::Value(0.1);
bc["restitution"] = json::Value(0.25);
bc["friction"] = json::Value(0.05);
processAgentCommand(json::Value(bc));
// Initial velocity: slightly downward + outward (away from dam)
std::uniform_real_distribution<float> vx(-4.0f, -2.0f);
std::uniform_real_distribution<float> vy(-1.5f, -0.5f);
std::uniform_real_distribution<float> vz(-0.8f, 0.8f);
json::Object vc;
vc["action"] = json::Value(std::string("velocity"));
vc["id"] = json::Value(id);
vc["linear"] = json::Value(json::Array{
json::Value((double)vx(drop_rng_)),
json::Value((double)vy(drop_rng_)),
json::Value((double)vz(drop_rng_))});
processAgentCommand(json::Value(vc));
active_drops_.push_back(id);
}
void OnUpdate(float dt) override {
// Waterfall emitter: ~12 droplets per second
drop_timer_ -= dt;
while (drop_timer_ <= 0.0f) {
EmitDroplet();
drop_timer_ += 0.08f;
}
// --- Free-fly camera (same pattern as other scenes) ---
glm::vec3 eng_pos = GetCameraPosition();
if (glm::length(eng_pos - last_cam_pos_) > 0.0001f) {
glm::vec3 fwd = GetCameraTarget() - eng_pos;
if (glm::length(fwd) > 0.0001f) {
fwd = glm::normalize(fwd);
cam_pitch_ = glm::degrees(std::asin(fwd.y));
cam_yaw_ = glm::degrees(std::atan2(fwd.z, fwd.x));
}
}
glm::vec2 ls{0}, rs{0};
float up = 0, down = 0, speed = cam_speed_;
bool touched = false;
if (Input::IsGamepadConnected()) {
ls = Input::GetLeftStick();
rs = Input::GetRightStick();
up = Input::GetR2();
down = Input::GetL2();
if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross)) speed *= 3.0f;
if (glm::length(ls) + glm::length(rs) + up + down > 0.01f) touched = true;
}
if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT)) speed *= 3.0f;
if (Input::IsKeyHeld(GLFW_KEY_W)) { ls.y -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_S)) { ls.y += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_A)) { ls.x -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_D)) { ls.x += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_LEFT)) { rs.x -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) { rs.x += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_UP)) { rs.y -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_DOWN)) { rs.y += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_SPACE)) { up += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL)) { down += 1.0f; touched = true; }
if (!touched) { last_cam_pos_ = eng_pos; return; }
cam_yaw_ += rs.x * look_sensitivity_ * dt;
cam_pitch_ -= rs.y * look_sensitivity_ * dt;
if (cam_pitch_ > 89.0f) cam_pitch_ = 89.0f;
if (cam_pitch_ < -89.0f) cam_pitch_ = -89.0f;
glm::vec3 fwd;
fwd.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
fwd.y = std::sin(glm::radians(cam_pitch_));
fwd.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
fwd = glm::normalize(fwd);
glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
glm::vec3 pos = eng_pos;
pos += fwd * (-ls.y) * speed * dt;
pos += right * ls.x * speed * dt;
pos.y += (up - down) * speed * dt;
SetCameraPosition(pos);
SetCameraTarget(pos + fwd);
last_cam_pos_ = pos;
}
};
// ---------------------------------------------
// SanctuaryDemo: end-to-end engine showcase.
// - PBR metals + normal-mapped stone/wood/grass
// - HDR + bloom + ACES + MSAA + atmospheric fog
// - Gerstner water with foam + buoyancy + wind current
// - Cascade physics: a heavy metal sphere rolls off a ramp, knocks over a
// line of wooden dominoes, which push a crate stack into the lake.
// - Shadows, emissive brazier, copper orb, stone pillar ring.
// Pure C++ placement; no AI.
// ---------------------------------------------
class SanctuaryDemo : public Engine {
float cam_yaw_ = 230.0f;
float cam_pitch_ = -10.0f;
float cam_speed_ = 22.0f;
float look_sensitivity_ = 80.0f;
glm::vec3 last_cam_pos_{0};
float time_elapsed_ = 0.0f;
bool kickoff_fired_ = false;
float shot_cooldown_ = 0.0f;
int shot_counter_ = 0;
std::deque<std::string> active_shots_;
static constexpr int kMaxShots = 60;
std::mt19937 shot_rng_{1337};
// Captured initial pose+body params for every dynamic object, so R / Triangle
// can put the scene back to t=0.
struct InitialState {
std::string id;
std::string shape; // "box" or "sphere"
glm::vec3 position;
glm::vec3 rotation; // Euler degrees
float mass;
float restitution;
float friction;
};
std::vector<InitialState> initial_states_;
public:
SanctuaryDemo()
: Engine(1920, 1080, "ZoomEngine - Sanctuary (Full Showcase)") {}
void ResetScene();
void FireBall() {
glm::vec3 cam_pos = GetCameraPosition();
glm::vec3 cam_tgt = GetCameraTarget();
glm::vec3 fwd = glm::normalize(cam_tgt - cam_pos);
glm::vec3 spawn = cam_pos + fwd * 1.5f;
std::string id = "shot_" + std::to_string(shot_counter_++);
// Retire oldest shot if we're at cap
if ((int)active_shots_.size() >= kMaxShots) {
std::string old = active_shots_.front();
active_shots_.pop_front();
json::Object dc;
dc["action"] = json::Value(std::string("delete"));
dc["id"] = json::Value(old);
processAgentCommand(json::Value(dc));
}
// Color palette — HDR pops for bloom
std::uniform_real_distribution<float> cd(0.6f, 1.6f);
glm::vec3 col(cd(shot_rng_), cd(shot_rng_), cd(shot_rng_));
auto ball = CreateGameObject(CreateSphere(20, col), spawn);
ball->SetScale(glm::vec3(0.35f));
ball->SetMetallic(1.0f);
ball->SetRoughness(0.3f);
AddGameObject(ball);
namedObjects_[id] = ball;
json::Object bc;
bc["action"] = json::Value(std::string("body"));
bc["id"] = json::Value(id);
bc["shape"] = json::Value(std::string("sphere"));
bc["mass"] = json::Value(1.5);
bc["restitution"] = json::Value(0.55);
bc["friction"] = json::Value(0.3);
processAgentCommand(json::Value(bc));
json::Object vc;
vc["action"] = json::Value(std::string("velocity"));
vc["id"] = json::Value(id);
vc["linear"] = json::Value(json::Array{
json::Value((double)(fwd.x * 55.0f)),
json::Value((double)(fwd.y * 55.0f)),
json::Value((double)(fwd.z * 55.0f)) });
processAgentCommand(json::Value(vc));
active_shots_.push_back(id);
}
bool OnInitialize() override {
std::mt19937 rng(17);
auto rnd = [&](float lo, float hi) {
std::uniform_real_distribution<float> d(lo, hi); return d(rng);
};
auto jit = [&](float s) { return rnd(-s, s); };
// --- Global lighting / post tuning ---
processAgentCommand(json::parse(R"({
"action":"sun",
"direction":[-0.62, 0.25, -0.18],
"color":[2.5, 1.45, 0.85],
"zenith":[0.14, 0.22, 0.48],
"horizon":[1.05, 0.58, 0.32],
"sky":[0.70, 0.48, 0.32],
"ground":[0.06, 0.06, 0.07],
"shadow_extent":110,
"exposure":0.95,
"bloom_threshold":1.15,
"bloom_intensity":1.2,
"fog_density":0.011
})"));
// --- Water: confined to a single central pool (visual only via water2).
// Main water mesh is disabled so the rest of the map is dry land.
processAgentCommand(json::parse(R"({"action":"water","enabled":false})"));
processAgentCommand(json::parse(R"({
"action":"water2","enabled":true,
"center":[0, 0, -22], "size":14, "level":0.18,
"amplitude":0.06,"wavelength":4,"speed":0.7,
"shallow":[0.45,0.55,0.55],
"deep":[0.04,0.10,0.16],
"resolution":96
})"));
processAgentCommand(json::parse(R"({
"action":"weather","wind_dir":[0.9,0,0.4],
"wind_speed":0.6,"storminess":0.05,"current_coef":0.3
})"));
// --- Ground (visual + solid physics floor) ---
auto grass = CreateGameObject(
CreatePlane(420.0f, 420.0f, glm::vec3(0.95f, 0.97f, 0.92f)),
glm::vec3(0, 0, 0));
grass->SetTexture(TextureCache::Get("grass"));
grass->SetNormalMap(TextureCache::Get("noise"));
grass->SetUVScale(glm::vec2(0.35f));
grass->SetRoughness(0.95f);
AddGameObject(grass);
// Register the grass as the ground physics body. The plane collider is
// infinite at y=0, so it catches anything anywhere on the map.
namedObjects_["__ground"] = grass;
processAgentCommand(json::parse(R"({
"action":"body","id":"__ground","shape":"plane",
"normal":[0,1,0],"offset":0,"static":true
})"));
// --- Textures / cached refs ---
GLuint wood_tex = TextureCache::Get("wood");
GLuint rock_tex = TextureCache::Get("rock");
GLuint marble_tex = TextureCache::Get("marble");
GLuint brick_tex = TextureCache::Get("brick");
GLuint concrete = TextureCache::Get("concrete");
GLuint noise_tex = TextureCache::Get("noise");
// --- Helper for physics-enabled objects ---
auto bodyCmd = [&](const std::string& id, const std::string& shape,
float mass, float rest, float frict,
bool is_static = false) {
json::Object c;
c["action"] = json::Value(std::string("body"));
c["id"] = json::Value(id);
c["shape"] = json::Value(shape);
c["mass"] = json::Value((double)mass);
c["restitution"] = json::Value((double)rest);
c["friction"] = json::Value((double)frict);
if (is_static) c["static"] = json::Value(true);
processAgentCommand(json::Value(c));
};
auto addBox = [&](const std::string& id, glm::vec3 pos, glm::vec3 scale,
glm::vec3 color, GLuint tex, GLuint normal,
float metallic, float roughness,
float mass, float rest = 0.2f, float frict = 0.65f,
glm::vec3 rot = glm::vec3(0)) {
auto obj = CreateGameObject(CreateCube(color), pos);
obj->SetScale(scale);
obj->SetRotation(rot);
if (tex) obj->SetTexture(tex);
if (normal) obj->SetNormalMap(normal);
obj->SetMetallic(metallic);
obj->SetRoughness(roughness);
AddGameObject(obj);
if (!id.empty()) namedObjects_[id] = obj;
if (mass > 0 || mass == -1.0f) // -1 = static
bodyCmd(id, "box", (mass == -1.0f) ? 0 : mass, rest, frict,
mass == -1.0f);
if (!id.empty() && mass > 0)
initial_states_.push_back({id, "box", pos, rot, mass, rest, frict});
return obj;
};
auto addSphere = [&](const std::string& id, glm::vec3 pos, float radius,
glm::vec3 color, GLuint tex, GLuint normal,
float metallic, float roughness,
float mass, float rest = 0.4f, float frict = 0.3f) {
auto obj = CreateGameObject(CreateSphere(36, color), pos);
obj->SetScale(glm::vec3(radius));
if (tex) obj->SetTexture(tex);
if (normal) obj->SetNormalMap(normal);
obj->SetMetallic(metallic);
obj->SetRoughness(roughness);
AddGameObject(obj);
if (!id.empty()) namedObjects_[id] = obj;
if (mass > 0) bodyCmd(id, "sphere", mass, rest, frict);
if (!id.empty() && mass > 0)
initial_states_.push_back({id, "sphere", pos, glm::vec3(0),
mass, rest, frict});
return obj;
};
// --- Map perimeter walls: 4 static stone walls ringing an 80×80 play
// area (X,Z in [-40,40]) so dynamic props can't bounce out of the map.
{
const float kBL = 81.0f; // wall length (slight corner overlap)
const float kBH = 6.0f; // wall height
const float kBT = 1.0f; // wall thickness
const glm::vec3 kBC(0.92f, 0.88f, 0.82f);
addBox("border_n", glm::vec3(0.0f, kBH * 0.5f, 40.5f),
glm::vec3(kBL, kBH, kBT), kBC,
marble_tex, noise_tex, 0.0f, 0.75f, -1.0f);
addBox("border_s", glm::vec3(0.0f, kBH * 0.5f, -40.5f),
glm::vec3(kBL, kBH, kBT), kBC,
marble_tex, noise_tex, 0.0f, 0.75f, -1.0f);
addBox("border_e", glm::vec3( 40.5f, kBH * 0.5f, 0.0f),
glm::vec3(kBT, kBH, kBL), kBC,
marble_tex, noise_tex, 0.0f, 0.75f, -1.0f);
addBox("border_w", glm::vec3(-40.5f, kBH * 0.5f, 0.0f),
glm::vec3(kBT, kBH, kBL), kBC,
marble_tex, noise_tex, 0.0f, 0.75f, -1.0f);
}
// --- Central pool curb: short stone walls forming a basin around the
// visible water at center=[0,0,-22], size 14. The curb hides the visual
// water seam at the edge and stops things rolling through into the pool.
{
const float kPC = -22.0f; // pool center Z
const float kPS = 14.0f; // pool size (matches water2)
const float kPH = 0.6f; // curb height
const float kPT = 0.5f; // curb thickness
const glm::vec3 kPCol(0.78f, 0.74f, 0.70f);
float half = kPS * 0.5f;
addBox("pool_n", glm::vec3(0.0f, kPH * 0.5f, kPC + half + kPT * 0.5f),
glm::vec3(kPS + kPT, kPH, kPT), kPCol,
marble_tex, noise_tex, 0.0f, 0.55f, -1.0f);
addBox("pool_s", glm::vec3(0.0f, kPH * 0.5f, kPC - half - kPT * 0.5f),
glm::vec3(kPS + kPT, kPH, kPT), kPCol,
marble_tex, noise_tex, 0.0f, 0.55f, -1.0f);
addBox("pool_e", glm::vec3( half + kPT * 0.5f, kPH * 0.5f, kPC),
glm::vec3(kPT, kPH, kPS), kPCol,
marble_tex, noise_tex, 0.0f, 0.55f, -1.0f);
addBox("pool_w", glm::vec3(-half - kPT * 0.5f, kPH * 0.5f, kPC),
glm::vec3(kPT, kPH, kPS), kPCol,
marble_tex, noise_tex, 0.0f, 0.55f, -1.0f);
}
// --- Central forge: stone altar + emissive brazier ---
addBox("altar",
glm::vec3(0, 0.75f, 0), glm::vec3(3.0f, 1.5f, 3.0f),
glm::vec3(0.88f, 0.84f, 0.80f),
marble_tex, noise_tex, 0.0f, 0.55f, 0);
// Brazier bowl (flattened sphere with copper tint)
auto bowl = CreateGameObject(
CreateSphere(32, glm::vec3(0.95f, 0.55f, 0.28f)),
glm::vec3(0, 1.65f, 0));
bowl->SetScale(glm::vec3(0.8f, 0.4f, 0.8f));
bowl->SetMetallic(1.0f);
bowl->SetRoughness(0.35f);
AddGameObject(bowl);
// Flame — two emissive cones
auto flame_outer = CreateGameObject(
CreateCone(0.5f, 1.6f, 12, glm::vec3(1.0f)),
glm::vec3(0, 2.5f, 0));
flame_outer->SetEmissive(glm::vec3(5.0f, 1.8f, 0.4f));
AddGameObject(flame_outer);
auto flame_inner = CreateGameObject(
CreateCone(0.25f, 1.0f, 12, glm::vec3(1.0f)),
glm::vec3(0, 2.6f, 0));
flame_inner->SetEmissive(glm::vec3(8.0f, 3.5f, 1.2f));
AddGameObject(flame_inner);
// --- Ring of stone pillars ---
for (int i = 0; i < 8; ++i) {
float ang = (float)i / 8 * 2 * M_PI;
float R = 7.5f;
float h = 4.0f + jit(0.5f);
auto pillar = CreateGameObject(
CreateCylinder(0.5f, h, 12,
glm::vec3(0.88f, 0.85f, 0.82f)),
glm::vec3(std::cos(ang) * R, h * 0.5f, std::sin(ang) * R));
pillar->SetTexture(marble_tex);
pillar->SetNormalMap(noise_tex);
pillar->SetRoughness(0.55f);
AddGameObject(pillar);
}
// --- PBR showcase pieces ---
// Polished chrome obelisk
addBox("obelisk",
glm::vec3(-7.0f, 3.0f, -2.0f), glm::vec3(0.8f, 6.0f, 0.8f),
glm::vec3(0.95f, 0.95f, 0.95f), 0, 0,
1.0f, 0.15f, -1.0f,
0.0f, 0.0f, glm::vec3(0, 15.0f, 0));
// Copper mirror-ish orb
addSphere("copper_orb",
glm::vec3(8.0f, 1.3f, -2.5f), 1.3f,
glm::vec3(0.95f, 0.55f, 0.28f), 0, 0,
1.0f, 0.2f,
0.0f); // static visual
// ============================================================
// PHYSICS PLAYGROUND SECTION — east half of the map (X >= +5).
// Self-contained chain reaction + bonus props, kept clear of the
// forge (origin) and the southern pool.
// ============================================================
// Static launch ramp. Tilted -18° around Z so the +X end (where the
// ball spawns) is the LOW end and the -X end is the HIGH end. Ball
// gets a strong -X kick at t=2s, slides off the high end, falls onto
// the floor and continues into the domino chain.
addBox("ramp",
glm::vec3(28.0f, 1.9f, 0.0f), glm::vec3(7.0f, 0.3f, 3.0f),
glm::vec3(0.9f, 0.85f, 0.78f),
wood_tex, noise_tex, 0.0f, 0.7f, -1.0f,
0.0f, 0.0f, glm::vec3(0, 0, -18.0f));
// Heavy metal impulse ball, sitting near the +X (low) end of the ramp.
addSphere("impulse_ball",
glm::vec3(30.0f, 4.2f, 0.0f), 0.9f,
glm::vec3(0.75f, 0.78f, 0.82f), 0, 0,
1.0f, 0.3f,
8.0f, 0.3f, 0.25f);
// Primary domino chain — 12 boards along z=0, between ramp and forge.
// Spacing 1.5 < domino height 2.0, so each falling domino's tip
// overlaps the next domino's base — the chain propagates reliably.
for (int i = 0; i < 12; ++i) {
float x = 22.0f - i * 1.5f;
addBox("dom_" + std::to_string(i),
glm::vec3(x, 1.0f, 0.0f), glm::vec3(0.25f, 2.0f, 1.2f),
glm::vec3(0.9f, 0.85f, 0.7f),
wood_tex, noise_tex, 0.0f, 0.6f,
0.6f, 0.05f, 0.7f);
}
// Secondary domino chain, parallel to the first at z = +6.
for (int i = 0; i < 12; ++i) {
float x = 22.0f - i * 1.5f;
addBox("dom2_" + std::to_string(i),
glm::vec3(x, 1.0f, 6.0f), glm::vec3(0.25f, 2.0f, 1.1f),
glm::vec3(0.9f, 0.82f, 0.65f),
wood_tex, noise_tex, 0.0f, 0.65f,
0.6f, 0.05f, 0.7f);
}
// Crate pyramid, north end of physics section.
for (int y = 0; y < 3; ++y) {
int n = 4 - y;
for (int i = 0; i < n; ++i) {
float x = 14.0f + i * 1.1f + (3 - n) * 0.5f;
float yp = 0.5f + y * 1.0f;
addBox("crate_" + std::to_string(y * 4 + i),
glm::vec3(x, yp, 14.0f), glm::vec3(1.0f),
glm::vec3(0.85f, 0.75f, 0.55f),
wood_tex, noise_tex, 0.0f, 0.7f,
0.8f, 0.1f, 0.7f);
}
}
// Second crate pyramid, mid-section.
for (int y = 0; y < 3; ++y) {
int n = 4 - y;
for (int i = 0; i < n; ++i) {
float x = 26.0f + i * 1.1f + (3 - n) * 0.5f;
float yp = 0.5f + y * 1.0f;
addBox("crate2_" + std::to_string(y * 4 + i),
glm::vec3(x, yp, 14.0f), glm::vec3(1.0f),
glm::vec3(0.85f, 0.75f, 0.55f),
wood_tex, noise_tex, 0.0f, 0.7f,
0.8f, 0.1f, 0.7f);
}
}
// Concrete crate wall, eastern edge of section.
for (int i = 0; i < 12; ++i) {
int col = i % 4, row = i / 4;
float x = 32.0f + col * 1.1f;
float y = 0.5f + row * 1.1f;
addBox("wallc_" + std::to_string(i),
glm::vec3(x, y, 22.0f), glm::vec3(1.0f),
glm::vec3(0.65f, 0.78f, 0.85f),
concrete, noise_tex, 0.0f, 0.75f,
0.6f, 0.2f, 0.6f);
}
// Bowling-pin triangle, set up to receive whatever falls past the
// domino chain.
static const float pin_offsets[10][2] = {
{ 0.0f, 0.0f},
{-0.45f, 0.8f}, { 0.45f, 0.8f},
{-0.9f, 1.6f}, { 0.0f, 1.6f}, { 0.9f, 1.6f},
{-1.35f, 2.4f}, {-0.45f, 2.4f}, { 0.45f, 2.4f}, { 1.35f, 2.4f}
};
for (int i = 0; i < 10; ++i) {
float px = 14.0f - pin_offsets[i][1];
float pz = -8.0f + pin_offsets[i][0];
auto pin = CreateGameObject(
CreateCylinder(0.35f, 1.6f, 14, glm::vec3(0.98f, 0.96f, 0.92f)),
glm::vec3(px, 0.8f, pz));
pin->SetMetallic(0.0f);
pin->SetRoughness(0.35f);
AddGameObject(pin);
std::string id = "pin_" + std::to_string(i);
namedObjects_[id] = pin;
bodyCmd(id, "box", 0.5f, 0.2f, 0.4f);
initial_states_.push_back({id, "box",
glm::vec3(px, 0.8f, pz), glm::vec3(0), 0.5f, 0.2f, 0.4f});
}
// Cube tower, southeast corner of section.
for (int i = 0; i < 8; ++i) {
float y = 0.6f + i * 1.2f;
addBox("tower_" + std::to_string(i),
glm::vec3(30.0f, y, -18.0f), glm::vec3(1.2f),
glm::vec3(0.85f, 0.80f, 0.72f),
marble_tex, noise_tex, 0.0f, 0.5f,
0.9f, 0.12f, 0.55f,
glm::vec3(0, i * 7.0f, 0));
}
// Scattered loose objects across the eastern half (avoiding the pool
// and the forge).
for (int i = 0; i < 16; ++i) {
float x = rnd(8.0f, 36.0f);
float z = rnd(-30.0f, 30.0f);
// Skip cells that overlap key set-pieces.
if (z > -2.0f && z < 8.0f && x > 6.0f && x < 24.0f) continue;
bool is_sphere = (i % 2) == 0;
std::string id = "loose_" + std::to_string(i);
if (is_sphere) {
addSphere(id,
glm::vec3(x, 1.0f, z), rnd(0.4f, 0.8f),
glm::vec3(rnd(0.4f, 0.95f), rnd(0.4f, 0.95f), rnd(0.4f, 0.95f)),
rock_tex, noise_tex,
rnd(0.0f, 0.9f), rnd(0.2f, 0.9f),
0.6f, 0.5f, 0.3f);
} else {
addBox(id,
glm::vec3(x, 1.0f, z), glm::vec3(rnd(0.6f, 1.1f)),
glm::vec3(rnd(0.5f, 0.95f), rnd(0.5f, 0.95f), rnd(0.5f, 0.95f)),
wood_tex, noise_tex,
0.0f, 0.65f,
0.7f, 0.15f, 0.55f,
glm::vec3(jit(15), jit(180), jit(15)));
}
}
// --- Distant mountain silhouettes ---
for (int i = 0; i < 13; ++i) {
float x = -175.0f + i * 28.0f + jit(6.0f);
float z = -170.0f + jit(12.0f);
float s = rnd(0.9f, 1.5f);
float h = 22.0f * s + rnd(0, 6);
float r = 14.0f * s;
auto m = CreateGameObject(
CreateCone(r, h, 10, glm::vec3(0.16f, 0.13f, 0.16f)),
glm::vec3(x, h * 0.5f, z));
m->SetRoughness(0.95f);
AddGameObject(m);
}
// --- Forest (trees around perimeter, avoiding focal area) ---
auto tree = [&](glm::vec3 at, float s) {
auto trunk = CreateGameObject(
CreateCylinder(0.24f * s, 2.7f * s, 10,
glm::vec3(0.92f, 0.88f, 0.82f)),
at + glm::vec3(0, 1.35f * s, 0));
trunk->SetRotation(glm::vec3(jit(4), jit(360), jit(4)));
trunk->SetTexture(wood_tex);
trunk->SetNormalMap(noise_tex);
trunk->SetRoughness(0.85f);
AddGameObject(trunk);
int layers = 1 + (int)(rng() % 3);
for (int k = 0; k < layers; ++k) {
float lh = 2.6f * s * (1.0f - k * 0.22f);
float lr = 1.3f * s * (1.0f - k * 0.13f);
glm::vec3 cc(rnd(0.10f, 0.22f), rnd(0.36f, 0.52f),
rnd(0.12f, 0.22f));
auto cone = CreateGameObject(
CreateCone(lr, lh, 10, cc),
at + glm::vec3(jit(0.15f), 3.0f * s + k * 1.0f * s, jit(0.15f)));
cone->SetNormalMap(noise_tex);
cone->SetRoughness(0.8f);
AddGameObject(cone);
}
};
for (int i = 0; i < 40; ++i) {
// Scatter around a ring, skipping the focal cone
float x, z;
do {
x = rnd(-80.0f, 80.0f);
z = rnd(-80.0f, 40.0f);
} while (std::abs(x) < 18.0f && z > -15.0f && z < 25.0f); // clearing
tree(glm::vec3(x, 0, z), rnd(0.7f, 1.4f));
}
// --- Boulder scatter ---
for (int i = 0; i < 35; ++i) {
float x, z;
do {
x = rnd(-70.0f, 70.0f);
z = rnd(-25.0f, 35.0f);
} while (std::abs(x) < 10.0f && std::abs(z) < 10.0f); // no clutter on forge
float s = rnd(0.4f, 1.3f);
auto rock = CreateGameObject(
CreateSphere(14, glm::vec3(0.8f, 0.77f, 0.74f)),
glm::vec3(x, s * 0.35f, z));
rock->SetScale(glm::vec3(s, s * rnd(0.5f, 0.8f), s * rnd(0.8f, 1.1f)));
rock->SetRotation(glm::vec3(jit(15), jit(180), jit(15)));
rock->SetTexture(rock_tex);
rock->SetNormalMap(noise_tex);
rock->SetRoughness(0.9f);
AddGameObject(rock);
}
// --- Reeds along shore ---
for (int i = 0; i < 70; ++i) {
float x = rnd(-45.0f, 45.0f);
float z = rnd(-20.0f, -8.0f);
float h = rnd(0.8f, 1.7f);
auto reed = CreateGameObject(
CreateCylinder(0.04f, h, 4,
glm::vec3(rnd(0.5f, 0.65f), rnd(0.45f, 0.55f),
rnd(0.2f, 0.3f))),
glm::vec3(x, h * 0.5f, z));
reed->SetRotation(glm::vec3(jit(12), 0, jit(12)));
reed->SetRoughness(0.8f);
AddGameObject(reed);
}
// --- Wildflowers (HDR-saturated for bloom) ---
for (int i = 0; i < 120; ++i) {
float x = rnd(-45.0f, 45.0f);
float z = rnd(-5.0f, 30.0f);
if (std::abs(x) < 6.0f && std::abs(z) < 6.0f) continue; // keep forge clear
glm::vec3 col;
int pick = (int)(rng() % 5);
if (pick == 0) col = glm::vec3(1.6f, 1.2f, 0.3f);
else if (pick == 1) col = glm::vec3(1.5f, 0.4f, 0.8f);
else if (pick == 2) col = glm::vec3(1.3f, 1.25f, 1.2f);
else if (pick == 3) col = glm::vec3(0.8f, 0.5f, 1.5f);
else col = glm::vec3(1.6f, 0.6f, 0.2f);
auto fl = CreateGameObject(CreateSphere(8, col),
glm::vec3(x, 0.2f + jit(0.05f), z));
fl->SetScale(glm::vec3(0.09f));
fl->SetEmissive(col * 0.3f);
AddGameObject(fl);
}
// Enable physics
processAgentCommand(json::parse(R"({"action":"physics","enabled":true})"));
// --- Camera pose ---
glm::vec3 eye(18.0f, 5.5f, 18.0f);
glm::vec3 target(2.0f, 2.0f, -8.0f);
SetCameraPosition(eye);
SetCameraTarget(target);
glm::vec3 fwd = glm::normalize(target - eye);
cam_pitch_ = glm::degrees(std::asin(fwd.y));
cam_yaw_ = glm::degrees(std::atan2(fwd.z, fwd.x));
last_cam_pos_ = eye;
std::cout << "Sanctuary demo ready: " << game_objects_.size()
<< " objects. Chain reaction fires at t=2s.\n"
<< "Camera: WASD/arrows, Space/LCtrl = up/down, LShift = sprint.\n"
<< "SHOOT: hold F, click LMB, or hold R1 (gamepad) to fire balls.\n"
<< "RESET: press R or Triangle (gamepad) to restart the demo.\n";
return true;
}
void OnUpdate(float dt) override {
time_elapsed_ += dt;
// Kick off the chain reaction 2 seconds in, once physics is warm.
// Heavy -X impulse sends the ball off the ramp and into the first
// domino at x=22.
if (!kickoff_fired_ && time_elapsed_ > 2.0f) {
kickoff_fired_ = true;
json::Object c;
c["action"] = json::Value(std::string("impulse"));
c["id"] = json::Value(std::string("impulse_ball"));
c["impulse"] = json::Value(json::Array{
json::Value(-260.0),
json::Value(0.0),
json::Value(0.0)});
processAgentCommand(json::Value(c));
}
// Reset (R / Triangle) — restore every dynamic prop to its initial
// pose with zero velocity, clear shot balls, and re-arm the kickoff.
if (Input::IsKeyPressed(GLFW_KEY_R)
|| Input::IsGamepadButtonPressed(Input::GamepadButton::Triangle)) {
ResetScene();
}
// Shoot balls from camera: F / LMB / R1 — rate-limited
shot_cooldown_ -= dt;
if (shot_cooldown_ <= 0.0f) {
bool fire = false;
if (Input::IsKeyHeld(GLFW_KEY_F))
fire = true;
if (Input::IsMouseButtonPressed(Input::MouseButton::Left))
fire = true;
if (Input::IsGamepadButtonDown(Input::GamepadButton::R1))
fire = true;
if (fire) {
FireBall();
shot_cooldown_ = 0.16f; // ~6 shots/sec
}
}
// --- Free-fly camera ---
glm::vec3 eng_pos = GetCameraPosition();
if (glm::length(eng_pos - last_cam_pos_) > 0.0001f) {
glm::vec3 fwd = GetCameraTarget() - eng_pos;
if (glm::length(fwd) > 0.0001f) {
fwd = glm::normalize(fwd);
cam_pitch_ = glm::degrees(std::asin(fwd.y));
cam_yaw_ = glm::degrees(std::atan2(fwd.z, fwd.x));
}
}
glm::vec2 ls{0}, rs{0};
float up = 0, down = 0, speed = cam_speed_;
bool touched = false;
if (Input::IsGamepadConnected()) {
ls = Input::GetLeftStick();
rs = Input::GetRightStick();
up = Input::GetR2();
down = Input::GetL2();
if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross)) speed *= 3.0f;
if (glm::length(ls) + glm::length(rs) + up + down > 0.01f) touched = true;
}
if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT)) speed *= 3.0f;
if (Input::IsKeyHeld(GLFW_KEY_W)) { ls.y -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_S)) { ls.y += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_A)) { ls.x -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_D)) { ls.x += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_LEFT)) { rs.x -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) { rs.x += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_UP)) { rs.y -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_DOWN)) { rs.y += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_SPACE)) { up += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL)) { down += 1.0f; touched = true; }
if (!touched) { last_cam_pos_ = eng_pos; return; }
cam_yaw_ += rs.x * look_sensitivity_ * dt;
cam_pitch_ -= rs.y * look_sensitivity_ * dt;
if (cam_pitch_ > 89.0f) cam_pitch_ = 89.0f;
if (cam_pitch_ < -89.0f) cam_pitch_ = -89.0f;
glm::vec3 fwd;
fwd.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
fwd.y = std::sin(glm::radians(cam_pitch_));
fwd.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
fwd = glm::normalize(fwd);
glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
glm::vec3 pos = eng_pos;
pos += fwd * (-ls.y) * speed * dt;
pos += right * ls.x * speed * dt;
pos.y += (up - down) * speed * dt;
SetCameraPosition(pos);
SetCameraTarget(pos + fwd);
last_cam_pos_ = pos;
}
};
void SanctuaryDemo::ResetScene() {
// Restore each captured dynamic body to its initial pose with zero
// velocity. Re-issuing the "body" command rebuilds the physics body at
// the GameObject's current position (so we set that first) with linear
// and angular velocity = 0.
for (const auto& s : initial_states_) {
auto it = namedObjects_.find(s.id);
if (it == namedObjects_.end()) continue;
it->second->SetPosition(s.position);
it->second->SetRotation(s.rotation);
json::Object c;
c["action"] = json::Value(std::string("body"));
c["id"] = json::Value(s.id);
c["shape"] = json::Value(s.shape);
c["mass"] = json::Value((double)s.mass);
c["restitution"] = json::Value((double)s.restitution);
c["friction"] = json::Value((double)s.friction);
processAgentCommand(json::Value(c));
}
// Despawn any in-flight player-fired balls.
for (const auto& id : active_shots_) {
json::Object dc;
dc["action"] = json::Value(std::string("delete"));
dc["id"] = json::Value(id);
processAgentCommand(json::Value(dc));
}
active_shots_.clear();
// Re-arm the chain-reaction kickoff.
time_elapsed_ = 0.0f;
kickoff_fired_ = false;
}
// ---------------------------------------------
// SunsetForestDemo: a composed, cinematic scene built to show off the whole
// rendering pipeline — HDR/bloom/tone mapping, shadows, Gerstner water,
// atmospheric fog, sun at a specific time of day. Pure C++ placement, no AI.
// ---------------------------------------------
class SunsetForestDemo : public Engine {
float cam_yaw_ = 225.0f;
float cam_pitch_ = -12.0f;
float cam_speed_ = 20.0f;
float look_sensitivity_ = 80.0f;
glm::vec3 last_cam_pos_{0};
public:
SunsetForestDemo()
: Engine(1920, 1080, "ZoomEngine - Sunset Forest (HDR)") {}
bool OnInitialize() override {
std::mt19937 rng(42);
auto rand = [&](float lo, float hi) {
std::uniform_real_distribution<float> d(lo, hi);
return d(rng);
};
auto jitter = [&](float scale) { return rand(-scale, scale); };
// --- Sunset palette + atmosphere + HDR tuning ---
processAgentCommand(json::parse(R"({
"action":"sun",
"direction":[-0.75, 0.18, -0.15],
"color":[2.6, 1.40, 0.80],
"zenith":[0.14, 0.20, 0.45],
"horizon":[1.05, 0.55, 0.28],
"sky":[0.75, 0.45, 0.28],
"ground":[0.05, 0.05, 0.07],
"shadow_extent":120,
"exposure":0.9,
"bloom_threshold":1.15,
"bloom_intensity":1.1,
"fog_density":0.012
})"));
// --- Ground (grass, textured, PBR) ---
auto grass = CreateGameObject(
CreatePlane(400.0f, 400.0f, glm::vec3(0.9f, 0.95f, 0.9f)),
glm::vec3(0, 0, 0));
grass->SetTexture(TextureCache::Get("grass"));
grass->SetNormalMap(TextureCache::Get("noise")); // fake micro-relief
grass->SetUVScale(glm::vec2(0.3f));
grass->SetMetallic(0.0f);
grass->SetRoughness(0.95f);
AddGameObject(grass);
// --- Lake + light breeze ---
processAgentCommand(json::parse(R"({
"action":"water",
"enabled":true,
"level":-0.55,
"size":420,
"amplitude":0.14,
"wavelength":14,
"speed":0.7,
"shallow":[0.55, 0.42, 0.30],
"deep":[0.03, 0.07, 0.13],
"resolution":180
})"));
processAgentCommand(json::parse(R"({
"action":"weather",
"wind_dir":[0.8, 0, 0.6],
"wind_speed":1.0,
"storminess":0.1
})"));
// Distant mountain silhouettes — broad cones along the horizon
for (int i = 0; i < 11; ++i) {
float x = -160.0f + i * 32.0f + jitter(6.0f);
float z = -170.0f + jitter(10.0f);
float s = rand(0.9f, 1.5f);
float h = 22.0f * s + rand(0.0f, 6.0f);
float r = 14.0f * s;
auto m = CreateGameObject(
CreateCone(r, h, 10, glm::vec3(0.17f, 0.13f, 0.16f)),
glm::vec3(x, h * 0.5f, z));
AddGameObject(m);
}
// Second ridge: smaller cones in front of the big ones, lighter tone
for (int i = 0; i < 14; ++i) {
float x = -140.0f + i * 20.0f + jitter(4.0f);
float z = -130.0f + jitter(8.0f);
float h = 10.0f + jitter(3.0f);
float r = 7.0f + jitter(1.5f);
auto m = CreateGameObject(
CreateCone(r, h, 9, glm::vec3(0.22f, 0.18f, 0.20f)),
glm::vec3(x, h * 0.5f, z));
AddGameObject(m);
}
GLuint wood_tex = TextureCache::Get("wood");
GLuint rock_tex = TextureCache::Get("rock");
GLuint noise_tex = TextureCache::Get("noise");
// Tree helper: trunk + 1-3 stacked canopy cones
auto tree = [&](glm::vec3 pos, float s, glm::vec3 canopyCol) {
auto trunk = CreateGameObject(
CreateCylinder(0.22f * s, 2.6f * s, 8,
glm::vec3(0.9f, 0.85f, 0.8f)),
pos + glm::vec3(0, 1.3f * s, 0));
trunk->SetRotation(glm::vec3(jitter(4.0f), rand(0.0f, 360.0f), jitter(4.0f)));
trunk->SetTexture(wood_tex);
trunk->SetNormalMap(noise_tex);
trunk->SetMetallic(0.0f);
trunk->SetRoughness(0.85f);
AddGameObject(trunk);
int layers = 1 + (int)(rng() % 3);
for (int i = 0; i < layers; ++i) {
float lh = 2.4f * s * (1.0f - i * 0.22f);
float lr = 1.25f * s * (1.0f - i * 0.14f);
auto cone = CreateGameObject(
CreateCone(lr, lh, 10, canopyCol * (0.85f + i * 0.07f)),
pos + glm::vec3(jitter(0.2f),
2.7f * s + i * 1.1f * s,
jitter(0.2f)));
cone->SetNormalMap(noise_tex);
cone->SetRoughness(0.8f);
AddGameObject(cone);
}
};
// Ridge/foreground trees (near camera, where ground is above lake level)
for (int i = 0; i < 30; ++i) {
float x = rand(-70.0f, 70.0f);
float z = rand(5.0f, 45.0f);
// Keep clear corridor near the camera focal area
if (std::abs(x + 8.0f) < 5.0f && z < 18.0f) continue;
float s = rand(0.9f, 1.6f);
glm::vec3 col(rand(0.11f, 0.22f), rand(0.34f, 0.50f), rand(0.13f, 0.22f));
tree(glm::vec3(x, 0, z), s, col);
}
// Far shore trees (past the lake)
for (int i = 0; i < 45; ++i) {
float x = rand(-120.0f, 120.0f);
float z = rand(-95.0f, -45.0f);
float s = rand(0.55f, 1.0f);
glm::vec3 col(rand(0.07f, 0.14f), rand(0.22f, 0.35f), rand(0.09f, 0.16f));
tree(glm::vec3(x, 0, z), s, col);
}
// Boulders along the shore and scattered across the ridge
for (int i = 0; i < 45; ++i) {
float x = rand(-70.0f, 70.0f);
float z = rand(-12.0f, 32.0f);
float s = rand(0.4f, 1.4f);
auto rock = CreateGameObject(
CreateSphere(16,
glm::vec3(rand(0.75f, 0.95f),
rand(0.72f, 0.92f),
rand(0.70f, 0.90f))),
glm::vec3(x, s * 0.35f, z));
rock->SetScale(glm::vec3(s, s * rand(0.5f, 0.8f), s * rand(0.8f, 1.1f)));
rock->SetRotation(glm::vec3(jitter(15.0f), jitter(180.0f), jitter(15.0f)));
rock->SetTexture(rock_tex);
rock->SetNormalMap(noise_tex);
rock->SetMetallic(0.0f);
rock->SetRoughness(0.9f);
AddGameObject(rock);
}
// Reeds along the shoreline (near the lake-grass boundary)
for (int i = 0; i < 80; ++i) {
float x = rand(-50.0f, 50.0f);
// Tight zone on the water side of the shore
float z = rand(-20.0f, -6.0f);
float h = rand(0.7f, 1.6f);
auto reed = CreateGameObject(
CreateCylinder(0.045f, h, 4,
glm::vec3(rand(0.45f, 0.60f),
rand(0.40f, 0.55f),
rand(0.18f, 0.28f))),
glm::vec3(x, h * 0.5f, z));
reed->SetRotation(glm::vec3(jitter(15.0f), 0, jitter(15.0f)));
AddGameObject(reed);
}
// Wildflowers: tiny emissive-ish spheres — boost colors so they pop in HDR
for (int i = 0; i < 100; ++i) {
float x = rand(-50.0f, 50.0f);
float z = rand(4.0f, 38.0f);
glm::vec3 col;
int pick = (int)(rng() % 5);
if (pick == 0) col = glm::vec3(1.40f, 1.10f, 0.30f); // yellow
else if (pick == 1) col = glm::vec3(1.30f, 0.40f, 0.75f); // pink
else if (pick == 2) col = glm::vec3(1.20f, 1.15f, 1.10f); // white
else if (pick == 3) col = glm::vec3(0.70f, 0.45f, 1.30f); // purple
else col = glm::vec3(1.40f, 0.55f, 0.20f); // orange
auto fl = CreateGameObject(CreateSphere(6, col),
glm::vec3(x, 0.18f + jitter(0.05f), z));
fl->SetScale(glm::vec3(0.09f));
AddGameObject(fl);
}
// Fallen log — story beat in foreground
{
auto log = CreateGameObject(
CreateCylinder(0.55f, 6.5f, 10, glm::vec3(0.92f, 0.88f, 0.82f)),
glm::vec3(6.0f, 0.55f, 13.0f));
log->SetRotation(glm::vec3(0, 15.0f, 90.0f));
log->SetTexture(wood_tex);
log->SetNormalMap(noise_tex);
log->SetRoughness(0.8f);
AddGameObject(log);
}
// Polished metal obelisk — PBR showcase (high metallic, low roughness)
{
auto obelisk = CreateGameObject(
CreateCube(glm::vec3(0.92f, 0.88f, 0.78f)),
glm::vec3(-8.0f, 3.0f, -4.0f));
obelisk->SetScale(glm::vec3(0.7f, 4.5f, 0.7f));
obelisk->SetRotation(glm::vec3(0, 18.0f, 0));
obelisk->SetMetallic(1.0f);
obelisk->SetRoughness(0.25f);
AddGameObject(obelisk);
// Brushed copper sphere — another PBR reference
auto orb = CreateGameObject(
CreateSphere(64, glm::vec3(0.95f, 0.55f, 0.28f)),
glm::vec3(10.0f, 1.2f, -1.0f));
orb->SetScale(glm::vec3(1.2f));
orb->SetMetallic(1.0f);
orb->SetRoughness(0.35f);
AddGameObject(orb);
}
// Campfire — bright HDR colors for the flame to bloom
{
glm::vec3 fire_pos(0.0f, 0, 12.0f);
// Two crossed logs
auto a = CreateGameObject(
CreateCylinder(0.16f, 1.3f, 6, glm::vec3(0.20f, 0.12f, 0.06f)),
fire_pos + glm::vec3(0, 0.16f, 0));
a->SetRotation(glm::vec3(0, 30.0f, 90.0f));
AddGameObject(a);
auto b = CreateGameObject(
CreateCylinder(0.16f, 1.3f, 6, glm::vec3(0.22f, 0.14f, 0.07f)),
fire_pos + glm::vec3(0, 0.16f, 0));
b->SetRotation(glm::vec3(0, -30.0f, 90.0f));
AddGameObject(b);
// Outer flame (dimmer, larger) — emissive so it glows via bloom
auto f1 = CreateGameObject(
CreateCone(0.45f, 1.3f, 10, glm::vec3(1.0f)),
fire_pos + glm::vec3(0, 0.75f, 0));
f1->SetEmissive(glm::vec3(4.5f, 1.6f, 0.35f));
AddGameObject(f1);
auto f2 = CreateGameObject(
CreateCone(0.22f, 0.8f, 10, glm::vec3(1.0f)),
fire_pos + glm::vec3(0, 0.90f, 0));
f2->SetEmissive(glm::vec3(7.0f, 3.0f, 0.9f));
AddGameObject(f2);
// Ring of stones
for (int i = 0; i < 9; ++i) {
float a_rad = i * 0.698f;
glm::vec3 p = fire_pos + glm::vec3(std::cos(a_rad) * 0.85f,
0.08f,
std::sin(a_rad) * 0.85f);
auto r = CreateGameObject(
CreateSphere(8, glm::vec3(0.28f, 0.26f, 0.24f)), p);
r->SetScale(glm::vec3(0.28f, 0.18f, 0.28f));
AddGameObject(r);
}
}
// Camera: cinematic three-quarter, looking across ridge -> lake -> mountains
glm::vec3 eye(20.0f, 7.5f, 28.0f);
glm::vec3 target(-4.0f, 1.0f, -25.0f);
SetCameraPosition(eye);
SetCameraTarget(target);
glm::vec3 fwd0 = glm::normalize(target - eye);
cam_pitch_ = glm::degrees(std::asin(fwd0.y));
cam_yaw_ = glm::degrees(std::atan2(fwd0.z, fwd0.x));
last_cam_pos_ = eye;
std::cout << "Sunset Forest ready: " << game_objects_.size()
<< " objects.\n"
<< "Controls: WASD/arrows or gamepad, Space/R2 up, "
"LCtrl/L2 down, LShift/Cross sprint.\n";
return true;
}
void OnUpdate(float dt) override {
// Free-fly camera (copied from GrassPoolsDemo for standalone mode).
glm::vec3 eng_pos = GetCameraPosition();
if (glm::length(eng_pos - last_cam_pos_) > 0.0001f) {
glm::vec3 fwd = GetCameraTarget() - eng_pos;
if (glm::length(fwd) > 0.0001f) {
fwd = glm::normalize(fwd);
cam_pitch_ = glm::degrees(std::asin(fwd.y));
cam_yaw_ = glm::degrees(std::atan2(fwd.z, fwd.x));
}
}
glm::vec2 ls{0}, rs{0};
float up = 0, down = 0, speed = cam_speed_;
bool touched = false;
if (Input::IsGamepadConnected()) {
ls = Input::GetLeftStick();
rs = Input::GetRightStick();
up = Input::GetR2();
down = Input::GetL2();
if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross)) speed *= 3.0f;
if (glm::length(ls) + glm::length(rs) + up + down > 0.01f) touched = true;
}
if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT)) speed *= 3.0f;
if (Input::IsKeyHeld(GLFW_KEY_W)) { ls.y -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_S)) { ls.y += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_A)) { ls.x -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_D)) { ls.x += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_LEFT)) { rs.x -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) { rs.x += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_UP)) { rs.y -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_DOWN)) { rs.y += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_SPACE)) { up += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL)) { down += 1.0f; touched = true; }
if (!touched) { last_cam_pos_ = eng_pos; return; }
cam_yaw_ += rs.x * look_sensitivity_ * dt;
cam_pitch_ -= rs.y * look_sensitivity_ * dt;
if (cam_pitch_ > 89.0f) cam_pitch_ = 89.0f;
if (cam_pitch_ < -89.0f) cam_pitch_ = -89.0f;
glm::vec3 fwd;
fwd.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
fwd.y = std::sin(glm::radians(cam_pitch_));
fwd.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
fwd = glm::normalize(fwd);
glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
glm::vec3 pos = eng_pos;
pos += fwd * (-ls.y) * speed * dt;
pos += right * ls.x * speed * dt;
pos.y += (up - down) * speed * dt;
SetCameraPosition(pos);
SetCameraTarget(pos + fwd);
last_cam_pos_ = pos;
}
};
// ---------------------------------------------
// GrassPoolsDemo: standalone showcase of shadows + grass ground + water + pools.
// No AI driving — everything set up in C++ from OnInitialize.
// ---------------------------------------------
class GrassPoolsDemo : public Engine {
float cam_yaw_ = 215.0f;
float cam_pitch_ = -15.0f;
float cam_speed_ = 15.0f;
float look_sensitivity_ = 80.0f;
glm::vec3 last_cam_pos_{0};
public:
GrassPoolsDemo()
: Engine(1920, 1080, "ZoomEngine - Grass / Pools / Shadows") {}
bool OnInitialize() override {
std::mt19937 rng(7);
auto rand = [&](float lo, float hi) {
std::uniform_real_distribution<float> d(lo, hi);
return d(rng);
};
// Golden-hour sun via the engine's existing state.
processAgentCommand(json::parse(R"({
"action":"sun",
"direction":[-0.55, 0.32, -0.20],
"color":[1.55, 1.15, 0.78],
"zenith":[0.20, 0.40, 0.72],
"horizon":[0.95, 0.70, 0.50],
"sky":[0.80, 0.62, 0.42],
"ground":[0.08, 0.07, 0.05],
"shadow_extent":80
})"));
// Grass ground — textured
auto grass = CreateGameObject(
CreatePlane(110.0f, 110.0f, glm::vec3(0.95f, 0.95f, 0.9f)),
glm::vec3(0, 0, 0));
grass->SetTexture(TextureCache::Get("grass"));
grass->SetUVScale(glm::vec2(0.25f));
AddGameObject(grass);
// Ocean around the grass: mesh is large, level just below the grass
// so the horizon line reads as a shore.
processAgentCommand(json::parse(R"({
"action":"water",
"enabled":true,
"level":-0.25,
"size":260,
"amplitude":0.35,
"wavelength":11,
"speed":1.0,
"shallow":[0.30, 0.58, 0.62],
"deep":[0.02, 0.11, 0.20],
"resolution":160
})"));
// A light breeze so the ocean moves but the scene stays calm.
processAgentCommand(json::parse(R"({
"action":"weather",
"wind_dir":[1, 0, 0.4],
"wind_speed":1.0,
"storminess":0.1
})"));
// Tiny pools — small translucent teal planes sitting on the grass.
// Slight Y offset prevents z-fighting; size <2m suggests puddles.
struct Pool { glm::vec3 pos; float size; };
const std::vector<Pool> pools = {
{ glm::vec3( 6.0f, 0.02f, 3.0f), 2.4f },
{ glm::vec3(-4.0f, 0.02f, -7.0f), 1.6f },
{ glm::vec3( 1.5f, 0.02f, 10.0f), 1.9f },
{ glm::vec3(-9.0f, 0.02f, 5.5f), 1.2f },
};
for (const auto& p : pools) {
// Use a color that reads as "shallow reflective water"
auto pool = CreateGameObject(
CreatePlane(p.size, p.size * 0.7f, glm::vec3(0.35f, 0.58f, 0.65f)),
p.pos);
// Slight random yaw to stop them looking like a grid
pool->SetRotation(glm::vec3(0, rand(0.0f, 360.0f), 0));
AddGameObject(pool);
}
// Shadow casters: 3 standing "monoliths" and a scatter of boulders.
auto monolith = [&](glm::vec3 pos, glm::vec3 scale, glm::vec3 color, float yaw) {
auto obj = CreateGameObject(CreateCube(color), pos);
obj->SetScale(scale);
obj->SetRotation(glm::vec3(0, yaw, 0));
AddGameObject(obj);
};
GLuint rock_tex = TextureCache::Get("rock");
GLuint wood_tex = TextureCache::Get("wood");
auto monolith2 = [&](glm::vec3 pos, glm::vec3 scale, float yaw) {
auto obj = CreateGameObject(CreateCube(glm::vec3(0.9f, 0.88f, 0.85f)), pos);
obj->SetScale(scale);
obj->SetRotation(glm::vec3(0, yaw, 0));
obj->SetTexture(rock_tex);
AddGameObject(obj);
};
monolith2(glm::vec3( 0.0f, 2.5f, 0.0f), glm::vec3(1.2f, 5.0f, 1.2f), 12.0f);
monolith2(glm::vec3( 4.5f, 1.8f, -6.0f), glm::vec3(0.9f, 3.6f, 0.9f), -22.0f);
monolith2(glm::vec3(-7.0f, 2.1f, -2.0f), glm::vec3(1.1f, 4.2f, 1.1f), 48.0f);
monolith2(glm::vec3( 8.5f, 1.4f, 7.5f), glm::vec3(0.8f, 2.8f, 0.8f), -8.0f);
(void)monolith; (void)wood_tex;
auto boulder = [&](glm::vec3 pos, float s, glm::vec3 color) {
(void)color;
auto obj = CreateGameObject(CreateSphere(24, glm::vec3(0.85f, 0.82f, 0.78f)), pos);
obj->SetScale(glm::vec3(s, s * 0.75f, s));
obj->SetTexture(rock_tex);
AddGameObject(obj);
};
boulder(glm::vec3(-3.0f, 0.6f, 4.0f), 1.2f, glm::vec3(0.45f, 0.42f, 0.38f));
boulder(glm::vec3( 5.0f, 0.45f, -2.0f), 0.9f, glm::vec3(0.42f, 0.40f, 0.36f));
boulder(glm::vec3( 2.5f, 0.7f, 12.0f), 1.4f, glm::vec3(0.50f, 0.46f, 0.42f));
boulder(glm::vec3(-10.0f, 0.5f, 9.0f), 1.0f, glm::vec3(0.40f, 0.38f, 0.35f));
boulder(glm::vec3(12.0f, 0.4f, -4.0f), 0.8f, glm::vec3(0.48f, 0.45f, 0.40f));
// Some trees: trunk cylinder (wood texture) + cone foliage.
auto tree = [&](glm::vec3 at, float s) {
auto trunk = CreateGameObject(
CreateCylinder(0.25f * s, 2.4f * s, 10, glm::vec3(0.9f, 0.85f, 0.8f)),
at + glm::vec3(0, 1.2f * s, 0));
trunk->SetTexture(TextureCache::Get("wood"));
AddGameObject(trunk);
auto canopy = CreateGameObject(
CreateCone(1.3f * s, 2.6f * s, 12, glm::vec3(0.15f, 0.40f, 0.18f)),
at + glm::vec3(0, 3.6f * s, 0));
AddGameObject(canopy);
};
tree(glm::vec3(-14.0f, 0, -8.0f), 1.3f);
tree(glm::vec3( 15.0f, 0, -10.0f), 1.1f);
tree(glm::vec3(-16.0f, 0, 12.0f), 1.4f);
tree(glm::vec3( 18.0f, 0, 4.0f), 1.0f);
tree(glm::vec3( 9.0f, 0, -14.0f), 1.2f);
// Camera setup: three-quarter view that catches the long shadows.
glm::vec3 eye(18.0f, 7.0f, 22.0f);
SetCameraPosition(eye);
SetCameraTarget(glm::vec3(0, 1.5f, 0));
last_cam_pos_ = eye;
std::cout << "Grass/Pools/Shadows demo ready.\n"
<< "Camera: WASD = move, arrows = look, Space/LCtrl = up/down,"
" LShift = sprint.\n";
return true;
}
void OnUpdate(float dt) override {
// Same free-fly camera as GodSandbox.
glm::vec3 eng_pos = GetCameraPosition();
if (glm::length(eng_pos - last_cam_pos_) > 0.0001f) {
glm::vec3 fwd = GetCameraTarget() - eng_pos;
if (glm::length(fwd) > 0.0001f) {
fwd = glm::normalize(fwd);
cam_pitch_ = glm::degrees(std::asin(fwd.y));
cam_yaw_ = glm::degrees(std::atan2(fwd.z, fwd.x));
}
}
glm::vec2 ls{0}, rs{0};
float up = 0, down = 0, speed = cam_speed_;
bool touched = false;
if (Input::IsGamepadConnected()) {
ls = Input::GetLeftStick();
rs = Input::GetRightStick();
up = Input::GetR2();
down = Input::GetL2();
if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross)) speed *= 3.0f;
if (glm::length(ls) + glm::length(rs) + up + down > 0.01f) touched = true;
}
if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT)) speed *= 3.0f;
if (Input::IsKeyHeld(GLFW_KEY_W)) { ls.y -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_S)) { ls.y += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_A)) { ls.x -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_D)) { ls.x += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_LEFT)) { rs.x -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) { rs.x += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_UP)) { rs.y -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_DOWN)) { rs.y += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_SPACE)) { up += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL)) { down += 1.0f; touched = true; }
if (!touched) { last_cam_pos_ = eng_pos; return; }
cam_yaw_ += rs.x * look_sensitivity_ * dt;
cam_pitch_ -= rs.y * look_sensitivity_ * dt;
if (cam_pitch_ > 89.0f) cam_pitch_ = 89.0f;
if (cam_pitch_ < -89.0f) cam_pitch_ = -89.0f;
glm::vec3 fwd;
fwd.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
fwd.y = std::sin(glm::radians(cam_pitch_));
fwd.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
fwd = glm::normalize(fwd);
glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0)));
glm::vec3 pos = eng_pos;
pos += fwd * (-ls.y) * speed * dt;
pos += right * ls.x * speed * dt;
pos.y += (up - down) * speed * dt;
SetCameraPosition(pos);
SetCameraTarget(pos + fwd);
last_cam_pos_ = pos;
}
};
// ---------------------------------------------
// GodSandbox: blank canvas for the AI god to play in, plus free-fly controls
// Ground plane + grid + gentle initial camera. The AI god drives the scene
// via HTTP; the human drives the camera via gamepad or keyboard. Claude's
// camera commands stay authoritative until the user touches an input.
// ---------------------------------------------
class GodSandbox : public Engine {
float cam_yaw_ = 225.0f; // looking toward origin from [15,10,15]
float cam_pitch_ = -25.0f;
float cam_speed_ = 15.0f;
float look_sensitivity_ = 80.0f;
glm::vec3 last_cam_pos_{15, 10, 15};
std::string load_path_; // empty = blank sandbox
public:
GodSandbox()
: Engine(1920, 1080, "ZoomEngine - God Sandbox (run ai_god.py)") {}
void SetLoadPath(const std::string& p) { load_path_ = p; }
bool OnInitialize() override {
auto grid = CreateGameObject(CreateGrid(30, 1.0f, glm::vec3(0.25f, 0.25f, 0.35f)),
glm::vec3(0, 0, 0));
AddGameObject(grid);
auto plane = CreateGameObject(CreatePlane(40.0f, 40.0f, glm::vec3(0.08f, 0.09f, 0.14f)),
glm::vec3(0, -0.01f, 0));
AddGameObject(plane);
SetCameraPosition(glm::vec3(15, 10, 15));
SetCameraTarget(glm::vec3(0, 0, 0));
last_cam_pos_ = GetCameraPosition();
if (!load_path_.empty()) {
std::string result = loadWorldFromFile(load_path_);
std::cout << "Loaded world from " << load_path_
<< " -> " << result << std::endl;
}
std::cout << "God Sandbox ready. In another terminal, run:" << std::endl;
std::cout << " python ai_god.py" << std::endl;
std::cout << "Agent API listening on http://localhost:9090/api" << std::endl;
std::cout << "Camera: WASD move, arrows/right-stick look, Space/R2 up,"
<< " LShift/L2 down, LCtrl/Cross sprint." << std::endl;
return true;
}
void OnUpdate(float dt) override {
// Resync local yaw/pitch whenever the AI (or any other source) has
// moved the camera out from under us since the last frame.
glm::vec3 eng_pos = GetCameraPosition();
if (glm::length(eng_pos - last_cam_pos_) > 0.0001f) {
glm::vec3 fwd = GetCameraTarget() - eng_pos;
if (glm::length(fwd) > 0.0001f) {
fwd = glm::normalize(fwd);
cam_pitch_ = glm::degrees(std::asin(fwd.y));
cam_yaw_ = glm::degrees(std::atan2(fwd.z, fwd.x));
}
}
// Read inputs
glm::vec2 ls{0.0f}, rs{0.0f};
float up = 0.0f, down = 0.0f;
float speed = cam_speed_;
bool touched = false;
if (Input::IsGamepadConnected()) {
ls = Input::GetLeftStick();
rs = Input::GetRightStick();
up = Input::GetR2();
down = Input::GetL2();
if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross))
speed *= 3.0f;
if (glm::length(ls) + glm::length(rs) + up + down > 0.01f)
touched = true;
}
if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT)) speed *= 3.0f;
if (Input::IsKeyHeld(GLFW_KEY_W)) { ls.y -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_S)) { ls.y += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_A)) { ls.x -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_D)) { ls.x += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_LEFT)) { rs.x -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) { rs.x += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_UP)) { rs.y -= 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_DOWN)) { rs.y += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_SPACE)) { up += 1.0f; touched = true; }
if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL)) { down += 1.0f; touched = true; }
if (!touched) {
last_cam_pos_ = eng_pos;
return; // leave whatever the AI god (or initial state) set alone
}
cam_yaw_ += rs.x * look_sensitivity_ * dt;
cam_pitch_ -= rs.y * look_sensitivity_ * dt;
if (cam_pitch_ > 89.0f) cam_pitch_ = 89.0f;
if (cam_pitch_ < -89.0f) cam_pitch_ = -89.0f;
glm::vec3 forward;
forward.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
forward.y = std::sin(glm::radians(cam_pitch_));
forward.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_));
forward = glm::normalize(forward);
glm::vec3 right = glm::normalize(glm::cross(forward, glm::vec3(0, 1, 0)));
glm::vec3 pos = eng_pos;
pos += forward * (-ls.y) * speed * dt;
pos += right * ls.x * speed * dt;
pos.y += (up - down) * speed * dt;
SetCameraPosition(pos);
SetCameraTarget(pos + forward);
last_cam_pos_ = pos;
}
};
// ---------------------------------------------
// Launch a scene by action string
// ---------------------------------------------
static void runScene(const std::string& mode) {
if (mode == "stress") {
std::cout << "Running Stress Test..." << std::endl;
StressTest demo;
demo.Run();
}
else if (mode == "monkeygrid") {
std::cout << "Starting Monkey Grid Demo..." << std::endl;
MonkeyGridDemo demo;
demo.Run();
}
else if (mode == "edit") {
std::cout << "Starting Zoom Level Editor..." << std::endl;
LevelEditor edit;
edit.Run();
}
else if (mode == "landscape") {
std::cout << "Starting Landscape Demo..." << std::endl;
LandscapeDemo demo;
demo.Run();
}
else if (mode == "monkey") {
SpinningMonkey demo;
demo.Run();
}
else if (mode == "god") {
std::cout << "Starting God Sandbox..." << std::endl;
GodSandbox demo;
demo.Run();
}
else if (mode == "grass") {
std::cout << "Starting Grass/Pools/Shadows Demo..." << std::endl;
GrassPoolsDemo demo;
demo.Run();
}
else if (mode == "sunset") {
std::cout << "Starting Sunset Forest (HDR) Demo..." << std::endl;
SunsetForestDemo demo;
demo.Run();
}
else if (mode == "sanctuary" || mode == "full") {
std::cout << "Starting Sanctuary (Full Engine Showcase)..." << std::endl;
SanctuaryDemo demo;
demo.Run();
}
else if (mode == "dam") {
std::cout << "Starting Dam (Multi-water) Demo..." << std::endl;
DamDemo demo;
demo.Run();
}
else if (mode == "waterfall") {
std::cout << "Starting Waterfall (Flow Physics) Demo..." << std::endl;
WaterfallDemo demo;
demo.Run();
}
else if (mode == "playground") {
std::cout << "Starting Physics Playground..." << std::endl;
PhysicsPlaygroundDemo demo;
demo.Run();
}
else if (mode == "fourmap" || mode == "quadrants") {
std::cout << "Starting Four-Quadrant Map..." << std::endl;
FourMapDemo demo;
demo.Run();
}
else if (mode == "builders") {
std::cout << "Starting Builders (top-down)..." << std::endl;
BuildersDemo demo;
demo.Run();
}
else if (mode == "odyssey") {
std::cout << "Starting Odyssey (Explore)..." << std::endl;
OdysseyDemo demo;
demo.Run();
}
else if (mode.rfind("loadworld:", 0) == 0) {
std::string path = mode.substr(10);
std::cout << "Loading world: " << path << std::endl;
GodSandbox demo;
demo.SetLoadPath(path);
demo.Run();
}
else {
std::cout << "Starting Scene Demo..." << std::endl;
SceneDemo demo;
demo.Run();
}
}
// ---------------------------------------------
// main: menu (default) or direct scene by arg
// ---------------------------------------------
int main(int argc, char** argv) {
try {
// Direct launch: ./ModernEngine <scene>
if (argc > 1) {
std::string mode = argv[1];
if (mode != "menu") {
runScene(mode);
return 0;
}
}
// Menu mode: load JSON config and show interactive menu
// Try assets/menu.json, then fall back to built-in default
json::Value menuConfig;
try {
menuConfig = json::parseFile("../assets/menu.json");
std::cout << "Loaded menu from assets/menu.json" << std::endl;
} catch (...) {
try {
menuConfig = json::parseFile("assets/menu.json");
std::cout << "Loaded menu from assets/menu.json" << std::endl;
} catch (...) {
// Built-in fallback
menuConfig = json::parse(R"({
"title": "ZOOMENGINE",
"background_color": [0.04, 0.04, 0.1],
"title_color": [1.0, 0.85, 0.2],
"item_color": [0.65, 0.65, 0.7],
"selected_color": [1.0, 1.0, 0.3],
"items": [
{ "label": "Landscape", "action": "landscape" },
{ "label": "Scene Demo", "action": "demo" },
{ "label": "Stress Test", "action": "stress" },
{ "label": "Spinning Monkey", "action": "monkey" },
{ "label": "Monkey Grid", "action": "monkeygrid" },
{ "label": "God Sandbox", "action": "god" },
{ "label": "Level Editor", "action": "edit" },
{ "label": "Quit", "action": "quit" }
]
})");
std::cout << "Using built-in menu config" << std::endl;
}
}
// Inject one menu entry per worlds/*.json file, before the Quit item.
{
namespace fs = std::filesystem;
std::error_code ec;
std::vector<std::string> worldFiles;
for (const char* dir : {"worlds", "../worlds"}) {
if (!fs::exists(dir, ec)) continue;
for (auto& e : fs::directory_iterator(dir, ec)) {
if (e.path().extension() == ".json")
worldFiles.push_back(e.path().string());
}
break;
}
if (!worldFiles.empty() && menuConfig.has("items")
&& menuConfig["items"].is_array()) {
json::Array items = menuConfig["items"].as_array();
json::Array rebuilt;
bool inserted = false;
for (auto& it : items) {
if (!inserted && it.is_object()
&& it.get_string("action") == "quit") {
for (auto& path : worldFiles) {
fs::path p(path);
json::Object entry;
entry["label"] = json::Value(
std::string("World: ") + p.stem().string());
entry["action"] = json::Value(
std::string("loadworld:") + path);
rebuilt.push_back(json::Value(entry));
}
inserted = true;
}
rebuilt.push_back(it);
}
if (!inserted) {
for (auto& path : worldFiles) {
fs::path p(path);
json::Object entry;
entry["label"] = json::Value(
std::string("World: ") + p.stem().string());
entry["action"] = json::Value(
std::string("loadworld:") + path);
rebuilt.push_back(json::Value(entry));
}
}
json::Object newCfg = menuConfig.as_object();
newCfg["items"] = json::Value(rebuilt);
menuConfig = json::Value(newCfg);
std::cout << "Loaded " << worldFiles.size()
<< " saved world(s) into menu" << std::endl;
}
}
// Menu loop: show menu, launch scene, return to menu
while (true) {
std::string action;
{
MainMenu menu(menuConfig, [&](const std::string& a) { action = a; });
menu.Run();
action = menu.getSelectedAction();
}
if (action.empty() || action == "quit")
break;
// GLFW needs re-init after the menu window closes
glfwTerminate();
runScene(action);
// After scene exits, GLFW terminates — loop back to re-show menu
glfwTerminate();
}
return 0;
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return -1;
}
}