diff --git a/assets/menu.json b/assets/menu.json index d2ffaf4..8d3f1e5 100644 --- a/assets/menu.json +++ b/assets/menu.json @@ -16,6 +16,10 @@ { "label": "Sanctuary (Full)", "action": "sanctuary" }, { "label": "Dam (Multi-water)", "action": "dam" }, { "label": "Waterfall (Flow)", "action": "waterfall" }, + { "label": "Physics Playground","action": "playground" }, + { "label": "Four-Quadrant Map", "action": "fourmap" }, + { "label": "Builders (top-down)", "action": "builders" }, + { "label": "Odyssey (Explore)", "action": "odyssey" }, { "label": "Level Editor", "action": "edit" }, { "label": "Quit", "action": "quit" } ] diff --git a/build/CMakeFiles/ModernEngine.dir/compiler_depend.internal b/build/CMakeFiles/ModernEngine.dir/compiler_depend.internal index 13694bc..2a7386e 100644 --- a/build/CMakeFiles/ModernEngine.dir/compiler_depend.internal +++ b/build/CMakeFiles/ModernEngine.dir/compiler_depend.internal @@ -17161,6 +17161,7 @@ CMakeFiles/ModernEngine.dir/src/main.cpp.o /Users/will/Documents/zoomengine/src/Audio.h /Users/will/Documents/zoomengine/src/Engine.h /Users/will/Documents/zoomengine/src/Font.h + /Users/will/Documents/zoomengine/src/Hud.h /Users/will/Documents/zoomengine/src/Input.h /Users/will/Documents/zoomengine/src/Json.h /Users/will/Documents/zoomengine/src/LevelEditor.cpp diff --git a/build/CMakeFiles/ModernEngine.dir/compiler_depend.make b/build/CMakeFiles/ModernEngine.dir/compiler_depend.make index 5497e2c..307570a 100644 --- a/build/CMakeFiles/ModernEngine.dir/compiler_depend.make +++ b/build/CMakeFiles/ModernEngine.dir/compiler_depend.make @@ -17144,6 +17144,7 @@ CMakeFiles/ModernEngine.dir/src/main.cpp.o: /Users/will/Documents/zoomengine/src /Users/will/Documents/zoomengine/src/Audio.h \ /Users/will/Documents/zoomengine/src/Engine.h \ /Users/will/Documents/zoomengine/src/Font.h \ + /Users/will/Documents/zoomengine/src/Hud.h \ /Users/will/Documents/zoomengine/src/Input.h \ /Users/will/Documents/zoomengine/src/Json.h \ /Users/will/Documents/zoomengine/src/LevelEditor.cpp \ @@ -20153,6 +20154,8 @@ CMakeFiles/ModernEngine.dir/src/main.cpp.o: /Users/will/Documents/zoomengine/src /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/__type_traits/is_callable.h: +/Users/will/Documents/zoomengine/src/Hud.h: + /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/__algorithm/iter_swap.h: /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/__cstddef/nullptr_t.h: diff --git a/build/CMakeFiles/ModernEngine.dir/src/Engine.cpp.o b/build/CMakeFiles/ModernEngine.dir/src/Engine.cpp.o index f09313b..e994889 100644 Binary files a/build/CMakeFiles/ModernEngine.dir/src/Engine.cpp.o and b/build/CMakeFiles/ModernEngine.dir/src/Engine.cpp.o differ diff --git a/build/CMakeFiles/ModernEngine.dir/src/Physics.cpp.o b/build/CMakeFiles/ModernEngine.dir/src/Physics.cpp.o index 4519bd2..aac91b6 100644 Binary files a/build/CMakeFiles/ModernEngine.dir/src/Physics.cpp.o and b/build/CMakeFiles/ModernEngine.dir/src/Physics.cpp.o differ diff --git a/build/CMakeFiles/ModernEngine.dir/src/main.cpp.o b/build/CMakeFiles/ModernEngine.dir/src/main.cpp.o index fa93ac9..f4cfc0c 100644 Binary files a/build/CMakeFiles/ModernEngine.dir/src/main.cpp.o and b/build/CMakeFiles/ModernEngine.dir/src/main.cpp.o differ diff --git a/build/CMakeFiles/ModernEngine.dir/src/main.cpp.o.d b/build/CMakeFiles/ModernEngine.dir/src/main.cpp.o.d index aeeee1b..e245e99 100644 --- a/build/CMakeFiles/ModernEngine.dir/src/main.cpp.o.d +++ b/build/CMakeFiles/ModernEngine.dir/src/main.cpp.o.d @@ -1451,6 +1451,7 @@ CMakeFiles/ModernEngine.dir/src/main.cpp.o: \ /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/unordered_set \ /Users/will/Documents/zoomengine/src/MainMenu.h \ /Users/will/Documents/zoomengine/src/TextureCache.h \ + /Users/will/Documents/zoomengine/src/Hud.h \ /Users/will/Documents/zoomengine/src/LevelEditor.cpp \ /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/random \ /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/c++/v1/__random/bernoulli_distribution.h \ diff --git a/build/ModernEngine b/build/ModernEngine index 3702631..2762116 100755 Binary files a/build/ModernEngine and b/build/ModernEngine differ diff --git a/src/Engine.cpp b/src/Engine.cpp index fbf0ae7..b865830 100644 --- a/src/Engine.cpp +++ b/src/Engine.cpp @@ -255,21 +255,25 @@ void Engine::InitializeShader() { #version 410 core layout(location=0) in vec2 aPos; layout(location=1) in vec3 aColor; + layout(location=2) in float aAlpha; uniform mat4 uOrtho; out vec3 vColor; + out float vAlpha; void main() { vec4 pos = uOrtho * vec4(aPos, 0.0, 1.0); gl_Position = pos; vColor = aColor; + vAlpha = aAlpha; } )"; const char* fs_overlay = R"( #version 410 core in vec3 vColor; + in float vAlpha; out vec4 FragColor; void main() { - FragColor = vec4(vColor, 1.0); + FragColor = vec4(vColor, vAlpha); } )"; overlay_program = CreateProgram(vs_overlay, fs_overlay); @@ -730,7 +734,8 @@ public: // TerrainPrimitive - heightmap terrain with procedural noise for landscapes class TerrainPrimitive : public Primitive { public: - TerrainPrimitive(int resolution, float size, float maxHeight, const glm::vec3& baseColor) { + TerrainPrimitive(int resolution, float size, float maxHeight, const glm::vec3& baseColor, + float edgeFalloff = 0.0f, bool tintByBase = false) { color_ = baseColor; std::vector vertices; std::vector indices; @@ -747,10 +752,22 @@ public: h += std::sin(px * 0.1f) * std::cos(pz * 0.1f) * maxHeight * 0.5f; h += std::sin(px * 0.25f + 1.3f) * std::cos(pz * 0.3f + 0.7f) * maxHeight * 0.25f; h += std::sin(px * 0.6f + 2.1f) * std::cos(pz * 0.5f + 1.5f) * maxHeight * 0.125f; - // Color by height: green low, brown mid, grey/white high + // Edge falloff: taper heights to zero within edgeFalloff units of mesh edge + if (edgeFalloff > 0.0f) { + float dx_edge = halfSize - std::fabs(px); + float dz_edge = halfSize - std::fabs(pz); + float d = std::min(dx_edge, dz_edge); + float f = std::clamp(d / edgeFalloff, 0.0f, 1.0f); + f = f * f * (3.0f - 2.0f * f); // smoothstep + h *= f; + } + // Color: either height-banded palette (default) or baseColor tinted by height float t = (h + maxHeight) / (2.0f * maxHeight); glm::vec3 col; - if (t < 0.3f) col = glm::mix(glm::vec3(0.2f, 0.35f, 0.1f), glm::vec3(0.3f, 0.5f, 0.15f), t / 0.3f); + if (tintByBase) { + float k = 0.65f + 0.55f * t; // 0.65..1.2 brightness + col = glm::clamp(baseColor * k, glm::vec3(0.0f), glm::vec3(1.5f)); + } else if (t < 0.3f) col = glm::mix(glm::vec3(0.2f, 0.35f, 0.1f), glm::vec3(0.3f, 0.5f, 0.15f), t / 0.3f); else if (t < 0.6f) col = glm::mix(glm::vec3(0.3f, 0.5f, 0.15f), glm::vec3(0.45f, 0.35f, 0.2f), (t - 0.3f) / 0.3f); else if (t < 0.85f)col = glm::mix(glm::vec3(0.45f, 0.35f, 0.2f), glm::vec3(0.5f, 0.5f, 0.5f), (t - 0.6f) / 0.25f); else col = glm::mix(glm::vec3(0.5f, 0.5f, 0.5f), glm::vec3(0.95f, 0.95f, 0.98f), (t - 0.85f) / 0.15f); @@ -1241,7 +1258,10 @@ void Engine::RenderOverlay() { {0.3f, 0.8f, 0.3f}, 1.5f); } - // 9) Restore GL state + // 9) Subclass overlay content (scene-specific HUD, message logs, etc.) + OnRenderOverlay(); + + // 10) Restore GL state font_.end(); } @@ -1727,6 +1747,19 @@ std::string Engine::processAgentCommand(const json::Value& cmd) { if (cmd.has("flow_speed")) physics_.water_flow_speed = std::max(0.0f, cmd.get_float("flow_speed", 0.0f)); + // Pool-bounds gating: only apply buoyancy inside a circle (for pools). + if (cmd.has("bounds_center")) { + glm::vec3 c = readVec3(cmd, "bounds_center", glm::vec3(0)); + physics_.water_bounds_center = glm::vec2(c.x, c.z); + physics_.water_bounds_enabled = true; + } + if (cmd.has("bounds_radius")) { + physics_.water_bounds_radius = std::max(0.1f, cmd.get_float("bounds_radius", 100.0f)); + physics_.water_bounds_enabled = true; + } + if (cmd.has("bounds_enabled")) + physics_.water_bounds_enabled = cmd.get_bool("bounds_enabled", true); + RebuildWaterMesh(cmd.get_int("resolution", 128)); return R"({"ok":true})"; } @@ -2004,8 +2037,8 @@ std::shared_ptr Engine::CreateCone(float radius, float height, int se return std::make_shared(radius, height, segments, color); } -std::shared_ptr Engine::CreateTerrain(int resolution, float size, float maxHeight, const glm::vec3& baseColor) { - return std::make_shared(resolution, size, maxHeight, baseColor); +std::shared_ptr Engine::CreateTerrain(int resolution, float size, float maxHeight, const glm::vec3& baseColor, float edgeFalloff, bool tintByBase) { + return std::make_shared(resolution, size, maxHeight, baseColor, edgeFalloff, tintByBase); } // GameObject management @@ -2038,6 +2071,25 @@ void Engine::SetCameraOrbit(float distance, float angle, float height) { ); } +glm::vec3 Engine::ScreenToGround() const { + glm::vec2 m = Input::GetMousePosition(); + float ndc_x = 2.0f * m.x / float(width_) - 1.0f; + float ndc_y = 1.0f - 2.0f * m.y / float(height_); + 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 inv = glm::inverse(proj * view); + glm::vec4 a = inv * glm::vec4(ndc_x, ndc_y, -1.0f, 1.0f); + glm::vec4 b = inv * glm::vec4(ndc_x, ndc_y, 1.0f, 1.0f); + glm::vec3 p0 = glm::vec3(a) / a.w; + glm::vec3 p1 = glm::vec3(b) / b.w; + glm::vec3 d = glm::normalize(p1 - p0); + if (std::abs(d.y) < 1e-5f) return glm::vec3(0); + float t = -p0.y / d.y; + if (t < 0) return glm::vec3(0); + return p0 + d * t; +} + // ---- Sky ---- void Engine::InitSky() { const char* vs = R"(#version 410 core diff --git a/src/Engine.h b/src/Engine.h index acc63e2..ef090df 100644 --- a/src/Engine.h +++ b/src/Engine.h @@ -31,6 +31,9 @@ public: virtual void OnShutdown(); virtual void OnUpdate(float delta_time); virtual void OnRender(); + // Extensibility hook for subclasses to add text overlay content. Called + // at the end of the engine's RenderOverlay pass with font_ already set up. + virtual void OnRenderOverlay() {} void Run(); @@ -44,7 +47,7 @@ public: std::shared_ptr CreateOBJModel(const std::string& objPath, const std::string& texturePath = ""); std::shared_ptr CreateCylinder(float radius = 0.5f, float height = 2.0f, int segments = 16, const glm::vec3& color = glm::vec3(1.0f)); std::shared_ptr CreateCone(float radius = 1.0f, float height = 2.0f, int segments = 16, const glm::vec3& color = glm::vec3(1.0f)); - std::shared_ptr CreateTerrain(int resolution = 64, float size = 100.0f, float maxHeight = 8.0f, const glm::vec3& baseColor = glm::vec3(0.3f, 0.5f, 0.2f)); + std::shared_ptr CreateTerrain(int resolution = 64, float size = 100.0f, float maxHeight = 8.0f, const glm::vec3& baseColor = glm::vec3(0.3f, 0.5f, 0.2f), float edgeFalloff = 0.0f, bool tintByBase = false); // GameObject management @@ -60,6 +63,11 @@ public: const glm::vec3& GetCameraPosition() const { return camera_position_; } const glm::vec3& GetCameraTarget() const { return camera_target_; } + // Unproject the current mouse cursor to the y=0 ground plane using the + // active camera + default FOV. Returns (0,0,0) if the ray points away + // from the plane. + glm::vec3 ScreenToGround() const; + GLuint text_vao = 0, text_vbo = 0; diff --git a/src/Font.h b/src/Font.h index aaa9234..29c46d9 100644 --- a/src/Font.h +++ b/src/Font.h @@ -60,47 +60,59 @@ public: float getSpacing() const { return spacing_; } float getWeight() const { return weight_; } - // Draw a string. lineWidth < 0 means use the default weight. - // Returns the total advance width in pixels. + // Draw a string as filled triangle quads (each stroke thickened into a + // ribbon). Returns total advance width in pixels. float drawText(const std::string& text, float x, float y, float size, const glm::vec3& color = glm::vec3(1.0f), float lineWidth = -1.0f) { if (text.empty()) return 0.0f; - float lw = (lineWidth < 0) ? weight_ : lineWidth; + float stroke = ((lineWidth < 0) ? weight_ : lineWidth) * 0.5f; verts_.clear(); float cx = x; - float advance = size * spacing_; - for (char ch : text) { const Glyph& g = getGlyph(ch); for (size_t i = 0; i + 1 < g.points.size(); i += 2) { - auto& p0 = g.points[i]; - auto& p1 = g.points[i + 1]; - verts_.push_back({cx + p0.x * size, y + p0.y * size, color.r, color.g, color.b}); - verts_.push_back({cx + p1.x * size, y + p1.y * size, color.r, color.g, color.b}); + glm::vec2 a(cx + g.points[i].x * size, y + g.points[i].y * size); + glm::vec2 b(cx + g.points[i+1].x * size, y + g.points[i+1].y * size); + emitQuad(a, b, stroke, color); } - cx += advance; + cx += advanceFor(ch, size); } - - if (!verts_.empty()) { - glBufferData(GL_ARRAY_BUFFER, verts_.size() * sizeof(Vertex), - verts_.data(), GL_DYNAMIC_DRAW); - glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0); - glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), - (void*)(2 * sizeof(float))); - if (smooth_) { - glEnable(GL_LINE_SMOOTH); - glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); - } - glLineWidth(lw); - glDrawArrays(GL_LINES, 0, (GLsizei)verts_.size()); - glLineWidth(1.0f); - if (smooth_) glDisable(GL_LINE_SMOOTH); - } - + flushTriangles(); return cx - x; } + // Outlined text: draws a dark halo behind the main stroke for legibility. + // outlineExtra is extra pixels added on each side (not a multiplier), so + // the main glyph shape stays legible. + float drawOutlinedText(const std::string& text, float x, float y, float size, + const glm::vec3& color = glm::vec3(1.0f), + const glm::vec3& outline = glm::vec3(0.0f, 0.0f, 0.0f), + float outlineExtra = 1.5f) { + float base = (weight_ < 0 ? 2.0f : weight_); + drawText(text, x, y, size, outline, base + outlineExtra * 2.0f); + return drawText(text, x, y, size, color, base); + } + + // Filled rectangle — useful for HUD panel backgrounds. + void fillRect(float x, float y, float w, float h, + const glm::vec4& rgba) { + verts_.clear(); + pushTri(x, y, x + w, y, x + w, y + h, glm::vec3(rgba), rgba.a); + pushTri(x, y, x + w, y + h, x, y + h, glm::vec3(rgba), rgba.a); + flushTriangles(); + } + + // Rectangle outline as 4 thin ribbons. + void strokeRect(float x, float y, float w, float h, + const glm::vec4& rgba, float stroke = 1.5f) { + float s = stroke * 0.5f; + fillRect(x - s, y - s, w + 2*s, 2*s, rgba); // bottom + fillRect(x - s, y + h - s, w + 2*s, 2*s, rgba); // top + fillRect(x - s, y - s, 2*s, h + 2*s, rgba); // left + fillRect(x + w - s, y - s, 2*s, h + 2*s, rgba); // right + } + // Draw centered text float drawTextCentered(const std::string& text, float centerX, float y, float size, const glm::vec3& color = glm::vec3(1.0f), float lineWidth = -1.0f) { @@ -131,24 +143,65 @@ public: return y - cy; } - // Measure text width in pixels without drawing + // Measure text width in pixels — respects per-glyph widths float measureText(const std::string& text, float size) const { - return text.size() * size * spacing_; + float w = 0; + for (char c : text) w += advanceFor(c, size); + return w; } private: - struct Vertex { float x, y, r, g, b; }; + struct Vertex { float x, y, r, g, b, a; }; struct Glyph { std::vector points; }; // pairs of line endpoints GLuint vao_ = 0, vbo_ = 0, program_ = 0; - float spacing_ = 0.75f; // character advance as fraction of size - float weight_ = 2.0f; // default line width in pixels - bool smooth_ = true; // anti-aliased line rendering + float spacing_ = 0.95f; // character advance as fraction of size + float weight_ = 1.8f; // default stroke thickness + bool smooth_ = true; std::array glyphs_; + std::array widths_{}; // per-glyph advance width (fraction) std::vector verts_; - const Glyph& getGlyph(char c) const { + float advanceFor(char c, float size) const { unsigned char uc = (unsigned char)c; + float w = (uc < 128 && widths_[uc] > 0) ? widths_[uc] : spacing_; + return size * w; + } + void pushTri(float x0, float y0, float x1, float y1, float x2, float y2, + const glm::vec3& rgb, float a) { + verts_.push_back({x0, y0, rgb.r, rgb.g, rgb.b, a}); + verts_.push_back({x1, y1, rgb.r, rgb.g, rgb.b, a}); + verts_.push_back({x2, y2, rgb.r, rgb.g, rgb.b, a}); + } + void emitQuad(const glm::vec2& a, const glm::vec2& b, float half, + const glm::vec3& color) { + glm::vec2 d = b - a; + float len = std::sqrt(d.x * d.x + d.y * d.y); + if (len < 1e-5f) return; + glm::vec2 n(-d.y / len * half, d.x / len * half); // perpendicular + glm::vec2 p0 = a - n, p1 = a + n, p2 = b + n, p3 = b - n; + pushTri(p0.x, p0.y, p1.x, p1.y, p2.x, p2.y, color, 1.0f); + pushTri(p0.x, p0.y, p2.x, p2.y, p3.x, p3.y, color, 1.0f); + } + void flushTriangles() { + if (verts_.empty()) return; + glBufferData(GL_ARRAY_BUFFER, verts_.size() * sizeof(Vertex), + verts_.data(), GL_DYNAMIC_DRAW); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)0); + glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), + (void*)(2 * sizeof(float))); + glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, sizeof(Vertex), + (void*)(5 * sizeof(float))); + glEnableVertexAttribArray(0); + glEnableVertexAttribArray(1); + glEnableVertexAttribArray(2); + glDrawArrays(GL_TRIANGLES, 0, (GLsizei)verts_.size()); + } + + const Glyph& getGlyph(char c) const { + static const Glyph empty; // shared whitespace sentinel + unsigned char uc = (unsigned char)c; + if (uc == ' ' || uc == '\t') return empty; if (uc < 128 && !glyphs_[uc].points.empty()) return glyphs_[uc]; return glyphs_[(unsigned char)'?']; } @@ -160,6 +213,17 @@ private: } void buildGlyphs() { + // Per-glyph advance widths (fraction of font size). Scale relative + // to the active spacing so changing spacing_ preserves rhythm. + for (int i = 0; i < 128; ++i) widths_[i] = spacing_; + const float s = spacing_; + widths_['i'] = widths_['I'] = widths_['!'] = widths_['|'] = widths_['.'] = + widths_[','] = widths_[';'] = widths_[':'] = widths_['\''] = 0.45f * s; + widths_['l'] = widths_['j'] = widths_['t'] = widths_['f'] = widths_['r'] = 0.60f * s; + widths_['M'] = widths_['W'] = widths_['m'] = widths_['w'] = 1.40f * s; + widths_[' '] = 0.70f * s; + widths_['-'] = widths_['_'] = 0.85f * s; + // ---- Uppercase Letters ---- { auto& g = glyphs_['A']; L(g, 0,0, 0.5f,1); L(g, 0.5f,1, 1,0); L(g, 0.2f,0.45f, 0.8f,0.45f); } diff --git a/src/Hud.h b/src/Hud.h new file mode 100644 index 0000000..b373290 --- /dev/null +++ b/src/Hud.h @@ -0,0 +1,184 @@ +#pragma once +#include "Font.h" +#include +#include +#include +#include +#include + +// Immediate-mode HUD builder. +// +// Hud hud(font, width, height); +// hud.beginPanel(10, 10, 320, 120, {0,0,0,0.55f}); +// hud.label("Title", 16, {1,1,1}); +// hud.bar(200, 14, 0.72f, {0.1f,0.6f,0.2f,1}, {1,1,1,0.2f}); +// hud.label("72% throughput", 12, {0.9f,0.95f,0.9f}); +// hud.endPanel(); +// hud.flush(); +// +// Panels are stacked rectangles with translucent backgrounds + optional +// borders. Widgets inside flow top-down from the panel's top, padded by +// `padding`. The font handles text rendering; the Hud draws its own rects +// via the font's fillRect/strokeRect helpers. + +class Hud { +public: + Hud(Font& font, int screen_w, int screen_h) + : font_(font), w_(screen_w), h_(screen_h) {} + + // --- Panel stacking --------------------------------------------------- + void beginPanel(float x, float y, float w, float h, + const glm::vec4& bg = glm::vec4(0, 0, 0, 0.55f), + const glm::vec4& border = glm::vec4(1, 1, 1, 0.0f), + float border_thickness = 1.0f) { + Panel p{x, y, w, h, x + padding_, y + h - padding_ - 16.0f, bg, border, border_thickness}; + panels_.push_back(p); + } + void endPanel() { + if (!panels_.empty()) { + // Defer the actual draw so text is drawn on top of backgrounds in + // the right order. For simplicity, draw bg now, widgets are already + // placed, border last. + panels_.pop_back(); + } + } + + // --- Widgets ---------------------------------------------------------- + void label(const std::string& text, float size = 14.0f, + const glm::vec3& color = glm::vec3(1.0f), + bool outlined = false) { + if (panels_.empty()) return; + Panel& p = panels_.back(); + float y = p.cursor_y; + draws_.push_back([this, text, p_x = p.cursor_x, y, size, color, outlined]() { + if (outlined) font_.drawOutlinedText(text, p_x, y, size, color); + else font_.drawText(text, p_x, y, size, color); + }); + p.cursor_y -= (size + 4.0f); + } + + void labelRight(const std::string& text, float size, + const glm::vec3& color = glm::vec3(1.0f)) { + if (panels_.empty()) return; + Panel& p = panels_.back(); + float right = p.x + p.w - padding_; + float y = p.cursor_y; + draws_.push_back([this, text, right, y, size, color]() { + font_.drawTextRight(text, right, y, size, color); + }); + p.cursor_y -= (size + 4.0f); + } + + // Progress bar: value in [0,1]. Width spans the panel minus padding. + void bar(float height, float value, + const glm::vec4& fill = glm::vec4(0.2f, 0.7f, 0.3f, 1.0f), + const glm::vec4& bg = glm::vec4(1.0f, 1.0f, 1.0f, 0.15f), + const glm::vec4& border_ = glm::vec4(1.0f, 1.0f, 1.0f, 0.35f)) { + if (panels_.empty()) return; + Panel& p = panels_.back(); + float x = p.cursor_x; + float y = p.cursor_y - height + 10.0f; + float w = p.w - 2 * padding_; + float v = std::clamp(value, 0.0f, 1.0f); + draws_.push_back([this, x, y, w, height, v, fill, bg, border_]() { + font_.fillRect(x, y, w, height, bg); + font_.fillRect(x, y, w * v, height, fill); + font_.strokeRect(x, y, w, height, border_, 1.0f); + }); + p.cursor_y -= (height + 6.0f); + } + + // Colored icon: solid rounded rect + void icon(float w_icon, float h_icon, const glm::vec4& col) { + if (panels_.empty()) return; + Panel& p = panels_.back(); + float x = p.cursor_x; + float y = p.cursor_y - h_icon + 10.0f; + draws_.push_back([this, x, y, w_icon, h_icon, col]() { + font_.fillRect(x, y, w_icon, h_icon, col); + }); + p.cursor_y -= (h_icon + 4.0f); + } + + // Horizontal separator line + void separator(float thickness = 1.0f, + const glm::vec4& col = glm::vec4(1, 1, 1, 0.25f)) { + if (panels_.empty()) return; + Panel& p = panels_.back(); + float x = p.cursor_x; + float y = p.cursor_y + 6.0f; + float w = p.w - 2 * padding_; + draws_.push_back([this, x, y, w, thickness, col]() { + font_.fillRect(x, y, w, thickness, col); + }); + p.cursor_y -= (thickness + 6.0f); + } + + void spacer(float h = 4.0f) { + if (panels_.empty()) return; + panels_.back().cursor_y -= h; + } + + // --- Free-form (outside panels) --------------------------------------- + void rect(float x, float y, float w, float h, const glm::vec4& col) { + draws_.push_back([this, x, y, w, h, col]() { font_.fillRect(x, y, w, h, col); }); + } + void text(float x, float y, const std::string& t, float size, + const glm::vec3& col, bool outlined = false) { + draws_.push_back([this, x, y, t, size, col, outlined]() { + if (outlined) font_.drawOutlinedText(t, x, y, size, col); + else font_.drawText(t, x, y, size, col); + }); + } + + // Draw anchored panel background frames then queued widgets in order + void flush() { + for (auto& f : bg_draws_) f(); + for (auto& f : draws_) f(); + for (auto& f : border_draws_) f(); + bg_draws_.clear(); + draws_.clear(); + border_draws_.clear(); + panels_.clear(); + } + + void setPadding(float p) { padding_ = p; } + +private: + struct Panel { + float x, y, w, h; + float cursor_x, cursor_y; + glm::vec4 bg; + glm::vec4 border; + float border_t; + }; + + Font& font_; + int w_, h_; + float padding_ = 12.0f; + std::vector panels_; + std::vector> bg_draws_; + std::vector> draws_; + std::vector> border_draws_; + +public: + // Called instead of endPanel() when you want the panel bg drawn into the + // background layer (so widget draws appear above it). + void drawPanelBackgroundNow() { + if (panels_.empty()) return; + const Panel& p = panels_.back(); + glm::vec4 bg = p.bg, br = p.border; + float x = p.x, y = p.y, w = p.w, h = p.h; + float bt = p.border_t; + if (bg.a > 0.0f) { + bg_draws_.push_back([this, x, y, w, h, bg]() { + font_.fillRect(x, y, w, h, bg); + }); + } + if (br.a > 0.0f) { + border_draws_.push_back([this, x, y, w, h, br, bt]() { + font_.strokeRect(x, y, w, h, br, bt); + }); + } + } +}; diff --git a/src/Physics.cpp b/src/Physics.cpp index bff44c8..67f8138 100644 --- a/src/Physics.cpp +++ b/src/Physics.cpp @@ -146,7 +146,14 @@ void World::IntegrateForces(float dt) { glm::vec3 total_torque = b.torque_accum; float submerged_frac_agg = 0.0f; - if (water_enabled && b.shape != Shape::Plane) { + bool inside_pool = true; + if (water_enabled && water_bounds_enabled) { + glm::vec2 xz(b.position.x, b.position.z); + glm::vec2 d = xz - water_bounds_center; + if (glm::dot(d, d) > water_bounds_radius * water_bounds_radius) + inside_pool = false; + } + if (water_enabled && inside_pool && b.shape != Shape::Plane) { if (b.shape == Shape::Sphere) { float r = b.half_extents.x; float wy = SampleWaterY(b.position.x, b.position.z); diff --git a/src/Physics.h b/src/Physics.h index a271bf8..cdf715f 100644 --- a/src/Physics.h +++ b/src/Physics.h @@ -93,6 +93,12 @@ public: glm::vec2 water_flow_dir = glm::vec2(0.0f, 0.0f); float water_flow_speed = 0.0f; + // Optional circular bounds on the buoyancy region (for pools). When + // enabled, only bodies inside (center, radius) get buoyancy forces. + bool water_bounds_enabled = false; + glm::vec2 water_bounds_center = glm::vec2(0.0f); + float water_bounds_radius = 0.0f; + // Gerstner sampler — mirrors the water shader exactly. float SampleWaterY(float x, float z) const; diff --git a/src/main.cpp b/src/main.cpp index a27b667..a1150b9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,7 @@ #include "Json.h" #include "MainMenu.h" #include "TextureCache.h" +#include "Hud.h" #include "LevelEditor.cpp" #include #include @@ -614,6 +615,2828 @@ public: } }; +// --------------------------------------------- +// OdysseyDemo — large explorable world with verticality and a playable +// character visible in first person (arm + hand), plus a 6-slot item +// hotbar. Each item throws a different kind of energy ball. +// Zones radiate from a central spawn: +// N: Forest valley (lowest) +// E: Sakura plateau (+8 elevation, reached via ramp) +// S: Desert basin (warm, flat) +// W: Tech cliffs (+14 elevation, glowing spires) +// Center: lava/volcano pit with an emissive glow column +// --------------------------------------------- +class OdysseyDemo : public Engine { + // Camera state + float cam_yaw_ = 180.0f; + float cam_pitch_ = -10.0f; + float speed_ = 18.0f; + float look_sens_ = 90.0f; + glm::vec3 last_cam_pos_{0}; + + // First-person arm + std::shared_ptr arm_upper_, arm_lower_, hand_; + float throw_anim_ = 0.0f; // 0..1 animation progress + bool arm_ready_ = false; + + // Hotbar items + struct Item { + const char* label; + glm::vec3 color; + glm::vec3 emissive; + float mass; + float velocity; + float radius; + }; + Item items_[6] = { + {"STEEL", glm::vec3(0.85f, 0.88f, 0.95f), glm::vec3(0.0f), 2.5f, 55.0f, 0.35f}, + {"FIRE", glm::vec3(1.40f, 0.35f, 0.10f), glm::vec3(4.5f, 1.2f, 0.3f), 1.2f, 55.0f, 0.38f}, + {"ICE", glm::vec3(0.65f, 0.92f, 1.10f), glm::vec3(0.4f, 0.9f, 1.5f), 1.8f, 55.0f, 0.35f}, + {"PLASMA", glm::vec3(1.30f, 0.40f, 1.30f), glm::vec3(2.2f, 0.5f, 3.5f), 0.8f, 70.0f, 0.32f}, + {"VOID", glm::vec3(0.12f, 0.08f, 0.18f), glm::vec3(0.0f), 5.0f, 45.0f, 0.45f}, + {"GOLD", glm::vec3(1.20f, 0.95f, 0.45f), glm::vec3(0.6f, 0.4f, 0.0f), 3.0f, 60.0f, 0.38f}, + }; + int selected_ = 0; + bool q_prev_ = false, e_prev_ = false; + bool ctrl_cycle_cooldown_ = false; + + // Shooting + int shot_counter_ = 0; + float shot_cooldown_ = 0.0f; + std::deque active_shots_; + static constexpr int kMaxShots = 80; + std::mt19937 rng_{66612}; + +public: + OdysseyDemo() + : Engine(1920, 1080, "ZoomEngine - Odyssey (expansive)") {} + + bool OnInitialize() override { + auto rnd = [&](float lo, float hi) { + std::uniform_real_distribution d(lo, hi); return d(rng_); + }; + auto jit = [&](float s) { return rnd(-s, s); }; + + // Neutral-daylight base; zone tint comes from textures + processAgentCommand(json::parse(R"({ + "action":"sun", + "direction":[-0.40, 0.70, -0.25], + "color":[1.95, 1.80, 1.55], + "zenith":[0.22, 0.40, 0.68], + "horizon":[0.85, 0.78, 0.70], + "sky":[0.62, 0.72, 0.78], + "ground":[0.08, 0.09, 0.10], + "shadow_extent":150, + "exposure":0.95, + "bloom_threshold":1.15, + "bloom_intensity":1.2, + "fog_density":0.0035 + })")); + processAgentCommand(json::parse(R"({"action":"water","enabled":false})")); + + GLuint grass_tex = TextureCache::Get("grass"); + GLuint rock_tex = TextureCache::Get("rock"); + GLuint dirt_t = TextureCache::Get("dirt"); + GLuint marble_t = TextureCache::Get("marble"); + GLuint concrete = TextureCache::Get("concrete"); + GLuint wood_tex = TextureCache::Get("wood"); + GLuint grid_t = TextureCache::Get("grid"); + GLuint noise_t = TextureCache::Get("noise"); + + // --- Forest valley (N, y≈0, default plane) --- + auto forest = CreateGameObject( + CreatePlane(240.0f, 120.0f, glm::vec3(0.72f, 0.85f, 0.68f)), + glm::vec3(0, 0, 70.0f)); + forest->SetTexture(grass_tex); forest->SetNormalMap(noise_t); + forest->SetUVScale(glm::vec2(0.2f)); forest->SetRoughness(0.95f); + AddGameObject(forest); + + // Spawn plaza (center) + auto spawn = CreateGameObject( + CreatePlane(40.0f, 40.0f, glm::vec3(0.92f, 0.92f, 0.88f)), + glm::vec3(0, 0.01f, 0)); + spawn->SetTexture(marble_t); spawn->SetNormalMap(noise_t); + spawn->SetUVScale(glm::vec2(0.2f)); spawn->SetRoughness(0.5f); + AddGameObject(spawn); + + // --- Sakura plateau (E, y=8) --- + // Stepped ramp up from center to plateau + for (int step = 1; step <= 4; ++step) { + auto s = CreateGameObject( + CreateCube(glm::vec3(0.95f, 0.85f, 0.82f)), + glm::vec3(20.0f + step * 3.5f, step * 1.0f, 0)); + s->SetScale(glm::vec3(4.0f, 2.0f, 18.0f)); + s->SetTexture(marble_t); s->SetNormalMap(noise_t); + s->SetRoughness(0.55f); + AddGameObject(s); + } + auto sakura_ground = CreateGameObject( + CreatePlane(100.0f, 140.0f, glm::vec3(1.0f, 0.82f, 0.85f)), + glm::vec3(90.0f, 8.0f, 0)); + sakura_ground->SetTexture(grass_tex); sakura_ground->SetNormalMap(noise_t); + sakura_ground->SetUVScale(glm::vec2(0.25f)); sakura_ground->SetRoughness(0.92f); + AddGameObject(sakura_ground); + // Pink plateau sides (visible from below) + for (int i = 0; i < 4; ++i) { + auto side = CreateGameObject( + CreateCube(glm::vec3(0.86f, 0.78f, 0.82f)), + glm::vec3(90.0f + (i - 1.5f) * 30.0f, 4.0f, 70.0f)); + side->SetScale(glm::vec3(30.0f, 8.0f, 4.0f)); + side->SetTexture(concrete); side->SetNormalMap(noise_t); + side->SetRoughness(0.85f); + AddGameObject(side); + } + + // Cherry trees on plateau + auto cherry = [&](glm::vec3 at, float s) { + auto trunk = CreateGameObject( + CreateCylinder(0.24f * s, 2.6f * s, 10, + glm::vec3(0.48f, 0.32f, 0.22f)), + at + glm::vec3(0, 1.3f * s, 0)); + trunk->SetRotation(glm::vec3(jit(4), jit(360), jit(4))); + trunk->SetTexture(wood_tex); trunk->SetNormalMap(noise_t); + trunk->SetRoughness(0.85f); + AddGameObject(trunk); + for (int k = 0; k < 3; ++k) { + float r = 1.6f * s * (1.0f - k * 0.20f); + auto canopy = CreateGameObject( + CreateSphere(24, glm::vec3(1.25f, 0.55f, 0.82f)), + at + glm::vec3(jit(0.25f), 2.8f * s + k * 0.9f * s, jit(0.25f))); + canopy->SetScale(glm::vec3(r, r * 0.75f, r)); + canopy->SetEmissive(glm::vec3(0.25f, 0.10f, 0.18f)); + canopy->SetRoughness(0.85f); + AddGameObject(canopy); + } + }; + for (int i = 0; i < 35; ++i) { + float x = 90.0f + rnd(-45.0f, 45.0f); + float z = rnd(-60.0f, 60.0f); + cherry(glm::vec3(x, 8.0f, z), rnd(0.9f, 1.4f)); + } + // Floating petals + for (int i = 0; i < 120; ++i) { + float x = 90.0f + rnd(-45.0f, 45.0f); + float z = rnd(-60.0f, 60.0f); + float y = 8.0f + rnd(2.0f, 9.0f); + auto petal = CreateGameObject( + CreateSphere(6, glm::vec3(1.5f, 0.6f, 0.85f)), + glm::vec3(x, y, z)); + petal->SetScale(glm::vec3(0.08f, 0.04f, 0.08f)); + petal->SetEmissive(glm::vec3(0.50f, 0.15f, 0.25f)); + AddGameObject(petal); + } + + // --- Desert basin (S, flat at y=0) --- + auto desert_g = CreateGameObject( + CreatePlane(160.0f, 120.0f, glm::vec3(1.05f, 0.88f, 0.60f)), + glm::vec3(0, 0.0f, -75.0f)); + desert_g->SetTexture(dirt_t); desert_g->SetNormalMap(noise_t); + desert_g->SetUVScale(glm::vec2(0.3f)); desert_g->SetRoughness(0.98f); + AddGameObject(desert_g); + // Step pyramid + for (int lvl = 0; lvl < 6; ++lvl) { + float s = 12.0f - lvl * 1.8f; + auto p = CreateGameObject( + CreateCube(glm::vec3(0.92f, 0.76f, 0.52f)), + glm::vec3(-30.0f, 1.1f + lvl * 2.2f, -90.0f)); + p->SetScale(glm::vec3(s, 2.2f, s)); + p->SetTexture(dirt_t); p->SetNormalMap(noise_t); + p->SetRoughness(0.95f); + AddGameObject(p); + } + // Obelisk + { + auto obel = CreateGameObject( + CreateCube(glm::vec3(0.95f, 0.85f, 0.55f)), + glm::vec3(30.0f, 6.0f, -90.0f)); + obel->SetScale(glm::vec3(1.8f, 12.0f, 1.8f)); + obel->SetTexture(marble_t); obel->SetNormalMap(noise_t); + obel->SetRoughness(0.45f); + obel->SetRotation(glm::vec3(0, 20.0f, 0)); + AddGameObject(obel); + } + // Cacti scattered + for (int i = 0; i < 24; ++i) { + float x = rnd(-70.0f, 70.0f); + float z = rnd(-120.0f, -30.0f); + float h = rnd(2.0f, 3.5f); + auto c = CreateGameObject( + CreateCylinder(0.4f, h, 8, glm::vec3(0.30f, 0.55f, 0.32f)), + glm::vec3(x, h * 0.5f, z)); + c->SetRoughness(0.8f); c->SetNormalMap(noise_t); + AddGameObject(c); + } + // Sand dunes (flattened spheres) + for (int i = 0; i < 10; ++i) { + float x = rnd(-70.0f, 70.0f); + float z = rnd(-120.0f, -30.0f); + auto d = CreateGameObject( + CreateSphere(18, glm::vec3(1.0f, 0.82f, 0.55f)), + glm::vec3(x, 0, z)); + d->SetScale(glm::vec3(rnd(4, 7), rnd(1, 2), rnd(3, 6))); + d->SetTexture(dirt_t); d->SetNormalMap(noise_t); + d->SetRoughness(0.95f); + AddGameObject(d); + } + + // --- Tech cliffs (W, high at y=14) --- + // Vertical cliff face approaching from the plaza + for (int i = 0; i < 6; ++i) { + auto block = CreateGameObject( + CreateCube(glm::vec3(0.75f, 0.78f, 0.84f)), + glm::vec3(-30.0f - i * 3.0f, (i + 1) * 1.2f, 0)); + block->SetScale(glm::vec3(3.0f, 2.4f * (i + 1), 18.0f)); + block->SetTexture(concrete); block->SetNormalMap(noise_t); + block->SetRoughness(0.75f); + AddGameObject(block); + } + auto tech_top = CreateGameObject( + CreatePlane(100.0f, 140.0f, glm::vec3(0.30f, 0.34f, 0.40f)), + glm::vec3(-90.0f, 14.0f, 0)); + tech_top->SetTexture(grid_t); tech_top->SetNormalMap(noise_t); + tech_top->SetUVScale(glm::vec2(0.5f)); + tech_top->SetMetallic(0.6f); + tech_top->SetRoughness(0.3f); + AddGameObject(tech_top); + // Chrome pylons on top + for (int i = 0; i < 12; ++i) { + float a = (float)i / 12.0f * 2 * (float)M_PI; + float R = 22.0f; + float x = -90.0f + std::cos(a) * R; + float z = std::sin(a) * R; + auto p = CreateGameObject( + CreateCube(glm::vec3(0.96f)), + glm::vec3(x, 18.0f, z)); + p->SetScale(glm::vec3(0.8f, 8.0f, 0.8f)); + p->SetMetallic(1.0f); p->SetRoughness(0.15f); + AddGameObject(p); + } + // Giant glowing spire at center of tech + { + auto spire = CreateGameObject( + CreateCube(glm::vec3(0.95f)), + glm::vec3(-90.0f, 24.0f, 0)); + spire->SetScale(glm::vec3(1.2f, 20.0f, 1.2f)); + spire->SetMetallic(1.0f); spire->SetRoughness(0.1f); + spire->SetEmissive(glm::vec3(1.5f, 0.3f, 3.5f)); + AddGameObject(spire); + } + // Cyan neon bars scattered + for (int i = 0; i < 16; ++i) { + float x = -90.0f + rnd(-40.0f, 40.0f); + float z = rnd(-55.0f, 55.0f); + auto bar = CreateGameObject( + CreateCube(glm::vec3(1.0f)), + glm::vec3(x, 14.2f, z)); + bar->SetScale(glm::vec3(rnd(1.2f, 3.0f), 0.08f, 0.25f)); + bar->SetRotation(glm::vec3(0, jit(180.0f), 0)); + bar->SetEmissive(glm::vec3(0.15f, 2.8f, 3.2f)); + AddGameObject(bar); + } + + // --- Central volcano/lava pit (height column + glowing ground) --- + { + auto mouth = CreateGameObject( + CreateCone(5.0f, 3.0f, 16, glm::vec3(0.30f, 0.22f, 0.18f)), + glm::vec3(0, 1.5f, 0)); + mouth->SetTexture(rock_tex); mouth->SetNormalMap(noise_t); + mouth->SetRoughness(0.9f); + // Invert so it's a bowl? Easier — just use as volcano base + AddGameObject(mouth); + // Lava glow (flat disk inside) + auto lava = CreateGameObject( + CreateCylinder(3.0f, 0.15f, 32, glm::vec3(1.0f)), + glm::vec3(0, 2.2f, 0)); + lava->SetEmissive(glm::vec3(5.0f, 1.5f, 0.2f)); + AddGameObject(lava); + // Glow column rising + for (int i = 0; i < 5; ++i) { + auto smoke = CreateGameObject( + CreateSphere(12, glm::vec3(1.0f)), + glm::vec3(jit(0.5f), 3.0f + i * 2.0f, jit(0.5f))); + float s = 1.0f + i * 0.3f; + smoke->SetScale(glm::vec3(s, s, s)); + smoke->SetEmissive(glm::vec3(2.5f - i * 0.35f, 0.8f, 0.15f)); + AddGameObject(smoke); + } + } + + // --- Forest trees --- + auto tree = [&](glm::vec3 at, float s) { + auto trunk = CreateGameObject( + CreateCylinder(0.25f * s, 2.8f * s, 10, + glm::vec3(0.85f, 0.78f, 0.65f)), + at + glm::vec3(0, 1.4f * s, 0)); + trunk->SetRotation(glm::vec3(jit(4), jit(360), jit(4))); + trunk->SetTexture(wood_tex); trunk->SetNormalMap(noise_t); + trunk->SetRoughness(0.85f); + AddGameObject(trunk); + glm::vec3 cc(rnd(0.10f, 0.22f), rnd(0.38f, 0.55f), rnd(0.12f, 0.22f)); + auto cone = CreateGameObject( + CreateCone(1.4f * s, 3.2f * s, 10, cc), + at + glm::vec3(jit(0.15f), 3.2f * s, jit(0.15f))); + cone->SetNormalMap(noise_t); cone->SetRoughness(0.8f); + AddGameObject(cone); + }; + for (int i = 0; i < 55; ++i) { + float x = rnd(-70.0f, 70.0f); + float z = 30.0f + rnd(10.0f, 120.0f); + tree(glm::vec3(x, 0, z), rnd(0.8f, 1.4f)); + } + + // --- Central plaza pylons (so you orient from spawn) --- + for (int i = 0; i < 4; ++i) { + float a = (float)i / 4.0f * 2 * (float)M_PI + 0.25f; + float R = 14.0f; + auto p = CreateGameObject( + CreateCylinder(0.4f, 6.0f, 12, glm::vec3(0.90f)), + glm::vec3(std::cos(a) * R, 3.0f, std::sin(a) * R)); + p->SetMetallic(1.0f); p->SetRoughness(0.25f); + p->SetEmissive(glm::vec3(1.2f, 1.0f, 0.6f)); + AddGameObject(p); + } + + // --- Outer mountain ring --- + for (int i = 0; i < 32; ++i) { + float a = (float)i / 32.0f * 2 * (float)M_PI; + float R = 220.0f; + float sc = 1.0f + ((float)((i * 37) % 100) / 100.0f) * 0.5f; + float h = 32.0f * sc; + auto m = CreateGameObject( + CreateCone(15.0f * sc, h, 10, glm::vec3(0.22f, 0.20f, 0.24f)), + glm::vec3(std::cos(a) * R + jit(5.0f), h * 0.5f, std::sin(a) * R + jit(5.0f))); + m->SetRoughness(0.92f); + AddGameObject(m); + } + + // --- First-person arm --- + BuildArm(); + + // Camera starts facing north across forest + glm::vec3 eye(0, 3.0f, -3.0f); + SetCameraPosition(eye); + SetCameraTarget(glm::vec3(0, 3.0f, 10.0f)); + glm::vec3 fwd = glm::normalize(GetCameraTarget() - eye); + cam_pitch_ = glm::degrees(std::asin(fwd.y)); + cam_yaw_ = glm::degrees(std::atan2(fwd.z, fwd.x)); + last_cam_pos_ = eye; + + std::cout << "Odyssey ready: " << game_objects_.size() << " objects.\n" + << " WASD/arrows move+look, Space/LCtrl up/down, LShift sprint.\n" + << " 1-6 or Q/E cycle items F/LMB/R1 to throw.\n"; + return true; + } + + void BuildArm() { + // Upper arm (shoulder → elbow) + arm_upper_ = CreateGameObject( + CreateCylinder(0.10f, 0.45f, 10, glm::vec3(0.92f, 0.85f, 0.78f)), + glm::vec3(0)); + arm_upper_->SetRoughness(0.75f); + AddGameObject(arm_upper_); + // Forearm (elbow → wrist) + arm_lower_ = CreateGameObject( + CreateCylinder(0.085f, 0.45f, 10, glm::vec3(0.92f, 0.85f, 0.78f)), + glm::vec3(0)); + arm_lower_->SetRoughness(0.75f); + AddGameObject(arm_lower_); + // Hand (sphere) + hand_ = CreateGameObject( + CreateSphere(18, glm::vec3(0.92f, 0.85f, 0.78f)), + glm::vec3(0)); + hand_->SetScale(glm::vec3(0.11f)); + hand_->SetRoughness(0.7f); + AddGameObject(hand_); + arm_ready_ = true; + } + + void UpdateArm(float dt) { + if (!arm_ready_) return; + throw_anim_ = std::max(0.0f, throw_anim_ - dt * 4.0f); + // Build camera basis + glm::vec3 cp = GetCameraPosition(); + glm::vec3 tg = GetCameraTarget(); + glm::vec3 fwd = glm::normalize(tg - cp); + glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0))); + glm::vec3 up = glm::cross(right, fwd); + // Throw motion: forearm pitches forward then snaps back + float t = throw_anim_; + float forward_boost = t * 0.45f; + // Shoulder offset: to the right and slightly down from the eye + glm::vec3 shoulder = cp + right * 0.55f + up * -0.40f + fwd * 0.35f; + // Elbow: slightly forward + down + glm::vec3 elbow = shoulder + fwd * (0.18f + forward_boost * 0.4f) + + up * (-0.20f + t * 0.1f); + // Wrist: more forward, pitches during throw + glm::vec3 wrist = elbow + fwd * (0.35f + forward_boost) + + up * (-0.05f + t * 0.3f); + + auto midpoint = [](const glm::vec3& a, const glm::vec3& b) { + return (a + b) * 0.5f; + }; + auto yawPitch = [&](const glm::vec3& dir) { + glm::vec3 d = glm::normalize(dir); + float pitch = glm::degrees(std::asin(d.y)); + float yaw = glm::degrees(std::atan2(d.x, d.z)); + // Cylinder primitive's axis is +Y. We want to align +Y to 'dir'. + // Compose rotations: yaw around Y then tilt. + // Using the engine's Euler order (X, Y, Z degrees), a tilt along + // the axis of rotation is approximated by pitch-around-X. + return glm::vec3(-pitch - 90.0f, yaw, 0.0f); + }; + + glm::vec3 upperDir = elbow - shoulder; + glm::vec3 lowerDir = wrist - elbow; + arm_upper_->SetPosition(midpoint(shoulder, elbow)); + arm_upper_->SetRotation(yawPitch(upperDir)); + float ul = glm::length(upperDir); + arm_upper_->SetScale(glm::vec3(1.0f, ul / 0.45f, 1.0f)); + + arm_lower_->SetPosition(midpoint(elbow, wrist)); + arm_lower_->SetRotation(yawPitch(lowerDir)); + float ll = glm::length(lowerDir); + arm_lower_->SetScale(glm::vec3(1.0f, ll / 0.45f, 1.0f)); + + hand_->SetPosition(wrist); + // Color hand tint by currently-selected item so you know what you'll throw + hand_->SetEmissive(items_[selected_].emissive * 0.3f); + } + + void FireBall() { + const Item& it = items_[selected_]; + glm::vec3 cp = GetCameraPosition(); + glm::vec3 tg = GetCameraTarget(); + glm::vec3 fwd = glm::normalize(tg - cp); + glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0))); + glm::vec3 up = glm::cross(right, fwd); + glm::vec3 spawn = cp + right * 0.55f + up * -0.25f + fwd * 1.2f; + + if ((int)active_shots_.size() >= kMaxShots) { + auto old = active_shots_.front(); active_shots_.pop_front(); + processAgentCommand(json::parse("{\"action\":\"delete\",\"id\":\"" + old + "\"}")); + } + std::string id = "shot_" + std::to_string(shot_counter_++); + auto b = CreateGameObject(CreateSphere(20, it.color), spawn); + b->SetScale(glm::vec3(it.radius)); + b->SetMetallic(selected_ == 0 || selected_ == 5 ? 1.0f : 0.0f); + b->SetRoughness(selected_ == 0 ? 0.3f : 0.5f); + b->SetEmissive(it.emissive); + AddGameObject(b); + namedObjects_[id] = b; + + json::Object bc; + bc["action"]=json::Value(std::string("body")); + bc["id"]=json::Value(id); + bc["shape"]=json::Value(std::string("sphere")); + bc["mass"]=json::Value((double)it.mass); + bc["restitution"]=json::Value(0.5); + bc["friction"]=json::Value(0.3); + processAgentCommand(json::Value(bc)); + + json::Object vc; + vc["action"]=json::Value(std::string("velocity")); + vc["id"]=json::Value(id); + vc["linear"]=json::Value(json::Array{ + json::Value((double)(fwd.x * it.velocity)), + json::Value((double)(fwd.y * it.velocity)), + json::Value((double)(fwd.z * it.velocity))}); + processAgentCommand(json::Value(vc)); + active_shots_.push_back(id); + + throw_anim_ = 1.0f; + processAgentCommand(json::parse("{\"action\":\"physics\",\"enabled\":true}")); + } + + void OnUpdate(float dt) override { + // Hotbar select (1-6, Q/E cycle, gamepad L1/R1-ish — but R1 is shoot, + // so cycle with shoulder ls-click or D-pad) + int keys[6] = {GLFW_KEY_1, GLFW_KEY_2, GLFW_KEY_3, GLFW_KEY_4, + GLFW_KEY_5, GLFW_KEY_6}; + for (int i = 0; i < 6; ++i) if (Input::IsKeyPressed(keys[i])) selected_ = i; + bool q = Input::IsKeyHeld(GLFW_KEY_Q); + bool e = Input::IsKeyHeld(GLFW_KEY_E); + if (q && !q_prev_) selected_ = (selected_ + 5) % 6; + if (e && !e_prev_) selected_ = (selected_ + 1) % 6; + q_prev_ = q; e_prev_ = e; + + // Shoot + shot_cooldown_ -= dt; + if (shot_cooldown_ <= 0.0f) { + bool fire = Input::IsKeyHeld(GLFW_KEY_F) + || Input::IsMouseButtonPressed(Input::MouseButton::Left) + || Input::IsGamepadButtonDown(Input::GamepadButton::R1); + if (fire) { FireBall(); shot_cooldown_ = 0.18f; } + } + + // Free-fly camera (sync from engine, apply pan + look) + glm::vec3 eng_pos = GetCameraPosition(); + if (glm::length(eng_pos - last_cam_pos_) > 0.0001f) { + glm::vec3 fwd = GetCameraTarget() - eng_pos; + if (glm::length(fwd) > 0.0001f) { + fwd = glm::normalize(fwd); + cam_pitch_ = glm::degrees(std::asin(fwd.y)); + cam_yaw_ = glm::degrees(std::atan2(fwd.z, fwd.x)); + } + } + glm::vec2 ls{0}, rs{0}; + float up = 0, down = 0, speed = speed_; + bool touched = false; + if (Input::IsGamepadConnected()) { + ls = Input::GetLeftStick(); + rs = Input::GetRightStick(); + up = Input::GetR2(); + down = Input::GetL2(); + if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross)) speed *= 3.0f; + if (glm::length(ls) + glm::length(rs) + up + down > 0.01f) touched = true; + } + if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT)) speed *= 3.0f; + if (Input::IsKeyHeld(GLFW_KEY_W)) { ls.y -= 1.0f; touched = true; } + if (Input::IsKeyHeld(GLFW_KEY_S)) { ls.y += 1.0f; touched = true; } + if (Input::IsKeyHeld(GLFW_KEY_A)) { ls.x -= 1.0f; touched = true; } + if (Input::IsKeyHeld(GLFW_KEY_D)) { ls.x += 1.0f; touched = true; } + if (Input::IsKeyHeld(GLFW_KEY_LEFT)) { rs.x -= 1.0f; touched = true; } + if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) { rs.x += 1.0f; touched = true; } + if (Input::IsKeyHeld(GLFW_KEY_UP)) { rs.y -= 1.0f; touched = true; } + if (Input::IsKeyHeld(GLFW_KEY_DOWN)) { rs.y += 1.0f; touched = true; } + if (Input::IsKeyHeld(GLFW_KEY_SPACE)) { up += 1.0f; touched = true; } + if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL)) { down += 1.0f; touched = true; } + if (touched) { + cam_yaw_ += rs.x * look_sens_ * dt; + cam_pitch_ -= rs.y * look_sens_ * dt; + cam_pitch_ = std::clamp(cam_pitch_, -89.0f, 89.0f); + glm::vec3 fwd; + fwd.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_)); + fwd.y = std::sin(glm::radians(cam_pitch_)); + fwd.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_)); + fwd = glm::normalize(fwd); + glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0))); + glm::vec3 pos = eng_pos; + pos += fwd * (-ls.y) * speed * dt; + pos += right * ls.x * speed * dt; + pos.y += (up - down) * speed * dt; + SetCameraPosition(pos); + SetCameraTarget(pos + fwd); + last_cam_pos_ = pos; + } else { + last_cam_pos_ = eng_pos; + } + + UpdateArm(dt); + } + + void OnRenderOverlay() override { + Hud hud(font_, width_, height_); + + // --- Hotbar: 6 slots centered at bottom --- + int N = 6; + float slot_w = 84.0f, slot_h = 84.0f; + float pad = 8.0f; + float total_w = N * slot_w + (N - 1) * pad; + float start_x = (width_ - total_w) * 0.5f; + float y = 18.0f; + for (int i = 0; i < N; ++i) { + float x = start_x + i * (slot_w + pad); + const Item& it = items_[i]; + bool sel = (i == selected_); + glm::vec4 bg(0.03f, 0.04f, 0.07f, 0.8f); + glm::vec4 br = sel ? glm::vec4(it.color, 1.0f) + : glm::vec4(0.8f, 0.8f, 0.8f, 0.35f); + float bt = sel ? 3.0f : 1.5f; + hud.beginPanel(x, y, slot_w, slot_h, bg, br, bt); + hud.drawPanelBackgroundNow(); + // Item icon: colored rect (emissive tint applied via color mix) + glm::vec3 ic = it.color + it.emissive * 0.2f; + hud.rect(x + 14, y + 26, slot_w - 28, slot_h - 44, + glm::vec4(ic, 1.0f)); + // Number badge (top-left) and label (bottom) + char num[4]; std::snprintf(num, sizeof(num), "%d", i + 1); + hud.text(x + 8, y + slot_h - 18, num, 14.0f, + glm::vec3(1, 1, 1), true); + hud.text(x + 10, y + 7, it.label, 12.0f, + glm::vec3(1, 1, 1), true); + hud.endPanel(); + } + + // Controls hint (top-left) + hud.beginPanel(14, (float)height_ - 104, 440, 88, + glm::vec4(0.02f, 0.03f, 0.05f, 0.7f), + glm::vec4(0.5f, 0.8f, 0.9f, 0.5f), 2.0f); + hud.drawPanelBackgroundNow(); + hud.label("ODYSSEY", 18.0f, glm::vec3(0.8f, 0.95f, 1.0f), true); + hud.separator(); + hud.label("WASD MOVE SPACE/LCTRL FLY SHIFT SPRINT", 13.0f, + glm::vec3(0.85f, 0.9f, 0.85f)); + hud.label("F OR LMB THROW 1-6 OR Q/E SELECT ITEM", 13.0f, + glm::vec3(0.85f, 0.9f, 0.85f)); + hud.endPanel(); + + // Crosshair at center + { + float cx = width_ * 0.5f, cy = height_ * 0.5f; + hud.rect(cx - 10, cy - 1, 7, 2, glm::vec4(1, 1, 1, 0.7f)); + hud.rect(cx + 3, cy - 1, 7, 2, glm::vec4(1, 1, 1, 0.7f)); + hud.rect(cx - 1, cy - 10, 2, 7, glm::vec4(1, 1, 1, 0.7f)); + hud.rect(cx - 1, cy + 3, 2, 7, glm::vec4(1, 1, 1, 0.7f)); + } + + hud.flush(); + } +}; + + +// --------------------------------------------- +// BuildersDemo — top-down "business village" foundation. +// - Camera: fixed-pitch bird's-eye, WASD pans, scroll/+/- zooms +// - Left-click on ground to place a building at the cursor +// - Keys 1-4 pick which building TYPE to place next +// - Each building spawns 3 animated "workers" that wander around it +// - Each building has a business idea (displayed above it) +// - Mock worker messages post periodically to a corner log (placeholder +// for Claude-backed task output in the next round) +// - Y key: cycle which building's messages show in detail +// --------------------------------------------- +class BuildersDemo : public Engine { + struct Worker { + glm::vec3 pos; + glm::vec3 target; + float speed; + std::shared_ptr obj; + std::shared_ptr head; // tiny sphere on top + }; + struct Building { + int type_idx; + std::string business; + glm::vec3 pos; + std::shared_ptr box; + std::shared_ptr roof; + std::vector workers; + float next_message_in = 2.0f; + }; + + std::vector buildings_; + std::deque message_log_; // last N messages + static constexpr int kMaxMessages = 12; + int active_btype_ = 0; + bool mouse_prev_down_ = false; + float cam_x_ = 0.0f, cam_z_ = 0.0f; + float cam_height_ = 40.0f; // zoom via scroll + float cam_yaw_ = 0.0f; // 0 = camera facing +z (straight down-forward) + float cam_pitch_frac_ = 0.35f; // how far the target is offset forward + float pan_speed_ = 30.0f; + float place_cooldown_ = 0.0f; + float map_radius_ = 90.0f; // soft boundary; camera clamped to this circle + bool gp_sel_prev_[4] = {false,false,false,false}; + bool gp_place_prev_ = false; + std::mt19937 rng_{31415}; + + // Business templates — 4 types with distinct color + prompt seed + struct BType { + const char* label; + const char* example_business; + glm::vec3 roof_color; // emissive + glm::vec3 wall_color; + }; + const BType kBTypes[4] = { + {"Tech Startup", "SaaS dashboard for mobile analytics", + glm::vec3(0.4f, 1.6f, 2.4f), glm::vec3(0.85f, 0.88f, 0.95f)}, + {"Design Studio", "Brand identity for indie games", + glm::vec3(2.4f, 0.7f, 1.6f), glm::vec3(0.95f, 0.82f, 0.85f)}, + {"Research Lab", "Material science for EV batteries", + glm::vec3(0.8f, 2.4f, 2.0f), glm::vec3(0.78f, 0.95f, 0.92f)}, + {"Workshop", "Custom handcrafted furniture", + glm::vec3(2.4f, 1.4f, 0.5f), glm::vec3(0.92f, 0.80f, 0.60f)}, + }; + +public: + BuildersDemo() + : Engine(1920, 1080, "ZoomEngine - Builders (top-down)") {} + + bool OnInitialize() override { + // Overcast flat lighting — reads top-down well, minimal harsh shadows + processAgentCommand(json::parse(R"({ + "action":"sun", + "direction":[-0.25, 0.95, -0.15], + "color":[1.85, 1.80, 1.70], + "zenith":[0.45, 0.55, 0.70], + "horizon":[0.85, 0.85, 0.82], + "sky":[0.72, 0.76, 0.80], + "ground":[0.10, 0.11, 0.12], + "shadow_extent":100, + "exposure":0.95, + "bloom_threshold":1.25, + "bloom_intensity":1.0, + "fog_density":0.002 + })")); + processAgentCommand(json::parse(R"({"action":"water","enabled":false})")); + + auto rnd = [&](float lo, float hi) { + std::uniform_real_distribution d(lo, hi); return d(rng_); + }; + auto jit = [&](float s) { return rnd(-s, s); }; + + GLuint grass_tex = TextureCache::Get("grass"); + GLuint dirt_t = TextureCache::Get("dirt"); + GLuint rock_tex = TextureCache::Get("rock"); + GLuint concrete = TextureCache::Get("concrete"); + GLuint stripes_t = TextureCache::Get("stripes"); + GLuint marble_t = TextureCache::Get("marble"); + GLuint wood_tex = TextureCache::Get("wood"); + GLuint noise_t = TextureCache::Get("noise"); + + // --- Varied terrain: 3x3 tiles (60 units each) with different + // texture + color palettes so zones read distinctly from above. + struct Tile { GLuint tex; glm::vec3 col; float uv; float rough; }; + const Tile tiles[9] = { + { grass_tex, glm::vec3(0.75f, 0.90f, 0.70f), 0.20f, 0.95f }, // NW green + { dirt_t, glm::vec3(0.92f, 0.80f, 0.55f), 0.25f, 0.95f }, // N dune + { grass_tex, glm::vec3(0.70f, 0.85f, 0.75f), 0.22f, 0.95f }, // NE green-cool + { rock_tex, glm::vec3(0.78f, 0.76f, 0.72f), 0.22f, 0.85f }, // W rocky + { marble_t, glm::vec3(0.92f, 0.90f, 0.85f), 0.20f, 0.55f }, // C plaza + { grass_tex, glm::vec3(0.80f, 0.88f, 0.78f), 0.20f, 0.92f }, // E grass + { stripes_t, glm::vec3(0.78f, 0.85f, 0.75f), 0.15f, 0.90f }, // SW field + { grass_tex, glm::vec3(0.78f, 0.92f, 0.72f), 0.22f, 0.92f }, // S grass + { dirt_t, glm::vec3(0.85f, 0.76f, 0.55f), 0.25f, 0.95f }, // SE dirt + }; + const float kTile = 60.0f; + for (int j = 0; j < 3; ++j) { + for (int i = 0; i < 3; ++i) { + int idx = j * 3 + i; + float x = (i - 1) * kTile; + float z = (j - 1) * kTile; + auto t = CreateGameObject( + CreatePlane(kTile, kTile, tiles[idx].col), + glm::vec3(x, 0.0f, z)); + t->SetTexture(tiles[idx].tex); + t->SetNormalMap(noise_t); + t->SetUVScale(glm::vec2(tiles[idx].uv)); + t->SetRoughness(tiles[idx].rough); + AddGameObject(t); + } + } + + // --- Central plaza rings (unchanged visual focus) --- + for (int i = 1; i <= 3; ++i) { + auto ring = CreateGameObject( + CreateCylinder((float)i * 4.0f, 0.06f, 48, + glm::vec3(0.94f, 0.92f, 0.88f)), + glm::vec3(0, 0.015f, 0)); + ring->SetTexture(marble_t); ring->SetNormalMap(noise_t); + ring->SetRoughness(0.45f); + AddGameObject(ring); + } + + // --- Map border: DENSE ring of trees inside the soft cam bound --- + // Two rows of trees so it reads as a real wall, not a thin fence. + auto border_tree = [&](float x, float z, float s, float rough = 0.85f) { + auto trunk = CreateGameObject( + CreateCylinder(0.30f * s, 2.8f * s, 10, + glm::vec3(0.80f, 0.72f, 0.60f)), + glm::vec3(x, 1.4f * s, z)); + trunk->SetRotation(glm::vec3(jit(3), jit(360), jit(3))); + trunk->SetTexture(wood_tex); trunk->SetNormalMap(noise_t); + trunk->SetRoughness(rough); + AddGameObject(trunk); + glm::vec3 cc(rnd(0.10f, 0.22f), rnd(0.38f, 0.55f), rnd(0.12f, 0.22f)); + auto cone = CreateGameObject( + CreateCone(1.8f * s, 4.2f * s, 10, cc), + glm::vec3(x + jit(0.15f), 3.2f * s, z + jit(0.15f))); + cone->SetNormalMap(noise_t); cone->SetRoughness(0.8f); + AddGameObject(cone); + }; + const float border_r_inner = 94.0f; + const float border_r_outer = 100.0f; + for (int i = 0; i < 48; ++i) { + float a = (float)i / 48.0f * 2 * (float)M_PI + jit(0.02f); + float s = rnd(1.0f, 1.6f); + border_tree(std::cos(a) * border_r_inner + jit(2.0f), + std::sin(a) * border_r_inner + jit(2.0f), + s); + } + for (int i = 0; i < 40; ++i) { + float a = (float)i / 40.0f * 2 * (float)M_PI + 0.065f; + float s = rnd(1.1f, 1.7f); + border_tree(std::cos(a) * border_r_outer + jit(2.5f), + std::sin(a) * border_r_outer + jit(2.5f), + s); + } + + // --- Outer mountain ring (backdrop + final boundary) --- + for (int i = 0; i < 28; ++i) { + float a = (float)i / 28.0f * 2 * (float)M_PI + jit(0.05f); + float R = 160.0f + rnd(-10.0f, 15.0f); + float sc = 1.0f + ((float)((i * 47) % 100) / 100.0f) * 0.6f; + float h = 30.0f * sc; + auto m = CreateGameObject( + CreateCone(15.0f * sc, h, 10, glm::vec3(0.22f, 0.21f, 0.24f)), + glm::vec3(std::cos(a) * R, h * 0.5f, std::sin(a) * R)); + m->SetRoughness(0.9f); + AddGameObject(m); + } + + // --- A few scattered trees inside the play area for variety --- + for (int i = 0; i < 18; ++i) { + float x = rnd(-80.0f, 80.0f); + float z = rnd(-80.0f, 80.0f); + // avoid the central plaza / spawn area + if (std::abs(x) < 16.0f && std::abs(z) < 16.0f) continue; + border_tree(x, z, rnd(0.7f, 1.2f)); + } + + // Initial log message + AddMessage("[System] BuildersDemo ready. Click ground to place a building."); + AddMessage("[System] Keys 1-4: select building type. F or LMB: place."); + + // Initial camera: top-down with slight pitch so mouse picking works + cam_x_ = 0.0f; cam_z_ = 0.0f; + UpdateCamera(); + return true; + } + + void UpdateCamera() { + // High-angle view that rotates around the target. Yaw determines the + // horizontal direction the camera is pushed from, so "forward" for + // the player is toward +Z in view-space. + float cy = std::cos(glm::radians(cam_yaw_)); + float sy = std::sin(glm::radians(cam_yaw_)); + float back = cam_height_ * cam_pitch_frac_; + glm::vec3 eye(cam_x_ + sy * back, cam_height_, cam_z_ + cy * back); + glm::vec3 tgt(cam_x_, 0.0f, cam_z_); + SetCameraPosition(eye); + SetCameraTarget(tgt); + } + + void AddMessage(const std::string& msg) { + message_log_.push_back(msg); + while ((int)message_log_.size() > kMaxMessages) message_log_.pop_front(); + std::cout << msg << "\n"; + } + + void PlaceBuilding(glm::vec3 pos) { + // Check it's not too close to another building + for (auto& b : buildings_) { + if (glm::length(glm::vec2(b.pos.x - pos.x, b.pos.z - pos.z)) < 5.0f) { + AddMessage("[System] Too close to another building — skipped."); + return; + } + } + Building b; + b.type_idx = active_btype_; + b.business = kBTypes[active_btype_].example_business; + b.pos = glm::vec3(pos.x, 0, pos.z); + + // Walls + auto box = CreateGameObject( + CreateCube(kBTypes[active_btype_].wall_color), + glm::vec3(pos.x, 2.0f, pos.z)); + box->SetScale(glm::vec3(3.0f, 4.0f, 3.0f)); + box->SetTexture(TextureCache::Get("concrete")); + box->SetNormalMap(TextureCache::Get("noise")); + box->SetRoughness(0.75f); + AddGameObject(box); + b.box = box; + + // Roof — emissive so it glows with type color + auto roof = CreateGameObject( + CreateCube(glm::vec3(1.0f)), + glm::vec3(pos.x, 4.2f, pos.z)); + roof->SetScale(glm::vec3(3.3f, 0.4f, 3.3f)); + roof->SetMetallic(0.3f); + roof->SetRoughness(0.35f); + roof->SetEmissive(kBTypes[active_btype_].roof_color); + AddGameObject(roof); + b.roof = roof; + + // 3 workers around the building + std::uniform_real_distribution ang(0.0f, 2 * M_PI); + for (int i = 0; i < 3; ++i) { + Worker w; + float a = ang(rng_); + w.pos = glm::vec3(pos.x + std::cos(a) * 2.2f, + 0.0f, + pos.z + std::sin(a) * 2.2f); + w.target = w.pos; + w.speed = 1.5f + (rng_() % 10) * 0.1f; + // Body + auto body = CreateGameObject( + CreateCylinder(0.22f, 0.9f, 10, + glm::vec3(0.95f, 0.95f, 0.95f)), + glm::vec3(w.pos.x, 0.45f, w.pos.z)); + body->SetMetallic(0.0f); body->SetRoughness(0.6f); + AddGameObject(body); + w.obj = body; + // Head (emissive to match building color) + auto head = CreateGameObject( + CreateSphere(12, glm::vec3(1.0f)), + glm::vec3(w.pos.x, 1.05f, w.pos.z)); + head->SetScale(glm::vec3(0.25f)); + head->SetEmissive(kBTypes[active_btype_].roof_color * 0.6f); + AddGameObject(head); + w.head = head; + b.workers.push_back(w); + } + buildings_.push_back(std::move(b)); + AddMessage(std::string("[") + kBTypes[active_btype_].label + "] Building #" + + std::to_string((int)buildings_.size()) + + " opened. Business: " + kBTypes[active_btype_].example_business); + } + + void StepWorkers(float dt) { + std::uniform_real_distribution offs(-2.4f, 2.4f); + for (auto& b : buildings_) { + for (auto& w : b.workers) { + glm::vec3 to = w.target - w.pos; + float d = glm::length(to); + if (d < 0.05f) { + // Pick new random target within building's zone + w.target = b.pos + glm::vec3(offs(rng_), 0, offs(rng_)); + } else { + glm::vec3 step = (to / d) * w.speed * dt; + if (glm::length(step) > d) step = to; + w.pos += step; + w.obj->SetPosition(glm::vec3(w.pos.x, 0.45f, w.pos.z)); + w.head->SetPosition(glm::vec3(w.pos.x, 1.05f, w.pos.z)); + // Simple bob to suggest walking + float bob = 0.04f * std::sin((float)glfwGetTime() * 6.0f + + w.speed * 2.0f); + w.obj->SetPosition(glm::vec3(w.pos.x, 0.45f + bob, w.pos.z)); + w.head->SetPosition(glm::vec3(w.pos.x, 1.05f + bob, w.pos.z)); + } + } + } + } + + void TickMessages(float dt) { + // Each building periodically posts a mock "work result" — in V2 + // this is replaced by a real Claude API call driven by a helper. + static const std::array, 4> templates = {{ + { "drafted a wireframe for the onboarding flow", + "committed a PR with auth refactors", + "reviewed metrics, CTR up 3.2%", + "spiked a new analytics aggregation", + "shipped a bug fix for chart rendering" }, + { "pitched a new logo direction", + "uploaded four mood-board options", + "iterated on the brand color system", + "finalized the key art for Chapter 2", + "drafted marketing copy for the press kit" }, + { "ran an overnight electrolyte viscosity test", + "calibrated the coin-cell jig", + "drafted a technical note on dendrite growth", + "annotated the SEM images from batch 7", + "updated the doping-ratio ablation chart" }, + { "milled a new maple chair leg set", + "sanded the walnut desk to 600-grit", + "applied the first oil coat to the bookshelf", + "sketched a new modular shelving concept", + "shipped the dining-table order to the client" }, + }}; + for (auto& b : buildings_) { + b.next_message_in -= dt; + if (b.next_message_in <= 0.0f) { + b.next_message_in = 4.0f + (rng_() % 40) / 10.0f; + int widx = rng_() % (int)b.workers.size(); + const auto& tmpls = templates[b.type_idx]; + int tidx = rng_() % (int)tmpls.size(); + std::ostringstream ss; + ss << "[" << kBTypes[b.type_idx].label << "#" + << (&b - &buildings_[0] + 1) + << "] Worker " << (widx + 1) << ": " << tmpls[tidx]; + AddMessage(ss.str()); + } + } + } + + void OnUpdate(float dt) override { + // -------- Gamepad pre-sample -------- + glm::vec2 ls(0), rs(0); + float gp_in = 0.0f, gp_out = 0.0f; + bool gp_sprint = false; + bool gp_place = false; + bool gp_pick[4] = {false,false,false,false}; + if (Input::IsGamepadConnected()) { + ls = Input::GetLeftStick(); + rs = Input::GetRightStick(); + gp_out = Input::GetR2(); // zoom out (raise camera) + gp_in = Input::GetL2(); // zoom in + gp_sprint = Input::IsGamepadButtonDown(Input::GamepadButton::Cross); + gp_place = Input::IsGamepadButtonDown(Input::GamepadButton::R1); + gp_pick[0] = Input::IsGamepadButtonDown(Input::GamepadButton::Square); + gp_pick[1] = Input::IsGamepadButtonDown(Input::GamepadButton::Triangle); + gp_pick[2] = Input::IsGamepadButtonDown(Input::GamepadButton::Circle); + // Cross is used for sprint; use D-pad down / Cross only if others + // are rarely pressed. Treat D-pad as fallback. For V1 just skip + // a 4th gamepad type-select slot (fine — keyboard has 1-4). + } + + // -------- Yaw (camera rotation) -------- + float yaw_rate = 80.0f; // deg/sec + if (Input::IsKeyHeld(GLFW_KEY_Q)) cam_yaw_ -= yaw_rate * dt; + if (Input::IsKeyHeld(GLFW_KEY_E)) cam_yaw_ += yaw_rate * dt; + if (Input::IsKeyHeld(GLFW_KEY_LEFT)) cam_yaw_ -= yaw_rate * dt; + if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) cam_yaw_ += yaw_rate * dt; + cam_yaw_ += rs.x * yaw_rate * dt; // gamepad right stick X + while (cam_yaw_ > 360.0f) cam_yaw_ -= 360.0f; + while (cam_yaw_ < -360.0f) cam_yaw_ += 360.0f; + + // -------- Pan (keyboard + gamepad left stick), relative to yaw -------- + float speed = pan_speed_; + if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT) || gp_sprint) speed *= 2.5f; + float ix = 0.0f, iz = 0.0f; + if (Input::IsKeyHeld(GLFW_KEY_W)) iz -= 1.0f; + if (Input::IsKeyHeld(GLFW_KEY_S)) iz += 1.0f; + if (Input::IsKeyHeld(GLFW_KEY_A)) ix -= 1.0f; + if (Input::IsKeyHeld(GLFW_KEY_D)) ix += 1.0f; + ix += ls.x; iz += ls.y; + // Rotate pan vector by yaw so "forward" tracks camera direction + float cy = std::cos(glm::radians(cam_yaw_)); + float sy = std::sin(glm::radians(cam_yaw_)); + float wx = ix * cy + iz * sy; + float wz = -ix * sy + iz * cy; + cam_x_ += wx * speed * dt; + cam_z_ += wz * speed * dt; + + // Zoom (keyboard +/-, gamepad R2/L2, up/down arrows) + if (Input::IsKeyHeld(GLFW_KEY_EQUAL) || gp_in > 0.05f) + cam_height_ = std::max(12.0f, cam_height_ - (15.0f + gp_in * 25.0f) * dt); + if (Input::IsKeyHeld(GLFW_KEY_MINUS) || gp_out > 0.05f) + cam_height_ = std::min(130.0f, cam_height_ + (15.0f + gp_out * 25.0f) * dt); + if (Input::IsKeyHeld(GLFW_KEY_UP)) cam_height_ = std::max(12.0f, cam_height_ - 18.0f * dt); + if (Input::IsKeyHeld(GLFW_KEY_DOWN)) cam_height_ = std::min(130.0f, cam_height_ + 18.0f * dt); + + // Soft circular clamp to keep camera from leaving the map + { + float r = std::sqrt(cam_x_ * cam_x_ + cam_z_ * cam_z_); + if (r > map_radius_) { + float k = map_radius_ / r; + cam_x_ *= k; + cam_z_ *= k; + } + } + UpdateCamera(); + + // -------- Building type select (keyboard + gamepad face buttons edge) -------- + if (Input::IsKeyPressed(GLFW_KEY_1)) { active_btype_ = 0; AddMessage("[System] Placing: Tech Startup"); } + if (Input::IsKeyPressed(GLFW_KEY_2)) { active_btype_ = 1; AddMessage("[System] Placing: Design Studio"); } + if (Input::IsKeyPressed(GLFW_KEY_3)) { active_btype_ = 2; AddMessage("[System] Placing: Research Lab"); } + if (Input::IsKeyPressed(GLFW_KEY_4)) { active_btype_ = 3; AddMessage("[System] Placing: Workshop"); } + for (int i = 0; i < 3; ++i) { + if (gp_pick[i] && !gp_sel_prev_[i]) { + active_btype_ = i; + const char* label = (i == 0 ? "Tech Startup" : + i == 1 ? "Design Studio" : "Research Lab"); + AddMessage(std::string("[System] Placing: ") + label); + } + gp_sel_prev_[i] = gp_pick[i]; + } + + // -------- Place building (mouse/F/R1, edge-triggered) -------- + place_cooldown_ -= dt; + bool mouse_now = Input::IsMouseButtonPressed(Input::MouseButton::Left); + bool place = (mouse_now && !mouse_prev_down_) + || Input::IsKeyPressed(GLFW_KEY_F) + || (gp_place && !gp_place_prev_); + mouse_prev_down_ = mouse_now; + gp_place_prev_ = gp_place; + if (place && place_cooldown_ <= 0.0f) { + // With gamepad + no mouse cursor, just drop at the camera target. + glm::vec3 hit = gp_place + ? glm::vec3(cam_x_, 0.0f, cam_z_) + : ScreenToGround(); + if (std::abs(hit.x) + std::abs(hit.z) > 0.001f || + (gp_place && (cam_x_ != 0 || cam_z_ != 0))) { + PlaceBuilding(hit); + place_cooldown_ = 0.25f; + } + } + + StepWorkers(dt); + TickMessages(dt); + } + + void OnRenderOverlay() override { + Hud hud(font_, width_, height_); + + // ====== TOP-LEFT: Build panel ====== + const BType& bt = kBTypes[active_btype_]; + hud.beginPanel(14, (float)height_ - 210, 480, 182, + glm::vec4(0.02f, 0.03f, 0.05f, 0.75f), + glm::vec4(bt.roof_color, 0.75f), + 2.5f); + hud.drawPanelBackgroundNow(); + hud.icon(14, 14, glm::vec4(bt.roof_color, 1.0f)); + hud.label(std::string("PLACING: ") + bt.label, 22.0f, + glm::vec3(1, 1, 1), true); + hud.label(bt.example_business, 16.0f, + glm::vec3(0.85f, 0.95f, 0.9f)); + hud.separator(); + hud.label("[1][2][3][4] type LMB/F place", 14.0f, + glm::vec3(0.7f, 0.85f, 0.75f)); + hud.label("WASD pan +/- zoom", 14.0f, + glm::vec3(0.7f, 0.85f, 0.75f)); + hud.endPanel(); + + // ====== TOP-RIGHT: Stats panel ====== + hud.beginPanel((float)width_ - 320, (float)height_ - 156, 300, 128, + glm::vec4(0.02f, 0.03f, 0.05f, 0.75f), + glm::vec4(0.5f, 0.75f, 0.9f, 0.6f), + 2.5f); + hud.drawPanelBackgroundNow(); + hud.label("VILLAGE STATS", 19.0f, glm::vec3(0.6f, 0.85f, 1.0f), true); + hud.separator(); + char buf[128]; + std::snprintf(buf, sizeof(buf), "Buildings: %d", (int)buildings_.size()); + hud.label(buf, 17.0f, glm::vec3(1, 1, 1)); + std::snprintf(buf, sizeof(buf), "Workers: %d", (int)(buildings_.size() * 3)); + hud.label(buf, 17.0f, glm::vec3(1, 1, 1)); + hud.endPanel(); + + // ====== BOTTOM-LEFT: Message log ====== + float log_h = std::min(360.0f, 48.0f + (float)message_log_.size() * 22.0f); + hud.beginPanel(14, 14, 720, log_h, + glm::vec4(0.02f, 0.03f, 0.05f, 0.75f), + glm::vec4(0.9f, 0.9f, 0.9f, 0.35f), + 2.0f); + hud.drawPanelBackgroundNow(); + hud.label("AGENT LOG", 16.0f, glm::vec3(0.7f, 0.9f, 1.0f), true); + hud.separator(); + int count = (int)message_log_.size(); + int shown = std::min(count, 10); + for (int i = count - 1; i >= std::max(0, count - shown); --i) { + float a = 0.45f + 0.55f * float(i + 1 - (count - shown)) / float(shown); + hud.label(message_log_[i], 14.0f, + glm::vec3(1.0f, 1.0f, 1.0f) * a); + } + hud.endPanel(); + + // ====== Per-building floating labels + capacity bars ====== + glm::mat4 view = glm::lookAt(camera_position_, camera_target_, camera_up_); + glm::mat4 proj = glm::perspective(glm::radians(settings_.fov), + float(width_) / height_, 0.1f, settings_.drawDistance); + glm::mat4 vp = proj * view; + for (int i = 0; i < (int)buildings_.size(); ++i) { + const Building& b = buildings_[i]; + glm::vec4 clip = vp * glm::vec4(b.pos.x, 6.0f, b.pos.z, 1.0f); + if (clip.w <= 0.01f) continue; + glm::vec3 ndc = glm::vec3(clip) / clip.w; + if (std::abs(ndc.x) > 1.2f || std::abs(ndc.y) > 1.2f) continue; + float sx = (ndc.x * 0.5f + 0.5f) * width_; + float sy = (ndc.y * 0.5f + 0.5f) * height_; + const BType& bt2 = kBTypes[b.type_idx]; + hud.rect(sx - 70, sy + 6, 140, 22, + glm::vec4(0.0f, 0.0f, 0.0f, 0.6f)); + hud.rect(sx - 70, sy + 28, 140, 2, glm::vec4(bt2.roof_color, 0.9f)); + hud.text(sx - 64, sy + 11, + std::string(bt2.label) + " #" + std::to_string(i + 1), + 12.0f, glm::vec3(1.0f), true); + } + + hud.flush(); + } +}; + + +// --------------------------------------------- +// FourMapDemo — large four-quadrant themed map (160×160). +// NW Forest — mushrooms, conifers, moss rocks, fallen logs +// NE Sakura — cherry groves, stone lanterns, torii, blossoms +// SW Desert — cacti, dunes, step pyramid, obelisk, sand rocks +// SE Tech — chrome pylons, neon bars, crate pyramids, glowing monolith +// CENTER — sunken physics pool (buoyancy only inside the 6-unit radius) +// +// Zone atmospheres blend smoothly as the camera moves: sun color, sky, +// bloom threshold/intensity, fog density — each zone has its own recipe. +// +// F / LMB / R1 — shoot HDR metal ball +// --------------------------------------------- +class FourMapDemo : public Engine { + float cam_yaw_ = 225.0f; + float cam_pitch_ = -10.0f; + float look_sensitivity_ = 80.0f; + glm::vec3 last_cam_pos_{0}; + + // Terrain params — heights for sakura (NE) and desert (SW) quadrants + static constexpr float kSakuraMaxH = 3.2f; + static constexpr float kDesertMaxH = 2.6f; + static constexpr float kTerrainFalloff = 8.0f; + static constexpr float kMapHalf = 78.0f; + + // Walking character + static constexpr float kEyeH = 1.72f; + static constexpr float kWalkSpeed = 6.5f; + static constexpr float kRunMult = 2.2f; + static constexpr float kJumpV = 7.5f; + static constexpr float kGravity = 20.0f; + static constexpr float kStepUp = 1.4f; // max ground-height rise per move allowed + + float vy_ = 0.0f; + bool grounded_ = true; + bool fly_mode_ = false; + bool fly_prev_ = false; + bool jump_prev_ = false; + + // First-person arm + std::shared_ptr arm_upper_, arm_lower_, hand_; + bool arm_ready_ = false; + float throw_anim_ = 0.0f; + + float shot_cooldown_ = 0.0f; + int shot_counter_ = 0; + std::deque active_shots_; + static constexpr int kMaxShots = 80; + std::mt19937 rng_{2026}; + +public: + FourMapDemo() + : Engine(1920, 1080, "ZoomEngine - Four-Quadrant Map") {} + + // Sample the ground height used by the mesh generator in each quadrant. + // Returns 0 outside the sakura/desert zones, tapered near quadrant edges. + static float SampleTerrainH(float wx, float wz) { + float mh = 0.0f, lx = 0.0f, lz = 0.0f; + bool inside = false; + // Sakura NE centered at (40, 40), local 80x80 + if (wx >= 0.0f && wz >= 0.0f && wx <= 80.0f && wz <= 80.0f) { + mh = kSakuraMaxH; lx = wx - 40.0f; lz = wz - 40.0f; inside = true; + } + // Desert SW centered at (-40, -40) + else if (wx <= 0.0f && wz <= 0.0f && wx >= -80.0f && wz >= -80.0f) { + mh = kDesertMaxH; lx = wx + 40.0f; lz = wz + 40.0f; inside = true; + } + if (!inside) return 0.0f; + float h = std::sin(lx * 0.1f) * std::cos(lz * 0.1f) * mh * 0.5f; + h += std::sin(lx * 0.25f + 1.3f) * std::cos(lz * 0.3f + 0.7f) * mh * 0.25f; + h += std::sin(lx * 0.6f + 2.1f) * std::cos(lz * 0.5f + 1.5f) * mh * 0.125f; + // Match the edge falloff applied in TerrainPrimitive + float dx_edge = 40.0f - std::fabs(lx); + float dz_edge = 40.0f - std::fabs(lz); + float d = std::min(dx_edge, dz_edge); + float f = std::clamp(d / kTerrainFalloff, 0.0f, 1.0f); + f = f * f * (3.0f - 2.0f * f); + return h * f; + } + + void bodyCmd(const std::string& id, const std::string& shape, + float mass, float rest, float frict, bool is_static = false) { + json::Object c; + c["action"]=json::Value(std::string("body")); + c["id"]=json::Value(id); + c["shape"]=json::Value(shape); + c["mass"]=json::Value((double)mass); + c["restitution"]=json::Value((double)rest); + c["friction"]=json::Value((double)frict); + if (is_static) c["static"]=json::Value(true); + processAgentCommand(json::Value(c)); + } + + std::shared_ptr + addBox(const std::string& id, glm::vec3 pos, glm::vec3 scale, + glm::vec3 color, GLuint tex, GLuint nmap, + float metallic, float roughness, float mass, + float rest = 0.2f, float frict = 0.6f, + glm::vec3 rot = glm::vec3(0), glm::vec3 emissive = glm::vec3(0)) { + auto obj = CreateGameObject(CreateCube(color), pos); + obj->SetScale(scale); + obj->SetRotation(rot); + if (tex) obj->SetTexture(tex); + if (nmap) obj->SetNormalMap(nmap); + obj->SetMetallic(metallic); + obj->SetRoughness(roughness); + if (glm::length(emissive) > 0) obj->SetEmissive(emissive); + AddGameObject(obj); + if (!id.empty()) namedObjects_[id] = obj; + if (mass > 0) bodyCmd(id, "box", mass, rest, frict, false); + else if (mass == -1.0f) bodyCmd(id, "box", 0, rest, frict, true); + return obj; + } + + std::shared_ptr + addSphere(const std::string& id, glm::vec3 pos, float r, + glm::vec3 color, GLuint tex, GLuint nmap, + float metallic, float roughness, float mass, + float rest = 0.35f, float frict = 0.35f, + glm::vec3 emissive = glm::vec3(0)) { + auto obj = CreateGameObject(CreateSphere(28, color), pos); + obj->SetScale(glm::vec3(r)); + if (tex) obj->SetTexture(tex); + if (nmap) obj->SetNormalMap(nmap); + obj->SetMetallic(metallic); + obj->SetRoughness(roughness); + if (glm::length(emissive) > 0) obj->SetEmissive(emissive); + AddGameObject(obj); + if (!id.empty()) namedObjects_[id] = obj; + if (mass > 0) bodyCmd(id, "sphere", mass, rest, frict); + return obj; + } + + bool OnInitialize() override { + auto rnd = [&](float lo, float hi) { + std::uniform_real_distribution d(lo, hi); return d(rng_); + }; + auto jit = [&](float s) { return rnd(-s, s); }; + + // --- Base palette: reduced fog, bright daylight. Gets blended per + // zone each frame in UpdateAtmosphere. Values here are the starting + // point before the camera picks a zone. + processAgentCommand(json::parse(R"({ + "action":"sun", + "direction":[-0.45, 0.60, -0.25], + "color":[2.05, 1.85, 1.60], + "zenith":[0.26, 0.42, 0.65], + "horizon":[0.82, 0.80, 0.74], + "sky":[0.65, 0.72, 0.78], + "ground":[0.08, 0.09, 0.10], + "shadow_extent":120, + "exposure":0.95, + "bloom_threshold":1.2, + "bloom_intensity":1.0, + "fog_density":0.003 + })")); + + // Physics-enabled pool: primary water mesh restricted to a small + // central disk via bounds. Objects outside get no buoyancy; objects + // inside bob in the pool properly. Water mesh size matches bounds + // diameter so it doesn't visually spill. + processAgentCommand(json::parse(R"({ + "action":"water","enabled":true, + "level":0.7,"size":11, + "amplitude":0.07,"wavelength":2.6,"speed":0.8, + "shallow":[0.52, 0.68, 0.70], + "deep":[0.04, 0.18, 0.28], + "density":2.2,"resolution":96, + "bounds_center":[0,0,0],"bounds_radius":5.0 + })")); + processAgentCommand(json::parse(R"({ + "action":"weather","wind_dir":[1,0,0.2],"wind_speed":0.2, + "storminess":0.0,"current_coef":0.0 + })")); + + GLuint grass_tex = TextureCache::Get("grass"); + GLuint rock_tex = TextureCache::Get("rock"); + GLuint wood_tex = TextureCache::Get("wood"); + GLuint dirt_t = TextureCache::Get("dirt"); + GLuint concrete = TextureCache::Get("concrete"); + GLuint marble_t = TextureCache::Get("marble"); + GLuint checker_t = TextureCache::Get("checker"); + GLuint grid_t = TextureCache::Get("grid"); + GLuint noise_t = TextureCache::Get("noise"); + + // --- Four quadrant ground planes (80×80 each, map total 160×160) --- + auto forest_ground = CreateGameObject( + CreatePlane(80.0f, 80.0f, glm::vec3(0.72f, 0.82f, 0.66f)), + glm::vec3(-40.0f, 0.0f, 40.0f)); + forest_ground->SetTexture(grass_tex); forest_ground->SetNormalMap(noise_t); + forest_ground->SetUVScale(glm::vec2(0.3f)); forest_ground->SetRoughness(0.95f); + AddGameObject(forest_ground); + + // Sakura quadrant: deformed heightmap terrain, edges taper to 0 + auto sakura_ground = CreateGameObject( + CreateTerrain(80, 80.0f, kSakuraMaxH, + glm::vec3(1.0f, 0.82f, 0.85f), + kTerrainFalloff, true), + glm::vec3(40.0f, 0.0f, 40.0f)); + sakura_ground->SetTexture(grass_tex); sakura_ground->SetNormalMap(noise_t); + sakura_ground->SetUVScale(glm::vec2(0.3f)); sakura_ground->SetRoughness(0.92f); + AddGameObject(sakura_ground); + + // Desert quadrant: variable-height deformed plane (like the dune style) + auto desert_ground = CreateGameObject( + CreateTerrain(80, 80.0f, kDesertMaxH, + glm::vec3(1.05f, 0.88f, 0.62f), + kTerrainFalloff, true), + glm::vec3(-40.0f, 0.0f, -40.0f)); + desert_ground->SetTexture(dirt_t); desert_ground->SetNormalMap(noise_t); + desert_ground->SetUVScale(glm::vec2(0.35f)); desert_ground->SetRoughness(0.98f); + AddGameObject(desert_ground); + + auto tech_ground = CreateGameObject( + CreatePlane(80.0f, 80.0f, glm::vec3(0.3f, 0.32f, 0.38f)), + glm::vec3(40.0f, 0.0f, -40.0f)); + tech_ground->SetTexture(grid_t); tech_ground->SetNormalMap(noise_t); + tech_ground->SetUVScale(glm::vec2(0.5f)); + tech_ground->SetMetallic(0.6f); + tech_ground->SetRoughness(0.3f); + AddGameObject(tech_ground); + + // --- Physics ground plane: infinite static plane at y=0 covers everything + // We register one of the quadrants as the physics body, but the plane + // collider itself is infinite so it catches objects anywhere. + namedObjects_["__ground"] = forest_ground; + processAgentCommand(json::parse(R"({ + "action":"body","id":"__ground","shape":"plane", + "normal":[0,1,0],"offset":0,"static":true + })")); + + // ============================ FOREST (NW) ===================== + // Trees + mossy boulders + glowing mushrooms + fallen logs + auto tree = [&](glm::vec3 at, float s) { + auto trunk = CreateGameObject( + CreateCylinder(0.22f * s, 2.6f * s, 10, + glm::vec3(0.86f, 0.80f, 0.70f)), + at + glm::vec3(0, 1.3f * s, 0)); + trunk->SetRotation(glm::vec3(jit(3), jit(360), jit(3))); + trunk->SetTexture(wood_tex); trunk->SetNormalMap(noise_t); + trunk->SetRoughness(0.85f); + AddGameObject(trunk); + glm::vec3 cc(rnd(0.08f, 0.16f), rnd(0.38f, 0.55f), rnd(0.10f, 0.20f)); + auto c = CreateGameObject(CreateCone(1.3f * s, 2.8f * s, 10, cc), + at + glm::vec3(0, 3.1f * s, 0)); + c->SetNormalMap(noise_t); c->SetRoughness(0.8f); + AddGameObject(c); + }; + for (int i = 0; i < 45; ++i) { + float x = rnd(-78.0f, -6.0f); + float z = rnd( 6.0f, 78.0f); + tree(glm::vec3(x, 0, z), rnd(0.8f, 1.5f)); + } + // Physics moss rocks (dynamic) + for (int i = 0; i < 14; ++i) { + float x = rnd(-76.0f, -8.0f); + float z = rnd( 8.0f, 76.0f); + float s = rnd(0.5f, 1.4f); + addSphere("frock_" + std::to_string(i), + glm::vec3(x, s + 0.1f, z), s, + glm::vec3(0.45f, 0.55f, 0.38f), + rock_tex, noise_t, + 0.0f, 0.9f, + 1.2f, 0.3f, 0.4f); + } + // Glowing mushrooms + for (int i = 0; i < 26; ++i) { + float x = rnd(-77.0f, -7.0f); + float z = rnd( 7.0f, 77.0f); + // Stem + auto stem = CreateGameObject( + CreateCylinder(0.12f, 0.45f, 8, + glm::vec3(0.92f, 0.90f, 0.82f)), + glm::vec3(x, 0.23f, z)); + stem->SetRoughness(0.75f); + AddGameObject(stem); + // Cap (emissive for bloom) + glm::vec3 cap_col = (i % 3 == 0) + ? glm::vec3(1.2f, 0.4f, 0.9f) // pink + : (i % 3 == 1 ? glm::vec3(0.3f, 1.1f, 1.3f) // cyan + : glm::vec3(1.3f, 0.9f, 0.2f)); // yellow + auto cap = CreateGameObject( + CreateCone(0.35f, 0.3f, 10, cap_col), + glm::vec3(x, 0.55f, z)); + cap->SetEmissive(cap_col * 0.7f); + AddGameObject(cap); + } + // Fallen logs (dynamic — can be rolled) + for (int i = 0; i < 6; ++i) { + float x = rnd(-74.0f, -10.0f); + float z = rnd(10.0f, 74.0f); + std::string id = "flog_" + std::to_string(i); + auto log = CreateGameObject( + CreateCylinder(0.4f, 3.0f, 10, glm::vec3(0.85f, 0.72f, 0.55f)), + glm::vec3(x, 0.45f, z)); + log->SetRotation(glm::vec3(0, jit(180.0f), 90.0f)); + log->SetTexture(wood_tex); log->SetNormalMap(noise_t); + log->SetRoughness(0.85f); + AddGameObject(log); + namedObjects_[id] = log; + bodyCmd(id, "box", 2.0f, 0.2f, 0.6f, false); + } + + // ============================ SAKURA PINK (NE) ================= + // Cherry trees (pink canopy), stone lanterns, blossom petals + auto cherry = [&](glm::vec3 at, float s) { + auto trunk = CreateGameObject( + CreateCylinder(0.22f * s, 2.4f * s, 10, + glm::vec3(0.45f, 0.30f, 0.20f)), + at + glm::vec3(0, 1.2f * s, 0)); + trunk->SetRotation(glm::vec3(jit(4), jit(360), jit(4))); + trunk->SetTexture(wood_tex); trunk->SetNormalMap(noise_t); + trunk->SetRoughness(0.85f); + AddGameObject(trunk); + // Pink canopy: stacked flattened spheres + for (int k = 0; k < 3; ++k) { + float r = 1.6f * s * (1.0f - k * 0.20f); + auto canopy = CreateGameObject( + CreateSphere(24, glm::vec3(1.2f, 0.55f, 0.82f)), + at + glm::vec3(jit(0.25f), 2.6f * s + k * 0.9f * s, jit(0.25f))); + canopy->SetScale(glm::vec3(r, r * 0.75f, r)); + canopy->SetEmissive(glm::vec3(0.25f, 0.10f, 0.18f)); + canopy->SetRoughness(0.85f); + AddGameObject(canopy); + } + }; + for (int i = 0; i < 30; ++i) { + float x = rnd( 7.0f, 78.0f); + float z = rnd( 7.0f, 78.0f); + cherry(glm::vec3(x, 0, z), rnd(0.9f, 1.5f)); + } + // Stone lanterns (static decorative) + for (int i = 0; i < 14; ++i) { + float x = rnd( 10.0f, 75.0f); + float z = rnd( 10.0f, 75.0f); + // Base + auto base = CreateGameObject( + CreateCube(glm::vec3(0.85f, 0.82f, 0.78f)), + glm::vec3(x, 0.3f, z)); + base->SetScale(glm::vec3(0.6f, 0.3f, 0.6f)); + base->SetTexture(marble_t); base->SetNormalMap(noise_t); + base->SetRoughness(0.6f); + AddGameObject(base); + // Pillar + auto pillar = CreateGameObject( + CreateCylinder(0.18f, 1.2f, 10, glm::vec3(0.88f, 0.85f, 0.80f)), + glm::vec3(x, 1.05f, z)); + pillar->SetTexture(marble_t); pillar->SetNormalMap(noise_t); + pillar->SetRoughness(0.55f); + AddGameObject(pillar); + // Lamp box (emissive warm) + auto lamp = CreateGameObject( + CreateCube(glm::vec3(1.0f)), + glm::vec3(x, 1.9f, z)); + lamp->SetScale(glm::vec3(0.45f)); + lamp->SetEmissive(glm::vec3(3.0f, 1.9f, 0.7f)); + AddGameObject(lamp); + // Roof cap + auto roof = CreateGameObject( + CreateCone(0.55f, 0.3f, 8, glm::vec3(0.45f, 0.25f, 0.2f)), + glm::vec3(x, 2.35f, z)); + roof->SetRoughness(0.85f); + AddGameObject(roof); + } + // Dynamic pink rocks and crates to knock around + for (int i = 0; i < 14; ++i) { + float x = rnd( 8.0f, 78.0f); + float z = rnd( 8.0f, 78.0f); + bool sph = (i % 2 == 0); + if (sph) { + addSphere("srk_" + std::to_string(i), + glm::vec3(x, 1.0f, z), rnd(0.5f, 0.9f), + glm::vec3(0.95f, 0.80f, 0.85f), + rock_tex, noise_t, + 0.0f, 0.8f, + 1.0f, 0.35f, 0.4f); + } else { + addBox("sbox_" + std::to_string(i), + glm::vec3(x, 0.6f, z), glm::vec3(rnd(0.9f, 1.3f)), + glm::vec3(1.1f, 0.65f, 0.85f), + marble_t, noise_t, 0.0f, 0.55f, + 0.9f, 0.15f, 0.5f, + glm::vec3(jit(15), jit(180), jit(15))); + } + } + // Floating pink petals (purely decorative, no physics) + for (int i = 0; i < 90; ++i) { + float x = rnd( 6.0f, 78.0f); + float z = rnd( 6.0f, 78.0f); + float y = rnd(1.5f, 5.0f); + auto petal = CreateGameObject( + CreateSphere(6, glm::vec3(1.4f, 0.6f, 0.85f)), + glm::vec3(x, y, z)); + petal->SetScale(glm::vec3(0.08f, 0.04f, 0.08f)); + petal->SetEmissive(glm::vec3(0.45f, 0.15f, 0.25f)); + AddGameObject(petal); + } + + // ============================ DESERT (SW) ====================== + // Cacti + sand boulders + a small pyramid + for (int i = 0; i < 12; ++i) { + float x = rnd(-77.0f, -7.0f); + float z = rnd(-77.0f, -7.0f); + std::string id = "cactus_" + std::to_string(i); + float h = rnd(2.0f, 3.4f); + auto c = CreateGameObject( + CreateCylinder(0.35f, h, 8, glm::vec3(0.30f, 0.55f, 0.32f)), + glm::vec3(x, h * 0.5f, z)); + c->SetRoughness(0.8f); + c->SetNormalMap(noise_t); + AddGameObject(c); + namedObjects_[id] = c; + bodyCmd(id, "box", 1.5f, 0.2f, 0.5f, false); + // Arm + if (i % 2 == 0) { + auto arm = CreateGameObject( + CreateCylinder(0.2f, 1.2f, 8, glm::vec3(0.32f, 0.56f, 0.33f)), + glm::vec3(x + 0.45f, h * 0.6f, z)); + arm->SetRotation(glm::vec3(0, 0, 60.0f)); + arm->SetRoughness(0.8f); + AddGameObject(arm); + } + } + // Sand boulders (physics) + for (int i = 0; i < 16; ++i) { + float x = rnd(-78.0f, -6.0f); + float z = rnd(-78.0f, -6.0f); + float s = rnd(0.5f, 1.3f); + addSphere("drock_" + std::to_string(i), + glm::vec3(x, s, z), s, + glm::vec3(0.88f, 0.72f, 0.50f), + rock_tex, noise_t, + 0.0f, 0.85f, + 1.2f, 0.25f, 0.45f); + } + // Step pyramid (larger, static) — signature landmark + for (int lvl = 0; lvl < 6; ++lvl) { + float s = 10.0f - lvl * 1.4f; + addBox("pyr_" + std::to_string(lvl), + glm::vec3(-55.0f, 0.9f + lvl * 1.8f, -55.0f), + glm::vec3(s, 1.8f, s), + glm::vec3(0.92f, 0.76f, 0.55f), + dirt_t, noise_t, 0.0f, 0.9f, + -1.0f); + } + // Obelisk near pyramid + addBox("desert_obelisk", + glm::vec3(-45.0f, 5.0f, -58.0f), + glm::vec3(1.5f, 10.0f, 1.5f), + glm::vec3(0.95f, 0.82f, 0.65f), + marble_t, noise_t, 0.0f, 0.5f, -1.0f, + 0.0f, 0.0f, glm::vec3(0, 18.0f, 0)); + // Dune mounds (static) + for (int i = 0; i < 14; ++i) { + float x = rnd(-74.0f, -8.0f); + float z = rnd(-74.0f, -8.0f); + auto dune = CreateGameObject( + CreateSphere(20, glm::vec3(1.0f, 0.85f, 0.55f)), + glm::vec3(x, 0.0f, z)); + dune->SetScale(glm::vec3(4.0f, 1.0f, 3.5f)); + dune->SetTexture(dirt_t); dune->SetNormalMap(noise_t); + dune->SetRoughness(0.95f); + AddGameObject(dune); + } + + // ============================ TECH (SE) ======================= + // Chrome pillars + emissive cyan bars + crate stack + for (int i = 0; i < 14; ++i) { + float ang = (float)i / 14.0f * 2 * M_PI; + float R = 25.0f; + float x = 40.0f + std::cos(ang) * R; + float z = -40.0f + std::sin(ang) * R; + std::string id = "pylon_" + std::to_string(i); + addBox(id, + glm::vec3(x, 3.0f, z), glm::vec3(0.7f, 6.0f, 0.7f), + glm::vec3(0.92f), + 0, 0, 1.0f, 0.15f, + -1.0f, + 0.0f, 0.0f, glm::vec3(0, jit(15.0f), 0)); + } + // Emissive cyan bars laid horizontally between pylons (decorative) + for (int i = 0; i < 24; ++i) { + float x = rnd( 8.0f, 76.0f); + float z = rnd(-76.0f, -8.0f); + auto bar = CreateGameObject( + CreateCube(glm::vec3(1.0f)), + glm::vec3(x, 0.15f, z)); + bar->SetScale(glm::vec3(rnd(1.2f, 2.4f), 0.05f, 0.2f)); + bar->SetRotation(glm::vec3(0, jit(180.0f), 0)); + bar->SetEmissive(glm::vec3(0.1f, 2.5f, 3.0f)); + AddGameObject(bar); + } + // Tech crate pyramid (dynamic) + for (int y = 0; y < 4; ++y) { + int n = 4 - y; + for (int i = 0; i < n; ++i) { + float x = 18.0f + i * 1.1f + (3 - n) * 0.55f; + float yp = 0.55f + y * 1.1f; + addBox("tcrate_" + std::to_string(y * 4 + i), + glm::vec3(x, yp, -18.0f), glm::vec3(1.0f), + glm::vec3(0.85f, 0.88f, 0.95f), + checker_t, noise_t, 0.85f, 0.25f, + 0.9f, 0.3f, 0.45f); + } + } + // Chrome spheres scattered (dynamic) + for (int i = 0; i < 6; ++i) { + float x = rnd( 8.0f, 76.0f); + float z = rnd(-76.0f, -8.0f); + std::string col_id = "tsph_" + std::to_string(i); + addSphere(col_id, + glm::vec3(x, 0.7f, z), 0.6f, + glm::vec3(0.95f, 0.95f, 0.95f), + 0, 0, 1.0f, 0.25f, + 1.3f, 0.55f, 0.2f); + } + // Glowing monolith — signature tech landmark + addBox("tech_obelisk", + glm::vec3(65.0f, 6.0f, -65.0f), glm::vec3(1.6f, 12.0f, 1.6f), + glm::vec3(0.92f, 0.94f, 1.0f), + 0, 0, 1.0f, 0.1f, -1.0f, + 0.0f, 0.0f, glm::vec3(0, 30.0f, 0), + glm::vec3(0.5f, 1.8f, 3.0f)); + // Secondary glowing pillar + addBox("tech_spire", + glm::vec3(25.0f, 8.0f, -60.0f), glm::vec3(0.8f, 16.0f, 0.8f), + glm::vec3(0.95f, 0.96f, 1.0f), + 0, 0, 1.0f, 0.08f, -1.0f, + 0.0f, 0.0f, glm::vec3(0), + glm::vec3(1.5f, 0.4f, 3.5f)); + + // ============================ CENTER: PHYSICS POOL ============ + // Raised basin (6.0 unit radius, wall height ~0.9) built from + // a ring of static stone blocks. Water mesh is already set via + // the "water" command with bounds_radius=5.0 so buoyancy only + // kicks in inside the ring. + for (int i = 0; i < 24; ++i) { + float ang = (float)i / 24.0f * 2 * M_PI; + float R = 5.8f; + float x = std::cos(ang) * R; + float z = std::sin(ang) * R; + std::string id = "poolw_" + std::to_string(i); + addBox(id, + glm::vec3(x, 0.5f, z), glm::vec3(0.9f, 1.0f, 0.9f), + glm::vec3(0.82f, 0.78f, 0.74f), + marble_t, noise_t, 0.0f, 0.55f, -1.0f, // static + 0.0f, 0.0f, glm::vec3(jit(5), ang * 57.3f + jit(10), jit(5))); + } + // Stepping-stone paths from pool toward each quadrant + for (int q = 0; q < 4; ++q) { + float dx = (q & 1) ? 1.0f : -1.0f; + float dz = (q & 2) ? -1.0f : 1.0f; + for (int s = 0; s < 6; ++s) { + float x = dx * (8.0f + s * 3.5f); + float z = dz * (8.0f + s * 3.5f); + auto stone = CreateGameObject( + CreateCube(glm::vec3(0.82f, 0.78f, 0.74f)), + glm::vec3(x, 0.05f, z)); + stone->SetScale(glm::vec3(1.6f, 0.1f, 1.6f)); + stone->SetTexture(marble_t); stone->SetNormalMap(noise_t); + stone->SetRoughness(0.55f); + AddGameObject(stone); + } + } + + // --- Level border: dense mountain ring just outside the 160x160 map. + // 56 cones at R=90 with overlapping footprints form an impassable wall. + for (int i = 0; i < 56; ++i) { + float ang = (float)i / 56.0f * 2 * M_PI + jit(0.03f); + float R = 90.0f + jit(4.0f); + float x = std::cos(ang) * R; + float z = std::sin(ang) * R; + float sc = rnd(1.0f, 1.5f); + float h = 34.0f * sc; + auto m = CreateGameObject( + CreateCone(14.0f * sc, h, 10, glm::vec3(0.22f, 0.20f, 0.25f)), + glm::vec3(x, h * 0.5f, z)); + m->SetRoughness(0.92f); + AddGameObject(m); + } + // Second, farther mountain ring for depth (behind the border) + for (int i = 0; i < 24; ++i) { + float ang = (float)i / 24.0f * 2 * M_PI + jit(0.06f); + float R = 140.0f + jit(15.0f); + float x = std::cos(ang) * R; + float z = std::sin(ang) * R; + float sc = rnd(1.2f, 1.8f); + float h = 40.0f * sc; + auto m = CreateGameObject( + CreateCone(18.0f * sc, h, 10, glm::vec3(0.18f, 0.17f, 0.22f)), + glm::vec3(x, h * 0.5f, z)); + AddGameObject(m); + } + + processAgentCommand(json::parse(R"({"action":"physics","enabled":true})")); + + // First-person arm: 3 GameObjects, repositioned from camera basis each frame + BuildArm(); + + // Spawn player on forest-side walkway, feet on ground, looking SE toward pool. + float spawn_x = -8.0f, spawn_z = 14.0f; + float foot_y = SampleTerrainH(spawn_x, spawn_z); + glm::vec3 eye(spawn_x, foot_y + kEyeH, spawn_z); + glm::vec3 target = eye + glm::vec3(1.0f, -0.2f, -1.0f); + SetCameraPosition(eye); + SetCameraTarget(target); + glm::vec3 fwd = glm::normalize(target - eye); + cam_pitch_ = glm::degrees(std::asin(fwd.y)); + cam_yaw_ = glm::degrees(std::atan2(fwd.z, fwd.x)); + last_cam_pos_ = eye; + grounded_ = true; + + std::cout << "Four-Map ready: " << game_objects_.size() << " objects.\n" + << " NW Forest | NE Sakura (hills) | SW Desert (dunes) | SE Tech | Center: pool\n" + << " WASD walk, mouse/arrows look, Space jump, LShift sprint. F2 toggles fly.\n" + << " F / LMB / R1 to throw balls. Mountain ring blocks escape.\n"; + return true; + } + + void BuildArm() { + arm_upper_ = CreateGameObject( + CreateCylinder(0.10f, 0.45f, 10, glm::vec3(0.92f, 0.85f, 0.78f)), + glm::vec3(0)); + arm_upper_->SetRoughness(0.75f); + AddGameObject(arm_upper_); + arm_lower_ = CreateGameObject( + CreateCylinder(0.085f, 0.45f, 10, glm::vec3(0.92f, 0.85f, 0.78f)), + glm::vec3(0)); + arm_lower_->SetRoughness(0.75f); + AddGameObject(arm_lower_); + hand_ = CreateGameObject( + CreateSphere(18, glm::vec3(0.92f, 0.85f, 0.78f)), + glm::vec3(0)); + hand_->SetScale(glm::vec3(0.11f)); + hand_->SetRoughness(0.7f); + AddGameObject(hand_); + arm_ready_ = true; + } + + void UpdateArm(float dt) { + if (!arm_ready_) return; + throw_anim_ = std::max(0.0f, throw_anim_ - dt * 4.0f); + glm::vec3 cp = GetCameraPosition(); + glm::vec3 tg = GetCameraTarget(); + glm::vec3 fwd = glm::normalize(tg - cp); + glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0))); + glm::vec3 up = glm::cross(right, fwd); + float t = throw_anim_; + float forward_boost = t * 0.45f; + glm::vec3 shoulder = cp + right * 0.55f + up * -0.40f + fwd * 0.35f; + glm::vec3 elbow = shoulder + fwd * (0.18f + forward_boost * 0.4f) + + up * (-0.20f + t * 0.1f); + glm::vec3 wrist = elbow + fwd * (0.35f + forward_boost) + + up * (-0.05f + t * 0.3f); + auto midpoint = [](const glm::vec3& a, const glm::vec3& b) { return (a + b) * 0.5f; }; + auto yawPitch = [&](const glm::vec3& dir) { + glm::vec3 d = glm::normalize(dir); + float pitch = glm::degrees(std::asin(d.y)); + float yaw = glm::degrees(std::atan2(d.x, d.z)); + return glm::vec3(-pitch - 90.0f, yaw, 0.0f); + }; + glm::vec3 upperDir = elbow - shoulder; + glm::vec3 lowerDir = wrist - elbow; + arm_upper_->SetPosition(midpoint(shoulder, elbow)); + arm_upper_->SetRotation(yawPitch(upperDir)); + arm_upper_->SetScale(glm::vec3(1.0f, glm::length(upperDir) / 0.45f, 1.0f)); + arm_lower_->SetPosition(midpoint(elbow, wrist)); + arm_lower_->SetRotation(yawPitch(lowerDir)); + arm_lower_->SetScale(glm::vec3(1.0f, glm::length(lowerDir) / 0.45f, 1.0f)); + hand_->SetPosition(wrist); + } + + // Per-zone atmosphere recipes — blended each frame based on camera XZ. + struct Zone { + glm::vec3 sun, zenith, horizon, sky, ground; + float fog, bloom_i, bloom_t, expo; + }; + void UpdateAtmosphere() { + glm::vec3 cp = GetCameraPosition(); + float tx = glm::clamp((cp.x + 40.0f) / 80.0f, 0.0f, 1.0f); + float tz = glm::clamp((cp.z + 40.0f) / 80.0f, 0.0f, 1.0f); + float wF = (1.0f - tx) * tz; // NW forest + float wS = tx * tz; // NE sakura + float wD = (1.0f - tx) * (1.0f - tz); // SW desert + float wT = tx * (1.0f - tz); // SE tech + + Zone Zf{ glm::vec3(2.0f, 1.85f, 1.55f), glm::vec3(0.20f, 0.40f, 0.55f), + glm::vec3(0.72f, 0.82f, 0.74f), glm::vec3(0.55f, 0.72f, 0.62f), + glm::vec3(0.08f, 0.12f, 0.08f), 0.004f, 1.0f, 1.25f, 0.95f }; + Zone Zs{ glm::vec3(2.20f, 1.75f, 1.85f), glm::vec3(0.36f, 0.45f, 0.66f), + glm::vec3(1.15f, 0.74f, 0.86f), glm::vec3(0.92f, 0.70f, 0.80f), + glm::vec3(0.10f, 0.06f, 0.10f), 0.003f, 1.6f, 1.05f, 1.00f }; + Zone Zd{ glm::vec3(2.45f, 1.75f, 1.25f), glm::vec3(0.25f, 0.34f, 0.55f), + glm::vec3(1.05f, 0.74f, 0.48f), glm::vec3(0.92f, 0.70f, 0.48f), + glm::vec3(0.14f, 0.10f, 0.06f), 0.006f, 0.75f, 1.30f, 0.95f }; + Zone Zt{ glm::vec3(1.60f, 1.95f, 2.25f), glm::vec3(0.04f, 0.10f, 0.25f), + glm::vec3(0.20f, 0.40f, 0.65f), glm::vec3(0.22f, 0.38f, 0.58f), + glm::vec3(0.04f, 0.06f, 0.10f), 0.010f, 2.00f, 0.95f, 0.92f }; + + auto blend = [&](auto Zf_v, auto Zs_v, auto Zd_v, auto Zt_v) { + return wF * Zf_v + wS * Zs_v + wD * Zd_v + wT * Zt_v; + }; + sun_color_ = blend(Zf.sun, Zs.sun, Zd.sun, Zt.sun); + zenith_color_ = blend(Zf.zenith, Zs.zenith, Zd.zenith, Zt.zenith); + horizon_color_ = blend(Zf.horizon, Zs.horizon, Zd.horizon, Zt.horizon); + sky_color_ = blend(Zf.sky, Zs.sky, Zd.sky, Zt.sky); + ground_color_ = blend(Zf.ground, Zs.ground, Zd.ground, Zt.ground); + fog_density_ = blend(Zf.fog, Zs.fog, Zd.fog, Zt.fog); + bloom_intensity_= blend(Zf.bloom_i, Zs.bloom_i, Zd.bloom_i, Zt.bloom_i); + bloom_threshold_= blend(Zf.bloom_t, Zs.bloom_t, Zd.bloom_t, Zt.bloom_t); + exposure_ = blend(Zf.expo, Zs.expo, Zd.expo, Zt.expo); + } + + void FireBall() { + glm::vec3 cp = GetCameraPosition(); + glm::vec3 tg = GetCameraTarget(); + glm::vec3 fwd = glm::normalize(tg - cp); + if ((int)active_shots_.size() >= kMaxShots) { + auto old = active_shots_.front(); active_shots_.pop_front(); + processAgentCommand(json::parse("{\"action\":\"delete\",\"id\":\"" + old + "\"}")); + } + std::uniform_real_distribution cd(0.7f, 1.7f); + glm::vec3 col(cd(rng_), cd(rng_), cd(rng_)); + std::string id = "shot_" + std::to_string(shot_counter_++); + auto b = CreateGameObject(CreateSphere(20, col), cp + fwd * 1.4f); + b->SetScale(glm::vec3(0.32f)); + b->SetMetallic(1.0f); + b->SetRoughness(0.25f); + AddGameObject(b); + namedObjects_[id] = b; + json::Object bc; + bc["action"]=json::Value(std::string("body")); + bc["id"]=json::Value(id); + bc["shape"]=json::Value(std::string("sphere")); + bc["mass"]=json::Value(1.2); + bc["restitution"]=json::Value(0.6); + bc["friction"]=json::Value(0.3); + processAgentCommand(json::Value(bc)); + json::Object vc; + vc["action"]=json::Value(std::string("velocity")); + vc["id"]=json::Value(id); + vc["linear"]=json::Value(json::Array{ + json::Value((double)(fwd.x * 55)), + json::Value((double)(fwd.y * 55)), + json::Value((double)(fwd.z * 55))}); + processAgentCommand(json::Value(vc)); + active_shots_.push_back(id); + } + + void OnUpdate(float dt) override { + UpdateAtmosphere(); // blend zone palettes each frame + + shot_cooldown_ -= dt; + if (shot_cooldown_ <= 0.0f) { + bool fire = Input::IsKeyHeld(GLFW_KEY_F) + || Input::IsMouseButtonPressed(Input::MouseButton::Left) + || Input::IsGamepadButtonDown(Input::GamepadButton::R1); + if (fire) { FireBall(); shot_cooldown_ = 0.15f; } + } + + // --- Input gathering ------------------------------------------------- + glm::vec3 eng_pos = GetCameraPosition(); + glm::vec2 ls{0}, rs{0}; + float up_in = 0, down_in = 0; + bool sprint = false, jump_now = false, fly_toggle = false; + + if (Input::IsGamepadConnected()) { + ls = Input::GetLeftStick(); + rs = Input::GetRightStick(); + up_in = Input::GetR2(); + down_in = Input::GetL2(); + if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross)) sprint = true; + if (Input::IsGamepadButtonDown(Input::GamepadButton::Circle)) jump_now = true; + } + if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT)) sprint = true; + if (Input::IsKeyHeld(GLFW_KEY_W)) ls.y -= 1.0f; + if (Input::IsKeyHeld(GLFW_KEY_S)) ls.y += 1.0f; + if (Input::IsKeyHeld(GLFW_KEY_A)) ls.x -= 1.0f; + if (Input::IsKeyHeld(GLFW_KEY_D)) ls.x += 1.0f; + if (Input::IsKeyHeld(GLFW_KEY_LEFT)) rs.x -= 1.0f; + if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) rs.x += 1.0f; + if (Input::IsKeyHeld(GLFW_KEY_UP)) rs.y -= 1.0f; + if (Input::IsKeyHeld(GLFW_KEY_DOWN)) rs.y += 1.0f; + if (Input::IsKeyHeld(GLFW_KEY_SPACE)) jump_now = true; + if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL)) down_in += 1.0f; + + bool fly_pressed_now = Input::IsKeyHeld(GLFW_KEY_F2); + if (fly_pressed_now && !fly_prev_) { fly_mode_ = !fly_mode_; vy_ = 0.0f; } + fly_prev_ = fly_pressed_now; + + // --- Look update ----------------------------------------------------- + cam_yaw_ += rs.x * look_sensitivity_ * dt; + cam_pitch_ -= rs.y * look_sensitivity_ * dt; + if (cam_pitch_ > 89.0f) cam_pitch_ = 89.0f; + if (cam_pitch_ < -89.0f) cam_pitch_ = -89.0f; + glm::vec3 fwd; + fwd.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_)); + fwd.y = std::sin(glm::radians(cam_pitch_)); + fwd.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_)); + fwd = glm::normalize(fwd); + glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0))); + glm::vec3 fwd_h = glm::normalize(glm::vec3(fwd.x, 0.0f, fwd.z)); + glm::vec3 right_h = glm::normalize(glm::vec3(right.x, 0.0f, right.z)); + + // --- Movement -------------------------------------------------------- + glm::vec3 pos = eng_pos; + + if (fly_mode_) { + // Free-fly for debug / fast traversal + float speed = 22.0f * (sprint ? 3.0f : 1.0f); + pos += fwd * (-ls.y) * speed * dt; + pos += right * ls.x * speed * dt; + pos.y += (up_in - down_in) * speed * dt; + } else { + // Grounded walking + float speed = kWalkSpeed * (sprint ? kRunMult : 1.0f); + glm::vec3 horiz = fwd_h * (-ls.y) * speed * dt + + right_h * ls.x * speed * dt; + + // Slope test: project candidate x/z, sample ground height + glm::vec3 cand = pos + horiz; + cand.x = glm::clamp(cand.x, -kMapHalf, kMapHalf); + cand.z = glm::clamp(cand.z, -kMapHalf, kMapHalf); + float foot_now = pos.y - kEyeH; + float foot_cand = SampleTerrainH(cand.x, cand.z); + + if (grounded_ && (foot_cand - foot_now) > kStepUp) { + // Too steep — block horizontal motion this frame + cand.x = pos.x; + cand.z = pos.z; + foot_cand = SampleTerrainH(cand.x, cand.z); + } + + // Vertical: jump + gravity + if (jump_now && !jump_prev_ && grounded_) { + vy_ = kJumpV; + grounded_ = false; + } + if (!grounded_) { + vy_ -= kGravity * dt; + cand.y = pos.y + vy_ * dt; + if (cand.y <= foot_cand + kEyeH) { + cand.y = foot_cand + kEyeH; + vy_ = 0.0f; + grounded_ = true; + } + } else { + // Stick to ground (walk up/down gentle slopes smoothly) + cand.y = foot_cand + kEyeH; + vy_ = 0.0f; + // Detect walking off a ledge + if (foot_cand < foot_now - 0.25f) grounded_ = false; + } + pos = cand; + } + jump_prev_ = jump_now; + + SetCameraPosition(pos); + SetCameraTarget(pos + fwd); + last_cam_pos_ = pos; + + // First-person arm + UpdateArm(dt); + } +}; + + +// --------------------------------------------- +// PhysicsPlaygroundDemo — expansive showcase scene. +// +// Zones: +// CENTER — chrome monolith, 10 pulsing emissive bollards, Jenga tower, +// loose props (HDR flame in the middle) +// NORTH — 7×7 PBR material grid (metallic × roughness) on pedestals +// EAST — ramp + 40-domino cascade + 10 bowling pins. Chain fires at t=2s. +// WEST — catapult frame + target crate stack. Periodic launch every 18s. +// SOUTH — cliff, waterfall, lake with flow current + floating barrels +// +// Interactive: +// F / LMB / R1 → shoot metallic HDR ball from camera +// T → cycle storm state (calm → squall → gale) +// G → chaos: apply random upward impulse to every dynamic body +// +// Time-driven: +// t=2s → domino kickoff ball fires +// every 18s → catapult hurls a heavy ball at the arena +// every frame → waterfall droplet stream; bollards pulse via SetEmissive +// --------------------------------------------- +class PhysicsPlaygroundDemo : public Engine { + float cam_yaw_ = 200.0f; + float cam_pitch_ = -8.0f; + float cam_speed_ = 22.0f; + float look_sensitivity_ = 80.0f; + glm::vec3 last_cam_pos_{0}; + + float time_ = 0.0f; + float shot_cooldown_ = 0.0f; + float drop_timer_ = 0.0f; + float catapult_timer_ = 12.0f; // first fire at ~12s + bool kickoff_fired_ = false; + int storm_state_ = 0; + float t_down_prev_ = false; + float g_down_prev_ = false; + + std::mt19937 rng_{2024}; + std::mt19937 drop_rng_{2025}; + int shot_counter_ = 0; + int drop_counter_ = 0; + int cata_counter_ = 0; + std::deque active_shots_; + std::deque active_drops_; + std::deque active_cata_; + std::vector bollard_ids_; + + static constexpr int kMaxShots = 80; + static constexpr int kMaxDrops = 120; + static constexpr int kMaxCata = 5; + +public: + PhysicsPlaygroundDemo() + : Engine(1920, 1080, "ZoomEngine - Physics Playground") {} + + // --- Placement helpers -------------------------------------------------- + void bodyCmd(const std::string& id, const std::string& shape, + float mass, float rest, float frict, bool is_static = false) { + json::Object c; + c["action"] = json::Value(std::string("body")); + c["id"] = json::Value(id); + c["shape"] = json::Value(shape); + c["mass"] = json::Value((double)mass); + c["restitution"] = json::Value((double)rest); + c["friction"] = json::Value((double)frict); + if (is_static) c["static"] = json::Value(true); + processAgentCommand(json::Value(c)); + } + std::shared_ptr addBox(const std::string& id, glm::vec3 pos, glm::vec3 scale, + glm::vec3 color, GLuint tex, GLuint nmap, + float metallic, float roughness, float mass, + float rest = 0.2f, float frict = 0.6f, + glm::vec3 rot = glm::vec3(0)) { + auto obj = CreateGameObject(CreateCube(color), pos); + obj->SetScale(scale); + obj->SetRotation(rot); + if (tex) obj->SetTexture(tex); + if (nmap) obj->SetNormalMap(nmap); + obj->SetMetallic(metallic); + obj->SetRoughness(roughness); + AddGameObject(obj); + if (!id.empty()) namedObjects_[id] = obj; + if (mass > 0) bodyCmd(id, "box", mass, rest, frict, false); + else if (mass == -1.0f) bodyCmd(id, "box", 0, rest, frict, true); + return obj; + } + std::shared_ptr addSphere(const std::string& id, glm::vec3 pos, float r, + glm::vec3 color, GLuint tex, GLuint nmap, + float metallic, float roughness, float mass, + float rest = 0.4f, float frict = 0.3f) { + auto obj = CreateGameObject(CreateSphere(32, color), pos); + obj->SetScale(glm::vec3(r)); + if (tex) obj->SetTexture(tex); + if (nmap) obj->SetNormalMap(nmap); + obj->SetMetallic(metallic); + obj->SetRoughness(roughness); + AddGameObject(obj); + if (!id.empty()) namedObjects_[id] = obj; + if (mass > 0) bodyCmd(id, "sphere", mass, rest, frict, false); + return obj; + } + + // --- Init --------------------------------------------------------------- + bool OnInitialize() override { + auto rnd = [&](float lo, float hi) { + std::uniform_real_distribution d(lo, hi); return d(rng_); + }; + auto jit = [&](float s) { return rnd(-s, s); }; + + // --- Sky + HDR tuning (clear-day base; storm toggle can darken later) --- + processAgentCommand(json::parse(R"({ + "action":"sun", + "direction":[-0.45, 0.55, -0.30], + "color":[2.0, 1.55, 1.15], + "zenith":[0.22, 0.40, 0.70], + "horizon":[0.85, 0.75, 0.62], + "sky":[0.60, 0.68, 0.78], + "ground":[0.08, 0.09, 0.10], + "shadow_extent":110, + "exposure":0.95, + "bloom_threshold":1.15, + "bloom_intensity":1.1, + "fog_density":0.007 + })")); + + // Primary water — the lake in the south zone; flows west so floats drift + processAgentCommand(json::parse(R"({ + "action":"water","enabled":true, + "level":-0.25,"size":200, + "amplitude":0.14,"wavelength":10,"speed":0.8, + "shallow":[0.42, 0.58, 0.58],"deep":[0.03, 0.10, 0.18], + "density":1.0,"resolution":160, + "flow_dir":[-1, 0, 0],"flow_speed":1.5 + })")); + processAgentCommand(json::parse(R"({ + "action":"weather","wind_dir":[-1,0,0.2], + "wind_speed":0.5,"storminess":0.0,"current_coef":0.4 + })")); + + GLuint grass_tex = TextureCache::Get("grass"); + GLuint rock_tex = TextureCache::Get("rock"); + GLuint wood_tex = TextureCache::Get("wood"); + GLuint marble_t = TextureCache::Get("marble"); + GLuint concrete = TextureCache::Get("concrete"); + GLuint brick_t = TextureCache::Get("brick"); + GLuint noise_t = TextureCache::Get("noise"); + GLuint dirt_t = TextureCache::Get("dirt"); + + // --- Ground (two strips, leaving the lake channel open to south) --- + auto north_grass = CreateGameObject( + CreatePlane(220.0f, 120.0f, glm::vec3(0.88f, 0.92f, 0.80f)), + glm::vec3(0, 0, 45.0f)); + north_grass->SetTexture(grass_tex); north_grass->SetNormalMap(noise_t); + north_grass->SetUVScale(glm::vec2(0.22f)); north_grass->SetRoughness(0.95f); + AddGameObject(north_grass); + + auto center_stone = CreateGameObject( + CreatePlane(80.0f, 40.0f, glm::vec3(0.78f, 0.78f, 0.76f)), + glm::vec3(0, 0.01f, 0)); + center_stone->SetTexture(marble_t); center_stone->SetNormalMap(noise_t); + center_stone->SetUVScale(glm::vec2(0.2f)); center_stone->SetRoughness(0.45f); + AddGameObject(center_stone); + + auto west_dirt = CreateGameObject( + CreatePlane(80.0f, 40.0f, glm::vec3(0.68f, 0.62f, 0.50f)), + glm::vec3(-50.0f, 0, 0)); + west_dirt->SetTexture(dirt_t); west_dirt->SetNormalMap(noise_t); + west_dirt->SetUVScale(glm::vec2(0.25f)); west_dirt->SetRoughness(0.95f); + AddGameObject(west_dirt); + + auto east_stone = CreateGameObject( + CreatePlane(80.0f, 40.0f, glm::vec3(0.72f, 0.70f, 0.66f)), + glm::vec3(50.0f, 0, 0)); + east_stone->SetTexture(concrete); east_stone->SetNormalMap(noise_t); + east_stone->SetUVScale(glm::vec2(0.22f)); east_stone->SetRoughness(0.75f); + AddGameObject(east_stone); + + // -------------------- CENTRAL ARENA ------------------------------ + // Chrome monolith centerpiece + addBox("monolith", + glm::vec3(0, 6.0f, 0), glm::vec3(1.4f, 12.0f, 1.4f), + glm::vec3(0.96f), 0, 0, 1.0f, 0.12f, -1.0f); // static + + // 10 pulsing emissive bollards in a ring around the monolith + for (int i = 0; i < 10; ++i) { + float ang = (float)i / 10.0f * 2 * M_PI; + float R = 6.0f; + std::string id = "boll_" + std::to_string(i); + auto boll = CreateGameObject( + CreateCylinder(0.25f, 1.6f, 10, glm::vec3(0.9f, 0.9f, 0.9f)), + glm::vec3(std::cos(ang) * R, 0.8f, std::sin(ang) * R)); + boll->SetMetallic(0.9f); + boll->SetRoughness(0.3f); + boll->SetEmissive(glm::vec3(1.5f, 0.6f, 0.2f)); // initial; pulses + AddGameObject(boll); + namedObjects_[id] = boll; + bollard_ids_.push_back(id); + } + + // Jenga tower — 5 levels × 3 boxes, alternating orientation + for (int lvl = 0; lvl < 5; ++lvl) { + for (int i = 0; i < 3; ++i) { + bool along_x = (lvl % 2 == 0); + glm::vec3 pos, scale; + if (along_x) { + pos = glm::vec3(8.0f + (i - 1) * 0.7f, 0.35f + lvl * 0.7f, 6.0f); + scale = glm::vec3(0.6f, 0.6f, 2.0f); + } else { + pos = glm::vec3(8.0f, 0.35f + lvl * 0.7f, 6.0f + (i - 1) * 0.7f); + scale = glm::vec3(2.0f, 0.6f, 0.6f); + } + addBox("jen_" + std::to_string(lvl * 3 + i), + pos, scale, glm::vec3(0.92f, 0.86f, 0.72f), + wood_tex, noise_t, 0.0f, 0.65f, + 0.7f, 0.08f, 0.55f); + } + } + + // HDR pyre in the monolith's shadow — signature bloom source + { + auto logA = CreateGameObject( + CreateCylinder(0.18f, 1.3f, 8, glm::vec3(0.25f, 0.16f, 0.08f)), + glm::vec3(-6.0f, 0.2f, 0.0f)); + logA->SetRotation(glm::vec3(0, 30.0f, 90.0f)); + logA->SetTexture(wood_tex); logA->SetNormalMap(noise_t); + AddGameObject(logA); + auto logB = CreateGameObject( + CreateCylinder(0.18f, 1.3f, 8, glm::vec3(0.27f, 0.18f, 0.1f)), + glm::vec3(-6.0f, 0.25f, 0.0f)); + logB->SetRotation(glm::vec3(0, -30.0f, 90.0f)); + logB->SetTexture(wood_tex); logB->SetNormalMap(noise_t); + AddGameObject(logB); + auto flame1 = CreateGameObject( + CreateCone(0.45f, 1.4f, 10, glm::vec3(1.0f)), + glm::vec3(-6.0f, 0.9f, 0.0f)); + flame1->SetEmissive(glm::vec3(5.5f, 2.0f, 0.4f)); + AddGameObject(flame1); + auto flame2 = CreateGameObject( + CreateCone(0.22f, 0.8f, 10, glm::vec3(1.0f)), + glm::vec3(-6.0f, 1.05f, 0.0f)); + flame2->SetEmissive(glm::vec3(9.0f, 4.0f, 1.5f)); + AddGameObject(flame2); + } + + // -------------------- NORTH: PBR MATERIAL GRID ------------------ + const int N = 7; + glm::vec3 material_base(0.88f, 0.32f, 0.18f); // red-orange showpiece + for (int row = 0; row < N; ++row) { + for (int col = 0; col < N; ++col) { + float m = (float)row / (N - 1); + float r = (float)col / (N - 1) * 0.92f + 0.05f; + float xx = -9.0f + col * 3.0f; + float zz = 25.0f + row * 3.5f; + // Pedestal + auto ped = CreateGameObject( + CreateCube(glm::vec3(0.80f, 0.77f, 0.72f)), + glm::vec3(xx, 0.6f, zz)); + ped->SetScale(glm::vec3(1.3f, 1.2f, 1.3f)); + ped->SetTexture(marble_t); ped->SetNormalMap(noise_t); + ped->SetRoughness(0.45f); + AddGameObject(ped); + // Sphere on top + auto sph = CreateGameObject( + CreateSphere(48, material_base), + glm::vec3(xx, 1.9f, zz)); + sph->SetScale(glm::vec3(0.95f)); + sph->SetMetallic(m); + sph->SetRoughness(r); + AddGameObject(sph); + } + } + // Backdrop wall behind the gallery + for (int i = 0; i < 10; ++i) { + float xx = -13.5f + i * 3.0f; + auto b = CreateGameObject( + CreateCube(glm::vec3(0.82f, 0.78f, 0.72f)), + glm::vec3(xx, 3.5f, 50.0f)); + b->SetScale(glm::vec3(3.0f, 7.0f, 1.2f)); + b->SetTexture(brick_t); b->SetNormalMap(noise_t); + b->SetRoughness(0.82f); + AddGameObject(b); + } + + // -------------------- EAST: DOMINOES + BOWLING ------------------ + // Ramp for kickoff ball at far east + addBox("ramp", + glm::vec3(45.0f, 2.0f, 0.0f), glm::vec3(7.0f, 0.3f, 3.0f), + glm::vec3(0.85f, 0.78f, 0.65f), + wood_tex, noise_t, 0.0f, 0.7f, -1.0f, + 0.0f, 0.0f, glm::vec3(0, 0, -20.0f)); + + // 40-domino straight line west from x=38 to x=6 + for (int i = 0; i < 40; ++i) { + float x = 38.0f - i * 0.82f; + addBox("dom_" + std::to_string(i), + glm::vec3(x, 0.9f, 0.0f), glm::vec3(0.2f, 1.8f, 0.95f), + glm::vec3(0.92f, 0.88f, 0.75f), + wood_tex, noise_t, 0.0f, 0.6f, + 0.8f, 0.07f, 0.55f); + } + + // Bowling pins at west end of domino line + static const float pin_off[10][2] = { + { 0.0f, 0.0f}, + {-0.45f, 0.9f}, { 0.45f, 0.9f}, + {-0.9f, 1.8f}, { 0.0f, 1.8f}, { 0.9f, 1.8f}, + {-1.35f, 2.7f}, {-0.45f, 2.7f}, { 0.45f, 2.7f}, { 1.35f, 2.7f} + }; + for (int i = 0; i < 10; ++i) { + glm::vec3 p(4.0f - pin_off[i][1], 0.8f, pin_off[i][0]); + auto pin = CreateGameObject( + CreateCylinder(0.32f, 1.5f, 14, + glm::vec3(0.97f, 0.96f, 0.92f)), + p); + pin->SetMetallic(0.0f); + pin->SetRoughness(0.3f); + AddGameObject(pin); + std::string id = "pin_" + std::to_string(i); + namedObjects_[id] = pin; + bodyCmd(id, "box", 0.4f, 0.2f, 0.4f, false); + } + + // Impulse ball on top of the ramp — fires at t=2 + addSphere("kickoff", + glm::vec3(48.0f, 5.5f, 0.0f), 0.85f, + glm::vec3(0.75f, 0.78f, 0.82f), 0, 0, + 1.0f, 0.28f, + 9.0f, 0.3f, 0.25f); + + // -------------------- WEST: CATAPULT FRAME ----------------------- + // Tall wooden A-frame made of stacked boxes (purely decorative static) + auto postL = CreateGameObject( + CreateCube(glm::vec3(0.58f, 0.40f, 0.24f)), + glm::vec3(-45.0f, 4.0f, 3.0f)); + postL->SetScale(glm::vec3(0.6f, 8.0f, 0.6f)); + postL->SetRotation(glm::vec3(0, 0, 12.0f)); + postL->SetTexture(wood_tex); postL->SetNormalMap(noise_t); + postL->SetRoughness(0.8f); + AddGameObject(postL); + auto postR = CreateGameObject( + CreateCube(glm::vec3(0.58f, 0.40f, 0.24f)), + glm::vec3(-45.0f, 4.0f, -3.0f)); + postR->SetScale(glm::vec3(0.6f, 8.0f, 0.6f)); + postR->SetRotation(glm::vec3(0, 0, -12.0f)); + postR->SetTexture(wood_tex); postR->SetNormalMap(noise_t); + postR->SetRoughness(0.8f); + AddGameObject(postR); + auto crossbeam = CreateGameObject( + CreateCube(glm::vec3(0.58f, 0.40f, 0.24f)), + glm::vec3(-45.0f, 8.3f, 0.0f)); + crossbeam->SetScale(glm::vec3(0.8f, 0.8f, 8.0f)); + crossbeam->SetTexture(wood_tex); crossbeam->SetNormalMap(noise_t); + crossbeam->SetRoughness(0.8f); + AddGameObject(crossbeam); + + // Target crate pyramid in the center — catapult aims here + for (int y = 0; y < 3; ++y) { + int n = 4 - y; + for (int i = 0; i < n; ++i) { + float xx = -30.0f + i * 1.05f + (3 - n) * 0.5f; + float yp = 0.55f + y * 1.05f; + addBox("ccrate_" + std::to_string(y * 4 + i), + glm::vec3(xx, yp, 0.0f), glm::vec3(1.0f), + glm::vec3(0.80f, 0.70f, 0.48f), + wood_tex, noise_t, 0.0f, 0.7f, + 0.7f, 0.12f, 0.65f); + } + } + + // -------------------- SOUTH: WATERFALL + LAKE ------------------- + // East-west cliff wall at z=-25, spanning x from -24 to 24 (waterfall + // source). Height y=0..5.4. Lake is south (z<-25), arena is north. + for (int i = 0; i < 9; ++i) { + float xx = -24.0f + i * 6.0f; + auto b = CreateGameObject( + CreateCube(glm::vec3(0.80f, 0.76f, 0.70f)), + glm::vec3(xx, 2.7f, -25.0f)); + b->SetScale(glm::vec3(6.2f, 5.4f + jit(0.3f), 4.0f)); + b->SetRotation(glm::vec3(jit(2), jit(6), jit(2))); + b->SetTexture(rock_tex); b->SetNormalMap(noise_t); + b->SetRoughness(0.85f); + AddGameObject(b); + } + // Waterfall curtain sheet — along the south face of the cliff + { + auto sheet = CreateGameObject( + CreateCube(glm::vec3(0.75f, 0.90f, 0.98f)), + glm::vec3(0.0f, 2.6f, -27.0f)); + sheet->SetScale(glm::vec3(50.0f, 5.2f, 0.2f)); + sheet->SetMetallic(0.0f); + sheet->SetRoughness(0.12f); + sheet->SetEmissive(glm::vec3(0.5f, 0.8f, 1.0f)); + AddGameObject(sheet); + } + // Splash boulders at the foot of the cliff, on the lake side + for (int i = 0; i < 12; ++i) { + float xx = rnd(-22.0f, 22.0f); + float zz = rnd(-32.0f, -27.0f); + float s = rnd(0.5f, 1.2f); + auto r = CreateGameObject( + CreateSphere(14, glm::vec3(0.86f, 0.82f, 0.78f)), + glm::vec3(xx, s * 0.35f, zz)); + r->SetScale(glm::vec3(s, s * 0.6f, s * 0.95f)); + r->SetTexture(rock_tex); r->SetNormalMap(noise_t); + r->SetRoughness(0.7f); + AddGameObject(r); + } + // Lake-side trees + auto tree = [&](glm::vec3 at, float s) { + auto trunk = CreateGameObject( + CreateCylinder(0.22f * s, 2.5f * s, 10, + glm::vec3(0.88f, 0.84f, 0.78f)), + at + glm::vec3(0, 1.25f * s, 0)); + trunk->SetRotation(glm::vec3(jit(3), jit(360), jit(3))); + trunk->SetTexture(wood_tex); trunk->SetNormalMap(noise_t); + trunk->SetRoughness(0.85f); + AddGameObject(trunk); + glm::vec3 cc(rnd(0.12f, 0.22f), rnd(0.38f, 0.55f), rnd(0.13f, 0.20f)); + auto cone = CreateGameObject( + CreateCone(1.2f * s, 2.7f * s, 10, cc), + at + glm::vec3(0, 2.9f * s, 0)); + cone->SetNormalMap(noise_t); cone->SetRoughness(0.8f); + AddGameObject(cone); + }; + + // Floating barrels in the lake (flow carries them west) + for (int i = 0; i < 6; ++i) { + float xx = rnd(-10.0f, 5.0f); + float zz = rnd(-40.0f, -15.0f); + auto barrel = CreateGameObject( + CreateCylinder(0.7f, 1.3f, 12, glm::vec3(0.9f, 0.78f, 0.55f)), + glm::vec3(xx, 1.2f, zz)); + barrel->SetRotation(glm::vec3(0, jit(180.0f), 90.0f)); + barrel->SetTexture(wood_tex); barrel->SetNormalMap(noise_t); + barrel->SetRoughness(0.8f); + AddGameObject(barrel); + std::string id = "barrel_" + std::to_string(i); + namedObjects_[id] = barrel; + bodyCmd(id, "box", 1.0f, 0.3f, 0.4f, false); + } + + // -------------------- PERIPHERY: trees, rocks, mountains -------- + // Trees scattered around perimeter (avoiding zones) + for (int i = 0; i < 40; ++i) { + float x, z; + int tries = 0; + do { + x = rnd(-90.0f, 90.0f); + z = rnd(-60.0f, 90.0f); + tries++; + } while ((std::abs(x) < 50.0f && std::abs(z) < 30.0f) && tries < 20); // avoid center zones + tree(glm::vec3(x, 0, z), rnd(0.7f, 1.4f)); + } + + // Scatter boulders + for (int i = 0; i < 50; ++i) { + float x, z; + do { + x = rnd(-90.0f, 90.0f); + z = rnd(-60.0f, 90.0f); + } while (std::abs(x) < 30.0f && std::abs(z) < 25.0f); + float s = rnd(0.4f, 1.4f); + auto r = CreateGameObject( + CreateSphere(12, glm::vec3(0.84f, 0.81f, 0.76f)), + glm::vec3(x, s * 0.35f, z)); + r->SetScale(glm::vec3(s, s * rnd(0.5f, 0.8f), s * rnd(0.85f, 1.1f))); + r->SetRotation(glm::vec3(jit(12), jit(180), jit(12))); + r->SetTexture(rock_tex); r->SetNormalMap(noise_t); + r->SetRoughness(0.9f); + AddGameObject(r); + } + + // Distant mountains + for (int i = 0; i < 14; ++i) { + float x = -160.0f + i * 24.0f + jit(6.0f); + float z = -140.0f + jit(14.0f); + float s = rnd(0.9f, 1.4f); + float h = 22.0f * s; + auto m = CreateGameObject( + CreateCone(12.0f * s, h, 10, glm::vec3(0.22f, 0.20f, 0.24f)), + glm::vec3(x, h * 0.5f, z)); + AddGameObject(m); + } + // Wildflowers for bloom accents near the arena + for (int i = 0; i < 80; ++i) { + float x, z; + do { + x = rnd(-50.0f, 50.0f); + z = rnd(-15.0f, 45.0f); + } while (std::abs(x) < 16.0f && std::abs(z) < 10.0f); + glm::vec3 col; + int pick = (int)(rng_() % 5); + if (pick == 0) col = glm::vec3(1.5f, 1.1f, 0.3f); + else if (pick == 1) col = glm::vec3(1.4f, 0.4f, 0.8f); + else if (pick == 2) col = glm::vec3(1.2f, 1.2f, 1.1f); + else if (pick == 3) col = glm::vec3(0.75f, 0.45f, 1.3f); + else col = glm::vec3(1.5f, 0.5f, 0.2f); + auto fl = CreateGameObject(CreateSphere(6, col), + glm::vec3(x, 0.2f + jit(0.05f), z)); + fl->SetScale(glm::vec3(0.08f)); + fl->SetEmissive(col * 0.25f); + AddGameObject(fl); + } + + processAgentCommand(json::parse(R"({"action":"physics","enabled":true})")); + + // Camera: overlook from southwest, angled at the arena + glm::vec3 eye(-22.0f, 9.0f, 18.0f); + glm::vec3 target(4.0f, 3.0f, 0.0f); + SetCameraPosition(eye); + SetCameraTarget(target); + glm::vec3 fwd = glm::normalize(target - eye); + cam_pitch_ = glm::degrees(std::asin(fwd.y)); + cam_yaw_ = glm::degrees(std::atan2(fwd.z, fwd.x)); + last_cam_pos_ = eye; + + std::cout << "Physics Playground ready: " << game_objects_.size() << " objects.\n" + << " F / LMB / R1 — shoot an HDR metal ball\n" + << " T — cycle storm (calm / squall / gale)\n" + << " G — chaos: launch all dynamic bodies upward\n" + << " Every 18s the catapult hurls a heavy sphere at the arena.\n" + << " Domino cascade fires at t=2s.\n"; + return true; + } + + void FireBall() { + glm::vec3 cp = GetCameraPosition(); + glm::vec3 tg = GetCameraTarget(); + glm::vec3 fwd = glm::normalize(tg - cp); + if ((int)active_shots_.size() >= kMaxShots) { + auto old = active_shots_.front(); active_shots_.pop_front(); + processAgentCommand(json::parse("{\"action\":\"delete\",\"id\":\"" + old + "\"}")); + } + std::uniform_real_distribution cd(0.6f, 1.7f); + glm::vec3 col(cd(rng_), cd(rng_), cd(rng_)); + std::string id = "shot_" + std::to_string(shot_counter_++); + auto b = CreateGameObject(CreateSphere(20, col), cp + fwd * 1.4f); + b->SetScale(glm::vec3(0.35f)); + b->SetMetallic(1.0f); + b->SetRoughness(0.25f); + AddGameObject(b); + namedObjects_[id] = b; + json::Object bc; + bc["action"]=json::Value(std::string("body")); + bc["id"]=json::Value(id); + bc["shape"]=json::Value(std::string("sphere")); + bc["mass"]=json::Value(1.5); + bc["restitution"]=json::Value(0.55); + bc["friction"]=json::Value(0.3); + processAgentCommand(json::Value(bc)); + json::Object vc; + vc["action"]=json::Value(std::string("velocity")); + vc["id"]=json::Value(id); + vc["linear"]=json::Value(json::Array{ + json::Value((double)(fwd.x * 60)), + json::Value((double)(fwd.y * 60)), + json::Value((double)(fwd.z * 60))}); + processAgentCommand(json::Value(vc)); + active_shots_.push_back(id); + } + + void CycleStorm() { + storm_state_ = (storm_state_ + 1) % 3; + float s = storm_state_ == 0 ? 0.0f : (storm_state_ == 1 ? 0.4f : 0.85f); + json::Object w; + w["action"] = json::Value(std::string("weather")); + w["storminess"] = json::Value((double)s); + w["wind_dir"] = json::Value(json::Array{ + json::Value(-1.0), json::Value(0.0), json::Value(0.3)}); + w["wind_speed"] = json::Value((double)(0.5 + s * 4.0)); + processAgentCommand(json::Value(w)); + std::cout << "[storm] state=" << storm_state_ << " storminess=" << s << "\n"; + } + + void ChaosBurst() { + std::uniform_real_distribution rx(-4.0f, 4.0f); + std::uniform_real_distribution ry(10.0f, 22.0f); + std::uniform_real_distribution rr(-6.0f, 6.0f); + for (auto& [name, handle] : bodyHandleByName_) { + phys::Body* b = physics_.Get(handle); + if (!b || b->inv_mass == 0.0f) continue; + b->linear_velocity += glm::vec3(rx(rng_), ry(rng_), rx(rng_)); + b->angular_velocity += glm::vec3(rr(rng_), rr(rng_), rr(rng_)); + b->sleeping = false; + b->sleep_timer = 0.0f; + } + std::cout << "[chaos] launched " << bodyHandleByName_.size() << " bodies\n"; + } + + void FireCatapult() { + if ((int)active_cata_.size() >= kMaxCata) { + auto old = active_cata_.front(); active_cata_.pop_front(); + processAgentCommand(json::parse("{\"action\":\"delete\",\"id\":\"" + old + "\"}")); + } + std::string id = "cata_" + std::to_string(cata_counter_++); + auto b = CreateGameObject( + CreateSphere(32, glm::vec3(0.55f, 0.48f, 0.40f)), + glm::vec3(-45.0f, 9.2f, 0.0f)); + b->SetScale(glm::vec3(1.2f)); + b->SetMetallic(1.0f); + b->SetRoughness(0.45f); + AddGameObject(b); + namedObjects_[id] = b; + json::Object bc; + bc["action"]=json::Value(std::string("body")); + bc["id"]=json::Value(id); + bc["shape"]=json::Value(std::string("sphere")); + bc["mass"]=json::Value(15.0); // heavy projectile + bc["restitution"]=json::Value(0.35); + bc["friction"]=json::Value(0.3); + processAgentCommand(json::Value(bc)); + json::Object vc; + vc["action"]=json::Value(std::string("velocity")); + vc["id"]=json::Value(id); + // Hurl toward +X and slightly down, aimed at the crate pyramid + vc["linear"]=json::Value(json::Array{ + json::Value(22.0), json::Value(-2.0), json::Value(0.0)}); + processAgentCommand(json::Value(vc)); + active_cata_.push_back(id); + std::cout << "[catapult] fire\n"; + } + + void EmitDroplet() { + if ((int)active_drops_.size() >= kMaxDrops) { + auto old = active_drops_.front(); active_drops_.pop_front(); + processAgentCommand(json::parse("{\"action\":\"delete\",\"id\":\"" + old + "\"}")); + } + // Spawn along the top of the cliff (x in [-24, 24]), z just south of cliff + std::uniform_real_distribution xx(-22.0f, 22.0f); + std::uniform_real_distribution zz(-27.5f, -26.0f); + std::uniform_real_distribution yy(-0.15f, 0.25f); + std::string id = "drop_" + std::to_string(drop_counter_++); + glm::vec3 spawn(xx(drop_rng_), 5.5f + yy(drop_rng_), zz(drop_rng_)); + auto d = CreateGameObject( + CreateSphere(8, glm::vec3(0.7f, 0.88f, 1.0f)), + spawn); + d->SetScale(glm::vec3(0.18f)); + d->SetMetallic(0.0f); + d->SetRoughness(0.15f); + d->SetEmissive(glm::vec3(0.4f, 0.7f, 0.95f)); + AddGameObject(d); + namedObjects_[id] = d; + json::Object bc; + bc["action"]=json::Value(std::string("body")); + bc["id"]=json::Value(id); + bc["shape"]=json::Value(std::string("sphere")); + bc["mass"]=json::Value(0.08); + bc["restitution"]=json::Value(0.2); + bc["friction"]=json::Value(0.05); + processAgentCommand(json::Value(bc)); + // Initial velocity: southward + slight downward + std::uniform_real_distribution vz(-2.5f, -1.0f); + std::uniform_real_distribution vy(-1.5f, -0.5f); + std::uniform_real_distribution vx(-0.4f, 0.4f); + json::Object vc; + vc["action"]=json::Value(std::string("velocity")); + vc["id"]=json::Value(id); + vc["linear"]=json::Value(json::Array{ + json::Value((double)vx(drop_rng_)), + json::Value((double)vy(drop_rng_)), + json::Value((double)vz(drop_rng_))}); + processAgentCommand(json::Value(vc)); + active_drops_.push_back(id); + } + + void OnUpdate(float dt) override { + time_ += dt; + + // Pulse emissive bollards + { + float pulse = 0.4f + 0.6f * (0.5f + 0.5f * std::sin(time_ * 2.3f)); + glm::vec3 hot(3.0f * pulse, 1.3f * pulse, 0.4f * pulse); + for (auto& id : bollard_ids_) { + auto it = namedObjects_.find(id); + if (it != namedObjects_.end()) it->second->SetEmissive(hot); + } + } + + // Kickoff chain reaction at t=2s + if (!kickoff_fired_ && time_ > 2.0f) { + kickoff_fired_ = true; + json::Object c; + c["action"]=json::Value(std::string("impulse")); + c["id"]=json::Value(std::string("kickoff")); + c["impulse"]=json::Value(json::Array{ + json::Value(-150.0), json::Value(0.0), json::Value(0.0)}); + processAgentCommand(json::Value(c)); + } + + // Periodic catapult + catapult_timer_ -= dt; + if (catapult_timer_ <= 0.0f) { + FireCatapult(); + catapult_timer_ = 18.0f; + } + + // Waterfall emitter (~20/sec) + drop_timer_ -= dt; + while (drop_timer_ <= 0.0f) { EmitDroplet(); drop_timer_ += 0.05f; } + + // Shoot (F / LMB / R1) + shot_cooldown_ -= dt; + if (shot_cooldown_ <= 0.0f) { + bool fire = Input::IsKeyHeld(GLFW_KEY_F) + || Input::IsMouseButtonPressed(Input::MouseButton::Left) + || Input::IsGamepadButtonDown(Input::GamepadButton::R1); + if (fire) { FireBall(); shot_cooldown_ = 0.14f; } + } + + // T toggle storm (edge trigger) + bool t_now = Input::IsKeyHeld(GLFW_KEY_T) + || Input::IsGamepadButtonDown(Input::GamepadButton::Triangle); + if (t_now && !t_down_prev_) CycleStorm(); + t_down_prev_ = t_now; + + // G chaos (edge trigger) + bool g_now = Input::IsKeyHeld(GLFW_KEY_G) + || Input::IsGamepadButtonDown(Input::GamepadButton::Circle); + if (g_now && !g_down_prev_) ChaosBurst(); + g_down_prev_ = g_now; + + // --- Free-fly camera --- + glm::vec3 eng_pos = GetCameraPosition(); + if (glm::length(eng_pos - last_cam_pos_) > 0.0001f) { + glm::vec3 fwd = GetCameraTarget() - eng_pos; + if (glm::length(fwd) > 0.0001f) { + fwd = glm::normalize(fwd); + cam_pitch_ = glm::degrees(std::asin(fwd.y)); + cam_yaw_ = glm::degrees(std::atan2(fwd.z, fwd.x)); + } + } + glm::vec2 ls{0}, rs{0}; + float up = 0, down = 0, speed = cam_speed_; + bool touched = false; + if (Input::IsGamepadConnected()) { + ls = Input::GetLeftStick(); + rs = Input::GetRightStick(); + up = Input::GetR2(); + down = Input::GetL2(); + if (Input::IsGamepadButtonDown(Input::GamepadButton::Cross)) speed *= 3.0f; + if (glm::length(ls) + glm::length(rs) + up + down > 0.01f) touched = true; + } + if (Input::IsKeyHeld(GLFW_KEY_LEFT_SHIFT)) speed *= 3.0f; + if (Input::IsKeyHeld(GLFW_KEY_W)) { ls.y -= 1.0f; touched = true; } + if (Input::IsKeyHeld(GLFW_KEY_S)) { ls.y += 1.0f; touched = true; } + if (Input::IsKeyHeld(GLFW_KEY_A)) { ls.x -= 1.0f; touched = true; } + if (Input::IsKeyHeld(GLFW_KEY_D)) { ls.x += 1.0f; touched = true; } + if (Input::IsKeyHeld(GLFW_KEY_LEFT)) { rs.x -= 1.0f; touched = true; } + if (Input::IsKeyHeld(GLFW_KEY_RIGHT)) { rs.x += 1.0f; touched = true; } + if (Input::IsKeyHeld(GLFW_KEY_UP)) { rs.y -= 1.0f; touched = true; } + if (Input::IsKeyHeld(GLFW_KEY_DOWN)) { rs.y += 1.0f; touched = true; } + if (Input::IsKeyHeld(GLFW_KEY_SPACE)) { up += 1.0f; touched = true; } + if (Input::IsKeyHeld(GLFW_KEY_LEFT_CONTROL)) { down += 1.0f; touched = true; } + if (!touched) { last_cam_pos_ = eng_pos; return; } + cam_yaw_ += rs.x * look_sensitivity_ * dt; + cam_pitch_ -= rs.y * look_sensitivity_ * dt; + if (cam_pitch_ > 89.0f) cam_pitch_ = 89.0f; + if (cam_pitch_ < -89.0f) cam_pitch_ = -89.0f; + glm::vec3 fwd; + fwd.x = std::cos(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_)); + fwd.y = std::sin(glm::radians(cam_pitch_)); + fwd.z = std::sin(glm::radians(cam_yaw_)) * std::cos(glm::radians(cam_pitch_)); + fwd = glm::normalize(fwd); + glm::vec3 right = glm::normalize(glm::cross(fwd, glm::vec3(0, 1, 0))); + glm::vec3 pos = eng_pos; + pos += fwd * (-ls.y) * speed * dt; + pos += right * ls.x * speed * dt; + pos.y += (up - down) * speed * dt; + SetCameraPosition(pos); + SetCameraTarget(pos + fwd); + last_cam_pos_ = pos; + } +}; + + // --------------------------------------------- // WaterfallDemo: natural waterfall over a rocky cliff. Showcases: // - Two water surfaces at different elevations (upper stream, lower pool) @@ -2781,6 +5604,26 @@ static void runScene(const std::string& mode) { 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;