2026-02-02 12:24:50 -08:00
|
|
|
#include "ui/game_screen.hpp"
|
2026-03-25 12:27:43 -07:00
|
|
|
#include "ui/ui_colors.hpp"
|
2026-02-22 03:32:08 -08:00
|
|
|
#include "rendering/vk_context.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "core/application.hpp"
|
2026-04-01 20:38:37 +03:00
|
|
|
#include "core/appearance_composer.hpp"
|
2026-03-20 11:12:07 -07:00
|
|
|
#include "addons/addon_manager.hpp"
|
2026-02-04 17:37:28 -08:00
|
|
|
#include "core/coordinates.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "core/input.hpp"
|
|
|
|
|
#include "rendering/renderer.hpp"
|
2026-02-23 01:10:58 -08:00
|
|
|
#include "rendering/wmo_renderer.hpp"
|
2026-02-21 01:26:16 -08:00
|
|
|
#include "rendering/terrain_manager.hpp"
|
2026-02-04 22:27:45 -08:00
|
|
|
#include "rendering/minimap.hpp"
|
2026-02-21 19:41:21 -08:00
|
|
|
#include "rendering/world_map.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "rendering/character_renderer.hpp"
|
|
|
|
|
#include "rendering/camera.hpp"
|
2026-02-05 17:55:30 -08:00
|
|
|
#include "rendering/camera_controller.hpp"
|
2026-02-17 16:26:49 -08:00
|
|
|
#include "audio/audio_engine.hpp"
|
2026-02-05 16:17:04 -08:00
|
|
|
#include "audio/music_manager.hpp"
|
2026-02-17 16:26:49 -08:00
|
|
|
#include "game/zone_manager.hpp"
|
2026-02-05 17:55:30 -08:00
|
|
|
#include "audio/footstep_manager.hpp"
|
|
|
|
|
#include "audio/activity_sound_manager.hpp"
|
Implement comprehensive audio control panel with tabbed settings interface
Adds complete audio volume controls for all 11 audio systems with master volume. Reorganizes settings window into Video, Audio, and Gameplay tabs for better UX.
Audio Features:
- Master volume control affecting all audio systems
- Individual volume sliders for: Music, Ambient, UI, Combat, Spell, Movement, Footsteps, NPC Voices, Mounts, Activity sounds
- Real-time volume adjustment with master volume multiplier
- Restore defaults button per tab
Technical Changes:
- Added getVolumeScale() getters to all audio managers
- Integrated all 10 audio managers into renderer (UI, Combat, Spell, Movement added)
- Expanded game_screen.hpp with 11 pending volume variables
- Reorganized settings window using ImGui tab bars (Video/Audio/Gameplay)
- Audio settings uses scrollable child window for 11 volume controls
- Settings window expanded to 520x720px to accommodate comprehensive controls
2026-02-09 17:07:22 -08:00
|
|
|
#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"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "pipeline/asset_manager.hpp"
|
|
|
|
|
#include "pipeline/dbc_loader.hpp"
|
2026-02-12 22:56:36 -08:00
|
|
|
#include "pipeline/dbc_layout.hpp"
|
2026-02-15 04:18:34 -08:00
|
|
|
|
2026-02-12 22:56:36 -08:00
|
|
|
#include "game/expansion_profile.hpp"
|
2026-03-12 10:41:18 -07:00
|
|
|
#include "game/character.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "core/logger.hpp"
|
|
|
|
|
#include <imgui.h>
|
2026-02-14 22:00:26 -08:00
|
|
|
#include <imgui_internal.h>
|
2026-02-06 18:34:45 -08:00
|
|
|
#include <algorithm>
|
2026-02-02 12:24:50 -08:00
|
|
|
#include <cmath>
|
2026-02-06 18:34:45 -08:00
|
|
|
#include <cstring>
|
2026-03-18 02:23:47 -07:00
|
|
|
#include <sstream>
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
#include <cstdlib>
|
|
|
|
|
#include <filesystem>
|
|
|
|
|
#include <fstream>
|
2026-02-11 21:14:35 -08:00
|
|
|
#include <cctype>
|
2026-02-14 18:27:59 -08:00
|
|
|
#include <chrono>
|
|
|
|
|
#include <ctime>
|
2026-02-02 12:24:50 -08:00
|
|
|
#include <unordered_set>
|
|
|
|
|
|
|
|
|
|
namespace {
|
2026-03-25 12:27:43 -07:00
|
|
|
// Common ImGui colors (aliases into local namespace for brevity)
|
|
|
|
|
using namespace wowee::ui::colors;
|
|
|
|
|
constexpr auto& kColorRed = kRed;
|
|
|
|
|
constexpr auto& kColorGreen = kGreen;
|
|
|
|
|
constexpr auto& kColorBrightGreen= kBrightGreen;
|
|
|
|
|
constexpr auto& kColorYellow = kYellow;
|
|
|
|
|
constexpr auto& kColorGray = kGray;
|
|
|
|
|
constexpr auto& kColorDarkGray = kDarkGray;
|
2026-03-25 11:57:22 -07:00
|
|
|
|
2026-03-27 14:11:05 -07:00
|
|
|
// Aura dispel-type names (indexed by dispelType 0-4)
|
|
|
|
|
constexpr const char* kDispelNames[] = { "", "Magic", "Curse", "Disease", "Poison" };
|
|
|
|
|
|
refactor: add 9 button/bar color constants, batch constexpr promotions
New ui_colors.hpp constants: kBtnGreen, kBtnGreenHover, kBtnRed,
kBtnRedHover, kBtnDkGreen/Hover, kBtnDkRed/Hover, kMidHealthYellow
— replacing 21 inline literals across accept/decline button and
health bar patterns.
Deduplicate kMon/kMonths month arrays (2 copies → 1 kMonthAbbrev).
Promote 22 remaining static const char*/int arrays to constexpr
(kQualHex, resLabels, kRepRankNames, kTotemNames, kReactLabels,
kChatHelp, kMacroHelp, kHelpLines, kMarkWords, componentDirs,
keyLabels, kRollLabels, gossipIcons, kMarkNames, kDiffLabels,
kStatLabels, kCatHeaders, kSlotNames, kResolutions, displayToInternal).
2026-03-27 14:44:52 -07:00
|
|
|
// Abbreviated month names (indexed 0-11)
|
|
|
|
|
constexpr const char* kMonthAbbrev[12] = {
|
|
|
|
|
"Jan","Feb","Mar","Apr","May","Jun",
|
|
|
|
|
"Jul","Aug","Sep","Oct","Nov","Dec"
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-27 14:13:16 -07:00
|
|
|
// Raid mark names with symbol prefixes (indexed 0-7: Star..Skull)
|
|
|
|
|
constexpr const char* kRaidMarkNames[] = {
|
|
|
|
|
"{*} Star", "{O} Circle", "{<>} Diamond", "{^} Triangle",
|
|
|
|
|
"{)} Moon", "{ } Square", "{x} Cross", "{8} Skull"
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-25 11:57:22 -07:00
|
|
|
// Common ImGui window flags for popup dialogs
|
|
|
|
|
const ImGuiWindowFlags kDialogFlags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize;
|
|
|
|
|
|
2026-03-11 21:27:16 -07:00
|
|
|
// Build a WoW-format item link string for chat insertion.
|
|
|
|
|
// Format: |cff<qualHex>|Hitem:<itemId>:0:0:0:0:0:0:0:0|h[<name>]|h|r
|
|
|
|
|
std::string buildItemChatLink(uint32_t itemId, uint8_t quality, const std::string& name) {
|
refactor: add 9 button/bar color constants, batch constexpr promotions
New ui_colors.hpp constants: kBtnGreen, kBtnGreenHover, kBtnRed,
kBtnRedHover, kBtnDkGreen/Hover, kBtnDkRed/Hover, kMidHealthYellow
— replacing 21 inline literals across accept/decline button and
health bar patterns.
Deduplicate kMon/kMonths month arrays (2 copies → 1 kMonthAbbrev).
Promote 22 remaining static const char*/int arrays to constexpr
(kQualHex, resLabels, kRepRankNames, kTotemNames, kReactLabels,
kChatHelp, kMacroHelp, kHelpLines, kMarkWords, componentDirs,
keyLabels, kRollLabels, gossipIcons, kMarkNames, kDiffLabels,
kStatLabels, kCatHeaders, kSlotNames, kResolutions, displayToInternal).
2026-03-27 14:44:52 -07:00
|
|
|
static constexpr const char* kQualHex[] = {"9d9d9d","ffffff","1eff00","0070dd","a335ee","ff8000","e6cc80","e6cc80"};
|
2026-03-13 10:30:54 -07:00
|
|
|
uint8_t qi = quality < 8 ? quality : 1;
|
2026-03-11 21:27:16 -07:00
|
|
|
char buf[512];
|
|
|
|
|
snprintf(buf, sizeof(buf), "|cff%s|Hitem:%u:0:0:0:0:0:0:0:0|h[%s]|h|r",
|
|
|
|
|
kQualHex[qi], itemId, name.c_str());
|
|
|
|
|
return buf;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 21:14:35 -08:00
|
|
|
std::string trim(const std::string& s) {
|
|
|
|
|
size_t first = s.find_first_not_of(" \t\r\n");
|
|
|
|
|
if (first == std::string::npos) return "";
|
|
|
|
|
size_t last = s.find_last_not_of(" \t\r\n");
|
|
|
|
|
return s.substr(first, last - first + 1);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 14:17:28 -07:00
|
|
|
// Format a duration in seconds as compact text: "2h", "3:05", "42"
|
|
|
|
|
void fmtDurationCompact(char* buf, size_t sz, int secs) {
|
|
|
|
|
if (secs >= 3600) snprintf(buf, sz, "%dh", secs / 3600);
|
|
|
|
|
else if (secs >= 60) snprintf(buf, sz, "%d:%02d", secs / 60, secs % 60);
|
|
|
|
|
else snprintf(buf, sz, "%d", secs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Render "Remaining: Xs" or "Remaining: Xm Ys" in a tooltip (light gray)
|
|
|
|
|
void renderAuraRemaining(int remainMs) {
|
|
|
|
|
if (remainMs <= 0) return;
|
|
|
|
|
int s = remainMs / 1000;
|
|
|
|
|
char buf[32];
|
|
|
|
|
if (s < 60) snprintf(buf, sizeof(buf), "Remaining: %ds", s);
|
|
|
|
|
else snprintf(buf, sizeof(buf), "Remaining: %dm %ds", s / 60, s % 60);
|
|
|
|
|
ImGui::TextColored(kLightGray, "%s", buf);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 21:14:35 -08:00
|
|
|
std::string toLower(std::string s) {
|
|
|
|
|
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) {
|
|
|
|
|
return static_cast<char>(std::tolower(c));
|
|
|
|
|
});
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 08:10:17 -07:00
|
|
|
// Render gold/silver/copper amounts in WoW-canonical colors on the current ImGui line.
|
|
|
|
|
// Skips zero-value denominations (except copper, which is always shown when gold=silver=0).
|
refactor: deduplicate class color functions, add 9 color constants
Move classColor/classColorU32 to shared getClassColor()/getClassColorU32()
in ui_colors.hpp, eliminating duplicate 10-case switch in character_screen
and game_screen.
New ui_colors.hpp constants: kInactiveGray, kVeryLightGray, kSymbolGold,
kLowHealthRed, kDangerRed, kEnergyYellow, kHappinessGreen, kRunicRed,
kSoulShardPurple — replacing 36 inline literals across 4 files.
2026-03-27 14:07:36 -07:00
|
|
|
// Aliases for shared class color helpers (wowee::ui namespace)
|
|
|
|
|
inline ImVec4 classColorVec4(uint8_t classId) { return wowee::ui::getClassColor(classId); }
|
|
|
|
|
inline ImU32 classColorU32(uint8_t classId, int alpha = 255) { return wowee::ui::getClassColorU32(classId, alpha); }
|
2026-03-12 08:33:34 -07:00
|
|
|
|
|
|
|
|
// Extract class id from a unit's UNIT_FIELD_BYTES_0 update field.
|
|
|
|
|
// Returns 0 if the entity pointer is null or field is unset.
|
|
|
|
|
uint8_t entityClassId(const wowee::game::Entity* entity) {
|
|
|
|
|
if (!entity) return 0;
|
|
|
|
|
using UF = wowee::game::UF;
|
|
|
|
|
uint32_t bytes0 = entity->getField(wowee::game::fieldIndex(UF::UNIT_FIELD_BYTES_0));
|
|
|
|
|
return static_cast<uint8_t>((bytes0 >> 8) & 0xFF);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 14:30:45 -07:00
|
|
|
// Alias for shared class name helper
|
2026-03-12 08:46:26 -07:00
|
|
|
const char* classNameStr(uint8_t classId) {
|
2026-03-27 14:30:45 -07:00
|
|
|
return wowee::game::getClassName(static_cast<wowee::game::Class>(classId));
|
2026-03-12 08:46:26 -07:00
|
|
|
}
|
|
|
|
|
|
2026-02-11 21:14:35 -08:00
|
|
|
bool isPortBotTarget(const std::string& target) {
|
|
|
|
|
std::string t = toLower(trim(target));
|
|
|
|
|
return t == "portbot" || t == "gmbot" || t == "telebot";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string buildPortBotCommand(const std::string& rawInput) {
|
|
|
|
|
std::string input = trim(rawInput);
|
|
|
|
|
if (input.empty()) return "";
|
|
|
|
|
|
|
|
|
|
std::string lower = toLower(input);
|
|
|
|
|
if (lower == "help" || lower == "?") {
|
|
|
|
|
return "__help__";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (lower.rfind(".tele ", 0) == 0 || lower.rfind(".go ", 0) == 0) {
|
|
|
|
|
return input;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (lower.rfind("xyz ", 0) == 0) {
|
|
|
|
|
return ".go " + input;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (lower == "sw" || lower == "stormwind") return ".tele stormwind";
|
|
|
|
|
if (lower == "if" || lower == "ironforge") return ".tele ironforge";
|
|
|
|
|
if (lower == "darn" || lower == "darnassus") return ".tele darnassus";
|
|
|
|
|
if (lower == "org" || lower == "orgrimmar") return ".tele orgrimmar";
|
|
|
|
|
if (lower == "tb" || lower == "thunderbluff") return ".tele thunderbluff";
|
|
|
|
|
if (lower == "uc" || lower == "undercity") return ".tele undercity";
|
|
|
|
|
if (lower == "shatt" || lower == "shattrath") return ".tele shattrath";
|
|
|
|
|
if (lower == "dal" || lower == "dalaran") return ".tele dalaran";
|
|
|
|
|
|
|
|
|
|
return ".tele " + input;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
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();
|
2026-02-08 00:59:40 -08:00
|
|
|
} 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();
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
return "Unknown";
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
|
|
|
|
|
// Collect all non-comment, non-empty lines from a macro body.
|
|
|
|
|
std::vector<std::string> allMacroCommands(const std::string& macroText) {
|
|
|
|
|
std::vector<std::string> cmds;
|
|
|
|
|
size_t pos = 0;
|
|
|
|
|
while (pos <= macroText.size()) {
|
|
|
|
|
size_t nl = macroText.find('\n', pos);
|
|
|
|
|
std::string line = (nl != std::string::npos) ? macroText.substr(pos, nl - pos) : macroText.substr(pos);
|
|
|
|
|
if (!line.empty() && line.back() == '\r') line.pop_back();
|
|
|
|
|
size_t start = line.find_first_not_of(" \t");
|
|
|
|
|
if (start != std::string::npos) line = line.substr(start);
|
|
|
|
|
if (!line.empty() && line.front() != '#')
|
|
|
|
|
cmds.push_back(std::move(line));
|
|
|
|
|
if (nl == std::string::npos) break;
|
|
|
|
|
pos = nl + 1;
|
|
|
|
|
}
|
|
|
|
|
return cmds;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Returns the #showtooltip argument from a macro body.
|
|
|
|
|
std::string getMacroShowtooltipArg(const std::string& macroText) {
|
|
|
|
|
size_t pos = 0;
|
|
|
|
|
while (pos <= macroText.size()) {
|
|
|
|
|
size_t nl = macroText.find('\n', pos);
|
|
|
|
|
std::string line = (nl != std::string::npos) ? macroText.substr(pos, nl - pos) : macroText.substr(pos);
|
|
|
|
|
if (!line.empty() && line.back() == '\r') line.pop_back();
|
|
|
|
|
size_t fs = line.find_first_not_of(" \t");
|
|
|
|
|
if (fs != std::string::npos) line = line.substr(fs);
|
|
|
|
|
if (line.rfind("#showtooltip", 0) == 0 || line.rfind("#show", 0) == 0) {
|
|
|
|
|
size_t sp = line.find(' ');
|
|
|
|
|
if (sp != std::string::npos) {
|
|
|
|
|
std::string arg = line.substr(sp + 1);
|
|
|
|
|
size_t as = arg.find_first_not_of(" \t");
|
|
|
|
|
if (as != std::string::npos) arg = arg.substr(as);
|
|
|
|
|
size_t ae = arg.find_last_not_of(" \t");
|
|
|
|
|
if (ae != std::string::npos) arg.resize(ae + 1);
|
|
|
|
|
if (!arg.empty()) return arg;
|
|
|
|
|
}
|
|
|
|
|
return "__auto__";
|
|
|
|
|
}
|
|
|
|
|
if (nl == std::string::npos) break;
|
|
|
|
|
pos = nl + 1;
|
|
|
|
|
}
|
|
|
|
|
return {};
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
namespace wowee { namespace ui {
|
|
|
|
|
|
|
|
|
|
GameScreen::GameScreen() {
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
loadSettings();
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
// Section 3.5: Set UI services and propagate to child components
|
|
|
|
|
void GameScreen::setServices(const UIServices& services) {
|
|
|
|
|
services_ = services;
|
|
|
|
|
// Update legacy pointer for Phase A compatibility
|
|
|
|
|
appearanceComposer_ = services.appearanceComposer;
|
|
|
|
|
// Propagate to child panels
|
|
|
|
|
chatPanel_.setServices(services);
|
|
|
|
|
toastManager_.setServices(services);
|
|
|
|
|
dialogManager_.setServices(services);
|
|
|
|
|
settingsPanel_.setServices(services);
|
|
|
|
|
combatUI_.setServices(services);
|
|
|
|
|
socialPanel_.setServices(services);
|
|
|
|
|
actionBarPanel_.setServices(services);
|
|
|
|
|
windowManager_.setServices(services);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
void GameScreen::render(game::GameHandler& gameHandler) {
|
2026-03-31 08:53:14 +03:00
|
|
|
// Set up chat bubble callback (once) and cache game handler in ChatPanel
|
|
|
|
|
chatPanel_.setupCallbacks(gameHandler);
|
2026-03-31 09:18:17 +03:00
|
|
|
toastManager_.setupCallbacks(gameHandler);
|
2026-03-12 16:33:08 -07:00
|
|
|
|
2026-03-18 12:17:00 -07:00
|
|
|
// Set up appearance-changed callback to refresh inventory preview (barber shop, etc.)
|
|
|
|
|
if (!appearanceCallbackSet_) {
|
|
|
|
|
gameHandler.setAppearanceChangedCallback([this]() {
|
|
|
|
|
inventoryScreenCharGuid_ = 0; // force preview re-sync on next frame
|
|
|
|
|
});
|
|
|
|
|
appearanceCallbackSet_ = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 01:15:11 -07:00
|
|
|
// Set up UI error frame callback (once)
|
|
|
|
|
if (!uiErrorCallbackSet_) {
|
|
|
|
|
gameHandler.setUIErrorCallback([this](const std::string& msg) {
|
|
|
|
|
uiErrors_.push_back({msg, 0.0f});
|
|
|
|
|
if (uiErrors_.size() > 5) uiErrors_.erase(uiErrors_.begin());
|
2026-03-13 10:05:10 -07:00
|
|
|
// Play error sound for each new error (rate-limited by deque cap of 5)
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
if (auto* r = services_.renderer) {
|
2026-03-13 10:05:10 -07:00
|
|
|
if (auto* sfx = r->getUiSoundManager()) sfx->playError();
|
|
|
|
|
}
|
2026-03-12 01:15:11 -07:00
|
|
|
});
|
|
|
|
|
uiErrorCallbackSet_ = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-18 04:30:33 -07:00
|
|
|
// Flash the action bar button whose spell just failed (0.5 s red overlay).
|
|
|
|
|
if (!castFailedCallbackSet_) {
|
|
|
|
|
gameHandler.setSpellCastFailedCallback([this](uint32_t spellId) {
|
|
|
|
|
if (spellId == 0) return;
|
|
|
|
|
float now = static_cast<float>(ImGui::GetTime());
|
2026-03-31 19:49:52 +03:00
|
|
|
actionBarPanel_.actionFlashEndTimes_[spellId] = now + actionBarPanel_.kActionFlashDuration;
|
2026-03-18 04:30:33 -07:00
|
|
|
});
|
|
|
|
|
castFailedCallbackSet_ = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 20:19:39 -08:00
|
|
|
// Apply UI transparency setting
|
|
|
|
|
float prevAlpha = ImGui::GetStyle().Alpha;
|
2026-03-31 10:07:58 +03:00
|
|
|
ImGui::GetStyle().Alpha = settingsPanel_.uiOpacity_;
|
2026-02-06 20:19:39 -08:00
|
|
|
|
2026-02-23 08:01:20 -08:00
|
|
|
// Sync minimap opacity with UI opacity
|
|
|
|
|
{
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-02-23 08:01:20 -08:00
|
|
|
if (renderer) {
|
|
|
|
|
if (auto* minimap = renderer->getMinimap()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
minimap->setOpacity(settingsPanel_.uiOpacity_);
|
2026-02-23 08:01:20 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 16:26:49 -08:00
|
|
|
// Apply initial settings when renderer becomes available
|
2026-03-31 10:07:58 +03:00
|
|
|
if (!settingsPanel_.minimapSettingsApplied_) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-02-09 17:39:21 -08:00
|
|
|
if (renderer) {
|
|
|
|
|
if (auto* minimap = renderer->getMinimap()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.minimapRotate_ = false;
|
|
|
|
|
settingsPanel_.pendingMinimapRotate = false;
|
2026-02-11 17:30:57 -08:00
|
|
|
minimap->setRotateWithCamera(false);
|
2026-03-31 10:07:58 +03:00
|
|
|
minimap->setSquareShape(settingsPanel_.minimapSquare_);
|
|
|
|
|
settingsPanel_.minimapSettingsApplied_ = true;
|
2026-02-09 17:39:21 -08:00
|
|
|
}
|
2026-02-17 16:26:49 -08:00
|
|
|
if (auto* zm = renderer->getZoneManager()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
zm->setUseOriginalSoundtrack(settingsPanel_.pendingUseOriginalSoundtrack);
|
2026-02-17 16:26:49 -08:00
|
|
|
}
|
2026-02-21 01:26:16 -08:00
|
|
|
if (auto* tm = renderer->getTerrainManager()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
tm->setGroundClutterDensityScale(static_cast<float>(settingsPanel_.pendingGroundClutterDensity) / 100.0f);
|
2026-02-21 01:26:16 -08:00
|
|
|
}
|
2026-02-17 16:26:49 -08:00
|
|
|
// Restore mute state: save actual master volume first, then apply mute
|
2026-03-31 10:07:58 +03:00
|
|
|
if (settingsPanel_.soundMuted_) {
|
2026-02-17 16:26:49 -08:00
|
|
|
float actual = audio::AudioEngine::instance().getMasterVolume();
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.preMuteVolume_ = (actual > 0.0f) ? actual
|
|
|
|
|
: static_cast<float>(settingsPanel_.pendingMasterVolume) / 100.0f;
|
2026-02-17 16:26:49 -08:00
|
|
|
audio::AudioEngine::instance().setMasterVolume(0.0f);
|
|
|
|
|
}
|
2026-02-09 17:39:21 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 17:37:20 -08:00
|
|
|
// Apply saved volume settings once when audio managers first become available
|
2026-03-31 10:07:58 +03:00
|
|
|
if (!settingsPanel_.volumeSettingsApplied_) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-02-17 17:37:20 -08:00
|
|
|
if (renderer && renderer->getUiSoundManager()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.applyAudioVolumes(renderer);
|
|
|
|
|
settingsPanel_.volumeSettingsApplied_ = true;
|
2026-02-17 17:37:20 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 02:59:24 -08:00
|
|
|
// Apply saved MSAA setting once when renderer is available
|
2026-03-31 10:07:58 +03:00
|
|
|
if (!settingsPanel_.msaaSettingsApplied_ && settingsPanel_.pendingAntiAliasing > 0) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-02-22 02:59:24 -08:00
|
|
|
if (renderer) {
|
|
|
|
|
static const VkSampleCountFlagBits aaSamples[] = {
|
|
|
|
|
VK_SAMPLE_COUNT_1_BIT, VK_SAMPLE_COUNT_2_BIT,
|
|
|
|
|
VK_SAMPLE_COUNT_4_BIT, VK_SAMPLE_COUNT_8_BIT
|
|
|
|
|
};
|
2026-03-31 10:07:58 +03:00
|
|
|
renderer->setMsaaSamples(aaSamples[settingsPanel_.pendingAntiAliasing]);
|
|
|
|
|
settingsPanel_.msaaSettingsApplied_ = true;
|
2026-02-22 02:59:24 -08:00
|
|
|
}
|
|
|
|
|
} else {
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.msaaSettingsApplied_ = true;
|
2026-02-22 02:59:24 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-12 16:43:48 -07:00
|
|
|
// Apply saved FXAA setting once when renderer is available
|
2026-03-31 10:07:58 +03:00
|
|
|
if (!settingsPanel_.fxaaSettingsApplied_) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-03-12 16:43:48 -07:00
|
|
|
if (renderer) {
|
2026-03-31 10:07:58 +03:00
|
|
|
renderer->setFXAAEnabled(settingsPanel_.pendingFXAA);
|
|
|
|
|
settingsPanel_.fxaaSettingsApplied_ = true;
|
2026-03-12 16:43:48 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 19:15:34 -08:00
|
|
|
// Apply saved water refraction setting once when renderer is available
|
2026-03-31 10:07:58 +03:00
|
|
|
if (!settingsPanel_.waterRefractionApplied_) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-03-06 19:15:34 -08:00
|
|
|
if (renderer) {
|
2026-03-31 10:07:58 +03:00
|
|
|
renderer->setWaterRefractionEnabled(settingsPanel_.pendingWaterRefraction);
|
|
|
|
|
settingsPanel_.waterRefractionApplied_ = true;
|
2026-03-06 19:15:34 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 01:10:58 -08:00
|
|
|
// Apply saved normal mapping / POM settings once when WMO renderer is available
|
2026-03-31 10:07:58 +03:00
|
|
|
if (!settingsPanel_.normalMapSettingsApplied_) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-02-23 01:10:58 -08:00
|
|
|
if (renderer) {
|
|
|
|
|
if (auto* wr = renderer->getWMORenderer()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
wr->setNormalMappingEnabled(settingsPanel_.pendingNormalMapping);
|
|
|
|
|
wr->setNormalMapStrength(settingsPanel_.pendingNormalMapStrength);
|
|
|
|
|
wr->setPOMEnabled(settingsPanel_.pendingPOM);
|
|
|
|
|
wr->setPOMQuality(settingsPanel_.pendingPOMQuality);
|
2026-02-23 01:40:23 -08:00
|
|
|
if (auto* cr = renderer->getCharacterRenderer()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
cr->setNormalMappingEnabled(settingsPanel_.pendingNormalMapping);
|
|
|
|
|
cr->setNormalMapStrength(settingsPanel_.pendingNormalMapStrength);
|
|
|
|
|
cr->setPOMEnabled(settingsPanel_.pendingPOM);
|
|
|
|
|
cr->setPOMQuality(settingsPanel_.pendingPOMQuality);
|
2026-02-23 01:40:23 -08:00
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.normalMapSettingsApplied_ = true;
|
2026-02-23 01:10:58 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-08 20:22:11 -07:00
|
|
|
// Apply saved upscaling setting once when renderer is available
|
2026-03-31 10:07:58 +03:00
|
|
|
if (!settingsPanel_.fsrSettingsApplied_) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-03-07 22:03:28 -08:00
|
|
|
if (renderer) {
|
2026-03-27 14:47:58 -07:00
|
|
|
static constexpr float fsrScales[] = { 0.77f, 0.67f, 0.59f, 1.00f };
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.pendingFSRQuality = std::clamp(settingsPanel_.pendingFSRQuality, 0, 3);
|
|
|
|
|
renderer->setFSRQuality(fsrScales[settingsPanel_.pendingFSRQuality]);
|
|
|
|
|
renderer->setFSRSharpness(settingsPanel_.pendingFSRSharpness);
|
|
|
|
|
renderer->setFSR2DebugTuning(settingsPanel_.pendingFSR2JitterSign, settingsPanel_.pendingFSR2MotionVecScaleX, settingsPanel_.pendingFSR2MotionVecScaleY);
|
|
|
|
|
renderer->setAmdFsr3FramegenEnabled(settingsPanel_.pendingAMDFramegen);
|
|
|
|
|
int effectiveMode = settingsPanel_.pendingUpscalingMode;
|
2026-03-08 20:48:46 -07:00
|
|
|
|
2026-03-20 07:53:07 -07:00
|
|
|
// Defer FSR2/FSR3 activation until fully in-world to avoid
|
|
|
|
|
// init issues during login/character selection screens.
|
2026-03-08 20:48:46 -07:00
|
|
|
if (effectiveMode == 2 && gameHandler.getState() != game::WorldState::IN_WORLD) {
|
2026-03-08 20:45:26 -07:00
|
|
|
renderer->setFSREnabled(false);
|
|
|
|
|
renderer->setFSR2Enabled(false);
|
|
|
|
|
} else {
|
2026-03-08 20:48:46 -07:00
|
|
|
renderer->setFSREnabled(effectiveMode == 1);
|
|
|
|
|
renderer->setFSR2Enabled(effectiveMode == 2);
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.fsrSettingsApplied_ = true;
|
2026-03-08 20:45:26 -07:00
|
|
|
}
|
2026-03-07 22:03:28 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 20:21:06 -07:00
|
|
|
// Apply auto-loot / auto-sell settings to GameHandler every frame (cheap bool sync)
|
2026-03-31 10:07:58 +03:00
|
|
|
gameHandler.setAutoLoot(settingsPanel_.pendingAutoLoot);
|
|
|
|
|
gameHandler.setAutoSellGrey(settingsPanel_.pendingAutoSellGrey);
|
|
|
|
|
gameHandler.setAutoRepair(settingsPanel_.pendingAutoRepair);
|
2026-02-17 16:31:00 -08:00
|
|
|
|
2026-02-14 18:27:59 -08:00
|
|
|
// Sync chat auto-join settings to GameHandler
|
2026-03-31 08:53:14 +03:00
|
|
|
gameHandler.chatAutoJoin.general = chatPanel_.chatAutoJoinGeneral;
|
|
|
|
|
gameHandler.chatAutoJoin.trade = chatPanel_.chatAutoJoinTrade;
|
|
|
|
|
gameHandler.chatAutoJoin.localDefense = chatPanel_.chatAutoJoinLocalDefense;
|
|
|
|
|
gameHandler.chatAutoJoin.lfg = chatPanel_.chatAutoJoinLFG;
|
|
|
|
|
gameHandler.chatAutoJoin.local = chatPanel_.chatAutoJoinLocal;
|
2026-02-14 18:27:59 -08:00
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Process targeting input before UI windows
|
|
|
|
|
processTargetInput(gameHandler);
|
|
|
|
|
|
|
|
|
|
// Player unit frame (top-left)
|
|
|
|
|
renderPlayerFrame(gameHandler);
|
|
|
|
|
|
2026-03-09 17:23:28 -07:00
|
|
|
// Pet frame (below player frame, only when player has an active pet)
|
|
|
|
|
if (gameHandler.hasPet()) {
|
|
|
|
|
renderPetFrame(gameHandler);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 20:59:29 -07:00
|
|
|
// Auto-open pet rename modal when server signals the pet is renameable (first tame)
|
|
|
|
|
if (gameHandler.consumePetRenameablePending()) {
|
|
|
|
|
petRenameOpen_ = true;
|
|
|
|
|
petRenameBuf_[0] = '\0';
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 10:14:44 -07:00
|
|
|
// Totem frame (Shaman only, when any totem is active)
|
|
|
|
|
if (gameHandler.getPlayerClass() == 7) {
|
|
|
|
|
renderTotemFrame(gameHandler);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Target frame (only when we have a target)
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
renderTargetFrame(gameHandler);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 21:15:24 -07:00
|
|
|
// Focus target frame (only when we have a focus)
|
|
|
|
|
if (gameHandler.hasFocus()) {
|
|
|
|
|
renderFocusFrame(gameHandler);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Render windows
|
|
|
|
|
if (showPlayerInfo) {
|
|
|
|
|
renderPlayerInfo(gameHandler);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (showEntityWindow) {
|
|
|
|
|
renderEntityList(gameHandler);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (showChatWindow) {
|
2026-03-31 08:53:14 +03:00
|
|
|
chatPanel_.getSpellIcon = [this](uint32_t id, pipeline::AssetManager* am) {
|
|
|
|
|
return getSpellIcon(id, am);
|
|
|
|
|
};
|
|
|
|
|
chatPanel_.render(gameHandler, inventoryScreen, spellbookScreen, questLogScreen);
|
|
|
|
|
// Process slash commands that affect GameScreen state
|
|
|
|
|
auto cmds = chatPanel_.consumeSlashCommands();
|
2026-03-31 19:49:52 +03:00
|
|
|
if (cmds.showInspect) socialPanel_.showInspectWindow_ = true;
|
|
|
|
|
if (cmds.toggleThreat) combatUI_.showThreatWindow_ = !combatUI_.showThreatWindow_;
|
|
|
|
|
if (cmds.showBgScore) combatUI_.showBgScoreboard_ = !combatUI_.showBgScoreboard_;
|
|
|
|
|
if (cmds.showGmTicket) windowManager_.showGmTicketWindow_ = true;
|
|
|
|
|
if (cmds.showWho) socialPanel_.showWhoWindow_ = true;
|
|
|
|
|
if (cmds.toggleCombatLog) combatUI_.showCombatLog_ = !combatUI_.showCombatLog_;
|
2026-03-31 08:53:14 +03:00
|
|
|
if (cmds.takeScreenshot) takeScreenshot(gameHandler);
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
// ---- New UI elements ----
|
2026-03-31 19:49:52 +03:00
|
|
|
actionBarPanel_.renderActionBar(gameHandler, settingsPanel_, chatPanel_,
|
|
|
|
|
inventoryScreen, spellbookScreen, questLogScreen,
|
|
|
|
|
[this](uint32_t id, pipeline::AssetManager* am) { return getSpellIcon(id, am); });
|
|
|
|
|
actionBarPanel_.renderStanceBar(gameHandler, settingsPanel_, spellbookScreen,
|
|
|
|
|
[this](uint32_t id, pipeline::AssetManager* am) { return getSpellIcon(id, am); });
|
|
|
|
|
actionBarPanel_.renderBagBar(gameHandler, settingsPanel_, inventoryScreen);
|
|
|
|
|
actionBarPanel_.renderXpBar(gameHandler, settingsPanel_);
|
|
|
|
|
actionBarPanel_.renderRepBar(gameHandler, settingsPanel_);
|
|
|
|
|
auto spellIconFn = [this](uint32_t id, pipeline::AssetManager* am) { return getSpellIcon(id, am); };
|
|
|
|
|
combatUI_.renderCastBar(gameHandler, spellIconFn);
|
2026-03-09 14:30:48 -07:00
|
|
|
renderMirrorTimers(gameHandler);
|
2026-03-31 19:49:52 +03:00
|
|
|
combatUI_.renderCooldownTracker(gameHandler, settingsPanel_, spellIconFn);
|
2026-03-09 15:05:38 -07:00
|
|
|
renderQuestObjectiveTracker(gameHandler);
|
2026-03-10 07:25:04 -07:00
|
|
|
renderNameplates(gameHandler); // player names always shown; NPC plates gated by showNameplates_
|
2026-03-31 19:49:52 +03:00
|
|
|
combatUI_.renderBattlegroundScore(gameHandler);
|
|
|
|
|
combatUI_.renderRaidWarningOverlay(gameHandler);
|
|
|
|
|
combatUI_.renderCombatText(gameHandler);
|
|
|
|
|
combatUI_.renderDPSMeter(gameHandler, settingsPanel_);
|
2026-03-12 14:25:37 -07:00
|
|
|
renderDurabilityWarning(gameHandler);
|
2026-03-12 01:15:11 -07:00
|
|
|
renderUIErrors(gameHandler, ImGui::GetIO().DeltaTime);
|
2026-03-31 09:18:17 +03:00
|
|
|
toastManager_.renderEarlyToasts(ImGui::GetIO().DeltaTime, gameHandler);
|
2026-03-31 19:49:52 +03:00
|
|
|
if (socialPanel_.showRaidFrames_) {
|
|
|
|
|
socialPanel_.renderPartyFrames(gameHandler, chatPanel_, spellIconFn);
|
2026-03-11 09:24:37 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
socialPanel_.renderBossFrames(gameHandler, spellbookScreen, spellIconFn);
|
2026-03-31 10:07:58 +03:00
|
|
|
dialogManager_.renderDialogs(gameHandler, inventoryScreen, chatPanel_);
|
2026-03-31 19:49:52 +03:00
|
|
|
socialPanel_.renderGuildRoster(gameHandler, chatPanel_);
|
|
|
|
|
socialPanel_.renderSocialFrame(gameHandler, chatPanel_);
|
|
|
|
|
combatUI_.renderBuffBar(gameHandler, spellbookScreen, spellIconFn);
|
|
|
|
|
windowManager_.renderLootWindow(gameHandler, inventoryScreen, chatPanel_);
|
|
|
|
|
windowManager_.renderGossipWindow(gameHandler, chatPanel_);
|
|
|
|
|
windowManager_.renderQuestDetailsWindow(gameHandler, chatPanel_, inventoryScreen);
|
|
|
|
|
windowManager_.renderQuestRequestItemsWindow(gameHandler, chatPanel_, inventoryScreen);
|
|
|
|
|
windowManager_.renderQuestOfferRewardWindow(gameHandler, chatPanel_, inventoryScreen);
|
|
|
|
|
windowManager_.renderVendorWindow(gameHandler, inventoryScreen, chatPanel_);
|
|
|
|
|
windowManager_.renderTrainerWindow(gameHandler,
|
|
|
|
|
[this](uint32_t id, pipeline::AssetManager* am) { return getSpellIcon(id, am); });
|
|
|
|
|
windowManager_.renderBarberShopWindow(gameHandler);
|
|
|
|
|
windowManager_.renderStableWindow(gameHandler);
|
|
|
|
|
windowManager_.renderTaxiWindow(gameHandler);
|
|
|
|
|
windowManager_.renderMailWindow(gameHandler, inventoryScreen, chatPanel_);
|
|
|
|
|
windowManager_.renderMailComposeWindow(gameHandler, inventoryScreen);
|
|
|
|
|
windowManager_.renderBankWindow(gameHandler, inventoryScreen, chatPanel_);
|
|
|
|
|
windowManager_.renderGuildBankWindow(gameHandler, inventoryScreen, chatPanel_);
|
|
|
|
|
windowManager_.renderAuctionHouseWindow(gameHandler, inventoryScreen, chatPanel_);
|
|
|
|
|
socialPanel_.renderDungeonFinderWindow(gameHandler, chatPanel_);
|
|
|
|
|
windowManager_.renderInstanceLockouts(gameHandler);
|
|
|
|
|
socialPanel_.renderWhoWindow(gameHandler, chatPanel_);
|
|
|
|
|
combatUI_.renderCombatLog(gameHandler, spellbookScreen);
|
|
|
|
|
windowManager_.renderAchievementWindow(gameHandler);
|
|
|
|
|
windowManager_.renderSkillsWindow(gameHandler);
|
|
|
|
|
windowManager_.renderTitlesWindow(gameHandler);
|
|
|
|
|
windowManager_.renderEquipSetWindow(gameHandler);
|
|
|
|
|
windowManager_.renderGmTicketWindow(gameHandler);
|
|
|
|
|
socialPanel_.renderInspectWindow(gameHandler, inventoryScreen);
|
|
|
|
|
windowManager_.renderBookWindow(gameHandler);
|
|
|
|
|
combatUI_.renderThreatWindow(gameHandler);
|
|
|
|
|
combatUI_.renderBgScoreboard(gameHandler);
|
2026-03-11 09:24:37 -07:00
|
|
|
if (showMinimap_) {
|
|
|
|
|
renderMinimapMarkers(gameHandler);
|
|
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
windowManager_.renderLogoutCountdown(gameHandler);
|
|
|
|
|
windowManager_.renderDeathScreen(gameHandler);
|
|
|
|
|
windowManager_.renderReclaimCorpseButton(gameHandler);
|
2026-03-31 10:07:58 +03:00
|
|
|
dialogManager_.renderLateDialogs(gameHandler);
|
2026-03-31 08:53:14 +03:00
|
|
|
chatPanel_.renderBubbles(gameHandler);
|
2026-03-31 19:49:52 +03:00
|
|
|
windowManager_.renderEscapeMenu(settingsPanel_);
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.renderSettingsWindow(inventoryScreen, chatPanel_, [this]() { saveSettings(); });
|
2026-03-31 09:18:17 +03:00
|
|
|
toastManager_.renderLateToasts(gameHandler);
|
2026-03-17 16:34:39 -07:00
|
|
|
renderWeatherOverlay(gameHandler);
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
2026-02-04 22:27:45 -08:00
|
|
|
// World map (M key toggle handled inside)
|
|
|
|
|
renderWorldMap(gameHandler);
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
// Quest Log (L key toggle handled inside)
|
2026-03-11 20:57:39 -07:00
|
|
|
questLogScreen.render(gameHandler, inventoryScreen);
|
2026-02-06 13:47:03 -08:00
|
|
|
|
2026-02-04 11:31:08 -08:00
|
|
|
// Spellbook (P key toggle handled inside)
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
spellbookScreen.render(gameHandler, services_.assetManager);
|
2026-02-04 11:31:08 -08:00
|
|
|
|
2026-03-11 21:57:13 -07:00
|
|
|
// Insert spell link into chat if player shift-clicked a spellbook entry
|
|
|
|
|
{
|
|
|
|
|
std::string pendingSpellLink = spellbookScreen.getAndClearPendingChatLink();
|
|
|
|
|
if (!pendingSpellLink.empty()) {
|
2026-03-31 08:53:14 +03:00
|
|
|
chatPanel_.insertChatLink(pendingSpellLink);
|
2026-03-11 21:57:13 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 16:04:25 -08:00
|
|
|
// Talents (N key toggle handled inside)
|
|
|
|
|
talentScreen.render(gameHandler);
|
|
|
|
|
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
// Set up inventory screen asset manager + player appearance (re-init on character switch)
|
2026-02-06 14:24:38 -08:00
|
|
|
{
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
uint64_t activeGuid = gameHandler.getActiveCharacterGuid();
|
|
|
|
|
if (activeGuid != 0 && activeGuid != inventoryScreenCharGuid_) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* am = services_.assetManager;
|
2026-02-06 14:24:38 -08:00
|
|
|
if (am) {
|
|
|
|
|
inventoryScreen.setAssetManager(am);
|
|
|
|
|
const auto* ch = gameHandler.getActiveCharacter();
|
|
|
|
|
if (ch) {
|
|
|
|
|
uint8_t skin = ch->appearanceBytes & 0xFF;
|
|
|
|
|
uint8_t face = (ch->appearanceBytes >> 8) & 0xFF;
|
|
|
|
|
uint8_t hairStyle = (ch->appearanceBytes >> 16) & 0xFF;
|
|
|
|
|
uint8_t hairColor = (ch->appearanceBytes >> 24) & 0xFF;
|
|
|
|
|
inventoryScreen.setPlayerAppearance(
|
|
|
|
|
ch->race, ch->gender, skin, face,
|
|
|
|
|
hairStyle, hairColor, ch->facialFeatures);
|
Fix vendor buying, improve character select, parallelize WMO culling, and optimize collision
- Fix CMSG_BUY_ITEM count field from uint8 to uint32 (server silently dropped undersized packets)
- Character select screen: remember last selected character, two-column layout with details panel, double-click to enter world, responsive window sizing
- Fix stale character data between logins by replacing static init flag with per-character GUID tracking
- Parallelize WMO visibility culling across worker threads (same pattern as M2 renderer)
- Optimize WMO collision queries with world-space group bounds early rejection in getFloorHeight, checkWallCollision, isInsideWMO, and raycastBoundingBoxes
- Reduce camera ground samples from 5 to 3 movement-aligned probes
- Add WMO interior lighting, unlit materials, vertex color multiply, and alpha blending support
2026-02-07 15:29:19 -08:00
|
|
|
inventoryScreenCharGuid_ = activeGuid;
|
2026-02-06 14:24:38 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
// Set vendor mode before rendering inventory
|
|
|
|
|
inventoryScreen.setVendorMode(gameHandler.isVendorWindowOpen(), &gameHandler);
|
|
|
|
|
|
2026-02-19 22:34:22 -08:00
|
|
|
// Auto-open bags once when vendor window first opens
|
2026-02-19 05:48:40 -08:00
|
|
|
if (gameHandler.isVendorWindowOpen()) {
|
2026-03-31 19:49:52 +03:00
|
|
|
if (!windowManager_.vendorBagsOpened_) {
|
|
|
|
|
windowManager_.vendorBagsOpened_ = true;
|
2026-02-19 22:34:22 -08:00
|
|
|
if (inventoryScreen.isSeparateBags()) {
|
2026-02-19 05:48:40 -08:00
|
|
|
inventoryScreen.openAllBags();
|
2026-02-19 22:34:22 -08:00
|
|
|
} else if (!inventoryScreen.isOpen()) {
|
|
|
|
|
inventoryScreen.setOpen(true);
|
2026-02-19 05:48:40 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-19 22:34:22 -08:00
|
|
|
} else {
|
2026-03-31 19:49:52 +03:00
|
|
|
windowManager_.vendorBagsOpened_ = false;
|
2026-02-06 13:47:03 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Bags (B key toggle handled inside)
|
2026-02-06 18:34:45 -08:00
|
|
|
inventoryScreen.setGameHandler(&gameHandler);
|
2026-02-05 14:01:26 -08:00
|
|
|
inventoryScreen.render(gameHandler.getInventory(), gameHandler.getMoneyCopper());
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-03-11 21:11:58 -07:00
|
|
|
// Character screen (C key toggle handled inside render())
|
|
|
|
|
inventoryScreen.renderCharacterScreen(gameHandler);
|
|
|
|
|
|
|
|
|
|
// Insert item link into chat if player shift-clicked any inventory/equipment slot
|
2026-03-11 21:09:42 -07:00
|
|
|
{
|
|
|
|
|
std::string pendingLink = inventoryScreen.getAndClearPendingChatLink();
|
|
|
|
|
if (!pendingLink.empty()) {
|
2026-03-31 08:53:14 +03:00
|
|
|
chatPanel_.insertChatLink(pendingLink);
|
2026-03-11 21:09:42 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 03:13:42 -08:00
|
|
|
if (inventoryScreen.consumeEquipmentDirty() || gameHandler.consumeOnlineEquipmentDirty()) {
|
2026-02-02 12:24:50 -08:00
|
|
|
updateCharacterGeosets(gameHandler.getInventory());
|
|
|
|
|
updateCharacterTextures(gameHandler.getInventory());
|
2026-04-01 20:38:37 +03:00
|
|
|
if (appearanceComposer_) appearanceComposer_->loadEquippedWeapons();
|
2026-02-06 14:24:38 -08:00
|
|
|
inventoryScreen.markPreviewDirty();
|
2026-02-06 15:41:29 -08:00
|
|
|
// Update renderer weapon type for animation selection
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* r = services_.renderer;
|
2026-02-06 15:41:29 -08:00
|
|
|
if (r) {
|
|
|
|
|
const auto& mh = gameHandler.getInventory().getEquipSlot(game::EquipSlot::MAIN_HAND);
|
|
|
|
|
r->setEquippedWeaponType(mh.empty() ? 0 : mh.item.inventoryType);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
// Update renderer face-target position and selection circle
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-02-02 12:24:50 -08:00
|
|
|
if (renderer) {
|
2026-03-14 08:27:32 -07:00
|
|
|
renderer->setInCombat(gameHandler.isInCombat() &&
|
|
|
|
|
!gameHandler.isPlayerDead() &&
|
|
|
|
|
!gameHandler.isPlayerGhost());
|
2026-03-14 08:31:08 -07:00
|
|
|
if (auto* cr = renderer->getCharacterRenderer()) {
|
|
|
|
|
uint32_t charInstId = renderer->getCharacterInstanceId();
|
|
|
|
|
if (charInstId != 0) {
|
|
|
|
|
const bool isGhost = gameHandler.isPlayerGhost();
|
|
|
|
|
if (!ghostOpacityStateKnown_ ||
|
|
|
|
|
ghostOpacityLastState_ != isGhost ||
|
|
|
|
|
ghostOpacityLastInstanceId_ != charInstId) {
|
|
|
|
|
cr->setInstanceOpacity(charInstId, isGhost ? 0.5f : 1.0f);
|
|
|
|
|
ghostOpacityStateKnown_ = true;
|
|
|
|
|
ghostOpacityLastState_ = isGhost;
|
|
|
|
|
ghostOpacityLastInstanceId_ = charInstId;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
static glm::vec3 targetGLPos;
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
auto target = gameHandler.getTarget();
|
|
|
|
|
if (target) {
|
2026-03-10 06:33:44 -07:00
|
|
|
// Prefer the renderer's actual instance position so the selection
|
|
|
|
|
// circle tracks the rendered model (not a parallel entity-space
|
|
|
|
|
// interpolator that can drift from the visual position).
|
|
|
|
|
glm::vec3 instPos;
|
|
|
|
|
if (core::Application::getInstance().getRenderPositionForGuid(target->getGuid(), instPos)) {
|
|
|
|
|
targetGLPos = instPos;
|
|
|
|
|
// Override Z with foot position to sit the circle on the ground.
|
|
|
|
|
float footZ = 0.0f;
|
|
|
|
|
if (core::Application::getInstance().getRenderFootZForGuid(target->getGuid(), footZ)) {
|
|
|
|
|
targetGLPos.z = footZ;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback: entity game-logic position (no CharacterRenderer instance yet)
|
|
|
|
|
targetGLPos = core::coords::canonicalToRender(
|
|
|
|
|
glm::vec3(target->getX(), target->getY(), target->getZ()));
|
|
|
|
|
float footZ = 0.0f;
|
|
|
|
|
if (core::Application::getInstance().getRenderFootZForGuid(target->getGuid(), footZ)) {
|
|
|
|
|
targetGLPos.z = footZ;
|
|
|
|
|
}
|
2026-02-20 16:02:34 -08:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
renderer->setTargetPosition(&targetGLPos);
|
2026-02-06 13:47:03 -08:00
|
|
|
|
2026-02-06 16:47:07 -08:00
|
|
|
// Selection circle color: WoW-canonical level-based colors
|
2026-02-21 02:52:05 -08:00
|
|
|
bool showSelectionCircle = false;
|
2026-02-06 13:47:03 -08:00
|
|
|
glm::vec3 circleColor(1.0f, 1.0f, 0.3f); // default yellow
|
|
|
|
|
float circleRadius = 1.5f;
|
2026-02-06 18:34:45 -08:00
|
|
|
{
|
|
|
|
|
glm::vec3 boundsCenter;
|
|
|
|
|
float boundsRadius = 0.0f;
|
|
|
|
|
if (core::Application::getInstance().getRenderBoundsForGuid(target->getGuid(), boundsCenter, boundsRadius)) {
|
|
|
|
|
float r = boundsRadius * 1.1f;
|
|
|
|
|
circleRadius = std::min(std::max(r, 0.8f), 8.0f);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-06 13:47:03 -08:00
|
|
|
if (target->getType() == game::ObjectType::UNIT) {
|
2026-02-21 02:52:05 -08:00
|
|
|
showSelectionCircle = true;
|
2026-02-06 13:47:03 -08:00
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(target);
|
|
|
|
|
if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) {
|
|
|
|
|
circleColor = glm::vec3(0.5f, 0.5f, 0.5f); // gray (dead)
|
2026-02-06 18:34:45 -08:00
|
|
|
} else if (unit->isHostile() || gameHandler.isAggressiveTowardPlayer(target->getGuid())) {
|
2026-02-06 16:47:07 -08:00
|
|
|
uint32_t playerLv = gameHandler.getPlayerLevel();
|
|
|
|
|
uint32_t mobLv = unit->getLevel();
|
|
|
|
|
int32_t diff = static_cast<int32_t>(mobLv) - static_cast<int32_t>(playerLv);
|
|
|
|
|
if (game::GameHandler::killXp(playerLv, mobLv) == 0) {
|
|
|
|
|
circleColor = glm::vec3(0.6f, 0.6f, 0.6f); // grey
|
|
|
|
|
} else if (diff >= 10) {
|
|
|
|
|
circleColor = glm::vec3(1.0f, 0.1f, 0.1f); // red
|
|
|
|
|
} else if (diff >= 5) {
|
|
|
|
|
circleColor = glm::vec3(1.0f, 0.5f, 0.1f); // orange
|
|
|
|
|
} else if (diff >= -2) {
|
|
|
|
|
circleColor = glm::vec3(1.0f, 1.0f, 0.1f); // yellow
|
|
|
|
|
} else {
|
|
|
|
|
circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green
|
|
|
|
|
}
|
2026-02-06 14:24:38 -08:00
|
|
|
} else {
|
|
|
|
|
circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green (friendly)
|
2026-02-06 13:47:03 -08:00
|
|
|
}
|
|
|
|
|
} else if (target->getType() == game::ObjectType::PLAYER) {
|
2026-02-21 02:52:05 -08:00
|
|
|
showSelectionCircle = true;
|
2026-02-06 13:47:03 -08:00
|
|
|
circleColor = glm::vec3(0.3f, 1.0f, 0.3f); // green (player)
|
|
|
|
|
}
|
2026-02-21 02:52:05 -08:00
|
|
|
if (showSelectionCircle) {
|
|
|
|
|
renderer->setSelectionCircle(targetGLPos, circleRadius, circleColor);
|
|
|
|
|
} else {
|
|
|
|
|
renderer->clearSelectionCircle();
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
} else {
|
|
|
|
|
renderer->setTargetPosition(nullptr);
|
2026-02-06 13:47:03 -08:00
|
|
|
renderer->clearSelectionCircle();
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
renderer->setTargetPosition(nullptr);
|
2026-02-06 13:47:03 -08:00
|
|
|
renderer->clearSelectionCircle();
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-06 20:19:39 -08:00
|
|
|
|
2026-03-11 22:57:04 -07:00
|
|
|
// Screen edge damage flash — red vignette that fires on HP decrease
|
|
|
|
|
{
|
|
|
|
|
auto playerEntity = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid());
|
|
|
|
|
uint32_t currentHp = 0;
|
|
|
|
|
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)
|
|
|
|
|
currentHp = unit->getHealth();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Detect HP drop (ignore transitions from 0 — entity just spawned or uninitialized)
|
2026-03-31 10:07:58 +03:00
|
|
|
if (settingsPanel_.damageFlashEnabled_ && lastPlayerHp_ > 0 && currentHp < lastPlayerHp_ && currentHp > 0)
|
2026-03-11 22:57:04 -07:00
|
|
|
damageFlashAlpha_ = 1.0f;
|
|
|
|
|
lastPlayerHp_ = currentHp;
|
|
|
|
|
|
|
|
|
|
// Fade out over ~0.5 seconds
|
|
|
|
|
if (damageFlashAlpha_ > 0.0f) {
|
|
|
|
|
damageFlashAlpha_ -= ImGui::GetIO().DeltaTime * 2.0f;
|
|
|
|
|
if (damageFlashAlpha_ < 0.0f) damageFlashAlpha_ = 0.0f;
|
|
|
|
|
|
|
|
|
|
// Draw four red gradient rectangles along each screen edge (vignette style)
|
|
|
|
|
ImDrawList* fg = ImGui::GetForegroundDrawList();
|
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
|
const float W = io.DisplaySize.x;
|
|
|
|
|
const float H = io.DisplaySize.y;
|
2026-03-12 03:24:25 -07:00
|
|
|
const int alpha = static_cast<int>(damageFlashAlpha_ * 100.0f);
|
2026-03-11 22:57:04 -07:00
|
|
|
const ImU32 edgeCol = IM_COL32(200, 0, 0, alpha);
|
|
|
|
|
const ImU32 fadeCol = IM_COL32(200, 0, 0, 0);
|
|
|
|
|
const float thickness = std::min(W, H) * 0.12f;
|
|
|
|
|
|
|
|
|
|
// Top
|
|
|
|
|
fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(W, thickness),
|
|
|
|
|
edgeCol, edgeCol, fadeCol, fadeCol);
|
|
|
|
|
// Bottom
|
|
|
|
|
fg->AddRectFilledMultiColor(ImVec2(0, H - thickness), ImVec2(W, H),
|
|
|
|
|
fadeCol, fadeCol, edgeCol, edgeCol);
|
|
|
|
|
// Left
|
|
|
|
|
fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(thickness, H),
|
|
|
|
|
edgeCol, fadeCol, fadeCol, edgeCol);
|
|
|
|
|
// Right
|
|
|
|
|
fg->AddRectFilledMultiColor(ImVec2(W - thickness, 0), ImVec2(W, H),
|
|
|
|
|
fadeCol, edgeCol, edgeCol, fadeCol);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-12 07:07:46 -07:00
|
|
|
// Persistent low-health vignette — pulsing red edges when HP < 20%
|
|
|
|
|
{
|
|
|
|
|
auto playerEntity = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid());
|
|
|
|
|
bool isDead = gameHandler.isPlayerDead();
|
|
|
|
|
float hpPct = 1.0f;
|
|
|
|
|
if (!isDead && playerEntity &&
|
|
|
|
|
(playerEntity->getType() == game::ObjectType::PLAYER ||
|
|
|
|
|
playerEntity->getType() == game::ObjectType::UNIT)) {
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(playerEntity);
|
|
|
|
|
if (unit->getMaxHealth() > 0)
|
|
|
|
|
hpPct = static_cast<float>(unit->getHealth()) / static_cast<float>(unit->getMaxHealth());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Only show when alive and below 20% HP; intensity increases as HP drops
|
2026-03-31 10:07:58 +03:00
|
|
|
if (settingsPanel_.lowHealthVignetteEnabled_ && !isDead && hpPct < 0.20f && hpPct > 0.0f) {
|
2026-03-12 07:07:46 -07:00
|
|
|
// Base intensity from HP deficit (0 at 20%, 1 at 0%); pulse at ~1.5 Hz
|
|
|
|
|
float danger = (0.20f - hpPct) / 0.20f;
|
|
|
|
|
float pulse = 0.55f + 0.45f * std::sin(static_cast<float>(ImGui::GetTime()) * 9.4f);
|
|
|
|
|
int alpha = static_cast<int>(danger * pulse * 90.0f); // max ~90 alpha, subtle
|
|
|
|
|
if (alpha > 0) {
|
|
|
|
|
ImDrawList* fg = ImGui::GetForegroundDrawList();
|
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
|
const float W = io.DisplaySize.x;
|
|
|
|
|
const float H = io.DisplaySize.y;
|
|
|
|
|
const float thickness = std::min(W, H) * 0.15f;
|
|
|
|
|
const ImU32 edgeCol = IM_COL32(200, 0, 0, alpha);
|
|
|
|
|
const ImU32 fadeCol = IM_COL32(200, 0, 0, 0);
|
|
|
|
|
fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(W, thickness),
|
|
|
|
|
edgeCol, edgeCol, fadeCol, fadeCol);
|
|
|
|
|
fg->AddRectFilledMultiColor(ImVec2(0, H - thickness), ImVec2(W, H),
|
|
|
|
|
fadeCol, fadeCol, edgeCol, edgeCol);
|
|
|
|
|
fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(thickness, H),
|
|
|
|
|
edgeCol, fadeCol, fadeCol, edgeCol);
|
|
|
|
|
fg->AddRectFilledMultiColor(ImVec2(W - thickness, 0), ImVec2(W, H),
|
|
|
|
|
fadeCol, edgeCol, edgeCol, fadeCol);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 23:10:21 -07:00
|
|
|
// Level-up golden burst overlay
|
2026-03-31 09:18:17 +03:00
|
|
|
if (toastManager_.levelUpFlashAlpha > 0.0f) {
|
|
|
|
|
toastManager_.levelUpFlashAlpha -= ImGui::GetIO().DeltaTime * 1.0f; // fade over ~1 second
|
|
|
|
|
if (toastManager_.levelUpFlashAlpha < 0.0f) toastManager_.levelUpFlashAlpha = 0.0f;
|
2026-03-11 23:10:21 -07:00
|
|
|
|
|
|
|
|
ImDrawList* fg = ImGui::GetForegroundDrawList();
|
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
|
const float W = io.DisplaySize.x;
|
|
|
|
|
const float H = io.DisplaySize.y;
|
2026-03-31 09:18:17 +03:00
|
|
|
const int alpha = static_cast<int>(toastManager_.levelUpFlashAlpha * 160.0f);
|
2026-03-11 23:10:21 -07:00
|
|
|
const ImU32 goldEdge = IM_COL32(255, 210, 50, alpha);
|
|
|
|
|
const ImU32 goldFade = IM_COL32(255, 210, 50, 0);
|
|
|
|
|
const float thickness = std::min(W, H) * 0.18f;
|
|
|
|
|
|
|
|
|
|
// Four golden gradient edges
|
|
|
|
|
fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(W, thickness),
|
|
|
|
|
goldEdge, goldEdge, goldFade, goldFade);
|
|
|
|
|
fg->AddRectFilledMultiColor(ImVec2(0, H - thickness), ImVec2(W, H),
|
|
|
|
|
goldFade, goldFade, goldEdge, goldEdge);
|
|
|
|
|
fg->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(thickness, H),
|
|
|
|
|
goldEdge, goldFade, goldFade, goldEdge);
|
|
|
|
|
fg->AddRectFilledMultiColor(ImVec2(W - thickness, 0), ImVec2(W, H),
|
|
|
|
|
goldFade, goldEdge, goldEdge, goldFade);
|
|
|
|
|
|
|
|
|
|
// "Level X!" text in the center during the first half of the animation
|
2026-03-31 09:18:17 +03:00
|
|
|
if (toastManager_.levelUpFlashAlpha > 0.5f && toastManager_.levelUpDisplayLevel > 0) {
|
2026-03-11 23:10:21 -07:00
|
|
|
char lvlText[32];
|
2026-03-31 09:18:17 +03:00
|
|
|
snprintf(lvlText, sizeof(lvlText), "Level %u!", toastManager_.levelUpDisplayLevel);
|
2026-03-11 23:10:21 -07:00
|
|
|
ImVec2 ts = ImGui::CalcTextSize(lvlText);
|
|
|
|
|
float tx = (W - ts.x) * 0.5f;
|
|
|
|
|
float ty = H * 0.35f;
|
|
|
|
|
// Large shadow + bright gold text
|
|
|
|
|
fg->AddText(nullptr, 28.0f, ImVec2(tx + 2, ty + 2), IM_COL32(0, 0, 0, alpha), lvlText);
|
|
|
|
|
fg->AddText(nullptr, 28.0f, ImVec2(tx, ty), IM_COL32(255, 230, 80, alpha), lvlText);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-06 20:19:39 -08:00
|
|
|
// Restore previous alpha
|
|
|
|
|
ImGui::GetStyle().Alpha = prevAlpha;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderPlayerInfo(game::GameHandler& gameHandler) {
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(350, 250), ImGuiCond_FirstUseEver);
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(10, 30), ImGuiCond_FirstUseEver);
|
|
|
|
|
ImGui::Begin("Player Info", &showPlayerInfo);
|
|
|
|
|
|
|
|
|
|
const auto& movement = gameHandler.getMovementInfo();
|
|
|
|
|
|
|
|
|
|
ImGui::Text("Position & Movement");
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
// Position
|
|
|
|
|
ImGui::Text("Position:");
|
|
|
|
|
ImGui::Indent();
|
|
|
|
|
ImGui::Text("X: %.2f", movement.x);
|
|
|
|
|
ImGui::Text("Y: %.2f", movement.y);
|
|
|
|
|
ImGui::Text("Z: %.2f", movement.z);
|
|
|
|
|
ImGui::Text("Orientation: %.2f rad (%.1f deg)", movement.orientation, movement.orientation * 180.0f / 3.14159f);
|
|
|
|
|
ImGui::Unindent();
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
// Movement flags
|
|
|
|
|
ImGui::Text("Movement Flags: 0x%08X", movement.flags);
|
|
|
|
|
ImGui::Text("Time: %u ms", movement.time);
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
// Connection state
|
|
|
|
|
ImGui::Text("Connection State:");
|
|
|
|
|
ImGui::Indent();
|
|
|
|
|
auto state = gameHandler.getState();
|
|
|
|
|
switch (state) {
|
|
|
|
|
case game::WorldState::IN_WORLD:
|
2026-03-25 12:12:03 -07:00
|
|
|
ImGui::TextColored(kColorBrightGreen, "In World");
|
2026-02-02 12:24:50 -08:00
|
|
|
break;
|
|
|
|
|
case game::WorldState::AUTHENTICATED:
|
2026-03-25 11:57:22 -07:00
|
|
|
ImGui::TextColored(kColorYellow, "Authenticated");
|
2026-02-02 12:24:50 -08:00
|
|
|
break;
|
|
|
|
|
case game::WorldState::ENTERING_WORLD:
|
2026-03-25 11:57:22 -07:00
|
|
|
ImGui::TextColored(kColorYellow, "Entering World...");
|
2026-02-02 12:24:50 -08:00
|
|
|
break;
|
|
|
|
|
default:
|
2026-03-25 11:57:22 -07:00
|
|
|
ImGui::TextColored(kColorRed, "State: %d", static_cast<int>(state));
|
2026-02-02 12:24:50 -08:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
ImGui::Unindent();
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderEntityList(game::GameHandler& gameHandler) {
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(500, 400), ImGuiCond_FirstUseEver);
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(10, 290), ImGuiCond_FirstUseEver);
|
|
|
|
|
ImGui::Begin("Entities", &showEntityWindow);
|
|
|
|
|
|
|
|
|
|
const auto& entityManager = gameHandler.getEntityManager();
|
|
|
|
|
const auto& entities = entityManager.getEntities();
|
|
|
|
|
|
|
|
|
|
ImGui::Text("Entities in View: %zu", entities.size());
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
if (entities.empty()) {
|
|
|
|
|
ImGui::TextDisabled("No entities in view");
|
|
|
|
|
} else {
|
|
|
|
|
// Entity table
|
|
|
|
|
if (ImGui::BeginTable("EntitiesTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) {
|
|
|
|
|
ImGui::TableSetupColumn("GUID", ImGuiTableColumnFlags_WidthFixed, 140.0f);
|
|
|
|
|
ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 100.0f);
|
|
|
|
|
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
|
ImGui::TableSetupColumn("Position", ImGuiTableColumnFlags_WidthFixed, 150.0f);
|
|
|
|
|
ImGui::TableSetupColumn("Distance", ImGuiTableColumnFlags_WidthFixed, 80.0f);
|
|
|
|
|
ImGui::TableHeadersRow();
|
|
|
|
|
|
|
|
|
|
const auto& playerMovement = gameHandler.getMovementInfo();
|
|
|
|
|
float playerX = playerMovement.x;
|
|
|
|
|
float playerY = playerMovement.y;
|
|
|
|
|
float playerZ = playerMovement.z;
|
|
|
|
|
|
|
|
|
|
for (const auto& [guid, entity] : entities) {
|
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
|
|
|
|
// GUID
|
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
2026-02-04 11:31:08 -08:00
|
|
|
char guidStr[24];
|
|
|
|
|
snprintf(guidStr, sizeof(guidStr), "0x%016llX", (unsigned long long)guid);
|
|
|
|
|
ImGui::Text("%s", guidStr);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Type
|
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
|
|
|
switch (entity->getType()) {
|
|
|
|
|
case game::ObjectType::PLAYER:
|
2026-03-25 12:12:03 -07:00
|
|
|
ImGui::TextColored(kColorBrightGreen, "Player");
|
2026-02-02 12:24:50 -08:00
|
|
|
break;
|
|
|
|
|
case game::ObjectType::UNIT:
|
2026-03-25 11:57:22 -07:00
|
|
|
ImGui::TextColored(kColorYellow, "Unit");
|
2026-02-02 12:24:50 -08:00
|
|
|
break;
|
|
|
|
|
case game::ObjectType::GAMEOBJECT:
|
|
|
|
|
ImGui::TextColored(ImVec4(0.3f, 0.8f, 1.0f, 1.0f), "GameObject");
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
ImGui::Text("Object");
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Name (for players and units)
|
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
|
|
|
|
if (entity->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
auto player = std::static_pointer_cast<game::Player>(entity);
|
|
|
|
|
ImGui::Text("%s", player->getName().c_str());
|
|
|
|
|
} else if (entity->getType() == game::ObjectType::UNIT) {
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
|
|
|
if (!unit->getName().empty()) {
|
|
|
|
|
ImGui::Text("%s", unit->getName().c_str());
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::TextDisabled("--");
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::TextDisabled("--");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Position
|
|
|
|
|
ImGui::TableSetColumnIndex(3);
|
|
|
|
|
ImGui::Text("%.1f, %.1f, %.1f", entity->getX(), entity->getY(), entity->getZ());
|
|
|
|
|
|
|
|
|
|
// Distance from player
|
|
|
|
|
ImGui::TableSetColumnIndex(4);
|
|
|
|
|
float dx = entity->getX() - playerX;
|
|
|
|
|
float dy = entity->getY() - playerY;
|
|
|
|
|
float dz = entity->getZ() - playerZ;
|
|
|
|
|
float distance = std::sqrt(dx*dx + dy*dy + dz*dz);
|
|
|
|
|
ImGui::Text("%.1f", distance);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
void GameScreen::processTargetInput(game::GameHandler& gameHandler) {
|
|
|
|
|
auto& io = ImGui::GetIO();
|
|
|
|
|
auto& input = core::Input::getInstance();
|
|
|
|
|
|
|
|
|
|
// If the user is typing (or about to focus chat this frame), do not allow
|
|
|
|
|
// A-Z or 1-0 shortcuts to fire.
|
|
|
|
|
if (!io.WantTextInput && !chatPanel_.isChatInputActive() && input.isKeyJustPressed(SDL_SCANCODE_SLASH)) {
|
|
|
|
|
chatPanel_.activateSlashInput();
|
2026-02-06 18:34:45 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
if (!io.WantTextInput && !chatPanel_.isChatInputActive() &&
|
|
|
|
|
KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHAT, true)) {
|
|
|
|
|
chatPanel_.activateInput();
|
2026-02-07 21:12:54 -08:00
|
|
|
}
|
2026-02-06 18:34:45 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
const bool textFocus = chatPanel_.isChatInputActive() || io.WantTextInput;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// Tab targeting (when keyboard not captured by UI)
|
|
|
|
|
if (!io.WantCaptureKeyboard) {
|
|
|
|
|
// When typing in chat (or any text input), never treat keys as gameplay/UI shortcuts.
|
|
|
|
|
if (!textFocus && input.isKeyJustPressed(SDL_SCANCODE_TAB)) {
|
|
|
|
|
const auto& movement = gameHandler.getMovementInfo();
|
|
|
|
|
gameHandler.tabTarget(movement.x, movement.y, movement.z);
|
2026-02-14 14:30:09 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_SETTINGS, true)) {
|
2026-03-31 10:07:58 +03:00
|
|
|
if (settingsPanel_.showSettingsWindow) {
|
|
|
|
|
settingsPanel_.showSettingsWindow = false;
|
2026-03-31 19:49:52 +03:00
|
|
|
} else if (windowManager_.showEscapeMenu) {
|
|
|
|
|
windowManager_.showEscapeMenu = false;
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.showEscapeSettingsNotice = false;
|
2026-03-31 08:53:14 +03:00
|
|
|
} else if (gameHandler.isCasting()) {
|
|
|
|
|
gameHandler.cancelCast();
|
|
|
|
|
} else if (gameHandler.isLootWindowOpen()) {
|
|
|
|
|
gameHandler.closeLoot();
|
|
|
|
|
} else if (gameHandler.isGossipWindowOpen()) {
|
|
|
|
|
gameHandler.closeGossip();
|
|
|
|
|
} else if (gameHandler.isVendorWindowOpen()) {
|
|
|
|
|
gameHandler.closeVendor();
|
|
|
|
|
} else if (gameHandler.isBarberShopOpen()) {
|
|
|
|
|
gameHandler.closeBarberShop();
|
|
|
|
|
} else if (gameHandler.isBankOpen()) {
|
|
|
|
|
gameHandler.closeBank();
|
|
|
|
|
} else if (gameHandler.isTrainerWindowOpen()) {
|
|
|
|
|
gameHandler.closeTrainer();
|
|
|
|
|
} else if (gameHandler.isMailboxOpen()) {
|
|
|
|
|
gameHandler.closeMailbox();
|
|
|
|
|
} else if (gameHandler.isAuctionHouseOpen()) {
|
|
|
|
|
gameHandler.closeAuctionHouse();
|
|
|
|
|
} else if (gameHandler.isQuestDetailsOpen()) {
|
|
|
|
|
gameHandler.declineQuest();
|
|
|
|
|
} else if (gameHandler.isQuestOfferRewardOpen()) {
|
|
|
|
|
gameHandler.closeQuestOfferReward();
|
|
|
|
|
} else if (gameHandler.isQuestRequestItemsOpen()) {
|
|
|
|
|
gameHandler.closeQuestRequestItems();
|
|
|
|
|
} else if (gameHandler.isTradeOpen()) {
|
|
|
|
|
gameHandler.cancelTrade();
|
2026-03-31 19:49:52 +03:00
|
|
|
} else if (socialPanel_.showWhoWindow_) {
|
|
|
|
|
socialPanel_.showWhoWindow_ = false;
|
|
|
|
|
} else if (combatUI_.showCombatLog_) {
|
|
|
|
|
combatUI_.showCombatLog_ = false;
|
|
|
|
|
} else if (socialPanel_.showSocialFrame_) {
|
|
|
|
|
socialPanel_.showSocialFrame_ = false;
|
2026-03-31 08:53:14 +03:00
|
|
|
} else if (talentScreen.isOpen()) {
|
|
|
|
|
talentScreen.setOpen(false);
|
|
|
|
|
} else if (spellbookScreen.isOpen()) {
|
|
|
|
|
spellbookScreen.setOpen(false);
|
|
|
|
|
} else if (questLogScreen.isOpen()) {
|
|
|
|
|
questLogScreen.setOpen(false);
|
|
|
|
|
} else if (inventoryScreen.isCharacterOpen()) {
|
|
|
|
|
inventoryScreen.toggleCharacter();
|
|
|
|
|
} else if (inventoryScreen.isOpen()) {
|
|
|
|
|
inventoryScreen.setOpen(false);
|
|
|
|
|
} else if (showWorldMap_) {
|
|
|
|
|
showWorldMap_ = false;
|
|
|
|
|
} else {
|
2026-03-31 19:49:52 +03:00
|
|
|
windowManager_.showEscapeMenu = true;
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (!textFocus) {
|
|
|
|
|
// Toggle character screen (C) and inventory/bags (I)
|
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_CHARACTER_SCREEN)) {
|
|
|
|
|
const bool wasOpen = inventoryScreen.isCharacterOpen();
|
|
|
|
|
inventoryScreen.toggleCharacter();
|
|
|
|
|
if (!wasOpen && gameHandler.isConnected()) {
|
|
|
|
|
gameHandler.requestPlayedTime();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-14 15:58:54 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_INVENTORY)) {
|
|
|
|
|
inventoryScreen.toggle();
|
2026-02-19 01:50:50 -08:00
|
|
|
}
|
2026-02-14 15:58:54 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_NAMEPLATES)) {
|
|
|
|
|
if (ImGui::GetIO().KeyShift)
|
2026-03-31 10:07:58 +03:00
|
|
|
settingsPanel_.showFriendlyNameplates_ = !settingsPanel_.showFriendlyNameplates_;
|
2026-02-14 15:58:54 -08:00
|
|
|
else
|
2026-03-31 08:53:14 +03:00
|
|
|
showNameplates_ = !showNameplates_;
|
2026-02-14 15:58:54 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
|
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_WORLD_MAP)) {
|
|
|
|
|
showWorldMap_ = !showWorldMap_;
|
2026-02-18 03:46:03 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
|
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_MINIMAP)) {
|
|
|
|
|
showMinimap_ = !showMinimap_;
|
2026-03-17 14:42:00 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
|
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_RAID_FRAMES)) {
|
2026-03-31 19:49:52 +03:00
|
|
|
socialPanel_.showRaidFrames_ = !socialPanel_.showRaidFrames_;
|
2026-03-17 16:42:19 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
|
|
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_ACHIEVEMENTS)) {
|
2026-03-31 19:49:52 +03:00
|
|
|
windowManager_.showAchievementWindow_ = !windowManager_.showAchievementWindow_;
|
2026-03-17 16:42:19 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
if (KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_SKILLS)) {
|
2026-03-31 19:49:52 +03:00
|
|
|
windowManager_.showSkillsWindow_ = !windowManager_.showSkillsWindow_;
|
2026-03-17 16:43:57 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
|
|
|
|
|
// Toggle Titles window with H (hero/title screen — no conflicting keybinding)
|
|
|
|
|
if (input.isKeyJustPressed(SDL_SCANCODE_H) && !ImGui::GetIO().WantCaptureKeyboard) {
|
2026-03-31 19:49:52 +03:00
|
|
|
windowManager_.showTitlesWindow_ = !windowManager_.showTitlesWindow_;
|
2026-03-17 16:47:33 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
|
|
|
|
|
// Screenshot (PrintScreen key)
|
|
|
|
|
if (input.isKeyJustPressed(SDL_SCANCODE_PRINTSCREEN)) {
|
|
|
|
|
takeScreenshot(gameHandler);
|
2026-03-17 16:47:33 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
|
|
|
|
|
// Action bar keys (1-9, 0, -, =)
|
|
|
|
|
static const SDL_Scancode actionBarKeys[] = {
|
|
|
|
|
SDL_SCANCODE_1, SDL_SCANCODE_2, SDL_SCANCODE_3, SDL_SCANCODE_4,
|
|
|
|
|
SDL_SCANCODE_5, SDL_SCANCODE_6, SDL_SCANCODE_7, SDL_SCANCODE_8,
|
|
|
|
|
SDL_SCANCODE_9, SDL_SCANCODE_0, SDL_SCANCODE_MINUS, SDL_SCANCODE_EQUALS
|
2026-03-17 16:47:33 -07:00
|
|
|
};
|
2026-03-31 08:53:14 +03:00
|
|
|
const bool shiftDown = input.isKeyPressed(SDL_SCANCODE_LSHIFT) || input.isKeyPressed(SDL_SCANCODE_RSHIFT);
|
|
|
|
|
const bool ctrlDown = input.isKeyPressed(SDL_SCANCODE_LCTRL) || input.isKeyPressed(SDL_SCANCODE_RCTRL);
|
|
|
|
|
const auto& bar = gameHandler.getActionBar();
|
2026-02-19 01:50:50 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// Ctrl+1..Ctrl+8 → switch stance/form/presence (WoW default bindings).
|
|
|
|
|
// Only fires for classes that use a stance bar; same slot ordering as
|
|
|
|
|
// renderStanceBar: Warrior, DK, Druid, Rogue, Priest.
|
|
|
|
|
if (ctrlDown) {
|
|
|
|
|
static const uint32_t warriorStances[] = { 2457, 71, 2458 };
|
|
|
|
|
static const uint32_t dkPresences[] = { 48266, 48263, 48265 };
|
|
|
|
|
static const uint32_t druidForms[] = { 5487, 9634, 768, 783, 1066, 24858, 33891, 33943, 40120 };
|
|
|
|
|
static const uint32_t rogueForms[] = { 1784 };
|
|
|
|
|
static const uint32_t priestForms[] = { 15473 };
|
|
|
|
|
const uint32_t* stArr = nullptr; int stCnt = 0;
|
|
|
|
|
switch (gameHandler.getPlayerClass()) {
|
|
|
|
|
case 1: stArr = warriorStances; stCnt = 3; break;
|
|
|
|
|
case 6: stArr = dkPresences; stCnt = 3; break;
|
|
|
|
|
case 11: stArr = druidForms; stCnt = 9; break;
|
|
|
|
|
case 4: stArr = rogueForms; stCnt = 1; break;
|
|
|
|
|
case 5: stArr = priestForms; stCnt = 1; break;
|
2026-02-19 01:50:50 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
if (stArr) {
|
|
|
|
|
const auto& known = gameHandler.getKnownSpells();
|
|
|
|
|
// Build available list (same order as UI)
|
|
|
|
|
std::vector<uint32_t> avail;
|
|
|
|
|
avail.reserve(stCnt);
|
|
|
|
|
for (int i = 0; i < stCnt; ++i)
|
|
|
|
|
if (known.count(stArr[i])) avail.push_back(stArr[i]);
|
|
|
|
|
// Ctrl+1 = first stance, Ctrl+2 = second, …
|
|
|
|
|
for (int i = 0; i < static_cast<int>(avail.size()) && i < 8; ++i) {
|
|
|
|
|
if (input.isKeyJustPressed(actionBarKeys[i]))
|
|
|
|
|
gameHandler.castSpell(avail[i]);
|
2026-03-17 14:50:28 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-19 01:50:50 -08:00
|
|
|
}
|
2026-02-14 15:58:54 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
for (int i = 0; i < game::GameHandler::SLOTS_PER_BAR; ++i) {
|
|
|
|
|
if (!ctrlDown && input.isKeyJustPressed(actionBarKeys[i])) {
|
|
|
|
|
int slotIdx = shiftDown ? (game::GameHandler::SLOTS_PER_BAR + i) : i;
|
|
|
|
|
if (bar[slotIdx].type == game::ActionBarSlot::SPELL && bar[slotIdx].isReady()) {
|
|
|
|
|
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
|
|
|
|
gameHandler.castSpell(bar[slotIdx].id, target);
|
|
|
|
|
} else if (bar[slotIdx].type == game::ActionBarSlot::ITEM && bar[slotIdx].id != 0) {
|
|
|
|
|
gameHandler.useItemById(bar[slotIdx].id);
|
|
|
|
|
} else if (bar[slotIdx].type == game::ActionBarSlot::MACRO) {
|
|
|
|
|
chatPanel_.executeMacroText(gameHandler, inventoryScreen, spellbookScreen, questLogScreen, gameHandler.getMacroText(bar[slotIdx].id));
|
|
|
|
|
}
|
2026-02-07 23:32:27 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
2026-02-07 23:32:27 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
2026-03-11 21:03:51 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// Cursor affordance: show hand cursor over interactable entities.
|
|
|
|
|
if (!io.WantCaptureMouse) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-03-31 08:53:14 +03:00
|
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* window = services_.window;
|
2026-03-31 08:53:14 +03:00
|
|
|
if (camera && window) {
|
|
|
|
|
glm::vec2 mousePos = input.getMousePosition();
|
|
|
|
|
float screenW = static_cast<float>(window->getWidth());
|
|
|
|
|
float screenH = static_cast<float>(window->getHeight());
|
|
|
|
|
rendering::Ray ray = camera->screenToWorldRay(mousePos.x, mousePos.y, screenW, screenH);
|
|
|
|
|
float closestT = 1e30f;
|
|
|
|
|
bool hoverInteractable = false;
|
|
|
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
bool isGo = (entity->getType() == game::ObjectType::GAMEOBJECT);
|
|
|
|
|
bool isUnit = (entity->getType() == game::ObjectType::UNIT);
|
|
|
|
|
bool isPlayer = (entity->getType() == game::ObjectType::PLAYER);
|
|
|
|
|
if (!isGo && !isUnit && !isPlayer) continue;
|
|
|
|
|
if (guid == gameHandler.getPlayerGuid()) continue; // skip self
|
2026-02-14 15:58:54 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
glm::vec3 hitCenter;
|
|
|
|
|
float hitRadius = 0.0f;
|
|
|
|
|
bool hasBounds = core::Application::getInstance().getRenderBoundsForGuid(guid, hitCenter, hitRadius);
|
|
|
|
|
if (!hasBounds) {
|
|
|
|
|
hitRadius = isGo ? 2.5f : 1.8f;
|
|
|
|
|
hitCenter = core::coords::canonicalToRender(glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
|
|
|
|
hitCenter.z += isGo ? 1.2f : 1.0f;
|
2026-02-14 16:42:47 -08:00
|
|
|
} else {
|
2026-03-31 08:53:14 +03:00
|
|
|
hitRadius = std::max(hitRadius * 1.1f, 0.8f);
|
2026-02-14 15:58:54 -08:00
|
|
|
}
|
2026-02-07 23:32:27 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
float hitT;
|
|
|
|
|
if (raySphereIntersect(ray, hitCenter, hitRadius, hitT) && hitT < closestT) {
|
|
|
|
|
closestT = hitT;
|
|
|
|
|
hoverInteractable = true;
|
2026-03-12 06:45:27 -07:00
|
|
|
}
|
2026-03-11 23:49:37 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
if (hoverInteractable) {
|
|
|
|
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
2026-03-11 22:00:30 -07:00
|
|
|
}
|
2026-03-11 23:24:27 -07:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// Left-click targeting: only on mouse-up if the mouse didn't drag (camera rotate)
|
|
|
|
|
// Record press position on mouse-down
|
|
|
|
|
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_LEFT) && !input.isMouseButtonPressed(SDL_BUTTON_RIGHT)) {
|
|
|
|
|
leftClickPressPos_ = input.getMousePosition();
|
|
|
|
|
leftClickWasPress_ = true;
|
2026-03-11 23:24:27 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// On mouse-up, check if it was a click (not a drag)
|
|
|
|
|
if (leftClickWasPress_ && input.isMouseButtonJustReleased(SDL_BUTTON_LEFT)) {
|
|
|
|
|
leftClickWasPress_ = false;
|
|
|
|
|
glm::vec2 releasePos = input.getMousePosition();
|
|
|
|
|
glm::vec2 dragDelta = releasePos - leftClickPressPos_;
|
|
|
|
|
float dragDistSq = glm::dot(dragDelta, dragDelta);
|
|
|
|
|
constexpr float CLICK_THRESHOLD = 5.0f; // pixels
|
2026-02-07 12:30:36 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (dragDistSq < CLICK_THRESHOLD * CLICK_THRESHOLD) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-03-31 08:53:14 +03:00
|
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* window = services_.window;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (camera && window) {
|
|
|
|
|
float screenW = static_cast<float>(window->getWidth());
|
|
|
|
|
float screenH = static_cast<float>(window->getHeight());
|
2026-03-12 11:16:42 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
rendering::Ray ray = camera->screenToWorldRay(leftClickPressPos_.x, leftClickPressPos_.y, screenW, screenH);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
float closestT = 1e30f;
|
|
|
|
|
uint64_t closestGuid = 0;
|
|
|
|
|
float closestHostileUnitT = 1e30f;
|
|
|
|
|
uint64_t closestHostileUnitGuid = 0;
|
2026-02-07 23:32:27 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
const uint64_t myGuid = gameHandler.getPlayerGuid();
|
|
|
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
auto t = entity->getType();
|
|
|
|
|
if (t != game::ObjectType::UNIT &&
|
|
|
|
|
t != game::ObjectType::PLAYER &&
|
|
|
|
|
t != game::ObjectType::GAMEOBJECT) continue;
|
|
|
|
|
if (guid == myGuid) continue; // Don't target self
|
2026-03-12 06:38:10 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
glm::vec3 hitCenter;
|
|
|
|
|
float hitRadius = 0.0f;
|
|
|
|
|
bool hasBounds = core::Application::getInstance().getRenderBoundsForGuid(guid, hitCenter, hitRadius);
|
|
|
|
|
if (!hasBounds) {
|
|
|
|
|
// Fallback hitbox based on entity type
|
|
|
|
|
float heightOffset = 1.5f;
|
|
|
|
|
hitRadius = 1.5f;
|
|
|
|
|
if (t == game::ObjectType::UNIT) {
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
|
|
|
// Critters have very low max health (< 100)
|
|
|
|
|
if (unit->getMaxHealth() > 0 && unit->getMaxHealth() < 100) {
|
|
|
|
|
hitRadius = 0.5f;
|
|
|
|
|
heightOffset = 0.3f;
|
|
|
|
|
}
|
|
|
|
|
} else if (t == game::ObjectType::GAMEOBJECT) {
|
|
|
|
|
hitRadius = 2.5f;
|
|
|
|
|
heightOffset = 1.2f;
|
2026-03-21 03:49:02 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
hitCenter = core::coords::canonicalToRender(glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
|
|
|
|
hitCenter.z += heightOffset;
|
|
|
|
|
} else {
|
|
|
|
|
hitRadius = std::max(hitRadius * 1.1f, 0.6f);
|
2026-03-21 03:49:02 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
float hitT;
|
|
|
|
|
if (raySphereIntersect(ray, hitCenter, hitRadius, hitT)) {
|
|
|
|
|
if (t == game::ObjectType::UNIT) {
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
|
|
|
bool hostileUnit = unit->isHostile() || gameHandler.isAggressiveTowardPlayer(guid);
|
|
|
|
|
if (hostileUnit && hitT < closestHostileUnitT) {
|
|
|
|
|
closestHostileUnitT = hitT;
|
|
|
|
|
closestHostileUnitGuid = guid;
|
2026-03-21 03:49:02 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
if (hitT < closestT) {
|
|
|
|
|
closestT = hitT;
|
|
|
|
|
closestGuid = guid;
|
2026-03-21 03:49:02 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 06:38:10 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// Prefer hostile monsters over nearby gameobjects/others when both are hittable.
|
|
|
|
|
if (closestHostileUnitGuid != 0) {
|
|
|
|
|
closestGuid = closestHostileUnitGuid;
|
|
|
|
|
}
|
2026-03-11 23:06:24 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (closestGuid != 0) {
|
|
|
|
|
gameHandler.setTarget(closestGuid);
|
|
|
|
|
} else {
|
|
|
|
|
// Clicked empty space — deselect current target
|
|
|
|
|
gameHandler.clearTarget();
|
2026-03-11 23:06:24 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-11 23:06:24 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// Right-click: select NPC (if needed) then interact / loot / auto-attack
|
|
|
|
|
// Suppress when left button is held (both-button run)
|
|
|
|
|
if (!io.WantCaptureMouse && input.isMouseButtonJustPressed(SDL_BUTTON_RIGHT) && !input.isMouseButtonPressed(SDL_BUTTON_LEFT)) {
|
|
|
|
|
// If a gameobject is already targeted, prioritize interacting with that target
|
|
|
|
|
// instead of re-picking under cursor (which can hit nearby decorative GOs).
|
|
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
auto target = gameHandler.getTarget();
|
|
|
|
|
if (target && target->getType() == game::ObjectType::GAMEOBJECT) {
|
2026-03-31 19:51:37 +03:00
|
|
|
LOG_WARNING("[GO-DIAG] Right-click: re-interacting with targeted GO 0x",
|
|
|
|
|
std::hex, target->getGuid(), std::dec);
|
2026-03-31 08:53:14 +03:00
|
|
|
gameHandler.setTarget(target->getGuid());
|
|
|
|
|
gameHandler.interactWithGameObject(target->getGuid());
|
|
|
|
|
return;
|
2026-03-11 23:06:24 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-06 18:34:45 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// If no target or right-clicking in world, try to pick one under cursor
|
|
|
|
|
{
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-03-31 08:53:14 +03:00
|
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* window = services_.window;
|
2026-03-31 08:53:14 +03:00
|
|
|
if (camera && window) {
|
|
|
|
|
// If a quest objective gameobject is under the cursor, prefer it over
|
|
|
|
|
// hostile units so quest pickups (e.g. "Bundle of Wood") are reliable.
|
|
|
|
|
std::unordered_set<uint32_t> questObjectiveGoEntries;
|
|
|
|
|
{
|
|
|
|
|
const auto& ql = gameHandler.getQuestLog();
|
|
|
|
|
questObjectiveGoEntries.reserve(32);
|
|
|
|
|
for (const auto& q : ql) {
|
|
|
|
|
if (q.complete) continue;
|
|
|
|
|
for (const auto& obj : q.killObjectives) {
|
|
|
|
|
if (obj.npcOrGoId >= 0 || obj.required == 0) continue;
|
|
|
|
|
uint32_t entry = static_cast<uint32_t>(-obj.npcOrGoId);
|
|
|
|
|
uint32_t cur = 0;
|
|
|
|
|
auto it = q.killCounts.find(entry);
|
|
|
|
|
if (it != q.killCounts.end()) cur = it->second.first;
|
|
|
|
|
if (cur < obj.required) questObjectiveGoEntries.insert(entry);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 13:39:36 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
glm::vec2 mousePos = input.getMousePosition();
|
|
|
|
|
float screenW = static_cast<float>(window->getWidth());
|
|
|
|
|
float screenH = static_cast<float>(window->getHeight());
|
|
|
|
|
rendering::Ray ray = camera->screenToWorldRay(mousePos.x, mousePos.y, screenW, screenH);
|
|
|
|
|
float closestT = 1e30f;
|
|
|
|
|
uint64_t closestGuid = 0;
|
|
|
|
|
game::ObjectType closestType = game::ObjectType::OBJECT;
|
|
|
|
|
float closestHostileUnitT = 1e30f;
|
|
|
|
|
uint64_t closestHostileUnitGuid = 0;
|
|
|
|
|
float closestQuestGoT = 1e30f;
|
|
|
|
|
uint64_t closestQuestGoGuid = 0;
|
|
|
|
|
float closestGoT = 1e30f;
|
|
|
|
|
uint64_t closestGoGuid = 0;
|
|
|
|
|
const uint64_t myGuid = gameHandler.getPlayerGuid();
|
|
|
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
auto t = entity->getType();
|
|
|
|
|
if (t != game::ObjectType::UNIT &&
|
|
|
|
|
t != game::ObjectType::PLAYER &&
|
|
|
|
|
t != game::ObjectType::GAMEOBJECT)
|
|
|
|
|
continue;
|
|
|
|
|
if (guid == myGuid) continue;
|
2026-03-12 13:39:36 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
glm::vec3 hitCenter;
|
|
|
|
|
float hitRadius = 0.0f;
|
|
|
|
|
bool hasBounds = core::Application::getInstance().getRenderBoundsForGuid(guid, hitCenter, hitRadius);
|
|
|
|
|
if (!hasBounds) {
|
|
|
|
|
float heightOffset = 1.5f;
|
|
|
|
|
hitRadius = 1.5f;
|
|
|
|
|
if (t == game::ObjectType::UNIT) {
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
|
|
|
if (unit->getMaxHealth() > 0 && unit->getMaxHealth() < 100) {
|
|
|
|
|
hitRadius = 0.5f;
|
|
|
|
|
heightOffset = 0.3f;
|
|
|
|
|
}
|
|
|
|
|
} else if (t == game::ObjectType::GAMEOBJECT) {
|
|
|
|
|
hitRadius = 2.5f;
|
|
|
|
|
heightOffset = 1.2f;
|
|
|
|
|
}
|
|
|
|
|
hitCenter = core::coords::canonicalToRender(
|
|
|
|
|
glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
|
|
|
|
hitCenter.z += heightOffset;
|
2026-03-31 19:51:37 +03:00
|
|
|
// Log each unique GO's raypick position once
|
|
|
|
|
if (t == game::ObjectType::GAMEOBJECT) {
|
|
|
|
|
static std::unordered_set<uint64_t> goPickLog;
|
|
|
|
|
if (goPickLog.insert(guid).second) {
|
|
|
|
|
auto go = std::static_pointer_cast<game::GameObject>(entity);
|
|
|
|
|
LOG_WARNING("[GO-DIAG] Raypick GO: guid=0x", std::hex, guid, std::dec,
|
|
|
|
|
" entry=", go->getEntry(), " name='", go->getName(),
|
|
|
|
|
"' pos=(", entity->getX(), ",", entity->getY(), ",", entity->getZ(),
|
|
|
|
|
") center=(", hitCenter.x, ",", hitCenter.y, ",", hitCenter.z,
|
|
|
|
|
") r=", hitRadius);
|
|
|
|
|
}
|
2026-03-21 03:49:02 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
} else {
|
|
|
|
|
hitRadius = std::max(hitRadius * 1.1f, 0.6f);
|
|
|
|
|
}
|
2026-03-12 13:39:36 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
float hitT;
|
|
|
|
|
if (raySphereIntersect(ray, hitCenter, hitRadius, hitT)) {
|
|
|
|
|
if (t == game::ObjectType::UNIT) {
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
|
|
|
bool hostileUnit = unit->isHostile() || gameHandler.isAggressiveTowardPlayer(guid);
|
|
|
|
|
if (hostileUnit && hitT < closestHostileUnitT) {
|
|
|
|
|
closestHostileUnitT = hitT;
|
|
|
|
|
closestHostileUnitGuid = guid;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (t == game::ObjectType::GAMEOBJECT) {
|
|
|
|
|
if (hitT < closestGoT) {
|
|
|
|
|
closestGoT = hitT;
|
|
|
|
|
closestGoGuid = guid;
|
|
|
|
|
}
|
|
|
|
|
if (!questObjectiveGoEntries.empty()) {
|
|
|
|
|
auto go = std::static_pointer_cast<game::GameObject>(entity);
|
|
|
|
|
if (questObjectiveGoEntries.count(go->getEntry())) {
|
|
|
|
|
if (hitT < closestQuestGoT) {
|
|
|
|
|
closestQuestGoT = hitT;
|
|
|
|
|
closestQuestGoGuid = guid;
|
2026-03-12 13:39:36 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
if (hitT < closestT) {
|
|
|
|
|
closestT = hitT;
|
|
|
|
|
closestGuid = guid;
|
|
|
|
|
closestType = t;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-11 23:51:27 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// Priority: quest GO > closer of (GO, hostile unit) > closest anything.
|
|
|
|
|
if (closestQuestGoGuid != 0) {
|
|
|
|
|
closestGuid = closestQuestGoGuid;
|
|
|
|
|
closestType = game::ObjectType::GAMEOBJECT;
|
|
|
|
|
} else if (closestGoGuid != 0 && closestHostileUnitGuid != 0) {
|
|
|
|
|
// Both a GO and hostile unit were hit — prefer whichever is closer.
|
|
|
|
|
if (closestGoT <= closestHostileUnitT) {
|
|
|
|
|
closestGuid = closestGoGuid;
|
|
|
|
|
closestType = game::ObjectType::GAMEOBJECT;
|
|
|
|
|
} else {
|
|
|
|
|
closestGuid = closestHostileUnitGuid;
|
|
|
|
|
closestType = game::ObjectType::UNIT;
|
|
|
|
|
}
|
|
|
|
|
} else if (closestGoGuid != 0) {
|
|
|
|
|
closestGuid = closestGoGuid;
|
|
|
|
|
closestType = game::ObjectType::GAMEOBJECT;
|
|
|
|
|
} else if (closestHostileUnitGuid != 0) {
|
|
|
|
|
closestGuid = closestHostileUnitGuid;
|
|
|
|
|
closestType = game::ObjectType::UNIT;
|
2026-03-18 11:48:22 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
|
|
|
|
|
if (closestGuid != 0) {
|
|
|
|
|
if (closestType == game::ObjectType::GAMEOBJECT) {
|
2026-03-31 19:51:37 +03:00
|
|
|
LOG_WARNING("[GO-DIAG] Right-click: raypick hit GO 0x",
|
|
|
|
|
std::hex, closestGuid, std::dec);
|
2026-03-31 08:53:14 +03:00
|
|
|
gameHandler.setTarget(closestGuid);
|
|
|
|
|
gameHandler.interactWithGameObject(closestGuid);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
gameHandler.setTarget(closestGuid);
|
2026-03-18 11:48:22 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
if (gameHandler.hasTarget()) {
|
|
|
|
|
auto target = gameHandler.getTarget();
|
|
|
|
|
if (target) {
|
|
|
|
|
if (target->getType() == game::ObjectType::UNIT) {
|
|
|
|
|
// Check if unit is dead (health == 0) → loot, otherwise interact/attack
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(target);
|
|
|
|
|
if (unit->getHealth() == 0 && unit->getMaxHealth() > 0) {
|
|
|
|
|
gameHandler.lootTarget(target->getGuid());
|
|
|
|
|
} else {
|
|
|
|
|
// Interact with service NPCs; otherwise treat non-interactable living units
|
|
|
|
|
// as attackable fallback (covers bad faction-template classification).
|
|
|
|
|
auto isSpiritNpc = [&]() -> bool {
|
|
|
|
|
constexpr uint32_t NPC_FLAG_SPIRIT_GUIDE = 0x00004000;
|
|
|
|
|
constexpr uint32_t NPC_FLAG_SPIRIT_HEALER = 0x00008000;
|
|
|
|
|
if (unit->getNpcFlags() & (NPC_FLAG_SPIRIT_GUIDE | NPC_FLAG_SPIRIT_HEALER)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
std::string name = unit->getName();
|
|
|
|
|
std::transform(name.begin(), name.end(), name.begin(),
|
|
|
|
|
[](unsigned char c){ return static_cast<char>(std::tolower(c)); });
|
|
|
|
|
return (name.find("spirit healer") != std::string::npos) ||
|
|
|
|
|
(name.find("spirit guide") != std::string::npos);
|
|
|
|
|
};
|
|
|
|
|
bool allowSpiritInteract = (gameHandler.isPlayerDead() || gameHandler.isPlayerGhost()) && isSpiritNpc();
|
|
|
|
|
bool canInteractNpc = unit->isInteractable() || allowSpiritInteract;
|
|
|
|
|
bool shouldAttackByFallback = !canInteractNpc;
|
|
|
|
|
if (!unit->isHostile() && canInteractNpc) {
|
|
|
|
|
gameHandler.interactWithNpc(target->getGuid());
|
|
|
|
|
} else if (unit->isHostile() || shouldAttackByFallback) {
|
|
|
|
|
gameHandler.startAutoAttack(target->getGuid());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if (target->getType() == game::ObjectType::GAMEOBJECT) {
|
|
|
|
|
gameHandler.interactWithGameObject(target->getGuid());
|
|
|
|
|
} else if (target->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
// Right-click another player could start attack in PvP context
|
|
|
|
|
}
|
2026-03-12 14:43:58 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 14:43:58 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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);
|
2026-03-12 14:21:02 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
2026-03-12 14:21:02 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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);
|
2026-03-12 14:21:02 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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;
|
2026-03-18 10:14:09 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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; }
|
2026-03-12 14:21:02 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
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;
|
2026-03-18 10:14:09 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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());
|
2026-03-11 23:51:27 -07:00
|
|
|
ImGui::Separator();
|
2026-03-31 08:53:14 +03:00
|
|
|
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()) {
|
2026-03-11 23:51:27 -07:00
|
|
|
ImGui::Separator();
|
2026-03-31 08:53:14 +03:00
|
|
|
if (ImGui::MenuItem("Leave Group")) {
|
|
|
|
|
gameHandler.leaveGroup();
|
2026-03-11 23:56:57 -07:00
|
|
|
}
|
2026-03-11 23:51:27 -07:00
|
|
|
}
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
ImGui::TextDisabled("Lv %u", playerLevel);
|
|
|
|
|
if (isDead) {
|
2026-03-10 21:15:24 -07:00
|
|
|
ImGui::SameLine();
|
2026-03-31 08:53:14 +03:00
|
|
|
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");
|
|
|
|
|
}
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
if (auto* ren = services_.renderer) {
|
2026-03-31 08:53:14 +03:00
|
|
|
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");
|
2026-03-10 21:15:24 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
|
|
|
|
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");
|
|
|
|
|
}
|
2026-03-10 21:15:24 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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());
|
2026-03-12 08:03:43 -07:00
|
|
|
}
|
2026-03-10 21:15:24 -07:00
|
|
|
}
|
2026-03-12 13:32:10 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 13:32:10 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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();
|
2026-03-12 13:32:10 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
2026-03-12 13:32:10 -07:00
|
|
|
} else {
|
2026-03-31 08:53:14 +03:00
|
|
|
powerColor = colors::kManaBlue;
|
2026-03-12 13:32:10 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
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();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 13:32:10 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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;
|
2026-03-12 13:32:10 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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();
|
2026-03-12 14:50:59 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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);
|
2026-03-12 13:32:10 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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;
|
2026-03-12 13:32:10 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
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);
|
2026-03-12 13:32:10 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::Dummy(ImVec2(totalW, squareH));
|
2026-03-10 21:15:24 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// Combo point display — Rogue (4) and Druid (11) in Cat Form
|
2026-03-18 03:29:48 -07:00
|
|
|
{
|
2026-03-31 08:53:14 +03:00
|
|
|
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));
|
|
|
|
|
}
|
2026-03-18 03:29:48 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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);
|
2026-03-18 03:29:48 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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]);
|
2026-03-18 03:29:48 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::EndTooltip();
|
2026-03-18 03:29:48 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::SetCursorScreenPos(ImVec2(cursor.x, cursor.y + slotH + 2.0f));
|
2026-03-18 03:29:48 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
2026-03-18 03:29:48 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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;
|
2026-03-12 14:34:21 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
2026-03-10 21:15:24 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
|
2026-03-10 21:15:24 -07:00
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
void GameScreen::renderPetFrame(game::GameHandler& gameHandler) {
|
|
|
|
|
uint64_t petGuid = gameHandler.getPetGuid();
|
|
|
|
|
if (petGuid == 0) return;
|
2026-03-18 02:27:34 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
auto petEntity = gameHandler.getEntityManager().getEntity(petGuid);
|
|
|
|
|
if (!petEntity) return;
|
|
|
|
|
auto* petUnit = petEntity->isUnit() ? static_cast<game::Unit*>(petEntity.get()) : nullptr;
|
|
|
|
|
if (!petUnit) return;
|
2026-03-18 02:44:28 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
2026-03-18 03:16:05 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
2026-03-18 03:04:45 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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));
|
2026-03-18 03:04:45 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (ImGui::Begin("##PetFrame", nullptr, flags)) {
|
|
|
|
|
const std::string& petName = petUnit->getName();
|
|
|
|
|
uint32_t petLevel = petUnit->getLevel();
|
2026-03-18 03:04:45 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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();
|
|
|
|
|
}
|
2026-03-18 03:04:45 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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();
|
|
|
|
|
}
|
2026-03-18 03:04:45 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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();
|
|
|
|
|
}
|
2026-03-18 03:04:45 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
|
|
|
|
|
// 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();
|
2026-03-18 03:04:45 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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 const 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.
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* assetMgr = services_.assetManager;
|
2026-03-31 08:53:14 +03:00
|
|
|
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;
|
2026-03-18 03:04:45 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
uint32_t actionId = slotVal & 0x00FFFFFFu;
|
|
|
|
|
// Use the authoritative autocast set from SMSG_PET_SPELLS spell list flags.
|
|
|
|
|
bool autocastOn = gameHandler.isPetSpellAutocast(actionId);
|
2026-03-18 03:04:45 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// Cooldown tracking for pet spells (actionId > 6 are spell IDs)
|
|
|
|
|
float petCd = (actionId > 6) ? gameHandler.getSpellCooldown(actionId) : 0.0f;
|
|
|
|
|
bool petOnCd = (petCd > 0.0f);
|
2026-03-18 02:44:28 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::PushID(i);
|
|
|
|
|
if (rendered > 0) ImGui::SameLine(0.0f, spacing);
|
2026-03-18 03:36:05 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
2026-03-11 23:06:24 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
2026-03-20 11:12:07 -07:00
|
|
|
} else {
|
2026-03-31 08:53:14 +03:00
|
|
|
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();
|
2026-03-20 11:12:07 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
2026-03-20 17:05:48 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (clicked && !petOnCd) {
|
|
|
|
|
// Send pet action; use current target for spells.
|
|
|
|
|
uint64_t targetGuid = (actionId > 5) ? gameHandler.getTargetGuid() : 0u;
|
|
|
|
|
gameHandler.sendPetAction(slotVal, targetGuid);
|
2026-03-20 11:40:58 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
// Right-click toggles autocast for castable pet spells (actionId > 6)
|
|
|
|
|
if (actionId > 6 && ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
|
|
|
|
gameHandler.togglePetSpellAutocast(actionId);
|
2026-03-20 16:17:04 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* spellAsset = services_.assetManager;
|
2026-03-31 08:53:14 +03:00
|
|
|
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();
|
2026-03-18 04:14:44 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-02-07 12:43:32 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::PopID();
|
|
|
|
|
++rendered;
|
2026-03-18 10:47:34 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
2026-03-18 10:47:34 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
}
|
2026-03-12 09:36:14 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Totem Frame (Shaman — below pet frame / player frame)
|
|
|
|
|
// ============================================================
|
2026-02-07 12:43:32 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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;
|
2026-03-12 09:45:03 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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
|
|
|
|
|
};
|
2026-03-12 02:31:12 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
2026-03-20 06:17:23 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize |
|
|
|
|
|
ImGuiWindowFlags_NoTitleBar;
|
2026-03-12 02:23:24 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.08f, 0.06f, 0.88f));
|
2026-02-11 21:14:35 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (ImGui::Begin("##TotemFrame", nullptr, flags)) {
|
|
|
|
|
ImGui::TextColored(ImVec4(0.9f, 0.75f, 0.3f, 1.0f), "Totems");
|
|
|
|
|
ImGui::Separator();
|
2026-02-11 21:14:35 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
for (int i = 0; i < game::GameHandler::NUM_TOTEM_SLOTS; ++i) {
|
|
|
|
|
const auto& slot = gameHandler.getTotemSlot(i);
|
|
|
|
|
if (!slot.active()) continue;
|
2026-02-11 21:14:35 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::PushID(i);
|
2026-02-07 12:43:32 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
feat: add persistent combat log window (/combatlog or /cl)
Stores up to 500 combat events in a rolling deque alongside the existing
floating combat text. Events are populated via the existing addCombatText()
call site, resolving attacker/target names from the entity manager and
player name cache at event time.
- CombatLogEntry struct in spell_defines.hpp (type, amount, spellId,
isPlayerSource, timestamp, sourceName, targetName)
- getCombatLog() / clearCombatLog() accessors on GameHandler
- renderCombatLog() in GameScreen: scrollable two-column table (Time +
Event), color-coded by event category, with Damage/Healing/Misc filter
checkboxes, auto-scroll toggle, and Clear button
- /combatlog (/cl) chat command toggles the window
2026-03-12 11:00:10 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
Add /roll and friend management commands
Roll Command:
- Add /roll, /random, /rnd commands for random number generation
- Support multiple formats: /roll, /roll 100, /roll 1-100, /roll 10 50
- Broadcasts rolls to party/raid with "[Name] rolls X (min-max)" format
- Cap max roll at 10,000 to prevent abuse
- Use MSG_RANDOM_ROLL (0x1FB) bidirectional opcode
Friend Commands:
- Add /friend add <name>, /addfriend <name> to add friends
- Add /friend remove <name>, /removefriend <name> to remove friends
- Support aliases: /delfriend, /remfriend
- Maintain local friends cache mapping names to GUIDs for lookups
- Display status messages for all friend actions:
- Friend added/removed confirmations
- Friend online/offline notifications
- Error messages (not found, already friends, list full, ignoring)
Social Opcodes:
- Add CMSG_ADD_FRIEND (0x69) and SMSG_FRIEND_STATUS (0x68)
- Add CMSG_DEL_FRIEND (0x6A) for friend removal
- Add CMSG_SET_CONTACT_NOTES (0x6B) for friend notes (future use)
- Add CMSG_ADD_IGNORE (0x6C) and CMSG_DEL_IGNORE (0x6D) (future use)
Implementation:
- Add RandomRollPacket builder and RandomRollParser for roll data
- Add AddFriendPacket and DelFriendPacket builders
- Add FriendStatusParser to handle server friend status updates
- Add friendsCache map to store friend name-to-GUID mappings
- Add handleRandomRoll() and handleFriendStatus() packet handlers
- Comprehensive slash command parsing with multiple formats and aliases
2026-02-07 12:51:30 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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;
|
Add /roll and friend management commands
Roll Command:
- Add /roll, /random, /rnd commands for random number generation
- Support multiple formats: /roll, /roll 100, /roll 1-100, /roll 10 50
- Broadcasts rolls to party/raid with "[Name] rolls X (min-max)" format
- Cap max roll at 10,000 to prevent abuse
- Use MSG_RANDOM_ROLL (0x1FB) bidirectional opcode
Friend Commands:
- Add /friend add <name>, /addfriend <name> to add friends
- Add /friend remove <name>, /removefriend <name> to remove friends
- Support aliases: /delfriend, /remfriend
- Maintain local friends cache mapping names to GUIDs for lookups
- Display status messages for all friend actions:
- Friend added/removed confirmations
- Friend online/offline notifications
- Error messages (not found, already friends, list full, ignoring)
Social Opcodes:
- Add CMSG_ADD_FRIEND (0x69) and SMSG_FRIEND_STATUS (0x68)
- Add CMSG_DEL_FRIEND (0x6A) for friend removal
- Add CMSG_SET_CONTACT_NOTES (0x6B) for friend notes (future use)
- Add CMSG_ADD_IGNORE (0x6C) and CMSG_DEL_IGNORE (0x6D) (future use)
Implementation:
- Add RandomRollPacket builder and RandomRollParser for roll data
- Add AddFriendPacket and DelFriendPacket builders
- Add FriendStatusParser to handle server friend status updates
- Add friendsCache map to store friend name-to-GUID mappings
- Add handleRandomRoll() and handleFriendStatus() packet handlers
- Comprehensive slash command parsing with multiple formats and aliases
2026-02-07 12:51:30 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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();
|
Add /roll and friend management commands
Roll Command:
- Add /roll, /random, /rnd commands for random number generation
- Support multiple formats: /roll, /roll 100, /roll 1-100, /roll 10 50
- Broadcasts rolls to party/raid with "[Name] rolls X (min-max)" format
- Cap max roll at 10,000 to prevent abuse
- Use MSG_RANDOM_ROLL (0x1FB) bidirectional opcode
Friend Commands:
- Add /friend add <name>, /addfriend <name> to add friends
- Add /friend remove <name>, /removefriend <name> to remove friends
- Support aliases: /delfriend, /remfriend
- Maintain local friends cache mapping names to GUIDs for lookups
- Display status messages for all friend actions:
- Friend added/removed confirmations
- Friend online/offline notifications
- Error messages (not found, already friends, list full, ignoring)
Social Opcodes:
- Add CMSG_ADD_FRIEND (0x69) and SMSG_FRIEND_STATUS (0x68)
- Add CMSG_DEL_FRIEND (0x6A) for friend removal
- Add CMSG_SET_CONTACT_NOTES (0x6B) for friend notes (future use)
- Add CMSG_ADD_IGNORE (0x6C) and CMSG_DEL_IGNORE (0x6D) (future use)
Implementation:
- Add RandomRollPacket builder and RandomRollParser for roll data
- Add AddFriendPacket and DelFriendPacket builders
- Add FriendStatusParser to handle server friend status updates
- Add friendsCache map to store friend name-to-GUID mappings
- Add handleRandomRoll() and handleFriendStatus() packet handlers
- Comprehensive slash command parsing with multiple formats and aliases
2026-02-07 12:51:30 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::PopID();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
Add /roll and friend management commands
Roll Command:
- Add /roll, /random, /rnd commands for random number generation
- Support multiple formats: /roll, /roll 100, /roll 1-100, /roll 10 50
- Broadcasts rolls to party/raid with "[Name] rolls X (min-max)" format
- Cap max roll at 10,000 to prevent abuse
- Use MSG_RANDOM_ROLL (0x1FB) bidirectional opcode
Friend Commands:
- Add /friend add <name>, /addfriend <name> to add friends
- Add /friend remove <name>, /removefriend <name> to remove friends
- Support aliases: /delfriend, /remfriend
- Maintain local friends cache mapping names to GUIDs for lookups
- Display status messages for all friend actions:
- Friend added/removed confirmations
- Friend online/offline notifications
- Error messages (not found, already friends, list full, ignoring)
Social Opcodes:
- Add CMSG_ADD_FRIEND (0x69) and SMSG_FRIEND_STATUS (0x68)
- Add CMSG_DEL_FRIEND (0x6A) for friend removal
- Add CMSG_SET_CONTACT_NOTES (0x6B) for friend notes (future use)
- Add CMSG_ADD_IGNORE (0x6C) and CMSG_DEL_IGNORE (0x6D) (future use)
Implementation:
- Add RandomRollPacket builder and RandomRollParser for roll data
- Add AddFriendPacket and DelFriendPacket builders
- Add FriendStatusParser to handle server friend status updates
- Add friendsCache map to store friend name-to-GUID mappings
- Add handleRandomRoll() and handleFriendStatus() packet handlers
- Comprehensive slash command parsing with multiple formats and aliases
2026-02-07 12:51:30 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
}
|
Add /roll and friend management commands
Roll Command:
- Add /roll, /random, /rnd commands for random number generation
- Support multiple formats: /roll, /roll 100, /roll 1-100, /roll 10 50
- Broadcasts rolls to party/raid with "[Name] rolls X (min-max)" format
- Cap max roll at 10,000 to prevent abuse
- Use MSG_RANDOM_ROLL (0x1FB) bidirectional opcode
Friend Commands:
- Add /friend add <name>, /addfriend <name> to add friends
- Add /friend remove <name>, /removefriend <name> to remove friends
- Support aliases: /delfriend, /remfriend
- Maintain local friends cache mapping names to GUIDs for lookups
- Display status messages for all friend actions:
- Friend added/removed confirmations
- Friend online/offline notifications
- Error messages (not found, already friends, list full, ignoring)
Social Opcodes:
- Add CMSG_ADD_FRIEND (0x69) and SMSG_FRIEND_STATUS (0x68)
- Add CMSG_DEL_FRIEND (0x6A) for friend removal
- Add CMSG_SET_CONTACT_NOTES (0x6B) for friend notes (future use)
- Add CMSG_ADD_IGNORE (0x6C) and CMSG_DEL_IGNORE (0x6D) (future use)
Implementation:
- Add RandomRollPacket builder and RandomRollParser for roll data
- Add AddFriendPacket and DelFriendPacket builders
- Add FriendStatusParser to handle server friend status updates
- Add friendsCache map to store friend name-to-GUID mappings
- Add handleRandomRoll() and handleFriendStatus() packet handlers
- Comprehensive slash command parsing with multiple formats and aliases
2026-02-07 12:51:30 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
void GameScreen::renderTargetFrame(game::GameHandler& gameHandler) {
|
|
|
|
|
auto target = gameHandler.getTarget();
|
|
|
|
|
if (!target) return;
|
Add /roll and friend management commands
Roll Command:
- Add /roll, /random, /rnd commands for random number generation
- Support multiple formats: /roll, /roll 100, /roll 1-100, /roll 10 50
- Broadcasts rolls to party/raid with "[Name] rolls X (min-max)" format
- Cap max roll at 10,000 to prevent abuse
- Use MSG_RANDOM_ROLL (0x1FB) bidirectional opcode
Friend Commands:
- Add /friend add <name>, /addfriend <name> to add friends
- Add /friend remove <name>, /removefriend <name> to remove friends
- Support aliases: /delfriend, /remfriend
- Maintain local friends cache mapping names to GUIDs for lookups
- Display status messages for all friend actions:
- Friend added/removed confirmations
- Friend online/offline notifications
- Error messages (not found, already friends, list full, ignoring)
Social Opcodes:
- Add CMSG_ADD_FRIEND (0x69) and SMSG_FRIEND_STATUS (0x68)
- Add CMSG_DEL_FRIEND (0x6A) for friend removal
- Add CMSG_SET_CONTACT_NOTES (0x6B) for friend notes (future use)
- Add CMSG_ADD_IGNORE (0x6C) and CMSG_DEL_IGNORE (0x6D) (future use)
Implementation:
- Add RandomRollPacket builder and RandomRollParser for roll data
- Add AddFriendPacket and DelFriendPacket builders
- Add FriendStatusParser to handle server friend status updates
- Add friendsCache map to store friend name-to-GUID mappings
- Add handleRandomRoll() and handleFriendStatus() packet handlers
- Comprehensive slash command parsing with multiple formats and aliases
2026-02-07 12:51:30 -08:00
|
|
|
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* window = services_.window;
|
2026-03-31 08:53:14 +03:00
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
2026-02-07 12:58:11 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
float frameW = 250.0f;
|
|
|
|
|
float frameX = (screenW - frameW) / 2.0f;
|
2026-02-07 12:58:11 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::SetNextWindowPos(ImVec2(frameX, 30.0f), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(frameW, 0.0f), ImGuiCond_Always);
|
2026-02-07 12:58:11 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
2026-02-07 12:58:11 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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
|
|
|
|
|
}
|
2026-02-07 17:59:40 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
} // end tapped else
|
|
|
|
|
} else {
|
|
|
|
|
hostileColor = kColorBrightGreen; // Friendly
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-07 17:59:40 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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);
|
2026-03-18 02:32:49 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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);
|
|
|
|
|
}
|
feat: add /cancelform, /cancelshapeshift, /cancelaura slash commands
These are standard WoW macro commands:
- /cancelform / /cancelshapeshift: exits current shapeshift form by
cancelling the first permanent aura (flag 0x20) on the player
- /cancelaura <name|#id>: cancels a specific player buff by spell name
or numeric ID (e.g. /cancelaura Stealth, /cancelaura #1784)
Also expand the Tab-autocomplete command list to include /cancelaura,
/cancelform, /cancelshapeshift, /dismount, /sit, /stand, /startattack,
/stopcasting, /target, and other commands that were previously missing.
2026-03-18 02:30:35 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// Entity name and type — Selectable so we can attach a right-click context menu
|
|
|
|
|
std::string name = getEntityName(target);
|
feat: add /cancelform, /cancelshapeshift, /cancelaura slash commands
These are standard WoW macro commands:
- /cancelform / /cancelshapeshift: exits current shapeshift form by
cancelling the first permanent aura (flag 0x20) on the player
- /cancelaura <name|#id>: cancels a specific player buff by spell name
or numeric ID (e.g. /cancelaura Stealth, /cancelaura #1784)
Also expand the Tab-autocomplete command list to include /cancelaura,
/cancelform, /cancelshapeshift, /dismount, /sit, /stand, /startattack,
/stopcasting, /target, and other commands that were previously missing.
2026-03-18 02:30:35 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
|
|
|
|
}
|
2026-02-07 12:58:11 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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);
|
2026-02-07 12:58:11 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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();
|
2026-02-07 12:58:11 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::TextDisabled("%s", name.c_str());
|
|
|
|
|
ImGui::Separator();
|
2026-02-07 12:58:11 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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();
|
2026-03-31 19:49:52 +03:00
|
|
|
socialPanel_.showInspectWindow_ = true;
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
|
|
|
|
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");
|
2026-03-27 18:05:42 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
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();
|
2026-02-07 12:58:11 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::EndPopup();
|
|
|
|
|
}
|
2026-02-07 12:58:11 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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");
|
2026-02-07 13:03:21 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
2026-02-07 13:03:21 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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");
|
2026-02-07 13:03:21 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
2026-02-07 13:03:21 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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());
|
2026-02-07 13:03:21 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
2026-02-07 13:03:21 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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());
|
|
|
|
|
}
|
2026-03-12 09:36:14 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
2026-03-12 09:36:14 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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();
|
2026-03-18 03:31:40 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::TextDisabled("%s", name.c_str());
|
|
|
|
|
ImGui::Separator();
|
2026-03-18 03:31:40 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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();
|
2026-03-31 19:49:52 +03:00
|
|
|
socialPanel_.showInspectWindow_ = true;
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
if (ImGui::MenuItem("Add Friend")) {
|
|
|
|
|
gameHandler.addFriend(name);
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::MenuItem("Ignore")) {
|
|
|
|
|
gameHandler.addIgnore(name);
|
2026-03-18 03:31:40 -07:00
|
|
|
}
|
2026-02-07 13:03:21 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
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();
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::EndPopup();
|
|
|
|
|
}
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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;
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
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");
|
|
|
|
|
}
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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;
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
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");
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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");
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
|
|
|
|
}
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::Dummy(ImVec2(totalW, dotSize + 2.0f));
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
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);
|
|
|
|
|
{
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* tcastAsset = services_.assetManager;
|
2026-03-31 08:53:14 +03:00
|
|
|
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);
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
}
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
2026-02-13 21:39:48 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
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);
|
2026-02-25 14:44:44 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
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);
|
|
|
|
|
}
|
2026-02-25 14:44:44 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-16 20:16:14 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
2026-02-16 20:16:14 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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));
|
2026-03-31 19:49:52 +03:00
|
|
|
if (ImGui::SmallButton("Threat")) combatUI_.showThreatWindow_ = !combatUI_.showThreatWindow_;
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
}
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// Target auras (buffs/debuffs)
|
|
|
|
|
const auto& targetAuras = gameHandler.getTargetAuras();
|
|
|
|
|
int activeAuras = 0;
|
|
|
|
|
for (const auto& a : targetAuras) {
|
|
|
|
|
if (!a.isEmpty()) activeAuras++;
|
|
|
|
|
}
|
|
|
|
|
if (activeAuras > 0) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* assetMgr = services_.assetManager;
|
2026-03-31 08:53:14 +03:00
|
|
|
constexpr float ICON_SIZE = 24.0f;
|
|
|
|
|
constexpr int ICONS_PER_ROW = 8;
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::Separator();
|
Add Tier 3 commands: guild management, PvP, ready check, and duel forfeit
- Guild commands: /ginfo, /groster, /gmotd, /gpromote, /gdemote, /gquit, /ginvite
- PvP toggle: /pvp to toggle PvP flag
- Ready check system: /readycheck, /ready, /notready for group coordination
- Duel forfeit: /yield, /forfeit, /surrender to cancel active duels
2026-02-07 13:09:12 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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;
|
|
|
|
|
});
|
2026-02-07 13:17:01 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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;
|
2026-02-07 13:17:01 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (shown > 0 && shown % ICONS_PER_ROW != 0) ImGui::SameLine();
|
|
|
|
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(10000 + i));
|
2026-02-07 13:17:01 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
bool isBuff = (aura.flags & 0x80) == 0;
|
|
|
|
|
ImVec4 auraBorderColor;
|
|
|
|
|
if (isBuff) {
|
|
|
|
|
auraBorderColor = ImVec4(0.2f, 0.8f, 0.2f, 0.9f);
|
2026-02-07 13:28:46 -08:00
|
|
|
} else {
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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
|
|
|
|
|
}
|
2026-02-07 13:28:46 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
VkDescriptorSet iconTex = VK_NULL_HANDLE;
|
|
|
|
|
if (assetMgr) {
|
|
|
|
|
iconTex = getSpellIcon(aura.spellId, assetMgr);
|
2026-02-07 13:28:46 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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();
|
2026-02-07 13:28:46 -08:00
|
|
|
} else {
|
2026-03-31 08:53:14 +03:00
|
|
|
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();
|
2026-02-07 13:28:46 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
2026-02-07 13:28:46 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-27 17:54:56 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
2026-03-27 17:54:56 -07:00
|
|
|
} else {
|
2026-03-31 08:53:14 +03:00
|
|
|
tTimerColor = IM_COL32(255, 255, 255, 255);
|
2026-03-27 17:54:56 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(cx + 1, cy + 1),
|
|
|
|
|
IM_COL32(0, 0, 0, 200), timeStr);
|
|
|
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(cx, cy),
|
|
|
|
|
tTimerColor, timeStr);
|
2026-03-27 17:54:56 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
2026-03-27 17:54:56 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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());
|
feat: add /mark slash command for setting raid target icons
Adds /mark [icon], /marktarget, and /raidtarget slash commands that
set a raid mark on the current target. Accepts icon names (star,
circle, diamond, triangle, moon, square, cross, skull), numbers 1-8,
or "clear"/"none" to remove the mark. Defaults to skull when no
argument is given.
2026-03-18 05:16:14 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
renderAuraRemaining(tRemainMs);
|
|
|
|
|
ImGui::EndTooltip();
|
feat: add /mark slash command for setting raid target icons
Adds /mark [icon], /marktarget, and /raidtarget slash commands that
set a raid mark on the current target. Accepts icon names (star,
circle, diamond, triangle, moon, square, cross, skull), numbers 1-8,
or "clear"/"none" to remove the mark. Defaults to skull when no
argument is given.
2026-03-18 05:16:14 -07:00
|
|
|
}
|
2026-02-07 13:36:50 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::PopID();
|
|
|
|
|
shown++;
|
2026-02-07 13:36:50 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
2026-02-07 13:36:50 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
ImGui::PopStyleVar();
|
2026-02-07 13:36:50 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// ---- 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);
|
|
|
|
|
}
|
2026-02-07 13:36:50 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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);
|
2026-03-18 03:36:05 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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));
|
2026-03-20 17:24:16 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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);
|
2026-03-18 03:53:59 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
2026-03-18 03:53:59 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::PopStyleColor(4);
|
2026-03-18 03:36:05 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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();
|
2026-03-18 03:36:05 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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());
|
2026-03-18 03:36:05 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
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();
|
2026-03-18 03:36:05 -07:00
|
|
|
}
|
2026-02-07 13:36:50 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* totAsset = services_.assetManager;
|
2026-03-31 08:53:14 +03:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 09:30:59 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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();
|
|
|
|
|
}
|
2026-03-18 03:04:45 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
|
|
|
|
}
|
2026-03-18 03:11:34 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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();
|
|
|
|
|
}
|
2026-03-18 02:14:10 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::PopID();
|
|
|
|
|
taShown++;
|
|
|
|
|
}
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 09:30:59 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::End();
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 09:30:59 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
void GameScreen::renderFocusFrame(game::GameHandler& gameHandler) {
|
|
|
|
|
auto focus = gameHandler.getFocus();
|
|
|
|
|
if (!focus) return;
|
2026-03-12 09:30:59 -07:00
|
|
|
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* window = services_.window;
|
2026-03-31 08:53:14 +03:00
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
2026-03-12 09:30:59 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// Position: right side of screen, mirroring the target frame on the opposite side
|
|
|
|
|
float frameW = 200.0f;
|
|
|
|
|
float frameX = screenW - frameW - 10.0f;
|
2026-03-12 09:30:59 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::SetNextWindowPos(ImVec2(frameX, 30.0f), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(frameW, 0.0f), ImGuiCond_Always);
|
2026-03-18 02:23:47 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
2026-03-18 03:06:23 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-18 02:23:47 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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
|
2026-03-18 02:23:47 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (ImGui::Begin("##FocusFrame", nullptr, flags)) {
|
|
|
|
|
// "Focus" label
|
|
|
|
|
ImGui::TextDisabled("[Focus]");
|
|
|
|
|
ImGui::SameLine();
|
2026-03-12 10:06:11 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
2026-03-12 10:06:11 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
2026-03-12 10:06:11 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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);
|
2026-03-12 10:06:11 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
2026-03-12 10:06:11 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
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();
|
2026-03-31 19:49:52 +03:00
|
|
|
socialPanel_.showInspectWindow_ = true;
|
2026-03-12 10:06:11 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::Separator();
|
|
|
|
|
if (ImGui::MenuItem("Add Friend"))
|
|
|
|
|
gameHandler.addFriend(focusName);
|
|
|
|
|
if (ImGui::MenuItem("Ignore"))
|
|
|
|
|
gameHandler.addIgnore(focusName);
|
2026-03-12 10:06:11 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::EndPopup();
|
|
|
|
|
}
|
2026-03-12 10:06:11 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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");
|
2026-02-07 13:44:36 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
2026-02-07 13:44:36 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
2026-03-18 03:21:27 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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, "?");
|
2026-03-11 22:57:04 -07:00
|
|
|
}
|
2026-02-07 13:44:36 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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]"); }
|
2026-02-07 13:44:36 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
|
|
|
|
}
|
2026-02-07 13:44:36 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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());
|
|
|
|
|
}
|
2026-03-18 03:21:27 -07:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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());
|
2026-02-07 13:44:36 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
2026-02-07 13:44:36 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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"))
|
2026-02-07 13:44:36 -08:00
|
|
|
gameHandler.clearFocus();
|
2026-03-31 08:53:14 +03:00
|
|
|
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();
|
2026-03-31 19:49:52 +03:00
|
|
|
socialPanel_.showInspectWindow_ = true;
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
if (ImGui::MenuItem("Add Friend"))
|
|
|
|
|
gameHandler.addFriend(focusName);
|
|
|
|
|
if (ImGui::MenuItem("Ignore"))
|
|
|
|
|
gameHandler.addIgnore(focusName);
|
2026-02-07 13:44:36 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::EndPopup();
|
|
|
|
|
}
|
2026-02-07 13:44:36 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (focus->getType() == game::ObjectType::UNIT ||
|
|
|
|
|
focus->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(focus);
|
2026-02-10 21:29:10 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// Level + health on same row
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
if (unit->getLevel() == 0)
|
|
|
|
|
ImGui::TextDisabled("Lv ??");
|
|
|
|
|
else
|
|
|
|
|
ImGui::TextDisabled("Lv %u", unit->getLevel());
|
2026-02-10 21:29:10 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
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();
|
2026-02-10 21:29:10 -08:00
|
|
|
}
|
2026-02-08 03:24:12 -08:00
|
|
|
}
|
2026-02-07 16:59:20 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
2026-02-14 14:30:09 -08:00
|
|
|
} else {
|
2026-03-31 08:53:14 +03:00
|
|
|
focusCastColor = ImVec4(0.9f, 0.3f, 0.2f, 1.0f);
|
2026-02-14 14:30:09 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
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);
|
|
|
|
|
{
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* fcAsset = services_.assetManager;
|
2026-03-31 08:53:14 +03:00
|
|
|
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);
|
2026-02-02 12:24:50 -08:00
|
|
|
} else {
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::ProgressBar(prog, ImVec2(-1, 12), castBuf);
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-03-17 10:12:49 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::PopStyleColor();
|
2026-02-07 12:30:36 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
}
|
2026-02-07 12:30:36 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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());
|
2026-02-07 20:02:14 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
if (focusAuras) {
|
|
|
|
|
int activeCount = 0;
|
|
|
|
|
for (const auto& a : *focusAuras) if (!a.isEmpty()) activeCount++;
|
|
|
|
|
if (activeCount > 0) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* focusAsset = services_.assetManager;
|
2026-03-31 08:53:14 +03:00
|
|
|
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();
|
|
|
|
|
}
|
2026-02-07 12:30:36 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
|
|
|
|
}
|
2026-02-14 14:30:09 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
|
|
|
|
}
|
2026-02-07 12:30:36 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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();
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::PopID();
|
|
|
|
|
faShown++;
|
2026-03-12 11:16:42 -07:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::PopStyleVar();
|
2026-03-12 11:16:42 -07:00
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
2026-02-11 21:14:35 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
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);
|
2026-02-11 21:14:35 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// 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);
|
2026-02-07 12:30:36 -08:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
// Clicking the focus frame targets it
|
|
|
|
|
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(0)) {
|
|
|
|
|
gameHandler.setTarget(focus->getGuid());
|
2026-02-07 23:32:27 -08:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::End();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
ImGui::PopStyleVar();
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GameScreen::updateCharacterGeosets(game::Inventory& inventory) {
|
|
|
|
|
auto& app = core::Application::getInstance();
|
|
|
|
|
auto* renderer = app.getRenderer();
|
|
|
|
|
if (!renderer) return;
|
|
|
|
|
|
|
|
|
|
uint32_t instanceId = renderer->getCharacterInstanceId();
|
|
|
|
|
if (instanceId == 0) return;
|
|
|
|
|
|
|
|
|
|
auto* charRenderer = renderer->getCharacterRenderer();
|
|
|
|
|
if (!charRenderer) return;
|
|
|
|
|
|
|
|
|
|
auto* assetManager = app.getAssetManager();
|
|
|
|
|
|
|
|
|
|
// Load ItemDisplayInfo.dbc for geosetGroup lookup
|
|
|
|
|
std::shared_ptr<pipeline::DBCFile> displayInfoDbc;
|
|
|
|
|
if (assetManager) {
|
|
|
|
|
displayInfoDbc = assetManager->loadDBC("ItemDisplayInfo.dbc");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper: get geosetGroup field for an equipped item's displayInfoId
|
|
|
|
|
// DBC binary fields: 7=geosetGroup_1, 8=geosetGroup_2, 9=geosetGroup_3
|
|
|
|
|
auto getGeosetGroup = [&](uint32_t displayInfoId, int groupField) -> uint32_t {
|
|
|
|
|
if (!displayInfoDbc || displayInfoId == 0) return 0;
|
|
|
|
|
int32_t recIdx = displayInfoDbc->findRecordById(displayInfoId);
|
|
|
|
|
if (recIdx < 0) return 0;
|
|
|
|
|
return displayInfoDbc->getUInt32(static_cast<uint32_t>(recIdx), 7 + groupField);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Helper: find first equipped item matching inventoryType, return its displayInfoId
|
|
|
|
|
auto findEquippedDisplayId = [&](std::initializer_list<uint8_t> types) -> uint32_t {
|
|
|
|
|
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
|
|
|
|
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
|
|
|
|
if (!slot.empty()) {
|
|
|
|
|
for (uint8_t t : types) {
|
|
|
|
|
if (slot.item.inventoryType == t)
|
|
|
|
|
return slot.item.displayInfoId;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Helper: check if any equipment slot has the given inventoryType
|
|
|
|
|
auto hasEquippedType = [&](std::initializer_list<uint8_t> types) -> bool {
|
|
|
|
|
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
|
|
|
|
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
|
|
|
|
if (!slot.empty()) {
|
|
|
|
|
for (uint8_t t : types) {
|
|
|
|
|
if (slot.item.inventoryType == t) return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
2026-02-15 20:53:01 -08:00
|
|
|
// Base geosets always present (group 0: IDs 0-99, some models use up to 27)
|
2026-02-02 12:24:50 -08:00
|
|
|
std::unordered_set<uint16_t> geosets;
|
2026-02-15 20:53:01 -08:00
|
|
|
for (uint16_t i = 0; i <= 99; i++) {
|
2026-02-02 12:24:50 -08:00
|
|
|
geosets.insert(i);
|
|
|
|
|
}
|
2026-02-12 14:55:27 -08:00
|
|
|
// Hair/facial geosets must match the active character's appearance, otherwise
|
|
|
|
|
// we end up forcing a default hair mesh (often perceived as "wrong hair").
|
|
|
|
|
{
|
|
|
|
|
uint8_t hairStyleId = 0;
|
|
|
|
|
uint8_t facialId = 0;
|
|
|
|
|
if (auto* gh = app.getGameHandler()) {
|
|
|
|
|
if (const auto* ch = gh->getActiveCharacter()) {
|
|
|
|
|
hairStyleId = static_cast<uint8_t>((ch->appearanceBytes >> 16) & 0xFF);
|
|
|
|
|
facialId = ch->facialFeatures;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
geosets.insert(static_cast<uint16_t>(100 + hairStyleId + 1)); // Group 1 hair
|
|
|
|
|
geosets.insert(static_cast<uint16_t>(200 + facialId + 1)); // Group 2 facial
|
|
|
|
|
}
|
2026-02-15 20:53:01 -08:00
|
|
|
geosets.insert(702); // Ears: visible (default)
|
|
|
|
|
geosets.insert(2002); // Bare feet mesh (group 20 = CG_FEET, always on)
|
|
|
|
|
|
|
|
|
|
// CharGeosets mapping (verified via vertex bounding boxes):
|
|
|
|
|
// Group 4 (401+) = GLOVES (forearm area, Z~1.1-1.4)
|
|
|
|
|
// Group 5 (501+) = BOOTS (shin area, Z~0.1-0.6)
|
|
|
|
|
// Group 8 (801+) = WRISTBANDS/SLEEVES (controlled by chest armor)
|
|
|
|
|
// Group 9 (901+) = KNEEPADS
|
|
|
|
|
// Group 13 (1301+) = TROUSERS/PANTS
|
|
|
|
|
// Group 15 (1501+) = CAPE/CLOAK
|
|
|
|
|
// Group 20 (2002) = FEET
|
|
|
|
|
|
|
|
|
|
// Gloves: inventoryType 10 → group 4 (forearms)
|
|
|
|
|
// 401=bare forearms, 402+=glove styles covering forearm
|
|
|
|
|
{
|
|
|
|
|
uint32_t did = findEquippedDisplayId({10});
|
|
|
|
|
uint32_t gg = getGeosetGroup(did, 0);
|
|
|
|
|
geosets.insert(static_cast<uint16_t>(gg > 0 ? 401 + gg : 401));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Boots: inventoryType 8 → group 5 (shins/lower legs)
|
|
|
|
|
// 501=narrow bare shin, 502=wider (matches thigh width better). Use 502 as bare default.
|
|
|
|
|
// When boots equipped, gg selects boot style: 501+gg (gg=1→502, gg=2→503, etc.)
|
|
|
|
|
{
|
|
|
|
|
uint32_t did = findEquippedDisplayId({8});
|
|
|
|
|
uint32_t gg = getGeosetGroup(did, 0);
|
|
|
|
|
geosets.insert(static_cast<uint16_t>(gg > 0 ? 501 + gg : 502));
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Chest/Shirt: inventoryType 4 (shirt), 5 (chest), 20 (robe)
|
2026-02-15 20:53:01 -08:00
|
|
|
// Controls group 8 (wristbands/sleeve length): 801=bare wrists, 802+=sleeve styles
|
|
|
|
|
// Also controls group 13 (trousers) via GeosetGroup[2] for robes
|
2026-02-02 12:24:50 -08:00
|
|
|
{
|
|
|
|
|
uint32_t did = findEquippedDisplayId({4, 5, 20});
|
|
|
|
|
uint32_t gg = getGeosetGroup(did, 0);
|
2026-02-15 20:53:01 -08:00
|
|
|
geosets.insert(static_cast<uint16_t>(gg > 0 ? 801 + gg : 801));
|
|
|
|
|
// Robe kilt: GeosetGroup[2] > 0 → show kilt legs (1302+)
|
2026-02-02 12:24:50 -08:00
|
|
|
uint32_t gg3 = getGeosetGroup(did, 2);
|
|
|
|
|
if (gg3 > 0) {
|
|
|
|
|
geosets.insert(static_cast<uint16_t>(1301 + gg3));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 20:53:01 -08:00
|
|
|
// Kneepads: group 9 (always default 902)
|
|
|
|
|
geosets.insert(902);
|
|
|
|
|
|
|
|
|
|
// Legs/Pants: inventoryType 7 → group 13 (trousers/thighs)
|
|
|
|
|
// 1301=bare legs, 1302+=pant/kilt styles
|
2026-02-02 12:24:50 -08:00
|
|
|
{
|
|
|
|
|
uint32_t did = findEquippedDisplayId({7});
|
|
|
|
|
uint32_t gg = getGeosetGroup(did, 0);
|
2026-02-15 20:53:01 -08:00
|
|
|
// Only add if robe hasn't already set a kilt geoset
|
2026-02-02 12:24:50 -08:00
|
|
|
if (geosets.count(1302) == 0 && geosets.count(1303) == 0) {
|
|
|
|
|
geosets.insert(static_cast<uint16_t>(gg > 0 ? 1301 + gg : 1301));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 20:53:01 -08:00
|
|
|
// Back/Cloak: inventoryType 16 → group 15
|
2026-02-02 12:24:50 -08:00
|
|
|
geosets.insert(hasEquippedType({16}) ? 1502 : 1501);
|
|
|
|
|
|
2026-02-15 20:53:01 -08:00
|
|
|
// Tabard: inventoryType 19 → group 12
|
2026-02-02 12:24:50 -08:00
|
|
|
if (hasEquippedType({19})) {
|
|
|
|
|
geosets.insert(1201);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
charRenderer->setActiveGeosets(instanceId, geosets);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GameScreen::updateCharacterTextures(game::Inventory& inventory) {
|
|
|
|
|
auto& app = core::Application::getInstance();
|
|
|
|
|
auto* renderer = app.getRenderer();
|
|
|
|
|
if (!renderer) return;
|
|
|
|
|
|
|
|
|
|
auto* charRenderer = renderer->getCharacterRenderer();
|
|
|
|
|
if (!charRenderer) return;
|
|
|
|
|
|
|
|
|
|
auto* assetManager = app.getAssetManager();
|
|
|
|
|
if (!assetManager) return;
|
|
|
|
|
|
|
|
|
|
const auto& bodySkinPath = app.getBodySkinPath();
|
|
|
|
|
const auto& underwearPaths = app.getUnderwearPaths();
|
|
|
|
|
uint32_t skinSlot = app.getSkinTextureSlotIndex();
|
|
|
|
|
|
|
|
|
|
if (bodySkinPath.empty()) return;
|
|
|
|
|
|
|
|
|
|
// Component directory names indexed by region
|
refactor: add 9 button/bar color constants, batch constexpr promotions
New ui_colors.hpp constants: kBtnGreen, kBtnGreenHover, kBtnRed,
kBtnRedHover, kBtnDkGreen/Hover, kBtnDkRed/Hover, kMidHealthYellow
— replacing 21 inline literals across accept/decline button and
health bar patterns.
Deduplicate kMon/kMonths month arrays (2 copies → 1 kMonthAbbrev).
Promote 22 remaining static const char*/int arrays to constexpr
(kQualHex, resLabels, kRepRankNames, kTotemNames, kReactLabels,
kChatHelp, kMacroHelp, kHelpLines, kMarkWords, componentDirs,
keyLabels, kRollLabels, gossipIcons, kMarkNames, kDiffLabels,
kStatLabels, kCatHeaders, kSlotNames, kResolutions, displayToInternal).
2026-03-27 14:44:52 -07:00
|
|
|
static constexpr const char* componentDirs[] = {
|
2026-02-02 12:24:50 -08:00
|
|
|
"ArmUpperTexture", // 0
|
|
|
|
|
"ArmLowerTexture", // 1
|
|
|
|
|
"HandTexture", // 2
|
|
|
|
|
"TorsoUpperTexture", // 3
|
|
|
|
|
"TorsoLowerTexture", // 4
|
|
|
|
|
"LegUpperTexture", // 5
|
|
|
|
|
"LegLowerTexture", // 6
|
|
|
|
|
"FootTexture", // 7
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Load ItemDisplayInfo.dbc
|
|
|
|
|
auto displayInfoDbc = assetManager->loadDBC("ItemDisplayInfo.dbc");
|
|
|
|
|
if (!displayInfoDbc) return;
|
2026-02-14 16:33:24 -08:00
|
|
|
const auto* idiL = pipeline::getActiveDBCLayout()
|
|
|
|
|
? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
|
|
|
|
|
// Texture component region fields (8 regions: ArmUpper..Foot)
|
|
|
|
|
// Binary DBC (23 fields) has textures at 14+
|
|
|
|
|
const uint32_t texRegionFields[8] = {
|
|
|
|
|
idiL ? (*idiL)["TextureArmUpper"] : 14u,
|
|
|
|
|
idiL ? (*idiL)["TextureArmLower"] : 15u,
|
|
|
|
|
idiL ? (*idiL)["TextureHand"] : 16u,
|
|
|
|
|
idiL ? (*idiL)["TextureTorsoUpper"]: 17u,
|
|
|
|
|
idiL ? (*idiL)["TextureTorsoLower"]: 18u,
|
|
|
|
|
idiL ? (*idiL)["TextureLegUpper"] : 19u,
|
|
|
|
|
idiL ? (*idiL)["TextureLegLower"] : 20u,
|
|
|
|
|
idiL ? (*idiL)["TextureFoot"] : 21u,
|
|
|
|
|
};
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Collect equipment texture regions from all equipped items
|
|
|
|
|
std::vector<std::pair<int, std::string>> regionLayers;
|
|
|
|
|
|
|
|
|
|
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
|
|
|
|
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
|
|
|
|
if (slot.empty() || slot.item.displayInfoId == 0) continue;
|
|
|
|
|
|
|
|
|
|
int32_t recIdx = displayInfoDbc->findRecordById(slot.item.displayInfoId);
|
|
|
|
|
if (recIdx < 0) continue;
|
|
|
|
|
|
|
|
|
|
for (int region = 0; region < 8; region++) {
|
2026-02-14 16:33:24 -08:00
|
|
|
std::string texName = displayInfoDbc->getString(
|
|
|
|
|
static_cast<uint32_t>(recIdx), texRegionFields[region]);
|
2026-02-02 12:24:50 -08:00
|
|
|
if (texName.empty()) continue;
|
|
|
|
|
|
|
|
|
|
// Actual MPQ files have a gender suffix: _M (male), _F (female), _U (unisex)
|
|
|
|
|
// Try gender-specific first, then unisex fallback
|
|
|
|
|
std::string base = "Item\\TextureComponents\\" +
|
|
|
|
|
std::string(componentDirs[region]) + "\\" + texName;
|
2026-02-13 18:59:09 -08:00
|
|
|
// Determine gender suffix from active character
|
|
|
|
|
bool isFemale = false;
|
|
|
|
|
if (auto* gh = app.getGameHandler()) {
|
|
|
|
|
if (auto* ch = gh->getActiveCharacter()) {
|
|
|
|
|
isFemale = (ch->gender == game::Gender::FEMALE) ||
|
|
|
|
|
(ch->gender == game::Gender::NONBINARY && ch->useFemaleModel);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
std::string genderPath = base + (isFemale ? "_F.blp" : "_M.blp");
|
2026-02-02 12:24:50 -08:00
|
|
|
std::string unisexPath = base + "_U.blp";
|
|
|
|
|
std::string fullPath;
|
2026-02-13 18:59:09 -08:00
|
|
|
if (assetManager->fileExists(genderPath)) {
|
|
|
|
|
fullPath = genderPath;
|
2026-02-02 12:24:50 -08:00
|
|
|
} else if (assetManager->fileExists(unisexPath)) {
|
|
|
|
|
fullPath = unisexPath;
|
|
|
|
|
} else {
|
|
|
|
|
// Last resort: try without suffix
|
|
|
|
|
fullPath = base + ".blp";
|
|
|
|
|
}
|
|
|
|
|
regionLayers.emplace_back(region, fullPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Re-composite: base skin + underwear + equipment regions
|
2026-02-15 20:53:01 -08:00
|
|
|
// Clear composite cache first to prevent stale textures from being reused
|
|
|
|
|
charRenderer->clearCompositeCache();
|
|
|
|
|
// Use per-instance texture override (not model-level) to avoid deleting cached composites.
|
|
|
|
|
uint32_t instanceId = renderer->getCharacterInstanceId();
|
2026-02-21 19:41:21 -08:00
|
|
|
auto* newTex = charRenderer->compositeWithRegions(bodySkinPath, underwearPaths, regionLayers);
|
|
|
|
|
if (newTex != nullptr && instanceId != 0) {
|
2026-02-15 20:53:01 -08:00
|
|
|
charRenderer->setTextureSlotOverride(instanceId, static_cast<uint16_t>(skinSlot), newTex);
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cloak cape texture — separate from skin atlas, uses texture slot type-2 (Object Skin)
|
|
|
|
|
uint32_t cloakSlot = app.getCloakTextureSlotIndex();
|
2026-02-15 20:53:01 -08:00
|
|
|
if (cloakSlot > 0 && instanceId != 0) {
|
2026-02-02 12:24:50 -08:00
|
|
|
// Find equipped cloak (inventoryType 16)
|
|
|
|
|
uint32_t cloakDisplayId = 0;
|
|
|
|
|
for (int s = 0; s < game::Inventory::NUM_EQUIP_SLOTS; s++) {
|
|
|
|
|
const auto& slot = inventory.getEquipSlot(static_cast<game::EquipSlot>(s));
|
|
|
|
|
if (!slot.empty() && slot.item.inventoryType == 16 && slot.item.displayInfoId != 0) {
|
|
|
|
|
cloakDisplayId = slot.item.displayInfoId;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (cloakDisplayId > 0) {
|
|
|
|
|
int32_t recIdx = displayInfoDbc->findRecordById(cloakDisplayId);
|
|
|
|
|
if (recIdx >= 0) {
|
|
|
|
|
// DBC field 3 = modelTexture_1 (cape texture name)
|
2026-02-12 22:56:36 -08:00
|
|
|
const auto* dispL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("ItemDisplayInfo") : nullptr;
|
|
|
|
|
std::string capeName = displayInfoDbc->getString(static_cast<uint32_t>(recIdx), dispL ? (*dispL)["LeftModelTexture"] : 3);
|
2026-02-02 12:24:50 -08:00
|
|
|
if (!capeName.empty()) {
|
|
|
|
|
std::string capePath = "Item\\ObjectComponents\\Cape\\" + capeName + ".blp";
|
2026-02-21 19:41:21 -08:00
|
|
|
auto* capeTex = charRenderer->loadTexture(capePath);
|
|
|
|
|
if (capeTex != nullptr) {
|
2026-02-15 20:53:01 -08:00
|
|
|
charRenderer->setTextureSlotOverride(instanceId, static_cast<uint16_t>(cloakSlot), capeTex);
|
2026-02-02 12:24:50 -08:00
|
|
|
LOG_INFO("Cloak texture applied: ", capePath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2026-02-15 20:53:01 -08:00
|
|
|
// No cloak equipped — clear override so model's default (white) shows
|
|
|
|
|
charRenderer->clearTextureSlotOverride(instanceId, static_cast<uint16_t>(cloakSlot));
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 22:27:45 -08:00
|
|
|
// ============================================================
|
|
|
|
|
// World Map
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
2026-02-11 18:25:04 -08:00
|
|
|
void GameScreen::renderWorldMap(game::GameHandler& gameHandler) {
|
2026-03-11 07:38:08 -07:00
|
|
|
if (!showWorldMap_) return;
|
|
|
|
|
|
2026-02-04 22:27:45 -08:00
|
|
|
auto& app = core::Application::getInstance();
|
|
|
|
|
auto* renderer = app.getRenderer();
|
2026-02-21 19:41:21 -08:00
|
|
|
if (!renderer) return;
|
2026-02-04 22:27:45 -08:00
|
|
|
|
2026-02-21 19:41:21 -08:00
|
|
|
auto* wm = renderer->getWorldMap();
|
|
|
|
|
if (!wm) return;
|
2026-02-04 22:27:45 -08:00
|
|
|
|
|
|
|
|
// Keep map name in sync with minimap's map name
|
|
|
|
|
auto* minimap = renderer->getMinimap();
|
|
|
|
|
if (minimap) {
|
2026-02-21 19:41:21 -08:00
|
|
|
wm->setMapName(minimap->getMapName());
|
2026-02-04 22:27:45 -08:00
|
|
|
}
|
2026-02-21 19:41:21 -08:00
|
|
|
wm->setServerExplorationMask(
|
2026-02-11 18:25:04 -08:00
|
|
|
gameHandler.getPlayerExploredZoneMasks(),
|
|
|
|
|
gameHandler.hasPlayerExploredZoneMasks());
|
2026-02-04 22:27:45 -08:00
|
|
|
|
2026-03-12 15:05:52 -07:00
|
|
|
// Party member dots on world map
|
|
|
|
|
{
|
|
|
|
|
std::vector<rendering::WorldMapPartyDot> dots;
|
|
|
|
|
if (gameHandler.isInGroup()) {
|
|
|
|
|
const auto& partyData = gameHandler.getPartyData();
|
|
|
|
|
for (const auto& member : partyData.members) {
|
|
|
|
|
if (!member.isOnline || !member.hasPartyStats) continue;
|
|
|
|
|
if (member.posX == 0 && member.posY == 0) continue;
|
|
|
|
|
// posY → canonical X (north), posX → canonical Y (west)
|
|
|
|
|
float wowX = static_cast<float>(member.posY);
|
|
|
|
|
float wowY = static_cast<float>(member.posX);
|
|
|
|
|
glm::vec3 rpos = core::coords::canonicalToRender(glm::vec3(wowX, wowY, 0.0f));
|
|
|
|
|
auto ent = gameHandler.getEntityManager().getEntity(member.guid);
|
|
|
|
|
uint8_t cid = entityClassId(ent.get());
|
|
|
|
|
ImU32 col = (cid != 0)
|
|
|
|
|
? classColorU32(cid, 230)
|
|
|
|
|
: (member.guid == partyData.leaderGuid
|
|
|
|
|
? IM_COL32(255, 210, 0, 230)
|
|
|
|
|
: IM_COL32(100, 180, 255, 230));
|
|
|
|
|
dots.push_back({ rpos, col, member.name });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
wm->setPartyDots(std::move(dots));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 19:01:03 -07:00
|
|
|
// Taxi node markers on world map
|
|
|
|
|
{
|
|
|
|
|
std::vector<rendering::WorldMapTaxiNode> taxiNodes;
|
|
|
|
|
const auto& nodes = gameHandler.getTaxiNodes();
|
|
|
|
|
taxiNodes.reserve(nodes.size());
|
|
|
|
|
for (const auto& [id, node] : nodes) {
|
|
|
|
|
rendering::WorldMapTaxiNode wtn;
|
|
|
|
|
wtn.id = node.id;
|
|
|
|
|
wtn.mapId = node.mapId;
|
|
|
|
|
wtn.wowX = node.x;
|
|
|
|
|
wtn.wowY = node.y;
|
|
|
|
|
wtn.wowZ = node.z;
|
|
|
|
|
wtn.name = node.name;
|
|
|
|
|
wtn.known = gameHandler.isKnownTaxiNode(id);
|
|
|
|
|
taxiNodes.push_back(std::move(wtn));
|
|
|
|
|
}
|
|
|
|
|
wm->setTaxiNodes(std::move(taxiNodes));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 15:00:29 -07:00
|
|
|
// Quest POI markers on world map (from SMSG_QUEST_POI_QUERY_RESPONSE / gossip POIs)
|
|
|
|
|
{
|
|
|
|
|
std::vector<rendering::WorldMap::QuestPoi> qpois;
|
|
|
|
|
for (const auto& poi : gameHandler.getGossipPois()) {
|
|
|
|
|
rendering::WorldMap::QuestPoi qp;
|
|
|
|
|
qp.wowX = poi.x;
|
|
|
|
|
qp.wowY = poi.y;
|
|
|
|
|
qp.name = poi.name;
|
|
|
|
|
qpois.push_back(std::move(qp));
|
|
|
|
|
}
|
|
|
|
|
wm->setQuestPois(std::move(qpois));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 19:52:17 -07:00
|
|
|
// Corpse marker: show skull X on world map when ghost with unclaimed corpse
|
|
|
|
|
{
|
|
|
|
|
float corpseCanX = 0.0f, corpseCanY = 0.0f;
|
|
|
|
|
bool ghostWithCorpse = gameHandler.isPlayerGhost() &&
|
|
|
|
|
gameHandler.getCorpseCanonicalPos(corpseCanX, corpseCanY);
|
|
|
|
|
glm::vec3 corpseRender = ghostWithCorpse
|
|
|
|
|
? core::coords::canonicalToRender(glm::vec3(corpseCanX, corpseCanY, 0.0f))
|
|
|
|
|
: glm::vec3{};
|
|
|
|
|
wm->setCorpsePos(ghostWithCorpse, corpseRender);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-04 22:27:45 -08:00
|
|
|
glm::vec3 playerPos = renderer->getCharacterPosition();
|
2026-03-17 14:10:56 -07:00
|
|
|
float playerYaw = renderer->getCharacterYaw();
|
2026-02-04 22:27:45 -08:00
|
|
|
auto* window = app.getWindow();
|
|
|
|
|
int screenW = window ? window->getWidth() : 1280;
|
|
|
|
|
int screenH = window ? window->getHeight() : 720;
|
2026-03-17 14:10:56 -07:00
|
|
|
wm->render(playerPos, screenW, screenH, playerYaw);
|
2026-03-13 01:27:30 -07:00
|
|
|
|
|
|
|
|
// Sync showWorldMap_ if the map closed itself (e.g. ESC key inside the overlay).
|
|
|
|
|
if (!wm->isOpen()) showWorldMap_ = false;
|
2026-02-04 22:27:45 -08:00
|
|
|
}
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
// ============================================================
|
|
|
|
|
// Action Bar (Phase 3)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
2026-02-22 03:32:08 -08:00
|
|
|
VkDescriptorSet GameScreen::getSpellIcon(uint32_t spellId, pipeline::AssetManager* am) {
|
|
|
|
|
if (spellId == 0 || !am) return VK_NULL_HANDLE;
|
2026-02-06 14:30:54 -08:00
|
|
|
|
|
|
|
|
// Check cache first
|
|
|
|
|
auto cit = spellIconCache_.find(spellId);
|
|
|
|
|
if (cit != spellIconCache_.end()) return cit->second;
|
|
|
|
|
|
|
|
|
|
// Lazy-load SpellIcon.dbc and Spell.dbc icon IDs
|
|
|
|
|
if (!spellIconDbLoaded_) {
|
|
|
|
|
spellIconDbLoaded_ = true;
|
|
|
|
|
|
|
|
|
|
// Load SpellIcon.dbc: field 0 = ID, field 1 = icon path
|
|
|
|
|
auto iconDbc = am->loadDBC("SpellIcon.dbc");
|
2026-02-12 22:56:36 -08:00
|
|
|
const auto* iconL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("SpellIcon") : nullptr;
|
2026-02-06 14:30:54 -08:00
|
|
|
if (iconDbc && iconDbc->isLoaded()) {
|
|
|
|
|
for (uint32_t i = 0; i < iconDbc->getRecordCount(); i++) {
|
2026-02-12 22:56:36 -08:00
|
|
|
uint32_t id = iconDbc->getUInt32(i, iconL ? (*iconL)["ID"] : 0);
|
|
|
|
|
std::string path = iconDbc->getString(i, iconL ? (*iconL)["Path"] : 1);
|
2026-02-06 14:30:54 -08:00
|
|
|
if (!path.empty() && id > 0) {
|
|
|
|
|
spellIconPaths_[id] = path;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 05:27:03 -08:00
|
|
|
// Load Spell.dbc: SpellIconID field
|
2026-02-06 14:30:54 -08:00
|
|
|
auto spellDbc = am->loadDBC("Spell.dbc");
|
2026-02-12 22:56:36 -08:00
|
|
|
const auto* spellL = pipeline::getActiveDBCLayout() ? pipeline::getActiveDBCLayout()->getLayout("Spell") : nullptr;
|
2026-02-17 05:27:03 -08:00
|
|
|
if (spellDbc && spellDbc->isLoaded()) {
|
|
|
|
|
uint32_t fieldCount = spellDbc->getFieldCount();
|
2026-03-11 05:26:38 -07:00
|
|
|
// Helper to load icons for a given field layout
|
2026-02-17 05:27:03 -08:00
|
|
|
auto tryLoadIcons = [&](uint32_t idField, uint32_t iconField) {
|
|
|
|
|
spellIconIds_.clear();
|
|
|
|
|
if (iconField >= fieldCount) return;
|
|
|
|
|
for (uint32_t i = 0; i < spellDbc->getRecordCount(); i++) {
|
|
|
|
|
uint32_t id = spellDbc->getUInt32(i, idField);
|
|
|
|
|
uint32_t iconId = spellDbc->getUInt32(i, iconField);
|
|
|
|
|
if (id > 0 && iconId > 0) {
|
|
|
|
|
spellIconIds_[id] = iconId;
|
|
|
|
|
}
|
2026-02-06 14:30:54 -08:00
|
|
|
}
|
2026-02-17 05:27:03 -08:00
|
|
|
};
|
2026-03-11 05:26:38 -07:00
|
|
|
|
2026-03-17 07:42:01 -07:00
|
|
|
// Use expansion-aware layout if available AND the DBC field count
|
|
|
|
|
// matches the expansion's expected format. Classic=173, TBC=216,
|
|
|
|
|
// WotLK=234 fields. When Classic is active but the base WotLK DBC
|
|
|
|
|
// is loaded (234 fields), field 117 is NOT IconID — we must use
|
|
|
|
|
// the WotLK field 133 instead.
|
|
|
|
|
uint32_t iconField = 133; // WotLK default
|
|
|
|
|
uint32_t idField = 0;
|
2026-03-11 05:26:38 -07:00
|
|
|
if (spellL) {
|
2026-03-17 07:42:01 -07:00
|
|
|
uint32_t layoutIcon = (*spellL)["IconID"];
|
|
|
|
|
// Only trust the expansion layout if the DBC has a compatible
|
|
|
|
|
// field count (within ~20 of the layout's icon field).
|
|
|
|
|
if (layoutIcon < fieldCount && fieldCount <= layoutIcon + 20) {
|
|
|
|
|
iconField = layoutIcon;
|
|
|
|
|
idField = (*spellL)["ID"];
|
|
|
|
|
}
|
2026-02-06 14:30:54 -08:00
|
|
|
}
|
2026-03-17 07:42:01 -07:00
|
|
|
tryLoadIcons(idField, iconField);
|
2026-02-06 14:30:54 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 20:17:41 -07:00
|
|
|
// Rate-limit GPU uploads per frame to prevent stalls when many icons are uncached
|
|
|
|
|
// (e.g., first login, after loading screen, or many new auras appearing at once).
|
|
|
|
|
static int gsLoadsThisFrame = 0;
|
|
|
|
|
static int gsLastImGuiFrame = -1;
|
|
|
|
|
int gsCurFrame = ImGui::GetFrameCount();
|
|
|
|
|
if (gsCurFrame != gsLastImGuiFrame) { gsLoadsThisFrame = 0; gsLastImGuiFrame = gsCurFrame; }
|
|
|
|
|
if (gsLoadsThisFrame >= 4) return VK_NULL_HANDLE; // defer — do NOT cache null here
|
|
|
|
|
|
2026-02-06 14:30:54 -08:00
|
|
|
// Look up spellId -> SpellIconID -> icon path
|
|
|
|
|
auto iit = spellIconIds_.find(spellId);
|
|
|
|
|
if (iit == spellIconIds_.end()) {
|
2026-02-22 03:32:08 -08:00
|
|
|
spellIconCache_[spellId] = VK_NULL_HANDLE;
|
|
|
|
|
return VK_NULL_HANDLE;
|
2026-02-06 14:30:54 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto pit = spellIconPaths_.find(iit->second);
|
|
|
|
|
if (pit == spellIconPaths_.end()) {
|
2026-02-22 03:32:08 -08:00
|
|
|
spellIconCache_[spellId] = VK_NULL_HANDLE;
|
|
|
|
|
return VK_NULL_HANDLE;
|
2026-02-06 14:30:54 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Path from DBC has no extension — append .blp
|
|
|
|
|
std::string iconPath = pit->second + ".blp";
|
|
|
|
|
auto blpData = am->readFile(iconPath);
|
|
|
|
|
if (blpData.empty()) {
|
2026-02-22 03:32:08 -08:00
|
|
|
spellIconCache_[spellId] = VK_NULL_HANDLE;
|
|
|
|
|
return VK_NULL_HANDLE;
|
2026-02-06 14:30:54 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto image = pipeline::BLPLoader::load(blpData);
|
|
|
|
|
if (!image.isValid()) {
|
2026-02-22 03:32:08 -08:00
|
|
|
spellIconCache_[spellId] = VK_NULL_HANDLE;
|
|
|
|
|
return VK_NULL_HANDLE;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Upload to Vulkan via VkContext
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* window = services_.window;
|
2026-02-22 03:32:08 -08:00
|
|
|
auto* vkCtx = window ? window->getVkContext() : nullptr;
|
|
|
|
|
if (!vkCtx) {
|
|
|
|
|
spellIconCache_[spellId] = VK_NULL_HANDLE;
|
|
|
|
|
return VK_NULL_HANDLE;
|
2026-02-06 14:30:54 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-11 20:17:41 -07:00
|
|
|
++gsLoadsThisFrame;
|
2026-02-22 03:32:08 -08:00
|
|
|
VkDescriptorSet ds = vkCtx->uploadImGuiTexture(image.data.data(), image.width, image.height);
|
|
|
|
|
spellIconCache_[spellId] = ds;
|
|
|
|
|
return ds;
|
2026-02-06 14:30:54 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-20 08:11:13 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Stance / Form / Presence Bar
|
|
|
|
|
// Shown for Warriors (stances), Death Knights (presences),
|
|
|
|
|
// Druids (shapeshift forms), Rogues (stealth), Priests (Shadowform).
|
|
|
|
|
// Buttons display the player's known stance/form spells.
|
|
|
|
|
// Active form is detected by checking permanent player auras.
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Bag Bar
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// XP Bar
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Reputation Bar
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Cast Bar (Phase 3)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Mirror Timers (breath / fatigue / feign death)
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderMirrorTimers(game::GameHandler& gameHandler) {
|
2026-03-10 15:56:41 -07:00
|
|
|
ImVec2 displaySize = ImGui::GetIO().DisplaySize;
|
|
|
|
|
float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f;
|
|
|
|
|
float screenH = displaySize.y > 0.0f ? displaySize.y : 720.0f;
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
static constexpr struct { const char* label; ImVec4 color; } kTimerInfo[3] = {
|
|
|
|
|
{ "Fatigue", ImVec4(0.8f, 0.4f, 0.1f, 1.0f) },
|
|
|
|
|
{ "Breath", ImVec4(0.2f, 0.5f, 1.0f, 1.0f) },
|
|
|
|
|
{ "Feign", kColorGray },
|
|
|
|
|
};
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
float barW = 280.0f;
|
|
|
|
|
float barH = 36.0f;
|
|
|
|
|
float barX = (screenW - barW) / 2.0f;
|
|
|
|
|
float baseY = screenH - 160.0f; // Just above the cast bar slot
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
|
|
|
|
|
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar |
|
|
|
|
|
ImGuiWindowFlags_NoInputs;
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
for (int i = 0; i < 3; ++i) {
|
|
|
|
|
const auto& t = gameHandler.getMirrorTimer(i);
|
|
|
|
|
if (!t.active || t.maxValue <= 0) continue;
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
float frac = static_cast<float>(t.value) / static_cast<float>(t.maxValue);
|
|
|
|
|
frac = std::max(0.0f, std::min(1.0f, frac));
|
2026-03-20 08:07:20 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
char winId[32];
|
|
|
|
|
std::snprintf(winId, sizeof(winId), "##MirrorTimer%d", i);
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(barX, baseY - i * (barH + 4.0f)), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(barW, barH), ImGuiCond_Always);
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.05f, 0.05f, 0.88f));
|
|
|
|
|
if (ImGui::Begin(winId, nullptr, flags)) {
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, kTimerInfo[i].color);
|
|
|
|
|
char overlay[48];
|
|
|
|
|
float sec = static_cast<float>(t.value) / 1000.0f;
|
|
|
|
|
std::snprintf(overlay, sizeof(overlay), "%s %.0fs", kTimerInfo[i].label, sec);
|
|
|
|
|
ImGui::ProgressBar(frac, ImVec2(-1, 20), overlay);
|
|
|
|
|
ImGui::PopStyleColor();
|
2026-03-12 05:57:45 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::End();
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 05:57:45 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Cooldown Tracker — floating panel showing all active spell CDs
|
|
|
|
|
// ============================================================
|
2026-03-12 06:01:42 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Quest Objective Tracker (right-side HUD)
|
|
|
|
|
// ============================================================
|
2026-02-05 15:07:13 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) {
|
|
|
|
|
const auto& questLog = gameHandler.getQuestLog();
|
|
|
|
|
if (questLog.empty()) return;
|
2026-02-06 14:30:54 -08:00
|
|
|
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* window = services_.window;
|
2026-03-31 19:49:52 +03:00
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
2026-03-18 03:16:05 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
constexpr float TRACKER_W = 220.0f;
|
|
|
|
|
constexpr float RIGHT_MARGIN = 10.0f;
|
|
|
|
|
constexpr int MAX_QUESTS = 5;
|
2026-03-17 13:59:42 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Build display list: tracked quests only, or all quests if none tracked
|
|
|
|
|
const auto& trackedIds = gameHandler.getTrackedQuestIds();
|
|
|
|
|
std::vector<const game::GameHandler::QuestLogEntry*> toShow;
|
|
|
|
|
toShow.reserve(MAX_QUESTS);
|
|
|
|
|
if (!trackedIds.empty()) {
|
|
|
|
|
for (const auto& q : questLog) {
|
|
|
|
|
if (q.questId == 0) continue;
|
|
|
|
|
if (trackedIds.count(q.questId)) toShow.push_back(&q);
|
|
|
|
|
if (static_cast<int>(toShow.size()) >= MAX_QUESTS) break;
|
2026-03-10 06:04:43 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
}
|
|
|
|
|
// Fallback: show all quests if nothing is tracked
|
|
|
|
|
if (toShow.empty()) {
|
|
|
|
|
for (const auto& q : questLog) {
|
|
|
|
|
if (q.questId == 0) continue;
|
|
|
|
|
toShow.push_back(&q);
|
|
|
|
|
if (static_cast<int>(toShow.size()) >= MAX_QUESTS) break;
|
2026-03-18 04:30:33 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
}
|
|
|
|
|
if (toShow.empty()) return;
|
|
|
|
|
|
|
|
|
|
float screenH = ImGui::GetIO().DisplaySize.y > 0.0f ? ImGui::GetIO().DisplaySize.y : 720.0f;
|
|
|
|
|
|
|
|
|
|
// Default position: top-right, below minimap + buff bar space.
|
|
|
|
|
// questTrackerRightOffset_ stores pixels from the right edge so the tracker
|
|
|
|
|
// stays anchored to the right side when the window is resized.
|
|
|
|
|
if (!questTrackerPosInit_ || questTrackerRightOffset_ < 0.0f) {
|
|
|
|
|
questTrackerRightOffset_ = TRACKER_W + RIGHT_MARGIN; // default: right-aligned
|
|
|
|
|
questTrackerPos_.y = 320.0f;
|
|
|
|
|
questTrackerPosInit_ = true;
|
|
|
|
|
}
|
|
|
|
|
// Recompute X from right offset every frame (handles window resize)
|
|
|
|
|
questTrackerPos_.x = screenW - questTrackerRightOffset_;
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(questTrackerPos_, ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(questTrackerSize_, ImGuiCond_FirstUseEver);
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar |
|
|
|
|
|
ImGuiWindowFlags_NoCollapse |
|
|
|
|
|
ImGuiWindowFlags_NoNav |
|
|
|
|
|
ImGuiWindowFlags_NoBringToFrontOnFocus;
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.55f));
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.0f, 6.0f));
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, 2.0f));
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##QuestTracker", nullptr, flags)) {
|
|
|
|
|
for (int i = 0; i < static_cast<int>(toShow.size()); ++i) {
|
|
|
|
|
const auto& q = *toShow[i];
|
2026-03-18 04:30:33 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Clickable quest title — opens quest log
|
|
|
|
|
ImGui::PushID(q.questId);
|
|
|
|
|
ImVec4 titleCol = q.complete ? colors::kWarmGold
|
|
|
|
|
: ImVec4(1.0f, 1.0f, 0.85f, 1.0f);
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, titleCol);
|
|
|
|
|
if (ImGui::Selectable(q.title.c_str(), false,
|
|
|
|
|
ImGuiSelectableFlags_DontClosePopups, ImVec2(ImGui::GetContentRegionAvail().x, 0))) {
|
|
|
|
|
questLogScreen.openAndSelectQuest(q.questId);
|
2026-03-10 06:04:43 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
if (ImGui::IsItemHovered() && !ImGui::IsPopupOpen("##QTCtx")) {
|
|
|
|
|
ImGui::SetTooltip("Click: open Quest Log | Right-click: tracking options");
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::PopStyleColor();
|
2026-03-11 23:59:51 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Right-click context menu for quest tracker entry
|
|
|
|
|
if (ImGui::BeginPopupContextItem("##QTCtx")) {
|
|
|
|
|
ImGui::TextDisabled("%s", q.title.c_str());
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
if (ImGui::MenuItem("Open in Quest Log")) {
|
|
|
|
|
questLogScreen.openAndSelectQuest(q.questId);
|
|
|
|
|
}
|
|
|
|
|
bool tracked = gameHandler.isQuestTracked(q.questId);
|
|
|
|
|
if (tracked) {
|
|
|
|
|
if (ImGui::MenuItem("Stop Tracking")) {
|
|
|
|
|
gameHandler.setQuestTracked(q.questId, false);
|
2026-03-11 23:59:51 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
} else {
|
|
|
|
|
if (ImGui::MenuItem("Track")) {
|
|
|
|
|
gameHandler.setQuestTracked(q.questId, true);
|
2026-03-18 02:07:59 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
}
|
|
|
|
|
if (gameHandler.isInGroup() && !q.complete) {
|
|
|
|
|
if (ImGui::MenuItem("Share Quest")) {
|
|
|
|
|
gameHandler.shareQuestWithParty(q.questId);
|
2026-03-18 02:07:59 -07:00
|
|
|
}
|
2026-03-11 23:59:51 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
if (!q.complete) {
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
if (ImGui::MenuItem("Abandon Quest")) {
|
|
|
|
|
gameHandler.abandonQuest(q.questId);
|
|
|
|
|
gameHandler.setQuestTracked(q.questId, false);
|
|
|
|
|
}
|
2026-03-11 23:59:51 -07:00
|
|
|
}
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::PopID();
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Objectives line (condensed)
|
|
|
|
|
if (q.complete) {
|
|
|
|
|
ImGui::TextColored(colors::kActiveGreen, " (Complete)");
|
|
|
|
|
} else {
|
|
|
|
|
// Kill counts — green when complete, gray when in progress
|
|
|
|
|
for (const auto& [entry, progress] : q.killCounts) {
|
|
|
|
|
bool objDone = (progress.first >= progress.second && progress.second > 0);
|
|
|
|
|
ImVec4 objColor = objDone ? kColorGreen
|
|
|
|
|
: ImVec4(0.75f, 0.75f, 0.75f, 1.0f);
|
|
|
|
|
std::string name = gameHandler.getCachedCreatureName(entry);
|
|
|
|
|
if (name.empty()) {
|
|
|
|
|
const auto* goInfo = gameHandler.getCachedGameObjectInfo(entry);
|
|
|
|
|
if (goInfo && !goInfo->name.empty()) name = goInfo->name;
|
|
|
|
|
}
|
|
|
|
|
if (!name.empty()) {
|
|
|
|
|
ImGui::TextColored(objColor,
|
|
|
|
|
" %s: %u/%u", name.c_str(),
|
|
|
|
|
progress.first, progress.second);
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::TextColored(objColor,
|
|
|
|
|
" %u/%u", progress.first, progress.second);
|
2026-03-20 08:18:28 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
// Item counts — green when complete, gray when in progress
|
|
|
|
|
for (const auto& [itemId, count] : q.itemCounts) {
|
|
|
|
|
uint32_t required = 1;
|
|
|
|
|
auto reqIt = q.requiredItemCounts.find(itemId);
|
|
|
|
|
if (reqIt != q.requiredItemCounts.end()) required = reqIt->second;
|
|
|
|
|
bool objDone = (count >= required);
|
|
|
|
|
ImVec4 objColor = objDone ? kColorGreen
|
|
|
|
|
: ImVec4(0.75f, 0.75f, 0.75f, 1.0f);
|
|
|
|
|
const auto* info = gameHandler.getItemInfo(itemId);
|
|
|
|
|
const char* itemName = (info && !info->name.empty()) ? info->name.c_str() : nullptr;
|
|
|
|
|
|
|
|
|
|
// Show small icon if available
|
|
|
|
|
uint32_t dispId = (info && info->displayInfoId) ? info->displayInfoId : 0;
|
|
|
|
|
VkDescriptorSet iconTex = dispId ? inventoryScreen.getItemIcon(dispId) : VK_NULL_HANDLE;
|
|
|
|
|
if (iconTex) {
|
|
|
|
|
ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(12, 12));
|
|
|
|
|
if (info && info->valid && ImGui::IsItemHovered()) {
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
|
|
|
|
ImGui::EndTooltip();
|
2026-03-20 10:12:42 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::SameLine(0, 3);
|
|
|
|
|
ImGui::TextColored(objColor,
|
|
|
|
|
"%s: %u/%u", itemName ? itemName : "Item", count, required);
|
|
|
|
|
if (info && info->valid && ImGui::IsItemHovered()) {
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
}
|
|
|
|
|
} else if (itemName) {
|
|
|
|
|
ImGui::TextColored(objColor,
|
|
|
|
|
" %s: %u/%u", itemName, count, required);
|
|
|
|
|
if (info && info->valid && ImGui::IsItemHovered()) {
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
|
|
|
|
ImGui::EndTooltip();
|
2026-03-20 10:12:42 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
} else {
|
|
|
|
|
ImGui::TextColored(objColor,
|
|
|
|
|
" Item: %u/%u", count, required);
|
2026-03-20 08:18:28 -07:00
|
|
|
}
|
2026-03-18 02:07:59 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
if (q.killCounts.empty() && q.itemCounts.empty() && !q.objectives.empty()) {
|
|
|
|
|
const std::string& obj = q.objectives;
|
|
|
|
|
if (obj.size() > 40) {
|
|
|
|
|
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f),
|
|
|
|
|
" %.37s...", obj.c_str());
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::TextColored(ImVec4(0.75f, 0.75f, 0.75f, 1.0f),
|
|
|
|
|
" %s", obj.c_str());
|
2026-03-12 05:38:13 -07:00
|
|
|
}
|
2026-03-12 04:12:07 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 05:12:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
if (i < static_cast<int>(toShow.size()) - 1) {
|
|
|
|
|
ImGui::Spacing();
|
2026-03-12 05:12:58 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Capture position and size after drag/resize
|
|
|
|
|
ImVec2 newPos = ImGui::GetWindowPos();
|
|
|
|
|
ImVec2 newSize = ImGui::GetWindowSize();
|
|
|
|
|
bool changed = false;
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Clamp within screen
|
|
|
|
|
newPos.x = std::clamp(newPos.x, 0.0f, screenW - newSize.x);
|
|
|
|
|
newPos.y = std::clamp(newPos.y, 0.0f, screenH - 40.0f);
|
2026-03-18 02:07:59 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
if (std::abs(newPos.x - questTrackerPos_.x) > 0.5f ||
|
|
|
|
|
std::abs(newPos.y - questTrackerPos_.y) > 0.5f) {
|
|
|
|
|
questTrackerPos_ = newPos;
|
|
|
|
|
// Update right offset so resizes keep the new position anchored
|
|
|
|
|
questTrackerRightOffset_ = screenW - newPos.x;
|
|
|
|
|
changed = true;
|
2026-03-18 02:07:59 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
if (std::abs(newSize.x - questTrackerSize_.x) > 0.5f ||
|
|
|
|
|
std::abs(newSize.y - questTrackerSize_.y) > 0.5f) {
|
|
|
|
|
questTrackerSize_ = newSize;
|
|
|
|
|
changed = true;
|
2026-03-18 02:07:59 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
if (changed) saveSettings();
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::PopStyleVar(2);
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
ImGui::PopStyleColor();
|
2026-03-31 19:49:52 +03:00
|
|
|
}
|
2026-03-10 15:56:41 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Raid Warning / Boss Emote Center-Screen Overlay
|
|
|
|
|
// ============================================================
|
2026-03-10 15:56:41 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Floating Combat Text (Phase 2)
|
|
|
|
|
// ============================================================
|
2026-03-10 15:56:41 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// DPS / HPS Meter
|
|
|
|
|
// ============================================================
|
2026-03-10 15:56:41 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Nameplates — world-space health bars projected to screen
|
|
|
|
|
// ============================================================
|
2026-03-10 15:56:41 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
void GameScreen::renderNameplates(game::GameHandler& gameHandler) {
|
|
|
|
|
if (gameHandler.getState() != game::WorldState::IN_WORLD) return;
|
2026-03-10 15:56:41 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Reset mouseover each frame; we'll set it below when the cursor is over a nameplate
|
|
|
|
|
gameHandler.setMouseoverGuid(0);
|
2026-03-12 17:25:00 -07:00
|
|
|
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* appRenderer = services_.renderer;
|
2026-03-31 19:49:52 +03:00
|
|
|
if (!appRenderer) return;
|
|
|
|
|
rendering::Camera* camera = appRenderer->getCamera();
|
|
|
|
|
if (!camera) return;
|
2026-03-12 17:25:00 -07:00
|
|
|
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* window = services_.window;
|
2026-03-31 19:49:52 +03:00
|
|
|
if (!window) return;
|
|
|
|
|
const float screenW = static_cast<float>(window->getWidth());
|
|
|
|
|
const float screenH = static_cast<float>(window->getHeight());
|
2026-02-06 19:24:44 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
const glm::mat4 viewProj = camera->getProjectionMatrix() * camera->getViewMatrix();
|
|
|
|
|
const glm::vec3 camPos = camera->getPosition();
|
|
|
|
|
const uint64_t playerGuid = gameHandler.getPlayerGuid();
|
|
|
|
|
const uint64_t targetGuid = gameHandler.getTargetGuid();
|
2026-02-06 19:24:44 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Build set of creature entries that are kill objectives in active (incomplete) quests.
|
|
|
|
|
std::unordered_set<uint32_t> questKillEntries;
|
|
|
|
|
{
|
|
|
|
|
const auto& questLog = gameHandler.getQuestLog();
|
|
|
|
|
const auto& trackedIds = gameHandler.getTrackedQuestIds();
|
|
|
|
|
for (const auto& q : questLog) {
|
|
|
|
|
if (q.complete || q.questId == 0) continue;
|
|
|
|
|
// Only highlight for tracked quests (or all if nothing tracked).
|
|
|
|
|
if (!trackedIds.empty() && !trackedIds.count(q.questId)) continue;
|
|
|
|
|
for (const auto& obj : q.killObjectives) {
|
|
|
|
|
if (obj.npcOrGoId > 0 && obj.required > 0) {
|
|
|
|
|
// Check if not already completed.
|
|
|
|
|
auto it = q.killCounts.find(static_cast<uint32_t>(obj.npcOrGoId));
|
|
|
|
|
if (it == q.killCounts.end() || it->second.first < it->second.second) {
|
|
|
|
|
questKillEntries.insert(static_cast<uint32_t>(obj.npcOrGoId));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-06 19:24:44 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
feat: add stance/form/presence bar for Warriors, Druids, Death Knights, Rogues, Priests
Renders a stance bar to the left of the main action bar showing the
player's known stance spells filtered to only those they have learned:
- Warrior: Battle Stance, Defensive Stance, Berserker Stance
- Death Knight: Blood Presence, Frost Presence, Unholy Presence
- Druid: Bear/Dire Bear, Cat, Travel, Aquatic, Moonkin, Tree, Flight forms
- Rogue: Stealth
- Priest: Shadowform
Active form detected from permanent player auras (maxDurationMs == -1).
Clicking an inactive stance casts the corresponding spell. Active stance
shown with green border/tint; inactive stances are slightly dimmed.
Spell name tooltips shown on hover using existing SpellbookScreen lookup.
2026-03-17 15:12:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImDrawList* drawList = ImGui::GetBackgroundDrawList();
|
feat: add stance/form/presence bar for Warriors, Druids, Death Knights, Rogues, Priests
Renders a stance bar to the left of the main action bar showing the
player's known stance spells filtered to only those they have learned:
- Warrior: Battle Stance, Defensive Stance, Berserker Stance
- Death Knight: Blood Presence, Frost Presence, Unholy Presence
- Druid: Bear/Dire Bear, Cat, Travel, Aquatic, Moonkin, Tree, Flight forms
- Rogue: Stealth
- Priest: Shadowform
Active form detected from permanent player auras (maxDurationMs == -1).
Clicking an inactive stance casts the corresponding spell. Active stance
shown with green border/tint; inactive stances are slightly dimmed.
Spell name tooltips shown on hover using existing SpellbookScreen lookup.
2026-03-17 15:12:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
for (const auto& [guid, entityPtr] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
if (!entityPtr || guid == playerGuid) continue;
|
feat: add stance/form/presence bar for Warriors, Druids, Death Knights, Rogues, Priests
Renders a stance bar to the left of the main action bar showing the
player's known stance spells filtered to only those they have learned:
- Warrior: Battle Stance, Defensive Stance, Berserker Stance
- Death Knight: Blood Presence, Frost Presence, Unholy Presence
- Druid: Bear/Dire Bear, Cat, Travel, Aquatic, Moonkin, Tree, Flight forms
- Rogue: Stealth
- Priest: Shadowform
Active form detected from permanent player auras (maxDurationMs == -1).
Clicking an inactive stance casts the corresponding spell. Active stance
shown with green border/tint; inactive stances are slightly dimmed.
Spell name tooltips shown on hover using existing SpellbookScreen lookup.
2026-03-17 15:12:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
if (!entityPtr->isUnit()) continue;
|
|
|
|
|
auto* unit = static_cast<game::Unit*>(entityPtr.get());
|
|
|
|
|
if (unit->getMaxHealth() == 0) continue;
|
feat: add stance/form/presence bar for Warriors, Druids, Death Knights, Rogues, Priests
Renders a stance bar to the left of the main action bar showing the
player's known stance spells filtered to only those they have learned:
- Warrior: Battle Stance, Defensive Stance, Berserker Stance
- Death Knight: Blood Presence, Frost Presence, Unholy Presence
- Druid: Bear/Dire Bear, Cat, Travel, Aquatic, Moonkin, Tree, Flight forms
- Rogue: Stealth
- Priest: Shadowform
Active form detected from permanent player auras (maxDurationMs == -1).
Clicking an inactive stance casts the corresponding spell. Active stance
shown with green border/tint; inactive stances are slightly dimmed.
Spell name tooltips shown on hover using existing SpellbookScreen lookup.
2026-03-17 15:12:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
bool isPlayer = (entityPtr->getType() == game::ObjectType::PLAYER);
|
|
|
|
|
bool isTarget = (guid == targetGuid);
|
feat: add stance/form/presence bar for Warriors, Druids, Death Knights, Rogues, Priests
Renders a stance bar to the left of the main action bar showing the
player's known stance spells filtered to only those they have learned:
- Warrior: Battle Stance, Defensive Stance, Berserker Stance
- Death Knight: Blood Presence, Frost Presence, Unholy Presence
- Druid: Bear/Dire Bear, Cat, Travel, Aquatic, Moonkin, Tree, Flight forms
- Rogue: Stealth
- Priest: Shadowform
Active form detected from permanent player auras (maxDurationMs == -1).
Clicking an inactive stance casts the corresponding spell. Active stance
shown with green border/tint; inactive stances are slightly dimmed.
Spell name tooltips shown on hover using existing SpellbookScreen lookup.
2026-03-17 15:12:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Player nameplates use Shift+V toggle; NPC/enemy nameplates use V toggle
|
|
|
|
|
if (isPlayer && !settingsPanel_.showFriendlyNameplates_) continue;
|
|
|
|
|
if (!isPlayer && !showNameplates_) continue;
|
feat: add stance/form/presence bar for Warriors, Druids, Death Knights, Rogues, Priests
Renders a stance bar to the left of the main action bar showing the
player's known stance spells filtered to only those they have learned:
- Warrior: Battle Stance, Defensive Stance, Berserker Stance
- Death Knight: Blood Presence, Frost Presence, Unholy Presence
- Druid: Bear/Dire Bear, Cat, Travel, Aquatic, Moonkin, Tree, Flight forms
- Rogue: Stealth
- Priest: Shadowform
Active form detected from permanent player auras (maxDurationMs == -1).
Clicking an inactive stance casts the corresponding spell. Active stance
shown with green border/tint; inactive stances are slightly dimmed.
Spell name tooltips shown on hover using existing SpellbookScreen lookup.
2026-03-17 15:12:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// For corpses (dead units), only show a minimal grey nameplate if selected
|
|
|
|
|
bool isCorpse = (unit->getHealth() == 0);
|
|
|
|
|
if (isCorpse && !isTarget) continue;
|
feat: add stance/form/presence bar for Warriors, Druids, Death Knights, Rogues, Priests
Renders a stance bar to the left of the main action bar showing the
player's known stance spells filtered to only those they have learned:
- Warrior: Battle Stance, Defensive Stance, Berserker Stance
- Death Knight: Blood Presence, Frost Presence, Unholy Presence
- Druid: Bear/Dire Bear, Cat, Travel, Aquatic, Moonkin, Tree, Flight forms
- Rogue: Stealth
- Priest: Shadowform
Active form detected from permanent player auras (maxDurationMs == -1).
Clicking an inactive stance casts the corresponding spell. Active stance
shown with green border/tint; inactive stances are slightly dimmed.
Spell name tooltips shown on hover using existing SpellbookScreen lookup.
2026-03-17 15:12:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Prefer the renderer's actual instance position so the nameplate tracks the
|
|
|
|
|
// rendered model exactly (avoids drift from the parallel entity interpolator).
|
|
|
|
|
glm::vec3 renderPos;
|
|
|
|
|
if (!core::Application::getInstance().getRenderPositionForGuid(guid, renderPos)) {
|
|
|
|
|
renderPos = core::coords::canonicalToRender(
|
|
|
|
|
glm::vec3(unit->getX(), unit->getY(), unit->getZ()));
|
|
|
|
|
}
|
|
|
|
|
renderPos.z += 2.3f;
|
feat: add stance/form/presence bar for Warriors, Druids, Death Knights, Rogues, Priests
Renders a stance bar to the left of the main action bar showing the
player's known stance spells filtered to only those they have learned:
- Warrior: Battle Stance, Defensive Stance, Berserker Stance
- Death Knight: Blood Presence, Frost Presence, Unholy Presence
- Druid: Bear/Dire Bear, Cat, Travel, Aquatic, Moonkin, Tree, Flight forms
- Rogue: Stealth
- Priest: Shadowform
Active form detected from permanent player auras (maxDurationMs == -1).
Clicking an inactive stance casts the corresponding spell. Active stance
shown with green border/tint; inactive stances are slightly dimmed.
Spell name tooltips shown on hover using existing SpellbookScreen lookup.
2026-03-17 15:12:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Cull distance: target or other players up to 40 units; NPC others up to 20 units
|
|
|
|
|
glm::vec3 nameDelta = renderPos - camPos;
|
|
|
|
|
float distSq = glm::dot(nameDelta, nameDelta);
|
|
|
|
|
float cullDist = (isTarget || isPlayer) ? 40.0f : 20.0f;
|
|
|
|
|
if (distSq > cullDist * cullDist) continue;
|
feat: add stance/form/presence bar for Warriors, Druids, Death Knights, Rogues, Priests
Renders a stance bar to the left of the main action bar showing the
player's known stance spells filtered to only those they have learned:
- Warrior: Battle Stance, Defensive Stance, Berserker Stance
- Death Knight: Blood Presence, Frost Presence, Unholy Presence
- Druid: Bear/Dire Bear, Cat, Travel, Aquatic, Moonkin, Tree, Flight forms
- Rogue: Stealth
- Priest: Shadowform
Active form detected from permanent player auras (maxDurationMs == -1).
Clicking an inactive stance casts the corresponding spell. Active stance
shown with green border/tint; inactive stances are slightly dimmed.
Spell name tooltips shown on hover using existing SpellbookScreen lookup.
2026-03-17 15:12:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Project to clip space
|
|
|
|
|
glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f);
|
|
|
|
|
if (clipPos.w <= 0.01f) continue; // Behind camera
|
feat: add stance/form/presence bar for Warriors, Druids, Death Knights, Rogues, Priests
Renders a stance bar to the left of the main action bar showing the
player's known stance spells filtered to only those they have learned:
- Warrior: Battle Stance, Defensive Stance, Berserker Stance
- Death Knight: Blood Presence, Frost Presence, Unholy Presence
- Druid: Bear/Dire Bear, Cat, Travel, Aquatic, Moonkin, Tree, Flight forms
- Rogue: Stealth
- Priest: Shadowform
Active form detected from permanent player auras (maxDurationMs == -1).
Clicking an inactive stance casts the corresponding spell. Active stance
shown with green border/tint; inactive stances are slightly dimmed.
Spell name tooltips shown on hover using existing SpellbookScreen lookup.
2026-03-17 15:12:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
glm::vec3 ndc = glm::vec3(clipPos) / clipPos.w;
|
|
|
|
|
if (ndc.x < -1.2f || ndc.x > 1.2f || ndc.y < -1.2f || ndc.y > 1.2f) continue;
|
feat: add stance/form/presence bar for Warriors, Druids, Death Knights, Rogues, Priests
Renders a stance bar to the left of the main action bar showing the
player's known stance spells filtered to only those they have learned:
- Warrior: Battle Stance, Defensive Stance, Berserker Stance
- Death Knight: Blood Presence, Frost Presence, Unholy Presence
- Druid: Bear/Dire Bear, Cat, Travel, Aquatic, Moonkin, Tree, Flight forms
- Rogue: Stealth
- Priest: Shadowform
Active form detected from permanent player auras (maxDurationMs == -1).
Clicking an inactive stance casts the corresponding spell. Active stance
shown with green border/tint; inactive stances are slightly dimmed.
Spell name tooltips shown on hover using existing SpellbookScreen lookup.
2026-03-17 15:12:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// NDC → screen pixels.
|
|
|
|
|
// The camera bakes the Vulkan Y-flip into the projection matrix, so
|
|
|
|
|
// NDC y = -1 is the top of the screen and y = 1 is the bottom.
|
|
|
|
|
// Map directly: sy = (ndc.y + 1) / 2 * screenH (no extra inversion).
|
|
|
|
|
float sx = (ndc.x * 0.5f + 0.5f) * screenW;
|
|
|
|
|
float sy = (ndc.y * 0.5f + 0.5f) * screenH;
|
feat: add stance/form/presence bar for Warriors, Druids, Death Knights, Rogues, Priests
Renders a stance bar to the left of the main action bar showing the
player's known stance spells filtered to only those they have learned:
- Warrior: Battle Stance, Defensive Stance, Berserker Stance
- Death Knight: Blood Presence, Frost Presence, Unholy Presence
- Druid: Bear/Dire Bear, Cat, Travel, Aquatic, Moonkin, Tree, Flight forms
- Rogue: Stealth
- Priest: Shadowform
Active form detected from permanent player auras (maxDurationMs == -1).
Clicking an inactive stance casts the corresponding spell. Active stance
shown with green border/tint; inactive stances are slightly dimmed.
Spell name tooltips shown on hover using existing SpellbookScreen lookup.
2026-03-17 15:12:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Fade out in the last 5 units of cull range
|
|
|
|
|
float fadeSq = (cullDist - 5.0f) * (cullDist - 5.0f);
|
|
|
|
|
float dist = std::sqrt(distSq);
|
|
|
|
|
float alpha = distSq < fadeSq ? 1.0f : 1.0f - (dist - (cullDist - 5.0f)) / 5.0f;
|
|
|
|
|
auto A = [&](int v) { return static_cast<int>(v * alpha); };
|
feat: add stance/form/presence bar for Warriors, Druids, Death Knights, Rogues, Priests
Renders a stance bar to the left of the main action bar showing the
player's known stance spells filtered to only those they have learned:
- Warrior: Battle Stance, Defensive Stance, Berserker Stance
- Death Knight: Blood Presence, Frost Presence, Unholy Presence
- Druid: Bear/Dire Bear, Cat, Travel, Aquatic, Moonkin, Tree, Flight forms
- Rogue: Stealth
- Priest: Shadowform
Active form detected from permanent player auras (maxDurationMs == -1).
Clicking an inactive stance casts the corresponding spell. Active stance
shown with green border/tint; inactive stances are slightly dimmed.
Spell name tooltips shown on hover using existing SpellbookScreen lookup.
2026-03-17 15:12:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Bar colour by hostility (grey for corpses)
|
|
|
|
|
ImU32 barColor, bgColor;
|
|
|
|
|
if (isCorpse) {
|
|
|
|
|
// Minimal grey bar for selected corpses (loot/skin targets)
|
|
|
|
|
barColor = IM_COL32(140, 140, 140, A(200));
|
|
|
|
|
bgColor = IM_COL32(70, 70, 70, A(160));
|
|
|
|
|
} else if (unit->isHostile()) {
|
|
|
|
|
// Check if mob is tapped by another player (grey nameplate)
|
|
|
|
|
uint32_t dynFlags = unit->getDynamicFlags();
|
|
|
|
|
bool tappedByOther = (dynFlags & 0x0004) != 0 && (dynFlags & 0x0008) == 0; // TAPPED but not TAPPED_BY_ALL_THREAT_LIST
|
|
|
|
|
if (tappedByOther) {
|
|
|
|
|
barColor = IM_COL32(160, 160, 160, A(200));
|
|
|
|
|
bgColor = IM_COL32(80, 80, 80, A(160));
|
|
|
|
|
} else {
|
|
|
|
|
barColor = IM_COL32(220, 60, 60, A(200));
|
|
|
|
|
bgColor = IM_COL32(100, 25, 25, A(160));
|
feat: add stance/form/presence bar for Warriors, Druids, Death Knights, Rogues, Priests
Renders a stance bar to the left of the main action bar showing the
player's known stance spells filtered to only those they have learned:
- Warrior: Battle Stance, Defensive Stance, Berserker Stance
- Death Knight: Blood Presence, Frost Presence, Unholy Presence
- Druid: Bear/Dire Bear, Cat, Travel, Aquatic, Moonkin, Tree, Flight forms
- Rogue: Stealth
- Priest: Shadowform
Active form detected from permanent player auras (maxDurationMs == -1).
Clicking an inactive stance casts the corresponding spell. Active stance
shown with green border/tint; inactive stances are slightly dimmed.
Spell name tooltips shown on hover using existing SpellbookScreen lookup.
2026-03-17 15:12:58 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
} else if (isPlayer) {
|
|
|
|
|
// Player nameplates: use class color for easy identification
|
|
|
|
|
uint8_t cid = entityClassId(unit);
|
|
|
|
|
if (cid != 0) {
|
|
|
|
|
ImVec4 cv = classColorVec4(cid);
|
|
|
|
|
barColor = IM_COL32(
|
|
|
|
|
static_cast<int>(cv.x * 255),
|
|
|
|
|
static_cast<int>(cv.y * 255),
|
|
|
|
|
static_cast<int>(cv.z * 255), A(210));
|
|
|
|
|
bgColor = IM_COL32(
|
|
|
|
|
static_cast<int>(cv.x * 80),
|
|
|
|
|
static_cast<int>(cv.y * 80),
|
|
|
|
|
static_cast<int>(cv.z * 80), A(160));
|
|
|
|
|
} else {
|
|
|
|
|
barColor = IM_COL32(60, 200, 80, A(200));
|
|
|
|
|
bgColor = IM_COL32(25, 100, 35, A(160));
|
feat: add stance/form/presence bar for Warriors, Druids, Death Knights, Rogues, Priests
Renders a stance bar to the left of the main action bar showing the
player's known stance spells filtered to only those they have learned:
- Warrior: Battle Stance, Defensive Stance, Berserker Stance
- Death Knight: Blood Presence, Frost Presence, Unholy Presence
- Druid: Bear/Dire Bear, Cat, Travel, Aquatic, Moonkin, Tree, Flight forms
- Rogue: Stealth
- Priest: Shadowform
Active form detected from permanent player auras (maxDurationMs == -1).
Clicking an inactive stance casts the corresponding spell. Active stance
shown with green border/tint; inactive stances are slightly dimmed.
Spell name tooltips shown on hover using existing SpellbookScreen lookup.
2026-03-17 15:12:58 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
} else {
|
|
|
|
|
barColor = IM_COL32(60, 200, 80, A(200));
|
|
|
|
|
bgColor = IM_COL32(25, 100, 35, A(160));
|
feat: add stance/form/presence bar for Warriors, Druids, Death Knights, Rogues, Priests
Renders a stance bar to the left of the main action bar showing the
player's known stance spells filtered to only those they have learned:
- Warrior: Battle Stance, Defensive Stance, Berserker Stance
- Death Knight: Blood Presence, Frost Presence, Unholy Presence
- Druid: Bear/Dire Bear, Cat, Travel, Aquatic, Moonkin, Tree, Flight forms
- Rogue: Stealth
- Priest: Shadowform
Active form detected from permanent player auras (maxDurationMs == -1).
Clicking an inactive stance casts the corresponding spell. Active stance
shown with green border/tint; inactive stances are slightly dimmed.
Spell name tooltips shown on hover using existing SpellbookScreen lookup.
2026-03-17 15:12:58 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
// Check if this unit is targeting the local player (threat indicator)
|
|
|
|
|
bool isTargetingPlayer = false;
|
|
|
|
|
if (unit->isHostile() && !isCorpse) {
|
|
|
|
|
const auto& fields = entityPtr->getFields();
|
|
|
|
|
auto loIt = fields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_LO));
|
|
|
|
|
if (loIt != fields.end() && loIt->second != 0) {
|
|
|
|
|
uint64_t unitTarget = loIt->second;
|
|
|
|
|
auto hiIt = fields.find(game::fieldIndex(game::UF::UNIT_FIELD_TARGET_HI));
|
|
|
|
|
if (hiIt != fields.end())
|
|
|
|
|
unitTarget |= (static_cast<uint64_t>(hiIt->second) << 32);
|
|
|
|
|
isTargetingPlayer = (unitTarget == playerGuid);
|
2026-02-10 01:24:37 -08:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
// Creature rank for border styling (Elite=gold double border, Boss=red, Rare=silver)
|
|
|
|
|
int creatureRank = -1;
|
|
|
|
|
if (!isPlayer) creatureRank = gameHandler.getCreatureRank(unit->getEntry());
|
2026-02-10 01:24:37 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Border: gold = currently selected, orange = targeting player, dark = default
|
|
|
|
|
ImU32 borderColor = isTarget
|
|
|
|
|
? IM_COL32(255, 215, 0, A(255))
|
|
|
|
|
: isTargetingPlayer
|
|
|
|
|
? IM_COL32(255, 140, 0, A(220)) // orange = this mob is targeting you
|
|
|
|
|
: IM_COL32(20, 20, 20, A(180));
|
2026-02-19 22:34:22 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Bar geometry
|
|
|
|
|
const float barW = 80.0f * settingsPanel_.nameplateScale_;
|
|
|
|
|
const float barH = 8.0f * settingsPanel_.nameplateScale_;
|
|
|
|
|
const float barX = sx - barW * 0.5f;
|
2026-02-19 22:34:22 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Guard against division by zero when maxHealth hasn't been populated yet
|
|
|
|
|
// (freshly spawned entity with default fields). 0/0 produces NaN which
|
|
|
|
|
// poisons all downstream geometry; +inf is clamped but still wasteful.
|
|
|
|
|
float healthPct = (unit->getMaxHealth() > 0)
|
|
|
|
|
? std::clamp(static_cast<float>(unit->getHealth()) / static_cast<float>(unit->getMaxHealth()), 0.0f, 1.0f)
|
|
|
|
|
: 0.0f;
|
2026-02-19 22:34:22 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
drawList->AddRectFilled(ImVec2(barX, sy), ImVec2(barX + barW, sy + barH), bgColor, 2.0f);
|
|
|
|
|
// For corpses, don't fill health bar (just show grey background)
|
|
|
|
|
if (!isCorpse) {
|
|
|
|
|
drawList->AddRectFilled(ImVec2(barX, sy), ImVec2(barX + barW * healthPct, sy + barH), barColor, 2.0f);
|
|
|
|
|
}
|
|
|
|
|
drawList->AddRect (ImVec2(barX - 1.0f, sy - 1.0f), ImVec2(barX + barW + 1.0f, sy + barH + 1.0f), borderColor, 2.0f);
|
2026-02-19 22:34:22 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Elite/Boss/Rare decoration: extra outer border with rank-specific color
|
|
|
|
|
if (creatureRank == 1 || creatureRank == 2) {
|
|
|
|
|
// Elite / Rare Elite: gold double border
|
|
|
|
|
drawList->AddRect(ImVec2(barX - 3.0f, sy - 3.0f),
|
|
|
|
|
ImVec2(barX + barW + 3.0f, sy + barH + 3.0f),
|
|
|
|
|
IM_COL32(255, 200, 50, A(200)), 3.0f);
|
|
|
|
|
} else if (creatureRank == 3) {
|
|
|
|
|
// Boss: red double border
|
|
|
|
|
drawList->AddRect(ImVec2(barX - 3.0f, sy - 3.0f),
|
|
|
|
|
ImVec2(barX + barW + 3.0f, sy + barH + 3.0f),
|
|
|
|
|
IM_COL32(255, 40, 40, A(200)), 3.0f);
|
|
|
|
|
} else if (creatureRank == 4) {
|
|
|
|
|
// Rare: silver double border
|
|
|
|
|
drawList->AddRect(ImVec2(barX - 3.0f, sy - 3.0f),
|
|
|
|
|
ImVec2(barX + barW + 3.0f, sy + barH + 3.0f),
|
|
|
|
|
IM_COL32(170, 200, 230, A(200)), 3.0f);
|
|
|
|
|
}
|
2026-02-19 22:34:22 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// HP % text centered on health bar (non-corpse, non-full-health for readability)
|
|
|
|
|
if (!isCorpse && unit->getMaxHealth() > 0) {
|
|
|
|
|
int hpPct = static_cast<int>(healthPct * 100.0f + 0.5f);
|
|
|
|
|
char hpBuf[8];
|
|
|
|
|
snprintf(hpBuf, sizeof(hpBuf), "%d%%", hpPct);
|
|
|
|
|
ImVec2 hpTextSz = ImGui::CalcTextSize(hpBuf);
|
|
|
|
|
float hpTx = sx - hpTextSz.x * 0.5f;
|
|
|
|
|
float hpTy = sy + (barH - hpTextSz.y) * 0.5f;
|
|
|
|
|
drawList->AddText(ImVec2(hpTx + 1.0f, hpTy + 1.0f), IM_COL32(0, 0, 0, A(140)), hpBuf);
|
|
|
|
|
drawList->AddText(ImVec2(hpTx, hpTy), IM_COL32(255, 255, 255, A(200)), hpBuf);
|
|
|
|
|
}
|
2026-02-10 01:24:37 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Cast bar below health bar when unit is casting
|
|
|
|
|
float castBarBaseY = sy + barH + 2.0f;
|
|
|
|
|
float nameplateBottom = castBarBaseY; // tracks lowest drawn element for debuff dots
|
|
|
|
|
{
|
|
|
|
|
const auto* cs = gameHandler.getUnitCastState(guid);
|
|
|
|
|
if (cs && cs->casting && cs->timeTotal > 0.0f) {
|
|
|
|
|
float castPct = std::clamp((cs->timeTotal - cs->timeRemaining) / cs->timeTotal, 0.0f, 1.0f);
|
|
|
|
|
const float cbH = 6.0f * settingsPanel_.nameplateScale_;
|
2026-02-19 01:50:50 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Spell icon + name above the cast bar
|
|
|
|
|
const std::string& spellName = gameHandler.getSpellName(cs->spellId);
|
|
|
|
|
{
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* castAm = services_.assetManager;
|
2026-03-31 19:49:52 +03:00
|
|
|
VkDescriptorSet castIcon = (cs->spellId && castAm)
|
|
|
|
|
? getSpellIcon(cs->spellId, castAm) : VK_NULL_HANDLE;
|
|
|
|
|
float iconSz = cbH + 8.0f;
|
|
|
|
|
if (castIcon) {
|
|
|
|
|
// Draw icon to the left of the cast bar
|
|
|
|
|
float iconX = barX - iconSz - 2.0f;
|
|
|
|
|
float iconY = castBarBaseY;
|
|
|
|
|
drawList->AddImage((ImTextureID)(uintptr_t)castIcon,
|
|
|
|
|
ImVec2(iconX, iconY),
|
|
|
|
|
ImVec2(iconX + iconSz, iconY + iconSz));
|
|
|
|
|
drawList->AddRect(ImVec2(iconX - 1.0f, iconY - 1.0f),
|
|
|
|
|
ImVec2(iconX + iconSz + 1.0f, iconY + iconSz + 1.0f),
|
|
|
|
|
IM_COL32(0, 0, 0, A(180)), 1.0f);
|
2026-03-12 00:21:25 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
if (!spellName.empty()) {
|
|
|
|
|
ImVec2 snSz = ImGui::CalcTextSize(spellName.c_str());
|
|
|
|
|
float snX = sx - snSz.x * 0.5f;
|
|
|
|
|
float snY = castBarBaseY;
|
|
|
|
|
drawList->AddText(ImVec2(snX + 1.0f, snY + 1.0f), IM_COL32(0, 0, 0, A(140)), spellName.c_str());
|
|
|
|
|
drawList->AddText(ImVec2(snX, snY), IM_COL32(255, 210, 100, A(220)), spellName.c_str());
|
|
|
|
|
castBarBaseY += snSz.y + 2.0f;
|
2026-03-12 00:21:25 -07:00
|
|
|
}
|
2026-02-10 01:24:37 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Cast bar: green = interruptible, red = uninterruptible; both pulse when >80% complete
|
|
|
|
|
ImU32 cbBg = IM_COL32(30, 25, 40, A(180));
|
|
|
|
|
ImU32 cbFill;
|
|
|
|
|
if (castPct > 0.8f && unit->isHostile()) {
|
|
|
|
|
float pulse = 0.7f + 0.3f * std::sin(static_cast<float>(ImGui::GetTime()) * 8.0f);
|
|
|
|
|
cbFill = cs->interruptible
|
|
|
|
|
? IM_COL32(static_cast<int>(40 * pulse), static_cast<int>(220 * pulse), static_cast<int>(40 * pulse), A(220)) // green pulse
|
|
|
|
|
: IM_COL32(static_cast<int>(255 * pulse), static_cast<int>(30 * pulse), static_cast<int>(30 * pulse), A(220)); // red pulse
|
2026-02-19 22:34:22 -08:00
|
|
|
} else {
|
2026-03-31 19:49:52 +03:00
|
|
|
cbFill = cs->interruptible
|
|
|
|
|
? IM_COL32(50, 190, 50, A(200)) // green = interruptible
|
|
|
|
|
: IM_COL32(190, 40, 40, A(200)); // red = uninterruptible
|
2026-02-19 22:34:22 -08:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
drawList->AddRectFilled(ImVec2(barX, castBarBaseY),
|
|
|
|
|
ImVec2(barX + barW, castBarBaseY + cbH), cbBg, 2.0f);
|
|
|
|
|
drawList->AddRectFilled(ImVec2(barX, castBarBaseY),
|
|
|
|
|
ImVec2(barX + barW * castPct, castBarBaseY + cbH), cbFill, 2.0f);
|
|
|
|
|
drawList->AddRect (ImVec2(barX - 1.0f, castBarBaseY - 1.0f),
|
|
|
|
|
ImVec2(barX + barW + 1.0f, castBarBaseY + cbH + 1.0f),
|
|
|
|
|
IM_COL32(20, 10, 40, A(200)), 2.0f);
|
|
|
|
|
|
|
|
|
|
// Time remaining text
|
|
|
|
|
char timeBuf[12];
|
|
|
|
|
snprintf(timeBuf, sizeof(timeBuf), "%.1fs", cs->timeRemaining);
|
|
|
|
|
ImVec2 timeSz = ImGui::CalcTextSize(timeBuf);
|
|
|
|
|
float timeX = sx - timeSz.x * 0.5f;
|
|
|
|
|
float timeY = castBarBaseY + (cbH - timeSz.y) * 0.5f;
|
|
|
|
|
drawList->AddText(ImVec2(timeX + 1.0f, timeY + 1.0f), IM_COL32(0, 0, 0, A(140)), timeBuf);
|
|
|
|
|
drawList->AddText(ImVec2(timeX, timeY), IM_COL32(220, 200, 255, A(220)), timeBuf);
|
|
|
|
|
nameplateBottom = castBarBaseY + cbH + 2.0f;
|
2026-02-19 22:34:22 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Debuff dot indicators: small colored squares below the nameplate showing
|
|
|
|
|
// player-applied auras on the current hostile target.
|
|
|
|
|
// Colors: Magic=blue, Curse=purple, Disease=yellow, Poison=green, Other=grey
|
|
|
|
|
if (isTarget && unit->isHostile() && !isCorpse) {
|
|
|
|
|
const auto& auras = gameHandler.getTargetAuras();
|
|
|
|
|
const uint64_t pguid = gameHandler.getPlayerGuid();
|
|
|
|
|
const float dotSize = 6.0f * settingsPanel_.nameplateScale_;
|
|
|
|
|
const float dotGap = 2.0f;
|
|
|
|
|
float dotX = barX;
|
|
|
|
|
for (const auto& aura : auras) {
|
|
|
|
|
if (aura.isEmpty() || aura.casterGuid != pguid) continue;
|
|
|
|
|
uint8_t dispelType = gameHandler.getSpellDispelType(aura.spellId);
|
|
|
|
|
ImU32 dotCol;
|
|
|
|
|
switch (dispelType) {
|
|
|
|
|
case 1: dotCol = IM_COL32( 64, 128, 255, A(210)); break; // Magic - blue
|
|
|
|
|
case 2: dotCol = IM_COL32(160, 32, 240, A(210)); break; // Curse - purple
|
|
|
|
|
case 3: dotCol = IM_COL32(180, 140, 40, A(210)); break; // Disease - yellow-brown
|
|
|
|
|
case 4: dotCol = IM_COL32( 50, 200, 50, A(210)); break; // Poison - green
|
|
|
|
|
default: dotCol = IM_COL32(170, 170, 170, A(170)); break; // Other - grey
|
|
|
|
|
}
|
|
|
|
|
drawList->AddRectFilled(ImVec2(dotX, nameplateBottom),
|
|
|
|
|
ImVec2(dotX + dotSize, nameplateBottom + dotSize), dotCol, 1.0f);
|
|
|
|
|
drawList->AddRect (ImVec2(dotX - 1.0f, nameplateBottom - 1.0f),
|
|
|
|
|
ImVec2(dotX + dotSize + 1.0f, nameplateBottom + dotSize + 1.0f),
|
|
|
|
|
IM_COL32(0, 0, 0, A(150)), 1.0f);
|
2026-02-05 12:07:58 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Duration clock-sweep overlay (like target frame auras)
|
|
|
|
|
uint64_t nowMs = static_cast<uint64_t>(
|
|
|
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
|
|
|
std::chrono::steady_clock::now().time_since_epoch()).count());
|
|
|
|
|
int32_t remainMs = aura.getRemainingMs(nowMs);
|
|
|
|
|
if (aura.maxDurationMs > 0 && remainMs > 0) {
|
|
|
|
|
float pct = 1.0f - static_cast<float>(remainMs) / static_cast<float>(aura.maxDurationMs);
|
|
|
|
|
pct = std::clamp(pct, 0.0f, 1.0f);
|
|
|
|
|
float cx = dotX + dotSize * 0.5f;
|
|
|
|
|
float cy = nameplateBottom + dotSize * 0.5f;
|
|
|
|
|
float r = dotSize * 0.5f;
|
|
|
|
|
float startAngle = -IM_PI * 0.5f;
|
|
|
|
|
float endAngle = startAngle + pct * IM_PI * 2.0f;
|
|
|
|
|
ImVec2 center(cx, cy);
|
|
|
|
|
const int segments = 12;
|
|
|
|
|
for (int seg = 0; seg < segments; seg++) {
|
|
|
|
|
float a0 = startAngle + (endAngle - startAngle) * seg / segments;
|
|
|
|
|
float a1 = startAngle + (endAngle - startAngle) * (seg + 1) / segments;
|
|
|
|
|
drawList->AddTriangleFilled(
|
|
|
|
|
center,
|
|
|
|
|
ImVec2(cx + r * std::cos(a0), cy + r * std::sin(a0)),
|
|
|
|
|
ImVec2(cx + r * std::cos(a1), cy + r * std::sin(a1)),
|
|
|
|
|
IM_COL32(0, 0, 0, A(100)));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-05 12:07:58 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Stack count on dot (upper-left corner)
|
|
|
|
|
if (aura.charges > 1) {
|
|
|
|
|
char stackBuf[8];
|
|
|
|
|
snprintf(stackBuf, sizeof(stackBuf), "%d", aura.charges);
|
|
|
|
|
drawList->AddText(ImVec2(dotX + 1.0f, nameplateBottom), IM_COL32(0, 0, 0, A(200)), stackBuf);
|
|
|
|
|
drawList->AddText(ImVec2(dotX, nameplateBottom - 1.0f), IM_COL32(255, 255, 255, A(240)), stackBuf);
|
|
|
|
|
}
|
2026-02-05 12:07:58 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Duration text below dot
|
|
|
|
|
if (remainMs > 0) {
|
|
|
|
|
char durBuf[8];
|
|
|
|
|
if (remainMs >= 60000)
|
|
|
|
|
snprintf(durBuf, sizeof(durBuf), "%dm", remainMs / 60000);
|
|
|
|
|
else
|
|
|
|
|
snprintf(durBuf, sizeof(durBuf), "%d", remainMs / 1000);
|
|
|
|
|
ImVec2 durSz = ImGui::CalcTextSize(durBuf);
|
|
|
|
|
float durX = dotX + (dotSize - durSz.x) * 0.5f;
|
|
|
|
|
float durY = nameplateBottom + dotSize + 1.0f;
|
|
|
|
|
drawList->AddText(ImVec2(durX + 1.0f, durY + 1.0f), IM_COL32(0, 0, 0, A(180)), durBuf);
|
|
|
|
|
// Color: red if < 5s, yellow if < 15s, white otherwise
|
|
|
|
|
ImU32 durCol = remainMs < 5000 ? IM_COL32(255, 60, 60, A(240))
|
|
|
|
|
: remainMs < 15000 ? IM_COL32(255, 200, 60, A(240))
|
|
|
|
|
: IM_COL32(230, 230, 230, A(220));
|
|
|
|
|
drawList->AddText(ImVec2(durX, durY), durCol, durBuf);
|
|
|
|
|
}
|
2026-02-05 12:07:58 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Spell name + duration tooltip on hover
|
|
|
|
|
{
|
|
|
|
|
ImVec2 mouse = ImGui::GetMousePos();
|
|
|
|
|
if (mouse.x >= dotX && mouse.x < dotX + dotSize &&
|
|
|
|
|
mouse.y >= nameplateBottom && mouse.y < nameplateBottom + dotSize) {
|
|
|
|
|
const std::string& dotSpellName = gameHandler.getSpellName(aura.spellId);
|
|
|
|
|
if (!dotSpellName.empty()) {
|
|
|
|
|
if (remainMs > 0) {
|
|
|
|
|
int secs = remainMs / 1000;
|
|
|
|
|
int mins = secs / 60;
|
|
|
|
|
secs %= 60;
|
|
|
|
|
char tipBuf[128];
|
|
|
|
|
if (mins > 0)
|
|
|
|
|
snprintf(tipBuf, sizeof(tipBuf), "%s (%dm %ds)", dotSpellName.c_str(), mins, secs);
|
|
|
|
|
else
|
|
|
|
|
snprintf(tipBuf, sizeof(tipBuf), "%s (%ds)", dotSpellName.c_str(), secs);
|
|
|
|
|
ImGui::SetTooltip("%s", tipBuf);
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::SetTooltip("%s", dotSpellName.c_str());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-06 18:34:45 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
dotX += dotSize + dotGap;
|
|
|
|
|
if (dotX + dotSize > barX + barW) break;
|
2026-03-10 07:35:30 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Name + level label above health bar
|
|
|
|
|
uint32_t level = unit->getLevel();
|
|
|
|
|
const std::string& unitName = unit->getName();
|
|
|
|
|
char labelBuf[96];
|
|
|
|
|
if (isPlayer) {
|
|
|
|
|
// Player nameplates: show name only (no level clutter).
|
|
|
|
|
// Fall back to level as placeholder while the name query is pending.
|
|
|
|
|
if (!unitName.empty())
|
|
|
|
|
snprintf(labelBuf, sizeof(labelBuf), "%s", unitName.c_str());
|
|
|
|
|
else {
|
|
|
|
|
// Name query may be pending; request it now to ensure it gets resolved
|
|
|
|
|
gameHandler.queryPlayerName(unit->getGuid());
|
|
|
|
|
if (level > 0)
|
|
|
|
|
snprintf(labelBuf, sizeof(labelBuf), "Player (%u)", level);
|
|
|
|
|
else
|
|
|
|
|
snprintf(labelBuf, sizeof(labelBuf), "Player");
|
|
|
|
|
}
|
|
|
|
|
} else if (level > 0) {
|
|
|
|
|
uint32_t playerLevel = gameHandler.getPlayerLevel();
|
|
|
|
|
// Show skull for units more than 10 levels above the player
|
|
|
|
|
if (playerLevel > 0 && level > playerLevel + 10)
|
|
|
|
|
snprintf(labelBuf, sizeof(labelBuf), "?? %s", unitName.c_str());
|
|
|
|
|
else
|
|
|
|
|
snprintf(labelBuf, sizeof(labelBuf), "%u %s", level, unitName.c_str());
|
|
|
|
|
} else {
|
|
|
|
|
snprintf(labelBuf, sizeof(labelBuf), "%s", unitName.c_str());
|
2026-03-10 07:35:30 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
ImVec2 textSize = ImGui::CalcTextSize(labelBuf);
|
|
|
|
|
float nameX = sx - textSize.x * 0.5f;
|
|
|
|
|
float nameY = sy - barH - 12.0f;
|
|
|
|
|
// Name color: players get WoW class colors; NPCs use hostility (red/yellow)
|
|
|
|
|
ImU32 nameColor;
|
|
|
|
|
if (isPlayer) {
|
|
|
|
|
// Class color with cyan fallback for unknown class
|
|
|
|
|
uint8_t cid = entityClassId(unit);
|
|
|
|
|
ImVec4 cc = (cid != 0) ? classColorVec4(cid) : ImVec4(0.31f, 0.78f, 1.0f, 1.0f);
|
|
|
|
|
nameColor = IM_COL32(static_cast<int>(cc.x*255), static_cast<int>(cc.y*255),
|
|
|
|
|
static_cast<int>(cc.z*255), A(230));
|
2026-03-10 07:35:30 -07:00
|
|
|
} else {
|
2026-03-31 19:49:52 +03:00
|
|
|
nameColor = unit->isHostile()
|
|
|
|
|
? IM_COL32(220, 80, 80, A(230)) // red — hostile NPC
|
|
|
|
|
: IM_COL32(240, 200, 100, A(230)); // yellow — friendly NPC
|
2026-03-10 07:35:30 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
// Sub-label below the name: guild tag for players, subtitle for NPCs
|
|
|
|
|
std::string subLabel;
|
|
|
|
|
if (isPlayer) {
|
|
|
|
|
uint32_t guildId = gameHandler.getEntityGuildId(guid);
|
|
|
|
|
if (guildId != 0) {
|
|
|
|
|
const std::string& gn = gameHandler.lookupGuildName(guildId);
|
|
|
|
|
if (!gn.empty()) subLabel = "<" + gn + ">";
|
2026-03-12 07:12:02 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
} else {
|
|
|
|
|
// NPC subtitle (e.g. "<Reagent Vendor>", "<Innkeeper>")
|
|
|
|
|
std::string sub = gameHandler.getCachedCreatureSubName(unit->getEntry());
|
|
|
|
|
if (!sub.empty()) subLabel = "<" + sub + ">";
|
2026-03-12 07:12:02 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
if (!subLabel.empty()) nameY -= 10.0f; // shift name up for sub-label line
|
2026-02-05 12:07:58 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
drawList->AddText(ImVec2(nameX + 1.0f, nameY + 1.0f), IM_COL32(0, 0, 0, A(160)), labelBuf);
|
|
|
|
|
drawList->AddText(ImVec2(nameX, nameY), nameColor, labelBuf);
|
2026-03-12 05:03:03 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Sub-label below the name (WoW-style <Guild Name> or <NPC Title> in lighter color)
|
|
|
|
|
if (!subLabel.empty()) {
|
|
|
|
|
ImVec2 subSz = ImGui::CalcTextSize(subLabel.c_str());
|
|
|
|
|
float subX = sx - subSz.x * 0.5f;
|
|
|
|
|
float subY = nameY + textSize.y + 1.0f;
|
|
|
|
|
drawList->AddText(ImVec2(subX + 1.0f, subY + 1.0f), IM_COL32(0, 0, 0, A(120)), subLabel.c_str());
|
|
|
|
|
drawList->AddText(ImVec2(subX, subY), IM_COL32(180, 180, 180, A(200)), subLabel.c_str());
|
|
|
|
|
}
|
2026-03-12 05:03:03 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Group leader crown to the right of the name on player nameplates
|
|
|
|
|
if (isPlayer && gameHandler.isInGroup() &&
|
|
|
|
|
gameHandler.getPartyData().leaderGuid == guid) {
|
|
|
|
|
float crownX = nameX + textSize.x + 3.0f;
|
|
|
|
|
const char* crownSym = "\xe2\x99\x9b"; // ♛
|
|
|
|
|
drawList->AddText(ImVec2(crownX + 1.0f, nameY + 1.0f), IM_COL32(0, 0, 0, A(160)), crownSym);
|
|
|
|
|
drawList->AddText(ImVec2(crownX, nameY), IM_COL32(255, 215, 0, A(240)), crownSym);
|
|
|
|
|
}
|
2026-03-12 05:03:03 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Raid mark (if any) to the left of the name
|
|
|
|
|
{
|
|
|
|
|
static constexpr struct { const char* sym; ImU32 col; } kNPMarks[] = {
|
|
|
|
|
{ "\xe2\x98\x85", IM_COL32(255,220, 50,230) }, // Star
|
|
|
|
|
{ "\xe2\x97\x8f", IM_COL32(255,140, 0,230) }, // Circle
|
|
|
|
|
{ "\xe2\x97\x86", IM_COL32(160, 32,240,230) }, // Diamond
|
|
|
|
|
{ "\xe2\x96\xb2", IM_COL32( 50,200, 50,230) }, // Triangle
|
|
|
|
|
{ "\xe2\x97\x8c", IM_COL32( 80,160,255,230) }, // Moon
|
|
|
|
|
{ "\xe2\x96\xa0", IM_COL32( 50,200,220,230) }, // Square
|
|
|
|
|
{ "\xe2\x9c\x9d", IM_COL32(255, 80, 80,230) }, // Cross
|
|
|
|
|
{ "\xe2\x98\xa0", IM_COL32(255,255,255,230) }, // Skull
|
|
|
|
|
};
|
|
|
|
|
uint8_t raidMark = gameHandler.getEntityRaidMark(guid);
|
|
|
|
|
if (raidMark < game::GameHandler::kRaidMarkCount) {
|
|
|
|
|
float markX = nameX - 14.0f;
|
|
|
|
|
drawList->AddText(ImVec2(markX + 1.0f, nameY + 1.0f), IM_COL32(0,0,0,120), kNPMarks[raidMark].sym);
|
|
|
|
|
drawList->AddText(ImVec2(markX, nameY), kNPMarks[raidMark].col, kNPMarks[raidMark].sym);
|
|
|
|
|
}
|
2026-03-12 05:03:03 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Quest kill objective indicator: small yellow sword icon to the right of the name
|
|
|
|
|
float questIconX = nameX + textSize.x + 4.0f;
|
|
|
|
|
if (!isPlayer && questKillEntries.count(unit->getEntry())) {
|
|
|
|
|
const char* objSym = "\xe2\x9a\x94"; // ⚔ crossed swords (UTF-8)
|
|
|
|
|
drawList->AddText(ImVec2(questIconX + 1.0f, nameY + 1.0f), IM_COL32(0, 0, 0, A(160)), objSym);
|
|
|
|
|
drawList->AddText(ImVec2(questIconX, nameY), IM_COL32(255, 220, 0, A(230)), objSym);
|
|
|
|
|
questIconX += ImGui::CalcTextSize("\xe2\x9a\x94").x + 2.0f;
|
|
|
|
|
}
|
2026-03-12 05:03:03 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Quest giver indicator: "!" for available quests, "?" for completable/incomplete
|
|
|
|
|
if (!isPlayer) {
|
|
|
|
|
using QGS = game::QuestGiverStatus;
|
|
|
|
|
QGS qgs = gameHandler.getQuestGiverStatus(guid);
|
|
|
|
|
const char* qSym = nullptr;
|
|
|
|
|
ImU32 qCol = IM_COL32(255, 210, 0, A(255));
|
|
|
|
|
if (qgs == QGS::AVAILABLE) {
|
|
|
|
|
qSym = "!";
|
|
|
|
|
} else if (qgs == QGS::AVAILABLE_LOW) {
|
|
|
|
|
qSym = "!";
|
|
|
|
|
qCol = IM_COL32(160, 160, 160, A(220));
|
|
|
|
|
} else if (qgs == QGS::REWARD || qgs == QGS::REWARD_REP) {
|
|
|
|
|
qSym = "?";
|
|
|
|
|
} else if (qgs == QGS::INCOMPLETE) {
|
|
|
|
|
qSym = "?";
|
|
|
|
|
qCol = IM_COL32(160, 160, 160, A(220));
|
|
|
|
|
}
|
|
|
|
|
if (qSym) {
|
|
|
|
|
drawList->AddText(ImVec2(questIconX + 1.0f, nameY + 1.0f), IM_COL32(0, 0, 0, A(160)), qSym);
|
|
|
|
|
drawList->AddText(ImVec2(questIconX, nameY), qCol, qSym);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 05:03:03 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Click to target / right-click context: detect clicks inside the nameplate region.
|
|
|
|
|
// Use the wider of name text or health bar for the horizontal hit area so short
|
|
|
|
|
// names like "Wolf" don't produce a tiny clickable strip narrower than the bar.
|
|
|
|
|
if (!ImGui::GetIO().WantCaptureMouse) {
|
|
|
|
|
ImVec2 mouse = ImGui::GetIO().MousePos;
|
|
|
|
|
float hitLeft = std::min(nameX, barX) - 2.0f;
|
|
|
|
|
float hitRight = std::max(nameX + textSize.x, barX + barW) + 2.0f;
|
|
|
|
|
float ny0 = nameY - 1.0f;
|
|
|
|
|
float ny1 = sy + barH + 2.0f;
|
|
|
|
|
float nx0 = hitLeft;
|
|
|
|
|
float nx1 = hitRight;
|
|
|
|
|
if (mouse.x >= nx0 && mouse.x <= nx1 && mouse.y >= ny0 && mouse.y <= ny1) {
|
|
|
|
|
// Track mouseover for [target=mouseover] macro conditionals
|
|
|
|
|
gameHandler.setMouseoverGuid(guid);
|
|
|
|
|
// Hover tooltip: name, level/class, guild
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
ImGui::TextUnformatted(unitName.c_str());
|
|
|
|
|
if (isPlayer) {
|
|
|
|
|
uint8_t cid = entityClassId(unit);
|
|
|
|
|
ImGui::Text("Level %u %s", level, classNameStr(cid));
|
|
|
|
|
} else if (level > 0) {
|
|
|
|
|
ImGui::Text("Level %u", level);
|
|
|
|
|
}
|
|
|
|
|
if (!subLabel.empty()) ImGui::TextColored(ImVec4(0.7f,0.7f,0.7f,1.0f), "%s", subLabel.c_str());
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
|
|
|
|
gameHandler.setTarget(guid);
|
|
|
|
|
} else if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
|
|
|
|
|
nameplateCtxGuid_ = guid;
|
|
|
|
|
nameplateCtxPos_ = mouse;
|
|
|
|
|
ImGui::OpenPopup("##NameplateCtx");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 05:03:03 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Render nameplate context popup (uses a tiny overlay window as host)
|
|
|
|
|
if (nameplateCtxGuid_ != 0) {
|
|
|
|
|
ImGui::SetNextWindowPos(nameplateCtxPos_, ImGuiCond_Appearing);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(0, 0), ImGuiCond_Always);
|
|
|
|
|
ImGuiWindowFlags ctxHostFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
|
|
|
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
|
|
|
|
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoFocusOnAppearing |
|
|
|
|
|
ImGuiWindowFlags_AlwaysAutoResize;
|
|
|
|
|
if (ImGui::Begin("##NameplateCtxHost", nullptr, ctxHostFlags)) {
|
|
|
|
|
if (ImGui::BeginPopup("##NameplateCtx")) {
|
|
|
|
|
auto entityPtr = gameHandler.getEntityManager().getEntity(nameplateCtxGuid_);
|
|
|
|
|
std::string ctxName = entityPtr ? getEntityName(entityPtr) : "";
|
|
|
|
|
if (!ctxName.empty()) {
|
|
|
|
|
ImGui::TextDisabled("%s", ctxName.c_str());
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::MenuItem("Target"))
|
|
|
|
|
gameHandler.setTarget(nameplateCtxGuid_);
|
|
|
|
|
if (ImGui::MenuItem("Set Focus"))
|
|
|
|
|
gameHandler.setFocus(nameplateCtxGuid_);
|
|
|
|
|
bool isPlayer = entityPtr && entityPtr->getType() == game::ObjectType::PLAYER;
|
|
|
|
|
if (isPlayer && !ctxName.empty()) {
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
if (ImGui::MenuItem("Whisper")) {
|
|
|
|
|
chatPanel_.setWhisperTarget(ctxName);
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::MenuItem("Invite to Group"))
|
|
|
|
|
gameHandler.inviteToGroup(ctxName);
|
|
|
|
|
if (ImGui::MenuItem("Trade"))
|
|
|
|
|
gameHandler.initiateTrade(nameplateCtxGuid_);
|
|
|
|
|
if (ImGui::MenuItem("Duel"))
|
|
|
|
|
gameHandler.proposeDuel(nameplateCtxGuid_);
|
|
|
|
|
if (ImGui::MenuItem("Inspect")) {
|
|
|
|
|
gameHandler.setTarget(nameplateCtxGuid_);
|
|
|
|
|
gameHandler.inspectTarget();
|
|
|
|
|
socialPanel_.showInspectWindow_ = true;
|
|
|
|
|
}
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
if (ImGui::MenuItem("Add Friend"))
|
|
|
|
|
gameHandler.addFriend(ctxName);
|
|
|
|
|
if (ImGui::MenuItem("Ignore"))
|
|
|
|
|
gameHandler.addIgnore(ctxName);
|
|
|
|
|
}
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
} else {
|
|
|
|
|
nameplateCtxGuid_ = 0;
|
|
|
|
|
}
|
2026-03-12 05:03:03 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::End();
|
2026-03-12 05:03:03 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
// ============================================================
|
2026-03-31 19:49:52 +03:00
|
|
|
// Party Frames (Phase 4)
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
// ============================================================
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Durability Warning (equipment damage indicator)
|
|
|
|
|
// ============================================================
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
void GameScreen::takeScreenshot(game::GameHandler& /*gameHandler*/) {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-03-31 19:49:52 +03:00
|
|
|
if (!renderer) return;
|
2026-03-12 07:52:47 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Build path: ~/.wowee/screenshots/WoWee_YYYYMMDD_HHMMSS.png
|
|
|
|
|
const char* home = std::getenv("HOME");
|
|
|
|
|
if (!home) home = std::getenv("USERPROFILE");
|
|
|
|
|
if (!home) home = "/tmp";
|
|
|
|
|
std::string dir = std::string(home) + "/.wowee/screenshots";
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
auto now = std::chrono::system_clock::now();
|
|
|
|
|
auto tt = std::chrono::system_clock::to_time_t(now);
|
|
|
|
|
std::tm tm{};
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
localtime_s(&tm, &tt);
|
|
|
|
|
#else
|
|
|
|
|
localtime_r(&tt, &tm);
|
|
|
|
|
#endif
|
2026-03-12 07:52:47 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
char filename[128];
|
|
|
|
|
std::snprintf(filename, sizeof(filename),
|
|
|
|
|
"WoWee_%04d%02d%02d_%02d%02d%02d.png",
|
|
|
|
|
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
|
|
|
|
|
tm.tm_hour, tm.tm_min, tm.tm_sec);
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
std::string path = dir + "/" + filename;
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
if (renderer->captureScreenshot(path)) {
|
|
|
|
|
game::MessageChatData sysMsg;
|
|
|
|
|
sysMsg.type = game::ChatType::SYSTEM;
|
|
|
|
|
sysMsg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
sysMsg.message = "Screenshot saved: " + path;
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
services_.gameHandler->addLocalChatMessage(sysMsg);
|
2026-03-31 19:49:52 +03:00
|
|
|
}
|
|
|
|
|
}
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
void GameScreen::renderDurabilityWarning(game::GameHandler& gameHandler) {
|
|
|
|
|
if (gameHandler.getPlayerGuid() == 0) return;
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
const auto& inv = gameHandler.getInventory();
|
|
|
|
|
|
|
|
|
|
// Scan all equipment slots (skip bag slots which have no durability)
|
|
|
|
|
float minDurPct = 1.0f;
|
|
|
|
|
bool hasBroken = false;
|
|
|
|
|
|
|
|
|
|
for (int i = static_cast<int>(game::EquipSlot::HEAD);
|
|
|
|
|
i < static_cast<int>(game::EquipSlot::BAG1); ++i) {
|
|
|
|
|
const auto& slot = inv.getEquipSlot(static_cast<game::EquipSlot>(i));
|
|
|
|
|
if (slot.empty() || slot.item.maxDurability == 0) continue;
|
|
|
|
|
if (slot.item.curDurability == 0) {
|
|
|
|
|
hasBroken = true;
|
2026-02-19 03:12:57 -08:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
float pct = static_cast<float>(slot.item.curDurability) /
|
|
|
|
|
static_cast<float>(slot.item.maxDurability);
|
|
|
|
|
if (pct < minDurPct) minDurPct = pct;
|
|
|
|
|
}
|
2026-03-12 07:52:47 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Only show warning below 20%
|
|
|
|
|
if (minDurPct >= 0.2f && !hasBroken) return;
|
2026-03-18 04:34:36 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
|
const float screenW = io.DisplaySize.x;
|
|
|
|
|
const float screenH = io.DisplaySize.y;
|
2026-03-18 04:34:36 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Position: just above the XP bar / action bar area (bottom-center)
|
|
|
|
|
const float warningW = 220.0f;
|
|
|
|
|
const float warningH = 26.0f;
|
|
|
|
|
const float posX = (screenW - warningW) * 0.5f;
|
|
|
|
|
const float posY = screenH - 140.0f; // above action bar
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(posX, posY), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(warningW, warningH), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowBgAlpha(0.75f);
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6, 4));
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(0, 0));
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoInputs |
|
|
|
|
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
|
|
|
|
|
ImGuiWindowFlags_NoBringToFrontOnFocus;
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##durability_warn", nullptr, flags)) {
|
|
|
|
|
if (hasBroken) {
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.15f, 0.15f, 1.0f),
|
|
|
|
|
"\xef\x94\x9b Gear broken! Visit a repair NPC");
|
2026-03-12 07:52:47 -07:00
|
|
|
} else {
|
2026-03-31 19:49:52 +03:00
|
|
|
int pctInt = static_cast<int>(minDurPct * 100.0f);
|
|
|
|
|
ImGui::TextColored(colors::kSymbolGold,
|
|
|
|
|
"\xef\x94\x9b Low durability: %d%%", pctInt);
|
2026-03-12 07:52:47 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
if (ImGui::IsWindowHovered())
|
|
|
|
|
ImGui::SetTooltip("Your equipment is damaged. Visit any blacksmith or repair NPC.");
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
}
|
|
|
|
|
ImGui::End();
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::PopStyleVar(3);
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
}
|
|
|
|
|
|
2026-03-09 14:30:48 -07:00
|
|
|
// ============================================================
|
2026-03-31 19:49:52 +03:00
|
|
|
// UI Error Frame (WoW-style center-bottom error overlay)
|
2026-03-09 14:30:48 -07:00
|
|
|
// ============================================================
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
void GameScreen::renderUIErrors(game::GameHandler& /*gameHandler*/, float deltaTime) {
|
|
|
|
|
// Age out old entries
|
|
|
|
|
for (auto& e : uiErrors_) e.age += deltaTime;
|
|
|
|
|
uiErrors_.erase(
|
|
|
|
|
std::remove_if(uiErrors_.begin(), uiErrors_.end(),
|
|
|
|
|
[](const UIErrorEntry& e) { return e.age >= kUIErrorLifetime; }),
|
|
|
|
|
uiErrors_.end());
|
2026-03-09 14:30:48 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
if (uiErrors_.empty()) return;
|
2026-03-09 14:30:48 -07:00
|
|
|
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* window = services_.window;
|
2026-03-31 19:49:52 +03:00
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
2026-03-09 14:30:48 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Fixed invisible overlay
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(0, 0));
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(screenW, screenH));
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration |
|
|
|
|
|
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav |
|
|
|
|
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar;
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0));
|
|
|
|
|
if (ImGui::Begin("##UIErrors", nullptr, flags)) {
|
|
|
|
|
// Render messages stacked above the action bar (~200px from bottom)
|
|
|
|
|
// The newest message is on top; older ones fade below it.
|
|
|
|
|
const float baseY = screenH - 200.0f;
|
|
|
|
|
const float lineH = 20.0f;
|
|
|
|
|
const int count = static_cast<int>(uiErrors_.size());
|
2026-03-09 14:30:48 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImDrawList* draw = ImGui::GetWindowDrawList();
|
|
|
|
|
for (int i = count - 1; i >= 0; --i) {
|
|
|
|
|
const auto& e = uiErrors_[i];
|
|
|
|
|
float alpha = 1.0f - (e.age / kUIErrorLifetime);
|
|
|
|
|
alpha = std::max(0.0f, std::min(1.0f, alpha));
|
2026-03-09 14:30:48 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Fade fast in the last 0.5 s
|
|
|
|
|
if (e.age > kUIErrorLifetime - 0.5f)
|
|
|
|
|
alpha *= (kUIErrorLifetime - e.age) / 0.5f;
|
2026-03-09 14:30:48 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
uint8_t a8 = static_cast<uint8_t>(alpha * 255.0f);
|
|
|
|
|
ImU32 textCol = IM_COL32(255, 50, 50, a8);
|
|
|
|
|
ImU32 shadowCol= IM_COL32( 0, 0, 0, static_cast<uint8_t>(alpha * 180));
|
|
|
|
|
|
|
|
|
|
const char* txt = e.text.c_str();
|
|
|
|
|
ImVec2 sz = ImGui::CalcTextSize(txt);
|
|
|
|
|
float x = std::round((screenW - sz.x) * 0.5f);
|
|
|
|
|
float y = std::round(baseY - (count - 1 - i) * lineH);
|
|
|
|
|
|
|
|
|
|
// Drop shadow
|
|
|
|
|
draw->AddText(ImVec2(x + 1, y + 1), shadowCol, txt);
|
|
|
|
|
draw->AddText(ImVec2(x, y), textCol, txt);
|
2026-03-09 14:30:48 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::End();
|
|
|
|
|
ImGui::PopStyleVar();
|
2026-03-09 14:30:48 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
|
2026-03-12 15:25:07 -07:00
|
|
|
// ============================================================
|
2026-03-31 19:49:52 +03:00
|
|
|
// Boss Encounter Frames
|
2026-03-12 15:25:07 -07:00
|
|
|
// ============================================================
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Social Frame — compact online friends panel (toggled by socialPanel_.showSocialFrame_)
|
|
|
|
|
// ============================================================
|
2026-03-12 15:25:07 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Buff/Debuff Bar (Phase 3)
|
|
|
|
|
// ============================================================
|
2026-03-12 15:25:07 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Loot Window (Phase 5)
|
|
|
|
|
// ============================================================
|
2026-03-12 15:25:07 -07:00
|
|
|
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Gossip Window (Phase 5)
|
|
|
|
|
// ============================================================
|
2026-03-12 15:25:07 -07:00
|
|
|
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Quest Details Window
|
|
|
|
|
// ============================================================
|
2026-03-12 15:25:07 -07:00
|
|
|
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Quest Request Items Window (turn-in progress check)
|
|
|
|
|
// ============================================================
|
2026-03-12 15:25:07 -07:00
|
|
|
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Quest Offer Reward Window (choose reward)
|
|
|
|
|
// ============================================================
|
2026-03-12 15:25:07 -07:00
|
|
|
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// ItemExtendedCost.dbc loader
|
|
|
|
|
// ============================================================
|
2026-03-12 15:25:07 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Vendor Window (Phase 5)
|
|
|
|
|
// ============================================================
|
2026-03-12 15:25:07 -07:00
|
|
|
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Trainer
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
2026-03-12 15:25:07 -07:00
|
|
|
|
2026-03-09 15:05:38 -07:00
|
|
|
// ============================================================
|
2026-03-31 19:49:52 +03:00
|
|
|
// Teleporter Panel
|
2026-03-09 15:05:38 -07:00
|
|
|
// ============================================================
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Escape Menu
|
|
|
|
|
// ============================================================
|
2026-03-09 15:05:38 -07:00
|
|
|
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Barber Shop Window
|
|
|
|
|
// ============================================================
|
2026-03-09 15:05:38 -07:00
|
|
|
|
2026-03-10 05:18:45 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Pet Stable Window
|
|
|
|
|
// ============================================================
|
2026-03-09 15:05:38 -07:00
|
|
|
|
2026-03-12 16:52:12 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Taxi Window
|
|
|
|
|
// ============================================================
|
2026-03-09 15:05:38 -07:00
|
|
|
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Logout Countdown
|
|
|
|
|
// ============================================================
|
2026-03-09 15:05:38 -07:00
|
|
|
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Death Screen
|
|
|
|
|
// ============================================================
|
2026-03-11 23:42:28 -07:00
|
|
|
|
2026-03-09 15:05:38 -07:00
|
|
|
|
2026-03-11 21:24:03 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
void GameScreen::renderQuestMarkers(game::GameHandler& gameHandler) {
|
|
|
|
|
const auto& statuses = gameHandler.getNpcQuestStatuses();
|
|
|
|
|
if (statuses.empty()) return;
|
2026-03-12 16:52:12 -07:00
|
|
|
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-03-31 19:49:52 +03:00
|
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* window = services_.window;
|
2026-03-31 19:49:52 +03:00
|
|
|
if (!camera || !window) return;
|
2026-03-13 04:04:29 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
float screenW = static_cast<float>(window->getWidth());
|
|
|
|
|
float screenH = static_cast<float>(window->getHeight());
|
|
|
|
|
glm::mat4 viewProj = camera->getViewProjectionMatrix();
|
|
|
|
|
auto* drawList = ImGui::GetForegroundDrawList();
|
2026-03-13 04:04:29 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
for (const auto& [guid, status] : statuses) {
|
|
|
|
|
// Only show markers for available (!) and reward/completable (?)
|
|
|
|
|
const char* marker = nullptr;
|
|
|
|
|
ImU32 color = IM_COL32(255, 210, 0, 255); // yellow
|
|
|
|
|
if (status == game::QuestGiverStatus::AVAILABLE) {
|
|
|
|
|
marker = "!";
|
|
|
|
|
} else if (status == game::QuestGiverStatus::AVAILABLE_LOW) {
|
|
|
|
|
marker = "!";
|
|
|
|
|
color = IM_COL32(160, 160, 160, 255); // gray
|
|
|
|
|
} else if (status == game::QuestGiverStatus::REWARD ||
|
|
|
|
|
status == game::QuestGiverStatus::REWARD_REP) {
|
|
|
|
|
marker = "?";
|
|
|
|
|
} else if (status == game::QuestGiverStatus::INCOMPLETE) {
|
|
|
|
|
marker = "?";
|
|
|
|
|
color = IM_COL32(160, 160, 160, 255); // gray
|
|
|
|
|
} else {
|
|
|
|
|
continue;
|
2026-03-13 04:04:29 -07:00
|
|
|
}
|
2026-03-09 15:05:38 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Get entity position (canonical coords)
|
|
|
|
|
auto entity = gameHandler.getEntityManager().getEntity(guid);
|
|
|
|
|
if (!entity) continue;
|
2026-03-09 15:05:38 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
|
|
|
|
|
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
2026-03-12 03:52:54 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Get model height for offset
|
|
|
|
|
float heightOffset = 3.0f;
|
|
|
|
|
glm::vec3 boundsCenter;
|
|
|
|
|
float boundsRadius = 0.0f;
|
|
|
|
|
if (core::Application::getInstance().getRenderBoundsForGuid(guid, boundsCenter, boundsRadius)) {
|
|
|
|
|
heightOffset = boundsRadius * 2.0f + 1.0f;
|
2026-03-12 03:52:54 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
renderPos.z += heightOffset;
|
2026-03-12 03:52:54 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Project to screen
|
|
|
|
|
glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f);
|
|
|
|
|
if (clipPos.w <= 0.0f) continue;
|
2026-03-12 03:52:54 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
glm::vec2 ndc(clipPos.x / clipPos.w, clipPos.y / clipPos.w);
|
|
|
|
|
float sx = (ndc.x + 1.0f) * 0.5f * screenW;
|
|
|
|
|
float sy = (1.0f - ndc.y) * 0.5f * screenH;
|
2026-03-12 03:52:54 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Skip if off-screen
|
|
|
|
|
if (sx < -50 || sx > screenW + 50 || sy < -50 || sy > screenH + 50) continue;
|
2026-03-12 03:52:54 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Scale text size based on distance
|
|
|
|
|
float dist = clipPos.w;
|
|
|
|
|
float fontSize = std::clamp(800.0f / dist, 14.0f, 48.0f);
|
2026-03-12 03:52:54 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Draw outlined text: 4 shadow copies then main text
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
ImU32 outlineColor = IM_COL32(0, 0, 0, 220);
|
|
|
|
|
float off = std::max(1.0f, fontSize * 0.06f);
|
|
|
|
|
ImVec2 textSize = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, marker);
|
|
|
|
|
float tx = sx - textSize.x * 0.5f;
|
|
|
|
|
float ty = sy - textSize.y * 0.5f;
|
2026-03-12 03:52:54 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
drawList->AddText(font, fontSize, ImVec2(tx - off, ty), outlineColor, marker);
|
|
|
|
|
drawList->AddText(font, fontSize, ImVec2(tx + off, ty), outlineColor, marker);
|
|
|
|
|
drawList->AddText(font, fontSize, ImVec2(tx, ty - off), outlineColor, marker);
|
|
|
|
|
drawList->AddText(font, fontSize, ImVec2(tx, ty + off), outlineColor, marker);
|
|
|
|
|
drawList->AddText(font, fontSize, ImVec2(tx, ty), color, marker);
|
2026-03-12 03:52:54 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) {
|
|
|
|
|
const auto& statuses = gameHandler.getNpcQuestStatuses();
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* renderer = services_.renderer;
|
2026-03-31 19:49:52 +03:00
|
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
|
|
|
|
auto* minimap = renderer ? renderer->getMinimap() : nullptr;
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* window = services_.window;
|
2026-03-31 19:49:52 +03:00
|
|
|
if (!camera || !minimap || !window) return;
|
2026-03-12 08:00:27 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
float screenW = static_cast<float>(window->getWidth());
|
2026-03-12 08:00:27 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Minimap parameters (matching minimap.cpp)
|
|
|
|
|
float mapSize = 200.0f;
|
|
|
|
|
float margin = 10.0f;
|
|
|
|
|
float mapRadius = mapSize * 0.5f;
|
|
|
|
|
float centerX = screenW - margin - mapRadius;
|
|
|
|
|
float centerY = margin + mapRadius;
|
|
|
|
|
float viewRadius = minimap->getViewRadius();
|
2026-03-12 08:00:27 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Use the exact same minimap center as Renderer::renderWorld() to keep markers anchored.
|
|
|
|
|
glm::vec3 playerRender = camera->getPosition();
|
|
|
|
|
if (renderer->getCharacterInstanceId() != 0) {
|
|
|
|
|
playerRender = renderer->getCharacterPosition();
|
Add gameplay systems: combat, spells, groups, loot, vendors, and UI
Implement ~70 new protocol opcodes across 5 phases while maintaining
full 3.3.5a private server compatibility:
- Phase 1: Server-aware targeting (CMSG_SET_SELECTION), player/creature
name queries, CMSG_SET_ACTIVE_MOVER after login
- Phase 2: Auto-attack, melee/spell damage parsing, health/mana/power
tracking from UPDATE_OBJECT fields, floating combat text
- Phase 3: Spell casting, action bar (12 slots, keys 1-=), cast bar,
cooldown tracking, aura/buff system with cancellation
- Phase 4: Group invite/accept/decline/leave, party frames UI,
/invite chat command
- Phase 5: Loot window, NPC gossip dialog, vendor buy/sell interface
Also: disable debug HUD/panels by default, gate 3D rendering to
IN_GAME state only, fix window resize not updating UI positions.
2026-02-04 10:30:52 -08:00
|
|
|
}
|
2026-03-18 09:54:52 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Camera bearing for minimap rotation
|
|
|
|
|
float bearing = 0.0f;
|
|
|
|
|
float cosB = 1.0f;
|
|
|
|
|
float sinB = 0.0f;
|
|
|
|
|
if (minimap->isRotateWithCamera()) {
|
|
|
|
|
glm::vec3 fwd = camera->getForward();
|
|
|
|
|
// Render space: +X=West, +Y=North. Camera fwd=(cos(yaw),sin(yaw)).
|
|
|
|
|
// Clockwise bearing from North: atan2(fwd.y, -fwd.x).
|
|
|
|
|
bearing = std::atan2(fwd.y, -fwd.x);
|
|
|
|
|
cosB = std::cos(bearing);
|
|
|
|
|
sinB = std::sin(bearing);
|
2026-03-18 09:54:52 -07:00
|
|
|
}
|
2026-03-12 04:04:27 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
auto* drawList = ImGui::GetForegroundDrawList();
|
2026-03-12 04:04:27 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
auto projectToMinimap = [&](const glm::vec3& worldRenderPos, float& sx, float& sy) -> bool {
|
|
|
|
|
float dx = worldRenderPos.x - playerRender.x;
|
|
|
|
|
float dy = worldRenderPos.y - playerRender.y;
|
2026-03-12 04:04:27 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Exact inverse of minimap display shader:
|
|
|
|
|
// shader: mapUV = playerUV + vec2(-rotated.x, rotated.y) * zoom * 2
|
|
|
|
|
// where rotated = R(bearing) * center, center in [-0.5, 0.5]
|
|
|
|
|
// Inverse: center = R^-1(bearing) * (-deltaUV.x, deltaUV.y) / (zoom*2)
|
|
|
|
|
// With deltaUV.x ∝ +dx (render +X=west=larger U) and deltaUV.y ∝ -dy (V increases south):
|
|
|
|
|
float rx = -(dx * cosB + dy * sinB);
|
|
|
|
|
float ry = dx * sinB - dy * cosB;
|
2026-03-12 04:04:27 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Scale to minimap pixels
|
|
|
|
|
float px = rx / viewRadius * mapRadius;
|
|
|
|
|
float py = ry / viewRadius * mapRadius;
|
2026-03-12 04:04:27 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
float distFromCenter = std::sqrt(px * px + py * py);
|
|
|
|
|
if (distFromCenter > mapRadius - 3.0f) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-03-12 04:04:27 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
sx = centerX + px;
|
|
|
|
|
sy = centerY + py;
|
|
|
|
|
return true;
|
2026-03-12 04:04:27 -07:00
|
|
|
};
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Build sets of entries that are incomplete objectives for tracked quests.
|
|
|
|
|
// minimapQuestEntries: NPC creature entries (npcOrGoId > 0)
|
|
|
|
|
// minimapQuestGoEntries: game object entries (npcOrGoId < 0, stored as abs value)
|
|
|
|
|
std::unordered_set<uint32_t> minimapQuestEntries;
|
|
|
|
|
std::unordered_set<uint32_t> minimapQuestGoEntries;
|
|
|
|
|
{
|
|
|
|
|
const auto& ql = gameHandler.getQuestLog();
|
|
|
|
|
const auto& tq = gameHandler.getTrackedQuestIds();
|
|
|
|
|
for (const auto& q : ql) {
|
|
|
|
|
if (q.complete || q.questId == 0) continue;
|
|
|
|
|
if (!tq.empty() && !tq.count(q.questId)) continue;
|
|
|
|
|
for (const auto& obj : q.killObjectives) {
|
|
|
|
|
if (obj.required == 0) continue;
|
|
|
|
|
if (obj.npcOrGoId > 0) {
|
|
|
|
|
auto it = q.killCounts.find(static_cast<uint32_t>(obj.npcOrGoId));
|
|
|
|
|
if (it == q.killCounts.end() || it->second.first < it->second.second)
|
|
|
|
|
minimapQuestEntries.insert(static_cast<uint32_t>(obj.npcOrGoId));
|
|
|
|
|
} else if (obj.npcOrGoId < 0) {
|
|
|
|
|
uint32_t goEntry = static_cast<uint32_t>(-obj.npcOrGoId);
|
|
|
|
|
auto it = q.killCounts.find(goEntry);
|
|
|
|
|
if (it == q.killCounts.end() || it->second.first < it->second.second)
|
|
|
|
|
minimapQuestGoEntries.insert(goEntry);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 11:40:31 -07:00
|
|
|
}
|
2026-03-12 04:04:27 -07:00
|
|
|
}
|
2026-03-18 03:09:43 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Optional base nearby NPC dots (independent of quest status packets).
|
|
|
|
|
if (settingsPanel_.minimapNpcDots_) {
|
|
|
|
|
ImVec2 mouse = ImGui::GetMousePos();
|
|
|
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
if (!entity || entity->getType() != game::ObjectType::UNIT) continue;
|
2026-03-09 17:01:38 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
|
|
|
if (!unit || unit->getHealth() == 0) continue;
|
2026-03-09 17:01:38 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
glm::vec3 npcRender = core::coords::canonicalToRender(glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
|
|
|
|
float sx = 0.0f, sy = 0.0f;
|
|
|
|
|
if (!projectToMinimap(npcRender, sx, sy)) continue;
|
2026-03-09 17:01:38 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
bool isQuestTarget = minimapQuestEntries.count(unit->getEntry()) != 0;
|
|
|
|
|
if (isQuestTarget) {
|
|
|
|
|
// Quest kill objective: larger gold dot with dark outline
|
|
|
|
|
drawList->AddCircleFilled(ImVec2(sx, sy), 3.5f, IM_COL32(255, 210, 30, 240));
|
|
|
|
|
drawList->AddCircle(ImVec2(sx, sy), 3.5f, IM_COL32(80, 50, 0, 180), 0, 1.0f);
|
|
|
|
|
// Tooltip on hover showing unit name
|
|
|
|
|
float mdx = mouse.x - sx, mdy = mouse.y - sy;
|
|
|
|
|
if (mdx * mdx + mdy * mdy < 64.0f) {
|
|
|
|
|
const std::string& nm = unit->getName();
|
|
|
|
|
if (!nm.empty()) ImGui::SetTooltip("%s (quest)", nm.c_str());
|
2026-03-11 00:29:35 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
} else {
|
|
|
|
|
ImU32 baseDot = unit->isHostile() ? IM_COL32(220, 70, 70, 220) : IM_COL32(245, 245, 245, 210);
|
|
|
|
|
drawList->AddCircleFilled(ImVec2(sx, sy), 1.0f, baseDot);
|
2026-03-11 00:29:35 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Nearby other-player dots — shown when NPC dots are enabled.
|
|
|
|
|
// Party members are already drawn as squares above; other players get a small circle.
|
|
|
|
|
if (settingsPanel_.minimapNpcDots_) {
|
|
|
|
|
const uint64_t selfGuid = gameHandler.getPlayerGuid();
|
|
|
|
|
const auto& partyData = gameHandler.getPartyData();
|
|
|
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
if (!entity || entity->getType() != game::ObjectType::PLAYER) continue;
|
|
|
|
|
if (entity->getGuid() == selfGuid) continue; // skip self (already drawn as arrow)
|
2026-03-09 17:01:38 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Skip party members (already drawn as squares above)
|
|
|
|
|
bool isPartyMember = false;
|
|
|
|
|
for (const auto& m : partyData.members) {
|
|
|
|
|
if (m.guid == guid) { isPartyMember = true; break; }
|
|
|
|
|
}
|
|
|
|
|
if (isPartyMember) continue;
|
2026-03-09 17:01:38 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
glm::vec3 pRender = core::coords::canonicalToRender(
|
|
|
|
|
glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
|
|
|
|
float sx = 0.0f, sy = 0.0f;
|
|
|
|
|
if (!projectToMinimap(pRender, sx, sy)) continue;
|
2026-03-09 17:01:38 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Blue dot for other nearby players
|
|
|
|
|
drawList->AddCircleFilled(ImVec2(sx, sy), 2.0f, IM_COL32(80, 160, 255, 220));
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-09 18:45:28 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Lootable corpse dots: small yellow-green diamonds on dead, lootable units.
|
|
|
|
|
// Shown whenever NPC dots are enabled (or always, since they're always useful).
|
|
|
|
|
{
|
|
|
|
|
constexpr uint32_t UNIT_DYNFLAG_LOOTABLE = 0x0001;
|
|
|
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
if (!entity || entity->getType() != game::ObjectType::UNIT) continue;
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
|
|
|
if (!unit) continue;
|
|
|
|
|
// Must be dead (health == 0) and marked lootable
|
|
|
|
|
if (unit->getHealth() != 0) continue;
|
|
|
|
|
if (!(unit->getDynamicFlags() & UNIT_DYNFLAG_LOOTABLE)) continue;
|
2026-03-10 07:25:04 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
glm::vec3 npcRender = core::coords::canonicalToRender(
|
|
|
|
|
glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
|
|
|
|
float sx = 0.0f, sy = 0.0f;
|
|
|
|
|
if (!projectToMinimap(npcRender, sx, sy)) continue;
|
2026-03-11 16:54:30 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Draw a small diamond (rotated square) in light yellow-green
|
|
|
|
|
const float dr = 3.5f;
|
|
|
|
|
ImVec2 top (sx, sy - dr);
|
|
|
|
|
ImVec2 right(sx + dr, sy );
|
|
|
|
|
ImVec2 bot (sx, sy + dr);
|
|
|
|
|
ImVec2 left (sx - dr, sy );
|
|
|
|
|
drawList->AddQuadFilled(top, right, bot, left, IM_COL32(180, 230, 80, 230));
|
|
|
|
|
drawList->AddQuad (top, right, bot, left, IM_COL32(60, 80, 20, 200), 1.0f);
|
|
|
|
|
|
|
|
|
|
// Tooltip on hover
|
|
|
|
|
if (ImGui::IsMouseHoveringRect(ImVec2(sx - dr, sy - dr), ImVec2(sx + dr, sy + dr))) {
|
|
|
|
|
const std::string& nm = unit->getName();
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.9f, 0.3f, 1.0f), "%s",
|
|
|
|
|
nm.empty() ? "Lootable corpse" : nm.c_str());
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
}
|
2026-03-10 19:49:33 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
}
|
2026-03-09 17:01:38 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Interactable game object dots (chests, resource nodes) when NPC dots are enabled.
|
|
|
|
|
// Shown as small orange triangles to distinguish from unit dots and loot corpses.
|
|
|
|
|
if (settingsPanel_.minimapNpcDots_) {
|
|
|
|
|
ImVec2 mouse = ImGui::GetMousePos();
|
|
|
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
if (!entity || entity->getType() != game::ObjectType::GAMEOBJECT) continue;
|
2026-03-09 17:01:38 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Only show objects that are likely interactive (chests/nodes: type 3;
|
|
|
|
|
// also show type 0=Door when open, but filter by dynamic-flag ACTIVATED).
|
|
|
|
|
// For simplicity, show all game objects that have a non-empty cached name.
|
|
|
|
|
auto go = std::static_pointer_cast<game::GameObject>(entity);
|
|
|
|
|
if (!go) continue;
|
2026-03-09 17:01:38 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Only show if we have name data (avoids cluttering with unknown objects)
|
|
|
|
|
const auto* goInfo = gameHandler.getCachedGameObjectInfo(go->getEntry());
|
|
|
|
|
if (!goInfo || !goInfo->isValid()) continue;
|
|
|
|
|
// Skip transport objects (boats/zeppelins): type 15 = MO_TRANSPORT, 11 = TRANSPORT
|
|
|
|
|
if (goInfo->type == 11 || goInfo->type == 15) continue;
|
2026-03-09 17:01:38 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
glm::vec3 goRender = core::coords::canonicalToRender(
|
|
|
|
|
glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
|
|
|
|
float sx = 0.0f, sy = 0.0f;
|
|
|
|
|
if (!projectToMinimap(goRender, sx, sy)) continue;
|
2026-03-09 17:01:38 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Triangle size and color: bright cyan for quest objectives, amber for others
|
|
|
|
|
bool isQuestGO = minimapQuestGoEntries.count(go->getEntry()) != 0;
|
|
|
|
|
const float ts = isQuestGO ? 4.5f : 3.5f;
|
|
|
|
|
ImVec2 goTip (sx, sy - ts);
|
|
|
|
|
ImVec2 goLeft (sx - ts, sy + ts * 0.6f);
|
|
|
|
|
ImVec2 goRight(sx + ts, sy + ts * 0.6f);
|
|
|
|
|
if (isQuestGO) {
|
|
|
|
|
drawList->AddTriangleFilled(goTip, goLeft, goRight, IM_COL32(50, 230, 255, 240));
|
|
|
|
|
drawList->AddTriangle(goTip, goLeft, goRight, IM_COL32(0, 60, 80, 200), 1.5f);
|
|
|
|
|
} else {
|
|
|
|
|
drawList->AddTriangleFilled(goTip, goLeft, goRight, IM_COL32(255, 185, 30, 220));
|
|
|
|
|
drawList->AddTriangle(goTip, goLeft, goRight, IM_COL32(100, 60, 0, 180), 1.0f);
|
|
|
|
|
}
|
2026-03-09 17:01:38 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Tooltip on hover
|
|
|
|
|
float mdx = mouse.x - sx, mdy = mouse.y - sy;
|
|
|
|
|
if (mdx * mdx + mdy * mdy < 64.0f) {
|
|
|
|
|
if (isQuestGO)
|
|
|
|
|
ImGui::SetTooltip("%s (quest)", goInfo->name.c_str());
|
Implement bank, guild bank, and auction house systems
Add 27 new opcodes, packet builders/parsers, handler methods, inventory
extension with 28 bank slots + 7 bank bags, and UI windows for personal
bank, guild bank (6 tabs x 98 slots), and auction house (browse/sell/bid).
Fix Classic gossip parser to omit boxMoney/boxText fields not present in
Vanilla protocol, fix gossip icon labels with text-based NPC type detection,
and add Turtle WoW opcode mappings for bank and auction interactions.
2026-02-16 21:11:18 -08:00
|
|
|
else
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::SetTooltip("%s", goInfo->name.c_str());
|
|
|
|
|
}
|
2026-03-17 16:34:39 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
}
|
2026-03-17 16:34:39 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Party member dots on minimap — small colored squares with name tooltip on hover
|
|
|
|
|
if (gameHandler.isInGroup()) {
|
|
|
|
|
const auto& partyData = gameHandler.getPartyData();
|
|
|
|
|
ImVec2 mouse = ImGui::GetMousePos();
|
|
|
|
|
for (const auto& member : partyData.members) {
|
|
|
|
|
if (!member.hasPartyStats) continue;
|
|
|
|
|
bool isOnline = (member.onlineStatus & 0x0001) != 0;
|
|
|
|
|
bool isDead = (member.onlineStatus & 0x0020) != 0;
|
|
|
|
|
bool isGhost = (member.onlineStatus & 0x0010) != 0;
|
|
|
|
|
if (!isOnline) continue;
|
|
|
|
|
if (member.posX == 0 && member.posY == 0) continue;
|
2026-03-17 16:34:39 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Party stat positions: posY = canonical X (north), posX = canonical Y (west)
|
|
|
|
|
glm::vec3 memberRender = core::coords::canonicalToRender(
|
|
|
|
|
glm::vec3(static_cast<float>(member.posY),
|
|
|
|
|
static_cast<float>(member.posX), 0.0f));
|
|
|
|
|
float sx = 0.0f, sy = 0.0f;
|
|
|
|
|
if (!projectToMinimap(memberRender, sx, sy)) continue;
|
|
|
|
|
|
|
|
|
|
// Determine dot color: class color > leader gold > light blue
|
|
|
|
|
ImU32 dotCol;
|
|
|
|
|
if (isDead || isGhost) {
|
|
|
|
|
dotCol = IM_COL32(140, 140, 140, 200); // gray for dead
|
|
|
|
|
} else {
|
|
|
|
|
auto mEnt = gameHandler.getEntityManager().getEntity(member.guid);
|
|
|
|
|
uint8_t cid = entityClassId(mEnt.get());
|
|
|
|
|
if (cid != 0) {
|
|
|
|
|
ImVec4 cv = classColorVec4(cid);
|
|
|
|
|
dotCol = IM_COL32(
|
|
|
|
|
static_cast<int>(cv.x * 255),
|
|
|
|
|
static_cast<int>(cv.y * 255),
|
|
|
|
|
static_cast<int>(cv.z * 255), 230);
|
|
|
|
|
} else if (member.guid == partyData.leaderGuid) {
|
|
|
|
|
dotCol = IM_COL32(255, 210, 0, 230); // gold for leader
|
|
|
|
|
} else {
|
|
|
|
|
dotCol = IM_COL32(100, 180, 255, 230); // blue for others
|
|
|
|
|
}
|
2026-03-17 16:34:39 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Draw a small square (WoW-style party member dot)
|
|
|
|
|
const float hs = 3.5f;
|
|
|
|
|
drawList->AddRectFilled(ImVec2(sx - hs, sy - hs), ImVec2(sx + hs, sy + hs), dotCol, 1.0f);
|
|
|
|
|
drawList->AddRect(ImVec2(sx - hs, sy - hs), ImVec2(sx + hs, sy + hs),
|
|
|
|
|
IM_COL32(0, 0, 0, 180), 1.0f, 0, 1.0f);
|
2026-03-17 16:34:39 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Name tooltip on hover
|
|
|
|
|
float mdx = mouse.x - sx, mdy = mouse.y - sy;
|
|
|
|
|
if (mdx * mdx + mdy * mdy < 64.0f && !member.name.empty()) {
|
|
|
|
|
ImGui::SetTooltip("%s", member.name.c_str());
|
2026-03-17 16:34:39 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-09 13:47:07 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
for (const auto& [guid, status] : statuses) {
|
|
|
|
|
ImU32 dotColor;
|
|
|
|
|
const char* marker = nullptr;
|
|
|
|
|
if (status == game::QuestGiverStatus::AVAILABLE) {
|
|
|
|
|
dotColor = IM_COL32(255, 210, 0, 255);
|
|
|
|
|
marker = "!";
|
|
|
|
|
} else if (status == game::QuestGiverStatus::AVAILABLE_LOW) {
|
|
|
|
|
dotColor = IM_COL32(160, 160, 160, 255);
|
|
|
|
|
marker = "!";
|
|
|
|
|
} else if (status == game::QuestGiverStatus::REWARD ||
|
|
|
|
|
status == game::QuestGiverStatus::REWARD_REP) {
|
|
|
|
|
dotColor = IM_COL32(255, 210, 0, 255);
|
|
|
|
|
marker = "?";
|
|
|
|
|
} else if (status == game::QuestGiverStatus::INCOMPLETE) {
|
|
|
|
|
dotColor = IM_COL32(160, 160, 160, 255);
|
|
|
|
|
marker = "?";
|
|
|
|
|
} else {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2026-03-09 13:47:07 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
auto entity = gameHandler.getEntityManager().getEntity(guid);
|
|
|
|
|
if (!entity) continue;
|
2026-03-09 13:47:07 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
|
|
|
|
|
glm::vec3 npcRender = core::coords::canonicalToRender(canonical);
|
2026-03-09 13:47:07 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
float sx = 0.0f, sy = 0.0f;
|
|
|
|
|
if (!projectToMinimap(npcRender, sx, sy)) continue;
|
2026-03-09 13:47:07 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Draw dot with marker text
|
|
|
|
|
drawList->AddCircleFilled(ImVec2(sx, sy), 5.0f, dotColor);
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
ImVec2 textSize = font->CalcTextSizeA(11.0f, FLT_MAX, 0.0f, marker);
|
|
|
|
|
drawList->AddText(font, 11.0f,
|
|
|
|
|
ImVec2(sx - textSize.x * 0.5f, sy - textSize.y * 0.5f),
|
|
|
|
|
IM_COL32(0, 0, 0, 255), marker);
|
2026-03-09 13:47:07 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Show NPC name and quest status on hover
|
|
|
|
|
{
|
|
|
|
|
ImVec2 mouse = ImGui::GetMousePos();
|
|
|
|
|
float mdx = mouse.x - sx, mdy = mouse.y - sy;
|
|
|
|
|
if (mdx * mdx + mdy * mdy < 64.0f) {
|
|
|
|
|
std::string npcName;
|
|
|
|
|
if (entity->getType() == game::ObjectType::UNIT) {
|
|
|
|
|
auto npcUnit = std::static_pointer_cast<game::Unit>(entity);
|
|
|
|
|
npcName = npcUnit->getName();
|
|
|
|
|
}
|
|
|
|
|
if (!npcName.empty()) {
|
|
|
|
|
bool hasQuest = (status == game::QuestGiverStatus::AVAILABLE ||
|
|
|
|
|
status == game::QuestGiverStatus::AVAILABLE_LOW);
|
|
|
|
|
ImGui::SetTooltip("%s\n%s", npcName.c_str(),
|
|
|
|
|
hasQuest ? "Has a quest for you" : "Quest ready to turn in");
|
|
|
|
|
}
|
2026-03-09 13:47:07 -07:00
|
|
|
}
|
2026-03-10 12:08:58 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Quest kill objective markers — highlight live NPCs matching active quest kill objectives
|
|
|
|
|
{
|
|
|
|
|
// Build map of NPC entry → (quest title, current, required) for tooltips
|
|
|
|
|
struct KillInfo { std::string questTitle; uint32_t current = 0; uint32_t required = 0; };
|
|
|
|
|
std::unordered_map<uint32_t, KillInfo> killInfoMap;
|
|
|
|
|
const auto& trackedIds = gameHandler.getTrackedQuestIds();
|
|
|
|
|
for (const auto& quest : gameHandler.getQuestLog()) {
|
|
|
|
|
if (quest.complete) continue;
|
|
|
|
|
if (!trackedIds.empty() && !trackedIds.count(quest.questId)) continue;
|
|
|
|
|
for (const auto& obj : quest.killObjectives) {
|
|
|
|
|
if (obj.npcOrGoId <= 0 || obj.required == 0) continue;
|
|
|
|
|
uint32_t npcEntry = static_cast<uint32_t>(obj.npcOrGoId);
|
|
|
|
|
auto it = quest.killCounts.find(npcEntry);
|
|
|
|
|
uint32_t current = (it != quest.killCounts.end()) ? it->second.first : 0;
|
|
|
|
|
if (current < obj.required) {
|
|
|
|
|
killInfoMap[npcEntry] = { quest.title, current, obj.required };
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-09 13:47:07 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
if (!killInfoMap.empty()) {
|
|
|
|
|
ImVec2 mouse = ImGui::GetMousePos();
|
|
|
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
if (!entity || entity->getType() != game::ObjectType::UNIT) continue;
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
|
|
|
if (!unit || unit->getHealth() == 0) continue;
|
|
|
|
|
auto infoIt = killInfoMap.find(unit->getEntry());
|
|
|
|
|
if (infoIt == killInfoMap.end()) continue;
|
2026-03-09 13:47:07 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
glm::vec3 unitRender = core::coords::canonicalToRender(
|
|
|
|
|
glm::vec3(entity->getX(), entity->getY(), entity->getZ()));
|
|
|
|
|
float sx = 0.0f, sy = 0.0f;
|
|
|
|
|
if (!projectToMinimap(unitRender, sx, sy)) continue;
|
2026-03-09 13:47:07 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Gold circle with a dark "x" mark — indicates a quest kill target
|
|
|
|
|
drawList->AddCircleFilled(ImVec2(sx, sy), 5.0f, IM_COL32(255, 185, 0, 240));
|
|
|
|
|
drawList->AddCircle(ImVec2(sx, sy), 5.5f, IM_COL32(0, 0, 0, 180), 12, 1.0f);
|
|
|
|
|
drawList->AddLine(ImVec2(sx - 2.5f, sy - 2.5f), ImVec2(sx + 2.5f, sy + 2.5f),
|
|
|
|
|
IM_COL32(20, 20, 20, 230), 1.2f);
|
|
|
|
|
drawList->AddLine(ImVec2(sx + 2.5f, sy - 2.5f), ImVec2(sx - 2.5f, sy + 2.5f),
|
|
|
|
|
IM_COL32(20, 20, 20, 230), 1.2f);
|
2026-03-09 13:47:07 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Tooltip on hover
|
|
|
|
|
float mdx = mouse.x - sx, mdy = mouse.y - sy;
|
|
|
|
|
if (mdx * mdx + mdy * mdy < 64.0f) {
|
|
|
|
|
const auto& ki = infoIt->second;
|
|
|
|
|
const std::string& npcName = unit->getName();
|
|
|
|
|
if (!npcName.empty()) {
|
|
|
|
|
ImGui::SetTooltip("%s\n%s: %u/%u",
|
|
|
|
|
npcName.c_str(),
|
|
|
|
|
ki.questTitle.empty() ? "Quest" : ki.questTitle.c_str(),
|
|
|
|
|
ki.current, ki.required);
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::SetTooltip("%s: %u/%u",
|
|
|
|
|
ki.questTitle.empty() ? "Quest" : ki.questTitle.c_str(),
|
|
|
|
|
ki.current, ki.required);
|
|
|
|
|
}
|
2026-03-18 12:43:04 -07:00
|
|
|
}
|
2026-03-09 13:47:07 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
}
|
2026-03-09 13:47:07 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Gossip POI markers (quest / NPC navigation targets)
|
|
|
|
|
for (const auto& poi : gameHandler.getGossipPois()) {
|
|
|
|
|
// Convert WoW canonical coords to render coords for minimap projection
|
|
|
|
|
glm::vec3 poiRender = core::coords::canonicalToRender(glm::vec3(poi.x, poi.y, 0.0f));
|
|
|
|
|
float sx = 0.0f, sy = 0.0f;
|
|
|
|
|
if (!projectToMinimap(poiRender, sx, sy)) continue;
|
2026-03-09 13:47:07 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Draw as a cyan diamond with tooltip on hover
|
|
|
|
|
const float d = 5.0f;
|
|
|
|
|
ImVec2 pts[4] = {
|
|
|
|
|
{ sx, sy - d },
|
|
|
|
|
{ sx + d, sy },
|
|
|
|
|
{ sx, sy + d },
|
|
|
|
|
{ sx - d, sy },
|
|
|
|
|
};
|
|
|
|
|
drawList->AddConvexPolyFilled(pts, 4, IM_COL32(0, 210, 255, 220));
|
|
|
|
|
drawList->AddPolyline(pts, 4, IM_COL32(255, 255, 255, 160), true, 1.0f);
|
2026-03-09 13:47:07 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Show name label if cursor is within ~8px
|
|
|
|
|
ImVec2 cursorPos = ImGui::GetMousePos();
|
|
|
|
|
float dx = cursorPos.x - sx, dy = cursorPos.y - sy;
|
|
|
|
|
if (!poi.name.empty() && (dx * dx + dy * dy) < 64.0f) {
|
|
|
|
|
ImGui::SetTooltip("%s", poi.name.c_str());
|
2026-03-09 13:47:07 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Minimap pings from party members
|
|
|
|
|
for (const auto& ping : gameHandler.getMinimapPings()) {
|
|
|
|
|
glm::vec3 pingRender = core::coords::canonicalToRender(glm::vec3(ping.wowX, ping.wowY, 0.0f));
|
|
|
|
|
float sx = 0.0f, sy = 0.0f;
|
|
|
|
|
if (!projectToMinimap(pingRender, sx, sy)) continue;
|
2026-03-09 15:52:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
float t = ping.age / game::GameHandler::MinimapPing::LIFETIME;
|
|
|
|
|
float alpha = 1.0f - t;
|
|
|
|
|
float pulse = 1.0f + 1.5f * t; // expands outward as it fades
|
2026-03-09 15:52:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImU32 col = IM_COL32(255, 220, 0, static_cast<int>(alpha * 200));
|
|
|
|
|
ImU32 col2 = IM_COL32(255, 150, 0, static_cast<int>(alpha * 100));
|
|
|
|
|
float r1 = 4.0f * pulse;
|
|
|
|
|
float r2 = 8.0f * pulse;
|
|
|
|
|
drawList->AddCircle(ImVec2(sx, sy), r1, col, 16, 2.0f);
|
|
|
|
|
drawList->AddCircle(ImVec2(sx, sy), r2, col2, 16, 1.0f);
|
|
|
|
|
drawList->AddCircleFilled(ImVec2(sx, sy), 2.5f, col);
|
2026-03-09 15:52:58 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Party member dots on minimap
|
|
|
|
|
{
|
|
|
|
|
const auto& partyData = gameHandler.getPartyData();
|
|
|
|
|
const uint64_t leaderGuid = partyData.leaderGuid;
|
|
|
|
|
for (const auto& member : partyData.members) {
|
|
|
|
|
if (!member.isOnline || !member.hasPartyStats) continue;
|
|
|
|
|
if (member.posX == 0 && member.posY == 0) continue;
|
2026-03-09 15:52:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// posX/posY follow same server axis convention as minimap pings:
|
|
|
|
|
// server posX = east/west axis → canonical Y (west)
|
|
|
|
|
// server posY = north/south axis → canonical X (north)
|
|
|
|
|
float wowX = static_cast<float>(member.posY);
|
|
|
|
|
float wowY = static_cast<float>(member.posX);
|
|
|
|
|
glm::vec3 memberRender = core::coords::canonicalToRender(glm::vec3(wowX, wowY, 0.0f));
|
2026-03-09 15:52:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
float sx = 0.0f, sy = 0.0f;
|
|
|
|
|
if (!projectToMinimap(memberRender, sx, sy)) continue;
|
2026-03-09 15:52:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImU32 dotColor;
|
|
|
|
|
{
|
|
|
|
|
auto mEnt = gameHandler.getEntityManager().getEntity(member.guid);
|
|
|
|
|
uint8_t cid = entityClassId(mEnt.get());
|
|
|
|
|
dotColor = (cid != 0)
|
|
|
|
|
? classColorU32(cid, 235)
|
|
|
|
|
: (member.guid == leaderGuid)
|
|
|
|
|
? IM_COL32(255, 210, 0, 235)
|
|
|
|
|
: IM_COL32(100, 180, 255, 235);
|
|
|
|
|
}
|
|
|
|
|
drawList->AddCircleFilled(ImVec2(sx, sy), 4.0f, dotColor);
|
|
|
|
|
drawList->AddCircle(ImVec2(sx, sy), 4.0f, IM_COL32(255, 255, 255, 160), 12, 1.0f);
|
2026-03-09 15:52:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Raid mark: tiny symbol drawn above the dot
|
|
|
|
|
{
|
|
|
|
|
static constexpr struct { const char* sym; ImU32 col; } kMMMarks[] = {
|
|
|
|
|
{ "\xe2\x98\x85", IM_COL32(255, 220, 50, 255) },
|
|
|
|
|
{ "\xe2\x97\x8f", IM_COL32(255, 140, 0, 255) },
|
|
|
|
|
{ "\xe2\x97\x86", IM_COL32(160, 32, 240, 255) },
|
|
|
|
|
{ "\xe2\x96\xb2", IM_COL32( 50, 200, 50, 255) },
|
|
|
|
|
{ "\xe2\x97\x8c", IM_COL32( 80, 160, 255, 255) },
|
|
|
|
|
{ "\xe2\x96\xa0", IM_COL32( 50, 200, 220, 255) },
|
|
|
|
|
{ "\xe2\x9c\x9d", IM_COL32(255, 80, 80, 255) },
|
|
|
|
|
{ "\xe2\x98\xa0", IM_COL32(255, 255, 255, 255) },
|
|
|
|
|
};
|
|
|
|
|
uint8_t pmk = gameHandler.getEntityRaidMark(member.guid);
|
|
|
|
|
if (pmk < game::GameHandler::kRaidMarkCount) {
|
|
|
|
|
ImFont* mmFont = ImGui::GetFont();
|
|
|
|
|
ImVec2 msz = mmFont->CalcTextSizeA(9.0f, FLT_MAX, 0.0f, kMMMarks[pmk].sym);
|
|
|
|
|
drawList->AddText(mmFont, 9.0f,
|
|
|
|
|
ImVec2(sx - msz.x * 0.5f, sy - 4.0f - msz.y),
|
|
|
|
|
kMMMarks[pmk].col, kMMMarks[pmk].sym);
|
2026-03-09 15:52:58 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
}
|
2026-03-09 15:52:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImVec2 cursorPos = ImGui::GetMousePos();
|
|
|
|
|
float mdx = cursorPos.x - sx, mdy = cursorPos.y - sy;
|
|
|
|
|
if (!member.name.empty() && (mdx * mdx + mdy * mdy) < 64.0f) {
|
|
|
|
|
uint8_t pmk2 = gameHandler.getEntityRaidMark(member.guid);
|
|
|
|
|
if (pmk2 < game::GameHandler::kRaidMarkCount) {
|
|
|
|
|
static constexpr const char* kMarkNames[] = {
|
|
|
|
|
"Star", "Circle", "Diamond", "Triangle",
|
|
|
|
|
"Moon", "Square", "Cross", "Skull"
|
|
|
|
|
};
|
|
|
|
|
ImGui::SetTooltip("%s {%s}", member.name.c_str(), kMarkNames[pmk2]);
|
2026-03-09 15:52:58 -07:00
|
|
|
} else {
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::SetTooltip("%s", member.name.c_str());
|
2026-03-09 15:52:58 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// BG flag carrier / important player positions (MSG_BATTLEGROUND_PLAYER_POSITIONS)
|
|
|
|
|
{
|
|
|
|
|
const auto& bgPositions = gameHandler.getBgPlayerPositions();
|
|
|
|
|
if (!bgPositions.empty()) {
|
|
|
|
|
ImVec2 mouse = ImGui::GetMousePos();
|
|
|
|
|
// group 0 = typically ally-held flag / first list; group 1 = enemy
|
|
|
|
|
static const ImU32 kBgGroupColors[2] = {
|
|
|
|
|
IM_COL32( 80, 180, 255, 240), // group 0: blue (alliance)
|
|
|
|
|
IM_COL32(220, 50, 50, 240), // group 1: red (horde)
|
|
|
|
|
};
|
|
|
|
|
for (const auto& bp : bgPositions) {
|
|
|
|
|
// Packet coords: wowX=canonical X (north), wowY=canonical Y (west)
|
|
|
|
|
glm::vec3 bpRender = core::coords::canonicalToRender(glm::vec3(bp.wowX, bp.wowY, 0.0f));
|
|
|
|
|
float sx = 0.0f, sy = 0.0f;
|
|
|
|
|
if (!projectToMinimap(bpRender, sx, sy)) continue;
|
2026-03-09 15:52:58 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImU32 col = kBgGroupColors[bp.group & 1];
|
2026-03-09 22:42:44 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Draw a flag-like diamond icon
|
|
|
|
|
const float r = 5.0f;
|
|
|
|
|
ImVec2 top (sx, sy - r);
|
|
|
|
|
ImVec2 right(sx + r, sy );
|
|
|
|
|
ImVec2 bot (sx, sy + r);
|
|
|
|
|
ImVec2 left (sx - r, sy );
|
|
|
|
|
drawList->AddQuadFilled(top, right, bot, left, col);
|
|
|
|
|
drawList->AddQuad(top, right, bot, left, IM_COL32(255, 255, 255, 180), 1.0f);
|
2026-03-09 22:42:44 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
float mdx = mouse.x - sx, mdy = mouse.y - sy;
|
|
|
|
|
if (mdx * mdx + mdy * mdy < 64.0f) {
|
|
|
|
|
// Show entity name if available, otherwise guid
|
|
|
|
|
auto ent = gameHandler.getEntityManager().getEntity(bp.guid);
|
|
|
|
|
if (ent) {
|
|
|
|
|
std::string nm;
|
|
|
|
|
if (ent->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
auto pl = std::static_pointer_cast<game::Unit>(ent);
|
|
|
|
|
nm = pl ? pl->getName() : "";
|
|
|
|
|
}
|
|
|
|
|
if (!nm.empty())
|
|
|
|
|
ImGui::SetTooltip("Flag carrier: %s", nm.c_str());
|
|
|
|
|
else
|
|
|
|
|
ImGui::SetTooltip("Flag carrier");
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::SetTooltip("Flag carrier");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-09 22:42:44 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Corpse direction indicator — shown when player is a ghost
|
|
|
|
|
if (gameHandler.isPlayerGhost()) {
|
|
|
|
|
float corpseCanX = 0.0f, corpseCanY = 0.0f;
|
|
|
|
|
if (gameHandler.getCorpseCanonicalPos(corpseCanX, corpseCanY)) {
|
|
|
|
|
glm::vec3 corpseRender = core::coords::canonicalToRender(glm::vec3(corpseCanX, corpseCanY, 0.0f));
|
|
|
|
|
float csx = 0.0f, csy = 0.0f;
|
|
|
|
|
bool onMap = projectToMinimap(corpseRender, csx, csy);
|
2026-03-09 22:42:44 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
if (onMap) {
|
|
|
|
|
// Draw a small skull-like X marker at the corpse position
|
|
|
|
|
const float r = 5.0f;
|
|
|
|
|
drawList->AddCircleFilled(ImVec2(csx, csy), r + 1.0f, IM_COL32(0, 0, 0, 140), 12);
|
|
|
|
|
drawList->AddCircle(ImVec2(csx, csy), r + 1.0f, IM_COL32(200, 200, 220, 220), 12, 1.5f);
|
|
|
|
|
// Draw an X in the circle
|
|
|
|
|
drawList->AddLine(ImVec2(csx - 3.0f, csy - 3.0f), ImVec2(csx + 3.0f, csy + 3.0f),
|
|
|
|
|
IM_COL32(180, 180, 220, 255), 1.5f);
|
|
|
|
|
drawList->AddLine(ImVec2(csx + 3.0f, csy - 3.0f), ImVec2(csx - 3.0f, csy + 3.0f),
|
|
|
|
|
IM_COL32(180, 180, 220, 255), 1.5f);
|
|
|
|
|
// Tooltip on hover
|
|
|
|
|
ImVec2 mouse = ImGui::GetMousePos();
|
|
|
|
|
float mdx = mouse.x - csx, mdy = mouse.y - csy;
|
|
|
|
|
if (mdx * mdx + mdy * mdy < 64.0f) {
|
|
|
|
|
float dist = gameHandler.getCorpseDistance();
|
|
|
|
|
if (dist >= 0.0f)
|
|
|
|
|
ImGui::SetTooltip("Your corpse (%.0f yd)", dist);
|
|
|
|
|
else
|
|
|
|
|
ImGui::SetTooltip("Your corpse");
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Corpse is outside minimap — draw an edge arrow pointing toward it
|
|
|
|
|
float dx = corpseRender.x - playerRender.x;
|
|
|
|
|
float dy = corpseRender.y - playerRender.y;
|
|
|
|
|
// Rotate delta into minimap frame (same as projectToMinimap)
|
|
|
|
|
float rx = -(dx * cosB + dy * sinB);
|
|
|
|
|
float ry = dx * sinB - dy * cosB;
|
|
|
|
|
float len = std::sqrt(rx * rx + ry * ry);
|
|
|
|
|
if (len > 0.001f) {
|
|
|
|
|
float nx = rx / len;
|
|
|
|
|
float ny = ry / len;
|
|
|
|
|
// Place arrow at the minimap edge
|
|
|
|
|
float edgeR = mapRadius - 7.0f;
|
|
|
|
|
float ax = centerX + nx * edgeR;
|
|
|
|
|
float ay = centerY + ny * edgeR;
|
|
|
|
|
// Arrow pointing outward (toward corpse)
|
|
|
|
|
float arrowLen = 6.0f;
|
|
|
|
|
float arrowW = 3.5f;
|
|
|
|
|
ImVec2 tip(ax + nx * arrowLen, ay + ny * arrowLen);
|
|
|
|
|
ImVec2 left(ax - ny * arrowW - nx * arrowLen * 0.4f,
|
|
|
|
|
ay + nx * arrowW - ny * arrowLen * 0.4f);
|
|
|
|
|
ImVec2 right(ax + ny * arrowW - nx * arrowLen * 0.4f,
|
|
|
|
|
ay - nx * arrowW - ny * arrowLen * 0.4f);
|
|
|
|
|
drawList->AddTriangleFilled(tip, left, right, IM_COL32(180, 180, 240, 230));
|
|
|
|
|
drawList->AddTriangle(tip, left, right, IM_COL32(0, 0, 0, 180), 1.0f);
|
|
|
|
|
// Tooltip on hover
|
|
|
|
|
ImVec2 mouse = ImGui::GetMousePos();
|
|
|
|
|
float mdx = mouse.x - ax, mdy = mouse.y - ay;
|
|
|
|
|
if (mdx * mdx + mdy * mdy < 100.0f) {
|
|
|
|
|
float dist = gameHandler.getCorpseDistance();
|
|
|
|
|
if (dist >= 0.0f)
|
|
|
|
|
ImGui::SetTooltip("Your corpse (%.0f yd)", dist);
|
|
|
|
|
else
|
|
|
|
|
ImGui::SetTooltip("Your corpse");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-09 22:42:44 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Player position arrow at minimap center, pointing in camera facing direction.
|
|
|
|
|
// On a rotating minimap the map already turns so forward = screen-up; on a fixed
|
|
|
|
|
// minimap we rotate the arrow to match the player's compass heading.
|
|
|
|
|
{
|
|
|
|
|
// Compute screen-space facing direction for the arrow.
|
|
|
|
|
// bearing = clockwise angle from screen-north (0 = facing north/up).
|
|
|
|
|
float arrowAngle = 0.0f; // 0 = pointing up (north)
|
|
|
|
|
if (!minimap->isRotateWithCamera()) {
|
|
|
|
|
// Fixed minimap: arrow must show actual facing relative to north.
|
|
|
|
|
glm::vec3 fwd = camera->getForward();
|
|
|
|
|
// +render_y = north = screen-up, +render_x = west = screen-left.
|
|
|
|
|
// bearing from north clockwise: atan2(-fwd.x_west, fwd.y_north)
|
|
|
|
|
// => sin=east component, cos=north component
|
|
|
|
|
// In render coords west=+x, east=-x, so sin(bearing)=east=-fwd.x
|
|
|
|
|
arrowAngle = std::atan2(-fwd.x, fwd.y); // clockwise from north in screen space
|
|
|
|
|
}
|
|
|
|
|
// Screen direction the arrow tip points toward
|
|
|
|
|
float nx = std::sin(arrowAngle); // screen +X = east
|
|
|
|
|
float ny = -std::cos(arrowAngle); // screen -Y = north
|
2026-03-09 22:42:44 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Draw a chevron-style arrow: tip, two base corners, and a notch at the back
|
|
|
|
|
const float tipLen = 8.0f; // tip forward distance
|
|
|
|
|
const float baseW = 5.0f; // half-width at base
|
|
|
|
|
const float notchIn = 3.0f; // how far back the center notch sits
|
|
|
|
|
// Perpendicular direction (rotated 90°)
|
|
|
|
|
float px = ny; // perpendicular x
|
|
|
|
|
float py = -nx; // perpendicular y
|
2026-03-09 22:42:44 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImVec2 tip (centerX + nx * tipLen, centerY + ny * tipLen);
|
|
|
|
|
ImVec2 baseL(centerX - nx * baseW + px * baseW, centerY - ny * baseW + py * baseW);
|
|
|
|
|
ImVec2 baseR(centerX - nx * baseW - px * baseW, centerY - ny * baseW - py * baseW);
|
|
|
|
|
ImVec2 notch(centerX - nx * (baseW - notchIn), centerY - ny * (baseW - notchIn));
|
2026-03-09 22:42:44 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Fill: bright white with slight gold tint, dark outline for readability
|
|
|
|
|
drawList->AddTriangleFilled(tip, baseL, notch, IM_COL32(255, 248, 200, 245));
|
|
|
|
|
drawList->AddTriangleFilled(tip, notch, baseR, IM_COL32(255, 248, 200, 245));
|
|
|
|
|
drawList->AddTriangle(tip, baseL, notch, IM_COL32(60, 40, 0, 200), 1.2f);
|
|
|
|
|
drawList->AddTriangle(tip, notch, baseR, IM_COL32(60, 40, 0, 200), 1.2f);
|
|
|
|
|
}
|
2026-03-09 22:42:44 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Scroll wheel over minimap → zoom in/out
|
|
|
|
|
{
|
|
|
|
|
float wheel = ImGui::GetIO().MouseWheel;
|
|
|
|
|
if (wheel != 0.0f) {
|
|
|
|
|
ImVec2 mouse = ImGui::GetMousePos();
|
|
|
|
|
float mdx = mouse.x - centerX;
|
|
|
|
|
float mdy = mouse.y - centerY;
|
|
|
|
|
if (mdx * mdx + mdy * mdy <= mapRadius * mapRadius) {
|
|
|
|
|
if (wheel > 0.0f)
|
|
|
|
|
minimap->zoomIn();
|
|
|
|
|
else
|
|
|
|
|
minimap->zoomOut();
|
|
|
|
|
}
|
2026-03-09 22:42:44 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
}
|
2026-03-09 22:42:44 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Ctrl+click on minimap → send minimap ping to party
|
|
|
|
|
if (ImGui::IsMouseClicked(0) && ImGui::GetIO().KeyCtrl) {
|
|
|
|
|
ImVec2 mouse = ImGui::GetMousePos();
|
|
|
|
|
float mdx = mouse.x - centerX;
|
|
|
|
|
float mdy = mouse.y - centerY;
|
|
|
|
|
float distSq = mdx * mdx + mdy * mdy;
|
|
|
|
|
if (distSq <= mapRadius * mapRadius) {
|
|
|
|
|
// Invert projectToMinimap: px=mdx, py=mdy → rx=px*viewRadius/mapRadius
|
|
|
|
|
float rx = mdx * viewRadius / mapRadius;
|
|
|
|
|
float ry = mdy * viewRadius / mapRadius;
|
|
|
|
|
// rx/ry are in rotated frame; unrotate to get world dx/dy
|
|
|
|
|
// rx = -(dx*cosB + dy*sinB), ry = dx*sinB - dy*cosB
|
|
|
|
|
// Solving: dx = -(rx*cosB - ry*sinB), dy = -(rx*sinB + ry*cosB)
|
|
|
|
|
float wdx = -(rx * cosB - ry * sinB);
|
|
|
|
|
float wdy = -(rx * sinB + ry * cosB);
|
|
|
|
|
// playerRender is in render coords; add delta to get render position then convert to canonical
|
|
|
|
|
glm::vec3 clickRender = playerRender + glm::vec3(wdx, wdy, 0.0f);
|
|
|
|
|
glm::vec3 clickCanon = core::coords::renderToCanonical(clickRender);
|
|
|
|
|
gameHandler.sendMinimapPing(clickCanon.x, clickCanon.y);
|
2026-03-09 22:42:44 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 10:41:18 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Persistent coordinate display below the minimap
|
|
|
|
|
{
|
|
|
|
|
glm::vec3 playerCanon = core::coords::renderToCanonical(playerRender);
|
|
|
|
|
char coordBuf[32];
|
|
|
|
|
std::snprintf(coordBuf, sizeof(coordBuf), "%.1f, %.1f", playerCanon.x, playerCanon.y);
|
2026-03-12 10:41:18 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
float fontSize = ImGui::GetFontSize();
|
|
|
|
|
ImVec2 textSz = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, coordBuf);
|
2026-03-12 10:41:18 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
float tx = centerX - textSz.x * 0.5f;
|
|
|
|
|
float ty = centerY + mapRadius + 3.0f;
|
2026-03-12 10:41:18 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Semi-transparent dark background pill
|
|
|
|
|
float pad = 3.0f;
|
|
|
|
|
drawList->AddRectFilled(
|
|
|
|
|
ImVec2(tx - pad, ty - pad),
|
|
|
|
|
ImVec2(tx + textSz.x + pad, ty + textSz.y + pad),
|
|
|
|
|
IM_COL32(0, 0, 0, 140), 4.0f);
|
|
|
|
|
// Coordinate text in warm yellow
|
|
|
|
|
drawList->AddText(font, fontSize, ImVec2(tx, ty), IM_COL32(230, 220, 140, 255), coordBuf);
|
2026-03-12 10:41:18 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Local time clock — displayed just below the coordinate label
|
|
|
|
|
{
|
|
|
|
|
auto now = std::chrono::system_clock::now();
|
|
|
|
|
std::time_t tt = std::chrono::system_clock::to_time_t(now);
|
|
|
|
|
std::tm tmLocal{};
|
|
|
|
|
#if defined(_WIN32)
|
|
|
|
|
localtime_s(&tmLocal, &tt);
|
|
|
|
|
#else
|
|
|
|
|
localtime_r(&tt, &tmLocal);
|
|
|
|
|
#endif
|
|
|
|
|
char clockBuf[16];
|
|
|
|
|
std::snprintf(clockBuf, sizeof(clockBuf), "%02d:%02d",
|
|
|
|
|
tmLocal.tm_hour, tmLocal.tm_min);
|
2026-03-12 10:45:31 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
float fontSize = ImGui::GetFontSize() * 0.9f; // slightly smaller than coords
|
|
|
|
|
ImVec2 clockSz = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, clockBuf);
|
2026-03-12 10:41:18 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
float tx = centerX - clockSz.x * 0.5f;
|
|
|
|
|
// Position below the coordinate line (+fontSize of coord + 2px gap)
|
|
|
|
|
float coordLineH = ImGui::GetFontSize();
|
|
|
|
|
float ty = centerY + mapRadius + 3.0f + coordLineH + 2.0f;
|
2026-03-12 10:41:18 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
float pad = 2.0f;
|
|
|
|
|
drawList->AddRectFilled(
|
|
|
|
|
ImVec2(tx - pad, ty - pad),
|
|
|
|
|
ImVec2(tx + clockSz.x + pad, ty + clockSz.y + pad),
|
|
|
|
|
IM_COL32(0, 0, 0, 120), 3.0f);
|
|
|
|
|
drawList->AddText(font, fontSize, ImVec2(tx, ty), IM_COL32(200, 200, 220, 220), clockBuf);
|
|
|
|
|
}
|
2026-03-12 10:41:18 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Zone name display — drawn inside the top edge of the minimap circle
|
|
|
|
|
{
|
|
|
|
|
auto* zmRenderer = renderer ? renderer->getZoneManager() : nullptr;
|
|
|
|
|
uint32_t zoneId = gameHandler.getWorldStateZoneId();
|
|
|
|
|
const game::ZoneInfo* zi = (zmRenderer && zoneId != 0) ? zmRenderer->getZoneInfo(zoneId) : nullptr;
|
|
|
|
|
if (zi && !zi->name.empty()) {
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
float fontSize = ImGui::GetFontSize();
|
|
|
|
|
ImVec2 ts = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, zi->name.c_str());
|
|
|
|
|
float tx = centerX - ts.x * 0.5f;
|
|
|
|
|
float ty = centerY - mapRadius + 4.0f; // just inside top edge of the circle
|
|
|
|
|
float pad = 2.0f;
|
|
|
|
|
drawList->AddRectFilled(
|
|
|
|
|
ImVec2(tx - pad, ty - pad),
|
|
|
|
|
ImVec2(tx + ts.x + pad, ty + ts.y + pad),
|
|
|
|
|
IM_COL32(0, 0, 0, 160), 2.0f);
|
|
|
|
|
drawList->AddText(font, fontSize, ImVec2(tx + 1.0f, ty + 1.0f),
|
|
|
|
|
IM_COL32(0, 0, 0, 180), zi->name.c_str());
|
|
|
|
|
drawList->AddText(font, fontSize, ImVec2(tx, ty),
|
|
|
|
|
IM_COL32(255, 230, 150, 220), zi->name.c_str());
|
2026-03-12 10:41:18 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Instance difficulty indicator — just below zone name, inside minimap top edge
|
|
|
|
|
if (gameHandler.isInInstance()) {
|
|
|
|
|
static constexpr const char* kDiffLabels[] = {"Normal", "Heroic", "25 Normal", "25 Heroic"};
|
|
|
|
|
uint32_t diff = gameHandler.getInstanceDifficulty();
|
|
|
|
|
const char* label = (diff < 4) ? kDiffLabels[diff] : "Unknown";
|
feat: add persistent combat log window (/combatlog or /cl)
Stores up to 500 combat events in a rolling deque alongside the existing
floating combat text. Events are populated via the existing addCombatText()
call site, resolving attacker/target names from the entity manager and
player name cache at event time.
- CombatLogEntry struct in spell_defines.hpp (type, amount, spellId,
isPlayerSource, timestamp, sourceName, targetName)
- getCombatLog() / clearCombatLog() accessors on GameHandler
- renderCombatLog() in GameScreen: scrollable two-column table (Time +
Event), color-coded by event category, with Damage/Healing/Misc filter
checkboxes, auto-scroll toggle, and Clear button
- /combatlog (/cl) chat command toggles the window
2026-03-12 11:00:10 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
float fontSize = ImGui::GetFontSize() * 0.85f;
|
|
|
|
|
ImVec2 ts = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, label);
|
|
|
|
|
float tx = centerX - ts.x * 0.5f;
|
|
|
|
|
// Position below zone name: top edge + zone font size + small gap
|
|
|
|
|
float ty = centerY - mapRadius + 4.0f + ImGui::GetFontSize() + 2.0f;
|
|
|
|
|
float pad = 2.0f;
|
feat: add persistent combat log window (/combatlog or /cl)
Stores up to 500 combat events in a rolling deque alongside the existing
floating combat text. Events are populated via the existing addCombatText()
call site, resolving attacker/target names from the entity manager and
player name cache at event time.
- CombatLogEntry struct in spell_defines.hpp (type, amount, spellId,
isPlayerSource, timestamp, sourceName, targetName)
- getCombatLog() / clearCombatLog() accessors on GameHandler
- renderCombatLog() in GameScreen: scrollable two-column table (Time +
Event), color-coded by event category, with Damage/Healing/Misc filter
checkboxes, auto-scroll toggle, and Clear button
- /combatlog (/cl) chat command toggles the window
2026-03-12 11:00:10 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Color-code: heroic=orange, normal=light gray
|
|
|
|
|
ImU32 bgCol = gameHandler.isInstanceHeroic() ? IM_COL32(120, 60, 0, 180) : IM_COL32(0, 0, 0, 160);
|
|
|
|
|
ImU32 textCol = gameHandler.isInstanceHeroic() ? IM_COL32(255, 180, 50, 255) : IM_COL32(200, 200, 200, 220);
|
feat: add persistent combat log window (/combatlog or /cl)
Stores up to 500 combat events in a rolling deque alongside the existing
floating combat text. Events are populated via the existing addCombatText()
call site, resolving attacker/target names from the entity manager and
player name cache at event time.
- CombatLogEntry struct in spell_defines.hpp (type, amount, spellId,
isPlayerSource, timestamp, sourceName, targetName)
- getCombatLog() / clearCombatLog() accessors on GameHandler
- renderCombatLog() in GameScreen: scrollable two-column table (Time +
Event), color-coded by event category, with Damage/Healing/Misc filter
checkboxes, auto-scroll toggle, and Clear button
- /combatlog (/cl) chat command toggles the window
2026-03-12 11:00:10 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
drawList->AddRectFilled(
|
|
|
|
|
ImVec2(tx - pad, ty - pad),
|
|
|
|
|
ImVec2(tx + ts.x + pad, ty + ts.y + pad),
|
|
|
|
|
bgCol, 2.0f);
|
|
|
|
|
drawList->AddText(font, fontSize, ImVec2(tx, ty), textCol, label);
|
feat: add persistent combat log window (/combatlog or /cl)
Stores up to 500 combat events in a rolling deque alongside the existing
floating combat text. Events are populated via the existing addCombatText()
call site, resolving attacker/target names from the entity manager and
player name cache at event time.
- CombatLogEntry struct in spell_defines.hpp (type, amount, spellId,
isPlayerSource, timestamp, sourceName, targetName)
- getCombatLog() / clearCombatLog() accessors on GameHandler
- renderCombatLog() in GameScreen: scrollable two-column table (Time +
Event), color-coded by event category, with Damage/Healing/Misc filter
checkboxes, auto-scroll toggle, and Clear button
- /combatlog (/cl) chat command toggles the window
2026-03-12 11:00:10 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Hover tooltip and right-click context menu
|
|
|
|
|
{
|
|
|
|
|
ImVec2 mouse = ImGui::GetMousePos();
|
|
|
|
|
float mdx = mouse.x - centerX;
|
|
|
|
|
float mdy = mouse.y - centerY;
|
|
|
|
|
bool overMinimap = (mdx * mdx + mdy * mdy <= mapRadius * mapRadius);
|
feat: add persistent combat log window (/combatlog or /cl)
Stores up to 500 combat events in a rolling deque alongside the existing
floating combat text. Events are populated via the existing addCombatText()
call site, resolving attacker/target names from the entity manager and
player name cache at event time.
- CombatLogEntry struct in spell_defines.hpp (type, amount, spellId,
isPlayerSource, timestamp, sourceName, targetName)
- getCombatLog() / clearCombatLog() accessors on GameHandler
- renderCombatLog() in GameScreen: scrollable two-column table (Time +
Event), color-coded by event category, with Damage/Healing/Misc filter
checkboxes, auto-scroll toggle, and Clear button
- /combatlog (/cl) chat command toggles the window
2026-03-12 11:00:10 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
if (overMinimap) {
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
// Compute the world coordinate under the mouse cursor
|
|
|
|
|
// Inverse of projectToMinimap: pixel offset → world offset in render space → canonical
|
|
|
|
|
float rxW = mdx / mapRadius * viewRadius;
|
|
|
|
|
float ryW = mdy / mapRadius * viewRadius;
|
|
|
|
|
// Un-rotate: [dx, dy] = R^-1 * [rxW, ryW]
|
|
|
|
|
// where R applied: rx = -(dx*cosB + dy*sinB), ry = dx*sinB - dy*cosB
|
|
|
|
|
float hoverDx = -cosB * rxW + sinB * ryW;
|
|
|
|
|
float hoverDy = -sinB * rxW - cosB * ryW;
|
|
|
|
|
glm::vec3 hoverRender(playerRender.x + hoverDx, playerRender.y + hoverDy, playerRender.z);
|
|
|
|
|
glm::vec3 hoverCanon = core::coords::renderToCanonical(hoverRender);
|
|
|
|
|
ImGui::TextColored(ImVec4(0.9f, 0.85f, 0.5f, 1.0f), "%.1f, %.1f", hoverCanon.x, hoverCanon.y);
|
|
|
|
|
ImGui::TextColored(colors::kMediumGray, "Ctrl+click to ping");
|
|
|
|
|
ImGui::EndTooltip();
|
feat: add persistent combat log window (/combatlog or /cl)
Stores up to 500 combat events in a rolling deque alongside the existing
floating combat text. Events are populated via the existing addCombatText()
call site, resolving attacker/target names from the entity manager and
player name cache at event time.
- CombatLogEntry struct in spell_defines.hpp (type, amount, spellId,
isPlayerSource, timestamp, sourceName, targetName)
- getCombatLog() / clearCombatLog() accessors on GameHandler
- renderCombatLog() in GameScreen: scrollable two-column table (Time +
Event), color-coded by event category, with Damage/Healing/Misc filter
checkboxes, auto-scroll toggle, and Clear button
- /combatlog (/cl) chat command toggles the window
2026-03-12 11:00:10 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
|
|
|
|
|
ImGui::OpenPopup("##minimapContextMenu");
|
2026-03-12 13:21:00 -07:00
|
|
|
}
|
feat: add persistent combat log window (/combatlog or /cl)
Stores up to 500 combat events in a rolling deque alongside the existing
floating combat text. Events are populated via the existing addCombatText()
call site, resolving attacker/target names from the entity manager and
player name cache at event time.
- CombatLogEntry struct in spell_defines.hpp (type, amount, spellId,
isPlayerSource, timestamp, sourceName, targetName)
- getCombatLog() / clearCombatLog() accessors on GameHandler
- renderCombatLog() in GameScreen: scrollable two-column table (Time +
Event), color-coded by event category, with Damage/Healing/Misc filter
checkboxes, auto-scroll toggle, and Clear button
- /combatlog (/cl) chat command toggles the window
2026-03-12 11:00:10 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
if (ImGui::BeginPopup("##minimapContextMenu")) {
|
|
|
|
|
ImGui::TextColored(ui::colors::kTooltipGold, "Minimap");
|
|
|
|
|
ImGui::Separator();
|
2026-03-12 02:09:35 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Zoom controls
|
|
|
|
|
if (ImGui::MenuItem("Zoom In")) {
|
|
|
|
|
minimap->zoomIn();
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::MenuItem("Zoom Out")) {
|
|
|
|
|
minimap->zoomOut();
|
2026-03-12 03:03:02 -07:00
|
|
|
}
|
2026-03-12 02:09:35 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
// Toggle options with checkmarks
|
|
|
|
|
bool rotWithCam = minimap->isRotateWithCamera();
|
|
|
|
|
if (ImGui::MenuItem("Rotate with Camera", nullptr, rotWithCam)) {
|
|
|
|
|
minimap->setRotateWithCamera(!rotWithCam);
|
2026-03-12 12:52:08 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
bool squareShape = minimap->isSquareShape();
|
|
|
|
|
if (ImGui::MenuItem("Square Shape", nullptr, squareShape)) {
|
|
|
|
|
minimap->setSquareShape(!squareShape);
|
|
|
|
|
}
|
2026-03-12 12:52:08 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
bool npcDots = settingsPanel_.minimapNpcDots_;
|
|
|
|
|
if (ImGui::MenuItem("Show NPC Dots", nullptr, npcDots)) {
|
|
|
|
|
settingsPanel_.minimapNpcDots_ = !settingsPanel_.minimapNpcDots_;
|
2026-03-12 03:03:02 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
|
|
|
|
|
ImGui::EndPopup();
|
2026-03-12 02:09:35 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
auto applyMuteState = [&]() {
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* activeRenderer = services_.renderer;
|
2026-03-31 19:49:52 +03:00
|
|
|
float masterScale = settingsPanel_.soundMuted_ ? 0.0f : static_cast<float>(settingsPanel_.pendingMasterVolume) / 100.0f;
|
|
|
|
|
audio::AudioEngine::instance().setMasterVolume(masterScale);
|
|
|
|
|
if (!activeRenderer) return;
|
|
|
|
|
if (auto* music = activeRenderer->getMusicManager()) {
|
|
|
|
|
music->setVolume(settingsPanel_.pendingMusicVolume);
|
|
|
|
|
}
|
|
|
|
|
if (auto* ambient = activeRenderer->getAmbientSoundManager()) {
|
|
|
|
|
ambient->setVolumeScale(settingsPanel_.pendingAmbientVolume / 100.0f);
|
|
|
|
|
}
|
|
|
|
|
if (auto* ui = activeRenderer->getUiSoundManager()) {
|
|
|
|
|
ui->setVolumeScale(settingsPanel_.pendingUiVolume / 100.0f);
|
|
|
|
|
}
|
|
|
|
|
if (auto* combat = activeRenderer->getCombatSoundManager()) {
|
|
|
|
|
combat->setVolumeScale(settingsPanel_.pendingCombatVolume / 100.0f);
|
|
|
|
|
}
|
|
|
|
|
if (auto* spell = activeRenderer->getSpellSoundManager()) {
|
|
|
|
|
spell->setVolumeScale(settingsPanel_.pendingSpellVolume / 100.0f);
|
|
|
|
|
}
|
|
|
|
|
if (auto* movement = activeRenderer->getMovementSoundManager()) {
|
|
|
|
|
movement->setVolumeScale(settingsPanel_.pendingMovementVolume / 100.0f);
|
|
|
|
|
}
|
|
|
|
|
if (auto* footstep = activeRenderer->getFootstepManager()) {
|
|
|
|
|
footstep->setVolumeScale(settingsPanel_.pendingFootstepVolume / 100.0f);
|
|
|
|
|
}
|
|
|
|
|
if (auto* npcVoice = activeRenderer->getNpcVoiceManager()) {
|
|
|
|
|
npcVoice->setVolumeScale(settingsPanel_.pendingNpcVoiceVolume / 100.0f);
|
|
|
|
|
}
|
|
|
|
|
if (auto* mount = activeRenderer->getMountSoundManager()) {
|
|
|
|
|
mount->setVolumeScale(settingsPanel_.pendingMountVolume / 100.0f);
|
|
|
|
|
}
|
|
|
|
|
if (auto* activity = activeRenderer->getActivitySoundManager()) {
|
|
|
|
|
activity->setVolumeScale(settingsPanel_.pendingActivityVolume / 100.0f);
|
|
|
|
|
}
|
|
|
|
|
};
|
feat: parse SMSG_GMTICKET_GETTICKET/SYSTEMSTATUS and SMSG_SPELLINSTAKILLLOG
Previously SMSG_GMTICKET_GETTICKET and SMSG_GMTICKET_SYSTEMSTATUS were
silently consumed. Now both are fully parsed:
- SMSG_GMTICKET_GETTICKET decodes all four status codes (no ticket,
open ticket, closed, suspended), extracts ticket text, age and
server-estimated wait time, and stores them on GameHandler.
- SMSG_GMTICKET_SYSTEMSTATUS shows a chat message when GM support
goes offline/online.
- Added requestGmTicket() (sends CMSG_GMTICKET_GETTICKET) called
automatically when the GM Ticket UI window is opened, so the player
sees their existing open ticket text and wait time on first open.
- GM Ticket UI window now shows current-ticket status bar, estimated
wait time, and hides the Delete button when no ticket is active.
Also implements SMSG_SPELLINSTAKILLLOG (previously silently consumed):
parses caster/victim/spellId for all expansions and emits combat text
when the local player is involved in an instant-kill spell event (e.g.
Execute, Obliterate).
2026-03-12 22:14:46 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Zone name label above the minimap (centered, WoW-style)
|
|
|
|
|
// Prefer the server-reported zone/area name (from SMSG_INIT_WORLD_STATES) so sub-zones
|
|
|
|
|
// like Ironforge or Wailing Caverns display correctly; fall back to renderer zone name.
|
|
|
|
|
{
|
|
|
|
|
std::string wsZoneName;
|
|
|
|
|
uint32_t wsZoneId = gameHandler.getWorldStateZoneId();
|
|
|
|
|
if (wsZoneId != 0)
|
|
|
|
|
wsZoneName = gameHandler.getWhoAreaName(wsZoneId);
|
|
|
|
|
const std::string& rendererZoneName = renderer ? renderer->getCurrentZoneName() : std::string{};
|
|
|
|
|
const std::string& zoneName = !wsZoneName.empty() ? wsZoneName : rendererZoneName;
|
|
|
|
|
if (!zoneName.empty()) {
|
|
|
|
|
auto* fgDl = ImGui::GetForegroundDrawList();
|
|
|
|
|
float zoneTextY = centerY - mapRadius - 16.0f;
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
2026-03-12 02:31:12 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Weather icon appended to zone name when active
|
|
|
|
|
uint32_t wType = gameHandler.getWeatherType();
|
|
|
|
|
float wIntensity = gameHandler.getWeatherIntensity();
|
|
|
|
|
const char* weatherIcon = nullptr;
|
|
|
|
|
ImU32 weatherColor = IM_COL32(255, 255, 255, 200);
|
|
|
|
|
if (wType == 1 && wIntensity > 0.05f) { // Rain
|
|
|
|
|
weatherIcon = " \xe2\x9b\x86"; // U+26C6 ⛆
|
|
|
|
|
weatherColor = IM_COL32(140, 180, 240, 220);
|
|
|
|
|
} else if (wType == 2 && wIntensity > 0.05f) { // Snow
|
|
|
|
|
weatherIcon = " \xe2\x9d\x84"; // U+2744 ❄
|
|
|
|
|
weatherColor = IM_COL32(210, 230, 255, 220);
|
|
|
|
|
} else if (wType == 3 && wIntensity > 0.05f) { // Storm/Fog
|
|
|
|
|
weatherIcon = " \xe2\x98\x81"; // U+2601 ☁
|
|
|
|
|
weatherColor = IM_COL32(160, 160, 190, 220);
|
|
|
|
|
}
|
2026-03-12 02:31:12 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
std::string displayName = zoneName;
|
|
|
|
|
// Build combined string if weather active
|
|
|
|
|
std::string fullLabel = weatherIcon ? (zoneName + weatherIcon) : zoneName;
|
|
|
|
|
ImVec2 tsz = font->CalcTextSizeA(12.0f, FLT_MAX, 0.0f, fullLabel.c_str());
|
|
|
|
|
float tzx = centerX - tsz.x * 0.5f;
|
2026-03-12 02:31:12 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Shadow pass
|
|
|
|
|
fgDl->AddText(font, 12.0f, ImVec2(tzx + 1.0f, zoneTextY + 1.0f),
|
|
|
|
|
IM_COL32(0, 0, 0, 180), zoneName.c_str());
|
|
|
|
|
// Zone name in gold
|
|
|
|
|
fgDl->AddText(font, 12.0f, ImVec2(tzx, zoneTextY),
|
|
|
|
|
IM_COL32(255, 220, 120, 230), zoneName.c_str());
|
|
|
|
|
// Weather symbol in its own color appended after
|
|
|
|
|
if (weatherIcon) {
|
|
|
|
|
ImVec2 nameSz = font->CalcTextSizeA(12.0f, FLT_MAX, 0.0f, zoneName.c_str());
|
|
|
|
|
fgDl->AddText(font, 12.0f, ImVec2(tzx + nameSz.x, zoneTextY), weatherColor, weatherIcon);
|
|
|
|
|
}
|
|
|
|
|
}
|
feat: parse SMSG_GMTICKET_GETTICKET/SYSTEMSTATUS and SMSG_SPELLINSTAKILLLOG
Previously SMSG_GMTICKET_GETTICKET and SMSG_GMTICKET_SYSTEMSTATUS were
silently consumed. Now both are fully parsed:
- SMSG_GMTICKET_GETTICKET decodes all four status codes (no ticket,
open ticket, closed, suspended), extracts ticket text, age and
server-estimated wait time, and stores them on GameHandler.
- SMSG_GMTICKET_SYSTEMSTATUS shows a chat message when GM support
goes offline/online.
- Added requestGmTicket() (sends CMSG_GMTICKET_GETTICKET) called
automatically when the GM Ticket UI window is opened, so the player
sees their existing open ticket text and wait time on first open.
- GM Ticket UI window now shows current-ticket status bar, estimated
wait time, and hides the Delete button when no ticket is active.
Also implements SMSG_SPELLINSTAKILLLOG (previously silently consumed):
parses caster/victim/spellId for all expansions and emits combat text
when the local player is involved in an instant-kill spell event (e.g.
Execute, Obliterate).
2026-03-12 22:14:46 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Speaker mute button at the minimap top-right corner
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(centerX + mapRadius - 26.0f, centerY - mapRadius + 4.0f), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(22.0f, 22.0f), ImGuiCond_Always);
|
|
|
|
|
ImGuiWindowFlags muteFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
|
|
|
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
|
|
|
|
ImGuiWindowFlags_NoBackground;
|
|
|
|
|
if (ImGui::Begin("##MinimapMute", nullptr, muteFlags)) {
|
|
|
|
|
ImDrawList* draw = ImGui::GetWindowDrawList();
|
|
|
|
|
ImVec2 p = ImGui::GetCursorScreenPos();
|
|
|
|
|
ImVec2 size(20.0f, 20.0f);
|
|
|
|
|
if (ImGui::InvisibleButton("##MinimapMuteButton", size)) {
|
|
|
|
|
settingsPanel_.soundMuted_ = !settingsPanel_.soundMuted_;
|
|
|
|
|
if (settingsPanel_.soundMuted_) {
|
|
|
|
|
settingsPanel_.preMuteVolume_ = audio::AudioEngine::instance().getMasterVolume();
|
|
|
|
|
}
|
|
|
|
|
applyMuteState();
|
|
|
|
|
saveSettings();
|
feat: parse SMSG_GMTICKET_GETTICKET/SYSTEMSTATUS and SMSG_SPELLINSTAKILLLOG
Previously SMSG_GMTICKET_GETTICKET and SMSG_GMTICKET_SYSTEMSTATUS were
silently consumed. Now both are fully parsed:
- SMSG_GMTICKET_GETTICKET decodes all four status codes (no ticket,
open ticket, closed, suspended), extracts ticket text, age and
server-estimated wait time, and stores them on GameHandler.
- SMSG_GMTICKET_SYSTEMSTATUS shows a chat message when GM support
goes offline/online.
- Added requestGmTicket() (sends CMSG_GMTICKET_GETTICKET) called
automatically when the GM Ticket UI window is opened, so the player
sees their existing open ticket text and wait time on first open.
- GM Ticket UI window now shows current-ticket status bar, estimated
wait time, and hides the Delete button when no ticket is active.
Also implements SMSG_SPELLINSTAKILLLOG (previously silently consumed):
parses caster/victim/spellId for all expansions and emits combat text
when the local player is involved in an instant-kill spell event (e.g.
Execute, Obliterate).
2026-03-12 22:14:46 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
bool hovered = ImGui::IsItemHovered();
|
|
|
|
|
ImU32 bg = settingsPanel_.soundMuted_ ? IM_COL32(135, 42, 42, 230) : IM_COL32(38, 38, 38, 210);
|
|
|
|
|
if (hovered) bg = settingsPanel_.soundMuted_ ? IM_COL32(160, 58, 58, 230) : IM_COL32(65, 65, 65, 220);
|
|
|
|
|
ImU32 fg = IM_COL32(255, 255, 255, 245);
|
|
|
|
|
draw->AddRectFilled(p, ImVec2(p.x + size.x, p.y + size.y), bg, 4.0f);
|
|
|
|
|
draw->AddRect(ImVec2(p.x + 0.5f, p.y + 0.5f), ImVec2(p.x + size.x - 0.5f, p.y + size.y - 0.5f),
|
|
|
|
|
IM_COL32(255, 255, 255, 42), 4.0f);
|
|
|
|
|
draw->AddRectFilled(ImVec2(p.x + 4.0f, p.y + 8.0f), ImVec2(p.x + 7.0f, p.y + 12.0f), fg, 1.0f);
|
|
|
|
|
draw->AddTriangleFilled(ImVec2(p.x + 7.0f, p.y + 7.0f),
|
|
|
|
|
ImVec2(p.x + 7.0f, p.y + 13.0f),
|
|
|
|
|
ImVec2(p.x + 11.8f, p.y + 10.0f), fg);
|
|
|
|
|
if (settingsPanel_.soundMuted_) {
|
|
|
|
|
draw->AddLine(ImVec2(p.x + 13.5f, p.y + 6.2f), ImVec2(p.x + 17.2f, p.y + 13.8f), fg, 1.8f);
|
|
|
|
|
draw->AddLine(ImVec2(p.x + 17.2f, p.y + 6.2f), ImVec2(p.x + 13.5f, p.y + 13.8f), fg, 1.8f);
|
|
|
|
|
} else {
|
|
|
|
|
draw->PathArcTo(ImVec2(p.x + 11.8f, p.y + 10.0f), 3.6f, -0.7f, 0.7f, 12);
|
|
|
|
|
draw->PathStroke(fg, 0, 1.4f);
|
|
|
|
|
draw->PathArcTo(ImVec2(p.x + 11.8f, p.y + 10.0f), 5.5f, -0.7f, 0.7f, 12);
|
|
|
|
|
draw->PathStroke(fg, 0, 1.2f);
|
feat: parse SMSG_GMTICKET_GETTICKET/SYSTEMSTATUS and SMSG_SPELLINSTAKILLLOG
Previously SMSG_GMTICKET_GETTICKET and SMSG_GMTICKET_SYSTEMSTATUS were
silently consumed. Now both are fully parsed:
- SMSG_GMTICKET_GETTICKET decodes all four status codes (no ticket,
open ticket, closed, suspended), extracts ticket text, age and
server-estimated wait time, and stores them on GameHandler.
- SMSG_GMTICKET_SYSTEMSTATUS shows a chat message when GM support
goes offline/online.
- Added requestGmTicket() (sends CMSG_GMTICKET_GETTICKET) called
automatically when the GM Ticket UI window is opened, so the player
sees their existing open ticket text and wait time on first open.
- GM Ticket UI window now shows current-ticket status bar, estimated
wait time, and hides the Delete button when no ticket is active.
Also implements SMSG_SPELLINSTAKILLLOG (previously silently consumed):
parses caster/victim/spellId for all expansions and emits combat text
when the local player is involved in an instant-kill spell event (e.g.
Execute, Obliterate).
2026-03-12 22:14:46 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
if (hovered) ImGui::SetTooltip(settingsPanel_.soundMuted_ ? "Unmute" : "Mute");
|
feat: parse SMSG_GMTICKET_GETTICKET/SYSTEMSTATUS and SMSG_SPELLINSTAKILLLOG
Previously SMSG_GMTICKET_GETTICKET and SMSG_GMTICKET_SYSTEMSTATUS were
silently consumed. Now both are fully parsed:
- SMSG_GMTICKET_GETTICKET decodes all four status codes (no ticket,
open ticket, closed, suspended), extracts ticket text, age and
server-estimated wait time, and stores them on GameHandler.
- SMSG_GMTICKET_SYSTEMSTATUS shows a chat message when GM support
goes offline/online.
- Added requestGmTicket() (sends CMSG_GMTICKET_GETTICKET) called
automatically when the GM Ticket UI window is opened, so the player
sees their existing open ticket text and wait time on first open.
- GM Ticket UI window now shows current-ticket status bar, estimated
wait time, and hides the Delete button when no ticket is active.
Also implements SMSG_SPELLINSTAKILLLOG (previously silently consumed):
parses caster/victim/spellId for all expansions and emits combat text
when the local player is involved in an instant-kill spell event (e.g.
Execute, Obliterate).
2026-03-12 22:14:46 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::End();
|
feat: parse SMSG_GMTICKET_GETTICKET/SYSTEMSTATUS and SMSG_SPELLINSTAKILLLOG
Previously SMSG_GMTICKET_GETTICKET and SMSG_GMTICKET_SYSTEMSTATUS were
silently consumed. Now both are fully parsed:
- SMSG_GMTICKET_GETTICKET decodes all four status codes (no ticket,
open ticket, closed, suspended), extracts ticket text, age and
server-estimated wait time, and stores them on GameHandler.
- SMSG_GMTICKET_SYSTEMSTATUS shows a chat message when GM support
goes offline/online.
- Added requestGmTicket() (sends CMSG_GMTICKET_GETTICKET) called
automatically when the GM Ticket UI window is opened, so the player
sees their existing open ticket text and wait time on first open.
- GM Ticket UI window now shows current-ticket status bar, estimated
wait time, and hides the Delete button when no ticket is active.
Also implements SMSG_SPELLINSTAKILLLOG (previously silently consumed):
parses caster/victim/spellId for all expansions and emits combat text
when the local player is involved in an instant-kill spell event (e.g.
Execute, Obliterate).
2026-03-12 22:14:46 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Friends button at top-left of minimap
|
|
|
|
|
{
|
|
|
|
|
const auto& contacts = gameHandler.getContacts();
|
|
|
|
|
int onlineCount = 0;
|
|
|
|
|
for (const auto& c : contacts)
|
|
|
|
|
if (c.isFriend() && c.isOnline()) ++onlineCount;
|
2026-03-12 02:31:12 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::SetNextWindowPos(ImVec2(centerX - mapRadius + 4.0f, centerY - mapRadius + 4.0f), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(22.0f, 22.0f), ImGuiCond_Always);
|
|
|
|
|
ImGuiWindowFlags friendsBtnFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
|
|
|
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
|
|
|
|
ImGuiWindowFlags_NoBackground;
|
|
|
|
|
if (ImGui::Begin("##MinimapFriendsBtn", nullptr, friendsBtnFlags)) {
|
|
|
|
|
ImDrawList* draw = ImGui::GetWindowDrawList();
|
|
|
|
|
ImVec2 p = ImGui::GetCursorScreenPos();
|
|
|
|
|
ImVec2 sz(20.0f, 20.0f);
|
|
|
|
|
if (ImGui::InvisibleButton("##FriendsBtnInv", sz)) {
|
|
|
|
|
socialPanel_.showSocialFrame_ = !socialPanel_.showSocialFrame_;
|
|
|
|
|
}
|
|
|
|
|
bool hovered = ImGui::IsItemHovered();
|
|
|
|
|
ImU32 bg = socialPanel_.showSocialFrame_
|
|
|
|
|
? IM_COL32(42, 100, 42, 230)
|
|
|
|
|
: IM_COL32(38, 38, 38, 210);
|
|
|
|
|
if (hovered) bg = socialPanel_.showSocialFrame_ ? IM_COL32(58, 130, 58, 230) : IM_COL32(65, 65, 65, 220);
|
|
|
|
|
draw->AddRectFilled(p, ImVec2(p.x + sz.x, p.y + sz.y), bg, 4.0f);
|
|
|
|
|
draw->AddRect(ImVec2(p.x + 0.5f, p.y + 0.5f),
|
|
|
|
|
ImVec2(p.x + sz.x - 0.5f, p.y + sz.y - 0.5f),
|
|
|
|
|
IM_COL32(255, 255, 255, 42), 4.0f);
|
|
|
|
|
// Simple smiley-face dots as "social" icon
|
|
|
|
|
ImU32 fg = IM_COL32(255, 255, 255, 245);
|
|
|
|
|
draw->AddCircle(ImVec2(p.x + 10.0f, p.y + 10.0f), 6.5f, fg, 16, 1.2f);
|
|
|
|
|
draw->AddCircleFilled(ImVec2(p.x + 7.5f, p.y + 8.0f), 1.2f, fg);
|
|
|
|
|
draw->AddCircleFilled(ImVec2(p.x + 12.5f, p.y + 8.0f), 1.2f, fg);
|
|
|
|
|
draw->PathArcTo(ImVec2(p.x + 10.0f, p.y + 11.5f), 3.0f, 0.2f, 2.9f, 8);
|
|
|
|
|
draw->PathStroke(fg, 0, 1.2f);
|
|
|
|
|
// Small green dot if friends online
|
|
|
|
|
if (onlineCount > 0) {
|
|
|
|
|
draw->AddCircleFilled(ImVec2(p.x + sz.x - 3.5f, p.y + 3.5f),
|
|
|
|
|
3.5f, IM_COL32(50, 220, 50, 255));
|
|
|
|
|
}
|
|
|
|
|
if (hovered) {
|
|
|
|
|
if (onlineCount > 0)
|
|
|
|
|
ImGui::SetTooltip("Friends (%d online)", onlineCount);
|
|
|
|
|
else
|
|
|
|
|
ImGui::SetTooltip("Friends");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
2026-03-12 02:31:12 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Zoom buttons at the bottom edge of the minimap
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(centerX - 22, centerY + mapRadius - 30), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(44, 24), ImGuiCond_Always);
|
|
|
|
|
ImGuiWindowFlags zoomFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
|
|
|
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
|
|
|
|
ImGuiWindowFlags_NoBackground;
|
|
|
|
|
if (ImGui::Begin("##MinimapZoom", nullptr, zoomFlags)) {
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2));
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2, 0));
|
|
|
|
|
if (ImGui::SmallButton("-")) {
|
|
|
|
|
if (minimap) minimap->zoomOut();
|
feat: parse SMSG_GMTICKET_GETTICKET/SYSTEMSTATUS and SMSG_SPELLINSTAKILLLOG
Previously SMSG_GMTICKET_GETTICKET and SMSG_GMTICKET_SYSTEMSTATUS were
silently consumed. Now both are fully parsed:
- SMSG_GMTICKET_GETTICKET decodes all four status codes (no ticket,
open ticket, closed, suspended), extracts ticket text, age and
server-estimated wait time, and stores them on GameHandler.
- SMSG_GMTICKET_SYSTEMSTATUS shows a chat message when GM support
goes offline/online.
- Added requestGmTicket() (sends CMSG_GMTICKET_GETTICKET) called
automatically when the GM Ticket UI window is opened, so the player
sees their existing open ticket text and wait time on first open.
- GM Ticket UI window now shows current-ticket status bar, estimated
wait time, and hides the Delete button when no ticket is active.
Also implements SMSG_SPELLINSTAKILLLOG (previously silently consumed):
parses caster/victim/spellId for all expansions and emits combat text
when the local player is involved in an instant-kill spell event (e.g.
Execute, Obliterate).
2026-03-12 22:14:46 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::SameLine();
|
|
|
|
|
if (ImGui::SmallButton("+")) {
|
|
|
|
|
if (minimap) minimap->zoomIn();
|
|
|
|
|
}
|
|
|
|
|
ImGui::PopStyleVar(2);
|
2026-03-12 02:31:12 -07:00
|
|
|
}
|
|
|
|
|
ImGui::End();
|
2026-03-12 02:59:09 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Clock display at bottom-right of minimap (local time)
|
|
|
|
|
{
|
|
|
|
|
auto now = std::chrono::system_clock::now();
|
|
|
|
|
auto tt = std::chrono::system_clock::to_time_t(now);
|
|
|
|
|
std::tm tmBuf{};
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
localtime_s(&tmBuf, &tt);
|
|
|
|
|
#else
|
|
|
|
|
localtime_r(&tt, &tmBuf);
|
|
|
|
|
#endif
|
|
|
|
|
char clockText[16];
|
|
|
|
|
std::snprintf(clockText, sizeof(clockText), "%d:%02d %s",
|
|
|
|
|
(tmBuf.tm_hour % 12 == 0) ? 12 : tmBuf.tm_hour % 12,
|
|
|
|
|
tmBuf.tm_min,
|
|
|
|
|
tmBuf.tm_hour >= 12 ? "PM" : "AM");
|
|
|
|
|
ImVec2 clockSz = ImGui::CalcTextSize(clockText);
|
|
|
|
|
float clockW = clockSz.x + 10.0f;
|
|
|
|
|
float clockH = clockSz.y + 6.0f;
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(centerX + mapRadius - clockW - 2.0f,
|
|
|
|
|
centerY + mapRadius - clockH - 2.0f), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(clockW, clockH), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowBgAlpha(0.45f);
|
|
|
|
|
ImGuiWindowFlags clockFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
|
|
|
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
|
|
|
|
ImGuiWindowFlags_NoInputs;
|
|
|
|
|
if (ImGui::Begin("##MinimapClock", nullptr, clockFlags)) {
|
|
|
|
|
ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.8f, 0.85f), "%s", clockText);
|
|
|
|
|
}
|
2026-03-12 02:59:09 -07:00
|
|
|
ImGui::End();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Indicators below the minimap (stacked: new mail, then BG queue, then latency)
|
|
|
|
|
float indicatorX = centerX - mapRadius;
|
|
|
|
|
float nextIndicatorY = centerY + mapRadius + 4.0f;
|
|
|
|
|
const float indicatorW = mapRadius * 2.0f;
|
|
|
|
|
constexpr float kIndicatorH = 22.0f;
|
|
|
|
|
ImGuiWindowFlags indicatorFlags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
|
|
|
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
|
|
|
|
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoInputs;
|
|
|
|
|
|
|
|
|
|
// "New Mail" indicator
|
|
|
|
|
if (gameHandler.hasNewMail()) {
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(indicatorX, nextIndicatorY), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(indicatorW, kIndicatorH), ImGuiCond_Always);
|
|
|
|
|
if (ImGui::Begin("##NewMailIndicator", nullptr, indicatorFlags)) {
|
|
|
|
|
float pulse = 0.7f + 0.3f * std::sin(static_cast<float>(ImGui::GetTime()) * 3.0f);
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, pulse), "New Mail!");
|
|
|
|
|
}
|
2026-03-12 02:59:09 -07:00
|
|
|
ImGui::End();
|
2026-03-31 19:49:52 +03:00
|
|
|
nextIndicatorY += kIndicatorH;
|
2026-03-12 02:59:09 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Unspent talent points indicator
|
2026-03-12 07:32:28 -07:00
|
|
|
{
|
2026-03-31 19:49:52 +03:00
|
|
|
uint8_t unspent = gameHandler.getUnspentTalentPoints();
|
|
|
|
|
if (unspent > 0) {
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(indicatorX, nextIndicatorY), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(indicatorW, kIndicatorH), ImGuiCond_Always);
|
|
|
|
|
if (ImGui::Begin("##TalentIndicator", nullptr, indicatorFlags)) {
|
|
|
|
|
float pulse = 0.7f + 0.3f * std::sin(static_cast<float>(ImGui::GetTime()) * 2.5f);
|
|
|
|
|
char talentBuf[40];
|
|
|
|
|
snprintf(talentBuf, sizeof(talentBuf), "! %u Talent Point%s Available",
|
|
|
|
|
static_cast<unsigned>(unspent), unspent == 1 ? "" : "s");
|
|
|
|
|
ImGui::TextColored(ImVec4(0.3f, 1.0f, 0.3f * pulse, pulse), "%s", talentBuf);
|
2026-03-12 07:32:28 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::End();
|
|
|
|
|
nextIndicatorY += kIndicatorH;
|
2026-03-12 07:32:28 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// BG queue status indicator (when in queue but not yet invited)
|
|
|
|
|
for (const auto& slot : gameHandler.getBgQueues()) {
|
|
|
|
|
if (slot.statusId != 1) continue; // STATUS_WAIT_QUEUE only
|
2026-03-12 02:59:09 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
std::string bgName;
|
|
|
|
|
if (slot.arenaType > 0) {
|
|
|
|
|
bgName = std::to_string(slot.arenaType) + "v" + std::to_string(slot.arenaType) + " Arena";
|
|
|
|
|
} else {
|
|
|
|
|
switch (slot.bgTypeId) {
|
|
|
|
|
case 1: bgName = "AV"; break;
|
|
|
|
|
case 2: bgName = "WSG"; break;
|
|
|
|
|
case 3: bgName = "AB"; break;
|
|
|
|
|
case 7: bgName = "EotS"; break;
|
|
|
|
|
case 9: bgName = "SotA"; break;
|
|
|
|
|
case 11: bgName = "IoC"; break;
|
|
|
|
|
default: bgName = "BG"; break;
|
2026-03-12 02:59:09 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 12:02:59 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::SetNextWindowPos(ImVec2(indicatorX, nextIndicatorY), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(indicatorW, kIndicatorH), ImGuiCond_Always);
|
|
|
|
|
if (ImGui::Begin("##BgQueueIndicator", nullptr, indicatorFlags)) {
|
|
|
|
|
float pulse = 0.6f + 0.4f * std::sin(static_cast<float>(ImGui::GetTime()) * 1.5f);
|
|
|
|
|
if (slot.avgWaitTimeSec > 0) {
|
|
|
|
|
int avgMin = static_cast<int>(slot.avgWaitTimeSec) / 60;
|
|
|
|
|
int avgSec = static_cast<int>(slot.avgWaitTimeSec) % 60;
|
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, pulse),
|
|
|
|
|
"Queue: %s (~%d:%02d)", bgName.c_str(), avgMin, avgSec);
|
|
|
|
|
} else {
|
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, pulse),
|
|
|
|
|
"In Queue: %s", bgName.c_str());
|
|
|
|
|
}
|
2026-03-12 23:46:38 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::End();
|
|
|
|
|
nextIndicatorY += kIndicatorH;
|
|
|
|
|
break; // Show at most one queue slot indicator
|
2026-03-12 23:46:38 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// LFG queue indicator — shown when Dungeon Finder queue is active (Queued or RoleCheck)
|
|
|
|
|
{
|
|
|
|
|
using LfgState = game::GameHandler::LfgState;
|
|
|
|
|
LfgState lfgSt = gameHandler.getLfgState();
|
|
|
|
|
if (lfgSt == LfgState::Queued || lfgSt == LfgState::RoleCheck) {
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(indicatorX, nextIndicatorY), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(indicatorW, kIndicatorH), ImGuiCond_Always);
|
|
|
|
|
if (ImGui::Begin("##LfgQueueIndicator", nullptr, indicatorFlags)) {
|
|
|
|
|
if (lfgSt == LfgState::RoleCheck) {
|
|
|
|
|
float pulse = 0.6f + 0.4f * std::sin(static_cast<float>(ImGui::GetTime()) * 3.0f);
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, pulse), "LFG: Role Check...");
|
|
|
|
|
} else {
|
|
|
|
|
uint32_t qMs = gameHandler.getLfgTimeInQueueMs();
|
|
|
|
|
int qMin = static_cast<int>(qMs / 60000);
|
|
|
|
|
int qSec = static_cast<int>((qMs % 60000) / 1000);
|
|
|
|
|
float pulse = 0.6f + 0.4f * std::sin(static_cast<float>(ImGui::GetTime()) * 1.2f);
|
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 1.0f, 0.4f, pulse),
|
|
|
|
|
"LFG: %d:%02d", qMin, qSec);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
nextIndicatorY += kIndicatorH;
|
2026-03-12 23:46:38 -07:00
|
|
|
}
|
2026-03-12 12:02:59 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Calendar pending invites indicator (WotLK only)
|
|
|
|
|
{
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
auto* expReg = services_.expansionRegistry;
|
2026-03-31 19:49:52 +03:00
|
|
|
bool isWotLK = expReg && expReg->getActive() && expReg->getActive()->id == "wotlk";
|
|
|
|
|
if (isWotLK) {
|
|
|
|
|
uint32_t calPending = gameHandler.getCalendarPendingInvites();
|
|
|
|
|
if (calPending > 0) {
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(indicatorX, nextIndicatorY), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(indicatorW, kIndicatorH), ImGuiCond_Always);
|
|
|
|
|
if (ImGui::Begin("##CalendarIndicator", nullptr, indicatorFlags)) {
|
|
|
|
|
float pulse = 0.7f + 0.3f * std::sin(static_cast<float>(ImGui::GetTime()) * 2.0f);
|
|
|
|
|
char calBuf[48];
|
|
|
|
|
snprintf(calBuf, sizeof(calBuf), "Calendar: %u Invite%s",
|
|
|
|
|
calPending, calPending == 1 ? "" : "s");
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 0.5f, 1.0f, pulse), "%s", calBuf);
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
nextIndicatorY += kIndicatorH;
|
|
|
|
|
}
|
2026-03-12 12:02:59 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Taxi flight indicator — shown while on a flight path
|
|
|
|
|
if (gameHandler.isOnTaxiFlight()) {
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(indicatorX, nextIndicatorY), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(indicatorW, kIndicatorH), ImGuiCond_Always);
|
|
|
|
|
if (ImGui::Begin("##TaxiIndicator", nullptr, indicatorFlags)) {
|
|
|
|
|
const std::string& dest = gameHandler.getTaxiDestName();
|
|
|
|
|
float pulse = 0.7f + 0.3f * std::sin(static_cast<float>(ImGui::GetTime()) * 1.0f);
|
|
|
|
|
if (dest.empty()) {
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 0.85f, 1.0f, pulse), "\xe2\x9c\x88 In Flight");
|
|
|
|
|
} else {
|
|
|
|
|
char buf[64];
|
|
|
|
|
snprintf(buf, sizeof(buf), "\xe2\x9c\x88 \xe2\x86\x92 %s", dest.c_str());
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 0.85f, 1.0f, pulse), "%s", buf);
|
2026-03-12 12:02:59 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::End();
|
|
|
|
|
nextIndicatorY += kIndicatorH;
|
2026-03-12 12:02:59 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Latency + FPS indicator — centered at top of screen
|
|
|
|
|
uint32_t latMs = gameHandler.getLatencyMs();
|
|
|
|
|
if (settingsPanel_.showLatencyMeter_ && gameHandler.getState() == game::WorldState::IN_WORLD) {
|
|
|
|
|
float currentFps = ImGui::GetIO().Framerate;
|
|
|
|
|
ImVec4 latColor;
|
|
|
|
|
if (latMs < 100) latColor = ImVec4(0.3f, 1.0f, 0.3f, 0.9f);
|
|
|
|
|
else if (latMs < 250) latColor = ImVec4(1.0f, 1.0f, 0.3f, 0.9f);
|
|
|
|
|
else if (latMs < 500) latColor = ImVec4(1.0f, 0.6f, 0.1f, 0.9f);
|
|
|
|
|
else latColor = ImVec4(1.0f, 0.2f, 0.2f, 0.9f);
|
2026-03-12 12:02:59 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImVec4 fpsColor;
|
|
|
|
|
if (currentFps >= 60.0f) fpsColor = ImVec4(0.3f, 1.0f, 0.3f, 0.9f);
|
|
|
|
|
else if (currentFps >= 30.0f) fpsColor = ImVec4(1.0f, 1.0f, 0.3f, 0.9f);
|
|
|
|
|
else fpsColor = ImVec4(1.0f, 0.3f, 0.3f, 0.9f);
|
feat: wire Warden funcList_ dispatchers and implement PacketHandler call
Previously initializeModule() read the 4 WardenFuncList function addresses
from emulated memory, logged them, then discarded them — funcList_ was never
populated, so tick(), generateRC4Keys(), and processCheckRequest() were
permanently no-ops even when the Unicorn emulator successfully ran the module.
Changes:
- initializeModule() now wraps each non-null emulated function address in a
std::function lambda that marshals args to/from emulated memory via
emulator_->writeData/callFunction/freeMemory
- generateRC4Keys: copies 4-byte seed to emulated space, calls function
- unload: calls function with NULL (module saves own RC4 state)
- tick: direct uint32_t(deltaMs) dispatch, returns emulated EAX
- packetHandler: 2-arg variant for generic callers
- Stores emulatedPacketHandlerAddr_ for full 4-arg call in processCheckRequest
- processCheckRequest() now calls the emulated PacketHandler with the proper
4-argument stdcall convention: (data, size, responseOut, responseSizeOut),
reads back the response size and bytes, returns them in responseOut
- unload() resets emulatedPacketHandlerAddr_ to 0 for clean re-initialization
- Remove dead no-op renderObjectiveTracker() (no call sites, superseded)
2026-03-17 21:29:09 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
char infoText[64];
|
|
|
|
|
if (latMs > 0)
|
|
|
|
|
snprintf(infoText, sizeof(infoText), "%.0f fps | %u ms", currentFps, latMs);
|
|
|
|
|
else
|
|
|
|
|
snprintf(infoText, sizeof(infoText), "%.0f fps", currentFps);
|
2026-03-12 03:39:10 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImVec2 textSize = ImGui::CalcTextSize(infoText);
|
|
|
|
|
float latW = textSize.x + 16.0f;
|
|
|
|
|
float latH = textSize.y + 8.0f;
|
|
|
|
|
ImGuiIO& lio = ImGui::GetIO();
|
|
|
|
|
float latX = (lio.DisplaySize.x - latW) * 0.5f;
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(latX, 4.0f), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(latW, latH), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowBgAlpha(0.45f);
|
|
|
|
|
if (ImGui::Begin("##LatencyIndicator", nullptr, indicatorFlags)) {
|
|
|
|
|
// Color the FPS and latency portions differently
|
|
|
|
|
ImGui::TextColored(fpsColor, "%.0f fps", currentFps);
|
|
|
|
|
if (latMs > 0) {
|
|
|
|
|
ImGui::SameLine(0, 4);
|
|
|
|
|
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 0.7f), "|");
|
|
|
|
|
ImGui::SameLine(0, 4);
|
|
|
|
|
ImGui::TextColored(latColor, "%u ms", latMs);
|
|
|
|
|
}
|
2026-03-12 18:21:50 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::End();
|
|
|
|
|
}
|
2026-03-12 18:21:50 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Low durability warning — shown when any equipped item has < 20% durability
|
|
|
|
|
if (gameHandler.getState() == game::WorldState::IN_WORLD) {
|
|
|
|
|
const auto& inv = gameHandler.getInventory();
|
|
|
|
|
float lowestDurPct = 1.0f;
|
|
|
|
|
for (int i = 0; i < game::Inventory::NUM_EQUIP_SLOTS; ++i) {
|
|
|
|
|
const auto& slot = inv.getEquipSlot(static_cast<game::EquipSlot>(i));
|
|
|
|
|
if (slot.empty()) continue;
|
|
|
|
|
const auto& it = slot.item;
|
|
|
|
|
if (it.maxDurability > 0) {
|
|
|
|
|
float pct = static_cast<float>(it.curDurability) / static_cast<float>(it.maxDurability);
|
|
|
|
|
if (pct < lowestDurPct) lowestDurPct = pct;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (lowestDurPct < 0.20f) {
|
|
|
|
|
bool critical = (lowestDurPct < 0.05f);
|
|
|
|
|
float pulse = critical
|
|
|
|
|
? (0.7f + 0.3f * std::sin(static_cast<float>(ImGui::GetTime()) * 4.0f))
|
|
|
|
|
: 1.0f;
|
|
|
|
|
ImVec4 durWarnColor = critical
|
|
|
|
|
? ImVec4(1.0f, 0.2f, 0.2f, pulse)
|
|
|
|
|
: ImVec4(1.0f, 0.65f, 0.1f, 0.9f);
|
|
|
|
|
const char* durWarnText = critical ? "Item breaking!" : "Low durability";
|
2026-03-12 18:21:50 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::SetNextWindowPos(ImVec2(indicatorX, nextIndicatorY), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(indicatorW, kIndicatorH), ImGuiCond_Always);
|
|
|
|
|
if (ImGui::Begin("##DurabilityIndicator", nullptr, indicatorFlags)) {
|
|
|
|
|
ImGui::TextColored(durWarnColor, "%s", durWarnText);
|
|
|
|
|
}
|
|
|
|
|
ImGui::End();
|
|
|
|
|
nextIndicatorY += kIndicatorH;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-12 18:21:50 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Local time clock — always visible below minimap indicators
|
|
|
|
|
{
|
|
|
|
|
auto now = std::chrono::system_clock::now();
|
|
|
|
|
std::time_t tt = std::chrono::system_clock::to_time_t(now);
|
|
|
|
|
struct tm tmBuf;
|
|
|
|
|
#ifdef _WIN32
|
|
|
|
|
localtime_s(&tmBuf, &tt);
|
|
|
|
|
#else
|
|
|
|
|
localtime_r(&tt, &tmBuf);
|
|
|
|
|
#endif
|
|
|
|
|
char clockStr[16];
|
|
|
|
|
snprintf(clockStr, sizeof(clockStr), "%02d:%02d", tmBuf.tm_hour, tmBuf.tm_min);
|
2026-03-12 18:21:50 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::SetNextWindowPos(ImVec2(indicatorX, nextIndicatorY), ImGuiCond_Always);
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(indicatorW, kIndicatorH), ImGuiCond_Always);
|
|
|
|
|
ImGuiWindowFlags clockFlags = indicatorFlags & ~ImGuiWindowFlags_NoInputs;
|
|
|
|
|
if (ImGui::Begin("##ClockIndicator", nullptr, clockFlags)) {
|
|
|
|
|
ImGui::TextColored(ImVec4(0.85f, 0.85f, 0.85f, 0.75f), "%s", clockStr);
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
char fullTime[32];
|
|
|
|
|
snprintf(fullTime, sizeof(fullTime), "%02d:%02d:%02d (local)",
|
|
|
|
|
tmBuf.tm_hour, tmBuf.tm_min, tmBuf.tm_sec);
|
|
|
|
|
ImGui::SetTooltip("%s", fullTime);
|
|
|
|
|
}
|
2026-03-12 18:21:50 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
ImGui::End();
|
2026-03-12 18:21:50 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
void GameScreen::saveSettings() {
|
|
|
|
|
std::string path = SettingsPanel::getSettingsPath();
|
|
|
|
|
std::filesystem::path dir = std::filesystem::path(path).parent_path();
|
|
|
|
|
std::error_code ec;
|
|
|
|
|
std::filesystem::create_directories(dir, ec);
|
|
|
|
|
|
|
|
|
|
std::ofstream out(path);
|
|
|
|
|
if (!out.is_open()) {
|
|
|
|
|
LOG_WARNING("Could not save settings to ", path);
|
|
|
|
|
return;
|
2026-03-12 12:32:19 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Interface
|
|
|
|
|
out << "ui_opacity=" << settingsPanel_.pendingUiOpacity << "\n";
|
|
|
|
|
out << "minimap_rotate=" << (settingsPanel_.pendingMinimapRotate ? 1 : 0) << "\n";
|
|
|
|
|
out << "minimap_square=" << (settingsPanel_.pendingMinimapSquare ? 1 : 0) << "\n";
|
|
|
|
|
out << "minimap_npc_dots=" << (settingsPanel_.pendingMinimapNpcDots ? 1 : 0) << "\n";
|
|
|
|
|
out << "show_latency_meter=" << (settingsPanel_.pendingShowLatencyMeter ? 1 : 0) << "\n";
|
|
|
|
|
out << "show_dps_meter=" << (settingsPanel_.showDPSMeter_ ? 1 : 0) << "\n";
|
|
|
|
|
out << "show_cooldown_tracker=" << (settingsPanel_.showCooldownTracker_ ? 1 : 0) << "\n";
|
|
|
|
|
out << "separate_bags=" << (settingsPanel_.pendingSeparateBags ? 1 : 0) << "\n";
|
|
|
|
|
out << "show_keyring=" << (settingsPanel_.pendingShowKeyring ? 1 : 0) << "\n";
|
|
|
|
|
out << "action_bar_scale=" << settingsPanel_.pendingActionBarScale << "\n";
|
|
|
|
|
out << "nameplate_scale=" << settingsPanel_.nameplateScale_ << "\n";
|
|
|
|
|
out << "show_friendly_nameplates=" << (settingsPanel_.showFriendlyNameplates_ ? 1 : 0) << "\n";
|
|
|
|
|
out << "show_action_bar2=" << (settingsPanel_.pendingShowActionBar2 ? 1 : 0) << "\n";
|
|
|
|
|
out << "action_bar2_offset_x=" << settingsPanel_.pendingActionBar2OffsetX << "\n";
|
|
|
|
|
out << "action_bar2_offset_y=" << settingsPanel_.pendingActionBar2OffsetY << "\n";
|
|
|
|
|
out << "show_right_bar=" << (settingsPanel_.pendingShowRightBar ? 1 : 0) << "\n";
|
|
|
|
|
out << "show_left_bar=" << (settingsPanel_.pendingShowLeftBar ? 1 : 0) << "\n";
|
|
|
|
|
out << "right_bar_offset_y=" << settingsPanel_.pendingRightBarOffsetY << "\n";
|
|
|
|
|
out << "left_bar_offset_y=" << settingsPanel_.pendingLeftBarOffsetY << "\n";
|
|
|
|
|
out << "damage_flash=" << (settingsPanel_.damageFlashEnabled_ ? 1 : 0) << "\n";
|
|
|
|
|
out << "low_health_vignette=" << (settingsPanel_.lowHealthVignetteEnabled_ ? 1 : 0) << "\n";
|
2026-03-12 02:52:40 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Audio
|
|
|
|
|
out << "sound_muted=" << (settingsPanel_.soundMuted_ ? 1 : 0) << "\n";
|
|
|
|
|
out << "use_original_soundtrack=" << (settingsPanel_.pendingUseOriginalSoundtrack ? 1 : 0) << "\n";
|
|
|
|
|
out << "master_volume=" << settingsPanel_.pendingMasterVolume << "\n";
|
|
|
|
|
out << "music_volume=" << settingsPanel_.pendingMusicVolume << "\n";
|
|
|
|
|
out << "ambient_volume=" << settingsPanel_.pendingAmbientVolume << "\n";
|
|
|
|
|
out << "ui_volume=" << settingsPanel_.pendingUiVolume << "\n";
|
|
|
|
|
out << "combat_volume=" << settingsPanel_.pendingCombatVolume << "\n";
|
|
|
|
|
out << "spell_volume=" << settingsPanel_.pendingSpellVolume << "\n";
|
|
|
|
|
out << "movement_volume=" << settingsPanel_.pendingMovementVolume << "\n";
|
|
|
|
|
out << "footstep_volume=" << settingsPanel_.pendingFootstepVolume << "\n";
|
|
|
|
|
out << "npc_voice_volume=" << settingsPanel_.pendingNpcVoiceVolume << "\n";
|
|
|
|
|
out << "mount_volume=" << settingsPanel_.pendingMountVolume << "\n";
|
|
|
|
|
out << "activity_volume=" << settingsPanel_.pendingActivityVolume << "\n";
|
2026-03-12 02:52:40 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Gameplay
|
|
|
|
|
out << "auto_loot=" << (settingsPanel_.pendingAutoLoot ? 1 : 0) << "\n";
|
|
|
|
|
out << "auto_sell_grey=" << (settingsPanel_.pendingAutoSellGrey ? 1 : 0) << "\n";
|
|
|
|
|
out << "auto_repair=" << (settingsPanel_.pendingAutoRepair ? 1 : 0) << "\n";
|
|
|
|
|
out << "graphics_preset=" << static_cast<int>(settingsPanel_.currentGraphicsPreset) << "\n";
|
|
|
|
|
out << "ground_clutter_density=" << settingsPanel_.pendingGroundClutterDensity << "\n";
|
|
|
|
|
out << "shadows=" << (settingsPanel_.pendingShadows ? 1 : 0) << "\n";
|
|
|
|
|
out << "shadow_distance=" << settingsPanel_.pendingShadowDistance << "\n";
|
|
|
|
|
out << "brightness=" << settingsPanel_.pendingBrightness << "\n";
|
|
|
|
|
out << "water_refraction=" << (settingsPanel_.pendingWaterRefraction ? 1 : 0) << "\n";
|
|
|
|
|
out << "antialiasing=" << settingsPanel_.pendingAntiAliasing << "\n";
|
|
|
|
|
out << "fxaa=" << (settingsPanel_.pendingFXAA ? 1 : 0) << "\n";
|
|
|
|
|
out << "normal_mapping=" << (settingsPanel_.pendingNormalMapping ? 1 : 0) << "\n";
|
|
|
|
|
out << "normal_map_strength=" << settingsPanel_.pendingNormalMapStrength << "\n";
|
|
|
|
|
out << "pom=" << (settingsPanel_.pendingPOM ? 1 : 0) << "\n";
|
|
|
|
|
out << "pom_quality=" << settingsPanel_.pendingPOMQuality << "\n";
|
|
|
|
|
out << "upscaling_mode=" << settingsPanel_.pendingUpscalingMode << "\n";
|
|
|
|
|
out << "fsr=" << (settingsPanel_.pendingFSR ? 1 : 0) << "\n";
|
|
|
|
|
out << "fsr_quality=" << settingsPanel_.pendingFSRQuality << "\n";
|
|
|
|
|
out << "fsr_sharpness=" << settingsPanel_.pendingFSRSharpness << "\n";
|
|
|
|
|
out << "fsr2_jitter_sign=" << settingsPanel_.pendingFSR2JitterSign << "\n";
|
|
|
|
|
out << "fsr2_mv_scale_x=" << settingsPanel_.pendingFSR2MotionVecScaleX << "\n";
|
|
|
|
|
out << "fsr2_mv_scale_y=" << settingsPanel_.pendingFSR2MotionVecScaleY << "\n";
|
|
|
|
|
out << "amd_fsr3_framegen=" << (settingsPanel_.pendingAMDFramegen ? 1 : 0) << "\n";
|
2026-03-12 02:52:40 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Controls
|
|
|
|
|
out << "mouse_sensitivity=" << settingsPanel_.pendingMouseSensitivity << "\n";
|
|
|
|
|
out << "invert_mouse=" << (settingsPanel_.pendingInvertMouse ? 1 : 0) << "\n";
|
|
|
|
|
out << "extended_zoom=" << (settingsPanel_.pendingExtendedZoom ? 1 : 0) << "\n";
|
|
|
|
|
out << "camera_stiffness=" << settingsPanel_.pendingCameraStiffness << "\n";
|
|
|
|
|
out << "camera_pivot_height=" << settingsPanel_.pendingPivotHeight << "\n";
|
|
|
|
|
out << "fov=" << settingsPanel_.pendingFov << "\n";
|
2026-03-12 02:52:40 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Quest tracker position/size
|
|
|
|
|
out << "quest_tracker_right_offset=" << questTrackerRightOffset_ << "\n";
|
|
|
|
|
out << "quest_tracker_y=" << questTrackerPos_.y << "\n";
|
|
|
|
|
out << "quest_tracker_w=" << questTrackerSize_.x << "\n";
|
|
|
|
|
out << "quest_tracker_h=" << questTrackerSize_.y << "\n";
|
2026-03-12 02:52:40 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Chat
|
|
|
|
|
out << "chat_active_tab=" << chatPanel_.activeChatTab << "\n";
|
|
|
|
|
out << "chat_timestamps=" << (chatPanel_.chatShowTimestamps ? 1 : 0) << "\n";
|
|
|
|
|
out << "chat_font_size=" << chatPanel_.chatFontSize << "\n";
|
|
|
|
|
out << "chat_autojoin_general=" << (chatPanel_.chatAutoJoinGeneral ? 1 : 0) << "\n";
|
|
|
|
|
out << "chat_autojoin_trade=" << (chatPanel_.chatAutoJoinTrade ? 1 : 0) << "\n";
|
|
|
|
|
out << "chat_autojoin_localdefense=" << (chatPanel_.chatAutoJoinLocalDefense ? 1 : 0) << "\n";
|
|
|
|
|
out << "chat_autojoin_lfg=" << (chatPanel_.chatAutoJoinLFG ? 1 : 0) << "\n";
|
|
|
|
|
out << "chat_autojoin_local=" << (chatPanel_.chatAutoJoinLocal ? 1 : 0) << "\n";
|
2026-03-12 02:52:40 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
out.close();
|
2026-03-12 02:52:40 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Save keybindings to the same config file (appends [Keybindings] section)
|
|
|
|
|
KeybindingManager::getInstance().saveToConfigFile(path);
|
2026-03-12 02:52:40 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
LOG_INFO("Settings saved to ", path);
|
|
|
|
|
}
|
2026-03-12 02:52:40 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
void GameScreen::loadSettings() {
|
|
|
|
|
std::string path = SettingsPanel::getSettingsPath();
|
|
|
|
|
std::ifstream in(path);
|
|
|
|
|
if (!in.is_open()) return;
|
2026-03-12 04:24:37 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
std::string line;
|
|
|
|
|
while (std::getline(in, line)) {
|
|
|
|
|
size_t eq = line.find('=');
|
|
|
|
|
if (eq == std::string::npos) continue;
|
|
|
|
|
std::string key = line.substr(0, eq);
|
|
|
|
|
std::string val = line.substr(eq + 1);
|
2026-03-12 04:24:37 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
try {
|
|
|
|
|
// Interface
|
|
|
|
|
if (key == "ui_opacity") {
|
|
|
|
|
int v = std::stoi(val);
|
|
|
|
|
if (v >= 20 && v <= 100) {
|
|
|
|
|
settingsPanel_.pendingUiOpacity = v;
|
|
|
|
|
settingsPanel_.uiOpacity_ = static_cast<float>(v) / 100.0f;
|
2026-03-12 07:37:29 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
} else if (key == "minimap_rotate") {
|
|
|
|
|
// Ignore persisted rotate state; keep north-up.
|
|
|
|
|
settingsPanel_.minimapRotate_ = false;
|
|
|
|
|
settingsPanel_.pendingMinimapRotate = false;
|
|
|
|
|
} else if (key == "minimap_square") {
|
|
|
|
|
int v = std::stoi(val);
|
|
|
|
|
settingsPanel_.minimapSquare_ = (v != 0);
|
|
|
|
|
settingsPanel_.pendingMinimapSquare = settingsPanel_.minimapSquare_;
|
|
|
|
|
} else if (key == "minimap_npc_dots") {
|
|
|
|
|
int v = std::stoi(val);
|
|
|
|
|
settingsPanel_.minimapNpcDots_ = (v != 0);
|
|
|
|
|
settingsPanel_.pendingMinimapNpcDots = settingsPanel_.minimapNpcDots_;
|
|
|
|
|
} else if (key == "show_latency_meter") {
|
|
|
|
|
settingsPanel_.showLatencyMeter_ = (std::stoi(val) != 0);
|
|
|
|
|
settingsPanel_.pendingShowLatencyMeter = settingsPanel_.showLatencyMeter_;
|
|
|
|
|
} else if (key == "show_dps_meter") {
|
|
|
|
|
settingsPanel_.showDPSMeter_ = (std::stoi(val) != 0);
|
|
|
|
|
} else if (key == "show_cooldown_tracker") {
|
|
|
|
|
settingsPanel_.showCooldownTracker_ = (std::stoi(val) != 0);
|
|
|
|
|
} else if (key == "separate_bags") {
|
|
|
|
|
settingsPanel_.pendingSeparateBags = (std::stoi(val) != 0);
|
|
|
|
|
inventoryScreen.setSeparateBags(settingsPanel_.pendingSeparateBags);
|
|
|
|
|
} else if (key == "show_keyring") {
|
|
|
|
|
settingsPanel_.pendingShowKeyring = (std::stoi(val) != 0);
|
|
|
|
|
inventoryScreen.setShowKeyring(settingsPanel_.pendingShowKeyring);
|
|
|
|
|
} else if (key == "action_bar_scale") {
|
|
|
|
|
settingsPanel_.pendingActionBarScale = std::clamp(std::stof(val), 0.5f, 1.5f);
|
|
|
|
|
} else if (key == "nameplate_scale") {
|
|
|
|
|
settingsPanel_.nameplateScale_ = std::clamp(std::stof(val), 0.5f, 2.0f);
|
|
|
|
|
} else if (key == "show_friendly_nameplates") {
|
|
|
|
|
settingsPanel_.showFriendlyNameplates_ = (std::stoi(val) != 0);
|
|
|
|
|
} else if (key == "show_action_bar2") {
|
|
|
|
|
settingsPanel_.pendingShowActionBar2 = (std::stoi(val) != 0);
|
|
|
|
|
} else if (key == "action_bar2_offset_x") {
|
|
|
|
|
settingsPanel_.pendingActionBar2OffsetX = std::clamp(std::stof(val), -600.0f, 600.0f);
|
|
|
|
|
} else if (key == "action_bar2_offset_y") {
|
|
|
|
|
settingsPanel_.pendingActionBar2OffsetY = std::clamp(std::stof(val), -400.0f, 400.0f);
|
|
|
|
|
} else if (key == "show_right_bar") {
|
|
|
|
|
settingsPanel_.pendingShowRightBar = (std::stoi(val) != 0);
|
|
|
|
|
} else if (key == "show_left_bar") {
|
|
|
|
|
settingsPanel_.pendingShowLeftBar = (std::stoi(val) != 0);
|
|
|
|
|
} else if (key == "right_bar_offset_y") {
|
|
|
|
|
settingsPanel_.pendingRightBarOffsetY = std::clamp(std::stof(val), -400.0f, 400.0f);
|
|
|
|
|
} else if (key == "left_bar_offset_y") {
|
|
|
|
|
settingsPanel_.pendingLeftBarOffsetY = std::clamp(std::stof(val), -400.0f, 400.0f);
|
|
|
|
|
} else if (key == "damage_flash") {
|
|
|
|
|
settingsPanel_.damageFlashEnabled_ = (std::stoi(val) != 0);
|
|
|
|
|
} else if (key == "low_health_vignette") {
|
|
|
|
|
settingsPanel_.lowHealthVignetteEnabled_ = (std::stoi(val) != 0);
|
|
|
|
|
}
|
|
|
|
|
// Audio
|
|
|
|
|
else if (key == "sound_muted") {
|
|
|
|
|
settingsPanel_.soundMuted_ = (std::stoi(val) != 0);
|
|
|
|
|
if (settingsPanel_.soundMuted_) {
|
|
|
|
|
// Apply mute on load; settingsPanel_.preMuteVolume_ will be set when AudioEngine is available
|
|
|
|
|
audio::AudioEngine::instance().setMasterVolume(0.0f);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (key == "use_original_soundtrack") settingsPanel_.pendingUseOriginalSoundtrack = (std::stoi(val) != 0);
|
|
|
|
|
else if (key == "master_volume") settingsPanel_.pendingMasterVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
else if (key == "music_volume") settingsPanel_.pendingMusicVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
else if (key == "ambient_volume") settingsPanel_.pendingAmbientVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
else if (key == "ui_volume") settingsPanel_.pendingUiVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
else if (key == "combat_volume") settingsPanel_.pendingCombatVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
else if (key == "spell_volume") settingsPanel_.pendingSpellVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
else if (key == "movement_volume") settingsPanel_.pendingMovementVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
else if (key == "footstep_volume") settingsPanel_.pendingFootstepVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
else if (key == "npc_voice_volume") settingsPanel_.pendingNpcVoiceVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
else if (key == "mount_volume") settingsPanel_.pendingMountVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
else if (key == "activity_volume") settingsPanel_.pendingActivityVolume = std::clamp(std::stoi(val), 0, 100);
|
|
|
|
|
// Gameplay
|
|
|
|
|
else if (key == "auto_loot") settingsPanel_.pendingAutoLoot = (std::stoi(val) != 0);
|
|
|
|
|
else if (key == "auto_sell_grey") settingsPanel_.pendingAutoSellGrey = (std::stoi(val) != 0);
|
|
|
|
|
else if (key == "auto_repair") settingsPanel_.pendingAutoRepair = (std::stoi(val) != 0);
|
|
|
|
|
else if (key == "graphics_preset") {
|
|
|
|
|
int presetVal = std::clamp(std::stoi(val), 0, 4);
|
|
|
|
|
settingsPanel_.currentGraphicsPreset = static_cast<SettingsPanel::GraphicsPreset>(presetVal);
|
|
|
|
|
settingsPanel_.pendingGraphicsPreset = settingsPanel_.currentGraphicsPreset;
|
|
|
|
|
}
|
|
|
|
|
else if (key == "ground_clutter_density") settingsPanel_.pendingGroundClutterDensity = std::clamp(std::stoi(val), 0, 150);
|
|
|
|
|
else if (key == "shadows") settingsPanel_.pendingShadows = (std::stoi(val) != 0);
|
|
|
|
|
else if (key == "shadow_distance") settingsPanel_.pendingShadowDistance = std::clamp(std::stof(val), 40.0f, 500.0f);
|
|
|
|
|
else if (key == "brightness") {
|
|
|
|
|
settingsPanel_.pendingBrightness = std::clamp(std::stoi(val), 0, 100);
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
if (auto* r = services_.renderer)
|
2026-03-31 19:49:52 +03:00
|
|
|
r->setBrightness(static_cast<float>(settingsPanel_.pendingBrightness) / 50.0f);
|
|
|
|
|
}
|
|
|
|
|
else if (key == "water_refraction") settingsPanel_.pendingWaterRefraction = (std::stoi(val) != 0);
|
|
|
|
|
else if (key == "antialiasing") settingsPanel_.pendingAntiAliasing = std::clamp(std::stoi(val), 0, 3);
|
|
|
|
|
else if (key == "fxaa") settingsPanel_.pendingFXAA = (std::stoi(val) != 0);
|
|
|
|
|
else if (key == "normal_mapping") settingsPanel_.pendingNormalMapping = (std::stoi(val) != 0);
|
|
|
|
|
else if (key == "normal_map_strength") settingsPanel_.pendingNormalMapStrength = std::clamp(std::stof(val), 0.0f, 2.0f);
|
|
|
|
|
else if (key == "pom") settingsPanel_.pendingPOM = (std::stoi(val) != 0);
|
|
|
|
|
else if (key == "pom_quality") settingsPanel_.pendingPOMQuality = std::clamp(std::stoi(val), 0, 2);
|
|
|
|
|
else if (key == "upscaling_mode") {
|
|
|
|
|
settingsPanel_.pendingUpscalingMode = std::clamp(std::stoi(val), 0, 2);
|
|
|
|
|
settingsPanel_.pendingFSR = (settingsPanel_.pendingUpscalingMode == 1);
|
|
|
|
|
} else if (key == "fsr") {
|
|
|
|
|
settingsPanel_.pendingFSR = (std::stoi(val) != 0);
|
|
|
|
|
// Backward compatibility: old configs only had fsr=0/1.
|
|
|
|
|
if (settingsPanel_.pendingUpscalingMode == 0 && settingsPanel_.pendingFSR) settingsPanel_.pendingUpscalingMode = 1;
|
|
|
|
|
}
|
|
|
|
|
else if (key == "fsr_quality") settingsPanel_.pendingFSRQuality = std::clamp(std::stoi(val), 0, 3);
|
|
|
|
|
else if (key == "fsr_sharpness") settingsPanel_.pendingFSRSharpness = std::clamp(std::stof(val), 0.0f, 2.0f);
|
|
|
|
|
else if (key == "fsr2_jitter_sign") settingsPanel_.pendingFSR2JitterSign = std::clamp(std::stof(val), -2.0f, 2.0f);
|
|
|
|
|
else if (key == "fsr2_mv_scale_x") settingsPanel_.pendingFSR2MotionVecScaleX = std::clamp(std::stof(val), -2.0f, 2.0f);
|
|
|
|
|
else if (key == "fsr2_mv_scale_y") settingsPanel_.pendingFSR2MotionVecScaleY = std::clamp(std::stof(val), -2.0f, 2.0f);
|
|
|
|
|
else if (key == "amd_fsr3_framegen") settingsPanel_.pendingAMDFramegen = (std::stoi(val) != 0);
|
|
|
|
|
// Controls
|
|
|
|
|
else if (key == "mouse_sensitivity") settingsPanel_.pendingMouseSensitivity = std::clamp(std::stof(val), 0.05f, 1.0f);
|
|
|
|
|
else if (key == "invert_mouse") settingsPanel_.pendingInvertMouse = (std::stoi(val) != 0);
|
|
|
|
|
else if (key == "extended_zoom") settingsPanel_.pendingExtendedZoom = (std::stoi(val) != 0);
|
|
|
|
|
else if (key == "camera_stiffness") settingsPanel_.pendingCameraStiffness = std::clamp(std::stof(val), 5.0f, 100.0f);
|
|
|
|
|
else if (key == "camera_pivot_height") settingsPanel_.pendingPivotHeight = std::clamp(std::stof(val), 0.0f, 3.0f);
|
|
|
|
|
else if (key == "fov") {
|
|
|
|
|
settingsPanel_.pendingFov = std::clamp(std::stof(val), 45.0f, 110.0f);
|
`chore(application): extract appearance controller and unify UI flow`
- Refactor UI application architecture: extracted appearance controller into ui_services.hpp + implementation updates
- Update UI components and managers to use new service layer:
- `action_bar_panel`, `auth_screen`, `character_screen`, `chat_panel`, `combat_ui`, `dialog_manager`, `game_screen`, `settings_panel`, `social_panel`, `toast_manager`, `ui_manager`, `window_manager`
- Adjust core application entrypoints:
- application.cpp
- Update component implementations for new controller flow:
- action_bar_panel.cpp, `chat_panel.cpp`, `combat_ui.cpp`, `dialog_manager.cpp`, `game_screen.cpp`, `settings_panel.cpp`, `social_panel.cpp`, `toast_manager.cpp`, `window_manager.cpp`
These staged changes implement a major architectural refactor for UI/appearance controller separation
2026-04-01 20:59:17 +03:00
|
|
|
if (auto* renderer = services_.renderer) {
|
2026-03-31 19:49:52 +03:00
|
|
|
if (auto* camera = renderer->getCamera()) camera->setFov(settingsPanel_.pendingFov);
|
2026-03-12 02:52:40 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
// Quest tracker position/size
|
|
|
|
|
else if (key == "quest_tracker_x") {
|
|
|
|
|
// Legacy: ignore absolute X (right_offset supersedes it)
|
|
|
|
|
(void)val;
|
2026-03-12 21:27:02 -07:00
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
else if (key == "quest_tracker_right_offset") {
|
|
|
|
|
questTrackerRightOffset_ = std::stof(val);
|
|
|
|
|
questTrackerPosInit_ = true;
|
|
|
|
|
}
|
|
|
|
|
else if (key == "quest_tracker_y") {
|
|
|
|
|
questTrackerPos_.y = std::stof(val);
|
|
|
|
|
questTrackerPosInit_ = true;
|
|
|
|
|
}
|
|
|
|
|
else if (key == "quest_tracker_w") {
|
|
|
|
|
questTrackerSize_.x = std::max(100.0f, std::stof(val));
|
|
|
|
|
}
|
|
|
|
|
else if (key == "quest_tracker_h") {
|
|
|
|
|
questTrackerSize_.y = std::max(60.0f, std::stof(val));
|
|
|
|
|
}
|
|
|
|
|
// Chat
|
|
|
|
|
else if (key == "chat_active_tab") chatPanel_.activeChatTab = std::clamp(std::stoi(val), 0, 3);
|
|
|
|
|
else if (key == "chat_timestamps") chatPanel_.chatShowTimestamps = (std::stoi(val) != 0);
|
|
|
|
|
else if (key == "chat_font_size") chatPanel_.chatFontSize = std::clamp(std::stoi(val), 0, 2);
|
|
|
|
|
else if (key == "chat_autojoin_general") chatPanel_.chatAutoJoinGeneral = (std::stoi(val) != 0);
|
|
|
|
|
else if (key == "chat_autojoin_trade") chatPanel_.chatAutoJoinTrade = (std::stoi(val) != 0);
|
|
|
|
|
else if (key == "chat_autojoin_localdefense") chatPanel_.chatAutoJoinLocalDefense = (std::stoi(val) != 0);
|
|
|
|
|
else if (key == "chat_autojoin_lfg") chatPanel_.chatAutoJoinLFG = (std::stoi(val) != 0);
|
|
|
|
|
else if (key == "chat_autojoin_local") chatPanel_.chatAutoJoinLocal = (std::stoi(val) != 0);
|
|
|
|
|
} catch (...) {}
|
2026-03-12 21:27:02 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Load keybindings from the same config file
|
|
|
|
|
KeybindingManager::getInstance().loadFromConfigFile(path);
|
2026-03-12 20:23:36 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
LOG_INFO("Settings loaded from ", path);
|
|
|
|
|
}
|
2026-03-12 20:23:36 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Mail Window
|
|
|
|
|
// ============================================================
|
2026-03-12 20:23:36 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Bank Window
|
|
|
|
|
// ============================================================
|
2026-03-12 20:23:36 -07:00
|
|
|
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Guild Bank Window
|
|
|
|
|
// ============================================================
|
2026-03-12 20:23:36 -07:00
|
|
|
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ============================================================
|
|
|
|
|
// Auction House Window
|
|
|
|
|
// ============================================================
|
2026-03-12 20:23:36 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Screen-space weather overlay (rain / snow / storm)
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
void GameScreen::renderWeatherOverlay(game::GameHandler& gameHandler) {
|
|
|
|
|
uint32_t wType = gameHandler.getWeatherType();
|
|
|
|
|
float intensity = gameHandler.getWeatherIntensity();
|
|
|
|
|
if (wType == 0 || intensity < 0.05f) return;
|
2026-03-12 20:23:36 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
const ImGuiIO& io = ImGui::GetIO();
|
|
|
|
|
float sw = io.DisplaySize.x;
|
|
|
|
|
float sh = io.DisplaySize.y;
|
|
|
|
|
if (sw <= 0.0f || sh <= 0.0f) return;
|
2026-03-12 20:28:03 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Seeded RNG for weather particle positions — replaces std::rand() which
|
|
|
|
|
// shares global state and has modulo bias.
|
|
|
|
|
static std::mt19937 wxRng(std::random_device{}());
|
|
|
|
|
auto wxRandInt = [](int maxExcl) {
|
|
|
|
|
return std::uniform_int_distribution<int>(0, std::max(0, maxExcl - 1))(wxRng);
|
|
|
|
|
};
|
2026-03-12 20:28:03 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
ImDrawList* dl = ImGui::GetForegroundDrawList();
|
|
|
|
|
const float dt = std::min(io.DeltaTime, 0.05f); // cap delta at 50ms to avoid teleporting particles
|
2026-03-12 20:28:03 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
if (wType == 1 || wType == 3) {
|
|
|
|
|
// ── Rain / Storm ─────────────────────────────────────────────────────
|
|
|
|
|
constexpr int MAX_DROPS = 300;
|
|
|
|
|
struct RainState {
|
|
|
|
|
float x[MAX_DROPS], y[MAX_DROPS];
|
|
|
|
|
bool initialized = false;
|
|
|
|
|
uint32_t lastType = 0;
|
|
|
|
|
float lastW = 0.0f, lastH = 0.0f;
|
|
|
|
|
};
|
|
|
|
|
static RainState rs;
|
2026-03-12 20:28:03 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Re-seed if weather type or screen size changed
|
|
|
|
|
if (!rs.initialized || rs.lastType != wType ||
|
|
|
|
|
rs.lastW != sw || rs.lastH != sh) {
|
|
|
|
|
for (int i = 0; i < MAX_DROPS; ++i) {
|
|
|
|
|
rs.x[i] = static_cast<float>(wxRandInt(static_cast<int>(sw) + 200)) - 100.0f;
|
|
|
|
|
rs.y[i] = static_cast<float>(wxRandInt(static_cast<int>(sh)));
|
|
|
|
|
}
|
|
|
|
|
rs.initialized = true;
|
|
|
|
|
rs.lastType = wType;
|
|
|
|
|
rs.lastW = sw;
|
|
|
|
|
rs.lastH = sh;
|
|
|
|
|
}
|
2026-03-12 20:28:03 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
const float fallSpeed = (wType == 3) ? 680.0f : 440.0f;
|
|
|
|
|
const float windSpeed = (wType == 3) ? 110.0f : 65.0f;
|
|
|
|
|
const int numDrops = static_cast<int>(MAX_DROPS * std::min(1.0f, intensity));
|
|
|
|
|
const float alpha = std::min(1.0f, 0.28f + intensity * 0.38f);
|
|
|
|
|
const uint8_t alphaU8 = static_cast<uint8_t>(alpha * 255.0f);
|
|
|
|
|
const ImU32 dropCol = IM_COL32(175, 195, 225, alphaU8);
|
|
|
|
|
const float dropLen = 7.0f + intensity * 7.0f;
|
|
|
|
|
// Normalised wind direction for the trail endpoint
|
|
|
|
|
const float invSpeed = 1.0f / std::sqrt(fallSpeed * fallSpeed + windSpeed * windSpeed);
|
|
|
|
|
const float trailDx = -windSpeed * invSpeed * dropLen;
|
|
|
|
|
const float trailDy = -fallSpeed * invSpeed * dropLen;
|
2026-03-12 20:28:03 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
for (int i = 0; i < numDrops; ++i) {
|
|
|
|
|
rs.x[i] += windSpeed * dt;
|
|
|
|
|
rs.y[i] += fallSpeed * dt;
|
|
|
|
|
if (rs.y[i] > sh + 10.0f) {
|
|
|
|
|
rs.y[i] = -10.0f;
|
|
|
|
|
rs.x[i] = static_cast<float>(wxRandInt(static_cast<int>(sw) + 200)) - 100.0f;
|
|
|
|
|
}
|
|
|
|
|
if (rs.x[i] > sw + 100.0f) rs.x[i] -= sw + 200.0f;
|
|
|
|
|
dl->AddLine(ImVec2(rs.x[i], rs.y[i]),
|
|
|
|
|
ImVec2(rs.x[i] + trailDx, rs.y[i] + trailDy),
|
|
|
|
|
dropCol, 1.0f);
|
2026-03-12 20:28:03 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// Storm: dark fog-vignette at screen edges
|
|
|
|
|
if (wType == 3) {
|
|
|
|
|
const float vigAlpha = std::min(1.0f, 0.12f + intensity * 0.18f);
|
|
|
|
|
const ImU32 vigCol = IM_COL32(60, 65, 80, static_cast<uint8_t>(vigAlpha * 255.0f));
|
|
|
|
|
const float vigW = sw * 0.22f;
|
|
|
|
|
const float vigH = sh * 0.22f;
|
|
|
|
|
dl->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(vigW, sh), vigCol, IM_COL32_BLACK_TRANS, IM_COL32_BLACK_TRANS, vigCol);
|
|
|
|
|
dl->AddRectFilledMultiColor(ImVec2(sw-vigW, 0), ImVec2(sw, sh), IM_COL32_BLACK_TRANS, vigCol, vigCol, IM_COL32_BLACK_TRANS);
|
|
|
|
|
dl->AddRectFilledMultiColor(ImVec2(0, 0), ImVec2(sw, vigH), vigCol, vigCol, IM_COL32_BLACK_TRANS, IM_COL32_BLACK_TRANS);
|
|
|
|
|
dl->AddRectFilledMultiColor(ImVec2(0, sh-vigH),ImVec2(sw, sh), IM_COL32_BLACK_TRANS, IM_COL32_BLACK_TRANS, vigCol, vigCol);
|
2026-03-12 20:28:03 -07:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
} else if (wType == 2) {
|
|
|
|
|
// ── Snow ─────────────────────────────────────────────────────────────
|
|
|
|
|
constexpr int MAX_FLAKES = 120;
|
|
|
|
|
struct SnowState {
|
|
|
|
|
float x[MAX_FLAKES], y[MAX_FLAKES], phase[MAX_FLAKES];
|
|
|
|
|
bool initialized = false;
|
|
|
|
|
float lastW = 0.0f, lastH = 0.0f;
|
|
|
|
|
};
|
|
|
|
|
static SnowState ss;
|
2026-03-12 20:28:03 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
if (!ss.initialized || ss.lastW != sw || ss.lastH != sh) {
|
|
|
|
|
for (int i = 0; i < MAX_FLAKES; ++i) {
|
|
|
|
|
ss.x[i] = static_cast<float>(wxRandInt(static_cast<int>(sw)));
|
|
|
|
|
ss.y[i] = static_cast<float>(wxRandInt(static_cast<int>(sh)));
|
|
|
|
|
ss.phase[i] = static_cast<float>(wxRandInt(628)) * 0.01f;
|
|
|
|
|
}
|
|
|
|
|
ss.initialized = true;
|
|
|
|
|
ss.lastW = sw;
|
|
|
|
|
ss.lastH = sh;
|
2026-03-12 20:28:03 -07:00
|
|
|
}
|
2026-03-17 20:46:41 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
const float fallSpeed = 45.0f + intensity * 45.0f;
|
|
|
|
|
const int numFlakes = static_cast<int>(MAX_FLAKES * std::min(1.0f, intensity));
|
|
|
|
|
const float alpha = std::min(1.0f, 0.5f + intensity * 0.3f);
|
|
|
|
|
const uint8_t alphaU8 = static_cast<uint8_t>(alpha * 255.0f);
|
|
|
|
|
const float radius = 1.5f + intensity * 1.5f;
|
|
|
|
|
const float time = static_cast<float>(ImGui::GetTime());
|
2026-03-17 20:46:41 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
for (int i = 0; i < numFlakes; ++i) {
|
|
|
|
|
float sway = std::sin(time * 0.7f + ss.phase[i]) * 18.0f;
|
|
|
|
|
ss.x[i] += sway * dt;
|
|
|
|
|
ss.y[i] += fallSpeed * dt;
|
|
|
|
|
ss.phase[i] += dt * 0.25f;
|
|
|
|
|
if (ss.y[i] > sh + 5.0f) {
|
|
|
|
|
ss.y[i] = -5.0f;
|
|
|
|
|
ss.x[i] = static_cast<float>(wxRandInt(static_cast<int>(sw)));
|
|
|
|
|
}
|
|
|
|
|
if (ss.x[i] < -5.0f) ss.x[i] += sw + 10.0f;
|
|
|
|
|
if (ss.x[i] > sw + 5.0f) ss.x[i] -= sw + 10.0f;
|
|
|
|
|
// Two-tone: bright centre dot + transparent outer ring for depth
|
|
|
|
|
dl->AddCircleFilled(ImVec2(ss.x[i], ss.y[i]), radius, IM_COL32(220, 235, 255, alphaU8));
|
|
|
|
|
dl->AddCircleFilled(ImVec2(ss.x[i], ss.y[i]), radius * 0.45f, IM_COL32(245, 250, 255, std::min(255, alphaU8 + 30)));
|
2026-03-17 20:46:41 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-31 19:49:52 +03:00
|
|
|
}
|
2026-03-17 20:46:41 -07:00
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// Dungeon Finder window (toggle with hotkey or bag-bar button)
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
// ============================================================
|
|
|
|
|
// Instance Lockouts
|
|
|
|
|
// ============================================================
|
2026-03-17 20:46:41 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-31 19:49:52 +03:00
|
|
|
// ─── Threat Window ────────────────────────────────────────────────────────────
|
|
|
|
|
// ─── BG Scoreboard ────────────────────────────────────────────────────────────
|
2026-03-17 20:46:41 -07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
}} // namespace wowee::ui
|