#include "Zone.h" #include #include namespace bs { namespace { constexpr float kPi = 3.14159265f; // Distance from a wobbly blob's center, normalized so the edge sits at `radius`. float blob(glm::vec2 p, glm::vec2 c, float radius, float seed) { glm::vec2 d = p - c; float a = std::atan2(d.y, d.x); float wobble = 1.0f + 0.16f * std::sin(a * 3.0f + seed) + 0.08f * std::sin(a * 5.0f + seed * 2.3f); return glm::length(d) / (radius * wobble); } float ridgeLow(float x) { return 13.0f + 2.2f * std::sin(x * 0.19f) + 1.3f * std::sin(x * 0.47f + 1.1f); } float ridgeHigh(float x) { return 6.5f + 1.6f * std::sin(x * 0.15f + 2.0f) + 1.0f * std::sin(x * 0.58f); } float segDist(glm::vec2 p, glm::vec2 a, glm::vec2 b) { glm::vec2 ab = b - a; float t = glm::clamp(glm::dot(p - a, ab) / std::max(glm::dot(ab, ab), 1e-6f), 0.0f, 1.0f); return glm::length(p - (a + ab * t)); } // Meadow landmarks (tile coordinates, +z = south). constexpr int kW = 96, kD = 88; const glm::vec2 kLake{24.0f, 46.0f}; constexpr float kLakeR = 10.5f; const glm::vec2 kPond{76.0f, 64.0f}; constexpr float kPondR = 4.4f; const glm::vec2 kPlateau{80.0f, 30.0f}; constexpr float kPlateauR = 13.0f; const glm::vec2 kPlazaMin{40.0f, 70.0f}, kPlazaMax{56.0f, 82.0f}; const glm::vec2 kGate{48.0f, 85.4f}; const glm::vec2 kTent{41.0f, 72.6f}; const ZonePatch kPatches[] = {{{58, 67}, 5.5f}, {{32, 64}, 5.0f}, {{41, 35}, 4.0f}, {{62, 46}, 4.5f}, {{83, 27}, 5.0f}, {{30, 10}, 4.0f}, {{13, 60}, 3.8f}, {{69, 75}, 4.0f}}; // 10 units: four 2-unit "dock" segments plus a "dock_end" (models from tools/blender_safari.py); // the east end rests on the sand so it is reachable on foot. const ZoneDock kDock{{27.6f, 45.2f}, {37.6f, 46.8f}, 0.05f}; bool inPlaza(glm::vec2 p, float margin) { return p.x > kPlazaMin.x - margin && p.x < kPlazaMax.x + margin && p.y > kPlazaMin.y - margin && p.y < kPlazaMax.y + margin; } } // namespace bool Zone::generate(const std::string& id, uint32_t seed) { if (id != "meadow") return false; id_ = id; seed_ = seed; props.clear(); walls.clear(); docks.clear(); tallGrassPatches.clear(); buildMeadow(seed); return true; } int Zone::level(int x, int z) const { x = std::clamp(x, 0, w_ - 1); z = std::clamp(z, 0, d_ - 1); return levels_[size_t(z) * w_ + x]; } uint8_t Zone::material(int x, int z) const { x = std::clamp(x, 0, w_ - 1); z = std::clamp(z, 0, d_ - 1); return mats_[size_t(z) * w_ + x]; } bool Zone::tallGrassTile(int x, int z) const { return inBounds(x, z) && grass_[size_t(z) * w_ + x]; } void Zone::setTile(int x, int z, int lvl, uint8_t mat) { if (!inBounds(x, z)) return; levels_[size_t(z) * w_ + x] = lvl; mats_[size_t(z) * w_ + x] = mat; } float Zone::terrainHeight(float x, float z) const { return float(level(int(std::floor(x)), int(std::floor(z)))) * kLevelHeight; } bool Zone::onDock(glm::vec2 p) const { for (const ZoneDock& d : docks) if (p.x >= d.min.x && p.x <= d.max.x && p.y >= d.min.y && p.y <= d.max.y) return true; return false; } float Zone::groundHeight(glm::vec2 p) const { for (const ZoneDock& d : docks) if (p.x >= d.min.x && p.x <= d.max.x && p.y >= d.min.y && p.y <= d.max.y) return d.deckY; return terrainHeight(p.x, p.y); } bool Zone::isWater(glm::vec2 p) const { int x = int(std::floor(p.x)), z = int(std::floor(p.y)); return inBounds(x, z) && level(x, z) < 0 && !onDock(p); } bool Zone::isDeepWater(glm::vec2 p) const { int x = int(std::floor(p.x)), z = int(std::floor(p.y)); return inBounds(x, z) && level(x, z) <= -2 && !onDock(p); } bool Zone::inTallGrass(glm::vec2 p) const { return tallGrassTile(int(std::floor(p.x)), int(std::floor(p.y))); } bool Zone::blocked(glm::vec2 p, float r) const { for (const ZoneProp& o : props) if (o.radius > 0.0f && glm::length(glm::vec2(o.pos.x, o.pos.z) - p) < o.radius + r) return true; for (const ZoneWall& w : walls) if (segDist(p, w.a, w.b) < w.halfWidth + r) return true; return false; } bool Zone::walkable(glm::vec2 from, glm::vec2 to, float radius) const { if (to.x < 1.0f || to.y < 1.0f || to.x > float(w_ - 1) || to.y > float(d_ - 1)) return false; bool dockTo = onDock(to), dockFrom = onDock(from); if (!dockTo) { int fl = level(int(std::floor(from.x)), int(std::floor(from.y))); int tl = level(int(std::floor(to.x)), int(std::floor(to.y))); if (tl < 0) return false; // water if (!dockFrom && (tl > fl + 1 || tl < fl - 2)) return false; // climb one level, drop at most two } // Only block motion that goes deeper into an obstacle, so nobody gets stuck. for (const ZoneProp& o : props) { if (o.radius <= 0.0f) continue; glm::vec2 c(o.pos.x, o.pos.z); float r = o.radius + radius; float dt = glm::length(c - to); if (dt < r && dt < glm::length(c - from)) return false; } for (const ZoneWall& w : walls) { float r = w.halfWidth + radius; float dt = segDist(to, w.a, w.b); if (dt < r && dt < segDist(from, w.a, w.b)) return false; } return true; } bool Zone::creatureWalkable(glm::vec2 p, bool water) const { if (p.x < 1.5f || p.y < 1.5f || p.x > float(w_) - 1.5f || p.y > float(d_) - 1.5f) return false; if (water) return isWater(p); if (isWater(p) || onDock(p)) return false; return !blocked(p, 0.3f); } bool Zone::fishingTarget(glm::vec2 from, glm::vec2 dir, glm::vec2& bobber) const { if (glm::length(dir) < 1e-3f || isWater(from)) return false; dir = glm::normalize(dir); for (float t = 1.6f; t <= 4.5f; t += 0.2f) { glm::vec2 p = from + dir * t; if (isWater(p)) { // land a bit beyond the shoreline so the bobber sits in open water glm::vec2 q = p + dir * 0.6f; bobber = isWater(q) ? q : p; return true; } } return false; } bool Zone::randomTallGrassNear(Rng& rng, glm::vec2 c, float rmin, float rmax, glm::vec2& out) const { for (int i = 0; i < 40; ++i) { float a = rng.range(0.0f, 2.0f * kPi), r = rng.range(rmin, rmax); glm::vec2 p = c + glm::vec2(std::cos(a), std::sin(a)) * r; if (inTallGrass(p) && creatureWalkable(p, false)) { out = p; return true; } } return false; } bool Zone::randomWater(Rng& rng, glm::vec2& out, bool deep) const { for (int i = 0; i < 60; ++i) { glm::vec2 p(rng.range(waterMin.x, waterMax.x), rng.range(waterMin.y, waterMax.y)); if (deep ? isDeepWater(p) : isWater(p)) { out = p; return true; } } return false; } bool Zone::randomWaterNear(Rng& rng, glm::vec2 c, float rmax, glm::vec2& out) const { for (int i = 0; i < 40; ++i) { float a = rng.range(0.0f, 2.0f * kPi), r = rng.range(0.5f, rmax); glm::vec2 p = c + glm::vec2(std::cos(a), std::sin(a)) * r; if (isWater(p)) { out = p; return true; } } return false; } bool Zone::randomWanderSpot(Rng& rng, glm::vec2& out) const { for (int i = 0; i < 80; ++i) { glm::vec2 p(rng.range(3.0f, float(w_) - 3.0f), rng.range(3.0f, float(d_) - 3.0f)); int x = int(std::floor(p.x)), z = int(std::floor(p.y)); uint8_t m = material(x, z); if (m != MatGrass && m != MatMeadow) continue; if (inPlaza(p, 7.0f) || !creatureWalkable(p, false)) continue; out = p; return true; } return false; } // ------------------------------------------------------------------ meadow void Zone::paintPath(const std::vector& pts, float halfWidth, bool clearGrass) { for (size_t i = 0; i + 1 < pts.size(); ++i) { glm::vec2 a = pts[i], b = pts[i + 1]; int steps = std::max(1, int(glm::length(b - a) * 4.0f)); for (int s = 0; s <= steps; ++s) { glm::vec2 q = glm::mix(a, b, float(s) / float(steps)); int r = int(std::ceil(halfWidth)) + 1; for (int dz = -r; dz <= r; ++dz) for (int dx = -r; dx <= r; ++dx) { int x = int(std::floor(q.x)) + dx, z = int(std::floor(q.y)) + dz; if (!inBounds(x, z) || level(x, z) < 0) continue; if (glm::length(glm::vec2(float(x) + 0.5f, float(z) + 0.5f) - q) > halfWidth) continue; if (material(x, z) != MatPlaza) mats_[size_t(z) * w_ + x] = MatPath; if (clearGrass) grass_[size_t(z) * w_ + x] = 0; } } } } bool Zone::clearForProp(glm::vec2 p, float r) const { int x = int(std::floor(p.x)), z = int(std::floor(p.y)); if (!inBounds(x, z) || level(x, z) < 0) return false; uint8_t m = material(x, z); if (m == MatPath || m == MatPlaza || m == MatStone || m == MatSand) return false; if (grass_[size_t(z) * w_ + x]) return false; if (onDock(p) || inPlaza(p, 2.5f)) return false; if (glm::length(p - kTent) < 3.5f || glm::length(p - kGate) < 3.0f) return false; if (p.x > kDock.min.x - 1.0f && p.x < kDock.max.x + 2.5f && p.y > kDock.min.y - 1.5f && p.y < kDock.max.y + 1.5f) return false; return !blocked(p, r); } void Zone::addProp(const std::string& model, glm::vec2 p, float yaw, float scale, float radius, float tint, float yOffset) { ZoneProp o; o.model = model; o.pos = glm::vec3(p.x, groundHeight(p) + yOffset, p.y); o.yaw = yaw; o.scale = glm::vec3(scale); o.tint = tint; o.radius = radius; props.push_back(o); } void Zone::addFence(glm::vec2 a, glm::vec2 b) { glm::vec2 d = b - a; float len = glm::length(d); int n = std::max(1, int(std::round(len / 2.0f))); glm::vec2 step = d / float(n); // fence model: 2 units along +X. yaw rotates +X toward the segment direction (mesh.vert convention). float yaw = std::atan2(-step.y, step.x); for (int i = 0; i < n; ++i) { glm::vec2 c = a + step * (float(i) + 0.5f); ZoneProp o; o.model = "fence"; o.pos = glm::vec3(c.x, groundHeight(c), c.y); o.yaw = yaw; o.scale = glm::vec3(glm::length(step) / 2.0f, 1.0f, 1.0f); props.push_back(o); } walls.push_back({a, b, 0.12f}); } void Zone::buildMeadow(uint32_t seed) { w_ = kW; d_ = kD; levels_.assign(size_t(w_) * d_, 0); mats_.assign(size_t(w_) * d_, MatGrass); grass_.assign(size_t(w_) * d_, 0); Rng rng(seed ^ 0x5AFA21u); // --- terrain: north ridge, east plateau, lake, pond, plaza for (int z = 0; z < d_; ++z) for (int x = 0; x < w_; ++x) { glm::vec2 p(float(x) + 0.5f, float(z) + 0.5f); int lvl = 0; uint8_t mat = MatGrass; if (p.y < ridgeLow(p.x)) lvl = 2, mat = MatRidge; if (p.y < ridgeHigh(p.x)) lvl = 4, mat = MatRidge; if (lvl == 0 && blob(p, kPlateau, kPlateauR, 1.9f) < 1.0f) lvl = 2, mat = MatRidge; if (blob(p, {15.0f, 74.0f}, 4.2f, 0.3f) < 1.0f) lvl = 1; // small hill by the west fence float lake = blob(p, kLake, kLakeR, 0.7f); if (lake < 1.16f) lvl = 0, mat = MatSand; if (lake < 1.0f) lvl = -1; if (lake < 0.68f) lvl = -3; float pond = blob(p, kPond, kPondR, 2.4f); if (pond < 1.22f) lvl = 0, mat = MatSand; if (pond < 1.0f) lvl = -1; if (pond < 0.6f) lvl = -2; if (inPlaza(p, 0.0f)) { float cx = std::max({kPlazaMin.x - p.x, p.x - kPlazaMax.x, 0.0f}); float cz = std::max({kPlazaMin.y - p.y, p.y - kPlazaMax.y, 0.0f}); (void)cx, (void)cz; // round the corners glm::vec2 c = glm::clamp(p, kPlazaMin + 2.5f, kPlazaMax - 2.5f); if (glm::length(p - c) < 2.5f) lvl = 0, mat = MatPlaza; } setTile(x, z, lvl, mat); } // --- tall grass patches (Meadow material under them) for (const ZonePatch& tg : kPatches) { tallGrassPatches.push_back(tg); for (int z = 0; z < d_; ++z) for (int x = 0; x < w_; ++x) { glm::vec2 p(float(x) + 0.5f, float(z) + 0.5f); if (blob(p, tg.c, tg.r, tg.c.x * 0.37f) >= 1.0f) continue; if (level(x, z) < 0 || material(x, z) == MatSand || material(x, z) == MatPlaza) continue; mats_[size_t(z) * w_ + x] = MatMeadow; grass_[size_t(z) * w_ + x] = 1; } } // --- paths from the gate through the plaza to every area const glm::vec2 cross{48.0f, 60.0f}; paintPath({{48, 87}, {48, 76}, cross}, 1.35f); paintPath({cross, {41, 54}, {37, 48.5f}, {36, 46}}, 1.25f); paintPath({cross, {50, 46}, {52, 30}, {52, 2}}, 1.3f); paintPath({cross, {60, 56}, {67, 49}, {72, 41}, {78, 33}, {84, 30}}, 1.25f); paintPath({{36, 46}, {34, 52}, {33, 58}, {38, 66}, {44, 70}}, 1.0f); // lake loop back to the plaza // Stairs: a path tile next to a path tile two levels higher becomes a one-level step. for (int pass = 0; pass < 3; ++pass) { std::vector> raise; for (int z = 0; z < d_; ++z) for (int x = 0; x < w_; ++x) { if (material(x, z) != MatPath && material(x, z) != MatStone) continue; int l = level(x, z); const int nx[4] = {1, -1, 0, 0}, nz[4] = {0, 0, 1, -1}; for (int k = 0; k < 4; ++k) { int xx = x + nx[k], zz = z + nz[k]; if (!inBounds(xx, zz)) continue; uint8_t m = material(xx, zz); if ((m == MatPath || m == MatStone) && level(xx, zz) >= l + 2) { raise.push_back({x, z}); break; } } } for (auto [x, z] : raise) setTile(x, z, level(x, z) + 1, MatStone); } // --- landmarks docks.push_back(kDock); plaza = (kPlazaMin + kPlazaMax) * 0.5f; henry = {44.6f, 73.4f}; spawn = {48.2f, 76.6f}; henryYaw = yawFromDir(spawn - henry); spawnYaw = yawFromDir(henry - spawn); // dock segments (2 units along +X each) from the shore out over the lake for (float x = kDock.max.x - 1.0f; x > kDock.min.x + 2.5f; x -= 2.0f) { ZoneProp o; o.model = "dock"; o.pos = glm::vec3(x, kDock.deckY, (kDock.min.y + kDock.max.y) * 0.5f); props.push_back(o); } { ZoneProp o; o.model = "dock_end"; o.pos = glm::vec3(kDock.min.x + 1.0f, kDock.deckY, (kDock.min.y + kDock.max.y) * 0.5f); o.yaw = 3.14159265f; // the end board (+X in the model) faces out over the lake props.push_back(o); } addProp("gate", kGate, 0.0f, 1.0f, 0.0f); props.back().radius = 0.0f; // gate pillars collide (the arch itself is overhead) walls.push_back({kGate + glm::vec2(-1.95f, 0.0f), kGate + glm::vec2(-1.45f, 0.0f), 0.3f}); walls.push_back({kGate + glm::vec2(1.45f, 0.0f), kGate + glm::vec2(1.95f, 0.0f), 0.3f}); { // door (+Z in the model) faces the plaza; mesh yaw maps local +Z to (sin yaw, cos yaw) glm::vec2 d = plaza - kTent; addProp("tent", kTent, std::atan2(d.x, d.y), 1.0f, 1.55f); } for (glm::vec2 p : {glm::vec2{42.6f, 80.6f}, {53.4f, 80.6f}, {53.4f, 71.4f}, {46.1f, 64.0f}, {50.0f, 50.0f}, {37.8f, 49.6f}, {66.2f, 51.6f}, {53.6f, 36.0f}, {75.6f, 37.6f}}) addProp("lamp", p, 0.0f, 1.0f, 0.22f); addProp("signpost", {50.9f, 62.2f}, -0.4f, 1.0f, 0.2f); addProp("signpost", {39.4f, 52.2f}, 0.8f, 1.0f, 0.2f); addProp("signpost", {70.6f, 46.6f}, -0.9f, 1.0f, 0.2f); addProp("crate", {38.9f, 74.9f}, 0.3f, 1.0f, 0.45f); addProp("crate", {38.6f, 76.1f}, 1.1f, 0.85f, 0.4f, 0.9f); addProp("barrel", {39.9f, 76.3f}, 0.0f, 1.0f, 0.35f); addProp("hay_bale", {55.0f, 75.6f}, 0.2f, 1.0f, 0.6f); addProp("hay_bale", {55.6f, 77.0f}, 1.4f, 1.0f, 0.6f, 0.92f); addProp("log_bench", {52.6f, 73.2f}, 0.0f, 1.0f, 0.5f); addProp("log_bench", {36.4f, 40.6f}, 1.57f, 1.0f, 0.5f); addProp("crate", {38.6f, 47.6f}, 0.6f, 0.8f, 0.4f); // fences along the southern border, either side of the gate addFence({33.0f, 85.4f}, {45.9f, 85.4f}); addFence({50.1f, 85.4f}, {63.0f, 85.4f}); // paddock fence west of the main path addFence({44.6f, 68.6f}, {44.6f, 62.6f}); // --- trees: dense border forest, groves on the ridge, sparse elsewhere const char* oaks[] = {"oak_a", "oak_b"}; const char* pines[] = {"pine_a", "pine_b"}; for (int z = 0; z < d_; ++z) for (int x = 0; x < w_; ++x) { int edge = std::min({x, z, w_ - 1 - x, d_ - 1 - z}); float chance = edge < 5 ? 0.5f - float(edge) * 0.07f : (level(x, z) >= 4 ? 0.16f : 0.014f); if (level(x, z) == 2 && material(x, z) == MatRidge) chance = std::max(chance, 0.03f); if (!rng.chance(chance)) continue; glm::vec2 p(float(x) + rng.range(0.2f, 0.8f), float(z) + rng.range(0.2f, 0.8f)); if (std::abs(p.x - kGate.x) < 4.0f && p.y > 80.0f) continue; float scale = rng.range(0.8f, 1.12f); if (!clearForProp(p, 1.0f * scale)) continue; if (blob(p, kLake, kLakeR, 0.7f) < 1.4f || blob(p, kPond, kPondR, 2.4f) < 1.5f) continue; bool pine = rng.chance(level(x, z) >= 2 || edge < 5 ? 0.55f : 0.3f); const char* model = pine ? pines[rng.range(0, 1)] : oaks[rng.range(0, 1)]; ZoneProp o; o.model = model; o.pos = glm::vec3(p.x, terrainHeight(p.x, p.y), p.y); o.yaw = rng.range(0.0f, 6.2832f); o.scale = glm::vec3(scale, scale * rng.range(0.92f, 1.08f), scale); o.tint = rng.range(0.86f, 1.08f); o.radius = 0.45f * scale; props.push_back(o); } // --- rocks (gathering around water and the plateau edge) and bushes for (int z = 1; z < d_ - 1; ++z) for (int x = 1; x < w_ - 1; ++x) { glm::vec2 p(float(x) + 0.5f + rng.range(-0.2f, 0.2f), float(z) + 0.5f + rng.range(-0.2f, 0.2f)); float lake = blob(p, kLake, kLakeR, 0.7f), pond = blob(p, kPond, kPondR, 2.4f); bool shore = (lake > 1.16f && lake < 1.45f) || (pond > 1.22f && pond < 1.55f); bool cliff = level(x, z) >= 2 && (level(x + 1, z) < level(x, z) || level(x, z + 1) < level(x, z)); float r = rng.uniform(); float s = rng.range(0.8f, 1.3f); if (r < (shore ? 0.07f : cliff ? 0.06f : 0.006f)) { if (!clearForProp(p, 0.6f * s)) continue; ZoneProp o; o.model = rng.chance(0.5f) ? "rock_a" : "rock_b"; o.pos = glm::vec3(p.x, terrainHeight(p.x, p.y) - 0.02f, p.y); o.yaw = rng.range(0.0f, 6.2832f); o.scale = glm::vec3(s, s * rng.range(0.8f, 1.2f), s); o.tint = rng.range(0.86f, 1.08f); o.radius = 0.42f * s; props.push_back(o); } else if (r < (shore ? 0.12f : 0.022f)) { float bs = rng.range(0.8f, 1.25f); if (!clearForProp(p, 0.65f * bs)) continue; ZoneProp o; o.model = rng.chance(0.5f) ? "bush_a" : "bush_b"; o.pos = glm::vec3(p.x, terrainHeight(p.x, p.y) - 0.03f, p.y); o.yaw = rng.range(0.0f, 6.2832f); o.scale = glm::vec3(bs); o.tint = rng.range(0.86f, 1.08f); o.radius = 0.5f * bs; props.push_back(o); } } // --- water bounds for the water plane and spawns waterMin = glm::vec2(float(w_), float(d_)); waterMax = glm::vec2(0.0f); for (int z = 0; z < d_; ++z) for (int x = 0; x < w_; ++x) if (level(x, z) < 0) { waterMin = glm::min(waterMin, glm::vec2(float(x), float(z))); waterMax = glm::max(waterMax, glm::vec2(float(x + 1), float(z + 1))); } waterMin -= glm::vec2(1.0f); waterMax += glm::vec2(1.0f); } } // namespace bs