Files
Gabe 238cd9c6c3 Brandling Safari: LAN safari-zone creature catching on the HD-2D engine
- Authoritative 30 Hz server (ENet, SQLite): accounts, characters, wild AI, tall grass /
  water / wandering spawns, fishing, captures, follower chains, clock + weather, LAN beacon
- Client: title / join / character creator / play screens, prediction + reconciliation,
  interpolated players and parties, HD-2D UI (dialogue with Professor Henry, HUD, collection)
- Shared deterministic zone generator, movement, protocol, roster, appearance
- Paper-doll character art pipeline (PixelLab base walk + per-item animation edits)
- Brandling roster from the company list (logo colors, jev types), Blender safari props
- Headless test bot
2026-10-02 15:20:27 -05:00

122 lines
4.5 KiB
C++

#pragma once
// Deterministic safari zone layout. The server and every client call
// Zone::generate(id, seed) and get the exact same terrain, props, water,
// tall grass and landmarks, so only the zone id + seed travel over the wire.
// Everything that matters for gameplay (collision, heights, water, tall
// grass, NPC and spawn points) lives here; purely visual decoration (grass
// tufts, flowers, the tall-grass sprites themselves) is added by the client.
#include "Rng.h"
#include <glm/glm.hpp>
#include <cstdint>
#include <cmath>
#include <string>
#include <vector>
namespace bs {
// Terrain materials (client maps them to texture layers).
enum ZoneMat : uint8_t { MatGrass = 0, MatMeadow = 1, MatPath = 2, MatSand = 3, MatStone = 4, MatRidge = 5, MatPlaza = 6 };
// One placed 3D model. `radius` > 0 makes it a round obstacle.
struct ZoneProp {
std::string model; // model name in scenery.glb / safari.glb
glm::vec3 pos{0.0f};
float yaw = 0.0f;
glm::vec3 scale{1.0f};
float tint = 1.0f;
float radius = 0.0f;
};
// Thin straight obstacle (fences, tent walls) as a capsule from a to b.
struct ZoneWall {
glm::vec2 a, b;
float halfWidth = 0.15f;
};
// Walkable wooden deck over water.
struct ZoneDock {
glm::vec2 min, max;
float deckY = 0.05f;
};
struct ZonePatch {
glm::vec2 c;
float r;
};
class Zone {
public:
static constexpr float kWaterLevel = -0.2f;
static constexpr float kLevelHeight = 0.5f;
// Known ids: "meadow" (the grassy safari zone). Returns false for unknown ids.
bool generate(const std::string& id, uint32_t seed);
const std::string& id() const { return id_; }
uint32_t seed() const { return seed_; }
int width() const { return w_; }
int depth() const { return d_; }
bool inBounds(int x, int z) const { return x >= 0 && z >= 0 && x < w_ && z < d_; }
int level(int x, int z) const;
uint8_t material(int x, int z) const;
bool tallGrassTile(int x, int z) const;
// Same rule as hd2d::Terrain::heightAt (flat tile tops).
float terrainHeight(float x, float z) const;
float groundHeight(glm::vec2 p) const; // dock deck or terrain
bool onDock(glm::vec2 p) const;
bool isWater(glm::vec2 p) const; // open water (not under a dock)
bool isDeepWater(glm::vec2 p) const; // basin, at least 1 level below the shelf
bool inTallGrass(glm::vec2 p) const;
bool walkable(glm::vec2 from, glm::vec2 to, float radius = 0.28f) const;
// Where a line cast from `from` along `dir` lands: first open water point
// 1.6 .. 4.5 units ahead. False when there is no water in range.
bool fishingTarget(glm::vec2 from, glm::vec2 dir, glm::vec2& bobber) const;
bool randomTallGrassNear(Rng& rng, glm::vec2 c, float rmin, float rmax, glm::vec2& out) const;
bool randomWater(Rng& rng, glm::vec2& out, bool deep = true) const;
bool randomWaterNear(Rng& rng, glm::vec2 c, float rmax, glm::vec2& out) const;
// Open grass / meadow for wandering creatures (away from the entrance plaza).
bool randomWanderSpot(Rng& rng, glm::vec2& out) const;
// True when a creature may stand / walk here (land creatures stay out of water).
bool creatureWalkable(glm::vec2 p, bool water) const;
std::vector<ZoneProp> props;
std::vector<ZoneWall> walls;
std::vector<ZoneDock> docks;
std::vector<ZonePatch> tallGrassPatches;
glm::vec2 spawn{0.0f}; // where new players appear
float spawnYaw = 0.0f;
glm::vec2 henry{0.0f}; // Professor Henry's post
float henryYaw = 0.0f;
glm::vec2 plaza{0.0f}; // entrance plaza center (no wild spawns nearby)
glm::vec2 waterMin{0.0f}, waterMax{0.0f}; // bounds of all water tiles (+ margin)
private:
void buildMeadow(uint32_t seed);
void setTile(int x, int z, int level, uint8_t mat);
void paintPath(const std::vector<glm::vec2>& pts, float halfWidth, bool clearGrass = true);
bool blocked(glm::vec2 p, float r) const; // overlaps a prop or wall
bool clearForProp(glm::vec2 p, float r) const;
void addProp(const std::string& model, glm::vec2 p, float yaw, float scale, float radius, float tint = 1.0f,
float yOffset = 0.0f);
void addFence(glm::vec2 a, glm::vec2 b);
std::string id_;
uint32_t seed_ = 0;
int w_ = 0, d_ = 0;
std::vector<int> levels_;
std::vector<uint8_t> mats_;
std::vector<uint8_t> grass_;
};
// 0 = north (-z), pi/2 = east (+x).
inline float yawFromDir(glm::vec2 d) { return std::atan2(d.x, -d.y); }
inline glm::vec2 dirFromYaw(float yaw) { return {std::sin(yaw), -std::cos(yaw)}; }
} // namespace bs