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
This commit is contained in:
Gabe
2026-10-02 15:20:27 -05:00
commit 238cd9c6c3
3409 changed files with 100493 additions and 0 deletions

219
server/Game.h Normal file
View File

@@ -0,0 +1,219 @@
#pragma once
// Authoritative game simulation: sessions/accounts, player movement, wild
// creatures, fishing, catching, followers, Professor Henry, chat, world clock.
#include "Appearance.h"
#include "Database.h"
#include "Movement.h"
#include "Net.h"
#include "Protocol.h"
#include "Rng.h"
#include "Roster.h"
#include "Spawns.h"
#include "Zone.h"
#include <cstdint>
#include <deque>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <vector>
namespace bs {
struct GameConfig {
std::string serverName = "Brandling Safari";
uint32_t seed = 1;
std::string zoneId = "meadow";
float dayMinutes = 30.0f;
float hour = -1.0f; // < 0: resume the saved clock (default 9:00)
int weather = -1; // < 0: resume the saved weather
};
// Professor Henry always has this entity id; dynamic entities start above it.
constexpr uint32_t kHenryEntityId = 1;
enum class LineState { None, Waiting, Biting };
struct Player {
uint32_t peer = 0;
uint32_t entity = 0; // 0 until the character is spawned
bool helloDone = false;
bool hasCharacter = false;
bool spawned = false;
PlayerRow row; // persistent fields (id, token, name, look, capsules, intro)
MoveState move;
uint32_t lastSeq = 0;
float inputBudget = 2.0f;
int idleTicks = 0; // ticks without an applied input (stops the walk anim)
std::vector<OwnedCreature> creatures;
std::set<uint16_t> seen, caught;
float throwTimer = 0.0f;
LineState line = LineState::None;
glm::vec2 bobber{0.0f}, castPos{0.0f};
float lineTimer = 0.0f, fishCooldown = 0.0f;
float grassDist = 0.0f;
float rustleCooldown = 0.0f; // misses shake the grass at most about once a second
std::deque<glm::vec2> trail; // newest first
float introTimer = -1.0f; // > 0: henry_intro dialogue pending
bool introOffered = false; // henry_intro was sent and not answered yet
double lastChat = -10.0;
bool posDirty = false;
};
enum class CMode { Wander, Emerge, Alert, Capturing, Flee, Hooked, Swim, Follow };
enum class COrigin { Wanderer, Grass, Swimmer, Fished, Console, Caught };
struct Creature {
uint32_t id = 0;
uint16_t species = 0;
EntityKind kind = EntityKind::Wild;
COrigin origin = COrigin::Wanderer;
CMode mode = CMode::Wander;
Anim anim = Anim::Idle;
glm::vec2 pos{0.0f};
float y = 0.0f;
float yaw = 0.0f;
uint32_t owner = 0; // follower owner / player hooking, capturing or scaring it
uint8_t order = 0;
uint32_t uid = 0; // follower: owned creature uid
bool dead = false; // removed at the end of the tick
uint32_t emergedFor = 0; // grass spawn: player entity it popped up for (per-player cap)
float timer = 0.0f; // time left in the current mode / pause
bool walking = false;
glm::vec2 target{0.0f};
float lifetime = 120.0f; // despawn once this runs out and nobody is within 12 units
bool hookBonus = false; // Alert after being fished out
int failedThrows = 0;
std::set<uint32_t> seenBy; // player entities that already had it registered as seen
// capture in flight
float captureTimer = 0.0f;
bool captureSuccess = false;
int wobbles = 0;
CMode resumeMode = CMode::Wander;
// hook arc
glm::vec2 arcFrom{0.0f}, arcTo{0.0f};
float arcFromY = 0.0f;
};
class Game {
public:
Game(net::Host& host, Database& db, Zone& zone, const Roster& roster, const CharacterManifest& manifest,
GameConfig cfg);
void onConnect(uint32_t peer);
void onDisconnect(uint32_t peer);
void onReceive(uint32_t peer, const std::vector<uint8_t>& data);
void tick();
void saveAll();
// Console command; returns false for "quit".
bool console(const std::string& line);
int playerCount() const;
const std::string& serverName() const { return cfg_.serverName; }
private:
// --- sessions
Player* byPeer(uint32_t peer);
Player* byEntity(uint32_t entity);
void handleHello(Player& p, Reader& r);
void handleCreate(Player& p, Reader& r);
void handleInput(Player& p, Reader& r);
void handleAction(Player& p, Reader& r);
void handleChat(Player& p, Reader& r);
void handleDialogueDone(Player& p, Reader& r);
void spawnPlayer(Player& p);
void despawnPlayer(Player& p); // save + PlayerLeft + remove followers
void reject(uint32_t peer, const std::string& reason);
// --- messaging
void send(const Player& p, const Writer& w, bool reliable = true);
void broadcast(const Writer& w, bool reliable = true);
void sendProfile(Player& p);
void sendEvent(const GameEvent& e);
void sendToast(Player& p, const std::string& text);
void sendDialogue(Player& p, const std::string& id);
PlayerInfoMsg playerInfo(const Player& p) const;
void sendSnapshots();
// --- persistence
void savePlayer(Player& p);
void saveWorld();
// --- players
void applyInput(Player& p, const InputCmd& c);
void updatePlayer(Player& p, float dt);
void cancelLine(Player& p);
void noteSeen(Player& p, uint16_t species);
glm::vec3 pos3(glm::vec2 p) const { return {p.x, zone_.groundHeight(p), p.y}; }
Anim playerAnim(const Player& p) const;
// --- creatures
Creature& addCreature(uint16_t species, glm::vec2 pos, COrigin origin);
void removeCreature(uint32_t id);
void announceAppeared(const Creature& c);
void updateCreatures(float dt);
void updateWild(Creature& c, float dt);
void updateFollowers(Player& p, float dt);
void wander(Creature& c, float dt, float speed);
bool pickWanderTarget(Creature& c);
void faceNearestPlayer(Creature& c);
float nearestPlayerDist(glm::vec2 p, Player** who = nullptr);
int emergedCount(const Player& p) const;
void maintainPopulation(float dt);
bool spawnWanderer();
bool spawnSwimmer(bool announce);
void grassEncounters(Player& p);
void resolveCapture(Creature& c);
void syncFollowers(Player& p);
Creature* spawnForConsole(uint16_t species);
// --- actions
void actionThrow(Player& p, uint32_t target);
void actionCast(Player& p, glm::vec2 dir);
void actionReel(Player& p);
void actionInteract(Player& p, uint32_t target);
void actionToggleParty(Player& p, uint32_t uid);
void finishIntro(Player& p);
bool landingSpot(glm::vec2 bobber, glm::vec2 player, glm::vec2& out) const;
// --- world
void updateWorld(float dt);
void updateHenry(float dt);
void setWeather(Weather w);
net::Host& host_;
Database& db_;
Zone& zone_;
const Roster& roster_;
const CharacterManifest& manifest_;
GameConfig cfg_;
Rng rng_;
SpawnTables tables_;
std::map<uint32_t, std::unique_ptr<Player>> players_; // by peer
std::map<uint32_t, Creature> creatures_; // wild + followers by entity id
uint32_t nextEntity_ = kHenryEntityId + 1;
uint32_t tick_ = 0;
double time_ = 0.0; // seconds since start
float hour_ = 9.0f;
Weather weather_ = Weather::Clear;
float weatherTimer_ = 300.0f;
float weatherBlend_ = 1.0f;
float saveTimer_ = 10.0f;
float henryYaw_ = 0.0f;
float wanderSpawnTimer_ = 0.0f;
float swimmerTimer_ = 10.0f; // next surface/dive
float swimmerRespawn_ = -1.0f; // > 0: a dived swimmer resurfaces elsewhere
};
} // namespace bs