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:
134
client/CharacterSprite.cpp
Normal file
134
client/CharacterSprite.cpp
Normal file
@@ -0,0 +1,134 @@
|
||||
#include "CharacterSprite.h"
|
||||
|
||||
#include "Common.h"
|
||||
|
||||
#include "hd2d/Log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
namespace bs {
|
||||
|
||||
std::array<uint32_t, 4> colorRamp(uint32_t rgb) {
|
||||
const float c[3] = {float((rgb >> 16) & 255), float((rgb >> 8) & 255), float(rgb & 255)};
|
||||
struct Step { float k; float tint[3]; };
|
||||
const Step table[3] = {{0.50f, {0.96f, 0.90f, 1.06f}}, {0.73f, {0.99f, 0.95f, 1.03f}}, {1.0f, {1.0f, 1.0f, 1.0f}}};
|
||||
std::array<uint32_t, 4> out{};
|
||||
auto pack = [](float r, float g, float b) {
|
||||
auto q = [](float v) { return uint32_t(std::clamp(int(std::lround(v)), 0, 255)); };
|
||||
return (q(r) << 16) | (q(g) << 8) | q(b);
|
||||
};
|
||||
for (int i = 0; i < 3; ++i)
|
||||
out[i] = pack(c[0] * table[i].k * table[i].tint[0], c[1] * table[i].k * table[i].tint[1],
|
||||
c[2] * table[i].k * table[i].tint[2]);
|
||||
const float warm[3] = {255.0f, 250.0f, 232.0f};
|
||||
out[3] = pack(c[0] + (warm[0] - c[0]) * 0.34f, c[1] + (warm[1] - c[1]) * 0.34f, c[2] + (warm[2] - c[2]) * 0.34f);
|
||||
return out;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
bool loadOptional(const std::string& path, hd2d::Image& out) {
|
||||
if (!std::filesystem::exists(path)) return false;
|
||||
return hd2d::loadImage(path, out);
|
||||
}
|
||||
|
||||
void paintShade(hd2d::Image& canvas, const hd2d::Image& shade, uint32_t color) {
|
||||
if (shade.rgba.size() != canvas.rgba.size()) return;
|
||||
auto ramp = colorRamp(color);
|
||||
const size_t n = canvas.rgba.size() / 4;
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
const unsigned char* s = &shade.rgba[i * 4];
|
||||
if (s[3] < 128) continue;
|
||||
int lv = std::clamp((int(s[0]) + 30) / 60, 1, 4);
|
||||
uint32_t c = ramp[size_t(lv - 1)];
|
||||
unsigned char* d = &canvas.rgba[i * 4];
|
||||
d[0] = (c >> 16) & 255, d[1] = (c >> 8) & 255, d[2] = c & 255, d[3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
void paintFixed(hd2d::Image& canvas, const hd2d::Image& fixed) {
|
||||
if (fixed.rgba.size() != canvas.rgba.size()) return;
|
||||
const size_t n = canvas.rgba.size() / 4;
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
if (fixed.rgba[i * 4 + 3] >= 128) {
|
||||
std::copy_n(&fixed.rgba[i * 4], 3, &canvas.rgba[i * 4]);
|
||||
canvas.rgba[i * 4 + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool CharacterArt::load(const std::string& charDir, const CharacterManifest& manifest) {
|
||||
manifest_ = &manifest;
|
||||
for (const auto& [id, def] : manifest.bodies) {
|
||||
Body b;
|
||||
const std::string d = charDir + "/" + id + "/";
|
||||
if (!hd2d::loadImage(d + "body.png", b.fixed)) {
|
||||
HD2D_ERR("character art: missing %sbody.png", d.c_str());
|
||||
return false;
|
||||
}
|
||||
loadOptional(d + "skin_shade.png", b.skin.shade);
|
||||
for (const std::string& item : def.items) {
|
||||
Layer l;
|
||||
loadOptional(d + item + "_shade.png", l.shade);
|
||||
loadOptional(d + item + ".png", l.fixed);
|
||||
b.items[item] = std::move(l);
|
||||
}
|
||||
bodies_[id] = std::move(b);
|
||||
}
|
||||
return !bodies_.empty();
|
||||
}
|
||||
|
||||
hd2d::Image CharacterArt::compose(const Appearance& look) const {
|
||||
auto it = bodies_.find(look.body);
|
||||
if (it == bodies_.end()) it = bodies_.begin();
|
||||
const Body& b = it->second;
|
||||
const CharacterManifest& m = *manifest_;
|
||||
hd2d::Image canvas;
|
||||
canvas.width = b.fixed.width;
|
||||
canvas.height = b.fixed.height;
|
||||
canvas.rgba.assign(b.fixed.rgba.size(), 0);
|
||||
|
||||
uint32_t skin = m.skinTones[std::min<size_t>(look.skin, m.skinTones.size() - 1)];
|
||||
paintShade(canvas, b.skin.shade, skin);
|
||||
// The base tee and shorts are always under everything; recolored only when chosen.
|
||||
for (auto [baseItem, slot] : {std::pair{"top_tee", int(Top)}, std::pair{"bottom_shorts", int(Bottom)}}) {
|
||||
auto li = b.items.find(baseItem);
|
||||
if (li == b.items.end()) continue;
|
||||
const ItemDef* def = m.item(baseItem);
|
||||
uint32_t color = look.items[slot] == baseItem ? look.colors[slot] : (def ? def->color : 0xf0f0f0);
|
||||
paintShade(canvas, li->second.shade, color);
|
||||
}
|
||||
paintFixed(canvas, b.fixed);
|
||||
|
||||
std::array<int, kSlotCount> order{};
|
||||
for (int s = 0; s < kSlotCount; ++s) order[s] = s;
|
||||
std::sort(order.begin(), order.end(), [&](int a, int c) { return m.slotZ[a] < m.slotZ[c]; });
|
||||
for (int s : order) {
|
||||
const std::string& id = look.items[s];
|
||||
if (id.empty()) continue;
|
||||
const ItemDef* def = m.item(id);
|
||||
if (!def || def->fromBody) continue;
|
||||
auto li = b.items.find(id);
|
||||
if (li == b.items.end()) continue;
|
||||
paintShade(canvas, li->second.shade, look.colors[s]);
|
||||
paintFixed(canvas, li->second.fixed);
|
||||
}
|
||||
hd2d::bleedTransparentColors(canvas);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
int SheetLayout::rowFor(float relYaw, bool& flip) {
|
||||
// rows: 0 south (toward camera), 1 east (screen right), 2 north (away)
|
||||
float a = wrapAngle(relYaw);
|
||||
flip = false;
|
||||
if (std::abs(a) <= 0.785f) return 2; // away from the camera
|
||||
if (std::abs(a) >= 2.356f) return 0; // toward the camera
|
||||
flip = a < 0.0f; // screen left = mirrored east
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace bs
|
||||
Reference in New Issue
Block a user