- 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
97 lines
3.5 KiB
C++
97 lines
3.5 KiB
C++
#include "Roster.h"
|
|
|
|
#include "Appearance.h"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <algorithm>
|
|
#include <fstream>
|
|
#include <map>
|
|
|
|
namespace bs {
|
|
|
|
namespace {
|
|
std::string str(const nlohmann::json& j, const char* key, const std::string& def = {}) {
|
|
auto it = j.find(key);
|
|
return it != j.end() && it->is_string() ? it->get<std::string>() : def;
|
|
}
|
|
float num(const nlohmann::json& j, const char* key, float def) {
|
|
auto it = j.find(key);
|
|
return it != j.end() && it->is_number() ? it->get<float>() : def;
|
|
}
|
|
} // namespace
|
|
|
|
bool Roster::load(const std::string& path, std::string* error) {
|
|
std::ifstream f(path);
|
|
if (!f) {
|
|
if (error) *error = "cannot open " + path;
|
|
return false;
|
|
}
|
|
try {
|
|
nlohmann::json j = nlohmann::json::parse(f);
|
|
species_.clear();
|
|
for (const auto& s : j.at("species")) {
|
|
Species sp;
|
|
sp.id = s.at("id").get<uint16_t>();
|
|
sp.slug = str(s, "slug");
|
|
sp.name = str(s, "name", sp.slug);
|
|
sp.domain = str(s, "domain");
|
|
sp.type = str(s, "type", "normal");
|
|
sp.type2 = str(s, "type2");
|
|
sp.body = str(s, "body");
|
|
std::string r = str(s, "rarity", "common");
|
|
sp.rarity = r == "legendary" ? Rarity::Legendary : r == "rare" ? Rarity::Rare
|
|
: r == "uncommon" ? Rarity::Uncommon : Rarity::Common;
|
|
std::string h = str(s, "habitat", "grass");
|
|
sp.habitat = h == "water" ? Habitat::Water : h == "both" ? Habitat::Both : Habitat::Grass;
|
|
sp.catchRate = num(s, "catchRate", 0.5f);
|
|
sp.fleeRate = num(s, "fleeRate", 0.1f);
|
|
if (s.contains("colors") && s["colors"].is_object()) {
|
|
sp.mainColor = parseHexColor(str(s["colors"], "main", "#888888"));
|
|
sp.accentColor = parseHexColor(str(s["colors"], "accent", "#cccccc"));
|
|
}
|
|
sp.sprite = str(s, "sprite");
|
|
sp.anchorRow = int(num(s, "anchorRow", 63.0f));
|
|
sp.retired = s.contains("retired") && s["retired"].is_boolean() && s["retired"].get<bool>();
|
|
species_.push_back(sp);
|
|
}
|
|
// ids index the vector directly
|
|
std::sort(species_.begin(), species_.end(), [](const Species& a, const Species& b) { return a.id < b.id; });
|
|
for (size_t i = 0; i < species_.size(); ++i)
|
|
if (species_[i].id != i) {
|
|
if (error) *error = path + ": species ids must be 0..N-1";
|
|
return false;
|
|
}
|
|
} catch (const std::exception& e) {
|
|
if (error) *error = path + ": " + e.what();
|
|
return false;
|
|
}
|
|
if (species_.empty()) {
|
|
if (error) *error = path + ": no species";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const char* rarityName(Rarity r) {
|
|
switch (r) {
|
|
case Rarity::Uncommon: return "Uncommon";
|
|
case Rarity::Rare: return "Rare";
|
|
case Rarity::Legendary: return "Legendary";
|
|
default: return "Common";
|
|
}
|
|
}
|
|
|
|
uint32_t typeColor(const std::string& type) {
|
|
static const std::map<std::string, uint32_t> colors = {
|
|
{"normal", 0xa8a878}, {"fire", 0xf08030}, {"water", 0x6890f0}, {"grass", 0x78c850},
|
|
{"electric", 0xf8d030}, {"ice", 0x98d8d8}, {"fighting", 0xc03028}, {"poison", 0xa040a0},
|
|
{"ground", 0xe0c068}, {"flying", 0xa890f0}, {"psychic", 0xf85888}, {"bug", 0xa8b820},
|
|
{"rock", 0xb8a038}, {"ghost", 0x705898}, {"dragon", 0x7038f8},
|
|
};
|
|
auto it = colors.find(type);
|
|
return it == colors.end() ? 0xa8a878 : it->second;
|
|
}
|
|
|
|
} // namespace bs
|