diff --git a/resources/config2/npcs.json b/resources/config2/npcs.json index 985301b..0ba6765 100644 --- a/resources/config2/npcs.json +++ b/resources/config2/npcs.json @@ -21,6 +21,28 @@ "description": "A token from the Guard - sign of respect", "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" + } } ] } \ No newline at end of file diff --git a/resources/dialogue/portrait_frame.png b/resources/dialogue/portrait_frame.png index 39f4138..083210e 100644 --- a/resources/dialogue/portrait_frame.png +++ b/resources/dialogue/portrait_frame.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:baf860b51ef2e8e610cbb84e3fb8c468b1ed3d92633c028e2b13013e14bcb216 -size 1286 +oid sha256:151a1f54d4e933aba017fb55274c63c2b38a13df04c010a9054c2ac3cea074b7 +size 1485 diff --git a/resources/dialogue/sample_dialogues.json b/resources/dialogue/sample_dialogues.json index 527b7eb..7c5f018 100644 --- a/resources/dialogue/sample_dialogues.json +++ b/resources/dialogue/sample_dialogues.json @@ -9,15 +9,31 @@ "type": "Line", "speaker": "Ghost", "portrait": "resources/ghost_avatar.png", - "text": "You finally came here.", + "text": "Наконец-то ты пришел.", "next": "line_2" }, { "id": "line_2", "type": "Line", "speaker": "Hero", - "portrait": "", - "text": "I need answers.", + "portrait": "resources/w/gg/gg2_s_podsvetkoy5.png", + "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" }, { diff --git a/resources/symbols.txt b/resources/symbols.txt new file mode 100644 index 0000000..1df4fa5 --- /dev/null +++ b/resources/symbols.txt @@ -0,0 +1 @@ +АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюяӨөҢңҮү \ No newline at end of file diff --git a/resources/w/gg/gg2_s_podsvetkoy5.png b/resources/w/gg/gg2_s_podsvetkoy5.png new file mode 100644 index 0000000..40de839 --- /dev/null +++ b/resources/w/gg/gg2_s_podsvetkoy5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d64d153216d7945ec939bad3f89ac11641182d6910cb084f40b4388b678b78bb +size 187161 diff --git a/src/render/TextRenderer.cpp b/src/render/TextRenderer.cpp index 532a995..5cbb122 100644 --- a/src/render/TextRenderer.cpp +++ b/src/render/TextRenderer.cpp @@ -8,9 +8,27 @@ #include #include #include +#include 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(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() { glyphs.clear(); @@ -87,6 +105,8 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s 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); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); @@ -101,7 +121,7 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s unsigned int advance = 0; }; - std::vector> glyphList; + std::vector> glyphList; glyphList.reserve(128 - 32); int maxGlyphHeight = 0; @@ -114,7 +134,7 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s gb.height = 0; gb.bearing = { 0.f, 0.f }; gb.advance = 0; - glyphList.emplace_back((char)c, std::move(gb)); + glyphList.emplace_back((uint32_t)c, std::move(gb)); continue; } @@ -130,7 +150,51 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s 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 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 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(face->glyph->advance.x); + size_t dataSize = static_cast(gb.width) * static_cast(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(static_cast(pixelSize) * 1.3f, static_cast(maxGlyphHeight) * 1.25f); @@ -185,7 +249,7 @@ bool TextRenderer::loadGlyphs(const std::string& ttfPath, int pixelSize, const s rowHeight = 0; for (auto &p : glyphList) { - char ch = p.first; + uint32_t ch = p.first; GlyphBitmap &gb = p.second; 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 maxLineWidth = 0.0f; - for (char ch : text) { - if (ch == '\n') { + size_t textPos = 0; + while (textPos < text.size()) { + uint32_t cp = nextUtf8Codepoint(text, textPos); + if (cp == '\n') { maxLineWidth = max(maxLineWidth, penX); penX = 0.0f; penY -= lineHeight * scale; continue; } - auto git = glyphs.find(ch); + auto git = glyphs.find(cp); if (git == glyphs.end()) continue; const GlyphInfo& g = git->second; diff --git a/src/render/TextRenderer.h b/src/render/TextRenderer.h index 21fc5f8..ae77229 100644 --- a/src/render/TextRenderer.h +++ b/src/render/TextRenderer.h @@ -37,7 +37,7 @@ private: Renderer* r = nullptr; - std::unordered_map glyphs; + std::unordered_map glyphs; // OpenGL objects for a dynamic quad //unsigned int vao = 0;