- 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
135 lines
5.0 KiB
C++
135 lines
5.0 KiB
C++
#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
|