5889 lines
251 KiB
C++
5889 lines
251 KiB
C++
#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;
|
||
}
|
||
}
|