From 01e0c2f9a36a8ddcbac61b8468a34a15b86e1470 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Mon, 9 Mar 2026 17:01:38 -0700 Subject: [PATCH] Add world-space unit nameplates projected to screen via camera VP matrix For each visible Unit entity within 40 yards, projects the canonical WoW position (converted to render space) through the camera view-projection matrix to screen pixels. Draws a health bar (hostile=red, friendly=green, target=gold border) and name label with drop shadow using ImGui's background draw list. Fades out smoothly in the last 5 yards of range. --- include/ui/game_screen.hpp | 1 + src/ui/game_screen.cpp | 91 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/include/ui/game_screen.hpp b/include/ui/game_screen.hpp index 639fd577..f293a2c6 100644 --- a/include/ui/game_screen.hpp +++ b/include/ui/game_screen.hpp @@ -238,6 +238,7 @@ private: void renderAuctionHouseWindow(game::GameHandler& gameHandler); void renderDungeonFinderWindow(game::GameHandler& gameHandler); void renderInstanceLockouts(game::GameHandler& gameHandler); + void renderNameplates(game::GameHandler& gameHandler); /** * Inventory screen diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 04550cc7..f81f8699 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -395,6 +395,7 @@ void GameScreen::render(game::GameHandler& gameHandler) { renderCastBar(gameHandler); renderMirrorTimers(gameHandler); renderQuestObjectiveTracker(gameHandler); + renderNameplates(gameHandler); renderCombatText(gameHandler); renderPartyFrames(gameHandler); renderGroupInvitePopup(gameHandler); @@ -4511,6 +4512,96 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) { ImGui::End(); } +// ============================================================ +// Nameplates — world-space health bars projected to screen +// ============================================================ + +void GameScreen::renderNameplates(game::GameHandler& gameHandler) { + if (gameHandler.getState() != game::WorldState::IN_WORLD) return; + + auto* appRenderer = core::Application::getInstance().getRenderer(); + if (!appRenderer) return; + rendering::Camera* camera = appRenderer->getCamera(); + if (!camera) return; + + auto* window = core::Application::getInstance().getWindow(); + if (!window) return; + const float screenW = static_cast(window->getWidth()); + const float screenH = static_cast(window->getHeight()); + + const glm::mat4 viewProj = camera->getProjectionMatrix() * camera->getViewMatrix(); + const glm::vec3 camPos = camera->getPosition(); + const uint64_t playerGuid = gameHandler.getPlayerGuid(); + const uint64_t targetGuid = gameHandler.getTargetGuid(); + + ImDrawList* drawList = ImGui::GetBackgroundDrawList(); + + for (const auto& [guid, entityPtr] : gameHandler.getEntityManager().getEntities()) { + if (!entityPtr || guid == playerGuid) continue; + + auto* unit = dynamic_cast(entityPtr.get()); + if (!unit || unit->getMaxHealth() == 0) continue; + + // Convert canonical WoW position → render space, raise to head height + glm::vec3 renderPos = core::coords::canonicalToRender( + glm::vec3(unit->getX(), unit->getY(), unit->getZ())); + renderPos.z += 2.3f; + + // Cull if too far (render units ≈ WoW yards) + float dist = glm::length(renderPos - camPos); + if (dist > 40.0f) continue; + + // Project to clip space + glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f); + if (clipPos.w <= 0.01f) continue; // Behind camera + + glm::vec3 ndc = glm::vec3(clipPos) / clipPos.w; + if (ndc.x < -1.2f || ndc.x > 1.2f || ndc.y < -1.2f || ndc.y > 1.2f) continue; + + // NDC → screen pixels (Y axis inverted) + float sx = (ndc.x * 0.5f + 0.5f) * screenW; + float sy = (1.0f - (ndc.y * 0.5f + 0.5f)) * screenH; + + // Fade out in the last 5 units of range + float alpha = dist < 35.0f ? 1.0f : 1.0f - (dist - 35.0f) / 5.0f; + auto A = [&](int v) { return static_cast(v * alpha); }; + + // Bar colour by hostility + ImU32 barColor, bgColor; + if (unit->isHostile()) { + barColor = IM_COL32(220, 60, 60, A(200)); + bgColor = IM_COL32(100, 25, 25, A(160)); + } else { + barColor = IM_COL32(60, 200, 80, A(200)); + bgColor = IM_COL32(25, 100, 35, A(160)); + } + ImU32 borderColor = (guid == targetGuid) + ? IM_COL32(255, 215, 0, A(255)) + : IM_COL32(20, 20, 20, A(180)); + + // Bar geometry + constexpr float barW = 80.0f; + constexpr float barH = 8.0f; + const float barX = sx - barW * 0.5f; + + float healthPct = std::clamp( + static_cast(unit->getHealth()) / static_cast(unit->getMaxHealth()), + 0.0f, 1.0f); + + drawList->AddRectFilled(ImVec2(barX, sy), ImVec2(barX + barW, sy + barH), bgColor, 2.0f); + drawList->AddRectFilled(ImVec2(barX, sy), ImVec2(barX + barW * healthPct, sy + barH), barColor, 2.0f); + drawList->AddRect (ImVec2(barX - 1.0f, sy - 1.0f), ImVec2(barX + barW + 1.0f, sy + barH + 1.0f), borderColor, 2.0f); + + // Name label with drop shadow + const char* name = unit->getName().c_str(); + ImVec2 textSize = ImGui::CalcTextSize(name); + float nameX = sx - textSize.x * 0.5f; + float nameY = sy - barH - 12.0f; + drawList->AddText(ImVec2(nameX + 1.0f, nameY + 1.0f), IM_COL32(0, 0, 0, A(160)), name); + drawList->AddText(ImVec2(nameX, nameY), IM_COL32(255, 255, 255, A(220)), name); + } +} + // ============================================================ // Party Frames (Phase 4) // ============================================================