mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-07 17:43:51 +00:00
Some checks failed
Build / Build (arm64) (push) Has been cancelled
Build / Build (x86-64) (push) Has been cancelled
Build / Build (macOS arm64) (push) Has been cancelled
Build / Build (windows-arm64) (push) Has been cancelled
Build / Build (windows-x86-64) (push) Has been cancelled
Security / CodeQL (C/C++) (push) Has been cancelled
Security / Semgrep (push) Has been cancelled
Security / Sanitizer Build (ASan/UBSan) (push) Has been cancelled
Re-allocate megaBoneSet_[0..1] in M2Renderer::clear() after vkResetDescriptorPool invalidates all sets from boneDescPool_. Stale handles were bound to command buffers during rendering, causing cascading validation errors. Also add ImGui::Dummy() after SetCursorScreenPos in the shaman totem bar to satisfy ImGui's window boundary extension assertion.
2433 lines
121 KiB
C++
2433 lines
121 KiB
C++
#include "ui/game_screen.hpp"
|
|
#include "ui/ui_colors.hpp"
|
|
#include "ui/ui_helpers.hpp"
|
|
#include "rendering/vk_context.hpp"
|
|
#include "core/application.hpp"
|
|
#include "core/appearance_composer.hpp"
|
|
#include "addons/addon_manager.hpp"
|
|
#include "core/coordinates.hpp"
|
|
#include "core/input.hpp"
|
|
#include "rendering/renderer.hpp"
|
|
#include "rendering/post_process_pipeline.hpp"
|
|
#include "rendering/animation_controller.hpp"
|
|
#include "rendering/wmo_renderer.hpp"
|
|
#include "rendering/terrain_manager.hpp"
|
|
#include "rendering/minimap.hpp"
|
|
#include "rendering/world_map.hpp"
|
|
#include "rendering/character_renderer.hpp"
|
|
#include "rendering/camera.hpp"
|
|
#include "rendering/camera_controller.hpp"
|
|
#include "audio/audio_coordinator.hpp"
|
|
#include "audio/audio_engine.hpp"
|
|
#include "audio/music_manager.hpp"
|
|
#include "game/zone_manager.hpp"
|
|
#include "audio/footstep_manager.hpp"
|
|
#include "audio/activity_sound_manager.hpp"
|
|
#include "audio/mount_sound_manager.hpp"
|
|
#include "audio/npc_voice_manager.hpp"
|
|
#include "audio/ambient_sound_manager.hpp"
|
|
#include "audio/ui_sound_manager.hpp"
|
|
#include "audio/combat_sound_manager.hpp"
|
|
#include "audio/spell_sound_manager.hpp"
|
|
#include "audio/movement_sound_manager.hpp"
|
|
#include "pipeline/asset_manager.hpp"
|
|
#include "pipeline/dbc_loader.hpp"
|
|
#include "pipeline/dbc_layout.hpp"
|
|
|
|
#include "game/expansion_profile.hpp"
|
|
#include "game/character.hpp"
|
|
#include "core/logger.hpp"
|
|
#include <imgui.h>
|
|
#include <imgui_internal.h>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <sstream>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <cctype>
|
|
#include <chrono>
|
|
#include <ctime>
|
|
|
|
#include <unordered_set>
|
|
|
|
namespace {
|
|
using namespace wowee::ui::colors;
|
|
using namespace wowee::ui::helpers;
|
|
constexpr auto& kColorRed = kRed;
|
|
constexpr auto& kColorGreen = kGreen;
|
|
constexpr auto& kColorBrightGreen= kBrightGreen;
|
|
constexpr auto& kColorYellow = kYellow;
|
|
constexpr auto& kColorGray = kGray;
|
|
constexpr auto& kColorDarkGray = kDarkGray;
|
|
|
|
// Abbreviated month names (indexed 0-11)
|
|
constexpr const char* kMonthAbbrev[12] = {
|
|
"Jan","Feb","Mar","Apr","May","Jun",
|
|
"Jul","Aug","Sep","Oct","Nov","Dec"
|
|
};
|
|
|
|
// Common ImGui window flags for popup dialogs
|
|
const ImGuiWindowFlags kDialogFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize;
|
|
|
|
bool raySphereIntersect(const wowee::rendering::Ray& ray, const glm::vec3& center, float radius, float& tOut) {
|
|
glm::vec3 oc = ray.origin - center;
|
|
float b = glm::dot(oc, ray.direction);
|
|
float c = glm::dot(oc, oc) - radius * radius;
|
|
float discriminant = b * b - c;
|
|
if (discriminant < 0.0f) return false;
|
|
float t = -b - std::sqrt(discriminant);
|
|
if (t < 0.0f) t = -b + std::sqrt(discriminant);
|
|
if (t < 0.0f) return false;
|
|
tOut = t;
|
|
return true;
|
|
}
|
|
|
|
std::string getEntityName(const std::shared_ptr<wowee::game::Entity>& entity) {
|
|
if (entity->getType() == wowee::game::ObjectType::PLAYER) {
|
|
auto player = std::static_pointer_cast<wowee::game::Player>(entity);
|
|
if (!player->getName().empty()) return player->getName();
|
|
} else if (entity->getType() == wowee::game::ObjectType::UNIT) {
|
|
auto unit = std::static_pointer_cast<wowee::game::Unit>(entity);
|
|
if (!unit->getName().empty()) return unit->getName();
|
|
} else if (entity->getType() == wowee::game::ObjectType::GAMEOBJECT) {
|
|
auto go = std::static_pointer_cast<wowee::game::GameObject>(entity);
|
|
if (!go->getName().empty()) return go->getName();
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
}
|
|
|
|
namespace wowee { namespace ui {
|
|
|
|
void GameScreen::renderPlayerFrame(game::GameHandler& gameHandler) {
|
|
bool isDead = gameHandler.isPlayerDead();
|
|
ImGui::SetNextWindowPos(ImVec2(10.0f, 30.0f), ImGuiCond_Always);
|
|
ImGui::SetNextWindowSize(ImVec2(250.0f, 0.0f), ImGuiCond_Always);
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.85f));
|
|
const bool inCombatConfirmed = gameHandler.isInCombat();
|
|
const bool attackIntentOnly = gameHandler.hasAutoAttackIntent() && !inCombatConfirmed;
|
|
ImVec4 playerBorder = isDead
|
|
? kColorDarkGray
|
|
: (inCombatConfirmed
|
|
? colors::kBrightRed
|
|
: (attackIntentOnly
|
|
? ImVec4(1.0f, 0.7f, 0.2f, 1.0f)
|
|
: ImVec4(0.4f, 0.4f, 0.4f, 1.0f)));
|
|
ImGui::PushStyleColor(ImGuiCol_Border, playerBorder);
|
|
|
|
if (ImGui::Begin("##PlayerFrame", nullptr, flags)) {
|
|
// Use selected character info if available, otherwise defaults
|
|
std::string playerName = "Adventurer";
|
|
uint32_t playerLevel = 1;
|
|
uint32_t playerHp = 100;
|
|
uint32_t playerMaxHp = 100;
|
|
|
|
const auto& characters = gameHandler.getCharacters();
|
|
uint64_t activeGuid = gameHandler.getActiveCharacterGuid();
|
|
const game::Character* activeChar = nullptr;
|
|
for (const auto& c : characters) {
|
|
if (c.guid == activeGuid) { activeChar = &c; break; }
|
|
}
|
|
if (!activeChar && !characters.empty()) activeChar = &characters[0];
|
|
if (activeChar) {
|
|
const auto& ch = *activeChar;
|
|
playerName = ch.name;
|
|
// Use live server level if available, otherwise character struct
|
|
playerLevel = gameHandler.getPlayerLevel();
|
|
if (playerLevel == 0) playerLevel = ch.level;
|
|
playerMaxHp = 20 + playerLevel * 10;
|
|
playerHp = playerMaxHp;
|
|
}
|
|
|
|
// Derive class color via shared helper
|
|
ImVec4 classColor = activeChar
|
|
? classColorVec4(static_cast<uint8_t>(activeChar->characterClass))
|
|
: kColorBrightGreen;
|
|
|
|
// Name in class color — clickable for self-target, right-click for menu
|
|
ImGui::PushStyleColor(ImGuiCol_Text, classColor);
|
|
if (ImGui::Selectable(playerName.c_str(), false, 0, ImVec2(0, 0))) {
|
|
gameHandler.setTarget(gameHandler.getPlayerGuid());
|
|
}
|
|
if (ImGui::BeginPopupContextItem("PlayerSelfCtx")) {
|
|
ImGui::TextDisabled("%s", playerName.c_str());
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Open Character")) {
|
|
inventoryScreen.setCharacterOpen(true);
|
|
}
|
|
if (ImGui::MenuItem("Toggle PvP")) {
|
|
gameHandler.togglePvp();
|
|
}
|
|
ImGui::Separator();
|
|
bool afk = gameHandler.isAfk();
|
|
bool dnd = gameHandler.isDnd();
|
|
if (ImGui::MenuItem(afk ? "Cancel AFK" : "Set AFK")) {
|
|
gameHandler.toggleAfk();
|
|
}
|
|
if (ImGui::MenuItem(dnd ? "Cancel DND" : "Set DND")) {
|
|
gameHandler.toggleDnd();
|
|
}
|
|
if (gameHandler.isInGroup()) {
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Leave Group")) {
|
|
gameHandler.leaveGroup();
|
|
}
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
ImGui::PopStyleColor();
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("Lv %u", playerLevel);
|
|
if (isDead) {
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(colors::kDarkRed, "DEAD");
|
|
}
|
|
// Group leader crown on self frame when you lead the party/raid
|
|
if (gameHandler.isInGroup() &&
|
|
gameHandler.getPartyData().leaderGuid == gameHandler.getPlayerGuid()) {
|
|
ImGui::SameLine(0, 4);
|
|
ImGui::TextColored(colors::kSymbolGold, "\xe2\x99\x9b");
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("You are the group leader");
|
|
}
|
|
if (gameHandler.isAfk()) {
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.3f, 1.0f), "<AFK>");
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Away from keyboard — /afk to cancel");
|
|
} else if (gameHandler.isDnd()) {
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(0.9f, 0.5f, 0.2f, 1.0f), "<DND>");
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Do not disturb — /dnd to cancel");
|
|
}
|
|
if (auto* ren = services_.renderer) {
|
|
if (auto* cam = ren->getCameraController()) {
|
|
if (cam->isAutoRunning()) {
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(0.4f, 0.9f, 1.0f, 1.0f), "[Auto-Run]");
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Auto-running — press ` or NumLock to stop");
|
|
}
|
|
}
|
|
}
|
|
if (inCombatConfirmed && !isDead) {
|
|
float combatPulse = 0.75f + 0.25f * std::sin(static_cast<float>(ImGui::GetTime()) * 4.0f);
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(1.0f, 0.2f * combatPulse, 0.2f * combatPulse, 1.0f), "[Combat]");
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("You are in combat");
|
|
}
|
|
|
|
// Active title — shown in gold below the name/level line
|
|
{
|
|
int32_t titleBit = gameHandler.getChosenTitleBit();
|
|
if (titleBit >= 0) {
|
|
const std::string titleText = gameHandler.getFormattedTitle(
|
|
static_cast<uint32_t>(titleBit));
|
|
if (!titleText.empty()) {
|
|
ImGui::TextColored(ImVec4(1.0f, 0.84f, 0.0f, 0.9f), "%s", titleText.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Try to get real HP/mana from the player entity
|
|
auto playerEntity = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid());
|
|
if (playerEntity && (playerEntity->getType() == game::ObjectType::PLAYER || playerEntity->getType() == game::ObjectType::UNIT)) {
|
|
auto unit = std::static_pointer_cast<game::Unit>(playerEntity);
|
|
if (unit->getMaxHealth() > 0) {
|
|
playerHp = unit->getHealth();
|
|
playerMaxHp = unit->getMaxHealth();
|
|
}
|
|
}
|
|
|
|
// Health bar — color transitions green→yellow→red as HP drops
|
|
float pct = static_cast<float>(playerHp) / static_cast<float>(playerMaxHp);
|
|
ImVec4 hpColor;
|
|
if (isDead) {
|
|
hpColor = kColorDarkGray;
|
|
} else if (pct > 0.5f) {
|
|
hpColor = colors::kHealthGreen; // green
|
|
} else if (pct > 0.2f) {
|
|
float t = (pct - 0.2f) / 0.3f; // 0 at 20%, 1 at 50%
|
|
hpColor = ImVec4(0.9f - 0.7f * t, 0.4f + 0.4f * t, 0.0f, 1.0f); // orange→yellow
|
|
} else {
|
|
// Critical — pulse red when < 20%
|
|
float pulse = 0.7f + 0.3f * std::sin(static_cast<float>(ImGui::GetTime()) * 3.5f);
|
|
hpColor = ImVec4(0.9f * pulse, 0.05f, 0.05f, 1.0f); // pulsing red
|
|
}
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, hpColor);
|
|
char overlay[64];
|
|
snprintf(overlay, sizeof(overlay), "%u / %u", playerHp, playerMaxHp);
|
|
ImGui::ProgressBar(pct, ImVec2(-1, 18), overlay);
|
|
ImGui::PopStyleColor();
|
|
|
|
// Mana/Power bar
|
|
if (playerEntity && (playerEntity->getType() == game::ObjectType::PLAYER || playerEntity->getType() == game::ObjectType::UNIT)) {
|
|
auto unit = std::static_pointer_cast<game::Unit>(playerEntity);
|
|
uint8_t powerType = unit->getPowerType();
|
|
uint32_t power = unit->getPower();
|
|
uint32_t maxPower = unit->getMaxPower();
|
|
// Rage (1), Focus (2), Energy (3), and Runic Power (6) always cap at 100.
|
|
// Show bar even if server hasn't sent UNIT_FIELD_MAXPOWER1 yet.
|
|
if (maxPower == 0 && (powerType == 1 || powerType == 2 || powerType == 3 || powerType == 6)) maxPower = 100;
|
|
if (maxPower > 0) {
|
|
float mpPct = static_cast<float>(power) / static_cast<float>(maxPower);
|
|
ImVec4 powerColor;
|
|
switch (powerType) {
|
|
case 0: {
|
|
// Mana: pulse desaturated blue when critically low (< 20%)
|
|
if (mpPct < 0.2f) {
|
|
float pulse = 0.6f + 0.4f * std::sin(static_cast<float>(ImGui::GetTime()) * 3.0f);
|
|
powerColor = ImVec4(0.1f, 0.1f, 0.8f * pulse, 1.0f);
|
|
} else {
|
|
powerColor = colors::kManaBlue;
|
|
}
|
|
break;
|
|
}
|
|
case 1: powerColor = colors::kDarkRed; break; // Rage (red)
|
|
case 2: powerColor = colors::kOrange; break; // Focus (orange)
|
|
case 3: powerColor = colors::kEnergyYellow; break; // Energy (yellow)
|
|
case 4: powerColor = colors::kHappinessGreen; break; // Happiness (green)
|
|
case 6: powerColor = colors::kRunicRed; break; // Runic Power (crimson)
|
|
case 7: powerColor = colors::kSoulShardPurple; break; // Soul Shards (purple)
|
|
default: powerColor = colors::kManaBlue; break;
|
|
}
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, powerColor);
|
|
char mpOverlay[64];
|
|
snprintf(mpOverlay, sizeof(mpOverlay), "%u / %u", power, maxPower);
|
|
ImGui::ProgressBar(mpPct, ImVec2(-1, 18), mpOverlay);
|
|
ImGui::PopStyleColor();
|
|
}
|
|
}
|
|
|
|
// Death Knight rune bar (class 6) — 6 colored squares with fill fraction
|
|
if (gameHandler.getPlayerClass() == 6) {
|
|
const auto& runes = gameHandler.getPlayerRunes();
|
|
float dt = ImGui::GetIO().DeltaTime;
|
|
|
|
ImGui::Spacing();
|
|
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
|
float totalW = ImGui::GetContentRegionAvail().x;
|
|
float spacing = 3.0f;
|
|
float squareW = (totalW - spacing * 5.0f) / 6.0f;
|
|
float squareH = 14.0f;
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
|
|
for (int i = 0; i < 6; i++) {
|
|
// Client-side prediction: advance fill over ~10s cooldown
|
|
runeClientFill_[i] = runes[i].ready ? 1.0f
|
|
: std::min(runeClientFill_[i] + dt / 10.0f, runes[i].readyFraction + 0.02f);
|
|
runeClientFill_[i] = std::clamp(runeClientFill_[i], 0.0f, runes[i].ready ? 1.0f : 0.97f);
|
|
|
|
float x0 = cursor.x + i * (squareW + spacing);
|
|
float y0 = cursor.y;
|
|
float x1 = x0 + squareW;
|
|
float y1 = y0 + squareH;
|
|
|
|
// Background (dark)
|
|
dl->AddRectFilled(ImVec2(x0, y0), ImVec2(x1, y1),
|
|
IM_COL32(30, 30, 30, 200), 2.0f);
|
|
|
|
// Fill color by rune type
|
|
ImVec4 fc;
|
|
switch (runes[i].type) {
|
|
case game::GameHandler::RuneType::Blood: fc = ImVec4(0.85f, 0.12f, 0.12f, 1.0f); break;
|
|
case game::GameHandler::RuneType::Unholy: fc = ImVec4(0.20f, 0.72f, 0.20f, 1.0f); break;
|
|
case game::GameHandler::RuneType::Frost: fc = ImVec4(0.30f, 0.55f, 0.90f, 1.0f); break;
|
|
case game::GameHandler::RuneType::Death: fc = ImVec4(0.55f, 0.20f, 0.70f, 1.0f); break;
|
|
default: fc = ImVec4(0.6f, 0.6f, 0.6f, 1.0f); break;
|
|
}
|
|
float fillX = x0 + (x1 - x0) * runeClientFill_[i];
|
|
dl->AddRectFilled(ImVec2(x0, y0), ImVec2(fillX, y1),
|
|
ImGui::ColorConvertFloat4ToU32(fc), 2.0f);
|
|
|
|
// Border
|
|
ImU32 borderCol = runes[i].ready
|
|
? IM_COL32(220, 220, 220, 180)
|
|
: IM_COL32(100, 100, 100, 160);
|
|
dl->AddRect(ImVec2(x0, y0), ImVec2(x1, y1), borderCol, 2.0f);
|
|
}
|
|
ImGui::Dummy(ImVec2(totalW, squareH));
|
|
}
|
|
|
|
// Combo point display — Rogue (4) and Druid (11) in Cat Form
|
|
{
|
|
uint8_t cls = gameHandler.getPlayerClass();
|
|
const bool isRogue = (cls == 4);
|
|
const bool isDruid = (cls == 11);
|
|
if (isRogue || isDruid) {
|
|
uint8_t cp = gameHandler.getComboPoints();
|
|
if (cp > 0 || isRogue) { // always show for rogue; only when non-zero for druid
|
|
ImGui::Spacing();
|
|
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
|
float totalW = ImGui::GetContentRegionAvail().x;
|
|
constexpr int MAX_CP = 5;
|
|
constexpr float DOT_R = 7.0f;
|
|
constexpr float SPACING = 4.0f;
|
|
float totalDotsW = MAX_CP * (DOT_R * 2.0f) + (MAX_CP - 1) * SPACING;
|
|
float startX = cursor.x + (totalW - totalDotsW) * 0.5f;
|
|
float cy = cursor.y + DOT_R;
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
for (int i = 0; i < MAX_CP; ++i) {
|
|
float cx = startX + i * (DOT_R * 2.0f + SPACING) + DOT_R;
|
|
ImU32 col = (i < static_cast<int>(cp))
|
|
? IM_COL32(255, 210, 0, 240) // bright gold — active
|
|
: IM_COL32(60, 60, 60, 160); // dark — empty
|
|
dl->AddCircleFilled(ImVec2(cx, cy), DOT_R, col);
|
|
dl->AddCircle(ImVec2(cx, cy), DOT_R, IM_COL32(160, 140, 0, 180), 0, 1.5f);
|
|
}
|
|
ImGui::Dummy(ImVec2(totalW, DOT_R * 2.0f));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Shaman totem bar (class 7) — 4 slots: Earth, Fire, Water, Air
|
|
if (gameHandler.getPlayerClass() == 7) {
|
|
static constexpr ImVec4 kTotemColors[] = {
|
|
ImVec4(0.80f, 0.55f, 0.25f, 1.0f), // Earth — brown
|
|
ImVec4(1.00f, 0.35f, 0.10f, 1.0f), // Fire — orange-red
|
|
ImVec4(0.20f, 0.55f, 0.90f, 1.0f), // Water — blue
|
|
ImVec4(0.70f, 0.90f, 1.00f, 1.0f), // Air — pale sky
|
|
};
|
|
static constexpr const char* kTotemNames[] = { "Earth", "Fire", "Water", "Air" };
|
|
|
|
ImGui::Spacing();
|
|
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
|
float totalW = ImGui::GetContentRegionAvail().x;
|
|
float spacing = 3.0f;
|
|
float slotW = (totalW - spacing * 3.0f) / 4.0f;
|
|
float slotH = 14.0f;
|
|
ImDrawList* tdl = ImGui::GetWindowDrawList();
|
|
|
|
for (int i = 0; i < game::GameHandler::NUM_TOTEM_SLOTS; i++) {
|
|
const auto& ts = gameHandler.getTotemSlot(i);
|
|
float x0 = cursor.x + i * (slotW + spacing);
|
|
float y0 = cursor.y;
|
|
float x1 = x0 + slotW;
|
|
float y1 = y0 + slotH;
|
|
|
|
// Background
|
|
tdl->AddRectFilled(ImVec2(x0, y0), ImVec2(x1, y1), IM_COL32(20, 20, 20, 200), 2.0f);
|
|
|
|
if (ts.active()) {
|
|
float rem = ts.remainingMs();
|
|
float frac = rem / static_cast<float>(ts.durationMs);
|
|
float fillX = x0 + (x1 - x0) * frac;
|
|
tdl->AddRectFilled(ImVec2(x0, y0), ImVec2(fillX, y1),
|
|
ImGui::ColorConvertFloat4ToU32(kTotemColors[i]), 2.0f);
|
|
// Remaining seconds label
|
|
char secBuf[16];
|
|
snprintf(secBuf, sizeof(secBuf), "%.0f", rem / 1000.0f);
|
|
ImVec2 tsz = ImGui::CalcTextSize(secBuf);
|
|
float lx = x0 + (slotW - tsz.x) * 0.5f;
|
|
float ly = y0 + (slotH - tsz.y) * 0.5f;
|
|
tdl->AddText(ImVec2(lx + 1, ly + 1), IM_COL32(0, 0, 0, 180), secBuf);
|
|
tdl->AddText(ImVec2(lx, ly), IM_COL32(255, 255, 255, 230), secBuf);
|
|
} else {
|
|
// Inactive — show element letter
|
|
const char* letter = kTotemNames[i];
|
|
char single[2] = { letter[0], '\0' };
|
|
ImVec2 tsz = ImGui::CalcTextSize(single);
|
|
float lx = x0 + (slotW - tsz.x) * 0.5f;
|
|
float ly = y0 + (slotH - tsz.y) * 0.5f;
|
|
tdl->AddText(ImVec2(lx, ly), IM_COL32(80, 80, 80, 200), single);
|
|
}
|
|
|
|
// Border
|
|
ImU32 borderCol = ts.active()
|
|
? ImGui::ColorConvertFloat4ToU32(kTotemColors[i])
|
|
: IM_COL32(60, 60, 60, 160);
|
|
tdl->AddRect(ImVec2(x0, y0), ImVec2(x1, y1), borderCol, 2.0f);
|
|
|
|
// Tooltip on hover
|
|
ImGui::SetCursorScreenPos(ImVec2(x0, y0));
|
|
char totemBtnId[16]; snprintf(totemBtnId, sizeof(totemBtnId), "##totem%d", i);
|
|
ImGui::InvisibleButton(totemBtnId, ImVec2(slotW, slotH));
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::BeginTooltip();
|
|
if (ts.active()) {
|
|
const std::string& spellNm = gameHandler.getSpellName(ts.spellId);
|
|
ImGui::TextColored(ImVec4(kTotemColors[i].x, kTotemColors[i].y,
|
|
kTotemColors[i].z, 1.0f),
|
|
"%s Totem", kTotemNames[i]);
|
|
if (!spellNm.empty()) ImGui::Text("%s", spellNm.c_str());
|
|
ImGui::Text("%.1fs remaining", ts.remainingMs() / 1000.0f);
|
|
} else {
|
|
ImGui::TextDisabled("%s Totem (empty)", kTotemNames[i]);
|
|
}
|
|
ImGui::EndTooltip();
|
|
}
|
|
}
|
|
ImGui::SetCursorScreenPos(ImVec2(cursor.x, cursor.y + slotH + 2.0f));
|
|
ImGui::Dummy(ImVec2(totalW, 0.0f));
|
|
}
|
|
}
|
|
|
|
// Melee swing timer — shown when player is auto-attacking
|
|
if (gameHandler.isAutoAttacking()) {
|
|
const uint64_t lastSwingMs = gameHandler.getLastMeleeSwingMs();
|
|
if (lastSwingMs > 0) {
|
|
// Determine weapon speed from the equipped main-hand weapon
|
|
uint32_t weaponDelayMs = 2000; // Default: 2.0s unarmed
|
|
const auto& mainSlot = gameHandler.getInventory().getEquipSlot(game::EquipSlot::MAIN_HAND);
|
|
if (!mainSlot.empty() && mainSlot.item.itemId != 0) {
|
|
const auto* info = gameHandler.getItemInfo(mainSlot.item.itemId);
|
|
if (info && info->delayMs > 0) {
|
|
weaponDelayMs = info->delayMs;
|
|
}
|
|
}
|
|
|
|
// Compute elapsed since last swing
|
|
uint64_t nowMs = static_cast<uint64_t>(
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::system_clock::now().time_since_epoch()).count());
|
|
uint64_t elapsedMs = (nowMs >= lastSwingMs) ? (nowMs - lastSwingMs) : 0;
|
|
|
|
// Clamp to weapon delay (cap at 1.0 so the bar fills but doesn't exceed)
|
|
float pct = std::min(static_cast<float>(elapsedMs) / static_cast<float>(weaponDelayMs), 1.0f);
|
|
|
|
// Light silver-orange color indicating auto-attack readiness
|
|
ImVec4 swingColor = (pct >= 0.95f)
|
|
? ImVec4(1.0f, 0.75f, 0.15f, 1.0f) // gold when ready to swing
|
|
: ImVec4(0.65f, 0.55f, 0.40f, 1.0f); // muted brown-orange while filling
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, swingColor);
|
|
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.15f, 0.12f, 0.08f, 0.8f));
|
|
char swingLabel[24];
|
|
float remainSec = std::max(0.0f, (weaponDelayMs - static_cast<float>(elapsedMs)) / 1000.0f);
|
|
if (pct >= 0.98f)
|
|
snprintf(swingLabel, sizeof(swingLabel), "Swing!");
|
|
else
|
|
snprintf(swingLabel, sizeof(swingLabel), "%.1fs", remainSec);
|
|
ImGui::ProgressBar(pct, ImVec2(-1.0f, 8.0f), swingLabel);
|
|
ImGui::PopStyleColor(2);
|
|
}
|
|
}
|
|
|
|
ImGui::End();
|
|
|
|
ImGui::PopStyleColor(2);
|
|
ImGui::PopStyleVar();
|
|
}
|
|
|
|
void GameScreen::renderPetFrame(game::GameHandler& gameHandler) {
|
|
uint64_t petGuid = gameHandler.getPetGuid();
|
|
if (petGuid == 0) return;
|
|
|
|
auto petEntity = gameHandler.getEntityManager().getEntity(petGuid);
|
|
if (!petEntity) return;
|
|
auto* petUnit = petEntity->isUnit() ? static_cast<game::Unit*>(petEntity.get()) : nullptr;
|
|
if (!petUnit) return;
|
|
|
|
// Position below player frame. If in a group, push below party frames
|
|
// (party frame at y=120, each member ~50px, up to 4 members → max ~320px + y=120 = ~440).
|
|
// When not grouped, the player frame ends at ~110px so y=125 is fine.
|
|
const int partyMemberCount = gameHandler.isInGroup()
|
|
? static_cast<int>(gameHandler.getPartyData().members.size()) : 0;
|
|
float petY = (partyMemberCount > 0)
|
|
? 120.0f + partyMemberCount * 52.0f + 8.0f
|
|
: 125.0f;
|
|
ImGui::SetNextWindowPos(ImVec2(10.0f, petY), ImGuiCond_Always);
|
|
ImGui::SetNextWindowSize(ImVec2(200.0f, 0.0f), ImGuiCond_Always);
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.1f, 0.08f, 0.85f));
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.2f, 0.6f, 0.2f, 1.0f));
|
|
|
|
if (ImGui::Begin("##PetFrame", nullptr, flags)) {
|
|
const std::string& petName = petUnit->getName();
|
|
uint32_t petLevel = petUnit->getLevel();
|
|
|
|
// Name + level on one row — clicking the pet name targets it
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.4f, 0.9f, 0.4f, 1.0f));
|
|
char petLabel[96];
|
|
snprintf(petLabel, sizeof(petLabel), "%s",
|
|
petName.empty() ? "Pet" : petName.c_str());
|
|
if (ImGui::Selectable(petLabel, false, 0, ImVec2(0, 0))) {
|
|
gameHandler.setTarget(petGuid);
|
|
}
|
|
// Right-click context menu on pet name
|
|
if (ImGui::BeginPopupContextItem("PetNameCtx")) {
|
|
ImGui::TextDisabled("%s", petLabel);
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Target Pet")) {
|
|
gameHandler.setTarget(petGuid);
|
|
}
|
|
if (ImGui::MenuItem("Rename Pet")) {
|
|
ImGui::CloseCurrentPopup();
|
|
petRenameOpen_ = true;
|
|
petRenameBuf_[0] = '\0';
|
|
}
|
|
if (ImGui::MenuItem("Dismiss Pet")) {
|
|
gameHandler.dismissPet();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
// Pet rename modal (opened via context menu)
|
|
if (petRenameOpen_) {
|
|
ImGui::OpenPopup("Rename Pet###PetRename");
|
|
petRenameOpen_ = false;
|
|
}
|
|
if (ImGui::BeginPopupModal("Rename Pet###PetRename", nullptr,
|
|
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse)) {
|
|
ImGui::Text("Enter new pet name (max 12 characters):");
|
|
ImGui::SetNextItemWidth(180.0f);
|
|
bool submitted = ImGui::InputText("##PetRenameInput", petRenameBuf_, sizeof(petRenameBuf_),
|
|
ImGuiInputTextFlags_EnterReturnsTrue);
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("OK") || submitted) {
|
|
std::string newName(petRenameBuf_);
|
|
if (!newName.empty() && newName.size() <= 12) {
|
|
gameHandler.renamePet(newName);
|
|
}
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Cancel")) {
|
|
ImGui::CloseCurrentPopup();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
ImGui::PopStyleColor();
|
|
if (petLevel > 0) {
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("Lv %u", petLevel);
|
|
}
|
|
|
|
// Health bar
|
|
uint32_t hp = petUnit->getHealth();
|
|
uint32_t maxHp = petUnit->getMaxHealth();
|
|
if (maxHp > 0) {
|
|
float pct = static_cast<float>(hp) / static_cast<float>(maxHp);
|
|
ImVec4 petHpColor = pct > 0.5f ? colors::kHealthGreen
|
|
: pct > 0.2f ? ImVec4(0.9f, 0.6f, 0.0f, 1.0f)
|
|
: ImVec4(0.9f, 0.15f, 0.15f, 1.0f);
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, petHpColor);
|
|
char hpText[32];
|
|
snprintf(hpText, sizeof(hpText), "%u/%u", hp, maxHp);
|
|
ImGui::ProgressBar(pct, ImVec2(-1, 14), hpText);
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
// Power/mana bar (hunters' pets use focus)
|
|
uint8_t powerType = petUnit->getPowerType();
|
|
uint32_t power = petUnit->getPower();
|
|
uint32_t maxPower = petUnit->getMaxPower();
|
|
if (maxPower == 0 && (powerType == 1 || powerType == 2 || powerType == 3)) maxPower = 100;
|
|
if (maxPower > 0) {
|
|
float mpPct = static_cast<float>(power) / static_cast<float>(maxPower);
|
|
ImVec4 powerColor;
|
|
switch (powerType) {
|
|
case 0: powerColor = colors::kManaBlue; break; // Mana
|
|
case 1: powerColor = colors::kDarkRed; break; // Rage
|
|
case 2: powerColor = colors::kOrange; break; // Focus (hunter pets)
|
|
case 3: powerColor = colors::kEnergyYellow; break; // Energy
|
|
default: powerColor = colors::kManaBlue; break;
|
|
}
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, powerColor);
|
|
char mpText[32];
|
|
snprintf(mpText, sizeof(mpText), "%u/%u", power, maxPower);
|
|
ImGui::ProgressBar(mpPct, ImVec2(-1, 14), mpText);
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
// Happiness bar — hunter pets store happiness as power type 4
|
|
{
|
|
uint32_t happiness = petUnit->getPowerByType(4);
|
|
uint32_t maxHappiness = petUnit->getMaxPowerByType(4);
|
|
if (maxHappiness > 0 && happiness > 0) {
|
|
float hapPct = static_cast<float>(happiness) / static_cast<float>(maxHappiness);
|
|
// Tier: < 33% = Unhappy (red), < 67% = Content (yellow), >= 67% = Happy (green)
|
|
ImVec4 hapColor = hapPct >= 0.667f ? ImVec4(0.2f, 0.85f, 0.2f, 1.0f)
|
|
: hapPct >= 0.333f ? ImVec4(0.9f, 0.75f, 0.1f, 1.0f)
|
|
: ImVec4(0.85f, 0.2f, 0.2f, 1.0f);
|
|
const char* hapLabel = hapPct >= 0.667f ? "Happy" : hapPct >= 0.333f ? "Content" : "Unhappy";
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, hapColor);
|
|
ImGui::ProgressBar(hapPct, ImVec2(-1, 8), hapLabel);
|
|
ImGui::PopStyleColor();
|
|
}
|
|
}
|
|
|
|
// Pet cast bar
|
|
if (auto* pcs = gameHandler.getUnitCastState(petGuid)) {
|
|
float castPct = (pcs->timeTotal > 0.0f)
|
|
? (pcs->timeTotal - pcs->timeRemaining) / pcs->timeTotal : 0.0f;
|
|
// Orange color to distinguish from health/power bars
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.85f, 0.55f, 0.1f, 1.0f));
|
|
char petCastLabel[48];
|
|
const std::string& spellNm = gameHandler.getSpellName(pcs->spellId);
|
|
if (!spellNm.empty())
|
|
snprintf(petCastLabel, sizeof(petCastLabel), "%s (%.1fs)", spellNm.c_str(), pcs->timeRemaining);
|
|
else
|
|
snprintf(petCastLabel, sizeof(petCastLabel), "Casting... (%.1fs)", pcs->timeRemaining);
|
|
ImGui::ProgressBar(castPct, ImVec2(-1, 10), petCastLabel);
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
// Stance row: Passive / Defensive / Aggressive — with Dismiss right-aligned
|
|
{
|
|
static constexpr const char* kReactLabels[] = { "Psv", "Def", "Agg" };
|
|
static constexpr const char* kReactTooltips[] = { "Passive", "Defensive", "Aggressive" };
|
|
static constexpr ImVec4 kReactColors[] = {
|
|
colors::kLightBlue, // passive — blue
|
|
ImVec4(0.3f, 0.85f, 0.3f, 1.0f), // defensive — green
|
|
colors::kHostileRed,// aggressive — red
|
|
};
|
|
static constexpr ImVec4 kReactDimColors[] = {
|
|
ImVec4(0.15f, 0.2f, 0.4f, 0.8f),
|
|
ImVec4(0.1f, 0.3f, 0.1f, 0.8f),
|
|
ImVec4(0.4f, 0.1f, 0.1f, 0.8f),
|
|
};
|
|
uint8_t curReact = gameHandler.getPetReact(); // 0=passive,1=defensive,2=aggressive
|
|
|
|
// Find each react-type slot in the action bar by known built-in IDs:
|
|
// 1=Passive, 4=Defensive, 6=Aggressive (WoW wire protocol)
|
|
static constexpr uint32_t kReactActionIds[] = { 1u, 4u, 6u };
|
|
uint32_t reactSlotVals[3] = { 0, 0, 0 };
|
|
const int slotTotal = game::GameHandler::PET_ACTION_BAR_SLOTS;
|
|
for (int i = 0; i < slotTotal; ++i) {
|
|
uint32_t sv = gameHandler.getPetActionSlot(i);
|
|
uint32_t aid = sv & 0x00FFFFFFu;
|
|
for (int r = 0; r < 3; ++r) {
|
|
if (aid == kReactActionIds[r]) { reactSlotVals[r] = sv; break; }
|
|
}
|
|
}
|
|
|
|
for (int r = 0; r < 3; ++r) {
|
|
if (r > 0) ImGui::SameLine(0.0f, 3.0f);
|
|
bool active = (curReact == static_cast<uint8_t>(r));
|
|
ImVec4 btnCol = active ? kReactColors[r] : kReactDimColors[r];
|
|
ImGui::PushID(r + 1000);
|
|
ImGui::PushStyleColor(ImGuiCol_Button, btnCol);
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, kReactColors[r]);
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive, kReactColors[r]);
|
|
if (ImGui::Button(kReactLabels[r], ImVec2(34.0f, 16.0f))) {
|
|
// Use server-provided slot value if available; fall back to raw ID
|
|
uint32_t action = (reactSlotVals[r] != 0)
|
|
? reactSlotVals[r]
|
|
: kReactActionIds[r];
|
|
gameHandler.sendPetAction(action, 0);
|
|
}
|
|
ImGui::PopStyleColor(3);
|
|
if (ImGui::IsItemHovered())
|
|
ImGui::SetTooltip("%s", kReactTooltips[r]);
|
|
ImGui::PopID();
|
|
}
|
|
|
|
// Dismiss button right-aligned on the same row
|
|
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 58.0f);
|
|
if (ImGui::SmallButton("Dismiss")) {
|
|
gameHandler.dismissPet();
|
|
}
|
|
}
|
|
|
|
// Pet action bar — show up to 10 action slots from SMSG_PET_SPELLS
|
|
{
|
|
const int slotCount = game::GameHandler::PET_ACTION_BAR_SLOTS;
|
|
// Filter to non-zero slots; lay them out as small icon/text buttons.
|
|
// Raw slot value layout (WotLK 3.3.5): low 24 bits = spell/action ID,
|
|
// high byte = flag (0x80=autocast on, 0x40=can-autocast, 0x0C=type).
|
|
// Built-in commands: id=2 follow, id=3 stay/move, id=5 attack.
|
|
auto* assetMgr = services_.assetManager;
|
|
const float iconSz = 20.0f;
|
|
const float spacing = 2.0f;
|
|
ImGui::Separator();
|
|
|
|
int rendered = 0;
|
|
for (int i = 0; i < slotCount; ++i) {
|
|
uint32_t slotVal = gameHandler.getPetActionSlot(i);
|
|
if (slotVal == 0) continue;
|
|
|
|
uint32_t actionId = slotVal & 0x00FFFFFFu;
|
|
// Use the authoritative autocast set from SMSG_PET_SPELLS spell list flags.
|
|
bool autocastOn = gameHandler.isPetSpellAutocast(actionId);
|
|
|
|
// Cooldown tracking for pet spells (actionId > 6 are spell IDs)
|
|
float petCd = (actionId > 6) ? gameHandler.getSpellCooldown(actionId) : 0.0f;
|
|
bool petOnCd = (petCd > 0.0f);
|
|
|
|
ImGui::PushID(i);
|
|
if (rendered > 0) ImGui::SameLine(0.0f, spacing);
|
|
|
|
// Try to show spell icon; fall back to abbreviated text label.
|
|
VkDescriptorSet iconTex = VK_NULL_HANDLE;
|
|
const char* builtinLabel = nullptr;
|
|
if (actionId == 1) builtinLabel = "Psv";
|
|
else if (actionId == 2) builtinLabel = "Fol";
|
|
else if (actionId == 3) builtinLabel = "Sty";
|
|
else if (actionId == 4) builtinLabel = "Def";
|
|
else if (actionId == 5) builtinLabel = "Atk";
|
|
else if (actionId == 6) builtinLabel = "Agg";
|
|
else if (assetMgr) iconTex = getSpellIcon(actionId, assetMgr);
|
|
|
|
// Dim when on cooldown; tint green when autocast is on
|
|
ImVec4 tint = petOnCd
|
|
? ImVec4(0.35f, 0.35f, 0.35f, 0.7f)
|
|
: (autocastOn ? colors::kLightGreen : ui::colors::kWhite);
|
|
bool clicked = false;
|
|
if (iconTex) {
|
|
clicked = ImGui::ImageButton("##pa",
|
|
(ImTextureID)(uintptr_t)iconTex,
|
|
ImVec2(iconSz, iconSz),
|
|
ImVec2(0,0), ImVec2(1,1),
|
|
ImVec4(0.1f,0.1f,0.1f,0.9f), tint);
|
|
} else {
|
|
char label[8];
|
|
if (builtinLabel) {
|
|
snprintf(label, sizeof(label), "%s", builtinLabel);
|
|
} else {
|
|
// Show first 3 chars of spell name or spell ID.
|
|
std::string nm = gameHandler.getSpellName(actionId);
|
|
if (nm.empty()) snprintf(label, sizeof(label), "?%u", actionId % 100);
|
|
else snprintf(label, sizeof(label), "%.3s", nm.c_str());
|
|
}
|
|
ImVec4 btnCol = petOnCd ? ImVec4(0.1f,0.1f,0.15f,0.9f)
|
|
: (autocastOn ? ImVec4(0.2f,0.5f,0.2f,0.9f)
|
|
: ImVec4(0.2f,0.2f,0.3f,0.9f));
|
|
ImGui::PushStyleColor(ImGuiCol_Button, btnCol);
|
|
clicked = ImGui::Button(label, ImVec2(iconSz + 4.0f, iconSz));
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
// Cooldown overlay: dark fill + time text centered on the button
|
|
if (petOnCd && !builtinLabel) {
|
|
ImVec2 bMin = ImGui::GetItemRectMin();
|
|
ImVec2 bMax = ImGui::GetItemRectMax();
|
|
auto* cdDL = ImGui::GetWindowDrawList();
|
|
cdDL->AddRectFilled(bMin, bMax, IM_COL32(0, 0, 0, 140));
|
|
char cdTxt[8];
|
|
if (petCd >= 60.0f)
|
|
snprintf(cdTxt, sizeof(cdTxt), "%dm", static_cast<int>(petCd / 60.0f));
|
|
else if (petCd >= 1.0f)
|
|
snprintf(cdTxt, sizeof(cdTxt), "%d", static_cast<int>(petCd));
|
|
else
|
|
snprintf(cdTxt, sizeof(cdTxt), "%.1f", petCd);
|
|
ImVec2 tsz = ImGui::CalcTextSize(cdTxt);
|
|
float cx = (bMin.x + bMax.x) * 0.5f;
|
|
float cy = (bMin.y + bMax.y) * 0.5f;
|
|
cdDL->AddText(ImVec2(cx - tsz.x * 0.5f, cy - tsz.y * 0.5f),
|
|
IM_COL32(255, 255, 255, 230), cdTxt);
|
|
}
|
|
|
|
if (clicked && !petOnCd) {
|
|
// Send pet action; use current target for spells.
|
|
uint64_t targetGuid = (actionId > 5) ? gameHandler.getTargetGuid() : 0u;
|
|
gameHandler.sendPetAction(slotVal, targetGuid);
|
|
}
|
|
// Right-click toggles autocast for castable pet spells (actionId > 6)
|
|
if (actionId > 6 && ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
|
gameHandler.togglePetSpellAutocast(actionId);
|
|
}
|
|
|
|
// Tooltip: rich spell info for pet spells, simple label for built-in commands
|
|
if (ImGui::IsItemHovered()) {
|
|
if (builtinLabel) {
|
|
const char* tip = nullptr;
|
|
if (actionId == 1) tip = "Passive";
|
|
else if (actionId == 2) tip = "Follow";
|
|
else if (actionId == 3) tip = "Stay";
|
|
else if (actionId == 4) tip = "Defensive";
|
|
else if (actionId == 5) tip = "Attack";
|
|
else if (actionId == 6) tip = "Aggressive";
|
|
if (tip) ImGui::SetTooltip("%s", tip);
|
|
} else if (actionId > 6) {
|
|
auto* spellAsset = services_.assetManager;
|
|
ImGui::BeginTooltip();
|
|
bool richOk = spellbookScreen.renderSpellInfoTooltip(actionId, gameHandler, spellAsset);
|
|
if (!richOk) {
|
|
std::string nm = gameHandler.getSpellName(actionId);
|
|
if (nm.empty()) nm = "Spell #" + std::to_string(actionId);
|
|
ImGui::Text("%s", nm.c_str());
|
|
}
|
|
ImGui::TextColored(autocastOn
|
|
? kColorGreen
|
|
: kColorGray,
|
|
"Autocast: %s (right-click to toggle)", autocastOn ? "On" : "Off");
|
|
if (petOnCd) {
|
|
if (petCd >= 60.0f)
|
|
ImGui::TextColored(kColorRed,
|
|
"Cooldown: %d min %d sec",
|
|
static_cast<int>(petCd) / 60, static_cast<int>(petCd) % 60);
|
|
else
|
|
ImGui::TextColored(kColorRed,
|
|
"Cooldown: %.1f sec", petCd);
|
|
}
|
|
ImGui::EndTooltip();
|
|
}
|
|
}
|
|
|
|
ImGui::PopID();
|
|
++rendered;
|
|
}
|
|
}
|
|
}
|
|
ImGui::End();
|
|
|
|
ImGui::PopStyleColor(2);
|
|
ImGui::PopStyleVar();
|
|
}
|
|
|
|
// ============================================================
|
|
// Totem Frame (Shaman — below pet frame / player frame)
|
|
// ============================================================
|
|
|
|
void GameScreen::renderTotemFrame(game::GameHandler& gameHandler) {
|
|
// Only show if at least one totem is active
|
|
bool anyActive = false;
|
|
for (int i = 0; i < game::GameHandler::NUM_TOTEM_SLOTS; ++i) {
|
|
if (gameHandler.getTotemSlot(i).active()) { anyActive = true; break; }
|
|
}
|
|
if (!anyActive) return;
|
|
|
|
static constexpr struct { const char* name; ImU32 color; } kTotemInfo[4] = {
|
|
{ "Earth", IM_COL32(139, 90, 43, 255) }, // brown
|
|
{ "Fire", IM_COL32(220, 80, 30, 255) }, // red-orange
|
|
{ "Water", IM_COL32( 30,120, 220, 255) }, // blue
|
|
{ "Air", IM_COL32(180,220, 255, 255) }, // light blue
|
|
};
|
|
|
|
// Position: below pet frame / player frame, left side
|
|
// Pet frame is at ~y=200 if active, player frame is at y=20; totem frame near y=300
|
|
// We anchor relative to screen left edge like pet frame
|
|
ImGui::SetNextWindowPos(ImVec2(8.0f, 300.0f), ImGuiCond_FirstUseEver);
|
|
ImGui::SetNextWindowSize(ImVec2(130.0f, 0.0f), ImGuiCond_Always);
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize |
|
|
ImGuiWindowFlags_NoTitleBar;
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.08f, 0.06f, 0.88f));
|
|
|
|
if (ImGui::Begin("##TotemFrame", nullptr, flags)) {
|
|
ImGui::TextColored(ImVec4(0.9f, 0.75f, 0.3f, 1.0f), "Totems");
|
|
ImGui::Separator();
|
|
|
|
for (int i = 0; i < game::GameHandler::NUM_TOTEM_SLOTS; ++i) {
|
|
const auto& slot = gameHandler.getTotemSlot(i);
|
|
if (!slot.active()) continue;
|
|
|
|
ImGui::PushID(i);
|
|
|
|
// Colored element dot
|
|
ImVec2 dotPos = ImGui::GetCursorScreenPos();
|
|
dotPos.x += 4.0f; dotPos.y += 6.0f;
|
|
ImGui::GetWindowDrawList()->AddCircleFilled(
|
|
ImVec2(dotPos.x + 4.0f, dotPos.y + 4.0f), 4.0f, kTotemInfo[i].color);
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 14.0f);
|
|
|
|
// Totem name or spell name
|
|
const std::string& spellName = gameHandler.getSpellName(slot.spellId);
|
|
const char* displayName = spellName.empty() ? kTotemInfo[i].name : spellName.c_str();
|
|
ImGui::Text("%s", displayName);
|
|
|
|
// Duration countdown bar
|
|
float remMs = slot.remainingMs();
|
|
float totMs = static_cast<float>(slot.durationMs);
|
|
float frac = (totMs > 0.0f) ? std::min(remMs / totMs, 1.0f) : 0.0f;
|
|
float remSec = remMs / 1000.0f;
|
|
|
|
// Color bar with totem element tint
|
|
ImVec4 barCol(
|
|
static_cast<float>((kTotemInfo[i].color >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f,
|
|
static_cast<float>((kTotemInfo[i].color >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f,
|
|
static_cast<float>((kTotemInfo[i].color >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f,
|
|
0.9f);
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barCol);
|
|
char timeBuf[16];
|
|
snprintf(timeBuf, sizeof(timeBuf), "%.0fs", remSec);
|
|
ImGui::ProgressBar(frac, ImVec2(-1, 8), timeBuf);
|
|
ImGui::PopStyleColor();
|
|
|
|
ImGui::PopID();
|
|
}
|
|
}
|
|
ImGui::End();
|
|
|
|
ImGui::PopStyleColor();
|
|
ImGui::PopStyleVar();
|
|
}
|
|
|
|
void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) {
|
|
auto target = gameHandler.getTarget();
|
|
if (!target) return;
|
|
|
|
auto* window = services_.window;
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
float frameW = 250.0f;
|
|
float frameX = (screenW - frameW) / 2.0f;
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(frameX, 30.0f), ImGuiCond_Always);
|
|
ImGui::SetNextWindowSize(ImVec2(frameW, 0.0f), ImGuiCond_Always);
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
|
|
|
// Determine hostility/level color for border and name (WoW-canonical)
|
|
ImVec4 hostileColor(0.7f, 0.7f, 0.7f, 1.0f);
|
|
if (target->getType() == game::ObjectType::PLAYER) {
|
|
hostileColor = kColorBrightGreen;
|
|
} else if (target->getType() == game::ObjectType::UNIT) {
|
|
auto u = std::static_pointer_cast<game::Unit>(target);
|
|
if (u->getHealth() == 0 && u->getMaxHealth() > 0) {
|
|
hostileColor = kColorDarkGray;
|
|
} else if (u->isHostile()) {
|
|
// Check tapped-by-other: grey name for mobs tagged by someone else
|
|
uint32_t tgtDynFlags = u->getDynamicFlags();
|
|
bool tgtTapped = (tgtDynFlags & 0x0004) != 0 && (tgtDynFlags & 0x0008) == 0;
|
|
if (tgtTapped) {
|
|
hostileColor = kColorGray; // Grey — tapped by other
|
|
} else {
|
|
// WoW level-based color for hostile mobs
|
|
uint32_t playerLv = gameHandler.getPlayerLevel();
|
|
uint32_t mobLv = u->getLevel();
|
|
if (mobLv == 0) {
|
|
// Level 0 = unknown/?? (e.g. high-level raid bosses) — always skull red
|
|
hostileColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f);
|
|
} else {
|
|
int32_t diff = static_cast<int32_t>(mobLv) - static_cast<int32_t>(playerLv);
|
|
if (game::GameHandler::killXp(playerLv, mobLv) == 0) {
|
|
hostileColor = kColorGray; // Grey - no XP
|
|
} else if (diff >= 10) {
|
|
hostileColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); // Red - skull/very hard
|
|
} else if (diff >= 5) {
|
|
hostileColor = ImVec4(1.0f, 0.5f, 0.1f, 1.0f); // Orange - hard
|
|
} else if (diff >= -2) {
|
|
hostileColor = ImVec4(1.0f, 1.0f, 0.1f, 1.0f); // Yellow - even
|
|
} else {
|
|
hostileColor = kColorBrightGreen; // Green - easy
|
|
}
|
|
}
|
|
} // end tapped else
|
|
} else {
|
|
hostileColor = kColorBrightGreen; // Friendly
|
|
}
|
|
}
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.85f));
|
|
const uint64_t targetGuid = target->getGuid();
|
|
const bool confirmedCombatWithTarget = gameHandler.isInCombatWith(targetGuid);
|
|
const bool intentTowardTarget =
|
|
gameHandler.hasAutoAttackIntent() &&
|
|
gameHandler.getAutoAttackTargetGuid() == targetGuid &&
|
|
!confirmedCombatWithTarget;
|
|
ImVec4 borderColor = ImVec4(hostileColor.x * 0.8f, hostileColor.y * 0.8f, hostileColor.z * 0.8f, 1.0f);
|
|
if (confirmedCombatWithTarget) {
|
|
float t = ImGui::GetTime();
|
|
float pulse = (std::fmod(t, 0.6f) < 0.3f) ? 1.0f : 0.0f;
|
|
borderColor = ImVec4(1.0f, 0.1f, 0.1f, pulse);
|
|
} else if (intentTowardTarget) {
|
|
borderColor = ImVec4(1.0f, 0.7f, 0.2f, 1.0f);
|
|
}
|
|
ImGui::PushStyleColor(ImGuiCol_Border, borderColor);
|
|
|
|
if (ImGui::Begin("##TargetFrame", nullptr, flags)) {
|
|
// Raid mark icon (Star/Circle/Diamond/Triangle/Moon/Square/Cross/Skull)
|
|
static constexpr struct { const char* sym; ImU32 col; } kRaidMarks[] = {
|
|
{ "\xe2\x98\x85", IM_COL32(255, 220, 50, 255) }, // 0 Star (yellow)
|
|
{ "\xe2\x97\x8f", IM_COL32(255, 140, 0, 255) }, // 1 Circle (orange)
|
|
{ "\xe2\x97\x86", IM_COL32(160, 32, 240, 255) }, // 2 Diamond (purple)
|
|
{ "\xe2\x96\xb2", IM_COL32( 50, 200, 50, 255) }, // 3 Triangle (green)
|
|
{ "\xe2\x97\x8c", IM_COL32( 80, 160, 255, 255) }, // 4 Moon (blue)
|
|
{ "\xe2\x96\xa0", IM_COL32( 50, 200, 220, 255) }, // 5 Square (teal)
|
|
{ "\xe2\x9c\x9d", IM_COL32(255, 80, 80, 255) }, // 6 Cross (red)
|
|
{ "\xe2\x98\xa0", IM_COL32(255, 255, 255, 255) }, // 7 Skull (white)
|
|
};
|
|
uint8_t mark = gameHandler.getEntityRaidMark(target->getGuid());
|
|
if (mark < game::GameHandler::kRaidMarkCount) {
|
|
ImGui::GetWindowDrawList()->AddText(
|
|
ImGui::GetCursorScreenPos(),
|
|
kRaidMarks[mark].col, kRaidMarks[mark].sym);
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 18.0f);
|
|
}
|
|
|
|
// Entity name and type — Selectable so we can attach a right-click context menu
|
|
std::string name = getEntityName(target);
|
|
|
|
// Player targets: use class color instead of the generic green
|
|
ImVec4 nameColor = hostileColor;
|
|
if (target->getType() == game::ObjectType::PLAYER) {
|
|
uint8_t cid = entityClassId(target.get());
|
|
if (cid != 0) nameColor = classColorVec4(cid);
|
|
}
|
|
|
|
ImGui::SameLine(0.0f, 0.0f);
|
|
ImGui::PushStyleColor(ImGuiCol_Text, nameColor);
|
|
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0,0,0,0));
|
|
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(1,1,1,0.08f));
|
|
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(1,1,1,0.12f));
|
|
ImGui::Selectable(name.c_str(), false, ImGuiSelectableFlags_DontClosePopups,
|
|
ImVec2(ImGui::CalcTextSize(name.c_str()).x, 0));
|
|
ImGui::PopStyleColor(4);
|
|
|
|
// Right-click context menu on target frame
|
|
if (ImGui::BeginPopupContextItem("##TargetFrameCtx")) {
|
|
const bool isPlayer = (target->getType() == game::ObjectType::PLAYER);
|
|
const uint64_t tGuid = target->getGuid();
|
|
|
|
ImGui::TextDisabled("%s", name.c_str());
|
|
ImGui::Separator();
|
|
|
|
if (ImGui::MenuItem("Set Focus"))
|
|
gameHandler.setFocus(tGuid);
|
|
if (ImGui::MenuItem("Clear Target"))
|
|
gameHandler.clearTarget();
|
|
if (isPlayer) {
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Whisper")) {
|
|
chatPanel_.setWhisperTarget(name);
|
|
}
|
|
if (ImGui::MenuItem("Follow"))
|
|
gameHandler.followTarget();
|
|
if (ImGui::MenuItem("Invite to Group"))
|
|
gameHandler.inviteToGroup(name);
|
|
if (ImGui::MenuItem("Trade"))
|
|
gameHandler.initiateTrade(tGuid);
|
|
if (ImGui::MenuItem("Duel"))
|
|
gameHandler.proposeDuel(tGuid);
|
|
if (ImGui::MenuItem("Inspect")) {
|
|
gameHandler.inspectTarget();
|
|
socialPanel_.showInspectWindow_ = true;
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Add Friend"))
|
|
gameHandler.addFriend(name);
|
|
if (ImGui::MenuItem("Ignore"))
|
|
gameHandler.addIgnore(name);
|
|
if (ImGui::MenuItem("Report Player"))
|
|
gameHandler.reportPlayer(tGuid, "Reported via UI");
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::BeginMenu("Set Raid Mark")) {
|
|
for (int mi = 0; mi < 8; ++mi) {
|
|
if (ImGui::MenuItem(kRaidMarkNames[mi]))
|
|
gameHandler.setRaidMark(tGuid, static_cast<uint8_t>(mi));
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Clear Mark"))
|
|
gameHandler.setRaidMark(tGuid, 0xFF);
|
|
ImGui::EndMenu();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
// Group leader crown — golden ♛ when the targeted player is the party/raid leader
|
|
if (gameHandler.isInGroup() && target->getType() == game::ObjectType::PLAYER) {
|
|
if (gameHandler.getPartyData().leaderGuid == target->getGuid()) {
|
|
ImGui::SameLine(0, 4);
|
|
ImGui::TextColored(colors::kSymbolGold, "\xe2\x99\x9b");
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Group Leader");
|
|
}
|
|
}
|
|
|
|
// Quest giver indicator — "!" for available quests, "?" for completable quests
|
|
{
|
|
using QGS = game::QuestGiverStatus;
|
|
QGS qgs = gameHandler.getQuestGiverStatus(target->getGuid());
|
|
if (qgs == QGS::AVAILABLE) {
|
|
ImGui::SameLine(0, 4);
|
|
ImGui::TextColored(colors::kBrightGold, "!");
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Has a quest available");
|
|
} else if (qgs == QGS::AVAILABLE_LOW) {
|
|
ImGui::SameLine(0, 4);
|
|
ImGui::TextColored(kColorGray, "!");
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Has a low-level quest available");
|
|
} else if (qgs == QGS::REWARD || qgs == QGS::REWARD_REP) {
|
|
ImGui::SameLine(0, 4);
|
|
ImGui::TextColored(colors::kBrightGold, "?");
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Quest ready to turn in");
|
|
} else if (qgs == QGS::INCOMPLETE) {
|
|
ImGui::SameLine(0, 4);
|
|
ImGui::TextColored(kColorGray, "?");
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Quest incomplete");
|
|
}
|
|
}
|
|
|
|
// Creature subtitle (e.g. "<Warchief of the Horde>", "Captain of the Guard")
|
|
if (target->getType() == game::ObjectType::UNIT) {
|
|
auto unit = std::static_pointer_cast<game::Unit>(target);
|
|
const std::string sub = gameHandler.getCachedCreatureSubName(unit->getEntry());
|
|
if (!sub.empty()) {
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 0.9f), "<%s>", sub.c_str());
|
|
}
|
|
}
|
|
|
|
// Player guild name (e.g. "<My Guild>") — mirrors NPC subtitle styling
|
|
if (target->getType() == game::ObjectType::PLAYER) {
|
|
uint32_t guildId = gameHandler.getEntityGuildId(target->getGuid());
|
|
if (guildId != 0) {
|
|
const std::string& gn = gameHandler.lookupGuildName(guildId);
|
|
if (!gn.empty()) {
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 0.9f), "<%s>", gn.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
// Right-click context menu on the target name
|
|
if (ImGui::BeginPopupContextItem("##TargetNameCtx")) {
|
|
const bool isPlayer = (target->getType() == game::ObjectType::PLAYER);
|
|
const uint64_t tGuid = target->getGuid();
|
|
|
|
ImGui::TextDisabled("%s", name.c_str());
|
|
ImGui::Separator();
|
|
|
|
if (ImGui::MenuItem("Set Focus")) {
|
|
gameHandler.setFocus(tGuid);
|
|
}
|
|
if (ImGui::MenuItem("Clear Target")) {
|
|
gameHandler.clearTarget();
|
|
}
|
|
if (isPlayer) {
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Whisper")) {
|
|
chatPanel_.setWhisperTarget(name);
|
|
}
|
|
if (ImGui::MenuItem("Follow")) {
|
|
gameHandler.followTarget();
|
|
}
|
|
if (ImGui::MenuItem("Invite to Group")) {
|
|
gameHandler.inviteToGroup(name);
|
|
}
|
|
if (ImGui::MenuItem("Trade")) {
|
|
gameHandler.initiateTrade(tGuid);
|
|
}
|
|
if (ImGui::MenuItem("Duel")) {
|
|
gameHandler.proposeDuel(tGuid);
|
|
}
|
|
if (ImGui::MenuItem("Inspect")) {
|
|
gameHandler.inspectTarget();
|
|
socialPanel_.showInspectWindow_ = true;
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Add Friend")) {
|
|
gameHandler.addFriend(name);
|
|
}
|
|
if (ImGui::MenuItem("Ignore")) {
|
|
gameHandler.addIgnore(name);
|
|
}
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::BeginMenu("Set Raid Mark")) {
|
|
for (int mi = 0; mi < 8; ++mi) {
|
|
if (ImGui::MenuItem(kRaidMarkNames[mi]))
|
|
gameHandler.setRaidMark(tGuid, static_cast<uint8_t>(mi));
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Clear Mark"))
|
|
gameHandler.setRaidMark(tGuid, 0xFF);
|
|
ImGui::EndMenu();
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
// Level (for units/players) — colored by difficulty
|
|
if (target->getType() == game::ObjectType::UNIT || target->getType() == game::ObjectType::PLAYER) {
|
|
auto unit = std::static_pointer_cast<game::Unit>(target);
|
|
ImGui::SameLine();
|
|
// Level color matches the hostility/difficulty color
|
|
ImVec4 levelColor = hostileColor;
|
|
if (target->getType() == game::ObjectType::PLAYER) {
|
|
levelColor = ui::colors::kLightGray;
|
|
}
|
|
if (unit->getLevel() == 0)
|
|
ImGui::TextColored(levelColor, "Lv ??");
|
|
else
|
|
ImGui::TextColored(levelColor, "Lv %u", unit->getLevel());
|
|
// Classification badge: Elite / Rare Elite / Boss / Rare
|
|
if (target->getType() == game::ObjectType::UNIT) {
|
|
int rank = gameHandler.getCreatureRank(unit->getEntry());
|
|
if (rank == 1) {
|
|
ImGui::SameLine(0, 4);
|
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "[Elite]");
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Elite — requires a group");
|
|
} else if (rank == 2) {
|
|
ImGui::SameLine(0, 4);
|
|
ImGui::TextColored(ImVec4(0.8f, 0.4f, 1.0f, 1.0f), "[Rare Elite]");
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Rare Elite — uncommon spawn, group recommended");
|
|
} else if (rank == 3) {
|
|
ImGui::SameLine(0, 4);
|
|
ImGui::TextColored(kColorRed, "[Boss]");
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Boss — raid / dungeon boss");
|
|
} else if (rank == 4) {
|
|
ImGui::SameLine(0, 4);
|
|
ImGui::TextColored(ImVec4(0.5f, 0.9f, 1.0f, 1.0f), "[Rare]");
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Rare — uncommon spawn with better loot");
|
|
}
|
|
}
|
|
// Creature type label (Beast, Humanoid, Demon, etc.)
|
|
if (target->getType() == game::ObjectType::UNIT) {
|
|
uint32_t ctype = gameHandler.getCreatureType(unit->getEntry());
|
|
const char* ctypeName = nullptr;
|
|
switch (ctype) {
|
|
case 1: ctypeName = "Beast"; break;
|
|
case 2: ctypeName = "Dragonkin"; break;
|
|
case 3: ctypeName = "Demon"; break;
|
|
case 4: ctypeName = "Elemental"; break;
|
|
case 5: ctypeName = "Giant"; break;
|
|
case 6: ctypeName = "Undead"; break;
|
|
case 7: ctypeName = "Humanoid"; break;
|
|
case 8: ctypeName = "Critter"; break;
|
|
case 9: ctypeName = "Mechanical"; break;
|
|
case 11: ctypeName = "Totem"; break;
|
|
case 12: ctypeName = "Non-combat Pet"; break;
|
|
case 13: ctypeName = "Gas Cloud"; break;
|
|
default: break;
|
|
}
|
|
if (ctypeName) {
|
|
ImGui::SameLine(0, 4);
|
|
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 0.9f), "(%s)", ctypeName);
|
|
}
|
|
}
|
|
if (confirmedCombatWithTarget) {
|
|
float cPulse = 0.75f + 0.25f * std::sin(static_cast<float>(ImGui::GetTime()) * 4.0f);
|
|
ImGui::SameLine();
|
|
ImGui::TextColored(ImVec4(1.0f, 0.2f * cPulse, 0.2f * cPulse, 1.0f), "[Attacking]");
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Engaged in combat with this target");
|
|
}
|
|
|
|
// Health bar
|
|
uint32_t hp = unit->getHealth();
|
|
uint32_t maxHp = unit->getMaxHealth();
|
|
if (maxHp > 0) {
|
|
float pct = static_cast<float>(hp) / static_cast<float>(maxHp);
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram,
|
|
pct > 0.5f ? colors::kHealthGreen :
|
|
pct > 0.2f ? colors::kMidHealthYellow :
|
|
colors::kLowHealthRed);
|
|
|
|
char overlay[64];
|
|
snprintf(overlay, sizeof(overlay), "%u / %u", hp, maxHp);
|
|
ImGui::ProgressBar(pct, ImVec2(-1, 18), overlay);
|
|
ImGui::PopStyleColor();
|
|
// Target power bar (mana/rage/energy)
|
|
uint8_t targetPowerType = unit->getPowerType();
|
|
uint32_t targetPower = unit->getPower();
|
|
uint32_t targetMaxPower = unit->getMaxPower();
|
|
if (targetMaxPower == 0 && (targetPowerType == 1 || targetPowerType == 3)) targetMaxPower = 100;
|
|
if (targetMaxPower > 0) {
|
|
float mpPct = static_cast<float>(targetPower) / static_cast<float>(targetMaxPower);
|
|
ImVec4 targetPowerColor;
|
|
switch (targetPowerType) {
|
|
case 0: targetPowerColor = colors::kManaBlue; break; // Mana (blue)
|
|
case 1: targetPowerColor = colors::kDarkRed; break; // Rage (red)
|
|
case 2: targetPowerColor = colors::kOrange; break; // Focus (orange)
|
|
case 3: targetPowerColor = colors::kEnergyYellow; break; // Energy (yellow)
|
|
case 4: targetPowerColor = colors::kHappinessGreen; break; // Happiness (green)
|
|
case 6: targetPowerColor = colors::kRunicRed; break; // Runic Power (crimson)
|
|
case 7: targetPowerColor = colors::kSoulShardPurple; break; // Soul Shards (purple)
|
|
default: targetPowerColor = colors::kManaBlue; break;
|
|
}
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, targetPowerColor);
|
|
char mpOverlay[64];
|
|
snprintf(mpOverlay, sizeof(mpOverlay), "%u / %u", targetPower, targetMaxPower);
|
|
ImGui::ProgressBar(mpPct, ImVec2(-1, 18), mpOverlay);
|
|
ImGui::PopStyleColor();
|
|
}
|
|
} else {
|
|
ImGui::TextDisabled("No health data");
|
|
}
|
|
}
|
|
|
|
// Combo points — shown when the player has combo points on this target
|
|
{
|
|
uint8_t cp = gameHandler.getComboPoints();
|
|
if (cp > 0 && gameHandler.getComboTarget() == target->getGuid()) {
|
|
const float dotSize = 12.0f;
|
|
const float dotSpacing = 4.0f;
|
|
const int maxCP = 5;
|
|
float totalW = maxCP * dotSize + (maxCP - 1) * dotSpacing;
|
|
float startX = (frameW - totalW) * 0.5f;
|
|
ImGui::SetCursorPosX(startX);
|
|
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
for (int ci = 0; ci < maxCP; ++ci) {
|
|
float cx = cursor.x + ci * (dotSize + dotSpacing) + dotSize * 0.5f;
|
|
float cy = cursor.y + dotSize * 0.5f;
|
|
if (ci < static_cast<int>(cp)) {
|
|
// Lit: yellow for 1-4, red glow for 5
|
|
ImU32 col = (cp >= 5)
|
|
? IM_COL32(255, 50, 30, 255)
|
|
: IM_COL32(255, 210, 30, 255);
|
|
dl->AddCircleFilled(ImVec2(cx, cy), dotSize * 0.45f, col);
|
|
// Subtle glow
|
|
dl->AddCircle(ImVec2(cx, cy), dotSize * 0.5f, IM_COL32(255, 255, 200, 80), 0, 1.5f);
|
|
} else {
|
|
// Unlit: dark outline
|
|
dl->AddCircle(ImVec2(cx, cy), dotSize * 0.4f, IM_COL32(80, 80, 80, 180), 0, 1.5f);
|
|
}
|
|
}
|
|
ImGui::Dummy(ImVec2(totalW, dotSize + 2.0f));
|
|
}
|
|
}
|
|
|
|
// Target cast bar — shown when the target is casting
|
|
if (gameHandler.isTargetCasting()) {
|
|
float castPct = gameHandler.getTargetCastProgress();
|
|
float castLeft = gameHandler.getTargetCastTimeRemaining();
|
|
uint32_t tspell = gameHandler.getTargetCastSpellId();
|
|
bool interruptible = gameHandler.isTargetCastInterruptible();
|
|
const std::string& castName = (tspell != 0) ? gameHandler.getSpellName(tspell) : "";
|
|
// Color: interruptible = green (can Kick/CS), not interruptible = red, both pulse when >80%
|
|
ImVec4 castBarColor;
|
|
if (castPct > 0.8f) {
|
|
float pulse = 0.7f + 0.3f * std::sin(static_cast<float>(ImGui::GetTime()) * 8.0f);
|
|
if (interruptible)
|
|
castBarColor = ImVec4(0.2f * pulse, 0.9f * pulse, 0.2f * pulse, 1.0f); // green pulse
|
|
else
|
|
castBarColor = ImVec4(1.0f * pulse, 0.1f * pulse, 0.1f * pulse, 1.0f); // red pulse
|
|
} else {
|
|
castBarColor = interruptible ? colors::kCastGreen // green = can interrupt
|
|
: ImVec4(0.85f, 0.15f, 0.15f, 1.0f); // red = uninterruptible
|
|
}
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, castBarColor);
|
|
char castLabel[72];
|
|
if (!castName.empty())
|
|
snprintf(castLabel, sizeof(castLabel), "%s (%.1fs)", castName.c_str(), castLeft);
|
|
else if (tspell != 0)
|
|
snprintf(castLabel, sizeof(castLabel), "Spell #%u (%.1fs)", tspell, castLeft);
|
|
else
|
|
snprintf(castLabel, sizeof(castLabel), "Casting... (%.1fs)", castLeft);
|
|
{
|
|
auto* tcastAsset = services_.assetManager;
|
|
VkDescriptorSet tIcon = (tspell != 0 && tcastAsset)
|
|
? getSpellIcon(tspell, tcastAsset) : VK_NULL_HANDLE;
|
|
if (tIcon) {
|
|
ImGui::Image((ImTextureID)(uintptr_t)tIcon, ImVec2(14, 14));
|
|
ImGui::SameLine(0, 2);
|
|
ImGui::ProgressBar(castPct, ImVec2(-1, 14), castLabel);
|
|
} else {
|
|
ImGui::ProgressBar(castPct, ImVec2(-1, 14), castLabel);
|
|
}
|
|
}
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
// Target-of-Target (ToT): show who the current target is targeting
|
|
{
|
|
uint64_t totGuid = 0;
|
|
const auto& tFields = target->getFields();
|
|
auto itLo = tFields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_LO));
|
|
if (itLo != tFields.end()) {
|
|
totGuid = itLo->second;
|
|
auto itHi = tFields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_HI));
|
|
if (itHi != tFields.end())
|
|
totGuid |= (static_cast<uint64_t>(itHi->second) << 32);
|
|
}
|
|
if (totGuid != 0) {
|
|
auto totEnt = gameHandler.getEntityManager().getEntity(totGuid);
|
|
std::string totName;
|
|
ImVec4 totColor(0.7f, 0.7f, 0.7f, 1.0f);
|
|
if (totGuid == gameHandler.getPlayerGuid()) {
|
|
auto playerEnt = gameHandler.getEntityManager().getEntity(totGuid);
|
|
totName = playerEnt ? getEntityName(playerEnt) : "You";
|
|
totColor = kColorBrightGreen;
|
|
} else if (totEnt) {
|
|
totName = getEntityName(totEnt);
|
|
uint8_t cid = entityClassId(totEnt.get());
|
|
if (cid != 0) totColor = classColorVec4(cid);
|
|
}
|
|
if (!totName.empty()) {
|
|
ImGui::TextDisabled("▶");
|
|
ImGui::SameLine(0, 2);
|
|
ImGui::TextColored(totColor, "%s", totName.c_str());
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::SetTooltip("Target's target: %s\nClick to target", totName.c_str());
|
|
}
|
|
if (ImGui::IsItemClicked()) {
|
|
gameHandler.setTarget(totGuid);
|
|
}
|
|
|
|
// Compact health bar for the ToT — essential for healers tracking boss target
|
|
if (totEnt) {
|
|
auto totUnit = std::dynamic_pointer_cast<game::Unit>(totEnt);
|
|
if (totUnit && totUnit->getMaxHealth() > 0) {
|
|
uint32_t totHp = totUnit->getHealth();
|
|
uint32_t totMaxHp = totUnit->getMaxHealth();
|
|
float totPct = static_cast<float>(totHp) / static_cast<float>(totMaxHp);
|
|
ImVec4 totBarColor =
|
|
totPct > 0.5f ? colors::kCastGreen :
|
|
totPct > 0.2f ? ImVec4(0.75f, 0.75f, 0.2f, 1.0f) :
|
|
ImVec4(0.75f, 0.2f, 0.2f, 1.0f);
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, totBarColor);
|
|
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.15f, 0.15f, 0.15f, 0.8f));
|
|
char totOverlay[32];
|
|
snprintf(totOverlay, sizeof(totOverlay), "%u%%",
|
|
static_cast<unsigned>(totPct * 100.0f + 0.5f));
|
|
ImGui::ProgressBar(totPct, ImVec2(-1, 10), totOverlay);
|
|
ImGui::PopStyleColor(2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Distance
|
|
const auto& movement = gameHandler.getMovementInfo();
|
|
float dx = target->getX() - movement.x;
|
|
float dy = target->getY() - movement.y;
|
|
float dz = target->getZ() - movement.z;
|
|
float distance = std::sqrt(dx*dx + dy*dy + dz*dz);
|
|
ImGui::TextDisabled("%.1f yd", distance);
|
|
|
|
// Threat button (shown when in combat and threat data is available)
|
|
if (gameHandler.getTargetThreatList()) {
|
|
ImGui::SameLine();
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.1f, 0.1f, 0.8f));
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.2f, 0.2f, 0.9f));
|
|
if (ImGui::SmallButton("Threat")) combatUI_.showThreatWindow_ = !combatUI_.showThreatWindow_;
|
|
ImGui::PopStyleColor(2);
|
|
}
|
|
|
|
// Target auras (buffs/debuffs)
|
|
const auto& targetAuras = gameHandler.getTargetAuras();
|
|
int activeAuras = 0;
|
|
for (const auto& a : targetAuras) {
|
|
if (!a.isEmpty()) activeAuras++;
|
|
}
|
|
if (activeAuras > 0) {
|
|
auto* assetMgr = services_.assetManager;
|
|
constexpr float ICON_SIZE = 24.0f;
|
|
constexpr int ICONS_PER_ROW = 8;
|
|
|
|
ImGui::Separator();
|
|
|
|
// Build sorted index list: debuffs before buffs, shorter duration first
|
|
uint64_t tNowSort = static_cast<uint64_t>(
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now().time_since_epoch()).count());
|
|
std::vector<size_t> sortedIdx;
|
|
sortedIdx.reserve(targetAuras.size());
|
|
for (size_t i = 0; i < targetAuras.size(); ++i)
|
|
if (!targetAuras[i].isEmpty()) sortedIdx.push_back(i);
|
|
std::sort(sortedIdx.begin(), sortedIdx.end(), [&](size_t a, size_t b) {
|
|
const auto& aa = targetAuras[a]; const auto& ab = targetAuras[b];
|
|
bool aDebuff = (aa.flags & 0x80) != 0;
|
|
bool bDebuff = (ab.flags & 0x80) != 0;
|
|
if (aDebuff != bDebuff) return aDebuff > bDebuff; // debuffs first
|
|
int32_t ra = aa.getRemainingMs(tNowSort);
|
|
int32_t rb = ab.getRemainingMs(tNowSort);
|
|
// Permanent (-1) goes last; shorter remaining goes first
|
|
if (ra < 0 && rb < 0) return false;
|
|
if (ra < 0) return false;
|
|
if (rb < 0) return true;
|
|
return ra < rb;
|
|
});
|
|
|
|
int shown = 0;
|
|
for (size_t si = 0; si < sortedIdx.size() && shown < 16; ++si) {
|
|
size_t i = sortedIdx[si];
|
|
const auto& aura = targetAuras[i];
|
|
if (aura.isEmpty()) continue;
|
|
|
|
if (shown > 0 && shown % ICONS_PER_ROW != 0) ImGui::SameLine();
|
|
|
|
ImGui::PushID(static_cast<int>(10000 + i));
|
|
|
|
bool isBuff = (aura.flags & 0x80) == 0;
|
|
ImVec4 auraBorderColor;
|
|
if (isBuff) {
|
|
auraBorderColor = ImVec4(0.2f, 0.8f, 0.2f, 0.9f);
|
|
} else {
|
|
// Debuff: color by dispel type, matching player buff bar convention
|
|
uint8_t dt = gameHandler.getSpellDispelType(aura.spellId);
|
|
switch (dt) {
|
|
case 1: auraBorderColor = ImVec4(0.15f, 0.50f, 1.00f, 0.9f); break; // magic: blue
|
|
case 2: auraBorderColor = ImVec4(0.70f, 0.20f, 0.90f, 0.9f); break; // curse: purple
|
|
case 3: auraBorderColor = ImVec4(0.55f, 0.30f, 0.10f, 0.9f); break; // disease: brown
|
|
case 4: auraBorderColor = ImVec4(0.10f, 0.70f, 0.10f, 0.9f); break; // poison: green
|
|
default: auraBorderColor = ImVec4(0.80f, 0.20f, 0.20f, 0.9f); break; // other: red
|
|
}
|
|
}
|
|
|
|
VkDescriptorSet iconTex = VK_NULL_HANDLE;
|
|
if (assetMgr) {
|
|
iconTex = getSpellIcon(aura.spellId, assetMgr);
|
|
}
|
|
|
|
if (iconTex) {
|
|
ImGui::PushStyleColor(ImGuiCol_Button, auraBorderColor);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1, 1));
|
|
ImGui::ImageButton("##taura",
|
|
(ImTextureID)(uintptr_t)iconTex,
|
|
ImVec2(ICON_SIZE - 2, ICON_SIZE - 2));
|
|
ImGui::PopStyleVar();
|
|
ImGui::PopStyleColor();
|
|
} else {
|
|
ImGui::PushStyleColor(ImGuiCol_Button, auraBorderColor);
|
|
const std::string& tAuraName = gameHandler.getSpellName(aura.spellId);
|
|
char label[32];
|
|
if (!tAuraName.empty())
|
|
snprintf(label, sizeof(label), "%.6s", tAuraName.c_str());
|
|
else
|
|
snprintf(label, sizeof(label), "%u", aura.spellId);
|
|
ImGui::Button(label, ImVec2(ICON_SIZE, ICON_SIZE));
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
// Compute remaining once for overlay + tooltip
|
|
uint64_t tNowMs = static_cast<uint64_t>(
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now().time_since_epoch()).count());
|
|
int32_t tRemainMs = aura.getRemainingMs(tNowMs);
|
|
|
|
// Clock-sweep overlay (elapsed = dark area, WoW style)
|
|
if (tRemainMs > 0 && aura.maxDurationMs > 0) {
|
|
ImVec2 tIconMin = ImGui::GetItemRectMin();
|
|
ImVec2 tIconMax = ImGui::GetItemRectMax();
|
|
float tcx = (tIconMin.x + tIconMax.x) * 0.5f;
|
|
float tcy = (tIconMin.y + tIconMax.y) * 0.5f;
|
|
float tR = (tIconMax.x - tIconMin.x) * 0.5f;
|
|
float tTot = static_cast<float>(aura.maxDurationMs);
|
|
float tFrac = std::clamp(
|
|
1.0f - static_cast<float>(tRemainMs) / tTot, 0.0f, 1.0f);
|
|
if (tFrac > 0.005f) {
|
|
constexpr int TSEGS = 24;
|
|
float tSa = -IM_PI * 0.5f;
|
|
float tEa = tSa + tFrac * 2.0f * IM_PI;
|
|
ImVec2 tPts[TSEGS + 2];
|
|
tPts[0] = ImVec2(tcx, tcy);
|
|
for (int s = 0; s <= TSEGS; ++s) {
|
|
float a = tSa + (tEa - tSa) * s / static_cast<float>(TSEGS);
|
|
tPts[s + 1] = ImVec2(tcx + std::cos(a) * tR,
|
|
tcy + std::sin(a) * tR);
|
|
}
|
|
ImGui::GetWindowDrawList()->AddConvexPolyFilled(
|
|
tPts, TSEGS + 2, IM_COL32(0, 0, 0, 145));
|
|
}
|
|
}
|
|
|
|
// Duration countdown overlay
|
|
if (tRemainMs > 0) {
|
|
ImVec2 iconMin = ImGui::GetItemRectMin();
|
|
ImVec2 iconMax = ImGui::GetItemRectMax();
|
|
char timeStr[12];
|
|
int secs = (tRemainMs + 999) / 1000;
|
|
if (secs >= 3600)
|
|
snprintf(timeStr, sizeof(timeStr), "%dh", secs / 3600);
|
|
else if (secs >= 60)
|
|
snprintf(timeStr, sizeof(timeStr), "%d:%02d", secs / 60, secs % 60);
|
|
else
|
|
snprintf(timeStr, sizeof(timeStr), "%d", secs);
|
|
ImVec2 textSize = ImGui::CalcTextSize(timeStr);
|
|
float cx = iconMin.x + (iconMax.x - iconMin.x - textSize.x) * 0.5f;
|
|
float cy = iconMax.y - textSize.y - 1.0f;
|
|
// Color by urgency (matches player buff bar)
|
|
ImU32 tTimerColor;
|
|
if (tRemainMs < 10000) {
|
|
float pulse = 0.7f + 0.3f * std::sin(
|
|
static_cast<float>(ImGui::GetTime()) * 6.0f);
|
|
tTimerColor = IM_COL32(
|
|
static_cast<int>(255 * pulse),
|
|
static_cast<int>(80 * pulse),
|
|
static_cast<int>(60 * pulse), 255);
|
|
} else if (tRemainMs < 30000) {
|
|
tTimerColor = IM_COL32(255, 165, 0, 255);
|
|
} else {
|
|
tTimerColor = IM_COL32(255, 255, 255, 255);
|
|
}
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(cx + 1, cy + 1),
|
|
IM_COL32(0, 0, 0, 200), timeStr);
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(cx, cy),
|
|
tTimerColor, timeStr);
|
|
}
|
|
|
|
// Stack / charge count — upper-left corner
|
|
if (aura.charges > 1) {
|
|
ImVec2 iconMin = ImGui::GetItemRectMin();
|
|
char chargeStr[8];
|
|
snprintf(chargeStr, sizeof(chargeStr), "%u", static_cast<unsigned>(aura.charges));
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(iconMin.x + 3, iconMin.y + 3),
|
|
IM_COL32(0, 0, 0, 200), chargeStr);
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(iconMin.x + 2, iconMin.y + 2),
|
|
IM_COL32(255, 220, 50, 255), chargeStr);
|
|
}
|
|
|
|
// Tooltip: rich spell info + remaining duration
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::BeginTooltip();
|
|
bool richOk = spellbookScreen.renderSpellInfoTooltip(aura.spellId, gameHandler, assetMgr);
|
|
if (!richOk) {
|
|
std::string name = spellbookScreen.lookupSpellName(aura.spellId, assetMgr);
|
|
if (name.empty()) name = "Spell #" + std::to_string(aura.spellId);
|
|
ImGui::Text("%s", name.c_str());
|
|
}
|
|
renderAuraRemaining(tRemainMs);
|
|
ImGui::EndTooltip();
|
|
}
|
|
|
|
ImGui::PopID();
|
|
shown++;
|
|
}
|
|
}
|
|
}
|
|
ImGui::End();
|
|
|
|
ImGui::PopStyleColor(2);
|
|
ImGui::PopStyleVar();
|
|
|
|
// ---- Target-of-Target (ToT) mini frame ----
|
|
// Read target's current target from UNIT_FIELD_TARGET_LO/HI update fields
|
|
if (target) {
|
|
const auto& fields = target->getFields();
|
|
uint64_t totGuid = 0;
|
|
auto loIt = fields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_LO));
|
|
if (loIt != fields.end()) {
|
|
totGuid = loIt->second;
|
|
auto hiIt = fields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_HI));
|
|
if (hiIt != fields.end())
|
|
totGuid |= (static_cast<uint64_t>(hiIt->second) << 32);
|
|
}
|
|
|
|
if (totGuid != 0) {
|
|
auto totEntity = gameHandler.getEntityManager().getEntity(totGuid);
|
|
if (totEntity) {
|
|
// Position ToT frame just below and right-aligned with the target frame
|
|
float totW = 160.0f;
|
|
float totX = (screenW - totW) / 2.0f + (frameW - totW);
|
|
ImGui::SetNextWindowPos(ImVec2(totX, 30.0f + 130.0f), ImGuiCond_Always);
|
|
ImGui::SetNextWindowSize(ImVec2(totW, 0.0f), ImGuiCond_Always);
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3.0f);
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.08f, 0.80f));
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.4f, 0.4f, 0.4f, 0.7f));
|
|
|
|
if (ImGui::Begin("##ToTFrame", nullptr,
|
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar)) {
|
|
std::string totName = getEntityName(totEntity);
|
|
// Class color for players; gray for NPCs
|
|
ImVec4 totNameColor = colors::kSilver;
|
|
if (totEntity->getType() == game::ObjectType::PLAYER) {
|
|
uint8_t cid = entityClassId(totEntity.get());
|
|
if (cid != 0) totNameColor = classColorVec4(cid);
|
|
}
|
|
// Selectable so we can attach a right-click context menu
|
|
ImGui::PushStyleColor(ImGuiCol_Text, totNameColor);
|
|
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0,0,0,0));
|
|
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(1,1,1,0.08f));
|
|
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(1,1,1,0.12f));
|
|
if (ImGui::Selectable(totName.c_str(), false,
|
|
ImGuiSelectableFlags_DontClosePopups,
|
|
ImVec2(ImGui::CalcTextSize(totName.c_str()).x, 0))) {
|
|
gameHandler.setTarget(totGuid);
|
|
}
|
|
ImGui::PopStyleColor(4);
|
|
|
|
if (ImGui::BeginPopupContextItem("##ToTCtx")) {
|
|
ImGui::TextDisabled("%s", totName.c_str());
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Target"))
|
|
gameHandler.setTarget(totGuid);
|
|
if (ImGui::MenuItem("Set Focus"))
|
|
gameHandler.setFocus(totGuid);
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
if (totEntity->getType() == game::ObjectType::UNIT ||
|
|
totEntity->getType() == game::ObjectType::PLAYER) {
|
|
auto totUnit = std::static_pointer_cast<game::Unit>(totEntity);
|
|
if (totUnit->getLevel() > 0) {
|
|
ImGui::SameLine();
|
|
ImGui::TextDisabled("Lv%u", totUnit->getLevel());
|
|
}
|
|
uint32_t hp = totUnit->getHealth();
|
|
uint32_t maxHp = totUnit->getMaxHealth();
|
|
if (maxHp > 0) {
|
|
float pct = static_cast<float>(hp) / static_cast<float>(maxHp);
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram,
|
|
pct > 0.5f ? colors::kFriendlyGreen :
|
|
pct > 0.2f ? ImVec4(0.7f, 0.7f, 0.2f, 1.0f) :
|
|
colors::kDangerRed);
|
|
ImGui::ProgressBar(pct, ImVec2(-1, 10), "");
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
// ToT cast bar — green if interruptible, red if not; pulses near completion
|
|
if (auto* totCs = gameHandler.getUnitCastState(totGuid)) {
|
|
float totCastPct = (totCs->timeTotal > 0.0f)
|
|
? (totCs->timeTotal - totCs->timeRemaining) / totCs->timeTotal : 0.0f;
|
|
ImVec4 tcColor;
|
|
if (totCastPct > 0.8f) {
|
|
float pulse = 0.7f + 0.3f * std::sin(static_cast<float>(ImGui::GetTime()) * 8.0f);
|
|
tcColor = totCs->interruptible
|
|
? ImVec4(0.2f * pulse, 0.9f * pulse, 0.2f * pulse, 1.0f)
|
|
: ImVec4(1.0f * pulse, 0.1f * pulse, 0.1f * pulse, 1.0f);
|
|
} else {
|
|
tcColor = totCs->interruptible
|
|
? colors::kCastGreen
|
|
: ImVec4(0.85f, 0.15f, 0.15f, 1.0f);
|
|
}
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, tcColor);
|
|
char tcLabel[48];
|
|
const std::string& tcName = gameHandler.getSpellName(totCs->spellId);
|
|
if (!tcName.empty())
|
|
snprintf(tcLabel, sizeof(tcLabel), "%s (%.1fs)", tcName.c_str(), totCs->timeRemaining);
|
|
else
|
|
snprintf(tcLabel, sizeof(tcLabel), "Casting... (%.1fs)", totCs->timeRemaining);
|
|
ImGui::ProgressBar(totCastPct, ImVec2(-1, 8), tcLabel);
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
// ToT aura row — compact icons, debuffs first
|
|
{
|
|
const std::vector<game::AuraSlot>* totAuras = nullptr;
|
|
if (totGuid == gameHandler.getPlayerGuid())
|
|
totAuras = &gameHandler.getPlayerAuras();
|
|
else if (totGuid == gameHandler.getTargetGuid())
|
|
totAuras = &gameHandler.getTargetAuras();
|
|
else
|
|
totAuras = gameHandler.getUnitAuras(totGuid);
|
|
|
|
if (totAuras) {
|
|
int totActive = 0;
|
|
for (const auto& a : *totAuras) if (!a.isEmpty()) totActive++;
|
|
if (totActive > 0) {
|
|
auto* totAsset = services_.assetManager;
|
|
constexpr float TA_ICON = 16.0f;
|
|
constexpr int TA_PER_ROW = 8;
|
|
|
|
ImGui::Separator();
|
|
|
|
uint64_t taNowMs = static_cast<uint64_t>(
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now().time_since_epoch()).count());
|
|
|
|
std::vector<size_t> taIdx;
|
|
taIdx.reserve(totAuras->size());
|
|
for (size_t i = 0; i < totAuras->size(); ++i)
|
|
if (!(*totAuras)[i].isEmpty()) taIdx.push_back(i);
|
|
std::sort(taIdx.begin(), taIdx.end(), [&](size_t a, size_t b) {
|
|
bool aD = ((*totAuras)[a].flags & 0x80) != 0;
|
|
bool bD = ((*totAuras)[b].flags & 0x80) != 0;
|
|
if (aD != bD) return aD > bD;
|
|
int32_t ra = (*totAuras)[a].getRemainingMs(taNowMs);
|
|
int32_t rb = (*totAuras)[b].getRemainingMs(taNowMs);
|
|
if (ra < 0 && rb < 0) return false;
|
|
if (ra < 0) return false;
|
|
if (rb < 0) return true;
|
|
return ra < rb;
|
|
});
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 2.0f));
|
|
int taShown = 0;
|
|
for (size_t si = 0; si < taIdx.size() && taShown < 16; ++si) {
|
|
const auto& aura = (*totAuras)[taIdx[si]];
|
|
bool isBuff = (aura.flags & 0x80) == 0;
|
|
|
|
if (taShown > 0 && taShown % TA_PER_ROW != 0) ImGui::SameLine();
|
|
ImGui::PushID(static_cast<int>(taIdx[si]) + 5000);
|
|
|
|
ImVec4 borderCol;
|
|
if (isBuff) {
|
|
borderCol = ImVec4(0.2f, 0.8f, 0.2f, 0.9f);
|
|
} else {
|
|
uint8_t dt = gameHandler.getSpellDispelType(aura.spellId);
|
|
switch (dt) {
|
|
case 1: borderCol = ImVec4(0.15f, 0.50f, 1.00f, 0.9f); break;
|
|
case 2: borderCol = ImVec4(0.70f, 0.20f, 0.90f, 0.9f); break;
|
|
case 3: borderCol = ImVec4(0.55f, 0.30f, 0.10f, 0.9f); break;
|
|
case 4: borderCol = ImVec4(0.10f, 0.70f, 0.10f, 0.9f); break;
|
|
default: borderCol = ImVec4(0.80f, 0.20f, 0.20f, 0.9f); break;
|
|
}
|
|
}
|
|
|
|
VkDescriptorSet taIcon = (totAsset)
|
|
? getSpellIcon(aura.spellId, totAsset) : VK_NULL_HANDLE;
|
|
if (taIcon) {
|
|
ImGui::PushStyleColor(ImGuiCol_Button, borderCol);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1, 1));
|
|
ImGui::ImageButton("##taura",
|
|
(ImTextureID)(uintptr_t)taIcon,
|
|
ImVec2(TA_ICON - 2, TA_ICON - 2));
|
|
ImGui::PopStyleVar();
|
|
ImGui::PopStyleColor();
|
|
} else {
|
|
ImGui::PushStyleColor(ImGuiCol_Button, borderCol);
|
|
char lab[8];
|
|
snprintf(lab, sizeof(lab), "%u", aura.spellId % 10000);
|
|
ImGui::Button(lab, ImVec2(TA_ICON, TA_ICON));
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
// Duration overlay
|
|
int32_t taRemain = aura.getRemainingMs(taNowMs);
|
|
if (taRemain > 0) {
|
|
ImVec2 imin = ImGui::GetItemRectMin();
|
|
ImVec2 imax = ImGui::GetItemRectMax();
|
|
char ts[12];
|
|
fmtDurationCompact(ts, sizeof(ts), (taRemain + 999) / 1000);
|
|
ImVec2 tsz = ImGui::CalcTextSize(ts);
|
|
float cx = imin.x + (imax.x - imin.x - tsz.x) * 0.5f;
|
|
float cy = imax.y - tsz.y;
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(cx + 1, cy + 1), IM_COL32(0, 0, 0, 180), ts);
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(cx, cy), IM_COL32(255, 255, 255, 220), ts);
|
|
}
|
|
|
|
// Tooltip
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::BeginTooltip();
|
|
bool richOk = spellbookScreen.renderSpellInfoTooltip(
|
|
aura.spellId, gameHandler, totAsset);
|
|
if (!richOk) {
|
|
std::string nm = spellbookScreen.lookupSpellName(aura.spellId, totAsset);
|
|
if (nm.empty()) nm = "Spell #" + std::to_string(aura.spellId);
|
|
ImGui::Text("%s", nm.c_str());
|
|
}
|
|
renderAuraRemaining(taRemain);
|
|
ImGui::EndTooltip();
|
|
}
|
|
|
|
ImGui::PopID();
|
|
taShown++;
|
|
}
|
|
ImGui::PopStyleVar();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ImGui::End();
|
|
ImGui::PopStyleColor(2);
|
|
ImGui::PopStyleVar();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) {
|
|
auto focus = gameHandler.getFocus();
|
|
if (!focus) return;
|
|
|
|
auto* window = services_.window;
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
// Position: right side of screen, mirroring the target frame on the opposite side
|
|
float frameW = 200.0f;
|
|
float frameX = screenW - frameW - 10.0f;
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(frameX, 30.0f), ImGuiCond_Always);
|
|
ImGui::SetNextWindowSize(ImVec2(frameW, 0.0f), ImGuiCond_Always);
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
|
|
|
// Determine color based on relation (same logic as target frame)
|
|
ImVec4 focusColor(0.7f, 0.7f, 0.7f, 1.0f);
|
|
if (focus->getType() == game::ObjectType::PLAYER) {
|
|
// Use class color for player focus targets
|
|
uint8_t cid = entityClassId(focus.get());
|
|
focusColor = (cid != 0) ? classColorVec4(cid) : kColorBrightGreen;
|
|
} else if (focus->getType() == game::ObjectType::UNIT) {
|
|
auto u = std::static_pointer_cast<game::Unit>(focus);
|
|
if (u->getHealth() == 0 && u->getMaxHealth() > 0) {
|
|
focusColor = kColorDarkGray;
|
|
} else if (u->isHostile()) {
|
|
// Tapped-by-other: grey focus frame name
|
|
uint32_t focDynFlags = u->getDynamicFlags();
|
|
bool focTapped = (focDynFlags & 0x0004) != 0 && (focDynFlags & 0x0008) == 0;
|
|
if (focTapped) {
|
|
focusColor = kColorGray;
|
|
} else {
|
|
uint32_t playerLv = gameHandler.getPlayerLevel();
|
|
uint32_t mobLv = u->getLevel();
|
|
if (mobLv == 0) {
|
|
focusColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f); // ?? level = skull red
|
|
} else {
|
|
int32_t diff = static_cast<int32_t>(mobLv) - static_cast<int32_t>(playerLv);
|
|
if (game::GameHandler::killXp(playerLv, mobLv) == 0)
|
|
focusColor = kColorGray;
|
|
else if (diff >= 10)
|
|
focusColor = ImVec4(1.0f, 0.1f, 0.1f, 1.0f);
|
|
else if (diff >= 5)
|
|
focusColor = ImVec4(1.0f, 0.5f, 0.1f, 1.0f);
|
|
else if (diff >= -2)
|
|
focusColor = ImVec4(1.0f, 1.0f, 0.1f, 1.0f);
|
|
else
|
|
focusColor = kColorBrightGreen;
|
|
}
|
|
} // end tapped else
|
|
} else {
|
|
focusColor = kColorBrightGreen;
|
|
}
|
|
}
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.15f, 0.85f));
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.5f, 0.9f, 0.8f)); // Blue tint = focus
|
|
|
|
if (ImGui::Begin("##FocusFrame", nullptr, flags)) {
|
|
// "Focus" label
|
|
ImGui::TextDisabled("[Focus]");
|
|
ImGui::SameLine();
|
|
|
|
// Raid mark icon (star, circle, diamond, …) preceding the name
|
|
{
|
|
static constexpr struct { const char* sym; ImU32 col; } kFocusMarks[] = {
|
|
{ "\xe2\x98\x85", IM_COL32(255, 204, 0, 255) }, // 0 Star (yellow)
|
|
{ "\xe2\x97\x8f", IM_COL32(255, 103, 0, 255) }, // 1 Circle (orange)
|
|
{ "\xe2\x97\x86", IM_COL32(160, 32, 240, 255) }, // 2 Diamond (purple)
|
|
{ "\xe2\x96\xb2", IM_COL32( 50, 200, 50, 255) }, // 3 Triangle (green)
|
|
{ "\xe2\x97\x8c", IM_COL32( 80, 160, 255, 255) }, // 4 Moon (blue)
|
|
{ "\xe2\x96\xa0", IM_COL32( 50, 200, 220, 255) }, // 5 Square (teal)
|
|
{ "\xe2\x9c\x9d", IM_COL32(255, 80, 80, 255) }, // 6 Cross (red)
|
|
{ "\xe2\x98\xa0", IM_COL32(255, 255, 255, 255) }, // 7 Skull (white)
|
|
};
|
|
uint8_t fmark = gameHandler.getEntityRaidMark(focus->getGuid());
|
|
if (fmark < game::GameHandler::kRaidMarkCount) {
|
|
ImGui::GetWindowDrawList()->AddText(
|
|
ImGui::GetCursorScreenPos(),
|
|
kFocusMarks[fmark].col, kFocusMarks[fmark].sym);
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 18.0f);
|
|
}
|
|
}
|
|
|
|
std::string focusName = getEntityName(focus);
|
|
ImGui::PushStyleColor(ImGuiCol_Text, focusColor);
|
|
ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0,0,0,0));
|
|
ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(1,1,1,0.08f));
|
|
ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(1,1,1,0.12f));
|
|
ImGui::Selectable(focusName.c_str(), false, ImGuiSelectableFlags_DontClosePopups,
|
|
ImVec2(ImGui::CalcTextSize(focusName.c_str()).x, 0));
|
|
ImGui::PopStyleColor(4);
|
|
|
|
// Right-click context menu on focus frame
|
|
if (ImGui::BeginPopupContextItem("##FocusFrameCtx")) {
|
|
ImGui::TextDisabled("%s", focusName.c_str());
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Target"))
|
|
gameHandler.setTarget(focus->getGuid());
|
|
if (ImGui::MenuItem("Clear Focus"))
|
|
gameHandler.clearFocus();
|
|
if (focus->getType() == game::ObjectType::PLAYER) {
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Whisper")) {
|
|
chatPanel_.setWhisperTarget(focusName);
|
|
}
|
|
if (ImGui::MenuItem("Invite to Group"))
|
|
gameHandler.inviteToGroup(focusName);
|
|
if (ImGui::MenuItem("Trade"))
|
|
gameHandler.initiateTrade(focus->getGuid());
|
|
if (ImGui::MenuItem("Duel"))
|
|
gameHandler.proposeDuel(focus->getGuid());
|
|
if (ImGui::MenuItem("Inspect")) {
|
|
gameHandler.setTarget(focus->getGuid());
|
|
gameHandler.inspectTarget();
|
|
socialPanel_.showInspectWindow_ = true;
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Add Friend"))
|
|
gameHandler.addFriend(focusName);
|
|
if (ImGui::MenuItem("Ignore"))
|
|
gameHandler.addIgnore(focusName);
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
// Group leader crown — golden ♛ when the focused player is the party/raid leader
|
|
if (gameHandler.isInGroup() && focus->getType() == game::ObjectType::PLAYER) {
|
|
if (gameHandler.getPartyData().leaderGuid == focus->getGuid()) {
|
|
ImGui::SameLine(0, 4);
|
|
ImGui::TextColored(colors::kSymbolGold, "\xe2\x99\x9b");
|
|
if (ImGui::IsItemHovered()) ImGui::SetTooltip("Group Leader");
|
|
}
|
|
}
|
|
|
|
// Quest giver indicator and classification badge for NPC focus targets
|
|
if (focus->getType() == game::ObjectType::UNIT) {
|
|
auto focusUnit = std::static_pointer_cast<game::Unit>(focus);
|
|
|
|
// Quest indicator: ! / ?
|
|
{
|
|
using QGS = game::QuestGiverStatus;
|
|
QGS qgs = gameHandler.getQuestGiverStatus(focus->getGuid());
|
|
if (qgs == QGS::AVAILABLE) {
|
|
ImGui::SameLine(0, 4);
|
|
ImGui::TextColored(colors::kBrightGold, "!");
|
|
} else if (qgs == QGS::AVAILABLE_LOW) {
|
|
ImGui::SameLine(0, 4);
|
|
ImGui::TextColored(kColorGray, "!");
|
|
} else if (qgs == QGS::REWARD || qgs == QGS::REWARD_REP) {
|
|
ImGui::SameLine(0, 4);
|
|
ImGui::TextColored(colors::kBrightGold, "?");
|
|
} else if (qgs == QGS::INCOMPLETE) {
|
|
ImGui::SameLine(0, 4);
|
|
ImGui::TextColored(kColorGray, "?");
|
|
}
|
|
}
|
|
|
|
// Classification badge
|
|
int fRank = gameHandler.getCreatureRank(focusUnit->getEntry());
|
|
if (fRank == 1) { ImGui::SameLine(0,4); ImGui::TextColored(ImVec4(1.0f,0.8f,0.2f,1.0f), "[Elite]"); }
|
|
else if (fRank == 2) { ImGui::SameLine(0,4); ImGui::TextColored(ImVec4(0.8f,0.4f,1.0f,1.0f), "[Rare Elite]"); }
|
|
else if (fRank == 3) { ImGui::SameLine(0,4); ImGui::TextColored(colors::kRed, "[Boss]"); }
|
|
else if (fRank == 4) { ImGui::SameLine(0,4); ImGui::TextColored(ImVec4(0.5f,0.9f,1.0f,1.0f), "[Rare]"); }
|
|
|
|
// Creature type
|
|
{
|
|
uint32_t fctype = gameHandler.getCreatureType(focusUnit->getEntry());
|
|
const char* fctName = nullptr;
|
|
switch (fctype) {
|
|
case 1: fctName="Beast"; break; case 2: fctName="Dragonkin"; break;
|
|
case 3: fctName="Demon"; break; case 4: fctName="Elemental"; break;
|
|
case 5: fctName="Giant"; break; case 6: fctName="Undead"; break;
|
|
case 7: fctName="Humanoid"; break; case 8: fctName="Critter"; break;
|
|
case 9: fctName="Mechanical"; break; case 11: fctName="Totem"; break;
|
|
case 12: fctName="Non-combat Pet"; break; case 13: fctName="Gas Cloud"; break;
|
|
default: break;
|
|
}
|
|
if (fctName) {
|
|
ImGui::SameLine(0, 4);
|
|
ImGui::TextColored(ImVec4(0.6f, 0.6f, 0.6f, 0.9f), "(%s)", fctName);
|
|
}
|
|
}
|
|
|
|
// Creature subtitle
|
|
const std::string fSub = gameHandler.getCachedCreatureSubName(focusUnit->getEntry());
|
|
if (!fSub.empty())
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 0.9f), "<%s>", fSub.c_str());
|
|
}
|
|
|
|
// Player guild name on focus frame
|
|
if (focus->getType() == game::ObjectType::PLAYER) {
|
|
uint32_t guildId = gameHandler.getEntityGuildId(focus->getGuid());
|
|
if (guildId != 0) {
|
|
const std::string& gn = gameHandler.lookupGuildName(guildId);
|
|
if (!gn.empty()) {
|
|
ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.7f, 0.9f), "<%s>", gn.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ImGui::BeginPopupContextItem("##FocusNameCtx")) {
|
|
const bool focusIsPlayer = (focus->getType() == game::ObjectType::PLAYER);
|
|
const uint64_t fGuid = focus->getGuid();
|
|
ImGui::TextDisabled("%s", focusName.c_str());
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Target"))
|
|
gameHandler.setTarget(fGuid);
|
|
if (ImGui::MenuItem("Clear Focus"))
|
|
gameHandler.clearFocus();
|
|
if (focusIsPlayer) {
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Whisper")) {
|
|
chatPanel_.setWhisperTarget(focusName);
|
|
}
|
|
if (ImGui::MenuItem("Invite to Group"))
|
|
gameHandler.inviteToGroup(focusName);
|
|
if (ImGui::MenuItem("Trade"))
|
|
gameHandler.initiateTrade(fGuid);
|
|
if (ImGui::MenuItem("Duel"))
|
|
gameHandler.proposeDuel(fGuid);
|
|
if (ImGui::MenuItem("Inspect")) {
|
|
gameHandler.setTarget(fGuid);
|
|
gameHandler.inspectTarget();
|
|
socialPanel_.showInspectWindow_ = true;
|
|
}
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Add Friend"))
|
|
gameHandler.addFriend(focusName);
|
|
if (ImGui::MenuItem("Ignore"))
|
|
gameHandler.addIgnore(focusName);
|
|
}
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
if (focus->getType() == game::ObjectType::UNIT ||
|
|
focus->getType() == game::ObjectType::PLAYER) {
|
|
auto unit = std::static_pointer_cast<game::Unit>(focus);
|
|
|
|
// Level + health on same row
|
|
ImGui::SameLine();
|
|
if (unit->getLevel() == 0)
|
|
ImGui::TextDisabled("Lv ??");
|
|
else
|
|
ImGui::TextDisabled("Lv %u", unit->getLevel());
|
|
|
|
uint32_t hp = unit->getHealth();
|
|
uint32_t maxHp = unit->getMaxHealth();
|
|
if (maxHp > 0) {
|
|
float pct = static_cast<float>(hp) / static_cast<float>(maxHp);
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram,
|
|
pct > 0.5f ? colors::kFriendlyGreen :
|
|
pct > 0.2f ? ImVec4(0.7f, 0.7f, 0.2f, 1.0f) :
|
|
colors::kDangerRed);
|
|
char overlay[32];
|
|
snprintf(overlay, sizeof(overlay), "%u / %u", hp, maxHp);
|
|
ImGui::ProgressBar(pct, ImVec2(-1, 14), overlay);
|
|
ImGui::PopStyleColor();
|
|
|
|
// Power bar
|
|
uint8_t pType = unit->getPowerType();
|
|
uint32_t pwr = unit->getPower();
|
|
uint32_t maxPwr = unit->getMaxPower();
|
|
if (maxPwr == 0 && (pType == 1 || pType == 3)) maxPwr = 100;
|
|
if (maxPwr > 0) {
|
|
float mpPct = static_cast<float>(pwr) / static_cast<float>(maxPwr);
|
|
ImVec4 pwrColor;
|
|
switch (pType) {
|
|
case 0: pwrColor = colors::kManaBlue; break;
|
|
case 1: pwrColor = colors::kDarkRed; break;
|
|
case 3: pwrColor = colors::kEnergyYellow; break;
|
|
case 6: pwrColor = colors::kRunicRed; break;
|
|
default: pwrColor = colors::kManaBlue; break;
|
|
}
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, pwrColor);
|
|
ImGui::ProgressBar(mpPct, ImVec2(-1, 10), "");
|
|
ImGui::PopStyleColor();
|
|
}
|
|
}
|
|
|
|
// Focus cast bar
|
|
const auto* focusCast = gameHandler.getUnitCastState(focus->getGuid());
|
|
if (focusCast) {
|
|
float total = focusCast->timeTotal > 0.f ? focusCast->timeTotal : 1.f;
|
|
float rem = focusCast->timeRemaining;
|
|
float prog = std::clamp(1.0f - rem / total, 0.f, 1.f);
|
|
const std::string& spName = gameHandler.getSpellName(focusCast->spellId);
|
|
// Pulse orange when > 80% complete — interrupt window closing
|
|
ImVec4 focusCastColor;
|
|
if (prog > 0.8f) {
|
|
float pulse = 0.7f + 0.3f * std::sin(static_cast<float>(ImGui::GetTime()) * 8.0f);
|
|
focusCastColor = ImVec4(1.0f * pulse, 0.5f * pulse, 0.0f, 1.0f);
|
|
} else {
|
|
focusCastColor = ImVec4(0.9f, 0.3f, 0.2f, 1.0f);
|
|
}
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, focusCastColor);
|
|
char castBuf[64];
|
|
if (!spName.empty())
|
|
snprintf(castBuf, sizeof(castBuf), "%s (%.1fs)", spName.c_str(), rem);
|
|
else
|
|
snprintf(castBuf, sizeof(castBuf), "Casting... (%.1fs)", rem);
|
|
{
|
|
auto* fcAsset = services_.assetManager;
|
|
VkDescriptorSet fcIcon = (focusCast->spellId != 0 && fcAsset)
|
|
? getSpellIcon(focusCast->spellId, fcAsset) : VK_NULL_HANDLE;
|
|
if (fcIcon) {
|
|
ImGui::Image((ImTextureID)(uintptr_t)fcIcon, ImVec2(12, 12));
|
|
ImGui::SameLine(0, 2);
|
|
ImGui::ProgressBar(prog, ImVec2(-1, 12), castBuf);
|
|
} else {
|
|
ImGui::ProgressBar(prog, ImVec2(-1, 12), castBuf);
|
|
}
|
|
}
|
|
ImGui::PopStyleColor();
|
|
}
|
|
}
|
|
|
|
// Focus auras — buffs first, then debuffs, up to 8 icons wide
|
|
{
|
|
const std::vector<game::AuraSlot>* focusAuras =
|
|
(focus->getGuid() == gameHandler.getTargetGuid())
|
|
? &gameHandler.getTargetAuras()
|
|
: gameHandler.getUnitAuras(focus->getGuid());
|
|
|
|
if (focusAuras) {
|
|
int activeCount = 0;
|
|
for (const auto& a : *focusAuras) if (!a.isEmpty()) activeCount++;
|
|
if (activeCount > 0) {
|
|
auto* focusAsset = services_.assetManager;
|
|
constexpr float FA_ICON = 20.0f;
|
|
constexpr int FA_PER_ROW = 10;
|
|
|
|
ImGui::Separator();
|
|
|
|
uint64_t faNowMs = static_cast<uint64_t>(
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
std::chrono::steady_clock::now().time_since_epoch()).count());
|
|
|
|
// Sort: debuffs first (so hostile-caster info is prominent), then buffs
|
|
std::vector<size_t> faIdx;
|
|
faIdx.reserve(focusAuras->size());
|
|
for (size_t i = 0; i < focusAuras->size(); ++i)
|
|
if (!(*focusAuras)[i].isEmpty()) faIdx.push_back(i);
|
|
std::sort(faIdx.begin(), faIdx.end(), [&](size_t a, size_t b) {
|
|
bool aD = ((*focusAuras)[a].flags & 0x80) != 0;
|
|
bool bD = ((*focusAuras)[b].flags & 0x80) != 0;
|
|
if (aD != bD) return aD > bD; // debuffs first
|
|
int32_t ra = (*focusAuras)[a].getRemainingMs(faNowMs);
|
|
int32_t rb = (*focusAuras)[b].getRemainingMs(faNowMs);
|
|
if (ra < 0 && rb < 0) return false;
|
|
if (ra < 0) return false;
|
|
if (rb < 0) return true;
|
|
return ra < rb;
|
|
});
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 2.0f));
|
|
int faShown = 0;
|
|
for (size_t si = 0; si < faIdx.size() && faShown < 20; ++si) {
|
|
const auto& aura = (*focusAuras)[faIdx[si]];
|
|
bool isBuff = (aura.flags & 0x80) == 0;
|
|
|
|
if (faShown > 0 && faShown % FA_PER_ROW != 0) ImGui::SameLine();
|
|
ImGui::PushID(static_cast<int>(faIdx[si]) + 3000);
|
|
|
|
ImVec4 borderCol;
|
|
if (isBuff) {
|
|
borderCol = ImVec4(0.2f, 0.8f, 0.2f, 0.9f);
|
|
} else {
|
|
uint8_t dt = gameHandler.getSpellDispelType(aura.spellId);
|
|
switch (dt) {
|
|
case 1: borderCol = ImVec4(0.15f, 0.50f, 1.00f, 0.9f); break;
|
|
case 2: borderCol = ImVec4(0.70f, 0.20f, 0.90f, 0.9f); break;
|
|
case 3: borderCol = ImVec4(0.55f, 0.30f, 0.10f, 0.9f); break;
|
|
case 4: borderCol = ImVec4(0.10f, 0.70f, 0.10f, 0.9f); break;
|
|
default: borderCol = ImVec4(0.80f, 0.20f, 0.20f, 0.9f); break;
|
|
}
|
|
}
|
|
|
|
VkDescriptorSet faIcon = (focusAsset)
|
|
? getSpellIcon(aura.spellId, focusAsset) : VK_NULL_HANDLE;
|
|
if (faIcon) {
|
|
ImGui::PushStyleColor(ImGuiCol_Button, borderCol);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1, 1));
|
|
ImGui::ImageButton("##faura",
|
|
(ImTextureID)(uintptr_t)faIcon,
|
|
ImVec2(FA_ICON - 2, FA_ICON - 2));
|
|
ImGui::PopStyleVar();
|
|
ImGui::PopStyleColor();
|
|
} else {
|
|
ImGui::PushStyleColor(ImGuiCol_Button, borderCol);
|
|
char lab[8];
|
|
snprintf(lab, sizeof(lab), "%u", aura.spellId);
|
|
ImGui::Button(lab, ImVec2(FA_ICON, FA_ICON));
|
|
ImGui::PopStyleColor();
|
|
}
|
|
|
|
// Duration overlay
|
|
int32_t faRemain = aura.getRemainingMs(faNowMs);
|
|
if (faRemain > 0) {
|
|
ImVec2 imin = ImGui::GetItemRectMin();
|
|
ImVec2 imax = ImGui::GetItemRectMax();
|
|
char ts[12];
|
|
fmtDurationCompact(ts, sizeof(ts), (faRemain + 999) / 1000);
|
|
ImVec2 tsz = ImGui::CalcTextSize(ts);
|
|
float cx = imin.x + (imax.x - imin.x - tsz.x) * 0.5f;
|
|
float cy = imax.y - tsz.y - 1.0f;
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(cx + 1, cy + 1), IM_COL32(0, 0, 0, 180), ts);
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(cx, cy), IM_COL32(255, 255, 255, 220), ts);
|
|
}
|
|
|
|
// Stack / charge count — upper-left corner (parity with target frame)
|
|
if (aura.charges > 1) {
|
|
ImVec2 faMin = ImGui::GetItemRectMin();
|
|
char chargeStr[8];
|
|
snprintf(chargeStr, sizeof(chargeStr), "%u", static_cast<unsigned>(aura.charges));
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(faMin.x + 3, faMin.y + 3),
|
|
IM_COL32(0, 0, 0, 200), chargeStr);
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(faMin.x + 2, faMin.y + 2),
|
|
IM_COL32(255, 220, 50, 255), chargeStr);
|
|
}
|
|
|
|
// Tooltip
|
|
if (ImGui::IsItemHovered()) {
|
|
ImGui::BeginTooltip();
|
|
bool richOk = spellbookScreen.renderSpellInfoTooltip(
|
|
aura.spellId, gameHandler, focusAsset);
|
|
if (!richOk) {
|
|
std::string nm = spellbookScreen.lookupSpellName(aura.spellId, focusAsset);
|
|
if (nm.empty()) nm = "Spell #" + std::to_string(aura.spellId);
|
|
ImGui::Text("%s", nm.c_str());
|
|
}
|
|
renderAuraRemaining(faRemain);
|
|
ImGui::EndTooltip();
|
|
}
|
|
|
|
ImGui::PopID();
|
|
faShown++;
|
|
}
|
|
ImGui::PopStyleVar();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Target-of-Focus: who the focus target is currently targeting
|
|
{
|
|
uint64_t fofGuid = 0;
|
|
const auto& fFields = focus->getFields();
|
|
auto fItLo = fFields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_LO));
|
|
if (fItLo != fFields.end()) {
|
|
fofGuid = fItLo->second;
|
|
auto fItHi = fFields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_HI));
|
|
if (fItHi != fFields.end())
|
|
fofGuid |= (static_cast<uint64_t>(fItHi->second) << 32);
|
|
}
|
|
if (fofGuid != 0) {
|
|
auto fofEnt = gameHandler.getEntityManager().getEntity(fofGuid);
|
|
std::string fofName;
|
|
ImVec4 fofColor(0.7f, 0.7f, 0.7f, 1.0f);
|
|
if (fofGuid == gameHandler.getPlayerGuid()) {
|
|
fofName = "You";
|
|
fofColor = kColorBrightGreen;
|
|
} else if (fofEnt) {
|
|
fofName = getEntityName(fofEnt);
|
|
uint8_t fcid = entityClassId(fofEnt.get());
|
|
if (fcid != 0) fofColor = classColorVec4(fcid);
|
|
}
|
|
if (!fofName.empty()) {
|
|
ImGui::TextDisabled("▶");
|
|
ImGui::SameLine(0, 2);
|
|
ImGui::TextColored(fofColor, "%s", fofName.c_str());
|
|
if (ImGui::IsItemHovered())
|
|
ImGui::SetTooltip("Focus's target: %s\nClick to target", fofName.c_str());
|
|
if (ImGui::IsItemClicked())
|
|
gameHandler.setTarget(fofGuid);
|
|
|
|
// Compact health bar for target-of-focus
|
|
if (fofEnt) {
|
|
auto fofUnit = std::dynamic_pointer_cast<game::Unit>(fofEnt);
|
|
if (fofUnit && fofUnit->getMaxHealth() > 0) {
|
|
float fofPct = static_cast<float>(fofUnit->getHealth()) /
|
|
static_cast<float>(fofUnit->getMaxHealth());
|
|
ImVec4 fofBarColor =
|
|
fofPct > 0.5f ? colors::kCastGreen :
|
|
fofPct > 0.2f ? ImVec4(0.75f, 0.75f, 0.2f, 1.0f) :
|
|
ImVec4(0.75f, 0.2f, 0.2f, 1.0f);
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, fofBarColor);
|
|
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.15f, 0.15f, 0.15f, 0.8f));
|
|
char fofOverlay[32];
|
|
snprintf(fofOverlay, sizeof(fofOverlay), "%u%%",
|
|
static_cast<unsigned>(fofPct * 100.0f + 0.5f));
|
|
ImGui::ProgressBar(fofPct, ImVec2(-1, 10), fofOverlay);
|
|
ImGui::PopStyleColor(2);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Distance to focus target
|
|
{
|
|
const auto& mv = gameHandler.getMovementInfo();
|
|
float fdx = focus->getX() - mv.x;
|
|
float fdy = focus->getY() - mv.y;
|
|
float fdz = focus->getZ() - mv.z;
|
|
float fdist = std::sqrt(fdx * fdx + fdy * fdy + fdz * fdz);
|
|
ImGui::TextDisabled("%.1f yd", fdist);
|
|
}
|
|
|
|
// Clicking the focus frame targets it
|
|
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(0)) {
|
|
gameHandler.setTarget(focus->getGuid());
|
|
}
|
|
}
|
|
ImGui::End();
|
|
|
|
ImGui::PopStyleColor(2);
|
|
ImGui::PopStyleVar();
|
|
}
|
|
|
|
|
|
}} // namespace wowee::ui
|