diff --git a/resources/config2/npcs.json b/resources/config2/npcs.json index e6676e1..127ac12 100644 --- a/resources/config2/npcs.json +++ b/resources/config2/npcs.json @@ -2,7 +2,7 @@ "npcs": [ { "id": "npc_01_default", - "name": "NPC Default Guard", + "name": "Студент", "animationIdlePath": "resources/w/jam/man_stand_idle002.anim", "animationWalkPath": "resources/w/jam/man_walk002.anim", "meshTextures": { @@ -35,7 +35,7 @@ }, { "id": "npc_02_woman", - "name": "NPC woman", + "name": "Студентка", "animationIdlePath": "resources/w/jam/woman_idle002.anim", "animationWalkPath": "resources/w/jam/woman_walk002.anim", "meshTextures": { @@ -60,7 +60,7 @@ }, { "id": "npc_03_salesman", - "name": "NPC salesman", + "name": "Мухтар Байке", "animationIdlePath": "resources/w/jam/salesperson_stand_idle003.anim", "animationWalkPath": "resources/w/jam/salesperson_walk001.anim", "meshTextures": { @@ -86,7 +86,7 @@ }, { "id": "npc_04_ghost", - "name": "NPC Floating Ghost", + "name": "Добрый Призрак", "texturePath": "resources/w/ghost_skin001.png", "animationIdlePath": "resources/w/default_float001.anim", "animationWalkPath": "resources/w/default_float001.anim", diff --git a/resources/shaders/defaultColor_web.fragment b/resources/shaders/defaultColor_web.fragment index 1e3d8c1..321a516 100644 --- a/resources/shaders/defaultColor_web.fragment +++ b/resources/shaders/defaultColor_web.fragment @@ -3,7 +3,5 @@ varying vec3 color; void main() { - //gl_FragColor = vec4(color, 1.0); - gl_FragColor = vec4(1.0, 1.0, 0.5, 1.0); - + gl_FragColor = vec4(color, 1.0); } \ No newline at end of file diff --git a/src/Character.cpp b/src/Character.cpp index b5a2bc7..f057880 100644 --- a/src/Character.cpp +++ b/src/Character.cpp @@ -1,7 +1,9 @@ #include "Character.h" +#include "render/TextRenderer.h" #include #include #include +#include #include "GameConstants.h" #include "Environment.h" @@ -104,6 +106,8 @@ AnimationState Character::resolveActiveState() const { } void Character::update(int64_t deltaMs) { + if (initialHp <= 0.f) initialHp = hp; + //weaponInitialRotation = Eigen::AngleAxisf(x/180.0, Eigen::Vector3f::UnitZ()).toRotationMatrix(); //weaponInitialRotation = Eigen::AngleAxisf(-M_PI*0.5, Eigen::Vector3f::UnitZ()).toRotationMatrix(); @@ -820,4 +824,95 @@ void Character::prepareHitSparksForDraw(const Eigen::Matrix4f& cameraViewMatrix) hitSparkEmitter.prepareForDraw(cameraViewMatrix); } +void Character::drawName(TextRenderer& textRenderer, + const Eigen::Matrix4f& cameraViewMatrix, + const Eigen::Matrix4f& projectionMatrix) +{ + if (isPlayer) return; + if (hp <= 0.f) return; + if (npcName.empty()) return; + + Eigen::Vector4f worldPos(position.x(), position.y() + 1.83f, position.z(), 1.f); + Eigen::Vector4f clip = projectionMatrix * cameraViewMatrix * worldPos; + if (clip.w() <= 0.f) return; // behind the camera + + float ndcX = clip.x() / clip.w(); + float ndcY = clip.y() / clip.w(); + if (ndcX < -1.f || ndcX > 1.f || ndcY < -1.f || ndcY > 1.f) return; // off-screen + + float screenX = (ndcX * 0.5f + 0.5f) * Environment::projectionWidth; + float screenY = (ndcY * 0.5f + 0.5f) * Environment::projectionHeight; + + std::array color = canAttack + ? std::array{ 1.f, 0.f, 0.f, 1.f } + : std::array{ 0.f, 1.f, 0.f, 1.f }; + + textRenderer.drawText(npcName, screenX, screenY, 1.0f, true, color); +} + +void Character::drawHealthBar(Renderer& renderer, + const Eigen::Matrix4f& cameraViewMatrix, + const Eigen::Matrix4f& projectionMatrix) +{ + if (!isPlayer && !canAttack) return; + if (hp <= 0.f) return; + if (initialHp <= 0.f) return; + + Eigen::Vector4f worldPos(position.x(), position.y() + 1.75f, position.z(), 1.f); + Eigen::Vector4f clip = projectionMatrix * cameraViewMatrix * worldPos; + if (clip.w() <= 0.f) return; + float ndcX = clip.x() / clip.w(); + float ndcY = clip.y() / clip.w(); + if (ndcX < -1.2f || ndcX > 1.2f || ndcY < -1.2f || ndcY > 1.2f) return; + + const float sx = (ndcX * 0.5f + 0.5f) * Environment::projectionWidth; + const float sy = (ndcY * 0.5f + 0.5f) * Environment::projectionHeight; + + const float barW = 60.f; + const float barH = 6.f; + + const float left = sx - barW * 0.5f; + const float right = sx + barW * 0.5f; + const float bottom = sy - barH * 0.5f; + const float top = sy + barH * 0.5f; + + float frac = hp / initialHp; + if (frac < 0.f) frac = 0.f; + if (frac > 1.f) frac = 1.f; + const float split = left + barW * frac; + + VertexDataStruct data; + const Eigen::Vector3f green(0.f, 1.f, 0.f); + const Eigen::Vector3f red (1.f, 0.f, 0.f); + + auto pushQuad = [&](float x0, float x1, const Eigen::Vector3f& col) { + data.PositionData.push_back({ x0, bottom, 0.f }); + data.PositionData.push_back({ x1, bottom, 0.f }); + data.PositionData.push_back({ x1, top, 0.f }); + data.PositionData.push_back({ x0, bottom, 0.f }); + data.PositionData.push_back({ x1, top, 0.f }); + data.PositionData.push_back({ x0, top, 0.f }); + for (int i = 0; i < 6; ++i) data.ColorData.push_back(col); + }; + + if (frac > 0.f) pushQuad(left, split, green); + if (frac < 1.f) pushQuad(split, right, red); + + healthBarMesh.AssignFrom(data); + healthBarMesh.RefreshVBO(); + + renderer.shaderManager.PushShader("defaultColor"); + renderer.PushProjectionMatrix(0.f, Environment::projectionWidth, + 0.f, Environment::projectionHeight, + -1.f, 1.f); + renderer.PushMatrix(); + renderer.LoadIdentity(); + + renderer.DrawVertexRenderStruct(healthBarMesh); + + renderer.PopMatrix(); + renderer.PopProjectionMatrix(); + renderer.shaderManager.PopShader(); +} + } // namespace ZL diff --git a/src/Character.h b/src/Character.h index bb4b440..8001006 100644 --- a/src/Character.h +++ b/src/Character.h @@ -14,6 +14,8 @@ namespace ZL { +class TextRenderer; + enum class AnimationState { STAND = 0, WALK = 1, @@ -66,6 +68,21 @@ public: // particles against that camera before its VBO is uploaded. void prepareHitSparksForDraw(const Eigen::Matrix4f& cameraViewMatrix); + // Draws the NPC name floating above this character in 2D screen space. + // No-op for the player, for dead characters, or if npcName is empty. + // The text is green for peaceful (canAttack == false) NPCs, red otherwise. + void drawName(TextRenderer& textRenderer, + const Eigen::Matrix4f& cameraViewMatrix, + const Eigen::Matrix4f& projectionMatrix); + + // Draws a horizontal health bar above this character in 2D screen space. + // Visible only for the player and hostile NPCs (canAttack == true), and + // only while hp > 0. The left portion is green (hp / initialHp) and the + // remainder is red. + void drawHealthBar(Renderer& renderer, + const Eigen::Matrix4f& cameraViewMatrix, + const Eigen::Matrix4f& projectionMatrix); + // Public: read by Game for camera tracking and ray-cast origin Eigen::Vector3f position = Eigen::Vector3f(0.f, 0.f, 0.f); float facingAngle = 0.0f; @@ -83,6 +100,9 @@ public: std::string npcName; bool giftReceived = false; float hp = 100.f; + // Captured lazily from `hp` on the first update() tick if left at zero; + // set explicitly if you need a different reference for the health bar. + float initialHp = 0.f; int battle_state = 0; @@ -112,6 +132,7 @@ private: std::map animations; VertexRenderStruct modelMutable; + VertexRenderStruct healthBarMesh; std::unordered_map> meshTextures; Eigen::Vector3f walkTarget = Eigen::Vector3f(0.f, 0.f, 0.f); diff --git a/src/Location.cpp b/src/Location.cpp index 718e7fd..c49581e 100644 --- a/src/Location.cpp +++ b/src/Location.cpp @@ -119,6 +119,7 @@ namespace ZL npc02->loadBinaryAnimation(AnimationState::DEATH_IDLE, "resources/w/default_float001_cut.anim", CONST_ZIP_FILE); npc02->npcId = "ghost_01x"; + npc02->npcName = "Evil Ghost"; npc02->setTexture(ghostTexture); npc02->walkSpeed = 1.5f; npc02->rotationSpeed = 8.0f; @@ -147,6 +148,12 @@ namespace ZL dialogueSystem.init(renderer, CONST_ZIP_FILE); dialogueSystem.loadDatabase("resources/dialogue/sample_dialogues.json"); + + npcNameText = std::make_unique(); + if (!npcNameText->init(renderer, "resources/fonts/DroidSans.ttf", 24, CONST_ZIP_FILE)) { + std::cerr << "Failed to init NPC name TextRenderer" << std::endl; + npcNameText.reset(); + } /*dialogueSystem.addTriggerZone({ "ghost_room_trigger", "test_line_dialogue", @@ -362,6 +369,18 @@ namespace ZL renderer.shaderManager.PopShader(); + if (npcNameText) { + Eigen::Matrix4f proj = MakePerspectiveMatrix(1.0f / 1.5f, + static_cast(Environment::width) / static_cast(Environment::height), + Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); + for (auto& npc : npcs) { + if (npc) npc->drawName(*npcNameText, currentView, proj); + } + if (player) player->drawHealthBar(renderer, currentView, proj); + for (auto& npc : npcs) { + if (npc) npc->drawHealthBar(renderer, currentView, proj); + } + } } void Location::drawShadowDepthPass() @@ -534,6 +553,19 @@ namespace ZL renderer.PopMatrix(); renderer.PopProjectionMatrix(); renderer.shaderManager.PopShader(); + + if (npcNameText) { + Eigen::Matrix4f proj = MakePerspectiveMatrix(1.0f / 1.5f, + static_cast(Environment::width) / static_cast(Environment::height), + Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); + for (auto& npc : npcs) { + if (npc) npc->drawName(*npcNameText, cameraViewMatrix, proj); + } + if (player) player->drawHealthBar(renderer, cameraViewMatrix, proj); + for (auto& npc : npcs) { + if (npc) npc->drawHealthBar(renderer, cameraViewMatrix, proj); + } + } } bool Location::setNavigationAreaAvailable(const std::string& areaName, bool available) diff --git a/src/Location.h b/src/Location.h index d0aa47f..bb8726e 100644 --- a/src/Location.h +++ b/src/Location.h @@ -3,6 +3,7 @@ #include "render/Renderer.h" #include "Environment.h" #include "render/TextureManager.h" +#include "render/TextRenderer.h" #include "items/GameObjectLoader.h" #include "items/InteractiveObject.h" #include "navigation/PathFinder.h" @@ -39,6 +40,8 @@ namespace ZL Eigen::Matrix4f cameraViewMatrix = Eigen::Matrix4f::Identity(); InteractiveObject* targetInteractiveObject = nullptr; + std::unique_ptr npcNameText; + ScriptEngine scriptEngine; Dialogue::DialogueSystem dialogueSystem; diff --git a/src/main.cpp b/src/main.cpp index 1258e80..f746464 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -175,7 +175,7 @@ extern "C" int SDL_main(int argc, char* argv[]) { ZL::Environment::window = SDL_CreateWindow( - "Space Ship Game", + "Bishkek Witcher Game", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, ZL::Environment::width, ZL::Environment::height, SDL_WINDOW_FULLSCREEN | SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN @@ -250,7 +250,7 @@ int main(int argc, char *argv[]) { SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); ZL::Environment::window = SDL_CreateWindow( - "Space Ship Game", + "Bishkek Witcher Game", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, CONST_WIDTH, CONST_HEIGHT, SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN diff --git a/src/render/TextRenderer.cpp b/src/render/TextRenderer.cpp index ad1e915..39efa63 100644 --- a/src/render/TextRenderer.cpp +++ b/src/render/TextRenderer.cpp @@ -461,6 +461,11 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, atlasTexture->getTexID()); + // The R8 atlas is sampled as alpha; without blending the non-glyph area + // of every quad writes opaque black and hides whatever is behind it. + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + //for (size_t i = 0; i < text.length(); ++i) { // auto it = glyphs.find(text[i]); // if (it == glyphs.end()) continue; @@ -479,6 +484,9 @@ void TextRenderer::drawText(const std::string& text, float x, float y, float sca //} r->DrawVertexRenderStruct(cached.mesh); //r->PopMatrix(); + + glDisable(GL_BLEND); + r->shaderManager.PopShader(); // Сброс бинда текстуры не обязателен, но можно для чистоты