Files
brandling-safari/shared/Appearance.cpp
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

140 lines
4.7 KiB
C++

#include "Appearance.h"
#include <nlohmann/json.hpp>
#include <cstdio>
#include <fstream>
namespace bs {
namespace {
const char* kSlotNames[kSlotCount] = {"shoes", "bottom", "top", "hair", "hat"};
}
const char* slotName(int slot) { return slot >= 0 && slot < kSlotCount ? kSlotNames[slot] : "?"; }
int slotFromName(const std::string& s) {
for (int i = 0; i < kSlotCount; ++i)
if (s == kSlotNames[i]) return i;
return -1;
}
uint32_t parseHexColor(const std::string& s, uint32_t fallback) {
std::string h = s;
if (!h.empty() && h[0] == '#') h.erase(0, 1);
if (h.size() != 6) return fallback;
char* end = nullptr;
unsigned long v = std::strtoul(h.c_str(), &end, 16);
return end && *end == '\0' ? uint32_t(v) : fallback;
}
std::string hexColor(uint32_t rgb) {
char buf[8];
std::snprintf(buf, sizeof buf, "#%06x", rgb & 0xffffffu);
return buf;
}
bool CharacterManifest::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);
cell = j.value("cell", 64);
framesPerDirection = j.value("framesPerDirection", 5);
walkFrames = j.value("walkFrames", 4);
shades = j.value("shades", 4);
directions = j.value("directions", std::vector<std::string>{});
for (auto& [k, v] : j.at("slots").items()) {
int s = slotFromName(k);
if (s >= 0) slotZ[s] = v.get<int>();
}
for (auto& c : j.at("skinTones")) skinTones.push_back(parseHexColor(c.get<std::string>()));
for (auto& c : j.value("palette", nlohmann::json::array())) palette.push_back(parseHexColor(c.get<std::string>()));
for (auto& [id, v] : j.at("items").items()) {
ItemDef d;
d.id = id;
d.label = v.value("label", id);
d.slot = slotFromName(v.at("slot").get<std::string>());
d.color = parseHexColor(v.value("color", "#ffffff"));
d.fromBody = v.value("fromBody", false);
if (d.slot >= 0) items[id] = d;
}
for (auto& [id, v] : j.at("bodies").items()) {
BodyDef b;
b.id = id;
b.label = v.value("label", id);
b.anchorRow = v.value("anchorRow", cell - 1);
b.items = v.value("items", std::vector<std::string>{});
bodies[id] = b;
}
if (j.contains("npcs"))
for (auto& [id, v] : j["npcs"].items())
npcs[id] = {v.value("sheet", ""), v.value("portrait", ""), v.value("anchorRow", cell - 1)};
} catch (const std::exception& e) {
if (error) *error = path + ": " + e.what();
return false;
}
if (bodies.empty() || skinTones.empty()) {
if (error) *error = path + ": no bodies or skin tones";
return false;
}
return true;
}
const ItemDef* CharacterManifest::item(const std::string& id) const {
auto it = items.find(id);
return it == items.end() ? nullptr : &it->second;
}
bool CharacterManifest::bodyHas(const std::string& body, const std::string& id) const {
auto it = bodies.find(body);
if (it == bodies.end()) return false;
for (const std::string& s : it->second.items)
if (s == id) return true;
return false;
}
Appearance CharacterManifest::defaultLook(const std::string& body) const {
Appearance a;
a.body = bodies.count(body) ? body : bodies.begin()->first;
a.skin = 1;
auto pick = [&](int slot, const char* preferred) {
if (bodyHas(a.body, preferred)) a.items[slot] = preferred;
else
for (const std::string& id : bodies.at(a.body).items)
if (item(id)->slot == slot) { a.items[slot] = id; break; }
};
pick(Top, "top_tee");
pick(Bottom, "bottom_shorts");
pick(Hair, "hair_short");
pick(Shoes, "shoes_sneakers");
for (int s = 0; s < kSlotCount; ++s)
a.colors[s] = a.items[s].empty() ? 0xffffff : item(a.items[s])->color;
return a;
}
bool CharacterManifest::sanitize(Appearance& a) const {
bool ok = true;
if (!bodies.count(a.body)) {
a = defaultLook();
return false;
}
if (a.skin >= skinTones.size()) a.skin = 0, ok = false;
for (int s = 0; s < kSlotCount; ++s) {
const std::string& id = a.items[s];
if (id.empty()) continue;
const ItemDef* d = item(id);
if (!d || d->slot != s || !bodyHas(a.body, id)) a.items[s].clear(), ok = false;
a.colors[s] &= 0xffffffu;
}
Appearance def = defaultLook(a.body);
for (int s : {int(Top), int(Bottom)}) // always dressed
if (a.items[s].empty()) a.items[s] = def.items[s], a.colors[s] = def.colors[s], ok = false;
return ok;
}
} // namespace bs