diff --git a/.DS_Store b/.DS_Store index 9a874b5..375e40c 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/build/CMakeFiles/ModernEngine.dir/src/main.cpp.o b/build/CMakeFiles/ModernEngine.dir/src/main.cpp.o index f4cfc0c..399eca2 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/ModernEngine b/build/ModernEngine index 2762116..bea9eed 100755 Binary files a/build/ModernEngine and b/build/ModernEngine differ diff --git a/src/main.cpp b/src/main.cpp index a1150b9..64273b9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2026,6 +2026,33 @@ public: "normal":[0,1,0],"offset":0,"static":true })")); + // --- Perimeter physics borders: static stone walls ringing the + // 160×160 map (X,Z in [-80,80]) so the player and dynamic props + // can't leave the play area. Pink-tinted marble blends with the + // sakura quadrant and reads as neutral stone against the others. + { + const float kBL = 162.0f; // wall length (slight corner overlap) + const float kBH = 4.0f; // wall height (above jump apex) + const float kBT = 1.0f; // wall thickness + const glm::vec3 kBC(0.95f, 0.82f, 0.85f); + // North wall (z = +80) + addBox("border_n", glm::vec3(0.0f, kBH * 0.5f, 80.5f), + glm::vec3(kBL, kBH, kBT), kBC, + marble_t, noise_t, 0.0f, 0.75f, -1.0f); + // South wall (z = -80) + addBox("border_s", glm::vec3(0.0f, kBH * 0.5f, -80.5f), + glm::vec3(kBL, kBH, kBT), kBC, + marble_t, noise_t, 0.0f, 0.75f, -1.0f); + // East wall (x = +80) + addBox("border_e", glm::vec3( 80.5f, kBH * 0.5f, 0.0f), + glm::vec3(kBT, kBH, kBL), kBC, + marble_t, noise_t, 0.0f, 0.75f, -1.0f); + // West wall (x = -80) + addBox("border_w", glm::vec3(-80.5f, kBH * 0.5f, 0.0f), + glm::vec3(kBT, kBH, kBL), kBC, + marble_t, noise_t, 0.0f, 0.75f, -1.0f); + } + // ============================ FOREST (NW) ===================== // Trees + mossy boulders + glowing mushrooms + fallen logs auto tree = [&](glm::vec3 at, float s) { @@ -4274,10 +4301,25 @@ class SanctuaryDemo : public Engine { static constexpr int kMaxShots = 60; std::mt19937 shot_rng_{1337}; + // Captured initial pose+body params for every dynamic object, so R / Triangle + // can put the scene back to t=0. + struct InitialState { + std::string id; + std::string shape; // "box" or "sphere" + glm::vec3 position; + glm::vec3 rotation; // Euler degrees + float mass; + float restitution; + float friction; + }; + std::vector initial_states_; + public: SanctuaryDemo() : Engine(1920, 1080, "ZoomEngine - Sanctuary (Full Showcase)") {} + void ResetScene(); + void FireBall() { glm::vec3 cam_pos = GetCameraPosition(); glm::vec3 cam_tgt = GetCameraTarget(); @@ -4351,20 +4393,23 @@ public: "fog_density":0.011 })")); - // --- Water & weather --- + // --- Water: confined to a single central pool (visual only via water2). + // Main water mesh is disabled so the rest of the map is dry land. + processAgentCommand(json::parse(R"({"action":"water","enabled":false})")); processAgentCommand(json::parse(R"({ - "action":"water","enabled":true, - "level":-0.45,"size":420, - "amplitude":0.18,"wavelength":13,"speed":0.8, - "shallow":[0.55,0.42,0.30],"deep":[0.03,0.07,0.13], - "density":1.1,"resolution":180 + "action":"water2","enabled":true, + "center":[0, 0, -22], "size":14, "level":0.18, + "amplitude":0.06,"wavelength":4,"speed":0.7, + "shallow":[0.45,0.55,0.55], + "deep":[0.04,0.10,0.16], + "resolution":96 })")); processAgentCommand(json::parse(R"({ "action":"weather","wind_dir":[0.9,0,0.4], - "wind_speed":0.6,"storminess":0.08,"current_coef":0.5 + "wind_speed":0.6,"storminess":0.05,"current_coef":0.3 })")); - // --- Ground --- + // --- Ground (visual + solid physics floor) --- auto grass = CreateGameObject( CreatePlane(420.0f, 420.0f, glm::vec3(0.95f, 0.97f, 0.92f)), glm::vec3(0, 0, 0)); @@ -4373,6 +4418,13 @@ public: grass->SetUVScale(glm::vec2(0.35f)); grass->SetRoughness(0.95f); AddGameObject(grass); + // Register the grass as the ground physics body. The plane collider is + // infinite at y=0, so it catches anything anywhere on the map. + namedObjects_["__ground"] = grass; + processAgentCommand(json::parse(R"({ + "action":"body","id":"__ground","shape":"plane", + "normal":[0,1,0],"offset":0,"static":true + })")); // --- Textures / cached refs --- GLuint wood_tex = TextureCache::Get("wood"); @@ -4414,6 +4466,8 @@ public: if (mass > 0 || mass == -1.0f) // -1 = static bodyCmd(id, "box", (mass == -1.0f) ? 0 : mass, rest, frict, mass == -1.0f); + if (!id.empty() && mass > 0) + initial_states_.push_back({id, "box", pos, rot, mass, rest, frict}); return obj; }; @@ -4430,9 +4484,57 @@ public: AddGameObject(obj); if (!id.empty()) namedObjects_[id] = obj; if (mass > 0) bodyCmd(id, "sphere", mass, rest, frict); + if (!id.empty() && mass > 0) + initial_states_.push_back({id, "sphere", pos, glm::vec3(0), + mass, rest, frict}); return obj; }; + // --- Map perimeter walls: 4 static stone walls ringing an 80×80 play + // area (X,Z in [-40,40]) so dynamic props can't bounce out of the map. + { + const float kBL = 81.0f; // wall length (slight corner overlap) + const float kBH = 6.0f; // wall height + const float kBT = 1.0f; // wall thickness + const glm::vec3 kBC(0.92f, 0.88f, 0.82f); + addBox("border_n", glm::vec3(0.0f, kBH * 0.5f, 40.5f), + glm::vec3(kBL, kBH, kBT), kBC, + marble_tex, noise_tex, 0.0f, 0.75f, -1.0f); + addBox("border_s", glm::vec3(0.0f, kBH * 0.5f, -40.5f), + glm::vec3(kBL, kBH, kBT), kBC, + marble_tex, noise_tex, 0.0f, 0.75f, -1.0f); + addBox("border_e", glm::vec3( 40.5f, kBH * 0.5f, 0.0f), + glm::vec3(kBT, kBH, kBL), kBC, + marble_tex, noise_tex, 0.0f, 0.75f, -1.0f); + addBox("border_w", glm::vec3(-40.5f, kBH * 0.5f, 0.0f), + glm::vec3(kBT, kBH, kBL), kBC, + marble_tex, noise_tex, 0.0f, 0.75f, -1.0f); + } + + // --- Central pool curb: short stone walls forming a basin around the + // visible water at center=[0,0,-22], size 14. The curb hides the visual + // water seam at the edge and stops things rolling through into the pool. + { + const float kPC = -22.0f; // pool center Z + const float kPS = 14.0f; // pool size (matches water2) + const float kPH = 0.6f; // curb height + const float kPT = 0.5f; // curb thickness + const glm::vec3 kPCol(0.78f, 0.74f, 0.70f); + float half = kPS * 0.5f; + addBox("pool_n", glm::vec3(0.0f, kPH * 0.5f, kPC + half + kPT * 0.5f), + glm::vec3(kPS + kPT, kPH, kPT), kPCol, + marble_tex, noise_tex, 0.0f, 0.55f, -1.0f); + addBox("pool_s", glm::vec3(0.0f, kPH * 0.5f, kPC - half - kPT * 0.5f), + glm::vec3(kPS + kPT, kPH, kPT), kPCol, + marble_tex, noise_tex, 0.0f, 0.55f, -1.0f); + addBox("pool_e", glm::vec3( half + kPT * 0.5f, kPH * 0.5f, kPC), + glm::vec3(kPT, kPH, kPS), kPCol, + marble_tex, noise_tex, 0.0f, 0.55f, -1.0f); + addBox("pool_w", glm::vec3(-half - kPT * 0.5f, kPH * 0.5f, kPC), + glm::vec3(kPT, kPH, kPS), kPCol, + marble_tex, noise_tex, 0.0f, 0.55f, -1.0f); + } + // --- Central forge: stone altar + emissive brazier --- addBox("altar", glm::vec3(0, 0.75f, 0), glm::vec3(3.0f, 1.5f, 3.0f), @@ -4487,89 +4589,93 @@ public: 1.0f, 0.2f, 0.0f); // static visual - // --- Physics chain-reaction setup --- - // A launch ramp angled down from east toward origin; metal ball at top. - // Ramp is a tilted box; static. + // ============================================================ + // PHYSICS PLAYGROUND SECTION — east half of the map (X >= +5). + // Self-contained chain reaction + bonus props, kept clear of the + // forge (origin) and the southern pool. + // ============================================================ + + // Static launch ramp. Tilted -18° around Z so the +X end (where the + // ball spawns) is the LOW end and the -X end is the HIGH end. Ball + // gets a strong -X kick at t=2s, slides off the high end, falls onto + // the floor and continues into the domino chain. addBox("ramp", - glm::vec3(22.0f, 1.9f, 0.0f), glm::vec3(7.0f, 0.3f, 3.0f), + glm::vec3(28.0f, 1.9f, 0.0f), glm::vec3(7.0f, 0.3f, 3.0f), glm::vec3(0.9f, 0.85f, 0.78f), wood_tex, noise_tex, 0.0f, 0.7f, -1.0f, - 0.0f, 0.0f, glm::vec3(0, 0, -18.0f)); // tilt down + 0.0f, 0.0f, glm::vec3(0, 0, -18.0f)); - // Domino chain — 12 wooden boards standing in a line from origin out + // Heavy metal impulse ball, sitting near the +X (low) end of the ramp. + addSphere("impulse_ball", + glm::vec3(30.0f, 4.2f, 0.0f), 0.9f, + glm::vec3(0.75f, 0.78f, 0.82f), 0, 0, + 1.0f, 0.3f, + 8.0f, 0.3f, 0.25f); + + // Primary domino chain — 12 boards along z=0, between ramp and forge. + // Spacing 1.5 < domino height 2.0, so each falling domino's tip + // overlaps the next domino's base — the chain propagates reliably. for (int i = 0; i < 12; ++i) { - float x = 12.0f - i * 1.6f; + float x = 22.0f - i * 1.5f; addBox("dom_" + std::to_string(i), - glm::vec3(x, 1.0f, 0), glm::vec3(0.3f, 2.0f, 1.2f), + glm::vec3(x, 1.0f, 0.0f), glm::vec3(0.25f, 2.0f, 1.2f), glm::vec3(0.9f, 0.85f, 0.7f), wood_tex, noise_tex, 0.0f, 0.6f, - 1.0f, 0.08f, 0.55f); + 0.6f, 0.05f, 0.7f); } - // Crate stack — pyramid of wooden crates east of dominoes, near shore - for (int y = 0; y < 3; ++y) { - int n = 4 - y; - for (int i = 0; i < n; ++i) { - float x = -10.0f - (3 - n) * 0.5f + i * 1.1f; - float yp = 0.5f + y * 1.0f; - addBox("crate_" + std::to_string(y * 4 + i), - glm::vec3(x, yp, -8.0f), glm::vec3(1.0f), - glm::vec3(0.85f, 0.75f, 0.55f), - wood_tex, noise_tex, 0.0f, 0.7f, - 0.8f, 0.1f, 0.7f); - } - } - - // Impulse ball — heavy metal sphere at top of ramp - auto ball = addSphere("impulse_ball", - glm::vec3(25.0f, 5.5f, 0.0f), 0.9f, - glm::vec3(0.75f, 0.78f, 0.82f), 0, 0, - 1.0f, 0.3f, - 8.0f, 0.3f, 0.25f); - (void)ball; - - // --- Bonus physics content: much more to knock around --- - - // Second domino line at an angle, north of the first - for (int i = 0; i < 14; ++i) { - float x = 8.0f - i * 1.4f; - float z = -4.0f - i * 0.4f; + // Secondary domino chain, parallel to the first at z = +6. + for (int i = 0; i < 12; ++i) { + float x = 22.0f - i * 1.5f; addBox("dom2_" + std::to_string(i), - glm::vec3(x, 1.0f, z), glm::vec3(0.28f, 2.0f, 1.1f), + glm::vec3(x, 1.0f, 6.0f), glm::vec3(0.25f, 2.0f, 1.1f), glm::vec3(0.9f, 0.82f, 0.65f), wood_tex, noise_tex, 0.0f, 0.65f, - 0.9f, 0.08f, 0.55f, - glm::vec3(0, 16.0f, 0)); + 0.6f, 0.05f, 0.7f); } - // Second crate pyramid, east of the first + // Crate pyramid, north end of physics section. for (int y = 0; y < 3; ++y) { int n = 4 - y; for (int i = 0; i < n; ++i) { - float x = 4.0f + i * 1.1f + (3 - n) * 0.5f; + float x = 14.0f + i * 1.1f + (3 - n) * 0.5f; float yp = 0.5f + y * 1.0f; - float z = -14.0f; - addBox("crate2_" + std::to_string(y * 4 + i), - glm::vec3(x, yp, z), glm::vec3(1.0f), + addBox("crate_" + std::to_string(y * 4 + i), + glm::vec3(x, yp, 14.0f), glm::vec3(1.0f), glm::vec3(0.85f, 0.75f, 0.55f), wood_tex, noise_tex, 0.0f, 0.7f, 0.8f, 0.1f, 0.7f); } } - // Third crate wall (flat stack), on the west side + // Second crate pyramid, mid-section. + for (int y = 0; y < 3; ++y) { + int n = 4 - y; + for (int i = 0; i < n; ++i) { + float x = 26.0f + i * 1.1f + (3 - n) * 0.5f; + float yp = 0.5f + y * 1.0f; + addBox("crate2_" + std::to_string(y * 4 + i), + glm::vec3(x, yp, 14.0f), glm::vec3(1.0f), + glm::vec3(0.85f, 0.75f, 0.55f), + wood_tex, noise_tex, 0.0f, 0.7f, + 0.8f, 0.1f, 0.7f); + } + } + + // Concrete crate wall, eastern edge of section. for (int i = 0; i < 12; ++i) { int col = i % 4, row = i / 4; - float x = -18.0f + col * 1.1f; + float x = 32.0f + col * 1.1f; float y = 0.5f + row * 1.1f; addBox("wallc_" + std::to_string(i), - glm::vec3(x, y, -6.0f), glm::vec3(1.0f), - glm::vec3(0.65f, 0.78f, 0.85f), // blueish + glm::vec3(x, y, 22.0f), glm::vec3(1.0f), + glm::vec3(0.65f, 0.78f, 0.85f), concrete, noise_tex, 0.0f, 0.75f, 0.6f, 0.2f, 0.6f); } - // Bowling-pin triangle near the center, 10 pins + // Bowling-pin triangle, set up to receive whatever falls past the + // domino chain. static const float pin_offsets[10][2] = { { 0.0f, 0.0f}, {-0.45f, 0.8f}, { 0.45f, 0.8f}, @@ -4577,8 +4683,8 @@ public: {-1.35f, 2.4f}, {-0.45f, 2.4f}, { 0.45f, 2.4f}, { 1.35f, 2.4f} }; for (int i = 0; i < 10; ++i) { - float px = 16.0f + pin_offsets[i][1]; - float pz = 3.0f + pin_offsets[i][0]; + float px = 14.0f - pin_offsets[i][1]; + float pz = -8.0f + pin_offsets[i][0]; auto pin = CreateGameObject( CreateCylinder(0.35f, 1.6f, 14, glm::vec3(0.98f, 0.96f, 0.92f)), glm::vec3(px, 0.8f, pz)); @@ -4587,26 +4693,29 @@ public: AddGameObject(pin); std::string id = "pin_" + std::to_string(i); namedObjects_[id] = pin; - bodyCmd(id, "box", 0.5f, 0.2f, 0.4f); // box collider is fine for pins + bodyCmd(id, "box", 0.5f, 0.2f, 0.4f); + initial_states_.push_back({id, "box", + glm::vec3(px, 0.8f, pz), glm::vec3(0), 0.5f, 0.2f, 0.4f}); } - // Tall cube tower (8 blocks high) inviting to be knocked down + // Cube tower, southeast corner of section. for (int i = 0; i < 8; ++i) { float y = 0.6f + i * 1.2f; addBox("tower_" + std::to_string(i), - glm::vec3(-15.0f, y, 8.0f), glm::vec3(1.2f), + glm::vec3(30.0f, y, -18.0f), glm::vec3(1.2f), glm::vec3(0.85f, 0.80f, 0.72f), marble_tex, noise_tex, 0.0f, 0.5f, 0.9f, 0.12f, 0.55f, glm::vec3(0, i * 7.0f, 0)); } - // Scattered loose objects around the forge area for casual physics - for (int i = 0; i < 20; ++i) { - float ang = (float)i / 20.0f * 2 * M_PI + rnd(-0.1f, 0.1f); - float R = 18.0f + rnd(-3.0f, 3.0f); - float x = std::cos(ang) * R; - float z = std::sin(ang) * R; + // Scattered loose objects across the eastern half (avoiding the pool + // and the forge). + for (int i = 0; i < 16; ++i) { + float x = rnd(8.0f, 36.0f); + float z = rnd(-30.0f, 30.0f); + // Skip cells that overlap key set-pieces. + if (z > -2.0f && z < 8.0f && x > 6.0f && x < 24.0f) continue; bool is_sphere = (i % 2) == 0; std::string id = "loose_" + std::to_string(i); if (is_sphere) { @@ -4627,35 +4736,6 @@ public: } } - // --- Floating barrels on the lake (buoyancy demo) --- - for (int i = 0; i < 5; ++i) { - float x = rnd(-20.0f, 20.0f); - float z = rnd(-55.0f, -25.0f); - auto barrel = CreateGameObject( - CreateCylinder(0.8f, 1.4f, 12, - glm::vec3(0.9f, 0.78f, 0.55f)), - glm::vec3(x, 1.5f, z)); - barrel->SetRotation(glm::vec3(0, jit(180.0f), 90.0f)); - barrel->SetTexture(wood_tex); - barrel->SetNormalMap(noise_tex); - 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); // approximate as box - } - - // Floating metal orbs - for (int i = 0; i < 3; ++i) { - float x = rnd(-12.0f, 12.0f); - float z = rnd(-50.0f, -30.0f); - addSphere("orb_float_" + std::to_string(i), - glm::vec3(x, 1.2f, z), 0.7f, - glm::vec3(0.92f, 0.65f, 0.35f), 0, 0, - 1.0f, 0.3f, - 0.7f, 0.4f); - } - // --- Distant mountain silhouettes --- for (int i = 0; i < 13; ++i) { float x = -175.0f + i * 28.0f + jit(6.0f); @@ -4774,27 +4854,36 @@ public: std::cout << "Sanctuary demo ready: " << game_objects_.size() << " objects. Chain reaction fires at t=2s.\n" << "Camera: WASD/arrows, Space/LCtrl = up/down, LShift = sprint.\n" - << "SHOOT: hold F, click LMB, or hold R1 (gamepad) to fire balls.\n"; + << "SHOOT: hold F, click LMB, or hold R1 (gamepad) to fire balls.\n" + << "RESET: press R or Triangle (gamepad) to restart the demo.\n"; return true; } void OnUpdate(float dt) override { time_elapsed_ += dt; - // Kick off the chain reaction 2 seconds in, once physics is warm + // Kick off the chain reaction 2 seconds in, once physics is warm. + // Heavy -X impulse sends the ball off the ramp and into the first + // domino at x=22. if (!kickoff_fired_ && time_elapsed_ > 2.0f) { kickoff_fired_ = true; - // Big left-ward nudge so the ball rolls down the ramp into dominoes json::Object c; c["action"] = json::Value(std::string("impulse")); c["id"] = json::Value(std::string("impulse_ball")); c["impulse"] = json::Value(json::Array{ - json::Value(-120.0), + json::Value(-260.0), json::Value(0.0), json::Value(0.0)}); processAgentCommand(json::Value(c)); } + // Reset (R / Triangle) — restore every dynamic prop to its initial + // pose with zero velocity, clear shot balls, and re-arm the kickoff. + if (Input::IsKeyPressed(GLFW_KEY_R) + || Input::IsGamepadButtonPressed(Input::GamepadButton::Triangle)) { + ResetScene(); + } + // Shoot balls from camera: F / LMB / R1 — rate-limited shot_cooldown_ -= dt; if (shot_cooldown_ <= 0.0f) { @@ -4867,6 +4956,38 @@ public: } }; +void SanctuaryDemo::ResetScene() { + // Restore each captured dynamic body to its initial pose with zero + // velocity. Re-issuing the "body" command rebuilds the physics body at + // the GameObject's current position (so we set that first) with linear + // and angular velocity = 0. + for (const auto& s : initial_states_) { + auto it = namedObjects_.find(s.id); + if (it == namedObjects_.end()) continue; + it->second->SetPosition(s.position); + it->second->SetRotation(s.rotation); + json::Object c; + c["action"] = json::Value(std::string("body")); + c["id"] = json::Value(s.id); + c["shape"] = json::Value(s.shape); + c["mass"] = json::Value((double)s.mass); + c["restitution"] = json::Value((double)s.restitution); + c["friction"] = json::Value((double)s.friction); + processAgentCommand(json::Value(c)); + } + // Despawn any in-flight player-fired balls. + for (const auto& id : active_shots_) { + json::Object dc; + dc["action"] = json::Value(std::string("delete")); + dc["id"] = json::Value(id); + processAgentCommand(json::Value(dc)); + } + active_shots_.clear(); + // Re-arm the chain-reaction kickoff. + time_elapsed_ = 0.0f; + kickoff_fired_ = false; +} + // --------------------------------------------- // SunsetForestDemo: a composed, cinematic scene built to show off the whole