sanctuary tweaks

This commit is contained in:
will
2026-04-27 22:18:55 +01:00
parent eae0bb7f8d
commit 3833b3e676
4 changed files with 223 additions and 102 deletions

BIN
.DS_Store vendored

Binary file not shown.

Binary file not shown.

View File

@@ -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<InitialState> initial_states_;
public:
SanctuaryDemo()
: Engine(1920, 1080, "ZoomEngine - Sanctuary (Full Showcase)") {}
void ResetScene();
void FireBall() {
glm::vec3 cam_pos = GetCameraPosition();
glm::vec3 cam_tgt = GetCameraTarget();
@@ -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
for (int i = 0; i < 12; ++i) {
float x = 12.0f - i * 1.6f;
addBox("dom_" + std::to_string(i),
glm::vec3(x, 1.0f, 0), glm::vec3(0.3f, 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);
}
// 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,
// 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);
(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;
addBox("dom2_" + std::to_string(i),
glm::vec3(x, 1.0f, z), glm::vec3(0.28f, 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));
// Primary domino chain — 12 boards along z=0, between ramp and forge.
// Spacing 1.5 < domino height 2.0, so each falling domino's tip
// overlaps the next domino's base — the chain propagates reliably.
for (int i = 0; i < 12; ++i) {
float x = 22.0f - i * 1.5f;
addBox("dom_" + std::to_string(i),
glm::vec3(x, 1.0f, 0.0f), glm::vec3(0.25f, 2.0f, 1.2f),
glm::vec3(0.9f, 0.85f, 0.7f),
wood_tex, noise_tex, 0.0f, 0.6f,
0.6f, 0.05f, 0.7f);
}
// Second crate pyramid, east of the first
// Secondary domino chain, parallel to the first at z = +6.
for (int i = 0; i < 12; ++i) {
float x = 22.0f - i * 1.5f;
addBox("dom2_" + std::to_string(i),
glm::vec3(x, 1.0f, 6.0f), glm::vec3(0.25f, 2.0f, 1.1f),
glm::vec3(0.9f, 0.82f, 0.65f),
wood_tex, noise_tex, 0.0f, 0.65f,
0.6f, 0.05f, 0.7f);
}
// Crate pyramid, north end of physics section.
for (int y = 0; y < 3; ++y) {
int n = 4 - y;
for (int i = 0; i < n; ++i) {
float x = 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