#include "CharacterSprite.h" #include "Common.h" #include "hd2d/Log.h" #include #include #include #include namespace bs { std::array 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 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(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 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