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-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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
if (auto* r = core::Application::getInstance().getRenderer()) {
|
|
|
|
|
|
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());
|
|
|
|
|
|
actionFlashEndTimes_[spellId] = now + kActionFlashDuration;
|
|
|
|
|
|
});
|
|
|
|
|
|
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
|
|
|
|
|
|
{
|
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
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_) {
|
2026-02-09 17:39:21 -08:00
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
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_) {
|
2026-02-17 17:37:20 -08:00
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
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) {
|
2026-02-22 02:59:24 -08:00
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
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_) {
|
2026-03-12 16:43:48 -07:00
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
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_) {
|
2026-03-06 19:15:34 -08:00
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
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_) {
|
2026-02-23 01:10:58 -08:00
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
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_) {
|
2026-03-07 22:03:28 -08:00
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
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();
|
|
|
|
|
|
if (cmds.showInspect) showInspectWindow_ = true;
|
|
|
|
|
|
if (cmds.toggleThreat) showThreatWindow_ = !showThreatWindow_;
|
|
|
|
|
|
if (cmds.showBgScore) showBgScoreboard_ = !showBgScoreboard_;
|
|
|
|
|
|
if (cmds.showGmTicket) showGmTicketWindow_ = true;
|
|
|
|
|
|
if (cmds.showWho) showWhoWindow_ = true;
|
|
|
|
|
|
if (cmds.toggleCombatLog) showCombatLog_ = !showCombatLog_;
|
|
|
|
|
|
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 ----
|
|
|
|
|
|
renderActionBar(gameHandler);
|
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
|
|
|
|
renderStanceBar(gameHandler);
|
2026-02-10 01:24:37 -08:00
|
|
|
|
renderBagBar(gameHandler);
|
2026-02-05 12:07:58 -08:00
|
|
|
|
renderXpBar(gameHandler);
|
2026-03-12 05:03:03 -07:00
|
|
|
|
renderRepBar(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
|
|
|
|
renderCastBar(gameHandler);
|
2026-03-09 14:30:48 -07:00
|
|
|
|
renderMirrorTimers(gameHandler);
|
2026-03-12 15:25:07 -07:00
|
|
|
|
renderCooldownTracker(gameHandler);
|
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-09 22:42:44 -07:00
|
|
|
|
renderBattlegroundScore(gameHandler);
|
2026-03-12 03:52:54 -07:00
|
|
|
|
renderRaidWarningOverlay(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
|
|
|
|
renderCombatText(gameHandler);
|
2026-03-12 04:04:27 -07:00
|
|
|
|
renderDPSMeter(gameHandler);
|
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-11 09:24:37 -07:00
|
|
|
|
if (showRaidFrames_) {
|
|
|
|
|
|
renderPartyFrames(gameHandler);
|
|
|
|
|
|
}
|
2026-03-09 20:05:09 -07:00
|
|
|
|
renderBossFrames(gameHandler);
|
2026-03-31 10:07:58 +03:00
|
|
|
|
dialogManager_.renderDialogs(gameHandler, inventoryScreen, chatPanel_);
|
2026-02-13 21:39:48 -08:00
|
|
|
|
renderGuildRoster(gameHandler);
|
2026-03-12 00:53:57 -07:00
|
|
|
|
renderSocialFrame(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
|
|
|
|
renderBuffBar(gameHandler);
|
|
|
|
|
|
renderLootWindow(gameHandler);
|
|
|
|
|
|
renderGossipWindow(gameHandler);
|
2026-02-06 11:59:51 -08:00
|
|
|
|
renderQuestDetailsWindow(gameHandler);
|
2026-02-06 21:50:15 -08:00
|
|
|
|
renderQuestRequestItemsWindow(gameHandler);
|
|
|
|
|
|
renderQuestOfferRewardWindow(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
|
|
|
|
renderVendorWindow(gameHandler);
|
2026-02-08 14:33:39 -08:00
|
|
|
|
renderTrainerWindow(gameHandler);
|
2026-03-18 11:58:01 -07:00
|
|
|
|
renderBarberShopWindow(gameHandler);
|
feat: implement pet stable system (MSG_LIST_STABLED_PETS, CMSG_STABLE_PET, CMSG_UNSTABLE_PET)
- Parse MSG_LIST_STABLED_PETS (SMSG): populate StabledPet list with
petNumber, entry, level, name, displayId, and active status
- Detect stable master via gossip option text/keyword matching and
auto-send MSG_LIST_STABLED_PETS request to open the stable UI
- Refresh list automatically after SMSG_STABLE_RESULT to reflect state
- New packet builders: ListStabledPetsPacket, StablePetPacket, UnstablePetPacket
- New public API: requestStabledPetList(), stablePet(slot), unstablePet(petNumber)
- Stable window UI: shows active/stabled pets with store/retrieve buttons,
slot count, refresh, and close; opens when server sends pet list
- Clear stable state on world logout/disconnect
2026-03-12 19:15:52 -07:00
|
|
|
|
renderStableWindow(gameHandler);
|
2026-02-07 16:59:20 -08:00
|
|
|
|
renderTaxiWindow(gameHandler);
|
2026-02-15 14:00:41 -08:00
|
|
|
|
renderMailWindow(gameHandler);
|
|
|
|
|
|
renderMailComposeWindow(gameHandler);
|
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
|
|
|
|
renderBankWindow(gameHandler);
|
|
|
|
|
|
renderGuildBankWindow(gameHandler);
|
|
|
|
|
|
renderAuctionHouseWindow(gameHandler);
|
2026-03-09 13:47:07 -07:00
|
|
|
|
renderDungeonFinderWindow(gameHandler);
|
2026-03-09 15:52:58 -07:00
|
|
|
|
renderInstanceLockouts(gameHandler);
|
2026-03-12 10:41:18 -07:00
|
|
|
|
renderWhoWindow(gameHandler);
|
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
|
|
|
|
renderCombatLog(gameHandler);
|
2026-03-12 02:09:35 -07:00
|
|
|
|
renderAchievementWindow(gameHandler);
|
2026-03-17 20:46:41 -07:00
|
|
|
|
renderSkillsWindow(gameHandler);
|
2026-03-12 20:23:36 -07:00
|
|
|
|
renderTitlesWindow(gameHandler);
|
2026-03-12 20:28:03 -07:00
|
|
|
|
renderEquipSetWindow(gameHandler);
|
2026-03-12 02:31:12 -07:00
|
|
|
|
renderGmTicketWindow(gameHandler);
|
2026-03-12 02:52:40 -07:00
|
|
|
|
renderInspectWindow(gameHandler);
|
2026-03-12 18:21:50 -07:00
|
|
|
|
renderBookWindow(gameHandler);
|
2026-03-12 02:59:09 -07:00
|
|
|
|
renderThreatWindow(gameHandler);
|
2026-03-12 12:02:59 -07:00
|
|
|
|
renderBgScoreboard(gameHandler);
|
2026-03-11 09:24:37 -07:00
|
|
|
|
if (showMinimap_) {
|
|
|
|
|
|
renderMinimapMarkers(gameHandler);
|
|
|
|
|
|
}
|
2026-03-13 10:13:54 -07:00
|
|
|
|
renderLogoutCountdown(gameHandler);
|
2026-02-06 17:27:20 -08:00
|
|
|
|
renderDeathScreen(gameHandler);
|
2026-03-09 22:31:56 -07:00
|
|
|
|
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-02-05 16:01:38 -08:00
|
|
|
|
renderEscapeMenu();
|
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)
|
|
|
|
|
|
spellbookScreen.render(gameHandler, core::Application::getInstance().getAssetManager());
|
|
|
|
|
|
|
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_) {
|
2026-02-06 14:24:38 -08:00
|
|
|
|
auto* am = core::Application::getInstance().getAssetManager();
|
|
|
|
|
|
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-02-19 22:34:22 -08:00
|
|
|
|
if (!vendorBagsOpened_) {
|
|
|
|
|
|
vendorBagsOpened_ = true;
|
|
|
|
|
|
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 {
|
|
|
|
|
|
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());
|
|
|
|
|
|
core::Application::getInstance().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
|
|
|
|
|
|
auto* r = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
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
|
2026-02-02 12:24:50 -08:00
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
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 08:53:14 +03:00
|
|
|
|
} else if (showEscapeMenu) {
|
|
|
|
|
|
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();
|
|
|
|
|
|
} else if (showWhoWindow_) {
|
|
|
|
|
|
showWhoWindow_ = false;
|
|
|
|
|
|
} else if (showCombatLog_) {
|
|
|
|
|
|
showCombatLog_ = false;
|
|
|
|
|
|
} else if (showSocialFrame_) {
|
|
|
|
|
|
showSocialFrame_ = false;
|
|
|
|
|
|
} 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 {
|
|
|
|
|
|
showEscapeMenu = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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)) {
|
|
|
|
|
|
showRaidFrames_ = !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)) {
|
|
|
|
|
|
showAchievementWindow_ = !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)) {
|
|
|
|
|
|
showSkillsWindow_ = !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) {
|
|
|
|
|
|
showTitlesWindow_ = !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) {
|
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
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) {
|
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
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) {
|
|
|
|
|
|
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
|
|
|
|
|
|
{
|
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
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;
|
|
|
|
|
|
} 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) {
|
|
|
|
|
|
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");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (auto* ren = core::Application::getInstance().getRenderer()) {
|
|
|
|
|
|
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.
|
|
|
|
|
|
auto* assetMgr = core::Application::getInstance().getAssetManager();
|
|
|
|
|
|
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) {
|
|
|
|
|
|
auto* spellAsset = core::Application::getInstance().getAssetManager();
|
|
|
|
|
|
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
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
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();
|
|
|
|
|
|
showInspectWindow_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Add Friend"))
|
|
|
|
|
|
gameHandler.addFriend(name);
|
|
|
|
|
|
if (ImGui::MenuItem("Ignore"))
|
|
|
|
|
|
gameHandler.addIgnore(name);
|
|
|
|
|
|
if (ImGui::MenuItem("Report Player"))
|
|
|
|
|
|
gameHandler.reportPlayer(tGuid, "Reported via UI");
|
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();
|
|
|
|
|
|
showInspectWindow_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
|
{
|
|
|
|
|
|
auto* tcastAsset = core::Application::getInstance().getAssetManager();
|
|
|
|
|
|
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));
|
|
|
|
|
|
if (ImGui::SmallButton("Threat")) showThreatWindow_ = !showThreatWindow_;
|
|
|
|
|
|
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) {
|
|
|
|
|
|
auto* assetMgr = core::Application::getInstance().getAssetManager();
|
|
|
|
|
|
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) {
|
|
|
|
|
|
auto* totAsset = core::Application::getInstance().getAssetManager();
|
|
|
|
|
|
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
|
|
|
|
|
2026-03-31 08:53:14 +03:00
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
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();
|
|
|
|
|
|
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();
|
|
|
|
|
|
showInspectWindow_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
|
{
|
|
|
|
|
|
auto* fcAsset = core::Application::getInstance().getAssetManager();
|
|
|
|
|
|
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) {
|
|
|
|
|
|
auto* focusAsset = core::Application::getInstance().getAssetManager();
|
|
|
|
|
|
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
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
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:14:08 -07:00
|
|
|
|
uint32_t GameScreen::resolveMacroPrimarySpellId(uint32_t macroId, game::GameHandler& gameHandler) {
|
2026-03-20 08:52:57 -07:00
|
|
|
|
// Invalidate cache when spell list changes (learning/unlearning spells)
|
|
|
|
|
|
size_t curSpellCount = gameHandler.getKnownSpells().size();
|
|
|
|
|
|
if (curSpellCount != macroCacheSpellCount_) {
|
|
|
|
|
|
macroPrimarySpellCache_.clear();
|
|
|
|
|
|
macroCacheSpellCount_ = curSpellCount;
|
|
|
|
|
|
}
|
2026-03-20 08:14:08 -07:00
|
|
|
|
auto cacheIt = macroPrimarySpellCache_.find(macroId);
|
2026-03-20 08:11:13 -07:00
|
|
|
|
if (cacheIt != macroPrimarySpellCache_.end()) return cacheIt->second;
|
|
|
|
|
|
|
|
|
|
|
|
const std::string& macroText = gameHandler.getMacroText(macroId);
|
|
|
|
|
|
uint32_t result = 0;
|
|
|
|
|
|
if (!macroText.empty()) {
|
|
|
|
|
|
for (const auto& cmdLine : allMacroCommands(macroText)) {
|
|
|
|
|
|
std::string cl = cmdLine;
|
|
|
|
|
|
for (char& c : cl) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
2026-03-20 08:43:19 -07:00
|
|
|
|
bool isCast = (cl.rfind("/cast ", 0) == 0);
|
|
|
|
|
|
bool isCastSeq = (cl.rfind("/castsequence ", 0) == 0);
|
2026-03-20 09:08:49 -07:00
|
|
|
|
bool isUse = (cl.rfind("/use ", 0) == 0);
|
|
|
|
|
|
if (!isCast && !isCastSeq && !isUse) continue;
|
2026-03-20 08:11:13 -07:00
|
|
|
|
size_t sp2 = cmdLine.find(' ');
|
|
|
|
|
|
if (sp2 == std::string::npos) continue;
|
|
|
|
|
|
std::string spellArg = cmdLine.substr(sp2 + 1);
|
2026-03-20 08:43:19 -07:00
|
|
|
|
// Strip conditionals [...]
|
2026-03-20 08:11:13 -07:00
|
|
|
|
if (!spellArg.empty() && spellArg.front() == '[') {
|
|
|
|
|
|
size_t ce = spellArg.find(']');
|
|
|
|
|
|
if (ce != std::string::npos) spellArg = spellArg.substr(ce + 1);
|
|
|
|
|
|
}
|
2026-03-20 08:43:19 -07:00
|
|
|
|
// Strip reset= spec for castsequence
|
|
|
|
|
|
if (isCastSeq) {
|
|
|
|
|
|
std::string tmp = spellArg;
|
|
|
|
|
|
while (!tmp.empty() && tmp.front() == ' ') tmp.erase(tmp.begin());
|
|
|
|
|
|
if (tmp.rfind("reset=", 0) == 0) {
|
|
|
|
|
|
size_t spAfter = tmp.find(' ');
|
|
|
|
|
|
if (spAfter != std::string::npos) spellArg = tmp.substr(spAfter + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Take first alternative before ';' (for /cast) or first spell before ',' (for /castsequence)
|
|
|
|
|
|
size_t semi = spellArg.find(isCastSeq ? ',' : ';');
|
2026-03-20 08:11:13 -07:00
|
|
|
|
if (semi != std::string::npos) spellArg = spellArg.substr(0, semi);
|
|
|
|
|
|
size_t ss = spellArg.find_first_not_of(" \t!");
|
|
|
|
|
|
if (ss != std::string::npos) spellArg = spellArg.substr(ss);
|
|
|
|
|
|
size_t se = spellArg.find_last_not_of(" \t");
|
|
|
|
|
|
if (se != std::string::npos) spellArg.resize(se + 1);
|
|
|
|
|
|
if (spellArg.empty()) continue;
|
|
|
|
|
|
std::string spLow = spellArg;
|
|
|
|
|
|
for (char& c : spLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
2026-03-20 09:08:49 -07:00
|
|
|
|
if (isUse) {
|
|
|
|
|
|
// /use resolves an item name → find the item's on-use spell ID
|
|
|
|
|
|
for (const auto& [entry, info] : gameHandler.getItemInfoCache()) {
|
|
|
|
|
|
if (!info.valid) continue;
|
|
|
|
|
|
std::string iName = info.name;
|
|
|
|
|
|
for (char& c : iName) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
if (iName == spLow) {
|
|
|
|
|
|
for (const auto& sp : info.spells) {
|
|
|
|
|
|
if (sp.spellId != 0 && sp.spellTrigger == 0) { result = sp.spellId; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// /cast and /castsequence resolve a spell name
|
|
|
|
|
|
for (uint32_t sid : gameHandler.getKnownSpells()) {
|
|
|
|
|
|
std::string sn = gameHandler.getSpellName(sid);
|
|
|
|
|
|
for (char& c : sn) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
if (sn == spLow) { result = sid; break; }
|
|
|
|
|
|
}
|
2026-03-20 08:11:13 -07:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-20 08:14:08 -07:00
|
|
|
|
macroPrimarySpellCache_[macroId] = result;
|
2026-03-20 08:11:13 -07:00
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
void GameScreen::renderActionBar(game::GameHandler& gameHandler) {
|
2026-03-10 15:56:41 -07:00
|
|
|
|
// Use ImGui's display size — always in sync with the current swap-chain/frame,
|
|
|
|
|
|
// whereas window->getWidth/Height() can lag by one frame on resize events.
|
|
|
|
|
|
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;
|
2026-02-05 15:07:13 -08:00
|
|
|
|
auto* assetMgr = core::Application::getInstance().getAssetManager();
|
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 10:07:58 +03:00
|
|
|
|
float slotSize = 48.0f * settingsPanel_.pendingActionBarScale;
|
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
|
|
|
|
float spacing = 4.0f;
|
|
|
|
|
|
float padding = 8.0f;
|
|
|
|
|
|
float barW = 12 * slotSize + 11 * spacing + padding * 2;
|
|
|
|
|
|
float barH = slotSize + 24.0f;
|
|
|
|
|
|
float barX = (screenW - barW) / 2.0f;
|
|
|
|
|
|
float barY = screenH - barH;
|
|
|
|
|
|
|
2026-02-17 03:50:36 -08:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(barX, barY), 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
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(barW, barH), ImGuiCond_Always);
|
|
|
|
|
|
|
2026-02-17 03:50:36 -08:00
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
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
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
2026-02-09 01:40:29 -08:00
|
|
|
|
ImGuiWindowFlags_NoScrollbar;
|
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::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
2026-02-15 03:17:51 -08:00
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(padding, padding));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.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
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.05f, 0.05f, 0.9f));
|
|
|
|
|
|
|
2026-03-10 06:04:43 -07:00
|
|
|
|
// Per-slot rendering lambda — shared by both action bars
|
|
|
|
|
|
const auto& bar = gameHandler.getActionBar();
|
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* keyLabels1[] = {"1","2","3","4","5","6","7","8","9","0","-","="};
|
2026-03-10 06:04:43 -07:00
|
|
|
|
// "⇧N" labels for bar 2 (UTF-8: E2 87 A7 = U+21E7 UPWARDS WHITE ARROW)
|
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* keyLabels2[] = {
|
2026-03-10 06:04:43 -07:00
|
|
|
|
"\xe2\x87\xa7" "1", "\xe2\x87\xa7" "2", "\xe2\x87\xa7" "3",
|
|
|
|
|
|
"\xe2\x87\xa7" "4", "\xe2\x87\xa7" "5", "\xe2\x87\xa7" "6",
|
|
|
|
|
|
"\xe2\x87\xa7" "7", "\xe2\x87\xa7" "8", "\xe2\x87\xa7" "9",
|
|
|
|
|
|
"\xe2\x87\xa7" "0", "\xe2\x87\xa7" "-", "\xe2\x87\xa7" "="
|
|
|
|
|
|
};
|
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-10 06:04:43 -07:00
|
|
|
|
auto renderBarSlot = [&](int absSlot, const char* keyLabel) {
|
|
|
|
|
|
ImGui::BeginGroup();
|
|
|
|
|
|
ImGui::PushID(absSlot);
|
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-10 06:04:43 -07:00
|
|
|
|
const auto& slot = bar[absSlot];
|
|
|
|
|
|
bool onCooldown = !slot.isReady();
|
2026-03-20 08:07:20 -07:00
|
|
|
|
|
2026-03-20 08:11:13 -07:00
|
|
|
|
// Macro cooldown: check the cached primary spell's cooldown.
|
2026-03-20 08:07:20 -07:00
|
|
|
|
float macroCooldownRemaining = 0.0f;
|
|
|
|
|
|
float macroCooldownTotal = 0.0f;
|
|
|
|
|
|
if (slot.type == game::ActionBarSlot::MACRO && slot.id != 0 && !onCooldown) {
|
2026-03-20 08:14:08 -07:00
|
|
|
|
uint32_t macroSpellId = resolveMacroPrimarySpellId(slot.id, gameHandler);
|
2026-03-20 08:11:13 -07:00
|
|
|
|
if (macroSpellId != 0) {
|
|
|
|
|
|
float cd = gameHandler.getSpellCooldown(macroSpellId);
|
|
|
|
|
|
if (cd > 0.0f) {
|
|
|
|
|
|
macroCooldownRemaining = cd;
|
|
|
|
|
|
macroCooldownTotal = cd;
|
|
|
|
|
|
onCooldown = true;
|
2026-03-20 08:07:20 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 05:38:13 -07:00
|
|
|
|
const bool onGCD = gameHandler.isGCDActive() && !onCooldown && !slot.isEmpty();
|
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-12 05:57:45 -07:00
|
|
|
|
// Out-of-range check: red tint when a targeted spell cannot reach the current target.
|
2026-03-20 08:27:10 -07:00
|
|
|
|
// Applies to SPELL and MACRO slots with a known max range (>5 yd) and an active target.
|
2026-03-17 13:59:42 -07:00
|
|
|
|
// Item range is checked below after barItemDef is populated.
|
2026-03-12 05:57:45 -07:00
|
|
|
|
bool outOfRange = false;
|
2026-03-20 08:27:10 -07:00
|
|
|
|
{
|
|
|
|
|
|
uint32_t rangeCheckSpellId = 0;
|
|
|
|
|
|
if (slot.type == game::ActionBarSlot::SPELL && slot.id != 0)
|
|
|
|
|
|
rangeCheckSpellId = slot.id;
|
|
|
|
|
|
else if (slot.type == game::ActionBarSlot::MACRO && slot.id != 0)
|
|
|
|
|
|
rangeCheckSpellId = resolveMacroPrimarySpellId(slot.id, gameHandler);
|
|
|
|
|
|
if (rangeCheckSpellId != 0 && !onCooldown && gameHandler.hasTarget()) {
|
|
|
|
|
|
uint32_t maxRange = spellbookScreen.getSpellMaxRange(rangeCheckSpellId, assetMgr);
|
|
|
|
|
|
if (maxRange > 5) {
|
|
|
|
|
|
auto& em = gameHandler.getEntityManager();
|
|
|
|
|
|
auto playerEnt = em.getEntity(gameHandler.getPlayerGuid());
|
|
|
|
|
|
auto targetEnt = em.getEntity(gameHandler.getTargetGuid());
|
|
|
|
|
|
if (playerEnt && targetEnt) {
|
|
|
|
|
|
float dx = playerEnt->getX() - targetEnt->getX();
|
|
|
|
|
|
float dy = playerEnt->getY() - targetEnt->getY();
|
|
|
|
|
|
float dz = playerEnt->getZ() - targetEnt->getZ();
|
|
|
|
|
|
if (std::sqrt(dx*dx + dy*dy + dz*dz) > static_cast<float>(maxRange))
|
|
|
|
|
|
outOfRange = true;
|
|
|
|
|
|
}
|
2026-03-12 05:57:45 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-20 08:32:07 -07:00
|
|
|
|
// Insufficient-power check: tint when player doesn't have enough power to cast.
|
|
|
|
|
|
// Applies to SPELL and MACRO slots with a known power cost.
|
2026-03-12 06:01:42 -07:00
|
|
|
|
bool insufficientPower = false;
|
2026-03-20 08:32:07 -07:00
|
|
|
|
{
|
|
|
|
|
|
uint32_t powerCheckSpellId = 0;
|
|
|
|
|
|
if (slot.type == game::ActionBarSlot::SPELL && slot.id != 0)
|
|
|
|
|
|
powerCheckSpellId = slot.id;
|
|
|
|
|
|
else if (slot.type == game::ActionBarSlot::MACRO && slot.id != 0)
|
|
|
|
|
|
powerCheckSpellId = resolveMacroPrimarySpellId(slot.id, gameHandler);
|
2026-03-12 06:01:42 -07:00
|
|
|
|
uint32_t spellCost = 0, spellPowerType = 0;
|
2026-03-20 08:32:07 -07:00
|
|
|
|
if (powerCheckSpellId != 0 && !onCooldown)
|
|
|
|
|
|
spellbookScreen.getSpellPowerInfo(powerCheckSpellId, assetMgr, spellCost, spellPowerType);
|
2026-03-12 06:01:42 -07:00
|
|
|
|
if (spellCost > 0) {
|
|
|
|
|
|
auto playerEnt = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid());
|
|
|
|
|
|
if (playerEnt && (playerEnt->getType() == game::ObjectType::PLAYER ||
|
|
|
|
|
|
playerEnt->getType() == game::ObjectType::UNIT)) {
|
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(playerEnt);
|
|
|
|
|
|
if (unit->getPowerType() == static_cast<uint8_t>(spellPowerType)) {
|
|
|
|
|
|
if (unit->getPower() < spellCost)
|
|
|
|
|
|
insufficientPower = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 06:04:43 -07:00
|
|
|
|
auto getSpellName = [&](uint32_t spellId) -> std::string {
|
|
|
|
|
|
std::string name = spellbookScreen.lookupSpellName(spellId, assetMgr);
|
|
|
|
|
|
if (!name.empty()) return name;
|
|
|
|
|
|
return "Spell #" + std::to_string(spellId);
|
|
|
|
|
|
};
|
2026-02-05 15:07:13 -08:00
|
|
|
|
|
2026-03-10 06:04:43 -07:00
|
|
|
|
// Try to get icon texture for this slot
|
|
|
|
|
|
VkDescriptorSet iconTex = VK_NULL_HANDLE;
|
|
|
|
|
|
const game::ItemDef* barItemDef = nullptr;
|
|
|
|
|
|
uint32_t itemDisplayInfoId = 0;
|
|
|
|
|
|
std::string itemNameFromQuery;
|
|
|
|
|
|
if (slot.type == game::ActionBarSlot::SPELL && slot.id != 0) {
|
|
|
|
|
|
iconTex = getSpellIcon(slot.id, assetMgr);
|
|
|
|
|
|
} else if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) {
|
|
|
|
|
|
auto& inv = gameHandler.getInventory();
|
|
|
|
|
|
for (int bi = 0; bi < inv.getBackpackSize(); bi++) {
|
|
|
|
|
|
const auto& bs = inv.getBackpackSlot(bi);
|
|
|
|
|
|
if (!bs.empty() && bs.item.itemId == slot.id) { barItemDef = &bs.item; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!barItemDef) {
|
|
|
|
|
|
for (int ei = 0; ei < game::Inventory::NUM_EQUIP_SLOTS; ei++) {
|
|
|
|
|
|
const auto& es = inv.getEquipSlot(static_cast<game::EquipSlot>(ei));
|
|
|
|
|
|
if (!es.empty() && es.item.itemId == slot.id) { barItemDef = &es.item; break; }
|
2026-02-17 03:50:36 -08:00
|
|
|
|
}
|
2026-03-10 06:04:43 -07:00
|
|
|
|
}
|
|
|
|
|
|
if (!barItemDef) {
|
|
|
|
|
|
for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS && !barItemDef; bag++) {
|
|
|
|
|
|
for (int si = 0; si < inv.getBagSize(bag); si++) {
|
|
|
|
|
|
const auto& bs = inv.getBagSlot(bag, si);
|
|
|
|
|
|
if (!bs.empty() && bs.item.itemId == slot.id) { barItemDef = &bs.item; break; }
|
2026-02-17 03:50:36 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-10 06:04:43 -07:00
|
|
|
|
}
|
|
|
|
|
|
if (barItemDef && barItemDef->displayInfoId != 0)
|
|
|
|
|
|
itemDisplayInfoId = barItemDef->displayInfoId;
|
|
|
|
|
|
if (itemDisplayInfoId == 0) {
|
|
|
|
|
|
if (auto* info = gameHandler.getItemInfo(slot.id)) {
|
|
|
|
|
|
itemDisplayInfoId = info->displayInfoId;
|
|
|
|
|
|
if (itemNameFromQuery.empty() && !info->name.empty())
|
|
|
|
|
|
itemNameFromQuery = info->name;
|
2026-02-06 19:17:35 -08:00
|
|
|
|
}
|
2026-02-06 14:30:54 -08:00
|
|
|
|
}
|
2026-03-10 06:04:43 -07:00
|
|
|
|
if (itemDisplayInfoId != 0)
|
|
|
|
|
|
iconTex = inventoryScreen.getItemIcon(itemDisplayInfoId);
|
|
|
|
|
|
}
|
2026-02-06 14:30:54 -08:00
|
|
|
|
|
2026-03-18 03:16:05 -07:00
|
|
|
|
// Macro icon: #showtooltip [SpellName] → show that spell's icon on the button
|
2026-03-20 10:06:14 -07:00
|
|
|
|
bool macroIsUseCmd = false; // tracks if the macro's primary command is /use (for item icon fallback)
|
2026-03-18 03:16:05 -07:00
|
|
|
|
if (slot.type == game::ActionBarSlot::MACRO && slot.id != 0 && !iconTex) {
|
|
|
|
|
|
const std::string& macroText = gameHandler.getMacroText(slot.id);
|
|
|
|
|
|
if (!macroText.empty()) {
|
|
|
|
|
|
std::string showArg = getMacroShowtooltipArg(macroText);
|
|
|
|
|
|
if (showArg.empty() || showArg == "__auto__") {
|
2026-03-20 10:06:14 -07:00
|
|
|
|
// No explicit #showtooltip arg — derive spell from first /cast, /castsequence, or /use line
|
2026-03-18 03:16:05 -07:00
|
|
|
|
for (const auto& cmdLine : allMacroCommands(macroText)) {
|
|
|
|
|
|
if (cmdLine.size() < 6) continue;
|
|
|
|
|
|
std::string cl = cmdLine;
|
|
|
|
|
|
for (char& c : cl) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
2026-03-20 08:43:19 -07:00
|
|
|
|
bool isCastCmd = (cl.rfind("/cast ", 0) == 0 || cl == "/cast");
|
|
|
|
|
|
bool isCastSeqCmd = (cl.rfind("/castsequence ", 0) == 0);
|
2026-03-20 09:08:49 -07:00
|
|
|
|
bool isUseCmd = (cl.rfind("/use ", 0) == 0);
|
2026-03-20 10:06:14 -07:00
|
|
|
|
if (isUseCmd) macroIsUseCmd = true;
|
2026-03-20 09:08:49 -07:00
|
|
|
|
if (!isCastCmd && !isCastSeqCmd && !isUseCmd) continue;
|
2026-03-18 03:16:05 -07:00
|
|
|
|
size_t sp2 = cmdLine.find(' ');
|
|
|
|
|
|
if (sp2 == std::string::npos) continue;
|
|
|
|
|
|
showArg = cmdLine.substr(sp2 + 1);
|
|
|
|
|
|
// Strip conditionals [...]
|
|
|
|
|
|
if (!showArg.empty() && showArg.front() == '[') {
|
|
|
|
|
|
size_t ce = showArg.find(']');
|
|
|
|
|
|
if (ce != std::string::npos) showArg = showArg.substr(ce + 1);
|
|
|
|
|
|
}
|
2026-03-20 08:43:19 -07:00
|
|
|
|
// Strip reset= spec for castsequence
|
|
|
|
|
|
if (isCastSeqCmd) {
|
|
|
|
|
|
std::string tmp = showArg;
|
|
|
|
|
|
while (!tmp.empty() && tmp.front() == ' ') tmp.erase(tmp.begin());
|
|
|
|
|
|
if (tmp.rfind("reset=", 0) == 0) {
|
|
|
|
|
|
size_t spA = tmp.find(' ');
|
|
|
|
|
|
if (spA != std::string::npos) showArg = tmp.substr(spA + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// First alternative: ';' for /cast, ',' for /castsequence
|
|
|
|
|
|
size_t sep = showArg.find(isCastSeqCmd ? ',' : ';');
|
|
|
|
|
|
if (sep != std::string::npos) showArg = showArg.substr(0, sep);
|
2026-03-18 03:16:05 -07:00
|
|
|
|
// Trim and strip '!'
|
|
|
|
|
|
size_t ss = showArg.find_first_not_of(" \t!");
|
|
|
|
|
|
if (ss != std::string::npos) showArg = showArg.substr(ss);
|
|
|
|
|
|
size_t se = showArg.find_last_not_of(" \t");
|
|
|
|
|
|
if (se != std::string::npos) showArg.resize(se + 1);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Look up the spell icon by name
|
|
|
|
|
|
if (!showArg.empty() && showArg != "__auto__") {
|
|
|
|
|
|
std::string showLower = showArg;
|
|
|
|
|
|
for (char& c : showLower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
// Also strip "(Rank N)" suffix for matching
|
|
|
|
|
|
size_t rankParen = showLower.find('(');
|
|
|
|
|
|
if (rankParen != std::string::npos) showLower.resize(rankParen);
|
|
|
|
|
|
while (!showLower.empty() && showLower.back() == ' ') showLower.pop_back();
|
|
|
|
|
|
for (uint32_t sid : gameHandler.getKnownSpells()) {
|
|
|
|
|
|
const std::string& sn = gameHandler.getSpellName(sid);
|
|
|
|
|
|
if (sn.empty()) continue;
|
|
|
|
|
|
std::string snl = sn;
|
|
|
|
|
|
for (char& c : snl) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
if (snl == showLower) {
|
|
|
|
|
|
iconTex = assetMgr ? getSpellIcon(sid, assetMgr) : VK_NULL_HANDLE;
|
|
|
|
|
|
if (iconTex) break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-20 10:06:14 -07:00
|
|
|
|
// Fallback for /use macros: if no spell matched, search item cache for the item icon
|
|
|
|
|
|
if (!iconTex && macroIsUseCmd) {
|
|
|
|
|
|
for (const auto& [entry, info] : gameHandler.getItemInfoCache()) {
|
|
|
|
|
|
if (!info.valid) continue;
|
|
|
|
|
|
std::string iName = info.name;
|
|
|
|
|
|
for (char& c : iName) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
if (iName == showLower && info.displayInfoId != 0) {
|
|
|
|
|
|
iconTex = inventoryScreen.getItemIcon(info.displayInfoId);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-18 03:16:05 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 06:03:04 -07:00
|
|
|
|
// Item-missing check: grey out item slots whose item is not in the player's inventory.
|
|
|
|
|
|
const bool itemMissing = (slot.type == game::ActionBarSlot::ITEM && slot.id != 0
|
|
|
|
|
|
&& barItemDef == nullptr && !onCooldown);
|
|
|
|
|
|
|
2026-03-17 13:59:42 -07:00
|
|
|
|
// Ranged item out-of-range check (runs after barItemDef is populated above).
|
|
|
|
|
|
// invType 15=Ranged (bow/gun/crossbow), 26=Thrown, 28=RangedRight (wand/crossbow).
|
|
|
|
|
|
if (!outOfRange && slot.type == game::ActionBarSlot::ITEM && barItemDef
|
|
|
|
|
|
&& !onCooldown && gameHandler.hasTarget()) {
|
|
|
|
|
|
constexpr uint8_t INVTYPE_RANGED = 15;
|
|
|
|
|
|
constexpr uint8_t INVTYPE_THROWN = 26;
|
|
|
|
|
|
constexpr uint8_t INVTYPE_RANGEDRIGHT = 28;
|
|
|
|
|
|
uint32_t itemMaxRange = 0;
|
|
|
|
|
|
if (barItemDef->inventoryType == INVTYPE_RANGED ||
|
|
|
|
|
|
barItemDef->inventoryType == INVTYPE_RANGEDRIGHT)
|
|
|
|
|
|
itemMaxRange = 40;
|
|
|
|
|
|
else if (barItemDef->inventoryType == INVTYPE_THROWN)
|
|
|
|
|
|
itemMaxRange = 30;
|
|
|
|
|
|
if (itemMaxRange > 0) {
|
|
|
|
|
|
auto& em = gameHandler.getEntityManager();
|
|
|
|
|
|
auto playerEnt = em.getEntity(gameHandler.getPlayerGuid());
|
|
|
|
|
|
auto targetEnt = em.getEntity(gameHandler.getTargetGuid());
|
|
|
|
|
|
if (playerEnt && targetEnt) {
|
|
|
|
|
|
float dx = playerEnt->getX() - targetEnt->getX();
|
|
|
|
|
|
float dy = playerEnt->getY() - targetEnt->getY();
|
|
|
|
|
|
float dz = playerEnt->getZ() - targetEnt->getZ();
|
|
|
|
|
|
if (std::sqrt(dx*dx + dy*dy + dz*dz) > static_cast<float>(itemMaxRange))
|
|
|
|
|
|
outOfRange = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 06:04:43 -07:00
|
|
|
|
bool clicked = false;
|
|
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
ImVec4 tintColor(1, 1, 1, 1);
|
|
|
|
|
|
ImVec4 bgColor(0.1f, 0.1f, 0.1f, 0.9f);
|
2026-03-12 06:03:04 -07:00
|
|
|
|
if (onCooldown) { tintColor = ImVec4(0.4f, 0.4f, 0.4f, 0.8f); }
|
|
|
|
|
|
else if (onGCD) { tintColor = ImVec4(0.6f, 0.6f, 0.6f, 0.85f); }
|
|
|
|
|
|
else if (outOfRange) { tintColor = ImVec4(0.85f, 0.35f, 0.35f, 0.9f); }
|
2026-03-12 06:01:42 -07:00
|
|
|
|
else if (insufficientPower) { tintColor = ImVec4(0.6f, 0.5f, 0.9f, 0.85f); }
|
2026-03-12 06:03:04 -07:00
|
|
|
|
else if (itemMissing) { tintColor = ImVec4(0.35f, 0.35f, 0.35f, 0.7f); }
|
2026-03-10 06:04:43 -07:00
|
|
|
|
clicked = ImGui::ImageButton("##icon",
|
|
|
|
|
|
(ImTextureID)(uintptr_t)iconTex,
|
|
|
|
|
|
ImVec2(slotSize, slotSize),
|
|
|
|
|
|
ImVec2(0, 0), ImVec2(1, 1),
|
|
|
|
|
|
bgColor, tintColor);
|
|
|
|
|
|
} else {
|
2026-03-12 06:01:42 -07:00
|
|
|
|
if (onCooldown) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.2f, 0.2f, 0.8f));
|
|
|
|
|
|
else if (outOfRange) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.45f, 0.15f, 0.15f, 0.9f));
|
|
|
|
|
|
else if (insufficientPower)ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.2f, 0.15f, 0.4f, 0.9f));
|
2026-03-12 06:03:04 -07:00
|
|
|
|
else if (itemMissing) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.12f, 0.12f, 0.12f, 0.7f));
|
2026-03-12 06:01:42 -07:00
|
|
|
|
else if (slot.isEmpty()) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.8f));
|
|
|
|
|
|
else ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.3f, 0.5f, 0.9f));
|
2026-03-10 06:04:43 -07:00
|
|
|
|
|
|
|
|
|
|
char label[32];
|
|
|
|
|
|
if (slot.type == game::ActionBarSlot::SPELL) {
|
|
|
|
|
|
std::string spellName = getSpellName(slot.id);
|
|
|
|
|
|
if (spellName.size() > 6) spellName = spellName.substr(0, 6);
|
|
|
|
|
|
snprintf(label, sizeof(label), "%s", spellName.c_str());
|
|
|
|
|
|
} else if (slot.type == game::ActionBarSlot::ITEM && barItemDef) {
|
|
|
|
|
|
std::string itemName = barItemDef->name;
|
|
|
|
|
|
if (itemName.size() > 6) itemName = itemName.substr(0, 6);
|
|
|
|
|
|
snprintf(label, sizeof(label), "%s", itemName.c_str());
|
|
|
|
|
|
} else if (slot.type == game::ActionBarSlot::ITEM) {
|
|
|
|
|
|
snprintf(label, sizeof(label), "Item");
|
|
|
|
|
|
} else if (slot.type == game::ActionBarSlot::MACRO) {
|
|
|
|
|
|
snprintf(label, sizeof(label), "Macro");
|
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
|
|
|
|
} else {
|
2026-03-10 06:04:43 -07:00
|
|
|
|
snprintf(label, sizeof(label), "--");
|
|
|
|
|
|
}
|
|
|
|
|
|
clicked = ImGui::Button(label, ImVec2(slotSize, slotSize));
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
2026-02-06 14:30:54 -08:00
|
|
|
|
|
2026-03-18 04:30:33 -07:00
|
|
|
|
// Error-flash overlay: red fade on spell cast failure (~0.5 s).
|
2026-03-20 08:38:24 -07:00
|
|
|
|
// Check both spell slots directly and macro slots via their primary spell.
|
|
|
|
|
|
{
|
|
|
|
|
|
uint32_t flashSpellId = 0;
|
|
|
|
|
|
if (slot.type == game::ActionBarSlot::SPELL && slot.id != 0)
|
|
|
|
|
|
flashSpellId = slot.id;
|
|
|
|
|
|
else if (slot.type == game::ActionBarSlot::MACRO && slot.id != 0)
|
|
|
|
|
|
flashSpellId = resolveMacroPrimarySpellId(slot.id, gameHandler);
|
|
|
|
|
|
auto flashIt = (flashSpellId != 0) ? actionFlashEndTimes_.find(flashSpellId) : actionFlashEndTimes_.end();
|
2026-03-18 04:30:33 -07:00
|
|
|
|
if (flashIt != actionFlashEndTimes_.end()) {
|
|
|
|
|
|
float now = static_cast<float>(ImGui::GetTime());
|
|
|
|
|
|
float remaining = flashIt->second - now;
|
|
|
|
|
|
if (remaining > 0.0f) {
|
|
|
|
|
|
float alpha = remaining / kActionFlashDuration; // 1→0
|
|
|
|
|
|
ImVec2 rMin = ImGui::GetItemRectMin();
|
|
|
|
|
|
ImVec2 rMax = ImGui::GetItemRectMax();
|
|
|
|
|
|
ImGui::GetWindowDrawList()->AddRectFilled(
|
|
|
|
|
|
rMin, rMax,
|
|
|
|
|
|
ImGui::ColorConvertFloat4ToU32(ImVec4(1.0f, 0.1f, 0.1f, 0.55f * alpha)));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
actionFlashEndTimes_.erase(flashIt);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 06:04:43 -07:00
|
|
|
|
bool hoveredOnRelease = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem) &&
|
|
|
|
|
|
ImGui::IsMouseReleased(ImGuiMouseButton_Left);
|
|
|
|
|
|
|
|
|
|
|
|
if (hoveredOnRelease && spellbookScreen.isDraggingSpell()) {
|
|
|
|
|
|
gameHandler.setActionBarSlot(absSlot, game::ActionBarSlot::SPELL,
|
|
|
|
|
|
spellbookScreen.getDragSpellId());
|
|
|
|
|
|
spellbookScreen.consumeDragSpell();
|
|
|
|
|
|
} else if (hoveredOnRelease && inventoryScreen.isHoldingItem()) {
|
|
|
|
|
|
const auto& held = inventoryScreen.getHeldItem();
|
|
|
|
|
|
gameHandler.setActionBarSlot(absSlot, game::ActionBarSlot::ITEM, held.itemId);
|
|
|
|
|
|
inventoryScreen.returnHeldItem(gameHandler.getInventory());
|
|
|
|
|
|
} else if (clicked && actionBarDragSlot_ >= 0) {
|
|
|
|
|
|
if (absSlot != actionBarDragSlot_) {
|
|
|
|
|
|
const auto& dragSrc = bar[actionBarDragSlot_];
|
|
|
|
|
|
gameHandler.setActionBarSlot(actionBarDragSlot_, slot.type, slot.id);
|
|
|
|
|
|
gameHandler.setActionBarSlot(absSlot, dragSrc.type, dragSrc.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
actionBarDragSlot_ = -1;
|
|
|
|
|
|
actionBarDragIcon_ = 0;
|
|
|
|
|
|
} else if (clicked && !slot.isEmpty()) {
|
|
|
|
|
|
if (slot.type == game::ActionBarSlot::SPELL && slot.isReady()) {
|
2026-03-28 14:55:58 -07:00
|
|
|
|
// Check if this spell belongs to an item (e.g., Hearthstone spell 8690).
|
|
|
|
|
|
// Item-use spells must go through CMSG_USE_ITEM, not CMSG_CAST_SPELL.
|
|
|
|
|
|
uint32_t itemForSpell = gameHandler.getItemIdForSpell(slot.id);
|
|
|
|
|
|
if (itemForSpell != 0) {
|
|
|
|
|
|
gameHandler.useItemById(itemForSpell);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
|
|
|
|
|
gameHandler.castSpell(slot.id, target);
|
|
|
|
|
|
}
|
2026-03-10 06:04:43 -07:00
|
|
|
|
} else if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) {
|
|
|
|
|
|
gameHandler.useItemById(slot.id);
|
2026-03-18 02:07:59 -07:00
|
|
|
|
} else if (slot.type == game::ActionBarSlot::MACRO) {
|
2026-03-31 08:53:14 +03:00
|
|
|
|
chatPanel_.executeMacroText(gameHandler, inventoryScreen, spellbookScreen, questLogScreen, gameHandler.getMacroText(slot.id));
|
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-11 23:59:51 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Right-click context menu for non-empty slots
|
|
|
|
|
|
if (!slot.isEmpty()) {
|
|
|
|
|
|
// Use a unique popup ID per slot so multiple slots don't share state
|
|
|
|
|
|
char ctxId[32];
|
|
|
|
|
|
snprintf(ctxId, sizeof(ctxId), "##ABCtx%d", absSlot);
|
|
|
|
|
|
if (ImGui::BeginPopupContextItem(ctxId)) {
|
|
|
|
|
|
if (slot.type == game::ActionBarSlot::SPELL) {
|
|
|
|
|
|
std::string spellName = getSpellName(slot.id);
|
|
|
|
|
|
ImGui::TextDisabled("%s", spellName.c_str());
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (onCooldown) ImGui::BeginDisabled();
|
|
|
|
|
|
if (ImGui::MenuItem("Cast")) {
|
|
|
|
|
|
uint64_t target = gameHandler.hasTarget() ? gameHandler.getTargetGuid() : 0;
|
|
|
|
|
|
gameHandler.castSpell(slot.id, target);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (onCooldown) ImGui::EndDisabled();
|
|
|
|
|
|
} else if (slot.type == game::ActionBarSlot::ITEM) {
|
|
|
|
|
|
const char* iName = (barItemDef && !barItemDef->name.empty())
|
|
|
|
|
|
? barItemDef->name.c_str()
|
|
|
|
|
|
: (!itemNameFromQuery.empty() ? itemNameFromQuery.c_str() : "Item");
|
|
|
|
|
|
ImGui::TextDisabled("%s", iName);
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Use")) {
|
|
|
|
|
|
gameHandler.useItemById(slot.id);
|
|
|
|
|
|
}
|
2026-03-18 01:42:07 -07:00
|
|
|
|
} else if (slot.type == game::ActionBarSlot::MACRO) {
|
|
|
|
|
|
ImGui::TextDisabled("Macro #%u", slot.id);
|
2026-03-18 02:07:59 -07:00
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Execute")) {
|
2026-03-31 08:53:14 +03:00
|
|
|
|
chatPanel_.executeMacroText(gameHandler, inventoryScreen, spellbookScreen, questLogScreen, gameHandler.getMacroText(slot.id));
|
2026-03-18 02:07:59 -07:00
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Edit")) {
|
|
|
|
|
|
const std::string& txt = gameHandler.getMacroText(slot.id);
|
|
|
|
|
|
strncpy(macroEditorBuf_, txt.c_str(), sizeof(macroEditorBuf_) - 1);
|
|
|
|
|
|
macroEditorBuf_[sizeof(macroEditorBuf_) - 1] = '\0';
|
|
|
|
|
|
macroEditorId_ = slot.id;
|
|
|
|
|
|
macroEditorOpen_ = true;
|
|
|
|
|
|
}
|
2026-03-11 23:59:51 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Clear Slot")) {
|
|
|
|
|
|
gameHandler.setActionBarSlot(absSlot, game::ActionBarSlot::EMPTY, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
2026-03-10 06:04:43 -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-10 06:04:43 -07:00
|
|
|
|
// Tooltip
|
|
|
|
|
|
if (ImGui::IsItemHovered() && !slot.isEmpty() && slot.id != 0) {
|
|
|
|
|
|
if (slot.type == game::ActionBarSlot::SPELL) {
|
2026-03-10 19:31:46 -07:00
|
|
|
|
// Use the spellbook's rich tooltip (school, cost, cast time, range, description).
|
|
|
|
|
|
// Falls back to the simple name if DBC data isn't loaded yet.
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
bool richOk = spellbookScreen.renderSpellInfoTooltip(slot.id, gameHandler, assetMgr);
|
|
|
|
|
|
if (!richOk) {
|
|
|
|
|
|
ImGui::Text("%s", getSpellName(slot.id).c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
// Hearthstone: add location note after the spell tooltip body
|
2026-03-10 06:04:43 -07:00
|
|
|
|
if (slot.id == 8690) {
|
|
|
|
|
|
uint32_t mapId = 0; glm::vec3 pos;
|
|
|
|
|
|
if (gameHandler.getHomeBind(mapId, pos)) {
|
2026-03-13 10:18:31 -07:00
|
|
|
|
std::string homeLocation;
|
|
|
|
|
|
// Zone name (from zoneId stored in bind point)
|
|
|
|
|
|
uint32_t zoneId = gameHandler.getHomeBindZoneId();
|
|
|
|
|
|
if (zoneId != 0) {
|
|
|
|
|
|
homeLocation = gameHandler.getWhoAreaName(zoneId);
|
2026-02-17 03:50:36 -08:00
|
|
|
|
}
|
2026-03-13 10:18:31 -07:00
|
|
|
|
// Fall back to continent name if zone unavailable
|
|
|
|
|
|
if (homeLocation.empty()) {
|
|
|
|
|
|
switch (mapId) {
|
|
|
|
|
|
case 0: homeLocation = "Eastern Kingdoms"; break;
|
|
|
|
|
|
case 1: homeLocation = "Kalimdor"; break;
|
|
|
|
|
|
case 530: homeLocation = "Outland"; break;
|
|
|
|
|
|
case 571: homeLocation = "Northrend"; break;
|
|
|
|
|
|
default: homeLocation = "Unknown"; break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f),
|
|
|
|
|
|
"Home: %s", homeLocation.c_str());
|
2026-02-17 03:50:36 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 05:57:45 -07:00
|
|
|
|
if (outOfRange) {
|
2026-03-27 10:20:45 -07:00
|
|
|
|
ImGui::TextColored(colors::kHostileRed, "Out of range");
|
2026-03-12 05:57:45 -07:00
|
|
|
|
}
|
2026-03-12 06:01:42 -07:00
|
|
|
|
if (insufficientPower) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.75f, 0.55f, 1.0f, 1.0f), "Not enough power");
|
|
|
|
|
|
}
|
2026-03-10 19:31:46 -07:00
|
|
|
|
if (onCooldown) {
|
|
|
|
|
|
float cd = slot.cooldownRemaining;
|
|
|
|
|
|
if (cd >= 60.0f)
|
2026-03-25 11:57:22 -07:00
|
|
|
|
ImGui::TextColored(kColorRed,
|
2026-03-25 11:40:49 -07:00
|
|
|
|
"Cooldown: %d min %d sec", static_cast<int>(cd)/60, static_cast<int>(cd)%60);
|
2026-03-10 19:31:46 -07:00
|
|
|
|
else
|
2026-03-25 11:57:22 -07:00
|
|
|
|
ImGui::TextColored(kColorRed, "Cooldown: %.1f sec", cd);
|
2026-03-10 19:31:46 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTooltip();
|
2026-03-18 01:42:07 -07:00
|
|
|
|
} else if (slot.type == game::ActionBarSlot::MACRO) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
2026-03-20 08:18:28 -07:00
|
|
|
|
// Show the primary spell's rich tooltip (like WoW does for macro buttons)
|
|
|
|
|
|
uint32_t macroSpellId = resolveMacroPrimarySpellId(slot.id, gameHandler);
|
|
|
|
|
|
bool showedRich = false;
|
|
|
|
|
|
if (macroSpellId != 0) {
|
|
|
|
|
|
showedRich = spellbookScreen.renderSpellInfoTooltip(macroSpellId, gameHandler, assetMgr);
|
|
|
|
|
|
if (onCooldown && macroCooldownRemaining > 0.0f) {
|
|
|
|
|
|
float cd = macroCooldownRemaining;
|
|
|
|
|
|
if (cd >= 60.0f)
|
2026-03-25 11:57:22 -07:00
|
|
|
|
ImGui::TextColored(kColorRed,
|
2026-03-25 11:40:49 -07:00
|
|
|
|
"Cooldown: %d min %d sec", static_cast<int>(cd)/60, static_cast<int>(cd)%60);
|
2026-03-20 08:18:28 -07:00
|
|
|
|
else
|
2026-03-25 11:57:22 -07:00
|
|
|
|
ImGui::TextColored(kColorRed, "Cooldown: %.1f sec", cd);
|
2026-03-20 08:18:28 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!showedRich) {
|
2026-03-20 10:12:42 -07:00
|
|
|
|
// For /use macros: try showing the item tooltip instead
|
|
|
|
|
|
if (macroIsUseCmd) {
|
|
|
|
|
|
const std::string& macroText = gameHandler.getMacroText(slot.id);
|
|
|
|
|
|
// Extract item name from first /use command
|
|
|
|
|
|
for (const auto& cmd : allMacroCommands(macroText)) {
|
|
|
|
|
|
std::string cl = cmd;
|
|
|
|
|
|
for (char& c : cl) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
if (cl.rfind("/use ", 0) != 0) continue;
|
|
|
|
|
|
size_t sp = cmd.find(' ');
|
|
|
|
|
|
if (sp == std::string::npos) continue;
|
|
|
|
|
|
std::string itemArg = cmd.substr(sp + 1);
|
|
|
|
|
|
while (!itemArg.empty() && itemArg.front() == ' ') itemArg.erase(itemArg.begin());
|
|
|
|
|
|
while (!itemArg.empty() && itemArg.back() == ' ') itemArg.pop_back();
|
|
|
|
|
|
std::string itemLow = itemArg;
|
|
|
|
|
|
for (char& c : itemLow) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
for (const auto& [entry, info] : gameHandler.getItemInfoCache()) {
|
|
|
|
|
|
if (!info.valid) continue;
|
|
|
|
|
|
std::string iName = info.name;
|
|
|
|
|
|
for (char& c : iName) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
if (iName == itemLow) {
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(info);
|
|
|
|
|
|
showedRich = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!showedRich) {
|
|
|
|
|
|
ImGui::Text("Macro #%u", slot.id);
|
|
|
|
|
|
const std::string& macroText = gameHandler.getMacroText(slot.id);
|
|
|
|
|
|
if (!macroText.empty()) {
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextUnformatted(macroText.c_str());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextDisabled("(no text — right-click to Edit)");
|
|
|
|
|
|
}
|
2026-03-20 08:18:28 -07:00
|
|
|
|
}
|
2026-03-18 02:07:59 -07:00
|
|
|
|
}
|
2026-03-18 01:42:07 -07:00
|
|
|
|
ImGui::EndTooltip();
|
2026-03-10 06:04:43 -07:00
|
|
|
|
} else if (slot.type == game::ActionBarSlot::ITEM) {
|
2026-03-10 19:31:46 -07:00
|
|
|
|
ImGui::BeginTooltip();
|
2026-03-12 13:14:24 -07:00
|
|
|
|
// Prefer full rich tooltip from ItemQueryResponseData (has stats, quality, set info)
|
|
|
|
|
|
const auto* itemQueryInfo = gameHandler.getItemInfo(slot.id);
|
|
|
|
|
|
if (itemQueryInfo && itemQueryInfo->valid) {
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*itemQueryInfo);
|
|
|
|
|
|
} else if (barItemDef && !barItemDef->name.empty()) {
|
2026-03-10 06:04:43 -07:00
|
|
|
|
ImGui::Text("%s", barItemDef->name.c_str());
|
2026-03-12 13:14:24 -07:00
|
|
|
|
} else if (!itemNameFromQuery.empty()) {
|
2026-03-10 06:04:43 -07:00
|
|
|
|
ImGui::Text("%s", itemNameFromQuery.c_str());
|
2026-03-12 13:14:24 -07:00
|
|
|
|
} else {
|
2026-03-10 06:04:43 -07:00
|
|
|
|
ImGui::Text("Item #%u", slot.id);
|
2026-03-12 13:14:24 -07:00
|
|
|
|
}
|
2026-03-10 19:31:46 -07:00
|
|
|
|
if (onCooldown) {
|
|
|
|
|
|
float cd = slot.cooldownRemaining;
|
|
|
|
|
|
if (cd >= 60.0f)
|
2026-03-25 11:57:22 -07:00
|
|
|
|
ImGui::TextColored(kColorRed,
|
2026-03-25 11:40:49 -07:00
|
|
|
|
"Cooldown: %d min %d sec", static_cast<int>(cd)/60, static_cast<int>(cd)%60);
|
2026-03-10 19:31:46 -07:00
|
|
|
|
else
|
2026-03-25 11:57:22 -07:00
|
|
|
|
ImGui::TextColored(kColorRed, "Cooldown: %.1f sec", cd);
|
2026-03-10 19:31:46 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTooltip();
|
2026-02-05 15:07:13 -08:00
|
|
|
|
}
|
2026-03-10 06:04:43 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Cooldown overlay: WoW-style clock-sweep + time text
|
|
|
|
|
|
if (onCooldown) {
|
|
|
|
|
|
ImVec2 btnMin = ImGui::GetItemRectMin();
|
|
|
|
|
|
ImVec2 btnMax = ImGui::GetItemRectMax();
|
|
|
|
|
|
float cx = (btnMin.x + btnMax.x) * 0.5f;
|
|
|
|
|
|
float cy = (btnMin.y + btnMax.y) * 0.5f;
|
|
|
|
|
|
float r = (btnMax.x - btnMin.x) * 0.5f;
|
|
|
|
|
|
auto* dl = ImGui::GetWindowDrawList();
|
|
|
|
|
|
|
2026-03-20 08:07:20 -07:00
|
|
|
|
// For macros, use the resolved primary spell cooldown instead of the slot's own.
|
|
|
|
|
|
float effCdTotal = (macroCooldownTotal > 0.0f) ? macroCooldownTotal : slot.cooldownTotal;
|
|
|
|
|
|
float effCdRemaining = (macroCooldownRemaining > 0.0f) ? macroCooldownRemaining : slot.cooldownRemaining;
|
|
|
|
|
|
float total = (effCdTotal > 0.0f) ? effCdTotal : 1.0f;
|
|
|
|
|
|
float elapsed = total - effCdRemaining;
|
2026-03-10 06:04:43 -07:00
|
|
|
|
float elapsedFrac = std::min(1.0f, std::max(0.0f, elapsed / total));
|
|
|
|
|
|
if (elapsedFrac > 0.005f) {
|
|
|
|
|
|
constexpr int N_SEGS = 32;
|
|
|
|
|
|
float startAngle = -IM_PI * 0.5f;
|
|
|
|
|
|
float endAngle = startAngle + elapsedFrac * 2.0f * IM_PI;
|
|
|
|
|
|
float fanR = r * 1.5f;
|
|
|
|
|
|
ImVec2 pts[N_SEGS + 2];
|
|
|
|
|
|
pts[0] = ImVec2(cx, cy);
|
|
|
|
|
|
for (int s = 0; s <= N_SEGS; ++s) {
|
|
|
|
|
|
float a = startAngle + (endAngle - startAngle) * s / static_cast<float>(N_SEGS);
|
|
|
|
|
|
pts[s + 1] = ImVec2(cx + std::cos(a) * fanR, cy + std::sin(a) * fanR);
|
|
|
|
|
|
}
|
|
|
|
|
|
dl->AddConvexPolyFilled(pts, N_SEGS + 2, IM_COL32(0, 0, 0, 170));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
char cdText[16];
|
2026-03-20 08:07:20 -07:00
|
|
|
|
float cd = effCdRemaining;
|
2026-03-25 11:40:49 -07:00
|
|
|
|
if (cd >= 3600.0f) snprintf(cdText, sizeof(cdText), "%dh", static_cast<int>(cd) / 3600);
|
|
|
|
|
|
else if (cd >= 60.0f) snprintf(cdText, sizeof(cdText), "%dm%ds", static_cast<int>(cd) / 60, static_cast<int>(cd) % 60);
|
|
|
|
|
|
else if (cd >= 5.0f) snprintf(cdText, sizeof(cdText), "%ds", static_cast<int>(cd));
|
2026-03-12 03:48:12 -07:00
|
|
|
|
else snprintf(cdText, sizeof(cdText), "%.1f", cd);
|
2026-03-10 06:04:43 -07:00
|
|
|
|
ImVec2 textSize = ImGui::CalcTextSize(cdText);
|
|
|
|
|
|
float tx = cx - textSize.x * 0.5f;
|
|
|
|
|
|
float ty = cy - textSize.y * 0.5f;
|
|
|
|
|
|
dl->AddText(ImVec2(tx + 1.0f, ty + 1.0f), IM_COL32(0, 0, 0, 220), cdText);
|
|
|
|
|
|
dl->AddText(ImVec2(tx, ty), IM_COL32(255, 255, 255, 255), cdText);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 05:38:13 -07:00
|
|
|
|
// GCD overlay — subtle dark fan sweep (thinner/lighter than regular cooldown)
|
|
|
|
|
|
if (onGCD) {
|
|
|
|
|
|
ImVec2 btnMin = ImGui::GetItemRectMin();
|
|
|
|
|
|
ImVec2 btnMax = ImGui::GetItemRectMax();
|
|
|
|
|
|
float cx = (btnMin.x + btnMax.x) * 0.5f;
|
|
|
|
|
|
float cy = (btnMin.y + btnMax.y) * 0.5f;
|
|
|
|
|
|
float r = (btnMax.x - btnMin.x) * 0.5f;
|
|
|
|
|
|
auto* dl = ImGui::GetWindowDrawList();
|
|
|
|
|
|
float gcdRem = gameHandler.getGCDRemaining();
|
|
|
|
|
|
float gcdTotal = gameHandler.getGCDTotal();
|
|
|
|
|
|
if (gcdTotal > 0.0f) {
|
|
|
|
|
|
float elapsed = gcdTotal - gcdRem;
|
|
|
|
|
|
float elapsedFrac = std::min(1.0f, std::max(0.0f, elapsed / gcdTotal));
|
|
|
|
|
|
if (elapsedFrac > 0.005f) {
|
|
|
|
|
|
constexpr int N_SEGS = 24;
|
|
|
|
|
|
float startAngle = -IM_PI * 0.5f;
|
|
|
|
|
|
float endAngle = startAngle + elapsedFrac * 2.0f * IM_PI;
|
|
|
|
|
|
float fanR = r * 1.4f;
|
|
|
|
|
|
ImVec2 pts[N_SEGS + 2];
|
|
|
|
|
|
pts[0] = ImVec2(cx, cy);
|
|
|
|
|
|
for (int s = 0; s <= N_SEGS; ++s) {
|
|
|
|
|
|
float a = startAngle + (endAngle - startAngle) * s / static_cast<float>(N_SEGS);
|
|
|
|
|
|
pts[s + 1] = ImVec2(cx + std::cos(a) * fanR, cy + std::sin(a) * fanR);
|
|
|
|
|
|
}
|
|
|
|
|
|
dl->AddConvexPolyFilled(pts, N_SEGS + 2, IM_COL32(0, 0, 0, 110));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:32:15 -07:00
|
|
|
|
// Auto-attack active glow — pulsing golden border when slot 6603 (Attack) is toggled on
|
|
|
|
|
|
if (slot.type == game::ActionBarSlot::SPELL && slot.id == 6603
|
|
|
|
|
|
&& gameHandler.isAutoAttacking()) {
|
|
|
|
|
|
ImVec2 bMin = ImGui::GetItemRectMin();
|
|
|
|
|
|
ImVec2 bMax = ImGui::GetItemRectMax();
|
|
|
|
|
|
float pulse = 0.55f + 0.45f * std::sin(static_cast<float>(ImGui::GetTime()) * 5.0f);
|
|
|
|
|
|
ImU32 glowCol = IM_COL32(
|
|
|
|
|
|
static_cast<int>(255),
|
|
|
|
|
|
static_cast<int>(200 * pulse),
|
|
|
|
|
|
static_cast<int>(0),
|
|
|
|
|
|
static_cast<int>(200 * pulse));
|
|
|
|
|
|
ImGui::GetWindowDrawList()->AddRect(bMin, bMax, glowCol, 2.0f, 0, 2.5f);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 04:12:07 -07:00
|
|
|
|
// Item stack count overlay — bottom-right corner of icon
|
|
|
|
|
|
if (slot.type == game::ActionBarSlot::ITEM && slot.id != 0) {
|
|
|
|
|
|
// Count total of this item across all inventory slots
|
|
|
|
|
|
auto& inv = gameHandler.getInventory();
|
|
|
|
|
|
int totalCount = 0;
|
|
|
|
|
|
for (int bi = 0; bi < inv.getBackpackSize(); bi++) {
|
|
|
|
|
|
const auto& bs = inv.getBackpackSlot(bi);
|
|
|
|
|
|
if (!bs.empty() && bs.item.itemId == slot.id) totalCount += bs.item.stackCount;
|
|
|
|
|
|
}
|
|
|
|
|
|
for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS; bag++) {
|
|
|
|
|
|
for (int si = 0; si < inv.getBagSize(bag); si++) {
|
|
|
|
|
|
const auto& bs = inv.getBagSlot(bag, si);
|
|
|
|
|
|
if (!bs.empty() && bs.item.itemId == slot.id) totalCount += bs.item.stackCount;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (totalCount > 0) {
|
2026-03-20 15:53:43 -07:00
|
|
|
|
char countStr[16];
|
2026-03-12 04:12:07 -07:00
|
|
|
|
snprintf(countStr, sizeof(countStr), "%d", totalCount);
|
|
|
|
|
|
ImVec2 btnMax = ImGui::GetItemRectMax();
|
|
|
|
|
|
ImVec2 tsz = ImGui::CalcTextSize(countStr);
|
|
|
|
|
|
float cx2 = btnMax.x - tsz.x - 2.0f;
|
|
|
|
|
|
float cy2 = btnMax.y - tsz.y - 1.0f;
|
|
|
|
|
|
auto* cdl = ImGui::GetWindowDrawList();
|
|
|
|
|
|
cdl->AddText(ImVec2(cx2 + 1.0f, cy2 + 1.0f), IM_COL32(0, 0, 0, 200), countStr);
|
|
|
|
|
|
cdl->AddText(ImVec2(cx2, cy2),
|
|
|
|
|
|
totalCount <= 1 ? IM_COL32(220, 100, 100, 255) : IM_COL32(255, 255, 255, 255),
|
|
|
|
|
|
countStr);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 05:12:58 -07:00
|
|
|
|
// Ready glow: animate a gold border for ~1.5s when a cooldown just expires
|
|
|
|
|
|
{
|
|
|
|
|
|
static std::unordered_map<int, float> slotGlowTimers; // absSlot -> remaining glow seconds
|
|
|
|
|
|
static std::unordered_map<int, bool> slotWasOnCooldown; // absSlot -> last frame state
|
|
|
|
|
|
|
|
|
|
|
|
float dt = ImGui::GetIO().DeltaTime;
|
|
|
|
|
|
bool wasOnCd = slotWasOnCooldown.count(absSlot) ? slotWasOnCooldown[absSlot] : false;
|
|
|
|
|
|
|
|
|
|
|
|
// Trigger glow when transitioning from on-cooldown to ready (and slot isn't empty)
|
|
|
|
|
|
if (wasOnCd && !onCooldown && !slot.isEmpty()) {
|
|
|
|
|
|
slotGlowTimers[absSlot] = 1.5f;
|
|
|
|
|
|
}
|
|
|
|
|
|
slotWasOnCooldown[absSlot] = onCooldown;
|
|
|
|
|
|
|
|
|
|
|
|
auto git = slotGlowTimers.find(absSlot);
|
|
|
|
|
|
if (git != slotGlowTimers.end() && git->second > 0.0f) {
|
|
|
|
|
|
git->second -= dt;
|
|
|
|
|
|
float t = git->second / 1.5f; // 1.0 → 0.0 over lifetime
|
|
|
|
|
|
// Pulse: bright when fresh, fading out
|
|
|
|
|
|
float pulse = std::sin(t * IM_PI * 4.0f) * 0.5f + 0.5f; // 4 pulses
|
|
|
|
|
|
uint8_t alpha = static_cast<uint8_t>(200 * t * (0.5f + 0.5f * pulse));
|
|
|
|
|
|
if (alpha > 0) {
|
|
|
|
|
|
ImVec2 bMin = ImGui::GetItemRectMin();
|
|
|
|
|
|
ImVec2 bMax = ImGui::GetItemRectMax();
|
|
|
|
|
|
auto* gdl = ImGui::GetWindowDrawList();
|
|
|
|
|
|
// Gold glow border (2px inset, 3px thick)
|
|
|
|
|
|
gdl->AddRect(ImVec2(bMin.x - 2, bMin.y - 2),
|
|
|
|
|
|
ImVec2(bMax.x + 2, bMax.y + 2),
|
|
|
|
|
|
IM_COL32(255, 215, 0, alpha), 3.0f, 0, 3.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (git->second <= 0.0f) slotGlowTimers.erase(git);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 06:04:43 -07:00
|
|
|
|
// Key label below
|
|
|
|
|
|
ImGui::TextDisabled("%s", keyLabel);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
ImGui::EndGroup();
|
|
|
|
|
|
};
|
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-10 06:04:43 -07:00
|
|
|
|
// Bar 2 (slots 12-23) — only show if at least one slot is populated
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (settingsPanel_.pendingShowActionBar2) {
|
2026-03-10 06:04:43 -07:00
|
|
|
|
bool bar2HasContent = false;
|
|
|
|
|
|
for (int i = 0; i < game::GameHandler::SLOTS_PER_BAR; ++i)
|
|
|
|
|
|
if (!bar[game::GameHandler::SLOTS_PER_BAR + i].isEmpty()) { bar2HasContent = true; break; }
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
float bar2X = barX + settingsPanel_.pendingActionBar2OffsetX;
|
|
|
|
|
|
float bar2Y = barY - barH - 2.0f + settingsPanel_.pendingActionBar2OffsetY;
|
2026-03-10 15:45:35 -07:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(bar2X, bar2Y), ImGuiCond_Always);
|
2026-03-10 06:04:43 -07:00
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(barW, barH), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(padding, padding));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg,
|
|
|
|
|
|
bar2HasContent ? ImVec4(0.05f, 0.05f, 0.05f, 0.85f) : ImVec4(0.05f, 0.05f, 0.05f, 0.4f));
|
|
|
|
|
|
if (ImGui::Begin("##ActionBar2", nullptr, flags)) {
|
|
|
|
|
|
for (int i = 0; i < game::GameHandler::SLOTS_PER_BAR; ++i) {
|
|
|
|
|
|
if (i > 0) ImGui::SameLine(0, spacing);
|
|
|
|
|
|
renderBarSlot(game::GameHandler::SLOTS_PER_BAR + i, keyLabels2[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::PopStyleVar(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-10 06:04:43 -07:00
|
|
|
|
// Bar 1 (slots 0-11)
|
|
|
|
|
|
if (ImGui::Begin("##ActionBar", nullptr, flags)) {
|
|
|
|
|
|
for (int i = 0; i < game::GameHandler::SLOTS_PER_BAR; ++i) {
|
|
|
|
|
|
if (i > 0) ImGui::SameLine(0, spacing);
|
|
|
|
|
|
renderBarSlot(i, keyLabels1[i]);
|
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 02:07:59 -07:00
|
|
|
|
|
|
|
|
|
|
// Macro editor modal — opened by "Edit" in action bar context menus
|
|
|
|
|
|
if (macroEditorOpen_) {
|
|
|
|
|
|
ImGui::OpenPopup("Edit Macro###MacroEdit");
|
|
|
|
|
|
macroEditorOpen_ = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::BeginPopupModal("Edit Macro###MacroEdit", nullptr,
|
|
|
|
|
|
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse)) {
|
2026-03-18 03:11:34 -07:00
|
|
|
|
ImGui::Text("Macro #%u (all lines execute; [cond] Spell; Default supported)", macroEditorId_);
|
2026-03-18 02:07:59 -07:00
|
|
|
|
ImGui::SetNextItemWidth(320.0f);
|
|
|
|
|
|
ImGui::InputTextMultiline("##MacroText", macroEditorBuf_, sizeof(macroEditorBuf_),
|
|
|
|
|
|
ImVec2(320.0f, 80.0f));
|
|
|
|
|
|
if (ImGui::Button("Save")) {
|
|
|
|
|
|
gameHandler.setMacroText(macroEditorId_, std::string(macroEditorBuf_));
|
2026-03-20 08:11:13 -07:00
|
|
|
|
macroPrimarySpellCache_.clear(); // invalidate resolved spell IDs
|
2026-03-18 02:07:59 -07:00
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Cancel")) {
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
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();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleColor();
|
Fix PopStyleVar mismatches and character geoset IDs
Fix 9 PopStyleVar(2) calls that should be PopStyleVar(1) across
player frame, target frame, cast bar, party frames, buff bar, escape
menu, death dialog, and resurrect dialog. Fix action bar from
PopStyleVar(2) to PopStyleVar(4) to match 4 pushes.
Fix character geoset defaults: 301→302 (bare hands), 701→702 (ears),
1501→1502 (back/cloak), add 802 (wristbands). No WoW character model
uses geoset 301/701/1501; all use 302/702/1502 as base. This fixes
missing hands/arms on undead and other races with separate hand meshes.
2026-02-15 06:09:38 -08:00
|
|
|
|
ImGui::PopStyleVar(4);
|
2026-02-06 19:24:44 -08:00
|
|
|
|
|
2026-03-10 15:56:41 -07:00
|
|
|
|
// Right side vertical bar (bar 3, slots 24-35)
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (settingsPanel_.pendingShowRightBar) {
|
2026-03-10 15:56:41 -07:00
|
|
|
|
bool bar3HasContent = false;
|
|
|
|
|
|
for (int i = 0; i < game::GameHandler::SLOTS_PER_BAR; ++i)
|
|
|
|
|
|
if (!bar[game::GameHandler::SLOTS_PER_BAR * 2 + i].isEmpty()) { bar3HasContent = true; break; }
|
|
|
|
|
|
|
|
|
|
|
|
float sideBarW = slotSize + padding * 2;
|
|
|
|
|
|
float sideBarH = game::GameHandler::SLOTS_PER_BAR * slotSize + (game::GameHandler::SLOTS_PER_BAR - 1) * spacing + padding * 2;
|
|
|
|
|
|
float sideBarX = screenW - sideBarW - 4.0f;
|
2026-03-31 10:07:58 +03:00
|
|
|
|
float sideBarY = (screenH - sideBarH) / 2.0f + settingsPanel_.pendingRightBarOffsetY;
|
2026-03-10 15:56:41 -07:00
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(sideBarX, sideBarY), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(sideBarW, sideBarH), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(padding, padding));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg,
|
|
|
|
|
|
bar3HasContent ? ImVec4(0.05f, 0.05f, 0.05f, 0.85f) : ImVec4(0.05f, 0.05f, 0.05f, 0.4f));
|
|
|
|
|
|
if (ImGui::Begin("##ActionBarRight", nullptr, flags)) {
|
|
|
|
|
|
for (int i = 0; i < game::GameHandler::SLOTS_PER_BAR; ++i) {
|
|
|
|
|
|
renderBarSlot(game::GameHandler::SLOTS_PER_BAR * 2 + i, "");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::PopStyleVar(4);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Left side vertical bar (bar 4, slots 36-47)
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (settingsPanel_.pendingShowLeftBar) {
|
2026-03-10 15:56:41 -07:00
|
|
|
|
bool bar4HasContent = false;
|
|
|
|
|
|
for (int i = 0; i < game::GameHandler::SLOTS_PER_BAR; ++i)
|
|
|
|
|
|
if (!bar[game::GameHandler::SLOTS_PER_BAR * 3 + i].isEmpty()) { bar4HasContent = true; break; }
|
|
|
|
|
|
|
|
|
|
|
|
float sideBarW = slotSize + padding * 2;
|
|
|
|
|
|
float sideBarH = game::GameHandler::SLOTS_PER_BAR * slotSize + (game::GameHandler::SLOTS_PER_BAR - 1) * spacing + padding * 2;
|
|
|
|
|
|
float sideBarX = 4.0f;
|
2026-03-31 10:07:58 +03:00
|
|
|
|
float sideBarY = (screenH - sideBarH) / 2.0f + settingsPanel_.pendingLeftBarOffsetY;
|
2026-03-10 15:56:41 -07:00
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(sideBarX, sideBarY), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(sideBarW, sideBarH), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(padding, padding));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg,
|
|
|
|
|
|
bar4HasContent ? ImVec4(0.05f, 0.05f, 0.05f, 0.85f) : ImVec4(0.05f, 0.05f, 0.05f, 0.4f));
|
|
|
|
|
|
if (ImGui::Begin("##ActionBarLeft", nullptr, flags)) {
|
|
|
|
|
|
for (int i = 0; i < game::GameHandler::SLOTS_PER_BAR; ++i) {
|
|
|
|
|
|
renderBarSlot(game::GameHandler::SLOTS_PER_BAR * 3 + i, "");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::PopStyleVar(4);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 17:25:00 -07:00
|
|
|
|
// Vehicle exit button (WotLK): floating button above action bar when player is in a vehicle
|
|
|
|
|
|
if (gameHandler.isInVehicle()) {
|
|
|
|
|
|
const float btnW = 120.0f;
|
|
|
|
|
|
const float btnH = 32.0f;
|
|
|
|
|
|
const float btnX = (screenW - btnW) / 2.0f;
|
|
|
|
|
|
const float btnY = barY - btnH - 6.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(btnX, btnY), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(btnW, btnH), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(4.0f, 4.0f));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
|
|
|
|
|
|
ImGuiWindowFlags vFlags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoBackground;
|
|
|
|
|
|
if (ImGui::Begin("##VehicleExit", nullptr, vFlags)) {
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.1f, 0.1f, 0.9f));
|
refactor: add 9 color constants, replace 36 more inline literals
New constants in ui_colors.hpp:
- Power types: kEnergyYellow, kHappinessGreen, kRunicRed, kSoulShardPurple
- UI elements: kInactiveGray, kVeryLightGray, kSymbolGold, kLowHealthRed, kDangerRed
Replacements across game_screen(30), inventory_screen(5), character_screen(1).
2026-03-27 14:05:32 -07:00
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::kLowHealthRed);
|
2026-03-12 17:25:00 -07:00
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.4f, 0.0f, 0.0f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f);
|
|
|
|
|
|
if (ImGui::Button("Leave Vehicle", ImVec2(btnW - 8.0f, btnH - 8.0f))) {
|
|
|
|
|
|
gameHandler.sendRequestVehicleExit();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
ImGui::PopStyleColor(3);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::PopStyleVar(3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 19:24:44 -08:00
|
|
|
|
// Handle action bar drag: render icon at cursor and detect drop outside
|
|
|
|
|
|
if (actionBarDragSlot_ >= 0) {
|
|
|
|
|
|
ImVec2 mousePos = ImGui::GetMousePos();
|
|
|
|
|
|
|
|
|
|
|
|
// Draw dragged icon at cursor
|
|
|
|
|
|
if (actionBarDragIcon_) {
|
|
|
|
|
|
ImGui::GetForegroundDrawList()->AddImage(
|
|
|
|
|
|
(ImTextureID)(uintptr_t)actionBarDragIcon_,
|
|
|
|
|
|
ImVec2(mousePos.x - 20, mousePos.y - 20),
|
|
|
|
|
|
ImVec2(mousePos.x + 20, mousePos.y + 20));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::GetForegroundDrawList()->AddRectFilled(
|
|
|
|
|
|
ImVec2(mousePos.x - 20, mousePos.y - 20),
|
|
|
|
|
|
ImVec2(mousePos.x + 20, mousePos.y + 20),
|
|
|
|
|
|
IM_COL32(80, 80, 120, 180));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 20:27:01 -08:00
|
|
|
|
// On right mouse release, check if outside the action bar area
|
|
|
|
|
|
if (ImGui::IsMouseReleased(ImGuiMouseButton_Right)) {
|
2026-02-06 19:24:44 -08:00
|
|
|
|
bool insideBar = (mousePos.x >= barX && mousePos.x <= barX + barW &&
|
|
|
|
|
|
mousePos.y >= barY && mousePos.y <= barY + barH);
|
|
|
|
|
|
if (!insideBar) {
|
|
|
|
|
|
// Dropped outside - clear the slot
|
|
|
|
|
|
gameHandler.setActionBarSlot(actionBarDragSlot_, game::ActionBarSlot::EMPTY, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
actionBarDragSlot_ = -1;
|
|
|
|
|
|
actionBarDragIcon_ = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// ============================================================
|
|
|
|
|
|
// 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.
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderStanceBar(game::GameHandler& gameHandler) {
|
|
|
|
|
|
uint8_t playerClass = gameHandler.getPlayerClass();
|
|
|
|
|
|
|
|
|
|
|
|
// Stance/form spell IDs per class (ordered by display priority)
|
|
|
|
|
|
// Class IDs: 1=Warrior, 4=Rogue, 5=Priest, 6=DeathKnight, 11=Druid
|
|
|
|
|
|
static const uint32_t warriorStances[] = { 2457, 71, 2458 }; // Battle, Defensive, Berserker
|
|
|
|
|
|
static const uint32_t dkPresences[] = { 48266, 48263, 48265 }; // Blood, Frost, Unholy
|
|
|
|
|
|
static const uint32_t druidForms[] = { 5487, 9634, 768, 783, 1066, 24858, 33891, 33943, 40120 };
|
|
|
|
|
|
// Bear, DireBear, Cat, Travel, Aquatic, Moonkin, Tree, Flight, SwiftFlight
|
|
|
|
|
|
static const uint32_t rogueForms[] = { 1784 }; // Stealth
|
|
|
|
|
|
static const uint32_t priestForms[] = { 15473 }; // Shadowform
|
|
|
|
|
|
|
|
|
|
|
|
const uint32_t* stanceArr = nullptr;
|
|
|
|
|
|
int stanceCount = 0;
|
|
|
|
|
|
switch (playerClass) {
|
|
|
|
|
|
case 1: stanceArr = warriorStances; stanceCount = 3; break;
|
|
|
|
|
|
case 6: stanceArr = dkPresences; stanceCount = 3; break;
|
|
|
|
|
|
case 11: stanceArr = druidForms; stanceCount = 9; break;
|
|
|
|
|
|
case 4: stanceArr = rogueForms; stanceCount = 1; break;
|
|
|
|
|
|
case 5: stanceArr = priestForms; stanceCount = 1; break;
|
|
|
|
|
|
default: return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Filter to spells the player actually knows
|
|
|
|
|
|
const auto& known = gameHandler.getKnownSpells();
|
|
|
|
|
|
std::vector<uint32_t> available;
|
|
|
|
|
|
available.reserve(stanceCount);
|
|
|
|
|
|
for (int i = 0; i < stanceCount; ++i)
|
|
|
|
|
|
if (known.count(stanceArr[i])) available.push_back(stanceArr[i]);
|
|
|
|
|
|
|
|
|
|
|
|
if (available.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Detect active stance from permanent player auras (maxDurationMs == -1)
|
|
|
|
|
|
uint32_t activeStance = 0;
|
|
|
|
|
|
for (const auto& aura : gameHandler.getPlayerAuras()) {
|
|
|
|
|
|
if (aura.isEmpty() || aura.maxDurationMs != -1) continue;
|
|
|
|
|
|
for (uint32_t sid : available) {
|
|
|
|
|
|
if (aura.spellId == sid) { activeStance = sid; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (activeStance) break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
auto* assetMgr = core::Application::getInstance().getAssetManager();
|
|
|
|
|
|
|
|
|
|
|
|
// Match the action bar slot size so they align neatly
|
|
|
|
|
|
float slotSize = 38.0f;
|
|
|
|
|
|
float spacing = 4.0f;
|
|
|
|
|
|
float padding = 6.0f;
|
|
|
|
|
|
int count = static_cast<int>(available.size());
|
|
|
|
|
|
|
|
|
|
|
|
float barW = count * slotSize + (count - 1) * spacing + padding * 2.0f;
|
|
|
|
|
|
float barH = slotSize + padding * 2.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// Position the stance bar immediately to the left of the action bar
|
2026-03-31 10:07:58 +03:00
|
|
|
|
float actionSlot = 48.0f * settingsPanel_.pendingActionBarScale;
|
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
|
|
|
|
float actionBarW = 12.0f * actionSlot + 11.0f * 4.0f + 8.0f * 2.0f;
|
|
|
|
|
|
float actionBarX = (screenW - actionBarW) / 2.0f;
|
|
|
|
|
|
float actionBarH = actionSlot + 24.0f;
|
|
|
|
|
|
float actionBarY = screenH - actionBarH;
|
|
|
|
|
|
|
|
|
|
|
|
float barX = actionBarX - barW - 8.0f;
|
|
|
|
|
|
float barY = actionBarY + (actionBarH - barH) / 2.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(barX, barY), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(barW, barH), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(padding, padding));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.05f, 0.05f, 0.9f));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##StanceBar", nullptr, flags)) {
|
|
|
|
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < count; ++i) {
|
|
|
|
|
|
if (i > 0) ImGui::SameLine(0.0f, spacing);
|
|
|
|
|
|
ImGui::PushID(i);
|
|
|
|
|
|
|
|
|
|
|
|
uint32_t spellId = available[i];
|
|
|
|
|
|
bool isActive = (spellId == activeStance);
|
|
|
|
|
|
|
|
|
|
|
|
VkDescriptorSet iconTex = assetMgr ? getSpellIcon(spellId, assetMgr) : VK_NULL_HANDLE;
|
|
|
|
|
|
|
|
|
|
|
|
ImVec2 pos = ImGui::GetCursorScreenPos();
|
|
|
|
|
|
ImVec2 posEnd = ImVec2(pos.x + slotSize, pos.y + slotSize);
|
|
|
|
|
|
|
|
|
|
|
|
// Background — green tint when active
|
|
|
|
|
|
ImU32 bgCol = isActive ? IM_COL32(30, 70, 30, 230) : IM_COL32(20, 20, 20, 220);
|
|
|
|
|
|
ImU32 borderCol = isActive ? IM_COL32(80, 220, 80, 255) : IM_COL32(80, 80, 80, 200);
|
|
|
|
|
|
dl->AddRectFilled(pos, posEnd, bgCol, 4.0f);
|
|
|
|
|
|
|
|
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
dl->AddImage((ImTextureID)(uintptr_t)iconTex, pos, posEnd);
|
|
|
|
|
|
// Darken inactive buttons slightly
|
|
|
|
|
|
if (!isActive)
|
|
|
|
|
|
dl->AddRectFilled(pos, posEnd, IM_COL32(0, 0, 0, 70), 4.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
dl->AddRect(pos, posEnd, borderCol, 4.0f, 0, 2.0f);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::InvisibleButton("##btn", ImVec2(slotSize, slotSize));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Left))
|
|
|
|
|
|
gameHandler.castSpell(spellId);
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
std::string name = spellbookScreen.lookupSpellName(spellId, assetMgr);
|
|
|
|
|
|
if (!name.empty()) ImGui::TextUnformatted(name.c_str());
|
|
|
|
|
|
else ImGui::Text("Spell #%u", spellId);
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::PopStyleVar(4);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 01:24:37 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Bag Bar
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderBagBar(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;
|
2026-02-10 01:24:37 -08:00
|
|
|
|
auto* assetMgr = core::Application::getInstance().getAssetManager();
|
|
|
|
|
|
|
|
|
|
|
|
float slotSize = 42.0f;
|
|
|
|
|
|
float spacing = 4.0f;
|
|
|
|
|
|
float padding = 6.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// 5 slots: backpack + 4 bags
|
|
|
|
|
|
float barW = 5 * slotSize + 4 * spacing + padding * 2;
|
|
|
|
|
|
float barH = slotSize + padding * 2;
|
|
|
|
|
|
|
|
|
|
|
|
// Position in bottom right corner
|
|
|
|
|
|
float barX = screenW - barW - 10.0f;
|
|
|
|
|
|
float barY = screenH - barH - 10.0f;
|
|
|
|
|
|
|
2026-02-17 03:50:36 -08:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(barX, barY), ImGuiCond_Always);
|
2026-02-10 01:24:37 -08:00
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(barW, barH), ImGuiCond_Always);
|
|
|
|
|
|
|
2026-02-17 03:50:36 -08:00
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
2026-02-10 01:24:37 -08:00
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
2026-02-15 03:17:51 -08:00
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(padding, padding));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
2026-02-10 01:24:37 -08:00
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.05f, 0.05f, 0.9f));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##BagBar", nullptr, flags)) {
|
|
|
|
|
|
auto& inv = gameHandler.getInventory();
|
|
|
|
|
|
|
|
|
|
|
|
// Load backpack icon if needed
|
|
|
|
|
|
if (!backpackIconTexture_ && assetMgr && assetMgr->isInitialized()) {
|
|
|
|
|
|
auto blpData = assetMgr->readFile("Interface\\Buttons\\Button-Backpack-Up.blp");
|
|
|
|
|
|
if (!blpData.empty()) {
|
|
|
|
|
|
auto image = pipeline::BLPLoader::load(blpData);
|
|
|
|
|
|
if (image.isValid()) {
|
2026-02-22 03:32:08 -08:00
|
|
|
|
auto* w = core::Application::getInstance().getWindow();
|
|
|
|
|
|
auto* vkCtx = w ? w->getVkContext() : nullptr;
|
|
|
|
|
|
if (vkCtx)
|
|
|
|
|
|
backpackIconTexture_ = vkCtx->uploadImGuiTexture(image.data.data(), image.width, image.height);
|
2026-02-10 01:24:37 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-19 22:34:22 -08:00
|
|
|
|
// Track bag slot screen rects for drop detection
|
|
|
|
|
|
ImVec2 bagSlotMins[4], bagSlotMaxs[4];
|
|
|
|
|
|
|
2026-02-10 01:24:37 -08:00
|
|
|
|
// Slots 1-4: Bag slots (leftmost)
|
|
|
|
|
|
for (int i = 0; i < 4; ++i) {
|
|
|
|
|
|
if (i > 0) ImGui::SameLine(0, spacing);
|
|
|
|
|
|
ImGui::PushID(i + 1);
|
|
|
|
|
|
|
|
|
|
|
|
game::EquipSlot bagSlot = static_cast<game::EquipSlot>(static_cast<int>(game::EquipSlot::BAG1) + i);
|
|
|
|
|
|
const auto& bagItem = inv.getEquipSlot(bagSlot);
|
|
|
|
|
|
|
2026-02-22 03:32:08 -08:00
|
|
|
|
VkDescriptorSet bagIcon = VK_NULL_HANDLE;
|
2026-02-10 01:24:37 -08:00
|
|
|
|
if (!bagItem.empty() && bagItem.item.displayInfoId != 0) {
|
|
|
|
|
|
bagIcon = inventoryScreen.getItemIcon(bagItem.item.displayInfoId);
|
|
|
|
|
|
}
|
2026-02-19 22:34:22 -08:00
|
|
|
|
// Render the slot as an invisible button so we control all interaction
|
|
|
|
|
|
ImVec2 cpos = ImGui::GetCursorScreenPos();
|
|
|
|
|
|
ImGui::InvisibleButton("##bagSlot", ImVec2(slotSize, slotSize));
|
|
|
|
|
|
bagSlotMins[i] = cpos;
|
|
|
|
|
|
bagSlotMaxs[i] = ImVec2(cpos.x + slotSize, cpos.y + slotSize);
|
|
|
|
|
|
|
|
|
|
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
2026-02-10 01:24:37 -08:00
|
|
|
|
|
2026-02-19 22:34:22 -08:00
|
|
|
|
// Draw background + icon
|
2026-02-10 01:24:37 -08:00
|
|
|
|
if (bagIcon) {
|
2026-02-19 22:34:22 -08:00
|
|
|
|
dl->AddRectFilled(bagSlotMins[i], bagSlotMaxs[i], IM_COL32(25, 25, 25, 230));
|
|
|
|
|
|
dl->AddImage((ImTextureID)(uintptr_t)bagIcon, bagSlotMins[i], bagSlotMaxs[i]);
|
2026-02-10 01:24:37 -08:00
|
|
|
|
} else {
|
2026-02-19 22:34:22 -08:00
|
|
|
|
dl->AddRectFilled(bagSlotMins[i], bagSlotMaxs[i], IM_COL32(38, 38, 38, 204));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Hover highlight
|
|
|
|
|
|
bool hovered = ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenBlockedByActiveItem);
|
|
|
|
|
|
if (hovered && bagBarPickedSlot_ < 0) {
|
|
|
|
|
|
dl->AddRect(bagSlotMins[i], bagSlotMaxs[i], IM_COL32(255, 255, 255, 100));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Track which slot was pressed for drag detection
|
|
|
|
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && bagBarPickedSlot_ < 0 && bagIcon) {
|
|
|
|
|
|
bagBarDragSource_ = i;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Click toggles bag open/close (handled in mouse release section below)
|
|
|
|
|
|
|
|
|
|
|
|
// Dim the slot being dragged
|
|
|
|
|
|
if (bagBarPickedSlot_ == i) {
|
|
|
|
|
|
dl->AddRectFilled(bagSlotMins[i], bagSlotMaxs[i], IM_COL32(0, 0, 0, 150));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Tooltip
|
|
|
|
|
|
if (hovered && bagBarPickedSlot_ < 0) {
|
|
|
|
|
|
if (bagIcon)
|
|
|
|
|
|
ImGui::SetTooltip("%s", bagItem.item.name.c_str());
|
|
|
|
|
|
else
|
2026-02-10 01:24:37 -08:00
|
|
|
|
ImGui::SetTooltip("Empty Bag Slot");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-19 22:34:22 -08:00
|
|
|
|
// Open bag indicator
|
|
|
|
|
|
if (inventoryScreen.isSeparateBags() && inventoryScreen.isBagOpen(i)) {
|
|
|
|
|
|
dl->AddRect(bagSlotMins[i], bagSlotMaxs[i], IM_COL32(255, 255, 255, 255), 3.0f, 0, 2.0f);
|
2026-02-19 01:50:50 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 00:21:25 -07:00
|
|
|
|
// Right-click context menu
|
|
|
|
|
|
if (ImGui::BeginPopupContextItem("##bagSlotCtx")) {
|
|
|
|
|
|
if (!bagItem.empty()) {
|
|
|
|
|
|
ImGui::TextDisabled("%s", bagItem.item.name.c_str());
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
bool isOpen = inventoryScreen.isSeparateBags() && inventoryScreen.isBagOpen(i);
|
|
|
|
|
|
if (ImGui::MenuItem(isOpen ? "Close Bag" : "Open Bag")) {
|
|
|
|
|
|
if (inventoryScreen.isSeparateBags())
|
|
|
|
|
|
inventoryScreen.toggleBag(i);
|
|
|
|
|
|
else
|
|
|
|
|
|
inventoryScreen.toggle();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Unequip Bag")) {
|
|
|
|
|
|
gameHandler.unequipToBackpack(bagSlot);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextDisabled("Empty Bag Slot");
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 01:24:37 -08:00
|
|
|
|
// Accept dragged item from inventory
|
2026-02-19 22:34:22 -08:00
|
|
|
|
if (hovered && inventoryScreen.isHoldingItem()) {
|
2026-02-10 01:24:37 -08:00
|
|
|
|
const auto& heldItem = inventoryScreen.getHeldItem();
|
2026-02-20 17:41:19 -08:00
|
|
|
|
if ((heldItem.inventoryType == 18 || heldItem.bagSlots > 0) &&
|
|
|
|
|
|
ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
2026-02-10 01:24:37 -08:00
|
|
|
|
auto& inventory = gameHandler.getInventory();
|
2026-02-20 17:41:19 -08:00
|
|
|
|
inventoryScreen.dropHeldItemToEquipSlot(inventory, bagSlot);
|
2026-02-10 01:24:37 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-19 22:34:22 -08:00
|
|
|
|
// Drag lifecycle: press on a slot sets bagBarDragSource_,
|
|
|
|
|
|
// dragging 3+ pixels promotes to bagBarPickedSlot_ (visual drag),
|
|
|
|
|
|
// releasing completes swap or click
|
|
|
|
|
|
if (bagBarDragSource_ >= 0) {
|
|
|
|
|
|
if (ImGui::IsMouseDragging(ImGuiMouseButton_Left, 3.0f) && bagBarPickedSlot_ < 0) {
|
2026-02-20 17:41:19 -08:00
|
|
|
|
// If an inventory window is open, hand off drag to inventory held-item
|
|
|
|
|
|
// so the bag can be dropped into backpack/bag slots.
|
|
|
|
|
|
if (inventoryScreen.isOpen() || inventoryScreen.isCharacterOpen()) {
|
|
|
|
|
|
auto equip = static_cast<game::EquipSlot>(
|
|
|
|
|
|
static_cast<int>(game::EquipSlot::BAG1) + bagBarDragSource_);
|
|
|
|
|
|
if (inventoryScreen.beginPickupFromEquipSlot(inv, equip)) {
|
|
|
|
|
|
bagBarDragSource_ = -1;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
bagBarPickedSlot_ = bagBarDragSource_;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Mouse moved enough — start visual drag
|
|
|
|
|
|
bagBarPickedSlot_ = bagBarDragSource_;
|
|
|
|
|
|
}
|
2026-02-19 22:34:22 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
|
|
|
|
|
if (bagBarPickedSlot_ >= 0) {
|
|
|
|
|
|
// Was dragging — check for drop target
|
|
|
|
|
|
ImVec2 mousePos = ImGui::GetIO().MousePos;
|
|
|
|
|
|
int dropTarget = -1;
|
|
|
|
|
|
for (int j = 0; j < 4; ++j) {
|
|
|
|
|
|
if (j == bagBarPickedSlot_) continue;
|
|
|
|
|
|
if (mousePos.x >= bagSlotMins[j].x && mousePos.x <= bagSlotMaxs[j].x &&
|
|
|
|
|
|
mousePos.y >= bagSlotMins[j].y && mousePos.y <= bagSlotMaxs[j].y) {
|
|
|
|
|
|
dropTarget = j;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (dropTarget >= 0) {
|
|
|
|
|
|
gameHandler.swapBagSlots(bagBarPickedSlot_, dropTarget);
|
|
|
|
|
|
}
|
|
|
|
|
|
bagBarPickedSlot_ = -1;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Was just a click (no drag) — toggle bag
|
|
|
|
|
|
int slot = bagBarDragSource_;
|
|
|
|
|
|
auto equip = static_cast<game::EquipSlot>(static_cast<int>(game::EquipSlot::BAG1) + slot);
|
|
|
|
|
|
if (!inv.getEquipSlot(equip).empty()) {
|
|
|
|
|
|
if (inventoryScreen.isSeparateBags())
|
|
|
|
|
|
inventoryScreen.toggleBag(slot);
|
|
|
|
|
|
else
|
|
|
|
|
|
inventoryScreen.toggle();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
bagBarDragSource_ = -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 01:24:37 -08:00
|
|
|
|
// Backpack (rightmost slot)
|
|
|
|
|
|
ImGui::SameLine(0, spacing);
|
|
|
|
|
|
ImGui::PushID(0);
|
|
|
|
|
|
if (backpackIconTexture_) {
|
|
|
|
|
|
if (ImGui::ImageButton("##backpack", (ImTextureID)(uintptr_t)backpackIconTexture_,
|
2026-02-15 03:17:51 -08:00
|
|
|
|
ImVec2(slotSize, slotSize),
|
2026-02-10 01:24:37 -08:00
|
|
|
|
ImVec2(0, 0), ImVec2(1, 1),
|
|
|
|
|
|
ImVec4(0.1f, 0.1f, 0.1f, 0.9f),
|
2026-03-27 14:00:15 -07:00
|
|
|
|
colors::kWhite)) {
|
2026-02-13 22:51:49 -08:00
|
|
|
|
if (inventoryScreen.isSeparateBags())
|
|
|
|
|
|
inventoryScreen.toggleBackpack();
|
|
|
|
|
|
else
|
|
|
|
|
|
inventoryScreen.toggle();
|
2026-02-10 01:24:37 -08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (ImGui::Button("B", ImVec2(slotSize, slotSize))) {
|
2026-02-13 22:51:49 -08:00
|
|
|
|
if (inventoryScreen.isSeparateBags())
|
|
|
|
|
|
inventoryScreen.toggleBackpack();
|
|
|
|
|
|
else
|
|
|
|
|
|
inventoryScreen.toggle();
|
2026-02-10 01:24:37 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::SetTooltip("Backpack");
|
|
|
|
|
|
}
|
2026-03-12 00:21:25 -07:00
|
|
|
|
// Right-click context menu on backpack
|
|
|
|
|
|
if (ImGui::BeginPopupContextItem("##backpackCtx")) {
|
|
|
|
|
|
bool isOpen = inventoryScreen.isSeparateBags() && inventoryScreen.isBackpackOpen();
|
|
|
|
|
|
if (ImGui::MenuItem(isOpen ? "Close Backpack" : "Open Backpack")) {
|
|
|
|
|
|
if (inventoryScreen.isSeparateBags())
|
|
|
|
|
|
inventoryScreen.toggleBackpack();
|
|
|
|
|
|
else
|
|
|
|
|
|
inventoryScreen.toggle();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Open All Bags")) {
|
|
|
|
|
|
inventoryScreen.openAllBags();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Close All Bags")) {
|
|
|
|
|
|
inventoryScreen.closeAllBags();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
2026-02-19 01:50:50 -08:00
|
|
|
|
if (inventoryScreen.isSeparateBags() &&
|
|
|
|
|
|
inventoryScreen.isBackpackOpen()) {
|
|
|
|
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
|
|
|
|
ImVec2 r0 = ImGui::GetItemRectMin();
|
|
|
|
|
|
ImVec2 r1 = ImGui::GetItemRectMax();
|
|
|
|
|
|
dl->AddRect(r0, r1, IM_COL32(255, 255, 255, 255), 3.0f, 0, 2.0f);
|
|
|
|
|
|
}
|
2026-02-10 01:24:37 -08:00
|
|
|
|
ImGui::PopID();
|
2026-02-19 01:50:50 -08:00
|
|
|
|
|
2026-02-10 01:24:37 -08:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleColor();
|
2026-02-15 03:17:51 -08:00
|
|
|
|
ImGui::PopStyleVar(4);
|
2026-02-19 22:34:22 -08:00
|
|
|
|
|
|
|
|
|
|
// Draw dragged bag icon following cursor
|
|
|
|
|
|
if (bagBarPickedSlot_ >= 0) {
|
|
|
|
|
|
auto& inv2 = gameHandler.getInventory();
|
|
|
|
|
|
auto pickedEquip = static_cast<game::EquipSlot>(
|
|
|
|
|
|
static_cast<int>(game::EquipSlot::BAG1) + bagBarPickedSlot_);
|
|
|
|
|
|
const auto& pickedItem = inv2.getEquipSlot(pickedEquip);
|
2026-02-22 03:32:08 -08:00
|
|
|
|
VkDescriptorSet pickedIcon = VK_NULL_HANDLE;
|
2026-02-19 22:34:22 -08:00
|
|
|
|
if (!pickedItem.empty() && pickedItem.item.displayInfoId != 0) {
|
|
|
|
|
|
pickedIcon = inventoryScreen.getItemIcon(pickedItem.item.displayInfoId);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (pickedIcon) {
|
|
|
|
|
|
ImVec2 mousePos = ImGui::GetIO().MousePos;
|
|
|
|
|
|
float sz = 40.0f;
|
|
|
|
|
|
ImVec2 p0(mousePos.x - sz * 0.5f, mousePos.y - sz * 0.5f);
|
|
|
|
|
|
ImVec2 p1(mousePos.x + sz * 0.5f, mousePos.y + sz * 0.5f);
|
|
|
|
|
|
ImDrawList* fg = ImGui::GetForegroundDrawList();
|
|
|
|
|
|
fg->AddImage((ImTextureID)(uintptr_t)pickedIcon, p0, p1);
|
|
|
|
|
|
fg->AddRect(p0, p1, IM_COL32(200, 200, 200, 255), 0.0f, 0, 2.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-10 01:24:37 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 12:07:58 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// XP Bar
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderXpBar(game::GameHandler& gameHandler) {
|
2026-03-17 15:28:33 -07:00
|
|
|
|
uint32_t nextLevelXp = gameHandler.getPlayerNextLevelXp();
|
|
|
|
|
|
uint32_t playerLevel = gameHandler.getPlayerLevel();
|
|
|
|
|
|
// At max level, server sends nextLevelXp=0. Only skip entirely when we have
|
|
|
|
|
|
// no level info at all (not yet logged in / no update-field data).
|
|
|
|
|
|
const bool isMaxLevel = (nextLevelXp == 0 && playerLevel > 0);
|
|
|
|
|
|
if (nextLevelXp == 0 && !isMaxLevel) return;
|
2026-02-05 12:07:58 -08:00
|
|
|
|
|
2026-03-10 07:35:30 -07:00
|
|
|
|
uint32_t currentXp = gameHandler.getPlayerXp();
|
|
|
|
|
|
uint32_t restedXp = gameHandler.getPlayerRestedXp();
|
|
|
|
|
|
bool isResting = gameHandler.isPlayerResting();
|
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;
|
2026-02-05 12:07:58 -08:00
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
2026-03-10 15:56:41 -07:00
|
|
|
|
(void)window; // Not used for positioning; kept for AssetManager if needed
|
2026-02-05 12:07:58 -08:00
|
|
|
|
|
2026-03-10 08:28:48 -07:00
|
|
|
|
// Position just above both action bars (bar1 at screenH-barH, bar2 above that)
|
2026-03-31 10:07:58 +03:00
|
|
|
|
float slotSize = 48.0f * settingsPanel_.pendingActionBarScale;
|
2026-02-05 12:07:58 -08:00
|
|
|
|
float spacing = 4.0f;
|
|
|
|
|
|
float padding = 8.0f;
|
|
|
|
|
|
float barW = 12 * slotSize + 11 * spacing + padding * 2;
|
|
|
|
|
|
float barH = slotSize + 24.0f;
|
|
|
|
|
|
|
2026-02-06 18:34:45 -08:00
|
|
|
|
float xpBarH = 20.0f;
|
2026-02-05 12:07:58 -08:00
|
|
|
|
float xpBarW = barW;
|
|
|
|
|
|
float xpBarX = (screenW - xpBarW) / 2.0f;
|
2026-03-10 15:56:41 -07:00
|
|
|
|
// XP bar sits just above whichever bar is topmost.
|
|
|
|
|
|
// bar1 top edge: screenH - barH
|
|
|
|
|
|
// bar2 top edge (when visible): bar1 top - barH - 2 + bar2 vertical offset
|
|
|
|
|
|
float bar1TopY = screenH - barH;
|
|
|
|
|
|
float xpBarY;
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (settingsPanel_.pendingShowActionBar2) {
|
|
|
|
|
|
float bar2TopY = bar1TopY - barH - 2.0f + settingsPanel_.pendingActionBar2OffsetY;
|
2026-03-10 15:56:41 -07:00
|
|
|
|
xpBarY = bar2TopY - xpBarH - 2.0f;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
xpBarY = bar1TopY - xpBarH - 2.0f;
|
|
|
|
|
|
}
|
2026-02-05 12:07:58 -08:00
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(xpBarX, xpBarY), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(xpBarW, xpBarH + 4.0f), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 2.0f);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(2.0f, 2.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.05f, 0.05f, 0.9f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.3f, 0.3f, 0.3f, 0.8f));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##XpBar", nullptr, flags)) {
|
2026-03-17 15:28:33 -07:00
|
|
|
|
ImVec2 barMin = ImGui::GetCursorScreenPos();
|
|
|
|
|
|
ImVec2 barSize = ImVec2(ImGui::GetContentRegionAvail().x, xpBarH - 4.0f);
|
|
|
|
|
|
ImVec2 barMax = ImVec2(barMin.x + barSize.x, barMin.y + barSize.y);
|
|
|
|
|
|
auto* drawList = ImGui::GetWindowDrawList();
|
|
|
|
|
|
|
|
|
|
|
|
if (isMaxLevel) {
|
|
|
|
|
|
// Max-level bar: fully filled in muted gold with "Max Level" label
|
|
|
|
|
|
ImU32 bgML = IM_COL32(15, 12, 5, 220);
|
|
|
|
|
|
ImU32 fgML = IM_COL32(180, 140, 40, 200);
|
|
|
|
|
|
drawList->AddRectFilled(barMin, barMax, bgML, 2.0f);
|
|
|
|
|
|
drawList->AddRectFilled(barMin, barMax, fgML, 2.0f);
|
|
|
|
|
|
drawList->AddRect(barMin, barMax, IM_COL32(100, 80, 20, 220), 2.0f);
|
|
|
|
|
|
const char* mlLabel = "Max Level";
|
|
|
|
|
|
ImVec2 mlSz = ImGui::CalcTextSize(mlLabel);
|
|
|
|
|
|
drawList->AddText(
|
|
|
|
|
|
ImVec2(barMin.x + (barSize.x - mlSz.x) * 0.5f,
|
|
|
|
|
|
barMin.y + (barSize.y - mlSz.y) * 0.5f),
|
|
|
|
|
|
IM_COL32(255, 230, 120, 255), mlLabel);
|
|
|
|
|
|
ImGui::Dummy(barSize);
|
|
|
|
|
|
if (ImGui::IsItemHovered())
|
|
|
|
|
|
ImGui::SetTooltip("Level %u — Maximum level reached", playerLevel);
|
|
|
|
|
|
} else {
|
2026-02-05 12:07:58 -08:00
|
|
|
|
float pct = static_cast<float>(currentXp) / static_cast<float>(nextLevelXp);
|
|
|
|
|
|
if (pct > 1.0f) pct = 1.0f;
|
|
|
|
|
|
|
2026-02-06 18:34:45 -08:00
|
|
|
|
// Custom segmented XP bar (20 bubbles)
|
2026-03-10 07:35:30 -07:00
|
|
|
|
ImU32 bg = IM_COL32(15, 15, 20, 220);
|
|
|
|
|
|
ImU32 fg = IM_COL32(148, 51, 238, 255);
|
|
|
|
|
|
ImU32 fgRest = IM_COL32(200, 170, 255, 220); // lighter purple for rested portion
|
|
|
|
|
|
ImU32 seg = IM_COL32(35, 35, 45, 255);
|
2026-02-06 18:34:45 -08:00
|
|
|
|
drawList->AddRectFilled(barMin, barMax, bg, 2.0f);
|
|
|
|
|
|
drawList->AddRect(barMin, barMax, IM_COL32(80, 80, 90, 220), 2.0f);
|
|
|
|
|
|
|
|
|
|
|
|
float fillW = barSize.x * pct;
|
|
|
|
|
|
if (fillW > 0.0f) {
|
|
|
|
|
|
drawList->AddRectFilled(barMin, ImVec2(barMin.x + fillW, barMax.y), fg, 2.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 07:35:30 -07:00
|
|
|
|
// Rested XP overlay: draw from current XP fill to (currentXp + restedXp) fill
|
|
|
|
|
|
if (restedXp > 0) {
|
|
|
|
|
|
float restedEndPct = std::min(1.0f, static_cast<float>(currentXp + restedXp)
|
|
|
|
|
|
/ static_cast<float>(nextLevelXp));
|
|
|
|
|
|
float restedStartX = barMin.x + fillW;
|
|
|
|
|
|
float restedEndX = barMin.x + barSize.x * restedEndPct;
|
|
|
|
|
|
if (restedEndX > restedStartX) {
|
|
|
|
|
|
drawList->AddRectFilled(ImVec2(restedStartX, barMin.y),
|
|
|
|
|
|
ImVec2(restedEndX, barMax.y),
|
|
|
|
|
|
fgRest, 2.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 18:34:45 -08:00
|
|
|
|
const int segments = 20;
|
|
|
|
|
|
float segW = barSize.x / static_cast<float>(segments);
|
|
|
|
|
|
for (int i = 1; i < segments; ++i) {
|
|
|
|
|
|
float x = barMin.x + segW * i;
|
|
|
|
|
|
drawList->AddLine(ImVec2(x, barMin.y + 1.0f), ImVec2(x, barMax.y - 1.0f), seg, 1.0f);
|
|
|
|
|
|
}
|
2026-02-05 12:07:58 -08:00
|
|
|
|
|
2026-03-10 07:35:30 -07:00
|
|
|
|
// Rest indicator "zzz" to the right of the bar when resting
|
|
|
|
|
|
if (isResting) {
|
|
|
|
|
|
const char* zzz = "zzz";
|
|
|
|
|
|
ImVec2 zSize = ImGui::CalcTextSize(zzz);
|
|
|
|
|
|
float zx = barMax.x - zSize.x - 4.0f;
|
|
|
|
|
|
float zy = barMin.y + (barSize.y - zSize.y) * 0.5f;
|
|
|
|
|
|
drawList->AddText(ImVec2(zx, zy), IM_COL32(180, 150, 255, 220), zzz);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 12:07:58 -08:00
|
|
|
|
char overlay[96];
|
2026-03-10 07:35:30 -07:00
|
|
|
|
if (restedXp > 0) {
|
|
|
|
|
|
snprintf(overlay, sizeof(overlay), "%u / %u XP (+%u rested)", currentXp, nextLevelXp, restedXp);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
snprintf(overlay, sizeof(overlay), "%u / %u XP", currentXp, nextLevelXp);
|
|
|
|
|
|
}
|
2026-02-06 18:34:45 -08:00
|
|
|
|
ImVec2 textSize = ImGui::CalcTextSize(overlay);
|
|
|
|
|
|
float tx = barMin.x + (barSize.x - textSize.x) * 0.5f;
|
|
|
|
|
|
float ty = barMin.y + (barSize.y - textSize.y) * 0.5f;
|
|
|
|
|
|
drawList->AddText(ImVec2(tx, ty), IM_COL32(230, 230, 230, 255), overlay);
|
2026-02-05 12:07:58 -08:00
|
|
|
|
|
2026-02-06 18:34:45 -08:00
|
|
|
|
ImGui::Dummy(barSize);
|
2026-03-12 07:12:02 -07:00
|
|
|
|
|
|
|
|
|
|
// Tooltip with XP-to-level and rested details
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
uint32_t xpToLevel = (currentXp < nextLevelXp) ? (nextLevelXp - currentXp) : 0;
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.9f, 0.85f, 1.0f, 1.0f), "Experience");
|
|
|
|
|
|
ImGui::Separator();
|
2026-03-17 12:17:23 -07:00
|
|
|
|
float xpPct = nextLevelXp > 0 ? (100.0f * currentXp / nextLevelXp) : 0.0f;
|
|
|
|
|
|
ImGui::Text("Current: %u / %u XP (%.1f%%)", currentXp, nextLevelXp, xpPct);
|
2026-03-12 07:12:02 -07:00
|
|
|
|
ImGui::Text("To next level: %u XP", xpToLevel);
|
|
|
|
|
|
if (restedXp > 0) {
|
|
|
|
|
|
float restedLevels = static_cast<float>(restedXp) / static_cast<float>(nextLevelXp);
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.78f, 0.60f, 1.0f, 1.0f),
|
|
|
|
|
|
"Rested: +%u XP (%.1f%% of a level)", restedXp, restedLevels * 100.0f);
|
|
|
|
|
|
if (isResting)
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 0.9f, 0.6f, 1.0f),
|
|
|
|
|
|
"Resting — accumulating bonus XP");
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
2026-02-05 12:07:58 -08:00
|
|
|
|
}
|
2026-03-17 15:28:33 -07:00
|
|
|
|
}
|
2026-02-05 12:07:58 -08:00
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
ImGui::PopStyleVar(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 05:03:03 -07:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Reputation Bar
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderRepBar(game::GameHandler& gameHandler) {
|
|
|
|
|
|
uint32_t factionId = gameHandler.getWatchedFactionId();
|
|
|
|
|
|
if (factionId == 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
const auto& standings = gameHandler.getFactionStandings();
|
|
|
|
|
|
auto it = standings.find(factionId);
|
|
|
|
|
|
if (it == standings.end()) return;
|
|
|
|
|
|
|
|
|
|
|
|
int32_t standing = it->second;
|
|
|
|
|
|
|
|
|
|
|
|
// WoW reputation rank thresholds
|
|
|
|
|
|
struct RepRank { const char* name; int32_t min; int32_t max; ImU32 color; };
|
|
|
|
|
|
static const RepRank kRanks[] = {
|
|
|
|
|
|
{ "Hated", -42000, -6001, IM_COL32(180, 40, 40, 255) },
|
|
|
|
|
|
{ "Hostile", -6000, -3001, IM_COL32(180, 40, 40, 255) },
|
|
|
|
|
|
{ "Unfriendly", -3000, -1, IM_COL32(220, 100, 50, 255) },
|
|
|
|
|
|
{ "Neutral", 0, 2999, IM_COL32(200, 200, 60, 255) },
|
|
|
|
|
|
{ "Friendly", 3000, 8999, IM_COL32( 60, 180, 60, 255) },
|
|
|
|
|
|
{ "Honored", 9000, 20999, IM_COL32( 60, 160, 220, 255) },
|
|
|
|
|
|
{ "Revered", 21000, 41999, IM_COL32(140, 80, 220, 255) },
|
|
|
|
|
|
{ "Exalted", 42000, 42999, IM_COL32(255, 200, 50, 255) },
|
|
|
|
|
|
};
|
|
|
|
|
|
constexpr int kNumRanks = static_cast<int>(sizeof(kRanks) / sizeof(kRanks[0]));
|
|
|
|
|
|
|
|
|
|
|
|
int rankIdx = kNumRanks - 1; // default to Exalted
|
|
|
|
|
|
for (int i = 0; i < kNumRanks; ++i) {
|
|
|
|
|
|
if (standing <= kRanks[i].max) { rankIdx = i; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
const RepRank& rank = kRanks[rankIdx];
|
|
|
|
|
|
|
|
|
|
|
|
float fraction = 1.0f;
|
|
|
|
|
|
if (rankIdx < kNumRanks - 1) {
|
|
|
|
|
|
float range = static_cast<float>(rank.max - rank.min + 1);
|
|
|
|
|
|
fraction = static_cast<float>(standing - rank.min) / range;
|
|
|
|
|
|
fraction = std::max(0.0f, std::min(1.0f, fraction));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const std::string& factionName = gameHandler.getFactionNamePublic(factionId);
|
|
|
|
|
|
|
|
|
|
|
|
// Position directly above the XP bar
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
float slotSize = 48.0f * settingsPanel_.pendingActionBarScale;
|
2026-03-12 05:03:03 -07:00
|
|
|
|
float spacing = 4.0f;
|
|
|
|
|
|
float padding = 8.0f;
|
|
|
|
|
|
float barW = 12 * slotSize + 11 * spacing + padding * 2;
|
|
|
|
|
|
float barH_ab = slotSize + 24.0f;
|
|
|
|
|
|
float xpBarH = 20.0f;
|
|
|
|
|
|
float repBarH = 12.0f;
|
|
|
|
|
|
float xpBarW = barW;
|
|
|
|
|
|
float xpBarX = (screenW - xpBarW) / 2.0f;
|
|
|
|
|
|
|
|
|
|
|
|
float bar1TopY = screenH - barH_ab;
|
|
|
|
|
|
float xpBarY;
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (settingsPanel_.pendingShowActionBar2) {
|
|
|
|
|
|
float bar2TopY = bar1TopY - barH_ab - 2.0f + settingsPanel_.pendingActionBar2OffsetY;
|
2026-03-12 05:03:03 -07:00
|
|
|
|
xpBarY = bar2TopY - xpBarH - 2.0f;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
xpBarY = bar1TopY - xpBarH - 2.0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
float repBarY = xpBarY - repBarH - 2.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(xpBarX, repBarY), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(xpBarW, repBarH + 4.0f), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_AlwaysAutoResize;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 2.0f);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(2.0f, 2.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.05f, 0.05f, 0.05f, 0.9f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.3f, 0.3f, 0.3f, 0.8f));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##RepBar", nullptr, flags)) {
|
|
|
|
|
|
ImVec2 barMin = ImGui::GetCursorScreenPos();
|
|
|
|
|
|
ImVec2 barSize = ImVec2(ImGui::GetContentRegionAvail().x, repBarH - 4.0f);
|
|
|
|
|
|
ImVec2 barMax = ImVec2(barMin.x + barSize.x, barMin.y + barSize.y);
|
|
|
|
|
|
auto* dl = ImGui::GetWindowDrawList();
|
|
|
|
|
|
|
|
|
|
|
|
dl->AddRectFilled(barMin, barMax, IM_COL32(15, 15, 20, 220), 2.0f);
|
|
|
|
|
|
dl->AddRect(barMin, barMax, IM_COL32(80, 80, 90, 220), 2.0f);
|
|
|
|
|
|
|
|
|
|
|
|
float fillW = barSize.x * fraction;
|
|
|
|
|
|
if (fillW > 0.0f)
|
|
|
|
|
|
dl->AddRectFilled(barMin, ImVec2(barMin.x + fillW, barMax.y), rank.color, 2.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// Label: "FactionName - Rank"
|
|
|
|
|
|
char label[96];
|
|
|
|
|
|
snprintf(label, sizeof(label), "%s - %s", factionName.c_str(), rank.name);
|
|
|
|
|
|
ImVec2 textSize = ImGui::CalcTextSize(label);
|
|
|
|
|
|
float tx = barMin.x + (barSize.x - textSize.x) * 0.5f;
|
|
|
|
|
|
float ty = barMin.y + (barSize.y - textSize.y) * 0.5f;
|
|
|
|
|
|
dl->AddText(ImVec2(tx, ty), IM_COL32(230, 230, 230, 255), label);
|
|
|
|
|
|
|
|
|
|
|
|
// Tooltip with exact values on hover
|
|
|
|
|
|
ImGui::Dummy(barSize);
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
float cr = ((rank.color ) & 0xFF) / 255.0f;
|
|
|
|
|
|
float cg = ((rank.color >> 8) & 0xFF) / 255.0f;
|
|
|
|
|
|
float cb = ((rank.color >> 16) & 0xFF) / 255.0f;
|
|
|
|
|
|
ImGui::TextColored(ImVec4(cr, cg, cb, 1.0f), "%s", rank.name);
|
|
|
|
|
|
int32_t rankMin = rank.min;
|
|
|
|
|
|
int32_t rankMax = (rankIdx < kNumRanks - 1) ? rank.max : 42000;
|
|
|
|
|
|
ImGui::Text("%s: %d / %d", factionName.c_str(), standing - rankMin, rankMax - rankMin + 1);
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
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
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Cast Bar (Phase 3)
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderCastBar(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isCasting()) return;
|
|
|
|
|
|
|
2026-03-12 07:52:47 -07:00
|
|
|
|
auto* assetMgr = core::Application::getInstance().getAssetManager();
|
|
|
|
|
|
|
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-12 07:52:47 -07:00
|
|
|
|
uint32_t currentSpellId = gameHandler.getCurrentCastSpellId();
|
|
|
|
|
|
VkDescriptorSet iconTex = (currentSpellId != 0 && assetMgr)
|
|
|
|
|
|
? getSpellIcon(currentSpellId, assetMgr) : VK_NULL_HANDLE;
|
|
|
|
|
|
|
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
|
|
|
|
float barW = 300.0f;
|
|
|
|
|
|
float barX = (screenW - barW) / 2.0f;
|
|
|
|
|
|
float barY = screenH - 120.0f;
|
|
|
|
|
|
|
2026-03-29 17:20:02 -07:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(barX, barY), 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
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(barW, 40), ImGuiCond_Always);
|
|
|
|
|
|
|
2026-02-15 03:17:51 -08:00
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize |
|
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
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
2026-03-29 17:20:02 -07:00
|
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings |
|
|
|
|
|
|
ImGuiWindowFlags_NoFocusOnAppearing;
|
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::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.9f));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##CastBar", nullptr, flags)) {
|
2026-03-12 00:43:29 -07:00
|
|
|
|
const bool channeling = gameHandler.isChanneling();
|
|
|
|
|
|
// Channels drain right-to-left; regular casts fill left-to-right
|
|
|
|
|
|
float progress = channeling
|
|
|
|
|
|
? (1.0f - gameHandler.getCastProgress())
|
|
|
|
|
|
: gameHandler.getCastProgress();
|
|
|
|
|
|
|
2026-03-17 19:56:52 -07:00
|
|
|
|
// Color by spell school for cast identification; channels always blue
|
|
|
|
|
|
ImVec4 barColor;
|
|
|
|
|
|
if (channeling) {
|
|
|
|
|
|
barColor = ImVec4(0.3f, 0.6f, 0.9f, 1.0f); // blue for channels
|
|
|
|
|
|
} else {
|
|
|
|
|
|
uint32_t school = (currentSpellId != 0) ? gameHandler.getSpellSchoolMask(currentSpellId) : 0;
|
|
|
|
|
|
if (school & 0x04) barColor = ImVec4(0.95f, 0.40f, 0.10f, 1.0f); // Fire: orange-red
|
|
|
|
|
|
else if (school & 0x10) barColor = ImVec4(0.30f, 0.65f, 0.95f, 1.0f); // Frost: icy blue
|
|
|
|
|
|
else if (school & 0x20) barColor = ImVec4(0.55f, 0.15f, 0.70f, 1.0f); // Shadow: purple
|
|
|
|
|
|
else if (school & 0x40) barColor = ImVec4(0.65f, 0.30f, 0.85f, 1.0f); // Arcane: violet
|
|
|
|
|
|
else if (school & 0x08) barColor = ImVec4(0.20f, 0.75f, 0.25f, 1.0f); // Nature: green
|
|
|
|
|
|
else if (school & 0x02) barColor = ImVec4(0.90f, 0.80f, 0.30f, 1.0f); // Holy: golden
|
|
|
|
|
|
else barColor = ImVec4(0.80f, 0.60f, 0.20f, 1.0f); // Physical/default: gold
|
|
|
|
|
|
}
|
2026-03-12 00:43:29 -07:00
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, barColor);
|
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-17 10:30:18 -07:00
|
|
|
|
char overlay[96];
|
2026-03-12 00:43:29 -07:00
|
|
|
|
if (currentSpellId == 0) {
|
2026-02-19 03:12:57 -08:00
|
|
|
|
snprintf(overlay, sizeof(overlay), "Opening... (%.1fs)", gameHandler.getCastTimeRemaining());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const std::string& spellName = gameHandler.getSpellName(currentSpellId);
|
2026-03-12 00:43:29 -07:00
|
|
|
|
const char* verb = channeling ? "Channeling" : "Casting";
|
2026-03-17 10:30:18 -07:00
|
|
|
|
int queueLeft = gameHandler.getCraftQueueRemaining();
|
|
|
|
|
|
if (!spellName.empty()) {
|
|
|
|
|
|
if (queueLeft > 0)
|
|
|
|
|
|
snprintf(overlay, sizeof(overlay), "%s (%.1fs) [%d left]", spellName.c_str(), gameHandler.getCastTimeRemaining(), queueLeft);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(overlay, sizeof(overlay), "%s (%.1fs)", spellName.c_str(), gameHandler.getCastTimeRemaining());
|
|
|
|
|
|
} else {
|
2026-03-12 00:43:29 -07:00
|
|
|
|
snprintf(overlay, sizeof(overlay), "%s... (%.1fs)", verb, gameHandler.getCastTimeRemaining());
|
2026-03-17 10:30:18 -07:00
|
|
|
|
}
|
2026-02-19 03:12:57 -08:00
|
|
|
|
}
|
2026-03-12 07:52:47 -07:00
|
|
|
|
|
2026-03-18 04:34:36 -07:00
|
|
|
|
// Queued spell icon (right edge): the next spell queued to fire within 400ms.
|
|
|
|
|
|
uint32_t queuedId = gameHandler.getQueuedSpellId();
|
|
|
|
|
|
VkDescriptorSet queuedTex = (queuedId != 0 && assetMgr)
|
|
|
|
|
|
? getSpellIcon(queuedId, assetMgr) : VK_NULL_HANDLE;
|
|
|
|
|
|
|
|
|
|
|
|
const float iconSz = 20.0f;
|
|
|
|
|
|
const float reservedRight = (queuedTex) ? (iconSz + 4.0f) : 0.0f;
|
|
|
|
|
|
|
2026-03-12 07:52:47 -07:00
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
// Spell icon to the left of the progress bar
|
2026-03-18 04:34:36 -07:00
|
|
|
|
ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(iconSz, iconSz));
|
2026-03-12 07:52:47 -07:00
|
|
|
|
ImGui::SameLine(0, 4);
|
2026-03-18 04:34:36 -07:00
|
|
|
|
ImGui::ProgressBar(progress, ImVec2(-reservedRight - 1.0f, iconSz), overlay);
|
2026-03-12 07:52:47 -07:00
|
|
|
|
} else {
|
2026-03-18 04:34:36 -07:00
|
|
|
|
ImGui::ProgressBar(progress, ImVec2(-reservedRight - 1.0f, iconSz), overlay);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Draw queued-spell icon on the right with a ">" arrow prefix tooltip.
|
|
|
|
|
|
if (queuedTex) {
|
|
|
|
|
|
ImGui::SameLine(0, 4);
|
|
|
|
|
|
ImGui::Image((ImTextureID)(uintptr_t)queuedTex, ImVec2(iconSz, iconSz),
|
|
|
|
|
|
ImVec2(0,0), ImVec2(1,1),
|
|
|
|
|
|
ImVec4(1,1,1,0.8f), ImVec4(0,0,0,0)); // slightly dimmed
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
const std::string& qn = gameHandler.getSpellName(queuedId);
|
|
|
|
|
|
ImGui::SetTooltip("Queued: %s", qn.empty() ? "Unknown" : qn.c_str());
|
|
|
|
|
|
}
|
2026-03-12 07:52:47 -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
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleColor();
|
Fix PopStyleVar mismatches and character geoset IDs
Fix 9 PopStyleVar(2) calls that should be PopStyleVar(1) across
player frame, target frame, cast bar, party frames, buff bar, escape
menu, death dialog, and resurrect dialog. Fix action bar from
PopStyleVar(2) to PopStyleVar(4) to match 4 pushes.
Fix character geoset defaults: 301→302 (bare hands), 701→702 (ears),
1501→1502 (back/cloak), add 802 (wristbands). No WoW character model
uses geoset 301/701/1501; all use 302/702/1502 as base. This fixes
missing hands/arms on undead and other races with separate hand meshes.
2026-02-15 06:09:38 -08:00
|
|
|
|
ImGui::PopStyleVar();
|
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
|
|
|
|
// ============================================================
|
|
|
|
|
|
// 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;
|
2026-03-09 14:30:48 -07:00
|
|
|
|
|
2026-03-27 14:47:58 -07:00
|
|
|
|
static constexpr struct { const char* label; ImVec4 color; } kTimerInfo[3] = {
|
2026-03-09 14:30:48 -07:00
|
|
|
|
{ "Fatigue", ImVec4(0.8f, 0.4f, 0.1f, 1.0f) },
|
|
|
|
|
|
{ "Breath", ImVec4(0.2f, 0.5f, 1.0f, 1.0f) },
|
2026-03-25 11:57:22 -07:00
|
|
|
|
{ "Feign", kColorGray },
|
2026-03-09 14:30:48 -07: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
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
|
|
|
|
|
|
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoScrollbar |
|
|
|
|
|
|
ImGuiWindowFlags_NoInputs;
|
|
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < 3; ++i) {
|
|
|
|
|
|
const auto& t = gameHandler.getMirrorTimer(i);
|
|
|
|
|
|
if (!t.active || t.maxValue <= 0) continue;
|
|
|
|
|
|
|
|
|
|
|
|
float frac = static_cast<float>(t.value) / static_cast<float>(t.maxValue);
|
|
|
|
|
|
frac = std::max(0.0f, std::min(1.0f, frac));
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 15:25:07 -07:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Cooldown Tracker — floating panel showing all active spell CDs
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderCooldownTracker(game::GameHandler& gameHandler) {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (!settingsPanel_.showCooldownTracker_) return;
|
2026-03-12 15:25:07 -07:00
|
|
|
|
|
|
|
|
|
|
const auto& cooldowns = gameHandler.getSpellCooldowns();
|
|
|
|
|
|
if (cooldowns.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Collect spells with remaining cooldown > 0.5s (skip GCD noise)
|
|
|
|
|
|
struct CDEntry { uint32_t spellId; float remaining; };
|
|
|
|
|
|
std::vector<CDEntry> active;
|
|
|
|
|
|
active.reserve(16);
|
|
|
|
|
|
for (const auto& [sid, rem] : cooldowns) {
|
|
|
|
|
|
if (rem > 0.5f) active.push_back({sid, rem});
|
|
|
|
|
|
}
|
|
|
|
|
|
if (active.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
// Sort: longest remaining first
|
|
|
|
|
|
std::sort(active.begin(), active.end(), [](const CDEntry& a, const CDEntry& b) {
|
|
|
|
|
|
return a.remaining > b.remaining;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
auto* assetMgr = core::Application::getInstance().getAssetManager();
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
|
|
|
|
|
|
|
|
|
|
|
constexpr float TRACKER_W = 200.0f;
|
|
|
|
|
|
constexpr int MAX_SHOWN = 12;
|
|
|
|
|
|
float posX = screenW - TRACKER_W - 10.0f;
|
|
|
|
|
|
float posY = screenH - 220.0f; // above the action bar area
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(posX, posY), ImGuiCond_Always, ImVec2(1.0f, 1.0f));
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(TRACKER_W, 0.0f), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowBgAlpha(0.75f);
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoNav |
|
|
|
|
|
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize |
|
|
|
|
|
|
ImGuiWindowFlags_NoBringToFrontOnFocus;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(4.0f, 4.0f));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.0f, 2.0f));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##CooldownTracker", nullptr, flags)) {
|
|
|
|
|
|
ImGui::TextDisabled("Cooldowns");
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
int shown = 0;
|
|
|
|
|
|
for (const auto& cd : active) {
|
|
|
|
|
|
if (shown >= MAX_SHOWN) break;
|
|
|
|
|
|
|
|
|
|
|
|
const std::string& name = gameHandler.getSpellName(cd.spellId);
|
|
|
|
|
|
if (name.empty()) continue; // skip unnamed spells (internal/passive)
|
|
|
|
|
|
|
|
|
|
|
|
// Small icon if available
|
|
|
|
|
|
VkDescriptorSet icon = assetMgr ? getSpellIcon(cd.spellId, assetMgr) : VK_NULL_HANDLE;
|
|
|
|
|
|
if (icon) {
|
|
|
|
|
|
ImGui::Image((ImTextureID)(uintptr_t)icon, ImVec2(14, 14));
|
|
|
|
|
|
ImGui::SameLine(0, 3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Name (truncated) + remaining time
|
|
|
|
|
|
char timeStr[16];
|
|
|
|
|
|
if (cd.remaining >= 60.0f)
|
2026-03-25 11:40:49 -07:00
|
|
|
|
snprintf(timeStr, sizeof(timeStr), "%dm%ds", static_cast<int>(cd.remaining) / 60, static_cast<int>(cd.remaining) % 60);
|
2026-03-12 15:25:07 -07:00
|
|
|
|
else
|
|
|
|
|
|
snprintf(timeStr, sizeof(timeStr), "%.0fs", cd.remaining);
|
|
|
|
|
|
|
|
|
|
|
|
// Color: red > 30s, orange > 10s, yellow > 5s, green otherwise
|
2026-03-25 11:57:22 -07:00
|
|
|
|
ImVec4 cdColor = cd.remaining > 30.0f ? kColorRed :
|
2026-03-12 15:25:07 -07:00
|
|
|
|
cd.remaining > 10.0f ? ImVec4(1.0f, 0.6f, 0.2f, 1.0f) :
|
2026-03-25 11:57:22 -07:00
|
|
|
|
cd.remaining > 5.0f ? kColorYellow :
|
2026-03-27 13:57:29 -07:00
|
|
|
|
colors::kActiveGreen;
|
2026-03-12 15:25:07 -07:00
|
|
|
|
|
|
|
|
|
|
// Truncate name to fit
|
|
|
|
|
|
std::string displayName = name;
|
|
|
|
|
|
if (displayName.size() > 16) displayName = displayName.substr(0, 15) + "\xe2\x80\xa6"; // ellipsis
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f), "%s", displayName.c_str());
|
|
|
|
|
|
ImGui::SameLine(TRACKER_W - 48.0f);
|
|
|
|
|
|
ImGui::TextColored(cdColor, "%s", timeStr);
|
|
|
|
|
|
|
|
|
|
|
|
++shown;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleVar(3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 15:05:38 -07:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Quest Objective Tracker (right-side HUD)
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderQuestObjectiveTracker(game::GameHandler& gameHandler) {
|
|
|
|
|
|
const auto& questLog = gameHandler.getQuestLog();
|
|
|
|
|
|
if (questLog.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
2026-03-09 15:09:50 -07:00
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
2026-03-09 15:05:38 -07:00
|
|
|
|
|
|
|
|
|
|
constexpr float TRACKER_W = 220.0f;
|
|
|
|
|
|
constexpr float RIGHT_MARGIN = 10.0f;
|
|
|
|
|
|
constexpr int MAX_QUESTS = 5;
|
|
|
|
|
|
|
2026-03-10 05:18:45 -07: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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (toShow.empty()) return;
|
|
|
|
|
|
|
2026-03-12 16:52:12 -07:00
|
|
|
|
float screenH = ImGui::GetIO().DisplaySize.y > 0.0f ? ImGui::GetIO().DisplaySize.y : 720.0f;
|
2026-03-09 15:05:38 -07:00
|
|
|
|
|
2026-03-13 04:04:29 -07:00
|
|
|
|
// 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;
|
2026-03-12 16:52:12 -07:00
|
|
|
|
questTrackerPosInit_ = true;
|
|
|
|
|
|
}
|
2026-03-13 04:04:29 -07:00
|
|
|
|
// Recompute X from right offset every frame (handles window resize)
|
|
|
|
|
|
questTrackerPos_.x = screenW - questTrackerRightOffset_;
|
2026-03-12 16:52:12 -07:00
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(questTrackerPos_, ImGuiCond_Always);
|
2026-03-13 04:04:29 -07:00
|
|
|
|
ImGui::SetNextWindowSize(questTrackerSize_, ImGuiCond_FirstUseEver);
|
2026-03-09 15:05:38 -07:00
|
|
|
|
|
2026-03-13 04:04:29 -07:00
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse |
|
2026-03-12 16:52:12 -07:00
|
|
|
|
ImGuiWindowFlags_NoNav |
|
2026-03-13 04:04:29 -07:00
|
|
|
|
ImGuiWindowFlags_NoBringToFrontOnFocus;
|
2026-03-09 15:05:38 -07:00
|
|
|
|
|
|
|
|
|
|
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)) {
|
2026-03-10 05:18:45 -07:00
|
|
|
|
for (int i = 0; i < static_cast<int>(toShow.size()); ++i) {
|
|
|
|
|
|
const auto& q = *toShow[i];
|
2026-03-09 15:05:38 -07:00
|
|
|
|
|
2026-03-10 05:18:45 -07:00
|
|
|
|
// Clickable quest title — opens quest log
|
|
|
|
|
|
ImGui::PushID(q.questId);
|
2026-03-27 13:57:29 -07:00
|
|
|
|
ImVec4 titleCol = q.complete ? colors::kWarmGold
|
2026-03-09 15:05:38 -07:00
|
|
|
|
: ImVec4(1.0f, 1.0f, 0.85f, 1.0f);
|
2026-03-10 05:18:45 -07:00
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, titleCol);
|
|
|
|
|
|
if (ImGui::Selectable(q.title.c_str(), false,
|
2026-03-13 04:04:29 -07:00
|
|
|
|
ImGuiSelectableFlags_DontClosePopups, ImVec2(ImGui::GetContentRegionAvail().x, 0))) {
|
2026-03-10 05:26:16 -07:00
|
|
|
|
questLogScreen.openAndSelectQuest(q.questId);
|
2026-03-10 05:18:45 -07:00
|
|
|
|
}
|
2026-03-11 23:42:28 -07:00
|
|
|
|
if (ImGui::IsItemHovered() && !ImGui::IsPopupOpen("##QTCtx")) {
|
|
|
|
|
|
ImGui::SetTooltip("Click: open Quest Log | Right-click: tracking options");
|
2026-03-10 05:18:45 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor();
|
2026-03-11 23:42:28 -07: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);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (ImGui::MenuItem("Track")) {
|
|
|
|
|
|
gameHandler.setQuestTracked(q.questId, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 09:56:38 -07:00
|
|
|
|
if (gameHandler.isInGroup() && !q.complete) {
|
|
|
|
|
|
if (ImGui::MenuItem("Share Quest")) {
|
|
|
|
|
|
gameHandler.shareQuestWithParty(q.questId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 00:04:11 -07:00
|
|
|
|
if (!q.complete) {
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Abandon Quest")) {
|
|
|
|
|
|
gameHandler.abandonQuest(q.questId);
|
|
|
|
|
|
gameHandler.setQuestTracked(q.questId, false);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-11 23:42:28 -07:00
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
2026-03-10 05:18:45 -07:00
|
|
|
|
ImGui::PopID();
|
2026-03-09 15:05:38 -07:00
|
|
|
|
|
|
|
|
|
|
// Objectives line (condensed)
|
|
|
|
|
|
if (q.complete) {
|
2026-03-27 13:57:29 -07:00
|
|
|
|
ImGui::TextColored(colors::kActiveGreen, " (Complete)");
|
2026-03-09 15:05:38 -07:00
|
|
|
|
} else {
|
2026-03-12 04:42:48 -07:00
|
|
|
|
// Kill counts — green when complete, gray when in progress
|
2026-03-09 15:05:38 -07:00
|
|
|
|
for (const auto& [entry, progress] : q.killCounts) {
|
2026-03-12 04:42:48 -07:00
|
|
|
|
bool objDone = (progress.first >= progress.second && progress.second > 0);
|
2026-03-25 11:57:22 -07:00
|
|
|
|
ImVec4 objColor = objDone ? kColorGreen
|
2026-03-12 04:42:48 -07:00
|
|
|
|
: ImVec4(0.75f, 0.75f, 0.75f, 1.0f);
|
2026-03-11 00:13:09 -07:00
|
|
|
|
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()) {
|
2026-03-12 04:42:48 -07:00
|
|
|
|
ImGui::TextColored(objColor,
|
2026-03-11 00:13:09 -07:00
|
|
|
|
" %s: %u/%u", name.c_str(),
|
2026-03-10 07:45:53 -07:00
|
|
|
|
progress.first, progress.second);
|
|
|
|
|
|
} else {
|
2026-03-12 04:42:48 -07:00
|
|
|
|
ImGui::TextColored(objColor,
|
2026-03-10 07:45:53 -07:00
|
|
|
|
" %u/%u", progress.first, progress.second);
|
|
|
|
|
|
}
|
2026-03-09 15:05:38 -07:00
|
|
|
|
}
|
2026-03-12 04:42:48 -07:00
|
|
|
|
// Item counts — green when complete, gray when in progress
|
2026-03-09 15:05:38 -07:00
|
|
|
|
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;
|
2026-03-12 04:42:48 -07:00
|
|
|
|
bool objDone = (count >= required);
|
2026-03-25 11:57:22 -07:00
|
|
|
|
ImVec4 objColor = objDone ? kColorGreen
|
2026-03-12 04:42:48 -07:00
|
|
|
|
: ImVec4(0.75f, 0.75f, 0.75f, 1.0f);
|
2026-03-09 15:05:38 -07:00
|
|
|
|
const auto* info = gameHandler.getItemInfo(itemId);
|
|
|
|
|
|
const char* itemName = (info && !info->name.empty()) ? info->name.c_str() : nullptr;
|
2026-03-11 21:24:03 -07:00
|
|
|
|
|
|
|
|
|
|
// 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));
|
2026-03-12 13:22:20 -07:00
|
|
|
|
if (info && info->valid && ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
2026-03-11 21:24:03 -07:00
|
|
|
|
ImGui::SameLine(0, 3);
|
2026-03-12 04:42:48 -07:00
|
|
|
|
ImGui::TextColored(objColor,
|
2026-03-11 21:24:03 -07:00
|
|
|
|
"%s: %u/%u", itemName ? itemName : "Item", count, required);
|
2026-03-12 13:22:20 -07:00
|
|
|
|
if (info && info->valid && ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
2026-03-11 21:24:03 -07:00
|
|
|
|
} else if (itemName) {
|
2026-03-12 04:42:48 -07:00
|
|
|
|
ImGui::TextColored(objColor,
|
2026-03-09 15:05:38 -07:00
|
|
|
|
" %s: %u/%u", itemName, count, required);
|
2026-03-12 13:22:20 -07:00
|
|
|
|
if (info && info->valid && ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
2026-03-09 15:05:38 -07:00
|
|
|
|
} else {
|
2026-03-12 04:42:48 -07:00
|
|
|
|
ImGui::TextColored(objColor,
|
2026-03-09 15:05:38 -07:00
|
|
|
|
" Item: %u/%u", count, required);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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-10 05:18:45 -07:00
|
|
|
|
if (i < static_cast<int>(toShow.size()) - 1) {
|
2026-03-09 15:05:38 -07:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 16:52:12 -07:00
|
|
|
|
|
2026-03-13 04:04:29 -07:00
|
|
|
|
// Capture position and size after drag/resize
|
|
|
|
|
|
ImVec2 newPos = ImGui::GetWindowPos();
|
|
|
|
|
|
ImVec2 newSize = ImGui::GetWindowSize();
|
|
|
|
|
|
bool changed = false;
|
|
|
|
|
|
|
|
|
|
|
|
// 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-12 16:52:12 -07:00
|
|
|
|
if (std::abs(newPos.x - questTrackerPos_.x) > 0.5f ||
|
|
|
|
|
|
std::abs(newPos.y - questTrackerPos_.y) > 0.5f) {
|
|
|
|
|
|
questTrackerPos_ = newPos;
|
2026-03-13 04:04:29 -07:00
|
|
|
|
// Update right offset so resizes keep the new position anchored
|
|
|
|
|
|
questTrackerRightOffset_ = screenW - newPos.x;
|
|
|
|
|
|
changed = true;
|
2026-03-12 16:52:12 -07:00
|
|
|
|
}
|
2026-03-13 04:04:29 -07:00
|
|
|
|
if (std::abs(newSize.x - questTrackerSize_.x) > 0.5f ||
|
|
|
|
|
|
std::abs(newSize.y - questTrackerSize_.y) > 0.5f) {
|
|
|
|
|
|
questTrackerSize_ = newSize;
|
|
|
|
|
|
changed = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (changed) saveSettings();
|
2026-03-09 15:05:38 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleVar(2);
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 03:52:54 -07:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Raid Warning / Boss Emote Center-Screen Overlay
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderRaidWarningOverlay(game::GameHandler& gameHandler) {
|
|
|
|
|
|
// Scan chat history for new RAID_WARNING / RAID_BOSS_EMOTE messages
|
|
|
|
|
|
const auto& chatHistory = gameHandler.getChatHistory();
|
|
|
|
|
|
size_t newCount = chatHistory.size();
|
|
|
|
|
|
if (newCount > raidWarnChatSeenCount_) {
|
|
|
|
|
|
// Walk only the new messages (deque — iterate from back by skipping old ones)
|
|
|
|
|
|
size_t toScan = newCount - raidWarnChatSeenCount_;
|
|
|
|
|
|
size_t startIdx = newCount > toScan ? newCount - toScan : 0;
|
2026-03-12 06:12:37 -07:00
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
2026-03-12 03:52:54 -07:00
|
|
|
|
for (size_t i = startIdx; i < newCount; ++i) {
|
|
|
|
|
|
const auto& msg = chatHistory[i];
|
|
|
|
|
|
if (msg.type == game::ChatType::RAID_WARNING ||
|
|
|
|
|
|
msg.type == game::ChatType::RAID_BOSS_EMOTE ||
|
|
|
|
|
|
msg.type == game::ChatType::MONSTER_EMOTE) {
|
|
|
|
|
|
bool isBoss = (msg.type != game::ChatType::RAID_WARNING);
|
|
|
|
|
|
// Limit display text length to avoid giant overlay
|
|
|
|
|
|
std::string text = msg.message;
|
|
|
|
|
|
if (text.size() > 200) text = text.substr(0, 200) + "...";
|
|
|
|
|
|
raidWarnEntries_.push_back({text, 0.0f, isBoss});
|
|
|
|
|
|
if (raidWarnEntries_.size() > 3)
|
|
|
|
|
|
raidWarnEntries_.erase(raidWarnEntries_.begin());
|
|
|
|
|
|
}
|
2026-03-12 06:12:37 -07:00
|
|
|
|
// Whisper audio notification
|
|
|
|
|
|
if (msg.type == game::ChatType::WHISPER && renderer) {
|
|
|
|
|
|
if (auto* ui = renderer->getUiSoundManager())
|
|
|
|
|
|
ui->playWhisperReceived();
|
|
|
|
|
|
}
|
2026-03-12 03:52:54 -07:00
|
|
|
|
}
|
|
|
|
|
|
raidWarnChatSeenCount_ = newCount;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Age and remove expired entries
|
|
|
|
|
|
float dt = ImGui::GetIO().DeltaTime;
|
|
|
|
|
|
for (auto& e : raidWarnEntries_) e.age += dt;
|
|
|
|
|
|
raidWarnEntries_.erase(
|
|
|
|
|
|
std::remove_if(raidWarnEntries_.begin(), raidWarnEntries_.end(),
|
|
|
|
|
|
[](const RaidWarnEntry& e){ return e.age >= RaidWarnEntry::LIFETIME; }),
|
|
|
|
|
|
raidWarnEntries_.end());
|
|
|
|
|
|
|
|
|
|
|
|
if (raidWarnEntries_.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
|
|
float screenW = io.DisplaySize.x;
|
|
|
|
|
|
float screenH = io.DisplaySize.y;
|
|
|
|
|
|
ImDrawList* fg = ImGui::GetForegroundDrawList();
|
|
|
|
|
|
|
|
|
|
|
|
// Stack entries vertically near upper-center (below target frame area)
|
|
|
|
|
|
float baseY = screenH * 0.28f;
|
|
|
|
|
|
for (const auto& e : raidWarnEntries_) {
|
|
|
|
|
|
float alpha = std::clamp(1.0f - (e.age / RaidWarnEntry::LIFETIME), 0.0f, 1.0f);
|
|
|
|
|
|
// Fade in quickly, hold, then fade out last 20%
|
|
|
|
|
|
if (e.age < 0.3f) alpha = e.age / 0.3f;
|
|
|
|
|
|
|
|
|
|
|
|
// Truncate to fit screen width reasonably
|
|
|
|
|
|
const char* txt = e.text.c_str();
|
|
|
|
|
|
const float fontSize = 22.0f;
|
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
|
|
|
|
|
|
|
// Word-wrap manually: compute text size, center horizontally
|
|
|
|
|
|
float maxW = screenW * 0.7f;
|
|
|
|
|
|
ImVec2 textSz = font->CalcTextSizeA(fontSize, maxW, maxW, txt);
|
|
|
|
|
|
float tx = (screenW - textSz.x) * 0.5f;
|
|
|
|
|
|
|
|
|
|
|
|
ImU32 shadowCol = IM_COL32(0, 0, 0, static_cast<int>(alpha * 200));
|
|
|
|
|
|
ImU32 mainCol;
|
|
|
|
|
|
if (e.isBossEmote) {
|
|
|
|
|
|
mainCol = IM_COL32(255, 185, 60, static_cast<int>(alpha * 255)); // amber
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Raid warning: alternating red/yellow flash during first second
|
|
|
|
|
|
float flashT = std::fmod(e.age * 4.0f, 1.0f);
|
|
|
|
|
|
if (flashT < 0.5f)
|
|
|
|
|
|
mainCol = IM_COL32(255, 50, 50, static_cast<int>(alpha * 255));
|
|
|
|
|
|
else
|
|
|
|
|
|
mainCol = IM_COL32(255, 220, 50, static_cast<int>(alpha * 255));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Background dim box for readability
|
|
|
|
|
|
float pad = 8.0f;
|
|
|
|
|
|
fg->AddRectFilled(ImVec2(tx - pad, baseY - pad),
|
|
|
|
|
|
ImVec2(tx + textSz.x + pad, baseY + textSz.y + pad),
|
|
|
|
|
|
IM_COL32(0, 0, 0, static_cast<int>(alpha * 120)), 4.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// Shadow + main text
|
|
|
|
|
|
fg->AddText(font, fontSize, ImVec2(tx + 2.0f, baseY + 2.0f), shadowCol, txt,
|
|
|
|
|
|
nullptr, maxW);
|
|
|
|
|
|
fg->AddText(font, fontSize, ImVec2(tx, baseY), mainCol, txt,
|
|
|
|
|
|
nullptr, maxW);
|
|
|
|
|
|
|
|
|
|
|
|
baseY += textSz.y + 6.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
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Floating Combat Text (Phase 2)
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderCombatText(game::GameHandler& gameHandler) {
|
|
|
|
|
|
const auto& entries = gameHandler.getCombatText();
|
|
|
|
|
|
if (entries.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
2026-03-18 09:54:52 -07:00
|
|
|
|
if (!window) return;
|
|
|
|
|
|
const float screenW = static_cast<float>(window->getWidth());
|
|
|
|
|
|
const float screenH = static_cast<float>(window->getHeight());
|
|
|
|
|
|
|
|
|
|
|
|
// Camera for world-space projection
|
|
|
|
|
|
auto* appRenderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
rendering::Camera* camera = appRenderer ? appRenderer->getCamera() : nullptr;
|
|
|
|
|
|
glm::mat4 viewProj;
|
|
|
|
|
|
if (camera) viewProj = camera->getProjectionMatrix() * camera->getViewMatrix();
|
|
|
|
|
|
|
|
|
|
|
|
ImDrawList* drawList = ImGui::GetForegroundDrawList();
|
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
|
const float baseFontSize = ImGui::GetFontSize();
|
|
|
|
|
|
|
|
|
|
|
|
// HUD fallback: entries without world-space anchor use classic screen-position layout.
|
|
|
|
|
|
// We still need an ImGui window for those.
|
|
|
|
|
|
const float hudIncomingX = screenW * 0.40f;
|
|
|
|
|
|
const float hudOutgoingX = screenW * 0.68f;
|
|
|
|
|
|
int hudInIdx = 0, hudOutIdx = 0;
|
|
|
|
|
|
bool needsHudWindow = false;
|
|
|
|
|
|
|
|
|
|
|
|
for (const auto& entry : entries) {
|
|
|
|
|
|
const float alpha = 1.0f - (entry.age / game::CombatTextEntry::LIFETIME);
|
|
|
|
|
|
const bool outgoing = entry.isPlayerSource;
|
|
|
|
|
|
|
|
|
|
|
|
// --- Format text and color (identical logic for both world and HUD paths) ---
|
|
|
|
|
|
ImVec4 color;
|
|
|
|
|
|
char text[128];
|
|
|
|
|
|
switch (entry.type) {
|
|
|
|
|
|
case game::CombatTextEntry::MELEE_DAMAGE:
|
|
|
|
|
|
case game::CombatTextEntry::SPELL_DAMAGE:
|
|
|
|
|
|
snprintf(text, sizeof(text), "-%d", entry.amount);
|
|
|
|
|
|
color = outgoing ?
|
|
|
|
|
|
ImVec4(1.0f, 1.0f, 0.3f, alpha) :
|
|
|
|
|
|
ImVec4(1.0f, 0.3f, 0.3f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::CRIT_DAMAGE:
|
|
|
|
|
|
snprintf(text, sizeof(text), "-%d!", entry.amount);
|
|
|
|
|
|
color = outgoing ?
|
|
|
|
|
|
ImVec4(1.0f, 0.8f, 0.0f, alpha) :
|
|
|
|
|
|
ImVec4(1.0f, 0.5f, 0.0f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::HEAL:
|
|
|
|
|
|
snprintf(text, sizeof(text), "+%d", entry.amount);
|
|
|
|
|
|
color = ImVec4(0.3f, 1.0f, 0.3f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::CRIT_HEAL:
|
|
|
|
|
|
snprintf(text, sizeof(text), "+%d!", entry.amount);
|
|
|
|
|
|
color = ImVec4(0.3f, 1.0f, 0.3f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::MISS:
|
|
|
|
|
|
snprintf(text, sizeof(text), "Miss");
|
|
|
|
|
|
color = ImVec4(0.7f, 0.7f, 0.7f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::DODGE:
|
|
|
|
|
|
snprintf(text, sizeof(text), outgoing ? "Dodge" : "You Dodge");
|
|
|
|
|
|
color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha)
|
|
|
|
|
|
: ImVec4(0.4f, 0.9f, 1.0f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::PARRY:
|
|
|
|
|
|
snprintf(text, sizeof(text), outgoing ? "Parry" : "You Parry");
|
|
|
|
|
|
color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha)
|
|
|
|
|
|
: ImVec4(0.4f, 0.9f, 1.0f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::BLOCK:
|
|
|
|
|
|
if (entry.amount > 0)
|
|
|
|
|
|
snprintf(text, sizeof(text), outgoing ? "Block %d" : "You Block %d", entry.amount);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(text, sizeof(text), outgoing ? "Block" : "You Block");
|
|
|
|
|
|
color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha)
|
|
|
|
|
|
: ImVec4(0.4f, 0.9f, 1.0f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::EVADE:
|
|
|
|
|
|
snprintf(text, sizeof(text), outgoing ? "Evade" : "You Evade");
|
|
|
|
|
|
color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha)
|
|
|
|
|
|
: ImVec4(0.4f, 0.9f, 1.0f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::PERIODIC_DAMAGE:
|
|
|
|
|
|
snprintf(text, sizeof(text), "-%d", entry.amount);
|
|
|
|
|
|
color = outgoing ?
|
|
|
|
|
|
ImVec4(1.0f, 0.9f, 0.3f, alpha) :
|
|
|
|
|
|
ImVec4(1.0f, 0.4f, 0.4f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::PERIODIC_HEAL:
|
|
|
|
|
|
snprintf(text, sizeof(text), "+%d", entry.amount);
|
|
|
|
|
|
color = ImVec4(0.4f, 1.0f, 0.5f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::ENVIRONMENTAL: {
|
|
|
|
|
|
const char* envLabel = "";
|
|
|
|
|
|
switch (entry.powerType) {
|
|
|
|
|
|
case 0: envLabel = "Fatigue "; break;
|
|
|
|
|
|
case 1: envLabel = "Drowning "; break;
|
|
|
|
|
|
case 2: envLabel = ""; break;
|
|
|
|
|
|
case 3: envLabel = "Lava "; break;
|
|
|
|
|
|
case 4: envLabel = "Slime "; break;
|
|
|
|
|
|
case 5: envLabel = "Fire "; break;
|
|
|
|
|
|
default: envLabel = ""; break;
|
|
|
|
|
|
}
|
|
|
|
|
|
snprintf(text, sizeof(text), "%s-%d", envLabel, entry.amount);
|
|
|
|
|
|
color = ImVec4(0.9f, 0.5f, 0.2f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case game::CombatTextEntry::ENERGIZE:
|
|
|
|
|
|
snprintf(text, sizeof(text), "+%d", entry.amount);
|
|
|
|
|
|
switch (entry.powerType) {
|
|
|
|
|
|
case 1: color = ImVec4(1.0f, 0.2f, 0.2f, alpha); break;
|
|
|
|
|
|
case 2: color = ImVec4(1.0f, 0.6f, 0.1f, alpha); break;
|
|
|
|
|
|
case 3: color = ImVec4(1.0f, 0.9f, 0.2f, alpha); break;
|
|
|
|
|
|
case 6: color = ImVec4(0.3f, 0.9f, 0.8f, alpha); break;
|
|
|
|
|
|
default: color = ImVec4(0.3f, 0.6f, 1.0f, alpha); break;
|
2026-03-17 10:54:07 -07:00
|
|
|
|
}
|
2026-03-18 09:54:52 -07:00
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::POWER_DRAIN:
|
|
|
|
|
|
snprintf(text, sizeof(text), "-%d", entry.amount);
|
|
|
|
|
|
switch (entry.powerType) {
|
|
|
|
|
|
case 1: color = ImVec4(1.0f, 0.35f, 0.35f, alpha); break;
|
|
|
|
|
|
case 2: color = ImVec4(1.0f, 0.7f, 0.2f, alpha); break;
|
|
|
|
|
|
case 3: color = ImVec4(1.0f, 0.95f, 0.35f, alpha); break;
|
|
|
|
|
|
case 6: color = ImVec4(0.45f, 0.95f, 0.85f, alpha); break;
|
|
|
|
|
|
default: color = ImVec4(0.45f, 0.75f, 1.0f, alpha); break;
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::XP_GAIN:
|
|
|
|
|
|
snprintf(text, sizeof(text), "+%d XP", entry.amount);
|
|
|
|
|
|
color = ImVec4(0.7f, 0.3f, 1.0f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::IMMUNE:
|
|
|
|
|
|
snprintf(text, sizeof(text), "Immune!");
|
|
|
|
|
|
color = ImVec4(0.9f, 0.9f, 0.9f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::ABSORB:
|
|
|
|
|
|
if (entry.amount > 0)
|
|
|
|
|
|
snprintf(text, sizeof(text), "Absorbed %d", entry.amount);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(text, sizeof(text), "Absorbed");
|
|
|
|
|
|
color = ImVec4(0.5f, 0.8f, 1.0f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::RESIST:
|
|
|
|
|
|
if (entry.amount > 0)
|
|
|
|
|
|
snprintf(text, sizeof(text), "Resisted %d", entry.amount);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(text, sizeof(text), "Resisted");
|
|
|
|
|
|
color = ImVec4(0.7f, 0.7f, 0.7f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::DEFLECT:
|
|
|
|
|
|
snprintf(text, sizeof(text), outgoing ? "Deflect" : "You Deflect");
|
|
|
|
|
|
color = outgoing ? ImVec4(0.7f, 0.7f, 0.7f, alpha)
|
|
|
|
|
|
: ImVec4(0.5f, 0.9f, 1.0f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::REFLECT: {
|
|
|
|
|
|
const std::string& reflectName = entry.spellId ? gameHandler.getSpellName(entry.spellId) : "";
|
|
|
|
|
|
if (!reflectName.empty())
|
|
|
|
|
|
snprintf(text, sizeof(text), outgoing ? "Reflected: %s" : "Reflect: %s", reflectName.c_str());
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(text, sizeof(text), outgoing ? "Reflected" : "You Reflect");
|
|
|
|
|
|
color = outgoing ? ImVec4(0.85f, 0.75f, 1.0f, alpha)
|
|
|
|
|
|
: ImVec4(0.75f, 0.85f, 1.0f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case game::CombatTextEntry::PROC_TRIGGER: {
|
|
|
|
|
|
const std::string& procName = entry.spellId ? gameHandler.getSpellName(entry.spellId) : "";
|
|
|
|
|
|
if (!procName.empty())
|
|
|
|
|
|
snprintf(text, sizeof(text), "%s!", procName.c_str());
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(text, sizeof(text), "PROC!");
|
|
|
|
|
|
color = ImVec4(1.0f, 0.85f, 0.0f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case game::CombatTextEntry::DISPEL:
|
|
|
|
|
|
if (entry.spellId != 0) {
|
|
|
|
|
|
const std::string& dispelledName = gameHandler.getSpellName(entry.spellId);
|
|
|
|
|
|
if (!dispelledName.empty())
|
|
|
|
|
|
snprintf(text, sizeof(text), "Dispel %s", dispelledName.c_str());
|
2026-03-17 14:26:10 -07:00
|
|
|
|
else
|
2026-03-18 09:54:52 -07:00
|
|
|
|
snprintf(text, sizeof(text), "Dispel");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
snprintf(text, sizeof(text), "Dispel");
|
2026-03-17 14:26:10 -07:00
|
|
|
|
}
|
2026-03-18 09:54:52 -07:00
|
|
|
|
color = ImVec4(0.6f, 0.9f, 1.0f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::STEAL:
|
|
|
|
|
|
if (entry.spellId != 0) {
|
|
|
|
|
|
const std::string& stolenName = gameHandler.getSpellName(entry.spellId);
|
|
|
|
|
|
if (!stolenName.empty())
|
|
|
|
|
|
snprintf(text, sizeof(text), "Spellsteal %s", stolenName.c_str());
|
2026-03-12 09:41:45 -07:00
|
|
|
|
else
|
2026-03-18 09:54:52 -07:00
|
|
|
|
snprintf(text, sizeof(text), "Spellsteal");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
snprintf(text, sizeof(text), "Spellsteal");
|
2026-03-12 09:41:45 -07:00
|
|
|
|
}
|
2026-03-18 09:54:52 -07:00
|
|
|
|
color = ImVec4(0.8f, 0.7f, 1.0f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::INTERRUPT: {
|
|
|
|
|
|
const std::string& interruptedName = entry.spellId ? gameHandler.getSpellName(entry.spellId) : "";
|
|
|
|
|
|
if (!interruptedName.empty())
|
|
|
|
|
|
snprintf(text, sizeof(text), "Interrupt %s", interruptedName.c_str());
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(text, sizeof(text), "Interrupt");
|
|
|
|
|
|
color = ImVec4(1.0f, 0.6f, 0.9f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
case game::CombatTextEntry::INSTAKILL:
|
|
|
|
|
|
snprintf(text, sizeof(text), outgoing ? "Kill!" : "Killed!");
|
|
|
|
|
|
color = outgoing ? ImVec4(1.0f, 0.25f, 0.25f, alpha)
|
|
|
|
|
|
: ImVec4(1.0f, 0.1f, 0.1f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::HONOR_GAIN:
|
|
|
|
|
|
snprintf(text, sizeof(text), "+%d Honor", entry.amount);
|
|
|
|
|
|
color = ImVec4(1.0f, 0.85f, 0.0f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::GLANCING:
|
|
|
|
|
|
snprintf(text, sizeof(text), "~%d", entry.amount);
|
|
|
|
|
|
color = outgoing ?
|
|
|
|
|
|
ImVec4(0.75f, 0.75f, 0.5f, alpha) :
|
|
|
|
|
|
ImVec4(0.75f, 0.35f, 0.35f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::CRUSHING:
|
|
|
|
|
|
snprintf(text, sizeof(text), "%d!", entry.amount);
|
|
|
|
|
|
color = outgoing ?
|
|
|
|
|
|
ImVec4(1.0f, 0.55f, 0.1f, alpha) :
|
|
|
|
|
|
ImVec4(1.0f, 0.15f, 0.15f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
default:
|
|
|
|
|
|
snprintf(text, sizeof(text), "%d", entry.amount);
|
|
|
|
|
|
color = ImVec4(1.0f, 1.0f, 1.0f, alpha);
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- Rendering style ---
|
|
|
|
|
|
bool isCrit = (entry.type == game::CombatTextEntry::CRIT_DAMAGE ||
|
|
|
|
|
|
entry.type == game::CombatTextEntry::CRIT_HEAL);
|
|
|
|
|
|
float renderFontSize = isCrit ? baseFontSize * 1.35f : baseFontSize;
|
|
|
|
|
|
|
|
|
|
|
|
ImU32 shadowCol = IM_COL32(0, 0, 0, static_cast<int>(alpha * 180));
|
|
|
|
|
|
ImU32 textCol = ImGui::ColorConvertFloat4ToU32(color);
|
|
|
|
|
|
|
|
|
|
|
|
// --- Try world-space anchor if we have a destination entity ---
|
|
|
|
|
|
// Types that should always stay as HUD elements (no world anchor)
|
|
|
|
|
|
bool isHudOnly = (entry.type == game::CombatTextEntry::XP_GAIN ||
|
|
|
|
|
|
entry.type == game::CombatTextEntry::HONOR_GAIN ||
|
|
|
|
|
|
entry.type == game::CombatTextEntry::PROC_TRIGGER);
|
|
|
|
|
|
|
|
|
|
|
|
bool rendered = false;
|
|
|
|
|
|
if (!isHudOnly && camera && entry.dstGuid != 0) {
|
|
|
|
|
|
// Look up the destination entity's render position
|
|
|
|
|
|
glm::vec3 renderPos;
|
|
|
|
|
|
bool havePos = core::Application::getInstance().getRenderPositionForGuid(entry.dstGuid, renderPos);
|
|
|
|
|
|
if (!havePos) {
|
|
|
|
|
|
// Fallback to entity canonical position
|
|
|
|
|
|
auto entity = gameHandler.getEntityManager().getEntity(entry.dstGuid);
|
|
|
|
|
|
if (entity) {
|
2026-03-27 17:04:13 -07:00
|
|
|
|
auto* unit = entity->isUnit() ? static_cast<game::Unit*>(entity.get()) : nullptr;
|
2026-03-18 09:54:52 -07:00
|
|
|
|
if (unit) {
|
|
|
|
|
|
renderPos = core::coords::canonicalToRender(
|
|
|
|
|
|
glm::vec3(unit->getX(), unit->getY(), unit->getZ()));
|
|
|
|
|
|
havePos = true;
|
2026-03-13 23:00:49 -07:00
|
|
|
|
}
|
2026-03-18 09:54:52 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (havePos) {
|
|
|
|
|
|
// Float upward from above the entity's head
|
|
|
|
|
|
renderPos.z += 2.5f + entry.age * 1.2f;
|
|
|
|
|
|
|
|
|
|
|
|
// Project to screen
|
|
|
|
|
|
glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f);
|
|
|
|
|
|
if (clipPos.w > 0.01f) {
|
|
|
|
|
|
glm::vec3 ndc = glm::vec3(clipPos) / clipPos.w;
|
|
|
|
|
|
if (ndc.x >= -1.5f && ndc.x <= 1.5f && ndc.y >= -1.5f && ndc.y <= 1.5f) {
|
|
|
|
|
|
float sx = (ndc.x * 0.5f + 0.5f) * screenW;
|
|
|
|
|
|
float sy = (ndc.y * 0.5f + 0.5f) * screenH;
|
|
|
|
|
|
|
|
|
|
|
|
// Horizontal stagger using the random seed
|
|
|
|
|
|
sx += entry.xSeed * 40.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// Center the text horizontally on the projected point
|
|
|
|
|
|
ImVec2 ts = font->CalcTextSizeA(renderFontSize, FLT_MAX, 0.0f, text);
|
|
|
|
|
|
sx -= ts.x * 0.5f;
|
|
|
|
|
|
|
|
|
|
|
|
// Clamp to screen bounds
|
|
|
|
|
|
sx = std::max(2.0f, std::min(sx, screenW - ts.x - 2.0f));
|
|
|
|
|
|
|
|
|
|
|
|
drawList->AddText(font, renderFontSize,
|
|
|
|
|
|
ImVec2(sx + 1.0f, sy + 1.0f), shadowCol, text);
|
|
|
|
|
|
drawList->AddText(font, renderFontSize,
|
|
|
|
|
|
ImVec2(sx, sy), textCol, text);
|
|
|
|
|
|
rendered = true;
|
2026-03-13 23:00:49 -07:00
|
|
|
|
}
|
2026-03-13 20:45:26 -07:00
|
|
|
|
}
|
2026-03-18 09:54:52 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- HUD fallback for entries without world anchor or HUD-only types ---
|
|
|
|
|
|
if (!rendered) {
|
|
|
|
|
|
if (!needsHudWindow) {
|
|
|
|
|
|
needsHudWindow = true;
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(0, 0));
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(screenW, 400));
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration |
|
|
|
|
|
|
ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav;
|
|
|
|
|
|
ImGui::Begin("##CombatText", nullptr, flags);
|
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
|
|
|
|
float yOffset = 200.0f - entry.age * 60.0f;
|
|
|
|
|
|
int& idx = outgoing ? hudOutIdx : hudInIdx;
|
|
|
|
|
|
float baseX = outgoing ? hudOutgoingX : hudIncomingX;
|
2026-02-17 15:24:39 -08:00
|
|
|
|
float xOffset = baseX + (idx % 3 - 1) * 60.0f;
|
|
|
|
|
|
++idx;
|
2026-03-12 08:00:27 -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
|
|
|
|
ImGui::SetCursorPos(ImVec2(xOffset, yOffset));
|
2026-03-12 08:00:27 -07:00
|
|
|
|
ImVec2 screenPos = ImGui::GetCursorScreenPos();
|
|
|
|
|
|
|
2026-03-18 09:54:52 -07:00
|
|
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
2026-03-12 08:00:27 -07:00
|
|
|
|
dl->AddText(font, renderFontSize, ImVec2(screenPos.x + 1.0f, screenPos.y + 1.0f),
|
|
|
|
|
|
shadowCol, text);
|
|
|
|
|
|
dl->AddText(font, renderFontSize, screenPos, textCol, text);
|
|
|
|
|
|
|
|
|
|
|
|
ImVec2 ts = font->CalcTextSizeA(renderFontSize, FLT_MAX, 0.0f, text);
|
|
|
|
|
|
ImGui::Dummy(ts);
|
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
|
|
|
|
|
|
|
|
|
|
if (needsHudWindow) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
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-12 04:04:27 -07:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// DPS / HPS Meter
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderDPSMeter(game::GameHandler& gameHandler) {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (!settingsPanel_.showDPSMeter_) return;
|
2026-03-12 04:04:27 -07:00
|
|
|
|
if (gameHandler.getState() != game::WorldState::IN_WORLD) return;
|
|
|
|
|
|
|
|
|
|
|
|
const float dt = ImGui::GetIO().DeltaTime;
|
|
|
|
|
|
|
|
|
|
|
|
// Track combat duration for accurate DPS denominator in short fights
|
|
|
|
|
|
bool inCombat = gameHandler.isInCombat();
|
2026-03-12 11:40:31 -07:00
|
|
|
|
if (inCombat && !dpsWasInCombat_) {
|
|
|
|
|
|
// Just entered combat — reset encounter accumulators
|
|
|
|
|
|
dpsEncounterDamage_ = 0.0f;
|
|
|
|
|
|
dpsEncounterHeal_ = 0.0f;
|
|
|
|
|
|
dpsLogSeenCount_ = gameHandler.getCombatLog().size();
|
|
|
|
|
|
dpsCombatAge_ = 0.0f;
|
|
|
|
|
|
}
|
2026-03-12 04:04:27 -07:00
|
|
|
|
if (inCombat) {
|
|
|
|
|
|
dpsCombatAge_ += dt;
|
2026-03-12 11:40:31 -07:00
|
|
|
|
// Scan any new log entries since last frame
|
|
|
|
|
|
const auto& log = gameHandler.getCombatLog();
|
|
|
|
|
|
while (dpsLogSeenCount_ < log.size()) {
|
|
|
|
|
|
const auto& e = log[dpsLogSeenCount_++];
|
|
|
|
|
|
if (!e.isPlayerSource) continue;
|
|
|
|
|
|
switch (e.type) {
|
|
|
|
|
|
case game::CombatTextEntry::MELEE_DAMAGE:
|
|
|
|
|
|
case game::CombatTextEntry::SPELL_DAMAGE:
|
|
|
|
|
|
case game::CombatTextEntry::CRIT_DAMAGE:
|
|
|
|
|
|
case game::CombatTextEntry::PERIODIC_DAMAGE:
|
2026-03-17 18:51:48 -07:00
|
|
|
|
case game::CombatTextEntry::GLANCING:
|
|
|
|
|
|
case game::CombatTextEntry::CRUSHING:
|
2026-03-12 11:40:31 -07:00
|
|
|
|
dpsEncounterDamage_ += static_cast<float>(e.amount);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::HEAL:
|
|
|
|
|
|
case game::CombatTextEntry::CRIT_HEAL:
|
|
|
|
|
|
case game::CombatTextEntry::PERIODIC_HEAL:
|
|
|
|
|
|
dpsEncounterHeal_ += static_cast<float>(e.amount);
|
|
|
|
|
|
break;
|
|
|
|
|
|
default: break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 04:04:27 -07:00
|
|
|
|
} else if (dpsWasInCombat_) {
|
2026-03-12 11:40:31 -07:00
|
|
|
|
// Just left combat — keep encounter totals but stop accumulating
|
2026-03-12 04:04:27 -07:00
|
|
|
|
}
|
|
|
|
|
|
dpsWasInCombat_ = inCombat;
|
|
|
|
|
|
|
|
|
|
|
|
// Sum all player-source damage and healing in the current combat-text window
|
|
|
|
|
|
float totalDamage = 0.0f, totalHeal = 0.0f;
|
|
|
|
|
|
for (const auto& e : gameHandler.getCombatText()) {
|
|
|
|
|
|
if (!e.isPlayerSource) continue;
|
|
|
|
|
|
switch (e.type) {
|
|
|
|
|
|
case game::CombatTextEntry::MELEE_DAMAGE:
|
|
|
|
|
|
case game::CombatTextEntry::SPELL_DAMAGE:
|
|
|
|
|
|
case game::CombatTextEntry::CRIT_DAMAGE:
|
|
|
|
|
|
case game::CombatTextEntry::PERIODIC_DAMAGE:
|
2026-03-17 18:51:48 -07:00
|
|
|
|
case game::CombatTextEntry::GLANCING:
|
|
|
|
|
|
case game::CombatTextEntry::CRUSHING:
|
2026-03-12 04:04:27 -07:00
|
|
|
|
totalDamage += static_cast<float>(e.amount);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case game::CombatTextEntry::HEAL:
|
|
|
|
|
|
case game::CombatTextEntry::CRIT_HEAL:
|
|
|
|
|
|
case game::CombatTextEntry::PERIODIC_HEAL:
|
|
|
|
|
|
totalHeal += static_cast<float>(e.amount);
|
|
|
|
|
|
break;
|
|
|
|
|
|
default: break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 11:40:31 -07:00
|
|
|
|
// Only show if there's something to report (rolling window or lingering encounter data)
|
|
|
|
|
|
if (totalDamage < 1.0f && totalHeal < 1.0f && !inCombat &&
|
|
|
|
|
|
dpsEncounterDamage_ < 1.0f && dpsEncounterHeal_ < 1.0f) return;
|
2026-03-12 04:04:27 -07:00
|
|
|
|
|
|
|
|
|
|
// DPS window = min(combat age, combat-text lifetime) to avoid under-counting
|
|
|
|
|
|
// at the start of a fight and over-counting when entries expire.
|
|
|
|
|
|
float window = std::min(dpsCombatAge_, game::CombatTextEntry::LIFETIME);
|
|
|
|
|
|
if (window < 0.1f) window = 0.1f;
|
|
|
|
|
|
|
|
|
|
|
|
float dps = totalDamage / window;
|
|
|
|
|
|
float hps = totalHeal / window;
|
|
|
|
|
|
|
|
|
|
|
|
// Format numbers with K/M suffix for readability
|
|
|
|
|
|
auto fmtNum = [](float v, char* buf, int bufSz) {
|
|
|
|
|
|
if (v >= 1e6f) snprintf(buf, bufSz, "%.1fM", v / 1e6f);
|
|
|
|
|
|
else if (v >= 1000.f) snprintf(buf, bufSz, "%.1fK", v / 1000.f);
|
|
|
|
|
|
else snprintf(buf, bufSz, "%.0f", v);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
char dpsBuf[16], hpsBuf[16];
|
|
|
|
|
|
fmtNum(dps, dpsBuf, sizeof(dpsBuf));
|
|
|
|
|
|
fmtNum(hps, hpsBuf, sizeof(hpsBuf));
|
|
|
|
|
|
|
|
|
|
|
|
// Position: small floating label just above the action bar, right of center
|
|
|
|
|
|
auto* appWin = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = appWin ? static_cast<float>(appWin->getWidth()) : 1280.0f;
|
|
|
|
|
|
float screenH = appWin ? static_cast<float>(appWin->getHeight()) : 720.0f;
|
|
|
|
|
|
|
2026-03-12 11:40:31 -07:00
|
|
|
|
// Show encounter row when fight has been going long enough (> 3s)
|
|
|
|
|
|
bool showEnc = (dpsCombatAge_ > 3.0f || (!inCombat && dpsEncounterDamage_ > 0.0f));
|
|
|
|
|
|
float encDPS = (dpsCombatAge_ > 0.1f) ? dpsEncounterDamage_ / dpsCombatAge_ : 0.0f;
|
|
|
|
|
|
float encHPS = (dpsCombatAge_ > 0.1f) ? dpsEncounterHeal_ / dpsCombatAge_ : 0.0f;
|
|
|
|
|
|
|
|
|
|
|
|
char encDpsBuf[16], encHpsBuf[16];
|
|
|
|
|
|
fmtNum(encDPS, encDpsBuf, sizeof(encDpsBuf));
|
|
|
|
|
|
fmtNum(encHPS, encHpsBuf, sizeof(encHpsBuf));
|
|
|
|
|
|
|
2026-03-12 04:04:27 -07:00
|
|
|
|
constexpr float WIN_W = 90.0f;
|
2026-03-12 11:40:31 -07:00
|
|
|
|
// Extra rows for encounter DPS/HPS if active
|
|
|
|
|
|
int extraRows = 0;
|
|
|
|
|
|
if (showEnc && encDPS > 0.5f) ++extraRows;
|
|
|
|
|
|
if (showEnc && encHPS > 0.5f) ++extraRows;
|
|
|
|
|
|
float WIN_H = 18.0f + extraRows * 14.0f;
|
|
|
|
|
|
if (dps > 0.5f || hps > 0.5f) WIN_H = std::max(WIN_H, 36.0f);
|
2026-03-12 04:04:27 -07:00
|
|
|
|
float wx = screenW * 0.5f + 160.0f; // right of cast bar
|
|
|
|
|
|
float wy = screenH - 130.0f; // above action bar area
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoNav |
|
|
|
|
|
|
ImGuiWindowFlags_NoInputs;
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(wx, wy), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(WIN_W, WIN_H), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowBgAlpha(0.55f);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(4, 3));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.3f, 0.3f, 0.3f, 0.7f));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##DPSMeter", nullptr, flags)) {
|
|
|
|
|
|
if (dps > 0.5f) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.15f, 1.0f), "%s", dpsBuf);
|
|
|
|
|
|
ImGui::SameLine(0, 2);
|
|
|
|
|
|
ImGui::TextDisabled("dps");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (hps > 0.5f) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.35f, 1.0f, 0.35f, 1.0f), "%s", hpsBuf);
|
|
|
|
|
|
ImGui::SameLine(0, 2);
|
|
|
|
|
|
ImGui::TextDisabled("hps");
|
|
|
|
|
|
}
|
2026-03-12 11:40:31 -07:00
|
|
|
|
// Encounter totals (full-fight average, shown when fight > 3s)
|
|
|
|
|
|
if (showEnc && encDPS > 0.5f) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.65f, 0.25f, 0.80f), "%s", encDpsBuf);
|
|
|
|
|
|
ImGui::SameLine(0, 2);
|
|
|
|
|
|
ImGui::TextDisabled("enc");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (showEnc && encHPS > 0.5f) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.50f, 1.0f, 0.50f, 0.80f), "%s", encHpsBuf);
|
|
|
|
|
|
ImGui::SameLine(0, 2);
|
|
|
|
|
|
ImGui::TextDisabled("enc");
|
|
|
|
|
|
}
|
2026-03-12 04:04:27 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::PopStyleVar(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 17:01:38 -07:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Nameplates — world-space health bars projected to screen
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderNameplates(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (gameHandler.getState() != game::WorldState::IN_WORLD) return;
|
|
|
|
|
|
|
2026-03-18 03:09:43 -07:00
|
|
|
|
// Reset mouseover each frame; we'll set it below when the cursor is over a nameplate
|
|
|
|
|
|
gameHandler.setMouseoverGuid(0);
|
|
|
|
|
|
|
2026-03-09 17:01:38 -07:00
|
|
|
|
auto* appRenderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
if (!appRenderer) return;
|
|
|
|
|
|
rendering::Camera* camera = appRenderer->getCamera();
|
|
|
|
|
|
if (!camera) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
if (!window) return;
|
|
|
|
|
|
const float screenW = static_cast<float>(window->getWidth());
|
|
|
|
|
|
const float screenH = static_cast<float>(window->getHeight());
|
|
|
|
|
|
|
|
|
|
|
|
const glm::mat4 viewProj = camera->getProjectionMatrix() * camera->getViewMatrix();
|
|
|
|
|
|
const glm::vec3 camPos = camera->getPosition();
|
|
|
|
|
|
const uint64_t playerGuid = gameHandler.getPlayerGuid();
|
|
|
|
|
|
const uint64_t targetGuid = gameHandler.getTargetGuid();
|
|
|
|
|
|
|
2026-03-11 00:29:35 -07: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-03-09 17:01:38 -07:00
|
|
|
|
ImDrawList* drawList = ImGui::GetBackgroundDrawList();
|
|
|
|
|
|
|
|
|
|
|
|
for (const auto& [guid, entityPtr] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
|
if (!entityPtr || guid == playerGuid) continue;
|
|
|
|
|
|
|
2026-03-27 17:04:13 -07:00
|
|
|
|
if (!entityPtr->isUnit()) continue;
|
|
|
|
|
|
auto* unit = static_cast<game::Unit*>(entityPtr.get());
|
|
|
|
|
|
if (unit->getMaxHealth() == 0) continue;
|
2026-03-09 17:01:38 -07:00
|
|
|
|
|
2026-03-10 07:25:04 -07:00
|
|
|
|
bool isPlayer = (entityPtr->getType() == game::ObjectType::PLAYER);
|
2026-03-10 05:38:52 -07:00
|
|
|
|
bool isTarget = (guid == targetGuid);
|
2026-03-09 18:45:28 -07:00
|
|
|
|
|
2026-03-18 11:43:39 -07:00
|
|
|
|
// Player nameplates use Shift+V toggle; NPC/enemy nameplates use V toggle
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (isPlayer && !settingsPanel_.showFriendlyNameplates_) continue;
|
2026-03-10 07:25:04 -07:00
|
|
|
|
if (!isPlayer && !showNameplates_) continue;
|
|
|
|
|
|
|
2026-03-11 16:54:30 -07:00
|
|
|
|
// For corpses (dead units), only show a minimal grey nameplate if selected
|
|
|
|
|
|
bool isCorpse = (unit->getHealth() == 0);
|
|
|
|
|
|
if (isCorpse && !isTarget) continue;
|
|
|
|
|
|
|
2026-03-10 19:49:33 -07: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()));
|
|
|
|
|
|
}
|
2026-03-09 17:01:38 -07:00
|
|
|
|
renderPos.z += 2.3f;
|
|
|
|
|
|
|
2026-03-10 07:25:04 -07:00
|
|
|
|
// Cull distance: target or other players up to 40 units; NPC others up to 20 units
|
2026-03-27 16:33:16 -07:00
|
|
|
|
glm::vec3 nameDelta = renderPos - camPos;
|
|
|
|
|
|
float distSq = glm::dot(nameDelta, nameDelta);
|
2026-03-10 07:25:04 -07:00
|
|
|
|
float cullDist = (isTarget || isPlayer) ? 40.0f : 20.0f;
|
2026-03-27 16:33:16 -07:00
|
|
|
|
if (distSq > cullDist * cullDist) continue;
|
2026-03-09 17:01:38 -07:00
|
|
|
|
|
|
|
|
|
|
// Project to clip space
|
|
|
|
|
|
glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f);
|
|
|
|
|
|
if (clipPos.w <= 0.01f) continue; // Behind camera
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec3 ndc = glm::vec3(clipPos) / clipPos.w;
|
|
|
|
|
|
if (ndc.x < -1.2f || ndc.x > 1.2f || ndc.y < -1.2f || ndc.y > 1.2f) continue;
|
|
|
|
|
|
|
2026-03-09 17:59:55 -07: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).
|
2026-03-09 17:01:38 -07:00
|
|
|
|
float sx = (ndc.x * 0.5f + 0.5f) * screenW;
|
2026-03-09 17:59:55 -07:00
|
|
|
|
float sy = (ndc.y * 0.5f + 0.5f) * screenH;
|
2026-03-09 17:01:38 -07:00
|
|
|
|
|
2026-03-10 07:25:04 -07:00
|
|
|
|
// Fade out in the last 5 units of cull range
|
2026-03-27 16:33:16 -07:00
|
|
|
|
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;
|
2026-03-09 17:01:38 -07:00
|
|
|
|
auto A = [&](int v) { return static_cast<int>(v * alpha); };
|
|
|
|
|
|
|
2026-03-11 16:54:30 -07:00
|
|
|
|
// Bar colour by hostility (grey for corpses)
|
2026-03-09 17:01:38 -07:00
|
|
|
|
ImU32 barColor, bgColor;
|
2026-03-11 16:54:30 -07:00
|
|
|
|
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()) {
|
2026-03-21 08:33:54 -07:00
|
|
|
|
// 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));
|
|
|
|
|
|
}
|
2026-03-12 15:36:25 -07: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));
|
|
|
|
|
|
}
|
2026-03-09 17:01:38 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
barColor = IM_COL32(60, 200, 80, A(200));
|
|
|
|
|
|
bgColor = IM_COL32(25, 100, 35, A(160));
|
|
|
|
|
|
}
|
2026-03-17 19:47:45 -07: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-03-18 10:07:40 -07: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-03-17 19:47:45 -07:00
|
|
|
|
// Border: gold = currently selected, orange = targeting player, dark = default
|
2026-03-10 05:38:52 -07:00
|
|
|
|
ImU32 borderColor = isTarget
|
2026-03-09 17:01:38 -07:00
|
|
|
|
? IM_COL32(255, 215, 0, A(255))
|
2026-03-17 19:47:45 -07:00
|
|
|
|
: isTargetingPlayer
|
|
|
|
|
|
? IM_COL32(255, 140, 0, A(220)) // orange = this mob is targeting you
|
|
|
|
|
|
: IM_COL32(20, 20, 20, A(180));
|
2026-03-09 17:01:38 -07:00
|
|
|
|
|
|
|
|
|
|
// Bar geometry
|
2026-03-31 10:07:58 +03:00
|
|
|
|
const float barW = 80.0f * settingsPanel_.nameplateScale_;
|
|
|
|
|
|
const float barH = 8.0f * settingsPanel_.nameplateScale_;
|
2026-03-09 17:01:38 -07:00
|
|
|
|
const float barX = sx - barW * 0.5f;
|
|
|
|
|
|
|
2026-03-29 19:08:51 -07: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-03-09 17:01:38 -07:00
|
|
|
|
|
|
|
|
|
|
drawList->AddRectFilled(ImVec2(barX, sy), ImVec2(barX + barW, sy + barH), bgColor, 2.0f);
|
2026-03-11 16:54:30 -07:00
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
2026-03-09 17:01:38 -07:00
|
|
|
|
drawList->AddRect (ImVec2(barX - 1.0f, sy - 1.0f), ImVec2(barX + barW + 1.0f, sy + barH + 1.0f), borderColor, 2.0f);
|
|
|
|
|
|
|
2026-03-18 10:07:40 -07: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-03-12 03:44:32 -07: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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Cast bar below health bar when unit is casting
|
|
|
|
|
|
float castBarBaseY = sy + barH + 2.0f;
|
2026-03-12 11:31:45 -07:00
|
|
|
|
float nameplateBottom = castBarBaseY; // tracks lowest drawn element for debuff dots
|
2026-03-12 03:44:32 -07:00
|
|
|
|
{
|
|
|
|
|
|
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);
|
2026-03-31 10:07:58 +03:00
|
|
|
|
const float cbH = 6.0f * settingsPanel_.nameplateScale_;
|
2026-03-12 03:44:32 -07:00
|
|
|
|
|
2026-03-18 11:29:08 -07:00
|
|
|
|
// Spell icon + name above the cast bar
|
2026-03-12 03:44:32 -07:00
|
|
|
|
const std::string& spellName = gameHandler.getSpellName(cs->spellId);
|
2026-03-18 11:29:08 -07:00
|
|
|
|
{
|
|
|
|
|
|
auto* castAm = core::Application::getInstance().getAssetManager();
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
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 03:44:32 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 19:43:19 -07:00
|
|
|
|
// Cast bar: green = interruptible, red = uninterruptible; both pulse when >80% complete
|
|
|
|
|
|
ImU32 cbBg = IM_COL32(30, 25, 40, A(180));
|
2026-03-12 04:21:33 -07:00
|
|
|
|
ImU32 cbFill;
|
|
|
|
|
|
if (castPct > 0.8f && unit->isHostile()) {
|
|
|
|
|
|
float pulse = 0.7f + 0.3f * std::sin(static_cast<float>(ImGui::GetTime()) * 8.0f);
|
2026-03-17 19:43:19 -07:00
|
|
|
|
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-03-12 04:21:33 -07:00
|
|
|
|
} else {
|
2026-03-17 19:43:19 -07:00
|
|
|
|
cbFill = cs->interruptible
|
|
|
|
|
|
? IM_COL32(50, 190, 50, A(200)) // green = interruptible
|
|
|
|
|
|
: IM_COL32(190, 40, 40, A(200)); // red = uninterruptible
|
2026-03-12 04:21:33 -07:00
|
|
|
|
}
|
2026-03-12 03:44:32 -07: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);
|
2026-03-12 11:31:45 -07:00
|
|
|
|
nameplateBottom = castBarBaseY + cbH + 2.0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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();
|
2026-03-31 10:07:58 +03:00
|
|
|
|
const float dotSize = 6.0f * settingsPanel_.nameplateScale_;
|
2026-03-12 11:31:45 -07:00
|
|
|
|
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-03-12 13:36:06 -07:00
|
|
|
|
|
2026-03-18 11:21:14 -07: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)));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Spell name + duration tooltip on hover
|
2026-03-12 13:36:06 -07:00
|
|
|
|
{
|
|
|
|
|
|
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);
|
2026-03-18 11:21:14 -07:00
|
|
|
|
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-03-12 13:36:06 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 11:31:45 -07:00
|
|
|
|
dotX += dotSize + dotGap;
|
|
|
|
|
|
if (dotX + dotSize > barX + barW) break;
|
2026-03-12 03:44:32 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 17:04:14 -07:00
|
|
|
|
// Name + level label above health bar
|
|
|
|
|
|
uint32_t level = unit->getLevel();
|
2026-03-10 15:56:41 -07:00
|
|
|
|
const std::string& unitName = unit->getName();
|
2026-03-09 17:04:14 -07:00
|
|
|
|
char labelBuf[96];
|
2026-03-10 15:56:41 -07:00
|
|
|
|
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());
|
2026-03-11 18:53:23 -07:00
|
|
|
|
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");
|
|
|
|
|
|
}
|
2026-03-10 15:56:41 -07:00
|
|
|
|
} else if (level > 0) {
|
2026-03-09 17:04:14 -07:00
|
|
|
|
uint32_t playerLevel = gameHandler.getPlayerLevel();
|
|
|
|
|
|
// Show skull for units more than 10 levels above the player
|
|
|
|
|
|
if (playerLevel > 0 && level > playerLevel + 10)
|
2026-03-10 15:56:41 -07:00
|
|
|
|
snprintf(labelBuf, sizeof(labelBuf), "?? %s", unitName.c_str());
|
2026-03-09 17:04:14 -07:00
|
|
|
|
else
|
2026-03-10 15:56:41 -07:00
|
|
|
|
snprintf(labelBuf, sizeof(labelBuf), "%u %s", level, unitName.c_str());
|
2026-03-09 17:04:14 -07:00
|
|
|
|
} else {
|
2026-03-10 15:56:41 -07:00
|
|
|
|
snprintf(labelBuf, sizeof(labelBuf), "%s", unitName.c_str());
|
2026-03-09 17:04:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImVec2 textSize = ImGui::CalcTextSize(labelBuf);
|
2026-03-09 17:01:38 -07:00
|
|
|
|
float nameX = sx - textSize.x * 0.5f;
|
|
|
|
|
|
float nameY = sy - barH - 12.0f;
|
2026-03-12 08:30:27 -07:00
|
|
|
|
// Name color: players get WoW class colors; NPCs use hostility (red/yellow)
|
|
|
|
|
|
ImU32 nameColor;
|
|
|
|
|
|
if (isPlayer) {
|
2026-03-12 08:33:34 -07:00
|
|
|
|
// 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-12 08:30:27 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
nameColor = unit->isHostile()
|
2026-03-10 07:25:04 -07:00
|
|
|
|
? IM_COL32(220, 80, 80, A(230)) // red — hostile NPC
|
|
|
|
|
|
: IM_COL32(240, 200, 100, A(230)); // yellow — friendly NPC
|
2026-03-12 08:30:27 -07:00
|
|
|
|
}
|
2026-03-18 10:05:49 -07:00
|
|
|
|
// Sub-label below the name: guild tag for players, subtitle for NPCs
|
|
|
|
|
|
std::string subLabel;
|
2026-03-18 09:44:43 -07:00
|
|
|
|
if (isPlayer) {
|
|
|
|
|
|
uint32_t guildId = gameHandler.getEntityGuildId(guid);
|
|
|
|
|
|
if (guildId != 0) {
|
|
|
|
|
|
const std::string& gn = gameHandler.lookupGuildName(guildId);
|
2026-03-18 10:05:49 -07:00
|
|
|
|
if (!gn.empty()) subLabel = "<" + gn + ">";
|
2026-03-18 09:44:43 -07:00
|
|
|
|
}
|
2026-03-18 10:05:49 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
// NPC subtitle (e.g. "<Reagent Vendor>", "<Innkeeper>")
|
|
|
|
|
|
std::string sub = gameHandler.getCachedCreatureSubName(unit->getEntry());
|
|
|
|
|
|
if (!sub.empty()) subLabel = "<" + sub + ">";
|
2026-03-18 09:44:43 -07:00
|
|
|
|
}
|
2026-03-18 10:05:49 -07:00
|
|
|
|
if (!subLabel.empty()) nameY -= 10.0f; // shift name up for sub-label line
|
2026-03-18 09:44:43 -07:00
|
|
|
|
|
2026-03-09 17:04:14 -07:00
|
|
|
|
drawList->AddText(ImVec2(nameX + 1.0f, nameY + 1.0f), IM_COL32(0, 0, 0, A(160)), labelBuf);
|
2026-03-09 17:18:18 -07:00
|
|
|
|
drawList->AddText(ImVec2(nameX, nameY), nameColor, labelBuf);
|
2026-03-10 05:50:26 -07:00
|
|
|
|
|
2026-03-18 10:05:49 -07: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-18 09:44:43 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:48:53 -07: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-10 06:10:29 -07:00
|
|
|
|
// Raid mark (if any) to the left of the name
|
|
|
|
|
|
{
|
2026-03-27 14:47:58 -07:00
|
|
|
|
static constexpr struct { const char* sym; ImU32 col; } kNPMarks[] = {
|
2026-03-10 06:10:29 -07:00
|
|
|
|
{ "\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-11 00:29:35 -07:00
|
|
|
|
|
|
|
|
|
|
// Quest kill objective indicator: small yellow sword icon to the right of the name
|
2026-03-12 14:18:22 -07:00
|
|
|
|
float questIconX = nameX + textSize.x + 4.0f;
|
2026-03-11 00:29:35 -07:00
|
|
|
|
if (!isPlayer && questKillEntries.count(unit->getEntry())) {
|
|
|
|
|
|
const char* objSym = "\xe2\x9a\x94"; // ⚔ crossed swords (UTF-8)
|
2026-03-12 14:18:22 -07:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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-11 00:29:35 -07:00
|
|
|
|
}
|
2026-03-10 06:10:29 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-29 21:54:57 -07: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.
|
2026-03-12 00:26:47 -07:00
|
|
|
|
if (!ImGui::GetIO().WantCaptureMouse) {
|
2026-03-10 05:50:26 -07:00
|
|
|
|
ImVec2 mouse = ImGui::GetIO().MousePos;
|
2026-03-29 21:54:57 -07:00
|
|
|
|
float hitLeft = std::min(nameX, barX) - 2.0f;
|
|
|
|
|
|
float hitRight = std::max(nameX + textSize.x, barX + barW) + 2.0f;
|
2026-03-10 05:50:26 -07:00
|
|
|
|
float ny0 = nameY - 1.0f;
|
|
|
|
|
|
float ny1 = sy + barH + 2.0f;
|
2026-03-29 21:54:57 -07:00
|
|
|
|
float nx0 = hitLeft;
|
|
|
|
|
|
float nx1 = hitRight;
|
2026-03-10 05:50:26 -07:00
|
|
|
|
if (mouse.x >= nx0 && mouse.x <= nx1 && mouse.y >= ny0 && mouse.y <= ny1) {
|
2026-03-18 03:09:43 -07:00
|
|
|
|
// Track mouseover for [target=mouseover] macro conditionals
|
|
|
|
|
|
gameHandler.setMouseoverGuid(guid);
|
2026-03-27 18:21:47 -07:00
|
|
|
|
// 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();
|
2026-03-12 00:26:47 -07:00
|
|
|
|
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
|
|
|
|
|
gameHandler.setTarget(guid);
|
|
|
|
|
|
} else if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
|
|
|
|
|
|
nameplateCtxGuid_ = guid;
|
|
|
|
|
|
nameplateCtxPos_ = mouse;
|
|
|
|
|
|
ImGui::OpenPopup("##NameplateCtx");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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")) {
|
2026-03-31 08:53:14 +03:00
|
|
|
|
chatPanel_.setWhisperTarget(ctxName);
|
2026-03-12 00:26:47 -07:00
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Invite to Group"))
|
|
|
|
|
|
gameHandler.inviteToGroup(ctxName);
|
2026-03-12 10:27:51 -07:00
|
|
|
|
if (ImGui::MenuItem("Trade"))
|
|
|
|
|
|
gameHandler.initiateTrade(nameplateCtxGuid_);
|
|
|
|
|
|
if (ImGui::MenuItem("Duel"))
|
|
|
|
|
|
gameHandler.proposeDuel(nameplateCtxGuid_);
|
|
|
|
|
|
if (ImGui::MenuItem("Inspect")) {
|
|
|
|
|
|
gameHandler.setTarget(nameplateCtxGuid_);
|
|
|
|
|
|
gameHandler.inspectTarget();
|
|
|
|
|
|
showInspectWindow_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
2026-03-12 00:26:47 -07:00
|
|
|
|
if (ImGui::MenuItem("Add Friend"))
|
|
|
|
|
|
gameHandler.addFriend(ctxName);
|
|
|
|
|
|
if (ImGui::MenuItem("Ignore"))
|
|
|
|
|
|
gameHandler.addIgnore(ctxName);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
nameplateCtxGuid_ = 0;
|
2026-03-10 05:50:26 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 00:26:47 -07:00
|
|
|
|
ImGui::End();
|
2026-03-09 17:01:38 -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
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Party Frames (Phase 4)
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderPartyFrames(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isInGroup()) return;
|
|
|
|
|
|
|
2026-03-12 07:58:36 -07:00
|
|
|
|
auto* assetMgr = core::Application::getInstance().getAssetManager();
|
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
|
|
|
|
const auto& partyData = gameHandler.getPartyData();
|
2026-03-10 05:26:16 -07:00
|
|
|
|
const bool isRaid = (partyData.groupType == 1);
|
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
|
|
|
|
float frameY = 120.0f;
|
|
|
|
|
|
|
2026-03-10 05:26:16 -07:00
|
|
|
|
// ---- Raid frame layout ----
|
|
|
|
|
|
if (isRaid) {
|
|
|
|
|
|
// Organize members by subgroup (0-7, up to 5 members each)
|
|
|
|
|
|
constexpr int MAX_SUBGROUPS = 8;
|
|
|
|
|
|
constexpr int MAX_PER_GROUP = 5;
|
|
|
|
|
|
std::vector<const game::GroupMember*> subgroups[MAX_SUBGROUPS];
|
|
|
|
|
|
for (const auto& m : partyData.members) {
|
|
|
|
|
|
int sg = m.subGroup < MAX_SUBGROUPS ? m.subGroup : 0;
|
|
|
|
|
|
if (static_cast<int>(subgroups[sg].size()) < MAX_PER_GROUP)
|
|
|
|
|
|
subgroups[sg].push_back(&m);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Count non-empty subgroups to determine layout
|
|
|
|
|
|
int activeSgs = 0;
|
|
|
|
|
|
for (int sg = 0; sg < MAX_SUBGROUPS; sg++)
|
|
|
|
|
|
if (!subgroups[sg].empty()) activeSgs++;
|
|
|
|
|
|
|
|
|
|
|
|
// Compact raid cell: name + 2 narrow bars
|
|
|
|
|
|
constexpr float CELL_W = 90.0f;
|
|
|
|
|
|
constexpr float CELL_H = 42.0f;
|
|
|
|
|
|
constexpr float BAR_H = 7.0f;
|
|
|
|
|
|
constexpr float CELL_PAD = 3.0f;
|
|
|
|
|
|
|
|
|
|
|
|
float winW = activeSgs * (CELL_W + CELL_PAD) + CELL_PAD + 8.0f;
|
|
|
|
|
|
float winH = MAX_PER_GROUP * (CELL_H + CELL_PAD) + CELL_PAD + 20.0f;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
|
|
|
|
|
float raidX = (screenW - winW) / 2.0f;
|
|
|
|
|
|
float raidY = screenH - winH - 120.0f; // above action bar area
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(raidX, raidY), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(winW, winH), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags raidFlags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
|
ImGuiWindowFlags_NoScrollbar;
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(CELL_PAD, CELL_PAD));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.07f, 0.07f, 0.1f, 0.85f));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##RaidFrames", nullptr, raidFlags)) {
|
|
|
|
|
|
ImDrawList* draw = ImGui::GetWindowDrawList();
|
|
|
|
|
|
ImVec2 winPos = ImGui::GetWindowPos();
|
|
|
|
|
|
|
|
|
|
|
|
int colIdx = 0;
|
|
|
|
|
|
for (int sg = 0; sg < MAX_SUBGROUPS; sg++) {
|
|
|
|
|
|
if (subgroups[sg].empty()) continue;
|
|
|
|
|
|
|
|
|
|
|
|
float colX = winPos.x + CELL_PAD + colIdx * (CELL_W + CELL_PAD);
|
|
|
|
|
|
|
|
|
|
|
|
for (int row = 0; row < static_cast<int>(subgroups[sg].size()); row++) {
|
|
|
|
|
|
const auto& m = *subgroups[sg][row];
|
|
|
|
|
|
float cellY = winPos.y + CELL_PAD + 14.0f + row * (CELL_H + CELL_PAD);
|
|
|
|
|
|
|
|
|
|
|
|
ImVec2 cellMin(colX, cellY);
|
|
|
|
|
|
ImVec2 cellMax(colX + CELL_W, cellY + CELL_H);
|
|
|
|
|
|
|
|
|
|
|
|
// Cell background
|
|
|
|
|
|
bool isTarget = (gameHandler.getTargetGuid() == m.guid);
|
|
|
|
|
|
ImU32 bg = isTarget ? IM_COL32(60, 80, 120, 200) : IM_COL32(30, 30, 40, 180);
|
|
|
|
|
|
draw->AddRectFilled(cellMin, cellMax, bg, 3.0f);
|
|
|
|
|
|
if (isTarget)
|
|
|
|
|
|
draw->AddRect(cellMin, cellMax, IM_COL32(100, 150, 255, 200), 3.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// Dead/ghost overlay
|
|
|
|
|
|
bool isOnline = (m.onlineStatus & 0x0001) != 0;
|
|
|
|
|
|
bool isDead = (m.onlineStatus & 0x0020) != 0;
|
|
|
|
|
|
bool isGhost = (m.onlineStatus & 0x0010) != 0;
|
|
|
|
|
|
|
2026-03-12 06:58:42 -07:00
|
|
|
|
// Out-of-range check (40 yard threshold)
|
|
|
|
|
|
bool isOOR = false;
|
|
|
|
|
|
if (m.hasPartyStats && isOnline && !isDead && !isGhost && m.zoneId != 0) {
|
|
|
|
|
|
auto playerEnt = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid());
|
|
|
|
|
|
if (playerEnt) {
|
|
|
|
|
|
float dx = playerEnt->getX() - static_cast<float>(m.posX);
|
|
|
|
|
|
float dy = playerEnt->getY() - static_cast<float>(m.posY);
|
|
|
|
|
|
isOOR = (dx * dx + dy * dy) > (40.0f * 40.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// Dim cell overlay when out of range
|
|
|
|
|
|
if (isOOR)
|
|
|
|
|
|
draw->AddRectFilled(cellMin, cellMax, IM_COL32(0, 0, 0, 80), 3.0f);
|
|
|
|
|
|
|
2026-03-12 08:29:17 -07:00
|
|
|
|
// Name text (truncated) — class color when alive+online, gray when dead/offline
|
2026-03-10 05:26:16 -07:00
|
|
|
|
char truncName[16];
|
|
|
|
|
|
snprintf(truncName, sizeof(truncName), "%.12s", m.name.c_str());
|
2026-03-10 21:24:40 -07:00
|
|
|
|
bool isMemberLeader = (m.guid == partyData.leaderGuid);
|
2026-03-12 08:29:17 -07:00
|
|
|
|
ImU32 nameCol;
|
|
|
|
|
|
if (!isOnline || isDead || isGhost) {
|
|
|
|
|
|
nameCol = IM_COL32(140, 140, 140, 200); // gray for dead/offline
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Default: gold for leader, light gray for others
|
|
|
|
|
|
nameCol = isMemberLeader ? IM_COL32(255, 215, 0, 255) : IM_COL32(220, 220, 220, 255);
|
|
|
|
|
|
// Override with WoW class color if entity is loaded
|
|
|
|
|
|
auto mEnt = gameHandler.getEntityManager().getEntity(m.guid);
|
2026-03-12 08:33:34 -07:00
|
|
|
|
uint8_t cid = entityClassId(mEnt.get());
|
|
|
|
|
|
if (cid != 0) nameCol = classColorU32(cid);
|
2026-03-12 08:29:17 -07:00
|
|
|
|
}
|
2026-03-10 05:26:16 -07:00
|
|
|
|
draw->AddText(ImVec2(cellMin.x + 4.0f, cellMin.y + 3.0f), nameCol, truncName);
|
|
|
|
|
|
|
2026-03-10 21:24:40 -07:00
|
|
|
|
// Leader crown star in top-right of cell
|
|
|
|
|
|
if (isMemberLeader)
|
|
|
|
|
|
draw->AddText(ImVec2(cellMax.x - 10.0f, cellMin.y + 2.0f), IM_COL32(255, 215, 0, 255), "*");
|
|
|
|
|
|
|
2026-03-12 13:50:46 -07:00
|
|
|
|
// Raid mark symbol — small, just to the left of the leader crown
|
|
|
|
|
|
{
|
2026-03-27 14:47:58 -07:00
|
|
|
|
static constexpr struct { const char* sym; ImU32 col; } kCellMarks[] = {
|
2026-03-12 13:50:46 -07:00
|
|
|
|
{ "\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 rmk = gameHandler.getEntityRaidMark(m.guid);
|
|
|
|
|
|
if (rmk < game::GameHandler::kRaidMarkCount) {
|
|
|
|
|
|
ImFont* rmFont = ImGui::GetFont();
|
|
|
|
|
|
ImVec2 rmsz = rmFont->CalcTextSizeA(9.0f, FLT_MAX, 0.0f, kCellMarks[rmk].sym);
|
|
|
|
|
|
float rmX = cellMax.x - 10.0f - 2.0f - rmsz.x;
|
|
|
|
|
|
draw->AddText(rmFont, 9.0f,
|
|
|
|
|
|
ImVec2(rmX, cellMin.y + 2.0f),
|
|
|
|
|
|
kCellMarks[rmk].col, kCellMarks[rmk].sym);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 21:24:40 -07:00
|
|
|
|
// LFG role badge in bottom-right corner of cell
|
|
|
|
|
|
if (m.roles & 0x02)
|
|
|
|
|
|
draw->AddText(ImVec2(cellMax.x - 11.0f, cellMax.y - 11.0f), IM_COL32(80, 130, 255, 230), "T");
|
|
|
|
|
|
else if (m.roles & 0x04)
|
|
|
|
|
|
draw->AddText(ImVec2(cellMax.x - 11.0f, cellMax.y - 11.0f), IM_COL32(60, 220, 80, 230), "H");
|
|
|
|
|
|
else if (m.roles & 0x08)
|
|
|
|
|
|
draw->AddText(ImVec2(cellMax.x - 11.0f, cellMax.y - 11.0f), IM_COL32(220, 80, 80, 230), "D");
|
|
|
|
|
|
|
2026-03-17 13:47:53 -07:00
|
|
|
|
// Tactical role badge in bottom-left corner (flags from SMSG_GROUP_LIST / SMSG_REAL_GROUP_UPDATE)
|
|
|
|
|
|
// 0x01=Assistant, 0x02=Main Tank, 0x04=Main Assist
|
|
|
|
|
|
if (m.flags & 0x02)
|
|
|
|
|
|
draw->AddText(ImVec2(cellMin.x + 2.0f, cellMax.y - 11.0f), IM_COL32(255, 140, 0, 230), "MT");
|
|
|
|
|
|
else if (m.flags & 0x04)
|
|
|
|
|
|
draw->AddText(ImVec2(cellMin.x + 2.0f, cellMax.y - 11.0f), IM_COL32(100, 180, 255, 230), "MA");
|
|
|
|
|
|
else if (m.flags & 0x01)
|
|
|
|
|
|
draw->AddText(ImVec2(cellMin.x + 2.0f, cellMax.y - 11.0f), IM_COL32(180, 215, 255, 180), "A");
|
|
|
|
|
|
|
2026-03-10 05:26:16 -07:00
|
|
|
|
// Health bar
|
|
|
|
|
|
uint32_t hp = m.hasPartyStats ? m.curHealth : 0;
|
|
|
|
|
|
uint32_t maxHp = m.hasPartyStats ? m.maxHealth : 0;
|
|
|
|
|
|
if (maxHp > 0) {
|
|
|
|
|
|
float pct = static_cast<float>(hp) / static_cast<float>(maxHp);
|
|
|
|
|
|
float barY = cellMin.y + 16.0f;
|
|
|
|
|
|
ImVec2 barBg(cellMin.x + 3.0f, barY);
|
|
|
|
|
|
ImVec2 barBgEnd(cellMax.x - 3.0f, barY + BAR_H);
|
|
|
|
|
|
draw->AddRectFilled(barBg, barBgEnd, IM_COL32(40, 40, 40, 200), 2.0f);
|
|
|
|
|
|
ImVec2 barFill(barBg.x, barBg.y);
|
|
|
|
|
|
ImVec2 barFillEnd(barBg.x + (barBgEnd.x - barBg.x) * pct, barBgEnd.y);
|
2026-03-12 06:58:42 -07:00
|
|
|
|
ImU32 hpCol = isOOR ? IM_COL32(100, 100, 100, 160) :
|
|
|
|
|
|
pct > 0.5f ? IM_COL32(60, 180, 60, 255) :
|
|
|
|
|
|
pct > 0.2f ? IM_COL32(200, 180, 50, 255) :
|
|
|
|
|
|
IM_COL32(200, 60, 60, 255);
|
2026-03-10 05:26:16 -07:00
|
|
|
|
draw->AddRectFilled(barFill, barFillEnd, hpCol, 2.0f);
|
2026-03-12 06:58:42 -07:00
|
|
|
|
// HP percentage or OOR text centered on bar
|
2026-03-11 22:31:55 -07:00
|
|
|
|
char hpPct[8];
|
2026-03-12 06:58:42 -07:00
|
|
|
|
if (isOOR)
|
|
|
|
|
|
snprintf(hpPct, sizeof(hpPct), "OOR");
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(hpPct, sizeof(hpPct), "%d%%", static_cast<int>(pct * 100.0f + 0.5f));
|
2026-03-11 22:31:55 -07:00
|
|
|
|
ImVec2 ts = ImGui::CalcTextSize(hpPct);
|
|
|
|
|
|
float tx = (barBg.x + barBgEnd.x - ts.x) * 0.5f;
|
|
|
|
|
|
float ty = barBg.y + (BAR_H - ts.y) * 0.5f;
|
|
|
|
|
|
draw->AddText(ImVec2(tx + 1.0f, ty + 1.0f), IM_COL32(0, 0, 0, 180), hpPct);
|
|
|
|
|
|
draw->AddText(ImVec2(tx, ty), IM_COL32(255, 255, 255, 230), hpPct);
|
2026-03-10 05:26:16 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Power bar
|
|
|
|
|
|
if (m.hasPartyStats && m.maxPower > 0) {
|
|
|
|
|
|
float pct = static_cast<float>(m.curPower) / static_cast<float>(m.maxPower);
|
|
|
|
|
|
float barY = cellMin.y + 16.0f + BAR_H + 2.0f;
|
|
|
|
|
|
ImVec2 barBg(cellMin.x + 3.0f, barY);
|
|
|
|
|
|
ImVec2 barBgEnd(cellMax.x - 3.0f, barY + BAR_H - 2.0f);
|
|
|
|
|
|
draw->AddRectFilled(barBg, barBgEnd, IM_COL32(30, 30, 40, 200), 2.0f);
|
|
|
|
|
|
ImVec2 barFill(barBg.x, barBg.y);
|
|
|
|
|
|
ImVec2 barFillEnd(barBg.x + (barBgEnd.x - barBg.x) * pct, barBgEnd.y);
|
|
|
|
|
|
ImU32 pwrCol;
|
|
|
|
|
|
switch (m.powerType) {
|
|
|
|
|
|
case 0: pwrCol = IM_COL32(50, 80, 220, 255); break; // Mana
|
|
|
|
|
|
case 1: pwrCol = IM_COL32(200, 50, 50, 255); break; // Rage
|
|
|
|
|
|
case 3: pwrCol = IM_COL32(220, 210, 50, 255); break; // Energy
|
|
|
|
|
|
case 6: pwrCol = IM_COL32(180, 30, 50, 255); break; // Runic Power
|
|
|
|
|
|
default: pwrCol = IM_COL32(80, 120, 80, 255); break;
|
|
|
|
|
|
}
|
|
|
|
|
|
draw->AddRectFilled(barFill, barFillEnd, pwrCol, 2.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 13:28:49 -07:00
|
|
|
|
// Dispellable debuff dots at the bottom of the raid cell
|
|
|
|
|
|
// Mirrors party frame debuff indicators for healers in 25/40-man raids
|
|
|
|
|
|
if (!isDead && !isGhost) {
|
|
|
|
|
|
const std::vector<game::AuraSlot>* unitAuras = nullptr;
|
|
|
|
|
|
if (m.guid == gameHandler.getPlayerGuid())
|
|
|
|
|
|
unitAuras = &gameHandler.getPlayerAuras();
|
|
|
|
|
|
else if (m.guid == gameHandler.getTargetGuid())
|
|
|
|
|
|
unitAuras = &gameHandler.getTargetAuras();
|
|
|
|
|
|
else
|
|
|
|
|
|
unitAuras = gameHandler.getUnitAuras(m.guid);
|
|
|
|
|
|
|
|
|
|
|
|
if (unitAuras) {
|
|
|
|
|
|
bool shown[5] = {};
|
|
|
|
|
|
float dotX = cellMin.x + 4.0f;
|
|
|
|
|
|
const float dotY = cellMax.y - 5.0f;
|
|
|
|
|
|
const float DOT_R = 3.5f;
|
|
|
|
|
|
ImVec2 mouse = ImGui::GetMousePos();
|
|
|
|
|
|
for (const auto& aura : *unitAuras) {
|
|
|
|
|
|
if (aura.isEmpty()) continue;
|
|
|
|
|
|
if ((aura.flags & 0x80) == 0) continue; // debuffs only
|
|
|
|
|
|
uint8_t dt = gameHandler.getSpellDispelType(aura.spellId);
|
|
|
|
|
|
if (dt == 0 || dt > 4 || shown[dt]) continue;
|
|
|
|
|
|
shown[dt] = true;
|
|
|
|
|
|
ImVec4 dc;
|
|
|
|
|
|
switch (dt) {
|
|
|
|
|
|
case 1: dc = ImVec4(0.25f, 0.50f, 1.00f, 0.90f); break; // Magic: blue
|
|
|
|
|
|
case 2: dc = ImVec4(0.70f, 0.15f, 0.90f, 0.90f); break; // Curse: purple
|
|
|
|
|
|
case 3: dc = ImVec4(0.65f, 0.45f, 0.10f, 0.90f); break; // Disease: brown
|
|
|
|
|
|
case 4: dc = ImVec4(0.10f, 0.75f, 0.10f, 0.90f); break; // Poison: green
|
|
|
|
|
|
default: continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImU32 dotColU = ImGui::ColorConvertFloat4ToU32(dc);
|
|
|
|
|
|
draw->AddCircleFilled(ImVec2(dotX, dotY), DOT_R, dotColU);
|
|
|
|
|
|
draw->AddCircle(ImVec2(dotX, dotY), DOT_R + 0.5f, IM_COL32(0, 0, 0, 160), 8, 1.0f);
|
|
|
|
|
|
|
|
|
|
|
|
float mdx = mouse.x - dotX, mdy = mouse.y - dotY;
|
|
|
|
|
|
if (mdx * mdx + mdy * mdy < (DOT_R + 4.0f) * (DOT_R + 4.0f)) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
ImGui::TextColored(dc, "%s", kDispelNames[dt]);
|
|
|
|
|
|
for (const auto& da : *unitAuras) {
|
|
|
|
|
|
if (da.isEmpty() || (da.flags & 0x80) == 0) continue;
|
|
|
|
|
|
if (gameHandler.getSpellDispelType(da.spellId) != dt) continue;
|
|
|
|
|
|
const std::string& dName = gameHandler.getSpellName(da.spellId);
|
|
|
|
|
|
if (!dName.empty())
|
|
|
|
|
|
ImGui::Text(" %s", dName.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
|
|
|
|
|
dotX += 9.0f;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 05:26:16 -07:00
|
|
|
|
// Clickable invisible region over the whole cell
|
|
|
|
|
|
ImGui::SetCursorScreenPos(cellMin);
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(m.guid));
|
|
|
|
|
|
if (ImGui::InvisibleButton("raidCell", ImVec2(CELL_W, CELL_H))) {
|
|
|
|
|
|
gameHandler.setTarget(m.guid);
|
|
|
|
|
|
}
|
2026-03-18 03:09:43 -07:00
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
gameHandler.setMouseoverGuid(m.guid);
|
|
|
|
|
|
}
|
2026-03-11 21:47:10 -07:00
|
|
|
|
if (ImGui::BeginPopupContextItem("RaidMemberCtx")) {
|
|
|
|
|
|
ImGui::TextDisabled("%s", m.name.c_str());
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Target"))
|
|
|
|
|
|
gameHandler.setTarget(m.guid);
|
|
|
|
|
|
if (ImGui::MenuItem("Set Focus"))
|
|
|
|
|
|
gameHandler.setFocus(m.guid);
|
|
|
|
|
|
if (ImGui::MenuItem("Whisper")) {
|
2026-03-31 08:53:14 +03:00
|
|
|
|
chatPanel_.setWhisperTarget(m.name);
|
2026-03-11 21:47:10 -07:00
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Trade"))
|
|
|
|
|
|
gameHandler.initiateTrade(m.guid);
|
|
|
|
|
|
if (ImGui::MenuItem("Inspect")) {
|
|
|
|
|
|
gameHandler.setTarget(m.guid);
|
|
|
|
|
|
gameHandler.inspectTarget();
|
2026-03-12 02:52:40 -07:00
|
|
|
|
showInspectWindow_ = true;
|
2026-03-11 21:47:10 -07:00
|
|
|
|
}
|
|
|
|
|
|
bool isLeader = (partyData.leaderGuid == gameHandler.getPlayerGuid());
|
|
|
|
|
|
if (isLeader) {
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Kick from Raid"))
|
|
|
|
|
|
gameHandler.uninvitePlayer(m.name);
|
|
|
|
|
|
}
|
2026-03-12 00:39:56 -07:00
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::BeginMenu("Set Raid Mark")) {
|
|
|
|
|
|
for (int mi = 0; mi < 8; ++mi) {
|
|
|
|
|
|
if (ImGui::MenuItem(kRaidMarkNames[mi]))
|
|
|
|
|
|
gameHandler.setRaidMark(m.guid, static_cast<uint8_t>(mi));
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Clear Mark"))
|
|
|
|
|
|
gameHandler.setRaidMark(m.guid, 0xFF);
|
|
|
|
|
|
ImGui::EndMenu();
|
|
|
|
|
|
}
|
2026-03-11 21:47:10 -07:00
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
2026-03-10 05:26:16 -07:00
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
colIdx++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Subgroup header row
|
|
|
|
|
|
colIdx = 0;
|
|
|
|
|
|
for (int sg = 0; sg < MAX_SUBGROUPS; sg++) {
|
|
|
|
|
|
if (subgroups[sg].empty()) continue;
|
|
|
|
|
|
float colX = winPos.x + CELL_PAD + colIdx * (CELL_W + CELL_PAD);
|
|
|
|
|
|
char sgLabel[8];
|
|
|
|
|
|
snprintf(sgLabel, sizeof(sgLabel), "G%d", sg + 1);
|
|
|
|
|
|
draw->AddText(ImVec2(colX + CELL_W / 2 - 8.0f, winPos.y + CELL_PAD), IM_COL32(160, 160, 180, 200), sgLabel);
|
|
|
|
|
|
colIdx++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::PopStyleVar(2);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- Party frame layout (5-man) ----
|
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::SetNextWindowPos(ImVec2(10.0f, frameY), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(200.0f, 0.0f), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
|
ImGuiWindowFlags_AlwaysAutoResize;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.8f));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##PartyFrames", nullptr, flags)) {
|
2026-03-10 21:24:40 -07:00
|
|
|
|
const uint64_t leaderGuid = partyData.leaderGuid;
|
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
|
|
|
|
for (const auto& member : partyData.members) {
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(member.guid));
|
|
|
|
|
|
|
2026-03-10 21:24:40 -07:00
|
|
|
|
bool isLeader = (member.guid == leaderGuid);
|
|
|
|
|
|
|
|
|
|
|
|
// Name with level and status info — leader gets a gold star prefix
|
|
|
|
|
|
std::string label = (isLeader ? "* " : " ") + member.name;
|
2026-02-26 10:25:55 -08:00
|
|
|
|
if (member.hasPartyStats && member.level > 0) {
|
|
|
|
|
|
label += " [" + std::to_string(member.level) + "]";
|
|
|
|
|
|
}
|
|
|
|
|
|
if (member.hasPartyStats) {
|
|
|
|
|
|
bool isOnline = (member.onlineStatus & 0x0001) != 0;
|
|
|
|
|
|
bool isDead = (member.onlineStatus & 0x0020) != 0;
|
|
|
|
|
|
bool isGhost = (member.onlineStatus & 0x0010) != 0;
|
|
|
|
|
|
if (!isOnline) label += " (offline)";
|
|
|
|
|
|
else if (isDead || isGhost) label += " (dead)";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: color party frame member names by WoW class
Uses UNIT_FIELD_BYTES_0 (byte 1) from the entity's update fields to
determine each party member's class when they are loaded in the world,
and applies canonical WoW class colors to their name in the 5-man
party frame. Falls back to gold (leader) or light gray (others) when
the entity is not currently loaded. All 10 classes (Warrior, Paladin,
Hunter, Rogue, Priest, Death Knight, Shaman, Mage, Warlock, Druid)
use the standard Blizzard-matching hex values.
2026-03-12 08:28:08 -07:00
|
|
|
|
// Clickable name to target — use WoW class colors when entity is loaded,
|
|
|
|
|
|
// fall back to gold for leader / light gray for others
|
|
|
|
|
|
ImVec4 nameColor = isLeader
|
refactor: add 6 color constants, replace 61 inline literals, remove const_cast
- Add kBrightGold, kPaleRed, kBrightRed, kLightBlue, kManaBlue, kCyan to ui_colors.hpp
- Replace 61 inline ImVec4 color literals across game_screen, inventory_screen,
talent_screen, and world_map with named constants
- Remove const_cast in character_renderer render loop by using non-const iteration
2026-03-27 10:08:30 -07:00
|
|
|
|
? colors::kBrightGold
|
refactor: add 9 color constants, replace 36 more inline literals
New constants in ui_colors.hpp:
- Power types: kEnergyYellow, kHappinessGreen, kRunicRed, kSoulShardPurple
- UI elements: kInactiveGray, kVeryLightGray, kSymbolGold, kLowHealthRed, kDangerRed
Replacements across game_screen(30), inventory_screen(5), character_screen(1).
2026-03-27 14:05:32 -07:00
|
|
|
|
: colors::kVeryLightGray;
|
feat: color party frame member names by WoW class
Uses UNIT_FIELD_BYTES_0 (byte 1) from the entity's update fields to
determine each party member's class when they are loaded in the world,
and applies canonical WoW class colors to their name in the 5-man
party frame. Falls back to gold (leader) or light gray (others) when
the entity is not currently loaded. All 10 classes (Warrior, Paladin,
Hunter, Rogue, Priest, Death Knight, Shaman, Mage, Warlock, Druid)
use the standard Blizzard-matching hex values.
2026-03-12 08:28:08 -07:00
|
|
|
|
{
|
|
|
|
|
|
auto memberEntity = gameHandler.getEntityManager().getEntity(member.guid);
|
2026-03-12 08:33:34 -07:00
|
|
|
|
uint8_t cid = entityClassId(memberEntity.get());
|
|
|
|
|
|
if (cid != 0) nameColor = classColorVec4(cid);
|
feat: color party frame member names by WoW class
Uses UNIT_FIELD_BYTES_0 (byte 1) from the entity's update fields to
determine each party member's class when they are loaded in the world,
and applies canonical WoW class colors to their name in the 5-man
party frame. Falls back to gold (leader) or light gray (others) when
the entity is not currently loaded. All 10 classes (Warrior, Paladin,
Hunter, Rogue, Priest, Death Knight, Shaman, Mage, Warlock, Druid)
use the standard Blizzard-matching hex values.
2026-03-12 08:28:08 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, nameColor);
|
2026-02-26 10:25:55 -08:00
|
|
|
|
if (ImGui::Selectable(label.c_str(), gameHandler.getTargetGuid() == member.guid)) {
|
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
|
|
|
|
gameHandler.setTarget(member.guid);
|
|
|
|
|
|
}
|
2026-03-18 03:11:34 -07:00
|
|
|
|
// Set mouseover for [target=mouseover] macro conditionals
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
gameHandler.setMouseoverGuid(member.guid);
|
|
|
|
|
|
}
|
2026-03-12 15:11:09 -07:00
|
|
|
|
// Zone tooltip on name hover
|
|
|
|
|
|
if (ImGui::IsItemHovered() && member.hasPartyStats && member.zoneId != 0) {
|
|
|
|
|
|
std::string zoneName = gameHandler.getWhoAreaName(member.zoneId);
|
|
|
|
|
|
if (!zoneName.empty())
|
|
|
|
|
|
ImGui::SetTooltip("%s", zoneName.c_str());
|
|
|
|
|
|
}
|
feat: color party frame member names by WoW class
Uses UNIT_FIELD_BYTES_0 (byte 1) from the entity's update fields to
determine each party member's class when they are loaded in the world,
and applies canonical WoW class colors to their name in the 5-man
party frame. Falls back to gold (leader) or light gray (others) when
the entity is not currently loaded. All 10 classes (Warrior, Paladin,
Hunter, Rogue, Priest, Death Knight, Shaman, Mage, Warlock, Druid)
use the standard Blizzard-matching hex values.
2026-03-12 08:28:08 -07:00
|
|
|
|
ImGui::PopStyleColor();
|
2026-03-10 21:24:40 -07:00
|
|
|
|
|
|
|
|
|
|
// LFG role badge (Tank/Healer/DPS) — shown on same line as name when set
|
|
|
|
|
|
if (member.roles != 0) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (member.roles & 0x02) ImGui::TextColored(ImVec4(0.3f, 0.5f, 1.0f, 1.0f), "[T]");
|
|
|
|
|
|
if (member.roles & 0x04) { ImGui::SameLine(); ImGui::TextColored(ImVec4(0.2f, 0.9f, 0.3f, 1.0f), "[H]"); }
|
|
|
|
|
|
if (member.roles & 0x08) { ImGui::SameLine(); ImGui::TextColored(ImVec4(0.9f, 0.3f, 0.3f, 1.0f), "[D]"); }
|
|
|
|
|
|
}
|
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-17 13:50:49 -07:00
|
|
|
|
// Tactical role badge (MT/MA/Asst) from group flags
|
|
|
|
|
|
if (member.flags & 0x02) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.0f, 0.9f), "[MT]");
|
|
|
|
|
|
} else if (member.flags & 0x04) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 0.7f, 1.0f, 0.9f), "[MA]");
|
|
|
|
|
|
} else if (member.flags & 0x01) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.7f, 0.85f, 1.0f, 0.7f), "[A]");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 13:47:02 -07:00
|
|
|
|
// Raid mark symbol — shown on same line as name when this party member has a mark
|
|
|
|
|
|
{
|
2026-03-27 14:47:58 -07:00
|
|
|
|
static constexpr struct { const char* sym; ImU32 col; } kPartyMarks[] = {
|
2026-03-12 13:47:02 -07:00
|
|
|
|
{ "\xe2\x98\x85", IM_COL32(255, 220, 50, 255) }, // 0 Star
|
|
|
|
|
|
{ "\xe2\x97\x8f", IM_COL32(255, 140, 0, 255) }, // 1 Circle
|
|
|
|
|
|
{ "\xe2\x97\x86", IM_COL32(160, 32, 240, 255) }, // 2 Diamond
|
|
|
|
|
|
{ "\xe2\x96\xb2", IM_COL32( 50, 200, 50, 255) }, // 3 Triangle
|
|
|
|
|
|
{ "\xe2\x97\x8c", IM_COL32( 80, 160, 255, 255) }, // 4 Moon
|
|
|
|
|
|
{ "\xe2\x96\xa0", IM_COL32( 50, 200, 220, 255) }, // 5 Square
|
|
|
|
|
|
{ "\xe2\x9c\x9d", IM_COL32(255, 80, 80, 255) }, // 6 Cross
|
|
|
|
|
|
{ "\xe2\x98\xa0", IM_COL32(255, 255, 255, 255) }, // 7 Skull
|
|
|
|
|
|
};
|
|
|
|
|
|
uint8_t pmk = gameHandler.getEntityRaidMark(member.guid);
|
|
|
|
|
|
if (pmk < game::GameHandler::kRaidMarkCount) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextColored(
|
|
|
|
|
|
ImGui::ColorConvertU32ToFloat4(kPartyMarks[pmk].col),
|
|
|
|
|
|
"%s", kPartyMarks[pmk].sym);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 10:25:55 -08:00
|
|
|
|
// Health bar: prefer party stats, fall back to entity
|
|
|
|
|
|
uint32_t hp = 0, maxHp = 0;
|
|
|
|
|
|
if (member.hasPartyStats && member.maxHealth > 0) {
|
|
|
|
|
|
hp = member.curHealth;
|
|
|
|
|
|
maxHp = member.maxHealth;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
auto entity = gameHandler.getEntityManager().getEntity(member.guid);
|
|
|
|
|
|
if (entity && (entity->getType() == game::ObjectType::PLAYER || entity->getType() == game::ObjectType::UNIT)) {
|
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
|
|
|
|
hp = unit->getHealth();
|
|
|
|
|
|
maxHp = unit->getMaxHealth();
|
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-12 04:31:01 -07:00
|
|
|
|
// Check dead/ghost state for health bar rendering
|
|
|
|
|
|
bool memberDead = false;
|
|
|
|
|
|
bool memberOffline = false;
|
|
|
|
|
|
if (member.hasPartyStats) {
|
|
|
|
|
|
bool isOnline2 = (member.onlineStatus & 0x0001) != 0;
|
|
|
|
|
|
bool isDead2 = (member.onlineStatus & 0x0020) != 0;
|
|
|
|
|
|
bool isGhost2 = (member.onlineStatus & 0x0010) != 0;
|
|
|
|
|
|
memberDead = isDead2 || isGhost2;
|
|
|
|
|
|
memberOffline = !isOnline2;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 06:58:42 -07:00
|
|
|
|
// Out-of-range check: compare player position to member's reported position
|
|
|
|
|
|
// Range threshold: 40 yards (standard heal/spell range)
|
|
|
|
|
|
bool memberOutOfRange = false;
|
|
|
|
|
|
if (member.hasPartyStats && !memberOffline && !memberDead &&
|
|
|
|
|
|
member.zoneId != 0) {
|
|
|
|
|
|
// Same map: use 2D Euclidean distance in WoW coordinates (yards)
|
|
|
|
|
|
auto playerEntity = gameHandler.getEntityManager().getEntity(gameHandler.getPlayerGuid());
|
|
|
|
|
|
if (playerEntity) {
|
|
|
|
|
|
float dx = playerEntity->getX() - static_cast<float>(member.posX);
|
|
|
|
|
|
float dy = playerEntity->getY() - static_cast<float>(member.posY);
|
|
|
|
|
|
float distSq = dx * dx + dy * dy;
|
|
|
|
|
|
memberOutOfRange = (distSq > 40.0f * 40.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 04:31:01 -07:00
|
|
|
|
if (memberDead) {
|
|
|
|
|
|
// Gray "Dead" bar for fallen party members
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.35f, 0.35f, 0.35f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.15f, 0.15f, 0.15f, 1.0f));
|
|
|
|
|
|
ImGui::ProgressBar(0.0f, ImVec2(-1, 14), "Dead");
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
} else if (memberOffline) {
|
|
|
|
|
|
// Dim bar for offline members
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.25f, 0.25f, 0.25f, 0.6f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.1f, 0.1f, 0.1f, 0.6f));
|
|
|
|
|
|
ImGui::ProgressBar(0.0f, ImVec2(-1, 14), "Offline");
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
} else if (maxHp > 0) {
|
2026-02-26 10:25:55 -08:00
|
|
|
|
float pct = static_cast<float>(hp) / static_cast<float>(maxHp);
|
2026-03-12 06:58:42 -07:00
|
|
|
|
// Out-of-range: desaturate health bar to gray
|
|
|
|
|
|
ImVec4 hpBarColor = memberOutOfRange
|
|
|
|
|
|
? ImVec4(0.45f, 0.45f, 0.45f, 0.7f)
|
2026-03-27 13:57:29 -07:00
|
|
|
|
: (pct > 0.5f ? colors::kHealthGreen :
|
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
|
|
|
|
pct > 0.2f ? colors::kMidHealthYellow :
|
refactor: add 9 color constants, replace 36 more inline literals
New constants in ui_colors.hpp:
- Power types: kEnergyYellow, kHappinessGreen, kRunicRed, kSoulShardPurple
- UI elements: kInactiveGray, kVeryLightGray, kSymbolGold, kLowHealthRed, kDangerRed
Replacements across game_screen(30), inventory_screen(5), character_screen(1).
2026-03-27 14:05:32 -07:00
|
|
|
|
colors::kLowHealthRed);
|
2026-03-12 06:58:42 -07:00
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, hpBarColor);
|
2026-03-11 22:25:15 -07:00
|
|
|
|
char hpText[32];
|
2026-03-12 06:58:42 -07:00
|
|
|
|
if (memberOutOfRange) {
|
|
|
|
|
|
snprintf(hpText, sizeof(hpText), "OOR");
|
|
|
|
|
|
} else if (maxHp >= 10000) {
|
2026-03-11 22:25:15 -07:00
|
|
|
|
snprintf(hpText, sizeof(hpText), "%dk/%dk",
|
2026-03-25 11:40:49 -07:00
|
|
|
|
static_cast<int>(hp) / 1000, static_cast<int>(maxHp) / 1000);
|
2026-03-12 06:58:42 -07:00
|
|
|
|
} else {
|
2026-03-11 22:25:15 -07:00
|
|
|
|
snprintf(hpText, sizeof(hpText), "%u/%u", hp, maxHp);
|
2026-03-12 06:58:42 -07:00
|
|
|
|
}
|
2026-03-11 22:25:15 -07:00
|
|
|
|
ImGui::ProgressBar(pct, ImVec2(-1, 14), hpText);
|
2026-02-26 10:25:55 -08:00
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 06:58:42 -07:00
|
|
|
|
// Power bar (mana/rage/energy) from party stats — hidden for dead/offline/OOR
|
2026-03-12 04:31:01 -07:00
|
|
|
|
if (!memberDead && !memberOffline && member.hasPartyStats && member.maxPower > 0) {
|
2026-02-26 10:25:55 -08:00
|
|
|
|
float powerPct = static_cast<float>(member.curPower) / static_cast<float>(member.maxPower);
|
|
|
|
|
|
ImVec4 powerColor;
|
|
|
|
|
|
switch (member.powerType) {
|
refactor: add 6 color constants, replace 61 inline literals, remove const_cast
- Add kBrightGold, kPaleRed, kBrightRed, kLightBlue, kManaBlue, kCyan to ui_colors.hpp
- Replace 61 inline ImVec4 color literals across game_screen, inventory_screen,
talent_screen, and world_map with named constants
- Remove const_cast in character_renderer render loop by using non-const iteration
2026-03-27 10:08:30 -07:00
|
|
|
|
case 0: powerColor = colors::kManaBlue; break; // Mana (blue)
|
2026-03-27 10:20:45 -07:00
|
|
|
|
case 1: powerColor = colors::kDarkRed; break; // Rage (red)
|
2026-03-27 13:57:29 -07:00
|
|
|
|
case 2: powerColor = colors::kOrange; break; // Focus (orange)
|
refactor: add 9 color constants, replace 36 more inline literals
New constants in ui_colors.hpp:
- Power types: kEnergyYellow, kHappinessGreen, kRunicRed, kSoulShardPurple
- UI elements: kInactiveGray, kVeryLightGray, kSymbolGold, kLowHealthRed, kDangerRed
Replacements across game_screen(30), inventory_screen(5), character_screen(1).
2026-03-27 14:05:32 -07:00
|
|
|
|
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)
|
2026-03-25 12:12:03 -07:00
|
|
|
|
default: powerColor = kColorDarkGray; break;
|
2026-02-26 10:25:55 -08:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, powerColor);
|
|
|
|
|
|
ImGui::ProgressBar(powerPct, ImVec2(-1, 8), "");
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
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-12 11:44:30 -07:00
|
|
|
|
// Dispellable debuff indicators — small colored dots for party member debuffs
|
|
|
|
|
|
// Only show magic/curse/disease/poison (types 1-4); skip non-dispellable
|
|
|
|
|
|
if (!memberDead && !memberOffline) {
|
|
|
|
|
|
const std::vector<game::AuraSlot>* unitAuras = nullptr;
|
|
|
|
|
|
if (member.guid == gameHandler.getPlayerGuid())
|
|
|
|
|
|
unitAuras = &gameHandler.getPlayerAuras();
|
|
|
|
|
|
else if (member.guid == gameHandler.getTargetGuid())
|
|
|
|
|
|
unitAuras = &gameHandler.getTargetAuras();
|
|
|
|
|
|
else
|
|
|
|
|
|
unitAuras = gameHandler.getUnitAuras(member.guid);
|
|
|
|
|
|
|
|
|
|
|
|
if (unitAuras) {
|
|
|
|
|
|
bool anyDebuff = false;
|
|
|
|
|
|
for (const auto& aura : *unitAuras) {
|
|
|
|
|
|
if (aura.isEmpty()) continue;
|
|
|
|
|
|
if ((aura.flags & 0x80) == 0) continue; // only debuffs
|
|
|
|
|
|
uint8_t dt = gameHandler.getSpellDispelType(aura.spellId);
|
|
|
|
|
|
if (dt == 0) continue; // skip non-dispellable
|
|
|
|
|
|
anyDebuff = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (anyDebuff) {
|
|
|
|
|
|
// Render one dot per unique dispel type present
|
|
|
|
|
|
bool shown[5] = {};
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 1.0f));
|
|
|
|
|
|
for (const auto& aura : *unitAuras) {
|
|
|
|
|
|
if (aura.isEmpty()) continue;
|
|
|
|
|
|
if ((aura.flags & 0x80) == 0) continue;
|
|
|
|
|
|
uint8_t dt = gameHandler.getSpellDispelType(aura.spellId);
|
|
|
|
|
|
if (dt == 0 || dt > 4 || shown[dt]) continue;
|
|
|
|
|
|
shown[dt] = true;
|
|
|
|
|
|
ImVec4 dotCol;
|
|
|
|
|
|
switch (dt) {
|
|
|
|
|
|
case 1: dotCol = ImVec4(0.25f, 0.50f, 1.00f, 1.0f); break; // Magic: blue
|
|
|
|
|
|
case 2: dotCol = ImVec4(0.70f, 0.15f, 0.90f, 1.0f); break; // Curse: purple
|
|
|
|
|
|
case 3: dotCol = ImVec4(0.65f, 0.45f, 0.10f, 1.0f); break; // Disease: brown
|
|
|
|
|
|
case 4: dotCol = ImVec4(0.10f, 0.75f, 0.10f, 1.0f); break; // Poison: green
|
|
|
|
|
|
default: break;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, dotCol);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, dotCol);
|
|
|
|
|
|
ImGui::Button("##d", ImVec2(8.0f, 8.0f));
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
2026-03-12 13:23:21 -07:00
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
// Find spell name(s) of this dispel type
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
ImGui::TextColored(dotCol, "%s", kDispelNames[dt]);
|
|
|
|
|
|
for (const auto& da : *unitAuras) {
|
|
|
|
|
|
if (da.isEmpty() || (da.flags & 0x80) == 0) continue;
|
|
|
|
|
|
if (gameHandler.getSpellDispelType(da.spellId) != dt) continue;
|
|
|
|
|
|
const std::string& dName = gameHandler.getSpellName(da.spellId);
|
|
|
|
|
|
if (!dName.empty())
|
|
|
|
|
|
ImGui::Text(" %s", dName.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
2026-03-12 11:44:30 -07:00
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::NewLine();
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 23:36:14 -07:00
|
|
|
|
// Party member cast bar — shows when the party member is casting
|
|
|
|
|
|
if (auto* cs = gameHandler.getUnitCastState(member.guid)) {
|
|
|
|
|
|
float castPct = (cs->timeTotal > 0.0f)
|
|
|
|
|
|
? (cs->timeTotal - cs->timeRemaining) / cs->timeTotal : 0.0f;
|
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
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, colors::kMidHealthYellow);
|
2026-03-09 23:36:14 -07:00
|
|
|
|
char pcastLabel[48];
|
|
|
|
|
|
const std::string& spellNm = gameHandler.getSpellName(cs->spellId);
|
|
|
|
|
|
if (!spellNm.empty())
|
|
|
|
|
|
snprintf(pcastLabel, sizeof(pcastLabel), "%s (%.1fs)", spellNm.c_str(), cs->timeRemaining);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(pcastLabel, sizeof(pcastLabel), "Casting... (%.1fs)", cs->timeRemaining);
|
2026-03-12 07:58:36 -07:00
|
|
|
|
{
|
|
|
|
|
|
VkDescriptorSet pIcon = (cs->spellId != 0 && assetMgr)
|
|
|
|
|
|
? getSpellIcon(cs->spellId, assetMgr) : VK_NULL_HANDLE;
|
|
|
|
|
|
if (pIcon) {
|
|
|
|
|
|
ImGui::Image((ImTextureID)(uintptr_t)pIcon, ImVec2(10, 10));
|
|
|
|
|
|
ImGui::SameLine(0, 2);
|
|
|
|
|
|
ImGui::ProgressBar(castPct, ImVec2(-1, 10), pcastLabel);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::ProgressBar(castPct, ImVec2(-1, 10), pcastLabel);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-09 23:36:14 -07:00
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
2026-03-10 21:27:26 -07:00
|
|
|
|
|
|
|
|
|
|
// Right-click context menu for party member actions
|
|
|
|
|
|
if (ImGui::BeginPopupContextItem("PartyMemberCtx")) {
|
|
|
|
|
|
ImGui::TextDisabled("%s", member.name.c_str());
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Target")) {
|
|
|
|
|
|
gameHandler.setTarget(member.guid);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Set Focus")) {
|
|
|
|
|
|
gameHandler.setFocus(member.guid);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Whisper")) {
|
2026-03-31 08:53:14 +03:00
|
|
|
|
chatPanel_.setWhisperTarget(member.name);
|
2026-03-10 21:27:26 -07:00
|
|
|
|
}
|
2026-03-11 23:58:37 -07:00
|
|
|
|
if (ImGui::MenuItem("Follow")) {
|
|
|
|
|
|
gameHandler.setTarget(member.guid);
|
|
|
|
|
|
gameHandler.followTarget();
|
|
|
|
|
|
}
|
2026-03-10 21:27:26 -07:00
|
|
|
|
if (ImGui::MenuItem("Trade")) {
|
|
|
|
|
|
gameHandler.initiateTrade(member.guid);
|
|
|
|
|
|
}
|
2026-03-11 23:56:57 -07:00
|
|
|
|
if (ImGui::MenuItem("Duel")) {
|
|
|
|
|
|
gameHandler.proposeDuel(member.guid);
|
|
|
|
|
|
}
|
2026-03-10 21:27:26 -07:00
|
|
|
|
if (ImGui::MenuItem("Inspect")) {
|
|
|
|
|
|
gameHandler.setTarget(member.guid);
|
|
|
|
|
|
gameHandler.inspectTarget();
|
2026-03-12 02:52:40 -07:00
|
|
|
|
showInspectWindow_ = true;
|
2026-03-10 21:27:26 -07:00
|
|
|
|
}
|
2026-03-12 00:19:10 -07:00
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (!member.name.empty()) {
|
|
|
|
|
|
if (ImGui::MenuItem("Add Friend")) {
|
|
|
|
|
|
gameHandler.addFriend(member.name);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Ignore")) {
|
|
|
|
|
|
gameHandler.addIgnore(member.name);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-11 21:47:10 -07:00
|
|
|
|
// Leader-only actions
|
|
|
|
|
|
bool isLeader = (gameHandler.getPartyData().leaderGuid == gameHandler.getPlayerGuid());
|
|
|
|
|
|
if (isLeader) {
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Kick from Group")) {
|
|
|
|
|
|
gameHandler.uninvitePlayer(member.name);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 00:39:56 -07:00
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::BeginMenu("Set Raid Mark")) {
|
|
|
|
|
|
for (int mi = 0; mi < 8; ++mi) {
|
|
|
|
|
|
if (ImGui::MenuItem(kRaidMarkNames[mi]))
|
|
|
|
|
|
gameHandler.setRaidMark(member.guid, static_cast<uint8_t>(mi));
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Clear Mark"))
|
|
|
|
|
|
gameHandler.setRaidMark(member.guid, 0xFF);
|
|
|
|
|
|
ImGui::EndMenu();
|
|
|
|
|
|
}
|
2026-03-10 21:27:26 -07:00
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
2026-03-09 23:36:14 -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
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleColor();
|
Fix PopStyleVar mismatches and character geoset IDs
Fix 9 PopStyleVar(2) calls that should be PopStyleVar(1) across
player frame, target frame, cast bar, party frames, buff bar, escape
menu, death dialog, and resurrect dialog. Fix action bar from
PopStyleVar(2) to PopStyleVar(4) to match 4 pushes.
Fix character geoset defaults: 301→302 (bare hands), 701→702 (ears),
1501→1502 (back/cloak), add 802 (wristbands). No WoW character model
uses geoset 301/701/1501; all use 302/702/1502 as base. This fixes
missing hands/arms on undead and other races with separate hand meshes.
2026-02-15 06:09:38 -08:00
|
|
|
|
ImGui::PopStyleVar();
|
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-12 01:15:11 -07:00
|
|
|
|
// ============================================================
|
2026-03-12 14:25:37 -07:00
|
|
|
|
// Durability Warning (equipment damage indicator)
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
2026-03-18 10:47:34 -07:00
|
|
|
|
void GameScreen::takeScreenshot(game::GameHandler& /*gameHandler*/) {
|
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
if (!renderer) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 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";
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
std::string path = dir + "/" + filename;
|
|
|
|
|
|
|
|
|
|
|
|
if (renderer->captureScreenshot(path)) {
|
|
|
|
|
|
game::MessageChatData sysMsg;
|
|
|
|
|
|
sysMsg.type = game::ChatType::SYSTEM;
|
|
|
|
|
|
sysMsg.language = game::ChatLanguage::UNIVERSAL;
|
|
|
|
|
|
sysMsg.message = "Screenshot saved: " + path;
|
|
|
|
|
|
core::Application::getInstance().getGameHandler()->addLocalChatMessage(sysMsg);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:25:37 -07:00
|
|
|
|
void GameScreen::renderDurabilityWarning(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (gameHandler.getPlayerGuid() == 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
float pct = static_cast<float>(slot.item.curDurability) /
|
|
|
|
|
|
static_cast<float>(slot.item.maxDurability);
|
|
|
|
|
|
if (pct < minDurPct) minDurPct = pct;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Only show warning below 20%
|
|
|
|
|
|
if (minDurPct >= 0.2f && !hasBroken) return;
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
|
|
const float screenW = io.DisplaySize.x;
|
|
|
|
|
|
const float screenH = io.DisplaySize.y;
|
|
|
|
|
|
|
|
|
|
|
|
// 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");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
int pctInt = static_cast<int>(minDurPct * 100.0f);
|
refactor: add 9 color constants, replace 36 more inline literals
New constants in ui_colors.hpp:
- Power types: kEnergyYellow, kHappinessGreen, kRunicRed, kSoulShardPurple
- UI elements: kInactiveGray, kVeryLightGray, kSymbolGold, kLowHealthRed, kDangerRed
Replacements across game_screen(30), inventory_screen(5), character_screen(1).
2026-03-27 14:05:32 -07:00
|
|
|
|
ImGui::TextColored(colors::kSymbolGold,
|
2026-03-12 14:25:37 -07:00
|
|
|
|
"\xef\x94\x9b Low durability: %d%%", pctInt);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::IsWindowHovered())
|
|
|
|
|
|
ImGui::SetTooltip("Your equipment is damaged. Visit any blacksmith or repair NPC.");
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleVar(3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
2026-03-12 01:15:11 -07:00
|
|
|
|
// UI Error Frame (WoW-style center-bottom error overlay)
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
|
|
|
|
|
|
|
if (uiErrors_.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// 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());
|
|
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
|
|
// Fade fast in the last 0.5 s
|
|
|
|
|
|
if (e.age > kUIErrorLifetime - 0.5f)
|
|
|
|
|
|
alpha *= (kUIErrorLifetime - e.age) / 0.5f;
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 11:06:40 -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-09 20:05:09 -07:00
|
|
|
|
// Boss Encounter Frames
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderBossFrames(game::GameHandler& gameHandler) {
|
2026-03-12 07:58:36 -07:00
|
|
|
|
auto* assetMgr = core::Application::getInstance().getAssetManager();
|
|
|
|
|
|
|
2026-03-09 20:05:09 -07:00
|
|
|
|
// Collect active boss unit slots
|
|
|
|
|
|
struct BossSlot { uint32_t slot; uint64_t guid; };
|
|
|
|
|
|
std::vector<BossSlot> active;
|
|
|
|
|
|
for (uint32_t s = 0; s < game::GameHandler::kMaxEncounterSlots; ++s) {
|
|
|
|
|
|
uint64_t g = gameHandler.getEncounterUnitGuid(s);
|
|
|
|
|
|
if (g != 0) active.push_back({s, g});
|
|
|
|
|
|
}
|
|
|
|
|
|
if (active.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
const float frameW = 200.0f;
|
|
|
|
|
|
const float startX = ImGui::GetIO().DisplaySize.x - frameW - 10.0f;
|
|
|
|
|
|
float frameY = 120.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
|
ImGuiWindowFlags_AlwaysAutoResize;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.15f, 0.05f, 0.05f, 0.85f));
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(startX, frameY), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(frameW, 0.0f), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##BossFrames", nullptr, flags)) {
|
|
|
|
|
|
for (const auto& bs : active) {
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(bs.guid));
|
|
|
|
|
|
|
2026-03-12 14:09:01 -07:00
|
|
|
|
// Try to resolve name, health, and power from entity manager
|
2026-03-09 20:05:09 -07:00
|
|
|
|
std::string name = "Boss";
|
|
|
|
|
|
uint32_t hp = 0, maxHp = 0;
|
2026-03-12 14:09:01 -07:00
|
|
|
|
uint8_t bossPowerType = 0;
|
|
|
|
|
|
uint32_t bossPower = 0, bossMaxPower = 0;
|
2026-03-09 20:05:09 -07:00
|
|
|
|
auto entity = gameHandler.getEntityManager().getEntity(bs.guid);
|
|
|
|
|
|
if (entity && (entity->getType() == game::ObjectType::UNIT ||
|
|
|
|
|
|
entity->getType() == game::ObjectType::PLAYER)) {
|
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(entity);
|
|
|
|
|
|
const auto& n = unit->getName();
|
|
|
|
|
|
if (!n.empty()) name = n;
|
2026-03-12 14:09:01 -07:00
|
|
|
|
hp = unit->getHealth();
|
|
|
|
|
|
maxHp = unit->getMaxHealth();
|
|
|
|
|
|
bossPowerType = unit->getPowerType();
|
|
|
|
|
|
bossPower = unit->getPower();
|
|
|
|
|
|
bossMaxPower = unit->getMaxPower();
|
2026-03-09 20:05:09 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Clickable name to target
|
|
|
|
|
|
if (ImGui::Selectable(name.c_str(), gameHandler.getTargetGuid() == bs.guid)) {
|
|
|
|
|
|
gameHandler.setTarget(bs.guid);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (maxHp > 0) {
|
|
|
|
|
|
float pct = static_cast<float>(hp) / static_cast<float>(maxHp);
|
|
|
|
|
|
// Boss health bar in red shades
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram,
|
refactor: add 9 color constants, replace 36 more inline literals
New constants in ui_colors.hpp:
- Power types: kEnergyYellow, kHappinessGreen, kRunicRed, kSoulShardPurple
- UI elements: kInactiveGray, kVeryLightGray, kSymbolGold, kLowHealthRed, kDangerRed
Replacements across game_screen(30), inventory_screen(5), character_screen(1).
2026-03-27 14:05:32 -07:00
|
|
|
|
pct > 0.5f ? colors::kLowHealthRed :
|
2026-03-09 20:05:09 -07:00
|
|
|
|
pct > 0.2f ? ImVec4(0.9f, 0.5f, 0.1f, 1.0f) :
|
|
|
|
|
|
ImVec4(1.0f, 0.8f, 0.1f, 1.0f));
|
|
|
|
|
|
char label[32];
|
|
|
|
|
|
std::snprintf(label, sizeof(label), "%u / %u", hp, maxHp);
|
|
|
|
|
|
ImGui::ProgressBar(pct, ImVec2(-1, 14), label);
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
2026-03-09 23:13:30 -07:00
|
|
|
|
|
2026-03-12 14:09:01 -07:00
|
|
|
|
// Boss power bar — shown when boss has a non-zero power pool
|
|
|
|
|
|
// Energy bosses (type 3) are particularly important: full energy signals ability use
|
|
|
|
|
|
if (bossMaxPower > 0 && bossPower > 0) {
|
|
|
|
|
|
float bpPct = static_cast<float>(bossPower) / static_cast<float>(bossMaxPower);
|
|
|
|
|
|
ImVec4 bpColor;
|
|
|
|
|
|
switch (bossPowerType) {
|
|
|
|
|
|
case 0: bpColor = ImVec4(0.2f, 0.3f, 0.9f, 1.0f); break; // Mana: blue
|
2026-03-27 10:20:45 -07:00
|
|
|
|
case 1: bpColor = colors::kDarkRed; break; // Rage: red
|
2026-03-27 13:57:29 -07:00
|
|
|
|
case 2: bpColor = colors::kOrange; break; // Focus: orange
|
2026-03-12 14:09:01 -07:00
|
|
|
|
case 3: bpColor = ImVec4(0.9f, 0.9f, 0.1f, 1.0f); break; // Energy: yellow
|
|
|
|
|
|
default: bpColor = ImVec4(0.4f, 0.8f, 0.4f, 1.0f); break;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, bpColor);
|
|
|
|
|
|
char bpLabel[24];
|
|
|
|
|
|
std::snprintf(bpLabel, sizeof(bpLabel), "%u", bossPower);
|
|
|
|
|
|
ImGui::ProgressBar(bpPct, ImVec2(-1, 6), bpLabel);
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 23:13:30 -07:00
|
|
|
|
// Boss cast bar — shown when the boss is casting (critical for interrupt)
|
|
|
|
|
|
if (auto* cs = gameHandler.getUnitCastState(bs.guid)) {
|
|
|
|
|
|
float castPct = (cs->timeTotal > 0.0f)
|
|
|
|
|
|
? (cs->timeTotal - cs->timeRemaining) / cs->timeTotal : 0.0f;
|
|
|
|
|
|
uint32_t bspell = cs->spellId;
|
|
|
|
|
|
const std::string& bcastName = (bspell != 0)
|
|
|
|
|
|
? gameHandler.getSpellName(bspell) : "";
|
2026-03-17 19:44:48 -07:00
|
|
|
|
// Green = interruptible, Red = immune; pulse when > 80% complete
|
2026-03-12 04:18:39 -07:00
|
|
|
|
ImVec4 bcastColor;
|
|
|
|
|
|
if (castPct > 0.8f) {
|
|
|
|
|
|
float pulse = 0.7f + 0.3f * std::sin(static_cast<float>(ImGui::GetTime()) * 8.0f);
|
2026-03-17 19:44:48 -07:00
|
|
|
|
bcastColor = cs->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);
|
2026-03-12 04:18:39 -07:00
|
|
|
|
} else {
|
2026-03-17 19:44:48 -07:00
|
|
|
|
bcastColor = cs->interruptible
|
2026-03-27 15:01:12 -07:00
|
|
|
|
? colors::kCastGreen
|
2026-03-17 19:44:48 -07:00
|
|
|
|
: ImVec4(0.9f, 0.15f, 0.15f, 1.0f);
|
2026-03-12 04:18:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, bcastColor);
|
2026-03-09 23:13:30 -07:00
|
|
|
|
char bcastLabel[72];
|
|
|
|
|
|
if (!bcastName.empty())
|
|
|
|
|
|
snprintf(bcastLabel, sizeof(bcastLabel), "%s (%.1fs)",
|
|
|
|
|
|
bcastName.c_str(), cs->timeRemaining);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(bcastLabel, sizeof(bcastLabel), "Casting... (%.1fs)", cs->timeRemaining);
|
2026-03-12 07:58:36 -07:00
|
|
|
|
{
|
|
|
|
|
|
VkDescriptorSet bIcon = (bspell != 0 && assetMgr)
|
|
|
|
|
|
? getSpellIcon(bspell, assetMgr) : VK_NULL_HANDLE;
|
|
|
|
|
|
if (bIcon) {
|
|
|
|
|
|
ImGui::Image((ImTextureID)(uintptr_t)bIcon, ImVec2(12, 12));
|
|
|
|
|
|
ImGui::SameLine(0, 2);
|
|
|
|
|
|
ImGui::ProgressBar(castPct, ImVec2(-1, 12), bcastLabel);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::ProgressBar(castPct, ImVec2(-1, 12), bcastLabel);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-09 23:13:30 -07:00
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
2026-03-09 20:05:09 -07:00
|
|
|
|
|
2026-03-12 13:43:12 -07:00
|
|
|
|
// Boss aura row: debuffs first (player DoTs), then boss buffs
|
|
|
|
|
|
{
|
|
|
|
|
|
const std::vector<game::AuraSlot>* bossAuras = nullptr;
|
|
|
|
|
|
if (bs.guid == gameHandler.getTargetGuid())
|
|
|
|
|
|
bossAuras = &gameHandler.getTargetAuras();
|
|
|
|
|
|
else
|
|
|
|
|
|
bossAuras = gameHandler.getUnitAuras(bs.guid);
|
|
|
|
|
|
|
|
|
|
|
|
if (bossAuras) {
|
|
|
|
|
|
int bossActive = 0;
|
|
|
|
|
|
for (const auto& a : *bossAuras) if (!a.isEmpty()) bossActive++;
|
|
|
|
|
|
if (bossActive > 0) {
|
|
|
|
|
|
constexpr float BA_ICON = 16.0f;
|
|
|
|
|
|
constexpr int BA_PER_ROW = 10;
|
|
|
|
|
|
|
|
|
|
|
|
uint64_t baNowMs = static_cast<uint64_t>(
|
|
|
|
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
|
|
|
|
std::chrono::steady_clock::now().time_since_epoch()).count());
|
|
|
|
|
|
|
|
|
|
|
|
// Sort: player-applied debuffs first (most relevant), then others
|
|
|
|
|
|
const uint64_t pguid = gameHandler.getPlayerGuid();
|
|
|
|
|
|
std::vector<size_t> baIdx;
|
|
|
|
|
|
baIdx.reserve(bossAuras->size());
|
|
|
|
|
|
for (size_t i = 0; i < bossAuras->size(); ++i)
|
|
|
|
|
|
if (!(*bossAuras)[i].isEmpty()) baIdx.push_back(i);
|
|
|
|
|
|
std::sort(baIdx.begin(), baIdx.end(), [&](size_t a, size_t b) {
|
|
|
|
|
|
const auto& aa = (*bossAuras)[a];
|
|
|
|
|
|
const auto& ab = (*bossAuras)[b];
|
|
|
|
|
|
bool aPlayerDot = (aa.flags & 0x80) != 0 && aa.casterGuid == pguid;
|
|
|
|
|
|
bool bPlayerDot = (ab.flags & 0x80) != 0 && ab.casterGuid == pguid;
|
|
|
|
|
|
if (aPlayerDot != bPlayerDot) return aPlayerDot > bPlayerDot;
|
|
|
|
|
|
bool aDebuff = (aa.flags & 0x80) != 0;
|
|
|
|
|
|
bool bDebuff = (ab.flags & 0x80) != 0;
|
|
|
|
|
|
if (aDebuff != bDebuff) return aDebuff > bDebuff;
|
|
|
|
|
|
int32_t ra = aa.getRemainingMs(baNowMs);
|
|
|
|
|
|
int32_t rb = ab.getRemainingMs(baNowMs);
|
|
|
|
|
|
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 baShown = 0;
|
|
|
|
|
|
for (size_t si = 0; si < baIdx.size() && baShown < 20; ++si) {
|
|
|
|
|
|
const auto& aura = (*bossAuras)[baIdx[si]];
|
|
|
|
|
|
bool isBuff = (aura.flags & 0x80) == 0;
|
|
|
|
|
|
bool isPlayerCast = (aura.casterGuid == pguid);
|
|
|
|
|
|
|
|
|
|
|
|
if (baShown > 0 && baShown % BA_PER_ROW != 0) ImGui::SameLine();
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(baIdx[si]) + 7000);
|
|
|
|
|
|
|
|
|
|
|
|
ImVec4 borderCol;
|
|
|
|
|
|
if (isBuff) {
|
|
|
|
|
|
// Boss buffs: gold for important enrage/shield types
|
|
|
|
|
|
borderCol = ImVec4(0.8f, 0.6f, 0.1f, 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 = isPlayerCast
|
|
|
|
|
|
? ImVec4(0.90f, 0.30f, 0.10f, 0.9f) // player DoT: orange-red
|
|
|
|
|
|
: ImVec4(0.60f, 0.20f, 0.20f, 0.9f); // other debuff: dark red
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
VkDescriptorSet baIcon = assetMgr
|
|
|
|
|
|
? getSpellIcon(aura.spellId, assetMgr) : VK_NULL_HANDLE;
|
|
|
|
|
|
if (baIcon) {
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, borderCol);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(1, 1));
|
|
|
|
|
|
ImGui::ImageButton("##baura",
|
|
|
|
|
|
(ImTextureID)(uintptr_t)baIcon,
|
|
|
|
|
|
ImVec2(BA_ICON - 2, BA_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(BA_ICON, BA_ICON));
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Duration overlay
|
|
|
|
|
|
int32_t baRemain = aura.getRemainingMs(baNowMs);
|
|
|
|
|
|
if (baRemain > 0) {
|
|
|
|
|
|
ImVec2 imin = ImGui::GetItemRectMin();
|
|
|
|
|
|
ImVec2 imax = ImGui::GetItemRectMax();
|
|
|
|
|
|
char ts[12];
|
2026-03-27 14:17:28 -07:00
|
|
|
|
fmtDurationCompact(ts, sizeof(ts), (baRemain + 999) / 1000);
|
2026-03-12 13:43:12 -07:00
|
|
|
|
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-12 14:53:14 -07:00
|
|
|
|
// Stack / charge count — upper-left corner (parity with target/focus frames)
|
|
|
|
|
|
if (aura.charges > 1) {
|
|
|
|
|
|
ImVec2 baMin = ImGui::GetItemRectMin();
|
|
|
|
|
|
char chargeStr[8];
|
|
|
|
|
|
snprintf(chargeStr, sizeof(chargeStr), "%u", static_cast<unsigned>(aura.charges));
|
|
|
|
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(baMin.x + 2, baMin.y + 2),
|
|
|
|
|
|
IM_COL32(0, 0, 0, 200), chargeStr);
|
|
|
|
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(baMin.x + 1, baMin.y + 1),
|
|
|
|
|
|
IM_COL32(255, 220, 50, 255), chargeStr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 13:43:12 -07:00
|
|
|
|
// Tooltip
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
bool richOk = spellbookScreen.renderSpellInfoTooltip(
|
|
|
|
|
|
aura.spellId, gameHandler, assetMgr);
|
|
|
|
|
|
if (!richOk) {
|
|
|
|
|
|
std::string nm = spellbookScreen.lookupSpellName(aura.spellId, assetMgr);
|
|
|
|
|
|
if (nm.empty()) nm = "Spell #" + std::to_string(aura.spellId);
|
|
|
|
|
|
ImGui::Text("%s", nm.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isPlayerCast && !isBuff)
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.9f, 0.7f, 0.3f, 1.0f), "Your DoT");
|
2026-03-27 14:17:28 -07:00
|
|
|
|
renderAuraRemaining(baRemain);
|
2026-03-12 13:43:12 -07:00
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
baShown++;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 20:05:09 -07:00
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
void GameScreen::renderGuildRoster(game::GameHandler& gameHandler) {
|
|
|
|
|
|
// Guild Roster toggle (customizable keybind)
|
|
|
|
|
|
if (!chatPanel_.isChatInputActive() && !ImGui::GetIO().WantTextInput &&
|
|
|
|
|
|
!ImGui::GetIO().WantCaptureKeyboard &&
|
|
|
|
|
|
KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_GUILD_ROSTER)) {
|
|
|
|
|
|
showGuildRoster_ = !showGuildRoster_;
|
|
|
|
|
|
if (showGuildRoster_) {
|
|
|
|
|
|
// Open friends tab directly if not in guild
|
|
|
|
|
|
if (!gameHandler.isInGuild()) {
|
|
|
|
|
|
guildRosterTab_ = 2; // Friends tab
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Re-query guild name if we have guildId but no name yet
|
|
|
|
|
|
if (gameHandler.getGuildName().empty()) {
|
|
|
|
|
|
const auto* ch = gameHandler.getActiveCharacter();
|
|
|
|
|
|
if (ch && ch->hasGuild()) {
|
|
|
|
|
|
gameHandler.queryGuildInfo(ch->guildId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
gameHandler.requestGuildRoster();
|
|
|
|
|
|
gameHandler.requestGuildInfo();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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 10:07:58 +03:00
|
|
|
|
// Petition creation dialog (shown when NPC sends SMSG_PETITION_SHOWLIST)
|
|
|
|
|
|
if (gameHandler.hasPetitionShowlist()) {
|
|
|
|
|
|
ImGui::OpenPopup("CreateGuildPetition");
|
|
|
|
|
|
gameHandler.clearPetitionDialog();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::BeginPopupModal("CreateGuildPetition", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
|
ImGui::Text("Create Guild Charter");
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
uint32_t cost = gameHandler.getPetitionCost();
|
|
|
|
|
|
ImGui::TextDisabled("Cost:"); ImGui::SameLine(0, 4);
|
|
|
|
|
|
renderCoinsFromCopper(cost);
|
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::Spacing();
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Text("Guild Name:");
|
|
|
|
|
|
ImGui::InputText("##petitionname", petitionNameBuffer_, sizeof(petitionNameBuffer_));
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
if (ImGui::Button("Create", ImVec2(120, 0))) {
|
|
|
|
|
|
if (petitionNameBuffer_[0] != '\0') {
|
|
|
|
|
|
gameHandler.buyPetition(gameHandler.getPetitionNpcGuid(), petitionNameBuffer_);
|
|
|
|
|
|
petitionNameBuffer_[0] = '\0';
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
}
|
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::SameLine();
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
|
|
|
|
|
|
petitionNameBuffer_[0] = '\0';
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
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 10:07:58 +03:00
|
|
|
|
ImGui::EndPopup();
|
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 13:58:02 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Petition signatures window (shown when a petition item is used or offered)
|
|
|
|
|
|
if (gameHandler.hasPetitionSignaturesUI()) {
|
|
|
|
|
|
ImGui::OpenPopup("PetitionSignatures");
|
|
|
|
|
|
gameHandler.clearPetitionSignaturesUI();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::BeginPopupModal("PetitionSignatures", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
|
const auto& pInfo = gameHandler.getPetitionInfo();
|
|
|
|
|
|
if (!pInfo.guildName.empty())
|
|
|
|
|
|
ImGui::Text("Guild Charter: %s", pInfo.guildName.c_str());
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::Text("Guild Charter");
|
|
|
|
|
|
ImGui::Separator();
|
2026-03-09 13:58:02 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Text("Signatures: %u / %u", pInfo.signatureCount, pInfo.signaturesRequired);
|
2026-03-09 13:58:02 -07:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (!pInfo.signatures.empty()) {
|
|
|
|
|
|
for (size_t i = 0; i < pInfo.signatures.size(); ++i) {
|
|
|
|
|
|
const auto& sig = pInfo.signatures[i];
|
|
|
|
|
|
// Try to resolve name from entity manager
|
|
|
|
|
|
std::string sigName;
|
|
|
|
|
|
if (sig.playerGuid != 0) {
|
|
|
|
|
|
auto entity = gameHandler.getEntityManager().getEntity(sig.playerGuid);
|
|
|
|
|
|
if (entity) {
|
|
|
|
|
|
auto* unit = entity->isUnit() ? static_cast<game::Unit*>(entity.get()) : nullptr;
|
|
|
|
|
|
if (unit) sigName = unit->getName();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (sigName.empty())
|
|
|
|
|
|
sigName = "Player " + std::to_string(i + 1);
|
|
|
|
|
|
ImGui::BulletText("%s", sigName.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Spacing();
|
2026-03-09 13:58:02 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
|
|
|
|
|
|
// If we're not the owner, show Sign button
|
|
|
|
|
|
bool isOwner = (pInfo.ownerGuid == gameHandler.getPlayerGuid());
|
|
|
|
|
|
if (!isOwner) {
|
|
|
|
|
|
if (ImGui::Button("Sign", ImVec2(120, 0))) {
|
|
|
|
|
|
gameHandler.signPetition(pInfo.petitionGuid);
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
} else if (pInfo.signatureCount >= pInfo.signaturesRequired) {
|
|
|
|
|
|
// Owner with enough sigs — turn in
|
|
|
|
|
|
if (ImGui::Button("Turn In", ImVec2(120, 0))) {
|
|
|
|
|
|
gameHandler.turnInPetition(pInfo.petitionGuid);
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
2026-03-09 13:58:02 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (ImGui::Button("Close", ImVec2(120, 0)))
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
ImGui::EndPopup();
|
Implement basic trade request/accept/decline flow
- Parse SMSG_TRADE_STATUS for all 20+ status codes: incoming request,
open/cancel/complete/accept notifications, error conditions (too far,
wrong faction, stunned, dead, trial account, etc.)
- SMSG_TRADE_STATUS_EXTENDED consumed via shared handler (no full item
window yet; state tracking sufficient for accept/decline flow)
- Add acceptTradeRequest() (CMSG_BEGIN_TRADE), declineTradeRequest(),
acceptTrade() (CMSG_ACCEPT_TRADE), cancelTrade() (CMSG_CANCEL_TRADE)
- Add BeginTradePacket, CancelTradePacket, AcceptTradePacket builders
- Add renderTradeRequestPopup(): shows "X wants to trade" with
Accept/Decline buttons when tradeStatus_ == PendingIncoming
- TradeStatus enum tracks None/PendingIncoming/Open/Accepted/Complete
2026-03-09 14:05:42 -07:00
|
|
|
|
}
|
2026-03-12 05:06:14 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (!showGuildRoster_) return;
|
2026-03-12 05:06:14 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Get zone manager for name lookup
|
|
|
|
|
|
game::ZoneManager* zoneManager = nullptr;
|
|
|
|
|
|
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
|
|
|
|
|
zoneManager = renderer->getZoneManager();
|
2026-03-12 05:06:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 14:15:59 -07:00
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
2026-03-31 10:07:58 +03:00
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
2026-03-09 14:15:59 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 375, screenH / 2 - 250), ImGuiCond_Once);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(750, 500), ImGuiCond_Once);
|
2026-03-09 14:15:59 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
std::string title = gameHandler.isInGuild() ? (gameHandler.getGuildName() + " - Social") : "Social";
|
|
|
|
|
|
bool open = showGuildRoster_;
|
|
|
|
|
|
if (ImGui::Begin(title.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
|
|
|
|
|
|
// Tab bar: Roster | Guild Info
|
|
|
|
|
|
if (ImGui::BeginTabBar("GuildTabs")) {
|
|
|
|
|
|
if (ImGui::BeginTabItem("Roster")) {
|
|
|
|
|
|
guildRosterTab_ = 0;
|
|
|
|
|
|
if (!gameHandler.hasGuildRoster()) {
|
|
|
|
|
|
ImGui::Text("Loading roster...");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const auto& roster = gameHandler.getGuildRoster();
|
2026-03-09 14:15:59 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// MOTD
|
|
|
|
|
|
if (!roster.motd.empty()) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "MOTD: %s", roster.motd.c_str());
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
}
|
2026-03-09 14:15:59 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Count online
|
|
|
|
|
|
int onlineCount = 0;
|
|
|
|
|
|
for (const auto& m : roster.members) {
|
|
|
|
|
|
if (m.online) ++onlineCount;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Text("%d members (%d online)", static_cast<int>(roster.members.size()), onlineCount);
|
|
|
|
|
|
ImGui::Separator();
|
2026-03-09 14:15:59 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
const auto& rankNames = gameHandler.getGuildRankNames();
|
2026-03-09 14:15:59 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Table
|
|
|
|
|
|
if (ImGui::BeginTable("GuildRoster", 7,
|
|
|
|
|
|
ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerV |
|
|
|
|
|
|
ImGuiTableFlags_Sortable)) {
|
|
|
|
|
|
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_DefaultSort);
|
|
|
|
|
|
ImGui::TableSetupColumn("Rank");
|
|
|
|
|
|
ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 40.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Class", ImGuiTableColumnFlags_WidthFixed, 70.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthFixed, 120.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Note");
|
|
|
|
|
|
ImGui::TableSetupColumn("Officer Note");
|
|
|
|
|
|
ImGui::TableHeadersRow();
|
2026-03-09 14:14:15 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Online members first, then offline
|
|
|
|
|
|
auto sortedMembers = roster.members;
|
|
|
|
|
|
std::sort(sortedMembers.begin(), sortedMembers.end(), [](const auto& a, const auto& b) {
|
|
|
|
|
|
if (a.online != b.online) return a.online > b.online;
|
|
|
|
|
|
return a.name < b.name;
|
|
|
|
|
|
});
|
2026-03-09 14:14:15 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
for (const auto& m : sortedMembers) {
|
|
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
ImVec4 textColor = m.online ? ui::colors::kWhite
|
|
|
|
|
|
: kColorDarkGray;
|
|
|
|
|
|
ImVec4 nameColor = m.online ? classColorVec4(m.classId) : textColor;
|
2026-03-09 14:14:15 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
|
|
ImGui::TextColored(nameColor, "%s", m.name.c_str());
|
2026-03-09 14:14:15 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Right-click context menu
|
|
|
|
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
|
|
|
|
|
selectedGuildMember_ = m.name;
|
|
|
|
|
|
ImGui::OpenPopup("GuildMemberContext");
|
|
|
|
|
|
}
|
2026-03-09 14:14:15 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
|
|
// Show rank name instead of index
|
|
|
|
|
|
if (m.rankIndex < rankNames.size()) {
|
|
|
|
|
|
ImGui::TextColored(textColor, "%s", rankNames[m.rankIndex].c_str());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(textColor, "Rank %u", m.rankIndex);
|
|
|
|
|
|
}
|
2026-03-09 14:07:50 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
|
|
ImGui::TextColored(textColor, "%u", m.level);
|
2026-03-09 14:08:49 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
|
|
const char* className = classNameStr(m.classId);
|
|
|
|
|
|
ImVec4 classCol = m.online ? classColorVec4(m.classId) : textColor;
|
|
|
|
|
|
ImGui::TextColored(classCol, "%s", className);
|
2026-03-09 14:07:50 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
|
|
// Zone name lookup
|
|
|
|
|
|
if (zoneManager) {
|
|
|
|
|
|
const auto* zoneInfo = zoneManager->getZoneInfo(m.zoneId);
|
|
|
|
|
|
if (zoneInfo && !zoneInfo->name.empty()) {
|
|
|
|
|
|
ImGui::TextColored(textColor, "%s", zoneInfo->name.c_str());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(textColor, "%u", m.zoneId);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(textColor, "%u", m.zoneId);
|
|
|
|
|
|
}
|
2026-03-09 14:07:50 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
|
|
ImGui::TextColored(textColor, "%s", m.publicNote.c_str());
|
2026-03-09 14:07:50 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
|
|
ImGui::TextColored(textColor, "%s", m.officerNote.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
|
|
}
|
2026-03-09 14:07:50 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Context menu popup
|
|
|
|
|
|
if (ImGui::BeginPopup("GuildMemberContext")) {
|
|
|
|
|
|
ImGui::TextDisabled("%s", selectedGuildMember_.c_str());
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
// Social actions — only for online members
|
|
|
|
|
|
bool memberOnline = false;
|
|
|
|
|
|
for (const auto& mem : roster.members) {
|
|
|
|
|
|
if (mem.name == selectedGuildMember_) { memberOnline = mem.online; break; }
|
2026-03-11 20:42:26 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (memberOnline) {
|
|
|
|
|
|
if (ImGui::MenuItem("Whisper")) {
|
|
|
|
|
|
chatPanel_.setWhisperTarget(selectedGuildMember_);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Invite to Group")) {
|
|
|
|
|
|
gameHandler.inviteToGroup(selectedGuildMember_);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!selectedGuildMember_.empty()) {
|
|
|
|
|
|
if (ImGui::MenuItem("Add Friend"))
|
|
|
|
|
|
gameHandler.addFriend(selectedGuildMember_);
|
|
|
|
|
|
if (ImGui::MenuItem("Ignore"))
|
|
|
|
|
|
gameHandler.addIgnore(selectedGuildMember_);
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Promote")) {
|
|
|
|
|
|
gameHandler.promoteGuildMember(selectedGuildMember_);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Demote")) {
|
|
|
|
|
|
gameHandler.demoteGuildMember(selectedGuildMember_);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Kick")) {
|
|
|
|
|
|
gameHandler.kickGuildMember(selectedGuildMember_);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Set Public Note...")) {
|
|
|
|
|
|
showGuildNoteEdit_ = true;
|
|
|
|
|
|
editingOfficerNote_ = false;
|
|
|
|
|
|
guildNoteEditBuffer_[0] = '\0';
|
|
|
|
|
|
// Pre-fill with existing note
|
|
|
|
|
|
for (const auto& mem : roster.members) {
|
|
|
|
|
|
if (mem.name == selectedGuildMember_) {
|
|
|
|
|
|
snprintf(guildNoteEditBuffer_, sizeof(guildNoteEditBuffer_), "%s", mem.publicNote.c_str());
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Set Officer Note...")) {
|
|
|
|
|
|
showGuildNoteEdit_ = true;
|
|
|
|
|
|
editingOfficerNote_ = true;
|
|
|
|
|
|
guildNoteEditBuffer_[0] = '\0';
|
|
|
|
|
|
for (const auto& mem : roster.members) {
|
|
|
|
|
|
if (mem.name == selectedGuildMember_) {
|
|
|
|
|
|
snprintf(guildNoteEditBuffer_, sizeof(guildNoteEditBuffer_), "%s", mem.officerNote.c_str());
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Set as Leader")) {
|
|
|
|
|
|
gameHandler.setGuildLeader(selectedGuildMember_);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
2026-03-11 20:42:26 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
|
|
|
|
|
|
// Note edit modal
|
|
|
|
|
|
if (showGuildNoteEdit_) {
|
|
|
|
|
|
ImGui::OpenPopup("EditGuildNote");
|
|
|
|
|
|
showGuildNoteEdit_ = false;
|
2026-03-11 00:44:07 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (ImGui::BeginPopupModal("EditGuildNote", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
|
ImGui::Text("%s %s for %s:",
|
|
|
|
|
|
editingOfficerNote_ ? "Officer" : "Public", "Note", selectedGuildMember_.c_str());
|
|
|
|
|
|
ImGui::InputText("##guildnote", guildNoteEditBuffer_, sizeof(guildNoteEditBuffer_));
|
|
|
|
|
|
if (ImGui::Button("Save")) {
|
|
|
|
|
|
if (editingOfficerNote_) {
|
|
|
|
|
|
gameHandler.setGuildOfficerNote(selectedGuildMember_, guildNoteEditBuffer_);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
gameHandler.setGuildPublicNote(selectedGuildMember_, guildNoteEditBuffer_);
|
2026-03-11 00:44:07 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Cancel")) {
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
2026-03-11 00:44:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::EndTabItem();
|
2026-03-11 00:44:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (ImGui::BeginTabItem("Guild Info")) {
|
|
|
|
|
|
guildRosterTab_ = 1;
|
|
|
|
|
|
const auto& infoData = gameHandler.getGuildInfoData();
|
|
|
|
|
|
const auto& queryData = gameHandler.getGuildQueryData();
|
|
|
|
|
|
const auto& roster = gameHandler.getGuildRoster();
|
|
|
|
|
|
const auto& rankNames = gameHandler.getGuildRankNames();
|
2026-03-11 00:44:07 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Guild name (large, gold)
|
|
|
|
|
|
ImGui::PushFont(nullptr); // default font
|
|
|
|
|
|
ImGui::TextColored(ui::colors::kTooltipGold, "<%s>", gameHandler.getGuildName().c_str());
|
|
|
|
|
|
ImGui::PopFont();
|
|
|
|
|
|
ImGui::Separator();
|
2026-03-11 00:44:07 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Creation date
|
|
|
|
|
|
if (infoData.isValid()) {
|
|
|
|
|
|
ImGui::Text("Created: %u/%u/%u", infoData.creationDay, infoData.creationMonth, infoData.creationYear);
|
|
|
|
|
|
ImGui::Text("Members: %u | Accounts: %u", infoData.numMembers, infoData.numAccounts);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Spacing();
|
2026-03-11 00:44:07 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Guild description / info text
|
|
|
|
|
|
if (!roster.guildInfo.empty()) {
|
|
|
|
|
|
ImGui::TextColored(colors::kSilver, "Description:");
|
|
|
|
|
|
ImGui::TextWrapped("%s", roster.guildInfo.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Spacing();
|
2026-03-11 00:44:07 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// MOTD with edit button
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "MOTD:");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (!roster.motd.empty()) {
|
|
|
|
|
|
ImGui::TextWrapped("%s", roster.motd.c_str());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(kColorDarkGray, "(not set)");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::Button("Set MOTD")) {
|
|
|
|
|
|
showMotdEdit_ = true;
|
|
|
|
|
|
snprintf(guildMotdEditBuffer_, sizeof(guildMotdEditBuffer_), "%s", roster.motd.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Spacing();
|
2026-03-11 00:44:07 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// MOTD edit modal
|
|
|
|
|
|
if (showMotdEdit_) {
|
|
|
|
|
|
ImGui::OpenPopup("EditMotd");
|
|
|
|
|
|
showMotdEdit_ = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::BeginPopupModal("EditMotd", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
|
ImGui::Text("Set Message of the Day:");
|
|
|
|
|
|
ImGui::InputText("##motdinput", guildMotdEditBuffer_, sizeof(guildMotdEditBuffer_));
|
|
|
|
|
|
if (ImGui::Button("Save", ImVec2(120, 0))) {
|
|
|
|
|
|
gameHandler.setGuildMotd(guildMotdEditBuffer_);
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
2026-03-09 14:01:27 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Emblem info
|
|
|
|
|
|
if (queryData.isValid()) {
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Text("Emblem: Style %u, Color %u | Border: Style %u, Color %u | BG: %u",
|
|
|
|
|
|
queryData.emblemStyle, queryData.emblemColor,
|
|
|
|
|
|
queryData.borderStyle, queryData.borderColor, queryData.backgroundColor);
|
|
|
|
|
|
}
|
2026-03-09 14:01:27 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Rank list
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(ui::colors::kTooltipGold, "Ranks:");
|
|
|
|
|
|
for (size_t i = 0; i < rankNames.size(); ++i) {
|
|
|
|
|
|
if (rankNames[i].empty()) continue;
|
|
|
|
|
|
// Show rank permission summary from roster data
|
|
|
|
|
|
if (i < roster.ranks.size()) {
|
|
|
|
|
|
uint32_t rights = roster.ranks[i].rights;
|
|
|
|
|
|
std::string perms;
|
|
|
|
|
|
if (rights & 0x01) perms += "Invite ";
|
|
|
|
|
|
if (rights & 0x02) perms += "Remove ";
|
|
|
|
|
|
if (rights & 0x40) perms += "Promote ";
|
|
|
|
|
|
if (rights & 0x80) perms += "Demote ";
|
|
|
|
|
|
if (rights & 0x04) perms += "OChat ";
|
|
|
|
|
|
if (rights & 0x10) perms += "MOTD ";
|
|
|
|
|
|
ImGui::Text(" %zu. %s", i + 1, rankNames[i].c_str());
|
|
|
|
|
|
if (!perms.empty()) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextColored(kColorDarkGray, "[%s]", perms.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::Text(" %zu. %s", i + 1, rankNames[i].c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-09 14:01:27 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Rank management buttons
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
if (ImGui::Button("Add Rank")) {
|
|
|
|
|
|
showAddRankModal_ = true;
|
|
|
|
|
|
addRankNameBuffer_[0] = '\0';
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Delete Last Rank")) {
|
|
|
|
|
|
gameHandler.deleteGuildRank();
|
|
|
|
|
|
}
|
2026-03-09 14:01:27 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Add rank modal
|
|
|
|
|
|
if (showAddRankModal_) {
|
|
|
|
|
|
ImGui::OpenPopup("AddGuildRank");
|
|
|
|
|
|
showAddRankModal_ = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::BeginPopupModal("AddGuildRank", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
|
ImGui::Text("New Rank Name:");
|
|
|
|
|
|
ImGui::InputText("##rankname", addRankNameBuffer_, sizeof(addRankNameBuffer_));
|
|
|
|
|
|
if (ImGui::Button("Add", ImVec2(120, 0))) {
|
|
|
|
|
|
if (addRankNameBuffer_[0] != '\0') {
|
|
|
|
|
|
gameHandler.addGuildRank(addRankNameBuffer_);
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
2026-03-09 14:01:27 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::EndTabItem();
|
2026-03-12 04:57:36 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// ---- Friends tab ----
|
|
|
|
|
|
if (ImGui::BeginTabItem("Friends")) {
|
|
|
|
|
|
guildRosterTab_ = 2;
|
|
|
|
|
|
const auto& contacts = gameHandler.getContacts();
|
2026-03-12 04:57:36 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Add Friend row
|
|
|
|
|
|
static char addFriendBuf[64] = {};
|
|
|
|
|
|
ImGui::SetNextItemWidth(180.0f);
|
|
|
|
|
|
ImGui::InputText("##addfriend", addFriendBuf, sizeof(addFriendBuf));
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Add Friend") && addFriendBuf[0] != '\0') {
|
|
|
|
|
|
gameHandler.addFriend(addFriendBuf);
|
|
|
|
|
|
addFriendBuf[0] = '\0';
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
2026-03-10 20:59:02 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Note-edit state
|
|
|
|
|
|
static std::string friendNoteTarget;
|
|
|
|
|
|
static char friendNoteBuf[256] = {};
|
|
|
|
|
|
static bool openNotePopup = false;
|
2026-03-09 14:01:27 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Filter to friends only
|
|
|
|
|
|
int friendCount = 0;
|
|
|
|
|
|
for (size_t ci = 0; ci < contacts.size(); ++ci) {
|
|
|
|
|
|
const auto& c = contacts[ci];
|
|
|
|
|
|
if (!c.isFriend()) continue;
|
|
|
|
|
|
++friendCount;
|
2026-03-12 08:59:38 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PushID(static_cast<int>(ci));
|
|
|
|
|
|
|
|
|
|
|
|
// Status dot
|
|
|
|
|
|
ImU32 dotColor = c.isOnline()
|
|
|
|
|
|
? IM_COL32(80, 200, 80, 255)
|
|
|
|
|
|
: IM_COL32(120, 120, 120, 255);
|
|
|
|
|
|
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
|
|
|
|
|
ImGui::GetWindowDrawList()->AddCircleFilled(
|
|
|
|
|
|
ImVec2(cursor.x + 6.0f, cursor.y + 8.0f), 5.0f, dotColor);
|
|
|
|
|
|
ImGui::Dummy(ImVec2(14.0f, 0.0f));
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
|
|
|
|
|
|
// Name as Selectable for right-click context menu
|
|
|
|
|
|
const char* displayName = c.name.empty() ? "(unknown)" : c.name.c_str();
|
|
|
|
|
|
ImVec4 nameCol = c.isOnline()
|
|
|
|
|
|
? ui::colors::kWhite
|
|
|
|
|
|
: colors::kInactiveGray;
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, nameCol);
|
|
|
|
|
|
ImGui::Selectable(displayName, false, ImGuiSelectableFlags_AllowOverlap, ImVec2(130.0f, 0.0f));
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
|
|
|
|
|
|
// Double-click to whisper
|
|
|
|
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)
|
|
|
|
|
|
&& !c.name.empty()) {
|
|
|
|
|
|
chatPanel_.setWhisperTarget(c.name);
|
2026-03-12 08:59:38 -07:00
|
|
|
|
}
|
2026-03-09 14:01:27 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Right-click context menu
|
|
|
|
|
|
if (ImGui::BeginPopupContextItem("FriendCtx")) {
|
|
|
|
|
|
ImGui::TextDisabled("%s", displayName);
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Whisper") && !c.name.empty()) {
|
|
|
|
|
|
chatPanel_.setWhisperTarget(c.name);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (c.isOnline() && ImGui::MenuItem("Invite to Group") && !c.name.empty()) {
|
|
|
|
|
|
gameHandler.inviteToGroup(c.name);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Edit Note")) {
|
|
|
|
|
|
friendNoteTarget = c.name;
|
|
|
|
|
|
strncpy(friendNoteBuf, c.note.c_str(), sizeof(friendNoteBuf) - 1);
|
|
|
|
|
|
friendNoteBuf[sizeof(friendNoteBuf) - 1] = '\0';
|
|
|
|
|
|
openNotePopup = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Remove Friend")) {
|
|
|
|
|
|
gameHandler.removeFriend(c.name);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Note tooltip on hover
|
|
|
|
|
|
if (ImGui::IsItemHovered() && !c.note.empty()) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
ImGui::TextDisabled("Note: %s", c.note.c_str());
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Level, class, and status
|
|
|
|
|
|
if (c.isOnline()) {
|
|
|
|
|
|
ImGui::SameLine(150.0f);
|
|
|
|
|
|
const char* statusLabel =
|
|
|
|
|
|
(c.status == 2) ? " (AFK)" :
|
|
|
|
|
|
(c.status == 3) ? " (DND)" : "";
|
|
|
|
|
|
// Class color for the level/class display
|
|
|
|
|
|
ImVec4 friendClassCol = classColorVec4(static_cast<uint8_t>(c.classId));
|
|
|
|
|
|
const char* friendClassName = classNameStr(static_cast<uint8_t>(c.classId));
|
|
|
|
|
|
if (c.level > 0 && c.classId > 0) {
|
|
|
|
|
|
ImGui::TextColored(friendClassCol, "Lv%u %s%s", c.level, friendClassName, statusLabel);
|
|
|
|
|
|
} else if (c.level > 0) {
|
|
|
|
|
|
ImGui::TextDisabled("Lv %u%s", c.level, statusLabel);
|
|
|
|
|
|
} else if (*statusLabel) {
|
|
|
|
|
|
ImGui::TextDisabled("%s", statusLabel + 1);
|
|
|
|
|
|
}
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Tooltip: zone info
|
|
|
|
|
|
if (ImGui::IsItemHovered() && c.areaId != 0) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
if (zoneManager) {
|
|
|
|
|
|
const auto* zi = zoneManager->getZoneInfo(c.areaId);
|
|
|
|
|
|
if (zi && !zi->name.empty())
|
|
|
|
|
|
ImGui::Text("Zone: %s", zi->name.c_str());
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::TextDisabled("Area ID: %u", c.areaId);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextDisabled("Area ID: %u", c.areaId);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (friendCount == 0) {
|
|
|
|
|
|
ImGui::TextDisabled("No friends found.");
|
|
|
|
|
|
}
|
2026-03-09 14:48:30 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Note edit modal
|
|
|
|
|
|
if (openNotePopup) {
|
|
|
|
|
|
ImGui::OpenPopup("EditFriendNote");
|
|
|
|
|
|
openNotePopup = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::BeginPopupModal("EditFriendNote", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
|
ImGui::Text("Note for %s:", friendNoteTarget.c_str());
|
|
|
|
|
|
ImGui::SetNextItemWidth(240.0f);
|
|
|
|
|
|
ImGui::InputText("##fnote", friendNoteBuf, sizeof(friendNoteBuf));
|
|
|
|
|
|
if (ImGui::Button("Save", ImVec2(110, 0))) {
|
|
|
|
|
|
gameHandler.setFriendNote(friendNoteTarget, friendNoteBuf);
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(110, 0))) {
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
2026-03-09 14:48:30 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
|
}
|
2026-03-09 14:48:30 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// ---- Ignore List tab ----
|
|
|
|
|
|
if (ImGui::BeginTabItem("Ignore")) {
|
|
|
|
|
|
guildRosterTab_ = 3;
|
|
|
|
|
|
const auto& contacts = gameHandler.getContacts();
|
2026-03-09 14:48:30 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Add Ignore row
|
|
|
|
|
|
static char addIgnoreBuf[64] = {};
|
|
|
|
|
|
ImGui::SetNextItemWidth(180.0f);
|
|
|
|
|
|
ImGui::InputText("##addignore", addIgnoreBuf, sizeof(addIgnoreBuf));
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Ignore Player") && addIgnoreBuf[0] != '\0') {
|
|
|
|
|
|
gameHandler.addIgnore(addIgnoreBuf);
|
|
|
|
|
|
addIgnoreBuf[0] = '\0';
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
2026-03-12 09:07:37 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
int ignoreCount = 0;
|
|
|
|
|
|
for (size_t ci = 0; ci < contacts.size(); ++ci) {
|
|
|
|
|
|
const auto& c = contacts[ci];
|
|
|
|
|
|
if (!c.isIgnored()) continue;
|
|
|
|
|
|
++ignoreCount;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(ci) + 10000);
|
|
|
|
|
|
const char* displayName = c.name.empty() ? "(unknown)" : c.name.c_str();
|
|
|
|
|
|
ImGui::Selectable(displayName, false, ImGuiSelectableFlags_AllowOverlap);
|
|
|
|
|
|
if (ImGui::BeginPopupContextItem("IgnoreCtx")) {
|
|
|
|
|
|
ImGui::TextDisabled("%s", displayName);
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Remove Ignore")) {
|
|
|
|
|
|
gameHandler.removeIgnore(c.name);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
2026-03-12 09:07:37 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PopID();
|
2026-03-12 09:07:37 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
|
|
|
|
|
|
if (ignoreCount == 0) {
|
|
|
|
|
|
ImGui::TextDisabled("Ignore list is empty.");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::EndTabItem();
|
2026-03-12 09:07:37 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
|
|
|
|
|
|
ImGui::EndTabBar();
|
2026-03-12 09:07:37 -07:00
|
|
|
|
}
|
2026-03-09 14:48:30 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
2026-03-31 10:07:58 +03:00
|
|
|
|
showGuildRoster_ = open;
|
2026-03-09 14:48:30 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Social Frame — compact online friends panel (toggled by showSocialFrame_)
|
|
|
|
|
|
// ============================================================
|
2026-03-10 21:12:28 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
void GameScreen::renderSocialFrame(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!showSocialFrame_) return;
|
2026-03-10 21:12:28 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
const auto& contacts = gameHandler.getContacts();
|
|
|
|
|
|
// Count online friends for early-out
|
|
|
|
|
|
int onlineCount = 0;
|
|
|
|
|
|
for (const auto& c : contacts)
|
|
|
|
|
|
if (c.isFriend() && c.isOnline()) ++onlineCount;
|
2026-03-10 21:12:28 -07:00
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW - 230.0f, 240.0f), ImGuiCond_Once);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(220.0f, 0.0f), ImGuiCond_Always);
|
2026-03-10 21:12:28 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.1f, 0.1f, 0.92f));
|
2026-03-10 21:12:28 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// State for "Set Note" inline editing
|
|
|
|
|
|
static int noteEditContactIdx = -1;
|
|
|
|
|
|
static char noteEditBuf[128] = {};
|
2026-03-10 21:12:28 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
bool open = showSocialFrame_;
|
|
|
|
|
|
char socialTitle[32];
|
|
|
|
|
|
snprintf(socialTitle, sizeof(socialTitle), "Social (%d online)##SocialFrame", onlineCount);
|
|
|
|
|
|
if (ImGui::Begin(socialTitle, &open,
|
|
|
|
|
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
|
|
|
|
|
|
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar)) {
|
2026-03-10 21:12:28 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Get zone manager for area name lookups
|
|
|
|
|
|
game::ZoneManager* socialZoneMgr = nullptr;
|
|
|
|
|
|
if (auto* rend = core::Application::getInstance().getRenderer())
|
|
|
|
|
|
socialZoneMgr = rend->getZoneManager();
|
2026-03-12 22:25:46 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (ImGui::BeginTabBar("##SocialTabs")) {
|
|
|
|
|
|
// ---- Friends tab ----
|
|
|
|
|
|
if (ImGui::BeginTabItem("Friends")) {
|
|
|
|
|
|
ImGui::BeginChild("##FriendsList", ImVec2(200, 200), false);
|
2026-03-12 22:25:46 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Online friends first
|
|
|
|
|
|
int shown = 0;
|
|
|
|
|
|
for (int pass = 0; pass < 2; ++pass) {
|
|
|
|
|
|
bool wantOnline = (pass == 0);
|
|
|
|
|
|
for (size_t ci = 0; ci < contacts.size(); ++ci) {
|
|
|
|
|
|
const auto& c = contacts[ci];
|
|
|
|
|
|
if (!c.isFriend()) continue;
|
|
|
|
|
|
if (c.isOnline() != wantOnline) continue;
|
2026-03-12 22:25:46 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PushID(static_cast<int>(ci));
|
2026-03-12 22:25:46 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Status dot
|
|
|
|
|
|
ImU32 dotColor;
|
|
|
|
|
|
if (!c.isOnline()) dotColor = IM_COL32(100, 100, 100, 200);
|
|
|
|
|
|
else if (c.status == 2) dotColor = IM_COL32(255, 200, 50, 255); // AFK
|
|
|
|
|
|
else if (c.status == 3) dotColor = IM_COL32(255, 120, 50, 255); // DND
|
|
|
|
|
|
else dotColor = IM_COL32( 50, 220, 50, 255); // online
|
2026-03-12 22:25:46 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImVec2 dotMin = ImGui::GetCursorScreenPos();
|
|
|
|
|
|
dotMin.y += 4.0f;
|
|
|
|
|
|
ImGui::GetWindowDrawList()->AddCircleFilled(
|
|
|
|
|
|
ImVec2(dotMin.x + 5.0f, dotMin.y + 5.0f), 4.5f, dotColor);
|
|
|
|
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 14.0f);
|
2026-03-12 22:25:46 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
const char* displayName = c.name.empty() ? "(unknown)" : c.name.c_str();
|
|
|
|
|
|
ImVec4 nameCol = c.isOnline()
|
|
|
|
|
|
? classColorVec4(static_cast<uint8_t>(c.classId))
|
|
|
|
|
|
: kColorDarkGray;
|
|
|
|
|
|
ImGui::TextColored(nameCol, "%s", displayName);
|
2026-03-12 22:25:46 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (c.isOnline() && c.level > 0) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
// Show level and class name in class color
|
|
|
|
|
|
ImGui::TextColored(classColorVec4(static_cast<uint8_t>(c.classId)),
|
|
|
|
|
|
"Lv%u %s", c.level, classNameStr(static_cast<uint8_t>(c.classId)));
|
|
|
|
|
|
}
|
2026-03-12 22:25:46 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Tooltip: zone info and note
|
|
|
|
|
|
if (ImGui::IsItemHovered() || (c.isOnline() && ImGui::IsItemHovered())) {
|
|
|
|
|
|
if (c.isOnline() && (c.areaId != 0 || !c.note.empty())) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
if (c.areaId != 0) {
|
|
|
|
|
|
const char* zoneName = nullptr;
|
|
|
|
|
|
if (socialZoneMgr) {
|
|
|
|
|
|
const auto* zi = socialZoneMgr->getZoneInfo(c.areaId);
|
|
|
|
|
|
if (zi && !zi->name.empty()) zoneName = zi->name.c_str();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (zoneName)
|
|
|
|
|
|
ImGui::Text("Zone: %s", zoneName);
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::Text("Area ID: %u", c.areaId);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!c.note.empty())
|
|
|
|
|
|
ImGui::TextDisabled("Note: %s", c.note.c_str());
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 22:25:46 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Right-click context menu
|
|
|
|
|
|
if (ImGui::BeginPopupContextItem("FriendCtx")) {
|
|
|
|
|
|
ImGui::TextDisabled("%s", displayName);
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (c.isOnline()) {
|
|
|
|
|
|
if (ImGui::MenuItem("Whisper")) {
|
|
|
|
|
|
showSocialFrame_ = false;
|
|
|
|
|
|
chatPanel_.setWhisperTarget(c.name);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Invite to Group"))
|
|
|
|
|
|
gameHandler.inviteToGroup(c.name);
|
|
|
|
|
|
if (c.guid != 0 && ImGui::MenuItem("Trade"))
|
|
|
|
|
|
gameHandler.initiateTrade(c.guid);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Set Note")) {
|
|
|
|
|
|
noteEditContactIdx = static_cast<int>(ci);
|
|
|
|
|
|
strncpy(noteEditBuf, c.note.c_str(), sizeof(noteEditBuf) - 1);
|
|
|
|
|
|
noteEditBuf[sizeof(noteEditBuf) - 1] = '\0';
|
|
|
|
|
|
ImGui::OpenPopup("##SetFriendNote");
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Remove Friend"))
|
|
|
|
|
|
gameHandler.removeFriend(c.name);
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
2026-03-12 22:25:46 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
++shown;
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Separator between online and offline if there are both
|
|
|
|
|
|
if (pass == 0 && shown > 0) {
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 22:25:46 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (shown == 0) {
|
|
|
|
|
|
ImGui::TextDisabled("No friends yet.");
|
|
|
|
|
|
}
|
2026-03-10 21:40:21 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::EndChild();
|
2026-03-10 21:40:21 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// "Set Note" modal popup
|
|
|
|
|
|
if (ImGui::BeginPopup("##SetFriendNote")) {
|
|
|
|
|
|
const std::string& noteName = (noteEditContactIdx >= 0 &&
|
|
|
|
|
|
noteEditContactIdx < static_cast<int>(contacts.size()))
|
|
|
|
|
|
? contacts[noteEditContactIdx].name : "";
|
|
|
|
|
|
ImGui::TextDisabled("Note for %s:", noteName.c_str());
|
|
|
|
|
|
ImGui::SetNextItemWidth(180.0f);
|
|
|
|
|
|
bool confirm = ImGui::InputText("##noteinput", noteEditBuf, sizeof(noteEditBuf),
|
|
|
|
|
|
ImGuiInputTextFlags_EnterReturnsTrue);
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (confirm || ImGui::Button("OK")) {
|
|
|
|
|
|
if (!noteName.empty())
|
|
|
|
|
|
gameHandler.setFriendNote(noteName, noteEditBuf);
|
|
|
|
|
|
noteEditContactIdx = -1;
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Cancel")) {
|
|
|
|
|
|
noteEditContactIdx = -1;
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
2026-03-10 21:40:21 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Separator();
|
2026-03-10 21:40:21 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Add friend
|
|
|
|
|
|
static char addFriendBuf[64] = {};
|
|
|
|
|
|
ImGui::SetNextItemWidth(140.0f);
|
|
|
|
|
|
ImGui::InputText("##sf_addfriend", addFriendBuf, sizeof(addFriendBuf));
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("+##addfriend") && addFriendBuf[0] != '\0') {
|
|
|
|
|
|
gameHandler.addFriend(addFriendBuf);
|
|
|
|
|
|
addFriendBuf[0] = '\0';
|
|
|
|
|
|
}
|
2026-03-10 21:40:21 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
|
}
|
2026-03-10 21:40:21 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// ---- Ignore tab ----
|
|
|
|
|
|
if (ImGui::BeginTabItem("Ignore")) {
|
|
|
|
|
|
const auto& ignores = gameHandler.getIgnoreCache();
|
|
|
|
|
|
ImGui::BeginChild("##IgnoreList", ImVec2(200, 200), false);
|
2026-03-10 21:40:21 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (ignores.empty()) {
|
|
|
|
|
|
ImGui::TextDisabled("Ignore list is empty.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for (const auto& kv : ignores) {
|
|
|
|
|
|
ImGui::PushID(kv.first.c_str());
|
|
|
|
|
|
ImGui::TextUnformatted(kv.first.c_str());
|
|
|
|
|
|
if (ImGui::BeginPopupContextItem("IgnoreCtx")) {
|
|
|
|
|
|
ImGui::TextDisabled("%s", kv.first.c_str());
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Unignore"))
|
|
|
|
|
|
gameHandler.removeIgnore(kv.first);
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-10 21:40:21 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
ImGui::Separator();
|
2026-03-10 21:40:21 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Add ignore
|
|
|
|
|
|
static char addIgnBuf[64] = {};
|
|
|
|
|
|
ImGui::SetNextItemWidth(140.0f);
|
|
|
|
|
|
ImGui::InputText("##sf_addignore", addIgnBuf, sizeof(addIgnBuf));
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("+##addignore") && addIgnBuf[0] != '\0') {
|
|
|
|
|
|
gameHandler.addIgnore(addIgnBuf);
|
|
|
|
|
|
addIgnBuf[0] = '\0';
|
|
|
|
|
|
}
|
2026-03-10 21:40:21 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
|
}
|
2026-03-20 16:10:29 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// ---- Channels tab ----
|
|
|
|
|
|
if (ImGui::BeginTabItem("Channels")) {
|
|
|
|
|
|
const auto& channels = gameHandler.getJoinedChannels();
|
|
|
|
|
|
ImGui::BeginChild("##ChannelList", ImVec2(200, 200), false);
|
2026-03-20 16:10:29 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (channels.empty()) {
|
|
|
|
|
|
ImGui::TextDisabled("Not in any channels.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for (size_t ci = 0; ci < channels.size(); ++ci) {
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(ci));
|
|
|
|
|
|
ImGui::TextUnformatted(channels[ci].c_str());
|
|
|
|
|
|
if (ImGui::BeginPopupContextItem("ChanCtx")) {
|
|
|
|
|
|
ImGui::TextDisabled("%s", channels[ci].c_str());
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Leave Channel"))
|
|
|
|
|
|
gameHandler.leaveChannel(channels[ci]);
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-20 16:10:29 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
ImGui::Separator();
|
2026-03-20 16:10:29 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Join a channel
|
|
|
|
|
|
static char joinChanBuf[64] = {};
|
|
|
|
|
|
ImGui::SetNextItemWidth(140.0f);
|
|
|
|
|
|
ImGui::InputText("##sf_joinchan", joinChanBuf, sizeof(joinChanBuf));
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("+##joinchan") && joinChanBuf[0] != '\0') {
|
|
|
|
|
|
gameHandler.joinChannel(joinChanBuf);
|
|
|
|
|
|
joinChanBuf[0] = '\0';
|
|
|
|
|
|
}
|
2026-03-20 16:10:29 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
|
}
|
2026-03-20 16:10:29 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// ---- Arena tab (WotLK: shows per-team rating/record + roster) ----
|
|
|
|
|
|
const auto& arenaStats = gameHandler.getArenaTeamStats();
|
|
|
|
|
|
if (!arenaStats.empty()) {
|
|
|
|
|
|
if (ImGui::BeginTabItem("Arena")) {
|
|
|
|
|
|
ImGui::BeginChild("##ArenaList", ImVec2(0, 0), false);
|
2026-03-20 16:10:29 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
for (size_t ai = 0; ai < arenaStats.size(); ++ai) {
|
|
|
|
|
|
const auto& ts = arenaStats[ai];
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(ai));
|
2026-03-20 16:10:29 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Team header: "2v2: Team Name" or fallback "Team #id"
|
|
|
|
|
|
std::string teamLabel;
|
|
|
|
|
|
if (ts.teamType > 0)
|
|
|
|
|
|
teamLabel = std::to_string(ts.teamType) + "v" + std::to_string(ts.teamType) + ": ";
|
|
|
|
|
|
if (!ts.teamName.empty())
|
|
|
|
|
|
teamLabel += ts.teamName;
|
|
|
|
|
|
else
|
|
|
|
|
|
teamLabel += "Team #" + std::to_string(ts.teamId);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "%s", teamLabel.c_str());
|
2026-03-20 16:10:29 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Indent(8.0f);
|
|
|
|
|
|
// Rating and rank
|
|
|
|
|
|
ImGui::Text("Rating: %u", ts.rating);
|
|
|
|
|
|
if (ts.rank > 0) {
|
|
|
|
|
|
ImGui::SameLine(0, 6);
|
|
|
|
|
|
ImGui::TextDisabled("(Rank #%u)", ts.rank);
|
|
|
|
|
|
}
|
2026-03-20 16:10:29 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Weekly record
|
|
|
|
|
|
uint32_t weekLosses = ts.weekGames > ts.weekWins
|
|
|
|
|
|
? ts.weekGames - ts.weekWins : 0;
|
|
|
|
|
|
ImGui::Text("Week: %u W / %u L", ts.weekWins, weekLosses);
|
2026-03-20 16:10:29 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Season record
|
|
|
|
|
|
uint32_t seasLosses = ts.seasonGames > ts.seasonWins
|
|
|
|
|
|
? ts.seasonGames - ts.seasonWins : 0;
|
|
|
|
|
|
ImGui::Text("Season: %u W / %u L", ts.seasonWins, seasLosses);
|
2026-03-20 16:10:29 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Roster members (from SMSG_ARENA_TEAM_ROSTER)
|
|
|
|
|
|
const auto* roster = gameHandler.getArenaTeamRoster(ts.teamId);
|
|
|
|
|
|
if (roster && !roster->members.empty()) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::TextDisabled("-- Roster (%zu members) --",
|
|
|
|
|
|
roster->members.size());
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::SmallButton("Refresh"))
|
|
|
|
|
|
gameHandler.requestArenaTeamRoster(ts.teamId);
|
2026-03-20 16:10:29 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Column headers
|
|
|
|
|
|
ImGui::Columns(4, "##arenaRosterCols", false);
|
|
|
|
|
|
ImGui::SetColumnWidth(0, 110.0f);
|
|
|
|
|
|
ImGui::SetColumnWidth(1, 60.0f);
|
|
|
|
|
|
ImGui::SetColumnWidth(2, 60.0f);
|
|
|
|
|
|
ImGui::SetColumnWidth(3, 60.0f);
|
|
|
|
|
|
ImGui::TextDisabled("Name"); ImGui::NextColumn();
|
|
|
|
|
|
ImGui::TextDisabled("Rating"); ImGui::NextColumn();
|
|
|
|
|
|
ImGui::TextDisabled("Week"); ImGui::NextColumn();
|
|
|
|
|
|
ImGui::TextDisabled("Season"); ImGui::NextColumn();
|
|
|
|
|
|
ImGui::Separator();
|
2026-03-20 16:10:29 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
for (const auto& m : roster->members) {
|
|
|
|
|
|
// Name coloured green (online) or grey (offline)
|
|
|
|
|
|
if (m.online)
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.4f,1.0f,0.4f,1.0f),
|
|
|
|
|
|
"%s", m.name.c_str());
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::TextDisabled("%s", m.name.c_str());
|
|
|
|
|
|
ImGui::NextColumn();
|
2026-03-20 16:10:29 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Text("%u", m.personalRating);
|
|
|
|
|
|
ImGui::NextColumn();
|
2026-02-25 14:44:44 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
uint32_t wL = m.weekGames > m.weekWins
|
|
|
|
|
|
? m.weekGames - m.weekWins : 0;
|
|
|
|
|
|
ImGui::Text("%uW/%uL", m.weekWins, wL);
|
|
|
|
|
|
ImGui::NextColumn();
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
uint32_t sL = m.seasonGames > m.seasonWins
|
|
|
|
|
|
? m.seasonGames - m.seasonWins : 0;
|
|
|
|
|
|
ImGui::Text("%uW/%uL", m.seasonWins, sL);
|
|
|
|
|
|
ImGui::NextColumn();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Columns(1);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
if (ImGui::SmallButton("Load Roster"))
|
|
|
|
|
|
gameHandler.requestArenaTeamRoster(ts.teamId);
|
|
|
|
|
|
}
|
2026-03-18 12:31:48 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Unindent(8.0f);
|
2026-03-18 12:31:48 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (ai + 1 < arenaStats.size())
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
2026-03-18 12:31:48 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
ImGui::EndTabItem();
|
2026-03-18 12:31:48 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::EndTabBar();
|
2026-03-18 12:31:48 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::End();
|
|
|
|
|
|
showSocialFrame_ = open;
|
2026-03-18 12:31:48 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
}
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Buff/Debuff Bar (Phase 3)
|
|
|
|
|
|
// ============================================================
|
2026-02-25 14:44:44 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
void GameScreen::renderBuffBar(game::GameHandler& gameHandler) {
|
|
|
|
|
|
const auto& auras = gameHandler.getPlayerAuras();
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Count non-empty auras
|
|
|
|
|
|
int activeCount = 0;
|
|
|
|
|
|
for (const auto& a : auras) {
|
|
|
|
|
|
if (!a.isEmpty()) activeCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (activeCount == 0 && !gameHandler.hasPet()) return;
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
auto* assetMgr = core::Application::getInstance().getAssetManager();
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Position below the minimap (minimap: 200x200 at top-right, bottom edge at Y≈210)
|
|
|
|
|
|
// Anchored to the right side to stay away from party frames on the left
|
|
|
|
|
|
constexpr float ICON_SIZE = 32.0f;
|
|
|
|
|
|
constexpr int ICONS_PER_ROW = 8;
|
|
|
|
|
|
float barW = ICONS_PER_ROW * (ICON_SIZE + 4.0f) + 8.0f;
|
|
|
|
|
|
ImVec2 displaySize = ImGui::GetIO().DisplaySize;
|
|
|
|
|
|
float screenW = displaySize.x > 0.0f ? displaySize.x : 1280.0f;
|
|
|
|
|
|
// Y=215 puts us just below the minimap's bottom edge (minimap bottom ≈ 210)
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW - barW - 10.0f, 215.0f), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(barW, 0), ImGuiCond_Always);
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
|
|
|
|
|
|
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoScrollbar;
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.0f));
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(2.0f, 2.0f));
|
2026-02-25 14:44:44 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (ImGui::Begin("##BuffBar", nullptr, flags)) {
|
|
|
|
|
|
// Pre-sort auras: buffs first, then debuffs; within each group, shorter remaining first
|
|
|
|
|
|
uint64_t buffNowMs = static_cast<uint64_t>(
|
|
|
|
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
|
|
|
|
std::chrono::steady_clock::now().time_since_epoch()).count());
|
|
|
|
|
|
std::vector<size_t> buffSortedIdx;
|
|
|
|
|
|
buffSortedIdx.reserve(auras.size());
|
|
|
|
|
|
for (size_t i = 0; i < auras.size(); ++i)
|
|
|
|
|
|
if (!auras[i].isEmpty()) buffSortedIdx.push_back(i);
|
|
|
|
|
|
std::sort(buffSortedIdx.begin(), buffSortedIdx.end(), [&](size_t a, size_t b) {
|
|
|
|
|
|
const auto& aa = auras[a]; const auto& ab = auras[b];
|
|
|
|
|
|
bool aDebuff = (aa.flags & 0x80) != 0;
|
|
|
|
|
|
bool bDebuff = (ab.flags & 0x80) != 0;
|
|
|
|
|
|
if (aDebuff != bDebuff) return aDebuff < bDebuff; // buffs (0) first
|
|
|
|
|
|
int32_t ra = aa.getRemainingMs(buffNowMs);
|
|
|
|
|
|
int32_t rb = ab.getRemainingMs(buffNowMs);
|
|
|
|
|
|
if (ra < 0 && rb < 0) return false;
|
|
|
|
|
|
if (ra < 0) return false;
|
|
|
|
|
|
if (rb < 0) return true;
|
|
|
|
|
|
return ra < rb;
|
|
|
|
|
|
});
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Render one pass for buffs, one for debuffs
|
|
|
|
|
|
for (int pass = 0; pass < 2; ++pass) {
|
|
|
|
|
|
bool wantBuff = (pass == 0);
|
|
|
|
|
|
int shown = 0;
|
|
|
|
|
|
for (size_t si = 0; si < buffSortedIdx.size() && shown < 40; ++si) {
|
|
|
|
|
|
size_t i = buffSortedIdx[si];
|
|
|
|
|
|
const auto& aura = auras[i];
|
|
|
|
|
|
if (aura.isEmpty()) continue;
|
2026-02-16 20:16:14 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
bool isBuff = (aura.flags & 0x80) == 0; // 0x80 = negative/debuff flag
|
|
|
|
|
|
if (isBuff != wantBuff) continue; // only render matching pass
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (shown > 0 && shown % ICONS_PER_ROW != 0) ImGui::SameLine();
|
2026-02-25 14:44:44 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PushID(static_cast<int>(i) + (pass * 256));
|
2026-02-25 14:44:44 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Determine border color: buffs = green; debuffs use WoW dispel-type colors
|
|
|
|
|
|
ImVec4 borderColor;
|
|
|
|
|
|
if (isBuff) {
|
|
|
|
|
|
borderColor = ImVec4(0.2f, 0.8f, 0.2f, 0.9f); // green
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Debuff: color by dispel type (0=none/red, 1=magic/blue, 2=curse/purple,
|
|
|
|
|
|
// 3=disease/brown, 4=poison/green, other=dark-red)
|
|
|
|
|
|
uint8_t dt = gameHandler.getSpellDispelType(aura.spellId);
|
|
|
|
|
|
switch (dt) {
|
|
|
|
|
|
case 1: borderColor = ImVec4(0.15f, 0.50f, 1.00f, 0.9f); break; // magic: blue
|
|
|
|
|
|
case 2: borderColor = ImVec4(0.70f, 0.20f, 0.90f, 0.9f); break; // curse: purple
|
|
|
|
|
|
case 3: borderColor = ImVec4(0.55f, 0.30f, 0.10f, 0.9f); break; // disease: brown
|
|
|
|
|
|
case 4: borderColor = ImVec4(0.10f, 0.70f, 0.10f, 0.9f); break; // poison: green
|
|
|
|
|
|
default: borderColor = ImVec4(0.80f, 0.20f, 0.20f, 0.9f); break; // other: red
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Try to get spell icon
|
|
|
|
|
|
VkDescriptorSet iconTex = VK_NULL_HANDLE;
|
|
|
|
|
|
if (assetMgr) {
|
|
|
|
|
|
iconTex = getSpellIcon(aura.spellId, assetMgr);
|
|
|
|
|
|
}
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, borderColor);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2));
|
|
|
|
|
|
ImGui::ImageButton("##aura",
|
|
|
|
|
|
(ImTextureID)(uintptr_t)iconTex,
|
|
|
|
|
|
ImVec2(ICON_SIZE - 4, ICON_SIZE - 4));
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, borderColor);
|
|
|
|
|
|
const std::string& pAuraName = gameHandler.getSpellName(aura.spellId);
|
|
|
|
|
|
char label[32];
|
|
|
|
|
|
if (!pAuraName.empty())
|
|
|
|
|
|
snprintf(label, sizeof(label), "%.6s", pAuraName.c_str());
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(label, sizeof(label), "%u", aura.spellId);
|
|
|
|
|
|
ImGui::Button(label, ImVec2(ICON_SIZE, ICON_SIZE));
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
2026-02-13 21:39:48 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Compute remaining duration once (shared by overlay and tooltip)
|
|
|
|
|
|
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);
|
2026-02-16 20:16:14 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Clock-sweep overlay: dark fan shows elapsed time (WoW style)
|
|
|
|
|
|
if (remainMs > 0 && aura.maxDurationMs > 0) {
|
|
|
|
|
|
ImVec2 iconMin2 = ImGui::GetItemRectMin();
|
|
|
|
|
|
ImVec2 iconMax2 = ImGui::GetItemRectMax();
|
|
|
|
|
|
float cx2 = (iconMin2.x + iconMax2.x) * 0.5f;
|
|
|
|
|
|
float cy2 = (iconMin2.y + iconMax2.y) * 0.5f;
|
|
|
|
|
|
float fanR2 = (iconMax2.x - iconMin2.x) * 0.5f;
|
|
|
|
|
|
float total2 = static_cast<float>(aura.maxDurationMs);
|
|
|
|
|
|
float elapsedFrac2 = std::clamp(
|
|
|
|
|
|
1.0f - static_cast<float>(remainMs) / total2, 0.0f, 1.0f);
|
|
|
|
|
|
if (elapsedFrac2 > 0.005f) {
|
|
|
|
|
|
constexpr int SWEEP_SEGS = 24;
|
|
|
|
|
|
float sa = -IM_PI * 0.5f;
|
|
|
|
|
|
float ea = sa + elapsedFrac2 * 2.0f * IM_PI;
|
|
|
|
|
|
ImVec2 pts[SWEEP_SEGS + 2];
|
|
|
|
|
|
pts[0] = ImVec2(cx2, cy2);
|
|
|
|
|
|
for (int s = 0; s <= SWEEP_SEGS; ++s) {
|
|
|
|
|
|
float a = sa + (ea - sa) * s / static_cast<float>(SWEEP_SEGS);
|
|
|
|
|
|
pts[s + 1] = ImVec2(cx2 + std::cos(a) * fanR2,
|
|
|
|
|
|
cy2 + std::sin(a) * fanR2);
|
2026-02-25 14:44:44 -08:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::GetWindowDrawList()->AddConvexPolyFilled(
|
|
|
|
|
|
pts, SWEEP_SEGS + 2, IM_COL32(0, 0, 0, 145));
|
2026-02-13 21:39:48 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-16 20:16:14 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Duration countdown overlay — always visible on the icon bottom
|
|
|
|
|
|
if (remainMs > 0) {
|
|
|
|
|
|
ImVec2 iconMin = ImGui::GetItemRectMin();
|
|
|
|
|
|
ImVec2 iconMax = ImGui::GetItemRectMax();
|
|
|
|
|
|
char timeStr[12];
|
|
|
|
|
|
int secs = (remainMs + 999) / 1000; // ceiling seconds
|
|
|
|
|
|
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 - 2.0f;
|
|
|
|
|
|
// Choose timer color based on urgency
|
|
|
|
|
|
ImU32 timerColor;
|
|
|
|
|
|
if (remainMs < 10000) {
|
|
|
|
|
|
// < 10s: pulse red
|
|
|
|
|
|
float pulse = 0.7f + 0.3f * std::sin(
|
|
|
|
|
|
static_cast<float>(ImGui::GetTime()) * 6.0f);
|
|
|
|
|
|
timerColor = IM_COL32(
|
|
|
|
|
|
static_cast<int>(255 * pulse),
|
|
|
|
|
|
static_cast<int>(80 * pulse),
|
|
|
|
|
|
static_cast<int>(60 * pulse), 255);
|
|
|
|
|
|
} else if (remainMs < 30000) {
|
|
|
|
|
|
timerColor = IM_COL32(255, 165, 0, 255); // orange
|
|
|
|
|
|
} else {
|
|
|
|
|
|
timerColor = IM_COL32(255, 255, 255, 255); // white
|
|
|
|
|
|
}
|
|
|
|
|
|
// Drop shadow for readability over any icon colour
|
|
|
|
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(cx + 1, cy + 1),
|
|
|
|
|
|
IM_COL32(0, 0, 0, 200), timeStr);
|
|
|
|
|
|
ImGui::GetWindowDrawList()->AddText(ImVec2(cx, cy),
|
|
|
|
|
|
timerColor, timeStr);
|
|
|
|
|
|
}
|
2026-02-25 14:44:44 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Stack / charge count overlay — upper-left corner of the icon
|
|
|
|
|
|
if (aura.charges > 1) {
|
|
|
|
|
|
ImVec2 iconMin = ImGui::GetItemRectMin();
|
|
|
|
|
|
char chargeStr[8];
|
|
|
|
|
|
snprintf(chargeStr, sizeof(chargeStr), "%u", static_cast<unsigned>(aura.charges));
|
|
|
|
|
|
// Drop shadow then bright yellow text
|
|
|
|
|
|
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-02-25 14:44:44 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Right-click to cancel buffs / dismount
|
|
|
|
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
|
|
|
|
|
if (gameHandler.isMounted()) {
|
|
|
|
|
|
gameHandler.dismount();
|
|
|
|
|
|
} else if (isBuff) {
|
|
|
|
|
|
gameHandler.cancelAura(aura.spellId);
|
2026-02-16 20:16:14 -08:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
}
|
2026-02-25 14:44:44 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +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());
|
2026-02-16 20:16:14 -08:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
renderAuraRemaining(remainMs);
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
2026-02-25 14:44:44 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
shown++;
|
|
|
|
|
|
} // end aura loop
|
|
|
|
|
|
// Add visual gap between buffs and debuffs
|
|
|
|
|
|
if (pass == 0 && shown > 0) ImGui::Spacing();
|
|
|
|
|
|
} // end pass loop
|
|
|
|
|
|
|
|
|
|
|
|
// Dismiss Pet button
|
|
|
|
|
|
if (gameHandler.hasPet()) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.6f, 0.2f, 0.2f, 0.9f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.8f, 0.3f, 0.3f, 1.0f));
|
|
|
|
|
|
if (ImGui::Button("Dismiss Pet", ImVec2(-1, 0))) {
|
|
|
|
|
|
gameHandler.dismissPet();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Temporary weapon enchant timers (Shaman imbues, Rogue poisons, whetstones, etc.)
|
|
|
|
|
|
{
|
|
|
|
|
|
const auto& timers = gameHandler.getTempEnchantTimers();
|
|
|
|
|
|
if (!timers.empty()) {
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::Spacing();
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
static constexpr ImVec4 kEnchantSlotColors[] = {
|
|
|
|
|
|
colors::kOrange, // main-hand: gold
|
|
|
|
|
|
ImVec4(0.5f, 0.8f, 0.9f, 1.0f), // off-hand: teal
|
|
|
|
|
|
ImVec4(0.7f, 0.5f, 0.9f, 1.0f), // ranged: purple
|
|
|
|
|
|
};
|
|
|
|
|
|
uint64_t enchNowMs = static_cast<uint64_t>(
|
|
|
|
|
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
|
|
|
|
std::chrono::steady_clock::now().time_since_epoch()).count());
|
2026-02-25 14:44:44 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
for (const auto& t : timers) {
|
|
|
|
|
|
if (t.slot > 2) continue;
|
|
|
|
|
|
uint64_t remMs = (t.expireMs > enchNowMs) ? (t.expireMs - enchNowMs) : 0;
|
|
|
|
|
|
if (remMs == 0) continue;
|
|
|
|
|
|
|
|
|
|
|
|
ImVec4 col = kEnchantSlotColors[t.slot];
|
|
|
|
|
|
// Flash red when < 60s remaining
|
|
|
|
|
|
if (remMs < 60000) {
|
|
|
|
|
|
float pulse = 0.6f + 0.4f * std::sin(
|
|
|
|
|
|
static_cast<float>(ImGui::GetTime()) * 4.0f);
|
|
|
|
|
|
col = ImVec4(pulse, 0.2f, 0.1f, 1.0f);
|
2026-02-25 14:44:44 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Format remaining time
|
|
|
|
|
|
uint32_t secs = static_cast<uint32_t>((remMs + 999) / 1000);
|
|
|
|
|
|
char timeStr[16];
|
|
|
|
|
|
if (secs >= 3600)
|
|
|
|
|
|
snprintf(timeStr, sizeof(timeStr), "%dh%02dm", secs / 3600, (secs % 3600) / 60);
|
|
|
|
|
|
else if (secs >= 60)
|
|
|
|
|
|
snprintf(timeStr, sizeof(timeStr), "%d:%02d", secs / 60, secs % 60);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(timeStr, sizeof(timeStr), "%ds", secs);
|
2026-02-16 20:16:14 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PushID(static_cast<int>(t.slot) + 5000);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, col);
|
|
|
|
|
|
char label[40];
|
|
|
|
|
|
snprintf(label, sizeof(label), "~%s %s",
|
|
|
|
|
|
game::GameHandler::kTempEnchantSlotNames[t.slot], timeStr);
|
|
|
|
|
|
ImGui::Button(label, ImVec2(-1, 16));
|
|
|
|
|
|
if (ImGui::IsItemHovered())
|
|
|
|
|
|
ImGui::SetTooltip("Temporary weapon enchant: %s\nRemaining: %s",
|
|
|
|
|
|
game::GameHandler::kTempEnchantSlotNames[t.slot],
|
|
|
|
|
|
timeStr);
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::PopID();
|
2026-02-25 14:44:44 -08:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
2026-02-25 14:44:44 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
2026-02-25 14:44:44 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Loot Window (Phase 5)
|
|
|
|
|
|
// ============================================================
|
2026-02-25 14:44:44 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
void GameScreen::renderLootWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isLootWindowOpen()) return;
|
2026-02-25 14:44:44 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
2026-03-10 05:46:03 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 150, 200), ImGuiCond_Appearing);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(300, 0), ImGuiCond_Always);
|
2026-03-11 21:53:15 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
bool open = true;
|
|
|
|
|
|
if (ImGui::Begin("Loot", &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
|
const auto& loot = gameHandler.getCurrentLoot();
|
2026-03-10 05:46:03 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Gold (auto-looted on open; shown for feedback)
|
|
|
|
|
|
if (loot.gold > 0) {
|
|
|
|
|
|
ImGui::TextDisabled("Gold:");
|
|
|
|
|
|
ImGui::SameLine(0, 4);
|
|
|
|
|
|
renderCoinsText(loot.getGold(), loot.getSilver(), loot.getCopper());
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
}
|
2026-03-11 21:53:15 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Items with icons and labels
|
|
|
|
|
|
constexpr float iconSize = 32.0f;
|
|
|
|
|
|
int lootSlotClicked = -1; // defer loot pickup to avoid iterator invalidation
|
|
|
|
|
|
for (const auto& item : loot.items) {
|
|
|
|
|
|
ImGui::PushID(item.slotIndex);
|
2026-03-10 05:46:03 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Get item info for name and quality
|
|
|
|
|
|
const auto* info = gameHandler.getItemInfo(item.itemId);
|
|
|
|
|
|
std::string itemName;
|
|
|
|
|
|
game::ItemQuality quality = game::ItemQuality::COMMON;
|
|
|
|
|
|
if (info && !info->name.empty()) {
|
|
|
|
|
|
itemName = info->name;
|
|
|
|
|
|
quality = static_cast<game::ItemQuality>(info->quality);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
itemName = "Item #" + std::to_string(item.itemId);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImVec4 qColor = InventoryScreen::getQualityColor(quality);
|
|
|
|
|
|
bool startsQuest = (info && info->startQuestId != 0);
|
2026-03-10 05:46:03 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Get item icon
|
|
|
|
|
|
uint32_t displayId = item.displayInfoId;
|
|
|
|
|
|
if (displayId == 0 && info) displayId = info->displayInfoId;
|
|
|
|
|
|
VkDescriptorSet iconTex = inventoryScreen.getItemIcon(displayId);
|
2026-03-11 21:53:15 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImVec2 cursor = ImGui::GetCursorScreenPos();
|
|
|
|
|
|
float rowH = std::max(iconSize, ImGui::GetTextLineHeight() * 2.0f);
|
2026-03-11 21:53:15 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Invisible selectable for click handling
|
|
|
|
|
|
if (ImGui::Selectable("##loot", false, 0, ImVec2(0, rowH))) {
|
|
|
|
|
|
if (ImGui::GetIO().KeyShift && info && !info->name.empty()) {
|
|
|
|
|
|
// Shift-click: insert item link into chat
|
|
|
|
|
|
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
|
|
|
|
|
|
chatPanel_.insertChatLink(link);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
lootSlotClicked = item.slotIndex;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
|
|
|
|
|
lootSlotClicked = item.slotIndex;
|
|
|
|
|
|
}
|
|
|
|
|
|
bool hovered = ImGui::IsItemHovered();
|
2026-03-11 21:53:15 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Show item tooltip on hover
|
|
|
|
|
|
if (hovered && info && info->valid) {
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
|
|
|
|
|
} else if (hovered && info && !info->name.empty()) {
|
|
|
|
|
|
// Item info received but not yet fully valid — show name at minimum
|
|
|
|
|
|
ImGui::SetTooltip("%s", info->name.c_str());
|
|
|
|
|
|
}
|
2026-03-12 07:39:11 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
2026-03-11 21:53:15 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Draw hover highlight
|
|
|
|
|
|
if (hovered) {
|
|
|
|
|
|
drawList->AddRectFilled(cursor,
|
|
|
|
|
|
ImVec2(cursor.x + ImGui::GetContentRegionAvail().x + iconSize + 8.0f,
|
|
|
|
|
|
cursor.y + rowH),
|
|
|
|
|
|
IM_COL32(255, 255, 255, 30));
|
|
|
|
|
|
}
|
2026-03-10 05:46:03 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Draw icon
|
|
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
drawList->AddImage((ImTextureID)(uintptr_t)iconTex,
|
|
|
|
|
|
cursor, ImVec2(cursor.x + iconSize, cursor.y + iconSize));
|
|
|
|
|
|
drawList->AddRect(cursor, ImVec2(cursor.x + iconSize, cursor.y + iconSize),
|
|
|
|
|
|
ImGui::ColorConvertFloat4ToU32(qColor));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
drawList->AddRectFilled(cursor,
|
|
|
|
|
|
ImVec2(cursor.x + iconSize, cursor.y + iconSize),
|
|
|
|
|
|
IM_COL32(40, 40, 50, 200));
|
|
|
|
|
|
drawList->AddRect(cursor, ImVec2(cursor.x + iconSize, cursor.y + iconSize),
|
|
|
|
|
|
IM_COL32(80, 80, 80, 200));
|
|
|
|
|
|
}
|
|
|
|
|
|
// Quest-starter: gold outer glow border + "!" badge on top-right corner
|
|
|
|
|
|
if (startsQuest) {
|
|
|
|
|
|
drawList->AddRect(ImVec2(cursor.x - 2.0f, cursor.y - 2.0f),
|
|
|
|
|
|
ImVec2(cursor.x + iconSize + 2.0f, cursor.y + iconSize + 2.0f),
|
|
|
|
|
|
IM_COL32(255, 210, 0, 210), 0.0f, 0, 2.0f);
|
|
|
|
|
|
drawList->AddText(ImVec2(cursor.x + iconSize - 10.0f, cursor.y + 1.0f),
|
|
|
|
|
|
IM_COL32(255, 210, 0, 255), "!");
|
|
|
|
|
|
}
|
2026-03-11 21:53:15 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Draw item name
|
|
|
|
|
|
float textX = cursor.x + iconSize + 6.0f;
|
|
|
|
|
|
float textY = cursor.y + 2.0f;
|
|
|
|
|
|
drawList->AddText(ImVec2(textX, textY),
|
|
|
|
|
|
ImGui::ColorConvertFloat4ToU32(qColor), itemName.c_str());
|
2026-03-10 05:46:03 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Draw count or "Begins a Quest" label on second line
|
|
|
|
|
|
float secondLineY = textY + ImGui::GetTextLineHeight();
|
|
|
|
|
|
if (startsQuest) {
|
|
|
|
|
|
drawList->AddText(ImVec2(textX, secondLineY),
|
|
|
|
|
|
IM_COL32(255, 210, 0, 255), "Begins a Quest");
|
|
|
|
|
|
} else if (item.count > 1) {
|
|
|
|
|
|
char countStr[32];
|
|
|
|
|
|
snprintf(countStr, sizeof(countStr), "x%u", item.count);
|
|
|
|
|
|
drawList->AddText(ImVec2(textX, secondLineY), IM_COL32(200, 200, 200, 220), countStr);
|
2026-03-11 21:53:15 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
2026-03-11 21:53:15 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Process deferred loot pickup (after loop to avoid iterator invalidation)
|
|
|
|
|
|
if (lootSlotClicked >= 0) {
|
|
|
|
|
|
if (gameHandler.hasMasterLootCandidates()) {
|
|
|
|
|
|
// Master looter: open popup to choose recipient
|
|
|
|
|
|
char popupId[32];
|
|
|
|
|
|
snprintf(popupId, sizeof(popupId), "##MLGive%d", lootSlotClicked);
|
|
|
|
|
|
ImGui::OpenPopup(popupId);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
gameHandler.lootItem(static_cast<uint8_t>(lootSlotClicked));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-11 21:53:15 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Master loot "Give to" popups
|
|
|
|
|
|
if (gameHandler.hasMasterLootCandidates()) {
|
|
|
|
|
|
for (const auto& item : loot.items) {
|
|
|
|
|
|
char popupId[32];
|
|
|
|
|
|
snprintf(popupId, sizeof(popupId), "##MLGive%d", item.slotIndex);
|
|
|
|
|
|
if (ImGui::BeginPopup(popupId)) {
|
|
|
|
|
|
ImGui::TextDisabled("Give to:");
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
const auto& candidates = gameHandler.getMasterLootCandidates();
|
|
|
|
|
|
for (uint64_t candidateGuid : candidates) {
|
|
|
|
|
|
auto entity = gameHandler.getEntityManager().getEntity(candidateGuid);
|
|
|
|
|
|
auto* unit = (entity && entity->isUnit()) ? static_cast<game::Unit*>(entity.get()) : nullptr;
|
|
|
|
|
|
const char* cName = unit ? unit->getName().c_str() : nullptr;
|
|
|
|
|
|
char nameBuf[64];
|
|
|
|
|
|
if (!cName || cName[0] == '\0') {
|
|
|
|
|
|
snprintf(nameBuf, sizeof(nameBuf), "Player 0x%llx",
|
|
|
|
|
|
static_cast<unsigned long long>(candidateGuid));
|
|
|
|
|
|
cName = nameBuf;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem(cName)) {
|
|
|
|
|
|
gameHandler.lootMasterGive(item.slotIndex, candidateGuid);
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
2026-03-11 21:53:15 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::EndPopup();
|
2026-03-11 21:53:15 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-11 21:53:15 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (loot.items.empty() && loot.gold == 0) {
|
|
|
|
|
|
gameHandler.closeLoot();
|
|
|
|
|
|
}
|
2026-03-11 21:53:15 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
bool hasItems = !loot.items.empty();
|
|
|
|
|
|
if (hasItems) {
|
|
|
|
|
|
if (ImGui::Button("Loot All", ImVec2(-1, 0))) {
|
|
|
|
|
|
for (const auto& item : loot.items) {
|
|
|
|
|
|
gameHandler.lootItem(item.slotIndex);
|
|
|
|
|
|
}
|
2026-03-10 05:46:03 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::Button("Close", ImVec2(-1, 0))) {
|
|
|
|
|
|
gameHandler.closeLoot();
|
2026-02-13 21:39:48 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
2026-03-31 10:07:58 +03:00
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
gameHandler.closeLoot();
|
|
|
|
|
|
}
|
2026-02-13 21:39:48 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 00:53:57 -07:00
|
|
|
|
// ============================================================
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Gossip Window (Phase 5)
|
2026-03-12 00:53:57 -07:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
void GameScreen::renderGossipWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isGossipWindowOpen()) return;
|
2026-03-12 00:53:57 -07:00
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
2026-03-12 02:17:49 -07:00
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
2026-03-12 00:53:57 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 150), ImGuiCond_Appearing);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Always);
|
2026-03-12 00:53:57 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
bool open = true;
|
|
|
|
|
|
if (ImGui::Begin("NPC Dialog", &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
|
const auto& gossip = gameHandler.getCurrentGossip();
|
2026-03-12 10:01:35 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// NPC name (from creature cache)
|
|
|
|
|
|
auto npcEntity = gameHandler.getEntityManager().getEntity(gossip.npcGuid);
|
|
|
|
|
|
if (npcEntity && npcEntity->getType() == game::ObjectType::UNIT) {
|
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(npcEntity);
|
|
|
|
|
|
if (!unit->getName().empty()) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", unit->getName().c_str());
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 00:53:57 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Spacing();
|
2026-03-12 07:25:56 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Gossip option icons - matches WoW GossipOptionIcon enum
|
|
|
|
|
|
static constexpr const char* gossipIcons[] = {
|
|
|
|
|
|
"[Chat]", // 0 = GOSSIP_ICON_CHAT
|
|
|
|
|
|
"[Vendor]", // 1 = GOSSIP_ICON_VENDOR
|
|
|
|
|
|
"[Taxi]", // 2 = GOSSIP_ICON_TAXI
|
|
|
|
|
|
"[Trainer]", // 3 = GOSSIP_ICON_TRAINER
|
|
|
|
|
|
"[Interact]", // 4 = GOSSIP_ICON_INTERACT_1
|
|
|
|
|
|
"[Interact]", // 5 = GOSSIP_ICON_INTERACT_2
|
|
|
|
|
|
"[Banker]", // 6 = GOSSIP_ICON_MONEY_BAG (banker)
|
|
|
|
|
|
"[Chat]", // 7 = GOSSIP_ICON_TALK
|
|
|
|
|
|
"[Tabard]", // 8 = GOSSIP_ICON_TABARD
|
|
|
|
|
|
"[Battlemaster]", // 9 = GOSSIP_ICON_BATTLE
|
|
|
|
|
|
"[Option]", // 10 = GOSSIP_ICON_DOT
|
|
|
|
|
|
};
|
2026-03-12 01:31:44 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Default text for server-sent gossip option placeholders
|
|
|
|
|
|
static const std::unordered_map<std::string, std::string> gossipPlaceholders = {
|
|
|
|
|
|
{"GOSSIP_OPTION_BANKER", "I would like to check my deposit box."},
|
|
|
|
|
|
{"GOSSIP_OPTION_AUCTIONEER", "I'd like to browse your auctions."},
|
|
|
|
|
|
{"GOSSIP_OPTION_VENDOR", "I want to browse your goods."},
|
|
|
|
|
|
{"GOSSIP_OPTION_TAXIVENDOR", "I'd like to fly."},
|
|
|
|
|
|
{"GOSSIP_OPTION_TRAINER", "I seek training."},
|
|
|
|
|
|
{"GOSSIP_OPTION_INNKEEPER", "Make this inn your home."},
|
|
|
|
|
|
{"GOSSIP_OPTION_SPIRITGUIDE", "Return me to life."},
|
|
|
|
|
|
{"GOSSIP_OPTION_SPIRITHEALER", "Bring me back to life."},
|
|
|
|
|
|
{"GOSSIP_OPTION_STABLEPET", "I'd like to stable my pet."},
|
|
|
|
|
|
{"GOSSIP_OPTION_ARMORER", "I need to repair my equipment."},
|
|
|
|
|
|
{"GOSSIP_OPTION_GOSSIP", "What can you tell me?"},
|
|
|
|
|
|
{"GOSSIP_OPTION_BATTLEFIELD", "I'd like to go to the battleground."},
|
|
|
|
|
|
{"GOSSIP_OPTION_TABARDDESIGNER", "I want to create a guild tabard."},
|
|
|
|
|
|
{"GOSSIP_OPTION_PETITIONER", "I want to create a guild."},
|
|
|
|
|
|
};
|
2026-03-12 01:31:44 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
for (const auto& opt : gossip.options) {
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(opt.id));
|
2026-03-12 01:31:44 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Determine icon label - use text-based detection for shared icons
|
|
|
|
|
|
const char* icon = (opt.icon < 11) ? gossipIcons[opt.icon] : "[Option]";
|
|
|
|
|
|
if (opt.text == "GOSSIP_OPTION_AUCTIONEER") icon = "[Auctioneer]";
|
|
|
|
|
|
else if (opt.text == "GOSSIP_OPTION_BANKER") icon = "[Banker]";
|
|
|
|
|
|
else if (opt.text == "GOSSIP_OPTION_VENDOR") icon = "[Vendor]";
|
|
|
|
|
|
else if (opt.text == "GOSSIP_OPTION_TRAINER") icon = "[Trainer]";
|
|
|
|
|
|
else if (opt.text == "GOSSIP_OPTION_INNKEEPER") icon = "[Innkeeper]";
|
|
|
|
|
|
else if (opt.text == "GOSSIP_OPTION_STABLEPET") icon = "[Stable Master]";
|
|
|
|
|
|
else if (opt.text == "GOSSIP_OPTION_ARMORER") icon = "[Repair]";
|
2026-03-12 01:31:44 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Resolve placeholder text from server
|
|
|
|
|
|
std::string displayText = opt.text;
|
|
|
|
|
|
auto placeholderIt = gossipPlaceholders.find(displayText);
|
|
|
|
|
|
if (placeholderIt != gossipPlaceholders.end()) {
|
|
|
|
|
|
displayText = placeholderIt->second;
|
|
|
|
|
|
}
|
2026-03-12 01:31:44 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
std::string processedText = chatPanel_.replaceGenderPlaceholders(displayText, gameHandler);
|
|
|
|
|
|
std::string label = std::string(icon) + " " + processedText;
|
|
|
|
|
|
if (ImGui::Selectable(label.c_str())) {
|
|
|
|
|
|
if (opt.text == "GOSSIP_OPTION_ARMORER") {
|
|
|
|
|
|
gameHandler.setVendorCanRepair(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
gameHandler.selectGossipOption(opt.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
2026-03-12 01:31:44 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Fallback: some spirit healers don't send gossip options.
|
|
|
|
|
|
if (gossip.options.empty() && gameHandler.isPlayerGhost()) {
|
|
|
|
|
|
bool isSpirit = false;
|
|
|
|
|
|
if (npcEntity && npcEntity->getType() == game::ObjectType::UNIT) {
|
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(npcEntity);
|
|
|
|
|
|
std::string name = unit->getName();
|
|
|
|
|
|
std::transform(name.begin(), name.end(), name.begin(),
|
|
|
|
|
|
[](unsigned char c){ return static_cast<char>(std::tolower(c)); });
|
|
|
|
|
|
if (name.find("spirit healer") != std::string::npos ||
|
|
|
|
|
|
name.find("spirit guide") != std::string::npos) {
|
|
|
|
|
|
isSpirit = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isSpirit) {
|
|
|
|
|
|
if (ImGui::Selectable("[Spiritguide] Return to Graveyard")) {
|
|
|
|
|
|
gameHandler.activateSpiritHealer(gossip.npcGuid);
|
|
|
|
|
|
gameHandler.closeGossip();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 07:25:56 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Quest items
|
|
|
|
|
|
if (!gossip.quests.empty()) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(kColorYellow, "Quests:");
|
|
|
|
|
|
for (size_t qi = 0; qi < gossip.quests.size(); qi++) {
|
|
|
|
|
|
const auto& quest = gossip.quests[qi];
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(qi));
|
2026-03-12 01:31:44 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Determine icon and color based on QuestGiverStatus stored in questIcon
|
|
|
|
|
|
// 5=INCOMPLETE (gray?), 6=REWARD_REP (yellow?), 7=AVAILABLE_LOW (gray!),
|
|
|
|
|
|
// 8=AVAILABLE (yellow!), 10=REWARD (yellow?)
|
|
|
|
|
|
const char* statusIcon = "!";
|
|
|
|
|
|
ImVec4 statusColor = kColorYellow; // yellow
|
|
|
|
|
|
switch (quest.questIcon) {
|
|
|
|
|
|
case 5: // INCOMPLETE — in progress but not done
|
|
|
|
|
|
statusIcon = "?";
|
|
|
|
|
|
statusColor = colors::kMediumGray; // gray
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 6: // REWARD_REP — repeatable, ready to turn in
|
|
|
|
|
|
case 10: // REWARD — ready to turn in
|
|
|
|
|
|
statusIcon = "?";
|
|
|
|
|
|
statusColor = kColorYellow; // yellow
|
|
|
|
|
|
break;
|
|
|
|
|
|
case 7: // AVAILABLE_LOW — available but gray (low-level)
|
|
|
|
|
|
statusIcon = "!";
|
|
|
|
|
|
statusColor = colors::kMediumGray; // gray
|
|
|
|
|
|
break;
|
|
|
|
|
|
default: // AVAILABLE (8) and any others
|
|
|
|
|
|
statusIcon = "!";
|
|
|
|
|
|
statusColor = kColorYellow; // yellow
|
|
|
|
|
|
break;
|
2026-03-12 01:31:44 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Render: colored icon glyph then [Lv] Title
|
|
|
|
|
|
ImGui::TextColored(statusColor, "%s", statusIcon);
|
|
|
|
|
|
ImGui::SameLine(0, 4);
|
|
|
|
|
|
char qlabel[256];
|
|
|
|
|
|
snprintf(qlabel, sizeof(qlabel), "[%d] %s", quest.questLevel, quest.title.c_str());
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, statusColor);
|
|
|
|
|
|
if (ImGui::Selectable(qlabel)) {
|
|
|
|
|
|
gameHandler.selectGossipQuest(quest.questId);
|
2026-03-12 01:31:44 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 01:31:44 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
if (ImGui::Button("Close", ImVec2(-1, 0))) {
|
|
|
|
|
|
gameHandler.closeGossip();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
2026-03-12 10:01:35 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (!open) {
|
|
|
|
|
|
gameHandler.closeGossip();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 10:01:35 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Quest Details Window
|
|
|
|
|
|
// ============================================================
|
2026-03-12 01:31:44 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
void GameScreen::renderQuestDetailsWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isQuestDetailsOpen()) return;
|
2026-03-12 01:31:44 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
2026-03-12 00:53:57 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 225, screenH / 2 - 200), ImGuiCond_Appearing);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(450, 400), ImGuiCond_Appearing);
|
2026-03-12 01:31:44 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
bool open = true;
|
|
|
|
|
|
const auto& quest = gameHandler.getQuestDetails();
|
|
|
|
|
|
std::string processedTitle = chatPanel_.replaceGenderPlaceholders(quest.title, gameHandler);
|
|
|
|
|
|
if (ImGui::Begin(processedTitle.c_str(), &open)) {
|
|
|
|
|
|
// Quest description
|
|
|
|
|
|
if (!quest.details.empty()) {
|
|
|
|
|
|
std::string processedDetails = chatPanel_.replaceGenderPlaceholders(quest.details, gameHandler);
|
|
|
|
|
|
ImGui::TextWrapped("%s", processedDetails.c_str());
|
|
|
|
|
|
}
|
2026-03-12 00:53:57 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Objectives
|
|
|
|
|
|
if (!quest.objectives.empty()) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(ui::colors::kTooltipGold, "Objectives:");
|
|
|
|
|
|
std::string processedObjectives = chatPanel_.replaceGenderPlaceholders(quest.objectives, gameHandler);
|
|
|
|
|
|
ImGui::TextWrapped("%s", processedObjectives.c_str());
|
|
|
|
|
|
}
|
2026-03-12 00:53:57 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Choice reward items (player picks one)
|
|
|
|
|
|
auto renderQuestRewardItem = [&](const game::QuestRewardItem& ri) {
|
|
|
|
|
|
gameHandler.ensureItemInfo(ri.itemId);
|
|
|
|
|
|
auto* info = gameHandler.getItemInfo(ri.itemId);
|
|
|
|
|
|
VkDescriptorSet iconTex = VK_NULL_HANDLE;
|
|
|
|
|
|
uint32_t dispId = ri.displayInfoId;
|
|
|
|
|
|
if (info && info->valid && info->displayInfoId != 0) dispId = info->displayInfoId;
|
|
|
|
|
|
if (dispId != 0) iconTex = inventoryScreen.getItemIcon(dispId);
|
2026-03-12 00:53:57 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
std::string label;
|
|
|
|
|
|
ImVec4 nameCol = ui::colors::kWhite;
|
|
|
|
|
|
if (info && info->valid && !info->name.empty()) {
|
|
|
|
|
|
label = info->name;
|
|
|
|
|
|
nameCol = InventoryScreen::getQualityColor(static_cast<game::ItemQuality>(info->quality));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
label = "Item " + std::to_string(ri.itemId);
|
2026-03-12 01:31:44 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (ri.count > 1) label += " x" + std::to_string(ri.count);
|
2026-03-12 01:31:44 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
ImGui::Image((void*)(intptr_t)iconTex, ImVec2(18, 18));
|
|
|
|
|
|
if (ImGui::IsItemHovered() && info && info->valid)
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
2026-03-12 01:51:18 -07:00
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::TextColored(nameCol, " %s", label.c_str());
|
|
|
|
|
|
if (ImGui::IsItemHovered() && info && info->valid)
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
|
|
|
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
|
|
|
|
|
ImGui::GetIO().KeyShift && info && info->valid && !info->name.empty()) {
|
|
|
|
|
|
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
|
|
|
|
|
|
chatPanel_.insertChatLink(link);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-03-12 01:51:18 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (!quest.rewardChoiceItems.empty()) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(ui::colors::kTooltipGold, "Choose one reward:");
|
|
|
|
|
|
for (const auto& ri : quest.rewardChoiceItems) {
|
|
|
|
|
|
renderQuestRewardItem(ri);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 14:58:48 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Fixed reward items (always given)
|
|
|
|
|
|
if (!quest.rewardItems.empty()) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(ui::colors::kTooltipGold, "You will receive:");
|
|
|
|
|
|
for (const auto& ri : quest.rewardItems) {
|
|
|
|
|
|
renderQuestRewardItem(ri);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 14:58:48 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// XP and money rewards
|
|
|
|
|
|
if (quest.rewardXp > 0 || quest.rewardMoney > 0) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(ui::colors::kTooltipGold, "Rewards:");
|
|
|
|
|
|
if (quest.rewardXp > 0) {
|
|
|
|
|
|
ImGui::Text(" %u experience", quest.rewardXp);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (quest.rewardMoney > 0) {
|
|
|
|
|
|
ImGui::TextDisabled(" Money:"); ImGui::SameLine(0, 4);
|
|
|
|
|
|
renderCoinsFromCopper(quest.rewardMoney);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 14:58:48 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (quest.suggestedPlayers > 1) {
|
|
|
|
|
|
ImGui::TextColored(ui::colors::kLightGray,
|
|
|
|
|
|
"Suggested players: %u", quest.suggestedPlayers);
|
|
|
|
|
|
}
|
2026-03-12 14:58:48 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Accept / Decline buttons
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
float buttonW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
|
|
|
|
|
if (ImGui::Button("Accept", ImVec2(buttonW, 0))) {
|
|
|
|
|
|
gameHandler.acceptQuest();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Decline", ImVec2(buttonW, 0))) {
|
|
|
|
|
|
gameHandler.declineQuest();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
2026-03-12 14:58:48 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (!open) {
|
|
|
|
|
|
gameHandler.declineQuest();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-18 12:26:23 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Quest Request Items Window (turn-in progress check)
|
|
|
|
|
|
// ============================================================
|
2026-03-12 21:01:51 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
void GameScreen::renderQuestRequestItemsWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isQuestRequestItemsOpen()) return;
|
2026-03-12 21:01:51 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
2026-03-12 21:01:51 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 225, screenH / 2 - 200), ImGuiCond_Appearing);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(450, 350), ImGuiCond_Appearing);
|
2026-03-12 21:01:51 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
bool open = true;
|
|
|
|
|
|
const auto& quest = gameHandler.getQuestRequestItems();
|
|
|
|
|
|
auto countItemInInventory = [&](uint32_t itemId) -> uint32_t {
|
|
|
|
|
|
const auto& inv = gameHandler.getInventory();
|
|
|
|
|
|
uint32_t total = 0;
|
|
|
|
|
|
for (int i = 0; i < inv.getBackpackSize(); ++i) {
|
|
|
|
|
|
const auto& slot = inv.getBackpackSlot(i);
|
|
|
|
|
|
if (!slot.empty() && slot.item.itemId == itemId) total += slot.item.stackCount;
|
|
|
|
|
|
}
|
|
|
|
|
|
for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS; ++bag) {
|
|
|
|
|
|
int bagSize = inv.getBagSize(bag);
|
|
|
|
|
|
for (int s = 0; s < bagSize; ++s) {
|
|
|
|
|
|
const auto& slot = inv.getBagSlot(bag, s);
|
|
|
|
|
|
if (!slot.empty() && slot.item.itemId == itemId) total += slot.item.stackCount;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return total;
|
|
|
|
|
|
};
|
2026-03-12 21:01:51 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
std::string processedTitle = chatPanel_.replaceGenderPlaceholders(quest.title, gameHandler);
|
|
|
|
|
|
if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
|
|
|
|
|
|
if (!quest.completionText.empty()) {
|
|
|
|
|
|
std::string processedCompletionText = chatPanel_.replaceGenderPlaceholders(quest.completionText, gameHandler);
|
|
|
|
|
|
ImGui::TextWrapped("%s", processedCompletionText.c_str());
|
|
|
|
|
|
}
|
2026-03-12 14:58:48 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Required items
|
|
|
|
|
|
if (!quest.requiredItems.empty()) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(ui::colors::kTooltipGold, "Required Items:");
|
|
|
|
|
|
for (const auto& item : quest.requiredItems) {
|
|
|
|
|
|
uint32_t have = countItemInInventory(item.itemId);
|
|
|
|
|
|
bool enough = have >= item.count;
|
|
|
|
|
|
ImVec4 textCol = enough ? colors::kLightGreen : ImVec4(1.0f, 0.6f, 0.6f, 1.0f);
|
|
|
|
|
|
auto* info = gameHandler.getItemInfo(item.itemId);
|
|
|
|
|
|
const char* name = (info && info->valid) ? info->name.c_str() : nullptr;
|
2026-03-12 14:58:48 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Show icon if display info is available
|
|
|
|
|
|
uint32_t dispId = item.displayInfoId;
|
|
|
|
|
|
if (info && info->valid && info->displayInfoId != 0) dispId = info->displayInfoId;
|
|
|
|
|
|
if (dispId != 0) {
|
|
|
|
|
|
VkDescriptorSet iconTex = inventoryScreen.getItemIcon(dispId);
|
|
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(18, 18));
|
|
|
|
|
|
ImGui::SameLine();
|
2026-03-12 14:58:48 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
}
|
|
|
|
|
|
if (name && *name) {
|
|
|
|
|
|
ImGui::TextColored(textCol, "%s %u/%u", name, have, item.count);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(textCol, "Item %u %u/%u", item.itemId, have, item.count);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
|
|
|
|
|
ImGui::GetIO().KeyShift && info && info->valid && !info->name.empty()) {
|
|
|
|
|
|
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
|
|
|
|
|
|
chatPanel_.insertChatLink(link);
|
2026-03-12 14:58:48 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
}
|
2026-03-12 14:58:48 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (quest.requiredMoney > 0) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::TextDisabled("Required money:"); ImGui::SameLine(0, 4);
|
|
|
|
|
|
renderCoinsFromCopper(quest.requiredMoney);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Complete / Cancel buttons
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
float buttonW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
|
|
|
|
|
if (ImGui::Button("Complete Quest", ImVec2(buttonW, 0))) {
|
|
|
|
|
|
gameHandler.completeQuest();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(buttonW, 0))) {
|
|
|
|
|
|
gameHandler.closeQuestRequestItems();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!quest.isCompletable()) {
|
|
|
|
|
|
ImGui::TextDisabled("Server flagged this quest as incomplete; completion will be server-validated.");
|
2026-03-12 00:53:57 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (!open) {
|
|
|
|
|
|
gameHandler.closeQuestRequestItems();
|
|
|
|
|
|
}
|
2026-03-12 00:53:57 -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 10:07:58 +03:00
|
|
|
|
// Quest Offer Reward Window (choose reward)
|
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 10:07:58 +03:00
|
|
|
|
void GameScreen::renderQuestOfferRewardWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isQuestOfferRewardOpen()) 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 10:07:58 +03:00
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 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 10:07:58 +03:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 225, screenH / 2 - 200), ImGuiCond_Appearing);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(450, 400), ImGuiCond_Appearing);
|
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 10:07:58 +03:00
|
|
|
|
bool open = true;
|
|
|
|
|
|
const auto& quest = gameHandler.getQuestOfferReward();
|
|
|
|
|
|
static int selectedChoice = -1;
|
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 10:07:58 +03:00
|
|
|
|
// Auto-select if only one choice reward
|
|
|
|
|
|
if (quest.choiceRewards.size() == 1 && selectedChoice == -1) {
|
|
|
|
|
|
selectedChoice = 0;
|
|
|
|
|
|
}
|
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 10:07:58 +03:00
|
|
|
|
std::string processedTitle = chatPanel_.replaceGenderPlaceholders(quest.title, gameHandler);
|
|
|
|
|
|
if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) {
|
|
|
|
|
|
if (!quest.rewardText.empty()) {
|
|
|
|
|
|
std::string processedRewardText = chatPanel_.replaceGenderPlaceholders(quest.rewardText, gameHandler);
|
|
|
|
|
|
ImGui::TextWrapped("%s", processedRewardText.c_str());
|
|
|
|
|
|
}
|
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 10:07:58 +03:00
|
|
|
|
// Choice rewards (pick one)
|
|
|
|
|
|
// Trigger item info fetch for all reward items
|
|
|
|
|
|
for (const auto& item : quest.choiceRewards) gameHandler.ensureItemInfo(item.itemId);
|
|
|
|
|
|
for (const auto& item : quest.fixedRewards) gameHandler.ensureItemInfo(item.itemId);
|
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 10:07:58 +03:00
|
|
|
|
// Helper: resolve icon tex + quality color for a reward item
|
|
|
|
|
|
auto resolveRewardItemVis = [&](const game::QuestRewardItem& ri)
|
|
|
|
|
|
-> std::pair<VkDescriptorSet, ImVec4>
|
|
|
|
|
|
{
|
|
|
|
|
|
auto* info = gameHandler.getItemInfo(ri.itemId);
|
|
|
|
|
|
uint32_t dispId = ri.displayInfoId;
|
|
|
|
|
|
if (info && info->valid && info->displayInfoId != 0) dispId = info->displayInfoId;
|
|
|
|
|
|
VkDescriptorSet iconTex = dispId ? inventoryScreen.getItemIcon(dispId) : VK_NULL_HANDLE;
|
|
|
|
|
|
ImVec4 col = (info && info->valid)
|
|
|
|
|
|
? InventoryScreen::getQualityColor(static_cast<game::ItemQuality>(info->quality))
|
|
|
|
|
|
: ui::colors::kWhite;
|
|
|
|
|
|
return {iconTex, col};
|
|
|
|
|
|
};
|
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 10:07:58 +03:00
|
|
|
|
// Helper: show full item tooltip (reuses InventoryScreen's rich tooltip)
|
|
|
|
|
|
auto rewardItemTooltip = [&](const game::QuestRewardItem& ri, ImVec4 /*nameCol*/) {
|
|
|
|
|
|
auto* info = gameHandler.getItemInfo(ri.itemId);
|
|
|
|
|
|
if (!info || !info->valid) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
ImGui::TextDisabled("Loading item data...");
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
return;
|
2026-03-12 06:55:16 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
|
|
|
|
|
};
|
2026-02-07 23:47:43 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (!quest.choiceRewards.empty()) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(ui::colors::kTooltipGold, "Choose a reward:");
|
2026-02-07 23:47:43 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
for (size_t i = 0; i < quest.choiceRewards.size(); ++i) {
|
|
|
|
|
|
const auto& item = quest.choiceRewards[i];
|
|
|
|
|
|
auto* info = gameHandler.getItemInfo(item.itemId);
|
|
|
|
|
|
auto [iconTex, qualityColor] = resolveRewardItemVis(item);
|
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 10:07:58 +03:00
|
|
|
|
std::string label;
|
|
|
|
|
|
if (info && info->valid && !info->name.empty()) label = info->name;
|
|
|
|
|
|
else label = "Item " + std::to_string(item.itemId);
|
|
|
|
|
|
if (item.count > 1) label += " x" + std::to_string(item.count);
|
2026-03-09 16:36:58 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
bool selected = (selectedChoice == static_cast<int>(i));
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(i));
|
|
|
|
|
|
|
|
|
|
|
|
// Icon then selectable on same line
|
|
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
ImGui::Image((void*)(intptr_t)iconTex, ImVec2(20, 20));
|
|
|
|
|
|
if (ImGui::IsItemHovered()) rewardItemTooltip(item, qualityColor);
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, qualityColor);
|
|
|
|
|
|
if (ImGui::Selectable(label.c_str(), selected, 0, ImVec2(0, 20))) {
|
|
|
|
|
|
if (ImGui::GetIO().KeyShift && info && info->valid && !info->name.empty()) {
|
|
|
|
|
|
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
|
|
|
|
|
|
chatPanel_.insertChatLink(link);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
selectedChoice = static_cast<int>(i);
|
2026-03-17 19:04:40 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
if (ImGui::IsItemHovered()) rewardItemTooltip(item, qualityColor);
|
2026-03-17 19:04:40 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PopID();
|
2026-03-09 16:36:58 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
}
|
2026-03-09 16:36:58 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Fixed rewards (always given)
|
|
|
|
|
|
if (!quest.fixedRewards.empty()) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(ui::colors::kTooltipGold, "You will also receive:");
|
|
|
|
|
|
for (const auto& item : quest.fixedRewards) {
|
|
|
|
|
|
auto* info = gameHandler.getItemInfo(item.itemId);
|
|
|
|
|
|
auto [iconTex, qualityColor] = resolveRewardItemVis(item);
|
2026-03-09 17:08:14 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
std::string label;
|
|
|
|
|
|
if (info && info->valid && !info->name.empty()) label = info->name;
|
|
|
|
|
|
else label = "Item " + std::to_string(item.itemId);
|
|
|
|
|
|
if (item.count > 1) label += " x" + std::to_string(item.count);
|
2026-02-08 00:00:12 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
ImGui::Image((void*)(intptr_t)iconTex, ImVec2(18, 18));
|
|
|
|
|
|
if (ImGui::IsItemHovered()) rewardItemTooltip(item, qualityColor);
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TextColored(qualityColor, " %s", label.c_str());
|
|
|
|
|
|
if (ImGui::IsItemHovered()) rewardItemTooltip(item, qualityColor);
|
|
|
|
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
|
|
|
|
|
ImGui::GetIO().KeyShift && info && info->valid && !info->name.empty()) {
|
|
|
|
|
|
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
|
|
|
|
|
|
chatPanel_.insertChatLink(link);
|
2026-03-10 19:33:25 -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 10:07:58 +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 10:07:58 +03:00
|
|
|
|
// Money / XP rewards
|
|
|
|
|
|
if (quest.rewardXp > 0 || quest.rewardMoney > 0) {
|
2026-03-10 21:29:47 -07:00
|
|
|
|
ImGui::Spacing();
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(ui::colors::kTooltipGold, "Rewards:");
|
|
|
|
|
|
if (quest.rewardXp > 0)
|
|
|
|
|
|
ImGui::Text(" %u experience", quest.rewardXp);
|
|
|
|
|
|
if (quest.rewardMoney > 0) {
|
|
|
|
|
|
ImGui::TextDisabled(" Money:"); ImGui::SameLine(0, 4);
|
|
|
|
|
|
renderCoinsFromCopper(quest.rewardMoney);
|
2026-02-26 10:41:29 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 18:15:51 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Complete button
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
float buttonW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f;
|
2026-03-12 18:15:51 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
bool canComplete = quest.choiceRewards.empty() || selectedChoice >= 0;
|
|
|
|
|
|
if (!canComplete) ImGui::BeginDisabled();
|
|
|
|
|
|
if (ImGui::Button("Complete Quest", ImVec2(buttonW, 0))) {
|
|
|
|
|
|
uint32_t rewardIdx = 0;
|
|
|
|
|
|
if (!quest.choiceRewards.empty() && selectedChoice >= 0 &&
|
|
|
|
|
|
selectedChoice < static_cast<int>(quest.choiceRewards.size())) {
|
|
|
|
|
|
// Server expects the original slot index from its fixed-size reward array.
|
|
|
|
|
|
rewardIdx = quest.choiceRewards[static_cast<size_t>(selectedChoice)].choiceSlot;
|
|
|
|
|
|
}
|
|
|
|
|
|
gameHandler.chooseQuestReward(rewardIdx);
|
|
|
|
|
|
selectedChoice = -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!canComplete) ImGui::EndDisabled();
|
2026-03-12 18:15:51 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(buttonW, 0))) {
|
|
|
|
|
|
gameHandler.closeQuestOfferReward();
|
|
|
|
|
|
selectedChoice = -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
2026-03-12 18:15:51 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (!open) {
|
|
|
|
|
|
gameHandler.closeQuestOfferReward();
|
|
|
|
|
|
selectedChoice = -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 18:15:51 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// ItemExtendedCost.dbc loader
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::loadExtendedCostDBC() {
|
|
|
|
|
|
if (extendedCostDbLoaded_) return;
|
|
|
|
|
|
extendedCostDbLoaded_ = true;
|
|
|
|
|
|
auto* am = core::Application::getInstance().getAssetManager();
|
|
|
|
|
|
if (!am || !am->isInitialized()) return;
|
|
|
|
|
|
auto dbc = am->loadDBC("ItemExtendedCost.dbc");
|
|
|
|
|
|
if (!dbc || !dbc->isLoaded()) return;
|
|
|
|
|
|
// WotLK ItemExtendedCost.dbc: field 0=ID, 1=honorPoints, 2=arenaPoints,
|
|
|
|
|
|
// 3=arenaSlotRestrictions, 4-8=itemId[5], 9-13=itemCount[5], 14=reqRating, 15=purchaseGroup
|
|
|
|
|
|
for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) {
|
|
|
|
|
|
uint32_t id = dbc->getUInt32(i, 0);
|
|
|
|
|
|
if (id == 0) continue;
|
|
|
|
|
|
ExtendedCostEntry e;
|
|
|
|
|
|
e.honorPoints = dbc->getUInt32(i, 1);
|
|
|
|
|
|
e.arenaPoints = dbc->getUInt32(i, 2);
|
|
|
|
|
|
for (int j = 0; j < 5; ++j) {
|
|
|
|
|
|
e.itemId[j] = dbc->getUInt32(i, 4 + j);
|
|
|
|
|
|
e.itemCount[j] = dbc->getUInt32(i, 9 + j);
|
2026-03-12 18:15:51 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
extendedCostCache_[id] = e;
|
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 10:07:58 +03:00
|
|
|
|
LOG_INFO("ItemExtendedCost.dbc: loaded ", extendedCostCache_.size(), " entries");
|
|
|
|
|
|
}
|
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 10:07:58 +03:00
|
|
|
|
std::string GameScreen::formatExtendedCost(uint32_t extendedCostId, game::GameHandler& gameHandler) {
|
|
|
|
|
|
loadExtendedCostDBC();
|
|
|
|
|
|
auto it = extendedCostCache_.find(extendedCostId);
|
|
|
|
|
|
if (it == extendedCostCache_.end()) return "[Tokens]";
|
|
|
|
|
|
const auto& e = it->second;
|
|
|
|
|
|
std::string result;
|
|
|
|
|
|
if (e.honorPoints > 0) {
|
|
|
|
|
|
result += std::to_string(e.honorPoints) + " Honor";
|
|
|
|
|
|
}
|
|
|
|
|
|
if (e.arenaPoints > 0) {
|
|
|
|
|
|
if (!result.empty()) result += ", ";
|
|
|
|
|
|
result += std::to_string(e.arenaPoints) + " Arena";
|
|
|
|
|
|
}
|
|
|
|
|
|
for (int j = 0; j < 5; ++j) {
|
|
|
|
|
|
if (e.itemId[j] == 0 || e.itemCount[j] == 0) continue;
|
|
|
|
|
|
if (!result.empty()) result += ", ";
|
|
|
|
|
|
gameHandler.ensureItemInfo(e.itemId[j]); // query if not cached
|
|
|
|
|
|
const auto* itemInfo = gameHandler.getItemInfo(e.itemId[j]);
|
|
|
|
|
|
if (itemInfo && itemInfo->valid && !itemInfo->name.empty()) {
|
|
|
|
|
|
result += std::to_string(e.itemCount[j]) + "x " + itemInfo->name;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
result += std::to_string(e.itemCount[j]) + "x Item#" + std::to_string(e.itemId[j]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return result.empty() ? "[Tokens]" : result;
|
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 10:07:58 +03:00
|
|
|
|
// Vendor Window (Phase 5)
|
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 10:07:58 +03:00
|
|
|
|
void GameScreen::renderVendorWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isVendorWindowOpen()) 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
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 100), ImGuiCond_Appearing);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(450, 400), ImGuiCond_Appearing);
|
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
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (ImGui::Begin("Vendor", &open)) {
|
|
|
|
|
|
const auto& vendor = gameHandler.getVendorItems();
|
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 10:07:58 +03:00
|
|
|
|
// Show player money
|
|
|
|
|
|
uint64_t money = gameHandler.getMoneyCopper();
|
|
|
|
|
|
ImGui::TextDisabled("Your money:"); ImGui::SameLine(0, 4);
|
|
|
|
|
|
renderCoinsFromCopper(money);
|
2026-02-06 15:41:29 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (vendor.canRepair) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 8.0f);
|
|
|
|
|
|
if (ImGui::SmallButton("Repair All")) {
|
|
|
|
|
|
gameHandler.repairAll(vendor.vendorGuid, false);
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
// Show durability summary of all equipment
|
|
|
|
|
|
const auto& inv = gameHandler.getInventory();
|
|
|
|
|
|
int damagedCount = 0;
|
|
|
|
|
|
int brokenCount = 0;
|
|
|
|
|
|
for (int s = 0; s < static_cast<int>(game::EquipSlot::BAG1); s++) {
|
|
|
|
|
|
const auto& slot = inv.getEquipSlot(static_cast<game::EquipSlot>(s));
|
|
|
|
|
|
if (slot.empty() || slot.item.maxDurability == 0) continue;
|
|
|
|
|
|
if (slot.item.curDurability == 0) brokenCount++;
|
|
|
|
|
|
else if (slot.item.curDurability < slot.item.maxDurability) damagedCount++;
|
2026-03-11 21:27:16 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (brokenCount > 0)
|
|
|
|
|
|
ImGui::SetTooltip("Repair all equipped items\n%d damaged, %d broken", damagedCount, brokenCount);
|
|
|
|
|
|
else if (damagedCount > 0)
|
|
|
|
|
|
ImGui::SetTooltip("Repair all equipped items\n%d item%s need repair", damagedCount, damagedCount > 1 ? "s" : "");
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::SetTooltip("All equipment is in good condition");
|
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 10:07:58 +03:00
|
|
|
|
if (gameHandler.isInGuild()) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::SmallButton("Repair (Guild)")) {
|
|
|
|
|
|
gameHandler.repairAll(vendor.vendorGuid, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::SetTooltip("Repair all equipped items using guild bank funds");
|
|
|
|
|
|
}
|
2026-03-10 20:53:21 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
2026-03-10 20:53:21 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::TextColored(ui::colors::kLightGray, "Right-click bag items to sell");
|
2026-02-06 15:41:29 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Count grey (POOR quality) sellable items across backpack and bags
|
|
|
|
|
|
const auto& inv = gameHandler.getInventory();
|
|
|
|
|
|
int junkCount = 0;
|
|
|
|
|
|
for (int i = 0; i < inv.getBackpackSize(); ++i) {
|
|
|
|
|
|
const auto& sl = inv.getBackpackSlot(i);
|
|
|
|
|
|
if (!sl.empty() && sl.item.quality == game::ItemQuality::POOR && sl.item.sellPrice > 0)
|
|
|
|
|
|
++junkCount;
|
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 10:07:58 +03:00
|
|
|
|
for (int b = 0; b < game::Inventory::NUM_BAG_SLOTS; ++b) {
|
|
|
|
|
|
for (int s = 0; s < inv.getBagSize(b); ++s) {
|
|
|
|
|
|
const auto& sl = inv.getBagSlot(b, s);
|
|
|
|
|
|
if (!sl.empty() && sl.item.quality == game::ItemQuality::POOR && sl.item.sellPrice > 0)
|
|
|
|
|
|
++junkCount;
|
2026-03-12 17:58:24 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (junkCount > 0) {
|
|
|
|
|
|
char junkLabel[64];
|
|
|
|
|
|
snprintf(junkLabel, sizeof(junkLabel), "Sell All Junk (%d item%s)",
|
|
|
|
|
|
junkCount, junkCount == 1 ? "" : "s");
|
|
|
|
|
|
if (ImGui::Button(junkLabel, ImVec2(-1, 0))) {
|
|
|
|
|
|
for (int i = 0; i < inv.getBackpackSize(); ++i) {
|
|
|
|
|
|
const auto& sl = inv.getBackpackSlot(i);
|
|
|
|
|
|
if (!sl.empty() && sl.item.quality == game::ItemQuality::POOR && sl.item.sellPrice > 0)
|
|
|
|
|
|
gameHandler.sellItemBySlot(i);
|
|
|
|
|
|
}
|
|
|
|
|
|
for (int b = 0; b < game::Inventory::NUM_BAG_SLOTS; ++b) {
|
|
|
|
|
|
for (int s = 0; s < inv.getBagSize(b); ++s) {
|
|
|
|
|
|
const auto& sl = inv.getBagSlot(b, s);
|
|
|
|
|
|
if (!sl.empty() && sl.item.quality == game::ItemQuality::POOR && sl.item.sellPrice > 0)
|
|
|
|
|
|
gameHandler.sellItemInBag(b, s);
|
2026-03-12 17:58:24 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Separator();
|
Fix camera orbit, deselect, chat formatting, loot/vendor bugs, critter hostility, and character screen
Smooth idle camera orbit without jump at loop boundary, click empty space to
deselect target, auto-target when attacked, fix critter hostility so neutral
factions aren't flagged red, add armor/stats to item templates, fix loot
iterator invalidation, show item template names as fallback, position drop
confirmation at cursor, remove [SYSTEM] chat prefix, show NPC names in monster
say/yell, and prevent auto-login on character select screen.
2026-02-06 16:40:44 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
const auto& buyback = gameHandler.getBuybackItems();
|
|
|
|
|
|
if (!buyback.empty()) {
|
|
|
|
|
|
ImGui::TextColored(ui::colors::kTooltipGold, "Buy Back");
|
|
|
|
|
|
if (ImGui::BeginTable("BuybackTable", 4, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
|
|
|
|
|
|
ImGui::TableSetupColumn("##icon", ImGuiTableColumnFlags_WidthFixed, 22.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
|
|
ImGui::TableSetupColumn("Price", ImGuiTableColumnFlags_WidthFixed, 110.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Buy", ImGuiTableColumnFlags_WidthFixed, 62.0f);
|
|
|
|
|
|
ImGui::TableHeadersRow();
|
|
|
|
|
|
// Show all buyback items (most recently sold first)
|
|
|
|
|
|
for (int i = 0; i < static_cast<int>(buyback.size()); ++i) {
|
|
|
|
|
|
const auto& entry = buyback[i];
|
|
|
|
|
|
gameHandler.ensureItemInfo(entry.item.itemId);
|
|
|
|
|
|
auto* bbInfo = gameHandler.getItemInfo(entry.item.itemId);
|
|
|
|
|
|
uint32_t sellPrice = entry.item.sellPrice;
|
|
|
|
|
|
if (sellPrice == 0) {
|
|
|
|
|
|
if (bbInfo && bbInfo->valid) sellPrice = bbInfo->sellPrice;
|
|
|
|
|
|
}
|
|
|
|
|
|
uint64_t price = static_cast<uint64_t>(sellPrice) *
|
|
|
|
|
|
static_cast<uint64_t>(entry.count > 0 ? entry.count : 1);
|
|
|
|
|
|
uint32_t g = static_cast<uint32_t>(price / 10000);
|
|
|
|
|
|
uint32_t s = static_cast<uint32_t>((price / 100) % 100);
|
|
|
|
|
|
uint32_t c = static_cast<uint32_t>(price % 100);
|
|
|
|
|
|
bool canAfford = money >= price;
|
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 10:07:58 +03:00
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
ImGui::PushID(8000 + i);
|
|
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
|
|
|
|
|
{
|
|
|
|
|
|
uint32_t dispId = entry.item.displayInfoId;
|
|
|
|
|
|
if (bbInfo && bbInfo->valid && bbInfo->displayInfoId != 0) dispId = bbInfo->displayInfoId;
|
|
|
|
|
|
if (dispId != 0) {
|
|
|
|
|
|
VkDescriptorSet iconTex = inventoryScreen.getItemIcon(dispId);
|
|
|
|
|
|
if (iconTex) ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(18, 18));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
|
|
|
|
game::ItemQuality bbQuality = entry.item.quality;
|
|
|
|
|
|
if (bbInfo && bbInfo->valid) bbQuality = static_cast<game::ItemQuality>(bbInfo->quality);
|
|
|
|
|
|
ImVec4 bbQc = InventoryScreen::getQualityColor(bbQuality);
|
|
|
|
|
|
const char* name = entry.item.name.empty() ? "Unknown Item" : entry.item.name.c_str();
|
|
|
|
|
|
if (entry.count > 1) {
|
|
|
|
|
|
ImGui::TextColored(bbQc, "%s x%u", name, entry.count);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(bbQc, "%s", name);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::IsItemHovered() && bbInfo && bbInfo->valid)
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*bbInfo);
|
|
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
|
|
|
|
|
if (canAfford) {
|
|
|
|
|
|
renderCoinsText(g, s, c);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(kColorRed, "%ug %us %uc", g, s, c);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TableSetColumnIndex(3);
|
|
|
|
|
|
if (!canAfford) ImGui::BeginDisabled();
|
|
|
|
|
|
char bbLabel[32];
|
|
|
|
|
|
snprintf(bbLabel, sizeof(bbLabel), "Buy Back##bb%d", i);
|
|
|
|
|
|
if (ImGui::SmallButton(bbLabel)) {
|
|
|
|
|
|
gameHandler.buyBackItem(static_cast<uint32_t>(i));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!canAfford) ImGui::EndDisabled();
|
|
|
|
|
|
ImGui::PopID();
|
2026-03-11 22:16:19 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::EndTable();
|
2026-03-11 22:16:19 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Separator();
|
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 10:07:58 +03:00
|
|
|
|
if (vendor.items.empty()) {
|
|
|
|
|
|
ImGui::TextDisabled("This vendor has nothing for sale.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Search + quantity controls on one row
|
|
|
|
|
|
ImGui::SetNextItemWidth(200.0f);
|
|
|
|
|
|
ImGui::InputTextWithHint("##VendorSearch", "Search...", vendorSearchFilter_, sizeof(vendorSearchFilter_));
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::Text("Qty:");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetNextItemWidth(60.0f);
|
|
|
|
|
|
static int vendorBuyQty = 1;
|
|
|
|
|
|
ImGui::InputInt("##VendorQty", &vendorBuyQty, 1, 5);
|
|
|
|
|
|
if (vendorBuyQty < 1) vendorBuyQty = 1;
|
|
|
|
|
|
if (vendorBuyQty > 99) vendorBuyQty = 99;
|
|
|
|
|
|
ImGui::Spacing();
|
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 10:07:58 +03:00
|
|
|
|
if (ImGui::BeginTable("VendorTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) {
|
|
|
|
|
|
ImGui::TableSetupColumn("##icon", ImGuiTableColumnFlags_WidthFixed, 22.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
|
|
ImGui::TableSetupColumn("Price", ImGuiTableColumnFlags_WidthFixed, 120.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Stock", ImGuiTableColumnFlags_WidthFixed, 60.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Buy", ImGuiTableColumnFlags_WidthFixed, 50.0f);
|
|
|
|
|
|
ImGui::TableHeadersRow();
|
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 10:07:58 +03:00
|
|
|
|
std::string vendorFilter(vendorSearchFilter_);
|
|
|
|
|
|
// Lowercase filter for case-insensitive match
|
|
|
|
|
|
for (char& c : vendorFilter) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
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 10:07:58 +03:00
|
|
|
|
for (int vi = 0; vi < static_cast<int>(vendor.items.size()); ++vi) {
|
|
|
|
|
|
const auto& item = vendor.items[vi];
|
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 10:07:58 +03:00
|
|
|
|
// Proactively ensure vendor item info is loaded
|
|
|
|
|
|
gameHandler.ensureItemInfo(item.itemId);
|
|
|
|
|
|
auto* info = gameHandler.getItemInfo(item.itemId);
|
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 10:07:58 +03:00
|
|
|
|
// Apply search filter
|
|
|
|
|
|
if (!vendorFilter.empty()) {
|
|
|
|
|
|
std::string nameLC = info && info->valid ? info->name : ("Item " + std::to_string(item.itemId));
|
|
|
|
|
|
for (char& c : nameLC) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
if (nameLC.find(vendorFilter) == std::string::npos) {
|
|
|
|
|
|
ImGui::PushID(vi);
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
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 10:07:58 +03:00
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
ImGui::PushID(vi);
|
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 10:07:58 +03:00
|
|
|
|
// Icon column
|
|
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
|
|
|
|
|
{
|
|
|
|
|
|
uint32_t dispId = item.displayInfoId;
|
|
|
|
|
|
if (info && info->valid && info->displayInfoId != 0) dispId = info->displayInfoId;
|
|
|
|
|
|
if (dispId != 0) {
|
|
|
|
|
|
VkDescriptorSet iconTex = inventoryScreen.getItemIcon(dispId);
|
|
|
|
|
|
if (iconTex) ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(18, 18));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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 10:07:58 +03:00
|
|
|
|
// Name column
|
|
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
|
|
|
|
if (info && info->valid) {
|
|
|
|
|
|
ImVec4 qc = InventoryScreen::getQualityColor(static_cast<game::ItemQuality>(info->quality));
|
|
|
|
|
|
ImGui::TextColored(qc, "%s", info->name.c_str());
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info, &gameHandler.getInventory());
|
|
|
|
|
|
}
|
|
|
|
|
|
// Shift-click: insert item link into chat
|
|
|
|
|
|
if (ImGui::IsItemClicked() && ImGui::GetIO().KeyShift) {
|
|
|
|
|
|
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
|
|
|
|
|
|
chatPanel_.insertChatLink(link);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::Text("Item %u", item.itemId);
|
|
|
|
|
|
}
|
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
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
|
|
|
|
|
if (item.buyPrice == 0 && item.extendedCost != 0) {
|
|
|
|
|
|
// Token-only item — show detailed cost from ItemExtendedCost.dbc
|
|
|
|
|
|
std::string costStr = formatExtendedCost(item.extendedCost, gameHandler);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "%s", costStr.c_str());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
uint32_t g = item.buyPrice / 10000;
|
|
|
|
|
|
uint32_t s = (item.buyPrice / 100) % 100;
|
|
|
|
|
|
uint32_t c = item.buyPrice % 100;
|
|
|
|
|
|
bool canAfford = money >= item.buyPrice;
|
|
|
|
|
|
if (canAfford) {
|
|
|
|
|
|
renderCoinsText(g, s, c);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(kColorRed, "%ug %us %uc", g, s, c);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Show additional token cost if both gold and tokens are required
|
|
|
|
|
|
if (item.extendedCost != 0) {
|
|
|
|
|
|
std::string costStr = formatExtendedCost(item.extendedCost, gameHandler);
|
|
|
|
|
|
if (costStr != "[Tokens]") {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 0.8f), "+ %s", costStr.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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 10:07:58 +03:00
|
|
|
|
ImGui::TableSetColumnIndex(3);
|
|
|
|
|
|
if (item.maxCount < 0) {
|
|
|
|
|
|
ImGui::TextDisabled("Inf");
|
|
|
|
|
|
} else if (item.maxCount == 0) {
|
|
|
|
|
|
ImGui::TextColored(kColorRed, "Out");
|
|
|
|
|
|
} else if (item.maxCount <= 5) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.1f, 1.0f), "%d", item.maxCount);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::Text("%d", item.maxCount);
|
|
|
|
|
|
}
|
2026-02-07 21:00:05 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::TableSetColumnIndex(4);
|
|
|
|
|
|
bool outOfStock = (item.maxCount == 0);
|
|
|
|
|
|
if (outOfStock) ImGui::BeginDisabled();
|
|
|
|
|
|
std::string buyBtnId = "Buy##vendor_" + std::to_string(vi);
|
|
|
|
|
|
if (ImGui::SmallButton(buyBtnId.c_str())) {
|
|
|
|
|
|
int qty = vendorBuyQty;
|
|
|
|
|
|
if (item.maxCount > 0 && qty > item.maxCount) qty = item.maxCount;
|
|
|
|
|
|
uint32_t totalCost = item.buyPrice * static_cast<uint32_t>(qty);
|
|
|
|
|
|
if (totalCost >= 10000) { // >= 1 gold: confirm
|
|
|
|
|
|
vendorConfirmOpen_ = true;
|
|
|
|
|
|
vendorConfirmGuid_ = vendor.vendorGuid;
|
|
|
|
|
|
vendorConfirmItemId_ = item.itemId;
|
|
|
|
|
|
vendorConfirmSlot_ = item.slot;
|
|
|
|
|
|
vendorConfirmQty_ = static_cast<uint32_t>(qty);
|
|
|
|
|
|
vendorConfirmPrice_ = totalCost;
|
|
|
|
|
|
vendorConfirmItemName_ = (info && info->valid) ? info->name : "Item";
|
|
|
|
|
|
} else {
|
|
|
|
|
|
gameHandler.buyItem(vendor.vendorGuid, item.itemId, item.slot,
|
|
|
|
|
|
static_cast<uint32_t>(qty));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (outOfStock) ImGui::EndDisabled();
|
2026-03-11 21:39:32 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PopID();
|
2026-03-11 21:39:32 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::EndTable();
|
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();
|
|
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
gameHandler.closeVendor();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Vendor purchase confirmation popup for expensive items
|
|
|
|
|
|
if (vendorConfirmOpen_) {
|
|
|
|
|
|
ImGui::OpenPopup("Confirm Purchase##vendor");
|
|
|
|
|
|
vendorConfirmOpen_ = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::BeginPopupModal("Confirm Purchase##vendor", nullptr,
|
|
|
|
|
|
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) {
|
|
|
|
|
|
ImGui::Text("Buy %s", vendorConfirmItemName_.c_str());
|
|
|
|
|
|
if (vendorConfirmQty_ > 1)
|
|
|
|
|
|
ImGui::Text("Quantity: %u", vendorConfirmQty_);
|
|
|
|
|
|
uint32_t g = vendorConfirmPrice_ / 10000;
|
|
|
|
|
|
uint32_t s = (vendorConfirmPrice_ / 100) % 100;
|
|
|
|
|
|
uint32_t c = vendorConfirmPrice_ % 100;
|
|
|
|
|
|
ImGui::Text("Cost: %ug %us %uc", g, s, c);
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
if (ImGui::Button("Buy", ImVec2(80, 0))) {
|
|
|
|
|
|
gameHandler.buyItem(vendorConfirmGuid_, vendorConfirmItemId_,
|
|
|
|
|
|
vendorConfirmSlot_, vendorConfirmQty_);
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(80, 0))) {
|
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndPopup();
|
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-06 11:59:51 -08:00
|
|
|
|
// ============================================================
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Trainer
|
2026-02-06 11:59:51 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
void GameScreen::renderTrainerWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isTrainerWindowOpen()) return;
|
2026-02-06 11:59:51 -08:00
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
2026-03-31 10:07:58 +03:00
|
|
|
|
auto* assetMgr = core::Application::getInstance().getAssetManager();
|
2026-02-06 11:59:51 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 225, 100), ImGuiCond_Appearing);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(500, 450), ImGuiCond_Appearing);
|
2026-02-06 11:59:51 -08:00
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (ImGui::Begin("Trainer", &open)) {
|
|
|
|
|
|
// If user clicked window close, short-circuit before rendering large trainer tables.
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
gameHandler.closeTrainer();
|
|
|
|
|
|
return;
|
2026-02-06 11:59:51 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
const auto& trainer = gameHandler.getTrainerSpells();
|
|
|
|
|
|
const bool isProfessionTrainer = (trainer.trainerType == 2);
|
2026-03-10 19:05:34 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// NPC name
|
|
|
|
|
|
auto npcEntity = gameHandler.getEntityManager().getEntity(trainer.trainerGuid);
|
|
|
|
|
|
if (npcEntity && npcEntity->getType() == game::ObjectType::UNIT) {
|
|
|
|
|
|
auto unit = std::static_pointer_cast<game::Unit>(npcEntity);
|
|
|
|
|
|
if (!unit->getName().empty()) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f), "%s", unit->getName().c_str());
|
2026-03-10 19:05:34 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Greeting
|
|
|
|
|
|
if (!trainer.greeting.empty()) {
|
|
|
|
|
|
ImGui::TextWrapped("%s", trainer.greeting.c_str());
|
2026-02-06 11:59:51 -08:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Separator();
|
2026-02-06 11:59:51 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Player money
|
|
|
|
|
|
uint64_t money = gameHandler.getMoneyCopper();
|
|
|
|
|
|
ImGui::TextDisabled("Your money:"); ImGui::SameLine(0, 4);
|
|
|
|
|
|
renderCoinsFromCopper(money);
|
2026-02-06 11:59:51 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Filter controls
|
|
|
|
|
|
static bool showUnavailable = false;
|
|
|
|
|
|
ImGui::Checkbox("Show unavailable spells", &showUnavailable);
|
2026-02-06 11:59:51 -08:00
|
|
|
|
ImGui::SameLine();
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::SetNextItemWidth(-1.0f);
|
|
|
|
|
|
ImGui::InputTextWithHint("##TrainerSearch", "Search...", trainerSearchFilter_, sizeof(trainerSearchFilter_));
|
|
|
|
|
|
ImGui::Separator();
|
2026-02-06 11:59:51 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (trainer.spells.empty()) {
|
|
|
|
|
|
ImGui::TextDisabled("This trainer has nothing to teach you.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Known spells for checking
|
|
|
|
|
|
const auto& knownSpells = gameHandler.getKnownSpells();
|
|
|
|
|
|
auto isKnown = [&](uint32_t id) {
|
|
|
|
|
|
if (id == 0) return true;
|
|
|
|
|
|
// Check if spell is in knownSpells list
|
|
|
|
|
|
bool found = knownSpells.count(id);
|
|
|
|
|
|
if (found) return true;
|
2026-02-06 21:50:15 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Also check if spell is in trainer list with state=2 (explicitly known)
|
|
|
|
|
|
// state=0 means unavailable (could be no prereqs, wrong level, etc.) - don't count as known
|
|
|
|
|
|
for (const auto& ts : trainer.spells) {
|
|
|
|
|
|
if (ts.spellId == id && ts.state == 2) {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
};
|
|
|
|
|
|
uint32_t playerLevel = gameHandler.getPlayerLevel();
|
2026-02-06 21:50:15 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Renders spell rows into the current table
|
|
|
|
|
|
auto renderSpellRows = [&](const std::vector<const game::TrainerSpell*>& spells) {
|
|
|
|
|
|
for (const auto* spell : spells) {
|
|
|
|
|
|
// Check prerequisites client-side first
|
|
|
|
|
|
bool prereq1Met = isKnown(spell->chainNode1);
|
|
|
|
|
|
bool prereq2Met = isKnown(spell->chainNode2);
|
|
|
|
|
|
bool prereq3Met = isKnown(spell->chainNode3);
|
|
|
|
|
|
bool prereqsMet = prereq1Met && prereq2Met && prereq3Met;
|
|
|
|
|
|
bool levelMet = (spell->reqLevel == 0 || playerLevel >= spell->reqLevel);
|
|
|
|
|
|
bool alreadyKnown = isKnown(spell->spellId);
|
2026-02-06 21:50:15 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Dynamically determine effective state based on current prerequisites
|
|
|
|
|
|
// Server sends state, but we override if prerequisites are now met
|
|
|
|
|
|
uint8_t effectiveState = spell->state;
|
|
|
|
|
|
if (spell->state == 1 && prereqsMet && levelMet) {
|
|
|
|
|
|
// Server said unavailable, but we now meet all requirements
|
|
|
|
|
|
effectiveState = 0; // Treat as available
|
|
|
|
|
|
}
|
2026-02-06 21:50:15 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Filter: skip unavailable spells if checkbox is unchecked
|
|
|
|
|
|
// Use effectiveState so spells with newly met prereqs aren't filtered
|
|
|
|
|
|
if (!showUnavailable && effectiveState == 1) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-02-19 00:56:24 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Apply text search filter
|
|
|
|
|
|
if (trainerSearchFilter_[0] != '\0') {
|
|
|
|
|
|
std::string trainerFilter(trainerSearchFilter_);
|
|
|
|
|
|
for (char& c : trainerFilter) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
const std::string& spellName = gameHandler.getSpellName(spell->spellId);
|
|
|
|
|
|
std::string nameLC = spellName.empty() ? std::to_string(spell->spellId) : spellName;
|
|
|
|
|
|
for (char& c : nameLC) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
if (nameLC.find(trainerFilter) == std::string::npos) {
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(spell->spellId));
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-06 21:50:15 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(spell->spellId));
|
2026-03-11 20:41:02 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImVec4 color;
|
|
|
|
|
|
const char* statusLabel;
|
|
|
|
|
|
// WotLK trainer states: 0=available, 1=unavailable, 2=known
|
|
|
|
|
|
if (effectiveState == 2 || alreadyKnown) {
|
|
|
|
|
|
color = colors::kQueueGreen;
|
|
|
|
|
|
statusLabel = "Known";
|
|
|
|
|
|
} else if (effectiveState == 0) {
|
|
|
|
|
|
color = ui::colors::kWhite;
|
|
|
|
|
|
statusLabel = "Available";
|
|
|
|
|
|
} else {
|
|
|
|
|
|
color = ImVec4(0.6f, 0.3f, 0.3f, 1.0f);
|
|
|
|
|
|
statusLabel = "Unavailable";
|
2026-03-11 20:41:02 -07:00
|
|
|
|
}
|
2026-02-06 21:50:15 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Icon column
|
|
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
|
|
|
|
|
{
|
|
|
|
|
|
VkDescriptorSet spellIcon = getSpellIcon(spell->spellId, assetMgr);
|
|
|
|
|
|
if (spellIcon) {
|
|
|
|
|
|
if (effectiveState == 1 && !alreadyKnown) {
|
|
|
|
|
|
ImGui::ImageWithBg((ImTextureID)(uintptr_t)spellIcon, ImVec2(18, 18),
|
|
|
|
|
|
ImVec2(0, 0), ImVec2(1, 1),
|
|
|
|
|
|
ImVec4(0, 0, 0, 0), ImVec4(0.5f, 0.5f, 0.5f, 0.6f));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::Image((ImTextureID)(uintptr_t)spellIcon, ImVec2(18, 18));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-10 19:25:26 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Spell name
|
|
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
|
|
|
|
const std::string& name = gameHandler.getSpellName(spell->spellId);
|
|
|
|
|
|
const std::string& rank = gameHandler.getSpellRank(spell->spellId);
|
|
|
|
|
|
if (!name.empty()) {
|
|
|
|
|
|
if (!rank.empty())
|
|
|
|
|
|
ImGui::TextColored(color, "%s (%s)", name.c_str(), rank.c_str());
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::TextColored(color, "%s", name.c_str());
|
2026-03-11 21:31:09 -07:00
|
|
|
|
} else {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::TextColored(color, "Spell #%u", spell->spellId);
|
2026-03-11 21:31:09 -07:00
|
|
|
|
}
|
2026-03-10 19:25:26 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
if (!name.empty()) {
|
|
|
|
|
|
ImGui::TextColored(kColorYellow, "%s", name.c_str());
|
|
|
|
|
|
if (!rank.empty()) ImGui::TextColored(kColorGray, "%s", rank.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
const std::string& spDesc = gameHandler.getSpellDescription(spell->spellId);
|
|
|
|
|
|
if (!spDesc.empty()) {
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 300.0f);
|
|
|
|
|
|
ImGui::TextWrapped("%s", spDesc.c_str());
|
|
|
|
|
|
ImGui::PopTextWrapPos();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::TextDisabled("Status: %s", statusLabel);
|
|
|
|
|
|
if (spell->reqLevel > 0) {
|
|
|
|
|
|
ImVec4 lvlColor = levelMet ? ui::colors::kLightGray : kColorRed;
|
|
|
|
|
|
ImGui::TextColored(lvlColor, "Required Level: %u", spell->reqLevel);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (spell->reqSkill > 0) ImGui::Text("Required Skill: %u (value %u)", spell->reqSkill, spell->reqSkillValue);
|
|
|
|
|
|
auto showPrereq = [&](uint32_t node) {
|
|
|
|
|
|
if (node == 0) return;
|
|
|
|
|
|
bool met = isKnown(node);
|
|
|
|
|
|
const std::string& pname = gameHandler.getSpellName(node);
|
|
|
|
|
|
ImVec4 pcolor = met ? colors::kQueueGreen : kColorRed;
|
|
|
|
|
|
if (!pname.empty())
|
|
|
|
|
|
ImGui::TextColored(pcolor, "Requires: %s%s", pname.c_str(), met ? " (known)" : "");
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::TextColored(pcolor, "Requires: Spell #%u%s", node, met ? " (known)" : "");
|
|
|
|
|
|
};
|
|
|
|
|
|
showPrereq(spell->chainNode1);
|
|
|
|
|
|
showPrereq(spell->chainNode2);
|
|
|
|
|
|
showPrereq(spell->chainNode3);
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
2026-03-10 19:25:26 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Level
|
|
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
|
|
|
|
|
ImGui::TextColored(color, "%u", spell->reqLevel);
|
2026-02-06 21:50:15 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Cost
|
|
|
|
|
|
ImGui::TableSetColumnIndex(3);
|
|
|
|
|
|
if (spell->spellCost > 0) {
|
|
|
|
|
|
uint32_t g = spell->spellCost / 10000;
|
|
|
|
|
|
uint32_t s = (spell->spellCost / 100) % 100;
|
|
|
|
|
|
uint32_t c = spell->spellCost % 100;
|
|
|
|
|
|
bool canAfford = money >= spell->spellCost;
|
|
|
|
|
|
if (canAfford) {
|
|
|
|
|
|
renderCoinsText(g, s, c);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(kColorRed, "%ug %us %uc", g, s, c);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(color, "Free");
|
|
|
|
|
|
}
|
2026-02-06 21:50:15 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Train button - only enabled if available, affordable, prereqs met
|
|
|
|
|
|
ImGui::TableSetColumnIndex(4);
|
|
|
|
|
|
// Use effectiveState so newly available spells (after learning prereqs) can be trained
|
|
|
|
|
|
bool canTrain = !alreadyKnown && effectiveState == 0
|
|
|
|
|
|
&& prereqsMet && levelMet
|
|
|
|
|
|
&& (money >= spell->spellCost);
|
2026-02-06 21:50:15 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Debug logging for first 3 spells to see why buttons are disabled
|
|
|
|
|
|
static int logCount = 0;
|
|
|
|
|
|
static uint64_t lastTrainerGuid = 0;
|
|
|
|
|
|
if (trainer.trainerGuid != lastTrainerGuid) {
|
|
|
|
|
|
logCount = 0;
|
|
|
|
|
|
lastTrainerGuid = trainer.trainerGuid;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (logCount < 3) {
|
|
|
|
|
|
LOG_INFO("Trainer button debug: spellId=", spell->spellId,
|
|
|
|
|
|
" alreadyKnown=", alreadyKnown, " state=", static_cast<int>(spell->state),
|
|
|
|
|
|
" prereqsMet=", prereqsMet, " (", prereq1Met, ",", prereq2Met, ",", prereq3Met, ")",
|
|
|
|
|
|
" levelMet=", levelMet,
|
|
|
|
|
|
" reqLevel=", spell->reqLevel, " playerLevel=", playerLevel,
|
|
|
|
|
|
" chain1=", spell->chainNode1, " chain2=", spell->chainNode2, " chain3=", spell->chainNode3,
|
|
|
|
|
|
" canAfford=", (money >= spell->spellCost),
|
|
|
|
|
|
" canTrain=", canTrain);
|
|
|
|
|
|
logCount++;
|
2026-03-11 23:08:35 -07:00
|
|
|
|
}
|
2026-02-07 23:12:24 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (isProfessionTrainer && alreadyKnown) {
|
|
|
|
|
|
// Profession trainer: known recipes show "Create" button to craft
|
|
|
|
|
|
bool isCasting = gameHandler.isCasting();
|
|
|
|
|
|
if (isCasting) ImGui::BeginDisabled();
|
|
|
|
|
|
if (ImGui::SmallButton("Create")) {
|
|
|
|
|
|
gameHandler.castSpell(spell->spellId, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isCasting) ImGui::EndDisabled();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (!canTrain) ImGui::BeginDisabled();
|
|
|
|
|
|
if (ImGui::SmallButton("Train")) {
|
|
|
|
|
|
gameHandler.trainSpell(spell->spellId);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!canTrain) ImGui::EndDisabled();
|
|
|
|
|
|
}
|
2026-02-07 23:12:24 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-02-07 23:12:24 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
auto renderSpellTable = [&](const char* tableId, const std::vector<const game::TrainerSpell*>& spells) {
|
|
|
|
|
|
if (ImGui::BeginTable(tableId, 5,
|
|
|
|
|
|
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) {
|
|
|
|
|
|
ImGui::TableSetupColumn("##icon", ImGuiTableColumnFlags_WidthFixed, 22.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Spell", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
|
|
ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 40.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Cost", ImGuiTableColumnFlags_WidthFixed, 120.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("##action", ImGuiTableColumnFlags_WidthFixed, 55.0f);
|
|
|
|
|
|
ImGui::TableHeadersRow();
|
|
|
|
|
|
renderSpellRows(spells);
|
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-02-07 23:12:24 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
const auto& tabs = gameHandler.getTrainerTabs();
|
|
|
|
|
|
if (tabs.size() > 1) {
|
|
|
|
|
|
// Multiple tabs - show tab bar
|
|
|
|
|
|
if (ImGui::BeginTabBar("TrainerTabs")) {
|
|
|
|
|
|
for (size_t i = 0; i < tabs.size(); i++) {
|
|
|
|
|
|
char tabLabel[64];
|
|
|
|
|
|
snprintf(tabLabel, sizeof(tabLabel), "%s (%zu)",
|
|
|
|
|
|
tabs[i].name.c_str(), tabs[i].spells.size());
|
2026-02-07 23:12:24 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (ImGui::BeginTabItem(tabLabel)) {
|
|
|
|
|
|
char tableId[32];
|
|
|
|
|
|
snprintf(tableId, sizeof(tableId), "TT%zu", i);
|
|
|
|
|
|
renderSpellTable(tableId, tabs[i].spells);
|
|
|
|
|
|
ImGui::EndTabItem();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTabBar();
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Single tab or no categorization - flat list
|
|
|
|
|
|
std::vector<const game::TrainerSpell*> allSpells;
|
|
|
|
|
|
allSpells.reserve(trainer.spells.size());
|
|
|
|
|
|
for (const auto& spell : trainer.spells) {
|
|
|
|
|
|
allSpells.push_back(&spell);
|
|
|
|
|
|
}
|
|
|
|
|
|
renderSpellTable("TrainerTable", allSpells);
|
|
|
|
|
|
}
|
2026-02-07 23:12:24 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Count how many spells are trainable right now
|
|
|
|
|
|
int trainableCount = 0;
|
|
|
|
|
|
uint64_t totalCost = 0;
|
|
|
|
|
|
for (const auto& spell : trainer.spells) {
|
|
|
|
|
|
bool prereq1Met = isKnown(spell.chainNode1);
|
|
|
|
|
|
bool prereq2Met = isKnown(spell.chainNode2);
|
|
|
|
|
|
bool prereq3Met = isKnown(spell.chainNode3);
|
|
|
|
|
|
bool prereqsMet = prereq1Met && prereq2Met && prereq3Met;
|
|
|
|
|
|
bool levelMet = (spell.reqLevel == 0 || playerLevel >= spell.reqLevel);
|
|
|
|
|
|
bool alreadyKnown = isKnown(spell.spellId);
|
|
|
|
|
|
uint8_t effectiveState = spell.state;
|
|
|
|
|
|
if (spell.state == 1 && prereqsMet && levelMet) effectiveState = 0;
|
|
|
|
|
|
bool canTrain = !alreadyKnown && effectiveState == 0
|
|
|
|
|
|
&& prereqsMet && levelMet
|
|
|
|
|
|
&& (money >= spell.spellCost);
|
|
|
|
|
|
if (canTrain) {
|
|
|
|
|
|
++trainableCount;
|
|
|
|
|
|
totalCost += spell.spellCost;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-07 23:12:24 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
bool canAffordAll = (money >= totalCost);
|
|
|
|
|
|
bool hasTrainable = (trainableCount > 0) && canAffordAll;
|
|
|
|
|
|
if (!hasTrainable) ImGui::BeginDisabled();
|
|
|
|
|
|
uint32_t tag = static_cast<uint32_t>(totalCost / 10000);
|
|
|
|
|
|
uint32_t tas = static_cast<uint32_t>((totalCost / 100) % 100);
|
|
|
|
|
|
uint32_t tac = static_cast<uint32_t>(totalCost % 100);
|
|
|
|
|
|
char trainAllLabel[80];
|
|
|
|
|
|
if (trainableCount == 0) {
|
|
|
|
|
|
snprintf(trainAllLabel, sizeof(trainAllLabel), "Train All Available (none)");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
snprintf(trainAllLabel, sizeof(trainAllLabel),
|
|
|
|
|
|
"Train All Available (%d spell%s, %ug %us %uc)",
|
|
|
|
|
|
trainableCount, trainableCount == 1 ? "" : "s",
|
|
|
|
|
|
tag, tas, tac);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::Button(trainAllLabel, ImVec2(-1.0f, 0.0f))) {
|
|
|
|
|
|
for (const auto& spell : trainer.spells) {
|
|
|
|
|
|
bool prereq1Met = isKnown(spell.chainNode1);
|
|
|
|
|
|
bool prereq2Met = isKnown(spell.chainNode2);
|
|
|
|
|
|
bool prereq3Met = isKnown(spell.chainNode3);
|
|
|
|
|
|
bool prereqsMet = prereq1Met && prereq2Met && prereq3Met;
|
|
|
|
|
|
bool levelMet = (spell.reqLevel == 0 || playerLevel >= spell.reqLevel);
|
|
|
|
|
|
bool alreadyKnown = isKnown(spell.spellId);
|
|
|
|
|
|
uint8_t effectiveState = spell.state;
|
|
|
|
|
|
if (spell.state == 1 && prereqsMet && levelMet) effectiveState = 0;
|
|
|
|
|
|
bool canTrain = !alreadyKnown && effectiveState == 0
|
|
|
|
|
|
&& prereqsMet && levelMet
|
|
|
|
|
|
&& (money >= spell.spellCost);
|
|
|
|
|
|
if (canTrain) {
|
|
|
|
|
|
gameHandler.trainSpell(spell.spellId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!hasTrainable) ImGui::EndDisabled();
|
2026-02-07 23:12:24 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Profession trainer: craft quantity controls
|
|
|
|
|
|
if (isProfessionTrainer) {
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
static int craftQuantity = 1;
|
|
|
|
|
|
static uint32_t selectedCraftSpell = 0;
|
2026-02-07 23:12:24 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Show craft queue status if active
|
|
|
|
|
|
int queueRemaining = gameHandler.getCraftQueueRemaining();
|
|
|
|
|
|
if (queueRemaining > 0) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.0f, 1.0f),
|
|
|
|
|
|
"Crafting... %d remaining", queueRemaining);
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::SmallButton("Stop")) {
|
|
|
|
|
|
gameHandler.cancelCraftQueue();
|
|
|
|
|
|
gameHandler.cancelCast();
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Spell selector + quantity input
|
|
|
|
|
|
// Build list of known (craftable) spells
|
|
|
|
|
|
std::vector<const game::TrainerSpell*> craftable;
|
|
|
|
|
|
for (const auto& spell : trainer.spells) {
|
|
|
|
|
|
if (isKnown(spell.spellId)) {
|
|
|
|
|
|
craftable.push_back(&spell);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!craftable.empty()) {
|
|
|
|
|
|
// Combo box for recipe selection
|
|
|
|
|
|
const char* previewName = "Select recipe...";
|
|
|
|
|
|
for (const auto* sp : craftable) {
|
|
|
|
|
|
if (sp->spellId == selectedCraftSpell) {
|
|
|
|
|
|
const std::string& n = gameHandler.getSpellName(sp->spellId);
|
|
|
|
|
|
if (!n.empty()) previewName = n.c_str();
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x * 0.55f);
|
|
|
|
|
|
if (ImGui::BeginCombo("##CraftSelect", previewName)) {
|
|
|
|
|
|
for (const auto* sp : craftable) {
|
|
|
|
|
|
const std::string& n = gameHandler.getSpellName(sp->spellId);
|
|
|
|
|
|
const std::string& r = gameHandler.getSpellRank(sp->spellId);
|
|
|
|
|
|
char label[128];
|
|
|
|
|
|
if (!r.empty())
|
|
|
|
|
|
snprintf(label, sizeof(label), "%s (%s)##%u",
|
|
|
|
|
|
n.empty() ? "???" : n.c_str(), r.c_str(), sp->spellId);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(label, sizeof(label), "%s##%u",
|
|
|
|
|
|
n.empty() ? "???" : n.c_str(), sp->spellId);
|
|
|
|
|
|
if (ImGui::Selectable(label, sp->spellId == selectedCraftSpell)) {
|
|
|
|
|
|
selectedCraftSpell = sp->spellId;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndCombo();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetNextItemWidth(50.0f);
|
|
|
|
|
|
ImGui::InputInt("##CraftQty", &craftQuantity, 0, 0);
|
|
|
|
|
|
if (craftQuantity < 1) craftQuantity = 1;
|
|
|
|
|
|
if (craftQuantity > 99) craftQuantity = 99;
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
bool canCraft = selectedCraftSpell != 0 && !gameHandler.isCasting();
|
|
|
|
|
|
if (!canCraft) ImGui::BeginDisabled();
|
|
|
|
|
|
if (ImGui::Button("Create")) {
|
|
|
|
|
|
if (craftQuantity == 1) {
|
|
|
|
|
|
gameHandler.castSpell(selectedCraftSpell, 0);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
gameHandler.startCraftQueue(selectedCraftSpell, craftQuantity);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Create All")) {
|
|
|
|
|
|
// Queue a large count — server stops the queue automatically
|
|
|
|
|
|
// when materials run out (sends SPELL_FAILED_REAGENTS).
|
|
|
|
|
|
gameHandler.startCraftQueue(selectedCraftSpell, 999);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!canCraft) ImGui::EndDisabled();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-07 23:12:24 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
2026-03-31 10:07:58 +03:00
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
gameHandler.closeTrainer();
|
|
|
|
|
|
}
|
2026-02-06 17:27:20 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 12:53:05 -07:00
|
|
|
|
// ============================================================
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Teleporter Panel
|
2026-03-10 12:53:05 -07:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Escape Menu
|
|
|
|
|
|
// ============================================================
|
2026-03-10 12:53:05 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
void GameScreen::renderEscapeMenu() {
|
|
|
|
|
|
if (!showEscapeMenu) return;
|
2026-03-10 12:53:05 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
|
|
float screenW = io.DisplaySize.x;
|
|
|
|
|
|
float screenH = io.DisplaySize.y;
|
|
|
|
|
|
ImVec2 size(260.0f, 248.0f);
|
|
|
|
|
|
ImVec2 pos((screenW - size.x) * 0.5f, (screenH - size.y) * 0.5f);
|
2026-03-10 12:53:05 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::SetNextWindowPos(pos, ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(size, ImGuiCond_Always);
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar;
|
2026-03-10 12:53:05 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (ImGui::Begin("##EscapeMenu", nullptr, flags)) {
|
|
|
|
|
|
ImGui::Text("Game Menu");
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Button("Logout", ImVec2(-1, 0))) {
|
|
|
|
|
|
core::Application::getInstance().logoutToLogin();
|
|
|
|
|
|
showEscapeMenu = false;
|
|
|
|
|
|
settingsPanel_.showEscapeSettingsNotice = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::Button("Quit", ImVec2(-1, 0))) {
|
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
if (renderer) {
|
|
|
|
|
|
if (auto* music = renderer->getMusicManager()) {
|
|
|
|
|
|
music->stopMusic(0.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
core::Application::getInstance().shutdown();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::Button("Settings", ImVec2(-1, 0))) {
|
|
|
|
|
|
settingsPanel_.showEscapeSettingsNotice = false;
|
|
|
|
|
|
settingsPanel_.showSettingsWindow = true;
|
|
|
|
|
|
settingsPanel_.settingsInit = false;
|
|
|
|
|
|
showEscapeMenu = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::Button("Instance Lockouts", ImVec2(-1, 0))) {
|
|
|
|
|
|
showInstanceLockouts_ = true;
|
|
|
|
|
|
showEscapeMenu = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::Button("Help / GM Ticket", ImVec2(-1, 0))) {
|
|
|
|
|
|
showGmTicketWindow_ = true;
|
|
|
|
|
|
showEscapeMenu = false;
|
|
|
|
|
|
}
|
2026-03-10 12:53:05 -07:00
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(10.0f, 10.0f));
|
|
|
|
|
|
if (ImGui::Button("Back to Game", ImVec2(-1, 0))) {
|
|
|
|
|
|
showEscapeMenu = false;
|
|
|
|
|
|
settingsPanel_.showEscapeSettingsNotice = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
2026-03-10 12:53:05 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Barber Shop Window
|
|
|
|
|
|
// ============================================================
|
2026-03-10 12:53:05 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
void GameScreen::renderBarberShopWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isBarberShopOpen()) {
|
|
|
|
|
|
barberInitialized_ = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-03-10 12:53:05 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
const auto* ch = gameHandler.getActiveCharacter();
|
|
|
|
|
|
if (!ch) return;
|
2026-03-10 12:53:05 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
uint8_t race = static_cast<uint8_t>(ch->race);
|
|
|
|
|
|
game::Gender gender = ch->gender;
|
|
|
|
|
|
game::Race raceEnum = ch->race;
|
2026-03-10 12:53:05 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Initialize sliders from current appearance
|
|
|
|
|
|
if (!barberInitialized_) {
|
|
|
|
|
|
barberOrigHairStyle_ = static_cast<int>((ch->appearanceBytes >> 16) & 0xFF);
|
|
|
|
|
|
barberOrigHairColor_ = static_cast<int>((ch->appearanceBytes >> 24) & 0xFF);
|
|
|
|
|
|
barberOrigFacialHair_ = static_cast<int>(ch->facialFeatures);
|
|
|
|
|
|
barberHairStyle_ = barberOrigHairStyle_;
|
|
|
|
|
|
barberHairColor_ = barberOrigHairColor_;
|
|
|
|
|
|
barberFacialHair_ = barberOrigFacialHair_;
|
|
|
|
|
|
barberInitialized_ = true;
|
2026-03-10 12:53:05 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
int maxHairStyle = static_cast<int>(game::getMaxHairStyle(raceEnum, gender));
|
|
|
|
|
|
int maxHairColor = static_cast<int>(game::getMaxHairColor(raceEnum, gender));
|
|
|
|
|
|
int maxFacialHair = static_cast<int>(game::getMaxFacialFeature(raceEnum, gender));
|
2026-03-17 21:13:27 -07:00
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
2026-03-31 10:07:58 +03:00
|
|
|
|
float winW = 300.0f;
|
|
|
|
|
|
float winH = 220.0f;
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2((screenW - winW) / 2.0f, (screenH - winH) / 2.0f), ImGuiCond_Appearing);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(winW, winH), ImGuiCond_Appearing);
|
2026-03-17 21:13:27 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
|
if (ImGui::Begin("Barber Shop", &open, flags)) {
|
|
|
|
|
|
ImGui::Text("Choose your new look:");
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Spacing();
|
2026-03-17 21:13:27 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PushItemWidth(-1);
|
2026-03-17 21:13:27 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Hair Style
|
|
|
|
|
|
ImGui::Text("Hair Style");
|
|
|
|
|
|
ImGui::SliderInt("##HairStyle", &barberHairStyle_, 0, maxHairStyle,
|
|
|
|
|
|
"%d");
|
2026-03-17 21:13:27 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Hair Color
|
|
|
|
|
|
ImGui::Text("Hair Color");
|
|
|
|
|
|
ImGui::SliderInt("##HairColor", &barberHairColor_, 0, maxHairColor,
|
|
|
|
|
|
"%d");
|
2026-03-17 21:13:27 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Facial Hair / Piercings / Markings
|
|
|
|
|
|
const char* facialLabel = (gender == game::Gender::FEMALE) ? "Piercings" : "Facial Hair";
|
|
|
|
|
|
// Some races use "Markings" or "Tusks" etc.
|
|
|
|
|
|
if (race == 8 || race == 6) facialLabel = "Features"; // Trolls, Tauren
|
|
|
|
|
|
ImGui::Text("%s", facialLabel);
|
|
|
|
|
|
ImGui::SliderInt("##FacialHair", &barberFacialHair_, 0, maxFacialHair,
|
|
|
|
|
|
"%d");
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopItemWidth();
|
2026-03-17 21:13:27 -07:00
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
// Show whether anything changed
|
|
|
|
|
|
bool changed = (barberHairStyle_ != barberOrigHairStyle_ ||
|
|
|
|
|
|
barberHairColor_ != barberOrigHairColor_ ||
|
|
|
|
|
|
barberFacialHair_ != barberOrigFacialHair_);
|
2026-03-17 21:13:27 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// OK / Reset / Cancel buttons
|
|
|
|
|
|
float btnW = 80.0f;
|
|
|
|
|
|
float totalW = btnW * 3 + ImGui::GetStyle().ItemSpacing.x * 2;
|
|
|
|
|
|
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - totalW) / 2.0f);
|
2026-03-17 21:13:27 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (!changed) ImGui::BeginDisabled();
|
|
|
|
|
|
if (ImGui::Button("OK", ImVec2(btnW, 0))) {
|
|
|
|
|
|
gameHandler.sendAlterAppearance(
|
|
|
|
|
|
static_cast<uint32_t>(barberHairStyle_),
|
|
|
|
|
|
static_cast<uint32_t>(barberHairColor_),
|
|
|
|
|
|
static_cast<uint32_t>(barberFacialHair_));
|
|
|
|
|
|
// Keep window open — server will respond with SMSG_BARBER_SHOP_RESULT
|
2026-03-17 21:13:27 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (!changed) ImGui::EndDisabled();
|
2026-03-17 21:13:27 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (!changed) ImGui::BeginDisabled();
|
|
|
|
|
|
if (ImGui::Button("Reset", ImVec2(btnW, 0))) {
|
|
|
|
|
|
barberHairStyle_ = barberOrigHairStyle_;
|
|
|
|
|
|
barberHairColor_ = barberOrigHairColor_;
|
|
|
|
|
|
barberFacialHair_ = barberOrigFacialHair_;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!changed) ImGui::EndDisabled();
|
2026-03-17 21:13:27 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(btnW, 0))) {
|
|
|
|
|
|
gameHandler.closeBarberShop();
|
2026-03-17 21:13:27 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
2026-03-31 10:07:58 +03:00
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
gameHandler.closeBarberShop();
|
|
|
|
|
|
}
|
2026-03-17 21:13:27 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-05 16:11:00 -08:00
|
|
|
|
// ============================================================
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Pet Stable Window
|
2026-02-05 16:11:00 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
void GameScreen::renderStableWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isStableWindowOpen()) return;
|
2026-03-25 16:07:04 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
2026-03-25 16:07:04 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2.0f - 240.0f, screenH / 2.0f - 180.0f),
|
|
|
|
|
|
ImGuiCond_Once);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(480.0f, 360.0f), ImGuiCond_Once);
|
2026-03-25 16:07:04 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
bool open = true;
|
|
|
|
|
|
if (!ImGui::Begin("Pet Stable", &open,
|
|
|
|
|
|
kDialogFlags)) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
// User closed the window; clear stable state
|
|
|
|
|
|
gameHandler.closeStableWindow();
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
2026-03-25 16:07:04 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
const auto& pets = gameHandler.getStabledPets();
|
|
|
|
|
|
uint8_t numSlots = gameHandler.getStableSlots();
|
2026-03-25 16:07:04 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::TextDisabled("Stable slots: %u", static_cast<unsigned>(numSlots));
|
|
|
|
|
|
ImGui::Separator();
|
2026-03-25 16:07:04 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Active pets section
|
|
|
|
|
|
bool hasActivePets = false;
|
|
|
|
|
|
for (const auto& p : pets) {
|
|
|
|
|
|
if (p.isActive) { hasActivePets = true; break; }
|
|
|
|
|
|
}
|
2026-03-25 16:07:04 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (hasActivePets) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 0.9f, 0.4f, 1.0f), "Active / Summoned");
|
|
|
|
|
|
for (const auto& p : pets) {
|
|
|
|
|
|
if (!p.isActive) continue;
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(p.petNumber) * -1 - 1);
|
2026-03-25 15:54:14 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
const std::string displayName = p.name.empty()
|
|
|
|
|
|
? ("Pet #" + std::to_string(p.petNumber))
|
|
|
|
|
|
: p.name;
|
|
|
|
|
|
ImGui::Text(" %s (Level %u)", displayName.c_str(), p.level);
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextDisabled("[Active]");
|
2026-03-25 15:54:14 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Offer to stable the active pet if there are free slots
|
|
|
|
|
|
uint8_t usedSlots = 0;
|
|
|
|
|
|
for (const auto& sp : pets) { if (!sp.isActive) ++usedSlots; }
|
|
|
|
|
|
if (usedSlots < numSlots) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::SmallButton("Store in stable")) {
|
|
|
|
|
|
// Slot 1 is first stable slot; server handles free slot assignment.
|
|
|
|
|
|
gameHandler.stablePet(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopID();
|
2026-03-25 15:54:14 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Separator();
|
2026-03-25 15:54:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Stabled pets section
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.9f, 0.8f, 0.4f, 1.0f), "Stabled Pets");
|
2026-03-25 15:54:14 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
bool hasStabledPets = false;
|
|
|
|
|
|
for (const auto& p : pets) {
|
|
|
|
|
|
if (!p.isActive) { hasStabledPets = true; break; }
|
2026-03-25 15:54:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (!hasStabledPets) {
|
|
|
|
|
|
ImGui::TextDisabled(" (No pets in stable)");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
for (const auto& p : pets) {
|
|
|
|
|
|
if (p.isActive) continue;
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(p.petNumber));
|
2026-03-25 15:54:14 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
const std::string displayName = p.name.empty()
|
|
|
|
|
|
? ("Pet #" + std::to_string(p.petNumber))
|
|
|
|
|
|
: p.name;
|
|
|
|
|
|
ImGui::Text(" %s (Level %u, Entry %u)",
|
|
|
|
|
|
displayName.c_str(), p.level, p.entry);
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::SmallButton("Retrieve")) {
|
|
|
|
|
|
gameHandler.unstablePet(p.petNumber);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopID();
|
2026-03-25 15:54:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Empty slots
|
|
|
|
|
|
uint8_t usedStableSlots = 0;
|
|
|
|
|
|
for (const auto& p : pets) { if (!p.isActive) ++usedStableSlots; }
|
|
|
|
|
|
if (usedStableSlots < numSlots) {
|
|
|
|
|
|
ImGui::TextDisabled(" %u empty slot(s) available",
|
|
|
|
|
|
static_cast<unsigned>(numSlots - usedStableSlots));
|
2026-03-25 15:54:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Separator();
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (ImGui::Button("Refresh")) {
|
|
|
|
|
|
gameHandler.requestStabledPetList();
|
2026-03-25 15:54:14 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Close")) {
|
|
|
|
|
|
gameHandler.closeStableWindow();
|
2026-03-25 15:54:14 -07:00
|
|
|
|
}
|
2026-03-25 15:49:38 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::End();
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
gameHandler.closeStableWindow();
|
2026-03-25 15:49:38 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Taxi Window
|
|
|
|
|
|
// ============================================================
|
2026-03-25 15:49:38 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
void GameScreen::renderTaxiWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isTaxiWindowOpen()) return;
|
2026-03-25 15:49:38 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
2026-03-25 15:49:38 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 200, 150), ImGuiCond_Appearing);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(400, 0), ImGuiCond_Always);
|
2026-03-25 15:49:38 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
bool open = true;
|
|
|
|
|
|
if (ImGui::Begin("Flight Master", &open, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
|
const auto& taxiData = gameHandler.getTaxiData();
|
|
|
|
|
|
const auto& nodes = gameHandler.getTaxiNodes();
|
|
|
|
|
|
uint32_t currentNode = gameHandler.getTaxiCurrentNode();
|
2026-03-25 15:49:38 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Get current node's map to filter destinations
|
|
|
|
|
|
uint32_t currentMapId = 0;
|
|
|
|
|
|
auto curIt = nodes.find(currentNode);
|
|
|
|
|
|
if (curIt != nodes.end()) {
|
|
|
|
|
|
currentMapId = curIt->second.mapId;
|
|
|
|
|
|
ImGui::TextColored(colors::kActiveGreen, "Current: %s", curIt->second.name.c_str());
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
}
|
2026-03-25 15:49:38 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Text("Select a destination:");
|
|
|
|
|
|
ImGui::Spacing();
|
2026-03-25 15:49:38 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
static uint32_t selectedNodeId = 0;
|
|
|
|
|
|
int destCount = 0;
|
|
|
|
|
|
if (ImGui::BeginTable("TaxiNodes", 3, ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_RowBg)) {
|
|
|
|
|
|
ImGui::TableSetupColumn("Destination", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
|
|
ImGui::TableSetupColumn("Cost", ImGuiTableColumnFlags_WidthFixed, 120.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Action", ImGuiTableColumnFlags_WidthFixed, 60.0f);
|
|
|
|
|
|
ImGui::TableHeadersRow();
|
2026-03-25 15:49:38 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
for (const auto& [nodeId, node] : nodes) {
|
|
|
|
|
|
if (nodeId == currentNode) continue;
|
|
|
|
|
|
if (node.mapId != currentMapId) continue;
|
|
|
|
|
|
if (!taxiData.isNodeKnown(nodeId)) continue;
|
2026-03-25 15:49:38 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
uint32_t costCopper = gameHandler.getTaxiCostTo(nodeId);
|
|
|
|
|
|
uint32_t gold = costCopper / 10000;
|
|
|
|
|
|
uint32_t silver = (costCopper / 100) % 100;
|
|
|
|
|
|
uint32_t copper = costCopper % 100;
|
2026-03-25 15:49:38 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PushID(static_cast<int>(nodeId));
|
|
|
|
|
|
ImGui::TableNextRow();
|
2026-03-25 15:49:38 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
|
|
|
|
|
bool isSelected = (selectedNodeId == nodeId);
|
|
|
|
|
|
if (ImGui::Selectable(node.name.c_str(), isSelected,
|
|
|
|
|
|
ImGuiSelectableFlags_SpanAllColumns |
|
|
|
|
|
|
ImGuiSelectableFlags_AllowDoubleClick)) {
|
|
|
|
|
|
selectedNodeId = nodeId;
|
|
|
|
|
|
LOG_INFO("Taxi UI: Selected dest=", nodeId);
|
|
|
|
|
|
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
|
|
|
|
|
|
LOG_INFO("Taxi UI: Double-click activate dest=", nodeId);
|
|
|
|
|
|
gameHandler.activateTaxi(nodeId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-25 15:49:38 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
|
|
|
|
renderCoinsText(gold, silver, copper);
|
2026-03-25 15:49:38 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
|
|
|
|
|
if (ImGui::SmallButton("Fly")) {
|
|
|
|
|
|
selectedNodeId = nodeId;
|
|
|
|
|
|
LOG_INFO("Taxi UI: Fly clicked dest=", nodeId);
|
|
|
|
|
|
gameHandler.activateTaxi(nodeId);
|
|
|
|
|
|
}
|
2026-02-05 16:11:00 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
destCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
|
|
}
|
2026-02-05 16:11:00 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (destCount == 0) {
|
|
|
|
|
|
ImGui::TextColored(ui::colors::kLightGray, "No destinations available.");
|
2026-02-05 17:40:15 -08:00
|
|
|
|
}
|
2026-02-05 16:11:00 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (selectedNodeId != 0 && ImGui::Button("Fly Selected", ImVec2(-1, 0))) {
|
|
|
|
|
|
LOG_INFO("Taxi UI: Fly Selected dest=", selectedNodeId);
|
|
|
|
|
|
gameHandler.activateTaxi(selectedNodeId);
|
2026-02-05 16:11:00 -08:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (ImGui::Button("Close", ImVec2(-1, 0))) {
|
|
|
|
|
|
gameHandler.closeTaxi();
|
2026-02-07 20:51:53 -08:00
|
|
|
|
}
|
2026-02-05 16:11:00 -08:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::End();
|
2026-02-05 16:11:00 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (!open) {
|
|
|
|
|
|
gameHandler.closeTaxi();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-11 15:21:48 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Logout Countdown
|
|
|
|
|
|
// ============================================================
|
2026-03-11 15:21:48 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
void GameScreen::renderLogoutCountdown(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isLoggingOut()) return;
|
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
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
2026-02-05 16:11:00 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
constexpr float W = 280.0f;
|
|
|
|
|
|
constexpr float H = 80.0f;
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2((screenW - W) * 0.5f, screenH * 0.5f - H * 0.5f - 60.0f),
|
|
|
|
|
|
ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(W, H), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowBgAlpha(0.88f);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.08f, 0.18f, 0.95f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.5f, 0.8f, 1.0f));
|
2026-02-05 16:11:00 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (ImGui::Begin("##LogoutCountdown", nullptr,
|
|
|
|
|
|
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoBringToFrontOnFocus)) {
|
2026-03-17 09:04:53 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
float cd = gameHandler.getLogoutCountdown();
|
|
|
|
|
|
if (cd > 0.0f) {
|
|
|
|
|
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 6.0f);
|
|
|
|
|
|
ImGui::SetCursorPosX((W - ImGui::CalcTextSize("Logging out in 20s...").x) * 0.5f);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.3f, 1.0f),
|
|
|
|
|
|
"Logging out in %ds...", static_cast<int>(std::ceil(cd)));
|
2026-03-17 09:04:53 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Progress bar (20 second countdown)
|
|
|
|
|
|
float frac = 1.0f - std::min(cd / 20.0f, 1.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.5f, 0.5f, 0.9f, 1.0f));
|
|
|
|
|
|
ImGui::ProgressBar(frac, ImVec2(-1.0f, 8.0f), "");
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 14.0f);
|
|
|
|
|
|
ImGui::SetCursorPosX((W - ImGui::CalcTextSize("Logging out...").x) * 0.5f);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.3f, 1.0f), "Logging out...");
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
}
|
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
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Cancel button — only while countdown is still running
|
|
|
|
|
|
if (cd > 0.0f) {
|
|
|
|
|
|
float btnW = 100.0f;
|
|
|
|
|
|
ImGui::SetCursorPosX((W - btnW) * 0.5f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.1f, 0.1f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.15f, 0.15f, 1.0f));
|
|
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(btnW, 0))) {
|
|
|
|
|
|
gameHandler.cancelLogout();
|
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
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
}
|
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
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Death Screen
|
|
|
|
|
|
// ============================================================
|
2026-03-10 15:45:35 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
void GameScreen::renderDeathScreen(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.showDeathDialog()) {
|
|
|
|
|
|
deathTimerRunning_ = false;
|
|
|
|
|
|
deathElapsed_ = 0.0f;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
float dt = ImGui::GetIO().DeltaTime;
|
|
|
|
|
|
if (!deathTimerRunning_) {
|
|
|
|
|
|
deathElapsed_ = 0.0f;
|
|
|
|
|
|
deathTimerRunning_ = true;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
deathElapsed_ += dt;
|
|
|
|
|
|
}
|
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
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
2026-02-12 22:56:36 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Dark red overlay covering the whole screen
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(0, 0));
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(screenW, screenH));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.15f, 0.0f, 0.0f, 0.45f));
|
|
|
|
|
|
ImGui::Begin("##DeathOverlay", nullptr,
|
|
|
|
|
|
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoInputs |
|
|
|
|
|
|
ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoFocusOnAppearing);
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleColor();
|
2026-03-11 06:51:48 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// "Release Spirit" dialog centered on screen
|
|
|
|
|
|
const bool hasSelfRes = gameHandler.canSelfRes();
|
|
|
|
|
|
float dlgW = 280.0f;
|
|
|
|
|
|
// Extra height when self-res button is available; +20 for the "wait for res" hint
|
|
|
|
|
|
float dlgH = hasSelfRes ? 190.0f : 150.0f;
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - dlgW / 2, screenH * 0.35f), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(dlgW, dlgH), ImGuiCond_Always);
|
2026-02-14 18:27:59 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 8.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.1f, 0.0f, 0.0f, 0.9f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.6f, 0.1f, 0.1f, 1.0f));
|
2026-02-23 07:51:10 -08:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (ImGui::Begin("##DeathDialog", nullptr,
|
|
|
|
|
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar)) {
|
2026-02-06 20:19:39 -08:00
|
|
|
|
|
2026-02-05 16:16:03 -08:00
|
|
|
|
ImGui::Spacing();
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Center "You are dead." text
|
|
|
|
|
|
const char* deathText = "You are dead.";
|
|
|
|
|
|
float textW = ImGui::CalcTextSize(deathText).x;
|
|
|
|
|
|
ImGui::SetCursorPosX((dlgW - textW) / 2);
|
|
|
|
|
|
ImGui::TextColored(colors::kBrightRed, "%s", deathText);
|
|
|
|
|
|
|
|
|
|
|
|
// Respawn timer: show how long until the server auto-releases the spirit
|
|
|
|
|
|
float timeLeft = kForcedReleaseSec - deathElapsed_;
|
|
|
|
|
|
if (timeLeft > 0.0f) {
|
|
|
|
|
|
int mins = static_cast<int>(timeLeft) / 60;
|
|
|
|
|
|
int secs = static_cast<int>(timeLeft) % 60;
|
|
|
|
|
|
char timerBuf[48];
|
|
|
|
|
|
snprintf(timerBuf, sizeof(timerBuf), "Auto-release in %d:%02d", mins, secs);
|
|
|
|
|
|
float tw = ImGui::CalcTextSize(timerBuf).x;
|
|
|
|
|
|
ImGui::SetCursorPosX((dlgW - tw) / 2);
|
|
|
|
|
|
ImGui::TextColored(colors::kMediumGray, "%s", timerBuf);
|
2026-02-05 16:11:00 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Spacing();
|
2026-03-11 15:21:48 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Self-resurrection button (Reincarnation / Twisting Nether / Deathpact)
|
|
|
|
|
|
if (hasSelfRes) {
|
|
|
|
|
|
float btnW2 = 220.0f;
|
|
|
|
|
|
ImGui::SetCursorPosX((dlgW - btnW2) / 2);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.35f, 0.55f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.2f, 0.5f, 0.75f, 1.0f));
|
|
|
|
|
|
if (ImGui::Button("Use Self-Resurrection", ImVec2(btnW2, 30))) {
|
|
|
|
|
|
gameHandler.useSelfRes();
|
2026-03-11 15:21:48 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
ImGui::Spacing();
|
2026-03-11 15:21:48 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
|
|
|
|
|
|
// Center the Release Spirit button
|
|
|
|
|
|
float btnW = 180.0f;
|
|
|
|
|
|
ImGui::SetCursorPosX((dlgW - btnW) / 2);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.5f, 0.1f, 0.1f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.7f, 0.15f, 0.15f, 1.0f));
|
|
|
|
|
|
if (ImGui::Button("Release Spirit", ImVec2(btnW, 30))) {
|
|
|
|
|
|
gameHandler.releaseSpirit();
|
2026-03-11 15:21:48 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::PopStyleColor(2);
|
2026-03-11 15:21:48 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
// Hint: player can stay dead and wait for another player to cast Resurrection
|
|
|
|
|
|
const char* resHint = "Or wait for a player to resurrect you.";
|
|
|
|
|
|
float hw = ImGui::CalcTextSize(resHint).x;
|
|
|
|
|
|
ImGui::SetCursorPosX((dlgW - hw) / 2);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.5f, 0.6f, 0.5f, 0.85f), "%s", resHint);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
ImGui::PopStyleVar();
|
2026-03-11 15:21:48 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
void GameScreen::renderReclaimCorpseButton(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isPlayerGhost() || !gameHandler.canReclaimCorpse()) return;
|
2026-03-11 15:21:48 -07:00
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
|
|
|
|
|
|
|
|
|
|
|
float delaySec = gameHandler.getCorpseReclaimDelaySec();
|
|
|
|
|
|
bool onDelay = (delaySec > 0.0f);
|
|
|
|
|
|
|
|
|
|
|
|
float btnW = 220.0f, btnH = 36.0f;
|
|
|
|
|
|
float winH = btnH + 16.0f + (onDelay ? 20.0f : 0.0f);
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - btnW / 2, screenH * 0.72f), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(btnW + 16.0f, winH), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(8.0f, 8.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.0f, 0.0f, 0.0f, 0.7f));
|
|
|
|
|
|
if (ImGui::Begin("##ReclaimCorpse", nullptr,
|
|
|
|
|
|
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoBringToFrontOnFocus)) {
|
|
|
|
|
|
if (onDelay) {
|
|
|
|
|
|
// Greyed-out button while PvP reclaim timer ticks down
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.25f, 0.25f, 0.25f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.25f, 0.25f, 1.0f));
|
|
|
|
|
|
ImGui::BeginDisabled(true);
|
|
|
|
|
|
char delayLabel[64];
|
|
|
|
|
|
snprintf(delayLabel, sizeof(delayLabel), "Resurrect from Corpse (%.0fs)", delaySec);
|
|
|
|
|
|
ImGui::Button(delayLabel, ImVec2(btnW, btnH));
|
|
|
|
|
|
ImGui::EndDisabled();
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
const char* waitMsg = "You cannot reclaim your corpse yet.";
|
|
|
|
|
|
float tw = ImGui::CalcTextSize(waitMsg).x;
|
|
|
|
|
|
ImGui::SetCursorPosX((btnW + 16.0f - tw) * 0.5f);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.8f, 0.5f, 0.2f, 1.0f), "%s", waitMsg);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.35f, 0.15f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.25f, 0.55f, 0.25f, 1.0f));
|
|
|
|
|
|
if (ImGui::Button("Resurrect from Corpse", ImVec2(btnW, btnH))) {
|
|
|
|
|
|
gameHandler.reclaimCorpse();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
float corpDist = gameHandler.getCorpseDistance();
|
|
|
|
|
|
if (corpDist >= 0.0f) {
|
|
|
|
|
|
char distBuf[48];
|
|
|
|
|
|
snprintf(distBuf, sizeof(distBuf), "Corpse: %.0f yards away", corpDist);
|
|
|
|
|
|
float dw = ImGui::CalcTextSize(distBuf).x;
|
|
|
|
|
|
ImGui::SetCursorPosX((btnW + 16.0f - dw) * 0.5f);
|
|
|
|
|
|
ImGui::TextDisabled("%s", distBuf);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-11 15:21:48 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::PopStyleVar(2);
|
2026-03-11 15:21:48 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 20:10:10 -08:00
|
|
|
|
void GameScreen::renderQuestMarkers(game::GameHandler& gameHandler) {
|
|
|
|
|
|
const auto& statuses = gameHandler.getNpcQuestStatuses();
|
|
|
|
|
|
if (statuses.empty()) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
if (!camera || !window) return;
|
|
|
|
|
|
|
|
|
|
|
|
float screenW = static_cast<float>(window->getWidth());
|
|
|
|
|
|
float screenH = static_cast<float>(window->getHeight());
|
|
|
|
|
|
glm::mat4 viewProj = camera->getViewProjectionMatrix();
|
|
|
|
|
|
auto* drawList = ImGui::GetForegroundDrawList();
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-02-19 02:04:56 -08:00
|
|
|
|
} else if (status == game::QuestGiverStatus::REWARD ||
|
|
|
|
|
|
status == game::QuestGiverStatus::REWARD_REP) {
|
2026-02-06 20:10:10 -08:00
|
|
|
|
marker = "?";
|
|
|
|
|
|
} else if (status == game::QuestGiverStatus::INCOMPLETE) {
|
|
|
|
|
|
marker = "?";
|
|
|
|
|
|
color = IM_COL32(160, 160, 160, 255); // gray
|
|
|
|
|
|
} else {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get entity position (canonical coords)
|
|
|
|
|
|
auto entity = gameHandler.getEntityManager().getEntity(guid);
|
|
|
|
|
|
if (!entity) continue;
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
|
|
|
|
|
|
glm::vec3 renderPos = core::coords::canonicalToRender(canonical);
|
|
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
renderPos.z += heightOffset;
|
|
|
|
|
|
|
|
|
|
|
|
// Project to screen
|
|
|
|
|
|
glm::vec4 clipPos = viewProj * glm::vec4(renderPos, 1.0f);
|
|
|
|
|
|
if (clipPos.w <= 0.0f) continue;
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
// Skip if off-screen
|
|
|
|
|
|
if (sx < -50 || sx > screenW + 50 || sy < -50 || sy > screenH + 50) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Scale text size based on distance
|
|
|
|
|
|
float dist = clipPos.w;
|
|
|
|
|
|
float fontSize = std::clamp(800.0f / dist, 14.0f, 48.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderMinimapMarkers(game::GameHandler& gameHandler) {
|
|
|
|
|
|
const auto& statuses = gameHandler.getNpcQuestStatuses();
|
|
|
|
|
|
auto* renderer = core::Application::getInstance().getRenderer();
|
|
|
|
|
|
auto* camera = renderer ? renderer->getCamera() : nullptr;
|
|
|
|
|
|
auto* minimap = renderer ? renderer->getMinimap() : nullptr;
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
if (!camera || !minimap || !window) return;
|
|
|
|
|
|
|
|
|
|
|
|
float screenW = static_cast<float>(window->getWidth());
|
|
|
|
|
|
|
|
|
|
|
|
// 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;
|
2026-02-20 16:27:21 -08:00
|
|
|
|
float viewRadius = minimap->getViewRadius();
|
2026-02-06 20:10:10 -08:00
|
|
|
|
|
2026-02-20 16:27:21 -08: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();
|
|
|
|
|
|
}
|
2026-02-06 20:10:10 -08:00
|
|
|
|
|
|
|
|
|
|
// Camera bearing for minimap rotation
|
2026-02-07 20:51:53 -08:00
|
|
|
|
float bearing = 0.0f;
|
|
|
|
|
|
float cosB = 1.0f;
|
|
|
|
|
|
float sinB = 0.0f;
|
2026-02-11 17:30:57 -08:00
|
|
|
|
if (minimap->isRotateWithCamera()) {
|
2026-02-07 20:51:53 -08:00
|
|
|
|
glm::vec3 fwd = camera->getForward();
|
2026-03-13 03:19:05 -07:00
|
|
|
|
// 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);
|
2026-02-07 20:51:53 -08:00
|
|
|
|
cosB = std::cos(bearing);
|
|
|
|
|
|
sinB = std::sin(bearing);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-06 20:10:10 -08:00
|
|
|
|
auto* drawList = ImGui::GetForegroundDrawList();
|
|
|
|
|
|
|
2026-02-20 16:40:22 -08: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-11 01:29:56 -07: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-02-20 16:40:22 -08:00
|
|
|
|
|
|
|
|
|
|
// Scale to minimap pixels
|
|
|
|
|
|
float px = rx / viewRadius * mapRadius;
|
|
|
|
|
|
float py = ry / viewRadius * mapRadius;
|
|
|
|
|
|
|
|
|
|
|
|
float distFromCenter = std::sqrt(px * px + py * py);
|
|
|
|
|
|
if (distFromCenter > mapRadius - 3.0f) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sx = centerX + px;
|
|
|
|
|
|
sy = centerY + py;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-03-12 16:05:34 -07: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)
|
2026-03-12 15:59:30 -07:00
|
|
|
|
std::unordered_set<uint32_t> minimapQuestEntries;
|
2026-03-12 16:05:34 -07:00
|
|
|
|
std::unordered_set<uint32_t> minimapQuestGoEntries;
|
2026-03-12 15:59:30 -07:00
|
|
|
|
{
|
|
|
|
|
|
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) {
|
2026-03-12 16:05:34 -07:00
|
|
|
|
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 15:59:30 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-20 16:40:22 -08:00
|
|
|
|
// Optional base nearby NPC dots (independent of quest status packets).
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (settingsPanel_.minimapNpcDots_) {
|
2026-03-12 15:59:30 -07:00
|
|
|
|
ImVec2 mouse = ImGui::GetMousePos();
|
2026-02-20 16:40:22 -08:00
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
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-12 15:59:30 -07: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());
|
|
|
|
|
|
}
|
|
|
|
|
|
} 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-02-20 16:40:22 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 15:20:31 -07: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.
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (settingsPanel_.minimapNpcDots_) {
|
2026-03-12 15:20:31 -07:00
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
// Blue dot for other nearby players
|
|
|
|
|
|
drawList->AddCircleFilled(ImVec2(sx, sy), 2.0f, IM_COL32(80, 160, 255, 220));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 11:04:10 -07: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;
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
// 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-12 15:28:31 -07: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.
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (settingsPanel_.minimapNpcDots_) {
|
2026-03-12 15:28:31 -07:00
|
|
|
|
ImVec2 mouse = ImGui::GetMousePos();
|
|
|
|
|
|
for (const auto& [guid, entity] : gameHandler.getEntityManager().getEntities()) {
|
|
|
|
|
|
if (!entity || entity->getType() != game::ObjectType::GAMEOBJECT) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
|
|
|
|
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-12 16:05:34 -07: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;
|
2026-03-12 15:28:31 -07:00
|
|
|
|
ImVec2 goTip (sx, sy - ts);
|
|
|
|
|
|
ImVec2 goLeft (sx - ts, sy + ts * 0.6f);
|
|
|
|
|
|
ImVec2 goRight(sx + ts, sy + ts * 0.6f);
|
2026-03-12 16:05:34 -07:00
|
|
|
|
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-12 15:28:31 -07:00
|
|
|
|
|
|
|
|
|
|
// Tooltip on hover
|
|
|
|
|
|
float mdx = mouse.x - sx, mdy = mouse.y - sy;
|
|
|
|
|
|
if (mdx * mdx + mdy * mdy < 64.0f) {
|
2026-03-12 16:05:34 -07:00
|
|
|
|
if (isQuestGO)
|
|
|
|
|
|
ImGui::SetTooltip("%s (quest)", goInfo->name.c_str());
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::SetTooltip("%s", goInfo->name.c_str());
|
2026-03-12 15:28:31 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 15:19:08 -07: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;
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
|
|
// 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-02-06 20:10:10 -08: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 = "!";
|
2026-02-19 02:04:56 -08:00
|
|
|
|
} else if (status == game::QuestGiverStatus::REWARD ||
|
|
|
|
|
|
status == game::QuestGiverStatus::REWARD_REP) {
|
2026-02-06 20:10:10 -08:00
|
|
|
|
dotColor = IM_COL32(255, 210, 0, 255);
|
|
|
|
|
|
marker = "?";
|
|
|
|
|
|
} else if (status == game::QuestGiverStatus::INCOMPLETE) {
|
|
|
|
|
|
dotColor = IM_COL32(160, 160, 160, 255);
|
|
|
|
|
|
marker = "?";
|
|
|
|
|
|
} else {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
auto entity = gameHandler.getEntityManager().getEntity(guid);
|
|
|
|
|
|
if (!entity) continue;
|
|
|
|
|
|
|
|
|
|
|
|
glm::vec3 canonical(entity->getX(), entity->getY(), entity->getZ());
|
|
|
|
|
|
glm::vec3 npcRender = core::coords::canonicalToRender(canonical);
|
|
|
|
|
|
|
2026-02-20 16:40:22 -08:00
|
|
|
|
float sx = 0.0f, sy = 0.0f;
|
|
|
|
|
|
if (!projectToMinimap(npcRender, sx, sy)) continue;
|
2026-02-06 20:10:10 -08: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-12 13:28:49 -07: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-02-06 20:10:10 -08:00
|
|
|
|
}
|
2026-02-09 17:39:21 -08:00
|
|
|
|
|
2026-03-12 07:04:45 -07:00
|
|
|
|
// Quest kill objective markers — highlight live NPCs matching active quest kill objectives
|
|
|
|
|
|
{
|
2026-03-12 07:18:11 -07:00
|
|
|
|
// 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;
|
2026-03-12 07:04:45 -07:00
|
|
|
|
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;
|
2026-03-12 07:18:11 -07:00
|
|
|
|
if (current < obj.required) {
|
|
|
|
|
|
killInfoMap[npcEntry] = { quest.title, current, obj.required };
|
|
|
|
|
|
}
|
2026-03-12 07:04:45 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 07:18:11 -07:00
|
|
|
|
if (!killInfoMap.empty()) {
|
|
|
|
|
|
ImVec2 mouse = ImGui::GetMousePos();
|
2026-03-12 07:04:45 -07:00
|
|
|
|
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;
|
2026-03-12 07:18:11 -07:00
|
|
|
|
auto infoIt = killInfoMap.find(unit->getEntry());
|
|
|
|
|
|
if (infoIt == killInfoMap.end()) continue;
|
2026-03-12 07:04:45 -07: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;
|
|
|
|
|
|
|
|
|
|
|
|
// 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-12 07:18:11 -07: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-12 07:04:45 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 14:38:45 -07: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;
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
|
|
// 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 20:36:20 -07: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;
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
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-10 05:33:21 -07: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;
|
|
|
|
|
|
|
|
|
|
|
|
// 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));
|
|
|
|
|
|
|
|
|
|
|
|
float sx = 0.0f, sy = 0.0f;
|
|
|
|
|
|
if (!projectToMinimap(memberRender, sx, sy)) continue;
|
|
|
|
|
|
|
2026-03-12 08:40:54 -07: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);
|
|
|
|
|
|
}
|
2026-03-10 05:33:21 -07:00
|
|
|
|
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-12 13:48:01 -07:00
|
|
|
|
// Raid mark: tiny symbol drawn above the dot
|
|
|
|
|
|
{
|
2026-03-27 14:47:58 -07:00
|
|
|
|
static constexpr struct { const char* sym; ImU32 col; } kMMMarks[] = {
|
2026-03-12 13:48:01 -07:00
|
|
|
|
{ "\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-10 05:33:21 -07:00
|
|
|
|
ImVec2 cursorPos = ImGui::GetMousePos();
|
|
|
|
|
|
float mdx = cursorPos.x - sx, mdy = cursorPos.y - sy;
|
|
|
|
|
|
if (!member.name.empty() && (mdx * mdx + mdy * mdy) < 64.0f) {
|
2026-03-12 13:48:01 -07:00
|
|
|
|
uint8_t pmk2 = gameHandler.getEntityRaidMark(member.guid);
|
|
|
|
|
|
if (pmk2 < game::GameHandler::kRaidMarkCount) {
|
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* kMarkNames[] = {
|
2026-03-12 13:48:01 -07:00
|
|
|
|
"Star", "Circle", "Diamond", "Triangle",
|
|
|
|
|
|
"Moon", "Square", "Cross", "Skull"
|
|
|
|
|
|
};
|
|
|
|
|
|
ImGui::SetTooltip("%s {%s}", member.name.c_str(), kMarkNames[pmk2]);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::SetTooltip("%s", member.name.c_str());
|
|
|
|
|
|
}
|
2026-03-10 05:33:21 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 20:54:59 -07: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;
|
|
|
|
|
|
|
|
|
|
|
|
ImU32 col = kBgGroupColors[bp.group & 1];
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
|
|
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-11 23:19:48 -07: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);
|
|
|
|
|
|
|
|
|
|
|
|
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-17 22:05:24 -07: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
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
|
|
// 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-11 23:01:37 -07: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-11 23:00:03 -07: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-12 04:27:26 -07: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);
|
|
|
|
|
|
|
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
|
float fontSize = ImGui::GetFontSize();
|
|
|
|
|
|
ImVec2 textSz = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, coordBuf);
|
|
|
|
|
|
|
|
|
|
|
|
float tx = centerX - textSz.x * 0.5f;
|
|
|
|
|
|
float ty = centerY + mapRadius + 3.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// 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-17 20:06:05 -07: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);
|
|
|
|
|
|
|
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
|
|
|
|
|
float fontSize = ImGui::GetFontSize() * 0.9f; // slightly smaller than coords
|
|
|
|
|
|
ImVec2 clockSz = font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, clockBuf);
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
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 07:56:59 -07: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-18 12:21:41 -07:00
|
|
|
|
// Instance difficulty indicator — just below zone name, inside minimap top edge
|
|
|
|
|
|
if (gameHandler.isInInstance()) {
|
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* kDiffLabels[] = {"Normal", "Heroic", "25 Normal", "25 Heroic"};
|
2026-03-18 12:21:41 -07:00
|
|
|
|
uint32_t diff = gameHandler.getInstanceDifficulty();
|
|
|
|
|
|
const char* label = (diff < 4) ? kDiffLabels[diff] : "Unknown";
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 05:25:46 -07:00
|
|
|
|
// Hover tooltip and right-click context menu
|
2026-03-12 01:57:03 -07:00
|
|
|
|
{
|
|
|
|
|
|
ImVec2 mouse = ImGui::GetMousePos();
|
|
|
|
|
|
float mdx = mouse.x - centerX;
|
|
|
|
|
|
float mdy = mouse.y - centerY;
|
2026-03-12 05:25:46 -07:00
|
|
|
|
bool overMinimap = (mdx * mdx + mdy * mdy <= mapRadius * mapRadius);
|
|
|
|
|
|
|
|
|
|
|
|
if (overMinimap) {
|
2026-03-12 01:57:03 -07:00
|
|
|
|
ImGui::BeginTooltip();
|
2026-03-12 07:41:22 -07:00
|
|
|
|
// 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);
|
2026-03-27 10:20:45 -07:00
|
|
|
|
ImGui::TextColored(colors::kMediumGray, "Ctrl+click to ping");
|
2026-03-12 01:57:03 -07:00
|
|
|
|
ImGui::EndTooltip();
|
2026-03-12 05:25:46 -07:00
|
|
|
|
|
|
|
|
|
|
if (ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
|
|
|
|
|
|
ImGui::OpenPopup("##minimapContextMenu");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::BeginPopup("##minimapContextMenu")) {
|
2026-03-25 19:30:23 -07:00
|
|
|
|
ImGui::TextColored(ui::colors::kTooltipGold, "Minimap");
|
2026-03-12 05:25:46 -07:00
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
// Zoom controls
|
|
|
|
|
|
if (ImGui::MenuItem("Zoom In")) {
|
|
|
|
|
|
minimap->zoomIn();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Zoom Out")) {
|
|
|
|
|
|
minimap->zoomOut();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
// Toggle options with checkmarks
|
|
|
|
|
|
bool rotWithCam = minimap->isRotateWithCamera();
|
|
|
|
|
|
if (ImGui::MenuItem("Rotate with Camera", nullptr, rotWithCam)) {
|
|
|
|
|
|
minimap->setRotateWithCamera(!rotWithCam);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool squareShape = minimap->isSquareShape();
|
|
|
|
|
|
if (ImGui::MenuItem("Square Shape", nullptr, squareShape)) {
|
|
|
|
|
|
minimap->setSquareShape(!squareShape);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-31 10:07:58 +03:00
|
|
|
|
bool npcDots = settingsPanel_.minimapNpcDots_;
|
2026-03-12 05:25:46 -07:00
|
|
|
|
if (ImGui::MenuItem("Show NPC Dots", nullptr, npcDots)) {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.minimapNpcDots_ = !settingsPanel_.minimapNpcDots_;
|
2026-03-12 05:25:46 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::EndPopup();
|
2026-03-12 01:57:03 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-19 02:46:52 -08:00
|
|
|
|
auto applyMuteState = [&]() {
|
|
|
|
|
|
auto* activeRenderer = core::Application::getInstance().getRenderer();
|
2026-03-31 10:07:58 +03:00
|
|
|
|
float masterScale = settingsPanel_.soundMuted_ ? 0.0f : static_cast<float>(settingsPanel_.pendingMasterVolume) / 100.0f;
|
2026-02-19 02:46:52 -08:00
|
|
|
|
audio::AudioEngine::instance().setMasterVolume(masterScale);
|
|
|
|
|
|
if (!activeRenderer) return;
|
|
|
|
|
|
if (auto* music = activeRenderer->getMusicManager()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
music->setVolume(settingsPanel_.pendingMusicVolume);
|
2026-02-19 02:46:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* ambient = activeRenderer->getAmbientSoundManager()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ambient->setVolumeScale(settingsPanel_.pendingAmbientVolume / 100.0f);
|
2026-02-19 02:46:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* ui = activeRenderer->getUiSoundManager()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
ui->setVolumeScale(settingsPanel_.pendingUiVolume / 100.0f);
|
2026-02-19 02:46:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* combat = activeRenderer->getCombatSoundManager()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
combat->setVolumeScale(settingsPanel_.pendingCombatVolume / 100.0f);
|
2026-02-19 02:46:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* spell = activeRenderer->getSpellSoundManager()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
spell->setVolumeScale(settingsPanel_.pendingSpellVolume / 100.0f);
|
2026-02-19 02:46:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* movement = activeRenderer->getMovementSoundManager()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
movement->setVolumeScale(settingsPanel_.pendingMovementVolume / 100.0f);
|
2026-02-19 02:46:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* footstep = activeRenderer->getFootstepManager()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
footstep->setVolumeScale(settingsPanel_.pendingFootstepVolume / 100.0f);
|
2026-02-19 02:46:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* npcVoice = activeRenderer->getNpcVoiceManager()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
npcVoice->setVolumeScale(settingsPanel_.pendingNpcVoiceVolume / 100.0f);
|
2026-02-19 02:46:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* mount = activeRenderer->getMountSoundManager()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
mount->setVolumeScale(settingsPanel_.pendingMountVolume / 100.0f);
|
2026-02-19 02:46:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (auto* activity = activeRenderer->getActivitySoundManager()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
activity->setVolumeScale(settingsPanel_.pendingActivityVolume / 100.0f);
|
2026-02-19 02:46:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-02-17 16:26:49 -08:00
|
|
|
|
|
2026-03-10 05:52:55 -07:00
|
|
|
|
// Zone name label above the minimap (centered, WoW-style)
|
2026-03-17 19:16:02 -07:00
|
|
|
|
// 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.
|
2026-03-10 05:52:55 -07:00
|
|
|
|
{
|
2026-03-17 19:16:02 -07:00
|
|
|
|
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;
|
2026-03-10 05:52:55 -07:00
|
|
|
|
if (!zoneName.empty()) {
|
|
|
|
|
|
auto* fgDl = ImGui::GetForegroundDrawList();
|
|
|
|
|
|
float zoneTextY = centerY - mapRadius - 16.0f;
|
|
|
|
|
|
ImFont* font = ImGui::GetFont();
|
2026-03-12 05:34:56 -07: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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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());
|
2026-03-10 05:52:55 -07:00
|
|
|
|
float tzx = centerX - tsz.x * 0.5f;
|
2026-03-12 05:34:56 -07:00
|
|
|
|
|
|
|
|
|
|
// Shadow pass
|
2026-03-10 05:52:55 -07:00
|
|
|
|
fgDl->AddText(font, 12.0f, ImVec2(tzx + 1.0f, zoneTextY + 1.0f),
|
|
|
|
|
|
IM_COL32(0, 0, 0, 180), zoneName.c_str());
|
2026-03-12 05:34:56 -07:00
|
|
|
|
// Zone name in gold
|
2026-03-10 05:52:55 -07:00
|
|
|
|
fgDl->AddText(font, 12.0f, ImVec2(tzx, zoneTextY),
|
|
|
|
|
|
IM_COL32(255, 220, 120, 230), zoneName.c_str());
|
2026-03-12 05:34:56 -07:00
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
2026-03-10 05:52:55 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-19 02:46:52 -08: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)) {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.soundMuted_ = !settingsPanel_.soundMuted_;
|
|
|
|
|
|
if (settingsPanel_.soundMuted_) {
|
|
|
|
|
|
settingsPanel_.preMuteVolume_ = audio::AudioEngine::instance().getMasterVolume();
|
2026-02-17 16:26:49 -08:00
|
|
|
|
}
|
2026-02-19 02:46:52 -08:00
|
|
|
|
applyMuteState();
|
2026-02-17 16:26:49 -08:00
|
|
|
|
saveSettings();
|
|
|
|
|
|
}
|
2026-02-19 02:46:52 -08:00
|
|
|
|
bool hovered = ImGui::IsItemHovered();
|
2026-03-31 10:07:58 +03:00
|
|
|
|
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);
|
2026-02-19 02:46:52 -08:00
|
|
|
|
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);
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (settingsPanel_.soundMuted_) {
|
2026-02-19 02:46:52 -08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (hovered) ImGui::SetTooltip(settingsPanel_.soundMuted_ ? "Unmute" : "Mute");
|
2026-02-19 02:46:52 -08:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
2026-02-17 16:26:49 -08:00
|
|
|
|
|
2026-03-12 00:53:57 -07: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;
|
|
|
|
|
|
|
|
|
|
|
|
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)) {
|
|
|
|
|
|
showSocialFrame_ = !showSocialFrame_;
|
|
|
|
|
|
}
|
|
|
|
|
|
bool hovered = ImGui::IsItemHovered();
|
|
|
|
|
|
ImU32 bg = showSocialFrame_
|
|
|
|
|
|
? IM_COL32(42, 100, 42, 230)
|
|
|
|
|
|
: IM_COL32(38, 38, 38, 210);
|
|
|
|
|
|
if (hovered) bg = 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-02-19 02:46:52 -08: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));
|
2026-02-09 17:39:21 -08:00
|
|
|
|
if (ImGui::SmallButton("-")) {
|
|
|
|
|
|
if (minimap) minimap->zoomOut();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::SmallButton("+")) {
|
|
|
|
|
|
if (minimap) minimap->zoomIn();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleVar(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
2026-02-16 18:46:44 -08:00
|
|
|
|
|
2026-03-18 10:54:03 -07: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);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 21:19:42 -07: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
|
2026-02-16 18:46:44 -08:00
|
|
|
|
if (gameHandler.hasNewMail()) {
|
2026-03-10 21:19:42 -07:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(indicatorX, nextIndicatorY), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(indicatorW, kIndicatorH), ImGuiCond_Always);
|
|
|
|
|
|
if (ImGui::Begin("##NewMailIndicator", nullptr, indicatorFlags)) {
|
2026-02-16 18:46:44 -08:00
|
|
|
|
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!");
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
2026-03-10 21:19:42 -07:00
|
|
|
|
nextIndicatorY += kIndicatorH;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 05:44:25 -07:00
|
|
|
|
// Unspent talent points indicator
|
|
|
|
|
|
{
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
nextIndicatorY += kIndicatorH;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 21:19:42 -07: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
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
2026-03-13 10:10:04 -07:00
|
|
|
|
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-10 21:19:42 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
nextIndicatorY += kIndicatorH;
|
|
|
|
|
|
break; // Show at most one queue slot indicator
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 14:00:14 -07: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-13 09:25:23 -07:00
|
|
|
|
// Calendar pending invites indicator (WotLK only)
|
|
|
|
|
|
{
|
|
|
|
|
|
auto* expReg = core::Application::getInstance().getExpansionRegistry();
|
|
|
|
|
|
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-13 09:44:27 -07: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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
nextIndicatorY += kIndicatorH;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 10:27:25 -07:00
|
|
|
|
// Latency + FPS indicator — centered at top of screen
|
2026-03-10 21:19:42 -07:00
|
|
|
|
uint32_t latMs = gameHandler.getLatencyMs();
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (settingsPanel_.showLatencyMeter_ && gameHandler.getState() == game::WorldState::IN_WORLD) {
|
2026-03-18 10:27:25 -07:00
|
|
|
|
float currentFps = ImGui::GetIO().Framerate;
|
2026-03-10 21:19:42 -07:00
|
|
|
|
ImVec4 latColor;
|
2026-03-12 03:31:09 -07:00
|
|
|
|
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-18 10:27:25 -07: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);
|
|
|
|
|
|
|
|
|
|
|
|
char infoText[64];
|
|
|
|
|
|
if (latMs > 0)
|
|
|
|
|
|
snprintf(infoText, sizeof(infoText), "%.0f fps | %u ms", currentFps, latMs);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(infoText, sizeof(infoText), "%.0f fps", currentFps);
|
|
|
|
|
|
|
|
|
|
|
|
ImVec2 textSize = ImGui::CalcTextSize(infoText);
|
2026-03-12 03:31:09 -07:00
|
|
|
|
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);
|
2026-03-10 21:19:42 -07:00
|
|
|
|
if (ImGui::Begin("##LatencyIndicator", nullptr, indicatorFlags)) {
|
2026-03-18 10:27:25 -07:00
|
|
|
|
// 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-10 21:19:42 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
2026-03-11 23:13:31 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 23:35:51 -07: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";
|
|
|
|
|
|
|
|
|
|
|
|
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-11 23:13:31 -07: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);
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
2026-02-16 18:46:44 -08:00
|
|
|
|
}
|
2026-02-06 20:10:10 -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
|
|
|
|
void GameScreen::saveSettings() {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
std::string path = SettingsPanel::getSettingsPath();
|
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
|
|
|
|
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-02-09 17:39:21 -08:00
|
|
|
|
// Interface
|
2026-03-31 10:07:58 +03:00
|
|
|
|
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-02-09 17:39:21 -08:00
|
|
|
|
|
|
|
|
|
|
// Audio
|
2026-03-31 10:07:58 +03:00
|
|
|
|
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-02-09 17:39:21 -08:00
|
|
|
|
|
2026-02-17 16:31:00 -08:00
|
|
|
|
// Gameplay
|
2026-03-31 10:07:58 +03:00
|
|
|
|
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-02-17 16:31:00 -08:00
|
|
|
|
|
2026-02-09 17:39:21 -08:00
|
|
|
|
// Controls
|
2026-03-31 10:07:58 +03:00
|
|
|
|
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-02-09 17:39:21 -08:00
|
|
|
|
|
2026-03-13 04:04:29 -07:00
|
|
|
|
// Quest tracker position/size
|
|
|
|
|
|
out << "quest_tracker_right_offset=" << questTrackerRightOffset_ << "\n";
|
2026-03-12 16:47:42 -07:00
|
|
|
|
out << "quest_tracker_y=" << questTrackerPos_.y << "\n";
|
2026-03-13 04:04:29 -07:00
|
|
|
|
out << "quest_tracker_w=" << questTrackerSize_.x << "\n";
|
|
|
|
|
|
out << "quest_tracker_h=" << questTrackerSize_.y << "\n";
|
2026-03-12 16:47:42 -07:00
|
|
|
|
|
2026-02-14 14:30:09 -08:00
|
|
|
|
// Chat
|
2026-03-31 08:53:14 +03:00
|
|
|
|
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-02-14 14:30:09 -08:00
|
|
|
|
|
2026-03-11 06:51:48 -07:00
|
|
|
|
out.close();
|
|
|
|
|
|
|
|
|
|
|
|
// Save keybindings to the same config file (appends [Keybindings] section)
|
|
|
|
|
|
KeybindingManager::getInstance().saveToConfigFile(path);
|
|
|
|
|
|
|
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
|
|
|
|
LOG_INFO("Settings saved to ", path);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::loadSettings() {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
std::string path = SettingsPanel::getSettingsPath();
|
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
|
|
|
|
std::ifstream in(path);
|
|
|
|
|
|
if (!in.is_open()) return;
|
|
|
|
|
|
|
|
|
|
|
|
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-02-09 17:39:21 -08:00
|
|
|
|
try {
|
|
|
|
|
|
// Interface
|
|
|
|
|
|
if (key == "ui_opacity") {
|
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
|
|
|
|
int v = std::stoi(val);
|
|
|
|
|
|
if (v >= 20 && v <= 100) {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.pendingUiOpacity = v;
|
|
|
|
|
|
settingsPanel_.uiOpacity_ = static_cast<float>(v) / 100.0f;
|
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
|
|
|
|
}
|
2026-02-09 17:39:21 -08:00
|
|
|
|
} else if (key == "minimap_rotate") {
|
2026-02-11 17:30:57 -08:00
|
|
|
|
// Ignore persisted rotate state; keep north-up.
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.minimapRotate_ = false;
|
|
|
|
|
|
settingsPanel_.pendingMinimapRotate = false;
|
2026-02-09 17:39:21 -08:00
|
|
|
|
} else if (key == "minimap_square") {
|
|
|
|
|
|
int v = std::stoi(val);
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.minimapSquare_ = (v != 0);
|
|
|
|
|
|
settingsPanel_.pendingMinimapSquare = settingsPanel_.minimapSquare_;
|
2026-02-20 16:40:22 -08:00
|
|
|
|
} else if (key == "minimap_npc_dots") {
|
|
|
|
|
|
int v = std::stoi(val);
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.minimapNpcDots_ = (v != 0);
|
|
|
|
|
|
settingsPanel_.pendingMinimapNpcDots = settingsPanel_.minimapNpcDots_;
|
2026-03-11 19:45:03 -07:00
|
|
|
|
} else if (key == "show_latency_meter") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.showLatencyMeter_ = (std::stoi(val) != 0);
|
|
|
|
|
|
settingsPanel_.pendingShowLatencyMeter = settingsPanel_.showLatencyMeter_;
|
2026-03-12 04:04:27 -07:00
|
|
|
|
} else if (key == "show_dps_meter") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.showDPSMeter_ = (std::stoi(val) != 0);
|
2026-03-12 15:25:07 -07:00
|
|
|
|
} else if (key == "show_cooldown_tracker") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.showCooldownTracker_ = (std::stoi(val) != 0);
|
2026-02-13 22:51:49 -08:00
|
|
|
|
} else if (key == "separate_bags") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.pendingSeparateBags = (std::stoi(val) != 0);
|
|
|
|
|
|
inventoryScreen.setSeparateBags(settingsPanel_.pendingSeparateBags);
|
2026-03-17 08:18:46 -07:00
|
|
|
|
} else if (key == "show_keyring") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.pendingShowKeyring = (std::stoi(val) != 0);
|
|
|
|
|
|
inventoryScreen.setShowKeyring(settingsPanel_.pendingShowKeyring);
|
2026-03-11 22:39:59 -07:00
|
|
|
|
} else if (key == "action_bar_scale") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.pendingActionBarScale = std::clamp(std::stof(val), 0.5f, 1.5f);
|
2026-03-11 22:49:54 -07:00
|
|
|
|
} else if (key == "nameplate_scale") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.nameplateScale_ = std::clamp(std::stof(val), 0.5f, 2.0f);
|
2026-03-18 11:43:39 -07:00
|
|
|
|
} else if (key == "show_friendly_nameplates") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.showFriendlyNameplates_ = (std::stoi(val) != 0);
|
2026-03-10 15:45:35 -07:00
|
|
|
|
} else if (key == "show_action_bar2") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.pendingShowActionBar2 = (std::stoi(val) != 0);
|
2026-03-10 15:45:35 -07:00
|
|
|
|
} else if (key == "action_bar2_offset_x") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.pendingActionBar2OffsetX = std::clamp(std::stof(val), -600.0f, 600.0f);
|
2026-03-10 15:45:35 -07:00
|
|
|
|
} else if (key == "action_bar2_offset_y") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.pendingActionBar2OffsetY = std::clamp(std::stof(val), -400.0f, 400.0f);
|
2026-03-10 15:56:41 -07:00
|
|
|
|
} else if (key == "show_right_bar") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.pendingShowRightBar = (std::stoi(val) != 0);
|
2026-03-10 15:56:41 -07:00
|
|
|
|
} else if (key == "show_left_bar") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.pendingShowLeftBar = (std::stoi(val) != 0);
|
2026-03-10 15:56:41 -07:00
|
|
|
|
} else if (key == "right_bar_offset_y") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.pendingRightBarOffsetY = std::clamp(std::stof(val), -400.0f, 400.0f);
|
2026-03-10 15:56:41 -07:00
|
|
|
|
} else if (key == "left_bar_offset_y") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.pendingLeftBarOffsetY = std::clamp(std::stof(val), -400.0f, 400.0f);
|
2026-03-12 03:21:49 -07:00
|
|
|
|
} else if (key == "damage_flash") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.damageFlashEnabled_ = (std::stoi(val) != 0);
|
2026-03-12 07:15:08 -07:00
|
|
|
|
} else if (key == "low_health_vignette") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.lowHealthVignetteEnabled_ = (std::stoi(val) != 0);
|
2026-02-09 17:39:21 -08:00
|
|
|
|
}
|
|
|
|
|
|
// Audio
|
2026-02-17 16:26:49 -08:00
|
|
|
|
else if (key == "sound_muted") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.soundMuted_ = (std::stoi(val) != 0);
|
|
|
|
|
|
if (settingsPanel_.soundMuted_) {
|
|
|
|
|
|
// Apply mute on load; settingsPanel_.preMuteVolume_ will be set when AudioEngine is available
|
2026-02-17 16:26:49 -08:00
|
|
|
|
audio::AudioEngine::instance().setMasterVolume(0.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
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);
|
2026-02-17 16:31:00 -08:00
|
|
|
|
// Gameplay
|
2026-03-31 10:07:58 +03:00
|
|
|
|
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);
|
2026-03-11 15:21:48 -07:00
|
|
|
|
else if (key == "graphics_preset") {
|
|
|
|
|
|
int presetVal = std::clamp(std::stoi(val), 0, 4);
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.currentGraphicsPreset = static_cast<SettingsPanel::GraphicsPreset>(presetVal);
|
|
|
|
|
|
settingsPanel_.pendingGraphicsPreset = settingsPanel_.currentGraphicsPreset;
|
2026-03-11 15:21:48 -07:00
|
|
|
|
}
|
2026-03-31 10:07:58 +03:00
|
|
|
|
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);
|
2026-03-17 09:04:53 -07:00
|
|
|
|
else if (key == "brightness") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.pendingBrightness = std::clamp(std::stoi(val), 0, 100);
|
2026-03-17 09:04:53 -07:00
|
|
|
|
if (auto* r = core::Application::getInstance().getRenderer())
|
2026-03-31 10:07:58 +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);
|
2026-03-08 20:22:11 -07:00
|
|
|
|
else if (key == "upscaling_mode") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.pendingUpscalingMode = std::clamp(std::stoi(val), 0, 2);
|
|
|
|
|
|
settingsPanel_.pendingFSR = (settingsPanel_.pendingUpscalingMode == 1);
|
2026-03-08 20:22:11 -07:00
|
|
|
|
} else if (key == "fsr") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.pendingFSR = (std::stoi(val) != 0);
|
2026-03-08 20:22:11 -07:00
|
|
|
|
// Backward compatibility: old configs only had fsr=0/1.
|
2026-03-31 10:07:58 +03:00
|
|
|
|
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);
|
2026-02-09 17:39:21 -08:00
|
|
|
|
// Controls
|
2026-03-31 10:07:58 +03:00
|
|
|
|
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);
|
2026-03-11 22:13:22 -07:00
|
|
|
|
else if (key == "fov") {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
settingsPanel_.pendingFov = std::clamp(std::stof(val), 45.0f, 110.0f);
|
2026-03-11 22:13:22 -07:00
|
|
|
|
if (auto* renderer = core::Application::getInstance().getRenderer()) {
|
2026-03-31 10:07:58 +03:00
|
|
|
|
if (auto* camera = renderer->getCamera()) camera->setFov(settingsPanel_.pendingFov);
|
2026-03-11 22:13:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-13 04:04:29 -07:00
|
|
|
|
// Quest tracker position/size
|
2026-03-12 16:47:42 -07:00
|
|
|
|
else if (key == "quest_tracker_x") {
|
2026-03-13 04:04:29 -07:00
|
|
|
|
// Legacy: ignore absolute X (right_offset supersedes it)
|
|
|
|
|
|
(void)val;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (key == "quest_tracker_right_offset") {
|
|
|
|
|
|
questTrackerRightOffset_ = std::stof(val);
|
2026-03-12 16:47:42 -07:00
|
|
|
|
questTrackerPosInit_ = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (key == "quest_tracker_y") {
|
|
|
|
|
|
questTrackerPos_.y = std::stof(val);
|
|
|
|
|
|
questTrackerPosInit_ = true;
|
|
|
|
|
|
}
|
2026-03-13 04:04:29 -07:00
|
|
|
|
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));
|
|
|
|
|
|
}
|
2026-02-14 14:30:09 -08:00
|
|
|
|
// Chat
|
2026-03-31 08:53:14 +03:00
|
|
|
|
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);
|
2026-02-09 17:39:21 -08:00
|
|
|
|
} catch (...) {}
|
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
|
|
|
|
}
|
2026-03-11 06:51:48 -07:00
|
|
|
|
|
|
|
|
|
|
// Load keybindings from the same config file
|
|
|
|
|
|
KeybindingManager::getInstance().loadFromConfigFile(path);
|
|
|
|
|
|
|
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
|
|
|
|
LOG_INFO("Settings loaded from ", path);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-15 14:00:41 -08:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Mail Window
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderMailWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isMailboxOpen()) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 250, 80), ImGuiCond_Appearing);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(600, 500), ImGuiCond_Appearing);
|
|
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
|
if (ImGui::Begin("Mailbox", &open)) {
|
|
|
|
|
|
const auto& inbox = gameHandler.getMailInbox();
|
|
|
|
|
|
|
|
|
|
|
|
// Top bar: money + compose button
|
2026-03-12 08:10:17 -07:00
|
|
|
|
ImGui::TextDisabled("Your money:"); ImGui::SameLine(0, 4);
|
2026-03-25 12:59:31 -07:00
|
|
|
|
renderCoinsFromCopper(gameHandler.getMoneyCopper());
|
2026-02-15 14:00:41 -08:00
|
|
|
|
ImGui::SameLine(ImGui::GetWindowWidth() - 100);
|
|
|
|
|
|
if (ImGui::Button("Compose")) {
|
|
|
|
|
|
mailRecipientBuffer_[0] = '\0';
|
|
|
|
|
|
mailSubjectBuffer_[0] = '\0';
|
|
|
|
|
|
mailBodyBuffer_[0] = '\0';
|
|
|
|
|
|
mailComposeMoney_[0] = 0;
|
|
|
|
|
|
mailComposeMoney_[1] = 0;
|
|
|
|
|
|
mailComposeMoney_[2] = 0;
|
|
|
|
|
|
gameHandler.openMailCompose();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
if (inbox.empty()) {
|
|
|
|
|
|
ImGui::TextDisabled("No mail.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Two-panel layout: left = mail list, right = selected mail detail
|
|
|
|
|
|
float listWidth = 220.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// Left panel - mail list
|
|
|
|
|
|
ImGui::BeginChild("MailList", ImVec2(listWidth, 0), true);
|
|
|
|
|
|
for (size_t i = 0; i < inbox.size(); ++i) {
|
|
|
|
|
|
const auto& mail = inbox[i];
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(i));
|
|
|
|
|
|
|
|
|
|
|
|
bool selected = (gameHandler.getSelectedMailIndex() == static_cast<int>(i));
|
|
|
|
|
|
std::string label = mail.subject.empty() ? "(No Subject)" : mail.subject;
|
|
|
|
|
|
|
|
|
|
|
|
// Unread indicator
|
|
|
|
|
|
if (!mail.read) {
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 0.5f, 1.0f));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Selectable(label.c_str(), selected)) {
|
|
|
|
|
|
gameHandler.setSelectedMailIndex(static_cast<int>(i));
|
|
|
|
|
|
// Mark as read
|
|
|
|
|
|
if (!mail.read) {
|
|
|
|
|
|
gameHandler.mailMarkAsRead(mail.messageId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!mail.read) {
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Sub-info line
|
2026-03-25 11:57:22 -07:00
|
|
|
|
ImGui::TextColored(kColorGray, " From: %s", mail.senderName.c_str());
|
2026-02-15 14:00:41 -08:00
|
|
|
|
if (mail.money > 0) {
|
|
|
|
|
|
ImGui::SameLine();
|
2026-03-27 13:57:29 -07:00
|
|
|
|
ImGui::TextColored(colors::kWarmGold, " [G]");
|
2026-02-15 14:00:41 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (!mail.attachments.empty()) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.5f, 0.8f, 1.0f, 1.0f), " [A]");
|
|
|
|
|
|
}
|
2026-03-12 07:28:18 -07:00
|
|
|
|
// Expiry warning if within 3 days
|
|
|
|
|
|
if (mail.expirationTime > 0.0f) {
|
|
|
|
|
|
auto nowSec = static_cast<float>(std::time(nullptr));
|
|
|
|
|
|
float secsLeft = mail.expirationTime - nowSec;
|
|
|
|
|
|
if (secsLeft < 3.0f * 86400.0f && secsLeft > 0.0f) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
int daysLeft = static_cast<int>(secsLeft / 86400.0f);
|
|
|
|
|
|
if (daysLeft == 0) {
|
refactor: add 6 color constants, replace 61 inline literals, remove const_cast
- Add kBrightGold, kPaleRed, kBrightRed, kLightBlue, kManaBlue, kCyan to ui_colors.hpp
- Replace 61 inline ImVec4 color literals across game_screen, inventory_screen,
talent_screen, and world_map with named constants
- Remove const_cast in character_renderer render loop by using non-const iteration
2026-03-27 10:08:30 -07:00
|
|
|
|
ImGui::TextColored(colors::kBrightRed, " [expires today!]");
|
2026-03-12 07:28:18 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.1f, 1.0f),
|
|
|
|
|
|
" [expires in %dd]", daysLeft);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-15 14:00:41 -08:00
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
|
|
|
|
|
|
// Right panel - selected mail detail
|
|
|
|
|
|
ImGui::BeginChild("MailDetail", ImVec2(0, 0), true);
|
|
|
|
|
|
int sel = gameHandler.getSelectedMailIndex();
|
|
|
|
|
|
if (sel >= 0 && sel < static_cast<int>(inbox.size())) {
|
|
|
|
|
|
const auto& mail = inbox[sel];
|
|
|
|
|
|
|
2026-03-27 13:57:29 -07:00
|
|
|
|
ImGui::TextColored(colors::kWarmGold, "%s",
|
2026-02-15 14:00:41 -08:00
|
|
|
|
mail.subject.empty() ? "(No Subject)" : mail.subject.c_str());
|
|
|
|
|
|
ImGui::Text("From: %s", mail.senderName.c_str());
|
|
|
|
|
|
|
|
|
|
|
|
if (mail.messageType == 2) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.8f, 0.6f, 0.2f, 1.0f), "[Auction House]");
|
|
|
|
|
|
}
|
2026-03-12 07:28:18 -07:00
|
|
|
|
|
|
|
|
|
|
// Show expiry date in the detail panel
|
|
|
|
|
|
if (mail.expirationTime > 0.0f) {
|
|
|
|
|
|
auto nowSec = static_cast<float>(std::time(nullptr));
|
|
|
|
|
|
float secsLeft = mail.expirationTime - nowSec;
|
|
|
|
|
|
// Format absolute expiry as a date using struct tm
|
|
|
|
|
|
time_t expT = static_cast<time_t>(mail.expirationTime);
|
|
|
|
|
|
struct tm* tmExp = std::localtime(&expT);
|
|
|
|
|
|
if (tmExp) {
|
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
|
|
|
|
const char* mname = kMonthAbbrev[tmExp->tm_mon];
|
2026-03-12 07:28:18 -07:00
|
|
|
|
int daysLeft = static_cast<int>(secsLeft / 86400.0f);
|
|
|
|
|
|
if (secsLeft <= 0.0f) {
|
2026-03-25 11:57:22 -07:00
|
|
|
|
ImGui::TextColored(kColorGray,
|
2026-03-12 07:28:18 -07:00
|
|
|
|
"Expired: %s %d, %d", mname, tmExp->tm_mday, 1900 + tmExp->tm_year);
|
|
|
|
|
|
} else if (secsLeft < 3.0f * 86400.0f) {
|
2026-03-25 11:57:22 -07:00
|
|
|
|
ImGui::TextColored(kColorRed,
|
2026-03-12 07:28:18 -07:00
|
|
|
|
"Expires: %s %d, %d (%d day%s!)",
|
|
|
|
|
|
mname, tmExp->tm_mday, 1900 + tmExp->tm_year,
|
|
|
|
|
|
daysLeft, daysLeft == 1 ? "" : "s");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextDisabled("Expires: %s %d, %d",
|
|
|
|
|
|
mname, tmExp->tm_mday, 1900 + tmExp->tm_year);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-15 14:00:41 -08:00
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
// Body text
|
|
|
|
|
|
if (!mail.body.empty()) {
|
|
|
|
|
|
ImGui::TextWrapped("%s", mail.body.c_str());
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Money
|
|
|
|
|
|
if (mail.money > 0) {
|
2026-03-12 08:15:46 -07:00
|
|
|
|
ImGui::TextDisabled("Money:"); ImGui::SameLine(0, 4);
|
2026-03-25 12:59:31 -07:00
|
|
|
|
renderCoinsFromCopper(mail.money);
|
2026-02-15 14:00:41 -08:00
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::SmallButton("Take Money")) {
|
|
|
|
|
|
gameHandler.mailTakeMoney(mail.messageId);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// COD warning
|
|
|
|
|
|
if (mail.cod > 0) {
|
2026-03-27 17:20:31 -07:00
|
|
|
|
uint64_t g = mail.cod / 10000;
|
|
|
|
|
|
uint64_t s = (mail.cod / 100) % 100;
|
|
|
|
|
|
uint64_t c = mail.cod % 100;
|
2026-03-25 11:57:22 -07:00
|
|
|
|
ImGui::TextColored(kColorRed,
|
2026-03-27 17:20:31 -07:00
|
|
|
|
"COD: %llug %llus %lluc (you pay this to take items)",
|
|
|
|
|
|
static_cast<unsigned long long>(g),
|
|
|
|
|
|
static_cast<unsigned long long>(s),
|
|
|
|
|
|
static_cast<unsigned long long>(c));
|
2026-02-15 14:00:41 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Attachments
|
|
|
|
|
|
if (!mail.attachments.empty()) {
|
|
|
|
|
|
ImGui::Text("Attachments: %zu", mail.attachments.size());
|
2026-03-11 20:39:15 -07:00
|
|
|
|
ImDrawList* mailDraw = ImGui::GetWindowDrawList();
|
|
|
|
|
|
constexpr float MAIL_SLOT = 34.0f;
|
2026-02-15 14:00:41 -08:00
|
|
|
|
for (size_t j = 0; j < mail.attachments.size(); ++j) {
|
|
|
|
|
|
const auto& att = mail.attachments[j];
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(j));
|
|
|
|
|
|
|
|
|
|
|
|
auto* info = gameHandler.getItemInfo(att.itemId);
|
2026-03-11 20:39:15 -07:00
|
|
|
|
game::ItemQuality quality = game::ItemQuality::COMMON;
|
|
|
|
|
|
std::string name = "Item " + std::to_string(att.itemId);
|
|
|
|
|
|
uint32_t displayInfoId = 0;
|
2026-02-15 14:00:41 -08:00
|
|
|
|
if (info && info->valid) {
|
2026-03-11 20:39:15 -07:00
|
|
|
|
quality = static_cast<game::ItemQuality>(info->quality);
|
|
|
|
|
|
name = info->name;
|
|
|
|
|
|
displayInfoId = info->displayInfoId;
|
2026-02-15 14:00:41 -08:00
|
|
|
|
} else {
|
|
|
|
|
|
gameHandler.ensureItemInfo(att.itemId);
|
|
|
|
|
|
}
|
2026-03-11 20:39:15 -07:00
|
|
|
|
ImVec4 qc = InventoryScreen::getQualityColor(quality);
|
|
|
|
|
|
ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qc);
|
|
|
|
|
|
|
|
|
|
|
|
ImVec2 pos = ImGui::GetCursorScreenPos();
|
|
|
|
|
|
VkDescriptorSet iconTex = displayInfoId
|
|
|
|
|
|
? inventoryScreen.getItemIcon(displayInfoId) : VK_NULL_HANDLE;
|
|
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
mailDraw->AddImage((ImTextureID)(uintptr_t)iconTex, pos,
|
|
|
|
|
|
ImVec2(pos.x + MAIL_SLOT, pos.y + MAIL_SLOT));
|
|
|
|
|
|
mailDraw->AddRect(pos, ImVec2(pos.x + MAIL_SLOT, pos.y + MAIL_SLOT),
|
|
|
|
|
|
borderCol, 0.0f, 0, 1.5f);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
mailDraw->AddRectFilled(pos,
|
|
|
|
|
|
ImVec2(pos.x + MAIL_SLOT, pos.y + MAIL_SLOT),
|
|
|
|
|
|
IM_COL32(40, 35, 30, 220));
|
|
|
|
|
|
mailDraw->AddRect(pos,
|
|
|
|
|
|
ImVec2(pos.x + MAIL_SLOT, pos.y + MAIL_SLOT),
|
|
|
|
|
|
borderCol, 0.0f, 0, 1.5f);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (att.stackCount > 1) {
|
|
|
|
|
|
char cnt[16];
|
|
|
|
|
|
snprintf(cnt, sizeof(cnt), "%u", att.stackCount);
|
|
|
|
|
|
float cw = ImGui::CalcTextSize(cnt).x;
|
|
|
|
|
|
mailDraw->AddText(ImVec2(pos.x + 1.0f, pos.y + 1.0f),
|
|
|
|
|
|
IM_COL32(0, 0, 0, 200), cnt);
|
|
|
|
|
|
mailDraw->AddText(
|
|
|
|
|
|
ImVec2(pos.x + MAIL_SLOT - cw - 2.0f, pos.y + MAIL_SLOT - 14.0f),
|
|
|
|
|
|
IM_COL32(255, 255, 255, 220), cnt);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::InvisibleButton("##mailatt", ImVec2(MAIL_SLOT, MAIL_SLOT));
|
2026-03-11 21:14:27 -07:00
|
|
|
|
if (ImGui::IsItemHovered() && info && info->valid)
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
2026-03-11 21:37:15 -07:00
|
|
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
|
|
|
|
|
ImGui::GetIO().KeyShift && info && info->valid && !info->name.empty()) {
|
|
|
|
|
|
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
|
2026-03-31 08:53:14 +03:00
|
|
|
|
chatPanel_.insertChatLink(link);
|
2026-03-11 21:37:15 -07:00
|
|
|
|
}
|
2026-03-11 20:39:15 -07:00
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextColored(qc, "%s", name.c_str());
|
2026-03-11 21:37:15 -07:00
|
|
|
|
if (ImGui::IsItemHovered() && info && info->valid)
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
|
|
|
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
|
|
|
|
|
ImGui::GetIO().KeyShift && info && info->valid && !info->name.empty()) {
|
|
|
|
|
|
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
|
2026-03-31 08:53:14 +03:00
|
|
|
|
chatPanel_.insertChatLink(link);
|
2026-03-11 21:37:15 -07:00
|
|
|
|
}
|
2026-02-15 14:00:41 -08:00
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::SmallButton("Take")) {
|
2026-03-14 07:11:18 -07:00
|
|
|
|
gameHandler.mailTakeItem(mail.messageId, att.itemGuidLow);
|
2026-02-15 14:00:41 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
2026-03-11 21:37:15 -07:00
|
|
|
|
// "Take All" button when there are multiple attachments
|
|
|
|
|
|
if (mail.attachments.size() > 1) {
|
|
|
|
|
|
if (ImGui::SmallButton("Take All")) {
|
|
|
|
|
|
for (const auto& att2 : mail.attachments) {
|
2026-03-14 07:11:18 -07:00
|
|
|
|
gameHandler.mailTakeItem(mail.messageId, att2.itemGuidLow);
|
2026-03-11 21:37:15 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-15 14:00:41 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
// Action buttons
|
|
|
|
|
|
if (ImGui::Button("Delete")) {
|
|
|
|
|
|
gameHandler.mailDelete(mail.messageId);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (mail.messageType == 0 && ImGui::Button("Reply")) {
|
|
|
|
|
|
// Pre-fill compose with sender as recipient
|
|
|
|
|
|
strncpy(mailRecipientBuffer_, mail.senderName.c_str(), sizeof(mailRecipientBuffer_) - 1);
|
|
|
|
|
|
mailRecipientBuffer_[sizeof(mailRecipientBuffer_) - 1] = '\0';
|
|
|
|
|
|
std::string reSubject = "Re: " + mail.subject;
|
|
|
|
|
|
strncpy(mailSubjectBuffer_, reSubject.c_str(), sizeof(mailSubjectBuffer_) - 1);
|
|
|
|
|
|
mailSubjectBuffer_[sizeof(mailSubjectBuffer_) - 1] = '\0';
|
|
|
|
|
|
mailBodyBuffer_[0] = '\0';
|
|
|
|
|
|
mailComposeMoney_[0] = 0;
|
|
|
|
|
|
mailComposeMoney_[1] = 0;
|
|
|
|
|
|
mailComposeMoney_[2] = 0;
|
|
|
|
|
|
gameHandler.openMailCompose();
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextDisabled("Select a mail to read.");
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
gameHandler.closeMailbox();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderMailComposeWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isMailComposeOpen()) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
|
|
|
|
|
|
2026-02-25 14:11:09 -08:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW / 2 - 190, screenH / 2 - 250), ImGuiCond_Appearing);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(400, 500), ImGuiCond_Appearing);
|
2026-02-15 14:00:41 -08:00
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
|
if (ImGui::Begin("Send Mail", &open)) {
|
|
|
|
|
|
ImGui::Text("To:");
|
|
|
|
|
|
ImGui::SameLine(60);
|
|
|
|
|
|
ImGui::SetNextItemWidth(-1);
|
|
|
|
|
|
ImGui::InputText("##MailTo", mailRecipientBuffer_, sizeof(mailRecipientBuffer_));
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Text("Subject:");
|
|
|
|
|
|
ImGui::SameLine(60);
|
|
|
|
|
|
ImGui::SetNextItemWidth(-1);
|
|
|
|
|
|
ImGui::InputText("##MailSubject", mailSubjectBuffer_, sizeof(mailSubjectBuffer_));
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Text("Body:");
|
|
|
|
|
|
ImGui::InputTextMultiline("##MailBody", mailBodyBuffer_, sizeof(mailBodyBuffer_),
|
2026-02-25 14:11:09 -08:00
|
|
|
|
ImVec2(-1, 120));
|
2026-02-15 14:00:41 -08:00
|
|
|
|
|
2026-02-25 14:11:09 -08:00
|
|
|
|
// Attachments section
|
|
|
|
|
|
int attachCount = gameHandler.getMailAttachmentCount();
|
|
|
|
|
|
ImGui::Text("Attachments (%d/12):", attachCount);
|
|
|
|
|
|
ImGui::SameLine();
|
2026-03-25 11:57:22 -07:00
|
|
|
|
ImGui::TextColored(kColorGray, "Right-click items in bags to attach");
|
2026-02-25 14:11:09 -08:00
|
|
|
|
|
|
|
|
|
|
const auto& attachments = gameHandler.getMailAttachments();
|
|
|
|
|
|
// Show attachment slots in a grid (6 per row)
|
|
|
|
|
|
for (int i = 0; i < game::GameHandler::MAIL_MAX_ATTACHMENTS; ++i) {
|
|
|
|
|
|
if (i % 6 != 0) ImGui::SameLine();
|
|
|
|
|
|
ImGui::PushID(i + 5000);
|
|
|
|
|
|
const auto& att = attachments[i];
|
|
|
|
|
|
if (att.occupied()) {
|
|
|
|
|
|
// Show item with quality color border
|
|
|
|
|
|
ImVec4 qualColor = ui::InventoryScreen::getQualityColor(att.item.quality);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(qualColor.x * 0.3f, qualColor.y * 0.3f, qualColor.z * 0.3f, 0.8f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(qualColor.x * 0.5f, qualColor.y * 0.5f, qualColor.z * 0.5f, 0.9f));
|
|
|
|
|
|
|
|
|
|
|
|
// Try to show icon
|
|
|
|
|
|
VkDescriptorSet icon = inventoryScreen.getItemIcon(att.item.displayInfoId);
|
|
|
|
|
|
bool clicked = false;
|
|
|
|
|
|
if (icon) {
|
|
|
|
|
|
clicked = ImGui::ImageButton("##att", (ImTextureID)icon, ImVec2(30, 30));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Truncate name to fit
|
|
|
|
|
|
std::string label = att.item.name.substr(0, 4);
|
|
|
|
|
|
clicked = ImGui::Button(label.c_str(), ImVec2(36, 36));
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
|
|
|
|
|
|
if (clicked) {
|
|
|
|
|
|
gameHandler.detachMailAttachment(i);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
ImGui::TextColored(qualColor, "%s", att.item.name.c_str());
|
2026-03-25 12:29:44 -07:00
|
|
|
|
ImGui::TextColored(ui::colors::kLightGray, "Click to remove");
|
2026-02-25 14:11:09 -08:00
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.15f, 0.15f, 0.15f, 0.5f));
|
|
|
|
|
|
ImGui::Button("##empty", ImVec2(36, 36));
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
2026-02-15 14:00:41 -08:00
|
|
|
|
ImGui::Text("Money:");
|
|
|
|
|
|
ImGui::SameLine(60);
|
|
|
|
|
|
ImGui::SetNextItemWidth(60);
|
|
|
|
|
|
ImGui::InputInt("##MailGold", &mailComposeMoney_[0], 0, 0);
|
|
|
|
|
|
if (mailComposeMoney_[0] < 0) mailComposeMoney_[0] = 0;
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::Text("g");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetNextItemWidth(40);
|
|
|
|
|
|
ImGui::InputInt("##MailSilver", &mailComposeMoney_[1], 0, 0);
|
|
|
|
|
|
if (mailComposeMoney_[1] < 0) mailComposeMoney_[1] = 0;
|
|
|
|
|
|
if (mailComposeMoney_[1] > 99) mailComposeMoney_[1] = 99;
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::Text("s");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetNextItemWidth(40);
|
|
|
|
|
|
ImGui::InputInt("##MailCopper", &mailComposeMoney_[2], 0, 0);
|
|
|
|
|
|
if (mailComposeMoney_[2] < 0) mailComposeMoney_[2] = 0;
|
|
|
|
|
|
if (mailComposeMoney_[2] > 99) mailComposeMoney_[2] = 99;
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::Text("c");
|
|
|
|
|
|
|
2026-03-27 17:20:31 -07:00
|
|
|
|
uint64_t totalMoney = static_cast<uint64_t>(mailComposeMoney_[0]) * 10000 +
|
|
|
|
|
|
static_cast<uint64_t>(mailComposeMoney_[1]) * 100 +
|
|
|
|
|
|
static_cast<uint64_t>(mailComposeMoney_[2]);
|
2026-02-15 14:00:41 -08:00
|
|
|
|
|
2026-02-25 14:11:09 -08:00
|
|
|
|
uint32_t sendCost = attachCount > 0 ? static_cast<uint32_t>(30 * attachCount) : 30u;
|
2026-03-25 11:57:22 -07:00
|
|
|
|
ImGui::TextColored(kColorGray, "Sending cost: %uc", sendCost);
|
2026-02-15 14:00:41 -08:00
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
bool canSend = (strlen(mailRecipientBuffer_) > 0);
|
|
|
|
|
|
if (!canSend) ImGui::BeginDisabled();
|
|
|
|
|
|
if (ImGui::Button("Send", ImVec2(80, 0))) {
|
|
|
|
|
|
gameHandler.sendMail(mailRecipientBuffer_, mailSubjectBuffer_,
|
|
|
|
|
|
mailBodyBuffer_, totalMoney);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!canSend) ImGui::EndDisabled();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(80, 0))) {
|
|
|
|
|
|
gameHandler.closeMailCompose();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
gameHandler.closeMailCompose();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Bank Window
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderBankWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isBankOpen()) return;
|
|
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(480, 420), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
if (!ImGui::Begin("Bank", &open)) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
if (!open) gameHandler.closeBank();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
auto& inv = gameHandler.getInventory();
|
2026-02-25 13:54:47 -08:00
|
|
|
|
bool isHolding = inventoryScreen.isHoldingItem();
|
2026-02-26 13:38:29 -08:00
|
|
|
|
constexpr float SLOT_SIZE = 42.0f;
|
|
|
|
|
|
static constexpr float kBankPickupHold = 0.10f; // seconds
|
|
|
|
|
|
// Persistent pickup tracking for bank (mirrors inventory_screen's pickupPending_)
|
|
|
|
|
|
static bool bankPickupPending = false;
|
|
|
|
|
|
static float bankPickupPressTime = 0.0f;
|
|
|
|
|
|
static int bankPickupType = 0; // 0=main bank, 1=bank bag slot, 2=bank bag equip slot
|
|
|
|
|
|
static int bankPickupIndex = -1;
|
|
|
|
|
|
static int bankPickupBagIndex = -1;
|
|
|
|
|
|
static int bankPickupBagSlotIndex = -1;
|
|
|
|
|
|
|
|
|
|
|
|
// Helper: render a bank item slot with icon, click-and-hold pickup, drop, tooltip
|
|
|
|
|
|
auto renderBankItemSlot = [&](const game::ItemSlot& slot, int pickType, int mainIdx,
|
|
|
|
|
|
int bagIdx, int bagSlotIdx, uint8_t dstBag, uint8_t dstSlot) {
|
|
|
|
|
|
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
|
|
|
|
|
ImVec2 pos = ImGui::GetCursorScreenPos();
|
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
|
|
|
|
|
|
|
|
|
|
if (slot.empty()) {
|
2026-02-26 13:38:29 -08:00
|
|
|
|
ImU32 bgCol = IM_COL32(30, 30, 30, 200);
|
|
|
|
|
|
ImU32 borderCol = IM_COL32(60, 60, 60, 200);
|
2026-02-25 13:54:47 -08:00
|
|
|
|
if (isHolding) {
|
2026-02-26 13:38:29 -08:00
|
|
|
|
bgCol = IM_COL32(20, 50, 20, 200);
|
|
|
|
|
|
borderCol = IM_COL32(0, 180, 0, 200);
|
2026-02-25 13:54:47 -08:00
|
|
|
|
}
|
2026-02-26 13:38:29 -08:00
|
|
|
|
drawList->AddRectFilled(pos, ImVec2(pos.x + SLOT_SIZE, pos.y + SLOT_SIZE), bgCol);
|
|
|
|
|
|
drawList->AddRect(pos, ImVec2(pos.x + SLOT_SIZE, pos.y + SLOT_SIZE), borderCol);
|
|
|
|
|
|
ImGui::InvisibleButton("slot", ImVec2(SLOT_SIZE, SLOT_SIZE));
|
|
|
|
|
|
if (isHolding && ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
|
|
|
|
|
inventoryScreen.dropIntoBankSlot(gameHandler, dstBag, dstSlot);
|
2026-02-25 13:54:47 -08:00
|
|
|
|
}
|
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-02-26 13:38:29 -08:00
|
|
|
|
const auto& item = slot.item;
|
|
|
|
|
|
ImVec4 qc = InventoryScreen::getQualityColor(item.quality);
|
|
|
|
|
|
ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qc);
|
|
|
|
|
|
VkDescriptorSet iconTex = inventoryScreen.getItemIcon(item.displayInfoId);
|
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
|
|
|
|
|
2026-02-26 13:38:29 -08:00
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
drawList->AddImage((ImTextureID)(uintptr_t)iconTex, pos,
|
|
|
|
|
|
ImVec2(pos.x + SLOT_SIZE, pos.y + SLOT_SIZE));
|
|
|
|
|
|
drawList->AddRect(pos, ImVec2(pos.x + SLOT_SIZE, pos.y + SLOT_SIZE),
|
|
|
|
|
|
borderCol, 0.0f, 0, 2.0f);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImU32 bgCol = IM_COL32(40, 35, 30, 220);
|
|
|
|
|
|
drawList->AddRectFilled(pos, ImVec2(pos.x + SLOT_SIZE, pos.y + SLOT_SIZE), bgCol);
|
|
|
|
|
|
drawList->AddRect(pos, ImVec2(pos.x + SLOT_SIZE, pos.y + SLOT_SIZE),
|
|
|
|
|
|
borderCol, 0.0f, 0, 2.0f);
|
|
|
|
|
|
if (!item.name.empty()) {
|
|
|
|
|
|
char abbr[3] = { item.name[0], item.name.size() > 1 ? item.name[1] : '\0', '\0' };
|
|
|
|
|
|
float tw = ImGui::CalcTextSize(abbr).x;
|
|
|
|
|
|
drawList->AddText(ImVec2(pos.x + (SLOT_SIZE - tw) * 0.5f, pos.y + 2.0f),
|
|
|
|
|
|
ImGui::ColorConvertFloat4ToU32(qc), abbr);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (item.stackCount > 1) {
|
|
|
|
|
|
char countStr[16];
|
|
|
|
|
|
snprintf(countStr, sizeof(countStr), "%u", item.stackCount);
|
|
|
|
|
|
float cw = ImGui::CalcTextSize(countStr).x;
|
|
|
|
|
|
drawList->AddText(ImVec2(pos.x + SLOT_SIZE - cw - 2.0f, pos.y + SLOT_SIZE - 14.0f),
|
|
|
|
|
|
IM_COL32(255, 255, 255, 220), countStr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::InvisibleButton("slot", ImVec2(SLOT_SIZE, SLOT_SIZE));
|
|
|
|
|
|
|
|
|
|
|
|
if (!isHolding) {
|
|
|
|
|
|
// Start pickup tracking on mouse press
|
|
|
|
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) {
|
|
|
|
|
|
bankPickupPending = true;
|
|
|
|
|
|
bankPickupPressTime = ImGui::GetTime();
|
|
|
|
|
|
bankPickupType = pickType;
|
|
|
|
|
|
bankPickupIndex = mainIdx;
|
|
|
|
|
|
bankPickupBagIndex = bagIdx;
|
|
|
|
|
|
bankPickupBagSlotIndex = bagSlotIdx;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Check if held long enough to pick up
|
|
|
|
|
|
if (bankPickupPending && ImGui::IsMouseDown(ImGuiMouseButton_Left) &&
|
|
|
|
|
|
(ImGui::GetTime() - bankPickupPressTime) >= kBankPickupHold) {
|
|
|
|
|
|
bool sameSlot = (bankPickupType == pickType);
|
|
|
|
|
|
if (pickType == 0)
|
|
|
|
|
|
sameSlot = sameSlot && (bankPickupIndex == mainIdx);
|
|
|
|
|
|
else if (pickType == 1)
|
|
|
|
|
|
sameSlot = sameSlot && (bankPickupBagIndex == bagIdx) && (bankPickupBagSlotIndex == bagSlotIdx);
|
|
|
|
|
|
else if (pickType == 2)
|
|
|
|
|
|
sameSlot = sameSlot && (bankPickupIndex == mainIdx);
|
|
|
|
|
|
|
|
|
|
|
|
if (sameSlot && ImGui::IsItemHovered()) {
|
|
|
|
|
|
bankPickupPending = false;
|
|
|
|
|
|
if (pickType == 0) {
|
|
|
|
|
|
inventoryScreen.pickupFromBank(inv, mainIdx);
|
|
|
|
|
|
} else if (pickType == 1) {
|
|
|
|
|
|
inventoryScreen.pickupFromBankBag(inv, bagIdx, bagSlotIdx);
|
|
|
|
|
|
} else if (pickType == 2) {
|
|
|
|
|
|
inventoryScreen.pickupFromBankBagEquip(inv, mainIdx);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// Drop/swap on mouse release
|
|
|
|
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
|
|
|
|
|
inventoryScreen.dropIntoBankSlot(gameHandler, dstBag, dstSlot);
|
|
|
|
|
|
}
|
2026-02-25 13:54:47 -08:00
|
|
|
|
}
|
2026-02-26 13:38:29 -08:00
|
|
|
|
|
|
|
|
|
|
// Tooltip
|
|
|
|
|
|
if (ImGui::IsItemHovered() && !isHolding) {
|
2026-03-11 21:15:41 -07:00
|
|
|
|
auto* info = gameHandler.getItemInfo(item.itemId);
|
|
|
|
|
|
if (info && info->valid)
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
|
|
|
|
|
else {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
ImGui::TextColored(qc, "%s", item.name.c_str());
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
2026-03-11 21:50:07 -07:00
|
|
|
|
|
|
|
|
|
|
// Shift-click to insert item link into chat
|
|
|
|
|
|
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) && ImGui::GetIO().KeyShift
|
|
|
|
|
|
&& !item.name.empty()) {
|
|
|
|
|
|
auto* info2 = gameHandler.getItemInfo(item.itemId);
|
|
|
|
|
|
uint8_t q = (info2 && info2->valid)
|
|
|
|
|
|
? static_cast<uint8_t>(info2->quality)
|
|
|
|
|
|
: static_cast<uint8_t>(item.quality);
|
|
|
|
|
|
const std::string& lname = (info2 && info2->valid && !info2->name.empty())
|
|
|
|
|
|
? info2->name : item.name;
|
|
|
|
|
|
std::string link = buildItemChatLink(item.itemId, q, lname);
|
2026-03-31 08:53:14 +03:00
|
|
|
|
chatPanel_.insertChatLink(link);
|
2026-03-11 21:50:07 -07:00
|
|
|
|
}
|
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
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-02-26 13:38:29 -08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Main bank slots (24 for Classic, 28 for TBC/WotLK)
|
|
|
|
|
|
int bankSlotCount = gameHandler.getEffectiveBankSlots();
|
|
|
|
|
|
int bankBagCount = gameHandler.getEffectiveBankBagSlots();
|
|
|
|
|
|
ImGui::Text("Bank Slots");
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
for (int i = 0; i < bankSlotCount; i++) {
|
|
|
|
|
|
if (i % 7 != 0) ImGui::SameLine();
|
|
|
|
|
|
ImGui::PushID(i + 1000);
|
|
|
|
|
|
renderBankItemSlot(inv.getBankSlot(i), 0, i, -1, -1, 0xFF, static_cast<uint8_t>(39 + i));
|
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
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 13:38:29 -08:00
|
|
|
|
// Bank bag equip slots — show bag icon with pickup/drop, or "Buy Slot"
|
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
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Text("Bank Bags");
|
|
|
|
|
|
uint8_t purchased = inv.getPurchasedBankBagSlots();
|
2026-02-26 11:12:34 -08:00
|
|
|
|
for (int i = 0; i < bankBagCount; i++) {
|
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
|
|
|
|
if (i > 0) ImGui::SameLine();
|
|
|
|
|
|
ImGui::PushID(i + 2000);
|
|
|
|
|
|
|
|
|
|
|
|
int bagSize = inv.getBankBagSize(i);
|
2026-02-26 13:38:29 -08:00
|
|
|
|
if (i < purchased || bagSize > 0) {
|
|
|
|
|
|
const auto& bagSlot = inv.getBankBagItem(i);
|
|
|
|
|
|
// Render as an item slot: icon with pickup/drop (pickType=2 for bag equip)
|
|
|
|
|
|
renderBankItemSlot(bagSlot, 2, i, -1, -1, 0xFF, static_cast<uint8_t>(67 + i));
|
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-02-26 13:38:29 -08:00
|
|
|
|
if (ImGui::Button("Buy Slot", ImVec2(50, 30))) {
|
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
|
|
|
|
gameHandler.buyBankSlot();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Show expanded bank bag contents
|
2026-02-26 11:12:34 -08:00
|
|
|
|
for (int bagIdx = 0; bagIdx < bankBagCount; bagIdx++) {
|
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
|
|
|
|
int bagSize = inv.getBankBagSize(bagIdx);
|
|
|
|
|
|
if (bagSize <= 0) continue;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::Text("Bank Bag %d (%d slots)", bagIdx + 1, bagSize);
|
|
|
|
|
|
for (int s = 0; s < bagSize; s++) {
|
|
|
|
|
|
if (s % 7 != 0) ImGui::SameLine();
|
|
|
|
|
|
ImGui::PushID(3000 + bagIdx * 100 + s);
|
2026-02-26 13:38:29 -08:00
|
|
|
|
renderBankItemSlot(inv.getBankBagSlot(bagIdx, s), 1, -1, bagIdx, s,
|
|
|
|
|
|
static_cast<uint8_t>(67 + bagIdx), static_cast<uint8_t>(s));
|
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
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
if (!open) gameHandler.closeBank();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Guild Bank Window
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderGuildBankWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isGuildBankOpen()) return;
|
|
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(520, 500), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
if (!ImGui::Begin("Guild Bank", &open)) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
if (!open) gameHandler.closeGuildBank();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const auto& data = gameHandler.getGuildBankData();
|
|
|
|
|
|
uint8_t activeTab = gameHandler.getGuildBankActiveTab();
|
|
|
|
|
|
|
|
|
|
|
|
// Money display
|
|
|
|
|
|
uint32_t gold = static_cast<uint32_t>(data.money / 10000);
|
|
|
|
|
|
uint32_t silver = static_cast<uint32_t>((data.money / 100) % 100);
|
|
|
|
|
|
uint32_t copper = static_cast<uint32_t>(data.money % 100);
|
2026-03-12 08:15:46 -07:00
|
|
|
|
ImGui::TextDisabled("Guild Bank Money:"); ImGui::SameLine(0, 4);
|
|
|
|
|
|
renderCoinsText(gold, silver, copper);
|
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
|
|
|
|
|
|
|
|
|
|
// Tab bar
|
|
|
|
|
|
if (!data.tabs.empty()) {
|
|
|
|
|
|
for (size_t i = 0; i < data.tabs.size(); i++) {
|
|
|
|
|
|
if (i > 0) ImGui::SameLine();
|
|
|
|
|
|
bool selected = (i == activeTab);
|
|
|
|
|
|
if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.5f, 0.8f, 1.0f));
|
|
|
|
|
|
std::string tabLabel = data.tabs[i].tabName.empty() ? ("Tab " + std::to_string(i + 1)) : data.tabs[i].tabName;
|
|
|
|
|
|
if (ImGui::Button(tabLabel.c_str())) {
|
|
|
|
|
|
gameHandler.queryGuildBankTab(static_cast<uint8_t>(i));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (selected) ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Buy tab button
|
|
|
|
|
|
if (data.tabs.size() < 6) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Buy Tab")) {
|
|
|
|
|
|
gameHandler.buyGuildBankTab();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
// Tab items (98 slots = 14 columns × 7 rows)
|
2026-03-11 20:33:46 -07:00
|
|
|
|
constexpr float GB_SLOT = 34.0f;
|
|
|
|
|
|
ImDrawList* gbDraw = ImGui::GetWindowDrawList();
|
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
|
|
|
|
for (size_t i = 0; i < data.tabItems.size(); i++) {
|
2026-03-11 20:33:46 -07:00
|
|
|
|
if (i % 14 != 0) ImGui::SameLine(0.0f, 2.0f);
|
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
|
|
|
|
const auto& item = data.tabItems[i];
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(i) + 5000);
|
|
|
|
|
|
|
2026-03-11 20:33:46 -07:00
|
|
|
|
ImVec2 pos = ImGui::GetCursorScreenPos();
|
|
|
|
|
|
|
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
|
|
|
|
if (item.itemEntry == 0) {
|
2026-03-11 20:33:46 -07:00
|
|
|
|
gbDraw->AddRectFilled(pos, ImVec2(pos.x + GB_SLOT, pos.y + GB_SLOT),
|
|
|
|
|
|
IM_COL32(30, 30, 30, 200));
|
|
|
|
|
|
gbDraw->AddRect(pos, ImVec2(pos.x + GB_SLOT, pos.y + GB_SLOT),
|
|
|
|
|
|
IM_COL32(60, 60, 60, 180));
|
|
|
|
|
|
ImGui::InvisibleButton("##gbempty", ImVec2(GB_SLOT, GB_SLOT));
|
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 {
|
|
|
|
|
|
auto* info = gameHandler.getItemInfo(item.itemEntry);
|
|
|
|
|
|
game::ItemQuality quality = game::ItemQuality::COMMON;
|
|
|
|
|
|
std::string name = "Item " + std::to_string(item.itemEntry);
|
2026-03-11 20:33:46 -07:00
|
|
|
|
uint32_t displayInfoId = 0;
|
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
|
|
|
|
if (info) {
|
|
|
|
|
|
quality = static_cast<game::ItemQuality>(info->quality);
|
|
|
|
|
|
name = info->name;
|
2026-03-11 20:33:46 -07:00
|
|
|
|
displayInfoId = info->displayInfoId;
|
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
|
|
|
|
}
|
|
|
|
|
|
ImVec4 qc = InventoryScreen::getQualityColor(quality);
|
2026-03-11 20:33:46 -07:00
|
|
|
|
ImU32 borderCol = ImGui::ColorConvertFloat4ToU32(qc);
|
|
|
|
|
|
|
|
|
|
|
|
VkDescriptorSet iconTex = displayInfoId ? inventoryScreen.getItemIcon(displayInfoId) : VK_NULL_HANDLE;
|
|
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
gbDraw->AddImage((ImTextureID)(uintptr_t)iconTex, pos,
|
|
|
|
|
|
ImVec2(pos.x + GB_SLOT, pos.y + GB_SLOT));
|
|
|
|
|
|
gbDraw->AddRect(pos, ImVec2(pos.x + GB_SLOT, pos.y + GB_SLOT),
|
|
|
|
|
|
borderCol, 0.0f, 0, 1.5f);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
gbDraw->AddRectFilled(pos, ImVec2(pos.x + GB_SLOT, pos.y + GB_SLOT),
|
|
|
|
|
|
IM_COL32(40, 35, 30, 220));
|
|
|
|
|
|
gbDraw->AddRect(pos, ImVec2(pos.x + GB_SLOT, pos.y + GB_SLOT),
|
|
|
|
|
|
borderCol, 0.0f, 0, 1.5f);
|
|
|
|
|
|
if (!name.empty() && name[0] != 'I') {
|
|
|
|
|
|
char abbr[3] = { name[0], name.size() > 1 ? name[1] : '\0', '\0' };
|
|
|
|
|
|
float tw = ImGui::CalcTextSize(abbr).x;
|
|
|
|
|
|
gbDraw->AddText(ImVec2(pos.x + (GB_SLOT - tw) * 0.5f, pos.y + 2.0f),
|
|
|
|
|
|
borderCol, abbr);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (item.stackCount > 1) {
|
|
|
|
|
|
char cnt[16];
|
|
|
|
|
|
snprintf(cnt, sizeof(cnt), "%u", item.stackCount);
|
|
|
|
|
|
float cw = ImGui::CalcTextSize(cnt).x;
|
|
|
|
|
|
gbDraw->AddText(ImVec2(pos.x + 1.0f, pos.y + 1.0f), IM_COL32(0, 0, 0, 200), cnt);
|
|
|
|
|
|
gbDraw->AddText(ImVec2(pos.x + GB_SLOT - cw - 2.0f, pos.y + GB_SLOT - 14.0f),
|
|
|
|
|
|
IM_COL32(255, 255, 255, 220), cnt);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::InvisibleButton("##gbslot", ImVec2(GB_SLOT, GB_SLOT));
|
2026-03-11 21:50:07 -07:00
|
|
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && !ImGui::GetIO().KeyShift) {
|
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
|
|
|
|
gameHandler.guildBankWithdrawItem(activeTab, item.slotId, 0xFF, 0);
|
|
|
|
|
|
}
|
2026-03-11 21:50:07 -07:00
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
if (info && info->valid)
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
|
|
|
|
|
// Shift-click to insert item link into chat
|
|
|
|
|
|
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left) && ImGui::GetIO().KeyShift
|
|
|
|
|
|
&& !name.empty() && item.itemEntry != 0) {
|
|
|
|
|
|
uint8_t q = static_cast<uint8_t>(quality);
|
|
|
|
|
|
std::string link = buildItemChatLink(item.itemEntry, q, name);
|
2026-03-31 08:53:14 +03:00
|
|
|
|
chatPanel_.insertChatLink(link);
|
2026-03-11 21:50:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
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
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Money deposit/withdraw
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Text("Money:");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetNextItemWidth(60);
|
|
|
|
|
|
ImGui::InputInt("##gbg", &guildBankMoneyInput_[0], 0); ImGui::SameLine(); ImGui::Text("g");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetNextItemWidth(40);
|
|
|
|
|
|
ImGui::InputInt("##gbs", &guildBankMoneyInput_[1], 0); ImGui::SameLine(); ImGui::Text("s");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetNextItemWidth(40);
|
|
|
|
|
|
ImGui::InputInt("##gbc", &guildBankMoneyInput_[2], 0); ImGui::SameLine(); ImGui::Text("c");
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Deposit")) {
|
|
|
|
|
|
uint32_t amount = guildBankMoneyInput_[0] * 10000 + guildBankMoneyInput_[1] * 100 + guildBankMoneyInput_[2];
|
|
|
|
|
|
if (amount > 0) gameHandler.depositGuildBankMoney(amount);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Withdraw")) {
|
|
|
|
|
|
uint32_t amount = guildBankMoneyInput_[0] * 10000 + guildBankMoneyInput_[1] * 100 + guildBankMoneyInput_[2];
|
|
|
|
|
|
if (amount > 0) gameHandler.withdrawGuildBankMoney(amount);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (data.withdrawAmount >= 0) {
|
|
|
|
|
|
ImGui::Text("Remaining withdrawals: %d", data.withdrawAmount);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
if (!open) gameHandler.closeGuildBank();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Auction House Window
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderAuctionHouseWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!gameHandler.isAuctionHouseOpen()) return;
|
|
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(650, 500), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
if (!ImGui::Begin("Auction House", &open)) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
if (!open) gameHandler.closeAuctionHouse();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int tab = gameHandler.getAuctionActiveTab();
|
|
|
|
|
|
|
|
|
|
|
|
// Tab buttons
|
|
|
|
|
|
const char* tabNames[] = {"Browse", "Bids", "Auctions"};
|
|
|
|
|
|
for (int i = 0; i < 3; i++) {
|
|
|
|
|
|
if (i > 0) ImGui::SameLine();
|
|
|
|
|
|
bool selected = (tab == i);
|
|
|
|
|
|
if (selected) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.3f, 0.5f, 0.8f, 1.0f));
|
|
|
|
|
|
if (ImGui::Button(tabNames[i], ImVec2(100, 0))) {
|
|
|
|
|
|
gameHandler.setAuctionActiveTab(i);
|
|
|
|
|
|
if (i == 1) gameHandler.auctionListBidderItems();
|
|
|
|
|
|
else if (i == 2) gameHandler.auctionListOwnerItems();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (selected) ImGui::PopStyleColor();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
if (tab == 0) {
|
|
|
|
|
|
// Browse tab - Search filters
|
2026-02-25 14:44:44 -08:00
|
|
|
|
|
|
|
|
|
|
// --- Helper: resolve current UI filter state into wire-format search params ---
|
|
|
|
|
|
// WoW 3.3.5a item class IDs:
|
|
|
|
|
|
// 0=Consumable, 1=Container, 2=Weapon, 3=Gem, 4=Armor,
|
|
|
|
|
|
// 7=Projectile/TradeGoods, 9=Recipe, 11=Quiver, 15=Miscellaneous
|
|
|
|
|
|
struct AHClassMapping { const char* label; uint32_t classId; };
|
|
|
|
|
|
static const AHClassMapping classMappings[] = {
|
|
|
|
|
|
{"All", 0xFFFFFFFF},
|
|
|
|
|
|
{"Weapon", 2},
|
|
|
|
|
|
{"Armor", 4},
|
|
|
|
|
|
{"Container", 1},
|
|
|
|
|
|
{"Consumable", 0},
|
|
|
|
|
|
{"Trade Goods", 7},
|
|
|
|
|
|
{"Gem", 3},
|
|
|
|
|
|
{"Recipe", 9},
|
|
|
|
|
|
{"Quiver", 11},
|
|
|
|
|
|
{"Miscellaneous", 15},
|
|
|
|
|
|
};
|
|
|
|
|
|
static constexpr int NUM_CLASSES = 10;
|
|
|
|
|
|
|
|
|
|
|
|
// Weapon subclass IDs (WoW 3.3.5a)
|
|
|
|
|
|
struct AHSubMapping { const char* label; uint32_t subId; };
|
|
|
|
|
|
static const AHSubMapping weaponSubs[] = {
|
|
|
|
|
|
{"All", 0xFFFFFFFF}, {"Axe (1H)", 0}, {"Axe (2H)", 1}, {"Bow", 2},
|
|
|
|
|
|
{"Gun", 3}, {"Mace (1H)", 4}, {"Mace (2H)", 5}, {"Polearm", 6},
|
|
|
|
|
|
{"Sword (1H)", 7}, {"Sword (2H)", 8}, {"Staff", 10},
|
|
|
|
|
|
{"Fist Weapon", 13}, {"Dagger", 15}, {"Thrown", 16},
|
|
|
|
|
|
{"Crossbow", 18}, {"Wand", 19},
|
|
|
|
|
|
};
|
|
|
|
|
|
static constexpr int NUM_WEAPON_SUBS = 16;
|
|
|
|
|
|
|
|
|
|
|
|
// Armor subclass IDs
|
|
|
|
|
|
static const AHSubMapping armorSubs[] = {
|
|
|
|
|
|
{"All", 0xFFFFFFFF}, {"Cloth", 1}, {"Leather", 2}, {"Mail", 3},
|
|
|
|
|
|
{"Plate", 4}, {"Shield", 6}, {"Miscellaneous", 0},
|
|
|
|
|
|
};
|
|
|
|
|
|
static constexpr int NUM_ARMOR_SUBS = 7;
|
|
|
|
|
|
|
|
|
|
|
|
auto getSearchClassId = [&]() -> uint32_t {
|
|
|
|
|
|
if (auctionItemClass_ < 0 || auctionItemClass_ >= NUM_CLASSES) return 0xFFFFFFFF;
|
|
|
|
|
|
return classMappings[auctionItemClass_].classId;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
auto getSearchSubClassId = [&]() -> uint32_t {
|
|
|
|
|
|
if (auctionItemSubClass_ < 0) return 0xFFFFFFFF;
|
|
|
|
|
|
uint32_t cid = getSearchClassId();
|
|
|
|
|
|
if (cid == 2 && auctionItemSubClass_ < NUM_WEAPON_SUBS)
|
|
|
|
|
|
return weaponSubs[auctionItemSubClass_].subId;
|
|
|
|
|
|
if (cid == 4 && auctionItemSubClass_ < NUM_ARMOR_SUBS)
|
|
|
|
|
|
return armorSubs[auctionItemSubClass_].subId;
|
|
|
|
|
|
return 0xFFFFFFFF;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
auto doSearch = [&](uint32_t offset) {
|
|
|
|
|
|
auctionBrowseOffset_ = offset;
|
2026-02-25 14:45:53 -08:00
|
|
|
|
if (auctionLevelMin_ < 0) auctionLevelMin_ = 0;
|
|
|
|
|
|
if (auctionLevelMax_ < 0) auctionLevelMax_ = 0;
|
2026-02-25 14:44:44 -08:00
|
|
|
|
uint32_t q = auctionQuality_ > 0 ? static_cast<uint32_t>(auctionQuality_ - 1) : 0xFFFFFFFF;
|
|
|
|
|
|
gameHandler.auctionSearch(auctionSearchName_,
|
|
|
|
|
|
static_cast<uint8_t>(auctionLevelMin_),
|
|
|
|
|
|
static_cast<uint8_t>(auctionLevelMax_),
|
2026-03-20 05:34:17 -07:00
|
|
|
|
q, getSearchClassId(), getSearchSubClassId(), 0,
|
|
|
|
|
|
auctionUsableOnly_ ? 1 : 0, offset);
|
2026-02-25 14:44:44 -08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Row 1: Name + Level range
|
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
|
|
|
|
ImGui::SetNextItemWidth(200);
|
2026-02-25 14:44:44 -08:00
|
|
|
|
bool enterPressed = ImGui::InputText("Name", auctionSearchName_, sizeof(auctionSearchName_),
|
|
|
|
|
|
ImGuiInputTextFlags_EnterReturnsTrue);
|
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
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetNextItemWidth(50);
|
|
|
|
|
|
ImGui::InputInt("Min Lv", &auctionLevelMin_, 0);
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetNextItemWidth(50);
|
|
|
|
|
|
ImGui::InputInt("Max Lv", &auctionLevelMax_, 0);
|
|
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
// Row 2: Quality + Category + Subcategory + Search button
|
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
|
|
|
|
const char* qualities[] = {"All", "Poor", "Common", "Uncommon", "Rare", "Epic", "Legendary"};
|
|
|
|
|
|
ImGui::SetNextItemWidth(100);
|
|
|
|
|
|
ImGui::Combo("Quality", &auctionQuality_, qualities, 7);
|
|
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
// Build class label list from mappings
|
|
|
|
|
|
const char* classLabels[NUM_CLASSES];
|
|
|
|
|
|
for (int c = 0; c < NUM_CLASSES; c++) classLabels[c] = classMappings[c].label;
|
|
|
|
|
|
ImGui::SetNextItemWidth(120);
|
|
|
|
|
|
int classIdx = auctionItemClass_ < 0 ? 0 : auctionItemClass_;
|
|
|
|
|
|
if (ImGui::Combo("Category", &classIdx, classLabels, NUM_CLASSES)) {
|
|
|
|
|
|
if (classIdx != auctionItemClass_) auctionItemSubClass_ = -1;
|
|
|
|
|
|
auctionItemClass_ = classIdx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Subcategory (only for Weapon and Armor)
|
|
|
|
|
|
uint32_t curClassId = getSearchClassId();
|
|
|
|
|
|
if (curClassId == 2 || curClassId == 4) {
|
|
|
|
|
|
const AHSubMapping* subs = (curClassId == 2) ? weaponSubs : armorSubs;
|
|
|
|
|
|
int numSubs = (curClassId == 2) ? NUM_WEAPON_SUBS : NUM_ARMOR_SUBS;
|
|
|
|
|
|
const char* subLabels[20];
|
|
|
|
|
|
for (int s = 0; s < numSubs && s < 20; s++) subLabels[s] = subs[s].label;
|
|
|
|
|
|
int subIdx = auctionItemSubClass_ + 1; // -1 → 0 ("All")
|
|
|
|
|
|
if (subIdx < 0 || subIdx >= numSubs) subIdx = 0;
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetNextItemWidth(110);
|
|
|
|
|
|
if (ImGui::Combo("Subcat", &subIdx, subLabels, numSubs)) {
|
|
|
|
|
|
auctionItemSubClass_ = subIdx - 1; // 0 → -1 ("All")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-20 05:34:17 -07:00
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::Checkbox("Usable", &auctionUsableOnly_);
|
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
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
float delay = gameHandler.getAuctionSearchDelay();
|
|
|
|
|
|
if (delay > 0.0f) {
|
2026-02-25 14:44:44 -08:00
|
|
|
|
char delayBuf[32];
|
|
|
|
|
|
snprintf(delayBuf, sizeof(delayBuf), "Search (%.0fs)", delay);
|
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
|
|
|
|
ImGui::BeginDisabled();
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::Button(delayBuf);
|
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
|
|
|
|
ImGui::EndDisabled();
|
|
|
|
|
|
} else {
|
2026-02-25 14:44:44 -08:00
|
|
|
|
if (ImGui::Button("Search") || enterPressed) {
|
|
|
|
|
|
doSearch(0);
|
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
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
// Results table
|
|
|
|
|
|
const auto& results = gameHandler.getAuctionBrowseResults();
|
2026-02-25 14:44:44 -08:00
|
|
|
|
constexpr uint32_t AH_PAGE_SIZE = 50;
|
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
|
|
|
|
ImGui::Text("%zu results (of %u total)", results.auctions.size(), results.totalCount);
|
|
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
// Pagination
|
|
|
|
|
|
if (results.totalCount > AH_PAGE_SIZE) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
uint32_t page = auctionBrowseOffset_ / AH_PAGE_SIZE + 1;
|
|
|
|
|
|
uint32_t totalPages = (results.totalCount + AH_PAGE_SIZE - 1) / AH_PAGE_SIZE;
|
|
|
|
|
|
|
|
|
|
|
|
if (auctionBrowseOffset_ == 0) ImGui::BeginDisabled();
|
|
|
|
|
|
if (ImGui::SmallButton("< Prev")) {
|
|
|
|
|
|
uint32_t newOff = (auctionBrowseOffset_ >= AH_PAGE_SIZE) ? auctionBrowseOffset_ - AH_PAGE_SIZE : 0;
|
|
|
|
|
|
doSearch(newOff);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (auctionBrowseOffset_ == 0) ImGui::EndDisabled();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::Text("Page %u/%u", page, totalPages);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (auctionBrowseOffset_ + AH_PAGE_SIZE >= results.totalCount) ImGui::BeginDisabled();
|
|
|
|
|
|
if (ImGui::SmallButton("Next >")) {
|
|
|
|
|
|
doSearch(auctionBrowseOffset_ + AH_PAGE_SIZE);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (auctionBrowseOffset_ + AH_PAGE_SIZE >= results.totalCount) ImGui::EndDisabled();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::BeginChild("AuctionResults", ImVec2(0, -110), true)) {
|
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
|
|
|
|
if (ImGui::BeginTable("AuctionTable", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY)) {
|
|
|
|
|
|
ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
|
|
ImGui::TableSetupColumn("Qty", ImGuiTableColumnFlags_WidthFixed, 40);
|
|
|
|
|
|
ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_WidthFixed, 60);
|
|
|
|
|
|
ImGui::TableSetupColumn("Bid", ImGuiTableColumnFlags_WidthFixed, 90);
|
|
|
|
|
|
ImGui::TableSetupColumn("Buyout", ImGuiTableColumnFlags_WidthFixed, 90);
|
|
|
|
|
|
ImGui::TableSetupColumn("##act", ImGuiTableColumnFlags_WidthFixed, 60);
|
|
|
|
|
|
ImGui::TableHeadersRow();
|
|
|
|
|
|
|
|
|
|
|
|
for (size_t i = 0; i < results.auctions.size(); i++) {
|
|
|
|
|
|
const auto& auction = results.auctions[i];
|
|
|
|
|
|
auto* info = gameHandler.getItemInfo(auction.itemEntry);
|
|
|
|
|
|
std::string name = info ? info->name : ("Item #" + std::to_string(auction.itemEntry));
|
2026-03-20 19:33:01 -07:00
|
|
|
|
// Append random suffix name (e.g., "of the Eagle") if present
|
|
|
|
|
|
if (auction.randomPropertyId != 0) {
|
|
|
|
|
|
std::string suffix = gameHandler.getRandomPropertyName(
|
|
|
|
|
|
static_cast<int32_t>(auction.randomPropertyId));
|
|
|
|
|
|
if (!suffix.empty()) name += " " + suffix;
|
|
|
|
|
|
}
|
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
|
|
|
|
game::ItemQuality quality = info ? static_cast<game::ItemQuality>(info->quality) : game::ItemQuality::COMMON;
|
|
|
|
|
|
ImVec4 qc = InventoryScreen::getQualityColor(quality);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
2026-02-25 14:44:44 -08:00
|
|
|
|
// Item icon
|
|
|
|
|
|
if (info && info->valid && info->displayInfoId != 0) {
|
|
|
|
|
|
VkDescriptorSet iconTex = inventoryScreen.getItemIcon(info->displayInfoId);
|
|
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
ImGui::Image((void*)(intptr_t)iconTex, ImVec2(16, 16));
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
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
|
|
|
|
ImGui::TextColored(qc, "%s", name.c_str());
|
2026-03-11 21:31:09 -07:00
|
|
|
|
// Item tooltip on hover; shift-click to insert chat link
|
2026-02-25 14:44:44 -08:00
|
|
|
|
if (ImGui::IsItemHovered() && info && info->valid) {
|
2026-03-11 21:02:02 -07:00
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
2026-02-25 14:44:44 -08:00
|
|
|
|
}
|
2026-03-11 21:31:09 -07:00
|
|
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
|
|
|
|
|
ImGui::GetIO().KeyShift && info && info->valid && !info->name.empty()) {
|
|
|
|
|
|
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
|
2026-03-31 08:53:14 +03:00
|
|
|
|
chatPanel_.insertChatLink(link);
|
2026-03-11 21:31:09 -07:00
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
|
|
|
|
ImGui::Text("%u", auction.stackCount);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
|
|
|
|
|
// Time left display
|
|
|
|
|
|
uint32_t mins = auction.timeLeftMs / 60000;
|
|
|
|
|
|
if (mins > 720) ImGui::Text("Long");
|
|
|
|
|
|
else if (mins > 120) ImGui::Text("Medium");
|
|
|
|
|
|
else ImGui::TextColored(ImVec4(1, 0.3f, 0.3f, 1), "Short");
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(3);
|
|
|
|
|
|
{
|
|
|
|
|
|
uint32_t bid = auction.currentBid > 0 ? auction.currentBid : auction.startBid;
|
2026-03-25 12:59:31 -07:00
|
|
|
|
renderCoinsFromCopper(bid);
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(4);
|
|
|
|
|
|
if (auction.buyoutPrice > 0) {
|
2026-03-25 12:59:31 -07:00
|
|
|
|
renderCoinsFromCopper(auction.buyoutPrice);
|
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 {
|
|
|
|
|
|
ImGui::TextDisabled("--");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(5);
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(i) + 7000);
|
|
|
|
|
|
if (auction.buyoutPrice > 0 && ImGui::SmallButton("Buy")) {
|
|
|
|
|
|
gameHandler.auctionBuyout(auction.auctionId, auction.buyoutPrice);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (auction.buyoutPrice > 0) ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::SmallButton("Bid")) {
|
|
|
|
|
|
uint32_t bidAmt = auction.currentBid > 0
|
|
|
|
|
|
? auction.currentBid + auction.minBidIncrement
|
|
|
|
|
|
: auction.startBid;
|
|
|
|
|
|
gameHandler.auctionPlaceBid(auction.auctionId, bidAmt);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
|
|
|
|
|
|
// Sell section
|
|
|
|
|
|
ImGui::Separator();
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::Text("Sell Item:");
|
|
|
|
|
|
|
|
|
|
|
|
// Item picker from backpack
|
|
|
|
|
|
{
|
|
|
|
|
|
auto& inv = gameHandler.getInventory();
|
|
|
|
|
|
// Build list of non-empty backpack slots
|
|
|
|
|
|
std::string preview = (auctionSellSlotIndex_ >= 0)
|
|
|
|
|
|
? ([&]() -> std::string {
|
|
|
|
|
|
const auto& slot = inv.getBackpackSlot(auctionSellSlotIndex_);
|
|
|
|
|
|
if (!slot.empty()) {
|
|
|
|
|
|
std::string s = slot.item.name;
|
|
|
|
|
|
if (slot.item.stackCount > 1) s += " x" + std::to_string(slot.item.stackCount);
|
|
|
|
|
|
return s;
|
|
|
|
|
|
}
|
|
|
|
|
|
return "Select item...";
|
|
|
|
|
|
})()
|
|
|
|
|
|
: "Select item...";
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextItemWidth(250);
|
|
|
|
|
|
if (ImGui::BeginCombo("##sellitem", preview.c_str())) {
|
|
|
|
|
|
for (int i = 0; i < game::Inventory::BACKPACK_SLOTS; i++) {
|
|
|
|
|
|
const auto& slot = inv.getBackpackSlot(i);
|
|
|
|
|
|
if (slot.empty()) continue;
|
|
|
|
|
|
ImGui::PushID(i + 9000);
|
|
|
|
|
|
// Item icon
|
|
|
|
|
|
if (slot.item.displayInfoId != 0) {
|
|
|
|
|
|
VkDescriptorSet sIcon = inventoryScreen.getItemIcon(slot.item.displayInfoId);
|
|
|
|
|
|
if (sIcon) {
|
|
|
|
|
|
ImGui::Image((void*)(intptr_t)sIcon, ImVec2(16, 16));
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
std::string label = slot.item.name;
|
|
|
|
|
|
if (slot.item.stackCount > 1) label += " x" + std::to_string(slot.item.stackCount);
|
|
|
|
|
|
ImVec4 iqc = InventoryScreen::getQualityColor(slot.item.quality);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, iqc);
|
|
|
|
|
|
if (ImGui::Selectable(label.c_str(), auctionSellSlotIndex_ == i)) {
|
|
|
|
|
|
auctionSellSlotIndex_ = i;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndCombo();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
ImGui::Text("Bid:");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetNextItemWidth(50);
|
|
|
|
|
|
ImGui::InputInt("##sbg", &auctionSellBid_[0], 0); ImGui::SameLine(); ImGui::Text("g");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetNextItemWidth(35);
|
|
|
|
|
|
ImGui::InputInt("##sbs", &auctionSellBid_[1], 0); ImGui::SameLine(); ImGui::Text("s");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetNextItemWidth(35);
|
|
|
|
|
|
ImGui::InputInt("##sbc", &auctionSellBid_[2], 0); ImGui::SameLine(); ImGui::Text("c");
|
|
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::SameLine(0, 20);
|
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
|
|
|
|
ImGui::Text("Buyout:");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetNextItemWidth(50);
|
|
|
|
|
|
ImGui::InputInt("##sbog", &auctionSellBuyout_[0], 0); ImGui::SameLine(); ImGui::Text("g");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetNextItemWidth(35);
|
|
|
|
|
|
ImGui::InputInt("##sbos", &auctionSellBuyout_[1], 0); ImGui::SameLine(); ImGui::Text("s");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetNextItemWidth(35);
|
|
|
|
|
|
ImGui::InputInt("##sboc", &auctionSellBuyout_[2], 0); ImGui::SameLine(); ImGui::Text("c");
|
|
|
|
|
|
|
|
|
|
|
|
const char* durations[] = {"12 hours", "24 hours", "48 hours"};
|
|
|
|
|
|
ImGui::SetNextItemWidth(90);
|
|
|
|
|
|
ImGui::Combo("##dur", &auctionSellDuration_, durations, 3);
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
|
|
|
|
|
|
// Create Auction button
|
|
|
|
|
|
bool canCreate = auctionSellSlotIndex_ >= 0 &&
|
|
|
|
|
|
!gameHandler.getInventory().getBackpackSlot(auctionSellSlotIndex_).empty() &&
|
|
|
|
|
|
(auctionSellBid_[0] > 0 || auctionSellBid_[1] > 0 || auctionSellBid_[2] > 0);
|
|
|
|
|
|
if (!canCreate) ImGui::BeginDisabled();
|
|
|
|
|
|
if (ImGui::Button("Create Auction")) {
|
|
|
|
|
|
uint32_t bidCopper = static_cast<uint32_t>(auctionSellBid_[0]) * 10000
|
|
|
|
|
|
+ static_cast<uint32_t>(auctionSellBid_[1]) * 100
|
|
|
|
|
|
+ static_cast<uint32_t>(auctionSellBid_[2]);
|
|
|
|
|
|
uint32_t buyoutCopper = static_cast<uint32_t>(auctionSellBuyout_[0]) * 10000
|
|
|
|
|
|
+ static_cast<uint32_t>(auctionSellBuyout_[1]) * 100
|
|
|
|
|
|
+ static_cast<uint32_t>(auctionSellBuyout_[2]);
|
|
|
|
|
|
const uint32_t durationMins[] = {720, 1440, 2880};
|
|
|
|
|
|
uint32_t dur = durationMins[auctionSellDuration_];
|
|
|
|
|
|
uint64_t itemGuid = gameHandler.getBackpackItemGuid(auctionSellSlotIndex_);
|
|
|
|
|
|
const auto& slot = gameHandler.getInventory().getBackpackSlot(auctionSellSlotIndex_);
|
|
|
|
|
|
uint32_t stackCount = slot.item.stackCount;
|
|
|
|
|
|
if (itemGuid != 0) {
|
|
|
|
|
|
gameHandler.auctionSellItem(itemGuid, stackCount, bidCopper, buyoutCopper, dur);
|
|
|
|
|
|
// Clear sell inputs
|
|
|
|
|
|
auctionSellSlotIndex_ = -1;
|
|
|
|
|
|
auctionSellBid_[0] = auctionSellBid_[1] = auctionSellBid_[2] = 0;
|
|
|
|
|
|
auctionSellBuyout_[0] = auctionSellBuyout_[1] = auctionSellBuyout_[2] = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!canCreate) ImGui::EndDisabled();
|
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 if (tab == 1) {
|
|
|
|
|
|
// Bids tab
|
|
|
|
|
|
const auto& results = gameHandler.getAuctionBidderResults();
|
|
|
|
|
|
ImGui::Text("Your Bids: %zu items", results.auctions.size());
|
|
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
if (ImGui::BeginTable("BidTable", 6, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
|
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
|
|
|
|
ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
|
|
ImGui::TableSetupColumn("Qty", ImGuiTableColumnFlags_WidthFixed, 40);
|
|
|
|
|
|
ImGui::TableSetupColumn("Your Bid", ImGuiTableColumnFlags_WidthFixed, 90);
|
|
|
|
|
|
ImGui::TableSetupColumn("Buyout", ImGuiTableColumnFlags_WidthFixed, 90);
|
|
|
|
|
|
ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_WidthFixed, 60);
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::TableSetupColumn("##act", ImGuiTableColumnFlags_WidthFixed, 60);
|
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
|
|
|
|
ImGui::TableHeadersRow();
|
|
|
|
|
|
|
2026-02-25 14:44:44 -08:00
|
|
|
|
for (size_t bi = 0; bi < results.auctions.size(); bi++) {
|
|
|
|
|
|
const auto& a = results.auctions[bi];
|
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
|
|
|
|
auto* info = gameHandler.getItemInfo(a.itemEntry);
|
|
|
|
|
|
std::string name = info ? info->name : ("Item #" + std::to_string(a.itemEntry));
|
2026-03-20 19:37:17 -07:00
|
|
|
|
if (a.randomPropertyId != 0) {
|
|
|
|
|
|
std::string suffix = gameHandler.getRandomPropertyName(
|
|
|
|
|
|
static_cast<int32_t>(a.randomPropertyId));
|
|
|
|
|
|
if (!suffix.empty()) name += " " + suffix;
|
|
|
|
|
|
}
|
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
|
|
|
|
game::ItemQuality quality = info ? static_cast<game::ItemQuality>(info->quality) : game::ItemQuality::COMMON;
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImVec4 bqc = InventoryScreen::getQualityColor(quality);
|
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
|
|
|
|
|
|
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
2026-02-25 14:44:44 -08:00
|
|
|
|
if (info && info->valid && info->displayInfoId != 0) {
|
|
|
|
|
|
VkDescriptorSet bIcon = inventoryScreen.getItemIcon(info->displayInfoId);
|
|
|
|
|
|
if (bIcon) {
|
|
|
|
|
|
ImGui::Image((void*)(intptr_t)bIcon, ImVec2(16, 16));
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-20 15:31:41 -07:00
|
|
|
|
// High bidder indicator
|
|
|
|
|
|
bool isHighBidder = (a.bidderGuid != 0 && a.bidderGuid == gameHandler.getPlayerGuid());
|
|
|
|
|
|
if (isHighBidder) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.2f, 0.9f, 0.2f, 1.0f), "[Winning]");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
} else if (a.bidderGuid != 0) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.9f, 0.3f, 0.3f, 1.0f), "[Outbid]");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
}
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::TextColored(bqc, "%s", name.c_str());
|
2026-03-11 21:34:28 -07:00
|
|
|
|
// Tooltip and shift-click
|
2026-03-11 21:02:02 -07:00
|
|
|
|
if (ImGui::IsItemHovered() && info && info->valid)
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
2026-03-11 21:34:28 -07:00
|
|
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
|
|
|
|
|
ImGui::GetIO().KeyShift && info && info->valid && !info->name.empty()) {
|
|
|
|
|
|
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
|
2026-03-31 08:53:14 +03:00
|
|
|
|
chatPanel_.insertChatLink(link);
|
2026-03-11 21:34:28 -07:00
|
|
|
|
}
|
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
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
|
|
|
|
ImGui::Text("%u", a.stackCount);
|
|
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
2026-03-25 12:59:31 -07:00
|
|
|
|
renderCoinsFromCopper(a.currentBid);
|
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
|
|
|
|
ImGui::TableSetColumnIndex(3);
|
|
|
|
|
|
if (a.buyoutPrice > 0)
|
2026-03-25 12:59:31 -07:00
|
|
|
|
renderCoinsFromCopper(a.buyoutPrice);
|
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
|
|
|
|
|
|
ImGui::TextDisabled("--");
|
|
|
|
|
|
ImGui::TableSetColumnIndex(4);
|
|
|
|
|
|
uint32_t mins = a.timeLeftMs / 60000;
|
|
|
|
|
|
if (mins > 720) ImGui::Text("Long");
|
|
|
|
|
|
else if (mins > 120) ImGui::Text("Medium");
|
|
|
|
|
|
else ImGui::TextColored(ImVec4(1, 0.3f, 0.3f, 1), "Short");
|
2026-02-25 14:44:44 -08:00
|
|
|
|
|
|
|
|
|
|
ImGui::TableSetColumnIndex(5);
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(bi) + 7500);
|
|
|
|
|
|
if (a.buyoutPrice > 0 && ImGui::SmallButton("Buy")) {
|
|
|
|
|
|
gameHandler.auctionBuyout(a.auctionId, a.buyoutPrice);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (a.buyoutPrice > 0) ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::SmallButton("Bid")) {
|
|
|
|
|
|
uint32_t bidAmt = a.currentBid > 0
|
|
|
|
|
|
? a.currentBid + a.minBidIncrement
|
|
|
|
|
|
: a.startBid;
|
|
|
|
|
|
gameHandler.auctionPlaceBid(a.auctionId, bidAmt);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopID();
|
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
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} else if (tab == 2) {
|
|
|
|
|
|
// Auctions tab (your listings)
|
|
|
|
|
|
const auto& results = gameHandler.getAuctionOwnerResults();
|
|
|
|
|
|
ImGui::Text("Your Auctions: %zu items", results.auctions.size());
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::BeginTable("OwnerTable", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg)) {
|
|
|
|
|
|
ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
|
|
ImGui::TableSetupColumn("Qty", ImGuiTableColumnFlags_WidthFixed, 40);
|
|
|
|
|
|
ImGui::TableSetupColumn("Bid", ImGuiTableColumnFlags_WidthFixed, 90);
|
|
|
|
|
|
ImGui::TableSetupColumn("Buyout", ImGuiTableColumnFlags_WidthFixed, 90);
|
|
|
|
|
|
ImGui::TableSetupColumn("##cancel", ImGuiTableColumnFlags_WidthFixed, 60);
|
|
|
|
|
|
ImGui::TableHeadersRow();
|
|
|
|
|
|
|
|
|
|
|
|
for (size_t i = 0; i < results.auctions.size(); i++) {
|
|
|
|
|
|
const auto& a = results.auctions[i];
|
|
|
|
|
|
auto* info = gameHandler.getItemInfo(a.itemEntry);
|
|
|
|
|
|
std::string name = info ? info->name : ("Item #" + std::to_string(a.itemEntry));
|
2026-03-20 19:37:17 -07:00
|
|
|
|
if (a.randomPropertyId != 0) {
|
|
|
|
|
|
std::string suffix = gameHandler.getRandomPropertyName(
|
|
|
|
|
|
static_cast<int32_t>(a.randomPropertyId));
|
|
|
|
|
|
if (!suffix.empty()) name += " " + suffix;
|
|
|
|
|
|
}
|
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
|
|
|
|
game::ItemQuality quality = info ? static_cast<game::ItemQuality>(info->quality) : game::ItemQuality::COMMON;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImVec4 oqc = InventoryScreen::getQualityColor(quality);
|
|
|
|
|
|
if (info && info->valid && info->displayInfoId != 0) {
|
|
|
|
|
|
VkDescriptorSet oIcon = inventoryScreen.getItemIcon(info->displayInfoId);
|
|
|
|
|
|
if (oIcon) {
|
|
|
|
|
|
ImGui::Image((void*)(intptr_t)oIcon, ImVec2(16, 16));
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-20 15:31:41 -07:00
|
|
|
|
// Bid activity indicator for seller
|
|
|
|
|
|
if (a.bidderGuid != 0) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.2f, 1.0f), "[Bid]");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
}
|
2026-02-25 14:44:44 -08:00
|
|
|
|
ImGui::TextColored(oqc, "%s", name.c_str());
|
2026-03-11 21:02:02 -07:00
|
|
|
|
if (ImGui::IsItemHovered() && info && info->valid)
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
2026-03-11 21:34:28 -07:00
|
|
|
|
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
|
|
|
|
|
|
ImGui::GetIO().KeyShift && info && info->valid && !info->name.empty()) {
|
|
|
|
|
|
std::string link = buildItemChatLink(info->entry, info->quality, info->name);
|
2026-03-31 08:53:14 +03:00
|
|
|
|
chatPanel_.insertChatLink(link);
|
2026-03-11 21:34:28 -07:00
|
|
|
|
}
|
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
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
|
|
|
|
ImGui::Text("%u", a.stackCount);
|
|
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
|
|
|
|
|
{
|
|
|
|
|
|
uint32_t bid = a.currentBid > 0 ? a.currentBid : a.startBid;
|
2026-03-25 12:59:31 -07:00
|
|
|
|
renderCoinsFromCopper(bid);
|
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
|
|
|
|
}
|
|
|
|
|
|
ImGui::TableSetColumnIndex(3);
|
|
|
|
|
|
if (a.buyoutPrice > 0)
|
2026-03-25 12:59:31 -07:00
|
|
|
|
renderCoinsFromCopper(a.buyoutPrice);
|
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
|
|
|
|
|
|
ImGui::TextDisabled("--");
|
|
|
|
|
|
ImGui::TableSetColumnIndex(4);
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(i) + 8000);
|
|
|
|
|
|
if (ImGui::SmallButton("Cancel")) {
|
|
|
|
|
|
gameHandler.auctionCancelItem(a.auctionId);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
|
|
|
|
|
|
if (!open) gameHandler.closeAuctionHouse();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 17:06:12 -07:00
|
|
|
|
|
2026-03-17 16:34:39 -07: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;
|
|
|
|
|
|
|
|
|
|
|
|
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-29 21:01:51 -07: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-17 16:34:39 -07:00
|
|
|
|
ImDrawList* dl = ImGui::GetForegroundDrawList();
|
|
|
|
|
|
const float dt = std::min(io.DeltaTime, 0.05f); // cap delta at 50ms to avoid teleporting particles
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
// 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) {
|
2026-03-29 21:01:51 -07:00
|
|
|
|
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)));
|
2026-03-17 16:34:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
rs.initialized = true;
|
|
|
|
|
|
rs.lastType = wType;
|
|
|
|
|
|
rs.lastW = sw;
|
|
|
|
|
|
rs.lastH = sh;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
2026-03-29 21:01:51 -07:00
|
|
|
|
rs.x[i] = static_cast<float>(wxRandInt(static_cast<int>(sw) + 200)) - 100.0f;
|
2026-03-17 16:34:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} 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;
|
|
|
|
|
|
|
|
|
|
|
|
if (!ss.initialized || ss.lastW != sw || ss.lastH != sh) {
|
|
|
|
|
|
for (int i = 0; i < MAX_FLAKES; ++i) {
|
2026-03-29 21:01:51 -07:00
|
|
|
|
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;
|
2026-03-17 16:34:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
ss.initialized = true;
|
|
|
|
|
|
ss.lastW = sw;
|
|
|
|
|
|
ss.lastH = sh;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
2026-03-29 21:01:51 -07:00
|
|
|
|
ss.x[i] = static_cast<float>(wxRandInt(static_cast<int>(sw)));
|
2026-03-17 16:34:39 -07:00
|
|
|
|
}
|
|
|
|
|
|
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-09 13:47:07 -07:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Dungeon Finder window (toggle with hotkey or bag-bar button)
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
void GameScreen::renderDungeonFinderWindow(game::GameHandler& gameHandler) {
|
2026-03-11 06:51:48 -07:00
|
|
|
|
// Toggle Dungeon Finder (customizable keybind)
|
2026-03-31 08:53:14 +03:00
|
|
|
|
if (!chatPanel_.isChatInputActive() && !ImGui::GetIO().WantTextInput &&
|
2026-03-14 03:49:42 -07:00
|
|
|
|
KeybindingManager::getInstance().isActionPressed(KeybindingManager::Action::TOGGLE_DUNGEON_FINDER)) {
|
2026-03-09 13:47:07 -07:00
|
|
|
|
showDungeonFinder_ = !showDungeonFinder_;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!showDungeonFinder_) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
float screenH = window ? static_cast<float>(window->getHeight()) : 720.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(screenW * 0.5f - 175.0f, screenH * 0.2f),
|
|
|
|
|
|
ImGuiCond_FirstUseEver);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(350, 0), ImGuiCond_Always);
|
|
|
|
|
|
|
|
|
|
|
|
bool open = true;
|
|
|
|
|
|
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize;
|
|
|
|
|
|
if (!ImGui::Begin("Dungeon Finder", &open, flags)) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
if (!open) showDungeonFinder_ = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
showDungeonFinder_ = false;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
using LfgState = game::GameHandler::LfgState;
|
|
|
|
|
|
LfgState state = gameHandler.getLfgState();
|
|
|
|
|
|
|
|
|
|
|
|
// ---- Status banner ----
|
|
|
|
|
|
switch (state) {
|
|
|
|
|
|
case LfgState::None:
|
2026-03-25 11:57:22 -07:00
|
|
|
|
ImGui::TextColored(kColorGray, "Status: Not queued");
|
2026-03-09 13:47:07 -07:00
|
|
|
|
break;
|
|
|
|
|
|
case LfgState::RoleCheck:
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "Status: Role check in progress...");
|
|
|
|
|
|
break;
|
|
|
|
|
|
case LfgState::Queued: {
|
|
|
|
|
|
int32_t avgSec = gameHandler.getLfgAvgWaitSec();
|
|
|
|
|
|
uint32_t qMs = gameHandler.getLfgTimeInQueueMs();
|
|
|
|
|
|
int qMin = static_cast<int>(qMs / 60000);
|
|
|
|
|
|
int qSec = static_cast<int>((qMs % 60000) / 1000);
|
2026-03-13 08:16:59 -07:00
|
|
|
|
std::string dName = gameHandler.getCurrentLfgDungeonName();
|
|
|
|
|
|
if (!dName.empty())
|
2026-03-27 15:01:12 -07:00
|
|
|
|
ImGui::TextColored(colors::kQueueGreen,
|
2026-03-13 08:16:59 -07:00
|
|
|
|
"Status: In queue for %s (%d:%02d)", dName.c_str(), qMin, qSec);
|
|
|
|
|
|
else
|
2026-03-27 15:01:12 -07:00
|
|
|
|
ImGui::TextColored(colors::kQueueGreen, "Status: In queue (%d:%02d)", qMin, qSec);
|
2026-03-09 13:47:07 -07:00
|
|
|
|
if (avgSec >= 0) {
|
|
|
|
|
|
int aMin = avgSec / 60;
|
|
|
|
|
|
int aSec = avgSec % 60;
|
2026-03-27 14:00:15 -07:00
|
|
|
|
ImGui::TextColored(colors::kSilver,
|
2026-03-09 13:47:07 -07:00
|
|
|
|
"Avg wait: %d:%02d", aMin, aSec);
|
|
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2026-03-13 08:16:59 -07:00
|
|
|
|
case LfgState::Proposal: {
|
|
|
|
|
|
std::string dName = gameHandler.getCurrentLfgDungeonName();
|
|
|
|
|
|
if (!dName.empty())
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.1f, 1.0f), "Status: Group found for %s!", dName.c_str());
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.5f, 0.1f, 1.0f), "Status: Group found!");
|
2026-03-09 13:47:07 -07:00
|
|
|
|
break;
|
2026-03-13 08:16:59 -07:00
|
|
|
|
}
|
2026-03-09 13:47:07 -07:00
|
|
|
|
case LfgState::Boot:
|
2026-03-25 11:57:22 -07:00
|
|
|
|
ImGui::TextColored(kColorRed, "Status: Vote kick in progress");
|
2026-03-09 13:47:07 -07:00
|
|
|
|
break;
|
2026-03-13 08:16:59 -07:00
|
|
|
|
case LfgState::InDungeon: {
|
|
|
|
|
|
std::string dName = gameHandler.getCurrentLfgDungeonName();
|
|
|
|
|
|
if (!dName.empty())
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "Status: In dungeon (%s)", dName.c_str());
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 0.8f, 1.0f, 1.0f), "Status: In dungeon");
|
2026-03-09 13:47:07 -07:00
|
|
|
|
break;
|
2026-03-13 08:16:59 -07:00
|
|
|
|
}
|
|
|
|
|
|
case LfgState::FinishedDungeon: {
|
|
|
|
|
|
std::string dName = gameHandler.getCurrentLfgDungeonName();
|
|
|
|
|
|
if (!dName.empty())
|
2026-03-27 13:57:29 -07:00
|
|
|
|
ImGui::TextColored(colors::kLightGreen, "Status: %s complete", dName.c_str());
|
2026-03-13 08:16:59 -07:00
|
|
|
|
else
|
2026-03-27 13:57:29 -07:00
|
|
|
|
ImGui::TextColored(colors::kLightGreen, "Status: Dungeon complete");
|
2026-03-09 13:47:07 -07:00
|
|
|
|
break;
|
2026-03-13 08:16:59 -07:00
|
|
|
|
}
|
2026-03-09 13:47:07 -07:00
|
|
|
|
case LfgState::RaidBrowser:
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.8f, 0.6f, 1.0f, 1.0f), "Status: Raid browser");
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
// ---- Proposal accept/decline ----
|
|
|
|
|
|
if (state == LfgState::Proposal) {
|
2026-03-13 08:16:59 -07:00
|
|
|
|
std::string dName = gameHandler.getCurrentLfgDungeonName();
|
|
|
|
|
|
if (!dName.empty())
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.3f, 1.0f),
|
|
|
|
|
|
"A group has been found for %s!", dName.c_str());
|
|
|
|
|
|
else
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.3f, 1.0f),
|
|
|
|
|
|
"A group has been found for your dungeon!");
|
2026-03-09 13:47:07 -07:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
if (ImGui::Button("Accept", ImVec2(120, 0))) {
|
|
|
|
|
|
gameHandler.lfgAcceptProposal(gameHandler.getLfgProposalId(), true);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Decline", ImVec2(120, 0))) {
|
|
|
|
|
|
gameHandler.lfgAcceptProposal(gameHandler.getLfgProposalId(), false);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-10 12:08:58 -07:00
|
|
|
|
// ---- Vote-to-kick buttons ----
|
|
|
|
|
|
if (state == LfgState::Boot) {
|
2026-03-25 11:57:22 -07:00
|
|
|
|
ImGui::TextColored(kColorRed, "Vote to kick in progress:");
|
2026-03-12 09:09:41 -07:00
|
|
|
|
const std::string& bootTarget = gameHandler.getLfgBootTargetName();
|
|
|
|
|
|
const std::string& bootReason = gameHandler.getLfgBootReason();
|
|
|
|
|
|
if (!bootTarget.empty()) {
|
|
|
|
|
|
ImGui::Text("Player: ");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.3f, 1.0f), "%s", bootTarget.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!bootReason.empty()) {
|
|
|
|
|
|
ImGui::Text("Reason: ");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextWrapped("%s", bootReason.c_str());
|
|
|
|
|
|
}
|
2026-03-12 02:17:49 -07:00
|
|
|
|
uint32_t bootVotes = gameHandler.getLfgBootVotes();
|
|
|
|
|
|
uint32_t bootTotal = gameHandler.getLfgBootTotal();
|
|
|
|
|
|
uint32_t bootNeeded = gameHandler.getLfgBootNeeded();
|
|
|
|
|
|
uint32_t bootTimeLeft= gameHandler.getLfgBootTimeLeft();
|
|
|
|
|
|
if (bootNeeded > 0) {
|
|
|
|
|
|
ImGui::Text("Votes: %u / %u (need %u) %us left",
|
|
|
|
|
|
bootVotes, bootTotal, bootNeeded, bootTimeLeft);
|
|
|
|
|
|
}
|
2026-03-10 12:08:58 -07:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
if (ImGui::Button("Vote Yes (kick)", ImVec2(140, 0))) {
|
|
|
|
|
|
gameHandler.lfgSetBootVote(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Vote No (keep)", ImVec2(140, 0))) {
|
|
|
|
|
|
gameHandler.lfgSetBootVote(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 13:47:07 -07:00
|
|
|
|
// ---- Teleport button (in dungeon) ----
|
|
|
|
|
|
if (state == LfgState::InDungeon) {
|
|
|
|
|
|
if (ImGui::Button("Teleport to Dungeon", ImVec2(-1, 0))) {
|
|
|
|
|
|
gameHandler.lfgTeleport(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- Role selection (only when not queued/in dungeon) ----
|
|
|
|
|
|
bool canConfigure = (state == LfgState::None || state == LfgState::FinishedDungeon);
|
|
|
|
|
|
|
|
|
|
|
|
if (canConfigure) {
|
|
|
|
|
|
ImGui::Text("Role:");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
bool isTank = (lfgRoles_ & 0x02) != 0;
|
|
|
|
|
|
bool isHealer = (lfgRoles_ & 0x04) != 0;
|
|
|
|
|
|
bool isDps = (lfgRoles_ & 0x08) != 0;
|
|
|
|
|
|
if (ImGui::Checkbox("Tank", &isTank)) lfgRoles_ = (lfgRoles_ & ~0x02) | (isTank ? 0x02 : 0);
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Checkbox("Healer", &isHealer)) lfgRoles_ = (lfgRoles_ & ~0x04) | (isHealer ? 0x04 : 0);
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Checkbox("DPS", &isDps)) lfgRoles_ = (lfgRoles_ & ~0x08) | (isDps ? 0x08 : 0);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
// ---- Dungeon selection ----
|
|
|
|
|
|
ImGui::Text("Dungeon:");
|
|
|
|
|
|
|
|
|
|
|
|
struct DungeonEntry { uint32_t id; const char* name; };
|
2026-03-18 12:43:04 -07:00
|
|
|
|
// Category 0=Random, 1=Classic, 2=TBC, 3=WotLK
|
|
|
|
|
|
struct DungeonEntryEx { uint32_t id; const char* name; uint8_t cat; };
|
|
|
|
|
|
static const DungeonEntryEx kDungeons[] = {
|
|
|
|
|
|
{ 861, "Random Dungeon", 0 },
|
|
|
|
|
|
{ 862, "Random Heroic", 0 },
|
|
|
|
|
|
{ 36, "Deadmines", 1 },
|
|
|
|
|
|
{ 43, "Ragefire Chasm", 1 },
|
|
|
|
|
|
{ 47, "Razorfen Kraul", 1 },
|
|
|
|
|
|
{ 48, "Blackfathom Deeps", 1 },
|
|
|
|
|
|
{ 52, "Uldaman", 1 },
|
|
|
|
|
|
{ 57, "Dire Maul: East", 1 },
|
|
|
|
|
|
{ 70, "Onyxia's Lair", 1 },
|
|
|
|
|
|
{ 264, "The Blood Furnace", 2 },
|
|
|
|
|
|
{ 269, "The Shattered Halls", 2 },
|
|
|
|
|
|
{ 576, "The Nexus", 3 },
|
|
|
|
|
|
{ 578, "The Oculus", 3 },
|
|
|
|
|
|
{ 595, "The Culling of Stratholme", 3 },
|
|
|
|
|
|
{ 599, "Halls of Stone", 3 },
|
|
|
|
|
|
{ 600, "Drak'Tharon Keep", 3 },
|
|
|
|
|
|
{ 601, "Azjol-Nerub", 3 },
|
|
|
|
|
|
{ 604, "Gundrak", 3 },
|
|
|
|
|
|
{ 608, "Violet Hold", 3 },
|
|
|
|
|
|
{ 619, "Ahn'kahet: Old Kingdom", 3 },
|
|
|
|
|
|
{ 623, "Halls of Lightning", 3 },
|
|
|
|
|
|
{ 632, "The Forge of Souls", 3 },
|
|
|
|
|
|
{ 650, "Trial of the Champion", 3 },
|
|
|
|
|
|
{ 658, "Pit of Saron", 3 },
|
|
|
|
|
|
{ 668, "Halls of Reflection", 3 },
|
2026-03-09 13:47:07 -07:00
|
|
|
|
};
|
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* kCatHeaders[] = { nullptr, "-- Classic --", "-- TBC --", "-- WotLK --" };
|
2026-03-09 13:47:07 -07:00
|
|
|
|
|
|
|
|
|
|
// Find current index
|
|
|
|
|
|
int curIdx = 0;
|
2026-03-25 11:40:49 -07:00
|
|
|
|
for (int i = 0; i < static_cast<int>(sizeof(kDungeons)/sizeof(kDungeons[0])); ++i) {
|
2026-03-09 13:47:07 -07:00
|
|
|
|
if (kDungeons[i].id == lfgSelectedDungeon_) { curIdx = i; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextItemWidth(-1);
|
|
|
|
|
|
if (ImGui::BeginCombo("##dungeon", kDungeons[curIdx].name)) {
|
2026-03-18 12:43:04 -07:00
|
|
|
|
uint8_t lastCat = 255;
|
2026-03-25 11:40:49 -07:00
|
|
|
|
for (int i = 0; i < static_cast<int>(sizeof(kDungeons)/sizeof(kDungeons[0])); ++i) {
|
2026-03-18 12:43:04 -07:00
|
|
|
|
if (kDungeons[i].cat != lastCat && kCatHeaders[kDungeons[i].cat]) {
|
|
|
|
|
|
if (lastCat != 255) ImGui::Separator();
|
|
|
|
|
|
ImGui::TextDisabled("%s", kCatHeaders[kDungeons[i].cat]);
|
|
|
|
|
|
lastCat = kDungeons[i].cat;
|
|
|
|
|
|
} else if (kDungeons[i].cat != lastCat) {
|
|
|
|
|
|
lastCat = kDungeons[i].cat;
|
|
|
|
|
|
}
|
2026-03-09 13:47:07 -07:00
|
|
|
|
bool selected = (kDungeons[i].id == lfgSelectedDungeon_);
|
|
|
|
|
|
if (ImGui::Selectable(kDungeons[i].name, selected))
|
|
|
|
|
|
lfgSelectedDungeon_ = kDungeons[i].id;
|
|
|
|
|
|
if (selected) ImGui::SetItemDefaultFocus();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndCombo();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
// ---- Join button ----
|
|
|
|
|
|
bool rolesOk = (lfgRoles_ != 0);
|
|
|
|
|
|
if (!rolesOk) {
|
|
|
|
|
|
ImGui::BeginDisabled();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::Button("Join Dungeon Finder", ImVec2(-1, 0))) {
|
|
|
|
|
|
gameHandler.lfgJoin(lfgSelectedDungeon_, lfgRoles_);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!rolesOk) {
|
|
|
|
|
|
ImGui::EndDisabled();
|
2026-03-27 10:20:45 -07:00
|
|
|
|
ImGui::TextColored(colors::kSoftRed, "Select at least one role.");
|
2026-03-09 13:47:07 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ---- Leave button (when queued or role check) ----
|
|
|
|
|
|
if (state == LfgState::Queued || state == LfgState::RoleCheck) {
|
|
|
|
|
|
if (ImGui::Button("Leave Queue", ImVec2(-1, 0))) {
|
|
|
|
|
|
gameHandler.lfgLeave();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 15:52:58 -07:00
|
|
|
|
// ============================================================
|
|
|
|
|
|
// Instance Lockouts
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
void GameScreen::renderInstanceLockouts(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!showInstanceLockouts_) return;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(480, 0), ImGuiCond_Appearing);
|
|
|
|
|
|
ImGui::SetNextWindowPos(
|
|
|
|
|
|
ImVec2(ImGui::GetIO().DisplaySize.x / 2 - 240, 140), ImGuiCond_Appearing);
|
|
|
|
|
|
|
|
|
|
|
|
if (!ImGui::Begin("Instance Lockouts", &showInstanceLockouts_,
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const auto& lockouts = gameHandler.getInstanceLockouts();
|
|
|
|
|
|
|
|
|
|
|
|
if (lockouts.empty()) {
|
2026-03-25 11:57:22 -07:00
|
|
|
|
ImGui::TextColored(kColorGray, "No active instance lockouts.");
|
2026-03-09 15:52:58 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
auto difficultyLabel = [](uint32_t diff) -> const char* {
|
|
|
|
|
|
switch (diff) {
|
|
|
|
|
|
case 0: return "Normal";
|
|
|
|
|
|
case 1: return "Heroic";
|
|
|
|
|
|
case 2: return "25-Man";
|
|
|
|
|
|
case 3: return "25-Man Heroic";
|
|
|
|
|
|
default: return "Unknown";
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Current UTC time for reset countdown
|
|
|
|
|
|
auto nowSec = static_cast<uint64_t>(std::time(nullptr));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::BeginTable("lockouts", 4,
|
|
|
|
|
|
ImGuiTableFlags_SizingStretchProp |
|
|
|
|
|
|
ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter)) {
|
|
|
|
|
|
ImGui::TableSetupColumn("Instance", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
|
|
ImGui::TableSetupColumn("Difficulty", ImGuiTableColumnFlags_WidthFixed, 110.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Resets In", ImGuiTableColumnFlags_WidthFixed, 100.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 60.0f);
|
|
|
|
|
|
ImGui::TableHeadersRow();
|
|
|
|
|
|
|
|
|
|
|
|
for (const auto& lo : lockouts) {
|
|
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
|
2026-03-13 08:23:43 -07:00
|
|
|
|
// Instance name — use GameHandler's Map.dbc cache (avoids duplicate DBC load)
|
2026-03-09 15:52:58 -07:00
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
2026-03-13 08:23:43 -07:00
|
|
|
|
std::string mapName = gameHandler.getMapName(lo.mapId);
|
|
|
|
|
|
if (!mapName.empty()) {
|
|
|
|
|
|
ImGui::TextUnformatted(mapName.c_str());
|
2026-03-09 15:52:58 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::Text("Map %u", lo.mapId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Difficulty
|
|
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
|
|
|
|
ImGui::TextUnformatted(difficultyLabel(lo.difficulty));
|
|
|
|
|
|
|
|
|
|
|
|
// Reset countdown
|
|
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
|
|
|
|
|
if (lo.resetTime > nowSec) {
|
|
|
|
|
|
uint64_t remaining = lo.resetTime - nowSec;
|
|
|
|
|
|
uint64_t days = remaining / 86400;
|
|
|
|
|
|
uint64_t hours = (remaining % 86400) / 3600;
|
|
|
|
|
|
if (days > 0) {
|
|
|
|
|
|
ImGui::Text("%llud %lluh",
|
|
|
|
|
|
static_cast<unsigned long long>(days),
|
|
|
|
|
|
static_cast<unsigned long long>(hours));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
uint64_t mins = (remaining % 3600) / 60;
|
|
|
|
|
|
ImGui::Text("%lluh %llum",
|
|
|
|
|
|
static_cast<unsigned long long>(hours),
|
|
|
|
|
|
static_cast<unsigned long long>(mins));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-03-25 12:12:03 -07:00
|
|
|
|
ImGui::TextColored(kColorDarkGray, "Expired");
|
2026-03-09 15:52:58 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Locked / Extended status
|
|
|
|
|
|
ImGui::TableSetColumnIndex(3);
|
|
|
|
|
|
if (lo.extended) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.3f, 0.7f, 1.0f, 1.0f), "Ext");
|
|
|
|
|
|
} else if (lo.locked) {
|
2026-03-27 10:20:45 -07:00
|
|
|
|
ImGui::TextColored(colors::kSoftRed, "Locked");
|
2026-03-09 15:52:58 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.5f, 0.9f, 0.5f, 1.0f), "Open");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 22:42:44 -07:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// Battleground score frame
|
|
|
|
|
|
//
|
|
|
|
|
|
// Displays the current score for the player's battleground using world states.
|
|
|
|
|
|
// Shown in the top-centre of the screen whenever SMSG_INIT_WORLD_STATES has
|
|
|
|
|
|
// been received for a known BG map. The layout adapts per battleground:
|
|
|
|
|
|
//
|
|
|
|
|
|
// WSG 489 – Alliance / Horde flag captures (max 3)
|
|
|
|
|
|
// AB 529 – Alliance / Horde resource scores (max 1600)
|
|
|
|
|
|
// AV 30 – Alliance / Horde reinforcements
|
|
|
|
|
|
// EotS 566 – Alliance / Horde resource scores (max 1600)
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
void GameScreen::renderBattlegroundScore(game::GameHandler& gameHandler) {
|
|
|
|
|
|
// Only show when in a recognised battleground map
|
|
|
|
|
|
uint32_t mapId = gameHandler.getWorldStateMapId();
|
|
|
|
|
|
|
|
|
|
|
|
// World state key sets per battleground
|
|
|
|
|
|
// Keys from the WoW 3.3.5a WorldState.dbc / client source
|
|
|
|
|
|
struct BgScoreDef {
|
|
|
|
|
|
uint32_t mapId;
|
|
|
|
|
|
const char* name;
|
|
|
|
|
|
uint32_t allianceKey; // world state key for Alliance value
|
|
|
|
|
|
uint32_t hordeKey; // world state key for Horde value
|
|
|
|
|
|
uint32_t maxKey; // max score world state key (0 = use hardcoded)
|
|
|
|
|
|
uint32_t hardcodedMax; // used when maxKey == 0
|
|
|
|
|
|
const char* unit; // suffix label (e.g. "flags", "resources")
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
static constexpr BgScoreDef kBgDefs[] = {
|
|
|
|
|
|
// Warsong Gulch: 3 flag captures wins
|
|
|
|
|
|
{ 489, "Warsong Gulch", 1581, 1582, 0, 3, "flags" },
|
|
|
|
|
|
// Arathi Basin: 1600 resources wins
|
|
|
|
|
|
{ 529, "Arathi Basin", 1218, 1219, 0, 1600, "resources" },
|
|
|
|
|
|
// Alterac Valley: reinforcements count down from 600 / 800 etc.
|
|
|
|
|
|
{ 30, "Alterac Valley", 1322, 1323, 0, 600, "reinforcements" },
|
|
|
|
|
|
// Eye of the Storm: 1600 resources wins
|
|
|
|
|
|
{ 566, "Eye of the Storm", 2757, 2758, 0, 1600, "resources" },
|
|
|
|
|
|
// Strand of the Ancients (WotLK)
|
|
|
|
|
|
{ 607, "Strand of the Ancients", 3476, 3477, 0, 4, "" },
|
2026-03-20 05:40:53 -07:00
|
|
|
|
// Isle of Conquest (WotLK): reinforcements (300 default)
|
|
|
|
|
|
{ 628, "Isle of Conquest", 4221, 4222, 0, 300, "reinforcements" },
|
2026-03-09 22:42:44 -07:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const BgScoreDef* def = nullptr;
|
|
|
|
|
|
for (const auto& d : kBgDefs) {
|
|
|
|
|
|
if (d.mapId == mapId) { def = &d; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!def) return;
|
|
|
|
|
|
|
|
|
|
|
|
auto allianceOpt = gameHandler.getWorldState(def->allianceKey);
|
|
|
|
|
|
auto hordeOpt = gameHandler.getWorldState(def->hordeKey);
|
|
|
|
|
|
if (!allianceOpt && !hordeOpt) return;
|
|
|
|
|
|
|
|
|
|
|
|
uint32_t allianceScore = allianceOpt.value_or(0);
|
|
|
|
|
|
uint32_t hordeScore = hordeOpt.value_or(0);
|
|
|
|
|
|
uint32_t maxScore = def->hardcodedMax;
|
|
|
|
|
|
if (def->maxKey != 0) {
|
|
|
|
|
|
if (auto mv = gameHandler.getWorldState(def->maxKey)) maxScore = *mv;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
auto* window = core::Application::getInstance().getWindow();
|
|
|
|
|
|
float screenW = window ? static_cast<float>(window->getWidth()) : 1280.0f;
|
|
|
|
|
|
|
|
|
|
|
|
// Width scales with screen but stays reasonable
|
|
|
|
|
|
float frameW = 260.0f;
|
|
|
|
|
|
float frameH = 60.0f;
|
|
|
|
|
|
float posX = screenW / 2.0f - frameW / 2.0f;
|
|
|
|
|
|
float posY = 4.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(posX, posY), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(frameW, frameH), ImGuiCond_Always);
|
|
|
|
|
|
ImGui::SetNextWindowBgAlpha(0.75f);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 6.0f);
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.0f, 4.0f));
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin("##BGScore", nullptr,
|
|
|
|
|
|
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
|
|
|
|
|
|
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoBringToFrontOnFocus |
|
|
|
|
|
|
ImGuiWindowFlags_NoSavedSettings)) {
|
|
|
|
|
|
|
|
|
|
|
|
// BG name centred at top
|
|
|
|
|
|
float nameW = ImGui::CalcTextSize(def->name).x;
|
|
|
|
|
|
ImGui::SetCursorPosX((frameW - nameW) / 2.0f);
|
refactor: add 6 color constants, replace 61 inline literals, remove const_cast
- Add kBrightGold, kPaleRed, kBrightRed, kLightBlue, kManaBlue, kCyan to ui_colors.hpp
- Replace 61 inline ImVec4 color literals across game_screen, inventory_screen,
talent_screen, and world_map with named constants
- Remove const_cast in character_renderer render loop by using non-const iteration
2026-03-27 10:08:30 -07:00
|
|
|
|
ImGui::TextColored(colors::kBrightGold, "%s", def->name);
|
2026-03-09 22:42:44 -07:00
|
|
|
|
|
|
|
|
|
|
// Alliance score | separator | Horde score
|
|
|
|
|
|
float innerW = frameW - 12.0f;
|
|
|
|
|
|
float halfW = innerW / 2.0f - 4.0f;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetCursorPosX(6.0f);
|
|
|
|
|
|
ImGui::BeginGroup();
|
|
|
|
|
|
{
|
|
|
|
|
|
// Alliance (blue)
|
|
|
|
|
|
char aBuf[32];
|
|
|
|
|
|
if (maxScore > 0 && strlen(def->unit) > 0)
|
|
|
|
|
|
snprintf(aBuf, sizeof(aBuf), "\xF0\x9F\x94\xB5 %u / %u", allianceScore, maxScore);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(aBuf, sizeof(aBuf), "\xF0\x9F\x94\xB5 %u", allianceScore);
|
refactor: add 6 color constants, replace 61 inline literals, remove const_cast
- Add kBrightGold, kPaleRed, kBrightRed, kLightBlue, kManaBlue, kCyan to ui_colors.hpp
- Replace 61 inline ImVec4 color literals across game_screen, inventory_screen,
talent_screen, and world_map with named constants
- Remove const_cast in character_renderer render loop by using non-const iteration
2026-03-27 10:08:30 -07:00
|
|
|
|
ImGui::TextColored(colors::kLightBlue, "%s", aBuf);
|
2026-03-09 22:42:44 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndGroup();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine(halfW + 16.0f);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::BeginGroup();
|
|
|
|
|
|
{
|
|
|
|
|
|
// Horde (red)
|
|
|
|
|
|
char hBuf[32];
|
|
|
|
|
|
if (maxScore > 0 && strlen(def->unit) > 0)
|
|
|
|
|
|
snprintf(hBuf, sizeof(hBuf), "\xF0\x9F\x94\xB4 %u / %u", hordeScore, maxScore);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(hBuf, sizeof(hBuf), "\xF0\x9F\x94\xB4 %u", hordeScore);
|
2026-03-27 10:20:45 -07:00
|
|
|
|
ImGui::TextColored(colors::kHostileRed, "%s", hBuf);
|
2026-03-09 22:42:44 -07:00
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndGroup();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleVar(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 10:41:18 -07:00
|
|
|
|
// ─── Who Results Window ───────────────────────────────────────────────────────
|
|
|
|
|
|
void GameScreen::renderWhoWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!showWhoWindow_) return;
|
|
|
|
|
|
|
|
|
|
|
|
const auto& results = gameHandler.getWhoResults();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(500, 300), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(200, 180), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
|
|
|
|
|
|
char title[64];
|
|
|
|
|
|
uint32_t onlineCount = gameHandler.getWhoOnlineCount();
|
|
|
|
|
|
if (onlineCount > 0)
|
|
|
|
|
|
snprintf(title, sizeof(title), "Players Online: %u###WhoWindow", onlineCount);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(title, sizeof(title), "Who###WhoWindow");
|
|
|
|
|
|
|
|
|
|
|
|
if (!ImGui::Begin(title, &showWhoWindow_)) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 10:45:31 -07:00
|
|
|
|
// Search bar with Send button
|
|
|
|
|
|
static char whoSearchBuf[64] = {};
|
|
|
|
|
|
bool doSearch = false;
|
|
|
|
|
|
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - 60.0f);
|
|
|
|
|
|
if (ImGui::InputTextWithHint("##whosearch", "Search players...", whoSearchBuf, sizeof(whoSearchBuf),
|
|
|
|
|
|
ImGuiInputTextFlags_EnterReturnsTrue))
|
|
|
|
|
|
doSearch = true;
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Search", ImVec2(-1, 0)))
|
|
|
|
|
|
doSearch = true;
|
|
|
|
|
|
if (doSearch) {
|
|
|
|
|
|
gameHandler.queryWho(std::string(whoSearchBuf));
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
2026-03-12 10:41:18 -07:00
|
|
|
|
if (results.empty()) {
|
2026-03-12 10:45:31 -07:00
|
|
|
|
ImGui::TextDisabled("No results. Type a filter above or use /who [filter].");
|
2026-03-12 10:41:18 -07:00
|
|
|
|
ImGui::End();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Table: Name | Guild | Level | Class | Zone
|
|
|
|
|
|
if (ImGui::BeginTable("##WhoTable", 5,
|
|
|
|
|
|
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg |
|
|
|
|
|
|
ImGuiTableFlags_ScrollY | ImGuiTableFlags_SizingStretchProp,
|
|
|
|
|
|
ImVec2(0, 0))) {
|
|
|
|
|
|
ImGui::TableSetupScrollFreeze(0, 1);
|
|
|
|
|
|
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch, 0.22f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Guild", ImGuiTableColumnFlags_WidthStretch, 0.20f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Level", ImGuiTableColumnFlags_WidthFixed, 40.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Class", ImGuiTableColumnFlags_WidthStretch, 0.20f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Zone", ImGuiTableColumnFlags_WidthStretch, 0.28f);
|
|
|
|
|
|
ImGui::TableHeadersRow();
|
|
|
|
|
|
|
|
|
|
|
|
for (size_t i = 0; i < results.size(); ++i) {
|
|
|
|
|
|
const auto& e = results[i];
|
|
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(i));
|
|
|
|
|
|
|
|
|
|
|
|
// Name (class-colored if class is known)
|
|
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
|
|
|
|
|
uint8_t cid = static_cast<uint8_t>(e.classId);
|
|
|
|
|
|
ImVec4 nameCol = classColorVec4(cid);
|
|
|
|
|
|
ImGui::TextColored(nameCol, "%s", e.name.c_str());
|
|
|
|
|
|
|
|
|
|
|
|
// Right-click context menu on the name
|
|
|
|
|
|
if (ImGui::BeginPopupContextItem("##WhoCtx")) {
|
|
|
|
|
|
ImGui::TextDisabled("%s", e.name.c_str());
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
if (ImGui::MenuItem("Whisper")) {
|
2026-03-31 08:53:14 +03:00
|
|
|
|
chatPanel_.setWhisperTarget(e.name);
|
2026-03-12 10:41:18 -07:00
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::MenuItem("Invite to Group"))
|
|
|
|
|
|
gameHandler.inviteToGroup(e.name);
|
|
|
|
|
|
if (ImGui::MenuItem("Add Friend"))
|
|
|
|
|
|
gameHandler.addFriend(e.name);
|
|
|
|
|
|
if (ImGui::MenuItem("Ignore"))
|
|
|
|
|
|
gameHandler.addIgnore(e.name);
|
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Guild
|
|
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
|
|
|
|
if (!e.guildName.empty())
|
|
|
|
|
|
ImGui::TextDisabled("<%s>", e.guildName.c_str());
|
|
|
|
|
|
|
|
|
|
|
|
// Level
|
|
|
|
|
|
ImGui::TableSetColumnIndex(2);
|
|
|
|
|
|
ImGui::Text("%u", e.level);
|
|
|
|
|
|
|
|
|
|
|
|
// Class
|
|
|
|
|
|
ImGui::TableSetColumnIndex(3);
|
|
|
|
|
|
const char* className = game::getClassName(static_cast<game::Class>(e.classId));
|
|
|
|
|
|
ImGui::TextColored(nameCol, "%s", className);
|
|
|
|
|
|
|
|
|
|
|
|
// Zone
|
|
|
|
|
|
ImGui::TableSetColumnIndex(4);
|
|
|
|
|
|
if (e.zoneId != 0) {
|
|
|
|
|
|
std::string zoneName = gameHandler.getWhoAreaName(e.zoneId);
|
2026-03-27 16:47:30 -07:00
|
|
|
|
if (!zoneName.empty())
|
|
|
|
|
|
ImGui::TextUnformatted(zoneName.c_str());
|
|
|
|
|
|
else {
|
|
|
|
|
|
char zfb[32];
|
|
|
|
|
|
snprintf(zfb, sizeof(zfb), "Zone #%u", e.zoneId);
|
|
|
|
|
|
ImGui::TextUnformatted(zfb);
|
|
|
|
|
|
}
|
2026-03-12 10:41:18 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// ─── Combat Log Window ────────────────────────────────────────────────────────
|
|
|
|
|
|
void GameScreen::renderCombatLog(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!showCombatLog_) return;
|
|
|
|
|
|
|
|
|
|
|
|
const auto& log = gameHandler.getCombatLog();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(520, 320), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(160, 200), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
|
|
|
|
|
|
char title[64];
|
|
|
|
|
|
snprintf(title, sizeof(title), "Combat Log (%zu)###CombatLog", log.size());
|
|
|
|
|
|
if (!ImGui::Begin(title, &showCombatLog_)) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Filter toggles
|
|
|
|
|
|
static bool filterDamage = true;
|
|
|
|
|
|
static bool filterHeal = true;
|
|
|
|
|
|
static bool filterMisc = true;
|
|
|
|
|
|
static bool autoScroll = true;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(4, 2));
|
|
|
|
|
|
ImGui::Checkbox("Damage", &filterDamage); ImGui::SameLine();
|
|
|
|
|
|
ImGui::Checkbox("Healing", &filterHeal); ImGui::SameLine();
|
|
|
|
|
|
ImGui::Checkbox("Misc", &filterMisc); ImGui::SameLine();
|
|
|
|
|
|
ImGui::Checkbox("Auto-scroll", &autoScroll);
|
|
|
|
|
|
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 40.0f);
|
|
|
|
|
|
if (ImGui::SmallButton("Clear"))
|
|
|
|
|
|
gameHandler.clearCombatLog();
|
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
// Helper: categorize entry
|
|
|
|
|
|
auto isDamageType = [](game::CombatTextEntry::Type t) {
|
|
|
|
|
|
using T = game::CombatTextEntry;
|
|
|
|
|
|
return t == T::MELEE_DAMAGE || t == T::SPELL_DAMAGE ||
|
|
|
|
|
|
t == T::CRIT_DAMAGE || t == T::PERIODIC_DAMAGE ||
|
2026-03-17 18:51:48 -07:00
|
|
|
|
t == T::ENVIRONMENTAL || t == T::GLANCING || t == T::CRUSHING;
|
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
|
|
|
|
};
|
|
|
|
|
|
auto isHealType = [](game::CombatTextEntry::Type t) {
|
|
|
|
|
|
using T = game::CombatTextEntry;
|
|
|
|
|
|
return t == T::HEAL || t == T::CRIT_HEAL || t == T::PERIODIC_HEAL;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Two-column table: Time | Event description
|
|
|
|
|
|
ImGuiTableFlags tableFlags = ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg |
|
|
|
|
|
|
ImGuiTableFlags_SizingFixedFit;
|
|
|
|
|
|
float availH = ImGui::GetContentRegionAvail().y;
|
|
|
|
|
|
if (ImGui::BeginTable("##CombatLogTable", 2, tableFlags, ImVec2(0.0f, availH))) {
|
|
|
|
|
|
ImGui::TableSetupScrollFreeze(0, 0);
|
|
|
|
|
|
ImGui::TableSetupColumn("Time", ImGuiTableColumnFlags_WidthFixed, 62.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Event", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
|
|
|
|
|
|
|
|
for (const auto& e : log) {
|
|
|
|
|
|
// Apply filters
|
|
|
|
|
|
bool isDmg = isDamageType(e.type);
|
|
|
|
|
|
bool isHeal = isHealType(e.type);
|
|
|
|
|
|
bool isMisc = !isDmg && !isHeal;
|
|
|
|
|
|
if (isDmg && !filterDamage) continue;
|
|
|
|
|
|
if (isHeal && !filterHeal) continue;
|
|
|
|
|
|
if (isMisc && !filterMisc) continue;
|
|
|
|
|
|
|
|
|
|
|
|
// Format timestamp as HH:MM:SS
|
|
|
|
|
|
char timeBuf[10];
|
|
|
|
|
|
{
|
|
|
|
|
|
struct tm* tm_info = std::localtime(&e.timestamp);
|
|
|
|
|
|
if (tm_info)
|
|
|
|
|
|
snprintf(timeBuf, sizeof(timeBuf), "%02d:%02d:%02d",
|
|
|
|
|
|
tm_info->tm_hour, tm_info->tm_min, tm_info->tm_sec);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(timeBuf, sizeof(timeBuf), "--:--:--");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Build event description and choose color
|
|
|
|
|
|
char desc[256];
|
|
|
|
|
|
ImVec4 color;
|
|
|
|
|
|
using T = game::CombatTextEntry;
|
|
|
|
|
|
const char* src = e.sourceName.empty() ? (e.isPlayerSource ? "You" : "?") : e.sourceName.c_str();
|
|
|
|
|
|
const char* tgt = e.targetName.empty() ? "?" : e.targetName.c_str();
|
|
|
|
|
|
const std::string& spellName = (e.spellId != 0) ? gameHandler.getSpellName(e.spellId) : std::string();
|
|
|
|
|
|
const char* spell = spellName.empty() ? nullptr : spellName.c_str();
|
|
|
|
|
|
|
|
|
|
|
|
switch (e.type) {
|
|
|
|
|
|
case T::MELEE_DAMAGE:
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s hits %s for %d", src, tgt, e.amount);
|
2026-03-27 10:20:45 -07:00
|
|
|
|
color = e.isPlayerSource ? ImVec4(1.0f, 0.9f, 0.3f, 1.0f) : colors::kSoftRed;
|
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
|
|
|
|
break;
|
|
|
|
|
|
case T::CRIT_DAMAGE:
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s crits %s for %d!", src, tgt, e.amount);
|
refactor: add 6 color constants, replace 61 inline literals, remove const_cast
- Add kBrightGold, kPaleRed, kBrightRed, kLightBlue, kManaBlue, kCyan to ui_colors.hpp
- Replace 61 inline ImVec4 color literals across game_screen, inventory_screen,
talent_screen, and world_map with named constants
- Remove const_cast in character_renderer render loop by using non-const iteration
2026-03-27 10:08:30 -07:00
|
|
|
|
color = e.isPlayerSource ? ImVec4(1.0f, 1.0f, 0.0f, 1.0f) : colors::kBrightRed;
|
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
|
|
|
|
break;
|
|
|
|
|
|
case T::SPELL_DAMAGE:
|
|
|
|
|
|
if (spell)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s's %s hits %s for %d", src, spell, tgt, e.amount);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s's spell hits %s for %d", src, tgt, e.amount);
|
2026-03-27 10:20:45 -07:00
|
|
|
|
color = e.isPlayerSource ? ImVec4(1.0f, 0.9f, 0.3f, 1.0f) : colors::kSoftRed;
|
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
|
|
|
|
break;
|
|
|
|
|
|
case T::PERIODIC_DAMAGE:
|
|
|
|
|
|
if (spell)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s's %s ticks %s for %d", src, spell, tgt, e.amount);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s's DoT ticks %s for %d", src, tgt, e.amount);
|
|
|
|
|
|
color = e.isPlayerSource ? ImVec4(0.9f, 0.7f, 0.3f, 1.0f) : ImVec4(0.9f, 0.3f, 0.3f, 1.0f);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case T::HEAL:
|
|
|
|
|
|
if (spell)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s heals %s for %d (%s)", src, tgt, e.amount, spell);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s heals %s for %d", src, tgt, e.amount);
|
2026-03-25 11:57:22 -07:00
|
|
|
|
color = kColorGreen;
|
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
|
|
|
|
break;
|
|
|
|
|
|
case T::CRIT_HEAL:
|
|
|
|
|
|
if (spell)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s critically heals %s for %d! (%s)", src, tgt, e.amount, spell);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s critically heals %s for %d!", src, tgt, e.amount);
|
2026-03-25 12:12:03 -07:00
|
|
|
|
color = kColorBrightGreen;
|
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
|
|
|
|
break;
|
|
|
|
|
|
case T::PERIODIC_HEAL:
|
|
|
|
|
|
if (spell)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s's %s heals %s for %d", src, spell, tgt, e.amount);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s's HoT heals %s for %d", src, tgt, e.amount);
|
|
|
|
|
|
color = ImVec4(0.4f, 0.9f, 0.4f, 1.0f);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case T::MISS:
|
2026-03-13 11:39:22 -07:00
|
|
|
|
if (spell)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s's %s misses %s", src, spell, tgt);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s misses %s", src, tgt);
|
2026-03-27 10:20:45 -07:00
|
|
|
|
color = colors::kMediumGray;
|
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
|
|
|
|
break;
|
|
|
|
|
|
case T::DODGE:
|
2026-03-13 11:39:22 -07:00
|
|
|
|
if (spell)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s dodges %s's %s", tgt, src, spell);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s dodges %s's attack", tgt, src);
|
2026-03-27 10:20:45 -07:00
|
|
|
|
color = colors::kMediumGray;
|
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
|
|
|
|
break;
|
|
|
|
|
|
case T::PARRY:
|
2026-03-13 11:39:22 -07:00
|
|
|
|
if (spell)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s parries %s's %s", tgt, src, spell);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s parries %s's attack", tgt, src);
|
2026-03-27 10:20:45 -07:00
|
|
|
|
color = colors::kMediumGray;
|
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
|
|
|
|
break;
|
|
|
|
|
|
case T::BLOCK:
|
2026-03-13 11:39:22 -07:00
|
|
|
|
if (spell)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s blocks %s's %s (%d blocked)", tgt, src, spell, e.amount);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s blocks %s's attack (%d blocked)", tgt, src, e.amount);
|
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
|
|
|
|
color = ImVec4(0.65f, 0.75f, 0.65f, 1.0f);
|
|
|
|
|
|
break;
|
2026-03-13 23:32:57 -07:00
|
|
|
|
case T::EVADE:
|
|
|
|
|
|
if (spell)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s evades %s's %s", tgt, src, spell);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s evades %s's attack", tgt, src);
|
2026-03-27 10:20:45 -07:00
|
|
|
|
color = colors::kMediumGray;
|
2026-03-13 23:32:57 -07:00
|
|
|
|
break;
|
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
|
|
|
|
case T::IMMUNE:
|
2026-03-13 11:39:22 -07:00
|
|
|
|
if (spell)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s is immune to %s", tgt, spell);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s is immune", tgt);
|
2026-03-27 14:00:15 -07:00
|
|
|
|
color = colors::kSilver;
|
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
|
|
|
|
break;
|
|
|
|
|
|
case T::ABSORB:
|
2026-03-13 11:39:22 -07:00
|
|
|
|
if (spell && e.amount > 0)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s's %s absorbs %d", src, spell, e.amount);
|
|
|
|
|
|
else if (spell)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s absorbs %s", tgt, spell);
|
|
|
|
|
|
else if (e.amount > 0)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%d absorbed", e.amount);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "Absorbed");
|
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
|
|
|
|
color = ImVec4(0.5f, 0.8f, 1.0f, 1.0f);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case T::RESIST:
|
2026-03-13 11:39:22 -07:00
|
|
|
|
if (spell && e.amount > 0)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s resists %s's %s (%d resisted)", tgt, src, spell, e.amount);
|
|
|
|
|
|
else if (spell)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s resists %s's %s", tgt, src, spell);
|
|
|
|
|
|
else if (e.amount > 0)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%d resisted", e.amount);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "Resisted");
|
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
|
|
|
|
color = ImVec4(0.6f, 0.6f, 0.9f, 1.0f);
|
|
|
|
|
|
break;
|
2026-03-13 23:08:49 -07:00
|
|
|
|
case T::DEFLECT:
|
|
|
|
|
|
if (spell)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s deflects %s's %s", tgt, src, spell);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s deflects %s's attack", tgt, src);
|
|
|
|
|
|
color = ImVec4(0.65f, 0.8f, 0.95f, 1.0f);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case T::REFLECT:
|
|
|
|
|
|
if (spell)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s reflects %s's %s", tgt, src, spell);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s reflects %s's attack", tgt, src);
|
|
|
|
|
|
color = ImVec4(0.8f, 0.7f, 1.0f, 1.0f);
|
|
|
|
|
|
break;
|
2026-03-17 10:54:07 -07:00
|
|
|
|
case T::ENVIRONMENTAL: {
|
|
|
|
|
|
const char* envName = "Environmental";
|
|
|
|
|
|
switch (e.powerType) {
|
|
|
|
|
|
case 0: envName = "Fatigue"; break;
|
|
|
|
|
|
case 1: envName = "Drowning"; break;
|
|
|
|
|
|
case 2: envName = "Falling"; break;
|
|
|
|
|
|
case 3: envName = "Lava"; break;
|
|
|
|
|
|
case 4: envName = "Slime"; break;
|
|
|
|
|
|
case 5: envName = "Fire"; break;
|
|
|
|
|
|
}
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s damage: %d", envName, e.amount);
|
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
|
|
|
|
color = ImVec4(1.0f, 0.5f, 0.2f, 1.0f);
|
|
|
|
|
|
break;
|
2026-03-17 10:54:07 -07:00
|
|
|
|
}
|
2026-03-17 11:03:20 -07:00
|
|
|
|
case T::ENERGIZE: {
|
|
|
|
|
|
const char* pwrName = "power";
|
|
|
|
|
|
switch (e.powerType) {
|
|
|
|
|
|
case 0: pwrName = "Mana"; break;
|
|
|
|
|
|
case 1: pwrName = "Rage"; break;
|
|
|
|
|
|
case 2: pwrName = "Focus"; break;
|
|
|
|
|
|
case 3: pwrName = "Energy"; break;
|
|
|
|
|
|
case 4: pwrName = "Happiness"; break;
|
|
|
|
|
|
case 6: pwrName = "Runic Power"; break;
|
|
|
|
|
|
}
|
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
|
|
|
|
if (spell)
|
2026-03-17 11:03:20 -07:00
|
|
|
|
snprintf(desc, sizeof(desc), "%s gains %d %s (%s)", tgt, e.amount, pwrName, spell);
|
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
|
|
|
|
else
|
2026-03-17 11:03:20 -07:00
|
|
|
|
snprintf(desc, sizeof(desc), "%s gains %d %s", tgt, e.amount, pwrName);
|
refactor: add 6 color constants, replace 61 inline literals, remove const_cast
- Add kBrightGold, kPaleRed, kBrightRed, kLightBlue, kManaBlue, kCyan to ui_colors.hpp
- Replace 61 inline ImVec4 color literals across game_screen, inventory_screen,
talent_screen, and world_map with named constants
- Remove const_cast in character_renderer render loop by using non-const iteration
2026-03-27 10:08:30 -07:00
|
|
|
|
color = colors::kLightBlue;
|
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
|
|
|
|
break;
|
2026-03-17 11:03:20 -07:00
|
|
|
|
}
|
|
|
|
|
|
case T::POWER_DRAIN: {
|
|
|
|
|
|
const char* drainName = "power";
|
|
|
|
|
|
switch (e.powerType) {
|
|
|
|
|
|
case 0: drainName = "Mana"; break;
|
|
|
|
|
|
case 1: drainName = "Rage"; break;
|
|
|
|
|
|
case 2: drainName = "Focus"; break;
|
|
|
|
|
|
case 3: drainName = "Energy"; break;
|
|
|
|
|
|
case 4: drainName = "Happiness"; break;
|
|
|
|
|
|
case 6: drainName = "Runic Power"; break;
|
|
|
|
|
|
}
|
2026-03-13 23:56:44 -07:00
|
|
|
|
if (spell)
|
2026-03-17 11:03:20 -07:00
|
|
|
|
snprintf(desc, sizeof(desc), "%s loses %d %s to %s's %s", tgt, e.amount, drainName, src, spell);
|
2026-03-13 23:56:44 -07:00
|
|
|
|
else
|
2026-03-17 11:03:20 -07:00
|
|
|
|
snprintf(desc, sizeof(desc), "%s loses %d %s", tgt, e.amount, drainName);
|
2026-03-13 23:56:44 -07:00
|
|
|
|
color = ImVec4(0.45f, 0.75f, 1.0f, 1.0f);
|
|
|
|
|
|
break;
|
2026-03-17 11:03:20 -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
|
|
|
|
case T::XP_GAIN:
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "You gain %d experience", e.amount);
|
|
|
|
|
|
color = ImVec4(0.8f, 0.6f, 1.0f, 1.0f);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case T::PROC_TRIGGER:
|
|
|
|
|
|
if (spell)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s procs!", spell);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "Proc triggered");
|
|
|
|
|
|
color = ImVec4(1.0f, 0.85f, 0.3f, 1.0f);
|
|
|
|
|
|
break;
|
2026-03-13 12:03:07 -07:00
|
|
|
|
case T::DISPEL:
|
|
|
|
|
|
if (spell && e.isPlayerSource)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "You dispel %s from %s", spell, tgt);
|
|
|
|
|
|
else if (spell)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s dispels %s from %s", src, spell, tgt);
|
|
|
|
|
|
else if (e.isPlayerSource)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "You dispel from %s", tgt);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s dispels from %s", src, tgt);
|
|
|
|
|
|
color = ImVec4(0.6f, 0.9f, 1.0f, 1.0f);
|
|
|
|
|
|
break;
|
2026-03-13 19:58:37 -07:00
|
|
|
|
case T::STEAL:
|
|
|
|
|
|
if (spell && e.isPlayerSource)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "You steal %s from %s", spell, tgt);
|
|
|
|
|
|
else if (spell)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s steals %s from %s", src, spell, tgt);
|
|
|
|
|
|
else if (e.isPlayerSource)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "You steal from %s", tgt);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s steals from %s", src, tgt);
|
|
|
|
|
|
color = ImVec4(0.8f, 0.7f, 1.0f, 1.0f);
|
|
|
|
|
|
break;
|
2026-03-13 12:03:07 -07:00
|
|
|
|
case T::INTERRUPT:
|
|
|
|
|
|
if (spell && e.isPlayerSource)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "You interrupt %s's %s", tgt, spell);
|
|
|
|
|
|
else if (spell)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s interrupts %s's %s", src, tgt, spell);
|
|
|
|
|
|
else if (e.isPlayerSource)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "You interrupt %s", tgt);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s interrupted", tgt);
|
|
|
|
|
|
color = ImVec4(1.0f, 0.6f, 0.9f, 1.0f);
|
|
|
|
|
|
break;
|
2026-03-13 22:22:00 -07:00
|
|
|
|
case T::INSTAKILL:
|
|
|
|
|
|
if (spell && e.isPlayerSource)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "You instantly kill %s with %s", tgt, spell);
|
|
|
|
|
|
else if (spell)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s instantly kills %s with %s", src, tgt, spell);
|
|
|
|
|
|
else if (e.isPlayerSource)
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "You instantly kill %s", tgt);
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s instantly kills %s", src, tgt);
|
refactor: add 6 color constants, replace 61 inline literals, remove const_cast
- Add kBrightGold, kPaleRed, kBrightRed, kLightBlue, kManaBlue, kCyan to ui_colors.hpp
- Replace 61 inline ImVec4 color literals across game_screen, inventory_screen,
talent_screen, and world_map with named constants
- Remove const_cast in character_renderer render loop by using non-const iteration
2026-03-27 10:08:30 -07:00
|
|
|
|
color = colors::kBrightRed;
|
2026-03-13 22:22:00 -07:00
|
|
|
|
break;
|
2026-03-17 14:38:57 -07:00
|
|
|
|
case T::HONOR_GAIN:
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "You gain %d honor", e.amount);
|
refactor: add 6 color constants, replace 61 inline literals, remove const_cast
- Add kBrightGold, kPaleRed, kBrightRed, kLightBlue, kManaBlue, kCyan to ui_colors.hpp
- Replace 61 inline ImVec4 color literals across game_screen, inventory_screen,
talent_screen, and world_map with named constants
- Remove const_cast in character_renderer render loop by using non-const iteration
2026-03-27 10:08:30 -07:00
|
|
|
|
color = colors::kBrightGold;
|
2026-03-17 14:38:57 -07:00
|
|
|
|
break;
|
2026-03-17 18:51:48 -07:00
|
|
|
|
case T::GLANCING:
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s glances %s for %d", src, tgt, e.amount);
|
|
|
|
|
|
color = e.isPlayerSource ? ImVec4(0.75f, 0.75f, 0.5f, 1.0f)
|
|
|
|
|
|
: ImVec4(0.75f, 0.4f, 0.4f, 1.0f);
|
|
|
|
|
|
break;
|
|
|
|
|
|
case T::CRUSHING:
|
|
|
|
|
|
snprintf(desc, sizeof(desc), "%s crushes %s for %d!", src, tgt, e.amount);
|
|
|
|
|
|
color = e.isPlayerSource ? ImVec4(1.0f, 0.55f, 0.1f, 1.0f)
|
|
|
|
|
|
: ImVec4(1.0f, 0.15f, 0.15f, 1.0f);
|
|
|
|
|
|
break;
|
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
|
|
|
|
default:
|
2026-03-25 11:40:49 -07:00
|
|
|
|
snprintf(desc, sizeof(desc), "Combat event (type %d, amount %d)", static_cast<int>(e.type), e.amount);
|
2026-03-25 12:29:44 -07:00
|
|
|
|
color = ui::colors::kLightGray;
|
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
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
ImGui::TableSetColumnIndex(0);
|
|
|
|
|
|
ImGui::TextDisabled("%s", timeBuf);
|
|
|
|
|
|
ImGui::TableSetColumnIndex(1);
|
|
|
|
|
|
ImGui::TextColored(color, "%s", desc);
|
2026-03-12 13:21:00 -07:00
|
|
|
|
// Hover tooltip: show rich spell info for entries with a known spell
|
|
|
|
|
|
if (e.spellId != 0 && ImGui::IsItemHovered()) {
|
|
|
|
|
|
auto* assetMgrLog = core::Application::getInstance().getAssetManager();
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
bool richOk = spellbookScreen.renderSpellInfoTooltip(e.spellId, gameHandler, assetMgrLog);
|
|
|
|
|
|
if (!richOk) {
|
|
|
|
|
|
ImGui::Text("%s", spellName.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Auto-scroll to bottom
|
|
|
|
|
|
if (autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY())
|
|
|
|
|
|
ImGui::SetScrollHereY(1.0f);
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 02:09:35 -07:00
|
|
|
|
// ─── Achievement Window ───────────────────────────────────────────────────────
|
|
|
|
|
|
void GameScreen::renderAchievementWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!showAchievementWindow_) return;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(420, 480), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(200, 150), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
|
|
|
|
|
|
if (!ImGui::Begin("Achievements", &showAchievementWindow_)) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const auto& earned = gameHandler.getEarnedAchievements();
|
2026-03-12 03:03:02 -07:00
|
|
|
|
const auto& criteria = gameHandler.getCriteriaProgress();
|
|
|
|
|
|
|
2026-03-12 02:09:35 -07:00
|
|
|
|
ImGui::SetNextItemWidth(180.0f);
|
|
|
|
|
|
ImGui::InputText("##achsearch", achievementSearchBuf_, sizeof(achievementSearchBuf_));
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Clear")) achievementSearchBuf_[0] = '\0';
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
std::string filter(achievementSearchBuf_);
|
|
|
|
|
|
for (char& c : filter) c = static_cast<char>(tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
|
2026-03-12 03:03:02 -07:00
|
|
|
|
if (ImGui::BeginTabBar("##achtabs")) {
|
|
|
|
|
|
// --- Earned tab ---
|
|
|
|
|
|
char earnedLabel[32];
|
2026-03-25 11:40:49 -07:00
|
|
|
|
snprintf(earnedLabel, sizeof(earnedLabel), "Earned (%u)###earned", static_cast<unsigned>(earned.size()));
|
2026-03-12 03:03:02 -07:00
|
|
|
|
if (ImGui::BeginTabItem(earnedLabel)) {
|
|
|
|
|
|
if (earned.empty()) {
|
|
|
|
|
|
ImGui::TextDisabled("No achievements earned yet.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::BeginChild("##achlist", ImVec2(0, 0), false);
|
|
|
|
|
|
std::vector<uint32_t> ids(earned.begin(), earned.end());
|
|
|
|
|
|
std::sort(ids.begin(), ids.end());
|
|
|
|
|
|
for (uint32_t id : ids) {
|
|
|
|
|
|
const std::string& name = gameHandler.getAchievementName(id);
|
|
|
|
|
|
const std::string& display = name.empty() ? std::to_string(id) : name;
|
|
|
|
|
|
if (!filter.empty()) {
|
|
|
|
|
|
std::string lower = display;
|
|
|
|
|
|
for (char& c : lower) c = static_cast<char>(tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
if (lower.find(filter) == std::string::npos) continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(id));
|
refactor: add 6 color constants, replace 61 inline literals, remove const_cast
- Add kBrightGold, kPaleRed, kBrightRed, kLightBlue, kManaBlue, kCyan to ui_colors.hpp
- Replace 61 inline ImVec4 color literals across game_screen, inventory_screen,
talent_screen, and world_map with named constants
- Remove const_cast in character_renderer render loop by using non-const iteration
2026-03-27 10:08:30 -07:00
|
|
|
|
ImGui::TextColored(colors::kBrightGold, "\xE2\x98\x85");
|
2026-03-12 03:03:02 -07:00
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextUnformatted(display.c_str());
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
2026-03-12 12:49:38 -07:00
|
|
|
|
// Points badge
|
|
|
|
|
|
uint32_t pts = gameHandler.getAchievementPoints(id);
|
|
|
|
|
|
if (pts > 0) {
|
refactor: add 6 color constants, replace 61 inline literals, remove const_cast
- Add kBrightGold, kPaleRed, kBrightRed, kLightBlue, kManaBlue, kCyan to ui_colors.hpp
- Replace 61 inline ImVec4 color literals across game_screen, inventory_screen,
talent_screen, and world_map with named constants
- Remove const_cast in character_renderer render loop by using non-const iteration
2026-03-27 10:08:30 -07:00
|
|
|
|
ImGui::TextColored(colors::kBrightGold,
|
2026-03-12 12:49:38 -07:00
|
|
|
|
"%u Achievement Point%s", pts, pts == 1 ? "" : "s");
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Description
|
|
|
|
|
|
const std::string& desc = gameHandler.getAchievementDescription(id);
|
|
|
|
|
|
if (!desc.empty()) {
|
|
|
|
|
|
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + 320.0f);
|
|
|
|
|
|
ImGui::TextUnformatted(desc.c_str());
|
|
|
|
|
|
ImGui::PopTextWrapPos();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Earn date
|
2026-03-12 07:22:36 -07:00
|
|
|
|
uint32_t packed = gameHandler.getAchievementDate(id);
|
|
|
|
|
|
if (packed != 0) {
|
|
|
|
|
|
// WoW PackedTime: year[31:25] month[24:21] day[20:17] weekday[16:14] hour[13:9] minute[8:3]
|
|
|
|
|
|
int minute = (packed >> 3) & 0x3F;
|
|
|
|
|
|
int hour = (packed >> 9) & 0x1F;
|
|
|
|
|
|
int day = (packed >> 17) & 0x1F;
|
|
|
|
|
|
int month = (packed >> 21) & 0x0F;
|
|
|
|
|
|
int year = ((packed >> 25) & 0x7F) + 2000;
|
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
|
|
|
|
const char* mname = (month >= 1 && month <= 12) ? kMonthAbbrev[month - 1] : "?";
|
2026-03-12 12:49:38 -07:00
|
|
|
|
ImGui::TextDisabled("Earned: %s %d, %d %02d:%02d", mname, day, year, hour, minute);
|
2026-03-12 07:22:36 -07:00
|
|
|
|
}
|
2026-03-12 03:03:02 -07:00
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTabItem();
|
2026-03-12 02:09:35 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 03:03:02 -07:00
|
|
|
|
// --- Criteria progress tab ---
|
|
|
|
|
|
char critLabel[32];
|
2026-03-25 11:40:49 -07:00
|
|
|
|
snprintf(critLabel, sizeof(critLabel), "Criteria (%u)###crit", static_cast<unsigned>(criteria.size()));
|
2026-03-12 03:03:02 -07:00
|
|
|
|
if (ImGui::BeginTabItem(critLabel)) {
|
2026-03-12 12:52:08 -07:00
|
|
|
|
// Lazy-load AchievementCriteria.dbc for descriptions
|
|
|
|
|
|
struct CriteriaEntry { uint32_t achievementId; uint64_t quantity; std::string description; };
|
|
|
|
|
|
static std::unordered_map<uint32_t, CriteriaEntry> s_criteriaData;
|
|
|
|
|
|
static bool s_criteriaDataLoaded = false;
|
|
|
|
|
|
if (!s_criteriaDataLoaded) {
|
|
|
|
|
|
s_criteriaDataLoaded = true;
|
|
|
|
|
|
auto* am = core::Application::getInstance().getAssetManager();
|
|
|
|
|
|
if (am && am->isInitialized()) {
|
|
|
|
|
|
auto dbc = am->loadDBC("AchievementCriteria.dbc");
|
|
|
|
|
|
if (dbc && dbc->isLoaded() && dbc->getFieldCount() >= 10) {
|
|
|
|
|
|
const auto* acL = pipeline::getActiveDBCLayout()
|
|
|
|
|
|
? pipeline::getActiveDBCLayout()->getLayout("AchievementCriteria") : nullptr;
|
|
|
|
|
|
uint32_t achField = acL ? acL->field("AchievementID") : 1u;
|
|
|
|
|
|
uint32_t qtyField = acL ? acL->field("Quantity") : 4u;
|
|
|
|
|
|
uint32_t descField = acL ? acL->field("Description") : 9u;
|
|
|
|
|
|
if (achField == 0xFFFFFFFF) achField = 1;
|
|
|
|
|
|
if (qtyField == 0xFFFFFFFF) qtyField = 4;
|
|
|
|
|
|
if (descField == 0xFFFFFFFF) descField = 9;
|
|
|
|
|
|
uint32_t fc = dbc->getFieldCount();
|
|
|
|
|
|
for (uint32_t r = 0; r < dbc->getRecordCount(); ++r) {
|
|
|
|
|
|
uint32_t cid = dbc->getUInt32(r, 0);
|
|
|
|
|
|
if (cid == 0) continue;
|
|
|
|
|
|
CriteriaEntry ce;
|
|
|
|
|
|
ce.achievementId = (achField < fc) ? dbc->getUInt32(r, achField) : 0;
|
|
|
|
|
|
ce.quantity = (qtyField < fc) ? dbc->getUInt32(r, qtyField) : 0;
|
|
|
|
|
|
ce.description = (descField < fc) ? dbc->getString(r, descField) : std::string{};
|
|
|
|
|
|
s_criteriaData[cid] = std::move(ce);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 03:03:02 -07:00
|
|
|
|
if (criteria.empty()) {
|
|
|
|
|
|
ImGui::TextDisabled("No criteria progress received yet.");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::BeginChild("##critlist", ImVec2(0, 0), false);
|
|
|
|
|
|
std::vector<std::pair<uint32_t, uint64_t>> clist(criteria.begin(), criteria.end());
|
|
|
|
|
|
std::sort(clist.begin(), clist.end());
|
|
|
|
|
|
for (const auto& [cid, cval] : clist) {
|
2026-03-12 12:52:08 -07:00
|
|
|
|
auto ceIt = s_criteriaData.find(cid);
|
|
|
|
|
|
|
|
|
|
|
|
// Build display text for filtering
|
|
|
|
|
|
std::string display;
|
|
|
|
|
|
if (ceIt != s_criteriaData.end() && !ceIt->second.description.empty()) {
|
|
|
|
|
|
display = ceIt->second.description;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
display = std::to_string(cid);
|
|
|
|
|
|
}
|
2026-03-12 03:03:02 -07:00
|
|
|
|
if (!filter.empty()) {
|
2026-03-12 12:52:08 -07:00
|
|
|
|
std::string lower = display;
|
2026-03-12 03:03:02 -07:00
|
|
|
|
for (char& c : lower) c = static_cast<char>(tolower(static_cast<unsigned char>(c)));
|
2026-03-12 12:52:08 -07:00
|
|
|
|
// Also allow filtering by achievement name
|
|
|
|
|
|
if (lower.find(filter) == std::string::npos && ceIt != s_criteriaData.end()) {
|
|
|
|
|
|
const std::string& achName = gameHandler.getAchievementName(ceIt->second.achievementId);
|
|
|
|
|
|
std::string achLower = achName;
|
|
|
|
|
|
for (char& c : achLower) c = static_cast<char>(tolower(static_cast<unsigned char>(c)));
|
|
|
|
|
|
if (achLower.find(filter) == std::string::npos) continue;
|
|
|
|
|
|
} else if (lower.find(filter) == std::string::npos) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2026-03-12 03:03:02 -07:00
|
|
|
|
}
|
2026-03-12 12:52:08 -07:00
|
|
|
|
|
2026-03-12 03:03:02 -07:00
|
|
|
|
ImGui::PushID(static_cast<int>(cid));
|
2026-03-12 12:52:08 -07:00
|
|
|
|
if (ceIt != s_criteriaData.end()) {
|
|
|
|
|
|
// Show achievement name as header (dim)
|
|
|
|
|
|
const std::string& achName = gameHandler.getAchievementName(ceIt->second.achievementId);
|
|
|
|
|
|
if (!achName.empty()) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.85f, 0.0f, 0.8f), "%s", achName.c_str());
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextDisabled(">");
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!ceIt->second.description.empty()) {
|
|
|
|
|
|
ImGui::TextUnformatted(ceIt->second.description.c_str());
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextDisabled("Criteria %u", cid);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ceIt->second.quantity > 0) {
|
2026-03-27 13:57:29 -07:00
|
|
|
|
ImGui::TextColored(colors::kLightGreen,
|
2026-03-12 12:52:08 -07:00
|
|
|
|
"%llu/%llu",
|
|
|
|
|
|
static_cast<unsigned long long>(cval),
|
|
|
|
|
|
static_cast<unsigned long long>(ceIt->second.quantity));
|
|
|
|
|
|
} else {
|
2026-03-27 13:57:29 -07:00
|
|
|
|
ImGui::TextColored(colors::kLightGreen,
|
2026-03-12 12:52:08 -07:00
|
|
|
|
"%llu", static_cast<unsigned long long>(cval));
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextDisabled("Criteria %u:", cid);
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::Text("%llu", static_cast<unsigned long long>(cval));
|
|
|
|
|
|
}
|
2026-03-12 03:03:02 -07:00
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTabItem();
|
2026-03-12 02:09:35 -07:00
|
|
|
|
}
|
2026-03-12 03:03:02 -07:00
|
|
|
|
ImGui::EndTabBar();
|
2026-03-12 02:09:35 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 02:31:12 -07:00
|
|
|
|
// ─── GM Ticket Window ─────────────────────────────────────────────────────────
|
|
|
|
|
|
void GameScreen::renderGmTicketWindow(game::GameHandler& gameHandler) {
|
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
|
|
|
|
// Fire a one-shot query when the window first becomes visible
|
|
|
|
|
|
if (showGmTicketWindow_ && !gmTicketWindowWasOpen_) {
|
|
|
|
|
|
gameHandler.requestGmTicket();
|
|
|
|
|
|
}
|
|
|
|
|
|
gmTicketWindowWasOpen_ = showGmTicketWindow_;
|
|
|
|
|
|
|
2026-03-12 02:31:12 -07:00
|
|
|
|
if (!showGmTicketWindow_) return;
|
|
|
|
|
|
|
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
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(440, 320), ImGuiCond_FirstUseEver);
|
2026-03-12 02:31:12 -07:00
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(300, 200), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
|
|
|
|
|
|
if (!ImGui::Begin("GM Ticket", &showGmTicketWindow_,
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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
|
|
|
|
// Show GM support availability
|
|
|
|
|
|
if (!gameHandler.isGmSupportAvailable()) {
|
2026-03-27 10:20:45 -07:00
|
|
|
|
ImGui::TextColored(colors::kSoftRed, "GM support is currently unavailable.");
|
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
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Show existing open ticket if any
|
|
|
|
|
|
if (gameHandler.hasActiveGmTicket()) {
|
2026-03-25 11:57:22 -07:00
|
|
|
|
ImGui::TextColored(kColorGreen, "You have an open GM ticket.");
|
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
|
|
|
|
const std::string& existingText = gameHandler.getGmTicketText();
|
|
|
|
|
|
if (!existingText.empty()) {
|
|
|
|
|
|
ImGui::TextWrapped("Current ticket: %s", existingText.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
float waitHours = gameHandler.getGmTicketWaitHours();
|
|
|
|
|
|
if (waitHours > 0.0f) {
|
|
|
|
|
|
char waitBuf[64];
|
|
|
|
|
|
std::snprintf(waitBuf, sizeof(waitBuf), "Estimated wait: %.1f hours", waitHours);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.8f, 0.8f, 0.4f, 1.0f), "%s", waitBuf);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 02:31:12 -07:00
|
|
|
|
ImGui::TextWrapped("Describe your issue and a Game Master will contact you.");
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::InputTextMultiline("##gmticket_body", gmTicketBuf_, sizeof(gmTicketBuf_),
|
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
|
|
|
|
ImVec2(-1, 120));
|
2026-03-12 02:31:12 -07:00
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
bool hasText = (gmTicketBuf_[0] != '\0');
|
|
|
|
|
|
if (!hasText) ImGui::BeginDisabled();
|
|
|
|
|
|
if (ImGui::Button("Submit Ticket", ImVec2(160, 0))) {
|
|
|
|
|
|
gameHandler.submitGmTicket(gmTicketBuf_);
|
|
|
|
|
|
gmTicketBuf_[0] = '\0';
|
|
|
|
|
|
showGmTicketWindow_ = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!hasText) ImGui::EndDisabled();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (ImGui::Button("Cancel", ImVec2(80, 0))) {
|
|
|
|
|
|
showGmTicketWindow_ = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
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
|
|
|
|
if (gameHandler.hasActiveGmTicket()) {
|
|
|
|
|
|
if (ImGui::Button("Delete Ticket", ImVec2(110, 0))) {
|
|
|
|
|
|
gameHandler.deleteGmTicket();
|
|
|
|
|
|
showGmTicketWindow_ = false;
|
|
|
|
|
|
}
|
2026-03-12 02:31:12 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 02:59:09 -07:00
|
|
|
|
// ─── Threat Window ────────────────────────────────────────────────────────────
|
|
|
|
|
|
void GameScreen::renderThreatWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!showThreatWindow_) return;
|
|
|
|
|
|
|
|
|
|
|
|
const auto* list = gameHandler.getTargetThreatList();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(280, 220), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(10, 300), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
ImGui::SetNextWindowBgAlpha(0.85f);
|
|
|
|
|
|
|
|
|
|
|
|
if (!ImGui::Begin("Threat###ThreatWin", &showThreatWindow_,
|
|
|
|
|
|
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize)) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!list || list->empty()) {
|
|
|
|
|
|
ImGui::TextDisabled("No threat data for current target.");
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
uint32_t maxThreat = list->front().threat;
|
|
|
|
|
|
|
2026-03-12 07:32:28 -07:00
|
|
|
|
// Pre-scan to find the player's rank and threat percentage
|
|
|
|
|
|
uint64_t playerGuid = gameHandler.getPlayerGuid();
|
|
|
|
|
|
int playerRank = 0;
|
|
|
|
|
|
float playerPct = 0.0f;
|
|
|
|
|
|
{
|
|
|
|
|
|
int scan = 0;
|
|
|
|
|
|
for (const auto& e : *list) {
|
|
|
|
|
|
++scan;
|
|
|
|
|
|
if (e.victimGuid == playerGuid) {
|
|
|
|
|
|
playerRank = scan;
|
|
|
|
|
|
playerPct = (maxThreat > 0) ? static_cast<float>(e.threat) / static_cast<float>(maxThreat) : 0.0f;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (scan >= 10) break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Status bar: aggro alert or rank summary
|
|
|
|
|
|
if (playerRank == 1) {
|
|
|
|
|
|
// Player has aggro — persistent red warning
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.25f, 0.25f, 1.0f), "!! YOU HAVE AGGRO !!");
|
|
|
|
|
|
} else if (playerRank > 1 && playerPct >= 0.8f) {
|
|
|
|
|
|
// Close to pulling — pulsing warning
|
|
|
|
|
|
float pulse = 0.55f + 0.45f * sinf(static_cast<float>(ImGui::GetTime()) * 5.0f);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.55f, 0.1f, pulse), "! PULLING AGGRO (%.0f%%) !", playerPct * 100.0f);
|
|
|
|
|
|
} else if (playerRank > 0) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 0.8f, 0.6f, 1.0f), "You: #%d %.0f%% threat", playerRank, playerPct * 100.0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 02:59:09 -07:00
|
|
|
|
ImGui::TextDisabled("%-19s Threat", "Player");
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
int rank = 0;
|
|
|
|
|
|
for (const auto& entry : *list) {
|
|
|
|
|
|
++rank;
|
|
|
|
|
|
bool isPlayer = (entry.victimGuid == playerGuid);
|
|
|
|
|
|
|
|
|
|
|
|
// Resolve name
|
|
|
|
|
|
std::string victimName;
|
|
|
|
|
|
auto entity = gameHandler.getEntityManager().getEntity(entry.victimGuid);
|
|
|
|
|
|
if (entity) {
|
|
|
|
|
|
if (entity->getType() == game::ObjectType::PLAYER) {
|
|
|
|
|
|
auto p = std::static_pointer_cast<game::Player>(entity);
|
|
|
|
|
|
victimName = p->getName().empty() ? "Player" : p->getName();
|
|
|
|
|
|
} else if (entity->getType() == game::ObjectType::UNIT) {
|
|
|
|
|
|
auto u = std::static_pointer_cast<game::Unit>(entity);
|
|
|
|
|
|
victimName = u->getName().empty() ? "NPC" : u->getName();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (victimName.empty())
|
|
|
|
|
|
victimName = "0x" + [&](){
|
|
|
|
|
|
char buf[20]; snprintf(buf, sizeof(buf), "%llX",
|
|
|
|
|
|
static_cast<unsigned long long>(entry.victimGuid)); return std::string(buf); }();
|
|
|
|
|
|
|
|
|
|
|
|
// Colour: gold for #1 (tank), red if player is highest, white otherwise
|
2026-03-25 19:37:22 -07:00
|
|
|
|
ImVec4 col = ui::colors::kWhite;
|
2026-03-25 19:30:23 -07:00
|
|
|
|
if (rank == 1) col = ui::colors::kTooltipGold; // gold
|
2026-03-25 11:57:22 -07:00
|
|
|
|
if (isPlayer && rank == 1) col = kColorRed; // red — you have aggro
|
2026-03-12 02:59:09 -07:00
|
|
|
|
|
|
|
|
|
|
// Threat bar
|
2026-03-25 11:40:49 -07:00
|
|
|
|
float pct = (maxThreat > 0) ? static_cast<float>(entry.threat) / static_cast<float>(maxThreat) : 0.0f;
|
2026-03-12 02:59:09 -07:00
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram,
|
|
|
|
|
|
isPlayer ? ImVec4(0.8f, 0.2f, 0.2f, 0.7f) : ImVec4(0.2f, 0.5f, 0.8f, 0.5f));
|
|
|
|
|
|
char barLabel[48];
|
|
|
|
|
|
snprintf(barLabel, sizeof(barLabel), "%.0f%%", pct * 100.0f);
|
|
|
|
|
|
ImGui::ProgressBar(pct, ImVec2(60, 14), barLabel);
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TextColored(col, "%-18s %u", victimName.c_str(), entry.threat);
|
|
|
|
|
|
|
|
|
|
|
|
if (rank >= 10) break; // cap display at 10 entries
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 12:02:59 -07:00
|
|
|
|
// ─── BG Scoreboard ────────────────────────────────────────────────────────────
|
|
|
|
|
|
void GameScreen::renderBgScoreboard(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!showBgScoreboard_) return;
|
|
|
|
|
|
|
|
|
|
|
|
const game::GameHandler::BgScoreboardData* data = gameHandler.getBgScoreboard();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(600, 400), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(150, 100), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
|
2026-03-12 23:46:38 -07:00
|
|
|
|
const char* title = data && data->isArena ? "Arena Score###BgScore"
|
|
|
|
|
|
: "Battleground Score###BgScore";
|
2026-03-12 12:02:59 -07:00
|
|
|
|
if (!ImGui::Begin(title, &showBgScoreboard_, ImGuiWindowFlags_NoCollapse)) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!data) {
|
|
|
|
|
|
ImGui::TextDisabled("No score data yet.");
|
2026-03-12 23:46:38 -07:00
|
|
|
|
ImGui::TextDisabled("Use /score to request the scoreboard while in a battleground or arena.");
|
2026-03-12 12:02:59 -07:00
|
|
|
|
ImGui::End();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 23:46:38 -07:00
|
|
|
|
// Arena team rating banner (shown only for arenas)
|
|
|
|
|
|
if (data->isArena) {
|
|
|
|
|
|
for (int t = 0; t < 2; ++t) {
|
|
|
|
|
|
const auto& at = data->arenaTeams[t];
|
|
|
|
|
|
if (at.teamName.empty()) continue;
|
|
|
|
|
|
int32_t ratingDelta = static_cast<int32_t>(at.ratingChange);
|
2026-03-27 10:20:45 -07:00
|
|
|
|
ImVec4 teamCol = (t == 0) ? colors::kHostileRed // team 0: red
|
refactor: add 6 color constants, replace 61 inline literals, remove const_cast
- Add kBrightGold, kPaleRed, kBrightRed, kLightBlue, kManaBlue, kCyan to ui_colors.hpp
- Replace 61 inline ImVec4 color literals across game_screen, inventory_screen,
talent_screen, and world_map with named constants
- Remove const_cast in character_renderer render loop by using non-const iteration
2026-03-27 10:08:30 -07:00
|
|
|
|
: colors::kLightBlue; // team 1: blue
|
2026-03-12 23:46:38 -07:00
|
|
|
|
ImGui::TextColored(teamCol, "%s", at.teamName.c_str());
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
char ratingBuf[32];
|
|
|
|
|
|
if (ratingDelta >= 0)
|
|
|
|
|
|
std::snprintf(ratingBuf, sizeof(ratingBuf), "Rating: %u (+%d)", at.newRating, ratingDelta);
|
|
|
|
|
|
else
|
|
|
|
|
|
std::snprintf(ratingBuf, sizeof(ratingBuf), "Rating: %u (%d)", at.newRating, ratingDelta);
|
|
|
|
|
|
ImGui::TextDisabled("%s", ratingBuf);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 12:02:59 -07:00
|
|
|
|
// Winner banner
|
|
|
|
|
|
if (data->hasWinner) {
|
2026-03-12 23:46:38 -07:00
|
|
|
|
const char* winnerStr;
|
|
|
|
|
|
ImVec4 winnerColor;
|
|
|
|
|
|
if (data->isArena) {
|
|
|
|
|
|
// For arenas, winner byte 0/1 refers to team index in arenaTeams[]
|
|
|
|
|
|
const auto& winTeam = data->arenaTeams[data->winner & 1];
|
|
|
|
|
|
winnerStr = winTeam.teamName.empty() ? "Team 1" : winTeam.teamName.c_str();
|
2026-03-27 10:20:45 -07:00
|
|
|
|
winnerColor = (data->winner == 0) ? colors::kHostileRed
|
refactor: add 6 color constants, replace 61 inline literals, remove const_cast
- Add kBrightGold, kPaleRed, kBrightRed, kLightBlue, kManaBlue, kCyan to ui_colors.hpp
- Replace 61 inline ImVec4 color literals across game_screen, inventory_screen,
talent_screen, and world_map with named constants
- Remove const_cast in character_renderer render loop by using non-const iteration
2026-03-27 10:08:30 -07:00
|
|
|
|
: colors::kLightBlue;
|
2026-03-12 23:46:38 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
winnerStr = (data->winner == 1) ? "Alliance" : "Horde";
|
refactor: add 6 color constants, replace 61 inline literals, remove const_cast
- Add kBrightGold, kPaleRed, kBrightRed, kLightBlue, kManaBlue, kCyan to ui_colors.hpp
- Replace 61 inline ImVec4 color literals across game_screen, inventory_screen,
talent_screen, and world_map with named constants
- Remove const_cast in character_renderer render loop by using non-const iteration
2026-03-27 10:08:30 -07:00
|
|
|
|
winnerColor = (data->winner == 1) ? colors::kLightBlue
|
2026-03-27 10:20:45 -07:00
|
|
|
|
: colors::kHostileRed;
|
2026-03-12 23:46:38 -07:00
|
|
|
|
}
|
2026-03-12 12:02:59 -07:00
|
|
|
|
float textW = ImGui::CalcTextSize(winnerStr).x + ImGui::CalcTextSize(" Victory!").x;
|
|
|
|
|
|
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - textW) * 0.5f);
|
|
|
|
|
|
ImGui::TextColored(winnerColor, "%s", winnerStr);
|
|
|
|
|
|
ImGui::SameLine(0, 4);
|
refactor: add 6 color constants, replace 61 inline literals, remove const_cast
- Add kBrightGold, kPaleRed, kBrightRed, kLightBlue, kManaBlue, kCyan to ui_colors.hpp
- Replace 61 inline ImVec4 color literals across game_screen, inventory_screen,
talent_screen, and world_map with named constants
- Remove const_cast in character_renderer render loop by using non-const iteration
2026-03-27 10:08:30 -07:00
|
|
|
|
ImGui::TextColored(colors::kBrightGold, "Victory!");
|
2026-03-12 12:02:59 -07:00
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Refresh button
|
|
|
|
|
|
if (ImGui::SmallButton("Refresh")) {
|
|
|
|
|
|
gameHandler.requestPvpLog();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextDisabled("%zu players", data->players.size());
|
|
|
|
|
|
|
|
|
|
|
|
// Score table
|
|
|
|
|
|
constexpr ImGuiTableFlags kTableFlags =
|
|
|
|
|
|
ImGuiTableFlags_ScrollY | ImGuiTableFlags_RowBg |
|
|
|
|
|
|
ImGuiTableFlags_BordersOuter | ImGuiTableFlags_BordersV |
|
|
|
|
|
|
ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_Sortable;
|
|
|
|
|
|
|
|
|
|
|
|
// Build dynamic column count based on what BG-specific stats are present
|
|
|
|
|
|
int numBgCols = 0;
|
|
|
|
|
|
std::vector<std::string> bgColNames;
|
|
|
|
|
|
for (const auto& ps : data->players) {
|
|
|
|
|
|
for (const auto& [fieldName, val] : ps.bgStats) {
|
|
|
|
|
|
// Extract short name after last '.' (e.g. "BattlegroundAB.AbFlagCaptures" → "Caps")
|
|
|
|
|
|
std::string shortName = fieldName;
|
|
|
|
|
|
auto dotPos = fieldName.rfind('.');
|
|
|
|
|
|
if (dotPos != std::string::npos) shortName = fieldName.substr(dotPos + 1);
|
|
|
|
|
|
bool found = false;
|
|
|
|
|
|
for (const auto& n : bgColNames) { if (n == shortName) { found = true; break; } }
|
|
|
|
|
|
if (!found) bgColNames.push_back(shortName);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
numBgCols = static_cast<int>(bgColNames.size());
|
|
|
|
|
|
|
|
|
|
|
|
// Fixed cols: Team | Name | KB | Deaths | HKs | Honor; then BG-specific
|
|
|
|
|
|
int totalCols = 6 + numBgCols;
|
|
|
|
|
|
float tableH = ImGui::GetContentRegionAvail().y;
|
|
|
|
|
|
if (ImGui::BeginTable("##BgScoreTable", totalCols, kTableFlags, ImVec2(0.0f, tableH))) {
|
|
|
|
|
|
ImGui::TableSetupScrollFreeze(0, 1);
|
|
|
|
|
|
ImGui::TableSetupColumn("Team", ImGuiTableColumnFlags_WidthFixed, 56.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
|
|
|
|
|
|
ImGui::TableSetupColumn("KB", ImGuiTableColumnFlags_WidthFixed, 38.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Deaths", ImGuiTableColumnFlags_WidthFixed, 52.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("HKs", ImGuiTableColumnFlags_WidthFixed, 38.0f);
|
|
|
|
|
|
ImGui::TableSetupColumn("Honor", ImGuiTableColumnFlags_WidthFixed, 52.0f);
|
|
|
|
|
|
for (const auto& col : bgColNames)
|
|
|
|
|
|
ImGui::TableSetupColumn(col.c_str(), ImGuiTableColumnFlags_WidthFixed, 52.0f);
|
|
|
|
|
|
ImGui::TableHeadersRow();
|
|
|
|
|
|
|
|
|
|
|
|
// Sort: Alliance first, then Horde; within each team by KB desc
|
|
|
|
|
|
std::vector<const game::GameHandler::BgPlayerScore*> sorted;
|
|
|
|
|
|
sorted.reserve(data->players.size());
|
|
|
|
|
|
for (const auto& ps : data->players) sorted.push_back(&ps);
|
|
|
|
|
|
std::stable_sort(sorted.begin(), sorted.end(),
|
|
|
|
|
|
[](const game::GameHandler::BgPlayerScore* a,
|
|
|
|
|
|
const game::GameHandler::BgPlayerScore* b) {
|
|
|
|
|
|
if (a->team != b->team) return a->team > b->team; // Alliance(1) first
|
|
|
|
|
|
return a->killingBlows > b->killingBlows;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
uint64_t playerGuid = gameHandler.getPlayerGuid();
|
|
|
|
|
|
for (const auto* ps : sorted) {
|
|
|
|
|
|
ImGui::TableNextRow();
|
|
|
|
|
|
|
|
|
|
|
|
// Team
|
|
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
|
|
if (ps->team == 1)
|
refactor: add 6 color constants, replace 61 inline literals, remove const_cast
- Add kBrightGold, kPaleRed, kBrightRed, kLightBlue, kManaBlue, kCyan to ui_colors.hpp
- Replace 61 inline ImVec4 color literals across game_screen, inventory_screen,
talent_screen, and world_map with named constants
- Remove const_cast in character_renderer render loop by using non-const iteration
2026-03-27 10:08:30 -07:00
|
|
|
|
ImGui::TextColored(colors::kLightBlue, "Alliance");
|
2026-03-12 12:02:59 -07:00
|
|
|
|
else
|
2026-03-27 10:20:45 -07:00
|
|
|
|
ImGui::TextColored(colors::kHostileRed, "Horde");
|
2026-03-12 12:02:59 -07:00
|
|
|
|
|
|
|
|
|
|
// Name (highlight player's own row)
|
|
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
|
|
bool isSelf = (ps->guid == playerGuid);
|
refactor: add 6 color constants, replace 61 inline literals, remove const_cast
- Add kBrightGold, kPaleRed, kBrightRed, kLightBlue, kManaBlue, kCyan to ui_colors.hpp
- Replace 61 inline ImVec4 color literals across game_screen, inventory_screen,
talent_screen, and world_map with named constants
- Remove const_cast in character_renderer render loop by using non-const iteration
2026-03-27 10:08:30 -07:00
|
|
|
|
if (isSelf) ImGui::PushStyleColor(ImGuiCol_Text, colors::kBrightGold);
|
2026-03-12 12:02:59 -07:00
|
|
|
|
const char* nameStr = ps->name.empty() ? "Unknown" : ps->name.c_str();
|
|
|
|
|
|
ImGui::TextUnformatted(nameStr);
|
|
|
|
|
|
if (isSelf) ImGui::PopStyleColor();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TableNextColumn(); ImGui::Text("%u", ps->killingBlows);
|
|
|
|
|
|
ImGui::TableNextColumn(); ImGui::Text("%u", ps->deaths);
|
|
|
|
|
|
ImGui::TableNextColumn(); ImGui::Text("%u", ps->honorableKills);
|
|
|
|
|
|
ImGui::TableNextColumn(); ImGui::Text("%u", ps->bonusHonor);
|
|
|
|
|
|
|
|
|
|
|
|
for (const auto& col : bgColNames) {
|
|
|
|
|
|
ImGui::TableNextColumn();
|
|
|
|
|
|
uint32_t val = 0;
|
|
|
|
|
|
for (const auto& [fieldName, fval] : ps->bgStats) {
|
|
|
|
|
|
std::string shortName = fieldName;
|
|
|
|
|
|
auto dotPos = fieldName.rfind('.');
|
|
|
|
|
|
if (dotPos != std::string::npos) shortName = fieldName.substr(dotPos + 1);
|
|
|
|
|
|
if (shortName == col) { val = fval; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
if (val > 0) ImGui::Text("%u", val);
|
|
|
|
|
|
else ImGui::TextDisabled("-");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndTable();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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-12 03:39:10 -07:00
|
|
|
|
|
2026-03-12 18:21:50 -07:00
|
|
|
|
// ─── Book / Scroll / Note Window ──────────────────────────────────────────────
|
|
|
|
|
|
void GameScreen::renderBookWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
// Auto-open when new pages arrive
|
|
|
|
|
|
if (gameHandler.hasBookOpen() && !showBookWindow_) {
|
|
|
|
|
|
showBookWindow_ = true;
|
|
|
|
|
|
bookCurrentPage_ = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!showBookWindow_) return;
|
|
|
|
|
|
|
|
|
|
|
|
const auto& pages = gameHandler.getBookPages();
|
|
|
|
|
|
if (pages.empty()) { showBookWindow_ = false; return; }
|
|
|
|
|
|
|
|
|
|
|
|
// Clamp page index
|
|
|
|
|
|
if (bookCurrentPage_ < 0) bookCurrentPage_ = 0;
|
|
|
|
|
|
if (bookCurrentPage_ >= static_cast<int>(pages.size()))
|
|
|
|
|
|
bookCurrentPage_ = static_cast<int>(pages.size()) - 1;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(420, 340), ImGuiCond_Appearing);
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(400, 180), ImGuiCond_Appearing);
|
|
|
|
|
|
|
|
|
|
|
|
bool open = showBookWindow_;
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.12f, 0.09f, 0.06f, 0.98f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_TitleBgActive, ImVec4(0.25f, 0.18f, 0.08f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0.5f, 0.37f, 0.18f, 1.0f));
|
|
|
|
|
|
|
|
|
|
|
|
char title[64];
|
|
|
|
|
|
if (pages.size() > 1)
|
|
|
|
|
|
snprintf(title, sizeof(title), "Page %d / %d###BookWin",
|
|
|
|
|
|
bookCurrentPage_ + 1, static_cast<int>(pages.size()));
|
|
|
|
|
|
else
|
|
|
|
|
|
snprintf(title, sizeof(title), "###BookWin");
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::Begin(title, &open, ImGuiWindowFlags_NoCollapse)) {
|
|
|
|
|
|
// Parchment text colour
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.85f, 0.78f, 0.62f, 1.0f));
|
|
|
|
|
|
|
|
|
|
|
|
const std::string& text = pages[bookCurrentPage_].text;
|
|
|
|
|
|
// Use a child region with word-wrap
|
|
|
|
|
|
ImGui::SetNextWindowContentSize(ImVec2(ImGui::GetContentRegionAvail().x, 0));
|
|
|
|
|
|
if (ImGui::BeginChild("##BookText",
|
|
|
|
|
|
ImVec2(0, ImGui::GetContentRegionAvail().y - 34),
|
|
|
|
|
|
false, ImGuiWindowFlags_HorizontalScrollbar)) {
|
|
|
|
|
|
ImGui::SetNextItemWidth(-1);
|
|
|
|
|
|
ImGui::TextWrapped("%s", text.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
|
|
|
|
|
|
// Navigation row
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
bool canPrev = (bookCurrentPage_ > 0);
|
|
|
|
|
|
bool canNext = (bookCurrentPage_ < static_cast<int>(pages.size()) - 1);
|
|
|
|
|
|
|
|
|
|
|
|
if (!canPrev) ImGui::BeginDisabled();
|
|
|
|
|
|
if (ImGui::Button("< Prev", ImVec2(80, 0))) bookCurrentPage_--;
|
|
|
|
|
|
if (!canPrev) ImGui::EndDisabled();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
if (!canNext) ImGui::BeginDisabled();
|
|
|
|
|
|
if (ImGui::Button("Next >", ImVec2(80, 0))) bookCurrentPage_++;
|
|
|
|
|
|
if (!canNext) ImGui::EndDisabled();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine(ImGui::GetContentRegionAvail().x - 60);
|
|
|
|
|
|
if (ImGui::Button("Close", ImVec2(60, 0))) {
|
|
|
|
|
|
open = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
ImGui::PopStyleColor(3);
|
|
|
|
|
|
|
|
|
|
|
|
if (!open) {
|
|
|
|
|
|
showBookWindow_ = false;
|
|
|
|
|
|
gameHandler.clearBook();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 02:52:40 -07:00
|
|
|
|
// ─── Inspect Window ───────────────────────────────────────────────────────────
|
|
|
|
|
|
void GameScreen::renderInspectWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!showInspectWindow_) return;
|
|
|
|
|
|
|
2026-03-12 12:32:19 -07:00
|
|
|
|
// Lazy-load SpellItemEnchantment.dbc for enchant name lookup
|
|
|
|
|
|
static std::unordered_map<uint32_t, std::string> s_enchantNames;
|
|
|
|
|
|
static bool s_enchantDbLoaded = false;
|
|
|
|
|
|
auto* assetMgrEnchant = core::Application::getInstance().getAssetManager();
|
|
|
|
|
|
if (!s_enchantDbLoaded && assetMgrEnchant && assetMgrEnchant->isInitialized()) {
|
|
|
|
|
|
s_enchantDbLoaded = true;
|
|
|
|
|
|
auto dbc = assetMgrEnchant->loadDBC("SpellItemEnchantment.dbc");
|
|
|
|
|
|
if (dbc && dbc->isLoaded()) {
|
|
|
|
|
|
const auto* layout = pipeline::getActiveDBCLayout()
|
|
|
|
|
|
? pipeline::getActiveDBCLayout()->getLayout("SpellItemEnchantment")
|
|
|
|
|
|
: nullptr;
|
|
|
|
|
|
uint32_t idField = layout ? (*layout)["ID"] : 0;
|
|
|
|
|
|
uint32_t nameField = layout ? (*layout)["Name"] : 8;
|
|
|
|
|
|
for (uint32_t i = 0; i < dbc->getRecordCount(); ++i) {
|
|
|
|
|
|
uint32_t id = dbc->getUInt32(i, idField);
|
|
|
|
|
|
if (id == 0) continue;
|
|
|
|
|
|
std::string nm = dbc->getString(i, nameField);
|
|
|
|
|
|
if (!nm.empty()) s_enchantNames[id] = std::move(nm);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 02:52:40 -07:00
|
|
|
|
// Slot index 0..18 maps to equipment slots 1..19 (WoW convention: slot 0 unused on server)
|
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* kSlotNames[19] = {
|
2026-03-12 02:52:40 -07:00
|
|
|
|
"Head", "Neck", "Shoulder", "Shirt", "Chest",
|
|
|
|
|
|
"Waist", "Legs", "Feet", "Wrist", "Hands",
|
|
|
|
|
|
"Finger 1", "Finger 2", "Trinket 1", "Trinket 2", "Back",
|
|
|
|
|
|
"Main Hand", "Off Hand", "Ranged", "Tabard"
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(360, 440), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(350, 120), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
|
|
|
|
|
|
const game::GameHandler::InspectResult* result = gameHandler.getInspectResult();
|
|
|
|
|
|
|
|
|
|
|
|
std::string title = result ? ("Inspect: " + result->playerName + "###InspectWin")
|
|
|
|
|
|
: "Inspect###InspectWin";
|
|
|
|
|
|
if (!ImGui::Begin(title.c_str(), &showInspectWindow_, ImGuiWindowFlags_NoCollapse)) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!result) {
|
|
|
|
|
|
ImGui::TextDisabled("No inspect data yet. Target a player and use Inspect.");
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 08:47:59 -07:00
|
|
|
|
// Player name — class-colored if entity is loaded, else gold
|
|
|
|
|
|
{
|
|
|
|
|
|
auto ent = gameHandler.getEntityManager().getEntity(result->guid);
|
|
|
|
|
|
uint8_t cid = entityClassId(ent.get());
|
2026-03-25 19:30:23 -07:00
|
|
|
|
ImVec4 nameColor = (cid != 0) ? classColorVec4(cid) : ui::colors::kTooltipGold;
|
2026-03-12 08:47:59 -07:00
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, nameColor);
|
|
|
|
|
|
ImGui::Text("%s", result->playerName.c_str());
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
if (cid != 0) {
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextColored(classColorVec4(cid), "(%s)", classNameStr(cid));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-12 02:52:40 -07:00
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextDisabled(" %u talent pts", result->totalTalents);
|
|
|
|
|
|
if (result->unspentTalents > 0) {
|
|
|
|
|
|
ImGui::SameLine();
|
2026-03-27 10:20:45 -07:00
|
|
|
|
ImGui::TextColored(colors::kSoftRed, "(%u unspent)", result->unspentTalents);
|
2026-03-12 02:52:40 -07:00
|
|
|
|
}
|
|
|
|
|
|
if (result->talentGroups > 1) {
|
|
|
|
|
|
ImGui::SameLine();
|
2026-03-25 11:40:49 -07:00
|
|
|
|
ImGui::TextDisabled(" Dual spec (active %u)", static_cast<unsigned>(result->activeTalentGroup) + 1);
|
2026-03-12 02:52:40 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
// Equipment list
|
|
|
|
|
|
bool hasAnyGear = false;
|
|
|
|
|
|
for (int s = 0; s < 19; ++s) {
|
|
|
|
|
|
if (result->itemEntries[s] != 0) { hasAnyGear = true; break; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!hasAnyGear) {
|
|
|
|
|
|
ImGui::TextDisabled("Equipment data not yet available.");
|
|
|
|
|
|
ImGui::TextDisabled("(Gear loads after the player is inspected in-range)");
|
|
|
|
|
|
} else {
|
2026-03-12 08:47:59 -07:00
|
|
|
|
// Average item level (only slots that have loaded info and are not shirt/tabard)
|
|
|
|
|
|
// Shirt=slot3, Tabard=slot18 — excluded from gear score by WoW convention
|
|
|
|
|
|
uint32_t iLevelSum = 0;
|
|
|
|
|
|
int iLevelCount = 0;
|
|
|
|
|
|
for (int s = 0; s < 19; ++s) {
|
|
|
|
|
|
if (s == 3 || s == 18) continue; // shirt, tabard
|
|
|
|
|
|
uint32_t entry = result->itemEntries[s];
|
|
|
|
|
|
if (entry == 0) continue;
|
|
|
|
|
|
const game::ItemQueryResponseData* info = gameHandler.getItemInfo(entry);
|
|
|
|
|
|
if (info && info->valid && info->itemLevel > 0) {
|
|
|
|
|
|
iLevelSum += info->itemLevel;
|
|
|
|
|
|
++iLevelCount;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (iLevelCount > 0) {
|
|
|
|
|
|
float avgIlvl = static_cast<float>(iLevelSum) / static_cast<float>(iLevelCount);
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Avg iLvl: %.1f", avgIlvl);
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextDisabled("(%d/%d slots loaded)", iLevelCount,
|
|
|
|
|
|
[&]{ int c=0; for(int s=0;s<19;++s){if(s==3||s==18)continue;if(result->itemEntries[s])++c;} return c; }());
|
|
|
|
|
|
}
|
2026-03-12 02:52:40 -07:00
|
|
|
|
if (ImGui::BeginChild("##inspect_gear", ImVec2(0, 0), false)) {
|
2026-03-12 04:24:37 -07:00
|
|
|
|
constexpr float kIconSz = 28.0f;
|
2026-03-12 02:52:40 -07:00
|
|
|
|
for (int s = 0; s < 19; ++s) {
|
|
|
|
|
|
uint32_t entry = result->itemEntries[s];
|
|
|
|
|
|
if (entry == 0) continue;
|
|
|
|
|
|
|
|
|
|
|
|
const game::ItemQueryResponseData* info = gameHandler.getItemInfo(entry);
|
|
|
|
|
|
if (!info) {
|
|
|
|
|
|
gameHandler.ensureItemInfo(entry);
|
2026-03-12 04:24:37 -07:00
|
|
|
|
ImGui::PushID(s);
|
2026-03-12 02:52:40 -07:00
|
|
|
|
ImGui::TextDisabled("[%s] (loading…)", kSlotNames[s]);
|
2026-03-12 04:24:37 -07:00
|
|
|
|
ImGui::PopID();
|
2026-03-12 02:52:40 -07:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 04:24:37 -07:00
|
|
|
|
ImGui::PushID(s);
|
2026-03-12 02:52:40 -07:00
|
|
|
|
auto qColor = InventoryScreen::getQualityColor(
|
|
|
|
|
|
static_cast<game::ItemQuality>(info->quality));
|
2026-03-12 07:37:29 -07:00
|
|
|
|
uint16_t enchantId = result->enchantIds[s];
|
2026-03-12 04:24:37 -07:00
|
|
|
|
|
|
|
|
|
|
// Item icon
|
|
|
|
|
|
VkDescriptorSet iconTex = inventoryScreen.getItemIcon(info->displayInfoId);
|
|
|
|
|
|
if (iconTex) {
|
|
|
|
|
|
ImGui::Image((ImTextureID)(uintptr_t)iconTex, ImVec2(kIconSz, kIconSz),
|
|
|
|
|
|
ImVec2(0,0), ImVec2(1,1),
|
2026-03-27 14:00:15 -07:00
|
|
|
|
colors::kWhite, qColor);
|
2026-03-12 04:24:37 -07:00
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::GetWindowDrawList()->AddRectFilled(
|
|
|
|
|
|
ImGui::GetCursorScreenPos(),
|
|
|
|
|
|
ImVec2(ImGui::GetCursorScreenPos().x + kIconSz,
|
|
|
|
|
|
ImGui::GetCursorScreenPos().y + kIconSz),
|
|
|
|
|
|
IM_COL32(40, 40, 50, 200));
|
|
|
|
|
|
ImGui::Dummy(ImVec2(kIconSz, kIconSz));
|
|
|
|
|
|
}
|
|
|
|
|
|
bool hovered = ImGui::IsItemHovered();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (kIconSz - ImGui::GetTextLineHeight()) * 0.5f);
|
|
|
|
|
|
ImGui::BeginGroup();
|
|
|
|
|
|
ImGui::TextDisabled("%s", kSlotNames[s]);
|
2026-03-12 02:52:40 -07:00
|
|
|
|
ImGui::TextColored(qColor, "%s", info->name.c_str());
|
2026-03-12 07:37:29 -07:00
|
|
|
|
// Enchant indicator on the same row as the name
|
|
|
|
|
|
if (enchantId != 0) {
|
2026-03-12 12:32:19 -07:00
|
|
|
|
auto enchIt = s_enchantNames.find(enchantId);
|
|
|
|
|
|
const std::string& enchName = (enchIt != s_enchantNames.end())
|
|
|
|
|
|
? enchIt->second : std::string{};
|
2026-03-12 07:37:29 -07:00
|
|
|
|
ImGui::SameLine();
|
2026-03-12 12:32:19 -07:00
|
|
|
|
if (!enchName.empty()) {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 0.85f, 1.0f, 1.0f),
|
|
|
|
|
|
"\xe2\x9c\xa6 %s", enchName.c_str()); // UTF-8 ✦
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.6f, 0.85f, 1.0f, 1.0f), "\xe2\x9c\xa6");
|
|
|
|
|
|
if (ImGui::IsItemHovered())
|
|
|
|
|
|
ImGui::SetTooltip("Enchanted (ID %u)", static_cast<unsigned>(enchantId));
|
|
|
|
|
|
}
|
2026-03-12 07:37:29 -07:00
|
|
|
|
}
|
2026-03-12 04:24:37 -07:00
|
|
|
|
ImGui::EndGroup();
|
|
|
|
|
|
hovered = hovered || ImGui::IsItemHovered();
|
|
|
|
|
|
|
|
|
|
|
|
if (hovered && info->valid) {
|
|
|
|
|
|
inventoryScreen.renderItemTooltip(*info);
|
|
|
|
|
|
} else if (hovered) {
|
|
|
|
|
|
ImGui::SetTooltip("%s", info->name.c_str());
|
2026-03-12 02:52:40 -07:00
|
|
|
|
}
|
2026-03-12 04:24:37 -07:00
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
ImGui::Spacing();
|
2026-03-12 02:52:40 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 21:27:02 -07:00
|
|
|
|
// Arena teams (WotLK — from MSG_INSPECT_ARENA_TEAMS)
|
|
|
|
|
|
if (!result->arenaTeams.empty()) {
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(1.0f, 0.75f, 0.2f, 1.0f), "Arena Teams");
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
for (const auto& team : result->arenaTeams) {
|
|
|
|
|
|
const char* bracket = (team.type == 2) ? "2v2"
|
|
|
|
|
|
: (team.type == 3) ? "3v3"
|
|
|
|
|
|
: (team.type == 5) ? "5v5" : "?v?";
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.9f, 0.9f, 0.9f, 1.0f),
|
|
|
|
|
|
"[%s] %s", bracket, team.name.c_str());
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextColored(ImVec4(0.4f, 0.85f, 1.0f, 1.0f),
|
|
|
|
|
|
" Rating: %u", team.personalRating);
|
|
|
|
|
|
if (team.weekGames > 0 || team.seasonGames > 0) {
|
|
|
|
|
|
ImGui::TextDisabled(" Week: %u/%u Season: %u/%u",
|
|
|
|
|
|
team.weekWins, team.weekGames,
|
|
|
|
|
|
team.seasonWins, team.seasonGames);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 02:52:40 -07:00
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 20:23:36 -07:00
|
|
|
|
// ─── Titles Window ────────────────────────────────────────────────────────────
|
|
|
|
|
|
void GameScreen::renderTitlesWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!showTitlesWindow_) return;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(320, 400), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(240, 170), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
|
|
|
|
|
|
if (!ImGui::Begin("Titles", &showTitlesWindow_)) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const auto& knownBits = gameHandler.getKnownTitleBits();
|
|
|
|
|
|
const int32_t chosen = gameHandler.getChosenTitleBit();
|
|
|
|
|
|
|
|
|
|
|
|
if (knownBits.empty()) {
|
|
|
|
|
|
ImGui::TextDisabled("No titles earned yet.");
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TextUnformatted("Select a title to display:");
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
// "No Title" option
|
|
|
|
|
|
bool noTitle = (chosen < 0);
|
|
|
|
|
|
if (ImGui::Selectable("(No Title)", noTitle)) {
|
|
|
|
|
|
if (!noTitle) gameHandler.sendSetTitle(-1);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (noTitle) {
|
|
|
|
|
|
ImGui::SameLine();
|
refactor: add 6 color constants, replace 61 inline literals, remove const_cast
- Add kBrightGold, kPaleRed, kBrightRed, kLightBlue, kManaBlue, kCyan to ui_colors.hpp
- Replace 61 inline ImVec4 color literals across game_screen, inventory_screen,
talent_screen, and world_map with named constants
- Remove const_cast in character_renderer render loop by using non-const iteration
2026-03-27 10:08:30 -07:00
|
|
|
|
ImGui::TextColored(colors::kBrightGold, "<-- active");
|
2026-03-12 20:23:36 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
|
|
|
|
|
|
// Sort known bits for stable display order
|
|
|
|
|
|
std::vector<uint32_t> sortedBits(knownBits.begin(), knownBits.end());
|
|
|
|
|
|
std::sort(sortedBits.begin(), sortedBits.end());
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::BeginChild("##titlelist", ImVec2(0, 0), false);
|
|
|
|
|
|
for (uint32_t bit : sortedBits) {
|
|
|
|
|
|
const std::string title = gameHandler.getFormattedTitle(bit);
|
|
|
|
|
|
const std::string display = title.empty()
|
|
|
|
|
|
? ("Title #" + std::to_string(bit)) : title;
|
|
|
|
|
|
|
|
|
|
|
|
bool isActive = (chosen >= 0 && static_cast<uint32_t>(chosen) == bit);
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(bit));
|
|
|
|
|
|
|
|
|
|
|
|
if (isActive) {
|
refactor: add 6 color constants, replace 61 inline literals, remove const_cast
- Add kBrightGold, kPaleRed, kBrightRed, kLightBlue, kManaBlue, kCyan to ui_colors.hpp
- Replace 61 inline ImVec4 color literals across game_screen, inventory_screen,
talent_screen, and world_map with named constants
- Remove const_cast in character_renderer render loop by using non-const iteration
2026-03-27 10:08:30 -07:00
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, colors::kBrightGold);
|
2026-03-12 20:23:36 -07:00
|
|
|
|
}
|
|
|
|
|
|
if (ImGui::Selectable(display.c_str(), isActive)) {
|
|
|
|
|
|
if (!isActive) gameHandler.sendSetTitle(static_cast<int32_t>(bit));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isActive) {
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
ImGui::TextDisabled("<-- active");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-12 20:28:03 -07:00
|
|
|
|
// ─── Equipment Set Manager Window ─────────────────────────────────────────────
|
|
|
|
|
|
void GameScreen::renderEquipSetWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!showEquipSetWindow_) return;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(280, 320), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(260, 180), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
|
|
|
|
|
|
if (!ImGui::Begin("Equipment Sets##equipsets", &showEquipSetWindow_)) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const auto& sets = gameHandler.getEquipmentSets();
|
|
|
|
|
|
|
|
|
|
|
|
if (sets.empty()) {
|
|
|
|
|
|
ImGui::TextDisabled("No equipment sets saved.");
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::TextWrapped("Create equipment sets in-game using the default WoW equipment manager (Shift+click the Equipment Sets button).");
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::TextUnformatted("Click a set to equip it:");
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::BeginChild("##equipsetlist", ImVec2(0, 0), false);
|
|
|
|
|
|
for (const auto& set : sets) {
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(set.setId));
|
|
|
|
|
|
|
|
|
|
|
|
// Icon placeholder (use a coloured square if no icon texture available)
|
|
|
|
|
|
ImVec2 iconSize(32.0f, 32.0f);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.25f, 0.20f, 0.10f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.40f, 0.30f, 0.15f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.60f, 0.45f, 0.20f, 1.0f));
|
|
|
|
|
|
if (ImGui::Button("##icon", iconSize)) {
|
|
|
|
|
|
gameHandler.useEquipmentSet(set.setId);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(3);
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::SetTooltip("Equip set: %s", set.name.c_str());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
|
|
|
|
|
|
|
// Name and equip button
|
|
|
|
|
|
ImGui::BeginGroup();
|
|
|
|
|
|
ImGui::TextUnformatted(set.name.c_str());
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.20f, 0.35f, 0.15f, 1.0f));
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.30f, 0.50f, 0.22f, 1.0f));
|
|
|
|
|
|
if (ImGui::SmallButton("Equip")) {
|
|
|
|
|
|
gameHandler.useEquipmentSet(set.setId);
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::PopStyleColor(2);
|
|
|
|
|
|
ImGui::EndGroup();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-17 20:46:41 -07:00
|
|
|
|
void GameScreen::renderSkillsWindow(game::GameHandler& gameHandler) {
|
|
|
|
|
|
if (!showSkillsWindow_) return;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(380, 480), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(220, 130), ImGuiCond_FirstUseEver);
|
|
|
|
|
|
|
|
|
|
|
|
if (!ImGui::Begin("Skills & Professions", &showSkillsWindow_)) {
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const auto& skills = gameHandler.getPlayerSkills();
|
|
|
|
|
|
if (skills.empty()) {
|
|
|
|
|
|
ImGui::TextDisabled("No skill data received yet.");
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Organise skills by category
|
|
|
|
|
|
// WoW SkillLine.dbc categories: 6=Weapon, 7=Class, 8=Armor, 9=Secondary, 11=Professions, others=Misc
|
|
|
|
|
|
struct SkillEntry {
|
|
|
|
|
|
uint32_t skillId;
|
|
|
|
|
|
const game::PlayerSkill* skill;
|
|
|
|
|
|
};
|
|
|
|
|
|
std::map<uint32_t, std::vector<SkillEntry>> byCategory;
|
|
|
|
|
|
for (const auto& [id, sk] : skills) {
|
|
|
|
|
|
uint32_t cat = gameHandler.getSkillCategory(id);
|
|
|
|
|
|
byCategory[cat].push_back({id, &sk});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-27 14:47:58 -07:00
|
|
|
|
static constexpr struct { uint32_t cat; const char* label; } kCatOrder[] = {
|
2026-03-17 20:46:41 -07:00
|
|
|
|
{11, "Professions"},
|
|
|
|
|
|
{ 9, "Secondary Skills"},
|
|
|
|
|
|
{ 7, "Class Skills"},
|
|
|
|
|
|
{ 6, "Weapon Skills"},
|
|
|
|
|
|
{ 8, "Armor"},
|
|
|
|
|
|
{ 5, "Languages"},
|
|
|
|
|
|
{ 0, "Other"},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Collect handled categories to fall back to "Other" for unknowns
|
|
|
|
|
|
static const uint32_t kKnownCats[] = {11, 9, 7, 6, 8, 5};
|
|
|
|
|
|
|
|
|
|
|
|
// Redirect unknown categories into bucket 0
|
|
|
|
|
|
for (auto& [cat, vec] : byCategory) {
|
|
|
|
|
|
bool known = false;
|
|
|
|
|
|
for (uint32_t kc : kKnownCats) if (cat == kc) { known = true; break; }
|
|
|
|
|
|
if (!known && cat != 0) {
|
|
|
|
|
|
auto& other = byCategory[0];
|
|
|
|
|
|
other.insert(other.end(), vec.begin(), vec.end());
|
|
|
|
|
|
vec.clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::BeginChild("##skillscroll", ImVec2(0, 0), false);
|
|
|
|
|
|
|
|
|
|
|
|
for (const auto& [cat, label] : kCatOrder) {
|
|
|
|
|
|
auto it = byCategory.find(cat);
|
|
|
|
|
|
if (it == byCategory.end() || it->second.empty()) continue;
|
|
|
|
|
|
|
|
|
|
|
|
auto& entries = it->second;
|
|
|
|
|
|
// Sort alphabetically within each category
|
|
|
|
|
|
std::sort(entries.begin(), entries.end(), [&](const SkillEntry& a, const SkillEntry& b) {
|
|
|
|
|
|
return gameHandler.getSkillName(a.skillId) < gameHandler.getSkillName(b.skillId);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::CollapsingHeader(label, ImGuiTreeNodeFlags_DefaultOpen)) {
|
|
|
|
|
|
for (const auto& e : entries) {
|
|
|
|
|
|
const std::string& name = gameHandler.getSkillName(e.skillId);
|
|
|
|
|
|
const char* displayName = name.empty() ? "Unknown" : name.c_str();
|
|
|
|
|
|
uint16_t val = e.skill->effectiveValue();
|
|
|
|
|
|
uint16_t maxVal = e.skill->maxValue;
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PushID(static_cast<int>(e.skillId));
|
|
|
|
|
|
|
|
|
|
|
|
// Name column
|
|
|
|
|
|
ImGui::TextUnformatted(displayName);
|
|
|
|
|
|
ImGui::SameLine(170.0f);
|
|
|
|
|
|
|
|
|
|
|
|
// Progress bar
|
|
|
|
|
|
float fraction = (maxVal > 0) ? static_cast<float>(val) / static_cast<float>(maxVal) : 0.0f;
|
|
|
|
|
|
char overlay[32];
|
|
|
|
|
|
snprintf(overlay, sizeof(overlay), "%u / %u", val, maxVal);
|
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.20f, 0.55f, 0.20f, 1.0f));
|
|
|
|
|
|
ImGui::ProgressBar(fraction, ImVec2(160.0f, 14.0f), overlay);
|
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
|
|
|
|
|
|
|
if (ImGui::IsItemHovered()) {
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
|
ImGui::Text("%s", displayName);
|
|
|
|
|
|
ImGui::Separator();
|
|
|
|
|
|
ImGui::Text("Base: %u", e.skill->value);
|
|
|
|
|
|
if (e.skill->bonusPerm > 0)
|
|
|
|
|
|
ImGui::Text("Permanent bonus: +%u", e.skill->bonusPerm);
|
|
|
|
|
|
if (e.skill->bonusTemp > 0)
|
|
|
|
|
|
ImGui::Text("Temporary bonus: +%u", e.skill->bonusTemp);
|
|
|
|
|
|
ImGui::Text("Max: %u", maxVal);
|
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ImGui::EndChild();
|
|
|
|
|
|
ImGui::End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
|
}} // namespace wowee::ui
|