Working on dialog portraits

This commit is contained in:
Vladislav Khorev 2026-04-15 11:56:38 +03:00
parent de2e18e38f
commit eff7322140
7 changed files with 121 additions and 13 deletions

View File

@ -21,6 +21,28 @@
"description": "A token from the Guard - sign of respect", "description": "A token from the Guard - sign of respect",
"icon": "resources/fire2.png" "icon": "resources/fire2.png"
} }
},
{
"id": "npc_03_ghost",
"name": "NPC Floating Ghost",
"texturePath": "resources/w/ghost_skin001.png",
"animationIdlePath": "resources/w/default_float001.txt",
"animationWalkPath": "resources/w/default_float001.txt",
"positionX": 0.0,
"positionY": 0.0,
"positionZ": -5.0,
"walkSpeed": 1.5,
"rotationSpeed": 8.0,
"modelScale": 0.01,
"modelCorrectionRotX": 0.0,
"modelCorrectionRotY": 0.0,
"modelCorrectionRotZ": 0.0,
"gift": {
"id": "ghost_essence",
"name": "Ghost's Essence",
"description": "A mysterious essence from the Ghost realm",
"icon": "resources/fire2.png"
}
} }
] ]
} }

BIN
resources/dialogue/portrait_frame.png (Stored with Git LFS)

Binary file not shown.

View File

@ -9,15 +9,31 @@
"type": "Line", "type": "Line",
"speaker": "Ghost", "speaker": "Ghost",
"portrait": "resources/ghost_avatar.png", "portrait": "resources/ghost_avatar.png",
"text": "You finally came here.", "text": "Наконец-то ты пришел.",
"next": "line_2" "next": "line_2"
}, },
{ {
"id": "line_2", "id": "line_2",
"type": "Line", "type": "Line",
"speaker": "Hero", "speaker": "Hero",
"portrait": "", "portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
"text": "I need answers.", "text": "Ты сделан из дыма?",
"next": "line_3"
},
{
"id": "line_3",
"type": "Line",
"speaker": "Ghost",
"portrait": "resources/ghost_avatar.png",
"text": "Ты думаешь, это смешно?",
"next": "line_4"
},
{
"id": "line_4",
"type": "Line",
"speaker": "Hero",
"portrait": "resources/w/gg/gg2_s_podsvetkoy5.png",
"text": "Я думаю что ты пахнешь как выхлоп от Камаза.",
"next": "end_1" "next": "end_1"
}, },
{ {

1
resources/symbols.txt Normal file
View File

@ -0,0 +1 @@
АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюяӨөҢңҮү

BIN
resources/w/gg/gg2_s_podsvetkoy5.png (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -8,9 +8,27 @@
#include <array> #include <array>
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <unordered_set>
namespace ZL { namespace ZL {
// Decode one UTF-8 codepoint from str at byte position i; advances i past the sequence.
static uint32_t nextUtf8Codepoint(const std::string& str, size_t& i)
{
auto b = [&](size_t offset) { return static_cast<unsigned char>(str[i + offset]); };
unsigned char c = b(0);
uint32_t cp;
size_t bytes;
if (c < 0x80) { cp = c; bytes = 1; }
else if (c < 0xE0) { cp = c & 0x1F; bytes = 2; }
else if (c < 0xF0) { cp = c & 0x0F; bytes = 3; }
else { cp = c & 0x07; bytes = 4; }
for (size_t j = 1; j < bytes && i + j < str.size(); ++j)
cp = (cp << 6) | (b(j) & 0x3F);
i += bytes;
return cp;
}
TextRenderer::~TextRenderer() TextRenderer::~TextRenderer()
{ {
glyphs.clear(); glyphs.clear();
@ -87,6 +105,8 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s
return false; return false;
} }
// Ensure we use the Unicode charmap so Cyrillic codepoints resolve correctly.
FT_Select_Charmap(face, FT_ENCODING_UNICODE);
FT_Set_Pixel_Sizes(face, 0, pixelSize); FT_Set_Pixel_Sizes(face, 0, pixelSize);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
@ -101,7 +121,7 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s
unsigned int advance = 0; unsigned int advance = 0;
}; };
std::vector<std::pair<char, GlyphBitmap>> glyphList; std::vector<std::pair<uint32_t, GlyphBitmap>> glyphList;
glyphList.reserve(128 - 32); glyphList.reserve(128 - 32);
int maxGlyphHeight = 0; int maxGlyphHeight = 0;
@ -114,7 +134,7 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s
gb.height = 0; gb.height = 0;
gb.bearing = { 0.f, 0.f }; gb.bearing = { 0.f, 0.f };
gb.advance = 0; gb.advance = 0;
glyphList.emplace_back((char)c, std::move(gb)); glyphList.emplace_back((uint32_t)c, std::move(gb));
continue; continue;
} }
@ -130,7 +150,51 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s
maxGlyphHeight = max(maxGlyphHeight, gb.height); maxGlyphHeight = max(maxGlyphHeight, gb.height);
} }
glyphList.emplace_back((char)c, std::move(gb)); glyphList.emplace_back((uint32_t)c, std::move(gb));
}
// Load extra codepoints from resources/symbols.txt (UTF-8 encoded).
{
std::vector<char> symbolsRaw;
try {
symbolsRaw = !zipfilename.empty()
? readFileFromZIP("resources/symbols.txt", zipfilename)
: readFile("resources/symbols.txt");
}
catch (...) {} // optional file — silently skip if missing
if (!symbolsRaw.empty()) {
// Collect codepoints already loaded (ASCII 32-127) to skip duplicates.
std::unordered_set<uint32_t> seen;
for (auto& p : glyphList) seen.insert(p.first);
std::string symbolsStr(symbolsRaw.begin(), symbolsRaw.end());
size_t pos = 0;
while (pos < symbolsStr.size()) {
uint32_t cp = nextUtf8Codepoint(symbolsStr, pos);
if (cp < 32 || !seen.insert(cp).second) continue; // skip controls and duplicates
GlyphBitmap gb;
if (FT_Load_Char(face, cp, FT_LOAD_RENDER)) {
gb.width = 0; gb.height = 0;
gb.bearing = { 0.f, 0.f };
gb.advance = 0;
} else {
gb.width = face->glyph->bitmap.width;
gb.height = face->glyph->bitmap.rows;
gb.bearing = Eigen::Vector2f((float)face->glyph->bitmap_left,
(float)face->glyph->bitmap_top);
gb.advance = static_cast<unsigned int>(face->glyph->advance.x);
size_t dataSize = static_cast<size_t>(gb.width) * static_cast<size_t>(gb.height);
if (dataSize > 0) {
gb.data.assign(face->glyph->bitmap.buffer,
face->glyph->bitmap.buffer + dataSize);
maxGlyphHeight = max(maxGlyphHeight, gb.height);
}
}
glyphList.emplace_back(cp, std::move(gb));
}
}
} }
lineHeight = std::max<float>(static_cast<float>(pixelSize) * 1.3f, static_cast<float>(maxGlyphHeight) * 1.25f); lineHeight = std::max<float>(static_cast<float>(pixelSize) * 1.3f, static_cast<float>(maxGlyphHeight) * 1.25f);
@ -185,7 +249,7 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s
rowHeight = 0; rowHeight = 0;
for (auto &p : glyphList) { for (auto &p : glyphList) {
char ch = p.first; uint32_t ch = p.first;
GlyphBitmap &gb = p.second; GlyphBitmap &gb = p.second;
if (gb.width == 0 || gb.height == 0) { if (gb.width == 0 || gb.height == 0) {
@ -300,15 +364,17 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca
float maxH = 0.0f; float maxH = 0.0f;
float maxLineWidth = 0.0f; float maxLineWidth = 0.0f;
for (char ch : text) { size_t textPos = 0;
if (ch == '\n') { while (textPos < text.size()) {
uint32_t cp = nextUtf8Codepoint(text, textPos);
if (cp == '\n') {
maxLineWidth = max(maxLineWidth, penX); maxLineWidth = max(maxLineWidth, penX);
penX = 0.0f; penX = 0.0f;
penY -= lineHeight * scale; penY -= lineHeight * scale;
continue; continue;
} }
auto git = glyphs.find(ch); auto git = glyphs.find(cp);
if (git == glyphs.end()) continue; if (git == glyphs.end()) continue;
const GlyphInfo& g = git->second; const GlyphInfo& g = git->second;

View File

@ -37,7 +37,7 @@ private:
Renderer* r = nullptr; Renderer* r = nullptr;
std::unordered_map<char, GlyphInfo> glyphs; std::unordered_map<uint32_t, GlyphInfo> glyphs;
// OpenGL objects for a dynamic quad // OpenGL objects for a dynamic quad
//unsigned int vao = 0; //unsigned int vao = 0;